浏览代码

completed auth database

Luke Pulverenti 11 年之前
父节点
当前提交
c02e917f56
共有 41 个文件被更改,包括 933 次插入136 次删除
  1. 15 19
      MediaBrowser.Api/ApiEntryPoint.cs
  2. 1 0
      MediaBrowser.Api/ConfigurationService.cs
  3. 5 4
      MediaBrowser.Api/Playback/Hls/BaseHlsService.cs
  4. 1 2
      MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs
  5. 1 1
      MediaBrowser.Api/Playback/Hls/HlsSegmentService.cs
  6. 17 1
      MediaBrowser.Api/SessionsService.cs
  7. 1 0
      MediaBrowser.Api/SystemService.cs
  8. 16 16
      MediaBrowser.Api/UserService.cs
  9. 9 4
      MediaBrowser.Controller/Channels/ChannelItemInfo.cs
  10. 37 0
      MediaBrowser.Controller/Collections/CollectionEvents.cs
  11. 15 0
      MediaBrowser.Controller/Collections/ICollectionManager.cs
  12. 7 0
      MediaBrowser.Controller/Dto/IDtoService.cs
  13. 4 0
      MediaBrowser.Controller/MediaBrowser.Controller.csproj
  14. 1 0
      MediaBrowser.Controller/Providers/IMetadataProvider.cs
  15. 61 0
      MediaBrowser.Controller/Security/AuthenticationInfo.cs
  16. 42 0
      MediaBrowser.Controller/Security/AuthenticationInfoQuery.cs
  17. 39 0
      MediaBrowser.Controller/Security/IAuthenticationRepository.cs
  18. 23 2
      MediaBrowser.Controller/Session/ISessionManager.cs
  19. 1 1
      MediaBrowser.Controller/Session/SessionInfo.cs
  20. 5 5
      MediaBrowser.LocalMetadata/BaseXmlProvider.cs
  21. 17 2
      MediaBrowser.Model/ApiClient/IApiClient.cs
  22. 2 0
      MediaBrowser.Model/Configuration/ServerConfiguration.cs
  23. 1 1
      MediaBrowser.Model/Users/AuthenticationResult.cs
  24. 0 1
      MediaBrowser.Providers/Manager/MetadataService.cs
  25. 1 1
      MediaBrowser.Providers/Manager/ProviderUtils.cs
  26. 1 1
      MediaBrowser.Providers/TV/MovieDbEpisodeImageProvider.cs
  27. 2 1
      MediaBrowser.Server.Implementations/Channels/ChannelManager.cs
  28. 52 5
      MediaBrowser.Server.Implementations/Collections/CollectionManager.cs
  29. 24 24
      MediaBrowser.Server.Implementations/HttpServer/Security/AuthService.cs
  30. 1 0
      MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj
  31. 1 1
      MediaBrowser.Server.Implementations/Persistence/SqliteFileOrganizationRepository.cs
  32. 338 0
      MediaBrowser.Server.Implementations/Security/AuthenticationRepository.cs
  33. 147 12
      MediaBrowser.Server.Implementations/Session/SessionManager.cs
  34. 17 3
      MediaBrowser.ServerApplication/ApplicationHost.cs
  35. 1 1
      MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs
  36. 5 5
      MediaBrowser.XbmcMetadata/Providers/BaseNfoProvider.cs
  37. 6 7
      MediaBrowser.XbmcMetadata/Savers/SeasonXmlSaver.cs
  38. 11 11
      MediaBrowser.XbmcMetadata/Savers/XmlSaverHelpers.cs
  39. 2 2
      Nuget/MediaBrowser.Common.Internal.nuspec
  40. 1 1
      Nuget/MediaBrowser.Common.nuspec
  41. 2 2
      Nuget/MediaBrowser.Server.Core.nuspec

+ 15 - 19
MediaBrowser.Api/ApiEntryPoint.cs

@@ -37,7 +37,7 @@ namespace MediaBrowser.Api
 
         private readonly ISessionManager _sessionManager;
 
-        public readonly SemaphoreSlim TranscodingStartLock = new SemaphoreSlim(1,1);
+        public readonly SemaphoreSlim TranscodingStartLock = new SemaphoreSlim(1, 1);
 
         /// <summary>
         /// Initializes a new instance of the <see cref="ApiEntryPoint" /> class.
