diff options
Diffstat (limited to 'Emby.Server.Implementations')
244 files changed, 13479 insertions, 17319 deletions
diff --git a/Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs b/Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs index 4e448ac64..079d0af0a 100644 --- a/Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs +++ b/Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs @@ -19,6 +19,11 @@ using System.Linq; using System.Text; using MediaBrowser.Model.Globalization; using MediaBrowser.Model.Extensions; +using MediaBrowser.Model.Notifications; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Dto; +using MediaBrowser.Controller.Devices; +using MediaBrowser.Controller.Authentication; namespace Emby.Server.Implementations.Activity { @@ -26,7 +31,6 @@ namespace Emby.Server.Implementations.Activity { private readonly IInstallationManager _installationManager; - //private readonly ILogManager _logManager; //private readonly ILogger _logger; private readonly ISessionManager _sessionManager; private readonly ITaskManager _taskManager; @@ -38,10 +42,10 @@ namespace Emby.Server.Implementations.Activity private readonly IUserManager _userManager; private readonly IServerConfigurationManager _config; private readonly IServerApplicationHost _appHost; + private readonly IDeviceManager _deviceManager; - public ActivityLogEntryPoint(ISessionManager sessionManager, ITaskManager taskManager, IActivityManager activityManager, ILocalizationManager localization, IInstallationManager installationManager, ILibraryManager libraryManager, ISubtitleManager subManager, IUserManager userManager, IServerConfigurationManager config, IServerApplicationHost appHost) + public ActivityLogEntryPoint(ISessionManager sessionManager, IDeviceManager deviceManager, ITaskManager taskManager, IActivityManager activityManager, ILocalizationManager localization, IInstallationManager installationManager, ILibraryManager libraryManager, ISubtitleManager subManager, IUserManager userManager, IServerConfigurationManager config, IServerApplicationHost appHost) { - //_logger = _logManager.GetLogger("ActivityLogEntryPoint"); _sessionManager = sessionManager; _taskManager = taskManager; _activityManager = activityManager; @@ -51,21 +55,18 @@ namespace Emby.Server.Implementations.Activity _subManager = subManager; _userManager = userManager; _config = config; - //_logManager = logManager; _appHost = appHost; + _deviceManager = deviceManager; } public void Run() { - //_taskManager.TaskExecuting += _taskManager_TaskExecuting; - //_taskManager.TaskCompleted += _taskManager_TaskCompleted; + _taskManager.TaskCompleted += _taskManager_TaskCompleted; - //_installationManager.PluginInstalled += _installationManager_PluginInstalled; - //_installationManager.PluginUninstalled += _installationManager_PluginUninstalled; - //_installationManager.PluginUpdated += _installationManager_PluginUpdated; - - //_libraryManager.ItemAdded += _libraryManager_ItemAdded; - //_libraryManager.ItemRemoved += _libraryManager_ItemRemoved; + _installationManager.PluginInstalled += _installationManager_PluginInstalled; + _installationManager.PluginUninstalled += _installationManager_PluginUninstalled; + _installationManager.PluginUpdated += _installationManager_PluginUpdated; + _installationManager.PackageInstallationFailed += _installationManager_PackageInstallationFailed; _sessionManager.SessionStarted += _sessionManager_SessionStarted; _sessionManager.AuthenticationFailed += _sessionManager_AuthenticationFailed; @@ -81,24 +82,33 @@ namespace Emby.Server.Implementations.Activity _userManager.UserCreated += _userManager_UserCreated; _userManager.UserPasswordChanged += _userManager_UserPasswordChanged; _userManager.UserDeleted += _userManager_UserDeleted; - _userManager.UserConfigurationUpdated += _userManager_UserConfigurationUpdated; + _userManager.UserPolicyUpdated += _userManager_UserPolicyUpdated; _userManager.UserLockedOut += _userManager_UserLockedOut; //_config.ConfigurationUpdated += _config_ConfigurationUpdated; //_config.NamedConfigurationUpdated += _config_NamedConfigurationUpdated; - //_logManager.LoggerLoaded += _logManager_LoggerLoaded; + _deviceManager.CameraImageUploaded += _deviceManager_CameraImageUploaded; _appHost.ApplicationUpdated += _appHost_ApplicationUpdated; } + void _deviceManager_CameraImageUploaded(object sender, GenericEventArgs<CameraImageUploadInfo> e) + { + CreateLogEntry(new ActivityLogEntry + { + Name = string.Format(_localization.GetLocalizedString("CameraImageUploadedFrom"), e.Argument.Device.Name), + Type = NotificationType.CameraImageUploaded.ToString() + }); + } + void _userManager_UserLockedOut(object sender, GenericEventArgs<User> e) { CreateLogEntry(new ActivityLogEntry { Name = string.Format(_localization.GetLocalizedString("UserLockedOutWithName"), e.Argument.Name), - Type = "UserLockedOut", - UserId = e.Argument.Id.ToString("N") + Type = NotificationType.UserLockedOut.ToString(), + UserId = e.Argument.Id }); } @@ -106,11 +116,10 @@ namespace Emby.Server.Implementations.Activity { CreateLogEntry(new ActivityLogEntry { - Name = string.Format(_localization.GetLocalizedString("SubtitleDownloadFailureForItem"), Notifications.Notifications.GetItemName(e.Item)), + Name = string.Format(_localization.GetLocalizedString("SubtitleDownloadFailureFromForItem"), e.Provider, Notifications.Notifications.GetItemName(e.Item)), Type = "SubtitleDownloadFailure", ItemId = e.Item.Id.ToString("N"), - ShortOverview = string.Format(_localization.GetLocalizedString("ProviderValue"), e.Provider), - Overview = LogHelper.GetLogMessage(e.Exception).ToString() + ShortOverview = e.Exception.Message }); } @@ -139,10 +148,9 @@ namespace Emby.Server.Implementations.Activity CreateLogEntry(new ActivityLogEntry { - Name = string.Format(_localization.GetLocalizedString("UserStoppedPlayingItemWithValues"), user.Name, Notifications.Notifications.GetItemName(item)), - Type = "PlaybackStopped", - ShortOverview = string.Format(_localization.GetLocalizedString("AppDeviceValues"), e.ClientName, e.DeviceName), - UserId = user.Id.ToString("N") + Name = string.Format(_localization.GetLocalizedString("UserStoppedPlayingItemWithValues"), user.Name, GetItemName(item), e.DeviceName), + Type = GetPlaybackStoppedNotificationType(item.MediaType), + UserId = user.Id }); } @@ -171,19 +179,71 @@ namespace Emby.Server.Implementations.Activity CreateLogEntry(new ActivityLogEntry { - Name = string.Format(_localization.GetLocalizedString("UserStartedPlayingItemWithValues"), user.Name, Notifications.Notifications.GetItemName(item)), - Type = "PlaybackStart", - ShortOverview = string.Format(_localization.GetLocalizedString("AppDeviceValues"), e.ClientName, e.DeviceName), - UserId = user.Id.ToString("N") + Name = string.Format(_localization.GetLocalizedString("UserStartedPlayingItemWithValues"), user.Name, GetItemName(item), e.DeviceName), + Type = GetPlaybackNotificationType(item.MediaType), + UserId = user.Id }); } + private static string GetItemName(BaseItemDto item) + { + var name = item.Name; + + if (!string.IsNullOrEmpty(item.SeriesName)) + { + name = item.SeriesName + " - " + name; + } + + if (item.Artists != null && item.Artists.Length > 0) + { + name = item.Artists[0] + " - " + name; + } + + return name; + } + + private string GetPlaybackNotificationType(string mediaType) + { + if (string.Equals(mediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase)) + { + return NotificationType.AudioPlayback.ToString(); + } + if (string.Equals(mediaType, MediaType.Game, StringComparison.OrdinalIgnoreCase)) + { + return NotificationType.GamePlayback.ToString(); + } + if (string.Equals(mediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase)) + { + return NotificationType.VideoPlayback.ToString(); + } + + return null; + } + + private string GetPlaybackStoppedNotificationType(string mediaType) + { + if (string.Equals(mediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase)) + { + return NotificationType.AudioPlaybackStopped.ToString(); + } + if (string.Equals(mediaType, MediaType.Game, StringComparison.OrdinalIgnoreCase)) + { + return NotificationType.GamePlaybackStopped.ToString(); + } + if (string.Equals(mediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase)) + { + return NotificationType.VideoPlaybackStopped.ToString(); + } + + return null; + } + void _sessionManager_SessionEnded(object sender, SessionEventArgs e) { string name; var session = e.SessionInfo; - if (string.IsNullOrWhiteSpace(session.UserName)) + if (string.IsNullOrEmpty(session.UserName)) { name = string.Format(_localization.GetLocalizedString("DeviceOfflineWithName"), session.DeviceName); @@ -200,17 +260,20 @@ namespace Emby.Server.Implementations.Activity Name = name, Type = "SessionEnded", ShortOverview = string.Format(_localization.GetLocalizedString("LabelIpAddressValue"), session.RemoteEndPoint), - UserId = session.UserId.HasValue ? session.UserId.Value.ToString("N") : null + UserId = session.UserId }); } - void _sessionManager_AuthenticationSucceeded(object sender, GenericEventArgs<AuthenticationRequest> e) + void _sessionManager_AuthenticationSucceeded(object sender, GenericEventArgs<AuthenticationResult> e) { + var user = e.Argument.User; + CreateLogEntry(new ActivityLogEntry { - Name = string.Format(_localization.GetLocalizedString("AuthenticationSucceededWithUserName"), e.Argument.Username), + Name = string.Format(_localization.GetLocalizedString("AuthenticationSucceededWithUserName"), user.Name), Type = "AuthenticationSucceeded", - ShortOverview = string.Format(_localization.GetLocalizedString("LabelIpAddressValue"), e.Argument.RemoteEndPoint) + ShortOverview = string.Format(_localization.GetLocalizedString("LabelIpAddressValue"), e.Argument.SessionInfo.RemoteEndPoint), + UserId = user.Id }); } @@ -229,9 +292,8 @@ namespace Emby.Server.Implementations.Activity { CreateLogEntry(new ActivityLogEntry { - Name = _localization.GetLocalizedString("MessageApplicationUpdated"), - Type = "ApplicationUpdated", - ShortOverview = string.Format(_localization.GetLocalizedString("VersionNumber"), e.Argument.versionStr), + Name = string.Format(_localization.GetLocalizedString("MessageApplicationUpdatedTo"), e.Argument.versionStr), + Type = NotificationType.ApplicationUpdateInstalled.ToString(), Overview = e.Argument.description }); } @@ -254,13 +316,13 @@ namespace Emby.Server.Implementations.Activity }); } - void _userManager_UserConfigurationUpdated(object sender, GenericEventArgs<User> e) + void _userManager_UserPolicyUpdated(object sender, GenericEventArgs<User> e) { CreateLogEntry(new ActivityLogEntry { - Name = string.Format(_localization.GetLocalizedString("UserConfigurationUpdatedWithName"), e.Argument.Name), - Type = "UserConfigurationUpdated", - UserId = e.Argument.Id.ToString("N") + Name = string.Format(_localization.GetLocalizedString("UserPolicyUpdatedWithName"), e.Argument.Name), + Type = "UserPolicyUpdated", + UserId = e.Argument.Id }); } @@ -279,7 +341,7 @@ namespace Emby.Server.Implementations.Activity { Name = string.Format(_localization.GetLocalizedString("UserPasswordChangedWithName"), e.Argument.Name), Type = "UserPasswordChanged", - UserId = e.Argument.Id.ToString("N") + UserId = e.Argument.Id }); } @@ -289,7 +351,7 @@ namespace Emby.Server.Implementations.Activity { Name = string.Format(_localization.GetLocalizedString("UserCreatedWithName"), e.Argument.Name), Type = "UserCreated", - UserId = e.Argument.Id.ToString("N") + UserId = e.Argument.Id }); } @@ -309,7 +371,7 @@ namespace Emby.Server.Implementations.Activity string name; var session = e.SessionInfo; - if (string.IsNullOrWhiteSpace(session.UserName)) + if (string.IsNullOrEmpty(session.UserName)) { name = string.Format(_localization.GetLocalizedString("DeviceOnlineWithName"), session.DeviceName); @@ -326,36 +388,7 @@ namespace Emby.Server.Implementations.Activity Name = name, Type = "SessionStarted", ShortOverview = string.Format(_localization.GetLocalizedString("LabelIpAddressValue"), session.RemoteEndPoint), - UserId = session.UserId.HasValue ? session.UserId.Value.ToString("N") : null - }); - } - - void _libraryManager_ItemRemoved(object sender, ItemChangeEventArgs e) - { - if (e.Item.SourceType != SourceType.Library) - { - return; - } - - CreateLogEntry(new ActivityLogEntry - { - Name = string.Format(_localization.GetLocalizedString("ItemRemovedWithName"), Notifications.Notifications.GetItemName(e.Item)), - Type = "ItemRemoved" - }); - } - - void _libraryManager_ItemAdded(object sender, ItemChangeEventArgs e) - { - if (e.Item.SourceType != SourceType.Library) - { - return; - } - - CreateLogEntry(new ActivityLogEntry - { - Name = string.Format(_localization.GetLocalizedString("ItemAddedWithName"), Notifications.Notifications.GetItemName(e.Item)), - Type = "ItemAdded", - ItemId = e.Item.Id.ToString("N") + UserId = session.UserId }); } @@ -364,7 +397,7 @@ namespace Emby.Server.Implementations.Activity CreateLogEntry(new ActivityLogEntry { Name = string.Format(_localization.GetLocalizedString("PluginUpdatedWithName"), e.Argument.Item1.Name), - Type = "PluginUpdated", + Type = NotificationType.PluginUpdateInstalled.ToString(), ShortOverview = string.Format(_localization.GetLocalizedString("VersionNumber"), e.Argument.Item2.versionStr), Overview = e.Argument.Item2.description }); @@ -375,7 +408,7 @@ namespace Emby.Server.Implementations.Activity CreateLogEntry(new ActivityLogEntry { Name = string.Format(_localization.GetLocalizedString("PluginUninstalledWithName"), e.Argument.Name), - Type = "PluginUninstalled" + Type = NotificationType.PluginUninstalled.ToString() }); } @@ -384,25 +417,21 @@ namespace Emby.Server.Implementations.Activity CreateLogEntry(new ActivityLogEntry { Name = string.Format(_localization.GetLocalizedString("PluginInstalledWithName"), e.Argument.name), - Type = "PluginInstalled", + Type = NotificationType.PluginInstalled.ToString(), ShortOverview = string.Format(_localization.GetLocalizedString("VersionNumber"), e.Argument.versionStr) }); } - void _taskManager_TaskExecuting(object sender, GenericEventArgs<IScheduledTaskWorker> e) + void _installationManager_PackageInstallationFailed(object sender, InstallationFailedEventArgs e) { - var task = e.Argument; - - var activityTask = task.ScheduledTask as IConfigurableScheduledTask; - if (activityTask != null && !activityTask.IsLogged) - { - return; - } + var installationInfo = e.InstallationInfo; CreateLogEntry(new ActivityLogEntry { - Name = string.Format(_localization.GetLocalizedString("ScheduledTaskStartedWithName"), task.Name), - Type = "ScheduledTaskStarted" + Name = string.Format(_localization.GetLocalizedString("NameInstallFailed"), installationInfo.Name), + Type = NotificationType.InstallationFailed.ToString(), + ShortOverview = string.Format(_localization.GetLocalizedString("VersionNumber"), installationInfo.Version), + Overview = e.Exception.Message }); } @@ -424,11 +453,11 @@ namespace Emby.Server.Implementations.Activity { var vals = new List<string>(); - if (!string.IsNullOrWhiteSpace(e.Result.ErrorMessage)) + if (!string.IsNullOrEmpty(e.Result.ErrorMessage)) { vals.Add(e.Result.ErrorMessage); } - if (!string.IsNullOrWhiteSpace(e.Result.LongErrorMessage)) + if (!string.IsNullOrEmpty(e.Result.LongErrorMessage)) { vals.Add(e.Result.LongErrorMessage); } @@ -436,7 +465,7 @@ namespace Emby.Server.Implementations.Activity CreateLogEntry(new ActivityLogEntry { Name = string.Format(_localization.GetLocalizedString("ScheduledTaskFailedWithName"), task.Name), - Type = "ScheduledTaskFailed", + Type = NotificationType.TaskFailed.ToString(), Overview = string.Join(Environment.NewLine, vals.ToArray(vals.Count)), ShortOverview = runningTime, Severity = LogSeverity.Error @@ -458,15 +487,12 @@ namespace Emby.Server.Implementations.Activity public void Dispose() { - _taskManager.TaskExecuting -= _taskManager_TaskExecuting; _taskManager.TaskCompleted -= _taskManager_TaskCompleted; _installationManager.PluginInstalled -= _installationManager_PluginInstalled; _installationManager.PluginUninstalled -= _installationManager_PluginUninstalled; _installationManager.PluginUpdated -= _installationManager_PluginUpdated; - - _libraryManager.ItemAdded -= _libraryManager_ItemAdded; - _libraryManager.ItemRemoved -= _libraryManager_ItemRemoved; + _installationManager.PackageInstallationFailed -= _installationManager_PackageInstallationFailed; _sessionManager.SessionStarted -= _sessionManager_SessionStarted; _sessionManager.AuthenticationFailed -= _sessionManager_AuthenticationFailed; @@ -482,16 +508,15 @@ namespace Emby.Server.Implementations.Activity _userManager.UserCreated -= _userManager_UserCreated; _userManager.UserPasswordChanged -= _userManager_UserPasswordChanged; _userManager.UserDeleted -= _userManager_UserDeleted; - _userManager.UserConfigurationUpdated -= _userManager_UserConfigurationUpdated; + _userManager.UserPolicyUpdated -= _userManager_UserPolicyUpdated; _userManager.UserLockedOut -= _userManager_UserLockedOut; _config.ConfigurationUpdated -= _config_ConfigurationUpdated; _config.NamedConfigurationUpdated -= _config_NamedConfigurationUpdated; - //_logManager.LoggerLoaded -= _logManager_LoggerLoaded; + _deviceManager.CameraImageUploaded -= _deviceManager_CameraImageUploaded; _appHost.ApplicationUpdated -= _appHost_ApplicationUpdated; - GC.SuppressFinalize(this); } /// <summary> diff --git a/Emby.Server.Implementations/Activity/ActivityManager.cs b/Emby.Server.Implementations/Activity/ActivityManager.cs index 9a3f1ae47..047bebf23 100644 --- a/Emby.Server.Implementations/Activity/ActivityManager.cs +++ b/Emby.Server.Implementations/Activity/ActivityManager.cs @@ -6,7 +6,6 @@ using MediaBrowser.Model.Logging; using MediaBrowser.Model.Querying; using System; using System.Linq; -using System.Threading.Tasks; namespace Emby.Server.Implementations.Activity { @@ -27,7 +26,6 @@ namespace Emby.Server.Implementations.Activity public void Create(ActivityLogEntry entry) { - entry.Id = Guid.NewGuid().ToString("N"); entry.Date = DateTime.UtcNow; _repo.Create(entry); @@ -35,11 +33,11 @@ namespace Emby.Server.Implementations.Activity EventHelper.FireEventIfNotNull(EntryCreated, this, new GenericEventArgs<ActivityLogEntry>(entry), _logger); } - public QueryResult<ActivityLogEntry> GetActivityLogEntries(DateTime? minDate, int? startIndex, int? limit) + public QueryResult<ActivityLogEntry> GetActivityLogEntries(DateTime? minDate, bool? hasUserId, int? startIndex, int? limit) { - var result = _repo.GetActivityLogEntries(minDate, startIndex, limit); + var result = _repo.GetActivityLogEntries(minDate, hasUserId, startIndex, limit); - foreach (var item in result.Items.Where(i => !string.IsNullOrWhiteSpace(i.UserId))) + foreach (var item in result.Items.Where(i => !i.UserId.Equals(Guid.Empty))) { var user = _userManager.GetUserById(item.UserId); @@ -52,5 +50,10 @@ namespace Emby.Server.Implementations.Activity return result; } + + public QueryResult<ActivityLogEntry> GetActivityLogEntries(DateTime? minDate, int? startIndex, int? limit) + { + return GetActivityLogEntries(minDate, null, startIndex, limit); + } } } diff --git a/Emby.Server.Implementations/Activity/ActivityRepository.cs b/Emby.Server.Implementations/Activity/ActivityRepository.cs index 6293cc69f..ce9f460ff 100644 --- a/Emby.Server.Implementations/Activity/ActivityRepository.cs +++ b/Emby.Server.Implementations/Activity/ActivityRepository.cs @@ -48,20 +48,76 @@ namespace Emby.Server.Implementations.Activity { RunDefaultInitialization(connection); - string[] queries = { - "create table if not exists ActivityLogEntries (Id GUID PRIMARY KEY NOT NULL, Name TEXT NOT NULL, Overview TEXT, ShortOverview TEXT, Type TEXT NOT NULL, ItemId TEXT, UserId TEXT, DateCreated DATETIME NOT NULL, LogSeverity TEXT NOT NULL)", - "create index if not exists idx_ActivityLogEntries on ActivityLogEntries(Id)" - }; + connection.RunQueries(new[] + { + "create table if not exists ActivityLog (Id INTEGER PRIMARY KEY, Name TEXT NOT NULL, Overview TEXT, ShortOverview TEXT, Type TEXT NOT NULL, ItemId TEXT, UserId TEXT, DateCreated DATETIME NOT NULL, LogSeverity TEXT NOT NULL)", + "drop index if exists idx_ActivityLogEntries" + }); - connection.RunQueries(queries); + TryMigrate(connection); } } - private const string BaseActivitySelectText = "select Id, Name, Overview, ShortOverview, Type, ItemId, UserId, DateCreated, LogSeverity from ActivityLogEntries"; + private void TryMigrate(ManagedConnection connection) + { + try + { + if (TableExists(connection, "ActivityLogEntries")) + { + connection.RunQueries(new[] + { + "INSERT INTO ActivityLog (Name, Overview, ShortOverview, Type, ItemId, UserId, DateCreated, LogSeverity) SELECT Name, Overview, ShortOverview, Type, ItemId, UserId, DateCreated, LogSeverity FROM ActivityLogEntries", + "drop table if exists ActivityLogEntries" + }); + } + } + catch (Exception ex) + { + Logger.ErrorException("Error migrating activity log database", ex); + } + } + + private const string BaseActivitySelectText = "select Id, Name, Overview, ShortOverview, Type, ItemId, UserId, DateCreated, LogSeverity from ActivityLog"; public void Create(ActivityLogEntry entry) { - Update(entry); + if (entry == null) + { + throw new ArgumentNullException("entry"); + } + + using (WriteLock.Write()) + { + using (var connection = CreateConnection()) + { + connection.RunInTransaction(db => + { + using (var statement = db.PrepareStatement("insert into ActivityLog (Name, Overview, ShortOverview, Type, ItemId, UserId, DateCreated, LogSeverity) values (@Name, @Overview, @ShortOverview, @Type, @ItemId, @UserId, @DateCreated, @LogSeverity)")) + { + statement.TryBind("@Name", entry.Name); + + statement.TryBind("@Overview", entry.Overview); + statement.TryBind("@ShortOverview", entry.ShortOverview); + statement.TryBind("@Type", entry.Type); + statement.TryBind("@ItemId", entry.ItemId); + + if (entry.UserId.Equals(Guid.Empty)) + { + statement.TryBindNull("@UserId"); + } + else + { + statement.TryBind("@UserId", entry.UserId.ToString("N")); + } + + statement.TryBind("@DateCreated", entry.Date.ToDateTimeParamValue()); + statement.TryBind("@LogSeverity", entry.Severity.ToString()); + + statement.MoveNext(); + } + }, TransactionMode); + } + } } public void Update(ActivityLogEntry entry) @@ -77,16 +133,25 @@ namespace Emby.Server.Implementations.Activity { connection.RunInTransaction(db => { - using (var statement = db.PrepareStatement("replace into ActivityLogEntries (Id, Name, Overview, ShortOverview, Type, ItemId, UserId, DateCreated, LogSeverity) values (@Id, @Name, @Overview, @ShortOverview, @Type, @ItemId, @UserId, @DateCreated, @LogSeverity)")) + using (var statement = db.PrepareStatement("Update ActivityLog set Name=@Name,Overview=@Overview,ShortOverview=@ShortOverview,Type=@Type,ItemId=@ItemId,UserId=@UserId,DateCreated=@DateCreated,LogSeverity=@LogSeverity where Id=@Id")) { - statement.TryBind("@Id", entry.Id.ToGuidBlob()); - statement.TryBind("@Name", entry.Name); + statement.TryBind("@Id", entry.Id); + statement.TryBind("@Name", entry.Name); statement.TryBind("@Overview", entry.Overview); statement.TryBind("@ShortOverview", entry.ShortOverview); statement.TryBind("@Type", entry.Type); statement.TryBind("@ItemId", entry.ItemId); - statement.TryBind("@UserId", entry.UserId); + + if (entry.UserId.Equals(Guid.Empty)) + { + statement.TryBindNull("@UserId"); + } + else + { + statement.TryBind("@UserId", entry.UserId.ToString("N")); + } + statement.TryBind("@DateCreated", entry.Date.ToDateTimeParamValue()); statement.TryBind("@LogSeverity", entry.Severity.ToString()); @@ -97,7 +162,7 @@ namespace Emby.Server.Implementations.Activity } } - public QueryResult<ActivityLogEntry> GetActivityLogEntries(DateTime? minDate, int? startIndex, int? limit) + public QueryResult<ActivityLogEntry> GetActivityLogEntries(DateTime? minDate, bool? hasUserId, int? startIndex, int? limit) { using (WriteLock.Read()) { @@ -110,6 +175,17 @@ namespace Emby.Server.Implementations.Activity { whereClauses.Add("DateCreated>=@DateCreated"); } + if (hasUserId.HasValue) + { + if (hasUserId.Value) + { + whereClauses.Add("UserId not null"); + } + else + { + whereClauses.Add("UserId is null"); + } + } var whereTextWithoutPaging = whereClauses.Count == 0 ? string.Empty : @@ -121,7 +197,7 @@ namespace Emby.Server.Implementations.Activity string.Empty : " where " + string.Join(" AND ", whereClauses.ToArray(whereClauses.Count)); - whereClauses.Add(string.Format("Id NOT IN (SELECT Id FROM ActivityLogEntries {0} ORDER BY DateCreated DESC LIMIT {1})", + whereClauses.Add(string.Format("Id NOT IN (SELECT Id FROM ActivityLog {0} ORDER BY DateCreated DESC LIMIT {1})", pagingWhereText, startIndex.Value.ToString(_usCulture))); } @@ -141,7 +217,7 @@ namespace Emby.Server.Implementations.Activity var statementTexts = new List<string>(); statementTexts.Add(commandText); - statementTexts.Add("select count (Id) from ActivityLogEntries" + whereTextWithoutPaging); + statementTexts.Add("select count (Id) from ActivityLog" + whereTextWithoutPaging); return connection.RunInTransaction(db => { @@ -187,7 +263,7 @@ namespace Emby.Server.Implementations.Activity var info = new ActivityLogEntry { - Id = reader[index].ReadGuidFromBlob().ToString("N") + Id = reader[index].ToInt64() }; index++; @@ -223,7 +299,7 @@ namespace Emby.Server.Implementations.Activity index++; if (reader[index].SQLiteType != SQLiteType.Null) { - info.UserId = reader[index].ToString(); + info.UserId = new Guid(reader[index].ToString()); } index++; diff --git a/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs b/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs index 1e63aa1a6..52e421374 100644 --- a/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs +++ b/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs @@ -49,6 +49,15 @@ namespace Emby.Server.Implementations.AppBase } } + private const string _virtualDataPath = "%AppDataPath%"; + public string VirtualDataPath + { + get + { + return _virtualDataPath; + } + } + /// <summary> /// Gets the image cache path. /// </summary> diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 26450c06c..3208c6a1f 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -1,12 +1,9 @@ using Emby.Common.Implementations.Serialization; +using Emby.Drawing; +using Emby.Photos; using Emby.Dlna; -using Emby.Dlna.ConnectionManager; -using Emby.Dlna.ContentDirectory; using Emby.Dlna.Main; -using Emby.Dlna.MediaReceiverRegistrar; using Emby.Dlna.Ssdp; -using Emby.Drawing; -using Emby.Photos; using Emby.Server.Implementations.Activity; using Emby.Server.Implementations.Archiving; using Emby.Server.Implementations.Channels; @@ -26,14 +23,13 @@ using Emby.Server.Implementations.LiveTv; using Emby.Server.Implementations.Localization; using Emby.Server.Implementations.MediaEncoder; using Emby.Server.Implementations.Net; -using Emby.Server.Implementations.Notifications; +using Emby.Notifications; using Emby.Server.Implementations.Playlists; using Emby.Server.Implementations.Reflection; using Emby.Server.Implementations.ScheduledTasks; using Emby.Server.Implementations.Security; using Emby.Server.Implementations.Serialization; using Emby.Server.Implementations.Session; -using Emby.Server.Implementations.Social; using Emby.Server.Implementations.Threading; using Emby.Server.Implementations.TV; using Emby.Server.Implementations.Updates; @@ -46,7 +42,7 @@ using MediaBrowser.Common.Events; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Common.Plugins; -using MediaBrowser.Common.Progress; +using MediaBrowser.Model.Extensions; using MediaBrowser.Common.Security; using MediaBrowser.Common.Updates; using MediaBrowser.Controller; @@ -60,6 +56,7 @@ using MediaBrowser.Controller.Dlna; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.IO; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.MediaEncoding; @@ -74,7 +71,6 @@ using MediaBrowser.Controller.Security; using MediaBrowser.Controller.Session; using MediaBrowser.Controller.Sorting; using MediaBrowser.Controller.Subtitles; -using MediaBrowser.Controller.Sync; using MediaBrowser.Controller.TV; using MediaBrowser.LocalMetadata.Savers; using MediaBrowser.MediaEncoding.BdInfo; @@ -93,7 +89,6 @@ using MediaBrowser.Model.News; using MediaBrowser.Model.Reflection; using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Services; -using MediaBrowser.Model.Social; using MediaBrowser.Model.System; using MediaBrowser.Model.Tasks; using MediaBrowser.Model.Text; @@ -105,7 +100,6 @@ using MediaBrowser.Providers.Manager; using MediaBrowser.Providers.Subtitles; using MediaBrowser.WebDashboard.Api; using MediaBrowser.XbmcMetadata.Providers; -using OpenSubtitlesHandler; using ServiceStack; using System; using System.Collections.Concurrent; @@ -122,13 +116,16 @@ using System.Threading; using System.Threading.Tasks; using StringExtensions = MediaBrowser.Controller.Extensions.StringExtensions; using X509Certificate = System.Security.Cryptography.X509Certificates.X509Certificate; +using MediaBrowser.Controller.Authentication; +using System.Diagnostics; +using ServiceStack.Text.Jsv; namespace Emby.Server.Implementations { /// <summary> /// Class CompositionRoot /// </summary> - public abstract class ApplicationHost : IServerApplicationHost, IDependencyContainer, IDisposable + public abstract class ApplicationHost : IServerApplicationHost, IDisposable { /// <summary> /// Gets a value indicating whether this instance can self restart. @@ -219,16 +216,10 @@ namespace Emby.Server.Implementations protected ServerApplicationPaths ApplicationPaths { get; set; } /// <summary> - /// Gets assemblies that failed to load - /// </summary> - /// <value>The failed assemblies.</value> - public List<string> FailedAssemblies { get; protected set; } - - /// <summary> /// Gets all concrete types. /// </summary> /// <value>All concrete types.</value> - public Type[] AllConcreteTypes { get; protected set; } + public Tuple<Type, string>[] AllConcreteTypes { get; protected set; } /// <summary> /// The disposable parts @@ -236,12 +227,6 @@ namespace Emby.Server.Implementations protected readonly List<IDisposable> DisposableParts = new List<IDisposable>(); /// <summary> - /// Gets a value indicating whether this instance is first run. - /// </summary> - /// <value><c>true</c> if this instance is first run; otherwise, <c>false</c>.</value> - public bool IsFirstRun { get; private set; } - - /// <summary> /// Gets the configuration manager. /// </summary> /// <value>The configuration manager.</value> @@ -276,7 +261,6 @@ namespace Emby.Server.Implementations protected readonly SimpleInjector.Container Container = new SimpleInjector.Container(); protected ISystemEvents SystemEvents { get; set; } - protected IMemoryStreamFactory MemoryStreamFactory { get; set; } /// <summary> /// Gets the server configuration manager. @@ -296,11 +280,11 @@ namespace Emby.Server.Implementations return new ServerConfigurationManager(ApplicationPaths, LogManager, XmlSerializer, FileSystemManager); } - /// <summary> - /// Gets or sets the server manager. - /// </summary> - /// <value>The server manager.</value> - private IServerManager ServerManager { get; set; } + protected virtual IResourceFileManager CreateResourceFileManager() + { + return new ResourceFileManager(HttpResultFactory, LogManager.GetLogger("ResourceManager"), FileSystemManager); + } + /// <summary> /// Gets or sets the user manager. /// </summary> @@ -345,7 +329,7 @@ namespace Emby.Server.Implementations private IEncodingManager EncodingManager { get; set; } private IChannelManager ChannelManager { get; set; } - private ISyncManager SyncManager { get; set; } + protected ITextEncoding TextEncoding { get; private set; } /// <summary> /// Gets or sets the user data repository. @@ -355,7 +339,6 @@ namespace Emby.Server.Implementations private IUserRepository UserRepository { get; set; } internal IDisplayPreferencesRepository DisplayPreferencesRepository { get; set; } internal IItemRepository ItemRepository { get; set; } - private INotificationsRepository NotificationsRepository { get; set; } private INotificationManager NotificationManager { get; set; } private ISubtitleManager SubtitleManager { get; set; } @@ -386,7 +369,7 @@ namespace Emby.Server.Implementations /// </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 StartupOptions StartupOptions { get; private set; } @@ -428,11 +411,9 @@ namespace Emby.Server.Implementations XmlSerializer = new MyXmlSerializer(fileSystem, logManager.GetLogger("XmlSerializer")); NetworkManager = networkManager; + networkManager.LocalSubnetsFn = GetConfiguredLocalSubnets; EnvironmentInfo = environmentInfo; SystemEvents = systemEvents; - MemoryStreamFactory = new MemoryStreamProvider(); - - FailedAssemblies = new List<string>(); ApplicationPaths = applicationPaths; LogManager = logManager; @@ -456,6 +437,27 @@ namespace Emby.Server.Implementations NetworkManager.NetworkChanged += NetworkManager_NetworkChanged; } + public string ExpandVirtualPath(string path) + { + var appPaths = ApplicationPaths; + + return path.Replace(appPaths.VirtualDataPath, appPaths.DataPath, StringComparison.OrdinalIgnoreCase) + .Replace(appPaths.VirtualInternalMetadataPath, appPaths.InternalMetadataPath, StringComparison.OrdinalIgnoreCase); + } + + public string ReverseVirtualPath(string path) + { + var appPaths = ApplicationPaths; + + return path.Replace(appPaths.DataPath, appPaths.VirtualDataPath, StringComparison.OrdinalIgnoreCase) + .Replace(appPaths.InternalMetadataPath, appPaths.VirtualInternalMetadataPath, StringComparison.OrdinalIgnoreCase); + } + + private string[] GetConfiguredLocalSubnets() + { + return ServerConfigurationManager.Configuration.LocalNetworkSubnets; + } + private void NetworkManager_NetworkChanged(object sender, EventArgs e) { _validAddressResults.Clear(); @@ -470,7 +472,7 @@ namespace Emby.Server.Implementations { get { - return _version ?? (_version = GetAssembly(GetType()).GetName().Version); + return _version ?? (_version = GetType().GetTypeInfo().Assembly.GetName().Version); } } @@ -500,9 +502,17 @@ namespace Emby.Server.Implementations } } - private Assembly GetAssembly(Type type) + private Tuple<Assembly, string> GetAssembly(Type type) { - return type.GetTypeInfo().Assembly; + var assembly = type.GetTypeInfo().Assembly; + string path = null; + + return new Tuple<Assembly, string>(assembly, path); + } + + public virtual IStreamHelper CreateStreamHelper() + { + return new StreamHelper(); } public virtual bool SupportsAutoRunAtStartup @@ -520,16 +530,7 @@ namespace Emby.Server.Implementations /// <returns>System.Object.</returns> public object CreateInstance(Type type) { - try - { - return Container.GetInstance(type); - } - catch (Exception ex) - { - Logger.ErrorException("Error creating {0}", ex, type.FullName); - - throw; - } + return Container.GetInstance(type); } /// <summary> @@ -537,8 +538,10 @@ namespace Emby.Server.Implementations /// </summary> /// <param name="type">The type.</param> /// <returns>System.Object.</returns> - protected object CreateInstanceSafe(Type type) + protected object CreateInstanceSafe(Tuple<Type, string> typeInfo) { + var type = typeInfo.Item1; + try { return Container.GetInstance(type); @@ -615,15 +618,16 @@ namespace Emby.Server.Implementations /// </summary> /// <param name="file">The file.</param> /// <returns>Assembly.</returns> - protected Assembly LoadAssembly(string file) + protected Tuple<Assembly, string> LoadAssembly(string file) { try { - return Assembly.Load(File.ReadAllBytes(file)); + var assembly = Assembly.Load(File.ReadAllBytes(file)); + + return new Tuple<Assembly, string>(assembly, file); } catch (Exception ex) { - FailedAssemblies.Add(file); Logger.ErrorException("Error loading assembly {0}", ex, file); return null; } @@ -634,11 +638,11 @@ namespace Emby.Server.Implementations /// </summary> /// <typeparam name="T"></typeparam> /// <returns>IEnumerable{Type}.</returns> - public IEnumerable<Type> GetExportTypes<T>() + public IEnumerable<Tuple<Type, string>> GetExportTypes<T>() { var currentType = typeof(T); - return AllConcreteTypes.Where(currentType.IsAssignableFrom); + return AllConcreteTypes.Where(i => currentType.IsAssignableFrom(i.Item1)); } /// <summary> @@ -666,6 +670,33 @@ namespace Emby.Server.Implementations return parts; } + public List<Tuple<T, string>> GetExportsWithInfo<T>(bool manageLiftime = true) + { + var parts = GetExportTypes<T>() + .Select(i => + { + var obj = CreateInstanceSafe(i); + + if (obj == null) + { + return null; + } + return new Tuple<T, string>((T)obj, i.Item2); + }) + .Where(i => i != null) + .ToList(); + + if (manageLiftime) + { + lock (DisposableParts) + { + DisposableParts.AddRange(parts.Select(i => i.Item1).OfType<IDisposable>()); + } + } + + return parts; + } + private void SetBaseExceptionMessage() { var builder = GetBaseExceptionMessage(ApplicationPaths); @@ -687,25 +718,42 @@ namespace Emby.Server.Implementations ConfigurationManager.ConfigurationUpdated += OnConfigurationUpdated; - await MediaEncoder.Init().ConfigureAwait(false); + MediaEncoder.Init(); - if (string.IsNullOrWhiteSpace(MediaEncoder.EncoderPath)) - { - if (ServerConfigurationManager.Configuration.IsStartupWizardCompleted) - { - ServerConfigurationManager.Configuration.IsStartupWizardCompleted = false; - ServerConfigurationManager.SaveConfiguration(); - } - } + //if (string.IsNullOrWhiteSpace(MediaEncoder.EncoderPath)) + //{ + // if (ServerConfigurationManager.Configuration.IsStartupWizardCompleted) + // { + // ServerConfigurationManager.Configuration.IsStartupWizardCompleted = false; + // ServerConfigurationManager.SaveConfiguration(); + // } + //} Logger.Info("ServerId: {0}", SystemId); + + var entryPoints = GetExports<IServerEntryPoint>().ToList(); + RunEntryPoints(entryPoints, true); + Logger.Info("Core startup complete"); HttpServer.GlobalResponse = null; Logger.Info("Post-init migrations complete"); - foreach (var entryPoint in GetExports<IServerEntryPoint>().ToList()) + RunEntryPoints(entryPoints, false); + Logger.Info("All entry points have started"); + + LogManager.RemoveConsoleOutput(); + } + + private void RunEntryPoints(IEnumerable<IServerEntryPoint> entryPoints, bool isBeforeStartup) + { + foreach (var entryPoint in entryPoints) { + if (isBeforeStartup != (entryPoint is IRunBeforeStartup)) + { + continue; + } + var name = entryPoint.GetType().FullName; Logger.Info("Starting entry point {0}", name); var now = DateTime.UtcNow; @@ -719,9 +767,6 @@ namespace Emby.Server.Implementations } Logger.Info("Entry point completed: {0}. Duration: {1} seconds", name, (DateTime.UtcNow - now).TotalSeconds.ToString(CultureInfo.InvariantCulture), "ImageInfos"); } - Logger.Info("All entry points have started"); - - LogManager.RemoveConsoleOutput(); } /// <summary> @@ -741,20 +786,10 @@ namespace Emby.Server.Implementations private IJsonSerializer CreateJsonSerializer() { - try - { - // https://github.com/ServiceStack/ServiceStack/blob/master/tests/ServiceStack.WebHost.IntegrationTests/Web.config#L4 - Licensing.RegisterLicense("1001-e1JlZjoxMDAxLE5hbWU6VGVzdCBCdXNpbmVzcyxUeXBlOkJ1c2luZXNzLEhhc2g6UHVNTVRPclhvT2ZIbjQ5MG5LZE1mUTd5RUMzQnBucTFEbTE3TDczVEF4QUNMT1FhNXJMOWkzVjFGL2ZkVTE3Q2pDNENqTkQyUktRWmhvUVBhYTBiekJGUUZ3ZE5aZHFDYm9hL3lydGlwUHI5K1JsaTBYbzNsUC85cjVJNHE5QVhldDN6QkE4aTlvdldrdTgyTk1relY2eis2dFFqTThYN2lmc0JveHgycFdjPSxFeHBpcnk6MjAxMy0wMS0wMX0="); - } - catch - { - // Failing under mono - } - return new JsonSerializer(FileSystemManager, LogManager.GetLogger("JsonSerializer")); } - public async Task Init(IProgress<double> progress) + public void Init() { HttpPort = ServerConfigurationManager.Configuration.HttpServerPortNumber; HttpsPort = ServerConfigurationManager.Configuration.HttpsPortNumber; @@ -766,39 +801,22 @@ namespace Emby.Server.Implementations HttpsPort = ServerConfiguration.DefaultHttpsPort; } - progress.Report(1); - JsonSerializer = CreateJsonSerializer(); OnLoggerLoaded(true); LogManager.LoggerLoaded += (s, e) => OnLoggerLoaded(false); - IsFirstRun = !ConfigurationManager.CommonConfiguration.IsStartupWizardCompleted; - progress.Report(2); - LogManager.LogSeverity = ConfigurationManager.CommonConfiguration.EnableDebugLevelLogging ? LogSeverity.Debug : LogSeverity.Info; - progress.Report(3); - DiscoverTypes(); - progress.Report(14); SetHttpLimit(); - progress.Report(15); - - var innerProgress = new ActionableProgress<double>(); - innerProgress.RegisterAction(p => progress.Report(.8 * p + 15)); - await RegisterResources(innerProgress).ConfigureAwait(false); + RegisterResources(); FindParts(); - progress.Report(95); - - await InstallIsoMounters(CancellationToken.None).ConfigureAwait(false); - - progress.Report(100); } protected virtual void OnLoggerLoaded(bool isFirstLoad) @@ -810,16 +828,6 @@ namespace Emby.Server.Implementations LogEnvironmentInfo(Logger, ApplicationPaths, false); } - // Put the app config in the log for troubleshooting purposes - var configJson = new StringBuilder(JsonSerializer.SerializeToString(ConfigurationManager.CommonConfiguration)); - - if (!string.IsNullOrWhiteSpace(ServerConfigurationManager.Configuration.CertificatePassword)) - { - configJson = configJson.Replace(ServerConfigurationManager.Configuration.CertificatePassword, "####"); - } - - Logger.LogMultiline("Application configuration:", LogSeverity.Info, configJson); - if (Plugins != null) { var pluginBuilder = new StringBuilder(); @@ -834,17 +842,18 @@ namespace Emby.Server.Implementations } protected abstract IConnectManager CreateConnectManager(); - protected abstract ISyncManager CreateSyncManager(); protected virtual IHttpClient CreateHttpClient() { - return new HttpClientManager.HttpClientManager(ApplicationPaths, LogManager.GetLogger("HttpClient"), FileSystemManager, MemoryStreamFactory, GetDefaultUserAgent); + return new HttpClientManager.HttpClientManager(ApplicationPaths, LogManager.GetLogger("HttpClient"), FileSystemManager, GetDefaultUserAgent); } + public static IStreamHelper StreamHelper { get; set; } + /// <summary> /// Registers resources that classes will depend on /// </summary> - protected async Task RegisterResources(IProgress<double> progress) + protected void RegisterResources() { RegisterSingleInstance(ConfigurationManager); RegisterSingleInstance<IApplicationHost>(this); @@ -852,7 +861,6 @@ namespace Emby.Server.Implementations RegisterSingleInstance<IApplicationPaths>(ApplicationPaths); RegisterSingleInstance(JsonSerializer); - RegisterSingleInstance(MemoryStreamFactory); RegisterSingleInstance(SystemEvents); RegisterSingleInstance(LogManager, false); @@ -881,6 +889,10 @@ namespace Emby.Server.Implementations TimerFactory = new TimerFactory(); RegisterSingleInstance(TimerFactory); + var streamHelper = CreateStreamHelper(); + ApplicationHost.StreamHelper = streamHelper; + RegisterSingleInstance(streamHelper); + RegisterSingleInstance(CryptographyProvider); SocketFactory = new SocketFactory(LogManager.GetLogger("SocketFactory")); @@ -891,13 +903,14 @@ namespace Emby.Server.Implementations SecurityManager = new PluginSecurityManager(this, HttpClient, JsonSerializer, ApplicationPaths, LogManager, FileSystemManager, CryptographyProvider); RegisterSingleInstance(SecurityManager); - InstallationManager = new InstallationManager(LogManager.GetLogger("InstallationManager"), this, ApplicationPaths, HttpClient, JsonSerializer, SecurityManager, ConfigurationManager, FileSystemManager, CryptographyProvider, PackageRuntime); + InstallationManager = new InstallationManager(LogManager.GetLogger("InstallationManager"), this, ApplicationPaths, HttpClient, JsonSerializer, SecurityManager, ServerConfigurationManager, FileSystemManager, CryptographyProvider, PackageRuntime); RegisterSingleInstance(InstallationManager); ZipClient = new ZipClient(FileSystemManager); RegisterSingleInstance(ZipClient); - RegisterSingleInstance<IHttpResultFactory>(new HttpResultFactory(LogManager, FileSystemManager, JsonSerializer, MemoryStreamFactory)); + HttpResultFactory = new HttpResultFactory(LogManager, FileSystemManager, JsonSerializer, CreateBrotliCompressor()); + RegisterSingleInstance(HttpResultFactory); RegisterSingleInstance<IServerApplicationHost>(this); RegisterSingleInstance<IServerApplicationPaths>(ApplicationPaths); @@ -911,26 +924,25 @@ namespace Emby.Server.Implementations StringExtensions.LocalizationManager = LocalizationManager; RegisterSingleInstance(LocalizationManager); - ITextEncoding textEncoding = new TextEncoding.TextEncoding(FileSystemManager, LogManager.GetLogger("TextEncoding"), JsonSerializer); - RegisterSingleInstance(textEncoding); - Utilities.EncodingHelper = textEncoding; - BlurayExaminer = new BdInfoExaminer(FileSystemManager, textEncoding); + TextEncoding = new TextEncoding.TextEncoding(FileSystemManager, LogManager.GetLogger("TextEncoding"), JsonSerializer); + RegisterSingleInstance(TextEncoding); + BlurayExaminer = new BdInfoExaminer(FileSystemManager, TextEncoding); RegisterSingleInstance(BlurayExaminer); RegisterSingleInstance<IXmlReaderSettingsFactory>(new XmlReaderSettingsFactory()); - UserDataManager = new UserDataManager(LogManager, ServerConfigurationManager); + UserDataManager = new UserDataManager(LogManager, ServerConfigurationManager, () => UserManager); RegisterSingleInstance(UserDataManager); UserRepository = GetUserRepository(); // This is only needed for disposal purposes. If removing this, make sure to have the manager handle disposing it RegisterSingleInstance(UserRepository); - var displayPreferencesRepo = new SqliteDisplayPreferencesRepository(LogManager.GetLogger("SqliteDisplayPreferencesRepository"), JsonSerializer, ApplicationPaths, MemoryStreamFactory, FileSystemManager); + var displayPreferencesRepo = new SqliteDisplayPreferencesRepository(LogManager.GetLogger("SqliteDisplayPreferencesRepository"), JsonSerializer, ApplicationPaths, FileSystemManager); DisplayPreferencesRepository = displayPreferencesRepo; RegisterSingleInstance(DisplayPreferencesRepository); - var itemRepo = new SqliteItemRepository(ServerConfigurationManager, JsonSerializer, LogManager.GetLogger("SqliteItemRepository"), MemoryStreamFactory, assemblyInfo, FileSystemManager, EnvironmentInfo, TimerFactory); + var itemRepo = new SqliteItemRepository(ServerConfigurationManager, this, JsonSerializer, LogManager.GetLogger("SqliteItemRepository"), assemblyInfo, FileSystemManager, EnvironmentInfo, TimerFactory); ItemRepository = itemRepo; RegisterSingleInstance(ItemRepository); @@ -940,7 +952,7 @@ namespace Emby.Server.Implementations UserManager = new UserManager(LogManager.GetLogger("UserManager"), ServerConfigurationManager, UserRepository, XmlSerializer, NetworkManager, () => ImageProcessor, () => DtoService, () => ConnectManager, this, JsonSerializer, FileSystemManager, CryptographyProvider); RegisterSingleInstance(UserManager); - LibraryManager = new LibraryManager(Logger, TaskManager, UserManager, ServerConfigurationManager, UserDataManager, () => LibraryMonitor, FileSystemManager, () => ProviderManager, () => UserViewManager); + LibraryManager = new LibraryManager(this, Logger, TaskManager, UserManager, ServerConfigurationManager, UserDataManager, () => LibraryMonitor, FileSystemManager, () => ProviderManager, () => UserViewManager); RegisterSingleInstance(LibraryManager); var musicManager = new MusicManager(LibraryManager); @@ -949,24 +961,23 @@ namespace Emby.Server.Implementations LibraryMonitor = new LibraryMonitor(LogManager, TaskManager, LibraryManager, ServerConfigurationManager, FileSystemManager, TimerFactory, SystemEvents, EnvironmentInfo); RegisterSingleInstance(LibraryMonitor); - ProviderManager = new ProviderManager(HttpClient, ServerConfigurationManager, LibraryMonitor, LogManager, FileSystemManager, ApplicationPaths, () => LibraryManager, JsonSerializer, MemoryStreamFactory); - RegisterSingleInstance(ProviderManager); - RegisterSingleInstance<ISearchEngine>(() => new SearchEngine(LogManager, LibraryManager, UserManager)); CertificateInfo = GetCertificateInfo(true); Certificate = GetCertificate(CertificateInfo); - HttpServer = HttpServerFactory.CreateServer(this, LogManager, ServerConfigurationManager, NetworkManager, MemoryStreamFactory, "Emby", "web/index.html", textEncoding, SocketFactory, CryptographyProvider, JsonSerializer, XmlSerializer, EnvironmentInfo, Certificate, FileSystemManager, SupportsDualModeSockets); - HttpServer.GlobalResponse = LocalizationManager.GetLocalizedString("StartupEmbyServerIsLoading"); - RegisterSingleInstance(HttpServer, false); - progress.Report(10); - - ServerManager = new ServerManager.ServerManager(this, JsonSerializer, LogManager.GetLogger("ServerManager"), ServerConfigurationManager, MemoryStreamFactory, textEncoding); - RegisterSingleInstance(ServerManager); + HttpServer = new HttpListenerHost(this, + LogManager.GetLogger("HttpServer"), + ServerConfigurationManager, + "web/index.html", + NetworkManager, + TextEncoding, + JsonSerializer, + XmlSerializer, + GetParseFn); - var innerProgress = new ActionableProgress<double>(); - innerProgress.RegisterAction(p => progress.Report((.75 * p) + 15)); + HttpServer.GlobalResponse = LocalizationManager.GetLocalizedString("StartupEmbyServerIsLoading"); + RegisterSingleInstance(HttpServer); ImageProcessor = GetImageProcessor(); RegisterSingleInstance(ImageProcessor); @@ -974,112 +985,101 @@ namespace Emby.Server.Implementations TVSeriesManager = new TVSeriesManager(UserManager, UserDataManager, LibraryManager, ServerConfigurationManager); RegisterSingleInstance(TVSeriesManager); - SyncManager = CreateSyncManager(); - RegisterSingleInstance(SyncManager); - - DtoService = new DtoService(LogManager.GetLogger("DtoService"), LibraryManager, UserDataManager, ItemRepository, ImageProcessor, ServerConfigurationManager, FileSystemManager, ProviderManager, () => ChannelManager, SyncManager, this, () => DeviceManager, () => MediaSourceManager, () => LiveTvManager); - RegisterSingleInstance(DtoService); - var encryptionManager = new EncryptionManager(); RegisterSingleInstance<IEncryptionManager>(encryptionManager); ConnectManager = CreateConnectManager(); RegisterSingleInstance(ConnectManager); - var deviceRepo = new SqliteDeviceRepository(LogManager.GetLogger("DeviceManager"), ServerConfigurationManager, FileSystemManager, JsonSerializer); - deviceRepo.Initialize(); - DeviceManager = new DeviceManager(deviceRepo, UserManager, FileSystemManager, LibraryMonitor, ServerConfigurationManager, LogManager.GetLogger("DeviceManager"), NetworkManager); - RegisterSingleInstance<IDeviceRepository>(deviceRepo); + DeviceManager = new DeviceManager(AuthenticationRepository, JsonSerializer, LibraryManager, LocalizationManager, UserManager, FileSystemManager, LibraryMonitor, ServerConfigurationManager, LogManager.GetLogger("DeviceManager"), NetworkManager); RegisterSingleInstance(DeviceManager); var newsService = new Emby.Server.Implementations.News.NewsService(ApplicationPaths, JsonSerializer); RegisterSingleInstance<INewsService>(newsService); - progress.Report(15); + MediaSourceManager = new MediaSourceManager(ItemRepository, ApplicationPaths, LocalizationManager, UserManager, LibraryManager, LogManager.GetLogger("MediaSourceManager"), JsonSerializer, FileSystemManager, UserDataManager, TimerFactory, () => MediaEncoder); + RegisterSingleInstance(MediaSourceManager); + + SubtitleManager = new SubtitleManager(LogManager.GetLogger("SubtitleManager"), FileSystemManager, LibraryMonitor, MediaSourceManager, ServerConfigurationManager, LocalizationManager); + RegisterSingleInstance(SubtitleManager); + + ProviderManager = new ProviderManager(HttpClient, SubtitleManager, ServerConfigurationManager, LibraryMonitor, LogManager, FileSystemManager, ApplicationPaths, () => LibraryManager, JsonSerializer); + RegisterSingleInstance(ProviderManager); + + DtoService = new DtoService(LogManager.GetLogger("DtoService"), LibraryManager, UserDataManager, ItemRepository, ImageProcessor, ServerConfigurationManager, FileSystemManager, ProviderManager, () => ChannelManager, this, () => DeviceManager, () => MediaSourceManager, () => LiveTvManager); + RegisterSingleInstance(DtoService); ChannelManager = new ChannelManager(UserManager, DtoService, LibraryManager, LogManager.GetLogger("ChannelManager"), ServerConfigurationManager, FileSystemManager, UserDataManager, JsonSerializer, LocalizationManager, HttpClient, ProviderManager); RegisterSingleInstance(ChannelManager); - MediaSourceManager = new MediaSourceManager(ItemRepository, UserManager, LibraryManager, LogManager.GetLogger("MediaSourceManager"), JsonSerializer, FileSystemManager, UserDataManager, TimerFactory); - RegisterSingleInstance(MediaSourceManager); - SessionManager = new SessionManager(UserDataManager, LogManager.GetLogger("SessionManager"), LibraryManager, UserManager, musicManager, DtoService, ImageProcessor, JsonSerializer, this, HttpClient, AuthenticationRepository, DeviceManager, MediaSourceManager, TimerFactory); RegisterSingleInstance(SessionManager); var dlnaManager = new DlnaManager(XmlSerializer, FileSystemManager, ApplicationPaths, LogManager.GetLogger("Dlna"), JsonSerializer, this, assemblyInfo); RegisterSingleInstance<IDlnaManager>(dlnaManager); - var connectionManager = new ConnectionManager(dlnaManager, ServerConfigurationManager, LogManager.GetLogger("UpnpConnectionManager"), HttpClient, new XmlReaderSettingsFactory()); - RegisterSingleInstance<IConnectionManager>(connectionManager); - - CollectionManager = new CollectionManager(LibraryManager, FileSystemManager, LibraryMonitor, LogManager.GetLogger("CollectionManager"), ProviderManager); + CollectionManager = new CollectionManager(LibraryManager, ApplicationPaths, LocalizationManager, FileSystemManager, LibraryMonitor, LogManager.GetLogger("CollectionManager"), ProviderManager); RegisterSingleInstance(CollectionManager); PlaylistManager = new PlaylistManager(LibraryManager, FileSystemManager, LibraryMonitor, LogManager.GetLogger("PlaylistManager"), UserManager, ProviderManager); RegisterSingleInstance<IPlaylistManager>(PlaylistManager); - LiveTvManager = new LiveTvManager(this, ServerConfigurationManager, Logger, ItemRepository, ImageProcessor, UserDataManager, DtoService, UserManager, LibraryManager, TaskManager, LocalizationManager, JsonSerializer, ProviderManager, FileSystemManager, SecurityManager); + LiveTvManager = new LiveTvManager(this, HttpClient, ServerConfigurationManager, Logger, ItemRepository, ImageProcessor, UserDataManager, DtoService, UserManager, LibraryManager, TaskManager, LocalizationManager, JsonSerializer, ProviderManager, FileSystemManager, SecurityManager, () => ChannelManager); RegisterSingleInstance(LiveTvManager); UserViewManager = new UserViewManager(LibraryManager, LocalizationManager, UserManager, ChannelManager, LiveTvManager, ServerConfigurationManager); RegisterSingleInstance(UserViewManager); - var contentDirectory = new ContentDirectory(dlnaManager, UserDataManager, ImageProcessor, LibraryManager, ServerConfigurationManager, UserManager, LogManager.GetLogger("UpnpContentDirectory"), HttpClient, LocalizationManager, ChannelManager, MediaSourceManager, UserViewManager, () => MediaEncoder, new XmlReaderSettingsFactory(), TVSeriesManager); - RegisterSingleInstance<IContentDirectory>(contentDirectory); - - var mediaRegistrar = new MediaReceiverRegistrar(LogManager.GetLogger("MediaReceiverRegistrar"), HttpClient, ServerConfigurationManager, new XmlReaderSettingsFactory()); - RegisterSingleInstance<IMediaReceiverRegistrar>(mediaRegistrar); - NotificationManager = new NotificationManager(LogManager, UserManager, ServerConfigurationManager); RegisterSingleInstance(NotificationManager); - SubtitleManager = new SubtitleManager(LogManager.GetLogger("SubtitleManager"), FileSystemManager, LibraryMonitor, LibraryManager, MediaSourceManager, ServerConfigurationManager); - RegisterSingleInstance(SubtitleManager); - RegisterSingleInstance<IDeviceDiscovery>(new DeviceDiscovery(LogManager.GetLogger("IDeviceDiscovery"), ServerConfigurationManager, SocketFactory, TimerFactory)); ChapterManager = new ChapterManager(LibraryManager, LogManager.GetLogger("ChapterManager"), ServerConfigurationManager, ItemRepository); RegisterSingleInstance(ChapterManager); - await RegisterMediaEncoder(innerProgress).ConfigureAwait(false); - progress.Report(90); + RegisterMediaEncoder(assemblyInfo); EncodingManager = new EncodingManager(FileSystemManager, Logger, MediaEncoder, ChapterManager, LibraryManager); RegisterSingleInstance(EncodingManager); - var sharingRepo = new SharingRepository(LogManager.GetLogger("SharingRepository"), ApplicationPaths, FileSystemManager); - sharingRepo.Initialize(); - // This is only needed for disposal purposes. If removing this, make sure to have the manager handle disposing it - RegisterSingleInstance<ISharingRepository>(sharingRepo); - RegisterSingleInstance<ISharingManager>(new SharingManager(sharingRepo, ServerConfigurationManager, LibraryManager, this)); - var activityLogRepo = GetActivityLogRepository(); RegisterSingleInstance(activityLogRepo); RegisterSingleInstance<IActivityManager>(new ActivityManager(LogManager.GetLogger("ActivityManager"), activityLogRepo, UserManager)); - var authContext = new AuthorizationContext(AuthenticationRepository, ConnectManager); + var authContext = new AuthorizationContext(AuthenticationRepository, ConnectManager, UserManager); RegisterSingleInstance<IAuthorizationContext>(authContext); RegisterSingleInstance<ISessionContext>(new SessionContext(UserManager, authContext, SessionManager)); - AuthService = new AuthService(UserManager, authContext, ServerConfigurationManager, ConnectManager, SessionManager, DeviceManager); + AuthService = new AuthService(UserManager, authContext, ServerConfigurationManager, ConnectManager, SessionManager, NetworkManager); RegisterSingleInstance<IAuthService>(AuthService); - SubtitleEncoder = new SubtitleEncoder(LibraryManager, LogManager.GetLogger("SubtitleEncoder"), ApplicationPaths, FileSystemManager, MediaEncoder, JsonSerializer, HttpClient, MediaSourceManager, MemoryStreamFactory, ProcessFactory, textEncoding); + SubtitleEncoder = new SubtitleEncoder(LibraryManager, LogManager.GetLogger("SubtitleEncoder"), ApplicationPaths, FileSystemManager, MediaEncoder, JsonSerializer, HttpClient, MediaSourceManager, ProcessFactory, TextEncoding); RegisterSingleInstance(SubtitleEncoder); + RegisterSingleInstance(CreateResourceFileManager()); + displayPreferencesRepo.Initialize(); var userDataRepo = new SqliteUserDataRepository(LogManager.GetLogger("SqliteUserDataRepository"), ApplicationPaths, FileSystemManager); + SetStaticProperties(); + + ((UserManager)UserManager).Initialize(); + ((UserDataManager)UserDataManager).Repository = userDataRepo; - itemRepo.Initialize(userDataRepo); + itemRepo.Initialize(userDataRepo, UserManager); ((LibraryManager)LibraryManager).ItemRepository = ItemRepository; - ConfigureNotificationsRepository(); - progress.Report(100); + } - SetStaticProperties(); + protected virtual IBrotliCompressor CreateBrotliCompressor() + { + return null; + } - ((UserManager)UserManager).Initialize(); + private static Func<string, object> GetParseFn(Type propertyType) + { + return s => JsvReader.GetParseFn(propertyType)(s); } public virtual string PackageRuntime @@ -1099,7 +1099,13 @@ namespace Emby.Server.Implementations { var builder = new StringBuilder(); - builder.AppendLine(string.Format("Command line: {0}", string.Join(" ", Environment.GetCommandLineArgs()))); + // Distinct these to prevent users from reporting problems that aren't actually problems + var commandLineArgs = Environment + .GetCommandLineArgs() + .Distinct() + .ToArray(); + + builder.AppendLine(string.Format("Command line: {0}", string.Join(" ", commandLineArgs))); builder.AppendLine(string.Format("Operating system: {0}", Environment.OSVersion)); builder.AppendLine(string.Format("64-Bit OS: {0}", Environment.Is64BitOperatingSystem)); @@ -1136,37 +1142,6 @@ namespace Emby.Server.Implementations } } - /// <summary> - /// Installs the iso mounters. - /// </summary> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task.</returns> - private async Task InstallIsoMounters(CancellationToken cancellationToken) - { - var list = new List<IIsoMounter>(); - - foreach (var isoMounter in GetExports<IIsoMounter>()) - { - try - { - if (isoMounter.RequiresInstallation && !isoMounter.IsInstalled) - { - Logger.Info("Installing {0}", isoMounter.Name); - - await isoMounter.Install(cancellationToken).ConfigureAwait(false); - } - - list.Add(isoMounter); - } - catch (Exception ex) - { - Logger.ErrorException("{0} failed to load.", ex, isoMounter.Name); - } - } - - IsoManager.AddParts(list); - } - protected string GetDefaultUserAgent() { var name = FormatAttribute(Name); @@ -1222,7 +1197,7 @@ namespace Emby.Server.Implementations //localCert.PrivateKey = PrivateKey.CreateFromFile(pvk_file).RSA; if (!localCert.HasPrivateKey) { - //throw new FileNotFoundException("Secure requested, no private key included", certificateLocation); + Logger.Error("No private key included in SSL cert {0}.", certificateLocation); return null; } @@ -1255,7 +1230,6 @@ namespace Emby.Server.Implementations info.FFProbeFilename = "ffprobe"; info.ArchiveType = "7z"; info.Version = "20170308"; - info.DownloadUrls = GetLinuxDownloadUrls(); } else if (EnvironmentInfo.OperatingSystem == MediaBrowser.Model.System.OperatingSystem.Windows) { @@ -1263,7 +1237,6 @@ namespace Emby.Server.Implementations info.FFProbeFilename = "ffprobe.exe"; info.Version = "20170308"; info.ArchiveType = "7z"; - info.DownloadUrls = GetWindowsDownloadUrls(); } else if (EnvironmentInfo.OperatingSystem == MediaBrowser.Model.System.OperatingSystem.OSX) { @@ -1271,80 +1244,27 @@ namespace Emby.Server.Implementations info.FFProbeFilename = "ffprobe"; info.ArchiveType = "7z"; info.Version = "20170308"; - info.DownloadUrls = GetMacDownloadUrls(); - } - else - { - // No version available - user requirement - info.DownloadUrls = new string[] { }; } return info; } - private string[] GetMacDownloadUrls() - { - switch (EnvironmentInfo.SystemArchitecture) - { - case MediaBrowser.Model.System.Architecture.X64: - return new[] - { - "https://embydata.com/downloads/ffmpeg/osx/ffmpeg-x64-20170308.7z" - }; - } - - return new string[] { }; - } - - private string[] GetWindowsDownloadUrls() - { - switch (EnvironmentInfo.SystemArchitecture) - { - case MediaBrowser.Model.System.Architecture.X64: - return new[] - { - "https://embydata.com/downloads/ffmpeg/windows/ffmpeg-20170308-win64.7z" - }; - case MediaBrowser.Model.System.Architecture.X86: - return new[] - { - "https://embydata.com/downloads/ffmpeg/windows/ffmpeg-20170308-win32.7z" - }; - } - - return new string[] { }; - } - - private string[] GetLinuxDownloadUrls() + protected virtual FFMpegInfo GetFFMpegInfo() { - switch (EnvironmentInfo.SystemArchitecture) - { - case MediaBrowser.Model.System.Architecture.X64: - return new[] - { - "https://embydata.com/downloads/ffmpeg/linux/ffmpeg-git-20170301-64bit-static.7z" - }; - case MediaBrowser.Model.System.Architecture.X86: - return new[] - { - "https://embydata.com/downloads/ffmpeg/linux/ffmpeg-git-20170301-32bit-static.7z" - }; - } - - return new string[] { }; + return new FFMpegLoader(Logger, ApplicationPaths, HttpClient, ZipClient, FileSystemManager, GetFfmpegInstallInfo()) + .GetFFMpegInfo(StartupOptions); } /// <summary> /// Registers the media encoder. /// </summary> /// <returns>Task.</returns> - private async Task RegisterMediaEncoder(IProgress<double> progress) + private void RegisterMediaEncoder(IAssemblyInfo assemblyInfo) { string encoderPath = null; string probePath = null; - var info = await new FFMpegLoader(Logger, ApplicationPaths, HttpClient, ZipClient, FileSystemManager, GetFfmpegInstallInfo()) - .GetFFMpegInfo(StartupOptions, progress).ConfigureAwait(false); + var info = GetFFMpegInfo(); encoderPath = info.EncoderPath; probePath = info.ProbePath; @@ -1366,12 +1286,11 @@ namespace Emby.Server.Implementations () => MediaSourceManager, HttpClient, ZipClient, - MemoryStreamFactory, ProcessFactory, - (Environment.ProcessorCount > 2 ? 14000 : 40000), - EnvironmentInfo.OperatingSystem == MediaBrowser.Model.System.OperatingSystem.Windows, EnvironmentInfo, - BlurayExaminer); + BlurayExaminer, + assemblyInfo, + this); MediaEncoder = mediaEncoder; RegisterSingleInstance(MediaEncoder); @@ -1383,7 +1302,7 @@ namespace Emby.Server.Implementations /// <returns>Task{IUserRepository}.</returns> private IUserRepository GetUserRepository() { - var repo = new SqliteUserRepository(LogManager.GetLogger("SqliteUserRepository"), ApplicationPaths, JsonSerializer, MemoryStreamFactory); + var repo = new SqliteUserRepository(LogManager.GetLogger("SqliteUserRepository"), ApplicationPaths, JsonSerializer); repo.Initialize(); @@ -1392,7 +1311,7 @@ namespace Emby.Server.Implementations private IAuthenticationRepository GetAuthenticationRepository() { - var repo = new AuthenticationRepository(LogManager.GetLogger("AuthenticationRepository"), ServerConfigurationManager.ApplicationPaths); + var repo = new AuthenticationRepository(LogManager.GetLogger("AuthenticationRepository"), ServerConfigurationManager); repo.Initialize(); @@ -1409,24 +1328,12 @@ namespace Emby.Server.Implementations } /// <summary> - /// Configures the repositories. - /// </summary> - private void ConfigureNotificationsRepository() - { - var repo = new SqliteNotificationsRepository(LogManager.GetLogger("SqliteNotificationsRepository"), ServerConfigurationManager.ApplicationPaths, FileSystemManager); - - repo.Initialize(); - - NotificationsRepository = repo; - - RegisterSingleInstance(NotificationsRepository); - } - - /// <summary> /// Dirty hacks /// </summary> private void SetStaticProperties() { + ((SqliteItemRepository)ItemRepository).ImageProcessor = ImageProcessor; + // For now there's no real way to inject these properly BaseItem.Logger = LogManager.GetLogger("BaseItem"); BaseItem.ConfigurationManager = ServerConfigurationManager; @@ -1434,20 +1341,19 @@ namespace Emby.Server.Implementations BaseItem.ProviderManager = ProviderManager; BaseItem.LocalizationManager = LocalizationManager; BaseItem.ItemRepository = ItemRepository; - User.XmlSerializer = XmlSerializer; User.UserManager = UserManager; - Folder.UserManager = UserManager; BaseItem.FileSystem = FileSystemManager; BaseItem.UserDataManager = UserDataManager; BaseItem.ChannelManager = ChannelManager; - BaseItem.LiveTvManager = LiveTvManager; + Video.LiveTvManager = LiveTvManager; Folder.UserViewManager = UserViewManager; UserView.TVSeriesManager = TVSeriesManager; UserView.PlaylistManager = PlaylistManager; - BaseItem.CollectionManager = CollectionManager; + UserView.CollectionManager = CollectionManager; BaseItem.MediaSourceManager = MediaSourceManager; CollectionFolder.XmlSerializer = XmlSerializer; - Utilities.CryptographyProvider = CryptographyProvider; + CollectionFolder.JsonSerializer = JsonSerializer; + CollectionFolder.ApplicationHost = this; AuthenticatedAttribute.AuthService = AuthService; } @@ -1463,19 +1369,14 @@ namespace Emby.Server.Implementations ConfigurationManager.SaveConfiguration(); } - RegisterModules(); - ConfigurationManager.AddParts(GetExports<IConfigurationFactory>()); - Plugins = GetExports<IPlugin>().Select(LoadPlugin).Where(i => i != null).ToArray(); + Plugins = GetExportsWithInfo<IPlugin>().Select(LoadPlugin).Where(i => i != null).ToArray(); - HttpServer.Init(GetExports<IService>(false)); - - ServerManager.AddWebSocketListeners(GetExports<IWebSocketListener>(false)); + HttpServer.Init(GetExports<IService>(false), GetExports<IWebSocketListener>()); StartServer(); LibraryManager.AddParts(GetExports<IResolverIgnoreRule>(), - GetExports<IVirtualFolderCreator>(), GetExports<IItemResolver>(), GetExports<IIntroProvider>(), GetExports<IBaseItemComparer>(), @@ -1493,18 +1394,21 @@ namespace Emby.Server.Implementations SubtitleManager.AddParts(GetExports<ISubtitleProvider>()); - SessionManager.AddParts(GetExports<ISessionControllerFactory>()); - ChannelManager.AddParts(GetExports<IChannel>()); MediaSourceManager.AddParts(GetExports<IMediaSourceProvider>()); NotificationManager.AddParts(GetExports<INotificationService>(), GetExports<INotificationTypeFactory>()); - SyncManager.AddParts(GetExports<ISyncProvider>()); + UserManager.AddParts(GetExports<IAuthenticationProvider>()); + + IsoManager.AddParts(GetExports<IIsoMounter>()); } - private IPlugin LoadPlugin(IPlugin plugin) + private IPlugin LoadPlugin(Tuple<IPlugin, string> info) { + var plugin = info.Item1; + var assemblyFilePath = info.Item2; + try { var assemblyPlugin = plugin as IPluginAssembly; @@ -1514,10 +1418,9 @@ namespace Emby.Server.Implementations var assembly = plugin.GetType().Assembly; var assemblyName = assembly.GetName(); - var assemblyFileName = assemblyName.Name + ".dll"; - var assemblyFilePath = Path.Combine(ApplicationPaths.PluginsPath, assemblyFileName); + var dataFolderPath = Path.Combine(ApplicationPaths.PluginsPath, Path.GetFileNameWithoutExtension(assemblyFilePath)); - assemblyPlugin.SetAttributes(assemblyFilePath, assemblyFileName, assemblyName.Version); + assemblyPlugin.SetAttributes(assemblyFilePath, dataFolderPath, assemblyName.Version); try { @@ -1536,8 +1439,11 @@ namespace Emby.Server.Implementations } } - var isFirstRun = !File.Exists(plugin.ConfigurationFilePath); - plugin.SetStartupInfo(isFirstRun, File.GetLastWriteTimeUtc, s => Directory.CreateDirectory(s)); + var hasPluginConfiguration = plugin as IHasPluginConfiguration; + if (hasPluginConfiguration != null) + { + hasPluginConfiguration.SetStartupInfo(s => Directory.CreateDirectory(s)); + } } catch (Exception ex) { @@ -1553,18 +1459,32 @@ namespace Emby.Server.Implementations /// </summary> protected void DiscoverTypes() { - FailedAssemblies.Clear(); + Logger.Info("Loading assemblies"); - var assemblies = GetComposablePartAssemblies().ToList(); + var assemblyInfos = GetComposablePartAssemblies(); - foreach (var assembly in assemblies) + foreach (var assemblyInfo in assemblyInfos) { - Logger.Info("Loading {0}", assembly.FullName); + var assembly = assemblyInfo.Item1; + var path = assemblyInfo.Item2; + + if (path == null) + { + Logger.Info("Loading {0}", assembly.FullName); + } + else + { + Logger.Info("Loading {0} from {1}", assembly.FullName, path); + } } - AllConcreteTypes = assemblies + AllConcreteTypes = assemblyInfos .SelectMany(GetTypes) - .Where(t => t.IsClass && !t.IsAbstract && !t.IsInterface && !t.IsGenericType) + .Where(info => + { + var t = info.Item1; + return t.IsClass && !t.IsAbstract && !t.IsInterface && !t.IsGenericType; + }) .ToArray(); } @@ -1572,22 +1492,21 @@ namespace Emby.Server.Implementations /// Gets a list of types within an assembly /// This will handle situations that would normally throw an exception - such as a type within the assembly that depends on some other non-existant reference /// </summary> - /// <param name="assembly">The assembly.</param> - /// <returns>IEnumerable{Type}.</returns> - /// <exception cref="System.ArgumentNullException">assembly</exception> - protected List<Type> GetTypes(Assembly assembly) + protected List<Tuple<Type, string>> GetTypes(Tuple<Assembly, string> assemblyInfo) { - if (assembly == null) + if (assemblyInfo == null) { - return new List<Type>(); + return new List<Tuple<Type, string>>(); } + var assembly = assemblyInfo.Item1; + try { // This null checking really shouldn't be needed but adding it due to some // unhandled exceptions in mono 5.0 that are a little hard to hunt down var types = assembly.GetTypes() ?? new Type[] { }; - return types.Where(t => t != null).ToList(); + return types.Where(t => t != null).Select(i => new Tuple<Type, string>(i, assemblyInfo.Item2)).ToList(); } catch (ReflectionTypeLoadException ex) { @@ -1604,24 +1523,22 @@ namespace Emby.Server.Implementations // If it fails we can still get a list of the Types it was able to resolve var types = ex.Types ?? new Type[] { }; - return types.Where(t => t != null).ToList(); + return types.Where(t => t != null).Select(i => new Tuple<Type, string>(i, assemblyInfo.Item2)).ToList(); } catch (Exception ex) { Logger.ErrorException("Error loading types from assembly", ex); - return new List<Type>(); + return new List<Tuple<Type, string>>(); } } private CertificateInfo CertificateInfo { get; set; } - private X509Certificate Certificate { get; set; } + protected X509Certificate Certificate { get; private set; } private IEnumerable<string> GetUrlPrefixes() { - var hosts = new List<string>(); - - hosts.Add("+"); + var hosts = new[] { "+" }; return hosts.SelectMany(i => { @@ -1639,6 +1556,8 @@ namespace Emby.Server.Implementations }); } + protected abstract IHttpListener CreateHttpListener(); + /// <summary> /// Starts the server. /// </summary> @@ -1646,12 +1565,16 @@ namespace Emby.Server.Implementations { try { - ServerManager.Start(GetUrlPrefixes().ToArray()); + ((HttpListenerHost)HttpServer).StartServer(GetUrlPrefixes().ToArray(), CreateHttpListener()); return; } catch (Exception ex) { - Logger.ErrorException("Error starting http server", ex); + var msg = string.Equals(ex.GetType().Name, "SocketException", StringComparison.OrdinalIgnoreCase) + ? "The http server is unable to start due to a Socket error. This can occasionally happen when the operating system takes longer than usual to release the IP bindings from the previous session. This can take up to five minutes. Please try waiting or rebooting the system." + : "Error starting Http Server"; + + Logger.ErrorException(msg, ex); if (HttpPort == ServerConfiguration.DefaultHttpPort) { @@ -1663,7 +1586,7 @@ namespace Emby.Server.Implementations try { - ServerManager.Start(GetUrlPrefixes().ToArray()); + ((HttpListenerHost)HttpServer).StartServer(GetUrlPrefixes().ToArray(), CreateHttpListener()); } catch (Exception ex) { @@ -1822,10 +1745,9 @@ namespace Emby.Server.Implementations /// Gets the composable part assemblies. /// </summary> /// <returns>IEnumerable{Assembly}.</returns> - protected IEnumerable<Assembly> GetComposablePartAssemblies() + protected List<Tuple<Assembly, string>> GetComposablePartAssemblies() { - var list = GetPluginAssemblies() - .ToList(); + var list = GetPluginAssemblies(); // Gets all plugin assemblies by first reading all bytes of the .dll and calling Assembly.Load against that // This will prevent the .dll file from getting locked, and allow us to replace it when needed @@ -1863,10 +1785,13 @@ namespace Emby.Server.Implementations // Local metadata list.Add(GetAssembly(typeof(BoxSetXmlSaver))); + // Notifications + list.Add(GetAssembly(typeof(NotificationManager))); + // Xbmc list.Add(GetAssembly(typeof(ArtistNfoProvider))); - list.AddRange(GetAssembliesWithPartsInternal()); + list.AddRange(GetAssembliesWithPartsInternal().Select(i => new Tuple<Assembly, string>(i, null))); return list.ToList(); } @@ -1877,25 +1802,92 @@ namespace Emby.Server.Implementations /// Gets the plugin assemblies. /// </summary> /// <returns>IEnumerable{Assembly}.</returns> - private IEnumerable<Assembly> GetPluginAssemblies() + private List<Tuple<Assembly, string>> GetPluginAssemblies() + { + // Copy pre-installed plugins + var sourcePath = Path.Combine(ApplicationPaths.ApplicationResourcesPath, "plugins"); + CopyPlugins(sourcePath, ApplicationPaths.PluginsPath); + + return GetPluginAssemblies(ApplicationPaths.PluginsPath); + } + + private void CopyPlugins(string source, string target) { + List<string> files; + try { - return Directory.EnumerateFiles(ApplicationPaths.PluginsPath, "*.dll", SearchOption.TopDirectoryOnly) - .Where(EnablePlugin) + files = Directory.EnumerateFiles(source, "*.dll", SearchOption.TopDirectoryOnly) + .ToList(); + + } + catch (DirectoryNotFoundException) + { + return; + } + + if (files.Count == 0) + { + return; + } + + foreach (var sourceFile in files) + { + var filename = Path.GetFileName(sourceFile); + var targetFile = Path.Combine(target, filename); + + var targetFileExists = File.Exists(targetFile); + + if (!targetFileExists && ServerConfigurationManager.Configuration.UninstalledPlugins.Contains(filename, StringComparer.OrdinalIgnoreCase)) + { + continue; + } + + if (targetFileExists && GetDllVersion(targetFile) >= GetDllVersion(sourceFile)) + { + continue; + } + + Directory.CreateDirectory(target); + File.Copy(sourceFile, targetFile, true); + } + } + + private Version GetDllVersion(string path) + { + try + { + var result = Version.Parse(FileVersionInfo.GetVersionInfo(path).FileVersion); + + Logger.Info("File {0} has version {1}", path, result); + + return result; + } + catch (Exception ex) + { + Logger.ErrorException("Error getting version number from {0}", ex, path); + + return new Version(1, 0); + } + } + + private List<Tuple<Assembly, string>> GetPluginAssemblies(string path) + { + try + { + return FilterAssembliesToLoad(Directory.EnumerateFiles(path, "*.dll", SearchOption.TopDirectoryOnly)) .Select(LoadAssembly) .Where(a => a != null) .ToList(); } catch (DirectoryNotFoundException) { - return new List<Assembly>(); + return new List<Tuple<Assembly, string>>(); } } - private bool EnablePlugin(string path) + private IEnumerable<string> FilterAssembliesToLoad(IEnumerable<string> paths) { - var filename = Path.GetFileName(path); var exclude = new[] { @@ -1903,6 +1895,7 @@ namespace Emby.Server.Implementations "mbintros.dll", "embytv.dll", "Messenger.dll", + "Messages.dll", "MediaBrowser.Plugins.TvMazeProvider.dll", "MBBookshelf.dll", "MediaBrowser.Channels.Adult.YouJizz.dll", @@ -1927,10 +1920,49 @@ namespace Emby.Server.Implementations "MediaBrowser.Channels.HitboxTV.dll", "MediaBrowser.Channels.HockeyStreams.dll", "MediaBrowser.Plugins.ITV.dll", - "MediaBrowser.Plugins.Lastfm.dll" + "MediaBrowser.Plugins.Lastfm.dll", + "ServerRestart.dll", + "MediaBrowser.Plugins.NotifyMyAndroidNotifications.dll", + "MetadataViewer.dll" + }; + + var minRequiredVersions = new Dictionary<string, Version>(StringComparer.OrdinalIgnoreCase) + { + { "GameBrowser.dll", new Version(3, 1) }, + { "moviethemesongs.dll", new Version(1, 6) }, + { "themesongs.dll", new Version(1, 2) } }; - return !exclude.Contains(filename ?? string.Empty, StringComparer.OrdinalIgnoreCase); + return paths.Where(path => + { + var filename = Path.GetFileName(path); + if (exclude.Contains(filename ?? string.Empty, StringComparer.OrdinalIgnoreCase)) + { + return false; + } + + Version minRequiredVersion; + if (minRequiredVersions.TryGetValue(filename, out minRequiredVersion)) + { + try + { + var version = Version.Parse(FileVersionInfo.GetVersionInfo(path).FileVersion); + + if (version < minRequiredVersion) + { + Logger.Info("Not loading {0} {1} because the minimum supported version is {2}. Please update to the newer version", filename, version, minRequiredVersion); + return false; + } + } + catch (Exception ex) + { + Logger.ErrorException("Error getting version number from {0}", ex, path); + + return false; + } + } + return true; + }); } /// <summary> @@ -1947,16 +1979,13 @@ namespace Emby.Server.Implementations IsShuttingDown = IsShuttingDown, Version = ApplicationVersion.ToString(), WebSocketPortNumber = HttpPort, - FailedPluginAssemblies = FailedAssemblies.ToArray(), - InProgressInstallations = InstallationManager.CurrentInstallations.Select(i => i.Item1).ToArray(), CompletedInstallations = InstallationManager.CompletedInstallations.ToArray(), Id = SystemId, ProgramDataPath = ApplicationPaths.ProgramDataPath, LogPath = ApplicationPaths.LogDirectoryPath, - ItemsByNamePath = ApplicationPaths.ItemsByNamePath, + ItemsByNamePath = ApplicationPaths.InternalMetadataPath, InternalMetadataPath = ApplicationPaths.InternalMetadataPath, CachePath = ApplicationPaths.CachePath, - MacAddress = GetMacAddress(), HttpServerPortNumber = HttpPort, SupportsHttps = SupportsHttps, HttpsPortNumber = HttpsPort, @@ -1979,6 +2008,16 @@ namespace Emby.Server.Implementations }; } + public WakeOnLanInfo[] GetWakeOnLanInfo() + { + return NetworkManager.GetMacAddresses() + .Select(i => new WakeOnLanInfo + { + MacAddress = i + }) + .ToArray(); + } + public async Task<PublicSystemInfo> GetPublicSystemInfo(CancellationToken cancellationToken) { var localAddress = await GetLocalApiUrl(cancellationToken).ConfigureAwait(false); @@ -1998,7 +2037,7 @@ namespace Emby.Server.Implementations { get { - return SupportsHttps && (ServerConfigurationManager.Configuration.EnableHttps || ServerConfigurationManager.Configuration.RequireHttps); + return SupportsHttps && ServerConfigurationManager.Configuration.EnableHttps; } } @@ -2127,14 +2166,20 @@ namespace Emby.Server.Implementations return cachedResult; } + var logPing = false; + +#if DEBUG + logPing = true; +#endif + try { using (var response = await HttpClient.SendAsync(new HttpRequestOptions { Url = apiUrl, LogErrorResponseBody = false, - LogErrors = false, - LogRequest = false, + LogErrors = logPing, + LogRequest = logPing, TimeoutMs = 30000, BufferContent = false, @@ -2158,9 +2203,9 @@ namespace Emby.Server.Implementations Logger.Debug("Ping test result to {0}. Success: {1}", apiUrl, "Cancelled"); throw; } - catch + catch (Exception ex) { - Logger.Debug("Ping test result to {0}. Success: {1}", apiUrl, false); + Logger.Debug("Ping test result to {0}. Success: {1} {2}", apiUrl, false, ex.Message); _validAddressResults.AddOrUpdate(apiUrl, false, (k, v) => false); return false; @@ -2171,7 +2216,7 @@ namespace Emby.Server.Implementations { get { - return string.IsNullOrWhiteSpace(ServerConfigurationManager.Configuration.ServerName) + return string.IsNullOrEmpty(ServerConfigurationManager.Configuration.ServerName) ? Environment.MachineName : ServerConfigurationManager.Configuration.ServerName; } @@ -2182,23 +2227,6 @@ namespace Emby.Server.Implementations public int HttpsPort { get; private set; } /// <summary> - /// Gets the mac address. - /// </summary> - /// <returns>System.String.</returns> - private string GetMacAddress() - { - try - { - return NetworkManager.GetMacAddress(); - } - catch (Exception ex) - { - Logger.ErrorException("Error getting mac address", ex); - return null; - } - } - - /// <summary> /// Shuts down. /// </summary> public async Task Shutdown() @@ -2296,7 +2324,7 @@ namespace Emby.Server.Implementations try { var result = await new GithubUpdater(HttpClient, JsonSerializer).CheckForUpdateResult("MediaBrowser", - "Emby", + "Emby.Releases", ApplicationVersion, updateLevel, ReleaseAssetFilename, @@ -2370,7 +2398,7 @@ namespace Emby.Server.Implementations /// <returns>The hostname in <paramref name="externalDns"/></returns> private static string GetHostnameFromExternalDns(string externalDns) { - if (string.IsNullOrWhiteSpace(externalDns)) + if (string.IsNullOrEmpty(externalDns)) { return "localhost"; } @@ -2424,25 +2452,6 @@ namespace Emby.Server.Implementations { } - private void RegisterModules() - { - var moduleTypes = GetExportTypes<IDependencyModule>(); - - foreach (var type in moduleTypes) - { - try - { - var instance = Activator.CreateInstance(type) as IDependencyModule; - if (instance != null) - instance.BindDependencies(this); - } - catch (Exception ex) - { - Logger.ErrorException("Error setting up dependency bindings for " + type.Name, ex); - } - } - } - /// <summary> /// Called when [application updated]. /// </summary> @@ -2471,7 +2480,6 @@ namespace Emby.Server.Implementations _disposed = true; Dispose(true); - GC.SuppressFinalize(this); } } @@ -2507,19 +2515,50 @@ namespace Emby.Server.Implementations } } - void IDependencyContainer.RegisterSingleInstance<T>(T obj, bool manageLifetime) + private Dictionary<string, string> _values; + public string GetValue(string name) { - RegisterSingleInstance(obj, manageLifetime); - } + if (_values == null) + { + _values = LoadValues(); + } - void IDependencyContainer.RegisterSingleInstance<T>(Func<T> func) - { - RegisterSingleInstance(func); + string value; + + if (_values.TryGetValue(name, out value)) + { + return value; + } + + return null; } - void IDependencyContainer.Register(Type typeInterface, Type typeImplementation) + private Dictionary<string, string> LoadValues() { - Container.Register(typeInterface, typeImplementation); + Dictionary<string, string> values = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); + + using (var stream = typeof(ApplicationHost).Assembly.GetManifestResourceStream(typeof(ApplicationHost).Namespace + ".values.txt")) + { + using (var reader = new StreamReader(stream)) + { + while (!reader.EndOfStream) + { + var line = reader.ReadLine(); + if (string.IsNullOrEmpty(line)) + { + continue; + } + + var index = line.IndexOf('='); + if (index != -1) + { + values[line.Substring(0, index)] = line.Substring(index + 1); + } + } + } + } + + return values; } } diff --git a/Emby.Server.Implementations/Archiving/ZipClient.cs b/Emby.Server.Implementations/Archiving/ZipClient.cs index 32938e151..fd61f2617 100644 --- a/Emby.Server.Implementations/Archiving/ZipClient.cs +++ b/Emby.Server.Implementations/Archiving/ZipClient.cs @@ -3,6 +3,7 @@ using MediaBrowser.Model.IO; using SharpCompress.Archives.Rar; using SharpCompress.Archives.SevenZip; using SharpCompress.Archives.Tar; +using SharpCompress.Common; using SharpCompress.Readers; using SharpCompress.Readers.GZip; using SharpCompress.Readers.Zip; @@ -185,44 +186,5 @@ namespace Emby.Server.Implementations.Archiving } } } - - /// <summary> - /// Extracts all from rar. - /// </summary> - /// <param name="sourceFile">The source file.</param> - /// <param name="targetPath">The target path.</param> - /// <param name="overwriteExistingFiles">if set to <c>true</c> [overwrite existing files].</param> - public void ExtractAllFromRar(string sourceFile, string targetPath, bool overwriteExistingFiles) - { - using (var fileStream = _fileSystem.OpenRead(sourceFile)) - { - ExtractAllFromRar(fileStream, targetPath, overwriteExistingFiles); - } - } - - /// <summary> - /// Extracts all from rar. - /// </summary> - /// <param name="source">The source.</param> - /// <param name="targetPath">The target path.</param> - /// <param name="overwriteExistingFiles">if set to <c>true</c> [overwrite existing files].</param> - public void ExtractAllFromRar(Stream source, string targetPath, bool overwriteExistingFiles) - { - using (var archive = RarArchive.Open(source)) - { - using (var reader = archive.ExtractAllEntries()) - { - var options = new ExtractionOptions(); - options.ExtractFullPath = true; - - if (overwriteExistingFiles) - { - options.Overwrite = true; - } - - reader.WriteAllToDirectory(targetPath, options); - } - } - } } } diff --git a/Emby.Server.Implementations/Browser/BrowserLauncher.cs b/Emby.Server.Implementations/Browser/BrowserLauncher.cs index 71497f6bf..007f60a9b 100644 --- a/Emby.Server.Implementations/Browser/BrowserLauncher.cs +++ b/Emby.Server.Implementations/Browser/BrowserLauncher.cs @@ -13,7 +13,7 @@ namespace Emby.Server.Implementations.Browser /// </summary> /// <param name="page">The page.</param> /// <param name="appHost">The app host.</param> - public static void OpenDashboardPage(string page, IServerApplicationHost appHost) + private static void OpenDashboardPage(string page, IServerApplicationHost appHost) { var url = appHost.GetLocalApiUrl("localhost") + "/web/" + page; @@ -21,37 +21,15 @@ namespace Emby.Server.Implementations.Browser } /// <summary> - /// Opens the community. - /// </summary> - public static void OpenCommunity(IServerApplicationHost appHost) - { - OpenUrl(appHost, "http://emby.media/community"); - } - - public static void OpenEmbyPremiere(IServerApplicationHost appHost) - { - OpenDashboardPage("supporterkey.html", appHost); - } - - /// <summary> /// Opens the web client. /// </summary> /// <param name="appHost">The app host.</param> - public static void OpenWebClient(IServerApplicationHost appHost) + public static void OpenWebApp(IServerApplicationHost appHost) { OpenDashboardPage("index.html", appHost); } /// <summary> - /// Opens the dashboard. - /// </summary> - /// <param name="appHost">The app host.</param> - public static void OpenDashboard(IServerApplicationHost appHost) - { - OpenDashboardPage("dashboard.html", appHost); - } - - /// <summary> /// Opens the URL. /// </summary> /// <param name="url">The URL.</param> diff --git a/Emby.Server.Implementations/Channels/ChannelConfigurations.cs b/Emby.Server.Implementations/Channels/ChannelConfigurations.cs deleted file mode 100644 index ef0973e7f..000000000 --- a/Emby.Server.Implementations/Channels/ChannelConfigurations.cs +++ /dev/null @@ -1,29 +0,0 @@ -using MediaBrowser.Common.Configuration; -using MediaBrowser.Model.Configuration; -using System.Collections.Generic; - -namespace Emby.Server.Implementations.Channels -{ - public static class ChannelConfigurationExtension - { - public static ChannelOptions GetChannelsConfiguration(this IConfigurationManager manager) - { - return manager.GetConfiguration<ChannelOptions>("channels"); - } - } - - public class ChannelConfigurationFactory : IConfigurationFactory - { - public IEnumerable<ConfigurationStore> GetConfigurations() - { - return new List<ConfigurationStore> - { - new ConfigurationStore - { - Key = "channels", - ConfigurationType = typeof (ChannelOptions) - } - }; - } - } -} diff --git a/Emby.Server.Implementations/Channels/ChannelDynamicMediaSourceProvider.cs b/Emby.Server.Implementations/Channels/ChannelDynamicMediaSourceProvider.cs index 7be4101c8..8448d3640 100644 --- a/Emby.Server.Implementations/Channels/ChannelDynamicMediaSourceProvider.cs +++ b/Emby.Server.Implementations/Channels/ChannelDynamicMediaSourceProvider.cs @@ -18,24 +18,17 @@ namespace Emby.Server.Implementations.Channels _channelManager = (ChannelManager)channelManager; } - public Task<IEnumerable<MediaSourceInfo>> GetMediaSources(IHasMediaSources item, CancellationToken cancellationToken) + public Task<IEnumerable<MediaSourceInfo>> GetMediaSources(BaseItem item, CancellationToken cancellationToken) { - var baseItem = (BaseItem) item; - - if (baseItem.SourceType == SourceType.Channel) + if (item.SourceType == SourceType.Channel) { - return _channelManager.GetDynamicMediaSources(baseItem, cancellationToken); + return _channelManager.GetDynamicMediaSources(item, cancellationToken); } return Task.FromResult<IEnumerable<MediaSourceInfo>>(new List<MediaSourceInfo>()); } - public Task<Tuple<MediaSourceInfo, IDirectStreamProvider>> OpenMediaSource(string openToken, bool allowLiveStreamProbe, CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } - - public Task CloseMediaSource(string liveStreamId) + public Task<ILiveStream> OpenMediaSource(string openToken, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken) { throw new NotImplementedException(); } diff --git a/Emby.Server.Implementations/Channels/ChannelImageProvider.cs b/Emby.Server.Implementations/Channels/ChannelImageProvider.cs index 0c363c585..a6643e83c 100644 --- a/Emby.Server.Implementations/Channels/ChannelImageProvider.cs +++ b/Emby.Server.Implementations/Channels/ChannelImageProvider.cs @@ -18,12 +18,12 @@ namespace Emby.Server.Implementations.Channels _channelManager = channelManager; } - public IEnumerable<ImageType> GetSupportedImages(IHasMetadata item) + public IEnumerable<ImageType> GetSupportedImages(BaseItem item) { return GetChannel(item).GetSupportedChannelImages(); } - public Task<DynamicImageResponse> GetImage(IHasMetadata item, ImageType type, CancellationToken cancellationToken) + public Task<DynamicImageResponse> GetImage(BaseItem item, ImageType type, CancellationToken cancellationToken) { var channel = GetChannel(item); @@ -35,19 +35,19 @@ namespace Emby.Server.Implementations.Channels get { return "Channel Image Provider"; } } - public bool Supports(IHasMetadata item) + public bool Supports(BaseItem item) { return item is Channel; } - private IChannel GetChannel(IHasMetadata item) + private IChannel GetChannel(BaseItem item) { var channel = (Channel)item; return ((ChannelManager)_channelManager).GetChannelProvider(channel); } - public bool HasChanged(IHasMetadata item, IDirectoryService directoryService) + public bool HasChanged(BaseItem item, IDirectoryService directoryService) { return GetSupportedImages(item).Any(i => !item.HasImage(i)); } diff --git a/Emby.Server.Implementations/Channels/ChannelManager.cs b/Emby.Server.Implementations/Channels/ChannelManager.cs index c566ca25b..e832c7c6f 100644 --- a/Emby.Server.Implementations/Channels/ChannelManager.cs +++ b/Emby.Server.Implementations/Channels/ChannelManager.cs @@ -51,7 +51,6 @@ namespace Emby.Server.Implementations.Channels private readonly IProviderManager _providerManager; private readonly ILocalizationManager _localization; - private readonly ConcurrentDictionary<Guid, bool> _refreshedItems = new ConcurrentDictionary<Guid, bool>(); public ChannelManager(IUserManager userManager, IDtoService dtoService, ILibraryManager libraryManager, ILogger logger, IServerConfigurationManager config, IFileSystem fileSystem, IUserDataManager userDataManager, IJsonSerializer jsonSerializer, ILocalizationManager localization, IHttpClient httpClient, IProviderManager providerManager) { @@ -72,7 +71,7 @@ namespace Emby.Server.Implementations.Channels { get { - return TimeSpan.FromHours(6); + return TimeSpan.FromHours(3); } } @@ -81,6 +80,51 @@ namespace Emby.Server.Implementations.Channels Channels = channels.ToArray(); } + public bool EnableMediaSourceDisplay(BaseItem item) + { + var internalChannel = _libraryManager.GetItemById(item.ChannelId); + var channel = Channels.FirstOrDefault(i => GetInternalChannelId(i.Name).Equals(internalChannel.Id)); + + return !(channel is IDisableMediaSourceDisplay); + } + + public bool CanDelete(BaseItem item) + { + var internalChannel = _libraryManager.GetItemById(item.ChannelId); + var channel = Channels.FirstOrDefault(i => GetInternalChannelId(i.Name).Equals(internalChannel.Id)); + + var supportsDelete = channel as ISupportsDelete; + return supportsDelete != null && supportsDelete.CanDelete(item); + } + + public bool EnableMediaProbe(BaseItem item) + { + var internalChannel = _libraryManager.GetItemById(item.ChannelId); + var channel = Channels.FirstOrDefault(i => GetInternalChannelId(i.Name).Equals(internalChannel.Id)); + + return channel is ISupportsMediaProbe; + } + + public Task DeleteItem(BaseItem item) + { + var internalChannel = _libraryManager.GetItemById(item.ChannelId); + if (internalChannel == null) + { + throw new ArgumentException(); + } + + var channel = Channels.FirstOrDefault(i => GetInternalChannelId(i.Name).Equals(internalChannel.Id)); + + var supportsDelete = channel as ISupportsDelete; + + if (supportsDelete == null) + { + throw new ArgumentException(); + } + + return supportsDelete.DeleteItem(item.ExternalId, CancellationToken.None); + } + private IEnumerable<IChannel> GetAllChannels() { return Channels @@ -92,9 +136,9 @@ namespace Emby.Server.Implementations.Channels return GetAllChannels().Select(i => GetInternalChannelId(i.Name)); } - public Task<QueryResult<Channel>> GetChannelsInternal(ChannelQuery query, CancellationToken cancellationToken) + public QueryResult<Channel> GetChannelsInternal(ChannelQuery query) { - var user = string.IsNullOrWhiteSpace(query.UserId) + var user = query.UserId.Equals(Guid.Empty) ? null : _userManager.GetUserById(query.UserId); @@ -103,6 +147,25 @@ namespace Emby.Server.Implementations.Channels .OrderBy(i => i.SortName) .ToList(); + if (query.IsRecordingsFolder.HasValue) + { + var val = query.IsRecordingsFolder.Value; + channels = channels.Where(i => + { + try + { + var hasAttributes = GetChannelProvider(i) as IHasFolderAttributes; + + return (hasAttributes != null && hasAttributes.Attributes.Contains("Recordings", StringComparer.OrdinalIgnoreCase)) == val; + } + catch + { + return false; + } + + }).ToList(); + } + if (query.SupportsLatestItems.HasValue) { var val = query.SupportsLatestItems.Value; @@ -119,6 +182,23 @@ namespace Emby.Server.Implementations.Channels }).ToList(); } + + if (query.SupportsMediaDeletion.HasValue) + { + var val = query.SupportsMediaDeletion.Value; + channels = channels.Where(i => + { + try + { + return GetChannelProvider(i) is ISupportsDelete == val; + } + catch + { + return false; + } + + }).ToList(); + } if (query.IsFavorite.HasValue) { var val = query.IsFavorite.Value; @@ -161,22 +241,29 @@ namespace Emby.Server.Implementations.Channels var returnItems = all.ToArray(all.Count); - var result = new QueryResult<Channel> + if (query.RefreshLatestChannelItems) + { + foreach (var item in returnItems) + { + var task = RefreshLatestChannelItems(GetChannelProvider(item), CancellationToken.None); + Task.WaitAll(task); + } + } + + return new QueryResult<Channel> { Items = returnItems, TotalRecordCount = totalCount }; - - return Task.FromResult(result); } - public async Task<QueryResult<BaseItemDto>> GetChannels(ChannelQuery query, CancellationToken cancellationToken) + public QueryResult<BaseItemDto> GetChannels(ChannelQuery query) { - var user = string.IsNullOrWhiteSpace(query.UserId) + var user = query.UserId.Equals(Guid.Empty) ? null : _userManager.GetUserById(query.UserId); - var internalResult = await GetChannelsInternal(query, cancellationToken).ConfigureAwait(false); + var internalResult = GetChannelsInternal(query); var dtoOptions = new DtoOptions() { @@ -195,8 +282,6 @@ namespace Emby.Server.Implementations.Channels public async Task RefreshChannels(IProgress<double> progress, CancellationToken cancellationToken) { - _refreshedItems.Clear(); - var allChannelsList = GetAllChannels().ToList(); var numComplete = 0; @@ -230,7 +315,7 @@ namespace Emby.Server.Implementations.Channels private Channel GetChannelEntity(IChannel channel) { - var item = GetChannel(GetInternalChannelId(channel.Name).ToString("N")); + var item = GetChannel(GetInternalChannelId(channel.Name)); if (item == null) { @@ -240,23 +325,23 @@ namespace Emby.Server.Implementations.Channels return item; } - private List<ChannelMediaInfo> GetSavedMediaSources(BaseItem item) + private List<MediaSourceInfo> GetSavedMediaSources(BaseItem item) { - var path = Path.Combine(item.GetInternalMetadataPath(), "channelmediasources.json"); + var path = Path.Combine(item.GetInternalMetadataPath(), "channelmediasourceinfos.json"); try { - return _jsonSerializer.DeserializeFromFile<List<ChannelMediaInfo>>(path) ?? new List<ChannelMediaInfo>(); + return _jsonSerializer.DeserializeFromFile<List<MediaSourceInfo>>(path) ?? new List<MediaSourceInfo>(); } catch { - return new List<ChannelMediaInfo>(); + return new List<MediaSourceInfo>(); } } - private void SaveMediaSources(BaseItem item, List<ChannelMediaInfo> mediaSources) + private void SaveMediaSources(BaseItem item, List<MediaSourceInfo> mediaSources) { - var path = Path.Combine(item.GetInternalMetadataPath(), "channelmediasources.json"); + var path = Path.Combine(item.GetInternalMetadataPath(), "channelmediasourceinfos.json"); if (mediaSources == null || mediaSources.Count == 0) { @@ -278,10 +363,10 @@ namespace Emby.Server.Implementations.Channels public IEnumerable<MediaSourceInfo> GetStaticMediaSources(BaseItem item, CancellationToken cancellationToken) { - IEnumerable<ChannelMediaInfo> results = GetSavedMediaSources(item); + IEnumerable<MediaSourceInfo> results = GetSavedMediaSources(item); - return SortMediaInfoResults(results) - .Select(i => GetMediaSource(item, i)) + return results + .Select(i => NormalizeMediaSource(item, i)) .ToList(); } @@ -292,7 +377,7 @@ namespace Emby.Server.Implementations.Channels var requiresCallback = channelPlugin as IRequiresMediaInfoCallback; - IEnumerable<ChannelMediaInfo> results; + IEnumerable<MediaSourceInfo> results; if (requiresCallback != null) { @@ -301,20 +386,20 @@ namespace Emby.Server.Implementations.Channels } else { - results = new List<ChannelMediaInfo>(); + results = new List<MediaSourceInfo>(); } - return SortMediaInfoResults(results) - .Select(i => GetMediaSource(item, i)) + return results + .Select(i => NormalizeMediaSource(item, i)) .ToList(); } - private readonly ConcurrentDictionary<string, Tuple<DateTime, List<ChannelMediaInfo>>> _channelItemMediaInfo = - new ConcurrentDictionary<string, Tuple<DateTime, List<ChannelMediaInfo>>>(); + private readonly ConcurrentDictionary<string, Tuple<DateTime, List<MediaSourceInfo>>> _channelItemMediaInfo = + new ConcurrentDictionary<string, Tuple<DateTime, List<MediaSourceInfo>>>(); - private async Task<IEnumerable<ChannelMediaInfo>> GetChannelItemMediaSourcesInternal(IRequiresMediaInfoCallback channel, string id, CancellationToken cancellationToken) + private async Task<IEnumerable<MediaSourceInfo>> GetChannelItemMediaSourcesInternal(IRequiresMediaInfoCallback channel, string id, CancellationToken cancellationToken) { - Tuple<DateTime, List<ChannelMediaInfo>> cachedInfo; + Tuple<DateTime, List<MediaSourceInfo>> cachedInfo; if (_channelItemMediaInfo.TryGetValue(id, out cachedInfo)) { @@ -328,56 +413,24 @@ namespace Emby.Server.Implementations.Channels .ConfigureAwait(false); var list = mediaInfo.ToList(); - var item2 = new Tuple<DateTime, List<ChannelMediaInfo>>(DateTime.UtcNow, list); + var item2 = new Tuple<DateTime, List<MediaSourceInfo>>(DateTime.UtcNow, list); _channelItemMediaInfo.AddOrUpdate(id, item2, (key, oldValue) => item2); return list; } - private MediaSourceInfo GetMediaSource(BaseItem item, ChannelMediaInfo info) + private MediaSourceInfo NormalizeMediaSource(BaseItem item, MediaSourceInfo info) { - var source = info.ToMediaSource(item.Id); + info.RunTimeTicks = info.RunTimeTicks ?? item.RunTimeTicks; - source.RunTimeTicks = source.RunTimeTicks ?? item.RunTimeTicks; - - return source; - } - - private IEnumerable<ChannelMediaInfo> SortMediaInfoResults(IEnumerable<ChannelMediaInfo> channelMediaSources) - { - var list = channelMediaSources.ToList(); - - var options = _config.GetChannelsConfiguration(); - - var width = options.PreferredStreamingWidth; - - if (width.HasValue) - { - var val = width.Value; - - var res = list - .OrderBy(i => i.Width.HasValue && i.Width.Value <= val ? 0 : 1) - .ThenBy(i => Math.Abs((i.Width ?? 0) - val)) - .ThenByDescending(i => i.Width ?? 0) - .ThenBy(list.IndexOf) - .ToList(); - - - return res; - } - - return list - .OrderByDescending(i => i.Width ?? 0) - .ThenBy(list.IndexOf); + return info; } private async Task<Channel> GetChannel(IChannel channelInfo, CancellationToken cancellationToken) { - var parentFolder = GetInternalChannelFolder(cancellationToken); - var parentFolderId = parentFolder.Id; + var parentFolderId = Guid.Empty; var id = GetInternalChannelId(channelInfo.Name); - var idString = id.ToString("N"); var path = Channel.GetInternalMetadataPath(_config.ApplicationPaths.InternalMetadataPath, id); @@ -405,11 +458,11 @@ namespace Emby.Server.Implementations.Channels } item.Path = path; - if (!string.Equals(item.ChannelId, idString, StringComparison.OrdinalIgnoreCase)) + if (!item.ChannelId.Equals(id)) { forceUpdate = true; } - item.ChannelId = idString; + item.ChannelId = id; if (item.ParentId != parentFolderId) { @@ -419,25 +472,24 @@ namespace Emby.Server.Implementations.Channels item.OfficialRating = GetOfficialRating(channelInfo.ParentalRating); item.Overview = channelInfo.Description; - item.HomePageUrl = channelInfo.HomePageUrl; if (string.IsNullOrWhiteSpace(item.Name)) { item.Name = channelInfo.Name; } - item.OnMetadataChanged(); - if (isNew) { - _libraryManager.CreateItem(item, cancellationToken); + item.OnMetadataChanged(); + _libraryManager.CreateItem(item, null); } - else if (forceUpdate) + + await item.RefreshMetadata(new MetadataRefreshOptions(_fileSystem) { - item.UpdateToRepository(ItemUpdateType.None, cancellationToken); - } + ForceSave = !isNew && forceUpdate + + }, cancellationToken); - await item.RefreshMetadata(new MetadataRefreshOptions(_fileSystem), cancellationToken); return item; } @@ -458,6 +510,11 @@ namespace Emby.Server.Implementations.Channels } } + public Channel GetChannel(Guid id) + { + return _libraryManager.GetItemById(id) as Channel; + } + public Channel GetChannel(string id) { return _libraryManager.GetItemById(id) as Channel; @@ -468,14 +525,14 @@ namespace Emby.Server.Implementations.Channels return _libraryManager.GetItemIds(new InternalItemsQuery { IncludeItemTypes = new[] { typeof(Channel).Name }, - OrderBy = new Tuple<string, SortOrder>[] { new Tuple<string, SortOrder>(ItemSortBy.SortName, SortOrder.Ascending) } + OrderBy = new ValueTuple<string, SortOrder>[] { new ValueTuple<string, SortOrder>(ItemSortBy.SortName, SortOrder.Ascending) } }).Select(i => GetChannelFeatures(i.ToString("N"))).ToArray(); } public ChannelFeatures GetChannelFeatures(string id) { - if (string.IsNullOrWhiteSpace(id)) + if (string.IsNullOrEmpty(id)) { throw new ArgumentNullException("id"); } @@ -486,13 +543,8 @@ namespace Emby.Server.Implementations.Channels return GetChannelFeaturesDto(channel, channelProvider, channelProvider.GetChannelFeatures()); } - public bool SupportsSync(string channelId) + public bool SupportsExternalTransfer(Guid channelId) { - if (string.IsNullOrWhiteSpace(channelId)) - { - throw new ArgumentNullException("channelId"); - } - //var channel = GetChannel(channelId); var channelProvider = GetChannelProvider(channelId); @@ -503,7 +555,6 @@ namespace Emby.Server.Implementations.Channels IChannel provider, InternalChannelFeatures features) { - var isIndexable = provider is IIndexableChannel; var supportsLatest = provider is ISupportsLatestMedia; return new ChannelFeatures @@ -518,57 +569,28 @@ namespace Emby.Server.Implementations.Channels SupportsLatestMedia = supportsLatest, Name = channel.Name, Id = channel.Id.ToString("N"), - SupportsContentDownloading = features.SupportsContentDownloading && (isIndexable || supportsLatest), + SupportsContentDownloading = features.SupportsContentDownloading, AutoRefreshLevels = features.AutoRefreshLevels }; } private Guid GetInternalChannelId(string name) { - if (string.IsNullOrWhiteSpace(name)) + if (string.IsNullOrEmpty(name)) { throw new ArgumentNullException("name"); } return _libraryManager.GetNewItemId("Channel " + name, typeof(Channel)); } - public async Task<QueryResult<BaseItemDto>> GetLatestChannelItems(AllChannelMediaQuery query, CancellationToken cancellationToken) + public async Task<QueryResult<BaseItemDto>> GetLatestChannelItems(InternalItemsQuery query, CancellationToken cancellationToken) { - var user = string.IsNullOrWhiteSpace(query.UserId) - ? null - : _userManager.GetUserById(query.UserId); - - var limit = query.Limit; - - // See below about parental control - if (user != null) - { - query.StartIndex = null; - query.Limit = null; - } - var internalResult = await GetLatestChannelItemsInternal(query, cancellationToken).ConfigureAwait(false); var items = internalResult.Items; var totalRecordCount = internalResult.TotalRecordCount; - // Supporting parental control is a hack because it has to be done after querying the remote data source - // This will get screwy if apps try to page, so limit to 10 results in an attempt to always keep them on the first page - if (user != null) - { - items = items.Where(i => i.IsVisible(user)) - .Take(limit ?? 10) - .ToArray(); - - totalRecordCount = items.Length; - } - - var dtoOptions = new DtoOptions() - { - Fields = query.Fields - }; - - var returnItems = _dtoService.GetBaseItemDtos(items, dtoOptions, user); + var returnItems = _dtoService.GetBaseItemDtos(items, query.DtoOptions, query.User); var result = new QueryResult<BaseItemDto> { @@ -579,407 +601,150 @@ namespace Emby.Server.Implementations.Channels return result; } - public async Task<QueryResult<BaseItem>> GetLatestChannelItemsInternal(AllChannelMediaQuery query, CancellationToken cancellationToken) + public async Task<QueryResult<BaseItem>> GetLatestChannelItemsInternal(InternalItemsQuery query, CancellationToken cancellationToken) { - var user = string.IsNullOrWhiteSpace(query.UserId) - ? null - : _userManager.GetUserById(query.UserId); - - if (!string.IsNullOrWhiteSpace(query.UserId) && user == null) - { - throw new ArgumentException("User not found."); - } - - var channels = GetAllChannels(); + var channels = GetAllChannels().Where(i => i is ISupportsLatestMedia).ToArray(); if (query.ChannelIds.Length > 0) { // Avoid implicitly captured closure var ids = query.ChannelIds; channels = channels - .Where(i => ids.Contains(GetInternalChannelId(i.Name).ToString("N"))) + .Where(i => ids.Contains(GetInternalChannelId(i.Name))) .ToArray(); } - // Avoid implicitly captured closure - var userId = query.UserId; - - var tasks = channels - .Select(async i => - { - var indexable = i as ISupportsLatestMedia; - - if (indexable != null) - { - try - { - var result = await GetLatestItems(indexable, i, userId, cancellationToken).ConfigureAwait(false); - - var resultItems = result.ToList(); - - return new Tuple<IChannel, ChannelItemResult>(i, new ChannelItemResult - { - Items = resultItems, - TotalRecordCount = resultItems.Count - }); - } - catch (Exception ex) - { - _logger.ErrorException("Error getting all media from {0}", ex, i.Name); - } - } - return new Tuple<IChannel, ChannelItemResult>(i, new ChannelItemResult()); - }); - - var results = await Task.WhenAll(tasks).ConfigureAwait(false); - - var totalCount = results.Length; - - IEnumerable<Tuple<IChannel, ChannelItemInfo>> items = results - .SelectMany(i => i.Item2.Items.Select(m => new Tuple<IChannel, ChannelItemInfo>(i.Item1, m))); - - if (query.ContentTypes.Length > 0) - { - // Avoid implicitly captured closure - var contentTypes = query.ContentTypes; - - items = items.Where(i => contentTypes.Contains(i.Item2.ContentType)); - } - if (query.ExtraTypes.Length > 0) + if (channels.Length == 0) { - // Avoid implicitly captured closure - var contentTypes = query.ExtraTypes; - - items = items.Where(i => contentTypes.Contains(i.Item2.ExtraType)); + return new QueryResult<BaseItem>(); } - // Avoid implicitly captured closure - var token = cancellationToken; - var internalItems = items.Select(i => - { - var channelProvider = i.Item1; - var internalChannelId = GetInternalChannelId(channelProvider.Name); - return GetChannelItemEntity(i.Item2, channelProvider, internalChannelId, token); - }).ToArray(); - - internalItems = ApplyFilters(internalItems, query.Filters, user).ToArray(); - RefreshIfNeeded(internalItems); - - if (query.StartIndex.HasValue) + foreach (var channel in channels) { - internalItems = internalItems.Skip(query.StartIndex.Value).ToArray(); - } - if (query.Limit.HasValue) - { - internalItems = internalItems.Take(query.Limit.Value).ToArray(); + await RefreshLatestChannelItems(channel, cancellationToken).ConfigureAwait(false); } - return new QueryResult<BaseItem> - { - TotalRecordCount = totalCount, - Items = internalItems - }; - } + query.IsFolder = false; - private async Task<IEnumerable<ChannelItemInfo>> GetLatestItems(ISupportsLatestMedia indexable, IChannel channel, string userId, CancellationToken cancellationToken) - { - var cacheLength = CacheLength; - var cachePath = GetChannelDataCachePath(channel, userId, "channelmanager-latest", null, false); + // hack for trailers, figure out a better way later + var sortByPremiereDate = channels.Length == 1 && channels[0].GetType().Name.IndexOf("Trailer") != -1; - try + if (sortByPremiereDate) { - if (_fileSystem.GetLastWriteTimeUtc(cachePath).Add(cacheLength) > DateTime.UtcNow) + query.OrderBy = new [] { - return _jsonSerializer.DeserializeFromFile<List<ChannelItemInfo>>(cachePath); - } - } - catch (FileNotFoundException) - { - - } - catch (IOException) - { - - } - - await _resourcePool.WaitAsync(cancellationToken).ConfigureAwait(false); - - try - { - try - { - if (_fileSystem.GetLastWriteTimeUtc(cachePath).Add(cacheLength) > DateTime.UtcNow) - { - return _jsonSerializer.DeserializeFromFile<List<ChannelItemInfo>>(cachePath); - } - } - catch (FileNotFoundException) - { - - } - catch (IOException) - { - - } - - var result = await indexable.GetLatestMedia(new ChannelLatestMediaSearch - { - UserId = userId - - }, cancellationToken).ConfigureAwait(false); - - var resultItems = result.ToList(); - - CacheResponse(resultItems, cachePath); - - return resultItems; - } - finally - { - _resourcePool.Release(); + new ValueTuple<string, SortOrder>(ItemSortBy.PremiereDate, SortOrder.Descending), + new ValueTuple<string, SortOrder>(ItemSortBy.ProductionYear, SortOrder.Descending), + new ValueTuple<string, SortOrder>(ItemSortBy.DateCreated, SortOrder.Descending) + }; } - } - - public async Task<QueryResult<BaseItem>> GetAllMediaInternal(AllChannelMediaQuery query, CancellationToken cancellationToken) - { - var channels = GetAllChannels(); - - if (query.ChannelIds.Length > 0) + else { - // Avoid implicitly captured closure - var ids = query.ChannelIds; - channels = channels - .Where(i => ids.Contains(GetInternalChannelId(i.Name).ToString("N"))) - .ToArray(); - } - - var tasks = channels - .Select(async i => + query.OrderBy = new [] { - var indexable = i as IIndexableChannel; - - if (indexable != null) - { - try - { - var result = await GetAllItems(indexable, i, new InternalAllChannelMediaQuery - { - UserId = query.UserId, - ContentTypes = query.ContentTypes, - ExtraTypes = query.ExtraTypes, - TrailerTypes = query.TrailerTypes - - }, cancellationToken).ConfigureAwait(false); - - return new Tuple<IChannel, ChannelItemResult>(i, result); - } - catch (Exception ex) - { - _logger.ErrorException("Error getting all media from {0}", ex, i.Name); - } - } - return new Tuple<IChannel, ChannelItemResult>(i, new ChannelItemResult()); - }); - - var results = await Task.WhenAll(tasks).ConfigureAwait(false); - - var totalCount = results.Length; - - IEnumerable<Tuple<IChannel, ChannelItemInfo>> items = results - .SelectMany(i => i.Item2.Items.Select(m => new Tuple<IChannel, ChannelItemInfo>(i.Item1, m))) - .OrderBy(i => i.Item2.Name); - - if (query.StartIndex.HasValue) - { - items = items.Skip(query.StartIndex.Value); - } - if (query.Limit.HasValue) - { - items = items.Take(query.Limit.Value); + new ValueTuple<string, SortOrder>(ItemSortBy.DateCreated, SortOrder.Descending) + }; } - // Avoid implicitly captured closure - var token = cancellationToken; - var internalItems = items.Select(i => - { - var channelProvider = i.Item1; - var internalChannelId = GetInternalChannelId(channelProvider.Name); - return GetChannelItemEntity(i.Item2, channelProvider, internalChannelId, token); - }).ToArray(); - - return new QueryResult<BaseItem> - { - TotalRecordCount = totalCount, - Items = internalItems - }; + return _libraryManager.GetItemsResult(query); } - public async Task<QueryResult<BaseItemDto>> GetAllMedia(AllChannelMediaQuery query, CancellationToken cancellationToken) + private async Task RefreshLatestChannelItems(IChannel channel, CancellationToken cancellationToken) { - var user = string.IsNullOrWhiteSpace(query.UserId) - ? null - : _userManager.GetUserById(query.UserId); - - var internalResult = await GetAllMediaInternal(query, cancellationToken).ConfigureAwait(false); - - RefreshIfNeeded(internalResult.Items); - - var dtoOptions = new DtoOptions() - { - Fields = query.Fields - }; + var internalChannel = await GetChannel(channel, cancellationToken); - var returnItems = _dtoService.GetBaseItemDtos(internalResult.Items, dtoOptions, user); - - var result = new QueryResult<BaseItemDto> - { - Items = returnItems, - TotalRecordCount = internalResult.TotalRecordCount - }; + var query = new InternalItemsQuery(); + query.Parent = internalChannel; + query.EnableTotalRecordCount = false; + query.ChannelIds = new Guid[] { internalChannel.Id }; - return result; - } - - private async Task<ChannelItemResult> GetAllItems(IIndexableChannel indexable, IChannel channel, InternalAllChannelMediaQuery query, CancellationToken cancellationToken) - { - var cacheLength = CacheLength; - var folderId = _jsonSerializer.SerializeToString(query).GetMD5().ToString("N"); - var cachePath = GetChannelDataCachePath(channel, query.UserId, folderId, null, false); - - try - { - if (_fileSystem.GetLastWriteTimeUtc(cachePath).Add(cacheLength) > DateTime.UtcNow) - { - return _jsonSerializer.DeserializeFromFile<ChannelItemResult>(cachePath); - } - } - catch (FileNotFoundException) - { + var result = await GetChannelItemsInternal(query, new SimpleProgress<double>(), cancellationToken).ConfigureAwait(false); - } - catch (IOException) + foreach (var item in result.Items) { + var folder = item as Folder; - } - - await _resourcePool.WaitAsync(cancellationToken).ConfigureAwait(false); - - try - { - try + if (folder != null) { - if (_fileSystem.GetLastWriteTimeUtc(cachePath).Add(cacheLength) > DateTime.UtcNow) + await GetChannelItemsInternal(new InternalItemsQuery { - return _jsonSerializer.DeserializeFromFile<ChannelItemResult>(cachePath); - } - } - catch (FileNotFoundException) - { - - } - catch (IOException) - { + Parent = folder, + EnableTotalRecordCount = false, + ChannelIds = new Guid[] { internalChannel.Id } + }, new SimpleProgress<double>(), cancellationToken).ConfigureAwait(false); } - - var result = await indexable.GetAllMedia(query, cancellationToken).ConfigureAwait(false); - - CacheResponse(result, cachePath); - - return result; - } - finally - { - _resourcePool.Release(); } } - public async Task<QueryResult<BaseItem>> GetChannelItemsInternal(ChannelItemQuery query, IProgress<double> progress, CancellationToken cancellationToken) + public async Task<QueryResult<BaseItem>> GetChannelItemsInternal(InternalItemsQuery query, IProgress<double> progress, CancellationToken cancellationToken) { // Get the internal channel entity - var channel = GetChannel(query.ChannelId); + var channel = GetChannel(query.ChannelIds[0]); // Find the corresponding channel provider plugin var channelProvider = GetChannelProvider(channel); - var channelInfo = channelProvider.GetChannelFeatures(); - - int? providerStartIndex = null; - int? providerLimit = null; - - if (channelInfo.MaxPageSize.HasValue) - { - providerStartIndex = query.StartIndex; - - if (query.Limit.HasValue && query.Limit.Value > channelInfo.MaxPageSize.Value) - { - query.Limit = Math.Min(query.Limit.Value, channelInfo.MaxPageSize.Value); - } - providerLimit = query.Limit; - - // This will cause some providers to fail - if (providerLimit == 0) - { - providerLimit = 1; - } - } - - var user = string.IsNullOrWhiteSpace(query.UserId) - ? null - : _userManager.GetUserById(query.UserId); + var user = query.User; ChannelItemSortField? sortField = null; - ChannelItemSortField parsedField; var sortDescending = false; - if (query.OrderBy.Length == 1 && - Enum.TryParse(query.OrderBy[0].Item1, true, out parsedField)) - { - sortField = parsedField; - sortDescending = query.OrderBy[0].Item2 == SortOrder.Descending; - } + var parentItem = !query.ParentId.Equals(Guid.Empty) ? _libraryManager.GetItemById(query.ParentId) : channel; var itemsResult = await GetChannelItems(channelProvider, user, - query.FolderId, - providerStartIndex, - providerLimit, + parentItem is Channel ? null : parentItem.ExternalId, sortField, sortDescending, cancellationToken) .ConfigureAwait(false); - var providerTotalRecordCount = providerLimit.HasValue ? itemsResult.TotalRecordCount : null; + if (query.ParentId.Equals(Guid.Empty)) + { + query.Parent = channel; + } + query.ChannelIds = Array.Empty<Guid>(); - var internalItems = itemsResult.Items.Select(i => GetChannelItemEntity(i, channelProvider, channel.Id, cancellationToken)).ToArray(); + // Not yet sure why this is causing a problem + query.GroupByPresentationUniqueKey = false; - if (user != null) + //_logger.Debug("GetChannelItemsInternal"); + + // null if came from cache + if (itemsResult != null) { - internalItems = internalItems.Where(i => i.IsVisible(user)).ToArray(); + var internalItems = itemsResult.Items + .Select(i => GetChannelItemEntity(i, channelProvider, channel.Id, parentItem, cancellationToken)) + .ToArray(); + + var existingIds = _libraryManager.GetItemIds(query); + var deadIds = existingIds.Except(internalItems.Select(i => i.Id)) + .ToArray(); - if (providerTotalRecordCount.HasValue) + foreach (var deadId in deadIds) { - providerTotalRecordCount = providerTotalRecordCount.Value; + var deadItem = _libraryManager.GetItemById(deadId); + if (deadItem != null) + { + _libraryManager.DeleteItem(deadItem, new DeleteOptions + { + DeleteFileLocation = false, + DeleteFromExternalProvider = false + + }, parentItem, false); + } } } - return GetReturnItems(internalItems, providerTotalRecordCount, user, query); + return _libraryManager.GetItemsResult(query); } - public async Task<QueryResult<BaseItemDto>> GetChannelItems(ChannelItemQuery query, CancellationToken cancellationToken) + public async Task<QueryResult<BaseItemDto>> GetChannelItems(InternalItemsQuery query, CancellationToken cancellationToken) { - var user = string.IsNullOrWhiteSpace(query.UserId) - ? null - : _userManager.GetUserById(query.UserId); - var internalResult = await GetChannelItemsInternal(query, new SimpleProgress<double>(), cancellationToken).ConfigureAwait(false); - var dtoOptions = new DtoOptions() - { - Fields = query.Fields - }; - - var returnItems = _dtoService.GetBaseItemDtos(internalResult.Items, dtoOptions, user); + var returnItems = _dtoService.GetBaseItemDtos(internalResult.Items, query.DtoOptions, query.User); var result = new QueryResult<BaseItemDto> { @@ -993,29 +758,24 @@ namespace Emby.Server.Implementations.Channels private readonly SemaphoreSlim _resourcePool = new SemaphoreSlim(1, 1); private async Task<ChannelItemResult> GetChannelItems(IChannel channel, User user, - string folderId, - int? startIndex, - int? limit, + string externalFolderId, ChannelItemSortField? sortField, bool sortDescending, CancellationToken cancellationToken) { - var userId = user.Id.ToString("N"); + var userId = user == null ? null : user.Id.ToString("N"); var cacheLength = CacheLength; - var cachePath = GetChannelDataCachePath(channel, userId, folderId, sortField, sortDescending); + var cachePath = GetChannelDataCachePath(channel, userId, externalFolderId, sortField, sortDescending); try { - if (!startIndex.HasValue && !limit.HasValue) + if (_fileSystem.GetLastWriteTimeUtc(cachePath).Add(cacheLength) > DateTime.UtcNow) { - if (_fileSystem.GetLastWriteTimeUtc(cachePath).Add(cacheLength) > DateTime.UtcNow) + var cachedResult = _jsonSerializer.DeserializeFromFile<ChannelItemResult>(cachePath); + if (cachedResult != null) { - var cachedResult = _jsonSerializer.DeserializeFromFile<ChannelItemResult>(cachePath); - if (cachedResult != null) - { - return cachedResult; - } + return null; } } } @@ -1034,15 +794,12 @@ namespace Emby.Server.Implementations.Channels { try { - if (!startIndex.HasValue && !limit.HasValue) + if (_fileSystem.GetLastWriteTimeUtc(cachePath).Add(cacheLength) > DateTime.UtcNow) { - if (_fileSystem.GetLastWriteTimeUtc(cachePath).Add(cacheLength) > DateTime.UtcNow) + var cachedResult = _jsonSerializer.DeserializeFromFile<ChannelItemResult>(cachePath); + if (cachedResult != null) { - var cachedResult = _jsonSerializer.DeserializeFromFile<ChannelItemResult>(cachePath); - if (cachedResult != null) - { - return cachedResult; - } + return null; } } } @@ -1057,19 +814,13 @@ namespace Emby.Server.Implementations.Channels var query = new InternalChannelItemQuery { - UserId = userId, - StartIndex = startIndex, - Limit = limit, + UserId = user == null ? Guid.Empty : user.Id, SortBy = sortField, - SortDescending = sortDescending + SortDescending = sortDescending, + FolderId = externalFolderId }; - if (!string.IsNullOrWhiteSpace(folderId)) - { - var categoryItem = _libraryManager.GetItemById(new Guid(folderId)); - - query.FolderId = categoryItem.ExternalId; - } + query.FolderId = externalFolderId; var result = await channel.GetChannelItems(query, cancellationToken).ConfigureAwait(false); @@ -1078,10 +829,7 @@ namespace Emby.Server.Implementations.Channels throw new InvalidOperationException("Channel returned a null result from GetChannelItems"); } - if (!startIndex.HasValue && !limit.HasValue) - { - CacheResponse(result, cachePath); - } + CacheResponse(result, cachePath); return result; } @@ -1107,7 +855,7 @@ namespace Emby.Server.Implementations.Channels private string GetChannelDataCachePath(IChannel channel, string userId, - string folderId, + string externalFolderId, ChannelItemSortField? sortField, bool sortDescending) { @@ -1121,10 +869,10 @@ namespace Emby.Server.Implementations.Channels userCacheKey = hasCacheKey.GetCacheKey(userId) ?? string.Empty; } - var filename = string.IsNullOrWhiteSpace(folderId) ? "root" : folderId; + var filename = string.IsNullOrEmpty(externalFolderId) ? "root" : externalFolderId.GetMD5().ToString("N"); filename += userCacheKey; - var version = (channel.DataVersion ?? string.Empty).GetMD5().ToString("N"); + var version = ((channel.DataVersion ?? string.Empty) + "2").GetMD5().ToString("N"); if (sortField.HasValue) { @@ -1144,40 +892,6 @@ namespace Emby.Server.Implementations.Channels filename + ".json"); } - private QueryResult<BaseItem> GetReturnItems(IEnumerable<BaseItem> items, - int? totalCountFromProvider, - User user, - ChannelItemQuery query) - { - items = ApplyFilters(items, query.Filters, user); - - items = _libraryManager.Sort(items, user, query.OrderBy); - - var all = items.ToList(); - var totalCount = totalCountFromProvider ?? all.Count; - - if (!totalCountFromProvider.HasValue) - { - if (query.StartIndex.HasValue) - { - all = all.Skip(query.StartIndex.Value).ToList(); - } - if (query.Limit.HasValue) - { - all = all.Take(query.Limit.Value).ToList(); - } - } - - var returnItemArray = all.ToArray(all.Count); - RefreshIfNeeded(returnItemArray); - - return new QueryResult<BaseItem> - { - Items = returnItemArray, - TotalRecordCount = totalCount - }; - } - private string GetIdToHash(string externalId, string channelName) { // Increment this as needed to force new downloads @@ -1185,7 +899,7 @@ namespace Emby.Server.Implementations.Channels return externalId + (channelName ?? string.Empty) + "16"; } - private T GetItemById<T>(string idString, string channelName, string channnelDataVersion, out bool isNew) + private T GetItemById<T>(string idString, string channelName, out bool isNew) where T : BaseItem, new() { var id = GetIdToHash(idString, channelName).GetMBId(typeof(T)); @@ -1201,7 +915,7 @@ namespace Emby.Server.Implementations.Channels _logger.ErrorException("Error retrieving channel item from database", ex); } - if (item == null || !string.Equals(item.ExternalEtag, channnelDataVersion, StringComparison.Ordinal)) + if (item == null) { item = new T(); isNew = true; @@ -1211,13 +925,14 @@ namespace Emby.Server.Implementations.Channels isNew = false; } - item.ExternalEtag = channnelDataVersion; item.Id = id; return item; } - private BaseItem GetChannelItemEntity(ChannelItemInfo info, IChannel channelProvider, Guid internalChannelId, CancellationToken cancellationToken) + private BaseItem GetChannelItemEntity(ChannelItemInfo info, IChannel channelProvider, Guid internalChannelId, BaseItem parentFolder, CancellationToken cancellationToken) { + var parentFolderId = parentFolder.Id; + BaseItem item; bool isNew; bool forceUpdate = false; @@ -1226,66 +941,76 @@ namespace Emby.Server.Implementations.Channels { if (info.FolderType == ChannelFolderType.MusicAlbum) { - item = GetItemById<MusicAlbum>(info.Id, channelProvider.Name, channelProvider.DataVersion, out isNew); + item = GetItemById<MusicAlbum>(info.Id, channelProvider.Name, out isNew); } else if (info.FolderType == ChannelFolderType.MusicArtist) { - item = GetItemById<MusicArtist>(info.Id, channelProvider.Name, channelProvider.DataVersion, out isNew); + item = GetItemById<MusicArtist>(info.Id, channelProvider.Name, out isNew); } else if (info.FolderType == ChannelFolderType.PhotoAlbum) { - item = GetItemById<PhotoAlbum>(info.Id, channelProvider.Name, channelProvider.DataVersion, out isNew); + item = GetItemById<PhotoAlbum>(info.Id, channelProvider.Name, out isNew); } else if (info.FolderType == ChannelFolderType.Series) { - item = GetItemById<Series>(info.Id, channelProvider.Name, channelProvider.DataVersion, out isNew); + item = GetItemById<Series>(info.Id, channelProvider.Name, out isNew); } else if (info.FolderType == ChannelFolderType.Season) { - item = GetItemById<Season>(info.Id, channelProvider.Name, channelProvider.DataVersion, out isNew); + item = GetItemById<Season>(info.Id, channelProvider.Name, out isNew); } else { - item = GetItemById<Folder>(info.Id, channelProvider.Name, channelProvider.DataVersion, out isNew); + item = GetItemById<Folder>(info.Id, channelProvider.Name, out isNew); } } else if (info.MediaType == ChannelMediaType.Audio) { if (info.ContentType == ChannelMediaContentType.Podcast) { - item = GetItemById<AudioPodcast>(info.Id, channelProvider.Name, channelProvider.DataVersion, out isNew); + item = GetItemById<AudioBook>(info.Id, channelProvider.Name, out isNew); } else { - item = GetItemById<Audio>(info.Id, channelProvider.Name, channelProvider.DataVersion, out isNew); + item = GetItemById<Audio>(info.Id, channelProvider.Name, out isNew); } } else { if (info.ContentType == ChannelMediaContentType.Episode) { - item = GetItemById<Episode>(info.Id, channelProvider.Name, channelProvider.DataVersion, out isNew); + item = GetItemById<Episode>(info.Id, channelProvider.Name, out isNew); } else if (info.ContentType == ChannelMediaContentType.Movie) { - item = GetItemById<Movie>(info.Id, channelProvider.Name, channelProvider.DataVersion, out isNew); + item = GetItemById<Movie>(info.Id, channelProvider.Name, out isNew); } else if (info.ContentType == ChannelMediaContentType.Trailer || info.ExtraType == ExtraType.Trailer) { - item = GetItemById<Trailer>(info.Id, channelProvider.Name, channelProvider.DataVersion, out isNew); + item = GetItemById<Trailer>(info.Id, channelProvider.Name, out isNew); } else { - item = GetItemById<Video>(info.Id, channelProvider.Name, channelProvider.DataVersion, out isNew); + item = GetItemById<Video>(info.Id, channelProvider.Name, out isNew); } } - item.RunTimeTicks = info.RunTimeTicks; + var enableMediaProbe = channelProvider is ISupportsMediaProbe; + + if (info.IsLiveStream) + { + item.RunTimeTicks = null; + } + + else if (isNew || !enableMediaProbe) + { + item.RunTimeTicks = info.RunTimeTicks; + } if (isNew) { item.Name = info.Name; - item.Genres = info.Genres; + item.Genres = info.Genres.ToArray(); item.Studios = info.Studios.ToArray(info.Studios.Count); item.CommunityRating = info.CommunityRating; item.Overview = info.Overview; @@ -1297,7 +1022,7 @@ namespace Emby.Server.Implementations.Channels item.OfficialRating = info.OfficialRating; item.DateCreated = info.DateCreated ?? DateTime.UtcNow; item.Tags = info.Tags.ToArray(info.Tags.Count); - item.HomePageUrl = info.HomePageUrl; + item.OriginalTitle = info.OriginalTitle; } else if (info.Type == ChannelItemType.Folder && info.FolderType == ChannelFolderType.Container) { @@ -1326,22 +1051,56 @@ namespace Emby.Server.Implementations.Channels { if (!info.TrailerTypes.SequenceEqual(trailer.TrailerTypes)) { + _logger.Debug("Forcing update due to TrailerTypes {0}", item.Name); forceUpdate = true; } - trailer.TrailerTypes = info.TrailerTypes; + trailer.TrailerTypes = info.TrailerTypes.ToArray(); + } + + if (info.DateModified > item.DateModified) + { + item.DateModified = info.DateModified; + _logger.Debug("Forcing update due to DateModified {0}", item.Name); + forceUpdate = true; } - item.ChannelId = internalChannelId.ToString("N"); + // was used for status + //if (!string.Equals(item.ExternalEtag ?? string.Empty, info.Etag ?? string.Empty, StringComparison.Ordinal)) + //{ + // item.ExternalEtag = info.Etag; + // forceUpdate = true; + // _logger.Debug("Forcing update due to ExternalEtag {0}", item.Name); + //} - if (item.ParentId != internalChannelId) + if (!internalChannelId.Equals(item.ChannelId)) { forceUpdate = true; + _logger.Debug("Forcing update due to ChannelId {0}", item.Name); + } + item.ChannelId = internalChannelId; + + if (!item.ParentId.Equals(parentFolderId)) + { + forceUpdate = true; + _logger.Debug("Forcing update due to parent folder Id {0}", item.Name); + } + item.ParentId = parentFolderId; + + var hasSeries = item as IHasSeriesName; + if (hasSeries != null) + { + if (!string.Equals(hasSeries.SeriesName, info.SeriesName, StringComparison.OrdinalIgnoreCase)) + { + forceUpdate = true; + _logger.Debug("Forcing update due to SeriesName {0}", item.Name); + } + hasSeries.SeriesName = info.SeriesName; } - item.ParentId = internalChannelId; if (!string.Equals(item.ExternalId, info.Id, StringComparison.OrdinalIgnoreCase)) { forceUpdate = true; + _logger.Debug("Forcing update due to ExternalId {0}", item.Name); } item.ExternalId = info.Id; @@ -1363,20 +1122,41 @@ namespace Emby.Server.Implementations.Channels item.Path = mediaSource == null ? null : mediaSource.Path; } - if (!string.IsNullOrWhiteSpace(info.ImageUrl) && !item.HasImage(ImageType.Primary)) + if (!string.IsNullOrEmpty(info.ImageUrl) && !item.HasImage(ImageType.Primary)) { item.SetImagePath(ImageType.Primary, info.ImageUrl); + _logger.Debug("Forcing update due to ImageUrl {0}", item.Name); + forceUpdate = true; + } + + if (!info.IsLiveStream) + { + if (item.Tags.Contains("livestream", StringComparer.OrdinalIgnoreCase)) + { + item.Tags = item.Tags.Except(new[] { "livestream" }, StringComparer.OrdinalIgnoreCase).ToArray(); + _logger.Debug("Forcing update due to Tags {0}", item.Name); + forceUpdate = true; + } + } + else + { + if (!item.Tags.Contains("livestream", StringComparer.OrdinalIgnoreCase)) + { + item.Tags = item.Tags.Concat(new[] { "livestream" }).ToArray(); + _logger.Debug("Forcing update due to Tags {0}", item.Name); + forceUpdate = true; + } } item.OnMetadataChanged(); if (isNew) { - _libraryManager.CreateItem(item, cancellationToken); + _libraryManager.CreateItem(item, parentFolder); if (info.People != null && info.People.Count > 0) { - _libraryManager.UpdatePeople(item, info.People ?? new List<PersonInfo>()); + _libraryManager.UpdatePeople(item, info.People); } } else if (forceUpdate) @@ -1384,27 +1164,24 @@ namespace Emby.Server.Implementations.Channels item.UpdateToRepository(ItemUpdateType.None, cancellationToken); } - SaveMediaSources(item, info.MediaSources); - - return item; - } - - private void RefreshIfNeeded(BaseItem[] programs) - { - foreach (var program in programs) + if ((isNew || forceUpdate) && info.Type == ChannelItemType.Media) { - RefreshIfNeeded(program); + if (enableMediaProbe && !info.IsLiveStream && item.HasPathProtocol) + { + SaveMediaSources(item, new List<MediaSourceInfo>()); + } + else + { + SaveMediaSources(item, info.MediaSources); + } } - } - private void RefreshIfNeeded(BaseItem program) - { - if (!_refreshedItems.ContainsKey(program.Id)) + if (isNew || forceUpdate || item.DateLastRefreshed == default(DateTime)) { - _refreshedItems.TryAdd(program.Id, true); - _providerManager.QueueRefresh(program.Id, new MetadataRefreshOptions(_fileSystem), RefreshPriority.Low); + _providerManager.QueueRefresh(item.Id, new MetadataRefreshOptions(_fileSystem), RefreshPriority.Normal); } + return item; } internal IChannel GetChannelProvider(Channel channel) @@ -1415,7 +1192,7 @@ namespace Emby.Server.Implementations.Channels } var result = GetAllChannels() - .FirstOrDefault(i => string.Equals(GetInternalChannelId(i.Name).ToString("N"), channel.ChannelId, StringComparison.OrdinalIgnoreCase) || string.Equals(i.Name, channel.Name, StringComparison.OrdinalIgnoreCase)); + .FirstOrDefault(i => GetInternalChannelId(i.Name).Equals(channel.ChannelId) || string.Equals(i.Name, channel.Name, StringComparison.OrdinalIgnoreCase)); if (result == null) { @@ -1425,15 +1202,10 @@ namespace Emby.Server.Implementations.Channels return result; } - internal IChannel GetChannelProvider(string internalChannelId) + internal IChannel GetChannelProvider(Guid internalChannelId) { - if (internalChannelId == null) - { - throw new ArgumentNullException("internalChannelId"); - } - var result = GetAllChannels() - .FirstOrDefault(i => string.Equals(GetInternalChannelId(i.Name).ToString("N"), internalChannelId, StringComparison.OrdinalIgnoreCase)); + .FirstOrDefault(i => internalChannelId.Equals(GetInternalChannelId(i.Name))); if (result == null) { @@ -1442,175 +1214,5 @@ namespace Emby.Server.Implementations.Channels return result; } - - private IEnumerable<BaseItem> ApplyFilters(IEnumerable<BaseItem> items, IEnumerable<ItemFilter> filters, User user) - { - foreach (var filter in filters.OrderByDescending(f => (int)f)) - { - items = ApplyFilter(items, filter, user); - } - - return items; - } - - private IEnumerable<BaseItem> ApplyFilter(IEnumerable<BaseItem> items, ItemFilter filter, User user) - { - // Avoid implicitly captured closure - var currentUser = user; - - switch (filter) - { - case ItemFilter.IsFavoriteOrLikes: - return items.Where(item => - { - var userdata = _userDataManager.GetUserData(user, item); - - if (userdata == null) - { - return false; - } - - var likes = userdata.Likes ?? false; - var favorite = userdata.IsFavorite; - - return likes || favorite; - }); - - case ItemFilter.Likes: - return items.Where(item => - { - var userdata = _userDataManager.GetUserData(user, item); - - return userdata != null && userdata.Likes.HasValue && userdata.Likes.Value; - }); - - case ItemFilter.Dislikes: - return items.Where(item => - { - var userdata = _userDataManager.GetUserData(user, item); - - return userdata != null && userdata.Likes.HasValue && !userdata.Likes.Value; - }); - - case ItemFilter.IsFavorite: - return items.Where(item => - { - var userdata = _userDataManager.GetUserData(user, item); - - return userdata != null && userdata.IsFavorite; - }); - - case ItemFilter.IsResumable: - return items.Where(item => - { - var userdata = _userDataManager.GetUserData(user, item); - - return userdata != null && userdata.PlaybackPositionTicks > 0; - }); - - case ItemFilter.IsPlayed: - return items.Where(item => item.IsPlayed(currentUser)); - - case ItemFilter.IsUnplayed: - return items.Where(item => item.IsUnplayed(currentUser)); - - case ItemFilter.IsFolder: - return items.Where(item => item.IsFolder); - - case ItemFilter.IsNotFolder: - return items.Where(item => !item.IsFolder); - } - - return items; - } - - public BaseItemDto GetChannelFolder(string userId, CancellationToken cancellationToken) - { - var user = string.IsNullOrEmpty(userId) ? null : _userManager.GetUserById(userId); - - var folder = GetInternalChannelFolder(cancellationToken); - - return _dtoService.GetBaseItemDto(folder, new DtoOptions(), user); - } - - public Folder GetInternalChannelFolder(CancellationToken cancellationToken) - { - var name = _localization.GetLocalizedString("Channels"); - - return _libraryManager.GetNamedView(name, "channels", "zz_" + name, cancellationToken); - } - } - - public class ChannelsEntryPoint : IServerEntryPoint - { - private readonly IServerConfigurationManager _config; - private readonly IChannelManager _channelManager; - private readonly ITaskManager _taskManager; - private readonly IFileSystem _fileSystem; - - public ChannelsEntryPoint(IChannelManager channelManager, ITaskManager taskManager, IServerConfigurationManager config, IFileSystem fileSystem) - { - _channelManager = channelManager; - _taskManager = taskManager; - _config = config; - _fileSystem = fileSystem; - } - - public void Run() - { - var channels = ((ChannelManager)_channelManager).Channels - .Select(i => i.GetType().FullName.GetMD5().ToString("N")) - .ToArray(); - - var channelsString = string.Join(",", channels); - - if (!string.Equals(channelsString, GetSavedLastChannels(), StringComparison.OrdinalIgnoreCase)) - { - _taskManager.QueueIfNotRunning<RefreshChannelsScheduledTask>(); - - SetSavedLastChannels(channelsString); - } - } - - private string DataPath - { - get { return Path.Combine(_config.ApplicationPaths.DataPath, "channels.txt"); } - } - - private string GetSavedLastChannels() - { - try - { - return _fileSystem.ReadAllText(DataPath); - } - catch - { - return string.Empty; - } - } - - private void SetSavedLastChannels(string value) - { - try - { - if (string.IsNullOrWhiteSpace(value)) - { - _fileSystem.DeleteFile(DataPath); - - } - else - { - _fileSystem.WriteAllText(DataPath, value); - } - } - catch - { - } - } - - public void Dispose() - { - GC.SuppressFinalize(this); - } } }
\ No newline at end of file diff --git a/Emby.Server.Implementations/Channels/ChannelPostScanTask.cs b/Emby.Server.Implementations/Channels/ChannelPostScanTask.cs index ae31fcf3f..b211908d8 100644 --- a/Emby.Server.Implementations/Channels/ChannelPostScanTask.cs +++ b/Emby.Server.Implementations/Channels/ChannelPostScanTask.cs @@ -1,15 +1,11 @@ -using MediaBrowser.Common.Progress; -using MediaBrowser.Controller.Channels; +using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; -using MediaBrowser.Model.Channels; using MediaBrowser.Model.Logging; using System; -using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; -using MediaBrowser.Model.Extensions; namespace Emby.Server.Implementations.Channels { @@ -28,35 +24,12 @@ namespace Emby.Server.Implementations.Channels _libraryManager = libraryManager; } - public async Task Run(IProgress<double> progress, CancellationToken cancellationToken) + public Task Run(IProgress<double> progress, CancellationToken cancellationToken) { - var users = _userManager.Users - .DistinctBy(GetUserDistinctValue) - .Select(i => i.Id.ToString("N")) - .ToList(); - - var numComplete = 0; - - foreach (var user in users) - { - double percentPerUser = 1; - percentPerUser /= users.Count; - var startingPercent = numComplete * percentPerUser * 100; - - var innerProgress = new ActionableProgress<double>(); - innerProgress.RegisterAction(p => progress.Report(startingPercent + percentPerUser * p)); - - await DownloadContent(user, cancellationToken, innerProgress).ConfigureAwait(false); - - numComplete++; - double percent = numComplete; - percent /= users.Count; - progress.Report(percent * 100); - } - - await CleanDatabase(cancellationToken).ConfigureAwait(false); + CleanDatabase(cancellationToken); progress.Report(100); + return Task.CompletedTask; } public static string GetUserDistinctValue(User user) @@ -68,60 +41,7 @@ namespace Emby.Server.Implementations.Channels return string.Join("|", channels.ToArray()); } - private async Task DownloadContent(string user, CancellationToken cancellationToken, IProgress<double> progress) - { - var channels = await _channelManager.GetChannelsInternal(new ChannelQuery - { - UserId = user - - }, cancellationToken); - - var numComplete = 0; - var numItems = channels.Items.Length; - - foreach (var channel in channels.Items) - { - var channelId = channel.Id.ToString("N"); - - var features = _channelManager.GetChannelFeatures(channelId); - - const int currentRefreshLevel = 1; - var maxRefreshLevel = features.AutoRefreshLevels ?? 0; - maxRefreshLevel = Math.Max(maxRefreshLevel, 2); - - if (maxRefreshLevel > 0) - { - var innerProgress = new ActionableProgress<double>(); - - var startingNumberComplete = numComplete; - innerProgress.RegisterAction(p => - { - double innerPercent = startingNumberComplete; - innerPercent += p / 100; - innerPercent /= numItems; - progress.Report(innerPercent * 100); - }); - - try - { - await GetAllItems(user, channelId, null, currentRefreshLevel, maxRefreshLevel, innerProgress, cancellationToken).ConfigureAwait(false); - } - catch (Exception ex) - { - _logger.ErrorException("Error getting channel content", ex); - } - } - - numComplete++; - double percent = numComplete; - percent /= numItems; - progress.Report(percent * 100); - } - - progress.Report(100); - } - - private async Task CleanDatabase(CancellationToken cancellationToken) + private void CleanDatabase(CancellationToken cancellationToken) { var installedChannelIds = ((ChannelManager)_channelManager).GetInstalledChannelIds(); @@ -138,120 +58,45 @@ namespace Emby.Server.Implementations.Channels { cancellationToken.ThrowIfCancellationRequested(); - await CleanChannel(id, cancellationToken).ConfigureAwait(false); + CleanChannel(id, cancellationToken); } } - private async Task CleanChannel(Guid id, CancellationToken cancellationToken) + private void CleanChannel(Guid id, CancellationToken cancellationToken) { _logger.Info("Cleaning channel {0} from database", id); // Delete all channel items var allIds = _libraryManager.GetItemIds(new InternalItemsQuery { - ChannelIds = new[] { id.ToString("N") } + ChannelIds = new[] { id } }); foreach (var deleteId in allIds) { cancellationToken.ThrowIfCancellationRequested(); - await DeleteItem(deleteId).ConfigureAwait(false); + DeleteItem(deleteId); } // Finally, delete the channel itself - await DeleteItem(id).ConfigureAwait(false); + DeleteItem(id); } - private Task DeleteItem(Guid id) + private void DeleteItem(Guid id) { var item = _libraryManager.GetItemById(id); if (item == null) { - return Task.FromResult(true); + return; } - return _libraryManager.DeleteItem(item, new DeleteOptions + _libraryManager.DeleteItem(item, new DeleteOptions { DeleteFileLocation = false - }); - } - - private async Task GetAllItems(string user, string channelId, string folderId, int currentRefreshLevel, int maxRefreshLevel, IProgress<double> progress, CancellationToken cancellationToken) - { - var folderItems = new List<string>(); - - var innerProgress = new ActionableProgress<double>(); - innerProgress.RegisterAction(p => progress.Report(p / 2)); - - var result = await _channelManager.GetChannelItemsInternal(new ChannelItemQuery - { - ChannelId = channelId, - UserId = user, - FolderId = folderId - - }, innerProgress, cancellationToken); - - folderItems.AddRange(result.Items.Where(i => i.IsFolder).Select(i => i.Id.ToString("N"))); - - var totalRetrieved = result.Items.Length; - var totalCount = result.TotalRecordCount; - - while (totalRetrieved < totalCount) - { - result = await _channelManager.GetChannelItemsInternal(new ChannelItemQuery - { - ChannelId = channelId, - UserId = user, - StartIndex = totalRetrieved, - FolderId = folderId - - }, new SimpleProgress<double>(), cancellationToken); - folderItems.AddRange(result.Items.Where(i => i.IsFolder).Select(i => i.Id.ToString("N"))); - - totalRetrieved += result.Items.Length; - totalCount = result.TotalRecordCount; - } - - progress.Report(50); - - if (currentRefreshLevel < maxRefreshLevel) - { - var numComplete = 0; - var numItems = folderItems.Count; - - foreach (var folder in folderItems) - { - try - { - innerProgress = new ActionableProgress<double>(); - - var startingNumberComplete = numComplete; - innerProgress.RegisterAction(p => - { - double innerPercent = startingNumberComplete; - innerPercent += p / 100; - innerPercent /= numItems; - progress.Report(innerPercent * 50 + 50); - }); - - await GetAllItems(user, channelId, folder, currentRefreshLevel + 1, maxRefreshLevel, innerProgress, cancellationToken).ConfigureAwait(false); - } - catch (Exception ex) - { - _logger.ErrorException("Error getting channel content", ex); - } - - numComplete++; - double percent = numComplete; - percent /= numItems; - progress.Report(percent * 50 + 50); - } - } - - progress.Report(100); + }, false); } } } diff --git a/Emby.Server.Implementations/Collections/CollectionImageProvider.cs b/Emby.Server.Implementations/Collections/CollectionImageProvider.cs index f47e2d10a..858dada4b 100644 --- a/Emby.Server.Implementations/Collections/CollectionImageProvider.cs +++ b/Emby.Server.Implementations/Collections/CollectionImageProvider.cs @@ -12,6 +12,7 @@ using System.Threading.Tasks; using Emby.Server.Implementations.Images; using MediaBrowser.Model.IO; using MediaBrowser.Model.Extensions; +using System; namespace Emby.Server.Implementations.Collections { @@ -21,7 +22,7 @@ namespace Emby.Server.Implementations.Collections { } - protected override bool Supports(IHasMetadata item) + protected override bool Supports(BaseItem item) { // Right now this is the only way to prevent this image from getting created ahead of internet image providers if (!item.IsLocked) @@ -32,11 +33,11 @@ namespace Emby.Server.Implementations.Collections return base.Supports(item); } - protected override List<BaseItem> GetItemsWithImages(IHasMetadata item) + protected override List<BaseItem> GetItemsWithImages(BaseItem item) { var playlist = (BoxSet)item; - var items = playlist.Children.Concat(playlist.GetLinkedChildren()) + return playlist.Children.Concat(playlist.GetLinkedChildren()) .Select(i => { var subItem = i; @@ -57,7 +58,7 @@ namespace Emby.Server.Implementations.Collections return subItem; } - var parent = subItem.IsOwnedItem ? subItem.GetOwner() : subItem.GetParent(); + var parent = subItem.GetOwner() ?? subItem.GetParent(); if (parent != null && parent.HasImage(ImageType.Primary)) { @@ -71,12 +72,11 @@ namespace Emby.Server.Implementations.Collections }) .Where(i => i != null) .DistinctBy(i => i.Id) + .OrderBy(i => Guid.NewGuid()) .ToList(); - - return GetFinalItems(items, 2); } - protected override string CreateImage(IHasMetadata item, List<BaseItem> itemsWithImages, string outputPathWithoutExtension, ImageType imageType, int imageIndex) + protected override string CreateImage(BaseItem item, List<BaseItem> itemsWithImages, string outputPathWithoutExtension, ImageType imageType, int imageIndex) { return CreateSingleImage(itemsWithImages, outputPathWithoutExtension, ImageType.Primary); } diff --git a/Emby.Server.Implementations/Collections/CollectionManager.cs b/Emby.Server.Implementations/Collections/CollectionManager.cs index c8e947fd7..675a726e5 100644 --- a/Emby.Server.Implementations/Collections/CollectionManager.cs +++ b/Emby.Server.Implementations/Collections/CollectionManager.cs @@ -13,6 +13,12 @@ using System.Threading; using System.Threading.Tasks; using MediaBrowser.Model.IO; using MediaBrowser.Model.Extensions; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Configuration; +using MediaBrowser.Controller.Plugins; +using MediaBrowser.Model.Globalization; namespace Emby.Server.Implementations.Collections { @@ -23,36 +29,84 @@ namespace Emby.Server.Implementations.Collections private readonly ILibraryMonitor _iLibraryMonitor; private readonly ILogger _logger; private readonly IProviderManager _providerManager; + private readonly ILocalizationManager _localizationManager; + private IApplicationPaths _appPaths; public event EventHandler<CollectionCreatedEventArgs> CollectionCreated; public event EventHandler<CollectionModifiedEventArgs> ItemsAddedToCollection; public event EventHandler<CollectionModifiedEventArgs> ItemsRemovedFromCollection; - public CollectionManager(ILibraryManager libraryManager, IFileSystem fileSystem, ILibraryMonitor iLibraryMonitor, ILogger logger, IProviderManager providerManager) + public CollectionManager(ILibraryManager libraryManager, IApplicationPaths appPaths, ILocalizationManager localizationManager, IFileSystem fileSystem, ILibraryMonitor iLibraryMonitor, ILogger logger, IProviderManager providerManager) { _libraryManager = libraryManager; _fileSystem = fileSystem; _iLibraryMonitor = iLibraryMonitor; _logger = logger; _providerManager = providerManager; + _localizationManager = localizationManager; + _appPaths = appPaths; } - public Folder GetCollectionsFolder(string userId) + private IEnumerable<Folder> FindFolders(string path) { - return _libraryManager.RootFolder.Children.OfType<ManualCollectionsFolder>() - .FirstOrDefault() ?? _libraryManager.GetUserRootFolder().Children.OfType<ManualCollectionsFolder>() - .FirstOrDefault(); + return _libraryManager + .RootFolder + .Children + .OfType<Folder>() + .Where(i => _fileSystem.AreEqual(path, i.Path) || _fileSystem.ContainsSubPath(i.Path, path)); } - public IEnumerable<BoxSet> GetCollections(User user) + internal async Task<Folder> EnsureLibraryFolder(string path, bool createIfNeeded) { - var folder = GetCollectionsFolder(user.Id.ToString("N")); + var existingFolders = FindFolders(path) + .ToList(); + + if (existingFolders.Count > 0) + { + return existingFolders[0]; + } + + if (!createIfNeeded) + { + return null; + } + + _fileSystem.CreateDirectory(path); + + var libraryOptions = new LibraryOptions + { + PathInfos = new[] { new MediaPathInfo { Path = path } }, + EnableRealtimeMonitor = false, + SaveLocalMetadata = true + }; + + var name = _localizationManager.GetLocalizedString("Collections"); + + await _libraryManager.AddVirtualFolder(name, CollectionType.BoxSets, libraryOptions, true).ConfigureAwait(false); + + return FindFolders(path).First(); + } + + internal string GetCollectionsFolderPath() + { + return Path.Combine(_appPaths.DataPath, "collections"); + } + + private Task<Folder> GetCollectionsFolder(bool createIfNeeded) + { + return EnsureLibraryFolder(GetCollectionsFolderPath(), createIfNeeded); + } + + private IEnumerable<BoxSet> GetCollections(User user) + { + var folder = GetCollectionsFolder(false).Result; + return folder == null ? new List<BoxSet>() : folder.GetChildren(user, true).OfType<BoxSet>(); } - public async Task<BoxSet> CreateCollection(CollectionCreationOptions options) + public BoxSet CreateCollection(CollectionCreationOptions options) { var name = options.Name; @@ -61,7 +115,7 @@ namespace Emby.Server.Implementations.Collections // This could cause it to get re-resolved as a plain folder var folderName = _fileSystem.GetValidFilename(name) + " [boxset]"; - var parentFolder = GetParentFolder(options.ParentId); + var parentFolder = GetCollectionsFolder(true).Result; if (parentFolder == null) { @@ -82,19 +136,14 @@ namespace Emby.Server.Implementations.Collections Path = path, IsLocked = options.IsLocked, ProviderIds = options.ProviderIds, - Shares = options.UserIds.Select(i => new Share - { - UserId = i, - CanEdit = true - - }).ToList() + DateCreated = DateTime.UtcNow }; parentFolder.AddChild(collection, CancellationToken.None); if (options.ItemIdList.Length > 0) { - await AddToCollection(collection.Id, options.ItemIdList, false, new MetadataRefreshOptions(_fileSystem) + AddToCollection(collection.Id, options.ItemIdList, false, new MetadataRefreshOptions(_fileSystem) { // The initial adding of items is going to create a local metadata file // This will cause internet metadata to be skipped as a result @@ -122,44 +171,17 @@ namespace Emby.Server.Implementations.Collections } } - private Folder GetParentFolder(Guid? parentId) + public void AddToCollection(Guid collectionId, IEnumerable<string> ids) { - if (parentId.HasValue) - { - if (parentId.Value == Guid.Empty) - { - throw new ArgumentNullException("parentId"); - } - - var folder = _libraryManager.GetItemById(parentId.Value) as Folder; - - // Find an actual physical folder - if (folder is CollectionFolder) - { - var child = _libraryManager.RootFolder.Children.OfType<Folder>() - .FirstOrDefault(i => folder.PhysicalLocations.Contains(i.Path, StringComparer.OrdinalIgnoreCase)); - - if (child != null) - { - return child; - } - } - } - - return GetCollectionsFolder(string.Empty); - } - - public Task AddToCollection(Guid collectionId, IEnumerable<string> ids) - { - return AddToCollection(collectionId, ids, true, new MetadataRefreshOptions(_fileSystem)); + AddToCollection(collectionId, ids, true, new MetadataRefreshOptions(_fileSystem)); } - public Task AddToCollection(Guid collectionId, IEnumerable<Guid> ids) + public void AddToCollection(Guid collectionId, IEnumerable<Guid> ids) { - return AddToCollection(collectionId, ids.Select(i => i.ToString("N")), true, new MetadataRefreshOptions(_fileSystem)); + AddToCollection(collectionId, ids.Select(i => i.ToString("N")), true, new MetadataRefreshOptions(_fileSystem)); } - private async Task AddToCollection(Guid collectionId, IEnumerable<string> ids, bool fireEvent, MetadataRefreshOptions refreshOptions) + private void AddToCollection(Guid collectionId, IEnumerable<string> ids, bool fireEvent, MetadataRefreshOptions refreshOptions) { var collection = _libraryManager.GetItemById(collectionId) as BoxSet; @@ -170,28 +192,26 @@ namespace Emby.Server.Implementations.Collections var list = new List<LinkedChild>(); var itemList = new List<BaseItem>(); - var currentLinkedChildrenIds = collection.GetLinkedChildren().Select(i => i.Id).ToList(); + + var linkedChildrenList = collection.GetLinkedChildren(); + var currentLinkedChildrenIds = linkedChildrenList.Select(i => i.Id).ToList(); foreach (var id in ids) { var guidId = new Guid(id); var item = _libraryManager.GetItemById(guidId); - if (string.IsNullOrWhiteSpace(item.Path)) - { - continue; - } - if (item == null) { throw new ArgumentException("No item exists with the supplied Id"); } - itemList.Add(item); - if (!currentLinkedChildrenIds.Contains(guidId)) { + itemList.Add(item); + list.Add(LinkedChild.Create(item)); + linkedChildrenList.Add(item); } } @@ -201,10 +221,11 @@ namespace Emby.Server.Implementations.Collections newList.AddRange(list); collection.LinkedChildren = newList.ToArray(newList.Count); - collection.UpdateRatingToContent(); + collection.UpdateRatingToItems(linkedChildrenList); collection.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None); + refreshOptions.ForceSave = true; _providerManager.QueueRefresh(collection.Id, refreshOptions, RefreshPriority.High); if (fireEvent) @@ -219,12 +240,12 @@ namespace Emby.Server.Implementations.Collections } } - public Task RemoveFromCollection(Guid collectionId, IEnumerable<string> itemIds) + public void RemoveFromCollection(Guid collectionId, IEnumerable<string> itemIds) { - return RemoveFromCollection(collectionId, itemIds.Select(i => new Guid(i))); + RemoveFromCollection(collectionId, itemIds.Select(i => new Guid(i))); } - public async Task RemoveFromCollection(Guid collectionId, IEnumerable<Guid> itemIds) + public void RemoveFromCollection(Guid collectionId, IEnumerable<Guid> itemIds) { var collection = _libraryManager.GetItemById(collectionId) as BoxSet; @@ -244,7 +265,8 @@ namespace Emby.Server.Implementations.Collections if (child == null) { - throw new ArgumentException("No collection title exists with the supplied Id"); + _logger.Warn("No collection title exists with the supplied Id"); + continue; } list.Add(child); @@ -260,10 +282,11 @@ namespace Emby.Server.Implementations.Collections collection.LinkedChildren = collection.LinkedChildren.Except(list).ToArray(); } - collection.UpdateRatingToContent(); - collection.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None); - _providerManager.QueueRefresh(collection.Id, new MetadataRefreshOptions(_fileSystem), RefreshPriority.High); + _providerManager.QueueRefresh(collection.Id, new MetadataRefreshOptions(_fileSystem) + { + ForceSave = true + }, RefreshPriority.High); EventHelper.FireEventIfNotNull(ItemsRemovedFromCollection, this, new CollectionModifiedEventArgs { @@ -292,7 +315,7 @@ namespace Emby.Server.Implementations.Collections var itemId = item.Id; var currentBoxSets = allBoxsets - .Where(i => i.GetLinkedChildren().Any(j => j.Id == itemId)) + .Where(i => i.ContainsLinkedChildByItemId(itemId)) .ToList(); if (currentBoxSets.Count > 0) @@ -312,4 +335,78 @@ namespace Emby.Server.Implementations.Collections return results.Values; } } + + public class CollectionManagerEntryPoint : IServerEntryPoint + { + private readonly CollectionManager _collectionManager; + private readonly IServerConfigurationManager _config; + private readonly IFileSystem _fileSystem; + private ILogger _logger; + + public CollectionManagerEntryPoint(ICollectionManager collectionManager, IServerConfigurationManager config, IFileSystem fileSystem, ILogger logger) + { + _collectionManager = (CollectionManager)collectionManager; + _config = config; + _fileSystem = fileSystem; + _logger = logger; + } + + public async void Run() + { + if (!_config.Configuration.CollectionsUpgraded && _config.Configuration.IsStartupWizardCompleted) + { + var path = _collectionManager.GetCollectionsFolderPath(); + + if (_fileSystem.DirectoryExists(path)) + { + try + { + await _collectionManager.EnsureLibraryFolder(path, true).ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.ErrorException("Error creating camera uploads library", ex); + } + + _config.Configuration.CollectionsUpgraded = true; + _config.SaveConfiguration(); + } + } + } + + #region IDisposable Support + private bool disposedValue = false; // To detect redundant calls + + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + // TODO: dispose managed state (managed objects). + } + + // TODO: free unmanaged resources (unmanaged objects) and override a finalizer below. + // TODO: set large fields to null. + + disposedValue = true; + } + } + + // TODO: override a finalizer only if Dispose(bool disposing) above has code to free unmanaged resources. + // ~CollectionManagerEntryPoint() { + // // Do not change this code. Put cleanup code in Dispose(bool disposing) above. + // Dispose(false); + // } + + // This code added to correctly implement the disposable pattern. + public void Dispose() + { + // Do not change this code. Put cleanup code in Dispose(bool disposing) above. + Dispose(true); + // TODO: uncomment the following line if the finalizer is overridden above. + // GC.SuppressFinalize(this); + } + #endregion + } } diff --git a/Emby.Server.Implementations/Collections/CollectionsDynamicFolder.cs b/Emby.Server.Implementations/Collections/CollectionsDynamicFolder.cs deleted file mode 100644 index c7bcdfe25..000000000 --- a/Emby.Server.Implementations/Collections/CollectionsDynamicFolder.cs +++ /dev/null @@ -1,34 +0,0 @@ -using MediaBrowser.Common.Configuration; -using MediaBrowser.Controller.Entities; -using System.IO; - -using MediaBrowser.Model.IO; -using MediaBrowser.Controller.Collections; -using MediaBrowser.Controller.IO; - -namespace Emby.Server.Implementations.Collections -{ - public class CollectionsDynamicFolder : IVirtualFolderCreator - { - private readonly IApplicationPaths _appPaths; - private readonly IFileSystem _fileSystem; - - public CollectionsDynamicFolder(IApplicationPaths appPaths, IFileSystem fileSystem) - { - _appPaths = appPaths; - _fileSystem = fileSystem; - } - - public BasePluginFolder GetFolder() - { - var path = Path.Combine(_appPaths.DataPath, "collections"); - - _fileSystem.CreateDirectory(path); - - return new ManualCollectionsFolder - { - Path = path - }; - } - } -} diff --git a/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs b/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs index e73a69892..47e2ec0a8 100644 --- a/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs +++ b/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs @@ -102,8 +102,6 @@ namespace Emby.Server.Implementations.Configuration } ((ServerApplicationPaths)ApplicationPaths).InternalMetadataPath = metadataPath; - - ((ServerApplicationPaths)ApplicationPaths).ItemsByNamePath = ((ServerApplicationPaths)ApplicationPaths).InternalMetadataPath; } private string GetInternalMetadataPath() @@ -237,6 +235,18 @@ namespace Emby.Server.Implementations.Configuration changed = true; } + if (!config.CameraUploadUpgraded) + { + config.CameraUploadUpgraded = true; + changed = true; + } + + if (!config.CollectionsUpgraded) + { + config.CollectionsUpgraded = true; + changed = true; + } + return changed; } } diff --git a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs index d207c8d4f..76ebff3a8 100644 --- a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs +++ b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs @@ -189,6 +189,26 @@ namespace Emby.Server.Implementations.Data return sql.Select(connection.PrepareStatement).ToList(); } + protected bool TableExists(ManagedConnection connection, string name) + { + return connection.RunInTransaction(db => + { + using (var statement = PrepareStatement(db, "select DISTINCT tbl_name from sqlite_master")) + { + foreach (var row in statement.ExecuteQuery()) + { + if (string.Equals(name, row.GetString(0), StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + } + + return false; + + }, ReadTransactionMode); + } + protected void RunDefaultInitialization(ManagedConnection db) { var queries = new List<string> @@ -264,7 +284,6 @@ namespace Emby.Server.Implementations.Data { _disposed = true; Dispose(true); - GC.SuppressFinalize(this); } private readonly object _disposeLock = new object(); diff --git a/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs b/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs index 37ba2eb5f..8611cabc1 100644 --- a/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs +++ b/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs @@ -33,10 +33,11 @@ namespace Emby.Server.Implementations.Data public Task Run(IProgress<double> progress, CancellationToken cancellationToken) { - return CleanDeadItems(cancellationToken, progress); + CleanDeadItems(cancellationToken, progress); + return Task.CompletedTask; } - private async Task CleanDeadItems(CancellationToken cancellationToken, IProgress<double> progress) + private void CleanDeadItems(CancellationToken cancellationToken, IProgress<double> progress) { var itemIds = _libraryManager.GetItemIds(new InternalItemsQuery { @@ -58,11 +59,11 @@ namespace Emby.Server.Implementations.Data { _logger.Info("Cleaning item {0} type: {1} path: {2}", item.Name, item.GetType().Name, item.Path ?? string.Empty); - await item.Delete(new DeleteOptions + _libraryManager.DeleteItem(item, new DeleteOptions { DeleteFileLocation = false - }).ConfigureAwait(false); + }); } numComplete++; diff --git a/Emby.Server.Implementations/Data/ManagedConnection.cs b/Emby.Server.Implementations/Data/ManagedConnection.cs index 5d0fc8ebc..91a2dfdf6 100644 --- a/Emby.Server.Implementations/Data/ManagedConnection.cs +++ b/Emby.Server.Implementations/Data/ManagedConnection.cs @@ -77,7 +77,6 @@ namespace Emby.Server.Implementations.Data { Close(); } - GC.SuppressFinalize(this); } } } diff --git a/Emby.Server.Implementations/Data/SqliteDisplayPreferencesRepository.cs b/Emby.Server.Implementations/Data/SqliteDisplayPreferencesRepository.cs index e6afcd410..09ff7e09d 100644 --- a/Emby.Server.Implementations/Data/SqliteDisplayPreferencesRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteDisplayPreferencesRepository.cs @@ -18,14 +18,12 @@ namespace Emby.Server.Implementations.Data /// </summary> public class SqliteDisplayPreferencesRepository : BaseSqliteRepository, IDisplayPreferencesRepository { - private readonly IMemoryStreamFactory _memoryStreamProvider; protected IFileSystem FileSystem { get; private set; } - public SqliteDisplayPreferencesRepository(ILogger logger, IJsonSerializer jsonSerializer, IApplicationPaths appPaths, IMemoryStreamFactory memoryStreamProvider, IFileSystem fileSystem) + public SqliteDisplayPreferencesRepository(ILogger logger, IJsonSerializer jsonSerializer, IApplicationPaths appPaths, IFileSystem fileSystem) : base(logger) { _jsonSerializer = jsonSerializer; - _memoryStreamProvider = memoryStreamProvider; FileSystem = fileSystem; DbFilePath = Path.Combine(appPaths.DataPath, "displaypreferences.db"); } @@ -98,7 +96,7 @@ namespace Emby.Server.Implementations.Data { throw new ArgumentNullException("displayPreferences"); } - if (string.IsNullOrWhiteSpace(displayPreferences.Id)) + if (string.IsNullOrEmpty(displayPreferences.Id)) { throw new ArgumentNullException("displayPreferences.Id"); } @@ -119,7 +117,7 @@ namespace Emby.Server.Implementations.Data private void SaveDisplayPreferences(DisplayPreferences displayPreferences, Guid userId, string client, IDatabaseConnection connection) { - var serialized = _jsonSerializer.SerializeToBytes(displayPreferences, _memoryStreamProvider); + var serialized = _jsonSerializer.SerializeToBytes(displayPreferences); using (var statement = connection.PrepareStatement("replace into userdisplaypreferences (id, userid, client, data) values (@id, @userId, @client, @data)")) { @@ -174,7 +172,7 @@ namespace Emby.Server.Implementations.Data /// <exception cref="System.ArgumentNullException">item</exception> public DisplayPreferences GetDisplayPreferences(string displayPreferencesId, Guid userId, string client) { - if (string.IsNullOrWhiteSpace(displayPreferencesId)) + if (string.IsNullOrEmpty(displayPreferencesId)) { throw new ArgumentNullException("displayPreferencesId"); } @@ -236,7 +234,7 @@ namespace Emby.Server.Implementations.Data private DisplayPreferences Get(IReadOnlyList<IResultSetValue> row) { - using (var stream = _memoryStreamProvider.CreateNew(row[0].ToBlob())) + using (var stream = new MemoryStream(row[0].ToBlob())) { stream.Position = 0; return _jsonSerializer.DeserializeFromStream<DisplayPreferences>(stream); diff --git a/Emby.Server.Implementations/Data/SqliteExtensions.cs b/Emby.Server.Implementations/Data/SqliteExtensions.cs index d2c851b3c..a755c65f4 100644 --- a/Emby.Server.Implementations/Data/SqliteExtensions.cs +++ b/Emby.Server.Implementations/Data/SqliteExtensions.cs @@ -4,6 +4,7 @@ using System.Globalization; using MediaBrowser.Model.IO; using MediaBrowser.Model.Serialization; using SQLitePCL.pretty; +using System.IO; namespace Emby.Server.Implementations.Data { @@ -110,19 +111,33 @@ namespace Emby.Server.Implementations.Data DateTimeStyles.None).ToUniversalTime(); } + public static DateTime? TryReadDateTime(this IResultSetValue result) + { + var dateText = result.ToString(); + + DateTime dateTimeResult; + + if (DateTime.TryParseExact(dateText, _datetimeFormats, DateTimeFormatInfo.InvariantInfo, DateTimeStyles.None, out dateTimeResult)) + { + return dateTimeResult.ToUniversalTime(); + } + + return null; + } + /// <summary> /// Serializes to bytes. /// </summary> /// <returns>System.Byte[][].</returns> /// <exception cref="System.ArgumentNullException">obj</exception> - public static byte[] SerializeToBytes(this IJsonSerializer json, object obj, IMemoryStreamFactory streamProvider) + public static byte[] SerializeToBytes(this IJsonSerializer json, object obj) { if (obj == null) { throw new ArgumentNullException("obj"); } - using (var stream = streamProvider.CreateNew()) + using (var stream = new MemoryStream()) { json.SerializeToStream(obj, stream); return stream.ToArray(); diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index 830d6447e..bde28c923 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -32,6 +32,9 @@ using SQLitePCL.pretty; using MediaBrowser.Model.System; using MediaBrowser.Model.Threading; using MediaBrowser.Model.Extensions; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Drawing; +using MediaBrowser.Controller.Library; namespace Emby.Server.Implementations.Data { @@ -65,18 +68,16 @@ namespace Emby.Server.Implementations.Data /// </summary> private readonly IServerConfigurationManager _config; - private readonly string _criticReviewsPath; - - private readonly IMemoryStreamFactory _memoryStreamProvider; private readonly IFileSystem _fileSystem; private readonly IEnvironmentInfo _environmentInfo; - private readonly ITimerFactory _timerFactory; - private ITimer _shrinkMemoryTimer; + private IServerApplicationHost _appHost; + + public IImageProcessor ImageProcessor { get; set; } /// <summary> /// Initializes a new instance of the <see cref="SqliteItemRepository"/> class. /// </summary> - public SqliteItemRepository(IServerConfigurationManager config, IJsonSerializer jsonSerializer, ILogger logger, IMemoryStreamFactory memoryStreamProvider, IAssemblyInfo assemblyInfo, IFileSystem fileSystem, IEnvironmentInfo environmentInfo, ITimerFactory timerFactory) + public SqliteItemRepository(IServerConfigurationManager config, IServerApplicationHost appHost, IJsonSerializer jsonSerializer, ILogger logger, IAssemblyInfo assemblyInfo, IFileSystem fileSystem, IEnvironmentInfo environmentInfo, ITimerFactory timerFactory) : base(logger) { if (config == null) @@ -88,15 +89,13 @@ namespace Emby.Server.Implementations.Data throw new ArgumentNullException("jsonSerializer"); } + _appHost = appHost; _config = config; _jsonSerializer = jsonSerializer; - _memoryStreamProvider = memoryStreamProvider; _fileSystem = fileSystem; _environmentInfo = environmentInfo; - _timerFactory = timerFactory; _typeMapper = new TypeMapper(assemblyInfo); - _criticReviewsPath = Path.Combine(_config.ApplicationPaths.DataPath, "critic-reviews"); DbFilePath = Path.Combine(_config.ApplicationPaths.DataPath, "library.db"); } @@ -118,28 +117,17 @@ namespace Emby.Server.Implementations.Data } } - protected override void CloseConnection() - { - if (_shrinkMemoryTimer != null) - { - _shrinkMemoryTimer.Dispose(); - _shrinkMemoryTimer = null; - } - - base.CloseConnection(); - } - /// <summary> /// Opens the connection to the database /// </summary> - public void Initialize(SqliteUserDataRepository userDataRepo) + public void Initialize(SqliteUserDataRepository userDataRepo, IUserManager userManager) { using (var connection = CreateConnection()) { RunDefaultInitialization(connection); var 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, PRIMARY KEY (ItemId, StreamIndex))"; + = "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", @@ -162,8 +150,6 @@ namespace Emby.Server.Implementations.Data createMediaStreamsTableCommand, - "create index if not exists idx_mediastreams1 on mediastreams(ItemId)", - "pragma shrink_memory" }; @@ -182,8 +168,6 @@ namespace Emby.Server.Implementations.Data AddColumn(db, "TypedBaseItems", "EndDate", "DATETIME", existingColumnNames); AddColumn(db, "TypedBaseItems", "ChannelId", "Text", existingColumnNames); AddColumn(db, "TypedBaseItems", "IsMovie", "BIT", existingColumnNames); - AddColumn(db, "TypedBaseItems", "IsSports", "BIT", existingColumnNames); - AddColumn(db, "TypedBaseItems", "IsKids", "BIT", existingColumnNames); AddColumn(db, "TypedBaseItems", "CommunityRating", "Float", existingColumnNames); AddColumn(db, "TypedBaseItems", "CustomRating", "Text", existingColumnNames); AddColumn(db, "TypedBaseItems", "IndexNumber", "INT", existingColumnNames); @@ -200,19 +184,13 @@ namespace Emby.Server.Implementations.Data AddColumn(db, "TypedBaseItems", "SortName", "Text", existingColumnNames); AddColumn(db, "TypedBaseItems", "ForcedSortName", "Text", existingColumnNames); AddColumn(db, "TypedBaseItems", "RunTimeTicks", "BIGINT", existingColumnNames); - AddColumn(db, "TypedBaseItems", "HomePageUrl", "Text", existingColumnNames); AddColumn(db, "TypedBaseItems", "DateCreated", "DATETIME", existingColumnNames); AddColumn(db, "TypedBaseItems", "DateModified", "DATETIME", existingColumnNames); AddColumn(db, "TypedBaseItems", "IsSeries", "BIT", existingColumnNames); - AddColumn(db, "TypedBaseItems", "IsLive", "BIT", existingColumnNames); - AddColumn(db, "TypedBaseItems", "IsNews", "BIT", existingColumnNames); - AddColumn(db, "TypedBaseItems", "IsPremiere", "BIT", existingColumnNames); AddColumn(db, "TypedBaseItems", "EpisodeTitle", "Text", existingColumnNames); AddColumn(db, "TypedBaseItems", "IsRepeat", "BIT", existingColumnNames); AddColumn(db, "TypedBaseItems", "PreferredMetadataLanguage", "Text", existingColumnNames); AddColumn(db, "TypedBaseItems", "PreferredMetadataCountryCode", "Text", existingColumnNames); - AddColumn(db, "TypedBaseItems", "IsHD", "BIT", existingColumnNames); - AddColumn(db, "TypedBaseItems", "ExternalEtag", "Text", existingColumnNames); AddColumn(db, "TypedBaseItems", "DateLastRefreshed", "DATETIME", existingColumnNames); AddColumn(db, "TypedBaseItems", "DateLastSaved", "DATETIME", existingColumnNames); AddColumn(db, "TypedBaseItems", "IsInMixedFolder", "BIT", existingColumnNames); @@ -244,8 +222,7 @@ namespace Emby.Server.Implementations.Data AddColumn(db, "TypedBaseItems", "ProviderIds", "Text", existingColumnNames); AddColumn(db, "TypedBaseItems", "Images", "Text", existingColumnNames); AddColumn(db, "TypedBaseItems", "ProductionLocations", "Text", existingColumnNames); - AddColumn(db, "TypedBaseItems", "ThemeSongIds", "Text", existingColumnNames); - AddColumn(db, "TypedBaseItems", "ThemeVideoIds", "Text", existingColumnNames); + AddColumn(db, "TypedBaseItems", "ExtraIds", "Text", existingColumnNames); AddColumn(db, "TypedBaseItems", "TotalBitrate", "INT", existingColumnNames); AddColumn(db, "TypedBaseItems", "ExtraType", "Text", existingColumnNames); AddColumn(db, "TypedBaseItems", "Artists", "Text", existingColumnNames); @@ -254,6 +231,8 @@ namespace Emby.Server.Implementations.Data AddColumn(db, "TypedBaseItems", "SeriesPresentationUniqueKey", "Text", existingColumnNames); AddColumn(db, "TypedBaseItems", "ShowId", "Text", existingColumnNames); AddColumn(db, "TypedBaseItems", "OwnerId", "Text", existingColumnNames); + AddColumn(db, "TypedBaseItems", "Width", "INT", existingColumnNames); + AddColumn(db, "TypedBaseItems", "Height", "INT", existingColumnNames); existingColumnNames = GetColumnNames(db, "ItemValues"); AddColumn(db, "ItemValues", "CleanValue", "Text", existingColumnNames); @@ -274,6 +253,11 @@ namespace Emby.Server.Implementations.Data AddColumn(db, "MediaStreams", "RefFrames", "INT", existingColumnNames); AddColumn(db, "MediaStreams", "KeyFrames", "TEXT", existingColumnNames); AddColumn(db, "MediaStreams", "IsAnamorphic", "BIT", existingColumnNames); + + AddColumn(db, "MediaStreams", "ColorPrimaries", "TEXT", existingColumnNames); + AddColumn(db, "MediaStreams", "ColorSpace", "TEXT", existingColumnNames); + AddColumn(db, "MediaStreams", "ColorTransfer", "TEXT", existingColumnNames); + }, TransactionMode); string[] postQueries = @@ -282,6 +266,7 @@ namespace Emby.Server.Implementations.Data // 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", @@ -316,7 +301,6 @@ namespace Emby.Server.Implementations.Data "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_GuidMediaTypeIsFolderIsVirtualItem on TypedBaseItems(Guid,MediaType,IsFolder,IsVirtualItem)", "create index if not exists idx_CleanNameType on TypedBaseItems(CleanName,Type)", // covering index @@ -359,32 +343,7 @@ namespace Emby.Server.Implementations.Data //await Vacuum(_connection).ConfigureAwait(false); } - userDataRepo.Initialize(WriteLock, _connection); - - _shrinkMemoryTimer = _timerFactory.Create(OnShrinkMemoryTimerCallback, null, TimeSpan.FromMinutes(1), TimeSpan.FromMinutes(30)); - } - - private void OnShrinkMemoryTimerCallback(object state) - { - try - { - using (WriteLock.Write()) - { - using (var connection = CreateConnection()) - { - connection.RunQueries(new string[] - { - "pragma shrink_memory" - }); - } - } - - GC.Collect(); - } - catch (Exception ex) - { - Logger.ErrorException("Error running shrink memory", ex); - } + userDataRepo.Initialize(WriteLock, _connection, userManager); } private readonly string[] _retriveItemColumns = @@ -395,12 +354,7 @@ namespace Emby.Server.Implementations.Data "EndDate", "ChannelId", "IsMovie", - "IsSports", - "IsKids", "IsSeries", - "IsLive", - "IsNews", - "IsPremiere", "EpisodeTitle", "IsRepeat", "CommunityRating", @@ -409,8 +363,8 @@ namespace Emby.Server.Implementations.Data "IsLocked", "PreferredMetadataLanguage", "PreferredMetadataCountryCode", - "IsHD", - "ExternalEtag", + "Width", + "Height", "DateLastRefreshed", "Name", "Path", @@ -419,7 +373,6 @@ namespace Emby.Server.Implementations.Data "ParentIndexNumber", "ProductionYear", "OfficialRating", - "HomePageUrl", "ForcedSortName", "RunTimeTicks", "DateCreated", @@ -452,8 +405,7 @@ namespace Emby.Server.Implementations.Data "ProviderIds", "Images", "ProductionLocations", - "ThemeSongIds", - "ThemeVideoIds", + "ExtraIds", "TotalBitrate", "ExtraType", "Artists", @@ -497,7 +449,10 @@ namespace Emby.Server.Implementations.Data "IsAvc", "Title", "TimeBase", - "CodecTimeBase" + "CodecTimeBase", + "ColorPrimaries", + "ColorSpace", + "ColorTransfer" }; private string GetSaveItemCommandText() @@ -511,13 +466,8 @@ namespace Emby.Server.Implementations.Data "StartDate", "EndDate", "ChannelId", - "IsKids", "IsMovie", - "IsSports", "IsSeries", - "IsLive", - "IsNews", - "IsPremiere", "EpisodeTitle", "IsRepeat", "CommunityRating", @@ -537,13 +487,12 @@ namespace Emby.Server.Implementations.Data "SortName", "ForcedSortName", "RunTimeTicks", - "HomePageUrl", "DateCreated", "DateModified", "PreferredMetadataLanguage", "PreferredMetadataCountryCode", - "IsHD", - "ExternalEtag", + "Width", + "Height", "DateLastRefreshed", "DateLastSaved", "IsInMixedFolder", @@ -574,8 +523,7 @@ namespace Emby.Server.Implementations.Data "ProviderIds", "Images", "ProductionLocations", - "ThemeSongIds", - "ThemeVideoIds", + "ExtraIds", "TotalBitrate", "ExtraType", "Artists", @@ -699,46 +647,58 @@ namespace Emby.Server.Implementations.Data var statements = PrepareAllSafe(db, new string[] { GetSaveItemCommandText(), - "delete from AncestorIds where ItemId=@ItemId", - "insert into AncestorIds (ItemId, AncestorId, AncestorIdText) values (@ItemId, @AncestorId, @AncestorIdText)" + "delete from AncestorIds where ItemId=@ItemId" + }).ToList(); using (var saveItemStatement = statements[0]) { using (var deleteAncestorsStatement = statements[1]) { - using (var updateAncestorsStatement = statements[2]) + foreach (var tuple in tuples) { - foreach (var tuple in tuples) + if (requiresReset) { - if (requiresReset) - { - saveItemStatement.Reset(); - } + saveItemStatement.Reset(); + } - var item = tuple.Item1; - var topParent = tuple.Item3; - var userDataKey = tuple.Item4; + var item = tuple.Item1; + var topParent = tuple.Item3; + var userDataKey = tuple.Item4; - SaveItem(item, topParent, userDataKey, saveItemStatement); - //Logger.Debug(_saveItemCommand.CommandText); + SaveItem(item, topParent, userDataKey, saveItemStatement); + //Logger.Debug(_saveItemCommand.CommandText); - var inheritedTags = tuple.Item5; + var inheritedTags = tuple.Item5; - if (item.SupportsAncestors) - { - UpdateAncestors(item.Id, tuple.Item2, db, deleteAncestorsStatement, updateAncestorsStatement); - } + if (item.SupportsAncestors) + { + UpdateAncestors(item.Id, tuple.Item2, db, deleteAncestorsStatement); + } - UpdateItemValues(item.Id, GetItemValuesToSave(item, inheritedTags), db); + UpdateItemValues(item.Id, GetItemValuesToSave(item, inheritedTags), db); - requiresReset = true; - } + requiresReset = true; } } } } + private string GetPathToSave(string path) + { + if (path == null) + { + return null; + } + + return _appHost.ReverseVirtualPath(path); + } + + private string RestorePath(string path) + { + return _appHost.ExpandVirtualPath(path); + } + private void SaveItem(BaseItem item, BaseItem topParent, string userDataKey, IStatement saveItemStatement) { saveItemStatement.TryBind("@guid", item.Id); @@ -746,14 +706,14 @@ namespace Emby.Server.Implementations.Data if (TypeRequiresDeserialization(item.GetType())) { - saveItemStatement.TryBind("@data", _jsonSerializer.SerializeToBytes(item, _memoryStreamProvider)); + saveItemStatement.TryBind("@data", _jsonSerializer.SerializeToBytes(item)); } else { saveItemStatement.TryBindNull("@data"); } - saveItemStatement.TryBind("@Path", item.Path); + saveItemStatement.TryBind("@Path", GetPathToSave(item.Path)); var hasStartDate = item as IHasStartDate; if (hasStartDate != null) @@ -774,30 +734,20 @@ namespace Emby.Server.Implementations.Data saveItemStatement.TryBindNull("@EndDate"); } - saveItemStatement.TryBind("@ChannelId", item.ChannelId); + saveItemStatement.TryBind("@ChannelId", item.ChannelId.Equals(Guid.Empty) ? null : item.ChannelId.ToString("N")); var hasProgramAttributes = item as IHasProgramAttributes; if (hasProgramAttributes != null) { - saveItemStatement.TryBind("@IsKids", hasProgramAttributes.IsKids); saveItemStatement.TryBind("@IsMovie", hasProgramAttributes.IsMovie); - saveItemStatement.TryBind("@IsSports", hasProgramAttributes.IsSports); saveItemStatement.TryBind("@IsSeries", hasProgramAttributes.IsSeries); - saveItemStatement.TryBind("@IsLive", hasProgramAttributes.IsLive); - saveItemStatement.TryBind("@IsNews", hasProgramAttributes.IsNews); - saveItemStatement.TryBind("@IsPremiere", hasProgramAttributes.IsPremiere); saveItemStatement.TryBind("@EpisodeTitle", hasProgramAttributes.EpisodeTitle); saveItemStatement.TryBind("@IsRepeat", hasProgramAttributes.IsRepeat); } else { - saveItemStatement.TryBindNull("@IsKids"); saveItemStatement.TryBindNull("@IsMovie"); - saveItemStatement.TryBindNull("@IsSports"); saveItemStatement.TryBindNull("@IsSeries"); - saveItemStatement.TryBindNull("@IsLive"); - saveItemStatement.TryBindNull("@IsNews"); - saveItemStatement.TryBindNull("@IsPremiere"); saveItemStatement.TryBindNull("@EpisodeTitle"); saveItemStatement.TryBindNull("@IsRepeat"); } @@ -815,7 +765,7 @@ namespace Emby.Server.Implementations.Data saveItemStatement.TryBind("@ProductionYear", item.ProductionYear); var parentId = item.ParentId; - if (parentId == Guid.Empty) + if (parentId.Equals(Guid.Empty)) { saveItemStatement.TryBindNull("@ParentId"); } @@ -824,9 +774,9 @@ namespace Emby.Server.Implementations.Data saveItemStatement.TryBind("@ParentId", parentId); } - if (item.Genres.Count > 0) + if (item.Genres.Length > 0) { - saveItemStatement.TryBind("@Genres", string.Join("|", item.Genres.ToArray())); + saveItemStatement.TryBind("@Genres", string.Join("|", item.Genres)); } else { @@ -841,14 +791,28 @@ namespace Emby.Server.Implementations.Data saveItemStatement.TryBind("@RunTimeTicks", item.RunTimeTicks); - saveItemStatement.TryBind("@HomePageUrl", item.HomePageUrl); saveItemStatement.TryBind("@DateCreated", item.DateCreated); saveItemStatement.TryBind("@DateModified", item.DateModified); saveItemStatement.TryBind("@PreferredMetadataLanguage", item.PreferredMetadataLanguage); saveItemStatement.TryBind("@PreferredMetadataCountryCode", item.PreferredMetadataCountryCode); - saveItemStatement.TryBind("@IsHD", item.IsHD); - saveItemStatement.TryBind("@ExternalEtag", item.ExternalEtag); + + if (item.Width > 0) + { + saveItemStatement.TryBind("@Width", item.Width); + } + else + { + saveItemStatement.TryBindNull("@Width"); + } + if (item.Height > 0) + { + saveItemStatement.TryBind("@Height", item.Height); + } + else + { + saveItemStatement.TryBindNull("@Height"); + } if (item.DateLastRefreshed != default(DateTime)) { @@ -897,7 +861,15 @@ namespace Emby.Server.Implementations.Data saveItemStatement.TryBindNull("@Audio"); } - saveItemStatement.TryBind("@ExternalServiceId", item.ServiceName); + var livetvChannel = item as LiveTvChannel; + if (livetvChannel != null) + { + saveItemStatement.TryBind("@ExternalServiceId", livetvChannel.ServiceName); + } + else + { + saveItemStatement.TryBindNull("@ExternalServiceId"); + } if (item.Tags.Length > 0) { @@ -924,7 +896,7 @@ namespace Emby.Server.Implementations.Data } var trailer = item as Trailer; - if (trailer != null && trailer.TrailerTypes.Count > 0) + if (trailer != null && trailer.TrailerTypes.Length > 0) { saveItemStatement.TryBind("@TrailerTypes", string.Join("|", trailer.TrailerTypes.Select(i => i.ToString()).ToArray())); } @@ -970,10 +942,10 @@ namespace Emby.Server.Implementations.Data saveItemStatement.TryBind("@Album", item.Album); saveItemStatement.TryBind("@IsVirtualItem", item.IsVirtualItem); - var hasSeries = item as IHasSeries; - if (hasSeries != null) + var hasSeriesName = item as IHasSeriesName; + if (hasSeriesName != null) { - saveItemStatement.TryBind("@SeriesName", hasSeries.SeriesName); + saveItemStatement.TryBind("@SeriesName", hasSeriesName.SeriesName); } else { @@ -993,7 +965,10 @@ namespace Emby.Server.Implementations.Data if (episode != null) { saveItemStatement.TryBind("@SeasonName", episode.SeasonName); - saveItemStatement.TryBind("@SeasonId", episode.SeasonId); + + var nullableSeasonId = episode.SeasonId.Equals(Guid.Empty) ? (Guid?)null : episode.SeasonId; + + saveItemStatement.TryBind("@SeasonId", nullableSeasonId); } else { @@ -1001,9 +976,12 @@ namespace Emby.Server.Implementations.Data saveItemStatement.TryBindNull("@SeasonId"); } + var hasSeries = item as IHasSeries; if (hasSeries != null) { - saveItemStatement.TryBind("@SeriesId", hasSeries.SeriesId); + var nullableSeriesId = hasSeries.SeriesId.Equals(Guid.Empty) ? (Guid?)null : hasSeries.SeriesId; + + saveItemStatement.TryBind("@SeriesId", nullableSeriesId); saveItemStatement.TryBind("@SeriesPresentationUniqueKey", hasSeries.SeriesPresentationUniqueKey); } else @@ -1027,22 +1005,13 @@ namespace Emby.Server.Implementations.Data saveItemStatement.TryBindNull("@ProductionLocations"); } - if (item.ThemeSongIds.Length > 0) + if (item.ExtraIds.Length > 0) { - saveItemStatement.TryBind("@ThemeSongIds", string.Join("|", item.ThemeSongIds.ToArray())); + saveItemStatement.TryBind("@ExtraIds", string.Join("|", item.ExtraIds.ToArray())); } else { - saveItemStatement.TryBindNull("@ThemeSongIds"); - } - - if (item.ThemeVideoIds.Length > 0) - { - saveItemStatement.TryBind("@ThemeVideoIds", string.Join("|", item.ThemeVideoIds.ToArray())); - } - else - { - saveItemStatement.TryBindNull("@ThemeVideoIds"); + saveItemStatement.TryBindNull("@ExtraIds"); } saveItemStatement.TryBind("@TotalBitrate", item.TotalBitrate); @@ -1089,7 +1058,7 @@ namespace Emby.Server.Implementations.Data } var ownerId = item.OwnerId; - if (ownerId != Guid.Empty) + if (!ownerId.Equals(Guid.Empty)) { saveItemStatement.TryBind("@OwnerId", ownerId); } @@ -1193,7 +1162,7 @@ namespace Emby.Server.Implementations.Data path = string.Empty; } - return path + + return GetPathToSave(path) + delimeter + image.DateModified.Ticks.ToString(CultureInfo.InvariantCulture) + delimeter + @@ -1215,7 +1184,7 @@ namespace Emby.Server.Implementations.Data var image = new ItemImageInfo(); - image.Path = parts[0]; + image.Path = RestorePath(parts[0]); long ticks; if (long.TryParse(parts[1], NumberStyles.Any, CultureInfo.InvariantCulture, out ticks)) @@ -1255,7 +1224,7 @@ namespace Emby.Server.Implementations.Data /// <exception cref="System.ArgumentException"></exception> public BaseItem RetrieveItem(Guid id) { - if (id == Guid.Empty) + if (id.Equals(Guid.Empty)) { throw new ArgumentNullException("id"); } @@ -1324,15 +1293,6 @@ namespace Emby.Server.Implementations.Data { return false; } - - if (type == typeof(ManualCollectionsFolder)) - { - return false; - } - if (type == typeof(CameraUploadsFolder)) - { - return false; - } if (type == typeof(PlaylistsFolder)) { return false; @@ -1351,22 +1311,10 @@ namespace Emby.Server.Implementations.Data { return false; } - if (type == typeof(RecordingGroup)) - { - return false; - } if (type == typeof(LiveTvProgram)) { return false; } - if (type == typeof(LiveTvAudioRecording)) - { - return false; - } - if (type == typeof(AudioPodcast)) - { - return false; - } if (type == typeof(AudioBook)) { return false; @@ -1386,10 +1334,10 @@ namespace Emby.Server.Implementations.Data private BaseItem GetItem(IReadOnlyList<IResultSetValue> reader, InternalItemsQuery query) { - return GetItem(reader, query, HasProgramAttributes(query), HasEpisodeAttributes(query), HasStartDate(query), HasTrailerTypes(query), HasArtistFields(query), HasSeriesFields(query)); + return GetItem(reader, query, HasProgramAttributes(query), HasEpisodeAttributes(query), HasServiceName(query), HasStartDate(query), HasTrailerTypes(query), HasArtistFields(query), HasSeriesFields(query)); } - private BaseItem GetItem(IReadOnlyList<IResultSetValue> reader, InternalItemsQuery query, bool enableProgramAttributes, bool hasEpisodeAttributes, bool queryHasStartDate, bool hasTrailerTypes, bool hasArtistFields, bool hasSeriesFields) + private BaseItem GetItem(IReadOnlyList<IResultSetValue> reader, InternalItemsQuery query, bool enableProgramAttributes, bool hasEpisodeAttributes, bool hasServiceName, bool queryHasStartDate, bool hasTrailerTypes, bool hasArtistFields, bool hasSeriesFields) { var typeString = reader.GetString(0); @@ -1406,7 +1354,7 @@ namespace Emby.Server.Implementations.Data if (TypeRequiresDeserialization(type)) { - using (var stream = _memoryStreamProvider.CreateNew(reader[1].ToBlob())) + using (var stream = new MemoryStream(reader[1].ToBlob())) { stream.Position = 0; @@ -1454,13 +1402,13 @@ namespace Emby.Server.Implementations.Data if (!reader.IsDBNull(index)) { - item.EndDate = reader[index].ReadDateTime(); + item.EndDate = reader[index].TryReadDateTime(); } index++; if (!reader.IsDBNull(index)) { - item.ChannelId = reader.GetString(index); + item.ChannelId = new Guid(reader.GetString(index)); } index++; @@ -1477,42 +1425,12 @@ namespace Emby.Server.Implementations.Data if (!reader.IsDBNull(index)) { - hasProgramAttributes.IsSports = reader.GetBoolean(index); - } - index++; - - if (!reader.IsDBNull(index)) - { - hasProgramAttributes.IsKids = reader.GetBoolean(index); - } - index++; - - if (!reader.IsDBNull(index)) - { hasProgramAttributes.IsSeries = reader.GetBoolean(index); } index++; if (!reader.IsDBNull(index)) { - hasProgramAttributes.IsLive = reader.GetBoolean(index); - } - index++; - - if (!reader.IsDBNull(index)) - { - hasProgramAttributes.IsNews = reader.GetBoolean(index); - } - index++; - - if (!reader.IsDBNull(index)) - { - hasProgramAttributes.IsPremiere = reader.GetBoolean(index); - } - index++; - - if (!reader.IsDBNull(index)) - { hasProgramAttributes.EpisodeTitle = reader.GetString(index); } index++; @@ -1525,7 +1443,7 @@ namespace Emby.Server.Implementations.Data } else { - index += 9; + index += 4; } } @@ -1571,17 +1489,20 @@ namespace Emby.Server.Implementations.Data index++; } - if (!reader.IsDBNull(index)) + if (HasField(query, ItemFields.Width)) { - item.IsHD = reader.GetBoolean(index); + if (!reader.IsDBNull(index)) + { + item.Width = reader.GetInt32(index); + } + index++; } - index++; - if (HasField(query, ItemFields.ExternalEtag)) + if (HasField(query, ItemFields.Height)) { if (!reader.IsDBNull(index)) { - item.ExternalEtag = reader.GetString(index); + item.Height = reader.GetInt32(index); } index++; } @@ -1603,13 +1524,13 @@ namespace Emby.Server.Implementations.Data if (!reader.IsDBNull(index)) { - item.Path = reader.GetString(index); + item.Path = RestorePath(reader.GetString(index)); } index++; if (!reader.IsDBNull(index)) { - item.PremiereDate = reader[index].ReadDateTime(); + item.PremiereDate = reader[index].TryReadDateTime(); } index++; @@ -1640,15 +1561,6 @@ namespace Emby.Server.Implementations.Data } index++; - if (HasField(query, ItemFields.HomePageUrl)) - { - if (!reader.IsDBNull(index)) - { - item.HomePageUrl = reader.GetString(index); - } - index++; - } - if (HasField(query, ItemFields.SortName)) { if (!reader.IsDBNull(index)) @@ -1686,7 +1598,7 @@ namespace Emby.Server.Implementations.Data { if (!reader.IsDBNull(index)) { - item.Genres = reader.GetString(index).Split('|').Where(i => !string.IsNullOrWhiteSpace(i)).ToList(); + item.Genres = reader.GetString(index).Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries); } index++; } @@ -1709,11 +1621,18 @@ namespace Emby.Server.Implementations.Data // TODO: Even if not needed by apps, the server needs it internally // But get this excluded from contexts where it is not needed - if (!reader.IsDBNull(index)) + if (hasServiceName) { - item.ServiceName = reader.GetString(index); + var livetvChannel = item as LiveTvChannel; + if (livetvChannel != null) + { + if (!reader.IsDBNull(index)) + { + livetvChannel.ServiceName = reader.GetString(index); + } + } + index++; } - index++; if (!reader.IsDBNull(index)) { @@ -1785,7 +1704,7 @@ namespace Emby.Server.Implementations.Data } return (TrailerType?)null; - }).Where(i => i.HasValue).Select(i => i.Value).ToList(); + }).Where(i => i.HasValue).Select(i => i.Value).ToArray(); } } index++; @@ -1815,7 +1734,7 @@ namespace Emby.Server.Implementations.Data var folder = item as Folder; if (folder != null && !reader.IsDBNull(index)) { - folder.DateLastMediaAdded = reader[index].ReadDateTime(); + folder.DateLastMediaAdded = reader[index].TryReadDateTime(); } index++; } @@ -1838,18 +1757,15 @@ namespace Emby.Server.Implementations.Data } index++; - var hasSeries = item as IHasSeries; - if (hasSeriesFields) + var hasSeriesName = item as IHasSeriesName; + if (hasSeriesName != null) { - if (hasSeries != null) + if (!reader.IsDBNull(index)) { - if (!reader.IsDBNull(index)) - { - hasSeries.SeriesName = reader.GetString(index); - } + hasSeriesName.SeriesName = reader.GetString(index); } - index++; } + index++; if (hasEpisodeAttributes) { @@ -1873,6 +1789,7 @@ namespace Emby.Server.Implementations.Data index++; } + var hasSeries = item as IHasSeries; if (hasSeriesFields) { if (hasSeries != null) @@ -1945,20 +1862,11 @@ namespace Emby.Server.Implementations.Data index++; } - if (HasField(query, ItemFields.ThemeSongIds)) + if (HasField(query, ItemFields.ExtraIds)) { if (!reader.IsDBNull(index)) { - item.ThemeSongIds = SplitToGuids(reader.GetString(index)); - } - index++; - } - - if (HasField(query, ItemFields.ThemeVideoIds)) - { - if (!reader.IsDBNull(index)) - { - item.ThemeVideoIds = SplitToGuids(reader.GetString(index)); + item.ExtraIds = SplitToGuids(reader.GetString(index)); } index++; } @@ -2055,36 +1963,14 @@ namespace Emby.Server.Implementations.Data } /// <summary> - /// Gets the critic reviews. - /// </summary> - /// <param name="itemId">The item id.</param> - public List<ItemReview> GetCriticReviews(Guid itemId) - { - return new List<ItemReview>(); - } - - /// <summary> - /// Saves the critic reviews. - /// </summary> - /// <param name="itemId">The item id.</param> - /// <param name="criticReviews">The critic reviews.</param> - public void SaveCriticReviews(Guid itemId, IEnumerable<ItemReview> criticReviews) - { - } - - /// <summary> /// Gets chapters for an item /// </summary> /// <param name="id">The id.</param> /// <returns>IEnumerable{ChapterInfo}.</returns> /// <exception cref="System.ArgumentNullException">id</exception> - public List<ChapterInfo> GetChapters(Guid id) + public List<ChapterInfo> GetChapters(BaseItem item) { CheckDisposed(); - if (id == Guid.Empty) - { - throw new ArgumentNullException("id"); - } using (WriteLock.Read()) { @@ -2094,11 +1980,11 @@ namespace Emby.Server.Implementations.Data using (var statement = PrepareStatementSafe(connection, "select StartPositionTicks,Name,ImagePath,ImageDateModified from " + ChaptersTableName + " where ItemId = @ItemId order by ChapterIndex asc")) { - statement.TryBind("@ItemId", id); + statement.TryBind("@ItemId", item.Id); foreach (var row in statement.ExecuteQuery()) { - list.Add(GetChapter(row)); + list.Add(GetChapter(row, item)); } } @@ -2114,13 +2000,9 @@ namespace Emby.Server.Implementations.Data /// <param name="index">The index.</param> /// <returns>ChapterInfo.</returns> /// <exception cref="System.ArgumentNullException">id</exception> - public ChapterInfo GetChapter(Guid id, int index) + public ChapterInfo GetChapter(BaseItem item, int index) { CheckDisposed(); - if (id == Guid.Empty) - { - throw new ArgumentNullException("id"); - } using (WriteLock.Read()) { @@ -2128,12 +2010,12 @@ namespace Emby.Server.Implementations.Data { using (var statement = PrepareStatementSafe(connection, "select StartPositionTicks,Name,ImagePath,ImageDateModified from " + ChaptersTableName + " where ItemId = @ItemId and ChapterIndex=@ChapterIndex")) { - statement.TryBind("@ItemId", id); + statement.TryBind("@ItemId", item.Id); statement.TryBind("@ChapterIndex", index); foreach (var row in statement.ExecuteQuery()) { - return GetChapter(row); + return GetChapter(row, item); } } } @@ -2146,7 +2028,7 @@ namespace Emby.Server.Implementations.Data /// </summary> /// <param name="reader">The reader.</param> /// <returns>ChapterInfo.</returns> - private ChapterInfo GetChapter(IReadOnlyList<IResultSetValue> reader) + private ChapterInfo GetChapter(IReadOnlyList<IResultSetValue> reader, BaseItem item) { var chapter = new ChapterInfo { @@ -2161,6 +2043,11 @@ namespace Emby.Server.Implementations.Data if (!reader.IsDBNull(2)) { chapter.ImagePath = reader.GetString(2); + + if (!string.IsNullOrEmpty(chapter.ImagePath)) + { + chapter.ImageTag = ImageProcessor.GetImageCacheTag(item, chapter); + } } if (!reader.IsDBNull(3)) @@ -2178,7 +2065,7 @@ namespace Emby.Server.Implementations.Data { CheckDisposed(); - if (id == Guid.Empty) + if (id.Equals(Guid.Empty)) { throw new ArgumentNullException("id"); } @@ -2188,40 +2075,72 @@ namespace Emby.Server.Implementations.Data throw new ArgumentNullException("chapters"); } - var index = 0; - using (WriteLock.Write()) { using (var connection = CreateConnection()) { connection.RunInTransaction(db => { + var idBlob = id.ToGuidBlob(); + // First delete chapters - db.Execute("delete from " + ChaptersTableName + " where ItemId=@ItemId", id.ToGuidBlob()); + db.Execute("delete from " + ChaptersTableName + " where ItemId=@ItemId", idBlob); - using (var saveChapterStatement = PrepareStatement(db, "replace into " + ChaptersTableName + " (ItemId, ChapterIndex, StartPositionTicks, Name, ImagePath, ImageDateModified) values (@ItemId, @ChapterIndex, @StartPositionTicks, @Name, @ImagePath, @ImageDateModified)")) - { - foreach (var chapter in chapters) - { - if (index > 0) - { - saveChapterStatement.Reset(); - } + InsertChapters(idBlob, chapters, db); + + }, TransactionMode); + } + } + } - saveChapterStatement.TryBind("@ItemId", id.ToGuidBlob()); - saveChapterStatement.TryBind("@ChapterIndex", index); - saveChapterStatement.TryBind("@StartPositionTicks", chapter.StartPositionTicks); - saveChapterStatement.TryBind("@Name", chapter.Name); - saveChapterStatement.TryBind("@ImagePath", chapter.ImagePath); - saveChapterStatement.TryBind("@ImageDateModified", chapter.ImageDateModified); + private void InsertChapters(byte[] idBlob, List<ChapterInfo> chapters, IDatabaseConnection db) + { + var startIndex = 0; + var limit = 100; + var chapterIndex = 0; - saveChapterStatement.MoveNext(); + while (startIndex < chapters.Count) + { + var insertText = new StringBuilder("insert into " + ChaptersTableName + " (ItemId, ChapterIndex, StartPositionTicks, Name, ImagePath, ImageDateModified) values "); - index++; - } - } - }, TransactionMode); + var endIndex = Math.Min(chapters.Count, startIndex + limit); + var isSubsequentRow = false; + + for (var i = startIndex; i < endIndex; i++) + { + if (isSubsequentRow) + { + insertText.Append(","); + } + + insertText.AppendFormat("(@ItemId, @ChapterIndex{0}, @StartPositionTicks{0}, @Name{0}, @ImagePath{0}, @ImageDateModified{0})", i.ToString(CultureInfo.InvariantCulture)); + isSubsequentRow = true; } + + using (var statement = PrepareStatementSafe(db, insertText.ToString())) + { + statement.TryBind("@ItemId", idBlob); + + for (var i = startIndex; i < endIndex; i++) + { + var index = i.ToString(CultureInfo.InvariantCulture); + + var chapter = chapters[i]; + + statement.TryBind("@ChapterIndex" + index, chapterIndex); + statement.TryBind("@StartPositionTicks" + index, chapter.StartPositionTicks); + statement.TryBind("@Name" + index, chapter.Name); + statement.TryBind("@ImagePath" + index, chapter.ImagePath); + statement.TryBind("@ImageDateModified" + index, chapter.ImageDateModified); + + chapterIndex++; + } + + statement.Reset(); + statement.MoveNext(); + } + + startIndex += limit; } } @@ -2232,11 +2151,6 @@ namespace Emby.Server.Implementations.Data return false; } - if (query.SimilarTo != null && query.User != null) - { - //return true; - } - var sortingFields = query.OrderBy.Select(i => i.Item1).ToList(); if (sortingFields.Contains(ItemSortBy.IsFavoriteOrLiked, StringComparer.OrdinalIgnoreCase)) @@ -2296,7 +2210,7 @@ namespace Emby.Server.Implementations.Data .Select(i => (ItemFields)Enum.Parse(typeof(ItemFields), i, true)) .ToList(); - private IEnumerable<string> GetColumnNamesFromField(ItemFields field) + private string[] GetColumnNamesFromField(ItemFields field) { if (field == ItemFields.Settings) { @@ -2318,17 +2232,20 @@ namespace Emby.Server.Implementations.Data { return new[] { "Tags" }; } + if (field == ItemFields.IsHD) + { + return Array.Empty<string>(); + } return new[] { field.ToString() }; } private bool HasField(InternalItemsQuery query, ItemFields name) { - var fields = query.DtoOptions.Fields; - switch (name) { - case ItemFields.HomePageUrl: + case ItemFields.Tags: + return query.DtoOptions.ContainsField(name) || HasProgramAttributes(query); case ItemFields.CustomRating: case ItemFields.ProductionLocations: case ItemFields.Settings: @@ -2336,23 +2253,20 @@ namespace Emby.Server.Implementations.Data case ItemFields.Taglines: case ItemFields.SortName: case ItemFields.Studios: - case ItemFields.Tags: - case ItemFields.ThemeSongIds: - case ItemFields.ThemeVideoIds: + case ItemFields.ExtraIds: case ItemFields.DateCreated: case ItemFields.Overview: case ItemFields.Genres: case ItemFields.DateLastMediaAdded: - case ItemFields.ExternalEtag: case ItemFields.PresentationUniqueKey: case ItemFields.InheritedParentalRatingValue: case ItemFields.ExternalSeriesId: case ItemFields.SeriesPresentationUniqueKey: case ItemFields.DateLastRefreshed: case ItemFields.DateLastSaved: - return fields.Contains(name); + return query.DtoOptions.ContainsField(name); case ItemFields.ServiceName: - return true; + return HasServiceName(query); default: return true; } @@ -2382,10 +2296,7 @@ namespace Emby.Server.Implementations.Data var types = new string[] { "Program", - "Recording", "TvChannel", - "LiveTvAudioRecording", - "LiveTvVideoRecording", "LiveTvProgram", "LiveTvTvChannel" }; @@ -2393,6 +2304,36 @@ namespace Emby.Server.Implementations.Data return types.Any(i => query.IncludeItemTypes.Contains(i, StringComparer.OrdinalIgnoreCase)); } + private bool HasServiceName(InternalItemsQuery query) + { + var excludeParentTypes = new string[] + { + "Series", + "Season", + "MusicAlbum", + "MusicArtist", + "PhotoAlbum" + }; + + if (excludeParentTypes.Contains(query.ParentType ?? string.Empty, StringComparer.OrdinalIgnoreCase)) + { + return false; + } + + if (query.IncludeItemTypes.Length == 0) + { + return true; + } + + var types = new string[] + { + "TvChannel", + "LiveTvTvChannel" + }; + + return types.Any(i => query.IncludeItemTypes.Contains(i, StringComparer.OrdinalIgnoreCase)); + } + private bool HasStartDate(InternalItemsQuery query) { var excludeParentTypes = new string[] @@ -2417,9 +2358,6 @@ namespace Emby.Server.Implementations.Data var types = new string[] { "Program", - "Recording", - "LiveTvAudioRecording", - "LiveTvVideoRecording", "LiveTvProgram" }; @@ -2481,9 +2419,7 @@ namespace Emby.Server.Implementations.Data "MusicAlbum", "MusicVideo", "AudioBook", - "AudioPodcast", - "LiveTvAudioRecording", - "Recording" + "AudioPodcast" }; return types.Any(i => query.IncludeItemTypes.Contains(i, StringComparer.OrdinalIgnoreCase)); @@ -2534,13 +2470,8 @@ namespace Emby.Server.Implementations.Data if (!HasProgramAttributes(query)) { - list.Remove("IsKids"); list.Remove("IsMovie"); - list.Remove("IsSports"); list.Remove("IsSeries"); - list.Remove("IsLive"); - list.Remove("IsNews"); - list.Remove("IsPremiere"); list.Remove("EpisodeTitle"); list.Remove("IsRepeat"); list.Remove("ShowId"); @@ -2571,7 +2502,6 @@ namespace Emby.Server.Implementations.Data if (!HasSeriesFields(query)) { list.Remove("SeriesId"); - list.Remove("SeriesName"); } if (!HasEpisodeAttributes(query)) @@ -2587,13 +2517,13 @@ namespace Emby.Server.Implementations.Data if (EnableJoinUserData(query)) { - list.Add("UserData.UserId"); - list.Add("UserData.lastPlayedDate"); - list.Add("UserData.playbackPositionTicks"); - list.Add("UserData.playcount"); - list.Add("UserData.isFavorite"); - list.Add("UserData.played"); - list.Add("UserData.rating"); + list.Add("UserDatas.UserId"); + list.Add("UserDatas.lastPlayedDate"); + list.Add("UserDatas.playbackPositionTicks"); + list.Add("UserDatas.playcount"); + list.Add("UserDatas.isFavorite"); + list.Add("UserDatas.played"); + list.Add("UserDatas.rating"); } if (query.SimilarTo != null) @@ -2603,14 +2533,24 @@ namespace Emby.Server.Implementations.Data var builder = new StringBuilder(); builder.Append("("); - builder.Append("((OfficialRating=@ItemOfficialRating) * 10)"); - //builder.Append("+ ((ProductionYear=@ItemProductionYear) * 10)"); + if (string.IsNullOrEmpty(item.OfficialRating)) + { + builder.Append("((OfficialRating is null) * 10)"); + } + else + { + builder.Append("((OfficialRating=@ItemOfficialRating) * 10)"); + } - builder.Append("+(Select Case When Abs(COALESCE(ProductionYear, 0) - @ItemProductionYear) < 10 Then 2 Else 0 End )"); - builder.Append("+(Select Case When Abs(COALESCE(ProductionYear, 0) - @ItemProductionYear) < 5 Then 2 Else 0 End )"); + if (item.ProductionYear.HasValue) + { + //builder.Append("+ ((ProductionYear=@ItemProductionYear) * 10)"); + builder.Append("+(Select Case When Abs(COALESCE(ProductionYear, 0) - @ItemProductionYear) < 10 Then 10 Else 0 End )"); + builder.Append("+(Select Case When Abs(COALESCE(ProductionYear, 0) - @ItemProductionYear) < 5 Then 5 Else 0 End )"); + } //// genres, tags - builder.Append("+ ((Select count(CleanValue) from ItemValues where ItemId=Guid and Type in (2,3,4,5) and CleanValue in (select CleanValue from itemvalues where ItemId=@SimilarItemId and Type in (2,3,4,5))) * 10)"); + builder.Append("+ ((Select count(CleanValue) from ItemValues where ItemId=Guid and CleanValue in (select CleanValue from itemvalues where ItemId=@SimilarItemId)) * 10)"); //builder.Append("+ ((Select count(CleanValue) from ItemValues where ItemId=Guid and Type=3 and CleanValue in (select CleanValue from itemvalues where ItemId=@SimilarItemId and type=3)) * 3)"); @@ -2623,24 +2563,56 @@ namespace Emby.Server.Implementations.Data list.Add(builder.ToString()); var excludeIds = query.ExcludeItemIds.ToList(); - excludeIds.Add(item.Id.ToString("N")); + excludeIds.Add(item.Id); + excludeIds.AddRange(item.ExtraIds); - if (query.IncludeItemTypes.Length == 0 || query.IncludeItemTypes.Contains(typeof(Trailer).Name)) + query.ExcludeItemIds = excludeIds.ToArray(excludeIds.Count); + query.ExcludeProviderIds = item.ProviderIds; + } + + if (!string.IsNullOrEmpty(query.SearchTerm)) + { + var builder = new StringBuilder(); + builder.Append("("); + + builder.Append("((CleanName like @SearchTermStartsWith or (OriginalTitle not null and OriginalTitle like @SearchTermStartsWith)) * 10)"); + + if (query.SearchTerm.Length > 1) { - var hasTrailers = item as IHasTrailers; - if (hasTrailers != null) - { - excludeIds.AddRange(hasTrailers.GetTrailerIds().Select(i => i.ToString("N"))); - } + builder.Append("+ ((CleanName like @SearchTermContains or (OriginalTitle not null and OriginalTitle like @SearchTermContains)) * 10)"); } - query.ExcludeItemIds = excludeIds.ToArray(excludeIds.Count); - query.ExcludeProviderIds = item.ProviderIds; + builder.Append(") as SearchScore"); + + list.Add(builder.ToString()); } return list.ToArray(list.Count); } + private void BindSearchParams(InternalItemsQuery query, IStatement statement) + { + var searchTerm = query.SearchTerm; + + if (string.IsNullOrEmpty(searchTerm)) + { + return; + } + + searchTerm = FixUnicodeChars(searchTerm); + searchTerm = GetCleanValue(searchTerm); + + var commandText = statement.SQL; + if (commandText.IndexOf("@SearchTermStartsWith", StringComparison.OrdinalIgnoreCase) != -1) + { + statement.TryBind("@SearchTermStartsWith", searchTerm + "%"); + } + if (commandText.IndexOf("@SearchTermContains", StringComparison.OrdinalIgnoreCase) != -1) + { + statement.TryBind("@SearchTermContains", "%" + searchTerm + "%"); + } + } + private void BindSimilarParams(InternalItemsQuery query, IStatement statement) { var item = query.SimilarTo; @@ -2650,9 +2622,22 @@ namespace Emby.Server.Implementations.Data return; } - statement.TryBind("@ItemOfficialRating", item.OfficialRating); - statement.TryBind("@ItemProductionYear", item.ProductionYear ?? 0); - statement.TryBind("@SimilarItemId", item.Id); + var commandText = statement.SQL; + + if (commandText.IndexOf("@ItemOfficialRating", StringComparison.OrdinalIgnoreCase) != -1) + { + statement.TryBind("@ItemOfficialRating", item.OfficialRating); + } + + if (commandText.IndexOf("@ItemProductionYear", StringComparison.OrdinalIgnoreCase) != -1) + { + statement.TryBind("@ItemProductionYear", item.ProductionYear ?? 0); + } + + if (commandText.IndexOf("@SimilarItemId", StringComparison.OrdinalIgnoreCase) != -1) + { + statement.TryBind("@SimilarItemId", item.Id); + } } private string GetJoinUserDataText(InternalItemsQuery query) @@ -2662,7 +2647,7 @@ namespace Emby.Server.Implementations.Data return string.Empty; } - return " left join UserData on UserDataKey=UserData.Key And (UserId=@UserId)"; + return " left join UserDatas on UserDataKey=UserDatas.Key And (UserId=@UserId)"; } private string GetGroupBy(InternalItemsQuery query) @@ -2732,10 +2717,11 @@ namespace Emby.Server.Implementations.Data { if (EnableJoinUserData(query)) { - statement.TryBind("@UserId", query.User.Id); + statement.TryBind("@UserId", query.User.InternalId); } BindSimilarParams(query, statement); + BindSearchParams(query, statement); // Running this again will bind the params GetWhereClauses(query, statement); @@ -2808,15 +2794,17 @@ namespace Emby.Server.Implementations.Data { if (EnableJoinUserData(query)) { - statement.TryBind("@UserId", query.User.Id); + statement.TryBind("@UserId", query.User.InternalId); } BindSimilarParams(query, statement); + BindSearchParams(query, statement); // Running this again will bind the params GetWhereClauses(query, statement); var hasEpisodeAttributes = HasEpisodeAttributes(query); + var hasServiceName = HasServiceName(query); var hasProgramAttributes = HasProgramAttributes(query); var hasStartDate = HasStartDate(query); var hasTrailerTypes = HasTrailerTypes(query); @@ -2825,7 +2813,7 @@ namespace Emby.Server.Implementations.Data foreach (var row in statement.ExecuteQuery()) { - var item = GetItem(row, query, hasProgramAttributes, hasEpisodeAttributes, hasStartDate, hasTrailerTypes, hasArtistFields, hasSeriesFields); + var item = GetItem(row, query, hasProgramAttributes, hasEpisodeAttributes, hasServiceName, hasStartDate, hasTrailerTypes, hasArtistFields, hasSeriesFields); if (item != null) { list.Add(item); @@ -2860,6 +2848,28 @@ namespace Emby.Server.Implementations.Data } } + private string FixUnicodeChars(string buffer) + { + if (buffer.IndexOf('\u2013') > -1) buffer = buffer.Replace('\u2013', '-'); // en dash + if (buffer.IndexOf('\u2014') > -1) buffer = buffer.Replace('\u2014', '-'); // em dash + if (buffer.IndexOf('\u2015') > -1) buffer = buffer.Replace('\u2015', '-'); // horizontal bar + if (buffer.IndexOf('\u2017') > -1) buffer = buffer.Replace('\u2017', '_'); // double low line + if (buffer.IndexOf('\u2018') > -1) buffer = buffer.Replace('\u2018', '\''); // left single quotation mark + if (buffer.IndexOf('\u2019') > -1) buffer = buffer.Replace('\u2019', '\''); // right single quotation mark + if (buffer.IndexOf('\u201a') > -1) buffer = buffer.Replace('\u201a', ','); // single low-9 quotation mark + if (buffer.IndexOf('\u201b') > -1) buffer = buffer.Replace('\u201b', '\''); // single high-reversed-9 quotation mark + if (buffer.IndexOf('\u201c') > -1) buffer = buffer.Replace('\u201c', '\"'); // left double quotation mark + if (buffer.IndexOf('\u201d') > -1) buffer = buffer.Replace('\u201d', '\"'); // right double quotation mark + if (buffer.IndexOf('\u201e') > -1) buffer = buffer.Replace('\u201e', '\"'); // double low-9 quotation mark + if (buffer.IndexOf('\u2026') > -1) buffer = buffer.Replace("\u2026", "..."); // horizontal ellipsis + if (buffer.IndexOf('\u2032') > -1) buffer = buffer.Replace('\u2032', '\''); // prime + if (buffer.IndexOf('\u2033') > -1) buffer = buffer.Replace('\u2033', '\"'); // double prime + if (buffer.IndexOf('\u0060') > -1) buffer = buffer.Replace('\u0060', '\''); // grave accent + if (buffer.IndexOf('\u00B4') > -1) buffer = buffer.Replace('\u00B4', '\''); // acute accent + + return buffer; + } + private void AddItem(List<BaseItem> items, BaseItem newItem) { var providerIds = newItem.ProviderIds.ToList(); @@ -2989,15 +2999,15 @@ namespace Emby.Server.Implementations.Data if (EnableGroupByPresentationUniqueKey(query)) { - commandText += " select count (distinct PresentationUniqueKey)" + GetFromText(); + commandText += " select " + string.Join(",", GetFinalColumnsToSelect(query, new[] { "count (distinct PresentationUniqueKey)" })) + GetFromText(); } else if (query.GroupBySeriesPresentationUniqueKey) { - commandText += " select count (distinct SeriesPresentationUniqueKey)" + GetFromText(); + commandText += " select " + string.Join(",", GetFinalColumnsToSelect(query, new[] { "count (distinct SeriesPresentationUniqueKey)" })) + GetFromText(); } else { - commandText += " select count (guid)" + GetFromText(); + commandText += " select " + string.Join(",", GetFinalColumnsToSelect(query, new[] { "count (guid)" })) + GetFromText(); } commandText += GetJoinUserDataText(query); @@ -3020,15 +3030,17 @@ namespace Emby.Server.Implementations.Data { if (EnableJoinUserData(query)) { - statement.TryBind("@UserId", query.User.Id); + statement.TryBind("@UserId", query.User.InternalId); } BindSimilarParams(query, statement); + BindSearchParams(query, statement); // Running this again will bind the params GetWhereClauses(query, statement); var hasEpisodeAttributes = HasEpisodeAttributes(query); + var hasServiceName = HasServiceName(query); var hasProgramAttributes = HasProgramAttributes(query); var hasStartDate = HasStartDate(query); var hasTrailerTypes = HasTrailerTypes(query); @@ -3037,7 +3049,7 @@ namespace Emby.Server.Implementations.Data foreach (var row in statement.ExecuteQuery()) { - var item = GetItem(row, query, hasProgramAttributes, hasEpisodeAttributes, hasStartDate, hasTrailerTypes, hasArtistFields, hasSeriesFields); + var item = GetItem(row, query, hasProgramAttributes, hasEpisodeAttributes, hasServiceName, hasStartDate, hasTrailerTypes, hasArtistFields, hasSeriesFields); if (item != null) { list.Add(item); @@ -3052,10 +3064,11 @@ namespace Emby.Server.Implementations.Data { if (EnableJoinUserData(query)) { - statement.TryBind("@UserId", query.User.Id); + statement.TryBind("@UserId", query.User.InternalId); } BindSimilarParams(query, statement); + BindSearchParams(query, statement); // Running this again will bind the params GetWhereClauses(query, statement); @@ -3083,12 +3096,19 @@ namespace Emby.Server.Implementations.Data { if (orderBy.Count == 0) { - orderBy.Add(new Tuple<string, SortOrder>("SimilarityScore", SortOrder.Descending)); - orderBy.Add(new Tuple<string, SortOrder>(ItemSortBy.Random, SortOrder.Ascending)); + orderBy.Add(new ValueTuple<string, SortOrder>("SimilarityScore", SortOrder.Descending)); + orderBy.Add(new ValueTuple<string, SortOrder>(ItemSortBy.Random, SortOrder.Ascending)); //orderBy.Add(new Tuple<string, SortOrder>(ItemSortBy.Random, SortOrder.Ascending)); } } + if (!string.IsNullOrEmpty(query.SearchTerm)) + { + orderBy = new List<(string, SortOrder)>(); + orderBy.Add(new ValueTuple<string, SortOrder>("SearchScore", SortOrder.Descending)); + orderBy.Add(new ValueTuple<string, SortOrder>(ItemSortBy.SortName, SortOrder.Ascending)); + } + query.OrderBy = orderBy.ToArray(); if (orderBy.Count == 0) @@ -3111,81 +3131,81 @@ namespace Emby.Server.Implementations.Data }).ToArray()); } - private Tuple<string, bool> MapOrderByField(string name, InternalItemsQuery query) + private ValueTuple<string, bool> MapOrderByField(string name, InternalItemsQuery query) { if (string.Equals(name, ItemSortBy.AirTime, StringComparison.OrdinalIgnoreCase)) { // TODO - return new Tuple<string, bool>("SortName", false); + return new ValueTuple<string, bool>("SortName", false); } if (string.Equals(name, ItemSortBy.Runtime, StringComparison.OrdinalIgnoreCase)) { - return new Tuple<string, bool>("RuntimeTicks", false); + return new ValueTuple<string, bool>("RuntimeTicks", false); } if (string.Equals(name, ItemSortBy.Random, StringComparison.OrdinalIgnoreCase)) { - return new Tuple<string, bool>("RANDOM()", false); + return new ValueTuple<string, bool>("RANDOM()", false); } if (string.Equals(name, ItemSortBy.DatePlayed, StringComparison.OrdinalIgnoreCase)) { if (query.GroupBySeriesPresentationUniqueKey) { - return new Tuple<string, bool>("MAX(LastPlayedDate)", false); + return new ValueTuple<string, bool>("MAX(LastPlayedDate)", false); } - return new Tuple<string, bool>("LastPlayedDate", false); + return new ValueTuple<string, bool>("LastPlayedDate", false); } if (string.Equals(name, ItemSortBy.PlayCount, StringComparison.OrdinalIgnoreCase)) { - return new Tuple<string, bool>("PlayCount", false); + return new ValueTuple<string, bool>("PlayCount", false); } if (string.Equals(name, ItemSortBy.IsFavoriteOrLiked, StringComparison.OrdinalIgnoreCase)) { // (Select Case When Abs(COALESCE(ProductionYear, 0) - @ItemProductionYear) < 10 Then 2 Else 0 End ) - return new Tuple<string, bool>("(Select Case When IsFavorite is null Then 0 Else IsFavorite End )", true); + return new ValueTuple<string, bool>("(Select Case When IsFavorite is null Then 0 Else IsFavorite End )", true); } if (string.Equals(name, ItemSortBy.IsFolder, StringComparison.OrdinalIgnoreCase)) { - return new Tuple<string, bool>("IsFolder", true); + return new ValueTuple<string, bool>("IsFolder", true); } if (string.Equals(name, ItemSortBy.IsPlayed, StringComparison.OrdinalIgnoreCase)) { - return new Tuple<string, bool>("played", true); + return new ValueTuple<string, bool>("played", true); } if (string.Equals(name, ItemSortBy.IsUnplayed, StringComparison.OrdinalIgnoreCase)) { - return new Tuple<string, bool>("played", false); + return new ValueTuple<string, bool>("played", false); } if (string.Equals(name, ItemSortBy.DateLastContentAdded, StringComparison.OrdinalIgnoreCase)) { - return new Tuple<string, bool>("DateLastMediaAdded", false); + return new ValueTuple<string, bool>("DateLastMediaAdded", false); } if (string.Equals(name, ItemSortBy.Artist, StringComparison.OrdinalIgnoreCase)) { - return new Tuple<string, bool>("(select CleanValue from itemvalues where ItemId=Guid and Type=0 LIMIT 1)", false); + return new ValueTuple<string, bool>("(select CleanValue from itemvalues where ItemId=Guid and Type=0 LIMIT 1)", false); } if (string.Equals(name, ItemSortBy.AlbumArtist, StringComparison.OrdinalIgnoreCase)) { - return new Tuple<string, bool>("(select CleanValue from itemvalues where ItemId=Guid and Type=1 LIMIT 1)", false); + return new ValueTuple<string, bool>("(select CleanValue from itemvalues where ItemId=Guid and Type=1 LIMIT 1)", false); } if (string.Equals(name, ItemSortBy.OfficialRating, StringComparison.OrdinalIgnoreCase)) { - return new Tuple<string, bool>("InheritedParentalRatingValue", false); + return new ValueTuple<string, bool>("InheritedParentalRatingValue", false); } if (string.Equals(name, ItemSortBy.Studio, StringComparison.OrdinalIgnoreCase)) { - return new Tuple<string, bool>("(select CleanValue from itemvalues where ItemId=Guid and Type=3 LIMIT 1)", false); + return new ValueTuple<string, bool>("(select CleanValue from itemvalues where ItemId=Guid and Type=3 LIMIT 1)", false); } if (string.Equals(name, ItemSortBy.SeriesDatePlayed, StringComparison.OrdinalIgnoreCase)) { - return new Tuple<string, bool>("(Select MAX(LastPlayedDate) from TypedBaseItems B" + GetJoinUserDataText(query) + " where Played=1 and B.SeriesPresentationUniqueKey=A.PresentationUniqueKey)", false); + return new ValueTuple<string, bool>("(Select MAX(LastPlayedDate) from TypedBaseItems B" + GetJoinUserDataText(query) + " where Played=1 and B.SeriesPresentationUniqueKey=A.PresentationUniqueKey)", false); } if (string.Equals(name, ItemSortBy.SeriesSortName, StringComparison.OrdinalIgnoreCase)) { - return new Tuple<string, bool>("SeriesName", false); + return new ValueTuple<string, bool>("SeriesName", false); } - return new Tuple<string, bool>(name, false); + return new ValueTuple<string, bool>(name, false); } public List<Guid> GetItemIdsList(InternalItemsQuery query) @@ -3240,10 +3260,11 @@ namespace Emby.Server.Implementations.Data { if (EnableJoinUserData(query)) { - statement.TryBind("@UserId", query.User.Id); + statement.TryBind("@UserId", query.User.InternalId); } BindSimilarParams(query, statement); + BindSearchParams(query, statement); // Running this again will bind the params GetWhereClauses(query, statement); @@ -3311,7 +3332,7 @@ namespace Emby.Server.Implementations.Data { if (EnableJoinUserData(query)) { - statement.TryBind("@UserId", query.User.Id); + statement.TryBind("@UserId", query.User.InternalId); } // Running this again will bind the params @@ -3405,15 +3426,15 @@ namespace Emby.Server.Implementations.Data if (EnableGroupByPresentationUniqueKey(query)) { - commandText += " select count (distinct PresentationUniqueKey)" + GetFromText(); + commandText += " select " + string.Join(",", GetFinalColumnsToSelect(query, new[] { "count (distinct PresentationUniqueKey)" })) + GetFromText(); } else if (query.GroupBySeriesPresentationUniqueKey) { - commandText += " select count (distinct SeriesPresentationUniqueKey)" + GetFromText(); + commandText += " select " + string.Join(",", GetFinalColumnsToSelect(query, new[] { "count (distinct SeriesPresentationUniqueKey)" })) + GetFromText(); } else { - commandText += " select count (guid)" + GetFromText(); + commandText += " select " + string.Join(",", GetFinalColumnsToSelect(query, new[] { "count (guid)" })) + GetFromText(); } commandText += GetJoinUserDataText(query); @@ -3437,10 +3458,11 @@ namespace Emby.Server.Implementations.Data { if (EnableJoinUserData(query)) { - statement.TryBind("@UserId", query.User.Id); + statement.TryBind("@UserId", query.User.InternalId); } BindSimilarParams(query, statement); + BindSearchParams(query, statement); // Running this again will bind the params GetWhereClauses(query, statement); @@ -3458,10 +3480,11 @@ namespace Emby.Server.Implementations.Data { if (EnableJoinUserData(query)) { - statement.TryBind("@UserId", query.User.Id); + statement.TryBind("@UserId", query.User.InternalId); } BindSimilarParams(query, statement); + BindSearchParams(query, statement); // Running this again will bind the params GetWhereClauses(query, statement); @@ -3527,14 +3550,69 @@ namespace Emby.Server.Implementations.Data { //whereClauses.Add("(UserId is null or UserId=@UserId)"); } + + var minWidth = query.MinWidth; + var maxWidth = query.MaxWidth; + if (query.IsHD.HasValue) { - whereClauses.Add("IsHD=@IsHD"); + var threshold = 1200; + if (query.IsHD.Value) + { + minWidth = threshold; + } + else + { + maxWidth = threshold - 1; + } + } + + if (query.Is4K.HasValue) + { + var threshold = 3800; + if (query.Is4K.Value) + { + minWidth = threshold; + } + else + { + maxWidth = threshold - 1; + } + } + + if (minWidth.HasValue) + { + whereClauses.Add("Width>=@MinWidth"); + if (statement != null) + { + statement.TryBind("@MinWidth", minWidth); + } + } + if (query.MinHeight.HasValue) + { + whereClauses.Add("Height>=@MinHeight"); if (statement != null) { - statement.TryBind("@IsHD", query.IsHD); + statement.TryBind("@MinHeight", query.MinHeight); } } + if (maxWidth.HasValue) + { + whereClauses.Add("Width<=@MaxWidth"); + if (statement != null) + { + statement.TryBind("@MaxWidth", maxWidth); + } + } + if (query.MaxHeight.HasValue) + { + whereClauses.Add("Height<=@MaxHeight"); + if (statement != null) + { + statement.TryBind("@MaxHeight", query.MaxHeight); + } + } + if (query.IsLocked.HasValue) { whereClauses.Add("IsLocked=@IsLocked"); @@ -3544,140 +3622,127 @@ namespace Emby.Server.Implementations.Data } } - var exclusiveProgramAttribtues = !(query.IsMovie ?? true) || - !(query.IsSports ?? true) || - !(query.IsKids ?? true) || - !(query.IsNews ?? true) || - !(query.IsSeries ?? true); + var tags = query.Tags.ToList(); + var excludeTags = query.ExcludeTags.ToList(); + + //if (!(query.IsMovie ?? true) || !(query.IsSeries ?? true)) + //{ + // if (query.IsMovie.HasValue) + // { + // var alternateTypes = new List<string>(); + // if (query.IncludeItemTypes.Length == 0 || query.IncludeItemTypes.Contains(typeof(Movie).Name)) + // { + // alternateTypes.Add(typeof(Movie).FullName); + // } + // if (query.IncludeItemTypes.Length == 0 || query.IncludeItemTypes.Contains(typeof(Trailer).Name)) + // { + // alternateTypes.Add(typeof(Trailer).FullName); + // } + + // if (alternateTypes.Count == 0) + // { + // whereClauses.Add("IsMovie=@IsMovie"); + // if (statement != null) + // { + // statement.TryBind("@IsMovie", query.IsMovie); + // } + // } + // else + // { + // whereClauses.Add("(IsMovie is null OR IsMovie=@IsMovie)"); + // if (statement != null) + // { + // statement.TryBind("@IsMovie", query.IsMovie); + // } + // } + // } + //} + //else + //{ - if (exclusiveProgramAttribtues) + //} + + if (query.IsMovie ?? false) { - if (query.IsMovie.HasValue) - { - var alternateTypes = new List<string>(); - if (query.IncludeItemTypes.Length == 0 || query.IncludeItemTypes.Contains(typeof(Movie).Name)) - { - alternateTypes.Add(typeof(Movie).FullName); - } - if (query.IncludeItemTypes.Length == 0 || query.IncludeItemTypes.Contains(typeof(Trailer).Name)) - { - alternateTypes.Add(typeof(Trailer).FullName); - } + var programAttribtues = new List<string>(); - if (alternateTypes.Count == 0) - { - whereClauses.Add("IsMovie=@IsMovie"); - if (statement != null) - { - statement.TryBind("@IsMovie", query.IsMovie); - } - } - else - { - whereClauses.Add("(IsMovie is null OR IsMovie=@IsMovie)"); - if (statement != null) - { - statement.TryBind("@IsMovie", query.IsMovie); - } - } + var alternateTypes = new List<string>(); + if (query.IncludeItemTypes.Length == 0 || query.IncludeItemTypes.Contains(typeof(Movie).Name)) + { + alternateTypes.Add(typeof(Movie).FullName); } - if (query.IsSeries.HasValue) + if (query.IncludeItemTypes.Length == 0 || query.IncludeItemTypes.Contains(typeof(Trailer).Name)) { - whereClauses.Add("IsSeries=@IsSeries"); - if (statement != null) - { - statement.TryBind("@IsSeries", query.IsSeries); - } + alternateTypes.Add(typeof(Trailer).FullName); } - if (query.IsNews.HasValue) + + if (alternateTypes.Count == 0) { - whereClauses.Add("IsNews=@IsNews"); - if (statement != null) - { - statement.TryBind("@IsNews", query.IsNews); - } + programAttribtues.Add("IsMovie=@IsMovie"); } - if (query.IsKids.HasValue) + else { - whereClauses.Add("IsKids=@IsKids"); - if (statement != null) - { - statement.TryBind("@IsKids", query.IsKids); - } + programAttribtues.Add("(IsMovie is null OR IsMovie=@IsMovie)"); } - if (query.IsSports.HasValue) + + if (statement != null) { - whereClauses.Add("IsSports=@IsSports"); - if (statement != null) - { - statement.TryBind("@IsSports", query.IsSports); - } + statement.TryBind("@IsMovie", true); } + + whereClauses.Add("(" + string.Join(" OR ", programAttribtues.ToArray(programAttribtues.Count)) + ")"); } - else + else if (query.IsMovie.HasValue) { - var programAttribtues = new List<string>(); - if (query.IsMovie ?? false) + whereClauses.Add("IsMovie=@IsMovie"); + if (statement != null) { - var alternateTypes = new List<string>(); - if (query.IncludeItemTypes.Length == 0 || query.IncludeItemTypes.Contains(typeof(Movie).Name)) - { - alternateTypes.Add(typeof(Movie).FullName); - } - if (query.IncludeItemTypes.Length == 0 || query.IncludeItemTypes.Contains(typeof(Trailer).Name)) - { - alternateTypes.Add(typeof(Trailer).FullName); - } + statement.TryBind("@IsMovie", query.IsMovie); + } + } - if (alternateTypes.Count == 0) - { - programAttribtues.Add("IsMovie=@IsMovie"); - } - else - { - programAttribtues.Add("(IsMovie is null OR IsMovie=@IsMovie)"); - } + if (query.IsSeries.HasValue) + { + whereClauses.Add("IsSeries=@IsSeries"); + if (statement != null) + { + statement.TryBind("@IsSeries", query.IsSeries); + } + } - if (statement != null) - { - statement.TryBind("@IsMovie", true); - } + if (query.IsSports.HasValue) + { + if (query.IsSports.Value) + { + tags.Add("Sports"); } - if (query.IsSports ?? false) + else { - programAttribtues.Add("IsSports=@IsSports"); - if (statement != null) - { - statement.TryBind("@IsSports", query.IsSports); - } + excludeTags.Add("Sports"); } - if (query.IsNews ?? false) + } + + if (query.IsNews.HasValue) + { + if (query.IsNews.Value) { - programAttribtues.Add("IsNews=@IsNews"); - if (statement != null) - { - statement.TryBind("@IsNews", query.IsNews); - } + tags.Add("News"); } - if (query.IsSeries ?? false) + else { - programAttribtues.Add("IsSeries=@IsSeries"); - if (statement != null) - { - statement.TryBind("@IsSeries", query.IsSeries); - } + excludeTags.Add("News"); } - if (query.IsKids ?? false) + } + + if (query.IsKids.HasValue) + { + if (query.IsKids.Value) { - programAttribtues.Add("IsKids=@IsKids"); - if (statement != null) - { - statement.TryBind("@IsKids", query.IsKids); - } + tags.Add("Kids"); } - if (programAttribtues.Count > 0) + else { - whereClauses.Add("(" + string.Join(" OR ", programAttribtues.ToArray(programAttribtues.Count)) + ")"); + excludeTags.Add("Kids"); } } @@ -3686,6 +3751,11 @@ namespace Emby.Server.Implementations.Data whereClauses.Add("SimilarityScore > " + (query.MinSimilarityScore - 1).ToString(CultureInfo.InvariantCulture)); } + if (!string.IsNullOrEmpty(query.SearchTerm)) + { + whereClauses.Add("SearchScore > 0"); + } + if (query.IsFolder.HasValue) { whereClauses.Add("IsFolder=@IsFolder"); @@ -3710,50 +3780,55 @@ namespace Emby.Server.Implementations.Data whereClauses.Add(string.Format("type in ({0})", inClause)); } - var excludeTypes = query.ExcludeItemTypes.SelectMany(MapIncludeItemTypes).ToArray(); - if (excludeTypes.Length == 1) + // Only specify excluded types if no included types are specified + if (includeTypes.Length == 0) { - whereClauses.Add("type<>@type"); - if (statement != null) + var excludeTypes = query.ExcludeItemTypes.SelectMany(MapIncludeItemTypes).ToArray(); + if (excludeTypes.Length == 1) + { + whereClauses.Add("type<>@type"); + if (statement != null) + { + statement.TryBind("@type", excludeTypes[0]); + } + } + else if (excludeTypes.Length > 1) { - statement.TryBind("@type", excludeTypes[0]); + var inClause = string.Join(",", excludeTypes.Select(i => "'" + i + "'").ToArray()); + whereClauses.Add(string.Format("type not in ({0})", inClause)); } } - else if (excludeTypes.Length > 1) - { - var inClause = string.Join(",", excludeTypes.Select(i => "'" + i + "'").ToArray()); - whereClauses.Add(string.Format("type not in ({0})", inClause)); - } if (query.ChannelIds.Length == 1) { whereClauses.Add("ChannelId=@ChannelId"); if (statement != null) { - statement.TryBind("@ChannelId", query.ChannelIds[0]); + statement.TryBind("@ChannelId", query.ChannelIds[0].ToString("N")); } } else if (query.ChannelIds.Length > 1) { - var inClause = string.Join(",", query.ChannelIds.Where(IsValidId).Select(i => "'" + i + "'").ToArray()); + var inClause = string.Join(",", query.ChannelIds.Select(i => "'" + i.ToString("N") + "'").ToArray()); whereClauses.Add(string.Format("ChannelId in ({0})", inClause)); } - if (query.ParentId.HasValue) + if (!query.ParentId.Equals(Guid.Empty)) { whereClauses.Add("ParentId=@ParentId"); if (statement != null) { - statement.TryBind("@ParentId", query.ParentId.Value); + statement.TryBind("@ParentId", query.ParentId); } } if (!string.IsNullOrWhiteSpace(query.Path)) { + //whereClauses.Add("(Path=@Path COLLATE NOCASE)"); whereClauses.Add("Path=@Path"); if (statement != null) { - statement.TryBind("@Path", query.Path); + statement.TryBind("@Path", GetPathToSave(query.Path)); } } @@ -3847,21 +3922,37 @@ namespace Emby.Server.Implementations.Data statement.TryBind("@ParentIndexNumberNotEquals", query.ParentIndexNumberNotEquals.Value); } } - if (query.MinEndDate.HasValue) + + var minEndDate = query.MinEndDate; + var maxEndDate = query.MaxEndDate; + + if (query.HasAired.HasValue) + { + if (query.HasAired.Value) + { + maxEndDate = DateTime.UtcNow; + } + else + { + minEndDate = DateTime.UtcNow; + } + } + + if (minEndDate.HasValue) { whereClauses.Add("EndDate>=@MinEndDate"); if (statement != null) { - statement.TryBind("@MinEndDate", query.MinEndDate.Value); + statement.TryBind("@MinEndDate", minEndDate.Value); } } - if (query.MaxEndDate.HasValue) + if (maxEndDate.HasValue) { whereClauses.Add("EndDate<=@MaxEndDate"); if (statement != null) { - statement.TryBind("@MaxEndDate", query.MaxEndDate.Value); + statement.TryBind("@MaxEndDate", maxEndDate.Value); } } @@ -3955,7 +4046,8 @@ namespace Emby.Server.Implementations.Data { var paramName = "@PersonId" + index; - clauses.Add("(select Name from TypedBaseItems where guid=" + paramName + ") in (select Name from People where ItemId=Guid)"); + clauses.Add("(guid in (select itemid from People where Name = (select Name from TypedBaseItems where guid=" + paramName + ")))"); + if (statement != null) { statement.TryBind(paramName, personId.ToGuidBlob()); @@ -4012,14 +4104,19 @@ namespace Emby.Server.Implementations.Data } } - if (!string.IsNullOrWhiteSpace(query.NameContains)) + // These are the same, for now + var nameContains = query.NameContains; + if (!string.IsNullOrWhiteSpace(nameContains)) { - whereClauses.Add("CleanName like @NameContains"); + whereClauses.Add("(CleanName like @NameContains or OriginalTitle like @NameContains)"); if (statement != null) { - statement.TryBind("@NameContains", "%" + GetCleanValue(query.NameContains) + "%"); + nameContains = FixUnicodeChars(nameContains); + + statement.TryBind("@NameContains", "%" + GetCleanValue(nameContains) + "%"); } } + if (!string.IsNullOrWhiteSpace(query.NameStartsWith)) { whereClauses.Add("SortName like @NameStartsWith"); @@ -4111,17 +4208,32 @@ namespace Emby.Server.Implementations.Data { if (query.IsPlayed.HasValue) { - if (query.IsPlayed.Value) + // We should probably figure this out for all folders, but for right now, this is the only place where we need it + if (query.IncludeItemTypes.Length == 1 && string.Equals(query.IncludeItemTypes[0], typeof(Series).Name, StringComparison.OrdinalIgnoreCase)) { - whereClauses.Add("(played=@IsPlayed)"); + if (query.IsPlayed.Value) + { + whereClauses.Add("PresentationUniqueKey not in (select S.SeriesPresentationUniqueKey from TypedBaseitems S left join UserDatas UD on S.UserDataKey=UD.Key And UD.UserId=@UserId where Coalesce(UD.Played, 0)=0 and S.IsFolder=0 and S.IsVirtualItem=0 and S.SeriesPresentationUniqueKey not null)"); + } + else + { + whereClauses.Add("PresentationUniqueKey in (select S.SeriesPresentationUniqueKey from TypedBaseitems S left join UserDatas UD on S.UserDataKey=UD.Key And UD.UserId=@UserId where Coalesce(UD.Played, 0)=0 and S.IsFolder=0 and S.IsVirtualItem=0 and S.SeriesPresentationUniqueKey not null)"); + } } else { - whereClauses.Add("(played is null or played=@IsPlayed)"); - } - if (statement != null) - { - statement.TryBind("@IsPlayed", query.IsPlayed.Value); + if (query.IsPlayed.Value) + { + whereClauses.Add("(played=@IsPlayed)"); + } + else + { + whereClauses.Add("(played is null or played=@IsPlayed)"); + } + if (statement != null) + { + statement.TryBind("@IsPlayed", query.IsPlayed.Value); + } } } } @@ -4146,7 +4258,45 @@ namespace Emby.Server.Implementations.Data { var paramName = "@ArtistIds" + index; - clauses.Add("(select CleanName from TypedBaseItems where guid=" + paramName + ") in (select CleanValue from itemvalues where ItemId=Guid and Type<=1)"); + clauses.Add("(guid in (select itemid from itemvalues where CleanValue = (select CleanName from TypedBaseItems where guid=" + paramName + ") and Type<=1))"); + if (statement != null) + { + statement.TryBind(paramName, artistId.ToGuidBlob()); + } + index++; + } + var clause = "(" + string.Join(" OR ", clauses.ToArray()) + ")"; + whereClauses.Add(clause); + } + + if (query.AlbumArtistIds.Length > 0) + { + var clauses = new List<string>(); + var index = 0; + foreach (var artistId in query.AlbumArtistIds) + { + var paramName = "@ArtistIds" + index; + + clauses.Add("(guid in (select itemid from itemvalues where CleanValue = (select CleanName from TypedBaseItems where guid=" + paramName + ") and Type=1))"); + if (statement != null) + { + statement.TryBind(paramName, artistId.ToGuidBlob()); + } + index++; + } + var clause = "(" + string.Join(" OR ", clauses.ToArray()) + ")"; + whereClauses.Add(clause); + } + + if (query.ContributingArtistIds.Length > 0) + { + var clauses = new List<string>(); + var index = 0; + foreach (var artistId in query.ContributingArtistIds) + { + var paramName = "@ArtistIds" + index; + + clauses.Add("((select CleanName from TypedBaseItems where guid=" + paramName + ") in (select CleanValue from itemvalues where ItemId=Guid and Type=0) AND (select CleanName from TypedBaseItems where guid=" + paramName + ") not in (select CleanValue from itemvalues where ItemId=Guid and Type=1))"); if (statement != null) { statement.TryBind(paramName, artistId.ToGuidBlob()); @@ -4184,7 +4334,7 @@ namespace Emby.Server.Implementations.Data { var paramName = "@ExcludeArtistId" + index; - clauses.Add("(select CleanName from TypedBaseItems where guid=" + paramName + ") not in (select CleanValue from itemvalues where ItemId=Guid and Type<=1)"); + clauses.Add("(guid not in (select itemid from itemvalues where CleanValue = (select CleanName from TypedBaseItems where guid=" + paramName + ") and Type<=1))"); if (statement != null) { statement.TryBind(paramName, artistId.ToGuidBlob()); @@ -4203,7 +4353,7 @@ namespace Emby.Server.Implementations.Data { var paramName = "@GenreId" + index; - clauses.Add("(select CleanName from TypedBaseItems where guid=" + paramName + ") in (select CleanValue from itemvalues where ItemId=Guid and Type=2)"); + clauses.Add("(guid in (select itemid from itemvalues where CleanValue = (select CleanName from TypedBaseItems where guid=" + paramName + ") and Type=2))"); if (statement != null) { statement.TryBind(paramName, genreId.ToGuidBlob()); @@ -4231,11 +4381,11 @@ namespace Emby.Server.Implementations.Data whereClauses.Add(clause); } - if (query.Tags.Length > 0) + if (tags.Count > 0) { var clauses = new List<string>(); var index = 0; - foreach (var item in query.Tags) + foreach (var item in tags) { clauses.Add("@Tag" + index + " in (select CleanValue from itemvalues where ItemId=Guid and Type=4)"); if (statement != null) @@ -4248,6 +4398,23 @@ namespace Emby.Server.Implementations.Data whereClauses.Add(clause); } + if (excludeTags.Count > 0) + { + var clauses = new List<string>(); + var index = 0; + foreach (var item in excludeTags) + { + clauses.Add("@ExcludeTag" + index + " not in (select CleanValue from itemvalues where ItemId=Guid and Type=4)"); + if (statement != null) + { + statement.TryBind("@ExcludeTag" + index, GetCleanValue(item)); + } + index++; + } + var clause = "(" + string.Join(" OR ", clauses.ToArray()) + ")"; + whereClauses.Add(clause); + } + if (query.StudioIds.Length > 0) { var clauses = new List<string>(); @@ -4256,7 +4423,8 @@ namespace Emby.Server.Implementations.Data { var paramName = "@StudioId" + index; - clauses.Add("(select CleanName from TypedBaseItems where guid=" + paramName + ") in (select CleanValue from itemvalues where ItemId=Guid and Type=3)"); + clauses.Add("(guid in (select itemid from itemvalues where CleanValue = (select CleanName from TypedBaseItems where guid=" + paramName + ") and Type=3))"); + if (statement != null) { statement.TryBind(paramName, studioId.ToGuidBlob()); @@ -4314,6 +4482,18 @@ namespace Emby.Server.Implementations.Data } } + if (query.HasOfficialRating.HasValue) + { + if (query.HasOfficialRating.Value) + { + whereClauses.Add("(OfficialRating not null AND OfficialRating<>'')"); + } + else + { + whereClauses.Add("(OfficialRating is null OR OfficialRating='')"); + } + } + if (query.HasOverview.HasValue) { if (query.HasOverview.Value) @@ -4326,6 +4506,18 @@ namespace Emby.Server.Implementations.Data } } + if (query.HasOwnerId.HasValue) + { + if (query.HasOwnerId.Value) + { + whereClauses.Add("OwnerId not null"); + } + else + { + whereClauses.Add("OwnerId is null"); + } + } + if (!string.IsNullOrWhiteSpace(query.HasNoAudioTrackWithLanguage)) { whereClauses.Add("((select language from MediaStreams where MediaStreams.ItemId=A.Guid and MediaStreams.StreamType='Audio' and MediaStreams.Language=@HasNoAudioTrackWithLanguage limit 1) is null)"); @@ -4362,6 +4554,18 @@ namespace Emby.Server.Implementations.Data } } + if (query.HasSubtitles.HasValue) + { + if (query.HasSubtitles.Value) + { + whereClauses.Add("((select type from MediaStreams where MediaStreams.ItemId=A.Guid and MediaStreams.StreamType='Subtitle' limit 1) not null)"); + } + else + { + whereClauses.Add("((select type from MediaStreams where MediaStreams.ItemId=A.Guid and MediaStreams.StreamType='Subtitle' limit 1) is null)"); + } + } + if (query.HasChapterImages.HasValue) { if (query.HasChapterImages.Value) @@ -4379,6 +4583,21 @@ namespace Emby.Server.Implementations.Data whereClauses.Add("ParentId NOT NULL AND ParentId NOT IN (select guid from TypedBaseItems)"); } + if (query.IsDeadArtist.HasValue && query.IsDeadArtist.Value) + { + whereClauses.Add("CleanName not in (Select CleanValue From ItemValues where Type in (0,1))"); + } + + if (query.IsDeadStudio.HasValue && query.IsDeadStudio.Value) + { + whereClauses.Add("CleanName not in (Select CleanValue From ItemValues where Type = 3)"); + } + + if (query.IsDeadPerson.HasValue && query.IsDeadPerson.Value) + { + whereClauses.Add("Name not in (Select Name From People)"); + } + if (query.Years.Length == 1) { whereClauses.Add("ProductionYear=@Years"); @@ -4450,7 +4669,7 @@ namespace Emby.Server.Implementations.Data includeIds.Add("Guid = @IncludeId" + index); if (statement != null) { - statement.TryBind("@IncludeId" + index, new Guid(id)); + statement.TryBind("@IncludeId" + index, id); } index++; } @@ -4467,7 +4686,7 @@ namespace Emby.Server.Implementations.Data excludeIds.Add("Guid <> @ExcludeId" + index); if (statement != null) { - statement.TryBind("@ExcludeId" + index, new Guid(id)); + statement.TryBind("@ExcludeId" + index, id); } index++; } @@ -4499,7 +4718,40 @@ namespace Emby.Server.Implementations.Data break; } - whereClauses.Add(string.Join(" AND ", excludeIds.ToArray())); + if (excludeIds.Count > 0) + { + whereClauses.Add(string.Join(" AND ", excludeIds.ToArray())); + } + } + + if (query.HasAnyProviderId.Count > 0) + { + var hasProviderIds = new List<string>(); + + var index = 0; + foreach (var pair in query.HasAnyProviderId) + { + if (string.Equals(pair.Key, MetadataProviders.TmdbCollection.ToString(), StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + var paramName = "@HasAnyProviderId" + index; + //hasProviderIds.Add("(COALESCE((select value from ProviderIds where ItemId=Guid and Name = '" + pair.Key + "'), '') <> " + paramName + ")"); + hasProviderIds.Add("ProviderIds like " + paramName + ""); + if (statement != null) + { + statement.TryBind(paramName, "%" + pair.Key + "=" + pair.Value + "%"); + } + index++; + + break; + } + + if (hasProviderIds.Count > 0) + { + whereClauses.Add("(" + string.Join(" OR ", hasProviderIds.ToArray()) + ")"); + } } if (query.HasImdbId.HasValue) @@ -4516,33 +4768,11 @@ namespace Emby.Server.Implementations.Data { whereClauses.Add("ProviderIds like '%tvdb=%'"); } - if (query.HasThemeSong.HasValue) - { - if (query.HasThemeSong.Value) - { - whereClauses.Add("ThemeSongIds not null"); - } - else - { - whereClauses.Add("ThemeSongIds is null"); - } - } - if (query.HasThemeVideo.HasValue) - { - if (query.HasThemeVideo.Value) - { - whereClauses.Add("ThemeVideoIds not null"); - } - else - { - whereClauses.Add("ThemeVideoIds is null"); - } - } var includedItemByNameTypes = GetItemByNameTypesInQuery(query).SelectMany(MapIncludeItemTypes).ToList(); var enableItemsByName = (query.IncludeItemsByName ?? false) && includedItemByNameTypes.Count > 0; - var queryTopParentIds = query.TopParentIds.Where(IsValidId).ToArray(); + var queryTopParentIds = query.TopParentIds; if (queryTopParentIds.Length == 1) { @@ -4565,12 +4795,12 @@ namespace Emby.Server.Implementations.Data } if (statement != null) { - statement.TryBind("@TopParentId", queryTopParentIds[0]); + statement.TryBind("@TopParentId", queryTopParentIds[0].ToString("N")); } } else if (queryTopParentIds.Length > 1) { - var val = string.Join(",", queryTopParentIds.Select(i => "'" + i + "'").ToArray()); + var val = string.Join(",", queryTopParentIds.Select(i => "'" + i.ToString("N") + "'").ToArray()); if (enableItemsByName && includedItemByNameTypes.Count == 1) { @@ -4597,12 +4827,12 @@ namespace Emby.Server.Implementations.Data if (statement != null) { - statement.TryBind("@AncestorId", new Guid(query.AncestorIds[0])); + statement.TryBind("@AncestorId", query.AncestorIds[0]); } } if (query.AncestorIds.Length > 1) { - var inClause = string.Join(",", query.AncestorIds.Select(i => "'" + new Guid(i).ToString("N") + "'").ToArray()); + var inClause = string.Join(",", query.AncestorIds.Select(i => "'" + i.ToString("N") + "'").ToArray()); whereClauses.Add(string.Format("Guid in (select itemId from AncestorIds where AncestorIdText in ({0}))", inClause)); } if (!string.IsNullOrWhiteSpace(query.AncestorWithPresentationUniqueKey)) @@ -4647,6 +4877,114 @@ namespace Emby.Server.Implementations.Data whereClauses.Add("((select CleanValue from itemvalues where ItemId=Guid and Type=6 and cleanvalue in (" + tagValuesList + ")) is null)"); } + if (query.SeriesStatuses.Length > 0) + { + var statuses = new List<string>(); + + foreach (var seriesStatus in query.SeriesStatuses) + { + statuses.Add("data like '%" + seriesStatus + "%'"); + } + + whereClauses.Add("(" + string.Join(" OR ", statuses.ToArray()) + ")"); + } + + if (query.BoxSetLibraryFolders.Length > 0) + { + var folderIdQueries = new List<string>(); + + foreach (var folderId in query.BoxSetLibraryFolders) + { + folderIdQueries.Add("data like '%" + folderId.ToString("N") + "%'"); + } + + whereClauses.Add("(" + string.Join(" OR ", folderIdQueries.ToArray()) + ")"); + } + + if (query.VideoTypes.Length > 0) + { + var videoTypes = new List<string>(); + + foreach (var videoType in query.VideoTypes) + { + videoTypes.Add("data like '%\"VideoType\":\"" + videoType.ToString() + "\"%'"); + } + + whereClauses.Add("(" + string.Join(" OR ", videoTypes.ToArray()) + ")"); + } + + if (query.Is3D.HasValue) + { + if (query.Is3D.Value) + { + whereClauses.Add("data like '%Video3DFormat%'"); + } + else + { + whereClauses.Add("data not like '%Video3DFormat%'"); + } + } + + if (query.IsPlaceHolder.HasValue) + { + if (query.IsPlaceHolder.Value) + { + whereClauses.Add("data like '%\"IsPlaceHolder\":true%'"); + } + else + { + whereClauses.Add("(data is null or data not like '%\"IsPlaceHolder\":true%')"); + } + } + + if (query.HasSpecialFeature.HasValue) + { + if (query.HasSpecialFeature.Value) + { + whereClauses.Add("ExtraIds not null"); + } + else + { + whereClauses.Add("ExtraIds is null"); + } + } + + if (query.HasTrailer.HasValue) + { + if (query.HasTrailer.Value) + { + whereClauses.Add("ExtraIds not null"); + } + else + { + whereClauses.Add("ExtraIds is null"); + } + } + + if (query.HasThemeSong.HasValue) + { + if (query.HasThemeSong.Value) + { + whereClauses.Add("ExtraIds not null"); + } + else + { + whereClauses.Add("ExtraIds is null"); + } + } + + if (query.HasThemeVideo.HasValue) + { + if (query.HasThemeVideo.Value) + { + whereClauses.Add("ExtraIds not null"); + } + else + { + whereClauses.Add("ExtraIds is null"); + } + } + return whereClauses; } @@ -4749,8 +5087,6 @@ namespace Emby.Server.Implementations.Data { typeof(LiveTvProgram), typeof(LiveTvChannel), - typeof(LiveTvVideoRecording), - typeof(LiveTvAudioRecording), typeof(Series), typeof(Audio), typeof(MusicAlbum), @@ -4759,7 +5095,6 @@ namespace Emby.Server.Implementations.Data typeof(MusicVideo), typeof(Movie), typeof(Playlist), - typeof(AudioPodcast), typeof(AudioBook), typeof(Trailer), typeof(BoxSet), @@ -4825,7 +5160,6 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type dict[t.Name] = new[] { t.FullName }; } - dict["Recording"] = new[] { typeof(LiveTvAudioRecording).FullName, typeof(LiveTvVideoRecording).FullName }; dict["Program"] = new[] { typeof(LiveTvProgram).FullName }; dict["TvChannel"] = new[] { typeof(LiveTvChannel).FullName }; @@ -4848,7 +5182,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type public void DeleteItem(Guid id, CancellationToken cancellationToken) { - if (id == Guid.Empty) + if (id.Equals(Guid.Empty)) { throw new ArgumentNullException("id"); } @@ -4861,23 +5195,25 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type { connection.RunInTransaction(db => { + var idBlob = id.ToGuidBlob(); + // Delete people - ExecuteWithSingleParam(db, "delete from People where ItemId=@Id", id.ToGuidBlob()); + ExecuteWithSingleParam(db, "delete from People where ItemId=@Id", idBlob); // Delete chapters - ExecuteWithSingleParam(db, "delete from " + ChaptersTableName + " where ItemId=@Id", id.ToGuidBlob()); + ExecuteWithSingleParam(db, "delete from " + ChaptersTableName + " where ItemId=@Id", idBlob); // Delete media streams - ExecuteWithSingleParam(db, "delete from mediastreams where ItemId=@Id", id.ToGuidBlob()); + ExecuteWithSingleParam(db, "delete from mediastreams where ItemId=@Id", idBlob); // Delete ancestors - ExecuteWithSingleParam(db, "delete from AncestorIds where ItemId=@Id", id.ToGuidBlob()); + ExecuteWithSingleParam(db, "delete from AncestorIds where ItemId=@Id", idBlob); // Delete item values - ExecuteWithSingleParam(db, "delete from ItemValues where ItemId=@Id", id.ToGuidBlob()); + ExecuteWithSingleParam(db, "delete from ItemValues where ItemId=@Id", idBlob); // Delete the item - ExecuteWithSingleParam(db, "delete from TypedBaseItems where guid=@Id", id.ToGuidBlob()); + ExecuteWithSingleParam(db, "delete from TypedBaseItems where guid=@Id", idBlob); }, TransactionMode); } } @@ -4979,7 +5315,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type { var whereClauses = new List<string>(); - if (query.ItemId != Guid.Empty) + if (!query.ItemId.Equals(Guid.Empty)) { whereClauses.Add("ItemId=@ItemId"); if (statement != null) @@ -4987,7 +5323,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type statement.TryBind("@ItemId", query.ItemId.ToGuidBlob()); } } - if (query.AppearsInItemId != Guid.Empty) + if (!query.AppearsInItemId.Equals(Guid.Empty)) { whereClauses.Add("Name in (Select Name from People where ItemId=@AppearsInItemId)"); if (statement != null) @@ -5047,9 +5383,9 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type return whereClauses; } - private void UpdateAncestors(Guid itemId, List<Guid> ancestorIds, IDatabaseConnection db, IStatement deleteAncestorsStatement, IStatement updateAncestorsStatement) + private void UpdateAncestors(Guid itemId, List<Guid> ancestorIds, IDatabaseConnection db, IStatement deleteAncestorsStatement) { - if (itemId == Guid.Empty) + if (itemId.Equals(Guid.Empty)) { throw new ArgumentNullException("itemId"); } @@ -5061,18 +5397,46 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type CheckDisposed(); + var itemIdBlob = itemId.ToGuidBlob(); + // First delete deleteAncestorsStatement.Reset(); - deleteAncestorsStatement.TryBind("@ItemId", itemId.ToGuidBlob()); + deleteAncestorsStatement.TryBind("@ItemId", itemIdBlob); deleteAncestorsStatement.MoveNext(); - foreach (var ancestorId in ancestorIds) + if (ancestorIds.Count == 0) { - updateAncestorsStatement.Reset(); - updateAncestorsStatement.TryBind("@ItemId", itemId.ToGuidBlob()); - updateAncestorsStatement.TryBind("@AncestorId", ancestorId.ToGuidBlob()); - updateAncestorsStatement.TryBind("@AncestorIdText", ancestorId.ToString("N")); - updateAncestorsStatement.MoveNext(); + return; + } + + var insertText = new StringBuilder("insert into AncestorIds (ItemId, AncestorId, AncestorIdText) values "); + + for (var i = 0; i < ancestorIds.Count; i++) + { + if (i > 0) + { + insertText.Append(","); + } + + insertText.AppendFormat("(@ItemId, @AncestorId{0}, @AncestorIdText{0})", i.ToString(CultureInfo.InvariantCulture)); + } + + using (var statement = PrepareStatementSafe(db, insertText.ToString())) + { + statement.TryBind("@ItemId", itemIdBlob); + + for (var i = 0; i < ancestorIds.Count; i++) + { + var index = i.ToString(CultureInfo.InvariantCulture); + + var ancestorId = ancestorIds[i]; + + statement.TryBind("@AncestorId" + index, ancestorId.ToGuidBlob()); + statement.TryBind("@AncestorIdText" + index, ancestorId.ToString("N")); + } + + statement.Reset(); + statement.MoveNext(); } } @@ -5249,18 +5613,13 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type var columns = _retriveItemColumns.ToList(); columns.AddRange(itemCountColumns.Select(i => i.Item2).ToArray()); - columns = GetFinalColumnsToSelect(query, columns.ToArray()).ToList(); - - var commandText = "select " + string.Join(",", columns.ToArray()) + GetFromText(); - commandText += GetJoinUserDataText(query); - + // do this first before calling GetFinalColumnsToSelect, otherwise ExcludeItemIds will be set by SimilarTo var innerQuery = new InternalItemsQuery(query.User) { ExcludeItemTypes = query.ExcludeItemTypes, IncludeItemTypes = query.IncludeItemTypes, MediaTypes = query.MediaTypes, AncestorIds = query.AncestorIds, - ExcludeItemIds = query.ExcludeItemIds, ItemIds = query.ItemIds, TopParentIds = query.TopParentIds, ParentId = query.ParentId, @@ -5273,6 +5632,11 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type IsSeries = query.IsSeries }; + columns = GetFinalColumnsToSelect(query, columns.ToArray()).ToList(); + + var commandText = "select " + string.Join(",", columns.ToArray()) + GetFromText(); + commandText += GetJoinUserDataText(query); + var innerWhereClauses = GetWhereClauses(innerQuery, null); var innerWhereText = innerWhereClauses.Count == 0 ? @@ -5305,7 +5669,10 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type GenreIds = query.GenreIds, Genres = query.Genres, Years = query.Years, - NameContains = query.NameContains + NameContains = query.NameContains, + SearchTerm = query.SearchTerm, + SimilarTo = query.SimilarTo, + ExcludeItemIds = query.ExcludeItemIds }; var outerWhereClauses = GetWhereClauses(outerQuery, null); @@ -5318,7 +5685,14 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type commandText += whereText; commandText += " group by PresentationUniqueKey"; - commandText += " order by SortName"; + if (query.SimilarTo != null || !string.IsNullOrEmpty(query.SearchTerm)) + { + commandText += GetOrderByText(query); + } + else + { + commandText += " order by SortName"; + } if (query.Limit.HasValue || query.StartIndex.HasValue) { @@ -5344,7 +5718,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type } if (query.EnableTotalRecordCount) { - var countText = "select count (distinct PresentationUniqueKey)" + GetFromText(); + var countText = "select " + string.Join(",", GetFinalColumnsToSelect(query, new[] { "count (distinct PresentationUniqueKey)" })) + GetFromText(); countText += GetJoinUserDataText(query); countText += whereText; @@ -5360,6 +5734,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type var list = new List<Tuple<BaseItem, ItemCounts>>(); var result = new QueryResult<Tuple<BaseItem, ItemCounts>>(); + //Logger.Info("GetItemValues {0}", string.Join(";", statementTexts.ToArray())); var statements = PrepareAllSafe(db, statementTexts); if (!isReturningZeroItems) @@ -5369,7 +5744,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type statement.TryBind("@SelectType", returnType); if (EnableJoinUserData(query)) { - statement.TryBind("@UserId", query.User.Id); + statement.TryBind("@UserId", query.User.InternalId); } if (typeSubQuery != null) @@ -5377,11 +5752,13 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type GetWhereClauses(typeSubQuery, null); } BindSimilarParams(query, statement); + BindSearchParams(query, statement); GetWhereClauses(innerQuery, statement); GetWhereClauses(outerQuery, statement); var hasEpisodeAttributes = HasEpisodeAttributes(query); var hasProgramAttributes = HasProgramAttributes(query); + var hasServiceName = HasServiceName(query); var hasStartDate = HasStartDate(query); var hasTrailerTypes = HasTrailerTypes(query); var hasArtistFields = HasArtistFields(query); @@ -5389,7 +5766,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type foreach (var row in statement.ExecuteQuery()) { - var item = GetItem(row, query, hasProgramAttributes, hasEpisodeAttributes, hasStartDate, hasTrailerTypes, hasArtistFields, hasSeriesFields); + var item = GetItem(row, query, hasProgramAttributes, hasEpisodeAttributes, hasServiceName, hasStartDate, hasTrailerTypes, hasArtistFields, hasSeriesFields); if (item != null) { var countStartColumn = columns.Count - 1; @@ -5404,7 +5781,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type if (query.EnableTotalRecordCount) { - commandText = "select count (distinct PresentationUniqueKey)" + GetFromText(); + commandText = "select " + string.Join(",", GetFinalColumnsToSelect(query, new[] { "count (distinct PresentationUniqueKey)" })) + GetFromText(); commandText += GetJoinUserDataText(query); commandText += whereText; @@ -5414,7 +5791,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type statement.TryBind("@SelectType", returnType); if (EnableJoinUserData(query)) { - statement.TryBind("@UserId", query.User.Id); + statement.TryBind("@UserId", query.User.InternalId); } if (typeSubQuery != null) @@ -5422,6 +5799,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type GetWhereClauses(typeSubQuery, null); } BindSimilarParams(query, statement); + BindSearchParams(query, statement); GetWhereClauses(innerQuery, statement); GetWhereClauses(outerQuery, statement); @@ -5535,7 +5913,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type private void UpdateItemValues(Guid itemId, List<Tuple<int, string>> values, IDatabaseConnection db) { - if (itemId == Guid.Empty) + if (itemId.Equals(Guid.Empty)) { throw new ArgumentNullException("itemId"); } @@ -5552,41 +5930,66 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type // First delete db.Execute("delete from ItemValues where ItemId=@Id", guidBlob); - using (var statement = PrepareStatement(db, "insert into ItemValues (ItemId, Type, Value, CleanValue) values (@ItemId, @Type, @Value, @CleanValue)")) + InsertItemValues(guidBlob, values, db); + } + + private void InsertItemValues(byte[] idBlob, List<Tuple<int, string>> values, IDatabaseConnection db) + { + var startIndex = 0; + var limit = 100; + + while (startIndex < values.Count) { - foreach (var pair in values) - { - var itemValue = pair.Item2; + var insertText = new StringBuilder("insert into ItemValues (ItemId, Type, Value, CleanValue) values "); - // Don't save if invalid - if (string.IsNullOrWhiteSpace(itemValue)) + var endIndex = Math.Min(values.Count, startIndex + limit); + var isSubsequentRow = false; + + for (var i = startIndex; i < endIndex; i++) + { + if (isSubsequentRow) { - continue; + insertText.Append(","); } - statement.Reset(); + insertText.AppendFormat("(@ItemId, @Type{0}, @Value{0}, @CleanValue{0})", i.ToString(CultureInfo.InvariantCulture)); + isSubsequentRow = true; + } - statement.TryBind("@ItemId", guidBlob); - statement.TryBind("@Type", pair.Item1); - statement.TryBind("@Value", itemValue); + using (var statement = PrepareStatementSafe(db, insertText.ToString())) + { + statement.TryBind("@ItemId", idBlob); - if (pair.Item2 == null) + for (var i = startIndex; i < endIndex; i++) { - statement.TryBindNull("@CleanValue"); - } - else - { - statement.TryBind("@CleanValue", GetCleanValue(pair.Item2)); + var index = i.ToString(CultureInfo.InvariantCulture); + + var currentValueInfo = values[i]; + + var itemValue = currentValueInfo.Item2; + + // Don't save if invalid + if (string.IsNullOrWhiteSpace(itemValue)) + { + continue; + } + + statement.TryBind("@Type" + index, currentValueInfo.Item1); + statement.TryBind("@Value" + index, itemValue); + statement.TryBind("@CleanValue" + index, GetCleanValue(itemValue)); } + statement.Reset(); statement.MoveNext(); } + + startIndex += limit; } } public void UpdatePeople(Guid itemId, List<PersonInfo> people) { - if (itemId == Guid.Empty) + if (itemId.Equals(Guid.Empty)) { throw new ArgumentNullException("itemId"); } @@ -5602,34 +6005,69 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type { using (var connection = CreateConnection()) { - // First delete - // "delete from People where ItemId=?" - connection.Execute("delete from People where ItemId=?", itemId.ToGuidBlob()); + connection.RunInTransaction(db => + { + var itemIdBlob = itemId.ToGuidBlob(); + + // First delete chapters + db.Execute("delete from People where ItemId=@ItemId", itemIdBlob); + + InsertPeople(itemIdBlob, people, db); + + }, TransactionMode); + + } + } + } + + private void InsertPeople(byte[] idBlob, List<PersonInfo> people, IDatabaseConnection db) + { + var startIndex = 0; + var limit = 100; + var listIndex = 0; - var listIndex = 0; + while (startIndex < people.Count) + { + var insertText = new StringBuilder("insert into People (ItemId, Name, Role, PersonType, SortOrder, ListOrder) values "); + + var endIndex = Math.Min(people.Count, startIndex + limit); + var isSubsequentRow = false; - using (var statement = PrepareStatement(connection, - "insert into People (ItemId, Name, Role, PersonType, SortOrder, ListOrder) values (@ItemId, @Name, @Role, @PersonType, @SortOrder, @ListOrder)")) + for (var i = startIndex; i < endIndex; i++) + { + if (isSubsequentRow) { - foreach (var person in people) - { - if (listIndex > 0) - { - statement.Reset(); - } + insertText.Append(","); + } - statement.TryBind("@ItemId", itemId.ToGuidBlob()); - statement.TryBind("@Name", person.Name); - statement.TryBind("@Role", person.Role); - statement.TryBind("@PersonType", person.Type); - statement.TryBind("@SortOrder", person.SortOrder); - statement.TryBind("@ListOrder", listIndex); + insertText.AppendFormat("(@ItemId, @Name{0}, @Role{0}, @PersonType{0}, @SortOrder{0}, @ListOrder{0})", i.ToString(CultureInfo.InvariantCulture)); + isSubsequentRow = true; + } - statement.MoveNext(); - listIndex++; - } + using (var statement = PrepareStatementSafe(db, insertText.ToString())) + { + statement.TryBind("@ItemId", idBlob); + + for (var i = startIndex; i < endIndex; i++) + { + var index = i.ToString(CultureInfo.InvariantCulture); + + var person = people[i]; + + statement.TryBind("@Name" + index, person.Name); + statement.TryBind("@Role" + index, person.Role); + statement.TryBind("@PersonType" + index, person.Type); + statement.TryBind("@SortOrder" + index, person.SortOrder); + statement.TryBind("@ListOrder" + index, listIndex); + + listIndex++; } + + statement.Reset(); + statement.MoveNext(); } + + startIndex += limit; } } @@ -5718,7 +6156,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type { CheckDisposed(); - if (id == Guid.Empty) + if (id.Equals(Guid.Empty)) { throw new ArgumentNullException("id"); } @@ -5734,62 +6172,111 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type { using (var connection = CreateConnection()) { - // First delete chapters - connection.Execute("delete from mediastreams where ItemId=@ItemId", id.ToGuidBlob()); + connection.RunInTransaction(db => + { + var itemIdBlob = id.ToGuidBlob(); + + // First delete chapters + db.Execute("delete from mediastreams where ItemId=@ItemId", itemIdBlob); + + InsertMediaStreams(itemIdBlob, streams, db); - using (var statement = PrepareStatement(connection, string.Format("replace into mediastreams ({0}) values ({1})", - string.Join(",", _mediaStreamSaveColumns), - string.Join(",", _mediaStreamSaveColumns.Select(i => "@" + i).ToArray())))) + }, TransactionMode); + } + } + } + + private void InsertMediaStreams(byte[] idBlob, List<MediaStream> streams, IDatabaseConnection db) + { + var startIndex = 0; + var limit = 10; + + while (startIndex < streams.Count) + { + var insertText = new StringBuilder(string.Format("insert into mediastreams ({0}) values ", string.Join(",", _mediaStreamSaveColumns))); + + var endIndex = Math.Min(streams.Count, startIndex + limit); + var isSubsequentRow = false; + + for (var i = startIndex; i < endIndex; i++) + { + if (isSubsequentRow) { - foreach (var stream in streams) - { - var paramList = new List<object>(); - - paramList.Add(id.ToGuidBlob()); - paramList.Add(stream.Index); - paramList.Add(stream.Type.ToString()); - paramList.Add(stream.Codec); - paramList.Add(stream.Language); - paramList.Add(stream.ChannelLayout); - paramList.Add(stream.Profile); - paramList.Add(stream.AspectRatio); - paramList.Add(stream.Path); - - paramList.Add(stream.IsInterlaced); - paramList.Add(stream.BitRate); - paramList.Add(stream.Channels); - paramList.Add(stream.SampleRate); - - paramList.Add(stream.IsDefault); - paramList.Add(stream.IsForced); - paramList.Add(stream.IsExternal); - - paramList.Add(stream.Width); - paramList.Add(stream.Height); - paramList.Add(stream.AverageFrameRate); - paramList.Add(stream.RealFrameRate); - paramList.Add(stream.Level); - paramList.Add(stream.PixelFormat); - paramList.Add(stream.BitDepth); - paramList.Add(stream.IsExternal); - paramList.Add(stream.RefFrames); - - paramList.Add(stream.CodecTag); - paramList.Add(stream.Comment); - paramList.Add(stream.NalLengthSize); - paramList.Add(stream.IsAVC); - paramList.Add(stream.Title); - - paramList.Add(stream.TimeBase); - paramList.Add(stream.CodecTimeBase); - - statement.Execute(paramList.ToArray()); - } + insertText.Append(","); + } + + var index = i.ToString(CultureInfo.InvariantCulture); + + var mediaStreamSaveColumns = string.Join(",", _mediaStreamSaveColumns.Skip(1).Select(m => "@" + m + index).ToArray()); + + insertText.AppendFormat("(@ItemId, {0})", mediaStreamSaveColumns); + isSubsequentRow = true; + } + + using (var statement = PrepareStatementSafe(db, insertText.ToString())) + { + statement.TryBind("@ItemId", idBlob); + + for (var i = startIndex; i < endIndex; i++) + { + var index = i.ToString(CultureInfo.InvariantCulture); + + var stream = streams[i]; + + statement.TryBind("@StreamIndex" + index, stream.Index); + statement.TryBind("@StreamType" + index, stream.Type.ToString()); + statement.TryBind("@Codec" + index, stream.Codec); + statement.TryBind("@Language" + index, stream.Language); + statement.TryBind("@ChannelLayout" + index, stream.ChannelLayout); + statement.TryBind("@Profile" + index, stream.Profile); + statement.TryBind("@AspectRatio" + index, stream.AspectRatio); + statement.TryBind("@Path" + index, GetPathToSave(stream.Path)); + + statement.TryBind("@IsInterlaced" + index, stream.IsInterlaced); + statement.TryBind("@BitRate" + index, stream.BitRate); + statement.TryBind("@Channels" + index, stream.Channels); + statement.TryBind("@SampleRate" + index, stream.SampleRate); + + statement.TryBind("@IsDefault" + index, stream.IsDefault); + statement.TryBind("@IsForced" + index, stream.IsForced); + statement.TryBind("@IsExternal" + index, stream.IsExternal); + + // Yes these are backwards due to a mistake + statement.TryBind("@Width" + index, stream.Height); + statement.TryBind("@Height" + index, stream.Width); + + statement.TryBind("@AverageFrameRate" + index, stream.AverageFrameRate); + statement.TryBind("@RealFrameRate" + index, stream.RealFrameRate); + statement.TryBind("@Level" + index, stream.Level); + + statement.TryBind("@PixelFormat" + index, stream.PixelFormat); + statement.TryBind("@BitDepth" + index, stream.BitDepth); + statement.TryBind("@IsExternal" + index, stream.IsExternal); + statement.TryBind("@RefFrames" + index, stream.RefFrames); + + statement.TryBind("@CodecTag" + index, stream.CodecTag); + statement.TryBind("@Comment" + index, stream.Comment); + statement.TryBind("@NalLengthSize" + index, stream.NalLengthSize); + statement.TryBind("@IsAvc" + index, stream.IsAVC); + statement.TryBind("@Title" + index, stream.Title); + + statement.TryBind("@TimeBase" + index, stream.TimeBase); + statement.TryBind("@CodecTimeBase" + index, stream.CodecTimeBase); + + statement.TryBind("@ColorPrimaries" + index, stream.ColorPrimaries); + statement.TryBind("@ColorSpace" + index, stream.ColorSpace); + statement.TryBind("@ColorTransfer" + index, stream.ColorTransfer); } + + statement.Reset(); + statement.MoveNext(); } + + startIndex += limit; } } + /// <summary> /// Gets the chapter. /// </summary> @@ -5831,7 +6318,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type if (reader[8].SQLiteType != SQLiteType.Null) { - item.Path = reader[8].ToString(); + item.Path = RestorePath(reader[8].ToString()); } item.IsInterlaced = reader.GetBoolean(9); @@ -5935,6 +6422,21 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type item.CodecTimeBase = reader[31].ToString(); } + if (reader[32].SQLiteType != SQLiteType.Null) + { + item.ColorPrimaries = reader[32].ToString(); + } + + if (reader[33].SQLiteType != SQLiteType.Null) + { + item.ColorSpace = reader[33].ToString(); + } + + if (reader[34].SQLiteType != SQLiteType.Null) + { + item.ColorTransfer = reader[34].ToString(); + } + return item; } diff --git a/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs b/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs index ad5c60ede..07d64a2b0 100644 --- a/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs @@ -9,12 +9,12 @@ using MediaBrowser.Controller.Persistence; using MediaBrowser.Model.IO; using MediaBrowser.Model.Logging; using SQLitePCL.pretty; +using MediaBrowser.Controller.Library; namespace Emby.Server.Implementations.Data { public class SqliteUserDataRepository : BaseSqliteRepository, IUserDataRepository { - private readonly string _importFile; private readonly IFileSystem _fileSystem; public SqliteUserDataRepository(ILogger logger, IApplicationPaths appPaths, IFileSystem fileSystem) @@ -22,7 +22,6 @@ namespace Emby.Server.Implementations.Data { _fileSystem = fileSystem; DbFilePath = Path.Combine(appPaths.DataPath, "library.db"); - _importFile = Path.Combine(appPaths.DataPath, "userdata_v2.db"); } /// <summary> @@ -41,7 +40,7 @@ namespace Emby.Server.Implementations.Data /// Opens the connection to the database /// </summary> /// <returns>Task.</returns> - public void Initialize(ReaderWriterLockSlim writeLock, ManagedConnection managedConnection) + public void Initialize(ReaderWriterLockSlim writeLock, ManagedConnection managedConnection, IUserManager userManager) { _connection = managedConnection; @@ -50,138 +49,134 @@ namespace Emby.Server.Implementations.Data using (var connection = CreateConnection()) { - string[] queries = { + var userDatasTableExists = TableExists(connection, "UserDatas"); + var userDataTableExists = TableExists(connection, "userdata"); - "create table if not exists userdata (key nvarchar not null, userId GUID not null, rating float null, played bit not null, playCount int not null, isFavorite bit not null, playbackPositionTicks bigint not null, lastPlayedDate datetime null)", - - "create table if not exists DataSettings (IsUserDataImported bit)", - - "drop index if exists idx_userdata", - "drop index if exists idx_userdata1", - "drop index if exists idx_userdata2", - "drop index if exists userdataindex1", - - "create unique index if not exists userdataindex on userdata (key, userId)", - "create index if not exists userdataindex2 on userdata (key, userId, played)", - "create index if not exists userdataindex3 on userdata (key, userId, playbackPositionTicks)", - "create index if not exists userdataindex4 on userdata (key, userId, isFavorite)", - - "pragma shrink_memory" - }; - - connection.RunQueries(queries); + var users = userDatasTableExists ? null : userManager.Users.ToArray(); connection.RunInTransaction(db => { - var existingColumnNames = GetColumnNames(db, "userdata"); + db.ExecuteAll(string.Join(";", new[] { + + "create table if not exists UserDatas (key nvarchar not null, userId INT not null, rating float null, played bit not null, playCount int not null, isFavorite bit not null, playbackPositionTicks bigint not null, lastPlayedDate datetime null, AudioStreamIndex INT, SubtitleStreamIndex INT)", + + "drop index if exists idx_userdata", + "drop index if exists idx_userdata1", + "drop index if exists idx_userdata2", + "drop index if exists userdataindex1", + "drop index if exists userdataindex", + "drop index if exists userdataindex3", + "drop index if exists userdataindex4", + "create unique index if not exists UserDatasIndex1 on UserDatas (key, userId)", + "create index if not exists UserDatasIndex2 on UserDatas (key, userId, played)", + "create index if not exists UserDatasIndex3 on UserDatas (key, userId, playbackPositionTicks)", + "create index if not exists UserDatasIndex4 on UserDatas (key, userId, isFavorite)" + })); + + if (userDataTableExists) + { + var existingColumnNames = GetColumnNames(db, "userdata"); - AddColumn(db, "userdata", "AudioStreamIndex", "int", existingColumnNames); - AddColumn(db, "userdata", "SubtitleStreamIndex", "int", existingColumnNames); - }, TransactionMode); + AddColumn(db, "userdata", "InternalUserId", "int", existingColumnNames); + AddColumn(db, "userdata", "AudioStreamIndex", "int", existingColumnNames); + AddColumn(db, "userdata", "SubtitleStreamIndex", "int", existingColumnNames); - try - { - ImportUserDataIfNeeded(connection); - } - catch (Exception ex) - { - Logger.ErrorException("Error in ImportUserDataIfNeeded", ex); - } - } - } + if (!userDatasTableExists) + { + ImportUserIds(db, users); - protected override bool EnableTempStoreMemory - { - get - { - return true; + db.ExecuteAll("INSERT INTO UserDatas (key, userId, rating, played, playCount, isFavorite, playbackPositionTicks, lastPlayedDate, AudioStreamIndex, SubtitleStreamIndex) SELECT key, InternalUserId, rating, played, playCount, isFavorite, playbackPositionTicks, lastPlayedDate, AudioStreamIndex, SubtitleStreamIndex from userdata where InternalUserId not null"); + } + } + }, TransactionMode); } } - private void ImportUserDataIfNeeded(ManagedConnection connection) + private void ImportUserIds(IDatabaseConnection db, User[] users) { - if (!_fileSystem.FileExists(_importFile)) + var userIdsWithUserData = GetAllUserIdsWithUserData(db); + + using (var statement = db.PrepareStatement("update userdata set InternalUserId=@InternalUserId where UserId=@UserId")) { - return; - } + foreach (var user in users) + { + if (!userIdsWithUserData.Contains(user.Id)) + { + continue; + } - var fileToImport = _importFile; - var isImported = connection.Query("select IsUserDataImported from DataSettings").SelectScalarBool().FirstOrDefault(); + statement.TryBind("@UserId", user.Id.ToGuidBlob()); + statement.TryBind("@InternalUserId", user.InternalId); - if (isImported) - { - return; + statement.MoveNext(); + statement.Reset(); + } } + } - ImportUserData(connection, fileToImport); + private List<Guid> GetAllUserIdsWithUserData(IDatabaseConnection db) + { + List<Guid> list = new List<Guid>(); - connection.RunInTransaction(db => + using (var statement = PrepareStatement(db, "select DISTINCT UserId from UserData where UserId not null")) { - using (var statement = db.PrepareStatement("replace into DataSettings (IsUserDataImported) values (@IsUserDataImported)")) + foreach (var row in statement.ExecuteQuery()) { - statement.TryBind("@IsUserDataImported", true); - statement.MoveNext(); + try + { + list.Add(row[0].ReadGuidFromBlob()); + } + catch + { + + } } - }, TransactionMode); + } + + return list; } - private void ImportUserData(ManagedConnection connection, string file) + protected override bool EnableTempStoreMemory { - SqliteExtensions.Attach(connection, file, "UserDataBackup"); - - var columns = "key, userId, rating, played, playCount, isFavorite, playbackPositionTicks, lastPlayedDate, AudioStreamIndex, SubtitleStreamIndex"; - - connection.RunInTransaction(db => + get { - db.Execute("REPLACE INTO userdata(" + columns + ") SELECT " + columns + " FROM UserDataBackup.userdata;"); - }, TransactionMode); + return true; + } } /// <summary> /// Saves the user data. /// </summary> - /// <param name="userId">The user id.</param> - /// <param name="key">The key.</param> - /// <param name="userData">The user data.</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task.</returns> - /// <exception cref="System.ArgumentNullException">userData - /// or - /// cancellationToken - /// or - /// userId - /// or - /// userDataId</exception> - public void SaveUserData(Guid userId, string key, UserItemData userData, CancellationToken cancellationToken) + public void SaveUserData(long internalUserId, string key, UserItemData userData, CancellationToken cancellationToken) { if (userData == null) { throw new ArgumentNullException("userData"); } - if (userId == Guid.Empty) + if (internalUserId <= 0) { - throw new ArgumentNullException("userId"); + throw new ArgumentNullException("internalUserId"); } if (string.IsNullOrEmpty(key)) { throw new ArgumentNullException("key"); } - PersistUserData(userId, key, userData, cancellationToken); + PersistUserData(internalUserId, key, userData, cancellationToken); } - public void SaveAllUserData(Guid userId, UserItemData[] userData, CancellationToken cancellationToken) + public void SaveAllUserData(long internalUserId, UserItemData[] userData, CancellationToken cancellationToken) { if (userData == null) { throw new ArgumentNullException("userData"); } - if (userId == Guid.Empty) + if (internalUserId <= 0) { - throw new ArgumentNullException("userId"); + throw new ArgumentNullException("internalUserId"); } - PersistAllUserData(userId, userData, cancellationToken); + PersistAllUserData(internalUserId, userData, cancellationToken); } /// <summary> @@ -192,7 +187,7 @@ namespace Emby.Server.Implementations.Data /// <param name="userData">The user data.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>Task.</returns> - public void PersistUserData(Guid userId, string key, UserItemData userData, CancellationToken cancellationToken) + public void PersistUserData(long internalUserId, string key, UserItemData userData, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); @@ -202,17 +197,17 @@ namespace Emby.Server.Implementations.Data { connection.RunInTransaction(db => { - SaveUserData(db, userId, key, userData); + SaveUserData(db, internalUserId, key, userData); }, TransactionMode); } } } - private void SaveUserData(IDatabaseConnection db, Guid userId, string key, UserItemData userData) + private void SaveUserData(IDatabaseConnection db, long internalUserId, string key, UserItemData userData) { - using (var statement = db.PrepareStatement("replace into userdata (key, userId, rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex) values (@key, @userId, @rating,@played,@playCount,@isFavorite,@playbackPositionTicks,@lastPlayedDate,@AudioStreamIndex,@SubtitleStreamIndex)")) + using (var statement = db.PrepareStatement("replace into UserDatas (key, userId, rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex) values (@key, @userId, @rating,@played,@playCount,@isFavorite,@playbackPositionTicks,@lastPlayedDate,@AudioStreamIndex,@SubtitleStreamIndex)")) { - statement.TryBind("@userId", userId.ToGuidBlob()); + statement.TryBind("@userId", internalUserId); statement.TryBind("@key", key); if (userData.Rating.HasValue) @@ -263,7 +258,7 @@ namespace Emby.Server.Implementations.Data /// <summary> /// Persist all user data for the specified user /// </summary> - private void PersistAllUserData(Guid userId, UserItemData[] userDataList, CancellationToken cancellationToken) + private void PersistAllUserData(long internalUserId, UserItemData[] userDataList, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); @@ -275,7 +270,7 @@ namespace Emby.Server.Implementations.Data { foreach (var userItemData in userDataList) { - SaveUserData(db, userId, userItemData.Key, userItemData); + SaveUserData(db, internalUserId, userItemData.Key, userItemData); } }, TransactionMode); } @@ -293,11 +288,11 @@ namespace Emby.Server.Implementations.Data /// or /// key /// </exception> - public UserItemData GetUserData(Guid userId, string key) + public UserItemData GetUserData(long internalUserId, string key) { - if (userId == Guid.Empty) + if (internalUserId <= 0) { - throw new ArgumentNullException("userId"); + throw new ArgumentNullException("internalUserId"); } if (string.IsNullOrEmpty(key)) { @@ -308,9 +303,9 @@ namespace Emby.Server.Implementations.Data { using (var connection = CreateConnection(true)) { - using (var statement = connection.PrepareStatement("select key,userid,rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex from userdata where key =@Key and userId=@UserId")) + using (var statement = connection.PrepareStatement("select key,userid,rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex from UserDatas where key =@Key and userId=@UserId")) { - statement.TryBind("@UserId", userId.ToGuidBlob()); + statement.TryBind("@UserId", internalUserId); statement.TryBind("@Key", key); foreach (var row in statement.ExecuteQuery()) @@ -324,12 +319,8 @@ namespace Emby.Server.Implementations.Data } } - public UserItemData GetUserData(Guid userId, List<string> keys) + public UserItemData GetUserData(long internalUserId, List<string> keys) { - if (userId == Guid.Empty) - { - throw new ArgumentNullException("userId"); - } if (keys == null) { throw new ArgumentNullException("keys"); @@ -340,7 +331,7 @@ namespace Emby.Server.Implementations.Data return null; } - return GetUserData(userId, keys[0]); + return GetUserData(internalUserId, keys[0]); } /// <summary> @@ -348,11 +339,11 @@ namespace Emby.Server.Implementations.Data /// </summary> /// <param name="userId"></param> /// <returns></returns> - public List<UserItemData> GetAllUserData(Guid userId) + public List<UserItemData> GetAllUserData(long internalUserId) { - if (userId == Guid.Empty) + if (internalUserId <= 0) { - throw new ArgumentNullException("userId"); + throw new ArgumentNullException("internalUserId"); } var list = new List<UserItemData>(); @@ -361,9 +352,9 @@ namespace Emby.Server.Implementations.Data { using (var connection = CreateConnection()) { - using (var statement = connection.PrepareStatement("select key,userid,rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex from userdata where userId=@UserId")) + using (var statement = connection.PrepareStatement("select key,userid,rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex from UserDatas where userId=@UserId")) { - statement.TryBind("@UserId", userId.ToGuidBlob()); + statement.TryBind("@UserId", internalUserId); foreach (var row in statement.ExecuteQuery()) { @@ -385,7 +376,7 @@ namespace Emby.Server.Implementations.Data var userData = new UserItemData(); userData.Key = reader[0].ToString(); - userData.UserId = reader[1].ReadGuidFromBlob(); + //userData.UserId = reader[1].ReadGuidFromBlob(); if (reader[2].SQLiteType != SQLiteType.Null) { @@ -399,7 +390,7 @@ namespace Emby.Server.Implementations.Data if (reader[7].SQLiteType != SQLiteType.Null) { - userData.LastPlayedDate = reader[7].ReadDateTime(); + userData.LastPlayedDate = reader[7].TryReadDateTime(); } if (reader[8].SQLiteType != SQLiteType.Null) diff --git a/Emby.Server.Implementations/Data/SqliteUserRepository.cs b/Emby.Server.Implementations/Data/SqliteUserRepository.cs index e89de11c6..da828aa11 100644 --- a/Emby.Server.Implementations/Data/SqliteUserRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteUserRepository.cs @@ -18,13 +18,11 @@ namespace Emby.Server.Implementations.Data public class SqliteUserRepository : BaseSqliteRepository, IUserRepository { private readonly IJsonSerializer _jsonSerializer; - private readonly IMemoryStreamFactory _memoryStreamProvider; - public SqliteUserRepository(ILogger logger, IServerApplicationPaths appPaths, IJsonSerializer jsonSerializer, IMemoryStreamFactory memoryStreamProvider) + public SqliteUserRepository(ILogger logger, IServerApplicationPaths appPaths, IJsonSerializer jsonSerializer) : base(logger) { _jsonSerializer = jsonSerializer; - _memoryStreamProvider = memoryStreamProvider; DbFilePath = Path.Combine(appPaths.DataPath, "users.db"); } @@ -51,37 +49,83 @@ namespace Emby.Server.Implementations.Data { RunDefaultInitialization(connection); - string[] queries = { + var localUsersTableExists = TableExists(connection, "LocalUsersv2"); - "create table if not exists users (guid GUID primary key NOT NULL, data BLOB NOT NULL)", - "create index if not exists idx_users on users(guid)", + connection.RunQueries(new[] { + "create table if not exists LocalUsersv2 (Id INTEGER PRIMARY KEY, guid GUID NOT NULL, data BLOB NOT NULL)", + "drop index if exists idx_users" + }); - "pragma shrink_memory" - }; + if (!localUsersTableExists && TableExists(connection, "Users")) + { + TryMigrateToLocalUsersTable(connection); + } + } + } - connection.RunQueries(queries); + private void TryMigrateToLocalUsersTable(ManagedConnection connection) + { + try + { + connection.RunQueries(new[] + { + "INSERT INTO LocalUsersv2 (guid, data) SELECT guid,data from users" + }); + } + catch (Exception ex) + { + Logger.ErrorException("Error migrating users database", ex); } } /// <summary> /// Save a user in the repo /// </summary> - /// <param name="user">The user.</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task.</returns> - /// <exception cref="System.ArgumentNullException">user</exception> - public void SaveUser(User user, CancellationToken cancellationToken) + public void CreateUser(User user) { if (user == null) { throw new ArgumentNullException("user"); } - cancellationToken.ThrowIfCancellationRequested(); + var serialized = _jsonSerializer.SerializeToBytes(user); + + using (WriteLock.Write()) + { + using (var connection = CreateConnection()) + { + connection.RunInTransaction(db => + { + using (var statement = db.PrepareStatement("insert into LocalUsersv2 (guid, data) values (@guid, @data)")) + { + statement.TryBind("@guid", user.Id.ToGuidBlob()); + statement.TryBind("@data", serialized); + + statement.MoveNext(); + } - var serialized = _jsonSerializer.SerializeToBytes(user, _memoryStreamProvider); + var createdUser = GetUser(user.Id, false); - cancellationToken.ThrowIfCancellationRequested(); + if (createdUser == null) + { + throw new ApplicationException("created user should never be null"); + } + + user.InternalId = createdUser.InternalId; + + }, TransactionMode); + } + } + } + + public void UpdateUser(User user) + { + if (user == null) + { + throw new ArgumentNullException("user"); + } + + var serialized = _jsonSerializer.SerializeToBytes(user); using (WriteLock.Write()) { @@ -89,22 +133,59 @@ namespace Emby.Server.Implementations.Data { connection.RunInTransaction(db => { - using (var statement = db.PrepareStatement("replace into users (guid, data) values (@guid, @data)")) + using (var statement = db.PrepareStatement("update LocalUsersv2 set data=@data where Id=@InternalId")) { - statement.TryBind("@guid", user.Id.ToGuidBlob()); + statement.TryBind("@InternalId", user.InternalId); statement.TryBind("@data", serialized); statement.MoveNext(); } + }, TransactionMode); } } } + private User GetUser(Guid guid, bool openLock) + { + using (openLock ? WriteLock.Read() : null) + { + using (var connection = CreateConnection(true)) + { + using (var statement = connection.PrepareStatement("select id,guid,data from LocalUsersv2 where guid=@guid")) + { + statement.TryBind("@guid", guid); + + foreach (var row in statement.ExecuteQuery()) + { + return GetUser(row); + } + } + } + } + + return null; + } + + private User GetUser(IReadOnlyList<IResultSetValue> row) + { + 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; + } + } + /// <summary> /// Retrieve all users from the database /// </summary> /// <returns>IEnumerable{User}.</returns> - public IEnumerable<User> RetrieveAllUsers() + public List<User> RetrieveAllUsers() { var list = new List<User>(); @@ -112,17 +193,9 @@ namespace Emby.Server.Implementations.Data { using (var connection = CreateConnection(true)) { - foreach (var row in connection.Query("select guid,data from users")) + foreach (var row in connection.Query("select id,guid,data from LocalUsersv2")) { - var id = row[0].ReadGuidFromBlob(); - - using (var stream = _memoryStreamProvider.CreateNew(row[1].ToBlob())) - { - stream.Position = 0; - var user = _jsonSerializer.DeserializeFromStream<User>(stream); - user.Id = id; - list.Add(user); - } + list.Add(GetUser(row)); } } } @@ -137,24 +210,22 @@ namespace Emby.Server.Implementations.Data /// <param name="cancellationToken">The cancellation token.</param> /// <returns>Task.</returns> /// <exception cref="System.ArgumentNullException">user</exception> - public void DeleteUser(User user, CancellationToken cancellationToken) + public void DeleteUser(User user) { if (user == null) { throw new ArgumentNullException("user"); } - cancellationToken.ThrowIfCancellationRequested(); - using (WriteLock.Write()) { using (var connection = CreateConnection()) { connection.RunInTransaction(db => { - using (var statement = db.PrepareStatement("delete from users where guid=@id")) + using (var statement = db.PrepareStatement("delete from LocalUsersv2 where Id=@id")) { - statement.TryBind("@id", user.Id.ToGuidBlob()); + statement.TryBind("@id", user.InternalId); statement.MoveNext(); } }, TransactionMode); diff --git a/Emby.Server.Implementations/Devices/CameraUploadsDynamicFolder.cs b/Emby.Server.Implementations/Devices/CameraUploadsDynamicFolder.cs deleted file mode 100644 index bb9ef157c..000000000 --- a/Emby.Server.Implementations/Devices/CameraUploadsDynamicFolder.cs +++ /dev/null @@ -1,40 +0,0 @@ -using MediaBrowser.Common.Configuration; -using MediaBrowser.Controller.Entities; -using System; -using System.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; - -using MediaBrowser.Controller.IO; -using MediaBrowser.Model.IO; -using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Serialization; - -namespace Emby.Server.Implementations.Devices -{ - public class CameraUploadsDynamicFolder : IVirtualFolderCreator - { - private readonly IApplicationPaths _appPaths; - private readonly IFileSystem _fileSystem; - - public CameraUploadsDynamicFolder(IApplicationPaths appPaths, IFileSystem fileSystem) - { - _appPaths = appPaths; - _fileSystem = fileSystem; - } - - public BasePluginFolder GetFolder() - { - var path = Path.Combine(_appPaths.DataPath, "camerauploads"); - - _fileSystem.CreateDirectory(path); - - return new CameraUploadsFolder - { - Path = path - }; - } - } - -} diff --git a/Emby.Server.Implementations/Devices/CameraUploadsFolder.cs b/Emby.Server.Implementations/Devices/CameraUploadsFolder.cs deleted file mode 100644 index 5c205dd19..000000000 --- a/Emby.Server.Implementations/Devices/CameraUploadsFolder.cs +++ /dev/null @@ -1,71 +0,0 @@ -using System; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Serialization; - -namespace Emby.Server.Implementations.Devices -{ - public class CameraUploadsFolder : BasePluginFolder, ISupportsUserSpecificView - { - public CameraUploadsFolder() - { - Name = "Camera Uploads"; - } - - public override bool IsVisible(User user) - { - if (!user.Policy.EnableAllFolders && !user.Policy.EnabledFolders.Contains(Id.ToString("N"), StringComparer.OrdinalIgnoreCase)) - { - return false; - } - - return base.IsVisible(user) && HasChildren(); - } - - [IgnoreDataMember] - public override string CollectionType - { - get { return MediaBrowser.Model.Entities.CollectionType.HomeVideos; } - } - - [IgnoreDataMember] - public override bool SupportsInheritedParentImages - { - get - { - return false; - } - } - - public override string GetClientTypeName() - { - return typeof(CollectionFolder).Name; - } - - private bool? _hasChildren; - private bool HasChildren() - { - if (!_hasChildren.HasValue) - { - _hasChildren = LibraryManager.GetItemIds(new InternalItemsQuery { Parent = this }).Count > 0; - } - - return _hasChildren.Value; - } - - protected override Task ValidateChildrenInternal(IProgress<double> progress, CancellationToken cancellationToken, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService) - { - _hasChildren = null; - return base.ValidateChildrenInternal(progress, cancellationToken, recursive, refreshChildMetadata, refreshOptions, directoryService); - } - - [IgnoreDataMember] - public bool EnableUserSpecificView - { - get { return true; } - } - } -} diff --git a/Emby.Server.Implementations/Devices/DeviceManager.cs b/Emby.Server.Implementations/Devices/DeviceManager.cs index ee4c4bb26..0fac886ef 100644 --- a/Emby.Server.Implementations/Devices/DeviceManager.cs +++ b/Emby.Server.Implementations/Devices/DeviceManager.cs @@ -18,114 +18,150 @@ using System.Linq; using System.Threading.Tasks; using MediaBrowser.Model.IO; using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.IO; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Configuration; +using MediaBrowser.Controller.Plugins; +using MediaBrowser.Model.Globalization; +using MediaBrowser.Controller.Security; +using MediaBrowser.Model.Serialization; +using MediaBrowser.Common.Extensions; namespace Emby.Server.Implementations.Devices { public class DeviceManager : IDeviceManager { - private readonly IDeviceRepository _repo; + private readonly IJsonSerializer _json; private readonly IUserManager _userManager; private readonly IFileSystem _fileSystem; private readonly ILibraryMonitor _libraryMonitor; private readonly IServerConfigurationManager _config; private readonly ILogger _logger; private readonly INetworkManager _network; + private readonly ILibraryManager _libraryManager; + private readonly ILocalizationManager _localizationManager; + private readonly IAuthenticationRepository _authRepo; + + public event EventHandler<GenericEventArgs<Tuple<string, DeviceOptions>>> DeviceOptionsUpdated; public event EventHandler<GenericEventArgs<CameraImageUploadInfo>> CameraImageUploaded; - /// <summary> - /// Occurs when [device options updated]. - /// </summary> - public event EventHandler<GenericEventArgs<DeviceInfo>> DeviceOptionsUpdated; + private readonly object _cameraUploadSyncLock = new object(); + private readonly object _capabilitiesSyncLock = new object(); - public DeviceManager(IDeviceRepository repo, IUserManager userManager, IFileSystem fileSystem, ILibraryMonitor libraryMonitor, IServerConfigurationManager config, ILogger logger, INetworkManager network) + public DeviceManager(IAuthenticationRepository authRepo, IJsonSerializer json, ILibraryManager libraryManager, ILocalizationManager localizationManager, IUserManager userManager, IFileSystem fileSystem, ILibraryMonitor libraryMonitor, IServerConfigurationManager config, ILogger logger, INetworkManager network) { - _repo = repo; + _json = json; _userManager = userManager; _fileSystem = fileSystem; _libraryMonitor = libraryMonitor; _config = config; _logger = logger; _network = network; + _libraryManager = libraryManager; + _localizationManager = localizationManager; + _authRepo = authRepo; } - public DeviceInfo RegisterDevice(string reportedId, string name, string appName, string appVersion, string usedByUserId) + + private Dictionary<string, ClientCapabilities> _capabilitiesCache = new Dictionary<string, ClientCapabilities>(StringComparer.OrdinalIgnoreCase); + public void SaveCapabilities(string deviceId, ClientCapabilities capabilities) { - if (string.IsNullOrWhiteSpace(reportedId)) - { - throw new ArgumentNullException("reportedId"); - } + var path = Path.Combine(GetDevicePath(deviceId), "capabilities.json"); + _fileSystem.CreateDirectory(_fileSystem.GetDirectoryName(path)); - var device = GetDevice(reportedId) ?? new DeviceInfo + lock (_capabilitiesSyncLock) { - Id = reportedId - }; + _capabilitiesCache[deviceId] = capabilities; - device.ReportedName = name; - device.AppName = appName; - device.AppVersion = appVersion; + _json.SerializeToFile(capabilities, path); + } + } - if (!string.IsNullOrWhiteSpace(usedByUserId)) - { - var user = _userManager.GetUserById(usedByUserId); + public void UpdateDeviceOptions(string deviceId, DeviceOptions options) + { + _authRepo.UpdateDeviceOptions(deviceId, options); - device.LastUserId = user.Id.ToString("N"); - device.LastUserName = user.Name; + if (DeviceOptionsUpdated != null) + { + DeviceOptionsUpdated(this, new GenericEventArgs<Tuple<string, DeviceOptions>>() + { + Argument = new Tuple<string, DeviceOptions>(deviceId, options) + }); } + } - device.DateLastModified = DateTime.UtcNow; + public DeviceOptions GetDeviceOptions(string deviceId) + { + return _authRepo.GetDeviceOptions(deviceId); + } - device.Name = string.IsNullOrWhiteSpace(device.CustomName) ? device.ReportedName : device.CustomName; + public ClientCapabilities GetCapabilities(string id) + { + lock (_capabilitiesSyncLock) + { + ClientCapabilities result; + if (_capabilitiesCache.TryGetValue(id, out result)) + { + return result; + } - _repo.SaveDevice(device); + var path = Path.Combine(GetDevicePath(id), "capabilities.json"); + try + { + return _json.DeserializeFromFile<ClientCapabilities>(path) ?? new ClientCapabilities(); + } + catch + { + } + } - return device; + return new ClientCapabilities(); } - public void SaveCapabilities(string reportedId, ClientCapabilities capabilities) + public DeviceInfo GetDevice(string id) { - _repo.SaveCapabilities(reportedId, capabilities); + return GetDevice(id, true); } - public ClientCapabilities GetCapabilities(string reportedId) + private DeviceInfo GetDevice(string id, bool includeCapabilities) { - return _repo.GetCapabilities(reportedId); - } + var session = _authRepo.Get(new AuthenticationInfoQuery + { + DeviceId = id - public DeviceInfo GetDevice(string id) - { - return _repo.GetDevice(id); + }).Items.FirstOrDefault(); + + var device = session == null ? null : ToDeviceInfo(session); + + return device; } public QueryResult<DeviceInfo> GetDevices(DeviceQuery query) { - IEnumerable<DeviceInfo> devices = _repo.GetDevices(); + var sessions = _authRepo.Get(new AuthenticationInfoQuery + { + //UserId = query.UserId + HasUser = true + + }).Items; if (query.SupportsSync.HasValue) { var val = query.SupportsSync.Value; - devices = devices.Where(i => i.Capabilities.SupportsSync == val); + sessions = sessions.Where(i => GetCapabilities(i.DeviceId).SupportsSync == val).ToArray(); } - if (query.SupportsPersistentIdentifier.HasValue) + if (!query.UserId.Equals(Guid.Empty)) { - var val = query.SupportsPersistentIdentifier.Value; + var user = _userManager.GetUserById(query.UserId); - devices = devices.Where(i => - { - var deviceVal = i.Capabilities.SupportsPersistentIdentifier; - return deviceVal == val; - }); + sessions = sessions.Where(i => CanAccessDevice(user, i.DeviceId)).ToArray(); } - if (!string.IsNullOrWhiteSpace(query.UserId)) - { - devices = devices.Where(i => CanAccessDevice(query.UserId, i.Id)); - } + var array = sessions.Select(ToDeviceInfo).ToArray(); - var array = devices.ToArray(); return new QueryResult<DeviceInfo> { Items = array, @@ -133,20 +169,59 @@ namespace Emby.Server.Implementations.Devices }; } - public void DeleteDevice(string id) + private DeviceInfo ToDeviceInfo(AuthenticationInfo authInfo) + { + var caps = GetCapabilities(authInfo.DeviceId); + + return new DeviceInfo + { + AppName = authInfo.AppName, + AppVersion = authInfo.AppVersion, + Id = authInfo.DeviceId, + LastUserId = authInfo.UserId, + LastUserName = authInfo.UserName, + Name = authInfo.DeviceName, + DateLastActivity = authInfo.DateLastActivity, + IconUrl = caps == null ? null : caps.IconUrl + }; + } + + private string GetDevicesPath() + { + return Path.Combine(_config.ApplicationPaths.DataPath, "devices"); + } + + private string GetDevicePath(string id) { - _repo.DeleteDevice(id); + return Path.Combine(GetDevicesPath(), id.GetMD5().ToString("N")); } public ContentUploadHistory GetCameraUploadHistory(string deviceId) { - return _repo.GetCameraUploadHistory(deviceId); + var path = Path.Combine(GetDevicePath(deviceId), "camerauploads.json"); + + lock (_cameraUploadSyncLock) + { + try + { + return _json.DeserializeFromFile<ContentUploadHistory>(path); + } + catch (IOException) + { + return new ContentUploadHistory + { + DeviceId = deviceId + }; + } + } } public async Task AcceptCameraUpload(string deviceId, Stream stream, LocalFileInfo file) { - var device = GetDevice(deviceId); - var path = GetUploadPath(device); + var device = GetDevice(deviceId, false); + var uploadPathInfo = GetUploadPath(device); + + var path = uploadPathInfo.Item1; if (!string.IsNullOrWhiteSpace(file.Album)) { @@ -156,10 +231,12 @@ namespace Emby.Server.Implementations.Devices path = Path.Combine(path, file.Name); path = Path.ChangeExtension(path, MimeTypes.ToExtension(file.MimeType) ?? "jpg"); - _libraryMonitor.ReportFileSystemChangeBeginning(path); - _fileSystem.CreateDirectory(_fileSystem.GetDirectoryName(path)); + await EnsureLibraryFolder(uploadPathInfo.Item2, uploadPathInfo.Item3).ConfigureAwait(false); + + _libraryMonitor.ReportFileSystemChangeBeginning(path); + try { using (var fs = _fileSystem.GetFileStream(path, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read)) @@ -167,7 +244,7 @@ namespace Emby.Server.Implementations.Devices await stream.CopyToAsync(fs).ConfigureAwait(false); } - _repo.AddCameraUpload(deviceId, file); + AddCameraUpload(deviceId, file); } finally { @@ -187,65 +264,118 @@ namespace Emby.Server.Implementations.Devices } } - private string GetUploadPath(DeviceInfo device) + private void AddCameraUpload(string deviceId, LocalFileInfo file) { - if (!string.IsNullOrWhiteSpace(device.CameraUploadPath)) + var path = Path.Combine(GetDevicePath(deviceId), "camerauploads.json"); + _fileSystem.CreateDirectory(_fileSystem.GetDirectoryName(path)); + + lock (_cameraUploadSyncLock) { - return device.CameraUploadPath; + ContentUploadHistory history; + + try + { + history = _json.DeserializeFromFile<ContentUploadHistory>(path); + } + catch (IOException) + { + history = new ContentUploadHistory + { + DeviceId = deviceId + }; + } + + history.DeviceId = deviceId; + + var list = history.FilesUploaded.ToList(); + list.Add(file); + history.FilesUploaded = list.ToArray(list.Count); + + _json.SerializeToFile(history, path); } + } + internal Task EnsureLibraryFolder(string path, string name) + { + var existingFolders = _libraryManager + .RootFolder + .Children + .OfType<Folder>() + .Where(i => _fileSystem.AreEqual(path, i.Path) || _fileSystem.ContainsSubPath(i.Path, path)) + .ToList(); + + if (existingFolders.Count > 0) + { + return Task.CompletedTask; + } + + _fileSystem.CreateDirectory(path); + + var libraryOptions = new LibraryOptions + { + PathInfos = new[] { new MediaPathInfo { Path = path } }, + EnablePhotos = true, + EnableRealtimeMonitor = false, + SaveLocalMetadata = true + }; + + if (string.IsNullOrWhiteSpace(name)) + { + name = _localizationManager.GetLocalizedString("HeaderCameraUploads"); + } + + return _libraryManager.AddVirtualFolder(name, CollectionType.HomeVideos, libraryOptions, true); + } + + private Tuple<string, string, string> GetUploadPath(DeviceInfo device) + { var config = _config.GetUploadOptions(); var path = config.CameraUploadPath; + if (string.IsNullOrWhiteSpace(path)) { path = DefaultCameraUploadsPath; } + var topLibraryPath = path; + if (config.EnableCameraUploadSubfolders) { path = Path.Combine(path, _fileSystem.GetValidFilename(device.Name)); } - return path; - } - - private string DefaultCameraUploadsPath - { - get { return Path.Combine(_config.CommonApplicationPaths.DataPath, "camerauploads"); } + return new Tuple<string, string, string>(path, topLibraryPath, null); } - public void UpdateDeviceInfo(string id, DeviceOptions options) + internal string GetUploadsPath() { - var device = GetDevice(id); - - device.CustomName = options.CustomName; - device.CameraUploadPath = options.CameraUploadPath; + var config = _config.GetUploadOptions(); + var path = config.CameraUploadPath; - device.Name = string.IsNullOrWhiteSpace(device.CustomName) ? device.ReportedName : device.CustomName; + if (string.IsNullOrWhiteSpace(path)) + { + path = DefaultCameraUploadsPath; + } - _repo.SaveDevice(device); + return path; + } - EventHelper.FireEventIfNotNull(DeviceOptionsUpdated, this, new GenericEventArgs<DeviceInfo>(device), _logger); + private string DefaultCameraUploadsPath + { + get { return Path.Combine(_config.CommonApplicationPaths.DataPath, "camerauploads"); } } - public bool CanAccessDevice(string userId, string deviceId) + public bool CanAccessDevice(User user, string deviceId) { - if (string.IsNullOrWhiteSpace(userId)) + if (user == null) { - throw new ArgumentNullException("userId"); + throw new ArgumentException("user not found"); } - if (string.IsNullOrWhiteSpace(deviceId)) + if (string.IsNullOrEmpty(deviceId)) { throw new ArgumentNullException("deviceId"); } - var user = _userManager.GetUserById(userId); - - if (user == null) - { - throw new ArgumentException("user not found"); - } - if (!CanAccessDevice(user.Policy, deviceId)) { var capabilities = GetCapabilities(deviceId); @@ -271,15 +401,89 @@ namespace Emby.Server.Implementations.Devices return true; } - return ListHelper.ContainsIgnoreCase(policy.EnabledDevices, id); + return policy.EnabledDevices.Contains(id, StringComparer.OrdinalIgnoreCase); + } + } + + public class DeviceManagerEntryPoint : IServerEntryPoint + { + private readonly DeviceManager _deviceManager; + private readonly IServerConfigurationManager _config; + private readonly IFileSystem _fileSystem; + private ILogger _logger; + + public DeviceManagerEntryPoint(IDeviceManager deviceManager, IServerConfigurationManager config, IFileSystem fileSystem, ILogger logger) + { + _deviceManager = (DeviceManager)deviceManager; + _config = config; + _fileSystem = fileSystem; + _logger = logger; + } + + public async void Run() + { + if (!_config.Configuration.CameraUploadUpgraded && _config.Configuration.IsStartupWizardCompleted) + { + var path = _deviceManager.GetUploadsPath(); + + if (_fileSystem.DirectoryExists(path)) + { + try + { + await _deviceManager.EnsureLibraryFolder(path, null).ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.ErrorException("Error creating camera uploads library", ex); + } + + _config.Configuration.CameraUploadUpgraded = true; + _config.SaveConfiguration(); + } + } + } + + #region IDisposable Support + private bool disposedValue = false; // To detect redundant calls + + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + // TODO: dispose managed state (managed objects). + } + + // TODO: free unmanaged resources (unmanaged objects) and override a finalizer below. + // TODO: set large fields to null. + + disposedValue = true; + } + } + + // TODO: override a finalizer only if Dispose(bool disposing) above has code to free unmanaged resources. + // ~DeviceManagerEntryPoint() { + // // Do not change this code. Put cleanup code in Dispose(bool disposing) above. + // Dispose(false); + // } + + // This code added to correctly implement the disposable pattern. + public void Dispose() + { + // Do not change this code. Put cleanup code in Dispose(bool disposing) above. + Dispose(true); + // TODO: uncomment the following line if the finalizer is overridden above. + // GC.SuppressFinalize(this); } + #endregion } public class DevicesConfigStore : IConfigurationFactory { public IEnumerable<ConfigurationStore> GetConfigurations() { - return new List<ConfigurationStore> + return new ConfigurationStore[] { new ConfigurationStore { diff --git a/Emby.Server.Implementations/Devices/SqliteDeviceRepository.cs b/Emby.Server.Implementations/Devices/SqliteDeviceRepository.cs deleted file mode 100644 index d7817b17a..000000000 --- a/Emby.Server.Implementations/Devices/SqliteDeviceRepository.cs +++ /dev/null @@ -1,451 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using Emby.Server.Implementations.Data; -using MediaBrowser.Controller; -using MediaBrowser.Model.Logging; -using SQLitePCL.pretty; -using MediaBrowser.Model.Extensions; -using MediaBrowser.Model.IO; -using MediaBrowser.Common.Extensions; -using MediaBrowser.Controller.Devices; -using MediaBrowser.Model.Devices; -using MediaBrowser.Model.Serialization; -using MediaBrowser.Model.Session; -using MediaBrowser.Controller.Configuration; - -namespace Emby.Server.Implementations.Devices -{ - public class SqliteDeviceRepository : BaseSqliteRepository, IDeviceRepository - { - private readonly CultureInfo _usCulture = new CultureInfo("en-US"); - protected IFileSystem FileSystem { get; private set; } - private readonly object _syncLock = new object(); - private readonly IJsonSerializer _json; - private IServerApplicationPaths _appPaths; - - public SqliteDeviceRepository(ILogger logger, IServerConfigurationManager config, IFileSystem fileSystem, IJsonSerializer json) - : base(logger) - { - var appPaths = config.ApplicationPaths; - - DbFilePath = Path.Combine(appPaths.DataPath, "devices.db"); - FileSystem = fileSystem; - _json = json; - _appPaths = appPaths; - } - - public void Initialize() - { - try - { - InitializeInternal(); - } - catch (Exception ex) - { - Logger.ErrorException("Error loading database file. Will reset and retry.", ex); - - FileSystem.DeleteFile(DbFilePath); - - InitializeInternal(); - } - } - - private void InitializeInternal() - { - using (var connection = CreateConnection()) - { - RunDefaultInitialization(connection); - - string[] queries = { - "create table if not exists Devices (Id TEXT PRIMARY KEY, Name TEXT NOT NULL, ReportedName TEXT NOT NULL, CustomName TEXT, CameraUploadPath TEXT, LastUserName TEXT, AppName TEXT NOT NULL, AppVersion TEXT NOT NULL, LastUserId TEXT, DateLastModified DATETIME NOT NULL, Capabilities TEXT NOT NULL)", - "create index if not exists idx_id on Devices(Id)" - }; - - connection.RunQueries(queries); - - MigrateDevices(); - } - } - - private void MigrateDevices() - { - List<string> files; - try - { - files = FileSystem - .GetFilePaths(GetDevicesPath(), true) - .Where(i => string.Equals(Path.GetFileName(i), "device.json", StringComparison.OrdinalIgnoreCase)) - .ToList(); - } - catch (IOException) - { - return; - } - - foreach (var file in files) - { - try - { - var device = _json.DeserializeFromFile<DeviceInfo>(file); - - device.Name = string.IsNullOrWhiteSpace(device.CustomName) ? device.ReportedName : device.CustomName; - - SaveDevice(device); - } - catch (Exception ex) - { - Logger.ErrorException("Error reading {0}", ex, file); - } - finally - { - try - { - FileSystem.DeleteFile(file); - } - catch (IOException) - { - try - { - FileSystem.MoveFile(file, Path.ChangeExtension(file, ".old")); - } - catch (IOException) - { - } - } - } - } - } - - private const string BaseSelectText = "select Id, Name, ReportedName, CustomName, CameraUploadPath, LastUserName, AppName, AppVersion, LastUserId, DateLastModified, Capabilities from Devices"; - - public void SaveCapabilities(string deviceId, ClientCapabilities capabilities) - { - using (WriteLock.Write()) - { - using (var connection = CreateConnection()) - { - connection.RunInTransaction(db => - { - using (var statement = db.PrepareStatement("update devices set Capabilities=@Capabilities where Id=@Id")) - { - statement.TryBind("@Id", deviceId); - - if (capabilities == null) - { - statement.TryBindNull("@Capabilities"); - } - else - { - statement.TryBind("@Capabilities", _json.SerializeToString(capabilities)); - } - - statement.MoveNext(); - } - }, TransactionMode); - } - } - } - - public void SaveDevice(DeviceInfo entry) - { - if (entry == null) - { - throw new ArgumentNullException("entry"); - } - - using (WriteLock.Write()) - { - using (var connection = CreateConnection()) - { - connection.RunInTransaction(db => - { - using (var statement = db.PrepareStatement("replace into Devices (Id, Name, ReportedName, CustomName, CameraUploadPath, LastUserName, AppName, AppVersion, LastUserId, DateLastModified, Capabilities) values (@Id, @Name, @ReportedName, @CustomName, @CameraUploadPath, @LastUserName, @AppName, @AppVersion, @LastUserId, @DateLastModified, @Capabilities)")) - { - statement.TryBind("@Id", entry.Id); - statement.TryBind("@Name", entry.Name); - statement.TryBind("@ReportedName", entry.ReportedName); - statement.TryBind("@CustomName", entry.CustomName); - statement.TryBind("@CameraUploadPath", entry.CameraUploadPath); - statement.TryBind("@LastUserName", entry.LastUserName); - statement.TryBind("@AppName", entry.AppName); - statement.TryBind("@AppVersion", entry.AppVersion); - statement.TryBind("@DateLastModified", entry.DateLastModified); - - if (entry.Capabilities == null) - { - statement.TryBindNull("@Capabilities"); - } - else - { - statement.TryBind("@Capabilities", _json.SerializeToString(entry.Capabilities)); - } - - statement.MoveNext(); - } - }, TransactionMode); - } - } - } - - public DeviceInfo GetDevice(string id) - { - using (WriteLock.Read()) - { - using (var connection = CreateConnection(true)) - { - var statementTexts = new List<string>(); - statementTexts.Add(BaseSelectText + " where Id=@Id"); - - return connection.RunInTransaction(db => - { - var statements = PrepareAllSafe(db, statementTexts).ToList(); - - using (var statement = statements[0]) - { - statement.TryBind("@Id", id); - - foreach (var row in statement.ExecuteQuery()) - { - return GetEntry(row); - } - } - - return null; - - }, ReadTransactionMode); - } - } - } - - public List<DeviceInfo> GetDevices() - { - using (WriteLock.Read()) - { - using (var connection = CreateConnection(true)) - { - var statementTexts = new List<string>(); - statementTexts.Add(BaseSelectText + " order by DateLastModified desc"); - - return connection.RunInTransaction(db => - { - var list = new List<DeviceInfo>(); - - var statements = PrepareAllSafe(db, statementTexts).ToList(); - - using (var statement = statements[0]) - { - foreach (var row in statement.ExecuteQuery()) - { - list.Add(GetEntry(row)); - } - } - - return list; - - }, ReadTransactionMode); - } - } - } - - public ClientCapabilities GetCapabilities(string id) - { - using (WriteLock.Read()) - { - using (var connection = CreateConnection(true)) - { - var statementTexts = new List<string>(); - statementTexts.Add("Select Capabilities from Devices where Id=@Id"); - - return connection.RunInTransaction(db => - { - var statements = PrepareAllSafe(db, statementTexts).ToList(); - - using (var statement = statements[0]) - { - statement.TryBind("@Id", id); - - foreach (var row in statement.ExecuteQuery()) - { - if (row[0].SQLiteType != SQLiteType.Null) - { - return _json.DeserializeFromString<ClientCapabilities>(row.GetString(0)); - } - } - } - - return null; - - }, ReadTransactionMode); - } - } - } - - private DeviceInfo GetEntry(IReadOnlyList<IResultSetValue> reader) - { - var index = 0; - - var info = new DeviceInfo - { - Id = reader.GetString(index) - }; - - index++; - if (reader[index].SQLiteType != SQLiteType.Null) - { - info.Name = reader.GetString(index); - } - - index++; - if (reader[index].SQLiteType != SQLiteType.Null) - { - info.ReportedName = reader.GetString(index); - } - - index++; - if (reader[index].SQLiteType != SQLiteType.Null) - { - info.CustomName = reader.GetString(index); - } - - index++; - if (reader[index].SQLiteType != SQLiteType.Null) - { - info.CameraUploadPath = reader.GetString(index); - } - - index++; - if (reader[index].SQLiteType != SQLiteType.Null) - { - info.LastUserName = reader.GetString(index); - } - - index++; - if (reader[index].SQLiteType != SQLiteType.Null) - { - info.AppName = reader.GetString(index); - } - - index++; - if (reader[index].SQLiteType != SQLiteType.Null) - { - info.AppVersion = reader.GetString(index); - } - - index++; - if (reader[index].SQLiteType != SQLiteType.Null) - { - info.LastUserId = reader.GetString(index); - } - - index++; - if (reader[index].SQLiteType != SQLiteType.Null) - { - info.DateLastModified = reader[index].ReadDateTime(); - } - - index++; - if (reader[index].SQLiteType != SQLiteType.Null) - { - info.Capabilities = _json.DeserializeFromString<ClientCapabilities>(reader.GetString(index)); - } - - return info; - } - - private string GetDevicesPath() - { - return Path.Combine(_appPaths.DataPath, "devices"); - } - - private string GetDevicePath(string id) - { - return Path.Combine(GetDevicesPath(), id.GetMD5().ToString("N")); - } - - public ContentUploadHistory GetCameraUploadHistory(string deviceId) - { - var path = Path.Combine(GetDevicePath(deviceId), "camerauploads.json"); - - lock (_syncLock) - { - try - { - return _json.DeserializeFromFile<ContentUploadHistory>(path); - } - catch (IOException) - { - return new ContentUploadHistory - { - DeviceId = deviceId - }; - } - } - } - - public void AddCameraUpload(string deviceId, LocalFileInfo file) - { - var path = Path.Combine(GetDevicePath(deviceId), "camerauploads.json"); - FileSystem.CreateDirectory(FileSystem.GetDirectoryName(path)); - - lock (_syncLock) - { - ContentUploadHistory history; - - try - { - history = _json.DeserializeFromFile<ContentUploadHistory>(path); - } - catch (IOException) - { - history = new ContentUploadHistory - { - DeviceId = deviceId - }; - } - - history.DeviceId = deviceId; - - var list = history.FilesUploaded.ToList(); - list.Add(file); - history.FilesUploaded = list.ToArray(list.Count); - - _json.SerializeToFile(history, path); - } - } - - public void DeleteDevice(string id) - { - using (WriteLock.Write()) - { - using (var connection = CreateConnection()) - { - connection.RunInTransaction(db => - { - using (var statement = db.PrepareStatement("delete from devices where Id=@Id")) - { - statement.TryBind("@Id", id); - - statement.MoveNext(); - } - }, TransactionMode); - } - } - - var path = GetDevicePath(id); - - lock (_syncLock) - { - try - { - FileSystem.DeleteDirectory(path, true); - } - catch (IOException) - { - } - } - } - } -} diff --git a/Emby.Server.Implementations/Diagnostics/CommonProcess.cs b/Emby.Server.Implementations/Diagnostics/CommonProcess.cs index a0a5f32ef..a709607bd 100644 --- a/Emby.Server.Implementations/Diagnostics/CommonProcess.cs +++ b/Emby.Server.Implementations/Diagnostics/CommonProcess.cs @@ -3,6 +3,7 @@ using System.Diagnostics; using System.IO; using System.Threading.Tasks; using MediaBrowser.Model.Diagnostics; +using System.Threading; namespace Emby.Server.Implementations.Diagnostics { @@ -48,8 +49,32 @@ namespace Emby.Server.Implementations.Diagnostics } } + private bool _hasExited; + private bool HasExited + { + get + { + if (_hasExited) + { + return true; + } + + try + { + _hasExited = _process.HasExited; + } + catch (InvalidOperationException) + { + _hasExited = true; + } + + return _hasExited; + } + } + private void _process_Exited(object sender, EventArgs e) { + _hasExited = true; if (Exited != null) { Exited(this, e); @@ -98,13 +123,33 @@ namespace Emby.Server.Implementations.Diagnostics public Task<bool> WaitForExitAsync(int timeMs) { - return Task.FromResult(_process.WaitForExit(timeMs)); + //if (_process.WaitForExit(100)) + //{ + // return Task.FromResult(true); + //} + + //timeMs -= 100; + timeMs = Math.Max(0, timeMs); + + var tcs = new TaskCompletionSource<bool>(); + + var cancellationToken = new CancellationTokenSource(timeMs).Token; + + if (HasExited) + { + return Task.FromResult(true); + } + + _process.Exited += (sender, args) => tcs.TrySetResult(true); + + cancellationToken.Register(() => tcs.TrySetResult(HasExited)); + + return tcs.Task; } public void Dispose() { _process.Dispose(); - GC.SuppressFinalize(this); } } } diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs index 0a316fcf1..437917c45 100644 --- a/Emby.Server.Implementations/Dto/DtoService.cs +++ b/Emby.Server.Implementations/Dto/DtoService.cs @@ -18,16 +18,14 @@ using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Logging; using MediaBrowser.Model.Querying; -using MediaBrowser.Model.Sync; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; - -using MediaBrowser.Controller.IO; using MediaBrowser.Model.IO; using MediaBrowser.Model.Extensions; +using MediaBrowser.Controller.Playlists; namespace Emby.Server.Implementations.Dto { @@ -44,13 +42,12 @@ namespace Emby.Server.Implementations.Dto private readonly IProviderManager _providerManager; private readonly Func<IChannelManager> _channelManagerFactory; - private readonly ISyncManager _syncManager; private readonly IApplicationHost _appHost; private readonly Func<IDeviceManager> _deviceManager; private readonly Func<IMediaSourceManager> _mediaSourceManager; private readonly Func<ILiveTvManager> _livetvManager; - public DtoService(ILogger logger, ILibraryManager libraryManager, IUserDataManager userDataRepository, IItemRepository itemRepo, IImageProcessor imageProcessor, IServerConfigurationManager config, IFileSystem fileSystem, IProviderManager providerManager, Func<IChannelManager> channelManagerFactory, ISyncManager syncManager, IApplicationHost appHost, Func<IDeviceManager> deviceManager, Func<IMediaSourceManager> mediaSourceManager, Func<ILiveTvManager> livetvManager) + public DtoService(ILogger logger, ILibraryManager libraryManager, IUserDataManager userDataRepository, IItemRepository itemRepo, IImageProcessor imageProcessor, IServerConfigurationManager config, IFileSystem fileSystem, IProviderManager providerManager, Func<IChannelManager> channelManagerFactory, IApplicationHost appHost, Func<IDeviceManager> deviceManager, Func<IMediaSourceManager> mediaSourceManager, Func<ILiveTvManager> livetvManager) { _logger = logger; _libraryManager = libraryManager; @@ -61,7 +58,6 @@ namespace Emby.Server.Implementations.Dto _fileSystem = fileSystem; _providerManager = providerManager; _channelManagerFactory = channelManagerFactory; - _syncManager = syncManager; _appHost = appHost; _deviceManager = deviceManager; _mediaSourceManager = mediaSourceManager; @@ -99,18 +95,6 @@ namespace Emby.Server.Implementations.Dto public BaseItemDto[] GetBaseItemDtos(IEnumerable<BaseItem> items, int itemCount, DtoOptions options, User user = null, BaseItem owner = null) { - if (items == null) - { - throw new ArgumentNullException("items"); - } - - if (options == null) - { - throw new ArgumentNullException("options"); - } - - var syncDictionary = GetSyncedItemProgress(options); - var returnItems = new BaseItemDto[itemCount]; var programTuples = new List<Tuple<BaseItem, BaseItemDto>>(); var channelTuples = new List<Tuple<BaseItemDto, LiveTvChannel>>(); @@ -136,7 +120,7 @@ namespace Emby.Server.Implementations.Dto if (byName != null) { - if (options.Fields.Contains(ItemFields.ItemCounts)) + if (options.ContainsField(ItemFields.ItemCounts)) { var libraryItems = byName.GetTaggedItems(new InternalItemsQuery(user) { @@ -147,12 +131,10 @@ namespace Emby.Server.Implementations.Dto } }); - SetItemByNameInfo(item, dto, libraryItems.ToList(), user); + SetItemByNameInfo(item, dto, libraryItems, user); } } - FillSyncInfo(dto, item, options, user, syncDictionary); - returnItems[index] = dto; index++; } @@ -173,8 +155,6 @@ namespace Emby.Server.Implementations.Dto public BaseItemDto GetBaseItemDto(BaseItem item, DtoOptions options, User user = null, BaseItem owner = null) { - var syncDictionary = GetSyncedItemProgress(options); - var allCollectionFolders = _libraryManager.GetUserRootFolder().Children.OfType<Folder>().ToList(); var dto = GetBaseItemDtoInternal(item, options, allCollectionFolders, user, owner); var tvChannel = item as LiveTvChannel; @@ -194,7 +174,7 @@ namespace Emby.Server.Implementations.Dto if (byName != null) { - if (options.Fields.Contains(ItemFields.ItemCounts)) + if (options.ContainsField(ItemFields.ItemCounts)) { SetItemByNameInfo(item, dto, GetTaggedItems(byName, user, new DtoOptions(false) { @@ -203,123 +183,24 @@ namespace Emby.Server.Implementations.Dto }), user); } - FillSyncInfo(dto, item, options, user, syncDictionary); return dto; } - FillSyncInfo(dto, item, options, user, syncDictionary); - return dto; } - private List<BaseItem> GetTaggedItems(IItemByName byName, User user, DtoOptions options) + private IList<BaseItem> GetTaggedItems(IItemByName byName, User user, DtoOptions options) { - var items = byName.GetTaggedItems(new InternalItemsQuery(user) + return byName.GetTaggedItems(new InternalItemsQuery(user) { Recursive = true, DtoOptions = options - }).ToList(); - - return items; - } - - public Dictionary<string, SyncedItemProgress> GetSyncedItemProgress(DtoOptions options) - { - if (!options.Fields.Contains(ItemFields.BasicSyncInfo) && - !options.Fields.Contains(ItemFields.SyncInfo)) - { - return new Dictionary<string, SyncedItemProgress>(); - } - - var deviceId = options.DeviceId; - if (string.IsNullOrWhiteSpace(deviceId)) - { - return new Dictionary<string, SyncedItemProgress>(); - } - - var caps = _deviceManager().GetCapabilities(deviceId); - if (caps == null || !caps.SupportsSync) - { - return new Dictionary<string, SyncedItemProgress>(); - } - - return _syncManager.GetSyncedItemProgresses(new SyncJobItemQuery - { - TargetId = deviceId, - Statuses = new[] - { - SyncJobItemStatus.Converting, - SyncJobItemStatus.Queued, - SyncJobItemStatus.Transferring, - SyncJobItemStatus.ReadyToTransfer, - SyncJobItemStatus.Synced - } }); } - public void FillSyncInfo(IEnumerable<Tuple<BaseItem, BaseItemDto>> tuples, DtoOptions options, User user) - { - if (options.Fields.Contains(ItemFields.BasicSyncInfo) || - options.Fields.Contains(ItemFields.SyncInfo)) - { - var syncProgress = GetSyncedItemProgress(options); - - foreach (var tuple in tuples) - { - var item = tuple.Item1; - - FillSyncInfo(tuple.Item2, item, options, user, syncProgress); - } - } - } - - private void FillSyncInfo(IHasSyncInfo dto, BaseItem item, DtoOptions options, User user, Dictionary<string, SyncedItemProgress> syncProgress) - { - var hasFullSyncInfo = options.Fields.Contains(ItemFields.SyncInfo); - - if (!hasFullSyncInfo && !options.Fields.Contains(ItemFields.BasicSyncInfo)) - { - return; - } - - if (dto.SupportsSync ?? false) - { - SyncedItemProgress syncStatus; - if (syncProgress.TryGetValue(dto.Id, out syncStatus)) - { - if (syncStatus.Status == SyncJobItemStatus.Synced) - { - dto.SyncPercent = 100; - } - else - { - dto.SyncPercent = syncStatus.Progress; - } - - if (hasFullSyncInfo) - { - dto.HasSyncJob = true; - dto.SyncStatus = syncStatus.Status; - } - } - } - } - private BaseItemDto GetBaseItemDtoInternal(BaseItem item, DtoOptions options, List<Folder> allCollectionFolders, User user = null, BaseItem owner = null) { - var fields = options.Fields; - - if (item == null) - { - throw new ArgumentNullException("item"); - } - - if (fields == null) - { - throw new ArgumentNullException("fields"); - } - var dto = new BaseItemDto { ServerId = _appHost.SystemId @@ -330,12 +211,12 @@ namespace Emby.Server.Implementations.Dto dto.SourceType = item.SourceType.ToString(); } - if (fields.Contains(ItemFields.People)) + if (options.ContainsField(ItemFields.People)) { AttachPeople(dto, item); } - if (fields.Contains(ItemFields.PrimaryImageAspectRatio)) + if (options.ContainsField(ItemFields.PrimaryImageAspectRatio)) { try { @@ -348,7 +229,7 @@ namespace Emby.Server.Implementations.Dto } } - if (fields.Contains(ItemFields.DisplayPreferencesId)) + if (options.ContainsField(ItemFields.DisplayPreferencesId)) { dto.DisplayPreferencesId = item.DisplayPreferencesId.ToString("N"); } @@ -361,74 +242,54 @@ namespace Emby.Server.Implementations.Dto var hasMediaSources = item as IHasMediaSources; if (hasMediaSources != null) { - if (fields.Contains(ItemFields.MediaSources)) + if (options.ContainsField(ItemFields.MediaSources)) { - if (user == null) - { - dto.MediaSources = _mediaSourceManager().GetStaticMediaSources(hasMediaSources, true); - } - else - { - dto.MediaSources = _mediaSourceManager().GetStaticMediaSources(hasMediaSources, true, user); - } + dto.MediaSources = _mediaSourceManager().GetStaticMediaSources(item, true, user).ToArray(); NormalizeMediaSourceContainers(dto); } } - if (fields.Contains(ItemFields.Studios)) + if (options.ContainsField(ItemFields.Studios)) { AttachStudios(dto, item); } AttachBasicFields(dto, item, owner, options); - var collectionFolder = item as ICollectionFolder; - if (collectionFolder != null) - { - dto.CollectionType = collectionFolder.CollectionType; - } - - if (fields.Contains(ItemFields.CanDelete)) + if (options.ContainsField(ItemFields.CanDelete)) { dto.CanDelete = user == null ? item.CanDelete() : item.CanDelete(user); } - if (fields.Contains(ItemFields.CanDownload)) + if (options.ContainsField(ItemFields.CanDownload)) { dto.CanDownload = user == null ? item.CanDownload() : item.CanDownload(user); } - if (fields.Contains(ItemFields.Etag)) + if (options.ContainsField(ItemFields.Etag)) { dto.Etag = item.GetEtag(user); } var liveTvManager = _livetvManager(); - if (item is ILiveTvRecording) - { - liveTvManager.AddInfoToRecordingDto(item, dto, user); - } - else + var activeRecording = liveTvManager.GetActiveRecordingInfo(item.Path); + if (activeRecording != null) { - var activeRecording = liveTvManager.GetActiveRecordingInfo(item.Path); - if (activeRecording != null) - { - dto.Type = "Recording"; - dto.CanDownload = false; - dto.RunTimeTicks = null; + dto.Type = "Recording"; + dto.CanDownload = false; + dto.RunTimeTicks = null; - if (!string.IsNullOrWhiteSpace(dto.SeriesName)) - { - dto.EpisodeTitle = dto.Name; - dto.Name = dto.SeriesName; - } - liveTvManager.AddInfoToRecordingDto(item, dto, activeRecording, user); + if (!string.IsNullOrEmpty(dto.SeriesName)) + { + dto.EpisodeTitle = dto.Name; + dto.Name = dto.SeriesName; } + liveTvManager.AddInfoToRecordingDto(item, dto, activeRecording, user); } return dto; @@ -439,7 +300,7 @@ namespace Emby.Server.Implementations.Dto foreach (var mediaSource in dto.MediaSources) { var container = mediaSource.Container; - if (string.IsNullOrWhiteSpace(container)) + if (string.IsNullOrEmpty(container)) { continue; } @@ -452,17 +313,17 @@ namespace Emby.Server.Implementations.Dto var path = mediaSource.Path; string fileExtensionContainer = null; - if (!string.IsNullOrWhiteSpace(path)) + if (!string.IsNullOrEmpty(path)) { path = Path.GetExtension(path); - if (!string.IsNullOrWhiteSpace(path)) + if (!string.IsNullOrEmpty(path)) { path = Path.GetExtension(path); - if (!string.IsNullOrWhiteSpace(path)) + if (!string.IsNullOrEmpty(path)) { path = path.TrimStart('.'); } - if (!string.IsNullOrWhiteSpace(path) && containers.Contains(path, StringComparer.OrdinalIgnoreCase)) + if (!string.IsNullOrEmpty(path) && containers.Contains(path, StringComparer.OrdinalIgnoreCase)) { fileExtensionContainer = path; } @@ -473,22 +334,20 @@ namespace Emby.Server.Implementations.Dto } } - public BaseItemDto GetItemByNameDto(BaseItem item, DtoOptions options, List<BaseItem> taggedItems, Dictionary<string, SyncedItemProgress> syncProgress, User user = null) + public BaseItemDto GetItemByNameDto(BaseItem item, DtoOptions options, List<BaseItem> taggedItems, User user = null) { var allCollectionFolders = _libraryManager.GetUserRootFolder().Children.OfType<Folder>().ToList(); var dto = GetBaseItemDtoInternal(item, options, allCollectionFolders, user); - if (taggedItems != null && options.Fields.Contains(ItemFields.ItemCounts)) + if (taggedItems != null && options.ContainsField(ItemFields.ItemCounts)) { SetItemByNameInfo(item, dto, taggedItems, user); } - FillSyncInfo(dto, item, options, user, syncProgress); - return dto; } - private void SetItemByNameInfo(BaseItem item, BaseItemDto dto, List<BaseItem> taggedItems, User user = null) + private void SetItemByNameInfo(BaseItem item, BaseItemDto dto, IList<BaseItem> taggedItems, User user = null) { if (item is MusicArtist) { @@ -529,39 +388,37 @@ namespace Emby.Server.Implementations.Dto /// <summary> /// Attaches the user specific info. /// </summary> - private void AttachUserSpecificInfo(BaseItemDto dto, BaseItem item, User user, DtoOptions dtoOptions) + private void AttachUserSpecificInfo(BaseItemDto dto, BaseItem item, User user, DtoOptions options) { - var fields = dtoOptions.Fields; - if (item.IsFolder) { var folder = (Folder)item; - if (dtoOptions.EnableUserData) + if (options.EnableUserData) { - dto.UserData = _userDataRepository.GetUserDataDto(item, dto, user, dtoOptions.Fields); + dto.UserData = _userDataRepository.GetUserDataDto(item, dto, user, options); } if (!dto.ChildCount.HasValue && item.SourceType == SourceType.Library) { // For these types we can try to optimize and assume these values will be equal - if (item is MusicAlbum || item is Season) + if (item is MusicAlbum || item is Season || item is Playlist) { dto.ChildCount = dto.RecursiveItemCount; } - if (dtoOptions.Fields.Contains(ItemFields.ChildCount)) + if (options.ContainsField(ItemFields.ChildCount)) { dto.ChildCount = dto.ChildCount ?? GetChildCount(folder, user); } } - if (fields.Contains(ItemFields.CumulativeRunTimeTicks)) + if (options.ContainsField(ItemFields.CumulativeRunTimeTicks)) { dto.CumulativeRunTimeTicks = item.RunTimeTicks; } - if (fields.Contains(ItemFields.DateLastMediaAdded)) + if (options.ContainsField(ItemFields.DateLastMediaAdded)) { dto.DateLastMediaAdded = folder.DateLastMediaAdded; } @@ -569,21 +426,21 @@ namespace Emby.Server.Implementations.Dto else { - if (dtoOptions.EnableUserData) + if (options.EnableUserData) { dto.UserData = _userDataRepository.GetUserDataDto(item, user); } } - if (/*!(item is LiveTvProgram) ||*/ fields.Contains(ItemFields.PlayAccess)) + if (options.ContainsField(ItemFields.PlayAccess)) { dto.PlayAccess = item.GetPlayAccess(user); } - if (fields.Contains(ItemFields.BasicSyncInfo) || fields.Contains(ItemFields.SyncInfo)) + if (options.ContainsField(ItemFields.BasicSyncInfo)) { var userCanSync = user != null && user.Policy.EnableContentDownloading; - if (userCanSync && _syncManager.SupportsSync(item)) + if (userCanSync && item.SupportsExternalTransfer) { dto.SupportsSync = true; } @@ -610,47 +467,15 @@ namespace Emby.Server.Implementations.Dto /// <exception cref="System.ArgumentNullException">item</exception> public string GetDtoId(BaseItem item) { - if (item == null) - { - throw new ArgumentNullException("item"); - } - return item.Id.ToString("N"); } - /// <summary> - /// Converts a UserItemData to a DTOUserItemData - /// </summary> - /// <param name="data">The data.</param> - /// <returns>DtoUserItemData.</returns> - /// <exception cref="System.ArgumentNullException"></exception> - public UserItemDataDto GetUserItemDataDto(UserItemData data) - { - if (data == null) - { - throw new ArgumentNullException("data"); - } - - return new UserItemDataDto - { - IsFavorite = data.IsFavorite, - Likes = data.Likes, - PlaybackPositionTicks = data.PlaybackPositionTicks, - PlayCount = data.PlayCount, - Rating = data.Rating, - Played = data.Played, - LastPlayedDate = data.LastPlayedDate, - Key = data.Key - }; - } private void SetBookProperties(BaseItemDto dto, Book item) { dto.SeriesName = item.SeriesName; } private void SetPhotoProperties(BaseItemDto dto, Photo item) { - dto.Width = item.Width; - dto.Height = item.Height; dto.CameraMake = item.CameraMake; dto.CameraModel = item.CameraModel; dto.Software = item.Software; @@ -670,7 +495,7 @@ namespace Emby.Server.Implementations.Dto if (album != null) { dto.Album = album.Name; - dto.AlbumId = album.Id.ToString("N"); + dto.AlbumId = album.Id; } } @@ -688,7 +513,7 @@ namespace Emby.Server.Implementations.Dto if (parentAlbumIds.Count > 0) { - dto.AlbumId = parentAlbumIds[0].ToString("N"); + dto.AlbumId = parentAlbumIds[0]; } } @@ -835,11 +660,11 @@ namespace Emby.Server.Implementations.Dto private void AttachStudios(BaseItemDto dto, BaseItem item) { dto.Studios = item.Studios - .Where(i => !string.IsNullOrWhiteSpace(i)) - .Select(i => new NameIdPair + .Where(i => !string.IsNullOrEmpty(i)) + .Select(i => new NameGuidPair { Name = i, - Id = _libraryManager.GetStudioId(i).ToString("N") + Id = _libraryManager.GetStudioId(i) }) .ToArray(); } @@ -847,8 +672,8 @@ namespace Emby.Server.Implementations.Dto private void AttachGenreItems(BaseItemDto dto, BaseItem item) { dto.GenreItems = item.Genres - .Where(i => !string.IsNullOrWhiteSpace(i)) - .Select(i => new NameIdPair + .Where(i => !string.IsNullOrEmpty(i)) + .Select(i => new NameGuidPair { Name = i, Id = GetGenreId(i, item) @@ -856,53 +681,19 @@ namespace Emby.Server.Implementations.Dto .ToArray(); } - private string GetGenreId(string name, BaseItem owner) + private Guid GetGenreId(string name, BaseItem owner) { if (owner is IHasMusicGenres) { - return _libraryManager.GetMusicGenreId(name).ToString("N"); + return _libraryManager.GetMusicGenreId(name); } if (owner is Game || owner is GameSystem) { - return _libraryManager.GetGameGenreId(name).ToString("N"); + return _libraryManager.GetGameGenreId(name); } - return _libraryManager.GetGenreId(name).ToString("N"); - } - - /// <summary> - /// Gets the chapter info dto. - /// </summary> - /// <param name="chapterInfo">The chapter info.</param> - /// <param name="item">The item.</param> - /// <returns>ChapterInfoDto.</returns> - private ChapterInfoDto GetChapterInfoDto(ChapterInfo chapterInfo, BaseItem item) - { - var dto = new ChapterInfoDto - { - Name = chapterInfo.Name, - StartPositionTicks = chapterInfo.StartPositionTicks - }; - - if (!string.IsNullOrEmpty(chapterInfo.ImagePath)) - { - dto.ImageTag = GetImageCacheTag(item, new ItemImageInfo - { - Path = chapterInfo.ImagePath, - Type = ImageType.Chapter, - DateModified = chapterInfo.ImageDateModified - }); - } - - return dto; - } - - public List<ChapterInfoDto> GetChapterInfoDtos(BaseItem item) - { - return _itemRepo.GetChapters(item.Id) - .Select(c => GetChapterInfoDto(c, item)) - .ToList(); + return _libraryManager.GetGenreId(name); } /// <summary> @@ -914,14 +705,12 @@ namespace Emby.Server.Implementations.Dto /// <param name="options">The options.</param> private void AttachBasicFields(BaseItemDto dto, BaseItem item, BaseItem owner, DtoOptions options) { - var fields = options.Fields; - - if (fields.Contains(ItemFields.DateCreated)) + if (options.ContainsField(ItemFields.DateCreated)) { dto.DateCreated = item.DateCreated; } - if (fields.Contains(ItemFields.Settings)) + if (options.ContainsField(ItemFields.Settings)) { dto.LockedFields = item.LockedFields; dto.LockData = item.IsLocked; @@ -931,17 +720,12 @@ namespace Emby.Server.Implementations.Dto dto.EndDate = item.EndDate; - if (fields.Contains(ItemFields.HomePageUrl)) - { - dto.HomePageUrl = item.HomePageUrl; - } - - if (fields.Contains(ItemFields.ExternalUrls)) + if (options.ContainsField(ItemFields.ExternalUrls)) { dto.ExternalUrls = _providerManager.GetExternalUrls(item).ToArray(); } - if (fields.Contains(ItemFields.Tags)) + if (options.ContainsField(ItemFields.Tags)) { dto.Tags = item.Tags; } @@ -958,7 +742,7 @@ namespace Emby.Server.Implementations.Dto dto.BackdropImageTags = GetImageTags(item, item.GetImages(ImageType.Backdrop).Take(backdropLimit).ToList()); } - if (fields.Contains(ItemFields.ScreenshotImageTags)) + if (options.ContainsField(ItemFields.ScreenshotImageTags)) { var screenshotLimit = options.GetImageLimit(ImageType.Screenshot); if (screenshotLimit > 0) @@ -967,7 +751,7 @@ namespace Emby.Server.Implementations.Dto } } - if (fields.Contains(ItemFields.Genres)) + if (options.ContainsField(ItemFields.Genres)) { dto.Genres = item.Genres; AttachGenreItems(dto, item); @@ -994,7 +778,7 @@ namespace Emby.Server.Implementations.Dto } } - dto.Id = GetDtoId(item); + dto.Id = item.Id; dto.IndexNumber = item.IndexNumber; dto.ParentIndexNumber = item.ParentIndexNumber; @@ -1014,13 +798,9 @@ namespace Emby.Server.Implementations.Dto dto.LocationType = item.LocationType; } - if (item.IsHD.HasValue && item.IsHD.Value) - { - dto.IsHD = item.IsHD; - } dto.Audio = item.Audio; - if (fields.Contains(ItemFields.Settings)) + if (options.ContainsField(ItemFields.Settings)) { dto.PreferredMetadataCountryCode = item.PreferredMetadataCountryCode; dto.PreferredMetadataLanguage = item.PreferredMetadataLanguage; @@ -1028,90 +808,83 @@ namespace Emby.Server.Implementations.Dto dto.CriticRating = item.CriticRating; - var hasTrailers = item as IHasTrailers; - if (hasTrailers != null) - { - dto.LocalTrailerCount = hasTrailers.GetTrailerIds().Count; - } - var hasDisplayOrder = item as IHasDisplayOrder; if (hasDisplayOrder != null) { dto.DisplayOrder = hasDisplayOrder.DisplayOrder; } - var userView = item as UserView; - if (userView != null) + var hasCollectionType = item as IHasCollectionType; + if (hasCollectionType != null) { - dto.CollectionType = userView.ViewType; + dto.CollectionType = hasCollectionType.CollectionType; } - if (fields.Contains(ItemFields.RemoteTrailers)) + if (options.ContainsField(ItemFields.RemoteTrailers)) { - dto.RemoteTrailers = hasTrailers != null ? - hasTrailers.RemoteTrailers : - new MediaUrl[] { }; + dto.RemoteTrailers = item.RemoteTrailers; } dto.Name = item.Name; dto.OfficialRating = item.OfficialRating; - if (fields.Contains(ItemFields.Overview)) + if (options.ContainsField(ItemFields.Overview)) { dto.Overview = item.Overview; } - if (fields.Contains(ItemFields.OriginalTitle)) + if (options.ContainsField(ItemFields.OriginalTitle)) { dto.OriginalTitle = item.OriginalTitle; } - if (fields.Contains(ItemFields.ParentId)) + if (options.ContainsField(ItemFields.ParentId)) { - var displayParentId = item.DisplayParentId; - if (displayParentId.HasValue) - { - dto.ParentId = displayParentId.Value.ToString("N"); - } + dto.ParentId = item.DisplayParentId; } AddInheritedImages(dto, item, options, owner); - if (fields.Contains(ItemFields.Path)) + if (options.ContainsField(ItemFields.Path)) { dto.Path = GetMappedPath(item, owner); } + if (options.ContainsField(ItemFields.EnableMediaSourceDisplay)) + { + dto.EnableMediaSourceDisplay = item.EnableMediaSourceDisplay; + } + dto.PremiereDate = item.PremiereDate; dto.ProductionYear = item.ProductionYear; - if (fields.Contains(ItemFields.ProviderIds)) + if (options.ContainsField(ItemFields.ProviderIds)) { dto.ProviderIds = item.ProviderIds; } dto.RunTimeTicks = item.RunTimeTicks; - if (fields.Contains(ItemFields.SortName)) + if (options.ContainsField(ItemFields.SortName)) { dto.SortName = item.SortName; } - if (fields.Contains(ItemFields.CustomRating)) + if (options.ContainsField(ItemFields.CustomRating)) { dto.CustomRating = item.CustomRating; } - if (fields.Contains(ItemFields.Taglines)) + if (options.ContainsField(ItemFields.Taglines)) { - if (!string.IsNullOrWhiteSpace(item.Tagline)) + if (!string.IsNullOrEmpty(item.Tagline)) { dto.Taglines = new string[] { item.Tagline }; } if (dto.Taglines == null) { - dto.Taglines = new string[] { }; + dto.Taglines = Array.Empty<string>(); } } @@ -1141,12 +914,12 @@ namespace Emby.Server.Implementations.Dto if (albumParent != null) { - dto.AlbumId = GetDtoId(albumParent); + dto.AlbumId = albumParent.Id; dto.AlbumPrimaryImageTag = GetImageCacheTag(albumParent, ImageType.Primary); } - //if (fields.Contains(ItemFields.MediaSourceCount)) + //if (options.ContainsField(ItemFields.MediaSourceCount)) //{ // Songs always have one //} @@ -1182,7 +955,7 @@ namespace Emby.Server.Implementations.Dto .Select(i => { // This should not be necessary but we're seeing some cases of it - if (string.IsNullOrWhiteSpace(i)) + if (string.IsNullOrEmpty(i)) { return null; } @@ -1193,10 +966,10 @@ namespace Emby.Server.Implementations.Dto }); if (artist != null) { - return new NameIdPair + return new NameGuidPair { Name = artist.Name, - Id = artist.Id.ToString("N") + Id = artist.Id }; } @@ -1233,7 +1006,7 @@ namespace Emby.Server.Implementations.Dto .Select(i => { // This should not be necessary but we're seeing some cases of it - if (string.IsNullOrWhiteSpace(i)) + if (string.IsNullOrEmpty(i)) { return null; } @@ -1244,10 +1017,10 @@ namespace Emby.Server.Implementations.Dto }); if (artist != null) { - return new NameIdPair + return new NameGuidPair { Name = artist.Name, - Id = artist.Id.ToString("N") + Id = artist.Id }; } @@ -1274,7 +1047,7 @@ namespace Emby.Server.Implementations.Dto dto.PartCount = video.AdditionalParts.Length + 1; } - if (fields.Contains(ItemFields.MediaSourceCount)) + if (options.ContainsField(ItemFields.MediaSourceCount)) { var mediaSourceCount = video.MediaSourceCount; if (mediaSourceCount != 1) @@ -1283,9 +1056,9 @@ namespace Emby.Server.Implementations.Dto } } - if (fields.Contains(ItemFields.Chapters)) + if (options.ContainsField(ItemFields.Chapters)) { - dto.Chapters = GetChapterInfoDtos(item); + dto.Chapters = _itemRepo.GetChapters(item); } if (video.ExtraType.HasValue) @@ -1294,7 +1067,7 @@ namespace Emby.Server.Implementations.Dto } } - if (fields.Contains(ItemFields.MediaStreams)) + if (options.ContainsField(ItemFields.MediaStreams)) { // Add VideoInfo var iHasMediaSources = item as IHasMediaSources; @@ -1303,30 +1076,48 @@ namespace Emby.Server.Implementations.Dto { MediaStream[] mediaStreams; - if (dto.MediaSources != null && dto.MediaSources.Count > 0) + if (dto.MediaSources != null && dto.MediaSources.Length > 0) { - mediaStreams = dto.MediaSources.Where(i => new Guid(i.Id) == item.Id) - .SelectMany(i => i.MediaStreams) - .ToArray(); + if (item.SourceType == SourceType.Channel) + { + mediaStreams = dto.MediaSources[0].MediaStreams.ToArray(); + } + else + { + mediaStreams = dto.MediaSources.Where(i => string.Equals(i.Id, item.Id.ToString("N"), StringComparison.OrdinalIgnoreCase)) + .SelectMany(i => i.MediaStreams) + .ToArray(); + } } else { - mediaStreams = _mediaSourceManager().GetStaticMediaSources(iHasMediaSources, true).First().MediaStreams.ToArray(); + mediaStreams = _mediaSourceManager().GetStaticMediaSources(item, true).First().MediaStreams.ToArray(); } dto.MediaStreams = mediaStreams; } } - var hasSpecialFeatures = item as IHasSpecialFeatures; - if (hasSpecialFeatures != null) + BaseItem[] allExtras = null; + + if (options.ContainsField(ItemFields.SpecialFeatureCount)) { - var specialFeatureCount = hasSpecialFeatures.SpecialFeatureIds.Length; + if (allExtras == null) + { + allExtras = item.GetExtras().ToArray(); + } - if (specialFeatureCount > 0) + dto.SpecialFeatureCount = allExtras.Count(i => i.ExtraType.HasValue && BaseItem.DisplayExtraTypes.Contains(i.ExtraType.Value)); + } + + if (options.ContainsField(ItemFields.LocalTrailerCount)) + { + if (allExtras == null) { - dto.SpecialFeatureCount = specialFeatureCount; + allExtras = item.GetExtras().ToArray(); } + + dto.LocalTrailerCount = allExtras.Count(i => i.ExtraType.HasValue && i.ExtraType.Value == ExtraType.Trailer); } // Add EpisodeInfo @@ -1336,37 +1127,20 @@ namespace Emby.Server.Implementations.Dto dto.IndexNumberEnd = episode.IndexNumberEnd; dto.SeriesName = episode.SeriesName; - if (fields.Contains(ItemFields.AlternateEpisodeNumbers)) - { - dto.DvdSeasonNumber = episode.DvdSeasonNumber; - dto.DvdEpisodeNumber = episode.DvdEpisodeNumber; - dto.AbsoluteEpisodeNumber = episode.AbsoluteEpisodeNumber; - } - - if (fields.Contains(ItemFields.SpecialEpisodeNumbers)) + if (options.ContainsField(ItemFields.SpecialEpisodeNumbers)) { dto.AirsAfterSeasonNumber = episode.AirsAfterSeasonNumber; dto.AirsBeforeEpisodeNumber = episode.AirsBeforeEpisodeNumber; dto.AirsBeforeSeasonNumber = episode.AirsBeforeSeasonNumber; } - var seasonId = episode.SeasonId; - if (seasonId.HasValue) - { - dto.SeasonId = seasonId.Value.ToString("N"); - } - dto.SeasonName = episode.SeasonName; - - var seriesId = episode.SeriesId; - if (seriesId.HasValue) - { - dto.SeriesId = seriesId.Value.ToString("N"); - } + dto.SeasonId = episode.SeasonId; + dto.SeriesId = episode.SeriesId; Series episodeSeries = null; - //if (fields.Contains(ItemFields.SeriesPrimaryImage)) + //if (options.ContainsField(ItemFields.SeriesPrimaryImage)) { episodeSeries = episodeSeries ?? episode.Series; if (episodeSeries != null) @@ -1375,7 +1149,7 @@ namespace Emby.Server.Implementations.Dto } } - if (fields.Contains(ItemFields.SeriesStudio)) + if (options.ContainsField(ItemFields.SeriesStudio)) { episodeSeries = episodeSeries ?? episode.Series; if (episodeSeries != null) @@ -1399,16 +1173,11 @@ namespace Emby.Server.Implementations.Dto if (season != null) { dto.SeriesName = season.SeriesName; - - var seriesId = season.SeriesId; - if (seriesId.HasValue) - { - dto.SeriesId = seriesId.Value.ToString("N"); - } + dto.SeriesId = season.SeriesId; series = null; - if (fields.Contains(ItemFields.SeriesStudio)) + if (options.ContainsField(ItemFields.SeriesStudio)) { series = series ?? season.Series; if (series != null) @@ -1417,7 +1186,7 @@ namespace Emby.Server.Implementations.Dto } } - //if (fields.Contains(ItemFields.SeriesPrimaryImage)) + //if (options.ContainsField(ItemFields.SeriesPrimaryImage)) { series = series ?? season.Series; if (series != null) @@ -1453,7 +1222,7 @@ namespace Emby.Server.Implementations.Dto SetBookProperties(dto, book); } - if (fields.Contains(ItemFields.ProductionLocations)) + if (options.ContainsField(ItemFields.ProductionLocations)) { if (item.ProductionLocations.Length > 0 || item is Movie) { @@ -1461,6 +1230,33 @@ namespace Emby.Server.Implementations.Dto } } + if (options.ContainsField(ItemFields.Width)) + { + var width = item.Width; + if (width > 0) + { + dto.Width = width; + } + } + + if (options.ContainsField(ItemFields.Height)) + { + var height = item.Height; + if (height > 0) + { + dto.Height = height; + } + } + + if (options.ContainsField(ItemFields.IsHD)) + { + // Compatibility + if (item.IsHD) + { + dto.IsHD = true; + } + } + var photo = item as Photo; if (photo != null) { @@ -1469,7 +1265,7 @@ namespace Emby.Server.Implementations.Dto dto.ChannelId = item.ChannelId; - if (item.SourceType == SourceType.Channel && !string.IsNullOrWhiteSpace(item.ChannelId)) + if (item.SourceType == SourceType.Channel) { var channel = _libraryManager.GetItemById(item.ChannelId); if (channel != null) @@ -1491,7 +1287,7 @@ namespace Emby.Server.Implementations.Dto } } - var parent = currentItem.DisplayParent ?? (currentItem.IsOwnedItem ? currentItem.GetOwner() : currentItem.GetParent()); + var parent = currentItem.DisplayParent ?? currentItem.GetOwner() ?? currentItem.GetParent(); if (parent == null && !(originalItem is UserRootFolder) && !(originalItem is UserView) && !(originalItem is AggregateFolder) && !(originalItem is ICollectionFolder) && !(originalItem is Channel)) { @@ -1592,9 +1388,7 @@ namespace Emby.Server.Implementations.Dto { var path = item.Path; - var locationType = item.LocationType; - - if (locationType == LocationType.FileSystem || locationType == LocationType.Offline) + if (item.IsFileProtocol) { path = _libraryManager.GetPathAfterNetworkSubstitution(path, ownerItem ?? item); } @@ -1628,15 +1422,15 @@ namespace Emby.Server.Implementations.Dto var defaultAspectRatio = item.GetDefaultPrimaryImageAspectRatio(); - if (defaultAspectRatio.HasValue) + if (defaultAspectRatio > 0) { - if (supportedEnhancers.Count == 0) + if (supportedEnhancers.Length == 0) { - return defaultAspectRatio.Value; + return defaultAspectRatio; } double dummyWidth = 200; - double dummyHeight = dummyWidth / defaultAspectRatio.Value; + double dummyHeight = dummyWidth / defaultAspectRatio; size = new ImageSize(dummyWidth, dummyHeight); } else diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj index cef37910e..66dd80dbe 100644 --- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj +++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj @@ -1,679 +1,44 @@ -<?xml version="1.0" encoding="utf-8"?> -<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> - <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" /> +<Project Sdk="Microsoft.NET.Sdk"> + + <ItemGroup> + <ProjectReference Include="..\Emby.Naming\Emby.Naming.csproj" /> + <ProjectReference Include="..\Emby.Notifications\Emby.Notifications.csproj" /> + <ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" /> + <ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj" /> + <ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" /> + <ProjectReference Include="..\MediaBrowser.Providers\MediaBrowser.Providers.csproj" /> + <ProjectReference Include="..\MediaBrowser.WebDashboard\MediaBrowser.WebDashboard.csproj" /> + <ProjectReference Include="..\MediaBrowser.XbmcMetadata\MediaBrowser.XbmcMetadata.csproj" /> + <ProjectReference Include="..\SocketHttpListener\SocketHttpListener.csproj" /> + <ProjectReference Include="..\Emby.Dlna\Emby.Dlna.csproj" /> + <ProjectReference Include="..\Mono.Nat\Mono.Nat.csproj" /> + <ProjectReference Include="..\MediaBrowser.Api\MediaBrowser.Api.csproj" /> + <ProjectReference Include="..\MediaBrowser.LocalMetadata\MediaBrowser.LocalMetadata.csproj" /> + <ProjectReference Include="..\OpenSubtitlesHandler\OpenSubtitlesHandler.csproj" /> + <ProjectReference Include="..\Emby.Photos\Emby.Photos.csproj" /> + <ProjectReference Include="..\Emby.Drawing\Emby.Drawing.csproj" /> + </ItemGroup> + + <ItemGroup> + <PackageReference Include="Emby.XmlTv" Version="1.0.18" /> + <PackageReference Include="ServiceStack.Text.Core" Version="5.2.0" /> + <PackageReference Include="sharpcompress" Version="0.22.0" /> + <PackageReference Include="SimpleInjector" Version="4.3.0" /> + <PackageReference Include="SQLitePCL.pretty.core" Version="1.1.8" /> + <PackageReference Include="SQLitePCLRaw.core" Version="1.1.11" /> + </ItemGroup> + + <ItemGroup> + <Compile Include="..\SharedVersion.cs" /> + </ItemGroup> + <PropertyGroup> - <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration> - <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform> - <ProjectGuid>{E383961B-9356-4D5D-8233-9A1079D03055}</ProjectGuid> - <OutputType>Library</OutputType> - <AppDesignerFolder>Properties</AppDesignerFolder> - <RootNamespace>Emby.Server.Implementations</RootNamespace> - <AssemblyName>Emby.Server.Implementations</AssemblyName> - <FileAlignment>512</FileAlignment> - <TargetFrameworkProfile /> - <TargetFrameworkVersion>v4.6</TargetFrameworkVersion> + <TargetFramework>netcoreapp2.1</TargetFramework> + <GenerateAssemblyInfo>false</GenerateAssemblyInfo> </PropertyGroup> - <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "> - <DebugSymbols>true</DebugSymbols> - <DebugType>full</DebugType> - <Optimize>false</Optimize> - <OutputPath>bin\Debug\</OutputPath> - <DefineConstants>DEBUG;TRACE</DefineConstants> - <ErrorReport>prompt</ErrorReport> - <WarningLevel>4</WarningLevel> - <AllowUnsafeBlocks>true</AllowUnsafeBlocks> - </PropertyGroup> - <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "> - <DebugType>pdbonly</DebugType> - <Optimize>true</Optimize> - <OutputPath>bin\Release\</OutputPath> - <DefineConstants>TRACE</DefineConstants> - <ErrorReport>prompt</ErrorReport> - <WarningLevel>4</WarningLevel> - <AllowUnsafeBlocks>true</AllowUnsafeBlocks> - </PropertyGroup> - <ItemGroup> - <Compile Include="..\SharedVersion.cs"> - <Link>Properties\SharedVersion.cs</Link> - </Compile> - <Compile Include="Activity\ActivityLogEntryPoint.cs" /> - <Compile Include="Activity\ActivityManager.cs" /> - <Compile Include="Activity\ActivityRepository.cs" /> - <Compile Include="AppBase\BaseApplicationPaths.cs" /> - <Compile Include="AppBase\BaseConfigurationManager.cs" /> - <Compile Include="AppBase\ConfigurationHelper.cs" /> - <Compile Include="ApplicationHost.cs" /> - <Compile Include="Archiving\ZipClient.cs" /> - <Compile Include="Branding\BrandingConfigurationFactory.cs" /> - <Compile Include="Browser\BrowserLauncher.cs" /> - <Compile Include="Channels\ChannelConfigurations.cs" /> - <Compile Include="Channels\ChannelDynamicMediaSourceProvider.cs" /> - <Compile Include="Channels\ChannelImageProvider.cs" /> - <Compile Include="Channels\ChannelManager.cs" /> - <Compile Include="Channels\ChannelPostScanTask.cs" /> - <Compile Include="Channels\RefreshChannelsScheduledTask.cs" /> - <Compile Include="Collections\CollectionImageProvider.cs" /> - <Compile Include="Collections\CollectionManager.cs" /> - <Compile Include="Collections\CollectionsDynamicFolder.cs" /> - <Compile Include="Configuration\ServerConfigurationManager.cs" /> - <Compile Include="Cryptography\CryptographyProvider.cs" /> - <Compile Include="Data\ManagedConnection.cs" /> - <Compile Include="Data\SqliteDisplayPreferencesRepository.cs" /> - <Compile Include="Data\SqliteItemRepository.cs" /> - <Compile Include="Data\SqliteUserDataRepository.cs" /> - <Compile Include="Data\SqliteUserRepository.cs" /> - <Compile Include="Data\TypeMapper.cs" /> - <Compile Include="Devices\CameraUploadsDynamicFolder.cs" /> - <Compile Include="Devices\CameraUploadsFolder.cs" /> - <Compile Include="Devices\DeviceId.cs" /> - <Compile Include="Devices\DeviceManager.cs" /> - <Compile Include="Devices\SqliteDeviceRepository.cs" /> - <Compile Include="Diagnostics\CommonProcess.cs" /> - <Compile Include="Diagnostics\ProcessFactory.cs" /> - <Compile Include="Dto\DtoService.cs" /> - <Compile Include="EntryPoints\AutomaticRestartEntryPoint.cs" /> - <Compile Include="EntryPoints\ExternalPortForwarding.cs" /> - <Compile Include="EntryPoints\KeepServerAwake.cs" /> - <Compile Include="EntryPoints\LibraryChangedNotifier.cs" /> - <Compile Include="EntryPoints\LoadRegistrations.cs" /> - <Compile Include="EntryPoints\RecordingNotifier.cs" /> - <Compile Include="EntryPoints\RefreshUsersMetadata.cs" /> - <Compile Include="EntryPoints\ServerEventNotifier.cs" /> - <Compile Include="EntryPoints\StartupWizard.cs" /> - <Compile Include="EntryPoints\SystemEvents.cs" /> - <Compile Include="EntryPoints\UdpServerEntryPoint.cs" /> - <Compile Include="EntryPoints\UsageEntryPoint.cs" /> - <Compile Include="EntryPoints\UsageReporter.cs" /> - <Compile Include="EntryPoints\UserDataChangeNotifier.cs" /> - <Compile Include="EnvironmentInfo\EnvironmentInfo.cs" /> - <Compile Include="FFMpeg\FFMpegInfo.cs" /> - <Compile Include="FFMpeg\FFMpegInstallInfo.cs" /> - <Compile Include="FFMpeg\FFMpegLoader.cs" /> - <Compile Include="HttpClientManager\HttpClientInfo.cs" /> - <Compile Include="HttpClientManager\HttpClientManager.cs" /> - <Compile Include="HttpServerFactory.cs" /> - <Compile Include="HttpServer\FileWriter.cs" /> - <Compile Include="HttpServer\HttpListenerHost.cs" /> - <Compile Include="HttpServer\HttpResultFactory.cs" /> - <Compile Include="HttpServer\LoggerUtils.cs" /> - <Compile Include="HttpServer\RangeRequestWriter.cs" /> - <Compile Include="HttpServer\ResponseFilter.cs" /> - <Compile Include="HttpServer\SocketSharp\Extensions.cs" /> - <Compile Include="HttpServer\SocketSharp\HttpUtility.cs" /> - <Compile Include="HttpServer\IHttpListener.cs" /> - <Compile Include="HttpServer\Security\AuthorizationContext.cs" /> - <Compile Include="HttpServer\Security\AuthService.cs" /> - <Compile Include="HttpServer\Security\SessionContext.cs" /> - <Compile Include="HttpServer\SocketSharp\RequestMono.cs" /> - <Compile Include="HttpServer\SocketSharp\SharpWebSocket.cs" /> - <Compile Include="HttpServer\SocketSharp\WebSocketSharpListener.cs" /> - <Compile Include="HttpServer\SocketSharp\WebSocketSharpRequest.cs" /> - <Compile Include="HttpServer\SocketSharp\WebSocketSharpResponse.cs" /> - <Compile Include="HttpServer\StreamWriter.cs" /> - <Compile Include="Images\BaseDynamicImageProvider.cs" /> - <Compile Include="IO\FileRefresher.cs" /> - <Compile Include="IO\IsoManager.cs" /> - <Compile Include="IO\LibraryMonitor.cs" /> - <Compile Include="IO\ManagedFileSystem.cs" /> - <Compile Include="IO\MbLinkShortcutHandler.cs" /> - <Compile Include="IO\MemoryStreamProvider.cs" /> - <Compile Include="IO\SharpCifsFileSystem.cs" /> - <Compile Include="IO\SharpCifs\Config.cs" /> - <Compile Include="IO\SharpCifs\Dcerpc\DcerpcBind.cs" /> - <Compile Include="IO\SharpCifs\Dcerpc\DcerpcBinding.cs" /> - <Compile Include="IO\SharpCifs\Dcerpc\DcerpcConstants.cs" /> - <Compile Include="IO\SharpCifs\Dcerpc\DcerpcError.cs" /> - <Compile Include="IO\SharpCifs\Dcerpc\DcerpcException.cs" /> - <Compile Include="IO\SharpCifs\Dcerpc\DcerpcHandle.cs" /> - <Compile Include="IO\SharpCifs\Dcerpc\DcerpcMessage.cs" /> - <Compile Include="IO\SharpCifs\Dcerpc\DcerpcPipeHandle.cs" /> - <Compile Include="IO\SharpCifs\Dcerpc\DcerpcSecurityProvider.cs" /> - <Compile Include="IO\SharpCifs\Dcerpc\Msrpc\LsaPolicyHandle.cs" /> - <Compile Include="IO\SharpCifs\Dcerpc\Msrpc\Lsarpc.cs" /> - <Compile Include="IO\SharpCifs\Dcerpc\Msrpc\LsarSidArrayX.cs" /> - <Compile Include="IO\SharpCifs\Dcerpc\Msrpc\MsrpcDfsRootEnum.cs" /> - <Compile Include="IO\SharpCifs\Dcerpc\Msrpc\MsrpcEnumerateAliasesInDomain.cs" /> - <Compile Include="IO\SharpCifs\Dcerpc\Msrpc\MsrpcGetMembersInAlias.cs" /> - <Compile Include="IO\SharpCifs\Dcerpc\Msrpc\MsrpcLookupSids.cs" /> - <Compile Include="IO\SharpCifs\Dcerpc\Msrpc\MsrpcLsarOpenPolicy2.cs" /> - <Compile Include="IO\SharpCifs\Dcerpc\Msrpc\MsrpcQueryInformationPolicy.cs" /> - <Compile Include="IO\SharpCifs\Dcerpc\Msrpc\MsrpcSamrConnect2.cs" /> - <Compile Include="IO\SharpCifs\Dcerpc\Msrpc\MsrpcSamrConnect4.cs" /> - <Compile Include="IO\SharpCifs\Dcerpc\Msrpc\MsrpcSamrOpenAlias.cs" /> - <Compile Include="IO\SharpCifs\Dcerpc\Msrpc\MsrpcSamrOpenDomain.cs" /> - <Compile Include="IO\SharpCifs\Dcerpc\Msrpc\MsrpcShareEnum.cs" /> - <Compile Include="IO\SharpCifs\Dcerpc\Msrpc\MsrpcShareGetInfo.cs" /> - <Compile Include="IO\SharpCifs\Dcerpc\Msrpc\Netdfs.cs" /> - <Compile Include="IO\SharpCifs\Dcerpc\Msrpc\Samr.cs" /> - <Compile Include="IO\SharpCifs\Dcerpc\Msrpc\SamrAliasHandle.cs" /> - <Compile Include="IO\SharpCifs\Dcerpc\Msrpc\SamrDomainHandle.cs" /> - <Compile Include="IO\SharpCifs\Dcerpc\Msrpc\SamrPolicyHandle.cs" /> - <Compile Include="IO\SharpCifs\Dcerpc\Msrpc\Srvsvc.cs" /> - <Compile Include="IO\SharpCifs\Dcerpc\Ndr\NdrBuffer.cs" /> - <Compile Include="IO\SharpCifs\Dcerpc\Ndr\NdrException.cs" /> - <Compile Include="IO\SharpCifs\Dcerpc\Ndr\NdrHyper.cs" /> - <Compile Include="IO\SharpCifs\Dcerpc\Ndr\NdrLong.cs" /> - <Compile Include="IO\SharpCifs\Dcerpc\Ndr\NdrObject.cs" /> - <Compile Include="IO\SharpCifs\Dcerpc\Ndr\NdrShort.cs" /> - <Compile Include="IO\SharpCifs\Dcerpc\Ndr\NdrSmall.cs" /> - <Compile Include="IO\SharpCifs\Dcerpc\Rpc.cs" /> - <Compile Include="IO\SharpCifs\Dcerpc\UnicodeString.cs" /> - <Compile Include="IO\SharpCifs\Dcerpc\UUID.cs" /> - <Compile Include="IO\SharpCifs\Netbios\Lmhosts.cs" /> - <Compile Include="IO\SharpCifs\Netbios\Name.cs" /> - <Compile Include="IO\SharpCifs\Netbios\NameQueryRequest.cs" /> - <Compile Include="IO\SharpCifs\Netbios\NameQueryResponse.cs" /> - <Compile Include="IO\SharpCifs\Netbios\NameServiceClient.cs" /> - <Compile Include="IO\SharpCifs\Netbios\NameServicePacket.cs" /> - <Compile Include="IO\SharpCifs\Netbios\NbtAddress.cs" /> - <Compile Include="IO\SharpCifs\Netbios\NbtException.cs" /> - <Compile Include="IO\SharpCifs\Netbios\NodeStatusRequest.cs" /> - <Compile Include="IO\SharpCifs\Netbios\NodeStatusResponse.cs" /> - <Compile Include="IO\SharpCifs\Netbios\SessionRequestPacket.cs" /> - <Compile Include="IO\SharpCifs\Netbios\SessionRetargetResponsePacket.cs" /> - <Compile Include="IO\SharpCifs\Netbios\SessionServicePacket.cs" /> - <Compile Include="IO\SharpCifs\Ntlmssp\NtlmFlags.cs" /> - <Compile Include="IO\SharpCifs\Ntlmssp\NtlmMessage.cs" /> - <Compile Include="IO\SharpCifs\Ntlmssp\Type1Message.cs" /> - <Compile Include="IO\SharpCifs\Ntlmssp\Type2Message.cs" /> - <Compile Include="IO\SharpCifs\Ntlmssp\Type3Message.cs" /> - <Compile Include="IO\SharpCifs\Smb\ACE.cs" /> - <Compile Include="IO\SharpCifs\Smb\AllocInfo.cs" /> - <Compile Include="IO\SharpCifs\Smb\AndXServerMessageBlock.cs" /> - <Compile Include="IO\SharpCifs\Smb\BufferCache.cs" /> - <Compile Include="IO\SharpCifs\Smb\Dfs.cs" /> - <Compile Include="IO\SharpCifs\Smb\DfsReferral.cs" /> - <Compile Include="IO\SharpCifs\Smb\DosError.cs" /> - <Compile Include="IO\SharpCifs\Smb\DosFileFilter.cs" /> - <Compile Include="IO\SharpCifs\Smb\FileEntry.cs" /> - <Compile Include="IO\SharpCifs\Smb\IInfo.cs" /> - <Compile Include="IO\SharpCifs\Smb\NetServerEnum2.cs" /> - <Compile Include="IO\SharpCifs\Smb\NetServerEnum2Response.cs" /> - <Compile Include="IO\SharpCifs\Smb\NetShareEnum.cs" /> - <Compile Include="IO\SharpCifs\Smb\NetShareEnumResponse.cs" /> - <Compile Include="IO\SharpCifs\Smb\NtlmAuthenticator.cs" /> - <Compile Include="IO\SharpCifs\Smb\NtlmChallenge.cs" /> - <Compile Include="IO\SharpCifs\Smb\NtlmContext.cs" /> - <Compile Include="IO\SharpCifs\Smb\NtlmPasswordAuthentication.cs" /> - <Compile Include="IO\SharpCifs\Smb\NtStatus.cs" /> - <Compile Include="IO\SharpCifs\Smb\NtTransQuerySecurityDesc.cs" /> - <Compile Include="IO\SharpCifs\Smb\NtTransQuerySecurityDescResponse.cs" /> - <Compile Include="IO\SharpCifs\Smb\Principal.cs" /> - <Compile Include="IO\SharpCifs\Smb\SecurityDescriptor.cs" /> - <Compile Include="IO\SharpCifs\Smb\ServerMessageBlock.cs" /> - <Compile Include="IO\SharpCifs\Smb\SID.cs" /> - <Compile Include="IO\SharpCifs\Smb\SigningDigest.cs" /> - <Compile Include="IO\SharpCifs\Smb\SmbAuthException.cs" /> - <Compile Include="IO\SharpCifs\Smb\SmbComBlankResponse.cs" /> - <Compile Include="IO\SharpCifs\Smb\SmbComClose.cs" /> - <Compile Include="IO\SharpCifs\Smb\SmbComCreateDirectory.cs" /> - <Compile Include="IO\SharpCifs\Smb\SmbComDelete.cs" /> - <Compile Include="IO\SharpCifs\Smb\SmbComDeleteDirectory.cs" /> - <Compile Include="IO\SharpCifs\Smb\SmbComFindClose2.cs" /> - <Compile Include="IO\SharpCifs\Smb\SmbComLogoffAndX.cs" /> - <Compile Include="IO\SharpCifs\Smb\SmbComNegotiate.cs" /> - <Compile Include="IO\SharpCifs\Smb\SmbComNegotiateResponse.cs" /> - <Compile Include="IO\SharpCifs\Smb\SmbComNTCreateAndX.cs" /> - <Compile Include="IO\SharpCifs\Smb\SmbComNTCreateAndXResponse.cs" /> - <Compile Include="IO\SharpCifs\Smb\SmbComNtTransaction.cs" /> - <Compile Include="IO\SharpCifs\Smb\SmbComNtTransactionResponse.cs" /> - <Compile Include="IO\SharpCifs\Smb\SmbComOpenAndX.cs" /> - <Compile Include="IO\SharpCifs\Smb\SmbComOpenAndXResponse.cs" /> - <Compile Include="IO\SharpCifs\Smb\SmbComQueryInformation.cs" /> - <Compile Include="IO\SharpCifs\Smb\SmbComQueryInformationResponse.cs" /> - <Compile Include="IO\SharpCifs\Smb\SmbComReadAndX.cs" /> - <Compile Include="IO\SharpCifs\Smb\SmbComReadAndXResponse.cs" /> - <Compile Include="IO\SharpCifs\Smb\SmbComRename.cs" /> - <Compile Include="IO\SharpCifs\Smb\SmbComSessionSetupAndX.cs" /> - <Compile Include="IO\SharpCifs\Smb\SmbComSessionSetupAndXResponse.cs" /> - <Compile Include="IO\SharpCifs\Smb\SmbComTransaction.cs" /> - <Compile Include="IO\SharpCifs\Smb\SmbComTransactionResponse.cs" /> - <Compile Include="IO\SharpCifs\Smb\SmbComTreeConnectAndX.cs" /> - <Compile Include="IO\SharpCifs\Smb\SmbComTreeConnectAndXResponse.cs" /> - <Compile Include="IO\SharpCifs\Smb\SmbComTreeDisconnect.cs" /> - <Compile Include="IO\SharpCifs\Smb\SmbComWrite.cs" /> - <Compile Include="IO\SharpCifs\Smb\SmbComWriteAndX.cs" /> - <Compile Include="IO\SharpCifs\Smb\SmbComWriteAndXResponse.cs" /> - <Compile Include="IO\SharpCifs\Smb\SmbComWriteResponse.cs" /> - <Compile Include="IO\SharpCifs\Smb\SmbConstants.cs" /> - <Compile Include="IO\SharpCifs\Smb\SmbException.cs" /> - <Compile Include="IO\SharpCifs\Smb\SmbFile.cs" /> - <Compile Include="IO\SharpCifs\Smb\SmbFileExtensions.cs" /> - <Compile Include="IO\SharpCifs\Smb\SmbFileFilter.cs" /> - <Compile Include="IO\SharpCifs\Smb\SmbFileInputStream.cs" /> - <Compile Include="IO\SharpCifs\Smb\SmbFilenameFilter.cs" /> - <Compile Include="IO\SharpCifs\Smb\SmbFileOutputStream.cs" /> - <Compile Include="IO\SharpCifs\Smb\SmbNamedPipe.cs" /> - <Compile Include="IO\SharpCifs\Smb\SmbRandomAccessFile.cs" /> - <Compile Include="IO\SharpCifs\Smb\SmbSession.cs" /> - <Compile Include="IO\SharpCifs\Smb\SmbShareInfo.cs" /> - <Compile Include="IO\SharpCifs\Smb\SmbTransport.cs" /> - <Compile Include="IO\SharpCifs\Smb\SmbTree.cs" /> - <Compile Include="IO\SharpCifs\Smb\Trans2FindFirst2.cs" /> - <Compile Include="IO\SharpCifs\Smb\Trans2FindFirst2Response.cs" /> - <Compile Include="IO\SharpCifs\Smb\Trans2FindNext2.cs" /> - <Compile Include="IO\SharpCifs\Smb\Trans2GetDfsReferral.cs" /> - <Compile Include="IO\SharpCifs\Smb\Trans2GetDfsReferralResponse.cs" /> - <Compile Include="IO\SharpCifs\Smb\Trans2QueryFSInformation.cs" /> - <Compile Include="IO\SharpCifs\Smb\Trans2QueryFSInformationResponse.cs" /> - <Compile Include="IO\SharpCifs\Smb\Trans2QueryPathInformation.cs" /> - <Compile Include="IO\SharpCifs\Smb\Trans2QueryPathInformationResponse.cs" /> - <Compile Include="IO\SharpCifs\Smb\Trans2SetFileInformation.cs" /> - <Compile Include="IO\SharpCifs\Smb\Trans2SetFileInformationResponse.cs" /> - <Compile Include="IO\SharpCifs\Smb\TransactNamedPipeInputStream.cs" /> - <Compile Include="IO\SharpCifs\Smb\TransactNamedPipeOutputStream.cs" /> - <Compile Include="IO\SharpCifs\Smb\TransCallNamedPipe.cs" /> - <Compile Include="IO\SharpCifs\Smb\TransCallNamedPipeResponse.cs" /> - <Compile Include="IO\SharpCifs\Smb\TransPeekNamedPipe.cs" /> - <Compile Include="IO\SharpCifs\Smb\TransPeekNamedPipeResponse.cs" /> - <Compile Include="IO\SharpCifs\Smb\TransTransactNamedPipe.cs" /> - <Compile Include="IO\SharpCifs\Smb\TransTransactNamedPipeResponse.cs" /> - <Compile Include="IO\SharpCifs\Smb\TransWaitNamedPipe.cs" /> - <Compile Include="IO\SharpCifs\Smb\TransWaitNamedPipeResponse.cs" /> - <Compile Include="IO\SharpCifs\Smb\WinError.cs" /> - <Compile Include="IO\SharpCifs\UniAddress.cs" /> - <Compile Include="IO\SharpCifs\Util\Base64.cs" /> - <Compile Include="IO\SharpCifs\Util\DES.cs" /> - <Compile Include="IO\SharpCifs\Util\Encdec.cs" /> - <Compile Include="IO\SharpCifs\Util\Hexdump.cs" /> - <Compile Include="IO\SharpCifs\Util\HMACT64.cs" /> - <Compile Include="IO\SharpCifs\Util\LogStream.cs" /> - <Compile Include="IO\SharpCifs\Util\MD4.cs" /> - <Compile Include="IO\SharpCifs\Util\RC4.cs" /> - <Compile Include="IO\SharpCifs\Util\Sharpen\AbstractMap.cs" /> - <Compile Include="IO\SharpCifs\Util\Sharpen\Arrays.cs" /> - <Compile Include="IO\SharpCifs\Util\Sharpen\BufferedReader.cs" /> - <Compile Include="IO\SharpCifs\Util\Sharpen\BufferedWriter.cs" /> - <Compile Include="IO\SharpCifs\Util\Sharpen\CharBuffer.cs" /> - <Compile Include="IO\SharpCifs\Util\Sharpen\CharSequence.cs" /> - <Compile Include="IO\SharpCifs\Util\Sharpen\Collections.cs" /> - <Compile Include="IO\SharpCifs\Util\Sharpen\ConcurrentHashMap.cs" /> - <Compile Include="IO\SharpCifs\Util\Sharpen\DateFormat.cs" /> - <Compile Include="IO\SharpCifs\Util\Sharpen\EnumeratorWrapper.cs" /> - <Compile Include="IO\SharpCifs\Util\Sharpen\Exceptions.cs" /> - <Compile Include="IO\SharpCifs\Util\Sharpen\Extensions.cs" /> - <Compile Include="IO\SharpCifs\Util\Sharpen\FileInputStream.cs" /> - <Compile Include="IO\SharpCifs\Util\Sharpen\FileOutputStream.cs" /> - <Compile Include="IO\SharpCifs\Util\Sharpen\FilePath.cs" /> - <Compile Include="IO\SharpCifs\Util\Sharpen\FileReader.cs" /> - <Compile Include="IO\SharpCifs\Util\Sharpen\FileWriter.cs" /> - <Compile Include="IO\SharpCifs\Util\Sharpen\FilterInputStream.cs" /> - <Compile Include="IO\SharpCifs\Util\Sharpen\FilterOutputStream.cs" /> - <Compile Include="IO\SharpCifs\Util\Sharpen\Hashtable.cs" /> - <Compile Include="IO\SharpCifs\Util\Sharpen\HttpURLConnection.cs" /> - <Compile Include="IO\SharpCifs\Util\Sharpen\ICallable.cs" /> - <Compile Include="IO\SharpCifs\Util\Sharpen\IConcurrentMap.cs" /> - <Compile Include="IO\SharpCifs\Util\Sharpen\IExecutor.cs" /> - <Compile Include="IO\SharpCifs\Util\Sharpen\IFilenameFilter.cs" /> - <Compile Include="IO\SharpCifs\Util\Sharpen\IFuture.cs" /> - <Compile Include="IO\SharpCifs\Util\Sharpen\InputStream.cs" /> - <Compile Include="IO\SharpCifs\Util\Sharpen\InputStreamReader.cs" /> - <Compile Include="IO\SharpCifs\Util\Sharpen\IPrivilegedAction.cs" /> - <Compile Include="IO\SharpCifs\Util\Sharpen\IRunnable.cs" /> - <Compile Include="IO\SharpCifs\Util\Sharpen\Iterator.cs" /> - <Compile Include="IO\SharpCifs\Util\Sharpen\LinkageError.cs" /> - <Compile Include="IO\SharpCifs\Util\Sharpen\Matcher.cs" /> - <Compile Include="IO\SharpCifs\Util\Sharpen\MD5.cs" /> - <Compile Include="IO\SharpCifs\Util\Sharpen\MD5Managed.cs" /> - <Compile Include="IO\SharpCifs\Util\Sharpen\MessageDigest.cs" /> - <Compile Include="IO\SharpCifs\Util\Sharpen\NetworkStream.cs" /> - <Compile Include="IO\SharpCifs\Util\Sharpen\ObjectInputStream.cs" /> - <Compile Include="IO\SharpCifs\Util\Sharpen\ObjectOutputStream.cs" /> - <Compile Include="IO\SharpCifs\Util\Sharpen\OutputStream.cs" /> - <Compile Include="IO\SharpCifs\Util\Sharpen\OutputStreamWriter.cs" /> - <Compile Include="IO\SharpCifs\Util\Sharpen\PipedInputStream.cs" /> - <Compile Include="IO\SharpCifs\Util\Sharpen\PipedOutputStream.cs" /> - <Compile Include="IO\SharpCifs\Util\Sharpen\PrintWriter.cs" /> - <Compile Include="IO\SharpCifs\Util\Sharpen\Properties.cs" /> - <Compile Include="IO\SharpCifs\Util\Sharpen\RandomAccessFile.cs" /> - <Compile Include="IO\SharpCifs\Util\Sharpen\ReentrantLock.cs" /> - <Compile Include="IO\SharpCifs\Util\Sharpen\Reference.cs" /> - <Compile Include="IO\SharpCifs\Util\Sharpen\Runtime.cs" /> - <Compile Include="IO\SharpCifs\Util\Sharpen\SimpleDateFormat.cs" /> - <Compile Include="IO\SharpCifs\Util\Sharpen\SocketEx.cs" /> - <Compile Include="IO\SharpCifs\Util\Sharpen\StringTokenizer.cs" /> - <Compile Include="IO\SharpCifs\Util\Sharpen\SynchronizedList.cs" /> - <Compile Include="IO\SharpCifs\Util\Sharpen\Thread.cs" /> - <Compile Include="IO\SharpCifs\Util\Sharpen\ThreadFactory.cs" /> - <Compile Include="IO\SharpCifs\Util\Sharpen\ThreadPoolExecutor.cs" /> - <Compile Include="IO\SharpCifs\Util\Sharpen\WrappedSystemStream.cs" /> - <Compile Include="IO\SharpCifs\Util\Transport\Request.cs" /> - <Compile Include="IO\SharpCifs\Util\Transport\Response.cs" /> - <Compile Include="IO\SharpCifs\Util\Transport\Transport.cs" /> - <Compile Include="IO\SharpCifs\Util\Transport\TransportException.cs" /> - <Compile Include="IO\ThrottledStream.cs" /> - <Compile Include="Library\CoreResolutionIgnoreRule.cs" /> - <Compile Include="Library\LibraryManager.cs" /> - <Compile Include="Library\LocalTrailerPostScanTask.cs" /> - <Compile Include="Library\MediaSourceManager.cs" /> - <Compile Include="Library\MusicManager.cs" /> - <Compile Include="Library\PathExtensions.cs" /> - <Compile Include="Library\ResolverHelper.cs" /> - <Compile Include="Library\Resolvers\Audio\AudioResolver.cs" /> - <Compile Include="Library\Resolvers\Audio\MusicAlbumResolver.cs" /> - <Compile Include="Library\Resolvers\Audio\MusicArtistResolver.cs" /> - <Compile Include="Library\Resolvers\BaseVideoResolver.cs" /> - <Compile Include="Library\Resolvers\Books\BookResolver.cs" /> - <Compile Include="Library\Resolvers\FolderResolver.cs" /> - <Compile Include="Library\Resolvers\ItemResolver.cs" /> - <Compile Include="Library\Resolvers\Movies\BoxSetResolver.cs" /> - <Compile Include="Library\Resolvers\Movies\MovieResolver.cs" /> - <Compile Include="Library\Resolvers\PhotoAlbumResolver.cs" /> - <Compile Include="Library\Resolvers\PhotoResolver.cs" /> - <Compile Include="Library\Resolvers\PlaylistResolver.cs" /> - <Compile Include="Library\Resolvers\SpecialFolderResolver.cs" /> - <Compile Include="Library\Resolvers\TV\EpisodeResolver.cs" /> - <Compile Include="Library\Resolvers\TV\SeasonResolver.cs" /> - <Compile Include="Library\Resolvers\TV\SeriesResolver.cs" /> - <Compile Include="Library\Resolvers\VideoResolver.cs" /> - <Compile Include="Library\SearchEngine.cs" /> - <Compile Include="Library\UserDataManager.cs" /> - <Compile Include="Library\UserManager.cs" /> - <Compile Include="Library\UserViewManager.cs" /> - <Compile Include="Library\Validators\ArtistsPostScanTask.cs" /> - <Compile Include="Library\Validators\ArtistsValidator.cs" /> - <Compile Include="Library\Validators\GameGenresPostScanTask.cs" /> - <Compile Include="Library\Validators\GameGenresValidator.cs" /> - <Compile Include="Library\Validators\GenresPostScanTask.cs" /> - <Compile Include="Library\Validators\GenresValidator.cs" /> - <Compile Include="Library\Validators\MusicGenresPostScanTask.cs" /> - <Compile Include="Library\Validators\MusicGenresValidator.cs" /> - <Compile Include="Library\Validators\PeopleValidator.cs" /> - <Compile Include="Library\Validators\StudiosPostScanTask.cs" /> - <Compile Include="Library\Validators\StudiosValidator.cs" /> - <Compile Include="LiveTv\ChannelImageProvider.cs" /> - <Compile Include="LiveTv\EmbyTV\DirectRecorder.cs" /> - <Compile Include="LiveTv\EmbyTV\EmbyTV.cs" /> - <Compile Include="LiveTv\EmbyTV\EmbyTVRegistration.cs" /> - <Compile Include="LiveTv\EmbyTV\EncodedRecorder.cs" /> - <Compile Include="LiveTv\EmbyTV\EntryPoint.cs" /> - <Compile Include="LiveTv\EmbyTV\IRecorder.cs" /> - <Compile Include="LiveTv\EmbyTV\ItemDataProvider.cs" /> - <Compile Include="LiveTv\EmbyTV\RecordingHelper.cs" /> - <Compile Include="LiveTv\EmbyTV\SeriesTimerManager.cs" /> - <Compile Include="LiveTv\EmbyTV\TimerManager.cs" /> - <Compile Include="LiveTv\Listings\SchedulesDirect.cs" /> - <Compile Include="LiveTv\Listings\XmlTvListingsProvider.cs" /> - <Compile Include="LiveTv\LiveStreamHelper.cs" /> - <Compile Include="LiveTv\LiveTvConfigurationFactory.cs" /> - <Compile Include="LiveTv\LiveTvDtoService.cs" /> - <Compile Include="LiveTv\LiveTvManager.cs" /> - <Compile Include="LiveTv\LiveTvMediaSourceProvider.cs" /> - <Compile Include="LiveTv\RecordingImageProvider.cs" /> - <Compile Include="LiveTv\RefreshChannelsScheduledTask.cs" /> - <Compile Include="LiveTv\TunerHosts\BaseTunerHost.cs" /> - <Compile Include="LiveTv\TunerHosts\HdHomerun\HdHomerunManager.cs" /> - <Compile Include="LiveTv\TunerHosts\HdHomerun\HdHomerunHost.cs" /> - <Compile Include="LiveTv\TunerHosts\HdHomerun\HdHomerunUdpStream.cs" /> - <Compile Include="LiveTv\TunerHosts\LiveStream.cs" /> - <Compile Include="LiveTv\TunerHosts\M3uParser.cs" /> - <Compile Include="LiveTv\TunerHosts\M3UTunerHost.cs" /> - <Compile Include="LiveTv\TunerHosts\SharedHttpStream.cs" /> - <Compile Include="Localization\LocalizationManager.cs" /> - <Compile Include="Localization\TextLocalizer.cs" /> - <Compile Include="Logging\ConsoleLogger.cs" /> - <Compile Include="Logging\SimpleLogManager.cs" /> - <Compile Include="Logging\UnhandledExceptionWriter.cs" /> - <Compile Include="MediaEncoder\EncodingManager.cs" /> - <Compile Include="Migrations\IVersionMigration.cs" /> - <Compile Include="Networking\NetworkManager.cs" /> - <Compile Include="Net\DisposableManagedObjectBase.cs" /> - <Compile Include="Net\NetAcceptSocket.cs" /> - <Compile Include="Net\SocketFactory.cs" /> - <Compile Include="Net\UdpSocket.cs" /> - <Compile Include="News\NewsEntryPoint.cs" /> - <Compile Include="News\NewsService.cs" /> - <Compile Include="Notifications\CoreNotificationTypes.cs" /> - <Compile Include="Notifications\IConfigurableNotificationService.cs" /> - <Compile Include="Notifications\InternalNotificationService.cs" /> - <Compile Include="Notifications\NotificationConfigurationFactory.cs" /> - <Compile Include="Notifications\NotificationManager.cs" /> - <Compile Include="Notifications\Notifications.cs" /> - <Compile Include="Notifications\SqliteNotificationsRepository.cs" /> - <Compile Include="Notifications\WebSocketNotifier.cs" /> - <Compile Include="Data\BaseSqliteRepository.cs" /> - <Compile Include="Data\CleanDatabaseScheduledTask.cs" /> - <Compile Include="Data\SqliteExtensions.cs" /> - <Compile Include="Playlists\ManualPlaylistsFolder.cs" /> - <Compile Include="Playlists\PlaylistImageProvider.cs" /> - <Compile Include="Playlists\PlaylistManager.cs" /> - <Compile Include="Playlists\PlaylistsDynamicFolder.cs" /> - <Compile Include="Properties\AssemblyInfo.cs" /> - <Compile Include="Reflection\AssemblyInfo.cs" /> - <Compile Include="ScheduledTasks\ChapterImagesTask.cs" /> - <Compile Include="ScheduledTasks\DailyTrigger.cs" /> - <Compile Include="ScheduledTasks\IntervalTrigger.cs" /> - <Compile Include="ScheduledTasks\PeopleValidationTask.cs" /> - <Compile Include="ScheduledTasks\PluginUpdateTask.cs" /> - <Compile Include="ScheduledTasks\RefreshMediaLibraryTask.cs" /> - <Compile Include="ScheduledTasks\ScheduledTaskWorker.cs" /> - <Compile Include="ScheduledTasks\StartupTrigger.cs" /> - <Compile Include="ScheduledTasks\SystemEventTrigger.cs" /> - <Compile Include="ScheduledTasks\SystemUpdateTask.cs" /> - <Compile Include="ScheduledTasks\TaskManager.cs" /> - <Compile Include="ScheduledTasks\Tasks\DeleteCacheFileTask.cs" /> - <Compile Include="ScheduledTasks\Tasks\DeleteLogFileTask.cs" /> - <Compile Include="ScheduledTasks\Tasks\ReloadLoggerFileTask.cs" /> - <Compile Include="ScheduledTasks\WeeklyTrigger.cs" /> - <Compile Include="Security\AuthenticationRepository.cs" /> - <Compile Include="Security\EncryptionManager.cs" /> - <Compile Include="Security\MBLicenseFile.cs" /> - <Compile Include="Security\PluginSecurityManager.cs" /> - <Compile Include="Security\RegRecord.cs" /> - <Compile Include="Serialization\JsonSerializer.cs" /> - <Compile Include="Serialization\XmlSerializer.cs" /> - <Compile Include="ServerApplicationPaths.cs" /> - <Compile Include="ServerManager\ServerManager.cs" /> - <Compile Include="ServerManager\WebSocketConnection.cs" /> - <Compile Include="Services\ServicePath.cs" /> - <Compile Include="Services\ServiceMethod.cs" /> - <Compile Include="Services\ResponseHelper.cs" /> - <Compile Include="Services\HttpResult.cs" /> - <Compile Include="Services\RequestHelper.cs" /> - <Compile Include="Services\ServiceHandler.cs" /> - <Compile Include="Services\ServiceController.cs" /> - <Compile Include="Services\ServiceExec.cs" /> - <Compile Include="Services\StringMapTypeDeserializer.cs" /> - <Compile Include="Services\SwaggerService.cs" /> - <Compile Include="Services\UrlExtensions.cs" /> - <Compile Include="Session\HttpSessionController.cs" /> - <Compile Include="Session\SessionManager.cs" /> - <Compile Include="Session\SessionWebSocketListener.cs" /> - <Compile Include="Session\WebSocketController.cs" /> - <Compile Include="Social\SharingManager.cs" /> - <Compile Include="Social\SharingRepository.cs" /> - <Compile Include="Sorting\AiredEpisodeOrderComparer.cs" /> - <Compile Include="Sorting\AlbumArtistComparer.cs" /> - <Compile Include="Sorting\AlbumComparer.cs" /> - <Compile Include="Sorting\AlphanumComparator.cs" /> - <Compile Include="Sorting\ArtistComparer.cs" /> - <Compile Include="Sorting\CommunityRatingComparer.cs" /> - <Compile Include="Sorting\CriticRatingComparer.cs" /> - <Compile Include="Sorting\DateCreatedComparer.cs" /> - <Compile Include="Sorting\DateLastMediaAddedComparer.cs" /> - <Compile Include="Sorting\DatePlayedComparer.cs" /> - <Compile Include="Sorting\GameSystemComparer.cs" /> - <Compile Include="Sorting\IsFavoriteOrLikeComparer.cs" /> - <Compile Include="Sorting\IsFolderComparer.cs" /> - <Compile Include="Sorting\IsPlayedComparer.cs" /> - <Compile Include="Sorting\IsUnplayedComparer.cs" /> - <Compile Include="Sorting\NameComparer.cs" /> - <Compile Include="Sorting\OfficialRatingComparer.cs" /> - <Compile Include="Sorting\PlayCountComparer.cs" /> - <Compile Include="Sorting\PlayersComparer.cs" /> - <Compile Include="Sorting\PremiereDateComparer.cs" /> - <Compile Include="Sorting\ProductionYearComparer.cs" /> - <Compile Include="Sorting\RandomComparer.cs" /> - <Compile Include="Sorting\RuntimeComparer.cs" /> - <Compile Include="Sorting\SeriesSortNameComparer.cs" /> - <Compile Include="Sorting\SortNameComparer.cs" /> - <Compile Include="Sorting\StartDateComparer.cs" /> - <Compile Include="Sorting\StudioComparer.cs" /> - <Compile Include="StartupOptions.cs" /> - <Compile Include="SystemEvents.cs" /> - <Compile Include="TextEncoding\NLangDetect\Detector.cs" /> - <Compile Include="TextEncoding\NLangDetect\DetectorFactory.cs" /> - <Compile Include="TextEncoding\NLangDetect\ErrorCode.cs" /> - <Compile Include="TextEncoding\NLangDetect\Extensions\CharExtensions.cs" /> - <Compile Include="TextEncoding\NLangDetect\Extensions\RandomExtensions.cs" /> - <Compile Include="TextEncoding\NLangDetect\Extensions\StringExtensions.cs" /> - <Compile Include="TextEncoding\NLangDetect\Extensions\UnicodeBlock.cs" /> - <Compile Include="TextEncoding\NLangDetect\GenProfile.cs" /> - <Compile Include="TextEncoding\NLangDetect\InternalException.cs" /> - <Compile Include="TextEncoding\NLangDetect\Language.cs" /> - <Compile Include="TextEncoding\NLangDetect\LanguageDetector.cs" /> - <Compile Include="TextEncoding\NLangDetect\NLangDetectException.cs" /> - <Compile Include="TextEncoding\NLangDetect\ProbVector.cs" /> - <Compile Include="TextEncoding\NLangDetect\Utils\LangProfile.cs" /> - <Compile Include="TextEncoding\NLangDetect\Utils\Messages.cs" /> - <Compile Include="TextEncoding\NLangDetect\Utils\NGram.cs" /> - <Compile Include="TextEncoding\NLangDetect\Utils\TagExtractor.cs" /> - <Compile Include="TextEncoding\TextEncoding.cs" /> - <Compile Include="TextEncoding\TextEncodingDetect.cs" /> - <Compile Include="TextEncoding\UniversalDetector\CharsetDetector.cs" /> - <Compile Include="TextEncoding\UniversalDetector\Core\Big5Prober.cs" /> - <Compile Include="TextEncoding\UniversalDetector\Core\BitPackage.cs" /> - <Compile Include="TextEncoding\UniversalDetector\Core\CharDistributionAnalyser.cs" /> - <Compile Include="TextEncoding\UniversalDetector\Core\CharsetProber.cs" /> - <Compile Include="TextEncoding\UniversalDetector\Core\Charsets.cs" /> - <Compile Include="TextEncoding\UniversalDetector\Core\CodingStateMachine.cs" /> - <Compile Include="TextEncoding\UniversalDetector\Core\EscCharsetProber.cs" /> - <Compile Include="TextEncoding\UniversalDetector\Core\EscSM.cs" /> - <Compile Include="TextEncoding\UniversalDetector\Core\EUCJPProber.cs" /> - <Compile Include="TextEncoding\UniversalDetector\Core\EUCKRProber.cs" /> - <Compile Include="TextEncoding\UniversalDetector\Core\EUCTWProber.cs" /> - <Compile Include="TextEncoding\UniversalDetector\Core\GB18030Prober.cs" /> - <Compile Include="TextEncoding\UniversalDetector\Core\HebrewProber.cs" /> - <Compile Include="TextEncoding\UniversalDetector\Core\JapaneseContextAnalyser.cs" /> - <Compile Include="TextEncoding\UniversalDetector\Core\LangBulgarianModel.cs" /> - <Compile Include="TextEncoding\UniversalDetector\Core\LangCyrillicModel.cs" /> - <Compile Include="TextEncoding\UniversalDetector\Core\LangGreekModel.cs" /> - <Compile Include="TextEncoding\UniversalDetector\Core\LangHebrewModel.cs" /> - <Compile Include="TextEncoding\UniversalDetector\Core\LangHungarianModel.cs" /> - <Compile Include="TextEncoding\UniversalDetector\Core\LangThaiModel.cs" /> - <Compile Include="TextEncoding\UniversalDetector\Core\Latin1Prober.cs" /> - <Compile Include="TextEncoding\UniversalDetector\Core\MBCSGroupProber.cs" /> - <Compile Include="TextEncoding\UniversalDetector\Core\MBCSSM.cs" /> - <Compile Include="TextEncoding\UniversalDetector\Core\SBCharsetProber.cs" /> - <Compile Include="TextEncoding\UniversalDetector\Core\SBCSGroupProber.cs" /> - <Compile Include="TextEncoding\UniversalDetector\Core\SequenceModel.cs" /> - <Compile Include="TextEncoding\UniversalDetector\Core\SJISProber.cs" /> - <Compile Include="TextEncoding\UniversalDetector\Core\SMModel.cs" /> - <Compile Include="TextEncoding\UniversalDetector\Core\UniversalDetector.cs" /> - <Compile Include="TextEncoding\UniversalDetector\Core\UTF8Prober.cs" /> - <Compile Include="TextEncoding\UniversalDetector\DetectionConfidence.cs" /> - <Compile Include="TextEncoding\UniversalDetector\ICharsetDetector.cs" /> - <Compile Include="Threading\CommonTimer.cs" /> - <Compile Include="Threading\TimerFactory.cs" /> - <Compile Include="TV\SeriesPostScanTask.cs" /> - <Compile Include="TV\TVSeriesManager.cs" /> - <Compile Include="Udp\UdpServer.cs" /> - <Compile Include="Updates\InstallationManager.cs" /> - <Compile Include="UserViews\CollectionFolderImageProvider.cs" /> - <Compile Include="UserViews\DynamicImageProvider.cs" /> - <Compile Include="UserViews\FolderImageProvider.cs" /> - <Compile Include="Xml\XmlReaderSettingsFactory.cs" /> - </ItemGroup> - <ItemGroup> + + <ItemGroup> <EmbeddedResource Include="Localization\iso6392.txt" /> - </ItemGroup> - <ItemGroup> - <ProjectReference Include="..\Emby.Dlna\Emby.Dlna.csproj"> - <Project>{805844ab-e92f-45e6-9d99-4f6d48d129a5}</Project> - <Name>Emby.Dlna</Name> - </ProjectReference> - <ProjectReference Include="..\Emby.Drawing\Emby.Drawing.csproj"> - <Project>{08fff49b-f175-4807-a2b5-73b0ebd9f716}</Project> - <Name>Emby.Drawing</Name> - </ProjectReference> - <ProjectReference Include="..\Emby.Photos\Emby.Photos.csproj"> - <Project>{89ab4548-770d-41fd-a891-8daff44f452c}</Project> - <Name>Emby.Photos</Name> - </ProjectReference> - <ProjectReference Include="..\MediaBrowser.Api\MediaBrowser.Api.csproj"> - <Project>{4fd51ac5-2c16-4308-a993-c3a84f3b4582}</Project> - <Name>MediaBrowser.Api</Name> - </ProjectReference> - <ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj"> - <Project>{9142eefa-7570-41e1-bfcc-468bb571af2f}</Project> - <Name>MediaBrowser.Common</Name> - </ProjectReference> - <ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj"> - <Project>{17e1f4e6-8abd-4fe5-9ecf-43d4b6087ba2}</Project> - <Name>MediaBrowser.Controller</Name> - </ProjectReference> - <ProjectReference Include="..\MediaBrowser.LocalMetadata\MediaBrowser.LocalMetadata.csproj"> - <Project>{7ef9f3e0-697d-42f3-a08f-19deb5f84392}</Project> - <Name>MediaBrowser.LocalMetadata</Name> - </ProjectReference> - <ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj"> - <Project>{7eeeb4bb-f3e8-48fc-b4c5-70f0fff8329b}</Project> - <Name>MediaBrowser.Model</Name> - </ProjectReference> - <ProjectReference Include="..\MediaBrowser.Providers\MediaBrowser.Providers.csproj"> - <Project>{442b5058-dcaf-4263-bb6a-f21e31120a1b}</Project> - <Name>MediaBrowser.Providers</Name> - </ProjectReference> - <ProjectReference Include="..\MediaBrowser.WebDashboard\MediaBrowser.WebDashboard.csproj"> - <Project>{5624b7b5-b5a7-41d8-9f10-cc5611109619}</Project> - <Name>MediaBrowser.WebDashboard</Name> - </ProjectReference> - <ProjectReference Include="..\MediaBrowser.XbmcMetadata\MediaBrowser.XbmcMetadata.csproj"> - <Project>{23499896-b135-4527-8574-c26e926ea99e}</Project> - <Name>MediaBrowser.XbmcMetadata</Name> - </ProjectReference> - <ProjectReference Include="..\Mono.Nat\Mono.Nat.csproj"> - <Project>{cb7f2326-6497-4a3d-ba03-48513b17a7be}</Project> - <Name>Mono.Nat</Name> - </ProjectReference> - <ProjectReference Include="..\OpenSubtitlesHandler\OpenSubtitlesHandler.csproj"> - <Project>{4a4402d4-e910-443b-b8fc-2c18286a2ca0}</Project> - <Name>OpenSubtitlesHandler</Name> - </ProjectReference> - <ProjectReference Include="..\SocketHttpListener\SocketHttpListener.csproj"> - <Project>{1d74413b-e7cf-455b-b021-f52bdf881542}</Project> - <Name>SocketHttpListener</Name> - </ProjectReference> - <Reference Include="Emby.Naming"> - <HintPath>..\ThirdParty\emby\Emby.Naming.dll</HintPath> - </Reference> - <Reference Include="Emby.Server.MediaEncoding"> - <HintPath>..\ThirdParty\emby\Emby.Server.MediaEncoding.dll</HintPath> - </Reference> - <Reference Include="Emby.XmlTv, Version=1.0.6387.29335, Culture=neutral, processorArchitecture=MSIL"> - <HintPath>..\packages\Emby.XmlTv.1.0.10\lib\portable-net45+netstandard2.0+win8\Emby.XmlTv.dll</HintPath> - </Reference> - <Reference Include="ServiceStack.Text, Version=4.5.8.0, Culture=neutral, processorArchitecture=MSIL"> - <HintPath>..\packages\ServiceStack.Text.4.5.8\lib\net45\ServiceStack.Text.dll</HintPath> - <Private>True</Private> - </Reference> - <Reference Include="SharpCompress, Version=0.18.2.0, Culture=neutral, PublicKeyToken=afb0a02973931d96, processorArchitecture=MSIL"> - <HintPath>..\packages\SharpCompress.0.18.2\lib\net45\SharpCompress.dll</HintPath> - </Reference> - <Reference Include="SimpleInjector, Version=4.0.12.0, Culture=neutral, PublicKeyToken=984cb50dea722e99, processorArchitecture=MSIL"> - <HintPath>..\packages\SimpleInjector.4.0.12\lib\net45\SimpleInjector.dll</HintPath> - </Reference> - <Reference Include="SQLitePCL.pretty, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL"> - <HintPath>..\packages\SQLitePCL.pretty.1.1.0\lib\portable-net45+netcore45+wpa81+wp8\SQLitePCL.pretty.dll</HintPath> - <Private>True</Private> - </Reference> - </ItemGroup> - <ItemGroup> - <Reference Include="SQLitePCLRaw.core, Version=1.0.0.0, Culture=neutral, PublicKeyToken=1488e028ca7ab535, processorArchitecture=MSIL"> - <HintPath>..\packages\SQLitePCLRaw.core.1.1.8\lib\net45\SQLitePCLRaw.core.dll</HintPath> - </Reference> - <Reference Include="System" /> - <Reference Include="System.Configuration" /> - <Reference Include="System.Core" /> - <Reference Include="System.Runtime.Serialization" /> - <Reference Include="System.Xml.Linq" /> - <Reference Include="System.Data.DataSetExtensions" /> - <Reference Include="Microsoft.CSharp" /> - <Reference Include="System.Data" /> - <Reference Include="System.Net.Http" /> - <Reference Include="System.Xml" /> - </ItemGroup> - <ItemGroup> <EmbeddedResource Include="Localization\countries.json" /> <EmbeddedResource Include="Localization\Core\ar.json" /> <EmbeddedResource Include="Localization\Core\bg-BG.json" /> @@ -710,128 +75,113 @@ <EmbeddedResource Include="Localization\Core\en-US.json" /> <EmbeddedResource Include="Localization\Core\el.json" /> <EmbeddedResource Include="Localization\Core\gsw.json" /> - <None Include="packages.config" /> - <None Include="TextEncoding\NLangDetect\Profiles\afr" /> - <None Include="TextEncoding\NLangDetect\Profiles\ara" /> - <None Include="TextEncoding\NLangDetect\Profiles\ben" /> - <None Include="TextEncoding\NLangDetect\Profiles\bul" /> - <None Include="TextEncoding\NLangDetect\Profiles\ces" /> - <None Include="TextEncoding\NLangDetect\Profiles\dan" /> - <None Include="TextEncoding\NLangDetect\Profiles\deu" /> - <None Include="TextEncoding\NLangDetect\Profiles\ell" /> - <None Include="TextEncoding\NLangDetect\Profiles\eng" /> - <None Include="TextEncoding\NLangDetect\Profiles\est" /> - <None Include="TextEncoding\NLangDetect\Profiles\fas" /> - <None Include="TextEncoding\NLangDetect\Profiles\fin" /> - <None Include="TextEncoding\NLangDetect\Profiles\fra" /> - <None Include="TextEncoding\NLangDetect\Profiles\guj" /> - <None Include="TextEncoding\NLangDetect\Profiles\heb" /> - <None Include="TextEncoding\NLangDetect\Profiles\hin" /> - <None Include="TextEncoding\NLangDetect\Profiles\hrv" /> - <None Include="TextEncoding\NLangDetect\Profiles\hun" /> - <None Include="TextEncoding\NLangDetect\Profiles\ind" /> - <None Include="TextEncoding\NLangDetect\Profiles\ita" /> - <None Include="TextEncoding\NLangDetect\Profiles\jpn" /> - <None Include="TextEncoding\NLangDetect\Profiles\kan" /> - <None Include="TextEncoding\NLangDetect\Profiles\kor" /> - <None Include="TextEncoding\NLangDetect\Profiles\lav" /> - <None Include="TextEncoding\NLangDetect\Profiles\lit" /> - <None Include="TextEncoding\NLangDetect\Profiles\mal" /> - <None Include="TextEncoding\NLangDetect\Profiles\mar" /> - <None Include="TextEncoding\NLangDetect\Profiles\mkd" /> - <None Include="TextEncoding\NLangDetect\Profiles\nep" /> - <None Include="TextEncoding\NLangDetect\Profiles\nld" /> - <None Include="TextEncoding\NLangDetect\Profiles\nor" /> - <None Include="TextEncoding\NLangDetect\Profiles\pan" /> - <None Include="TextEncoding\NLangDetect\Profiles\pol" /> - <None Include="TextEncoding\NLangDetect\Profiles\por" /> - <None Include="TextEncoding\NLangDetect\Profiles\ron" /> - <None Include="TextEncoding\NLangDetect\Profiles\rus" /> - <None Include="TextEncoding\NLangDetect\Profiles\slk" /> - <None Include="TextEncoding\NLangDetect\Profiles\slv" /> - <None Include="TextEncoding\NLangDetect\Profiles\som" /> - <None Include="TextEncoding\NLangDetect\Profiles\spa" /> - <None Include="TextEncoding\NLangDetect\Profiles\sqi" /> - <None Include="TextEncoding\NLangDetect\Profiles\swa" /> - <None Include="TextEncoding\NLangDetect\Profiles\swe" /> - <None Include="TextEncoding\NLangDetect\Profiles\tam" /> - <None Include="TextEncoding\NLangDetect\Profiles\tel" /> - <None Include="TextEncoding\NLangDetect\Profiles\tgl" /> - <None Include="TextEncoding\NLangDetect\Profiles\tha" /> - <None Include="TextEncoding\NLangDetect\Profiles\tur" /> - <None Include="TextEncoding\NLangDetect\Profiles\ukr" /> - <None Include="TextEncoding\NLangDetect\Profiles\urd" /> - <None Include="TextEncoding\NLangDetect\Profiles\vie" /> + <EmbeddedResource Include="TextEncoding\NLangDetect\Profiles\afr" /> + <EmbeddedResource Include="TextEncoding\NLangDetect\Profiles\ara" /> + <EmbeddedResource Include="TextEncoding\NLangDetect\Profiles\ben" /> + <EmbeddedResource Include="TextEncoding\NLangDetect\Profiles\bul" /> + <EmbeddedResource Include="TextEncoding\NLangDetect\Profiles\ces" /> + <EmbeddedResource Include="TextEncoding\NLangDetect\Profiles\dan" /> + <EmbeddedResource Include="TextEncoding\NLangDetect\Profiles\deu" /> + <EmbeddedResource Include="TextEncoding\NLangDetect\Profiles\ell" /> + <EmbeddedResource Include="TextEncoding\NLangDetect\Profiles\eng" /> + <EmbeddedResource Include="TextEncoding\NLangDetect\Profiles\est" /> + <EmbeddedResource Include="TextEncoding\NLangDetect\Profiles\fas" /> + <EmbeddedResource Include="TextEncoding\NLangDetect\Profiles\fin" /> + <EmbeddedResource Include="TextEncoding\NLangDetect\Profiles\fra" /> + <EmbeddedResource Include="TextEncoding\NLangDetect\Profiles\guj" /> + <EmbeddedResource Include="TextEncoding\NLangDetect\Profiles\heb" /> + <EmbeddedResource Include="TextEncoding\NLangDetect\Profiles\hin" /> + <EmbeddedResource Include="TextEncoding\NLangDetect\Profiles\hrv" /> + <EmbeddedResource Include="TextEncoding\NLangDetect\Profiles\hun" /> + <EmbeddedResource Include="TextEncoding\NLangDetect\Profiles\ind" /> + <EmbeddedResource Include="TextEncoding\NLangDetect\Profiles\ita" /> + <EmbeddedResource Include="TextEncoding\NLangDetect\Profiles\jpn" /> + <EmbeddedResource Include="TextEncoding\NLangDetect\Profiles\kan" /> + <EmbeddedResource Include="TextEncoding\NLangDetect\Profiles\kor" /> + <EmbeddedResource Include="TextEncoding\NLangDetect\Profiles\lav" /> + <EmbeddedResource Include="TextEncoding\NLangDetect\Profiles\lit" /> + <EmbeddedResource Include="TextEncoding\NLangDetect\Profiles\mal" /> + <EmbeddedResource Include="TextEncoding\NLangDetect\Profiles\mar" /> + <EmbeddedResource Include="TextEncoding\NLangDetect\Profiles\mkd" /> + <EmbeddedResource Include="TextEncoding\NLangDetect\Profiles\nep" /> + <EmbeddedResource Include="TextEncoding\NLangDetect\Profiles\nld" /> + <EmbeddedResource Include="TextEncoding\NLangDetect\Profiles\nor" /> + <EmbeddedResource Include="TextEncoding\NLangDetect\Profiles\pan" /> + <EmbeddedResource Include="TextEncoding\NLangDetect\Profiles\pol" /> + <EmbeddedResource Include="TextEncoding\NLangDetect\Profiles\por" /> + <EmbeddedResource Include="TextEncoding\NLangDetect\Profiles\ron" /> + <EmbeddedResource Include="TextEncoding\NLangDetect\Profiles\rus" /> + <EmbeddedResource Include="TextEncoding\NLangDetect\Profiles\slk" /> + <EmbeddedResource Include="TextEncoding\NLangDetect\Profiles\slv" /> + <EmbeddedResource Include="TextEncoding\NLangDetect\Profiles\som" /> + <EmbeddedResource Include="TextEncoding\NLangDetect\Profiles\spa" /> + <EmbeddedResource Include="TextEncoding\NLangDetect\Profiles\sqi" /> + <EmbeddedResource Include="TextEncoding\NLangDetect\Profiles\swa" /> + <EmbeddedResource Include="TextEncoding\NLangDetect\Profiles\swe" /> + <EmbeddedResource Include="TextEncoding\NLangDetect\Profiles\tam" /> + <EmbeddedResource Include="TextEncoding\NLangDetect\Profiles\tel" /> + <EmbeddedResource Include="TextEncoding\NLangDetect\Profiles\tgl" /> + <EmbeddedResource Include="TextEncoding\NLangDetect\Profiles\tha" /> + <EmbeddedResource Include="TextEncoding\NLangDetect\Profiles\tur" /> + <EmbeddedResource Include="TextEncoding\NLangDetect\Profiles\ukr" /> + <EmbeddedResource Include="TextEncoding\NLangDetect\Profiles\urd" /> + <EmbeddedResource Include="TextEncoding\NLangDetect\Profiles\vie" /> <EmbeddedResource Include="TextEncoding\NLangDetect\Profiles\zh-cn" /> <EmbeddedResource Include="TextEncoding\NLangDetect\Profiles\zh-tw" /> <EmbeddedResource Include="TextEncoding\NLangDetect\Utils\messages.properties" /> - </ItemGroup> - <ItemGroup> - <EmbeddedResource Include="Localization\Ratings\au.txt" /> - </ItemGroup> - <ItemGroup> - <EmbeddedResource Include="Localization\Ratings\be.txt" /> - </ItemGroup> - <ItemGroup> - <EmbeddedResource Include="Localization\Ratings\br.txt" /> - </ItemGroup> - <ItemGroup> - <EmbeddedResource Include="Localization\Ratings\ca.txt" /> - </ItemGroup> - <ItemGroup> - <EmbeddedResource Include="Localization\Ratings\co.txt" /> - </ItemGroup> - <ItemGroup> - <EmbeddedResource Include="Localization\Ratings\de.txt" /> - </ItemGroup> - <ItemGroup> - <EmbeddedResource Include="Localization\Ratings\dk.txt" /> - </ItemGroup> - <ItemGroup> - <EmbeddedResource Include="Localization\Ratings\fr.txt" /> - </ItemGroup> - <ItemGroup> - <EmbeddedResource Include="Localization\Ratings\gb.txt" /> - </ItemGroup> - <ItemGroup> - <EmbeddedResource Include="Localization\Ratings\ie.txt" /> - </ItemGroup> - <ItemGroup> - <EmbeddedResource Include="Localization\Ratings\jp.txt" /> - </ItemGroup> - <ItemGroup> - <EmbeddedResource Include="Localization\Ratings\kz.txt" /> - </ItemGroup> - <ItemGroup> - <EmbeddedResource Include="Localization\Ratings\mx.txt" /> - </ItemGroup> - <ItemGroup> - <EmbeddedResource Include="Localization\Ratings\nl.txt" /> - </ItemGroup> - <ItemGroup> - <EmbeddedResource Include="Localization\Ratings\nz.txt" /> - </ItemGroup> - <ItemGroup> - <EmbeddedResource Include="Localization\Ratings\ru.txt" /> - </ItemGroup> - <ItemGroup> - <EmbeddedResource Include="Localization\Ratings\us.txt" /> - </ItemGroup> - <ItemGroup> - <EmbeddedResource Include="Localization\Ratings\uk.txt" /> - </ItemGroup> - <ItemGroup> - <EmbeddedResource Include="Localization\Ratings\es.txt" /> - </ItemGroup> - <ItemGroup> - <EmbeddedResource Include="Localization\Ratings\ro.txt" /> - </ItemGroup> - <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> - <!-- To modify your build process, add your task inside one of the targets below and uncomment it. - Other similar extension points exist, see Microsoft.Common.targets. - <Target Name="BeforeBuild"> - </Target> - <Target Name="AfterBuild"> - </Target> - --> -</Project>
\ No newline at end of file + </ItemGroup> + <ItemGroup> + <EmbeddedResource Include="Localization\Ratings\br.txt" /> + </ItemGroup> + <ItemGroup> + <EmbeddedResource Include="Localization\Ratings\ca.txt" /> + </ItemGroup> + <ItemGroup> + <EmbeddedResource Include="Localization\Ratings\co.txt" /> + </ItemGroup> + <ItemGroup> + <EmbeddedResource Include="Localization\Ratings\dk.txt" /> + </ItemGroup> + <ItemGroup> + <EmbeddedResource Include="Localization\Ratings\fr.txt" /> + </ItemGroup> + <ItemGroup> + <EmbeddedResource Include="Localization\Ratings\gb.txt" /> + </ItemGroup> + <ItemGroup> + <EmbeddedResource Include="Localization\Ratings\ie.txt" /> + </ItemGroup> + <ItemGroup> + <EmbeddedResource Include="Localization\Ratings\jp.txt" /> + </ItemGroup> + <ItemGroup> + <EmbeddedResource Include="Localization\Ratings\kz.txt" /> + </ItemGroup> + <ItemGroup> + <EmbeddedResource Include="Localization\Ratings\mx.txt" /> + </ItemGroup> + <ItemGroup> + <EmbeddedResource Include="Localization\Ratings\nl.txt" /> + </ItemGroup> + <ItemGroup> + <EmbeddedResource Include="Localization\Ratings\nz.txt" /> + </ItemGroup> + <ItemGroup> + <EmbeddedResource Include="Localization\Ratings\us.txt" /> + </ItemGroup> + <ItemGroup> + <EmbeddedResource Include="Localization\Ratings\uk.txt" /> + </ItemGroup> + <ItemGroup> + <EmbeddedResource Include="Localization\Ratings\es.txt" /> + </ItemGroup> + <ItemGroup> + <EmbeddedResource Include="Localization\Ratings\ro.txt" /> + </ItemGroup> + <ItemGroup> + <Reference Include="Emby.Server.MediaEncoding"> + <HintPath>..\ThirdParty\emby\Emby.Server.MediaEncoding.dll</HintPath> + </Reference> + </ItemGroup> + +</Project> diff --git a/Emby.Server.Implementations/EntryPoints/AutomaticRestartEntryPoint.cs b/Emby.Server.Implementations/EntryPoints/AutomaticRestartEntryPoint.cs index c2cee00c8..561f5ee12 100644 --- a/Emby.Server.Implementations/EntryPoints/AutomaticRestartEntryPoint.cs +++ b/Emby.Server.Implementations/EntryPoints/AutomaticRestartEntryPoint.cs @@ -112,7 +112,6 @@ namespace Emby.Server.Implementations.EntryPoints _appHost.HasPendingRestartChanged -= _appHost_HasPendingRestartChanged; DisposeTimer(); - GC.SuppressFinalize(this); } private void DisposeTimer() diff --git a/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs b/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs index 903bb0ff4..6801b2823 100644 --- a/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs +++ b/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs @@ -26,9 +26,10 @@ namespace Emby.Server.Implementations.EntryPoints private readonly IDeviceDiscovery _deviceDiscovery; private ITimer _timer; - private bool _isStarted; private readonly ITimerFactory _timerFactory; + private NatManager _natManager; + public ExternalPortForwarding(ILogManager logmanager, IServerApplicationHost appHost, IServerConfigurationManager config, IDeviceDiscovery deviceDiscovery, IHttpClient httpClient, ITimerFactory timerFactory) { _logger = logmanager.GetLogger("PortMapper"); @@ -37,6 +38,12 @@ namespace Emby.Server.Implementations.EntryPoints _deviceDiscovery = deviceDiscovery; _httpClient = httpClient; _timerFactory = timerFactory; + _config.ConfigurationUpdated += _config_ConfigurationUpdated1; + } + + private void _config_ConfigurationUpdated1(object sender, EventArgs e) + { + _config_ConfigurationUpdated(sender, e); } private string _lastConfigIdentifier; @@ -49,8 +56,8 @@ namespace Emby.Server.Implementations.EntryPoints values.Add(config.PublicPort.ToString(CultureInfo.InvariantCulture)); values.Add(_appHost.HttpPort.ToString(CultureInfo.InvariantCulture)); values.Add(_appHost.HttpsPort.ToString(CultureInfo.InvariantCulture)); - values.Add((config.EnableHttps || config.RequireHttps).ToString()); values.Add(_appHost.EnableHttps.ToString()); + values.Add((config.EnableRemoteAccess).ToString()); return string.Join("|", values.ToArray(values.Count)); } @@ -59,10 +66,7 @@ namespace Emby.Server.Implementations.EntryPoints { if (!string.Equals(_lastConfigIdentifier, GetConfigIdentifier(), StringComparison.OrdinalIgnoreCase)) { - if (_isStarted) - { - DisposeNat(); - } + DisposeNat(); Run(); } @@ -70,10 +74,7 @@ namespace Emby.Server.Implementations.EntryPoints public void Run() { - NatUtility.Logger = _logger; - NatUtility.HttpClient = _httpClient; - - if (_config.Configuration.EnableUPnP) + if (_config.Configuration.EnableUPnP && _config.Configuration.EnableRemoteAccess) { Start(); } @@ -85,26 +86,18 @@ namespace Emby.Server.Implementations.EntryPoints private void Start() { _logger.Debug("Starting NAT discovery"); - NatUtility.EnabledProtocols = new List<NatProtocol> + if (_natManager == null) { - NatProtocol.Pmp - }; - NatUtility.DeviceFound += NatUtility_DeviceFound; - - // Mono.Nat does never rise this event. The event is there however it is useless. - // You could remove it with no risk. - NatUtility.DeviceLost += NatUtility_DeviceLost; - - - NatUtility.StartDiscovery(); + _natManager = new NatManager(_logger, _httpClient); + _natManager.DeviceFound += NatUtility_DeviceFound; + _natManager.StartDiscovery(); + } _timer = _timerFactory.Create(ClearCreatedRules, null, TimeSpan.FromMinutes(10), TimeSpan.FromMinutes(10)); _deviceDiscovery.DeviceDiscovered += _deviceDiscovery_DeviceDiscovered; _lastConfigIdentifier = GetConfigIdentifier(); - - _isStarted = true; } private async void _deviceDiscovery_DeviceDiscovered(object sender, GenericEventArgs<UpnpDeviceInfo> e) @@ -182,8 +175,17 @@ namespace Emby.Server.Implementations.EntryPoints return; } - _logger.Debug("Calling Nat.Handle on " + identifier); - NatUtility.Handle(localAddress, info, endpoint, NatProtocol.Upnp); + // This should never happen, but the Handle method will throw ArgumentNullException if it does + if (localAddress == null) + { + return; + } + + var natManager = _natManager; + if (natManager != null) + { + await natManager.Handle(localAddress, info, endpoint, NatProtocol.Upnp).ConfigureAwait(false); + } } } @@ -209,19 +211,11 @@ namespace Emby.Server.Implementations.EntryPoints try { var device = e.Device; - _logger.Debug("NAT device found: {0}", device.LocalAddress.ToString()); CreateRules(device); } catch { - // I think it could be a good idea to log the exception because - // you are using permanent portmapping here (never expire) and that means that next time - // CreatePortMap is invoked it can fails with a 718-ConflictInMappingEntry or not. That depends - // on the router's upnp implementation (specs says it should fail however some routers don't do it) - // It also can fail with others like 727-ExternalPortOnlySupportsWildcard, 728-NoPortMapsAvailable - // and those errors (upnp errors) could be useful for diagnosting. - // Commenting out because users are reporting problems out of our control //_logger.ErrorException("Error creating port forwarding rules", ex); } @@ -238,14 +232,15 @@ namespace Emby.Server.Implementations.EntryPoints // On some systems the device discovered event seems to fire repeatedly // This check will help ensure we're not trying to port map the same device over and over + var address = device.LocalAddress; - var address = device.LocalAddress.ToString(); + var addressString = address.ToString(); lock (_createdRules) { - if (!_createdRules.Contains(address)) + if (!_createdRules.Contains(addressString)) { - _createdRules.Add(address); + _createdRules.Add(addressString); } else { @@ -253,41 +248,32 @@ namespace Emby.Server.Implementations.EntryPoints } } - var success = await CreatePortMap(device, _appHost.HttpPort, _config.Configuration.PublicPort).ConfigureAwait(false); - - if (success) + try { - await CreatePortMap(device, _appHost.HttpsPort, _config.Configuration.PublicHttpsPort).ConfigureAwait(false); + await CreatePortMap(device, _appHost.HttpPort, _config.Configuration.PublicPort).ConfigureAwait(false); + } + catch (Exception ex) + { + return; } - } - - private async Task<bool> CreatePortMap(INatDevice device, int privatePort, int publicPort) - { - _logger.Debug("Creating port map on port {0}", privatePort); try { - await device.CreatePortMap(new Mapping(Protocol.Tcp, privatePort, publicPort) - { - Description = _appHost.Name - - }).ConfigureAwait(false); - - return true; + await CreatePortMap(device, _appHost.HttpsPort, _config.Configuration.PublicHttpsPort).ConfigureAwait(false); } catch (Exception ex) { - _logger.Error("Error creating port map: " + ex.Message); - - return false; } } - // As I said before, this method will be never invoked. You can remove it. - void NatUtility_DeviceLost(object sender, DeviceEventArgs e) + private Task CreatePortMap(INatDevice device, int privatePort, int publicPort) { - var device = e.Device; - _logger.Debug("NAT device lost: {0}", device.LocalAddress.ToString()); + _logger.Debug("Creating port map on local port {0} to public port {1} with device {2}", privatePort, publicPort, device.LocalAddress.ToString()); + + return device.CreatePortMap(new Mapping(Protocol.Tcp, privatePort, publicPort) + { + Description = _appHost.Name + }); } private bool _disposed = false; @@ -295,7 +281,6 @@ namespace Emby.Server.Implementations.EntryPoints { _disposed = true; DisposeNat(); - GC.SuppressFinalize(this); } private void DisposeNat() @@ -310,27 +295,24 @@ namespace Emby.Server.Implementations.EntryPoints _deviceDiscovery.DeviceDiscovered -= _deviceDiscovery_DeviceDiscovered; - try - { - // This is not a significant improvement - NatUtility.StopDiscovery(); - NatUtility.DeviceFound -= NatUtility_DeviceFound; - NatUtility.DeviceLost -= NatUtility_DeviceLost; - } - // Statements in try-block will no fail because StopDiscovery is a one-line - // method that was no chances to fail. - // public static void StopDiscovery () - // { - // searching.Reset(); - // } - // IMO you could remove the catch-block - catch (Exception ex) - { - _logger.ErrorException("Error stopping NAT Discovery", ex); - } - finally + var natManager = _natManager; + + if (natManager != null) { - _isStarted = false; + _natManager = null; + + using (natManager) + { + try + { + natManager.StopDiscovery(); + natManager.DeviceFound -= NatUtility_DeviceFound; + } + catch (Exception ex) + { + _logger.ErrorException("Error stopping NAT Discovery", ex); + } + } } } } diff --git a/Emby.Server.Implementations/EntryPoints/KeepServerAwake.cs b/Emby.Server.Implementations/EntryPoints/KeepServerAwake.cs index 221580681..8ae85e390 100644 --- a/Emby.Server.Implementations/EntryPoints/KeepServerAwake.cs +++ b/Emby.Server.Implementations/EntryPoints/KeepServerAwake.cs @@ -60,7 +60,6 @@ namespace Emby.Server.Implementations.EntryPoints _timer.Dispose(); _timer = null; } - GC.SuppressFinalize(this); } } } diff --git a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs index 0e771cbec..9a2ae34bc 100644 --- a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs +++ b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs @@ -52,7 +52,7 @@ namespace Emby.Server.Implementations.EntryPoints /// <summary> /// The library update duration /// </summary> - private const int LibraryUpdateDuration = 5000; + private const int LibraryUpdateDuration = 30000; private readonly IProviderManager _providerManager; @@ -315,41 +315,39 @@ namespace Emby.Server.Implementations.EntryPoints /// <param name="cancellationToken">The cancellation token.</param> private async void SendChangeNotifications(List<BaseItem> itemsAdded, List<BaseItem> itemsUpdated, List<BaseItem> itemsRemoved, List<Folder> foldersAddedTo, List<Folder> foldersRemovedFrom, CancellationToken cancellationToken) { - foreach (var user in _userManager.Users.ToList()) + var userIds = _sessionManager.Sessions + .Select(i => i.UserId) + .Where(i => !i.Equals(Guid.Empty)) + .Distinct() + .ToArray(); + + foreach (var userId in userIds) { - var id = user.Id; - var userSessions = _sessionManager.Sessions - .Where(u => u.UserId.HasValue && u.UserId.Value == id && u.SessionController != null && u.IsActive) - .ToList(); + LibraryUpdateInfo info; - if (userSessions.Count > 0) + try { - LibraryUpdateInfo info; - - try - { - info = GetLibraryUpdateInfo(itemsAdded, itemsUpdated, itemsRemoved, foldersAddedTo, - foldersRemovedFrom, id); - } - catch (Exception ex) - { - _logger.ErrorException("Error in GetLibraryUpdateInfo", ex); - return; - } - - foreach (var userSession in userSessions) - { - try - { - await userSession.SessionController.SendLibraryUpdateInfo(info, cancellationToken).ConfigureAwait(false); - } - catch (Exception ex) - { - _logger.ErrorException("Error sending LibraryChanged message", ex); - } - } + info = GetLibraryUpdateInfo(itemsAdded, itemsUpdated, itemsRemoved, foldersAddedTo, foldersRemovedFrom, userId); + } + catch (Exception ex) + { + _logger.ErrorException("Error in GetLibraryUpdateInfo", ex); + return; } + if (info.IsEmpty) + { + continue; + } + + try + { + await _sessionManager.SendMessageToUserSessions(new List<Guid> { userId }, "LibraryChanged", info, cancellationToken).ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.ErrorException("Error sending LibraryChanged message", ex); + } } } @@ -391,7 +389,7 @@ namespace Emby.Server.Implementations.EntryPoints private bool FilterItem(BaseItem item) { - if (!item.IsFolder && item.LocationType == LocationType.Virtual) + if (!item.IsFolder && !item.HasPathProtocol) { return false; } @@ -440,7 +438,7 @@ namespace Emby.Server.Implementations.EntryPoints // If the physical root changed, return the user root if (item is AggregateFolder) { - return new[] { user.RootFolder as T }; + return new[] { _libraryManager.GetUserRootFolder() as T }; } // Return it only if it's in the user's library @@ -458,7 +456,6 @@ namespace Emby.Server.Implementations.EntryPoints public void Dispose() { Dispose(true); - GC.SuppressFinalize(this); } /// <summary> @@ -474,10 +471,14 @@ namespace Emby.Server.Implementations.EntryPoints LibraryUpdateTimer.Dispose(); LibraryUpdateTimer = null; } - + _libraryManager.ItemAdded -= libraryManager_ItemAdded; _libraryManager.ItemUpdated -= libraryManager_ItemUpdated; _libraryManager.ItemRemoved -= libraryManager_ItemRemoved; + + _providerManager.RefreshCompleted -= _providerManager_RefreshCompleted; + _providerManager.RefreshStarted -= _providerManager_RefreshStarted; + _providerManager.RefreshProgress -= _providerManager_RefreshProgress; } } } diff --git a/Emby.Server.Implementations/EntryPoints/LoadRegistrations.cs b/Emby.Server.Implementations/EntryPoints/LoadRegistrations.cs deleted file mode 100644 index 21e075cf5..000000000 --- a/Emby.Server.Implementations/EntryPoints/LoadRegistrations.cs +++ /dev/null @@ -1,74 +0,0 @@ -using MediaBrowser.Common.Security; -using MediaBrowser.Controller.Plugins; -using MediaBrowser.Model.Logging; -using System; -using System.Threading.Tasks; -using MediaBrowser.Model.Threading; - -namespace Emby.Server.Implementations.EntryPoints -{ - /// <summary> - /// Class LoadRegistrations - /// </summary> - public class LoadRegistrations : IServerEntryPoint - { - /// <summary> - /// The _security manager - /// </summary> - private readonly ISecurityManager _securityManager; - - /// <summary> - /// The _logger - /// </summary> - private readonly ILogger _logger; - - private ITimer _timer; - private readonly ITimerFactory _timerFactory; - - /// <summary> - /// Initializes a new instance of the <see cref="LoadRegistrations" /> class. - /// </summary> - /// <param name="securityManager">The security manager.</param> - /// <param name="logManager">The log manager.</param> - public LoadRegistrations(ISecurityManager securityManager, ILogManager logManager, ITimerFactory timerFactory) - { - _securityManager = securityManager; - _timerFactory = timerFactory; - - _logger = logManager.GetLogger("Registration Loader"); - } - - /// <summary> - /// Runs this instance. - /// </summary> - public void Run() - { - _timer = _timerFactory.Create(s => LoadAllRegistrations(), null, TimeSpan.FromMilliseconds(100), TimeSpan.FromHours(12)); - } - - private async Task LoadAllRegistrations() - { - try - { - await _securityManager.LoadAllRegistrationInfo().ConfigureAwait(false); - } - catch (Exception ex) - { - _logger.ErrorException("Error loading registration info", ex); - } - } - - /// <summary> - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. - /// </summary> - public void Dispose() - { - if (_timer != null) - { - _timer.Dispose(); - _timer = null; - } - GC.SuppressFinalize(this); - } - } -} diff --git a/Emby.Server.Implementations/EntryPoints/RecordingNotifier.cs b/Emby.Server.Implementations/EntryPoints/RecordingNotifier.cs index f73b40b46..d41d76c6b 100644 --- a/Emby.Server.Implementations/EntryPoints/RecordingNotifier.cs +++ b/Emby.Server.Implementations/EntryPoints/RecordingNotifier.cs @@ -54,12 +54,16 @@ namespace Emby.Server.Implementations.EntryPoints private async void SendMessage(string name, TimerEventInfo info) { - var users = _userManager.Users.Where(i => i.Policy.EnableLiveTvAccess).Select(i => i.Id.ToString("N")).ToList(); + var users = _userManager.Users.Where(i => i.Policy.EnableLiveTvAccess).Select(i => i.Id).ToList(); try { await _sessionManager.SendMessageToUserSessions<TimerEventInfo>(users, name, info, CancellationToken.None); } + catch (ObjectDisposedException) + { + + } catch (Exception ex) { _logger.ErrorException("Error sending message", ex); @@ -72,7 +76,6 @@ namespace Emby.Server.Implementations.EntryPoints _liveTvManager.SeriesTimerCancelled -= _liveTvManager_SeriesTimerCancelled; _liveTvManager.TimerCreated -= _liveTvManager_TimerCreated; _liveTvManager.SeriesTimerCreated -= _liveTvManager_SeriesTimerCreated; - GC.SuppressFinalize(this); } } } diff --git a/Emby.Server.Implementations/EntryPoints/ServerEventNotifier.cs b/Emby.Server.Implementations/EntryPoints/ServerEventNotifier.cs index 514321e20..e5748989e 100644 --- a/Emby.Server.Implementations/EntryPoints/ServerEventNotifier.cs +++ b/Emby.Server.Implementations/EntryPoints/ServerEventNotifier.cs @@ -22,11 +22,6 @@ namespace Emby.Server.Implementations.EntryPoints public class ServerEventNotifier : IServerEntryPoint { /// <summary> - /// The _server manager - /// </summary> - private readonly IServerManager _serverManager; - - /// <summary> /// The _user manager /// </summary> private readonly IUserManager _userManager; @@ -47,23 +42,21 @@ namespace Emby.Server.Implementations.EntryPoints private readonly ITaskManager _taskManager; private readonly ISessionManager _sessionManager; - private readonly ISyncManager _syncManager; - public ServerEventNotifier(IServerManager serverManager, IServerApplicationHost appHost, IUserManager userManager, IInstallationManager installationManager, ITaskManager taskManager, ISessionManager sessionManager, ISyncManager syncManager) + public ServerEventNotifier(IServerApplicationHost appHost, IUserManager userManager, IInstallationManager installationManager, ITaskManager taskManager, ISessionManager sessionManager) { - _serverManager = serverManager; _userManager = userManager; _installationManager = installationManager; _appHost = appHost; _taskManager = taskManager; _sessionManager = sessionManager; - _syncManager = syncManager; } public void Run() { _userManager.UserDeleted += userManager_UserDeleted; _userManager.UserUpdated += userManager_UserUpdated; + _userManager.UserPolicyUpdated += _userManager_UserPolicyUpdated; _userManager.UserConfigurationUpdated += _userManager_UserConfigurationUpdated; _appHost.HasPendingRestartChanged += kernel_HasPendingRestartChanged; @@ -75,43 +68,31 @@ namespace Emby.Server.Implementations.EntryPoints _installationManager.PackageInstallationFailed += _installationManager_PackageInstallationFailed; _taskManager.TaskCompleted += _taskManager_TaskCompleted; - _syncManager.SyncJobCreated += _syncManager_SyncJobCreated; - _syncManager.SyncJobCancelled += _syncManager_SyncJobCancelled; - } - - void _syncManager_SyncJobCancelled(object sender, GenericEventArgs<SyncJob> e) - { - _sessionManager.SendMessageToUserDeviceSessions(e.Argument.TargetId, "SyncJobCancelled", e.Argument, CancellationToken.None); - } - - void _syncManager_SyncJobCreated(object sender, GenericEventArgs<SyncJobCreationResult> e) - { - _sessionManager.SendMessageToUserDeviceSessions(e.Argument.Job.TargetId, "SyncJobCreated", e.Argument, CancellationToken.None); } void _installationManager_PackageInstalling(object sender, InstallationEventArgs e) { - _serverManager.SendWebSocketMessage("PackageInstalling", e.InstallationInfo); + SendMessageToAdminSessions("PackageInstalling", e.InstallationInfo); } void _installationManager_PackageInstallationCancelled(object sender, InstallationEventArgs e) { - _serverManager.SendWebSocketMessage("PackageInstallationCancelled", e.InstallationInfo); + SendMessageToAdminSessions("PackageInstallationCancelled", e.InstallationInfo); } void _installationManager_PackageInstallationCompleted(object sender, InstallationEventArgs e) { - _serverManager.SendWebSocketMessage("PackageInstallationCompleted", e.InstallationInfo); + SendMessageToAdminSessions("PackageInstallationCompleted", e.InstallationInfo); } void _installationManager_PackageInstallationFailed(object sender, InstallationFailedEventArgs e) { - _serverManager.SendWebSocketMessage("PackageInstallationFailed", e.InstallationInfo); + SendMessageToAdminSessions("PackageInstallationFailed", e.InstallationInfo); } void _taskManager_TaskCompleted(object sender, TaskCompletionEventArgs e) { - _serverManager.SendWebSocketMessage("ScheduledTaskEnded", e.Result); + SendMessageToAdminSessions("ScheduledTaskEnded", e.Result); } /// <summary> @@ -121,7 +102,7 @@ namespace Emby.Server.Implementations.EntryPoints /// <param name="e">The e.</param> void InstallationManager_PluginUninstalled(object sender, GenericEventArgs<IPlugin> e) { - _serverManager.SendWebSocketMessage("PluginUninstalled", e.Argument.GetPluginInfo()); + SendMessageToAdminSessions("PluginUninstalled", e.Argument.GetPluginInfo()); } /// <summary> @@ -156,6 +137,13 @@ namespace Emby.Server.Implementations.EntryPoints SendMessageToUserSession(e.Argument, "UserDeleted", e.Argument.Id.ToString("N")); } + void _userManager_UserPolicyUpdated(object sender, GenericEventArgs<User> e) + { + var dto = _userManager.GetUserDto(e.Argument); + + SendMessageToUserSession(e.Argument, "UserPolicyUpdated", dto); + } + void _userManager_UserConfigurationUpdated(object sender, GenericEventArgs<User> e) { var dto = _userManager.GetUserDto(e.Argument); @@ -163,9 +151,36 @@ namespace Emby.Server.Implementations.EntryPoints SendMessageToUserSession(e.Argument, "UserConfigurationUpdated", dto); } + private async void SendMessageToAdminSessions<T>(string name, T data) + { + try + { + await _sessionManager.SendMessageToAdminSessions(name, data, CancellationToken.None); + } + catch (ObjectDisposedException) + { + + } + catch (Exception) + { + //Logger.ErrorException("Error sending message", ex); + } + } + private async void SendMessageToUserSession<T>(User user, string name, T data) { - await _sessionManager.SendMessageToUserSessions(new List<string> { user.Id.ToString("N") }, name, data, CancellationToken.None); + try + { + await _sessionManager.SendMessageToUserSessions(new List<Guid> { user.Id }, name, data, CancellationToken.None); + } + catch (ObjectDisposedException) + { + + } + catch (Exception) + { + //Logger.ErrorException("Error sending message", ex); + } } /// <summary> @@ -174,7 +189,6 @@ namespace Emby.Server.Implementations.EntryPoints public void Dispose() { Dispose(true); - GC.SuppressFinalize(this); } /// <summary> @@ -187,6 +201,7 @@ namespace Emby.Server.Implementations.EntryPoints { _userManager.UserDeleted -= userManager_UserDeleted; _userManager.UserUpdated -= userManager_UserUpdated; + _userManager.UserPolicyUpdated -= _userManager_UserPolicyUpdated; _userManager.UserConfigurationUpdated -= _userManager_UserConfigurationUpdated; _installationManager.PluginUninstalled -= InstallationManager_PluginUninstalled; @@ -196,8 +211,6 @@ namespace Emby.Server.Implementations.EntryPoints _installationManager.PackageInstallationFailed -= _installationManager_PackageInstallationFailed; _appHost.HasPendingRestartChanged -= kernel_HasPendingRestartChanged; - _syncManager.SyncJobCreated -= _syncManager_SyncJobCreated; - _syncManager.SyncJobCancelled -= _syncManager_SyncJobCancelled; } } } diff --git a/Emby.Server.Implementations/EntryPoints/StartupWizard.cs b/Emby.Server.Implementations/EntryPoints/StartupWizard.cs index 103b4b321..6d73f98ad 100644 --- a/Emby.Server.Implementations/EntryPoints/StartupWizard.cs +++ b/Emby.Server.Implementations/EntryPoints/StartupWizard.cs @@ -1,5 +1,4 @@ -using System; -using Emby.Server.Implementations.Browser; +using Emby.Server.Implementations.Browser; using MediaBrowser.Controller; using MediaBrowser.Controller.Plugins; using MediaBrowser.Model.Logging; @@ -40,17 +39,17 @@ namespace Emby.Server.Implementations.EntryPoints return; } - if (_appHost.IsFirstRun) + if (!_config.Configuration.IsStartupWizardCompleted) { - BrowserLauncher.OpenDashboardPage("wizardstart.html", _appHost); + BrowserLauncher.OpenWebApp(_appHost); } - else if (_config.Configuration.IsStartupWizardCompleted && _config.Configuration.AutoRunWebApp) + else if (_config.Configuration.AutoRunWebApp) { var options = ((ApplicationHost)_appHost).StartupOptions; if (!options.ContainsOption("-noautorunwebapp")) { - BrowserLauncher.OpenDashboardPage("index.html", _appHost); + BrowserLauncher.OpenWebApp(_appHost); } } } @@ -60,7 +59,6 @@ namespace Emby.Server.Implementations.EntryPoints /// </summary> public void Dispose() { - GC.SuppressFinalize(this); } } }
\ No newline at end of file diff --git a/Emby.Server.Implementations/EntryPoints/SystemEvents.cs b/Emby.Server.Implementations/EntryPoints/SystemEvents.cs index 08f3edb3d..e27de8967 100644 --- a/Emby.Server.Implementations/EntryPoints/SystemEvents.cs +++ b/Emby.Server.Implementations/EntryPoints/SystemEvents.cs @@ -34,7 +34,6 @@ namespace Emby.Server.Implementations.EntryPoints public void Dispose() { _systemEvents.SystemShutdown -= _systemEvents_SystemShutdown; - GC.SuppressFinalize(this); } } } diff --git a/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs b/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs index d04df0d2b..5edc5fade 100644 --- a/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs +++ b/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs @@ -45,6 +45,9 @@ namespace Emby.Server.Implementations.EntryPoints /// </summary> public void Run() { + // ToDo: Fix This + return; + var udpServer = new UdpServer(_logger, _appHost, _json, _socketFactory); try @@ -65,7 +68,6 @@ namespace Emby.Server.Implementations.EntryPoints public void Dispose() { Dispose(true); - GC.SuppressFinalize(this); } /// <summary> diff --git a/Emby.Server.Implementations/EntryPoints/UsageEntryPoint.cs b/Emby.Server.Implementations/EntryPoints/UsageEntryPoint.cs index 11e806b0c..97feb32c0 100644 --- a/Emby.Server.Implementations/EntryPoints/UsageEntryPoint.cs +++ b/Emby.Server.Implementations/EntryPoints/UsageEntryPoint.cs @@ -1,5 +1,4 @@ -using MediaBrowser.Common; -using MediaBrowser.Common.Extensions; +using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Plugins; @@ -61,17 +60,29 @@ namespace Emby.Server.Implementations.EntryPoints var key = string.Join("_", keys.ToArray(keys.Count)).GetMD5(); - _apps.GetOrAdd(key, guid => GetNewClientInfo(session)); + ClientInfo info; + if (!_apps.TryGetValue(key, out info)) + { + info = new ClientInfo + { + AppName = session.Client, + AppVersion = session.ApplicationVersion, + DeviceName = session.DeviceName, + DeviceId = session.DeviceId + }; + + _apps[key] = info; + + if (_config.Configuration.EnableAnonymousUsageReporting) + { + Task.Run(() => ReportNewSession(info)); + } + } } } - private async void ReportNewSession(ClientInfo client) + private async Task ReportNewSession(ClientInfo client) { - if (!_config.Configuration.EnableAnonymousUsageReporting) - { - return; - } - try { await new UsageReporter(_applicationHost, _httpClient, _logger) @@ -80,25 +91,10 @@ namespace Emby.Server.Implementations.EntryPoints } catch (Exception ex) { - _logger.ErrorException("Error sending anonymous usage statistics.", ex); + //_logger.ErrorException("Error sending anonymous usage statistics.", ex); } } - private ClientInfo GetNewClientInfo(SessionInfo session) - { - var info = new ClientInfo - { - AppName = session.Client, - AppVersion = session.ApplicationVersion, - DeviceName = session.DeviceName, - DeviceId = session.DeviceId - }; - - ReportNewSession(info); - - return info; - } - public async void Run() { await Task.Delay(5000).ConfigureAwait(false); @@ -123,14 +119,13 @@ namespace Emby.Server.Implementations.EntryPoints } catch (Exception ex) { - _logger.ErrorException("Error sending anonymous usage statistics.", ex); + //_logger.ErrorException("Error sending anonymous usage statistics.", ex); } } public void Dispose() { _sessionManager.SessionStarted -= _sessionManager_SessionStarted; - GC.SuppressFinalize(this); } } } diff --git a/Emby.Server.Implementations/EntryPoints/UsageReporter.cs b/Emby.Server.Implementations/EntryPoints/UsageReporter.cs index deee8d64b..86b335b77 100644 --- a/Emby.Server.Implementations/EntryPoints/UsageReporter.cs +++ b/Emby.Server.Implementations/EntryPoints/UsageReporter.cs @@ -70,7 +70,7 @@ namespace Emby.Server.Implementations.EntryPoints public async Task ReportAppUsage(ClientInfo app, CancellationToken cancellationToken) { - if (string.IsNullOrWhiteSpace(app.DeviceId)) + if (string.IsNullOrEmpty(app.DeviceId)) { throw new ArgumentException("Client info must have a device Id"); } diff --git a/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs b/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs index 13c72bf3c..36e29e46a 100644 --- a/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs +++ b/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs @@ -27,7 +27,7 @@ namespace Emby.Server.Implementations.EntryPoints private readonly ITimerFactory _timerFactory; private const int UpdateDuration = 500; - private readonly Dictionary<Guid, List<IHasUserData>> _changedItems = new Dictionary<Guid, List<IHasUserData>>(); + private readonly Dictionary<Guid, List<BaseItem>> _changedItems = new Dictionary<Guid, List<BaseItem>>(); public UserDataChangeNotifier(IUserDataManager userDataManager, ISessionManager sessionManager, ILogger logger, IUserManager userManager, ITimerFactory timerFactory) { @@ -62,22 +62,22 @@ namespace Emby.Server.Implementations.EntryPoints UpdateTimer.Change(UpdateDuration, Timeout.Infinite); } - List<IHasUserData> keys; + List<BaseItem> keys; if (!_changedItems.TryGetValue(e.UserId, out keys)) { - keys = new List<IHasUserData>(); + keys = new List<BaseItem>(); _changedItems[e.UserId] = keys; } keys.Add(e.Item); - var baseItem = e.Item as BaseItem; + var baseItem = e.Item; // Go up one level for indicators if (baseItem != null) { - var parent = baseItem.IsOwnedItem ? baseItem.GetOwner() : baseItem.GetParent(); + var parent = baseItem.GetOwner() ?? baseItem.GetParent(); if (parent != null) { @@ -105,50 +105,41 @@ namespace Emby.Server.Implementations.EntryPoints } } - private async Task SendNotifications(IEnumerable<KeyValuePair<Guid, List<IHasUserData>>> changes, CancellationToken cancellationToken) + private async Task SendNotifications(List<KeyValuePair<Guid, List<BaseItem>>> changes, CancellationToken cancellationToken) { foreach (var pair in changes) { - var userId = pair.Key; - var userSessions = _sessionManager.Sessions - .Where(u => u.ContainsUser(userId) && u.SessionController != null && u.IsActive) - .ToList(); + await SendNotifications(pair.Key, pair.Value, cancellationToken).ConfigureAwait(false); + } + } + + private Task SendNotifications(Guid userId, List<BaseItem> changedItems, CancellationToken cancellationToken) + { + return _sessionManager.SendMessageToUserSessions(new List<Guid> { userId }, "UserDataChanged", () => GetUserDataChangeInfo(userId, changedItems), cancellationToken); + } + + private UserDataChangeInfo GetUserDataChangeInfo(Guid userId, List<BaseItem> changedItems) + { + var user = _userManager.GetUserById(userId); - if (userSessions.Count > 0) + var dtoList = changedItems + .DistinctBy(i => i.Id) + .Select(i => { - var user = _userManager.GetUserById(userId); - - var dtoList = pair.Value - .DistinctBy(i => i.Id) - .Select(i => - { - var dto = _userDataManager.GetUserDataDto(i, user); - dto.ItemId = i.Id.ToString("N"); - return dto; - }) - .ToArray(); - - var info = new UserDataChangeInfo - { - UserId = userId.ToString("N"), + var dto = _userDataManager.GetUserDataDto(i, user); + dto.ItemId = i.Id.ToString("N"); + return dto; + }) + .ToArray(); - UserDataList = dtoList - }; + var userIdString = userId.ToString("N"); - foreach (var userSession in userSessions) - { - try - { - await userSession.SessionController.SendUserDataChangeInfo(info, cancellationToken).ConfigureAwait(false); - } - catch (Exception ex) - { - _logger.ErrorException("Error sending UserDataChanged message", ex); - } - } - } + return new UserDataChangeInfo + { + UserId = userIdString, - } + UserDataList = dtoList + }; } public void Dispose() @@ -160,7 +151,6 @@ namespace Emby.Server.Implementations.EntryPoints } _userDataManager.UserDataSaved -= _userDataManager_UserDataSaved; - GC.SuppressFinalize(this); } } } diff --git a/Emby.Server.Implementations/EnvironmentInfo/EnvironmentInfo.cs b/Emby.Server.Implementations/EnvironmentInfo/EnvironmentInfo.cs index f86279f37..583e93706 100644 --- a/Emby.Server.Implementations/EnvironmentInfo/EnvironmentInfo.cs +++ b/Emby.Server.Implementations/EnvironmentInfo/EnvironmentInfo.cs @@ -82,11 +82,6 @@ namespace Emby.Server.Implementations.EnvironmentInfo return Environment.GetEnvironmentVariable(name); } - public virtual string GetUserId() - { - return null; - } - public string StackTrace { get { return Environment.StackTrace; } diff --git a/Emby.Server.Implementations/FFMpeg/FFMpegInstallInfo.cs b/Emby.Server.Implementations/FFMpeg/FFMpegInstallInfo.cs index 1d769acec..a1080a839 100644 --- a/Emby.Server.Implementations/FFMpeg/FFMpegInstallInfo.cs +++ b/Emby.Server.Implementations/FFMpeg/FFMpegInstallInfo.cs @@ -7,11 +7,9 @@ namespace Emby.Server.Implementations.FFMpeg public string FFMpegFilename { get; set; } public string FFProbeFilename { get; set; } public string ArchiveType { get; set; } - public string[] DownloadUrls { get; set; } public FFMpegInstallInfo() { - DownloadUrls = new string[] { }; Version = "Path"; FFMpegFilename = "ffmpeg"; FFProbeFilename = "ffprobe"; diff --git a/Emby.Server.Implementations/FFMpeg/FFMpegLoader.cs b/Emby.Server.Implementations/FFMpeg/FFMpegLoader.cs index 9f4cd05fa..fe1df0953 100644 --- a/Emby.Server.Implementations/FFMpeg/FFMpegLoader.cs +++ b/Emby.Server.Implementations/FFMpeg/FFMpegLoader.cs @@ -6,10 +6,6 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Emby.Server.Implementations; -using Emby.Server.Implementations.FFMpeg; namespace Emby.Server.Implementations.FFMpeg { @@ -32,7 +28,7 @@ namespace Emby.Server.Implementations.FFMpeg _ffmpegInstallInfo = ffmpegInstallInfo; } - public async Task<FFMpegInfo> GetFFMpegInfo(StartupOptions options, IProgress<double> progress) + public FFMpegInfo GetFFMpegInfo(StartupOptions options) { var customffMpegPath = options.GetOption("-ffmpeg"); var customffProbePath = options.GetOption("-ffprobe"); @@ -49,8 +45,9 @@ namespace Emby.Server.Implementations.FFMpeg var downloadInfo = _ffmpegInstallInfo; - var prebuiltffmpeg = Path.Combine(_appPaths.ProgramSystemPath, downloadInfo.FFMpegFilename); - var prebuiltffprobe = Path.Combine(_appPaths.ProgramSystemPath, downloadInfo.FFProbeFilename); + var prebuiltFolder = _appPaths.ProgramSystemPath; + var prebuiltffmpeg = Path.Combine(prebuiltFolder, downloadInfo.FFMpegFilename); + var prebuiltffprobe = Path.Combine(prebuiltFolder, downloadInfo.FFProbeFilename); if (_fileSystem.FileExists(prebuiltffmpeg) && _fileSystem.FileExists(prebuiltffprobe)) { return new FFMpegInfo @@ -90,11 +87,7 @@ namespace Emby.Server.Implementations.FFMpeg // No older version. Need to download and block until complete if (existingVersion == null) { - var success = await DownloadFFMpeg(downloadInfo, versionedDirectoryPath, progress).ConfigureAwait(false); - if (!success) - { - return new FFMpegInfo(); - } + return new FFMpegInfo(); } else { @@ -144,99 +137,5 @@ namespace Emby.Server.Implementations.FFMpeg return null; } - - private async Task<bool> DownloadFFMpeg(FFMpegInstallInfo downloadinfo, string directory, IProgress<double> progress) - { - foreach (var url in downloadinfo.DownloadUrls) - { - progress.Report(0); - - try - { - var tempFile = await _httpClient.GetTempFile(new HttpRequestOptions - { - Url = url, - CancellationToken = CancellationToken.None, - Progress = progress - - }).ConfigureAwait(false); - - ExtractFFMpeg(downloadinfo, tempFile, directory); - return true; - } - catch (Exception ex) - { - _logger.ErrorException("Error downloading {0}", ex, url); - } - } - return false; - } - - private void ExtractFFMpeg(FFMpegInstallInfo downloadinfo, string tempFile, string targetFolder) - { - _logger.Info("Extracting ffmpeg from {0}", tempFile); - - var tempFolder = Path.Combine(_appPaths.TempDirectory, Guid.NewGuid().ToString()); - - _fileSystem.CreateDirectory(tempFolder); - - try - { - ExtractArchive(downloadinfo, tempFile, tempFolder); - - var files = _fileSystem.GetFilePaths(tempFolder, true) - .ToList(); - - foreach (var file in files.Where(i => - { - var filename = Path.GetFileName(i); - - return - string.Equals(filename, downloadinfo.FFProbeFilename, StringComparison.OrdinalIgnoreCase) || - string.Equals(filename, downloadinfo.FFMpegFilename, StringComparison.OrdinalIgnoreCase); - })) - { - var targetFile = Path.Combine(targetFolder, Path.GetFileName(file)); - _fileSystem.CopyFile(file, targetFile, true); - SetFilePermissions(targetFile); - } - } - finally - { - DeleteFile(tempFile); - } - } - - private void SetFilePermissions(string path) - { - _fileSystem.SetExecutable(path); - } - - private void ExtractArchive(FFMpegInstallInfo downloadinfo, string archivePath, string targetPath) - { - _logger.Info("Extracting {0} to {1}", archivePath, targetPath); - - if (string.Equals(downloadinfo.ArchiveType, "7z", StringComparison.OrdinalIgnoreCase)) - { - _zipClient.ExtractAllFrom7z(archivePath, targetPath, true); - } - else if (string.Equals(downloadinfo.ArchiveType, "gz", StringComparison.OrdinalIgnoreCase)) - { - _zipClient.ExtractAllFromTar(archivePath, targetPath, true); - } - } - - private void DeleteFile(string path) - { - try - { - _fileSystem.DeleteFile(path); - } - catch (IOException ex) - { - _logger.ErrorException("Error deleting temp file {0}", ex, path); - } - } - } } diff --git a/Emby.Server.Implementations/HttpClientManager/HttpClientManager.cs b/Emby.Server.Implementations/HttpClientManager/HttpClientManager.cs index 4a9e417f2..d53606e87 100644 --- a/Emby.Server.Implementations/HttpClientManager/HttpClientManager.cs +++ b/Emby.Server.Implementations/HttpClientManager/HttpClientManager.cs @@ -41,13 +41,12 @@ namespace Emby.Server.Implementations.HttpClientManager private readonly IApplicationPaths _appPaths; private readonly IFileSystem _fileSystem; - private readonly IMemoryStreamFactory _memoryStreamProvider; private readonly Func<string> _defaultUserAgentFn; /// <summary> /// Initializes a new instance of the <see cref="HttpClientManager" /> class. /// </summary> - public HttpClientManager(IApplicationPaths appPaths, ILogger logger, IFileSystem fileSystem, IMemoryStreamFactory memoryStreamProvider, Func<string> defaultUserAgentFn) + public HttpClientManager(IApplicationPaths appPaths, ILogger logger, IFileSystem fileSystem, Func<string> defaultUserAgentFn) { if (appPaths == null) { @@ -60,7 +59,6 @@ namespace Emby.Server.Implementations.HttpClientManager _logger = logger; _fileSystem = fileSystem; - _memoryStreamProvider = memoryStreamProvider; _appPaths = appPaths; _defaultUserAgentFn = defaultUserAgentFn; @@ -310,7 +308,7 @@ namespace Emby.Server.Implementations.HttpClientManager { using (var stream = _fileSystem.GetFileStream(responseCachePath, FileOpenMode.Open, FileAccessMode.Read, FileShareMode.Read, true)) { - var memoryStream = _memoryStreamProvider.CreateNew(); + var memoryStream = new MemoryStream(); await stream.CopyToAsync(memoryStream).ConfigureAwait(false); memoryStream.Position = 0; @@ -343,7 +341,7 @@ namespace Emby.Server.Implementations.HttpClientManager using (var responseStream = response.Content) { - var memoryStream = _memoryStreamProvider.CreateNew(); + var memoryStream = new MemoryStream(); await responseStream.CopyToAsync(memoryStream).ConfigureAwait(false); memoryStream.Position = 0; @@ -458,7 +456,7 @@ namespace Emby.Server.Implementations.HttpClientManager using (var stream = httpResponse.GetResponseStream()) { - var memoryStream = _memoryStreamProvider.CreateNew(); + var memoryStream = new MemoryStream(); await stream.CopyToAsync(memoryStream).ConfigureAwait(false); @@ -636,7 +634,7 @@ namespace Emby.Server.Implementations.HttpClientManager { using (var fs = _fileSystem.GetFileStream(tempFile, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read, true)) { - await StreamHelper.CopyToAsync(httpResponse.GetResponseStream(), fs, StreamDefaults.DefaultCopyToBufferSize, options.Progress, contentLength.Value, options.CancellationToken).ConfigureAwait(false); + await httpResponse.GetResponseStream().CopyToAsync(fs, StreamDefaults.DefaultCopyToBufferSize, options.CancellationToken).ConfigureAwait(false); } } diff --git a/Emby.Server.Implementations/HttpServer/FileWriter.cs b/Emby.Server.Implementations/HttpServer/FileWriter.cs index aa679e1b9..353ba5282 100644 --- a/Emby.Server.Implementations/HttpServer/FileWriter.cs +++ b/Emby.Server.Implementations/HttpServer/FileWriter.cs @@ -7,6 +7,7 @@ using System.Threading.Tasks; using MediaBrowser.Model.IO; using MediaBrowser.Model.Logging; using MediaBrowser.Model.Services; +using System.Linq; namespace Emby.Server.Implementations.HttpServer { @@ -147,6 +148,13 @@ namespace Emby.Server.Implementations.HttpServer } } + private string[] SkipLogExtensions = new string[] + { + ".js", + ".html", + ".css" + }; + public async Task WriteToAsync(IResponse response, CancellationToken cancellationToken) { try @@ -157,17 +165,24 @@ namespace Emby.Server.Implementations.HttpServer return; } + var path = Path; + if (string.IsNullOrWhiteSpace(RangeHeader) || (RangeStart <= 0 && RangeEnd >= TotalContentLength - 1)) { - Logger.Info("Transmit file {0}", Path); + var extension = System.IO.Path.GetExtension(path); + + if (extension == null || !SkipLogExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase)) + { + Logger.Debug("Transmit file {0}", path); + } //var count = FileShare == FileShareMode.ReadWrite ? TotalContentLength : 0; - await response.TransmitFile(Path, 0, 0, FileShare, cancellationToken).ConfigureAwait(false); + await response.TransmitFile(path, 0, 0, FileShare, cancellationToken).ConfigureAwait(false); return; } - await response.TransmitFile(Path, RangeStart, RangeLength, FileShare, cancellationToken).ConfigureAwait(false); + await response.TransmitFile(path, RangeStart, RangeLength, FileShare, cancellationToken).ConfigureAwait(false); } finally { diff --git a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs index 937eb8029..0093258e3 100644 --- a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs +++ b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs @@ -12,7 +12,6 @@ using System.Security.Cryptography.X509Certificates; using System.Text; using System.Threading; using System.Threading.Tasks; -using Emby.Server.Implementations.HttpServer.SocketSharp; using Emby.Server.Implementations.Services; using MediaBrowser.Common.Net; using MediaBrowser.Common.Security; @@ -25,6 +24,10 @@ using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Services; using MediaBrowser.Model.System; using MediaBrowser.Model.Text; +using System.Net.Sockets; +using Emby.Server.Implementations.Net; +using MediaBrowser.Common.Events; +using MediaBrowser.Model.Events; namespace Emby.Server.Implementations.HttpServer { @@ -35,64 +38,47 @@ namespace Emby.Server.Implementations.HttpServer private readonly ILogger _logger; public string[] UrlPrefixes { get; private set; } - private readonly List<IService> _restServices = new List<IService>(); - private IHttpListener _listener; - public event EventHandler<WebSocketConnectEventArgs> WebSocketConnected; - public event EventHandler<WebSocketConnectingEventArgs> WebSocketConnecting; + public event EventHandler<GenericEventArgs<IWebSocketConnection>> WebSocketConnected; private readonly IServerConfigurationManager _config; private readonly INetworkManager _networkManager; - private readonly IMemoryStreamFactory _memoryStreamProvider; private readonly IServerApplicationHost _appHost; private readonly ITextEncoding _textEncoding; - private readonly ISocketFactory _socketFactory; - private readonly ICryptoProvider _cryptoProvider; - private readonly IFileSystem _fileSystem; private readonly IJsonSerializer _jsonSerializer; private readonly IXmlSerializer _xmlSerializer; - private readonly X509Certificate _certificate; - private readonly IEnvironmentInfo _environment; private readonly Func<Type, Func<string, object>> _funcParseFn; - private readonly bool _enableDualModeSockets; - public Action<IRequest, IResponse, object>[] RequestFilters { get; set; } public Action<IRequest, IResponse, object>[] ResponseFilters { get; set; } private readonly Dictionary<Type, Type> ServiceOperationsMap = new Dictionary<Type, Type>(); public static HttpListenerHost Instance { get; protected set; } + private IWebSocketListener[] _webSocketListeners = Array.Empty<IWebSocketListener>(); + private readonly List<IWebSocketConnection> _webSocketConnections = new List<IWebSocketConnection>(); + public HttpListenerHost(IServerApplicationHost applicationHost, ILogger logger, IServerConfigurationManager config, - string serviceName, - string defaultRedirectPath, INetworkManager networkManager, IMemoryStreamFactory memoryStreamProvider, ITextEncoding textEncoding, ISocketFactory socketFactory, ICryptoProvider cryptoProvider, IJsonSerializer jsonSerializer, IXmlSerializer xmlSerializer, IEnvironmentInfo environment, X509Certificate certificate, Func<Type, Func<string, object>> funcParseFn, bool enableDualModeSockets, IFileSystem fileSystem) + string defaultRedirectPath, INetworkManager networkManager, ITextEncoding textEncoding, IJsonSerializer jsonSerializer, IXmlSerializer xmlSerializer, Func<Type, Func<string, object>> funcParseFn) { Instance = this; _appHost = applicationHost; DefaultRedirectPath = defaultRedirectPath; _networkManager = networkManager; - _memoryStreamProvider = memoryStreamProvider; _textEncoding = textEncoding; - _socketFactory = socketFactory; - _cryptoProvider = cryptoProvider; _jsonSerializer = jsonSerializer; _xmlSerializer = xmlSerializer; - _environment = environment; - _certificate = certificate; - _funcParseFn = funcParseFn; - _enableDualModeSockets = enableDualModeSockets; - _fileSystem = fileSystem; _config = config; _logger = logger; + _funcParseFn = funcParseFn; - RequestFilters = new Action<IRequest, IResponse, object>[] { }; ResponseFilters = new Action<IRequest, IResponse, object>[] { }; } @@ -140,12 +126,6 @@ namespace Emby.Server.Implementations.HttpServer attribute.RequestFilter(req, res, requestDto); } - //Exec global filters - foreach (var requestFilter in RequestFilters) - { - requestFilter(req, res, requestDto); - } - //Exec remaining RequestFilter attributes with Priority >= 0 for (; i < count && attributes[i].Priority >= 0; i++) { @@ -181,45 +161,38 @@ namespace Emby.Server.Implementations.HttpServer return attributes; } - private IHttpListener GetListener() - { - //return new KestrelHost.KestrelListener(_logger, _environment, _fileSystem); - - return new WebSocketSharpListener(_logger, - _certificate, - _memoryStreamProvider, - _textEncoding, - _networkManager, - _socketFactory, - _cryptoProvider, - _enableDualModeSockets, - _fileSystem, - _environment); - } - - private void OnWebSocketConnecting(WebSocketConnectingEventArgs args) + private void OnWebSocketConnected(WebSocketConnectEventArgs e) { if (_disposed) { return; } - if (WebSocketConnecting != null) + var connection = new WebSocketConnection(e.WebSocket, e.Endpoint, _jsonSerializer, _logger, _textEncoding) { - WebSocketConnecting(this, args); - } - } + OnReceive = ProcessWebSocketMessageReceived, + Url = e.Url, + QueryString = e.QueryString ?? new QueryParamCollection() + }; - private void OnWebSocketConnected(WebSocketConnectEventArgs args) - { - if (_disposed) + connection.Closed += Connection_Closed; + + lock (_webSocketConnections) { - return; + _webSocketConnections.Add(connection); } if (WebSocketConnected != null) { - WebSocketConnected(this, args); + EventHelper.FireEventIfNotNull(WebSocketConnected, this, new GenericEventArgs<IWebSocketConnection>(connection), _logger); + } + } + + private void Connection_Closed(object sender, EventArgs e) + { + lock (_webSocketConnections) + { + _webSocketConnections.Remove((IWebSocketConnection)sender); } } @@ -271,16 +244,20 @@ namespace Emby.Server.Implementations.HttpServer return statusCode; } - private void ErrorHandler(Exception ex, IRequest httpReq, bool logException = true) + private async Task ErrorHandler(Exception ex, IRequest httpReq, bool logExceptionStackTrace, bool logExceptionMessage) { try { ex = GetActualException(ex); - if (logException) + if (logExceptionStackTrace) { _logger.ErrorException("Error processing request", ex); } + else if (logExceptionMessage) + { + _logger.Error(ex.Message); + } var httpRes = httpReq.Response; @@ -293,7 +270,7 @@ namespace Emby.Server.Implementations.HttpServer httpRes.StatusCode = statusCode; httpRes.ContentType = "text/html"; - Write(httpRes, ex.Message); + await Write(httpRes, NormalizeExceptionMessage(ex.Message)).ConfigureAwait(false); } catch { @@ -301,11 +278,46 @@ namespace Emby.Server.Implementations.HttpServer } } + private string NormalizeExceptionMessage(string msg) + { + if (msg == null) + { + return string.Empty; + } + + // Strip any information we don't want to reveal + + msg = msg.Replace(_config.ApplicationPaths.ProgramSystemPath, string.Empty, StringComparison.OrdinalIgnoreCase); + msg = msg.Replace(_config.ApplicationPaths.ProgramDataPath, string.Empty, StringComparison.OrdinalIgnoreCase); + + return msg; + } + /// <summary> /// Shut down the Web Service /// </summary> public void Stop() { + List<IWebSocketConnection> connections; + + lock (_webSocketConnections) + { + connections = _webSocketConnections.ToList(); + _webSocketConnections.Clear(); + } + + foreach (var connection in connections) + { + try + { + connection.Dispose(); + } + catch + { + + } + } + if (_listener != null) { _logger.Info("Stopping HttpListener..."); @@ -329,9 +341,9 @@ namespace Emby.Server.Implementations.HttpServer { var extension = GetExtension(url); - if (string.IsNullOrWhiteSpace(extension) || !_skipLogExtensions.ContainsKey(extension)) + if (string.IsNullOrEmpty(extension) || !_skipLogExtensions.ContainsKey(extension)) { - if (string.IsNullOrWhiteSpace(localPath) || localPath.IndexOf("system/ping", StringComparison.OrdinalIgnoreCase) == -1) + if (string.IsNullOrEmpty(localPath) || localPath.IndexOf("system/ping", StringComparison.OrdinalIgnoreCase) == -1) { return true; } @@ -422,12 +434,53 @@ namespace Emby.Server.Implementations.HttpServer return true; } + private bool ValidateRequest(string remoteIp, bool isLocal) + { + if (isLocal) + { + return true; + } + + if (_config.Configuration.EnableRemoteAccess) + { + var addressFilter = _config.Configuration.RemoteIPFilter.Where(i => !string.IsNullOrWhiteSpace(i)).ToArray(); + + if (addressFilter.Length > 0 && !_networkManager.IsInLocalNetwork(remoteIp)) + { + if (_config.Configuration.IsRemoteIPFilterBlacklist) + { + return !_networkManager.IsAddressInSubnets(remoteIp, addressFilter); + } + else + { + return _networkManager.IsAddressInSubnets(remoteIp, addressFilter); + } + } + } + else + { + if (!_networkManager.IsInLocalNetwork(remoteIp)) + { + return false; + } + } + + return true; + } + private bool ValidateSsl(string remoteIp, string urlString) { - if (_config.Configuration.RequireHttps && _appHost.EnableHttps) + if (_config.Configuration.RequireHttps && _appHost.EnableHttps && !_config.Configuration.IsBehindProxy) { if (urlString.IndexOf("https://", StringComparison.OrdinalIgnoreCase) == -1) { + // These are hacks, but if these ever occur on ipv6 in the local network they could be incorrectly redirected + if (urlString.IndexOf("system/ping", StringComparison.OrdinalIgnoreCase) != -1 || + urlString.IndexOf("dlna/", StringComparison.OrdinalIgnoreCase) != -1) + { + return true; + } + if (!_networkManager.IsInLocalNetwork(remoteIp)) { return false; @@ -448,7 +501,7 @@ namespace Emby.Server.Implementations.HttpServer bool enableLog = false; bool logHeaders = false; string urlToLog = null; - string remoteIp = null; + string remoteIp = httpReq.RemoteIp; try { @@ -456,7 +509,7 @@ namespace Emby.Server.Implementations.HttpServer { httpRes.StatusCode = 503; httpRes.ContentType = "text/plain"; - Write(httpRes, "Server shutting down"); + await Write(httpRes, "Server shutting down").ConfigureAwait(false); return; } @@ -464,17 +517,21 @@ namespace Emby.Server.Implementations.HttpServer { httpRes.StatusCode = 400; httpRes.ContentType = "text/plain"; - Write(httpRes, "Invalid host"); + await Write(httpRes, "Invalid host").ConfigureAwait(false); return; } - if (!ValidateSsl(httpReq.RemoteIp, urlString)) + if (!ValidateRequest(remoteIp, httpReq.IsLocal)) { - var httpsUrl = urlString - .Replace("http://", "https://", StringComparison.OrdinalIgnoreCase) - .Replace(":" + _config.Configuration.PublicPort.ToString(CultureInfo.InvariantCulture), ":" + _config.Configuration.PublicHttpsPort.ToString(CultureInfo.InvariantCulture), StringComparison.OrdinalIgnoreCase); + httpRes.StatusCode = 403; + httpRes.ContentType = "text/plain"; + await Write(httpRes, "Forbidden").ConfigureAwait(false); + return; + } - RedirectToUrl(httpRes, httpsUrl); + if (!ValidateSsl(httpReq.RemoteIp, urlString)) + { + RedirectToSecureUrl(httpReq, httpRes, urlString); return; } @@ -485,7 +542,7 @@ namespace Emby.Server.Implementations.HttpServer httpRes.AddHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS"); httpRes.AddHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, Range, X-MediaBrowser-Token, X-Emby-Authorization"); httpRes.ContentType = "text/plain"; - Write(httpRes, string.Empty); + await Write(httpRes, string.Empty).ConfigureAwait(false); return; } @@ -498,7 +555,6 @@ namespace Emby.Server.Implementations.HttpServer if (enableLog) { urlToLog = GetUrlToLog(urlString); - remoteIp = httpReq.RemoteIp; LoggerUtils.LogRequest(_logger, urlToLog, httpReq.HttpMethod, httpReq.UserAgent, logHeaders ? httpReq.Headers : null); } @@ -527,9 +583,9 @@ namespace Emby.Server.Implementations.HttpServer if (!string.Equals(newUrl, urlString, StringComparison.OrdinalIgnoreCase)) { - Write(httpRes, + await Write(httpRes, "<!doctype html><html><head><title>Emby</title></head><body>Please update your Emby bookmark to <a href=\"" + - newUrl + "\">" + newUrl + "</a></body></html>"); + newUrl + "\">" + newUrl + "</a></body></html>").ConfigureAwait(false); return; } } @@ -544,9 +600,9 @@ namespace Emby.Server.Implementations.HttpServer if (!string.Equals(newUrl, urlString, StringComparison.OrdinalIgnoreCase)) { - Write(httpRes, + await Write(httpRes, "<!doctype html><html><head><title>Emby</title></head><body>Please update your Emby bookmark to <a href=\"" + - newUrl + "\">" + newUrl + "</a></body></html>"); + newUrl + "\">" + newUrl + "</a></body></html>").ConfigureAwait(false); return; } } @@ -572,18 +628,29 @@ namespace Emby.Server.Implementations.HttpServer return; } - if (string.Equals(localPath, "/emby/pin", StringComparison.OrdinalIgnoreCase)) + if (!string.Equals(httpReq.QueryString["r"], "0", StringComparison.OrdinalIgnoreCase)) { - RedirectToUrl(httpRes, "web/pin.html"); - return; + if (localPath.EndsWith("web/dashboard.html", StringComparison.OrdinalIgnoreCase)) + { + RedirectToUrl(httpRes, "index.html#!/dashboard.html"); + } + + if (localPath.EndsWith("web/home.html", StringComparison.OrdinalIgnoreCase)) + { + RedirectToUrl(httpRes, "index.html"); + } } - if (!string.IsNullOrWhiteSpace(GlobalResponse)) + if (!string.IsNullOrEmpty(GlobalResponse)) { - httpRes.StatusCode = 503; - httpRes.ContentType = "text/html"; - Write(httpRes, GlobalResponse); - return; + // We don't want the address pings in ApplicationHost to fail + if (localPath.IndexOf("system/ping", StringComparison.OrdinalIgnoreCase) == -1) + { + httpRes.StatusCode = 503; + httpRes.ContentType = "text/html"; + await Write(httpRes, GlobalResponse).ConfigureAwait(false); + return; + } } var handler = GetServiceHandler(httpReq); @@ -594,23 +661,34 @@ namespace Emby.Server.Implementations.HttpServer } else { - ErrorHandler(new FileNotFoundException(), httpReq, false); + await ErrorHandler(new FileNotFoundException(), httpReq, false, false).ConfigureAwait(false); } } catch (OperationCanceledException ex) { - ErrorHandler(ex, httpReq, false); + await ErrorHandler(ex, httpReq, false, false).ConfigureAwait(false); + } + + catch (IOException ex) + { + await ErrorHandler(ex, httpReq, false, false).ConfigureAwait(false); + } + + catch (SocketException ex) + { + await ErrorHandler(ex, httpReq, false, false).ConfigureAwait(false); + } + + catch (SecurityException ex) + { + await ErrorHandler(ex, httpReq, false, true).ConfigureAwait(false); } catch (Exception ex) { var logException = !string.Equals(ex.GetType().Name, "SocketException", StringComparison.OrdinalIgnoreCase); -#if DEBUG - logException = true; -#endif - - ErrorHandler(ex, httpReq, logException); + await ErrorHandler(ex, httpReq, logException, false).ConfigureAwait(false); } finally { @@ -655,13 +733,36 @@ namespace Emby.Server.Implementations.HttpServer return null; } - private void Write(IResponse response, string text) + private Task Write(IResponse response, string text) { var bOutput = Encoding.UTF8.GetBytes(text); response.SetContentLength(bOutput.Length); - var outputStream = response.OutputStream; - outputStream.Write(bOutput, 0, bOutput.Length); + return response.OutputStream.WriteAsync(bOutput, 0, bOutput.Length); + } + + private void RedirectToSecureUrl(IHttpRequest httpReq, IResponse httpRes, string url) + { + int currentPort; + Uri uri; + if (Uri.TryCreate(url, UriKind.Absolute, out uri)) + { + currentPort = uri.Port; + var builder = new UriBuilder(uri); + builder.Port = _config.Configuration.PublicHttpsPort; + builder.Scheme = "https"; + url = builder.Uri.ToString(); + + RedirectToUrl(httpRes, url); + } + else + { + var httpsUrl = url + .Replace("http://", "https://", StringComparison.OrdinalIgnoreCase) + .Replace(":" + _config.Configuration.PublicPort.ToString(CultureInfo.InvariantCulture), ":" + _config.Configuration.PublicHttpsPort.ToString(CultureInfo.InvariantCulture), StringComparison.OrdinalIgnoreCase); + + RedirectToUrl(httpRes, url); + } } public static void RedirectToUrl(IResponse httpRes, string url) @@ -676,26 +777,18 @@ namespace Emby.Server.Implementations.HttpServer /// Adds the rest handlers. /// </summary> /// <param name="services">The services.</param> - public void Init(IEnumerable<IService> services) + public void Init(IEnumerable<IService> services, IEnumerable<IWebSocketListener> listeners) { - _restServices.AddRange(services); + _webSocketListeners = listeners.ToArray(); ServiceController = new ServiceController(); _logger.Info("Calling ServiceStack AppHost.Init"); - var types = _restServices.Select(r => r.GetType()).ToArray(); + var types = services.Select(r => r.GetType()).ToArray(); ServiceController.Init(this, types); - var list = new List<Action<IRequest, IResponse, object>>(); - foreach (var filter in _appHost.GetExports<IRequestFilter>()) - { - list.Add(filter.Filter); - } - - RequestFilters = list.ToArray(); - ResponseFilters = new Action<IRequest, IResponse, object>[] { new ResponseFilter(_logger).FilterResponse @@ -750,12 +843,12 @@ namespace Emby.Server.Implementations.HttpServer _xmlSerializer.SerializeToStream(o, stream); } - public object DeserializeXml(Type type, Stream stream) + public Task<object> DeserializeXml(Type type, Stream stream) { - return _xmlSerializer.DeserializeFromStream(type, stream); + return Task.FromResult(_xmlSerializer.DeserializeFromStream(type, stream)); } - public object DeserializeJson(Type type, Stream stream) + public Task<object> DeserializeJson(Type type, Stream stream) { //using (var reader = new StreamReader(stream)) //{ @@ -763,7 +856,7 @@ namespace Emby.Server.Implementations.HttpServer // Logger.Info(json); // return _jsonSerializer.DeserializeFromString(json, type); //} - return _jsonSerializer.DeserializeFromStream(stream, type); + return _jsonSerializer.DeserializeFromStreamAsync(stream, type); } private string NormalizeEmbyRoutePath(string path) @@ -815,20 +908,46 @@ namespace Emby.Server.Implementations.HttpServer } } + /// <summary> + /// Processes the web socket message received. + /// </summary> + /// <param name="result">The result.</param> + private Task ProcessWebSocketMessageReceived(WebSocketMessageInfo result) + { + if (_disposed) + { + return Task.CompletedTask; + } + + //_logger.Debug("Websocket message received: {0}", result.MessageType); + + var tasks = _webSocketListeners.Select(i => Task.Run(async () => + { + try + { + await i.ProcessMessage(result).ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.ErrorException("{0} failed processing WebSocket message {1}", ex, i.GetType().Name, result.MessageType ?? string.Empty); + } + })); + + return Task.WhenAll(tasks); + } + public void Dispose() { Dispose(true); - GC.SuppressFinalize(this); } - public void StartServer(string[] urlPrefixes) + public void StartServer(string[] urlPrefixes, IHttpListener httpListener) { UrlPrefixes = urlPrefixes; - _listener = GetListener(); + _listener = httpListener; _listener.WebSocketConnected = OnWebSocketConnected; - _listener.WebSocketConnecting = OnWebSocketConnecting; _listener.ErrorHandler = ErrorHandler; _listener.RequestHandler = RequestHandler; diff --git a/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs b/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs index 86deccee1..df493b4c3 100644 --- a/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs +++ b/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; using System.Globalization; using System.IO; +using System.IO.Compression; using System.Net; using System.Runtime.Serialization; using System.Text; @@ -30,16 +31,17 @@ namespace Emby.Server.Implementations.HttpServer private readonly ILogger _logger; private readonly IFileSystem _fileSystem; private readonly IJsonSerializer _jsonSerializer; - private readonly IMemoryStreamFactory _memoryStreamFactory; + + private IBrotliCompressor _brotliCompressor; /// <summary> /// Initializes a new instance of the <see cref="HttpResultFactory" /> class. /// </summary> - public HttpResultFactory(ILogManager logManager, IFileSystem fileSystem, IJsonSerializer jsonSerializer, IMemoryStreamFactory memoryStreamFactory) + public HttpResultFactory(ILogManager logManager, IFileSystem fileSystem, IJsonSerializer jsonSerializer, IBrotliCompressor brotliCompressor) { _fileSystem = fileSystem; _jsonSerializer = jsonSerializer; - _memoryStreamFactory = memoryStreamFactory; + _brotliCompressor = brotliCompressor; _logger = logManager.GetLogger("HttpResultFactory"); } @@ -50,9 +52,24 @@ namespace Emby.Server.Implementations.HttpServer /// <param name="contentType">Type of the content.</param> /// <param name="responseHeaders">The response headers.</param> /// <returns>System.Object.</returns> - public object GetResult(object content, string contentType, IDictionary<string, string> responseHeaders = null) + public object GetResult(IRequest requestContext, byte[] content, string contentType, IDictionary<string, string> responseHeaders = null) + { + return GetHttpResult(requestContext, content, contentType, true, responseHeaders); + } + + public object GetResult(string content, string contentType, IDictionary<string, string> responseHeaders = null) { - return GetHttpResult(content, contentType, true, responseHeaders); + return GetHttpResult(null, content, contentType, true, responseHeaders); + } + + public object GetResult(IRequest requestContext, Stream content, string contentType, IDictionary<string, string> responseHeaders = null) + { + return GetHttpResult(requestContext, content, contentType, true, responseHeaders); + } + + public object GetResult(IRequest requestContext, string content, string contentType, IDictionary<string, string> responseHeaders = null) + { + return GetHttpResult(requestContext, content, contentType, true, responseHeaders); } public object GetRedirectResult(string url) @@ -60,7 +77,7 @@ namespace Emby.Server.Implementations.HttpServer var responseHeaders = new Dictionary<string, string>(); responseHeaders["Location"] = url; - var result = new HttpResult(new byte[] { }, "text/plain", HttpStatusCode.Redirect); + var result = new HttpResult(Array.Empty<byte>(), "text/plain", HttpStatusCode.Redirect); AddResponseHeaders(result, responseHeaders); @@ -70,39 +87,98 @@ namespace Emby.Server.Implementations.HttpServer /// <summary> /// Gets the HTTP result. /// </summary> - private IHasHeaders GetHttpResult(object content, string contentType, bool addCachePrevention, IDictionary<string, string> responseHeaders = null) + private IHasHeaders GetHttpResult(IRequest requestContext, Stream content, string contentType, bool addCachePrevention, IDictionary<string, string> responseHeaders = null) { - IHasHeaders result; + var result = new StreamWriter(content, contentType, _logger); - var stream = content as Stream; + if (responseHeaders == null) + { + responseHeaders = new Dictionary<string, string>(); + } - if (stream != null) + string expires; + if (addCachePrevention && !responseHeaders.TryGetValue("Expires", out expires)) { - result = new StreamWriter(stream, contentType, _logger); + responseHeaders["Expires"] = "-1"; } - else + AddResponseHeaders(result, responseHeaders); + + return result; + } + + /// <summary> + /// Gets the HTTP result. + /// </summary> + private IHasHeaders GetHttpResult(IRequest requestContext, byte[] content, string contentType, bool addCachePrevention, IDictionary<string, string> responseHeaders = null) + { + IHasHeaders result; + + var compressionType = requestContext == null ? null : GetCompressionType(requestContext, content, contentType); + + var isHeadRequest = string.Equals(requestContext.Verb, "head", StringComparison.OrdinalIgnoreCase); + + if (string.IsNullOrEmpty(compressionType)) { - var bytes = content as byte[]; + var contentLength = content.Length; - if (bytes != null) + if (isHeadRequest) { - result = new StreamWriter(bytes, contentType, _logger); + content = Array.Empty<byte>(); } - else - { - var text = content as string; - if (text != null) - { - result = new StreamWriter(Encoding.UTF8.GetBytes(text), contentType, _logger); - } - else - { - result = new HttpResult(content, contentType, HttpStatusCode.OK); - } + result = new StreamWriter(content, contentType, contentLength, _logger); + } + else + { + result = GetCompressedResult(content, compressionType, responseHeaders, isHeadRequest, contentType); + } + + if (responseHeaders == null) + { + responseHeaders = new Dictionary<string, string>(); + } + + string expires; + if (addCachePrevention && !responseHeaders.TryGetValue("Expires", out expires)) + { + responseHeaders["Expires"] = "-1"; + } + + AddResponseHeaders(result, responseHeaders); + + return result; + } + + /// <summary> + /// Gets the HTTP result. + /// </summary> + private IHasHeaders GetHttpResult(IRequest requestContext, string content, string contentType, bool addCachePrevention, IDictionary<string, string> responseHeaders = null) + { + IHasHeaders result; + + var bytes = Encoding.UTF8.GetBytes(content); + + var compressionType = requestContext == null ? null : GetCompressionType(requestContext, bytes, contentType); + + var isHeadRequest = requestContext == null ? false : string.Equals(requestContext.Verb, "head", StringComparison.OrdinalIgnoreCase); + + if (string.IsNullOrEmpty(compressionType)) + { + var contentLength = bytes.Length; + + if (isHeadRequest) + { + bytes = Array.Empty<byte>(); } + + result = new StreamWriter(bytes, contentType, contentLength, _logger); } + else + { + result = GetCompressedResult(bytes, compressionType, responseHeaders, isHeadRequest, contentType); + } + if (responseHeaders == null) { responseHeaders = new Dictionary<string, string>(); @@ -123,20 +199,9 @@ namespace Emby.Server.Implementations.HttpServer /// Gets the optimized result. /// </summary> /// <typeparam name="T"></typeparam> - /// <param name="requestContext">The request context.</param> - /// <param name="result">The result.</param> - /// <param name="responseHeaders">The response headers.</param> - /// <returns>System.Object.</returns> - /// <exception cref="System.ArgumentNullException">result</exception> - public object GetOptimizedResult<T>(IRequest requestContext, T result, IDictionary<string, string> responseHeaders = null) + public object GetResult<T>(IRequest requestContext, T result, IDictionary<string, string> responseHeaders = null) where T : class { - return GetOptimizedResultInternal<T>(requestContext, result, true, responseHeaders); - } - - private object GetOptimizedResultInternal<T>(IRequest requestContext, T result, bool addCachePrevention, IDictionary<string, string> responseHeaders = null) - where T : class - { if (result == null) { throw new ArgumentNullException("result"); @@ -147,24 +212,49 @@ namespace Emby.Server.Implementations.HttpServer responseHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); } - if (addCachePrevention) + responseHeaders["Expires"] = "-1"; + + return ToOptimizedResultInternal(requestContext, result, responseHeaders); + } + + private string GetCompressionType(IRequest request, byte[] content, string responseContentType) + { + if (responseContentType == null) { - responseHeaders["Expires"] = "-1"; + return null; } - return ToOptimizedResultInternal(requestContext, result, responseHeaders); + // Per apple docs, hls manifests must be compressed + if (!responseContentType.StartsWith("text/", StringComparison.OrdinalIgnoreCase) && + responseContentType.IndexOf("json", StringComparison.OrdinalIgnoreCase) == -1 && + responseContentType.IndexOf("javascript", StringComparison.OrdinalIgnoreCase) == -1 && + responseContentType.IndexOf("xml", StringComparison.OrdinalIgnoreCase) == -1 && + responseContentType.IndexOf("application/x-mpegURL", StringComparison.OrdinalIgnoreCase) == -1) + { + return null; + } + + if (content.Length < 1024) + { + return null; + } + + return GetCompressionType(request); } - public static string GetCompressionType(IRequest request) + private string GetCompressionType(IRequest request) { var acceptEncoding = request.Headers["Accept-Encoding"]; - if (!string.IsNullOrWhiteSpace(acceptEncoding)) + if (acceptEncoding != null) { - if (acceptEncoding.Contains("deflate")) + //if (_brotliCompressor != null && acceptEncoding.IndexOf("br", StringComparison.OrdinalIgnoreCase) != -1) + // return "br"; + + if (acceptEncoding.IndexOf("deflate", StringComparison.OrdinalIgnoreCase) != -1) return "deflate"; - if (acceptEncoding.Contains("gzip")) + if (acceptEncoding.IndexOf("gzip", StringComparison.OrdinalIgnoreCase) != -1) return "gzip"; } @@ -180,7 +270,7 @@ namespace Emby.Server.Implementations.HttpServer /// <returns></returns> public object ToOptimizedResult<T>(IRequest request, T dto) { - return ToOptimizedResultInternal(request, dto, null); + return ToOptimizedResultInternal(request, dto); } private object ToOptimizedResultInternal<T>(IRequest request, T dto, IDictionary<string, string> responseHeaders = null) @@ -192,153 +282,137 @@ namespace Emby.Server.Implementations.HttpServer case "application/xml": case "text/xml": case "text/xml; charset=utf-8": //"text/xml; charset=utf-8" also matches xml - return GetHttpResult(SerializeToXmlString(dto), contentType, false, responseHeaders); + return GetHttpResult(request, SerializeToXmlString(dto), contentType, false, responseHeaders); case "application/json": case "text/json": - return GetHttpResult(_jsonSerializer.SerializeToString(dto), contentType, false, responseHeaders); + return GetHttpResult(request, _jsonSerializer.SerializeToString(dto), contentType, false, responseHeaders); default: - { - var ms = new MemoryStream(); - var writerFn = RequestHelper.GetResponseWriter(HttpListenerHost.Instance, contentType); - - writerFn(dto, ms); + break; + } - ms.Position = 0; + var isHeadRequest = string.Equals(request.Verb, "head", StringComparison.OrdinalIgnoreCase); - if (string.Equals(request.Verb, "head", StringComparison.OrdinalIgnoreCase)) - { - return GetHttpResult(new byte[] { }, contentType, true, responseHeaders); - } + var ms = new MemoryStream(); + var writerFn = RequestHelper.GetResponseWriter(HttpListenerHost.Instance, contentType); - return GetHttpResult(ms, contentType, true, responseHeaders); - } - } - } + writerFn(dto, ms); - public static string GetRealContentType(string contentType) - { - return contentType == null - ? null - : contentType.Split(';')[0].ToLower().Trim(); - } + ms.Position = 0; - private string SerializeToXmlString(object from) - { - using (var ms = new MemoryStream()) + if (isHeadRequest) { - var xwSettings = new XmlWriterSettings(); - xwSettings.Encoding = new UTF8Encoding(false); - xwSettings.OmitXmlDeclaration = false; - - using (var xw = XmlWriter.Create(ms, xwSettings)) + using (ms) { - var serializer = new DataContractSerializer(from.GetType()); - serializer.WriteObject(xw, from); - xw.Flush(); - ms.Seek(0, SeekOrigin.Begin); - var reader = new StreamReader(ms); - return reader.ReadToEnd(); + return GetHttpResult(request, Array.Empty<byte>(), contentType, true, responseHeaders); } } + + return GetHttpResult(request, ms, contentType, true, responseHeaders); } - /// <summary> - /// Gets the optimized result using cache. - /// </summary> - /// <typeparam name="T"></typeparam> - /// <param name="requestContext">The request context.</param> - /// <param name="cacheKey">The cache key.</param> - /// <param name="lastDateModified">The last date modified.</param> - /// <param name="cacheDuration">Duration of the cache.</param> - /// <param name="factoryFn">The factory fn.</param> - /// <param name="responseHeaders">The response headers.</param> - /// <returns>System.Object.</returns> - /// <exception cref="System.ArgumentNullException">cacheKey - /// or - /// factoryFn</exception> - public object GetOptimizedResultUsingCache<T>(IRequest requestContext, Guid cacheKey, DateTime? lastDateModified, TimeSpan? cacheDuration, Func<T> factoryFn, IDictionary<string, string> responseHeaders = null) - where T : class + private IHasHeaders GetCompressedResult(byte[] content, + string requestedCompressionType, + IDictionary<string, string> responseHeaders, + bool isHeadRequest, + string contentType) { - if (cacheKey == Guid.Empty) - { - throw new ArgumentNullException("cacheKey"); - } - if (factoryFn == null) - { - throw new ArgumentNullException("factoryFn"); - } - - var key = cacheKey.ToString("N"); - if (responseHeaders == null) { responseHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); } - // See if the result is already cached in the browser - var result = GetCachedResult(requestContext, responseHeaders, cacheKey, key, lastDateModified, cacheDuration, null); + content = Compress(content, requestedCompressionType); + responseHeaders["Content-Encoding"] = requestedCompressionType; - if (result != null) - { - return result; - } + responseHeaders["Vary"] = "Accept-Encoding"; - return GetOptimizedResultInternal(requestContext, factoryFn(), false, responseHeaders); - } + var contentLength = content.Length; - /// <summary> - /// To the cached result. - /// </summary> - /// <typeparam name="T"></typeparam> - /// <param name="requestContext">The request context.</param> - /// <param name="cacheKey">The cache key.</param> - /// <param name="lastDateModified">The last date modified.</param> - /// <param name="cacheDuration">Duration of the cache.</param> - /// <param name="factoryFn">The factory fn.</param> - /// <param name="contentType">Type of the content.</param> - /// <param name="responseHeaders">The response headers.</param> - /// <returns>System.Object.</returns> - /// <exception cref="System.ArgumentNullException">cacheKey</exception> - public object GetCachedResult<T>(IRequest requestContext, Guid cacheKey, DateTime? lastDateModified, TimeSpan? cacheDuration, Func<T> factoryFn, string contentType, IDictionary<string, string> responseHeaders = null) - where T : class - { - if (cacheKey == Guid.Empty) + if (isHeadRequest) { - throw new ArgumentNullException("cacheKey"); + var result = new StreamWriter(Array.Empty<byte>(), contentType, contentLength, _logger); + AddResponseHeaders(result, responseHeaders); + return result; } - if (factoryFn == null) + else { - throw new ArgumentNullException("factoryFn"); + var result = new StreamWriter(content, contentType, contentLength, _logger); + AddResponseHeaders(result, responseHeaders); + return result; } + } - var key = cacheKey.ToString("N"); + private byte[] Compress(byte[] bytes, string compressionType) + { + if (string.Equals(compressionType, "br", StringComparison.OrdinalIgnoreCase)) + return CompressBrotli(bytes); - if (responseHeaders == null) - { - responseHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); - } + if (string.Equals(compressionType, "deflate", StringComparison.OrdinalIgnoreCase)) + return Deflate(bytes); + + if (string.Equals(compressionType, "gzip", StringComparison.OrdinalIgnoreCase)) + return GZip(bytes); - // See if the result is already cached in the browser - var result = GetCachedResult(requestContext, responseHeaders, cacheKey, key, lastDateModified, cacheDuration, contentType); + throw new NotSupportedException(compressionType); + } + + private byte[] CompressBrotli(byte[] bytes) + { + return _brotliCompressor.Compress(bytes); + } - if (result != null) + private byte[] Deflate(byte[] bytes) + { + // In .NET FX incompat-ville, you can't access compressed bytes without closing DeflateStream + // Which means we must use MemoryStream since you have to use ToArray() on a closed Stream + using (var ms = new MemoryStream()) + using (var zipStream = new DeflateStream(ms, CompressionMode.Compress)) { - return result; + zipStream.Write(bytes, 0, bytes.Length); + zipStream.Dispose(); + + return ms.ToArray(); } + } - result = factoryFn(); + private byte[] GZip(byte[] buffer) + { + using (var ms = new MemoryStream()) + using (var zipStream = new GZipStream(ms, CompressionMode.Compress)) + { + zipStream.Write(buffer, 0, buffer.Length); + zipStream.Dispose(); - // Apply caching headers - var hasHeaders = result as IHasHeaders; + return ms.ToArray(); + } + } - if (hasHeaders != null) + public static string GetRealContentType(string contentType) + { + return contentType == null + ? null + : contentType.Split(';')[0].ToLower().Trim(); + } + + private string SerializeToXmlString(object from) + { + using (var ms = new MemoryStream()) { - AddResponseHeaders(hasHeaders, responseHeaders); - return hasHeaders; - } + var xwSettings = new XmlWriterSettings(); + xwSettings.Encoding = new UTF8Encoding(false); + xwSettings.OmitXmlDeclaration = false; - return GetHttpResult(result, contentType, false, responseHeaders); + using (var xw = XmlWriter.Create(ms, xwSettings)) + { + var serializer = new DataContractSerializer(from.GetType()); + serializer.WriteObject(xw, from); + xw.Flush(); + ms.Seek(0, SeekOrigin.Begin); + var reader = new StreamReader(ms); + return reader.ReadToEnd(); + } + } } /// <summary> @@ -357,7 +431,7 @@ namespace Emby.Server.Implementations.HttpServer AddAgeHeader(responseHeaders, lastDateModified); AddExpiresHeader(responseHeaders, cacheKeyString, cacheDuration); - var result = new HttpResult(new byte[] { }, contentType ?? "text/html", HttpStatusCode.NotModified); + var result = new HttpResult(Array.Empty<byte>(), contentType ?? "text/html", HttpStatusCode.NotModified); AddResponseHeaders(result, responseHeaders); @@ -402,7 +476,7 @@ namespace Emby.Server.Implementations.HttpServer throw new ArgumentException("FileShare must be either Read or ReadWrite"); } - if (string.IsNullOrWhiteSpace(options.ContentType)) + if (string.IsNullOrEmpty(options.ContentType)) { options.ContentType = MimeTypes.GetMimeType(path); } @@ -460,19 +534,17 @@ namespace Emby.Server.Implementations.HttpServer options.ResponseHeaders = options.ResponseHeaders ?? new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); var contentType = options.ContentType; - if (cacheKey == Guid.Empty) + if (!cacheKey.Equals(Guid.Empty)) { - throw new ArgumentNullException("cacheKey"); - } + var key = cacheKey.ToString("N"); - var key = cacheKey.ToString("N"); + // See if the result is already cached in the browser + var result = GetCachedResult(requestContext, options.ResponseHeaders, cacheKey, key, options.DateLastModified, options.CacheDuration, contentType); - // See if the result is already cached in the browser - var result = GetCachedResult(requestContext, options.ResponseHeaders, cacheKey, key, options.DateLastModified, options.CacheDuration, contentType); - - if (result != null) - { - return result; + if (result != null) + { + return result; + } } // TODO: We don't really need the option value @@ -484,7 +556,7 @@ namespace Emby.Server.Implementations.HttpServer var rangeHeader = requestContext.Headers.Get("Range"); - if (!isHeadRequest && !string.IsNullOrWhiteSpace(options.Path)) + if (!isHeadRequest && !string.IsNullOrEmpty(options.Path)) { var hasHeaders = new FileWriter(options.Path, contentType, rangeHeader, _logger, _fileSystem) { @@ -497,11 +569,24 @@ namespace Emby.Server.Implementations.HttpServer return hasHeaders; } - if (!string.IsNullOrWhiteSpace(rangeHeader)) + var stream = await factoryFn().ConfigureAwait(false); + + var totalContentLength = options.ContentLength; + if (!totalContentLength.HasValue) { - var stream = await factoryFn().ConfigureAwait(false); + try + { + totalContentLength = stream.Length; + } + catch (NotSupportedException) + { - var hasHeaders = new RangeRequestWriter(rangeHeader, stream, contentType, isHeadRequest, _logger) + } + } + + if (!string.IsNullOrWhiteSpace(rangeHeader) && totalContentLength.HasValue) + { + var hasHeaders = new RangeRequestWriter(rangeHeader, totalContentLength.Value, stream, contentType, isHeadRequest, _logger) { OnComplete = options.OnComplete }; @@ -511,15 +596,17 @@ namespace Emby.Server.Implementations.HttpServer } else { - var stream = await factoryFn().ConfigureAwait(false); - - responseHeaders["Content-Length"] = stream.Length.ToString(UsCulture); + if (totalContentLength.HasValue) + { + responseHeaders["Content-Length"] = totalContentLength.Value.ToString(UsCulture); + } if (isHeadRequest) { - stream.Dispose(); - - return GetHttpResult(new byte[] { }, contentType, true, responseHeaders); + using (stream) + { + return GetHttpResult(requestContext, Array.Empty<byte>(), contentType, true, responseHeaders); + } } var hasHeaders = new StreamWriter(stream, contentType, _logger) @@ -603,7 +690,7 @@ namespace Emby.Server.Implementations.HttpServer /// <param name="lastDateModified">The last date modified.</param> /// <param name="cacheDuration">Duration of the cache.</param> /// <returns><c>true</c> if [is not modified] [the specified cache key]; otherwise, <c>false</c>.</returns> - private bool IsNotModified(IRequest requestContext, Guid? cacheKey, DateTime? lastDateModified, TimeSpan? cacheDuration) + private bool IsNotModified(IRequest requestContext, Guid cacheKey, DateTime? lastDateModified, TimeSpan? cacheDuration) { //var isNotModified = true; @@ -624,8 +711,10 @@ namespace Emby.Server.Implementations.HttpServer var ifNoneMatchHeader = requestContext.Headers.Get("If-None-Match"); + var hasCacheKey = !cacheKey.Equals(Guid.Empty); + // Validate If-None-Match - if ((cacheKey.HasValue || !string.IsNullOrEmpty(ifNoneMatchHeader))) + if ((hasCacheKey || !string.IsNullOrEmpty(ifNoneMatchHeader))) { Guid ifNoneMatch; @@ -633,7 +722,7 @@ namespace Emby.Server.Implementations.HttpServer if (Guid.TryParse(ifNoneMatchHeader, out ifNoneMatch)) { - if (cacheKey.HasValue && cacheKey.Value == ifNoneMatch) + if (hasCacheKey && cacheKey.Equals(ifNoneMatch)) { return true; } @@ -697,4 +786,9 @@ namespace Emby.Server.Implementations.HttpServer } } } + + public interface IBrotliCompressor + { + byte[] Compress(byte[] content); + } }
\ No newline at end of file diff --git a/Emby.Server.Implementations/HttpServer/IHttpListener.cs b/Emby.Server.Implementations/HttpServer/IHttpListener.cs index 9feb2311d..e21607ebd 100644 --- a/Emby.Server.Implementations/HttpServer/IHttpListener.cs +++ b/Emby.Server.Implementations/HttpServer/IHttpListener.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Model.Services; +using Emby.Server.Implementations.Net; namespace Emby.Server.Implementations.HttpServer { @@ -13,7 +14,7 @@ namespace Emby.Server.Implementations.HttpServer /// Gets or sets the error handler. /// </summary> /// <value>The error handler.</value> - Action<Exception, IRequest, bool> ErrorHandler { get; set; } + Func<Exception, IRequest, bool, bool, Task> ErrorHandler { get; set; } /// <summary> /// Gets or sets the request handler. diff --git a/Emby.Server.Implementations/HttpServer/LoggerUtils.cs b/Emby.Server.Implementations/HttpServer/LoggerUtils.cs index 46bb4c7f9..3aa48efd4 100644 --- a/Emby.Server.Implementations/HttpServer/LoggerUtils.cs +++ b/Emby.Server.Implementations/HttpServer/LoggerUtils.cs @@ -2,24 +2,11 @@ using System; using System.Globalization; using MediaBrowser.Model.Services; -using SocketHttpListener.Net; namespace Emby.Server.Implementations.HttpServer { public static class LoggerUtils { - /// <summary> - /// Logs the request. - /// </summary> - /// <param name="logger">The logger.</param> - /// <param name="request">The request.</param> - public static void LogRequest(ILogger logger, HttpListenerRequest request) - { - var url = request.Url.ToString(); - - logger.Info("{0} {1}. UserAgent: {2}", request.IsWebSocketRequest ? "WS" : "HTTP " + request.HttpMethod, url, request.UserAgent ?? string.Empty); - } - public static void LogRequest(ILogger logger, string url, string method, string userAgent, QueryParamCollection headers) { if (headers == null) diff --git a/Emby.Server.Implementations/HttpServer/RangeRequestWriter.cs b/Emby.Server.Implementations/HttpServer/RangeRequestWriter.cs index 7c967949b..4177c7e78 100644 --- a/Emby.Server.Implementations/HttpServer/RangeRequestWriter.cs +++ b/Emby.Server.Implementations/HttpServer/RangeRequestWriter.cs @@ -58,7 +58,7 @@ namespace Emby.Server.Implementations.HttpServer /// <param name="source">The source.</param> /// <param name="contentType">Type of the content.</param> /// <param name="isHeadRequest">if set to <c>true</c> [is head request].</param> - public RangeRequestWriter(string rangeHeader, Stream source, string contentType, bool isHeadRequest, ILogger logger) + public RangeRequestWriter(string rangeHeader, long contentLength, Stream source, string contentType, bool isHeadRequest, ILogger logger) { if (string.IsNullOrEmpty(contentType)) { @@ -76,17 +76,17 @@ namespace Emby.Server.Implementations.HttpServer StatusCode = HttpStatusCode.PartialContent; Cookies = new List<Cookie>(); - SetRangeValues(); + SetRangeValues(contentLength); } /// <summary> /// Sets the range values. /// </summary> - private void SetRangeValues() + private void SetRangeValues(long contentLength) { var requestedRange = RequestedRanges[0]; - TotalContentLength = SourceStream.Length; + TotalContentLength = contentLength; // If the requested range is "0-", we can optimize by just doing a stream copy if (!requestedRange.Value.HasValue) @@ -105,7 +105,7 @@ namespace Emby.Server.Implementations.HttpServer Headers["Content-Length"] = RangeLength.ToString(UsCulture); Headers["Content-Range"] = string.Format("bytes {0}-{1}/{2}", RangeStart, RangeEnd, TotalContentLength); - if (RangeStart > 0) + if (RangeStart > 0 && SourceStream.CanSeek) { SourceStream.Position = RangeStart; } diff --git a/Emby.Server.Implementations/HttpServer/ResponseFilter.cs b/Emby.Server.Implementations/HttpServer/ResponseFilter.cs index ac36f8f51..385d19b6b 100644 --- a/Emby.Server.Implementations/HttpServer/ResponseFilter.cs +++ b/Emby.Server.Implementations/HttpServer/ResponseFilter.cs @@ -2,7 +2,6 @@ using System; using System.Globalization; using System.Text; -using Emby.Server.Implementations.HttpServer.SocketSharp; using MediaBrowser.Model.Services; namespace Emby.Server.Implementations.HttpServer @@ -47,7 +46,6 @@ namespace Emby.Server.Implementations.HttpServer } var hasHeaders = dto as IHasHeaders; - var sharpResponse = res as WebSocketSharpResponse; if (hasHeaders != null) { @@ -67,7 +65,7 @@ namespace Emby.Server.Implementations.HttpServer if (length > 0) { res.SetContentLength(length); - + //var listenerResponse = res.OriginalResponse as HttpListenerResponse; //if (listenerResponse != null) @@ -78,10 +76,7 @@ namespace Emby.Server.Implementations.HttpServer // return; //} - if (sharpResponse != null) - { - sharpResponse.SendChunked = false; - } + res.SendChunked = false; } } } diff --git a/Emby.Server.Implementations/HttpServer/Security/AuthService.cs b/Emby.Server.Implementations/HttpServer/Security/AuthService.cs index fadab4482..8f6d042dd 100644 --- a/Emby.Server.Implementations/HttpServer/Security/AuthService.cs +++ b/Emby.Server.Implementations/HttpServer/Security/AuthService.cs @@ -9,6 +9,7 @@ using MediaBrowser.Controller.Session; using System; using System.Linq; using MediaBrowser.Model.Services; +using MediaBrowser.Common.Net; namespace Emby.Server.Implementations.HttpServer.Security { @@ -16,21 +17,21 @@ namespace Emby.Server.Implementations.HttpServer.Security { private readonly IServerConfigurationManager _config; - public AuthService(IUserManager userManager, IAuthorizationContext authorizationContext, IServerConfigurationManager config, IConnectManager connectManager, ISessionManager sessionManager, IDeviceManager deviceManager) + public AuthService(IUserManager userManager, IAuthorizationContext authorizationContext, IServerConfigurationManager config, IConnectManager connectManager, ISessionManager sessionManager, INetworkManager networkManager) { AuthorizationContext = authorizationContext; _config = config; - DeviceManager = deviceManager; SessionManager = sessionManager; ConnectManager = connectManager; UserManager = userManager; + NetworkManager = networkManager; } public IUserManager UserManager { get; private set; } public IAuthorizationContext AuthorizationContext { get; private set; } public IConnectManager ConnectManager { get; private set; } public ISessionManager SessionManager { get; private set; } - public IDeviceManager DeviceManager { get; private set; } + public INetworkManager NetworkManager { get; private set; } /// <summary> /// Redirect the client to a specific URL if authentication failed. @@ -38,14 +39,12 @@ namespace Emby.Server.Implementations.HttpServer.Security /// </summary> public string HtmlRedirect { get; set; } - public void Authenticate(IRequest request, - IAuthenticationAttributes authAttribtues) + public void Authenticate(IRequest request, IAuthenticationAttributes authAttribtues) { ValidateUser(request, authAttribtues); } - private void ValidateUser(IRequest request, - IAuthenticationAttributes authAttribtues) + private void ValidateUser(IRequest request, IAuthenticationAttributes authAttribtues) { // This code is executed before the service var auth = AuthorizationContext.GetAuthorizationInfo(request); @@ -60,11 +59,14 @@ namespace Emby.Server.Implementations.HttpServer.Security } } - var user = string.IsNullOrWhiteSpace(auth.UserId) - ? null - : UserManager.GetUserById(auth.UserId); + if (authAttribtues.AllowLocalOnly && !request.IsLocal) + { + throw new SecurityException("Operation not found."); + } - if (user == null & !string.IsNullOrWhiteSpace(auth.UserId)) + var user = auth.User; + + if (user == null & !auth.UserId.Equals(Guid.Empty)) { throw new SecurityException("User with Id " + auth.UserId + " not found"); } @@ -83,9 +85,9 @@ namespace Emby.Server.Implementations.HttpServer.Security ValidateRoles(roles, user); } - if (!string.IsNullOrWhiteSpace(auth.DeviceId) && - !string.IsNullOrWhiteSpace(auth.Client) && - !string.IsNullOrWhiteSpace(auth.Device)) + if (!string.IsNullOrEmpty(auth.DeviceId) && + !string.IsNullOrEmpty(auth.Client) && + !string.IsNullOrEmpty(auth.Device)) { SessionManager.LogSessionActivity(auth.Client, auth.Version, @@ -108,6 +110,14 @@ namespace Emby.Server.Implementations.HttpServer.Security }; } + if (!user.Policy.EnableRemoteAccess && !NetworkManager.IsInLocalNetwork(request.RemoteIp)) + { + throw new SecurityException("User account has been disabled.") + { + SecurityExceptionType = SecurityExceptionType.Unauthenticated + }; + } + if (!user.Policy.IsAdministrator && !authAttribtues.EscapeParentalControl && !user.IsParentalScheduleAllowed()) @@ -119,17 +129,6 @@ namespace Emby.Server.Implementations.HttpServer.Security SecurityExceptionType = SecurityExceptionType.ParentalControl }; } - - if (!string.IsNullOrWhiteSpace(auth.DeviceId)) - { - if (!DeviceManager.CanAccessDevice(user.Id.ToString("N"), auth.DeviceId)) - { - throw new SecurityException("User is not allowed access from this device.") - { - SecurityExceptionType = SecurityExceptionType.ParentalControl - }; - } - } } private bool IsExemptFromAuthenticationToken(AuthorizationInfo auth, IAuthenticationAttributes authAttribtues, IRequest request) @@ -143,6 +142,10 @@ namespace Emby.Server.Implementations.HttpServer.Security { return true; } + if (authAttribtues.AllowLocalOnly && request.IsLocal) + { + return true; + } return false; } @@ -159,12 +162,17 @@ namespace Emby.Server.Implementations.HttpServer.Security return true; } - if (string.IsNullOrWhiteSpace(auth.Token)) + if (authAttribtues.AllowLocalOnly && request.IsLocal) + { + return true; + } + + if (string.IsNullOrEmpty(auth.Token)) { return true; } - if (tokenInfo != null && string.IsNullOrWhiteSpace(tokenInfo.UserId)) + if (tokenInfo != null && tokenInfo.UserId.Equals(Guid.Empty)) { return true; } @@ -225,7 +233,7 @@ namespace Emby.Server.Implementations.HttpServer.Security private void ValidateSecurityToken(IRequest request, string token) { - if (string.IsNullOrWhiteSpace(token)) + if (string.IsNullOrEmpty(token)) { throw new SecurityException("Access token is required."); } @@ -237,12 +245,7 @@ namespace Emby.Server.Implementations.HttpServer.Security throw new SecurityException("Access token is invalid or expired."); } - if (!info.IsActive) - { - throw new SecurityException("Access token has expired."); - } - - //if (!string.IsNullOrWhiteSpace(info.UserId)) + //if (!string.IsNullOrEmpty(info.UserId)) //{ // var user = _userManager.GetUserById(info.UserId); diff --git a/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs b/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs index a41c51d1a..ee8f6c60b 100644 --- a/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs +++ b/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using MediaBrowser.Model.Services; using System.Linq; using System.Threading; +using MediaBrowser.Controller.Library; namespace Emby.Server.Implementations.HttpServer.Security { @@ -13,11 +14,13 @@ namespace Emby.Server.Implementations.HttpServer.Security { private readonly IAuthenticationRepository _authRepo; private readonly IConnectManager _connectManager; + private readonly IUserManager _userManager; - public AuthorizationContext(IAuthenticationRepository authRepo, IConnectManager connectManager) + public AuthorizationContext(IAuthenticationRepository authRepo, IConnectManager connectManager, IUserManager userManager) { _authRepo = authRepo; _connectManager = connectManager; + _userManager = userManager; } public AuthorizationInfo GetAuthorizationInfo(object requestContext) @@ -60,16 +63,16 @@ namespace Emby.Server.Implementations.HttpServer.Security auth.TryGetValue("Token", out token); } - if (string.IsNullOrWhiteSpace(token)) + if (string.IsNullOrEmpty(token)) { token = httpReq.Headers["X-Emby-Token"]; } - if (string.IsNullOrWhiteSpace(token)) + if (string.IsNullOrEmpty(token)) { token = httpReq.Headers["X-MediaBrowser-Token"]; } - if (string.IsNullOrWhiteSpace(token)) + if (string.IsNullOrEmpty(token)) { token = httpReq.QueryString["api_key"]; } @@ -94,8 +97,6 @@ namespace Emby.Server.Implementations.HttpServer.Security if (tokenInfo != null) { - info.UserId = tokenInfo.UserId; - var updateToken = false; // TODO: Remove these checks for IsNullOrWhiteSpace @@ -109,15 +110,21 @@ namespace Emby.Server.Implementations.HttpServer.Security info.DeviceId = tokenInfo.DeviceId; } + // Temporary. TODO - allow clients to specify that the token has been shared with a casting device + var allowTokenInfoUpdate = info.Client == null || info.Client.IndexOf("chromecast", StringComparison.OrdinalIgnoreCase) == -1; if (string.IsNullOrWhiteSpace(info.Device)) { info.Device = tokenInfo.DeviceName; } + else if (!string.Equals(info.Device, tokenInfo.DeviceName, StringComparison.OrdinalIgnoreCase)) { - updateToken = true; - tokenInfo.DeviceName = info.Device; + if (allowTokenInfoUpdate) + { + updateToken = true; + tokenInfo.DeviceName = info.Device; + } } if (string.IsNullOrWhiteSpace(info.Version)) @@ -126,22 +133,38 @@ namespace Emby.Server.Implementations.HttpServer.Security } else if (!string.Equals(info.Version, tokenInfo.AppVersion, StringComparison.OrdinalIgnoreCase)) { + if (allowTokenInfoUpdate) + { + updateToken = true; + tokenInfo.AppVersion = info.Version; + } + } + + if ((DateTime.UtcNow - tokenInfo.DateLastActivity).TotalMinutes > 3) + { + tokenInfo.DateLastActivity = DateTime.UtcNow; updateToken = true; - tokenInfo.AppVersion = info.Version; + } + + if (!tokenInfo.UserId.Equals(Guid.Empty)) + { + info.User = _userManager.GetUserById(tokenInfo.UserId); + + if (info.User != null && !string.Equals(info.User.Name, tokenInfo.UserName, StringComparison.OrdinalIgnoreCase)) + { + tokenInfo.UserName = info.User.Name; + updateToken = true; + } } if (updateToken) { - _authRepo.Update(tokenInfo, CancellationToken.None); + _authRepo.Update(tokenInfo); } } else { - var user = _connectManager.GetUserFromExchangeToken(token); - if (user != null) - { - info.UserId = user.Id.ToString("N"); - } + info.User = _connectManager.GetUserFromExchangeToken(token); } httpReq.Items["OriginalAuthenticationInfo"] = tokenInfo; } @@ -160,7 +183,7 @@ namespace Emby.Server.Implementations.HttpServer.Security { var auth = httpReq.Headers["X-Emby-Authorization"]; - if (string.IsNullOrWhiteSpace(auth)) + if (string.IsNullOrEmpty(auth)) { auth = httpReq.Headers["Authorization"]; } @@ -212,7 +235,7 @@ namespace Emby.Server.Implementations.HttpServer.Security private string NormalizeValue(string value) { - if (string.IsNullOrWhiteSpace(value)) + if (string.IsNullOrEmpty(value)) { return value; } diff --git a/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs b/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs index 9826a0d56..a919ce008 100644 --- a/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs +++ b/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs @@ -5,6 +5,7 @@ using MediaBrowser.Controller.Security; using MediaBrowser.Controller.Session; using System.Threading.Tasks; using MediaBrowser.Model.Services; +using System; namespace Emby.Server.Implementations.HttpServer.Security { @@ -21,11 +22,11 @@ namespace Emby.Server.Implementations.HttpServer.Security _sessionManager = sessionManager; } - public Task<SessionInfo> GetSession(IRequest requestContext) + public SessionInfo GetSession(IRequest requestContext) { var authorization = _authContext.GetAuthorizationInfo(requestContext); - var user = string.IsNullOrWhiteSpace(authorization.UserId) ? null : _userManager.GetUserById(authorization.UserId); + var user = authorization.User; return _sessionManager.LogSessionActivity(authorization.Client, authorization.Version, authorization.DeviceId, authorization.Device, requestContext.RemoteIp, user); } @@ -36,19 +37,19 @@ namespace Emby.Server.Implementations.HttpServer.Security return info as AuthenticationInfo; } - public Task<SessionInfo> GetSession(object requestContext) + public SessionInfo GetSession(object requestContext) { return GetSession((IRequest)requestContext); } - public async Task<User> GetUser(IRequest requestContext) + public User GetUser(IRequest requestContext) { - var session = await GetSession(requestContext).ConfigureAwait(false); + var session = GetSession(requestContext); - return session == null || !session.UserId.HasValue ? null : _userManager.GetUserById(session.UserId.Value); + return session == null || session.UserId.Equals(Guid.Empty) ? null : _userManager.GetUserById(session.UserId); } - public Task<User> GetUser(object requestContext) + public User GetUser(object requestContext) { return GetUser((IRequest)requestContext); } diff --git a/Emby.Server.Implementations/HttpServer/SocketSharp/Extensions.cs b/Emby.Server.Implementations/HttpServer/SocketSharp/Extensions.cs deleted file mode 100644 index 07a338f19..000000000 --- a/Emby.Server.Implementations/HttpServer/SocketSharp/Extensions.cs +++ /dev/null @@ -1,12 +0,0 @@ -using SocketHttpListener.Net; - -namespace Emby.Server.Implementations.HttpServer.SocketSharp -{ - public static class Extensions - { - public static string GetOperationName(this HttpListenerRequest request) - { - return request.Url.Segments[request.Url.Segments.Length - 1]; - } - } -} diff --git a/Emby.Server.Implementations/HttpServer/SocketSharp/HttpUtility.cs b/Emby.Server.Implementations/HttpServer/SocketSharp/HttpUtility.cs deleted file mode 100644 index 4e8dd7362..000000000 --- a/Emby.Server.Implementations/HttpServer/SocketSharp/HttpUtility.cs +++ /dev/null @@ -1,923 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Text; -using MediaBrowser.Model.Services; -using MediaBrowser.Model.Extensions; - -namespace Emby.Server.Implementations.HttpServer.SocketSharp -{ - public static class MyHttpUtility - { - // Must be sorted - static readonly long[] entities = new long[] { - (long)'A' << 56 | (long)'E' << 48 | (long)'l' << 40 | (long)'i' << 32 | (long)'g' << 24, - (long)'A' << 56 | (long)'a' << 48 | (long)'c' << 40 | (long)'u' << 32 | (long)'t' << 24 | (long)'e' << 16, - (long)'A' << 56 | (long)'c' << 48 | (long)'i' << 40 | (long)'r' << 32 | (long)'c' << 24, - (long)'A' << 56 | (long)'g' << 48 | (long)'r' << 40 | (long)'a' << 32 | (long)'v' << 24 | (long)'e' << 16, - (long)'A' << 56 | (long)'l' << 48 | (long)'p' << 40 | (long)'h' << 32 | (long)'a' << 24, - (long)'A' << 56 | (long)'r' << 48 | (long)'i' << 40 | (long)'n' << 32 | (long)'g' << 24, - (long)'A' << 56 | (long)'t' << 48 | (long)'i' << 40 | (long)'l' << 32 | (long)'d' << 24 | (long)'e' << 16, - (long)'A' << 56 | (long)'u' << 48 | (long)'m' << 40 | (long)'l' << 32, - (long)'B' << 56 | (long)'e' << 48 | (long)'t' << 40 | (long)'a' << 32, - (long)'C' << 56 | (long)'c' << 48 | (long)'e' << 40 | (long)'d' << 32 | (long)'i' << 24 | (long)'l' << 16, - (long)'C' << 56 | (long)'h' << 48 | (long)'i' << 40, - (long)'D' << 56 | (long)'a' << 48 | (long)'g' << 40 | (long)'g' << 32 | (long)'e' << 24 | (long)'r' << 16, - (long)'D' << 56 | (long)'e' << 48 | (long)'l' << 40 | (long)'t' << 32 | (long)'a' << 24, - (long)'E' << 56 | (long)'T' << 48 | (long)'H' << 40, - (long)'E' << 56 | (long)'a' << 48 | (long)'c' << 40 | (long)'u' << 32 | (long)'t' << 24 | (long)'e' << 16, - (long)'E' << 56 | (long)'c' << 48 | (long)'i' << 40 | (long)'r' << 32 | (long)'c' << 24, - (long)'E' << 56 | (long)'g' << 48 | (long)'r' << 40 | (long)'a' << 32 | (long)'v' << 24 | (long)'e' << 16, - (long)'E' << 56 | (long)'p' << 48 | (long)'s' << 40 | (long)'i' << 32 | (long)'l' << 24 | (long)'o' << 16 | (long)'n' << 8, - (long)'E' << 56 | (long)'t' << 48 | (long)'a' << 40, - (long)'E' << 56 | (long)'u' << 48 | (long)'m' << 40 | (long)'l' << 32, - (long)'G' << 56 | (long)'a' << 48 | (long)'m' << 40 | (long)'m' << 32 | (long)'a' << 24, - (long)'I' << 56 | (long)'a' << 48 | (long)'c' << 40 | (long)'u' << 32 | (long)'t' << 24 | (long)'e' << 16, - (long)'I' << 56 | (long)'c' << 48 | (long)'i' << 40 | (long)'r' << 32 | (long)'c' << 24, - (long)'I' << 56 | (long)'g' << 48 | (long)'r' << 40 | (long)'a' << 32 | (long)'v' << 24 | (long)'e' << 16, - (long)'I' << 56 | (long)'o' << 48 | (long)'t' << 40 | (long)'a' << 32, - (long)'I' << 56 | (long)'u' << 48 | (long)'m' << 40 | (long)'l' << 32, - (long)'K' << 56 | (long)'a' << 48 | (long)'p' << 40 | (long)'p' << 32 | (long)'a' << 24, - (long)'L' << 56 | (long)'a' << 48 | (long)'m' << 40 | (long)'b' << 32 | (long)'d' << 24 | (long)'a' << 16, - (long)'M' << 56 | (long)'u' << 48, - (long)'N' << 56 | (long)'t' << 48 | (long)'i' << 40 | (long)'l' << 32 | (long)'d' << 24 | (long)'e' << 16, - (long)'N' << 56 | (long)'u' << 48, - (long)'O' << 56 | (long)'E' << 48 | (long)'l' << 40 | (long)'i' << 32 | (long)'g' << 24, - (long)'O' << 56 | (long)'a' << 48 | (long)'c' << 40 | (long)'u' << 32 | (long)'t' << 24 | (long)'e' << 16, - (long)'O' << 56 | (long)'c' << 48 | (long)'i' << 40 | (long)'r' << 32 | (long)'c' << 24, - (long)'O' << 56 | (long)'g' << 48 | (long)'r' << 40 | (long)'a' << 32 | (long)'v' << 24 | (long)'e' << 16, - (long)'O' << 56 | (long)'m' << 48 | (long)'e' << 40 | (long)'g' << 32 | (long)'a' << 24, - (long)'O' << 56 | (long)'m' << 48 | (long)'i' << 40 | (long)'c' << 32 | (long)'r' << 24 | (long)'o' << 16 | (long)'n' << 8, - (long)'O' << 56 | (long)'s' << 48 | (long)'l' << 40 | (long)'a' << 32 | (long)'s' << 24 | (long)'h' << 16, - (long)'O' << 56 | (long)'t' << 48 | (long)'i' << 40 | (long)'l' << 32 | (long)'d' << 24 | (long)'e' << 16, - (long)'O' << 56 | (long)'u' << 48 | (long)'m' << 40 | (long)'l' << 32, - (long)'P' << 56 | (long)'h' << 48 | (long)'i' << 40, - (long)'P' << 56 | (long)'i' << 48, - (long)'P' << 56 | (long)'r' << 48 | (long)'i' << 40 | (long)'m' << 32 | (long)'e' << 24, - (long)'P' << 56 | (long)'s' << 48 | (long)'i' << 40, - (long)'R' << 56 | (long)'h' << 48 | (long)'o' << 40, - (long)'S' << 56 | (long)'c' << 48 | (long)'a' << 40 | (long)'r' << 32 | (long)'o' << 24 | (long)'n' << 16, - (long)'S' << 56 | (long)'i' << 48 | (long)'g' << 40 | (long)'m' << 32 | (long)'a' << 24, - (long)'T' << 56 | (long)'H' << 48 | (long)'O' << 40 | (long)'R' << 32 | (long)'N' << 24, - (long)'T' << 56 | (long)'a' << 48 | (long)'u' << 40, - (long)'T' << 56 | (long)'h' << 48 | (long)'e' << 40 | (long)'t' << 32 | (long)'a' << 24, - (long)'U' << 56 | (long)'a' << 48 | (long)'c' << 40 | (long)'u' << 32 | (long)'t' << 24 | (long)'e' << 16, - (long)'U' << 56 | (long)'c' << 48 | (long)'i' << 40 | (long)'r' << 32 | (long)'c' << 24, - (long)'U' << 56 | (long)'g' << 48 | (long)'r' << 40 | (long)'a' << 32 | (long)'v' << 24 | (long)'e' << 16, - (long)'U' << 56 | (long)'p' << 48 | (long)'s' << 40 | (long)'i' << 32 | (long)'l' << 24 | (long)'o' << 16 | (long)'n' << 8, - (long)'U' << 56 | (long)'u' << 48 | (long)'m' << 40 | (long)'l' << 32, - (long)'X' << 56 | (long)'i' << 48, - (long)'Y' << 56 | (long)'a' << 48 | (long)'c' << 40 | (long)'u' << 32 | (long)'t' << 24 | (long)'e' << 16, - (long)'Y' << 56 | (long)'u' << 48 | (long)'m' << 40 | (long)'l' << 32, - (long)'Z' << 56 | (long)'e' << 48 | (long)'t' << 40 | (long)'a' << 32, - (long)'a' << 56 | (long)'a' << 48 | (long)'c' << 40 | (long)'u' << 32 | (long)'t' << 24 | (long)'e' << 16, - (long)'a' << 56 | (long)'c' << 48 | (long)'i' << 40 | (long)'r' << 32 | (long)'c' << 24, - (long)'a' << 56 | (long)'c' << 48 | (long)'u' << 40 | (long)'t' << 32 | (long)'e' << 24, - (long)'a' << 56 | (long)'e' << 48 | (long)'l' << 40 | (long)'i' << 32 | (long)'g' << 24, - (long)'a' << 56 | (long)'g' << 48 | (long)'r' << 40 | (long)'a' << 32 | (long)'v' << 24 | (long)'e' << 16, - (long)'a' << 56 | (long)'l' << 48 | (long)'e' << 40 | (long)'f' << 32 | (long)'s' << 24 | (long)'y' << 16 | (long)'m' << 8, - (long)'a' << 56 | (long)'l' << 48 | (long)'p' << 40 | (long)'h' << 32 | (long)'a' << 24, - (long)'a' << 56 | (long)'m' << 48 | (long)'p' << 40, - (long)'a' << 56 | (long)'n' << 48 | (long)'d' << 40, - (long)'a' << 56 | (long)'n' << 48 | (long)'g' << 40, - (long)'a' << 56 | (long)'p' << 48 | (long)'o' << 40 | (long)'s' << 32, - (long)'a' << 56 | (long)'r' << 48 | (long)'i' << 40 | (long)'n' << 32 | (long)'g' << 24, - (long)'a' << 56 | (long)'s' << 48 | (long)'y' << 40 | (long)'m' << 32 | (long)'p' << 24, - (long)'a' << 56 | (long)'t' << 48 | (long)'i' << 40 | (long)'l' << 32 | (long)'d' << 24 | (long)'e' << 16, - (long)'a' << 56 | (long)'u' << 48 | (long)'m' << 40 | (long)'l' << 32, - (long)'b' << 56 | (long)'d' << 48 | (long)'q' << 40 | (long)'u' << 32 | (long)'o' << 24, - (long)'b' << 56 | (long)'e' << 48 | (long)'t' << 40 | (long)'a' << 32, - (long)'b' << 56 | (long)'r' << 48 | (long)'v' << 40 | (long)'b' << 32 | (long)'a' << 24 | (long)'r' << 16, - (long)'b' << 56 | (long)'u' << 48 | (long)'l' << 40 | (long)'l' << 32, - (long)'c' << 56 | (long)'a' << 48 | (long)'p' << 40, - (long)'c' << 56 | (long)'c' << 48 | (long)'e' << 40 | (long)'d' << 32 | (long)'i' << 24 | (long)'l' << 16, - (long)'c' << 56 | (long)'e' << 48 | (long)'d' << 40 | (long)'i' << 32 | (long)'l' << 24, - (long)'c' << 56 | (long)'e' << 48 | (long)'n' << 40 | (long)'t' << 32, - (long)'c' << 56 | (long)'h' << 48 | (long)'i' << 40, - (long)'c' << 56 | (long)'i' << 48 | (long)'r' << 40 | (long)'c' << 32, - (long)'c' << 56 | (long)'l' << 48 | (long)'u' << 40 | (long)'b' << 32 | (long)'s' << 24, - (long)'c' << 56 | (long)'o' << 48 | (long)'n' << 40 | (long)'g' << 32, - (long)'c' << 56 | (long)'o' << 48 | (long)'p' << 40 | (long)'y' << 32, - (long)'c' << 56 | (long)'r' << 48 | (long)'a' << 40 | (long)'r' << 32 | (long)'r' << 24, - (long)'c' << 56 | (long)'u' << 48 | (long)'p' << 40, - (long)'c' << 56 | (long)'u' << 48 | (long)'r' << 40 | (long)'r' << 32 | (long)'e' << 24 | (long)'n' << 16, - (long)'d' << 56 | (long)'A' << 48 | (long)'r' << 40 | (long)'r' << 32, - (long)'d' << 56 | (long)'a' << 48 | (long)'g' << 40 | (long)'g' << 32 | (long)'e' << 24 | (long)'r' << 16, - (long)'d' << 56 | (long)'a' << 48 | (long)'r' << 40 | (long)'r' << 32, - (long)'d' << 56 | (long)'e' << 48 | (long)'g' << 40, - (long)'d' << 56 | (long)'e' << 48 | (long)'l' << 40 | (long)'t' << 32 | (long)'a' << 24, - (long)'d' << 56 | (long)'i' << 48 | (long)'a' << 40 | (long)'m' << 32 | (long)'s' << 24, - (long)'d' << 56 | (long)'i' << 48 | (long)'v' << 40 | (long)'i' << 32 | (long)'d' << 24 | (long)'e' << 16, - (long)'e' << 56 | (long)'a' << 48 | (long)'c' << 40 | (long)'u' << 32 | (long)'t' << 24 | (long)'e' << 16, - (long)'e' << 56 | (long)'c' << 48 | (long)'i' << 40 | (long)'r' << 32 | (long)'c' << 24, - (long)'e' << 56 | (long)'g' << 48 | (long)'r' << 40 | (long)'a' << 32 | (long)'v' << 24 | (long)'e' << 16, - (long)'e' << 56 | (long)'m' << 48 | (long)'p' << 40 | (long)'t' << 32 | (long)'y' << 24, - (long)'e' << 56 | (long)'m' << 48 | (long)'s' << 40 | (long)'p' << 32, - (long)'e' << 56 | (long)'n' << 48 | (long)'s' << 40 | (long)'p' << 32, - (long)'e' << 56 | (long)'p' << 48 | (long)'s' << 40 | (long)'i' << 32 | (long)'l' << 24 | (long)'o' << 16 | (long)'n' << 8, - (long)'e' << 56 | (long)'q' << 48 | (long)'u' << 40 | (long)'i' << 32 | (long)'v' << 24, - (long)'e' << 56 | (long)'t' << 48 | (long)'a' << 40, - (long)'e' << 56 | (long)'t' << 48 | (long)'h' << 40, - (long)'e' << 56 | (long)'u' << 48 | (long)'m' << 40 | (long)'l' << 32, - (long)'e' << 56 | (long)'u' << 48 | (long)'r' << 40 | (long)'o' << 32, - (long)'e' << 56 | (long)'x' << 48 | (long)'i' << 40 | (long)'s' << 32 | (long)'t' << 24, - (long)'f' << 56 | (long)'n' << 48 | (long)'o' << 40 | (long)'f' << 32, - (long)'f' << 56 | (long)'o' << 48 | (long)'r' << 40 | (long)'a' << 32 | (long)'l' << 24 | (long)'l' << 16, - (long)'f' << 56 | (long)'r' << 48 | (long)'a' << 40 | (long)'c' << 32 | (long)'1' << 24 | (long)'2' << 16, - (long)'f' << 56 | (long)'r' << 48 | (long)'a' << 40 | (long)'c' << 32 | (long)'1' << 24 | (long)'4' << 16, - (long)'f' << 56 | (long)'r' << 48 | (long)'a' << 40 | (long)'c' << 32 | (long)'3' << 24 | (long)'4' << 16, - (long)'f' << 56 | (long)'r' << 48 | (long)'a' << 40 | (long)'s' << 32 | (long)'l' << 24, - (long)'g' << 56 | (long)'a' << 48 | (long)'m' << 40 | (long)'m' << 32 | (long)'a' << 24, - (long)'g' << 56 | (long)'e' << 48, - (long)'g' << 56 | (long)'t' << 48, - (long)'h' << 56 | (long)'A' << 48 | (long)'r' << 40 | (long)'r' << 32, - (long)'h' << 56 | (long)'a' << 48 | (long)'r' << 40 | (long)'r' << 32, - (long)'h' << 56 | (long)'e' << 48 | (long)'a' << 40 | (long)'r' << 32 | (long)'t' << 24 | (long)'s' << 16, - (long)'h' << 56 | (long)'e' << 48 | (long)'l' << 40 | (long)'l' << 32 | (long)'i' << 24 | (long)'p' << 16, - (long)'i' << 56 | (long)'a' << 48 | (long)'c' << 40 | (long)'u' << 32 | (long)'t' << 24 | (long)'e' << 16, - (long)'i' << 56 | (long)'c' << 48 | (long)'i' << 40 | (long)'r' << 32 | (long)'c' << 24, - (long)'i' << 56 | (long)'e' << 48 | (long)'x' << 40 | (long)'c' << 32 | (long)'l' << 24, - (long)'i' << 56 | (long)'g' << 48 | (long)'r' << 40 | (long)'a' << 32 | (long)'v' << 24 | (long)'e' << 16, - (long)'i' << 56 | (long)'m' << 48 | (long)'a' << 40 | (long)'g' << 32 | (long)'e' << 24, - (long)'i' << 56 | (long)'n' << 48 | (long)'f' << 40 | (long)'i' << 32 | (long)'n' << 24, - (long)'i' << 56 | (long)'n' << 48 | (long)'t' << 40, - (long)'i' << 56 | (long)'o' << 48 | (long)'t' << 40 | (long)'a' << 32, - (long)'i' << 56 | (long)'q' << 48 | (long)'u' << 40 | (long)'e' << 32 | (long)'s' << 24 | (long)'t' << 16, - (long)'i' << 56 | (long)'s' << 48 | (long)'i' << 40 | (long)'n' << 32, - (long)'i' << 56 | (long)'u' << 48 | (long)'m' << 40 | (long)'l' << 32, - (long)'k' << 56 | (long)'a' << 48 | (long)'p' << 40 | (long)'p' << 32 | (long)'a' << 24, - (long)'l' << 56 | (long)'A' << 48 | (long)'r' << 40 | (long)'r' << 32, - (long)'l' << 56 | (long)'a' << 48 | (long)'m' << 40 | (long)'b' << 32 | (long)'d' << 24 | (long)'a' << 16, - (long)'l' << 56 | (long)'a' << 48 | (long)'n' << 40 | (long)'g' << 32, - (long)'l' << 56 | (long)'a' << 48 | (long)'q' << 40 | (long)'u' << 32 | (long)'o' << 24, - (long)'l' << 56 | (long)'a' << 48 | (long)'r' << 40 | (long)'r' << 32, - (long)'l' << 56 | (long)'c' << 48 | (long)'e' << 40 | (long)'i' << 32 | (long)'l' << 24, - (long)'l' << 56 | (long)'d' << 48 | (long)'q' << 40 | (long)'u' << 32 | (long)'o' << 24, - (long)'l' << 56 | (long)'e' << 48, - (long)'l' << 56 | (long)'f' << 48 | (long)'l' << 40 | (long)'o' << 32 | (long)'o' << 24 | (long)'r' << 16, - (long)'l' << 56 | (long)'o' << 48 | (long)'w' << 40 | (long)'a' << 32 | (long)'s' << 24 | (long)'t' << 16, - (long)'l' << 56 | (long)'o' << 48 | (long)'z' << 40, - (long)'l' << 56 | (long)'r' << 48 | (long)'m' << 40, - (long)'l' << 56 | (long)'s' << 48 | (long)'a' << 40 | (long)'q' << 32 | (long)'u' << 24 | (long)'o' << 16, - (long)'l' << 56 | (long)'s' << 48 | (long)'q' << 40 | (long)'u' << 32 | (long)'o' << 24, - (long)'l' << 56 | (long)'t' << 48, - (long)'m' << 56 | (long)'a' << 48 | (long)'c' << 40 | (long)'r' << 32, - (long)'m' << 56 | (long)'d' << 48 | (long)'a' << 40 | (long)'s' << 32 | (long)'h' << 24, - (long)'m' << 56 | (long)'i' << 48 | (long)'c' << 40 | (long)'r' << 32 | (long)'o' << 24, - (long)'m' << 56 | (long)'i' << 48 | (long)'d' << 40 | (long)'d' << 32 | (long)'o' << 24 | (long)'t' << 16, - (long)'m' << 56 | (long)'i' << 48 | (long)'n' << 40 | (long)'u' << 32 | (long)'s' << 24, - (long)'m' << 56 | (long)'u' << 48, - (long)'n' << 56 | (long)'a' << 48 | (long)'b' << 40 | (long)'l' << 32 | (long)'a' << 24, - (long)'n' << 56 | (long)'b' << 48 | (long)'s' << 40 | (long)'p' << 32, - (long)'n' << 56 | (long)'d' << 48 | (long)'a' << 40 | (long)'s' << 32 | (long)'h' << 24, - (long)'n' << 56 | (long)'e' << 48, - (long)'n' << 56 | (long)'i' << 48, - (long)'n' << 56 | (long)'o' << 48 | (long)'t' << 40, - (long)'n' << 56 | (long)'o' << 48 | (long)'t' << 40 | (long)'i' << 32 | (long)'n' << 24, - (long)'n' << 56 | (long)'s' << 48 | (long)'u' << 40 | (long)'b' << 32, - (long)'n' << 56 | (long)'t' << 48 | (long)'i' << 40 | (long)'l' << 32 | (long)'d' << 24 | (long)'e' << 16, - (long)'n' << 56 | (long)'u' << 48, - (long)'o' << 56 | (long)'a' << 48 | (long)'c' << 40 | (long)'u' << 32 | (long)'t' << 24 | (long)'e' << 16, - (long)'o' << 56 | (long)'c' << 48 | (long)'i' << 40 | (long)'r' << 32 | (long)'c' << 24, - (long)'o' << 56 | (long)'e' << 48 | (long)'l' << 40 | (long)'i' << 32 | (long)'g' << 24, - (long)'o' << 56 | (long)'g' << 48 | (long)'r' << 40 | (long)'a' << 32 | (long)'v' << 24 | (long)'e' << 16, - (long)'o' << 56 | (long)'l' << 48 | (long)'i' << 40 | (long)'n' << 32 | (long)'e' << 24, - (long)'o' << 56 | (long)'m' << 48 | (long)'e' << 40 | (long)'g' << 32 | (long)'a' << 24, - (long)'o' << 56 | (long)'m' << 48 | (long)'i' << 40 | (long)'c' << 32 | (long)'r' << 24 | (long)'o' << 16 | (long)'n' << 8, - (long)'o' << 56 | (long)'p' << 48 | (long)'l' << 40 | (long)'u' << 32 | (long)'s' << 24, - (long)'o' << 56 | (long)'r' << 48, - (long)'o' << 56 | (long)'r' << 48 | (long)'d' << 40 | (long)'f' << 32, - (long)'o' << 56 | (long)'r' << 48 | (long)'d' << 40 | (long)'m' << 32, - (long)'o' << 56 | (long)'s' << 48 | (long)'l' << 40 | (long)'a' << 32 | (long)'s' << 24 | (long)'h' << 16, - (long)'o' << 56 | (long)'t' << 48 | (long)'i' << 40 | (long)'l' << 32 | (long)'d' << 24 | (long)'e' << 16, - (long)'o' << 56 | (long)'t' << 48 | (long)'i' << 40 | (long)'m' << 32 | (long)'e' << 24 | (long)'s' << 16, - (long)'o' << 56 | (long)'u' << 48 | (long)'m' << 40 | (long)'l' << 32, - (long)'p' << 56 | (long)'a' << 48 | (long)'r' << 40 | (long)'a' << 32, - (long)'p' << 56 | (long)'a' << 48 | (long)'r' << 40 | (long)'t' << 32, - (long)'p' << 56 | (long)'e' << 48 | (long)'r' << 40 | (long)'m' << 32 | (long)'i' << 24 | (long)'l' << 16, - (long)'p' << 56 | (long)'e' << 48 | (long)'r' << 40 | (long)'p' << 32, - (long)'p' << 56 | (long)'h' << 48 | (long)'i' << 40, - (long)'p' << 56 | (long)'i' << 48, - (long)'p' << 56 | (long)'i' << 48 | (long)'v' << 40, - (long)'p' << 56 | (long)'l' << 48 | (long)'u' << 40 | (long)'s' << 32 | (long)'m' << 24 | (long)'n' << 16, - (long)'p' << 56 | (long)'o' << 48 | (long)'u' << 40 | (long)'n' << 32 | (long)'d' << 24, - (long)'p' << 56 | (long)'r' << 48 | (long)'i' << 40 | (long)'m' << 32 | (long)'e' << 24, - (long)'p' << 56 | (long)'r' << 48 | (long)'o' << 40 | (long)'d' << 32, - (long)'p' << 56 | (long)'r' << 48 | (long)'o' << 40 | (long)'p' << 32, - (long)'p' << 56 | (long)'s' << 48 | (long)'i' << 40, - (long)'q' << 56 | (long)'u' << 48 | (long)'o' << 40 | (long)'t' << 32, - (long)'r' << 56 | (long)'A' << 48 | (long)'r' << 40 | (long)'r' << 32, - (long)'r' << 56 | (long)'a' << 48 | (long)'d' << 40 | (long)'i' << 32 | (long)'c' << 24, - (long)'r' << 56 | (long)'a' << 48 | (long)'n' << 40 | (long)'g' << 32, - (long)'r' << 56 | (long)'a' << 48 | (long)'q' << 40 | (long)'u' << 32 | (long)'o' << 24, - (long)'r' << 56 | (long)'a' << 48 | (long)'r' << 40 | (long)'r' << 32, - (long)'r' << 56 | (long)'c' << 48 | (long)'e' << 40 | (long)'i' << 32 | (long)'l' << 24, - (long)'r' << 56 | (long)'d' << 48 | (long)'q' << 40 | (long)'u' << 32 | (long)'o' << 24, - (long)'r' << 56 | (long)'e' << 48 | (long)'a' << 40 | (long)'l' << 32, - (long)'r' << 56 | (long)'e' << 48 | (long)'g' << 40, - (long)'r' << 56 | (long)'f' << 48 | (long)'l' << 40 | (long)'o' << 32 | (long)'o' << 24 | (long)'r' << 16, - (long)'r' << 56 | (long)'h' << 48 | (long)'o' << 40, - (long)'r' << 56 | (long)'l' << 48 | (long)'m' << 40, - (long)'r' << 56 | (long)'s' << 48 | (long)'a' << 40 | (long)'q' << 32 | (long)'u' << 24 | (long)'o' << 16, - (long)'r' << 56 | (long)'s' << 48 | (long)'q' << 40 | (long)'u' << 32 | (long)'o' << 24, - (long)'s' << 56 | (long)'b' << 48 | (long)'q' << 40 | (long)'u' << 32 | (long)'o' << 24, - (long)'s' << 56 | (long)'c' << 48 | (long)'a' << 40 | (long)'r' << 32 | (long)'o' << 24 | (long)'n' << 16, - (long)'s' << 56 | (long)'d' << 48 | (long)'o' << 40 | (long)'t' << 32, - (long)'s' << 56 | (long)'e' << 48 | (long)'c' << 40 | (long)'t' << 32, - (long)'s' << 56 | (long)'h' << 48 | (long)'y' << 40, - (long)'s' << 56 | (long)'i' << 48 | (long)'g' << 40 | (long)'m' << 32 | (long)'a' << 24, - (long)'s' << 56 | (long)'i' << 48 | (long)'g' << 40 | (long)'m' << 32 | (long)'a' << 24 | (long)'f' << 16, - (long)'s' << 56 | (long)'i' << 48 | (long)'m' << 40, - (long)'s' << 56 | (long)'p' << 48 | (long)'a' << 40 | (long)'d' << 32 | (long)'e' << 24 | (long)'s' << 16, - (long)'s' << 56 | (long)'u' << 48 | (long)'b' << 40, - (long)'s' << 56 | (long)'u' << 48 | (long)'b' << 40 | (long)'e' << 32, - (long)'s' << 56 | (long)'u' << 48 | (long)'m' << 40, - (long)'s' << 56 | (long)'u' << 48 | (long)'p' << 40, - (long)'s' << 56 | (long)'u' << 48 | (long)'p' << 40 | (long)'1' << 32, - (long)'s' << 56 | (long)'u' << 48 | (long)'p' << 40 | (long)'2' << 32, - (long)'s' << 56 | (long)'u' << 48 | (long)'p' << 40 | (long)'3' << 32, - (long)'s' << 56 | (long)'u' << 48 | (long)'p' << 40 | (long)'e' << 32, - (long)'s' << 56 | (long)'z' << 48 | (long)'l' << 40 | (long)'i' << 32 | (long)'g' << 24, - (long)'t' << 56 | (long)'a' << 48 | (long)'u' << 40, - (long)'t' << 56 | (long)'h' << 48 | (long)'e' << 40 | (long)'r' << 32 | (long)'e' << 24 | (long)'4' << 16, - (long)'t' << 56 | (long)'h' << 48 | (long)'e' << 40 | (long)'t' << 32 | (long)'a' << 24, - (long)'t' << 56 | (long)'h' << 48 | (long)'e' << 40 | (long)'t' << 32 | (long)'a' << 24 | (long)'s' << 16 | (long)'y' << 8 | (long)'m' << 0, - (long)'t' << 56 | (long)'h' << 48 | (long)'i' << 40 | (long)'n' << 32 | (long)'s' << 24 | (long)'p' << 16, - (long)'t' << 56 | (long)'h' << 48 | (long)'o' << 40 | (long)'r' << 32 | (long)'n' << 24, - (long)'t' << 56 | (long)'i' << 48 | (long)'l' << 40 | (long)'d' << 32 | (long)'e' << 24, - (long)'t' << 56 | (long)'i' << 48 | (long)'m' << 40 | (long)'e' << 32 | (long)'s' << 24, - (long)'t' << 56 | (long)'r' << 48 | (long)'a' << 40 | (long)'d' << 32 | (long)'e' << 24, - (long)'u' << 56 | (long)'A' << 48 | (long)'r' << 40 | (long)'r' << 32, - (long)'u' << 56 | (long)'a' << 48 | (long)'c' << 40 | (long)'u' << 32 | (long)'t' << 24 | (long)'e' << 16, - (long)'u' << 56 | (long)'a' << 48 | (long)'r' << 40 | (long)'r' << 32, - (long)'u' << 56 | (long)'c' << 48 | (long)'i' << 40 | (long)'r' << 32 | (long)'c' << 24, - (long)'u' << 56 | (long)'g' << 48 | (long)'r' << 40 | (long)'a' << 32 | (long)'v' << 24 | (long)'e' << 16, - (long)'u' << 56 | (long)'m' << 48 | (long)'l' << 40, - (long)'u' << 56 | (long)'p' << 48 | (long)'s' << 40 | (long)'i' << 32 | (long)'h' << 24, - (long)'u' << 56 | (long)'p' << 48 | (long)'s' << 40 | (long)'i' << 32 | (long)'l' << 24 | (long)'o' << 16 | (long)'n' << 8, - (long)'u' << 56 | (long)'u' << 48 | (long)'m' << 40 | (long)'l' << 32, - (long)'w' << 56 | (long)'e' << 48 | (long)'i' << 40 | (long)'e' << 32 | (long)'r' << 24 | (long)'p' << 16, - (long)'x' << 56 | (long)'i' << 48, - (long)'y' << 56 | (long)'a' << 48 | (long)'c' << 40 | (long)'u' << 32 | (long)'t' << 24 | (long)'e' << 16, - (long)'y' << 56 | (long)'e' << 48 | (long)'n' << 40, - (long)'y' << 56 | (long)'u' << 48 | (long)'m' << 40 | (long)'l' << 32, - (long)'z' << 56 | (long)'e' << 48 | (long)'t' << 40 | (long)'a' << 32, - (long)'z' << 56 | (long)'w' << 48 | (long)'j' << 40, - (long)'z' << 56 | (long)'w' << 48 | (long)'n' << 40 | (long)'j' << 32 - }; - - static readonly char[] entities_values = new char[] { - '\u00C6', - '\u00C1', - '\u00C2', - '\u00C0', - '\u0391', - '\u00C5', - '\u00C3', - '\u00C4', - '\u0392', - '\u00C7', - '\u03A7', - '\u2021', - '\u0394', - '\u00D0', - '\u00C9', - '\u00CA', - '\u00C8', - '\u0395', - '\u0397', - '\u00CB', - '\u0393', - '\u00CD', - '\u00CE', - '\u00CC', - '\u0399', - '\u00CF', - '\u039A', - '\u039B', - '\u039C', - '\u00D1', - '\u039D', - '\u0152', - '\u00D3', - '\u00D4', - '\u00D2', - '\u03A9', - '\u039F', - '\u00D8', - '\u00D5', - '\u00D6', - '\u03A6', - '\u03A0', - '\u2033', - '\u03A8', - '\u03A1', - '\u0160', - '\u03A3', - '\u00DE', - '\u03A4', - '\u0398', - '\u00DA', - '\u00DB', - '\u00D9', - '\u03A5', - '\u00DC', - '\u039E', - '\u00DD', - '\u0178', - '\u0396', - '\u00E1', - '\u00E2', - '\u00B4', - '\u00E6', - '\u00E0', - '\u2135', - '\u03B1', - '\u0026', - '\u2227', - '\u2220', - '\u0027', - '\u00E5', - '\u2248', - '\u00E3', - '\u00E4', - '\u201E', - '\u03B2', - '\u00A6', - '\u2022', - '\u2229', - '\u00E7', - '\u00B8', - '\u00A2', - '\u03C7', - '\u02C6', - '\u2663', - '\u2245', - '\u00A9', - '\u21B5', - '\u222A', - '\u00A4', - '\u21D3', - '\u2020', - '\u2193', - '\u00B0', - '\u03B4', - '\u2666', - '\u00F7', - '\u00E9', - '\u00EA', - '\u00E8', - '\u2205', - '\u2003', - '\u2002', - '\u03B5', - '\u2261', - '\u03B7', - '\u00F0', - '\u00EB', - '\u20AC', - '\u2203', - '\u0192', - '\u2200', - '\u00BD', - '\u00BC', - '\u00BE', - '\u2044', - '\u03B3', - '\u2265', - '\u003E', - '\u21D4', - '\u2194', - '\u2665', - '\u2026', - '\u00ED', - '\u00EE', - '\u00A1', - '\u00EC', - '\u2111', - '\u221E', - '\u222B', - '\u03B9', - '\u00BF', - '\u2208', - '\u00EF', - '\u03BA', - '\u21D0', - '\u03BB', - '\u2329', - '\u00AB', - '\u2190', - '\u2308', - '\u201C', - '\u2264', - '\u230A', - '\u2217', - '\u25CA', - '\u200E', - '\u2039', - '\u2018', - '\u003C', - '\u00AF', - '\u2014', - '\u00B5', - '\u00B7', - '\u2212', - '\u03BC', - '\u2207', - '\u00A0', - '\u2013', - '\u2260', - '\u220B', - '\u00AC', - '\u2209', - '\u2284', - '\u00F1', - '\u03BD', - '\u00F3', - '\u00F4', - '\u0153', - '\u00F2', - '\u203E', - '\u03C9', - '\u03BF', - '\u2295', - '\u2228', - '\u00AA', - '\u00BA', - '\u00F8', - '\u00F5', - '\u2297', - '\u00F6', - '\u00B6', - '\u2202', - '\u2030', - '\u22A5', - '\u03C6', - '\u03C0', - '\u03D6', - '\u00B1', - '\u00A3', - '\u2032', - '\u220F', - '\u221D', - '\u03C8', - '\u0022', - '\u21D2', - '\u221A', - '\u232A', - '\u00BB', - '\u2192', - '\u2309', - '\u201D', - '\u211C', - '\u00AE', - '\u230B', - '\u03C1', - '\u200F', - '\u203A', - '\u2019', - '\u201A', - '\u0161', - '\u22C5', - '\u00A7', - '\u00AD', - '\u03C3', - '\u03C2', - '\u223C', - '\u2660', - '\u2282', - '\u2286', - '\u2211', - '\u2283', - '\u00B9', - '\u00B2', - '\u00B3', - '\u2287', - '\u00DF', - '\u03C4', - '\u2234', - '\u03B8', - '\u03D1', - '\u2009', - '\u00FE', - '\u02DC', - '\u00D7', - '\u2122', - '\u21D1', - '\u00FA', - '\u2191', - '\u00FB', - '\u00F9', - '\u00A8', - '\u03D2', - '\u03C5', - '\u00FC', - '\u2118', - '\u03BE', - '\u00FD', - '\u00A5', - '\u00FF', - '\u03B6', - '\u200D', - '\u200C' - }; - - #region Methods - - static void WriteCharBytes(IList buf, char ch, Encoding e) - { - if (ch > 255) - { - foreach (byte b in e.GetBytes(new char[] { ch })) - buf.Add(b); - } - else - buf.Add((byte)ch); - } - - public static string UrlDecode(string s, Encoding e) - { - if (null == s) - return null; - - if (s.IndexOf('%') == -1 && s.IndexOf('+') == -1) - return s; - - if (e == null) - e = Encoding.UTF8; - - long len = s.Length; - var bytes = new List<byte>(); - int xchar; - char ch; - - for (int i = 0; i < len; i++) - { - ch = s[i]; - if (ch == '%' && i + 2 < len && s[i + 1] != '%') - { - if (s[i + 1] == 'u' && i + 5 < len) - { - // unicode hex sequence - xchar = GetChar(s, i + 2, 4); - if (xchar != -1) - { - WriteCharBytes(bytes, (char)xchar, e); - i += 5; - } - else - WriteCharBytes(bytes, '%', e); - } - else if ((xchar = GetChar(s, i + 1, 2)) != -1) - { - WriteCharBytes(bytes, (char)xchar, e); - i += 2; - } - else - { - WriteCharBytes(bytes, '%', e); - } - continue; - } - - if (ch == '+') - WriteCharBytes(bytes, ' ', e); - else - WriteCharBytes(bytes, ch, e); - } - - byte[] buf = bytes.ToArray(bytes.Count); - bytes = null; - return e.GetString(buf, 0, buf.Length); - - } - - static int GetInt(byte b) - { - char c = (char)b; - if (c >= '0' && c <= '9') - return c - '0'; - - if (c >= 'a' && c <= 'f') - return c - 'a' + 10; - - if (c >= 'A' && c <= 'F') - return c - 'A' + 10; - - return -1; - } - - static int GetChar(string str, int offset, int length) - { - int val = 0; - int end = length + offset; - for (int i = offset; i < end; i++) - { - char c = str[i]; - if (c > 127) - return -1; - - int current = GetInt((byte)c); - if (current == -1) - return -1; - val = (val << 4) + current; - } - - return val; - } - - static bool TryConvertKeyToEntity(string key, out char value) - { - var token = CalculateKeyValue(key); - if (token == 0) - { - value = '\0'; - return false; - } - - var idx = Array.BinarySearch(entities, token); - if (idx < 0) - { - value = '\0'; - return false; - } - - value = entities_values[idx]; - return true; - } - - static long CalculateKeyValue(string s) - { - if (s.Length > 8) - return 0; - - long key = 0; - for (int i = 0; i < s.Length; ++i) - { - long ch = s[i]; - if (ch > 'z' || ch < '0') - return 0; - - key |= ch << ((7 - i) * 8); - } - - return key; - } - - /// <summary> - /// Decodes an HTML-encoded string and returns the decoded string. - /// </summary> - /// <param name="s">The HTML string to decode. </param> - /// <returns>The decoded text.</returns> - public static string HtmlDecode(string s) - { - if (s == null) - throw new ArgumentNullException("s"); - - if (s.IndexOf('&') == -1) - return s; - - StringBuilder entity = new StringBuilder(); - StringBuilder output = new StringBuilder(); - int len = s.Length; - // 0 -> nothing, - // 1 -> right after '&' - // 2 -> between '&' and ';' but no '#' - // 3 -> '#' found after '&' and getting numbers - int state = 0; - int number = 0; - int digit_start = 0; - bool hex_number = false; - - for (int i = 0; i < len; i++) - { - char c = s[i]; - if (state == 0) - { - if (c == '&') - { - entity.Append(c); - state = 1; - } - else - { - output.Append(c); - } - continue; - } - - if (c == '&') - { - state = 1; - if (digit_start > 0) - { - entity.Append(s, digit_start, i - digit_start); - digit_start = 0; - } - - output.Append(entity.ToString()); - entity.Length = 0; - entity.Append('&'); - continue; - } - - switch (state) - { - case 1: - if (c == ';') - { - state = 0; - output.Append(entity.ToString()); - output.Append(c); - entity.Length = 0; - break; - } - - number = 0; - hex_number = false; - if (c != '#') - { - state = 2; - } - else - { - state = 3; - } - entity.Append(c); - - break; - case 2: - entity.Append(c); - if (c == ';') - { - string key = entity.ToString(); - state = 0; - entity.Length = 0; - - if (key.Length > 1) - { - var skey = key.Substring(1, key.Length - 2); - if (TryConvertKeyToEntity(skey, out c)) - { - output.Append(c); - break; - } - } - - output.Append(key); - } - - break; - case 3: - if (c == ';') - { - if (number < 0x10000) - { - output.Append((char)number); - } - else - { - output.Append((char)(0xd800 + ((number - 0x10000) >> 10))); - output.Append((char)(0xdc00 + ((number - 0x10000) & 0x3ff))); - } - state = 0; - entity.Length = 0; - digit_start = 0; - break; - } - - if (c == 'x' || c == 'X' && !hex_number) - { - digit_start = i; - hex_number = true; - break; - } - - if (Char.IsDigit(c)) - { - if (digit_start == 0) - digit_start = i; - - number = number * (hex_number ? 16 : 10) + ((int)c - '0'); - break; - } - - if (hex_number) - { - if (c >= 'a' && c <= 'f') - { - number = number * 16 + 10 + ((int)c - 'a'); - break; - } - if (c >= 'A' && c <= 'F') - { - number = number * 16 + 10 + ((int)c - 'A'); - break; - } - } - - state = 2; - if (digit_start > 0) - { - entity.Append(s, digit_start, i - digit_start); - digit_start = 0; - } - - entity.Append(c); - break; - } - } - - if (entity.Length > 0) - { - output.Append(entity); - } - else if (digit_start > 0) - { - output.Append(s, digit_start, s.Length - digit_start); - } - return output.ToString(); - } - - public static QueryParamCollection ParseQueryString(string query) - { - return ParseQueryString(query, Encoding.UTF8); - } - - public static QueryParamCollection ParseQueryString(string query, Encoding encoding) - { - if (query == null) - throw new ArgumentNullException("query"); - if (encoding == null) - throw new ArgumentNullException("encoding"); - if (query.Length == 0 || (query.Length == 1 && query[0] == '?')) - return new QueryParamCollection(); - if (query[0] == '?') - query = query.Substring(1); - - QueryParamCollection result = new QueryParamCollection(); - ParseQueryString(query, encoding, result); - return result; - } - - internal static void ParseQueryString(string query, Encoding encoding, QueryParamCollection result) - { - if (query.Length == 0) - return; - - string decoded = HtmlDecode(query); - int decodedLength = decoded.Length; - int namePos = 0; - bool first = true; - while (namePos <= decodedLength) - { - int valuePos = -1, valueEnd = -1; - for (int q = namePos; q < decodedLength; q++) - { - if (valuePos == -1 && decoded[q] == '=') - { - valuePos = q + 1; - } - else if (decoded[q] == '&') - { - valueEnd = q; - break; - } - } - - if (first) - { - first = false; - if (decoded[namePos] == '?') - namePos++; - } - - string name, value; - if (valuePos == -1) - { - name = null; - valuePos = namePos; - } - else - { - name = UrlDecode(decoded.Substring(namePos, valuePos - namePos - 1), encoding); - } - if (valueEnd < 0) - { - namePos = -1; - valueEnd = decoded.Length; - } - else - { - namePos = valueEnd + 1; - } - value = UrlDecode(decoded.Substring(valuePos, valueEnd - valuePos), encoding); - - result.Add(name, value); - if (namePos == -1) - break; - } - } - #endregion // Methods - } -} diff --git a/Emby.Server.Implementations/HttpServer/SocketSharp/RequestMono.cs b/Emby.Server.Implementations/HttpServer/SocketSharp/RequestMono.cs deleted file mode 100644 index ec14c32c8..000000000 --- a/Emby.Server.Implementations/HttpServer/SocketSharp/RequestMono.cs +++ /dev/null @@ -1,846 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.Specialized; -using System.Globalization; -using System.IO; -using System.Net; -using System.Text; -using System.Threading.Tasks; -using MediaBrowser.Model.Services; - -namespace Emby.Server.Implementations.HttpServer.SocketSharp -{ - public partial class WebSocketSharpRequest : IHttpRequest - { - static internal string GetParameter(string header, string attr) - { - int ap = header.IndexOf(attr); - if (ap == -1) - return null; - - ap += attr.Length; - if (ap >= header.Length) - return null; - - char ending = header[ap]; - if (ending != '"') - ending = ' '; - - int end = header.IndexOf(ending, ap + 1); - if (end == -1) - return ending == '"' ? null : header.Substring(ap); - - return header.Substring(ap + 1, end - ap - 1); - } - - async Task LoadMultiPart() - { - string boundary = GetParameter(ContentType, "; boundary="); - if (boundary == null) - return; - - using (var requestStream = GetSubStream(InputStream, _memoryStreamProvider)) - { - //DB: 30/01/11 - Hack to get around non-seekable stream and received HTTP request - //Not ending with \r\n? - var ms = _memoryStreamProvider.CreateNew(32 * 1024); - await requestStream.CopyToAsync(ms).ConfigureAwait(false); - - var input = ms; - ms.WriteByte((byte)'\r'); - ms.WriteByte((byte)'\n'); - - input.Position = 0; - - //Uncomment to debug - //var content = new StreamReader(ms).ReadToEnd(); - //Console.WriteLine(boundary + "::" + content); - //input.Position = 0; - - var multi_part = new HttpMultipart(input, boundary, ContentEncoding); - - HttpMultipart.Element e; - while ((e = multi_part.ReadNextElement()) != null) - { - if (e.Filename == null) - { - byte[] copy = new byte[e.Length]; - - input.Position = e.Start; - input.Read(copy, 0, (int)e.Length); - - form.Add(e.Name, (e.Encoding ?? ContentEncoding).GetString(copy, 0, copy.Length)); - } - else - { - // - // We use a substream, as in 2.x we will support large uploads streamed to disk, - // - HttpPostedFile sub = new HttpPostedFile(e.Filename, e.ContentType, input, e.Start, e.Length); - files[e.Name] = sub; - } - } - } - } - - public QueryParamCollection Form - { - get - { - if (form == null) - { - form = new WebROCollection(); - files = new Dictionary<string, HttpPostedFile>(); - - if (IsContentType("multipart/form-data", true)) - { - var task = LoadMultiPart(); - Task.WaitAll(task); - } - else if (IsContentType("application/x-www-form-urlencoded", true)) - { - var task = LoadWwwForm(); - Task.WaitAll(task); - } - - form.Protect(); - } - -#if NET_4_0 - if (validateRequestNewMode && !checked_form) { - // Setting this before calling the validator prevents - // possible endless recursion - checked_form = true; - ValidateNameValueCollection ("Form", query_string_nvc, RequestValidationSource.Form); - } else -#endif - if (validate_form && !checked_form) - { - checked_form = true; - ValidateNameValueCollection("Form", form); - } - - return form; - } - } - - public string Accept - { - get - { - return string.IsNullOrEmpty(request.Headers["Accept"]) ? null : request.Headers["Accept"]; - } - } - - public string Authorization - { - get - { - return string.IsNullOrEmpty(request.Headers["Authorization"]) ? null : request.Headers["Authorization"]; - } - } - - protected bool validate_cookies, validate_query_string, validate_form; - protected bool checked_cookies, checked_query_string, checked_form; - - static void ThrowValidationException(string name, string key, string value) - { - string v = "\"" + value + "\""; - if (v.Length > 20) - v = v.Substring(0, 16) + "...\""; - - string msg = String.Format("A potentially dangerous Request.{0} value was " + - "detected from the client ({1}={2}).", name, key, v); - - throw new Exception(msg); - } - - static void ValidateNameValueCollection(string name, QueryParamCollection coll) - { - if (coll == null) - return; - - foreach (var pair in coll) - { - var key = pair.Name; - var val = pair.Value; - if (val != null && val.Length > 0 && IsInvalidString(val)) - ThrowValidationException(name, key, val); - } - } - - internal static bool IsInvalidString(string val) - { - int validationFailureIndex; - - return IsInvalidString(val, out validationFailureIndex); - } - - internal static bool IsInvalidString(string val, out int validationFailureIndex) - { - validationFailureIndex = 0; - - int len = val.Length; - if (len < 2) - return false; - - char current = val[0]; - for (int idx = 1; idx < len; idx++) - { - char next = val[idx]; - // See http://secunia.com/advisories/14325 - if (current == '<' || current == '\xff1c') - { - if (next == '!' || next < ' ' - || (next >= 'a' && next <= 'z') - || (next >= 'A' && next <= 'Z')) - { - validationFailureIndex = idx - 1; - return true; - } - } - else if (current == '&' && next == '#') - { - validationFailureIndex = idx - 1; - return true; - } - - current = next; - } - - return false; - } - - public void ValidateInput() - { - validate_cookies = true; - validate_query_string = true; - validate_form = true; - } - - bool IsContentType(string ct, bool starts_with) - { - if (ct == null || ContentType == null) return false; - - if (starts_with) - return StrUtils.StartsWith(ContentType, ct, true); - - return string.Equals(ContentType, ct, StringComparison.OrdinalIgnoreCase); - } - - async Task LoadWwwForm() - { - using (Stream input = GetSubStream(InputStream, _memoryStreamProvider)) - { - using (var ms = _memoryStreamProvider.CreateNew()) - { - await input.CopyToAsync(ms).ConfigureAwait(false); - ms.Position = 0; - - using (StreamReader s = new StreamReader(ms, ContentEncoding)) - { - StringBuilder key = new StringBuilder(); - StringBuilder value = new StringBuilder(); - int c; - - while ((c = s.Read()) != -1) - { - if (c == '=') - { - value.Length = 0; - while ((c = s.Read()) != -1) - { - if (c == '&') - { - AddRawKeyValue(key, value); - break; - } - else - value.Append((char)c); - } - if (c == -1) - { - AddRawKeyValue(key, value); - return; - } - } - else if (c == '&') - AddRawKeyValue(key, value); - else - key.Append((char)c); - } - if (c == -1) - AddRawKeyValue(key, value); - } - } - } - } - - void AddRawKeyValue(StringBuilder key, StringBuilder value) - { - string decodedKey = WebUtility.UrlDecode(key.ToString()); - form.Add(decodedKey, - WebUtility.UrlDecode(value.ToString())); - - key.Length = 0; - value.Length = 0; - } - - WebROCollection form; - - Dictionary<string, HttpPostedFile> files; - - class WebROCollection : QueryParamCollection - { - bool got_id; - int id; - - public bool GotID - { - get { return got_id; } - } - - public int ID - { - get { return id; } - set - { - got_id = true; - id = value; - } - } - public void Protect() - { - //IsReadOnly = true; - } - - public void Unprotect() - { - //IsReadOnly = false; - } - - public override string ToString() - { - StringBuilder result = new StringBuilder(); - foreach (var pair in this) - { - if (result.Length > 0) - result.Append('&'); - - var key = pair.Name; - if (key != null && key.Length > 0) - { - result.Append(key); - result.Append('='); - } - result.Append(pair.Value); - } - - return result.ToString(); - } - } - - public sealed class HttpPostedFile - { - string name; - string content_type; - Stream stream; - - class ReadSubStream : Stream - { - Stream s; - long offset; - long end; - long position; - - public ReadSubStream(Stream s, long offset, long length) - { - this.s = s; - this.offset = offset; - this.end = offset + length; - position = offset; - } - - public override void Flush() - { - } - - public override int Read(byte[] buffer, int dest_offset, int count) - { - if (buffer == null) - throw new ArgumentNullException("buffer"); - - if (dest_offset < 0) - throw new ArgumentOutOfRangeException("dest_offset", "< 0"); - - if (count < 0) - throw new ArgumentOutOfRangeException("count", "< 0"); - - int len = buffer.Length; - if (dest_offset > len) - throw new ArgumentException("destination offset is beyond array size"); - // reordered to avoid possible integer overflow - if (dest_offset > len - count) - throw new ArgumentException("Reading would overrun buffer"); - - if (count > end - position) - count = (int)(end - position); - - if (count <= 0) - return 0; - - s.Position = position; - int result = s.Read(buffer, dest_offset, count); - if (result > 0) - position += result; - else - position = end; - - return result; - } - - public override int ReadByte() - { - if (position >= end) - return -1; - - s.Position = position; - int result = s.ReadByte(); - if (result < 0) - position = end; - else - position++; - - return result; - } - - public override long Seek(long d, SeekOrigin origin) - { - long real; - switch (origin) - { - case SeekOrigin.Begin: - real = offset + d; - break; - case SeekOrigin.End: - real = end + d; - break; - case SeekOrigin.Current: - real = position + d; - break; - default: - throw new ArgumentException(); - } - - long virt = real - offset; - if (virt < 0 || virt > Length) - throw new ArgumentException(); - - position = s.Seek(real, SeekOrigin.Begin); - return position; - } - - public override void SetLength(long value) - { - throw new NotSupportedException(); - } - - public override void Write(byte[] buffer, int offset, int count) - { - throw new NotSupportedException(); - } - - public override bool CanRead - { - get { return true; } - } - public override bool CanSeek - { - get { return true; } - } - public override bool CanWrite - { - get { return false; } - } - - public override long Length - { - get { return end - offset; } - } - - public override long Position - { - get - { - return position - offset; - } - set - { - if (value > Length) - throw new ArgumentOutOfRangeException(); - - position = Seek(value, SeekOrigin.Begin); - } - } - } - - internal HttpPostedFile(string name, string content_type, Stream base_stream, long offset, long length) - { - this.name = name; - this.content_type = content_type; - this.stream = new ReadSubStream(base_stream, offset, length); - } - - public string ContentType - { - get - { - return content_type; - } - } - - public int ContentLength - { - get - { - return (int)stream.Length; - } - } - - public string FileName - { - get - { - return name; - } - } - - public Stream InputStream - { - get - { - return stream; - } - } - } - - class Helpers - { - public static readonly CultureInfo InvariantCulture = CultureInfo.InvariantCulture; - } - - internal sealed class StrUtils - { - public static bool StartsWith(string str1, string str2, bool ignore_case) - { - if (string.IsNullOrWhiteSpace(str1)) - { - return false; - } - - var comparison = ignore_case ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal; - return str1.IndexOf(str2, comparison) == 0; - } - - public static bool EndsWith(string str1, string str2, bool ignore_case) - { - int l2 = str2.Length; - if (l2 == 0) - return true; - - int l1 = str1.Length; - if (l2 > l1) - return false; - - var comparison = ignore_case ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal; - return str1.IndexOf(str2, comparison) == str1.Length - str2.Length - 1; - } - } - - class HttpMultipart - { - - public class Element - { - public string ContentType; - public string Name; - public string Filename; - public Encoding Encoding; - public long Start; - public long Length; - - public override string ToString() - { - return "ContentType " + ContentType + ", Name " + Name + ", Filename " + Filename + ", Start " + - Start.ToString() + ", Length " + Length.ToString(); - } - } - - Stream data; - string boundary; - byte[] boundary_bytes; - byte[] buffer; - bool at_eof; - Encoding encoding; - StringBuilder sb; - - const byte HYPHEN = (byte)'-', LF = (byte)'\n', CR = (byte)'\r'; - - // See RFC 2046 - // In the case of multipart entities, in which one or more different - // sets of data are combined in a single body, a "multipart" media type - // field must appear in the entity's header. The body must then contain - // one or more body parts, each preceded by a boundary delimiter line, - // and the last one followed by a closing boundary delimiter line. - // After its boundary delimiter line, each body part then consists of a - // header area, a blank line, and a body area. Thus a body part is - // similar to an RFC 822 message in syntax, but different in meaning. - - public HttpMultipart(Stream data, string b, Encoding encoding) - { - this.data = data; - //DB: 30/01/11: cannot set or read the Position in HttpListener in Win.NET - //var ms = new MemoryStream(32 * 1024); - //data.CopyTo(ms); - //this.data = ms; - - boundary = b; - boundary_bytes = encoding.GetBytes(b); - buffer = new byte[boundary_bytes.Length + 2]; // CRLF or '--' - this.encoding = encoding; - sb = new StringBuilder(); - } - - string ReadLine() - { - // CRLF or LF are ok as line endings. - bool got_cr = false; - int b = 0; - sb.Length = 0; - while (true) - { - b = data.ReadByte(); - if (b == -1) - { - return null; - } - - if (b == LF) - { - break; - } - got_cr = b == CR; - sb.Append((char)b); - } - - if (got_cr) - sb.Length--; - - return sb.ToString(); - - } - - static string GetContentDispositionAttribute(string l, string name) - { - int idx = l.IndexOf(name + "=\""); - if (idx < 0) - return null; - int begin = idx + name.Length + "=\"".Length; - int end = l.IndexOf('"', begin); - if (end < 0) - return null; - if (begin == end) - return ""; - return l.Substring(begin, end - begin); - } - - string GetContentDispositionAttributeWithEncoding(string l, string name) - { - int idx = l.IndexOf(name + "=\""); - if (idx < 0) - return null; - int begin = idx + name.Length + "=\"".Length; - int end = l.IndexOf('"', begin); - if (end < 0) - return null; - if (begin == end) - return ""; - - string temp = l.Substring(begin, end - begin); - byte[] source = new byte[temp.Length]; - for (int i = temp.Length - 1; i >= 0; i--) - source[i] = (byte)temp[i]; - - return encoding.GetString(source, 0, source.Length); - } - - bool ReadBoundary() - { - try - { - string line = ReadLine(); - while (line == "") - line = ReadLine(); - if (line[0] != '-' || line[1] != '-') - return false; - - if (!StrUtils.EndsWith(line, boundary, false)) - return true; - } - catch - { - } - - return false; - } - - string ReadHeaders() - { - string s = ReadLine(); - if (s == "") - return null; - - return s; - } - - bool CompareBytes(byte[] orig, byte[] other) - { - for (int i = orig.Length - 1; i >= 0; i--) - if (orig[i] != other[i]) - return false; - - return true; - } - - long MoveToNextBoundary() - { - long retval = 0; - bool got_cr = false; - - int state = 0; - int c = data.ReadByte(); - while (true) - { - if (c == -1) - return -1; - - if (state == 0 && c == LF) - { - retval = data.Position - 1; - if (got_cr) - retval--; - state = 1; - c = data.ReadByte(); - } - else if (state == 0) - { - got_cr = c == CR; - c = data.ReadByte(); - } - else if (state == 1 && c == '-') - { - c = data.ReadByte(); - if (c == -1) - return -1; - - if (c != '-') - { - state = 0; - got_cr = false; - continue; // no ReadByte() here - } - - int nread = data.Read(buffer, 0, buffer.Length); - int bl = buffer.Length; - if (nread != bl) - return -1; - - if (!CompareBytes(boundary_bytes, buffer)) - { - state = 0; - data.Position = retval + 2; - if (got_cr) - { - data.Position++; - got_cr = false; - } - c = data.ReadByte(); - continue; - } - - if (buffer[bl - 2] == '-' && buffer[bl - 1] == '-') - { - at_eof = true; - } - else if (buffer[bl - 2] != CR || buffer[bl - 1] != LF) - { - state = 0; - data.Position = retval + 2; - if (got_cr) - { - data.Position++; - got_cr = false; - } - c = data.ReadByte(); - continue; - } - data.Position = retval + 2; - if (got_cr) - data.Position++; - break; - } - else - { - // state == 1 - state = 0; // no ReadByte() here - } - } - - return retval; - } - - public Element ReadNextElement() - { - if (at_eof || ReadBoundary()) - return null; - - Element elem = new Element(); - string header; - while ((header = ReadHeaders()) != null) - { - if (StrUtils.StartsWith(header, "Content-Disposition:", true)) - { - elem.Name = GetContentDispositionAttribute(header, "name"); - elem.Filename = StripPath(GetContentDispositionAttributeWithEncoding(header, "filename")); - } - else if (StrUtils.StartsWith(header, "Content-Type:", true)) - { - elem.ContentType = header.Substring("Content-Type:".Length).Trim(); - elem.Encoding = GetEncoding(elem.ContentType); - } - } - - long start = 0; - start = data.Position; - elem.Start = start; - long pos = MoveToNextBoundary(); - if (pos == -1) - return null; - - elem.Length = pos - start; - return elem; - } - - static string StripPath(string path) - { - if (path == null || path.Length == 0) - return path; - - if (path.IndexOf(":\\") != 1 && !path.StartsWith("\\\\")) - return path; - return path.Substring(path.LastIndexOf('\\') + 1); - } - } - - } -} diff --git a/Emby.Server.Implementations/HttpServer/SocketSharp/SharpWebSocket.cs b/Emby.Server.Implementations/HttpServer/SocketSharp/SharpWebSocket.cs deleted file mode 100644 index cc7a4557e..000000000 --- a/Emby.Server.Implementations/HttpServer/SocketSharp/SharpWebSocket.cs +++ /dev/null @@ -1,159 +0,0 @@ -using MediaBrowser.Common.Events; -using MediaBrowser.Controller.Net; -using MediaBrowser.Model.Logging; -using System; -using System.Threading; -using System.Threading.Tasks; -using WebSocketState = MediaBrowser.Model.Net.WebSocketState; - -namespace Emby.Server.Implementations.HttpServer.SocketSharp -{ - public class SharpWebSocket : IWebSocket - { - /// <summary> - /// The logger - /// </summary> - private readonly ILogger _logger; - - public event EventHandler<EventArgs> Closed; - - /// <summary> - /// Gets or sets the web socket. - /// </summary> - /// <value>The web socket.</value> - private SocketHttpListener.WebSocket WebSocket { get; set; } - - private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); - - public SharpWebSocket(SocketHttpListener.WebSocket socket, ILogger logger) - { - if (socket == null) - { - throw new ArgumentNullException("socket"); - } - - if (logger == null) - { - throw new ArgumentNullException("logger"); - } - - _logger = logger; - WebSocket = socket; - - socket.OnMessage += socket_OnMessage; - socket.OnClose += socket_OnClose; - socket.OnError += socket_OnError; - - WebSocket.ConnectAsServer(); - } - - void socket_OnError(object sender, SocketHttpListener.ErrorEventArgs e) - { - _logger.Error("Error in SharpWebSocket: {0}", e.Message ?? string.Empty); - //EventHelper.FireEventIfNotNull(Closed, this, EventArgs.Empty, _logger); - } - - void socket_OnClose(object sender, SocketHttpListener.CloseEventArgs e) - { - EventHelper.FireEventIfNotNull(Closed, this, EventArgs.Empty, _logger); - } - - void socket_OnMessage(object sender, SocketHttpListener.MessageEventArgs e) - { - //if (!string.IsNullOrWhiteSpace(e.Data)) - //{ - // if (OnReceive != null) - // { - // OnReceive(e.Data); - // } - // return; - //} - if (OnReceiveBytes != null) - { - OnReceiveBytes(e.RawData); - } - } - - /// <summary> - /// Gets or sets the state. - /// </summary> - /// <value>The state.</value> - public WebSocketState State - { - get - { - WebSocketState commonState; - - if (!Enum.TryParse(WebSocket.ReadyState.ToString(), true, out commonState)) - { - _logger.Warn("Unrecognized WebSocketState: {0}", WebSocket.ReadyState.ToString()); - } - - return commonState; - } - } - - /// <summary> - /// Sends the async. - /// </summary> - /// <param name="bytes">The bytes.</param> - /// <param name="endOfMessage">if set to <c>true</c> [end of message].</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task.</returns> - public Task SendAsync(byte[] bytes, bool endOfMessage, CancellationToken cancellationToken) - { - return WebSocket.SendAsync(bytes); - } - - /// <summary> - /// Sends the asynchronous. - /// </summary> - /// <param name="text">The text.</param> - /// <param name="endOfMessage">if set to <c>true</c> [end of message].</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task.</returns> - public Task SendAsync(string text, bool endOfMessage, CancellationToken cancellationToken) - { - return WebSocket.SendAsync(text); - } - - /// <summary> - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. - /// </summary> - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - /// <summary> - /// Releases unmanaged and - optionally - managed resources. - /// </summary> - /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param> - protected virtual void Dispose(bool dispose) - { - if (dispose) - { - WebSocket.OnMessage -= socket_OnMessage; - WebSocket.OnClose -= socket_OnClose; - WebSocket.OnError -= socket_OnError; - - _cancellationTokenSource.Cancel(); - - WebSocket.Close(); - } - } - - /// <summary> - /// Gets or sets the receive action. - /// </summary> - /// <value>The receive action.</value> - public Action<byte[]> OnReceiveBytes { get; set; } - - /// <summary> - /// Gets or sets the on receive. - /// </summary> - /// <value>The on receive.</value> - public Action<string> OnReceive { get; set; } - } -} diff --git a/Emby.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpListener.cs b/Emby.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpListener.cs deleted file mode 100644 index 8fb0d4f3e..000000000 --- a/Emby.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpListener.cs +++ /dev/null @@ -1,220 +0,0 @@ -using MediaBrowser.Controller.Net; -using MediaBrowser.Model.Logging; -using SocketHttpListener.Net; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Security.Cryptography.X509Certificates; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Common.Net; -using MediaBrowser.Model.Cryptography; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.Net; -using MediaBrowser.Model.Services; -using MediaBrowser.Model.System; -using MediaBrowser.Model.Text; -using SocketHttpListener.Primitives; - -namespace Emby.Server.Implementations.HttpServer.SocketSharp -{ - public class WebSocketSharpListener : IHttpListener - { - private HttpListener _listener; - - private readonly ILogger _logger; - private readonly X509Certificate _certificate; - private readonly IMemoryStreamFactory _memoryStreamProvider; - private readonly ITextEncoding _textEncoding; - private readonly INetworkManager _networkManager; - private readonly ISocketFactory _socketFactory; - private readonly ICryptoProvider _cryptoProvider; - private readonly IFileSystem _fileSystem; - private readonly bool _enableDualMode; - private readonly IEnvironmentInfo _environment; - - private CancellationTokenSource _disposeCancellationTokenSource = new CancellationTokenSource(); - private CancellationToken _disposeCancellationToken; - - public WebSocketSharpListener(ILogger logger, X509Certificate certificate, IMemoryStreamFactory memoryStreamProvider, ITextEncoding textEncoding, INetworkManager networkManager, ISocketFactory socketFactory, ICryptoProvider cryptoProvider, bool enableDualMode, IFileSystem fileSystem, IEnvironmentInfo environment) - { - _logger = logger; - _certificate = certificate; - _memoryStreamProvider = memoryStreamProvider; - _textEncoding = textEncoding; - _networkManager = networkManager; - _socketFactory = socketFactory; - _cryptoProvider = cryptoProvider; - _enableDualMode = enableDualMode; - _fileSystem = fileSystem; - _environment = environment; - - _disposeCancellationToken = _disposeCancellationTokenSource.Token; - } - - public Action<Exception, IRequest, bool> ErrorHandler { get; set; } - public Func<IHttpRequest, string, string, string, CancellationToken, Task> RequestHandler { get; set; } - - public Action<WebSocketConnectingEventArgs> WebSocketConnecting { get; set; } - - public Action<WebSocketConnectEventArgs> WebSocketConnected { get; set; } - - public void Start(IEnumerable<string> urlPrefixes) - { - if (_listener == null) - _listener = new HttpListener(_logger, _cryptoProvider, _socketFactory, _networkManager, _textEncoding, _memoryStreamProvider, _fileSystem, _environment); - - _listener.EnableDualMode = _enableDualMode; - - if (_certificate != null) - { - _listener.LoadCert(_certificate); - } - - foreach (var prefix in urlPrefixes) - { - _logger.Info("Adding HttpListener prefix " + prefix); - _listener.Prefixes.Add(prefix); - } - - _listener.OnContext = ProcessContext; - - _listener.Start(); - } - - private void ProcessContext(HttpListenerContext context) - { - //InitTask(context, _disposeCancellationToken); - Task.Run(() => InitTask(context, _disposeCancellationToken)); - } - - private Task InitTask(HttpListenerContext context, CancellationToken cancellationToken) - { - IHttpRequest httpReq = null; - var request = context.Request; - - try - { - if (request.IsWebSocketRequest) - { - LoggerUtils.LogRequest(_logger, request); - - ProcessWebSocketRequest(context); - return Task.FromResult(true); - } - - httpReq = GetRequest(context); - } - catch (Exception ex) - { - _logger.ErrorException("Error processing request", ex); - - httpReq = httpReq ?? GetRequest(context); - ErrorHandler(ex, httpReq, true); - return Task.FromResult(true); - } - - var uri = request.Url; - - return RequestHandler(httpReq, uri.OriginalString, uri.Host, uri.LocalPath, cancellationToken); - } - - private void ProcessWebSocketRequest(HttpListenerContext ctx) - { - try - { - var endpoint = ctx.Request.RemoteEndPoint.ToString(); - var url = ctx.Request.RawUrl; - - var connectingArgs = new WebSocketConnectingEventArgs - { - Url = url, - QueryString = ctx.Request.QueryString, - Endpoint = endpoint - }; - - if (WebSocketConnecting != null) - { - WebSocketConnecting(connectingArgs); - } - - if (connectingArgs.AllowConnection) - { - _logger.Debug("Web socket connection allowed"); - - var webSocketContext = ctx.AcceptWebSocket(null); - - if (WebSocketConnected != null) - { - WebSocketConnected(new WebSocketConnectEventArgs - { - Url = url, - QueryString = ctx.Request.QueryString, - WebSocket = new SharpWebSocket(webSocketContext.WebSocket, _logger), - Endpoint = endpoint - }); - } - } - else - { - _logger.Warn("Web socket connection not allowed"); - ctx.Response.StatusCode = 401; - ctx.Response.Close(); - } - } - catch (Exception ex) - { - _logger.ErrorException("AcceptWebSocketAsync error", ex); - ctx.Response.StatusCode = 500; - ctx.Response.Close(); - } - } - - private IHttpRequest GetRequest(HttpListenerContext httpContext) - { - var operationName = httpContext.Request.GetOperationName(); - - var req = new WebSocketSharpRequest(httpContext, operationName, _logger, _memoryStreamProvider); - - return req; - } - - public Task Stop() - { - _disposeCancellationTokenSource.Cancel(); - - if (_listener != null) - { - _listener.Close(); - } - - return Task.FromResult(true); - } - - public void Dispose() - { - Dispose(true); - } - - private bool _disposed; - private readonly object _disposeLock = new object(); - protected virtual void Dispose(bool disposing) - { - if (_disposed) return; - - lock (_disposeLock) - { - if (_disposed) return; - - if (disposing) - { - Stop(); - } - - //release unmanaged resources here... - _disposed = true; - } - } - } - -}
\ No newline at end of file diff --git a/Emby.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpRequest.cs b/Emby.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpRequest.cs deleted file mode 100644 index 522377f0c..000000000 --- a/Emby.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpRequest.cs +++ /dev/null @@ -1,611 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Text; -using Emby.Server.Implementations.HttpServer; -using Emby.Server.Implementations.HttpServer.SocketSharp; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.Logging; -using MediaBrowser.Model.Services; -using SocketHttpListener.Net; -using IHttpFile = MediaBrowser.Model.Services.IHttpFile; -using IHttpRequest = MediaBrowser.Model.Services.IHttpRequest; -using IHttpResponse = MediaBrowser.Model.Services.IHttpResponse; -using IResponse = MediaBrowser.Model.Services.IResponse; - -namespace Emby.Server.Implementations.HttpServer.SocketSharp -{ - public partial class WebSocketSharpRequest : IHttpRequest - { - private readonly HttpListenerRequest request; - private readonly IHttpResponse response; - private readonly IMemoryStreamFactory _memoryStreamProvider; - - public WebSocketSharpRequest(HttpListenerContext httpContext, string operationName, ILogger logger, IMemoryStreamFactory memoryStreamProvider) - { - this.OperationName = operationName; - _memoryStreamProvider = memoryStreamProvider; - this.request = httpContext.Request; - this.response = new WebSocketSharpResponse(logger, httpContext.Response, this); - - //HandlerFactoryPath = GetHandlerPathIfAny(UrlPrefixes[0]); - } - - private static string GetHandlerPathIfAny(string listenerUrl) - { - if (listenerUrl == null) return null; - var pos = listenerUrl.IndexOf("://", StringComparison.OrdinalIgnoreCase); - if (pos == -1) return null; - var startHostUrl = listenerUrl.Substring(pos + "://".Length); - var endPos = startHostUrl.IndexOf('/'); - if (endPos == -1) return null; - var endHostUrl = startHostUrl.Substring(endPos + 1); - return string.IsNullOrEmpty(endHostUrl) ? null : endHostUrl.TrimEnd('/'); - } - - public HttpListenerRequest HttpRequest - { - get { return request; } - } - - public object OriginalRequest - { - get { return request; } - } - - public IResponse Response - { - get { return response; } - } - - public IHttpResponse HttpResponse - { - get { return response; } - } - - public string OperationName { get; set; } - - public object Dto { get; set; } - - public string RawUrl - { - get { return request.RawUrl; } - } - - public string AbsoluteUri - { - get { return request.Url.AbsoluteUri.TrimEnd('/'); } - } - - public string UserHostAddress - { - get { return request.UserHostAddress; } - } - - public string XForwardedFor - { - get - { - return String.IsNullOrEmpty(request.Headers["X-Forwarded-For"]) ? null : request.Headers["X-Forwarded-For"]; - } - } - - public int? XForwardedPort - { - get - { - return string.IsNullOrEmpty(request.Headers["X-Forwarded-Port"]) ? (int?)null : int.Parse(request.Headers["X-Forwarded-Port"]); - } - } - - public string XForwardedProtocol - { - get - { - return string.IsNullOrEmpty(request.Headers["X-Forwarded-Proto"]) ? null : request.Headers["X-Forwarded-Proto"]; - } - } - - public string XRealIp - { - get - { - return String.IsNullOrEmpty(request.Headers["X-Real-IP"]) ? null : request.Headers["X-Real-IP"]; - } - } - - private string remoteIp; - public string RemoteIp - { - get - { - return remoteIp ?? - (remoteIp = (CheckBadChars(XForwardedFor)) ?? - (NormalizeIp(CheckBadChars(XRealIp)) ?? - (request.RemoteEndPoint != null ? NormalizeIp(request.RemoteEndPoint.Address.ToString()) : null))); - } - } - - private static readonly char[] HttpTrimCharacters = new char[] { (char)0x09, (char)0xA, (char)0xB, (char)0xC, (char)0xD, (char)0x20 }; - - // - // CheckBadChars - throws on invalid chars to be not found in header name/value - // - internal static string CheckBadChars(string name) - { - if (name == null || name.Length == 0) - { - return name; - } - - // VALUE check - //Trim spaces from both ends - name = name.Trim(HttpTrimCharacters); - - //First, check for correctly formed multi-line value - //Second, check for absenece of CTL characters - int crlf = 0; - for (int i = 0; i < name.Length; ++i) - { - char c = (char)(0x000000ff & (uint)name[i]); - switch (crlf) - { - case 0: - if (c == '\r') - { - crlf = 1; - } - else if (c == '\n') - { - // Technically this is bad HTTP. But it would be a breaking change to throw here. - // Is there an exploit? - crlf = 2; - } - else if (c == 127 || (c < ' ' && c != '\t')) - { - throw new ArgumentException("net_WebHeaderInvalidControlChars"); - } - break; - - case 1: - if (c == '\n') - { - crlf = 2; - break; - } - throw new ArgumentException("net_WebHeaderInvalidCRLFChars"); - - case 2: - if (c == ' ' || c == '\t') - { - crlf = 0; - break; - } - throw new ArgumentException("net_WebHeaderInvalidCRLFChars"); - } - } - if (crlf != 0) - { - throw new ArgumentException("net_WebHeaderInvalidCRLFChars"); - } - return name; - } - - internal static bool ContainsNonAsciiChars(string token) - { - for (int i = 0; i < token.Length; ++i) - { - if ((token[i] < 0x20) || (token[i] > 0x7e)) - { - return true; - } - } - return false; - } - - private string NormalizeIp(string ip) - { - if (!string.IsNullOrWhiteSpace(ip)) - { - // Handle ipv4 mapped to ipv6 - const string srch = "::ffff:"; - var index = ip.IndexOf(srch, StringComparison.OrdinalIgnoreCase); - if (index == 0) - { - ip = ip.Substring(srch.Length); - } - } - - return ip; - } - - public bool IsSecureConnection - { - get { return request.IsSecureConnection || XForwardedProtocol == "https"; } - } - - public string[] AcceptTypes - { - get { return request.AcceptTypes; } - } - - private Dictionary<string, object> items; - public Dictionary<string, object> Items - { - get { return items ?? (items = new Dictionary<string, object>()); } - } - - private string responseContentType; - public string ResponseContentType - { - get - { - return responseContentType - ?? (responseContentType = GetResponseContentType(this)); - } - set - { - this.responseContentType = value; - } - } - - public const string FormUrlEncoded = "application/x-www-form-urlencoded"; - public const string MultiPartFormData = "multipart/form-data"; - public static string GetResponseContentType(IRequest httpReq) - { - var specifiedContentType = GetQueryStringContentType(httpReq); - if (!string.IsNullOrEmpty(specifiedContentType)) return specifiedContentType; - - var serverDefaultContentType = "application/json"; - - var acceptContentTypes = httpReq.AcceptTypes; - var defaultContentType = httpReq.ContentType; - if (HasAnyOfContentTypes(httpReq, FormUrlEncoded, MultiPartFormData)) - { - defaultContentType = serverDefaultContentType; - } - - var preferredContentTypes = new string[] {}; - - var acceptsAnything = false; - var hasDefaultContentType = !string.IsNullOrEmpty(defaultContentType); - if (acceptContentTypes != null) - { - var hasPreferredContentTypes = new bool[preferredContentTypes.Length]; - foreach (var acceptsType in acceptContentTypes) - { - var contentType = HttpResultFactory.GetRealContentType(acceptsType); - acceptsAnything = acceptsAnything || contentType == "*/*"; - - for (var i = 0; i < preferredContentTypes.Length; i++) - { - if (hasPreferredContentTypes[i]) continue; - var preferredContentType = preferredContentTypes[i]; - hasPreferredContentTypes[i] = contentType.StartsWith(preferredContentType); - - //Prefer Request.ContentType if it is also a preferredContentType - if (hasPreferredContentTypes[i] && preferredContentType == defaultContentType) - return preferredContentType; - } - } - - for (var i = 0; i < preferredContentTypes.Length; i++) - { - if (hasPreferredContentTypes[i]) return preferredContentTypes[i]; - } - - if (acceptsAnything) - { - if (hasDefaultContentType) - return defaultContentType; - if (serverDefaultContentType != null) - return serverDefaultContentType; - } - } - - if (acceptContentTypes == null && httpReq.ContentType == Soap11) - { - return Soap11; - } - - //We could also send a '406 Not Acceptable', but this is allowed also - return serverDefaultContentType; - } - - public const string Soap11 = "text/xml; charset=utf-8"; - - public static bool HasAnyOfContentTypes(IRequest request, params string[] contentTypes) - { - if (contentTypes == null || request.ContentType == null) return false; - foreach (var contentType in contentTypes) - { - if (IsContentType(request, contentType)) return true; - } - return false; - } - - public static bool IsContentType(IRequest request, string contentType) - { - return request.ContentType.StartsWith(contentType, StringComparison.OrdinalIgnoreCase); - } - - public const string Xml = "application/xml"; - private static string GetQueryStringContentType(IRequest httpReq) - { - var format = httpReq.QueryString["format"]; - if (format == null) - { - const int formatMaxLength = 4; - var pi = httpReq.PathInfo; - if (pi == null || pi.Length <= formatMaxLength) return null; - if (pi[0] == '/') pi = pi.Substring(1); - format = LeftPart(pi, '/'); - if (format.Length > formatMaxLength) return null; - } - - format = LeftPart(format, '.').ToLower(); - if (format.Contains("json")) return "application/json"; - if (format.Contains("xml")) return Xml; - - return null; - } - - public static string LeftPart(string strVal, char needle) - { - if (strVal == null) return null; - var pos = strVal.IndexOf(needle); - return pos == -1 - ? strVal - : strVal.Substring(0, pos); - } - - public static string HandlerFactoryPath; - - private string pathInfo; - public string PathInfo - { - get - { - if (this.pathInfo == null) - { - var mode = HandlerFactoryPath; - - var pos = request.RawUrl.IndexOf("?"); - if (pos != -1) - { - var path = request.RawUrl.Substring(0, pos); - this.pathInfo = GetPathInfo( - path, - mode, - mode ?? ""); - } - else - { - this.pathInfo = request.RawUrl; - } - - this.pathInfo = System.Net.WebUtility.UrlDecode(pathInfo); - this.pathInfo = NormalizePathInfo(pathInfo, mode); - } - return this.pathInfo; - } - } - - private static string GetPathInfo(string fullPath, string mode, string appPath) - { - var pathInfo = ResolvePathInfoFromMappedPath(fullPath, mode); - if (!string.IsNullOrEmpty(pathInfo)) return pathInfo; - - //Wildcard mode relies on this to work out the handlerPath - pathInfo = ResolvePathInfoFromMappedPath(fullPath, appPath); - if (!string.IsNullOrEmpty(pathInfo)) return pathInfo; - - return fullPath; - } - - - - private static string ResolvePathInfoFromMappedPath(string fullPath, string mappedPathRoot) - { - if (mappedPathRoot == null) return null; - - var sbPathInfo = new StringBuilder(); - var fullPathParts = fullPath.Split('/'); - var mappedPathRootParts = mappedPathRoot.Split('/'); - var fullPathIndexOffset = mappedPathRootParts.Length - 1; - var pathRootFound = false; - - for (var fullPathIndex = 0; fullPathIndex < fullPathParts.Length; fullPathIndex++) - { - if (pathRootFound) - { - sbPathInfo.Append("/" + fullPathParts[fullPathIndex]); - } - else if (fullPathIndex - fullPathIndexOffset >= 0) - { - pathRootFound = true; - for (var mappedPathRootIndex = 0; mappedPathRootIndex < mappedPathRootParts.Length; mappedPathRootIndex++) - { - if (!string.Equals(fullPathParts[fullPathIndex - fullPathIndexOffset + mappedPathRootIndex], mappedPathRootParts[mappedPathRootIndex], StringComparison.OrdinalIgnoreCase)) - { - pathRootFound = false; - break; - } - } - } - } - if (!pathRootFound) return null; - - var path = sbPathInfo.ToString(); - return path.Length > 1 ? path.TrimEnd('/') : "/"; - } - - private Dictionary<string, System.Net.Cookie> cookies; - public IDictionary<string, System.Net.Cookie> Cookies - { - get - { - if (cookies == null) - { - cookies = new Dictionary<string, System.Net.Cookie>(); - foreach (var cookie in this.request.Cookies) - { - var httpCookie = (System.Net.Cookie) cookie; - cookies[httpCookie.Name] = new System.Net.Cookie(httpCookie.Name, httpCookie.Value, httpCookie.Path, httpCookie.Domain); - } - } - - return cookies; - } - } - - public string UserAgent - { - get { return request.UserAgent; } - } - - public QueryParamCollection Headers - { - get { return request.Headers; } - } - - private QueryParamCollection queryString; - public QueryParamCollection QueryString - { - get { return queryString ?? (queryString = MyHttpUtility.ParseQueryString(request.Url.Query)); } - } - - private QueryParamCollection formData; - public QueryParamCollection FormData - { - get { return formData ?? (formData = this.Form); } - } - - public bool IsLocal - { - get { return request.IsLocal; } - } - - private string httpMethod; - public string HttpMethod - { - get - { - return httpMethod - ?? (httpMethod = request.HttpMethod); - } - } - - public string Verb - { - get { return HttpMethod; } - } - - public string ContentType - { - get { return request.ContentType; } - } - - public Encoding contentEncoding; - public Encoding ContentEncoding - { - get { return contentEncoding ?? request.ContentEncoding; } - set { contentEncoding = value; } - } - - public Uri UrlReferrer - { - get { return request.UrlReferrer; } - } - - public static Encoding GetEncoding(string contentTypeHeader) - { - var param = GetParameter(contentTypeHeader, "charset="); - if (param == null) return null; - try - { - return Encoding.GetEncoding(param); - } - catch (ArgumentException) - { - return null; - } - } - - public Stream InputStream - { - get { return request.InputStream; } - } - - public long ContentLength - { - get { return request.ContentLength64; } - } - - private IHttpFile[] httpFiles; - public IHttpFile[] Files - { - get - { - if (httpFiles == null) - { - if (files == null) - return httpFiles = new IHttpFile[0]; - - httpFiles = new IHttpFile[files.Count]; - var i = 0; - foreach (var pair in files) - { - var reqFile = pair.Value; - httpFiles[i] = new HttpFile - { - ContentType = reqFile.ContentType, - ContentLength = reqFile.ContentLength, - FileName = reqFile.FileName, - InputStream = reqFile.InputStream, - }; - i++; - } - } - return httpFiles; - } - } - - static Stream GetSubStream(Stream stream, IMemoryStreamFactory streamProvider) - { - if (stream is MemoryStream) - { - var other = (MemoryStream)stream; - - byte[] buffer; - if (streamProvider.TryGetBuffer(other, out buffer)) - { - return streamProvider.CreateNew(buffer); - } - return streamProvider.CreateNew(other.ToArray()); - } - - return stream; - } - - public static string NormalizePathInfo(string pathInfo, string handlerPath) - { - if (handlerPath != null && pathInfo.TrimStart('/').StartsWith( - handlerPath, StringComparison.OrdinalIgnoreCase)) - { - return pathInfo.TrimStart('/').Substring(handlerPath.Length); - } - - return pathInfo; - } - } - - public class HttpFile : IHttpFile - { - public string Name { get; set; } - public string FileName { get; set; } - public long ContentLength { get; set; } - public string ContentType { get; set; } - public Stream InputStream { get; set; } - } -} diff --git a/Emby.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpResponse.cs b/Emby.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpResponse.cs deleted file mode 100644 index 5b51c0cf1..000000000 --- a/Emby.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpResponse.cs +++ /dev/null @@ -1,203 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Net; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.Logging; -using MediaBrowser.Model.Services; -using SocketHttpListener.Net; -using HttpListenerResponse = SocketHttpListener.Net.HttpListenerResponse; -using IHttpResponse = MediaBrowser.Model.Services.IHttpResponse; -using IRequest = MediaBrowser.Model.Services.IRequest; - -namespace Emby.Server.Implementations.HttpServer.SocketSharp -{ - public class WebSocketSharpResponse : IHttpResponse - { - private readonly ILogger _logger; - private readonly HttpListenerResponse _response; - - public WebSocketSharpResponse(ILogger logger, HttpListenerResponse response, IRequest request) - { - _logger = logger; - this._response = response; - Items = new Dictionary<string, object>(); - Request = request; - } - - public IRequest Request { get; private set; } - public Dictionary<string, object> Items { get; private set; } - public object OriginalResponse - { - get { return _response; } - } - - public int StatusCode - { - get { return this._response.StatusCode; } - set { this._response.StatusCode = value; } - } - - public string StatusDescription - { - get { return this._response.StatusDescription; } - set { this._response.StatusDescription = value; } - } - - public string ContentType - { - get { return _response.ContentType; } - set { _response.ContentType = value; } - } - - //public ICookies Cookies { get; set; } - - public void AddHeader(string name, string value) - { - if (string.Equals(name, "Content-Type", StringComparison.OrdinalIgnoreCase)) - { - ContentType = value; - return; - } - - _response.AddHeader(name, value); - } - - public QueryParamCollection Headers - { - get - { - return _response.Headers; - } - } - - public string GetHeader(string name) - { - return _response.Headers[name]; - } - - public void Redirect(string url) - { - _response.Redirect(url); - } - - public Stream OutputStream - { - get { return _response.OutputStream; } - } - - public void Close() - { - if (!this.IsClosed) - { - this.IsClosed = true; - - try - { - CloseOutputStream(this._response); - } - catch (Exception ex) - { - _logger.ErrorException("Error closing HttpListener output stream", ex); - } - } - } - - public void CloseOutputStream(HttpListenerResponse response) - { - try - { - var outputStream = response.OutputStream; - - // This is needed with compression - outputStream.Flush(); - outputStream.Dispose(); - - response.Close(); - } - catch (Exception ex) - { - _logger.ErrorException("Error in HttpListenerResponseWrapper: " + ex.Message, ex); - } - } - - public bool IsClosed - { - get; - private set; - } - - public void SetContentLength(long contentLength) - { - //you can happily set the Content-Length header in Asp.Net - //but HttpListener will complain if you do - you have to set ContentLength64 on the response. - //workaround: HttpListener throws "The parameter is incorrect" exceptions when we try to set the Content-Length header - _response.ContentLength64 = contentLength; - } - - public void SetCookie(Cookie cookie) - { - var cookieStr = AsHeaderValue(cookie); - _response.Headers.Add("Set-Cookie", cookieStr); - } - - public static string AsHeaderValue(Cookie cookie) - { - var defaultExpires = DateTime.MinValue; - - var path = cookie.Expires == defaultExpires - ? "/" - : cookie.Path ?? "/"; - - var sb = new StringBuilder(); - - sb.Append($"{cookie.Name}={cookie.Value};path={path}"); - - if (cookie.Expires != defaultExpires) - { - sb.Append($";expires={cookie.Expires:R}"); - } - - if (!string.IsNullOrEmpty(cookie.Domain)) - { - sb.Append($";domain={cookie.Domain}"); - } - //else if (restrictAllCookiesToDomain != null) - //{ - // sb.Append($";domain={restrictAllCookiesToDomain}"); - //} - - if (cookie.Secure) - { - sb.Append(";Secure"); - } - if (cookie.HttpOnly) - { - sb.Append(";HttpOnly"); - } - - return sb.ToString(); - } - - - public bool SendChunked - { - get { return _response.SendChunked; } - set { _response.SendChunked = value; } - } - - public bool KeepAlive { get; set; } - - public void ClearCookies() - { - } - - public Task TransmitFile(string path, long offset, long count, FileShareMode fileShareMode, CancellationToken cancellationToken) - { - return _response.TransmitFile(path, offset, count, fileShareMode, cancellationToken); - } - } -} diff --git a/Emby.Server.Implementations/HttpServer/StreamWriter.cs b/Emby.Server.Implementations/HttpServer/StreamWriter.cs index 5d42f42fa..3f394d3ac 100644 --- a/Emby.Server.Implementations/HttpServer/StreamWriter.cs +++ b/Emby.Server.Implementations/HttpServer/StreamWriter.cs @@ -73,7 +73,7 @@ namespace Emby.Server.Implementations.HttpServer /// <param name="source">The source.</param> /// <param name="contentType">Type of the content.</param> /// <param name="logger">The logger.</param> - public StreamWriter(byte[] source, string contentType, ILogger logger) + public StreamWriter(byte[] source, string contentType, int contentLength, ILogger logger) { if (string.IsNullOrEmpty(contentType)) { @@ -85,7 +85,7 @@ namespace Emby.Server.Implementations.HttpServer Headers["Content-Type"] = contentType; - Headers["Content-Length"] = source.Length.ToString(UsCulture); + Headers["Content-Length"] = contentLength.ToString(UsCulture); } public async Task WriteToAsync(Stream responseStream, CancellationToken cancellationToken) @@ -106,10 +106,8 @@ namespace Emby.Server.Implementations.HttpServer } } } - catch (Exception ex) + catch { - Logger.ErrorException("Error streaming data", ex); - if (OnError != null) { OnError(); diff --git a/Emby.Server.Implementations/ServerManager/WebSocketConnection.cs b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs index 076f50d93..d449e4424 100644 --- a/Emby.Server.Implementations/ServerManager/WebSocketConnection.cs +++ b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs @@ -5,15 +5,14 @@ using MediaBrowser.Model.Logging; using MediaBrowser.Model.Net; using MediaBrowser.Model.Serialization; using System; -using System.Collections.Specialized; -using System.IO; using System.Threading; using System.Threading.Tasks; -using MediaBrowser.Model.IO; using MediaBrowser.Model.Services; using MediaBrowser.Model.Text; +using System.Net.WebSockets; +using Emby.Server.Implementations.Net; -namespace Emby.Server.Implementations.ServerManager +namespace Emby.Server.Implementations.HttpServer { /// <summary> /// Class WebSocketConnection @@ -33,11 +32,6 @@ namespace Emby.Server.Implementations.ServerManager public string RemoteEndPoint { get; private set; } /// <summary> - /// The _cancellation token source - /// </summary> - private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); - - /// <summary> /// The logger /// </summary> private readonly ILogger _logger; @@ -51,7 +45,7 @@ namespace Emby.Server.Implementations.ServerManager /// Gets or sets the receive action. /// </summary> /// <value>The receive action.</value> - public Action<WebSocketMessageInfo> OnReceive { get; set; } + public Func<WebSocketMessageInfo, Task> OnReceive { get; set; } /// <summary> /// Gets the last activity date. @@ -75,7 +69,6 @@ namespace Emby.Server.Implementations.ServerManager /// </summary> /// <value>The query string.</value> public QueryParamCollection QueryString { get; set; } - private readonly IMemoryStreamFactory _memoryStreamProvider; private readonly ITextEncoding _textEncoding; /// <summary> @@ -86,7 +79,7 @@ namespace Emby.Server.Implementations.ServerManager /// <param name="jsonSerializer">The json serializer.</param> /// <param name="logger">The logger.</param> /// <exception cref="System.ArgumentNullException">socket</exception> - public WebSocketConnection(IWebSocket socket, string remoteEndPoint, IJsonSerializer jsonSerializer, ILogger logger, IMemoryStreamFactory memoryStreamProvider, ITextEncoding textEncoding) + public WebSocketConnection(IWebSocket socket, string remoteEndPoint, IJsonSerializer jsonSerializer, ILogger logger, ITextEncoding textEncoding) { if (socket == null) { @@ -109,10 +102,15 @@ namespace Emby.Server.Implementations.ServerManager _jsonSerializer = jsonSerializer; _socket = socket; _socket.OnReceiveBytes = OnReceiveInternal; - _socket.OnReceive = OnReceiveInternal; + + var memorySocket = socket as IMemoryWebSocket; + if (memorySocket != null) + { + memorySocket.OnReceiveMemoryBytes = OnReceiveInternal; + } + RemoteEndPoint = remoteEndPoint; _logger = logger; - _memoryStreamProvider = memoryStreamProvider; _textEncoding = textEncoding; socket.Closed += socket_Closed; @@ -148,6 +146,33 @@ namespace Emby.Server.Implementations.ServerManager } } + /// <summary> + /// Called when [receive]. + /// </summary> + /// <param name="bytes">The bytes.</param> + private void OnReceiveInternal(Memory<byte> memory, int length) + { + LastActivityDate = DateTime.UtcNow; + + if (OnReceive == null) + { + return; + } + + var bytes = memory.Slice(0, length).ToArray(); + + var charset = _textEncoding.GetDetectedEncodingName(bytes, bytes.Length, null, false); + + if (string.Equals(charset, "utf-8", StringComparison.OrdinalIgnoreCase)) + { + OnReceiveInternal(Encoding.UTF8.GetString(bytes, 0, bytes.Length)); + } + else + { + OnReceiveInternal(_textEncoding.GetASCIIEncoding().GetString(bytes, 0, bytes.Length)); + } + } + private void OnReceiveInternal(string message) { LastActivityDate = DateTime.UtcNow; @@ -223,7 +248,7 @@ namespace Emby.Server.Implementations.ServerManager public Task SendAsync(string text, CancellationToken cancellationToken) { - if (string.IsNullOrWhiteSpace(text)) + if (string.IsNullOrEmpty(text)) { throw new ArgumentNullException("text"); } @@ -248,7 +273,6 @@ namespace Emby.Server.Implementations.ServerManager public void Dispose() { Dispose(true); - GC.SuppressFinalize(this); } /// <summary> @@ -259,7 +283,6 @@ namespace Emby.Server.Implementations.ServerManager { if (dispose) { - _cancellationTokenSource.Dispose(); _socket.Dispose(); } } diff --git a/Emby.Server.Implementations/HttpServerFactory.cs b/Emby.Server.Implementations/HttpServerFactory.cs deleted file mode 100644 index 717c50e7b..000000000 --- a/Emby.Server.Implementations/HttpServerFactory.cs +++ /dev/null @@ -1,76 +0,0 @@ -using System; -using System.IO; -using System.Net.Security; -using System.Security.Cryptography.X509Certificates; -using System.Threading.Tasks; -using Emby.Server.Implementations.HttpServer; -using Emby.Server.Implementations.Net; -using MediaBrowser.Common.Net; -using MediaBrowser.Controller; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Net; -using MediaBrowser.Model.Cryptography; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.Logging; -using MediaBrowser.Model.Net; -using MediaBrowser.Model.Serialization; -using MediaBrowser.Model.System; -using MediaBrowser.Model.Text; -using ServiceStack.Text.Jsv; -using SocketHttpListener.Primitives; - -namespace Emby.Server.Implementations -{ - /// <summary> - /// Class ServerFactory - /// </summary> - public static class HttpServerFactory - { - /// <summary> - /// Creates the server. - /// </summary> - /// <returns>IHttpServer.</returns> - public static IHttpServer CreateServer(IServerApplicationHost applicationHost, - ILogManager logManager, - IServerConfigurationManager config, - INetworkManager networkmanager, - IMemoryStreamFactory streamProvider, - string serverName, - string defaultRedirectpath, - ITextEncoding textEncoding, - ISocketFactory socketFactory, - ICryptoProvider cryptoProvider, - IJsonSerializer json, - IXmlSerializer xml, - IEnvironmentInfo environment, - X509Certificate certificate, - IFileSystem fileSystem, - bool enableDualModeSockets) - { - var logger = logManager.GetLogger("HttpServer"); - - return new HttpListenerHost(applicationHost, - logger, - config, - serverName, - defaultRedirectpath, - networkmanager, - streamProvider, - textEncoding, - socketFactory, - cryptoProvider, - json, - xml, - environment, - certificate, - GetParseFn, - enableDualModeSockets, - fileSystem); - } - - private static Func<string, object> GetParseFn(Type propertyType) - { - return s => JsvReader.GetParseFn(propertyType)(s); - } - } -} diff --git a/Emby.Server.Implementations/IO/ExtendedFileSystemInfo.cs b/Emby.Server.Implementations/IO/ExtendedFileSystemInfo.cs new file mode 100644 index 000000000..6b08c26c9 --- /dev/null +++ b/Emby.Server.Implementations/IO/ExtendedFileSystemInfo.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Emby.Server.Implementations.IO +{ + public class ExtendedFileSystemInfo + { + public bool IsHidden { get; set; } + public bool IsReadOnly { get; set; } + public bool Exists { get; set; } + } +} diff --git a/Emby.Server.Implementations/IO/FileRefresher.cs b/Emby.Server.Implementations/IO/FileRefresher.cs index 85b8bddd2..4be30f8b7 100644 --- a/Emby.Server.Implementations/IO/FileRefresher.cs +++ b/Emby.Server.Implementations/IO/FileRefresher.cs @@ -54,7 +54,7 @@ namespace Emby.Server.Implementations.IO private void AddAffectedPath(string path) { - if (string.IsNullOrWhiteSpace(path)) + if (string.IsNullOrEmpty(path)) { throw new ArgumentNullException("path"); } @@ -67,7 +67,7 @@ namespace Emby.Server.Implementations.IO public void AddPath(string path) { - if (string.IsNullOrWhiteSpace(path)) + if (string.IsNullOrEmpty(path)) { throw new ArgumentNullException("path"); } @@ -113,7 +113,7 @@ namespace Emby.Server.Implementations.IO Path = path; AddAffectedPath(path); - if (!string.IsNullOrWhiteSpace(affectedFile)) + if (!string.IsNullOrEmpty(affectedFile)) { AddAffectedPath(affectedFile); } @@ -202,7 +202,7 @@ namespace Emby.Server.Implementations.IO // If the item has been deleted find the first valid parent that still exists while (!_fileSystem.DirectoryExists(item.Path) && !_fileSystem.FileExists(item.Path)) { - item = item.IsOwnedItem ? item.GetOwner() : item.GetParent(); + item = item.GetOwner() ?? item.GetParent(); if (item == null) { @@ -231,7 +231,6 @@ namespace Emby.Server.Implementations.IO { _disposed = true; DisposeTimer(); - GC.SuppressFinalize(this); } } } diff --git a/Emby.Server.Implementations/IO/IsoManager.cs b/Emby.Server.Implementations/IO/IsoManager.cs index dc0b9e122..903d5f301 100644 --- a/Emby.Server.Implementations/IO/IsoManager.cs +++ b/Emby.Server.Implementations/IO/IsoManager.cs @@ -70,7 +70,6 @@ namespace Emby.Server.Implementations.IO { mounter.Dispose(); } - GC.SuppressFinalize(this); } } } diff --git a/Emby.Server.Implementations/IO/LibraryMonitor.cs b/Emby.Server.Implementations/IO/LibraryMonitor.cs index a2abb2a5c..00fe447f0 100644 --- a/Emby.Server.Implementations/IO/LibraryMonitor.cs +++ b/Emby.Server.Implementations/IO/LibraryMonitor.cs @@ -266,7 +266,7 @@ namespace Emby.Server.Implementations.IO /// <exception cref="System.ArgumentNullException">path</exception> private static bool ContainsParentFolder(IEnumerable<string> lst, string path) { - if (string.IsNullOrWhiteSpace(path)) + if (string.IsNullOrEmpty(path)) { throw new ArgumentNullException("path"); } @@ -304,6 +304,12 @@ namespace Emby.Server.Implementations.IO } } + if (_environmentInfo.OperatingSystem == MediaBrowser.Model.System.OperatingSystem.Android) + { + // causing crashing + return; + } + // Already being watched if (_fileSystemWatchers.ContainsKey(path)) { @@ -320,11 +326,7 @@ namespace Emby.Server.Implementations.IO IncludeSubdirectories = true }; - if (_environmentInfo.OperatingSystem == MediaBrowser.Model.System.OperatingSystem.Windows || - _environmentInfo.OperatingSystem == MediaBrowser.Model.System.OperatingSystem.OSX) - { - newWatcher.InternalBufferSize = 32767; - } + newWatcher.InternalBufferSize = 65536; newWatcher.NotifyFilter = NotifyFilters.CreationTime | NotifyFilters.DirectoryName | @@ -337,7 +339,6 @@ namespace Emby.Server.Implementations.IO newWatcher.Deleted += watcher_Changed; newWatcher.Renamed += watcher_Changed; newWatcher.Changed += watcher_Changed; - newWatcher.Error += watcher_Error; if (_fileSystemWatchers.TryAdd(path, newWatcher)) @@ -347,7 +348,7 @@ namespace Emby.Server.Implementations.IO } else { - newWatcher.Dispose(); + DisposeWatcher(newWatcher, false); } } @@ -368,15 +369,14 @@ namespace Emby.Server.Implementations.IO if (_fileSystemWatchers.TryGetValue(path, out watcher)) { - DisposeWatcher(watcher); + DisposeWatcher(watcher, true); } } /// <summary> /// Disposes the watcher. /// </summary> - /// <param name="watcher">The watcher.</param> - private void DisposeWatcher(FileSystemWatcher watcher) + private void DisposeWatcher(FileSystemWatcher watcher, bool removeFromList) { try { @@ -384,16 +384,37 @@ namespace Emby.Server.Implementations.IO { Logger.Info("Stopping directory watching for path {0}", watcher.Path); - watcher.EnableRaisingEvents = false; + watcher.Created -= watcher_Changed; + watcher.Deleted -= watcher_Changed; + watcher.Renamed -= watcher_Changed; + watcher.Changed -= watcher_Changed; + watcher.Error -= watcher_Error; + + try + { + watcher.EnableRaisingEvents = false; + } + catch (InvalidOperationException) + { + // Seeing this under mono on linux sometimes + // Collection was modified; enumeration operation may not execute. + } } } + catch (NotImplementedException) + { + // the dispose method on FileSystemWatcher is sometimes throwing NotImplementedException on Xamarin Android + } catch { } finally { - RemoveWatcherFromList(watcher); + if (removeFromList) + { + RemoveWatcherFromList(watcher); + } } } @@ -420,7 +441,7 @@ namespace Emby.Server.Implementations.IO Logger.ErrorException("Error in Directory watcher for: " + dw.Path, ex); - DisposeWatcher(dw); + DisposeWatcher(dw, true); } /// <summary> @@ -452,7 +473,7 @@ namespace Emby.Server.Implementations.IO } var filename = Path.GetFileName(path); - + var monitorPath = !string.IsNullOrEmpty(filename) && !_alwaysIgnoreFiles.Contains(filename, StringComparer.OrdinalIgnoreCase) && !_alwaysIgnoreExtensions.Contains(Path.GetExtension(path) ?? string.Empty, StringComparer.OrdinalIgnoreCase) && @@ -466,13 +487,13 @@ namespace Emby.Server.Implementations.IO { if (_fileSystem.AreEqual(i, path)) { - Logger.Debug("Ignoring change to {0}", path); + //Logger.Debug("Ignoring change to {0}", path); return true; } if (_fileSystem.ContainsSubPath(i, path)) { - Logger.Debug("Ignoring change to {0}", path); + //Logger.Debug("Ignoring change to {0}", path); return true; } @@ -482,7 +503,7 @@ namespace Emby.Server.Implementations.IO { if (_fileSystem.AreEqual(parent, path)) { - Logger.Debug("Ignoring change to {0}", path); + //Logger.Debug("Ignoring change to {0}", path); return true; } } @@ -561,22 +582,7 @@ namespace Emby.Server.Implementations.IO foreach (var watcher in _fileSystemWatchers.Values.ToList()) { - watcher.Created -= watcher_Changed; - watcher.Deleted -= watcher_Changed; - watcher.Renamed -= watcher_Changed; - watcher.Changed -= watcher_Changed; - - try - { - watcher.EnableRaisingEvents = false; - } - catch (InvalidOperationException) - { - // Seeing this under mono on linux sometimes - // Collection was modified; enumeration operation may not execute. - } - - watcher.Dispose(); + DisposeWatcher(watcher, false); } _fileSystemWatchers.Clear(); @@ -612,7 +618,6 @@ namespace Emby.Server.Implementations.IO { _disposed = true; Dispose(true); - GC.SuppressFinalize(this); } /// <summary> @@ -644,7 +649,6 @@ namespace Emby.Server.Implementations.IO public void Dispose() { - GC.SuppressFinalize(this); } } } diff --git a/Emby.Server.Implementations/IO/ManagedFileSystem.cs b/Emby.Server.Implementations/IO/ManagedFileSystem.cs index c8e4031a9..66d7802c6 100644 --- a/Emby.Server.Implementations/IO/ManagedFileSystem.cs +++ b/Emby.Server.Implementations/IO/ManagedFileSystem.cs @@ -20,27 +20,57 @@ namespace Emby.Server.Implementations.IO private readonly bool _supportsAsyncFileStreams; private char[] _invalidFileNameChars; private readonly List<IShortcutHandler> _shortcutHandlers = new List<IShortcutHandler>(); - private bool EnableFileSystemRequestConcat; + private bool EnableSeparateFileAndDirectoryQueries; private string _tempPath; private SharpCifsFileSystem _sharpCifsFileSystem; private IEnvironmentInfo _environmentInfo; + private bool _isEnvironmentCaseInsensitive; - public ManagedFileSystem(ILogger logger, IEnvironmentInfo environmentInfo, string tempPath) + private string _defaultDirectory; + + public ManagedFileSystem(ILogger logger, IEnvironmentInfo environmentInfo, string defaultDirectory, string tempPath, bool enableSeparateFileAndDirectoryQueries) { Logger = logger; _supportsAsyncFileStreams = true; _tempPath = tempPath; _environmentInfo = environmentInfo; + _defaultDirectory = defaultDirectory; - // On Linux, this needs to be true or symbolic links are ignored - EnableFileSystemRequestConcat = environmentInfo.OperatingSystem != MediaBrowser.Model.System.OperatingSystem.Windows && - environmentInfo.OperatingSystem != MediaBrowser.Model.System.OperatingSystem.OSX; + // On Linux with mono, this needs to be true or symbolic links are ignored + EnableSeparateFileAndDirectoryQueries = enableSeparateFileAndDirectoryQueries; SetInvalidFileNameChars(environmentInfo.OperatingSystem == MediaBrowser.Model.System.OperatingSystem.Windows); _sharpCifsFileSystem = new SharpCifsFileSystem(environmentInfo.OperatingSystem); + + _isEnvironmentCaseInsensitive = environmentInfo.OperatingSystem == MediaBrowser.Model.System.OperatingSystem.Windows; + } + + public string DefaultDirectory + { + get + { + var value = _defaultDirectory; + + if (!string.IsNullOrEmpty(value)) + { + try + { + if (DirectoryExists(value)) + { + return value; + } + } + catch + { + + } + } + + return null; + } } public void AddShortcutHandler(IShortcutHandler handler) @@ -56,11 +86,9 @@ namespace Emby.Server.Implementations.IO } else { - // GetInvalidFileNameChars is less restrictive in Linux/Mac than Windows, this mimic Windows behavior for mono under Linux/Mac. - _invalidFileNameChars = new char[41] { '\x00', '\x01', '\x02', '\x03', '\x04', '\x05', '\x06', '\x07', - '\x08', '\x09', '\x0A', '\x0B', '\x0C', '\x0D', '\x0E', '\x0F', '\x10', '\x11', '\x12', - '\x13', '\x14', '\x15', '\x16', '\x17', '\x18', '\x19', '\x1A', '\x1B', '\x1C', '\x1D', - '\x1E', '\x1F', '\x22', '\x3C', '\x3E', '\x7C', ':', '*', '?', '\\', '/' }; + // Be consistent across platforms because the windows server will fail to query network shares that don't follow windows conventions + // https://referencesource.microsoft.com/#mscorlib/system/io/path.cs + _invalidFileNameChars = new char[] { '\"', '<', '>', '|', '\0', (Char)1, (Char)2, (Char)3, (Char)4, (Char)5, (Char)6, (Char)7, (Char)8, (Char)9, (Char)10, (Char)11, (Char)12, (Char)13, (Char)14, (Char)15, (Char)16, (Char)17, (Char)18, (Char)19, (Char)20, (Char)21, (Char)22, (Char)23, (Char)24, (Char)25, (Char)26, (Char)27, (Char)28, (Char)29, (Char)30, (Char)31, ':', '*', '?', '\\', '/' }; } } @@ -118,6 +146,49 @@ namespace Emby.Server.Implementations.IO return null; } + public string MakeAbsolutePath(string folderPath, string filePath) + { + if (String.IsNullOrWhiteSpace(filePath)) return filePath; + + if (filePath.Contains(@"://")) return filePath; //stream + if (filePath.Length > 3 && filePath[1] == ':' && filePath[2] == '/') return filePath; //absolute local path + + // unc path + if (filePath.StartsWith("\\\\")) + { + return filePath; + } + + var firstChar = filePath[0]; + if (firstChar == '/') + { + // For this we don't really know. + return filePath; + } + if (firstChar == '\\') //relative path + { + filePath = filePath.Substring(1); + } + try + { + string path = System.IO.Path.Combine(folderPath, filePath); + path = System.IO.Path.GetFullPath(path); + return path; + } + catch (ArgumentException ex) + { + return filePath; + } + catch (PathTooLongException) + { + return filePath; + } + catch (NotSupportedException) + { + return filePath; + } + } + /// <summary> /// Creates the shortcut. /// </summary> @@ -162,11 +233,6 @@ namespace Emby.Server.Implementations.IO /// <see cref="FileSystemMetadata.IsDirectory"/> property will be set to true and all other properties will reflect the properties of the directory.</remarks> public FileSystemMetadata GetFileSystemInfo(string path) { - if (string.IsNullOrEmpty(path)) - { - throw new ArgumentNullException("path"); - } - if (_sharpCifsFileSystem.IsEnabledForPath(path)) { return _sharpCifsFileSystem.GetFileSystemInfo(path); @@ -207,11 +273,6 @@ namespace Emby.Server.Implementations.IO /// <para>For automatic handling of files <b>and</b> directories, use <see cref="GetFileSystemInfo"/>.</para></remarks> public FileSystemMetadata GetFileInfo(string path) { - if (string.IsNullOrEmpty(path)) - { - throw new ArgumentNullException("path"); - } - if (_sharpCifsFileSystem.IsEnabledForPath(path)) { return _sharpCifsFileSystem.GetFileInfo(path); @@ -232,11 +293,6 @@ namespace Emby.Server.Implementations.IO /// <para>For automatic handling of files <b>and</b> directories, use <see cref="GetFileSystemInfo"/>.</para></remarks> public FileSystemMetadata GetDirectoryInfo(string path) { - if (string.IsNullOrEmpty(path)) - { - throw new ArgumentNullException("path"); - } - if (_sharpCifsFileSystem.IsEnabledForPath(path)) { return _sharpCifsFileSystem.GetDirectoryInfo(path); @@ -258,10 +314,12 @@ namespace Emby.Server.Implementations.IO if (result.Exists) { - var attributes = info.Attributes; - result.IsDirectory = info is DirectoryInfo || (attributes & FileAttributes.Directory) == FileAttributes.Directory; - result.IsHidden = (attributes & FileAttributes.Hidden) == FileAttributes.Hidden; - result.IsReadOnly = (attributes & FileAttributes.ReadOnly) == FileAttributes.ReadOnly; + result.IsDirectory = info is DirectoryInfo || (info.Attributes & FileAttributes.Directory) == FileAttributes.Directory; + + //if (!result.IsDirectory) + //{ + // result.IsHidden = (info.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden; + //} var fileInfo = info as FileInfo; if (fileInfo != null) @@ -281,6 +339,25 @@ namespace Emby.Server.Implementations.IO return result; } + private ExtendedFileSystemInfo GetExtendedFileSystemInfo(string path) + { + var result = new ExtendedFileSystemInfo(); + + var info = new FileInfo(path); + + if (info.Exists) + { + result.Exists = true; + + var attributes = info.Attributes; + + result.IsHidden = (attributes & FileAttributes.Hidden) == FileAttributes.Hidden; + result.IsReadOnly = (attributes & FileAttributes.ReadOnly) == FileAttributes.ReadOnly; + } + + return result; + } + /// <summary> /// The space char /// </summary> @@ -294,11 +371,6 @@ namespace Emby.Server.Implementations.IO /// <exception cref="System.ArgumentNullException">filename</exception> public string GetValidFilename(string filename) { - if (string.IsNullOrEmpty(filename)) - { - throw new ArgumentNullException("filename"); - } - var builder = new StringBuilder(filename); foreach (var c in _invalidFileNameChars) @@ -484,7 +556,7 @@ namespace Emby.Server.Implementations.IO return; } - var info = GetFileInfo(path); + var info = GetExtendedFileSystemInfo(path); if (info.Exists && info.IsHidden != isHidden) { @@ -514,7 +586,7 @@ namespace Emby.Server.Implementations.IO return; } - var info = GetFileInfo(path); + var info = GetExtendedFileSystemInfo(path); if (info.Exists && info.IsReadOnly != isReadOnly) { @@ -544,7 +616,7 @@ namespace Emby.Server.Implementations.IO return; } - var info = GetFileInfo(path); + var info = GetExtendedFileSystemInfo(path); if (!info.Exists) { @@ -720,11 +792,6 @@ namespace Emby.Server.Implementations.IO public bool IsPathFile(string path) { - if (string.IsNullOrWhiteSpace(path)) - { - throw new ArgumentNullException("path"); - } - // Cannot use Path.IsPathRooted because it returns false under mono when using windows-based paths, e.g. C:\\ if (_sharpCifsFileSystem.IsEnabledForPath(path)) @@ -822,7 +889,7 @@ namespace Emby.Server.Implementations.IO // On linux and osx the search pattern is case sensitive // If we're OK with case-sensitivity, and we're only filtering for one extension, then use the native method - if (enableCaseSensitiveExtensions && extensions != null && extensions.Length == 1) + if ((enableCaseSensitiveExtensions || _isEnvironmentCaseInsensitive) && extensions != null && extensions.Length == 1) { return ToMetadata(new DirectoryInfo(path).EnumerateFiles("*" + extensions[0], searchOption)); } @@ -855,7 +922,7 @@ namespace Emby.Server.Implementations.IO var directoryInfo = new DirectoryInfo(path); var searchOption = recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly; - if (EnableFileSystemRequestConcat) + if (EnableSeparateFileAndDirectoryQueries) { return ToMetadata(directoryInfo.EnumerateDirectories("*", searchOption)) .Concat(ToMetadata(directoryInfo.EnumerateFiles("*", searchOption))); @@ -897,9 +964,28 @@ namespace Emby.Server.Implementations.IO return File.OpenRead(path); } + private void CopyFileUsingStreams(string source, string target, bool overwrite) + { + using (var sourceStream = OpenRead(source)) + { + using (var targetStream = GetFileStream(target, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read)) + { + sourceStream.CopyTo(targetStream); + } + } + } + public void CopyFile(string source, string target, bool overwrite) { - if (_sharpCifsFileSystem.IsEnabledForPath(source)) + var enableSharpCifsForSource = _sharpCifsFileSystem.IsEnabledForPath(source); + + if (enableSharpCifsForSource != _sharpCifsFileSystem.IsEnabledForPath(target)) + { + CopyFileUsingStreams(source, target, overwrite); + return; + } + + if (enableSharpCifsForSource) { _sharpCifsFileSystem.CopyFile(source, target, overwrite); return; @@ -1033,7 +1119,7 @@ namespace Emby.Server.Implementations.IO // On linux and osx the search pattern is case sensitive // If we're OK with case-sensitivity, and we're only filtering for one extension, then use the native method - if (enableCaseSensitiveExtensions && extensions != null && extensions.Length == 1) + if ((enableCaseSensitiveExtensions || _isEnvironmentCaseInsensitive) && extensions != null && extensions.Length == 1) { return Directory.EnumerateFiles(path, "*" + extensions[0], searchOption); } diff --git a/Emby.Server.Implementations/IO/MemoryStreamProvider.cs b/Emby.Server.Implementations/IO/MemoryStreamProvider.cs deleted file mode 100644 index e9ecb7e44..000000000 --- a/Emby.Server.Implementations/IO/MemoryStreamProvider.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System.IO; -using MediaBrowser.Model.IO; - -namespace Emby.Server.Implementations.IO -{ - public class MemoryStreamProvider : IMemoryStreamFactory - { - public MemoryStream CreateNew() - { - return new MemoryStream(); - } - - public MemoryStream CreateNew(int capacity) - { - return new MemoryStream(capacity); - } - - public MemoryStream CreateNew(byte[] buffer) - { - return new MemoryStream(buffer); - } - - public bool TryGetBuffer(MemoryStream stream, out byte[] buffer) - { - buffer = stream.GetBuffer(); - return true; - } - } -} diff --git a/Emby.Server.Implementations/IO/SharpCifsFileSystem.cs b/Emby.Server.Implementations/IO/SharpCifsFileSystem.cs index 0e1f6bb00..a48543bc7 100644 --- a/Emby.Server.Implementations/IO/SharpCifsFileSystem.cs +++ b/Emby.Server.Implementations/IO/SharpCifsFileSystem.cs @@ -136,9 +136,6 @@ namespace Emby.Server.Implementations.IO if (result.Exists) { result.IsDirectory = info.IsDirectory(); - result.IsHidden = info.IsHidden(); - - result.IsReadOnly = !info.CanWrite(); if (info.IsFile()) { diff --git a/Emby.Server.Implementations/IO/StreamHelper.cs b/Emby.Server.Implementations/IO/StreamHelper.cs new file mode 100644 index 000000000..48a5063e8 --- /dev/null +++ b/Emby.Server.Implementations/IO/StreamHelper.cs @@ -0,0 +1,190 @@ +using System.IO; +using System.Threading; +using System; +using System.Threading.Tasks; +using MediaBrowser.Model.IO; + +namespace Emby.Server.Implementations.IO +{ + public class StreamHelper : IStreamHelper + { + public async Task CopyToAsync(Stream source, Stream destination, int bufferSize, Action onStarted, CancellationToken cancellationToken) + { + byte[] buffer = new byte[bufferSize]; + int read; + while ((read = source.Read(buffer, 0, buffer.Length)) != 0) + { + cancellationToken.ThrowIfCancellationRequested(); + + await destination.WriteAsync(buffer, 0, read).ConfigureAwait(false); + + if (onStarted != null) + { + onStarted(); + onStarted = null; + } + } + } + + public async Task CopyToAsync(Stream source, Stream destination, int bufferSize, int emptyReadLimit, CancellationToken cancellationToken) + { + byte[] buffer = new byte[bufferSize]; + + if (emptyReadLimit <= 0) + { + int read; + while ((read = source.Read(buffer, 0, buffer.Length)) != 0) + { + cancellationToken.ThrowIfCancellationRequested(); + + await destination.WriteAsync(buffer, 0, read).ConfigureAwait(false); + } + + return; + } + + var eofCount = 0; + + while (eofCount < emptyReadLimit) + { + cancellationToken.ThrowIfCancellationRequested(); + + var bytesRead = source.Read(buffer, 0, buffer.Length); + + if (bytesRead == 0) + { + eofCount++; + await Task.Delay(50, cancellationToken).ConfigureAwait(false); + } + else + { + eofCount = 0; + + await destination.WriteAsync(buffer, 0, bytesRead).ConfigureAwait(false); + } + } + } + + const int StreamCopyToBufferSize = 81920; + public async Task<int> CopyToAsync(Stream source, Stream destination, CancellationToken cancellationToken) + { + var array = new byte[StreamCopyToBufferSize]; + int bytesRead; + int totalBytesRead = 0; + + while ((bytesRead = await source.ReadAsync(array, 0, array.Length, cancellationToken).ConfigureAwait(false)) != 0) + { + var bytesToWrite = bytesRead; + + if (bytesToWrite > 0) + { + await destination.WriteAsync(array, 0, Convert.ToInt32(bytesToWrite), cancellationToken).ConfigureAwait(false); + + totalBytesRead += bytesRead; + } + } + + return totalBytesRead; + } + + public async Task<int> CopyToAsyncWithSyncRead(Stream source, Stream destination, CancellationToken cancellationToken) + { + var array = new byte[StreamCopyToBufferSize]; + int bytesRead; + int totalBytesRead = 0; + + while ((bytesRead = source.Read(array, 0, array.Length)) != 0) + { + var bytesToWrite = bytesRead; + + if (bytesToWrite > 0) + { + await destination.WriteAsync(array, 0, Convert.ToInt32(bytesToWrite), cancellationToken).ConfigureAwait(false); + + totalBytesRead += bytesRead; + } + } + + return totalBytesRead; + } + + public async Task CopyToAsyncWithSyncRead(Stream source, Stream destination, long copyLength, CancellationToken cancellationToken) + { + var array = new byte[StreamCopyToBufferSize]; + int bytesRead; + + while ((bytesRead = source.Read(array, 0, array.Length)) != 0) + { + var bytesToWrite = Math.Min(bytesRead, copyLength); + + if (bytesToWrite > 0) + { + await destination.WriteAsync(array, 0, Convert.ToInt32(bytesToWrite), cancellationToken).ConfigureAwait(false); + } + + copyLength -= bytesToWrite; + + if (copyLength <= 0) + { + break; + } + } + } + + public async Task CopyToAsync(Stream source, Stream destination, long copyLength, CancellationToken cancellationToken) + { + var array = new byte[StreamCopyToBufferSize]; + int bytesRead; + + while ((bytesRead = await source.ReadAsync(array, 0, array.Length, cancellationToken).ConfigureAwait(false)) != 0) + { + var bytesToWrite = Math.Min(bytesRead, copyLength); + + if (bytesToWrite > 0) + { + await destination.WriteAsync(array, 0, Convert.ToInt32(bytesToWrite), cancellationToken).ConfigureAwait(false); + } + + copyLength -= bytesToWrite; + + if (copyLength <= 0) + { + break; + } + } + } + + public async Task CopyUntilCancelled(Stream source, Stream target, int bufferSize, CancellationToken cancellationToken) + { + byte[] buffer = new byte[bufferSize]; + + while (!cancellationToken.IsCancellationRequested) + { + var bytesRead = await CopyToAsyncInternal(source, target, buffer, cancellationToken).ConfigureAwait(false); + + //var position = fs.Position; + //_logger.Debug("Streamed {0} bytes to position {1} from file {2}", bytesRead, position, path); + + if (bytesRead == 0) + { + await Task.Delay(100).ConfigureAwait(false); + } + } + } + + private static async Task<int> CopyToAsyncInternal(Stream source, Stream destination, byte[] buffer, CancellationToken cancellationToken) + { + int bytesRead; + int totalBytesRead = 0; + + while ((bytesRead = await source.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false)) != 0) + { + await destination.WriteAsync(buffer, 0, bytesRead).ConfigureAwait(false); + + totalBytesRead += bytesRead; + } + + return totalBytesRead; + } + } +} diff --git a/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs b/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs index 5cd7e4262..be17893d8 100644 --- a/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs +++ b/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs @@ -22,7 +22,7 @@ using MediaBrowser.Model.Net; namespace Emby.Server.Implementations.Images { public abstract class BaseDynamicImageProvider<T> : IHasItemChangeMonitor, IForcedProvider, ICustomMetadataProvider<T>, IHasOrder - where T : IHasMetadata + where T : BaseItem { protected IFileSystem FileSystem { get; private set; } protected IProviderManager ProviderManager { get; private set; } @@ -37,12 +37,12 @@ namespace Emby.Server.Implementations.Images ImageProcessor = imageProcessor; } - protected virtual bool Supports(IHasMetadata item) + protected virtual bool Supports(BaseItem item) { return true; } - public virtual ImageType[] GetSupportedImages(IHasMetadata item) + public virtual ImageType[] GetSupportedImages(BaseItem item) { return new ImageType[] { @@ -75,7 +75,7 @@ namespace Emby.Server.Implementations.Images return updateType; } - protected async Task<ItemUpdateType> FetchAsync(IHasMetadata item, ImageType imageType, MetadataRefreshOptions options, CancellationToken cancellationToken) + protected Task<ItemUpdateType> FetchAsync(BaseItem item, ImageType imageType, MetadataRefreshOptions options, CancellationToken cancellationToken) { var image = item.GetImageInfo(imageType, 0); @@ -83,21 +83,21 @@ namespace Emby.Server.Implementations.Images { if (!image.IsLocalFile) { - return ItemUpdateType.None; + return Task.FromResult(ItemUpdateType.None); } if (!FileSystem.ContainsSubPath(item.GetInternalMetadataPath(), image.Path)) { - return ItemUpdateType.None; + return Task.FromResult(ItemUpdateType.None); } } var items = GetItemsWithImages(item); - return await FetchToFileInternal(item, items, imageType, cancellationToken).ConfigureAwait(false); + return FetchToFileInternal(item, items, imageType, cancellationToken); } - protected async Task<ItemUpdateType> FetchToFileInternal(IHasMetadata item, + protected async Task<ItemUpdateType> FetchToFileInternal(BaseItem item, List<BaseItem> itemsWithImages, ImageType imageType, CancellationToken cancellationToken) @@ -106,7 +106,7 @@ namespace Emby.Server.Implementations.Images FileSystem.CreateDirectory(FileSystem.GetDirectoryName(outputPathWithoutExtension)); string outputPath = CreateImage(item, itemsWithImages, outputPathWithoutExtension, imageType, 0); - if (string.IsNullOrWhiteSpace(outputPath)) + if (string.IsNullOrEmpty(outputPath)) { return ItemUpdateType.None; } @@ -123,14 +123,14 @@ namespace Emby.Server.Implementations.Images return ItemUpdateType.ImageUpdate; } - protected abstract List<BaseItem> GetItemsWithImages(IHasMetadata item); + protected abstract List<BaseItem> GetItemsWithImages(BaseItem item); - protected string CreateThumbCollage(IHasMetadata primaryItem, List<BaseItem> items, string outputPath) + protected string CreateThumbCollage(BaseItem primaryItem, List<BaseItem> items, string outputPath) { return CreateCollage(primaryItem, items, outputPath, 640, 360); } - protected virtual IEnumerable<string> GetStripCollageImagePaths(IHasMetadata primaryItem, IEnumerable<BaseItem> items) + protected virtual IEnumerable<string> GetStripCollageImagePaths(BaseItem primaryItem, IEnumerable<BaseItem> items) { return items .Select(i => @@ -149,25 +149,25 @@ namespace Emby.Server.Implementations.Images } return null; }) - .Where(i => !string.IsNullOrWhiteSpace(i)); + .Where(i => !string.IsNullOrEmpty(i)); } - protected string CreatePosterCollage(IHasMetadata primaryItem, List<BaseItem> items, string outputPath) + protected string CreatePosterCollage(BaseItem primaryItem, List<BaseItem> items, string outputPath) { return CreateCollage(primaryItem, items, outputPath, 400, 600); } - protected string CreateSquareCollage(IHasMetadata primaryItem, List<BaseItem> items, string outputPath) + protected string CreateSquareCollage(BaseItem primaryItem, List<BaseItem> items, string outputPath) { return CreateCollage(primaryItem, items, outputPath, 600, 600); } - protected string CreateThumbCollage(IHasMetadata primaryItem, List<BaseItem> items, string outputPath, int width, int height) + protected string CreateThumbCollage(BaseItem primaryItem, List<BaseItem> items, string outputPath, int width, int height) { return CreateCollage(primaryItem, items, outputPath, width, height); } - private string CreateCollage(IHasMetadata primaryItem, List<BaseItem> items, string outputPath, int width, int height) + private string CreateCollage(BaseItem primaryItem, List<BaseItem> items, string outputPath, int width, int height) { FileSystem.CreateDirectory(FileSystem.GetDirectoryName(outputPath)); @@ -198,7 +198,7 @@ namespace Emby.Server.Implementations.Images get { return "Dynamic Image Provider"; } } - protected virtual string CreateImage(IHasMetadata item, + protected virtual string CreateImage(BaseItem item, List<BaseItem> itemsWithImages, string outputPathWithoutExtension, ImageType imageType, @@ -237,7 +237,7 @@ namespace Emby.Server.Implementations.Images get { return 7; } } - public bool HasChanged(IHasMetadata item, IDirectoryService directoryServicee) + public bool HasChanged(BaseItem item, IDirectoryService directoryServicee) { if (!Supports(item)) { @@ -258,7 +258,7 @@ namespace Emby.Server.Implementations.Images return false; } - protected bool HasChanged(IHasMetadata item, ImageType type) + protected bool HasChanged(BaseItem item, ImageType type) { var image = item.GetImageInfo(type, 0); @@ -283,7 +283,7 @@ namespace Emby.Server.Implementations.Images return true; } - protected virtual bool HasChangedByDate(IHasMetadata item, ItemImageInfo image) + protected virtual bool HasChangedByDate(BaseItem item, ItemImageInfo image) { var age = DateTime.UtcNow - image.DateModified; if (age.TotalDays <= MaxImageAgeDays) @@ -293,19 +293,6 @@ namespace Emby.Server.Implementations.Images return true; } - protected List<BaseItem> GetFinalItems(IEnumerable<BaseItem> items) - { - return GetFinalItems(items, 4); - } - - protected virtual List<BaseItem> GetFinalItems(IEnumerable<BaseItem> items, int limit) - { - return items - .OrderBy(i => Guid.NewGuid()) - .Take(limit) - .ToList(); - } - public int Order { get @@ -322,7 +309,7 @@ namespace Emby.Server.Implementations.Images .Select(i => i.GetImagePath(imageType)) .FirstOrDefault(); - if (string.IsNullOrWhiteSpace(image)) + if (string.IsNullOrEmpty(image)) { return null; } diff --git a/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs b/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs index 5c3e1dab1..59f0a9fc9 100644 --- a/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs +++ b/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs @@ -22,10 +22,12 @@ namespace Emby.Server.Implementations.Library private readonly ILibraryManager _libraryManager; private readonly ILogger _logger; + private bool _ignoreDotPrefix; + /// <summary> /// Any folder named in this list will be ignored - can be added to at runtime for extensibility /// </summary> - public static readonly List<string> IgnoreFolders = new List<string> + public static readonly Dictionary<string, string> IgnoreFolders = new List<string> { "metadata", "ps3_update", @@ -41,15 +43,24 @@ namespace Emby.Server.Implementations.Library "#recycle", // Qnap - "@Recycle" + "@Recycle", + ".@__thumb", + "$RECYCLE.BIN", + "System Volume Information", + ".grab", + + // macos + ".AppleDouble" + + }.ToDictionary(i => i, StringComparer.OrdinalIgnoreCase); - }; - public CoreResolutionIgnoreRule(IFileSystem fileSystem, ILibraryManager libraryManager, ILogger logger) { _fileSystem = fileSystem; _libraryManager = libraryManager; _logger = logger; + + _ignoreDotPrefix = Environment.OSVersion.Platform != PlatformID.Win32NT; } /// <summary> @@ -67,46 +78,48 @@ namespace Emby.Server.Implementations.Library } var filename = fileInfo.Name; - var isHidden = fileInfo.IsHidden; var path = fileInfo.FullName; // Handle mac .DS_Store // https://github.com/MediaBrowser/MediaBrowser/issues/427 - if (filename.IndexOf("._", StringComparison.OrdinalIgnoreCase) == 0) + if (_ignoreDotPrefix) { - return true; - } - - // Ignore hidden files and folders - if (isHidden) - { - if (parent == null) + if (filename.IndexOf('.') == 0) { - var parentFolderName = Path.GetFileName(_fileSystem.GetDirectoryName(path)); - - if (string.Equals(parentFolderName, BaseItem.ThemeSongsFolderName, StringComparison.OrdinalIgnoreCase)) - { - return false; - } - if (string.Equals(parentFolderName, BaseItem.ThemeVideosFolderName, StringComparison.OrdinalIgnoreCase)) - { - return false; - } - } - - // Sometimes these are marked hidden - if (_fileSystem.IsRootPath(path)) - { - return false; + return true; } - - return true; } + // Ignore hidden files and folders + //if (fileInfo.IsHidden) + //{ + // if (parent == null) + // { + // var parentFolderName = Path.GetFileName(_fileSystem.GetDirectoryName(path)); + + // if (string.Equals(parentFolderName, BaseItem.ThemeSongsFolderName, StringComparison.OrdinalIgnoreCase)) + // { + // return false; + // } + // if (string.Equals(parentFolderName, BaseItem.ThemeVideosFolderName, StringComparison.OrdinalIgnoreCase)) + // { + // return false; + // } + // } + + // // Sometimes these are marked hidden + // if (_fileSystem.IsRootPath(path)) + // { + // return false; + // } + + // return true; + //} + if (fileInfo.IsDirectory) { // Ignore any folders in our list - if (IgnoreFolders.Contains(filename, StringComparer.OrdinalIgnoreCase)) + if (IgnoreFolders.ContainsKey(filename)) { return true; } @@ -141,6 +154,17 @@ namespace Emby.Server.Implementations.Library return true; } } + + // Ignore samples + var sampleFilename = " " + filename.Replace(".", " ", StringComparison.OrdinalIgnoreCase) + .Replace("-", " ", StringComparison.OrdinalIgnoreCase) + .Replace("_", " ", StringComparison.OrdinalIgnoreCase) + .Replace("!", " ", StringComparison.OrdinalIgnoreCase); + + if (sampleFilename.IndexOf(" sample ", StringComparison.OrdinalIgnoreCase) != -1) + { + return true; + } } return false; diff --git a/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs b/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs new file mode 100644 index 000000000..7c79a7c69 --- /dev/null +++ b/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs @@ -0,0 +1,105 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using MediaBrowser.Controller.Authentication; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Model.Cryptography; + +namespace Emby.Server.Implementations.Library +{ + public class DefaultAuthenticationProvider : IAuthenticationProvider, IRequiresResolvedUser + { + private readonly ICryptoProvider _cryptographyProvider; + public DefaultAuthenticationProvider(ICryptoProvider crypto) + { + _cryptographyProvider = crypto; + } + + public string Name => "Default"; + + public bool IsEnabled => true; + + public Task<ProviderAuthenticationResult> Authenticate(string username, string password) + { + throw new NotImplementedException(); + } + + public Task<ProviderAuthenticationResult> Authenticate(string username, string password, User resolvedUser) + { + if (resolvedUser == null) + { + throw new Exception("Invalid username or password"); + } + + var success = string.Equals(GetPasswordHash(resolvedUser), GetHashedString(resolvedUser, password), StringComparison.OrdinalIgnoreCase); + + if (!success) + { + throw new Exception("Invalid username or password"); + } + + return Task.FromResult(new ProviderAuthenticationResult + { + Username = username + }); + } + + public Task<bool> HasPassword(User user) + { + var hasConfiguredPassword = !IsPasswordEmpty(user, GetPasswordHash(user)); + return Task.FromResult(hasConfiguredPassword); + } + + private bool IsPasswordEmpty(User user, string passwordHash) + { + return string.Equals(passwordHash, GetEmptyHashedString(user), StringComparison.OrdinalIgnoreCase); + } + + public Task ChangePassword(User user, string newPassword) + { + string newPasswordHash = null; + + if (newPassword != null) + { + newPasswordHash = GetHashedString(user, newPassword); + } + + if (string.IsNullOrWhiteSpace(newPasswordHash)) + { + throw new ArgumentNullException("newPasswordHash"); + } + + user.Password = newPasswordHash; + + return Task.CompletedTask; + } + + public string GetPasswordHash(User user) + { + return string.IsNullOrEmpty(user.Password) + ? GetEmptyHashedString(user) + : user.Password; + } + + public string GetEmptyHashedString(User user) + { + return GetHashedString(user, string.Empty); + } + + /// <summary> + /// Gets the hashed string. + /// </summary> + public string GetHashedString(User user, string str) + { + var salt = user.Salt; + if (salt != null) + { + // return BCrypt.HashPassword(str, salt); + } + + // legacy + return BitConverter.ToString(_cryptographyProvider.ComputeSHA1(Encoding.UTF8.GetBytes(str))).Replace("-", string.Empty); + } + } +} diff --git a/Emby.Server.Implementations/Library/ExclusiveLiveStream.cs b/Emby.Server.Implementations/Library/ExclusiveLiveStream.cs new file mode 100644 index 000000000..186ec63da --- /dev/null +++ b/Emby.Server.Implementations/Library/ExclusiveLiveStream.cs @@ -0,0 +1,42 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Model.Dto; +using MediaBrowser.Controller.Library; + +namespace Emby.Server.Implementations.Library +{ + public class ExclusiveLiveStream : ILiveStream + { + public int ConsumerCount { get; set; } + public string OriginalStreamId { get; set; } + + public string TunerHostId => null; + + public bool EnableStreamSharing { get; set; } + public MediaSourceInfo MediaSource { get; set; } + + public string UniqueId { get; private set; } + + private Func<Task> _closeFn; + + public ExclusiveLiveStream(MediaSourceInfo mediaSource, Func<Task> closeFn) + { + MediaSource = mediaSource; + EnableStreamSharing = false; + _closeFn = closeFn; + ConsumerCount = 1; + UniqueId = Guid.NewGuid().ToString("N"); + } + + public Task Close() + { + return _closeFn(); + } + + public Task Open(CancellationToken openCancellationToken) + { + return Task.CompletedTask; + } + } +} diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index 2934a5147..31af9370c 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -44,6 +44,9 @@ using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.LiveTv; using MediaBrowser.Model.Tasks; +using Emby.Server.Implementations.Playlists; +using MediaBrowser.Providers.MediaInfo; +using MediaBrowser.Controller; namespace Emby.Server.Implementations.Library { @@ -71,12 +74,6 @@ namespace Emby.Server.Implementations.Library private IResolverIgnoreRule[] EntityResolutionIgnoreRules { get; set; } /// <summary> - /// Gets the list of BasePluginFolders added by plugins - /// </summary> - /// <value>The plugin folders.</value> - private IVirtualFolderCreator[] PluginFolderCreators { get; set; } - - /// <summary> /// Gets the list of currently registered entity resolvers /// </summary> /// <value>The entity resolvers enumerable.</value> @@ -140,6 +137,7 @@ namespace Emby.Server.Implementations.Library private readonly Func<IProviderManager> _providerManagerFactory; private readonly Func<IUserViewManager> _userviewManager; public bool IsScanRunning { get; private set; } + private IServerApplicationHost _appHost; /// <summary> /// The _library items cache @@ -167,7 +165,7 @@ namespace Emby.Server.Implementations.Library /// <param name="userManager">The user manager.</param> /// <param name="configurationManager">The configuration manager.</param> /// <param name="userDataRepository">The user data repository.</param> - public LibraryManager(ILogger logger, ITaskManager taskManager, IUserManager userManager, IServerConfigurationManager configurationManager, IUserDataManager userDataRepository, Func<ILibraryMonitor> libraryMonitorFactory, IFileSystem fileSystem, Func<IProviderManager> providerManagerFactory, Func<IUserViewManager> userviewManager) + public LibraryManager(IServerApplicationHost appHost, ILogger logger, ITaskManager taskManager, IUserManager userManager, IServerConfigurationManager configurationManager, IUserDataManager userDataRepository, Func<ILibraryMonitor> libraryMonitorFactory, IFileSystem fileSystem, Func<IProviderManager> providerManagerFactory, Func<IUserViewManager> userviewManager) { _logger = logger; _taskManager = taskManager; @@ -178,6 +176,7 @@ namespace Emby.Server.Implementations.Library _fileSystem = fileSystem; _providerManagerFactory = providerManagerFactory; _userviewManager = userviewManager; + _appHost = appHost; _libraryItemsCache = new ConcurrentDictionary<Guid, BaseItem>(); ConfigurationManager.ConfigurationUpdated += ConfigurationUpdated; @@ -195,14 +194,12 @@ namespace Emby.Server.Implementations.Library /// <param name="itemComparers">The item comparers.</param> /// <param name="postscanTasks">The postscan tasks.</param> public void AddParts(IEnumerable<IResolverIgnoreRule> rules, - IEnumerable<IVirtualFolderCreator> pluginFolders, IEnumerable<IItemResolver> resolvers, IEnumerable<IIntroProvider> introProviders, IEnumerable<IBaseItemComparer> itemComparers, IEnumerable<ILibraryPostScanTask> postscanTasks) { EntityResolutionIgnoreRules = rules.ToArray(); - PluginFolderCreators = pluginFolders.ToArray(); EntityResolvers = resolvers.OrderBy(i => i.Priority).ToArray(); MultiItemResolvers = EntityResolvers.OfType<IMultiItemResolver>().ToArray(); IntroProviders = introProviders.ToArray(); @@ -302,7 +299,7 @@ namespace Emby.Server.Implementations.Library } else { - if (!(item is Video)) + if (!(item is Video) && !(item is LiveTvChannel)) { return; } @@ -311,13 +308,47 @@ namespace Emby.Server.Implementations.Library LibraryItemsCache.AddOrUpdate(item.Id, item, delegate { return item; }); } - public async Task DeleteItem(BaseItem item, DeleteOptions options) + public void DeleteItem(BaseItem item, DeleteOptions options) + { + DeleteItem(item, options, false); + } + + public void DeleteItem(BaseItem item, DeleteOptions options, bool notifyParentItem) + { + if (item == null) + { + throw new ArgumentNullException("item"); + } + + var parent = item.GetOwner() ?? item.GetParent(); + + DeleteItem(item, options, parent, notifyParentItem); + } + + public void DeleteItem(BaseItem item, DeleteOptions options, BaseItem parent, bool notifyParentItem) { if (item == null) { throw new ArgumentNullException("item"); } + if (item.SourceType == SourceType.Channel) + { + if (options.DeleteFromExternalProvider) + { + try + { + var task = BaseItem.ChannelManager.DeleteItem(item); + Task.WaitAll(task); + } + catch (ArgumentException) + { + // channel no longer installed + } + } + options.DeleteFileLocation = false; + } + if (item is LiveTvProgram) { _logger.Debug("Deleting item, Type: {0}, Name: {1}, Path: {2}, Id: {3}", @@ -335,10 +366,6 @@ namespace Emby.Server.Implementations.Library item.Id); } - var parent = item.IsOwnedItem ? item.GetOwner() : item.GetParent(); - - var locationType = item.LocationType; - var children = item.IsFolder ? ((Folder)item).GetRecursiveChildren(false).ToList() : new List<BaseItem>(); @@ -361,7 +388,7 @@ namespace Emby.Server.Implementations.Library } } - if (options.DeleteFileLocation && locationType != LocationType.Remote && locationType != LocationType.Virtual) + if (options.DeleteFileLocation && item.IsFileProtocol) { // Assume only the first is required // Add this flag to GetDeletePaths if required in the future @@ -407,33 +434,10 @@ namespace Emby.Server.Implementations.Library isRequiredForDelete = false; } - - if (parent != null) - { - var parentFolder = parent as Folder; - if (parentFolder != null) - { - await parentFolder.ValidateChildren(new SimpleProgress<double>(), CancellationToken.None, new MetadataRefreshOptions(_fileSystem), false).ConfigureAwait(false); - } - else - { - await parent.RefreshMetadata(new MetadataRefreshOptions(_fileSystem), CancellationToken.None).ConfigureAwait(false); - } - } - } - else if (parent != null) - { - var parentFolder = parent as Folder; - if (parentFolder != null) - { - parentFolder.RemoveChild(item); - } - else - { - await parent.RefreshMetadata(new MetadataRefreshOptions(_fileSystem), CancellationToken.None).ConfigureAwait(false); - } } + item.SetParent(null); + ItemRepository.DeleteItem(item.Id, CancellationToken.None); foreach (var child in children) { @@ -497,7 +501,7 @@ namespace Emby.Server.Implementations.Library private Guid GetNewItemIdInternal(string key, Type type, bool forceCaseInsensitive) { - if (string.IsNullOrWhiteSpace(key)) + if (string.IsNullOrEmpty(key)) { throw new ArgumentNullException("key"); } @@ -544,7 +548,7 @@ namespace Emby.Server.Implementations.Library var fullPath = fileInfo.FullName; - if (string.IsNullOrWhiteSpace(collectionType) && parent != null) + if (string.IsNullOrEmpty(collectionType) && parent != null) { collectionType = GetContentTypeOverride(fullPath, true); } @@ -572,7 +576,26 @@ namespace Emby.Server.Implementations.Library // When resolving the root, we need it's grandchildren (children of user views) var flattenFolderDepth = isPhysicalRoot ? 2 : 0; - var files = FileData.GetFilteredFileSystemEntries(directoryService, args.Path, _fileSystem, _logger, args, flattenFolderDepth: flattenFolderDepth, resolveShortcuts: isPhysicalRoot || args.IsVf); + FileSystemMetadata[] files; + var isVf = args.IsVf; + + try + { + files = FileData.GetFilteredFileSystemEntries(directoryService, args.Path, _fileSystem, _appHost, _logger, args, flattenFolderDepth: flattenFolderDepth, resolveShortcuts: isPhysicalRoot || isVf); + } + catch (Exception ex) + { + if (parent != null && parent.IsPhysicalRoot) + { + _logger.ErrorException("Error in GetFilteredFileSystemEntries isPhysicalRoot: {0} IsVf: {1}", ex, isPhysicalRoot, isVf); + + files = new FileSystemMetadata[] { }; + } + else + { + throw; + } + } // Need to remove subpaths that may have been resolved from shortcuts // Example: if \\server\movies exists, then strip out \\server\movies\action @@ -717,42 +740,43 @@ namespace Emby.Server.Implementations.Library } // Add in the plug-in folders - foreach (var child in PluginFolderCreators) + var path = Path.Combine(ConfigurationManager.ApplicationPaths.DataPath, "playlists"); + + _fileSystem.CreateDirectory(path); + + Folder folder = new PlaylistsFolder { - var folder = child.GetFolder(); + Path = path + }; - if (folder != null) + if (folder.Id.Equals(Guid.Empty)) + { + if (string.IsNullOrEmpty(folder.Path)) { - if (folder.Id == Guid.Empty) - { - if (string.IsNullOrWhiteSpace(folder.Path)) - { - folder.Id = GetNewItemId(folder.GetType().Name, folder.GetType()); - } - else - { - folder.Id = GetNewItemId(folder.Path, folder.GetType()); - } - } + folder.Id = GetNewItemId(folder.GetType().Name, folder.GetType()); + } + else + { + folder.Id = GetNewItemId(folder.Path, folder.GetType()); + } + } - var dbItem = GetItemById(folder.Id) as BasePluginFolder; + var dbItem = GetItemById(folder.Id) as BasePluginFolder; - if (dbItem != null && string.Equals(dbItem.Path, folder.Path, StringComparison.OrdinalIgnoreCase)) - { - folder = dbItem; - } + if (dbItem != null && string.Equals(dbItem.Path, folder.Path, StringComparison.OrdinalIgnoreCase)) + { + folder = dbItem; + } - if (folder.ParentId != rootFolder.Id) - { - folder.ParentId = rootFolder.Id; - folder.UpdateToRepository(ItemUpdateType.MetadataImport, CancellationToken.None); - } + if (folder.ParentId != rootFolder.Id) + { + folder.ParentId = rootFolder.Id; + folder.UpdateToRepository(ItemUpdateType.MetadataImport, CancellationToken.None); + } - rootFolder.AddVirtualChild(folder); + rootFolder.AddVirtualChild(folder); - RegisterItem(folder); - } - } + RegisterItem(folder); return rootFolder; } @@ -798,16 +822,18 @@ namespace Emby.Server.Implementations.Library // If this returns multiple items it could be tricky figuring out which one is correct. // In most cases, the newest one will be and the others obsolete but not yet cleaned up - if (string.IsNullOrWhiteSpace(path)) + if (string.IsNullOrEmpty(path)) { throw new ArgumentNullException("path"); } + //_logger.Info("FindByPath {0}", path); + var query = new InternalItemsQuery { Path = path, IsFolder = isFolder, - OrderBy = new[] { ItemSortBy.DateCreated }.Select(i => new Tuple<string, SortOrder>(i, SortOrder.Descending)).ToArray(), + OrderBy = new[] { ItemSortBy.DateCreated }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Descending)).ToArray(), Limit = 1, DtoOptions = new DtoOptions(true) }; @@ -957,7 +983,7 @@ namespace Emby.Server.Implementations.Library Path = path }; - CreateItem(item, CancellationToken.None); + CreateItem(item, null); } return item; @@ -997,7 +1023,7 @@ namespace Emby.Server.Implementations.Library // Just run the scheduled task so that the user can see it _taskManager.CancelIfRunningAndQueue<RefreshMediaLibraryTask>(); - return Task.FromResult(true); + return Task.CompletedTask; } /// <summary> @@ -1031,48 +1057,45 @@ namespace Emby.Server.Implementations.Library } } - private async Task PerformLibraryValidation(IProgress<double> progress, CancellationToken cancellationToken) + private async Task ValidateTopLibraryFolders(CancellationToken cancellationToken) { - _logger.Info("Validating media library"); - - // Ensure these objects are lazy loaded. - // Without this there is a deadlock that will need to be investigated var rootChildren = RootFolder.Children.ToList(); rootChildren = GetUserRootFolder().Children.ToList(); await RootFolder.RefreshMetadata(cancellationToken).ConfigureAwait(false); - progress.Report(.5); - // Start by just validating the children of the root, but go no further await RootFolder.ValidateChildren(new SimpleProgress<double>(), cancellationToken, new MetadataRefreshOptions(_fileSystem), recursive: false); - progress.Report(1); - await GetUserRootFolder().RefreshMetadata(cancellationToken).ConfigureAwait(false); await GetUserRootFolder().ValidateChildren(new SimpleProgress<double>(), cancellationToken, new MetadataRefreshOptions(_fileSystem), recursive: false).ConfigureAwait(false); - progress.Report(2); // Quickly scan CollectionFolders for changes foreach (var folder in GetUserRootFolder().Children.OfType<Folder>().ToList()) { await folder.RefreshMetadata(cancellationToken).ConfigureAwait(false); } - progress.Report(3); + } + + private async Task PerformLibraryValidation(IProgress<double> progress, CancellationToken cancellationToken) + { + _logger.Info("Validating media library"); + + await ValidateTopLibraryFolders(cancellationToken).ConfigureAwait(false); var innerProgress = new ActionableProgress<double>(); - innerProgress.RegisterAction(pct => progress.Report(3 + pct * .72)); + innerProgress.RegisterAction(pct => progress.Report(pct * .96)); // Now validate the entire media library await RootFolder.ValidateChildren(innerProgress, cancellationToken, new MetadataRefreshOptions(_fileSystem), recursive: true).ConfigureAwait(false); - progress.Report(75); + progress.Report(96); innerProgress = new ActionableProgress<double>(); - innerProgress.RegisterAction(pct => progress.Report(75 + pct * .25)); + innerProgress.RegisterAction(pct => progress.Report(96 + (pct * .04))); // Run post-scan tasks await RunPostScanTasks(innerProgress, cancellationToken).ConfigureAwait(false); @@ -1102,8 +1125,13 @@ namespace Emby.Server.Implementations.Library innerProgress.RegisterAction(pct => { - double innerPercent = currentNumComplete * 100 + pct; + double innerPercent = pct; + innerPercent /= 100; + innerPercent += currentNumComplete; + innerPercent /= numTasks; + innerPercent *= 100; + progress.Report(innerPercent); }); @@ -1163,7 +1191,19 @@ namespace Emby.Server.Implementations.Library Locations = _fileSystem.GetFilePaths(dir, false) .Where(i => string.Equals(ShortcutFileExtension, Path.GetExtension(i), StringComparison.OrdinalIgnoreCase)) - .Select(_fileSystem.ResolveShortcut) + .Select(i => + { + try + { + return _appHost.ExpandVirtualPath(_fileSystem.ResolveShortcut(i)); + } + catch (Exception ex) + { + _logger.ErrorException("Error resolving shortcut file {0}", ex, i); + return null; + } + }) + .Where(i => i != null) .OrderBy(i => i) .ToArray(), @@ -1197,7 +1237,7 @@ namespace Emby.Server.Implementations.Library { return _fileSystem.GetFilePaths(path, new[] { ".collection" }, true, false) .Select(i => _fileSystem.GetFileNameWithoutExtension(i)) - .FirstOrDefault(i => !string.IsNullOrWhiteSpace(i)); + .FirstOrDefault(i => !string.IsNullOrEmpty(i)); } /// <summary> @@ -1208,7 +1248,7 @@ namespace Emby.Server.Implementations.Library /// <exception cref="System.ArgumentNullException">id</exception> public BaseItem GetItemById(Guid id) { - if (id == Guid.Empty) + if (id.Equals(Guid.Empty)) { throw new ArgumentNullException("id"); } @@ -1234,9 +1274,9 @@ namespace Emby.Server.Implementations.Library public List<BaseItem> GetItemList(InternalItemsQuery query, bool allowExternalContent) { - if (query.Recursive && query.ParentId.HasValue) + if (query.Recursive && !query.ParentId.Equals(Guid.Empty)) { - var parent = GetItemById(query.ParentId.Value); + var parent = GetItemById(query.ParentId); if (parent != null) { SetTopParentIdsOrAncestors(query, new List<BaseItem> { parent }); @@ -1258,9 +1298,9 @@ namespace Emby.Server.Implementations.Library public int GetCount(InternalItemsQuery query) { - if (query.Recursive && query.ParentId.HasValue) + if (query.Recursive && !query.ParentId.Equals(Guid.Empty)) { - var parent = GetItemById(query.ParentId.Value); + var parent = GetItemById(query.ParentId); if (parent != null) { SetTopParentIdsOrAncestors(query, new List<BaseItem> { parent }); @@ -1391,7 +1431,7 @@ namespace Emby.Server.Implementations.Library return; } - var parents = query.AncestorIds.Select(i => GetItemById(new Guid(i))).ToList(); + var parents = query.AncestorIds.Select(i => GetItemById(i)).ToList(); if (parents.All(i => { @@ -1406,13 +1446,13 @@ namespace Emby.Server.Implementations.Library })) { // Optimize by querying against top level views - query.TopParentIds = parents.SelectMany(i => GetTopParentIdsForQuery(i, query.User)).Select(i => i.ToString("N")).ToArray(); - query.AncestorIds = new string[] { }; + query.TopParentIds = parents.SelectMany(i => GetTopParentIdsForQuery(i, query.User)).ToArray(); + query.AncestorIds = Array.Empty<Guid>(); // Prevent searching in all libraries due to empty filter if (query.TopParentIds.Length == 0) { - query.TopParentIds = new[] { Guid.NewGuid().ToString("N") }; + query.TopParentIds = new[] { Guid.NewGuid() }; } } } @@ -1430,9 +1470,9 @@ namespace Emby.Server.Implementations.Library public QueryResult<BaseItem> GetItemsResult(InternalItemsQuery query) { - if (query.Recursive && query.ParentId.HasValue) + if (query.Recursive && !query.ParentId.Equals(Guid.Empty)) { - var parent = GetItemById(query.ParentId.Value); + var parent = GetItemById(query.ParentId); if (parent != null) { SetTopParentIdsOrAncestors(query, new List<BaseItem> { parent }); @@ -1472,23 +1512,23 @@ namespace Emby.Server.Implementations.Library })) { // Optimize by querying against top level views - query.TopParentIds = parents.SelectMany(i => GetTopParentIdsForQuery(i, query.User)).Select(i => i.ToString("N")).ToArray(); + query.TopParentIds = parents.SelectMany(i => GetTopParentIdsForQuery(i, query.User)).ToArray(); // Prevent searching in all libraries due to empty filter if (query.TopParentIds.Length == 0) { - query.TopParentIds = new[] { Guid.NewGuid().ToString("N") }; + query.TopParentIds = new[] { Guid.NewGuid() }; } } else { // We need to be able to query from any arbitrary ancestor up the tree - query.AncestorIds = parents.SelectMany(i => i.GetIdsForAncestorQuery()).Select(i => i.ToString("N")).ToArray(); + query.AncestorIds = parents.SelectMany(i => i.GetIdsForAncestorQuery()).ToArray(); // Prevent searching in all libraries due to empty filter if (query.AncestorIds.Length == 0) { - query.AncestorIds = new[] { Guid.NewGuid().ToString("N") }; + query.AncestorIds = new[] { Guid.NewGuid() }; } } @@ -1498,22 +1538,21 @@ namespace Emby.Server.Implementations.Library private void AddUserToQuery(InternalItemsQuery query, User user, bool allowExternalContent = true) { if (query.AncestorIds.Length == 0 && - !query.ParentId.HasValue && + query.ParentId.Equals(Guid.Empty) && query.ChannelIds.Length == 0 && query.TopParentIds.Length == 0 && - string.IsNullOrWhiteSpace(query.AncestorWithPresentationUniqueKey) && - string.IsNullOrWhiteSpace(query.SeriesPresentationUniqueKey) && + string.IsNullOrEmpty(query.AncestorWithPresentationUniqueKey) && + string.IsNullOrEmpty(query.SeriesPresentationUniqueKey) && query.ItemIds.Length == 0) { var userViews = _userviewManager().GetUserViews(new UserViewQuery { - UserId = user.Id.ToString("N"), + UserId = user.Id, IncludeHidden = true, IncludeExternalContent = allowExternalContent + }); - }, CancellationToken.None).Result; - - query.TopParentIds = userViews.SelectMany(i => GetTopParentIdsForQuery(i, user)).Select(i => i.ToString("N")).ToArray(); + query.TopParentIds = userViews.SelectMany(i => GetTopParentIdsForQuery(i, user)).ToArray(); } } @@ -1527,48 +1566,38 @@ namespace Emby.Server.Implementations.Library { return new[] { view.Id }; } - if (string.Equals(view.ViewType, CollectionType.Channels)) - { - var channelResult = BaseItem.ChannelManager.GetChannelsInternal(new ChannelQuery - { - UserId = user.Id.ToString("N") - - }, CancellationToken.None).Result; - - return channelResult.Items.Select(i => i.Id); - } // Translate view into folders - if (view.DisplayParentId != Guid.Empty) + if (!view.DisplayParentId.Equals(Guid.Empty)) { var displayParent = GetItemById(view.DisplayParentId); if (displayParent != null) { return GetTopParentIdsForQuery(displayParent, user); } - return new Guid[] { }; + return Array.Empty<Guid>(); } - if (view.ParentId != Guid.Empty) + if (!view.ParentId.Equals(Guid.Empty)) { var displayParent = GetItemById(view.ParentId); if (displayParent != null) { return GetTopParentIdsForQuery(displayParent, user); } - return new Guid[] { }; + return Array.Empty<Guid>(); } // Handle grouping - if (user != null && !string.IsNullOrWhiteSpace(view.ViewType) && UserView.IsEligibleForGrouping(view.ViewType) && user.Configuration.GroupedFolders.Length > 0) + if (user != null && !string.IsNullOrEmpty(view.ViewType) && UserView.IsEligibleForGrouping(view.ViewType) && user.Configuration.GroupedFolders.Length > 0) { - return user.RootFolder + return GetUserRootFolder() .GetChildren(user, true) .OfType<CollectionFolder>() - .Where(i => string.IsNullOrWhiteSpace(i.CollectionType) || string.Equals(i.CollectionType, view.ViewType, StringComparison.OrdinalIgnoreCase)) + .Where(i => string.IsNullOrEmpty(i.CollectionType) || string.Equals(i.CollectionType, view.ViewType, StringComparison.OrdinalIgnoreCase)) .Where(i => user.IsFolderGrouped(i.Id)) .SelectMany(i => GetTopParentIdsForQuery(i, user)); } - return new Guid[] { }; + return Array.Empty<Guid>(); } var collectionFolder = item as CollectionFolder; @@ -1582,7 +1611,7 @@ namespace Emby.Server.Implementations.Library { return new[] { topParent.Id }; } - return new Guid[] { }; + return Array.Empty<Guid>(); } /// <summary> @@ -1737,7 +1766,7 @@ namespace Emby.Server.Implementations.Library return orderedItems ?? items; } - public IEnumerable<BaseItem> Sort(IEnumerable<BaseItem> items, User user, IEnumerable<Tuple<string, SortOrder>> orderByList) + public IEnumerable<BaseItem> Sort(IEnumerable<BaseItem> items, User user, IEnumerable<ValueTuple<string, SortOrder>> orderByList) { var isFirst = true; @@ -1802,9 +1831,9 @@ namespace Emby.Server.Implementations.Library /// <param name="item">The item.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>Task.</returns> - public void CreateItem(BaseItem item, CancellationToken cancellationToken) + public void CreateItem(BaseItem item, BaseItem parent) { - CreateItems(new[] { item }, item.GetParent(), cancellationToken); + CreateItems(new[] { item }, parent, CancellationToken.None); } /// <summary> @@ -1828,6 +1857,12 @@ namespace Emby.Server.Implementations.Library { foreach (var item in list) { + // With the live tv guide this just creates too much noise + if (item.SourceType != SourceType.Library) + { + continue; + } + try { ItemAdded(this, new ItemChangeEventArgs @@ -1854,46 +1889,65 @@ namespace Emby.Server.Implementations.Library /// <summary> /// Updates the item. /// </summary> - /// <param name="item">The item.</param> - /// <param name="updateReason">The update reason.</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task.</returns> - public void UpdateItem(BaseItem item, ItemUpdateType updateReason, CancellationToken cancellationToken) + public void UpdateItems(List<BaseItem> items, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken) { - var locationType = item.LocationType; - if (locationType != LocationType.Remote && locationType != LocationType.Virtual) + foreach (var item in items) { - _providerManagerFactory().SaveMetadata(item, updateReason); - } + if (item.IsFileProtocol) + { + _providerManagerFactory().SaveMetadata(item, updateReason); + } - item.DateLastSaved = DateTime.UtcNow; + item.DateLastSaved = DateTime.UtcNow; - var logName = item.LocationType == LocationType.Remote ? item.Name ?? item.Path : item.Path ?? item.Name; - _logger.Debug("Saving {0} to database.", logName); + RegisterItem(item); + } - ItemRepository.SaveItem(item, cancellationToken); + //var logName = item.LocationType == LocationType.Remote ? item.Name ?? item.Path : item.Path ?? item.Name; + //_logger.Debug("Saving {0} to database.", logName); - RegisterItem(item); + ItemRepository.SaveItems(items, cancellationToken); if (ItemUpdated != null) { - try + foreach (var item in items) { - ItemUpdated(this, new ItemChangeEventArgs + // With the live tv guide this just creates too much noise + if (item.SourceType != SourceType.Library) { - Item = item, - Parent = item.GetParent(), - UpdateReason = updateReason - }); - } - catch (Exception ex) - { - _logger.ErrorException("Error in ItemUpdated event handler", ex); + continue; + } + + try + { + ItemUpdated(this, new ItemChangeEventArgs + { + Item = item, + Parent = parent, + UpdateReason = updateReason + }); + } + catch (Exception ex) + { + _logger.ErrorException("Error in ItemUpdated event handler", ex); + } } } } /// <summary> + /// Updates the item. + /// </summary> + /// <param name="item">The item.</param> + /// <param name="updateReason">The update reason.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + public void UpdateItem(BaseItem item, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken) + { + UpdateItems(new List<BaseItem> { item }, parent, updateReason, cancellationToken); + } + + /// <summary> /// Reports the item removed. /// </summary> /// <param name="item">The item.</param> @@ -1995,12 +2049,12 @@ namespace Emby.Server.Implementations.Library public string GetContentType(BaseItem item) { string configuredContentType = GetConfiguredContentType(item, false); - if (!string.IsNullOrWhiteSpace(configuredContentType)) + if (!string.IsNullOrEmpty(configuredContentType)) { return configuredContentType; } configuredContentType = GetConfiguredContentType(item, true); - if (!string.IsNullOrWhiteSpace(configuredContentType)) + if (!string.IsNullOrEmpty(configuredContentType)) { return configuredContentType; } @@ -2011,14 +2065,14 @@ namespace Emby.Server.Implementations.Library { var type = GetTopFolderContentType(item); - if (!string.IsNullOrWhiteSpace(type)) + if (!string.IsNullOrEmpty(type)) { return type; } return item.GetParents() .Select(GetConfiguredContentType) - .LastOrDefault(i => !string.IsNullOrWhiteSpace(i)); + .LastOrDefault(i => !string.IsNullOrEmpty(i)); } public string GetConfiguredContentType(BaseItem item) @@ -2043,7 +2097,7 @@ namespace Emby.Server.Implementations.Library private string GetContentTypeOverride(string path, bool inherit) { - var nameValuePair = ConfigurationManager.Configuration.ContentTypes.FirstOrDefault(i => _fileSystem.AreEqual(i.Name, path) || (inherit && !string.IsNullOrWhiteSpace(i.Name) && _fileSystem.ContainsSubPath(i.Name, path))); + var nameValuePair = ConfigurationManager.Configuration.ContentTypes.FirstOrDefault(i => _fileSystem.AreEqual(i.Name, path) || (inherit && !string.IsNullOrEmpty(i.Name) && _fileSystem.ContainsSubPath(i.Name, path))); if (nameValuePair != null) { return nameValuePair.Value; @@ -2058,16 +2112,21 @@ namespace Emby.Server.Implementations.Library return null; } - while (!(item.GetParent() is AggregateFolder) && item.GetParent() != null) + while (!item.ParentId.Equals(Guid.Empty)) { - item = item.GetParent(); + var parent = item.GetParent(); + if (parent == null || parent is AggregateFolder) + { + break; + } + item = parent; } return GetUserRootFolder().Children .OfType<ICollectionFolder>() .Where(i => string.Equals(i.Path, item.Path, StringComparison.OrdinalIgnoreCase) || i.PhysicalLocations.Contains(item.Path)) .Select(i => i.CollectionType) - .FirstOrDefault(i => !string.IsNullOrWhiteSpace(i)); + .FirstOrDefault(i => !string.IsNullOrEmpty(i)); } private readonly TimeSpan _viewRefreshInterval = TimeSpan.FromHours(24); @@ -2076,18 +2135,16 @@ namespace Emby.Server.Implementations.Library public UserView GetNamedView(User user, string name, string viewType, - string sortName, - CancellationToken cancellationToken) + string sortName) { - return GetNamedView(user, name, null, viewType, sortName, cancellationToken); + return GetNamedView(user, name, Guid.Empty, viewType, sortName); } public UserView GetNamedView(string name, string viewType, - string sortName, - CancellationToken cancellationToken) + string sortName) { - var path = Path.Combine(ConfigurationManager.ApplicationPaths.ItemsByNamePath, "views"); + var path = Path.Combine(ConfigurationManager.ApplicationPaths.InternalMetadataPath, "views"); path = Path.Combine(path, _fileSystem.GetValidFilename(viewType)); @@ -2111,32 +2168,15 @@ namespace Emby.Server.Implementations.Library ForcedSortName = sortName }; - CreateItem(item, cancellationToken); + CreateItem(item, null); refresh = true; } - if (!refresh) - { - refresh = DateTime.UtcNow - item.DateLastRefreshed >= _viewRefreshInterval; - } - - if (!refresh && item.DisplayParentId != Guid.Empty) - { - var displayParent = GetItemById(item.DisplayParentId); - refresh = displayParent != null && displayParent.DateLastSaved > item.DateLastRefreshed; - } - if (refresh) { item.UpdateToRepository(ItemUpdateType.MetadataImport, CancellationToken.None); - _providerManagerFactory().QueueRefresh(item.Id, new MetadataRefreshOptions(_fileSystem) - { - // Not sure why this is necessary but need to figure it out - // View images are not getting utilized without this - ForceSave = true - - }, RefreshPriority.Normal); + _providerManagerFactory().QueueRefresh(item.Id, new MetadataRefreshOptions(_fileSystem), RefreshPriority.Normal); } return item; @@ -2144,12 +2184,12 @@ namespace Emby.Server.Implementations.Library public UserView GetNamedView(User user, string name, - string parentId, + Guid parentId, string viewType, - string sortName, - CancellationToken cancellationToken) + string sortName) { - var idValues = "38_namedview_" + name + user.Id.ToString("N") + (parentId ?? string.Empty) + (viewType ?? string.Empty); + var parentIdString = parentId.Equals(Guid.Empty) ? null : parentId.ToString("N"); + var idValues = "38_namedview_" + name + user.Id.ToString("N") + (parentIdString ?? string.Empty) + (viewType ?? string.Empty); var id = GetNewItemId(idValues, typeof(UserView)); @@ -2174,19 +2214,16 @@ namespace Emby.Server.Implementations.Library UserId = user.Id }; - if (!string.IsNullOrWhiteSpace(parentId)) - { - item.DisplayParentId = new Guid(parentId); - } + item.DisplayParentId = parentId; - CreateItem(item, cancellationToken); + CreateItem(item, null); isNew = true; } var refresh = isNew || DateTime.UtcNow - item.DateLastRefreshed >= _viewRefreshInterval; - if (!refresh && item.DisplayParentId != Guid.Empty) + if (!refresh && !item.DisplayParentId.Equals(Guid.Empty)) { var displayParent = GetItemById(item.DisplayParentId); refresh = displayParent != null && displayParent.DateLastSaved > item.DateLastRefreshed; @@ -2207,8 +2244,7 @@ namespace Emby.Server.Implementations.Library public UserView GetShadowView(BaseItem parent, string viewType, - string sortName, - CancellationToken cancellationToken) + string sortName) { if (parent == null) { @@ -2244,14 +2280,14 @@ namespace Emby.Server.Implementations.Library item.DisplayParentId = parentId; - CreateItem(item, cancellationToken); + CreateItem(item, null); isNew = true; } var refresh = isNew || DateTime.UtcNow - item.DateLastRefreshed >= _viewRefreshInterval; - if (!refresh && item.DisplayParentId != Guid.Empty) + if (!refresh && !item.DisplayParentId.Equals(Guid.Empty)) { var displayParent = GetItemById(item.DisplayParentId); refresh = displayParent != null && displayParent.DateLastSaved > item.DateLastRefreshed; @@ -2271,19 +2307,19 @@ namespace Emby.Server.Implementations.Library } public UserView GetNamedView(string name, - string parentId, + Guid parentId, string viewType, string sortName, - string uniqueId, - CancellationToken cancellationToken) + string uniqueId) { - if (string.IsNullOrWhiteSpace(name)) + if (string.IsNullOrEmpty(name)) { throw new ArgumentNullException("name"); } - var idValues = "37_namedview_" + name + (parentId ?? string.Empty) + (viewType ?? string.Empty); - if (!string.IsNullOrWhiteSpace(uniqueId)) + var parentIdString = parentId.Equals(Guid.Empty) ? null : parentId.ToString("N"); + var idValues = "37_namedview_" + name + (parentIdString ?? string.Empty) + (viewType ?? string.Empty); + if (!string.IsNullOrEmpty(uniqueId)) { idValues += uniqueId; } @@ -2310,12 +2346,9 @@ namespace Emby.Server.Implementations.Library ForcedSortName = sortName }; - if (!string.IsNullOrWhiteSpace(parentId)) - { - item.DisplayParentId = new Guid(parentId); - } + item.DisplayParentId = parentId; - CreateItem(item, cancellationToken); + CreateItem(item, null); isNew = true; } @@ -2323,12 +2356,12 @@ namespace Emby.Server.Implementations.Library if (!string.Equals(viewType, item.ViewType, StringComparison.OrdinalIgnoreCase)) { item.ViewType = viewType; - item.UpdateToRepository(ItemUpdateType.MetadataEdit, cancellationToken); + item.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None); } var refresh = isNew || DateTime.UtcNow - item.DateLastRefreshed >= _viewRefreshInterval; - if (!refresh && item.DisplayParentId != Guid.Empty) + if (!refresh && !item.DisplayParentId.Equals(Guid.Empty)) { var displayParent = GetItemById(item.DisplayParentId); refresh = displayParent != null && displayParent.DateLastSaved > item.DateLastRefreshed; @@ -2346,6 +2379,13 @@ namespace Emby.Server.Implementations.Library return item; } + public void AddExternalSubtitleStreams(List<MediaStream> streams, + string videoPath, + string[] files) + { + new SubtitleResolver(BaseItem.LocalizationManager, _fileSystem).AddExternalSubtitleStreams(streams, videoPath, streams.Count, files); + } + public bool IsVideoFile(string path, LibraryOptions libraryOptions) { var resolver = new VideoResolver(GetNamingOptions()); @@ -2370,19 +2410,25 @@ namespace Emby.Server.Implementations.Library public int? GetSeasonNumberFromPath(string path) { - return new SeasonPathParser(GetNamingOptions(), new RegexProvider()).Parse(path, true, true).SeasonNumber; + return new SeasonPathParser(GetNamingOptions()).Parse(path, true, true).SeasonNumber; } - public bool FillMissingEpisodeNumbersFromPath(Episode episode) + public bool FillMissingEpisodeNumbersFromPath(Episode episode, bool forceRefresh) { + var series = episode.Series; + bool? isAbsoluteNaming = series == null ? false : string.Equals(series.DisplayOrder, "absolute", StringComparison.OrdinalIgnoreCase); + if (!isAbsoluteNaming.Value) + { + // In other words, no filter applied + isAbsoluteNaming = null; + } + var resolver = new EpisodeResolver(GetNamingOptions()); var isFolder = episode.VideoType == VideoType.BluRay || episode.VideoType == VideoType.Dvd; - var locationType = episode.LocationType; - - var episodeInfo = locationType == LocationType.FileSystem || locationType == LocationType.Offline ? - resolver.Resolve(episode.Path, isFolder) : + var episodeInfo = episode.IsFileProtocol ? + resolver.Resolve(episode.Path, isFolder, null, null, isAbsoluteNaming) : new Emby.Naming.TV.EpisodeInfo(); if (episodeInfo == null) @@ -2428,105 +2474,67 @@ namespace Emby.Server.Implementations.Library changed = true; } } - - if (!episode.ParentIndexNumber.HasValue) - { - var season = episode.Season; - - if (season != null) - { - episode.ParentIndexNumber = season.IndexNumber; - } - - if (episode.ParentIndexNumber.HasValue) - { - changed = true; - } - } } else { - if (!episode.IndexNumber.HasValue) + if (!episode.IndexNumber.HasValue || forceRefresh) { - episode.IndexNumber = episodeInfo.EpisodeNumber; - - if (episode.IndexNumber.HasValue) + if (episode.IndexNumber != episodeInfo.EpisodeNumber) { changed = true; } + episode.IndexNumber = episodeInfo.EpisodeNumber; } - if (!episode.IndexNumberEnd.HasValue) + if (!episode.IndexNumberEnd.HasValue || forceRefresh) { - episode.IndexNumberEnd = episodeInfo.EndingEpsiodeNumber; - - if (episode.IndexNumberEnd.HasValue) + if (episode.IndexNumberEnd != episodeInfo.EndingEpsiodeNumber) { changed = true; } + episode.IndexNumberEnd = episodeInfo.EndingEpsiodeNumber; } - if (!episode.ParentIndexNumber.HasValue) + if (!episode.ParentIndexNumber.HasValue || forceRefresh) { - episode.ParentIndexNumber = episodeInfo.SeasonNumber; - - if (!episode.ParentIndexNumber.HasValue) - { - var season = episode.Season; - - if (season != null) - { - episode.ParentIndexNumber = season.IndexNumber; - } - } - - if (episode.ParentIndexNumber.HasValue) + if (episode.ParentIndexNumber != episodeInfo.SeasonNumber) { changed = true; } + episode.ParentIndexNumber = episodeInfo.SeasonNumber; } } - return changed; - } - - public NamingOptions GetNamingOptions() - { - return GetNamingOptions(true); - } - - public NamingOptions GetNamingOptions(bool allowOptimisticEpisodeDetection) - { - if (!allowOptimisticEpisodeDetection) + if (!episode.ParentIndexNumber.HasValue) { - if (_namingOptionsWithoutOptimisticEpisodeDetection == null) - { - var namingOptions = new ExtendedNamingOptions(); + var season = episode.Season; - InitNamingOptions(namingOptions); - namingOptions.EpisodeExpressions = namingOptions.EpisodeExpressions - .Where(i => i.IsNamed && !i.IsOptimistic) - .ToList(); - - _namingOptionsWithoutOptimisticEpisodeDetection = namingOptions; + if (season != null) + { + episode.ParentIndexNumber = season.IndexNumber; } - return _namingOptionsWithoutOptimisticEpisodeDetection; + if (episode.ParentIndexNumber.HasValue) + { + changed = true; + } } + return changed; + } + + public NamingOptions GetNamingOptions() + { return GetNamingOptionsInternal(); } - private NamingOptions _namingOptionsWithoutOptimisticEpisodeDetection; private NamingOptions _namingOptions; private string[] _videoFileExtensions; private NamingOptions GetNamingOptionsInternal() { if (_namingOptions == null) { - var options = new ExtendedNamingOptions(); - - InitNamingOptions(options); + var options = new NamingOptions(); _namingOptions = options; _videoFileExtensions = _namingOptions.VideoFileExtensions.ToArray(); @@ -2535,27 +2543,6 @@ namespace Emby.Server.Implementations.Library return _namingOptions; } - private void InitNamingOptions(NamingOptions options) - { - // These cause apps to have problems - options.AudioFileExtensions.Remove(".m3u"); - options.AudioFileExtensions.Remove(".wpl"); - - //if (!libraryOptions.EnableArchiveMediaFiles) - { - options.AudioFileExtensions.Remove(".rar"); - options.AudioFileExtensions.Remove(".zip"); - } - - //if (!libraryOptions.EnableArchiveMediaFiles) - { - options.VideoFileExtensions.Remove(".rar"); - options.VideoFileExtensions.Remove(".zip"); - } - - options.VideoFileExtensions.Add(".tp"); - } - public ItemLookupInfo ParseName(string name) { var resolver = new VideoResolver(GetNamingOptions()); @@ -2606,12 +2593,11 @@ namespace Emby.Server.Implementations.Library { video = dbItem; } - else - { - // item is new - video.ExtraType = ExtraType.Trailer; - } - video.TrailerTypes = new List<TrailerType> { TrailerType.LocalTrailer }; + + video.ParentId = Guid.Empty; + video.OwnerId = owner.Id; + video.ExtraType = ExtraType.Trailer; + video.TrailerTypes = new [] { TrailerType.LocalTrailer }; return video; @@ -2625,7 +2611,7 @@ namespace Emby.Server.Implementations.Library { var namingOptions = GetNamingOptions(); - var files = fileSystemChildren.Where(i => i.IsDirectory) + var files = owner.IsInMixedFolder ? new List<FileSystemMetadata>() : fileSystemChildren.Where(i => i.IsDirectory) .Where(i => ExtrasSubfolderNames.Contains(i.Name ?? string.Empty, StringComparer.OrdinalIgnoreCase)) .SelectMany(i => _fileSystem.GetFiles(i.FullName, _videoFileExtensions, false, false)) .ToList(); @@ -2653,6 +2639,9 @@ namespace Emby.Server.Implementations.Library video = dbItem; } + video.ParentId = Guid.Empty; + video.OwnerId = owner.Id; + SetExtraTypeFromFilename(video); return video; @@ -2756,7 +2745,7 @@ namespace Emby.Server.Implementations.Library private void SetExtraTypeFromFilename(Video item) { - var resolver = new ExtraResolver(GetNamingOptions(), new RegexProvider()); + var resolver = new ExtraResolver(GetNamingOptions()); var result = resolver.GetExtraInfo(item.Path); @@ -2841,7 +2830,7 @@ namespace Emby.Server.Implementations.Library ItemRepository.UpdatePeople(item.Id, people); } - public async Task<ItemImageInfo> ConvertImageToLocal(IHasMetadata item, ItemImageInfo image, int imageIndex) + public async Task<ItemImageInfo> ConvertImageToLocal(BaseItem item, ItemImageInfo image, int imageIndex) { foreach (var url in image.Path.Split('|')) { @@ -2872,7 +2861,7 @@ namespace Emby.Server.Implementations.Library throw new InvalidOperationException(); } - public void AddVirtualFolder(string name, string collectionType, LibraryOptions options, bool refreshLibrary) + public async Task AddVirtualFolder(string name, string collectionType, LibraryOptions options, bool refreshLibrary) { if (string.IsNullOrWhiteSpace(name)) { @@ -2910,7 +2899,7 @@ namespace Emby.Server.Implementations.Library { var path = Path.Combine(virtualFolderPath, collectionType + ".collection"); - _fileSystem.WriteAllBytes(path, new byte[] { }); + _fileSystem.WriteAllBytes(path, Array.Empty<byte>()); } CollectionFolder.SaveLibraryOptions(virtualFolderPath, options); @@ -2925,26 +2914,30 @@ namespace Emby.Server.Implementations.Library } finally { - Task.Run(() => + if (refreshLibrary) { - // No need to start if scanning the library because it will handle it - if (refreshLibrary) - { - ValidateMediaLibrary(new SimpleProgress<double>(), CancellationToken.None); - } - else - { - // Need to add a delay here or directory watchers may still pick up the changes - var task = Task.Delay(1000); - // Have to block here to allow exceptions to bubble - Task.WaitAll(task); + await ValidateTopLibraryFolders(CancellationToken.None).ConfigureAwait(false); - _libraryMonitorFactory().Start(); - } - }); + StartScanInBackground(); + } + else + { + // Need to add a delay here or directory watchers may still pick up the changes + await Task.Delay(1000).ConfigureAwait(false); + _libraryMonitorFactory().Start(); + } } } + private void StartScanInBackground() + { + Task.Run(() => + { + // No need to start if scanning the library because it will handle it + ValidateMediaLibrary(new SimpleProgress<double>(), CancellationToken.None); + }); + } + private bool ValidateNetworkPath(string path) { //if (Environment.OSVersion.Platform == PlatformID.Win32NT) @@ -3003,7 +2996,7 @@ namespace Emby.Server.Implementations.Library lnk = Path.Combine(virtualFolderPath, shortcutFilename + ShortcutFileExtension); } - _fileSystem.CreateShortcut(lnk, path); + _fileSystem.CreateShortcut(lnk, _appHost.ReverseVirtualPath(path)); RemoveContentTypeOverrides(path); @@ -3079,7 +3072,7 @@ namespace Emby.Server.Implementations.Library } } - public void RemoveVirtualFolder(string name, bool refreshLibrary) + public async Task RemoveVirtualFolder(string name, bool refreshLibrary) { if (string.IsNullOrWhiteSpace(name)) { @@ -3103,23 +3096,20 @@ namespace Emby.Server.Implementations.Library } finally { - Task.Run(() => + CollectionFolder.OnCollectionFolderChange(); + + if (refreshLibrary) { - // No need to start if scanning the library because it will handle it - if (refreshLibrary) - { - ValidateMediaLibrary(new SimpleProgress<double>(), CancellationToken.None); - } - else - { - // Need to add a delay here or directory watchers may still pick up the changes - var task = Task.Delay(1000); - // Have to block here to allow exceptions to bubble - Task.WaitAll(task); + await ValidateTopLibraryFolders(CancellationToken.None).ConfigureAwait(false); - _libraryMonitorFactory().Start(); - } - }); + StartScanInBackground(); + } + else + { + // Need to add a delay here or directory watchers may still pick up the changes + await Task.Delay(1000).ConfigureAwait(false); + _libraryMonitorFactory().Start(); + } } } @@ -3157,7 +3147,7 @@ namespace Emby.Server.Implementations.Library public void RemoveMediaPath(string virtualFolderName, string mediaPath) { - if (string.IsNullOrWhiteSpace(mediaPath)) + if (string.IsNullOrEmpty(mediaPath)) { throw new ArgumentNullException("mediaPath"); } @@ -3172,7 +3162,7 @@ namespace Emby.Server.Implementations.Library var shortcut = _fileSystem.GetFilePaths(virtualFolderPath, true) .Where(i => string.Equals(ShortcutFileExtension, Path.GetExtension(i), StringComparison.OrdinalIgnoreCase)) - .FirstOrDefault(f => _fileSystem.ResolveShortcut(f).Equals(mediaPath, StringComparison.OrdinalIgnoreCase)); + .FirstOrDefault(f => _appHost.ExpandVirtualPath(_fileSystem.ResolveShortcut(f)).Equals(mediaPath, StringComparison.OrdinalIgnoreCase)); if (!string.IsNullOrEmpty(shortcut)) { diff --git a/Emby.Server.Implementations/Library/LiveStreamHelper.cs b/Emby.Server.Implementations/Library/LiveStreamHelper.cs new file mode 100644 index 000000000..e027e133f --- /dev/null +++ b/Emby.Server.Implementations/Library/LiveStreamHelper.cs @@ -0,0 +1,181 @@ +using System; +using System.Globalization; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Controller.MediaEncoding; +using MediaBrowser.Model.Dlna; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.MediaInfo; +using System.Collections.Generic; +using MediaBrowser.Model.Serialization; +using MediaBrowser.Common.Configuration; +using System.IO; +using MediaBrowser.Common.Extensions; + +namespace Emby.Server.Implementations.Library +{ + public class LiveStreamHelper + { + private readonly IMediaEncoder _mediaEncoder; + private readonly ILogger _logger; + + private IJsonSerializer _json; + private IApplicationPaths _appPaths; + + public LiveStreamHelper(IMediaEncoder mediaEncoder, ILogger logger, IJsonSerializer json, IApplicationPaths appPaths) + { + _mediaEncoder = mediaEncoder; + _logger = logger; + _json = json; + _appPaths = appPaths; + } + + public async Task AddMediaInfoWithProbe(MediaSourceInfo mediaSource, bool isAudio, string cacheKey, bool addProbeDelay, CancellationToken cancellationToken) + { + var originalRuntime = mediaSource.RunTimeTicks; + + var now = DateTime.UtcNow; + + MediaInfo mediaInfo = null; + var cacheFilePath = string.IsNullOrEmpty(cacheKey) ? null : Path.Combine(_appPaths.CachePath, "mediainfo", cacheKey.GetMD5().ToString("N") + ".json"); + + if (!string.IsNullOrEmpty(cacheKey)) + { + try + { + mediaInfo = _json.DeserializeFromFile<MediaInfo>(cacheFilePath); + + //_logger.Debug("Found cached media info"); + } + catch + { + } + } + + if (mediaInfo == null) + { + if (addProbeDelay) + { + var delayMs = mediaSource.AnalyzeDurationMs ?? 0; + delayMs = Math.Max(3000, delayMs); + if (delayMs > 0) + { + _logger.Info("Waiting {0}ms before probing the live stream", delayMs); + await Task.Delay(delayMs, cancellationToken).ConfigureAwait(false); + } + } + + mediaSource.AnalyzeDurationMs = 3000; + + mediaInfo = await _mediaEncoder.GetMediaInfo(new MediaInfoRequest + { + MediaSource = mediaSource, + MediaType = isAudio ? DlnaProfileType.Audio : DlnaProfileType.Video, + ExtractChapters = false + + }, cancellationToken).ConfigureAwait(false); + + if (cacheFilePath != null) + { + Directory.CreateDirectory(Path.GetDirectoryName(cacheFilePath)); + _json.SerializeToFile(mediaInfo, cacheFilePath); + + //_logger.Debug("Saved media info to {0}", cacheFilePath); + } + } + + var mediaStreams = mediaInfo.MediaStreams; + + if (!string.IsNullOrEmpty(cacheKey)) + { + var newList = new List<MediaStream>(); + newList.AddRange(mediaStreams.Where(i => i.Type == MediaStreamType.Video).Take(1)); + newList.AddRange(mediaStreams.Where(i => i.Type == MediaStreamType.Audio).Take(1)); + + foreach (var stream in newList) + { + stream.Index = -1; + stream.Language = null; + } + + mediaStreams = newList; + } + + _logger.Info("Live tv media info probe took {0} seconds", (DateTime.UtcNow - now).TotalSeconds.ToString(CultureInfo.InvariantCulture)); + + mediaSource.Bitrate = mediaInfo.Bitrate; + mediaSource.Container = mediaInfo.Container; + mediaSource.Formats = mediaInfo.Formats; + mediaSource.MediaStreams = mediaStreams; + mediaSource.RunTimeTicks = mediaInfo.RunTimeTicks; + mediaSource.Size = mediaInfo.Size; + mediaSource.Timestamp = mediaInfo.Timestamp; + mediaSource.Video3DFormat = mediaInfo.Video3DFormat; + mediaSource.VideoType = mediaInfo.VideoType; + + mediaSource.DefaultSubtitleStreamIndex = null; + + // Null this out so that it will be treated like a live stream + if (!originalRuntime.HasValue) + { + mediaSource.RunTimeTicks = null; + } + + var audioStream = mediaStreams.FirstOrDefault(i => i.Type == MediaBrowser.Model.Entities.MediaStreamType.Audio); + + if (audioStream == null || audioStream.Index == -1) + { + mediaSource.DefaultAudioStreamIndex = null; + } + else + { + mediaSource.DefaultAudioStreamIndex = audioStream.Index; + } + + var videoStream = mediaStreams.FirstOrDefault(i => i.Type == MediaBrowser.Model.Entities.MediaStreamType.Video); + if (videoStream != null) + { + if (!videoStream.BitRate.HasValue) + { + var width = videoStream.Width ?? 1920; + + if (width >= 3000) + { + videoStream.BitRate = 30000000; + } + + else if (width >= 1900) + { + videoStream.BitRate = 20000000; + } + + else if (width >= 1200) + { + videoStream.BitRate = 8000000; + } + + else if (width >= 700) + { + videoStream.BitRate = 2000000; + } + } + + // This is coming up false and preventing stream copy + videoStream.IsAVC = null; + } + + mediaSource.AnalyzeDurationMs = 3000; + + // Try to estimate this + mediaSource.InferTotalBitrate(true); + } + + public Task AddMediaInfoWithProbe(MediaSourceInfo mediaSource, bool isAudio, bool addProbeDelay, CancellationToken cancellationToken) + { + return AddMediaInfoWithProbe(mediaSource, isAudio, null, addProbeDelay, cancellationToken); + } + } +} diff --git a/Emby.Server.Implementations/Library/LocalTrailerPostScanTask.cs b/Emby.Server.Implementations/Library/LocalTrailerPostScanTask.cs deleted file mode 100644 index 4830da8fc..000000000 --- a/Emby.Server.Implementations/Library/LocalTrailerPostScanTask.cs +++ /dev/null @@ -1,105 +0,0 @@ -using MediaBrowser.Controller.Channels; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Library; -using MediaBrowser.Model.Entities; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Controller.Dto; -using MediaBrowser.Controller.Entities.Movies; -using MediaBrowser.Controller.Entities.TV; - -namespace Emby.Server.Implementations.Library -{ - public class LocalTrailerPostScanTask : ILibraryPostScanTask - { - private readonly ILibraryManager _libraryManager; - private readonly IChannelManager _channelManager; - - public LocalTrailerPostScanTask(ILibraryManager libraryManager, IChannelManager channelManager) - { - _libraryManager = libraryManager; - _channelManager = channelManager; - } - - public async Task Run(IProgress<double> progress, CancellationToken cancellationToken) - { - var items = _libraryManager.GetItemList(new InternalItemsQuery - { - IncludeItemTypes = new[] { typeof(BoxSet).Name, typeof(Game).Name, typeof(Movie).Name, typeof(Series).Name }, - Recursive = true, - DtoOptions = new DtoOptions(true) - - }).OfType<IHasTrailers>().ToList(); - - var trailerTypes = Enum.GetNames(typeof(TrailerType)) - .Select(i => (TrailerType)Enum.Parse(typeof(TrailerType), i, true)) - .Except(new[] { TrailerType.LocalTrailer }) - .ToArray(); - - var trailers = _libraryManager.GetItemList(new InternalItemsQuery - { - IncludeItemTypes = new[] { typeof(Trailer).Name }, - TrailerTypes = trailerTypes, - Recursive = true, - DtoOptions = new DtoOptions(false) - - }); - - var numComplete = 0; - - foreach (var item in items) - { - cancellationToken.ThrowIfCancellationRequested(); - - AssignTrailers(item, trailers); - - numComplete++; - double percent = numComplete; - percent /= items.Count; - progress.Report(percent * 100); - } - - progress.Report(100); - } - - private void AssignTrailers(IHasTrailers item, IEnumerable<BaseItem> channelTrailers) - { - if (item is Game) - { - return; - } - - var imdbId = item.GetProviderId(MetadataProviders.Imdb); - var tmdbId = item.GetProviderId(MetadataProviders.Tmdb); - - var trailers = channelTrailers.Where(i => - { - if (!string.IsNullOrWhiteSpace(imdbId) && - string.Equals(imdbId, i.GetProviderId(MetadataProviders.Imdb), StringComparison.OrdinalIgnoreCase)) - { - return true; - } - if (!string.IsNullOrWhiteSpace(tmdbId) && - string.Equals(tmdbId, i.GetProviderId(MetadataProviders.Tmdb), StringComparison.OrdinalIgnoreCase)) - { - return true; - } - return false; - }); - - var trailerIds = trailers.Select(i => i.Id) - .ToArray(); - - if (!trailerIds.SequenceEqual(item.RemoteTrailerIds)) - { - item.RemoteTrailerIds = trailerIds; - - var baseItem = (BaseItem)item; - baseItem.UpdateToRepository(ItemUpdateType.MetadataImport, CancellationToken.None); - } - } - } -} diff --git a/Emby.Server.Implementations/Library/MediaSourceManager.cs b/Emby.Server.Implementations/Library/MediaSourceManager.cs index 688da5764..0dc436800 100644 --- a/Emby.Server.Implementations/Library/MediaSourceManager.cs +++ b/Emby.Server.Implementations/Library/MediaSourceManager.cs @@ -1,5 +1,6 @@ using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.Persistence; @@ -16,6 +17,11 @@ using System.Threading.Tasks; using MediaBrowser.Model.IO; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Threading; +using MediaBrowser.Model.Dlna; +using MediaBrowser.Model.Globalization; +using System.IO; +using System.Globalization; +using MediaBrowser.Common.Configuration; namespace Emby.Server.Implementations.Library { @@ -31,8 +37,11 @@ namespace Emby.Server.Implementations.Library private readonly ILogger _logger; private readonly IUserDataManager _userDataManager; private readonly ITimerFactory _timerFactory; + private readonly Func<IMediaEncoder> _mediaEncoder; + private ILocalizationManager _localizationManager; + private IApplicationPaths _appPaths; - public MediaSourceManager(IItemRepository itemRepo, IUserManager userManager, ILibraryManager libraryManager, ILogger logger, IJsonSerializer jsonSerializer, IFileSystem fileSystem, IUserDataManager userDataManager, ITimerFactory timerFactory) + public MediaSourceManager(IItemRepository itemRepo, IApplicationPaths applicationPaths, ILocalizationManager localizationManager, IUserManager userManager, ILibraryManager libraryManager, ILogger logger, IJsonSerializer jsonSerializer, IFileSystem fileSystem, IUserDataManager userDataManager, ITimerFactory timerFactory, Func<IMediaEncoder> mediaEncoder) { _itemRepo = itemRepo; _userManager = userManager; @@ -42,6 +51,9 @@ namespace Emby.Server.Implementations.Library _fileSystem = fileSystem; _userDataManager = userDataManager; _timerFactory = timerFactory; + _mediaEncoder = mediaEncoder; + _localizationManager = localizationManager; + _appPaths = applicationPaths; } public void AddParts(IEnumerable<IMediaSourceProvider> providers) @@ -109,20 +121,23 @@ namespace Emby.Server.Implementations.Library return streams; } - public async Task<IEnumerable<MediaSourceInfo>> GetPlayackMediaSources(string id, string userId, bool enablePathSubstitution, string[] supportedLiveMediaTypes, CancellationToken cancellationToken) + public async Task<List<MediaSourceInfo>> GetPlayackMediaSources(BaseItem item, User user, bool allowMediaProbe, bool enablePathSubstitution, CancellationToken cancellationToken) { - var item = _libraryManager.GetItemById(id); + var mediaSources = GetStaticMediaSources(item, enablePathSubstitution, user); - var hasMediaSources = (IHasMediaSources)item; - User user = null; - - if (!string.IsNullOrWhiteSpace(userId)) + if (allowMediaProbe && mediaSources[0].Type != MediaSourceType.Placeholder && !mediaSources[0].MediaStreams.Any(i => i.Type == MediaStreamType.Audio || i.Type == MediaStreamType.Video)) { - user = _userManager.GetUserById(userId); + await item.RefreshMetadata(new MediaBrowser.Controller.Providers.MetadataRefreshOptions(_fileSystem) + { + EnableRemoteContentProbe = true, + MetadataRefreshMode = MediaBrowser.Controller.Providers.MetadataRefreshMode.FullRefresh + + }, cancellationToken).ConfigureAwait(false); + + mediaSources = GetStaticMediaSources(item, enablePathSubstitution, user); } - var mediaSources = GetStaticMediaSources(hasMediaSources, enablePathSubstitution, user); - var dynamicMediaSources = await GetDynamicMediaSources(hasMediaSources, cancellationToken).ConfigureAwait(false); + var dynamicMediaSources = await GetDynamicMediaSources(item, cancellationToken).ConfigureAwait(false); var list = new List<MediaSourceInfo>(); @@ -132,24 +147,13 @@ namespace Emby.Server.Implementations.Library { if (user != null) { - SetUserProperties(hasMediaSources, source, user); - } - if (source.Protocol == MediaProtocol.File) - { - // TODO: Path substitution - if (!_fileSystem.FileExists(source.Path)) - { - source.SupportsDirectStream = false; - } - } - else if (source.Protocol == MediaProtocol.Http) - { - // TODO: Allow this when the source is plain http, e.g. not HLS or Mpeg Dash - source.SupportsDirectStream = false; + SetDefaultAudioAndSubtitleStreamIndexes(item, source, user); } - else + + // Validate that this is actually possible + if (source.SupportsDirectStream) { - source.SupportsDirectStream = false; + source.SupportsDirectStream = SupportsDirectStream(source.Path, source.Protocol); } list.Add(source); @@ -169,10 +173,63 @@ namespace Emby.Server.Implementations.Library } } - return SortMediaSources(list).Where(i => i.Type != MediaSourceType.Placeholder); + return SortMediaSources(list).Where(i => i.Type != MediaSourceType.Placeholder).ToList(); + } + + public MediaProtocol GetPathProtocol(string path) + { + if (path.StartsWith("Rtsp", StringComparison.OrdinalIgnoreCase)) + { + return MediaProtocol.Rtsp; + } + if (path.StartsWith("Rtmp", StringComparison.OrdinalIgnoreCase)) + { + return MediaProtocol.Rtmp; + } + if (path.StartsWith("Http", StringComparison.OrdinalIgnoreCase)) + { + return MediaProtocol.Http; + } + if (path.StartsWith("rtp", StringComparison.OrdinalIgnoreCase)) + { + return MediaProtocol.Rtp; + } + if (path.StartsWith("ftp", StringComparison.OrdinalIgnoreCase)) + { + return MediaProtocol.Ftp; + } + if (path.StartsWith("udp", StringComparison.OrdinalIgnoreCase)) + { + return MediaProtocol.Udp; + } + + return _fileSystem.IsPathFile(path) ? MediaProtocol.File : MediaProtocol.Http; } - private async Task<IEnumerable<MediaSourceInfo>> GetDynamicMediaSources(IHasMediaSources item, CancellationToken cancellationToken) + public bool SupportsDirectStream(string path, MediaProtocol protocol) + { + if (protocol == MediaProtocol.File) + { + return true; + } + + if (protocol == MediaProtocol.Http) + { + if (path != null) + { + if (path.IndexOf(".m3u", StringComparison.OrdinalIgnoreCase) != -1) + { + return false; + } + + return true; + } + } + + return false; + } + + private async Task<IEnumerable<MediaSourceInfo>> GetDynamicMediaSources(BaseItem item, CancellationToken cancellationToken) { var tasks = _providers.Select(i => GetDynamicMediaSources(item, i, cancellationToken)); var results = await Task.WhenAll(tasks).ConfigureAwait(false); @@ -180,7 +237,7 @@ namespace Emby.Server.Implementations.Library return results.SelectMany(i => i.ToList()); } - private async Task<IEnumerable<MediaSourceInfo>> GetDynamicMediaSources(IHasMediaSources item, IMediaSourceProvider provider, CancellationToken cancellationToken) + private async Task<IEnumerable<MediaSourceInfo>> GetDynamicMediaSources(BaseItem item, IMediaSourceProvider provider, CancellationToken cancellationToken) { try { @@ -207,78 +264,65 @@ namespace Emby.Server.Implementations.Library { var prefix = provider.GetType().FullName.GetMD5().ToString("N") + LiveStreamIdDelimeter; - if (!string.IsNullOrWhiteSpace(mediaSource.OpenToken) && !mediaSource.OpenToken.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) + if (!string.IsNullOrEmpty(mediaSource.OpenToken) && !mediaSource.OpenToken.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) { mediaSource.OpenToken = prefix + mediaSource.OpenToken; } - if (!string.IsNullOrWhiteSpace(mediaSource.LiveStreamId) && !mediaSource.LiveStreamId.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) + if (!string.IsNullOrEmpty(mediaSource.LiveStreamId) && !mediaSource.LiveStreamId.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) { mediaSource.LiveStreamId = prefix + mediaSource.LiveStreamId; } } - public async Task<MediaSourceInfo> GetMediaSource(IHasMediaSources item, string mediaSourceId, string liveStreamId, bool enablePathSubstitution, CancellationToken cancellationToken) + public async Task<MediaSourceInfo> GetMediaSource(BaseItem item, string mediaSourceId, string liveStreamId, bool enablePathSubstitution, CancellationToken cancellationToken) { - if (!string.IsNullOrWhiteSpace(liveStreamId)) + if (!string.IsNullOrEmpty(liveStreamId)) { return await GetLiveStream(liveStreamId, cancellationToken).ConfigureAwait(false); } - //await _liveStreamSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false); - - //try - //{ - // var stream = _openStreams.Values.FirstOrDefault(i => string.Equals(i.MediaSource.Id, mediaSourceId, StringComparison.OrdinalIgnoreCase)); - // if (stream != null) - // { - // return stream.MediaSource; - // } - //} - //finally - //{ - // _liveStreamSemaphore.Release(); - //} - - var sources = await GetPlayackMediaSources(item.Id.ToString("N"), null, enablePathSubstitution, new[] { MediaType.Audio, MediaType.Video }, - CancellationToken.None).ConfigureAwait(false); + var sources = await GetPlayackMediaSources(item, null, false, enablePathSubstitution, cancellationToken).ConfigureAwait(false); return sources.FirstOrDefault(i => string.Equals(i.Id, mediaSourceId, StringComparison.OrdinalIgnoreCase)); } - public List<MediaSourceInfo> GetStaticMediaSources(IHasMediaSources item, bool enablePathSubstitution, User user = null) + public List<MediaSourceInfo> GetStaticMediaSources(BaseItem item, bool enablePathSubstitution, User user = null) { if (item == null) { throw new ArgumentNullException("item"); } - if (!(item is Video)) - { - return item.GetMediaSources(enablePathSubstitution); - } + var hasMediaSources = (IHasMediaSources)item; - var sources = item.GetMediaSources(enablePathSubstitution); + var sources = hasMediaSources.GetMediaSources(enablePathSubstitution); if (user != null) { foreach (var source in sources) { - SetUserProperties(item, source, user); + SetDefaultAudioAndSubtitleStreamIndexes(item, source, user); } } return sources; } - private void SetUserProperties(IHasUserData item, MediaSourceInfo source, User user) + private string[] NormalizeLanguage(string language) { - var userData = item == null ? new UserItemData() : _userDataManager.GetUserData(user, item); + if (language != null) + { + var culture = _localizationManager.FindLanguageInfo(language); + if (culture != null) + { + return culture.ThreeLetterISOLanguageNames; + } - var allowRememberingSelection = item == null || item.EnableRememberingTrackSelections; + return new string[] { language }; + } - SetDefaultAudioStreamIndex(source, userData, user, allowRememberingSelection); - SetDefaultSubtitleStreamIndex(source, userData, user, allowRememberingSelection); + return Array.Empty<string>(); } private void SetDefaultSubtitleStreamIndex(MediaSourceInfo source, UserItemData userData, User user, bool allowRememberingSelection) @@ -293,9 +337,9 @@ namespace Emby.Server.Implementations.Library return; } } - + var preferredSubs = string.IsNullOrEmpty(user.Configuration.SubtitleLanguagePreference) - ? new List<string>() : new List<string> { user.Configuration.SubtitleLanguagePreference }; + ? Array.Empty<string>() : NormalizeLanguage(user.Configuration.SubtitleLanguagePreference); var defaultAudioIndex = source.DefaultAudioStreamIndex; var audioLangage = defaultAudioIndex == null @@ -325,12 +369,37 @@ namespace Emby.Server.Implementations.Library } var preferredAudio = string.IsNullOrEmpty(user.Configuration.AudioLanguagePreference) - ? new string[] { } - : new[] { user.Configuration.AudioLanguagePreference }; + ? Array.Empty<string>() + : NormalizeLanguage(user.Configuration.AudioLanguagePreference); source.DefaultAudioStreamIndex = MediaStreamSelector.GetDefaultAudioStreamIndex(source.MediaStreams, preferredAudio, user.Configuration.PlayDefaultAudioTrack); } + public void SetDefaultAudioAndSubtitleStreamIndexes(BaseItem item, MediaSourceInfo source, User user) + { + // Item would only be null if the app didn't supply ItemId as part of the live stream open request + var mediaType = item == null ? MediaType.Video : item.MediaType; + + if (string.Equals(mediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase)) + { + var userData = item == null ? new UserItemData() : _userDataManager.GetUserData(user, item); + + var allowRememberingSelection = item == null || item.EnableRememberingTrackSelections; + + SetDefaultAudioStreamIndex(source, userData, user, allowRememberingSelection); + SetDefaultSubtitleStreamIndex(source, userData, user, allowRememberingSelection); + } + else if (string.Equals(mediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase)) + { + var audio = source.MediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Audio); + + if (audio != null) + { + source.DefaultAudioStreamIndex = audio.Index; + } + } + } + private IEnumerable<MediaSourceInfo> SortMediaSources(IEnumerable<MediaSourceInfo> sources) { return sources.OrderBy(i => @@ -352,55 +421,157 @@ namespace Emby.Server.Implementations.Library .ToList(); } - private readonly Dictionary<string, LiveStreamInfo> _openStreams = new Dictionary<string, LiveStreamInfo>(StringComparer.OrdinalIgnoreCase); + private readonly Dictionary<string, ILiveStream> _openStreams = new Dictionary<string, ILiveStream>(StringComparer.OrdinalIgnoreCase); private readonly SemaphoreSlim _liveStreamSemaphore = new SemaphoreSlim(1, 1); - public async Task<LiveStreamResponse> OpenLiveStream(LiveStreamRequest request, CancellationToken cancellationToken) + public async Task<Tuple<LiveStreamResponse, IDirectStreamProvider>> OpenLiveStreamInternal(LiveStreamRequest request, CancellationToken cancellationToken) { await _liveStreamSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false); + MediaSourceInfo mediaSource; + ILiveStream liveStream; + try { var tuple = GetProvider(request.OpenToken); var provider = tuple.Item1; - var mediaSourceTuple = await provider.OpenMediaSource(tuple.Item2, request.EnableMediaProbe, cancellationToken).ConfigureAwait(false); + var currentLiveStreams = _openStreams.Values.ToList(); + + liveStream = await provider.OpenMediaSource(tuple.Item2, currentLiveStreams, cancellationToken).ConfigureAwait(false); - var mediaSource = mediaSourceTuple.Item1; + mediaSource = liveStream.MediaSource; - if (string.IsNullOrWhiteSpace(mediaSource.LiveStreamId)) + // Validate that this is actually possible + if (mediaSource.SupportsDirectStream) { - throw new InvalidOperationException(string.Format("{0} returned null LiveStreamId", provider.GetType().Name)); + mediaSource.SupportsDirectStream = SupportsDirectStream(mediaSource.Path, mediaSource.Protocol); } SetKeyProperties(provider, mediaSource); - var info = new LiveStreamInfo + _openStreams[mediaSource.LiveStreamId] = liveStream; + } + finally + { + _liveStreamSemaphore.Release(); + } + + // TODO: Don't hardcode this + var isAudio = false; + + try + { + if (mediaSource.MediaStreams.Any(i => i.Index != -1) || !mediaSource.SupportsProbing) { - Id = mediaSource.LiveStreamId, - MediaSource = mediaSource, - DirectStreamProvider = mediaSourceTuple.Item2 - }; - - _openStreams[mediaSource.LiveStreamId] = info; - - var json = _jsonSerializer.SerializeToString(mediaSource); - _logger.Debug("Live stream opened: " + json); - var clone = _jsonSerializer.DeserializeFromString<MediaSourceInfo>(json); - - if (!string.IsNullOrWhiteSpace(request.UserId)) - { - var user = _userManager.GetUserById(request.UserId); - var item = string.IsNullOrWhiteSpace(request.ItemId) - ? null - : _libraryManager.GetItemById(request.ItemId); - SetUserProperties(item, clone, user); + AddMediaInfo(mediaSource, isAudio); + } + else + { + // hack - these two values were taken from LiveTVMediaSourceProvider + var cacheKey = request.OpenToken; + + await new LiveStreamHelper(_mediaEncoder(), _logger, _jsonSerializer, _appPaths).AddMediaInfoWithProbe(mediaSource, isAudio, cacheKey, true, cancellationToken).ConfigureAwait(false); } + } + catch (Exception ex) + { + _logger.ErrorException("Error probing live tv stream", ex); + AddMediaInfo(mediaSource, isAudio); + } + + var json = _jsonSerializer.SerializeToString(mediaSource); + _logger.Info("Live stream opened: " + json); + var clone = _jsonSerializer.DeserializeFromString<MediaSourceInfo>(json); + + if (!request.UserId.Equals(Guid.Empty)) + { + var user = _userManager.GetUserById(request.UserId); + var item = request.ItemId.Equals(Guid.Empty) + ? null + : _libraryManager.GetItemById(request.ItemId); + SetDefaultAudioAndSubtitleStreamIndexes(item, clone, user); + } + + return new Tuple<LiveStreamResponse, IDirectStreamProvider>(new LiveStreamResponse + { + MediaSource = clone + + }, liveStream as IDirectStreamProvider); + } + + private void AddMediaInfo(MediaSourceInfo mediaSource, bool isAudio) + { + mediaSource.DefaultSubtitleStreamIndex = null; + + // Null this out so that it will be treated like a live stream + if (mediaSource.IsInfiniteStream) + { + mediaSource.RunTimeTicks = null; + } - return new LiveStreamResponse + var audioStream = mediaSource.MediaStreams.FirstOrDefault(i => i.Type == MediaBrowser.Model.Entities.MediaStreamType.Audio); + + if (audioStream == null || audioStream.Index == -1) + { + mediaSource.DefaultAudioStreamIndex = null; + } + else + { + mediaSource.DefaultAudioStreamIndex = audioStream.Index; + } + + var videoStream = mediaSource.MediaStreams.FirstOrDefault(i => i.Type == MediaBrowser.Model.Entities.MediaStreamType.Video); + if (videoStream != null) + { + if (!videoStream.BitRate.HasValue) { - MediaSource = clone - }; + var width = videoStream.Width ?? 1920; + + if (width >= 3000) + { + videoStream.BitRate = 30000000; + } + + else if (width >= 1900) + { + videoStream.BitRate = 20000000; + } + + else if (width >= 1200) + { + videoStream.BitRate = 8000000; + } + + else if (width >= 700) + { + videoStream.BitRate = 2000000; + } + } + } + + // Try to estimate this + mediaSource.InferTotalBitrate(); + } + + public async Task<IDirectStreamProvider> GetDirectStreamProviderByUniqueId(string uniqueId, CancellationToken cancellationToken) + { + await _liveStreamSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false); + + try + { + var info = _openStreams.Values.FirstOrDefault(i => + { + var liveStream = i as ILiveStream; + if (liveStream != null) + { + return string.Equals(liveStream.UniqueId, uniqueId, StringComparison.OrdinalIgnoreCase); + } + + return false; + }); + + return info as IDirectStreamProvider; } finally { @@ -408,23 +579,207 @@ namespace Emby.Server.Implementations.Library } } + public async Task<LiveStreamResponse> OpenLiveStream(LiveStreamRequest request, CancellationToken cancellationToken) + { + var result = await OpenLiveStreamInternal(request, cancellationToken).ConfigureAwait(false); + return result.Item1; + } + + public async Task<MediaSourceInfo> GetLiveStreamMediaInfo(string id, CancellationToken cancellationToken) + { + var liveStreamInfo = await GetLiveStreamInfo(id, cancellationToken).ConfigureAwait(false); + + var mediaSource = liveStreamInfo.MediaSource; + + if (liveStreamInfo is IDirectStreamProvider) + { + var info = await _mediaEncoder().GetMediaInfo(new MediaInfoRequest + { + MediaSource = mediaSource, + ExtractChapters = false, + MediaType = DlnaProfileType.Video + + }, cancellationToken).ConfigureAwait(false); + + mediaSource.MediaStreams = info.MediaStreams; + mediaSource.Container = info.Container; + mediaSource.Bitrate = info.Bitrate; + } + + return mediaSource; + } + + public async Task AddMediaInfoWithProbe(MediaSourceInfo mediaSource, bool isAudio, string cacheKey, bool addProbeDelay, bool isLiveStream, CancellationToken cancellationToken) + { + var originalRuntime = mediaSource.RunTimeTicks; + + var now = DateTime.UtcNow; + + MediaInfo mediaInfo = null; + var cacheFilePath = string.IsNullOrEmpty(cacheKey) ? null : Path.Combine(_appPaths.CachePath, "mediainfo", cacheKey.GetMD5().ToString("N") + ".json"); + + if (!string.IsNullOrEmpty(cacheKey)) + { + try + { + mediaInfo = _jsonSerializer.DeserializeFromFile<MediaInfo>(cacheFilePath); + + //_logger.Debug("Found cached media info"); + } + catch (Exception ex) + { + } + } + + if (mediaInfo == null) + { + if (addProbeDelay) + { + var delayMs = mediaSource.AnalyzeDurationMs ?? 0; + delayMs = Math.Max(3000, delayMs); + await Task.Delay(delayMs, cancellationToken).ConfigureAwait(false); + } + + if (isLiveStream) + { + mediaSource.AnalyzeDurationMs = 3000; + } + + mediaInfo = await _mediaEncoder().GetMediaInfo(new MediaInfoRequest + { + MediaSource = mediaSource, + MediaType = isAudio ? DlnaProfileType.Audio : DlnaProfileType.Video, + ExtractChapters = false + + }, cancellationToken).ConfigureAwait(false); + + if (cacheFilePath != null) + { + _fileSystem.CreateDirectory(_fileSystem.GetDirectoryName(cacheFilePath)); + _jsonSerializer.SerializeToFile(mediaInfo, cacheFilePath); + + //_logger.Debug("Saved media info to {0}", cacheFilePath); + } + } + + var mediaStreams = mediaInfo.MediaStreams; + + if (isLiveStream && !string.IsNullOrEmpty(cacheKey)) + { + var newList = new List<MediaStream>(); + newList.AddRange(mediaStreams.Where(i => i.Type == MediaStreamType.Video).Take(1)); + newList.AddRange(mediaStreams.Where(i => i.Type == MediaStreamType.Audio).Take(1)); + + foreach (var stream in newList) + { + stream.Index = -1; + stream.Language = null; + } + + mediaStreams = newList; + } + + _logger.Info("Live tv media info probe took {0} seconds", (DateTime.UtcNow - now).TotalSeconds.ToString(CultureInfo.InvariantCulture)); + + mediaSource.Bitrate = mediaInfo.Bitrate; + mediaSource.Container = mediaInfo.Container; + mediaSource.Formats = mediaInfo.Formats; + mediaSource.MediaStreams = mediaStreams; + mediaSource.RunTimeTicks = mediaInfo.RunTimeTicks; + mediaSource.Size = mediaInfo.Size; + mediaSource.Timestamp = mediaInfo.Timestamp; + mediaSource.Video3DFormat = mediaInfo.Video3DFormat; + mediaSource.VideoType = mediaInfo.VideoType; + + mediaSource.DefaultSubtitleStreamIndex = null; + + if (isLiveStream) + { + // Null this out so that it will be treated like a live stream + if (!originalRuntime.HasValue) + { + mediaSource.RunTimeTicks = null; + } + } + + var audioStream = mediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Audio); + + if (audioStream == null || audioStream.Index == -1) + { + mediaSource.DefaultAudioStreamIndex = null; + } + else + { + mediaSource.DefaultAudioStreamIndex = audioStream.Index; + } + + var videoStream = mediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Video); + if (videoStream != null) + { + if (!videoStream.BitRate.HasValue) + { + var width = videoStream.Width ?? 1920; + + if (width >= 3000) + { + videoStream.BitRate = 30000000; + } + + else if (width >= 1900) + { + videoStream.BitRate = 20000000; + } + + else if (width >= 1200) + { + videoStream.BitRate = 8000000; + } + + else if (width >= 700) + { + videoStream.BitRate = 2000000; + } + } + + // This is coming up false and preventing stream copy + videoStream.IsAVC = null; + } + + if (isLiveStream) + { + mediaSource.AnalyzeDurationMs = 3000; + } + + // Try to estimate this + mediaSource.InferTotalBitrate(true); + } + public async Task<Tuple<MediaSourceInfo, IDirectStreamProvider>> GetLiveStreamWithDirectStreamProvider(string id, CancellationToken cancellationToken) { - if (string.IsNullOrWhiteSpace(id)) + if (string.IsNullOrEmpty(id)) { throw new ArgumentNullException("id"); } - _logger.Debug("Getting already opened live stream {0}", id); + var info = await GetLiveStreamInfo(id, cancellationToken).ConfigureAwait(false); + return new Tuple<MediaSourceInfo, IDirectStreamProvider>(info.MediaSource, info as IDirectStreamProvider); + } + + private async Task<ILiveStream> GetLiveStreamInfo(string id, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(id)) + { + throw new ArgumentNullException("id"); + } await _liveStreamSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false); try { - LiveStreamInfo info; + ILiveStream info; if (_openStreams.TryGetValue(id, out info)) { - return new Tuple<MediaSourceInfo, IDirectStreamProvider>(info.MediaSource, info.DirectStreamProvider); + return info; } else { @@ -443,26 +798,9 @@ namespace Emby.Server.Implementations.Library return result.Item1; } - private async Task CloseLiveStreamWithProvider(IMediaSourceProvider provider, string streamId) - { - _logger.Info("Closing live stream {0} with provider {1}", streamId, provider.GetType().Name); - - try - { - await provider.CloseMediaSource(streamId).ConfigureAwait(false); - } - catch (NotImplementedException) - { - } - catch (Exception ex) - { - _logger.ErrorException("Error closing live stream {0}", ex, streamId); - } - } - public async Task CloseLiveStream(string id) { - if (string.IsNullOrWhiteSpace(id)) + if (string.IsNullOrEmpty(id)) { throw new ArgumentNullException("id"); } @@ -471,18 +809,22 @@ namespace Emby.Server.Implementations.Library try { - LiveStreamInfo current; + ILiveStream liveStream; - if (_openStreams.TryGetValue(id, out current)) + if (_openStreams.TryGetValue(id, out liveStream)) { - _openStreams.Remove(id); - current.Closed = true; + liveStream.ConsumerCount--; - if (current.MediaSource.RequiresClosing) + _logger.Info("Live stream {0} consumer count is now {1}", liveStream.OriginalStreamId, liveStream.ConsumerCount); + + if (liveStream.ConsumerCount <= 0) { - var tuple = GetProvider(id); + _openStreams.Remove(id); + + _logger.Info("Closing live stream {0}", id); - await CloseLiveStreamWithProvider(tuple.Item1, tuple.Item2).ConfigureAwait(false); + await liveStream.Close().ConfigureAwait(false); + _logger.Info("Live stream {0} closed successfully", id); } } } @@ -497,7 +839,7 @@ namespace Emby.Server.Implementations.Library private Tuple<IMediaSourceProvider, string> GetProvider(string key) { - if (string.IsNullOrWhiteSpace(key)) + if (string.IsNullOrEmpty(key)) { throw new ArgumentException("key"); } @@ -518,7 +860,6 @@ namespace Emby.Server.Implementations.Library public void Dispose() { Dispose(true); - GC.SuppressFinalize(this); } private readonly object _disposeLock = new object(); @@ -541,13 +882,5 @@ namespace Emby.Server.Implementations.Library } } } - - private class LiveStreamInfo - { - public string Id; - public bool Closed; - public MediaSourceInfo MediaSource; - public IDirectStreamProvider DirectStreamProvider; - } } }
\ No newline at end of file diff --git a/Emby.Server.Implementations/Library/MediaStreamSelector.cs b/Emby.Server.Implementations/Library/MediaStreamSelector.cs new file mode 100644 index 000000000..5d4c5a452 --- /dev/null +++ b/Emby.Server.Implementations/Library/MediaStreamSelector.cs @@ -0,0 +1,217 @@ +using MediaBrowser.Model.Configuration; +using MediaBrowser.Model.Entities; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Emby.Server.Implementations.Library +{ + public static class MediaStreamSelector + { + public static int? GetDefaultAudioStreamIndex(List<MediaStream> streams, string[] preferredLanguages, bool preferDefaultTrack) + { + streams = GetSortedStreams(streams, MediaStreamType.Audio, preferredLanguages) + .ToList(); + + if (preferDefaultTrack) + { + var defaultStream = streams.FirstOrDefault(i => i.IsDefault); + + if (defaultStream != null) + { + return defaultStream.Index; + } + } + + var stream = streams.FirstOrDefault(); + + if (stream != null) + { + return stream.Index; + } + + return null; + } + + public static int? GetDefaultSubtitleStreamIndex(List<MediaStream> streams, + string[] preferredLanguages, + SubtitlePlaybackMode mode, + string audioTrackLanguage) + { + streams = GetSortedStreams(streams, MediaStreamType.Subtitle, preferredLanguages) + .ToList(); + + MediaStream stream = null; + + if (mode == SubtitlePlaybackMode.None) + { + return null; + } + + if (mode == SubtitlePlaybackMode.Default) + { + // Prefer embedded metadata over smart logic + + stream = streams.FirstOrDefault(s => s.IsForced && string.Equals(s.Language, audioTrackLanguage, StringComparison.OrdinalIgnoreCase)) ?? + streams.FirstOrDefault(s => s.IsForced) ?? + streams.FirstOrDefault(s => s.IsDefault); + + // if the audio language is not understood by the user, load their preferred subs, if there are any + if (stream == null && !preferredLanguages.Contains(audioTrackLanguage, StringComparer.OrdinalIgnoreCase)) + { + stream = streams.Where(s => !s.IsForced).FirstOrDefault(s => preferredLanguages.Contains(s.Language, StringComparer.OrdinalIgnoreCase)); + } + } + else if (mode == SubtitlePlaybackMode.Smart) + { + // Prefer smart logic over embedded metadata + + // if the audio language is not understood by the user, load their preferred subs, if there are any + if (!preferredLanguages.Contains(audioTrackLanguage, StringComparer.OrdinalIgnoreCase)) + { + stream = streams.Where(s => !s.IsForced).FirstOrDefault(s => preferredLanguages.Contains(s.Language, StringComparer.OrdinalIgnoreCase)) ?? + streams.FirstOrDefault(s => preferredLanguages.Contains(s.Language, StringComparer.OrdinalIgnoreCase)); + } + } + else if (mode == SubtitlePlaybackMode.Always) + { + // always load the most suitable full subtitles + stream = streams.FirstOrDefault(s => !s.IsForced); + } + else if (mode == SubtitlePlaybackMode.OnlyForced) + { + // always load the most suitable full subtitles + stream = streams.FirstOrDefault(s => s.IsForced && string.Equals(s.Language, audioTrackLanguage, StringComparison.OrdinalIgnoreCase)) ?? + streams.FirstOrDefault(s => s.IsForced); + } + + // load forced subs if we have found no suitable full subtitles + stream = stream ?? streams.FirstOrDefault(s => s.IsForced && string.Equals(s.Language, audioTrackLanguage, StringComparison.OrdinalIgnoreCase)); + + if (stream != null) + { + return stream.Index; + } + + return null; + } + + private static IEnumerable<MediaStream> GetSortedStreams(IEnumerable<MediaStream> streams, MediaStreamType type, string[] languagePreferences) + { + // Give some preferance to external text subs for better performance + return streams.Where(i => i.Type == type) + .OrderBy(i => + { + var index = FindIndex(languagePreferences, i.Language); + + return index == -1 ? 100 : index; + }) + .ThenBy(i => GetBooleanOrderBy(i.IsDefault)) + .ThenBy(i => GetBooleanOrderBy(i.SupportsExternalStream)) + .ThenBy(i => GetBooleanOrderBy(i.IsTextSubtitleStream)) + .ThenBy(i => GetBooleanOrderBy(i.IsExternal)) + .ThenBy(i => i.Index); + } + + public static void SetSubtitleStreamScores(List<MediaStream> streams, + string[] preferredLanguages, + SubtitlePlaybackMode mode, + string audioTrackLanguage) + { + if (mode == SubtitlePlaybackMode.None) + { + return; + } + + streams = GetSortedStreams(streams, MediaStreamType.Subtitle, preferredLanguages) + .ToList(); + + var filteredStreams = new List<MediaStream>(); + + if (mode == SubtitlePlaybackMode.Default) + { + // Prefer embedded metadata over smart logic + filteredStreams = streams.Where(s => s.IsForced || s.IsDefault) + .ToList(); + } + else if (mode == SubtitlePlaybackMode.Smart) + { + // Prefer smart logic over embedded metadata + if (!preferredLanguages.Contains(audioTrackLanguage, StringComparer.OrdinalIgnoreCase)) + { + filteredStreams = streams.Where(s => !s.IsForced && preferredLanguages.Contains(s.Language, StringComparer.OrdinalIgnoreCase)) + .ToList(); + } + } + else if (mode == SubtitlePlaybackMode.Always) + { + // always load the most suitable full subtitles + filteredStreams = streams.Where(s => !s.IsForced) + .ToList(); + } + else if (mode == SubtitlePlaybackMode.OnlyForced) + { + // always load the most suitable full subtitles + filteredStreams = streams.Where(s => s.IsForced).ToList(); + } + + // load forced subs if we have found no suitable full subtitles + if (filteredStreams.Count == 0) + { + filteredStreams = streams + .Where(s => s.IsForced && string.Equals(s.Language, audioTrackLanguage, StringComparison.OrdinalIgnoreCase)) + .ToList(); + } + + foreach (var stream in filteredStreams) + { + stream.Score = GetSubtitleScore(stream, preferredLanguages); + } + } + + private static int FindIndex(string[] list, string value) + { + for (var i=0; i< list.Length; i++) + { + if (string.Equals(list[i], value, StringComparison.OrdinalIgnoreCase)) + { + return i; + } + } + + return -1; + } + + private static int GetSubtitleScore(MediaStream stream, string[] languagePreferences) + { + var values = new List<int>(); + + var index = FindIndex(languagePreferences, stream.Language); + + values.Add(index == -1 ? 0 : 100 - index); + + values.Add(stream.IsForced ? 1 : 0); + values.Add(stream.IsDefault ? 1 : 0); + values.Add(stream.SupportsExternalStream ? 1 : 0); + values.Add(stream.IsTextSubtitleStream ? 1 : 0); + values.Add(stream.IsExternal ? 1 : 0); + + values.Reverse(); + var scale = 1; + var score = 0; + + foreach (var value in values) + { + score += scale * (value + 1); + scale *= 10; + } + + return score; + } + + private static int GetBooleanOrderBy(bool value) + { + return value ? 0 : 1; + } + } +} diff --git a/Emby.Server.Implementations/Library/MusicManager.cs b/Emby.Server.Implementations/Library/MusicManager.cs index 1cbf4235a..1319ee6f4 100644 --- a/Emby.Server.Implementations/Library/MusicManager.cs +++ b/Emby.Server.Implementations/Library/MusicManager.cs @@ -67,19 +67,19 @@ namespace Emby.Server.Implementations.Library { try { - return _libraryManager.GetMusicGenre(i).Id.ToString("N"); + return _libraryManager.GetMusicGenre(i).Id; } catch { - return null; + return Guid.Empty; } - }).Where(i => i != null); + }).Where(i => !i.Equals(Guid.Empty)).ToArray(); return GetInstantMixFromGenreIds(genreIds, user, dtoOptions); } - public List<BaseItem> GetInstantMixFromGenreIds(IEnumerable<string> genreIds, User user, DtoOptions dtoOptions) + public List<BaseItem> GetInstantMixFromGenreIds(Guid[] genreIds, User user, DtoOptions dtoOptions) { return _libraryManager.GetItemList(new InternalItemsQuery(user) { @@ -89,7 +89,7 @@ namespace Emby.Server.Implementations.Library Limit = 200, - OrderBy = new [] { new Tuple<string, SortOrder>(ItemSortBy.Random, SortOrder.Ascending) }, + OrderBy = new [] { new ValueTuple<string, SortOrder>(ItemSortBy.Random, SortOrder.Ascending) }, DtoOptions = dtoOptions @@ -101,7 +101,7 @@ namespace Emby.Server.Implementations.Library var genre = item as MusicGenre; if (genre != null) { - return GetInstantMixFromGenreIds(new[] { item.Id.ToString("N") }, user, dtoOptions); + return GetInstantMixFromGenreIds(new[] { item.Id }, user, dtoOptions); } var playlist = item as Playlist; diff --git a/Emby.Server.Implementations/Library/ResolverHelper.cs b/Emby.Server.Implementations/Library/ResolverHelper.cs index d0096de0c..14b28966a 100644 --- a/Emby.Server.Implementations/Library/ResolverHelper.cs +++ b/Emby.Server.Implementations/Library/ResolverHelper.cs @@ -26,7 +26,7 @@ namespace Emby.Server.Implementations.Library public static void SetInitialItemValues(BaseItem item, Folder parent, IFileSystem fileSystem, ILibraryManager libraryManager, IDirectoryService directoryService) { // This version of the below method has no ItemResolveArgs, so we have to require the path already being set - if (string.IsNullOrWhiteSpace(item.Path)) + if (string.IsNullOrEmpty(item.Path)) { throw new ArgumentException("Item must have a Path"); } @@ -108,17 +108,6 @@ namespace Emby.Server.Implementations.Library } /// <summary> - /// The MB name regex - /// </summary> - private static readonly Regex MbNameRegex = new Regex(@"(\[.*?\])"); - - internal static string StripBrackets(string inputString) - { - var output = MbNameRegex.Replace(inputString, string.Empty).Trim(); - return Regex.Replace(output, @"\s+", " "); - } - - /// <summary> /// Ensures DateCreated and DateModified have values /// </summary> /// <param name="fileSystem">The file system.</param> @@ -140,7 +129,7 @@ namespace Emby.Server.Implementations.Library } // See if a different path came out of the resolver than what went in - if (!string.Equals(args.Path, item.Path, StringComparison.OrdinalIgnoreCase)) + if (!fileSystem.AreEqual(args.Path, item.Path)) { var childData = args.IsDirectory ? args.GetFileSystemEntryByPath(item.Path) : null; @@ -173,7 +162,14 @@ namespace Emby.Server.Implementations.Library // directoryService.getFile may return null if (info != null) { - item.DateCreated = fileSystem.GetCreationTimeUtc(info); + var dateCreated = fileSystem.GetCreationTimeUtc(info); + + if (dateCreated.Equals(DateTime.MinValue)) + { + dateCreated = DateTime.UtcNow; + } + + item.DateCreated = dateCreated; } } else diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs index d30aaa133..8872bd641 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs @@ -101,13 +101,15 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio if (LibraryManager.IsAudioFile(args.Path, libraryOptions)) { - if (string.Equals(Path.GetExtension(args.Path), ".cue", StringComparison.OrdinalIgnoreCase)) + var extension = Path.GetExtension(args.Path); + + if (string.Equals(extension, ".cue", StringComparison.OrdinalIgnoreCase)) { // if audio file exists of same name, return null return null; } - var isMixedCollectionType = string.IsNullOrWhiteSpace(collectionType); + var isMixedCollectionType = string.IsNullOrEmpty(collectionType); // For conflicting extensions, give priority to videos if (isMixedCollectionType && LibraryManager.IsVideoFile(args.Path, libraryOptions)) @@ -134,6 +136,8 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio if (item != null) { + item.IsShortcut = string.Equals(extension, ".strm", StringComparison.OrdinalIgnoreCase); + item.IsInMixedFolder = true; } diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs index b8ec41805..a33f101ae 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs @@ -52,14 +52,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio /// <returns>MusicAlbum.</returns> protected override MusicAlbum Resolve(ItemResolveArgs args) { - if (!args.IsDirectory) return null; - - // Avoid mis-identifying top folders - if (args.HasParent<MusicAlbum>()) return null; - if (args.Parent.IsRoot) return null; - var collectionType = args.GetCollectionType(); - var isMusicMediaFolder = string.Equals(collectionType, CollectionType.Music, StringComparison.OrdinalIgnoreCase); // If there's a collection type and it's not music, don't allow it. @@ -68,6 +61,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio return null; } + if (!args.IsDirectory) return null; + + // Avoid mis-identifying top folders + if (args.HasParent<MusicAlbum>()) return null; + if (args.Parent.IsRoot) return null; + return IsMusicAlbum(args) ? new MusicAlbum() : null; } @@ -117,24 +116,22 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio { if (allowSubfolders) { + if (notMultiDisc) + { + continue; + } + var path = fileSystemInfo.FullName; - var isMultiDisc = IsMultiDiscFolder(path, libraryOptions); + var hasMusic = ContainsMusic(directoryService.GetFileSystemEntries(path), false, directoryService, logger, fileSystem, libraryOptions, libraryManager); - if (isMultiDisc) + if (hasMusic) { - var hasMusic = ContainsMusic(directoryService.GetFileSystemEntries(path), false, directoryService, logger, fileSystem, libraryOptions, libraryManager); - - if (hasMusic) + if (IsMultiDiscFolder(path, libraryOptions)) { logger.Debug("Found multi-disc folder: " + path); discSubfolderCount++; } - } - else - { - var hasMusic = ContainsMusic(directoryService.GetFileSystemEntries(path), false, directoryService, logger, fileSystem, libraryOptions, libraryManager); - - if (hasMusic) + else { // If there are folders underneath with music that are not multidisc, then this can't be a multi-disc album notMultiDisc = true; diff --git a/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs b/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs index 7e960f85e..556748183 100644 --- a/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs @@ -184,11 +184,6 @@ namespace Emby.Server.Implementations.Library.Resolvers else if (string.Equals(videoInfo.StubType, "bluray", StringComparison.OrdinalIgnoreCase)) { video.VideoType = VideoType.BluRay; - video.IsHD = true; - } - else if (string.Equals(videoInfo.StubType, "hdtv", StringComparison.OrdinalIgnoreCase)) - { - video.IsHD = true; } } diff --git a/Emby.Server.Implementations/Library/Resolvers/Movies/BoxSetResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Movies/BoxSetResolver.cs index df441c5ed..b9aca1417 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Movies/BoxSetResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Movies/BoxSetResolver.cs @@ -4,6 +4,7 @@ using MediaBrowser.Controller.Library; using MediaBrowser.Model.Entities; using System; using System.IO; +using MediaBrowser.Model.Extensions; namespace Emby.Server.Implementations.Library.Resolvers.Movies { @@ -30,14 +31,13 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies { return null; } - - if (filename.IndexOf("[boxset]", StringComparison.OrdinalIgnoreCase) != -1 || - args.ContainsFileSystemEntryByName("collection.xml")) + + if (filename.IndexOf("[boxset]", StringComparison.OrdinalIgnoreCase) != -1 || args.ContainsFileSystemEntryByName("collection.xml")) { return new BoxSet { Path = args.Path, - Name = ResolverHelper.StripBrackets(Path.GetFileName(args.Path)) + Name = Path.GetFileName(args.Path).Replace("[boxset]", string.Empty, StringComparison.OrdinalIgnoreCase).Trim() }; } } diff --git a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs index d74235ec7..1394e3858 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs @@ -78,7 +78,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies return ResolveVideos<Video>(parent, files, directoryService, false, collectionType, false); } - if (string.IsNullOrWhiteSpace(collectionType)) + if (string.IsNullOrEmpty(collectionType)) { // Owned items should just use the plain video type if (parent == null) @@ -113,7 +113,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies foreach (var child in fileSystemEntries) { // This is a hack but currently no better way to resolve a sometimes ambiguous situation - if (string.IsNullOrWhiteSpace(collectionType)) + if (string.IsNullOrEmpty(collectionType)) { if (string.Equals(child.Name, "tvshow.nfo", StringComparison.OrdinalIgnoreCase) || string.Equals(child.Name, "season.nfo", StringComparison.OrdinalIgnoreCase)) @@ -126,6 +126,10 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies { leftOver.Add(child); } + else if (IsIgnored(child.Name)) + { + + } else { files.Add(child); @@ -172,6 +176,22 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies return result; } + private bool IsIgnored(string filename) + { + // Ignore samples + var sampleFilename = " " + filename.Replace(".", " ", StringComparison.OrdinalIgnoreCase) + .Replace("-", " ", StringComparison.OrdinalIgnoreCase) + .Replace("_", " ", StringComparison.OrdinalIgnoreCase) + .Replace("!", " ", StringComparison.OrdinalIgnoreCase); + + if (sampleFilename.IndexOf(" sample ", StringComparison.OrdinalIgnoreCase) != -1) + { + return true; + } + + return false; + } + private bool ContainsFile(List<VideoInfo> result, FileSystemMetadata file) { return result.Any(i => ContainsFile(i, file)); @@ -317,7 +337,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies //we need to only look at the name of this actual item (not parents) var justName = item.IsInMixedFolder ? Path.GetFileName(item.Path) : Path.GetFileName(item.ContainingFolderPath); - if (!string.IsNullOrWhiteSpace(justName)) + if (!string.IsNullOrEmpty(justName)) { // check for tmdb id var tmdbid = justName.GetAttributeValue("tmdbid"); @@ -328,7 +348,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies } } - if (!string.IsNullOrWhiteSpace(item.Path)) + if (!string.IsNullOrEmpty(item.Path)) { // check for imdb id - we use full media path, as we can assume, that this will match in any use case (wither id in parent dir or in file name) var imdbid = item.Path.GetAttributeValue("imdbid"); @@ -395,16 +415,14 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies Set3DFormat(movie); return movie; } - else if (supportPhotos && !child.IsHidden && PhotoResolver.IsImageFile(child.FullName, _imageProcessor)) + else if (supportPhotos && PhotoResolver.IsImageFile(child.FullName, _imageProcessor)) { photos.Add(child); } } // TODO: Allow GetMultiDiscMovie in here - var supportsMultiVersion = !string.Equals(collectionType, CollectionType.HomeVideos) && - !string.Equals(collectionType, CollectionType.Photos) && - !string.Equals(collectionType, CollectionType.MusicVideos); + var supportsMultiVersion = true; var result = ResolveVideos<T>(parent, fileSystemEntries, directoryService, supportsMultiVersion, collectionType, parseName) ?? new MultiItemResolverResult(); @@ -532,7 +550,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies } } - if (string.IsNullOrWhiteSpace(collectionType)) + if (string.IsNullOrEmpty(collectionType)) { return false; } diff --git a/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs b/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs index 48f5802a9..e3cce5f4b 100644 --- a/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs @@ -94,7 +94,8 @@ namespace Emby.Server.Implementations.Library.Resolvers "backdrop", "poster", "cover", - "logo" + "logo", + "default" }; internal static bool IsImageFile(string path, IImageProcessor imageProcessor) diff --git a/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs b/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs index 8c59cf20f..e66c9f087 100644 --- a/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs @@ -2,11 +2,20 @@ using MediaBrowser.Controller.Playlists; using System; using System.IO; +using MediaBrowser.Model.Extensions; +using MediaBrowser.Model.Entities; +using System.Linq; namespace Emby.Server.Implementations.Library.Resolvers { public class PlaylistResolver : FolderResolver<Playlist> { + private string[] SupportedCollectionTypes = new string[] { + + string.Empty, + CollectionType.Music + }; + /// <summary> /// Resolves the specified args. /// </summary> @@ -31,10 +40,26 @@ namespace Emby.Server.Implementations.Library.Resolvers return new Playlist { Path = args.Path, - Name = ResolverHelper.StripBrackets(Path.GetFileName(args.Path)) + Name = Path.GetFileName(args.Path).Replace("[playlist]", string.Empty, StringComparison.OrdinalIgnoreCase).Trim() }; } } + else + { + if (SupportedCollectionTypes.Contains(args.CollectionType ?? string.Empty, StringComparer.OrdinalIgnoreCase)) + { + var extension = Path.GetExtension(args.Path); + if (Playlist.SupportedExtensions.Contains(extension ?? string.Empty, StringComparer.OrdinalIgnoreCase)) + { + return new Playlist + { + Path = args.Path, + Name = Path.GetFileNameWithoutExtension(args.Path), + IsInMixedFolder = true + }; + } + } + } return null; } diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs index 3bad69b56..d8343f7c6 100644 --- a/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs @@ -50,24 +50,29 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV var path = args.Path; + var seasonParserResult = new SeasonPathParser(namingOptions).Parse(path, true, true); + var season = new Season { - IndexNumber = new SeasonPathParser(namingOptions, new RegexProvider()).Parse(path, true, true).SeasonNumber, + IndexNumber = seasonParserResult.SeasonNumber, SeriesId = series.Id, SeriesName = series.Name }; - if (season.IndexNumber.HasValue) + if (!season.IndexNumber.HasValue || !seasonParserResult.IsSeasonFolder) { var resolver = new Emby.Naming.TV.EpisodeResolver(namingOptions); - var episodeInfo = resolver.Resolve(path, true); + var folderName = System.IO.Path.GetFileName(path); + var testPath = "\\\\test\\" + folderName; + + var episodeInfo = resolver.Resolve(testPath, true); if (episodeInfo != null) { if (episodeInfo.EpisodeNumber.HasValue && episodeInfo.SeasonNumber.HasValue) { - _logger.Info("Found folder underneath series with episode number: {0}. Season {1}. Episode {2}", + _logger.Debug("Found folder underneath series with episode number: {0}. Season {1}. Episode {2}", path, episodeInfo.SeasonNumber.Value, episodeInfo.EpisodeNumber.Value); @@ -75,7 +80,10 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV return null; } } + } + if (season.IndexNumber.HasValue) + { var seasonNumber = season.IndexNumber.Value; season.Name = seasonNumber == 0 ? diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs index a693e3b26..951f439c2 100644 --- a/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs @@ -82,11 +82,11 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV }; } } - else if (string.IsNullOrWhiteSpace(collectionType)) + else if (string.IsNullOrEmpty(collectionType)) { if (args.ContainsFileSystemEntryByName("tvshow.nfo")) { - if (args.Parent.IsRoot) + if (args.Parent != null && args.Parent.IsRoot) { // For now, return null, but if we want to allow this in the future then add some additional checks to guard against a misplaced tvshow.nfo return null; @@ -99,7 +99,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV }; } - if (args.Parent.IsRoot) + if (args.Parent != null && args.Parent.IsRoot) { return null; } @@ -160,11 +160,19 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV return true; } - var allowOptimisticEpisodeDetection = isTvContentType; - var namingOptions = ((LibraryManager)libraryManager).GetNamingOptions(allowOptimisticEpisodeDetection); + var namingOptions = ((LibraryManager)libraryManager).GetNamingOptions(); var episodeResolver = new Emby.Naming.TV.EpisodeResolver(namingOptions); - var episodeInfo = episodeResolver.Resolve(fullName, false, false); + bool? isNamed = null; + bool? isOptimistic = null; + + if (!isTvContentType) + { + isNamed = true; + isOptimistic = false; + } + + var episodeInfo = episodeResolver.Resolve(fullName, false, isNamed, isOptimistic, null, false); if (episodeInfo != null && episodeInfo.EpisodeNumber.HasValue) { return true; @@ -206,7 +214,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV { var namingOptions = ((LibraryManager)libraryManager).GetNamingOptions(); - var seasonNumber = new SeasonPathParser(namingOptions, new RegexProvider()).Parse(path, isTvContentType, isTvContentType).SeasonNumber; + var seasonNumber = new SeasonPathParser(namingOptions).Parse(path, isTvContentType, isTvContentType).SeasonNumber; return seasonNumber.HasValue; } diff --git a/Emby.Server.Implementations/Library/SearchEngine.cs b/Emby.Server.Implementations/Library/SearchEngine.cs index 8021399bd..7f04ac5bc 100644 --- a/Emby.Server.Implementations/Library/SearchEngine.cs +++ b/Emby.Server.Implementations/Library/SearchEngine.cs @@ -28,14 +28,14 @@ namespace Emby.Server.Implementations.Library _libraryManager = libraryManager; _userManager = userManager; - _logger = logManager.GetLogger("Lucene"); + _logger = logManager.GetLogger("SearchEngine"); } - public async Task<QueryResult<SearchHintInfo>> GetSearchHints(SearchQuery query) + public QueryResult<SearchHintInfo> GetSearchHints(SearchQuery query) { User user = null; - if (string.IsNullOrWhiteSpace(query.UserId)) + if (query.UserId.Equals(Guid.Empty)) { } else @@ -43,26 +43,22 @@ namespace Emby.Server.Implementations.Library user = _userManager.GetUserById(query.UserId); } - var results = await GetSearchHints(query, user).ConfigureAwait(false); - - var searchResultArray = results.ToArray(); - results = searchResultArray; - - var count = searchResultArray.Length; + var results = GetSearchHints(query, user); + var totalRecordCount = results.Count; if (query.StartIndex.HasValue) { - results = results.Skip(query.StartIndex.Value); + results = results.Skip(query.StartIndex.Value).ToList(); } if (query.Limit.HasValue) { - results = results.Take(query.Limit.Value); + results = results.Take(query.Limit.Value).ToList(); } return new QueryResult<SearchHintInfo> { - TotalRecordCount = count, + TotalRecordCount = totalRecordCount, Items = results.ToArray() }; @@ -83,24 +79,19 @@ namespace Emby.Server.Implementations.Library /// <param name="user">The user.</param> /// <returns>IEnumerable{SearchHintResult}.</returns> /// <exception cref="System.ArgumentNullException">searchTerm</exception> - private Task<IEnumerable<SearchHintInfo>> GetSearchHints(SearchQuery query, User user) + private List<SearchHintInfo> GetSearchHints(SearchQuery query, User user) { var searchTerm = query.SearchTerm; - if (searchTerm != null) - { - searchTerm = searchTerm.Trim().RemoveDiacritics(); - } - - if (string.IsNullOrWhiteSpace(searchTerm)) + if (string.IsNullOrEmpty(searchTerm)) { throw new ArgumentNullException("searchTerm"); } - var terms = GetWords(searchTerm); + searchTerm = searchTerm.Trim().RemoveDiacritics(); var excludeItemTypes = query.ExcludeItemTypes.ToList(); - var includeItemTypes = (query.IncludeItemTypes ?? new string[] { }).ToList(); + var includeItemTypes = (query.IncludeItemTypes ?? Array.Empty<string>()).ToList(); excludeItemTypes.Add(typeof(Year).Name); excludeItemTypes.Add(typeof(Folder).Name); @@ -169,13 +160,13 @@ namespace Emby.Server.Implementations.Library var searchQuery = new InternalItemsQuery(user) { - NameContains = searchTerm, + SearchTerm = searchTerm, ExcludeItemTypes = excludeItemTypes.ToArray(excludeItemTypes.Count), IncludeItemTypes = includeItemTypes.ToArray(includeItemTypes.Count), Limit = query.Limit, - IncludeItemsByName = string.IsNullOrWhiteSpace(query.ParentId), - ParentId = string.IsNullOrWhiteSpace(query.ParentId) ? (Guid?)null : new Guid(query.ParentId), - OrderBy = new[] { new Tuple<string, SortOrder>(ItemSortBy.SortName, SortOrder.Ascending) }, + IncludeItemsByName = string.IsNullOrEmpty(query.ParentId), + ParentId = string.IsNullOrEmpty(query.ParentId) ? Guid.Empty : new Guid(query.ParentId), + OrderBy = new[] { new ValueTuple<string, SortOrder>(ItemSortBy.SortName, SortOrder.Ascending) }, Recursive = true, IsKids = query.IsKids, @@ -201,120 +192,25 @@ namespace Emby.Server.Implementations.Library if (searchQuery.IncludeItemTypes.Length == 1 && string.Equals(searchQuery.IncludeItemTypes[0], "MusicArtist", StringComparison.OrdinalIgnoreCase)) { - if (searchQuery.ParentId.HasValue) + if (!searchQuery.ParentId.Equals(Guid.Empty)) { - searchQuery.AncestorIds = new string[] { searchQuery.ParentId.Value.ToString("N") }; + searchQuery.AncestorIds = new[] { searchQuery.ParentId }; } - searchQuery.ParentId = null; + searchQuery.ParentId = Guid.Empty; searchQuery.IncludeItemsByName = true; - searchQuery.IncludeItemTypes = new string[] { }; - mediaItems = _libraryManager.GetArtists(searchQuery).Items.Select(i => i.Item1).ToList(); + searchQuery.IncludeItemTypes = Array.Empty<string>(); + mediaItems = _libraryManager.GetAllArtists(searchQuery).Items.Select(i => i.Item1).ToList(); } else { mediaItems = _libraryManager.GetItemList(searchQuery); } - var returnValue = mediaItems.Select(item => - { - var index = GetIndex(item.Name, searchTerm, terms); - - return new Tuple<BaseItem, string, int>(item, index.Item1, index.Item2); - - }).OrderBy(i => i.Item3).ThenBy(i => i.Item1.SortName).Select(i => new SearchHintInfo - { - Item = i.Item1, - MatchedTerm = i.Item2 - }); - - return Task.FromResult(returnValue); - } - - /// <summary> - /// Gets the index. - /// </summary> - /// <param name="input">The input.</param> - /// <param name="searchInput">The search input.</param> - /// <param name="searchWords">The search input.</param> - /// <returns>System.Int32.</returns> - private Tuple<string, int> GetIndex(string input, string searchInput, List<string> searchWords) - { - if (string.IsNullOrWhiteSpace(input)) - { - throw new ArgumentNullException("input"); - } - - input = input.RemoveDiacritics(); - - if (string.Equals(input, searchInput, StringComparison.OrdinalIgnoreCase)) - { - return new Tuple<string, int>(searchInput, 0); - } - - var index = input.IndexOf(searchInput, StringComparison.OrdinalIgnoreCase); - - if (index == 0) - { - return new Tuple<string, int>(searchInput, 1); - } - if (index > 0) - { - return new Tuple<string, int>(searchInput, 2); - } - - var items = GetWords(input); - - for (var i = 0; i < searchWords.Count; i++) + return mediaItems.Select(i => new SearchHintInfo { - var searchTerm = searchWords[i]; - - for (var j = 0; j < items.Count; j++) - { - var item = items[j]; - - if (string.Equals(item, searchTerm, StringComparison.OrdinalIgnoreCase)) - { - return new Tuple<string, int>(searchTerm, 3 + (i + 1) * (j + 1)); - } - - index = item.IndexOf(searchTerm, StringComparison.OrdinalIgnoreCase); - - if (index == 0) - { - return new Tuple<string, int>(searchTerm, 4 + (i + 1) * (j + 1)); - } - if (index > 0) - { - return new Tuple<string, int>(searchTerm, 5 + (i + 1) * (j + 1)); - } - } - } - return new Tuple<string, int>(null, -1); - } + Item = i - /// <summary> - /// Gets the words. - /// </summary> - /// <param name="term">The term.</param> - /// <returns>System.String[][].</returns> - private List<string> GetWords(string term) - { - var stoplist = GetStopList().ToList(); - - return term.Split() - .Where(i => !string.IsNullOrWhiteSpace(i) && !stoplist.Contains(i, StringComparer.OrdinalIgnoreCase)) - .ToList(); - } - - private IEnumerable<string> GetStopList() - { - return new[] - { - "the", - "a", - "of", - "an" - }; + }).ToList(); } } } diff --git a/Emby.Server.Implementations/Library/UserDataManager.cs b/Emby.Server.Implementations/Library/UserDataManager.cs index 7ef5ca35e..3714a7544 100644 --- a/Emby.Server.Implementations/Library/UserDataManager.cs +++ b/Emby.Server.Implementations/Library/UserDataManager.cs @@ -12,7 +12,8 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -using MediaBrowser.Model.Querying; +using MediaBrowser.Controller.Dto; +using System.Globalization; namespace Emby.Server.Implementations.Library { @@ -29,10 +30,13 @@ namespace Emby.Server.Implementations.Library private readonly ILogger _logger; private readonly IServerConfigurationManager _config; - public UserDataManager(ILogManager logManager, IServerConfigurationManager config) + private Func<IUserManager> _userManager; + + public UserDataManager(ILogManager logManager, IServerConfigurationManager config, Func<IUserManager> userManager) { _config = config; _logger = logManager.GetLogger(GetType().Name); + _userManager = userManager; } /// <summary> @@ -41,7 +45,14 @@ namespace Emby.Server.Implementations.Library /// <value>The repository.</value> public IUserDataRepository Repository { get; set; } - public void SaveUserData(Guid userId, IHasUserData item, UserItemData userData, UserDataSaveReason reason, CancellationToken cancellationToken) + public void SaveUserData(Guid userId, BaseItem item, UserItemData userData, UserDataSaveReason reason, CancellationToken cancellationToken) + { + var user = _userManager().GetUserById(userId); + + SaveUserData(user, item, userData, reason, cancellationToken); + } + + public void SaveUserData(User user, BaseItem item, UserItemData userData, UserDataSaveReason reason, CancellationToken cancellationToken) { if (userData == null) { @@ -51,15 +62,13 @@ namespace Emby.Server.Implementations.Library { throw new ArgumentNullException("item"); } - if (userId == Guid.Empty) - { - throw new ArgumentNullException("userId"); - } cancellationToken.ThrowIfCancellationRequested(); var keys = item.GetUserDataKeys(); + var userId = user.InternalId; + foreach (var key in keys) { Repository.SaveUserData(userId, key, userData, cancellationToken); @@ -73,7 +82,7 @@ namespace Emby.Server.Implementations.Library Keys = keys, UserData = userData, SaveReason = reason, - UserId = userId, + UserId = user.Id, Item = item }, _logger); @@ -88,18 +97,9 @@ namespace Emby.Server.Implementations.Library /// <returns></returns> public void SaveAllUserData(Guid userId, UserItemData[] userData, CancellationToken cancellationToken) { - if (userData == null) - { - throw new ArgumentNullException("userData"); - } - if (userId == Guid.Empty) - { - throw new ArgumentNullException("userId"); - } + var user = _userManager().GetUserById(userId); - cancellationToken.ThrowIfCancellationRequested(); - - Repository.SaveAllUserData(userId, userData, cancellationToken); + Repository.SaveAllUserData(user.InternalId, userData, cancellationToken); } /// <summary> @@ -109,37 +109,30 @@ namespace Emby.Server.Implementations.Library /// <returns></returns> public List<UserItemData> GetAllUserData(Guid userId) { - if (userId == Guid.Empty) - { - throw new ArgumentNullException("userId"); - } + var user = _userManager().GetUserById(userId); - return Repository.GetAllUserData(userId); + return Repository.GetAllUserData(user.InternalId); } public UserItemData GetUserData(Guid userId, Guid itemId, List<string> keys) { - if (userId == Guid.Empty) - { - throw new ArgumentNullException("userId"); - } - if (keys == null) - { - throw new ArgumentNullException("keys"); - } - if (keys.Count == 0) - { - throw new ArgumentException("UserData keys cannot be empty."); - } + var user = _userManager().GetUserById(userId); + + return GetUserData(user, itemId, keys); + } + + public UserItemData GetUserData(User user, Guid itemId, List<string> keys) + { + var userId = user.InternalId; var cacheKey = GetCacheKey(userId, itemId); return _userData.GetOrAdd(cacheKey, k => GetUserDataInternal(userId, keys)); } - private UserItemData GetUserDataInternal(Guid userId, List<string> keys) + private UserItemData GetUserDataInternal(long internalUserId, List<string> keys) { - var userData = Repository.GetUserData(userId, keys); + var userData = Repository.GetUserData(internalUserId, keys); if (userData != null) { @@ -150,7 +143,6 @@ namespace Emby.Server.Implementations.Library { return new UserItemData { - UserId = userId, Key = keys[0] }; } @@ -162,41 +154,41 @@ namespace Emby.Server.Implementations.Library /// Gets the internal key. /// </summary> /// <returns>System.String.</returns> - private string GetCacheKey(Guid userId, Guid itemId) + private string GetCacheKey(long internalUserId, Guid itemId) { - return userId.ToString("N") + itemId.ToString("N"); + return internalUserId.ToString(CultureInfo.InvariantCulture) + "-" + itemId.ToString("N"); } - public UserItemData GetUserData(IHasUserData user, IHasUserData item) + public UserItemData GetUserData(User user, BaseItem item) { - return GetUserData(user.Id, item); + return GetUserData(user, item.Id, item.GetUserDataKeys()); } - public UserItemData GetUserData(string userId, IHasUserData item) + public UserItemData GetUserData(string userId, BaseItem item) { return GetUserData(new Guid(userId), item); } - public UserItemData GetUserData(Guid userId, IHasUserData item) + public UserItemData GetUserData(Guid userId, BaseItem item) { return GetUserData(userId, item.Id, item.GetUserDataKeys()); } - public UserItemDataDto GetUserDataDto(IHasUserData item, User user) + public UserItemDataDto GetUserDataDto(BaseItem item, User user) { - var userData = GetUserData(user.Id, item); + var userData = GetUserData(user, item); var dto = GetUserItemDataDto(userData); - item.FillUserDataDtoValues(dto, userData, null, user, new ItemFields[] { }); + item.FillUserDataDtoValues(dto, userData, null, user, new DtoOptions()); return dto; } - public UserItemDataDto GetUserDataDto(IHasUserData item, BaseItemDto itemDto, User user, ItemFields[] fields) + public UserItemDataDto GetUserDataDto(BaseItem item, BaseItemDto itemDto, User user, DtoOptions options) { - var userData = GetUserData(user.Id, item); + var userData = GetUserData(user, item); var dto = GetUserItemDataDto(userData); - item.FillUserDataDtoValues(dto, userData, itemDto, user, fields); + item.FillUserDataDtoValues(dto, userData, itemDto, user, options); return dto; } @@ -230,13 +222,15 @@ namespace Emby.Server.Implementations.Library { var playedToCompletion = false; - var positionTicks = reportedPositionTicks ?? item.RunTimeTicks ?? 0; - var hasRuntime = item.RunTimeTicks.HasValue && item.RunTimeTicks > 0; + var runtimeTicks = item.GetRunTimeTicksForPlayState(); + + var positionTicks = reportedPositionTicks ?? runtimeTicks; + var hasRuntime = runtimeTicks > 0; // If a position has been reported, and if we know the duration if (positionTicks > 0 && hasRuntime) { - var pctIn = Decimal.Divide(positionTicks, item.RunTimeTicks.Value) * 100; + var pctIn = Decimal.Divide(positionTicks, runtimeTicks) * 100; // Don't track in very beginning if (pctIn < _config.Configuration.MinResumePct) @@ -245,7 +239,7 @@ namespace Emby.Server.Implementations.Library } // If we're at the end, assume completed - else if (pctIn > _config.Configuration.MaxResumePct || positionTicks >= item.RunTimeTicks.Value) + else if (pctIn > _config.Configuration.MaxResumePct || positionTicks >= runtimeTicks) { positionTicks = 0; data.Played = playedToCompletion = true; @@ -254,7 +248,7 @@ namespace Emby.Server.Implementations.Library else { // Enforce MinResumeDuration - var durationSeconds = TimeSpan.FromTicks(item.RunTimeTicks.Value).TotalSeconds; + var durationSeconds = TimeSpan.FromTicks(runtimeTicks).TotalSeconds; if (durationSeconds < _config.Configuration.MinResumeDurationSeconds) { diff --git a/Emby.Server.Implementations/Library/UserManager.cs b/Emby.Server.Implementations/Library/UserManager.cs index 71c953b2c..b13a255aa 100644 --- a/Emby.Server.Implementations/Library/UserManager.cs +++ b/Emby.Server.Implementations/Library/UserManager.cs @@ -28,6 +28,11 @@ using System.Threading; using System.Threading.Tasks; using MediaBrowser.Model.Cryptography; using MediaBrowser.Model.IO; +using MediaBrowser.Controller.Authentication; +using MediaBrowser.Controller.Security; +using MediaBrowser.Controller.Devices; +using MediaBrowser.Controller.Session; +using MediaBrowser.Controller.Plugins; namespace Emby.Server.Implementations.Library { @@ -40,7 +45,9 @@ namespace Emby.Server.Implementations.Library /// Gets the users. /// </summary> /// <value>The users.</value> - public IEnumerable<User> Users { get; private set; } + public IEnumerable<User> Users { get { return _users; } } + + private User[] _users; /// <summary> /// The _logger @@ -72,6 +79,9 @@ namespace Emby.Server.Implementations.Library private readonly IFileSystem _fileSystem; private readonly ICryptoProvider _cryptographyProvider; + private IAuthenticationProvider[] _authenticationProviders; + private DefaultAuthenticationProvider _defaultAuthenticationProvider; + public UserManager(ILogger logger, IServerConfigurationManager configurationManager, IUserRepository userRepository, IXmlSerializer xmlSerializer, INetworkManager networkManager, Func<IImageProcessor> imageProcessorFactory, Func<IDtoService> dtoServiceFactory, Func<IConnectManager> connectFactory, IServerApplicationHost appHost, IJsonSerializer jsonSerializer, IFileSystem fileSystem, ICryptoProvider cryptographyProvider) { _logger = logger; @@ -86,16 +96,38 @@ namespace Emby.Server.Implementations.Library _fileSystem = fileSystem; _cryptographyProvider = cryptographyProvider; ConfigurationManager = configurationManager; - Users = new List<User>(); + _users = Array.Empty<User>(); DeletePinFile(); } + public NameIdPair[] GetAuthenticationProviders() + { + return _authenticationProviders + .Where(i => i.IsEnabled) + .OrderBy(i => i is DefaultAuthenticationProvider ? 0 : 1) + .ThenBy(i => i.Name) + .Select(i => new NameIdPair + { + Name = i.Name, + Id = GetAuthenticationProviderId(i) + }) + .ToArray(); + } + + public void AddParts(IEnumerable<IAuthenticationProvider> authenticationProviders) + { + _authenticationProviders = authenticationProviders.ToArray(); + + _defaultAuthenticationProvider = _authenticationProviders.OfType<DefaultAuthenticationProvider>().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; @@ -132,7 +164,7 @@ namespace Emby.Server.Implementations.Library /// <exception cref="System.ArgumentNullException"></exception> public User GetUserById(Guid id) { - if (id == Guid.Empty) + if (id.Equals(Guid.Empty)) { throw new ArgumentNullException("id"); } @@ -162,7 +194,7 @@ namespace Emby.Server.Implementations.Library public void Initialize() { - Users = LoadUsers(); + _users = LoadUsers(); var users = Users.ToList(); @@ -218,7 +250,7 @@ namespace Emby.Server.Implementations.Library return builder.ToString(); } - public async Task<User> AuthenticateUser(string username, string password, string hashedPassword, string passwordMd5, string remoteEndPoint, bool isUserSession) + public async Task<User> AuthenticateUser(string username, string password, string hashedPassword, string remoteEndPoint, bool isUserSession) { if (string.IsNullOrWhiteSpace(username)) { @@ -229,18 +261,16 @@ namespace Emby.Server.Implementations.Library .FirstOrDefault(i => string.Equals(username, i.Name, StringComparison.OrdinalIgnoreCase)); var success = false; + IAuthenticationProvider authenticationProvider = null; if (user != null) { - if (password != null) - { - hashedPassword = GetHashedString(user, password); - } - // Authenticate using local credentials if not a guest if (!user.ConnectLinkType.HasValue || user.ConnectLinkType.Value != UserLinkType.Guest) { - success = AuthenticateLocalUser(user, password, hashedPassword, remoteEndPoint); + var authResult = await AuthenticateLocalUser(username, password, hashedPassword, user, remoteEndPoint).ConfigureAwait(false); + authenticationProvider = authResult.Item1; + success = authResult.Item2; } // Maybe user accidently entered connect credentials. let's be flexible @@ -248,7 +278,7 @@ namespace Emby.Server.Implementations.Library { try { - await _connectFactory().Authenticate(user.ConnectUserName, password, passwordMd5).ConfigureAwait(false); + await _connectFactory().Authenticate(user.ConnectUserName, password).ConfigureAwait(false); success = true; } catch @@ -257,13 +287,43 @@ namespace Emby.Server.Implementations.Library } } } + else + { + // user is null + var authResult = await AuthenticateLocalUser(username, password, hashedPassword, null, remoteEndPoint).ConfigureAwait(false); + authenticationProvider = authResult.Item1; + success = authResult.Item2; + + if (success && authenticationProvider != null && !(authenticationProvider is DefaultAuthenticationProvider)) + { + user = await CreateUser(username).ConfigureAwait(false); + + var hasNewUserPolicy = authenticationProvider as IHasNewUserPolicy; + if (hasNewUserPolicy != null) + { + var policy = hasNewUserPolicy.GetNewUserPolicy(); + UpdateUserPolicy(user, policy, true); + } + } + } + + if (success && user != null && authenticationProvider != null) + { + var providerId = GetAuthenticationProviderId(authenticationProvider); + + if (!string.Equals(providerId, user.Policy.AuthenticationProviderId, StringComparison.OrdinalIgnoreCase)) + { + user.Policy.AuthenticationProviderId = providerId; + UpdateUserPolicy(user, user.Policy, true); + } + } // Try originally entered username if (!success && (user == null || !string.Equals(user.ConnectUserName, username, StringComparison.OrdinalIgnoreCase))) { try { - var connectAuthResult = await _connectFactory().Authenticate(username, password, passwordMd5).ConfigureAwait(false); + var connectAuthResult = await _connectFactory().Authenticate(username, password).ConfigureAwait(false); user = Users.FirstOrDefault(i => string.Equals(i.ConnectUserId, connectAuthResult.User.Id, StringComparison.OrdinalIgnoreCase)); @@ -285,6 +345,19 @@ namespace Emby.Server.Implementations.Library throw new SecurityException(string.Format("The {0} account is currently disabled. Please consult with your administrator.", user.Name)); } + if (user != null) + { + if (!user.Policy.EnableRemoteAccess && !_networkManager.IsInLocalNetwork(remoteEndPoint)) + { + throw new SecurityException("Forbidden."); + } + + if (!user.IsParentalScheduleAllowed()) + { + throw new SecurityException("User is not allowed access at this time."); + } + } + // Update LastActivityDate and LastLoginDate, then save if (success) { @@ -305,34 +378,106 @@ namespace Emby.Server.Implementations.Library return success ? user : null; } - private bool AuthenticateLocalUser(User user, string password, string hashedPassword, string remoteEndPoint) + private string GetAuthenticationProviderId(IAuthenticationProvider provider) { - bool success; + return provider.GetType().FullName; + } - if (password == null) + private IAuthenticationProvider GetAuthenticationProvider(User user) + { + return GetAuthenticationProviders(user).First(); + } + + private IAuthenticationProvider[] GetAuthenticationProviders(User user) + { + var authenticationProviderId = user == null ? null : user.Policy.AuthenticationProviderId; + + var providers = _authenticationProviders.Where(i => i.IsEnabled).ToArray(); + + if (!string.IsNullOrEmpty(authenticationProviderId)) { - // legacy - success = string.Equals(GetPasswordHash(user), hashedPassword.Replace("-", string.Empty), StringComparison.OrdinalIgnoreCase); + providers = providers.Where(i => string.Equals(authenticationProviderId, GetAuthenticationProviderId(i), StringComparison.OrdinalIgnoreCase)).ToArray(); } - else + + if (providers.Length == 0) { - success = string.Equals(GetPasswordHash(user), GetHashedString(user, password), StringComparison.OrdinalIgnoreCase); + providers = new IAuthenticationProvider[] { _defaultAuthenticationProvider }; } - if (!success && _networkManager.IsInLocalNetwork(remoteEndPoint) && user.Configuration.EnableLocalPassword) + return providers; + } + + private async Task<bool> AuthenticateWithProvider(IAuthenticationProvider provider, string username, string password, User resolvedUser) + { + try { - if (password == null) + var requiresResolvedUser = provider as IRequiresResolvedUser; + if (requiresResolvedUser != null) { - // legacy - success = string.Equals(GetLocalPasswordHash(user), hashedPassword.Replace("-", string.Empty), StringComparison.OrdinalIgnoreCase); + await requiresResolvedUser.Authenticate(username, password, resolvedUser).ConfigureAwait(false); } else { - success = string.Equals(GetLocalPasswordHash(user), GetHashedString(user, password), StringComparison.OrdinalIgnoreCase); + await provider.Authenticate(username, password).ConfigureAwait(false); + } + + return true; + } + catch (Exception ex) + { + _logger.ErrorException("Error authenticating with provider {0}", ex, provider.Name); + + return false; + } + } + + private async Task<Tuple<IAuthenticationProvider, bool>> AuthenticateLocalUser(string username, string password, string hashedPassword, User user, string remoteEndPoint) + { + bool success = false; + IAuthenticationProvider authenticationProvider = null; + + if (password != null && user != null) + { + // Doesn't look like this is even possible to be used, because of password == null checks below + hashedPassword = _defaultAuthenticationProvider.GetHashedString(user, password); + } + + if (password == null) + { + // legacy + success = string.Equals(_defaultAuthenticationProvider.GetPasswordHash(user), hashedPassword.Replace("-", string.Empty), StringComparison.OrdinalIgnoreCase); + } + else + { + foreach (var provider in GetAuthenticationProviders(user)) + { + success = await AuthenticateWithProvider(provider, username, password, user).ConfigureAwait(false); + + if (success) + { + authenticationProvider = provider; + break; + } } } - return success; + if (user != null) + { + if (!success && _networkManager.IsInLocalNetwork(remoteEndPoint) && user.Configuration.EnableLocalPassword) + { + if (password == null) + { + // 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 new Tuple<IAuthenticationProvider, bool>(authenticationProvider, success); } private void UpdateInvalidLoginAttemptCount(User user, int newValue) @@ -367,63 +512,41 @@ namespace Emby.Server.Implementations.Library } } - private string GetPasswordHash(User user) - { - return string.IsNullOrEmpty(user.Password) - ? GetEmptyHashedString(user) - : user.Password; - } - private string GetLocalPasswordHash(User user) { return string.IsNullOrEmpty(user.EasyPassword) - ? GetEmptyHashedString(user) + ? _defaultAuthenticationProvider.GetEmptyHashedString(user) : user.EasyPassword; } private bool IsPasswordEmpty(User user, string passwordHash) { - return string.Equals(passwordHash, GetEmptyHashedString(user), StringComparison.OrdinalIgnoreCase); - } - - private string GetEmptyHashedString(User user) - { - return GetHashedString(user, string.Empty); - } - - /// <summary> - /// Gets the hashed string. - /// </summary> - private string GetHashedString(User user, string str) - { - var salt = user.Salt; - if (salt != null) - { - // return BCrypt.HashPassword(str, salt); - } - - // legacy - return BitConverter.ToString(_cryptographyProvider.ComputeSHA1(Encoding.UTF8.GetBytes(str))).Replace("-", string.Empty); + return string.Equals(passwordHash, _defaultAuthenticationProvider.GetEmptyHashedString(user), StringComparison.OrdinalIgnoreCase); } /// <summary> /// Loads the users from the repository /// </summary> /// <returns>IEnumerable{User}.</returns> - private List<User> LoadUsers() + private User[] LoadUsers() { - var users = UserRepository.RetrieveAllUsers().ToList(); + var users = UserRepository.RetrieveAllUsers(); // There always has to be at least one user. if (users.Count == 0) { - var name = MakeValidUsername(Environment.UserName); + var defaultName = Environment.UserName; + if (string.IsNullOrWhiteSpace(defaultName)) + { + defaultName = "MyEmbyUser"; + } + var name = MakeValidUsername(defaultName); var user = InstantiateNewUser(name); user.DateLastSaved = DateTime.UtcNow; - UserRepository.SaveUser(user, CancellationToken.None); + UserRepository.CreateUser(user); users.Add(user); @@ -433,7 +556,7 @@ namespace Emby.Server.Implementations.Library UpdateUserPolicy(user, user.Policy, false); } - return users; + return users.ToArray(); } public UserDto GetUserDto(User user, string remoteEndPoint = null) @@ -443,9 +566,7 @@ namespace Emby.Server.Implementations.Library throw new ArgumentNullException("user"); } - var passwordHash = GetPasswordHash(user); - - var hasConfiguredPassword = !IsPasswordEmpty(user, passwordHash); + var hasConfiguredPassword = GetAuthenticationProvider(user).HasPassword(user).Result; var hasConfiguredEasyPassword = !IsPasswordEmpty(user, GetLocalPasswordHash(user)); var hasPassword = user.Configuration.EnableLocalPassword && !string.IsNullOrEmpty(remoteEndPoint) && _networkManager.IsInLocalNetwork(remoteEndPoint) ? @@ -454,7 +575,7 @@ namespace Emby.Server.Implementations.Library var dto = new UserDto { - Id = user.Id.ToString("N"), + Id = user.Id, Name = user.Name, HasPassword = hasPassword, HasConfiguredPassword = hasConfiguredPassword, @@ -577,7 +698,7 @@ namespace Emby.Server.Implementations.Library throw new ArgumentNullException("user"); } - if (user.Id == Guid.Empty || !Users.Any(u => u.Id.Equals(user.Id))) + if (user.Id.Equals(Guid.Empty) || !Users.Any(u => u.Id.Equals(user.Id))) { throw new ArgumentException(string.Format("User with name '{0}' and Id {1} does not exist.", user.Name, user.Id)); } @@ -585,7 +706,7 @@ namespace Emby.Server.Implementations.Library user.DateModified = DateTime.UtcNow; user.DateLastSaved = DateTime.UtcNow; - UserRepository.SaveUser(user, CancellationToken.None); + UserRepository.UpdateUser(user); OnUserUpdated(user); } @@ -626,11 +747,11 @@ namespace Emby.Server.Implementations.Library var list = Users.ToList(); list.Add(user); - Users = list; + _users = list.ToArray(); user.DateLastSaved = DateTime.UtcNow; - UserRepository.SaveUser(user, CancellationToken.None); + UserRepository.CreateUser(user); EventHelper.QueueEventIfNotNull(UserCreated, this, new GenericEventArgs<User> { Argument = user }, _logger); @@ -658,7 +779,7 @@ namespace Emby.Server.Implementations.Library if (user.ConnectLinkType.HasValue) { - await _connectFactory().RemoveConnect(user.Id.ToString("N")).ConfigureAwait(false); + await _connectFactory().RemoveConnect(user).ConfigureAwait(false); } var allUsers = Users.ToList(); @@ -684,7 +805,7 @@ namespace Emby.Server.Implementations.Library { var configPath = GetConfigurationFilePath(user); - UserRepository.DeleteUser(user, CancellationToken.None); + UserRepository.DeleteUser(user); try { @@ -697,7 +818,7 @@ namespace Emby.Server.Implementations.Library DeleteUserPolicy(user); - Users = allUsers.Where(i => i.Id != user.Id).ToList(); + _users = allUsers.Where(i => i.Id != user.Id).ToArray(); OnUserDeleted(user); } @@ -711,9 +832,9 @@ namespace Emby.Server.Implementations.Library /// Resets the password by clearing it. /// </summary> /// <returns>Task.</returns> - public void ResetPassword(User user) + public Task ResetPassword(User user) { - ChangePassword(user, string.Empty, null); + return ChangePassword(user, string.Empty); } public void ResetEasyPassword(User user) @@ -721,29 +842,19 @@ namespace Emby.Server.Implementations.Library ChangeEasyPassword(user, string.Empty, null); } - public void ChangePassword(User user, string newPassword, string newPasswordHash) + public async Task ChangePassword(User user, string newPassword) { if (user == null) { throw new ArgumentNullException("user"); } - if (newPassword != null) - { - newPasswordHash = GetHashedString(user, newPassword); - } - - if (string.IsNullOrWhiteSpace(newPasswordHash)) - { - throw new ArgumentNullException("newPasswordHash"); - } - if (user.ConnectLinkType.HasValue && user.ConnectLinkType.Value == UserLinkType.Guest) { throw new ArgumentException("Passwords for guests cannot be changed."); } - user.Password = newPasswordHash; + await GetAuthenticationProvider(user).ChangePassword(user, newPassword).ConfigureAwait(false); UpdateUser(user); @@ -759,7 +870,7 @@ namespace Emby.Server.Implementations.Library if (newPassword != null) { - newPasswordHash = GetHashedString(user, newPassword); + newPasswordHash = _defaultAuthenticationProvider.GetHashedString(user, newPassword); } if (string.IsNullOrWhiteSpace(newPasswordHash)) @@ -801,7 +912,7 @@ namespace Emby.Server.Implementations.Library private PasswordPinCreationResult _lastPasswordPinCreationResult; private int _pinAttempts; - private PasswordPinCreationResult CreatePasswordResetPin() + private async Task<PasswordPinCreationResult> CreatePasswordResetPin() { var num = new Random().Next(1, 9999); @@ -815,7 +926,7 @@ namespace Emby.Server.Implementations.Library var text = new StringBuilder(); - var localAddress = _appHost.GetLocalApiUrl(CancellationToken.None).Result ?? string.Empty; + var localAddress = (await _appHost.GetLocalApiUrl(CancellationToken.None).ConfigureAwait(false)) ?? string.Empty; text.AppendLine("Use your web browser to visit:"); text.AppendLine(string.Empty); @@ -844,7 +955,7 @@ namespace Emby.Server.Implementations.Library return result; } - public ForgotPasswordResult StartForgotPasswordProcess(string enteredUsername, bool isInNetwork) + public async Task<ForgotPasswordResult> StartForgotPasswordProcess(string enteredUsername, bool isInNetwork) { DeletePinFile(); @@ -872,7 +983,7 @@ namespace Emby.Server.Implementations.Library action = ForgotPasswordAction.PinCode; } - var result = CreatePasswordResetPin(); + var result = await CreatePasswordResetPin().ConfigureAwait(false); pinFile = result.PinFile; expirationDate = result.ExpirationDate; } @@ -885,7 +996,7 @@ namespace Emby.Server.Implementations.Library }; } - public PinRedeemResult RedeemPasswordResetPin(string pin) + public async Task<PinRedeemResult> RedeemPasswordResetPin(string pin) { DeletePinFile(); @@ -906,7 +1017,7 @@ namespace Emby.Server.Implementations.Library foreach (var user in users) { - ResetPassword(user); + await ResetPassword(user).ConfigureAwait(false); if (user.Policy.IsDisabled) { @@ -953,7 +1064,7 @@ namespace Emby.Server.Implementations.Library public UserPolicy GetUserPolicy(User user) { - var path = GetPolifyFilePath(user); + var path = GetPolicyFilePath(user); try { @@ -988,7 +1099,7 @@ namespace Emby.Server.Implementations.Library } private readonly object _policySyncLock = new object(); - public void UpdateUserPolicy(string userId, UserPolicy userPolicy) + public void UpdateUserPolicy(Guid userId, UserPolicy userPolicy) { var user = GetUserById(userId); UpdateUserPolicy(user, userPolicy, true); @@ -1003,7 +1114,7 @@ namespace Emby.Server.Implementations.Library userPolicy = _jsonSerializer.DeserializeFromString<UserPolicy>(json); } - var path = GetPolifyFilePath(user); + var path = GetPolicyFilePath(user); _fileSystem.CreateDirectory(_fileSystem.GetDirectoryName(path)); @@ -1013,12 +1124,15 @@ namespace Emby.Server.Implementations.Library user.Policy = userPolicy; } - UpdateConfiguration(user, user.Configuration, true); + if (fireEvent) + { + EventHelper.FireEventIfNotNull(UserPolicyUpdated, this, new GenericEventArgs<User> { Argument = user }, _logger); + } } private void DeleteUserPolicy(User user) { - var path = GetPolifyFilePath(user); + var path = GetPolicyFilePath(user); try { @@ -1037,7 +1151,7 @@ namespace Emby.Server.Implementations.Library } } - private string GetPolifyFilePath(User user) + private string GetPolicyFilePath(User user) { return Path.Combine(user.ConfigurationDirectoryPath, "policy.xml"); } @@ -1075,9 +1189,14 @@ namespace Emby.Server.Implementations.Library } private readonly object _configSyncLock = new object(); - public void UpdateConfiguration(string userId, UserConfiguration config) + public void UpdateConfiguration(Guid userId, UserConfiguration config) { var user = GetUserById(userId); + UpdateConfiguration(user, config); + } + + public void UpdateConfiguration(User user, UserConfiguration config) + { UpdateConfiguration(user, config, true); } @@ -1106,4 +1225,56 @@ namespace Emby.Server.Implementations.Library } } } + + public class DeviceAccessEntryPoint : IServerEntryPoint + { + private IUserManager _userManager; + private IAuthenticationRepository _authRepo; + private IDeviceManager _deviceManager; + private ISessionManager _sessionManager; + + public DeviceAccessEntryPoint(IUserManager userManager, IAuthenticationRepository authRepo, IDeviceManager deviceManager, ISessionManager sessionManager) + { + _userManager = userManager; + _authRepo = authRepo; + _deviceManager = deviceManager; + _sessionManager = sessionManager; + } + + public void Run() + { + _userManager.UserPolicyUpdated += _userManager_UserPolicyUpdated; + } + + private void _userManager_UserPolicyUpdated(object sender, GenericEventArgs<User> e) + { + var user = e.Argument; + if (!user.Policy.EnableAllDevices) + { + UpdateDeviceAccess(user); + } + } + + private void UpdateDeviceAccess(User user) + { + var existing = _authRepo.Get(new AuthenticationInfoQuery + { + UserId = user.Id + + }).Items; + + foreach (var authInfo in existing) + { + if (!string.IsNullOrEmpty(authInfo.DeviceId) && !_deviceManager.CanAccessDevice(user, authInfo.DeviceId)) + { + _sessionManager.Logout(authInfo); + } + } + } + + public void Dispose() + { + + } + } }
\ No newline at end of file diff --git a/Emby.Server.Implementations/Library/UserViewManager.cs b/Emby.Server.Implementations/Library/UserViewManager.cs index e97bf11c3..42f922710 100644 --- a/Emby.Server.Implementations/Library/UserViewManager.cs +++ b/Emby.Server.Implementations/Library/UserViewManager.cs @@ -39,24 +39,15 @@ namespace Emby.Server.Implementations.Library _config = config; } - public async Task<Folder[]> GetUserViews(UserViewQuery query, CancellationToken cancellationToken) + public Folder[] GetUserViews(UserViewQuery query) { var user = _userManager.GetUserById(query.UserId); - var folders = user.RootFolder + var folders = _libraryManager.GetUserRootFolder() .GetChildren(user, true) .OfType<Folder>() .ToList(); - if (!query.IncludeHidden) - { - folders = folders.Where(i => - { - var hidden = i as IHiddenFromDisplay; - return hidden == null || !hidden.IsHiddenFromUser(user); - }).ToList(); - } - var groupedFolders = new List<ICollectionFolder>(); var list = new List<Folder>(); @@ -68,7 +59,7 @@ namespace Emby.Server.Implementations.Library if (UserView.IsUserSpecific(folder)) { - list.Add(_libraryManager.GetNamedView(user, folder.Name, folder.Id.ToString("N"), folderViewType, null, cancellationToken)); + list.Add(_libraryManager.GetNamedView(user, folder.Name, folder.Id, folderViewType, null)); continue; } @@ -80,7 +71,7 @@ namespace Emby.Server.Implementations.Library if (query.PresetViews.Contains(folderViewType ?? string.Empty, StringComparer.OrdinalIgnoreCase)) { - list.Add(GetUserView(folder, folderViewType, string.Empty, cancellationToken)); + list.Add(GetUserView(folder, folderViewType, string.Empty)); } else { @@ -90,7 +81,7 @@ namespace Emby.Server.Implementations.Library foreach (var viewType in new[] { CollectionType.Movies, CollectionType.TvShows }) { - var parents = groupedFolders.Where(i => string.Equals(i.CollectionType, viewType, StringComparison.OrdinalIgnoreCase) || string.IsNullOrWhiteSpace(i.CollectionType)) + var parents = groupedFolders.Where(i => string.Equals(i.CollectionType, viewType, StringComparison.OrdinalIgnoreCase) || string.IsNullOrEmpty(i.CollectionType)) .ToList(); if (parents.Count > 0) @@ -99,41 +90,38 @@ namespace Emby.Server.Implementations.Library "TvShows" : "Movies"; - list.Add(GetUserView(parents, viewType, localizationKey, string.Empty, user, query.PresetViews, cancellationToken)); + list.Add(GetUserView(parents, viewType, localizationKey, string.Empty, user, query.PresetViews)); } } if (_config.Configuration.EnableFolderView) { var name = _localizationManager.GetLocalizedString("Folders"); - list.Add(_libraryManager.GetNamedView(name, CollectionType.Folders, string.Empty, cancellationToken)); + list.Add(_libraryManager.GetNamedView(name, CollectionType.Folders, string.Empty)); } if (query.IncludeExternalContent) { - var channelResult = await _channelManager.GetChannelsInternal(new ChannelQuery + var channelResult = _channelManager.GetChannelsInternal(new ChannelQuery { UserId = query.UserId - - }, cancellationToken).ConfigureAwait(false); + }); var channels = channelResult.Items; - if (_config.Configuration.EnableChannelView && channels.Length > 0) - { - list.Add(_channelManager.GetInternalChannelFolder(cancellationToken)); - } - else - { - list.AddRange(channels); - } + list.AddRange(channels); - if (_liveTvManager.GetEnabledUsers().Select(i => i.Id.ToString("N")).Contains(query.UserId)) + if (_liveTvManager.GetEnabledUsers().Select(i => i.Id).Contains(query.UserId)) { list.Add(_liveTvManager.GetInternalLiveTvFolder(CancellationToken.None)); } } + if (!query.IncludeHidden) + { + list = list.Where(i => !user.Configuration.MyMediaExcludes.Contains(i.Id.ToString("N"))).ToList(); + } + var sorted = _libraryManager.Sort(list, user, new[] { ItemSortBy.SortName }, SortOrder.Ascending).ToList(); var orders = user.Configuration.OrderedViews.ToList(); @@ -148,7 +136,7 @@ namespace Emby.Server.Implementations.Library var view = i as UserView; if (view != null) { - if (view.DisplayParentId != Guid.Empty) + if (!view.DisplayParentId.Equals(Guid.Empty)) { index = orders.IndexOf(view.DisplayParentId.ToString("N")); } @@ -162,21 +150,21 @@ namespace Emby.Server.Implementations.Library .ToArray(); } - public UserView GetUserSubViewWithName(string name, string parentId, string type, string sortName, CancellationToken cancellationToken) + public UserView GetUserSubViewWithName(string name, Guid parentId, string type, string sortName) { var uniqueId = parentId + "subview" + type; - return _libraryManager.GetNamedView(name, parentId, type, sortName, uniqueId, cancellationToken); + return _libraryManager.GetNamedView(name, parentId, type, sortName, uniqueId); } - public UserView GetUserSubView(string parentId, string type, string localizationKey, string sortName, CancellationToken cancellationToken) + public UserView GetUserSubView(Guid parentId, string type, string localizationKey, string sortName) { var name = _localizationManager.GetLocalizedString(localizationKey); - return GetUserSubViewWithName(name, parentId, type, sortName, cancellationToken); + return GetUserSubViewWithName(name, parentId, type, sortName); } - private Folder GetUserView(List<ICollectionFolder> parents, string viewType, string localizationKey, string sortName, User user, string[] presetViews, CancellationToken cancellationToken) + private Folder GetUserView(List<ICollectionFolder> parents, string viewType, string localizationKey, string sortName, User user, string[] presetViews) { if (parents.Count == 1 && parents.All(i => string.Equals(i.CollectionType, viewType, StringComparison.OrdinalIgnoreCase))) { @@ -185,16 +173,16 @@ namespace Emby.Server.Implementations.Library return (Folder)parents[0]; } - return GetUserView((Folder)parents[0], viewType, string.Empty, cancellationToken); + return GetUserView((Folder)parents[0], viewType, string.Empty); } var name = _localizationManager.GetLocalizedString(localizationKey); - return _libraryManager.GetNamedView(user, name, viewType, sortName, cancellationToken); + return _libraryManager.GetNamedView(user, name, viewType, sortName); } - public UserView GetUserView(Folder parent, string viewType, string sortName, CancellationToken cancellationToken) + public UserView GetUserView(Folder parent, string viewType, string sortName) { - return _libraryManager.GetShadowView(parent, viewType, sortName, cancellationToken); + return _libraryManager.GetShadowView(parent, viewType, sortName); } public List<Tuple<BaseItem, List<BaseItem>>> GetLatestItems(LatestItemsQuery request, DtoOptions options) @@ -246,9 +234,26 @@ namespace Emby.Server.Implementations.Library var parents = new List<BaseItem>(); - if (!string.IsNullOrWhiteSpace(parentId)) + if (!parentId.Equals(Guid.Empty)) { - var parent = _libraryManager.GetItemById(parentId) as Folder; + var parentItem = _libraryManager.GetItemById(parentId); + var parentItemChannel = parentItem as Channel; + if (parentItemChannel != null) + { + return _channelManager.GetLatestChannelItemsInternal(new InternalItemsQuery(user) + { + ChannelIds = new [] { parentId }, + IsPlayed = request.IsPlayed, + StartIndex = request.StartIndex, + Limit = request.Limit, + IncludeItemTypes = request.IncludeItemTypes, + EnableTotalRecordCount = false + + + }, CancellationToken.None).Result.Items.ToList(); + } + + var parent = parentItem as Folder; if (parent != null) { parents.Add(parent); @@ -264,7 +269,7 @@ namespace Emby.Server.Implementations.Library if (parents.Count == 0) { - parents = user.RootFolder.GetChildren(user, true) + parents = _libraryManager.GetUserRootFolder().GetChildren(user, true) .Where(i => i is Folder) .Where(i => !user.Configuration.LatestItemsExcludes.Contains(i.Id.ToString("N"))) .ToList(); @@ -275,6 +280,24 @@ namespace Emby.Server.Implementations.Library return new List<BaseItem>(); } + if (includeItemTypes.Length == 0) + { + // Handle situations with the grouping setting, e.g. movies showing up in tv, etc. + // Thanks to mixed content libraries included in the UserView + var hasCollectionType = parents.OfType<UserView>().ToArray(); + if (hasCollectionType.Length > 0) + { + if (hasCollectionType.All(i => string.Equals(i.CollectionType, CollectionType.Movies, StringComparison.OrdinalIgnoreCase))) + { + includeItemTypes = new string[] { "Movie" }; + } + else if (hasCollectionType.All(i => string.Equals(i.CollectionType, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase))) + { + includeItemTypes = new string[] { "Episode" }; + } + } + } + var mediaTypes = new List<string>(); if (includeItemTypes.Length == 0) @@ -285,6 +308,7 @@ namespace Emby.Server.Implementations.Library { case CollectionType.Books: mediaTypes.Add(MediaType.Book); + mediaTypes.Add(MediaType.Audio); break; case CollectionType.Games: mediaTypes.Add(MediaType.Game); @@ -318,12 +342,12 @@ namespace Emby.Server.Implementations.Library typeof(MusicGenre).Name, typeof(Genre).Name - } : new string[] { }; + } : Array.Empty<string>(); var query = new InternalItemsQuery(user) { IncludeItemTypes = includeItemTypes, - OrderBy = new[] { new Tuple<string, SortOrder>(ItemSortBy.DateCreated, SortOrder.Descending) }, + OrderBy = new[] { new ValueTuple<string, SortOrder>(ItemSortBy.DateCreated, SortOrder.Descending) }, IsFolder = includeItemTypes.Length == 0 ? false : (bool?)null, ExcludeItemTypes = excludeItemTypes, IsVirtualItem = false, diff --git a/Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs b/Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs index 1a53ad672..cd2aab4c8 100644 --- a/Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs +++ b/Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs @@ -81,33 +81,27 @@ namespace Emby.Server.Implementations.Library.Validators progress.Report(percent); } - names = names.Select(i => i.RemoveDiacritics()).DistinctNames().ToList(); - - var artistEntities = _libraryManager.GetItemList(new InternalItemsQuery + var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery { - IncludeItemTypes = new[] { typeof(MusicArtist).Name } - + IncludeItemTypes = new[] { typeof(MusicArtist).Name }, + IsDeadArtist = true, + IsLocked = false }).Cast<MusicArtist>().ToList(); - foreach (var artist in artistEntities) + foreach (var item in deadEntities) { - if (!artist.IsAccessedByName) + if (!item.IsAccessedByName) { continue; } + + _logger.Info("Deleting dead {2} {0} {1}.", item.Id.ToString("N"), item.Name, item.GetType().Name); - var name = (artist.Name ?? string.Empty).RemoveDiacritics(); - - if (!names.Contains(name, StringComparer.OrdinalIgnoreCase)) + _libraryManager.DeleteItem(item, new DeleteOptions { - _logger.Info("Deleting dead artist {0} {1}.", artist.Id.ToString("N"), artist.Name); + DeleteFileLocation = false - await _libraryManager.DeleteItem(artist, new DeleteOptions - { - DeleteFileLocation = false - - }).ConfigureAwait(false); - } + }, false); } progress.Report(100); diff --git a/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs b/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs index 39630cf96..1f4e1de92 100644 --- a/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs +++ b/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs @@ -1,18 +1,11 @@ -using MediaBrowser.Common.Progress; -using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Configuration; -using MediaBrowser.Model.Entities; using MediaBrowser.Model.Logging; using System; -using System.Collections.Generic; -using System.Linq; using System.Threading; using System.Threading.Tasks; - -using MediaBrowser.Controller.IO; using MediaBrowser.Model.IO; namespace Emby.Server.Implementations.Library.Validators @@ -73,7 +66,7 @@ namespace Emby.Server.Implementations.Library.Validators var options = new MetadataRefreshOptions(_fileSystem) { - ImageRefreshMode = ImageRefreshMode.ValidationOnly, + ImageRefreshMode = MetadataRefreshMode.ValidationOnly, MetadataRefreshMode = MetadataRefreshMode.ValidationOnly }; @@ -96,6 +89,23 @@ namespace Emby.Server.Implementations.Library.Validators progress.Report(100 * percent); } + var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery + { + IncludeItemTypes = new[] { typeof(Person).Name }, + IsDeadPerson = true, + IsLocked = false + }); + + foreach (var item in deadEntities) + { + _logger.Info("Deleting dead {2} {0} {1}.", item.Id.ToString("N"), item.Name, item.GetType().Name); + + _libraryManager.DeleteItem(item, new DeleteOptions + { + DeleteFileLocation = false + }, false); + } + progress.Report(100); _logger.Info("People validation complete"); diff --git a/Emby.Server.Implementations/Library/Validators/StudiosValidator.cs b/Emby.Server.Implementations/Library/Validators/StudiosValidator.cs index 97b8ff0ac..f306309b3 100644 --- a/Emby.Server.Implementations/Library/Validators/StudiosValidator.cs +++ b/Emby.Server.Implementations/Library/Validators/StudiosValidator.cs @@ -68,6 +68,24 @@ namespace Emby.Server.Implementations.Library.Validators progress.Report(percent); } + var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery + { + IncludeItemTypes = new[] { typeof(Studio).Name }, + IsDeadStudio = true, + IsLocked = false + }); + + foreach (var item in deadEntities) + { + _logger.Info("Deleting dead {2} {0} {1}.", item.Id.ToString("N"), item.Name, item.GetType().Name); + + _libraryManager.DeleteItem(item, new DeleteOptions + { + DeleteFileLocation = false + + }, false); + } + progress.Report(100); } } diff --git a/Emby.Server.Implementations/LiveTv/ChannelImageProvider.cs b/Emby.Server.Implementations/LiveTv/ChannelImageProvider.cs deleted file mode 100644 index 0c8049d8b..000000000 --- a/Emby.Server.Implementations/LiveTv/ChannelImageProvider.cs +++ /dev/null @@ -1,85 +0,0 @@ -using MediaBrowser.Common; -using MediaBrowser.Common.Net; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.LiveTv; -using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Logging; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; - -namespace Emby.Server.Implementations.LiveTv -{ - public class ChannelImageProvider : IDynamicImageProvider, IHasItemChangeMonitor - { - private readonly ILiveTvManager _liveTvManager; - private readonly IHttpClient _httpClient; - private readonly ILogger _logger; - private readonly IApplicationHost _appHost; - - public ChannelImageProvider(ILiveTvManager liveTvManager, IHttpClient httpClient, ILogger logger, IApplicationHost appHost) - { - _liveTvManager = liveTvManager; - _httpClient = httpClient; - _logger = logger; - _appHost = appHost; - } - - public IEnumerable<ImageType> GetSupportedImages(IHasMetadata item) - { - return new[] { ImageType.Primary }; - } - - public async Task<DynamicImageResponse> GetImage(IHasMetadata item, ImageType type, CancellationToken cancellationToken) - { - var liveTvItem = (LiveTvChannel)item; - - var imageResponse = new DynamicImageResponse(); - - var service = _liveTvManager.Services.FirstOrDefault(i => string.Equals(i.Name, liveTvItem.ServiceName, StringComparison.OrdinalIgnoreCase)); - - if (service != null && !item.HasImage(ImageType.Primary)) - { - try - { - var response = await service.GetChannelImageAsync(liveTvItem.ExternalId, cancellationToken).ConfigureAwait(false); - - if (response != null) - { - imageResponse.HasImage = true; - imageResponse.Stream = response.Stream; - imageResponse.Format = response.Format; - } - } - catch (NotImplementedException) - { - } - } - - return imageResponse; - } - - public string Name - { - get { return "Live TV Service Provider"; } - } - - public bool Supports(IHasMetadata item) - { - return item is LiveTvChannel; - } - - public int Order - { - get { return 0; } - } - - public bool HasChanged(IHasMetadata item, IDirectoryService directoryService) - { - return GetSupportedImages(item).Any(i => !item.HasImage(i)); - } - } -} diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs index f0578d9ef..0c7980ca0 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs @@ -17,12 +17,14 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV private readonly ILogger _logger; private readonly IHttpClient _httpClient; private readonly IFileSystem _fileSystem; + private readonly IStreamHelper _streamHelper; - public DirectRecorder(ILogger logger, IHttpClient httpClient, IFileSystem fileSystem) + public DirectRecorder(ILogger logger, IHttpClient httpClient, IFileSystem fileSystem, IStreamHelper streamHelper) { _logger = logger; _httpClient = httpClient; _fileSystem = fileSystem; + _streamHelper = streamHelper; } public string GetOutputPath(MediaSourceInfo mediaSource, string targetFile) @@ -50,7 +52,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV _logger.Info("Copying recording stream to file {0}", targetFile); - // The media source if infinite so we need to handle stopping ourselves + // The media source is infinite so we need to handle stopping ourselves var durationToken = new CancellationTokenSource(duration); cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token).Token; @@ -90,45 +92,11 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV var durationToken = new CancellationTokenSource(duration); cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token).Token; - await CopyUntilCancelled(response.Content, output, cancellationToken).ConfigureAwait(false); + await _streamHelper.CopyUntilCancelled(response.Content, output, 81920, cancellationToken).ConfigureAwait(false); } } _logger.Info("Recording completed to file {0}", targetFile); } - - private const int BufferSize = 81920; - public static async Task CopyUntilCancelled(Stream source, Stream target, CancellationToken cancellationToken) - { - byte[] buffer = new byte[BufferSize]; - - while (!cancellationToken.IsCancellationRequested) - { - var bytesRead = await CopyToAsyncInternal(source, target, buffer, cancellationToken).ConfigureAwait(false); - - //var position = fs.Position; - //_logger.Debug("Streamed {0} bytes to position {1} from file {2}", bytesRead, position, path); - - if (bytesRead == 0) - { - await Task.Delay(100).ConfigureAwait(false); - } - } - } - - private static async Task<int> CopyToAsyncInternal(Stream source, Stream destination, byte[] buffer, CancellationToken cancellationToken) - { - int bytesRead; - int totalBytesRead = 0; - - while ((bytesRead = await source.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false)) != 0) - { - destination.Write(buffer, 0, bytesRead); - - totalBytesRead += bytesRead; - } - - return totalBytesRead; - } } } diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs index 35d2d3c0a..d21abb74e 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs @@ -39,6 +39,10 @@ using MediaBrowser.Model.System; using MediaBrowser.Model.Threading; using MediaBrowser.Model.Extensions; using MediaBrowser.Model.Querying; +using MediaBrowser.Model.Reflection; +using MediaBrowser.Model.Providers; +using MediaBrowser.Model.MediaInfo; +using Emby.Server.Implementations.Library; namespace Emby.Server.Implementations.LiveTv.EmbyTV { @@ -62,16 +66,21 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV private readonly IMediaEncoder _mediaEncoder; private readonly IProcessFactory _processFactory; private readonly ISystemEvents _systemEvents; + private readonly IAssemblyInfo _assemblyInfo; + private IMediaSourceManager _mediaSourceManager; public static EmbyTV Current; public event EventHandler DataSourceChanged; - public event EventHandler<RecordingStatusChangedEventArgs> RecordingStatusChanged; + public event EventHandler<GenericEventArgs<TimerInfo>> TimerCreated; + public event EventHandler<GenericEventArgs<string>> TimerCancelled; private readonly ConcurrentDictionary<string, ActiveRecordingInfo> _activeRecordings = new ConcurrentDictionary<string, ActiveRecordingInfo>(StringComparer.OrdinalIgnoreCase); - public EmbyTV(IServerApplicationHost appHost, ILogger logger, IJsonSerializer jsonSerializer, IHttpClient httpClient, IServerConfigurationManager config, ILiveTvManager liveTvManager, IFileSystem fileSystem, ILibraryManager libraryManager, ILibraryMonitor libraryMonitor, IProviderManager providerManager, IMediaEncoder mediaEncoder, ITimerFactory timerFactory, IProcessFactory processFactory, ISystemEvents systemEvents) + private readonly IStreamHelper _streamHelper; + + public EmbyTV(IServerApplicationHost appHost, IStreamHelper streamHelper, IMediaSourceManager mediaSourceManager, IAssemblyInfo assemblyInfo, ILogger logger, IJsonSerializer jsonSerializer, IPowerManagement powerManagement, IHttpClient httpClient, IServerConfigurationManager config, ILiveTvManager liveTvManager, IFileSystem fileSystem, ILibraryManager libraryManager, ILibraryMonitor libraryMonitor, IProviderManager providerManager, IMediaEncoder mediaEncoder, ITimerFactory timerFactory, IProcessFactory processFactory, ISystemEvents systemEvents) { Current = this; @@ -88,9 +97,12 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV _systemEvents = systemEvents; _liveTvManager = (LiveTvManager)liveTvManager; _jsonSerializer = jsonSerializer; + _assemblyInfo = assemblyInfo; + _mediaSourceManager = mediaSourceManager; + _streamHelper = streamHelper; _seriesTimerProvider = new SeriesTimerManager(fileSystem, jsonSerializer, _logger, Path.Combine(DataPath, "seriestimers")); - _timerProvider = new TimerManager(fileSystem, jsonSerializer, _logger, Path.Combine(DataPath, "timers"), _logger, timerFactory); + _timerProvider = new TimerManager(fileSystem, jsonSerializer, _logger, Path.Combine(DataPath, "timers"), _logger, timerFactory, powerManagement); _timerProvider.TimerFired += _timerProvider_TimerFired; _config.NamedConfigurationUpdated += _config_NamedConfigurationUpdated; @@ -104,12 +116,12 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV } } - public void Start() + public async void Start() { _timerProvider.RestartTimers(); _systemEvents.Resume += _systemEvents_Resume; - CreateRecordingFolders(); + await CreateRecordingFolders().ConfigureAwait(false); } private void _systemEvents_Resume(object sender, EventArgs e) @@ -117,83 +129,78 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV _timerProvider.RestartTimers(); } - private void OnRecordingFoldersChanged() + private async void OnRecordingFoldersChanged() { - CreateRecordingFolders(); + await CreateRecordingFolders().ConfigureAwait(false); } - internal void CreateRecordingFolders() + internal async Task CreateRecordingFolders() { try { - CreateRecordingFoldersInternal(); - } - catch (Exception ex) - { - _logger.ErrorException("Error creating recording folders", ex); - } - } + var recordingFolders = GetRecordingFolders(); - internal void CreateRecordingFoldersInternal() - { - var recordingFolders = GetRecordingFolders(); + var virtualFolders = _libraryManager.GetVirtualFolders() + .ToList(); - var virtualFolders = _libraryManager.GetVirtualFolders() - .ToList(); + var allExistingPaths = virtualFolders.SelectMany(i => i.Locations).ToList(); - var allExistingPaths = virtualFolders.SelectMany(i => i.Locations).ToList(); + var pathsAdded = new List<string>(); - var pathsAdded = new List<string>(); + foreach (var recordingFolder in recordingFolders) + { + var pathsToCreate = recordingFolder.Locations + .Where(i => !allExistingPaths.Any(p => _fileSystem.AreEqual(p, i))) + .ToList(); - foreach (var recordingFolder in recordingFolders) - { - var pathsToCreate = recordingFolder.Locations - .Where(i => !allExistingPaths.Any(p => _fileSystem.AreEqual(p, i))) - .ToList(); + if (pathsToCreate.Count == 0) + { + continue; + } - if (pathsToCreate.Count == 0) - { - continue; - } + var mediaPathInfos = pathsToCreate.Select(i => new MediaPathInfo { Path = i }).ToArray(); - var mediaPathInfos = pathsToCreate.Select(i => new MediaPathInfo { Path = i }).ToArray(); + var libraryOptions = new LibraryOptions + { + PathInfos = mediaPathInfos + }; + try + { + await _libraryManager.AddVirtualFolder(recordingFolder.Name, recordingFolder.CollectionType, libraryOptions, true).ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.ErrorException("Error creating virtual folder", ex); + } - var libraryOptions = new LibraryOptions - { - PathInfos = mediaPathInfos - }; - try - { - _libraryManager.AddVirtualFolder(recordingFolder.Name, recordingFolder.CollectionType, libraryOptions, true); - } - catch (Exception ex) - { - _logger.ErrorException("Error creating virtual folder", ex); + pathsAdded.AddRange(pathsToCreate); } - pathsAdded.AddRange(pathsToCreate); - } + var config = GetConfiguration(); - var config = GetConfiguration(); + var pathsToRemove = config.MediaLocationsCreated + .Except(recordingFolders.SelectMany(i => i.Locations)) + .ToList(); - var pathsToRemove = config.MediaLocationsCreated - .Except(recordingFolders.SelectMany(i => i.Locations)) - .ToList(); + if (pathsAdded.Count > 0 || pathsToRemove.Count > 0) + { + pathsAdded.InsertRange(0, config.MediaLocationsCreated); + config.MediaLocationsCreated = pathsAdded.Except(pathsToRemove).Distinct(StringComparer.OrdinalIgnoreCase).ToArray(); + _config.SaveConfiguration("livetv", config); + } - if (pathsAdded.Count > 0 || pathsToRemove.Count > 0) - { - pathsAdded.InsertRange(0, config.MediaLocationsCreated); - config.MediaLocationsCreated = pathsAdded.Except(pathsToRemove).Distinct(StringComparer.OrdinalIgnoreCase).ToArray(); - _config.SaveConfiguration("livetv", config); + foreach (var path in pathsToRemove) + { + await RemovePathFromLibrary(path).ConfigureAwait(false); + } } - - foreach (var path in pathsToRemove) + catch (Exception ex) { - RemovePathFromLibrary(path); + _logger.ErrorException("Error creating recording folders", ex); } } - private void RemovePathFromLibrary(string path) + private async Task RemovePathFromLibrary(string path) { _logger.Debug("Removing path from library: {0}", path); @@ -213,7 +220,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV // remove entire virtual folder try { - _libraryManager.RemoveVirtualFolder(virtualFolder.Name, true); + await _libraryManager.RemoveVirtualFolder(virtualFolder.Name, true).ConfigureAwait(false); } catch (Exception ex) { @@ -272,33 +279,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV public string HomePageUrl { - get { return "http://emby.media"; } - } - - public async Task<LiveTvServiceStatusInfo> GetStatusInfoAsync(CancellationToken cancellationToken) - { - var status = new LiveTvServiceStatusInfo(); - var list = new List<LiveTvTunerInfo>(); - - foreach (var hostInstance in _liveTvManager.TunerHosts) - { - try - { - var tuners = await hostInstance.GetTunerInfos(cancellationToken).ConfigureAwait(false); - - list.AddRange(tuners); - } - catch (Exception ex) - { - _logger.ErrorException("Error getting tuners", ex); - } - } - - status.Tuners = list; - status.Status = LiveTvServiceStatus.Ok; - status.Version = _appHost.ApplicationVersion.ToString(); - status.IsVisible = false; - return status; + get { return "https://emby.media"; } } public async Task RefreshSeriesTimers(CancellationToken cancellationToken, IProgress<double> progress) @@ -315,7 +296,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV { var timers = await GetTimersAsync(cancellationToken).ConfigureAwait(false); - var tempChannelCache = new Dictionary<string, LiveTvChannel>(); + var tempChannelCache = new Dictionary<Guid, LiveTvChannel>(); foreach (var timer in timers) { @@ -408,33 +389,90 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV if (!string.IsNullOrWhiteSpace(epgChannel.ImageUrl)) { tunerChannel.ImageUrl = epgChannel.ImageUrl; - tunerChannel.HasImage = true; } } } } - private readonly ConcurrentDictionary<string, List<ChannelInfo>> _epgChannels = - new ConcurrentDictionary<string, List<ChannelInfo>>(StringComparer.OrdinalIgnoreCase); + private readonly ConcurrentDictionary<string, EpgChannelData> _epgChannels = + new ConcurrentDictionary<string, EpgChannelData>(StringComparer.OrdinalIgnoreCase); - private async Task<List<ChannelInfo>> GetEpgChannels(IListingsProvider provider, ListingsProviderInfo info, bool enableCache, CancellationToken cancellationToken) + private async Task<EpgChannelData> GetEpgChannels(IListingsProvider provider, ListingsProviderInfo info, bool enableCache, CancellationToken cancellationToken) { - List<ChannelInfo> result; + EpgChannelData result; if (!enableCache || !_epgChannels.TryGetValue(info.Id, out result)) { - result = await provider.GetChannels(info, cancellationToken).ConfigureAwait(false); + var channels = await provider.GetChannels(info, cancellationToken).ConfigureAwait(false); - foreach (var channel in result) + foreach (var channel in channels) { _logger.Info("Found epg channel in {0} {1} {2} {3}", provider.Name, info.ListingsId, channel.Name, channel.Id); } + result = new EpgChannelData(channels); _epgChannels.AddOrUpdate(info.Id, result, (k, v) => result); } return result; } + private class EpgChannelData + { + public EpgChannelData(List<ChannelInfo> channels) + { + ChannelsById = new Dictionary<string, ChannelInfo>(StringComparer.OrdinalIgnoreCase); + ChannelsByNumber = new Dictionary<string, ChannelInfo>(StringComparer.OrdinalIgnoreCase); + ChannelsByName = new Dictionary<string, ChannelInfo>(StringComparer.OrdinalIgnoreCase); + + foreach (var channel in channels) + { + ChannelsById[channel.Id] = channel; + + if (!string.IsNullOrEmpty(channel.Number)) + { + ChannelsByNumber[channel.Number] = channel; + } + + var normalizedName = NormalizeName(channel.Name ?? string.Empty); + if (!string.IsNullOrWhiteSpace(normalizedName)) + { + ChannelsByName[normalizedName] = channel; + } + } + } + + private Dictionary<string, ChannelInfo> ChannelsById { get; set; } + private Dictionary<string, ChannelInfo> ChannelsByNumber { get; set; } + private Dictionary<string, ChannelInfo> ChannelsByName { get; set; } + + public ChannelInfo GetChannelById(string id) + { + ChannelInfo result = null; + + ChannelsById.TryGetValue(id, out result); + + return result; + } + + public ChannelInfo GetChannelByNumber(string number) + { + ChannelInfo result = null; + + ChannelsByNumber.TryGetValue(number, out result); + + return result; + } + + public ChannelInfo GetChannelByName(string name) + { + ChannelInfo result = null; + + ChannelsByName.TryGetValue(name, out result); + + return result; + } + } + private async Task<ChannelInfo> GetEpgChannelFromTunerChannel(IListingsProvider provider, ListingsProviderInfo info, ChannelInfo tunerChannel, CancellationToken cancellationToken) { var epgChannels = await GetEpgChannels(provider, info, true, cancellationToken).ConfigureAwait(false); @@ -454,12 +492,17 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV return channelId; } - private ChannelInfo GetEpgChannelFromTunerChannel(ListingsProviderInfo info, ChannelInfo tunerChannel, List<ChannelInfo> epgChannels) + internal ChannelInfo GetEpgChannelFromTunerChannel(NameValuePair[] mappings, ChannelInfo tunerChannel, List<ChannelInfo> epgChannels) + { + return GetEpgChannelFromTunerChannel(mappings, tunerChannel, new EpgChannelData(epgChannels)); + } + + private ChannelInfo GetEpgChannelFromTunerChannel(ListingsProviderInfo info, ChannelInfo tunerChannel, EpgChannelData epgChannels) { return GetEpgChannelFromTunerChannel(info.ChannelMappings, tunerChannel, epgChannels); } - public ChannelInfo GetEpgChannelFromTunerChannel(NameValuePair[] mappings, ChannelInfo tunerChannel, List<ChannelInfo> epgChannels) + private ChannelInfo GetEpgChannelFromTunerChannel(NameValuePair[] mappings, ChannelInfo tunerChannel, EpgChannelData epgChannelData) { if (!string.IsNullOrWhiteSpace(tunerChannel.Id)) { @@ -470,7 +513,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV mappedTunerChannelId = tunerChannel.Id; } - var channel = epgChannels.FirstOrDefault(i => string.Equals(mappedTunerChannelId, i.Id, StringComparison.OrdinalIgnoreCase)); + var channel = epgChannelData.GetChannelById(mappedTunerChannelId); if (channel != null) { @@ -493,7 +536,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV mappedTunerChannelId = tunerChannelId; } - var channel = epgChannels.FirstOrDefault(i => string.Equals(mappedTunerChannelId, i.Id, StringComparison.OrdinalIgnoreCase)); + var channel = epgChannelData.GetChannelById(mappedTunerChannelId); if (channel != null) { @@ -510,7 +553,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV tunerChannelNumber = tunerChannel.Number; } - var channel = epgChannels.FirstOrDefault(i => string.Equals(tunerChannelNumber, i.Number, StringComparison.OrdinalIgnoreCase)); + var channel = epgChannelData.GetChannelByNumber(tunerChannelNumber); if (channel != null) { @@ -522,7 +565,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV { var normalizedName = NormalizeName(tunerChannel.Name); - var channel = epgChannels.FirstOrDefault(i => string.Equals(normalizedName, NormalizeName(i.Name ?? string.Empty), StringComparison.OrdinalIgnoreCase)); + var channel = epgChannelData.GetChannelByName(normalizedName); if (channel != null) { @@ -533,7 +576,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV return null; } - private string NormalizeName(string value) + private static string NormalizeName(string value) { return value.Replace(" ", string.Empty).Replace("-", string.Empty); } @@ -575,7 +618,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV foreach (var timer in timers) { - CancelTimerInternal(timer.Id, true); + CancelTimerInternal(timer.Id, true, true); } var remove = _seriesTimerProvider.GetAll().FirstOrDefault(r => string.Equals(r.Id, timerId, StringComparison.OrdinalIgnoreCase)); @@ -583,16 +626,22 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV { _seriesTimerProvider.Delete(remove); } - return Task.FromResult(true); + return Task.CompletedTask; } - private void CancelTimerInternal(string timerId, bool isSeriesCancelled) + private void CancelTimerInternal(string timerId, bool isSeriesCancelled, bool isManualCancellation) { var timer = _timerProvider.GetTimer(timerId); if (timer != null) { + var statusChanging = timer.Status != RecordingStatus.Cancelled; timer.Status = RecordingStatus.Cancelled; + if (isManualCancellation) + { + timer.IsManual = true; + } + if (string.IsNullOrWhiteSpace(timer.SeriesTimerId) || isSeriesCancelled) { _timerProvider.Delete(timer); @@ -601,6 +650,11 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV { _timerProvider.AddOrUpdate(timer, false); } + + if (statusChanging && TimerCancelled != null) + { + TimerCancelled(this, new GenericEventArgs<string>(timerId)); + } } ActiveRecordingInfo activeRecordingInfo; @@ -613,13 +667,13 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV public Task CancelTimerAsync(string timerId, CancellationToken cancellationToken) { - CancelTimerInternal(timerId, false); - return Task.FromResult(true); + CancelTimerInternal(timerId, false, true); + return Task.CompletedTask; } public Task DeleteRecordingAsync(string recordingId, CancellationToken cancellationToken) { - return Task.FromResult(true); + return Task.CompletedTask; } public Task CreateSeriesTimerAsync(SeriesTimerInfo info, CancellationToken cancellationToken) @@ -675,6 +729,12 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV timer.IsManual = true; _timerProvider.Add(timer); + + if (TimerCreated != null) + { + TimerCreated(this, new GenericEventArgs<TimerInfo>(timer)); + } + return Task.FromResult(timer.Id); } @@ -776,7 +836,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV _timerProvider.Update(existingTimer); } - return Task.FromResult(true); + return Task.CompletedTask; } private void UpdateExistingTimerWithNewMetadata(TimerInfo existingTimer, TimerInfo updatedTimer) @@ -788,16 +848,11 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV existingTimer.EpisodeNumber = updatedTimer.EpisodeNumber; existingTimer.EpisodeTitle = updatedTimer.EpisodeTitle; existingTimer.Genres = updatedTimer.Genres; - existingTimer.HomePageUrl = updatedTimer.HomePageUrl; - existingTimer.IsKids = updatedTimer.IsKids; - existingTimer.IsNews = updatedTimer.IsNews; existingTimer.IsMovie = updatedTimer.IsMovie; existingTimer.IsSeries = updatedTimer.IsSeries; - existingTimer.IsLive = updatedTimer.IsLive; - existingTimer.IsPremiere = updatedTimer.IsPremiere; + existingTimer.Tags = updatedTimer.Tags; existingTimer.IsProgramSeries = updatedTimer.IsProgramSeries; existingTimer.IsRepeat = updatedTimer.IsRepeat; - existingTimer.IsSports = updatedTimer.IsSports; existingTimer.Name = updatedTimer.Name; existingTimer.OfficialRating = updatedTimer.OfficialRating; existingTimer.OriginalAirDate = updatedTimer.OriginalAirDate; @@ -807,26 +862,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV existingTimer.SeasonNumber = updatedTimer.SeasonNumber; existingTimer.StartDate = updatedTimer.StartDate; existingTimer.ShowId = updatedTimer.ShowId; - } - - public Task<ImageStream> GetChannelImageAsync(string channelId, CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } - - public Task<ImageStream> GetRecordingImageAsync(string recordingId, CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } - - public Task<ImageStream> GetProgramImageAsync(string programId, string channelId, CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } - - public async Task<IEnumerable<RecordingInfo>> GetRecordingsAsync(CancellationToken cancellationToken) - { - return new List<RecordingInfo>(); + existingTimer.ProviderIds = updatedTimer.ProviderIds; + existingTimer.SeriesProviderIds = updatedTimer.SeriesProviderIds; } public string GetActiveRecordingPath(string id) @@ -909,6 +946,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV defaults.SeriesId = program.SeriesId; defaults.ProgramId = program.Id; defaults.RecordNewOnly = !program.IsRepeat; + defaults.Name = program.Name; } defaults.SkipEpisodesInLibrary = defaults.RecordNewOnly; @@ -972,10 +1010,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV { program.ChannelId = channelId; - if (provider.Item2.EnableNewProgramIds) - { - program.Id += "_" + channelId; - } + program.Id += "_" + channelId; } if (programs.Count > 0) @@ -1000,26 +1035,51 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV .ToList(); } - public Task<MediaSourceInfo> GetRecordingStream(string recordingId, string streamId, CancellationToken cancellationToken) + public Task<MediaSourceInfo> GetChannelStream(string channelId, string streamId, CancellationToken cancellationToken) { throw new NotImplementedException(); } - private readonly SemaphoreSlim _liveStreamsSemaphore = new SemaphoreSlim(1, 1); - private readonly List<ILiveStream> _liveStreams = new List<ILiveStream>(); - - public async Task<MediaSourceInfo> GetChannelStream(string channelId, string streamId, CancellationToken cancellationToken) + public async Task<ILiveStream> GetChannelStreamWithDirectStreamProvider(string channelId, string streamId, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken) { - var result = await GetChannelStreamWithDirectStreamProvider(channelId, streamId, cancellationToken).ConfigureAwait(false); + _logger.Info("Streaming Channel " + channelId); - return result.Item1; - } + var result = string.IsNullOrEmpty(streamId) ? + null : + currentLiveStreams.FirstOrDefault(i => string.Equals(i.OriginalStreamId, streamId, StringComparison.OrdinalIgnoreCase)); - public async Task<Tuple<MediaSourceInfo, IDirectStreamProvider>> GetChannelStreamWithDirectStreamProvider(string channelId, string streamId, CancellationToken cancellationToken) - { - var result = await GetChannelStreamInternal(channelId, streamId, cancellationToken).ConfigureAwait(false); + if (result != null && result.EnableStreamSharing) + { + result.ConsumerCount++; + + _logger.Info("Live stream {0} consumer count is now {1}", streamId, result.ConsumerCount); + + return result; + } + + foreach (var hostInstance in _liveTvManager.TunerHosts) + { + try + { + result = await hostInstance.GetChannelStream(channelId, streamId, currentLiveStreams, cancellationToken).ConfigureAwait(false); + + var openedMediaSource = result.MediaSource; + + result.OriginalStreamId = streamId; + + _logger.Info("Returning mediasource streamId {0}, mediaSource.Id {1}, mediaSource.LiveStreamId {2}", streamId, openedMediaSource.Id, openedMediaSource.LiveStreamId); + + return result; + } + catch (FileNotFoundException) + { + } + catch (OperationCanceledException) + { + } + } - return new Tuple<MediaSourceInfo, IDirectStreamProvider>(result.Item2, result.Item1 as IDirectStreamProvider); + throw new Exception("Tuner not found."); } private MediaSourceInfo CloneMediaSource(MediaSourceInfo mediaSource, bool enableStreamSharing) @@ -1039,93 +1099,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV return mediaSource; } - public async Task<ILiveStream> GetLiveStream(string uniqueId) - { - await _liveStreamsSemaphore.WaitAsync().ConfigureAwait(false); - - try - { - return _liveStreams - .FirstOrDefault(i => string.Equals(i.UniqueId, uniqueId, StringComparison.OrdinalIgnoreCase)); - } - finally - { - _liveStreamsSemaphore.Release(); - } - } - - public async Task<List<ILiveStream>> GetLiveStreams(TunerHostInfo host, CancellationToken cancellationToken) - { - //await _liveStreamsSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false); - - //try - //{ - var hostId = host.Id; - - return _liveStreams - .Where(i => string.Equals(i.TunerHostId, hostId, StringComparison.OrdinalIgnoreCase)) - .ToList(); - //} - //finally - //{ - // _liveStreamsSemaphore.Release(); - //} - } - - private async Task<Tuple<ILiveStream, MediaSourceInfo>> GetChannelStreamInternal(string channelId, string streamId, CancellationToken cancellationToken) - { - _logger.Info("Streaming Channel " + channelId); - - await _liveStreamsSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false); - - try - { - var result = _liveStreams.FirstOrDefault(i => string.Equals(i.OriginalStreamId, streamId, StringComparison.OrdinalIgnoreCase)); - - if (result != null && result.EnableStreamSharing) - { - var openedMediaSource = CloneMediaSource(result.OpenedMediaSource, result.EnableStreamSharing); - result.SharedStreamIds.Add(openedMediaSource.Id); - - _logger.Info("Live stream {0} consumer count is now {1}", streamId, result.ConsumerCount); - - return new Tuple<ILiveStream, MediaSourceInfo>(result, openedMediaSource); - } - - foreach (var hostInstance in _liveTvManager.TunerHosts) - { - try - { - result = await hostInstance.GetChannelStream(channelId, streamId, cancellationToken).ConfigureAwait(false); - - var openedMediaSource = CloneMediaSource(result.OpenedMediaSource, result.EnableStreamSharing); - - result.SharedStreamIds.Add(openedMediaSource.Id); - _liveStreams.Add(result); - - result.OriginalStreamId = streamId; - - _logger.Info("Returning mediasource streamId {0}, mediaSource.Id {1}, mediaSource.LiveStreamId {2}", - streamId, openedMediaSource.Id, openedMediaSource.LiveStreamId); - - return new Tuple<ILiveStream, MediaSourceInfo>(result, openedMediaSource); - } - catch (FileNotFoundException) - { - } - catch (OperationCanceledException) - { - } - } - } - finally - { - _liveStreamsSemaphore.Release(); - } - - throw new Exception("Tuner not found."); - } - public async Task<List<MediaSourceInfo>> GetChannelStreamMediaSources(string channelId, CancellationToken cancellationToken) { if (string.IsNullOrWhiteSpace(channelId)) @@ -1153,47 +1126,14 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV throw new NotImplementedException(); } - public async Task<List<MediaSourceInfo>> GetRecordingStreamMediaSources(string recordingId, CancellationToken cancellationToken) - { - ActiveRecordingInfo info; - - recordingId = recordingId.Replace("recording", string.Empty); - - if (_activeRecordings.TryGetValue(recordingId, out info)) - { - var stream = new MediaSourceInfo - { - Path = _appHost.GetLocalApiUrl("127.0.0.1") + "/LiveTv/LiveRecordings/" + recordingId + "/stream", - Id = recordingId, - SupportsDirectPlay = false, - SupportsDirectStream = true, - SupportsTranscoding = true, - IsInfiniteStream = true, - RequiresOpening = false, - RequiresClosing = false, - Protocol = MediaBrowser.Model.MediaInfo.MediaProtocol.Http, - BufferMs = 0, - IgnoreDts = true, - IgnoreIndex = true - }; - - var isAudio = false; - await new LiveStreamHelper(_mediaEncoder, _logger).AddMediaInfoWithProbe(stream, isAudio, cancellationToken).ConfigureAwait(false); - - return new List<MediaSourceInfo> - { - stream - }; - } - - throw new FileNotFoundException(); - } - public async Task<List<MediaSourceInfo>> GetRecordingStreamMediaSources(ActiveRecordingInfo info, CancellationToken cancellationToken) { var stream = new MediaSourceInfo { - Path = _appHost.GetLocalApiUrl("127.0.0.1") + "/LiveTv/LiveRecordings/" + info.Id + "/stream", + EncoderPath = _appHost.GetLocalApiUrl("127.0.0.1") + "/LiveTv/LiveRecordings/" + info.Id + "/stream", + EncoderProtocol = MediaProtocol.Http, + Path = info.Path, + Protocol = MediaProtocol.File, Id = info.Id, SupportsDirectPlay = false, SupportsDirectStream = true, @@ -1201,14 +1141,13 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV IsInfiniteStream = true, RequiresOpening = false, RequiresClosing = false, - Protocol = MediaBrowser.Model.MediaInfo.MediaProtocol.Http, BufferMs = 0, IgnoreDts = true, IgnoreIndex = true }; var isAudio = false; - await new LiveStreamHelper(_mediaEncoder, _logger).AddMediaInfoWithProbe(stream, isAudio, cancellationToken).ConfigureAwait(false); + await new LiveStreamHelper(_mediaEncoder, _logger, _jsonSerializer, _config.CommonApplicationPaths).AddMediaInfoWithProbe(stream, isAudio, false, cancellationToken).ConfigureAwait(false); return new List<MediaSourceInfo> { @@ -1216,48 +1155,9 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV }; } - public async Task CloseLiveStream(string id, CancellationToken cancellationToken) + public Task CloseLiveStream(string id, CancellationToken cancellationToken) { - // Ignore the consumer id - //id = id.Substring(id.IndexOf('_') + 1); - - await _liveStreamsSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false); - - try - { - var stream = _liveStreams.FirstOrDefault(i => i.SharedStreamIds.Contains(id)); - if (stream != null) - { - stream.SharedStreamIds.Remove(id); - - _logger.Info("Live stream {0} consumer count is now {1}", id, stream.ConsumerCount); - - if (stream.ConsumerCount <= 0) - { - _liveStreams.Remove(stream); - - _logger.Info("Closing live stream {0}", id); - - stream.Close(); - _logger.Info("Live stream {0} closed successfully", id); - } - } - else - { - _logger.Warn("Live stream not found: {0}, unable to close", id); - } - } - catch (OperationCanceledException) - { - } - catch (Exception ex) - { - _logger.ErrorException("Error closing live stream", ex); - } - finally - { - _liveStreamsSemaphore.Release(); - } + return Task.CompletedTask; } public Task RecordLiveStream(string id, CancellationToken cancellationToken) @@ -1274,7 +1174,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV { var timer = e.Argument; - _logger.Info("Recording timer fired."); + _logger.Info("Recording timer fired for {0}.", timer.Name); try { @@ -1321,7 +1221,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV } } - private string GetRecordingPath(TimerInfo timer, out string seriesPath) + private string GetRecordingPath(TimerInfo timer, RemoteSearchResult metadata, out string seriesPath) { var recordPath = RecordingPath; var config = GetConfiguration(); @@ -1342,7 +1242,13 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV recordPath = Path.Combine(recordPath, "Series"); } - var folderName = _fileSystem.GetValidFilename(timer.Name).Trim(); + // trim trailing period from the folder name + var folderName = _fileSystem.GetValidFilename(timer.Name).Trim().TrimEnd('.').Trim(); + + if (metadata != null && metadata.ProductionYear.HasValue) + { + folderName += " (" + metadata.ProductionYear.Value.ToString(CultureInfo.InvariantCulture) + ")"; + } // Can't use the year here in the folder name because it is the year of the episode, not the series. recordPath = Path.Combine(recordPath, folderName); @@ -1375,6 +1281,10 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV { folderName += " (" + timer.ProductionYear.Value.ToString(CultureInfo.InvariantCulture) + ")"; } + + // trim trailing period from the folder name + folderName = folderName.TrimEnd('.').Trim(); + recordPath = Path.Combine(recordPath, folderName); } else if (timer.IsKids) @@ -1389,6 +1299,10 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV { folderName += " (" + timer.ProductionYear.Value.ToString(CultureInfo.InvariantCulture) + ")"; } + + // trim trailing period from the folder name + folderName = folderName.TrimEnd('.').Trim(); + recordPath = Path.Combine(recordPath, folderName); } else if (timer.IsSports) @@ -1438,23 +1352,36 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV } string seriesPath = null; - var recordPath = GetRecordingPath(timer, out seriesPath); + var remoteMetadata = await FetchInternetMetadata(timer, CancellationToken.None).ConfigureAwait(false); + var recordPath = GetRecordingPath(timer, remoteMetadata, out seriesPath); var recordingStatus = RecordingStatus.New; - var recorder = await GetRecorder().ConfigureAwait(false); - string liveStreamId = null; + var channelItem = _liveTvManager.GetLiveTvChannel(timer, this); + try { - var allMediaSources = await GetChannelStreamMediaSources(timer.ChannelId, CancellationToken.None).ConfigureAwait(false); + var allMediaSources = await _mediaSourceManager.GetPlayackMediaSources(channelItem, null, true, false, CancellationToken.None).ConfigureAwait(false); + + var mediaStreamInfo = allMediaSources[0]; + IDirectStreamProvider directStreamProvider = null; - _logger.Info("Opening recording stream from tuner provider"); - var liveStreamInfo = await GetChannelStreamInternal(timer.ChannelId, allMediaSources[0].Id, CancellationToken.None) - .ConfigureAwait(false); + if (mediaStreamInfo.RequiresOpening) + { + var liveStreamResponse = await _mediaSourceManager.OpenLiveStreamInternal(new LiveStreamRequest + { + ItemId = channelItem.Id, + OpenToken = mediaStreamInfo.OpenToken - var mediaStreamInfo = liveStreamInfo.Item2; - liveStreamId = mediaStreamInfo.Id; + }, CancellationToken.None).ConfigureAwait(false); + + mediaStreamInfo = liveStreamResponse.Item1.MediaSource; + liveStreamId = mediaStreamInfo.LiveStreamId; + directStreamProvider = liveStreamResponse.Item2; + } + + var recorder = GetRecorder(mediaStreamInfo); recordPath = recorder.GetOutputPath(mediaStreamInfo, recordPath); recordPath = EnsureFileUnique(recordPath, timer.Id); @@ -1478,13 +1405,13 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV await SaveRecordingMetadata(timer, recordPath, seriesPath).ConfigureAwait(false); - CreateRecordingFolders(); + await CreateRecordingFolders().ConfigureAwait(false); TriggerRefresh(recordPath); EnforceKeepUpTo(timer, seriesPath); }; - await recorder.Record(liveStreamInfo.Item1 as IDirectStreamProvider, mediaStreamInfo, recordPath, duration, onStarted, activeRecordingInfo.CancellationTokenSource.Token).ConfigureAwait(false); + await recorder.Record(directStreamProvider, mediaStreamInfo, recordPath, duration, onStarted, activeRecordingInfo.CancellationTokenSource.Token).ConfigureAwait(false); recordingStatus = RecordingStatus.Completed; _logger.Info("Recording completed: {0}", recordPath); @@ -1504,7 +1431,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV { try { - await CloseLiveStream(liveStreamId, CancellationToken.None).ConfigureAwait(false); + await _mediaSourceManager.CloseLiveStream(liveStreamId).ConfigureAwait(false); } catch (Exception ex) { @@ -1544,6 +1471,34 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV } } + private async Task<RemoteSearchResult> FetchInternetMetadata(TimerInfo timer, CancellationToken cancellationToken) + { + if (timer.IsSeries) + { + if (timer.SeriesProviderIds.Count == 0) + { + return null; + } + + var query = new RemoteSearchQuery<SeriesInfo>() + { + SearchInfo = new SeriesInfo + { + ProviderIds = timer.SeriesProviderIds, + Name = timer.Name, + MetadataCountryCode = _config.Configuration.MetadataCountryCode, + MetadataLanguage = _config.Configuration.PreferredMetadataLanguage + } + }; + + var results = await _providerManager.GetRemoteSearchResults<Series, SeriesInfo>(query, cancellationToken).ConfigureAwait(false); + + return results.FirstOrDefault(); + } + + return null; + } + private void DeleteFileIfEmpty(string path) { var file = _fileSystem.GetFileInfo(path); @@ -1573,8 +1528,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV _providerManager.QueueRefresh(item.Id, new MetadataRefreshOptions(_fileSystem) { - ValidateChildren = true, - RefreshPaths = new List<string> + RefreshPaths = new string[] { path, _fileSystem.GetDirectoryName(path), @@ -1627,7 +1581,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV var seriesTimerId = timer.SeriesTimerId; var seriesTimer = _seriesTimerProvider.GetAll().FirstOrDefault(i => string.Equals(i.Id, seriesTimerId, StringComparison.OrdinalIgnoreCase)); - if (seriesTimer == null || seriesTimer.KeepUpTo <= 1) + if (seriesTimer == null || seriesTimer.KeepUpTo <= 0) { return; } @@ -1654,7 +1608,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV .Skip(seriesTimer.KeepUpTo - 1) .ToList(); - await DeleteLibraryItemsForTimers(timersToDelete).ConfigureAwait(false); + DeleteLibraryItemsForTimers(timersToDelete); var librarySeries = _libraryManager.FindByPath(seriesPath, true) as Folder; @@ -1665,14 +1619,14 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV var episodesToDelete = (librarySeries.GetItemList(new InternalItemsQuery { - OrderBy = new[] { new Tuple<string, SortOrder>(ItemSortBy.DateCreated, SortOrder.Descending) }, + OrderBy = new[] { new ValueTuple<string, SortOrder>(ItemSortBy.DateCreated, SortOrder.Descending) }, IsVirtualItem = false, IsFolder = false, Recursive = true, DtoOptions = new DtoOptions(true) })) - .Where(i => i.LocationType == LocationType.FileSystem && _fileSystem.FileExists(i.Path)) + .Where(i => i.IsFileProtocol && _fileSystem.FileExists(i.Path)) .Skip(seriesTimer.KeepUpTo - 1) .ToList(); @@ -1680,11 +1634,11 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV { try { - await _libraryManager.DeleteItem(item, new DeleteOptions + _libraryManager.DeleteItem(item, new DeleteOptions { DeleteFileLocation = true - }).ConfigureAwait(false); + }, true); } catch (Exception ex) { @@ -1699,7 +1653,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV } private readonly SemaphoreSlim _recordingDeleteSemaphore = new SemaphoreSlim(1, 1); - private async Task DeleteLibraryItemsForTimers(List<TimerInfo> timers) + private void DeleteLibraryItemsForTimers(List<TimerInfo> timers) { foreach (var timer in timers) { @@ -1710,7 +1664,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV try { - await DeleteLibraryItemForTimer(timer).ConfigureAwait(false); + DeleteLibraryItemForTimer(timer); } catch (Exception ex) { @@ -1719,17 +1673,17 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV } } - private async Task DeleteLibraryItemForTimer(TimerInfo timer) + private void DeleteLibraryItemForTimer(TimerInfo timer) { var libraryItem = _libraryManager.FindByPath(timer.RecordingPath, false); if (libraryItem != null) { - await _libraryManager.DeleteItem(libraryItem, new DeleteOptions + _libraryManager.DeleteItem(libraryItem, new DeleteOptions { DeleteFileLocation = true - }).ConfigureAwait(false); + }, true); } else { @@ -1783,57 +1737,18 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV return false; } - private async Task<IRecorder> GetRecorder() + private IRecorder GetRecorder(MediaSourceInfo mediaSource) { - var config = GetConfiguration(); - - var regInfo = await _liveTvManager.GetRegistrationInfo("dvr").ConfigureAwait(false); - - if (regInfo.IsValid) + if (mediaSource.RequiresLooping || !(mediaSource.Container ?? string.Empty).EndsWith("ts", StringComparison.OrdinalIgnoreCase) || (mediaSource.Protocol != MediaProtocol.File && mediaSource.Protocol != MediaProtocol.Http)) { - if (config.EnableRecordingEncoding) - { - return new EncodedRecorder(_logger, _fileSystem, _mediaEncoder, _config.ApplicationPaths, _jsonSerializer, config, _httpClient, _processFactory, _config); - } - - return new DirectRecorder(_logger, _httpClient, _fileSystem); - - //var options = new LiveTvOptions - //{ - // EnableOriginalAudioWithEncodedRecordings = true, - // RecordedVideoCodec = "copy", - // RecordingEncodingFormat = "ts" - //}; - //return new EncodedRecorder(_logger, _fileSystem, _mediaEncoder, _config.ApplicationPaths, _jsonSerializer, options, _httpClient, _processFactory, _config); + return new EncodedRecorder(_logger, _fileSystem, _mediaEncoder, _config.ApplicationPaths, _jsonSerializer, _httpClient, _processFactory, _config, _assemblyInfo); } - throw new InvalidOperationException("Emby DVR Requires an active Emby Premiere subscription."); + return new DirectRecorder(_logger, _httpClient, _fileSystem, _streamHelper); } private void OnSuccessfulRecording(TimerInfo timer, string path) { - //if (timer.IsProgramSeries && GetConfiguration().EnableAutoOrganize) - //{ - // try - // { - // // this is to account for the library monitor holding a lock for additional time after the change is complete. - // // ideally this shouldn't be hard-coded - // await Task.Delay(30000).ConfigureAwait(false); - - // var organize = new EpisodeFileOrganizer(_organizationService, _config, _fileSystem, _logger, _libraryManager, _libraryMonitor, _providerManager); - - // var result = await organize.OrganizeEpisodeFile(path, _config.GetAutoOrganizeOptions(), false, CancellationToken.None).ConfigureAwait(false); - - // if (result.Status == FileSortingStatus.Success) - // { - // return; - // } - // } - // catch (Exception ex) - // { - // _logger.ErrorException("Error processing new recording", ex); - // } - //} PostProcessRecording(timer, path); } @@ -2026,7 +1941,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV program = new LiveTvProgram { Name = timer.Name, - HomePageUrl = timer.HomePageUrl, Overview = timer.Overview, Genres = timer.Genres, CommunityRating = timer.CommunityRating, @@ -2040,16 +1954,16 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV if (timer.IsSports) { - AddGenre(program.Genres, "Sports"); + program.AddGenre("Sports"); } if (timer.IsKids) { - AddGenre(program.Genres, "Kids"); - AddGenre(program.Genres, "Children"); + program.AddGenre("Kids"); + program.AddGenre("Children"); } if (timer.IsNews) { - AddGenre(program.Genres, "News"); + program.AddGenre("News"); } if (timer.IsProgramSeries) @@ -2097,12 +2011,30 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV writer.WriteStartDocument(true); writer.WriteStartElement("tvshow"); + string id; + if (timer.SeriesProviderIds.TryGetValue(MetadataProviders.Tvdb.ToString(), out id)) + { + writer.WriteElementString("id", id); + } + if (timer.SeriesProviderIds.TryGetValue(MetadataProviders.Imdb.ToString(), out id)) + { + writer.WriteElementString("imdb_id", id); + } + if (timer.SeriesProviderIds.TryGetValue(MetadataProviders.Tmdb.ToString(), out id)) + { + writer.WriteElementString("tmdbid", id); + } + if (timer.SeriesProviderIds.TryGetValue(MetadataProviders.Zap2It.ToString(), out id)) + { + writer.WriteElementString("zap2itid", id); + } + if (!string.IsNullOrWhiteSpace(timer.Name)) { writer.WriteElementString("title", timer.Name); } - if (!string.IsNullOrEmpty(timer.OfficialRating)) + if (!string.IsNullOrWhiteSpace(timer.OfficialRating)) { writer.WriteElementString("mpaa", timer.OfficialRating); } @@ -2139,11 +2071,13 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV var options = _config.GetNfoConfiguration(); + var isSeriesEpisode = timer.IsProgramSeries; + using (XmlWriter writer = XmlWriter.Create(stream, settings)) { writer.WriteStartDocument(true); - if (timer.IsProgramSeries) + if (isSeriesEpisode) { writer.WriteStartElement("episodedetails"); @@ -2152,11 +2086,13 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV writer.WriteElementString("title", timer.EpisodeTitle); } - if (item.PremiereDate.HasValue) + var premiereDate = item.PremiereDate ?? (!timer.IsRepeat ? DateTime.UtcNow : (DateTime?)null); + + if (premiereDate.HasValue) { var formatString = options.ReleaseDateFormat; - writer.WriteElementString("aired", item.PremiereDate.Value.ToLocalTime().ToString(formatString)); + writer.WriteElementString("aired", premiereDate.Value.ToLocalTime().ToString(formatString)); } if (item.IndexNumber.HasValue) @@ -2210,11 +2146,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV writer.WriteElementString("plot", overview); - if (lockData) - { - writer.WriteElementString("lockdata", true.ToString().ToLower()); - } - if (item.CommunityRating.HasValue) { writer.WriteElementString("rating", item.CommunityRating.Value.ToString(CultureInfo.InvariantCulture)); @@ -2225,12 +2156,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV writer.WriteElementString("genre", genre); } - if (!string.IsNullOrWhiteSpace(item.HomePageUrl)) - { - writer.WriteElementString("website", item.HomePageUrl); - } - - var people = item.Id == Guid.Empty ? new List<PersonInfo>() : _libraryManager.GetPeople(item); + var people = item.Id.Equals(Guid.Empty) ? new List<PersonInfo>() : _libraryManager.GetPeople(item); var directors = people .Where(i => IsPersonType(i, PersonType.Director)) @@ -2268,26 +2194,38 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV var imdb = item.GetProviderId(MetadataProviders.Imdb); if (!string.IsNullOrEmpty(imdb)) { - if (item is Series) - { - writer.WriteElementString("imdb_id", imdb); - } - else + if (!isSeriesEpisode) { - writer.WriteElementString("imdbid", imdb); + writer.WriteElementString("id", imdb); } + + writer.WriteElementString("imdbid", imdb); + + // No need to lock if we have identified the content already + lockData = false; } var tvdb = item.GetProviderId(MetadataProviders.Tvdb); if (!string.IsNullOrEmpty(tvdb)) { writer.WriteElementString("tvdbid", tvdb); + + // No need to lock if we have identified the content already + lockData = false; } var tmdb = item.GetProviderId(MetadataProviders.Tmdb); if (!string.IsNullOrEmpty(tmdb)) { writer.WriteElementString("tmdbid", tmdb); + + // No need to lock if we have identified the content already + lockData = false; + } + + if (lockData) + { + writer.WriteElementString("lockdata", true.ToString().ToLower()); } if (item.CriticRating.HasValue) @@ -2328,7 +2266,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV { var query = new InternalItemsQuery { - ItemIds = new[] { _liveTvManager.GetInternalProgramId(Name, programId).ToString("N") }, + ItemIds = new[] { _liveTvManager.GetInternalProgramId(programId) }, Limit = 1, DtoOptions = new DtoOptions() }; @@ -2358,12 +2296,12 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV }, MinStartDate = startDateUtc.AddMinutes(-3), MaxStartDate = startDateUtc.AddMinutes(3), - OrderBy = new[] { new Tuple<string, SortOrder>(ItemSortBy.StartDate, SortOrder.Ascending) } + OrderBy = new[] { new ValueTuple<string, SortOrder>(ItemSortBy.StartDate, SortOrder.Ascending) } }; if (!string.IsNullOrWhiteSpace(channelId)) { - query.ChannelIds = new[] { _liveTvManager.GetInternalChannelId(Name, channelId).ToString("N") }; + query.ChannelIds = new[] { _liveTvManager.GetInternalChannelId(Name, channelId) }; } return _libraryManager.GetItemList(query).Cast<LiveTvProgram>().FirstOrDefault(); @@ -2383,7 +2321,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV if (!seriesTimer.RecordAnyTime) { - if (Math.Abs(seriesTimer.StartDate.TimeOfDay.Ticks - timer.StartDate.TimeOfDay.Ticks) >= TimeSpan.FromMinutes(5).Ticks) + if (Math.Abs(seriesTimer.StartDate.TimeOfDay.Ticks - timer.StartDate.TimeOfDay.Ticks) >= TimeSpan.FromMinutes(10).Ticks) { return true; } @@ -2473,6 +2411,11 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV enabledTimersForSeries.Add(timer); } _timerProvider.Add(timer); + + if (TimerCreated != null) + { + TimerCreated(this, new GenericEventArgs<TimerInfo>(timer)); + } } else { @@ -2525,7 +2468,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV .Select(i => i.Id) .ToList(); - var deleteStatuses = new List<RecordingStatus> + var deleteStatuses = new[] { RecordingStatus.New }; @@ -2538,7 +2481,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV foreach (var timer in deletes) { - CancelTimerInternal(timer.Id, false); + CancelTimerInternal(timer.Id, false, false); } } } @@ -2550,11 +2493,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV throw new ArgumentNullException("seriesTimer"); } - if (string.IsNullOrWhiteSpace(seriesTimer.SeriesId)) - { - return new List<TimerInfo>(); - } - var query = new InternalItemsQuery { IncludeItemTypes = new string[] { typeof(LiveTvProgram).Name }, @@ -2566,21 +2504,26 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV MinEndDate = DateTime.UtcNow }; + if (string.IsNullOrEmpty(seriesTimer.SeriesId)) + { + query.Name = seriesTimer.Name; + } + if (!seriesTimer.RecordAnyChannel) { - query.ChannelIds = new[] { _liveTvManager.GetInternalChannelId(Name, seriesTimer.ChannelId).ToString("N") }; + query.ChannelIds = new[] { _liveTvManager.GetInternalChannelId(Name, seriesTimer.ChannelId) }; } - var tempChannelCache = new Dictionary<string, LiveTvChannel>(); + var tempChannelCache = new Dictionary<Guid, LiveTvChannel>(); return _libraryManager.GetItemList(query).Cast<LiveTvProgram>().Select(i => CreateTimer(i, seriesTimer, tempChannelCache)); } - private TimerInfo CreateTimer(LiveTvProgram parent, SeriesTimerInfo seriesTimer, Dictionary<string, LiveTvChannel> tempChannelCache) + private TimerInfo CreateTimer(LiveTvProgram parent, SeriesTimerInfo seriesTimer, Dictionary<Guid, LiveTvChannel> tempChannelCache) { string channelId = seriesTimer.RecordAnyChannel ? null : seriesTimer.ChannelId; - if (string.IsNullOrWhiteSpace(channelId) && !string.IsNullOrWhiteSpace(parent.ChannelId)) + if (string.IsNullOrWhiteSpace(channelId) && !parent.ChannelId.Equals(Guid.Empty)) { LiveTvChannel channel; @@ -2633,15 +2576,15 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV private void CopyProgramInfoToTimerInfo(LiveTvProgram programInfo, TimerInfo timerInfo) { - var tempChannelCache = new Dictionary<string, LiveTvChannel>(); + var tempChannelCache = new Dictionary<Guid, LiveTvChannel>(); CopyProgramInfoToTimerInfo(programInfo, timerInfo, tempChannelCache); } - private void CopyProgramInfoToTimerInfo(LiveTvProgram programInfo, TimerInfo timerInfo, Dictionary<string, LiveTvChannel> tempChannelCache) + private void CopyProgramInfoToTimerInfo(LiveTvProgram programInfo, TimerInfo timerInfo, Dictionary<Guid, LiveTvChannel> tempChannelCache) { string channelId = null; - if (!string.IsNullOrWhiteSpace(programInfo.ChannelId)) + if (!programInfo.ChannelId.Equals(Guid.Empty)) { LiveTvChannel channel; @@ -2679,24 +2622,33 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV timerInfo.SeasonNumber = programInfo.ParentIndexNumber; timerInfo.EpisodeNumber = programInfo.IndexNumber; timerInfo.IsMovie = programInfo.IsMovie; - timerInfo.IsKids = programInfo.IsKids; - timerInfo.IsNews = programInfo.IsNews; - timerInfo.IsSports = programInfo.IsSports; timerInfo.ProductionYear = programInfo.ProductionYear; timerInfo.EpisodeTitle = programInfo.EpisodeTitle; timerInfo.OriginalAirDate = programInfo.PremiereDate; timerInfo.IsProgramSeries = programInfo.IsSeries; timerInfo.IsSeries = programInfo.IsSeries; - timerInfo.IsLive = programInfo.IsLive; - timerInfo.IsPremiere = programInfo.IsPremiere; - timerInfo.HomePageUrl = programInfo.HomePageUrl; timerInfo.CommunityRating = programInfo.CommunityRating; timerInfo.Overview = programInfo.Overview; timerInfo.OfficialRating = programInfo.OfficialRating; timerInfo.IsRepeat = programInfo.IsRepeat; timerInfo.SeriesId = programInfo.ExternalSeriesId; + timerInfo.ProviderIds = programInfo.ProviderIds; + timerInfo.Tags = programInfo.Tags; + + var seriesProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); + + foreach (var providerId in timerInfo.ProviderIds) + { + var srch = "Series"; + if (providerId.Key.StartsWith(srch, StringComparison.OrdinalIgnoreCase)) + { + seriesProviderIds[providerId.Key.Substring(srch.Length)] = providerId.Value; + } + } + + timerInfo.SeriesProviderIds = seriesProviderIds; } private bool IsProgramAlreadyInLibrary(TimerInfo program) @@ -2708,7 +2660,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV IncludeItemTypes = new[] { typeof(Series).Name }, Name = program.Name - }).Select(i => i.ToString("N")).ToArray(); + }).ToArray(); if (seriesIds.Length == 0) { @@ -2745,7 +2697,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV { pair.Value.CancellationTokenSource.Cancel(); } - GC.SuppressFinalize(this); } public List<VirtualFolderInfo> GetRecordingFolders() diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTVRegistration.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTVRegistration.cs deleted file mode 100644 index b339537ae..000000000 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTVRegistration.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Threading.Tasks; -using MediaBrowser.Common.Security; - -namespace Emby.Server.Implementations.LiveTv.EmbyTV -{ - public class EmbyTVRegistration : IRequiresRegistration - { - private readonly ISecurityManager _securityManager; - - public static EmbyTVRegistration Instance; - - public EmbyTVRegistration(ISecurityManager securityManager) - { - _securityManager = securityManager; - Instance = this; - } - - private bool? _isXmlTvEnabled; - - public Task LoadRegistrationInfoAsync() - { - _isXmlTvEnabled = null; - return Task.FromResult(true); - } - - public async Task<bool> EnableXmlTv() - { - if (!_isXmlTvEnabled.HasValue) - { - var info = await _securityManager.GetRegistrationStatus("xmltv").ConfigureAwait(false); - _isXmlTvEnabled = info.IsValid; - } - return _isXmlTvEnabled.Value; - } - } -} diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs index d6f5e0d9f..9506a82be 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs @@ -22,6 +22,7 @@ using MediaBrowser.Model.Logging; using MediaBrowser.Model.Serialization; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Library; +using MediaBrowser.Model.Reflection; namespace Emby.Server.Implementations.LiveTv.EmbyTV { @@ -32,7 +33,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV private readonly IHttpClient _httpClient; private readonly IMediaEncoder _mediaEncoder; private readonly IServerApplicationPaths _appPaths; - private readonly LiveTvOptions _liveTvOptions; private bool _hasExited; private Stream _logFileStream; private string _targetPath; @@ -41,37 +41,19 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV private readonly IJsonSerializer _json; private readonly TaskCompletionSource<bool> _taskCompletionSource = new TaskCompletionSource<bool>(); private readonly IServerConfigurationManager _config; + private readonly IAssemblyInfo _assemblyInfo; - public EncodedRecorder(ILogger logger, IFileSystem fileSystem, IMediaEncoder mediaEncoder, IServerApplicationPaths appPaths, IJsonSerializer json, LiveTvOptions liveTvOptions, IHttpClient httpClient, IProcessFactory processFactory, IServerConfigurationManager config) + public EncodedRecorder(ILogger logger, IFileSystem fileSystem, IMediaEncoder mediaEncoder, IServerApplicationPaths appPaths, IJsonSerializer json, IHttpClient httpClient, IProcessFactory processFactory, IServerConfigurationManager config, IAssemblyInfo assemblyInfo) { _logger = logger; _fileSystem = fileSystem; _mediaEncoder = mediaEncoder; _appPaths = appPaths; _json = json; - _liveTvOptions = liveTvOptions; _httpClient = httpClient; _processFactory = processFactory; _config = config; - } - - private string OutputFormat - { - get - { - var format = _liveTvOptions.RecordingEncodingFormat; - - if (string.Equals(format, "mkv", StringComparison.OrdinalIgnoreCase)) - { - return "mkv"; - } - if (string.Equals(format, "ts", StringComparison.OrdinalIgnoreCase)) - { - return "ts"; - } - - return "mkv"; - } + _assemblyInfo = assemblyInfo; } private bool CopySubtitles @@ -85,20 +67,14 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV public string GetOutputPath(MediaSourceInfo mediaSource, string targetFile) { - var extension = OutputFormat; - - if (string.Equals(extension, "mpegts", StringComparison.OrdinalIgnoreCase)) - { - extension = "ts"; - } - - return Path.ChangeExtension(targetFile, "." + extension); + return Path.ChangeExtension(targetFile, ".ts"); } public async Task Record(IDirectStreamProvider directStreamProvider, MediaSourceInfo mediaSource, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken) { - //var durationToken = new CancellationTokenSource(duration); - //cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token).Token; + // The media source is infinite so we need to handle stopping ourselves + var durationToken = new CancellationTokenSource(duration); + cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token).Token; await RecordFromFile(mediaSource, mediaSource.Path, targetFile, duration, onStarted, cancellationToken).ConfigureAwait(false); @@ -185,6 +161,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV videoArgs += " -fflags +genpts"; var durationParam = " -t " + _mediaEncoder.GetTimeParameter(duration.Ticks); + durationParam = string.Empty; var flags = new List<string>(); if (mediaSource.IgnoreDts) @@ -208,9 +185,9 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV } var videoStream = mediaSource.VideoStream; - var videoDecoder = videoStream == null ? null : new EncodingHelper(_mediaEncoder, _fileSystem, null).GetVideoDecoder(VideoType.VideoFile, videoStream, GetEncodingOptions()); + string videoDecoder = null; - if (!string.IsNullOrWhiteSpace(videoDecoder)) + if (!string.IsNullOrEmpty(videoDecoder)) { inputModifier += " " + videoDecoder; } @@ -222,7 +199,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV if (mediaSource.RequiresLooping) { - inputModifier += " -stream_loop -1"; + inputModifier += " -stream_loop -1 -reconnect_at_eof 1 -reconnect_streamed 1 -reconnect_delay_max 2"; } var analyzeDurationSeconds = 5; @@ -255,39 +232,27 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV var mediaStreams = mediaSource.MediaStreams ?? new List<MediaStream>(); var inputAudioCodec = mediaStreams.Where(i => i.Type == MediaStreamType.Audio).Select(i => i.Codec).FirstOrDefault() ?? string.Empty; - // do not copy aac because many players have difficulty with aac_latm - if (_liveTvOptions.EnableOriginalAudioWithEncodedRecordings && !string.Equals(inputAudioCodec, "aac", StringComparison.OrdinalIgnoreCase)) - { - return "-codec:a:0 copy"; - } + return "-codec:a:0 copy"; - var audioChannels = 2; - var audioStream = mediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Audio); - if (audioStream != null) - { - audioChannels = audioStream.Channels ?? audioChannels; - } - return "-codec:a:0 aac -strict experimental -ab 320000"; + //var audioChannels = 2; + //var audioStream = mediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Audio); + //if (audioStream != null) + //{ + // audioChannels = audioStream.Channels ?? audioChannels; + //} + //return "-codec:a:0 aac -strict experimental -ab 320000"; } private bool EncodeVideo(MediaSourceInfo mediaSource) { - var mediaStreams = mediaSource.MediaStreams ?? new List<MediaStream>(); - return !mediaStreams.Any(i => i.Type == MediaStreamType.Video && string.Equals(i.Codec, "h264", StringComparison.OrdinalIgnoreCase) && !i.IsInterlaced); + return false; } protected string GetOutputSizeParam() { var filters = new List<string>(); - - if (string.Equals(GetEncodingOptions().DeinterlaceMethod, "bobandweave", StringComparison.OrdinalIgnoreCase)) - { - filters.Add("yadif=1:-1:0"); - } - else - { - filters.Add("yadif=0:-1:0"); - } + + filters.Add("yadif=0:-1:0"); var output = string.Empty; diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EntryPoint.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EntryPoint.cs index 7c5f630a7..cc9e80a82 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/EntryPoint.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EntryPoint.cs @@ -12,7 +12,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV public void Dispose() { - GC.SuppressFinalize(this); } } } diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/RecordingHelper.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/RecordingHelper.cs index a5712b480..e694a8281 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/RecordingHelper.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/RecordingHelper.cs @@ -26,11 +26,18 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV } else if (info.OriginalAirDate.HasValue) { - name += " " + info.OriginalAirDate.Value.ToString("yyyy-MM-dd"); + if (info.OriginalAirDate.Value.Date.Equals(info.StartDate.Date)) + { + name += " " + GetDateString(info.StartDate); + } + else + { + name += " " + info.OriginalAirDate.Value.ToLocalTime().ToString("yyyy-MM-dd"); + } } else { - name += " " + DateTime.Now.ToString("yyyy-MM-dd"); + name += " " + GetDateString(info.StartDate); } if (!string.IsNullOrWhiteSpace(info.EpisodeTitle)) @@ -50,10 +57,24 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV } else { - name += " " + info.StartDate.ToString("yyyy-MM-dd"); + name += " " + GetDateString(info.StartDate); } return name; } + + private static string GetDateString(DateTime date) + { + date = date.ToLocalTime(); + + return string.Format("{0}_{1}_{2}_{3}_{4}_{5}", + date.Year.ToString("0000", CultureInfo.InvariantCulture), + date.Month.ToString("00", CultureInfo.InvariantCulture), + date.Day.ToString("00", CultureInfo.InvariantCulture), + date.Hour.ToString("00", CultureInfo.InvariantCulture), + date.Minute.ToString("00", CultureInfo.InvariantCulture), + date.Second.ToString("00", CultureInfo.InvariantCulture) + ); + } } } diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/SeriesTimerManager.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/SeriesTimerManager.cs index 843ba7e42..63cd26c7e 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/SeriesTimerManager.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/SeriesTimerManager.cs @@ -15,7 +15,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV public override void Add(SeriesTimerInfo item) { - if (string.IsNullOrWhiteSpace(item.Id)) + if (string.IsNullOrEmpty(item.Id)) { throw new ArgumentException("SeriesTimerInfo.Id cannot be null or empty."); } diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs index 380b24800..b5f93b882 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs @@ -13,6 +13,7 @@ using MediaBrowser.Controller.IO; using MediaBrowser.Model.IO; using MediaBrowser.Model.LiveTv; using MediaBrowser.Model.Threading; +using MediaBrowser.Model.System; namespace Emby.Server.Implementations.LiveTv.EmbyTV { @@ -23,12 +24,14 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV public event EventHandler<GenericEventArgs<TimerInfo>> TimerFired; private readonly ITimerFactory _timerFactory; + private readonly IPowerManagement _powerManagement; - public TimerManager(IFileSystem fileSystem, IJsonSerializer jsonSerializer, ILogger logger, string dataPath, ILogger logger1, ITimerFactory timerFactory) + public TimerManager(IFileSystem fileSystem, IJsonSerializer jsonSerializer, ILogger logger, string dataPath, ILogger logger1, ITimerFactory timerFactory, IPowerManagement powerManagement) : base(fileSystem, jsonSerializer, logger, dataPath, (r1, r2) => string.Equals(r1.Id, r2.Id, StringComparison.OrdinalIgnoreCase)) { _logger = logger1; _timerFactory = timerFactory; + _powerManagement = powerManagement; } public void RestartTimers() @@ -37,7 +40,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV foreach (var item in GetAll().ToList()) { - AddOrUpdateSystemTimer(item); + AddOrUpdateSystemTimer(item, false); } } @@ -60,7 +63,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV public override void Update(TimerInfo item) { base.Update(item); - AddOrUpdateSystemTimer(item); + AddOrUpdateSystemTimer(item, false); } public void AddOrUpdate(TimerInfo item, bool resetTimer) @@ -85,13 +88,13 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV public override void Add(TimerInfo item) { - if (string.IsNullOrWhiteSpace(item.Id)) + if (string.IsNullOrEmpty(item.Id)) { throw new ArgumentException("TimerInfo.Id cannot be null or empty."); } base.Add(item); - AddOrUpdateSystemTimer(item); + AddOrUpdateSystemTimer(item, true); } private bool ShouldStartTimer(TimerInfo item) @@ -105,7 +108,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV return true; } - private void AddOrUpdateSystemTimer(TimerInfo item) + private void AddOrUpdateSystemTimer(TimerInfo item, bool scheduleSystemWakeTimer) { StopTimer(item); @@ -125,6 +128,23 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV var dueTime = startDate - now; StartTimer(item, dueTime); + + if (scheduleSystemWakeTimer && dueTime >= TimeSpan.FromMinutes(15)) + { + ScheduleSystemWakeTimer(startDate, item.Name); + } + } + + private void ScheduleSystemWakeTimer(DateTime startDate, string displayName) + { + try + { + _powerManagement.ScheduleWake(startDate.AddMinutes(-5), displayName); + } + catch (Exception ex) + { + _logger.ErrorException("Error scheduling wake timer", ex); + } } private void StartTimer(TimerInfo item, TimeSpan dueTime) diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs index e210e2224..9021666a3 100644 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs +++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs @@ -16,6 +16,9 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Model.Extensions; +using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; namespace Emby.Server.Implementations.LiveTv.Listings { @@ -63,7 +66,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings public async Task<IEnumerable<ProgramInfo>> GetProgramsAsync(ListingsProviderInfo info, string channelId, DateTime startDateUtc, DateTime endDateUtc, CancellationToken cancellationToken) { - if (string.IsNullOrWhiteSpace(channelId)) + if (string.IsNullOrEmpty(channelId)) { throw new ArgumentNullException("channelId"); } @@ -75,7 +78,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings var token = await GetToken(info, cancellationToken).ConfigureAwait(false); - if (string.IsNullOrWhiteSpace(token)) + if (string.IsNullOrEmpty(token)) { _logger.Warn("SchedulesDirect token is empty, returning empty program list"); return programsInfo; @@ -246,8 +249,6 @@ namespace Emby.Server.Implementations.LiveTv.Listings DateTime endAt = startAt.AddSeconds(programInfo.duration); ProgramAudio audioType = ProgramAudio.Stereo; - bool repeat = programInfo.@new == null; - var programId = programInfo.programID ?? string.Empty; string newID = programId + "T" + startAt.Ticks + "C" + channelId; @@ -293,14 +294,17 @@ namespace Emby.Server.Implementations.LiveTv.Listings CommunityRating = null, EpisodeTitle = episodeTitle, Audio = audioType, - IsRepeat = repeat, + //IsNew = programInfo.@new ?? false, + IsRepeat = programInfo.@new == null, IsSeries = string.Equals(details.entityType, "episode", StringComparison.OrdinalIgnoreCase), ImageUrl = details.primaryImage, ThumbImageUrl = details.thumbImage, IsKids = string.Equals(details.audience, "children", StringComparison.OrdinalIgnoreCase), IsSports = string.Equals(details.entityType, "sports", StringComparison.OrdinalIgnoreCase), IsMovie = IsMovie(details), - Etag = programInfo.md5 + Etag = programInfo.md5, + IsLive = string.Equals(programInfo.liveTapeDelay, "live", StringComparison.OrdinalIgnoreCase), + IsPremiere = programInfo.premiere || (programInfo.isPremiereOrFinale ?? string.Empty).IndexOf("premiere", StringComparison.OrdinalIgnoreCase) != -1 }; var showId = programId; @@ -356,6 +360,8 @@ namespace Emby.Server.Implementations.LiveTv.Listings { info.SeriesId = programId.Substring(0, 10); + info.SeriesProviderIds[MetadataProviders.Zap2It.ToString()] = info.SeriesId; + if (details.metadata != null) { foreach (var metadataProgram in details.metadata) @@ -376,12 +382,21 @@ namespace Emby.Server.Implementations.LiveTv.Listings } } - if (!string.IsNullOrWhiteSpace(details.originalAirDate) && (!info.IsSeries || info.IsRepeat)) + if (!string.IsNullOrWhiteSpace(details.originalAirDate)) { info.OriginalAirDate = DateTime.Parse(details.originalAirDate); info.ProductionYear = info.OriginalAirDate.Value.Year; } + if (details.movie != null) + { + int year; + if (!string.IsNullOrEmpty(details.movie.year) && int.TryParse(details.movie.year, out year)) + { + info.ProductionYear = year; + } + } + if (details.genres != null) { info.Genres = details.genres.Where(g => !string.IsNullOrWhiteSpace(g)).ToList(); @@ -506,8 +521,8 @@ namespace Emby.Server.Implementations.LiveTv.Listings { using (var innerResponse2 = await Post(httpOptions, true, info).ConfigureAwait(false)) { - return _jsonSerializer.DeserializeFromStream<List<ScheduleDirect.ShowImages>>( - innerResponse2.Content); + return await _jsonSerializer.DeserializeFromStreamAsync<List<ScheduleDirect.ShowImages>>( + innerResponse2.Content).ConfigureAwait(false); } } catch (Exception ex) @@ -545,7 +560,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings { using (Stream responce = httpResponse.Content) { - var root = _jsonSerializer.DeserializeFromStream<List<ScheduleDirect.Headends>>(responce); + var root = await _jsonSerializer.DeserializeFromStreamAsync<List<ScheduleDirect.Headends>>(responce).ConfigureAwait(false); if (root != null) { @@ -589,7 +604,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings } var password = info.Password; - if (string.IsNullOrWhiteSpace(password)) + if (string.IsNullOrEmpty(password)) { return null; } @@ -607,7 +622,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings _tokens.TryAdd(username, savedToken); } - if (!string.IsNullOrWhiteSpace(savedToken.Name) && !string.IsNullOrWhiteSpace(savedToken.Value)) + if (!string.IsNullOrEmpty(savedToken.Name) && !string.IsNullOrEmpty(savedToken.Value)) { long ticks; if (long.TryParse(savedToken.Value, NumberStyles.Any, CultureInfo.InvariantCulture, out ticks)) @@ -740,7 +755,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings using (var responce = await Post(httpOptions, false, null).ConfigureAwait(false)) { - var root = _jsonSerializer.DeserializeFromStream<ScheduleDirect.Token>(responce.Content); + var root = await _jsonSerializer.DeserializeFromStreamAsync<ScheduleDirect.Token>(responce.Content).ConfigureAwait(false); if (root.message == "OK") { _logger.Info("Authenticated with Schedules Direct token: " + root.token); @@ -755,12 +770,12 @@ namespace Emby.Server.Implementations.LiveTv.Listings { var token = await GetToken(info, cancellationToken); - if (string.IsNullOrWhiteSpace(token)) + if (string.IsNullOrEmpty(token)) { throw new ArgumentException("Authentication required."); } - if (string.IsNullOrWhiteSpace(info.ListingsId)) + if (string.IsNullOrEmpty(info.ListingsId)) { throw new ArgumentException("Listings Id required"); } @@ -796,14 +811,14 @@ namespace Emby.Server.Implementations.LiveTv.Listings private async Task<bool> HasLineup(ListingsProviderInfo info, CancellationToken cancellationToken) { - if (string.IsNullOrWhiteSpace(info.ListingsId)) + if (string.IsNullOrEmpty(info.ListingsId)) { throw new ArgumentException("Listings Id required"); } var token = await GetToken(info, cancellationToken); - if (string.IsNullOrWhiteSpace(token)) + if (string.IsNullOrEmpty(token)) { throw new Exception("token required"); } @@ -826,7 +841,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings { using (var response = httpResponse.Content) { - var root = _jsonSerializer.DeserializeFromStream<ScheduleDirect.Lineups>(response); + var root = await _jsonSerializer.DeserializeFromStreamAsync<ScheduleDirect.Lineups>(response).ConfigureAwait(false); return root.lineups.Any(i => string.Equals(info.ListingsId, i.lineup, StringComparison.OrdinalIgnoreCase)); } @@ -848,18 +863,18 @@ namespace Emby.Server.Implementations.LiveTv.Listings { if (validateLogin) { - if (string.IsNullOrWhiteSpace(info.Username)) + if (string.IsNullOrEmpty(info.Username)) { throw new ArgumentException("Username is required"); } - if (string.IsNullOrWhiteSpace(info.Password)) + if (string.IsNullOrEmpty(info.Password)) { throw new ArgumentException("Password is required"); } } if (validateListings) { - if (string.IsNullOrWhiteSpace(info.ListingsId)) + if (string.IsNullOrEmpty(info.ListingsId)) { throw new ArgumentException("Listings Id required"); } @@ -881,14 +896,14 @@ namespace Emby.Server.Implementations.LiveTv.Listings public async Task<List<ChannelInfo>> GetChannels(ListingsProviderInfo info, CancellationToken cancellationToken) { var listingsId = info.ListingsId; - if (string.IsNullOrWhiteSpace(listingsId)) + if (string.IsNullOrEmpty(listingsId)) { throw new Exception("ListingsId required"); } var token = await GetToken(info, cancellationToken); - if (string.IsNullOrWhiteSpace(token)) + if (string.IsNullOrEmpty(token)) { throw new Exception("token required"); } @@ -911,7 +926,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings { using (var response = httpResponse.Content) { - var root = _jsonSerializer.DeserializeFromStream<ScheduleDirect.Channel>(response); + var root = await _jsonSerializer.DeserializeFromStreamAsync<ScheduleDirect.Channel>(response).ConfigureAwait(false); _logger.Info("Found " + root.map.Count + " channels on the lineup on ScheduleDirect"); _logger.Info("Mapping Stations to Channel"); @@ -951,7 +966,6 @@ namespace Emby.Server.Implementations.LiveTv.Listings if (station.logo != null) { channelInfo.ImageUrl = station.logo.URL; - channelInfo.HasImage = true; } } @@ -1112,6 +1126,10 @@ namespace Emby.Server.Implementations.LiveTv.Listings public List<Rating> ratings { get; set; } public bool? @new { get; set; } public Multipart multipart { get; set; } + public string liveTapeDelay { get; set; } + public bool premiere { get; set; } + public bool repeat { get; set; } + public string isPremiereOrFinale { get; set; } } diff --git a/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs b/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs deleted file mode 100644 index 7c251e303..000000000 --- a/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs +++ /dev/null @@ -1,315 +0,0 @@ -using MediaBrowser.Controller.LiveTv; -using MediaBrowser.Model.Dto; -using MediaBrowser.Model.LiveTv; -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.IO.Compression; -using System.Linq; -using System.Net; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using Emby.XmlTv.Classes; -using Emby.XmlTv.Entities; -using MediaBrowser.Common.Extensions; -using MediaBrowser.Common.Net; -using MediaBrowser.Common.Progress; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.Logging; - -namespace Emby.Server.Implementations.LiveTv.Listings -{ - public class XmlTvListingsProvider : IListingsProvider - { - private readonly IServerConfigurationManager _config; - private readonly IHttpClient _httpClient; - private readonly ILogger _logger; - private readonly IFileSystem _fileSystem; - private readonly IZipClient _zipClient; - - public XmlTvListingsProvider(IServerConfigurationManager config, IHttpClient httpClient, ILogger logger, IFileSystem fileSystem, IZipClient zipClient) - { - _config = config; - _httpClient = httpClient; - _logger = logger; - _fileSystem = fileSystem; - _zipClient = zipClient; - } - - public string Name - { - get { return "XmlTV"; } - } - - public string Type - { - get { return "xmltv"; } - } - - private string GetLanguage(ListingsProviderInfo info) - { - if (!string.IsNullOrWhiteSpace(info.PreferredLanguage)) - { - return info.PreferredLanguage; - } - - return _config.Configuration.PreferredMetadataLanguage; - } - - private async Task<string> GetXml(string path, CancellationToken cancellationToken) - { - _logger.Info("xmltv path: {0}", path); - - if (!path.StartsWith("http", StringComparison.OrdinalIgnoreCase)) - { - return UnzipIfNeeded(path, path); - } - - var cacheFilename = DateTime.UtcNow.DayOfYear.ToString(CultureInfo.InvariantCulture) + "-" + DateTime.UtcNow.Hour.ToString(CultureInfo.InvariantCulture) + ".xml"; - var cacheFile = Path.Combine(_config.ApplicationPaths.CachePath, "xmltv", cacheFilename); - if (_fileSystem.FileExists(cacheFile)) - { - return UnzipIfNeeded(path, cacheFile); - } - - _logger.Info("Downloading xmltv listings from {0}", path); - - var tempFile = await _httpClient.GetTempFile(new HttpRequestOptions - { - CancellationToken = cancellationToken, - Url = path, - Progress = new SimpleProgress<Double>(), - DecompressionMethod = CompressionMethod.Gzip, - - // It's going to come back gzipped regardless of this value - // So we need to make sure the decompression method is set to gzip - EnableHttpCompression = true, - - UserAgent = "Emby/3.0" - - }).ConfigureAwait(false); - - _fileSystem.CreateDirectory(_fileSystem.GetDirectoryName(cacheFile)); - - _fileSystem.CopyFile(tempFile, cacheFile, true); - - return UnzipIfNeeded(path, cacheFile); - } - - private string UnzipIfNeeded(string originalUrl, string file) - { - var ext = Path.GetExtension(originalUrl.Split('?')[0]); - - if (string.Equals(ext, ".gz", StringComparison.OrdinalIgnoreCase)) - { - try - { - var tempFolder = ExtractGz(file); - return FindXmlFile(tempFolder); - } - catch (Exception ex) - { - //_logger.ErrorException("Error extracting from gz file {0}", ex, file); - } - - try - { - var tempFolder = ExtractFirstFileFromGz(file); - return FindXmlFile(tempFolder); - } - catch (Exception ex) - { - //_logger.ErrorException("Error extracting from zip file {0}", ex, file); - } - } - - return file; - } - - private string ExtractFirstFileFromGz(string file) - { - using (var stream = _fileSystem.OpenRead(file)) - { - var tempFolder = Path.Combine(_config.ApplicationPaths.TempDirectory, Guid.NewGuid().ToString()); - _fileSystem.CreateDirectory(tempFolder); - - _zipClient.ExtractFirstFileFromGz(stream, tempFolder, "data.xml"); - - return tempFolder; - } - } - - private string ExtractGz(string file) - { - using (var stream = _fileSystem.OpenRead(file)) - { - var tempFolder = Path.Combine(_config.ApplicationPaths.TempDirectory, Guid.NewGuid().ToString()); - _fileSystem.CreateDirectory(tempFolder); - - _zipClient.ExtractAllFromGz(stream, tempFolder, true); - - return tempFolder; - } - } - - private string FindXmlFile(string directory) - { - return _fileSystem.GetFiles(directory, true) - .Where(i => string.Equals(i.Extension, ".xml", StringComparison.OrdinalIgnoreCase)) - .Select(i => i.FullName) - .FirstOrDefault(); - } - - public async Task<IEnumerable<ProgramInfo>> GetProgramsAsync(ListingsProviderInfo info, string channelId, DateTime startDateUtc, DateTime endDateUtc, CancellationToken cancellationToken) - { - if (string.IsNullOrWhiteSpace(channelId)) - { - throw new ArgumentNullException("channelId"); - } - - if (!await EmbyTV.EmbyTVRegistration.Instance.EnableXmlTv().ConfigureAwait(false)) - { - var length = endDateUtc - startDateUtc; - if (length.TotalDays > 1) - { - endDateUtc = startDateUtc.AddDays(1); - } - } - - _logger.Debug("Getting xmltv programs for channel {0}", channelId); - - var path = await GetXml(info.Path, cancellationToken).ConfigureAwait(false); - _logger.Debug("Opening XmlTvReader for {0}", path); - var reader = new XmlTvReader(path, GetLanguage(info)); - - var results = reader.GetProgrammes(channelId, startDateUtc, endDateUtc, cancellationToken); - return results.Select(p => GetProgramInfo(p, info)); - } - - private ProgramInfo GetProgramInfo(XmlTvProgram p, ListingsProviderInfo info) - { - var episodeTitle = p.Episode == null ? null : p.Episode.Title; - - var programInfo = new ProgramInfo - { - ChannelId = p.ChannelId, - EndDate = GetDate(p.EndDate), - EpisodeNumber = p.Episode == null ? null : p.Episode.Episode, - EpisodeTitle = episodeTitle, - Genres = p.Categories, - StartDate = GetDate(p.StartDate), - Name = p.Title, - Overview = p.Description, - ProductionYear = !p.CopyrightDate.HasValue ? (int?)null : p.CopyrightDate.Value.Year, - SeasonNumber = p.Episode == null ? null : p.Episode.Series, - IsSeries = p.Episode != null, - IsRepeat = p.IsPreviouslyShown && !p.IsNew, - IsPremiere = p.Premiere != null, - IsKids = p.Categories.Any(c => info.KidsCategories.Contains(c, StringComparer.OrdinalIgnoreCase)), - IsMovie = p.Categories.Any(c => info.MovieCategories.Contains(c, StringComparer.OrdinalIgnoreCase)), - IsNews = p.Categories.Any(c => info.NewsCategories.Contains(c, StringComparer.OrdinalIgnoreCase)), - IsSports = p.Categories.Any(c => info.SportsCategories.Contains(c, StringComparer.OrdinalIgnoreCase)), - ImageUrl = p.Icon != null && !String.IsNullOrEmpty(p.Icon.Source) ? p.Icon.Source : null, - HasImage = p.Icon != null && !String.IsNullOrEmpty(p.Icon.Source), - OfficialRating = p.Rating != null && !String.IsNullOrEmpty(p.Rating.Value) ? p.Rating.Value : null, - CommunityRating = p.StarRating.HasValue ? p.StarRating.Value : (float?)null, - SeriesId = p.Episode != null ? p.Title.GetMD5().ToString("N") : null - }; - - if (!string.IsNullOrWhiteSpace(p.ProgramId)) - { - programInfo.ShowId = p.ProgramId; - } - else - { - var uniqueString = (p.Title ?? string.Empty) + (episodeTitle ?? string.Empty) + (p.IceTvEpisodeNumber ?? string.Empty); - - if (programInfo.SeasonNumber.HasValue) - { - uniqueString = "-" + programInfo.SeasonNumber.Value.ToString(CultureInfo.InvariantCulture); - } - if (programInfo.EpisodeNumber.HasValue) - { - uniqueString = "-" + programInfo.EpisodeNumber.Value.ToString(CultureInfo.InvariantCulture); - } - - programInfo.ShowId = uniqueString.GetMD5().ToString("N"); - - // If we don't have valid episode info, assume it's a unique program, otherwise recordings might be skipped - if (programInfo.IsSeries && !programInfo.IsRepeat) - { - if ((programInfo.EpisodeNumber ?? 0) == 0) - { - programInfo.ShowId = programInfo.ShowId + programInfo.StartDate.Ticks.ToString(CultureInfo.InvariantCulture); - } - } - } - - // Construct an id from the channel and start date - programInfo.Id = String.Format("{0}_{1:O}", p.ChannelId, p.StartDate); - - if (programInfo.IsMovie) - { - programInfo.IsSeries = false; - programInfo.EpisodeNumber = null; - programInfo.EpisodeTitle = null; - } - - return programInfo; - } - - private DateTime GetDate(DateTime date) - { - if (date.Kind != DateTimeKind.Utc) - { - date = DateTime.SpecifyKind(date, DateTimeKind.Utc); - } - return date; - } - - public Task Validate(ListingsProviderInfo info, bool validateLogin, bool validateListings) - { - // Assume all urls are valid. check files for existence - if (!info.Path.StartsWith("http", StringComparison.OrdinalIgnoreCase) && !_fileSystem.FileExists(info.Path)) - { - throw new FileNotFoundException("Could not find the XmlTv file specified:", info.Path); - } - - return Task.FromResult(true); - } - - public async Task<List<NameIdPair>> GetLineups(ListingsProviderInfo info, string country, string location) - { - // In theory this should never be called because there is always only one lineup - var path = await GetXml(info.Path, CancellationToken.None).ConfigureAwait(false); - _logger.Debug("Opening XmlTvReader for {0}", path); - var reader = new XmlTvReader(path, GetLanguage(info)); - var results = reader.GetChannels(); - - // Should this method be async? - return results.Select(c => new NameIdPair() { Id = c.Id, Name = c.DisplayName }).ToList(); - } - - public async Task<List<ChannelInfo>> GetChannels(ListingsProviderInfo info, CancellationToken cancellationToken) - { - // In theory this should never be called because there is always only one lineup - var path = await GetXml(info.Path, cancellationToken).ConfigureAwait(false); - _logger.Debug("Opening XmlTvReader for {0}", path); - var reader = new XmlTvReader(path, GetLanguage(info)); - var results = reader.GetChannels(); - - // Should this method be async? - return results.Select(c => new ChannelInfo - { - Id = c.Id, - Name = c.DisplayName, - ImageUrl = c.Icon != null && !String.IsNullOrEmpty(c.Icon.Source) ? c.Icon.Source : null, - Number = string.IsNullOrWhiteSpace(c.Number) ? c.Id : c.Number - - }).ToList(); - } - } -}
\ No newline at end of file diff --git a/Emby.Server.Implementations/LiveTv/LiveStreamHelper.cs b/Emby.Server.Implementations/LiveTv/LiveStreamHelper.cs deleted file mode 100644 index 143350a8b..000000000 --- a/Emby.Server.Implementations/LiveTv/LiveStreamHelper.cs +++ /dev/null @@ -1,113 +0,0 @@ -using System; -using System.Globalization; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Controller.MediaEncoding; -using MediaBrowser.Model.Dlna; -using MediaBrowser.Model.Dto; -using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Logging; - -namespace Emby.Server.Implementations.LiveTv -{ - public class LiveStreamHelper - { - private readonly IMediaEncoder _mediaEncoder; - private readonly ILogger _logger; - - const int ProbeAnalyzeDurationMs = 3000; - const int PlaybackAnalyzeDurationMs = 3000; - - public LiveStreamHelper(IMediaEncoder mediaEncoder, ILogger logger) - { - _mediaEncoder = mediaEncoder; - _logger = logger; - } - - public async Task AddMediaInfoWithProbe(MediaSourceInfo mediaSource, bool isAudio, CancellationToken cancellationToken) - { - var originalRuntime = mediaSource.RunTimeTicks; - - var now = DateTime.UtcNow; - - var info = await _mediaEncoder.GetMediaInfo(new MediaInfoRequest - { - InputPath = mediaSource.Path, - Protocol = mediaSource.Protocol, - MediaType = isAudio ? DlnaProfileType.Audio : DlnaProfileType.Video, - ExtractChapters = false, - AnalyzeDurationMs = ProbeAnalyzeDurationMs - - }, cancellationToken).ConfigureAwait(false); - - _logger.Info("Live tv media info probe took {0} seconds", (DateTime.UtcNow - now).TotalSeconds.ToString(CultureInfo.InvariantCulture)); - - mediaSource.Bitrate = info.Bitrate; - mediaSource.Container = info.Container; - mediaSource.Formats = info.Formats; - mediaSource.MediaStreams = info.MediaStreams; - mediaSource.RunTimeTicks = info.RunTimeTicks; - mediaSource.Size = info.Size; - mediaSource.Timestamp = info.Timestamp; - mediaSource.Video3DFormat = info.Video3DFormat; - mediaSource.VideoType = info.VideoType; - - mediaSource.DefaultSubtitleStreamIndex = null; - - // Null this out so that it will be treated like a live stream - if (!originalRuntime.HasValue) - { - mediaSource.RunTimeTicks = null; - } - - var audioStream = mediaSource.MediaStreams.FirstOrDefault(i => i.Type == MediaBrowser.Model.Entities.MediaStreamType.Audio); - - if (audioStream == null || audioStream.Index == -1) - { - mediaSource.DefaultAudioStreamIndex = null; - } - else - { - mediaSource.DefaultAudioStreamIndex = audioStream.Index; - } - - var videoStream = mediaSource.MediaStreams.FirstOrDefault(i => i.Type == MediaBrowser.Model.Entities.MediaStreamType.Video); - if (videoStream != null) - { - if (!videoStream.BitRate.HasValue) - { - var width = videoStream.Width ?? 1920; - - if (width >= 3000) - { - videoStream.BitRate = 30000000; - } - - else if (width >= 1900) - { - videoStream.BitRate = 20000000; - } - - else if (width >= 1200) - { - videoStream.BitRate = 8000000; - } - - else if (width >= 700) - { - videoStream.BitRate = 2000000; - } - } - - // This is coming up false and preventing stream copy - videoStream.IsAVC = null; - } - - // Try to estimate this - mediaSource.InferTotalBitrate(true); - - mediaSource.AnalyzeDurationMs = PlaybackAnalyzeDurationMs; - } - } -} diff --git a/Emby.Server.Implementations/LiveTv/LiveTvConfigurationFactory.cs b/Emby.Server.Implementations/LiveTv/LiveTvConfigurationFactory.cs index 2be642737..205a767eb 100644 --- a/Emby.Server.Implementations/LiveTv/LiveTvConfigurationFactory.cs +++ b/Emby.Server.Implementations/LiveTv/LiveTvConfigurationFactory.cs @@ -8,7 +8,7 @@ namespace Emby.Server.Implementations.LiveTv { public IEnumerable<ConfigurationStore> GetConfigurations() { - return new List<ConfigurationStore> + return new ConfigurationStore[] { new ConfigurationStore { diff --git a/Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs b/Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs index 15bbca136..56b3b5e4b 100644 --- a/Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs +++ b/Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs @@ -15,6 +15,7 @@ using System.Threading.Tasks; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Extensions; +using System.Collections.Generic; namespace Emby.Server.Implementations.LiveTv { @@ -36,19 +37,19 @@ namespace Emby.Server.Implementations.LiveTv _libraryManager = libraryManager; } - public TimerInfoDto GetTimerInfoDto(TimerInfo info, ILiveTvService service, LiveTvProgram program, LiveTvChannel channel) + public TimerInfoDto GetTimerInfoDto(TimerInfo info, ILiveTvService service, LiveTvProgram program, BaseItem channel) { var dto = new TimerInfoDto { - Id = GetInternalTimerId(service.Name, info.Id).ToString("N"), + Id = GetInternalTimerId(info.Id), Overview = info.Overview, EndDate = info.EndDate, Name = info.Name, StartDate = info.StartDate, ExternalId = info.Id, - ChannelId = GetInternalChannelId(service.Name, info.ChannelId).ToString("N"), + ChannelId = GetInternalChannelId(service.Name, info.ChannelId), Status = info.Status, - SeriesTimerId = string.IsNullOrEmpty(info.SeriesTimerId) ? null : GetInternalSeriesTimerId(service.Name, info.SeriesTimerId).ToString("N"), + SeriesTimerId = string.IsNullOrEmpty(info.SeriesTimerId) ? null : GetInternalSeriesTimerId(info.SeriesTimerId).ToString("N"), PrePaddingSeconds = info.PrePaddingSeconds, PostPaddingSeconds = info.PostPaddingSeconds, IsPostPaddingRequired = info.IsPostPaddingRequired, @@ -65,7 +66,7 @@ namespace Emby.Server.Implementations.LiveTv if (!string.IsNullOrEmpty(info.ProgramId)) { - dto.ProgramId = GetInternalProgramId(service.Name, info.ProgramId).ToString("N"); + dto.ProgramId = GetInternalProgramId(info.ProgramId).ToString("N"); } if (program != null) @@ -80,7 +81,7 @@ namespace Emby.Server.Implementations.LiveTv dto.ProgramInfo.SeriesTimerId = dto.SeriesTimerId; - if (!string.IsNullOrWhiteSpace(info.SeriesTimerId)) + if (!string.IsNullOrEmpty(info.SeriesTimerId)) { FillImages(dto.ProgramInfo, info.Name, info.SeriesId); } @@ -103,7 +104,7 @@ namespace Emby.Server.Implementations.LiveTv { var dto = new SeriesTimerInfoDto { - Id = GetInternalSeriesTimerId(service.Name, info.Id).ToString("N"), + Id = GetInternalSeriesTimerId(info.Id).ToString("N"), Overview = info.Overview, EndDate = info.EndDate, Name = info.Name, @@ -130,12 +131,12 @@ namespace Emby.Server.Implementations.LiveTv if (!string.IsNullOrEmpty(info.ChannelId)) { - dto.ChannelId = GetInternalChannelId(service.Name, info.ChannelId).ToString("N"); + dto.ChannelId = GetInternalChannelId(service.Name, info.ChannelId); } if (!string.IsNullOrEmpty(info.ProgramId)) { - dto.ProgramId = GetInternalProgramId(service.Name, info.ProgramId).ToString("N"); + dto.ProgramId = GetInternalProgramId(info.ProgramId).ToString("N"); } dto.DayPattern = info.Days == null ? null : GetDayPattern(info.Days.ToArray(info.Days.Count)); @@ -188,49 +189,47 @@ namespace Emby.Server.Implementations.LiveTv } } - if (!string.IsNullOrWhiteSpace(programSeriesId)) + var program = _libraryManager.GetItemList(new InternalItemsQuery { - var program = _libraryManager.GetItemList(new InternalItemsQuery - { - IncludeItemTypes = new string[] { typeof(LiveTvProgram).Name }, - ExternalSeriesId = programSeriesId, - Limit = 1, - ImageTypes = new ImageType[] { ImageType.Primary }, - DtoOptions = new DtoOptions(false) + IncludeItemTypes = new string[] { typeof(LiveTvProgram).Name }, + ExternalSeriesId = programSeriesId, + Limit = 1, + ImageTypes = new ImageType[] { ImageType.Primary }, + DtoOptions = new DtoOptions(false), + Name = string.IsNullOrEmpty(programSeriesId) ? seriesName : null - }).FirstOrDefault(); + }).FirstOrDefault(); - if (program != null) + if (program != null) + { + var image = program.GetImageInfo(ImageType.Primary, 0); + if (image != null) + { + try + { + dto.ParentPrimaryImageTag = _imageProcessor.GetImageCacheTag(program, image); + dto.ParentPrimaryImageItemId = program.Id.ToString("N"); + } + catch (Exception ex) + { + } + } + + if (dto.ParentBackdropImageTags == null || dto.ParentBackdropImageTags.Length == 0) { - var image = program.GetImageInfo(ImageType.Primary, 0); + image = program.GetImageInfo(ImageType.Backdrop, 0); if (image != null) { try { - dto.ParentPrimaryImageTag = _imageProcessor.GetImageCacheTag(program, image); - dto.ParentPrimaryImageItemId = program.Id.ToString("N"); - } - catch (Exception ex) + dto.ParentBackdropImageTags = new string[] { + _imageProcessor.GetImageCacheTag(program, image) + }; + dto.ParentBackdropItemId = program.Id.ToString("N"); } - } - - if (dto.ParentBackdropImageTags == null || dto.ParentBackdropImageTags.Length == 0) - { - image = program.GetImageInfo(ImageType.Backdrop, 0); - if (image != null) + catch (Exception ex) { - try - { - dto.ParentBackdropImageTags = new string[] - { - _imageProcessor.GetImageCacheTag(program, image) - }; - dto.ParentBackdropItemId = program.Id.ToString("N"); - } - catch (Exception ex) - { - } } } } @@ -280,59 +279,62 @@ namespace Emby.Server.Implementations.LiveTv } } - if (!string.IsNullOrWhiteSpace(programSeriesId)) + var program = _libraryManager.GetItemList(new InternalItemsQuery { - var program = _libraryManager.GetItemList(new InternalItemsQuery - { - IncludeItemTypes = new string[] { typeof(Series).Name }, - Name = seriesName, - Limit = 1, - ImageTypes = new ImageType[] { ImageType.Primary }, - DtoOptions = new DtoOptions(false) + IncludeItemTypes = new string[] { typeof(Series).Name }, + Name = seriesName, + Limit = 1, + ImageTypes = new ImageType[] { ImageType.Primary }, + DtoOptions = new DtoOptions(false) + + }).FirstOrDefault(); - }).FirstOrDefault() ?? _libraryManager.GetItemList(new InternalItemsQuery + if (program == null) + { + program = _libraryManager.GetItemList(new InternalItemsQuery { IncludeItemTypes = new string[] { typeof(LiveTvProgram).Name }, ExternalSeriesId = programSeriesId, Limit = 1, ImageTypes = new ImageType[] { ImageType.Primary }, - DtoOptions = new DtoOptions(false) + DtoOptions = new DtoOptions(false), + Name = string.IsNullOrEmpty(programSeriesId) ? seriesName : null }).FirstOrDefault(); + } - if (program != null) + if (program != null) + { + var image = program.GetImageInfo(ImageType.Primary, 0); + if (image != null) { - var image = program.GetImageInfo(ImageType.Primary, 0); + try + { + dto.ParentPrimaryImageTag = _imageProcessor.GetImageCacheTag(program, image); + dto.ParentPrimaryImageItemId = program.Id.ToString("N"); + } + catch (Exception ex) + { + } + } + + if (dto.ParentBackdropImageTags == null || dto.ParentBackdropImageTags.Length == 0) + { + image = program.GetImageInfo(ImageType.Backdrop, 0); if (image != null) { try { - dto.ParentPrimaryImageTag = _imageProcessor.GetImageCacheTag(program, image); - dto.ParentPrimaryImageItemId = program.Id.ToString("N"); + dto.ParentBackdropImageTags = new string[] + { + _imageProcessor.GetImageCacheTag(program, image) + }; + dto.ParentBackdropItemId = program.Id.ToString("N"); } catch (Exception ex) { } } - - if (dto.ParentBackdropImageTags == null || dto.ParentBackdropImageTags.Length == 0) - { - image = program.GetImageInfo(ImageType.Backdrop, 0); - if (image != null) - { - try - { - dto.ParentBackdropImageTags = new string[] - { - _imageProcessor.GetImageCacheTag(program, image) - }; - dto.ParentBackdropItemId = program.Id.ToString("N"); - } - catch (Exception ex) - { - } - } - } } } } @@ -366,35 +368,7 @@ namespace Emby.Server.Implementations.LiveTv return pattern; } - public LiveTvTunerInfoDto GetTunerInfoDto(string serviceName, LiveTvTunerInfo info, string channelName) - { - var dto = new LiveTvTunerInfoDto - { - Name = info.Name, - Id = info.Id, - Clients = info.Clients.ToArray(), - ProgramName = info.ProgramName, - SourceType = info.SourceType, - Status = info.Status, - ChannelName = channelName, - Url = info.Url, - CanReset = info.CanReset - }; - - if (!string.IsNullOrEmpty(info.ChannelId)) - { - dto.ChannelId = GetInternalChannelId(serviceName, info.ChannelId).ToString("N"); - } - - if (!string.IsNullOrEmpty(info.RecordingId)) - { - dto.RecordingId = GetInternalRecordingId(serviceName, info.RecordingId).ToString("N"); - } - - return dto; - } - - internal string GetImageTag(IHasMetadata info) + internal string GetImageTag(BaseItem info) { try { @@ -417,46 +391,28 @@ namespace Emby.Server.Implementations.LiveTv return _libraryManager.GetNewItemId(name.ToLower(), typeof(LiveTvChannel)); } - public Guid GetInternalTimerId(string serviceName, string externalId) + private const string ServiceName = "Emby"; + public string GetInternalTimerId(string externalId) { - var name = serviceName + externalId + InternalVersionNumber; + var name = ServiceName + externalId + InternalVersionNumber; - return name.ToLower().GetMD5(); + return name.ToLower().GetMD5().ToString("N"); } - public Guid GetInternalSeriesTimerId(string serviceName, string externalId) + public Guid GetInternalSeriesTimerId(string externalId) { - var name = serviceName + externalId + InternalVersionNumber; + var name = ServiceName + externalId + InternalVersionNumber; return name.ToLower().GetMD5(); } - public Guid GetInternalProgramId(string serviceName, string externalId) + public Guid GetInternalProgramId(string externalId) { - var name = serviceName + externalId + InternalVersionNumber; + var name = ServiceName + externalId + InternalVersionNumber; return _libraryManager.GetNewItemId(name.ToLower(), typeof(LiveTvProgram)); } - public Guid GetInternalRecordingId(string serviceName, string externalId) - { - var name = serviceName + externalId + InternalVersionNumber + "0"; - - return _libraryManager.GetNewItemId(name.ToLower(), typeof(ILiveTvRecording)); - } - - private string GetItemExternalId(BaseItem item) - { - var externalId = item.ExternalId; - - if (string.IsNullOrWhiteSpace(externalId)) - { - externalId = item.GetProviderId("ProviderExternalId"); - } - - return externalId; - } - public async Task<TimerInfo> GetTimerInfo(TimerInfoDto dto, bool isNew, LiveTvManager liveTv, CancellationToken cancellationToken) { var info = new TimerInfo @@ -486,23 +442,23 @@ namespace Emby.Server.Implementations.LiveTv info.Id = timer.ExternalId; } - if (!string.IsNullOrEmpty(dto.ChannelId) && string.IsNullOrEmpty(info.ChannelId)) + if (!dto.ChannelId.Equals(Guid.Empty) && string.IsNullOrEmpty(info.ChannelId)) { - var channel = liveTv.GetInternalChannel(dto.ChannelId); + var channel = _libraryManager.GetItemById(dto.ChannelId); if (channel != null) { - info.ChannelId = GetItemExternalId(channel); + info.ChannelId = channel.ExternalId; } } if (!string.IsNullOrEmpty(dto.ProgramId) && string.IsNullOrEmpty(info.ProgramId)) { - var program = liveTv.GetInternalProgram(dto.ProgramId); + var program = _libraryManager.GetItemById(dto.ProgramId); if (program != null) { - info.ProgramId = GetItemExternalId(program); + info.ProgramId = program.ExternalId; } } @@ -552,23 +508,23 @@ namespace Emby.Server.Implementations.LiveTv info.Id = timer.ExternalId; } - if (!string.IsNullOrEmpty(dto.ChannelId) && string.IsNullOrEmpty(info.ChannelId)) + if (!dto.ChannelId.Equals(Guid.Empty) && string.IsNullOrEmpty(info.ChannelId)) { - var channel = liveTv.GetInternalChannel(dto.ChannelId); + var channel = _libraryManager.GetItemById(dto.ChannelId); if (channel != null) { - info.ChannelId = GetItemExternalId(channel); + info.ChannelId = channel.ExternalId; } } if (!string.IsNullOrEmpty(dto.ProgramId) && string.IsNullOrEmpty(info.ProgramId)) { - var program = liveTv.GetInternalProgram(dto.ProgramId); + var program = _libraryManager.GetItemById(dto.ProgramId); if (program != null) { - info.ProgramId = GetItemExternalId(program); + info.ProgramId = program.ExternalId; } } diff --git a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs index 211e0de4b..9cdf105d7 100644 --- a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs +++ b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs @@ -26,7 +26,6 @@ using System.Threading; using System.Threading.Tasks; using MediaBrowser.Model.IO; using MediaBrowser.Common.Events; - using MediaBrowser.Common.Security; using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Entities.TV; @@ -36,6 +35,10 @@ using MediaBrowser.Model.Extensions; using MediaBrowser.Model.Globalization; using MediaBrowser.Model.Tasks; using Emby.Server.Implementations.LiveTv.Listings; +using MediaBrowser.Controller.Channels; +using Emby.Server.Implementations.Library; +using MediaBrowser.Controller; +using MediaBrowser.Common.Net; namespace Emby.Server.Implementations.LiveTv { @@ -54,6 +57,7 @@ namespace Emby.Server.Implementations.LiveTv private readonly IJsonSerializer _jsonSerializer; private readonly IProviderManager _providerManager; private readonly ISecurityManager _security; + private readonly Func<IChannelManager> _channelManager; private readonly IDtoService _dtoService; private readonly ILocalizationManager _localization; @@ -62,10 +66,8 @@ namespace Emby.Server.Implementations.LiveTv private ILiveTvService[] _services = new ILiveTvService[] { }; - private readonly SemaphoreSlim _refreshRecordingsLock = new SemaphoreSlim(1, 1); - - private readonly List<ITunerHost> _tunerHosts = new List<ITunerHost>(); - private readonly List<IListingsProvider> _listingProviders = new List<IListingsProvider>(); + private ITunerHost[] _tunerHosts = Array.Empty<ITunerHost>(); + private IListingsProvider[] _listingProviders = Array.Empty<IListingsProvider>(); private readonly IFileSystem _fileSystem; public event EventHandler<GenericEventArgs<TimerEventInfo>> SeriesTimerCancelled; @@ -78,13 +80,12 @@ namespace Emby.Server.Implementations.LiveTv return EmbyTV.EmbyTV.Current.GetActiveRecordingPath(id); } - public Task<ILiveStream> GetEmbyTvLiveStream(string id) - { - return EmbyTV.EmbyTV.Current.GetLiveStream(id); - } + private IServerApplicationHost _appHost; + private IHttpClient _httpClient; - public LiveTvManager(IApplicationHost appHost, IServerConfigurationManager config, ILogger logger, IItemRepository itemRepo, IImageProcessor imageProcessor, IUserDataManager userDataManager, IDtoService dtoService, IUserManager userManager, ILibraryManager libraryManager, ITaskManager taskManager, ILocalizationManager localization, IJsonSerializer jsonSerializer, IProviderManager providerManager, IFileSystem fileSystem, ISecurityManager security) + public LiveTvManager(IServerApplicationHost appHost, IHttpClient httpClient, IServerConfigurationManager config, ILogger logger, IItemRepository itemRepo, IImageProcessor imageProcessor, IUserDataManager userDataManager, IDtoService dtoService, IUserManager userManager, ILibraryManager libraryManager, ITaskManager taskManager, ILocalizationManager localization, IJsonSerializer jsonSerializer, IProviderManager providerManager, IFileSystem fileSystem, ISecurityManager security, Func<IChannelManager> channelManager) { + _appHost = appHost; _config = config; _logger = logger; _itemRepo = itemRepo; @@ -98,6 +99,8 @@ namespace Emby.Server.Implementations.LiveTv _security = security; _dtoService = dtoService; _userDataManager = userDataManager; + _channelManager = channelManager; + _httpClient = httpClient; _tvDtoService = new LiveTvDtoService(dtoService, imageProcessor, logger, appHost, _libraryManager); } @@ -125,27 +128,58 @@ namespace Emby.Server.Implementations.LiveTv public void AddParts(IEnumerable<ILiveTvService> services, IEnumerable<ITunerHost> tunerHosts, IEnumerable<IListingsProvider> listingProviders) { _services = services.ToArray(); - _tunerHosts.AddRange(tunerHosts.Where(i => i.IsSupported)); - _listingProviders.AddRange(listingProviders); + _tunerHosts = tunerHosts.Where(i => i.IsSupported).ToArray(); + + _listingProviders = listingProviders.ToArray(); foreach (var service in _services) { service.DataSourceChanged += service_DataSourceChanged; - service.RecordingStatusChanged += Service_RecordingStatusChanged; + + var embyTv = service as EmbyTV.EmbyTV; + + if (embyTv != null) + { + embyTv.TimerCreated += EmbyTv_TimerCreated; + embyTv.TimerCancelled += EmbyTv_TimerCancelled; + } } } - private void Service_RecordingStatusChanged(object sender, RecordingStatusChangedEventArgs e) + private void EmbyTv_TimerCancelled(object sender, GenericEventArgs<string> e) { - _lastRecordingRefreshTime = DateTime.MinValue; + var timerId = e.Argument; + + EventHelper.FireEventIfNotNull(TimerCancelled, this, new GenericEventArgs<TimerEventInfo> + { + Argument = new TimerEventInfo + { + Id = timerId + } + }, _logger); + } + + private void EmbyTv_TimerCreated(object sender, GenericEventArgs<TimerInfo> e) + { + var timer = e.Argument; + var service = sender as ILiveTvService; + + EventHelper.FireEventIfNotNull(TimerCreated, this, new GenericEventArgs<TimerEventInfo> + { + Argument = new TimerEventInfo + { + ProgramId = _tvDtoService.GetInternalProgramId(timer.ProgramId), + Id = timer.Id + } + }, _logger); } - public List<ITunerHost> TunerHosts + public ITunerHost[] TunerHosts { get { return _tunerHosts; } } - public List<IListingsProvider> ListingProviders + public IListingsProvider[] ListingProviders { get { return _listingProviders; } } @@ -175,7 +209,7 @@ namespace Emby.Server.Implementations.LiveTv public QueryResult<BaseItem> GetInternalChannels(LiveTvChannelQuery query, DtoOptions dtoOptions, CancellationToken cancellationToken) { - var user = string.IsNullOrEmpty(query.UserId) ? null : _userManager.GetUserById(query.UserId); + var user = query.UserId.Equals(Guid.Empty) ? null : _userManager.GetUserById(query.UserId); var topFolder = GetInternalLiveTvFolder(cancellationToken); @@ -187,7 +221,7 @@ namespace Emby.Server.Implementations.LiveTv IsSports = query.IsSports, IsSeries = query.IsSeries, IncludeItemTypes = new[] { typeof(LiveTvChannel).Name }, - TopParentIds = new[] { topFolder.Id.ToString("N") }, + TopParentIds = new[] { topFolder.Id }, IsFavorite = query.IsFavorite, IsLiked = query.IsLiked, StartIndex = query.StartIndex, @@ -197,16 +231,16 @@ namespace Emby.Server.Implementations.LiveTv var orderBy = internalQuery.OrderBy.ToList(); - orderBy.AddRange(query.SortBy.Select(i => new Tuple<string, SortOrder>(i, query.SortOrder ?? SortOrder.Ascending))); + orderBy.AddRange(query.SortBy.Select(i => new ValueTuple<string, SortOrder>(i, query.SortOrder ?? SortOrder.Ascending))); if (query.EnableFavoriteSorting) { - orderBy.Insert(0, new Tuple<string, SortOrder>(ItemSortBy.IsFavoriteOrLiked, SortOrder.Descending)); + orderBy.Insert(0, new ValueTuple<string, SortOrder>(ItemSortBy.IsFavoriteOrLiked, SortOrder.Descending)); } if (!internalQuery.OrderBy.Any(i => string.Equals(i.Item1, ItemSortBy.SortName, StringComparison.OrdinalIgnoreCase))) { - orderBy.Add(new Tuple<string, SortOrder>(ItemSortBy.SortName, SortOrder.Ascending)); + orderBy.Add(new ValueTuple<string, SortOrder>(ItemSortBy.SortName, SortOrder.Ascending)); } internalQuery.OrderBy = orderBy.ToArray(); @@ -214,75 +248,54 @@ namespace Emby.Server.Implementations.LiveTv return _libraryManager.GetItemsResult(internalQuery); } - public LiveTvChannel GetInternalChannel(string id) - { - return GetInternalChannel(new Guid(id)); - } - - private LiveTvChannel GetInternalChannel(Guid id) + public async Task<Tuple<MediaSourceInfo, ILiveStream>> GetChannelStream(string id, string mediaSourceId, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken) { - return _libraryManager.GetItemById(id) as LiveTvChannel; - } + if (string.Equals(id, mediaSourceId, StringComparison.OrdinalIgnoreCase)) + { + mediaSourceId = null; + } - internal LiveTvProgram GetInternalProgram(string id) - { - return _libraryManager.GetItemById(id) as LiveTvProgram; - } + MediaSourceInfo info; + bool isVideo; + ILiveTvService service; + ILiveStream liveStream; - internal LiveTvProgram GetInternalProgram(Guid id) - { - return _libraryManager.GetItemById(id) as LiveTvProgram; - } + var channel = (LiveTvChannel)_libraryManager.GetItemById(id); + isVideo = channel.ChannelType == ChannelType.TV; + service = GetService(channel); + _logger.Info("Opening channel stream from {0}, external channel Id: {1}", service.Name, channel.ExternalId); - public async Task<BaseItem> GetInternalRecording(string id, CancellationToken cancellationToken) - { - if (string.IsNullOrWhiteSpace(id)) + var supportsManagedStream = service as ISupportsDirectStreamProvider; + if (supportsManagedStream != null) { - throw new ArgumentNullException("id"); + liveStream = await supportsManagedStream.GetChannelStreamWithDirectStreamProvider(channel.ExternalId, mediaSourceId, currentLiveStreams, cancellationToken).ConfigureAwait(false); + info = liveStream.MediaSource; } - - var result = await GetInternalRecordings(new RecordingQuery + else { - Id = id - - }, new DtoOptions(), cancellationToken).ConfigureAwait(false); + info = await service.GetChannelStream(channel.ExternalId, mediaSourceId, cancellationToken).ConfigureAwait(false); + var openedId = info.Id; + Func<Task> closeFn = () => service.CloseLiveStream(openedId, CancellationToken.None); - return result.Items.FirstOrDefault(); - } - - public async Task<MediaSourceInfo> GetRecordingStream(string id, CancellationToken cancellationToken) - { - var info = await GetLiveStream(id, null, false, cancellationToken).ConfigureAwait(false); - - return info.Item1; - } - - public Task<Tuple<MediaSourceInfo, IDirectStreamProvider>> GetChannelStream(string id, string mediaSourceId, CancellationToken cancellationToken) - { - return GetLiveStream(id, mediaSourceId, true, cancellationToken); - } - - private string GetItemExternalId(BaseItem item) - { - var externalId = item.ExternalId; + liveStream = new ExclusiveLiveStream(info, closeFn); - if (string.IsNullOrWhiteSpace(externalId)) - { - externalId = item.GetProviderId("ProviderExternalId"); + var startTime = DateTime.UtcNow; + await liveStream.Open(cancellationToken).ConfigureAwait(false); + var endTime = DateTime.UtcNow; + _logger.Info("Live stream opened after {0}ms", (endTime - startTime).TotalMilliseconds); } + info.RequiresClosing = true; - return externalId; - } + var idPrefix = service.GetType().FullName.GetMD5().ToString("N") + "_"; - public async Task<IEnumerable<MediaSourceInfo>> GetRecordingMediaSources(IHasMediaSources item, CancellationToken cancellationToken) - { - var baseItem = (BaseItem)item; - var service = GetService(baseItem); + info.LiveStreamId = idPrefix + info.Id; + + Normalize(info, service, isVideo); - return await service.GetRecordingStreamMediaSources(GetItemExternalId(baseItem), cancellationToken).ConfigureAwait(false); + return new Tuple<MediaSourceInfo, ILiveStream>(info, liveStream); } - public async Task<IEnumerable<MediaSourceInfo>> GetChannelMediaSources(IHasMediaSources item, CancellationToken cancellationToken) + public async Task<IEnumerable<MediaSourceInfo>> GetChannelMediaSources(BaseItem item, CancellationToken cancellationToken) { var baseItem = (LiveTvChannel)item; var service = GetService(baseItem); @@ -304,14 +317,17 @@ namespace Emby.Server.Implementations.LiveTv return list; } - private ILiveTvService GetService(ILiveTvRecording item) + private ILiveTvService GetService(LiveTvChannel item) { - return GetService(item.ServiceName); + var name = item.ServiceName; + return GetService(name); } - private ILiveTvService GetService(BaseItem item) + private ILiveTvService GetService(LiveTvProgram item) { - return GetService(item.ServiceName); + var channel = _libraryManager.GetItemById(item.ChannelId) as LiveTvChannel; + + return GetService(channel); } private ILiveTvService GetService(string name) @@ -319,70 +335,11 @@ namespace Emby.Server.Implementations.LiveTv return _services.FirstOrDefault(i => string.Equals(i.Name, name, StringComparison.OrdinalIgnoreCase)); } - private async Task<Tuple<MediaSourceInfo, IDirectStreamProvider>> GetLiveStream(string id, string mediaSourceId, bool isChannel, CancellationToken cancellationToken) - { - if (string.Equals(id, mediaSourceId, StringComparison.OrdinalIgnoreCase)) - { - mediaSourceId = null; - } - - MediaSourceInfo info; - bool isVideo; - ILiveTvService service; - IDirectStreamProvider directStreamProvider = null; - - if (isChannel) - { - var channel = GetInternalChannel(id); - isVideo = channel.ChannelType == ChannelType.TV; - service = GetService(channel); - _logger.Info("Opening channel stream from {0}, external channel Id: {1}", service.Name, GetItemExternalId(channel)); - - var supportsManagedStream = service as ISupportsDirectStreamProvider; - if (supportsManagedStream != null) - { - var streamInfo = await supportsManagedStream.GetChannelStreamWithDirectStreamProvider(GetItemExternalId(channel), mediaSourceId, cancellationToken).ConfigureAwait(false); - info = streamInfo.Item1; - directStreamProvider = streamInfo.Item2; - } - else - { - info = await service.GetChannelStream(GetItemExternalId(channel), mediaSourceId, cancellationToken).ConfigureAwait(false); - } - info.RequiresClosing = true; - - if (info.RequiresClosing) - { - var idPrefix = service.GetType().FullName.GetMD5().ToString("N") + "_"; - - info.LiveStreamId = idPrefix + info.Id; - } - } - else - { - var recording = await GetInternalRecording(id, cancellationToken).ConfigureAwait(false); - isVideo = !string.Equals(recording.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase); - service = GetService(recording); - - _logger.Info("Opening recording stream from {0}, external recording Id: {1}", service.Name, GetItemExternalId(recording)); - info = await service.GetRecordingStream(GetItemExternalId(recording), null, cancellationToken).ConfigureAwait(false); - info.RequiresClosing = true; - - if (info.RequiresClosing) - { - var idPrefix = service.GetType().FullName.GetMD5().ToString("N") + "_"; - - info.LiveStreamId = idPrefix + info.Id; - } - } - - Normalize(info, service, isVideo); - - return new Tuple<MediaSourceInfo, IDirectStreamProvider>(info, directStreamProvider); - } - private void Normalize(MediaSourceInfo mediaSource, ILiveTvService service, bool isVideo) { + // Not all of the plugins are setting this + mediaSource.IsInfiniteStream = true; + if (mediaSource.MediaStreams.Count == 0) { if (isVideo) @@ -475,7 +432,7 @@ namespace Emby.Server.Implementations.LiveTv { // We can't trust that we'll be able to direct stream it through emby server, no matter what the provider says //mediaSource.SupportsDirectPlay = false; - mediaSource.SupportsDirectStream = false; + //mediaSource.SupportsDirectStream = false; mediaSource.SupportsTranscoding = true; foreach (var stream in mediaSource.MediaStreams) { @@ -492,8 +449,10 @@ namespace Emby.Server.Implementations.LiveTv } } - private async Task<LiveTvChannel> GetChannel(ChannelInfo channelInfo, string serviceName, Guid parentFolderId, CancellationToken cancellationToken) + private const string ExternalServiceTag = "ExternalServiceId"; + private LiveTvChannel GetChannel(ChannelInfo channelInfo, string serviceName, BaseItem parentFolder, CancellationToken cancellationToken) { + var parentFolderId = parentFolder.Id; var isNew = false; var forceUpdate = false; @@ -507,17 +466,20 @@ namespace Emby.Server.Implementations.LiveTv { Name = channelInfo.Name, Id = id, - DateCreated = DateTime.UtcNow, + DateCreated = DateTime.UtcNow }; isNew = true; } - if (!string.Equals(channelInfo.Id, item.ExternalId, StringComparison.Ordinal)) + if (channelInfo.Tags != null) { - isNew = true; + if (!channelInfo.Tags.SequenceEqual(item.Tags, StringComparer.OrdinalIgnoreCase)) + { + isNew = true; + } + item.Tags = channelInfo.Tags; } - item.ExternalId = channelInfo.Id; if (!item.ParentId.Equals(parentFolderId)) { @@ -528,6 +490,18 @@ namespace Emby.Server.Implementations.LiveTv item.ChannelType = channelInfo.ChannelType; item.ServiceName = serviceName; + if (!string.Equals(item.GetProviderId(ExternalServiceTag), serviceName, StringComparison.OrdinalIgnoreCase)) + { + forceUpdate = true; + } + item.SetProviderId(ExternalServiceTag, serviceName); + + if (!string.Equals(channelInfo.Id, item.ExternalId, StringComparison.Ordinal)) + { + forceUpdate = true; + } + item.ExternalId = channelInfo.Id; + if (!string.Equals(channelInfo.Number, item.Number, StringComparison.Ordinal)) { forceUpdate = true; @@ -556,25 +530,21 @@ namespace Emby.Server.Implementations.LiveTv if (isNew) { - _libraryManager.CreateItem(item, cancellationToken); + _libraryManager.CreateItem(item, parentFolder); } else if (forceUpdate) { - _libraryManager.UpdateItem(item, ItemUpdateType.MetadataImport, cancellationToken); + _libraryManager.UpdateItem(item, parentFolder, ItemUpdateType.MetadataImport, cancellationToken); } - await item.RefreshMetadata(new MetadataRefreshOptions(_fileSystem) - { - ForceSave = isNew || forceUpdate - - }, cancellationToken); - return item; } + private const string EtagKey = "ProgramEtag"; + private Tuple<LiveTvProgram, bool, bool> GetProgram(ProgramInfo info, Dictionary<Guid, LiveTvProgram> allExistingPrograms, LiveTvChannel channel, ChannelType channelType, string serviceName, CancellationToken cancellationToken) { - var id = _tvDtoService.GetInternalProgramId(serviceName, info.Id); + var id = _tvDtoService.GetInternalProgramId(info.Id); LiveTvProgram item = null; allExistingPrograms.TryGetValue(id, out item); @@ -590,9 +560,13 @@ namespace Emby.Server.Implementations.LiveTv Name = info.Name, Id = id, DateCreated = DateTime.UtcNow, - DateModified = DateTime.UtcNow, - ExternalEtag = info.Etag + DateModified = DateTime.UtcNow }; + + if (!string.IsNullOrEmpty(info.Etag)) + { + item.SetProviderId(EtagKey, info.Etag); + } } if (!string.Equals(info.ShowId, item.ShowId, StringComparison.OrdinalIgnoreCase)) @@ -610,10 +584,9 @@ namespace Emby.Server.Implementations.LiveTv item.ParentId = channel.Id; //item.ChannelType = channelType; - item.ServiceName = serviceName; item.Audio = info.Audio; - item.ChannelId = channel.Id.ToString("N"); + item.ChannelId = channel.Id; item.CommunityRating = item.CommunityRating ?? info.CommunityRating; if ((item.CommunityRating ?? 0).Equals(0)) { @@ -629,20 +602,76 @@ namespace Emby.Server.Implementations.LiveTv } item.ExternalSeriesId = seriesId; - item.Genres = info.Genres; - item.IsHD = info.IsHD; - item.IsKids = info.IsKids; - item.IsLive = info.IsLive; + var isSeries = info.IsSeries || !string.IsNullOrEmpty(info.EpisodeTitle); + + if (isSeries || !string.IsNullOrEmpty(info.EpisodeTitle)) + { + item.SeriesName = info.Name; + } + + var tags = new List<string>(); + if (info.IsLive) + { + tags.Add("Live"); + } + if (info.IsPremiere) + { + tags.Add("Premiere"); + } + if (info.IsNews) + { + tags.Add("News"); + } + if (info.IsSports) + { + tags.Add("Sports"); + } + if (info.IsKids) + { + tags.Add("Kids"); + } + if (info.IsRepeat) + { + tags.Add("Repeat"); + } + if (info.IsMovie) + { + tags.Add("Movie"); + } + if (isSeries) + { + tags.Add("Series"); + } + + item.Tags = tags.ToArray(); + + item.Genres = info.Genres.ToArray(); + + if (info.IsHD ?? false) + { + item.Width = 1280; + item.Height = 720; + } + item.IsMovie = info.IsMovie; - item.IsNews = info.IsNews; - item.IsPremiere = info.IsPremiere; item.IsRepeat = info.IsRepeat; - item.IsSeries = info.IsSeries; - item.IsSports = info.IsSports; + + if (item.IsSeries != isSeries) + { + forceUpdate = true; + } + item.IsSeries = isSeries; + item.Name = info.Name; item.OfficialRating = item.OfficialRating ?? info.OfficialRating; item.Overview = item.Overview ?? info.Overview; item.RunTimeTicks = (info.EndDate - info.StartDate).Ticks; + item.ProviderIds = info.ProviderIds; + + foreach (var providerId in info.SeriesProviderIds) + { + info.ProviderIds["Series" + providerId.Key] = providerId.Value; + } if (item.StartDate != info.StartDate) { @@ -656,11 +685,9 @@ namespace Emby.Server.Implementations.LiveTv } item.EndDate = info.EndDate; - item.HomePageUrl = info.HomePageUrl; - item.ProductionYear = info.ProductionYear; - if (!info.IsSeries || info.IsRepeat) + if (!isSeries || info.IsRepeat) { item.PremiereDate = info.OriginalAirDate; } @@ -737,12 +764,11 @@ namespace Emby.Server.Implementations.LiveTv } else { - // Increment this whenver some internal change deems it necessary - var etag = info.Etag + "6"; + var etag = info.Etag; - if (!string.Equals(etag, item.ExternalEtag, StringComparison.OrdinalIgnoreCase)) + if (!string.Equals(etag, item.GetProviderId(EtagKey), StringComparison.OrdinalIgnoreCase)) { - item.ExternalEtag = etag; + item.SetProviderId(EtagKey, etag); isUpdated = true; } } @@ -755,180 +781,47 @@ namespace Emby.Server.Implementations.LiveTv return new Tuple<LiveTvProgram, bool, bool>(item, isNew, isUpdated); } - private Guid CreateRecordingRecord(RecordingInfo info, string serviceName, Guid parentFolderId, CancellationToken cancellationToken) - { - var isNew = false; - - var id = _tvDtoService.GetInternalRecordingId(serviceName, info.Id); - - var item = _itemRepo.RetrieveItem(id); - - if (item == null) - { - if (info.ChannelType == ChannelType.TV) - { - item = new LiveTvVideoRecording - { - Name = info.Name, - Id = id, - DateCreated = DateTime.UtcNow, - DateModified = DateTime.UtcNow, - VideoType = VideoType.VideoFile - }; - } - else - { - item = new LiveTvAudioRecording - { - Name = info.Name, - Id = id, - DateCreated = DateTime.UtcNow, - DateModified = DateTime.UtcNow - }; - } - - isNew = true; - } - - item.ChannelId = _tvDtoService.GetInternalChannelId(serviceName, info.ChannelId).ToString("N"); - item.CommunityRating = info.CommunityRating; - item.OfficialRating = info.OfficialRating; - item.Overview = info.Overview; - item.EndDate = info.EndDate; - item.Genres = info.Genres; - item.PremiereDate = info.OriginalAirDate; - - var recording = (ILiveTvRecording)item; - - recording.ExternalId = info.Id; - - var dataChanged = false; - - recording.Audio = info.Audio; - recording.EndDate = info.EndDate; - recording.EpisodeTitle = info.EpisodeTitle; - recording.IsHD = info.IsHD; - recording.IsKids = info.IsKids; - recording.IsLive = info.IsLive; - recording.IsMovie = info.IsMovie; - recording.IsNews = info.IsNews; - recording.IsPremiere = info.IsPremiere; - recording.IsRepeat = info.IsRepeat; - recording.IsSports = info.IsSports; - recording.SeriesTimerId = info.SeriesTimerId; - recording.TimerId = info.TimerId; - recording.StartDate = info.StartDate; - - if (!dataChanged) - { - dataChanged = recording.IsSeries != info.IsSeries; - } - recording.IsSeries = info.IsSeries; - - if (!item.ParentId.Equals(parentFolderId)) - { - dataChanged = true; - } - item.ParentId = parentFolderId; - - if (!item.HasImage(ImageType.Primary)) - { - if (!string.IsNullOrWhiteSpace(info.ImagePath)) - { - item.SetImage(new ItemImageInfo - { - Path = info.ImagePath, - Type = ImageType.Primary - }, 0); - } - else if (!string.IsNullOrWhiteSpace(info.ImageUrl)) - { - item.SetImage(new ItemImageInfo - { - Path = info.ImageUrl, - Type = ImageType.Primary - }, 0); - } - } - - var statusChanged = info.Status != recording.Status; - - recording.Status = info.Status; - - recording.ServiceName = serviceName; - - if (!string.IsNullOrEmpty(info.Path)) - { - if (!dataChanged) - { - dataChanged = !string.Equals(item.Path, info.Path); - } - var fileInfo = _fileSystem.GetFileInfo(info.Path); - - recording.DateCreated = _fileSystem.GetCreationTimeUtc(fileInfo); - recording.DateModified = _fileSystem.GetLastWriteTimeUtc(fileInfo); - item.Path = info.Path; - } - else if (!string.IsNullOrEmpty(info.Url)) - { - if (!dataChanged) - { - dataChanged = !string.Equals(item.Path, info.Url); - } - item.Path = info.Url; - } - - var metadataRefreshMode = MetadataRefreshMode.Default; - - if (isNew) - { - _libraryManager.CreateItem(item, cancellationToken); - } - else if (dataChanged || info.DateLastUpdated > recording.DateLastSaved || statusChanged) - { - metadataRefreshMode = MetadataRefreshMode.FullRefresh; - _libraryManager.UpdateItem(item, ItemUpdateType.MetadataImport, cancellationToken); - } - - if (info.Status != RecordingStatus.InProgress) - { - _providerManager.QueueRefresh(item.Id, new MetadataRefreshOptions(_fileSystem) - { - MetadataRefreshMode = metadataRefreshMode - - }, RefreshPriority.Normal); - } - - return item.Id; - } - public async Task<BaseItemDto> GetProgram(string id, CancellationToken cancellationToken, User user = null) { - var program = GetInternalProgram(id); + var program = _libraryManager.GetItemById(id); var dto = _dtoService.GetBaseItemDto(program, new DtoOptions(), user); - var list = new List<Tuple<BaseItemDto, string, string, string>>(); + var list = new List<Tuple<BaseItemDto, string, string>>(); var externalSeriesId = program.ExternalSeriesId; - list.Add(new Tuple<BaseItemDto, string, string, string>(dto, program.ServiceName, GetItemExternalId(program), externalSeriesId)); + list.Add(new Tuple<BaseItemDto, string, string>(dto, program.ExternalId, externalSeriesId)); await AddRecordingInfo(list, cancellationToken).ConfigureAwait(false); return dto; } - public async Task<QueryResult<BaseItemDto>> GetPrograms(ProgramQuery query, DtoOptions options, CancellationToken cancellationToken) + public async Task<QueryResult<BaseItemDto>> GetPrograms(InternalItemsQuery query, DtoOptions options, CancellationToken cancellationToken) { - var user = string.IsNullOrEmpty(query.UserId) ? null : _userManager.GetUserById(query.UserId); + var user = query.User; var topFolder = GetInternalLiveTvFolder(cancellationToken); if (query.OrderBy.Length == 0) { - // Unless something else was specified, order by start date to take advantage of a specialized index - query.OrderBy = new Tuple<string, SortOrder>[] { new Tuple<string, SortOrder>(ItemSortBy.StartDate, SortOrder.Ascending) }; + if (query.IsAiring ?? false) + { + // Unless something else was specified, order by start date to take advantage of a specialized index + query.OrderBy = new ValueTuple<string, SortOrder>[] + { + new ValueTuple<string, SortOrder>(ItemSortBy.StartDate, SortOrder.Ascending) + }; + } + else + { + // Unless something else was specified, order by start date to take advantage of a specialized index + query.OrderBy = new ValueTuple<string, SortOrder>[] + { + new ValueTuple<string, SortOrder>(ItemSortBy.StartDate, SortOrder.Ascending) + }; + } } RemoveFields(options); @@ -952,15 +845,17 @@ namespace Emby.Server.Implementations.LiveTv Limit = query.Limit, OrderBy = query.OrderBy, EnableTotalRecordCount = query.EnableTotalRecordCount, - TopParentIds = new[] { topFolder.Id.ToString("N") }, + TopParentIds = new[] { topFolder.Id }, Name = query.Name, - DtoOptions = options + DtoOptions = options, + HasAired = query.HasAired, + IsAiring = query.IsAiring }; if (!string.IsNullOrWhiteSpace(query.SeriesTimerId)) { var seriesTimers = await GetSeriesTimersInternal(new SeriesTimerQuery { }, cancellationToken).ConfigureAwait(false); - var seriesTimer = seriesTimers.Items.FirstOrDefault(i => string.Equals(_tvDtoService.GetInternalSeriesTimerId(i.ServiceName, i.Id).ToString("N"), query.SeriesTimerId, StringComparison.OrdinalIgnoreCase)); + var seriesTimer = seriesTimers.Items.FirstOrDefault(i => string.Equals(_tvDtoService.GetInternalSeriesTimerId(i.Id).ToString("N"), query.SeriesTimerId, StringComparison.OrdinalIgnoreCase)); if (seriesTimer != null) { internalQuery.ExternalSeriesId = seriesTimer.SeriesId; @@ -978,18 +873,6 @@ namespace Emby.Server.Implementations.LiveTv } } - if (query.HasAired.HasValue) - { - if (query.HasAired.Value) - { - internalQuery.MaxEndDate = DateTime.UtcNow; - } - else - { - internalQuery.MinEndDate = DateTime.UtcNow; - } - } - var queryResult = _libraryManager.QueryItems(internalQuery); var returnArray = _dtoService.GetBaseItemDtos(queryResult.Items, options, user); @@ -1003,9 +886,9 @@ namespace Emby.Server.Implementations.LiveTv return result; } - public QueryResult<BaseItem> GetRecommendedProgramsInternal(RecommendedProgramQuery query, DtoOptions options, CancellationToken cancellationToken) + public QueryResult<BaseItem> GetRecommendedProgramsInternal(InternalItemsQuery query, DtoOptions options, CancellationToken cancellationToken) { - var user = _userManager.GetUserById(query.UserId); + var user = query.User; var topFolder = GetInternalLiveTvFolder(cancellationToken); @@ -1013,14 +896,15 @@ namespace Emby.Server.Implementations.LiveTv { IncludeItemTypes = new[] { typeof(LiveTvProgram).Name }, IsAiring = query.IsAiring, + HasAired = query.HasAired, IsNews = query.IsNews, IsMovie = query.IsMovie, IsSeries = query.IsSeries, IsSports = query.IsSports, IsKids = query.IsKids, EnableTotalRecordCount = query.EnableTotalRecordCount, - OrderBy = new[] { new Tuple<string, SortOrder>(ItemSortBy.StartDate, SortOrder.Ascending) }, - TopParentIds = new[] { topFolder.Id.ToString("N") }, + OrderBy = new[] { new ValueTuple<string, SortOrder>(ItemSortBy.StartDate, SortOrder.Ascending) }, + TopParentIds = new[] { topFolder.Id }, DtoOptions = options, GenreIds = query.GenreIds }; @@ -1030,18 +914,6 @@ namespace Emby.Server.Implementations.LiveTv internalQuery.Limit = Math.Max(query.Limit.Value * 4, 200); } - if (query.HasAired.HasValue) - { - if (query.HasAired.Value) - { - internalQuery.MaxEndDate = DateTime.UtcNow; - } - else - { - internalQuery.MinEndDate = DateTime.UtcNow; - } - } - var programList = _libraryManager.QueryItems(internalQuery).Items; var totalCount = programList.Length; @@ -1050,7 +922,7 @@ namespace Emby.Server.Implementations.LiveTv if (query.IsAiring ?? false) { orderedPrograms = orderedPrograms - .ThenByDescending(i => GetRecommendationScore(i, user.Id, true)); + .ThenByDescending(i => GetRecommendationScore(i, user, true)); } IEnumerable<BaseItem> programs = orderedPrograms; @@ -1069,13 +941,18 @@ namespace Emby.Server.Implementations.LiveTv return result; } - public QueryResult<BaseItemDto> GetRecommendedPrograms(RecommendedProgramQuery query, DtoOptions options, CancellationToken cancellationToken) + public QueryResult<BaseItemDto> GetRecommendedPrograms(InternalItemsQuery query, DtoOptions options, CancellationToken cancellationToken) { + if (!(query.IsAiring ?? false)) + { + return GetPrograms(query, options, cancellationToken).Result; + } + RemoveFields(options); var internalResult = GetRecommendedProgramsInternal(query, options, cancellationToken); - var user = _userManager.GetUserById(query.UserId); + var user = query.User; var returnArray = _dtoService.GetBaseItemDtos(internalResult.Items, options, user); @@ -1088,7 +965,7 @@ namespace Emby.Server.Implementations.LiveTv return result; } - private int GetRecommendationScore(LiveTvProgram program, Guid userId, bool factorChannelWatchCount) + private int GetRecommendationScore(LiveTvProgram program, User user, bool factorChannelWatchCount) { var score = 0; @@ -1102,11 +979,11 @@ namespace Emby.Server.Implementations.LiveTv score++; } - var channel = GetInternalChannel(program.ChannelId); + var channel = _libraryManager.GetItemById(program.ChannelId); if (channel != null) { - var channelUserdata = _userDataManager.GetUserData(userId, channel); + var channelUserdata = _userDataManager.GetUserData(user, channel); if (channelUserdata.Likes ?? false) { @@ -1131,36 +1008,23 @@ namespace Emby.Server.Implementations.LiveTv return score; } - private async Task AddRecordingInfo(IEnumerable<Tuple<BaseItemDto, string, string, string>> programs, CancellationToken cancellationToken) + private async Task AddRecordingInfo(IEnumerable<Tuple<BaseItemDto, string, string>> programs, CancellationToken cancellationToken) { var timers = new Dictionary<string, List<TimerInfo>>(); var seriesTimers = new Dictionary<string, List<SeriesTimerInfo>>(); + TimerInfo[] timerList = null; + SeriesTimerInfo[] seriesTimerList = null; + foreach (var programTuple in programs) { var program = programTuple.Item1; - var serviceName = programTuple.Item2; - var externalProgramId = programTuple.Item3; - string externalSeriesId = programTuple.Item4; + var externalProgramId = programTuple.Item2; + string externalSeriesId = programTuple.Item3; - if (string.IsNullOrWhiteSpace(serviceName)) + if (timerList == null) { - continue; - } - - List<TimerInfo> timerList; - if (!timers.TryGetValue(serviceName, out timerList)) - { - try - { - var tempTimers = await GetService(serviceName).GetTimersAsync(cancellationToken).ConfigureAwait(false); - timers[serviceName] = timerList = tempTimers.ToList(); - } - catch (Exception ex) - { - _logger.ErrorException("Error getting timer infos", ex); - timers[serviceName] = timerList = new List<TimerInfo>(); - } + timerList = (await GetTimersInternal(new TimerQuery(), cancellationToken).ConfigureAwait(false)).Items; } var timer = timerList.FirstOrDefault(i => string.Equals(i.ProgramId, externalProgramId, StringComparison.OrdinalIgnoreCase)); @@ -1170,15 +1034,14 @@ namespace Emby.Server.Implementations.LiveTv { if (timer.Status != RecordingStatus.Cancelled && timer.Status != RecordingStatus.Error) { - program.TimerId = _tvDtoService.GetInternalTimerId(serviceName, timer.Id) - .ToString("N"); + program.TimerId = _tvDtoService.GetInternalTimerId(timer.Id); program.Status = timer.Status.ToString(); } if (!string.IsNullOrEmpty(timer.SeriesTimerId)) { - program.SeriesTimerId = _tvDtoService.GetInternalSeriesTimerId(serviceName, timer.SeriesTimerId) + program.SeriesTimerId = _tvDtoService.GetInternalSeriesTimerId(timer.SeriesTimerId) .ToString("N"); foundSeriesTimer = true; @@ -1190,26 +1053,16 @@ namespace Emby.Server.Implementations.LiveTv continue; } - List<SeriesTimerInfo> seriesTimerList; - if (!seriesTimers.TryGetValue(serviceName, out seriesTimerList)) + if (seriesTimerList == null) { - try - { - var tempTimers = await GetService(serviceName).GetSeriesTimersAsync(cancellationToken).ConfigureAwait(false); - seriesTimers[serviceName] = seriesTimerList = tempTimers.ToList(); - } - catch (Exception ex) - { - _logger.ErrorException("Error getting series timer infos", ex); - seriesTimers[serviceName] = seriesTimerList = new List<SeriesTimerInfo>(); - } + seriesTimerList = (await GetSeriesTimersInternal(new SeriesTimerQuery(), cancellationToken).ConfigureAwait(false)).Items; } var seriesTimer = seriesTimerList.FirstOrDefault(i => string.Equals(i.SeriesId, externalSeriesId, StringComparison.OrdinalIgnoreCase)); if (seriesTimer != null) { - program.SeriesTimerId = _tvDtoService.GetInternalSeriesTimerId(serviceName, seriesTimer.Id) + program.SeriesTimerId = _tvDtoService.GetInternalSeriesTimerId(seriesTimer.Id) .ToString("N"); } } @@ -1222,7 +1075,7 @@ namespace Emby.Server.Implementations.LiveTv private async Task RefreshChannelsInternal(IProgress<double> progress, CancellationToken cancellationToken) { - EmbyTV.EmbyTV.Current.CreateRecordingFolders(); + await EmbyTV.EmbyTV.Current.CreateRecordingFolders().ConfigureAwait(false); await EmbyTV.EmbyTV.Current.ScanForTunerDeviceChanges(cancellationToken).ConfigureAwait(false); @@ -1271,8 +1124,8 @@ namespace Emby.Server.Implementations.LiveTv if (cleanDatabase) { - await CleanDatabaseInternal(newChannelIdList.ToArray(), new[] { typeof(LiveTvChannel).Name }, progress, cancellationToken).ConfigureAwait(false); - await CleanDatabaseInternal(newProgramIdList.ToArray(), new[] { typeof(LiveTvProgram).Name }, progress, cancellationToken).ConfigureAwait(false); + CleanDatabaseInternal(newChannelIdList.ToArray(), new[] { typeof(LiveTvChannel).Name }, progress, cancellationToken); + CleanDatabaseInternal(newProgramIdList.ToArray(), new[] { typeof(LiveTvProgram).Name }, progress, cancellationToken); } var coreService = _services.OfType<EmbyTV.EmbyTV>().FirstOrDefault(); @@ -1286,12 +1139,9 @@ namespace Emby.Server.Implementations.LiveTv // Load these now which will prefetch metadata var dtoOptions = new DtoOptions(); var fields = dtoOptions.Fields.ToList(); - fields.Remove(ItemFields.SyncInfo); fields.Remove(ItemFields.BasicSyncInfo); dtoOptions.Fields = fields.ToArray(fields.Count); - await GetRecordings(new RecordingQuery(), dtoOptions, cancellationToken).ConfigureAwait(false); - progress.Report(100); } @@ -1315,7 +1165,7 @@ namespace Emby.Server.Implementations.LiveTv try { - var item = await GetChannel(channelInfo.Item2, channelInfo.Item1, parentFolderId, cancellationToken).ConfigureAwait(false); + var item = GetChannel(channelInfo.Item2, channelInfo.Item1, parentFolder, cancellationToken); list.Add(item); } @@ -1363,19 +1213,19 @@ namespace Emby.Server.Implementations.LiveTv var isKids = false; var iSSeries = false; - var channelPrograms = (await service.GetProgramsAsync(GetItemExternalId(currentChannel), start, end, cancellationToken).ConfigureAwait(false)).ToList(); + var channelPrograms = (await service.GetProgramsAsync(currentChannel.ExternalId, start, end, cancellationToken).ConfigureAwait(false)).ToList(); var existingPrograms = _libraryManager.GetItemList(new InternalItemsQuery { IncludeItemTypes = new string[] { typeof(LiveTvProgram).Name }, - ChannelIds = new string[] { currentChannel.Id.ToString("N") }, + ChannelIds = new Guid[] { currentChannel.Id }, DtoOptions = new DtoOptions(true) }).Cast<LiveTvProgram>().ToDictionary(i => i.Id); var newPrograms = new List<LiveTvProgram>(); - var updatedPrograms = new List<LiveTvProgram>(); + var updatedPrograms = new List<BaseItem>(); foreach (var program in channelPrograms) { @@ -1426,19 +1276,27 @@ namespace Emby.Server.Implementations.LiveTv _libraryManager.CreateItems(newPrograms, null, cancellationToken); } - // TODO: Do this in bulk - foreach (var program in updatedPrograms) + if (updatedPrograms.Count > 0) { - _libraryManager.UpdateItem(program, ItemUpdateType.MetadataImport, cancellationToken); + _libraryManager.UpdateItems(updatedPrograms, currentChannel, ItemUpdateType.MetadataImport, cancellationToken); } currentChannel.IsMovie = isMovie; currentChannel.IsNews = isNews; currentChannel.IsSports = isSports; - currentChannel.IsKids = isKids; currentChannel.IsSeries = iSSeries; - currentChannel.UpdateToRepository(ItemUpdateType.MetadataImport, cancellationToken); + if (isKids) + { + currentChannel.AddTag("Kids"); + } + + //currentChannel.UpdateToRepository(ItemUpdateType.MetadataImport, cancellationToken); + await currentChannel.RefreshMetadata(new MetadataRefreshOptions(_fileSystem) + { + ForceSave = true + + }, cancellationToken).ConfigureAwait(false); } catch (OperationCanceledException) { @@ -1455,12 +1313,12 @@ namespace Emby.Server.Implementations.LiveTv progress.Report(85 * percent + 15); } - progress.Report(100); + progress.Report(100); return new Tuple<List<Guid>, List<Guid>>(channels, programs); } - private async Task CleanDatabaseInternal(Guid[] currentIdList, string[] validTypes, IProgress<double> progress, CancellationToken cancellationToken) + private void CleanDatabaseInternal(Guid[] currentIdList, string[] validTypes, IProgress<double> progress, CancellationToken cancellationToken) { var list = _itemRepo.GetItemIdsList(new InternalItemsQuery { @@ -1475,7 +1333,7 @@ namespace Emby.Server.Implementations.LiveTv { cancellationToken.ThrowIfCancellationRequested(); - if (itemId == Guid.Empty) + if (itemId.Equals(Guid.Empty)) { // Somehow some invalid data got into the db. It probably predates the boundary checking continue; @@ -1487,11 +1345,12 @@ namespace Emby.Server.Implementations.LiveTv if (item != null) { - await _libraryManager.DeleteItem(item, new DeleteOptions + _libraryManager.DeleteItem(item, new DeleteOptions { - DeleteFileLocation = false + DeleteFileLocation = false, + DeleteFromExternalProvider = false - }).ConfigureAwait(false); + }, false); } } @@ -1516,72 +1375,19 @@ namespace Emby.Server.Implementations.LiveTv return 7; } - private DateTime _lastRecordingRefreshTime; - private async Task RefreshRecordings(Guid internalLiveTvFolderId, CancellationToken cancellationToken) - { - const int cacheMinutes = 2; - - await _refreshRecordingsLock.WaitAsync(cancellationToken).ConfigureAwait(false); - - try - { - if ((DateTime.UtcNow - _lastRecordingRefreshTime).TotalMinutes < cacheMinutes) - { - return; - } - - var tasks = _services.Select(async i => - { - try - { - var recs = await i.GetRecordingsAsync(cancellationToken).ConfigureAwait(false); - return recs.Select(r => new Tuple<RecordingInfo, ILiveTvService>(r, i)); - } - catch (Exception ex) - { - _logger.ErrorException("Error getting recordings", ex); - return new List<Tuple<RecordingInfo, ILiveTvService>>(); - } - }); - - var results = await Task.WhenAll(tasks).ConfigureAwait(false); - - var idList = results.SelectMany(i => i.ToList()).Select(i => CreateRecordingRecord(i.Item1, i.Item2.Name, internalLiveTvFolderId, cancellationToken)) - .ToArray(); - - await CleanDatabaseInternal(idList, new[] { typeof(LiveTvVideoRecording).Name, typeof(LiveTvAudioRecording).Name }, new SimpleProgress<double>(), cancellationToken).ConfigureAwait(false); - - _lastRecordingRefreshTime = DateTime.UtcNow; - } - finally - { - _refreshRecordingsLock.Release(); - } - } - - private QueryResult<BaseItem> GetEmbyRecordings(RecordingQuery query, DtoOptions dtoOptions, Guid internalLiveTvFolderId, User user) + private QueryResult<BaseItem> GetEmbyRecordings(RecordingQuery query, DtoOptions dtoOptions, User user) { if (user == null) { return new QueryResult<BaseItem>(); } - var folderIds = EmbyTV.EmbyTV.Current.GetRecordingFolders() - .SelectMany(i => i.Locations) - .Distinct(StringComparer.OrdinalIgnoreCase) - .Select(i => _libraryManager.FindByPath(i, true)) - .Where(i => i != null) - .Where(i => i.IsVisibleStandalone(user)) + var folderIds = GetRecordingFolders(user, true) .Select(i => i.Id) .ToList(); var excludeItemTypes = new List<string>(); - folderIds.Add(internalLiveTvFolderId); - - excludeItemTypes.Add(typeof(LiveTvChannel).Name); - excludeItemTypes.Add(typeof(LiveTvProgram).Name); - if (folderIds.Count == 0) { return new QueryResult<BaseItem>(); @@ -1629,221 +1435,61 @@ namespace Emby.Server.Implementations.LiveTv } } + var limit = query.Limit; + if ((query.IsInProgress ?? false)) { - // TODO: filter - var allActivePaths = EmbyTV.EmbyTV.Current.GetAllActiveRecordings().Select(i => i.Path).ToArray(); - var items = allActivePaths.Select(i => _libraryManager.FindByPath(i, false)).Where(i => i != null).ToArray(); + limit = (query.Limit ?? 10) * 2; + limit = null; - return new QueryResult<BaseItem> - { - Items = items, - TotalRecordCount = items.Length - }; + //var allActivePaths = EmbyTV.EmbyTV.Current.GetAllActiveRecordings().Select(i => i.Path).ToArray(); + //var items = allActivePaths.Select(i => _libraryManager.FindByPath(i, false)).Where(i => i != null).ToArray(); + + //return new QueryResult<BaseItem> + //{ + // Items = items, + // TotalRecordCount = items.Length + //}; + + dtoOptions.Fields = dtoOptions.Fields.Concat(new[] { ItemFields.Tags }).Distinct().ToArray(); } - return _libraryManager.GetItemsResult(new InternalItemsQuery(user) + var result = _libraryManager.GetItemsResult(new InternalItemsQuery(user) { MediaTypes = new[] { MediaType.Video }, Recursive = true, - AncestorIds = folderIds.Select(i => i.ToString("N")).ToArray(folderIds.Count), + AncestorIds = folderIds.ToArray(folderIds.Count), IsFolder = false, IsVirtualItem = false, - Limit = query.Limit, + Limit = limit, StartIndex = query.StartIndex, - OrderBy = new[] { new Tuple<string, SortOrder>(ItemSortBy.DateCreated, SortOrder.Descending) }, + OrderBy = new[] { new ValueTuple<string, SortOrder>(ItemSortBy.DateCreated, SortOrder.Descending) }, EnableTotalRecordCount = query.EnableTotalRecordCount, IncludeItemTypes = includeItemTypes.ToArray(includeItemTypes.Count), ExcludeItemTypes = excludeItemTypes.ToArray(excludeItemTypes.Count), Genres = genres.ToArray(genres.Count), DtoOptions = dtoOptions }); - } - - public QueryResult<BaseItemDto> GetRecordingSeries(RecordingQuery query, DtoOptions options, CancellationToken cancellationToken) - { - var user = string.IsNullOrEmpty(query.UserId) ? null : _userManager.GetUserById(query.UserId); - if (user != null && !IsLiveTvEnabled(user)) - { - return new QueryResult<BaseItemDto>(); - } - - if (user == null || (query.IsInProgress ?? false)) - { - return new QueryResult<BaseItemDto>(); - } - - var folders = EmbyTV.EmbyTV.Current.GetRecordingFolders() - .SelectMany(i => i.Locations) - .Distinct(StringComparer.OrdinalIgnoreCase) - .Select(i => _libraryManager.FindByPath(i, true)) - .Where(i => i != null) - .Where(i => i.IsVisibleStandalone(user)) - .ToList(); - - if (folders.Count == 0) - { - return new QueryResult<BaseItemDto>(); - } - - var includeItemTypes = new List<string>(); - var excludeItemTypes = new List<string>(); - - includeItemTypes.Add(typeof(Series).Name); - - RemoveFields(options); - - var internalResult = _libraryManager.GetItemsResult(new InternalItemsQuery(user) - { - Recursive = true, - AncestorIds = folders.Select(i => i.Id.ToString("N")).ToArray(folders.Count), - Limit = query.Limit, - OrderBy = new[] { new Tuple<string, SortOrder>(ItemSortBy.DateCreated, SortOrder.Descending) }, - EnableTotalRecordCount = query.EnableTotalRecordCount, - IncludeItemTypes = includeItemTypes.ToArray(includeItemTypes.Count), - ExcludeItemTypes = excludeItemTypes.ToArray(excludeItemTypes.Count), - DtoOptions = options - }); - - var returnArray = _dtoService.GetBaseItemDtos(internalResult.Items, options, user); - - return new QueryResult<BaseItemDto> - { - Items = returnArray, - TotalRecordCount = internalResult.TotalRecordCount - }; - } - - public async Task<QueryResult<BaseItem>> GetInternalRecordings(RecordingQuery query, DtoOptions options, CancellationToken cancellationToken) - { - var user = string.IsNullOrEmpty(query.UserId) ? null : _userManager.GetUserById(query.UserId); - if (user != null && !IsLiveTvEnabled(user)) - { - return new QueryResult<BaseItem>(); - } - - var folder = GetInternalLiveTvFolder(cancellationToken); - - // TODO: Figure out how to merge emby recordings + service recordings - if (_services.Length == 1) - { - return GetEmbyRecordings(query, options, folder.Id, user); - } - - return await GetInternalRecordingsFromServices(query, user, options, folder.Id, cancellationToken).ConfigureAwait(false); - } - - private async Task<QueryResult<BaseItem>> GetInternalRecordingsFromServices(RecordingQuery query, User user, DtoOptions options, Guid internalLiveTvFolderId, CancellationToken cancellationToken) - { - await RefreshRecordings(internalLiveTvFolderId, cancellationToken).ConfigureAwait(false); - - var internalQuery = new InternalItemsQuery(user) - { - IncludeItemTypes = new[] { typeof(LiveTvVideoRecording).Name, typeof(LiveTvAudioRecording).Name }, - DtoOptions = options - }; - - if (!string.IsNullOrEmpty(query.ChannelId)) - { - internalQuery.ChannelIds = new[] { query.ChannelId }; - } - - var queryResult = _libraryManager.GetItemList(internalQuery); - IEnumerable<ILiveTvRecording> recordings = queryResult.Cast<ILiveTvRecording>(); - - if (!string.IsNullOrWhiteSpace(query.Id)) - { - var guid = new Guid(query.Id); - - recordings = recordings - .Where(i => i.Id == guid); - } - - if (!string.IsNullOrWhiteSpace(query.GroupId)) - { - var guid = new Guid(query.GroupId); - - recordings = recordings.Where(i => GetRecordingGroupIds(i).Contains(guid)); - } - - if (query.IsInProgress.HasValue) - { - var val = query.IsInProgress.Value; - recordings = recordings.Where(i => i.Status == RecordingStatus.InProgress == val); - } - - if (query.Status.HasValue) - { - var val = query.Status.Value; - recordings = recordings.Where(i => i.Status == val); - } - - if (query.IsMovie.HasValue) - { - var val = query.IsMovie.Value; - recordings = recordings.Where(i => i.IsMovie == val); - } - - if (query.IsNews.HasValue) - { - var val = query.IsNews.Value; - recordings = recordings.Where(i => i.IsNews == val); - } - - if (query.IsSeries.HasValue) - { - var val = query.IsSeries.Value; - recordings = recordings.Where(i => i.IsSeries == val); - } - - if (query.IsKids.HasValue) - { - var val = query.IsKids.Value; - recordings = recordings.Where(i => i.IsKids == val); - } - - if (query.IsSports.HasValue) - { - var val = query.IsSports.Value; - recordings = recordings.Where(i => i.IsSports == val); - } - - if (!string.IsNullOrEmpty(query.SeriesTimerId)) - { - var guid = new Guid(query.SeriesTimerId); - - recordings = recordings - .Where(i => _tvDtoService.GetInternalSeriesTimerId(i.ServiceName, i.SeriesTimerId) == guid); - } - - recordings = recordings.OrderByDescending(i => i.StartDate); - var entityList = recordings.ToList(); - IEnumerable<ILiveTvRecording> entities = entityList; - - if (query.StartIndex.HasValue) + if ((query.IsInProgress ?? false)) { - entities = entities.Skip(query.StartIndex.Value); - } + result.Items = result + .Items + .OfType<Video>() + .Where(i => !i.IsCompleteMedia) + .ToArray(); - if (query.Limit.HasValue) - { - entities = entities.Take(query.Limit.Value); + result.TotalRecordCount = result.Items.Length; } - return new QueryResult<BaseItem> - { - Items = entities.Cast<BaseItem>().ToArray(), - TotalRecordCount = entityList.Count - }; + return result; } - public async Task AddInfoToProgramDto(List<Tuple<BaseItem, BaseItemDto>> tuples, ItemFields[] fields, User user = null) + public Task AddInfoToProgramDto(List<Tuple<BaseItem, BaseItemDto>> tuples, ItemFields[] fields, User user = null) { - var programTuples = new List<Tuple<BaseItemDto, string, string, string>>(); + var programTuples = new List<Tuple<BaseItemDto, string, string>>(); var hasChannelImage = fields.Contains(ItemFields.ChannelImage); var hasChannelInfo = fields.Contains(ItemFields.ChannelInfo); - var hasServiceName = fields.Contains(ItemFields.ServiceName); foreach (var tuple in tuples) { @@ -1888,7 +1534,7 @@ namespace Emby.Server.Implementations.LiveTv if (hasChannelInfo || hasChannelImage) { - var channel = GetInternalChannel(program.ChannelId); + var channel = _libraryManager.GetItemById(program.ChannelId) as LiveTvChannel; if (channel != null) { @@ -1903,19 +1549,12 @@ namespace Emby.Server.Implementations.LiveTv } } - var serviceName = program.ServiceName; - - if (hasServiceName) - { - dto.ServiceName = serviceName; - } - var externalSeriesId = program.ExternalSeriesId; - programTuples.Add(new Tuple<BaseItemDto, string, string, string>(dto, serviceName, GetItemExternalId(program), externalSeriesId)); + programTuples.Add(new Tuple<BaseItemDto, string, string>(dto, program.ExternalId, externalSeriesId)); } - await AddRecordingInfo(programTuples, CancellationToken.None).ConfigureAwait(false); + return AddRecordingInfo(programTuples, CancellationToken.None); } public ActiveRecordingInfo GetActiveRecordingInfo(string path) @@ -1923,73 +1562,21 @@ namespace Emby.Server.Implementations.LiveTv return EmbyTV.EmbyTV.Current.GetActiveRecordingInfo(path); } - public void AddInfoToRecordingDto(BaseItem item, BaseItemDto dto, User user = null) - { - var recording = (ILiveTvRecording)item; - var service = GetService(recording); - - var channel = string.IsNullOrWhiteSpace(recording.ChannelId) ? null : GetInternalChannel(recording.ChannelId); - - var info = recording; - - dto.SeriesTimerId = string.IsNullOrEmpty(info.SeriesTimerId) || service == null - ? null - : _tvDtoService.GetInternalSeriesTimerId(service.Name, info.SeriesTimerId).ToString("N"); - - dto.TimerId = string.IsNullOrEmpty(info.TimerId) || service == null - ? null - : _tvDtoService.GetInternalTimerId(service.Name, info.TimerId).ToString("N"); - - dto.StartDate = info.StartDate; - dto.Status = info.Status.ToString(); - dto.IsRepeat = info.IsRepeat; - dto.EpisodeTitle = info.EpisodeTitle; - dto.IsMovie = info.IsMovie; - dto.IsSeries = info.IsSeries; - dto.IsSports = info.IsSports; - dto.IsLive = info.IsLive; - dto.IsNews = info.IsNews; - dto.IsKids = info.IsKids; - dto.IsPremiere = info.IsPremiere; - - if (info.Status == RecordingStatus.InProgress && info.EndDate.HasValue) - { - var now = DateTime.UtcNow.Ticks; - var start = info.StartDate.Ticks; - var end = info.EndDate.Value.Ticks; - - var pct = now - start; - pct /= end; - pct *= 100; - dto.CompletionPercentage = pct; - } - - if (channel != null) - { - dto.ChannelName = channel.Name; - - if (channel.HasImage(ImageType.Primary)) - { - dto.ChannelPrimaryImageTag = _tvDtoService.GetImageTag(channel); - } - } - } - public void AddInfoToRecordingDto(BaseItem item, BaseItemDto dto, ActiveRecordingInfo activeRecordingInfo, User user = null) { var service = EmbyTV.EmbyTV.Current; var info = activeRecordingInfo.Timer; - var channel = string.IsNullOrWhiteSpace(info.ChannelId) ? null : GetInternalChannel(_tvDtoService.GetInternalChannelId(service.Name, info.ChannelId)); + var channel = string.IsNullOrWhiteSpace(info.ChannelId) ? null : _libraryManager.GetItemById(_tvDtoService.GetInternalChannelId(service.Name, info.ChannelId)); dto.SeriesTimerId = string.IsNullOrEmpty(info.SeriesTimerId) ? null - : _tvDtoService.GetInternalSeriesTimerId(service.Name, info.SeriesTimerId).ToString("N"); + : _tvDtoService.GetInternalSeriesTimerId(info.SeriesTimerId).ToString("N"); dto.TimerId = string.IsNullOrEmpty(info.Id) ? null - : _tvDtoService.GetInternalTimerId(service.Name, info.Id).ToString("N"); + : _tvDtoService.GetInternalTimerId(info.Id); var startDate = info.StartDate; var endDate = info.EndDate; @@ -2034,13 +1621,13 @@ namespace Emby.Server.Implementations.LiveTv } } - public async Task<QueryResult<BaseItemDto>> GetRecordings(RecordingQuery query, DtoOptions options, CancellationToken cancellationToken) + public QueryResult<BaseItemDto> GetRecordings(RecordingQuery query, DtoOptions options) { - var user = string.IsNullOrEmpty(query.UserId) ? null : _userManager.GetUserById(query.UserId); + var user = query.UserId.Equals(Guid.Empty) ? null : _userManager.GetUserById(query.UserId); RemoveFields(options); - var internalResult = await GetInternalRecordings(query, options, cancellationToken).ConfigureAwait(false); + var internalResult = GetEmbyRecordings(query, options, user); var returnArray = _dtoService.GetBaseItemDtos(internalResult.Items, options, user); @@ -2051,7 +1638,7 @@ namespace Emby.Server.Implementations.LiveTv }; } - public async Task<QueryResult<TimerInfoDto>> GetTimers(TimerQuery query, CancellationToken cancellationToken) + private async Task<QueryResult<TimerInfo>> GetTimersInternal(TimerQuery query, CancellationToken cancellationToken) { var tasks = _services.Select(async i => { @@ -2104,7 +1691,7 @@ namespace Emby.Server.Implementations.LiveTv var guid = new Guid(query.SeriesTimerId); timers = timers - .Where(i => _tvDtoService.GetInternalSeriesTimerId(i.Item2.Name, i.Item1.SeriesTimerId) == guid); + .Where(i => _tvDtoService.GetInternalSeriesTimerId(i.Item1.SeriesTimerId) == guid); } if (!string.IsNullOrEmpty(query.Id)) @@ -2112,72 +1699,105 @@ namespace Emby.Server.Implementations.LiveTv var guid = new Guid(query.Id); timers = timers - .Where(i => _tvDtoService.GetInternalTimerId(i.Item2.Name, i.Item1.Id) == guid); + .Where(i => string.Equals(_tvDtoService.GetInternalTimerId(i.Item1.Id), query.Id, StringComparison.OrdinalIgnoreCase)); } - var returnList = new List<TimerInfoDto>(); - - foreach (var i in timers) - { - var program = string.IsNullOrEmpty(i.Item1.ProgramId) ? - null : - GetInternalProgram(_tvDtoService.GetInternalProgramId(i.Item2.Name, i.Item1.ProgramId).ToString("N")); - - var channel = string.IsNullOrEmpty(i.Item1.ChannelId) ? null : GetInternalChannel(_tvDtoService.GetInternalChannelId(i.Item2.Name, i.Item1.ChannelId)); - - returnList.Add(_tvDtoService.GetTimerInfoDto(i.Item1, i.Item2, program, channel)); - } - - var returnArray = returnList + var returnArray = timers + .Select(i => i.Item1) .OrderBy(i => i.StartDate) - .ToArray(returnList.Count); + .ToArray(); - return new QueryResult<TimerInfoDto> + return new QueryResult<TimerInfo> { Items = returnArray, TotalRecordCount = returnArray.Length }; } - public async Task DeleteRecording(string recordingId) + public async Task<QueryResult<TimerInfoDto>> GetTimers(TimerQuery query, CancellationToken cancellationToken) { - var recording = await GetInternalRecording(recordingId, CancellationToken.None).ConfigureAwait(false); + var tasks = _services.Select(async i => + { + try + { + var recs = await i.GetTimersAsync(cancellationToken).ConfigureAwait(false); + return recs.Select(r => new Tuple<TimerInfo, ILiveTvService>(r, i)); + } + catch (Exception ex) + { + _logger.ErrorException("Error getting recordings", ex); + return new List<Tuple<TimerInfo, ILiveTvService>>(); + } + }); + var results = await Task.WhenAll(tasks).ConfigureAwait(false); + var timers = results.SelectMany(i => i.ToList()); - if (recording == null) + if (query.IsActive.HasValue) { - throw new ResourceNotFoundException(string.Format("Recording with Id {0} not found", recordingId)); + if (query.IsActive.Value) + { + timers = timers.Where(i => i.Item1.Status == RecordingStatus.InProgress); + } + else + { + timers = timers.Where(i => i.Item1.Status != RecordingStatus.InProgress); + } } - await DeleteRecording((BaseItem)recording).ConfigureAwait(false); - } - - public async Task DeleteRecording(BaseItem recording) - { - var service = GetService(recording.ServiceName); - - if (service != null) + if (query.IsScheduled.HasValue) { - // handle the service being uninstalled and the item hanging around in the database - try + if (query.IsScheduled.Value) { - await service.DeleteRecordingAsync(GetItemExternalId(recording), CancellationToken.None).ConfigureAwait(false); + timers = timers.Where(i => i.Item1.Status == RecordingStatus.New); } - catch (ResourceNotFoundException) + else { - + timers = timers.Where(i => i.Item1.Status != RecordingStatus.New); } } - _lastRecordingRefreshTime = DateTime.MinValue; + if (!string.IsNullOrEmpty(query.ChannelId)) + { + var guid = new Guid(query.ChannelId); + timers = timers.Where(i => guid == _tvDtoService.GetInternalChannelId(i.Item2.Name, i.Item1.ChannelId)); + } + + if (!string.IsNullOrEmpty(query.SeriesTimerId)) + { + var guid = new Guid(query.SeriesTimerId); + + timers = timers + .Where(i => _tvDtoService.GetInternalSeriesTimerId(i.Item1.SeriesTimerId) == guid); + } + + if (!string.IsNullOrEmpty(query.Id)) + { + timers = timers + .Where(i => string.Equals(_tvDtoService.GetInternalTimerId(i.Item1.Id), query.Id, StringComparison.OrdinalIgnoreCase)); + } + + var returnList = new List<TimerInfoDto>(); - // This is the responsibility of the live tv service - await _libraryManager.DeleteItem((BaseItem)recording, new DeleteOptions + foreach (var i in timers) { - DeleteFileLocation = false + var program = string.IsNullOrEmpty(i.Item1.ProgramId) ? + null : + _libraryManager.GetItemById(_tvDtoService.GetInternalProgramId(i.Item1.ProgramId)) as LiveTvProgram; + + var channel = string.IsNullOrEmpty(i.Item1.ChannelId) ? null : _libraryManager.GetItemById(_tvDtoService.GetInternalChannelId(i.Item2.Name, i.Item1.ChannelId)); - }).ConfigureAwait(false); + returnList.Add(_tvDtoService.GetTimerInfoDto(i.Item1, i.Item2, program, channel)); + } - _lastRecordingRefreshTime = DateTime.MinValue; + var returnArray = returnList + .OrderBy(i => i.StartDate) + .ToArray(returnList.Count); + + return new QueryResult<TimerInfoDto> + { + Items = returnArray, + TotalRecordCount = returnArray.Length + }; } public async Task CancelTimer(string id) @@ -2192,15 +1812,17 @@ namespace Emby.Server.Implementations.LiveTv var service = GetService(timer.ServiceName); await service.CancelTimerAsync(timer.ExternalId, CancellationToken.None).ConfigureAwait(false); - _lastRecordingRefreshTime = DateTime.MinValue; - EventHelper.FireEventIfNotNull(TimerCancelled, this, new GenericEventArgs<TimerEventInfo> + if (!(service is EmbyTV.EmbyTV)) { - Argument = new TimerEventInfo + EventHelper.FireEventIfNotNull(TimerCancelled, this, new GenericEventArgs<TimerEventInfo> { - Id = id - } - }, _logger); + Argument = new TimerEventInfo + { + Id = id + } + }, _logger); + } } public async Task CancelSeriesTimer(string id) @@ -2215,7 +1837,6 @@ namespace Emby.Server.Implementations.LiveTv var service = GetService(timer.ServiceName); await service.CancelSeriesTimerAsync(timer.ExternalId, CancellationToken.None).ConfigureAwait(false); - _lastRecordingRefreshTime = DateTime.MinValue; EventHelper.FireEventIfNotNull(SeriesTimerCancelled, this, new GenericEventArgs<TimerEventInfo> { @@ -2226,18 +1847,6 @@ namespace Emby.Server.Implementations.LiveTv }, _logger); } - public async Task<BaseItemDto> GetRecording(string id, DtoOptions options, CancellationToken cancellationToken, User user = null) - { - var item = await GetInternalRecording(id, cancellationToken).ConfigureAwait(false); - - if (item == null) - { - return null; - } - - return _dtoService.GetBaseItemDto((BaseItem)item, options, user); - } - public async Task<TimerInfoDto> GetTimer(string id, CancellationToken cancellationToken) { var results = await GetTimers(new TimerQuery @@ -2345,7 +1954,7 @@ namespace Emby.Server.Implementations.LiveTv if (!string.IsNullOrEmpty(i.Item1.ChannelId)) { var internalChannelId = _tvDtoService.GetInternalChannelId(i.Item2.Name, i.Item1.ChannelId); - var channel = GetInternalChannel(internalChannelId); + var channel = _libraryManager.GetItemById(internalChannelId); channelName = channel == null ? null : channel.Name; } @@ -2361,11 +1970,17 @@ namespace Emby.Server.Implementations.LiveTv }; } + public BaseItem GetLiveTvChannel(TimerInfo timer, ILiveTvService service) + { + var internalChannelId = _tvDtoService.GetInternalChannelId(service.Name, timer.ChannelId); + return _libraryManager.GetItemById(internalChannelId); + } + public void AddChannelInfo(List<Tuple<BaseItemDto, LiveTvChannel>> tuples, DtoOptions options, User user) { var now = DateTime.UtcNow; - var channelIds = tuples.Select(i => i.Item2.Id.ToString("N")).Distinct().ToArray(); + var channelIds = tuples.Select(i => i.Item2.Id).Distinct().ToArray(); var programs = options.AddCurrentProgram ? _libraryManager.GetItemList(new InternalItemsQuery(user) { @@ -2374,8 +1989,8 @@ namespace Emby.Server.Implementations.LiveTv MaxStartDate = now, MinEndDate = now, Limit = channelIds.Length, - OrderBy = new[] { new Tuple<string, SortOrder>(ItemSortBy.StartDate, SortOrder.Ascending) }, - TopParentIds = new[] { GetInternalLiveTvFolder(CancellationToken.None).Id.ToString("N") }, + OrderBy = new[] { new ValueTuple<string, SortOrder>(ItemSortBy.StartDate, SortOrder.Ascending) }, + TopParentIds = new[] { GetInternalLiveTvFolder(CancellationToken.None).Id }, DtoOptions = options }) : new List<BaseItem>(); @@ -2383,10 +1998,9 @@ namespace Emby.Server.Implementations.LiveTv RemoveFields(options); var currentProgramsList = new List<BaseItem>(); - var currentChannelsDict = new Dictionary<string, BaseItemDto>(); + var currentChannelsDict = new Dictionary<Guid, BaseItemDto>(); var addCurrentProgram = options.AddCurrentProgram; - var addServiceName = options.Fields.Contains(ItemFields.ServiceName); foreach (var tuple in tuples) { @@ -2397,17 +2011,11 @@ namespace Emby.Server.Implementations.LiveTv dto.ChannelNumber = channel.Number; dto.ChannelType = channel.ChannelType; - if (addServiceName) - { - dto.ServiceName = channel.ServiceName; - } - currentChannelsDict[dto.Id] = dto; if (addCurrentProgram) { - var channelIdString = channel.Id.ToString("N"); - var currentProgram = programs.FirstOrDefault(i => string.Equals(i.ChannelId, channelIdString)); + var currentProgram = programs.FirstOrDefault(i => channel.Id.Equals(i.ChannelId)); if (currentProgram != null) { @@ -2422,13 +2030,10 @@ namespace Emby.Server.Implementations.LiveTv foreach (var programDto in currentProgramDtos) { - if (!string.IsNullOrWhiteSpace(programDto.ChannelId)) + BaseItemDto channelDto; + if (currentChannelsDict.TryGetValue(programDto.ChannelId, out channelDto)) { - BaseItemDto channelDto; - if (currentChannelsDict.TryGetValue(programDto.ChannelId, out channelDto)) - { - channelDto.CurrentProgram = programDto; - } + channelDto.CurrentProgram = programDto; } } } @@ -2436,25 +2041,30 @@ namespace Emby.Server.Implementations.LiveTv private async Task<Tuple<SeriesTimerInfo, ILiveTvService>> GetNewTimerDefaultsInternal(CancellationToken cancellationToken, LiveTvProgram program = null) { - var service = program != null && !string.IsNullOrWhiteSpace(program.ServiceName) ? + var service = program != null ? GetService(program) : - _services.FirstOrDefault(); + null; + + if (service == null) + { + service = _services.First(); + } ProgramInfo programInfo = null; if (program != null) { - var channel = GetInternalChannel(program.ChannelId); + var channel = _libraryManager.GetItemById(program.ChannelId); programInfo = new ProgramInfo { Audio = program.Audio, - ChannelId = GetItemExternalId(channel), + ChannelId = channel.ExternalId, CommunityRating = program.CommunityRating, EndDate = program.EndDate ?? DateTime.MinValue, EpisodeTitle = program.EpisodeTitle, - Genres = program.Genres, - Id = GetItemExternalId(program), + Genres = program.Genres.ToList(), + Id = program.ExternalId, IsHD = program.IsHD, IsKids = program.IsKids, IsLive = program.IsLive, @@ -2503,7 +2113,7 @@ namespace Emby.Server.Implementations.LiveTv public async Task<SeriesTimerInfoDto> GetNewTimerDefaults(string programId, CancellationToken cancellationToken) { - var program = GetInternalProgram(programId); + var program = (LiveTvProgram)_libraryManager.GetItemById(programId); var programDto = await GetProgram(programId, cancellationToken).ConfigureAwait(false); var defaults = await GetNewTimerDefaultsInternal(cancellationToken, program).ConfigureAwait(false); @@ -2519,8 +2129,8 @@ namespace Emby.Server.Implementations.LiveTv info.StartDate = program.StartDate; info.Name = program.Name; info.Overview = program.Overview; - info.ProgramId = programDto.Id; - info.ExternalProgramId = GetItemExternalId(program); + info.ProgramId = programDto.Id.ToString("N"); + info.ExternalProgramId = program.ExternalId; if (program.EndDate.HasValue) { @@ -2545,24 +2155,26 @@ namespace Emby.Server.Implementations.LiveTv if (supportsNewTimerIds != null) { newTimerId = await supportsNewTimerIds.CreateTimer(info, cancellationToken).ConfigureAwait(false); - newTimerId = _tvDtoService.GetInternalTimerId(timer.ServiceName, newTimerId).ToString("N"); + newTimerId = _tvDtoService.GetInternalTimerId(newTimerId); } else { await service.CreateTimerAsync(info, cancellationToken).ConfigureAwait(false); } - _lastRecordingRefreshTime = DateTime.MinValue; _logger.Info("New recording scheduled"); - EventHelper.FireEventIfNotNull(TimerCreated, this, new GenericEventArgs<TimerEventInfo> + if (!(service is EmbyTV.EmbyTV)) { - Argument = new TimerEventInfo + EventHelper.FireEventIfNotNull(TimerCreated, this, new GenericEventArgs<TimerEventInfo> { - ProgramId = _tvDtoService.GetInternalProgramId(timer.ServiceName, info.ProgramId).ToString("N"), - Id = newTimerId - } - }, _logger); + Argument = new TimerEventInfo + { + ProgramId = _tvDtoService.GetInternalProgramId(info.ProgramId), + Id = newTimerId + } + }, _logger); + } } public async Task CreateSeriesTimer(SeriesTimerInfoDto timer, CancellationToken cancellationToken) @@ -2588,20 +2200,18 @@ namespace Emby.Server.Implementations.LiveTv if (supportsNewTimerIds != null) { newTimerId = await supportsNewTimerIds.CreateSeriesTimer(info, cancellationToken).ConfigureAwait(false); - newTimerId = _tvDtoService.GetInternalSeriesTimerId(timer.ServiceName, newTimerId).ToString("N"); + newTimerId = _tvDtoService.GetInternalSeriesTimerId(newTimerId).ToString("N"); } else { await service.CreateSeriesTimerAsync(info, cancellationToken).ConfigureAwait(false); } - _lastRecordingRefreshTime = DateTime.MinValue; - EventHelper.FireEventIfNotNull(SeriesTimerCreated, this, new GenericEventArgs<TimerEventInfo> { Argument = new TimerEventInfo { - ProgramId = _tvDtoService.GetInternalProgramId(timer.ServiceName, info.ProgramId).ToString("N"), + ProgramId = _tvDtoService.GetInternalProgramId(info.ProgramId), Id = newTimerId } }, _logger); @@ -2614,7 +2224,6 @@ namespace Emby.Server.Implementations.LiveTv var service = GetService(timer.ServiceName); await service.UpdateTimerAsync(info, cancellationToken).ConfigureAwait(false); - _lastRecordingRefreshTime = DateTime.MinValue; } public async Task UpdateSeriesTimer(SeriesTimerInfoDto timer, CancellationToken cancellationToken) @@ -2624,138 +2233,6 @@ namespace Emby.Server.Implementations.LiveTv var service = GetService(timer.ServiceName); await service.UpdateSeriesTimerAsync(info, cancellationToken).ConfigureAwait(false); - _lastRecordingRefreshTime = DateTime.MinValue; - } - - private IEnumerable<string> GetRecordingGroupNames(ILiveTvRecording recording) - { - var list = new List<string>(); - - if (recording.IsSeries) - { - list.Add(recording.Name); - } - - if (recording.IsKids) - { - list.Add("Kids"); - } - - if (recording.IsMovie) - { - list.Add("Movies"); - } - - if (recording.IsNews) - { - list.Add("News"); - } - - if (recording.IsSports) - { - list.Add("Sports"); - } - - if (!recording.IsSports && !recording.IsNews && !recording.IsMovie && !recording.IsKids && !recording.IsSeries) - { - list.Add("Others"); - } - - return list; - } - - private List<Guid> GetRecordingGroupIds(ILiveTvRecording recording) - { - return GetRecordingGroupNames(recording).Select(i => i.ToLower() - .GetMD5()) - .ToList(); - } - - public async Task<QueryResult<BaseItemDto>> GetRecordingGroups(RecordingGroupQuery query, CancellationToken cancellationToken) - { - var recordingResult = await GetInternalRecordings(new RecordingQuery - { - UserId = query.UserId - - }, new DtoOptions(), cancellationToken).ConfigureAwait(false); - - var embyServiceName = EmbyTV.EmbyTV.Current.Name; - var recordings = recordingResult.Items.Where(i => !string.Equals(i.ServiceName, embyServiceName, StringComparison.OrdinalIgnoreCase)).OfType<ILiveTvRecording>().ToList(); - - var groups = new List<BaseItemDto>(); - - var series = recordings - .Where(i => i.IsSeries) - .ToLookup(i => i.Name, StringComparer.OrdinalIgnoreCase); - - groups.AddRange(series.OrderByString(i => i.Key).Select(i => new BaseItemDto - { - Name = i.Key, - RecordingCount = i.Count() - })); - - groups.Add(new BaseItemDto - { - Name = "Kids", - RecordingCount = recordings.Count(i => i.IsKids) - }); - - groups.Add(new BaseItemDto - { - Name = "Movies", - RecordingCount = recordings.Count(i => i.IsMovie) - }); - - groups.Add(new BaseItemDto - { - Name = "News", - RecordingCount = recordings.Count(i => i.IsNews) - }); - - groups.Add(new BaseItemDto - { - Name = "Sports", - RecordingCount = recordings.Count(i => i.IsSports) - }); - - groups.Add(new BaseItemDto - { - Name = "Others", - RecordingCount = recordings.Count(i => !i.IsSports && !i.IsNews && !i.IsMovie && !i.IsKids && !i.IsSeries) - }); - - groups = groups - .Where(i => i.RecordingCount > 0) - .ToList(); - - foreach (var group in groups) - { - group.Id = group.Name.ToLower().GetMD5().ToString("N"); - } - - return new QueryResult<BaseItemDto> - { - Items = groups.ToArray(groups.Count), - TotalRecordCount = groups.Count - }; - } - - public async Task CloseLiveStream(string id) - { - var parts = id.Split(new[] { '_' }, 2); - - var service = _services.FirstOrDefault(i => string.Equals(i.GetType().FullName.GetMD5().ToString("N"), parts[0], StringComparison.OrdinalIgnoreCase)); - - if (service == null) - { - throw new ArgumentException("Service not found."); - } - - id = parts[1]; - - _logger.Info("Closing live stream from {0}, stream Id: {1}", service.Name, id); - - await service.CloseLiveStream(id, CancellationToken.None).ConfigureAwait(false); } public GuideInfo GetGuideInfo() @@ -2776,7 +2253,6 @@ namespace Emby.Server.Implementations.LiveTv public void Dispose() { Dispose(true); - GC.SuppressFinalize(this); } private bool _isDisposed = false; @@ -2792,66 +2268,22 @@ namespace Emby.Server.Implementations.LiveTv } } - private async Task<LiveTvServiceInfo[]> GetServiceInfos(CancellationToken cancellationToken) + private LiveTvServiceInfo[] GetServiceInfos() { - var tasks = Services.Select(i => GetServiceInfo(i, cancellationToken)); - - return await Task.WhenAll(tasks).ConfigureAwait(false); + return Services.Select(GetServiceInfo).ToArray(); } - private async Task<LiveTvServiceInfo> GetServiceInfo(ILiveTvService service, CancellationToken cancellationToken) + private LiveTvServiceInfo GetServiceInfo(ILiveTvService service) { - var info = new LiveTvServiceInfo + return new LiveTvServiceInfo { Name = service.Name }; - - var tunerIdPrefix = service.GetType().FullName.GetMD5().ToString("N") + "_"; - - try - { - var statusInfo = await service.GetStatusInfoAsync(cancellationToken).ConfigureAwait(false); - - info.Status = statusInfo.Status; - info.StatusMessage = statusInfo.StatusMessage; - info.Version = statusInfo.Version; - info.HasUpdateAvailable = statusInfo.HasUpdateAvailable; - info.HomePageUrl = service.HomePageUrl; - info.IsVisible = statusInfo.IsVisible; - - info.Tuners = statusInfo.Tuners.Select(i => - { - string channelName = null; - - if (!string.IsNullOrEmpty(i.ChannelId)) - { - var internalChannelId = _tvDtoService.GetInternalChannelId(service.Name, i.ChannelId); - var channel = GetInternalChannel(internalChannelId); - channelName = channel == null ? null : channel.Name; - } - - var dto = _tvDtoService.GetTunerInfoDto(service.Name, i, channelName); - - dto.Id = tunerIdPrefix + dto.Id; - - return dto; - - }).ToArray(); - } - catch (Exception ex) - { - _logger.ErrorException("Error getting service status info from {0}", ex, service.Name ?? string.Empty); - - info.Status = LiveTvServiceStatus.Unavailable; - info.StatusMessage = ex.Message; - } - - return info; } - public async Task<LiveTvInfo> GetLiveTvInfo(CancellationToken cancellationToken) + public LiveTvInfo GetLiveTvInfo(CancellationToken cancellationToken) { - var services = await GetServiceInfos(CancellationToken.None).ConfigureAwait(false); + var services = GetServiceInfos(); var info = new LiveTvInfo { @@ -2898,15 +2330,6 @@ namespace Emby.Server.Implementations.LiveTv return service.ResetTuner(parts[1], cancellationToken); } - public BaseItemDto GetLiveTvFolder(string userId, CancellationToken cancellationToken) - { - var user = string.IsNullOrEmpty(userId) ? null : _userManager.GetUserById(userId); - - var folder = GetInternalLiveTvFolder(cancellationToken); - - return _dtoService.GetBaseItemDto(folder, new DtoOptions(), user); - } - private void RemoveFields(DtoOptions options) { var fields = options.Fields.ToList(); @@ -2921,7 +2344,7 @@ namespace Emby.Server.Implementations.LiveTv public Folder GetInternalLiveTvFolder(CancellationToken cancellationToken) { var name = _localization.GetLocalizedString("HeaderLiveTV"); - return _libraryManager.GetNamedView(name, CollectionType.LiveTv, name, cancellationToken); + return _libraryManager.GetNamedView(name, CollectionType.LiveTv, name); } public async Task<TunerHostInfo> SaveTunerHost(TunerHostInfo info, bool dataSourceChanged = true) @@ -2990,7 +2413,6 @@ namespace Emby.Server.Implementations.LiveTv info.Id = Guid.NewGuid().ToString("N"); list.Add(info); config.ListingProviders = list.ToArray(list.Count); - info.EnableNewProgramIds = true; } else { @@ -3146,9 +2568,38 @@ namespace Emby.Server.Implementations.LiveTv return _tvDtoService.GetInternalChannelId(serviceName, externalId); } - public Guid GetInternalProgramId(string serviceName, string externalId) + public Guid GetInternalProgramId(string externalId) + { + return _tvDtoService.GetInternalProgramId(externalId); + } + + public List<BaseItem> GetRecordingFolders(User user) + { + return GetRecordingFolders(user, false); + } + + private List<BaseItem> GetRecordingFolders(User user, bool refreshChannels) { - return _tvDtoService.GetInternalProgramId(serviceName, externalId); + var folders = EmbyTV.EmbyTV.Current.GetRecordingFolders() + .SelectMany(i => i.Locations) + .Distinct(StringComparer.OrdinalIgnoreCase) + .Select(i => _libraryManager.FindByPath(i, true)) + .Where(i => i != null) + .Where(i => i.IsVisibleStandalone(user)) + .SelectMany(i => _libraryManager.GetCollectionFolders(i)) + .DistinctBy(i => i.Id) + .OrderBy(i => i.SortName) + .ToList(); + + folders.AddRange(_channelManager().GetChannelsInternal(new MediaBrowser.Model.Channels.ChannelQuery + { + UserId = user.Id, + IsRecordingsFolder = true, + RefreshLatestChannelItems = refreshChannels + + }).Items); + + return folders.Cast<BaseItem>().ToList(); } } }
\ No newline at end of file diff --git a/Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs b/Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs index 29b7c41ef..808672d46 100644 --- a/Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs +++ b/Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs @@ -15,6 +15,7 @@ using System.Threading; using System.Threading.Tasks; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Extensions; +using MediaBrowser.Common.Configuration; namespace Emby.Server.Implementations.LiveTv { @@ -26,8 +27,9 @@ namespace Emby.Server.Implementations.LiveTv private readonly IMediaSourceManager _mediaSourceManager; private readonly IMediaEncoder _mediaEncoder; private readonly IServerApplicationHost _appHost; + private IApplicationPaths _appPaths; - public LiveTvMediaSourceProvider(ILiveTvManager liveTvManager, IJsonSerializer jsonSerializer, ILogManager logManager, IMediaSourceManager mediaSourceManager, IMediaEncoder mediaEncoder, IServerApplicationHost appHost) + public LiveTvMediaSourceProvider(ILiveTvManager liveTvManager, IApplicationPaths appPaths, IJsonSerializer jsonSerializer, ILogManager logManager, IMediaSourceManager mediaSourceManager, IMediaEncoder mediaEncoder, IServerApplicationHost appHost) { _liveTvManager = liveTvManager; _jsonSerializer = jsonSerializer; @@ -35,9 +37,10 @@ namespace Emby.Server.Implementations.LiveTv _mediaEncoder = mediaEncoder; _appHost = appHost; _logger = logManager.GetLogger(GetType().Name); + _appPaths = appPaths; } - public Task<IEnumerable<MediaSourceInfo>> GetMediaSources(IHasMediaSources item, CancellationToken cancellationToken) + public Task<IEnumerable<MediaSourceInfo>> GetMediaSources(BaseItem item, CancellationToken cancellationToken) { var baseItem = (BaseItem)item; @@ -45,20 +48,20 @@ namespace Emby.Server.Implementations.LiveTv { var activeRecordingInfo = _liveTvManager.GetActiveRecordingInfo(item.Path); - if (string.IsNullOrWhiteSpace(baseItem.Path) || activeRecordingInfo != null) + if (string.IsNullOrEmpty(baseItem.Path) || activeRecordingInfo != null) { return GetMediaSourcesInternal(item, activeRecordingInfo, cancellationToken); } } - return Task.FromResult<IEnumerable<MediaSourceInfo>>(new List<MediaSourceInfo>()); + return Task.FromResult<IEnumerable<MediaSourceInfo>>(Array.Empty<MediaSourceInfo>()); } // Do not use a pipe here because Roku http requests to the server will fail, without any explicit error message. private const char StreamIdDelimeter = '_'; private const string StreamIdDelimeterString = "_"; - private async Task<IEnumerable<MediaSourceInfo>> GetMediaSourcesInternal(IHasMediaSources item, ActiveRecordingInfo activeRecordingInfo, CancellationToken cancellationToken) + private async Task<IEnumerable<MediaSourceInfo>> GetMediaSourcesInternal(BaseItem item, ActiveRecordingInfo activeRecordingInfo, CancellationToken cancellationToken) { IEnumerable<MediaSourceInfo> sources; @@ -66,30 +69,20 @@ namespace Emby.Server.Implementations.LiveTv try { - if (item is ILiveTvRecording) + if (activeRecordingInfo != null) { - sources = await _liveTvManager.GetRecordingMediaSources(item, cancellationToken) + sources = await EmbyTV.EmbyTV.Current.GetRecordingStreamMediaSources(activeRecordingInfo, cancellationToken) .ConfigureAwait(false); } else { - if (activeRecordingInfo != null) - { - sources = await EmbyTV.EmbyTV.Current.GetRecordingStreamMediaSources(activeRecordingInfo, cancellationToken) - .ConfigureAwait(false); - } - else - { - sources = await _liveTvManager.GetChannelMediaSources(item, cancellationToken) - .ConfigureAwait(false); - } + sources = await _liveTvManager.GetChannelMediaSources(item, cancellationToken) + .ConfigureAwait(false); } } catch (NotImplementedException) { - var hasMediaSources = (IHasMediaSources)item; - - sources = _mediaSourceManager.GetStaticMediaSources(hasMediaSources, false); + sources = _mediaSourceManager.GetStaticMediaSources(item, false); forceRequireOpening = true; } @@ -128,102 +121,15 @@ namespace Emby.Server.Implementations.LiveTv return list; } - public async Task<Tuple<MediaSourceInfo, IDirectStreamProvider>> OpenMediaSource(string openToken, bool allowLiveStreamProbe, CancellationToken cancellationToken) + public async Task<ILiveStream> OpenMediaSource(string openToken, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken) { - MediaSourceInfo stream = null; - const bool isAudio = false; - var keys = openToken.Split(new[] { StreamIdDelimeter }, 3); var mediaSourceId = keys.Length >= 3 ? keys[2] : null; - IDirectStreamProvider directStreamProvider = null; - - if (string.Equals(keys[0], typeof(LiveTvChannel).Name, StringComparison.OrdinalIgnoreCase)) - { - var info = await _liveTvManager.GetChannelStream(keys[1], mediaSourceId, cancellationToken).ConfigureAwait(false); - stream = info.Item1; - directStreamProvider = info.Item2; - - //allowLiveStreamProbe = false; - } - else - { - stream = await _liveTvManager.GetRecordingStream(keys[1], cancellationToken).ConfigureAwait(false); - } - - try - { - if (!allowLiveStreamProbe || !stream.SupportsProbing || stream.MediaStreams.Any(i => i.Index != -1)) - { - AddMediaInfo(stream, isAudio, cancellationToken); - } - else - { - await new LiveStreamHelper(_mediaEncoder, _logger).AddMediaInfoWithProbe(stream, isAudio, cancellationToken).ConfigureAwait(false); - } - } - catch (Exception ex) - { - _logger.ErrorException("Error probing live tv stream", ex); - } - - _logger.Info("Live stream info: {0}", _jsonSerializer.SerializeToString(stream)); - return new Tuple<MediaSourceInfo, IDirectStreamProvider>(stream, directStreamProvider); - } - - private void AddMediaInfo(MediaSourceInfo mediaSource, bool isAudio, CancellationToken cancellationToken) - { - mediaSource.DefaultSubtitleStreamIndex = null; - // Null this out so that it will be treated like a live stream - mediaSource.RunTimeTicks = null; + var info = await _liveTvManager.GetChannelStream(keys[1], mediaSourceId, currentLiveStreams, cancellationToken).ConfigureAwait(false); + var liveStream = info.Item2; - var audioStream = mediaSource.MediaStreams.FirstOrDefault(i => i.Type == MediaBrowser.Model.Entities.MediaStreamType.Audio); - - if (audioStream == null || audioStream.Index == -1) - { - mediaSource.DefaultAudioStreamIndex = null; - } - else - { - mediaSource.DefaultAudioStreamIndex = audioStream.Index; - } - - var videoStream = mediaSource.MediaStreams.FirstOrDefault(i => i.Type == MediaBrowser.Model.Entities.MediaStreamType.Video); - if (videoStream != null) - { - if (!videoStream.BitRate.HasValue) - { - var width = videoStream.Width ?? 1920; - - if (width >= 3000) - { - videoStream.BitRate = 30000000; - } - - else if (width >= 1900) - { - videoStream.BitRate = 20000000; - } - - else if (width >= 1200) - { - videoStream.BitRate = 8000000; - } - - else if (width >= 700) - { - videoStream.BitRate = 2000000; - } - } - } - - // Try to estimate this - mediaSource.InferTotalBitrate(); - } - - public Task CloseMediaSource(string liveStreamId) - { - return _liveTvManager.CloseLiveStream(liveStreamId); + return liveStream; } } } diff --git a/Emby.Server.Implementations/LiveTv/RecordingImageProvider.cs b/Emby.Server.Implementations/LiveTv/RecordingImageProvider.cs deleted file mode 100644 index 992badbb5..000000000 --- a/Emby.Server.Implementations/LiveTv/RecordingImageProvider.cs +++ /dev/null @@ -1,82 +0,0 @@ -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.LiveTv; -using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Entities; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; - -namespace Emby.Server.Implementations.LiveTv -{ - public class RecordingImageProvider : IDynamicImageProvider, IHasItemChangeMonitor - { - private readonly ILiveTvManager _liveTvManager; - - public RecordingImageProvider(ILiveTvManager liveTvManager) - { - _liveTvManager = liveTvManager; - } - - public IEnumerable<ImageType> GetSupportedImages(IHasMetadata item) - { - return new[] { ImageType.Primary }; - } - - public async Task<DynamicImageResponse> GetImage(IHasMetadata item, ImageType type, CancellationToken cancellationToken) - { - var liveTvItem = (ILiveTvRecording)item; - - var imageResponse = new DynamicImageResponse(); - - var service = _liveTvManager.Services.FirstOrDefault(i => string.Equals(i.Name, liveTvItem.ServiceName, StringComparison.OrdinalIgnoreCase)); - - if (service != null) - { - try - { - var response = await service.GetRecordingImageAsync(liveTvItem.ExternalId, cancellationToken).ConfigureAwait(false); - - if (response != null) - { - imageResponse.HasImage = true; - imageResponse.Stream = response.Stream; - imageResponse.Format = response.Format; - } - } - catch (NotImplementedException) - { - } - } - - return imageResponse; - } - - public string Name - { - get { return "Live TV Service Provider"; } - } - - public bool Supports(IHasMetadata item) - { - return item is ILiveTvRecording; - } - - public int Order - { - get { return 0; } - } - - public bool HasChanged(IHasMetadata item, IDirectoryService directoryService) - { - var liveTvItem = item as ILiveTvRecording; - - if (liveTvItem != null) - { - return !liveTvItem.HasImage(ImageType.Primary); - } - return false; - } - } -} diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs index 45e96c36d..ca5e51971 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs @@ -16,6 +16,7 @@ using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.IO; using MediaBrowser.Model.Serialization; +using MediaBrowser.Controller.Library; namespace Emby.Server.Implementations.LiveTv.TunerHosts { @@ -55,22 +56,18 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts ChannelCache cache = null; var key = tuner.Id; - if (enableCache && !string.IsNullOrWhiteSpace(key) && _channelCache.TryGetValue(key, out cache)) + if (enableCache && !string.IsNullOrEmpty(key) && _channelCache.TryGetValue(key, out cache)) { - if (DateTime.UtcNow - cache.Date < TimeSpan.FromMinutes(60)) - { - return cache.Channels.ToList(); - } + return cache.Channels.ToList(); } var result = await GetChannelsInternal(tuner, cancellationToken).ConfigureAwait(false); var list = result.ToList(); - Logger.Info("Channels from {0}: {1}", tuner.Url, JsonSerializer.SerializeToString(list)); + //Logger.Info("Channels from {0}: {1}", tuner.Url, JsonSerializer.SerializeToString(list)); - if (!string.IsNullOrWhiteSpace(key) && list.Count > 0) + if (!string.IsNullOrEmpty(key) && list.Count > 0) { cache = cache ?? new ChannelCache(); - cache.Date = DateTime.UtcNow; cache.Channels = list; _channelCache.AddOrUpdate(key, cache, (k, v) => cache); } @@ -137,11 +134,11 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts return list; } - protected abstract Task<List<MediaSourceInfo>> GetChannelStreamMediaSources(TunerHostInfo tuner, string channelId, CancellationToken cancellationToken); + protected abstract Task<List<MediaSourceInfo>> GetChannelStreamMediaSources(TunerHostInfo tuner, ChannelInfo channel, CancellationToken cancellationToken); public async Task<List<MediaSourceInfo>> GetChannelStreamMediaSources(string channelId, CancellationToken cancellationToken) { - if (string.IsNullOrWhiteSpace(channelId)) + if (string.IsNullOrEmpty(channelId)) { throw new ArgumentNullException("channelId"); } @@ -150,17 +147,16 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts { var hosts = GetTunerHosts(); - var hostsWithChannel = new List<TunerHostInfo>(); - foreach (var host in hosts) { try { var channels = await GetChannels(host, true, cancellationToken).ConfigureAwait(false); + var channelInfo = channels.FirstOrDefault(i => string.Equals(i.Id, channelId, StringComparison.OrdinalIgnoreCase)); - if (channels.Any(i => string.Equals(i.Id, channelId, StringComparison.OrdinalIgnoreCase))) + if (channelInfo != null) { - hostsWithChannel.Add(host); + return await GetChannelStreamMediaSources(host, channelInfo, cancellationToken).ConfigureAwait(false); } } catch (Exception ex) @@ -168,44 +164,16 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts Logger.Error("Error getting channels", ex); } } - - foreach (var host in hostsWithChannel) - { - try - { - // Check to make sure the tuner is available - // If there's only one tuner, don't bother with the check and just let the tuner be the one to throw an error - if (hostsWithChannel.Count > 1 && !await IsAvailable(host, channelId, cancellationToken).ConfigureAwait(false)) - { - Logger.Error("Tuner is not currently available"); - continue; - } - - var mediaSources = await GetChannelStreamMediaSources(host, channelId, cancellationToken).ConfigureAwait(false); - - // Prefix the id with the host Id so that we can easily find it - foreach (var mediaSource in mediaSources) - { - mediaSource.Id = host.Id + mediaSource.Id; - } - - return mediaSources; - } - catch (Exception ex) - { - Logger.Error("Error opening tuner", ex); - } - } } return new List<MediaSourceInfo>(); } - protected abstract Task<ILiveStream> GetChannelStream(TunerHostInfo tuner, string channelId, string streamId, CancellationToken cancellationToken); + protected abstract Task<ILiveStream> GetChannelStream(TunerHostInfo tuner, ChannelInfo channel, string streamId, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken); - public async Task<ILiveStream> GetChannelStream(string channelId, string streamId, CancellationToken cancellationToken) + public async Task<ILiveStream> GetChannelStream(string channelId, string streamId, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken) { - if (string.IsNullOrWhiteSpace(channelId)) + if (string.IsNullOrEmpty(channelId)) { throw new ArgumentNullException("channelId"); } @@ -217,44 +185,34 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts var hosts = GetTunerHosts(); - var hostsWithChannel = new List<TunerHostInfo>(); + var hostsWithChannel = new List<Tuple<TunerHostInfo, ChannelInfo>>(); foreach (var host in hosts) { - if (string.IsNullOrWhiteSpace(streamId)) + try { - try - { - var channels = await GetChannels(host, true, cancellationToken).ConfigureAwait(false); + var channels = await GetChannels(host, true, cancellationToken).ConfigureAwait(false); + var channelInfo = channels.FirstOrDefault(i => string.Equals(i.Id, channelId, StringComparison.OrdinalIgnoreCase)); - if (channels.Any(i => string.Equals(i.Id, channelId, StringComparison.OrdinalIgnoreCase))) - { - hostsWithChannel.Add(host); - } - } - catch (Exception ex) + if (channelInfo != null) { - Logger.Error("Error getting channels", ex); + hostsWithChannel.Add(new Tuple<TunerHostInfo, ChannelInfo>(host, channelInfo)); } } - else if (streamId.StartsWith(host.Id, StringComparison.OrdinalIgnoreCase)) + catch (Exception ex) { - hostsWithChannel = new List<TunerHostInfo> { host }; - streamId = streamId.Substring(host.Id.Length); - break; + Logger.Error("Error getting channels", ex); } } - foreach (var host in hostsWithChannel) + foreach (var hostTuple in hostsWithChannel) { - if (!channelId.StartsWith(ChannelIdPrefix, StringComparison.OrdinalIgnoreCase)) - { - continue; - } + var host = hostTuple.Item1; + var channelInfo = hostTuple.Item2; try { - var liveStream = await GetChannelStream(host, channelId, streamId, cancellationToken).ConfigureAwait(false); + var liveStream = await GetChannelStream(host, channelInfo, streamId, currentLiveStreams, cancellationToken).ConfigureAwait(false); var startTime = DateTime.UtcNow; await liveStream.Open(cancellationToken).ConfigureAwait(false); var endTime = DateTime.UtcNow; @@ -270,21 +228,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts throw new LiveTvConflictException(); } - protected async Task<bool> IsAvailable(TunerHostInfo tuner, string channelId, CancellationToken cancellationToken) - { - try - { - return await IsAvailableInternal(tuner, channelId, cancellationToken).ConfigureAwait(false); - } - catch (Exception ex) - { - Logger.ErrorException("Error checking tuner availability", ex); - return false; - } - } - - protected abstract Task<bool> IsAvailableInternal(TunerHostInfo tuner, string channelId, CancellationToken cancellationToken); - protected virtual string ChannelIdPrefix { get @@ -294,7 +237,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts } protected virtual bool IsValidChannelId(string channelId) { - if (string.IsNullOrWhiteSpace(channelId)) + if (string.IsNullOrEmpty(channelId)) { throw new ArgumentNullException("channelId"); } @@ -309,7 +252,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts private class ChannelCache { - public DateTime Date; public List<ChannelInfo> Channels; } } diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs index 74758e906..e873eb8e9 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs @@ -21,6 +21,7 @@ using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Net; using MediaBrowser.Model.System; +using MediaBrowser.Controller.Library; namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun { @@ -68,11 +69,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun { var id = ChannelIdPrefix + i.GuideNumber; - if (!info.EnableNewHdhrChannelIds) - { - id += '_' + (i.GuideName ?? string.Empty).GetMD5().ToString("N"); - } - return id; } @@ -90,7 +86,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun { using (var stream = response.Content) { - var lineup = JsonSerializer.DeserializeFromStream<List<Channels>>(stream) ?? new List<Channels>(); + var lineup = await JsonSerializer.DeserializeFromStreamAsync<List<Channels>>(stream).ConfigureAwait(false) ?? new List<Channels>(); if (info.ImportFavoritesOnly) { @@ -131,12 +127,14 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun private readonly Dictionary<string, DiscoverResponse> _modelCache = new Dictionary<string, DiscoverResponse>(); private async Task<DiscoverResponse> GetModelInfo(TunerHostInfo info, bool throwAllExceptions, CancellationToken cancellationToken) { + var cacheKey = info.Id; + lock (_modelCache) { - DiscoverResponse response; - if (_modelCache.TryGetValue(info.Url, out response)) + if (!string.IsNullOrEmpty(cacheKey)) { - if ((DateTime.UtcNow - response.DateQueried).TotalHours <= 12) + DiscoverResponse response; + if (_modelCache.TryGetValue(cacheKey, out response)) { return response; } @@ -149,20 +147,20 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun { Url = string.Format("{0}/discover.json", GetApiUrl(info)), CancellationToken = cancellationToken, - TimeoutMs = Convert.ToInt32(TimeSpan.FromSeconds(5).TotalMilliseconds), + TimeoutMs = Convert.ToInt32(TimeSpan.FromSeconds(10).TotalMilliseconds), BufferContent = false }, "GET").ConfigureAwait(false)) { using (var stream = response.Content) { - var discoverResponse = JsonSerializer.DeserializeFromStream<DiscoverResponse>(stream); + var discoverResponse = await JsonSerializer.DeserializeFromStreamAsync<DiscoverResponse>(stream).ConfigureAwait(false); - if (!string.IsNullOrWhiteSpace(info.Id)) + if (!string.IsNullOrEmpty(cacheKey)) { lock (_modelCache) { - _modelCache[info.Id] = discoverResponse; + _modelCache[cacheKey] = discoverResponse; } } @@ -179,12 +177,12 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun { ModelNumber = defaultValue }; - if (!string.IsNullOrWhiteSpace(info.Id)) + if (!string.IsNullOrEmpty(cacheKey)) { // HDHR4 doesn't have this api lock (_modelCache) { - _modelCache[info.Id] = response; + _modelCache[cacheKey] = response; } } return response; @@ -395,10 +393,18 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun videoCodec = "h264"; videoBitrate = 15000000; } + else if (string.Equals(profile, "internet720", StringComparison.OrdinalIgnoreCase)) + { + width = 1280; + height = 720; + isInterlaced = false; + videoCodec = "h264"; + videoBitrate = 8000000; + } else if (string.Equals(profile, "internet540", StringComparison.OrdinalIgnoreCase)) { width = 960; - height = 546; + height = 540; isInterlaced = false; videoCodec = "h264"; videoBitrate = 2500000; @@ -511,8 +517,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun SupportsTranscoding = true, IsInfiniteStream = true, IgnoreDts = true, - SupportsProbing = false, - //AnalyzeDurationMs = 2000000 //IgnoreIndex = true, //ReadAtNativeFramerate = true }; @@ -522,19 +526,13 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun return mediaSource; } - protected override async Task<List<MediaSourceInfo>> GetChannelStreamMediaSources(TunerHostInfo info, string channelId, CancellationToken cancellationToken) + protected override async Task<List<MediaSourceInfo>> GetChannelStreamMediaSources(TunerHostInfo info, ChannelInfo channelInfo, CancellationToken cancellationToken) { var list = new List<MediaSourceInfo>(); - if (!channelId.StartsWith(ChannelIdPrefix, StringComparison.OrdinalIgnoreCase)) - { - return list; - } + var channelId = channelInfo.Id; var hdhrId = GetHdHrIdFromChannelId(channelId); - var channels = await GetChannels(info, true, CancellationToken.None).ConfigureAwait(false); - var channelInfo = channels.FirstOrDefault(i => string.Equals(i.Id, channelId, StringComparison.OrdinalIgnoreCase)); - var hdHomerunChannelInfo = channelInfo as HdHomerunChannelInfo; var isLegacyTuner = hdHomerunChannelInfo != null && hdHomerunChannelInfo.IsLegacyTuner; @@ -548,12 +546,9 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun try { var modelInfo = await GetModelInfo(info, false, cancellationToken).ConfigureAwait(false); - var model = modelInfo == null ? string.Empty : (modelInfo.ModelNumber ?? string.Empty); - if ((model.IndexOf("hdtc", StringComparison.OrdinalIgnoreCase) != -1)) + if (modelInfo != null && modelInfo.SupportsTranscoding) { - list.Add(GetMediaSource(info, hdhrId, channelInfo, "native")); - if (info.AllowHWTranscoding) { list.Add(GetMediaSource(info, hdhrId, channelInfo, "heavy")); @@ -564,6 +559,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun list.Add(GetMediaSource(info, hdhrId, channelInfo, "internet240")); list.Add(GetMediaSource(info, hdhrId, channelInfo, "mobile")); } + + list.Add(GetMediaSource(info, hdhrId, channelInfo, "native")); } } catch @@ -580,31 +577,31 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun return list; } - protected override async Task<ILiveStream> GetChannelStream(TunerHostInfo info, string channelId, string streamId, CancellationToken cancellationToken) + protected override async Task<ILiveStream> GetChannelStream(TunerHostInfo info, ChannelInfo channelInfo, string streamId, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken) { var profile = streamId.Split('_')[0]; - Logger.Info("GetChannelStream: channel id: {0}. stream id: {1} profile: {2}", channelId, streamId, profile); - - var hdhrId = GetHdHrIdFromChannelId(channelId); + Logger.Info("GetChannelStream: channel id: {0}. stream id: {1} profile: {2}", channelInfo.Id, streamId, profile); - var channels = await GetChannels(info, true, CancellationToken.None).ConfigureAwait(false); - var channelInfo = channels.FirstOrDefault(i => string.Equals(i.Id, channelId, StringComparison.OrdinalIgnoreCase)); + var hdhrId = GetHdHrIdFromChannelId(channelInfo.Id); var hdhomerunChannel = channelInfo as HdHomerunChannelInfo; - var mediaSource = GetMediaSource(info, hdhrId, channelInfo, profile); var modelInfo = await GetModelInfo(info, false, cancellationToken).ConfigureAwait(false); + if (!modelInfo.SupportsTranscoding) + { + profile = "native"; + } + + var mediaSource = GetMediaSource(info, hdhrId, channelInfo, profile); + 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, _environment); + return new HdHomerunUdpStream(mediaSource, info, streamId, new LegacyHdHomerunChannelCommands(hdhomerunChannel.Path), modelInfo.TunerCount, FileSystem, _httpClient, Logger, Config.ApplicationPaths, _appHost, _socketFactory, _networkManager); } - // The UDP method is not working reliably on OSX, and on BSD it hasn't been tested yet - var enableHttpStream = _environment.OperatingSystem == MediaBrowser.Model.System.OperatingSystem.OSX - || _environment.OperatingSystem == MediaBrowser.Model.System.OperatingSystem.BSD; - enableHttpStream = true; + var enableHttpStream = true; if (enableHttpStream) { mediaSource.Protocol = MediaProtocol.Http; @@ -618,10 +615,10 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun } mediaSource.Path = httpUrl; - return new SharedHttpStream(mediaSource, info, streamId, FileSystem, _httpClient, Logger, Config.ApplicationPaths, _appHost, _environment); + 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, _environment); + return new HdHomerunUdpStream(mediaSource, info, streamId, new HdHomerunChannelCommands(hdhomerunChannel.Number, profile), modelInfo.TunerCount, FileSystem, _httpClient, Logger, Config.ApplicationPaths, _appHost, _socketFactory, _networkManager); } public async Task Validate(TunerHostInfo info) @@ -649,13 +646,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun } } - protected override async Task<bool> IsAvailableInternal(TunerHostInfo tuner, string channelId, CancellationToken cancellationToken) - { - var info = await GetTunerInfos(tuner, cancellationToken).ConfigureAwait(false); - - return info.Any(i => i.Status == LiveTvTunerStatus.Available); - } - public class DiscoverResponse { public string FriendlyName { get; set; } @@ -668,16 +658,29 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun public string LineupURL { get; set; } public int TunerCount { get; set; } - public DateTime DateQueried { get; set; } - - public DiscoverResponse() + public bool SupportsTranscoding { - DateQueried = DateTime.UtcNow; + get + { + var model = ModelNumber ?? string.Empty; + + if ((model.IndexOf("hdtc", StringComparison.OrdinalIgnoreCase) != -1)) + { + return true; + } + + return false; + } } } public async Task<List<TunerHostInfo>> DiscoverDevices(int discoveryDurationMs, CancellationToken cancellationToken) { + lock (_modelCache) + { + _modelCache.Clear(); + } + cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(new CancellationTokenSource(discoveryDurationMs).Token, cancellationToken).Token; var list = new List<TunerHostInfo>(); diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs index 5156f1744..2bc5a0ed3 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs @@ -7,6 +7,8 @@ using System.Threading; using System.Threading.Tasks; using MediaBrowser.Model.Net; using MediaBrowser.Model.Logging; +using MediaBrowser.Controller.LiveTv; +using System.Net; namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun { @@ -61,7 +63,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun if (!String.IsNullOrEmpty(_channel)) { - if (!string.IsNullOrWhiteSpace(_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))); } @@ -86,11 +88,12 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun private static ushort GetSetReply = 5; private uint? _lockkey = null; - private int _activeTuner = 0; + private int _activeTuner = -1; private readonly ISocketFactory _socketFactory; private IpAddressInfo _remoteIp; private ILogger _logger; + private ISocket _currentTcpSocket; public HdHomerunManager(ISocketFactory socketFactory, ILogger logger) { @@ -100,10 +103,16 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun public void Dispose() { - var task = StopStreaming(); + using (var socket = _currentTcpSocket) + { + if (socket != null) + { + _currentTcpSocket = null; - Task.WaitAll(task); - GC.SuppressFinalize(this); + var task = StopStreaming(socket); + Task.WaitAll(task); + } + } } public async Task<bool> CheckTunerAvailability(IpAddressInfo remoteIp, int tuner, CancellationToken cancellationToken) @@ -130,58 +139,45 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun return string.Equals(returnVal, "none", StringComparison.OrdinalIgnoreCase); } - public async Task StartStreaming(IpAddressInfo remoteIp, IpAddressInfo localIp, int localPort, IHdHomerunChannelCommands commands, int numTuners, CancellationToken cancellationToken) + public async Task StartStreaming(IpAddressInfo remoteIp, IPAddress localIp, int localPort, IHdHomerunChannelCommands commands, int numTuners, CancellationToken cancellationToken) { _remoteIp = remoteIp; - - using (var tcpClient = _socketFactory.CreateTcpSocket(_remoteIp, HdHomeRunPort)) - { - var receiveBuffer = new byte[8192]; - - if (!_lockkey.HasValue) - { - var rand = new Random(); - _lockkey = (uint)rand.Next(); - } - var lockKeyValue = _lockkey.Value; + var tcpClient = _socketFactory.CreateTcpSocket(_remoteIp, HdHomeRunPort); + _currentTcpSocket = tcpClient; - var ipEndPoint = new IpEndPointInfo(_remoteIp, HdHomeRunPort); + var receiveBuffer = new byte[8192]; - for (int i = 0; i < numTuners; ++i) - { - if (!await CheckTunerAvailability(tcpClient, _remoteIp, i, cancellationToken).ConfigureAwait(false)) - continue; + if (!_lockkey.HasValue) + { + var rand = new Random(); + _lockkey = (uint)rand.Next(); + } - _activeTuner = i; - var lockKeyString = String.Format("{0:d}", lockKeyValue); - var lockkeyMsg = CreateSetMessage(i, "lockkey", lockKeyString, null); - await tcpClient.SendToAsync(lockkeyMsg, 0, lockkeyMsg.Length, ipEndPoint, cancellationToken).ConfigureAwait(false); - var response = await tcpClient.ReceiveAsync(receiveBuffer, 0, receiveBuffer.Length, cancellationToken).ConfigureAwait(false); - string returnVal; - // parse response to make sure it worked - if (!ParseReturnMessage(response.Buffer, response.ReceivedBytes, out returnVal)) - continue; + var lockKeyValue = _lockkey.Value; - var commandList = commands.GetCommands(); - foreach(Tuple<string,string> command in commandList) - { - var channelMsg = CreateSetMessage(i, command.Item1, command.Item2, lockKeyValue); - await tcpClient.SendToAsync(channelMsg, 0, channelMsg.Length, ipEndPoint, cancellationToken).ConfigureAwait(false); - response = await tcpClient.ReceiveAsync(receiveBuffer, 0, receiveBuffer.Length, cancellationToken).ConfigureAwait(false); - // parse response to make sure it worked - if (!ParseReturnMessage(response.Buffer, response.ReceivedBytes, out returnVal)) - { - await ReleaseLockkey(tcpClient, lockKeyValue).ConfigureAwait(false); - continue; - } + var ipEndPoint = new IpEndPointInfo(_remoteIp, HdHomeRunPort); - } - - var targetValue = String.Format("rtp://{0}:{1}", localIp, localPort); - var targetMsg = CreateSetMessage(i, "target", targetValue, lockKeyValue); + for (int i = 0; i < numTuners; ++i) + { + if (!await CheckTunerAvailability(tcpClient, _remoteIp, i, cancellationToken).ConfigureAwait(false)) + continue; + + _activeTuner = i; + var lockKeyString = String.Format("{0:d}", lockKeyValue); + var lockkeyMsg = CreateSetMessage(i, "lockkey", lockKeyString, null); + await tcpClient.SendToAsync(lockkeyMsg, 0, lockkeyMsg.Length, ipEndPoint, cancellationToken).ConfigureAwait(false); + var response = await tcpClient.ReceiveAsync(receiveBuffer, 0, receiveBuffer.Length, cancellationToken).ConfigureAwait(false); + string returnVal; + // parse response to make sure it worked + if (!ParseReturnMessage(response.Buffer, response.ReceivedBytes, out returnVal)) + continue; - await tcpClient.SendToAsync(targetMsg, 0, targetMsg.Length, ipEndPoint, cancellationToken).ConfigureAwait(false); + var commandList = commands.GetCommands(); + foreach (Tuple<string, string> command in commandList) + { + var channelMsg = CreateSetMessage(i, command.Item1, command.Item2, lockKeyValue); + await tcpClient.SendToAsync(channelMsg, 0, channelMsg.Length, ipEndPoint, cancellationToken).ConfigureAwait(false); response = await tcpClient.ReceiveAsync(receiveBuffer, 0, receiveBuffer.Length, cancellationToken).ConfigureAwait(false); // parse response to make sure it worked if (!ParseReturnMessage(response.Buffer, response.ReceivedBytes, out returnVal)) @@ -190,9 +186,25 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun continue; } - break; } + + var targetValue = String.Format("rtp://{0}:{1}", localIp, localPort); + var targetMsg = CreateSetMessage(i, "target", targetValue, lockKeyValue); + + await tcpClient.SendToAsync(targetMsg, 0, targetMsg.Length, ipEndPoint, cancellationToken).ConfigureAwait(false); + response = await tcpClient.ReceiveAsync(receiveBuffer, 0, receiveBuffer.Length, cancellationToken).ConfigureAwait(false); + // parse response to make sure it worked + if (!ParseReturnMessage(response.Buffer, response.ReceivedBytes, out returnVal)) + { + await ReleaseLockkey(tcpClient, lockKeyValue).ConfigureAwait(false); + continue; + } + + return; } + + _activeTuner = -1; + throw new LiveTvConflictException(); } public async Task ChangeChannel(IHdHomerunChannelCommands commands, CancellationToken cancellationToken) @@ -220,32 +232,31 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun } } - public async Task StopStreaming() + public Task StopStreaming(ISocket socket) { var lockKey = _lockkey; if (!lockKey.HasValue) - return; + return Task.CompletedTask; - using (var socket = _socketFactory.CreateTcpSocket(_remoteIp, HdHomeRunPort)) - { - await ReleaseLockkey(socket, lockKey.Value).ConfigureAwait(false); - } + return ReleaseLockkey(socket, lockKey.Value); } private async Task ReleaseLockkey(ISocket tcpClient, uint lockKeyValue) { _logger.Info("HdHomerunManager.ReleaseLockkey {0}", lockKeyValue); + var ipEndPoint = new IpEndPointInfo(_remoteIp, HdHomeRunPort); + var releaseTarget = CreateSetMessage(_activeTuner, "target", "none", lockKeyValue); - await tcpClient.SendToAsync(releaseTarget, 0, releaseTarget.Length, new IpEndPointInfo(_remoteIp, HdHomeRunPort), CancellationToken.None).ConfigureAwait(false); + await tcpClient.SendToAsync(releaseTarget, 0, releaseTarget.Length, ipEndPoint, CancellationToken.None).ConfigureAwait(false); var receiveBuffer = new byte[8192]; await tcpClient.ReceiveAsync(receiveBuffer, 0, receiveBuffer.Length, CancellationToken.None).ConfigureAwait(false); var releaseKeyMsg = CreateSetMessage(_activeTuner, "lockkey", "none", lockKeyValue); _lockkey = null; - await tcpClient.SendToAsync(releaseKeyMsg, 0, releaseKeyMsg.Length, new IpEndPointInfo(_remoteIp, HdHomeRunPort), CancellationToken.None).ConfigureAwait(false); + await tcpClient.SendToAsync(releaseKeyMsg, 0, releaseKeyMsg.Length, ipEndPoint, CancellationToken.None).ConfigureAwait(false); await tcpClient.ReceiveAsync(receiveBuffer, 0, receiveBuffer.Length, CancellationToken.None).ConfigureAwait(false); } diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs index 6e93055be..33103979e 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs @@ -9,23 +9,25 @@ using MediaBrowser.Model.Dto; using MediaBrowser.Model.IO; using MediaBrowser.Model.Logging; using MediaBrowser.Model.MediaInfo; -using MediaBrowser.Model.Net; using MediaBrowser.Model.System; using MediaBrowser.Model.LiveTv; +using System.Collections.Generic; +using System.Net.Sockets; +using System.Net; namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun { public class HdHomerunUdpStream : LiveStream, IDirectStreamProvider { private readonly IServerApplicationHost _appHost; - private readonly ISocketFactory _socketFactory; + private readonly MediaBrowser.Model.Net.ISocketFactory _socketFactory; private readonly IHdHomerunChannelCommands _channelCommands; private readonly int _numTuners; private readonly INetworkManager _networkManager; - public HdHomerunUdpStream(MediaSourceInfo mediaSource, TunerHostInfo tunerHostInfo, string originalStreamId, IHdHomerunChannelCommands channelCommands, int numTuners, IFileSystem fileSystem, IHttpClient httpClient, ILogger logger, IServerApplicationPaths appPaths, IServerApplicationHost appHost, ISocketFactory socketFactory, INetworkManager networkManager, IEnvironmentInfo environment) - : base(mediaSource, tunerHostInfo, environment, fileSystem, logger, appPaths) + public HdHomerunUdpStream(MediaSourceInfo mediaSource, TunerHostInfo tunerHostInfo, string originalStreamId, 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) { _appHost = appHost; _socketFactory = socketFactory; @@ -36,6 +38,13 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun EnableStreamSharing = true; } + private Socket CreateSocket(AddressFamily addressFamily, SocketType socketType, ProtocolType protocolType) + { + var socket = new Socket(addressFamily, SocketType.Stream, ProtocolType.Tcp); + + return socket; + } + public override async Task Open(CancellationToken openCancellationToken) { LiveStreamCancellationTokenSource.Token.ThrowIfCancellationRequested(); @@ -49,14 +58,15 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun Logger.Info("Opening HDHR UDP Live stream from {0}", uri.Host); - var remoteAddress = _networkManager.ParseIpAddress(uri.Host); - IpAddressInfo localAddress = null; - using (var tcpSocket = _socketFactory.CreateSocket(remoteAddress.AddressFamily, SocketType.Stream, ProtocolType.Tcp, false)) + var remoteAddress = IPAddress.Parse(uri.Host); + var embyRemoteAddress = _networkManager.ParseIpAddress(uri.Host); + IPAddress localAddress = null; + using (var tcpSocket = CreateSocket(remoteAddress.AddressFamily, SocketType.Stream, ProtocolType.Tcp)) { try { - tcpSocket.Connect(new IpEndPointInfo(remoteAddress, HdHomerunManager.HdHomeRunPort)); - localAddress = tcpSocket.LocalEndPoint.IpAddress; + tcpSocket.Connect(new IPEndPoint(remoteAddress, HdHomerunManager.HdHomeRunPort)); + localAddress = ((IPEndPoint)tcpSocket.LocalEndPoint).Address; tcpSocket.Close(); } catch (Exception) @@ -72,7 +82,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun try { // send url to start streaming - await hdHomerunManager.StartStreaming(remoteAddress, localAddress, localPort, _channelCommands, _numTuners, openCancellationToken).ConfigureAwait(false); + await hdHomerunManager.StartStreaming(embyRemoteAddress, localAddress, localPort, _channelCommands, _numTuners, openCancellationToken).ConfigureAwait(false); } catch (Exception ex) { @@ -91,14 +101,14 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun var taskCompletionSource = new TaskCompletionSource<bool>(); - StartStreaming(udpClient, hdHomerunManager, remoteAddress, localAddress, localPort, taskCompletionSource, LiveStreamCancellationTokenSource.Token); + StartStreaming(udpClient, hdHomerunManager, remoteAddress, taskCompletionSource, LiveStreamCancellationTokenSource.Token); //OpenedMediaSource.Protocol = MediaProtocol.File; //OpenedMediaSource.Path = tempFile; //OpenedMediaSource.ReadAtNativeFramerate = true; - OpenedMediaSource.Path = _appHost.GetLocalApiUrl("127.0.0.1") + "/LiveTv/LiveStreamFiles/" + UniqueId + "/stream.ts"; - OpenedMediaSource.Protocol = MediaProtocol.Http; + MediaSource.Path = _appHost.GetLocalApiUrl("127.0.0.1") + "/LiveTv/LiveStreamFiles/" + UniqueId + "/stream.ts"; + MediaSource.Protocol = MediaProtocol.Http; //OpenedMediaSource.SupportsDirectPlay = false; //OpenedMediaSource.SupportsDirectStream = true; //OpenedMediaSource.SupportsTranscoding = true; @@ -107,12 +117,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun await taskCompletionSource.Task.ConfigureAwait(false); } - protected override void CloseInternal() - { - LiveStreamCancellationTokenSource.Cancel(); - } - - private Task StartStreaming(ISocket udpClient, HdHomerunManager hdHomerunManager, IpAddressInfo remoteAddress, IpAddressInfo localAddress, int localPort, TaskCompletionSource<bool> openTaskCompletionSource, CancellationToken cancellationToken) + private Task StartStreaming(MediaBrowser.Model.Net.ISocket udpClient, HdHomerunManager hdHomerunManager, IPAddress remoteAddress, TaskCompletionSource<bool> openTaskCompletionSource, CancellationToken cancellationToken) { return Task.Run(async () => { @@ -136,19 +141,10 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun } EnableStreamSharing = false; - - try - { - await hdHomerunManager.StopStreaming().ConfigureAwait(false); - } - catch - { - - } } } - await DeleteTempFile(TempFilePath).ConfigureAwait(false); + await DeleteTempFiles(new List<string> { TempFilePath }).ConfigureAwait(false); }); } @@ -161,7 +157,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun } private static int RtpHeaderBytes = 12; - private async Task CopyTo(ISocket udpClient, string file, TaskCompletionSource<bool> openTaskCompletionSource, CancellationToken cancellationToken) + private async Task CopyTo(MediaBrowser.Model.Net.ISocket udpClient, string file, TaskCompletionSource<bool> openTaskCompletionSource, CancellationToken cancellationToken) { var bufferSize = 81920; @@ -191,6 +187,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun if (!resolved) { resolved = true; + DateOpened = DateTime.UtcNow; Resolve(openTaskCompletionSource); } } @@ -202,10 +199,10 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun { private static int RtpHeaderBytes = 12; private static int PacketSize = 1316; - private readonly ISocket _udpClient; + private readonly MediaBrowser.Model.Net.ISocket _udpClient; bool disposed; - public UdpClientStream(ISocket udpClient) : base() + public UdpClientStream(MediaBrowser.Model.Net.ISocket udpClient) : base() { _udpClient = udpClient; } diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs index f6758e94e..7e0ac4131 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs @@ -11,47 +11,52 @@ using MediaBrowser.Model.IO; using MediaBrowser.Model.Logging; using MediaBrowser.Model.System; using MediaBrowser.Model.LiveTv; +using System.Linq; +using MediaBrowser.Controller.Library; namespace Emby.Server.Implementations.LiveTv.TunerHosts { public class LiveStream : ILiveStream { public MediaSourceInfo OriginalMediaSource { get; set; } - public MediaSourceInfo OpenedMediaSource { get; set; } - public int ConsumerCount - { - get { return SharedStreamIds.Count; } - } + public MediaSourceInfo MediaSource { get; set; } + + public int ConsumerCount { get; set; } public string OriginalStreamId { get; set; } public bool EnableStreamSharing { get; set; } public string UniqueId { get; private set; } - public List<string> SharedStreamIds { get; private set; } - protected readonly IEnvironmentInfo Environment; protected readonly IFileSystem FileSystem; protected readonly IServerApplicationPaths AppPaths; - protected string TempFilePath; + protected string TempFilePath; protected readonly ILogger Logger; protected readonly CancellationTokenSource LiveStreamCancellationTokenSource = new CancellationTokenSource(); public string TunerHostId { get; private set; } - public LiveStream(MediaSourceInfo mediaSource, TunerHostInfo tuner, IEnvironmentInfo environment, IFileSystem fileSystem, ILogger logger, IServerApplicationPaths appPaths) + public DateTime DateOpened { get; protected set; } + + public Func<Task> OnClose { get; set; } + + public LiveStream(MediaSourceInfo mediaSource, TunerHostInfo tuner, IFileSystem fileSystem, ILogger logger, IServerApplicationPaths appPaths) { OriginalMediaSource = mediaSource; - Environment = environment; FileSystem = fileSystem; - OpenedMediaSource = mediaSource; + MediaSource = mediaSource; Logger = logger; EnableStreamSharing = true; - SharedStreamIds = new List<string>(); UniqueId = Guid.NewGuid().ToString("N"); - TunerHostId = tuner.Id; + + if (tuner != null) + { + TunerHostId = tuner.Id; + } AppPaths = appPaths; + ConsumerCount = 1; SetTempFilePath("ts"); } @@ -62,20 +67,36 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts public virtual Task Open(CancellationToken openCancellationToken) { - return Task.FromResult(true); + DateOpened = DateTime.UtcNow; + return Task.CompletedTask; } - public void Close() + public Task Close() { EnableStreamSharing = false; Logger.Info("Closing " + GetType().Name); - CloseInternal(); + LiveStreamCancellationTokenSource.Cancel(); + + if (OnClose != null) + { + return CloseWithExternalFn(); + } + + return Task.CompletedTask; } - protected virtual void CloseInternal() + private async Task CloseWithExternalFn() { + try + { + await OnClose().ConfigureAwait(false); + } + catch (Exception ex) + { + Logger.ErrorException("Error closing live stream", ex); + } } protected Stream GetInputStream(string path, bool allowAsyncFileRead) @@ -90,91 +111,123 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts return FileSystem.GetFileStream(path, FileOpenMode.Open, FileAccessMode.Read, FileShareMode.ReadWrite, fileOpenOptions); } - protected async Task DeleteTempFile(string path, int retryCount = 0) + public Task DeleteTempFiles() + { + return DeleteTempFiles(GetStreamFilePaths()); + } + + protected async Task DeleteTempFiles(List<string> paths, int retryCount = 0) { if (retryCount == 0) { - Logger.Info("Deleting temp file {0}", path); + Logger.Info("Deleting temp files {0}", string.Join(", ", paths.ToArray())); } - try - { - FileSystem.DeleteFile(path); - return; - } - catch (DirectoryNotFoundException) - { - return; - } - catch (FileNotFoundException) - { - return; - } - catch - { + var failedFiles = new List<string>(); + foreach (var path in paths) + { + try + { + FileSystem.DeleteFile(path); + } + catch (DirectoryNotFoundException) + { + } + catch (FileNotFoundException) + { + } + catch (Exception ex) + { + //Logger.ErrorException("Error deleting file {0}", ex, path); + failedFiles.Add(path); + } } - if (retryCount > 20) + if (failedFiles.Count > 0 && retryCount <= 40) { - return; + await Task.Delay(500).ConfigureAwait(false); + await DeleteTempFiles(failedFiles, retryCount + 1).ConfigureAwait(false); } + } - await Task.Delay(500).ConfigureAwait(false); - await DeleteTempFile(path, retryCount + 1).ConfigureAwait(false); + protected virtual List<string> GetStreamFilePaths() + { + return new List<string> { TempFilePath }; } public async Task CopyToAsync(Stream stream, CancellationToken cancellationToken) { cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, LiveStreamCancellationTokenSource.Token).Token; - var allowAsync = false;//Environment.OperatingSystem != MediaBrowser.Model.System.OperatingSystem.Windows; + var allowAsync = false; // use non-async filestream along with read due to https://github.com/dotnet/corefx/issues/6039 - using (var inputStream = (FileStream)GetInputStream(TempFilePath, allowAsync)) + bool seekFile = (DateTime.UtcNow - DateOpened).TotalSeconds > 10; + + var nextFileInfo = GetNextFile(null); + var nextFile = nextFileInfo.Item1; + var isLastFile = nextFileInfo.Item2; + + while (!string.IsNullOrEmpty(nextFile)) { - TrySeek(inputStream, -20000); + var emptyReadLimit = isLastFile ? EmptyReadLimit : 1; + + await CopyFile(nextFile, seekFile, emptyReadLimit, allowAsync, stream, cancellationToken).ConfigureAwait(false); - await CopyTo(inputStream, stream, 81920, null, cancellationToken).ConfigureAwait(false); + seekFile = false; + nextFileInfo = GetNextFile(nextFile); + nextFile = nextFileInfo.Item1; + isLastFile = nextFileInfo.Item2; } + + Logger.Info("Live Stream ended."); } - private static async Task CopyTo(Stream source, Stream destination, int bufferSize, Action onStarted, CancellationToken cancellationToken) + private Tuple<string, bool> GetNextFile(string currentFile) { - byte[] buffer = new byte[bufferSize]; + var files = GetStreamFilePaths(); - var eofCount = 0; - var emptyReadLimit = 1000; + //Logger.Info("Live stream files: {0}", string.Join(", ", files.ToArray())); - while (eofCount < emptyReadLimit) + if (string.IsNullOrEmpty(currentFile)) { - cancellationToken.ThrowIfCancellationRequested(); + return new Tuple<string, bool>(files.Last(), true); + } + + var nextIndex = files.FindIndex(i => string.Equals(i, currentFile, StringComparison.OrdinalIgnoreCase)) + 1; + + var isLastFile = nextIndex == files.Count - 1; - var bytesRead = source.Read(buffer, 0, buffer.Length); + return new Tuple<string, bool>(files.ElementAtOrDefault(nextIndex), isLastFile); + } + + private async Task CopyFile(string path, bool seekFile, int emptyReadLimit, bool allowAsync, Stream stream, CancellationToken cancellationToken) + { + //Logger.Info("Opening live stream file {0}. Empty read limit: {1}", path, emptyReadLimit); - if (bytesRead == 0) + using (var inputStream = (FileStream)GetInputStream(path, allowAsync)) + { + if (seekFile) { - eofCount++; - await Task.Delay(10, cancellationToken).ConfigureAwait(false); + TrySeek(inputStream, -20000); } - else - { - eofCount = 0; - //await destination.WriteAsync(buffer, 0, read).ConfigureAwait(false); - destination.Write(buffer, 0, bytesRead); + await ApplicationHost.StreamHelper.CopyToAsync(inputStream, stream, 81920, emptyReadLimit, cancellationToken).ConfigureAwait(false); + } + } - if (onStarted != null) - { - onStarted(); - onStarted = null; - } - } + protected virtual int EmptyReadLimit + { + get + { + return 1000; } } private void TrySeek(FileStream stream, long offset) { + //Logger.Info("TrySeek live stream"); try { stream.Seek(offset, SeekOrigin.End); diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs index 04c5303f1..a1bff2b5b 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs @@ -18,6 +18,7 @@ using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Model.Serialization; using MediaBrowser.Model.System; using System.IO; +using MediaBrowser.Controller.Library; namespace Emby.Server.Implementations.LiveTv.TunerHosts { @@ -27,13 +28,15 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts private readonly IServerApplicationHost _appHost; private readonly IEnvironmentInfo _environment; private readonly INetworkManager _networkManager; + private readonly IMediaSourceManager _mediaSourceManager; - public M3UTunerHost(IServerConfigurationManager config, ILogger logger, IJsonSerializer jsonSerializer, IMediaEncoder mediaEncoder, IFileSystem fileSystem, IHttpClient httpClient, IServerApplicationHost appHost, IEnvironmentInfo environment, INetworkManager networkManager) : base(config, logger, jsonSerializer, mediaEncoder, fileSystem) + public M3UTunerHost(IServerConfigurationManager config, IMediaSourceManager mediaSourceManager, ILogger logger, IJsonSerializer jsonSerializer, IMediaEncoder mediaEncoder, IFileSystem fileSystem, IHttpClient httpClient, IServerApplicationHost appHost, IEnvironmentInfo environment, INetworkManager networkManager) : base(config, logger, jsonSerializer, mediaEncoder, fileSystem) { _httpClient = httpClient; _appHost = appHost; _environment = environment; _networkManager = networkManager; + _mediaSourceManager = mediaSourceManager; } public override string Type @@ -76,7 +79,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts return Task.FromResult(list); } - private string[] _disallowedSharedStreamExtensions = new string[] + private string[] _disallowedSharedStreamExtensions = new string[] { ".mkv", ".mp4", @@ -84,21 +87,22 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts ".mpd" }; - protected override async Task<ILiveStream> GetChannelStream(TunerHostInfo info, string channelId, string streamId, CancellationToken cancellationToken) + protected override async Task<ILiveStream> GetChannelStream(TunerHostInfo info, ChannelInfo channelInfo, string streamId, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken) { var tunerCount = info.TunerCount; if (tunerCount > 0) { - var liveStreams = await EmbyTV.EmbyTV.Current.GetLiveStreams(info, cancellationToken).ConfigureAwait(false); + var tunerHostId = info.Id; + var liveStreams = currentLiveStreams.Where(i => string.Equals(i.TunerHostId, tunerHostId, StringComparison.OrdinalIgnoreCase)).ToList(); - if (liveStreams.Count >= info.TunerCount) + if (liveStreams.Count >= tunerCount) { - throw new LiveTvConflictException(); + throw new LiveTvConflictException("M3U simultaneous stream limit has been reached."); } } - var sources = await GetChannelStreamMediaSources(info, channelId, cancellationToken).ConfigureAwait(false); + var sources = await GetChannelStreamMediaSources(info, channelInfo, cancellationToken).ConfigureAwait(false); var mediaSource = sources.First(); @@ -108,11 +112,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, _environment); + return new SharedHttpStream(mediaSource, info, streamId, FileSystem, _httpClient, Logger, Config.ApplicationPaths, _appHost); } } - return new LiveStream(mediaSource, info, _environment, FileSystem, Logger, Config.ApplicationPaths); + return new LiveStream(mediaSource, info, FileSystem, Logger, Config.ApplicationPaths); } public async Task Validate(TunerHostInfo info) @@ -123,41 +127,19 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts } } - protected override async Task<List<MediaSourceInfo>> GetChannelStreamMediaSources(TunerHostInfo info, string channelId, CancellationToken cancellationToken) + protected override Task<List<MediaSourceInfo>> GetChannelStreamMediaSources(TunerHostInfo info, ChannelInfo channelInfo, CancellationToken cancellationToken) { - var channels = await GetChannels(info, true, cancellationToken).ConfigureAwait(false); - var channel = channels.FirstOrDefault(c => string.Equals(c.Id, channelId, StringComparison.OrdinalIgnoreCase)); - if (channel != null) - { - return new List<MediaSourceInfo> { CreateMediaSourceInfo(info, channel) }; - } - return new List<MediaSourceInfo>(); + return Task.FromResult(new List<MediaSourceInfo> { CreateMediaSourceInfo(info, channelInfo) }); } protected virtual MediaSourceInfo CreateMediaSourceInfo(TunerHostInfo info, ChannelInfo channel) { var path = channel.Path; - MediaProtocol protocol = MediaProtocol.File; - if (path.StartsWith("http", StringComparison.OrdinalIgnoreCase)) - { - protocol = MediaProtocol.Http; - } - else if (path.StartsWith("rtmp", StringComparison.OrdinalIgnoreCase)) - { - protocol = MediaProtocol.Rtmp; - } - else if (path.StartsWith("rtsp", StringComparison.OrdinalIgnoreCase)) - { - protocol = MediaProtocol.Rtsp; - } - else if (path.StartsWith("udp", StringComparison.OrdinalIgnoreCase)) - { - protocol = MediaProtocol.Udp; - } - else if (path.StartsWith("rtp", StringComparison.OrdinalIgnoreCase)) - { - protocol = MediaProtocol.Rtmp; - } + + var supportsDirectPlay = !info.EnableStreamLooping && info.TunerCount == 0; + var supportsDirectStream = !info.EnableStreamLooping; + + var protocol = _mediaSourceManager.GetPathProtocol(path); Uri uri; var isRemote = true; @@ -166,7 +148,15 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts isRemote = !_networkManager.IsInLocalNetwork(uri.Host); } - var supportsDirectPlay = !info.EnableStreamLooping && info.TunerCount == 0; + var httpHeaders = new Dictionary<string, string>(); + + if (protocol == MediaProtocol.Http) + { + // Use user-defined user-agent. If there isn't one, make it look like a browser. + httpHeaders["User-Agent"] = string.IsNullOrWhiteSpace(info.UserAgent) ? + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.85 Safari/537.36" : + info.UserAgent; + } var mediaSource = new MediaSourceInfo { @@ -186,7 +176,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts Type = MediaStreamType.Audio, // Set the index to -1 because we don't know the exact index of the audio stream within the container Index = -1 - } }, RequiresOpening = true, @@ -200,7 +189,10 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts IsRemote = isRemote, IgnoreDts = true, - SupportsDirectPlay = supportsDirectPlay + SupportsDirectPlay = supportsDirectPlay, + SupportsDirectStream = supportsDirectStream, + + RequiredHttpHeaders = httpHeaders }; mediaSource.InferTotalBitrate(); @@ -208,11 +200,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts return mediaSource; } - protected override Task<bool> IsAvailableInternal(TunerHostInfo tuner, string channelId, CancellationToken cancellationToken) - { - return Task.FromResult(true); - } - public Task<List<TunerHostInfo>> DiscoverDevices(int discoveryDurationMs, CancellationToken cancellationToken) { return Task.FromResult(new List<TunerHostInfo>()); diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs index af7491e86..572edb167 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs @@ -15,6 +15,7 @@ using MediaBrowser.Model.System; using System.Globalization; using MediaBrowser.Controller.IO; using MediaBrowser.Model.LiveTv; +using System.Collections.Generic; namespace Emby.Server.Implementations.LiveTv.TunerHosts { @@ -23,8 +24,8 @@ 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, IEnvironmentInfo environment) - : base(mediaSource, tunerHostInfo, environment, fileSystem, logger, appPaths) + public SharedHttpStream(MediaSourceInfo mediaSource, TunerHostInfo tunerHostInfo, string originalStreamId, IFileSystem fileSystem, IHttpClient httpClient, ILogger logger, IServerApplicationPaths appPaths, IServerApplicationHost appHost) + : base(mediaSource, tunerHostInfo, fileSystem, logger, appPaths) { _httpClient = httpClient; _appHost = appHost; @@ -45,7 +46,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts var typeName = GetType().Name; Logger.Info("Opening " + typeName + " Live stream from {0}", url); - var response = await _httpClient.SendAsync(new HttpRequestOptions + var httpRequestOptions = new HttpRequestOptions { Url = url, CancellationToken = CancellationToken.None, @@ -58,8 +59,14 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts LogResponse = true, LogResponseHeaders = true + }; - }, "GET").ConfigureAwait(false); + foreach (var header in mediaSource.RequiredHttpHeaders) + { + httpRequestOptions.RequestHeaders[header.Key] = header.Value; + } + + var response = await _httpClient.SendAsync(httpRequestOptions, "GET").ConfigureAwait(false); var extension = "ts"; var requiresRemux = false; @@ -98,8 +105,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts //OpenedMediaSource.Path = tempFile; //OpenedMediaSource.ReadAtNativeFramerate = true; - OpenedMediaSource.Path = _appHost.GetLocalApiUrl("127.0.0.1") + "/LiveTv/LiveStreamFiles/" + UniqueId + "/stream.ts"; - OpenedMediaSource.Protocol = MediaProtocol.Http; + MediaSource.Path = _appHost.GetLocalApiUrl("127.0.0.1") + "/LiveTv/LiveStreamFiles/" + UniqueId + "/stream.ts"; + MediaSource.Protocol = MediaProtocol.Http; //OpenedMediaSource.Path = TempFilePath; //OpenedMediaSource.Protocol = MediaProtocol.File; @@ -110,25 +117,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts //OpenedMediaSource.SupportsDirectStream = true; //OpenedMediaSource.SupportsTranscoding = true; await taskCompletionSource.Task.ConfigureAwait(false); - - if (OpenedMediaSource.SupportsProbing) - { - var elapsed = (DateTime.UtcNow - now).TotalMilliseconds; - - var delay = Convert.ToInt32(3000 - elapsed); - - if (delay > 0) - { - Logger.Info("Delaying shared stream by {0}ms to allow the buffer to build.", delay); - - await Task.Delay(delay).ConfigureAwait(false); - } - } - } - - protected override void CloseInternal() - { - LiveStreamCancellationTokenSource.Cancel(); } private Task StartStreaming(HttpResponseInfo response, TaskCompletionSource<bool> openTaskCompletionSource, CancellationToken cancellationToken) @@ -145,7 +133,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts using (var fileStream = FileSystem.GetFileStream(TempFilePath, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read, FileOpenOptions.None)) { - StreamHelper.CopyTo(stream, fileStream, 81920, () => Resolve(openTaskCompletionSource), cancellationToken); + await ApplicationHost.StreamHelper.CopyToAsync(stream, fileStream, 81920, () => Resolve(openTaskCompletionSource), cancellationToken).ConfigureAwait(false); } } } @@ -158,12 +146,13 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts Logger.ErrorException("Error copying live stream.", ex); } EnableStreamSharing = false; - await DeleteTempFile(TempFilePath).ConfigureAwait(false); + await DeleteTempFiles(new List<string> { TempFilePath }).ConfigureAwait(false); }); } private void Resolve(TaskCompletionSource<bool> openTaskCompletionSource) { + DateOpened = DateTime.UtcNow; openTaskCompletionSource.TrySetResult(true); } } diff --git a/Emby.Server.Implementations/Localization/Core/ar.json b/Emby.Server.Implementations/Localization/Core/ar.json index 54200605d..3a84195ee 100644 --- a/Emby.Server.Implementations/Localization/Core/ar.json +++ b/Emby.Server.Implementations/Localization/Core/ar.json @@ -1,4 +1,12 @@ { + "HeaderCameraUploads": "Camera Uploads", + "ValueHasBeenAddedToLibrary": "{0} has been added to your media library", + "NameInstallFailed": "{0} installation failed", + "CameraImageUploadedFrom": "A new camera image has been uploaded from {0}", + "ServerNameNeedsToBeRestarted": "{0} needs to be restarted", + "NewVersionIsAvailable": "A new version of Emby Server is available for download.", + "MessageApplicationUpdatedTo": "Emby Server has been updated to {0}", + "SubtitleDownloadFailureFromForItem": "Subtitles failed to download from {0} for {1}", "Latest": "\u0627\u0644\u0623\u062d\u062f\u062b", "ValueSpecialEpisodeName": "\u062e\u0627\u0635 - {0}", "Inherit": "\u062a\u0648\u0631\u064a\u062b", @@ -24,6 +32,7 @@ "Channels": "\u0627\u0644\u0642\u0646\u0648\u0627\u062a", "Movies": "\u0627\u0644\u0623\u0641\u0644\u0627\u0645", "Albums": "\u0627\u0644\u0623\u0644\u0628\u0648\u0645\u0627\u062a", + "NameSeasonUnknown": "Season Unknown", "Artists": "\u0627\u0644\u0641\u0646\u0627\u0646\u0648\u0646", "Folders": "\u0627\u0644\u0645\u062c\u0644\u062f\u0627\u062a", "Songs": "\u0627\u0644\u0623\u063a\u0627\u0646\u064a", @@ -53,7 +62,7 @@ "UserCreatedWithName": "\u062a\u0645 \u0625\u0646\u0634\u0627\u0621 \u0627\u0644\u0645\u0633\u062a\u062e\u062f\u0645 {0}", "UserPasswordChangedWithName": "\u062a\u0645 \u062a\u063a\u064a\u064a\u0631 \u0643\u0644\u0645\u0629 \u0627\u0644\u0633\u0631 \u0644\u0644\u0645\u0633\u062a\u062e\u062f\u0645 {0}", "UserDeletedWithName": "\u062a\u0645 \u062d\u0630\u0641 \u0627\u0644\u0645\u0633\u062a\u062e\u062f\u0645 {0}", - "UserConfigurationUpdatedWithName": "\u0625\u0639\u062f\u0627\u062f\u0627\u062a \u0627\u0644\u0645\u0633\u062a\u062e\u062f\u0645 \u062a\u0645 \u062a\u062d\u062f\u064a\u062b\u0647\u0627 \u0644\u0640 {0}", + "UserPolicyUpdatedWithName": "User policy has been updated for {0}", "MessageServerConfigurationUpdated": "\u062a\u0645 \u062a\u062d\u062f\u064a\u062b \u0625\u0639\u062f\u0627\u062f\u0627\u062a \u0627\u0644\u062e\u0627\u062f\u0645", "MessageNamedServerConfigurationUpdatedWithValue": "\u062a\u0645 \u062a\u062d\u062f\u064a\u062b \u0625\u0639\u062f\u0627\u062f\u0627\u062a \u0627\u0644\u062e\u0627\u062f\u0645 \u0641\u064a \u0642\u0633\u0645 {0}", "MessageApplicationUpdated": "\u0644\u0642\u062f \u062a\u0645 \u062a\u062d\u062f\u064a\u062b \u062e\u0627\u062f\u0645 \u0623\u0645\u0628\u064a", diff --git a/Emby.Server.Implementations/Localization/Core/bg-BG.json b/Emby.Server.Implementations/Localization/Core/bg-BG.json index 02c03f578..a80b6797a 100644 --- a/Emby.Server.Implementations/Localization/Core/bg-BG.json +++ b/Emby.Server.Implementations/Localization/Core/bg-BG.json @@ -1,4 +1,12 @@ { + "HeaderCameraUploads": "Camera Uploads", + "ValueHasBeenAddedToLibrary": "{0} has been added to your media library", + "NameInstallFailed": "{0} installation failed", + "CameraImageUploadedFrom": "A new camera image has been uploaded from {0}", + "ServerNameNeedsToBeRestarted": "{0} needs to be restarted", + "NewVersionIsAvailable": "A new version of Emby Server is available for download.", + "MessageApplicationUpdatedTo": "Emby Server has been updated to {0}", + "SubtitleDownloadFailureFromForItem": "Subtitles failed to download from {0} for {1}", "Latest": "\u041f\u043e\u0441\u043b\u0435\u0434\u043d\u0438", "ValueSpecialEpisodeName": "\u0421\u043f\u0435\u0446\u0438\u0430\u043b\u043d\u0438 - {0}", "Inherit": "\u041d\u0430\u0441\u043b\u0435\u0434\u044f\u0432\u0430\u043d\u0435", @@ -24,6 +32,7 @@ "Channels": "\u041a\u0430\u043d\u0430\u043b\u0438", "Movies": "\u0424\u0438\u043b\u043c\u0438", "Albums": "\u0410\u043b\u0431\u0443\u043c\u0438", + "NameSeasonUnknown": "Season Unknown", "Artists": "\u0418\u0437\u043f\u044a\u043b\u043d\u0438\u0442\u0435\u043b\u0438", "Folders": "\u041f\u0430\u043f\u043a\u0438", "Songs": "\u041f\u0435\u0441\u043d\u0438", @@ -53,7 +62,7 @@ "UserCreatedWithName": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u044f\u0442 {0} \u0435 \u0441\u044a\u0437\u0434\u0430\u0434\u0435\u043d", "UserPasswordChangedWithName": "\u041f\u0430\u0440\u043e\u043b\u0430\u0442\u0430 \u043d\u0430 \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u044f {0} \u0435 \u043f\u0440\u043e\u043c\u0435\u043d\u0435\u043d\u0430", "UserDeletedWithName": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u044f\u0442 {0} \u0435 \u0438\u0437\u0442\u0440\u0438\u0442", - "UserConfigurationUpdatedWithName": "User configuration has been updated for {0}", + "UserPolicyUpdatedWithName": "User policy has been updated for {0}", "MessageServerConfigurationUpdated": "Server configuration has been updated", "MessageNamedServerConfigurationUpdatedWithValue": "Server configuration section {0} has been updated", "MessageApplicationUpdated": "\u0421\u044a\u0440\u0432\u044a\u0440\u044a\u0442 \u0435 \u043e\u0431\u043d\u043e\u0432\u0435\u043d", @@ -61,8 +70,8 @@ "AuthenticationSucceededWithUserName": "{0} \u0441\u0435 \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u0438 \u0443\u0441\u043f\u0435\u0448\u043d\u043e", "UserOfflineFromDevice": "{0} \u0441\u0435 \u0440\u0430\u0437\u043a\u0430\u0447\u0438 \u043e\u0442 {1}", "DeviceOfflineWithName": "{0} \u0441\u0435 \u0440\u0430\u0437\u043a\u0430\u0447\u0438", - "UserStartedPlayingItemWithValues": "{0} has started playing {1}", - "UserStoppedPlayingItemWithValues": "{0} has stopped playing {1}", + "UserStartedPlayingItemWithValues": "{0} \u043f\u0443\u0441\u043d\u0430 {1}", + "UserStoppedPlayingItemWithValues": "{0} \u0441\u043f\u0440\u044f {1}", "NotificationOptionPluginError": "\u0413\u0440\u0435\u0448\u043a\u0430 \u0432 \u043f\u0440\u0438\u0441\u0442\u0430\u0432\u043a\u0430", "NotificationOptionApplicationUpdateAvailable": "\u041d\u0430\u043b\u0438\u0447\u043d\u043e \u0435 \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0435 \u043d\u0430 \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u0430\u0442\u0430", "NotificationOptionApplicationUpdateInstalled": "\u041e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0435\u0442\u043e \u043d\u0430 \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u0430\u0442\u0430 \u0435 \u0438\u043d\u0441\u0442\u0430\u043b\u0438\u0440\u0430\u043d\u043e", diff --git a/Emby.Server.Implementations/Localization/Core/ca.json b/Emby.Server.Implementations/Localization/Core/ca.json index 7c55540e5..8f3a2287c 100644 --- a/Emby.Server.Implementations/Localization/Core/ca.json +++ b/Emby.Server.Implementations/Localization/Core/ca.json @@ -1,4 +1,12 @@ { + "HeaderCameraUploads": "Camera Uploads", + "ValueHasBeenAddedToLibrary": "{0} has been added to your media library", + "NameInstallFailed": "{0} installation failed", + "CameraImageUploadedFrom": "A new camera image has been uploaded from {0}", + "ServerNameNeedsToBeRestarted": "{0} needs to be restarted", + "NewVersionIsAvailable": "A new version of Emby Server is available for download.", + "MessageApplicationUpdatedTo": "Emby Server has been updated to {0}", + "SubtitleDownloadFailureFromForItem": "Subtitles failed to download from {0} for {1}", "Latest": "Darreres", "ValueSpecialEpisodeName": "Especial - {0}", "Inherit": "Heretat", @@ -24,6 +32,7 @@ "Channels": "Canals", "Movies": "Pel\u00b7l\u00edcules", "Albums": "\u00c0lbums", + "NameSeasonUnknown": "Season Unknown", "Artists": "Artistes", "Folders": "Directoris", "Songs": "Can\u00e7ons", @@ -53,7 +62,7 @@ "UserCreatedWithName": "S'ha creat l'usuari {0}", "UserPasswordChangedWithName": "La contrasenya ha estat canviada per a l'usuari {0}", "UserDeletedWithName": "L'usuari {0} ha estat eliminat", - "UserConfigurationUpdatedWithName": "La configuraci\u00f3 d'usuari ha estat actualitzada per a {0}", + "UserPolicyUpdatedWithName": "User policy has been updated for {0}", "MessageServerConfigurationUpdated": "S'ha actualitzat la configuraci\u00f3 del servidor", "MessageNamedServerConfigurationUpdatedWithValue": "La secci\u00f3 de configuraci\u00f3 {0} ha estat actualitzada", "MessageApplicationUpdated": "El Servidor d'Emby ha estat actualitzat", diff --git a/Emby.Server.Implementations/Localization/Core/cs.json b/Emby.Server.Implementations/Localization/Core/cs.json index d59e40ed3..a78b3d3e3 100644 --- a/Emby.Server.Implementations/Localization/Core/cs.json +++ b/Emby.Server.Implementations/Localization/Core/cs.json @@ -1,4 +1,12 @@ { + "HeaderCameraUploads": "Camera Uploads", + "ValueHasBeenAddedToLibrary": "{0} has been added to your media library", + "NameInstallFailed": "{0} installation failed", + "CameraImageUploadedFrom": "A new camera image has been uploaded from {0}", + "ServerNameNeedsToBeRestarted": "{0} needs to be restarted", + "NewVersionIsAvailable": "A new version of Emby Server is available for download.", + "MessageApplicationUpdatedTo": "Emby Server has been updated to {0}", + "SubtitleDownloadFailureFromForItem": "Subtitles failed to download from {0} for {1}", "Latest": "Nejnov\u011bj\u0161\u00ed", "ValueSpecialEpisodeName": "Speci\u00e1l - {0}", "Inherit": "Zd\u011bdit", @@ -24,6 +32,7 @@ "Channels": "Kan\u00e1ly", "Movies": "Filmy", "Albums": "Alba", + "NameSeasonUnknown": "Nezn\u00e1m\u00e1 sez\u00f3na", "Artists": "Um\u011blci", "Folders": "Slo\u017eky", "Songs": "Skladby", @@ -53,7 +62,7 @@ "UserCreatedWithName": "U\u017eivatel {0} byl vytvo\u0159en", "UserPasswordChangedWithName": "Provedena zm\u011bna hesla pro u\u017eivatele {0}", "UserDeletedWithName": "U\u017eivatel {0} byl smaz\u00e1n", - "UserConfigurationUpdatedWithName": "Konfigurace u\u017eivatele byla aktualizov\u00e1na pro {0}", + "UserPolicyUpdatedWithName": "User policy has been updated for {0}", "MessageServerConfigurationUpdated": "Konfigurace serveru aktualizov\u00e1na", "MessageNamedServerConfigurationUpdatedWithValue": "Konfigurace sekce {0} na serveru byla aktualizov\u00e1na", "MessageApplicationUpdated": "Emby Server byl aktualizov\u00e1n", diff --git a/Emby.Server.Implementations/Localization/Core/da.json b/Emby.Server.Implementations/Localization/Core/da.json index eb05943f9..c3a782161 100644 --- a/Emby.Server.Implementations/Localization/Core/da.json +++ b/Emby.Server.Implementations/Localization/Core/da.json @@ -1,4 +1,12 @@ { + "HeaderCameraUploads": "Camera Uploads", + "ValueHasBeenAddedToLibrary": "{0} has been added to your media library", + "NameInstallFailed": "{0} installation failed", + "CameraImageUploadedFrom": "A new camera image has been uploaded from {0}", + "ServerNameNeedsToBeRestarted": "{0} needs to be restarted", + "NewVersionIsAvailable": "A new version of Emby Server is available for download.", + "MessageApplicationUpdatedTo": "Emby Server has been updated to {0}", + "SubtitleDownloadFailureFromForItem": "Subtitles failed to download from {0} for {1}", "Latest": "Seneste", "ValueSpecialEpisodeName": "Special - {0}", "Inherit": "Arv", @@ -24,6 +32,7 @@ "Channels": "Kanaler", "Movies": "Film", "Albums": "Album", + "NameSeasonUnknown": "Season Unknown", "Artists": "Kunstner", "Folders": "Mapper", "Songs": "Sange", @@ -53,7 +62,7 @@ "UserCreatedWithName": "Bruger {0} er blevet oprettet", "UserPasswordChangedWithName": "Adgangskode er \u00e6ndret for bruger {0}", "UserDeletedWithName": "Brugeren {0} er blevet slettet", - "UserConfigurationUpdatedWithName": "Brugerkonfiguration er blevet opdateret for {0}", + "UserPolicyUpdatedWithName": "User policy has been updated for {0}", "MessageServerConfigurationUpdated": "Serverkonfiguration er blevet opdateret", "MessageNamedServerConfigurationUpdatedWithValue": "Server konfigurationssektion {0} er blevet opdateret", "MessageApplicationUpdated": "Emby Server er blevet opdateret", diff --git a/Emby.Server.Implementations/Localization/Core/de.json b/Emby.Server.Implementations/Localization/Core/de.json index bcfadb61c..1836ca5e7 100644 --- a/Emby.Server.Implementations/Localization/Core/de.json +++ b/Emby.Server.Implementations/Localization/Core/de.json @@ -1,4 +1,12 @@ { + "HeaderCameraUploads": "Kamera Uploads", + "ValueHasBeenAddedToLibrary": "{0} wurde ihrer Bibliothek hinzugef\u00fcgt", + "NameInstallFailed": "{0} Installation fehlgeschlagen", + "CameraImageUploadedFrom": "Ein neues Bild wurde hochgeladen von {0}", + "ServerNameNeedsToBeRestarted": "{0} muss neu gestartet werden", + "NewVersionIsAvailable": "Eine neue Version von Emby Server steht zum Download bereit.", + "MessageApplicationUpdatedTo": "Emby Server wurde auf Version {0} aktualisiert", + "SubtitleDownloadFailureFromForItem": "Subtitles failed to download from {0} for {1}", "Latest": "Neueste", "ValueSpecialEpisodeName": "Special - {0}", "Inherit": "\u00dcbernehmen", @@ -24,6 +32,7 @@ "Channels": "Kan\u00e4le", "Movies": "Filme", "Albums": "Alben", + "NameSeasonUnknown": "Staffel unbekannt", "Artists": "Interpreten", "Folders": "Verzeichnisse", "Songs": "Songs", @@ -53,7 +62,7 @@ "UserCreatedWithName": "Benutzer {0} wurde erstellt", "UserPasswordChangedWithName": "Das Passwort f\u00fcr Benutzer {0} wurde ge\u00e4ndert", "UserDeletedWithName": "Benutzer {0} wurde gel\u00f6scht", - "UserConfigurationUpdatedWithName": "Benutzereinstellungen wurden aktualisiert f\u00fcr {0}", + "UserPolicyUpdatedWithName": "Benutzerrichtlinie wurde f\u00fcr {0} aktualisiert", "MessageServerConfigurationUpdated": "Server Einstellungen wurden aktualisiert", "MessageNamedServerConfigurationUpdatedWithValue": "Der Server Einstellungsbereich {0} wurde aktualisiert", "MessageApplicationUpdated": "Emby Server wurde auf den neusten Stand gebracht.", diff --git a/Emby.Server.Implementations/Localization/Core/el.json b/Emby.Server.Implementations/Localization/Core/el.json index ab229e111..c81f3d2ac 100644 --- a/Emby.Server.Implementations/Localization/Core/el.json +++ b/Emby.Server.Implementations/Localization/Core/el.json @@ -1,6 +1,14 @@ { - "Latest": "\u03a4\u03b5\u03bb\u03b5\u03c5\u03c4\u03b1\u03af\u03b1", - "ValueSpecialEpisodeName": "\u0395\u03b9\u03b4\u03b9\u03ba\u03ac - {0} ", + "HeaderCameraUploads": "Camera Uploads", + "ValueHasBeenAddedToLibrary": "{0} \u03c0\u03c1\u03bf\u03c3\u03c4\u03ad\u03b8\u03b7\u03ba\u03b1\u03bd \u03c3\u03c4\u03b7 \u03b2\u03b9\u03b2\u03bb\u03b9\u03bf\u03b8\u03ae\u03ba\u03b7 \u03c0\u03bf\u03bb\u03c5\u03bc\u03ad\u03c3\u03c9\u03bd \u03c3\u03b1\u03c2", + "NameInstallFailed": "{0} \u03b7 \u03b5\u03b3\u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7 \u03b1\u03c0\u03ad\u03c4\u03c5\u03c7\u03b5", + "CameraImageUploadedFrom": "\u039c\u03b9\u03b1 \u03bd\u03ad\u03b1 \u03b5\u03b9\u03ba\u03cc\u03bd\u03b1 \u03ba\u03ac\u03bc\u03b5\u03c1\u03b1\u03c2 \u03ad\u03c7\u03b5\u03b9 \u03b1\u03c0\u03bf\u03c3\u03c4\u03b1\u03bb\u03b5\u03af \u03b1\u03c0\u03cc {0}", + "ServerNameNeedsToBeRestarted": "{0} \u03c7\u03c1\u03b5\u03b9\u03ac\u03b6\u03b5\u03c4\u03b1\u03b9 \u03b5\u03c0\u03b1\u03bd\u03b5\u03ba\u03ba\u03af\u03bd\u03b7\u03c3\u03b7", + "NewVersionIsAvailable": "\u039c\u03b9\u03b1 \u03bd\u03ad\u03b1 \u03ad\u03ba\u03b4\u03bf\u03c3\u03b7 \u03c4\u03bf\u03c5 Emby Server \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03b9\u03b1\u03b8\u03ad\u03c3\u03b9\u03bc\u03b7 \u03b3\u03b9\u03b1 \u03bb\u03ae\u03c8\u03b7.", + "MessageApplicationUpdatedTo": "Emby Server has been updated to {0}", + "SubtitleDownloadFailureFromForItem": "Subtitles failed to download from {0} for {1}", + "Latest": "\u03a0\u03c1\u03cc\u03c3\u03c6\u03b1\u03c4\u03b1", + "ValueSpecialEpisodeName": "Special - {0}", "Inherit": "Inherit", "Books": "\u0392\u03b9\u03b2\u03bb\u03af\u03b1", "Music": "\u039c\u03bf\u03c5\u03c3\u03b9\u03ba\u03ae", @@ -8,15 +16,15 @@ "Photos": "\u03a6\u03c9\u03c4\u03bf\u03b3\u03c1\u03b1\u03c6\u03af\u03b5\u03c2", "MixedContent": "\u0391\u03bd\u03ac\u03bc\u03b5\u03b9\u03ba\u03c4\u03bf \u03a0\u03b5\u03c1\u03b9\u03b5\u03c7\u03cc\u03bc\u03b5\u03bd\u03bf", "MusicVideos": "\u039c\u03bf\u03c5\u03c3\u03b9\u03ba\u03ac \u03b2\u03af\u03bd\u03c4\u03b5\u03bf", - "HomeVideos": "\u03a0\u03c1\u03bf\u03c3\u03c9\u03c0\u03b9\u03ba\u03ac \u0392\u03af\u03bd\u03c4\u03b5\u03bf", + "HomeVideos": "\u03a0\u03c1\u03bf\u03c3\u03c9\u03c0\u03b9\u03ba\u03ac \u03b2\u03af\u03bd\u03c4\u03b5\u03bf", "Playlists": "\u039b\u03af\u03c3\u03c4\u03b5\u03c2 \u03b1\u03bd\u03b1\u03c0\u03b1\u03c1\u03b1\u03b3\u03c9\u03b3\u03ae\u03c2", "HeaderRecordingGroups": "\u0393\u03ba\u03c1\u03bf\u03c5\u03c0 \u0395\u03b3\u03b3\u03c1\u03b1\u03c6\u03ce\u03bd", - "HeaderContinueWatching": "\u03a3\u03c5\u03bd\u03b5\u03c7\u03af\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03c0\u03b1\u03c1\u03b1\u03ba\u03bf\u03bb\u03bf\u03cd\u03b8\u03b7\u03c3\u03b7", + "HeaderContinueWatching": "\u03a3\u03c5\u03bd\u03b5\u03c7\u03af\u03c3\u03c4\u03b5 \u03bd\u03b1 \u03c0\u03b1\u03c1\u03b1\u03ba\u03bf\u03bb\u03bf\u03c5\u03b8\u03b5\u03af\u03c4\u03b5", "HeaderFavoriteArtists": "\u0391\u03b3\u03b1\u03c0\u03b7\u03bc\u03ad\u03bd\u03bf\u03b9 \u039a\u03b1\u03bb\u03bb\u03b9\u03c4\u03ad\u03c7\u03bd\u03b5\u03c2", "HeaderFavoriteSongs": "\u0391\u03b3\u03b1\u03c0\u03b7\u03bc\u03ad\u03bd\u03b1 \u03a4\u03c1\u03b1\u03b3\u03bf\u03cd\u03b4\u03b9\u03b1", "HeaderAlbumArtists": "\u0386\u03bb\u03bc\u03c0\u03bf\u03c5\u03bc \u039a\u03b1\u03bb\u03bb\u03b9\u03c4\u03b5\u03c7\u03bd\u03ce\u03bd", "HeaderFavoriteAlbums": "\u0391\u03b3\u03b1\u03c0\u03b7\u03bc\u03ad\u03bd\u03b1 \u0386\u03bb\u03bc\u03c0\u03bf\u03c5\u03bc", - "HeaderFavoriteEpisodes": "\u0391\u03b3\u03b1\u03c0\u03b7\u03bc\u03ad\u03bd\u03b1 \u03b5\u03c0\u03b5\u03b9\u03c3\u03cc\u03b4\u03b9\u03b1", + "HeaderFavoriteEpisodes": "\u0391\u03b3\u03b1\u03c0\u03b7\u03bc\u03ad\u03bd\u03b1 \u0395\u03c0\u03b5\u03b9\u03c3\u03cc\u03b4\u03b9\u03b1", "HeaderFavoriteShows": "\u0391\u03b3\u03b1\u03c0\u03b7\u03bc\u03ad\u03bd\u03b5\u03c2 \u03a3\u03b5\u03b9\u03c1\u03ad\u03c2", "HeaderNextUp": "\u0395\u03c0\u03cc\u03bc\u03b5\u03bd\u03bf", "Favorites": "\u0391\u03b3\u03b1\u03c0\u03b7\u03bc\u03ad\u03bd\u03b1", @@ -24,10 +32,11 @@ "Channels": "\u039a\u03b1\u03bd\u03ac\u03bb\u03b9\u03b1", "Movies": "\u03a4\u03b1\u03b9\u03bd\u03af\u03b5\u03c2", "Albums": "\u0386\u03bb\u03bc\u03c0\u03bf\u03c5\u03bc", + "NameSeasonUnknown": "\u0386\u03b3\u03bd\u03c9\u03c3\u03c4\u03bf\u03c2 \u039a\u03cd\u03ba\u03bb\u03bf\u03c2", "Artists": "\u039a\u03b1\u03bb\u03bb\u03b9\u03c4\u03ad\u03c7\u03bd\u03b5\u03c2", "Folders": "\u03a6\u03ac\u03ba\u03b5\u03bb\u03bf\u03b9", "Songs": "\u03a4\u03c1\u03b1\u03b3\u03bf\u03cd\u03b4\u03b9\u03b1", - "TvShows": "\u03a4\u03b7\u03bb\u03b5\u03bf\u03c0\u03c4\u03b9\u03ba\u03ac \u03c0\u03c1\u03bf\u03b3\u03c1\u03ac\u03bc\u03bc\u03b1\u03c4\u03b1", + "TvShows": "\u03a4\u03b7\u03bb\u03b5\u03bf\u03c0\u03c4\u03b9\u03ba\u03ad\u03c2 \u03a3\u03b5\u03b9\u03c1\u03ad\u03c2", "Shows": "\u03a3\u03b5\u03b9\u03c1\u03ad\u03c2", "Genres": "\u0395\u03af\u03b4\u03b7", "NameSeasonNumber": "\u039a\u03cd\u03ba\u03bb\u03bf\u03c2 {0}", @@ -35,57 +44,57 @@ "UserDownloadingItemWithValues": "{0} \u03ba\u03b1\u03c4\u03b5\u03b2\u03ac\u03b6\u03b5\u03b9 {1}", "HeaderLiveTV": "\u0396\u03c9\u03bd\u03c4\u03b1\u03bd\u03ae \u03a4\u03b7\u03bb\u03b5\u03cc\u03c1\u03b1\u03c3\u03b7", "ChapterNameValue": "\u039a\u03b5\u03c6\u03ac\u03bb\u03b1\u03b9\u03bf {0}", - "ScheduledTaskFailedWithName": "{0} \u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1", + "ScheduledTaskFailedWithName": "{0} \u03b1\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1", "LabelRunningTimeValue": "\u0394\u03b9\u03ac\u03c1\u03ba\u03b5\u03b9\u03b1: {0}", - "ScheduledTaskStartedWithName": "{0} \u03ad\u03bd\u03b1\u03c1\u03be\u03b7", + "ScheduledTaskStartedWithName": "{0} \u03be\u03b5\u03ba\u03af\u03bd\u03b7\u03c3\u03b5", "VersionNumber": "\u0388\u03ba\u03b4\u03bf\u03c3\u03b7 {0}", "PluginInstalledWithName": "{0} \u03b5\u03b3\u03ba\u03b1\u03c4\u03b1\u03c3\u03c4\u03ae\u03b8\u03b7\u03ba\u03b5", - "StartupEmbyServerIsLoading": "\u039f \u03a3\u03ad\u03c1\u03b2\u03b5\u03c1 \u03c6\u03bf\u03c1\u03c4\u03ce\u03bd\u03b5\u03b9. \u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03ce \u03b4\u03bf\u03ba\u03b9\u03bc\u03ac\u03c3\u03c4\u03b5 \u03c3\u03b5 \u03bb\u03af\u03b3\u03bf", + "StartupEmbyServerIsLoading": "\u039f Emby Server \u03c6\u03bf\u03c1\u03c4\u03ce\u03bd\u03b5\u03b9. \u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03ce \u03b4\u03bf\u03ba\u03b9\u03bc\u03ac\u03c3\u03c4\u03b5 \u03c3\u03b5 \u03bb\u03af\u03b3\u03bf.", "PluginUpdatedWithName": "{0} \u03ad\u03c7\u03b5\u03b9 \u03b1\u03bd\u03b1\u03b2\u03b1\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af", "PluginUninstalledWithName": "{0} \u03ad\u03c7\u03b5\u03b9 \u03b1\u03c0\u03b5\u03b3\u03ba\u03b1\u03c4\u03b1\u03c3\u03c4\u03b1\u03b8\u03b5\u03af", "ItemAddedWithName": "{0} \u03c0\u03c1\u03bf\u03c3\u03c4\u03ad\u03b8\u03b7\u03ba\u03b5 \u03c3\u03c4\u03b7 \u03b2\u03b9\u03b2\u03bb\u03b9\u03bf\u03b8\u03ae\u03ba\u03b7", "ItemRemovedWithName": "{0} \u03b4\u03b9\u03b1\u03b3\u03c1\u03ac\u03c6\u03b7\u03ba\u03b5 \u03b1\u03c0\u03cc \u03c4\u03b7 \u03b2\u03b9\u03b2\u03bb\u03b9\u03bf\u03b8\u03ae\u03ba\u03b7", "LabelIpAddressValue": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP: {0}", "DeviceOnlineWithName": "{0} \u03c3\u03c5\u03bd\u03b4\u03ad\u03b8\u03b7\u03ba\u03b5", - "UserOnlineFromDevice": "{0} \u03b5\u03af\u03bd\u03b1\u03b9 \u03c3\u03c5\u03bd\u03b4\u03b5\u03b4\u03b5\u03bc\u03ad\u03bd\u03bf\u03c2 \u03b1\u03c0\u03bf {1}", - "ProviderValue": "\u03a0\u03ac\u03c1\u03bf\u03c7\u03bf\u03c2: {0}", - "SubtitlesDownloadedForItem": "\u03a5\u03c0\u03cc\u03c4\u03b9\u03c4\u03bb\u03bf\u03b9 \u03bb\u03ae\u03c6\u03b8\u03b7\u03ba\u03b1\u03bd \u03b1\u03c0\u03cc {0}", + "UserOnlineFromDevice": "{0} \u03b5\u03af\u03bd\u03b1\u03b9 online \u03b1\u03c0\u03bf {1}", + "ProviderValue": "Provider: {0}", + "SubtitlesDownloadedForItem": "\u039f\u03b9 \u03c5\u03c0\u03cc\u03c4\u03b9\u03c4\u03bb\u03bf\u03b9 \u03ba\u03b1\u03c4\u03ad\u03b2\u03b7\u03ba\u03b1\u03bd \u03b3\u03b9\u03b1 {0}", "UserCreatedWithName": "\u0394\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03ae\u03b8\u03b7\u03ba\u03b5 \u03bf \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7\u03c2 {0}", - "UserPasswordChangedWithName": "\u039f \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c4\u03bf\u03c5 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7 {0} \u03b1\u03bb\u03bb\u03ac\u03c7\u03b8\u03b7\u03ba\u03b5", - "UserDeletedWithName": "\u039f \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7\u03c2 {0} \u03b4\u03b9\u03b5\u03b3\u03c1\u03ac\u03c6\u03b5\u03b9", - "UserConfigurationUpdatedWithName": "\u039f\u03b9 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 \u03c4\u03bf\u03c5 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7 {0} \u03ad\u03c7\u03bf\u03c5\u03bd \u03b1\u03bb\u03bb\u03ac\u03be\u03b5\u03b9", - "MessageServerConfigurationUpdated": "Server configuration has been updated", - "MessageNamedServerConfigurationUpdatedWithValue": "\u039f\u03b9 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 \u03c4\u03bf\u03bc\u03ad\u03b1 \u03c4\u03bf\u03c5 \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae {0} \u03ad\u03c7\u03bf\u03c5\u03bd \u03b1\u03bb\u03bb\u03ac\u03be\u03b5\u03b9", - "MessageApplicationUpdated": "\u039f \u03a3\u03ad\u03c1\u03b2\u03b5\u03c1 \u03ad\u03c7\u03b5\u03b9 \u03b1\u03bd\u03b1\u03b2\u03b1\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af", - "FailedLoginAttemptWithUserName": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c0\u03c1\u03bf\u03c3\u03c0\u03ac\u03b8\u03b5\u03b9\u03b1 \u03b5\u03b9\u03c3\u03cc\u03b4\u03bf\u03c5 \u03b1\u03c0\u03cc {0}", + "UserPasswordChangedWithName": "\u039f \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c4\u03bf\u03c5 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7 {0} \u03ad\u03c7\u03b5\u03b9 \u03b1\u03bb\u03bb\u03ac\u03be\u03b5\u03b9", + "UserDeletedWithName": "\u039f \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7\u03c2 {0} \u03ad\u03c7\u03b5\u03b9 \u03b4\u03b9\u03b1\u03b3\u03c1\u03b1\u03c6\u03b5\u03af", + "UserPolicyUpdatedWithName": "\u0397 \u03c0\u03bf\u03bb\u03b9\u03c4\u03b9\u03ba\u03ae \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7 \u03ad\u03c7\u03b5\u03b9 \u03b5\u03bd\u03b7\u03bc\u03b5\u03c1\u03c9\u03b8\u03b5\u03af \u03b3\u03b9\u03b1 {0}", + "MessageServerConfigurationUpdated": "\u0397 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c0\u03b1\u03c1\u03b1\u03bc\u03ad\u03c4\u03c1\u03c9\u03bd \u03c4\u03bf\u03c5 server \u03ad\u03c7\u03b5\u03b9 \u03b5\u03bd\u03b7\u03bc\u03b5\u03c1\u03c9\u03b8\u03b5\u03af", + "MessageNamedServerConfigurationUpdatedWithValue": "\u0397 \u03b5\u03bd\u03cc\u03c4\u03b7\u03c4\u03b1 {0} \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7\u03c2 \u03c0\u03b1\u03c1\u03b1\u03bc\u03ad\u03c4\u03c1\u03c9\u03bd \u03c4\u03bf\u03c5 server \u03ad\u03c7\u03b5\u03b9 \u03b5\u03bd\u03b7\u03bc\u03b5\u03c1\u03c9\u03b8\u03b5\u03af", + "MessageApplicationUpdated": "\u039f Emby Server \u03ad\u03c7\u03b5\u03b9 \u03b5\u03bd\u03b7\u03bc\u03b5\u03c1\u03c9\u03b8\u03b5\u03af", + "FailedLoginAttemptWithUserName": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03b7\u03bc\u03ad\u03bd\u03b7 \u03c0\u03c1\u03bf\u03c3\u03c0\u03ac\u03b8\u03b5\u03b9\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03b1\u03c0\u03cc {0}", "AuthenticationSucceededWithUserName": "{0} \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03b5\u03af\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7", "UserOfflineFromDevice": "{0} \u03b1\u03c0\u03bf\u03c3\u03c5\u03bd\u03b4\u03ad\u03b8\u03b7\u03ba\u03b5 \u03b1\u03c0\u03cc {1}", "DeviceOfflineWithName": "{0} \u03b1\u03c0\u03bf\u03c3\u03c5\u03bd\u03b4\u03ad\u03b8\u03b7\u03ba\u03b5", - "UserStartedPlayingItemWithValues": "{0} \u03be\u03b5\u03ba\u03af\u03bd\u03b7\u03c3\u03b5 \u03bd\u03b1 \u03c0\u03b1\u03af\u03b6\u03b5\u03b9 {1}", - "UserStoppedPlayingItemWithValues": "{0} \u03c3\u03c4\u03b1\u03bc\u03ac\u03c4\u03b7\u03c3\u03b5 \u03bd\u03b1 \u03c0\u03b1\u03af\u03b6\u03b5\u03b9 {1}", - "NotificationOptionPluginError": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c0\u03c1\u03bf\u03c3\u03b8\u03ad\u03c4\u03bf\u03c5", - "NotificationOptionApplicationUpdateAvailable": "\u03a5\u03c0\u03ac\u03c1\u03c7\u03b5\u03b9 \u03b1\u03bd\u03b1\u03b2\u03ac\u03b8\u03bc\u03b9\u03c3\u03b7", - "NotificationOptionApplicationUpdateInstalled": "\u0397 \u03b1\u03bd\u03b1\u03b2\u03ac\u03b8\u03bc\u03b9\u03c3\u03b7 \u03bf\u03bb\u03bf\u03ba\u03bb\u03b7\u03c1\u03ce\u03b8\u03b7\u03ba\u03b5", - "NotificationOptionPluginUpdateInstalled": "\u0397 \u03b1\u03bd\u03b1\u03b2\u03ac\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c4\u03bf\u03c5 plugin \u03bf\u03bb\u03bf\u03ba\u03bb\u03b7\u03c1\u03ce\u03b8\u03b7\u03ba\u03b5", + "UserStartedPlayingItemWithValues": "{0} \u03c0\u03b1\u03af\u03b6\u03b5\u03b9 {1} \u03c3\u03b5 {2}", + "UserStoppedPlayingItemWithValues": "{0} \u03c4\u03b5\u03bb\u03b5\u03af\u03c9\u03c3\u03b5 \u03bd\u03b1 \u03c0\u03b1\u03af\u03b6\u03b5\u03b9 {1} \u03c3\u03b5 {2}", + "NotificationOptionPluginError": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c4\u03bf\u03c5 plugin", + "NotificationOptionApplicationUpdateAvailable": "\u0394\u03b9\u03b1\u03b8\u03ad\u03c3\u03b9\u03bc\u03b7 \u03b5\u03bd\u03b7\u03bc\u03b5\u03c1\u03c9\u03bc\u03ad\u03bd\u03b7 \u03ad\u03ba\u03b4\u03bf\u03c3\u03b7 \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae\u03c2", + "NotificationOptionApplicationUpdateInstalled": "\u0397 \u03b5\u03bd\u03b7\u03bc\u03ad\u03c1\u03c9\u03c3\u03b7 \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae\u03c2 \u03b5\u03b3\u03ba\u03b1\u03c4\u03b1\u03c3\u03c4\u03ac\u03b8\u03b7\u03ba\u03b5", + "NotificationOptionPluginUpdateInstalled": "\u0397 \u03b1\u03bd\u03b1\u03b2\u03ac\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c4\u03bf\u03c5 plugin \u03b5\u03b3\u03ba\u03b1\u03c4\u03b1\u03c3\u03c4\u03ac\u03b8\u03b7\u03ba\u03b5", "NotificationOptionPluginInstalled": "\u03a4\u03bf plugin \u03b5\u03b3\u03ba\u03b1\u03c4\u03b1\u03c3\u03c4\u03ac\u03b8\u03b7\u03ba\u03b5", "NotificationOptionPluginUninstalled": "\u03a4\u03bf plugin \u03b1\u03c0\u03b5\u03b3\u03ba\u03b1\u03c4\u03b1\u03c3\u03c4\u03ac\u03b8\u03b7\u03ba\u03b5", - "NotificationOptionVideoPlayback": "\u03a4\u03bf \u03b2\u03af\u03bd\u03c4\u03b5\u03bf \u03c0\u03c1\u03bf\u03b2\u03ac\u03bb\u03bb\u03b5\u03c4\u03b1\u03b9", - "NotificationOptionAudioPlayback": "\u0397 \u03bc\u03bf\u03c5\u03c3\u03b9\u03ba\u03ae \u03c0\u03b1\u03af\u03b6\u03b5\u03b9", - "NotificationOptionGamePlayback": "\u03a4\u03bf \u03c0\u03b1\u03b9\u03c7\u03bd\u03af\u03b4\u03b9 \u03be\u03b5\u03ba\u03af\u03bd\u03b7\u03c3\u03b5", - "NotificationOptionVideoPlaybackStopped": "\u03a4\u03bf \u03b2\u03af\u03bd\u03c4\u03b5\u03bf \u03c3\u03c4\u03b1\u03bc\u03ac\u03c4\u03b7\u03c3\u03b5", - "NotificationOptionAudioPlaybackStopped": "\u0397 \u03bc\u03bf\u03c5\u03c3\u03b9\u03ba\u03ae \u03c3\u03c4\u03b1\u03bc\u03ac\u03c4\u03b7\u03c3\u03b5", - "NotificationOptionGamePlaybackStopped": "\u03a4\u03bf \u03c0\u03b1\u03b9\u03c7\u03bd\u03af\u03b4\u03b9 \u03c3\u03c4\u03b1\u03bc\u03ac\u03c4\u03b7\u03c3\u03b5", + "NotificationOptionVideoPlayback": "\u0397 \u03b1\u03bd\u03b1\u03c0\u03b1\u03c1\u03b1\u03b3\u03c9\u03b3\u03ae \u03b2\u03af\u03bd\u03c4\u03b5\u03bf \u03be\u03b5\u03ba\u03af\u03bd\u03b7\u03c3\u03b5", + "NotificationOptionAudioPlayback": "\u0397 \u03b1\u03bd\u03b1\u03c0\u03b1\u03c1\u03b1\u03b3\u03c9\u03b3\u03ae \u03ae\u03c7\u03bf\u03c5 \u03be\u03b5\u03ba\u03af\u03bd\u03b7\u03c3\u03b5", + "NotificationOptionGamePlayback": "\u0397 \u03b1\u03bd\u03b1\u03c0\u03b1\u03c1\u03b1\u03b3\u03c9\u03b3\u03ae \u03c4\u03bf\u03c5 \u03c0\u03b1\u03b9\u03c7\u03bd\u03b9\u03b4\u03b9\u03bf\u03cd \u03be\u03b5\u03ba\u03af\u03bd\u03b7\u03c3\u03b5", + "NotificationOptionVideoPlaybackStopped": "\u0397 \u03b1\u03bd\u03b1\u03c0\u03b1\u03c1\u03b1\u03b3\u03c9\u03b3\u03ae \u03b2\u03af\u03bd\u03c4\u03b5\u03bf \u03c3\u03c4\u03b1\u03bc\u03ac\u03c4\u03b7\u03c3\u03b5", + "NotificationOptionAudioPlaybackStopped": "\u0397 \u03b1\u03bd\u03b1\u03c0\u03b1\u03c1\u03b1\u03b3\u03c9\u03b3\u03ae \u03ae\u03c7\u03bf\u03c5 \u03c3\u03c4\u03b1\u03bc\u03ac\u03c4\u03b7\u03c3\u03b5", + "NotificationOptionGamePlaybackStopped": "\u0397 \u03b1\u03bd\u03b1\u03c0\u03b1\u03c1\u03b1\u03b3\u03c9\u03b3\u03ae \u03c4\u03bf\u03c5 \u03c0\u03b1\u03b9\u03c7\u03bd\u03b9\u03b4\u03b9\u03bf\u03cd \u03c3\u03c4\u03b1\u03bc\u03ac\u03c4\u03b7\u03c3\u03b5", "NotificationOptionTaskFailed": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c0\u03c1\u03bf\u03b3\u03c1\u03b1\u03bc\u03bc\u03b1\u03c4\u03b9\u03c3\u03bc\u03ad\u03bd\u03b7\u03c2 \u03b5\u03c1\u03b3\u03b1\u03c3\u03af\u03b1\u03c2", "NotificationOptionInstallationFailed": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03b5\u03b3\u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7\u03c2", "NotificationOptionNewLibraryContent": "\u03a0\u03c1\u03bf\u03c3\u03c4\u03ad\u03b8\u03b7\u03ba\u03b5 \u03bd\u03ad\u03bf \u03c0\u03b5\u03c1\u03b9\u03b5\u03c7\u03cc\u03bc\u03b5\u03bd\u03bf", "NotificationOptionCameraImageUploaded": "Camera image uploaded", "NotificationOptionUserLockedOut": "\u039f \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7\u03c2 \u03b1\u03c0\u03bf\u03ba\u03bb\u03b5\u03af\u03c3\u03c4\u03b7\u03ba\u03b5", - "NotificationOptionServerRestartRequired": "\u0391\u03c0\u03b1\u03b9\u03c4\u03b5\u03af\u03c4\u03b1\u03b9 \u03b5\u03c0\u03b1\u03bd\u03b5\u03ba\u03ba\u03af\u03bd\u03b7\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae", + "NotificationOptionServerRestartRequired": "\u0391\u03c0\u03b1\u03b9\u03c4\u03b5\u03af\u03c4\u03b1\u03b9 \u03b5\u03c0\u03b1\u03bd\u03b5\u03ba\u03ba\u03af\u03bd\u03b7\u03c3\u03b7 \u03c4\u03bf\u03c5 server", "UserLockedOutWithName": "\u039f \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7\u03c2 {0} \u03b1\u03c0\u03bf\u03ba\u03bb\u03b5\u03af\u03c3\u03c4\u03b7\u03ba\u03b5", - "SubtitleDownloadFailureForItem": "\u0391\u03b4\u03c5\u03bd\u03b1\u03bc\u03af\u03b1 \u03bb\u03ae\u03c8\u03b7\u03c2 \u03c5\u03c0\u03bf\u03c4\u03af\u03c4\u03bb\u03c9\u03bd \u03b1\u03c0\u03cc {0}", + "SubtitleDownloadFailureForItem": "\u039f\u03b9 \u03c5\u03c0\u03cc\u03c4\u03b9\u03c4\u03bb\u03bf\u03b9 \u03b1\u03c0\u03ad\u03c4\u03c5\u03c7\u03b1\u03bd \u03bd\u03b1 \u03ba\u03b1\u03c4\u03ad\u03b2\u03bf\u03c5\u03bd \u03b3\u03b9\u03b1 {0}", "Sync": "\u03a3\u03c5\u03b3\u03c7\u03c1\u03bf\u03bd\u03b9\u03c3\u03bc\u03cc\u03c2", "User": "\u03a7\u03c1\u03ae\u03c3\u03c4\u03b7\u03c2", "System": "\u03a3\u03cd\u03c3\u03c4\u03b7\u03bc\u03b1", "Application": "\u0395\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae", - "Plugin": "\u03a0\u03c1\u03cc\u03c3\u03b8\u03b5\u03c4\u03bf" + "Plugin": "Plugin" }
\ No newline at end of file diff --git a/Emby.Server.Implementations/Localization/Core/en-GB.json b/Emby.Server.Implementations/Localization/Core/en-GB.json index 62db5a358..6e3d29b13 100644 --- a/Emby.Server.Implementations/Localization/Core/en-GB.json +++ b/Emby.Server.Implementations/Localization/Core/en-GB.json @@ -1,4 +1,12 @@ { + "HeaderCameraUploads": "Camera Uploads", + "ValueHasBeenAddedToLibrary": "{0} has been added to your media library", + "NameInstallFailed": "{0} installation failed", + "CameraImageUploadedFrom": "A new camera image has been uploaded from {0}", + "ServerNameNeedsToBeRestarted": "{0} needs to be restarted", + "NewVersionIsAvailable": "A new version of Emby Server is available for download.", + "MessageApplicationUpdatedTo": "Emby Server has been updated to {0}", + "SubtitleDownloadFailureFromForItem": "Subtitles failed to download from {0} for {1}", "Latest": "Latest", "ValueSpecialEpisodeName": "Special - {0}", "Inherit": "Inherit", @@ -24,6 +32,7 @@ "Channels": "Channels", "Movies": "Movies", "Albums": "Albums", + "NameSeasonUnknown": "Season Unknown", "Artists": "Artists", "Folders": "Folders", "Songs": "Songs", @@ -53,7 +62,7 @@ "UserCreatedWithName": "User {0} has been created", "UserPasswordChangedWithName": "Password has been changed for user {0}", "UserDeletedWithName": "User {0} has been deleted", - "UserConfigurationUpdatedWithName": "User configuration has been updated for {0}", + "UserPolicyUpdatedWithName": "User policy has been updated for {0}", "MessageServerConfigurationUpdated": "Server configuration has been updated", "MessageNamedServerConfigurationUpdatedWithValue": "Server configuration section {0} has been updated", "MessageApplicationUpdated": "Emby Server has been updated", diff --git a/Emby.Server.Implementations/Localization/Core/en-US.json b/Emby.Server.Implementations/Localization/Core/en-US.json index 9c58b4539..a97848f8a 100644 --- a/Emby.Server.Implementations/Localization/Core/en-US.json +++ b/Emby.Server.Implementations/Localization/Core/en-US.json @@ -24,6 +24,7 @@ "Channels": "Channels", "Movies": "Movies", "Albums": "Albums", + "NameSeasonUnknown": "Season Unknown", "Artists": "Artists", "Folders": "Folders", "Songs": "Songs", @@ -44,6 +45,7 @@ "PluginUpdatedWithName": "{0} was updated", "PluginUninstalledWithName": "{0} was uninstalled", "ItemAddedWithName": "{0} was added to the library", + "MessageApplicationUpdatedTo": "Emby Server has been updated to {0}", "ItemRemovedWithName": "{0} was removed from the library", "LabelIpAddressValue": "Ip address: {0}", "DeviceOnlineWithName": "{0} is connected", @@ -53,7 +55,7 @@ "UserCreatedWithName": "User {0} has been created", "UserPasswordChangedWithName": "Password has been changed for user {0}", "UserDeletedWithName": "User {0} has been deleted", - "UserConfigurationUpdatedWithName": "User configuration has been updated for {0}", + "UserPolicyUpdatedWithName": "User policy has been updated for {0}", "MessageServerConfigurationUpdated": "Server configuration has been updated", "MessageNamedServerConfigurationUpdatedWithValue": "Server configuration section {0} has been updated", "MessageApplicationUpdated": "Emby Server has been updated", @@ -61,8 +63,9 @@ "AuthenticationSucceededWithUserName": "{0} successfully authenticated", "UserOfflineFromDevice": "{0} has disconnected from {1}", "DeviceOfflineWithName": "{0} has disconnected", - "UserStartedPlayingItemWithValues": "{0} has started playing {1}", - "UserStoppedPlayingItemWithValues": "{0} has stopped playing {1}", + "UserStartedPlayingItemWithValues": "{0} is playing {1} on {2}", + "UserStoppedPlayingItemWithValues": "{0} has finished playing {1} on {2}", + "ValueHasBeenAddedToLibrary": "{0} has been added to your media library", "NotificationOptionPluginError": "Plugin failure", "NotificationOptionApplicationUpdateAvailable": "Application update available", "NotificationOptionApplicationUpdateInstalled": "Application update installed", @@ -82,10 +85,15 @@ "NotificationOptionUserLockedOut": "User locked out", "NotificationOptionServerRestartRequired": "Server restart required", "UserLockedOutWithName": "User {0} has been locked out", - "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}", + "SubtitleDownloadFailureFromForItem": "Subtitles failed to download from {0} for {1}", + "NameInstallFailed": "{0} installation failed", "Sync": "Sync", "User": "User", "System": "System", "Application": "Application", - "Plugin": "Plugin" + "Plugin": "Plugin", + "HeaderCameraUploads": "Camera Uploads", + "CameraImageUploadedFrom": "A new camera image has been uploaded from {0}", + "ServerNameNeedsToBeRestarted": "{0} needs to be restarted", + "NewVersionIsAvailable": "A new version of Emby Server is available for download." }
\ No newline at end of file diff --git a/Emby.Server.Implementations/Localization/Core/es-AR.json b/Emby.Server.Implementations/Localization/Core/es-AR.json index c48042d9a..17d9c80a0 100644 --- a/Emby.Server.Implementations/Localization/Core/es-AR.json +++ b/Emby.Server.Implementations/Localization/Core/es-AR.json @@ -1,4 +1,12 @@ { + "HeaderCameraUploads": "Camera Uploads", + "ValueHasBeenAddedToLibrary": "{0} has been added to your media library", + "NameInstallFailed": "{0} installation failed", + "CameraImageUploadedFrom": "A new camera image has been uploaded from {0}", + "ServerNameNeedsToBeRestarted": "{0} needs to be restarted", + "NewVersionIsAvailable": "A new version of Emby Server is available for download.", + "MessageApplicationUpdatedTo": "Emby Server has been updated to {0}", + "SubtitleDownloadFailureFromForItem": "Subtitles failed to download from {0} for {1}", "Latest": "Latest", "ValueSpecialEpisodeName": "Special - {0}", "Inherit": "Inherit", @@ -24,6 +32,7 @@ "Channels": "Channels", "Movies": "Movies", "Albums": "Albums", + "NameSeasonUnknown": "Season Unknown", "Artists": "Artists", "Folders": "Folders", "Songs": "Songs", @@ -53,7 +62,7 @@ "UserCreatedWithName": "User {0} has been created", "UserPasswordChangedWithName": "Password has been changed for user {0}", "UserDeletedWithName": "User {0} has been deleted", - "UserConfigurationUpdatedWithName": "User configuration has been updated for {0}", + "UserPolicyUpdatedWithName": "User policy has been updated for {0}", "MessageServerConfigurationUpdated": "Server configuration has been updated", "MessageNamedServerConfigurationUpdatedWithValue": "Server configuration section {0} has been updated", "MessageApplicationUpdated": "Emby Server has been updated", @@ -61,8 +70,8 @@ "AuthenticationSucceededWithUserName": "{0} successfully authenticated", "UserOfflineFromDevice": "{0} has disconnected from {1}", "DeviceOfflineWithName": "{0} has disconnected", - "UserStartedPlayingItemWithValues": "{0} has started playing {1}", - "UserStoppedPlayingItemWithValues": "{0} has stopped playing {1}", + "UserStartedPlayingItemWithValues": "{0} is playing {1} on {2}", + "UserStoppedPlayingItemWithValues": "{0} has finished playing {1} on {2}", "NotificationOptionPluginError": "Plugin failure", "NotificationOptionApplicationUpdateAvailable": "Application update available", "NotificationOptionApplicationUpdateInstalled": "Application update installed", diff --git a/Emby.Server.Implementations/Localization/Core/es-MX.json b/Emby.Server.Implementations/Localization/Core/es-MX.json index 8bfaffec8..704566550 100644 --- a/Emby.Server.Implementations/Localization/Core/es-MX.json +++ b/Emby.Server.Implementations/Localization/Core/es-MX.json @@ -1,4 +1,12 @@ { + "HeaderCameraUploads": "Subidos desde Camara", + "ValueHasBeenAddedToLibrary": "{0} se han a\u00f1adido a su biblioteca de medios", + "NameInstallFailed": "{0} instalaci\u00f3n fallida", + "CameraImageUploadedFrom": "Una nueva imagen de c\u00e1mara ha sido subida desde {0}", + "ServerNameNeedsToBeRestarted": "{0} debe ser reiniciado", + "NewVersionIsAvailable": "Una nueva versi\u00f3n del Servidor Emby est\u00e1 disponible para descargar.", + "MessageApplicationUpdatedTo": "El servidor Emby ha sido actualizado a {0}", + "SubtitleDownloadFailureFromForItem": "Fall\u00f3 la descarga de subtitulos desde {0} para {1}", "Latest": "Recientes", "ValueSpecialEpisodeName": "Especial - {0}", "Inherit": "Heredar", @@ -24,6 +32,7 @@ "Channels": "Canales", "Movies": "Pel\u00edculas", "Albums": "\u00c1lbumes", + "NameSeasonUnknown": "Temporada Desconocida", "Artists": "Artistas", "Folders": "Carpetas", "Songs": "Canciones", @@ -53,7 +62,7 @@ "UserCreatedWithName": "Se ha creado el usuario {0}", "UserPasswordChangedWithName": "Se ha cambiado la contrase\u00f1a para el usuario {0}", "UserDeletedWithName": "Se ha eliminado el usuario {0}", - "UserConfigurationUpdatedWithName": "Se ha actualizado la configuraci\u00f3n del usuario {0}", + "UserPolicyUpdatedWithName": "Las pol\u00edtica de usuario ha sido actualizada por {0}", "MessageServerConfigurationUpdated": "Se ha actualizado la configuraci\u00f3n del servidor", "MessageNamedServerConfigurationUpdatedWithValue": "Se ha actualizado la secci\u00f3n {0} de la configuraci\u00f3n del servidor", "MessageApplicationUpdated": "El servidor Emby ha sido actualizado", @@ -61,8 +70,8 @@ "AuthenticationSucceededWithUserName": "{0} autenticado con \u00e9xito", "UserOfflineFromDevice": "{0} se ha desconectado desde {1}", "DeviceOfflineWithName": "{0} se ha desconectado", - "UserStartedPlayingItemWithValues": "{0} ha iniciado la reproducci\u00f3n de {1}", - "UserStoppedPlayingItemWithValues": "{0} ha detenido la reproducci\u00f3n de {1}", + "UserStartedPlayingItemWithValues": "{0} est\u00e1 reproduci\u00e9ndose {1} en {2}", + "UserStoppedPlayingItemWithValues": "{0} ha terminado de reproducirse {1} en {2}", "NotificationOptionPluginError": "Falla de complemento", "NotificationOptionApplicationUpdateAvailable": "Actualizaci\u00f3n de aplicaci\u00f3n disponible", "NotificationOptionApplicationUpdateInstalled": "Actualizaci\u00f3n de aplicaci\u00f3n instalada", diff --git a/Emby.Server.Implementations/Localization/Core/es.json b/Emby.Server.Implementations/Localization/Core/es.json index c7fa51c03..eb9e75054 100644 --- a/Emby.Server.Implementations/Localization/Core/es.json +++ b/Emby.Server.Implementations/Localization/Core/es.json @@ -1,4 +1,12 @@ { + "HeaderCameraUploads": "Camera Uploads", + "ValueHasBeenAddedToLibrary": "{0} has been added to your media library", + "NameInstallFailed": "{0} installation failed", + "CameraImageUploadedFrom": "A new camera image has been uploaded from {0}", + "ServerNameNeedsToBeRestarted": "{0} needs to be restarted", + "NewVersionIsAvailable": "A new version of Emby Server is available for download.", + "MessageApplicationUpdatedTo": "Emby Server has been updated to {0}", + "SubtitleDownloadFailureFromForItem": "Subtitles failed to download from {0} for {1}", "Latest": "\u00daltimos", "ValueSpecialEpisodeName": "Especial - {0}", "Inherit": "Heredar", @@ -24,6 +32,7 @@ "Channels": "Canales", "Movies": "Peliculas", "Albums": "\u00c1lbumes", + "NameSeasonUnknown": "Season Unknown", "Artists": "Artistas", "Folders": "Carpetas", "Songs": "Canciones", @@ -53,7 +62,7 @@ "UserCreatedWithName": "El usuario {0} ha sido creado", "UserPasswordChangedWithName": "Se ha cambiado la contrase\u00f1a para el usuario {0}", "UserDeletedWithName": "El usuario {0} ha sido borrado", - "UserConfigurationUpdatedWithName": "Configuraci\u00f3n de usuario se ha actualizado para {0}", + "UserPolicyUpdatedWithName": "User policy has been updated for {0}", "MessageServerConfigurationUpdated": "Se ha actualizado la configuraci\u00f3n del servidor", "MessageNamedServerConfigurationUpdatedWithValue": "La secci\u00f3n de configuraci\u00f3n del servidor {0} ha sido actualizado", "MessageApplicationUpdated": "Se ha actualizado el servidor Emby", diff --git a/Emby.Server.Implementations/Localization/Core/fa.json b/Emby.Server.Implementations/Localization/Core/fa.json new file mode 100644 index 000000000..3d494328a --- /dev/null +++ b/Emby.Server.Implementations/Localization/Core/fa.json @@ -0,0 +1,100 @@ +{ + "HeaderCameraUploads": "\u0622\u067e\u0644\u0648\u062f\u0647\u0627\u06cc \u062f\u0648\u0631\u0628\u06cc\u0646", + "ValueHasBeenAddedToLibrary": "{0} has been added to your media library", + "NameInstallFailed": "{0} installation failed", + "CameraImageUploadedFrom": "A new camera image has been uploaded from {0}", + "ServerNameNeedsToBeRestarted": "{0} needs to be restarted", + "NewVersionIsAvailable": "A new version of Emby Server is available for download.", + "MessageApplicationUpdatedTo": "Emby Server has been updated to {0}", + "SubtitleDownloadFailureFromForItem": "Subtitles failed to download from {0} for {1}", + "Latest": "\u0622\u062e\u0631\u06cc\u0646", + "ValueSpecialEpisodeName": "\u0648\u06cc\u0698\u0647- {0}", + "Inherit": "\u0628\u0647 \u0627\u0631\u062b \u0628\u0631\u062f\u0647", + "Books": "\u06a9\u062a\u0627\u0628 \u0647\u0627", + "Music": "\u0645\u0648\u0633\u06cc\u0642\u06cc", + "Games": "\u0628\u0627\u0632\u06cc \u0647\u0627", + "Photos": "\u0639\u06a9\u0633 \u0647\u0627", + "MixedContent": "\u0645\u062d\u062a\u0648\u0627\u06cc \u062f\u0631\u0647\u0645", + "MusicVideos": "\u0645\u0648\u0632\u06cc\u06a9 \u0648\u06cc\u062f\u06cc\u0648\u0647\u0627", + "HomeVideos": "\u0648\u06cc\u062f\u06cc\u0648\u0647\u0627\u06cc \u062e\u0627\u0646\u06af\u06cc", + "Playlists": "\u0644\u06cc\u0633\u062a \u0647\u0627\u06cc \u067e\u062e\u0634", + "HeaderRecordingGroups": "\u06af\u0631\u0648\u0647 \u0647\u0627\u06cc \u0636\u0628\u0637", + "HeaderContinueWatching": "\u0627\u062f\u0627\u0645\u0647 \u062a\u0645\u0627\u0634\u0627", + "HeaderFavoriteArtists": "\u0647\u0646\u0631\u0645\u0646\u062f\u0627\u0646 \u0645\u0648\u0631\u062f \u0639\u0644\u0627\u0642\u0647", + "HeaderFavoriteSongs": "\u0622\u0647\u0646\u06af \u0647\u0627\u06cc \u0645\u0648\u0631\u062f \u0639\u0644\u0627\u0642\u0647", + "HeaderAlbumArtists": "\u0647\u0646\u0631\u0645\u0646\u062f\u0627\u0646 \u0622\u0644\u0628\u0648\u0645", + "HeaderFavoriteAlbums": "\u0622\u0644\u0628\u0648\u0645 \u0647\u0627\u06cc \u0645\u0648\u0631\u062f \u0639\u0644\u0627\u0642\u0647", + "HeaderFavoriteEpisodes": "\u0642\u0633\u0645\u062a \u0647\u0627\u06cc \u0645\u0648\u0631\u062f \u0639\u0644\u0627\u0642\u0647", + "HeaderFavoriteShows": "\u0633\u0631\u06cc\u0627\u0644 \u0647\u0627\u06cc \u0645\u0648\u0631\u062f \u0639\u0644\u0627\u0642\u0647", + "HeaderNextUp": "\u0628\u0639\u062f\u06cc \u0686\u06cc\u0647", + "Favorites": "\u0645\u0648\u0631\u062f \u0639\u0644\u0627\u0642\u0647 \u0647\u0627", + "Collections": "\u06a9\u0644\u06a9\u0633\u06cc\u0648\u0646 \u0647\u0627", + "Channels": "\u06a9\u0627\u0646\u0627\u0644 \u0647\u0627", + "Movies": "\u0641\u06cc\u0644\u0645 \u0647\u0627\u06cc \u0633\u06cc\u0646\u0645\u0627\u06cc\u06cc", + "Albums": "\u0622\u0644\u0628\u0648\u0645 \u0647\u0627", + "NameSeasonUnknown": "\u0641\u0635\u0644 \u0647\u0627\u06cc \u0646\u0627\u0634\u0646\u0627\u062e\u062a\u0647", + "Artists": "\u0647\u0646\u0631\u0645\u0646\u062f\u0627\u0646", + "Folders": "\u067e\u0648\u0634\u0647 \u0647\u0627", + "Songs": "\u0622\u0647\u0646\u06af \u0647\u0627", + "TvShows": "\u0633\u0631\u06cc\u0627\u0644 \u0647\u0627\u06cc \u062a\u0644\u0648\u06cc\u0632\u06cc\u0648\u0646\u06cc", + "Shows": "\u0633\u0631\u06cc\u0627\u0644 \u0647\u0627", + "Genres": "\u0698\u0627\u0646\u0631\u0647\u0627", + "NameSeasonNumber": "\u0641\u0635\u0644 {0}", + "AppDeviceValues": "\u0628\u0631\u0646\u0627\u0645\u0647: {0} \u060c \u062f\u0633\u062a\u06af\u0627\u0647: {1}", + "UserDownloadingItemWithValues": "{0} \u062f\u0631 \u062d\u0627\u0644 \u062f\u0627\u0646\u0644\u0648\u062f \u0627\u0633\u062a {1}", + "HeaderLiveTV": "\u067e\u062e\u0634 \u0632\u0646\u062f\u0647 \u062a\u0644\u0648\u06cc\u0632\u06cc\u0648\u0646", + "ChapterNameValue": "\u0641\u0635\u0644 {0}", + "ScheduledTaskFailedWithName": "{0} \u0646\u0627\u0645\u0648\u0641\u0642 \u0628\u0648\u062f", + "LabelRunningTimeValue": "\u0632\u0645\u0627\u0646 \u0627\u062c\u0631\u0627: {0}", + "ScheduledTaskStartedWithName": "{0} \u0634\u0631\u0648\u0639 \u0634\u062f", + "VersionNumber": "\u0646\u0633\u062e\u0647 {0}", + "PluginInstalledWithName": "{0} \u0646\u0635\u0628 \u0634\u062f", + "StartupEmbyServerIsLoading": "\u0633\u0631\u0648\u0631 Emby \u062f\u0631 \u062d\u0627\u0644 \u0628\u0627\u0631\u06af\u06cc\u0631\u06cc \u0627\u0633\u062a. \u0644\u0637\u0641\u0627 \u06a9\u0645\u06cc \u0628\u0639\u062f \u062f\u0648\u0628\u0627\u0631\u0647 \u062a\u0644\u0627\u0634 \u06a9\u0646\u06cc\u062f.", + "PluginUpdatedWithName": "{0} \u0622\u067e\u062f\u06cc\u062a \u0634\u062f", + "PluginUninstalledWithName": "{0} \u062d\u0630\u0641 \u0634\u062f", + "ItemAddedWithName": "{0} \u0628\u0647 \u06a9\u062a\u0627\u0628\u062e\u0627\u0646\u0647 \u0627\u0641\u0632\u0648\u062f\u0647 \u0634\u062f", + "ItemRemovedWithName": "{0} \u0627\u0632 \u06a9\u062a\u0627\u0628\u062e\u0627\u0646\u0647 \u062d\u0630\u0641 \u0634\u062f", + "LabelIpAddressValue": "\u0622\u062f\u0631\u0633 \u0622\u06cc \u067e\u06cc: {0}", + "DeviceOnlineWithName": "{0} \u0645\u062a\u0635\u0644 \u0634\u062f\u0647", + "UserOnlineFromDevice": "{0}\u0627\u0632 {1} \u0622\u0646\u0644\u0627\u06cc\u0646 \u0645\u06cc\u0628\u0627\u0634\u062f", + "ProviderValue": "\u0627\u0631\u0627\u0626\u0647 \u062f\u0647\u0646\u062f\u0647: {0}", + "SubtitlesDownloadedForItem": "\u0632\u06cc\u0631\u0646\u0648\u06cc\u0633 {0} \u062f\u0627\u0646\u0644\u0648\u062f \u0634\u062f", + "UserCreatedWithName": "\u06a9\u0627\u0631\u0628\u0631 {0} \u0627\u06cc\u062c\u0627\u062f \u0634\u062f", + "UserPasswordChangedWithName": "\u0631\u0645\u0632 \u0628\u0631\u0627\u06cc \u06a9\u0627\u0631\u0628\u0631 {0} \u062a\u063a\u06cc\u06cc\u0631 \u06cc\u0627\u0641\u062a", + "UserDeletedWithName": "\u06a9\u0627\u0631\u0628\u0631 {0} \u062d\u0630\u0641 \u0634\u062f", + "UserPolicyUpdatedWithName": "\u0633\u06cc\u0627\u0633\u062a \u06a9\u0627\u0631\u0628\u0631\u06cc \u0628\u0631\u0627\u06cc {0} \u0628\u0631\u0648\u0632\u0631\u0633\u0627\u0646\u06cc \u0634\u062f", + "MessageServerConfigurationUpdated": "\u067e\u06cc\u06a9\u0631\u0628\u0646\u062f\u06cc \u0633\u0631\u0648\u0631 \u0628\u0631\u0648\u0632\u0631\u0633\u0627\u0646\u06cc \u0634\u062f", + "MessageNamedServerConfigurationUpdatedWithValue": "\u067e\u06a9\u0631\u0628\u0646\u062f\u06cc \u0628\u062e\u0634 {0} \u0633\u0631\u0648\u0631 \u0628\u0631\u0648\u0632\u0631\u0633\u0627\u0646\u06cc \u0634\u062f", + "MessageApplicationUpdated": "\u0633\u0631\u0648\u0631 Emby \u0628\u0631\u0648\u0632\u0631\u0633\u0627\u0646\u06cc \u0634\u062f", + "FailedLoginAttemptWithUserName": "\u062a\u0644\u0627\u0634 \u0628\u0631\u0627\u06cc \u0648\u0631\u0648\u062f \u0627\u0632 {0} \u0646\u0627\u0645\u0648\u0641\u0642 \u0628\u0648\u062f", + "AuthenticationSucceededWithUserName": "{0} \u0628\u0627 \u0645\u0648\u0641\u0642\u06cc\u062a \u062a\u0627\u06cc\u06cc\u062f \u0627\u0639\u062a\u0628\u0627\u0631 \u0634\u062f", + "UserOfflineFromDevice": "\u0627\u0631\u062a\u0628\u0627\u0637 {0} \u0627\u0632 {1} \u0642\u0637\u0639 \u0634\u062f", + "DeviceOfflineWithName": "\u0627\u0631\u062a\u0628\u0627\u0637 {0} \u0642\u0637\u0639 \u0634\u062f", + "UserStartedPlayingItemWithValues": "{0} \u0634\u0631\u0648\u0639 \u0628\u0647 \u067e\u062e\u0634 {1} \u06a9\u0631\u062f", + "UserStoppedPlayingItemWithValues": "{0} \u067e\u062e\u0634 {1} \u0631\u0627 \u0645\u062a\u0648\u0642\u0641 \u06a9\u0631\u062f", + "NotificationOptionPluginError": "\u062e\u0631\u0627\u0628\u06cc \u0627\u0641\u0632\u0648\u0646\u0647", + "NotificationOptionApplicationUpdateAvailable": "\u0628\u0631\u0648\u0632\u0631\u0633\u0627\u0646\u06cc \u0628\u0631\u0646\u0627\u0645\u0647 \u0645\u0648\u062c\u0648\u062f \u0627\u0633\u062a", + "NotificationOptionApplicationUpdateInstalled": "\u0628\u0631\u0648\u0632\u0631\u0633\u0627\u0646\u06cc \u0628\u0631\u0646\u0627\u0645\u0647 \u0646\u0635\u0628 \u0634\u062f", + "NotificationOptionPluginUpdateInstalled": "\u0628\u0631\u0648\u0632\u0631\u0633\u0627\u0646\u06cc \u0627\u0641\u0632\u0648\u0646\u0647 \u0646\u0635\u0628 \u0634\u062f", + "NotificationOptionPluginInstalled": "\u0627\u0641\u0632\u0648\u0646\u0647 \u0646\u0635\u0628 \u0634\u062f", + "NotificationOptionPluginUninstalled": "\u0627\u0641\u0632\u0648\u0646\u0647 \u062d\u0630\u0641 \u0634\u062f", + "NotificationOptionVideoPlayback": "\u067e\u062e\u0634 \u0648\u06cc\u062f\u06cc\u0648 \u0622\u063a\u0627\u0632 \u0634\u062f", + "NotificationOptionAudioPlayback": "\u067e\u062e\u0634 \u0635\u062f\u0627 \u0622\u063a\u0627\u0632 \u0634\u062f", + "NotificationOptionGamePlayback": "\u067e\u062e\u0634 \u0628\u0627\u0632\u06cc \u0622\u063a\u0627\u0632 \u0634\u062f", + "NotificationOptionVideoPlaybackStopped": "\u067e\u062e\u0634 \u0648\u06cc\u062f\u06cc\u0648 \u0645\u062a\u0648\u0642\u0641 \u0634\u062f", + "NotificationOptionAudioPlaybackStopped": "\u067e\u062e\u0634 \u0635\u062f\u0627 \u0645\u062a\u0648\u0642\u0641 \u0634\u062f", + "NotificationOptionGamePlaybackStopped": "\u067e\u062e\u0634 \u0628\u0627\u0632\u06cc \u0645\u062a\u0648\u0642\u0641 \u0634\u062f", + "NotificationOptionTaskFailed": "\u0634\u06a9\u0633\u062a \u0648\u0638\u06cc\u0641\u0647 \u0628\u0631\u0646\u0627\u0645\u0647 \u0631\u06cc\u0632\u06cc \u0634\u062f\u0647", + "NotificationOptionInstallationFailed": "\u0634\u06a9\u0633\u062a \u0646\u0635\u0628", + "NotificationOptionNewLibraryContent": "\u0645\u062d\u062a\u0648\u0627\u06cc \u062c\u062f\u06cc\u062f \u0627\u0641\u0632\u0648\u062f\u0647 \u0634\u062f", + "NotificationOptionCameraImageUploaded": "\u062a\u0635\u0627\u0648\u06cc\u0631 \u062f\u0648\u0631\u0628\u06cc\u0646 \u0622\u067e\u0644\u0648\u062f \u0634\u062f", + "NotificationOptionUserLockedOut": "\u06a9\u0627\u0631\u0628\u0631 \u0627\u0632 \u0633\u06cc\u0633\u062a\u0645 \u062e\u0627\u0631\u062c \u0634\u062f", + "NotificationOptionServerRestartRequired": "\u0634\u0631\u0648\u0639 \u0645\u062c\u062f\u062f \u0633\u0631\u0648\u0631 \u0646\u06cc\u0627\u0632 \u0627\u0633\u062a", + "UserLockedOutWithName": "\u06a9\u0627\u0631\u0628\u0631 {0} \u0627\u0632 \u0633\u06cc\u0633\u062a\u0645 \u062e\u0627\u0631\u062c \u0634\u062f", + "SubtitleDownloadFailureForItem": "\u062f\u0627\u0646\u0644\u0648\u062f \u0632\u06cc\u0631\u0646\u0648\u06cc\u0633 \u0628\u0631\u0627\u06cc {0} \u0646\u0627\u0645\u0648\u0641\u0642 \u0628\u0648\u062f", + "Sync": "\u0647\u0645\u06af\u0627\u0645\u0633\u0627\u0632\u06cc", + "User": "\u06a9\u0627\u0631\u0628\u0631", + "System": "\u0633\u06cc\u0633\u062a\u0645", + "Application": "\u0628\u0631\u0646\u0627\u0645\u0647", + "Plugin": "\u0627\u0641\u0632\u0648\u0646\u0647" +}
\ No newline at end of file diff --git a/Emby.Server.Implementations/Localization/Core/fr-CA.json b/Emby.Server.Implementations/Localization/Core/fr-CA.json index 7743905f0..1acee0ae9 100644 --- a/Emby.Server.Implementations/Localization/Core/fr-CA.json +++ b/Emby.Server.Implementations/Localization/Core/fr-CA.json @@ -1,4 +1,12 @@ { + "HeaderCameraUploads": "Camera Uploads", + "ValueHasBeenAddedToLibrary": "{0} has been added to your media library", + "NameInstallFailed": "{0} installation failed", + "CameraImageUploadedFrom": "A new camera image has been uploaded from {0}", + "ServerNameNeedsToBeRestarted": "{0} needs to be restarted", + "NewVersionIsAvailable": "A new version of Emby Server is available for download.", + "MessageApplicationUpdatedTo": "Emby Server has been updated to {0}", + "SubtitleDownloadFailureFromForItem": "Subtitles failed to download from {0} for {1}", "Latest": "Latest", "ValueSpecialEpisodeName": "Sp\u00e9cial - {0}", "Inherit": "Inherit", @@ -24,6 +32,7 @@ "Channels": "Channels", "Movies": "Movies", "Albums": "Albums", + "NameSeasonUnknown": "Season Unknown", "Artists": "Artists", "Folders": "Folders", "Songs": "Songs", @@ -53,7 +62,7 @@ "UserCreatedWithName": "User {0} has been created", "UserPasswordChangedWithName": "Password has been changed for user {0}", "UserDeletedWithName": "User {0} has been deleted", - "UserConfigurationUpdatedWithName": "User configuration has been updated for {0}", + "UserPolicyUpdatedWithName": "User policy has been updated for {0}", "MessageServerConfigurationUpdated": "Server configuration has been updated", "MessageNamedServerConfigurationUpdatedWithValue": "Server configuration section {0} has been updated", "MessageApplicationUpdated": "Emby Server has been updated", @@ -61,8 +70,8 @@ "AuthenticationSucceededWithUserName": "{0} successfully authenticated", "UserOfflineFromDevice": "{0} has disconnected from {1}", "DeviceOfflineWithName": "{0} has disconnected", - "UserStartedPlayingItemWithValues": "{0} has started playing {1}", - "UserStoppedPlayingItemWithValues": "{0} has stopped playing {1}", + "UserStartedPlayingItemWithValues": "{0} is playing {1} on {2}", + "UserStoppedPlayingItemWithValues": "{0} has finished playing {1} on {2}", "NotificationOptionPluginError": "Plugin failure", "NotificationOptionApplicationUpdateAvailable": "Application update available", "NotificationOptionApplicationUpdateInstalled": "Application update installed", diff --git a/Emby.Server.Implementations/Localization/Core/fr.json b/Emby.Server.Implementations/Localization/Core/fr.json index 6552a47ab..30fe22ac2 100644 --- a/Emby.Server.Implementations/Localization/Core/fr.json +++ b/Emby.Server.Implementations/Localization/Core/fr.json @@ -1,5 +1,13 @@ { - "Latest": "R\u00e9cent", + "HeaderCameraUploads": "Photos transf\u00e9r\u00e9es", + "ValueHasBeenAddedToLibrary": "{0} a \u00e9t\u00e9 ajout\u00e9 \u00e0 votre librairie", + "NameInstallFailed": "{0} \u00e9chec d'installation", + "CameraImageUploadedFrom": "Une image de cam\u00e9ra a \u00e9t\u00e9 charg\u00e9e depuis {0}", + "ServerNameNeedsToBeRestarted": "{0} doit \u00eatre red\u00e9marr\u00e9", + "NewVersionIsAvailable": "Une nouvelle version d'Emby Serveur est disponible au t\u00e9l\u00e9chargement.", + "MessageApplicationUpdatedTo": "Emby Serveur a \u00e9t\u00e9 mis \u00e0 jour en version {0}", + "SubtitleDownloadFailureFromForItem": "\u00c9chec du t\u00e9l\u00e9chargement des sous-titres depuis {0} pour {1}", + "Latest": "Derniers", "ValueSpecialEpisodeName": "Sp\u00e9cial - {0}", "Inherit": "H\u00e9riter", "Books": "Livres", @@ -24,6 +32,7 @@ "Channels": "Cha\u00eenes", "Movies": "Films", "Albums": "Albums", + "NameSeasonUnknown": "Saison Inconnue", "Artists": "Artistes", "Folders": "Dossiers", "Songs": "Chansons", @@ -53,7 +62,7 @@ "UserCreatedWithName": "L'utilisateur {0} a \u00e9t\u00e9 cr\u00e9\u00e9", "UserPasswordChangedWithName": "Le mot de passe pour l'utilisateur {0} a \u00e9t\u00e9 modifi\u00e9", "UserDeletedWithName": "L'utilisateur {0} a \u00e9t\u00e9 supprim\u00e9", - "UserConfigurationUpdatedWithName": "La configuration utilisateur de {0} a \u00e9t\u00e9 mise \u00e0 jour", + "UserPolicyUpdatedWithName": "La politique de l'utilisateur a \u00e9t\u00e9 mise \u00e0 jour pour {0}", "MessageServerConfigurationUpdated": "La configuration du serveur a \u00e9t\u00e9 mise \u00e0 jour.", "MessageNamedServerConfigurationUpdatedWithValue": "La configuration de la section {0} du serveur a \u00e9t\u00e9 mise \u00e0 jour", "MessageApplicationUpdated": "Le serveur Emby a \u00e9t\u00e9 mis \u00e0 jour", @@ -61,8 +70,8 @@ "AuthenticationSucceededWithUserName": "{0} s'est authentifi\u00e9 avec succ\u00e8s", "UserOfflineFromDevice": "{0} s'est d\u00e9connect\u00e9 depuis {1}", "DeviceOfflineWithName": "{0} s'est d\u00e9connect\u00e9", - "UserStartedPlayingItemWithValues": "{0} vient de commencer la lecture de {1}", - "UserStoppedPlayingItemWithValues": "{0} vient d'arr\u00eater la lecture de {1}", + "UserStartedPlayingItemWithValues": "{0} est entrain de lire {1} sur {2}", + "UserStoppedPlayingItemWithValues": "{0} vient d'arr\u00eater la lecture de {1} sur {2}", "NotificationOptionPluginError": "Erreur d'extension", "NotificationOptionApplicationUpdateAvailable": "Mise \u00e0 jour de l'application disponible", "NotificationOptionApplicationUpdateInstalled": "Mise \u00e0 jour de l'application install\u00e9e", diff --git a/Emby.Server.Implementations/Localization/Core/gsw.json b/Emby.Server.Implementations/Localization/Core/gsw.json index 5c7ff5d6d..4f51a5ef4 100644 --- a/Emby.Server.Implementations/Localization/Core/gsw.json +++ b/Emby.Server.Implementations/Localization/Core/gsw.json @@ -1,4 +1,12 @@ { + "HeaderCameraUploads": "Camera Uploads", + "ValueHasBeenAddedToLibrary": "{0} has been added to your media library", + "NameInstallFailed": "{0} installation failed", + "CameraImageUploadedFrom": "A new camera image has been uploaded from {0}", + "ServerNameNeedsToBeRestarted": "{0} needs to be restarted", + "NewVersionIsAvailable": "A new version of Emby Server is available for download.", + "MessageApplicationUpdatedTo": "Emby Server has been updated to {0}", + "SubtitleDownloadFailureFromForItem": "Subtitles failed to download from {0} for {1}", "Latest": "Letschte", "ValueSpecialEpisodeName": "Spezial - {0}", "Inherit": "Hinzuef\u00fcege", @@ -24,6 +32,7 @@ "Channels": "Channels", "Movies": "Movies", "Albums": "Albums", + "NameSeasonUnknown": "Season Unknown", "Artists": "Artists", "Folders": "Folders", "Songs": "Songs", @@ -53,7 +62,7 @@ "UserCreatedWithName": "User {0} has been created", "UserPasswordChangedWithName": "Password has been changed for user {0}", "UserDeletedWithName": "User {0} has been deleted", - "UserConfigurationUpdatedWithName": "User configuration has been updated for {0}", + "UserPolicyUpdatedWithName": "User policy has been updated for {0}", "MessageServerConfigurationUpdated": "Server configuration has been updated", "MessageNamedServerConfigurationUpdatedWithValue": "Server configuration section {0} has been updated", "MessageApplicationUpdated": "Emby Server has been updated", @@ -61,8 +70,8 @@ "AuthenticationSucceededWithUserName": "{0} successfully authenticated", "UserOfflineFromDevice": "{0} has disconnected from {1}", "DeviceOfflineWithName": "{0} has disconnected", - "UserStartedPlayingItemWithValues": "{0} has started playing {1}", - "UserStoppedPlayingItemWithValues": "{0} has stopped playing {1}", + "UserStartedPlayingItemWithValues": "{0} is playing {1} on {2}", + "UserStoppedPlayingItemWithValues": "{0} has finished playing {1} on {2}", "NotificationOptionPluginError": "Plugin failure", "NotificationOptionApplicationUpdateAvailable": "Application update available", "NotificationOptionApplicationUpdateInstalled": "Application update installed", diff --git a/Emby.Server.Implementations/Localization/Core/he.json b/Emby.Server.Implementations/Localization/Core/he.json index c679ed289..c3d1ee7ae 100644 --- a/Emby.Server.Implementations/Localization/Core/he.json +++ b/Emby.Server.Implementations/Localization/Core/he.json @@ -1,17 +1,25 @@ { - "Latest": "Latest", + "HeaderCameraUploads": "Camera Uploads", + "ValueHasBeenAddedToLibrary": "{0} has been added to your media library", + "NameInstallFailed": "{0} installation failed", + "CameraImageUploadedFrom": "A new camera image has been uploaded from {0}", + "ServerNameNeedsToBeRestarted": "{0} needs to be restarted", + "NewVersionIsAvailable": "A new version of Emby Server is available for download.", + "MessageApplicationUpdatedTo": "Emby Server has been updated to {0}", + "SubtitleDownloadFailureFromForItem": "Subtitles failed to download from {0} for {1}", + "Latest": "\u05d0\u05d7\u05e8\u05d5\u05df", "ValueSpecialEpisodeName": "\u05de\u05d9\u05d5\u05d7\u05d3- {0}", "Inherit": "Inherit", - "Books": "Books", - "Music": "Music", - "Games": "Games", - "Photos": "Photos", - "MixedContent": "Mixed content", + "Books": "\u05e1\u05e4\u05e8\u05d9\u05dd", + "Music": "\u05de\u05d5\u05d6\u05d9\u05e7\u05d4", + "Games": "\u05de\u05e9\u05d7\u05e7\u05d9\u05dd", + "Photos": "\u05ea\u05de\u05d5\u05e0\u05d5\u05ea", + "MixedContent": "\u05ea\u05d5\u05db\u05df \u05de\u05e2\u05d5\u05e8\u05d1", "MusicVideos": "Music videos", "HomeVideos": "Home videos", - "Playlists": "Playlists", - "HeaderRecordingGroups": "Recording Groups", - "HeaderContinueWatching": "Continue Watching", + "Playlists": "\u05e8\u05e9\u05d9\u05de\u05d5\u05ea \u05e0\u05d9\u05d2\u05d5\u05df", + "HeaderRecordingGroups": "\u05e7\u05d1\u05d5\u05e6\u05d5\u05ea \u05d4\u05e7\u05dc\u05d8\u05d4", + "HeaderContinueWatching": "\u05d4\u05de\u05e9\u05da \u05d1\u05e6\u05e4\u05d9\u05d9\u05d4", "HeaderFavoriteArtists": "Favorite Artists", "HeaderFavoriteSongs": "Favorite Songs", "HeaderAlbumArtists": "Album Artists", @@ -24,6 +32,7 @@ "Channels": "Channels", "Movies": "\u05e1\u05e8\u05d8\u05d9\u05dd", "Albums": "Albums", + "NameSeasonUnknown": "Season Unknown", "Artists": "Artists", "Folders": "Folders", "Songs": "Songs", @@ -53,7 +62,7 @@ "UserCreatedWithName": "User {0} has been created", "UserPasswordChangedWithName": "Password has been changed for user {0}", "UserDeletedWithName": "User {0} has been deleted", - "UserConfigurationUpdatedWithName": "User configuration has been updated for {0}", + "UserPolicyUpdatedWithName": "User policy has been updated for {0}", "MessageServerConfigurationUpdated": "Server configuration has been updated", "MessageNamedServerConfigurationUpdatedWithValue": "Server configuration section {0} has been updated", "MessageApplicationUpdated": "Emby Server has been updated", @@ -61,8 +70,8 @@ "AuthenticationSucceededWithUserName": "{0} successfully authenticated", "UserOfflineFromDevice": "{0} has disconnected from {1}", "DeviceOfflineWithName": "{0} has disconnected", - "UserStartedPlayingItemWithValues": "{0} has started playing {1}", - "UserStoppedPlayingItemWithValues": "{0} has stopped playing {1}", + "UserStartedPlayingItemWithValues": "{0} is playing {1} on {2}", + "UserStoppedPlayingItemWithValues": "{0} has finished playing {1} on {2}", "NotificationOptionPluginError": "Plugin failure", "NotificationOptionApplicationUpdateAvailable": "Application update available", "NotificationOptionApplicationUpdateInstalled": "Application update installed", diff --git a/Emby.Server.Implementations/Localization/Core/hr.json b/Emby.Server.Implementations/Localization/Core/hr.json index c807e53b4..47c41d97c 100644 --- a/Emby.Server.Implementations/Localization/Core/hr.json +++ b/Emby.Server.Implementations/Localization/Core/hr.json @@ -1,4 +1,12 @@ { + "HeaderCameraUploads": "Camera Uploads", + "ValueHasBeenAddedToLibrary": "{0} has been added to your media library", + "NameInstallFailed": "{0} installation failed", + "CameraImageUploadedFrom": "A new camera image has been uploaded from {0}", + "ServerNameNeedsToBeRestarted": "{0} needs to be restarted", + "NewVersionIsAvailable": "A new version of Emby Server is available for download.", + "MessageApplicationUpdatedTo": "Emby Server has been updated to {0}", + "SubtitleDownloadFailureFromForItem": "Subtitles failed to download from {0} for {1}", "Latest": "Najnovije", "ValueSpecialEpisodeName": "Specijal - {0}", "Inherit": "Naslijedi", @@ -24,6 +32,7 @@ "Channels": "Kanali", "Movies": "Filmovi", "Albums": "Albumi", + "NameSeasonUnknown": "Season Unknown", "Artists": "Izvo\u0111a\u010di", "Folders": "Mape", "Songs": "Pjesme", @@ -53,7 +62,7 @@ "UserCreatedWithName": "Korisnik {0} je stvoren", "UserPasswordChangedWithName": "Lozinka je promijenjena za korisnika {0}", "UserDeletedWithName": "Korisnik {0} je obrisan", - "UserConfigurationUpdatedWithName": "Postavke korisnika su a\u017eurirane za {0}", + "UserPolicyUpdatedWithName": "User policy has been updated for {0}", "MessageServerConfigurationUpdated": "Postavke servera su a\u017eurirane", "MessageNamedServerConfigurationUpdatedWithValue": "Odjeljak postavka servera {0} je a\u017euriran", "MessageApplicationUpdated": "Emby Server je a\u017euriran", diff --git a/Emby.Server.Implementations/Localization/Core/hu.json b/Emby.Server.Implementations/Localization/Core/hu.json index dc8f2b702..5d90d03f2 100644 --- a/Emby.Server.Implementations/Localization/Core/hu.json +++ b/Emby.Server.Implementations/Localization/Core/hu.json @@ -1,4 +1,12 @@ { + "HeaderCameraUploads": "Camera Uploads", + "ValueHasBeenAddedToLibrary": "{0} has been added to your media library", + "NameInstallFailed": "{0} installation failed", + "CameraImageUploadedFrom": "A new camera image has been uploaded from {0}", + "ServerNameNeedsToBeRestarted": "{0} needs to be restarted", + "NewVersionIsAvailable": "A new version of Emby Server is available for download.", + "MessageApplicationUpdatedTo": "Emby Server has been updated to {0}", + "SubtitleDownloadFailureFromForItem": "Subtitles failed to download from {0} for {1}", "Latest": "Leg\u00fajabb", "ValueSpecialEpisodeName": "Special - {0}", "Inherit": "Inherit", @@ -24,11 +32,12 @@ "Channels": "Csatorn\u00e1k", "Movies": "Filmek", "Albums": "Albumok", + "NameSeasonUnknown": "Season Unknown", "Artists": "El\u0151ad\u00f3k", "Folders": "K\u00f6nyvt\u00e1rak", "Songs": "Dalok", - "TvShows": "TV Shows", - "Shows": "Shows", + "TvShows": "TV M\u0171sorok", + "Shows": "M\u0171sorok", "Genres": "M\u0171fajok", "NameSeasonNumber": "Season {0}", "AppDeviceValues": "Program: {0}, Eszk\u00f6z: {1}", @@ -53,18 +62,18 @@ "UserCreatedWithName": "User {0} has been created", "UserPasswordChangedWithName": "Password has been changed for user {0}", "UserDeletedWithName": "User {0} has been deleted", - "UserConfigurationUpdatedWithName": "User configuration has been updated for {0}", - "MessageServerConfigurationUpdated": "Szerver konfigur\u00e1ci\u00f3 friss\u00fclt", + "UserPolicyUpdatedWithName": "User policy has been updated for {0}", + "MessageServerConfigurationUpdated": "Szerver konfigur\u00e1ci\u00f3 friss\u00edtve", "MessageNamedServerConfigurationUpdatedWithValue": "Szerver konfigur\u00e1ci\u00f3s r\u00e9sz {0} friss\u00edtve", "MessageApplicationUpdated": "Emby Szerver friss\u00edtve", "FailedLoginAttemptWithUserName": "Failed login attempt from {0}", - "AuthenticationSucceededWithUserName": "{0} successfully authenticated", + "AuthenticationSucceededWithUserName": "{0} sikeresen azonos\u00edtva", "UserOfflineFromDevice": "{0} kijelentkezett innen {1}", "DeviceOfflineWithName": "{0} kijelentkezett", - "UserStartedPlayingItemWithValues": "{0} has started playing {1}", - "UserStoppedPlayingItemWithValues": "{0} has stopped playing {1}", + "UserStartedPlayingItemWithValues": "{0} elkezdte j\u00e1tszani a k\u00f6vetkez\u0151t {1}", + "UserStoppedPlayingItemWithValues": "{0} befejezte a k\u00f6vetkez\u0151t {1}", "NotificationOptionPluginError": "B\u0151v\u00edtm\u00e9ny hiba", - "NotificationOptionApplicationUpdateAvailable": "Friss\u00edt\u00e9s el\u00e9rhet\u0151", + "NotificationOptionApplicationUpdateAvailable": "Program friss\u00edt\u00e9s el\u00e9rhet\u0151", "NotificationOptionApplicationUpdateInstalled": "Program friss\u00edt\u00e9s telep\u00edtve", "NotificationOptionPluginUpdateInstalled": "B\u0151v\u00edtm\u00e9ny friss\u00edt\u00e9s telep\u00edtve", "NotificationOptionPluginInstalled": "B\u0151v\u00edtm\u00e9ny telep\u00edtve", @@ -84,8 +93,8 @@ "UserLockedOutWithName": "User {0} has been locked out", "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}", "Sync": "Szinkroniz\u00e1l", - "User": "User", - "System": "System", - "Application": "Application", - "Plugin": "Plugin" + "User": "Felhaszn\u00e1l\u00f3", + "System": "Rendszer", + "Application": "Program", + "Plugin": "B\u0151v\u00edtm\u00e9ny" }
\ No newline at end of file diff --git a/Emby.Server.Implementations/Localization/Core/it.json b/Emby.Server.Implementations/Localization/Core/it.json index 42605acdb..f1ce264cf 100644 --- a/Emby.Server.Implementations/Localization/Core/it.json +++ b/Emby.Server.Implementations/Localization/Core/it.json @@ -1,4 +1,12 @@ { + "HeaderCameraUploads": "Caricamenti Fotocamera", + "ValueHasBeenAddedToLibrary": "{0} has been added to your media library", + "NameInstallFailed": "{0} installation failed", + "CameraImageUploadedFrom": "A new camera image has been uploaded from {0}", + "ServerNameNeedsToBeRestarted": "{0} needs to be restarted", + "NewVersionIsAvailable": "A new version of Emby Server is available for download.", + "MessageApplicationUpdatedTo": "Emby Server has been updated to {0}", + "SubtitleDownloadFailureFromForItem": "Subtitles failed to download from {0} for {1}", "Latest": "Pi\u00f9 recenti", "ValueSpecialEpisodeName": "Speciale - {0}", "Inherit": "Eredita", @@ -24,10 +32,11 @@ "Channels": "Canali", "Movies": "Film", "Albums": "Album", + "NameSeasonUnknown": "Stagione sconosciuto", "Artists": "Artisti", "Folders": "Cartelle", "Songs": "Canzoni", - "TvShows": "TV Shows", + "TvShows": "Serie TV", "Shows": "Programmi", "Genres": "Generi", "NameSeasonNumber": "Stagione {0}", @@ -53,7 +62,7 @@ "UserCreatedWithName": "L'utente {0} \u00e8 stato creato", "UserPasswordChangedWithName": "La password \u00e8 stata cambiata per l'utente {0}", "UserDeletedWithName": "L'utente {0} \u00e8 stato rimosso", - "UserConfigurationUpdatedWithName": "La configurazione utente \u00e8 stata aggiornata per {0}", + "UserPolicyUpdatedWithName": "La politica dell'utente \u00e8 stata aggiornata per {0}", "MessageServerConfigurationUpdated": "La configurazione del server \u00e8 stata aggiornata", "MessageNamedServerConfigurationUpdatedWithValue": "La sezione {0} della configurazione server \u00e8 stata aggiornata", "MessageApplicationUpdated": "Il Server Emby \u00e8 stato aggiornato", diff --git a/Emby.Server.Implementations/Localization/Core/kk.json b/Emby.Server.Implementations/Localization/Core/kk.json index a991fe363..ef487aa8c 100644 --- a/Emby.Server.Implementations/Localization/Core/kk.json +++ b/Emby.Server.Implementations/Localization/Core/kk.json @@ -1,4 +1,12 @@ { + "HeaderCameraUploads": "\u041a\u0430\u043c\u0435\u0440\u0430\u0434\u0430\u043d \u0436\u04af\u043a\u0442\u0435\u043b\u0433\u0435\u043d\u0434\u0435\u0440", + "ValueHasBeenAddedToLibrary": "{0} (\u0442\u0430\u0441\u044b\u0493\u044b\u0448\u0445\u0430\u043d\u0430\u0493\u0430 \u04af\u0441\u0442\u0435\u043b\u0456\u043d\u0434\u0456)", + "NameInstallFailed": "{0} \u043e\u0440\u043d\u0430\u0442\u044b\u043b\u0443\u044b \u0441\u04d9\u0442\u0441\u0456\u0437", + "CameraImageUploadedFrom": "\u0416\u0430\u04a3\u0430 \u0441\u0443\u0440\u0435\u0442 {0} \u043a\u0430\u043c\u0435\u0440\u0430\u0441\u044b\u043d\u0430\u043d \u0436\u04af\u043a\u0442\u0435\u043f \u0430\u043b\u044b\u043d\u0434\u044b", + "ServerNameNeedsToBeRestarted": "{0} \u049b\u0430\u0439\u0442\u0430 \u0456\u0441\u043a\u0435 \u049b\u043e\u0441\u0443 \u049b\u0430\u0436\u0435\u0442", + "NewVersionIsAvailable": "\u0416\u0430\u04a3\u0430 Emby Server \u043d\u04b1\u0441\u049b\u0430\u0441\u044b \u0436\u04af\u043a\u0442\u0435\u043f \u0430\u043b\u0443\u0493\u0430 \u049b\u043e\u043b\u0436\u0435\u0442\u0456\u043c\u0434\u0456.", + "MessageApplicationUpdatedTo": "Emby Server {0} \u04af\u0448\u0456\u043d \u0436\u0430\u04a3\u0430\u0440\u0442\u044b\u043b\u0434\u044b", + "SubtitleDownloadFailureFromForItem": "Subtitles failed to download from {0} for {1}", "Latest": "\u0415\u04a3 \u043a\u0435\u0439\u0456\u043d\u0433\u0456", "ValueSpecialEpisodeName": "\u0410\u0440\u043d\u0430\u0439\u044b - {0}", "Inherit": "\u041c\u04b1\u0440\u0430\u0493\u0430 \u0438\u0435\u043b\u0435\u043d\u0443", @@ -24,6 +32,7 @@ "Channels": "\u0410\u0440\u043d\u0430\u043b\u0430\u0440", "Movies": "\u0424\u0438\u043b\u044c\u043c\u0434\u0435\u0440", "Albums": "\u0410\u043b\u044c\u0431\u043e\u043c\u0434\u0430\u0440", + "NameSeasonUnknown": "\u0411\u0435\u043b\u0433\u0456\u0441\u0456\u0437 \u043c\u0430\u0443\u0441\u044b\u043c", "Artists": "\u041e\u0440\u044b\u043d\u0434\u0430\u0443\u0448\u044b\u043b\u0430\u0440", "Folders": "\u049a\u0430\u043b\u0442\u0430\u043b\u0430\u0440", "Songs": "\u04d8\u0443\u0435\u043d\u0434\u0435\u0440", @@ -53,7 +62,7 @@ "UserCreatedWithName": "\u041f\u0430\u0439\u0434\u0430\u043b\u0430\u043d\u0443\u0448\u044b {0} \u0436\u0430\u0441\u0430\u043b\u0493\u0430\u043d", "UserPasswordChangedWithName": "\u041f\u0430\u0439\u0434\u0430\u043b\u0430\u043d\u0443\u0448\u044b {0} \u04af\u0448\u0456\u043d \u049b\u04b1\u043f\u0438\u044f \u0441\u04e9\u0437 \u04e9\u0437\u0433\u0435\u0440\u0442\u0456\u043b\u0434\u0456", "UserDeletedWithName": "\u041f\u0430\u0439\u0434\u0430\u043b\u0430\u043d\u0443\u0448\u044b {0} \u0436\u043e\u0439\u044b\u043b\u0493\u0430\u043d", - "UserConfigurationUpdatedWithName": "\u041f\u0430\u0439\u0434\u0430\u043b\u0430\u043d\u0443\u0448\u044b {0} \u04af\u0448\u0456\u043d \u0442\u0435\u04a3\u0448\u0435\u043b\u0456\u043c \u0436\u0430\u04a3\u0430\u0440\u0442\u044b\u043b\u0434\u044b", + "UserPolicyUpdatedWithName": "\u041f\u0430\u0439\u0434\u0430\u043b\u0430\u043d\u0443\u0448\u044b {0} \u04af\u0448\u0456\u043d \u0441\u0430\u044f\u0441\u0430\u0442\u0442\u0430\u0440\u044b \u0436\u0430\u04a3\u0430\u0440\u0442\u044b\u043b\u0434\u044b", "MessageServerConfigurationUpdated": "\u0421\u0435\u0440\u0432\u0435\u0440 \u0442\u0435\u04a3\u0448\u0435\u043b\u0456\u043c\u0456 \u0436\u0430\u04a3\u0430\u0440\u0442\u044b\u043b\u0434\u044b", "MessageNamedServerConfigurationUpdatedWithValue": "\u0421\u0435\u0440\u0432\u0435\u0440 \u0442\u0435\u04a3\u0448\u0435\u043b\u0456\u043c\u0456 ({0} \u0431\u04e9\u043b\u0456\u043c\u0456) \u0436\u0430\u04a3\u0430\u0440\u0442\u044b\u043b\u0434\u044b", "MessageApplicationUpdated": "Emby Server \u0436\u0430\u04a3\u0430\u0440\u0442\u044b\u043b\u0434\u044b.", @@ -61,8 +70,8 @@ "AuthenticationSucceededWithUserName": "{0} \u0442\u04af\u043f\u043d\u04b1\u0441\u049b\u0430\u043b\u044b\u0493\u044b\u043d \u0440\u0430\u0441\u0442\u0430\u043b\u0443\u044b \u0441\u04d9\u0442\u0442\u0456", "UserOfflineFromDevice": "{0} - {1} \u0442\u0430\u0440\u0430\u043f\u044b\u043d\u0430\u043d \u0430\u0436\u044b\u0440\u0430\u0442\u044b\u043b\u0493\u0430\u043d", "DeviceOfflineWithName": "{0} \u0430\u0436\u044b\u0440\u0430\u0442\u044b\u043b\u0493\u0430\u043d", - "UserStartedPlayingItemWithValues": "{0} - {1} \u043e\u0439\u043d\u0430\u0442\u0443\u044b\u043d \u0431\u0430\u0441\u0442\u0430\u0434\u044b", - "UserStoppedPlayingItemWithValues": "{0} - {1} \u043e\u0439\u043d\u0430\u0442\u0443\u044b\u043d \u0442\u043e\u049b\u0442\u0430\u0442\u0442\u044b", + "UserStartedPlayingItemWithValues": "{0} - {1} \u043e\u0439\u043d\u0430\u0442\u0443\u044b\u043d {2} \u0431\u0430\u0441\u0442\u0430\u0434\u044b", + "UserStoppedPlayingItemWithValues": "{0} - {1} \u043e\u0439\u043d\u0430\u0442\u0443\u044b\u043d {2} \u0442\u043e\u049b\u0442\u0430\u0442\u0442\u044b", "NotificationOptionPluginError": "\u041f\u043b\u0430\u0433\u0438\u043d \u0441\u04d9\u0442\u0441\u0456\u0437\u0434\u0456\u0433\u0456", "NotificationOptionApplicationUpdateAvailable": "\u049a\u043e\u043b\u0434\u0430\u043d\u0431\u0430 \u0436\u0430\u04a3\u0430\u0440\u0442\u0443\u044b \u049b\u043e\u043b\u0436\u0435\u0442\u0456\u043c\u0434\u0456", "NotificationOptionApplicationUpdateInstalled": "\u049a\u043e\u043b\u0434\u0430\u043d\u0431\u0430 \u0436\u0430\u04a3\u0430\u0440\u0442\u0443\u044b \u043e\u0440\u043d\u0430\u0442\u044b\u043b\u0434\u044b", diff --git a/Emby.Server.Implementations/Localization/Core/ko.json b/Emby.Server.Implementations/Localization/Core/ko.json index 0f99c8432..30709dde0 100644 --- a/Emby.Server.Implementations/Localization/Core/ko.json +++ b/Emby.Server.Implementations/Localization/Core/ko.json @@ -1,4 +1,12 @@ { + "HeaderCameraUploads": "Camera Uploads", + "ValueHasBeenAddedToLibrary": "{0} has been added to your media library", + "NameInstallFailed": "{0} installation failed", + "CameraImageUploadedFrom": "A new camera image has been uploaded from {0}", + "ServerNameNeedsToBeRestarted": "{0} needs to be restarted", + "NewVersionIsAvailable": "A new version of Emby Server is available for download.", + "MessageApplicationUpdatedTo": "Emby Server has been updated to {0}", + "SubtitleDownloadFailureFromForItem": "Subtitles failed to download from {0} for {1}", "Latest": "Latest", "ValueSpecialEpisodeName": "Special - {0}", "Inherit": "Inherit", @@ -24,6 +32,7 @@ "Channels": "Channels", "Movies": "Movies", "Albums": "Albums", + "NameSeasonUnknown": "Season Unknown", "Artists": "Artists", "Folders": "Folders", "Songs": "Songs", @@ -53,7 +62,7 @@ "UserCreatedWithName": "User {0} has been created", "UserPasswordChangedWithName": "Password has been changed for user {0}", "UserDeletedWithName": "User {0} has been deleted", - "UserConfigurationUpdatedWithName": "User configuration has been updated for {0}", + "UserPolicyUpdatedWithName": "User policy has been updated for {0}", "MessageServerConfigurationUpdated": "Server configuration has been updated", "MessageNamedServerConfigurationUpdatedWithValue": "Server configuration section {0} has been updated", "MessageApplicationUpdated": "Emby Server has been updated", @@ -61,8 +70,8 @@ "AuthenticationSucceededWithUserName": "{0} successfully authenticated", "UserOfflineFromDevice": "{0} has disconnected from {1}", "DeviceOfflineWithName": "{0} has disconnected", - "UserStartedPlayingItemWithValues": "{0} has started playing {1}", - "UserStoppedPlayingItemWithValues": "{0} has stopped playing {1}", + "UserStartedPlayingItemWithValues": "{0} is playing {1} on {2}", + "UserStoppedPlayingItemWithValues": "{0} has finished playing {1} on {2}", "NotificationOptionPluginError": "Plugin failure", "NotificationOptionApplicationUpdateAvailable": "Application update available", "NotificationOptionApplicationUpdateInstalled": "Application update installed", diff --git a/Emby.Server.Implementations/Localization/Core/lt-LT.json b/Emby.Server.Implementations/Localization/Core/lt-LT.json index 9e1fede1d..c71d2424a 100644 --- a/Emby.Server.Implementations/Localization/Core/lt-LT.json +++ b/Emby.Server.Implementations/Localization/Core/lt-LT.json @@ -1,4 +1,12 @@ { + "HeaderCameraUploads": "Camera Uploads", + "ValueHasBeenAddedToLibrary": "{0} has been added to your media library", + "NameInstallFailed": "{0} installation failed", + "CameraImageUploadedFrom": "A new camera image has been uploaded from {0}", + "ServerNameNeedsToBeRestarted": "{0} needs to be restarted", + "NewVersionIsAvailable": "A new version of Emby Server is available for download.", + "MessageApplicationUpdatedTo": "Emby Server has been updated to {0}", + "SubtitleDownloadFailureFromForItem": "Subtitles failed to download from {0} for {1}", "Latest": "Latest", "ValueSpecialEpisodeName": "Ypatinga - {0}", "Inherit": "Inherit", @@ -24,6 +32,7 @@ "Channels": "Channels", "Movies": "Filmai", "Albums": "Albums", + "NameSeasonUnknown": "Season Unknown", "Artists": "Artists", "Folders": "Folders", "Songs": "Songs", @@ -53,7 +62,7 @@ "UserCreatedWithName": "User {0} has been created", "UserPasswordChangedWithName": "Password has been changed for user {0}", "UserDeletedWithName": "User {0} has been deleted", - "UserConfigurationUpdatedWithName": "User configuration has been updated for {0}", + "UserPolicyUpdatedWithName": "User policy has been updated for {0}", "MessageServerConfigurationUpdated": "Server configuration has been updated", "MessageNamedServerConfigurationUpdatedWithValue": "Server configuration section {0} has been updated", "MessageApplicationUpdated": "Emby Server has been updated", @@ -61,8 +70,8 @@ "AuthenticationSucceededWithUserName": "{0} successfully authenticated", "UserOfflineFromDevice": "{0} has disconnected from {1}", "DeviceOfflineWithName": "{0} has disconnected", - "UserStartedPlayingItemWithValues": "{0} has started playing {1}", - "UserStoppedPlayingItemWithValues": "{0} has stopped playing {1}", + "UserStartedPlayingItemWithValues": "{0} is playing {1} on {2}", + "UserStoppedPlayingItemWithValues": "{0} has finished playing {1} on {2}", "NotificationOptionPluginError": "Plugin failure", "NotificationOptionApplicationUpdateAvailable": "Application update available", "NotificationOptionApplicationUpdateInstalled": "Application update installed", diff --git a/Emby.Server.Implementations/Localization/Core/ms.json b/Emby.Server.Implementations/Localization/Core/ms.json index c48042d9a..17d9c80a0 100644 --- a/Emby.Server.Implementations/Localization/Core/ms.json +++ b/Emby.Server.Implementations/Localization/Core/ms.json @@ -1,4 +1,12 @@ { + "HeaderCameraUploads": "Camera Uploads", + "ValueHasBeenAddedToLibrary": "{0} has been added to your media library", + "NameInstallFailed": "{0} installation failed", + "CameraImageUploadedFrom": "A new camera image has been uploaded from {0}", + "ServerNameNeedsToBeRestarted": "{0} needs to be restarted", + "NewVersionIsAvailable": "A new version of Emby Server is available for download.", + "MessageApplicationUpdatedTo": "Emby Server has been updated to {0}", + "SubtitleDownloadFailureFromForItem": "Subtitles failed to download from {0} for {1}", "Latest": "Latest", "ValueSpecialEpisodeName": "Special - {0}", "Inherit": "Inherit", @@ -24,6 +32,7 @@ "Channels": "Channels", "Movies": "Movies", "Albums": "Albums", + "NameSeasonUnknown": "Season Unknown", "Artists": "Artists", "Folders": "Folders", "Songs": "Songs", @@ -53,7 +62,7 @@ "UserCreatedWithName": "User {0} has been created", "UserPasswordChangedWithName": "Password has been changed for user {0}", "UserDeletedWithName": "User {0} has been deleted", - "UserConfigurationUpdatedWithName": "User configuration has been updated for {0}", + "UserPolicyUpdatedWithName": "User policy has been updated for {0}", "MessageServerConfigurationUpdated": "Server configuration has been updated", "MessageNamedServerConfigurationUpdatedWithValue": "Server configuration section {0} has been updated", "MessageApplicationUpdated": "Emby Server has been updated", @@ -61,8 +70,8 @@ "AuthenticationSucceededWithUserName": "{0} successfully authenticated", "UserOfflineFromDevice": "{0} has disconnected from {1}", "DeviceOfflineWithName": "{0} has disconnected", - "UserStartedPlayingItemWithValues": "{0} has started playing {1}", - "UserStoppedPlayingItemWithValues": "{0} has stopped playing {1}", + "UserStartedPlayingItemWithValues": "{0} is playing {1} on {2}", + "UserStoppedPlayingItemWithValues": "{0} has finished playing {1} on {2}", "NotificationOptionPluginError": "Plugin failure", "NotificationOptionApplicationUpdateAvailable": "Application update available", "NotificationOptionApplicationUpdateInstalled": "Application update installed", diff --git a/Emby.Server.Implementations/Localization/Core/nb.json b/Emby.Server.Implementations/Localization/Core/nb.json index 5cd9894be..3ceeb9ce9 100644 --- a/Emby.Server.Implementations/Localization/Core/nb.json +++ b/Emby.Server.Implementations/Localization/Core/nb.json @@ -1,4 +1,12 @@ { + "HeaderCameraUploads": "Camera Uploads", + "ValueHasBeenAddedToLibrary": "{0} has been added to your media library", + "NameInstallFailed": "{0} installation failed", + "CameraImageUploadedFrom": "A new camera image has been uploaded from {0}", + "ServerNameNeedsToBeRestarted": "{0} needs to be restarted", + "NewVersionIsAvailable": "A new version of Emby Server is available for download.", + "MessageApplicationUpdatedTo": "Emby Server has been updated to {0}", + "SubtitleDownloadFailureFromForItem": "Subtitles failed to download from {0} for {1}", "Latest": "Siste", "ValueSpecialEpisodeName": "Spesial - {0}", "Inherit": "Arve", @@ -24,6 +32,7 @@ "Channels": "Kanaler", "Movies": "Filmer", "Albums": "Album", + "NameSeasonUnknown": "Season Unknown", "Artists": "Artister", "Folders": "Mapper", "Songs": "Sanger", @@ -53,7 +62,7 @@ "UserCreatedWithName": "Bruker {0} er opprettet", "UserPasswordChangedWithName": "Passordet for {0} er oppdatert", "UserDeletedWithName": "Bruker {0} har blitt slettet", - "UserConfigurationUpdatedWithName": "Brukerkonfigurasjon har blitt oppdatert for {0}", + "UserPolicyUpdatedWithName": "User policy has been updated for {0}", "MessageServerConfigurationUpdated": "Server konfigurasjon er oppdatert", "MessageNamedServerConfigurationUpdatedWithValue": "Server konfigurasjon seksjon {0} har blitt oppdatert", "MessageApplicationUpdated": "Emby server har blitt oppdatert", diff --git a/Emby.Server.Implementations/Localization/Core/nl.json b/Emby.Server.Implementations/Localization/Core/nl.json index d79fcf747..a206ed92a 100644 --- a/Emby.Server.Implementations/Localization/Core/nl.json +++ b/Emby.Server.Implementations/Localization/Core/nl.json @@ -1,4 +1,12 @@ { + "HeaderCameraUploads": "Camera uploads", + "ValueHasBeenAddedToLibrary": "{0} has been added to your media library", + "NameInstallFailed": "{0} installation failed", + "CameraImageUploadedFrom": "A new camera image has been uploaded from {0}", + "ServerNameNeedsToBeRestarted": "{0} needs to be restarted", + "NewVersionIsAvailable": "A new version of Emby Server is available for download.", + "MessageApplicationUpdatedTo": "Emby Server has been updated to {0}", + "SubtitleDownloadFailureFromForItem": "Subtitles failed to download from {0} for {1}", "Latest": "Nieuwste", "ValueSpecialEpisodeName": "Speciaal - {0}", "Inherit": "Overerven", @@ -7,27 +15,28 @@ "Games": "Spellen", "Photos": "Foto's", "MixedContent": "Gemengde inhoud", - "MusicVideos": "Muziek video's", + "MusicVideos": "Muziekvideo's", "HomeVideos": "Thuis video's", "Playlists": "Afspeellijsten", - "HeaderRecordingGroups": "Opname groepen", + "HeaderRecordingGroups": "Opnamegroepen", "HeaderContinueWatching": "Kijken hervatten", "HeaderFavoriteArtists": "Favoriete artiesten", "HeaderFavoriteSongs": "Favoriete titels", "HeaderAlbumArtists": "Album artiesten", "HeaderFavoriteAlbums": "Favoriete albums", - "HeaderFavoriteEpisodes": "Favoriete Afleveringen", - "HeaderFavoriteShows": "Favoriete Shows", + "HeaderFavoriteEpisodes": "Favoriete afleveringen", + "HeaderFavoriteShows": "Favoriete shows", "HeaderNextUp": "Volgende", "Favorites": "Favorieten", "Collections": "Collecties", "Channels": "Kanalen", "Movies": "Films", "Albums": "Albums", + "NameSeasonUnknown": "Seizoen onbekend", "Artists": "Artiesten", "Folders": "Mappen", "Songs": "Titels", - "TvShows": "TV Shows", + "TvShows": "TV-series", "Shows": "Series", "Genres": "Genres", "NameSeasonNumber": "Seizoen {0}", @@ -42,7 +51,7 @@ "PluginInstalledWithName": "{0} is ge\u00efnstalleerd", "StartupEmbyServerIsLoading": "Emby Server is aan het laden, probeer het later opnieuw.", "PluginUpdatedWithName": "{0} is bijgewerkt", - "PluginUninstalledWithName": "{0} is gede\u00efnstalleerd", + "PluginUninstalledWithName": "{0} is verwijderd", "ItemAddedWithName": "{0} is toegevoegd aan de bibliotheek", "ItemRemovedWithName": "{0} is verwijderd uit de bibliotheek", "LabelIpAddressValue": "IP adres: {0}", @@ -53,7 +62,7 @@ "UserCreatedWithName": "Gebruiker {0} is aangemaakt", "UserPasswordChangedWithName": "Wachtwoord voor {0} is gewijzigd", "UserDeletedWithName": "Gebruiker {0} is verwijderd", - "UserConfigurationUpdatedWithName": "Gebruikersinstellingen voor {0} zijn bijgewerkt", + "UserPolicyUpdatedWithName": "Gebruikersbeleid gewijzigd voor {0}", "MessageServerConfigurationUpdated": "Server configuratie is bijgewerkt", "MessageNamedServerConfigurationUpdatedWithValue": "Sectie {0} van de server configuratie is bijgewerkt", "MessageApplicationUpdated": "Emby Server is bijgewerkt", diff --git a/Emby.Server.Implementations/Localization/Core/pl.json b/Emby.Server.Implementations/Localization/Core/pl.json index 896df24dd..065b2280c 100644 --- a/Emby.Server.Implementations/Localization/Core/pl.json +++ b/Emby.Server.Implementations/Localization/Core/pl.json @@ -1,5 +1,13 @@ { - "Latest": "Ostatnio dodane do", + "HeaderCameraUploads": "Przekazane obrazy", + "ValueHasBeenAddedToLibrary": "{0} zosta\u0142 dodany to biblioteki medi\u00f3w", + "NameInstallFailed": "Instalacja {0} nieudana.", + "CameraImageUploadedFrom": "Nowy obraz zosta\u0142 przekazany z {0}", + "ServerNameNeedsToBeRestarted": "{0} wymaga ponownego uruchomienia", + "NewVersionIsAvailable": "Nowa wersja serwera Emby jest dost\u0119pna do pobrania.", + "MessageApplicationUpdatedTo": "Serwer Emby zosta\u0142 zaktualizowany do wersji {0}", + "SubtitleDownloadFailureFromForItem": "Nieudane pobieranie napis\u00f3w z {0} dla {1}", + "Latest": "Ostatnio dodane", "ValueSpecialEpisodeName": "Specjalne - {0}", "Inherit": "Dziedzicz", "Books": "Ksi\u0105\u017cki", @@ -11,7 +19,7 @@ "HomeVideos": "Nagrania prywatne", "Playlists": "Listy odtwarzania", "HeaderRecordingGroups": "Grupy nagra\u0144", - "HeaderContinueWatching": "Kontynuuj ogl\u0105danie", + "HeaderContinueWatching": "Kontynuuj odtwarzanie", "HeaderFavoriteArtists": "Wykonawcy ulubieni", "HeaderFavoriteSongs": "Utwory ulubione", "HeaderAlbumArtists": "Wykonawcy album\u00f3w", @@ -24,6 +32,7 @@ "Channels": "Kana\u0142y", "Movies": "Filmy", "Albums": "Albumy", + "NameSeasonUnknown": "Sezon nieznany", "Artists": "Wykonawcy", "Folders": "Foldery", "Songs": "Utwory", @@ -53,7 +62,7 @@ "UserCreatedWithName": "U\u017cytkownik {0} zosta\u0142 utworzony", "UserPasswordChangedWithName": "Has\u0142o u\u017cytkownika {0} zosta\u0142o zmienione", "UserDeletedWithName": "U\u017cytkownik {0} zosta\u0142 usuni\u0119ty", - "UserConfigurationUpdatedWithName": "Konfiguracja u\u017cytkownika {0} zosta\u0142a zaktualizowana", + "UserPolicyUpdatedWithName": "Zmieniono zasady u\u017cytkowania dla {0}", "MessageServerConfigurationUpdated": "Konfiguracja serwera zosta\u0142a zaktualizowana", "MessageNamedServerConfigurationUpdatedWithValue": "Sekcja {0} konfiguracji serwera zosta\u0142a zaktualizowana", "MessageApplicationUpdated": "Serwer Emby zosta\u0142 zaktualizowany", @@ -61,8 +70,8 @@ "AuthenticationSucceededWithUserName": "{0} zosta\u0142 pomy\u015blnie uwierzytelniony", "UserOfflineFromDevice": "{0} z {1} zosta\u0142 roz\u0142\u0105czony", "DeviceOfflineWithName": "{0} zosta\u0142 roz\u0142\u0105czony", - "UserStartedPlayingItemWithValues": "{0} rozpocz\u0105\u0142 odtwarzanie {1}", - "UserStoppedPlayingItemWithValues": "{0} zatrzyma\u0142 odtwarzanie {1}", + "UserStartedPlayingItemWithValues": "{0} odtwarza {1} na {2}", + "UserStoppedPlayingItemWithValues": "{0} zako\u0144czy\u0142 odtwarzanie {1} na {2}", "NotificationOptionPluginError": "Awaria wtyczki", "NotificationOptionApplicationUpdateAvailable": "Dost\u0119pna aktualizacja aplikacji", "NotificationOptionApplicationUpdateInstalled": "Zainstalowano aktualizacj\u0119 aplikacji", diff --git a/Emby.Server.Implementations/Localization/Core/pt-BR.json b/Emby.Server.Implementations/Localization/Core/pt-BR.json index e0a375170..6e948f507 100644 --- a/Emby.Server.Implementations/Localization/Core/pt-BR.json +++ b/Emby.Server.Implementations/Localization/Core/pt-BR.json @@ -1,4 +1,12 @@ { + "HeaderCameraUploads": "Uploads da C\u00e2mera", + "ValueHasBeenAddedToLibrary": "{0} has been added to your media library", + "NameInstallFailed": "{0} installation failed", + "CameraImageUploadedFrom": "A new camera image has been uploaded from {0}", + "ServerNameNeedsToBeRestarted": "{0} needs to be restarted", + "NewVersionIsAvailable": "A new version of Emby Server is available for download.", + "MessageApplicationUpdatedTo": "Emby Server has been updated to {0}", + "SubtitleDownloadFailureFromForItem": "Subtitles failed to download from {0} for {1}", "Latest": "Recente", "ValueSpecialEpisodeName": "Especial - {0}", "Inherit": "Herdar", @@ -24,6 +32,7 @@ "Channels": "Canais", "Movies": "Filmes", "Albums": "\u00c1lbuns", + "NameSeasonUnknown": "Temporada Desconhecida", "Artists": "Artistas", "Folders": "Pastas", "Songs": "M\u00fasicas", @@ -53,7 +62,7 @@ "UserCreatedWithName": "O usu\u00e1rio {0} foi criado", "UserPasswordChangedWithName": "A senha foi alterada para o usu\u00e1rio {0}", "UserDeletedWithName": "O usu\u00e1rio {0} foi exclu\u00eddo", - "UserConfigurationUpdatedWithName": "A configura\u00e7\u00e3o do usu\u00e1rio foi atualizada para {0}", + "UserPolicyUpdatedWithName": "A pol\u00edtica de usu\u00e1rio foi atualizada para {0}", "MessageServerConfigurationUpdated": "A configura\u00e7\u00e3o do servidor foi atualizada", "MessageNamedServerConfigurationUpdatedWithValue": "A se\u00e7\u00e3o {0} da configura\u00e7\u00e3o do servidor foi atualizada", "MessageApplicationUpdated": "O servidor Emby foi atualizado", diff --git a/Emby.Server.Implementations/Localization/Core/pt-PT.json b/Emby.Server.Implementations/Localization/Core/pt-PT.json index ac20fa1e5..71d7e142e 100644 --- a/Emby.Server.Implementations/Localization/Core/pt-PT.json +++ b/Emby.Server.Implementations/Localization/Core/pt-PT.json @@ -1,4 +1,12 @@ { + "HeaderCameraUploads": "Camera Uploads", + "ValueHasBeenAddedToLibrary": "{0} has been added to your media library", + "NameInstallFailed": "{0} installation failed", + "CameraImageUploadedFrom": "A new camera image has been uploaded from {0}", + "ServerNameNeedsToBeRestarted": "{0} needs to be restarted", + "NewVersionIsAvailable": "A new version of Emby Server is available for download.", + "MessageApplicationUpdatedTo": "Emby Server has been updated to {0}", + "SubtitleDownloadFailureFromForItem": "Subtitles failed to download from {0} for {1}", "Latest": "Latest", "ValueSpecialEpisodeName": "Especial - {0}", "Inherit": "Inherit", @@ -24,6 +32,7 @@ "Channels": "Channels", "Movies": "Movies", "Albums": "Albums", + "NameSeasonUnknown": "Season Unknown", "Artists": "Artists", "Folders": "Folders", "Songs": "Songs", @@ -53,7 +62,7 @@ "UserCreatedWithName": "User {0} has been created", "UserPasswordChangedWithName": "Password has been changed for user {0}", "UserDeletedWithName": "User {0} has been deleted", - "UserConfigurationUpdatedWithName": "User configuration has been updated for {0}", + "UserPolicyUpdatedWithName": "User policy has been updated for {0}", "MessageServerConfigurationUpdated": "Server configuration has been updated", "MessageNamedServerConfigurationUpdatedWithValue": "Server configuration section {0} has been updated", "MessageApplicationUpdated": "Emby Server has been updated", @@ -61,8 +70,8 @@ "AuthenticationSucceededWithUserName": "{0} successfully authenticated", "UserOfflineFromDevice": "{0} has disconnected from {1}", "DeviceOfflineWithName": "{0} has disconnected", - "UserStartedPlayingItemWithValues": "{0} has started playing {1}", - "UserStoppedPlayingItemWithValues": "{0} has stopped playing {1}", + "UserStartedPlayingItemWithValues": "{0} is playing {1} on {2}", + "UserStoppedPlayingItemWithValues": "{0} has finished playing {1} on {2}", "NotificationOptionPluginError": "Plugin failure", "NotificationOptionApplicationUpdateAvailable": "Application update available", "NotificationOptionApplicationUpdateInstalled": "Application update installed", diff --git a/Emby.Server.Implementations/Localization/Core/ru.json b/Emby.Server.Implementations/Localization/Core/ru.json index 12345ca14..f842f8d2d 100644 --- a/Emby.Server.Implementations/Localization/Core/ru.json +++ b/Emby.Server.Implementations/Localization/Core/ru.json @@ -1,4 +1,12 @@ { + "HeaderCameraUploads": "\u041a\u0430\u043c\u0435\u0440\u044b", + "ValueHasBeenAddedToLibrary": "{0} (\u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 \u043c\u0435\u0434\u0438\u0430\u0442\u0435\u043a\u0443)", + "NameInstallFailed": "\u0423\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0430 {0} \u043d\u0435\u0443\u0434\u0430\u0447\u043d\u0430", + "CameraImageUploadedFrom": "\u041d\u043e\u0432\u043e\u0435 \u0444\u043e\u0442\u043e \u0431\u044b\u043b\u043e \u0432\u044b\u043b\u043e\u0436\u0435\u043d\u043e \u0441 {0}", + "ServerNameNeedsToBeRestarted": "\u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u043a {0}", + "NewVersionIsAvailable": "\u0418\u043c\u0435\u0435\u0442\u0441\u044f \u043d\u043e\u0432\u0430\u044f \u0432\u0435\u0440\u0441\u0438\u044f Emby Server", + "MessageApplicationUpdatedTo": "Emby Server \u0431\u044b\u043b \u043e\u0431\u043d\u043e\u0432\u043b\u0451\u043d \u0434\u043e {0}", + "SubtitleDownloadFailureFromForItem": "Subtitles failed to download from {0} for {1}", "Latest": "\u041d\u043e\u0432\u0435\u0439\u0448\u0435\u0435", "ValueSpecialEpisodeName": "\u0421\u043f\u0435\u0446\u044d\u043f\u0438\u0437\u043e\u0434 - {0}", "Inherit": "\u041d\u0430\u0441\u043b\u0435\u0434\u0443\u0435\u043c\u043e\u0435", @@ -24,6 +32,7 @@ "Channels": "\u041a\u0430\u043d\u0430\u043b\u044b", "Movies": "\u041a\u0438\u043d\u043e", "Albums": "\u0410\u043b\u044c\u0431\u043e\u043c\u044b", + "NameSeasonUnknown": "\u0421\u0435\u0437\u043e\u043d \u043d\u0435\u043e\u043f\u043e\u0437\u043d\u0430\u043d", "Artists": "\u0418\u0441\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u0438", "Folders": "\u041f\u0430\u043f\u043a\u0438", "Songs": "\u041a\u043e\u043c\u043f\u043e\u0437\u0438\u0446\u0438\u0438", @@ -53,7 +62,7 @@ "UserCreatedWithName": "\u041f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c {0} \u0431\u044b\u043b \u0441\u043e\u0437\u0434\u0430\u043d", "UserPasswordChangedWithName": "\u041f\u0430\u0440\u043e\u043b\u044c \u043f\u043e\u043b\u044c\u0437-\u043b\u044f {0} \u0431\u044b\u043b \u0438\u0437\u043c\u0435\u043d\u0451\u043d", "UserDeletedWithName": "\u041f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c {0} \u0431\u044b\u043b \u0443\u0434\u0430\u043b\u0451\u043d", - "UserConfigurationUpdatedWithName": "\u041a\u043e\u043d\u0444\u0438\u0433-\u0438\u044f \u043f\u043e\u043b\u044c\u0437-\u043b\u044f {0} \u0431\u044b\u043b\u0430 \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0430", + "UserPolicyUpdatedWithName": "\u041f\u043e\u043b\u044c\u0437-\u0438\u0435 \u043f\u043e\u043b\u0438\u0442\u0438\u043a\u0438 {0} \u0431\u044b\u043b\u0438 \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u044b", "MessageServerConfigurationUpdated": "\u041a\u043e\u043d\u0444\u0438\u0433-\u0438\u044f \u0441\u0435\u0440\u0432\u0435\u0440\u0430 \u0431\u044b\u043b\u0430 \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0430", "MessageNamedServerConfigurationUpdatedWithValue": "\u041a\u043e\u043d\u0444\u0438\u0433-\u0438\u044f \u0441\u0435\u0440\u0432\u0435\u0440\u0430 (\u0440\u0430\u0437\u0434\u0435\u043b {0}) \u0431\u044b\u043b\u0430 \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0430", "MessageApplicationUpdated": "Emby Server \u0431\u044b\u043b \u043e\u0431\u043d\u043e\u0432\u043b\u0451\u043d", @@ -61,8 +70,8 @@ "AuthenticationSucceededWithUserName": "{0} - \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044f \u0443\u0441\u043f\u0435\u0448\u043d\u0430", "UserOfflineFromDevice": "{0} - \u043f\u043e\u0434\u043a\u043b. \u0441 {1} \u0440\u0430\u0437\u044a-\u043d\u043e", "DeviceOfflineWithName": "{0} - \u043f\u043e\u0434\u043a\u043b. \u0440\u0430\u0437\u044a-\u043d\u043e", - "UserStartedPlayingItemWithValues": "{0} - \u0432\u043e\u0441\u043f\u0440. \u00ab{1}\u00bb \u0437\u0430\u043f-\u043d\u043e", - "UserStoppedPlayingItemWithValues": "{0} - \u0432\u043e\u0441\u043f\u0440. \u00ab{1}\u00bb \u043e\u0441\u0442-\u043d\u043e", + "UserStartedPlayingItemWithValues": "{0} - \u0432\u043e\u0441\u043f\u0440. \u00ab{1}\u00bb \u043d\u0430 {2}", + "UserStoppedPlayingItemWithValues": "{0} - \u0432\u043e\u0441\u043f\u0440. \u00ab{1}\u00bb \u043e\u0441\u0442-\u043d\u043e \u043d\u0430 {2}", "NotificationOptionPluginError": "\u0421\u0431\u043e\u0439 \u043f\u043b\u0430\u0433\u0438\u043d\u0430", "NotificationOptionApplicationUpdateAvailable": "\u0418\u043c\u0435\u0435\u0442\u0441\u044f \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f", "NotificationOptionApplicationUpdateInstalled": "\u041e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u043e", diff --git a/Emby.Server.Implementations/Localization/Core/sk.json b/Emby.Server.Implementations/Localization/Core/sk.json index f09eb1506..68d9222ff 100644 --- a/Emby.Server.Implementations/Localization/Core/sk.json +++ b/Emby.Server.Implementations/Localization/Core/sk.json @@ -1,4 +1,12 @@ { + "HeaderCameraUploads": "Camera Uploads", + "ValueHasBeenAddedToLibrary": "{0} has been added to your media library", + "NameInstallFailed": "{0} installation failed", + "CameraImageUploadedFrom": "A new camera image has been uploaded from {0}", + "ServerNameNeedsToBeRestarted": "{0} needs to be restarted", + "NewVersionIsAvailable": "A new version of Emby Server is available for download.", + "MessageApplicationUpdatedTo": "Emby Server has been updated to {0}", + "SubtitleDownloadFailureFromForItem": "Subtitles failed to download from {0} for {1}", "Latest": "Najnov\u0161ie", "ValueSpecialEpisodeName": "\u0160peci\u00e1l - {0}", "Inherit": "Zdedi\u0165", @@ -24,6 +32,7 @@ "Channels": "Kan\u00e1ly", "Movies": "Filmy", "Albums": "Albumy", + "NameSeasonUnknown": "Nezn\u00e1ma sez\u00f3na", "Artists": "Umelci", "Folders": "Prie\u010dinky", "Songs": "Skladby", @@ -33,7 +42,7 @@ "NameSeasonNumber": "Sez\u00f3na {0}", "AppDeviceValues": "Aplik\u00e1cia: {0}, Zariadenie: {1}", "UserDownloadingItemWithValues": "{0} s\u0165ahuje {1}", - "HeaderLiveTV": "Live TV", + "HeaderLiveTV": "\u017div\u00e1 TV", "ChapterNameValue": "Kapitola {0}", "ScheduledTaskFailedWithName": "{0} zlyhalo", "LabelRunningTimeValue": "D\u013a\u017eka: {0}", @@ -48,12 +57,12 @@ "LabelIpAddressValue": "IP adresa: {0}", "DeviceOnlineWithName": "{0} je pripojen\u00fd", "UserOnlineFromDevice": "{0} je online z {1}", - "ProviderValue": "Provider: {0}", + "ProviderValue": "Poskytovate\u013e: {0}", "SubtitlesDownloadedForItem": "Titulky pre {0} stiahnut\u00e9", "UserCreatedWithName": "Pou\u017e\u00edvate\u013e {0} bol vytvoren\u00fd", "UserPasswordChangedWithName": "Heslo pou\u017e\u00edvate\u013ea {0} zmenen\u00e9", "UserDeletedWithName": "Pou\u017e\u00edvate\u013e {0} bol vymazan\u00fd", - "UserConfigurationUpdatedWithName": "User configuration has been updated for {0}", + "UserPolicyUpdatedWithName": "User policy has been updated for {0}", "MessageServerConfigurationUpdated": "Konfigur\u00e1cia servera aktualizovan\u00e1", "MessageNamedServerConfigurationUpdatedWithValue": "Sekcia {0} konfigur\u00e1cie servera bola aktualizovan\u00e1", "MessageApplicationUpdated": "Emby Server bol aktualizovan\u00fd", @@ -63,12 +72,12 @@ "DeviceOfflineWithName": "{0} je odpojen\u00fd", "UserStartedPlayingItemWithValues": "{0} spustil prehr\u00e1vanie {1}", "UserStoppedPlayingItemWithValues": "{0} zastavil prehr\u00e1vanie {1}", - "NotificationOptionPluginError": "Plugin failure", + "NotificationOptionPluginError": "Chyba roz\u0161\u00edrenia", "NotificationOptionApplicationUpdateAvailable": "Je dostupn\u00e1 aktualiz\u00e1cia aplik\u00e1cie", "NotificationOptionApplicationUpdateInstalled": "Aktualiz\u00e1cia aplik\u00e1cie nain\u0161talovan\u00e1", - "NotificationOptionPluginUpdateInstalled": "Plugin update installed", - "NotificationOptionPluginInstalled": "Plugin installed", - "NotificationOptionPluginUninstalled": "Plugin uninstalled", + "NotificationOptionPluginUpdateInstalled": "Aktualiz\u00e1cia roz\u0161\u00edrenia nain\u0161talovan\u00e1", + "NotificationOptionPluginInstalled": "Roz\u0161\u00edrenie nain\u0161talovan\u00e9", + "NotificationOptionPluginUninstalled": "Roz\u0161\u00edrenie odin\u0161talovan\u00e9", "NotificationOptionVideoPlayback": "Spusten\u00e9 prehr\u00e1vanie videa", "NotificationOptionAudioPlayback": "Spusten\u00e9 prehr\u00e1vanie audia", "NotificationOptionGamePlayback": "Game playback started", @@ -78,7 +87,7 @@ "NotificationOptionTaskFailed": "Napl\u00e1novan\u00e1 \u00faloha zlyhala", "NotificationOptionInstallationFailed": "Chyba in\u0161tal\u00e1cie", "NotificationOptionNewLibraryContent": "Pridan\u00fd nov\u00fd obsah", - "NotificationOptionCameraImageUploaded": "Camera image uploaded", + "NotificationOptionCameraImageUploaded": "Nahran\u00fd obr\u00e1zok z fotoapar\u00e1tu", "NotificationOptionUserLockedOut": "User locked out", "NotificationOptionServerRestartRequired": "Vy\u017eaduje sa re\u0161tart servera", "UserLockedOutWithName": "User {0} has been locked out", @@ -87,5 +96,5 @@ "User": "Pou\u017e\u00edvate\u013e", "System": "Syst\u00e9m", "Application": "Aplik\u00e1cia", - "Plugin": "Plugin" + "Plugin": "Roz\u0161\u00edrenie" }
\ No newline at end of file diff --git a/Emby.Server.Implementations/Localization/Core/sl-SI.json b/Emby.Server.Implementations/Localization/Core/sl-SI.json index 27561a890..ebee7b692 100644 --- a/Emby.Server.Implementations/Localization/Core/sl-SI.json +++ b/Emby.Server.Implementations/Localization/Core/sl-SI.json @@ -1,4 +1,12 @@ { + "HeaderCameraUploads": "Camera Uploads", + "ValueHasBeenAddedToLibrary": "{0} has been added to your media library", + "NameInstallFailed": "{0} installation failed", + "CameraImageUploadedFrom": "A new camera image has been uploaded from {0}", + "ServerNameNeedsToBeRestarted": "{0} needs to be restarted", + "NewVersionIsAvailable": "A new version of Emby Server is available for download.", + "MessageApplicationUpdatedTo": "Emby Server has been updated to {0}", + "SubtitleDownloadFailureFromForItem": "Subtitles failed to download from {0} for {1}", "Latest": "Latest", "ValueSpecialEpisodeName": "Special - {0}", "Inherit": "Inherit", @@ -24,6 +32,7 @@ "Channels": "Channels", "Movies": "Movies", "Albums": "Albums", + "NameSeasonUnknown": "Season Unknown", "Artists": "Artists", "Folders": "Folders", "Songs": "Songs", @@ -53,7 +62,7 @@ "UserCreatedWithName": "User {0} has been created", "UserPasswordChangedWithName": "Password has been changed for user {0}", "UserDeletedWithName": "User {0} has been deleted", - "UserConfigurationUpdatedWithName": "User configuration has been updated for {0}", + "UserPolicyUpdatedWithName": "User policy has been updated for {0}", "MessageServerConfigurationUpdated": "Server configuration has been updated", "MessageNamedServerConfigurationUpdatedWithValue": "Server configuration section {0} has been updated", "MessageApplicationUpdated": "Emby Server has been updated", @@ -61,8 +70,8 @@ "AuthenticationSucceededWithUserName": "{0} successfully authenticated", "UserOfflineFromDevice": "{0} has disconnected from {1}", "DeviceOfflineWithName": "{0} has disconnected", - "UserStartedPlayingItemWithValues": "{0} has started playing {1}", - "UserStoppedPlayingItemWithValues": "{0} has stopped playing {1}", + "UserStartedPlayingItemWithValues": "{0} is playing {1} on {2}", + "UserStoppedPlayingItemWithValues": "{0} has finished playing {1} on {2}", "NotificationOptionPluginError": "Plugin failure", "NotificationOptionApplicationUpdateAvailable": "Application update available", "NotificationOptionApplicationUpdateInstalled": "Application update installed", diff --git a/Emby.Server.Implementations/Localization/Core/sv.json b/Emby.Server.Implementations/Localization/Core/sv.json index ed1867024..4323b38ca 100644 --- a/Emby.Server.Implementations/Localization/Core/sv.json +++ b/Emby.Server.Implementations/Localization/Core/sv.json @@ -1,4 +1,12 @@ { + "HeaderCameraUploads": "Camera Uploads", + "ValueHasBeenAddedToLibrary": "{0} has been added to your media library", + "NameInstallFailed": "{0} installation failed", + "CameraImageUploadedFrom": "A new camera image has been uploaded from {0}", + "ServerNameNeedsToBeRestarted": "{0} needs to be restarted", + "NewVersionIsAvailable": "A new version of Emby Server is available for download.", + "MessageApplicationUpdatedTo": "Emby Server has been updated to {0}", + "SubtitleDownloadFailureFromForItem": "Subtitles failed to download from {0} for {1}", "Latest": "Senaste", "ValueSpecialEpisodeName": "Specialavsnitt - {0}", "Inherit": "\u00c4rv", @@ -24,6 +32,7 @@ "Channels": "Kanaler", "Movies": "Filmer", "Albums": "Album", + "NameSeasonUnknown": "Ok\u00e4nd s\u00e4song", "Artists": "Artister", "Folders": "Mappar", "Songs": "L\u00e5tar", @@ -53,7 +62,7 @@ "UserCreatedWithName": "Anv\u00e4ndaren {0} har skapats", "UserPasswordChangedWithName": "L\u00f6senordet f\u00f6r {0} har \u00e4ndrats", "UserDeletedWithName": "Anv\u00e4ndaren {0} har tagits bort", - "UserConfigurationUpdatedWithName": "Anv\u00e4ndarinst\u00e4llningarna f\u00f6r {0} har uppdaterats", + "UserPolicyUpdatedWithName": "Anv\u00e4ndarpolicyn har uppdaterats f\u00f6r {0}", "MessageServerConfigurationUpdated": "Server konfigurationen har uppdaterats", "MessageNamedServerConfigurationUpdatedWithValue": "Serverinst\u00e4llningarna {0} har uppdaterats", "MessageApplicationUpdated": "Emby Server har uppdaterats", diff --git a/Emby.Server.Implementations/Localization/Core/tr.json b/Emby.Server.Implementations/Localization/Core/tr.json index 71af4110d..9d6922df0 100644 --- a/Emby.Server.Implementations/Localization/Core/tr.json +++ b/Emby.Server.Implementations/Localization/Core/tr.json @@ -1,4 +1,12 @@ { + "HeaderCameraUploads": "Camera Uploads", + "ValueHasBeenAddedToLibrary": "{0} has been added to your media library", + "NameInstallFailed": "{0} installation failed", + "CameraImageUploadedFrom": "A new camera image has been uploaded from {0}", + "ServerNameNeedsToBeRestarted": "{0} needs to be restarted", + "NewVersionIsAvailable": "A new version of Emby Server is available for download.", + "MessageApplicationUpdatedTo": "Emby Server has been updated to {0}", + "SubtitleDownloadFailureFromForItem": "Subtitles failed to download from {0} for {1}", "Latest": "Latest", "ValueSpecialEpisodeName": "Special - {0}", "Inherit": "Inherit", @@ -24,6 +32,7 @@ "Channels": "Channels", "Movies": "Movies", "Albums": "Albums", + "NameSeasonUnknown": "Season Unknown", "Artists": "Artists", "Folders": "Folders", "Songs": "Songs", @@ -53,7 +62,7 @@ "UserCreatedWithName": "User {0} has been created", "UserPasswordChangedWithName": "Password has been changed for user {0}", "UserDeletedWithName": "User {0} has been deleted", - "UserConfigurationUpdatedWithName": "User configuration has been updated for {0}", + "UserPolicyUpdatedWithName": "User policy has been updated for {0}", "MessageServerConfigurationUpdated": "Server configuration has been updated", "MessageNamedServerConfigurationUpdatedWithValue": "Server configuration section {0} has been updated", "MessageApplicationUpdated": "Emby Server has been updated", @@ -61,8 +70,8 @@ "AuthenticationSucceededWithUserName": "{0} successfully authenticated", "UserOfflineFromDevice": "{0} has disconnected from {1}", "DeviceOfflineWithName": "{0} has disconnected", - "UserStartedPlayingItemWithValues": "{0} has started playing {1}", - "UserStoppedPlayingItemWithValues": "{0} has stopped playing {1}", + "UserStartedPlayingItemWithValues": "{0} is playing {1} on {2}", + "UserStoppedPlayingItemWithValues": "{0} has finished playing {1} on {2}", "NotificationOptionPluginError": "Plugin failure", "NotificationOptionApplicationUpdateAvailable": "Application update available", "NotificationOptionApplicationUpdateInstalled": "Application update installed", diff --git a/Emby.Server.Implementations/Localization/Core/zh-CN.json b/Emby.Server.Implementations/Localization/Core/zh-CN.json index 0f248f3cd..6b7bd2f71 100644 --- a/Emby.Server.Implementations/Localization/Core/zh-CN.json +++ b/Emby.Server.Implementations/Localization/Core/zh-CN.json @@ -1,4 +1,12 @@ { + "HeaderCameraUploads": "\u76f8\u673a\u4e0a\u4f20", + "ValueHasBeenAddedToLibrary": "{0} has been added to your media library", + "NameInstallFailed": "{0} installation failed", + "CameraImageUploadedFrom": "A new camera image has been uploaded from {0}", + "ServerNameNeedsToBeRestarted": "{0} needs to be restarted", + "NewVersionIsAvailable": "A new version of Emby Server is available for download.", + "MessageApplicationUpdatedTo": "Emby Server has been updated to {0}", + "SubtitleDownloadFailureFromForItem": "Subtitles failed to download from {0} for {1}", "Latest": "\u6700\u65b0", "ValueSpecialEpisodeName": "\u7279\u5178 - {0}", "Inherit": "\u7ee7\u627f", @@ -24,10 +32,11 @@ "Channels": "\u9891\u9053", "Movies": "\u7535\u5f71", "Albums": "\u4e13\u8f91", + "NameSeasonUnknown": "\u672a\u77e5\u5b63", "Artists": "\u827a\u672f\u5bb6", "Folders": "\u6587\u4ef6\u5939", "Songs": "\u6b4c\u66f2", - "TvShows": "TV Shows", + "TvShows": "\u7535\u89c6\u8282\u76ee", "Shows": "\u8282\u76ee", "Genres": "\u98ce\u683c", "NameSeasonNumber": "\u5b63 {0}", @@ -53,7 +62,7 @@ "UserCreatedWithName": "\u7528\u6237 {0} \u5df2\u521b\u5efa", "UserPasswordChangedWithName": "\u5df2\u4e3a\u7528\u6237 {0} \u66f4\u6539\u5bc6\u7801", "UserDeletedWithName": "\u7528\u6237 {0} \u5df2\u5220\u9664", - "UserConfigurationUpdatedWithName": "{0} \u7684\u7528\u6237\u914d\u7f6e\u5df2\u66f4\u65b0", + "UserPolicyUpdatedWithName": "\u7528\u6237\u534f\u8bae\u5df2\u7ecf\u88ab\u66f4\u65b0\u4e3a {0}", "MessageServerConfigurationUpdated": "\u670d\u52a1\u5668\u914d\u7f6e\u5df2\u66f4\u65b0", "MessageNamedServerConfigurationUpdatedWithValue": "\u670d\u52a1\u5668\u914d\u7f6e {0} \u90e8\u5206\u5df2\u66f4\u65b0", "MessageApplicationUpdated": "Emby \u670d\u52a1\u5668\u5df2\u66f4\u65b0", diff --git a/Emby.Server.Implementations/Localization/Core/zh-HK.json b/Emby.Server.Implementations/Localization/Core/zh-HK.json index b60edb176..1abfe7e98 100644 --- a/Emby.Server.Implementations/Localization/Core/zh-HK.json +++ b/Emby.Server.Implementations/Localization/Core/zh-HK.json @@ -1,4 +1,12 @@ { + "HeaderCameraUploads": "Camera Uploads", + "ValueHasBeenAddedToLibrary": "{0} has been added to your media library", + "NameInstallFailed": "{0} installation failed", + "CameraImageUploadedFrom": "A new camera image has been uploaded from {0}", + "ServerNameNeedsToBeRestarted": "{0} needs to be restarted", + "NewVersionIsAvailable": "A new version of Emby Server is available for download.", + "MessageApplicationUpdatedTo": "Emby Server has been updated to {0}", + "SubtitleDownloadFailureFromForItem": "Subtitles failed to download from {0} for {1}", "Latest": "Latest", "ValueSpecialEpisodeName": "Special - {0}", "Inherit": "Inherit", @@ -24,6 +32,7 @@ "Channels": "Channels", "Movies": "Movies", "Albums": "Albums", + "NameSeasonUnknown": "Season Unknown", "Artists": "Artists", "Folders": "Folders", "Songs": "Songs", @@ -53,7 +62,7 @@ "UserCreatedWithName": "User {0} has been created", "UserPasswordChangedWithName": "Password has been changed for user {0}", "UserDeletedWithName": "User {0} has been deleted", - "UserConfigurationUpdatedWithName": "User configuration has been updated for {0}", + "UserPolicyUpdatedWithName": "User policy has been updated for {0}", "MessageServerConfigurationUpdated": "Server configuration has been updated", "MessageNamedServerConfigurationUpdatedWithValue": "Server configuration section {0} has been updated", "MessageApplicationUpdated": "Emby Server has been updated", @@ -61,8 +70,8 @@ "AuthenticationSucceededWithUserName": "{0} successfully authenticated", "UserOfflineFromDevice": "{0} has disconnected from {1}", "DeviceOfflineWithName": "{0} has disconnected", - "UserStartedPlayingItemWithValues": "{0} has started playing {1}", - "UserStoppedPlayingItemWithValues": "{0} has stopped playing {1}", + "UserStartedPlayingItemWithValues": "{0} is playing {1} on {2}", + "UserStoppedPlayingItemWithValues": "{0} has finished playing {1} on {2}", "NotificationOptionPluginError": "Plugin failure", "NotificationOptionApplicationUpdateAvailable": "Application update available", "NotificationOptionApplicationUpdateInstalled": "Application update installed", diff --git a/Emby.Server.Implementations/Localization/LocalizationManager.cs b/Emby.Server.Implementations/Localization/LocalizationManager.cs index 6d271c0e1..71a4ca824 100644 --- a/Emby.Server.Implementations/Localization/LocalizationManager.cs +++ b/Emby.Server.Implementations/Localization/LocalizationManager.cs @@ -30,8 +30,8 @@ namespace Emby.Server.Implementations.Localization /// </summary> private static readonly CultureInfo UsCulture = new CultureInfo("en-US"); - private readonly ConcurrentDictionary<string, Dictionary<string, ParentalRating>> _allParentalRatings = - new ConcurrentDictionary<string, Dictionary<string, ParentalRating>>(StringComparer.OrdinalIgnoreCase); + private readonly Dictionary<string, Dictionary<string, ParentalRating>> _allParentalRatings = + new Dictionary<string, Dictionary<string, ParentalRating>>(StringComparer.OrdinalIgnoreCase); private readonly IFileSystem _fileSystem; private readonly IJsonSerializer _jsonSerializer; @@ -96,6 +96,61 @@ namespace Emby.Server.Implementations.Localization { LoadRatings(file); } + + LoadAdditionalRatings(); + } + + private void LoadAdditionalRatings() + { + LoadRatings("au", new[] { + + new ParentalRating("AU-G", 1), + new ParentalRating("AU-PG", 5), + new ParentalRating("AU-M", 6), + new ParentalRating("AU-MA15+", 7), + new ParentalRating("AU-M15+", 8), + new ParentalRating("AU-R18+", 9), + new ParentalRating("AU-X18+", 10), + new ParentalRating("AU-RC", 11) + }); + + LoadRatings("be", new[] { + + new ParentalRating("BE-AL", 1), + new ParentalRating("BE-MG6", 2), + new ParentalRating("BE-6", 3), + new ParentalRating("BE-9", 5), + new ParentalRating("BE-12", 6), + new ParentalRating("BE-16", 8) + }); + + LoadRatings("de", new[] { + + new ParentalRating("DE-0", 1), + new ParentalRating("FSK-0", 1), + new ParentalRating("DE-6", 5), + new ParentalRating("FSK-6", 5), + new ParentalRating("DE-12", 7), + new ParentalRating("FSK-12", 7), + new ParentalRating("DE-16", 8), + new ParentalRating("FSK-16", 8), + new ParentalRating("DE-18", 9), + new ParentalRating("FSK-18", 9) + }); + + LoadRatings("ru", new [] { + + new ParentalRating("RU-0+", 1), + new ParentalRating("RU-6+", 3), + new ParentalRating("RU-12+", 7), + new ParentalRating("RU-16+", 9), + new ParentalRating("RU-18+", 10) + }); + } + + private void LoadRatings(string country, ParentalRating[] ratings) + { + _allParentalRatings[country] = ratings.ToDictionary(i => i.Name); } private List<string> GetRatingsFiles(string directory) @@ -161,11 +216,17 @@ namespace Emby.Server.Implementations.Localization if (parts.Length == 5) { + var threeletterNames = new List<string> { parts[0] }; + if (!string.IsNullOrWhiteSpace(parts[1])) + { + threeletterNames.Add(parts[1]); + } + list.Add(new CultureDto { DisplayName = parts[3], Name = parts[3], - ThreeLetterISOLanguageName = parts[0], + ThreeLetterISOLanguageNames = threeletterNames.ToArray(), TwoLetterISOLanguageName = parts[2] }); } @@ -176,7 +237,7 @@ namespace Emby.Server.Implementations.Localization result = list.Where(i => !string.IsNullOrWhiteSpace(i.Name) && !string.IsNullOrWhiteSpace(i.DisplayName) && - !string.IsNullOrWhiteSpace(i.ThreeLetterISOLanguageName) && + i.ThreeLetterISOLanguageNames.Length > 0 && !string.IsNullOrWhiteSpace(i.TwoLetterISOLanguageName)).ToArray(); _cultures = result; @@ -184,19 +245,25 @@ namespace Emby.Server.Implementations.Localization return result; } + public CultureDto FindLanguageInfo(string language) + { + return GetCultures() + .FirstOrDefault(i => string.Equals(i.DisplayName, language, StringComparison.OrdinalIgnoreCase) || + string.Equals(i.Name, language, StringComparison.OrdinalIgnoreCase) || + i.ThreeLetterISOLanguageNames.Contains(language, StringComparer.OrdinalIgnoreCase) || + string.Equals(i.TwoLetterISOLanguageName, language, StringComparison.OrdinalIgnoreCase)); + } + /// <summary> /// Gets the countries. /// </summary> /// <returns>IEnumerable{CountryInfo}.</returns> public CountryInfo[] GetCountries() { - var type = GetType(); - var path = type.Namespace + ".countries.json"; + // ToDo: DeserializeFromStream seems broken in this case + string jsonCountries = "[{\"Name\":\"AF\",\"DisplayName\":\"Afghanistan\",\"TwoLetterISORegionName\":\"AF\",\"ThreeLetterISORegionName\":\"AFG\"},{\"Name\":\"AL\",\"DisplayName\":\"Albania\",\"TwoLetterISORegionName\":\"AL\",\"ThreeLetterISORegionName\":\"ALB\"},{\"Name\":\"DZ\",\"DisplayName\":\"Algeria\",\"TwoLetterISORegionName\":\"DZ\",\"ThreeLetterISORegionName\":\"DZA\"},{\"Name\":\"AR\",\"DisplayName\":\"Argentina\",\"TwoLetterISORegionName\":\"AR\",\"ThreeLetterISORegionName\":\"ARG\"},{\"Name\":\"AM\",\"DisplayName\":\"Armenia\",\"TwoLetterISORegionName\":\"AM\",\"ThreeLetterISORegionName\":\"ARM\"},{\"Name\":\"AU\",\"DisplayName\":\"Australia\",\"TwoLetterISORegionName\":\"AU\",\"ThreeLetterISORegionName\":\"AUS\"},{\"Name\":\"AT\",\"DisplayName\":\"Austria\",\"TwoLetterISORegionName\":\"AT\",\"ThreeLetterISORegionName\":\"AUT\"},{\"Name\":\"AZ\",\"DisplayName\":\"Azerbaijan\",\"TwoLetterISORegionName\":\"AZ\",\"ThreeLetterISORegionName\":\"AZE\"},{\"Name\":\"BH\",\"DisplayName\":\"Bahrain\",\"TwoLetterISORegionName\":\"BH\",\"ThreeLetterISORegionName\":\"BHR\"},{\"Name\":\"BD\",\"DisplayName\":\"Bangladesh\",\"TwoLetterISORegionName\":\"BD\",\"ThreeLetterISORegionName\":\"BGD\"},{\"Name\":\"BY\",\"DisplayName\":\"Belarus\",\"TwoLetterISORegionName\":\"BY\",\"ThreeLetterISORegionName\":\"BLR\"},{\"Name\":\"BE\",\"DisplayName\":\"Belgium\",\"TwoLetterISORegionName\":\"BE\",\"ThreeLetterISORegionName\":\"BEL\"},{\"Name\":\"BZ\",\"DisplayName\":\"Belize\",\"TwoLetterISORegionName\":\"BZ\",\"ThreeLetterISORegionName\":\"BLZ\"},{\"Name\":\"VE\",\"DisplayName\":\"Bolivarian Republic of Venezuela\",\"TwoLetterISORegionName\":\"VE\",\"ThreeLetterISORegionName\":\"VEN\"},{\"Name\":\"BO\",\"DisplayName\":\"Bolivia\",\"TwoLetterISORegionName\":\"BO\",\"ThreeLetterISORegionName\":\"BOL\"},{\"Name\":\"BA\",\"DisplayName\":\"Bosnia and Herzegovina\",\"TwoLetterISORegionName\":\"BA\",\"ThreeLetterISORegionName\":\"BIH\"},{\"Name\":\"BW\",\"DisplayName\":\"Botswana\",\"TwoLetterISORegionName\":\"BW\",\"ThreeLetterISORegionName\":\"BWA\"},{\"Name\":\"BR\",\"DisplayName\":\"Brazil\",\"TwoLetterISORegionName\":\"BR\",\"ThreeLetterISORegionName\":\"BRA\"},{\"Name\":\"BN\",\"DisplayName\":\"Brunei Darussalam\",\"TwoLetterISORegionName\":\"BN\",\"ThreeLetterISORegionName\":\"BRN\"},{\"Name\":\"BG\",\"DisplayName\":\"Bulgaria\",\"TwoLetterISORegionName\":\"BG\",\"ThreeLetterISORegionName\":\"BGR\"},{\"Name\":\"KH\",\"DisplayName\":\"Cambodia\",\"TwoLetterISORegionName\":\"KH\",\"ThreeLetterISORegionName\":\"KHM\"},{\"Name\":\"CM\",\"DisplayName\":\"Cameroon\",\"TwoLetterISORegionName\":\"CM\",\"ThreeLetterISORegionName\":\"CMR\"},{\"Name\":\"CA\",\"DisplayName\":\"Canada\",\"TwoLetterISORegionName\":\"CA\",\"ThreeLetterISORegionName\":\"CAN\"},{\"Name\":\"029\",\"DisplayName\":\"Caribbean\",\"TwoLetterISORegionName\":\"029\",\"ThreeLetterISORegionName\":\"029\"},{\"Name\":\"CL\",\"DisplayName\":\"Chile\",\"TwoLetterISORegionName\":\"CL\",\"ThreeLetterISORegionName\":\"CHL\"},{\"Name\":\"CO\",\"DisplayName\":\"Colombia\",\"TwoLetterISORegionName\":\"CO\",\"ThreeLetterISORegionName\":\"COL\"},{\"Name\":\"CD\",\"DisplayName\":\"Congo [DRC]\",\"TwoLetterISORegionName\":\"CD\",\"ThreeLetterISORegionName\":\"COD\"},{\"Name\":\"CR\",\"DisplayName\":\"Costa Rica\",\"TwoLetterISORegionName\":\"CR\",\"ThreeLetterISORegionName\":\"CRI\"},{\"Name\":\"HR\",\"DisplayName\":\"Croatia\",\"TwoLetterISORegionName\":\"HR\",\"ThreeLetterISORegionName\":\"HRV\"},{\"Name\":\"CZ\",\"DisplayName\":\"Czech Republic\",\"TwoLetterISORegionName\":\"CZ\",\"ThreeLetterISORegionName\":\"CZE\"},{\"Name\":\"DK\",\"DisplayName\":\"Denmark\",\"TwoLetterISORegionName\":\"DK\",\"ThreeLetterISORegionName\":\"DNK\"},{\"Name\":\"DO\",\"DisplayName\":\"Dominican Republic\",\"TwoLetterISORegionName\":\"DO\",\"ThreeLetterISORegionName\":\"DOM\"},{\"Name\":\"EC\",\"DisplayName\":\"Ecuador\",\"TwoLetterISORegionName\":\"EC\",\"ThreeLetterISORegionName\":\"ECU\"},{\"Name\":\"EG\",\"DisplayName\":\"Egypt\",\"TwoLetterISORegionName\":\"EG\",\"ThreeLetterISORegionName\":\"EGY\"},{\"Name\":\"SV\",\"DisplayName\":\"El Salvador\",\"TwoLetterISORegionName\":\"SV\",\"ThreeLetterISORegionName\":\"SLV\"},{\"Name\":\"ER\",\"DisplayName\":\"Eritrea\",\"TwoLetterISORegionName\":\"ER\",\"ThreeLetterISORegionName\":\"ERI\"},{\"Name\":\"EE\",\"DisplayName\":\"Estonia\",\"TwoLetterISORegionName\":\"EE\",\"ThreeLetterISORegionName\":\"EST\"},{\"Name\":\"ET\",\"DisplayName\":\"Ethiopia\",\"TwoLetterISORegionName\":\"ET\",\"ThreeLetterISORegionName\":\"ETH\"},{\"Name\":\"FO\",\"DisplayName\":\"Faroe Islands\",\"TwoLetterISORegionName\":\"FO\",\"ThreeLetterISORegionName\":\"FRO\"},{\"Name\":\"FI\",\"DisplayName\":\"Finland\",\"TwoLetterISORegionName\":\"FI\",\"ThreeLetterISORegionName\":\"FIN\"},{\"Name\":\"FR\",\"DisplayName\":\"France\",\"TwoLetterISORegionName\":\"FR\",\"ThreeLetterISORegionName\":\"FRA\"},{\"Name\":\"GE\",\"DisplayName\":\"Georgia\",\"TwoLetterISORegionName\":\"GE\",\"ThreeLetterISORegionName\":\"GEO\"},{\"Name\":\"DE\",\"DisplayName\":\"Germany\",\"TwoLetterISORegionName\":\"DE\",\"ThreeLetterISORegionName\":\"DEU\"},{\"Name\":\"GR\",\"DisplayName\":\"Greece\",\"TwoLetterISORegionName\":\"GR\",\"ThreeLetterISORegionName\":\"GRC\"},{\"Name\":\"GL\",\"DisplayName\":\"Greenland\",\"TwoLetterISORegionName\":\"GL\",\"ThreeLetterISORegionName\":\"GRL\"},{\"Name\":\"GT\",\"DisplayName\":\"Guatemala\",\"TwoLetterISORegionName\":\"GT\",\"ThreeLetterISORegionName\":\"GTM\"},{\"Name\":\"HT\",\"DisplayName\":\"Haiti\",\"TwoLetterISORegionName\":\"HT\",\"ThreeLetterISORegionName\":\"HTI\"},{\"Name\":\"HN\",\"DisplayName\":\"Honduras\",\"TwoLetterISORegionName\":\"HN\",\"ThreeLetterISORegionName\":\"HND\"},{\"Name\":\"HK\",\"DisplayName\":\"Hong Kong S.A.R.\",\"TwoLetterISORegionName\":\"HK\",\"ThreeLetterISORegionName\":\"HKG\"},{\"Name\":\"HU\",\"DisplayName\":\"Hungary\",\"TwoLetterISORegionName\":\"HU\",\"ThreeLetterISORegionName\":\"HUN\"},{\"Name\":\"IS\",\"DisplayName\":\"Iceland\",\"TwoLetterISORegionName\":\"IS\",\"ThreeLetterISORegionName\":\"ISL\"},{\"Name\":\"IN\",\"DisplayName\":\"India\",\"TwoLetterISORegionName\":\"IN\",\"ThreeLetterISORegionName\":\"IND\"},{\"Name\":\"ID\",\"DisplayName\":\"Indonesia\",\"TwoLetterISORegionName\":\"ID\",\"ThreeLetterISORegionName\":\"IDN\"},{\"Name\":\"IR\",\"DisplayName\":\"Iran\",\"TwoLetterISORegionName\":\"IR\",\"ThreeLetterISORegionName\":\"IRN\"},{\"Name\":\"IQ\",\"DisplayName\":\"Iraq\",\"TwoLetterISORegionName\":\"IQ\",\"ThreeLetterISORegionName\":\"IRQ\"},{\"Name\":\"IE\",\"DisplayName\":\"Ireland\",\"TwoLetterISORegionName\":\"IE\",\"ThreeLetterISORegionName\":\"IRL\"},{\"Name\":\"PK\",\"DisplayName\":\"Islamic Republic of Pakistan\",\"TwoLetterISORegionName\":\"PK\",\"ThreeLetterISORegionName\":\"PAK\"},{\"Name\":\"IL\",\"DisplayName\":\"Israel\",\"TwoLetterISORegionName\":\"IL\",\"ThreeLetterISORegionName\":\"ISR\"},{\"Name\":\"IT\",\"DisplayName\":\"Italy\",\"TwoLetterISORegionName\":\"IT\",\"ThreeLetterISORegionName\":\"ITA\"},{\"Name\":\"CI\",\"DisplayName\":\"Ivory Coast\",\"TwoLetterISORegionName\":\"CI\",\"ThreeLetterISORegionName\":\"CIV\"},{\"Name\":\"JM\",\"DisplayName\":\"Jamaica\",\"TwoLetterISORegionName\":\"JM\",\"ThreeLetterISORegionName\":\"JAM\"},{\"Name\":\"JP\",\"DisplayName\":\"Japan\",\"TwoLetterISORegionName\":\"JP\",\"ThreeLetterISORegionName\":\"JPN\"},{\"Name\":\"JO\",\"DisplayName\":\"Jordan\",\"TwoLetterISORegionName\":\"JO\",\"ThreeLetterISORegionName\":\"JOR\"},{\"Name\":\"KZ\",\"DisplayName\":\"Kazakhstan\",\"TwoLetterISORegionName\":\"KZ\",\"ThreeLetterISORegionName\":\"KAZ\"},{\"Name\":\"KE\",\"DisplayName\":\"Kenya\",\"TwoLetterISORegionName\":\"KE\",\"ThreeLetterISORegionName\":\"KEN\"},{\"Name\":\"KR\",\"DisplayName\":\"Korea\",\"TwoLetterISORegionName\":\"KR\",\"ThreeLetterISORegionName\":\"KOR\"},{\"Name\":\"KW\",\"DisplayName\":\"Kuwait\",\"TwoLetterISORegionName\":\"KW\",\"ThreeLetterISORegionName\":\"KWT\"},{\"Name\":\"KG\",\"DisplayName\":\"Kyrgyzstan\",\"TwoLetterISORegionName\":\"KG\",\"ThreeLetterISORegionName\":\"KGZ\"},{\"Name\":\"LA\",\"DisplayName\":\"Lao P.D.R.\",\"TwoLetterISORegionName\":\"LA\",\"ThreeLetterISORegionName\":\"LAO\"},{\"Name\":\"419\",\"DisplayName\":\"Latin America\",\"TwoLetterISORegionName\":\"419\",\"ThreeLetterISORegionName\":\"419\"},{\"Name\":\"LV\",\"DisplayName\":\"Latvia\",\"TwoLetterISORegionName\":\"LV\",\"ThreeLetterISORegionName\":\"LVA\"},{\"Name\":\"LB\",\"DisplayName\":\"Lebanon\",\"TwoLetterISORegionName\":\"LB\",\"ThreeLetterISORegionName\":\"LBN\"},{\"Name\":\"LY\",\"DisplayName\":\"Libya\",\"TwoLetterISORegionName\":\"LY\",\"ThreeLetterISORegionName\":\"LBY\"},{\"Name\":\"LI\",\"DisplayName\":\"Liechtenstein\",\"TwoLetterISORegionName\":\"LI\",\"ThreeLetterISORegionName\":\"LIE\"},{\"Name\":\"LT\",\"DisplayName\":\"Lithuania\",\"TwoLetterISORegionName\":\"LT\",\"ThreeLetterISORegionName\":\"LTU\"},{\"Name\":\"LU\",\"DisplayName\":\"Luxembourg\",\"TwoLetterISORegionName\":\"LU\",\"ThreeLetterISORegionName\":\"LUX\"},{\"Name\":\"MO\",\"DisplayName\":\"Macao S.A.R.\",\"TwoLetterISORegionName\":\"MO\",\"ThreeLetterISORegionName\":\"MAC\"},{\"Name\":\"MK\",\"DisplayName\":\"Macedonia (FYROM)\",\"TwoLetterISORegionName\":\"MK\",\"ThreeLetterISORegionName\":\"MKD\"},{\"Name\":\"MY\",\"DisplayName\":\"Malaysia\",\"TwoLetterISORegionName\":\"MY\",\"ThreeLetterISORegionName\":\"MYS\"},{\"Name\":\"MV\",\"DisplayName\":\"Maldives\",\"TwoLetterISORegionName\":\"MV\",\"ThreeLetterISORegionName\":\"MDV\"},{\"Name\":\"ML\",\"DisplayName\":\"Mali\",\"TwoLetterISORegionName\":\"ML\",\"ThreeLetterISORegionName\":\"MLI\"},{\"Name\":\"MT\",\"DisplayName\":\"Malta\",\"TwoLetterISORegionName\":\"MT\",\"ThreeLetterISORegionName\":\"MLT\"},{\"Name\":\"MX\",\"DisplayName\":\"Mexico\",\"TwoLetterISORegionName\":\"MX\",\"ThreeLetterISORegionName\":\"MEX\"},{\"Name\":\"MN\",\"DisplayName\":\"Mongolia\",\"TwoLetterISORegionName\":\"MN\",\"ThreeLetterISORegionName\":\"MNG\"},{\"Name\":\"ME\",\"DisplayName\":\"Montenegro\",\"TwoLetterISORegionName\":\"ME\",\"ThreeLetterISORegionName\":\"MNE\"},{\"Name\":\"MA\",\"DisplayName\":\"Morocco\",\"TwoLetterISORegionName\":\"MA\",\"ThreeLetterISORegionName\":\"MAR\"},{\"Name\":\"NP\",\"DisplayName\":\"Nepal\",\"TwoLetterISORegionName\":\"NP\",\"ThreeLetterISORegionName\":\"NPL\"},{\"Name\":\"NL\",\"DisplayName\":\"Netherlands\",\"TwoLetterISORegionName\":\"NL\",\"ThreeLetterISORegionName\":\"NLD\"},{\"Name\":\"NZ\",\"DisplayName\":\"New Zealand\",\"TwoLetterISORegionName\":\"NZ\",\"ThreeLetterISORegionName\":\"NZL\"},{\"Name\":\"NI\",\"DisplayName\":\"Nicaragua\",\"TwoLetterISORegionName\":\"NI\",\"ThreeLetterISORegionName\":\"NIC\"},{\"Name\":\"NG\",\"DisplayName\":\"Nigeria\",\"TwoLetterISORegionName\":\"NG\",\"ThreeLetterISORegionName\":\"NGA\"},{\"Name\":\"NO\",\"DisplayName\":\"Norway\",\"TwoLetterISORegionName\":\"NO\",\"ThreeLetterISORegionName\":\"NOR\"},{\"Name\":\"OM\",\"DisplayName\":\"Oman\",\"TwoLetterISORegionName\":\"OM\",\"ThreeLetterISORegionName\":\"OMN\"},{\"Name\":\"PA\",\"DisplayName\":\"Panama\",\"TwoLetterISORegionName\":\"PA\",\"ThreeLetterISORegionName\":\"PAN\"},{\"Name\":\"PY\",\"DisplayName\":\"Paraguay\",\"TwoLetterISORegionName\":\"PY\",\"ThreeLetterISORegionName\":\"PRY\"},{\"Name\":\"CN\",\"DisplayName\":\"People's Republic of China\",\"TwoLetterISORegionName\":\"CN\",\"ThreeLetterISORegionName\":\"CHN\"},{\"Name\":\"PE\",\"DisplayName\":\"Peru\",\"TwoLetterISORegionName\":\"PE\",\"ThreeLetterISORegionName\":\"PER\"},{\"Name\":\"PH\",\"DisplayName\":\"Philippines\",\"TwoLetterISORegionName\":\"PH\",\"ThreeLetterISORegionName\":\"PHL\"},{\"Name\":\"PL\",\"DisplayName\":\"Poland\",\"TwoLetterISORegionName\":\"PL\",\"ThreeLetterISORegionName\":\"POL\"},{\"Name\":\"PT\",\"DisplayName\":\"Portugal\",\"TwoLetterISORegionName\":\"PT\",\"ThreeLetterISORegionName\":\"PRT\"},{\"Name\":\"MC\",\"DisplayName\":\"Principality of Monaco\",\"TwoLetterISORegionName\":\"MC\",\"ThreeLetterISORegionName\":\"MCO\"},{\"Name\":\"PR\",\"DisplayName\":\"Puerto Rico\",\"TwoLetterISORegionName\":\"PR\",\"ThreeLetterISORegionName\":\"PRI\"},{\"Name\":\"QA\",\"DisplayName\":\"Qatar\",\"TwoLetterISORegionName\":\"QA\",\"ThreeLetterISORegionName\":\"QAT\"},{\"Name\":\"MD\",\"DisplayName\":\"Republica Moldova\",\"TwoLetterISORegionName\":\"MD\",\"ThreeLetterISORegionName\":\"MDA\"},{\"Name\":\"RE\",\"DisplayName\":\"Réunion\",\"TwoLetterISORegionName\":\"RE\",\"ThreeLetterISORegionName\":\"REU\"},{\"Name\":\"RO\",\"DisplayName\":\"Romania\",\"TwoLetterISORegionName\":\"RO\",\"ThreeLetterISORegionName\":\"ROU\"},{\"Name\":\"RU\",\"DisplayName\":\"Russia\",\"TwoLetterISORegionName\":\"RU\",\"ThreeLetterISORegionName\":\"RUS\"},{\"Name\":\"RW\",\"DisplayName\":\"Rwanda\",\"TwoLetterISORegionName\":\"RW\",\"ThreeLetterISORegionName\":\"RWA\"},{\"Name\":\"SA\",\"DisplayName\":\"Saudi Arabia\",\"TwoLetterISORegionName\":\"SA\",\"ThreeLetterISORegionName\":\"SAU\"},{\"Name\":\"SN\",\"DisplayName\":\"Senegal\",\"TwoLetterISORegionName\":\"SN\",\"ThreeLetterISORegionName\":\"SEN\"},{\"Name\":\"RS\",\"DisplayName\":\"Serbia\",\"TwoLetterISORegionName\":\"RS\",\"ThreeLetterISORegionName\":\"SRB\"},{\"Name\":\"CS\",\"DisplayName\":\"Serbia and Montenegro (Former)\",\"TwoLetterISORegionName\":\"CS\",\"ThreeLetterISORegionName\":\"SCG\"},{\"Name\":\"SG\",\"DisplayName\":\"Singapore\",\"TwoLetterISORegionName\":\"SG\",\"ThreeLetterISORegionName\":\"SGP\"},{\"Name\":\"SK\",\"DisplayName\":\"Slovakia\",\"TwoLetterISORegionName\":\"SK\",\"ThreeLetterISORegionName\":\"SVK\"},{\"Name\":\"SI\",\"DisplayName\":\"Slovenia\",\"TwoLetterISORegionName\":\"SI\",\"ThreeLetterISORegionName\":\"SVN\"},{\"Name\":\"SO\",\"DisplayName\":\"Soomaaliya\",\"TwoLetterISORegionName\":\"SO\",\"ThreeLetterISORegionName\":\"SOM\"},{\"Name\":\"ZA\",\"DisplayName\":\"South Africa\",\"TwoLetterISORegionName\":\"ZA\",\"ThreeLetterISORegionName\":\"ZAF\"},{\"Name\":\"ES\",\"DisplayName\":\"Spain\",\"TwoLetterISORegionName\":\"ES\",\"ThreeLetterISORegionName\":\"ESP\"},{\"Name\":\"LK\",\"DisplayName\":\"Sri Lanka\",\"TwoLetterISORegionName\":\"LK\",\"ThreeLetterISORegionName\":\"LKA\"},{\"Name\":\"SE\",\"DisplayName\":\"Sweden\",\"TwoLetterISORegionName\":\"SE\",\"ThreeLetterISORegionName\":\"SWE\"},{\"Name\":\"CH\",\"DisplayName\":\"Switzerland\",\"TwoLetterISORegionName\":\"CH\",\"ThreeLetterISORegionName\":\"CHE\"},{\"Name\":\"SY\",\"DisplayName\":\"Syria\",\"TwoLetterISORegionName\":\"SY\",\"ThreeLetterISORegionName\":\"SYR\"},{\"Name\":\"TW\",\"DisplayName\":\"Taiwan\",\"TwoLetterISORegionName\":\"TW\",\"ThreeLetterISORegionName\":\"TWN\"},{\"Name\":\"TJ\",\"DisplayName\":\"Tajikistan\",\"TwoLetterISORegionName\":\"TJ\",\"ThreeLetterISORegionName\":\"TAJ\"},{\"Name\":\"TH\",\"DisplayName\":\"Thailand\",\"TwoLetterISORegionName\":\"TH\",\"ThreeLetterISORegionName\":\"THA\"},{\"Name\":\"TT\",\"DisplayName\":\"Trinidad and Tobago\",\"TwoLetterISORegionName\":\"TT\",\"ThreeLetterISORegionName\":\"TTO\"},{\"Name\":\"TN\",\"DisplayName\":\"Tunisia\",\"TwoLetterISORegionName\":\"TN\",\"ThreeLetterISORegionName\":\"TUN\"},{\"Name\":\"TR\",\"DisplayName\":\"Turkey\",\"TwoLetterISORegionName\":\"TR\",\"ThreeLetterISORegionName\":\"TUR\"},{\"Name\":\"TM\",\"DisplayName\":\"Turkmenistan\",\"TwoLetterISORegionName\":\"TM\",\"ThreeLetterISORegionName\":\"TKM\"},{\"Name\":\"AE\",\"DisplayName\":\"U.A.E.\",\"TwoLetterISORegionName\":\"AE\",\"ThreeLetterISORegionName\":\"ARE\"},{\"Name\":\"UA\",\"DisplayName\":\"Ukraine\",\"TwoLetterISORegionName\":\"UA\",\"ThreeLetterISORegionName\":\"UKR\"},{\"Name\":\"GB\",\"DisplayName\":\"United Kingdom\",\"TwoLetterISORegionName\":\"GB\",\"ThreeLetterISORegionName\":\"GBR\"},{\"Name\":\"US\",\"DisplayName\":\"United States\",\"TwoLetterISORegionName\":\"US\",\"ThreeLetterISORegionName\":\"USA\"},{\"Name\":\"UY\",\"DisplayName\":\"Uruguay\",\"TwoLetterISORegionName\":\"UY\",\"ThreeLetterISORegionName\":\"URY\"},{\"Name\":\"UZ\",\"DisplayName\":\"Uzbekistan\",\"TwoLetterISORegionName\":\"UZ\",\"ThreeLetterISORegionName\":\"UZB\"},{\"Name\":\"VN\",\"DisplayName\":\"Vietnam\",\"TwoLetterISORegionName\":\"VN\",\"ThreeLetterISORegionName\":\"VNM\"},{\"Name\":\"YE\",\"DisplayName\":\"Yemen\",\"TwoLetterISORegionName\":\"YE\",\"ThreeLetterISORegionName\":\"YEM\"},{\"Name\":\"ZW\",\"DisplayName\":\"Zimbabwe\",\"TwoLetterISORegionName\":\"ZW\",\"ThreeLetterISORegionName\":\"ZWE\"}]"; - using (var stream = _assemblyInfo.GetManifestResourceStream(type, path)) - { - return _jsonSerializer.DeserializeFromStream<CountryInfo[]>(stream); - } + return _jsonSerializer.DeserializeFromString<CountryInfo[]>(jsonCountries); } /// <summary> @@ -278,7 +345,7 @@ namespace Emby.Server.Implementations.Localization .Split('-') .Last(); - _allParentalRatings.TryAdd(countryCode, dict); + _allParentalRatings[countryCode] = dict; } private readonly string[] _unratedValues = { "n/a", "unrated", "not rated" }; @@ -305,19 +372,34 @@ namespace Emby.Server.Implementations.Localization ParentalRating value; - if (!ratingsDictionary.TryGetValue(rating, out value)) + if (ratingsDictionary.TryGetValue(rating, out value)) { - // If we don't find anything check all ratings systems - foreach (var dictionary in _allParentalRatings.Values) + return value.Value; + } + + // If we don't find anything check all ratings systems + foreach (var dictionary in _allParentalRatings.Values) + { + if (dictionary.TryGetValue(rating, out value)) { - if (dictionary.TryGetValue(rating, out value)) - { - return value.Value; - } + return value.Value; + } + } + + // Try splitting by : to handle "Germany: FSK 18" + var index = rating.IndexOf(':'); + if (index != -1) + { + rating = rating.Substring(index).TrimStart(':').Trim(); + + if (!string.IsNullOrWhiteSpace(rating)) + { + return GetRatingLevel(rating); } } - return value == null ? (int?)null : value.Value; + // TODO: Further improve by normalizing out all spaces and dashes + return null; } public bool HasUnicodeCategory(string value, UnicodeCategory category) @@ -340,11 +422,11 @@ namespace Emby.Server.Implementations.Localization public string GetLocalizedString(string phrase, string culture) { - if (string.IsNullOrWhiteSpace(culture)) + if (string.IsNullOrEmpty(culture)) { culture = _configurationManager.Configuration.UICulture; } - if (string.IsNullOrWhiteSpace(culture)) + if (string.IsNullOrEmpty(culture)) { culture = DefaultCulture; } @@ -368,7 +450,7 @@ namespace Emby.Server.Implementations.Localization public Dictionary<string, string> GetLocalizationDictionary(string culture) { - if (string.IsNullOrWhiteSpace(culture)) + if (string.IsNullOrEmpty(culture)) { throw new ArgumentNullException("culture"); } @@ -381,7 +463,7 @@ namespace Emby.Server.Implementations.Localization private Dictionary<string, string> GetDictionary(string prefix, string culture, string baseFilename) { - if (string.IsNullOrWhiteSpace(culture)) + if (string.IsNullOrEmpty(culture)) { throw new ArgumentNullException("culture"); } diff --git a/Emby.Server.Implementations/Localization/Ratings/au.txt b/Emby.Server.Implementations/Localization/Ratings/au.txt deleted file mode 100644 index fa60f5305..000000000 --- a/Emby.Server.Implementations/Localization/Ratings/au.txt +++ /dev/null @@ -1,8 +0,0 @@ -AU-G,1 -AU-PG,5 -AU-M,6 -AU-MA15+,7 -AU-M15+,8 -AU-R18+,9 -AU-X18+,10 -AU-RC,11 diff --git a/Emby.Server.Implementations/Localization/Ratings/be.txt b/Emby.Server.Implementations/Localization/Ratings/be.txt deleted file mode 100644 index 99a53f664..000000000 --- a/Emby.Server.Implementations/Localization/Ratings/be.txt +++ /dev/null @@ -1,6 +0,0 @@ -BE-AL,1 -BE-MG6,2 -BE-6,3 -BE-9,5 -BE-12,6 -BE-16,8
\ No newline at end of file diff --git a/Emby.Server.Implementations/Localization/Ratings/de.txt b/Emby.Server.Implementations/Localization/Ratings/de.txt deleted file mode 100644 index ad1f18619..000000000 --- a/Emby.Server.Implementations/Localization/Ratings/de.txt +++ /dev/null @@ -1,10 +0,0 @@ -DE-0,1 -FSK-0,1 -DE-6,5 -FSK-6,5 -DE-12,7 -FSK-12,7 -DE-16,8 -FSK-16,8 -DE-18,9 -FSK-18,9
\ No newline at end of file diff --git a/Emby.Server.Implementations/Localization/Ratings/ru.txt b/Emby.Server.Implementations/Localization/Ratings/ru.txt deleted file mode 100644 index 1bc94affd..000000000 --- a/Emby.Server.Implementations/Localization/Ratings/ru.txt +++ /dev/null @@ -1,5 +0,0 @@ -RU-0+,1 -RU-6+,3 -RU-12+,7 -RU-16+,9 -RU-18+,10 diff --git a/Emby.Server.Implementations/Localization/Ratings/us.txt b/Emby.Server.Implementations/Localization/Ratings/us.txt index 9bd78c72b..eebd828c7 100644 --- a/Emby.Server.Implementations/Localization/Ratings/us.txt +++ b/Emby.Server.Implementations/Localization/Ratings/us.txt @@ -1,9 +1,9 @@ +TV-Y,1 APPROVED,1 G,1 E,1 EC,1 TV-G,1 -TV-Y,2 TV-Y7,3 TV-Y7-FV,4 PG,5 diff --git a/Emby.Server.Implementations/Localization/iso6392.txt b/Emby.Server.Implementations/Localization/iso6392.txt index 5616d41bc..a7e7cfc20 100644 --- a/Emby.Server.Implementations/Localization/iso6392.txt +++ b/Emby.Server.Implementations/Localization/iso6392.txt @@ -403,7 +403,7 @@ sot||st|Sotho, Southern|sotho du Sud spa||es|Spanish; Castilian|espagnol; castillan srd||sc|Sardinian|sarde srn|||Sranan Tongo|sranan tongo -srp||sr|Serbian|serbe +srp|scc|sr|Serbian|serbe srr|||Serer|sérère ssa|||Nilo-Saharan languages|nilo-sahariennes, langues ssw||ss|Swati|swati diff --git a/Emby.Server.Implementations/Logging/SimpleLogManager.cs b/Emby.Server.Implementations/Logging/SimpleLogManager.cs index 6129f38c4..390814c34 100644 --- a/Emby.Server.Implementations/Logging/SimpleLogManager.cs +++ b/Emby.Server.Implementations/Logging/SimpleLogManager.cs @@ -31,7 +31,7 @@ namespace Emby.Server.Implementations.Logging return new NamedLogger(name, this); } - public void ReloadLogger(LogSeverity severity) + public async Task ReloadLogger(LogSeverity severity, CancellationToken cancellationToken) { LogSeverity = severity; @@ -39,19 +39,23 @@ namespace Emby.Server.Implementations.Logging if (logger != null) { logger.Dispose(); + await TryMoveToArchive(logger.Path, cancellationToken).ConfigureAwait(false); } - var path = Path.Combine(LogDirectory, LogFilePrefix + "-" + decimal.Floor(DateTime.Now.Ticks / 10000000) + ".txt"); + var newPath = Path.Combine(LogDirectory, LogFilePrefix + ".txt"); - _fileLogger = new FileLogger(path); + if (File.Exists(newPath)) + { + newPath = await TryMoveToArchive(newPath, cancellationToken).ConfigureAwait(false); + } + + _fileLogger = new FileLogger(newPath); if (LoggerLoaded != null) { try { - LoggerLoaded(this, EventArgs.Empty); - } catch (Exception ex) { @@ -60,6 +64,42 @@ namespace Emby.Server.Implementations.Logging } } + private async Task<string> TryMoveToArchive(string file, CancellationToken cancellationToken, int retryCount = 0) + { + var archivePath = GetArchiveFilePath(); + + try + { + File.Move(file, archivePath); + + return file; + } + catch (FileNotFoundException) + { + return file; + } + catch (DirectoryNotFoundException) + { + return file; + } + catch + { + if (retryCount >= 50) + { + return GetArchiveFilePath(); + } + + await Task.Delay(100, cancellationToken).ConfigureAwait(false); + + return await TryMoveToArchive(file, cancellationToken, retryCount + 1).ConfigureAwait(false); + } + } + + private string GetArchiveFilePath() + { + return Path.Combine(LogDirectory, LogFilePrefix + "-" + decimal.Floor(DateTime.Now.Ticks / 10000000) + ".txt"); + } + public event EventHandler LoggerLoaded; public void Flush() @@ -104,10 +144,12 @@ namespace Emby.Server.Implementations.Logging if (logger != null) { logger.Dispose(); + + var task = TryMoveToArchive(logger.Path, CancellationToken.None); + Task.WaitAll(task); } _fileLogger = null; - GC.SuppressFinalize(this); } } @@ -119,9 +161,13 @@ namespace Emby.Server.Implementations.Logging private readonly CancellationTokenSource _cancellationTokenSource; private readonly BlockingCollection<string> _queue = new BlockingCollection<string>(); + public string Path { get; set; } + public FileLogger(string path) { - Directory.CreateDirectory(Path.GetDirectoryName(path)); + Path = path; + + Directory.CreateDirectory(System.IO.Path.GetDirectoryName(path)); _fileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read, 32768); _cancellationTokenSource = new CancellationTokenSource(); @@ -144,6 +190,10 @@ namespace Emby.Server.Implementations.Logging } _fileStream.Write(bytes, 0, bytes.Length); + if (_disposed) + { + return; + } _fileStream.Flush(true); } @@ -177,13 +227,22 @@ namespace Emby.Server.Implementations.Logging public void Dispose() { - _cancellationTokenSource.Cancel(); - - Flush(); + if (_disposed) + { + return; + } _disposed = true; - _fileStream.Dispose(); - GC.SuppressFinalize(this); + _cancellationTokenSource.Cancel(); + + var stream = _fileStream; + if (stream != null) + { + using (stream) + { + stream.Flush(true); + } + } } } diff --git a/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs b/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs index d790f4ab8..c6033b4f4 100644 --- a/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs +++ b/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs @@ -46,7 +46,7 @@ namespace Emby.Server.Implementations.MediaEncoder /// Gets the chapter images data path. /// </summary> /// <value>The chapter images data path.</value> - private string GetChapterImagesPath(IHasMetadata item) + private string GetChapterImagesPath(BaseItem item) { return Path.Combine(item.GetInternalMetadataPath(), "chapters"); } @@ -133,6 +133,8 @@ namespace Emby.Server.Implementations.MediaEncoder { if (extractImages) { + cancellationToken.ThrowIfCancellationRequested(); + try { // Add some time for the first chapter to make sure we don't end up with a black image @@ -140,7 +142,7 @@ namespace Emby.Server.Implementations.MediaEncoder var protocol = MediaProtocol.File; - var inputPath = MediaEncoderHelpers.GetInputArgument(_fileSystem, video.Path, protocol, null, new string[] { }); + var inputPath = MediaEncoderHelpers.GetInputArgument(_fileSystem, video.Path, protocol, null, Array.Empty<string>()); _fileSystem.CreateDirectory(_fileSystem.GetDirectoryName(path)); diff --git a/Emby.Server.Implementations/Migrations/IVersionMigration.cs b/Emby.Server.Implementations/Migrations/IVersionMigration.cs deleted file mode 100644 index 7804912e3..000000000 --- a/Emby.Server.Implementations/Migrations/IVersionMigration.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.Threading.Tasks; - -namespace Emby.Server.Implementations.Migrations -{ - public interface IVersionMigration - { - Task Run(); - } -} diff --git a/Emby.Server.Implementations/Net/DisposableManagedObjectBase.cs b/Emby.Server.Implementations/Net/DisposableManagedObjectBase.cs index b18335da7..b721e8a26 100644 --- a/Emby.Server.Implementations/Net/DisposableManagedObjectBase.cs +++ b/Emby.Server.Implementations/Net/DisposableManagedObjectBase.cs @@ -54,16 +54,9 @@ namespace Emby.Server.Implementations.Net /// <seealso cref="IsDisposed"/> public void Dispose() { - try - { - IsDisposed = true; + IsDisposed = true; - Dispose(true); - } - finally - { - GC.SuppressFinalize(this); - } + Dispose(true); } #endregion diff --git a/Emby.Server.Implementations/Net/IWebSocket.cs b/Emby.Server.Implementations/Net/IWebSocket.cs new file mode 100644 index 000000000..f79199a07 --- /dev/null +++ b/Emby.Server.Implementations/Net/IWebSocket.cs @@ -0,0 +1,53 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using System.Net.WebSockets; + +namespace Emby.Server.Implementations.Net +{ + /// <summary> + /// Interface IWebSocket + /// </summary> + public interface IWebSocket : IDisposable + { + /// <summary> + /// Occurs when [closed]. + /// </summary> + event EventHandler<EventArgs> Closed; + + /// <summary> + /// Gets or sets the state. + /// </summary> + /// <value>The state.</value> + WebSocketState State { get; } + + /// <summary> + /// Gets or sets the receive action. + /// </summary> + /// <value>The receive action.</value> + Action<byte[]> OnReceiveBytes { get; set; } + + /// <summary> + /// Sends the async. + /// </summary> + /// <param name="bytes">The bytes.</param> + /// <param name="endOfMessage">if set to <c>true</c> [end of message].</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + Task SendAsync(byte[] bytes, bool endOfMessage, CancellationToken cancellationToken); + + /// <summary> + /// Sends the asynchronous. + /// </summary> + /// <param name="text">The text.</param> + /// <param name="endOfMessage">if set to <c>true</c> [end of message].</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + Task SendAsync(string text, bool endOfMessage, CancellationToken cancellationToken); + } + + public interface IMemoryWebSocket + { + Action<Memory<byte>, int> OnReceiveMemoryBytes { get; set; } + } +} diff --git a/Emby.Server.Implementations/Net/NetAcceptSocket.cs b/Emby.Server.Implementations/Net/NetAcceptSocket.cs deleted file mode 100644 index d80341a07..000000000 --- a/Emby.Server.Implementations/Net/NetAcceptSocket.cs +++ /dev/null @@ -1,98 +0,0 @@ -using System; -using System.Net; -using System.Net.Sockets; -using System.Threading; -using System.Threading.Tasks; -using Emby.Server.Implementations.Networking; -using MediaBrowser.Model.Logging; -using MediaBrowser.Model.Net; - -namespace Emby.Server.Implementations.Net -{ - public class NetAcceptSocket : IAcceptSocket - { - public Socket Socket { get; private set; } - private readonly ILogger _logger; - - public bool DualMode { get; private set; } - - public NetAcceptSocket(Socket socket, ILogger logger, bool isDualMode) - { - if (socket == null) - { - throw new ArgumentNullException("socket"); - } - if (logger == null) - { - throw new ArgumentNullException("logger"); - } - - Socket = socket; - _logger = logger; - DualMode = isDualMode; - } - - public IpEndPointInfo LocalEndPoint - { - get - { - return NetworkManager.ToIpEndPointInfo((IPEndPoint)Socket.LocalEndPoint); - } - } - - public IpEndPointInfo RemoteEndPoint - { - get - { - return NetworkManager.ToIpEndPointInfo((IPEndPoint)Socket.RemoteEndPoint); - } - } - - public void Connect(IpEndPointInfo endPoint) - { - var nativeEndpoint = NetworkManager.ToIPEndPoint(endPoint); - - Socket.Connect(nativeEndpoint); - } - - public void Close() - { -#if NET46 - Socket.Close(); -#else - Socket.Dispose(); -#endif - } - - public void Shutdown(bool both) - { - if (both) - { - Socket.Shutdown(SocketShutdown.Both); - } - else - { - // Change interface if ever needed - throw new NotImplementedException(); - } - } - - public void Listen(int backlog) - { - Socket.Listen(backlog); - } - - public void Bind(IpEndPointInfo endpoint) - { - var nativeEndpoint = NetworkManager.ToIPEndPoint(endpoint); - - Socket.Bind(nativeEndpoint); - } - - public void Dispose() - { - Socket.Dispose(); - GC.SuppressFinalize(this); - } - } -} diff --git a/Emby.Server.Implementations/Net/SocketFactory.cs b/Emby.Server.Implementations/Net/SocketFactory.cs index bdae1728f..9726ef097 100644 --- a/Emby.Server.Implementations/Net/SocketFactory.cs +++ b/Emby.Server.Implementations/Net/SocketFactory.cs @@ -29,41 +29,6 @@ namespace Emby.Server.Implementations.Net _logger = logger; } - public IAcceptSocket CreateSocket(IpAddressFamily family, MediaBrowser.Model.Net.SocketType socketType, MediaBrowser.Model.Net.ProtocolType protocolType, bool dualMode) - { - try - { - var addressFamily = family == IpAddressFamily.InterNetwork - ? AddressFamily.InterNetwork - : AddressFamily.InterNetworkV6; - - var socket = new Socket(addressFamily, System.Net.Sockets.SocketType.Stream, System.Net.Sockets.ProtocolType.Tcp); - - if (dualMode) - { - socket.DualMode = true; - } - - return new NetAcceptSocket(socket, _logger, dualMode); - } - catch (SocketException ex) - { - throw new SocketCreateException(ex.SocketErrorCode.ToString(), ex); - } - catch (ArgumentException ex) - { - if (dualMode) - { - // Mono for BSD incorrectly throws ArgumentException instead of SocketException - throw new SocketCreateException("AddressFamilyNotSupported", ex); - } - else - { - throw; - } - } - } - public ISocket CreateTcpSocket(IpAddressInfo remoteAddress, int remotePort) { if (remotePort < 0) throw new ArgumentException("remotePort cannot be less than zero.", "remotePort"); diff --git a/Emby.Server.Implementations/Net/UdpSocket.cs b/Emby.Server.Implementations/Net/UdpSocket.cs index 58e4d6f89..523ca3752 100644 --- a/Emby.Server.Implementations/Net/UdpSocket.cs +++ b/Emby.Server.Implementations/Net/UdpSocket.cs @@ -118,6 +118,8 @@ namespace Emby.Server.Implementations.Net public IAsyncResult BeginReceive(byte[] buffer, int offset, int count, AsyncCallback callback) { + ThrowIfDisposed(); + EndPoint receivedFromEndPoint = new IPEndPoint(IPAddress.Any, 0); return _Socket.BeginReceiveFrom(buffer, offset, count, SocketFlags.None, ref receivedFromEndPoint, callback, buffer); @@ -125,17 +127,21 @@ namespace Emby.Server.Implementations.Net public int Receive(byte[] buffer, int offset, int count) { + ThrowIfDisposed(); + return _Socket.Receive(buffer, 0, buffer.Length, SocketFlags.None); } public SocketReceiveResult EndReceive(IAsyncResult result) { + ThrowIfDisposed(); + IPEndPoint sender = new IPEndPoint(IPAddress.Any, 0); EndPoint remoteEndPoint = (EndPoint)sender; var receivedBytes = _Socket.EndReceiveFrom(result, ref remoteEndPoint); - var buffer = (byte[]) result.AsyncState; + var buffer = (byte[])result.AsyncState; return new SocketReceiveResult { @@ -148,13 +154,20 @@ namespace Emby.Server.Implementations.Net public Task<SocketReceiveResult> ReceiveAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) { + ThrowIfDisposed(); + var taskCompletion = new TaskCompletionSource<SocketReceiveResult>(); + bool isResultSet = false; Action<IAsyncResult> callback = callbackResult => { try { - taskCompletion.TrySetResult(EndReceive(callbackResult)); + if (!isResultSet) + { + isResultSet = true; + taskCompletion.TrySetResult(EndReceive(callbackResult)); + } } catch (Exception ex) { @@ -167,6 +180,7 @@ namespace Emby.Server.Implementations.Net if (result.CompletedSynchronously) { callback(result); + return taskCompletion.Task; } cancellationToken.Register(() => taskCompletion.TrySetCanceled()); @@ -176,6 +190,8 @@ namespace Emby.Server.Implementations.Net public Task<SocketReceiveResult> ReceiveAsync(CancellationToken cancellationToken) { + ThrowIfDisposed(); + var buffer = new byte[8192]; return ReceiveAsync(buffer, 0, buffer.Length, cancellationToken); @@ -183,13 +199,20 @@ namespace Emby.Server.Implementations.Net public Task SendToAsync(byte[] buffer, int offset, int size, IpEndPointInfo endPoint, CancellationToken cancellationToken) { + ThrowIfDisposed(); + var taskCompletion = new TaskCompletionSource<int>(); + bool isResultSet = false; Action<IAsyncResult> callback = callbackResult => { try { - taskCompletion.TrySetResult(EndSendTo(callbackResult)); + if (!isResultSet) + { + isResultSet = true; + taskCompletion.TrySetResult(EndSendTo(callbackResult)); + } } catch (Exception ex) { @@ -202,6 +225,7 @@ namespace Emby.Server.Implementations.Net if (result.CompletedSynchronously) { callback(result); + return taskCompletion.Task; } cancellationToken.Register(() => taskCompletion.TrySetCanceled()); @@ -211,6 +235,8 @@ namespace Emby.Server.Implementations.Net public IAsyncResult BeginSendTo(byte[] buffer, int offset, int size, IpEndPointInfo endPoint, AsyncCallback callback, object state) { + ThrowIfDisposed(); + var ipEndPoint = NetworkManager.ToIPEndPoint(endPoint); return _Socket.BeginSendTo(buffer, offset, size, SocketFlags.None, ipEndPoint, callback, state); @@ -218,6 +244,8 @@ namespace Emby.Server.Implementations.Net public int EndSendTo(IAsyncResult result) { + ThrowIfDisposed(); + return _Socket.EndSendTo(result); } diff --git a/Emby.Server.Implementations/Net/WebSocketConnectEventArgs.cs b/Emby.Server.Implementations/Net/WebSocketConnectEventArgs.cs new file mode 100644 index 000000000..7b7f12d50 --- /dev/null +++ b/Emby.Server.Implementations/Net/WebSocketConnectEventArgs.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Text; +using MediaBrowser.Model.Services; + +namespace Emby.Server.Implementations.Net +{ + public class WebSocketConnectEventArgs : EventArgs + { + /// <summary> + /// Gets or sets the URL. + /// </summary> + /// <value>The URL.</value> + public string Url { get; set; } + /// <summary> + /// Gets or sets the query string. + /// </summary> + /// <value>The query string.</value> + public QueryParamCollection QueryString { get; set; } + /// <summary> + /// Gets or sets the web socket. + /// </summary> + /// <value>The web socket.</value> + public IWebSocket WebSocket { get; set; } + /// <summary> + /// Gets or sets the endpoint. + /// </summary> + /// <value>The endpoint.</value> + public string Endpoint { get; set; } + } +} diff --git a/Emby.Server.Implementations/Networking/IPNetwork/BigIntegerExt.cs b/Emby.Server.Implementations/Networking/IPNetwork/BigIntegerExt.cs new file mode 100644 index 000000000..afb202fa3 --- /dev/null +++ b/Emby.Server.Implementations/Networking/IPNetwork/BigIntegerExt.cs @@ -0,0 +1,168 @@ +using System.Collections.Generic; + +namespace System.Net +{ + using System; + using System.Numerics; + using System.Text; + + /// <summary> + /// Extension methods to convert <see cref="System.Numerics.BigInteger"/> + /// instances to hexadecimal, octal, and binary strings. + /// </summary> + public static class BigIntegerExtensions + { + /// <summary> + /// Converts a <see cref="BigInteger"/> to a binary string. + /// </summary> + /// <param name="bigint">A <see cref="BigInteger"/>.</param> + /// <returns> + /// A <see cref="System.String"/> containing a binary + /// representation of the supplied <see cref="BigInteger"/>. + /// </returns> + public static string ToBinaryString(this BigInteger bigint) + { + var bytes = bigint.ToByteArray(); + var idx = bytes.Length - 1; + + // Create a StringBuilder having appropriate capacity. + var base2 = new StringBuilder(bytes.Length * 8); + + // Convert first byte to binary. + var binary = Convert.ToString(bytes[idx], 2); + + // Ensure leading zero exists if value is positive. + if (binary[0] != '0' && bigint.Sign == 1) + { + base2.Append('0'); + } + + // Append binary string to StringBuilder. + base2.Append(binary); + + // Convert remaining bytes adding leading zeros. + for (idx--; idx >= 0; idx--) + { + base2.Append(Convert.ToString(bytes[idx], 2).PadLeft(8, '0')); + } + + return base2.ToString(); + } + + /// <summary> + /// Converts a <see cref="BigInteger"/> to a hexadecimal string. + /// </summary> + /// <param name="bigint">A <see cref="BigInteger"/>.</param> + /// <returns> + /// A <see cref="System.String"/> containing a hexadecimal + /// representation of the supplied <see cref="BigInteger"/>. + /// </returns> + public static string ToHexadecimalString(this BigInteger bigint) + { + return bigint.ToString("X"); + } + + /// <summary> + /// Converts a <see cref="BigInteger"/> to a octal string. + /// </summary> + /// <param name="bigint">A <see cref="BigInteger"/>.</param> + /// <returns> + /// A <see cref="System.String"/> containing an octal + /// representation of the supplied <see cref="BigInteger"/>. + /// </returns> + public static string ToOctalString(this BigInteger bigint) + { + var bytes = bigint.ToByteArray(); + var idx = bytes.Length - 1; + + // Create a StringBuilder having appropriate capacity. + var base8 = new StringBuilder(((bytes.Length / 3) + 1) * 8); + + // Calculate how many bytes are extra when byte array is split + // into three-byte (24-bit) chunks. + var extra = bytes.Length % 3; + + // If no bytes are extra, use three bytes for first chunk. + if (extra == 0) + { + extra = 3; + } + + // Convert first chunk (24-bits) to integer value. + int int24 = 0; + for (; extra != 0; extra--) + { + int24 <<= 8; + int24 += bytes[idx--]; + } + + // Convert 24-bit integer to octal without adding leading zeros. + var octal = Convert.ToString(int24, 8); + + // Ensure leading zero exists if value is positive. + if (octal[0] != '0') + { + if (bigint.Sign == 1) + { + base8.Append('0'); + } + } + + // Append first converted chunk to StringBuilder. + base8.Append(octal); + + // Convert remaining 24-bit chunks, adding leading zeros. + for (; idx >= 0; idx -= 3) + { + int24 = (bytes[idx] << 16) + (bytes[idx - 1] << 8) + bytes[idx - 2]; + base8.Append(Convert.ToString(int24, 8).PadLeft(8, '0')); + } + + return base8.ToString(); + } + + /// <summary> + /// + /// Reverse a Positive BigInteger ONLY + /// Bitwise ~ operator + /// + /// Input : FF FF FF FF + /// Width : 4 + /// Result : 00 00 00 00 + /// + /// + /// Input : 00 00 00 00 + /// Width : 4 + /// Result : FF FF FF FF + /// + /// Input : FF FF FF FF + /// Width : 8 + /// Result : FF FF FF FF 00 00 00 00 + /// + /// + /// Input : 00 00 00 00 + /// Width : 8 + /// Result : FF FF FF FF FF FF FF FF + /// + /// </summary> + /// <param name="input"></param> + /// <param name="width"></param> + /// <returns></returns> + public static BigInteger PositiveReverse(this BigInteger input, int width) + { + + var result = new List<byte>(); + var bytes = input.ToByteArray(); + var work = new byte[width]; + Array.Copy(bytes, 0, work, 0, bytes.Length - 1); // Length -1 : positive BigInteger + + for (int i = 0; i < work.Length; i++) + { + result.Add((byte)(~work[i])); + } + result.Add(0); // positive BigInteger + return new BigInteger(result.ToArray()); + + } + } +}
\ No newline at end of file diff --git a/Emby.Server.Implementations/Networking/IPNetwork/IPAddressCollection.cs b/Emby.Server.Implementations/Networking/IPNetwork/IPAddressCollection.cs new file mode 100644 index 000000000..2b31a0a32 --- /dev/null +++ b/Emby.Server.Implementations/Networking/IPNetwork/IPAddressCollection.cs @@ -0,0 +1,104 @@ +using System.Collections; +using System.Collections.Generic; +using System.Numerics; + +namespace System.Net +{ + public class IPAddressCollection : IEnumerable<IPAddress>, IEnumerator<IPAddress> + { + + private IPNetwork _ipnetwork; + private BigInteger _enumerator; + + internal IPAddressCollection(IPNetwork ipnetwork) + { + this._ipnetwork = ipnetwork; + this._enumerator = -1; + } + + + #region Count, Array, Enumerator + + public BigInteger Count + { + get + { + return this._ipnetwork.Total; + } + } + + public IPAddress this[BigInteger i] + { + get + { + if (i >= this.Count) + { + throw new ArgumentOutOfRangeException("i"); + } + byte width = this._ipnetwork.AddressFamily == Sockets.AddressFamily.InterNetwork ? (byte)32 : (byte)128; + IPNetworkCollection ipn = this._ipnetwork.Subnet(width); + return ipn[i].Network; + } + } + + #endregion + + #region IEnumerable Members + + IEnumerator<IPAddress> IEnumerable<IPAddress>.GetEnumerator() + { + return this; + } + + IEnumerator IEnumerable.GetEnumerator() + { + return this; + } + + #region IEnumerator<IPNetwork> Members + + public IPAddress Current + { + get { return this[this._enumerator]; } + } + + #endregion + + #region IDisposable Members + + public void Dispose() + { + // nothing to dispose + return; + } + + #endregion + + #region IEnumerator Members + + object IEnumerator.Current + { + get { return this.Current; } + } + + public bool MoveNext() + { + this._enumerator++; + if (this._enumerator >= this.Count) + { + return false; + } + return true; + + } + + public void Reset() + { + this._enumerator = -1; + } + + #endregion + + #endregion + } +}
\ No newline at end of file diff --git a/Emby.Server.Implementations/Networking/IPNetwork/IPNetwork.cs b/Emby.Server.Implementations/Networking/IPNetwork/IPNetwork.cs new file mode 100644 index 000000000..6d7785b90 --- /dev/null +++ b/Emby.Server.Implementations/Networking/IPNetwork/IPNetwork.cs @@ -0,0 +1,2170 @@ +using System.Collections.Generic; +using System.IO; +using System.Net.Sockets; +using System.Numerics; +using System.Text.RegularExpressions; + +namespace System.Net +{ + /// <summary> + /// IP Network utility class. + /// Use IPNetwork.Parse to create instances. + /// </summary> + public class IPNetwork : IComparable<IPNetwork> + { + + #region properties + + //private uint _network; + private BigInteger _ipaddress; + private AddressFamily _family; + //private uint _netmask; + //private uint _broadcast; + //private uint _firstUsable; + //private uint _lastUsable; + //private uint _usable; + private byte _cidr; + + #endregion + + #region accessors + + private BigInteger _network + { + get + { + BigInteger uintNetwork = this._ipaddress & this._netmask; + return uintNetwork; + } + } + + /// <summary> + /// Network address + /// </summary> + public IPAddress Network + { + get + { + + return IPNetwork.ToIPAddress(this._network, this._family); + } + } + + /// <summary> + /// Address Family + /// </summary> + public AddressFamily AddressFamily + { + get + { + return this._family; + } + } + + private BigInteger _netmask + { + get + { + return IPNetwork.ToUint(this._cidr, this._family); + } + } + + /// <summary> + /// Netmask + /// </summary> + public IPAddress Netmask + { + get + { + return IPNetwork.ToIPAddress(this._netmask, this._family); + } + } + + private BigInteger _broadcast + { + get + { + + int width = this._family == Sockets.AddressFamily.InterNetwork ? 4 : 16; + BigInteger uintBroadcast = this._network + this._netmask.PositiveReverse(width); + return uintBroadcast; + } + } + + /// <summary> + /// Broadcast address + /// </summary> + public IPAddress Broadcast + { + get + { + if (this._family == Sockets.AddressFamily.InterNetworkV6) + { + return null; + } + return IPNetwork.ToIPAddress(this._broadcast, this._family); + } + } + + /// <summary> + /// First usable IP adress in Network + /// </summary> + public IPAddress FirstUsable + { + get + { + BigInteger fisrt = this._family == Sockets.AddressFamily.InterNetworkV6 + ? this._network + : (this.Usable <= 0) ? this._network : this._network + 1; + return IPNetwork.ToIPAddress(fisrt, this._family); + } + } + + /// <summary> + /// Last usable IP adress in Network + /// </summary> + public IPAddress LastUsable + { + get + { + BigInteger last = this._family == Sockets.AddressFamily.InterNetworkV6 + ? this._broadcast + : (this.Usable <= 0) ? this._network : this._broadcast - 1; + return IPNetwork.ToIPAddress(last, this._family); + } + } + + /// <summary> + /// Number of usable IP adress in Network + /// </summary> + public BigInteger Usable + { + get + { + + if (this._family == Sockets.AddressFamily.InterNetworkV6) + { + return this.Total; + } + byte[] mask = new byte[] { 0xff, 0xff, 0xff, 0xff, 0x00 }; + BigInteger bmask = new BigInteger(mask); + BigInteger usableIps = (_cidr > 30) ? 0 : ((bmask >> _cidr) - 1); + return usableIps; + } + } + + /// <summary> + /// Number of IP adress in Network + /// </summary> + public BigInteger Total + { + get + { + + int max = this._family == Sockets.AddressFamily.InterNetwork ? 32 : 128; + BigInteger count = BigInteger.Pow(2, (max - _cidr)); + return count; + } + } + + + /// <summary> + /// The CIDR netmask notation + /// </summary> + public byte Cidr + { + get + { + return this._cidr; + } + } + + #endregion + + #region constructor + +#if TRAVISCI + public +#else + internal +#endif + + IPNetwork(BigInteger ipaddress, AddressFamily family, byte cidr) + { + + int maxCidr = family == Sockets.AddressFamily.InterNetwork ? 32 : 128; + if (cidr > maxCidr) + { + throw new ArgumentOutOfRangeException("cidr"); + } + + this._ipaddress = ipaddress; + this._family = family; + this._cidr = cidr; + + } + + #endregion + + #region parsers + + /// <summary> + /// 192.168.168.100 - 255.255.255.0 + /// + /// Network : 192.168.168.0 + /// Netmask : 255.255.255.0 + /// Cidr : 24 + /// Start : 192.168.168.1 + /// End : 192.168.168.254 + /// Broadcast : 192.168.168.255 + /// </summary> + /// <param name="ipaddress"></param> + /// <param name="netmask"></param> + /// <returns></returns> + public static IPNetwork Parse(string ipaddress, string netmask) + { + + IPNetwork ipnetwork = null; + IPNetwork.InternalParse(false, ipaddress, netmask, out ipnetwork); + return ipnetwork; + } + + /// <summary> + /// 192.168.168.100/24 + /// + /// Network : 192.168.168.0 + /// Netmask : 255.255.255.0 + /// Cidr : 24 + /// Start : 192.168.168.1 + /// End : 192.168.168.254 + /// Broadcast : 192.168.168.255 + /// </summary> + /// <param name="ipaddress"></param> + /// <param name="cidr"></param> + /// <returns></returns> + public static IPNetwork Parse(string ipaddress, byte cidr) + { + + IPNetwork ipnetwork = null; + IPNetwork.InternalParse(false, ipaddress, cidr, out ipnetwork); + return ipnetwork; + + } + + /// <summary> + /// 192.168.168.100 255.255.255.0 + /// + /// Network : 192.168.168.0 + /// Netmask : 255.255.255.0 + /// Cidr : 24 + /// Start : 192.168.168.1 + /// End : 192.168.168.254 + /// Broadcast : 192.168.168.255 + /// </summary> + /// <param name="ipaddress"></param> + /// <param name="netmask"></param> + /// <returns></returns> + public static IPNetwork Parse(IPAddress ipaddress, IPAddress netmask) + { + + IPNetwork ipnetwork = null; + IPNetwork.InternalParse(false, ipaddress, netmask, out ipnetwork); + return ipnetwork; + + } + + /// <summary> + /// 192.168.0.1/24 + /// 192.168.0.1 255.255.255.0 + /// + /// Network : 192.168.0.0 + /// Netmask : 255.255.255.0 + /// Cidr : 24 + /// Start : 192.168.0.1 + /// End : 192.168.0.254 + /// Broadcast : 192.168.0.255 + /// </summary> + /// <param name="network"></param> + /// <returns></returns> + public static IPNetwork Parse(string network) + { + + IPNetwork ipnetwork = null; + IPNetwork.InternalParse(false, network, out ipnetwork); + return ipnetwork; + + } + + #endregion + + #region TryParse + + + + /// <summary> + /// 192.168.168.100 - 255.255.255.0 + /// + /// Network : 192.168.168.0 + /// Netmask : 255.255.255.0 + /// Cidr : 24 + /// Start : 192.168.168.1 + /// End : 192.168.168.254 + /// Broadcast : 192.168.168.255 + /// </summary> + /// <param name="ipaddress"></param> + /// <param name="netmask"></param> + /// <returns></returns> + public static bool TryParse(string ipaddress, string netmask, out IPNetwork ipnetwork) + { + + IPNetwork ipnetwork2 = null; + IPNetwork.InternalParse(true, ipaddress, netmask, out ipnetwork2); + bool parsed = (ipnetwork2 != null); + ipnetwork = ipnetwork2; + return parsed; + + } + + + + /// <summary> + /// 192.168.168.100/24 + /// + /// Network : 192.168.168.0 + /// Netmask : 255.255.255.0 + /// Cidr : 24 + /// Start : 192.168.168.1 + /// End : 192.168.168.254 + /// Broadcast : 192.168.168.255 + /// </summary> + /// <param name="ipaddress"></param> + /// <param name="cidr"></param> + /// <returns></returns> + public static bool TryParse(string ipaddress, byte cidr, out IPNetwork ipnetwork) + { + + IPNetwork ipnetwork2 = null; + IPNetwork.InternalParse(true, ipaddress, cidr, out ipnetwork2); + bool parsed = (ipnetwork2 != null); + ipnetwork = ipnetwork2; + return parsed; + + } + + /// <summary> + /// 192.168.0.1/24 + /// 192.168.0.1 255.255.255.0 + /// + /// Network : 192.168.0.0 + /// Netmask : 255.255.255.0 + /// Cidr : 24 + /// Start : 192.168.0.1 + /// End : 192.168.0.254 + /// Broadcast : 192.168.0.255 + /// </summary> + /// <param name="network"></param> + /// <param name="ipnetwork"></param> + /// <returns></returns> + public static bool TryParse(string network, out IPNetwork ipnetwork) + { + + IPNetwork ipnetwork2 = null; + IPNetwork.InternalParse(true, network, out ipnetwork2); + bool parsed = (ipnetwork2 != null); + ipnetwork = ipnetwork2; + return parsed; + + } + + /// <summary> + /// 192.168.0.1/24 + /// 192.168.0.1 255.255.255.0 + /// + /// Network : 192.168.0.0 + /// Netmask : 255.255.255.0 + /// Cidr : 24 + /// Start : 192.168.0.1 + /// End : 192.168.0.254 + /// Broadcast : 192.168.0.255 + /// </summary> + /// <param name="ipaddress"></param> + /// <param name="netmask"></param> + /// <param name="ipnetwork"></param> + /// <returns></returns> + public static bool TryParse(IPAddress ipaddress, IPAddress netmask, out IPNetwork ipnetwork) + { + + IPNetwork ipnetwork2 = null; + IPNetwork.InternalParse(true, ipaddress, netmask, out ipnetwork2); + bool parsed = (ipnetwork2 != null); + ipnetwork = ipnetwork2; + return parsed; + + } + + + #endregion + + #region InternalParse + + /// <summary> + /// 192.168.168.100 - 255.255.255.0 + /// + /// Network : 192.168.168.0 + /// Netmask : 255.255.255.0 + /// Cidr : 24 + /// Start : 192.168.168.1 + /// End : 192.168.168.254 + /// Broadcast : 192.168.168.255 + /// </summary> + /// <param name="ipaddress"></param> + /// <param name="netmask"></param> + /// <returns></returns> + private static void InternalParse(bool tryParse, string ipaddress, string netmask, out IPNetwork ipnetwork) + { + + if (string.IsNullOrEmpty(ipaddress)) + { + if (tryParse == false) + { + throw new ArgumentNullException("ipaddress"); + } + ipnetwork = null; + return; + } + + if (string.IsNullOrEmpty(netmask)) + { + if (tryParse == false) + { + throw new ArgumentNullException("netmask"); + } + ipnetwork = null; + return; + } + + IPAddress ip = null; + bool ipaddressParsed = IPAddress.TryParse(ipaddress, out ip); + if (ipaddressParsed == false) + { + if (tryParse == false) + { + throw new ArgumentException("ipaddress"); + } + ipnetwork = null; + return; + } + + IPAddress mask = null; + bool netmaskParsed = IPAddress.TryParse(netmask, out mask); + if (netmaskParsed == false) + { + if (tryParse == false) + { + throw new ArgumentException("netmask"); + } + ipnetwork = null; + return; + } + + IPNetwork.InternalParse(tryParse, ip, mask, out ipnetwork); + } + + private static void InternalParse(bool tryParse, string network, out IPNetwork ipnetwork) + { + + if (string.IsNullOrEmpty(network)) + { + if (tryParse == false) + { + throw new ArgumentNullException("network"); + } + ipnetwork = null; + return; + } + + network = Regex.Replace(network, @"[^0-9a-fA-F\.\/\s\:]+", ""); + network = Regex.Replace(network, @"\s{2,}", " "); + network = network.Trim(); + string[] args = network.Split(new char[] { ' ', '/' }); + byte cidr = 0; + if (args.Length == 1) + { + + if (IPNetwork.TryGuessCidr(args[0], out cidr)) + { + IPNetwork.InternalParse(tryParse, args[0], cidr, out ipnetwork); + return; + } + + if (tryParse == false) + { + throw new ArgumentException("network"); + } + ipnetwork = null; + return; + } + + if (byte.TryParse(args[1], out cidr)) + { + IPNetwork.InternalParse(tryParse, args[0], cidr, out ipnetwork); + return; + } + + IPNetwork.InternalParse(tryParse, args[0], args[1], out ipnetwork); + return; + + } + + + + /// <summary> + /// 192.168.168.100 255.255.255.0 + /// + /// Network : 192.168.168.0 + /// Netmask : 255.255.255.0 + /// Cidr : 24 + /// Start : 192.168.168.1 + /// End : 192.168.168.254 + /// Broadcast : 192.168.168.255 + /// </summary> + /// <param name="ipaddress"></param> + /// <param name="netmask"></param> + /// <returns></returns> + private static void InternalParse(bool tryParse, IPAddress ipaddress, IPAddress netmask, out IPNetwork ipnetwork) + { + + if (ipaddress == null) + { + if (tryParse == false) + { + throw new ArgumentNullException("ipaddress"); + } + ipnetwork = null; + return; + } + + if (netmask == null) + { + if (tryParse == false) + { + throw new ArgumentNullException("netmask"); + } + ipnetwork = null; + return; + } + + BigInteger uintIpAddress = IPNetwork.ToBigInteger(ipaddress); + byte? cidr2 = null; + bool parsed = IPNetwork.TryToCidr(netmask, out cidr2); + if (parsed == false) + { + if (tryParse == false) + { + throw new ArgumentException("netmask"); + } + ipnetwork = null; + return; + } + byte cidr = (byte)cidr2; + + IPNetwork ipnet = new IPNetwork(uintIpAddress, ipaddress.AddressFamily, cidr); + ipnetwork = ipnet; + + return; + } + + + + /// <summary> + /// 192.168.168.100/24 + /// + /// Network : 192.168.168.0 + /// Netmask : 255.255.255.0 + /// Cidr : 24 + /// Start : 192.168.168.1 + /// End : 192.168.168.254 + /// Broadcast : 192.168.168.255 + /// </summary> + /// <param name="ipaddress"></param> + /// <param name="cidr"></param> + /// <returns></returns> + private static void InternalParse(bool tryParse, string ipaddress, byte cidr, out IPNetwork ipnetwork) + { + + if (string.IsNullOrEmpty(ipaddress)) + { + if (tryParse == false) + { + throw new ArgumentNullException("ipaddress"); + } + ipnetwork = null; + return; + } + + + IPAddress ip = null; + bool ipaddressParsed = IPAddress.TryParse(ipaddress, out ip); + if (ipaddressParsed == false) + { + if (tryParse == false) + { + throw new ArgumentException("ipaddress"); + } + ipnetwork = null; + return; + } + + IPAddress mask = null; + bool parsedNetmask = IPNetwork.TryToNetmask(cidr, ip.AddressFamily, out mask); + if (parsedNetmask == false) + { + if (tryParse == false) + { + throw new ArgumentException("cidr"); + } + ipnetwork = null; + return; + } + + + IPNetwork.InternalParse(tryParse, ip, mask, out ipnetwork); + } + + #endregion + + #region converters + + #region ToUint + + /// <summary> + /// Convert an ipadress to decimal + /// 0.0.0.0 -> 0 + /// 0.0.1.0 -> 256 + /// </summary> + /// <param name="ipaddress"></param> + /// <returns></returns> + public static BigInteger ToBigInteger(IPAddress ipaddress) + { + BigInteger? uintIpAddress = null; + IPNetwork.InternalToBigInteger(false, ipaddress, out uintIpAddress); + return (BigInteger)uintIpAddress; + + } + + /// <summary> + /// Convert an ipadress to decimal + /// 0.0.0.0 -> 0 + /// 0.0.1.0 -> 256 + /// </summary> + /// <param name="ipaddress"></param> + /// <returns></returns> + public static bool TryToBigInteger(IPAddress ipaddress, out BigInteger? uintIpAddress) + { + BigInteger? uintIpAddress2 = null; + IPNetwork.InternalToBigInteger(true, ipaddress, out uintIpAddress2); + bool parsed = (uintIpAddress2 != null); + uintIpAddress = uintIpAddress2; + return parsed; + } + +#if TRAVISCI + public +#else + internal +#endif + static void InternalToBigInteger(bool tryParse, IPAddress ipaddress, out BigInteger? uintIpAddress) + { + + if (ipaddress == null) + { + if (tryParse == false) + { + throw new ArgumentNullException("ipaddress"); + } + uintIpAddress = null; + return; + } + + byte[] bytes = ipaddress.GetAddressBytes(); + /// 20180217 lduchosal + /// code impossible to reach, GetAddressBytes returns either 4 or 16 bytes length addresses + /// if (bytes.Length != 4 && bytes.Length != 16) { + /// if (tryParse == false) { + /// throw new ArgumentException("bytes"); + /// } + /// uintIpAddress = null; + /// return; + /// } + + Array.Reverse(bytes); + var unsigned = new List<byte>(bytes); + unsigned.Add(0); + uintIpAddress = new BigInteger(unsigned.ToArray()); + return; + } + + + /// <summary> + /// Convert a cidr to BigInteger netmask + /// </summary> + /// <param name="cidr"></param> + /// <returns></returns> + public static BigInteger ToUint(byte cidr, AddressFamily family) + { + + BigInteger? uintNetmask = null; + IPNetwork.InternalToBigInteger(false, cidr, family, out uintNetmask); + return (BigInteger)uintNetmask; + } + + + /// <summary> + /// Convert a cidr to uint netmask + /// </summary> + /// <param name="cidr"></param> + /// <returns></returns> + public static bool TryToUint(byte cidr, AddressFamily family, out BigInteger? uintNetmask) + { + + BigInteger? uintNetmask2 = null; + IPNetwork.InternalToBigInteger(true, cidr, family, out uintNetmask2); + bool parsed = (uintNetmask2 != null); + uintNetmask = uintNetmask2; + return parsed; + } + + /// <summary> + /// Convert a cidr to uint netmask + /// </summary> + /// <param name="cidr"></param> + /// <returns></returns> +#if TRAVISCI + public +#else + internal +#endif + static void InternalToBigInteger(bool tryParse, byte cidr, AddressFamily family, out BigInteger? uintNetmask) + { + + if (family == AddressFamily.InterNetwork && cidr > 32) + { + if (tryParse == false) + { + throw new ArgumentOutOfRangeException("cidr"); + } + uintNetmask = null; + return; + } + + if (family == AddressFamily.InterNetworkV6 && cidr > 128) + { + if (tryParse == false) + { + throw new ArgumentOutOfRangeException("cidr"); + } + uintNetmask = null; + return; + } + + if (family != AddressFamily.InterNetwork + && family != AddressFamily.InterNetworkV6) + { + if (tryParse == false) + { + throw new NotSupportedException(family.ToString()); + } + uintNetmask = null; + return; + } + + if (family == AddressFamily.InterNetwork) + { + + uintNetmask = cidr == 0 ? 0 : 0xffffffff << (32 - cidr); + return; + } + + BigInteger mask = new BigInteger(new byte[] { + 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, + 0x00 + }); + + BigInteger masked = cidr == 0 ? 0 : mask << (128 - cidr); + byte[] m = masked.ToByteArray(); + byte[] bmask = new byte[17]; + int copy = m.Length > 16 ? 16 : m.Length; + Array.Copy(m, 0, bmask, 0, copy); + uintNetmask = new BigInteger(bmask); + + + } + + #endregion + + #region ToCidr + + /// <summary> + /// Convert netmask to CIDR + /// 255.255.255.0 -> 24 + /// 255.255.0.0 -> 16 + /// 255.0.0.0 -> 8 + /// </summary> + /// <param name="netmask"></param> + /// <returns></returns> + private static void InternalToCidr(bool tryParse, BigInteger netmask, AddressFamily family, out byte? cidr) + { + + if (!IPNetwork.InternalValidNetmask(netmask, family)) + { + if (tryParse == false) + { + throw new ArgumentException("netmask"); + } + cidr = null; + return; + } + + byte cidr2 = IPNetwork.BitsSet(netmask, family); + cidr = cidr2; + return; + + } + /// <summary> + /// Convert netmask to CIDR + /// 255.255.255.0 -> 24 + /// 255.255.0.0 -> 16 + /// 255.0.0.0 -> 8 + /// </summary> + /// <param name="netmask"></param> + /// <returns></returns> + public static byte ToCidr(IPAddress netmask) + { + byte? cidr = null; + IPNetwork.InternalToCidr(false, netmask, out cidr); + return (byte)cidr; + } + + /// <summary> + /// Convert netmask to CIDR + /// 255.255.255.0 -> 24 + /// 255.255.0.0 -> 16 + /// 255.0.0.0 -> 8 + /// </summary> + /// <param name="netmask"></param> + /// <returns></returns> + public static bool TryToCidr(IPAddress netmask, out byte? cidr) + { + byte? cidr2 = null; + IPNetwork.InternalToCidr(true, netmask, out cidr2); + bool parsed = (cidr2 != null); + cidr = cidr2; + return parsed; + } + + private static void InternalToCidr(bool tryParse, IPAddress netmask, out byte? cidr) + { + + if (netmask == null) + { + if (tryParse == false) + { + throw new ArgumentNullException("netmask"); + } + cidr = null; + return; + } + BigInteger? uintNetmask2 = null; + bool parsed = IPNetwork.TryToBigInteger(netmask, out uintNetmask2); + + /// 20180217 lduchosal + /// impossible to reach code. + /// if (parsed == false) { + /// if (tryParse == false) { + /// throw new ArgumentException("netmask"); + /// } + /// cidr = null; + /// return; + /// } + BigInteger uintNetmask = (BigInteger)uintNetmask2; + + byte? cidr2 = null; + IPNetwork.InternalToCidr(tryParse, uintNetmask, netmask.AddressFamily, out cidr2); + cidr = cidr2; + + return; + + } + + + #endregion + + #region ToNetmask + + /// <summary> + /// Convert CIDR to netmask + /// 24 -> 255.255.255.0 + /// 16 -> 255.255.0.0 + /// 8 -> 255.0.0.0 + /// </summary> + /// <see cref="http://snipplr.com/view/15557/cidr-class-for-ipv4/"/> + /// <param name="cidr"></param> + /// <returns></returns> + public static IPAddress ToNetmask(byte cidr, AddressFamily family) + { + + IPAddress netmask = null; + IPNetwork.InternalToNetmask(false, cidr, family, out netmask); + return netmask; + } + + /// <summary> + /// Convert CIDR to netmask + /// 24 -> 255.255.255.0 + /// 16 -> 255.255.0.0 + /// 8 -> 255.0.0.0 + /// </summary> + /// <see cref="http://snipplr.com/view/15557/cidr-class-for-ipv4/"/> + /// <param name="cidr"></param> + /// <returns></returns> + public static bool TryToNetmask(byte cidr, AddressFamily family, out IPAddress netmask) + { + + IPAddress netmask2 = null; + IPNetwork.InternalToNetmask(true, cidr, family, out netmask2); + bool parsed = (netmask2 != null); + netmask = netmask2; + return parsed; + } + + +#if TRAVISCI + public +#else + internal +#endif + static void InternalToNetmask(bool tryParse, byte cidr, AddressFamily family, out IPAddress netmask) + { + + if (family != AddressFamily.InterNetwork + && family != AddressFamily.InterNetworkV6) + { + if (tryParse == false) + { + throw new ArgumentException("family"); + } + netmask = null; + return; + } + + /// 20180217 lduchosal + /// impossible to reach code, byte cannot be negative : + /// + /// if (cidr < 0) { + /// if (tryParse == false) { + /// throw new ArgumentOutOfRangeException("cidr"); + /// } + /// netmask = null; + /// return; + /// } + + int maxCidr = family == Sockets.AddressFamily.InterNetwork ? 32 : 128; + if (cidr > maxCidr) + { + if (tryParse == false) + { + throw new ArgumentOutOfRangeException("cidr"); + } + netmask = null; + return; + } + + BigInteger mask = IPNetwork.ToUint(cidr, family); + IPAddress netmask2 = IPNetwork.ToIPAddress(mask, family); + netmask = netmask2; + + return; + } + + #endregion + + #endregion + + #region utils + + #region BitsSet + + /// <summary> + /// Count bits set to 1 in netmask + /// </summary> + /// <see cref="http://stackoverflow.com/questions/109023/best-algorithm-to-count-the-number-of-set-bits-in-a-32-bit-integer"/> + /// <param name="netmask"></param> + /// <returns></returns> + private static byte BitsSet(BigInteger netmask, AddressFamily family) + { + + string s = netmask.ToBinaryString(); + return (byte)s.Replace("0", "") + .ToCharArray() + .Length; + + } + + + /// <summary> + /// Count bits set to 1 in netmask + /// </summary> + /// <param name="netmask"></param> + /// <returns></returns> + public static uint BitsSet(IPAddress netmask) + { + BigInteger uintNetmask = IPNetwork.ToBigInteger(netmask); + uint bits = IPNetwork.BitsSet(uintNetmask, netmask.AddressFamily); + return bits; + } + + #endregion + + #region ValidNetmask + + /// <summary> + /// return true if netmask is a valid netmask + /// 255.255.255.0, 255.0.0.0, 255.255.240.0, ... + /// </summary> + /// <see cref="http://www.actionsnip.com/snippets/tomo_atlacatl/calculate-if-a-netmask-is-valid--as2-"/> + /// <param name="netmask"></param> + /// <returns></returns> + public static bool ValidNetmask(IPAddress netmask) + { + + if (netmask == null) + { + throw new ArgumentNullException("netmask"); + } + BigInteger uintNetmask = IPNetwork.ToBigInteger(netmask); + bool valid = IPNetwork.InternalValidNetmask(uintNetmask, netmask.AddressFamily); + return valid; + } + +#if TRAVISCI + public +#else + internal +#endif + static bool InternalValidNetmask(BigInteger netmask, AddressFamily family) + { + + if (family != AddressFamily.InterNetwork + && family != AddressFamily.InterNetworkV6) + { + throw new ArgumentException("family"); + } + + var mask = family == AddressFamily.InterNetwork + ? new BigInteger(0x0ffffffff) + : new BigInteger(new byte[]{ + 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, + 0x00 + }); + + BigInteger neg = ((~netmask) & (mask)); + bool isNetmask = ((neg + 1) & neg) == 0; + return isNetmask; + + } + + #endregion + + #region ToIPAddress + + /// <summary> + /// Transform a uint ipaddress into IPAddress object + /// </summary> + /// <param name="ipaddress"></param> + /// <returns></returns> + public static IPAddress ToIPAddress(BigInteger ipaddress, AddressFamily family) + { + + int width = family == AddressFamily.InterNetwork ? 4 : 16; + byte[] bytes = ipaddress.ToByteArray(); + byte[] bytes2 = new byte[width]; + int copy = bytes.Length > width ? width : bytes.Length; + Array.Copy(bytes, 0, bytes2, 0, copy); + Array.Reverse(bytes2); + + byte[] sized = Resize(bytes2, family); + IPAddress ip = new IPAddress(sized); + return ip; + } + +#if TRAVISCI + public +#else + internal +#endif + static byte[] Resize(byte[] bytes, AddressFamily family) + { + + if (family != AddressFamily.InterNetwork + && family != AddressFamily.InterNetworkV6) + { + throw new ArgumentException("family"); + } + + int width = family == AddressFamily.InterNetwork ? 4 : 16; + + if (bytes.Length > width) + { + throw new ArgumentException("bytes"); + } + + byte[] result = new byte[width]; + Array.Copy(bytes, 0, result, 0, bytes.Length); + return result; + } + + #endregion + + #endregion + + #region contains + + /// <summary> + /// return true if ipaddress is contained in network + /// </summary> + /// <param name="ipaddress"></param> + /// <returns></returns> + public bool Contains(IPAddress ipaddress) + { + + if (ipaddress == null) + { + throw new ArgumentNullException("ipaddress"); + } + + if (AddressFamily != ipaddress.AddressFamily) + { + return false; + } + + BigInteger uintNetwork = _network; + BigInteger uintBroadcast = _broadcast; + BigInteger uintAddress = IPNetwork.ToBigInteger(ipaddress); + + bool contains = (uintAddress >= uintNetwork + && uintAddress <= uintBroadcast); + + return contains; + + } + + [Obsolete("static Contains is deprecated, please use instance Contains.")] + public static bool Contains(IPNetwork network, IPAddress ipaddress) + { + + if (network == null) + { + throw new ArgumentNullException("network"); + } + + return network.Contains(ipaddress); + } + + /// <summary> + /// return true is network2 is fully contained in network + /// </summary> + /// <param name="network2"></param> + /// <returns></returns> + public bool Contains(IPNetwork network2) + { + + if (network2 == null) + { + throw new ArgumentNullException("network2"); + } + + BigInteger uintNetwork = _network; + BigInteger uintBroadcast = _broadcast; + + BigInteger uintFirst = network2._network; + BigInteger uintLast = network2._broadcast; + + bool contains = (uintFirst >= uintNetwork + && uintLast <= uintBroadcast); + + return contains; + } + + [Obsolete("static Contains is deprecated, please use instance Contains.")] + public static bool Contains(IPNetwork network, IPNetwork network2) + { + + if (network == null) + { + throw new ArgumentNullException("network"); + } + + return network.Contains(network2); + } + + #endregion + + #region overlap + + /// <summary> + /// return true is network2 overlap network + /// </summary> + /// <param name="network2"></param> + /// <returns></returns> + public bool Overlap(IPNetwork network2) + { + + if (network2 == null) + { + throw new ArgumentNullException("network2"); + } + + BigInteger uintNetwork = _network; + BigInteger uintBroadcast = _broadcast; + + BigInteger uintFirst = network2._network; + BigInteger uintLast = network2._broadcast; + + bool overlap = + (uintFirst >= uintNetwork && uintFirst <= uintBroadcast) + || (uintLast >= uintNetwork && uintLast <= uintBroadcast) + || (uintFirst <= uintNetwork && uintLast >= uintBroadcast) + || (uintFirst >= uintNetwork && uintLast <= uintBroadcast); + + return overlap; + } + + [Obsolete("static Overlap is deprecated, please use instance Overlap.")] + public static bool Overlap(IPNetwork network, IPNetwork network2) + { + + if (network == null) + { + throw new ArgumentNullException("network"); + } + + return network.Overlap(network2); + } + + #endregion + + #region ToString + + public override string ToString() + { + return string.Format("{0}/{1}", this.Network, this.Cidr); + } + + #endregion + + #region IANA block + + private static readonly Lazy<IPNetwork> _iana_ablock_reserved = new Lazy<IPNetwork>(() => IPNetwork.Parse("10.0.0.0/8")); + private static readonly Lazy<IPNetwork> _iana_bblock_reserved = new Lazy<IPNetwork>(() => IPNetwork.Parse("172.16.0.0/12")); + private static readonly Lazy<IPNetwork> _iana_cblock_reserved = new Lazy<IPNetwork>(() => IPNetwork.Parse("192.168.0.0/16")); + + /// <summary> + /// 10.0.0.0/8 + /// </summary> + /// <returns></returns> + public static IPNetwork IANA_ABLK_RESERVED1 + { + get + { + return _iana_ablock_reserved.Value; + } + } + + /// <summary> + /// 172.12.0.0/12 + /// </summary> + /// <returns></returns> + public static IPNetwork IANA_BBLK_RESERVED1 + { + get + { + return _iana_bblock_reserved.Value; + } + } + + /// <summary> + /// 192.168.0.0/16 + /// </summary> + /// <returns></returns> + public static IPNetwork IANA_CBLK_RESERVED1 + { + get + { + return _iana_cblock_reserved.Value; + } + } + + /// <summary> + /// return true if ipaddress is contained in + /// IANA_ABLK_RESERVED1, IANA_BBLK_RESERVED1, IANA_CBLK_RESERVED1 + /// </summary> + /// <param name="ipaddress"></param> + /// <returns></returns> + public static bool IsIANAReserved(IPAddress ipaddress) + { + + if (ipaddress == null) + { + throw new ArgumentNullException("ipaddress"); + } + + return IPNetwork.IANA_ABLK_RESERVED1.Contains(ipaddress) + || IPNetwork.IANA_BBLK_RESERVED1.Contains(ipaddress) + || IPNetwork.IANA_CBLK_RESERVED1.Contains(ipaddress); + } + + /// <summary> + /// return true if ipnetwork is contained in + /// IANA_ABLK_RESERVED1, IANA_BBLK_RESERVED1, IANA_CBLK_RESERVED1 + /// </summary> + /// <returns></returns> + public bool IsIANAReserved() + { + return IPNetwork.IANA_ABLK_RESERVED1.Contains(this) + || IPNetwork.IANA_BBLK_RESERVED1.Contains(this) + || IPNetwork.IANA_CBLK_RESERVED1.Contains(this); + } + + [Obsolete("static IsIANAReserved is deprecated, please use instance IsIANAReserved.")] + public static bool IsIANAReserved(IPNetwork ipnetwork) + { + + if (ipnetwork == null) + { + throw new ArgumentNullException("ipnetwork"); + } + + return ipnetwork.IsIANAReserved(); + } + + #endregion + + #region Subnet + + /// <summary> + /// Subnet a network into multiple nets of cidr mask + /// Subnet 192.168.0.0/24 into cidr 25 gives 192.168.0.0/25, 192.168.0.128/25 + /// Subnet 10.0.0.0/8 into cidr 9 gives 10.0.0.0/9, 10.128.0.0/9 + /// </summary> + /// <param name="cidr"></param> + /// <returns></returns> + public IPNetworkCollection Subnet(byte cidr) + { + IPNetworkCollection ipnetworkCollection = null; + IPNetwork.InternalSubnet(false, this, cidr, out ipnetworkCollection); + return ipnetworkCollection; + } + + [Obsolete("static Subnet is deprecated, please use instance Subnet.")] + public static IPNetworkCollection Subnet(IPNetwork network, byte cidr) + { + if (network == null) + { + throw new ArgumentNullException("network"); + } + return network.Subnet(cidr); + } + + /// <summary> + /// Subnet a network into multiple nets of cidr mask + /// Subnet 192.168.0.0/24 into cidr 25 gives 192.168.0.0/25, 192.168.0.128/25 + /// Subnet 10.0.0.0/8 into cidr 9 gives 10.0.0.0/9, 10.128.0.0/9 + /// </summary> + /// <param name="cidr"></param> + /// <returns></returns> + public bool TrySubnet(byte cidr, out IPNetworkCollection ipnetworkCollection) + { + IPNetworkCollection inc = null; + IPNetwork.InternalSubnet(true, this, cidr, out inc); + if (inc == null) + { + ipnetworkCollection = null; + return false; + } + + ipnetworkCollection = inc; + return true; + } + + [Obsolete("static TrySubnet is deprecated, please use instance TrySubnet.")] + public static bool TrySubnet(IPNetwork network, byte cidr, out IPNetworkCollection ipnetworkCollection) + { + if (network == null) + { + throw new ArgumentNullException("network"); + } + return network.TrySubnet(cidr, out ipnetworkCollection); + } + +#if TRAVISCI + public +#else + internal +#endif + static void InternalSubnet(bool trySubnet, IPNetwork network, byte cidr, out IPNetworkCollection ipnetworkCollection) + { + + if (network == null) + { + if (trySubnet == false) + { + throw new ArgumentNullException("network"); + } + ipnetworkCollection = null; + return; + } + + int maxCidr = network._family == Sockets.AddressFamily.InterNetwork ? 32 : 128; + if (cidr > maxCidr) + { + if (trySubnet == false) + { + throw new ArgumentOutOfRangeException("cidr"); + } + ipnetworkCollection = null; + return; + } + + if (cidr < network.Cidr) + { + if (trySubnet == false) + { + throw new ArgumentException("cidr"); + } + ipnetworkCollection = null; + return; + } + + ipnetworkCollection = new IPNetworkCollection(network, cidr); + return; + } + + + + #endregion + + #region Supernet + + /// <summary> + /// Supernet two consecutive cidr equal subnet into a single one + /// 192.168.0.0/24 + 192.168.1.0/24 = 192.168.0.0/23 + /// 10.1.0.0/16 + 10.0.0.0/16 = 10.0.0.0/15 + /// 192.168.0.0/24 + 192.168.0.0/25 = 192.168.0.0/24 + /// </summary> + /// <param name="network2"></param> + /// <returns></returns> + public IPNetwork Supernet(IPNetwork network2) + { + IPNetwork supernet = null; + IPNetwork.InternalSupernet(false, this, network2, out supernet); + return supernet; + } + + [Obsolete("static Supernet is deprecated, please use instance Supernet.")] + public static IPNetwork Supernet(IPNetwork network, IPNetwork network2) + { + return network.Supernet(network2); + } + + /// <summary> + /// Try to supernet two consecutive cidr equal subnet into a single one + /// 192.168.0.0/24 + 192.168.1.0/24 = 192.168.0.0/23 + /// 10.1.0.0/16 + 10.0.0.0/16 = 10.0.0.0/15 + /// 192.168.0.0/24 + 192.168.0.0/25 = 192.168.0.0/24 + /// </summary> + /// <param name="network2"></param> + /// <returns></returns> + public bool TrySupernet(IPNetwork network2, out IPNetwork supernet) + { + + IPNetwork outSupernet = null; + IPNetwork.InternalSupernet(true, this, network2, out outSupernet); + bool parsed = (outSupernet != null); + supernet = outSupernet; + return parsed; + } + + [Obsolete("static TrySupernet is deprecated, please use instance TrySupernet.")] + public static bool TrySupernet(IPNetwork network, IPNetwork network2, out IPNetwork supernet) + { + if (network == null) + { + throw new ArgumentNullException("network"); + } + return network.TrySupernet(network2, out supernet); + } + +#if TRAVISCI + public +#else + internal +#endif + static void InternalSupernet(bool trySupernet, IPNetwork network1, IPNetwork network2, out IPNetwork supernet) + { + + if (network1 == null) + { + if (trySupernet == false) + { + throw new ArgumentNullException("network1"); + } + supernet = null; + return; + } + + if (network2 == null) + { + if (trySupernet == false) + { + throw new ArgumentNullException("network2"); + } + supernet = null; + return; + } + + + if (network1.Contains(network2)) + { + supernet = new IPNetwork(network1._network, network1._family, network1.Cidr); + return; + } + + if (network2.Contains(network1)) + { + supernet = new IPNetwork(network2._network, network2._family, network2.Cidr); + return; + } + + if (network1._cidr != network2._cidr) + { + if (trySupernet == false) + { + throw new ArgumentException("cidr"); + } + supernet = null; + return; + } + + IPNetwork first = (network1._network < network2._network) ? network1 : network2; + IPNetwork last = (network1._network > network2._network) ? network1 : network2; + + /// Starting from here : + /// network1 and network2 have the same cidr, + /// network1 does not contain network2, + /// network2 does not contain network1, + /// first is the lower subnet + /// last is the higher subnet + + + if ((first._broadcast + 1) != last._network) + { + if (trySupernet == false) + { + throw new ArgumentOutOfRangeException("network"); + } + supernet = null; + return; + } + + BigInteger uintSupernet = first._network; + byte cidrSupernet = (byte)(first._cidr - 1); + + IPNetwork networkSupernet = new IPNetwork(uintSupernet, first._family, cidrSupernet); + if (networkSupernet._network != first._network) + { + if (trySupernet == false) + { + throw new ArgumentException("network"); + } + supernet = null; + return; + } + supernet = networkSupernet; + return; + } + + #endregion + + #region GetHashCode + + public override int GetHashCode() + { + return string.Format("{0}|{1}|{2}", + this._ipaddress.GetHashCode(), + this._network.GetHashCode(), + this._cidr.GetHashCode()).GetHashCode(); + } + + #endregion + + #region SupernetArray + + /// <summary> + /// Supernet a list of subnet + /// 192.168.0.0/24 + 192.168.1.0/24 = 192.168.0.0/23 + /// 192.168.0.0/24 + 192.168.1.0/24 + 192.168.2.0/24 + 192.168.3.0/24 = 192.168.0.0/22 + /// </summary> + /// <param name="ipnetworks"></param> + /// <param name="supernet"></param> + /// <returns></returns> + public static IPNetwork[] Supernet(IPNetwork[] ipnetworks) + { + IPNetwork[] supernet; + InternalSupernet(false, ipnetworks, out supernet); + return supernet; + } + + /// <summary> + /// Supernet a list of subnet + /// 192.168.0.0/24 + 192.168.1.0/24 = 192.168.0.0/23 + /// 192.168.0.0/24 + 192.168.1.0/24 + 192.168.2.0/24 + 192.168.3.0/24 = 192.168.0.0/22 + /// </summary> + /// <param name="ipnetworks"></param> + /// <param name="supernet"></param> + /// <returns></returns> + public static bool TrySupernet(IPNetwork[] ipnetworks, out IPNetwork[] supernet) + { + bool supernetted = InternalSupernet(true, ipnetworks, out supernet); + return supernetted; + + } + +#if TRAVISCI + public +#else + internal +#endif + static bool InternalSupernet(bool trySupernet, IPNetwork[] ipnetworks, out IPNetwork[] supernet) + { + + if (ipnetworks == null) + { + if (trySupernet == false) + { + throw new ArgumentNullException("ipnetworks"); + } + supernet = null; + return false; + } + + if (ipnetworks.Length <= 0) + { + supernet = new IPNetwork[0]; + return true; + } + + List<IPNetwork> supernetted = new List<IPNetwork>(); + List<IPNetwork> ipns = IPNetwork.Array2List(ipnetworks); + Stack<IPNetwork> current = IPNetwork.List2Stack(ipns); + int previousCount = 0; + int currentCount = current.Count; + + while (previousCount != currentCount) + { + + supernetted.Clear(); + while (current.Count > 1) + { + IPNetwork ipn1 = current.Pop(); + IPNetwork ipn2 = current.Peek(); + + IPNetwork outNetwork = null; + bool success = ipn1.TrySupernet(ipn2, out outNetwork); + if (success) + { + current.Pop(); + current.Push(outNetwork); + } + else + { + supernetted.Add(ipn1); + } + } + if (current.Count == 1) + { + supernetted.Add(current.Pop()); + } + + previousCount = currentCount; + currentCount = supernetted.Count; + current = IPNetwork.List2Stack(supernetted); + + } + supernet = supernetted.ToArray(); + return true; + } + + private static Stack<IPNetwork> List2Stack(List<IPNetwork> list) + { + Stack<IPNetwork> stack = new Stack<IPNetwork>(); + list.ForEach(new Action<IPNetwork>( + delegate (IPNetwork ipn) + { + stack.Push(ipn); + } + )); + return stack; + } + + private static List<IPNetwork> Array2List(IPNetwork[] array) + { + List<IPNetwork> ipns = new List<IPNetwork>(); + ipns.AddRange(array); + IPNetwork.RemoveNull(ipns); + ipns.Sort(new Comparison<IPNetwork>( + delegate (IPNetwork ipn1, IPNetwork ipn2) + { + int networkCompare = ipn1._network.CompareTo(ipn2._network); + if (networkCompare == 0) + { + int cidrCompare = ipn1._cidr.CompareTo(ipn2._cidr); + return cidrCompare; + } + return networkCompare; + } + )); + ipns.Reverse(); + + return ipns; + } + + private static void RemoveNull(List<IPNetwork> ipns) + { + ipns.RemoveAll(new Predicate<IPNetwork>( + delegate (IPNetwork ipn) + { + if (ipn == null) + { + return true; + } + return false; + } + )); + + } + + #endregion + + #region WideSubnet + + public static IPNetwork WideSubnet(string start, string end) + { + + if (string.IsNullOrEmpty(start)) + { + throw new ArgumentNullException("start"); + } + + if (string.IsNullOrEmpty(end)) + { + throw new ArgumentNullException("end"); + } + + IPAddress startIP; + if (!IPAddress.TryParse(start, out startIP)) + { + throw new ArgumentException("start"); + } + + IPAddress endIP; + if (!IPAddress.TryParse(end, out endIP)) + { + throw new ArgumentException("end"); + } + + if (startIP.AddressFamily != endIP.AddressFamily) + { + throw new NotSupportedException("MixedAddressFamily"); + } + + IPNetwork ipnetwork = new IPNetwork(0, startIP.AddressFamily, 0); + for (byte cidr = 32; cidr >= 0; cidr--) + { + IPNetwork wideSubnet = IPNetwork.Parse(start, cidr); + if (wideSubnet.Contains(endIP)) + { + ipnetwork = wideSubnet; + break; + } + } + return ipnetwork; + + } + + public static bool TryWideSubnet(IPNetwork[] ipnetworks, out IPNetwork ipnetwork) + { + IPNetwork ipn = null; + IPNetwork.InternalWideSubnet(true, ipnetworks, out ipn); + if (ipn == null) + { + ipnetwork = null; + return false; + } + ipnetwork = ipn; + return true; + } + + public static IPNetwork WideSubnet(IPNetwork[] ipnetworks) + { + IPNetwork ipn = null; + IPNetwork.InternalWideSubnet(false, ipnetworks, out ipn); + return ipn; + } + + internal static void InternalWideSubnet(bool tryWide, IPNetwork[] ipnetworks, out IPNetwork ipnetwork) + { + + if (ipnetworks == null) + { + if (tryWide == false) + { + throw new ArgumentNullException("ipnetworks"); + } + ipnetwork = null; + return; + } + + + IPNetwork[] nnin = Array.FindAll<IPNetwork>(ipnetworks, new Predicate<IPNetwork>( + delegate (IPNetwork ipnet) { + return ipnet != null; + } + )); + + if (nnin.Length <= 0) + { + if (tryWide == false) + { + throw new ArgumentException("ipnetworks"); + } + ipnetwork = null; + return; + } + + if (nnin.Length == 1) + { + IPNetwork ipn0 = nnin[0]; + ipnetwork = ipn0; + return; + } + + Array.Sort<IPNetwork>(nnin); + IPNetwork nnin0 = nnin[0]; + BigInteger uintNnin0 = nnin0._ipaddress; + + IPNetwork nninX = nnin[nnin.Length - 1]; + IPAddress ipaddressX = nninX.Broadcast; + + AddressFamily family = ipnetworks[0]._family; + foreach (var ipnx in ipnetworks) + { + if (ipnx._family != family) + { + throw new ArgumentException("MixedAddressFamily"); + } + } + + IPNetwork ipn = new IPNetwork(0, family, 0); + for (byte cidr = nnin0._cidr; cidr >= 0; cidr--) + { + IPNetwork wideSubnet = new IPNetwork(uintNnin0, family, cidr); + if (wideSubnet.Contains(ipaddressX)) + { + ipn = wideSubnet; + break; + } + } + + ipnetwork = ipn; + return; + } + + #endregion + + #region Print + + /// <summary> + /// Print an ipnetwork in a clear representation string + /// </summary> + /// <returns></returns> + public string Print() + { + + StringWriter sw = new StringWriter(); + + sw.WriteLine("IPNetwork : {0}", ToString()); + sw.WriteLine("Network : {0}", Network); + sw.WriteLine("Netmask : {0}", Netmask); + sw.WriteLine("Cidr : {0}", Cidr); + sw.WriteLine("Broadcast : {0}", Broadcast); + sw.WriteLine("FirstUsable : {0}", FirstUsable); + sw.WriteLine("LastUsable : {0}", LastUsable); + sw.WriteLine("Usable : {0}", Usable); + + return sw.ToString(); + } + + [Obsolete("static Print is deprecated, please use instance Print.")] + public static string Print(IPNetwork ipnetwork) + { + + if (ipnetwork == null) + { + throw new ArgumentNullException("ipnetwork"); + } + + return ipnetwork.Print(); + } + + #endregion + + #region TryGuessCidr + + /// <summary> + /// + /// Class Leading bits Default netmask + /// A (CIDR /8) 00 255.0.0.0 + /// A (CIDR /8) 01 255.0.0.0 + /// B (CIDR /16) 10 255.255.0.0 + /// C (CIDR /24) 11 255.255.255.0 + /// + /// </summary> + /// <param name="ip"></param> + /// <param name="cidr"></param> + /// <returns></returns> + public static bool TryGuessCidr(string ip, out byte cidr) + { + + IPAddress ipaddress = null; + bool parsed = IPAddress.TryParse(string.Format("{0}", ip), out ipaddress); + if (parsed == false) + { + cidr = 0; + return false; + } + + if (ipaddress.AddressFamily == AddressFamily.InterNetworkV6) + { + cidr = 64; + return true; + } + BigInteger uintIPAddress = IPNetwork.ToBigInteger(ipaddress); + uintIPAddress = uintIPAddress >> 29; + if (uintIPAddress <= 3) + { + cidr = 8; + return true; + } + else if (uintIPAddress <= 5) + { + cidr = 16; + return true; + } + else if (uintIPAddress <= 6) + { + cidr = 24; + return true; + } + + cidr = 0; + return false; + + } + + /// <summary> + /// Try to parse cidr. Have to be >= 0 and <= 32 or 128 + /// </summary> + /// <param name="sidr"></param> + /// <param name="cidr"></param> + /// <returns></returns> + public static bool TryParseCidr(string sidr, AddressFamily family, out byte? cidr) + { + + byte b = 0; + if (!byte.TryParse(sidr, out b)) + { + cidr = null; + return false; + } + + IPAddress netmask = null; + if (!IPNetwork.TryToNetmask(b, family, out netmask)) + { + cidr = null; + return false; + } + + cidr = b; + return true; + } + + #endregion + + #region ListIPAddress + + [Obsolete("static ListIPAddress is deprecated, please use instance ListIPAddress.")] + public static IPAddressCollection ListIPAddress(IPNetwork ipnetwork) + { + return ipnetwork.ListIPAddress(); + } + + public IPAddressCollection ListIPAddress() + { + return new IPAddressCollection(this); + } + + #endregion + + /** + * Need a better way to do it + * +#region TrySubstractNetwork + + public static bool TrySubstractNetwork(IPNetwork[] ipnetworks, IPNetwork substract, out IEnumerable<IPNetwork> result) { + + if (ipnetworks == null) { + result = null; + return false; + } + if (ipnetworks.Length <= 0) { + result = null; + return false; + } + if (substract == null) { + result = null; + return false; + } + var results = new List<IPNetwork>(); + foreach (var ipn in ipnetworks) { + if (!Overlap(ipn, substract)) { + results.Add(ipn); + continue; + } + + var collection = ipn.Subnet(substract.Cidr); + var rtemp = new List<IPNetwork>(); + foreach(var subnet in collection) { + if (subnet != substract) { + rtemp.Add(subnet); + } + } + var supernets = Supernet(rtemp.ToArray()); + results.AddRange(supernets); + } + result = results; + return true; + } +#endregion + * **/ + + #region IComparable<IPNetwork> Members + + public static Int32 Compare(IPNetwork left, IPNetwork right) + { + // two null IPNetworks are equal + if (ReferenceEquals(left, null) && ReferenceEquals(right, null)) return 0; + + // two same IPNetworks are equal + if (ReferenceEquals(left, right)) return 0; + + // null is always sorted first + if (ReferenceEquals(left, null)) return -1; + if (ReferenceEquals(right, null)) return 1; + + // first test the network + var result = left._network.CompareTo(right._network); + if (result != 0) return result; + + // then test the cidr + result = left._cidr.CompareTo(right._cidr); + return result; + } + + public Int32 CompareTo(IPNetwork other) + { + return Compare(this, other); + } + + public Int32 CompareTo(Object obj) + { + // null is at less + if (obj == null) return 1; + + // convert to a proper Cidr object + var other = obj as IPNetwork; + + // type problem if null + if (other == null) + { + throw new ArgumentException( + "The supplied parameter is an invalid type. Please supply an IPNetwork type.", + "obj"); + } + + // perform the comparision + return CompareTo(other); + } + + #endregion + + #region IEquatable<IPNetwork> Members + + public static Boolean Equals(IPNetwork left, IPNetwork right) + { + return Compare(left, right) == 0; + } + + public Boolean Equals(IPNetwork other) + { + return Equals(this, other); + } + + public override Boolean Equals(Object obj) + { + return Equals(this, obj as IPNetwork); + } + + #endregion + + #region Operators + + public static Boolean operator ==(IPNetwork left, IPNetwork right) + { + return Equals(left, right); + } + + public static Boolean operator !=(IPNetwork left, IPNetwork right) + { + return !Equals(left, right); + } + + public static Boolean operator <(IPNetwork left, IPNetwork right) + { + return Compare(left, right) < 0; + } + + public static Boolean operator >(IPNetwork left, IPNetwork right) + { + return Compare(left, right) > 0; + } + + #endregion + + } +}
\ No newline at end of file diff --git a/Emby.Server.Implementations/Networking/IPNetwork/IPNetworkCollection.cs b/Emby.Server.Implementations/Networking/IPNetwork/IPNetworkCollection.cs new file mode 100644 index 000000000..35cff88dc --- /dev/null +++ b/Emby.Server.Implementations/Networking/IPNetwork/IPNetworkCollection.cs @@ -0,0 +1,144 @@ +using System.Collections; +using System.Collections.Generic; +using System.Numerics; + +namespace System.Net +{ + public class IPNetworkCollection : IEnumerable<IPNetwork>, IEnumerator<IPNetwork> + { + + private BigInteger _enumerator; + private byte _cidrSubnet; + private IPNetwork _ipnetwork; + + private byte _cidr + { + get { return this._ipnetwork.Cidr; } + } + private BigInteger _broadcast + { + get { return IPNetwork.ToBigInteger(this._ipnetwork.Broadcast); } + } + private BigInteger _lastUsable + { + get { return IPNetwork.ToBigInteger(this._ipnetwork.LastUsable); } + } + private BigInteger _network + { + get { return IPNetwork.ToBigInteger(this._ipnetwork.Network); } + } +#if TRAVISCI + public +#else + internal +#endif + IPNetworkCollection(IPNetwork ipnetwork, byte cidrSubnet) + { + + int maxCidr = ipnetwork.AddressFamily == Sockets.AddressFamily.InterNetwork ? 32 : 128; + if (cidrSubnet > maxCidr) + { + throw new ArgumentOutOfRangeException("cidrSubnet"); + } + + if (cidrSubnet < ipnetwork.Cidr) + { + throw new ArgumentException("cidr"); + } + + this._cidrSubnet = cidrSubnet; + this._ipnetwork = ipnetwork; + this._enumerator = -1; + } + + #region Count, Array, Enumerator + + public BigInteger Count + { + get + { + BigInteger count = BigInteger.Pow(2, this._cidrSubnet - this._cidr); + return count; + } + } + + public IPNetwork this[BigInteger i] + { + get + { + if (i >= this.Count) + { + throw new ArgumentOutOfRangeException("i"); + } + + BigInteger last = this._ipnetwork.AddressFamily == Sockets.AddressFamily.InterNetworkV6 + ? this._lastUsable : this._broadcast; + BigInteger increment = (last - this._network) / this.Count; + BigInteger uintNetwork = this._network + ((increment + 1) * i); + IPNetwork ipn = new IPNetwork(uintNetwork, this._ipnetwork.AddressFamily, this._cidrSubnet); + return ipn; + } + } + + #endregion + + #region IEnumerable Members + + IEnumerator<IPNetwork> IEnumerable<IPNetwork>.GetEnumerator() + { + return this; + } + + IEnumerator IEnumerable.GetEnumerator() + { + return this; + } + + #region IEnumerator<IPNetwork> Members + + public IPNetwork Current + { + get { return this[this._enumerator]; } + } + + #endregion + + #region IDisposable Members + + public void Dispose() + { + // nothing to dispose + return; + } + + #endregion + + #region IEnumerator Members + + object IEnumerator.Current + { + get { return this.Current; } + } + + public bool MoveNext() + { + this._enumerator++; + if (this._enumerator >= this.Count) + { + return false; + } + return true; + + } + + public void Reset() + { + this._enumerator = -1; + } + + #endregion + + #endregion + + } +}
\ No newline at end of file diff --git a/Emby.Server.Implementations/Networking/IPNetwork/LICENSE.txt b/Emby.Server.Implementations/Networking/IPNetwork/LICENSE.txt new file mode 100644 index 000000000..45d7392ac --- /dev/null +++ b/Emby.Server.Implementations/Networking/IPNetwork/LICENSE.txt @@ -0,0 +1,24 @@ +Copyright (c) 2015, lduchosal +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + diff --git a/Emby.Server.Implementations/Networking/NetworkManager.cs b/Emby.Server.Implementations/Networking/NetworkManager.cs index 60da8a012..20abaf27c 100644 --- a/Emby.Server.Implementations/Networking/NetworkManager.cs +++ b/Emby.Server.Implementations/Networking/NetworkManager.cs @@ -11,6 +11,8 @@ using MediaBrowser.Model.Extensions; using MediaBrowser.Model.IO; using MediaBrowser.Model.Logging; using MediaBrowser.Model.Net; +using MediaBrowser.Model.System; +using System.Numerics; namespace Emby.Server.Implementations.Networking { @@ -19,27 +21,32 @@ namespace Emby.Server.Implementations.Networking protected ILogger Logger { get; private set; } public event EventHandler NetworkChanged; + public Func<string[]> LocalSubnetsFn { get; set; } - public NetworkManager(ILogger logger) + public NetworkManager(ILogger logger, IEnvironmentInfo environment) { Logger = logger; - try - { - NetworkChange.NetworkAddressChanged += NetworkChange_NetworkAddressChanged; - } - catch (Exception ex) + // In FreeBSD these events cause a crash + if (environment.OperatingSystem != MediaBrowser.Model.System.OperatingSystem.BSD) { - Logger.ErrorException("Error binding to NetworkAddressChanged event", ex); - } + try + { + NetworkChange.NetworkAddressChanged += NetworkChange_NetworkAddressChanged; + } + catch (Exception ex) + { + Logger.ErrorException("Error binding to NetworkAddressChanged event", ex); + } - try - { - NetworkChange.NetworkAvailabilityChanged += NetworkChange_NetworkAvailabilityChanged; - } - catch (Exception ex) - { - Logger.ErrorException("Error binding to NetworkChange_NetworkAvailabilityChanged event", ex); + try + { + NetworkChange.NetworkAvailabilityChanged += NetworkChange_NetworkAvailabilityChanged; + } + catch (Exception ex) + { + Logger.ErrorException("Error binding to NetworkChange_NetworkAvailabilityChanged event", ex); + } } } @@ -60,6 +67,7 @@ namespace Emby.Server.Implementations.Networking lock (_localIpAddressSyncLock) { _localIpAddresses = null; + _macAddresses = null; } if (NetworkChanged != null) { @@ -67,16 +75,16 @@ namespace Emby.Server.Implementations.Networking } } - private List<IpAddressInfo> _localIpAddresses; + private IpAddressInfo[] _localIpAddresses; private readonly object _localIpAddressSyncLock = new object(); - public List<IpAddressInfo> GetLocalIpAddresses() + public IpAddressInfo[] GetLocalIpAddresses() { lock (_localIpAddressSyncLock) { if (_localIpAddresses == null) { - var addresses = GetLocalIpAddressesInternal().Result.Select(ToIpAddressInfo).ToList(); + var addresses = GetLocalIpAddressesInternal().Result.Select(ToIpAddressInfo).ToArray(); _localIpAddresses = addresses; @@ -120,6 +128,11 @@ namespace Emby.Server.Implementations.Networking public bool IsInPrivateAddressSpace(string endpoint) { + return IsInPrivateAddressSpace(endpoint, true); + } + + private bool IsInPrivateAddressSpace(string endpoint, bool checkSubnets) + { if (string.Equals(endpoint, "::1", StringComparison.OrdinalIgnoreCase)) { return true; @@ -146,12 +159,24 @@ namespace Emby.Server.Implementations.Networking return Is172AddressPrivate(endpoint); } - return endpoint.StartsWith("localhost", StringComparison.OrdinalIgnoreCase) || + if (endpoint.StartsWith("localhost", StringComparison.OrdinalIgnoreCase) || endpoint.StartsWith("127.", StringComparison.OrdinalIgnoreCase) || - endpoint.StartsWith("192.168", StringComparison.OrdinalIgnoreCase) || - endpoint.StartsWith("169.", StringComparison.OrdinalIgnoreCase) || - //endpoint.StartsWith("10.", StringComparison.OrdinalIgnoreCase) || - IsInPrivateAddressSpaceAndLocalSubnet(endpoint); + endpoint.StartsWith("169.", StringComparison.OrdinalIgnoreCase)) + { + return true; + } + + if (checkSubnets && endpoint.StartsWith("192.168", StringComparison.OrdinalIgnoreCase)) + { + return true; + } + + if (checkSubnets && IsInPrivateAddressSpaceAndLocalSubnet(endpoint)) + { + return true; + } + + return false; } public bool IsInPrivateAddressSpaceAndLocalSubnet(string endpoint) @@ -238,9 +263,38 @@ namespace Emby.Server.Implementations.Networking return IsInLocalNetworkInternal(endpoint, true); } - public bool IsInLocalNetworkInternal(string endpoint, bool resolveHost) + public bool IsAddressInSubnets(string addressString, string[] subnets) { - if (string.IsNullOrWhiteSpace(endpoint)) + return IsAddressInSubnets(IPAddress.Parse(addressString), addressString, subnets); + } + + private bool IsAddressInSubnets(IPAddress address, string addressString, string[] subnets) + { + foreach (var subnet in subnets) + { + var normalizedSubnet = subnet.Trim(); + + if (string.Equals(normalizedSubnet, addressString, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + + if (normalizedSubnet.IndexOf('/') != -1) + { + var ipnetwork = IPNetwork.Parse(normalizedSubnet); + if (ipnetwork.Contains(address)) + { + return true; + } + } + } + + return false; + } + + private bool IsInLocalNetworkInternal(string endpoint, bool resolveHost) + { + if (string.IsNullOrEmpty(endpoint)) { throw new ArgumentNullException("endpoint"); } @@ -250,11 +304,25 @@ namespace Emby.Server.Implementations.Networking { var addressString = address.ToString(); + var localSubnetsFn = LocalSubnetsFn; + if (localSubnetsFn != null) + { + var localSubnets = localSubnetsFn(); + foreach (var subnet in localSubnets) + { + // only validate if there's at least one valid entry + if (!string.IsNullOrWhiteSpace(subnet)) + { + return IsAddressInSubnets(address, addressString, localSubnets) || IsInPrivateAddressSpace(addressString, false); + } + } + } + int lengthMatch = 100; if (address.AddressFamily == AddressFamily.InterNetwork) { lengthMatch = 4; - if (IsInPrivateAddressSpace(addressString)) + if (IsInPrivateAddressSpace(addressString, true)) { return true; } @@ -262,7 +330,7 @@ namespace Emby.Server.Implementations.Networking else if (address.AddressFamily == AddressFamily.InterNetworkV6) { lengthMatch = 9; - if (IsInPrivateAddressSpace(endpoint)) + if (IsInPrivateAddressSpace(endpoint, true)) { return true; } @@ -353,13 +421,7 @@ namespace Emby.Server.Implementations.Networking return new List<IPAddress>(); } - //if (!_validNetworkInterfaceTypes.Contains(network.NetworkInterfaceType)) - //{ - // return new List<IPAddress>(); - //} - return ipProperties.UnicastAddresses - //.Where(i => i.IsDnsEligible) .Select(i => i.Address) .Where(i => i.AddressFamily == AddressFamily.InterNetwork || i.AddressFamily == AddressFamily.InterNetworkV6) .ToList(); @@ -408,16 +470,40 @@ namespace Emby.Server.Implementations.Networking } } - /// <summary> - /// Returns MAC Address from first Network Card in Computer - /// </summary> - /// <returns>[string] MAC Address</returns> - public string GetMacAddress() + private List<string> _macAddresses; + public List<string> GetMacAddresses() + { + if (_macAddresses == null) + { + _macAddresses = GetMacAddressesInternal(); + } + return _macAddresses; + } + + private List<string> GetMacAddressesInternal() { return NetworkInterface.GetAllNetworkInterfaces() .Where(i => i.NetworkInterfaceType != NetworkInterfaceType.Loopback) - .Select(i => BitConverter.ToString(i.GetPhysicalAddress().GetAddressBytes())) - .FirstOrDefault(); + .Select(i => + { + try + { + var physicalAddress = i.GetPhysicalAddress(); + + if (physicalAddress == null) + { + return null; + } + + return physicalAddress.ToString(); + } + catch (Exception ex) + { + return null; + } + }) + .Where(i => i != null) + .ToList(); } /// <summary> diff --git a/Emby.Server.Implementations/News/NewsEntryPoint.cs b/Emby.Server.Implementations/News/NewsEntryPoint.cs index 74366233c..3ce3d2315 100644 --- a/Emby.Server.Implementations/News/NewsEntryPoint.cs +++ b/Emby.Server.Implementations/News/NewsEntryPoint.cs @@ -82,7 +82,7 @@ namespace Emby.Server.Implementations.News var requestOptions = new HttpRequestOptions { - Url = "http://emby.media/community/index.php?/blog/rss/1-media-browser-developers-blog", + Url = "https://emby.media/community/index.php?/blog/rss/1-media-browser-developers-blog", Progress = new SimpleProgress<double>(), UserAgent = "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.42 Safari/537.36", BufferContent = false @@ -118,7 +118,7 @@ namespace Emby.Server.Implementations.News Name = i.Title, Description = i.Description, Url = i.Link, - UserIds = _userManager.Users.Select(u => u.Id.ToString("N")).ToList() + UserIds = _userManager.Users.Select(u => u.Id).ToArray() }, cancellationToken)); @@ -274,7 +274,6 @@ namespace Emby.Server.Implementations.News _timer.Dispose(); _timer = null; } - GC.SuppressFinalize(this); } } } diff --git a/Emby.Server.Implementations/Notifications/CoreNotificationTypes.cs b/Emby.Server.Implementations/Notifications/CoreNotificationTypes.cs deleted file mode 100644 index b00b5d43b..000000000 --- a/Emby.Server.Implementations/Notifications/CoreNotificationTypes.cs +++ /dev/null @@ -1,198 +0,0 @@ -using MediaBrowser.Controller; -using MediaBrowser.Controller.Notifications; -using MediaBrowser.Model.Notifications; -using System; -using System.Collections.Generic; -using System.Linq; -using MediaBrowser.Model.Globalization; - -namespace Emby.Server.Implementations.Notifications -{ - public class CoreNotificationTypes : INotificationTypeFactory - { - private readonly ILocalizationManager _localization; - private readonly IServerApplicationHost _appHost; - - public CoreNotificationTypes(ILocalizationManager localization, IServerApplicationHost appHost) - { - _localization = localization; - _appHost = appHost; - } - - public IEnumerable<NotificationTypeInfo> GetNotificationTypes() - { - var knownTypes = new List<NotificationTypeInfo> - { - new NotificationTypeInfo - { - Type = NotificationType.ApplicationUpdateInstalled.ToString(), - DefaultDescription = "{ReleaseNotes}", - DefaultTitle = "A new version of Emby Server has been installed.", - Variables = new string[]{"Version"} - }, - - new NotificationTypeInfo - { - Type = NotificationType.InstallationFailed.ToString(), - DefaultTitle = "{Name} installation failed.", - Variables = new string[]{"Name", "Version"} - }, - - new NotificationTypeInfo - { - Type = NotificationType.PluginInstalled.ToString(), - DefaultTitle = "{Name} was installed.", - Variables = new string[]{"Name", "Version"} - }, - - new NotificationTypeInfo - { - Type = NotificationType.PluginError.ToString(), - DefaultTitle = "{Name} has encountered an error.", - DefaultDescription = "{ErrorMessage}", - Variables = new string[]{"Name", "ErrorMessage"} - }, - - new NotificationTypeInfo - { - Type = NotificationType.PluginUninstalled.ToString(), - DefaultTitle = "{Name} was uninstalled.", - Variables = new string[]{"Name", "Version"} - }, - - new NotificationTypeInfo - { - Type = NotificationType.PluginUpdateInstalled.ToString(), - DefaultTitle = "{Name} was updated.", - DefaultDescription = "{ReleaseNotes}", - Variables = new string[]{"Name", "ReleaseNotes", "Version"} - }, - - new NotificationTypeInfo - { - Type = NotificationType.ServerRestartRequired.ToString(), - DefaultTitle = "Please restart Emby Server to finish updating." - }, - - new NotificationTypeInfo - { - Type = NotificationType.TaskFailed.ToString(), - DefaultTitle = "{Name} failed.", - DefaultDescription = "{ErrorMessage}", - Variables = new string[]{"Name", "ErrorMessage"} - }, - - new NotificationTypeInfo - { - Type = NotificationType.NewLibraryContent.ToString(), - DefaultTitle = "{Name} has been added to your media library.", - Variables = new string[]{"Name"} - }, - - new NotificationTypeInfo - { - Type = NotificationType.AudioPlayback.ToString(), - DefaultTitle = "{UserName} is playing {ItemName} on {DeviceName}.", - Variables = new string[]{"UserName", "ItemName", "DeviceName", "AppName"} - }, - - new NotificationTypeInfo - { - Type = NotificationType.GamePlayback.ToString(), - DefaultTitle = "{UserName} is playing {ItemName} on {DeviceName}.", - Variables = new string[]{"UserName", "ItemName", "DeviceName", "AppName"} - }, - - new NotificationTypeInfo - { - Type = NotificationType.VideoPlayback.ToString(), - DefaultTitle = "{UserName} is playing {ItemName} on {DeviceName}.", - Variables = new string[]{"UserName", "ItemName", "DeviceName", "AppName"} - }, - - new NotificationTypeInfo - { - Type = NotificationType.AudioPlaybackStopped.ToString(), - DefaultTitle = "{UserName} has finished playing {ItemName} on {DeviceName}.", - Variables = new string[]{"UserName", "ItemName", "DeviceName", "AppName"} - }, - - new NotificationTypeInfo - { - Type = NotificationType.GamePlaybackStopped.ToString(), - DefaultTitle = "{UserName} has finished playing {ItemName} on {DeviceName}.", - Variables = new string[]{"UserName", "ItemName", "DeviceName", "AppName"} - }, - - new NotificationTypeInfo - { - Type = NotificationType.VideoPlaybackStopped.ToString(), - DefaultTitle = "{UserName} has finished playing {ItemName} on {DeviceName}.", - Variables = new string[]{"UserName", "ItemName", "DeviceName", "AppName"} - }, - - new NotificationTypeInfo - { - Type = NotificationType.CameraImageUploaded.ToString(), - DefaultTitle = "A new camera image has been uploaded from {DeviceName}.", - Variables = new string[]{"DeviceName"} - }, - - new NotificationTypeInfo - { - Type = NotificationType.UserLockedOut.ToString(), - DefaultTitle = "{UserName} has been locked out.", - Variables = new string[]{"UserName"} - } - }; - - if (!_appHost.CanSelfUpdate) - { - knownTypes.Add(new NotificationTypeInfo - { - Type = NotificationType.ApplicationUpdateAvailable.ToString(), - DefaultTitle = "A new version of Emby Server is available for download." - }); - } - - foreach (var type in knownTypes) - { - Update(type); - } - - var systemName = _localization.GetLocalizedString("System"); - - return knownTypes.OrderByDescending(i => string.Equals(i.Category, systemName, StringComparison.OrdinalIgnoreCase)) - .ThenBy(i => i.Category) - .ThenBy(i => i.Name); - } - - private void Update(NotificationTypeInfo note) - { - note.Name = _localization.GetLocalizedString("NotificationOption" + note.Type) ?? note.Type; - - note.IsBasedOnUserEvent = note.Type.IndexOf("Playback", StringComparison.OrdinalIgnoreCase) != -1; - - if (note.Type.IndexOf("Playback", StringComparison.OrdinalIgnoreCase) != -1) - { - note.Category = _localization.GetLocalizedString("User"); - } - else if (note.Type.IndexOf("Plugin", StringComparison.OrdinalIgnoreCase) != -1) - { - note.Category = _localization.GetLocalizedString("Plugin"); - } - else if (note.Type.IndexOf("CameraImageUploaded", StringComparison.OrdinalIgnoreCase) != -1) - { - note.Category = _localization.GetLocalizedString("Sync"); - } - else if (note.Type.IndexOf("UserLockedOut", StringComparison.OrdinalIgnoreCase) != -1) - { - note.Category = _localization.GetLocalizedString("User"); - } - else - { - note.Category = _localization.GetLocalizedString("System"); - } - } - } -} diff --git a/Emby.Server.Implementations/Notifications/IConfigurableNotificationService.cs b/Emby.Server.Implementations/Notifications/IConfigurableNotificationService.cs deleted file mode 100644 index d74667c48..000000000 --- a/Emby.Server.Implementations/Notifications/IConfigurableNotificationService.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Emby.Server.Implementations.Notifications -{ - public interface IConfigurableNotificationService - { - bool IsHidden { get; } - bool IsEnabled(string notificationType); - } -} diff --git a/Emby.Server.Implementations/Notifications/InternalNotificationService.cs b/Emby.Server.Implementations/Notifications/InternalNotificationService.cs deleted file mode 100644 index 61c564f18..000000000 --- a/Emby.Server.Implementations/Notifications/InternalNotificationService.cs +++ /dev/null @@ -1,61 +0,0 @@ -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Notifications; -using MediaBrowser.Model.Notifications; -using System.Threading; -using System.Threading.Tasks; -using System; - -namespace Emby.Server.Implementations.Notifications -{ - public class InternalNotificationService : INotificationService, IConfigurableNotificationService - { - private readonly INotificationsRepository _repo; - - public InternalNotificationService(INotificationsRepository repo) - { - _repo = repo; - } - - public string Name - { - get { return "Dashboard Notifications"; } - } - - public Task SendNotification(UserNotification request, CancellationToken cancellationToken) - { - return _repo.AddNotification(new Notification - { - Date = request.Date, - Description = request.Description, - Level = request.Level, - Name = request.Name, - Url = request.Url, - UserId = request.User.Id.ToString("N") - - }, cancellationToken); - } - - public bool IsEnabledForUser(User user) - { - return user.Policy.IsAdministrator; - } - - public bool IsHidden - { - get { return true; } - } - - public bool IsEnabled(string notificationType) - { - if (notificationType.IndexOf("playback", StringComparison.OrdinalIgnoreCase) != -1) - { - return false; - } - if (notificationType.IndexOf("newlibrarycontent", StringComparison.OrdinalIgnoreCase) != -1) - { - return false; - } - return true; - } - } -} diff --git a/Emby.Server.Implementations/Notifications/NotificationConfigurationFactory.cs b/Emby.Server.Implementations/Notifications/NotificationConfigurationFactory.cs deleted file mode 100644 index a7c5b1233..000000000 --- a/Emby.Server.Implementations/Notifications/NotificationConfigurationFactory.cs +++ /dev/null @@ -1,21 +0,0 @@ -using MediaBrowser.Common.Configuration; -using MediaBrowser.Model.Notifications; -using System.Collections.Generic; - -namespace Emby.Server.Implementations.Notifications -{ - public class NotificationConfigurationFactory : IConfigurationFactory - { - public IEnumerable<ConfigurationStore> GetConfigurations() - { - return new List<ConfigurationStore> - { - new ConfigurationStore - { - Key = "notifications", - ConfigurationType = typeof (NotificationOptions) - } - }; - } - } -} diff --git a/Emby.Server.Implementations/Notifications/NotificationManager.cs b/Emby.Server.Implementations/Notifications/NotificationManager.cs deleted file mode 100644 index e11f2790e..000000000 --- a/Emby.Server.Implementations/Notifications/NotificationManager.cs +++ /dev/null @@ -1,302 +0,0 @@ -using MediaBrowser.Common.Configuration; -using MediaBrowser.Common.Extensions; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Notifications; -using MediaBrowser.Model.Logging; -using MediaBrowser.Model.Notifications; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Model.Extensions; - -namespace Emby.Server.Implementations.Notifications -{ - public class NotificationManager : INotificationManager - { - private readonly ILogger _logger; - private readonly IUserManager _userManager; - private readonly IServerConfigurationManager _config; - - private INotificationService[] _services; - private INotificationTypeFactory[] _typeFactories; - - public NotificationManager(ILogManager logManager, IUserManager userManager, IServerConfigurationManager config) - { - _userManager = userManager; - _config = config; - _logger = logManager.GetLogger(GetType().Name); - } - - private NotificationOptions GetConfiguration() - { - return _config.GetConfiguration<NotificationOptions>("notifications"); - } - - public Task SendNotification(NotificationRequest request, CancellationToken cancellationToken) - { - return SendNotification(request, null, cancellationToken); - } - - public Task SendNotification(NotificationRequest request, BaseItem relatedItem, CancellationToken cancellationToken) - { - var notificationType = request.NotificationType; - - var options = string.IsNullOrWhiteSpace(notificationType) ? - null : - GetConfiguration().GetOptions(notificationType); - - var users = GetUserIds(request, options) - .Select(i => _userManager.GetUserById(i)) - .Where(i => relatedItem == null || relatedItem.IsVisibleStandalone(i)) - .ToArray(); - - var title = GetTitle(request, options); - var description = GetDescription(request, options); - - var tasks = _services.Where(i => IsEnabled(i, notificationType)) - .Select(i => SendNotification(request, i, users, title, description, cancellationToken)); - - return Task.WhenAll(tasks); - } - - private Task SendNotification(NotificationRequest request, - INotificationService service, - IEnumerable<User> users, - string title, - string description, - CancellationToken cancellationToken) - { - users = users.Where(i => IsEnabledForUser(service, i)) - .ToList(); - - var tasks = users.Select(i => SendNotification(request, service, title, description, i, cancellationToken)); - - return Task.WhenAll(tasks); - } - - private IEnumerable<string> GetUserIds(NotificationRequest request, NotificationOption options) - { - if (request.SendToUserMode.HasValue) - { - switch (request.SendToUserMode.Value) - { - case SendToUserType.Admins: - return _userManager.Users.Where(i => i.Policy.IsAdministrator) - .Select(i => i.Id.ToString("N")); - case SendToUserType.All: - return _userManager.Users.Select(i => i.Id.ToString("N")); - case SendToUserType.Custom: - return request.UserIds; - default: - throw new ArgumentException("Unrecognized SendToUserMode: " + request.SendToUserMode.Value); - } - } - - if (options != null && !string.IsNullOrWhiteSpace(request.NotificationType)) - { - var config = GetConfiguration(); - - return _userManager.Users - .Where(i => config.IsEnabledToSendToUser(request.NotificationType, i.Id.ToString("N"), i.Policy)) - .Select(i => i.Id.ToString("N")); - } - - return request.UserIds; - } - - private async Task SendNotification(NotificationRequest request, - INotificationService service, - string title, - string description, - User user, - CancellationToken cancellationToken) - { - var notification = new UserNotification - { - Date = request.Date, - Description = description, - Level = request.Level, - Name = title, - Url = request.Url, - User = user - }; - - _logger.Debug("Sending notification via {0} to user {1}", service.Name, user.Name); - - try - { - await service.SendNotification(notification, cancellationToken).ConfigureAwait(false); - } - catch (Exception ex) - { - _logger.ErrorException("Error sending notification to {0}", ex, service.Name); - } - } - - private string GetTitle(NotificationRequest request, NotificationOption options) - { - var title = request.Name; - - // If empty, grab from options - if (string.IsNullOrEmpty(title)) - { - if (!string.IsNullOrEmpty(request.NotificationType)) - { - if (options != null) - { - title = options.Title; - } - } - } - - // If still empty, grab default - if (string.IsNullOrEmpty(title)) - { - if (!string.IsNullOrEmpty(request.NotificationType)) - { - var info = GetNotificationTypes().FirstOrDefault(i => string.Equals(i.Type, request.NotificationType, StringComparison.OrdinalIgnoreCase)); - - if (info != null) - { - title = info.DefaultTitle; - } - } - } - - title = title ?? string.Empty; - - foreach (var pair in request.Variables) - { - var token = "{" + pair.Key + "}"; - - title = title.Replace(token, pair.Value, StringComparison.OrdinalIgnoreCase); - } - - return title; - } - - private string GetDescription(NotificationRequest request, NotificationOption options) - { - var text = request.Description; - - // If empty, grab from options - if (string.IsNullOrEmpty(text)) - { - if (!string.IsNullOrEmpty(request.NotificationType)) - { - if (options != null) - { - text = options.Description; - } - } - } - - // If still empty, grab default - if (string.IsNullOrEmpty(text)) - { - if (!string.IsNullOrEmpty(request.NotificationType)) - { - var info = GetNotificationTypes().FirstOrDefault(i => string.Equals(i.Type, request.NotificationType, StringComparison.OrdinalIgnoreCase)); - - if (info != null) - { - text = info.DefaultDescription; - } - } - } - - text = text ?? string.Empty; - - foreach (var pair in request.Variables) - { - var token = "{" + pair.Key + "}"; - - text = text.Replace(token, pair.Value, StringComparison.OrdinalIgnoreCase); - } - - return text; - } - - private bool IsEnabledForUser(INotificationService service, User user) - { - try - { - return service.IsEnabledForUser(user); - } - catch (Exception ex) - { - _logger.ErrorException("Error in IsEnabledForUser", ex); - return false; - } - } - - private bool IsEnabled(INotificationService service, string notificationType) - { - if (string.IsNullOrEmpty(notificationType)) - { - return true; - } - - var configurable = service as IConfigurableNotificationService; - - if (configurable != null) - { - return configurable.IsEnabled(notificationType); - } - - return GetConfiguration().IsServiceEnabled(service.Name, notificationType); - } - - public void AddParts(IEnumerable<INotificationService> services, IEnumerable<INotificationTypeFactory> notificationTypeFactories) - { - _services = services.ToArray(); - _typeFactories = notificationTypeFactories.ToArray(); - } - - public List<NotificationTypeInfo> GetNotificationTypes() - { - var list = _typeFactories.Select(i => - { - try - { - return i.GetNotificationTypes().ToList(); - } - catch (Exception ex) - { - _logger.ErrorException("Error in GetNotificationTypes", ex); - return new List<NotificationTypeInfo>(); - } - - }).SelectMany(i => i).ToList(); - - var config = GetConfiguration(); - - foreach (var i in list) - { - i.Enabled = config.IsEnabled(i.Type); - } - - return list; - } - - public IEnumerable<NotificationServiceInfo> GetNotificationServices() - { - return _services.Where(i => - { - var configurable = i as IConfigurableNotificationService; - - return configurable == null || !configurable.IsHidden; - - }).Select(i => new NotificationServiceInfo - { - Name = i.Name, - Id = i.Name.GetMD5().ToString("N") - - }).OrderBy(i => i.Name); - } - } -} diff --git a/Emby.Server.Implementations/Notifications/Notifications.cs b/Emby.Server.Implementations/Notifications/Notifications.cs deleted file mode 100644 index b7e1d6559..000000000 --- a/Emby.Server.Implementations/Notifications/Notifications.cs +++ /dev/null @@ -1,566 +0,0 @@ -using MediaBrowser.Common.Configuration; -using MediaBrowser.Common.Plugins; -using MediaBrowser.Common.Updates; -using MediaBrowser.Controller; -using MediaBrowser.Controller.Devices; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Entities.Audio; -using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Notifications; -using MediaBrowser.Controller.Plugins; -using MediaBrowser.Controller.Session; -using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Events; -using MediaBrowser.Model.Logging; -using MediaBrowser.Model.Notifications; -using MediaBrowser.Model.Tasks; -using MediaBrowser.Model.Updates; -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Controller.Entities.TV; -using MediaBrowser.Model.Threading; -using MediaBrowser.Model.Dto; - -namespace Emby.Server.Implementations.Notifications -{ - /// <summary> - /// Creates notifications for various system events - /// </summary> - public class Notifications : IServerEntryPoint - { - private readonly IInstallationManager _installationManager; - private readonly IUserManager _userManager; - private readonly ILogger _logger; - - private readonly ITaskManager _taskManager; - private readonly INotificationManager _notificationManager; - - private readonly ILibraryManager _libraryManager; - private readonly ISessionManager _sessionManager; - private readonly IServerApplicationHost _appHost; - private readonly ITimerFactory _timerFactory; - - private ITimer LibraryUpdateTimer { get; set; } - private readonly object _libraryChangedSyncLock = new object(); - - private readonly IConfigurationManager _config; - private readonly IDeviceManager _deviceManager; - - public Notifications(IInstallationManager installationManager, IUserManager userManager, ILogger logger, ITaskManager taskManager, INotificationManager notificationManager, ILibraryManager libraryManager, ISessionManager sessionManager, IServerApplicationHost appHost, IConfigurationManager config, IDeviceManager deviceManager, ITimerFactory timerFactory) - { - _installationManager = installationManager; - _userManager = userManager; - _logger = logger; - _taskManager = taskManager; - _notificationManager = notificationManager; - _libraryManager = libraryManager; - _sessionManager = sessionManager; - _appHost = appHost; - _config = config; - _deviceManager = deviceManager; - _timerFactory = timerFactory; - } - - public void Run() - { - _installationManager.PluginInstalled += _installationManager_PluginInstalled; - _installationManager.PluginUpdated += _installationManager_PluginUpdated; - _installationManager.PackageInstallationFailed += _installationManager_PackageInstallationFailed; - _installationManager.PluginUninstalled += _installationManager_PluginUninstalled; - - _taskManager.TaskCompleted += _taskManager_TaskCompleted; - - _userManager.UserCreated += _userManager_UserCreated; - _libraryManager.ItemAdded += _libraryManager_ItemAdded; - _sessionManager.PlaybackStart += _sessionManager_PlaybackStart; - _sessionManager.PlaybackStopped += _sessionManager_PlaybackStopped; - _appHost.HasPendingRestartChanged += _appHost_HasPendingRestartChanged; - _appHost.HasUpdateAvailableChanged += _appHost_HasUpdateAvailableChanged; - _appHost.ApplicationUpdated += _appHost_ApplicationUpdated; - _deviceManager.CameraImageUploaded += _deviceManager_CameraImageUploaded; - - _userManager.UserLockedOut += _userManager_UserLockedOut; - } - - async void _userManager_UserLockedOut(object sender, GenericEventArgs<User> e) - { - var type = NotificationType.UserLockedOut.ToString(); - - var notification = new NotificationRequest - { - NotificationType = type - }; - - notification.Variables["UserName"] = e.Argument.Name; - - await SendNotification(notification, null).ConfigureAwait(false); - } - - async void _deviceManager_CameraImageUploaded(object sender, GenericEventArgs<CameraImageUploadInfo> e) - { - var type = NotificationType.CameraImageUploaded.ToString(); - - var notification = new NotificationRequest - { - NotificationType = type - }; - - notification.Variables["DeviceName"] = e.Argument.Device.Name; - - await SendNotification(notification, null).ConfigureAwait(false); - } - - async void _appHost_ApplicationUpdated(object sender, GenericEventArgs<PackageVersionInfo> e) - { - var type = NotificationType.ApplicationUpdateInstalled.ToString(); - - var notification = new NotificationRequest - { - NotificationType = type, - Url = e.Argument.infoUrl - }; - - notification.Variables["Version"] = e.Argument.versionStr; - notification.Variables["ReleaseNotes"] = e.Argument.description; - - await SendNotification(notification, null).ConfigureAwait(false); - } - - async void _installationManager_PluginUpdated(object sender, GenericEventArgs<Tuple<IPlugin, PackageVersionInfo>> e) - { - var type = NotificationType.PluginUpdateInstalled.ToString(); - - var installationInfo = e.Argument.Item1; - - var notification = new NotificationRequest - { - Description = e.Argument.Item2.description, - NotificationType = type - }; - - notification.Variables["Name"] = installationInfo.Name; - notification.Variables["Version"] = installationInfo.Version.ToString(); - notification.Variables["ReleaseNotes"] = e.Argument.Item2.description; - - await SendNotification(notification, null).ConfigureAwait(false); - } - - async void _installationManager_PluginInstalled(object sender, GenericEventArgs<PackageVersionInfo> e) - { - var type = NotificationType.PluginInstalled.ToString(); - - var installationInfo = e.Argument; - - var notification = new NotificationRequest - { - Description = installationInfo.description, - NotificationType = type - }; - - notification.Variables["Name"] = installationInfo.name; - notification.Variables["Version"] = installationInfo.versionStr; - - await SendNotification(notification, null).ConfigureAwait(false); - } - - async void _appHost_HasUpdateAvailableChanged(object sender, EventArgs e) - { - // This notification is for users who can't auto-update (aka running as service) - if (!_appHost.HasUpdateAvailable || _appHost.CanSelfUpdate) - { - return; - } - - var type = NotificationType.ApplicationUpdateAvailable.ToString(); - - var notification = new NotificationRequest - { - Description = "Please see emby.media for details.", - NotificationType = type - }; - - await SendNotification(notification, null).ConfigureAwait(false); - } - - async void _appHost_HasPendingRestartChanged(object sender, EventArgs e) - { - if (!_appHost.HasPendingRestart) - { - return; - } - - var type = NotificationType.ServerRestartRequired.ToString(); - - var notification = new NotificationRequest - { - NotificationType = type - }; - - await SendNotification(notification, null).ConfigureAwait(false); - } - - private NotificationOptions GetOptions() - { - return _config.GetConfiguration<NotificationOptions>("notifications"); - } - - void _sessionManager_PlaybackStart(object sender, PlaybackProgressEventArgs e) - { - var item = e.MediaInfo; - - if (item == null) - { - _logger.Warn("PlaybackStart reported with null media info."); - return; - } - - var video = e.Item as Video; - if (video != null && video.IsThemeMedia) - { - return; - } - - var type = GetPlaybackNotificationType(item.MediaType); - - SendPlaybackNotification(type, e); - } - - void _sessionManager_PlaybackStopped(object sender, PlaybackStopEventArgs e) - { - var item = e.MediaInfo; - - if (item == null) - { - _logger.Warn("PlaybackStopped reported with null media info."); - return; - } - - var video = e.Item as Video; - if (video != null && video.IsThemeMedia) - { - return; - } - - var type = GetPlaybackStoppedNotificationType(item.MediaType); - - SendPlaybackNotification(type, e); - } - - private async void SendPlaybackNotification(string type, PlaybackProgressEventArgs e) - { - var user = e.Users.FirstOrDefault(); - - if (user != null && !GetOptions().IsEnabledToMonitorUser(type, user.Id.ToString("N"))) - { - return; - } - - var item = e.MediaInfo; - - if (e.Item != null && e.Item.IsThemeMedia) - { - // Don't report theme song or local trailer playback - return; - } - - var notification = new NotificationRequest - { - NotificationType = type - }; - - if (e.Item != null) - { - notification.Variables["ItemName"] = GetItemName(e.Item); - } - else - { - notification.Variables["ItemName"] = item.Name; - } - - notification.Variables["UserName"] = user == null ? "Unknown user" : user.Name; - notification.Variables["AppName"] = e.ClientName; - notification.Variables["DeviceName"] = e.DeviceName; - - await SendNotification(notification, null).ConfigureAwait(false); - } - - private string GetPlaybackNotificationType(string mediaType) - { - if (string.Equals(mediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase)) - { - return NotificationType.AudioPlayback.ToString(); - } - if (string.Equals(mediaType, MediaType.Game, StringComparison.OrdinalIgnoreCase)) - { - return NotificationType.GamePlayback.ToString(); - } - if (string.Equals(mediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase)) - { - return NotificationType.VideoPlayback.ToString(); - } - - return null; - } - - private string GetPlaybackStoppedNotificationType(string mediaType) - { - if (string.Equals(mediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase)) - { - return NotificationType.AudioPlaybackStopped.ToString(); - } - if (string.Equals(mediaType, MediaType.Game, StringComparison.OrdinalIgnoreCase)) - { - return NotificationType.GamePlaybackStopped.ToString(); - } - if (string.Equals(mediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase)) - { - return NotificationType.VideoPlaybackStopped.ToString(); - } - - return null; - } - - private readonly List<BaseItem> _itemsAdded = new List<BaseItem>(); - void _libraryManager_ItemAdded(object sender, ItemChangeEventArgs e) - { - if (!FilterItem(e.Item)) - { - return; - } - - lock (_libraryChangedSyncLock) - { - if (LibraryUpdateTimer == null) - { - LibraryUpdateTimer = _timerFactory.Create(LibraryUpdateTimerCallback, null, 5000, - Timeout.Infinite); - } - else - { - LibraryUpdateTimer.Change(5000, Timeout.Infinite); - } - - _itemsAdded.Add(e.Item); - } - } - - private bool FilterItem(BaseItem item) - { - if (item.IsFolder) - { - return false; - } - - if (item.LocationType == LocationType.Virtual) - { - return false; - } - - if (item is IItemByName) - { - return false; - } - - return item.SourceType == SourceType.Library; - } - - private async void LibraryUpdateTimerCallback(object state) - { - List<BaseItem> items; - - lock (_libraryChangedSyncLock) - { - items = _itemsAdded.ToList(); - _itemsAdded.Clear(); - DisposeLibraryUpdateTimer(); - } - - items = items.Take(10).ToList(); - - foreach (var item in items) - { - var notification = new NotificationRequest - { - NotificationType = NotificationType.NewLibraryContent.ToString() - }; - - notification.Variables["Name"] = GetItemName(item); - - await SendNotification(notification, item).ConfigureAwait(false); - } - } - - public static string GetItemName(BaseItem item) - { - var name = item.Name; - var episode = item as Episode; - if (episode != null) - { - if (episode.IndexNumber.HasValue) - { - name = string.Format("Ep{0} - {1}", episode.IndexNumber.Value.ToString(CultureInfo.InvariantCulture), name); - } - if (episode.ParentIndexNumber.HasValue) - { - name = string.Format("S{0}, {1}", episode.ParentIndexNumber.Value.ToString(CultureInfo.InvariantCulture), name); - } - } - - var hasSeries = item as IHasSeries; - - if (hasSeries != null) - { - name = hasSeries.SeriesName + " - " + name; - } - - var hasArtist = item as IHasArtist; - if (hasArtist != null) - { - var artists = hasArtist.AllArtists; - - if (artists.Length > 0) - { - name = hasArtist.AllArtists[0] + " - " + name; - } - } - - return name; - } - - public static string GetItemName(BaseItemDto item) - { - var name = item.Name; - - if (!string.IsNullOrWhiteSpace(item.SeriesName)) - { - name = item.SeriesName + " - " + name; - } - - if (item.Artists != null && item.Artists.Length > 0) - { - name = item.Artists[0] + " - " + name; - } - - return name; - } - - async void _userManager_UserCreated(object sender, GenericEventArgs<User> e) - { - var notification = new NotificationRequest - { - UserIds = new List<string> { e.Argument.Id.ToString("N") }, - Name = "Welcome to Emby!", - Description = "Check back here for more notifications." - }; - - await SendNotification(notification, null).ConfigureAwait(false); - } - - async void _taskManager_TaskCompleted(object sender, TaskCompletionEventArgs e) - { - var result = e.Result; - - if (result.Status == TaskCompletionStatus.Failed) - { - var type = NotificationType.TaskFailed.ToString(); - - var notification = new NotificationRequest - { - Description = result.ErrorMessage, - Level = NotificationLevel.Error, - NotificationType = type - }; - - notification.Variables["Name"] = result.Name; - notification.Variables["ErrorMessage"] = result.ErrorMessage; - - await SendNotification(notification, null).ConfigureAwait(false); - } - } - - async void _installationManager_PluginUninstalled(object sender, GenericEventArgs<IPlugin> e) - { - var type = NotificationType.PluginUninstalled.ToString(); - - var plugin = e.Argument; - - var notification = new NotificationRequest - { - NotificationType = type - }; - - notification.Variables["Name"] = plugin.Name; - notification.Variables["Version"] = plugin.Version.ToString(); - - await SendNotification(notification, null).ConfigureAwait(false); - } - - async void _installationManager_PackageInstallationFailed(object sender, InstallationFailedEventArgs e) - { - var installationInfo = e.InstallationInfo; - - var type = NotificationType.InstallationFailed.ToString(); - - var notification = new NotificationRequest - { - Level = NotificationLevel.Error, - Description = e.Exception.Message, - NotificationType = type - }; - - notification.Variables["Name"] = installationInfo.Name; - notification.Variables["Version"] = installationInfo.Version; - - await SendNotification(notification, null).ConfigureAwait(false); - } - - private async Task SendNotification(NotificationRequest notification, BaseItem relatedItem) - { - try - { - await _notificationManager.SendNotification(notification, relatedItem, CancellationToken.None).ConfigureAwait(false); - } - catch (Exception ex) - { - _logger.ErrorException("Error sending notification", ex); - } - } - - public void Dispose() - { - DisposeLibraryUpdateTimer(); - - _installationManager.PluginInstalled -= _installationManager_PluginInstalled; - _installationManager.PluginUpdated -= _installationManager_PluginUpdated; - _installationManager.PackageInstallationFailed -= _installationManager_PackageInstallationFailed; - _installationManager.PluginUninstalled -= _installationManager_PluginUninstalled; - - _taskManager.TaskCompleted -= _taskManager_TaskCompleted; - - _userManager.UserCreated -= _userManager_UserCreated; - _libraryManager.ItemAdded -= _libraryManager_ItemAdded; - _sessionManager.PlaybackStart -= _sessionManager_PlaybackStart; - - _appHost.HasPendingRestartChanged -= _appHost_HasPendingRestartChanged; - _appHost.HasUpdateAvailableChanged -= _appHost_HasUpdateAvailableChanged; - _appHost.ApplicationUpdated -= _appHost_ApplicationUpdated; - - _deviceManager.CameraImageUploaded -= _deviceManager_CameraImageUploaded; - _userManager.UserLockedOut -= _userManager_UserLockedOut; - GC.SuppressFinalize(this); - } - - private void DisposeLibraryUpdateTimer() - { - if (LibraryUpdateTimer != null) - { - LibraryUpdateTimer.Dispose(); - LibraryUpdateTimer = null; - } - } - } -} diff --git a/Emby.Server.Implementations/Notifications/SqliteNotificationsRepository.cs b/Emby.Server.Implementations/Notifications/SqliteNotificationsRepository.cs deleted file mode 100644 index f3a8a18ee..000000000 --- a/Emby.Server.Implementations/Notifications/SqliteNotificationsRepository.cs +++ /dev/null @@ -1,358 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Emby.Server.Implementations.Data; -using MediaBrowser.Controller; -using MediaBrowser.Controller.Notifications; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.Logging; -using MediaBrowser.Model.Notifications; -using SQLitePCL.pretty; -using MediaBrowser.Model.Extensions; - -namespace Emby.Server.Implementations.Notifications -{ - public class SqliteNotificationsRepository : BaseSqliteRepository, INotificationsRepository - { - protected IFileSystem FileSystem { get; private set; } - - public SqliteNotificationsRepository(ILogger logger, IServerApplicationPaths appPaths, IFileSystem fileSystem) : base(logger) - { - FileSystem = fileSystem; - DbFilePath = Path.Combine(appPaths.DataPath, "notifications.db"); - } - - public event EventHandler<NotificationUpdateEventArgs> NotificationAdded; - public event EventHandler<NotificationReadEventArgs> NotificationsMarkedRead; - ////public event EventHandler<NotificationUpdateEventArgs> NotificationUpdated; - - public void Initialize() - { - try - { - InitializeInternal(); - } - catch (Exception ex) - { - Logger.ErrorException("Error loading database file. Will reset and retry.", ex); - - FileSystem.DeleteFile(DbFilePath); - - InitializeInternal(); - } - } - - private void InitializeInternal() - { - using (var connection = CreateConnection()) - { - RunDefaultInitialization(connection); - - string[] queries = { - - "create table if not exists Notifications (Id GUID NOT NULL, UserId GUID NOT NULL, Date DATETIME NOT NULL, Name TEXT NOT NULL, Description TEXT NULL, Url TEXT NULL, Level TEXT NOT NULL, IsRead BOOLEAN NOT NULL, Category TEXT NOT NULL, RelatedId TEXT NULL, PRIMARY KEY (Id, UserId))", - "create index if not exists idx_Notifications1 on Notifications(Id)", - "create index if not exists idx_Notifications2 on Notifications(UserId)" - }; - - connection.RunQueries(queries); - } - } - - /// <summary> - /// Gets the notifications. - /// </summary> - /// <param name="query">The query.</param> - /// <returns>NotificationResult.</returns> - public NotificationResult GetNotifications(NotificationQuery query) - { - var result = new NotificationResult(); - - var clauses = new List<string>(); - var paramList = new List<object>(); - - if (query.IsRead.HasValue) - { - clauses.Add("IsRead=?"); - paramList.Add(query.IsRead.Value); - } - - clauses.Add("UserId=?"); - paramList.Add(query.UserId.ToGuidBlob()); - - var whereClause = " where " + string.Join(" And ", clauses.ToArray(clauses.Count)); - - using (WriteLock.Read()) - { - using (var connection = CreateConnection(true)) - { - result.TotalRecordCount = connection.Query("select count(Id) from Notifications" + whereClause, paramList.ToArray(paramList.Count)).SelectScalarInt().First(); - - var commandText = string.Format("select Id,UserId,Date,Name,Description,Url,Level,IsRead,Category,RelatedId from Notifications{0} order by IsRead asc, Date desc", whereClause); - - if (query.Limit.HasValue || query.StartIndex.HasValue) - { - var offset = query.StartIndex ?? 0; - - if (query.Limit.HasValue || offset > 0) - { - commandText += " LIMIT " + (query.Limit ?? int.MaxValue).ToString(CultureInfo.InvariantCulture); - } - - if (offset > 0) - { - commandText += " OFFSET " + offset.ToString(CultureInfo.InvariantCulture); - } - } - - var resultList = new List<Notification>(); - - foreach (var row in connection.Query(commandText, paramList.ToArray(paramList.Count))) - { - resultList.Add(GetNotification(row)); - } - - result.Notifications = resultList.ToArray(resultList.Count); - } - } - - return result; - } - - public NotificationsSummary GetNotificationsSummary(string userId) - { - var result = new NotificationsSummary(); - - using (WriteLock.Read()) - { - using (var connection = CreateConnection(true)) - { - using (var statement = connection.PrepareStatement("select Level from Notifications where UserId=@UserId and IsRead=@IsRead")) - { - statement.TryBind("@IsRead", false); - statement.TryBind("@UserId", userId.ToGuidBlob()); - - var levels = new List<NotificationLevel>(); - - foreach (var row in statement.ExecuteQuery()) - { - levels.Add(GetLevel(row, 0)); - } - - result.UnreadCount = levels.Count; - - if (levels.Count > 0) - { - result.MaxUnreadNotificationLevel = levels.Max(); - } - } - - return result; - } - } - } - - private Notification GetNotification(IReadOnlyList<IResultSetValue> reader) - { - var notification = new Notification - { - Id = reader[0].ReadGuidFromBlob().ToString("N"), - UserId = reader[1].ReadGuidFromBlob().ToString("N"), - Date = reader[2].ReadDateTime(), - Name = reader[3].ToString() - }; - - if (reader[4].SQLiteType != SQLiteType.Null) - { - notification.Description = reader[4].ToString(); - } - - if (reader[5].SQLiteType != SQLiteType.Null) - { - notification.Url = reader[5].ToString(); - } - - notification.Level = GetLevel(reader, 6); - notification.IsRead = reader[7].ToBool(); - - return notification; - } - - /// <summary> - /// Gets the level. - /// </summary> - /// <param name="reader">The reader.</param> - /// <param name="index">The index.</param> - /// <returns>NotificationLevel.</returns> - private NotificationLevel GetLevel(IReadOnlyList<IResultSetValue> reader, int index) - { - NotificationLevel level; - - var val = reader[index].ToString(); - - Enum.TryParse(val, true, out level); - - return level; - } - - /// <summary> - /// Adds the notification. - /// </summary> - /// <param name="notification">The notification.</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task.</returns> - public async Task AddNotification(Notification notification, CancellationToken cancellationToken) - { - await ReplaceNotification(notification, cancellationToken).ConfigureAwait(false); - - if (NotificationAdded != null) - { - try - { - NotificationAdded(this, new NotificationUpdateEventArgs - { - Notification = notification - }); - } - catch (Exception ex) - { - Logger.ErrorException("Error in NotificationAdded event handler", ex); - } - } - } - - /// <summary> - /// Replaces the notification. - /// </summary> - /// <param name="notification">The notification.</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task.</returns> - private async Task ReplaceNotification(Notification notification, CancellationToken cancellationToken) - { - if (string.IsNullOrEmpty(notification.Id)) - { - notification.Id = Guid.NewGuid().ToString("N"); - } - if (string.IsNullOrEmpty(notification.UserId)) - { - throw new ArgumentException("The notification must have a user id"); - } - - cancellationToken.ThrowIfCancellationRequested(); - - lock (WriteLock) - { - using (var connection = CreateConnection()) - { - connection.RunInTransaction(conn => - { - using (var statement = conn.PrepareStatement("replace into Notifications (Id, UserId, Date, Name, Description, Url, Level, IsRead, Category, RelatedId) values (@Id, @UserId, @Date, @Name, @Description, @Url, @Level, @IsRead, @Category, @RelatedId)")) - { - statement.TryBind("@Id", notification.Id.ToGuidBlob()); - statement.TryBind("@UserId", notification.UserId.ToGuidBlob()); - statement.TryBind("@Date", notification.Date.ToDateTimeParamValue()); - statement.TryBind("@Name", notification.Name); - statement.TryBind("@Description", notification.Description); - statement.TryBind("@Url", notification.Url); - statement.TryBind("@Level", notification.Level.ToString()); - statement.TryBind("@IsRead", notification.IsRead); - statement.TryBind("@Category", string.Empty); - statement.TryBind("@RelatedId", string.Empty); - - statement.MoveNext(); - } - }, TransactionMode); - } - } - } - - /// <summary> - /// Marks the read. - /// </summary> - /// <param name="notificationIdList">The notification id list.</param> - /// <param name="userId">The user id.</param> - /// <param name="isRead">if set to <c>true</c> [is read].</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task.</returns> - public async Task MarkRead(IEnumerable<string> notificationIdList, string userId, bool isRead, CancellationToken cancellationToken) - { - var list = notificationIdList.ToList(); - var idArray = list.Select(i => new Guid(i)).ToArray(list.Count); - - await MarkReadInternal(idArray, userId, isRead, cancellationToken).ConfigureAwait(false); - - if (NotificationsMarkedRead != null) - { - try - { - NotificationsMarkedRead(this, new NotificationReadEventArgs - { - IdList = list.ToArray(list.Count), - IsRead = isRead, - UserId = userId - }); - } - catch (Exception ex) - { - Logger.ErrorException("Error in NotificationsMarkedRead event handler", ex); - } - } - } - - public async Task MarkAllRead(string userId, bool isRead, CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - - using (WriteLock.Write()) - { - using (var connection = CreateConnection()) - { - connection.RunInTransaction(conn => - { - using (var statement = conn.PrepareStatement("update Notifications set IsRead=@IsRead where UserId=@UserId")) - { - statement.TryBind("@IsRead", isRead); - statement.TryBind("@UserId", userId.ToGuidBlob()); - - statement.MoveNext(); - } - }, TransactionMode); - } - } - } - - private async Task MarkReadInternal(IEnumerable<Guid> notificationIdList, string userId, bool isRead, CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - - using (WriteLock.Write()) - { - using (var connection = CreateConnection()) - { - connection.RunInTransaction(conn => - { - using (var statement = conn.PrepareStatement("update Notifications set IsRead=@IsRead where UserId=@UserId and Id=@Id")) - { - statement.TryBind("@IsRead", isRead); - statement.TryBind("@UserId", userId.ToGuidBlob()); - - foreach (var id in notificationIdList) - { - statement.Reset(); - - statement.TryBind("@Id", id.ToGuidBlob()); - - statement.MoveNext(); - } - } - - }, TransactionMode); - } - } - } - } -} diff --git a/Emby.Server.Implementations/Notifications/WebSocketNotifier.cs b/Emby.Server.Implementations/Notifications/WebSocketNotifier.cs deleted file mode 100644 index 6e57e7f81..000000000 --- a/Emby.Server.Implementations/Notifications/WebSocketNotifier.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System; -using MediaBrowser.Controller.Net; -using MediaBrowser.Controller.Notifications; -using MediaBrowser.Controller.Plugins; -using System.Linq; -using MediaBrowser.Model.Extensions; - -namespace Emby.Server.Implementations.Notifications -{ - /// <summary> - /// Notifies clients anytime a notification is added or udpated - /// </summary> - public class WebSocketNotifier : IServerEntryPoint - { - private readonly INotificationsRepository _notificationsRepo; - - private readonly IServerManager _serverManager; - - public WebSocketNotifier(INotificationsRepository notificationsRepo, IServerManager serverManager) - { - _notificationsRepo = notificationsRepo; - _serverManager = serverManager; - } - - public void Run() - { - _notificationsRepo.NotificationAdded += _notificationsRepo_NotificationAdded; - _notificationsRepo.NotificationsMarkedRead += _notificationsRepo_NotificationsMarkedRead; - } - - void _notificationsRepo_NotificationsMarkedRead(object sender, NotificationReadEventArgs e) - { - var list = e.IdList.ToList(); - - list.Add(e.UserId); - list.Add(e.IsRead.ToString().ToLower()); - - var msg = string.Join("|", list.ToArray(list.Count)); - - _serverManager.SendWebSocketMessage("NotificationsMarkedRead", msg); - } - - void _notificationsRepo_NotificationAdded(object sender, NotificationUpdateEventArgs e) - { - var msg = e.Notification.UserId + "|" + e.Notification.Id; - - _serverManager.SendWebSocketMessage("NotificationAdded", msg); - } - - public void Dispose() - { - _notificationsRepo.NotificationAdded -= _notificationsRepo_NotificationAdded; - GC.SuppressFinalize(this); - } - } -} diff --git a/Emby.Server.Implementations/Playlists/ManualPlaylistsFolder.cs b/Emby.Server.Implementations/Playlists/ManualPlaylistsFolder.cs index 2a178895c..908fa65de 100644 --- a/Emby.Server.Implementations/Playlists/ManualPlaylistsFolder.cs +++ b/Emby.Server.Implementations/Playlists/ManualPlaylistsFolder.cs @@ -50,8 +50,16 @@ namespace Emby.Server.Implementations.Playlists protected override QueryResult<BaseItem> GetItemsInternal(InternalItemsQuery query) { - query.Recursive = false; - return base.GetItemsInternal(query); + if (query.User == null) + { + query.Recursive = false; + return base.GetItemsInternal(query); + } + + query.Recursive = true; + query.IncludeItemTypes = new string[] { "Playlist" }; + query.Parent = null; + return LibraryManager.GetItemsResult(query); } } } diff --git a/Emby.Server.Implementations/Playlists/PlaylistImageProvider.cs b/Emby.Server.Implementations/Playlists/PlaylistImageProvider.cs index 36e8b4dd8..e69b4a34e 100644 --- a/Emby.Server.Implementations/Playlists/PlaylistImageProvider.cs +++ b/Emby.Server.Implementations/Playlists/PlaylistImageProvider.cs @@ -28,11 +28,11 @@ namespace Emby.Server.Implementations.Playlists { } - protected override List<BaseItem> GetItemsWithImages(IHasMetadata item) + protected override List<BaseItem> GetItemsWithImages(BaseItem item) { var playlist = (Playlist)item; - var items = playlist.GetManageableItems() + return playlist.GetManageableItems() .Select(i => { var subItem = i.Item2; @@ -53,7 +53,7 @@ namespace Emby.Server.Implementations.Playlists return subItem; } - var parent = subItem.IsOwnedItem ? subItem.GetOwner() : subItem.GetParent(); + var parent = subItem.GetOwner() ?? subItem.GetParent(); if (parent != null && parent.HasImage(ImageType.Primary)) { @@ -66,9 +66,9 @@ namespace Emby.Server.Implementations.Playlists return null; }) .Where(i => i != null) - .DistinctBy(i => i.Id); - - return GetFinalItems(items); + .OrderBy(i => Guid.NewGuid()) + .DistinctBy(i => i.Id) + .ToList(); } } @@ -81,27 +81,20 @@ namespace Emby.Server.Implementations.Playlists _libraryManager = libraryManager; } - protected override List<BaseItem> GetItemsWithImages(IHasMetadata item) + protected override List<BaseItem> GetItemsWithImages(BaseItem item) { - var items = _libraryManager.GetItemList(new InternalItemsQuery + return _libraryManager.GetItemList(new InternalItemsQuery { Genres = new[] { item.Name }, IncludeItemTypes = new[] { typeof(MusicAlbum).Name, typeof(MusicVideo).Name, typeof(Audio).Name }, - OrderBy = new[] { new Tuple<string, SortOrder>(ItemSortBy.Random, SortOrder.Ascending) }, + OrderBy = new[] { new ValueTuple<string, SortOrder>(ItemSortBy.Random, SortOrder.Ascending) }, Limit = 4, Recursive = true, ImageTypes = new[] { ImageType.Primary }, DtoOptions = new DtoOptions(false) }); - - return GetFinalItems(items); } - - //protected override Task<string> CreateImage(IHasMetadata item, List<BaseItem> itemsWithImages, string outputPathWithoutExtension, ImageType imageType, int imageIndex) - //{ - // return CreateSingleImage(itemsWithImages, outputPathWithoutExtension, ImageType.Primary); - //} } public class GenreImageProvider : BaseDynamicImageProvider<Genre> @@ -113,21 +106,18 @@ namespace Emby.Server.Implementations.Playlists _libraryManager = libraryManager; } - protected override List<BaseItem> GetItemsWithImages(IHasMetadata item) + protected override List<BaseItem> GetItemsWithImages(BaseItem item) { - var items = _libraryManager.GetItemList(new InternalItemsQuery + return _libraryManager.GetItemList(new InternalItemsQuery { Genres = new[] { item.Name }, IncludeItemTypes = new[] { typeof(Series).Name, typeof(Movie).Name }, - OrderBy = new[] { new Tuple<string, SortOrder>(ItemSortBy.Random, SortOrder.Ascending) }, + OrderBy = new[] { new ValueTuple<string, SortOrder>(ItemSortBy.Random, SortOrder.Ascending) }, Limit = 4, Recursive = true, ImageTypes = new[] { ImageType.Primary }, DtoOptions = new DtoOptions(false) - }); - - return GetFinalItems(items); } //protected override Task<string> CreateImage(IHasMetadata item, List<BaseItem> itemsWithImages, string outputPathWithoutExtension, ImageType imageType, int imageIndex) diff --git a/Emby.Server.Implementations/Playlists/PlaylistManager.cs b/Emby.Server.Implementations/Playlists/PlaylistManager.cs index f268e9c0c..e0a6f56ec 100644 --- a/Emby.Server.Implementations/Playlists/PlaylistManager.cs +++ b/Emby.Server.Implementations/Playlists/PlaylistManager.cs @@ -15,6 +15,10 @@ using System.Threading.Tasks; using MediaBrowser.Controller.Dto; using MediaBrowser.Model.IO; using MediaBrowser.Model.Extensions; +using PlaylistsNET; +using PlaylistsNET.Content; +using PlaylistsNET.Models; +using PlaylistsNET.Utils; namespace Emby.Server.Implementations.Playlists { @@ -37,7 +41,7 @@ namespace Emby.Server.Implementations.Playlists _providerManager = providerManager; } - public IEnumerable<Playlist> GetPlaylists(string userId) + public IEnumerable<Playlist> GetPlaylists(Guid userId) { var user = _userManager.GetUserById(userId); @@ -50,14 +54,14 @@ namespace Emby.Server.Implementations.Playlists var folderName = _fileSystem.GetValidFilename(name) + " [playlist]"; - var parentFolder = GetPlaylistsFolder(null); + var parentFolder = GetPlaylistsFolder(Guid.Empty); if (parentFolder == null) { throw new ArgumentException(); } - if (string.IsNullOrWhiteSpace(options.MediaType)) + if (string.IsNullOrEmpty(options.MediaType)) { foreach (var itemId in options.ItemIdList) { @@ -68,7 +72,7 @@ namespace Emby.Server.Implementations.Playlists throw new ArgumentException("No item exists with the supplied Id"); } - if (!string.IsNullOrWhiteSpace(item.MediaType)) + if (!string.IsNullOrEmpty(item.MediaType)) { options.MediaType = item.MediaType; } @@ -87,18 +91,18 @@ namespace Emby.Server.Implementations.Playlists { options.MediaType = folder.GetRecursiveChildren(i => !i.IsFolder && i.SupportsAddingToPlaylist) .Select(i => i.MediaType) - .FirstOrDefault(i => !string.IsNullOrWhiteSpace(i)); + .FirstOrDefault(i => !string.IsNullOrEmpty(i)); } } - if (!string.IsNullOrWhiteSpace(options.MediaType)) + if (!string.IsNullOrEmpty(options.MediaType)) { break; } } } - if (string.IsNullOrWhiteSpace(options.MediaType)) + if (string.IsNullOrEmpty(options.MediaType)) { options.MediaType = "Audio"; } @@ -117,15 +121,17 @@ namespace Emby.Server.Implementations.Playlists var playlist = new Playlist { Name = name, - Path = path + Path = path, + Shares = new[] + { + new Share + { + UserId = options.UserId.Equals(Guid.Empty) ? null : options.UserId.ToString("N"), + CanEdit = true + } + } }; - playlist.Shares.Add(new Share - { - UserId = options.UserId, - CanEdit = true - }); - playlist.SetMediaType(options.MediaType); parentFolder.AddChild(playlist, CancellationToken.None); @@ -135,7 +141,7 @@ namespace Emby.Server.Implementations.Playlists if (options.ItemIdList.Length > 0) { - await AddToPlaylistInternal(playlist.Id.ToString("N"), options.ItemIdList, user, new DtoOptions(false) + AddToPlaylistInternal(playlist.Id.ToString("N"), options.ItemIdList, user, new DtoOptions(false) { EnableImages = true }); @@ -163,24 +169,24 @@ namespace Emby.Server.Implementations.Playlists return path; } - private List<BaseItem> GetPlaylistItems(IEnumerable<string> itemIds, string playlistMediaType, User user, DtoOptions options) + private List<BaseItem> GetPlaylistItems(IEnumerable<Guid> itemIds, string playlistMediaType, User user, DtoOptions options) { var items = itemIds.Select(i => _libraryManager.GetItemById(i)).Where(i => i != null); return Playlist.GetPlaylistItems(playlistMediaType, items, user, options); } - public Task AddToPlaylist(string playlistId, IEnumerable<string> itemIds, string userId) + public void AddToPlaylist(string playlistId, IEnumerable<Guid> itemIds, Guid userId) { - var user = string.IsNullOrWhiteSpace(userId) ? null : _userManager.GetUserById(userId); + var user = userId.Equals(Guid.Empty) ? null : _userManager.GetUserById(userId); - return AddToPlaylistInternal(playlistId, itemIds, user, new DtoOptions(false) + AddToPlaylistInternal(playlistId, itemIds, user, new DtoOptions(false) { EnableImages = true }); } - private async Task AddToPlaylistInternal(string playlistId, IEnumerable<string> itemIds, User user, DtoOptions options) + private void AddToPlaylistInternal(string playlistId, IEnumerable<Guid> itemIds, User user, DtoOptions options) { var playlist = _libraryManager.GetItemById(playlistId) as Playlist; @@ -197,11 +203,6 @@ namespace Emby.Server.Implementations.Playlists foreach (var item in items) { - if (string.IsNullOrWhiteSpace(item.Path)) - { - continue; - } - list.Add(LinkedChild.Create(item)); } @@ -211,6 +212,11 @@ namespace Emby.Server.Implementations.Playlists playlist.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None); + if (playlist.IsFile) + { + SavePlaylistFile(playlist); + } + _providerManager.QueueRefresh(playlist.Id, new MetadataRefreshOptions(_fileSystem) { ForceSave = true @@ -218,7 +224,7 @@ namespace Emby.Server.Implementations.Playlists }, RefreshPriority.High); } - public async Task RemoveFromPlaylist(string playlistId, IEnumerable<string> entryIds) + public void RemoveFromPlaylist(string playlistId, IEnumerable<string> entryIds) { var playlist = _libraryManager.GetItemById(playlistId) as Playlist; @@ -239,6 +245,11 @@ namespace Emby.Server.Implementations.Playlists playlist.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None); + if (playlist.IsFile) + { + SavePlaylistFile(playlist); + } + _providerManager.QueueRefresh(playlist.Id, new MetadataRefreshOptions(_fileSystem) { ForceSave = true @@ -246,7 +257,7 @@ namespace Emby.Server.Implementations.Playlists }, RefreshPriority.High); } - public async Task MoveItem(string playlistId, string entryId, int newIndex) + public void MoveItem(string playlistId, string entryId, int newIndex) { var playlist = _libraryManager.GetItemById(playlistId) as Playlist; @@ -282,9 +293,207 @@ namespace Emby.Server.Implementations.Playlists playlist.LinkedChildren = newList.ToArray(newList.Count); playlist.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None); + + if (playlist.IsFile) + { + SavePlaylistFile(playlist); + } + } + + private void SavePlaylistFile(Playlist item) + { + // This is probably best done as a metatata provider, but saving a file over itself will first require some core work to prevent this from happening when not needed + var playlistPath = item.Path; + var extension = Path.GetExtension(playlistPath); + + if (string.Equals(".wpl", extension, StringComparison.OrdinalIgnoreCase)) + { + var playlist = new WplPlaylist(); + foreach (var child in item.GetLinkedChildren()) + { + var entry = new WplPlaylistEntry() + { + Path = NormalizeItemPath(playlistPath, child.Path), + TrackTitle = child.Name, + AlbumTitle = child.Album + }; + + var hasAlbumArtist = child as IHasAlbumArtist; + if (hasAlbumArtist != null) + { + entry.AlbumArtist = hasAlbumArtist.AlbumArtists.FirstOrDefault(); + } + + var hasArtist = child as IHasArtist; + if (hasArtist != null) + { + entry.TrackArtist = hasArtist.Artists.FirstOrDefault(); + } + + if (child.RunTimeTicks.HasValue) + { + entry.Duration = TimeSpan.FromTicks(child.RunTimeTicks.Value); + } + playlist.PlaylistEntries.Add(entry); + } + + _fileSystem.WriteAllText(playlistPath, new WplContent().ToText(playlist)); + } + if (string.Equals(".zpl", extension, StringComparison.OrdinalIgnoreCase)) + { + var playlist = new ZplPlaylist(); + foreach (var child in item.GetLinkedChildren()) + { + var entry = new ZplPlaylistEntry() + { + Path = NormalizeItemPath(playlistPath, child.Path), + TrackTitle = child.Name, + AlbumTitle = child.Album + }; + + var hasAlbumArtist = child as IHasAlbumArtist; + if (hasAlbumArtist != null) + { + entry.AlbumArtist = hasAlbumArtist.AlbumArtists.FirstOrDefault(); + } + + var hasArtist = child as IHasArtist; + if (hasArtist != null) + { + entry.TrackArtist = hasArtist.Artists.FirstOrDefault(); + } + + if (child.RunTimeTicks.HasValue) + { + entry.Duration = TimeSpan.FromTicks(child.RunTimeTicks.Value); + } + playlist.PlaylistEntries.Add(entry); + } + + _fileSystem.WriteAllText(playlistPath, new ZplContent().ToText(playlist)); + } + if (string.Equals(".m3u", extension, StringComparison.OrdinalIgnoreCase)) + { + var playlist = new M3uPlaylist(); + playlist.IsExtended = true; + foreach (var child in item.GetLinkedChildren()) + { + var entry = new M3uPlaylistEntry() + { + Path = NormalizeItemPath(playlistPath, child.Path), + Title = child.Name, + Album = child.Album + }; + + var hasAlbumArtist = child as IHasAlbumArtist; + if (hasAlbumArtist != null) + { + entry.AlbumArtist = hasAlbumArtist.AlbumArtists.FirstOrDefault(); + } + + if (child.RunTimeTicks.HasValue) + { + entry.Duration = TimeSpan.FromTicks(child.RunTimeTicks.Value); + } + playlist.PlaylistEntries.Add(entry); + } + + _fileSystem.WriteAllText(playlistPath, new M3uContent().ToText(playlist)); + } + if (string.Equals(".m3u8", extension, StringComparison.OrdinalIgnoreCase)) + { + var playlist = new M3uPlaylist(); + playlist.IsExtended = true; + foreach (var child in item.GetLinkedChildren()) + { + var entry = new M3uPlaylistEntry() + { + Path = NormalizeItemPath(playlistPath, child.Path), + Title = child.Name, + Album = child.Album + }; + + var hasAlbumArtist = child as IHasAlbumArtist; + if (hasAlbumArtist != null) + { + entry.AlbumArtist = hasAlbumArtist.AlbumArtists.FirstOrDefault(); + } + + if (child.RunTimeTicks.HasValue) + { + entry.Duration = TimeSpan.FromTicks(child.RunTimeTicks.Value); + } + playlist.PlaylistEntries.Add(entry); + } + + _fileSystem.WriteAllText(playlistPath, new M3u8Content().ToText(playlist)); + } + if (string.Equals(".pls", extension, StringComparison.OrdinalIgnoreCase)) + { + var playlist = new PlsPlaylist(); + foreach (var child in item.GetLinkedChildren()) + { + var entry = new PlsPlaylistEntry() + { + Path = NormalizeItemPath(playlistPath, child.Path), + Title = child.Name + }; + + if (child.RunTimeTicks.HasValue) + { + entry.Length = TimeSpan.FromTicks(child.RunTimeTicks.Value); + } + playlist.PlaylistEntries.Add(entry); + } + + _fileSystem.WriteAllText(playlistPath, new PlsContent().ToText(playlist)); + } + } + + private string NormalizeItemPath(string playlistPath, string itemPath) + { + return MakeRelativePath(_fileSystem.GetDirectoryName(playlistPath), itemPath); + } + + private static String MakeRelativePath(string folderPath, string fileAbsolutePath) + { + if (String.IsNullOrEmpty(folderPath)) throw new ArgumentNullException("folderPath"); + if (String.IsNullOrEmpty(fileAbsolutePath)) throw new ArgumentNullException("filePath"); + + if (!folderPath.EndsWith(Path.DirectorySeparatorChar.ToString())) + { + folderPath = folderPath + Path.DirectorySeparatorChar; + } + + Uri folderUri = new Uri(folderPath); + Uri fileAbsoluteUri = new Uri(fileAbsolutePath); + + if (folderUri.Scheme != fileAbsoluteUri.Scheme) { return fileAbsolutePath; } // path can't be made relative. + + Uri relativeUri = folderUri.MakeRelativeUri(fileAbsoluteUri); + String relativePath = Uri.UnescapeDataString(relativeUri.ToString()); + + if (fileAbsoluteUri.Scheme.Equals("file", StringComparison.CurrentCultureIgnoreCase)) + { + relativePath = relativePath.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar); + } + + return relativePath; + } + + private static string UnEscape(string content) + { + if (content == null) return content; + return content.Replace("&", "&").Replace("'", "'").Replace(""", "\"").Replace(">", ">").Replace("<", "<"); + } + + private static string Escape(string content) + { + if (content == null) return null; + return content.Replace("&", "&").Replace("'", "'").Replace("\"", """).Replace(">", ">").Replace("<", "<"); } - public Folder GetPlaylistsFolder(string userId) + public Folder GetPlaylistsFolder(Guid userId) { var typeName = "PlaylistsFolder"; diff --git a/Emby.Server.Implementations/Playlists/PlaylistsDynamicFolder.cs b/Emby.Server.Implementations/Playlists/PlaylistsDynamicFolder.cs deleted file mode 100644 index 2ce835576..000000000 --- a/Emby.Server.Implementations/Playlists/PlaylistsDynamicFolder.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System.IO; -using MediaBrowser.Common.Configuration; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Model.IO; - -namespace Emby.Server.Implementations.Playlists -{ - public class PlaylistsDynamicFolder : IVirtualFolderCreator - { - private readonly IApplicationPaths _appPaths; - private readonly IFileSystem _fileSystem; - - public PlaylistsDynamicFolder(IApplicationPaths appPaths, IFileSystem fileSystem) - { - _appPaths = appPaths; - _fileSystem = fileSystem; - } - - public BasePluginFolder GetFolder() - { - var path = Path.Combine(_appPaths.DataPath, "playlists"); - - _fileSystem.CreateDirectory(path); - - return new PlaylistsFolder - { - Path = path - }; - } - } -} diff --git a/Emby.Server.Implementations/ResourceFileManager.cs b/Emby.Server.Implementations/ResourceFileManager.cs new file mode 100644 index 000000000..ce29b52f6 --- /dev/null +++ b/Emby.Server.Implementations/ResourceFileManager.cs @@ -0,0 +1,74 @@ +using MediaBrowser.Common.Extensions; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Net; +using MediaBrowser.Controller.Plugins; +using MediaBrowser.Model.Extensions; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Net; +using MediaBrowser.Model.Serialization; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using MediaBrowser.Common.Plugins; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Globalization; +using MediaBrowser.Model.Plugins; +using MediaBrowser.Model.Reflection; +using MediaBrowser.Model.Services; + +namespace Emby.Server.Implementations +{ + public class ResourceFileManager : IResourceFileManager + { + private readonly IFileSystem _fileSystem; + private readonly ILogger _logger; + private readonly IHttpResultFactory _resultFactory; + + public ResourceFileManager(IHttpResultFactory resultFactory, ILogger logger, IFileSystem fileSystem) + { + _resultFactory = resultFactory; + _logger = logger; + _fileSystem = fileSystem; + } + + public Stream GetResourceFileStream(string basePath, string virtualPath) + { + return _fileSystem.GetFileStream(GetResourcePath(basePath, virtualPath), FileOpenMode.Open, FileAccessMode.Read, FileShareMode.ReadWrite, true); + } + + public Task<object> GetStaticFileResult(IRequest request, string basePath, string virtualPath, string contentType, TimeSpan? cacheDuration) + { + return _resultFactory.GetStaticFileResult(request, GetResourcePath(basePath, virtualPath)); + } + + public string ReadAllText(string basePath, string virtualPath) + { + return _fileSystem.ReadAllText(GetResourcePath(basePath, virtualPath)); + } + + private string GetResourcePath(string basePath, string virtualPath) + { + var fullPath = Path.Combine(basePath, virtualPath.Replace('/', _fileSystem.DirectorySeparatorChar)); + + try + { + fullPath = _fileSystem.GetFullPath(fullPath); + } + catch (Exception ex) + { + _logger.ErrorException("Error in Path.GetFullPath", ex); + } + + // Don't allow file system access outside of the source folder + if (!_fileSystem.ContainsSubPath(basePath, fullPath)) + { + throw new SecurityException("Access denied"); + } + + return fullPath; + } + } +} diff --git a/Emby.Server.Implementations/ScheduledTasks/ChapterImagesTask.cs b/Emby.Server.Implementations/ScheduledTasks/ChapterImagesTask.cs index fe0652f66..7a5efded3 100644 --- a/Emby.Server.Implementations/ScheduledTasks/ChapterImagesTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/ChapterImagesTask.cs @@ -65,7 +65,7 @@ namespace Emby.Server.Implementations.ScheduledTasks { Type = TaskTriggerInfo.TriggerDaily, TimeOfDayTicks = TimeSpan.FromHours(2).Ticks, - MaxRuntimeMs = Convert.ToInt32(TimeSpan.FromHours(4).TotalMilliseconds) + MaxRuntimeTicks = TimeSpan.FromHours(4).Ticks } }; } @@ -133,9 +133,9 @@ namespace Emby.Server.Implementations.ScheduledTasks try { - var chapters = _itemRepo.GetChapters(video.Id); + var chapters = _itemRepo.GetChapters(video); - var success = await _encodingManager.RefreshChapterImages(video, directoryService, chapters, extract, true, CancellationToken.None); + var success = await _encodingManager.RefreshChapterImages(video, directoryService, chapters, extract, true, cancellationToken).ConfigureAwait(false); if (!success) { diff --git a/Emby.Server.Implementations/ScheduledTasks/DailyTrigger.cs b/Emby.Server.Implementations/ScheduledTasks/DailyTrigger.cs index 1ba5d4329..a2779c7cd 100644 --- a/Emby.Server.Implementations/ScheduledTasks/DailyTrigger.cs +++ b/Emby.Server.Implementations/ScheduledTasks/DailyTrigger.cs @@ -19,18 +19,15 @@ namespace Emby.Server.Implementations.ScheduledTasks public TimeSpan TimeOfDay { get; set; } /// <summary> - /// Gets or sets the timer. + /// Gets or sets the options of this task. /// </summary> - /// <value>The timer.</value> - private Timer Timer { get; set; } + public TaskOptions TaskOptions { get; set; } /// <summary> - /// Gets the execution properties of this task. + /// Gets or sets the timer. /// </summary> - /// <value> - /// The execution properties of this task. - /// </value> - public TaskExecutionOptions TaskOptions { get; set; } + /// <value>The timer.</value> + private Timer Timer { get; set; } /// <summary> /// Stars waiting for the trigger action @@ -75,7 +72,7 @@ namespace Emby.Server.Implementations.ScheduledTasks /// <summary> /// Occurs when [triggered]. /// </summary> - public event EventHandler<GenericEventArgs<TaskExecutionOptions>> Triggered; + public event EventHandler<EventArgs> Triggered; /// <summary> /// Called when [triggered]. @@ -84,7 +81,7 @@ namespace Emby.Server.Implementations.ScheduledTasks { if (Triggered != null) { - Triggered(this, new GenericEventArgs<TaskExecutionOptions>(TaskOptions)); + Triggered(this, EventArgs.Empty); } } } diff --git a/Emby.Server.Implementations/ScheduledTasks/IntervalTrigger.cs b/Emby.Server.Implementations/ScheduledTasks/IntervalTrigger.cs index d09765e34..dcccb252a 100644 --- a/Emby.Server.Implementations/ScheduledTasks/IntervalTrigger.cs +++ b/Emby.Server.Implementations/ScheduledTasks/IntervalTrigger.cs @@ -19,18 +19,15 @@ namespace Emby.Server.Implementations.ScheduledTasks public TimeSpan Interval { get; set; } /// <summary> - /// Gets or sets the timer. + /// Gets or sets the options of this task. /// </summary> - /// <value>The timer.</value> - private Timer Timer { get; set; } + public TaskOptions TaskOptions { get; set; } /// <summary> - /// Gets the execution properties of this task. + /// Gets or sets the timer. /// </summary> - /// <value> - /// The execution properties of this task. - /// </value> - public TaskExecutionOptions TaskOptions { get; set; } + /// <value>The timer.</value> + private Timer Timer { get; set; } private DateTime _lastStartDate; @@ -93,7 +90,7 @@ namespace Emby.Server.Implementations.ScheduledTasks /// <summary> /// Occurs when [triggered]. /// </summary> - public event EventHandler<GenericEventArgs<TaskExecutionOptions>> Triggered; + public event EventHandler<EventArgs> Triggered; /// <summary> /// Called when [triggered]. @@ -105,7 +102,7 @@ namespace Emby.Server.Implementations.ScheduledTasks if (Triggered != null) { _lastStartDate = DateTime.UtcNow; - Triggered(this, new GenericEventArgs<TaskExecutionOptions>(TaskOptions)); + Triggered(this, EventArgs.Empty); } } } diff --git a/Emby.Server.Implementations/ScheduledTasks/PluginUpdateTask.cs b/Emby.Server.Implementations/ScheduledTasks/PluginUpdateTask.cs index 9f887ba03..691112638 100644 --- a/Emby.Server.Implementations/ScheduledTasks/PluginUpdateTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/PluginUpdateTask.cs @@ -16,7 +16,7 @@ namespace Emby.Server.Implementations.ScheduledTasks /// <summary> /// Plugin Update Task /// </summary> - public class PluginUpdateTask : IScheduledTask + public class PluginUpdateTask : IScheduledTask, IConfigurableScheduledTask { /// <summary> /// The _logger @@ -71,14 +71,13 @@ namespace Emby.Server.Implementations.ScheduledTasks var numComplete = 0; - // Create tasks for each one - var tasks = packagesToInstall.Select(i => Task.Run(async () => + foreach (var package in packagesToInstall) { cancellationToken.ThrowIfCancellationRequested(); try { - await _installationManager.InstallPackage(i, true, new SimpleProgress<double>(), cancellationToken).ConfigureAwait(false); + await _installationManager.InstallPackage(package, true, new SimpleProgress<double>(), cancellationToken).ConfigureAwait(false); } catch (OperationCanceledException) { @@ -90,11 +89,11 @@ namespace Emby.Server.Implementations.ScheduledTasks } catch (HttpException ex) { - _logger.ErrorException("Error downloading {0}", ex, i.name); + _logger.ErrorException("Error downloading {0}", ex, package.name); } catch (IOException ex) { - _logger.ErrorException("Error updating {0}", ex, i.name); + _logger.ErrorException("Error updating {0}", ex, package.name); } // Update progress @@ -106,11 +105,7 @@ namespace Emby.Server.Implementations.ScheduledTasks progress.Report(90 * percent + 10); } - })); - - cancellationToken.ThrowIfCancellationRequested(); - - await Task.WhenAll(tasks).ConfigureAwait(false); + } progress.Report(100); } @@ -137,5 +132,11 @@ namespace Emby.Server.Implementations.ScheduledTasks { get { return "Application"; } } + + public bool IsHidden => true; + + public bool IsEnabled => true; + + public bool IsLogged => true; } }
\ No newline at end of file diff --git a/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs b/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs index bdc29c16b..f6397b670 100644 --- a/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs +++ b/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs @@ -160,7 +160,7 @@ namespace Emby.Server.Implementations.ScheduledTasks _lastExecutionResult = value; var path = GetHistoryFilePath(); - _fileSystem.CreateDirectory(_fileSystem.GetDirectoryName(path)); + _fileSystem.CreateDirectory(_fileSystem.GetDirectoryName(path)); lock (_lastExecutionResultSyncLock) { @@ -236,7 +236,7 @@ namespace Emby.Server.Implementations.ScheduledTasks /// <summary> /// The _triggers /// </summary> - private Tuple<TaskTriggerInfo,ITaskTrigger>[] _triggers; + private Tuple<TaskTriggerInfo, ITaskTrigger>[] _triggers; /// <summary> /// Gets the triggers that define when the task will run /// </summary> @@ -350,7 +350,7 @@ namespace Emby.Server.Implementations.ScheduledTasks /// </summary> /// <param name="sender">The source of the event.</param> /// <param name="e">The <see cref="EventArgs" /> instance containing the event data.</param> - async void trigger_Triggered(object sender, GenericEventArgs<TaskExecutionOptions> e) + async void trigger_Triggered(object sender, EventArgs e) { var trigger = (ITaskTrigger)sender; @@ -365,7 +365,7 @@ namespace Emby.Server.Implementations.ScheduledTasks trigger.Stop(); - TaskManager.QueueScheduledTask(ScheduledTask, e.Argument); + TaskManager.QueueScheduledTask(ScheduledTask, trigger.TaskOptions); await Task.Delay(1000).ConfigureAwait(false); @@ -380,7 +380,7 @@ namespace Emby.Server.Implementations.ScheduledTasks /// <param name="options">Task options.</param> /// <returns>Task.</returns> /// <exception cref="System.InvalidOperationException">Cannot execute a Task that is already running</exception> - public async Task Execute(TaskExecutionOptions options) + public async Task Execute(TaskOptions options) { var task = Task.Run(async () => await ExecuteInternal(options).ConfigureAwait(false)); @@ -397,7 +397,7 @@ namespace Emby.Server.Implementations.ScheduledTasks } } - private async Task ExecuteInternal(TaskExecutionOptions options) + private async Task ExecuteInternal(TaskOptions options) { // Cancel the current execution, if any if (CurrentCancellationTokenSource != null) @@ -422,14 +422,12 @@ namespace Emby.Server.Implementations.ScheduledTasks try { - if (options != null && options.MaxRuntimeMs.HasValue) + if (options != null && options.MaxRuntimeTicks.HasValue) { - CurrentCancellationTokenSource.CancelAfter(options.MaxRuntimeMs.Value); + CurrentCancellationTokenSource.CancelAfter(TimeSpan.FromTicks(options.MaxRuntimeTicks.Value)); } - var localTask = ScheduledTask.Execute(CurrentCancellationTokenSource.Token, progress); - - await localTask.ConfigureAwait(false); + await ScheduledTask.Execute(CurrentCancellationTokenSource.Token, progress).ConfigureAwait(false); status = TaskCompletionStatus.Completed; } @@ -563,13 +561,35 @@ namespace Emby.Server.Implementations.ScheduledTasks catch (FileNotFoundException) { // File doesn't exist. No biggie. Return defaults. - return ScheduledTask.GetDefaultTriggers().ToArray(); } catch (DirectoryNotFoundException) { // File doesn't exist. No biggie. Return defaults. } - return ScheduledTask.GetDefaultTriggers().ToArray(); + catch + { + + } + return GetDefaultTriggers(); + } + + private TaskTriggerInfo[] GetDefaultTriggers() + { + try + { + return ScheduledTask.GetDefaultTriggers().ToArray(); + } + catch + { + return new TaskTriggerInfo[] + { + new TaskTriggerInfo + { + IntervalTicks = TimeSpan.FromDays(1).Ticks, + Type = TaskTriggerInfo.TriggerInterval + } + }; + } } /// <summary> @@ -580,7 +600,7 @@ namespace Emby.Server.Implementations.ScheduledTasks { var path = GetConfigurationFilePath(); - _fileSystem.CreateDirectory(_fileSystem.GetDirectoryName(path)); + _fileSystem.CreateDirectory(_fileSystem.GetDirectoryName(path)); JsonSerializer.SerializeToFile(triggers, path); } @@ -625,7 +645,6 @@ namespace Emby.Server.Implementations.ScheduledTasks public void Dispose() { Dispose(true); - GC.SuppressFinalize(this); } /// <summary> @@ -705,9 +724,9 @@ namespace Emby.Server.Implementations.ScheduledTasks /// <exception cref="System.ArgumentException">Invalid trigger type: + info.Type</exception> private ITaskTrigger GetTrigger(TaskTriggerInfo info) { - var options = new TaskExecutionOptions + var options = new TaskOptions { - MaxRuntimeMs = info.MaxRuntimeMs + MaxRuntimeTicks = info.MaxRuntimeTicks }; if (info.Type.Equals(typeof(DailyTrigger).Name, StringComparison.OrdinalIgnoreCase)) diff --git a/Emby.Server.Implementations/ScheduledTasks/StartupTrigger.cs b/Emby.Server.Implementations/ScheduledTasks/StartupTrigger.cs index d708c905d..20a4304cc 100644 --- a/Emby.Server.Implementations/ScheduledTasks/StartupTrigger.cs +++ b/Emby.Server.Implementations/ScheduledTasks/StartupTrigger.cs @@ -14,12 +14,9 @@ namespace Emby.Server.Implementations.ScheduledTasks public int DelayMs { get; set; } /// <summary> - /// Gets the execution properties of this task. + /// Gets or sets the options of this task. /// </summary> - /// <value> - /// The execution properties of this task. - /// </value> - public TaskExecutionOptions TaskOptions { get; set; } + public TaskOptions TaskOptions { get; set; } public StartupTrigger() { @@ -51,7 +48,7 @@ namespace Emby.Server.Implementations.ScheduledTasks /// <summary> /// Occurs when [triggered]. /// </summary> - public event EventHandler<GenericEventArgs<TaskExecutionOptions>> Triggered; + public event EventHandler<EventArgs> Triggered; /// <summary> /// Called when [triggered]. @@ -60,7 +57,7 @@ namespace Emby.Server.Implementations.ScheduledTasks { if (Triggered != null) { - Triggered(this, new GenericEventArgs<TaskExecutionOptions>(TaskOptions)); + Triggered(this, EventArgs.Empty); } } } diff --git a/Emby.Server.Implementations/ScheduledTasks/SystemEventTrigger.cs b/Emby.Server.Implementations/ScheduledTasks/SystemEventTrigger.cs index 976754a40..c4623bf5b 100644 --- a/Emby.Server.Implementations/ScheduledTasks/SystemEventTrigger.cs +++ b/Emby.Server.Implementations/ScheduledTasks/SystemEventTrigger.cs @@ -19,12 +19,9 @@ namespace Emby.Server.Implementations.ScheduledTasks public SystemEvent SystemEvent { get; set; } /// <summary> - /// Gets the execution properties of this task. + /// Gets or sets the options of this task. /// </summary> - /// <value> - /// The execution properties of this task. - /// </value> - public TaskExecutionOptions TaskOptions { get; set; } + public TaskOptions TaskOptions { get; set; } private readonly ISystemEvents _systemEvents; @@ -70,7 +67,7 @@ namespace Emby.Server.Implementations.ScheduledTasks /// <summary> /// Occurs when [triggered]. /// </summary> - public event EventHandler<GenericEventArgs<TaskExecutionOptions>> Triggered; + public event EventHandler<EventArgs> Triggered; /// <summary> /// Called when [triggered]. @@ -79,7 +76,7 @@ namespace Emby.Server.Implementations.ScheduledTasks { if (Triggered != null) { - Triggered(this, new GenericEventArgs<TaskExecutionOptions>(TaskOptions)); + Triggered(this, EventArgs.Empty); } } } diff --git a/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs b/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs index c0fcb459d..8963693ab 100644 --- a/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs +++ b/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs @@ -32,8 +32,8 @@ namespace Emby.Server.Implementations.ScheduledTasks /// <summary> /// The _task queue /// </summary> - private readonly ConcurrentQueue<Tuple<Type, TaskExecutionOptions>> _taskQueue = - new ConcurrentQueue<Tuple<Type, TaskExecutionOptions>>(); + private readonly ConcurrentQueue<Tuple<Type, TaskOptions>> _taskQueue = + new ConcurrentQueue<Tuple<Type, TaskOptions>>(); /// <summary> /// Gets or sets the json serializer. @@ -114,6 +114,10 @@ namespace Emby.Server.Implementations.ScheduledTasks { var path = Path.Combine(ApplicationPaths.CachePath, "startuptasks.txt"); + // ToDo: Fix this shit + if (!File.Exists(path)) + return; + List<string> lines; try @@ -126,7 +130,7 @@ namespace Emby.Server.Implementations.ScheduledTasks if (task != null) { - QueueScheduledTask(task, new TaskExecutionOptions()); + QueueScheduledTask(task, new TaskOptions()); } } @@ -143,7 +147,7 @@ namespace Emby.Server.Implementations.ScheduledTasks /// </summary> /// <typeparam name="T"></typeparam> /// <param name="options">Task options.</param> - public void CancelIfRunningAndQueue<T>(TaskExecutionOptions options) + public void CancelIfRunningAndQueue<T>(TaskOptions options) where T : IScheduledTask { var task = ScheduledTasks.First(t => t.ScheduledTask.GetType() == typeof(T)); @@ -155,7 +159,7 @@ namespace Emby.Server.Implementations.ScheduledTasks public void CancelIfRunningAndQueue<T>() where T : IScheduledTask { - CancelIfRunningAndQueue<T>(new TaskExecutionOptions()); + CancelIfRunningAndQueue<T>(new TaskOptions()); } /// <summary> @@ -174,7 +178,7 @@ namespace Emby.Server.Implementations.ScheduledTasks /// </summary> /// <typeparam name="T"></typeparam> /// <param name="options">Task options</param> - public void QueueScheduledTask<T>(TaskExecutionOptions options) + public void QueueScheduledTask<T>(TaskOptions options) where T : IScheduledTask { var scheduledTask = ScheduledTasks.FirstOrDefault(t => t.ScheduledTask.GetType() == typeof(T)); @@ -192,7 +196,7 @@ namespace Emby.Server.Implementations.ScheduledTasks public void QueueScheduledTask<T>() where T : IScheduledTask { - QueueScheduledTask<T>(new TaskExecutionOptions()); + QueueScheduledTask<T>(new TaskOptions()); } public void QueueIfNotRunning<T>() @@ -202,7 +206,7 @@ namespace Emby.Server.Implementations.ScheduledTasks if (task.State != TaskState.Running) { - QueueScheduledTask<T>(new TaskExecutionOptions()); + QueueScheduledTask<T>(new TaskOptions()); } } @@ -225,7 +229,7 @@ namespace Emby.Server.Implementations.ScheduledTasks { if (scheduledTask.State == TaskState.Idle) { - Execute(scheduledTask, new TaskExecutionOptions()); + Execute(scheduledTask, new TaskOptions()); } } } @@ -236,7 +240,7 @@ namespace Emby.Server.Implementations.ScheduledTasks /// </summary> /// <param name="task">The task.</param> /// <param name="options">The task options.</param> - public void QueueScheduledTask(IScheduledTask task, TaskExecutionOptions options) + public void QueueScheduledTask(IScheduledTask task, TaskOptions options) { var scheduledTask = ScheduledTasks.FirstOrDefault(t => t.ScheduledTask.GetType() == task.GetType()); @@ -255,7 +259,7 @@ namespace Emby.Server.Implementations.ScheduledTasks /// </summary> /// <param name="task">The task.</param> /// <param name="options">The task options.</param> - private void QueueScheduledTask(IScheduledTaskWorker task, TaskExecutionOptions options) + private void QueueScheduledTask(IScheduledTaskWorker task, TaskOptions options) { var type = task.ScheduledTask.GetType(); @@ -269,7 +273,7 @@ namespace Emby.Server.Implementations.ScheduledTasks return; } - _taskQueue.Enqueue(new Tuple<Type, TaskExecutionOptions>(type, options)); + _taskQueue.Enqueue(new Tuple<Type, TaskOptions>(type, options)); } } @@ -297,7 +301,6 @@ namespace Emby.Server.Implementations.ScheduledTasks public void Dispose() { Dispose(true); - GC.SuppressFinalize(this); } /// <summary> @@ -317,7 +320,7 @@ namespace Emby.Server.Implementations.ScheduledTasks ((ScheduledTaskWorker)task).Cancel(); } - public Task Execute(IScheduledTaskWorker task, TaskExecutionOptions options) + public Task Execute(IScheduledTaskWorker task, TaskOptions options) { return ((ScheduledTaskWorker)task).Execute(options); } @@ -362,9 +365,9 @@ namespace Emby.Server.Implementations.ScheduledTasks // Execute queued tasks lock (_taskQueue) { - var list = new List<Tuple<Type, TaskExecutionOptions>>(); + var list = new List<Tuple<Type, TaskOptions>>(); - Tuple<Type, TaskExecutionOptions> item; + Tuple<Type, TaskOptions> item; while (_taskQueue.TryDequeue(out item)) { if (list.All(i => i.Item1 != item.Item1)) diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs index 701358fd4..05fb08447 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs @@ -81,7 +81,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks // No biggie here. Nothing to delete } - return Task.FromResult(true); + return Task.CompletedTask; } diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteLogFileTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteLogFileTask.cs index f98b09659..d5a7ccadb 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteLogFileTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteLogFileTask.cs @@ -78,7 +78,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks progress.Report(100); - return Task.FromResult(true); + return Task.CompletedTask; } public string Key diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/ReloadLoggerFileTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/ReloadLoggerFileTask.cs index 032fa05a0..fbc3a309e 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/ReloadLoggerFileTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/ReloadLoggerFileTask.cs @@ -58,11 +58,9 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks progress.Report(0); - LogManager.ReloadLogger(ConfigurationManager.CommonConfiguration.EnableDebugLevelLogging + return LogManager.ReloadLogger(ConfigurationManager.CommonConfiguration.EnableDebugLevelLogging ? LogSeverity.Debug - : LogSeverity.Info); - - return Task.FromResult(true); + : LogSeverity.Info, cancellationToken); } /// <summary> @@ -71,7 +69,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks /// <value>The name.</value> public string Name { - get { return "Start new log file"; } + get { return "Rotate log file"; } } public string Key { get; } @@ -96,7 +94,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks public bool IsHidden { - get { return true; } + get { return false; } } public bool IsEnabled diff --git a/Emby.Server.Implementations/ScheduledTasks/WeeklyTrigger.cs b/Emby.Server.Implementations/ScheduledTasks/WeeklyTrigger.cs index 1a944ebf2..82b449917 100644 --- a/Emby.Server.Implementations/ScheduledTasks/WeeklyTrigger.cs +++ b/Emby.Server.Implementations/ScheduledTasks/WeeklyTrigger.cs @@ -24,12 +24,9 @@ namespace Emby.Server.Implementations.ScheduledTasks public DayOfWeek DayOfWeek { get; set; } /// <summary> - /// Gets the execution properties of this task. + /// Gets or sets the options of this task. /// </summary> - /// <value> - /// The execution properties of this task. - /// </value> - public TaskExecutionOptions TaskOptions { get; set; } + public TaskOptions TaskOptions { get; set; } /// <summary> /// Gets or sets the timer. @@ -100,7 +97,7 @@ namespace Emby.Server.Implementations.ScheduledTasks /// <summary> /// Occurs when [triggered]. /// </summary> - public event EventHandler<GenericEventArgs<TaskExecutionOptions>> Triggered; + public event EventHandler<EventArgs> Triggered; /// <summary> /// Called when [triggered]. @@ -109,7 +106,7 @@ namespace Emby.Server.Implementations.ScheduledTasks { if (Triggered != null) { - Triggered(this, new GenericEventArgs<TaskExecutionOptions>(TaskOptions)); + Triggered(this, EventArgs.Empty); } } } diff --git a/Emby.Server.Implementations/Security/AuthenticationRepository.cs b/Emby.Server.Implementations/Security/AuthenticationRepository.cs index b1877d776..45f7f1e95 100644 --- a/Emby.Server.Implementations/Security/AuthenticationRepository.cs +++ b/Emby.Server.Implementations/Security/AuthenticationRepository.cs @@ -11,19 +11,21 @@ using MediaBrowser.Model.Logging; using MediaBrowser.Model.Querying; using SQLitePCL.pretty; using MediaBrowser.Model.Extensions; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Model.Devices; namespace Emby.Server.Implementations.Security { public class AuthenticationRepository : BaseSqliteRepository, IAuthenticationRepository { - private readonly IServerApplicationPaths _appPaths; + private readonly IServerConfigurationManager _config; private readonly CultureInfo _usCulture = new CultureInfo("en-US"); - public AuthenticationRepository(ILogger logger, IServerApplicationPaths appPaths) + public AuthenticationRepository(ILogger logger, IServerConfigurationManager config) : base(logger) { - _appPaths = appPaths; - DbFilePath = Path.Combine(appPaths.DataPath, "authentication.db"); + _config = config; + DbFilePath = Path.Combine(config.ApplicationPaths.DataPath, "authentication.db"); } public void Initialize() @@ -32,99 +34,171 @@ namespace Emby.Server.Implementations.Security { RunDefaultInitialization(connection); + var tableNewlyCreated = !TableExists(connection, "Tokens"); + string[] queries = { - "create table if not exists AccessTokens (Id GUID PRIMARY KEY NOT NULL, AccessToken TEXT NOT NULL, DeviceId TEXT NOT NULL, AppName TEXT NOT NULL, AppVersion TEXT NOT NULL, DeviceName TEXT NOT NULL, UserId TEXT, IsActive BIT NOT NULL, DateCreated DATETIME NOT NULL, DateRevoked DATETIME)", - "create index if not exists idx_AccessTokens on AccessTokens(Id)" + "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", + "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)" }; connection.RunQueries(queries); - connection.RunInTransaction(db => + TryMigrate(connection, tableNewlyCreated); + } + } + + private void TryMigrate(ManagedConnection connection, bool tableNewlyCreated) + { + try + { + if (tableNewlyCreated && TableExists(connection, "AccessTokens")) { - var existingColumnNames = GetColumnNames(db, "AccessTokens"); + connection.RunInTransaction(db => + { + var existingColumnNames = GetColumnNames(db, "AccessTokens"); + + AddColumn(db, "AccessTokens", "UserName", "TEXT", existingColumnNames); + AddColumn(db, "AccessTokens", "DateLastActivity", "DATETIME", existingColumnNames); + AddColumn(db, "AccessTokens", "AppVersion", "TEXT", existingColumnNames); - AddColumn(db, "AccessTokens", "AppVersion", "TEXT", existingColumnNames); + }, TransactionMode); - }, TransactionMode); + connection.RunQueries(new[] + { + "update accesstokens set DateLastActivity=DateCreated where DateLastActivity is null", + "update accesstokens set DeviceName='Unknown' where DeviceName is null", + "update accesstokens set AppName='Unknown' where AppName is null", + "update accesstokens set AppVersion='1' where AppVersion is null", + "INSERT INTO Tokens (AccessToken, DeviceId, AppName, AppVersion, DeviceName, UserId, UserName, IsActive, DateCreated, DateLastActivity) SELECT AccessToken, DeviceId, AppName, AppVersion, DeviceName, UserId, UserName, IsActive, DateCreated, DateLastActivity FROM AccessTokens where deviceid not null and devicename not null and appname not null and isactive=1" + }); + } + } + catch (Exception ex) + { + Logger.ErrorException("Error migrating authentication database", ex); } } - public void Create(AuthenticationInfo info, CancellationToken cancellationToken) + public void Create(AuthenticationInfo info) { - info.Id = Guid.NewGuid().ToString("N"); + if (info == null) + { + throw new ArgumentNullException("info"); + } + + using (WriteLock.Write()) + { + using (var connection = CreateConnection()) + { + connection.RunInTransaction(db => + { + using (var statement = db.PrepareStatement("insert into Tokens (AccessToken, DeviceId, AppName, AppVersion, DeviceName, UserId, UserName, IsActive, DateCreated, DateLastActivity) values (@AccessToken, @DeviceId, @AppName, @AppVersion, @DeviceName, @UserId, @UserName, @IsActive, @DateCreated, @DateLastActivity)")) + { + statement.TryBind("@AccessToken", info.AccessToken); + + statement.TryBind("@DeviceId", info.DeviceId); + statement.TryBind("@AppName", info.AppName); + statement.TryBind("@AppVersion", info.AppVersion); + statement.TryBind("@DeviceName", info.DeviceName); + statement.TryBind("@UserId", (info.UserId.Equals(Guid.Empty) ? null : info.UserId.ToString("N"))); + statement.TryBind("@UserName", info.UserName); + statement.TryBind("@IsActive", true); + statement.TryBind("@DateCreated", info.DateCreated.ToDateTimeParamValue()); + statement.TryBind("@DateLastActivity", info.DateLastActivity.ToDateTimeParamValue()); + + statement.MoveNext(); + } - Update(info, cancellationToken); + }, TransactionMode); + } + } } - public void Update(AuthenticationInfo info, CancellationToken cancellationToken) + public void Update(AuthenticationInfo info) { if (info == null) { - throw new ArgumentNullException("info"); + throw new ArgumentNullException("entry"); } - cancellationToken.ThrowIfCancellationRequested(); - using (WriteLock.Write()) { using (var connection = CreateConnection()) { connection.RunInTransaction(db => { - using (var statement = db.PrepareStatement("replace into AccessTokens (Id, AccessToken, DeviceId, AppName, AppVersion, DeviceName, UserId, IsActive, DateCreated, DateRevoked) values (@Id, @AccessToken, @DeviceId, @AppName, @AppVersion, @DeviceName, @UserId, @IsActive, @DateCreated, @DateRevoked)")) + using (var statement = db.PrepareStatement("Update Tokens set AccessToken=@AccessToken, DeviceId=@DeviceId, AppName=@AppName, AppVersion=@AppVersion, DeviceName=@DeviceName, UserId=@UserId, UserName=@UserName, DateCreated=@DateCreated, DateLastActivity=@DateLastActivity where Id=@Id")) { - statement.TryBind("@Id", info.Id.ToGuidBlob()); + statement.TryBind("@Id", info.Id); + statement.TryBind("@AccessToken", info.AccessToken); statement.TryBind("@DeviceId", info.DeviceId); statement.TryBind("@AppName", info.AppName); statement.TryBind("@AppVersion", info.AppVersion); statement.TryBind("@DeviceName", info.DeviceName); - statement.TryBind("@UserId", info.UserId); - statement.TryBind("@IsActive", info.IsActive); + statement.TryBind("@UserId", (info.UserId.Equals(Guid.Empty) ? null : info.UserId.ToString("N"))); + statement.TryBind("@UserName", info.UserName); statement.TryBind("@DateCreated", info.DateCreated.ToDateTimeParamValue()); - - if (info.DateRevoked.HasValue) - { - statement.TryBind("@DateRevoked", info.DateRevoked.Value.ToDateTimeParamValue()); - } - else - { - statement.TryBindNull("@DateRevoked"); - } + statement.TryBind("@DateLastActivity", info.DateLastActivity.ToDateTimeParamValue()); statement.MoveNext(); } + }, TransactionMode); + } + } + } + + public void Delete(AuthenticationInfo info) + { + if (info == null) + { + throw new ArgumentNullException("entry"); + } + using (WriteLock.Write()) + { + using (var connection = CreateConnection()) + { + connection.RunInTransaction(db => + { + using (var statement = db.PrepareStatement("Delete from Tokens where Id=@Id")) + { + statement.TryBind("@Id", info.Id); + + statement.MoveNext(); + } }, TransactionMode); } } } - private const string BaseSelectText = "select Id, AccessToken, DeviceId, AppName, AppVersion, DeviceName, UserId, IsActive, DateCreated, DateRevoked from AccessTokens"; + private const string BaseSelectText = "select Tokens.Id, AccessToken, DeviceId, AppName, AppVersion, DeviceName, UserId, UserName, DateCreated, DateLastActivity, Devices.CustomName from Tokens left join Devices on Tokens.DeviceId=Devices.Id"; private void BindAuthenticationQueryParams(AuthenticationInfoQuery query, IStatement statement) { - if (!string.IsNullOrWhiteSpace(query.AccessToken)) + if (!string.IsNullOrEmpty(query.AccessToken)) { statement.TryBind("@AccessToken", query.AccessToken); } - if (!string.IsNullOrWhiteSpace(query.UserId)) + if (!query.UserId.Equals(Guid.Empty)) { - statement.TryBind("@UserId", query.UserId); + statement.TryBind("@UserId", query.UserId.ToString("N")); } - if (!string.IsNullOrWhiteSpace(query.DeviceId)) + if (!string.IsNullOrEmpty(query.DeviceId)) { statement.TryBind("@DeviceId", query.DeviceId); } - - if (query.IsActive.HasValue) - { - statement.TryBind("@IsActive", query.IsActive.Value); - } } public QueryResult<AuthenticationInfo> Get(AuthenticationInfoQuery query) @@ -138,26 +212,19 @@ namespace Emby.Server.Implementations.Security var whereClauses = new List<string>(); - var startIndex = query.StartIndex ?? 0; - - if (!string.IsNullOrWhiteSpace(query.AccessToken)) + if (!string.IsNullOrEmpty(query.AccessToken)) { whereClauses.Add("AccessToken=@AccessToken"); } - if (!string.IsNullOrWhiteSpace(query.UserId)) - { - whereClauses.Add("UserId=@UserId"); - } - - if (!string.IsNullOrWhiteSpace(query.DeviceId)) + if (!string.IsNullOrEmpty(query.DeviceId)) { whereClauses.Add("DeviceId=@DeviceId"); } - if (query.IsActive.HasValue) + if (!query.UserId.Equals(Guid.Empty)) { - whereClauses.Add("IsActive=@IsActive"); + whereClauses.Add("UserId=@UserId"); } if (query.HasUser.HasValue) @@ -176,28 +243,23 @@ namespace Emby.Server.Implementations.Security string.Empty : " where " + string.Join(" AND ", whereClauses.ToArray(whereClauses.Count)); - if (startIndex > 0) - { - var pagingWhereText = whereClauses.Count == 0 ? - string.Empty : - " where " + string.Join(" AND ", whereClauses.ToArray(whereClauses.Count)); - - whereClauses.Add(string.Format("Id NOT IN (SELECT Id FROM AccessTokens {0} ORDER BY DateCreated LIMIT {1})", - pagingWhereText, - startIndex.ToString(_usCulture))); - } + commandText += whereTextWithoutPaging; - var whereText = whereClauses.Count == 0 ? - string.Empty : - " where " + string.Join(" AND ", whereClauses.ToArray(whereClauses.Count)); + commandText += " ORDER BY DateLastActivity desc"; - commandText += whereText; + if (query.Limit.HasValue || query.StartIndex.HasValue) + { + var offset = query.StartIndex ?? 0; - commandText += " ORDER BY DateCreated"; + if (query.Limit.HasValue || offset > 0) + { + commandText += " LIMIT " + (query.Limit ?? int.MaxValue).ToString(CultureInfo.InvariantCulture); + } - if (query.Limit.HasValue) - { - commandText += " LIMIT " + query.Limit.Value.ToString(_usCulture); + if (offset > 0) + { + commandText += " OFFSET " + offset.ToString(CultureInfo.InvariantCulture); + } } var list = new List<AuthenticationInfo>(); @@ -212,7 +274,7 @@ namespace Emby.Server.Implementations.Security var statementTexts = new List<string>(); statementTexts.Add(commandText); - statementTexts.Add("select count (Id) from AccessTokens" + whereTextWithoutPaging); + statementTexts.Add("select count (Id) from Tokens" + whereTextWithoutPaging); var statements = PrepareAllSafe(db, statementTexts) .ToList(); @@ -244,38 +306,11 @@ namespace Emby.Server.Implementations.Security } } - public AuthenticationInfo Get(string id) - { - if (string.IsNullOrEmpty(id)) - { - throw new ArgumentNullException("id"); - } - - using (WriteLock.Read()) - { - using (var connection = CreateConnection(true)) - { - var commandText = BaseSelectText + " where Id=@Id"; - - using (var statement = connection.PrepareStatement(commandText)) - { - statement.BindParameters["@Id"].Bind(id.ToGuidBlob()); - - foreach (var row in statement.ExecuteQuery()) - { - return Get(row); - } - return null; - } - } - } - } - private AuthenticationInfo Get(IReadOnlyList<IResultSetValue> reader) { var info = new AuthenticationInfo { - Id = reader[0].ReadGuidFromBlob().ToString("N"), + Id = reader[0].ToInt64(), AccessToken = reader[1].ToString() }; @@ -301,18 +336,95 @@ namespace Emby.Server.Implementations.Security if (reader[6].SQLiteType != SQLiteType.Null) { - info.UserId = reader[6].ToString(); + info.UserId = new Guid(reader[6].ToString()); + } + + if (reader[7].SQLiteType != SQLiteType.Null) + { + info.UserName = reader[7].ToString(); } - info.IsActive = reader[7].ToBool(); info.DateCreated = reader[8].ReadDateTime(); if (reader[9].SQLiteType != SQLiteType.Null) { - info.DateRevoked = reader[9].ReadDateTime(); + info.DateLastActivity = reader[9].ReadDateTime(); + } + else + { + info.DateLastActivity = info.DateCreated; + } + + if (reader[10].SQLiteType != SQLiteType.Null) + { + info.DeviceName = reader[10].ToString(); } return info; } + + public DeviceOptions GetDeviceOptions(string deviceId) + { + using (WriteLock.Read()) + { + using (var connection = CreateConnection(true)) + { + return connection.RunInTransaction(db => + { + using (var statement = PrepareStatementSafe(db, "select CustomName from Devices where Id=@DeviceId")) + { + statement.TryBind("@DeviceId", deviceId); + + var result = new DeviceOptions(); + + foreach (var row in statement.ExecuteQuery()) + { + if (row[0].SQLiteType != SQLiteType.Null) + { + result.CustomName = row[0].ToString(); + } + } + + return result; + } + + }, ReadTransactionMode); + } + } + } + + public void UpdateDeviceOptions(string deviceId, DeviceOptions options) + { + if (options == null) + { + throw new ArgumentNullException("options"); + } + + using (WriteLock.Write()) + { + using (var connection = CreateConnection()) + { + connection.RunInTransaction(db => + { + using (var statement = db.PrepareStatement("replace into devices (Id, CustomName, Capabilities) VALUES (@Id, @CustomName, (Select Capabilities from Devices where Id=@Id))")) + { + statement.TryBind("@Id", deviceId); + + if (string.IsNullOrWhiteSpace(options.CustomName)) + { + statement.TryBindNull("@CustomName"); + } + else + { + statement.TryBind("@CustomName", options.CustomName); + } + + statement.MoveNext(); + } + + }, TransactionMode); + } + } + } } } diff --git a/Emby.Server.Implementations/Security/MBLicenseFile.cs b/Emby.Server.Implementations/Security/MBLicenseFile.cs index dc0e8b161..1810cbcd2 100644 --- a/Emby.Server.Implementations/Security/MBLicenseFile.cs +++ b/Emby.Server.Implementations/Security/MBLicenseFile.cs @@ -22,12 +22,8 @@ namespace Emby.Server.Implementations.Security get { return _regKey; } set { - if (value != _regKey) - { - //if key is changed - clear out our saved validations - _updateRecords.Clear(); - _regKey = value; - } + _updateRecords.Clear(); + _regKey = value; } } @@ -114,14 +110,14 @@ namespace Emby.Server.Implementations.Security { lock (_fileLock) { - _fileSystem.WriteAllBytes(licenseFile, new byte[] { }); + _fileSystem.WriteAllBytes(licenseFile, Array.Empty<byte>()); } } catch (IOException) { lock (_fileLock) { - _fileSystem.WriteAllBytes(licenseFile, new byte[] { }); + _fileSystem.WriteAllBytes(licenseFile, Array.Empty<byte>()); } } } diff --git a/Emby.Server.Implementations/Security/PluginSecurityManager.cs b/Emby.Server.Implementations/Security/PluginSecurityManager.cs index 615ffa1f4..c9c68703e 100644 --- a/Emby.Server.Implementations/Security/PluginSecurityManager.cs +++ b/Emby.Server.Implementations/Security/PluginSecurityManager.cs @@ -26,30 +26,11 @@ namespace Emby.Server.Implementations.Security private const string MBValidateUrl = "https://mb3admin.com/admin/service/registration/validate"; private const string AppstoreRegUrl = /*MbAdmin.HttpsUrl*/ "https://mb3admin.com/admin/service/appstore/register"; - /// <summary> - /// The _is MB supporter - /// </summary> - private bool? _isMbSupporter; - /// <summary> - /// The _is MB supporter initialized - /// </summary> - private bool _isMbSupporterInitialized; - /// <summary> - /// The _is MB supporter sync lock - /// </summary> - private object _isMbSupporterSyncLock = new object(); - - /// <summary> - /// Gets a value indicating whether this instance is MB supporter. - /// </summary> - /// <value><c>true</c> if this instance is MB supporter; otherwise, <c>false</c>.</value> - public bool IsMBSupporter + public async Task<bool> IsSupporter() { - get - { - LazyInitializer.EnsureInitialized(ref _isMbSupporter, ref _isMbSupporterInitialized, ref _isMbSupporterSyncLock, () => GetSupporterRegistrationStatus().Result.IsRegistered); - return _isMbSupporter.Value; - } + var result = await GetRegistrationStatusInternal("MBSupporter", false, _appHost.ApplicationVersion.ToString(), CancellationToken.None).ConfigureAwait(false); + + return result.IsRegistered; } private MBLicenseFile _licenseFile; @@ -66,15 +47,6 @@ namespace Emby.Server.Implementations.Security private readonly IFileSystem _fileSystem; private readonly ICryptoProvider _cryptographyProvider; - private IEnumerable<IRequiresRegistration> _registeredEntities; - protected IEnumerable<IRequiresRegistration> RegisteredEntities - { - get - { - return _registeredEntities ?? (_registeredEntities = _appHost.GetExports<IRequiresRegistration>()); - } - } - /// <summary> /// Initializes a new instance of the <see cref="PluginSecurityManager" /> class. /// </summary> @@ -96,45 +68,12 @@ namespace Emby.Server.Implementations.Security } /// <summary> - /// Load all registration info for all entities that require registration - /// </summary> - /// <returns></returns> - public async Task LoadAllRegistrationInfo() - { - var tasks = new List<Task>(); - - ResetSupporterInfo(); - tasks.AddRange(RegisteredEntities.Select(i => i.LoadRegistrationInfoAsync())); - await Task.WhenAll(tasks); - } - - /// <summary> /// Gets the registration status. /// This overload supports existing plug-ins. /// </summary> - /// <param name="feature">The feature.</param> - /// <param name="mb2Equivalent">The MB2 equivalent.</param> - /// <returns>Task{MBRegistrationRecord}.</returns> - public Task<MBRegistrationRecord> GetRegistrationStatus(string feature, string mb2Equivalent = null) - { - return GetRegistrationStatusInternal(feature, mb2Equivalent); - } - - /// <summary> - /// Gets the registration status. - /// </summary> - /// <param name="feature">The feature.</param> - /// <param name="mb2Equivalent">The MB2 equivalent.</param> - /// <param name="version">The version of this feature</param> - /// <returns>Task{MBRegistrationRecord}.</returns> - public Task<MBRegistrationRecord> GetRegistrationStatus(string feature, string mb2Equivalent, string version) - { - return GetRegistrationStatusInternal(feature, mb2Equivalent, version); - } - - private Task<MBRegistrationRecord> GetSupporterRegistrationStatus() + public Task<MBRegistrationRecord> GetRegistrationStatus(string feature) { - return GetRegistrationStatusInternal("MBSupporter", null, _appHost.ApplicationVersion.ToString()); + return GetRegistrationStatusInternal(feature, false, null, CancellationToken.None); } /// <summary> @@ -149,20 +88,24 @@ namespace Emby.Server.Implementations.Security } set { - var newValue = value; - if (newValue != null) - { - newValue = newValue.Trim(); - } + throw new Exception("Please call UpdateSupporterKey"); + } + } - if (newValue != LicenseFile.RegKey) - { - LicenseFile.RegKey = newValue; - LicenseFile.Save(); + public async Task UpdateSupporterKey(string newValue) + { + if (newValue != null) + { + newValue = newValue.Trim(); + } - // re-load registration info - Task.Run(() => LoadAllRegistrationInfo()); - } + if (!string.Equals(newValue, LicenseFile.RegKey, StringComparison.Ordinal)) + { + LicenseFile.RegKey = newValue; + LicenseFile.Save(); + + // Reset this + await GetRegistrationStatusInternal("MBSupporter", true, _appHost.ApplicationVersion.ToString(), CancellationToken.None).ConfigureAwait(false); } } @@ -187,7 +130,7 @@ namespace Emby.Server.Implementations.Security { using (var response = await _httpClient.Post(options).ConfigureAwait(false)) { - var reg = _jsonSerializer.DeserializeFromStream<RegRecord>(response.Content); + var reg = await _jsonSerializer.DeserializeFromStreamAsync<RegRecord>(response.Content).ConfigureAwait(false); if (reg == null) { @@ -197,7 +140,7 @@ namespace Emby.Server.Implementations.Security } if (!String.IsNullOrEmpty(reg.key)) { - SupporterKey = reg.key; + await UpdateSupporterKey(reg.key).ConfigureAwait(false); } } @@ -241,97 +184,113 @@ namespace Emby.Server.Implementations.Security } } - private async Task<MBRegistrationRecord> GetRegistrationStatusInternal(string feature, - string mb2Equivalent = null, - string version = null) - { - var regInfo = LicenseFile.GetRegInfo(feature); - var lastChecked = regInfo == null ? DateTime.MinValue : regInfo.LastChecked; - var expDate = regInfo == null ? DateTime.MinValue : regInfo.ExpirationDate; + private SemaphoreSlim _regCheckLock = new SemaphoreSlim(1, 1); - var maxCacheDays = 14; - var nextCheckDate = new [] { expDate, lastChecked.AddDays(maxCacheDays) }.Min(); + private async Task<MBRegistrationRecord> GetRegistrationStatusInternal(string feature, bool forceCallToServer, string version, CancellationToken cancellationToken) + { + await _regCheckLock.WaitAsync(cancellationToken).ConfigureAwait(false); - if (nextCheckDate > DateTime.UtcNow.AddDays(maxCacheDays)) + try { - nextCheckDate = DateTime.MinValue; - } + var regInfo = LicenseFile.GetRegInfo(feature); + var lastChecked = regInfo == null ? DateTime.MinValue : regInfo.LastChecked; + var expDate = regInfo == null ? DateTime.MinValue : regInfo.ExpirationDate; - //check the reg file first to alleviate strain on the MB admin server - must actually check in every 30 days tho - var reg = new RegRecord - { - // Cache the result for up to a week - registered = regInfo != null && nextCheckDate >= DateTime.UtcNow && expDate >= DateTime.UtcNow, - expDate = expDate - }; + var maxCacheDays = 14; + var nextCheckDate = new[] { expDate, lastChecked.AddDays(maxCacheDays) }.Min(); - var success = reg.registered; + if (nextCheckDate > DateTime.UtcNow.AddDays(maxCacheDays)) + { + nextCheckDate = DateTime.MinValue; + } - if (!(lastChecked > DateTime.UtcNow.AddDays(-1)) || !reg.registered) - { - var data = new Dictionary<string, string> + //check the reg file first to alleviate strain on the MB admin server - must actually check in every 30 days tho + var reg = new RegRecord { - { "feature", feature }, - { "key", SupporterKey }, - { "mac", _appHost.SystemId }, - { "systemid", _appHost.SystemId }, - { "mb2equiv", mb2Equivalent }, - { "ver", version }, - { "platform", _appHost.OperatingSystemDisplayName } + // Cache the result for up to a week + registered = regInfo != null && nextCheckDate >= DateTime.UtcNow && expDate >= DateTime.UtcNow, + expDate = expDate }; - try + var key = SupporterKey; + + if (!forceCallToServer && string.IsNullOrWhiteSpace(key)) { - var options = new HttpRequestOptions - { - Url = MBValidateUrl, + return new MBRegistrationRecord(); + } - // Seeing block length errors - EnableHttpCompression = false, - BufferContent = false - }; + var success = reg.registered; - options.SetPostData(data); + if (!(lastChecked > DateTime.UtcNow.AddDays(-1)) || (!reg.registered)) + { + var data = new Dictionary<string, string> + { + { "feature", feature }, + { "key", key }, + { "mac", _appHost.SystemId }, + { "systemid", _appHost.SystemId }, + { "ver", version }, + { "platform", _appHost.OperatingSystemDisplayName } + }; - using (var response = (await _httpClient.Post(options).ConfigureAwait(false))) + try { - using (var json = response.Content) + var options = new HttpRequestOptions { - reg = _jsonSerializer.DeserializeFromStream<RegRecord>(json); - success = true; + Url = MBValidateUrl, + + // Seeing block length errors + EnableHttpCompression = false, + BufferContent = false, + CancellationToken = cancellationToken + }; + + options.SetPostData(data); + + using (var response = (await _httpClient.Post(options).ConfigureAwait(false))) + { + using (var json = response.Content) + { + reg = await _jsonSerializer.DeserializeFromStreamAsync<RegRecord>(json).ConfigureAwait(false); + success = true; + } + } + + if (reg.registered) + { + _logger.Info("Registered for feature {0}", feature); + LicenseFile.AddRegCheck(feature, reg.expDate); + } + else + { + _logger.Info("Not registered for feature {0}", feature); + LicenseFile.RemoveRegCheck(feature); } - } - if (reg.registered) - { - _logger.Info("Registered for feature {0}", feature); - LicenseFile.AddRegCheck(feature, reg.expDate); } - else + catch (Exception e) { - _logger.Info("Not registered for feature {0}", feature); - LicenseFile.RemoveRegCheck(feature); + _logger.ErrorException("Error checking registration status of {0}", e, feature); } - - } - catch (Exception e) - { - _logger.ErrorException("Error checking registration status of {0}", e, feature); } - } - var record = new MBRegistrationRecord - { - IsRegistered = reg.registered, - ExpirationDate = reg.expDate, - RegChecked = true, - RegError = !success - }; + var record = new MBRegistrationRecord + { + IsRegistered = reg.registered, + ExpirationDate = reg.expDate, + RegChecked = true, + RegError = !success + }; - record.TrialVersion = IsInTrial(reg.expDate, record.RegChecked, record.IsRegistered); - record.IsValid = !record.RegChecked || record.IsRegistered || record.TrialVersion; + record.TrialVersion = IsInTrial(reg.expDate, record.RegChecked, record.IsRegistered); + record.IsValid = !record.RegChecked || record.IsRegistered || record.TrialVersion; - return record; + return record; + } + finally + { + _regCheckLock.Release(); + } } private bool IsInTrial(DateTime expirationDate, bool regChecked, bool isRegistered) @@ -346,14 +305,5 @@ namespace Emby.Server.Implementations.Security return isInTrial && !isRegistered; } - - /// <summary> - /// Resets the supporter info. - /// </summary> - private void ResetSupporterInfo() - { - _isMbSupporter = null; - _isMbSupporterInitialized = false; - } } }
\ No newline at end of file diff --git a/Emby.Server.Implementations/Serialization/JsonSerializer.cs b/Emby.Server.Implementations/Serialization/JsonSerializer.cs index c9db33689..26371d21d 100644 --- a/Emby.Server.Implementations/Serialization/JsonSerializer.cs +++ b/Emby.Server.Implementations/Serialization/JsonSerializer.cs @@ -3,6 +3,7 @@ using System.IO; using MediaBrowser.Model.IO; using MediaBrowser.Model.Logging; using MediaBrowser.Model.Serialization; +using System.Threading.Tasks; namespace Emby.Common.Implementations.Serialization { @@ -60,7 +61,7 @@ namespace Emby.Common.Implementations.Serialization throw new ArgumentNullException("file"); } - using (Stream stream = _fileSystem.GetFileStream(file, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read)) + using (Stream stream = _fileSystem.GetFileStream(file, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read)) { SerializeToStream(obj, stream); } @@ -68,7 +69,7 @@ namespace Emby.Common.Implementations.Serialization private Stream OpenFile(string path) { - _logger.Debug("Deserializing file {0}", path); + //_logger.Debug("Deserializing file {0}", path); return new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, 131072); } @@ -135,6 +136,21 @@ namespace Emby.Common.Implementations.Serialization return ServiceStack.Text.JsonSerializer.DeserializeFromStream<T>(stream); } + public async Task<T> DeserializeFromStreamAsync<T>(Stream stream) + { + if (stream == null) + { + throw new ArgumentNullException("stream"); + } + + using (var reader = new StreamReader(stream)) + { + var json = await reader.ReadToEndAsync().ConfigureAwait(false); + + return ServiceStack.Text.JsonSerializer.DeserializeFromString<T>(json); + } + } + /// <summary> /// Deserializes from string. /// </summary> @@ -174,6 +190,26 @@ namespace Emby.Common.Implementations.Serialization return ServiceStack.Text.JsonSerializer.DeserializeFromStream(type, stream); } + public async Task<object> DeserializeFromStreamAsync(Stream stream, Type type) + { + if (stream == null) + { + throw new ArgumentNullException("stream"); + } + + if (type == null) + { + throw new ArgumentNullException("type"); + } + + using (var reader = new StreamReader(stream)) + { + var json = await reader.ReadToEndAsync().ConfigureAwait(false); + + return ServiceStack.Text.JsonSerializer.DeserializeFromString(json, type); + } + } + /// <summary> /// Configures this instance. /// </summary> @@ -184,6 +220,18 @@ namespace Emby.Common.Implementations.Serialization ServiceStack.Text.JsConfig.IncludeNullValues = false; ServiceStack.Text.JsConfig.AlwaysUseUtc = true; ServiceStack.Text.JsConfig.AssumeUtc = true; + + ServiceStack.Text.JsConfig<Guid>.SerializeFn = SerializeGuid; + } + + private string SerializeGuid(Guid guid) + { + if (guid.Equals(Guid.Empty)) + { + return null; + } + + return guid.ToString("N"); } /// <summary> diff --git a/Emby.Server.Implementations/ServerApplicationPaths.cs b/Emby.Server.Implementations/ServerApplicationPaths.cs index 3e3f7e0d7..1686a548b 100644 --- a/Emby.Server.Implementations/ServerApplicationPaths.cs +++ b/Emby.Server.Implementations/ServerApplicationPaths.cs @@ -58,26 +58,6 @@ namespace Emby.Server.Implementations } /// <summary> - /// The _ibn path - /// </summary> - private string _ibnPath; - /// <summary> - /// Gets the path to the Images By Name directory - /// </summary> - /// <value>The images by name path.</value> - public string ItemsByNamePath - { - get - { - return _ibnPath ?? (_ibnPath = Path.Combine(ProgramDataPath, "ImagesByName")); - } - set - { - _ibnPath = value; - } - } - - /// <summary> /// Gets the path to the People directory /// </summary> /// <value>The people path.</value> @@ -85,7 +65,7 @@ namespace Emby.Server.Implementations { get { - return Path.Combine(ItemsByNamePath, "People"); + return Path.Combine(InternalMetadataPath, "People"); } } @@ -93,7 +73,7 @@ namespace Emby.Server.Implementations { get { - return Path.Combine(ItemsByNamePath, "artists"); + return Path.Combine(InternalMetadataPath, "artists"); } } @@ -105,7 +85,7 @@ namespace Emby.Server.Implementations { get { - return Path.Combine(ItemsByNamePath, "Genre"); + return Path.Combine(InternalMetadataPath, "Genre"); } } @@ -117,7 +97,7 @@ namespace Emby.Server.Implementations { get { - return Path.Combine(ItemsByNamePath, "MusicGenre"); + return Path.Combine(InternalMetadataPath, "MusicGenre"); } } @@ -129,7 +109,7 @@ namespace Emby.Server.Implementations { get { - return Path.Combine(ItemsByNamePath, "Studio"); + return Path.Combine(InternalMetadataPath, "Studio"); } } @@ -141,7 +121,7 @@ namespace Emby.Server.Implementations { get { - return Path.Combine(ItemsByNamePath, "Year"); + return Path.Combine(InternalMetadataPath, "Year"); } } @@ -153,7 +133,7 @@ namespace Emby.Server.Implementations { get { - return Path.Combine(ItemsByNamePath, "general"); + return Path.Combine(InternalMetadataPath, "general"); } } @@ -165,7 +145,7 @@ namespace Emby.Server.Implementations { get { - return Path.Combine(ItemsByNamePath, "ratings"); + return Path.Combine(InternalMetadataPath, "ratings"); } } @@ -177,7 +157,7 @@ namespace Emby.Server.Implementations { get { - return Path.Combine(ItemsByNamePath, "mediainfo"); + return Path.Combine(InternalMetadataPath, "mediainfo"); } } @@ -193,12 +173,21 @@ namespace Emby.Server.Implementations } } + private string _defaultTranscodingTempPath; + public string DefaultTranscodingTempPath + { + get + { + return _defaultTranscodingTempPath ?? (_defaultTranscodingTempPath = Path.Combine(ProgramDataPath, "transcoding-temp")); + } + } + private string _transcodingTempPath; public string TranscodingTempPath { get { - return _transcodingTempPath ?? (_transcodingTempPath = Path.Combine(ProgramDataPath, "transcoding-temp")); + return _transcodingTempPath ?? (_transcodingTempPath = DefaultTranscodingTempPath); } set { @@ -210,17 +199,26 @@ namespace Emby.Server.Implementations { var path = TranscodingTempPath; - try - { - Directory.CreateDirectory(path); - return path; - } - catch + if (!string.Equals(path, DefaultTranscodingTempPath, StringComparison.OrdinalIgnoreCase)) { - path = Path.Combine(ProgramDataPath, "transcoding-temp"); - Directory.CreateDirectory(path); - return path; + try + { + Directory.CreateDirectory(path); + + var testPath = Path.Combine(path, Guid.NewGuid().ToString()); + Directory.CreateDirectory(testPath); + Directory.Delete(testPath); + + return path; + } + catch + { + } } + + path = DefaultTranscodingTempPath; + Directory.CreateDirectory(path); + return path; } /// <summary> @@ -231,7 +229,7 @@ namespace Emby.Server.Implementations { get { - return Path.Combine(ItemsByNamePath, "GameGenre"); + return Path.Combine(InternalMetadataPath, "GameGenre"); } } @@ -247,5 +245,15 @@ namespace Emby.Server.Implementations _internalMetadataPath = value; } } + + private const string _virtualInternalMetadataPath = "%MetadataPath%"; + public string VirtualInternalMetadataPath + { + get + { + return _virtualInternalMetadataPath; + } + } + } } diff --git a/Emby.Server.Implementations/ServerManager/ServerManager.cs b/Emby.Server.Implementations/ServerManager/ServerManager.cs deleted file mode 100644 index b267f928b..000000000 --- a/Emby.Server.Implementations/ServerManager/ServerManager.cs +++ /dev/null @@ -1,357 +0,0 @@ -using MediaBrowser.Common.Events; -using MediaBrowser.Controller; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Net; -using MediaBrowser.Model.Events; -using MediaBrowser.Model.Logging; -using MediaBrowser.Model.Net; -using MediaBrowser.Model.Serialization; -using System; -using System.Collections.Generic; -using System.Collections.Specialized; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; - -using MediaBrowser.Model.IO; -using MediaBrowser.Model.Services; -using MediaBrowser.Model.Text; - -namespace Emby.Server.Implementations.ServerManager -{ - /// <summary> - /// Manages the Http Server, Udp Server and WebSocket connections - /// </summary> - public class ServerManager : IServerManager - { - /// <summary> - /// Both the Ui and server will have a built-in HttpServer. - /// People will inevitably want remote control apps so it's needed in the Ui too. - /// </summary> - /// <value>The HTTP server.</value> - private IHttpServer HttpServer { get; set; } - - /// <summary> - /// Gets or sets the json serializer. - /// </summary> - /// <value>The json serializer.</value> - private readonly IJsonSerializer _jsonSerializer; - - /// <summary> - /// The web socket connections - /// </summary> - private readonly List<IWebSocketConnection> _webSocketConnections = new List<IWebSocketConnection>(); - /// <summary> - /// Gets the web socket connections. - /// </summary> - /// <value>The web socket connections.</value> - public IEnumerable<IWebSocketConnection> WebSocketConnections - { - get { return _webSocketConnections; } - } - - public event EventHandler<GenericEventArgs<IWebSocketConnection>> WebSocketConnected; - - /// <summary> - /// The _logger - /// </summary> - private readonly ILogger _logger; - - /// <summary> - /// The _application host - /// </summary> - private readonly IServerApplicationHost _applicationHost; - - /// <summary> - /// Gets or sets the configuration manager. - /// </summary> - /// <value>The configuration manager.</value> - private IServerConfigurationManager ConfigurationManager { get; set; } - - /// <summary> - /// Gets the web socket listeners. - /// </summary> - /// <value>The web socket listeners.</value> - private readonly List<IWebSocketListener> _webSocketListeners = new List<IWebSocketListener>(); - - private bool _disposed; - private readonly IMemoryStreamFactory _memoryStreamProvider; - private readonly ITextEncoding _textEncoding; - - /// <summary> - /// Initializes a new instance of the <see cref="ServerManager" /> class. - /// </summary> - /// <param name="applicationHost">The application host.</param> - /// <param name="jsonSerializer">The json serializer.</param> - /// <param name="logger">The logger.</param> - /// <param name="configurationManager">The configuration manager.</param> - /// <exception cref="System.ArgumentNullException">applicationHost</exception> - public ServerManager(IServerApplicationHost applicationHost, IJsonSerializer jsonSerializer, ILogger logger, IServerConfigurationManager configurationManager, IMemoryStreamFactory memoryStreamProvider, ITextEncoding textEncoding) - { - if (applicationHost == null) - { - throw new ArgumentNullException("applicationHost"); - } - if (jsonSerializer == null) - { - throw new ArgumentNullException("jsonSerializer"); - } - if (logger == null) - { - throw new ArgumentNullException("logger"); - } - - _logger = logger; - _jsonSerializer = jsonSerializer; - _applicationHost = applicationHost; - ConfigurationManager = configurationManager; - _memoryStreamProvider = memoryStreamProvider; - _textEncoding = textEncoding; - } - - /// <summary> - /// Starts this instance. - /// </summary> - public void Start(string[] urlPrefixes) - { - ReloadHttpServer(urlPrefixes); - } - - /// <summary> - /// Restarts the Http Server, or starts it if not currently running - /// </summary> - private void ReloadHttpServer(string[] urlPrefixes) - { - _logger.Info("Loading Http Server"); - - try - { - HttpServer = _applicationHost.Resolve<IHttpServer>(); - HttpServer.StartServer(urlPrefixes); - } - catch (Exception ex) - { - var msg = string.Equals(ex.GetType().Name, "SocketException", StringComparison.OrdinalIgnoreCase) - ? "The http server is unable to start due to a Socket error. This can occasionally happen when the operating system takes longer than usual to release the IP bindings from the previous session. This can take up to five minutes. Please try waiting or rebooting the system." - : "Error starting Http Server"; - - _logger.ErrorException(msg, ex); - - throw; - } - - HttpServer.WebSocketConnected += HttpServer_WebSocketConnected; - } - - /// <summary> - /// Handles the WebSocketConnected event of the HttpServer control. - /// </summary> - /// <param name="sender">The source of the event.</param> - /// <param name="e">The <see cref="WebSocketConnectEventArgs" /> instance containing the event data.</param> - void HttpServer_WebSocketConnected(object sender, WebSocketConnectEventArgs e) - { - if (_disposed) - { - return; - } - - var connection = new WebSocketConnection(e.WebSocket, e.Endpoint, _jsonSerializer, _logger, _memoryStreamProvider, _textEncoding) - { - OnReceive = ProcessWebSocketMessageReceived, - Url = e.Url, - QueryString = e.QueryString ?? new QueryParamCollection() - }; - - _webSocketConnections.Add(connection); - - if (WebSocketConnected != null) - { - EventHelper.FireEventIfNotNull(WebSocketConnected, this, new GenericEventArgs<IWebSocketConnection> (connection), _logger); - } - } - - /// <summary> - /// Processes the web socket message received. - /// </summary> - /// <param name="result">The result.</param> - private async void ProcessWebSocketMessageReceived(WebSocketMessageInfo result) - { - if (_disposed) - { - return; - } - - //_logger.Debug("Websocket message received: {0}", result.MessageType); - - var tasks = _webSocketListeners.Select(i => Task.Run(async () => - { - try - { - await i.ProcessMessage(result).ConfigureAwait(false); - } - catch (Exception ex) - { - _logger.ErrorException("{0} failed processing WebSocket message {1}", ex, i.GetType().Name, result.MessageType ?? string.Empty); - } - })); - - await Task.WhenAll(tasks).ConfigureAwait(false); - } - - /// <summary> - /// Sends a message to all clients currently connected via a web socket - /// </summary> - /// <typeparam name="T"></typeparam> - /// <param name="messageType">Type of the message.</param> - /// <param name="data">The data.</param> - /// <returns>Task.</returns> - public void SendWebSocketMessage<T>(string messageType, T data) - { - SendWebSocketMessage(messageType, () => data); - } - - /// <summary> - /// Sends a message to all clients currently connected via a web socket - /// </summary> - /// <typeparam name="T"></typeparam> - /// <param name="messageType">Type of the message.</param> - /// <param name="dataFunction">The function that generates the data to send, if there are any connected clients</param> - public void SendWebSocketMessage<T>(string messageType, Func<T> dataFunction) - { - SendWebSocketMessageAsync(messageType, dataFunction, CancellationToken.None); - } - - /// <summary> - /// Sends a message to all clients currently connected via a web socket - /// </summary> - /// <typeparam name="T"></typeparam> - /// <param name="messageType">Type of the message.</param> - /// <param name="dataFunction">The function that generates the data to send, if there are any connected clients</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task.</returns> - /// <exception cref="System.ArgumentNullException">messageType</exception> - public Task SendWebSocketMessageAsync<T>(string messageType, Func<T> dataFunction, CancellationToken cancellationToken) - { - return SendWebSocketMessageAsync(messageType, dataFunction, _webSocketConnections, cancellationToken); - } - - /// <summary> - /// Sends the web socket message async. - /// </summary> - /// <typeparam name="T"></typeparam> - /// <param name="messageType">Type of the message.</param> - /// <param name="dataFunction">The data function.</param> - /// <param name="connections">The connections.</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task.</returns> - /// <exception cref="System.ArgumentNullException">messageType - /// or - /// dataFunction - /// or - /// cancellationToken</exception> - private async Task SendWebSocketMessageAsync<T>(string messageType, Func<T> dataFunction, IEnumerable<IWebSocketConnection> connections, CancellationToken cancellationToken) - { - if (string.IsNullOrEmpty(messageType)) - { - throw new ArgumentNullException("messageType"); - } - - if (dataFunction == null) - { - throw new ArgumentNullException("dataFunction"); - } - - if (_disposed) - { - throw new ObjectDisposedException(GetType().Name); - } - - cancellationToken.ThrowIfCancellationRequested(); - - var connectionsList = connections.Where(s => s.State == WebSocketState.Open).ToList(); - - if (connectionsList.Count > 0) - { - _logger.Info("Sending web socket message {0}", messageType); - - var message = new WebSocketMessage<T> { MessageType = messageType, Data = dataFunction() }; - var json = _jsonSerializer.SerializeToString(message); - - var tasks = connectionsList.Select(s => Task.Run(() => - { - try - { - s.SendAsync(json, cancellationToken); - } - catch (OperationCanceledException) - { - throw; - } - catch (Exception ex) - { - _logger.ErrorException("Error sending web socket message {0} to {1}", ex, messageType, s.RemoteEndPoint); - } - - }, cancellationToken)); - - await Task.WhenAll(tasks).ConfigureAwait(false); - } - } - - /// <summary> - /// Disposes the current HttpServer - /// </summary> - private void DisposeHttpServer() - { - _logger.Info("Disposing web socket connections"); - foreach (var socket in _webSocketConnections) - { - // Dispose the connection - socket.Dispose(); - } - - _webSocketConnections.Clear(); - - if (HttpServer != null) - { - HttpServer.WebSocketConnected -= HttpServer_WebSocketConnected; - - _logger.Info("Disposing http server"); - - HttpServer.Dispose(); - } - } - - /// <summary> - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. - /// </summary> - public void Dispose() - { - _disposed = true; - - Dispose(true); - GC.SuppressFinalize(this); - } - - /// <summary> - /// Releases unmanaged and - optionally - managed resources. - /// </summary> - /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param> - protected virtual void Dispose(bool dispose) - { - if (dispose) - { - DisposeHttpServer(); - } - } - - /// <summary> - /// Adds the web socket listeners. - /// </summary> - /// <param name="listeners">The listeners.</param> - public void AddWebSocketListeners(IEnumerable<IWebSocketListener> listeners) - { - _webSocketListeners.AddRange(listeners); - } - } -} diff --git a/Emby.Server.Implementations/Services/RequestHelper.cs b/Emby.Server.Implementations/Services/RequestHelper.cs index 7538d3102..711ba8bbc 100644 --- a/Emby.Server.Implementations/Services/RequestHelper.cs +++ b/Emby.Server.Implementations/Services/RequestHelper.cs @@ -1,12 +1,13 @@ using System; using System.IO; using Emby.Server.Implementations.HttpServer; +using System.Threading.Tasks; namespace Emby.Server.Implementations.Services { public class RequestHelper { - public static Func<Type, Stream, object> GetRequestReader(HttpListenerHost host, string contentType) + public static Func<Type, Stream, Task<object>> GetRequestReader(HttpListenerHost host, string contentType) { switch (GetContentTypeWithoutEncoding(contentType)) { diff --git a/Emby.Server.Implementations/Services/ResponseHelper.cs b/Emby.Server.Implementations/Services/ResponseHelper.cs index 22e1bc4aa..16de1a083 100644 --- a/Emby.Server.Implementations/Services/ResponseHelper.cs +++ b/Emby.Server.Implementations/Services/ResponseHelper.cs @@ -12,7 +12,7 @@ namespace Emby.Server.Implementations.Services { public static class ResponseHelper { - public static async Task WriteToResponse(IResponse response, IRequest request, object result, CancellationToken cancellationToken) + public static Task WriteToResponse(IResponse response, IRequest request, object result, CancellationToken cancellationToken) { if (result == null) { @@ -22,7 +22,7 @@ namespace Emby.Server.Implementations.Services } response.SetContentLength(0); - return; + return Task.CompletedTask; } var httpResult = result as IHttpResult; @@ -46,18 +46,6 @@ namespace Emby.Server.Implementations.Services // httpResult.ContentType = defaultContentType; //} //response.ContentType = httpResult.ContentType; - - if (httpResult.Cookies != null) - { - var httpRes = response as IHttpResponse; - if (httpRes != null) - { - foreach (var cookie in httpResult.Cookies) - { - httpRes.SetCookie(cookie); - } - } - } } var responseOptions = result as IHasHeaders; @@ -90,32 +78,26 @@ namespace Emby.Server.Implementations.Services var asyncStreamWriter = result as IAsyncStreamWriter; if (asyncStreamWriter != null) { - await asyncStreamWriter.WriteToAsync(response.OutputStream, cancellationToken).ConfigureAwait(false); - return; + return asyncStreamWriter.WriteToAsync(response.OutputStream, cancellationToken); } var streamWriter = result as IStreamWriter; if (streamWriter != null) { streamWriter.WriteTo(response.OutputStream); - return; + return Task.CompletedTask; } var fileWriter = result as FileWriter; if (fileWriter != null) { - await fileWriter.WriteToAsync(response, cancellationToken).ConfigureAwait(false); - return; + return fileWriter.WriteToAsync(response, cancellationToken); } var stream = result as Stream; if (stream != null) { - using (stream) - { - await stream.CopyToAsync(response.OutputStream).ConfigureAwait(false); - return; - } + return CopyStream(stream, response.OutputStream); } var bytes = result as byte[]; @@ -126,9 +108,9 @@ namespace Emby.Server.Implementations.Services if (bytes.Length > 0) { - await response.OutputStream.WriteAsync(bytes, 0, bytes.Length, cancellationToken).ConfigureAwait(false); + return response.OutputStream.WriteAsync(bytes, 0, bytes.Length, cancellationToken); } - return; + return Task.CompletedTask; } var responseText = result as string; @@ -138,12 +120,20 @@ namespace Emby.Server.Implementations.Services response.SetContentLength(bytes.Length); if (bytes.Length > 0) { - await response.OutputStream.WriteAsync(bytes, 0, bytes.Length, cancellationToken).ConfigureAwait(false); + return response.OutputStream.WriteAsync(bytes, 0, bytes.Length, cancellationToken); } - return; + return Task.CompletedTask; } - await WriteObject(request, result, response).ConfigureAwait(false); + return WriteObject(request, result, response); + } + + private static async Task CopyStream(Stream src, Stream dest) + { + using (src) + { + await src.CopyToAsync(dest).ConfigureAwait(false); + } } public static async Task WriteObject(IRequest request, object result, IResponse response) diff --git a/Emby.Server.Implementations/Services/ServiceController.cs b/Emby.Server.Implementations/Services/ServiceController.cs index 3fd6d88f8..3726c9f6b 100644 --- a/Emby.Server.Implementations/Services/ServiceController.cs +++ b/Emby.Server.Implementations/Services/ServiceController.cs @@ -50,7 +50,7 @@ namespace Emby.Server.Implementations.Services // mi.ReturnType // : Type.GetType(requestType.FullName + "Response"); - RegisterRestPaths(appHost, requestType); + RegisterRestPaths(appHost, requestType, serviceType); appHost.AddServiceInfo(serviceType, requestType); } @@ -68,14 +68,14 @@ namespace Emby.Server.Implementations.Services return null; } - public readonly Dictionary<string, List<RestPath>> RestPathMap = new Dictionary<string, List<RestPath>>(StringComparer.OrdinalIgnoreCase); + public readonly RestPath.RestPathMap RestPathMap = new RestPath.RestPathMap(); - public void RegisterRestPaths(HttpListenerHost appHost, Type requestType) + public void RegisterRestPaths(HttpListenerHost appHost, Type requestType, Type serviceType) { var attrs = appHost.GetRouteAttributes(requestType); foreach (RouteAttribute attr in attrs) { - var restPath = new RestPath(appHost.CreateInstance, appHost.GetParseFn, requestType, attr.Path, attr.Verbs, attr.IsHidden, attr.Summary, attr.Description); + var restPath = new RestPath(appHost.CreateInstance, appHost.GetParseFn, requestType, serviceType, attr.Path, attr.Verbs, attr.IsHidden, attr.Summary, attr.Description); RegisterRestPath(restPath); } @@ -114,19 +114,20 @@ namespace Emby.Server.Implementations.Services } var bestScore = -1; + RestPath bestMatch = null; foreach (var restPath in firstMatches) { var score = restPath.MatchScore(httpMethod, matchUsingPathParts, logger); - if (score > bestScore) bestScore = score; + if (score > bestScore) + { + bestScore = score; + bestMatch = restPath; + } } - if (bestScore > 0) + if (bestScore > 0 && bestMatch != null) { - foreach (var restPath in firstMatches) - { - if (bestScore == restPath.MatchScore(httpMethod, matchUsingPathParts, logger)) - return restPath; - } + return bestMatch; } } @@ -136,19 +137,21 @@ namespace Emby.Server.Implementations.Services if (!this.RestPathMap.TryGetValue(potentialHashMatch, out firstMatches)) continue; var bestScore = -1; + RestPath bestMatch = null; foreach (var restPath in firstMatches) { var score = restPath.MatchScore(httpMethod, matchUsingPathParts, logger); - if (score > bestScore) bestScore = score; - } - if (bestScore > 0) - { - foreach (var restPath in firstMatches) + if (score > bestScore) { - if (bestScore == restPath.MatchScore(httpMethod, matchUsingPathParts, logger)) - return restPath; + bestScore = score; + bestMatch = restPath; } } + + if (bestScore > 0 && bestMatch != null) + { + return bestMatch; + } } return null; diff --git a/Emby.Server.Implementations/Services/ServiceExec.cs b/Emby.Server.Implementations/Services/ServiceExec.cs index 5709d3e0a..79b57438c 100644 --- a/Emby.Server.Implementations/Services/ServiceExec.cs +++ b/Emby.Server.Implementations/Services/ServiceExec.cs @@ -70,7 +70,7 @@ namespace Emby.Server.Implementations.Services } } - public static async Task<object> Execute(Type serviceType, IRequest request, object instance, object requestDto, string requestName) + public static Task<object> Execute(Type serviceType, IRequest request, object instance, object requestDto, string requestName) { var actionName = request.Verb ?? "POST"; @@ -82,7 +82,10 @@ namespace Emby.Server.Implementations.Services foreach (var requestFilter in actionContext.RequestFilters) { requestFilter.RequestFilter(request, request.Response, requestDto); - if (request.Response.IsClosed) return null; + if (request.Response.IsClosed) + { + Task.FromResult<object>(null); + } } } @@ -91,17 +94,56 @@ namespace Emby.Server.Implementations.Services var taskResponse = response as Task; if (taskResponse != null) { - await taskResponse.ConfigureAwait(false); - response = ServiceHandler.GetTaskResult(taskResponse); + return GetTaskResult(taskResponse); } - return response; + return Task.FromResult(response); } var expectedMethodName = actionName.Substring(0, 1) + actionName.Substring(1).ToLower(); throw new NotImplementedException(string.Format("Could not find method named {1}({0}) or Any({0}) on Service {2}", requestDto.GetType().GetMethodName(), expectedMethodName, serviceType.GetMethodName())); } + private static async Task<object> GetTaskResult(Task task) + { + try + { + var taskObject = task as Task<object>; + if (taskObject != null) + { + return await taskObject.ConfigureAwait(false); + } + + await task.ConfigureAwait(false); + + var type = task.GetType().GetTypeInfo(); + if (!type.IsGenericType) + { + return null; + } + + var resultProperty = type.GetDeclaredProperty("Result"); + if (resultProperty == null) + { + return null; + } + + var result = resultProperty.GetValue(task); + + // hack alert + if (result.GetType().Name.IndexOf("voidtaskresult", StringComparison.OrdinalIgnoreCase) != -1) + { + return null; + } + + return result; + } + catch (TypeAccessException) + { + return null; //return null for void Task's + } + } + public static List<ServiceMethod> Reset(Type serviceType) { var actions = new List<ServiceMethod>(); diff --git a/Emby.Server.Implementations/Services/ServiceHandler.cs b/Emby.Server.Implementations/Services/ServiceHandler.cs index d500595ce..e76857a8d 100644 --- a/Emby.Server.Implementations/Services/ServiceHandler.cs +++ b/Emby.Server.Implementations/Services/ServiceHandler.cs @@ -11,55 +11,7 @@ namespace Emby.Server.Implementations.Services { public class ServiceHandler { - public async Task<object> HandleResponseAsync(object response) - { - var taskResponse = response as Task; - - if (taskResponse == null) - { - return response; - } - - await taskResponse.ConfigureAwait(false); - - var taskResult = GetTaskResult(taskResponse); - - var subTask = taskResult as Task; - if (subTask != null) - { - taskResult = GetTaskResult(subTask); - } - - return taskResult; - } - - internal static object GetTaskResult(Task task) - { - try - { - var taskObject = task as Task<object>; - if (taskObject != null) - { - return taskObject.Result; - } - - task.Wait(); - - var type = task.GetType().GetTypeInfo(); - if (!type.IsGenericType) - { - return null; - } - - return type.GetDeclaredProperty("Result").GetValue(task); - } - catch (TypeAccessException) - { - return null; //return null for void Task's - } - } - - protected static object CreateContentTypeRequest(HttpListenerHost host, IRequest httpReq, Type requestType, string contentType) + protected static Task<object> CreateContentTypeRequest(HttpListenerHost host, IRequest httpReq, Type requestType, string contentType) { if (!string.IsNullOrEmpty(contentType) && httpReq.ContentLength > 0) { @@ -69,7 +21,7 @@ namespace Emby.Server.Implementations.Services return deserializer(requestType, httpReq.InputStream); } } - return host.CreateInstance(requestType); + return Task.FromResult(host.CreateInstance(requestType)); } public static RestPath FindMatchingRestPath(string httpMethod, string pathInfo, ILogger logger, out string contentType) @@ -137,14 +89,11 @@ namespace Emby.Server.Implementations.Services if (ResponseContentType != null) httpReq.ResponseContentType = ResponseContentType; - var request = httpReq.Dto = CreateRequest(appHost, httpReq, restPath, logger); + var request = httpReq.Dto = await CreateRequest(appHost, httpReq, restPath, logger).ConfigureAwait(false); appHost.ApplyRequestFilters(httpReq, httpRes, request); - var rawResponse = await appHost.ServiceController.Execute(appHost, request, httpReq).ConfigureAwait(false); - - //var response = await HandleResponseAsync(rawResponse).ConfigureAwait(false); - var response = rawResponse; + var response = await appHost.ServiceController.Execute(appHost, request, httpReq).ConfigureAwait(false); // Apply response filters foreach (var responseFilter in appHost.ResponseFilters) @@ -155,38 +104,37 @@ namespace Emby.Server.Implementations.Services await ResponseHelper.WriteToResponse(httpRes, httpReq, response, cancellationToken).ConfigureAwait(false); } - public static object CreateRequest(HttpListenerHost host, IRequest httpReq, RestPath restPath, ILogger logger) + public static async Task<object> CreateRequest(HttpListenerHost host, IRequest httpReq, RestPath restPath, ILogger logger) { var requestType = restPath.RequestType; if (RequireqRequestStream(requestType)) { // Used by IRequiresRequestStream - var request = ServiceHandler.CreateRequest(httpReq, restPath, GetRequestParams(httpReq), host.CreateInstance(requestType)); + var requestParams = await GetRequestParams(httpReq).ConfigureAwait(false); + var request = ServiceHandler.CreateRequest(httpReq, restPath, requestParams, host.CreateInstance(requestType)); var rawReq = (IRequiresRequestStream)request; rawReq.RequestStream = httpReq.InputStream; return rawReq; } + else + { + var requestParams = await GetFlattenedRequestParams(httpReq).ConfigureAwait(false); - var requestParams = GetFlattenedRequestParams(httpReq); - return CreateRequest(host, httpReq, restPath, requestParams); + var requestDto = await CreateContentTypeRequest(host, httpReq, restPath.RequestType, httpReq.ContentType).ConfigureAwait(false); + + return CreateRequest(httpReq, restPath, requestParams, requestDto); + } } - private static bool RequireqRequestStream(Type requestType) + public static bool RequireqRequestStream(Type requestType) { var requiresRequestStreamTypeInfo = typeof(IRequiresRequestStream).GetTypeInfo(); return requiresRequestStreamTypeInfo.IsAssignableFrom(requestType.GetTypeInfo()); } - public static object CreateRequest(HttpListenerHost host, IRequest httpReq, RestPath restPath, Dictionary<string, string> requestParams) - { - var requestDto = CreateContentTypeRequest(host, httpReq, restPath.RequestType, httpReq.ContentType); - - return CreateRequest(httpReq, restPath, requestParams, requestDto); - } - public static object CreateRequest(IRequest httpReq, RestPath restPath, Dictionary<string, string> requestParams, object requestDto) { string contentType; @@ -200,7 +148,7 @@ namespace Emby.Server.Implementations.Services /// <summary> /// Duplicate Params are given a unique key by appending a #1 suffix /// </summary> - private static Dictionary<string, string> GetRequestParams(IRequest request) + private static async Task<Dictionary<string, string>> GetRequestParams(IRequest request) { var map = new Dictionary<string, string>(); @@ -224,7 +172,7 @@ namespace Emby.Server.Implementations.Services if ((IsMethod(request.Verb, "POST") || IsMethod(request.Verb, "PUT"))) { - var formData = request.FormData; + var formData = await request.GetFormData().ConfigureAwait(false); if (formData != null) { foreach (var name in formData.Keys) @@ -258,7 +206,7 @@ namespace Emby.Server.Implementations.Services /// <summary> /// Duplicate params have their values joined together in a comma-delimited string /// </summary> - private static Dictionary<string, string> GetFlattenedRequestParams(IRequest request) + private static async Task<Dictionary<string, string>> GetFlattenedRequestParams(IRequest request) { var map = new Dictionary<string, string>(); @@ -270,7 +218,7 @@ namespace Emby.Server.Implementations.Services if ((IsMethod(request.Verb, "POST") || IsMethod(request.Verb, "PUT"))) { - var formData = request.FormData; + var formData = await request.GetFormData().ConfigureAwait(false); if (formData != null) { foreach (var name in formData.Keys) diff --git a/Emby.Server.Implementations/Services/ServicePath.cs b/Emby.Server.Implementations/Services/ServicePath.cs index 0ca36df19..282269e7b 100644 --- a/Emby.Server.Implementations/Services/ServicePath.cs +++ b/Emby.Server.Implementations/Services/ServicePath.cs @@ -48,6 +48,8 @@ namespace Emby.Server.Implementations.Services public Type RequestType { get; private set; } + public Type ServiceType { get; private set; } + public string Path { get { return this.restPath; } } public string Summary { get; private set; } @@ -56,6 +58,11 @@ namespace Emby.Server.Implementations.Services public int Priority { get; set; } //passed back to RouteAttribute + public IEnumerable<string> PathVariables + { + get { return this.variablesNames.Where(e => !string.IsNullOrWhiteSpace(e)); } + } + public static string[] GetPathPartsForMatching(string pathInfo) { return pathInfo.ToLower().Split(new[] { PathSeperatorChar }, StringSplitOptions.RemoveEmptyEntries); @@ -93,9 +100,10 @@ namespace Emby.Server.Implementations.Services return list; } - public RestPath(Func<Type, object> createInstanceFn, Func<Type, Func<string, object>> getParseFn, Type requestType, string path, string verbs, bool isHidden = false, string summary = null, string description = null) + public RestPath(Func<Type, object> createInstanceFn, Func<Type, Func<string, object>> getParseFn, Type requestType, Type serviceType, string path, string verbs, bool isHidden = false, string summary = null, string description = null) { this.RequestType = requestType; + this.ServiceType = serviceType; this.Summary = summary; this.IsHidden = isHidden; this.Description = description; @@ -558,5 +566,12 @@ namespace Emby.Server.Implementations.Services return this.typeDeserializer.PopulateFromMap(fromInstance, requestKeyValuesMap); } + + public class RestPathMap : SortedDictionary<string, List<RestPath>> + { + public RestPathMap() : base(StringComparer.OrdinalIgnoreCase) + { + } + } } }
\ No newline at end of file diff --git a/Emby.Server.Implementations/Services/SwaggerService.cs b/Emby.Server.Implementations/Services/SwaggerService.cs deleted file mode 100644 index fc2bdbd55..000000000 --- a/Emby.Server.Implementations/Services/SwaggerService.cs +++ /dev/null @@ -1,260 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using MediaBrowser.Model.Services; - -namespace Emby.Server.Implementations.Services -{ - [Route("/swagger", "GET", Summary = "Gets the swagger specifications")] - [Route("/swagger.json", "GET", Summary = "Gets the swagger specifications")] - public class GetSwaggerSpec : IReturn<SwaggerSpec> - { - } - - public class SwaggerSpec - { - public string swagger { get; set; } - public string[] schemes { get; set; } - public SwaggerInfo info { get; set; } - public string host { get; set; } - public string basePath { get; set; } - public SwaggerTag[] tags { get; set; } - public IDictionary<string, Dictionary<string, SwaggerMethod>> paths { get; set; } - public Dictionary<string, SwaggerDefinition> definitions { get; set; } - public SwaggerComponents components { get; set; } - } - - public class SwaggerComponents - { - public Dictionary<string, SwaggerSecurityScheme> securitySchemes { get; set; } - } - - public class SwaggerSecurityScheme - { - public string name { get; set; } - public string type { get; set; } - public string @in { get; set; } - } - - public class SwaggerInfo - { - public string description { get; set; } - public string version { get; set; } - public string title { get; set; } - public string termsOfService { get; set; } - - public SwaggerConcactInfo contact { get; set; } - } - - public class SwaggerConcactInfo - { - public string email { get; set; } - public string name { get; set; } - public string url { get; set; } - } - - public class SwaggerTag - { - public string description { get; set; } - public string name { get; set; } - } - - public class SwaggerMethod - { - public string summary { get; set; } - public string description { get; set; } - public string[] tags { get; set; } - public string operationId { get; set; } - public string[] consumes { get; set; } - public string[] produces { get; set; } - public SwaggerParam[] parameters { get; set; } - public Dictionary<string, SwaggerResponse> responses { get; set; } - public Dictionary<string, string[]>[] security { get; set; } - } - - public class SwaggerParam - { - public string @in { get; set; } - public string name { get; set; } - public string description { get; set; } - public bool required { get; set; } - public string type { get; set; } - public string collectionFormat { get; set; } - } - - public class SwaggerResponse - { - public string description { get; set; } - - // ex. "$ref":"#/definitions/Pet" - public Dictionary<string, string> schema { get; set; } - } - - public class SwaggerDefinition - { - public string type { get; set; } - public Dictionary<string, SwaggerProperty> properties { get; set; } - } - - public class SwaggerProperty - { - public string type { get; set; } - public string format { get; set; } - public string description { get; set; } - public string[] @enum { get; set; } - public string @default { get; set; } - } - - public class SwaggerService : IService, IRequiresRequest - { - private SwaggerSpec _spec; - - public IRequest Request { get; set; } - - public object Get(GetSwaggerSpec request) - { - return _spec ?? (_spec = GetSpec()); - } - - private SwaggerSpec GetSpec() - { - string host = null; - Uri uri; - if (Uri.TryCreate(Request.RawUrl, UriKind.Absolute, out uri)) - { - host = uri.Host; - } - - var securitySchemes = new Dictionary<string, SwaggerSecurityScheme>(); - - securitySchemes["api_key"] = new SwaggerSecurityScheme - { - name = "api_key", - type = "apiKey", - @in = "query" - }; - - var spec = new SwaggerSpec - { - schemes = new[] { "http" }, - tags = GetTags(), - swagger = "2.0", - info = new SwaggerInfo - { - title = "Emby Server API", - version = "1.0.0", - description = "Explore the Emby Server API", - contact = new SwaggerConcactInfo - { - name = "Emby Developer Community", - url = "https://emby.media/community/index.php?/forum/47-developer-api" - }, - termsOfService = "https://emby.media/terms" - }, - paths = GetPaths(), - definitions = GetDefinitions(), - basePath = "/emby", - host = host, - - components = new SwaggerComponents - { - securitySchemes = securitySchemes - } - }; - - return spec; - } - - - private SwaggerTag[] GetTags() - { - return new SwaggerTag[] { }; - } - - private Dictionary<string, SwaggerDefinition> GetDefinitions() - { - return new Dictionary<string, SwaggerDefinition>(); - } - - private IDictionary<string, Dictionary<string, SwaggerMethod>> GetPaths() - { - var paths = new SortedDictionary<string, Dictionary<string, SwaggerMethod>>(); - - var all = ServiceController.Instance.RestPathMap.OrderBy(i => i.Key, StringComparer.OrdinalIgnoreCase).ToList(); - - foreach (var current in all) - { - foreach (var info in current.Value) - { - if (info.IsHidden) - { - continue; - } - - if (info.Path.StartsWith("/mediabrowser", StringComparison.OrdinalIgnoreCase)) - { - continue; - } - if (info.Path.StartsWith("/emby", StringComparison.OrdinalIgnoreCase)) - { - continue; - } - - paths[info.Path] = GetPathInfo(info); - } - } - - return paths; - } - - private Dictionary<string, SwaggerMethod> GetPathInfo(RestPath info) - { - var result = new Dictionary<string, SwaggerMethod>(); - - foreach (var verb in info.Verbs) - { - var responses = new Dictionary<string, SwaggerResponse> - { - }; - - responses["200"] = new SwaggerResponse - { - description = "OK" - }; - - var security = new List<Dictionary<string, string[]>>(); - - var apiKeySecurity = new Dictionary<string, string[]>(); - apiKeySecurity["api_key"] = new string[] { }; - - security.Add(apiKeySecurity); - - result[verb.ToLower()] = new SwaggerMethod - { - summary = info.Summary, - description = info.Description, - produces = new[] - { - "application/json" - }, - consumes = new[] - { - "application/json" - }, - operationId = info.RequestType.Name, - tags = new string[] { }, - - parameters = new SwaggerParam[] { }, - - responses = responses, - - security = security.ToArray() - }; - } - - return result; - } - } -} diff --git a/Emby.Server.Implementations/Services/UrlExtensions.cs b/Emby.Server.Implementations/Services/UrlExtensions.cs index c7346789a..ba9889c41 100644 --- a/Emby.Server.Implementations/Services/UrlExtensions.cs +++ b/Emby.Server.Implementations/Services/UrlExtensions.cs @@ -21,7 +21,7 @@ namespace Emby.Server.Implementations.Services return type.IsGenericParameter ? "'" + typeName : typeName; } - public static string LeftPart(string strVal, string needle) + private static string LeftPart(string strVal, string needle) { if (strVal == null) return null; var pos = strVal.IndexOf(needle, StringComparison.OrdinalIgnoreCase); diff --git a/Emby.Server.Implementations/Session/FirebaseSessionController.cs b/Emby.Server.Implementations/Session/FirebaseSessionController.cs new file mode 100644 index 000000000..cfe513305 --- /dev/null +++ b/Emby.Server.Implementations/Session/FirebaseSessionController.cs @@ -0,0 +1,131 @@ +using MediaBrowser.Controller.Session; +using MediaBrowser.Model.Net; +using MediaBrowser.Common.Net; +using MediaBrowser.Model.Serialization; +using System; +using System.Threading; +using System.Threading.Tasks; +using System.Text; +using MediaBrowser.Common; + +namespace Emby.Server.Implementations.Session +{ + public class FirebaseSessionController : ISessionController + { + private readonly IHttpClient _httpClient; + private readonly IJsonSerializer _json; + private readonly ISessionManager _sessionManager; + + public SessionInfo Session { get; private set; } + + private readonly string _token; + + private IApplicationHost _appHost; + private string _senderId; + private string _applicationId; + + public FirebaseSessionController(IHttpClient httpClient, + IApplicationHost appHost, + IJsonSerializer json, + SessionInfo session, + string token, ISessionManager sessionManager) + { + _httpClient = httpClient; + _json = json; + _appHost = appHost; + Session = session; + _token = token; + _sessionManager = sessionManager; + + _applicationId = _appHost.GetValue("firebase_applicationid"); + _senderId = _appHost.GetValue("firebase_senderid"); + } + + public static bool IsSupported(IApplicationHost appHost) + { + return !string.IsNullOrEmpty(appHost.GetValue("firebase_applicationid")) && !string.IsNullOrEmpty(appHost.GetValue("firebase_senderid")); + } + + public bool IsSessionActive + { + get + { + return (DateTime.UtcNow - Session.LastActivityDate).TotalDays <= 3; + } + } + + public bool SupportsMediaControl + { + get { return true; } + } + + public async Task SendMessage<T>(string name, string messageId, T data, ISessionController[] allControllers, CancellationToken cancellationToken) + { + if (!IsSessionActive) + { + return; + } + + if (string.IsNullOrEmpty(_senderId) || string.IsNullOrEmpty(_applicationId)) + { + return; + } + + foreach (var controller in allControllers) + { + // Don't send if there's an active web socket connection + if ((controller is WebSocketController) && controller.IsSessionActive) + { + return; + } + } + + var msg = new WebSocketMessage<T> + { + Data = data, + MessageType = name, + MessageId = messageId, + ServerId = _appHost.SystemId + }; + + var req = new FirebaseBody<T> + { + to = _token, + data = msg + }; + + var byteArray = Encoding.UTF8.GetBytes(_json.SerializeToString(req)); + + var enableLogging = false; + +#if DEBUG + enableLogging = true; +#endif + + var options = new HttpRequestOptions + { + Url = "https://fcm.googleapis.com/fcm/send", + RequestContentType = "application/json", + RequestContentBytes = byteArray, + CancellationToken = cancellationToken, + LogRequest = enableLogging, + LogResponse = enableLogging, + LogErrors = enableLogging + }; + + options.RequestHeaders["Authorization"] = string.Format("key={0}", _applicationId); + options.RequestHeaders["Sender"] = string.Format("id={0}", _senderId); + + using (var response = await _httpClient.Post(options).ConfigureAwait(false)) + { + + } + } + } + + internal class FirebaseBody<T> + { + public string to { get; set; } + public WebSocketMessage<T> data { get; set; } + } +} diff --git a/Emby.Server.Implementations/Session/HttpSessionController.cs b/Emby.Server.Implementations/Session/HttpSessionController.cs index 6725cd7af..ff9b3fefc 100644 --- a/Emby.Server.Implementations/Session/HttpSessionController.cs +++ b/Emby.Server.Implementations/Session/HttpSessionController.cs @@ -36,10 +36,6 @@ namespace Emby.Server.Implementations.Session _sessionManager = sessionManager; } - public void OnActivity() - { - } - private string PostUrl { get @@ -52,7 +48,7 @@ namespace Emby.Server.Implementations.Session { get { - return (DateTime.UtcNow - Session.LastActivityDate).TotalMinutes <= 10; + return (DateTime.UtcNow - Session.LastActivityDate).TotalMinutes <= 5; } } @@ -61,49 +57,29 @@ namespace Emby.Server.Implementations.Session get { return true; } } - private Task SendMessage(string name, CancellationToken cancellationToken) + private Task SendMessage(string name, string messageId, CancellationToken cancellationToken) { - return SendMessage(name, new Dictionary<string, string>(), cancellationToken); + return SendMessage(name, messageId, new Dictionary<string, string>(), cancellationToken); } - private async Task SendMessage(string name, - Dictionary<string, string> args, - CancellationToken cancellationToken) + private Task SendMessage(string name, string messageId, Dictionary<string, string> args, CancellationToken cancellationToken) { + args["messageId"] = messageId; var url = PostUrl + "/" + name + ToQueryString(args); - using ((await _httpClient.Post(new HttpRequestOptions + return SendRequest(new HttpRequestOptions { Url = url, CancellationToken = cancellationToken, BufferContent = false - - }).ConfigureAwait(false))) - { - - } - } - - public Task SendSessionEndedNotification(SessionInfoDto sessionInfo, CancellationToken cancellationToken) - { - return Task.FromResult(true); - } - - public Task SendPlaybackStartNotification(SessionInfoDto sessionInfo, CancellationToken cancellationToken) - { - return Task.FromResult(true); + }); } - public Task SendPlaybackStoppedNotification(SessionInfoDto sessionInfo, CancellationToken cancellationToken) - { - return Task.FromResult(true); - } - - public Task SendPlayCommand(PlayRequest command, CancellationToken cancellationToken) + private Task SendPlayCommand(PlayRequest command, string messageId, CancellationToken cancellationToken) { var dict = new Dictionary<string, string>(); - dict["ItemIds"] = string.Join(",", command.ItemIds); + dict["ItemIds"] = string.Join(",", command.ItemIds.Select(i => i.ToString("N")).ToArray()); if (command.StartPositionTicks.HasValue) { @@ -121,15 +97,15 @@ namespace Emby.Server.Implementations.Session { dict["StartIndex"] = command.StartIndex.Value.ToString(CultureInfo.InvariantCulture); } - if (!string.IsNullOrWhiteSpace(command.MediaSourceId)) + if (!string.IsNullOrEmpty(command.MediaSourceId)) { dict["MediaSourceId"] = command.MediaSourceId; } - return SendMessage(command.PlayCommand.ToString(), dict, cancellationToken); + return SendMessage(command.PlayCommand.ToString(), messageId, dict, cancellationToken); } - public Task SendPlaystateCommand(PlaystateRequest command, CancellationToken cancellationToken) + private Task SendPlaystateCommand(PlaystateRequest command, string messageId, CancellationToken cancellationToken) { var args = new Dictionary<string, string>(); @@ -143,60 +119,74 @@ namespace Emby.Server.Implementations.Session args["SeekPositionTicks"] = command.SeekPositionTicks.Value.ToString(CultureInfo.InvariantCulture); } - return SendMessage(command.Command.ToString(), args, cancellationToken); + return SendMessage(command.Command.ToString(), messageId, args, cancellationToken); } - public Task SendLibraryUpdateInfo(LibraryUpdateInfo info, CancellationToken cancellationToken) + private string[] _supportedMessages = new string[] { }; + public Task SendMessage<T>(string name, string messageId, T data, ISessionController[] allControllers, CancellationToken cancellationToken) { - return SendMessage("LibraryChanged", info, cancellationToken); - } + if (!IsSessionActive) + { + return Task.CompletedTask; + } - public Task SendRestartRequiredNotification(CancellationToken cancellationToken) - { - return SendMessage("RestartRequired", cancellationToken); - } + if (string.Equals(name, "Play", StringComparison.OrdinalIgnoreCase)) + { + return SendPlayCommand(data as PlayRequest, messageId, cancellationToken); + } + if (string.Equals(name, "PlayState", StringComparison.OrdinalIgnoreCase)) + { + return SendPlaystateCommand(data as PlaystateRequest, messageId, cancellationToken); + } + if (string.Equals(name, "GeneralCommand", StringComparison.OrdinalIgnoreCase)) + { + var command = data as GeneralCommand; + return SendMessage(command.Name, messageId, command.Arguments, cancellationToken); + } - public Task SendUserDataChangeInfo(UserDataChangeInfo info, CancellationToken cancellationToken) - { - return Task.FromResult(true); - } + if (!_supportedMessages.Contains(name, StringComparer.OrdinalIgnoreCase)) + { + return Task.CompletedTask; + } - public Task SendServerShutdownNotification(CancellationToken cancellationToken) - { - return SendMessage("ServerShuttingDown", cancellationToken); - } + var url = PostUrl + "/" + name; - public Task SendServerRestartNotification(CancellationToken cancellationToken) - { - return SendMessage("ServerRestarting", cancellationToken); - } + url += "?messageId=" + messageId; - public Task SendGeneralCommand(GeneralCommand command, CancellationToken cancellationToken) - { - return SendMessage(command.Name, command.Arguments, cancellationToken); + var options = new HttpRequestOptions + { + Url = url, + CancellationToken = cancellationToken, + BufferContent = false + }; + + if (data != null) + { + if (typeof(T) == typeof(string)) + { + var str = data as String; + if (!string.IsNullOrEmpty(str)) + { + options.RequestContent = str; + options.RequestContentType = "application/json"; + } + } + else + { + options.RequestContent = _json.SerializeToString(data); + options.RequestContentType = "application/json"; + } + } + + return SendRequest(options); } - public Task SendMessage<T>(string name, T data, CancellationToken cancellationToken) + private async Task SendRequest(HttpRequestOptions options) { - return Task.FromResult(true); - //var url = PostUrl + "/" + name; - - //var options = new HttpRequestOptions - //{ - // Url = url, - // CancellationToken = cancellationToken, - // BufferContent = false - //}; - - //options.RequestContent = _json.SerializeToString(data); - //options.RequestContentType = "application/json"; - - //return _httpClient.Post(new HttpRequestOptions - //{ - // Url = url, - // CancellationToken = cancellationToken, - // BufferContent = false - //}); + using (var response = await _httpClient.Post(options).ConfigureAwait(false)) + { + + } } private string ToQueryString(Dictionary<string, string> nvc) diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index 6b70f2cda..9db4f4423 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -30,13 +30,14 @@ using MediaBrowser.Controller.Net; using MediaBrowser.Model.Querying; using MediaBrowser.Model.Threading; using MediaBrowser.Model.Extensions; +using MediaBrowser.Controller.Authentication; namespace Emby.Server.Implementations.Session { /// <summary> /// Class SessionManager /// </summary> - public class SessionManager : ISessionManager + public class SessionManager : ISessionManager, IDisposable { /// <summary> /// The _user data repository @@ -71,7 +72,7 @@ namespace Emby.Server.Implementations.Session public event EventHandler<GenericEventArgs<AuthenticationRequest>> AuthenticationFailed; - public event EventHandler<GenericEventArgs<AuthenticationRequest>> AuthenticationSucceeded; + public event EventHandler<GenericEventArgs<AuthenticationResult>> AuthenticationSucceeded; /// <summary> /// Occurs when [playback start]. @@ -91,10 +92,6 @@ namespace Emby.Server.Implementations.Session public event EventHandler<SessionEventArgs> SessionEnded; public event EventHandler<SessionEventArgs> SessionActivity; - private IEnumerable<ISessionControllerFactory> _sessionFactories = new List<ISessionControllerFactory>(); - - private readonly SemaphoreSlim _sessionLock = new SemaphoreSlim(1, 1); - public SessionManager(IUserDataManager userDataManager, ILogger logger, ILibraryManager libraryManager, IUserManager userManager, IMusicManager musicManager, IDtoService dtoService, IImageProcessor imageProcessor, IJsonSerializer jsonSerializer, IServerApplicationHost appHost, IHttpClient httpClient, IAuthenticationRepository authRepo, IDeviceManager deviceManager, IMediaSourceManager mediaSourceManager, ITimerFactory timerFactory) { _userDataManager = userDataManager; @@ -111,28 +108,41 @@ namespace Emby.Server.Implementations.Session _deviceManager = deviceManager; _mediaSourceManager = mediaSourceManager; _timerFactory = timerFactory; - _deviceManager.DeviceOptionsUpdated += _deviceManager_DeviceOptionsUpdated; } - void _deviceManager_DeviceOptionsUpdated(object sender, GenericEventArgs<DeviceInfo> e) + private void _deviceManager_DeviceOptionsUpdated(object sender, GenericEventArgs<Tuple<string, DeviceOptions>> e) { foreach (var session in Sessions) { - if (string.Equals(session.DeviceId, e.Argument.Id)) + if (string.Equals(session.DeviceId, e.Argument.Item1)) { - session.DeviceName = e.Argument.Name; + if (!string.IsNullOrWhiteSpace(e.Argument.Item2.CustomName)) + { + session.HasCustomDeviceName = true; + session.DeviceName = e.Argument.Item2.CustomName; + } + else + { + session.HasCustomDeviceName = false; + } } } } - /// <summary> - /// Adds the parts. - /// </summary> - /// <param name="sessionFactories">The session factories.</param> - public void AddParts(IEnumerable<ISessionControllerFactory> sessionFactories) + private bool _disposed; + public void Dispose() + { + _disposed = true; + _deviceManager.DeviceOptionsUpdated -= _deviceManager_DeviceOptionsUpdated; + } + + public void CheckDisposed() { - _sessionFactories = sessionFactories.ToList(); + if (_disposed) + { + throw new ObjectDisposedException(GetType().Name); + } } /// <summary> @@ -146,58 +156,44 @@ namespace Emby.Server.Implementations.Session private void OnSessionStarted(SessionInfo info) { - EventHelper.QueueEventIfNotNull(SessionStarted, this, new SessionEventArgs - { - SessionInfo = info - - }, _logger); - - if (!string.IsNullOrWhiteSpace(info.DeviceId)) + if (!string.IsNullOrEmpty(info.DeviceId)) { var capabilities = GetSavedCapabilities(info.DeviceId); if (capabilities != null) { - info.AppIconUrl = capabilities.IconUrl; ReportCapabilities(info, capabilities, false); } } - } - private async void OnSessionEnded(SessionInfo info) - { - try - { - await SendSessionEndedNotification(info, CancellationToken.None).ConfigureAwait(false); - } - catch (Exception ex) + EventHelper.QueueEventIfNotNull(SessionStarted, this, new SessionEventArgs { - _logger.ErrorException("Error in SendSessionEndedNotification", ex); - } + SessionInfo = info + }, _logger); + } + + private void OnSessionEnded(SessionInfo info) + { EventHelper.QueueEventIfNotNull(SessionEnded, this, new SessionEventArgs { SessionInfo = info }, _logger); - var disposable = info.SessionController as IDisposable; + info.Dispose(); + } - if (disposable != null) - { - _logger.Debug("Disposing session controller {0}", disposable.GetType().Name); + public void UpdateDeviceName(string sessionId, string deviceName) + { + var session = GetSession(sessionId); - try - { - disposable.Dispose(); - } - catch (Exception ex) - { - _logger.ErrorException("Error disposing session controller", ex); - } - } + var key = GetSessionKey(session.Client, session.DeviceId); - info.Dispose(); + if (session != null) + { + session.DeviceName = deviceName; + } } /// <summary> @@ -212,13 +208,15 @@ namespace Emby.Server.Implementations.Session /// <returns>Task.</returns> /// <exception cref="System.ArgumentNullException">user</exception> /// <exception cref="System.UnauthorizedAccessException"></exception> - public async Task<SessionInfo> LogSessionActivity(string appName, + public SessionInfo LogSessionActivity(string appName, string appVersion, string deviceId, string deviceName, string remoteEndPoint, User user) { + CheckDisposed(); + if (string.IsNullOrEmpty(appName)) { throw new ArgumentNullException("appName"); @@ -231,13 +229,9 @@ namespace Emby.Server.Implementations.Session { throw new ArgumentNullException("deviceId"); } - if (string.IsNullOrEmpty(deviceName)) - { - throw new ArgumentNullException("deviceName"); - } var activityDate = DateTime.UtcNow; - var session = await GetSessionInfo(appName, appVersion, deviceId, deviceName, remoteEndPoint, user).ConfigureAwait(false); + var session = GetSessionInfo(appName, appVersion, deviceId, deviceName, remoteEndPoint, user); var lastActivityDate = session.LastActivityDate; session.LastActivityDate = activityDate; @@ -268,40 +262,39 @@ namespace Emby.Server.Implementations.Session }, _logger); } - var controller = session.SessionController; - if (controller != null) - { - controller.OnActivity(); - } - return session; } - public async void ReportSessionEnded(string sessionId) + public void CloseIfNeeded(SessionInfo session) { - await _sessionLock.WaitAsync(CancellationToken.None).ConfigureAwait(false); - - try + if (!session.SessionControllers.Any(i => i.IsSessionActive)) { - var session = GetSession(sessionId, false); + var key = GetSessionKey(session.Client, session.DeviceId); - if (session != null) - { - var key = GetSessionKey(session.Client, session.DeviceId); - - SessionInfo removed; - _activeConnections.TryRemove(key, out removed); + SessionInfo removed; + _activeConnections.TryRemove(key, out removed); - OnSessionEnded(session); - } + OnSessionEnded(session); } - finally + } + + public void ReportSessionEnded(string sessionId) + { + CheckDisposed(); + var session = GetSession(sessionId, false); + + if (session != null) { - _sessionLock.Release(); + var key = GetSessionKey(session.Client, session.DeviceId); + + SessionInfo removed; + _activeConnections.TryRemove(key, out removed); + + OnSessionEnded(session); } } - private Task<MediaSourceInfo> GetMediaSource(IHasMediaSources item, string mediaSourceId, string liveStreamId) + private Task<MediaSourceInfo> GetMediaSource(BaseItem item, string mediaSourceId, string liveStreamId) { return _mediaSourceManager.GetMediaSource(item, mediaSourceId, liveStreamId, false, CancellationToken.None); } @@ -311,16 +304,16 @@ namespace Emby.Server.Implementations.Session /// </summary> private async Task UpdateNowPlayingItem(SessionInfo session, PlaybackProgressInfo info, BaseItem libraryItem, bool updateLastCheckInTime) { - if (string.IsNullOrWhiteSpace(info.MediaSourceId)) + if (string.IsNullOrEmpty(info.MediaSourceId)) { - info.MediaSourceId = info.ItemId; + info.MediaSourceId = info.ItemId.ToString("N"); } - if (!string.IsNullOrWhiteSpace(info.ItemId) && info.Item == null && libraryItem != null) + if (!info.ItemId.Equals(Guid.Empty) && info.Item == null && libraryItem != null) { var current = session.NowPlayingItem; - if (current == null || !string.Equals(current.Id, info.ItemId, StringComparison.OrdinalIgnoreCase)) + if (current == null || !info.ItemId.Equals(current.Id)) { var runtimeTicks = libraryItem.RunTimeTicks; @@ -328,7 +321,7 @@ namespace Emby.Server.Implementations.Session var hasMediaSources = libraryItem as IHasMediaSources; if (hasMediaSources != null) { - mediaSource = await GetMediaSource(hasMediaSources, info.MediaSourceId, info.LiveStreamId).ConfigureAwait(false); + mediaSource = await GetMediaSource(libraryItem, info.MediaSourceId, info.LiveStreamId).ConfigureAwait(false); if (mediaSource != null) { @@ -364,6 +357,14 @@ namespace Emby.Server.Implementations.Session session.PlayState.SubtitleStreamIndex = info.SubtitleStreamIndex; session.PlayState.PlayMethod = info.PlayMethod; session.PlayState.RepeatMode = info.RepeatMode; + session.PlaylistItemId = info.PlaylistItemId; + + var nowPlayingQueue = info.NowPlayingQueue; + + if (nowPlayingQueue != null) + { + session.NowPlayingQueue = nowPlayingQueue; + } } /// <summary> @@ -397,99 +398,89 @@ namespace Emby.Server.Implementations.Session /// <param name="remoteEndPoint">The remote end point.</param> /// <param name="user">The user.</param> /// <returns>SessionInfo.</returns> - private async Task<SessionInfo> GetSessionInfo(string appName, string appVersion, string deviceId, string deviceName, string remoteEndPoint, User user) + private SessionInfo GetSessionInfo(string appName, string appVersion, string deviceId, string deviceName, string remoteEndPoint, User user) { - if (string.IsNullOrWhiteSpace(deviceId)) + CheckDisposed(); + + if (string.IsNullOrEmpty(deviceId)) { throw new ArgumentNullException("deviceId"); } var key = GetSessionKey(appName, deviceId); - await _sessionLock.WaitAsync(CancellationToken.None).ConfigureAwait(false); + CheckDisposed(); - var userId = user == null ? (Guid?)null : user.Id; - var username = user == null ? null : user.Name; - - try + SessionInfo sessionInfo = _activeConnections.GetOrAdd(key, k => { - SessionInfo sessionInfo; - DeviceInfo device = null; - - if (!_activeConnections.TryGetValue(key, out sessionInfo)) - { - sessionInfo = new SessionInfo(this, _logger) - { - Client = appName, - DeviceId = deviceId, - ApplicationVersion = appVersion, - Id = key.GetMD5().ToString("N") - }; - - sessionInfo.DeviceName = deviceName; - sessionInfo.UserId = userId; - sessionInfo.UserName = username; - sessionInfo.RemoteEndPoint = remoteEndPoint; + return CreateSession(k, appName, appVersion, deviceId, deviceName, remoteEndPoint, user); + }); - OnSessionStarted(sessionInfo); + sessionInfo.UserId = user == null ? Guid.Empty : user.Id; + sessionInfo.UserName = user == null ? null : user.Name; + sessionInfo.UserPrimaryImageTag = user == null ? null : GetImageCacheTag(user, ImageType.Primary); + sessionInfo.RemoteEndPoint = remoteEndPoint; + sessionInfo.Client = appName; - _activeConnections.TryAdd(key, sessionInfo); + if (!sessionInfo.HasCustomDeviceName || string.IsNullOrEmpty(sessionInfo.DeviceName)) + { + sessionInfo.DeviceName = deviceName; + } - if (!string.IsNullOrEmpty(deviceId)) - { - var userIdString = userId.HasValue ? userId.Value.ToString("N") : null; - device = _deviceManager.RegisterDevice(deviceId, deviceName, appName, appVersion, userIdString); - } - } + sessionInfo.ApplicationVersion = appVersion; - device = device ?? _deviceManager.GetDevice(deviceId); + if (user == null) + { + sessionInfo.AdditionalUsers = new SessionUserInfo[] { }; + } - if (device == null) - { - var userIdString = userId.HasValue ? userId.Value.ToString("N") : null; - device = _deviceManager.RegisterDevice(deviceId, deviceName, appName, appVersion, userIdString); - } + return sessionInfo; + } - if (device != null) - { - if (!string.IsNullOrEmpty(device.CustomName)) - { - deviceName = device.CustomName; - } - } + private SessionInfo CreateSession(string key, string appName, string appVersion, string deviceId, string deviceName, string remoteEndPoint, User user) + { + var sessionInfo = new SessionInfo(this, _logger) + { + Client = appName, + DeviceId = deviceId, + ApplicationVersion = appVersion, + Id = key.GetMD5().ToString("N"), + ServerId = _appHost.SystemId + }; - sessionInfo.DeviceName = deviceName; - sessionInfo.UserId = userId; - sessionInfo.UserName = username; - sessionInfo.RemoteEndPoint = remoteEndPoint; - sessionInfo.ApplicationVersion = appVersion; + var username = user == null ? null : user.Name; - if (!userId.HasValue) - { - sessionInfo.AdditionalUsers = new SessionUserInfo[] { }; - } + sessionInfo.UserId = user == null ? Guid.Empty : user.Id; + sessionInfo.UserName = username; + sessionInfo.UserPrimaryImageTag = user == null ? null : GetImageCacheTag(user, ImageType.Primary); + sessionInfo.RemoteEndPoint = remoteEndPoint; - if (sessionInfo.SessionController == null) - { - sessionInfo.SessionController = _sessionFactories - .Select(i => i.GetSessionController(sessionInfo)) - .FirstOrDefault(i => i != null); - } + if (string.IsNullOrEmpty(deviceName)) + { + deviceName = "Network Device"; + } - return sessionInfo; + var deviceOptions = _deviceManager.GetDeviceOptions(deviceId); + if (string.IsNullOrEmpty(deviceOptions.CustomName)) + { + sessionInfo.DeviceName = deviceName; } - finally + else { - _sessionLock.Release(); + sessionInfo.DeviceName = deviceOptions.CustomName; + sessionInfo.HasCustomDeviceName = true; } + + OnSessionStarted(sessionInfo); + return sessionInfo; } private List<User> GetUsers(SessionInfo session) { var users = new List<User>(); - if (session.UserId.HasValue) + if (!session.UserId.Equals(Guid.Empty)) { - var user = _userManager.GetUserById(session.UserId.Value); + var user = _userManager.GetUserById(session.UserId); if (user == null) { @@ -498,11 +489,9 @@ namespace Emby.Server.Implementations.Session users.Add(user); - var additionalUsers = session.AdditionalUsers + users.AddRange(session.AdditionalUsers .Select(i => _userManager.GetUserById(i.UserId)) - .Where(i => i != null); - - users.AddRange(additionalUsers); + .Where(i => i != null)); } return users; @@ -546,7 +535,7 @@ namespace Emby.Server.Implementations.Session await OnPlaybackStopped(new PlaybackStopInfo { Item = session.NowPlayingItem, - ItemId = session.NowPlayingItem == null ? null : session.NowPlayingItem.Id, + ItemId = session.NowPlayingItem == null ? Guid.Empty : session.NowPlayingItem.Id, SessionId = session.Id, MediaSourceId = session.PlayState == null ? null : session.PlayState.MediaSourceId, PositionTicks = session.PlayState == null ? null : session.PlayState.PositionTicks @@ -568,12 +557,10 @@ namespace Emby.Server.Implementations.Session } } - private BaseItem GetNowPlayingItem(SessionInfo session, string itemId) + private BaseItem GetNowPlayingItem(SessionInfo session, Guid itemId) { - var idGuid = new Guid(itemId); - var item = session.FullNowPlayingItem; - if (item != null && item.Id == idGuid) + if (item != null && item.Id.Equals(itemId)) { return item; } @@ -593,6 +580,8 @@ namespace Emby.Server.Implementations.Session /// <exception cref="System.ArgumentNullException">info</exception> public async Task OnPlaybackStart(PlaybackStartInfo info) { + CheckDisposed(); + if (info == null) { throw new ArgumentNullException("info"); @@ -600,7 +589,7 @@ namespace Emby.Server.Implementations.Session var session = GetSession(info.SessionId); - var libraryItem = string.IsNullOrWhiteSpace(info.ItemId) + var libraryItem = info.ItemId.Equals(Guid.Empty) ? null : GetNowPlayingItem(session, info.ItemId); @@ -619,7 +608,7 @@ namespace Emby.Server.Implementations.Session { foreach (var user in users) { - OnPlaybackStart(user.Id, libraryItem); + OnPlaybackStart(user, libraryItem); } } @@ -633,12 +622,11 @@ namespace Emby.Server.Implementations.Session MediaInfo = info.Item, DeviceName = session.DeviceName, ClientName = session.Client, - DeviceId = session.DeviceId + DeviceId = session.DeviceId, + Session = session }, _logger); - await SendPlaybackStartNotification(session, CancellationToken.None).ConfigureAwait(false); - StartIdleCheckTimer(); } @@ -647,9 +635,9 @@ namespace Emby.Server.Implementations.Session /// </summary> /// <param name="userId">The user identifier.</param> /// <param name="item">The item.</param> - private void OnPlaybackStart(Guid userId, IHasUserData item) + private void OnPlaybackStart(User user, BaseItem item) { - var data = _userDataManager.GetUserData(userId, item); + var data = _userDataManager.GetUserData(user, item); data.PlayCount++; data.LastPlayedDate = DateTime.UtcNow; @@ -666,7 +654,7 @@ namespace Emby.Server.Implementations.Session data.Played = false; } - _userDataManager.SaveUserData(userId, item, data, UserDataSaveReason.PlaybackStart, CancellationToken.None); + _userDataManager.SaveUserData(user, item, data, UserDataSaveReason.PlaybackStart, CancellationToken.None); } public Task OnPlaybackProgress(PlaybackProgressInfo info) @@ -679,6 +667,8 @@ namespace Emby.Server.Implementations.Session /// </summary> public async Task OnPlaybackProgress(PlaybackProgressInfo info, bool isAutomated) { + CheckDisposed(); + if (info == null) { throw new ArgumentNullException("info"); @@ -686,7 +676,7 @@ namespace Emby.Server.Implementations.Session var session = GetSession(info.SessionId); - var libraryItem = string.IsNullOrWhiteSpace(info.ItemId) + var libraryItem = info.ItemId.Equals(Guid.Empty) ? null : GetNowPlayingItem(session, info.ItemId); @@ -694,7 +684,8 @@ namespace Emby.Server.Implementations.Session var users = GetUsers(session); - if (libraryItem != null) + // only update saved user data on actual check-ins, not automated ones + if (libraryItem != null && !isAutomated) { foreach (var user in users) { @@ -714,7 +705,8 @@ namespace Emby.Server.Implementations.Session DeviceId = session.DeviceId, IsPaused = info.IsPaused, PlaySessionId = info.PlaySessionId, - IsAutomated = isAutomated + IsAutomated = isAutomated, + Session = session }, _logger); @@ -728,39 +720,70 @@ namespace Emby.Server.Implementations.Session private void OnPlaybackProgress(User user, BaseItem item, PlaybackProgressInfo info) { - var data = _userDataManager.GetUserData(user.Id, item); + var data = _userDataManager.GetUserData(user, item); var positionTicks = info.PositionTicks; + var changed = false; + if (positionTicks.HasValue) { _userDataManager.UpdatePlayState(item, data, positionTicks.Value); + changed = true; + } - UpdatePlaybackSettings(user, info, data); + var tracksChanged = UpdatePlaybackSettings(user, info, data); + if (!tracksChanged) + { + changed = true; + } - _userDataManager.SaveUserData(user.Id, item, data, UserDataSaveReason.PlaybackProgress, CancellationToken.None); + if (changed) + { + _userDataManager.SaveUserData(user, item, data, UserDataSaveReason.PlaybackProgress, CancellationToken.None); } + } - private void UpdatePlaybackSettings(User user, PlaybackProgressInfo info, UserItemData data) + private bool UpdatePlaybackSettings(User user, PlaybackProgressInfo info, UserItemData data) { + var changed = false; + if (user.Configuration.RememberAudioSelections) { - data.AudioStreamIndex = info.AudioStreamIndex; + if (data.AudioStreamIndex != info.AudioStreamIndex) + { + data.AudioStreamIndex = info.AudioStreamIndex; + changed = true; + } } else { - data.AudioStreamIndex = null; + if (data.AudioStreamIndex.HasValue) + { + data.AudioStreamIndex = null; + changed = true; + } } if (user.Configuration.RememberSubtitleSelections) { - data.SubtitleStreamIndex = info.SubtitleStreamIndex; + if (data.SubtitleStreamIndex != info.SubtitleStreamIndex) + { + data.SubtitleStreamIndex = info.SubtitleStreamIndex; + changed = true; + } } else { - data.SubtitleStreamIndex = null; + if (data.SubtitleStreamIndex.HasValue) + { + data.SubtitleStreamIndex = null; + changed = true; + } } + + return changed; } /// <summary> @@ -772,6 +795,8 @@ namespace Emby.Server.Implementations.Session /// <exception cref="System.ArgumentOutOfRangeException">positionTicks</exception> public async Task OnPlaybackStopped(PlaybackStopInfo info) { + CheckDisposed(); + if (info == null) { throw new ArgumentNullException("info"); @@ -786,28 +811,28 @@ namespace Emby.Server.Implementations.Session session.StopAutomaticProgress(); - var libraryItem = string.IsNullOrWhiteSpace(info.ItemId) + var libraryItem = info.ItemId.Equals(Guid.Empty) ? null : GetNowPlayingItem(session, info.ItemId); // Normalize - if (string.IsNullOrWhiteSpace(info.MediaSourceId)) + if (string.IsNullOrEmpty(info.MediaSourceId)) { - info.MediaSourceId = info.ItemId; + info.MediaSourceId = info.ItemId.ToString("N"); } - if (!string.IsNullOrWhiteSpace(info.ItemId) && info.Item == null && libraryItem != null) + if (!info.ItemId.Equals(Guid.Empty) && info.Item == null && libraryItem != null) { var current = session.NowPlayingItem; - if (current == null || !string.Equals(current.Id, info.ItemId, StringComparison.OrdinalIgnoreCase)) + if (current == null || !info.ItemId.Equals(current.Id)) { MediaSourceInfo mediaSource = null; var hasMediaSources = libraryItem as IHasMediaSources; if (hasMediaSources != null) { - mediaSource = await GetMediaSource(hasMediaSources, info.MediaSourceId, info.LiveStreamId).ConfigureAwait(false); + mediaSource = await GetMediaSource(libraryItem, info.MediaSourceId, info.LiveStreamId).ConfigureAwait(false); } info.Item = GetItemInfo(libraryItem, mediaSource); @@ -829,6 +854,13 @@ namespace Emby.Server.Implementations.Session msString); } + if (info.NowPlayingQueue != null) + { + session.NowPlayingQueue = info.NowPlayingQueue; + } + + session.PlaylistItemId = info.PlaylistItemId; + RemoveNowPlayingItem(session); var users = GetUsers(session); @@ -838,11 +870,11 @@ namespace Emby.Server.Implementations.Session { foreach (var user in users) { - playedToCompletion = OnPlaybackStopped(user.Id, libraryItem, info.PositionTicks, info.Failed); + playedToCompletion = OnPlaybackStopped(user, libraryItem, info.PositionTicks, info.Failed); } } - if (!string.IsNullOrWhiteSpace(info.LiveStreamId)) + if (!string.IsNullOrEmpty(info.LiveStreamId)) { try { @@ -864,20 +896,19 @@ namespace Emby.Server.Implementations.Session MediaInfo = info.Item, DeviceName = session.DeviceName, ClientName = session.Client, - DeviceId = session.DeviceId + DeviceId = session.DeviceId, + Session = session }, _logger); - - await SendPlaybackStoppedNotification(session, CancellationToken.None).ConfigureAwait(false); } - private bool OnPlaybackStopped(Guid userId, BaseItem item, long? positionTicks, bool playbackFailed) + private bool OnPlaybackStopped(User user, BaseItem item, long? positionTicks, bool playbackFailed) { bool playedToCompletion = false; if (!playbackFailed) { - var data = _userDataManager.GetUserData(userId, item); + var data = _userDataManager.GetUserData(user, item); if (positionTicks.HasValue) { @@ -892,7 +923,7 @@ namespace Emby.Server.Implementations.Session playedToCompletion = true; } - _userDataManager.SaveUserData(userId, item, data, UserDataSaveReason.PlaybackFinished, CancellationToken.None); + _userDataManager.SaveUserData(user, item, data, UserDataSaveReason.PlaybackFinished, CancellationToken.None); } return playedToCompletion; @@ -932,6 +963,8 @@ namespace Emby.Server.Implementations.Session public Task SendMessageCommand(string controllingSessionId, string sessionId, MessageCommand command, CancellationToken cancellationToken) { + CheckDisposed(); + var generalCommand = new GeneralCommand { Name = GeneralCommandType.DisplayMessage.ToString() @@ -950,22 +983,37 @@ namespace Emby.Server.Implementations.Session public Task SendGeneralCommand(string controllingSessionId, string sessionId, GeneralCommand command, CancellationToken cancellationToken) { + CheckDisposed(); + var session = GetSessionToRemoteControl(sessionId); - if (!string.IsNullOrWhiteSpace(controllingSessionId)) + if (!string.IsNullOrEmpty(controllingSessionId)) { var controllingSession = GetSession(controllingSessionId); AssertCanControl(session, controllingSession); } - return session.SessionController.SendGeneralCommand(command, cancellationToken); + return SendMessageToSession(session, "GeneralCommand", command, cancellationToken); + } + + private async Task SendMessageToSession<T>(SessionInfo session, string name, T data, CancellationToken cancellationToken) + { + var controllers = session.SessionControllers.ToArray(); + var messageId = Guid.NewGuid().ToString("N"); + + foreach (var controller in controllers) + { + await controller.SendMessage(name, messageId, data, controllers, cancellationToken).ConfigureAwait(false); + } } public async Task SendPlayCommand(string controllingSessionId, string sessionId, PlayRequest command, CancellationToken cancellationToken) { + CheckDisposed(); + var session = GetSessionToRemoteControl(sessionId); - var user = session.UserId.HasValue ? _userManager.GetUserById(session.UserId.Value) : null; + var user = !session.UserId.Equals(Guid.Empty) ? _userManager.GetUserById(session.UserId) : null; List<BaseItem> items; @@ -994,7 +1042,7 @@ namespace Emby.Server.Implementations.Session command.PlayCommand = PlayCommand.PlayNow; } - command.ItemIds = items.Select(i => i.Id.ToString("N")).ToArray(items.Count); + command.ItemIds = items.Select(i => i.Id).ToArray(items.Count); if (user != null) { @@ -1004,11 +1052,6 @@ namespace Emby.Server.Implementations.Session } } - if (items.Any(i => !session.PlayableMediaTypes.Contains(i.MediaType, StringComparer.OrdinalIgnoreCase))) - { - throw new ArgumentException(string.Format("{0} is unable to play the requested media type.", session.DeviceName ?? session.Id)); - } - if (user != null && command.ItemIds.Length == 1 && user.Configuration.EnableNextEpisodeAutoPlay) { var episode = _libraryManager.GetItemById(command.ItemIds[0]) as Episode; @@ -1027,26 +1070,26 @@ namespace Emby.Server.Implementations.Session if (episodes.Count > 0) { - command.ItemIds = episodes.Select(i => i.Id.ToString("N")).ToArray(episodes.Count); + command.ItemIds = episodes.Select(i => i.Id).ToArray(episodes.Count); } } } } - if (!string.IsNullOrWhiteSpace(controllingSessionId)) + if (!string.IsNullOrEmpty(controllingSessionId)) { var controllingSession = GetSession(controllingSessionId); AssertCanControl(session, controllingSession); - if (controllingSession.UserId.HasValue) + if (!controllingSession.UserId.Equals(Guid.Empty)) { - command.ControllingUserId = controllingSession.UserId.Value.ToString("N"); + command.ControllingUserId = controllingSession.UserId; } } - await session.SessionController.SendPlayCommand(command, cancellationToken).ConfigureAwait(false); + await SendMessageToSession(session, "Play", command, cancellationToken).ConfigureAwait(false); } - private List<BaseItem> TranslateItemForPlayback(string id, User user) + private IList<BaseItem> TranslateItemForPlayback(Guid id, User user) { var item = _libraryManager.GetItemById(id); @@ -1060,7 +1103,7 @@ namespace Emby.Server.Implementations.Session if (byName != null) { - var items = byName.GetTaggedItems(new InternalItemsQuery(user) + return byName.GetTaggedItems(new InternalItemsQuery(user) { IsFolder = false, Recursive = true, @@ -1072,19 +1115,16 @@ namespace Emby.Server.Implementations.Session ItemFields.SortName } }, - IsVirtualItem = false + IsVirtualItem = false, + OrderBy = new ValueTuple<string, SortOrder>[] { new ValueTuple<string, SortOrder>(ItemSortBy.SortName, SortOrder.Ascending) } }); - - return FilterToSingleMediaType(items) - .OrderBy(i => i.SortName) - .ToList(); } if (item.IsFolder) { var folder = (Folder)item; - var itemsResult = folder.GetItemList(new InternalItemsQuery(user) + return folder.GetItemList(new InternalItemsQuery(user) { Recursive = true, IsFolder = false, @@ -1096,28 +1136,16 @@ namespace Emby.Server.Implementations.Session ItemFields.SortName } }, - IsVirtualItem = false + IsVirtualItem = false, + OrderBy = new ValueTuple<string, SortOrder>[] { new ValueTuple<string, SortOrder>(ItemSortBy.SortName, SortOrder.Ascending) } }); - - return FilterToSingleMediaType(itemsResult) - .OrderBy(i => i.SortName) - .ToList(); } return new List<BaseItem> { item }; } - private IEnumerable<BaseItem> FilterToSingleMediaType(IEnumerable<BaseItem> items) - { - return items - .Where(i => !string.IsNullOrWhiteSpace(i.MediaType)) - .ToLookup(i => i.MediaType, StringComparer.OrdinalIgnoreCase) - .OrderByDescending(i => i.Count()) - .FirstOrDefault(); - } - - private IEnumerable<BaseItem> TranslateItemForInstantMix(string id, User user) + private IEnumerable<BaseItem> TranslateItemForInstantMix(Guid id, User user) { var item = _libraryManager.GetItemById(id); @@ -1146,19 +1174,21 @@ namespace Emby.Server.Implementations.Session public Task SendPlaystateCommand(string controllingSessionId, string sessionId, PlaystateRequest command, CancellationToken cancellationToken) { + CheckDisposed(); + var session = GetSessionToRemoteControl(sessionId); - if (!string.IsNullOrWhiteSpace(controllingSessionId)) + if (!string.IsNullOrEmpty(controllingSessionId)) { var controllingSession = GetSession(controllingSessionId); AssertCanControl(session, controllingSession); - if (controllingSession.UserId.HasValue) + if (!controllingSession.UserId.Equals(Guid.Empty)) { - command.ControllingUserId = controllingSession.UserId.Value.ToString("N"); + command.ControllingUserId = controllingSession.UserId.ToString("N"); } } - return session.SessionController.SendPlaystateCommand(command, cancellationToken); + return SendMessageToSession(session, "Playstate", command, cancellationToken); } private void AssertCanControl(SessionInfo session, SessionInfo controllingSession) @@ -1180,20 +1210,22 @@ namespace Emby.Server.Implementations.Session /// <returns>Task.</returns> public async Task SendRestartRequiredNotification(CancellationToken cancellationToken) { - var sessions = Sessions.Where(i => i.IsActive && i.SessionController != null).ToList(); + CheckDisposed(); + + var sessions = Sessions.ToList(); var tasks = sessions.Select(session => Task.Run(async () => { try { - await session.SessionController.SendRestartRequiredNotification(cancellationToken).ConfigureAwait(false); + await SendMessageToSession(session, "RestartRequired", string.Empty, cancellationToken).ConfigureAwait(false); } catch (Exception ex) { _logger.ErrorException("Error in SendRestartRequiredNotification.", ex); } - }, cancellationToken)); + }, cancellationToken)).ToArray(); await Task.WhenAll(tasks).ConfigureAwait(false); } @@ -1205,20 +1237,22 @@ namespace Emby.Server.Implementations.Session /// <returns>Task.</returns> public Task SendServerShutdownNotification(CancellationToken cancellationToken) { - var sessions = Sessions.Where(i => i.IsActive && i.SessionController != null).ToList(); + CheckDisposed(); + + var sessions = Sessions.ToList(); var tasks = sessions.Select(session => Task.Run(async () => { try { - await session.SessionController.SendServerShutdownNotification(cancellationToken).ConfigureAwait(false); + await SendMessageToSession(session, "ServerShuttingDown", string.Empty, cancellationToken).ConfigureAwait(false); } catch (Exception ex) { _logger.ErrorException("Error in SendServerShutdownNotification.", ex); } - }, cancellationToken)); + }, cancellationToken)).ToArray(); return Task.WhenAll(tasks); } @@ -1230,85 +1264,24 @@ namespace Emby.Server.Implementations.Session /// <returns>Task.</returns> public Task SendServerRestartNotification(CancellationToken cancellationToken) { + CheckDisposed(); + _logger.Debug("Beginning SendServerRestartNotification"); - var sessions = Sessions.Where(i => i.IsActive && i.SessionController != null).ToList(); + var sessions = Sessions.ToList(); var tasks = sessions.Select(session => Task.Run(async () => { try { - await session.SessionController.SendServerRestartNotification(cancellationToken).ConfigureAwait(false); + await SendMessageToSession(session, "ServerRestarting", string.Empty, cancellationToken).ConfigureAwait(false); } catch (Exception ex) { _logger.ErrorException("Error in SendServerRestartNotification.", ex); } - }, cancellationToken)); - - return Task.WhenAll(tasks); - } - - public Task SendSessionEndedNotification(SessionInfo sessionInfo, CancellationToken cancellationToken) - { - var sessions = Sessions.Where(i => i.IsActive && i.SessionController != null).ToList(); - var dto = GetSessionInfoDto(sessionInfo); - - var tasks = sessions.Select(session => Task.Run(async () => - { - try - { - await session.SessionController.SendSessionEndedNotification(dto, cancellationToken).ConfigureAwait(false); - } - catch (Exception ex) - { - _logger.ErrorException("Error in SendSessionEndedNotification.", ex); - } - - }, cancellationToken)); - - return Task.WhenAll(tasks); - } - - public Task SendPlaybackStartNotification(SessionInfo sessionInfo, CancellationToken cancellationToken) - { - var sessions = Sessions.Where(i => i.IsActive && i.SessionController != null).ToList(); - var dto = GetSessionInfoDto(sessionInfo); - - var tasks = sessions.Select(session => Task.Run(async () => - { - try - { - await session.SessionController.SendPlaybackStartNotification(dto, cancellationToken).ConfigureAwait(false); - } - catch (Exception ex) - { - _logger.ErrorException("Error in SendPlaybackStartNotification.", ex); - } - - }, cancellationToken)); - - return Task.WhenAll(tasks); - } - - public Task SendPlaybackStoppedNotification(SessionInfo sessionInfo, CancellationToken cancellationToken) - { - var sessions = Sessions.Where(i => i.IsActive && i.SessionController != null).ToList(); - var dto = GetSessionInfoDto(sessionInfo); - - var tasks = sessions.Select(session => Task.Run(async () => - { - try - { - await session.SessionController.SendPlaybackStoppedNotification(dto, cancellationToken).ConfigureAwait(false); - } - catch (Exception ex) - { - _logger.ErrorException("Error in SendPlaybackStoppedNotification.", ex); - } - - }, cancellationToken)); + }, cancellationToken)).ToArray(); return Task.WhenAll(tasks); } @@ -1320,16 +1293,18 @@ namespace Emby.Server.Implementations.Session /// <param name="userId">The user identifier.</param> /// <exception cref="System.UnauthorizedAccessException">Cannot modify additional users without authenticating first.</exception> /// <exception cref="System.ArgumentException">The requested user is already the primary user of the session.</exception> - public void AddAdditionalUser(string sessionId, string userId) + public void AddAdditionalUser(string sessionId, Guid userId) { + CheckDisposed(); + var session = GetSession(sessionId); - if (session.UserId.HasValue && session.UserId.Value == new Guid(userId)) + if (session.UserId.Equals(userId)) { throw new ArgumentException("The requested user is already the primary user of the session."); } - if (session.AdditionalUsers.All(i => new Guid(i.UserId) != new Guid(userId))) + if (session.AdditionalUsers.All(i => !i.UserId.Equals(userId))) { var user = _userManager.GetUserById(userId); @@ -1352,16 +1327,18 @@ namespace Emby.Server.Implementations.Session /// <param name="userId">The user identifier.</param> /// <exception cref="System.UnauthorizedAccessException">Cannot modify additional users without authenticating first.</exception> /// <exception cref="System.ArgumentException">The requested user is already the primary user of the session.</exception> - public void RemoveAdditionalUser(string sessionId, string userId) + public void RemoveAdditionalUser(string sessionId, Guid userId) { + CheckDisposed(); + var session = GetSession(sessionId); - if (session.UserId.HasValue && session.UserId.Value == new Guid(userId)) + if (session.UserId.Equals(userId)) { throw new ArgumentException("The requested user is already the primary user of the session."); } - var user = session.AdditionalUsers.FirstOrDefault(i => new Guid(i.UserId) == new Guid(userId)); + var user = session.AdditionalUsers.FirstOrDefault(i => i.UserId.Equals(userId)); if (user != null) { @@ -1389,12 +1366,13 @@ namespace Emby.Server.Implementations.Session private async Task<AuthenticationResult> AuthenticateNewSessionInternal(AuthenticationRequest request, bool enforcePassword) { + CheckDisposed(); + User user = null; - if (!string.IsNullOrWhiteSpace(request.UserId)) + if (!request.UserId.Equals(Guid.Empty)) { - var idGuid = new Guid(request.UserId); user = _userManager.Users - .FirstOrDefault(i => i.Id == idGuid); + .FirstOrDefault(i => i.Id == request.UserId); } if (user == null) @@ -1405,14 +1383,10 @@ namespace Emby.Server.Implementations.Session if (user != null) { - if (!user.IsParentalScheduleAllowed()) - { - throw new SecurityException("User is not allowed access at this time."); - } - - if (!string.IsNullOrWhiteSpace(request.DeviceId)) + // TODO: Move this to userManager? + if (!string.IsNullOrEmpty(request.DeviceId)) { - if (!_deviceManager.CanAccessDevice(user.Id.ToString("N"), request.DeviceId)) + if (!_deviceManager.CanAccessDevice(user, request.DeviceId)) { throw new SecurityException("User is not allowed access from this device."); } @@ -1421,7 +1395,7 @@ namespace Emby.Server.Implementations.Session if (enforcePassword) { - var result = await _userManager.AuthenticateUser(request.Username, request.Password, request.PasswordSha1, request.PasswordMd5, request.RemoteEndPoint, true).ConfigureAwait(false); + var result = await _userManager.AuthenticateUser(request.Username, request.Password, request.PasswordSha1, request.RemoteEndPoint, true).ConfigureAwait(false); if (result == null) { @@ -1433,72 +1407,95 @@ namespace Emby.Server.Implementations.Session user = result; } - var token = GetAuthorizationToken(user.Id.ToString("N"), request.DeviceId, request.App, request.AppVersion, request.DeviceName); + var token = GetAuthorizationToken(user, request.DeviceId, request.App, request.AppVersion, request.DeviceName); - EventHelper.FireEventIfNotNull(AuthenticationSucceeded, this, new GenericEventArgs<AuthenticationRequest>(request), _logger); - - var session = await LogSessionActivity(request.App, + var session = LogSessionActivity(request.App, request.AppVersion, request.DeviceId, request.DeviceName, request.RemoteEndPoint, - user) - .ConfigureAwait(false); + user); - return new AuthenticationResult + var returnResult = new AuthenticationResult { User = _userManager.GetUserDto(user, request.RemoteEndPoint), - SessionInfo = GetSessionInfoDto(session), + SessionInfo = session, AccessToken = token, ServerId = _appHost.SystemId }; - } + EventHelper.FireEventIfNotNull(AuthenticationSucceeded, this, new GenericEventArgs<AuthenticationResult>(returnResult), _logger); + + return returnResult; + } - private string GetAuthorizationToken(string userId, string deviceId, string app, string appVersion, string deviceName) + private string GetAuthorizationToken(User user, string deviceId, string app, string appVersion, string deviceName) { var existing = _authRepo.Get(new AuthenticationInfoQuery { DeviceId = deviceId, - IsActive = true, - UserId = userId, + UserId = user.Id, Limit = 1 - }); - if (existing.Items.Length > 0) + }).Items.FirstOrDefault(); + + var allExistingForDevice = _authRepo.Get(new AuthenticationInfoQuery + { + DeviceId = deviceId + + }).Items; + + foreach (var auth in allExistingForDevice) + { + if (existing == null || !string.Equals(auth.AccessToken, existing.AccessToken, StringComparison.Ordinal)) + { + try + { + Logout(auth); + } + catch + { + + } + } + } + + if (existing != null) { - var token = existing.Items[0].AccessToken; - _logger.Info("Reissuing access token: " + token); - return token; + _logger.Info("Reissuing access token: " + existing.AccessToken); + return existing.AccessToken; } + var now = DateTime.UtcNow; + var newToken = new AuthenticationInfo { AppName = app, AppVersion = appVersion, - DateCreated = DateTime.UtcNow, + DateCreated = now, + DateLastActivity = now, DeviceId = deviceId, DeviceName = deviceName, - UserId = userId, - IsActive = true, - AccessToken = Guid.NewGuid().ToString("N") + UserId = user.Id, + AccessToken = Guid.NewGuid().ToString("N"), + UserName = user.Name }; - _logger.Info("Creating new access token for user {0}", userId); - _authRepo.Create(newToken, CancellationToken.None); + _logger.Info("Creating new access token for user {0}", user.Id); + _authRepo.Create(newToken); return newToken.AccessToken; } public void Logout(string accessToken) { - if (string.IsNullOrWhiteSpace(accessToken)) + CheckDisposed(); + + if (string.IsNullOrEmpty(accessToken)) { throw new ArgumentNullException("accessToken"); } - _logger.Info("Logging out access token {0}", accessToken); - var existing = _authRepo.Get(new AuthenticationInfoQuery { Limit = 1, @@ -1508,33 +1505,41 @@ namespace Emby.Server.Implementations.Session if (existing != null) { - existing.IsActive = false; + Logout(existing); + } + } - _authRepo.Update(existing, CancellationToken.None); + public void Logout(AuthenticationInfo existing) + { + CheckDisposed(); - var sessions = Sessions - .Where(i => string.Equals(i.DeviceId, existing.DeviceId, StringComparison.OrdinalIgnoreCase)) - .ToList(); + _logger.Info("Logging out access token {0}", existing.AccessToken); + + _authRepo.Delete(existing); - foreach (var session in sessions) + var sessions = Sessions + .Where(i => string.Equals(i.DeviceId, existing.DeviceId, StringComparison.OrdinalIgnoreCase)) + .ToList(); + + foreach (var session in sessions) + { + try { - try - { - ReportSessionEnded(session.Id); - } - catch (Exception ex) - { - _logger.ErrorException("Error reporting session ended", ex); - } + ReportSessionEnded(session.Id); + } + catch (Exception ex) + { + _logger.ErrorException("Error reporting session ended", ex); } } } - public void RevokeUserTokens(string userId, string currentAccessToken) + public void RevokeUserTokens(Guid userId, string currentAccessToken) { + CheckDisposed(); + var existing = _authRepo.Get(new AuthenticationInfoQuery { - IsActive = true, UserId = userId }); @@ -1542,7 +1547,7 @@ namespace Emby.Server.Implementations.Session { if (!string.Equals(currentAccessToken, info.AccessToken, StringComparison.OrdinalIgnoreCase)) { - Logout(info.AccessToken); + Logout(info); } } } @@ -1559,6 +1564,8 @@ namespace Emby.Server.Implementations.Session /// <param name="capabilities">The capabilities.</param> public void ReportCapabilities(string sessionId, ClientCapabilities capabilities) { + CheckDisposed(); + var session = GetSession(sessionId); ReportCapabilities(session, capabilities, true); @@ -1570,24 +1577,22 @@ namespace Emby.Server.Implementations.Session { session.Capabilities = capabilities; - if (!string.IsNullOrWhiteSpace(capabilities.MessageCallbackUrl)) + if (!string.IsNullOrEmpty(capabilities.PushToken)) { - var controller = session.SessionController as HttpSessionController; - - if (controller == null) + if (string.Equals(capabilities.PushTokenType, "firebase", StringComparison.OrdinalIgnoreCase) && FirebaseSessionController.IsSupported(_appHost)) { - session.SessionController = new HttpSessionController(_httpClient, _jsonSerializer, session, capabilities.MessageCallbackUrl, this); + EnsureFirebaseController(session, capabilities.PushToken); } } - EventHelper.FireEventIfNotNull(CapabilitiesChanged, this, new SessionEventArgs + if (saveCapabilities) { - SessionInfo = session + EventHelper.FireEventIfNotNull(CapabilitiesChanged, this, new SessionEventArgs + { + SessionInfo = session - }, _logger); + }, _logger); - if (saveCapabilities) - { try { SaveCapabilities(session.DeviceId, capabilities); @@ -1599,6 +1604,11 @@ namespace Emby.Server.Implementations.Session } } + private void EnsureFirebaseController(SessionInfo session, string token) + { + session.EnsureController<FirebaseSessionController>(s => new FirebaseSessionController(_httpClient, _appHost, _jsonSerializer, s, token, this)); + } + private ClientCapabilities GetSavedCapabilities(string deviceId) { return _deviceManager.GetCapabilities(deviceId); @@ -1609,45 +1619,6 @@ namespace Emby.Server.Implementations.Session _deviceManager.SaveCapabilities(deviceId, capabilities); } - public SessionInfoDto GetSessionInfoDto(SessionInfo session) - { - var dto = new SessionInfoDto - { - Client = session.Client, - DeviceId = session.DeviceId, - DeviceName = session.DeviceName, - Id = session.Id, - LastActivityDate = session.LastActivityDate, - NowViewingItem = session.NowViewingItem, - ApplicationVersion = session.ApplicationVersion, - PlayableMediaTypes = session.PlayableMediaTypes, - AdditionalUsers = session.AdditionalUsers, - SupportedCommands = session.SupportedCommands, - UserName = session.UserName, - NowPlayingItem = session.NowPlayingItem, - SupportsRemoteControl = session.SupportsMediaControl, - PlayState = session.PlayState, - AppIconUrl = session.AppIconUrl, - TranscodingInfo = session.NowPlayingItem == null ? null : session.TranscodingInfo - }; - - dto.ServerId = _appHost.SystemId; - - if (session.UserId.HasValue) - { - dto.UserId = session.UserId.Value.ToString("N"); - - var user = _userManager.GetUserById(session.UserId.Value); - - if (user != null) - { - dto.UserPrimaryImageTag = GetImageCacheTag(user, ImageType.Primary); - } - } - - return dto; - } - private DtoOptions _itemInfoDtoOptions; /// <summary> @@ -1672,7 +1643,6 @@ namespace Emby.Server.Implementations.Session var fields = dtoOptions.Fields.ToList(); fields.Remove(ItemFields.BasicSyncInfo); - fields.Remove(ItemFields.SyncInfo); fields.Remove(ItemFields.CanDelete); fields.Remove(ItemFields.CanDownload); fields.Remove(ItemFields.ChildCount); @@ -1682,7 +1652,6 @@ namespace Emby.Server.Implementations.Session fields.Remove(ItemFields.DateLastSaved); fields.Remove(ItemFields.DisplayPreferencesId); fields.Remove(ItemFields.Etag); - fields.Remove(ItemFields.ExternalEtag); fields.Remove(ItemFields.InheritedParentalRatingValue); fields.Remove(ItemFields.ItemCounts); fields.Remove(ItemFields.MediaSourceCount); @@ -1698,8 +1667,7 @@ namespace Emby.Server.Implementations.Session fields.Remove(ItemFields.Settings); fields.Remove(ItemFields.SortName); fields.Remove(ItemFields.Tags); - fields.Remove(ItemFields.ThemeSongIds); - fields.Remove(ItemFields.ThemeVideoIds); + fields.Remove(ItemFields.ExtraIds); dtoOptions.Fields = fields.ToArray(fields.Count); @@ -1729,14 +1697,9 @@ namespace Emby.Server.Implementations.Session } } - private string GetDtoId(BaseItem item) - { - return _dtoService.GetDtoId(item); - } - public void ReportNowViewingItem(string sessionId, string itemId) { - if (string.IsNullOrWhiteSpace(itemId)) + if (string.IsNullOrEmpty(itemId)) { throw new ArgumentNullException("itemId"); } @@ -1776,46 +1739,31 @@ namespace Emby.Server.Implementations.Session string.Equals(i.Client, client)); } - public Task<SessionInfo> GetSessionByAuthenticationToken(AuthenticationInfo info, string deviceId, string remoteEndpoint, string appVersion) + public SessionInfo GetSessionByAuthenticationToken(AuthenticationInfo info, string deviceId, string remoteEndpoint, string appVersion) { if (info == null) { throw new ArgumentNullException("info"); } - var user = string.IsNullOrWhiteSpace(info.UserId) + var user = info.UserId.Equals(Guid.Empty) ? null : _userManager.GetUserById(info.UserId); - appVersion = string.IsNullOrWhiteSpace(appVersion) + appVersion = string.IsNullOrEmpty(appVersion) ? info.AppVersion : appVersion; var deviceName = info.DeviceName; var appName = info.AppName; - if (!string.IsNullOrWhiteSpace(deviceId)) - { - // Replace the info from the token with more recent info - var device = _deviceManager.GetDevice(deviceId); - if (device != null) - { - deviceName = device.Name; - appName = device.AppName; - - if (!string.IsNullOrWhiteSpace(device.AppVersion)) - { - appVersion = device.AppVersion; - } - } - } - else + if (string.IsNullOrEmpty(deviceId)) { deviceId = info.DeviceId; } // Prevent argument exception - if (string.IsNullOrWhiteSpace(appVersion)) + if (string.IsNullOrEmpty(appVersion)) { appVersion = "1"; } @@ -1823,7 +1771,7 @@ namespace Emby.Server.Implementations.Session return LogSessionActivity(appName, appVersion, deviceId, deviceName, remoteEndpoint, user); } - public Task<SessionInfo> GetSessionByAuthenticationToken(string token, string deviceId, string remoteEndpoint) + public SessionInfo GetSessionByAuthenticationToken(string token, string deviceId, string remoteEndpoint) { var result = _authRepo.Get(new AuthenticationInfoQuery { @@ -1834,7 +1782,7 @@ namespace Emby.Server.Implementations.Session if (info == null) { - return Task.FromResult<SessionInfo>(null); + return null; } return GetSessionByAuthenticationToken(info, deviceId, remoteEndpoint, null); @@ -1842,51 +1790,115 @@ namespace Emby.Server.Implementations.Session public Task SendMessageToAdminSessions<T>(string name, T data, CancellationToken cancellationToken) { - var adminUserIds = _userManager.Users.Where(i => i.Policy.IsAdministrator).Select(i => i.Id.ToString("N")).ToList(); + CheckDisposed(); + + var adminUserIds = _userManager.Users.Where(i => i.Policy.IsAdministrator).Select(i => i.Id).ToList(); return SendMessageToUserSessions(adminUserIds, name, data, cancellationToken); } - public Task SendMessageToUserSessions<T>(List<string> userIds, string name, T data, - CancellationToken cancellationToken) + public Task SendMessageToUserSessions<T>(List<Guid> userIds, string name, Func<T> dataFn, CancellationToken cancellationToken) + { + CheckDisposed(); + + var sessions = Sessions.Where(i => userIds.Any(i.ContainsUser)).ToList(); + + if (sessions.Count == 0) + { + return Task.CompletedTask; + } + + var data = dataFn(); + + var tasks = sessions.Select(session => Task.Run(async () => + { + try + { + await SendMessageToSession(session, name, data, cancellationToken).ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.ErrorException("Error sending message", ex); + } + + }, cancellationToken)).ToArray(); + + return Task.WhenAll(tasks); + } + + public Task SendMessageToUserSessions<T>(List<Guid> userIds, string name, T data, CancellationToken cancellationToken) + { + CheckDisposed(); + + var sessions = Sessions.Where(i => userIds.Any(i.ContainsUser)).ToList(); + + var tasks = sessions.Select(session => Task.Run(async () => + { + try + { + await SendMessageToSession(session, name, data, cancellationToken).ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.ErrorException("Error sending message", ex); + } + + }, cancellationToken)).ToArray(); + + return Task.WhenAll(tasks); + } + + public Task SendMessageToUserDeviceSessions<T>(string deviceId, string name, T data, CancellationToken cancellationToken) { - var sessions = Sessions.Where(i => i.IsActive && i.SessionController != null && userIds.Any(i.ContainsUser)).ToList(); + CheckDisposed(); + + var sessions = Sessions.Where(i => string.Equals(i.DeviceId, deviceId, StringComparison.OrdinalIgnoreCase)).ToList(); var tasks = sessions.Select(session => Task.Run(async () => { try { - await session.SessionController.SendMessage(name, data, cancellationToken).ConfigureAwait(false); + await SendMessageToSession(session, name, data, cancellationToken).ConfigureAwait(false); } catch (Exception ex) { _logger.ErrorException("Error sending message", ex); } - }, cancellationToken)); + }, cancellationToken)).ToArray(); return Task.WhenAll(tasks); } - public Task SendMessageToUserDeviceSessions<T>(string deviceId, string name, T data, - CancellationToken cancellationToken) + public Task SendMessageToUserDeviceAndAdminSessions<T>(string deviceId, string name, T data, CancellationToken cancellationToken) { - var sessions = Sessions.Where(i => i.IsActive && i.SessionController != null && string.Equals(i.DeviceId, deviceId, StringComparison.OrdinalIgnoreCase)).ToList(); + CheckDisposed(); + + var sessions = Sessions + .Where(i => string.Equals(i.DeviceId, deviceId, StringComparison.OrdinalIgnoreCase) || IsAdminSession(i)) + .ToList(); var tasks = sessions.Select(session => Task.Run(async () => { try { - await session.SessionController.SendMessage(name, data, cancellationToken).ConfigureAwait(false); + await SendMessageToSession(session, name, data, cancellationToken).ConfigureAwait(false); } catch (Exception ex) { _logger.ErrorException("Error sending message", ex); } - }, cancellationToken)); + }, cancellationToken)).ToArray(); return Task.WhenAll(tasks); } + + private bool IsAdminSession(SessionInfo s) + { + var user = _userManager.GetUserById(s.UserId); + + return user != null && user.Policy.IsAdministrator; + } } }
\ No newline at end of file diff --git a/Emby.Server.Implementations/Session/SessionWebSocketListener.cs b/Emby.Server.Implementations/Session/SessionWebSocketListener.cs index a5af843db..9ab4753fb 100644 --- a/Emby.Server.Implementations/Session/SessionWebSocketListener.cs +++ b/Emby.Server.Implementations/Session/SessionWebSocketListener.cs @@ -19,11 +19,6 @@ namespace Emby.Server.Implementations.Session public class SessionWebSocketListener : IWebSocketListener, IDisposable { /// <summary> - /// The _true task result - /// </summary> - private readonly Task _trueTaskResult = Task.FromResult(true); - - /// <summary> /// The _session manager /// </summary> private readonly ISessionManager _sessionManager; @@ -39,7 +34,6 @@ namespace Emby.Server.Implementations.Session private readonly IJsonSerializer _json; private readonly IHttpServer _httpServer; - private readonly IServerManager _serverManager; /// <summary> @@ -50,32 +44,22 @@ namespace Emby.Server.Implementations.Session /// <param name="json">The json.</param> /// <param name="httpServer">The HTTP server.</param> /// <param name="serverManager">The server manager.</param> - public SessionWebSocketListener(ISessionManager sessionManager, ILogManager logManager, IJsonSerializer json, IHttpServer httpServer, IServerManager serverManager) + public SessionWebSocketListener(ISessionManager sessionManager, ILogManager logManager, IJsonSerializer json, IHttpServer httpServer) { _sessionManager = sessionManager; _logger = logManager.GetLogger(GetType().Name); _json = json; _httpServer = httpServer; - _serverManager = serverManager; - serverManager.WebSocketConnected += _serverManager_WebSocketConnected; + httpServer.WebSocketConnected += _serverManager_WebSocketConnected; } - async void _serverManager_WebSocketConnected(object sender, GenericEventArgs<IWebSocketConnection> e) + void _serverManager_WebSocketConnected(object sender, GenericEventArgs<IWebSocketConnection> e) { - var session = await GetSession(e.Argument.QueryString, e.Argument.RemoteEndPoint).ConfigureAwait(false); + var session = GetSession(e.Argument.QueryString, e.Argument.RemoteEndPoint); if (session != null) { - var controller = session.SessionController as WebSocketController; - - if (controller == null) - { - controller = new WebSocketController(session, _logger, _sessionManager); - } - - controller.AddWebSocket(e.Argument); - - session.SessionController = controller; + EnsureController(session, e.Argument); } else { @@ -83,7 +67,7 @@ namespace Emby.Server.Implementations.Session } } - private Task<SessionInfo> GetSession(QueryParamCollection queryString, string remoteEndpoint) + private SessionInfo GetSession(QueryParamCollection queryString, string remoteEndpoint) { if (queryString == null) { @@ -93,7 +77,7 @@ namespace Emby.Server.Implementations.Session var token = queryString["api_key"]; if (string.IsNullOrWhiteSpace(token)) { - return Task.FromResult<SessionInfo>(null); + return null; } var deviceId = queryString["deviceId"]; return _sessionManager.GetSessionByAuthenticationToken(token, deviceId, remoteEndpoint); @@ -101,8 +85,7 @@ namespace Emby.Server.Implementations.Session public void Dispose() { - _serverManager.WebSocketConnected -= _serverManager_WebSocketConnected; - GC.SuppressFinalize(this); + _httpServer.WebSocketConnected -= _serverManager_WebSocketConnected; } /// <summary> @@ -112,350 +95,15 @@ namespace Emby.Server.Implementations.Session /// <returns>Task.</returns> public Task ProcessMessage(WebSocketMessageInfo message) { - if (string.Equals(message.MessageType, "Identity", StringComparison.OrdinalIgnoreCase)) - { - ProcessIdentityMessage(message); - } - else if (string.Equals(message.MessageType, "Context", StringComparison.OrdinalIgnoreCase)) - { - ProcessContextMessage(message); - } - else if (string.Equals(message.MessageType, "PlaybackStart", StringComparison.OrdinalIgnoreCase)) - { - OnPlaybackStart(message); - } - else if (string.Equals(message.MessageType, "PlaybackProgress", StringComparison.OrdinalIgnoreCase)) - { - OnPlaybackProgress(message); - } - else if (string.Equals(message.MessageType, "PlaybackStopped", StringComparison.OrdinalIgnoreCase)) - { - OnPlaybackStopped(message); - } - else if (string.Equals(message.MessageType, "ReportPlaybackStart", StringComparison.OrdinalIgnoreCase)) - { - ReportPlaybackStart(message); - } - else if (string.Equals(message.MessageType, "ReportPlaybackProgress", StringComparison.OrdinalIgnoreCase)) - { - ReportPlaybackProgress(message); - } - else if (string.Equals(message.MessageType, "ReportPlaybackStopped", StringComparison.OrdinalIgnoreCase)) - { - ReportPlaybackStopped(message); - } - - return _trueTaskResult; - } - - /// <summary> - /// Processes the identity message. - /// </summary> - /// <param name="message">The message.</param> - private async void ProcessIdentityMessage(WebSocketMessageInfo message) - { - _logger.Debug("Received Identity message: " + message.Data); - - var vals = message.Data.Split('|'); - - if (vals.Length < 3) - { - _logger.Error("Client sent invalid identity message."); - return; - } - - var client = vals[0]; - var deviceId = vals[1]; - var version = vals[2]; - var deviceName = vals.Length > 3 ? vals[3] : string.Empty; - - var session = _sessionManager.GetSession(deviceId, client, version); - - if (session == null && !string.IsNullOrEmpty(deviceName)) - { - _logger.Debug("Logging session activity"); - - session = await _sessionManager.LogSessionActivity(client, version, deviceId, deviceName, message.Connection.RemoteEndPoint, null).ConfigureAwait(false); - } - - if (session != null) - { - var controller = session.SessionController as WebSocketController; - - if (controller == null) - { - controller = new WebSocketController(session, _logger, _sessionManager); - } - - controller.AddWebSocket(message.Connection); - - session.SessionController = controller; - } - else - { - _logger.Warn("Unable to determine session based on identity message: {0}", message.Data); - } - } - - /// <summary> - /// Processes the context message. - /// </summary> - /// <param name="message">The message.</param> - private void ProcessContextMessage(WebSocketMessageInfo message) - { - var session = GetSessionFromMessage(message); - - if (session != null) - { - var vals = message.Data.Split('|'); - - var itemId = vals[1]; - - if (!string.IsNullOrWhiteSpace(itemId)) - { - _sessionManager.ReportNowViewingItem(session.Id, itemId); - } - } - } - - /// <summary> - /// Gets the session from message. - /// </summary> - /// <param name="message">The message.</param> - /// <returns>SessionInfo.</returns> - private SessionInfo GetSessionFromMessage(WebSocketMessageInfo message) - { - var result = _sessionManager.Sessions.FirstOrDefault(i => - { - var controller = i.SessionController as WebSocketController; - - if (controller != null) - { - if (controller.Sockets.Any(s => s.Id == message.Connection.Id)) - { - return true; - } - } - - return false; - - }); - - if (result == null) - { - _logger.Error("Unable to find session based on web socket message"); - } - - return result; - } - - private readonly CultureInfo _usCulture = new CultureInfo("en-US"); - - /// <summary> - /// Reports the playback start. - /// </summary> - /// <param name="message">The message.</param> - private void OnPlaybackStart(WebSocketMessageInfo message) - { - _logger.Debug("Received PlaybackStart message"); - - var session = GetSessionFromMessage(message); - - if (session != null && session.UserId.HasValue) - { - var vals = message.Data.Split('|'); - - var itemId = vals[0]; - - var canSeek = true; - - if (vals.Length > 1) - { - canSeek = string.Equals(vals[1], "true", StringComparison.OrdinalIgnoreCase); - } - if (vals.Length > 2) - { - // vals[2] used to be QueueableMediaTypes - } - - var info = new PlaybackStartInfo - { - CanSeek = canSeek, - ItemId = itemId, - SessionId = session.Id - }; - - if (vals.Length > 3) - { - info.MediaSourceId = vals[3]; - } - - if (vals.Length > 4 && !string.IsNullOrWhiteSpace(vals[4])) - { - info.AudioStreamIndex = int.Parse(vals[4], _usCulture); - } - - if (vals.Length > 5 && !string.IsNullOrWhiteSpace(vals[5])) - { - info.SubtitleStreamIndex = int.Parse(vals[5], _usCulture); - } - - _sessionManager.OnPlaybackStart(info); - } + return Task.CompletedTask; } - private void ReportPlaybackStart(WebSocketMessageInfo message) + private void EnsureController(SessionInfo session, IWebSocketConnection connection) { - _logger.Debug("Received ReportPlaybackStart message"); - - var session = GetSessionFromMessage(message); - - if (session != null && session.UserId.HasValue) - { - var info = _json.DeserializeFromString<PlaybackStartInfo>(message.Data); + var controllerInfo = session.EnsureController<WebSocketController>(s => new WebSocketController(s, _logger, _sessionManager)); - info.SessionId = session.Id; - - _sessionManager.OnPlaybackStart(info); - } - } - - private void ReportPlaybackProgress(WebSocketMessageInfo message) - { - //_logger.Debug("Received ReportPlaybackProgress message"); - - var session = GetSessionFromMessage(message); - - if (session != null && session.UserId.HasValue) - { - var info = _json.DeserializeFromString<PlaybackProgressInfo>(message.Data); - - info.SessionId = session.Id; - - _sessionManager.OnPlaybackProgress(info); - } - } - - /// <summary> - /// Reports the playback progress. - /// </summary> - /// <param name="message">The message.</param> - private void OnPlaybackProgress(WebSocketMessageInfo message) - { - var session = GetSessionFromMessage(message); - - if (session != null && session.UserId.HasValue) - { - var vals = message.Data.Split('|'); - - var itemId = vals[0]; - - long? positionTicks = null; - - if (vals.Length > 1) - { - long pos; - - if (long.TryParse(vals[1], out pos)) - { - positionTicks = pos; - } - } - - var isPaused = vals.Length > 2 && string.Equals(vals[2], "true", StringComparison.OrdinalIgnoreCase); - var isMuted = vals.Length > 3 && string.Equals(vals[3], "true", StringComparison.OrdinalIgnoreCase); - - var info = new PlaybackProgressInfo - { - ItemId = itemId, - PositionTicks = positionTicks, - IsMuted = isMuted, - IsPaused = isPaused, - SessionId = session.Id - }; - - if (vals.Length > 4) - { - info.MediaSourceId = vals[4]; - } - - if (vals.Length > 5 && !string.IsNullOrWhiteSpace(vals[5])) - { - info.VolumeLevel = int.Parse(vals[5], _usCulture); - } - - if (vals.Length > 5 && !string.IsNullOrWhiteSpace(vals[6])) - { - info.AudioStreamIndex = int.Parse(vals[6], _usCulture); - } - - if (vals.Length > 7 && !string.IsNullOrWhiteSpace(vals[7])) - { - info.SubtitleStreamIndex = int.Parse(vals[7], _usCulture); - } - - _sessionManager.OnPlaybackProgress(info); - } - } - - private void ReportPlaybackStopped(WebSocketMessageInfo message) - { - _logger.Debug("Received ReportPlaybackStopped message"); - - var session = GetSessionFromMessage(message); - - if (session != null && session.UserId.HasValue) - { - var info = _json.DeserializeFromString<PlaybackStopInfo>(message.Data); - - info.SessionId = session.Id; - - _sessionManager.OnPlaybackStopped(info); - } - } - - /// <summary> - /// Reports the playback stopped. - /// </summary> - /// <param name="message">The message.</param> - private void OnPlaybackStopped(WebSocketMessageInfo message) - { - _logger.Debug("Received PlaybackStopped message"); - - var session = GetSessionFromMessage(message); - - if (session != null && session.UserId.HasValue) - { - var vals = message.Data.Split('|'); - - var itemId = vals[0]; - - long? positionTicks = null; - - if (vals.Length > 1) - { - long pos; - - if (long.TryParse(vals[1], out pos)) - { - positionTicks = pos; - } - } - - var info = new PlaybackStopInfo - { - ItemId = itemId, - PositionTicks = positionTicks, - SessionId = session.Id - }; - - if (vals.Length > 2) - { - info.MediaSourceId = vals[2]; - } - - _sessionManager.OnPlaybackStopped(info); - } + var controller = (WebSocketController)controllerInfo.Item1; + controller.AddWebSocket(connection); } } } diff --git a/Emby.Server.Implementations/Session/WebSocketController.cs b/Emby.Server.Implementations/Session/WebSocketController.cs index b13eb6116..ddac9660f 100644 --- a/Emby.Server.Implementations/Session/WebSocketController.cs +++ b/Emby.Server.Implementations/Session/WebSocketController.cs @@ -10,6 +10,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; +using System.Net.WebSockets; namespace Emby.Server.Implementations.Session { @@ -40,28 +41,14 @@ namespace Emby.Server.Implementations.Session get { return HasOpenSockets; } } - private bool _isActive; - private DateTime _lastActivityDate; public bool IsSessionActive { get { - if (HasOpenSockets) - { - return true; - } - - //return false; - return _isActive && (DateTime.UtcNow - _lastActivityDate).TotalMinutes <= 10; + return HasOpenSockets; } } - public void OnActivity() - { - _isActive = true; - _lastActivityDate = DateTime.UtcNow; - } - private IEnumerable<IWebSocketConnection> GetActiveSockets() { return Sockets @@ -81,209 +68,40 @@ namespace Emby.Server.Implementations.Session void connection_Closed(object sender, EventArgs e) { - if (!GetActiveSockets().Any()) - { - _isActive = false; + var connection = (IWebSocketConnection)sender; + var sockets = Sockets.ToList(); + sockets.Remove(connection); - try - { - _sessionManager.ReportSessionEnded(Session.Id); - } - catch (Exception ex) - { - _logger.ErrorException("Error reporting session ended.", ex); - } - } + Sockets = sockets; + + _sessionManager.CloseIfNeeded(Session); } - private IWebSocketConnection GetActiveSocket() + public Task SendMessage<T>(string name, string messageId, T data, ISessionController[] allControllers, CancellationToken cancellationToken) { var socket = GetActiveSockets() .FirstOrDefault(); if (socket == null) { - throw new InvalidOperationException("The requested session does not have an open web socket."); + return Task.CompletedTask; } - return socket; - } - - public Task SendPlayCommand(PlayRequest command, CancellationToken cancellationToken) - { - return SendMessageInternal(new WebSocketMessage<PlayRequest> - { - MessageType = "Play", - Data = command - - }, cancellationToken); - } - - public Task SendPlaystateCommand(PlaystateRequest command, CancellationToken cancellationToken) - { - return SendMessageInternal(new WebSocketMessage<PlaystateRequest> - { - MessageType = "Playstate", - Data = command - - }, cancellationToken); - } - - public Task SendLibraryUpdateInfo(LibraryUpdateInfo info, CancellationToken cancellationToken) - { - return SendMessagesInternal(new WebSocketMessage<LibraryUpdateInfo> - { - MessageType = "LibraryChanged", - Data = info - - }, cancellationToken); - } - - /// <summary> - /// Sends the restart required message. - /// </summary> - /// <param name="info">The information.</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task.</returns> - public Task SendRestartRequiredNotification(CancellationToken cancellationToken) - { - return SendMessagesInternal(new WebSocketMessage<string> - { - MessageType = "RestartRequired", - Data = string.Empty - - }, cancellationToken); - } - - - /// <summary> - /// Sends the user data change info. - /// </summary> - /// <param name="info">The info.</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task.</returns> - public Task SendUserDataChangeInfo(UserDataChangeInfo info, CancellationToken cancellationToken) - { - return SendMessagesInternal(new WebSocketMessage<UserDataChangeInfo> - { - MessageType = "UserDataChanged", - Data = info - - }, cancellationToken); - } - - /// <summary> - /// Sends the server shutdown notification. - /// </summary> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task.</returns> - public Task SendServerShutdownNotification(CancellationToken cancellationToken) - { - return SendMessagesInternal(new WebSocketMessage<string> - { - MessageType = "ServerShuttingDown", - Data = string.Empty - - }, cancellationToken); - } - - /// <summary> - /// Sends the server restart notification. - /// </summary> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task.</returns> - public Task SendServerRestartNotification(CancellationToken cancellationToken) - { - return SendMessagesInternal(new WebSocketMessage<string> - { - MessageType = "ServerRestarting", - Data = string.Empty - - }, cancellationToken); - } - - public Task SendGeneralCommand(GeneralCommand command, CancellationToken cancellationToken) - { - return SendMessageInternal(new WebSocketMessage<GeneralCommand> - { - MessageType = "GeneralCommand", - Data = command - - }, cancellationToken); - } - - public Task SendSessionEndedNotification(SessionInfoDto sessionInfo, CancellationToken cancellationToken) - { - return SendMessagesInternal(new WebSocketMessage<SessionInfoDto> - { - MessageType = "SessionEnded", - Data = sessionInfo - - }, cancellationToken); - } - - public Task SendPlaybackStartNotification(SessionInfoDto sessionInfo, CancellationToken cancellationToken) - { - return SendMessagesInternal(new WebSocketMessage<SessionInfoDto> - { - MessageType = "PlaybackStart", - Data = sessionInfo - - }, cancellationToken); - } - - public Task SendPlaybackStoppedNotification(SessionInfoDto sessionInfo, CancellationToken cancellationToken) - { - return SendMessagesInternal(new WebSocketMessage<SessionInfoDto> - { - MessageType = "PlaybackStopped", - Data = sessionInfo - - }, cancellationToken); - } - - public Task SendMessage<T>(string name, T data, CancellationToken cancellationToken) - { - return SendMessagesInternal(new WebSocketMessage<T> + return socket.SendAsync(new WebSocketMessage<T> { Data = data, - MessageType = name + MessageType = name, + MessageId = messageId }, cancellationToken); } - private Task SendMessageInternal<T>(WebSocketMessage<T> message, CancellationToken cancellationToken) - { - var socket = GetActiveSocket(); - - return socket.SendAsync(message, cancellationToken); - } - - private Task SendMessagesInternal<T>(WebSocketMessage<T> message, CancellationToken cancellationToken) - { - var tasks = GetActiveSockets().Select(i => Task.Run(async () => - { - try - { - await i.SendAsync(message, cancellationToken).ConfigureAwait(false); - } - catch (Exception ex) - { - _logger.ErrorException("Error sending web socket message", ex); - } - - }, cancellationToken)); - - return Task.WhenAll(tasks); - } - public void Dispose() { foreach (var socket in Sockets.ToList()) { socket.Closed -= connection_Closed; } - GC.SuppressFinalize(this); } } } diff --git a/Emby.Server.Implementations/Social/SharingManager.cs b/Emby.Server.Implementations/Social/SharingManager.cs deleted file mode 100644 index 23ce7492a..000000000 --- a/Emby.Server.Implementations/Social/SharingManager.cs +++ /dev/null @@ -1,101 +0,0 @@ -using MediaBrowser.Common.Extensions; -using MediaBrowser.Controller; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Library; -using MediaBrowser.Model.Social; -using System; -using System.Threading; -using System.Threading.Tasks; - -namespace Emby.Server.Implementations.Social -{ - public class SharingManager : ISharingManager - { - private readonly ISharingRepository _repository; - private readonly IServerConfigurationManager _config; - private readonly ILibraryManager _libraryManager; - private readonly IServerApplicationHost _appHost; - - public SharingManager(ISharingRepository repository, IServerConfigurationManager config, ILibraryManager libraryManager, IServerApplicationHost appHost) - { - _repository = repository; - _config = config; - _libraryManager = libraryManager; - _appHost = appHost; - } - - public async Task<SocialShareInfo> CreateShare(string itemId, string userId) - { - if (string.IsNullOrWhiteSpace(itemId)) - { - throw new ArgumentNullException("itemId"); - } - if (string.IsNullOrWhiteSpace(userId)) - { - throw new ArgumentNullException("userId"); - } - - var item = _libraryManager.GetItemById(itemId); - - if (item == null) - { - throw new ResourceNotFoundException(); - } - - var externalUrl = (await _appHost.GetPublicSystemInfo(CancellationToken.None).ConfigureAwait(false)).WanAddress; - - if (string.IsNullOrWhiteSpace(externalUrl)) - { - throw new InvalidOperationException("No external server address is currently available."); - } - - var info = new SocialShareInfo - { - Id = Guid.NewGuid().ToString("N"), - ExpirationDate = DateTime.UtcNow.AddDays(_config.Configuration.SharingExpirationDays), - ItemId = itemId, - UserId = userId - }; - - AddShareInfo(info, externalUrl); - - _repository.CreateShare(info); - - return info; - } - - private string GetTitle(BaseItem item) - { - return item.Name; - } - - public SocialShareInfo GetShareInfo(string id) - { - var info = _repository.GetShareInfo(id); - - AddShareInfo(info, _appHost.GetPublicSystemInfo(CancellationToken.None).Result.WanAddress); - - return info; - } - - private void AddShareInfo(SocialShareInfo info, string externalUrl) - { - info.ImageUrl = externalUrl + "/Social/Shares/Public/" + info.Id + "/Image"; - info.Url = externalUrl + "/emby/web/shared.html?id=" + info.Id; - - var item = _libraryManager.GetItemById(info.ItemId); - - if (item != null) - { - info.Overview = item.Overview; - info.Name = GetTitle(item); - } - } - - public void DeleteShare(string id) - { - _repository.DeleteShare(id); - } - } -} diff --git a/Emby.Server.Implementations/Social/SharingRepository.cs b/Emby.Server.Implementations/Social/SharingRepository.cs deleted file mode 100644 index 3c9e1024f..000000000 --- a/Emby.Server.Implementations/Social/SharingRepository.cs +++ /dev/null @@ -1,135 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using Emby.Server.Implementations.Data; -using MediaBrowser.Common.Configuration; -using MediaBrowser.Model.Logging; -using MediaBrowser.Model.Social; -using SQLitePCL.pretty; -using MediaBrowser.Model.Extensions; -using MediaBrowser.Model.IO; - -namespace Emby.Server.Implementations.Social -{ - public class SharingRepository : BaseSqliteRepository, ISharingRepository - { - protected IFileSystem FileSystem { get; private set; } - - public SharingRepository(ILogger logger, IApplicationPaths appPaths, IFileSystem fileSystem) - : base(logger) - { - FileSystem = fileSystem; - DbFilePath = Path.Combine(appPaths.DataPath, "shares.db"); - } - - public void Initialize() - { - try - { - InitializeInternal(); - } - catch (Exception ex) - { - Logger.ErrorException("Error loading database file. Will reset and retry.", ex); - - FileSystem.DeleteFile(DbFilePath); - - InitializeInternal(); - } - } - - /// <summary> - /// Opens the connection to the database - /// </summary> - /// <returns>Task.</returns> - private void InitializeInternal() - { - using (var connection = CreateConnection()) - { - RunDefaultInitialization(connection); - - string[] queries = { - - "create table if not exists Shares (Id GUID NOT NULL, ItemId TEXT NOT NULL, UserId TEXT NOT NULL, ExpirationDate DateTime NOT NULL, PRIMARY KEY (Id))", - "create index if not exists idx_Shares on Shares(Id)", - - "pragma shrink_memory" - }; - - connection.RunQueries(queries); - } - } - - public void CreateShare(SocialShareInfo info) - { - if (info == null) - { - throw new ArgumentNullException("info"); - } - if (string.IsNullOrWhiteSpace(info.Id)) - { - throw new ArgumentNullException("info.Id"); - } - - using (WriteLock.Write()) - { - using (var connection = CreateConnection()) - { - connection.RunInTransaction(db => - { - var commandText = "replace into Shares (Id, ItemId, UserId, ExpirationDate) values (?, ?, ?, ?)"; - - db.Execute(commandText, - info.Id.ToGuidBlob(), - info.ItemId, - info.UserId, - info.ExpirationDate.ToDateTimeParamValue()); - }, TransactionMode); - } - } - } - - public SocialShareInfo GetShareInfo(string id) - { - if (string.IsNullOrWhiteSpace(id)) - { - throw new ArgumentNullException("id"); - } - - using (WriteLock.Read()) - { - using (var connection = CreateConnection(true)) - { - var commandText = "select Id, ItemId, UserId, ExpirationDate from Shares where id = ?"; - - var paramList = new List<object>(); - paramList.Add(id.ToGuidBlob()); - - foreach (var row in connection.Query(commandText, paramList.ToArray(paramList.Count))) - { - return GetSocialShareInfo(row); - } - } - } - - return null; - } - - private SocialShareInfo GetSocialShareInfo(IReadOnlyList<IResultSetValue> reader) - { - var info = new SocialShareInfo(); - - info.Id = reader[0].ReadGuidFromBlob().ToString("N"); - info.ItemId = reader[1].ToString(); - info.UserId = reader[2].ToString(); - info.ExpirationDate = reader[3].ReadDateTime(); - - return info; - } - - public void DeleteShare(string id) - { - - } - } -} diff --git a/Emby.Server.Implementations/SystemEvents.cs b/Emby.Server.Implementations/SystemEvents.cs index dfff92f1e..c1e6dc1da 100644 --- a/Emby.Server.Implementations/SystemEvents.cs +++ b/Emby.Server.Implementations/SystemEvents.cs @@ -17,34 +17,6 @@ namespace Emby.Server.Implementations public SystemEvents(ILogger logger) { _logger = logger; - Microsoft.Win32.SystemEvents.PowerModeChanged += SystemEvents_PowerModeChanged; - Microsoft.Win32.SystemEvents.SessionEnding += SystemEvents_SessionEnding; - } - - private void SystemEvents_SessionEnding(object sender, Microsoft.Win32.SessionEndingEventArgs e) - { - switch (e.Reason) - { - case Microsoft.Win32.SessionEndReasons.Logoff: - EventHelper.FireEventIfNotNull(SessionLogoff, this, EventArgs.Empty, _logger); - break; - case Microsoft.Win32.SessionEndReasons.SystemShutdown: - EventHelper.FireEventIfNotNull(SystemShutdown, this, EventArgs.Empty, _logger); - break; - } - } - - private void SystemEvents_PowerModeChanged(object sender, Microsoft.Win32.PowerModeChangedEventArgs e) - { - switch (e.Mode) - { - case Microsoft.Win32.PowerModes.Resume: - EventHelper.FireEventIfNotNull(Resume, this, EventArgs.Empty, _logger); - break; - case Microsoft.Win32.PowerModes.Suspend: - EventHelper.FireEventIfNotNull(Suspend, this, EventArgs.Empty, _logger); - break; - } } } } diff --git a/Emby.Server.Implementations/TV/SeriesPostScanTask.cs b/Emby.Server.Implementations/TV/SeriesPostScanTask.cs deleted file mode 100644 index 764df8baf..000000000 --- a/Emby.Server.Implementations/TV/SeriesPostScanTask.cs +++ /dev/null @@ -1,241 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Dto; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Entities.TV; -using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Plugins; -using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Globalization; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.Logging; -using MediaBrowser.Model.Tasks; -using MediaBrowser.Model.Threading; -using MediaBrowser.Model.Xml; -using MediaBrowser.Providers.TV; - -namespace Emby.Server.Implementations.TV -{ - class SeriesGroup : List<Series>, IGrouping<string, Series> - { - public string Key { get; set; } - } - - class SeriesPostScanTask : ILibraryPostScanTask, IHasOrder - { - /// <summary> - /// The _library manager - /// </summary> - private readonly ILibraryManager _libraryManager; - private readonly IServerConfigurationManager _config; - private readonly ILogger _logger; - private readonly ILocalizationManager _localization; - private readonly IFileSystem _fileSystem; - private readonly IXmlReaderSettingsFactory _xmlSettings; - - public SeriesPostScanTask(ILibraryManager libraryManager, ILogger logger, IServerConfigurationManager config, ILocalizationManager localization, IFileSystem fileSystem, IXmlReaderSettingsFactory xmlSettings) - { - _libraryManager = libraryManager; - _logger = logger; - _config = config; - _localization = localization; - _fileSystem = fileSystem; - _xmlSettings = xmlSettings; - } - - public Task Run(IProgress<double> progress, CancellationToken cancellationToken) - { - return RunInternal(progress, cancellationToken); - } - - private Task RunInternal(IProgress<double> progress, CancellationToken cancellationToken) - { - var seriesList = _libraryManager.GetItemList(new InternalItemsQuery() - { - IncludeItemTypes = new[] { typeof(Series).Name }, - Recursive = true, - GroupByPresentationUniqueKey = false, - DtoOptions = new DtoOptions(true) - - }).Cast<Series>().ToList(); - - var seriesGroups = FindSeriesGroups(seriesList).Where(g => !string.IsNullOrEmpty(g.Key)).ToList(); - - return new MissingEpisodeProvider(_logger, _config, _libraryManager, _localization, _fileSystem, _xmlSettings).Run(seriesGroups, true, cancellationToken); - } - - internal static IEnumerable<IGrouping<string, Series>> FindSeriesGroups(List<Series> seriesList) - { - var links = seriesList.ToDictionary(s => s, s => seriesList.Where(c => c != s && ShareProviderId(s, c)).ToList()); - - var visited = new HashSet<Series>(); - - foreach (var series in seriesList) - { - if (!visited.Contains(series)) - { - var group = new SeriesGroup(); - FindAllLinked(series, visited, links, group); - - group.Key = group.Select(s => s.PresentationUniqueKey).FirstOrDefault(id => !string.IsNullOrEmpty(id)); - - yield return group; - } - } - } - - private static void FindAllLinked(Series series, HashSet<Series> visited, IDictionary<Series, List<Series>> linksMap, List<Series> results) - { - results.Add(series); - visited.Add(series); - - var links = linksMap[series]; - - foreach (var s in links) - { - if (!visited.Contains(s)) - { - FindAllLinked(s, visited, linksMap, results); - } - } - } - - private static bool ShareProviderId(Series a, Series b) - { - return string.Equals(a.PresentationUniqueKey, b.PresentationUniqueKey, StringComparison.Ordinal); - } - - public int Order - { - get - { - // Run after tvdb update task - return 1; - } - } - } - - public class CleanMissingEpisodesEntryPoint : IServerEntryPoint - { - private readonly ILibraryManager _libraryManager; - private readonly IServerConfigurationManager _config; - private readonly ILogger _logger; - private readonly ILocalizationManager _localization; - private readonly IFileSystem _fileSystem; - private readonly object _libraryChangedSyncLock = new object(); - private const int LibraryUpdateDuration = 180000; - private readonly ITaskManager _taskManager; - private readonly IXmlReaderSettingsFactory _xmlSettings; - private readonly ITimerFactory _timerFactory; - - public CleanMissingEpisodesEntryPoint(ILibraryManager libraryManager, IServerConfigurationManager config, ILogger logger, ILocalizationManager localization, IFileSystem fileSystem, ITaskManager taskManager, IXmlReaderSettingsFactory xmlSettings, ITimerFactory timerFactory) - { - _libraryManager = libraryManager; - _config = config; - _logger = logger; - _localization = localization; - _fileSystem = fileSystem; - _taskManager = taskManager; - _xmlSettings = xmlSettings; - _timerFactory = timerFactory; - } - - private ITimer LibraryUpdateTimer { get; set; } - - public void Run() - { - _libraryManager.ItemAdded += _libraryManager_ItemAdded; - } - - private void _libraryManager_ItemAdded(object sender, ItemChangeEventArgs e) - { - if (!FilterItem(e.Item)) - { - return; - } - - lock (_libraryChangedSyncLock) - { - if (LibraryUpdateTimer == null) - { - LibraryUpdateTimer = _timerFactory.Create(LibraryUpdateTimerCallback, null, LibraryUpdateDuration, Timeout.Infinite); - } - else - { - LibraryUpdateTimer.Change(LibraryUpdateDuration, Timeout.Infinite); - } - } - } - - private async void LibraryUpdateTimerCallback(object state) - { - try - { - if (MissingEpisodeProvider.IsRunning) - { - return; - } - - if (_libraryManager.IsScanRunning) - { - return; - } - - var seriesList = _libraryManager.GetItemList(new InternalItemsQuery() - { - IncludeItemTypes = new[] { typeof(Series).Name }, - Recursive = true, - GroupByPresentationUniqueKey = false, - DtoOptions = new DtoOptions(true) - - }).Cast<Series>().ToList(); - - var seriesGroups = SeriesPostScanTask.FindSeriesGroups(seriesList).Where(g => !string.IsNullOrEmpty(g.Key)).ToList(); - - await new MissingEpisodeProvider(_logger, _config, _libraryManager, _localization, _fileSystem, _xmlSettings) - .Run(seriesGroups, false, CancellationToken.None).ConfigureAwait(false); - } - catch (Exception ex) - { - _logger.ErrorException("Error in SeriesPostScanTask", ex); - } - } - - private bool FilterItem(BaseItem item) - { - return item is Episode && item.LocationType != LocationType.Virtual; - } - - /// <summary> - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. - /// </summary> - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - /// <summary> - /// Releases unmanaged and - optionally - managed resources. - /// </summary> - /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param> - protected virtual void Dispose(bool dispose) - { - if (dispose) - { - if (LibraryUpdateTimer != null) - { - LibraryUpdateTimer.Dispose(); - LibraryUpdateTimer = null; - } - - _libraryManager.ItemAdded -= _libraryManager_ItemAdded; - } - } - } -} diff --git a/Emby.Server.Implementations/TV/TVSeriesManager.cs b/Emby.Server.Implementations/TV/TVSeriesManager.cs index d92245a67..1f9cb9164 100644 --- a/Emby.Server.Implementations/TV/TVSeriesManager.cs +++ b/Emby.Server.Implementations/TV/TVSeriesManager.cs @@ -37,48 +37,50 @@ namespace Emby.Server.Implementations.TV } string presentationUniqueKey = null; - int? limit = null; - if (!string.IsNullOrWhiteSpace(request.SeriesId)) + if (!string.IsNullOrEmpty(request.SeriesId)) { var series = _libraryManager.GetItemById(request.SeriesId) as Series; if (series != null) { presentationUniqueKey = GetUniqueSeriesKey(series); - limit = 1; } } - if (!string.IsNullOrWhiteSpace(presentationUniqueKey)) + if (!string.IsNullOrEmpty(presentationUniqueKey)) { return GetResult(GetNextUpEpisodes(request, user, new[] { presentationUniqueKey }, dtoOptions), request); } - var parentIdGuid = string.IsNullOrWhiteSpace(request.ParentId) ? (Guid?)null : new Guid(request.ParentId); + var parentIdGuid = string.IsNullOrEmpty(request.ParentId) ? (Guid?)null : new Guid(request.ParentId); - List<BaseItem> parents; + BaseItem[] parents; if (parentIdGuid.HasValue) { var parent = _libraryManager.GetItemById(parentIdGuid.Value); - parents = new List<BaseItem>(); + if (parent != null) { - parents.Add(parent); + parents = new[] { parent }; + } + else + { + parents = Array.Empty<BaseItem>(); } } else { - parents = user.RootFolder.GetChildren(user, true) + parents = _libraryManager.GetUserRootFolder().GetChildren(user, true) .Where(i => i is Folder) .Where(i => !user.Configuration.LatestItemsExcludes.Contains(i.Id.ToString("N"))) - .ToList(); + .ToArray(); } return GetNextUp(request, parents, dtoOptions); } - public QueryResult<BaseItem> GetNextUp(NextUpQuery request, List<BaseItem> parentsFolders, DtoOptions dtoOptions) + public QueryResult<BaseItem> GetNextUp(NextUpQuery request, BaseItem[] parentsFolders, DtoOptions dtoOptions) { var user = _userManager.GetUserById(request.UserId); @@ -89,7 +91,7 @@ namespace Emby.Server.Implementations.TV string presentationUniqueKey = null; int? limit = null; - if (!string.IsNullOrWhiteSpace(request.SeriesId)) + if (!string.IsNullOrEmpty(request.SeriesId)) { var series = _libraryManager.GetItemById(request.SeriesId) as Series; @@ -100,7 +102,7 @@ namespace Emby.Server.Implementations.TV } } - if (!string.IsNullOrWhiteSpace(presentationUniqueKey)) + if (!string.IsNullOrEmpty(presentationUniqueKey)) { return GetResult(GetNextUpEpisodes(request, user, new[] { presentationUniqueKey }, dtoOptions), request); } @@ -113,7 +115,7 @@ namespace Emby.Server.Implementations.TV var items = _libraryManager.GetItemList(new InternalItemsQuery(user) { IncludeItemTypes = new[] { typeof(Episode).Name }, - OrderBy = new[] { new Tuple<string, SortOrder>(ItemSortBy.DatePlayed, SortOrder.Descending) }, + OrderBy = new[] { new ValueTuple<string, SortOrder>(ItemSortBy.DatePlayed, SortOrder.Descending) }, SeriesPresentationUniqueKey = presentationUniqueKey, Limit = limit, DtoOptions = new MediaBrowser.Controller.Dto.DtoOptions @@ -126,7 +128,7 @@ namespace Emby.Server.Implementations.TV }, GroupBySeriesPresentationUniqueKey = true - }, parentsFolders).Cast<Episode>().Select(GetUniqueSeriesKey); + }, parentsFolders.ToList()).Cast<Episode>().Select(GetUniqueSeriesKey); // Avoid implicitly captured closure var episodes = GetNextUpEpisodes(request, user, items, dtoOptions); @@ -146,7 +148,7 @@ namespace Emby.Server.Implementations.TV // If viewing all next up for all series, remove first episodes // But if that returns empty, keep those first episodes (avoid completely empty view) - var alwaysEnableFirstEpisode = !string.IsNullOrWhiteSpace(request.SeriesId); + var alwaysEnableFirstEpisode = !string.IsNullOrEmpty(request.SeriesId); var anyFound = false; return allNextUp @@ -190,7 +192,7 @@ namespace Emby.Server.Implementations.TV AncestorWithPresentationUniqueKey = null, SeriesPresentationUniqueKey = seriesKey, IncludeItemTypes = new[] { typeof(Episode).Name }, - OrderBy = new[] { new Tuple<string, SortOrder>(ItemSortBy.SortName, SortOrder.Descending) }, + OrderBy = new[] { new ValueTuple<string, SortOrder>(ItemSortBy.SortName, SortOrder.Descending) }, IsPlayed = true, Limit = 1, ParentIndexNumberNotEquals = 0, @@ -212,7 +214,7 @@ namespace Emby.Server.Implementations.TV AncestorWithPresentationUniqueKey = null, SeriesPresentationUniqueKey = seriesKey, IncludeItemTypes = new[] { typeof(Episode).Name }, - OrderBy = new[] { new Tuple<string, SortOrder>(ItemSortBy.SortName, SortOrder.Ascending) }, + OrderBy = new[] { new ValueTuple<string, SortOrder>(ItemSortBy.SortName, SortOrder.Ascending) }, Limit = 1, IsPlayed = false, IsVirtualItem = false, diff --git a/Emby.Server.Implementations/TextEncoding/TextEncoding.cs b/Emby.Server.Implementations/TextEncoding/TextEncoding.cs index 9eb9be7ea..ea87a9539 100644 --- a/Emby.Server.Implementations/TextEncoding/TextEncoding.cs +++ b/Emby.Server.Implementations/TextEncoding/TextEncoding.cs @@ -193,6 +193,8 @@ namespace Emby.Server.Implementations.TextEncoding switch (language.ToLower()) { + case "tha": + return "windows-874"; case "hun": return "windows-1252"; case "pol": @@ -203,6 +205,7 @@ namespace Emby.Server.Implementations.TextEncoding case "hrv": case "rum": case "ron": + case "rom": case "rup": return "windows-1250"; // albanian diff --git a/Emby.Server.Implementations/Threading/CommonTimer.cs b/Emby.Server.Implementations/Threading/CommonTimer.cs index bb67325d1..9451b07f3 100644 --- a/Emby.Server.Implementations/Threading/CommonTimer.cs +++ b/Emby.Server.Implementations/Threading/CommonTimer.cs @@ -31,7 +31,6 @@ namespace Emby.Server.Implementations.Threading public void Dispose() { _timer.Dispose(); - GC.SuppressFinalize(this); } } } diff --git a/Emby.Server.Implementations/Udp/UdpServer.cs b/Emby.Server.Implementations/Udp/UdpServer.cs index 28de80da1..f195ca710 100644 --- a/Emby.Server.Implementations/Udp/UdpServer.cs +++ b/Emby.Server.Implementations/Udp/UdpServer.cs @@ -234,7 +234,6 @@ namespace Emby.Server.Implementations.Udp public void Dispose() { Dispose(true); - GC.SuppressFinalize(this); } /// <summary> diff --git a/Emby.Server.Implementations/Updates/InstallationManager.cs b/Emby.Server.Implementations/Updates/InstallationManager.cs index 51acfee88..435bcfd04 100644 --- a/Emby.Server.Implementations/Updates/InstallationManager.cs +++ b/Emby.Server.Implementations/Updates/InstallationManager.cs @@ -19,6 +19,7 @@ using MediaBrowser.Model.IO; using MediaBrowser.Model.Logging; using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Updates; +using MediaBrowser.Controller.Configuration; namespace Emby.Server.Implementations.Updates { @@ -111,7 +112,7 @@ namespace Emby.Server.Implementations.Updates private readonly IHttpClient _httpClient; private readonly IJsonSerializer _jsonSerializer; private readonly ISecurityManager _securityManager; - private readonly IConfigurationManager _config; + private readonly IServerConfigurationManager _config; private readonly IFileSystem _fileSystem; /// <summary> @@ -125,7 +126,7 @@ namespace Emby.Server.Implementations.Updates // netframework or netcore private readonly string _packageRuntime; - public InstallationManager(ILogger logger, IApplicationHost appHost, IApplicationPaths appPaths, IHttpClient httpClient, IJsonSerializer jsonSerializer, ISecurityManager securityManager, IConfigurationManager config, IFileSystem fileSystem, ICryptoProvider cryptographyProvider, string packageRuntime) + public InstallationManager(ILogger logger, IApplicationHost appHost, IApplicationPaths appPaths, IHttpClient httpClient, IJsonSerializer jsonSerializer, ISecurityManager securityManager, IServerConfigurationManager config, IFileSystem fileSystem, ICryptoProvider cryptographyProvider, string packageRuntime) { if (logger == null) { @@ -189,7 +190,7 @@ namespace Emby.Server.Implementations.Updates { cancellationToken.ThrowIfCancellationRequested(); - var packages = _jsonSerializer.DeserializeFromStream<PackageInfo[]>(json); + var packages = await _jsonSerializer.DeserializeFromStreamAsync<PackageInfo[]>(json).ConfigureAwait(false); return FilterPackages(packages, packageType, applicationVersion); } @@ -203,8 +204,6 @@ namespace Emby.Server.Implementations.Updates } } - private DateTime _lastPackageUpdateTime; - /// <summary> /// Gets all available packages. /// </summary> @@ -212,77 +211,21 @@ namespace Emby.Server.Implementations.Updates /// <returns>Task{List{PackageInfo}}.</returns> public async Task<List<PackageInfo>> GetAvailablePackagesWithoutRegistrationInfo(CancellationToken cancellationToken) { - _logger.Info("Opening {0}", PackageCachePath); - try - { - using (var stream = _fileSystem.OpenRead(PackageCachePath)) - { - var packages = _jsonSerializer.DeserializeFromStream<PackageInfo[]>(stream); - - if (DateTime.UtcNow - _lastPackageUpdateTime > GetCacheLength()) - { - UpdateCachedPackages(CancellationToken.None, false); - } - - return FilterPackages(packages); - } - } - catch (Exception) - { - - } - - _lastPackageUpdateTime = DateTime.MinValue; - await UpdateCachedPackages(cancellationToken, true).ConfigureAwait(false); - using (var stream = _fileSystem.OpenRead(PackageCachePath)) - { - return FilterPackages(_jsonSerializer.DeserializeFromStream<PackageInfo[]>(stream)); - } - } - - private string PackageCachePath - { - get { return Path.Combine(_appPaths.CachePath, "serverpackages.json"); } - } - - private readonly SemaphoreSlim _updateSemaphore = new SemaphoreSlim(1, 1); - private async Task UpdateCachedPackages(CancellationToken cancellationToken, bool throwErrors) - { - await _updateSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false); - - try + using (var response = await _httpClient.SendAsync(new HttpRequestOptions { - if (DateTime.UtcNow - _lastPackageUpdateTime < GetCacheLength()) - { - return; - } - - var tempFile = await _httpClient.GetTempFile(new HttpRequestOptions - { - Url = "https://www.mb3admin.com/admin/service/EmbyPackages.json", - CancellationToken = cancellationToken, - Progress = new SimpleProgress<Double>() - - }).ConfigureAwait(false); - - _fileSystem.CreateDirectory(_fileSystem.GetDirectoryName(PackageCachePath)); + Url = "https://www.mb3admin.com/admin/service/EmbyPackages.json", + CancellationToken = cancellationToken, + Progress = new SimpleProgress<Double>(), + CacheLength = GetCacheLength(), + CacheMode = CacheMode.Unconditional - _fileSystem.CopyFile(tempFile, PackageCachePath, true); - _lastPackageUpdateTime = DateTime.UtcNow; - } - catch (Exception ex) + }, "GET").ConfigureAwait(false)) { - _logger.ErrorException("Error updating package cache", ex); - - if (throwErrors) + using (var stream = response.Content) { - throw; + return FilterPackages(await _jsonSerializer.DeserializeFromStreamAsync<PackageInfo[]>(stream).ConfigureAwait(false)); } } - finally - { - _updateSemaphore.Release(); - } } private PackageVersionClass GetSystemUpdateLevel() @@ -304,12 +247,12 @@ namespace Emby.Server.Implementations.Updates var versions = new List<PackageVersionInfo>(); foreach (var version in package.versions) { - if (string.IsNullOrWhiteSpace(version.sourceUrl)) + if (string.IsNullOrEmpty(version.sourceUrl)) { continue; } - if (string.IsNullOrWhiteSpace(version.runtimes) || version.runtimes.IndexOf(_packageRuntime, StringComparison.OrdinalIgnoreCase) == -1) + if (string.IsNullOrEmpty(version.runtimes) || version.runtimes.IndexOf(_packageRuntime, StringComparison.OrdinalIgnoreCase) == -1) { continue; } @@ -339,7 +282,7 @@ namespace Emby.Server.Implementations.Updates var returnList = new List<PackageInfo>(); - var filterOnPackageType = !string.IsNullOrWhiteSpace(packageType); + var filterOnPackageType = !string.IsNullOrEmpty(packageType); foreach (var p in packagesList) { @@ -465,7 +408,7 @@ namespace Emby.Server.Implementations.Updates return latestPluginInfo != null && GetPackageVersion(latestPluginInfo) > p.Version ? latestPluginInfo : null; }).Where(i => i != null) - .Where(p => !string.IsNullOrWhiteSpace(p.sourceUrl) && !CompletedInstallations.Any(i => string.Equals(i.AssemblyGuid, p.guid, StringComparison.OrdinalIgnoreCase))); + .Where(p => !string.IsNullOrEmpty(p.sourceUrl) && !CompletedInstallations.Any(i => string.Equals(i.AssemblyGuid, p.guid, StringComparison.OrdinalIgnoreCase))); } /// <summary> @@ -491,7 +434,7 @@ namespace Emby.Server.Implementations.Updates var installationInfo = new InstallationInfo { - Id = Guid.NewGuid().ToString("N"), + Id = Guid.NewGuid(), Name = package.name, AssemblyGuid = package.guid, UpdateClass = package.classification, @@ -575,7 +518,6 @@ namespace Emby.Server.Implementations.Updates finally { // Dispose the progress object and remove the installation from the in-progress list - innerProgress.Dispose(); tuple.Item2.Dispose(); } } @@ -590,16 +532,23 @@ namespace Emby.Server.Implementations.Updates /// <returns>Task.</returns> private async Task InstallPackageInternal(PackageVersionInfo package, bool isPlugin, IProgress<double> progress, CancellationToken cancellationToken) { - // Do the install - await PerformPackageInstallation(progress, package, cancellationToken).ConfigureAwait(false); + IPlugin plugin = null; - // Do plugin-specific processing if (isPlugin) { // Set last update time if we were installed before - var plugin = _applicationHost.Plugins.FirstOrDefault(p => string.Equals(p.Id.ToString(), package.guid, StringComparison.OrdinalIgnoreCase)) - ?? _applicationHost.Plugins.FirstOrDefault(p => p.Name.Equals(package.name, StringComparison.OrdinalIgnoreCase)); + plugin = _applicationHost.Plugins.FirstOrDefault(p => string.Equals(p.Id.ToString(), package.guid, StringComparison.OrdinalIgnoreCase)) + ?? _applicationHost.Plugins.FirstOrDefault(p => p.Name.Equals(package.name, StringComparison.OrdinalIgnoreCase)); + } + + string targetPath = plugin == null ? null : plugin.AssemblyFilePath; + + // Do the install + await PerformPackageInstallation(progress, targetPath, package, cancellationToken).ConfigureAwait(false); + // Do plugin-specific processing + if (isPlugin) + { if (plugin != null) { OnPluginUpdated(plugin, package); @@ -611,13 +560,17 @@ namespace Emby.Server.Implementations.Updates } } - private async Task PerformPackageInstallation(IProgress<double> progress, PackageVersionInfo package, CancellationToken cancellationToken) + private async Task PerformPackageInstallation(IProgress<double> progress, string target, PackageVersionInfo package, CancellationToken cancellationToken) { // Target based on if it is an archive or single assembly // zip archives are assumed to contain directory structures relative to our ProgramDataPath var extension = Path.GetExtension(package.targetFilename); var isArchive = string.Equals(extension, ".zip", StringComparison.OrdinalIgnoreCase) || string.Equals(extension, ".rar", StringComparison.OrdinalIgnoreCase) || string.Equals(extension, ".7z", StringComparison.OrdinalIgnoreCase); - var target = Path.Combine(isArchive ? _appPaths.TempUpdatePath : _appPaths.PluginsPath, package.targetFilename); + + if (target == null) + { + target = Path.Combine(isArchive ? _appPaths.TempUpdatePath : _appPaths.PluginsPath, package.targetFilename); + } // Download to temporary file so that, if interrupted, it won't destroy the existing installation var tempFile = await _httpClient.GetTempFile(new HttpRequestOptions @@ -632,7 +585,7 @@ namespace Emby.Server.Implementations.Updates // Validate with a checksum var packageChecksum = string.IsNullOrWhiteSpace(package.checksum) ? Guid.Empty : new Guid(package.checksum); - if (packageChecksum != Guid.Empty) // support for legacy uploads for now + if (!packageChecksum.Equals(Guid.Empty)) // support for legacy uploads for now { using (var stream = _fileSystem.OpenRead(tempFile)) { @@ -700,6 +653,15 @@ namespace Emby.Server.Implementations.Updates _fileSystem.DeleteFile(path); + var list = _config.Configuration.UninstalledPlugins.ToList(); + var filename = Path.GetFileName(path); + if (!list.Contains(filename, StringComparer.OrdinalIgnoreCase)) + { + list.Add(filename); + _config.Configuration.UninstalledPlugins = list.ToArray(); + _config.SaveConfiguration(); + } + OnPluginUninstalled(plugin); _applicationHost.NotifyPendingRestart(); @@ -728,7 +690,6 @@ namespace Emby.Server.Implementations.Updates public void Dispose() { Dispose(true); - GC.SuppressFinalize(this); } } } diff --git a/Emby.Server.Implementations/UserViews/CollectionFolderImageProvider.cs b/Emby.Server.Implementations/UserViews/CollectionFolderImageProvider.cs index f051e856a..2543fd372 100644 --- a/Emby.Server.Implementations/UserViews/CollectionFolderImageProvider.cs +++ b/Emby.Server.Implementations/UserViews/CollectionFolderImageProvider.cs @@ -28,120 +28,70 @@ namespace Emby.Server.Implementations.UserViews { } - protected override List<BaseItem> GetItemsWithImages(IHasMetadata item) + protected override List<BaseItem> GetItemsWithImages(BaseItem item) { var view = (CollectionFolder)item; + var viewType = view.CollectionType; - var recursive = !new[] { CollectionType.Playlists, CollectionType.Channels }.Contains(view.CollectionType ?? string.Empty, StringComparer.OrdinalIgnoreCase); + string[] includeItemTypes; - var result = view.GetItemList(new InternalItemsQuery + if (string.Equals(viewType, CollectionType.Movies)) { - CollapseBoxSetItems = false, - Recursive = recursive, - ExcludeItemTypes = new[] { "UserView", "CollectionFolder", "Playlist" }, - DtoOptions = new DtoOptions(false) - - }); - - var items = result.Select(i => + includeItemTypes = new string[] { "Movie" }; + } + else if (string.Equals(viewType, CollectionType.TvShows)) { - var episode = i as Episode; - if (episode != null) - { - var series = episode.Series; - if (series != null) - { - return series; - } - - return episode; - } - - var season = i as Season; - if (season != null) - { - var series = season.Series; - if (series != null) - { - return series; - } - - return season; - } - - var audio = i as Audio; - if (audio != null) - { - var album = audio.AlbumEntity; - if (album != null && album.HasImage(ImageType.Primary)) - { - return album; - } - } - - return i; - - }).DistinctBy(i => i.Id); - - return GetFinalItems(items.Where(i => i.HasImage(ImageType.Primary) || i.HasImage(ImageType.Thumb)), 8); - } - - protected override bool Supports(IHasMetadata item) - { - return item is CollectionFolder; - } - - protected override string CreateImage(IHasMetadata item, List<BaseItem> itemsWithImages, string outputPathWithoutExtension, ImageType imageType, int imageIndex) - { - var outputPath = Path.ChangeExtension(outputPathWithoutExtension, ".png"); - - if (imageType == ImageType.Primary) + includeItemTypes = new string[] { "Series" }; + } + else if (string.Equals(viewType, CollectionType.Music)) { - if (itemsWithImages.Count == 0) - { - return null; - } - - return CreateThumbCollage(item, itemsWithImages, outputPath, 960, 540); + includeItemTypes = new string[] { "MusicAlbum" }; + } + else if (string.Equals(viewType, CollectionType.Books)) + { + includeItemTypes = new string[] { "Book", "AudioBook" }; + } + else if (string.Equals(viewType, CollectionType.Games)) + { + includeItemTypes = new string[] { "Game" }; + } + else if (string.Equals(viewType, CollectionType.BoxSets)) + { + includeItemTypes = new string[] { "BoxSet" }; + } + else if (string.Equals(viewType, CollectionType.HomeVideos) || string.Equals(viewType, CollectionType.Photos)) + { + includeItemTypes = new string[] { "Video", "Photo" }; + } + else + { + includeItemTypes = new string[] { "Video", "Audio", "Photo", "Movie", "Series" }; } - return base.CreateImage(item, itemsWithImages, outputPath, imageType, imageIndex); - } - } - - public class ManualCollectionFolderImageProvider : BaseDynamicImageProvider<ManualCollectionsFolder> - { - private readonly ILibraryManager _libraryManager; - - public ManualCollectionFolderImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths, IImageProcessor imageProcessor, ILibraryManager libraryManager) : base(fileSystem, providerManager, applicationPaths, imageProcessor) - { - _libraryManager = libraryManager; - } - - protected override List<BaseItem> GetItemsWithImages(IHasMetadata item) - { - var view = (ManualCollectionsFolder)item; - - var recursive = !new[] { CollectionType.Playlists, CollectionType.Channels }.Contains(view.CollectionType ?? string.Empty, StringComparer.OrdinalIgnoreCase); + var recursive = !new[] { CollectionType.Playlists }.Contains(view.CollectionType ?? string.Empty, StringComparer.OrdinalIgnoreCase); - var items = _libraryManager.GetItemList(new InternalItemsQuery + return view.GetItemList(new InternalItemsQuery { + CollapseBoxSetItems = false, Recursive = recursive, - IncludeItemTypes = new[] { typeof(BoxSet).Name }, - Limit = 20, - OrderBy = new [] { new Tuple<string, SortOrder>(ItemSortBy.Random, SortOrder.Ascending) }, - DtoOptions = new DtoOptions(false) - }); + DtoOptions = new DtoOptions(false), + ImageTypes = new ImageType[] { ImageType.Primary }, + Limit = 8, + OrderBy = new ValueTuple<string, SortOrder>[] + { + new ValueTuple<string, SortOrder>(ItemSortBy.Random, SortOrder.Ascending) + }, + IncludeItemTypes = includeItemTypes - return GetFinalItems(items.Where(i => i.HasImage(ImageType.Primary) || i.HasImage(ImageType.Thumb)), 8); + }).ToList(); } - protected override bool Supports(IHasMetadata item) + protected override bool Supports(BaseItem item) { - return item is ManualCollectionsFolder; + return item is CollectionFolder; } - protected override string CreateImage(IHasMetadata item, List<BaseItem> itemsWithImages, string outputPathWithoutExtension, ImageType imageType, int imageIndex) + protected override string CreateImage(BaseItem item, List<BaseItem> itemsWithImages, string outputPathWithoutExtension, ImageType imageType, int imageIndex) { var outputPath = Path.ChangeExtension(outputPathWithoutExtension, ".png"); @@ -158,5 +108,4 @@ namespace Emby.Server.Implementations.UserViews return base.CreateImage(item, itemsWithImages, outputPath, imageType, imageIndex); } } - } diff --git a/Emby.Server.Implementations/UserViews/DynamicImageProvider.cs b/Emby.Server.Implementations/UserViews/DynamicImageProvider.cs index 23b8c9b9e..c75033261 100644 --- a/Emby.Server.Implementations/UserViews/DynamicImageProvider.cs +++ b/Emby.Server.Implementations/UserViews/DynamicImageProvider.cs @@ -31,39 +31,12 @@ namespace Emby.Server.Implementations.UserViews _libraryManager = libraryManager; } - protected override List<BaseItem> GetItemsWithImages(IHasMetadata item) + protected override List<BaseItem> GetItemsWithImages(BaseItem item) { var view = (UserView)item; - if (string.Equals(view.ViewType, CollectionType.LiveTv, StringComparison.OrdinalIgnoreCase)) - { - var programs = _libraryManager.GetItemList(new InternalItemsQuery - { - IncludeItemTypes = new[] { typeof(LiveTvProgram).Name }, - ImageTypes = new[] { ImageType.Primary }, - Limit = 30, - IsMovie = true, - DtoOptions = new DtoOptions(false) - - }); - - return GetFinalItems(programs); - } - - if (string.Equals(view.ViewType, SpecialFolder.MovieGenre, StringComparison.OrdinalIgnoreCase) || - string.Equals(view.ViewType, SpecialFolder.TvGenre, StringComparison.OrdinalIgnoreCase)) - { - var userItemsResult = view.GetItemList(new InternalItemsQuery - { - CollapseBoxSetItems = false, - DtoOptions = new DtoOptions(false) - }); - - return userItemsResult.ToList(); - } - var isUsingCollectionStrip = IsUsingCollectionStrip(view); - var recursive = isUsingCollectionStrip && !new[] { CollectionType.Channels, CollectionType.BoxSets, CollectionType.Playlists }.Contains(view.ViewType ?? string.Empty, StringComparer.OrdinalIgnoreCase); + var recursive = isUsingCollectionStrip && !new[] { CollectionType.BoxSets, CollectionType.Playlists }.Contains(view.ViewType ?? string.Empty, StringComparer.OrdinalIgnoreCase); var result = view.GetItemList(new InternalItemsQuery { @@ -116,13 +89,19 @@ namespace Emby.Server.Implementations.UserViews if (isUsingCollectionStrip) { - return GetFinalItems(items.Where(i => i.HasImage(ImageType.Primary) || i.HasImage(ImageType.Thumb)), 8); + return items + .Where(i => i.HasImage(ImageType.Primary) || i.HasImage(ImageType.Thumb)) + .OrderBy(i => Guid.NewGuid()) + .ToList(); } - return GetFinalItems(items.Where(i => i.HasImage(ImageType.Primary))); + return items + .Where(i => i.HasImage(ImageType.Primary)) + .OrderBy(i => Guid.NewGuid()) + .ToList(); } - protected override bool Supports(IHasMetadata item) + protected override bool Supports(BaseItem item) { var view = item as UserView; if (view != null) @@ -139,14 +118,13 @@ namespace Emby.Server.Implementations.UserViews { CollectionType.Movies, CollectionType.TvShows, - CollectionType.Playlists, - CollectionType.Photos + CollectionType.Playlists }; return collectionStripViewTypes.Contains(view.ViewType ?? string.Empty); } - protected override string CreateImage(IHasMetadata item, List<BaseItem> itemsWithImages, string outputPathWithoutExtension, ImageType imageType, int imageIndex) + protected override string CreateImage(BaseItem item, List<BaseItem> itemsWithImages, string outputPathWithoutExtension, ImageType imageType, int imageIndex) { if (itemsWithImages.Count == 0) { diff --git a/Emby.Server.Implementations/UserViews/FolderImageProvider.cs b/Emby.Server.Implementations/UserViews/FolderImageProvider.cs index 80a74e877..abd6810b0 100644 --- a/Emby.Server.Implementations/UserViews/FolderImageProvider.cs +++ b/Emby.Server.Implementations/UserViews/FolderImageProvider.cs @@ -17,7 +17,7 @@ using MediaBrowser.Controller.Dto; namespace Emby.Server.Implementations.Photos { public abstract class BaseFolderImageProvider<T> : BaseDynamicImageProvider<T> - where T : Folder, new () + where T : Folder, new() { protected ILibraryManager _libraryManager; @@ -27,43 +27,33 @@ namespace Emby.Server.Implementations.Photos _libraryManager = libraryManager; } - protected override List<BaseItem> GetItemsWithImages(IHasMetadata item) + protected override List<BaseItem> GetItemsWithImages(BaseItem item) { return _libraryManager.GetItemList(new InternalItemsQuery { - Parent = item as BaseItem, - GroupByPresentationUniqueKey = false, + Parent = item, DtoOptions = new DtoOptions(true), - ImageTypes = new ImageType[] { ImageType.Primary } + ImageTypes = new ImageType[] { ImageType.Primary }, + OrderBy = new System.ValueTuple<string, SortOrder>[] + { + new System.ValueTuple<string, SortOrder>(ItemSortBy.IsFolder, SortOrder.Ascending), + new System.ValueTuple<string, SortOrder>(ItemSortBy.SortName, SortOrder.Ascending) + }, + Limit = 1 }); } - protected override string CreateImage(IHasMetadata item, List<BaseItem> itemsWithImages, string outputPathWithoutExtension, ImageType imageType, int imageIndex) + protected override string CreateImage(BaseItem item, List<BaseItem> itemsWithImages, string outputPathWithoutExtension, ImageType imageType, int imageIndex) { return CreateSingleImage(itemsWithImages, outputPathWithoutExtension, ImageType.Primary); } - protected override bool Supports(IHasMetadata item) + protected override bool Supports(BaseItem item) { - if (item is PhotoAlbum || item is MusicAlbum) - { - return true; - } - - if (item.GetType() == typeof(Folder)) - { - var folder = item as Folder; - if (folder.IsTopParent) - { - return false; - } - return true; - } - - return false; + return item is T; } - protected override bool HasChangedByDate(IHasMetadata item, ItemImageInfo image) + protected override bool HasChangedByDate(BaseItem item, ItemImageInfo image) { if (item is MusicAlbum) { @@ -80,6 +70,25 @@ namespace Emby.Server.Implementations.Photos : base(fileSystem, providerManager, applicationPaths, imageProcessor, libraryManager) { } + + protected override bool Supports(BaseItem item) + { + if (item is PhotoAlbum || item is MusicAlbum) + { + return false; + } + + var folder = item as Folder; + if (folder != null) + { + if (folder.IsTopParent) + { + return false; + } + } + return true; + //return item.SourceType == SourceType.Library; + } } public class MusicAlbumImageProvider : BaseFolderImageProvider<MusicAlbum> diff --git a/Emby.Server.Implementations/packages.config b/Emby.Server.Implementations/packages.config deleted file mode 100644 index 6e68810d8..000000000 --- a/Emby.Server.Implementations/packages.config +++ /dev/null @@ -1,9 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<packages> - <package id="Emby.XmlTv" version="1.0.10" targetFramework="net46" /> - <package id="ServiceStack.Text" version="4.5.8" targetFramework="net46" /> - <package id="SharpCompress" version="0.18.2" targetFramework="net46" /> - <package id="SimpleInjector" version="4.0.12" targetFramework="net46" /> - <package id="SQLitePCL.pretty" version="1.1.0" targetFramework="portable45-net45+win8" /> - <package id="SQLitePCLRaw.core" version="1.1.8" targetFramework="net46" /> -</packages>
\ No newline at end of file diff --git a/Emby.Server.Implementations/values.txt b/Emby.Server.Implementations/values.txt new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/Emby.Server.Implementations/values.txt |
