Luke Pulverenti 10 роки тому
батько
коміт
e84ba17b9f
59 змінених файлів з 1536 додано та 300 видалено
  1. 1 1
      MediaBrowser.Api/ChannelService.cs
  2. 1 0
      MediaBrowser.Api/Images/ImageService.cs
  3. 6 5
      MediaBrowser.Api/MediaBrowser.Api.csproj
  4. 1 1
      MediaBrowser.Api/Session/SessionInfoWebSocketListener.cs
  5. 1 1
      MediaBrowser.Api/Session/SessionsService.cs
  6. 44 0
      MediaBrowser.Api/System/ActivityLogService.cs
  7. 1 1
      MediaBrowser.Api/System/SystemInfoWebSocketListener.cs
  8. 5 4
      MediaBrowser.Api/System/SystemService.cs
  9. 15 6
      MediaBrowser.Api/UserService.cs
  10. 12 0
      MediaBrowser.Common.Implementations/Configuration/BaseConfigurationManager.cs
  11. 1 0
      MediaBrowser.Common.Implementations/ScheduledTasks/ScheduledTaskWorker.cs
  12. 5 0
      MediaBrowser.Common/Configuration/IConfigurationManager.cs
  13. 5 0
      MediaBrowser.Common/ScheduledTasks/IConfigurableScheduledTask.cs
  14. 17 0
      MediaBrowser.Controller/Activity/IActivityManager.cs
  15. 13 0
      MediaBrowser.Controller/Activity/IActivityRepository.cs
  16. 0 7
      MediaBrowser.Controller/Configuration/IServerConfigurationManager.cs
  17. 1 13
      MediaBrowser.Controller/Entities/Movies/BoxSet.cs
  18. 1 0
      MediaBrowser.Controller/Library/IUserManager.cs
  19. 1 1
      MediaBrowser.Controller/Library/TVUtils.cs
  20. 5 1
      MediaBrowser.Controller/MediaBrowser.Controller.csproj
  21. 0 12
      MediaBrowser.Controller/Notifications/INotificationsRepository.cs
  22. 14 0
      MediaBrowser.Controller/Session/AuthenticationRequest.cs
  23. 13 15
      MediaBrowser.Controller/Session/ISessionManager.cs
  24. 14 3
      MediaBrowser.Controller/Subtitles/ISubtitleManager.cs
  25. 27 0
      MediaBrowser.Controller/Subtitles/SubtitleDownloadEventArgs.cs
  26. 2 2
      MediaBrowser.LocalMetadata/BaseXmlProvider.cs
  27. 6 3
      MediaBrowser.Model.Portable/MediaBrowser.Model.Portable.csproj
  28. 6 3
      MediaBrowser.Model.net35/MediaBrowser.Model.net35.csproj
  29. 62 0
      MediaBrowser.Model/Activity/ActivityLogEntry.cs
  30. 1 1
      MediaBrowser.Model/ApiClient/IApiClient.cs
  31. 2 3
      MediaBrowser.Model/Channels/ChannelItemQuery.cs
  32. 1 2
      MediaBrowser.Model/Configuration/ServerConfiguration.cs
  33. 16 0
      MediaBrowser.Model/Events/GenericEventArgs.cs
  34. 3 2
      MediaBrowser.Model/MediaBrowser.Model.csproj
  35. 1 1
      MediaBrowser.Model/Providers/SubtitleOptions.cs
  36. 6 0
      MediaBrowser.Model/Tasks/TaskResult.cs
  37. 2 1
      MediaBrowser.Providers/MediaBrowser.Providers.csproj
  38. 14 5
      MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs
  39. 20 10
      MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs
  40. 29 0
      MediaBrowser.Providers/Subtitles/ConfigurationExtension.cs
  41. 24 11
      MediaBrowser.Providers/Subtitles/OpenSubtitleDownloader.cs
  42. 51 18
      MediaBrowser.Providers/Subtitles/SubtitleManager.cs
  43. 40 0
      MediaBrowser.Server.Implementations/Activity/ActivityManager.cs
  44. 293 0
      MediaBrowser.Server.Implementations/Activity/ActivityRepository.cs
  45. 543 0
      MediaBrowser.Server.Implementations/EntryPoints/ActivityLogEntryPoint.cs
  46. 2 6
      MediaBrowser.Server.Implementations/EntryPoints/Notifications/Notifications.cs
  47. 0 9
      MediaBrowser.Server.Implementations/EntryPoints/Notifications/WebSocketNotifier.cs
  48. 12 1
      MediaBrowser.Server.Implementations/EntryPoints/ServerEventNotifier.cs
  49. 6 1
      MediaBrowser.Server.Implementations/FileOrganization/OrganizerScheduledTask.cs
  50. 1 1
      MediaBrowser.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpListener.cs
  51. 5 2
      MediaBrowser.Server.Implementations/Library/UserManager.cs
  52. 85 53
      MediaBrowser.Server.Implementations/Localization/Server/server.json
  53. 4 1
      MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj
  54. 0 66
      MediaBrowser.Server.Implementations/Notifications/SqliteNotificationsRepository.cs
  55. 2 0
      MediaBrowser.Server.Implementations/ServerManager/ServerManager.cs
  56. 27 24
      MediaBrowser.Server.Implementations/Session/SessionManager.cs
  57. 0 1
      MediaBrowser.Server.Implementations/Session/SessionWebSocketListener.cs
  58. 44 2
      MediaBrowser.Server.Implementations/Sync/SyncRepository.cs
  59. 22 0
      MediaBrowser.ServerApplication/ApplicationHost.cs

+ 1 - 1
MediaBrowser.Api/ChannelService.cs

@@ -230,7 +230,7 @@ namespace MediaBrowser.Api
                 SortOrder = request.SortOrder,
                 SortBy = (request.SortBy ?? string.Empty).Split(',').Where(i => !string.IsNullOrWhiteSpace(i)).ToArray(),
                 Filters = request.GetFilters().ToArray(),
-                Fields = request.GetItemFields().ToList()
+                Fields = request.GetItemFields().ToArray()
 
             }, CancellationToken.None).Result;
 

+ 1 - 0
MediaBrowser.Api/Images/ImageService.cs

@@ -40,6 +40,7 @@ namespace MediaBrowser.Api.Images
     [Route("/Items/{Id}/Images/{Type}", "GET")]
     [Route("/Items/{Id}/Images/{Type}/{Index}", "GET")]
     [Route("/Items/{Id}/Images/{Type}/{Index}/{Tag}/{Format}/{MaxWidth}/{MaxHeight}", "GET")]