@@ -102,7 +102,7 @@ namespace MediaBrowser.Api
         {
             var jobCount = _activeTranscodingJobs.Count;
 
-            Parallel.ForEach(_activeTranscodingJobs.ToList(), j => KillTranscodingJob(j, FileDeleteMode.All));
+            Parallel.ForEach(_activeTranscodingJobs.ToList(), j => KillTranscodingJob(j, path => true));
 
             // Try to allow for some time to kill the ffmpeg processes and delete the partial stream files
             if (jobCount > 0)
@@ -295,17 +295,18 @@ namespace MediaBrowser.Api
         {
             var job = (TranscodingJob)state;
 
-            KillTranscodingJob(job, FileDeleteMode.All);
+            KillTranscodingJob(job, path => true);
         }
 
         /// <summary>
         /// Kills the single transcoding job.
         /// </summary>
         /// <param name="deviceId">The device id.</param>
-        /// <param name="deleteMode">The delete mode.</param>
+        /// <param name="delete">The delete.</param>
         /// <param name="acquireLock">if set to <c>true</c> [acquire lock].</param>
+        /// <returns>Task.</returns>
         /// <exception cref="System.ArgumentNullException">sourcePath</exception>
-        internal async Task KillTranscodingJobs(string deviceId, FileDeleteMode deleteMode, bool acquireLock)
+        internal async Task KillTranscodingJobs(string deviceId, Func<string, bool> delete, bool acquireLock)
         {
             if (string.IsNullOrEmpty(deviceId))
             {
@@ -330,12 +331,12 @@ namespace MediaBrowser.Api
             {
                 await TranscodingStartLock.WaitAsync(CancellationToken.None).ConfigureAwait(false);
             }
-            
+
             try
             {
                 foreach (var job in jobs)
                 {
-                    KillTranscodingJob(job, deleteMode);
+                    KillTranscodingJob(job, delete);
                 }
             }
             finally
@@ -352,10 +353,11 @@ namespace MediaBrowser.Api
         /// </summary>
         /// <param name="deviceId">The device identifier.</param>
         /// <param name="type">The type.</param>
-        /// <param name="deleteMode">The delete mode.</param>
+        /// <param name="delete">The delete.</param>
         /// <param name="acquireLock">if set to <c>true</c> [acquire lock].</param>
+        /// <returns>Task.</returns>
         /// <exception cref="System.ArgumentNullException">deviceId</exception>
-        internal async Task KillTranscodingJobs(string deviceId, TranscodingJobType type, FileDeleteMode deleteMode, bool acquireLock)
+        internal async Task KillTranscodingJobs(string deviceId, TranscodingJobType type, Func<string, bool> delete, bool acquireLock)
         {
             if (string.IsNullOrEmpty(deviceId))
             {
@@ -385,7 +387,7 @@ namespace MediaBrowser.Api
             {
                 foreach (var job in jobs)
                 {
-                    KillTranscodingJob(job, deleteMode);
+                    KillTranscodingJob(job, delete);
                 }
             }
             finally
@@ -401,8 +403,8 @@ namespace MediaBrowser.Api
         /// Kills the transcoding job.
         /// </summary>
         /// <param name="job">The job.</param>
-        /// <param name="deleteMode">The delete mode.</param>
-        private void KillTranscodingJob(TranscodingJob job, FileDeleteMode deleteMode)
+        /// <param name="delete">The delete.</param>
+        private void KillTranscodingJob(TranscodingJob job, Func<string, bool> delete)
         {
             lock (_activeTranscodingJobs)
             {
@@ -454,7 +456,7 @@ namespace MediaBrowser.Api
                 }
             }
 
-            if (deleteMode == FileDeleteMode.All)
+            if (delete(job.Path))
             {
                 DeletePartialStreamFiles(job.Path, job.Type, 0, 1500);
             }
@@ -593,10 +595,4 @@ namespace MediaBrowser.Api
         /// </summary>
         Hls
     }
-
-    public enum FileDeleteMode
-    {
-        None,
-        All
-    }
 }

+ 1 - 0
MediaBrowser.Api/ConfigurationService.cs

@@ -18,6 +18,7 @@ namespace MediaBrowser.Api
     /// Class GetConfiguration
     /// </summary>
     [Route("/System/Configuration", "GET", Summary = "Gets application configuration")]
+    [Authenticated]
     public class GetConfiguration : IReturn<ServerConfiguration>
     {
 

+ 5 - 4
MediaBrowser.Api/Playback/Hls/BaseHlsService.cs

@@ -22,7 +22,8 @@ namespace MediaBrowser.Api.Playback.Hls
     /// </summary>
     public abstract class BaseHlsService : BaseStreamingService
     {
-        protected BaseHlsService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, ILiveTvManager liveTvManager, IDlnaManager dlnaManager, IChannelManager channelManager, ISubtitleEncoder subtitleEncoder) : base(serverConfig, userManager, libraryManager, isoManager, mediaEncoder, fileSystem, liveTvManager, dlnaManager, channelManager, subtitleEncoder)
+        protected BaseHlsService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, ILiveTvManager liveTvManager, IDlnaManager dlnaManager, IChannelManager channelManager, ISubtitleEncoder subtitleEncoder)
+            : base(serverConfig, userManager, libraryManager, isoManager, mediaEncoder, fileSystem, liveTvManager, dlnaManager, channelManager, subtitleEncoder)
         {
         }
 
@@ -103,8 +104,8 @@ namespace MediaBrowser.Api.Playback.Hls
                     }
                     else
                     {
-                        await ApiEntryPoint.Instance.KillTranscodingJobs(state.Request.DeviceId, TranscodingJobType.Hls, FileDeleteMode.All, false).ConfigureAwait(false);
-                        
+                        await ApiEntryPoint.Instance.KillTranscodingJobs(state.Request.DeviceId, TranscodingJobType.Hls, p => true, false).ConfigureAwait(false);
+
                         // If the playlist doesn't already exist, startup ffmpeg
                         try
                         {
@@ -252,7 +253,7 @@ namespace MediaBrowser.Api.Playback.Hls
         protected override string GetCommandLineArguments(string outputPath, StreamState state, bool isEncoding)
         {
             var hlsVideoRequest = state.VideoRequest as GetHlsVideoStream;
-            
+
             var itsOffsetMs = hlsVideoRequest == null
                                        ? 0
                                        : hlsVideoRequest.TimeStampOffsetMs;

+ 1 - 2
MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs

@@ -127,8 +127,7 @@ namespace MediaBrowser.Api.Playback.Hls
                         // If the playlist doesn't already exist, startup ffmpeg
                         try
                         {
-                            // TODO: Delete files from other jobs, but not this one
-                            await ApiEntryPoint.Instance.KillTranscodingJobs(state.Request.DeviceId, TranscodingJobType.Hls, FileDeleteMode.None, false).ConfigureAwait(false);
+                            await ApiEntryPoint.Instance.KillTranscodingJobs(state.Request.DeviceId, TranscodingJobType.Hls, p => !string.Equals(p, playlistPath, StringComparison.OrdinalIgnoreCase), false).ConfigureAwait(false);
 
                             if (currentTranscodingIndex.HasValue)
                             {

+ 1 - 1
MediaBrowser.Api/Playback/Hls/HlsSegmentService.cs

@@ -74,7 +74,7 @@ namespace MediaBrowser.Api.Playback.Hls
 
         public void Delete(StopEncodingProcess request)
         {
-            var task = ApiEntryPoint.Instance.KillTranscodingJobs(request.DeviceId, FileDeleteMode.All, true);
+            var task = ApiEntryPoint.Instance.KillTranscodingJobs(request.DeviceId, path => true, true);
 
             Task.WaitAll(task);
         }

+ 17 - 1
MediaBrowser.Api/SessionsService.cs

@@ -213,6 +213,7 @@ namespace MediaBrowser.Api
     }
 
     [Route("/Sessions/Capabilities", "POST", Summary = "Updates capabilities for a device")]
+    [Authenticated]
     public class PostCapabilities : IReturnVoid
     {
         /// <summary>
@@ -235,6 +236,11 @@ namespace MediaBrowser.Api
         public bool SupportsMediaControl { get; set; }
     }
 
+    [Route("/Sessions/Logout", "POST", Summary = "Reports that a session has ended")]
+    public class ReportSessionEnded : IReturnVoid
+    {
+    }
+
     /// <summary>
     /// Class SessionsService
     /// </summary>
@@ -246,16 +252,26 @@ namespace MediaBrowser.Api
         private readonly ISessionManager _sessionManager;
 
         private readonly IUserManager _userManager;
+        private readonly IAuthorizationContext _authContext;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="SessionsService" /> class.
         /// </summary>
         /// <param name="sessionManager">The session manager.</param>
         /// <param name="userManager">The user manager.</param>
-        public SessionsService(ISessionManager sessionManager, IUserManager userManager)
+        public SessionsService(ISessionManager sessionManager, IUserManager userManager, IAuthorizationContext authContext)
         {
             _sessionManager = sessionManager;
             _userManager = userManager;
+            _authContext = authContext;
+        }
+
+
+        public void Post(ReportSessionEnded request)
+        {
+            var auth = _authContext.GetAuthorizationInfo(Request);
+
+            _sessionManager.Logout(auth.Token);
         }
 
         /// <summary>

+ 1 - 0
MediaBrowser.Api/SystemService.cs

@@ -15,6 +15,7 @@ namespace MediaBrowser.Api
     /// Class GetSystemInfo
     /// </summary>
     [Route("/System/Info", "GET", Summary = "Gets information about the server")]
+    [Authenticated]
     public class GetSystemInfo : IReturn<SystemInfo>
     {
 

+ 16 - 16
MediaBrowser.Api/UserService.cs

@@ -4,7 +4,6 @@ using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Session;
 using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Serialization;
 using MediaBrowser.Model.Users;
 using ServiceStack;
 using ServiceStack.Text.Controller;
@@ -19,6 +18,7 @@ namespace MediaBrowser.Api
     /// Class GetUsers
     /// </summary>
     [Route("/Users", "GET", Summary = "Gets a list of users")]
+    [Authenticated]
     public class GetUsers : IReturn<List<UserDto>>
     {
         [ApiMember(Name = "IsHidden", Description = "Optional filter by IsHidden=true or false", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
@@ -37,6 +37,7 @@ namespace MediaBrowser.Api
     /// Class GetUser
     /// </summary>
     [Route("/Users/{Id}", "GET", Summary = "Gets a user by Id")]
+    [Authenticated]
     public class GetUser : IReturn<UserDto>
     {
         /// <summary>
@@ -159,11 +160,6 @@ namespace MediaBrowser.Api
     /// </summary>
     public class UserService : BaseApiService, IHasAuthorization
     {
-        /// <summary>
-        /// The _XML serializer
-        /// </summary>
-        private readonly IXmlSerializer _xmlSerializer;
-
         /// <summary>
         /// The _user manager
         /// </summary>
@@ -176,19 +172,12 @@ namespace MediaBrowser.Api
         /// <summary>
         /// Initializes a new instance of the <see cref="UserService" /> class.
         /// </summary>
-        /// <param name="xmlSerializer">The XML serializer.</param>
         /// <param name="userManager">The user manager.</param>
         /// <param name="dtoService">The dto service.</param>
+        /// <param name="sessionMananger">The session mananger.</param>
         /// <exception cref="System.ArgumentNullException">xmlSerializer</exception>
-        public UserService(IXmlSerializer xmlSerializer, IUserManager userManager, IDtoService dtoService, ISessionManager sessionMananger)
-            : base()
+        public UserService(IUserManager userManager, IDtoService dtoService, ISessionManager sessionMananger)
         {
-            if (xmlSerializer == null)
-            {
-                throw new ArgumentNullException("xmlSerializer");
-            }
-
-            _xmlSerializer = xmlSerializer;
             _userManager = userManager;
             _dtoService = dtoService;
             _sessionMananger = sessionMananger;
@@ -196,6 +185,11 @@ namespace MediaBrowser.Api
 
         public object Get(GetPublicUsers request)
         {
+            if (!Request.IsLocal && !_sessionMananger.IsLocal(Request.RemoteIp))
+            {
+                return ToOptimizedResult(new List<UserDto>());
+            }
+
             return Get(new GetUsers
             {
                 IsHidden = false,
@@ -368,9 +362,15 @@ namespace MediaBrowser.Api
                 {
                     throw new ArgumentException("There must be at least one enabled user in the system.");
                 }
+
+                var revokeTask = _sessionMananger.RevokeUserTokens(user.Id.ToString("N"));
+
+                Task.WaitAll(revokeTask);
             }
 
-            var task = user.Name.Equals(dtoUser.Name, StringComparison.Ordinal) ? _userManager.UpdateUser(user) : _userManager.RenameUser(user, dtoUser.Name);
+            var task = user.Name.Equals(dtoUser.Name, StringComparison.Ordinal) ? 
+                _userManager.UpdateUser(user) : 
+                _userManager.RenameUser(user, dtoUser.Name);
 
             Task.WaitAll(task);
 

+ 9 - 4
MediaBrowser.Controller/Channels/ChannelItemInfo.cs

@@ -10,6 +10,8 @@ namespace MediaBrowser.Controller.Channels
     {
         public string Name { get; set; }
 
+        public string SeriesName { get; set; }
+
         public string Id { get; set; }
 
         public ChannelItemType Type { get; set; }
@@ -28,8 +30,6 @@ namespace MediaBrowser.Controller.Channels
 
         public long? RunTimeTicks { get; set; }
 
-        public bool IsInfiniteStream { get; set; }
-        
         public string ImageUrl { get; set; }
 
         public ChannelMediaType MediaType { get; set; }
@@ -43,9 +43,14 @@ namespace MediaBrowser.Controller.Channels
         public int? ProductionYear { get; set; }
 
         public DateTime? DateCreated { get; set; }
-        
+
+        public int? IndexNumber { get; set; }
+        public int? ParentIndexNumber { get; set; }
+
         public List<ChannelMediaInfo> MediaSources { get; set; }
-        
+
+        public bool IsInfiniteStream { get; set; }
+
         public ChannelItemInfo()
         {
             MediaSources = new List<ChannelMediaInfo>();

+ 37 - 0
MediaBrowser.Controller/Collections/CollectionEvents.cs

@@ -0,0 +1,37 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Movies;
+using System;
+using System.Collections.Generic;
+
+namespace MediaBrowser.Controller.Collections
+{
+    public class CollectionCreatedEventArgs : EventArgs
+    {
+        /// <summary>
+        /// Gets or sets the collection.
+        /// </summary>
+        /// <value>The collection.</value>
+        public BoxSet Collection { get; set; }
+
+        /// <summary>
+        /// Gets or sets the options.
+        /// </summary>
+        /// <value>The options.</value>
+        public CollectionCreationOptions Options { get; set; }
+    }
+
+    public class CollectionModifiedEventArgs : EventArgs
+    {
+        /// <summary>
+        /// Gets or sets the collection.
+        /// </summary>
+        /// <value>The collection.</value>
+        public BoxSet Collection { get; set; }
+
+        /// <summary>
+        /// Gets or sets the items changed.
+        /// </summary>
+        /// <value>The items changed.</value>
+        public List<BaseItem> ItemsChanged { get; set; }
+    }
+}

+ 15 - 0
MediaBrowser.Controller/Collections/ICollectionManager.cs

@@ -8,6 +8,21 @@ namespace MediaBrowser.Controller.Collections
 {
     public interface ICollectionManager
     {
+        /// <summary>
+        /// Occurs when [collection created].
+        /// </summary>
+        event EventHandler<CollectionCreatedEventArgs> CollectionCreated;
+
+        /// <summary>
+        /// Occurs when [items added to collection].
+        /// </summary>
+        event EventHandler<CollectionModifiedEventArgs> ItemsAddedToCollection;
+
+        /// <summary>
+        /// Occurs when [items removed from collection].
+        /// </summary>
+        event EventHandler<CollectionModifiedEventArgs> ItemsRemovedFromCollection;
+
         /// <summary>
         /// Creates the collection.
         /// </summary>

+ 7 - 0
MediaBrowser.Controller/Dto/IDtoService.cs

@@ -50,6 +50,13 @@ namespace MediaBrowser.Controller.Dto
         /// <returns>ChapterInfoDto.</returns>
         ChapterInfoDto GetChapterInfoDto(ChapterInfo chapterInfo, BaseItem item);
 
+        /// <summary>
+        /// Gets the user item data dto.
+        /// </summary>
+        /// <param name="data">The data.</param>
+        /// <returns>UserItemDataDto.</returns>
+        UserItemDataDto GetUserItemDataDto(UserItemData data);
+
         /// <summary>
         /// Gets the item by name dto.
         /// </summary>

+ 4 - 0
MediaBrowser.Controller/MediaBrowser.Controller.csproj

@@ -95,6 +95,7 @@
     <Compile Include="Chapters\IChapterProvider.cs" />
     <Compile Include="Chapters\ChapterResponse.cs" />
     <Compile Include="Collections\CollectionCreationOptions.cs" />
+    <Compile Include="Collections\CollectionEvents.cs" />
     <Compile Include="Collections\ICollectionManager.cs" />
     <Compile Include="Dlna\ControlRequest.cs" />
     <Compile Include="Dlna\ControlResponse.cs" />
@@ -233,6 +234,9 @@
     <Compile Include="Providers\IRemoteMetadataProvider.cs" />
     <Compile Include="Providers\VideoContentType.cs" />
     <Compile Include="RelatedMedia\IRelatedMediaProvider.cs" />
+    <Compile Include="Security\AuthenticationInfo.cs" />
+    <Compile Include="Security\AuthenticationInfoQuery.cs" />
+    <Compile Include="Security\IAuthenticationRepository.cs" />
     <Compile Include="Security\IEncryptionManager.cs" />
     <Compile Include="Subtitles\ISubtitleManager.cs" />
     <Compile Include="Subtitles\ISubtitleProvider.cs" />

+ 1 - 0
MediaBrowser.Controller/Providers/IMetadataProvider.cs

@@ -1,4 +1,5 @@
 using MediaBrowser.Controller.Entities;
+using System.Collections.Generic;
 
 namespace MediaBrowser.Controller.Providers
 {

+ 61 - 0
MediaBrowser.Controller/Security/AuthenticationInfo.cs

@@ -0,0 +1,61 @@
+using System;
+
+namespace MediaBrowser.Controller.Security
+{
+    public class AuthenticationInfo
+    {
+        /// <summary>
+        /// Gets or sets the identifier.
+        /// </summary>
+        /// <value>The identifier.</value>
+        public string Id { get; set; }
+
+        /// <summary>
+        /// Gets or sets the access token.
+        /// </summary>
+        /// <value>The access token.</value>
+        public string AccessToken { get; set; }
+
+        /// <summary>
+        /// Gets or sets the device identifier.
+        /// </summary>
+        /// <value>The device identifier.</value>
+        public string DeviceId { get; set; }
+
+        /// <summary>
+        /// Gets or sets the name of the application.
+        /// </summary>
+        /// <value>The name of the application.</value>
+        public string AppName { get; set; }
+
+        /// <summary>
+        /// Gets or sets the name of the device.
+        /// </summary>
+        /// <value>The name of the device.</value>
+        public string DeviceName { get; set; }
+
+        /// <summary>
+        /// Gets or sets the user identifier.
+        /// </summary>
+        /// <value>The user identifier.</value>
+        public string UserId { get; set; }
+
+        /// <summary>
+        /// Gets or sets a value indicating whether this instance is active.
+        /// </summary>
+        /// <value><c>true</c> if this instance is active; otherwise, <c>false</c>.</value>
+        public bool IsActive { get; set; }
+
+        /// <summary>
+        /// Gets or sets the date created.
+        /// </summary>
+        /// <value>The date created.</value>
+        public DateTime DateCreated { get; set; }
+
+        /// <summary>
+        /// Gets or sets the date revoked.
+        /// </summary>
+        /// <value>The date revoked.</value>
+        public DateTime? DateRevoked { get; set; }
+    }
+}

+ 42 - 0
MediaBrowser.Controller/Security/AuthenticationInfoQuery.cs

@@ -0,0 +1,42 @@
+
+namespace MediaBrowser.Controller.Security
+{
+    public class AuthenticationInfoQuery
+    {
+        /// <summary>
+        /// Gets or sets the device identifier.
+        /// </summary>
+        /// <value>The device identifier.</value>
+        public string DeviceId { 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 access token.
+        /// </summary>
+        /// <value>The access token.</value>
+        public string AccessToken { get; set; }
+        
+        /// <summary>
+        /// Gets or sets a value indicating whether this instance is active.
+        /// </summary>
+        /// <value><c>null</c> if [is active] contains no value, <c>true</c> if [is active]; otherwise, <c>false</c>.</value>
+        public bool? IsActive { get; set; }
+
+        /// <summary>
+        /// Gets or sets the start index.
+        /// </summary>
+        /// <value>The start index.</value>
+        public int? StartIndex { get; set; }
+
+        /// <summary>
+        /// Gets or sets the limit.
+        /// </summary>
+        /// <value>The limit.</value>
+        public int? Limit { get; set; }
+    }
+}

+ 39 - 0
MediaBrowser.Controller/Security/IAuthenticationRepository.cs

@@ -0,0 +1,39 @@
+using MediaBrowser.Model.Querying;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Security
+{
+    public interface IAuthenticationRepository
+    {
+        /// <summary>
+        /// Creates the specified information.
+        /// </summary>
+        /// <param name="info">The information.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task.</returns>
+        Task Create(AuthenticationInfo info, CancellationToken cancellationToken);
+
+        /// <summary>
+        /// Updates the specified information.
+        /// </summary>
+        /// <param name="info">The information.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task.</returns>
+        Task Update(AuthenticationInfo info, CancellationToken cancellationToken);
+
+        /// <summary>
+        /// Gets the specified query.
+        /// </summary>
+        /// <param name="query">The query.</param>
+        /// <returns>QueryResult{AuthenticationInfo}.</returns>
+        QueryResult<AuthenticationInfo> Get(AuthenticationInfoQuery query);
+
+        /// <summary>
+        /// Gets the specified identifier.
+        /// </summary>
+        /// <param name="id">The identifier.</param>
+        /// <returns>AuthenticationInfo.</returns>
+        AuthenticationInfo Get(string id);
+    }
+}

+ 23 - 2
MediaBrowser.Controller/Session/ISessionManager.cs

@@ -259,7 +259,28 @@ namespace MediaBrowser.Controller.Session
         /// <summary>
         /// Validates the security token.
         /// </summary>
-        /// <param name="token">The token.</param>
-        void ValidateSecurityToken(string token);
+        /// <param name="accessToken">The access token.</param>
+        void ValidateSecurityToken(string accessToken);
+
+        /// <summary>
+        /// Logouts the specified access token.
+        /// </summary>
+        /// <param name="accessToken">The access token.</param>
+        /// <returns>Task.</returns>
+        Task Logout(string accessToken);
+
+        /// <summary>
+        /// Revokes the user tokens.
+        /// </summary>
+        /// <param name="userId">The user identifier.</param>
+        /// <returns>Task.</returns>
+        Task RevokeUserTokens(string userId);
+
+        /// <summary>
+        /// Determines whether the specified remote endpoint is local.
+        /// </summary>
+        /// <param name="remoteEndpoint">The remote endpoint.</param>
+        /// <returns><c>true</c> if the specified remote endpoint is local; otherwise, <c>false</c>.</returns>
+        bool IsLocal(string remoteEndpoint);
     }
 }

+ 1 - 1
MediaBrowser.Controller/Session/SessionInfo.cs

@@ -123,7 +123,7 @@ namespace MediaBrowser.Controller.Session
         public List<string> SupportedCommands { get; set; }
 
         public TranscodingInfo TranscodingInfo { get; set; }
-        
+
         /// <summary>
         /// Gets a value indicating whether this instance is active.
         /// </summary>

+ 5 - 5
MediaBrowser.LocalMetadata/BaseXmlProvider.cs

@@ -1,11 +1,11 @@
-using System;
-using System.IO;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Common.IO;
+using MediaBrowser.Common.IO;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Logging;
+using System;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
 
 namespace MediaBrowser.LocalMetadata
 {

+ 17 - 2
MediaBrowser.Model/ApiClient/IApiClient.cs

@@ -246,7 +246,7 @@ namespace MediaBrowser.Model.ApiClient
         /// Gets the client session asynchronous.
         /// </summary>
         /// <returns>Task{SessionInfoDto}.</returns>
-        Task<SessionInfoDto> GetCurrentSessionAsync();
+        Task<SessionInfoDto> GetCurrentSessionAsync(CancellationToken cancellationToken);
         
         /// <summary>
         /// Gets the item counts async.
@@ -644,6 +644,13 @@ namespace MediaBrowser.Model.ApiClient
         /// <returns>Task.</returns>
         Task SetVolume(string sessionId, int volume);
 
+        /// <summary>
+        /// Stops the transcoding processes.
+        /// </summary>
+        /// <param name="deviceId">The device identifier.</param>
+        /// <returns>Task.</returns>
+        Task StopTranscodingProcesses(string deviceId);
+
         /// <summary>
         /// Sets the index of the audio stream.
         /// </summary>
@@ -984,7 +991,7 @@ namespace MediaBrowser.Model.ApiClient
         /// <param name="query">The query.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <returns>Task{LiveTvInfo}.</returns>
-        Task<QueryResult<ChannelInfoDto>> GetLiveTvChannelsAsync(ChannelQuery query, CancellationToken cancellationToken);
+        Task<QueryResult<ChannelInfoDto>> GetLiveTvChannelsAsync(LiveTvChannelQuery query, CancellationToken cancellationToken);
 
         /// <summary>
         /// Gets the live tv channel asynchronous.
@@ -1187,5 +1194,13 @@ namespace MediaBrowser.Model.ApiClient
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <returns>Task{QueryResult{BaseItemDto}}.</returns>
         Task<QueryResult<BaseItemDto>> GetChannels(ChannelQuery query, CancellationToken cancellationToken);
+
+        /// <summary>
+        /// Gets the latest channel items.
+        /// </summary>
+        /// <param name="query">The query.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task{QueryResult{BaseItemDto}}.</returns>
+        Task<QueryResult<BaseItemDto>> GetLatestChannelItems(AllChannelMediaQuery query, CancellationToken cancellationToken);
     }
 }

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

@@ -210,6 +210,8 @@ namespace MediaBrowser.Model.Configuration
 
         public bool DefaultMetadataSettingsApplied { get; set; }
 
+        public bool EnableTokenAuthentication { get; set; }
+
         /// <summary>
         /// Initializes a new instance of the <see cref="ServerConfiguration" /> class.
         /// </summary>

+ 1 - 1
MediaBrowser.Model/Users/AuthenticationResult.cs

@@ -21,6 +21,6 @@ namespace MediaBrowser.Model.Users
         /// Gets or sets the authentication token.
         /// </summary>
         /// <value>The authentication token.</value>
-        public string AuthenticationToken { get; set; }
+        public string AccessToken { get; set; }
     }
 }

+ 0 - 1
MediaBrowser.Providers/Manager/MetadataService.cs

@@ -8,7 +8,6 @@ using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Logging;
 using System;
 using System.Collections.Generic;
-using System.IO;
 using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;

+ 1 - 1
MediaBrowser.Providers/Manager/ProviderUtils.cs

@@ -170,7 +170,7 @@ namespace MediaBrowser.Providers.Manager
                 var key = id.Key;
 
                 // Don't replace existing Id's.
-                if (!target.ProviderIds.ContainsKey(key))
+                if (replaceData || !target.ProviderIds.ContainsKey(key))
                 {
                     target.ProviderIds[key] = id.Value;
                 }

+ 1 - 1
MediaBrowser.Providers/TV/MovieDbEpisodeImageProvider.cs

@@ -19,7 +19,7 @@ using System.Threading.Tasks;
 
 namespace MediaBrowser.Providers.TV
 {
-    public class MovieDbEpisodeImageProvider : IRemoteImageProvider, IHasOrder
+    public class MovieDbEpisodeImageProvider/* : IRemoteImageProvider, IHasOrder*/
     {
         private const string GetTvInfo3 = @"http://api.themoviedb.org/3/tv/{0}/season/{1}/episode/{2}?api_key={3}&append_to_response=images,external_ids,credits,videos";
         private readonly IHttpClient _httpClient;

+ 2 - 1
MediaBrowser.Server.Implementations/Channels/ChannelManager.cs

@@ -1133,6 +1133,8 @@ namespace MediaBrowser.Server.Implementations.Channels
                 item.CommunityRating = info.CommunityRating;
                 item.OfficialRating = info.OfficialRating;
                 item.Overview = info.Overview;
+                item.IndexNumber = info.IndexNumber;
+                item.ParentIndexNumber = info.ParentIndexNumber;
                 item.People = info.People;
                 item.PremiereDate = info.PremiereDate;
                 item.ProductionYear = info.ProductionYear;
@@ -1159,7 +1161,6 @@ namespace MediaBrowser.Server.Implementations.Channels
 
             if (channelMediaItem != null)
             {
-                channelMediaItem.IsInfiniteStream = info.IsInfiniteStream;
                 channelMediaItem.ContentType = info.ContentType;
                 channelMediaItem.ChannelMediaSources = info.MediaSources;
 

+ 52 - 5
MediaBrowser.Server.Implementations/Collections/CollectionManager.cs

@@ -1,9 +1,11 @@
-using MediaBrowser.Common.IO;
+using MediaBrowser.Common.Events;
+using MediaBrowser.Common.IO;
 using MediaBrowser.Controller.Collections;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Movies;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Logging;
 using MoreLinq;
 using System;
 using System.Collections.Generic;
@@ -19,12 +21,18 @@ namespace MediaBrowser.Server.Implementations.Collections
         private readonly ILibraryManager _libraryManager;
         private readonly IFileSystem _fileSystem;
         private readonly ILibraryMonitor _iLibraryMonitor;
+        private readonly ILogger _logger;
 
-        public CollectionManager(ILibraryManager libraryManager, IFileSystem fileSystem, ILibraryMonitor iLibraryMonitor)
+        public event EventHandler<CollectionCreatedEventArgs> CollectionCreated;
+        public event EventHandler<CollectionModifiedEventArgs> ItemsAddedToCollection;
+        public event EventHandler<CollectionModifiedEventArgs> ItemsRemovedFromCollection;
+
+        public CollectionManager(ILibraryManager libraryManager, IFileSystem fileSystem, ILibraryMonitor iLibraryMonitor, ILogger logger)
         {
             _libraryManager = libraryManager;
             _fileSystem = fileSystem;
             _iLibraryMonitor = iLibraryMonitor;
+            _logger = logger;
         }
 
         public Folder GetCollectionsFolder(string userId)
@@ -74,9 +82,16 @@ namespace MediaBrowser.Server.Implementations.Collections
 
                 if (options.ItemIdList.Count > 0)
                 {
-                    await AddToCollection(collection.Id, options.ItemIdList);
+                    await AddToCollection(collection.Id, options.ItemIdList, false);
                 }
 
+                EventHelper.FireEventIfNotNull(CollectionCreated, this, new CollectionCreatedEventArgs
+                {
+                    Collection = collection,
+                    Options = options
+
+                }, _logger);
+
                 return collection;
             }
             finally
@@ -113,7 +128,12 @@ namespace MediaBrowser.Server.Implementations.Collections
             return GetCollectionsFolder(string.Empty);
         }
 
-        public async Task AddToCollection(Guid collectionId, IEnumerable<Guid> ids)
+        public Task AddToCollection(Guid collectionId, IEnumerable<Guid> ids)
+        {
+            return AddToCollection(collectionId, ids, true);
+        }
+
+        private async Task AddToCollection(Guid collectionId, IEnumerable<Guid> ids, bool fireEvent)
         {
             var collection = _libraryManager.GetItemById(collectionId) as BoxSet;
 
@@ -123,6 +143,7 @@ namespace MediaBrowser.Server.Implementations.Collections
             }
 
             var list = new List<LinkedChild>();
+            var itemList = new List<BaseItem>();
             var currentLinkedChildren = collection.GetLinkedChildren().ToList();
 
             foreach (var itemId in ids)
@@ -134,6 +155,8 @@ namespace MediaBrowser.Server.Implementations.Collections
                     throw new ArgumentException("No item exists with the supplied Id");
                 }
 
+                itemList.Add(item);
+                
                 if (currentLinkedChildren.Any(i => i.Id == itemId))
                 {
                     throw new ArgumentException("Item already exists in collection");
@@ -165,6 +188,16 @@ namespace MediaBrowser.Server.Implementations.Collections
             await collection.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
 
             await collection.RefreshMetadata(CancellationToken.None).ConfigureAwait(false);
+
+            if (fireEvent)
+            {
+                EventHelper.FireEventIfNotNull(ItemsRemovedFromCollection, this, new CollectionModifiedEventArgs
+                {
+                    Collection = collection,
+                    ItemsChanged = itemList
+
+                }, _logger);
+            }
         }
 
         public async Task RemoveFromCollection(Guid collectionId, IEnumerable<Guid> itemIds)
@@ -177,6 +210,7 @@ namespace MediaBrowser.Server.Implementations.Collections
             }
 
             var list = new List<LinkedChild>();
+            var itemList = new List<BaseItem>();
 
             foreach (var itemId in itemIds)
             {
@@ -190,6 +224,12 @@ namespace MediaBrowser.Server.Implementations.Collections
                 list.Add(child);
 
                 var childItem = _libraryManager.GetItemById(itemId);
+
+                if (childItem != null)
+                {
+                    itemList.Add(childItem);
+                }
+
                 var supportsGrouping = childItem as ISupportsBoxSetGrouping;
 
                 if (supportsGrouping != null)
@@ -221,7 +261,7 @@ namespace MediaBrowser.Server.Implementations.Collections
                 {
                     File.Delete(file);
                 }
-                
+
                 foreach (var child in list)
                 {
                     collection.LinkedChildren.Remove(child);
@@ -238,6 +278,13 @@ namespace MediaBrowser.Server.Implementations.Collections
             await collection.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
 
             await collection.RefreshMetadata(CancellationToken.None).ConfigureAwait(false);
+
+            EventHelper.FireEventIfNotNull(ItemsRemovedFromCollection, this, new CollectionModifiedEventArgs
+            {
+                Collection = collection,
+                ItemsChanged = itemList
+
+            }, _logger);
         }
 
         public IEnumerable<BaseItem> CollapseItemsWithinBoxSets(IEnumerable<BaseItem> items, User user)

+ 24 - 24
MediaBrowser.Server.Implementations/HttpServer/Security/AuthService.cs

@@ -1,4 +1,4 @@
-using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Session;
@@ -13,9 +13,12 @@ namespace MediaBrowser.Server.Implementations.HttpServer.Security
 {
     public class AuthService : IAuthService
     {
-        public AuthService(IUserManager userManager, ISessionManager sessionManager, IAuthorizationContext authorizationContext)
+        private readonly IServerConfigurationManager _config;
+
+        public AuthService(IUserManager userManager, ISessionManager sessionManager, IAuthorizationContext authorizationContext, IServerConfigurationManager config)
         {
             AuthorizationContext = authorizationContext;
+            _config = config;
             SessionManager = sessionManager;
             UserManager = userManager;
         }
@@ -54,28 +57,30 @@ namespace MediaBrowser.Server.Implementations.HttpServer.Security
             //This code is executed before the service
             var auth = AuthorizationContext.GetAuthorizationInfo(req);
 
-            if (string.IsNullOrWhiteSpace(auth.Token))
+            if (!string.IsNullOrWhiteSpace(auth.Token) || _config.Configuration.EnableTokenAuthentication)
             {
-                // Legacy
-                // TODO: Deprecate this in Oct 2014
-
-                User user = null;
-
-                if (!string.IsNullOrWhiteSpace(auth.UserId))
-                {
-                    var userId = auth.UserId;
+                SessionManager.ValidateSecurityToken(auth.Token);
+            }
 
-                    user = UserManager.GetUserById(new Guid(userId));
-                }
+            var user = string.IsNullOrWhiteSpace(auth.UserId)
+                ? null
+                : UserManager.GetUserById(new Guid(auth.UserId));
 
-                if (user == null || user.Configuration.IsDisabled)
-                {
-                    throw new UnauthorizedAccessException("Unauthorized access.");
-                }
+            if (user != null && user.Configuration.IsDisabled)
+            {
+                throw new UnauthorizedAccessException("User account has been disabled.");
             }
-            else
+
+            if (!string.IsNullOrWhiteSpace(auth.DeviceId) &&
+                !string.IsNullOrWhiteSpace(auth.Client) &&
+                !string.IsNullOrWhiteSpace(auth.Device))
             {
-                SessionManager.ValidateSecurityToken(auth.Token);
+                SessionManager.LogSessionActivity(auth.Client,
+                    auth.Version,
+                    auth.DeviceId,
+                    auth.Device,
+                    req.RemoteIp,
+                    user);
             }
         }
 
@@ -108,11 +113,6 @@ namespace MediaBrowser.Server.Implementations.HttpServer.Security
             }
         }
 
-        private void LogRequest()
-        {
-
-        }
-
         protected bool DoHtmlRedirectIfConfigured(IRequest req, IResponse res, bool includeRedirectParam = false)
         {
             var htmlRedirect = this.HtmlRedirect ?? AuthenticateService.HtmlRedirect;

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

@@ -216,6 +216,7 @@
     <Compile Include="ScheduledTasks\ChapterImagesTask.cs" />
     <Compile Include="ScheduledTasks\RefreshIntrosTask.cs" />
     <Compile Include="ScheduledTasks\RefreshMediaLibraryTask.cs" />
+    <Compile Include="Security\AuthenticationRepository.cs" />
     <Compile Include="Security\EncryptionManager.cs" />
     <Compile Include="ServerApplicationPaths.cs" />
     <Compile Include="ServerManager\ServerManager.cs" />

+ 1 - 1
MediaBrowser.Server.Implementations/Persistence/SqliteFileOrganizationRepository.cs

@@ -14,7 +14,7 @@ using System.Threading.Tasks;
 
 namespace MediaBrowser.Server.Implementations.Persistence
 {
-    public class SqliteFileOrganizationRepository : IFileOrganizationRepository
+    public class SqliteFileOrganizationRepository : IFileOrganizationRepository, IDisposable
     {
         private IDbConnection _connection;
 

+ 338 - 0
MediaBrowser.Server.Implementations/Security/AuthenticationRepository.cs

@@ -0,0 +1,338 @@
+using MediaBrowser.Controller;
+using MediaBrowser.Controller.Security;
+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.Security
+{
+    public class AuthenticationRepository : IAuthenticationRepository
+    {
+        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 _saveInfoCommand;
+
+        public AuthenticationRepository(ILogger logger, IServerApplicationPaths appPaths)
+        {
+            _logger = logger;
+            _appPaths = appPaths;
+        }
+
+        public async Task Initialize()
+        {
+            var dbFile = Path.Combine(_appPaths.DataPath, "authentication.db");
+
+            _connection = await SqliteExtensions.ConnectToDb(dbFile, _logger).ConfigureAwait(false);
+
+            string[] queries = {
+
+                                "create table if not exists AccessTokens (Id GUID PRIMARY KEY, AccessToken TEXT NOT NULL, DeviceId TEXT, AppName TEXT, DeviceName TEXT, UserId TEXT, IsActive BIT, DateCreated DATETIME NOT NULL, DateRevoked DATETIME)",
+                                "create index if not exists idx_AccessTokens on AccessTokens(Id)",
+
+                                //pragmas
+                                "pragma temp_store = memory",
+
+                                "pragma shrink_memory"
+                               };
+
+            _connection.RunQueries(queries, _logger);
+
+            PrepareStatements();
+        }
+
+        private void PrepareStatements()
+        {
+            _saveInfoCommand = _connection.CreateCommand();
+            _saveInfoCommand.CommandText = "replace into AccessTokens (Id, AccessToken, DeviceId, AppName, DeviceName, UserId, IsActive, DateCreated, DateRevoked) values (@Id, @AccessToken, @DeviceId, @AppName, @DeviceName, @UserId, @IsActive, @DateCreated, @DateRevoked)";
+
+            _saveInfoCommand.Parameters.Add(_saveInfoCommand, "@Id");
+            _saveInfoCommand.Parameters.Add(_saveInfoCommand, "@AccessToken");
+            _saveInfoCommand.Parameters.Add(_saveInfoCommand, "@DeviceId");
+            _saveInfoCommand.Parameters.Add(_saveInfoCommand, "@AppName");
+            _saveInfoCommand.Parameters.Add(_saveInfoCommand, "@DeviceName");
+            _saveInfoCommand.Parameters.Add(_saveInfoCommand, "@UserId");
+            _saveInfoCommand.Parameters.Add(_saveInfoCommand, "@IsActive");
+            _saveInfoCommand.Parameters.Add(_saveInfoCommand, "@DateCreated");
+            _saveInfoCommand.Parameters.Add(_saveInfoCommand, "@DateRevoked");
+        }
+
+        public Task Create(AuthenticationInfo info, CancellationToken cancellationToken)
+        {
+            info.Id = Guid.NewGuid().ToString("N");
+
+            return Update(info, cancellationToken);
+        }
+
+        public async Task Update(AuthenticationInfo info, CancellationToken cancellationToken)
+        {
+            if (info == null)
+            {
+                throw new ArgumentNullException("info");
+            }
+
+            cancellationToken.ThrowIfCancellationRequested();
+
+            await _writeLock.WaitAsync(cancellationToken).ConfigureAwait(false);
+
+            IDbTransaction transaction = null;
+
+            try
+            {
+                transaction = _connection.BeginTransaction();
+
+                var index = 0;
+
+                _saveInfoCommand.GetParameter(index++).Value = new Guid(info.Id);
+                _saveInfoCommand.GetParameter(index++).Value = info.AccessToken;
+                _saveInfoCommand.GetParameter(index++).Value = info.DeviceId;
+                _saveInfoCommand.GetParameter(index++).Value = info.AppName;
+                _saveInfoCommand.GetParameter(index++).Value = info.DeviceName;
+                _saveInfoCommand.GetParameter(index++).Value = info.UserId;
+                _saveInfoCommand.GetParameter(index++).Value = info.IsActive;
+                _saveInfoCommand.GetParameter(index++).Value = info.DateCreated;
+                _saveInfoCommand.GetParameter(index++).Value = info.DateRevoked;
+
+                _saveInfoCommand.Transaction = transaction;
+
+                _saveInfoCommand.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();
+            }
+        }
+
+        private const string BaseSelectText = "select Id, AccessToken, DeviceId, AppName, DeviceName, UserId, IsActive, DateCreated, DateRevoked from AccessTokens";
+
+        public QueryResult<AuthenticationInfo> Get(AuthenticationInfoQuery query)
+        {
+            if (query == null)
+            {
+                throw new ArgumentNullException("query");
+            }
+
+            using (var cmd = _connection.CreateCommand())
+            {
+                cmd.CommandText = BaseSelectText;
+
+                var whereClauses = new List<string>();
+
+                var startIndex = query.StartIndex ?? 0;
+
+                if (startIndex > 0)
+                {
+                    whereClauses.Add(string.Format("Id NOT IN (SELECT Id FROM AccessTokens ORDER BY DateCreated LIMIT {0})",
+                        startIndex.ToString(_usCulture)));
+                }
+
+                if (!string.IsNullOrWhiteSpace(query.AccessToken))
+                {
+                    whereClauses.Add("AccessToken=@AccessToken");
+                    cmd.Parameters.Add(cmd, "@AccessToken", DbType.String).Value = query.AccessToken;
+                }
+
+                if (!string.IsNullOrWhiteSpace(query.UserId))
+                {
+                    whereClauses.Add("UserId=@UserId");
+                    cmd.Parameters.Add(cmd, "@UserId", DbType.String).Value = query.UserId;
+                }
+
+                if (!string.IsNullOrWhiteSpace(query.DeviceId))
+                {
+                    whereClauses.Add("DeviceId=@DeviceId");
+                    cmd.Parameters.Add(cmd, "@DeviceId", DbType.String).Value = query.DeviceId;
+                }
+
+                if (query.IsActive.HasValue)
+                {
+                    whereClauses.Add("IsActive=@IsActive");
+                    cmd.Parameters.Add(cmd, "@IsActive", DbType.Boolean).Value = query.IsActive.Value;
+                }
+
+                if (whereClauses.Count > 0)
+                {
+                    cmd.CommandText += " where " + string.Join(" AND ", whereClauses.ToArray());
+                }
+
+                cmd.CommandText += " ORDER BY DateCreated";
+
+                if (query.Limit.HasValue)
+                {
+                    cmd.CommandText += " LIMIT " + query.Limit.Value.ToString(_usCulture);
+                }
+
+                cmd.CommandText += "; select count (Id) from AccessTokens";
+
+                var list = new List<AuthenticationInfo>();
+                var count = 0;
+
+                using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess))
+                {
+                    while (reader.Read())
+                    {
+                        list.Add(Get(reader));
+                    }
+
+                    if (reader.NextResult() && reader.Read())
+                    {
+                        count = reader.GetInt32(0);
+                    }
+                }
+
+                return new QueryResult<AuthenticationInfo>()
+                {
+                    Items = list.ToArray(),
+                    TotalRecordCount = count
+                };
+            }
+        }
+
+        public AuthenticationInfo Get(string id)
+        {
+            if (string.IsNullOrEmpty(id))
+            {
+                throw new ArgumentNullException("id");
+            }
+
+            var guid = new Guid(id);
+
+            using (var cmd = _connection.CreateCommand())
+            {
+                cmd.CommandText = BaseSelectText + " where Id=@Id";
+
+                cmd.Parameters.Add(cmd, "@Id", DbType.Guid).Value = guid;
+
+                using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult | CommandBehavior.SingleRow))
+                {
+                    if (reader.Read())
+                    {
+                        return Get(reader);
+                    }
+                }
+            }
+
+            return null;
+        }
+
+        private AuthenticationInfo Get(IDataReader reader)
+        {
+            var s = "select Id, AccessToken, DeviceId, AppName, DeviceName, UserId, IsActive, DateCreated, DateRevoked from AccessTokens";
+
+            var info = new AuthenticationInfo
+            {
+                Id = reader.GetGuid(0).ToString("N"),
+                AccessToken = reader.GetString(1)
+            };
+
+            if (!reader.IsDBNull(2))
+            {
+                info.DeviceId = reader.GetString(2);
+            }
+
+            if (!reader.IsDBNull(3))
+            {
+                info.AppName = reader.GetString(3);
+            }
+
+            if (!reader.IsDBNull(4))
+            {
+                info.DeviceName = reader.GetString(4);
+            }
+            
+            if (!reader.IsDBNull(5))
+            {
+                info.UserId = reader.GetString(5);
+            }
+
+            info.IsActive = reader.GetBoolean(6);
+            info.DateCreated = reader.GetDateTime(7);
+
+            if (!reader.IsDBNull(8))
+            {
+                info.DateRevoked = reader.GetDateTime(8);
+            }
+         
+            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);
+                }
+            }
+        }
+    }
+}

+ 147 - 12
MediaBrowser.Server.Implementations/Session/SessionManager.cs

@@ -1,6 +1,4 @@
-using System.Security.Cryptography;
-using System.Text;
-using MediaBrowser.Common.Events;
+using MediaBrowser.Common.Events;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Controller;
@@ -13,12 +11,14 @@ using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.LiveTv;
 using MediaBrowser.Controller.Persistence;
+using MediaBrowser.Controller.Security;
 using MediaBrowser.Controller.Session;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Library;
 using MediaBrowser.Model.Logging;
 using MediaBrowser.Model.Serialization;
 using MediaBrowser.Model.Session;
+using MediaBrowser.Model.Users;
 using System;
 using System.Collections.Concurrent;
 using System.Collections.Generic;
@@ -27,7 +27,6 @@ using System.IO;
 using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
-using MediaBrowser.Model.Users;
 
 namespace MediaBrowser.Server.Implementations.Session
 {
@@ -62,6 +61,8 @@ namespace MediaBrowser.Server.Implementations.Session
         private readonly IJsonSerializer _jsonSerializer;
         private readonly IServerApplicationHost _appHost;
 
+        private readonly IAuthenticationRepository _authRepo;
+
         /// <summary>
         /// Gets or sets the configuration manager.
         /// </summary>
@@ -104,7 +105,7 @@ namespace MediaBrowser.Server.Implementations.Session
         /// <param name="logger">The logger.</param>
         /// <param name="userRepository">The user repository.</param>
         /// <param name="libraryManager">The library manager.</param>
-        public SessionManager(IUserDataManager userDataRepository, IServerConfigurationManager configurationManager, ILogger logger, IUserRepository userRepository, ILibraryManager libraryManager, IUserManager userManager, IMusicManager musicManager, IDtoService dtoService, IImageProcessor imageProcessor, IItemRepository itemRepo, IJsonSerializer jsonSerializer, IServerApplicationHost appHost, IHttpClient httpClient)
+        public SessionManager(IUserDataManager userDataRepository, IServerConfigurationManager configurationManager, ILogger logger, IUserRepository userRepository, ILibraryManager libraryManager, IUserManager userManager, IMusicManager musicManager, IDtoService dtoService, IImageProcessor imageProcessor, IItemRepository itemRepo, IJsonSerializer jsonSerializer, IServerApplicationHost appHost, IHttpClient httpClient, IAuthenticationRepository authRepo)
         {
             _userDataRepository = userDataRepository;
             _configurationManager = configurationManager;
@@ -119,6 +120,7 @@ namespace MediaBrowser.Server.Implementations.Session
             _jsonSerializer = jsonSerializer;
             _appHost = appHost;
             _httpClient = httpClient;
+            _authRepo = authRepo;
         }
 
         /// <summary>
@@ -204,7 +206,12 @@ namespace MediaBrowser.Server.Implementations.Session
         /// <returns>Task.</returns>
         /// <exception cref="System.ArgumentNullException">user</exception>
         /// <exception cref="System.UnauthorizedAccessException"></exception>
-        public async Task<SessionInfo> LogSessionActivity(string clientType, string appVersion, string deviceId, string deviceName, string remoteEndPoint, User user)
+        public async Task<SessionInfo> LogSessionActivity(string clientType,
+            string appVersion,
+            string deviceId,
+            string deviceName,
+            string remoteEndPoint,
+            User user)
         {
             if (string.IsNullOrEmpty(clientType))
             {
@@ -1157,7 +1164,37 @@ namespace MediaBrowser.Server.Implementations.Session
 
         public void ValidateSecurityToken(string token)
         {
+            if (string.IsNullOrWhiteSpace(token))
+            {
+                throw new UnauthorizedAccessException();
+            }
 
+            var result = _authRepo.Get(new AuthenticationInfoQuery
+            {
+                AccessToken = token
+            });
+
+            var info = result.Items.FirstOrDefault();
+
+            if (info == null)
+            {
+                throw new UnauthorizedAccessException();
+            }
+
+            if (!info.IsActive)
+            {
+                throw new UnauthorizedAccessException("Access token has expired.");
+            }
+
+            if (!string.IsNullOrWhiteSpace(info.UserId))
+            {
+                var user = _userManager.GetUserById(new Guid(info.UserId));
+
+                if (user == null || user.Configuration.IsDisabled)
+                {
+                    throw new UnauthorizedAccessException("User account has been disabled.");
+                }
+            }
         }
 
         /// <summary>
@@ -1175,7 +1212,7 @@ namespace MediaBrowser.Server.Implementations.Session
         /// <exception cref="UnauthorizedAccessException"></exception>
         public async Task<AuthenticationResult> AuthenticateNewSession(string username, string password, string clientType, string appVersion, string deviceId, string deviceName, string remoteEndPoint)
         {
-            var result = await _userManager.AuthenticateUser(username, password).ConfigureAwait(false);
+            var result = IsLocalhost(remoteEndPoint) || await _userManager.AuthenticateUser(username, password).ConfigureAwait(false);
 
             if (!result)
             {
@@ -1185,6 +1222,8 @@ namespace MediaBrowser.Server.Implementations.Session
             var user = _userManager.Users
                 .First(i => string.Equals(username, i.Name, StringComparison.OrdinalIgnoreCase));
 
+            var token = await GetAuthorizationToken(user.Id.ToString("N"), deviceId, clientType, deviceName).ConfigureAwait(false);
+
             var session = await LogSessionActivity(clientType,
                 appVersion,
                 deviceId,
@@ -1197,11 +1236,108 @@ namespace MediaBrowser.Server.Implementations.Session
             {
                 User = _dtoService.GetUserDto(user),
                 SessionInfo = GetSessionInfoDto(session),
-                AuthenticationToken = Guid.NewGuid().ToString("N")
+                AccessToken = token
             };
         }
 
-        private bool IsLocal(string remoteEndpoint)
+        private async Task<string> GetAuthorizationToken(string userId, string deviceId, string app, string deviceName)
+        {
+            var existing = _authRepo.Get(new AuthenticationInfoQuery
+            {
+                DeviceId = deviceId,
+                IsActive = true,
+                UserId = userId,
+                Limit = 1
+            });
+
+            if (existing.Items.Length > 0)
+            {
+                _logger.Debug("Reissuing access token");
+                return existing.Items[0].AccessToken;
+            }
+
+            var newToken = new AuthenticationInfo
+            {
+                AppName = app,
+                DateCreated = DateTime.UtcNow,
+                DeviceId = deviceId,
+                DeviceName = deviceName,
+                UserId = userId,
+                IsActive = true,
+                AccessToken = Guid.NewGuid().ToString("N")
+            };
+
+            _logger.Debug("Creating new access token for user {0}", userId);
+            await _authRepo.Create(newToken, CancellationToken.None).ConfigureAwait(false);
+
+            return newToken.AccessToken;
+        }
+
+        public async Task Logout(string accessToken)
+        {
+            if (string.IsNullOrWhiteSpace(accessToken))
+            {
+                throw new ArgumentNullException("accessToken");
+            }
+
+            var existing = _authRepo.Get(new AuthenticationInfoQuery
+            {
+                Limit = 1,
+                AccessToken = accessToken
+
+            }).Items.FirstOrDefault();
+
+            if (existing != null)
+            {
+                existing.IsActive = false;
+
+                await _authRepo.Update(existing, CancellationToken.None).ConfigureAwait(false);
+
+                var sessions = Sessions
+                    .Where(i => string.Equals(i.DeviceId, existing.DeviceId, StringComparison.OrdinalIgnoreCase))
+                    .ToList();
+
+                foreach (var session in sessions)
+                {
+                    try
+                    {
+                        ReportSessionEnded(session.Id);
+                    }
+                    catch (Exception ex)
+                    {
+                        _logger.ErrorException("Error reporting session ended", ex);
+                    }
+                }
+            }
+        }
+
+        public async Task RevokeUserTokens(string userId)
+        {
+            var existing = _authRepo.Get(new AuthenticationInfoQuery
+            {
+                IsActive = true,
+                UserId = userId
+            });
+
+            foreach (var info in existing.Items)
+            {
+                await Logout(info.AccessToken).ConfigureAwait(false);
+            }
+        }
+
+        private bool IsLocalhost(string remoteEndpoint)
+        {
+            if (string.IsNullOrWhiteSpace(remoteEndpoint))
+            {
+                throw new ArgumentNullException("remoteEndpoint");
+            }
+
+            return remoteEndpoint.IndexOf("localhost", StringComparison.OrdinalIgnoreCase) != -1 ||
+                remoteEndpoint.StartsWith("127.", StringComparison.OrdinalIgnoreCase) ||
+                remoteEndpoint.StartsWith("::", StringComparison.OrdinalIgnoreCase);
+        }
+
+        public bool IsLocal(string remoteEndpoint)
         {
             if (string.IsNullOrWhiteSpace(remoteEndpoint))
             {
@@ -1211,12 +1347,11 @@ namespace MediaBrowser.Server.Implementations.Session
             // Private address space:
             // http://en.wikipedia.org/wiki/Private_network
 
-            return remoteEndpoint.IndexOf("localhost", StringComparison.OrdinalIgnoreCase) != -1 ||
+            return IsLocalhost(remoteEndpoint) ||
                 remoteEndpoint.StartsWith("10.", StringComparison.OrdinalIgnoreCase) ||
                 remoteEndpoint.StartsWith("192.", StringComparison.OrdinalIgnoreCase) ||
                 remoteEndpoint.StartsWith("172.", StringComparison.OrdinalIgnoreCase) ||
-                remoteEndpoint.StartsWith("127.", StringComparison.OrdinalIgnoreCase) ||
-                remoteEndpoint.StartsWith("::", StringComparison.OrdinalIgnoreCase);
+                remoteEndpoint.StartsWith("169.", StringComparison.OrdinalIgnoreCase);
         }
 
         /// <summary>

+ 17 - 3
MediaBrowser.ServerApplication/ApplicationHost.cs

@@ -210,6 +210,8 @@ namespace MediaBrowser.ServerApplication
 
         private IUserViewManager UserViewManager { get; set; }
 
+        private IAuthenticationRepository AuthenticationRepository { get; set; }
+        
         /// <summary>
         /// Initializes a new instance of the <see cref="ApplicationHost"/> class.
         /// </summary>
@@ -586,6 +588,9 @@ namespace MediaBrowser.ServerApplication
             FileOrganizationRepository = await GetFileOrganizationRepository().ConfigureAwait(false);
             RegisterSingleInstance(FileOrganizationRepository);
 
+            AuthenticationRepository = await GetAuthenticationRepository().ConfigureAwait(false);
+            RegisterSingleInstance(AuthenticationRepository);
+
             UserManager = new UserManager(LogManager.GetLogger("UserManager"), ServerConfigurationManager, UserRepository, XmlSerializer);
             RegisterSingleInstance(UserManager);
 
@@ -625,7 +630,7 @@ namespace MediaBrowser.ServerApplication
             DtoService = new DtoService(Logger, LibraryManager, UserDataManager, ItemRepository, ImageProcessor, ServerConfigurationManager, FileSystemManager, ProviderManager, () => ChannelManager);
             RegisterSingleInstance(DtoService);
 
-            SessionManager = new SessionManager(UserDataManager, ServerConfigurationManager, Logger, UserRepository, LibraryManager, UserManager, musicManager, DtoService, ImageProcessor, ItemRepository, JsonSerializer, this, HttpClient);
+            SessionManager = new SessionManager(UserDataManager, ServerConfigurationManager, Logger, UserRepository, LibraryManager, UserManager, musicManager, DtoService, ImageProcessor, ItemRepository, JsonSerializer, this, HttpClient, AuthenticationRepository);
             RegisterSingleInstance(SessionManager);
 
             var newsService = new Server.Implementations.News.NewsService(ApplicationPaths, JsonSerializer);
@@ -651,7 +656,7 @@ namespace MediaBrowser.ServerApplication
             var connectionManager = new ConnectionManager(dlnaManager, ServerConfigurationManager, LogManager.GetLogger("UpnpConnectionManager"), HttpClient);
             RegisterSingleInstance<IConnectionManager>(connectionManager);
 
-            var collectionManager = new CollectionManager(LibraryManager, FileSystemManager, LibraryMonitor);
+            var collectionManager = new CollectionManager(LibraryManager, FileSystemManager, LibraryMonitor, LogManager.GetLogger("CollectionManager"));
             RegisterSingleInstance<ICollectionManager>(collectionManager);
 
             LiveTvManager = new LiveTvManager(ServerConfigurationManager, FileSystemManager, Logger, ItemRepository, ImageProcessor, UserDataManager, DtoService, UserManager, LibraryManager, TaskManager, LocalizationManager);
@@ -678,7 +683,7 @@ namespace MediaBrowser.ServerApplication
             var authContext = new AuthorizationContext();
             RegisterSingleInstance<IAuthorizationContext>(authContext);
             RegisterSingleInstance<ISessionContext>(new SessionContext(UserManager, authContext, SessionManager));
-            RegisterSingleInstance<IAuthService>(new AuthService(UserManager, SessionManager, authContext));
+            RegisterSingleInstance<IAuthService>(new AuthService(UserManager, SessionManager, authContext, ServerConfigurationManager));
 
             RegisterSingleInstance<ISubtitleEncoder>(new SubtitleEncoder(LibraryManager, LogManager.GetLogger("SubtitleEncoder"), ApplicationPaths, FileSystemManager, MediaEncoder));
 
@@ -755,6 +760,15 @@ namespace MediaBrowser.ServerApplication
             return repo;
         }
 
+        private async Task<IAuthenticationRepository> GetAuthenticationRepository()
+        {
+            var repo = new AuthenticationRepository(LogManager.GetLogger("AuthenticationRepository"), ServerConfigurationManager.ApplicationPaths);
+
+            await repo.Initialize().ConfigureAwait(false);
+
+            return repo;
+        }
+
         /// <summary>
         /// Configures the repositories.
         /// </summary>

+ 1 - 1
MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs

@@ -77,7 +77,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers
         /// <param name="cancellationToken">The cancellation token.</param>
         private void Fetch(T item, string metadataFile, XmlReaderSettings settings, Encoding encoding, CancellationToken cancellationToken)
         {
-            using (var streamReader = new StreamReader(metadataFile, encoding))
+            using (var streamReader = new StreamReader(metadataFile))
             {
                 // Use XmlReader for best performance
                 using (var reader = XmlReader.Create(streamReader, settings))

+ 5 - 5
MediaBrowser.XbmcMetadata/Providers/BaseNfoProvider.cs

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

+ 6 - 7
MediaBrowser.XbmcMetadata/Savers/SeasonXmlSaver.cs

@@ -1,15 +1,14 @@
-using System.Collections.Generic;
+using MediaBrowser.Common.IO;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Library;
+using System.Collections.Generic;
 using System.Globalization;
 using System.IO;
 using System.Security;
 using System.Text;
 using System.Threading;
-using MediaBrowser.Common.IO;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.TV;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Model.Entities;
 
 namespace MediaBrowser.XbmcMetadata.Savers
 {

+ 11 - 11
MediaBrowser.XbmcMetadata/Savers/XmlSaverHelpers.cs

@@ -1,12 +1,4 @@
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.IO;
-using System.Linq;
-using System.Security;
-using System.Text;
-using System.Xml;
-using MediaBrowser.Common.Extensions;
+using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.IO;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities;
@@ -18,6 +10,14 @@ using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Model.Configuration;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.XbmcMetadata.Configuration;
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Security;
+using System.Text;
+using System.Xml;
 
 namespace MediaBrowser.XbmcMetadata.Savers
 {
@@ -392,9 +392,9 @@ namespace MediaBrowser.XbmcMetadata.Savers
                 builder.Append("<writer>" + SecurityElement.Escape(person) + "</writer>");
             }
 
-            if (writers.Count > 0)
+            foreach (var person in writers)
             {
-                builder.Append("<credits>" + SecurityElement.Escape(string.Join(" / ", writers.ToArray())) + "</credits>");
+                builder.Append("<credits>" + SecurityElement.Escape(person) + "</credits>");
             }
 
             var hasTrailer = item as IHasTrailers;

+ 2 - 2
Nuget/MediaBrowser.Common.Internal.nuspec

@@ -2,7 +2,7 @@
 <package xmlns="http://schemas.microsoft.com/packaging/2011/08/nuspec.xsd">
     <metadata>
         <id>MediaBrowser.Common.Internal</id>
-        <version>3.0.412</version>
+        <version>3.0.414</version>
         <title>MediaBrowser.Common.Internal</title>
         <authors>Luke</authors>
         <owners>ebr,Luke,scottisafool</owners>
@@ -12,7 +12,7 @@
         <description>Contains common components shared by Media Browser Theater and Media Browser Server. Not intended for plugin developer consumption.</description>
         <copyright>Copyright © Media Browser 2013</copyright>
         <dependencies>
-            <dependency id="MediaBrowser.Common" version="3.0.412" />
+            <dependency id="MediaBrowser.Common" version="3.0.414" />
             <dependency id="NLog" version="2.1.0" />
             <dependency id="SimpleInjector" version="2.5.0" />
             <dependency id="sharpcompress" version="0.10.2" />

+ 1 - 1
Nuget/MediaBrowser.Common.nuspec

@@ -2,7 +2,7 @@
 <package xmlns="http://schemas.microsoft.com/packaging/2011/08/nuspec.xsd">
     <metadata>
         <id>MediaBrowser.Common</id>
-        <version>3.0.412</version>
+        <version>3.0.414</version>
         <title>MediaBrowser.Common</title>
         <authors>Media Browser Team</authors>
         <owners>ebr,Luke,scottisafool</owners>

+ 2 - 2
Nuget/MediaBrowser.Server.Core.nuspec

@@ -2,7 +2,7 @@
 <package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
     <metadata>
         <id>MediaBrowser.Server.Core</id>
-        <version>3.0.412</version>
+        <version>3.0.414</version>
         <title>Media Browser.Server.Core</title>
         <authors>Media Browser Team</authors>
         <owners>ebr,Luke,scottisafool</owners>
@@ -12,7 +12,7 @@
         <description>Contains core components required to build plugins for Media Browser Server.</description>
         <copyright>Copyright © Media Browser 2013</copyright>
         <dependencies>
-            <dependency id="MediaBrowser.Common" version="3.0.412" />
+            <dependency id="MediaBrowser.Common" version="3.0.414" />
         </dependencies>
     </metadata>
     <files>