mirror of
https://github.com/jellyfin/jellyfin.git
synced 2026-06-01 05:18:27 +01:00
Update to 3.5.2 and .net core 2.1
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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++;
|
||||
|
||||
@@ -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>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,44 +13,22 @@ 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;
|
||||
|
||||
OpenUrl(appHost, url);
|
||||
}
|
||||
|
||||
/// <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>
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
AddToCollection(collectionId, ids, true, new MetadataRefreshOptions(_fileSystem));
|
||||
}
|
||||
|
||||
public Task AddToCollection(Guid collectionId, IEnumerable<string> ids)
|
||||
public void AddToCollection(Guid collectionId, IEnumerable<Guid> ids)
|
||||
{
|
||||
return AddToCollection(collectionId, ids, true, new MetadataRefreshOptions(_fileSystem));
|
||||
AddToCollection(collectionId, ids.Select(i => i.ToString("N")), true, new MetadataRefreshOptions(_fileSystem));
|
||||
}
|
||||
|
||||
public Task AddToCollection(Guid collectionId, IEnumerable<Guid> ids)
|
||||
{
|
||||
return 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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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++;
|
||||
|
||||
@@ -77,7 +77,6 @@ namespace Emby.Server.Implementations.Data
|
||||
{
|
||||
Close();
|
||||
}
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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,46 +49,93 @@ 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[] {
|
||||
|
||||
AddColumn(db, "userdata", "AudioStreamIndex", "int", existingColumnNames);
|
||||
AddColumn(db, "userdata", "SubtitleStreamIndex", "int", existingColumnNames);
|
||||
"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", "InternalUserId", "int", existingColumnNames);
|
||||
AddColumn(db, "userdata", "AudioStreamIndex", "int", existingColumnNames);
|
||||
AddColumn(db, "userdata", "SubtitleStreamIndex", "int", existingColumnNames);
|
||||
|
||||
if (!userDatasTableExists)
|
||||
{
|
||||
ImportUserIds(db, users);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
private void ImportUserIds(IDatabaseConnection db, User[] users)
|
||||
{
|
||||
var userIdsWithUserData = GetAllUserIdsWithUserData(db);
|
||||
|
||||
using (var statement = db.PrepareStatement("update userdata set InternalUserId=@InternalUserId where UserId=@UserId"))
|
||||
{
|
||||
foreach (var user in users)
|
||||
{
|
||||
ImportUserDataIfNeeded(connection);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.ErrorException("Error in ImportUserDataIfNeeded", ex);
|
||||
if (!userIdsWithUserData.Contains(user.Id))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
statement.TryBind("@UserId", user.Id.ToGuidBlob());
|
||||
statement.TryBind("@InternalUserId", user.InternalId);
|
||||
|
||||
statement.MoveNext();
|
||||
statement.Reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private List<Guid> GetAllUserIdsWithUserData(IDatabaseConnection db)
|
||||
{
|
||||
List<Guid> list = new List<Guid>();
|
||||
|
||||
using (var statement = PrepareStatement(db, "select DISTINCT UserId from UserData where UserId not null"))
|
||||
{
|
||||
foreach (var row in statement.ExecuteQuery())
|
||||
{
|
||||
try
|
||||
{
|
||||
list.Add(row[0].ReadGuidFromBlob());
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
protected override bool EnableTempStoreMemory
|
||||
{
|
||||
get
|
||||
@@ -98,90 +144,39 @@ namespace Emby.Server.Implementations.Data
|
||||
}
|
||||
}
|
||||
|
||||
private void ImportUserDataIfNeeded(ManagedConnection connection)
|
||||
{
|
||||
if (!_fileSystem.FileExists(_importFile))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var fileToImport = _importFile;
|
||||
var isImported = connection.Query("select IsUserDataImported from DataSettings").SelectScalarBool().FirstOrDefault();
|
||||
|
||||
if (isImported)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ImportUserData(connection, fileToImport);
|
||||
|
||||
connection.RunInTransaction(db =>
|
||||
{
|
||||
using (var statement = db.PrepareStatement("replace into DataSettings (IsUserDataImported) values (@IsUserDataImported)"))
|
||||
{
|
||||
statement.TryBind("@IsUserDataImported", true);
|
||||
statement.MoveNext();
|
||||
}
|
||||
}, TransactionMode);
|
||||
}
|
||||
|
||||
private void ImportUserData(ManagedConnection connection, string file)
|
||||
{
|
||||
SqliteExtensions.Attach(connection, file, "UserDataBackup");
|
||||
|
||||
var columns = "key, userId, rating, played, playCount, isFavorite, playbackPositionTicks, lastPlayedDate, AudioStreamIndex, SubtitleStreamIndex";
|
||||
|
||||
connection.RunInTransaction(db =>
|
||||
{
|
||||
db.Execute("REPLACE INTO userdata(" + columns + ") SELECT " + columns + " FROM UserDataBackup.userdata;");
|
||||
}, TransactionMode);
|
||||
}
|
||||
|
||||
/// <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)
|
||||
|
||||
@@ -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,46 @@ 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, _memoryStreamProvider);
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
var serialized = _jsonSerializer.SerializeToBytes(user);
|
||||
|
||||
using (WriteLock.Write())
|
||||
{
|
||||
@@ -89,22 +96,96 @@ 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("insert into LocalUsersv2 (guid, data) values (@guid, @data)"))
|
||||
{
|
||||
statement.TryBind("@guid", user.Id.ToGuidBlob());
|
||||
statement.TryBind("@data", serialized);
|
||||
|
||||
statement.MoveNext();
|
||||
}
|
||||
|
||||
var createdUser = GetUser(user.Id, false);
|
||||
|
||||
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())
|
||||
{
|
||||
using (var connection = CreateConnection())
|
||||
{
|
||||
connection.RunInTransaction(db =>
|
||||
{
|
||||
using (var statement = db.PrepareStatement("update LocalUsersv2 set data=@data where Id=@InternalId"))
|
||||
{
|
||||
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);
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
var path = Path.Combine(GetDevicePath(deviceId), "capabilities.json");
|
||||
_fileSystem.CreateDirectory(_fileSystem.GetDirectoryName(path));
|
||||
|
||||
lock (_capabilitiesSyncLock)
|
||||
{
|
||||
throw new ArgumentNullException("reportedId");
|
||||
_capabilitiesCache[deviceId] = capabilities;
|
||||
|
||||
_json.SerializeToFile(capabilities, path);
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateDeviceOptions(string deviceId, DeviceOptions options)
|
||||
{
|
||||
_authRepo.UpdateDeviceOptions(deviceId, options);
|
||||
|
||||
if (DeviceOptionsUpdated != null)
|
||||
{
|
||||
DeviceOptionsUpdated(this, new GenericEventArgs<Tuple<string, DeviceOptions>>()
|
||||
{
|
||||
Argument = new Tuple<string, DeviceOptions>(deviceId, options)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public DeviceOptions GetDeviceOptions(string deviceId)
|
||||
{
|
||||
return _authRepo.GetDeviceOptions(deviceId);
|
||||
}
|
||||
|
||||
public ClientCapabilities GetCapabilities(string id)
|
||||
{
|
||||
lock (_capabilitiesSyncLock)
|
||||
{
|
||||
ClientCapabilities result;
|
||||
if (_capabilitiesCache.TryGetValue(id, out result))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
var path = Path.Combine(GetDevicePath(id), "capabilities.json");
|
||||
try
|
||||
{
|
||||
return _json.DeserializeFromFile<ClientCapabilities>(path) ?? new ClientCapabilities();
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
var device = GetDevice(reportedId) ?? new DeviceInfo
|
||||
{
|
||||
Id = reportedId
|
||||
};
|
||||
|
||||
device.ReportedName = name;
|
||||
device.AppName = appName;
|
||||
device.AppVersion = appVersion;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(usedByUserId))
|
||||
{
|
||||
var user = _userManager.GetUserById(usedByUserId);
|
||||
|
||||
device.LastUserId = user.Id.ToString("N");
|
||||
device.LastUserName = user.Name;
|
||||
}
|
||||
|
||||
device.DateLastModified = DateTime.UtcNow;
|
||||
|
||||
device.Name = string.IsNullOrWhiteSpace(device.CustomName) ? device.ReportedName : device.CustomName;
|
||||
|
||||
_repo.SaveDevice(device);
|
||||
|
||||
return device;
|
||||
}
|
||||
|
||||
public void SaveCapabilities(string reportedId, ClientCapabilities capabilities)
|
||||
{
|
||||
_repo.SaveCapabilities(reportedId, capabilities);
|
||||
}
|
||||
|
||||
public ClientCapabilities GetCapabilities(string reportedId)
|
||||
{
|
||||
return _repo.GetCapabilities(reportedId);
|
||||
return new ClientCapabilities();
|
||||
}
|
||||
|
||||
public DeviceInfo GetDevice(string id)
|
||||
{
|
||||
return _repo.GetDevice(id);
|
||||
return GetDevice(id, true);
|
||||
}
|
||||
|
||||
private DeviceInfo GetDevice(string id, bool includeCapabilities)
|
||||
{
|
||||
var session = _authRepo.Get(new AuthenticationInfoQuery
|
||||
{
|
||||
DeviceId = 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)
|
||||
{
|
||||
_repo.DeleteDevice(id);
|
||||
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)
|
||||
{
|
||||
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,25 +264,99 @@ 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 new Tuple<string, string, string>(path, topLibraryPath, null);
|
||||
}
|
||||
|
||||
internal string GetUploadsPath()
|
||||
{
|
||||
var config = _config.GetUploadOptions();
|
||||
var path = config.CameraUploadPath;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
{
|
||||
path = DefaultCameraUploadsPath;
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
@@ -214,37 +365,16 @@ namespace Emby.Server.Implementations.Devices
|
||||
get { return Path.Combine(_config.CommonApplicationPaths.DataPath, "camerauploads"); }
|
||||
}
|
||||
|
||||
public void UpdateDeviceInfo(string id, DeviceOptions options)
|
||||
public bool CanAccessDevice(User user, string deviceId)
|
||||
{
|
||||
var device = GetDevice(id);
|
||||
|
||||
device.CustomName = options.CustomName;
|
||||
device.CameraUploadPath = options.CameraUploadPath;
|
||||
|
||||
device.Name = string.IsNullOrWhiteSpace(device.CustomName) ? device.ReportedName : device.CustomName;
|
||||
|
||||
_repo.SaveDevice(device);
|
||||
|
||||
EventHelper.FireEventIfNotNull(DeviceOptionsUpdated, this, new GenericEventArgs<DeviceInfo>(device), _logger);
|
||||
}
|
||||
|
||||
public bool CanAccessDevice(string userId, string deviceId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(userId))
|
||||
{
|
||||
throw new ArgumentNullException("userId");
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(deviceId))
|
||||
{
|
||||
throw new ArgumentNullException("deviceId");
|
||||
}
|
||||
|
||||
var user = _userManager.GetUserById(userId);
|
||||
|
||||
if (user == null)
|
||||
{
|
||||
throw new ArgumentException("user not found");
|
||||
}
|
||||
if (string.IsNullOrEmpty(deviceId))
|
||||
{
|
||||
throw new ArgumentNullException("deviceId");
|
||||
}
|
||||
|
||||
if (!CanAccessDevice(user.Policy, 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
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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>
|
||||
</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>
|
||||
|
||||
@@ -112,7 +112,6 @@ namespace Emby.Server.Implementations.EntryPoints
|
||||
_appHost.HasPendingRestartChanged -= _appHost_HasPendingRestartChanged;
|
||||
|
||||
DisposeTimer();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
private void DisposeTimer()
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
await CreatePortMap(device, _appHost.HttpsPort, _config.Configuration.PublicHttpsPort).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
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.HttpPort, _config.Configuration.PublicPort).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error("Error creating port map: " + ex.Message);
|
||||
return;
|
||||
}
|
||||
|
||||
return false;
|
||||
try
|
||||
{
|
||||
await CreatePortMap(device, _appHost.HttpsPort, _config.Configuration.PublicHttpsPort).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
var natManager = _natManager;
|
||||
|
||||
if (natManager != null)
|
||||
{
|
||||
// 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
|
||||
{
|
||||
_isStarted = false;
|
||||
_natManager = null;
|
||||
|
||||
using (natManager)
|
||||
{
|
||||
try
|
||||
{
|
||||
natManager.StopDiscovery();
|
||||
natManager.DeviceFound -= NatUtility_DeviceFound;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.ErrorException("Error stopping NAT Discovery", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,7 +60,6 @@ namespace Emby.Server.Implementations.EntryPoints
|
||||
_timer.Dispose();
|
||||
_timer = null;
|
||||
}
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -54,11 +54,15 @@ 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)
|
||||
{
|
||||
@@ -72,7 +76,6 @@ namespace Emby.Server.Implementations.EntryPoints
|
||||
_liveTvManager.SeriesTimerCancelled -= _liveTvManager_SeriesTimerCancelled;
|
||||
_liveTvManager.TimerCreated -= _liveTvManager_TimerCreated;
|
||||
_liveTvManager.SeriesTimerCreated -= _liveTvManager_SeriesTimerCreated;
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,11 +21,6 @@ namespace Emby.Server.Implementations.EntryPoints
|
||||
/// </summary>
|
||||
public class ServerEventNotifier : IServerEntryPoint
|
||||
{
|
||||
/// <summary>
|
||||
/// The _server manager
|
||||
/// </summary>
|
||||
private readonly IServerManager _serverManager;
|
||||
|
||||
/// <summary>
|
||||
/// The _user manager
|
||||
/// </summary>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -34,7 +34,6 @@ namespace Emby.Server.Implementations.EntryPoints
|
||||
public void Dispose()
|
||||
{
|
||||
_systemEvents.SystemShutdown -= _systemEvents_SystemShutdown;
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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,52 +105,43 @@ 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();
|
||||
|
||||
if (userSessions.Count > 0)
|
||||
{
|
||||
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"),
|
||||
|
||||
UserDataList = dtoList
|
||||
};
|
||||
|
||||
foreach (var userSession in userSessions)
|
||||
{
|
||||
try
|
||||
{
|
||||
await userSession.SessionController.SendUserDataChangeInfo(info, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.ErrorException("Error sending UserDataChanged message", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
var dtoList = changedItems
|
||||
.DistinctBy(i => i.Id)
|
||||
.Select(i =>
|
||||
{
|
||||
var dto = _userDataManager.GetUserDataDto(i, user);
|
||||
dto.ItemId = i.Id.ToString("N");
|
||||
return dto;
|
||||
})
|
||||
.ToArray();
|
||||
|
||||
var userIdString = userId.ToString("N");
|
||||
|
||||
return new UserDataChangeInfo
|
||||
{
|
||||
UserId = userIdString,
|
||||
|
||||
UserDataList = dtoList
|
||||
};
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (UpdateTimer != null)
|
||||
@@ -160,7 +151,6 @@ namespace Emby.Server.Implementations.EntryPoints
|
||||
}
|
||||
|
||||
_userDataManager.UserDataSaved -= _userDataManager_UserDataSaved;
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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 (!ValidateRequest(remoteIp, httpReq.IsLocal))
|
||||
{
|
||||
httpRes.StatusCode = 403;
|
||||
httpRes.ContentType = "text/plain";
|
||||
await Write(httpRes, "Forbidden").ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ValidateSsl(httpReq.RemoteIp, urlString))
|
||||
{
|
||||
var httpsUrl = urlString
|
||||
.Replace("http://", "https://", StringComparison.OrdinalIgnoreCase)
|
||||
.Replace(":" + _config.Configuration.PublicPort.ToString(CultureInfo.InvariantCulture), ":" + _config.Configuration.PublicHttpsPort.ToString(CultureInfo.InvariantCulture), StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
RedirectToUrl(httpRes, httpsUrl);
|
||||
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;
|
||||
|
||||
|
||||
@@ -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(content, contentType, true, responseHeaders);
|
||||
return GetHttpResult(requestContext, content, contentType, true, responseHeaders);
|
||||
}
|
||||
|
||||
public object GetResult(string content, string contentType, IDictionary<string, string> responseHeaders = null)
|
||||
{
|
||||
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)
|
||||
{
|
||||
var result = new StreamWriter(content, contentType, _logger);
|
||||
|
||||
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, byte[] content, string contentType, bool addCachePrevention, IDictionary<string, string> responseHeaders = null)
|
||||
{
|
||||
IHasHeaders result;
|
||||
|
||||
var stream = content as Stream;
|
||||
var compressionType = requestContext == null ? null : GetCompressionType(requestContext, content, contentType);
|
||||
|
||||
if (stream != null)
|
||||
var isHeadRequest = string.Equals(requestContext.Verb, "head", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
if (string.IsNullOrEmpty(compressionType))
|
||||
{
|
||||
result = new StreamWriter(stream, contentType, _logger);
|
||||
}
|
||||
var contentLength = content.Length;
|
||||
|
||||
if (isHeadRequest)
|
||||
{
|
||||
content = Array.Empty<byte>();
|
||||
}
|
||||
|
||||
result = new StreamWriter(content, contentType, contentLength, _logger);
|
||||
}
|
||||
else
|
||||
{
|
||||
var bytes = content as byte[];
|
||||
|
||||
if (bytes != null)
|
||||
{
|
||||
result = new StreamWriter(bytes, contentType, _logger);
|
||||
}
|
||||
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 = 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,19 +199,8 @@ 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)
|
||||
{
|
||||
@@ -147,24 +212,49 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
responseHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
if (addCachePrevention)
|
||||
{
|
||||
responseHeaders["Expires"] = "-1";
|
||||
}
|
||||
responseHeaders["Expires"] = "-1";
|
||||
|
||||
return ToOptimizedResultInternal(requestContext, result, responseHeaders);
|
||||
}
|
||||
|
||||
public static string GetCompressionType(IRequest request)
|
||||
private string GetCompressionType(IRequest request, byte[] content, string responseContentType)
|
||||
{
|
||||
if (responseContentType == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
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,28 +282,110 @@ 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:
|
||||
break;
|
||||
}
|
||||
|
||||
var isHeadRequest = string.Equals(request.Verb, "head", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
var ms = new MemoryStream();
|
||||
var writerFn = RequestHelper.GetResponseWriter(HttpListenerHost.Instance, contentType);
|
||||
|
||||
writerFn(dto, ms);
|
||||
|
||||
ms.Position = 0;
|
||||
|
||||
if (isHeadRequest)
|
||||
{
|
||||
using (ms)
|
||||
{
|
||||
var ms = new MemoryStream();
|
||||
var writerFn = RequestHelper.GetResponseWriter(HttpListenerHost.Instance, contentType);
|
||||
|
||||
writerFn(dto, ms);
|
||||
|
||||
ms.Position = 0;
|
||||
|
||||
if (string.Equals(request.Verb, "head", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return GetHttpResult(new byte[] { }, contentType, true, responseHeaders);
|
||||
}
|
||||
|
||||
return GetHttpResult(ms, contentType, true, responseHeaders);
|
||||
return GetHttpResult(request, Array.Empty<byte>(), contentType, true, responseHeaders);
|
||||
}
|
||||
}
|
||||
|
||||
return GetHttpResult(request, ms, contentType, true, responseHeaders);
|
||||
}
|
||||
|
||||
private IHasHeaders GetCompressedResult(byte[] content,
|
||||
string requestedCompressionType,
|
||||
IDictionary<string, string> responseHeaders,
|
||||
bool isHeadRequest,
|
||||
string contentType)
|
||||
{
|
||||
if (responseHeaders == null)
|
||||
{
|
||||
responseHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
content = Compress(content, requestedCompressionType);
|
||||
responseHeaders["Content-Encoding"] = requestedCompressionType;
|
||||
|
||||
responseHeaders["Vary"] = "Accept-Encoding";
|
||||
|
||||
var contentLength = content.Length;
|
||||
|
||||
if (isHeadRequest)
|
||||
{
|
||||
var result = new StreamWriter(Array.Empty<byte>(), contentType, contentLength, _logger);
|
||||
AddResponseHeaders(result, responseHeaders);
|
||||
return result;
|
||||
}
|
||||
else
|
||||
{
|
||||
var result = new StreamWriter(content, contentType, contentLength, _logger);
|
||||
AddResponseHeaders(result, responseHeaders);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] Compress(byte[] bytes, string compressionType)
|
||||
{
|
||||
if (string.Equals(compressionType, "br", StringComparison.OrdinalIgnoreCase))
|
||||
return CompressBrotli(bytes);
|
||||
|
||||
if (string.Equals(compressionType, "deflate", StringComparison.OrdinalIgnoreCase))
|
||||
return Deflate(bytes);
|
||||
|
||||
if (string.Equals(compressionType, "gzip", StringComparison.OrdinalIgnoreCase))
|
||||
return GZip(bytes);
|
||||
|
||||
throw new NotSupportedException(compressionType);
|
||||
}
|
||||
|
||||
private byte[] CompressBrotli(byte[] bytes)
|
||||
{
|
||||
return _brotliCompressor.Compress(bytes);
|
||||
}
|
||||
|
||||
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))
|
||||
{
|
||||
zipStream.Write(bytes, 0, bytes.Length);
|
||||
zipStream.Dispose();
|
||||
|
||||
return ms.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
return ms.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
public static string GetRealContentType(string contentType)
|
||||
@@ -243,104 +415,6 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
}
|
||||
}
|
||||
|
||||
/// <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
|
||||
{
|
||||
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);
|
||||
|
||||
if (result != null)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
return GetOptimizedResultInternal(requestContext, factoryFn(), false, responseHeaders);
|
||||
}
|
||||
|
||||
/// <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)
|
||||
{
|
||||
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, contentType);
|
||||
|
||||
if (result != null)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
result = factoryFn();
|
||||
|
||||
// Apply caching headers
|
||||
var hasHeaders = result as IHasHeaders;
|
||||
|
||||
if (hasHeaders != null)
|
||||
{
|
||||
AddResponseHeaders(hasHeaders, responseHeaders);
|
||||
return hasHeaders;
|
||||
}
|
||||
|
||||
return GetHttpResult(result, contentType, false, responseHeaders);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pres the process optimized result.
|
||||
/// </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 stream = await factoryFn().ConfigureAwait(false);
|
||||
|
||||
var hasHeaders = new RangeRequestWriter(rangeHeader, stream, contentType, isHeadRequest, _logger)
|
||||
var totalContentLength = options.ContentLength;
|
||||
if (!totalContentLength.HasValue)
|
||||
{
|
||||
try
|
||||
{
|
||||
totalContentLength = stream.Length;
|
||||
}
|
||||
catch (NotSupportedException)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 (tokenInfo != null && string.IsNullOrWhiteSpace(tokenInfo.UserId))
|
||||
if (string.IsNullOrEmpty(auth.Token))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
@@ -32,11 +31,6 @@ namespace Emby.Server.Implementations.ServerManager
|
||||
/// </summary>
|
||||
public string RemoteEndPoint { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The _cancellation token source
|
||||
/// </summary>
|
||||
private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
|
||||
|
||||
/// <summary>
|
||||
/// The logger
|
||||
/// </summary>
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
13
Emby.Server.Implementations/IO/ExtendedFileSystemInfo.cs
Normal file
13
Emby.Server.Implementations/IO/ExtendedFileSystemInfo.cs
Normal file
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,7 +70,6 @@ namespace Emby.Server.Implementations.IO
|
||||
{
|
||||
mounter.Dispose();
|
||||
}
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
{
|
||||
|
||||
190
Emby.Server.Implementations/IO/StreamHelper.cs
Normal file
190
Emby.Server.Implementations/IO/StreamHelper.cs
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
if (filename.IndexOf('.') == 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Ignore hidden files and folders
|
||||
if (isHidden)
|
||||
{
|
||||
if (parent == null)
|
||||
{
|
||||
var parentFolderName = Path.GetFileName(_fileSystem.GetDirectoryName(path));
|
||||
//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;
|
||||
}
|
||||
}
|
||||
// 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;
|
||||
}
|
||||
// // Sometimes these are marked hidden
|
||||
// if (_fileSystem.IsRootPath(path))
|
||||
// {
|
||||
// return false;
|
||||
// }
|
||||
|
||||
return true;
|
||||
}
|
||||
// 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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
42
Emby.Server.Implementations/Library/ExclusiveLiveStream.cs
Normal file
42
Emby.Server.Implementations/Library/ExclusiveLiveStream.cs
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
181
Emby.Server.Implementations/Library/LiveStreamHelper.cs
Normal file
181
Emby.Server.Implementations/Library/LiveStreamHelper.cs
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
SetDefaultAudioAndSubtitleStreamIndexes(item, source, user);
|
||||
}
|
||||
if (source.Protocol == MediaProtocol.File)
|
||||
|
||||
// Validate that this is actually possible
|
||||
if (source.SupportsDirectStream)
|
||||
{
|
||||
// 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;
|
||||
}
|
||||
else
|
||||
{
|
||||
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();
|
||||
}
|
||||
|
||||
private async Task<IEnumerable<MediaSourceInfo>> GetDynamicMediaSources(IHasMediaSources item, CancellationToken cancellationToken)
|
||||
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;
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
var mediaSource = mediaSourceTuple.Item1;
|
||||
liveStream = await provider.OpenMediaSource(tuple.Item2, currentLiveStreams, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(mediaSource.LiveStreamId))
|
||||
mediaSource = liveStream.MediaSource;
|
||||
|
||||
// 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
|
||||
{
|
||||
Id = mediaSource.LiveStreamId,
|
||||
MediaSource = mediaSource,
|
||||
DirectStreamProvider = mediaSourceTuple.Item2
|
||||
};
|
||||
_openStreams[mediaSource.LiveStreamId] = liveStream;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_liveStreamSemaphore.Release();
|
||||
}
|
||||
|
||||
_openStreams[mediaSource.LiveStreamId] = info;
|
||||
// TODO: Don't hardcode this
|
||||
var isAudio = false;
|
||||
|
||||
var json = _jsonSerializer.SerializeToString(mediaSource);
|
||||
_logger.Debug("Live stream opened: " + json);
|
||||
var clone = _jsonSerializer.DeserializeFromString<MediaSourceInfo>(json);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(request.UserId))
|
||||
try
|
||||
{
|
||||
if (mediaSource.MediaStreams.Any(i => i.Index != -1) || !mediaSource.SupportsProbing)
|
||||
{
|
||||
var user = _userManager.GetUserById(request.UserId);
|
||||
var item = string.IsNullOrWhiteSpace(request.ItemId)
|
||||
? null
|
||||
: _libraryManager.GetItemById(request.ItemId);
|
||||
SetUserProperties(item, clone, user);
|
||||
AddMediaInfo(mediaSource, isAudio);
|
||||
}
|
||||
|
||||
return new LiveStreamResponse
|
||||
else
|
||||
{
|
||||
MediaSource = clone
|
||||
};
|
||||
// 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;
|
||||
}
|
||||
|
||||
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 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);
|
||||
|
||||
await CloseLiveStreamWithProvider(tuple.Item1, tuple.Item2).ConfigureAwait(false);
|
||||
_logger.Info("Closing live stream {0}", id);
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
217
Emby.Server.Implementations/Library/MediaStreamSelector.cs
Normal file
217
Emby.Server.Implementations/Library/MediaStreamSelector.cs
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
@@ -107,17 +107,6 @@ namespace Emby.Server.Implementations.Library
|
||||
return isDirectory ? Path.GetFileName(path) : Path.GetFileNameWithoutExtension(path);
|
||||
}
|
||||
|
||||
/// <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>
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
var path = fileSystemInfo.FullName;
|
||||
var isMultiDisc = IsMultiDiscFolder(path, libraryOptions);
|
||||
|
||||
if (isMultiDisc)
|
||||
if (notMultiDisc)
|
||||
{
|
||||
var hasMusic = ContainsMusic(directoryService.GetFileSystemEntries(path), false, directoryService, logger, fileSystem, libraryOptions, libraryManager);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (hasMusic)
|
||||
var path = fileSystemInfo.FullName;
|
||||
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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
@@ -125,6 +125,10 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
|
||||
if (child.IsDirectory)
|
||||
{
|
||||
leftOver.Add(child);
|
||||
}
|
||||
else if (IsIgnored(child.Name))
|
||||
{
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -94,7 +94,8 @@ namespace Emby.Server.Implementations.Library.Resolvers
|
||||
"backdrop",
|
||||
"poster",
|
||||
"cover",
|
||||
"logo"
|
||||
"logo",
|
||||
"default"
|
||||
};
|
||||
|
||||
internal static bool IsImageFile(string path, IImageProcessor imageProcessor)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 ?
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 =>
|
||||
return mediaItems.Select(i => new SearchHintInfo
|
||||
{
|
||||
var index = GetIndex(item.Name, searchTerm, terms);
|
||||
Item = i
|
||||
|
||||
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++)
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
/// <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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
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))
|
||||
{
|
||||
providers = providers.Where(i => string.Equals(authenticationProviderId, GetAuthenticationProviderId(i), StringComparison.OrdinalIgnoreCase)).ToArray();
|
||||
}
|
||||
|
||||
if (providers.Length == 0)
|
||||
{
|
||||
providers = new IAuthenticationProvider[] { _defaultAuthenticationProvider };
|
||||
}
|
||||
|
||||
return providers;
|
||||
}
|
||||
|
||||
private async Task<bool> AuthenticateWithProvider(IAuthenticationProvider provider, string username, string password, User resolvedUser)
|
||||
{
|
||||
try
|
||||
{
|
||||
var requiresResolvedUser = provider as IRequiresResolvedUser;
|
||||
if (requiresResolvedUser != null)
|
||||
{
|
||||
await requiresResolvedUser.Authenticate(username, password, resolvedUser).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
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(GetPasswordHash(user), hashedPassword.Replace("-", string.Empty), StringComparison.OrdinalIgnoreCase);
|
||||
success = string.Equals(_defaultAuthenticationProvider.GetPasswordHash(user), hashedPassword.Replace("-", string.Empty), StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
else
|
||||
{
|
||||
success = string.Equals(GetPasswordHash(user), GetHashedString(user, password), StringComparison.OrdinalIgnoreCase);
|
||||
foreach (var provider in GetAuthenticationProviders(user))
|
||||
{
|
||||
success = await AuthenticateWithProvider(provider, username, password, user).ConfigureAwait(false);
|
||||
|
||||
if (success)
|
||||
{
|
||||
authenticationProvider = provider;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!success && _networkManager.IsInLocalNetwork(remoteEndPoint) && user.Configuration.EnableLocalPassword)
|
||||
if (user != null)
|
||||
{
|
||||
if (password == null)
|
||||
if (!success && _networkManager.IsInLocalNetwork(remoteEndPoint) && user.Configuration.EnableLocalPassword)
|
||||
{
|
||||
// legacy
|
||||
success = string.Equals(GetLocalPasswordHash(user), hashedPassword.Replace("-", string.Empty), StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
else
|
||||
{
|
||||
success = string.Equals(GetLocalPasswordHash(user), GetHashedString(user, password), StringComparison.OrdinalIgnoreCase);
|
||||
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 success;
|
||||
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()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user