+    [Route("/Items/{Id}/Images/{Type}/{Index}/{Tag}/{Format}/{MaxWidth}/{MaxHeight}", "HEAD")]
     [Api(Description = "Gets an item image")]
     public class GetItemImage : ImageRequest
     {

+ 6 - 5
MediaBrowser.Api/MediaBrowser.Api.csproj

@@ -118,10 +118,11 @@
     <Compile Include="ScheduledTasks\ScheduledTasksWebSocketListener.cs" />
     <Compile Include="ApiEntryPoint.cs" />
     <Compile Include="SearchService.cs" />
-    <Compile Include="SessionsService.cs" />
+    <Compile Include="Session\SessionsService.cs" />
     <Compile Include="SimilarItemsHelper.cs" />
     <Compile Include="Sync\SyncService.cs" />
-    <Compile Include="SystemService.cs" />
+    <Compile Include="System\ActivityLogService.cs" />
+    <Compile Include="System\SystemService.cs" />
     <Compile Include="Movies\TrailersService.cs" />
     <Compile Include="TvShowsService.cs" />
     <Compile Include="UserLibrary\ArtistsService.cs" />
@@ -139,8 +140,8 @@
     <Compile Include="UserService.cs" />
     <Compile Include="Properties\AssemblyInfo.cs" />
     <Compile Include="VideosService.cs" />
-    <Compile Include="WebSocket\SessionInfoWebSocketListener.cs" />
-    <Compile Include="WebSocket\SystemInfoWebSocketListener.cs" />
+    <Compile Include="Session\SessionInfoWebSocketListener.cs" />
+    <Compile Include="System\SystemInfoWebSocketListener.cs" />
   </ItemGroup>
   <ItemGroup>
     <ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj">
@@ -173,4 +174,4 @@
   <Target Name="AfterBuild">
   </Target>
   -->
-</Project>
+</Project>

+ 1 - 1
MediaBrowser.Api/WebSocket/SessionInfoWebSocketListener.cs → MediaBrowser.Api/Session/SessionInfoWebSocketListener.cs

@@ -7,7 +7,7 @@ using System.Collections.Generic;
 using System.Linq;
 using System.Threading.Tasks;
 
-namespace MediaBrowser.Api.WebSocket
+namespace MediaBrowser.Api.Session
 {
     /// <summary>
     /// Class SessionInfoWebSocketListener

+ 1 - 1
MediaBrowser.Api/SessionsService.cs → MediaBrowser.Api/Session/SessionsService.cs

@@ -10,7 +10,7 @@ using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
 
-namespace MediaBrowser.Api
+namespace MediaBrowser.Api.Session
 {
     /// <summary>
     /// Class GetSessions

+ 44 - 0
MediaBrowser.Api/System/ActivityLogService.cs

@@ -0,0 +1,44 @@
+using MediaBrowser.Controller.Activity;
+using MediaBrowser.Controller.Net;
+using MediaBrowser.Model.Activity;
+using MediaBrowser.Model.Querying;
+using ServiceStack;
+
+namespace MediaBrowser.Api.System
+{
+    [Route("/System/ActivityLog/Entries", "GET", Summary = "Gets activity log entries")]
+    public class GetActivityLogs : IReturn<QueryResult<ActivityLogEntry>>
+    {
+        /// <summary>
+        /// Skips over a given number of items within the results. Use for paging.
+        /// </summary>
+        /// <value>The start index.</value>
+        [ApiMember(Name = "StartIndex", Description = "Optional. The record index to start at. All items with a lower index will be dropped from the results.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
+        public int? StartIndex { get; set; }
+
+        /// <summary>
+        /// The maximum number of items to return
+        /// </summary>
+        /// <value>The limit.</value>
+        [ApiMember(Name = "Limit", Description = "Optional. The maximum number of records to return", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
+        public int? Limit { get; set; }
+    }
+
+    [Authenticated]
+    public class ActivityLogService : BaseApiService
+    {
+        private readonly IActivityManager _activityManager;
+
+        public ActivityLogService(IActivityManager activityManager)
+        {
+            _activityManager = activityManager;
+        }
+
+        public object Get(GetActivityLogs request)
+        {
+            var result = _activityManager.GetActivityLogEntries(request.StartIndex, request.Limit);
+
+            return ToOptimizedResult(result);
+        }
+    }
+}

+ 1 - 1
MediaBrowser.Api/WebSocket/SystemInfoWebSocketListener.cs → MediaBrowser.Api/System/SystemInfoWebSocketListener.cs

@@ -4,7 +4,7 @@ using MediaBrowser.Model.Logging;
 using MediaBrowser.Model.System;
 using System.Threading.Tasks;
 
-namespace MediaBrowser.Api.WebSocket
+namespace MediaBrowser.Api.System
 {
     /// <summary>
     /// Class SystemInfoWebSocketListener

+ 5 - 4
MediaBrowser.Api/SystemService.cs → MediaBrowser.Api/System/SystemService.cs

@@ -4,12 +4,13 @@ using MediaBrowser.Controller;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Model.System;
 using ServiceStack;
+using System;
 using System.Collections.Generic;
 using System.IO;
 using System.Linq;
 using System.Threading.Tasks;
 
-namespace MediaBrowser.Api
+namespace MediaBrowser.Api.System
 {
     /// <summary>
     /// Class GetSystemInfo
@@ -73,7 +74,7 @@ namespace MediaBrowser.Api
         /// <param name="appHost">The app host.</param>
         /// <param name="appPaths">The application paths.</param>
         /// <param name="fileSystem">The file system.</param>
-        /// <exception cref="System.ArgumentNullException">jsonSerializer</exception>
+        /// <exception cref="ArgumentNullException">jsonSerializer</exception>
         public SystemService(IServerApplicationHost appHost, IApplicationPaths appPaths, IFileSystem fileSystem)
         {
             _appHost = appHost;
@@ -89,7 +90,7 @@ namespace MediaBrowser.Api
             {
                 files = new DirectoryInfo(_appPaths.LogDirectoryPath)
                     .EnumerateFiles("*", SearchOption.AllDirectories)
-                    .Where(i => string.Equals(i.Extension, ".txt", System.StringComparison.OrdinalIgnoreCase))
+                    .Where(i => string.Equals(i.Extension, ".txt", global::System.StringComparison.OrdinalIgnoreCase))
                     .ToList();
             }
             catch (DirectoryNotFoundException)
@@ -116,7 +117,7 @@ namespace MediaBrowser.Api
         {
             var file = new DirectoryInfo(_appPaths.LogDirectoryPath)
                 .EnumerateFiles("*", SearchOption.AllDirectories)
-                .First(i => string.Equals(i.Name, request.Name, System.StringComparison.OrdinalIgnoreCase));
+                .First(i => string.Equals(i.Name, request.Name, global::System.StringComparison.OrdinalIgnoreCase));
 
             return ResultFactory.GetStaticFileResult(Request, file.FullName, FileShare.ReadWrite);
         }

+ 15 - 6
MediaBrowser.Api/UserService.cs

@@ -195,7 +195,7 @@ namespace MediaBrowser.Api
             var authInfo = AuthorizationContext.GetAuthorizationInfo(Request);
             var isDashboard = string.Equals(authInfo.Client, "Dashboard", StringComparison.OrdinalIgnoreCase);
 
-            if ((Request.IsLocal && isDashboard) || 
+            if ((Request.IsLocal && isDashboard) ||
                 !_config.Configuration.IsStartupWizardCompleted)
             {
                 return Get(new GetUsers
@@ -327,7 +327,7 @@ namespace MediaBrowser.Api
             var revokeTask = _sessionMananger.RevokeUserTokens(user.Id.ToString("N"));
 
             Task.WaitAll(revokeTask);
-            
+
             var task = _userManager.DeleteUser(user);
 
             Task.WaitAll(task);
@@ -374,8 +374,17 @@ namespace MediaBrowser.Api
                 auth.DeviceId = "Unknown device id";
             }
 
-            var result = _sessionMananger.AuthenticateNewSession(request.Username, request.Password, auth.Client, auth.Version,
-                        auth.DeviceId, auth.Device, Request.RemoteIp, Request.IsLocal).Result;
+            var result = _sessionMananger.AuthenticateNewSession(new AuthenticationRequest
+            {
+                App = auth.Client,
+                AppVersion = auth.Version,
+                DeviceId = auth.DeviceId,
+                DeviceName = auth.Device,
+                Password = request.Password,
+                RemoteEndPoint = Request.RemoteIp,
+                Username = request.Username
+
+            }, Request.IsLocal).Result;
 
             return ToOptimizedResult(result);
         }
@@ -457,8 +466,8 @@ namespace MediaBrowser.Api
                 Task.WaitAll(revokeTask);
             }
 
-            var task = user.Name.Equals(dtoUser.Name, StringComparison.Ordinal) ? 
-                _userManager.UpdateUser(user) : 
+            var task = user.Name.Equals(dtoUser.Name, StringComparison.Ordinal) ?
+                _userManager.UpdateUser(user) :
                 _userManager.RenameUser(user, dtoUser.Name);
 
             Task.WaitAll(task);

+ 12 - 0
MediaBrowser.Common.Implementations/Configuration/BaseConfigurationManager.cs

@@ -28,6 +28,11 @@ namespace MediaBrowser.Common.Implementations.Configuration
         /// </summary>
         public event EventHandler<EventArgs> ConfigurationUpdated;
 
+        /// <summary>
+        /// Occurs when [configuration updating].
+        /// </summary>
+        public event EventHandler<ConfigurationUpdateEventArgs> NamedConfigurationUpdating;
+        
         /// <summary>
         /// Occurs when [named configuration updated].
         /// </summary>
@@ -217,6 +222,13 @@ namespace MediaBrowser.Common.Implementations.Configuration
                 throw new ArgumentException("Expected configuration type is " + configurationType.Name);
             }
 
+            EventHelper.FireEventIfNotNull(NamedConfigurationUpdating, this, new ConfigurationUpdateEventArgs
+            {
+                Key = key,
+                NewConfiguration = configuration
+
+            }, Logger);
+            
             _configurations.AddOrUpdate(key, configuration, (k, v) => configuration);
 
             var path = GetConfigurationFile(key);

+ 1 - 0
MediaBrowser.Common.Implementations/ScheduledTasks/ScheduledTaskWorker.cs

@@ -547,6 +547,7 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks
             if (ex != null)
             {
                 result.ErrorMessage = ex.Message;
+                result.LongErrorMessage = ex.StackTrace;
             }
 
             var path = GetHistoryFilePath();

+ 5 - 0
MediaBrowser.Common/Configuration/IConfigurationManager.cs

@@ -6,6 +6,11 @@ namespace MediaBrowser.Common.Configuration
 {
     public interface IConfigurationManager
     {
+        /// <summary>
+        /// Occurs when [configuration updating].
+        /// </summary>
+        event EventHandler<ConfigurationUpdateEventArgs> NamedConfigurationUpdating;
+
         /// <summary>
         /// Occurs when [configuration updated].
         /// </summary>

+ 5 - 0
MediaBrowser.Common/ScheduledTasks/IConfigurableScheduledTask.cs

@@ -13,4 +13,9 @@
         /// <value><c>true</c> if this instance is enabled; otherwise, <c>false</c>.</value>
         bool IsEnabled { get; }
     }
+
+    public interface IScheduledTaskActivityLog
+    {
+        bool IsActivityLogged { get; }
+    }
 }

+ 17 - 0
MediaBrowser.Controller/Activity/IActivityManager.cs

@@ -0,0 +1,17 @@
+using MediaBrowser.Model.Activity;
+using MediaBrowser.Model.Events;
+using MediaBrowser.Model.Querying;
+using System;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Activity
+{
+    public interface IActivityManager
+    {
+        event EventHandler<GenericEventArgs<ActivityLogEntry>> EntryCreated;
+
+        Task Create(ActivityLogEntry entry);
+
+        QueryResult<ActivityLogEntry> GetActivityLogEntries(int? startIndex, int? limit);
+    }
+}

+ 13 - 0
MediaBrowser.Controller/Activity/IActivityRepository.cs

@@ -0,0 +1,13 @@
+using MediaBrowser.Model.Activity;
+using MediaBrowser.Model.Querying;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Activity
+{
+    public interface IActivityRepository
+    {
+        Task Create(ActivityLogEntry entry);
+
+        QueryResult<ActivityLogEntry> GetActivityLogEntries(int? startIndex, int? limit);
+    }
+}

+ 0 - 7
MediaBrowser.Controller/Configuration/IServerConfigurationManager.cs

@@ -1,7 +1,5 @@
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Model.Configuration;
-using MediaBrowser.Model.Events;
-using System;
 
 namespace MediaBrowser.Controller.Configuration
 {
@@ -10,11 +8,6 @@ namespace MediaBrowser.Controller.Configuration
     /// </summary>
     public interface IServerConfigurationManager : IConfigurationManager
     {
-        /// <summary>
-        /// Occurs when [configuration updating].
-        /// </summary>
-        event EventHandler<GenericEventArgs<ServerConfiguration>> ConfigurationUpdating;
-        
         /// <summary>
         /// Gets the application paths.
         /// </summary>

+ 1 - 13
MediaBrowser.Controller/Entities/Movies/BoxSet.cs

@@ -102,17 +102,9 @@ namespace MediaBrowser.Controller.Entities.Movies
             var totalItems = items.Count;
             var percentages = new Dictionary<Guid, double>(totalItems);
 
-            var tasks = new List<Task>();
-
             // Refresh songs
             foreach (var item in items)
             {
-                if (tasks.Count >= 3)
-                {
-                    await Task.WhenAll(tasks).ConfigureAwait(false);
-                    tasks.Clear();
-                }
-
                 cancellationToken.ThrowIfCancellationRequested();
                 var innerProgress = new ActionableProgress<double>();
 
@@ -132,13 +124,9 @@ namespace MediaBrowser.Controller.Entities.Movies
                 });
 
                 // Avoid implicitly captured closure
-                var taskChild = item;
-                tasks.Add(Task.Run(async () => await RefreshItem(taskChild, refreshOptions, innerProgress, cancellationToken).ConfigureAwait(false), cancellationToken));
+                await RefreshItem(item, refreshOptions, innerProgress, cancellationToken).ConfigureAwait(false);
             }
 
-            await Task.WhenAll(tasks).ConfigureAwait(false);
-            tasks.Clear();
-
             // Refresh current item
             await RefreshMetadata(refreshOptions, cancellationToken).ConfigureAwait(false);
 

+ 1 - 0
MediaBrowser.Controller/Library/IUserManager.cs

@@ -31,6 +31,7 @@ namespace MediaBrowser.Controller.Library
 
         event EventHandler<GenericEventArgs<User>> UserCreated;
         event EventHandler<GenericEventArgs<User>> UserConfigurationUpdated;
+        event EventHandler<GenericEventArgs<User>> UserPasswordChanged;
 
         /// <summary>
         /// Updates the configuration.

+ 1 - 1
MediaBrowser.Controller/Library/TVUtils.cs

@@ -269,7 +269,7 @@ namespace MediaBrowser.Controller.Library
 
                 if ((attributes & FileAttributes.Hidden) == FileAttributes.Hidden)
                 {
-                    logger.Debug("Igoring series file or folder marked hidden: {0}", child.FullName);
+                    //logger.Debug("Igoring series file or folder marked hidden: {0}", child.FullName);
                     continue;
                 }
 

+ 5 - 1
MediaBrowser.Controller/MediaBrowser.Controller.csproj

@@ -68,6 +68,8 @@
     <Compile Include="..\SharedVersion.cs">
       <Link>Properties\SharedVersion.cs</Link>
     </Compile>
+    <Compile Include="Activity\IActivityManager.cs" />
+    <Compile Include="Activity\IActivityRepository.cs" />
     <Compile Include="Channels\ChannelFolderItem.cs" />
     <Compile Include="Channels\ChannelItemInfo.cs" />
     <Compile Include="Channels\ChannelItemResult.cs" />
@@ -241,6 +243,7 @@
     <Compile Include="Security\AuthenticationInfoQuery.cs" />
     <Compile Include="Security\IAuthenticationRepository.cs" />
     <Compile Include="Security\IEncryptionManager.cs" />
+    <Compile Include="Session\AuthenticationRequest.cs" />
     <Compile Include="Subtitles\ISubtitleManager.cs" />
     <Compile Include="Subtitles\ISubtitleProvider.cs" />
     <Compile Include="Providers\ItemIdentifier.cs" />
@@ -320,6 +323,7 @@
     <Compile Include="Sorting\IUserBaseItemComparer.cs" />
     <Compile Include="Providers\BaseItemXmlParser.cs" />
     <Compile Include="Sorting\SortExtensions.cs" />
+    <Compile Include="Subtitles\SubtitleDownloadEventArgs.cs" />
     <Compile Include="Subtitles\SubtitleResponse.cs" />
     <Compile Include="Subtitles\SubtitleSearchRequest.cs" />
     <Compile Include="Sync\ICloudSyncProvider.cs" />
@@ -360,4 +364,4 @@ xcopy "$(TargetPath)" "$(SolutionDir)\Nuget\dlls\" /y /d /r /i
   <Target Name="AfterBuild">
   </Target>
   -->
-</Project>
+</Project>

+ 0 - 12
MediaBrowser.Controller/Notifications/INotificationsRepository.cs

@@ -16,10 +16,6 @@ namespace MediaBrowser.Controller.Notifications
         /// </summary>
         event EventHandler<NotificationUpdateEventArgs> NotificationAdded;
         /// <summary>
-        /// Occurs when [notification updated].
-        /// </summary>
-        event EventHandler<NotificationUpdateEventArgs> NotificationUpdated;
-        /// <summary>
         /// Occurs when [notifications marked read].
         /// </summary>
         event EventHandler<NotificationReadEventArgs> NotificationsMarkedRead;
@@ -37,14 +33,6 @@ namespace MediaBrowser.Controller.Notifications
         /// <returns>NotificationResult.</returns>
         NotificationResult GetNotifications(NotificationQuery query);
 
-        /// <summary>
-        /// Gets the notification.
-        /// </summary>
-        /// <param name="id">The id.</param>
-        /// <param name="userId">The user id.</param>
-        /// <returns>Notification.</returns>
-        Notification GetNotification(string id, string userId);
-
         /// <summary>
         /// Adds the notification.
         /// </summary>

+ 14 - 0
MediaBrowser.Controller/Session/AuthenticationRequest.cs

@@ -0,0 +1,14 @@
+
+namespace MediaBrowser.Controller.Session
+{
+    public class AuthenticationRequest
+    {
+        public string Username { get; set; }
+        public string Password { get; set; }
+        public string App { get; set; }
+        public string AppVersion { get; set; }
+        public string DeviceId { get; set; }
+        public string DeviceName { get; set; }
+        public string RemoteEndPoint { get; set; }
+    }
+}

+ 13 - 15
MediaBrowser.Controller/Session/ISessionManager.cs

@@ -1,6 +1,7 @@
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Events;
 using MediaBrowser.Model.Session;
 using MediaBrowser.Model.Users;
 using System;
@@ -46,6 +47,16 @@ namespace MediaBrowser.Controller.Session
         /// Occurs when [capabilities changed].
         /// </summary>
         event EventHandler<SessionEventArgs> CapabilitiesChanged;
+
+        /// <summary>
+        /// Occurs when [authentication failed].
+        /// </summary>
+        event EventHandler<GenericEventArgs<AuthenticationRequest>> AuthenticationFailed;
+
+        /// <summary>
+        /// Occurs when [authentication succeeded].
+        /// </summary>
+        event EventHandler<GenericEventArgs<AuthenticationRequest>> AuthenticationSucceeded;
         
         /// <summary>
         /// Gets the sessions.
@@ -211,23 +222,10 @@ namespace MediaBrowser.Controller.Session
         /// <summary>
         /// Authenticates the new session.
         /// </summary>
-        /// <param name="username">The username.</param>
-        /// <param name="password">The password.</param>
-        /// <param name="clientType">Type of the client.</param>
-        /// <param name="appVersion">The application version.</param>
-        /// <param name="deviceId">The device identifier.</param>
-        /// <param name="deviceName">Name of the device.</param>
-        /// <param name="remoteEndPoint">The remote end point.</param>
+        /// <param name="request">The request.</param>
         /// <param name="isLocal">if set to <c>true</c> [is local].</param>
         /// <returns>Task{SessionInfo}.</returns>
-        Task<AuthenticationResult> AuthenticateNewSession(string username, 
-            string password, 
-            string clientType, 
-            string appVersion, 
-            string deviceId, 
-            string deviceName, 
-            string remoteEndPoint,
-            bool isLocal);
+        Task<AuthenticationResult> AuthenticateNewSession(AuthenticationRequest request, bool isLocal);
 
         /// <summary>
         /// Reports the capabilities.

+ 14 - 3
MediaBrowser.Controller/Subtitles/ISubtitleManager.cs

@@ -1,5 +1,6 @@
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Model.Providers;
+using System;
 using System.Collections.Generic;
 using System.Threading;
 using System.Threading.Tasks;
@@ -8,6 +9,16 @@ namespace MediaBrowser.Controller.Subtitles
 {
     public interface ISubtitleManager
     {
+        /// <summary>
+        /// Occurs when [subtitle download failure].
+        /// </summary>
+        event EventHandler<SubtitleDownloadFailureEventArgs> SubtitleDownloadFailure;
+
+        /// <summary>
+        /// Occurs when [subtitles downloaded].
+        /// </summary>
+        event EventHandler<SubtitleDownloadEventArgs> SubtitlesDownloaded;
+
         /// <summary>
         /// Adds the parts.
         /// </summary>
@@ -31,7 +42,7 @@ namespace MediaBrowser.Controller.Subtitles
         /// <param name="request">The request.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <returns>Task{IEnumerable{RemoteSubtitleInfo}}.</returns>
-        Task<IEnumerable<RemoteSubtitleInfo>> SearchSubtitles(SubtitleSearchRequest request, 
+        Task<IEnumerable<RemoteSubtitleInfo>> SearchSubtitles(SubtitleSearchRequest request,
             CancellationToken cancellationToken);
 
         /// <summary>
@@ -41,8 +52,8 @@ namespace MediaBrowser.Controller.Subtitles
         /// <param name="subtitleId">The subtitle identifier.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <returns>Task.</returns>
-        Task DownloadSubtitles(Video video, 
-            string subtitleId, 
+        Task DownloadSubtitles(Video video,
+            string subtitleId,
             CancellationToken cancellationToken);
 
         /// <summary>

+ 27 - 0
MediaBrowser.Controller/Subtitles/SubtitleDownloadEventArgs.cs

@@ -0,0 +1,27 @@
+using System;
+using MediaBrowser.Controller.Entities;
+
+namespace MediaBrowser.Controller.Subtitles
+{
+    public class SubtitleDownloadEventArgs
+    {
+        public BaseItem Item { get; set; }
+
+        public string Format { get; set; }
+
+        public string Language { get; set; }
+
+        public bool IsForced { get; set; }
+
+        public string Provider { get; set; }
+    }
+
+    public class SubtitleDownloadFailureEventArgs
+    {
+        public BaseItem Item { get; set; }
+
+        public string Provider { get; set; }
+
+        public Exception Exception { get; set; }
+    }
+}

+ 2 - 2
MediaBrowser.LocalMetadata/BaseXmlProvider.cs

@@ -27,7 +27,7 @@ namespace MediaBrowser.LocalMetadata
 
             var path = file.FullName;
 
-            await XmlProviderUtils.XmlParsingResourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
+            //await XmlProviderUtils.XmlParsingResourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
 
             try
             {
@@ -46,7 +46,7 @@ namespace MediaBrowser.LocalMetadata
             }
             finally
             {
-                XmlProviderUtils.XmlParsingResourcePool.Release();
+                //XmlProviderUtils.XmlParsingResourcePool.Release();
             }
 
             return result;

+ 6 - 3
MediaBrowser.Model.Portable/MediaBrowser.Model.Portable.csproj

@@ -83,6 +83,9 @@
     </Reference>
   </ItemGroup>
   <ItemGroup>
+    <Compile Include="..\MediaBrowser.Model\Activity\ActivityLogEntry.cs">
+      <Link>Activity\ActivityLogEntry.cs</Link>
+    </Compile>
     <Compile Include="..\MediaBrowser.Model\ApiClient\ApiClientExtensions.cs">
       <Link>ApiClient\ApiClientExtensions.cs</Link>
     </Compile>
@@ -173,9 +176,6 @@
     <Compile Include="..\MediaBrowser.Model\Configuration\ServerConfiguration.cs">
       <Link>Configuration\ServerConfiguration.cs</Link>
     </Compile>
-    <Compile Include="..\MediaBrowser.Model\Configuration\SubtitleOptions.cs">
-      <Link>Configuration\SubtitleOptions.cs</Link>
-    </Compile>
     <Compile Include="..\MediaBrowser.Model\Configuration\SubtitlePlaybackMode.cs">
       <Link>Configuration\SubtitlePlaybackMode.cs</Link>
     </Compile>
@@ -728,6 +728,9 @@
     <Compile Include="..\MediaBrowser.Model\Providers\RemoteSubtitleInfo.cs">
       <Link>Providers\RemoteSubtitleInfo.cs</Link>
     </Compile>
+    <Compile Include="..\MediaBrowser.Model\Providers\SubtitleOptions.cs">
+      <Link>Providers\SubtitleOptions.cs</Link>
+    </Compile>
     <Compile Include="..\MediaBrowser.Model\Querying\AllThemeMediaResult.cs">
       <Link>Querying\AllThemeMediaResult.cs</Link>
     </Compile>

+ 6 - 3
MediaBrowser.Model.net35/MediaBrowser.Model.net35.csproj

@@ -52,6 +52,9 @@
     <Reference Include="System.Xml" />
   </ItemGroup>
   <ItemGroup>
+    <Compile Include="..\mediabrowser.model\activity\ActivityLogEntry.cs">
+      <Link>Activity\ActivityLogEntry.cs</Link>
+    </Compile>
     <Compile Include="..\MediaBrowser.Model\ApiClient\GeneralCommandEventArgs.cs">
       <Link>ApiClient\GeneralCommandEventArgs.cs</Link>
     </Compile>
@@ -136,9 +139,6 @@
     <Compile Include="..\MediaBrowser.Model\Configuration\ServerConfiguration.cs">
       <Link>Configuration\ServerConfiguration.cs</Link>
     </Compile>
-    <Compile Include="..\MediaBrowser.Model\Configuration\SubtitleOptions.cs">
-      <Link>Configuration\SubtitleOptions.cs</Link>
-    </Compile>
     <Compile Include="..\MediaBrowser.Model\Configuration\SubtitlePlaybackMode.cs">
       <Link>Configuration\SubtitlePlaybackMode.cs</Link>
     </Compile>
@@ -685,6 +685,9 @@
     <Compile Include="..\MediaBrowser.Model\Providers\RemoteSubtitleInfo.cs">
       <Link>Providers\RemoteSubtitleInfo.cs</Link>
     </Compile>
+    <Compile Include="..\MediaBrowser.Model\Providers\SubtitleOptions.cs">
+      <Link>Providers\SubtitleOptions.cs</Link>
+    </Compile>
     <Compile Include="..\MediaBrowser.Model\Querying\AllThemeMediaResult.cs">
       <Link>Querying\AllThemeMediaResult.cs</Link>
     </Compile>

+ 62 - 0
MediaBrowser.Model/Activity/ActivityLogEntry.cs

@@ -0,0 +1,62 @@
+using MediaBrowser.Model.Logging;
+using System;
+
+namespace MediaBrowser.Model.Activity
+{
+    public class ActivityLogEntry
+    {
+        /// <summary>
+        /// Gets or sets the identifier.
+        /// </summary>
+        /// <value>The identifier.</value>
+        public string Id { get; set; }
+
+        /// <summary>
+        /// Gets or sets the name.
+        /// </summary>
+        /// <value>The name.</value>
+        public string Name { get; set; }
+
+        /// <summary>
+        /// Gets or sets the overview.
+        /// </summary>
+        /// <value>The overview.</value>
+        public string Overview { get; set; }
+
+        /// <summary>
+        /// Gets or sets the short overview.
+        /// </summary>
+        /// <value>The short overview.</value>
+        public string ShortOverview { get; set; }
+
+        /// <summary>
+        /// Gets or sets the type.
+        /// </summary>
+        /// <value>The type.</value>
+        public string Type { get; set; }
+
+        /// <summary>
+        /// Gets or sets the item identifier.
+        /// </summary>
+        /// <value>The item identifier.</value>
+        public string ItemId { get; set; }
+
+        /// <summary>
+        /// Gets or sets the date.
+        /// </summary>
+        /// <value>The date.</value>
+        public DateTime Date { get; set; }
+
+        /// <summary>
+        /// Gets or sets the user identifier.
+        /// </summary>
+        /// <value>The user identifier.</value>
+        public string UserId { get; set; }
+
+        /// <summary>
+        /// Gets or sets the log severity.
+        /// </summary>
+        /// <value>The log severity.</value>
+        public LogSeverity Severity { get; set; }
+    }
+}

+ 1 - 1
MediaBrowser.Model/ApiClient/IApiClient.cs

@@ -623,7 +623,7 @@ namespace MediaBrowser.Model.ApiClient
         Task ReportPlaybackStoppedAsync(PlaybackStopInfo info);
 
         /// <summary>
-        /// Instructs antoher client to browse to a library item.
+        /// Instructs another client to browse to a library item.
         /// </summary>
         /// <param name="sessionId">The session id.</param>
         /// <param name="itemId">The id of the item to browse to.</param>

+ 2 - 3
MediaBrowser.Model/Channels/ChannelItemQuery.cs

@@ -1,6 +1,5 @@
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Querying;
-using System.Collections.Generic;
 
 namespace MediaBrowser.Model.Channels
 {
@@ -39,13 +38,13 @@ namespace MediaBrowser.Model.Channels
         public SortOrder? SortOrder { get; set; }
         public string[] SortBy { get; set; }
         public ItemFilter[] Filters { get; set; }
-        public List<ItemFields> Fields { get; set; }
+        public ItemFields[] Fields { get; set; }
 
         public ChannelItemQuery()
         {
             Filters = new ItemFilter[] { };
             SortBy = new string[] { };
-            Fields = new List<ItemFields>();
+            Fields = new ItemFields[] { };
         }
     }
 

+ 1 - 2
MediaBrowser.Model/Configuration/ServerConfiguration.cs

@@ -2,6 +2,7 @@
 using MediaBrowser.Model.FileOrganization;
 using MediaBrowser.Model.LiveTv;
 using MediaBrowser.Model.Notifications;
+using MediaBrowser.Model.Providers;
 
 namespace MediaBrowser.Model.Configuration
 {
@@ -285,8 +286,6 @@ namespace MediaBrowser.Model.Configuration
 
                 new MetadataOptions(0, 1280) {ItemType = "Season"}
             };
-
-            SubtitleOptions = new SubtitleOptions();
         }
     }
 }

+ 16 - 0
MediaBrowser.Model/Events/GenericEventArgs.cs

@@ -13,5 +13,21 @@ namespace MediaBrowser.Model.Events
         /// </summary>
         /// <value>The argument.</value>
         public T Argument { get; set; }
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="GenericEventArgs{T}"/> class.
+        /// </summary>
+        /// <param name="arg">The argument.</param>
+        public GenericEventArgs(T arg)
+        {
+            Argument = arg;
+        }
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="GenericEventArgs{T}"/> class.
+        /// </summary>
+        public GenericEventArgs()
+        {
+        }
     }
 }

+ 3 - 2
MediaBrowser.Model/MediaBrowser.Model.csproj

@@ -59,6 +59,7 @@
     <Compile Include="..\SharedVersion.cs">
       <Link>Properties\SharedVersion.cs</Link>
     </Compile>
+    <Compile Include="Activity\ActivityLogEntry.cs" />
     <Compile Include="ApiClient\HttpResponseEventArgs.cs" />
     <Compile Include="ApiClient\IApiClient.cs" />
     <Compile Include="ApiClient\ApiClientExtensions.cs" />
@@ -99,7 +100,7 @@
     <Compile Include="Configuration\PathSubstitution.cs" />
     <Compile Include="Notifications\SendToUserType.cs" />
     <Compile Include="Configuration\ServerConfiguration.cs" />
-    <Compile Include="Configuration\SubtitleOptions.cs" />
+    <Compile Include="Providers\SubtitleOptions.cs" />
     <Compile Include="Configuration\UnratedItem.cs" />
     <Compile Include="Dlna\AudioOptions.cs" />
     <Compile Include="Dlna\CodecProfile.cs" />
@@ -378,4 +379,4 @@ xcopy "$(TargetPath)" "$(SolutionDir)\Nuget\dlls\net45\" /y /d /r /i
   <Target Name="AfterBuild">
   </Target>
   -->
-</Project>
+</Project>

+ 1 - 1
MediaBrowser.Model/Configuration/SubtitleOptions.cs → MediaBrowser.Model/Providers/SubtitleOptions.cs

@@ -1,4 +1,4 @@
-namespace MediaBrowser.Model.Configuration
+namespace MediaBrowser.Model.Providers
 {
     public class SubtitleOptions
     {

+ 6 - 0
MediaBrowser.Model/Tasks/TaskResult.cs

@@ -42,5 +42,11 @@ namespace MediaBrowser.Model.Tasks
         /// </summary>
         /// <value>The error message.</value>
         public string ErrorMessage { get; set; }
+
+        /// <summary>
+        /// Gets or sets the long error message.
+        /// </summary>
+        /// <value>The long error message.</value>
+        public string LongErrorMessage { get; set; }
     }
 }

+ 2 - 1
MediaBrowser.Providers/MediaBrowser.Providers.csproj

@@ -152,6 +152,7 @@
     <Compile Include="Manager\ProviderUtils.cs" />
     <Compile Include="Studios\StudiosImageProvider.cs" />
     <Compile Include="Studios\StudioMetadataService.cs" />
+    <Compile Include="Subtitles\ConfigurationExtension.cs" />
     <Compile Include="Subtitles\OpenSubtitleDownloader.cs" />
     <Compile Include="Subtitles\SubtitleManager.cs" />
     <Compile Include="TV\EpisodeMetadataService.cs" />
@@ -213,4 +214,4 @@
   <Target Name="AfterBuild">
   </Target>
   -->
-</Project>
+</Project>

+ 14 - 5
MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs

@@ -13,10 +13,12 @@ using MediaBrowser.Controller.MediaEncoding;
 using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Controller.Subtitles;
+using MediaBrowser.Model.Configuration;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.Logging;
 using MediaBrowser.Model.MediaInfo;
+using MediaBrowser.Model.Providers;
 using MediaBrowser.Model.Serialization;
 using System;
 using System.Collections.Generic;
@@ -464,6 +466,11 @@ namespace MediaBrowser.Providers.MediaInfo
             }
         }
 
+        private SubtitleOptions GetOptions()
+        {
+            return _config.GetConfiguration<SubtitleOptions>("subtitles");
+        }
+
         /// <summary>
         /// Adds the external subtitles.
         /// </summary>
@@ -484,9 +491,11 @@ namespace MediaBrowser.Providers.MediaInfo
             var enableSubtitleDownloading = options.MetadataRefreshMode == MetadataRefreshMode.Default ||
                                             options.MetadataRefreshMode == MetadataRefreshMode.FullRefresh;
 
-            if (enableSubtitleDownloading && (_config.Configuration.SubtitleOptions.DownloadEpisodeSubtitles &&
+            var subtitleOptions = GetOptions();
+
+            if (enableSubtitleDownloading && (subtitleOptions.DownloadEpisodeSubtitles &&
                 video is Episode) ||
-                (_config.Configuration.SubtitleOptions.DownloadMovieSubtitles &&
+                (subtitleOptions.DownloadMovieSubtitles &&
                 video is Movie))
             {
                 var downloadedLanguages = await new SubtitleDownloader(_logger,
@@ -494,9 +503,9 @@ namespace MediaBrowser.Providers.MediaInfo
                     .DownloadSubtitles(video,
                     currentStreams,
                     externalSubtitleStreams,
-                    _config.Configuration.SubtitleOptions.SkipIfGraphicalSubtitlesPresent,
-                    _config.Configuration.SubtitleOptions.SkipIfAudioTrackMatches,
-                    _config.Configuration.SubtitleOptions.DownloadLanguages,
+                    subtitleOptions.SkipIfGraphicalSubtitlesPresent,
+                    subtitleOptions.SkipIfAudioTrackMatches,
+                    subtitleOptions.DownloadLanguages,
                     cancellationToken).ConfigureAwait(false);
 
                 // Rescan

+ 20 - 10
MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs

@@ -1,10 +1,12 @@
-using MediaBrowser.Common.ScheduledTasks;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.ScheduledTasks;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Movies;
 using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Subtitles;
+using MediaBrowser.Model.Configuration;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Logging;
 using System;
@@ -12,6 +14,7 @@ using System.Collections.Generic;
 using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
+using MediaBrowser.Model.Providers;
 
 namespace MediaBrowser.Providers.MediaInfo
 {
@@ -45,8 +48,15 @@ namespace MediaBrowser.Providers.MediaInfo
             get { return "Library"; }
         }
 
+        private SubtitleOptions GetOptions()
+        {
+            return _config.GetConfiguration<SubtitleOptions>("subtitles");
+        }
+
         public async Task Execute(CancellationToken cancellationToken, IProgress<double> progress)
         {
+            var options = GetOptions();
+
             var videos = _libraryManager.RootFolder
                 .RecursiveChildren
                 .OfType<Video>()
@@ -57,9 +67,9 @@ namespace MediaBrowser.Providers.MediaInfo
                         return false;
                     }
 
-                    return (_config.Configuration.SubtitleOptions.DownloadEpisodeSubtitles &&
+                    return (options.DownloadEpisodeSubtitles &&
                             i is Episode) ||
-                           (_config.Configuration.SubtitleOptions.DownloadMovieSubtitles &&
+                           (options.DownloadMovieSubtitles &&
                             i is Movie);
                 })
                 .ToList();
@@ -70,7 +80,7 @@ namespace MediaBrowser.Providers.MediaInfo
             {
                 try
                 {
-                    await DownloadSubtitles(video, cancellationToken).ConfigureAwait(false);
+                    await DownloadSubtitles(video, options, cancellationToken).ConfigureAwait(false);
                 }
                 catch (Exception ex)
                 {
@@ -86,11 +96,11 @@ namespace MediaBrowser.Providers.MediaInfo
             }
         }
 
-        private async Task DownloadSubtitles(Video video, CancellationToken cancellationToken)
+        private async Task DownloadSubtitles(Video video, SubtitleOptions options, CancellationToken cancellationToken)
         {
-            if ((_config.Configuration.SubtitleOptions.DownloadEpisodeSubtitles &&
+            if ((options.DownloadEpisodeSubtitles &&
                 video is Episode) ||
-                (_config.Configuration.SubtitleOptions.DownloadMovieSubtitles &&
+                (options.DownloadMovieSubtitles &&
                 video is Movie))
             {
                 var mediaStreams = video.GetMediaSources(false).First().MediaStreams;
@@ -103,9 +113,9 @@ namespace MediaBrowser.Providers.MediaInfo
                     .DownloadSubtitles(video,
                     currentStreams,
                     externalSubtitleStreams,
-                    _config.Configuration.SubtitleOptions.SkipIfGraphicalSubtitlesPresent,
-                    _config.Configuration.SubtitleOptions.SkipIfAudioTrackMatches,
-                    _config.Configuration.SubtitleOptions.DownloadLanguages,
+                    options.SkipIfGraphicalSubtitlesPresent,
+                    options.SkipIfAudioTrackMatches,
+                    options.DownloadLanguages,
                     cancellationToken).ConfigureAwait(false);
 
                 // Rescan

+ 29 - 0
MediaBrowser.Providers/Subtitles/ConfigurationExtension.cs

@@ -0,0 +1,29 @@
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Model.Providers;
+using System.Collections.Generic;
+
+namespace MediaBrowser.Providers.Subtitles
+{
+    public static class ConfigurationExtension
+    {
+        public static SubtitleOptions GetSubtitleConfiguration(this IConfigurationManager manager)
+        {
+            return manager.GetConfiguration<SubtitleOptions>("subtitles");
+        }
+    }
+
+    public class SubtitleConfigurationFactory : IConfigurationFactory
+    {
+        public IEnumerable<ConfigurationStore> GetConfigurations()
+        {
+            return new List<ConfigurationStore>
+            {
+                new ConfigurationStore
+                {
+                    Key = "subtitles",
+                    ConfigurationType = typeof (SubtitleOptions)
+                }
+            };
+        }
+    }
+}

+ 24 - 11
MediaBrowser.Providers/Subtitles/OpenSubtitleDownloader.cs

@@ -1,4 +1,5 @@
-using MediaBrowser.Common.Extensions;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Providers;
@@ -6,7 +7,6 @@ using MediaBrowser.Controller.Security;
 using MediaBrowser.Controller.Subtitles;
 using MediaBrowser.Model.Configuration;
 using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Events;
 using MediaBrowser.Model.Logging;
 using MediaBrowser.Model.Providers;
 using OpenSubtitlesHandler;
@@ -45,16 +45,21 @@ namespace MediaBrowser.Providers.Subtitles
             _config = config;
             _encryption = encryption;
 
-            _config.ConfigurationUpdating += _config_ConfigurationUpdating;
+            _config.NamedConfigurationUpdating += _config_NamedConfigurationUpdating;
 
             // Reset the count every 24 hours
             _dailyTimer = new Timer(state => _dailyDownloadCount = 0, null, TimeSpan.FromHours(24), TimeSpan.FromHours(24));
         }
 
         private const string PasswordHashPrefix = "h:";
-        void _config_ConfigurationUpdating(object sender, GenericEventArgs<ServerConfiguration> e)
+        void _config_NamedConfigurationUpdating(object sender, ConfigurationUpdateEventArgs e)
         {
-            var options = e.Argument.SubtitleOptions;
+            if (!string.Equals(e.Key, "subtitles", StringComparison.OrdinalIgnoreCase))
+            {
+                return;
+            }
+
+            var options = (SubtitleOptions)e.NewConfiguration;
 
             if (options != null &&
                 !string.IsNullOrWhiteSpace(options.OpenSubtitlesPasswordHash) &&
@@ -85,12 +90,19 @@ namespace MediaBrowser.Providers.Subtitles
             get { return "Open Subtitles"; }
         }
 
+        private SubtitleOptions GetOptions()
+        {
+            return _config.GetSubtitleConfiguration();
+        }
+
         public IEnumerable<VideoContentType> SupportedMediaTypes
         {
             get
             {
-                if (string.IsNullOrWhiteSpace(_config.Configuration.SubtitleOptions.OpenSubtitlesUsername) ||
-                    string.IsNullOrWhiteSpace(_config.Configuration.SubtitleOptions.OpenSubtitlesPasswordHash))
+                var options = GetOptions();
+
+                if (string.IsNullOrWhiteSpace(options.OpenSubtitlesUsername) ||
+                    string.IsNullOrWhiteSpace(options.OpenSubtitlesPasswordHash))
                 {
                     return new VideoContentType[] { };
                 }
@@ -101,10 +113,11 @@ namespace MediaBrowser.Providers.Subtitles
 
         public Task<SubtitleResponse> GetSubtitles(string id, CancellationToken cancellationToken)
         {
-            return GetSubtitlesInternal(id, cancellationToken);
+            return GetSubtitlesInternal(id, GetOptions(), cancellationToken);
         }
 
         private async Task<SubtitleResponse> GetSubtitlesInternal(string id,
+            SubtitleOptions options,
             CancellationToken cancellationToken)
         {
             if (string.IsNullOrWhiteSpace(id))
@@ -113,7 +126,7 @@ namespace MediaBrowser.Providers.Subtitles
             }
 
             if (_dailyDownloadCount >= MaxDownloadsPerDay &&
-                !_config.Configuration.SubtitleOptions.IsOpenSubtitleVipAccount)
+                !options.IsOpenSubtitleVipAccount)
             {
                 throw new InvalidOperationException("Open Subtitle's daily download limit has been exceeded. Please try again tomorrow.");
             }
@@ -167,7 +180,7 @@ namespace MediaBrowser.Providers.Subtitles
                 return;
             }
 
-            var options = _config.Configuration.SubtitleOptions ?? new SubtitleOptions();
+            var options = GetOptions();
 
             var user = options.OpenSubtitlesUsername ?? string.Empty;
             var password = DecryptPassword(options.OpenSubtitlesPasswordHash);
@@ -289,7 +302,7 @@ namespace MediaBrowser.Providers.Subtitles
 
         public void Dispose()
         {
-            _config.ConfigurationUpdating -= _config_ConfigurationUpdating;
+            _config.NamedConfigurationUpdating -= _config_NamedConfigurationUpdating;
 
             if (_dailyTimer != null)
             {

+ 51 - 18
MediaBrowser.Providers/Subtitles/SubtitleManager.cs

@@ -1,4 +1,5 @@
-using MediaBrowser.Common.Extensions;
+using MediaBrowser.Common.Events;
+using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.IO;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Movies;
@@ -28,6 +29,9 @@ namespace MediaBrowser.Providers.Subtitles
         private readonly ILibraryManager _libraryManager;
         private readonly IItemRepository _itemRepo;
 
+        public event EventHandler<SubtitleDownloadEventArgs> SubtitlesDownloaded;
+        public event EventHandler<SubtitleDownloadFailureEventArgs> SubtitleDownloadFailure;
+
         public SubtitleManager(ILogger logger, IFileSystem fileSystem, ILibraryMonitor monitor, ILibraryManager libraryManager, IItemRepository itemRepo)
         {
             _logger = logger;
@@ -100,35 +104,63 @@ namespace MediaBrowser.Providers.Subtitles
             string subtitleId,
             CancellationToken cancellationToken)
         {
-            var response = await GetRemoteSubtitles(subtitleId, cancellationToken).ConfigureAwait(false);
+            var parts = subtitleId.Split(new[] { '_' }, 2);
+            var provider = GetProvider(parts.First());
 
-            using (var stream = response.Stream)
+            try
             {
-                var savePath = Path.Combine(Path.GetDirectoryName(video.Path),
-                    _fileSystem.GetFileNameWithoutExtension(video.Path) + "." + response.Language.ToLower());
+                var response = await GetRemoteSubtitles(subtitleId, cancellationToken).ConfigureAwait(false);
 
-                if (response.IsForced)
+                using (var stream = response.Stream)
                 {
-                    savePath += ".forced";
-                }
+                    var savePath = Path.Combine(Path.GetDirectoryName(video.Path),
+                        _fileSystem.GetFileNameWithoutExtension(video.Path) + "." + response.Language.ToLower());
 
-                savePath += "." + response.Format.ToLower();
+                    if (response.IsForced)
+                    {
+                        savePath += ".forced";
+                    }
 
-                _logger.Info("Saving subtitles to {0}", savePath);
+                    savePath += "." + response.Format.ToLower();
 
-                _monitor.ReportFileSystemChangeBeginning(savePath);
+                    _logger.Info("Saving subtitles to {0}", savePath);
 
-                try
-                {
-                    using (var fs = _fileSystem.GetFileStream(savePath, FileMode.Create, FileAccess.Write, FileShare.Read, true))
+                    _monitor.ReportFileSystemChangeBeginning(savePath);
+
+                    try
+                    {
+                        using (var fs = _fileSystem.GetFileStream(savePath, FileMode.Create, FileAccess.Write, FileShare.Read, true))
+                        {
+                            await stream.CopyToAsync(fs).ConfigureAwait(false);
+                        }
+
+                        EventHelper.FireEventIfNotNull(SubtitlesDownloaded, this, new SubtitleDownloadEventArgs
+                        {
+                            Item = video,
+                            Format = response.Format,
+                            Language = response.Language,
+                            IsForced = response.IsForced,
+                            Provider = provider.Name
+
+                        }, _logger);
+                    }
+                    finally
                     {
-                        await stream.CopyToAsync(fs).ConfigureAwait(false);
+                        _monitor.ReportFileSystemChangeComplete(savePath, false);
                     }
                 }
-                finally
+            }
+            catch (Exception ex)
+            {
+                EventHelper.FireEventIfNotNull(SubtitleDownloadFailure, this, new SubtitleDownloadFailureEventArgs
                 {
-                    _monitor.ReportFileSystemChangeComplete(savePath, false);
-                }
+                    Item = video,
+                    Exception = ex,
+                    Provider = provider.Name
+
+                }, _logger);
+                
+                throw;
             }
         }
 
@@ -267,5 +299,6 @@ namespace MediaBrowser.Providers.Subtitles
                 Id = GetProviderId(i.Name)
             });
         }
+
     }
 }

+ 40 - 0
MediaBrowser.Server.Implementations/Activity/ActivityManager.cs

@@ -0,0 +1,40 @@
+using MediaBrowser.Common.Events;
+using MediaBrowser.Controller.Activity;
+using MediaBrowser.Model.Activity;
+using MediaBrowser.Model.Events;
+using MediaBrowser.Model.Logging;
+using MediaBrowser.Model.Querying;
+using System;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Server.Implementations.Activity
+{
+    public class ActivityManager : IActivityManager
+    {
+        public event EventHandler<GenericEventArgs<ActivityLogEntry>> EntryCreated;
+        
+        private readonly IActivityRepository _repo;
+        private readonly ILogger _logger;
+
+        public ActivityManager(ILogger logger, IActivityRepository repo)
+        {
+            _logger = logger;
+            _repo = repo;
+        }
+
+        public async Task Create(ActivityLogEntry entry)
+        {
+            entry.Id = Guid.NewGuid().ToString("N");
+            entry.Date = DateTime.UtcNow;
+
+            await _repo.Create(entry).ConfigureAwait(false);
+
+            EventHelper.FireEventIfNotNull(EntryCreated, this, new GenericEventArgs<ActivityLogEntry>(entry), _logger);
+        }
+
+        public QueryResult<ActivityLogEntry> GetActivityLogEntries(int? startIndex, int? limit)
+        {
+            return _repo.GetActivityLogEntries(startIndex, limit);
+        }
+    }
+}

+ 293 - 0
MediaBrowser.Server.Implementations/Activity/ActivityRepository.cs

@@ -0,0 +1,293 @@
+using MediaBrowser.Controller;
+using MediaBrowser.Controller.Activity;
+using MediaBrowser.Model.Activity;
+using MediaBrowser.Model.Logging;
+using MediaBrowser.Model.Querying;
+using MediaBrowser.Server.Implementations.Persistence;
+using System;
+using System.Collections.Generic;
+using System.Data;
+using System.Globalization;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Server.Implementations.Activity
+{
+    public class ActivityRepository : IActivityRepository, IDisposable
+    {
+        private IDbConnection _connection;
+        private readonly ILogger _logger;
+        private readonly SemaphoreSlim _writeLock = new SemaphoreSlim(1, 1);
+        private readonly IServerApplicationPaths _appPaths;
+        private readonly CultureInfo _usCulture = new CultureInfo("en-US");
+
+        private IDbCommand _saveActivityCommand;
+
+        public ActivityRepository(ILogger logger, IServerApplicationPaths appPaths)
+        {
+            _logger = logger;
+            _appPaths = appPaths;
+        }
+
+        public async Task Initialize()
+        {
+            var dbFile = Path.Combine(_appPaths.DataPath, "activitylog.db");
+
+            _connection = await SqliteExtensions.ConnectToDb(dbFile, _logger).ConfigureAwait(false);
+
+            string[] queries = {
+
+                                "create table if not exists ActivityLogEntries (Id GUID PRIMARY KEY, Name TEXT, Overview TEXT, ShortOverview TEXT, Type TEXT, ItemId TEXT, UserId TEXT, DateCreated DATETIME, LogSeverity TEXT)",
+                                "create index if not exists idx_ActivityLogEntries on ActivityLogEntries(Id)",
+
+                                //pragmas
+                                "pragma temp_store = memory",
+
+                                "pragma shrink_memory"
+                               };
+
+            _connection.RunQueries(queries, _logger);
+
+            PrepareStatements();
+        }
+
+        private void PrepareStatements()
+        {
+            _saveActivityCommand = _connection.CreateCommand();
+            _saveActivityCommand.CommandText = "replace into ActivityLogEntries (Id, Name, Overview, ShortOverview, Type, ItemId, UserId, DateCreated, LogSeverity) values (@Id, @Name, @Overview, @ShortOverview, @Type, @ItemId, @UserId, @DateCreated, @LogSeverity)";
+
+            _saveActivityCommand.Parameters.Add(_saveActivityCommand, "@Id");
+            _saveActivityCommand.Parameters.Add(_saveActivityCommand, "@Name");
+            _saveActivityCommand.Parameters.Add(_saveActivityCommand, "@Overview");
+            _saveActivityCommand.Parameters.Add(_saveActivityCommand, "@ShortOverview");
+            _saveActivityCommand.Parameters.Add(_saveActivityCommand, "@Type");
+            _saveActivityCommand.Parameters.Add(_saveActivityCommand, "@ItemId");
+            _saveActivityCommand.Parameters.Add(_saveActivityCommand, "@UserId");
+            _saveActivityCommand.Parameters.Add(_saveActivityCommand, "@DateCreated");
+            _saveActivityCommand.Parameters.Add(_saveActivityCommand, "@LogSeverity");
+        }
+
+        private const string BaseActivitySelectText = "select Id, Name, Overview, ShortOverview, Type, ItemId, UserId, DateCreated, LogSeverity from ActivityLogEntries";
+
+        public Task Create(ActivityLogEntry entry)
+        {
+            return Update(entry);
+        }
+
+        public async Task Update(ActivityLogEntry entry)
+        {
+            if (entry == null)
+            {
+                throw new ArgumentNullException("entry");
+            }
+
+            await _writeLock.WaitAsync().ConfigureAwait(false);
+
+            IDbTransaction transaction = null;
+
+            try
+            {
+                transaction = _connection.BeginTransaction();
+
+                var index = 0;
+
+                _saveActivityCommand.GetParameter(index++).Value = new Guid(entry.Id);
+                _saveActivityCommand.GetParameter(index++).Value = entry.Name;
+                _saveActivityCommand.GetParameter(index++).Value = entry.Overview;
+                _saveActivityCommand.GetParameter(index++).Value = entry.ShortOverview;
+                _saveActivityCommand.GetParameter(index++).Value = entry.Type;
+                _saveActivityCommand.GetParameter(index++).Value = entry.ItemId;
+                _saveActivityCommand.GetParameter(index++).Value = entry.UserId;
+                _saveActivityCommand.GetParameter(index++).Value = entry.Date;
+                _saveActivityCommand.GetParameter(index++).Value = entry.Severity.ToString();
+
+                _saveActivityCommand.Transaction = transaction;
+
+                _saveActivityCommand.ExecuteNonQuery();
+
+                transaction.Commit();
+            }
+            catch (OperationCanceledException)
+            {
+                if (transaction != null)
+                {
+                    transaction.Rollback();
+                }
+
+                throw;
+            }
+            catch (Exception e)
+            {
+                _logger.ErrorException("Failed to save record:", e);
+
+                if (transaction != null)
+                {
+                    transaction.Rollback();
+                }
+
+                throw;
+            }
+            finally
+            {
+                if (transaction != null)
+                {
+                    transaction.Dispose();
+                }
+
+                _writeLock.Release();
+            }
+        }
+
+        public QueryResult<ActivityLogEntry> GetActivityLogEntries(int? startIndex, int? limit)
+        {
+            using (var cmd = _connection.CreateCommand())
+            {
+                cmd.CommandText = BaseActivitySelectText;
+
+                var whereClauses = new List<string>();
+
+                if (startIndex.HasValue && startIndex.Value > 0)
+                {
+                    whereClauses.Add(string.Format("Id NOT IN (SELECT Id FROM ActivityLogEntries ORDER BY DateCreated DESC LIMIT {0})",
+                        startIndex.Value.ToString(_usCulture)));
+                }
+
+                if (whereClauses.Count > 0)
+                {
+                    cmd.CommandText += " where " + string.Join(" AND ", whereClauses.ToArray());
+                }
+
+                cmd.CommandText += " ORDER BY DateCreated DESC";
+
+                if (limit.HasValue)
+                {
+                    cmd.CommandText += " LIMIT " + limit.Value.ToString(_usCulture);
+                }
+
+                cmd.CommandText += "; select count (Id) from ActivityLogEntries";
+
+                var list = new List<ActivityLogEntry>();
+                var count = 0;
+
+                using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess))
+                {
+                    while (reader.Read())
+                    {
+                        list.Add(GetEntry(reader));
+                    }
+
+                    if (reader.NextResult() && reader.Read())
+                    {
+                        count = reader.GetInt32(0);
+                    }
+                }
+
+                return new QueryResult<ActivityLogEntry>()
+                {
+                    Items = list.ToArray(),
+                    TotalRecordCount = count
+                };
+            }
+        }
+
+        private ActivityLogEntry GetEntry(IDataReader reader)
+        {
+            var index = 0;
+
+            var info = new ActivityLogEntry
+            {
+                Id = reader.GetGuid(index).ToString("N")
+            };
+
+            index++;
+            if (!reader.IsDBNull(index))
+            {
+                info.Name = reader.GetString(index);
+            }
+
+            index++;
+            if (!reader.IsDBNull(index))
+            {
+                info.Overview = reader.GetString(index);
+            }
+
+            index++;
+            if (!reader.IsDBNull(index))
+            {
+                info.ShortOverview = reader.GetString(index);
+            }
+
+            index++;
+            if (!reader.IsDBNull(index))
+            {
+                info.Type = reader.GetString(index);
+            }
+
+            index++;
+            if (!reader.IsDBNull(index))
+            {
+                info.ItemId = reader.GetString(index);
+            }
+
+            index++;
+            if (!reader.IsDBNull(index))
+            {
+                info.UserId = reader.GetString(index);
+            }
+
+            index++;
+            info.Date = reader.GetDateTime(index).ToUniversalTime();
+
+            index++;
+            if (!reader.IsDBNull(index))
+            {
+                info.Severity = (LogSeverity)Enum.Parse(typeof(LogSeverity), reader.GetString(index), true);
+            }
+
+            return info;
+        }
+
+        /// <summary>
+        /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
+        /// </summary>
+        public void Dispose()
+        {
+            Dispose(true);
+            GC.SuppressFinalize(this);
+        }
+
+        private readonly object _disposeLock = new object();
+
+        /// <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)
+            {
+                try
+                {
+                    lock (_disposeLock)
+                    {
+                        if (_connection != null)
+                        {
+                            if (_connection.IsOpen())
+                            {
+                                _connection.Close();
+                            }
+
+                            _connection.Dispose();
+                            _connection = null;
+                        }
+                    }
+                }
+                catch (Exception ex)
+                {
+                    _logger.ErrorException("Error disposing database", ex);
+                }
+            }
+        }
+    }
+}

+ 543 - 0
MediaBrowser.Server.Implementations/EntryPoints/ActivityLogEntryPoint.cs

@@ -0,0 +1,543 @@
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Implementations.Logging;
+using MediaBrowser.Common.Plugins;
+using MediaBrowser.Common.ScheduledTasks;
+using MediaBrowser.Common.Updates;
+using MediaBrowser.Controller;
+using MediaBrowser.Controller.Activity;
+using MediaBrowser.Controller.Channels;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.LiveTv;
+using MediaBrowser.Controller.Localization;
+using MediaBrowser.Controller.Plugins;
+using MediaBrowser.Controller.Session;
+using MediaBrowser.Controller.Subtitles;
+using MediaBrowser.Model.Activity;
+using MediaBrowser.Model.Events;
+using MediaBrowser.Model.Logging;
+using MediaBrowser.Model.Tasks;
+using MediaBrowser.Model.Updates;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace MediaBrowser.Server.Implementations.EntryPoints
+{
+    public class ActivityLogEntryPoint : IServerEntryPoint
+    {
+        private readonly IInstallationManager _installationManager;
+
+        //private readonly ILogManager _logManager;
+        private readonly ILogger _logger;
+        private readonly ISessionManager _sessionManager;
+        private readonly ITaskManager _taskManager;
+        private readonly IActivityManager _activityManager;
+        private readonly ILocalizationManager _localization;
+
+        private readonly ILibraryManager _libraryManager;
+        private readonly ISubtitleManager _subManager;
+        private readonly IUserManager _userManager;
+        private readonly IServerConfigurationManager _config;
+        private readonly IServerApplicationHost _appHost;
+
+        public ActivityLogEntryPoint(ISessionManager sessionManager, 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;
+            _localization = localization;
+            _installationManager = installationManager;
+            _libraryManager = libraryManager;
+            _subManager = subManager;
+            _userManager = userManager;
+            _config = config;
+            //_logManager = logManager;
+            _appHost = appHost;
+        }
+
+        public void Run()
+        {
+            _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;
+
+            _sessionManager.SessionStarted += _sessionManager_SessionStarted;
+            _sessionManager.AuthenticationFailed += _sessionManager_AuthenticationFailed;
+            _sessionManager.AuthenticationSucceeded += _sessionManager_AuthenticationSucceeded;
+            _sessionManager.SessionEnded += _sessionManager_SessionEnded;
+
+            _sessionManager.PlaybackStart += _sessionManager_PlaybackStart;
+            _sessionManager.PlaybackStopped += _sessionManager_PlaybackStopped;
+
+            _subManager.SubtitlesDownloaded += _subManager_SubtitlesDownloaded;
+            _subManager.SubtitleDownloadFailure += _subManager_SubtitleDownloadFailure;
+
+            _userManager.UserCreated += _userManager_UserCreated;
+            _userManager.UserPasswordChanged += _userManager_UserPasswordChanged;
+            _userManager.UserDeleted += _userManager_UserDeleted;
+            _userManager.UserConfigurationUpdated += _userManager_UserConfigurationUpdated;
+
+            _config.ConfigurationUpdated += _config_ConfigurationUpdated;
+            _config.NamedConfigurationUpdated += _config_NamedConfigurationUpdated;
+
+            //_logManager.LoggerLoaded += _logManager_LoggerLoaded;
+
+            _appHost.ApplicationUpdated += _appHost_ApplicationUpdated;
+        }
+
+        void _subManager_SubtitleDownloadFailure(object sender, SubtitleDownloadFailureEventArgs e)
+        {
+            CreateLogEntry(new ActivityLogEntry
+            {
+                Name = string.Format(_localization.GetLocalizedString("SubtitleDownloadFailureForItem"), 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()
+            });
+        }
+
+        void _sessionManager_PlaybackStopped(object sender, PlaybackStopEventArgs e)
+        {
+            var item = e.MediaInfo;
+
+            if (item == null)
+            {
+                //_logger.Warn("PlaybackStopped reported with null media info.");
+                return;
+            }
+
+            if (e.Users.Count == 0)
+            {
+                return;
+            }
+
+            var username = e.Users.First().Name;
+
+            CreateLogEntry(new ActivityLogEntry
+            {
+                Name = string.Format(_localization.GetLocalizedString("UserStoppedPlayingItemWithValues"), username, item.Name),
+                Type = "PlaybackStopped",
+                ShortOverview = string.Format(_localization.GetLocalizedString("AppDeviceValues"), e.ClientName, e.DeviceName)
+            });
+        }
+
+        void _sessionManager_PlaybackStart(object sender, PlaybackProgressEventArgs e)
+        {
+            var item = e.MediaInfo;
+
+            if (item == null)
+            {
+                //_logger.Warn("PlaybackStart reported with null media info.");
+                return;
+            }
+
+            if (e.Users.Count == 0)
+            {
+                return;
+            }
+
+            var username = e.Users.First().Name;
+
+            CreateLogEntry(new ActivityLogEntry
+            {
+                Name = string.Format(_localization.GetLocalizedString("UserStartedPlayingItemWithValues"), username, item.Name),
+                Type = "PlaybackStart",
+                ShortOverview = string.Format(_localization.GetLocalizedString("AppDeviceValues"), e.ClientName, e.DeviceName)
+            });
+        }
+
+        void _sessionManager_SessionEnded(object sender, SessionEventArgs e)
+        {
+            string name;
+            var session = e.SessionInfo;
+
+            if (string.IsNullOrWhiteSpace(session.UserName))
+            {
+                name = string.Format(_localization.GetLocalizedString("DeviceOfflineWithName"), session.DeviceName);
+            }
+            else
+            {
+                name = string.Format(_localization.GetLocalizedString("UserOfflineFromDevice"), session.UserName, session.DeviceName);
+            }
+
+            CreateLogEntry(new ActivityLogEntry
+            {
+                Name = name,
+                Type = "SessionEnded",
+                ShortOverview = string.Format(_localization.GetLocalizedString("LabelIpAddressValue"), session.RemoteEndPoint)
+            });
+        }
+
+        void _sessionManager_AuthenticationSucceeded(object sender, GenericEventArgs<AuthenticationRequest> e)
+        {
+            CreateLogEntry(new ActivityLogEntry
+            {
+                Name = string.Format(_localization.GetLocalizedString("AuthenticationSucceededWithUserName"), e.Argument.Username),
+                Type = "AuthenticationSucceeded"
+            });
+        }
+
+        void _sessionManager_AuthenticationFailed(object sender, GenericEventArgs<AuthenticationRequest> e)
+        {
+            CreateLogEntry(new ActivityLogEntry
+            {
+                Name = string.Format(_localization.GetLocalizedString("FailedLoginAttemptWithUserName"), e.Argument.Username),
+                Type = "AuthenticationFailed"
+            });
+        }
+
+        void _appHost_ApplicationUpdated(object sender, GenericEventArgs<PackageVersionInfo> e)
+        {
+            CreateLogEntry(new ActivityLogEntry
+            {
+                Name = _localization.GetLocalizedString("MessageApplicationUpdated"),
+                Type = "ApplicationUpdated",
+                ShortOverview = string.Format(_localization.GetLocalizedString("VersionNumber"), e.Argument.versionStr),
+                Overview = e.Argument.description
+            });
+        }
+
+        void _logManager_LoggerLoaded(object sender, EventArgs e)
+        {
+        }
+
+        void _config_NamedConfigurationUpdated(object sender, ConfigurationUpdateEventArgs e)
+        {
+            CreateLogEntry(new ActivityLogEntry
+            {
+                Name = string.Format(_localization.GetLocalizedString("MessageNamedServerConfigurationUpdatedWithValue"), e.Key),
+                Type = "NamedConfigurationUpdated"
+            });
+        }
+
+        void _config_ConfigurationUpdated(object sender, EventArgs e)
+        {
+            CreateLogEntry(new ActivityLogEntry
+            {
+                Name = _localization.GetLocalizedString("MessageServerConfigurationUpdated"),
+                Type = "ServerConfigurationUpdated"
+            });
+        }
+
+        void _userManager_UserConfigurationUpdated(object sender, GenericEventArgs<User> e)
+        {
+            CreateLogEntry(new ActivityLogEntry
+            {
+                Name = string.Format(_localization.GetLocalizedString("UserConfigurationUpdatedWithName"), e.Argument.Name),
+                Type = "UserConfigurationUpdated"
+            });
+        }
+
+        void _userManager_UserDeleted(object sender, GenericEventArgs<User> e)
+        {
+            CreateLogEntry(new ActivityLogEntry
+            {
+                Name = string.Format(_localization.GetLocalizedString("UserDeletedWithName"), e.Argument.Name),
+                Type = "UserDeleted"
+            });
+        }
+
+        void _userManager_UserPasswordChanged(object sender, GenericEventArgs<User> e)
+        {
+            CreateLogEntry(new ActivityLogEntry
+            {
+                Name = string.Format(_localization.GetLocalizedString("UserPasswordChangedWithName"), e.Argument.Name),
+                Type = "UserPasswordChanged"
+            });
+        }
+
+        void _userManager_UserCreated(object sender, GenericEventArgs<User> e)
+        {
+            CreateLogEntry(new ActivityLogEntry
+            {
+                Name = string.Format(_localization.GetLocalizedString("UserCreatedWithName"), e.Argument.Name),
+                Type = "UserCreated"
+            });
+        }
+
+        void _subManager_SubtitlesDownloaded(object sender, SubtitleDownloadEventArgs e)
+        {
+            CreateLogEntry(new ActivityLogEntry
+            {
+                Name = string.Format(_localization.GetLocalizedString("SubtitlesDownloadedForItem"), Notifications.Notifications.GetItemName(e.Item)),
+                Type = "SubtitlesDownloaded",
+                ItemId = e.Item.Id.ToString("N"),
+                ShortOverview = string.Format(_localization.GetLocalizedString("ProviderValue"), e.Provider)
+            });
+        }
+
+        void _sessionManager_SessionStarted(object sender, SessionEventArgs e)
+        {
+            string name;
+            var session = e.SessionInfo;
+
+            if (string.IsNullOrWhiteSpace(session.UserName))
+            {
+                name = string.Format(_localization.GetLocalizedString("DeviceOnlineWithName"), session.DeviceName);
+            }
+            else
+            {
+                name = string.Format(_localization.GetLocalizedString("UserOnlineFromDevice"), session.UserName, session.DeviceName);
+            }
+
+            CreateLogEntry(new ActivityLogEntry
+            {
+                Name = name,
+                Type = "SessionStarted",
+                ShortOverview = string.Format(_localization.GetLocalizedString("LabelIpAddressValue"), session.RemoteEndPoint)
+            });
+        }
+
+        void _libraryManager_ItemRemoved(object sender, ItemChangeEventArgs e)
+        {
+            if (e.Item is LiveTvProgram || e.Item is IChannelItem)
+            {
+                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 is LiveTvProgram || e.Item is IChannelItem)
+            {
+                return;
+            }
+
+            CreateLogEntry(new ActivityLogEntry
+            {
+                Name = string.Format(_localization.GetLocalizedString("ItemAddedWithName"), Notifications.Notifications.GetItemName(e.Item)),
+                Type = "ItemAdded",
+                ItemId = e.Item.Id.ToString("N")
+            });
+        }
+
+        void _installationManager_PluginUpdated(object sender, GenericEventArgs<Tuple<IPlugin, PackageVersionInfo>> e)
+        {
+            CreateLogEntry(new ActivityLogEntry
+            {
+                Name = string.Format(_localization.GetLocalizedString("PluginUpdatedWithName"), e.Argument.Item1.Name),
+                Type = "PluginUpdated",
+                ShortOverview = string.Format(_localization.GetLocalizedString("VersionNumber"), e.Argument.Item2.versionStr),
+                Overview = e.Argument.Item2.description
+            });
+        }
+
+        void _installationManager_PluginUninstalled(object sender, GenericEventArgs<IPlugin> e)
+        {
+            CreateLogEntry(new ActivityLogEntry
+            {
+                Name = string.Format(_localization.GetLocalizedString("PluginUninstalledWithName"), e.Argument.Name),
+                Type = "PluginUninstalled"
+            });
+        }
+
+        void _installationManager_PluginInstalled(object sender, GenericEventArgs<PackageVersionInfo> e)
+        {
+            CreateLogEntry(new ActivityLogEntry
+            {
+                Name = string.Format(_localization.GetLocalizedString("PluginInstalledWithName"), e.Argument.name),
+                Type = "PluginInstalled",
+                ShortOverview = string.Format(_localization.GetLocalizedString("VersionNumber"), e.Argument.versionStr)
+            });
+        }
+
+        void _taskManager_TaskExecuting(object sender, GenericEventArgs<IScheduledTaskWorker> e)
+        {
+            var task = e.Argument;
+
+            var activityTask = task.ScheduledTask as IScheduledTaskActivityLog;
+            if (activityTask != null && !activityTask.IsActivityLogged)
+            {
+                return;
+            }
+
+            CreateLogEntry(new ActivityLogEntry
+            {
+                Name = string.Format(_localization.GetLocalizedString("ScheduledTaskStartedWithName"), task.Name),
+                Type = "ScheduledTaskStarted"
+            });
+        }
+
+        void _taskManager_TaskCompleted(object sender, TaskCompletionEventArgs e)
+        {
+            var result = e.Result;
+            var task = e.Task;
+
+            var activityTask = task.ScheduledTask as IScheduledTaskActivityLog;
+            if (activityTask != null && !activityTask.IsActivityLogged)
+            {
+                return;
+            }
+            
+            var time = result.EndTimeUtc - result.StartTimeUtc;
+            var runningTime = string.Format(_localization.GetLocalizedString("LabelRunningTimeValue"), ToUserFriendlyString(time));
+
+            if (result.Status == TaskCompletionStatus.Cancelled)
+            {
+                CreateLogEntry(new ActivityLogEntry
+                {
+                    Name = string.Format(_localization.GetLocalizedString("ScheduledTaskCancelledWithName"), task.Name),
+                    Type = "ScheduledTaskCancelled",
+                    ShortOverview = runningTime
+                });
+            }
+            else if (result.Status == TaskCompletionStatus.Completed)
+            {
+                CreateLogEntry(new ActivityLogEntry
+                {
+                    Name = string.Format(_localization.GetLocalizedString("ScheduledTaskCompletedWithName"), task.Name),
+                    Type = "ScheduledTaskCompleted",
+                    ShortOverview = runningTime
+                });
+            }
+            else if (result.Status == TaskCompletionStatus.Failed)
+            {
+                var vals = new List<string>();
+
+                if (!string.IsNullOrWhiteSpace(e.Result.ErrorMessage))
+                {
+                    vals.Add(e.Result.ErrorMessage);
+                }
+                if (!string.IsNullOrWhiteSpace(e.Result.LongErrorMessage))
+                {
+                    vals.Add(e.Result.LongErrorMessage);
+                }
+
+                CreateLogEntry(new ActivityLogEntry
+                {
+                    Name = string.Format(_localization.GetLocalizedString("ScheduledTaskFailedWithName"), task.Name),
+                    Type = "ScheduledTaskFailed",
+                    Overview = string.Join(Environment.NewLine, vals.ToArray()),
+                    ShortOverview = runningTime
+                });
+            }
+        }
+
+        private async void CreateLogEntry(ActivityLogEntry entry)
+        {
+            try
+            {
+                await _activityManager.Create(entry).ConfigureAwait(false);
+            }
+            catch
+            {
+                // Logged at lower levels
+            }
+        }
+
+        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;
+
+            _sessionManager.SessionStarted -= _sessionManager_SessionStarted;
+            _sessionManager.AuthenticationFailed -= _sessionManager_AuthenticationFailed;
+            _sessionManager.AuthenticationSucceeded -= _sessionManager_AuthenticationSucceeded;
+            _sessionManager.SessionEnded -= _sessionManager_SessionEnded;
+
+            _sessionManager.PlaybackStart -= _sessionManager_PlaybackStart;
+            _sessionManager.PlaybackStopped -= _sessionManager_PlaybackStopped;
+
+            _subManager.SubtitlesDownloaded -= _subManager_SubtitlesDownloaded;
+            _subManager.SubtitleDownloadFailure -= _subManager_SubtitleDownloadFailure;
+
+            _userManager.UserCreated -= _userManager_UserCreated;
+            _userManager.UserPasswordChanged -= _userManager_UserPasswordChanged;
+            _userManager.UserDeleted -= _userManager_UserDeleted;
+            _userManager.UserConfigurationUpdated -= _userManager_UserConfigurationUpdated;
+
+            _config.ConfigurationUpdated -= _config_ConfigurationUpdated;
+            _config.NamedConfigurationUpdated -= _config_NamedConfigurationUpdated;
+
+            //_logManager.LoggerLoaded -= _logManager_LoggerLoaded;
+
+            _appHost.ApplicationUpdated -= _appHost_ApplicationUpdated;
+        }
+
+        /// <summary>
+        /// Constructs a user-friendly string for this TimeSpan instance.
+        /// </summary>
+        public static string ToUserFriendlyString(TimeSpan span)
+        {
+            const int DaysInYear = 365;
+            const int DaysInMonth = 30;
+
+            // Get each non-zero value from TimeSpan component
+            List<string> values = new List<string>();
+
+            // Number of years
+            int days = span.Days;
+            if (days >= DaysInYear)
+            {
+                int years = (days / DaysInYear);
+                values.Add(CreateValueString(years, "year"));
+                days = (days % DaysInYear);
+            }
+            // Number of months
+            if (days >= DaysInMonth)
+            {
+                int months = (days / DaysInMonth);
+                values.Add(CreateValueString(months, "month"));
+                days = (days % DaysInMonth);
+            }
+            // Number of days
+            if (days >= 1)
+                values.Add(CreateValueString(days, "day"));
+            // Number of hours
+            if (span.Hours >= 1)
+                values.Add(CreateValueString(span.Hours, "hour"));
+            // Number of minutes
+            if (span.Minutes >= 1)
+                values.Add(CreateValueString(span.Minutes, "minute"));
+            // Number of seconds (include when 0 if no other components included)
+            if (span.Seconds >= 1 || values.Count == 0)
+                values.Add(CreateValueString(span.Seconds, "second"));
+
+            // Combine values into string
+            StringBuilder builder = new StringBuilder();
+            for (int i = 0; i < values.Count; i++)
+            {
+                if (builder.Length > 0)
+                    builder.Append((i == (values.Count - 1)) ? " and " : ", ");
+                builder.Append(values[i]);
+            }
+            // Return result
+            return builder.ToString();
+        }
+
+        /// <summary>
+        /// Constructs a string description of a time-span value.
+        /// </summary>
+        /// <param name="value">The value of this item</param>
+        /// <param name="description">The name of this item (singular form)</param>
+        private static string CreateValueString(int value, string description)
+        {
+            return String.Format("{0:#,##0} {1}",
+                value, (value == 1) ? description : String.Format("{0}s", description));
+        }
+    }
+}

+ 2 - 6
MediaBrowser.Server.Implementations/EntryPoints/Notifications/Notifications.cs

@@ -2,14 +2,12 @@
 using MediaBrowser.Common.ScheduledTasks;
 using MediaBrowser.Common.Updates;
 using MediaBrowser.Controller;
-using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Notifications;
 using MediaBrowser.Controller.Plugins;
 using MediaBrowser.Controller.Session;
-using MediaBrowser.Model.Configuration;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Events;
 using MediaBrowser.Model.Logging;
@@ -37,7 +35,6 @@ namespace MediaBrowser.Server.Implementations.EntryPoints.Notifications
         private readonly ITaskManager _taskManager;
         private readonly INotificationManager _notificationManager;
 
-        private readonly IServerConfigurationManager _config;
         private readonly ILibraryManager _libraryManager;
         private readonly ISessionManager _sessionManager;
         private readonly IServerApplicationHost _appHost;
@@ -45,14 +42,13 @@ namespace MediaBrowser.Server.Implementations.EntryPoints.Notifications
         private Timer LibraryUpdateTimer { get; set; }
         private readonly object _libraryChangedSyncLock = new object();
 
-        public Notifications(IInstallationManager installationManager, IUserManager userManager, ILogger logger, ITaskManager taskManager, INotificationManager notificationManager, IServerConfigurationManager config, ILibraryManager libraryManager, ISessionManager sessionManager, IServerApplicationHost appHost)
+        public Notifications(IInstallationManager installationManager, IUserManager userManager, ILogger logger, ITaskManager taskManager, INotificationManager notificationManager, ILibraryManager libraryManager, ISessionManager sessionManager, IServerApplicationHost appHost)
         {
             _installationManager = installationManager;
             _userManager = userManager;
             _logger = logger;
             _taskManager = taskManager;
             _notificationManager = notificationManager;
-            _config = config;
             _libraryManager = libraryManager;
             _sessionManager = sessionManager;
             _appHost = appHost;
@@ -317,7 +313,7 @@ namespace MediaBrowser.Server.Implementations.EntryPoints.Notifications
             }
         }
 
-        private string GetItemName(BaseItem item)
+        public static string GetItemName(BaseItem item)
         {
             var name = item.Name;
 

+ 0 - 9
MediaBrowser.Server.Implementations/EntryPoints/Notifications/WebSocketNotifier.cs

@@ -23,7 +23,6 @@ namespace MediaBrowser.Server.Implementations.EntryPoints.Notifications
         public void Run()
         {
             _notificationsRepo.NotificationAdded += _notificationsRepo_NotificationAdded;
-            _notificationsRepo.NotificationUpdated += _notificationsRepo_NotificationUpdated;
 
             _notificationsRepo.NotificationsMarkedRead += _notificationsRepo_NotificationsMarkedRead;
         }
@@ -40,13 +39,6 @@ namespace MediaBrowser.Server.Implementations.EntryPoints.Notifications
             _serverManager.SendWebSocketMessage("NotificationsMarkedRead", msg);
         }
 
-        void _notificationsRepo_NotificationUpdated(object sender, NotificationUpdateEventArgs e)
-        {
-            var msg = e.Notification.UserId + "|" + e.Notification.Id;
-
-            _serverManager.SendWebSocketMessage("NotificationUpdated", msg);
-        }
-
         void _notificationsRepo_NotificationAdded(object sender, NotificationUpdateEventArgs e)
         {
             var msg = e.Notification.UserId + "|" + e.Notification.Id;
@@ -57,7 +49,6 @@ namespace MediaBrowser.Server.Implementations.EntryPoints.Notifications
         public void Dispose()
         {
             _notificationsRepo.NotificationAdded -= _notificationsRepo_NotificationAdded;
-            _notificationsRepo.NotificationUpdated -= _notificationsRepo_NotificationUpdated;
         }
     }
 }

+ 12 - 1
MediaBrowser.Server.Implementations/EntryPoints/ServerEventNotifier.cs

@@ -3,11 +3,13 @@ using MediaBrowser.Common.Plugins;
 using MediaBrowser.Common.ScheduledTasks;
 using MediaBrowser.Common.Updates;
 using MediaBrowser.Controller;
+using MediaBrowser.Controller.Activity;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Plugins;
 using MediaBrowser.Controller.Session;
+using MediaBrowser.Model.Activity;
 using MediaBrowser.Model.Events;
 using System;
 using System.Threading;
@@ -47,6 +49,7 @@ namespace MediaBrowser.Server.Implementations.EntryPoints
         private readonly IDtoService _dtoService;
 
         private readonly ISessionManager _sessionManager;
+        private readonly IActivityManager _activityManager;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="ServerEventNotifier" /> class.
@@ -58,7 +61,7 @@ namespace MediaBrowser.Server.Implementations.EntryPoints
         /// <param name="taskManager">The task manager.</param>
         /// <param name="dtoService">The dto service.</param>
         /// <param name="sessionManager">The session manager.</param>
-        public ServerEventNotifier(IServerManager serverManager, IServerApplicationHost appHost, IUserManager userManager, IInstallationManager installationManager, ITaskManager taskManager, IDtoService dtoService, ISessionManager sessionManager)
+        public ServerEventNotifier(IServerManager serverManager, IServerApplicationHost appHost, IUserManager userManager, IInstallationManager installationManager, ITaskManager taskManager, IDtoService dtoService, ISessionManager sessionManager, IActivityManager activityManager)
         {
             _serverManager = serverManager;
             _userManager = userManager;
@@ -67,6 +70,7 @@ namespace MediaBrowser.Server.Implementations.EntryPoints
             _taskManager = taskManager;
             _dtoService = dtoService;
             _sessionManager = sessionManager;
+            _activityManager = activityManager;
         }
 
         public void Run()
@@ -84,6 +88,13 @@ namespace MediaBrowser.Server.Implementations.EntryPoints
             _installationManager.PackageInstallationFailed += _installationManager_PackageInstallationFailed;
 
             _taskManager.TaskCompleted += _taskManager_TaskCompleted;
+
+            _activityManager.EntryCreated += _activityManager_EntryCreated;
+        }
+
+        void _activityManager_EntryCreated(object sender, GenericEventArgs<ActivityLogEntry> e)
+        {
+            _serverManager.SendWebSocketMessage("ActivityLogEntryCreated", e.Argument);
         }
 
         void _userManager_UserConfigurationUpdated(object sender, GenericEventArgs<User> e)

+ 6 - 1
MediaBrowser.Server.Implementations/FileOrganization/OrganizerScheduledTask.cs

@@ -13,7 +13,7 @@ using System.Threading.Tasks;
 
 namespace MediaBrowser.Server.Implementations.FileOrganization
 {
-    public class OrganizerScheduledTask : IScheduledTask, IConfigurableScheduledTask
+    public class OrganizerScheduledTask : IScheduledTask, IConfigurableScheduledTask, IScheduledTaskActivityLog
     {
         private readonly ILibraryMonitor _libraryMonitor;
         private readonly ILibraryManager _libraryManager;
@@ -77,5 +77,10 @@ namespace MediaBrowser.Server.Implementations.FileOrganization
         {
             get { return GetTvOptions().IsEnabled; }
         }
+
+        public bool IsActivityLogged
+        {
+            get { return false; }
+        }
     }
 }

+ 1 - 1
MediaBrowser.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpListener.cs

@@ -110,7 +110,7 @@ namespace MediaBrowser.Server.Implementations.HttpServer.SocketSharp
         {
             try
             {
-                var webSocketContext = ctx.AcceptWebSocket(null, null);
+                var webSocketContext = ctx.AcceptWebSocket(null);
 
                 if (WebSocketHandler != null)
                 {

+ 5 - 2
MediaBrowser.Server.Implementations/Library/UserManager.cs

@@ -48,6 +48,7 @@ namespace MediaBrowser.Server.Implementations.Library
         /// </summary>
         /// <value>The user repository.</value>
         private IUserRepository UserRepository { get; set; }
+        public event EventHandler<GenericEventArgs<User>> UserPasswordChanged;
 
         private readonly IXmlSerializer _xmlSerializer;
 
@@ -390,7 +391,7 @@ namespace MediaBrowser.Server.Implementations.Library
         /// <param name="user">The user.</param>
         /// <param name="newPassword">The new password.</param>
         /// <returns>Task.</returns>
-        public Task ChangePassword(User user, string newPassword)
+        public async Task ChangePassword(User user, string newPassword)
         {
             if (user == null)
             {
@@ -399,7 +400,9 @@ namespace MediaBrowser.Server.Implementations.Library
 
             user.Password = string.IsNullOrEmpty(newPassword) ? string.Empty : GetSha1String(newPassword);
 
-            return UpdateUser(user);
+            await UpdateUser(user).ConfigureAwait(false);
+
+            EventHelper.FireEventIfNotNull(UserPasswordChanged, this, new GenericEventArgs<User>(user), _logger);
         }
 
         /// <summary>

+ 85 - 53
MediaBrowser.Server.Implementations/Localization/Server/server.json

@@ -699,10 +699,10 @@
     "HeaderProfileServerSettingsHelp": "These values control how Media Browser will present itself to the device.",
     "LabelMaxBitrate": "Max bitrate:",
     "LabelMaxBitrateHelp": "Specify a max bitrate in bandwidth constrained environments, or if the device imposes it's own limit.",
-    "LabelMaxStreamingBitrate":  "Max streaming bitrate:",
-    "LabelMaxStreamingBitrateHelp":  "Specify a max bitrate when streaming.",
-    "LabelMaxStaticBitrate":  "Max sync bitrate:",
-    "LabelMaxStaticBitrateHelp":  "Specify a max bitrate when syncing content at high quality.",
+    "LabelMaxStreamingBitrate": "Max streaming bitrate:",
+    "LabelMaxStreamingBitrateHelp": "Specify a max bitrate when streaming.",
+    "LabelMaxStaticBitrate": "Max sync bitrate:",
+    "LabelMaxStaticBitrateHelp": "Specify a max bitrate when syncing content at high quality.",
     "OptionIgnoreTranscodeByteRangeRequests": "Ignore transcode byte range requests",
     "OptionIgnoreTranscodeByteRangeRequestsHelp": "If enabled, these requests will be honored but will ignore the byte range header.",
     "LabelFriendlyName": "Friendly name",
@@ -808,8 +808,8 @@
     "TabNextUp": "Next Up",
     "MessageNoMovieSuggestionsAvailable": "No movie suggestions are currently available. Start watching and rating your movies, and then come back to view your recommendations.",
     "MessageNoCollectionsAvailable": "Collections allow you to enjoy personalized groupings of Movies, Series, Albums, Books and Games. Click the New button to start creating Collections.",
-    "MessageNoPlaylistsAvailable":  "Playlists allow you to create lists of content to play consecutively at a time. To add items to playlists, right click or tap and hold, then select Add to Playlist.",
-    "MessageNoPlaylistItemsAvailable":  "This playlist is currently empty.",
+    "MessageNoPlaylistsAvailable": "Playlists allow you to create lists of content to play consecutively at a time. To add items to playlists, right click or tap and hold, then select Add to Playlist.",
+    "MessageNoPlaylistItemsAvailable": "This playlist is currently empty.",
     "HeaderWelcomeToMediaBrowserWebClient": "Welcome to the Media Browser Web Client",
     "ButtonDismiss": "Dismiss",
     "MessageLearnHowToCustomize": "Learn how to customize this page to your own personal tastes. Click your user icon in the top right corner of the screen to view and update your preferences.",
@@ -918,52 +918,84 @@
     "LabelContext": "Context:",
     "OptionContextStreaming": "Streaming",
     "OptionContextStatic": "Sync",
-    "ButtonAddToPlaylist":  "Add to playlist",
-    "TabPlaylists":  "Playlists",
-	"ButtonClose": "Close",
-	"LabelAllLanguages": "All languages",
-	"HeaderBrowseOnlineImages": "Browse Online Images",
-	"LabelSource": "Source:",
-	"OptionAll": "All",
-	"LabelImage": "Image:",
-	"ButtonUpload": "Upload",
-	"ButtonBrowseImages": "Browse Images",
-	"HeaderImages": "Images",
-	"HeaderBackdrops": "Backdrops",
-	"HeaderScreenshots": "Screenshots",
-	"HeaderAddUpdateImage": "Add/Update Image",
-	"LabelDropImageHere": "Drop image here",
-	"LabelJpgPngOnly": "JPG/PNG only",
-	"LabelImageType": "Image type:",
-	"OptionPrimary": "Primary",
-	"OptionArt": "Art",
-	"OptionBackdrop": "Backdrop",
-	"OptionBox": "Box",
-	"OptionBoxRear": "Box rear",
-	"OptionDisc": "Disc",
-	"OptionLogo": "Logo",
-	"OptionMenu": "Menu",
-	"OptionScreenshot": "Screenshot",
-	"OptionLocked": "Locked",
-	"OptionUnidentified": "Unidentified",
-	"OptionMissingParentalRating": "Missing parental rating",
-	"OptionStub": "Stub",
-	"HeaderEpisodes": "Episodes:",
-	"OptionSeason0": "Season 0",
-	"LabelReport": "Report:",
-	"OptionReportSongs": "Songs",
-	"OptionReportSeries": "Series",
-	"OptionReportSeasons": "Seasons",
-	"OptionReportTrailers": "Trailers",
-	"OptionReportMusicVideos": "Music videos",
-	"OptionReportMovies": "Movies",
-	"OptionReportHomeVideos": "Home videos",
-	"OptionReportGames": "Games",
-	"OptionReportEpisodes": "Episodes",
-	"OptionReportCollections": "Collections",
-	"OptionReportBooks": "Books",
-	"OptionReportArtists": "Artists",
-	"OptionReportAlbums": "Albums",
+    "ButtonAddToPlaylist": "Add to playlist",
+    "TabPlaylists": "Playlists",
+    "ButtonClose": "Close",
+    "LabelAllLanguages": "All languages",
+    "HeaderBrowseOnlineImages": "Browse Online Images",
+    "LabelSource": "Source:",
+    "OptionAll": "All",
+    "LabelImage": "Image:",
+    "ButtonUpload": "Upload",
+    "ButtonBrowseImages": "Browse Images",
+    "HeaderImages": "Images",
+    "HeaderBackdrops": "Backdrops",
+    "HeaderScreenshots": "Screenshots",
+    "HeaderAddUpdateImage": "Add/Update Image",
+    "LabelDropImageHere": "Drop image here",
+    "LabelJpgPngOnly": "JPG/PNG only",
+    "LabelImageType": "Image type:",
+    "OptionPrimary": "Primary",
+    "OptionArt": "Art",
+    "OptionBackdrop": "Backdrop",
+    "OptionBox": "Box",
+    "OptionBoxRear": "Box rear",
+    "OptionDisc": "Disc",
+    "OptionLogo": "Logo",
+    "OptionMenu": "Menu",
+    "OptionScreenshot": "Screenshot",
+    "OptionLocked": "Locked",
+    "OptionUnidentified": "Unidentified",
+    "OptionMissingParentalRating": "Missing parental rating",
+    "OptionStub": "Stub",
+    "HeaderEpisodes": "Episodes:",
+    "OptionSeason0": "Season 0",
+    "LabelReport": "Report:",
+    "OptionReportSongs": "Songs",
+    "OptionReportSeries": "Series",
+    "OptionReportSeasons": "Seasons",
+    "OptionReportTrailers": "Trailers",
+    "OptionReportMusicVideos": "Music videos",
+    "OptionReportMovies": "Movies",
+    "OptionReportHomeVideos": "Home videos",
+    "OptionReportGames": "Games",
+    "OptionReportEpisodes": "Episodes",
+    "OptionReportCollections": "Collections",
+    "OptionReportBooks": "Books",
+    "OptionReportArtists": "Artists",
+    "OptionReportAlbums": "Albums",
     "OptionReportAdultVideos": "Adult videos",
-    "ButtonMore":  "More"
+    "ButtonMore": "More",
+    "HeaderActivity": "Activity",
+    "ScheduledTaskStartedWithName": "{0} started",
+    "ScheduledTaskCancelledWithName": "{0} was cancelled",
+    "ScheduledTaskCompletedWithName": "{0} completed",
+    "ScheduledTaskFailed": "Scheduled task completed",
+    "PluginInstalledWithName": "{0} was installed",
+    "PluginUpdatedWithName": "{0} was updated",
+    "PluginUninstalledWithName": "{0} was uninstalled",
+    "ScheduledTaskFailedWithName": "{0} failed",
+    "ItemAddedWithName": "{0} was added to the library",
+    "ItemRemovedWithName": "{0} was removed from the library",
+    "DeviceOnlineWithName": "{0} is connected",
+    "UserOnlineFromDevice": "{0} is online from {1}",
+    "DeviceOfflineWithName": "{0} has disconnected",
+    "UserOfflineFromDevice": "{0} has disconnected from {1}",
+    "SubtitlesDownloadedForItem": "Subtitles downloaded for {0}",
+    "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
+    "LabelRunningTimeValue": "Running time: {0}",
+    "LabelIpAddressValue": "Ip address: {0}",
+    "UserConfigurationUpdatedWithName": "User configuration has been updated for {0}",
+    "UserCreatedWithName": "User {0} has been created",
+    "UserPasswordChangedWithName": "Password has been changed for user {0}",
+    "UserDeletedWithName": "User {0} has been deleted",
+    "MessageServerConfigurationUpdated": "Server configuration has been updated",
+    "MessageNamedServerConfigurationUpdatedWithValue": "Server configuration section {0} has been updated",
+    "MessageApplicationUpdated": "Media Browser Server has been updated",
+    "AuthenticationSucceededWithUserName": "{0} successfully authenticated",
+    "FailedLoginAttemptWithUserName": "Failed login attempt from {0}",
+    "UserStartedPlayingItemWithValues":  "{0} has started playing {1}",
+    "UserStoppedPlayingItemWithValues": "{0} has stopped playing {1}",
+    "AppDeviceValues": "App: {0}, Device: {1}",
+    "ProviderValue":  "Provider: {0}"
 }

+ 4 - 1
MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj

@@ -101,6 +101,8 @@
     <Compile Include="..\SharedVersion.cs">
       <Link>Properties\SharedVersion.cs</Link>
     </Compile>
+    <Compile Include="Activity\ActivityManager.cs" />
+    <Compile Include="Activity\ActivityRepository.cs" />
     <Compile Include="Branding\BrandingConfigurationFactory.cs" />
     <Compile Include="Channels\ChannelConfigurations.cs" />
     <Compile Include="Channels\ChannelDownloadScheduledTask.cs" />
@@ -117,6 +119,7 @@
     <Compile Include="Drawing\PlayedIndicatorDrawer.cs" />
     <Compile Include="Drawing\UnplayedCountIndicator.cs" />
     <Compile Include="Dto\DtoService.cs" />
+    <Compile Include="EntryPoints\ActivityLogEntryPoint.cs" />
     <Compile Include="EntryPoints\AutomaticRestartEntryPoint.cs" />
     <Compile Include="EntryPoints\ExternalPortForwarding.cs" />
     <Compile Include="EntryPoints\LibraryChangedNotifier.cs" />
@@ -492,4 +495,4 @@
   <Target Name="AfterBuild">
   </Target>
   -->
-</Project>
+</Project>

+ 0 - 66
MediaBrowser.Server.Implementations/Notifications/SqliteNotificationsRepository.cs

@@ -206,46 +206,6 @@ namespace MediaBrowser.Server.Implementations.Notifications
             return notification;
         }
 
-        /// <summary>
-        /// Gets the notification.
-        /// </summary>
-        /// <param name="id">The id.</param>
-        /// <param name="userId">The user id.</param>
-        /// <returns>Notification.</returns>
-        /// <exception cref="System.ArgumentNullException">
-        /// id
-        /// or
-        /// userId
-        /// </exception>
-        public Notification GetNotification(string id, string userId)
-        {
-            if (string.IsNullOrEmpty(id))
-            {
-                throw new ArgumentNullException("id");
-            }
-            if (string.IsNullOrEmpty(userId))
-            {
-                throw new ArgumentNullException("userId");
-            }
-
-            using (var cmd = _connection.CreateCommand())
-            {
-                cmd.CommandText = "select Id,UserId,Date,Name,Description,Url,Level,IsRead,Category,RelatedId where Id=@Id And UserId = @UserId";
-
-                cmd.Parameters.Add(cmd, "@Id", DbType.Guid).Value = new Guid(id);
-                cmd.Parameters.Add(cmd, "@UserId", DbType.Guid).Value = new Guid(userId);
-
-                using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult | CommandBehavior.SingleRow))
-                {
-                    if (reader.Read())
-                    {
-                        return GetNotification(reader);
-                    }
-                }
-                return null;
-            }
-        }
-
         /// <summary>
         /// Gets the level.
         /// </summary>
@@ -289,32 +249,6 @@ namespace MediaBrowser.Server.Implementations.Notifications
             }
         }
 
-        /// <summary>
-        /// Updates the notification.
-        /// </summary>
-        /// <param name="notification">The notification.</param>
-        /// <param name="cancellationToken">The cancellation token.</param>
-        /// <returns>Task.</returns>
-        public async Task UpdateNotification(Notification notification, CancellationToken cancellationToken)
-        {
-            await ReplaceNotification(notification, cancellationToken).ConfigureAwait(false);
-
-            if (NotificationUpdated != null)
-            {
-                try
-                {
-                    NotificationUpdated(this, new NotificationUpdateEventArgs
-                    {
-                        Notification = notification
-                    });
-                }
-                catch (Exception ex)
-                {
-                    _logger.ErrorException("Error in NotificationUpdated event handler", ex);
-                }
-            }
-        }
-
         /// <summary>
         /// Replaces the notification.
         /// </summary>

+ 2 - 0
MediaBrowser.Server.Implementations/ServerManager/ServerManager.cs

@@ -153,6 +153,8 @@ namespace MediaBrowser.Server.Implementations.ServerManager
         /// <param name="result">The result.</param>
         private async void ProcessWebSocketMessageReceived(WebSocketMessageInfo result)
         {
+            //_logger.Debug("Websocket message received: {0}", result.MessageType);
+
             var tasks = _webSocketListeners.Select(i => Task.Run(async () =>
             {
                 try

+ 27 - 24
MediaBrowser.Server.Implementations/Session/SessionManager.cs

@@ -14,11 +14,13 @@ using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Controller.Security;
 using MediaBrowser.Controller.Session;
 using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Events;
 using MediaBrowser.Model.Library;
 using MediaBrowser.Model.Logging;
 using MediaBrowser.Model.Serialization;
 using MediaBrowser.Model.Session;
 using MediaBrowser.Model.Users;
+using MediaBrowser.Server.Implementations.Security;
 using System;
 using System.Collections.Concurrent;
 using System.Collections.Generic;
@@ -27,7 +29,6 @@ using System.IO;
 using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
-using MediaBrowser.Server.Implementations.Security;
 
 namespace MediaBrowser.Server.Implementations.Session
 {
@@ -76,6 +77,10 @@ namespace MediaBrowser.Server.Implementations.Session
         private readonly ConcurrentDictionary<string, SessionInfo> _activeConnections =
             new ConcurrentDictionary<string, SessionInfo>(StringComparer.OrdinalIgnoreCase);
 
+        public event EventHandler<GenericEventArgs<AuthenticationRequest>> AuthenticationFailed;
+
+        public event EventHandler<GenericEventArgs<AuthenticationRequest>> AuthenticationSucceeded;
+        
         /// <summary>
         /// Occurs when [playback start].
         /// </summary>
@@ -399,6 +404,11 @@ namespace MediaBrowser.Server.Implementations.Session
                         Id = Guid.NewGuid().ToString("N")
                     };
 
+                    sessionInfo.DeviceName = deviceName;
+                    sessionInfo.UserId = userId;
+                    sessionInfo.UserName = username;
+                    sessionInfo.RemoteEndPoint = remoteEndPoint;
+
                     OnSessionStarted(sessionInfo);
 
                     return sessionInfo;
@@ -1191,44 +1201,37 @@ namespace MediaBrowser.Server.Implementations.Session
         /// <summary>
         /// Authenticates the new session.
         /// </summary>
-        /// <param name="username">The username.</param>
-        /// <param name="password">The password.</param>
-        /// <param name="clientType">Type of the client.</param>
-        /// <param name="appVersion">The application version.</param>
-        /// <param name="deviceId">The device identifier.</param>
-        /// <param name="deviceName">Name of the device.</param>
-        /// <param name="remoteEndPoint">The remote end point.</param>
+        /// <param name="request">The request.</param>
         /// <param name="isLocal">if set to <c>true</c> [is local].</param>
         /// <returns>Task{SessionInfo}.</returns>
+        /// <exception cref="AuthenticationException">Invalid user or password entered.</exception>
         /// <exception cref="System.UnauthorizedAccessException">Invalid user or password entered.</exception>
         /// <exception cref="UnauthorizedAccessException">Invalid user or password entered.</exception>
-        public async Task<AuthenticationResult> AuthenticateNewSession(string username,
-            string password,
-            string clientType,
-            string appVersion,
-            string deviceId,
-            string deviceName,
-            string remoteEndPoint,
+        public async Task<AuthenticationResult> AuthenticateNewSession(AuthenticationRequest request,
             bool isLocal)
         {
-            var result = (isLocal && string.Equals(clientType, "Dashboard", StringComparison.OrdinalIgnoreCase)) ||
-                await _userManager.AuthenticateUser(username, password).ConfigureAwait(false);
+            var result = (isLocal && string.Equals(request.App, "Dashboard", StringComparison.OrdinalIgnoreCase)) ||
+                await _userManager.AuthenticateUser(request.Username, request.Password).ConfigureAwait(false);
 
             if (!result)
             {
+                EventHelper.FireEventIfNotNull(AuthenticationFailed, this, new GenericEventArgs<AuthenticationRequest>(request), _logger);
+
                 throw new AuthenticationException("Invalid user or password entered.");
             }
 
             var user = _userManager.Users
-                .First(i => string.Equals(username, i.Name, StringComparison.OrdinalIgnoreCase));
+                .First(i => string.Equals(request.Username, i.Name, StringComparison.OrdinalIgnoreCase));
 
-            var token = await GetAuthorizationToken(user.Id.ToString("N"), deviceId, clientType, deviceName).ConfigureAwait(false);
+            var token = await GetAuthorizationToken(user.Id.ToString("N"), request.DeviceId, request.App, request.DeviceName).ConfigureAwait(false);
 
-            var session = await LogSessionActivity(clientType,
-                appVersion,
-                deviceId,
-                deviceName,
-                remoteEndPoint,
+            EventHelper.FireEventIfNotNull(AuthenticationSucceeded, this, new GenericEventArgs<AuthenticationRequest>(request), _logger);
+            
+            var session = await LogSessionActivity(request.App,
+                request.AppVersion,
+                request.DeviceId,
+                request.DeviceName,
+                request.RemoteEndPoint,
                 user)
                 .ConfigureAwait(false);
 

+ 0 - 1
MediaBrowser.Server.Implementations/Session/SessionWebSocketListener.cs

@@ -1,5 +1,4 @@
 using MediaBrowser.Common.Net;
-using MediaBrowser.Controller;
 using MediaBrowser.Controller.Session;
 using MediaBrowser.Model.Logging;
 using MediaBrowser.Model.Serialization;

+ 44 - 2
MediaBrowser.Server.Implementations/Sync/SyncRepository.cs

@@ -15,7 +15,7 @@ using System.Threading.Tasks;
 
 namespace MediaBrowser.Server.Implementations.Sync
 {
-    public class SyncRepository : ISyncRepository
+    public class SyncRepository : ISyncRepository, IDisposable
     {
         private IDbConnection _connection;
         private readonly ILogger _logger;
@@ -422,8 +422,50 @@ namespace MediaBrowser.Server.Implementations.Sync
             }
 
             info.TargetId = reader.GetString(5);
-            
+
             return info;
         }
+
+        /// <summary>
+        /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
+        /// </summary>
+        public void Dispose()
+        {
+            Dispose(true);
+            GC.SuppressFinalize(this);
+        }
+
+        private readonly object _disposeLock = new object();
+
+        /// <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)
+            {
+                try
+                {
+                    lock (_disposeLock)
+                    {
+                        if (_connection != null)
+                        {
+                            if (_connection.IsOpen())
+                            {
+                                _connection.Close();
+                            }
+
+                            _connection.Dispose();
+                            _connection = null;
+                        }
+                    }
+                }
+                catch (Exception ex)
+                {
+                    _logger.ErrorException("Error disposing database", ex);
+                }
+            }
+        }
     }
 }

+ 22 - 0
MediaBrowser.ServerApplication/ApplicationHost.cs

@@ -9,6 +9,7 @@ using MediaBrowser.Common.IO;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Common.Progress;
 using MediaBrowser.Controller;
+using MediaBrowser.Controller.Activity;
 using MediaBrowser.Controller.Channels;
 using MediaBrowser.Controller.Chapters;
 using MediaBrowser.Controller.Collections;
@@ -53,6 +54,7 @@ using MediaBrowser.Providers.Chapters;
 using MediaBrowser.Providers.Manager;
 using MediaBrowser.Providers.Subtitles;
 using MediaBrowser.Server.Implementations;
+using MediaBrowser.Server.Implementations.Activity;
 using MediaBrowser.Server.Implementations.Channels;
 using MediaBrowser.Server.Implementations.Collections;
 using MediaBrowser.Server.Implementations.Configuration;
@@ -343,6 +345,13 @@ namespace MediaBrowser.ServerApplication
                 saveConfig = true;
             }
 
+            if (ServerConfigurationManager.Configuration.SubtitleOptions != null)
+            {
+                ServerConfigurationManager.SaveConfiguration("subtitles", ServerConfigurationManager.Configuration.SubtitleOptions);
+                ServerConfigurationManager.Configuration.SubtitleOptions = null;
+                saveConfig = true;
+            }
+
             if (saveConfig)
             {
                 ServerConfigurationManager.SaveConfiguration();
@@ -641,6 +650,10 @@ namespace MediaBrowser.ServerApplication
                 MediaEncoder, ChapterManager);
             RegisterSingleInstance(EncodingManager);
 
+            var activityLogRepo = await GetActivityLogRepository().ConfigureAwait(false);
+            RegisterSingleInstance(activityLogRepo);
+            RegisterSingleInstance<IActivityManager>(new ActivityManager(LogManager.GetLogger("ActivityManager"), activityLogRepo));
+
             var authContext = new AuthorizationContext();
             RegisterSingleInstance<IAuthorizationContext>(authContext);
             RegisterSingleInstance<ISessionContext>(new SessionContext(UserManager, authContext, SessionManager));
@@ -730,6 +743,15 @@ namespace MediaBrowser.ServerApplication
             return repo;
         }
 
+        private async Task<IActivityRepository> GetActivityLogRepository()
+        {
+            var repo = new ActivityRepository(LogManager.GetLogger("ActivityRepository"), ServerConfigurationManager.ApplicationPaths);
+
+            await repo.Initialize().ConfigureAwait(false);
+
+            return repo;
+        }
+
         private async Task<ISyncRepository> GetSyncRepository()
         {
             var repo = new SyncRepository(LogManager.GetLogger("SyncRepository"), ServerConfigurationManager.ApplicationPaths);