Ver código fonte

Merge branch 'beta'

Luke Pulverenti 9 anos atrás
pai
commit
f57ce51d12
79 arquivos alterados com 1443 adições e 1081 exclusões
  1. 29 24
      Emby.Drawing/ImageProcessor.cs
  2. 6 3
      MediaBrowser.Api/Images/ImageService.cs
  3. 25 11
      MediaBrowser.Api/Playback/BaseStreamingService.cs
  4. 7 4
      MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs
  5. 10 12
      MediaBrowser.Api/Playback/Progressive/ProgressiveStreamWriter.cs
  6. 86 75
      MediaBrowser.Api/Reports/ReportsService.cs
  7. 1 2
      MediaBrowser.Api/StartupWizardService.cs
  8. 7 0
      MediaBrowser.Api/UserLibrary/BaseItemsRequest.cs
  9. 4 3
      MediaBrowser.Api/UserLibrary/ItemsService.cs
  10. 29 1
      MediaBrowser.Common.Implementations/HttpClientManager/HttpClientManager.cs
  11. 4 2
      MediaBrowser.Common.Implementations/Updates/GithubUpdater.cs
  12. 1 1
      MediaBrowser.Controller/Drawing/IImageProcessor.cs
  13. 1 1
      MediaBrowser.Controller/Entities/BaseItem.cs
  14. 21 1
      MediaBrowser.Controller/Entities/Book.cs
  15. 0 23
      MediaBrowser.Controller/Entities/Folder.cs
  16. 8 1
      MediaBrowser.Controller/Entities/IHasSeries.cs
  17. 44 28
      MediaBrowser.Controller/Entities/TV/Episode.cs
  18. 23 6
      MediaBrowser.Controller/Entities/TV/Season.cs
  19. 2 0
      MediaBrowser.Controller/Net/IHttpResultFactory.cs
  20. 8 0
      MediaBrowser.MediaEncoding/Encoder/BaseEncoder.cs
  21. 48 2
      MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
  22. 1 2
      MediaBrowser.Model.Portable/FodyWeavers.xml
  23. 2 10
      MediaBrowser.Model.Portable/MediaBrowser.Model.Portable.csproj
  24. 0 5
      MediaBrowser.Model.Portable/packages.config
  25. 0 3
      MediaBrowser.Model.net35/MediaBrowser.Model.net35.csproj
  26. 0 6
      MediaBrowser.Model/Configuration/BaseApplicationConfiguration.cs
  27. 0 1
      MediaBrowser.Model/Configuration/ServerConfiguration.cs
  28. 6 0
      MediaBrowser.Model/Dlna/AudioOptions.cs
  29. 3 0
      MediaBrowser.Model/Dlna/CodecProfile.cs
  30. 133 25
      MediaBrowser.Model/Dlna/StreamBuilder.cs
  31. 13 6
      MediaBrowser.Model/Dto/BaseItemDto.cs
  32. 2 8
      MediaBrowser.Model/Dto/BaseItemPerson.cs
  33. 2 6
      MediaBrowser.Model/Dto/ChapterInfoDto.cs
  34. 1 7
      MediaBrowser.Model/Dto/UserDto.cs
  35. 2 5
      MediaBrowser.Model/Dto/UserItemDataDto.cs
  36. 3 1
      MediaBrowser.Model/Entities/ChapterInfo.cs
  37. 1 8
      MediaBrowser.Model/Entities/DisplayPreferences.cs
  38. 0 8
      MediaBrowser.Model/Extensions/IHasPropertyChangedEvent.cs
  39. 1 8
      MediaBrowser.Model/LiveTv/BaseTimerInfoDto.cs
  40. 1 4
      MediaBrowser.Model/LiveTv/ChannelInfoDto.cs
  41. 7 0
      MediaBrowser.Model/LiveTv/TimerInfoDto.cs
  42. 0 1
      MediaBrowser.Model/MediaBrowser.Model.csproj
  43. 2 0
      MediaBrowser.Model/Querying/ItemFields.cs
  44. 1 4
      MediaBrowser.Model/Session/SessionInfoDto.cs
  45. 7 2
      MediaBrowser.Providers/MediaInfo/SubtitleResolver.cs
  46. 2 1
      MediaBrowser.Providers/TV/DummySeasonProvider.cs
  47. 3 1
      MediaBrowser.Providers/TV/MissingEpisodeProvider.cs
  48. 1 5
      MediaBrowser.Server.Implementations/Configuration/ServerConfigurationManager.cs
  49. 123 165
      MediaBrowser.Server.Implementations/Dto/DtoService.cs
  50. 56 0
      MediaBrowser.Server.Implementations/HttpServer/AsyncStreamWriterFunc.cs
  51. 19 22
      MediaBrowser.Server.Implementations/HttpServer/HttpListenerHost.cs
  52. 10 1
      MediaBrowser.Server.Implementations/HttpServer/HttpResultFactory.cs
  53. 0 285
      MediaBrowser.Server.Implementations/HttpServer/NetListener/HttpListenerServer.cs
  54. 63 11
      MediaBrowser.Server.Implementations/HttpServer/RangeRequestWriter.cs
  55. 71 64
      MediaBrowser.Server.Implementations/HttpServer/SocketSharp/RequestMono.cs
  56. 79 6
      MediaBrowser.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpRequest.cs
  57. 34 13
      MediaBrowser.Server.Implementations/HttpServer/StreamWriter.cs
  58. 1 1
      MediaBrowser.Server.Implementations/IO/LibraryMonitor.cs
  59. 35 20
      MediaBrowser.Server.Implementations/Library/LibraryManager.cs
  60. 39 9
      MediaBrowser.Server.Implementations/Library/Resolvers/PhotoResolver.cs
  61. 21 2
      MediaBrowser.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs
  62. 4 2
      MediaBrowser.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs
  63. 0 2
      MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs
  64. 43 16
      MediaBrowser.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs
  65. 6 7
      MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj
  66. 2 0
      MediaBrowser.Server.Implementations/MediaEncoder/EncodingManager.cs
  67. 37 12
      MediaBrowser.Server.Implementations/Persistence/CleanDatabaseScheduledTask.cs
  68. 131 8
      MediaBrowser.Server.Implementations/Persistence/SqliteItemRepository.cs
  69. 9 6
      MediaBrowser.Server.Implementations/Playlists/PlaylistManager.cs
  70. 2 22
      MediaBrowser.Server.Implementations/Sorting/SeriesSortNameComparer.cs
  71. 12 2
      MediaBrowser.Server.Implementations/TV/TVSeriesManager.cs
  72. 0 5
      MediaBrowser.Server.Implementations/UserViews/CollectionFolderImageProvider.cs
  73. 0 5
      MediaBrowser.Server.Implementations/UserViews/DynamicImageProvider.cs
  74. 2 2
      MediaBrowser.Server.Implementations/packages.config
  75. 1 1
      MediaBrowser.Server.Startup.Common/ApplicationHost.cs
  76. 76 7
      MediaBrowser.ServerApplication/MainStartup.cs
  77. 1 0
      MediaBrowser.ServerApplication/MediaBrowser.ServerApplication.csproj
  78. 5 10
      MediaBrowser.WebDashboard/Api/DashboardService.cs
  79. 3 48
      MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj

+ 29 - 24
Emby.Drawing/ImageProcessor.cs

@@ -163,7 +163,7 @@ namespace Emby.Drawing
             return _imageEncoder.SupportedOutputFormats;
         }
 
-        public async Task<Tuple<string, string>> ProcessImage(ImageProcessingOptions options)
+        public async Task<Tuple<string, string, DateTime>> ProcessImage(ImageProcessingOptions options)
         {
             if (options == null)
             {
@@ -178,14 +178,13 @@ namespace Emby.Drawing
             }
 
             var originalImagePath = originalImage.Path;
+            var dateModified = originalImage.DateModified;
 
             if (!_imageEncoder.SupportsImageEncoding)
             {
-                return new Tuple<string, string>(originalImagePath, MimeTypes.GetMimeType(originalImagePath));
+                return new Tuple<string, string, DateTime>(originalImagePath, MimeTypes.GetMimeType(originalImagePath), dateModified);
             }
 
-            var dateModified = originalImage.DateModified;
-
             if (options.CropWhiteSpace && _imageEncoder.SupportsImageEncoding)
             {
                 var tuple = await GetWhitespaceCroppedImage(originalImagePath, dateModified).ConfigureAwait(false);
@@ -211,7 +210,7 @@ namespace Emby.Drawing
             if (options.HasDefaultOptions(originalImagePath))
             {
                 // Just spit out the original file if all the options are default
-                return new Tuple<string, string>(originalImagePath, MimeTypes.GetMimeType(originalImagePath));
+                return new Tuple<string, string, DateTime>(originalImagePath, MimeTypes.GetMimeType(originalImagePath), dateModified);
             }
 
             ImageSize? originalImageSize;
@@ -221,7 +220,7 @@ namespace Emby.Drawing
                 if (options.HasDefaultOptions(originalImagePath, originalImageSize.Value))
                 {
                     // Just spit out the original file if all the options are default
-                    return new Tuple<string, string>(originalImagePath, MimeTypes.GetMimeType(originalImagePath));
+                    return new Tuple<string, string, DateTime>(originalImagePath, MimeTypes.GetMimeType(originalImagePath), dateModified);
                 }
             }
             catch
@@ -235,10 +234,6 @@ namespace Emby.Drawing
             var outputFormat = GetOutputFormat(options.SupportedOutputFormats[0]);
             var cacheFilePath = GetCacheFilePath(originalImagePath, newSize, quality, dateModified, outputFormat, options.AddPlayedIndicator, options.PercentPlayed, options.UnplayedCount, options.BackgroundColor, options.ForegroundLayer);
 
-            var semaphore = GetLock(cacheFilePath);
-
-            await semaphore.WaitAsync().ConfigureAwait(false);
-
             var imageProcessingLockTaken = false;
 
             try
@@ -251,15 +246,20 @@ namespace Emby.Drawing
                     var newHeight = Convert.ToInt32(newSize.Height);
 
                     _fileSystem.CreateDirectory(Path.GetDirectoryName(cacheFilePath));
+                    var tmpPath = Path.Combine(_appPaths.TempDirectory, Guid.NewGuid().ToString("N"));
+                    _fileSystem.CreateDirectory(Path.GetDirectoryName(tmpPath));
 
                     await _imageProcessingSemaphore.WaitAsync().ConfigureAwait(false);
 
                     imageProcessingLockTaken = true;
 
-                    _imageEncoder.EncodeImage(originalImagePath, cacheFilePath, AutoOrient(options.Item), newWidth, newHeight, quality, options, outputFormat);
+                    _imageEncoder.EncodeImage(originalImagePath, tmpPath, AutoOrient(options.Item), newWidth, newHeight, quality, options, outputFormat);
+                    CopyFile(tmpPath, cacheFilePath);
+
+                    return new Tuple<string, string, DateTime>(tmpPath, GetMimeType(outputFormat, cacheFilePath), _fileSystem.GetLastWriteTimeUtc(tmpPath));
                 }
 
-                return new Tuple<string, string>(cacheFilePath, GetMimeType(outputFormat, cacheFilePath));
+                return new Tuple<string, string, DateTime>(cacheFilePath, GetMimeType(outputFormat, cacheFilePath), _fileSystem.GetLastWriteTimeUtc(cacheFilePath));
             }
             catch (Exception ex)
             {
@@ -267,7 +267,7 @@ namespace Emby.Drawing
                 _logger.ErrorException("Error encoding image", ex);
 
                 // Just spit out the original file if all the options are default
-                return new Tuple<string, string>(originalImagePath, MimeTypes.GetMimeType(originalImagePath));
+                return new Tuple<string, string, DateTime>(originalImagePath, MimeTypes.GetMimeType(originalImagePath), dateModified);
             }
             finally
             {
@@ -275,8 +275,18 @@ namespace Emby.Drawing
                 {
                     _imageProcessingSemaphore.Release();
                 }
+            }
+        }
+
+        private void CopyFile(string src, string destination)
+        {
+            try
+            {
+                File.Copy(src, destination, true);
+            }
+            catch
+            {
 
-                semaphore.Release();
             }
         }
 
@@ -412,14 +422,9 @@ namespace Emby.Drawing
 
             var croppedImagePath = GetCachePath(CroppedWhitespaceImageCachePath, name, Path.GetExtension(originalImagePath));
 
-            var semaphore = GetLock(croppedImagePath);
-
-            await semaphore.WaitAsync().ConfigureAwait(false);
-
             // Check again in case of contention
             if (_fileSystem.FileExists(croppedImagePath))
             {
-                semaphore.Release();
                 return GetResult(croppedImagePath);
             }
 
@@ -428,11 +433,15 @@ namespace Emby.Drawing
             try
             {
                 _fileSystem.CreateDirectory(Path.GetDirectoryName(croppedImagePath));
+                var tmpPath = Path.Combine(_appPaths.TempDirectory, Guid.NewGuid().ToString("N"));
+                _fileSystem.CreateDirectory(Path.GetDirectoryName(tmpPath));
 
                 await _imageProcessingSemaphore.WaitAsync().ConfigureAwait(false);
                 imageProcessingLockTaken = true;
 
-                _imageEncoder.CropWhiteSpace(originalImagePath, croppedImagePath);
+                _imageEncoder.CropWhiteSpace(originalImagePath, tmpPath);
+                CopyFile(tmpPath, croppedImagePath);
+                return GetResult(tmpPath);
             }
             catch (NotImplementedException)
             {
@@ -452,11 +461,7 @@ namespace Emby.Drawing
                 {
                     _imageProcessingSemaphore.Release();
                 }
-
-                semaphore.Release();
             }
-
-            return GetResult(croppedImagePath);
         }
 
         private Tuple<string, DateTime> GetResult(string path)

+ 6 - 3
MediaBrowser.Api/Images/ImageService.cs

@@ -273,7 +273,9 @@ namespace MediaBrowser.Api.Images
         {
             var list = new List<ImageInfo>();
 
-            foreach (var image in item.ImageInfos.Where(i => !item.AllowsMultipleImages(i.Type)))
+            var itemImages = item.ImageInfos;
+
+            foreach (var image in itemImages.Where(i => !item.AllowsMultipleImages(i.Type)))
             {
                 var info = GetImageInfo(item, image, null);
 
@@ -283,14 +285,14 @@ namespace MediaBrowser.Api.Images
                 }
             }
 
-            foreach (var imageType in item.ImageInfos.Select(i => i.Type).Distinct().Where(item.AllowsMultipleImages))
+            foreach (var imageType in itemImages.Select(i => i.Type).Distinct().Where(item.AllowsMultipleImages))
             {
                 var index = 0;
 
                 // Prevent implicitly captured closure
                 var currentImageType = imageType;
 
-                foreach (var image in item.ImageInfos.Where(i => i.Type == currentImageType))
+                foreach (var image in itemImages.Where(i => i.Type == currentImageType))
                 {
                     var info = GetImageInfo(item, image, index);
 
@@ -636,6 +638,7 @@ namespace MediaBrowser.Api.Images
                 CacheDuration = cacheDuration,
                 ResponseHeaders = headers,
                 ContentType = imageResult.Item2,
+                DateLastModified = imageResult.Item3,
                 IsHeadRequest = isHeadRequest,
                 Path = imageResult.Item1,
 

+ 25 - 11
MediaBrowser.Api/Playback/BaseStreamingService.cs

@@ -286,19 +286,25 @@ namespace MediaBrowser.Api.Playback
 
         protected string GetH264Encoder(StreamState state)
         {
-            if (string.Equals(ApiEntryPoint.Instance.GetEncodingOptions().HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase) ||
-                string.Equals(ApiEntryPoint.Instance.GetEncodingOptions().HardwareAccelerationType, "h264_qsv", StringComparison.OrdinalIgnoreCase))
+            // Only use alternative encoders for video files.
+            // When using concat with folder rips, if the mfx session fails to initialize, ffmpeg will be stuck retrying and will not exit gracefully
+            // Since transcoding of folder rips is expiremental anyway, it's not worth adding additional variables such as this.
+            if (state.VideoType == VideoType.VideoFile)
             {
-                return "h264_qsv";
-            }
+                if (string.Equals(ApiEntryPoint.Instance.GetEncodingOptions().HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase) ||
+                    string.Equals(ApiEntryPoint.Instance.GetEncodingOptions().HardwareAccelerationType, "h264_qsv", StringComparison.OrdinalIgnoreCase))
+                {
+                    return "h264_qsv";
+                }
 
-            if (string.Equals(ApiEntryPoint.Instance.GetEncodingOptions().HardwareAccelerationType, "nvenc", StringComparison.OrdinalIgnoreCase))
-            {
-                return "h264_nvenc";
-            }
-            if (string.Equals(ApiEntryPoint.Instance.GetEncodingOptions().HardwareAccelerationType, "h264_omx", StringComparison.OrdinalIgnoreCase))
-            {
-                return "h264_omx";
+                if (string.Equals(ApiEntryPoint.Instance.GetEncodingOptions().HardwareAccelerationType, "nvenc", StringComparison.OrdinalIgnoreCase))
+                {
+                    return "h264_nvenc";
+                }
+                if (string.Equals(ApiEntryPoint.Instance.GetEncodingOptions().HardwareAccelerationType, "h264_omx", StringComparison.OrdinalIgnoreCase))
+                {
+                    return "h264_omx";
+                }
             }
 
             return "libx264";
@@ -843,6 +849,14 @@ namespace MediaBrowser.Api.Playback
                 return null;
             }
 
+            // Only use alternative encoders for video files.
+            // When using concat with folder rips, if the mfx session fails to initialize, ffmpeg will be stuck retrying and will not exit gracefully
+            // Since transcoding of folder rips is expiremental anyway, it's not worth adding additional variables such as this.
+            if (state.VideoType != VideoType.VideoFile)
+            {
+                return null;
+            }
+
             if (state.VideoStream != null && !string.IsNullOrWhiteSpace(state.VideoStream.Codec))
             {
                 if (string.Equals(ApiEntryPoint.Instance.GetEncodingOptions().HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase))

+ 7 - 4
MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs

@@ -13,6 +13,7 @@ using ServiceStack.Web;
 using System;
 using System.Collections.Generic;
 using System.Globalization;
+using System.IO;
 using System.Threading;
 using System.Threading.Tasks;
 using CommonIO;
@@ -336,17 +337,19 @@ namespace MediaBrowser.Api.Playback.Progressive
                     state.Dispose();
                 }
 
-                var result = new ProgressiveStreamWriter(outputPath, Logger, FileSystem, job);
+                var outputHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
 
-                result.Options["Content-Type"] = contentType;
+                outputHeaders["Content-Type"] = contentType;
 
                 // Add the response headers to the result object
                 foreach (var item in responseHeaders)
                 {
-                    result.Options[item.Key] = item.Value;
+                    outputHeaders[item.Key] = item.Value;
                 }
 
-                return result;
+                Func<Stream,Task> streamWriter = stream => new ProgressiveFileCopier(FileSystem, job, Logger).StreamFile(outputPath, stream);
+
+                return ResultFactory.GetAsyncStreamWriter(streamWriter, outputHeaders);
             }
             finally
             {

+ 10 - 12
MediaBrowser.Api/Playback/Progressive/ProgressiveStreamWriter.cs

@@ -48,21 +48,19 @@ namespace MediaBrowser.Api.Playback.Progressive
         /// <param name="responseStream">The response stream.</param>
         public void WriteTo(Stream responseStream)
         {
-            WriteToInternal(responseStream);
+            var task = WriteToAsync(responseStream);
+            Task.WaitAll(task);
         }
 
         /// <summary>
-        /// Writes to async.
+        /// Writes to.
         /// </summary>
         /// <param name="responseStream">The response stream.</param>
-        /// <returns>Task.</returns>
-        private void WriteToInternal(Stream responseStream)
+        public async Task WriteToAsync(Stream responseStream)
         {
             try
             {
-                var task = new ProgressiveFileCopier(_fileSystem, _job, Logger).StreamFile(Path, responseStream);
-
-                Task.WaitAll(task);
+                await new ProgressiveFileCopier(_fileSystem, _job, Logger).StreamFile(Path, responseStream).ConfigureAwait(false);
             }
             catch (IOException)
             {
@@ -110,11 +108,11 @@ namespace MediaBrowser.Api.Playback.Progressive
             var eofCount = 0;
             long position = 0;
 
-            using (var fs = _fileSystem.GetFileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, false))
+            using (var fs = _fileSystem.GetFileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, true))
             {
                 while (eofCount < 15)
                 {
-                    CopyToInternal(fs, outputStream, BufferSize);
+                    await CopyToInternal(fs, outputStream, BufferSize).ConfigureAwait(false);
 
                     var fsPosition = fs.Position;
 
@@ -140,11 +138,11 @@ namespace MediaBrowser.Api.Playback.Progressive
             }
         }
 
-        private void CopyToInternal(Stream source, Stream destination, int bufferSize)
+        private async Task CopyToInternal(Stream source, Stream destination, int bufferSize)
         {
             var array = new byte[bufferSize];
             int count;
-            while ((count = source.Read(array, 0, array.Length)) != 0)
+            while ((count = await source.ReadAsync(array, 0, array.Length).ConfigureAwait(false)) != 0)
             {
                 //if (_job != null)
                 //{
@@ -170,7 +168,7 @@ namespace MediaBrowser.Api.Playback.Progressive
                 //    }
                 //}
 
-                destination.Write(array, 0, count);
+                await destination.WriteAsync(array, 0, count).ConfigureAwait(false);
 
                 _bytesWritten += count;
 

+ 86 - 75
MediaBrowser.Api/Reports/ReportsService.cs

@@ -42,7 +42,7 @@ namespace MediaBrowser.Api.Reports
 
         /// <summary> Manager for library. </summary>
         private readonly ILibraryManager _libraryManager;   ///< Manager for library
-        /// <summary> The localization. </summary>
+                                                            /// <summary> The localization. </summary>
 
         private readonly ILocalizationManager _localization;    ///< The localization
 
@@ -58,10 +58,10 @@ namespace MediaBrowser.Api.Reports
         /// <summary> Gets the given request. </summary>
         /// <param name="request"> The request. </param>
         /// <returns> A Task&lt;object&gt; </returns>
-        public async Task<object> Get(GetActivityLogs request)
+        public object Get(GetActivityLogs request)
         {
             request.DisplayType = "Screen";
-            ReportResult result = await GetReportActivities(request).ConfigureAwait(false);
+            ReportResult result = GetReportActivities(request);
             return ToOptimizedResult(result);
         }
 
@@ -104,7 +104,8 @@ namespace MediaBrowser.Api.Reports
                 return null;
 
             request.DisplayType = "Screen";
-            var reportResult = await GetReportResult(request);
+            var user = !string.IsNullOrWhiteSpace(request.UserId) ? _userManager.GetUserById(request.UserId) : null;
+            var reportResult = await GetReportResult(request, user);
 
             return ToOptimizedResult(reportResult);
         }
@@ -117,7 +118,8 @@ namespace MediaBrowser.Api.Reports
             if (string.IsNullOrEmpty(request.IncludeItemTypes))
                 return null;
             request.DisplayType = "Screen";
-            var reportResult = await GetReportStatistic(request);
+            var user = !string.IsNullOrWhiteSpace(request.UserId) ? _userManager.GetUserById(request.UserId) : null;
+            var reportResult = await GetReportStatistic(request, user);
 
             return ToOptimizedResult(reportResult);
         }
@@ -150,6 +152,7 @@ namespace MediaBrowser.Api.Reports
             headers["Content-Disposition"] = string.Format("attachment; filename=\"{0}\"", filename);
             headers["Content-Encoding"] = "UTF-8";
 
+            var user = !string.IsNullOrWhiteSpace(request.UserId) ? _userManager.GetUserById(request.UserId) : null;
             ReportResult result = null;
             switch (reportViewType)
             {
@@ -157,12 +160,12 @@ namespace MediaBrowser.Api.Reports
                 case ReportViewType.ReportData:
                     ReportIncludeItemTypes reportRowType = ReportHelper.GetRowType(request.IncludeItemTypes);
                     ReportBuilder dataBuilder = new ReportBuilder(_libraryManager);
-                    QueryResult<BaseItem> queryResult = await GetQueryResult(request).ConfigureAwait(false);
+                    QueryResult<BaseItem> queryResult = await GetQueryResult(request, user).ConfigureAwait(false);
                     result = dataBuilder.GetResult(queryResult.Items, request);
                     result.TotalRecordCount = queryResult.TotalRecordCount;
                     break;
                 case ReportViewType.ReportActivities:
-                    result = await GetReportActivities(request).ConfigureAwait(false);
+                    result = GetReportActivities(request);
                     break;
             }
 
@@ -177,23 +180,15 @@ namespace MediaBrowser.Api.Reports
                     break;
             }
 
-            object ro = ResultFactory.GetResult(returnResult, contentType, headers);
-            return ro;
+            return ResultFactory.GetResult(returnResult, contentType, headers);
         }
 
         #endregion
 
-        #region [Private Methods]
-
-        /// <summary> Gets items query. </summary>
-        /// <param name="request"> The request. </param>
-        /// <param name="user"> The user. </param>
-        /// <returns> The items query. </returns>
         private InternalItemsQuery GetItemsQuery(BaseReportRequest request, User user)
         {
-            var query = new InternalItemsQuery
+            var query = new InternalItemsQuery(user)
             {
-                User = user,
                 IsPlayed = request.IsPlayed,
                 MediaTypes = request.GetMediaTypes(),
                 IncludeItemTypes = request.GetIncludeItemTypes(),
@@ -231,6 +226,7 @@ namespace MediaBrowser.Api.Reports
                 Tags = request.GetTags(),
                 OfficialRatings = request.GetOfficialRatings(),
                 Genres = request.GetGenres(),
+                GenreIds = request.GetGenreIds(),
                 Studios = request.GetStudios(),
                 StudioIds = request.GetStudioIds(),
                 Person = request.Person,
@@ -245,9 +241,11 @@ namespace MediaBrowser.Api.Reports
                 MaxPlayers = request.MaxPlayers,
                 MinCommunityRating = request.MinCommunityRating,
                 MinCriticRating = request.MinCriticRating,
+                ParentId = string.IsNullOrWhiteSpace(request.ParentId) ? (Guid?)null : new Guid(request.ParentId),
                 ParentIndexNumber = request.ParentIndexNumber,
                 AiredDuringSeason = request.AiredDuringSeason,
-                AlbumArtistStartsWithOrGreater = request.AlbumArtistStartsWithOrGreater
+                AlbumArtistStartsWithOrGreater = request.AlbumArtistStartsWithOrGreater,
+                EnableTotalRecordCount = request.EnableTotalRecordCount
             };
 
             if (!string.IsNullOrWhiteSpace(request.Ids))
@@ -357,98 +355,111 @@ namespace MediaBrowser.Api.Reports
                 query.AlbumNames = request.Albums.Split('|');
             }
 
-            if (request.HasQueryLimit == false)
-            {
-                query.StartIndex = null;
-                query.Limit = null;
-            }
-
             return query;
         }
 
-        /// <summary> Gets query result. </summary>
-        /// <param name="request"> The request. </param>
-        /// <returns> The query result. </returns>
-        private async Task<QueryResult<BaseItem>> GetQueryResult(BaseReportRequest request)
+        private async Task<QueryResult<BaseItem>> GetQueryResult(BaseReportRequest request, User user)
         {
-            // Placeholder in case needed later
+            // all report queries currently need this because it's not being specified
             request.Recursive = true;
-            var user = !string.IsNullOrWhiteSpace(request.UserId) ? _userManager.GetUserById(request.UserId) : null;
-            request.Fields = "MediaSources,DateCreated,Settings,Studios,SyncInfo,ItemCounts";
-
-            var parentItem = string.IsNullOrEmpty(request.ParentId) ?
-                (user == null ? _libraryManager.RootFolder : user.RootFolder) :
-                _libraryManager.GetItemById(request.ParentId);
 
             var item = string.IsNullOrEmpty(request.ParentId) ?
                 user == null ? _libraryManager.RootFolder : user.RootFolder :
-                parentItem;
+                _libraryManager.GetItemById(request.ParentId);
 
-            IEnumerable<BaseItem> items;
+            if (string.Equals(request.IncludeItemTypes, "Playlist", StringComparison.OrdinalIgnoreCase))
+            {
+                //item = user == null ? _libraryManager.RootFolder : user.RootFolder;
+            }
+            else if (string.Equals(request.IncludeItemTypes, "BoxSet", StringComparison.OrdinalIgnoreCase))
+            {
+                item = user == null ? _libraryManager.RootFolder : user.RootFolder;
+            }
 
-            if (request.Recursive)
+            // Default list type = children
+
+            var folder = item as Folder;
+            if (folder == null)
             {
-                var result = await ((Folder)item).GetItems(GetItemsQuery(request, user)).ConfigureAwait(false);
-                return result;
+                folder = user == null ? _libraryManager.RootFolder : _libraryManager.GetUserRootFolder();
             }
-            else
+
+            if (!string.IsNullOrEmpty(request.Ids))
             {
-                if (user == null)
+                request.Recursive = true;
+                var query = GetItemsQuery(request, user);
+                var result = await folder.GetItems(query).ConfigureAwait(false);
+
+                if (string.IsNullOrWhiteSpace(request.SortBy))
                 {
-                    var result = await ((Folder)item).GetItems(GetItemsQuery(request, null)).ConfigureAwait(false);
-                    return result;
+                    var ids = query.ItemIds.ToList();
+
+                    // Try to preserve order
+                    result.Items = result.Items.OrderBy(i => ids.IndexOf(i.Id.ToString("N"))).ToArray();
                 }
 
-                var userRoot = item as UserRootFolder;
+                return result;
+            }
 
-                if (userRoot == null)
-                {
-                    var result = await ((Folder)item).GetItems(GetItemsQuery(request, user)).ConfigureAwait(false);
+            if (request.Recursive)
+            {
+                return await folder.GetItems(GetItemsQuery(request, user)).ConfigureAwait(false);
+            }
 
-                    return result;
-                }
+            if (user == null)
+            {
+                return await folder.GetItems(GetItemsQuery(request, null)).ConfigureAwait(false);
+            }
+
+            var userRoot = item as UserRootFolder;
 
-                items = ((Folder)item).GetChildren(user, true);
+            if (userRoot == null)
+            {
+                return await folder.GetItems(GetItemsQuery(request, user)).ConfigureAwait(false);
             }
 
-            return new QueryResult<BaseItem> { Items = items.ToArray() };
+            IEnumerable<BaseItem> items = folder.GetChildren(user, true);
 
+            var itemsArray = items.ToArray();
+
+            return new QueryResult<BaseItem>
+            {
+                Items = itemsArray,
+                TotalRecordCount = itemsArray.Length
+            };
         }
 
+        #region [Private Methods]
+
         /// <summary> Gets report activities. </summary>
         /// <param name="request"> The request. </param>
         /// <returns> The report activities. </returns>
-        private Task<ReportResult> GetReportActivities(IReportsDownload request)
+        private ReportResult GetReportActivities(IReportsDownload request)
         {
-            return Task<ReportResult>.Run(() =>
-            {
-                DateTime? minDate = string.IsNullOrWhiteSpace(request.MinDate) ?
-                (DateTime?)null :
-                DateTime.Parse(request.MinDate, null, DateTimeStyles.RoundtripKind).ToUniversalTime();
-
-                QueryResult<ActivityLogEntry> queryResult;
-                 if (request.HasQueryLimit)   
-                   queryResult = _repo.GetActivityLogEntries(minDate, request.StartIndex, request.Limit);
-                 else
-                     queryResult = _repo.GetActivityLogEntries(minDate, request.StartIndex, null);
-                //var queryResult = _activityManager.GetActivityLogEntries(minDate, request.StartIndex, request.Limit);
-
-                ReportActivitiesBuilder builder = new ReportActivitiesBuilder(_libraryManager, _userManager);
-                var result = builder.GetResult(queryResult, request);
-                result.TotalRecordCount = queryResult.TotalRecordCount;
-                return result;
+            DateTime? minDate = string.IsNullOrWhiteSpace(request.MinDate) ?
+            (DateTime?)null :
+            DateTime.Parse(request.MinDate, null, DateTimeStyles.RoundtripKind).ToUniversalTime();
 
-            });
+            QueryResult<ActivityLogEntry> queryResult;
+            if (request.HasQueryLimit)
+                queryResult = _repo.GetActivityLogEntries(minDate, request.StartIndex, request.Limit);
+            else
+                queryResult = _repo.GetActivityLogEntries(minDate, request.StartIndex, null);
+            //var queryResult = _activityManager.GetActivityLogEntries(minDate, request.StartIndex, request.Limit);
 
+            ReportActivitiesBuilder builder = new ReportActivitiesBuilder(_libraryManager, _userManager);
+            var result = builder.GetResult(queryResult, request);
+            result.TotalRecordCount = queryResult.TotalRecordCount;
+            return result;
         }
 
         /// <summary> Gets report result. </summary>
         /// <param name="request"> The request. </param>
         /// <returns> The report result. </returns>
-        private async Task<ReportResult> GetReportResult(GetItemReport request)
+        private async Task<ReportResult> GetReportResult(GetItemReport request, User user)
         {
             ReportBuilder reportBuilder = new ReportBuilder(_libraryManager);
-            QueryResult<BaseItem> queryResult = await GetQueryResult(request).ConfigureAwait(false);
+            QueryResult<BaseItem> queryResult = await GetQueryResult(request, user).ConfigureAwait(false);
             ReportResult reportResult = reportBuilder.GetResult(queryResult.Items, request);
             reportResult.TotalRecordCount = queryResult.TotalRecordCount;
 
@@ -458,10 +469,10 @@ namespace MediaBrowser.Api.Reports
         /// <summary> Gets report statistic. </summary>
         /// <param name="request"> The request. </param>
         /// <returns> The report statistic. </returns>
-        private async Task<ReportStatResult> GetReportStatistic(GetReportStatistics request)
+        private async Task<ReportStatResult> GetReportStatistic(GetReportStatistics request, User user)
         {
             ReportIncludeItemTypes reportRowType = ReportHelper.GetRowType(request.IncludeItemTypes);
-            QueryResult<BaseItem> queryResult = await GetQueryResult(request).ConfigureAwait(false);
+            QueryResult<BaseItem> queryResult = await GetQueryResult(request, user).ConfigureAwait(false);
 
             ReportStatBuilder reportBuilder = new ReportStatBuilder(_libraryManager);
             ReportStatResult reportResult = reportBuilder.GetResult(queryResult.Items, ReportHelper.GetRowType(request.IncludeItemTypes), request.TopItems ?? 5);

+ 1 - 2
MediaBrowser.Api/StartupWizardService.cs

@@ -114,11 +114,10 @@ namespace MediaBrowser.Api
         private void SetWizardFinishValues(ServerConfiguration config)
         {
             config.EnableLocalizedGuids = true;
-            config.EnableCustomPathSubFolders = true;
             config.EnableStandaloneMusicKeys = true;
             config.EnableCaseSensitiveItemIds = true;
             //config.EnableFolderView = true;
-            config.SchemaVersion = 97;
+            config.SchemaVersion = 108;
         }
 
         public void Post(UpdateStartupConfiguration request)

+ 7 - 0
MediaBrowser.Api/UserLibrary/BaseItemsRequest.cs

@@ -103,6 +103,8 @@ namespace MediaBrowser.Api.UserLibrary
         [ApiMember(Name = "IsInBoxSet", Description = "Optional filter by items that are in boxsets, or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
         public bool? IsInBoxSet { get; set; }
 
+        public string ExcludeItemIds { get; set; }
+
         public bool EnableTotalRecordCount { get; set; }
 
         /// <summary>
@@ -367,6 +369,11 @@ namespace MediaBrowser.Api.UserLibrary
             return (IncludeItemTypes ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
         }
 
+        public string[] GetExcludeItemIds()
+        {
+            return (ExcludeItemIds ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
+        }
+
         public string[] GetExcludeItemTypes()
         {
             return (ExcludeItemTypes ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);

+ 4 - 3
MediaBrowser.Api/UserLibrary/ItemsService.cs

@@ -101,7 +101,7 @@ namespace MediaBrowser.Api.UserLibrary
         {
             var user = !string.IsNullOrWhiteSpace(request.UserId) ? _userManager.GetUserById(request.UserId) : null;
         
-            var result = await GetItemsToSerialize(request, user).ConfigureAwait(false);
+            var result = await GetQueryResult(request, user).ConfigureAwait(false);
 
             if (result == null)
             {
@@ -135,7 +135,7 @@ namespace MediaBrowser.Api.UserLibrary
         /// <param name="request">The request.</param>
         /// <param name="user">The user.</param>
         /// <returns>IEnumerable{BaseItem}.</returns>
-        private async Task<QueryResult<BaseItem>> GetItemsToSerialize(GetItems request, User user)
+        private async Task<QueryResult<BaseItem>> GetQueryResult(GetItems request, User user)
         {
             var item = string.IsNullOrEmpty(request.ParentId) ?
                 user == null ? _libraryManager.RootFolder : user.RootFolder :
@@ -263,7 +263,8 @@ namespace MediaBrowser.Api.UserLibrary
                 ParentIndexNumber = request.ParentIndexNumber,
                 AiredDuringSeason = request.AiredDuringSeason,
                 AlbumArtistStartsWithOrGreater = request.AlbumArtistStartsWithOrGreater,
-                EnableTotalRecordCount = request.EnableTotalRecordCount
+                EnableTotalRecordCount = request.EnableTotalRecordCount,
+                ExcludeItemIds = request.GetExcludeItemIds()
             };
 
             if (!string.IsNullOrWhiteSpace(request.Ids))

+ 29 - 1
MediaBrowser.Common.Implementations/HttpClientManager/HttpClientManager.cs

@@ -140,7 +140,17 @@ namespace MediaBrowser.Common.Implementations.HttpClientManager
 
         private WebRequest GetRequest(HttpRequestOptions options, string method)
         {
-            var request = CreateWebRequest(options.Url);
+            var url = options.Url;
+
+            var uriAddress = new Uri(url);
+            var userInfo = uriAddress.UserInfo;
+            if (!string.IsNullOrWhiteSpace(userInfo))
+            {
+                _logger.Info("Found userInfo in url: {0} ... url: {1}", userInfo, url);
+                url = url.Replace(userInfo + "@", string.Empty);
+            }
+
+            var request = CreateWebRequest(url);
             var httpWebRequest = request as HttpWebRequest;
 
             if (httpWebRequest != null)
@@ -183,9 +193,27 @@ namespace MediaBrowser.Common.Implementations.HttpClientManager
                 }
             }
 
+            if (!string.IsNullOrWhiteSpace(userInfo))
+            {
+                var parts = userInfo.Split(':');
+                if (parts.Length == 2)
+                {
+                    request.Credentials = GetCredential(url, parts[0], parts[1]);
+                    request.PreAuthenticate = true;
+                }
+            }
+
             return request;
         }
 
+        private CredentialCache GetCredential(string url, string username, string password)
+        {
+            //ServicePointManager.SecurityProtocol = SecurityProtocolType.Ssl3;
+            CredentialCache credentialCache = new CredentialCache();
+            credentialCache.Add(new Uri(url), "Basic", new NetworkCredential(username, password));
+            return credentialCache;
+        }
+
         private void AddRequestHeaders(HttpWebRequest request, HttpRequestOptions options)
         {
             foreach (var header in options.RequestHeaders.ToList())

+ 4 - 2
MediaBrowser.Common.Implementations/Updates/GithubUpdater.cs

@@ -54,7 +54,9 @@ namespace MediaBrowser.Common.Implementations.Updates
         {
             if (updateLevel == PackageVersionClass.Release)
             {
-                obj = obj.Where(i => !i.prerelease).ToArray();
+                // Technically all we need to do is check that it's not pre-release
+                // But let's addititional checks for -beta and -dev to handle builds that might be temporarily tagged incorrectly.
+                obj = obj.Where(i => !i.prerelease && !i.name.EndsWith("-beta", StringComparison.OrdinalIgnoreCase) && !i.name.EndsWith("-dev", StringComparison.OrdinalIgnoreCase)).ToArray();
             }
             else if (updateLevel == PackageVersionClass.Beta)
             {
@@ -70,7 +72,7 @@ namespace MediaBrowser.Common.Implementations.Updates
                 .Where(i => i != null)
                 .OrderByDescending(i => Version.Parse(i.AvailableVersion))
                 .FirstOrDefault();
-            
+
             return availableUpdate ?? new CheckForUpdateResult
             {
                 IsUpdateAvailable = false

+ 1 - 1
MediaBrowser.Controller/Drawing/IImageProcessor.cs

@@ -84,7 +84,7 @@ namespace MediaBrowser.Controller.Drawing
         /// </summary>
         /// <param name="options">The options.</param>
         /// <returns>Task.</returns>
-        Task<Tuple<string, string>> ProcessImage(ImageProcessingOptions options);
+        Task<Tuple<string, string, DateTime>> ProcessImage(ImageProcessingOptions options);
 
         /// <summary>
         /// Gets the enhanced image.

+ 1 - 1
MediaBrowser.Controller/Entities/BaseItem.cs

@@ -1878,7 +1878,7 @@ namespace MediaBrowser.Controller.Entities
                 return new ItemImageInfo
                 {
                     Path = path,
-                    DateModified = FileSystem.GetLastWriteTimeUtc(path),
+                    DateModified = chapter.ImageDateModified,
                     Type = imageType
                 };
             }

+ 21 - 1
MediaBrowser.Controller/Entities/Book.cs

@@ -1,4 +1,5 @@
-using MediaBrowser.Controller.Providers;
+using System;
+using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Configuration;
 using System.Linq;
 using System.Runtime.Serialization;
@@ -17,7 +18,26 @@ namespace MediaBrowser.Controller.Entities
             }
         }
 
+        [IgnoreDataMember]
         public string SeriesName { get; set; }
+        [IgnoreDataMember]
+        public Guid? SeriesId { get; set; }
+        [IgnoreDataMember]
+        public string SeriesSortName { get; set; }
+
+        public string FindSeriesSortName()
+        {
+            return SeriesSortName;
+        }
+        public string FindSeriesName()
+        {
+            return SeriesName;
+        }
+
+        public Guid? FindSeriesId()
+        {
+            return SeriesId;
+        }
 
         public override bool CanDownload()
         {

+ 0 - 23
MediaBrowser.Controller/Entities/Folder.cs

@@ -760,11 +760,6 @@ namespace MediaBrowser.Controller.Entities
                     Logger.Debug("Query requires post-filtering due to ItemSortBy.Revenue");
                     return true;
                 }
-                if (query.SortBy.Contains(ItemSortBy.SeriesSortName, StringComparer.OrdinalIgnoreCase))
-                {
-                    Logger.Debug("Query requires post-filtering due to ItemSortBy.SeriesSortName");
-                    return true;
-                }
                 if (query.SortBy.Contains(ItemSortBy.VideoBitRate, StringComparer.OrdinalIgnoreCase))
                 {
                     Logger.Debug("Query requires post-filtering due to ItemSortBy.VideoBitRate");
@@ -859,24 +854,6 @@ namespace MediaBrowser.Controller.Entities
                 return true;
             }
 
-            if (query.IsMissing.HasValue)
-            {
-                Logger.Debug("Query requires post-filtering due to IsMissing");
-                return true;
-            }
-
-            if (query.IsUnaired.HasValue)
-            {
-                Logger.Debug("Query requires post-filtering due to IsUnaired");
-                return true;
-            }
-
-            if (query.IsVirtualUnaired.HasValue)
-            {
-                Logger.Debug("Query requires post-filtering due to IsVirtualUnaired");
-                return true;
-            }
-
             if (UserViewBuilder.CollapseBoxSetItems(query, this, query.User, ConfigurationManager))
             {
                 Logger.Debug("Query requires post-filtering due to CollapseBoxSetItems");

+ 8 - 1
MediaBrowser.Controller/Entities/IHasSeries.cs

@@ -1,4 +1,6 @@
 
+using System;
+
 namespace MediaBrowser.Controller.Entities
 {
     public interface IHasSeries
@@ -7,6 +9,11 @@ namespace MediaBrowser.Controller.Entities
         /// Gets the name of the series.
         /// </summary>
         /// <value>The name of the series.</value>
-        string SeriesName { get; }
+        string SeriesName { get; set; }
+        string FindSeriesName();
+        string SeriesSortName { get; set; }
+        string FindSeriesSortName();
+        Guid? SeriesId { get; set; }
+        Guid? FindSeriesId();
     }
 }

+ 44 - 28
MediaBrowser.Controller/Entities/TV/Episode.cs

@@ -13,7 +13,6 @@ namespace MediaBrowser.Controller.Entities.TV
     /// </summary>
     public class Episode : Video, IHasTrailers, IHasLookupInfo<EpisodeInfo>, IHasSeries
     {
-
         public Episode()
         {
             RemoteTrailers = new List<MediaUrl>();
@@ -25,11 +24,11 @@ namespace MediaBrowser.Controller.Entities.TV
         public List<Guid> RemoteTrailerIds { get; set; }
         public List<MediaUrl> RemoteTrailers { get; set; }
 
-    /// <summary>
-    /// Gets the season in which it aired.
-    /// </summary>
-    /// <value>The aired season.</value>
-    public int? AirsBeforeSeasonNumber { get; set; }
+        /// <summary>
+        /// Gets the season in which it aired.
+        /// </summary>
+        /// <value>The aired season.</value>
+        public int? AirsBeforeSeasonNumber { get; set; }
         public int? AirsAfterSeasonNumber { get; set; }
         public int? AirsBeforeEpisodeNumber { get; set; }
 
@@ -54,7 +53,16 @@ namespace MediaBrowser.Controller.Entities.TV
         /// This is the ending episode number for double episodes.
         /// </summary>
         /// <value>The index number.</value>
-        public int? IndexNumberEnd { get; set; }
+        public int? IndexNumberEnd { get; set; }
+
+        [IgnoreDataMember]
+        public string SeriesSortName { get; set; }
+
+        public string FindSeriesSortName()
+        {
+            var series = Series;
+            return series == null ? SeriesSortName : series.SortName;
+        }
 
         [IgnoreDataMember]
         protected override bool SupportsOwnedItems
@@ -166,13 +174,27 @@ namespace MediaBrowser.Controller.Entities.TV
         }
 
         [IgnoreDataMember]
-        public string SeriesName
-        {
-            get
-            {
-                var series = Series;
-                return series == null ? null : series.Name;
-            }
+        public string SeriesName { get; set; }
+
+        [IgnoreDataMember]
+        public string SeasonName { get; set; }
+
+        public string FindSeasonName()
+        {
+            var season = Season;
+            return season == null ? SeasonName : season.Name;
+        }
+
+        public string FindSeriesName()
+        {
+            var series = Series;
+            return series == null ? SeriesName : series.Name;
+        }
+
+        public Guid? FindSeasonId()
+        {
+            var season = Season;
+            return season == null ? (Guid?)null : season.Id;
         }
 
         /// <summary>
@@ -235,20 +257,14 @@ namespace MediaBrowser.Controller.Entities.TV
         }
 
         [IgnoreDataMember]
-        public Guid? SeasonId
-        {
-            get
-            {
-                // First see if the parent is a Season
-                var season = Season;
-
-                if (season != null)
-                {
-                    return season.Id;
-                }
-
-                return null;
-            }
+        public Guid? SeasonId { get; set; }
+        [IgnoreDataMember]
+        public Guid? SeriesId { get; set; }
+
+        public Guid? FindSeriesId()
+        {
+            var series = Series;
+            return series == null ? (Guid?)null : series.Id;
         }
 
         public override IEnumerable<Guid> GetAncestorIds()

+ 23 - 6
MediaBrowser.Controller/Entities/TV/Season.cs

@@ -51,6 +51,15 @@ namespace MediaBrowser.Controller.Entities.TV
             }
         }
 
+        [IgnoreDataMember]
+        public string SeriesSortName { get; set; }
+
+        public string FindSeriesSortName()
+        {
+            var series = Series;
+            return series == null ? SeriesSortName : series.SortName;
+        }
+
         // Genre, Rating and Stuido will all be the same
         protected override IEnumerable<string> GetIndexByOptions()
         {
@@ -235,13 +244,21 @@ namespace MediaBrowser.Controller.Entities.TV
         }
 
         [IgnoreDataMember]
-        public string SeriesName
+        public string SeriesName { get; set; }
+
+        [IgnoreDataMember]
+        public Guid? SeriesId { get; set; }
+
+        public string FindSeriesName()
         {
-            get
-            {
-                var series = Series;
-                return series == null ? null : series.Name;
-            }
+            var series = Series;
+            return series == null ? SeriesName : series.Name;
+        }
+
+        public Guid? FindSeriesId()
+        {
+            var series = Series;
+            return series == null ? (Guid?)null : series.Id;
         }
 
         /// <summary>

+ 2 - 0
MediaBrowser.Controller/Net/IHttpResultFactory.cs

@@ -28,6 +28,8 @@ namespace MediaBrowser.Controller.Net
         /// <returns>System.Object.</returns>
         object GetResult(object content, string contentType, IDictionary<string,string> responseHeaders = null);
 
+        object GetAsyncStreamWriter(Func<Stream,Task> streamWriter, IDictionary<string, string> responseHeaders = null);
+
         /// <summary>
         /// Gets the optimized result.
         /// </summary>

+ 8 - 0
MediaBrowser.MediaEncoding/Encoder/BaseEncoder.cs

@@ -366,6 +366,14 @@ namespace MediaBrowser.MediaEncoding.Encoder
                 return null;
             }
 
+            // Only use alternative encoders for video files.
+            // When using concat with folder rips, if the mfx session fails to initialize, ffmpeg will be stuck retrying and will not exit gracefully
+            // Since transcoding of folder rips is expiremental anyway, it's not worth adding additional variables such as this.
+            if (state.VideoType != VideoType.VideoFile)
+            {
+                return null;
+            }
+
             if (state.VideoStream != null && !string.IsNullOrWhiteSpace(state.VideoStream.Codec))
             {
                 if (string.Equals(GetEncodingOptions().HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase))

+ 48 - 2
MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs

@@ -120,6 +120,15 @@ namespace MediaBrowser.MediaEncoding.Subtitles
             bool preserveOriginalTimestamps,
             CancellationToken cancellationToken)
         {
+            if (string.IsNullOrWhiteSpace(itemId))
+            {
+                throw new ArgumentNullException("itemId");
+            }
+            if (string.IsNullOrWhiteSpace(mediaSourceId))
+            {
+                throw new ArgumentNullException("mediaSourceId");
+            }
+
             var subtitle = await GetSubtitleStream(itemId, mediaSourceId, subtitleStreamIndex, cancellationToken)
                         .ConfigureAwait(false);
 
@@ -141,10 +150,19 @@ namespace MediaBrowser.MediaEncoding.Subtitles
             int subtitleStreamIndex,
             CancellationToken cancellationToken)
         {
+            if (string.IsNullOrWhiteSpace(itemId))
+            {
+                throw new ArgumentNullException("itemId");
+            }
+            if (string.IsNullOrWhiteSpace(mediaSourceId))
+            {
+                throw new ArgumentNullException("mediaSourceId");
+            }
+
             var mediaSources = await _mediaSourceManager.GetPlayackMediaSources(itemId, null, false, new[] { MediaType.Audio, MediaType.Video }, cancellationToken).ConfigureAwait(false);
 
             var mediaSource = mediaSources
-                .First(i => string.Equals(i.Id, mediaSourceId));
+                .First(i => string.Equals(i.Id, mediaSourceId, StringComparison.OrdinalIgnoreCase));
 
             var subtitleStream = mediaSource.MediaStreams
                 .First(i => i.Type == MediaStreamType.Subtitle && i.Index == subtitleStreamIndex);
@@ -609,7 +627,8 @@ namespace MediaBrowser.MediaEncoding.Subtitles
                 throw;
             }
 
-            process.StandardError.BaseStream.CopyToAsync(logFileStream);
+            // Important - don't await the log task or we won't be able to kill ffmpeg when the user stops playback
+            Task.Run(() => StartStreamingLog(process.StandardError.BaseStream, logFileStream));
 
             var ranToCompletion = process.WaitForExit(300000);
 
@@ -686,6 +705,33 @@ namespace MediaBrowser.MediaEncoding.Subtitles
             }
         }
 
+        private async Task StartStreamingLog(Stream source, Stream target)
+        {
+            try
+            {
+                using (var reader = new StreamReader(source))
+                {
+                    while (!reader.EndOfStream)
+                    {
+                        var line = await reader.ReadLineAsync().ConfigureAwait(false);
+
+                        var bytes = Encoding.UTF8.GetBytes(Environment.NewLine + line);
+
+                        await target.WriteAsync(bytes, 0, bytes.Length).ConfigureAwait(false);
+                        await target.FlushAsync().ConfigureAwait(false);
+                    }
+                }
+            }
+            catch (ObjectDisposedException)
+            {
+                // Don't spam the log. This doesn't seem to throw in windows, but sometimes under linux
+            }
+            catch (Exception ex)
+            {
+                _logger.ErrorException("Error reading ffmpeg log", ex);
+            }
+        }
+
         /// <summary>
         /// Sets the ass font.
         /// </summary>

+ 1 - 2
MediaBrowser.Model.Portable/FodyWeavers.xml

@@ -1,4 +1,3 @@
-<?xml version="1.0" encoding="utf-8"?>
+<?xml version="1.0" encoding="utf-8"?>
 <Weavers>
-  <PropertyChanged />
 </Weavers>

+ 2 - 10
MediaBrowser.Model.Portable/MediaBrowser.Model.Portable.csproj

@@ -36,6 +36,8 @@
     <UseApplicationTrust>false</UseApplicationTrust>
     <BootstrapperEnabled>true</BootstrapperEnabled>
     <RestorePackages>true</RestorePackages>
+    <NuGetPackageImportStamp>
+    </NuGetPackageImportStamp>
   </PropertyGroup>
   <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
     <DebugSymbols>true</DebugSymbols>
@@ -617,9 +619,6 @@
     <Compile Include="..\MediaBrowser.Model\Extensions\FloatHelper.cs">
       <Link>Extensions\FloatHelper.cs</Link>
     </Compile>
-    <Compile Include="..\MediaBrowser.Model\Extensions\IHasPropertyChangedEvent.cs">
-      <Link>Extensions\IHasPropertyChangedEvent.cs</Link>
-    </Compile>
     <Compile Include="..\MediaBrowser.Model\Extensions\IntHelper.cs">
       <Link>Extensions\IntHelper.cs</Link>
     </Compile>
@@ -1233,13 +1232,6 @@
     <PostBuildEvent>
     </PostBuildEvent>
   </PropertyGroup>
-  <Import Project="..\packages\Fody.1.29.2\build\portable-net+sl+win+wpa+wp\Fody.targets" Condition="Exists('..\packages\Fody.1.29.2\build\portable-net+sl+win+wpa+wp\Fody.targets')" />
-  <Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
-    <PropertyGroup>
-      <ErrorText>This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them.  For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
-    </PropertyGroup>
-    <Error Condition="!Exists('..\packages\Fody.1.29.2\build\portable-net+sl+win+wpa+wp\Fody.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Fody.1.29.2\build\portable-net+sl+win+wpa+wp\Fody.targets'))" />
-  </Target>
   <!-- To modify your build process, add your task inside one of the targets below and uncomment it. 
        Other similar extension points exist, see Microsoft.Common.targets.
   <Target Name="BeforeBuild">

+ 0 - 5
MediaBrowser.Model.Portable/packages.config

@@ -1,5 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<packages>
-  <package id="Fody" version="1.29.2" targetFramework="portable45-net45+win8+wp8+wpa81" developmentDependency="true" />
-  <package id="PropertyChanged.Fody" version="1.50.4" targetFramework="portable45-net45+win8+wp8+wpa81" developmentDependency="true" />
-</packages>

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

@@ -591,9 +591,6 @@
     <Compile Include="..\MediaBrowser.Model\Extensions\FloatHelper.cs">
       <Link>Extensions\FloatHelper.cs</Link>
     </Compile>
-    <Compile Include="..\MediaBrowser.Model\Extensions\IHasPropertyChangedEvent.cs">
-      <Link>Extensions\IHasPropertyChangedEvent.cs</Link>
-    </Compile>
     <Compile Include="..\MediaBrowser.Model\Extensions\IntHelper.cs">
       <Link>Extensions\IntHelper.cs</Link>
     </Compile>

+ 0 - 6
MediaBrowser.Model/Configuration/BaseApplicationConfiguration.cs

@@ -49,12 +49,6 @@ namespace MediaBrowser.Model.Configuration
         /// </summary>
         /// <value>The cache path.</value>
         public string CachePath { get; set; }
-
-        /// <summary>
-        /// Gets or sets a value indicating whether [enable custom path sub folders].
-        /// </summary>
-        /// <value><c>true</c> if [enable custom path sub folders]; otherwise, <c>false</c>.</value>
-        public bool EnableCustomPathSubFolders { get; set; }
         
         /// <summary>
         /// Initializes a new instance of the <see cref="BaseApplicationConfiguration" /> class.

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

@@ -214,7 +214,6 @@ namespace MediaBrowser.Model.Configuration
             Migrations = new string[] { };
             SqliteCacheSize = 0;
 
-            EnableCustomPathSubFolders = true;
             EnableLocalizedGuids = true;
             DisplaySpecialsWithinSeasons = true;
 

+ 6 - 0
MediaBrowser.Model/Dlna/AudioOptions.cs

@@ -11,8 +11,14 @@ namespace MediaBrowser.Model.Dlna
         public AudioOptions()
         {
             Context = EncodingContext.Streaming;
+
+            EnableDirectPlay = true;
+            EnableDirectStream = true;
         }
 
+        public bool EnableDirectPlay { get; set; }
+        public bool EnableDirectStream { get; set; }
+
         public string ItemId { get; set; }
         public List<MediaSourceInfo> MediaSources { get; set; }
         public DeviceProfile Profile { get; set; }

+ 3 - 0
MediaBrowser.Model/Dlna/CodecProfile.cs

@@ -11,6 +11,8 @@ namespace MediaBrowser.Model.Dlna
        
         public ProfileCondition[] Conditions { get; set; }
 
+        public ProfileCondition[] ApplyConditions { get; set; }
+
         [XmlAttribute("codec")]
         public string Codec { get; set; }
 
@@ -20,6 +22,7 @@ namespace MediaBrowser.Model.Dlna
         public CodecProfile()
         {
             Conditions = new ProfileCondition[] {};
+            ApplyConditions = new ProfileCondition[] { };
         }
 
         public List<string> GetCodecs()

+ 133 - 25
MediaBrowser.Model/Dlna/StreamBuilder.cs

@@ -131,6 +131,11 @@ namespace MediaBrowser.Model.Dlna
 
             List<PlayMethod> directPlayMethods = GetAudioDirectPlayMethods(item, audioStream, options);
 
+            ConditionProcessor conditionProcessor = new ConditionProcessor();
+
+            int? inputAudioChannels = audioStream == null ? null : audioStream.Channels;
+            int? inputAudioBitrate = audioStream == null ? null : audioStream.BitDepth;
+
             if (directPlayMethods.Count > 0)
             {
                 string audioCodec = audioStream == null ? null : audioStream.Codec;
@@ -138,27 +143,36 @@ namespace MediaBrowser.Model.Dlna
                 // Make sure audio codec profiles are satisfied
                 if (!string.IsNullOrEmpty(audioCodec))
                 {
-                    ConditionProcessor conditionProcessor = new ConditionProcessor();
-
                     List<ProfileCondition> conditions = new List<ProfileCondition>();
                     foreach (CodecProfile i in options.Profile.CodecProfiles)
                     {
                         if (i.Type == CodecType.Audio && i.ContainsCodec(audioCodec, item.Container))
                         {
-                            foreach (ProfileCondition c in i.Conditions)
+                            bool applyConditions = true;
+                            foreach (ProfileCondition applyCondition in i.ApplyConditions)
                             {
-                                conditions.Add(c);
+                                if (!conditionProcessor.IsAudioConditionSatisfied(applyCondition, inputAudioChannels, inputAudioBitrate))
+                                {
+                                    LogConditionFailure(options.Profile, "AudioCodecProfile", applyCondition, item);
+                                    applyConditions = false;
+                                    break;
+                                }
+                            }
+
+                            if (applyConditions)
+                            {
+                                foreach (ProfileCondition c in i.Conditions)
+                                {
+                                    conditions.Add(c);
+                                }
                             }
                         }
                     }
 
-                    int? audioChannels = audioStream.Channels;
-                    int? audioBitrate = audioStream.BitRate;
-
                     bool all = true;
                     foreach (ProfileCondition c in conditions)
                     {
-                        if (!conditionProcessor.IsAudioConditionSatisfied(c, audioChannels, audioBitrate))
+                        if (!conditionProcessor.IsAudioConditionSatisfied(c, inputAudioChannels, inputAudioBitrate))
                         {
                             LogConditionFailure(options.Profile, "AudioCodecProfile", c, item);
                             all = false;
@@ -241,9 +255,23 @@ namespace MediaBrowser.Model.Dlna
                 List<ProfileCondition> audioTranscodingConditions = new List<ProfileCondition>();
                 foreach (CodecProfile i in audioCodecProfiles)
                 {
-                    foreach (ProfileCondition c in i.Conditions)
+                    bool applyConditions = true;
+                    foreach (ProfileCondition applyCondition in i.ApplyConditions)
                     {
-                        audioTranscodingConditions.Add(c);
+                        if (!conditionProcessor.IsAudioConditionSatisfied(applyCondition, inputAudioChannels, inputAudioBitrate))
+                        {
+                            LogConditionFailure(options.Profile, "AudioCodecProfile", applyCondition, item);
+                            applyConditions = false;
+                            break;
+                        }
+                    }
+
+                    if (applyConditions)
+                    {
+                        foreach (ProfileCondition c in i.Conditions)
+                        {
+                            audioTranscodingConditions.Add(c);
+                        }
                     }
                 }
 
@@ -294,7 +322,7 @@ namespace MediaBrowser.Model.Dlna
             if (directPlayProfile != null)
             {
                 // While options takes the network and other factors into account. Only applies to direct stream
-                if (item.SupportsDirectStream && IsAudioEligibleForDirectPlay(item, options.GetMaxBitrate()))
+                if (item.SupportsDirectStream && IsAudioEligibleForDirectPlay(item, options.GetMaxBitrate()) && options.EnableDirectStream)
                 {
                     playMethods.Add(PlayMethod.DirectStream);
                 }
@@ -302,7 +330,7 @@ namespace MediaBrowser.Model.Dlna
                 // The profile describes what the device supports
                 // If device requirements are satisfied then allow both direct stream and direct play
                 if (item.SupportsDirectPlay &&
-                    IsAudioEligibleForDirectPlay(item, GetBitrateForDirectPlayCheck(item, options)))
+                    IsAudioEligibleForDirectPlay(item, GetBitrateForDirectPlayCheck(item, options)) && options.EnableDirectPlay)
                 {
                     playMethods.Add(PlayMethod.DirectPlay);
                 }
@@ -385,8 +413,8 @@ namespace MediaBrowser.Model.Dlna
             MediaStream videoStream = item.VideoStream;
 
             // TODO: This doesn't accout for situation of device being able to handle media bitrate, but wifi connection not fast enough
-            bool isEligibleForDirectPlay = IsEligibleForDirectPlay(item, GetBitrateForDirectPlayCheck(item, options), subtitleStream, options, PlayMethod.DirectPlay);
-            bool isEligibleForDirectStream = IsEligibleForDirectPlay(item, options.GetMaxBitrate(), subtitleStream, options, PlayMethod.DirectStream);
+            bool isEligibleForDirectPlay = options.EnableDirectPlay && IsEligibleForDirectPlay(item, GetBitrateForDirectPlayCheck(item, options), subtitleStream, options, PlayMethod.DirectPlay);
+            bool isEligibleForDirectStream = options.EnableDirectStream && IsEligibleForDirectPlay(item, options.GetMaxBitrate(), subtitleStream, options, PlayMethod.DirectStream);
 
             _logger.Info("Profile: {0}, Path: {1}, isEligibleForDirectPlay: {2}, isEligibleForDirectStream: {3}",
                 options.Profile.Name ?? "Unknown Profile",
@@ -464,17 +492,37 @@ namespace MediaBrowser.Model.Dlna
                 }
                 playlistItem.SubProtocol = transcodingProfile.Protocol;
                 playlistItem.AudioStreamIndex = audioStreamIndex;
+                ConditionProcessor conditionProcessor = new ConditionProcessor();
 
                 List<ProfileCondition> videoTranscodingConditions = new List<ProfileCondition>();
                 foreach (CodecProfile i in options.Profile.CodecProfiles)
                 {
                     if (i.Type == CodecType.Video && i.ContainsCodec(transcodingProfile.VideoCodec, transcodingProfile.Container))
                     {
-                        foreach (ProfileCondition c in i.Conditions)
+                        bool applyConditions = true;
+                        foreach (ProfileCondition applyCondition in i.ApplyConditions)
                         {
-                            videoTranscodingConditions.Add(c);
+                            bool? isSecondaryAudio = audioStream == null ? null : item.IsSecondaryAudio(audioStream);
+                            int? inputAudioBitrate = audioStream == null ? null : audioStream.BitRate;
+                            int? audioChannels = audioStream == null ? null : audioStream.Channels;
+                            string audioProfile = audioStream == null ? null : audioStream.Profile;
+
+                            if (!conditionProcessor.IsVideoAudioConditionSatisfied(applyCondition, audioChannels, inputAudioBitrate, audioProfile, isSecondaryAudio))
+                            {
+                                LogConditionFailure(options.Profile, "AudioCodecProfile", applyCondition, item);
+                                applyConditions = false;
+                                break;
+                            }
+                        }
+
+                        if (applyConditions)
+                        {
+                            foreach (ProfileCondition c in i.Conditions)
+                            {
+                                videoTranscodingConditions.Add(c);
+                            }
+                            break;
                         }
-                        break;
                     }
                 }
                 ApplyTranscodingConditions(playlistItem, videoTranscodingConditions);
@@ -484,11 +532,42 @@ namespace MediaBrowser.Model.Dlna
                 {
                     if (i.Type == CodecType.VideoAudio && i.ContainsCodec(playlistItem.TargetAudioCodec, transcodingProfile.Container))
                     {
-                        foreach (ProfileCondition c in i.Conditions)
+                        bool applyConditions = true;
+                        foreach (ProfileCondition applyCondition in i.ApplyConditions)
                         {
-                            audioTranscodingConditions.Add(c);
+                            int? width = videoStream == null ? null : videoStream.Width;
+                            int? height = videoStream == null ? null : videoStream.Height;
+                            int? bitDepth = videoStream == null ? null : videoStream.BitDepth;
+                            int? videoBitrate = videoStream == null ? null : videoStream.BitRate;
+                            double? videoLevel = videoStream == null ? null : videoStream.Level;
+                            string videoProfile = videoStream == null ? null : videoStream.Profile;
+                            float? videoFramerate = videoStream == null ? null : videoStream.AverageFrameRate ?? videoStream.AverageFrameRate;
+                            bool? isAnamorphic = videoStream == null ? null : videoStream.IsAnamorphic;
+                            string videoCodecTag = videoStream == null ? null : videoStream.CodecTag;
+
+                            TransportStreamTimestamp? timestamp = videoStream == null ? TransportStreamTimestamp.None : item.Timestamp;
+                            int? packetLength = videoStream == null ? null : videoStream.PacketLength;
+                            int? refFrames = videoStream == null ? null : videoStream.RefFrames;
+
+                            int? numAudioStreams = item.GetStreamCount(MediaStreamType.Audio);
+                            int? numVideoStreams = item.GetStreamCount(MediaStreamType.Video);
+
+                            if (!conditionProcessor.IsVideoConditionSatisfied(applyCondition, width, height, bitDepth, videoBitrate, videoProfile, videoLevel, videoFramerate, packetLength, timestamp, isAnamorphic, refFrames, numVideoStreams, numAudioStreams, videoCodecTag))
+                            {
+                                LogConditionFailure(options.Profile, "VideoCodecProfile", applyCondition, item);
+                                applyConditions = false;
+                                break;
+                            }
+                        }
+
+                        if (applyConditions)
+                        {
+                            foreach (ProfileCondition c in i.Conditions)
+                            {
+                                audioTranscodingConditions.Add(c);
+                            }
+                            break;
                         }
-                        break;
                     }
                 }
                 ApplyTranscodingConditions(playlistItem, audioTranscodingConditions);
@@ -667,9 +746,23 @@ namespace MediaBrowser.Model.Dlna
             {
                 if (i.Type == CodecType.Video && i.ContainsCodec(videoCodec, container))
                 {
-                    foreach (ProfileCondition c in i.Conditions)
+                    bool applyConditions = true;
+                    foreach (ProfileCondition applyCondition in i.ApplyConditions)
                     {
-                        conditions.Add(c);
+                        if (!conditionProcessor.IsVideoConditionSatisfied(applyCondition, width, height, bitDepth, videoBitrate, videoProfile, videoLevel, videoFramerate, packetLength, timestamp, isAnamorphic, refFrames, numVideoStreams, numAudioStreams, videoCodecTag))
+                        {
+                            LogConditionFailure(profile, "VideoCodecProfile", applyCondition, mediaSource);
+                            applyConditions = false;
+                            break;
+                        }
+                    }
+
+                    if (applyConditions)
+                    {
+                        foreach (ProfileCondition c in i.Conditions)
+                        {
+                            conditions.Add(c);
+                        }
                     }
                 }
             }
@@ -698,20 +791,35 @@ namespace MediaBrowser.Model.Dlna
                 }
 
                 conditions = new List<ProfileCondition>();
+                bool? isSecondaryAudio = audioStream == null ? null : mediaSource.IsSecondaryAudio(audioStream);
+
                 foreach (CodecProfile i in profile.CodecProfiles)
                 {
                     if (i.Type == CodecType.VideoAudio && i.ContainsCodec(audioCodec, container))
                     {
-                        foreach (ProfileCondition c in i.Conditions)
+                        bool applyConditions = true;
+                        foreach (ProfileCondition applyCondition in i.ApplyConditions)
                         {
-                            conditions.Add(c);
+                            if (!conditionProcessor.IsVideoAudioConditionSatisfied(applyCondition, audioChannels, audioBitrate, audioProfile, isSecondaryAudio))
+                            {
+                                LogConditionFailure(profile, "VideoAudioCodecProfile", applyCondition, mediaSource);
+                                applyConditions = false;
+                                break;
+                            }
+                        }
+
+                        if (applyConditions)
+                        {
+                            foreach (ProfileCondition c in i.Conditions)
+                            {
+                                conditions.Add(c);
+                            }
                         }
                     }
                 }
 
                 foreach (ProfileCondition i in conditions)
                 {
-                    bool? isSecondaryAudio = audioStream == null ? null : mediaSource.IsSecondaryAudio(audioStream);
                     if (!conditionProcessor.IsVideoAudioConditionSatisfied(i, audioChannels, audioBitrate, audioProfile, isSecondaryAudio))
                     {
                         LogConditionFailure(profile, "VideoAudioCodecProfile", i, mediaSource);

+ 13 - 6
MediaBrowser.Model/Dto/BaseItemDto.cs

@@ -18,7 +18,7 @@ namespace MediaBrowser.Model.Dto
     /// This holds information about a BaseItem in a format that is convenient for the client.
     /// </summary>
     [DebuggerDisplay("Name = {Name}, ID = {Id}, Type = {Type}")]
-    public class BaseItemDto : IHasProviderIds, IHasPropertyChangedEvent, IItemDto, IHasServerId, IHasSyncInfo
+    public class BaseItemDto : IHasProviderIds, IItemDto, IHasServerId, IHasSyncInfo
     {
         /// <summary>
         /// Gets or sets the name.
@@ -114,6 +114,8 @@ namespace MediaBrowser.Model.Dto
         /// <value>The synchronize percent.</value>
         public double? SyncPercent { get; set; }
 
+        public string Container { get; set; }
+
         /// <summary>
         /// Gets or sets the DVD season number.
         /// </summary>
@@ -954,6 +956,16 @@ namespace MediaBrowser.Model.Dto
             get { return ImageTags != null && ImageTags.ContainsKey(ImageType.Thumb); }
         }
 
+        /// <summary>
+        /// Gets a value indicating whether this instance has thumb.
+        /// </summary>
+        /// <value><c>true</c> if this instance has thumb; otherwise, <c>false</c>.</value>
+        [IgnoreDataMember]
+        public bool HasBackdrop
+        {
+            get { return (BackdropImageTags != null && BackdropImageTags.Count > 0) || (ParentBackdropImageTags != null && ParentBackdropImageTags.Count > 0); }
+        }
+
         /// <summary>
         /// Gets a value indicating whether this instance has primary image.
         /// </summary>
@@ -1099,11 +1111,6 @@ namespace MediaBrowser.Model.Dto
             }
         }
 
-        /// <summary>
-        /// Occurs when [property changed].
-        /// </summary>
-        public event PropertyChangedEventHandler PropertyChanged;
-
         /// <summary>
         /// Gets or sets the program identifier.
         /// </summary>

+ 2 - 8
MediaBrowser.Model/Dto/BaseItemPerson.cs

@@ -1,5 +1,4 @@
-using MediaBrowser.Model.Extensions;
-using System.ComponentModel;
+using System.ComponentModel;
 using System.Diagnostics;
 using System.Runtime.Serialization;
 
@@ -9,7 +8,7 @@ namespace MediaBrowser.Model.Dto
     /// This is used by the api to get information about a Person within a BaseItem
     /// </summary>
     [DebuggerDisplay("Name = {Name}, Role = {Role}, Type = {Type}")]
-    public class BaseItemPerson : IHasPropertyChangedEvent
+    public class BaseItemPerson
     {
         /// <summary>
         /// Gets or sets the name.
@@ -53,10 +52,5 @@ namespace MediaBrowser.Model.Dto
                 return PrimaryImageTag != null;
             }
         }
-
-        /// <summary>
-        /// Occurs when [property changed].
-        /// </summary>
-        public event PropertyChangedEventHandler PropertyChanged;
     }
 }

+ 2 - 6
MediaBrowser.Model/Dto/ChapterInfoDto.cs

@@ -1,7 +1,5 @@
-using System.ComponentModel;
-using System.Diagnostics;
+using System.Diagnostics;
 using System.Runtime.Serialization;
-using MediaBrowser.Model.Extensions;
 
 namespace MediaBrowser.Model.Dto
 {
@@ -9,7 +7,7 @@ namespace MediaBrowser.Model.Dto
     /// Class ChapterInfo
     /// </summary>
     [DebuggerDisplay("Name = {Name}")]
-    public class ChapterInfoDto : IHasPropertyChangedEvent
+    public class ChapterInfoDto
     {
         /// <summary>
         /// Gets or sets the start position ticks.
@@ -38,7 +36,5 @@ namespace MediaBrowser.Model.Dto
         {
             get { return ImageTag != null; }
         }
-
-        public event PropertyChangedEventHandler PropertyChanged;
     }
 }

+ 1 - 7
MediaBrowser.Model/Dto/UserDto.cs

@@ -1,6 +1,5 @@
 using MediaBrowser.Model.Configuration;
 using MediaBrowser.Model.Connect;
-using MediaBrowser.Model.Extensions;
 using MediaBrowser.Model.Users;
 using System;
 using System.ComponentModel;
@@ -13,7 +12,7 @@ namespace MediaBrowser.Model.Dto
     /// Class UserDto
     /// </summary>
     [DebuggerDisplay("Name = {Name}, ID = {Id}, HasPassword = {HasPassword}")]
-    public class UserDto : IHasPropertyChangedEvent, IItemDto, IHasServerId
+    public class UserDto : IItemDto, IHasServerId
     {
         /// <summary>
         /// Gets or sets the name.
@@ -141,11 +140,6 @@ namespace MediaBrowser.Model.Dto
             Policy = new UserPolicy();
         }
 
-        /// <summary>
-        /// Occurs when [property changed].
-        /// </summary>
-        public event PropertyChangedEventHandler PropertyChanged;
-
         public override string ToString()
         {
             return Name ?? base.ToString();

+ 2 - 5
MediaBrowser.Model/Dto/UserItemDataDto.cs

@@ -1,5 +1,4 @@
-using MediaBrowser.Model.Extensions;
-using System;
+using System;
 using System.ComponentModel;
 
 namespace MediaBrowser.Model.Dto
@@ -7,7 +6,7 @@ namespace MediaBrowser.Model.Dto
     /// <summary>
     /// Class UserItemDataDto
     /// </summary>
-    public class UserItemDataDto : IHasPropertyChangedEvent
+    public class UserItemDataDto
     {
         /// <summary>
         /// Gets or sets the rating.
@@ -74,7 +73,5 @@ namespace MediaBrowser.Model.Dto
         /// </summary>
         /// <value>The item identifier.</value>
         public string ItemId { get; set; }
-        
-        public event PropertyChangedEventHandler PropertyChanged;
     }
 }

+ 3 - 1
MediaBrowser.Model/Entities/ChapterInfo.cs

@@ -1,4 +1,5 @@
-
+using System;
+
 namespace MediaBrowser.Model.Entities
 {
     /// <summary>
@@ -23,5 +24,6 @@ namespace MediaBrowser.Model.Entities
         /// </summary>
         /// <value>The image path.</value>
         public string ImagePath { get; set; }
+        public DateTime ImageDateModified { get; set; }
     }
 }

+ 1 - 8
MediaBrowser.Model/Entities/DisplayPreferences.cs

@@ -1,21 +1,14 @@
 using MediaBrowser.Model.Drawing;
 using System;
 using System.Collections.Generic;
-using System.ComponentModel;
-using MediaBrowser.Model.Extensions;
 
 namespace MediaBrowser.Model.Entities
 {
     /// <summary>
     /// Defines the display preferences for any item that supports them (usually Folders)
     /// </summary>
-    public class DisplayPreferences : IHasPropertyChangedEvent
+    public class DisplayPreferences
     {
-        /// <summary>
-        /// Occurs when [property changed].
-        /// </summary>
-        public event PropertyChangedEventHandler PropertyChanged;
-        
         /// <summary>
         /// The image scale
         /// </summary>

+ 0 - 8
MediaBrowser.Model/Extensions/IHasPropertyChangedEvent.cs

@@ -1,8 +0,0 @@
-using System.ComponentModel;
-
-namespace MediaBrowser.Model.Extensions
-{
-    public interface IHasPropertyChangedEvent : INotifyPropertyChanged
-    {
-    }
-}

+ 1 - 8
MediaBrowser.Model/LiveTv/BaseTimerInfoDto.cs

@@ -1,17 +1,10 @@
 using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Extensions;
 using System;
-using System.ComponentModel;
 
 namespace MediaBrowser.Model.LiveTv
 {
-    public class BaseTimerInfoDto : IHasPropertyChangedEvent, IHasServerId
+    public class BaseTimerInfoDto : IHasServerId
     {
-        /// <summary>
-        /// Occurs when a property value changes.
-        /// </summary>
-        public event PropertyChangedEventHandler PropertyChanged;
-
         /// <summary>
         /// Id of the recording.
         /// </summary>

+ 1 - 4
MediaBrowser.Model/LiveTv/ChannelInfoDto.cs

@@ -1,6 +1,5 @@
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Extensions;
 using MediaBrowser.Model.Library;
 using System.Collections.Generic;
 using System.ComponentModel;
@@ -13,7 +12,7 @@ namespace MediaBrowser.Model.LiveTv
     /// Class ChannelInfoDto
     /// </summary>
     [DebuggerDisplay("Name = {Name}, Number = {Number}")]
-    public class ChannelInfoDto : IHasPropertyChangedEvent, IItemDto, IHasServerId
+    public class ChannelInfoDto : IItemDto, IHasServerId
     {
         /// <summary>
         /// Gets or sets the name.
@@ -120,7 +119,5 @@ namespace MediaBrowser.Model.LiveTv
             ImageTags = new Dictionary<ImageType, string>();
             MediaSources = new List<MediaSourceInfo>();
         }
-
-        public event PropertyChangedEventHandler PropertyChanged;
     }
 }

+ 7 - 0
MediaBrowser.Model/LiveTv/TimerInfoDto.cs

@@ -4,6 +4,11 @@ namespace MediaBrowser.Model.LiveTv
 {
     public class TimerInfoDto : BaseTimerInfoDto
     {
+        public TimerInfoDto()
+        {
+            Type = "Timer";
+        }
+
         /// <summary>
         /// Gets or sets the status.
         /// </summary>
@@ -22,6 +27,8 @@ namespace MediaBrowser.Model.LiveTv
         /// <value>The external series timer identifier.</value>
         public string ExternalSeriesTimerId { get; set; }
 
+        public string Type { get; set; }
+
         /// <summary>
         /// Gets or sets the run time ticks.
         /// </summary>

+ 0 - 1
MediaBrowser.Model/MediaBrowser.Model.csproj

@@ -229,7 +229,6 @@
     <Compile Include="Entities\SortOrder.cs" />
     <Compile Include="Events\GenericEventArgs.cs" />
     <Compile Include="Extensions\DoubleHelper.cs" />
-    <Compile Include="Extensions\IHasPropertyChangedEvent.cs" />
     <Compile Include="Extensions\IntHelper.cs" />
     <Compile Include="Extensions\ListHelper.cs" />
     <Compile Include="Extensions\StringHelper.cs" />

+ 2 - 0
MediaBrowser.Model/Querying/ItemFields.cs

@@ -197,6 +197,8 @@
         /// </summary>
         SeriesGenres,
 
+        SeriesPrimaryImage,
+
         /// <summary>
         /// The series studio
         /// </summary>

+ 1 - 4
MediaBrowser.Model/Session/SessionInfoDto.cs

@@ -1,5 +1,4 @@
 using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Extensions;
 using System;
 using System.Collections.Generic;
 using System.ComponentModel;
@@ -8,7 +7,7 @@ using System.Diagnostics;
 namespace MediaBrowser.Model.Session
 {
     [DebuggerDisplay("Client = {Client}, Username = {UserName}")]
-    public class SessionInfoDto : IHasPropertyChangedEvent
+    public class SessionInfoDto
     {
         /// <summary>
         /// Gets or sets the supported commands.
@@ -116,8 +115,6 @@ namespace MediaBrowser.Model.Session
 
         public TranscodingInfo TranscodingInfo { get; set; }
         
-        public event PropertyChangedEventHandler PropertyChanged;
-
         public SessionInfoDto()
         {
             AdditionalUsers = new List<SessionUserInfo>();

+ 7 - 2
MediaBrowser.Providers/MediaInfo/SubtitleResolver.cs

@@ -72,7 +72,10 @@ namespace MediaBrowser.Providers.MediaInfo
                     // Try to translate to three character code
                     // Be flexible and check against both the full and three character versions
                     var culture = _localization.GetCultures()
-                        .FirstOrDefault(i => string.Equals(i.DisplayName, language, StringComparison.OrdinalIgnoreCase) || string.Equals(i.Name, language, StringComparison.OrdinalIgnoreCase) || string.Equals(i.ThreeLetterISOLanguageName, language, StringComparison.OrdinalIgnoreCase) || string.Equals(i.TwoLetterISOLanguageName, language, StringComparison.OrdinalIgnoreCase));
+                        .FirstOrDefault(i => string.Equals(i.DisplayName, language, StringComparison.OrdinalIgnoreCase) || 
+                        string.Equals(i.Name, language, StringComparison.OrdinalIgnoreCase) || 
+                        string.Equals(i.ThreeLetterISOLanguageName, language, StringComparison.OrdinalIgnoreCase) || 
+                        string.Equals(i.TwoLetterISOLanguageName, language, StringComparison.OrdinalIgnoreCase));
 
                     if (culture != null)
                     {
@@ -99,10 +102,12 @@ namespace MediaBrowser.Providers.MediaInfo
         private string NormalizeFilenameForSubtitleComparison(string filename)
         {
             // Try to account for sloppy file naming
-            filename = filename.Replace("-", string.Empty);
             filename = filename.Replace("_", string.Empty);
             filename = filename.Replace(" ", string.Empty);
 
+            // can't normalize this due to languages such as pt-br
+            //filename = filename.Replace("-", string.Empty);
+
             //filename = filename.Replace(".", string.Empty);
 
             return filename;

+ 2 - 1
MediaBrowser.Providers/TV/DummySeasonProvider.cs

@@ -111,7 +111,8 @@ namespace MediaBrowser.Providers.TV
                 Name = seasonName,
                 IndexNumber = seasonNumber,
                 Id = _libraryManager.GetNewItemId((series.Id + (seasonNumber ?? -1).ToString(_usCulture) + seasonName), typeof(Season)),
-                IsVirtualItem = isVirtualItem
+                IsVirtualItem = isVirtualItem,
+                SeriesId = series.Id
             };
 
             season.SetParent(series);

+ 3 - 1
MediaBrowser.Providers/TV/MissingEpisodeProvider.cs

@@ -429,7 +429,9 @@ namespace MediaBrowser.Providers.TV
                 IndexNumber = episodeNumber,
                 ParentIndexNumber = seasonNumber,
                 Id = _libraryManager.GetNewItemId((series.Id + seasonNumber.ToString(_usCulture) + name), typeof(Episode)),
-                IsVirtualItem = true
+                IsVirtualItem = true,
+                SeasonId = season == null ? (Guid?)null : season.Id,
+                SeriesId = series.Id
             };
 
             episode.SetParent(season);

+ 1 - 5
MediaBrowser.Server.Implementations/Configuration/ServerConfigurationManager.cs

@@ -95,13 +95,9 @@ namespace MediaBrowser.Server.Implementations.Configuration
             {
                 metadataPath = GetInternalMetadataPath();
             }
-            else if (Configuration.EnableCustomPathSubFolders)
-            {
-                metadataPath = Path.Combine(Configuration.MetadataPath, "metadata");
-            }
             else
             {
-                metadataPath = Configuration.MetadataPath;
+                metadataPath = Path.Combine(Configuration.MetadataPath, "metadata");
             }
 
             ((ServerApplicationPaths)ApplicationPaths).InternalMetadataPath = metadataPath;

+ 123 - 165
MediaBrowser.Server.Implementations/Dto/DtoService.cs

@@ -663,29 +663,11 @@ namespace MediaBrowser.Server.Implementations.Dto
             dto.GameSystem = item.GameSystemName;
         }
 
-        private List<string> GetBackdropImageTags(BaseItem item, int limit)
+        private List<string> GetImageTags(BaseItem item, List<ItemImageInfo> images)
         {
-            return GetCacheTags(item, ImageType.Backdrop, limit).ToList();
-        }
-
-        private List<string> GetScreenshotImageTags(BaseItem item, int limit)
-        {
-            var hasScreenshots = item as IHasScreenshots;
-            if (hasScreenshots == null)
-            {
-                return new List<string>();
-            }
-            return GetCacheTags(item, ImageType.Screenshot, limit).ToList();
-        }
-
-        private IEnumerable<string> GetCacheTags(BaseItem item, ImageType type, int limit)
-        {
-            return item.GetImages(type)
-                // Convert to a list now in case GetImageCacheTag is slow
-                .ToList()
+            return images
                 .Select(p => GetImageCacheTag(item, p))
                 .Where(i => i != null)
-                .Take(limit)
                 .ToList();
         }
 
@@ -850,53 +832,6 @@ namespace MediaBrowser.Server.Implementations.Dto
             }
         }
 
-        /// <summary>
-        /// If an item does not any backdrops, this can be used to find the first parent that does have one
-        /// </summary>
-        /// <param name="item">The item.</param>
-        /// <param name="owner">The owner.</param>
-        /// <returns>BaseItem.</returns>
-        private BaseItem GetParentBackdropItem(BaseItem item, BaseItem owner)
-        {
-            var parent = item.GetParent() ?? owner;
-
-            while (parent != null)
-            {
-                if (parent.GetImages(ImageType.Backdrop).Any())
-                {
-                    return parent;
-                }
-
-                parent = parent.GetParent();
-            }
-
-            return null;
-        }
-
-        /// <summary>
-        /// If an item does not have a logo, this can be used to find the first parent that does have one
-        /// </summary>
-        /// <param name="item">The item.</param>
-        /// <param name="type">The type.</param>
-        /// <param name="owner">The owner.</param>
-        /// <returns>BaseItem.</returns>
-        private BaseItem GetParentImageItem(BaseItem item, ImageType type, BaseItem owner)
-        {
-            var parent = item.GetParent() ?? owner;
-
-            while (parent != null)
-            {
-                if (parent.HasImage(type))
-                {
-                    return parent;
-                }
-
-                parent = parent.GetParent();
-            }
-
-            return null;
-        }
-
         /// <summary>
         /// Gets the chapter info dto.
         /// </summary>
@@ -917,7 +852,7 @@ namespace MediaBrowser.Server.Implementations.Dto
                 {
                     Path = chapterInfo.ImagePath,
                     Type = ImageType.Chapter,
-                    DateModified = _fileSystem.GetLastWriteTimeUtc(chapterInfo.ImagePath)
+                    DateModified = chapterInfo.ImageDateModified
                 });
             }
 
@@ -958,6 +893,7 @@ namespace MediaBrowser.Server.Implementations.Dto
                 dto.LockData = item.IsLocked;
                 dto.ForcedSortName = item.ForcedSortName;
             }
+            dto.Container = item.Container;
 
             var hasBudget = item as IHasBudget;
             if (hasBudget != null)
@@ -1027,7 +963,7 @@ namespace MediaBrowser.Server.Implementations.Dto
             var backdropLimit = options.GetImageLimit(ImageType.Backdrop);
             if (backdropLimit > 0)
             {
-                dto.BackdropImageTags = GetBackdropImageTags(item, backdropLimit);
+                dto.BackdropImageTags = GetImageTags(item, item.GetImages(ImageType.Backdrop).Take(backdropLimit).ToList());
             }
 
             if (fields.Contains(ItemFields.ScreenshotImageTags))
@@ -1035,7 +971,7 @@ namespace MediaBrowser.Server.Implementations.Dto
                 var screenshotLimit = options.GetImageLimit(ImageType.Screenshot);
                 if (screenshotLimit > 0)
                 {
-                    dto.ScreenshotImageTags = GetScreenshotImageTags(item, screenshotLimit);
+                    dto.ScreenshotImageTags = GetImageTags(item, item.GetImages(ImageType.Screenshot).Take(screenshotLimit).ToList());
                 }
             }
 
@@ -1064,6 +1000,7 @@ namespace MediaBrowser.Server.Implementations.Dto
 
             dto.Id = GetDtoId(item);
             dto.IndexNumber = item.IndexNumber;
+            dto.ParentIndexNumber = item.ParentIndexNumber;
             dto.IsFolder = item.IsFolder;
             dto.MediaType = item.MediaType;
             dto.LocationType = item.LocationType;
@@ -1076,15 +1013,11 @@ namespace MediaBrowser.Server.Implementations.Dto
             dto.PreferredMetadataCountryCode = item.PreferredMetadataCountryCode;
             dto.PreferredMetadataLanguage = item.PreferredMetadataLanguage;
 
-            var hasCriticRating = item as IHasCriticRating;
-            if (hasCriticRating != null)
-            {
-                dto.CriticRating = hasCriticRating.CriticRating;
+            dto.CriticRating = item.CriticRating;
 
-                if (fields.Contains(ItemFields.CriticRatingSummary))
-                {
-                    dto.CriticRatingSummary = hasCriticRating.CriticRatingSummary;
-                }
+            if (fields.Contains(ItemFields.CriticRatingSummary))
+            {
+                dto.CriticRatingSummary = item.CriticRatingSummary;
             }
 
             var hasTrailers = item as IHasTrailers;
@@ -1127,23 +1060,7 @@ namespace MediaBrowser.Server.Implementations.Dto
 
             if (fields.Contains(ItemFields.ShortOverview))
             {
-                var hasShortOverview = item as IHasShortOverview;
-                if (hasShortOverview != null)
-                {
-                    dto.ShortOverview = hasShortOverview.ShortOverview;
-                }
-            }
-
-            // If there are no backdrops, indicate what parent has them in case the Ui wants to allow inheritance
-            if (backdropLimit > 0 && dto.BackdropImageTags.Count == 0)
-            {
-                var parentWithBackdrop = GetParentBackdropItem(item, owner);
-
-                if (parentWithBackdrop != null)
-                {
-                    dto.ParentBackdropItemId = GetDtoId(parentWithBackdrop);
-                    dto.ParentBackdropImageTags = GetBackdropImageTags(parentWithBackdrop, backdropLimit);
-                }
+                dto.ShortOverview = item.ShortOverview;
             }
 
             if (fields.Contains(ItemFields.ParentId))
@@ -1155,46 +1072,7 @@ namespace MediaBrowser.Server.Implementations.Dto
                 }
             }
 
-            dto.ParentIndexNumber = item.ParentIndexNumber;
-
-            // If there is no logo, indicate what parent has one in case the Ui wants to allow inheritance
-            if (!dto.HasLogo && options.GetImageLimit(ImageType.Logo) > 0)
-            {
-                var parentWithLogo = GetParentImageItem(item, ImageType.Logo, owner);
-
-                if (parentWithLogo != null)
-                {
-                    dto.ParentLogoItemId = GetDtoId(parentWithLogo);
-
-                    dto.ParentLogoImageTag = GetImageCacheTag(parentWithLogo, ImageType.Logo);
-                }
-            }
-
-            // If there is no art, indicate what parent has one in case the Ui wants to allow inheritance
-            if (!dto.HasArtImage && options.GetImageLimit(ImageType.Art) > 0)
-            {
-                var parentWithImage = GetParentImageItem(item, ImageType.Art, owner);
-
-                if (parentWithImage != null)
-                {
-                    dto.ParentArtItemId = GetDtoId(parentWithImage);
-
-                    dto.ParentArtImageTag = GetImageCacheTag(parentWithImage, ImageType.Art);
-                }
-            }
-
-            // If there is no thumb, indicate what parent has one in case the Ui wants to allow inheritance
-            if (!dto.HasThumb && options.GetImageLimit(ImageType.Thumb) > 0)
-            {
-                var parentWithImage = GetParentImageItem(item, ImageType.Thumb, owner);
-
-                if (parentWithImage != null)
-                {
-                    dto.ParentThumbItemId = GetDtoId(parentWithImage);
-
-                    dto.ParentThumbImageTag = GetImageCacheTag(parentWithImage, ImageType.Thumb);
-                }
-            }
+            AddInheritedImages(dto, item, options, owner);
 
             if (fields.Contains(ItemFields.Path))
             {
@@ -1426,42 +1304,38 @@ namespace MediaBrowser.Server.Implementations.Dto
                     dto.SeasonId = seasonId.Value.ToString("N");
                 }
 
-                var episodeSeason = episode.Season;
-                if (episodeSeason != null)
+                dto.SeasonName = episode.SeasonName;
+
+                var seriesId = episode.SeriesId;
+                if (seriesId.HasValue)
                 {
-                    if (fields.Contains(ItemFields.SeasonName))
-                    {
-                        dto.SeasonName = episodeSeason.Name;
-                    }
+                    dto.SeriesId = seriesId.Value.ToString("N");
                 }
 
-                var episodeSeries = episode.Series;
+                Series episodeSeries = null;
 
-                if (episodeSeries != null)
+                if (fields.Contains(ItemFields.SeriesGenres))
                 {
-                    if (fields.Contains(ItemFields.SeriesGenres))
+                    episodeSeries = episodeSeries ?? episode.Series;
+                    if (episodeSeries != null)
                     {
                         dto.SeriesGenres = episodeSeries.Genres.ToList();
                     }
+                }
 
-                    dto.SeriesId = GetDtoId(episodeSeries);
-
-                    if (fields.Contains(ItemFields.AirTime))
-                    {
-                        dto.AirTime = episodeSeries.AirTime;
-                    }
-
-                    if (options.GetImageLimit(ImageType.Thumb) > 0)
-                    {
-                        dto.SeriesThumbImageTag = GetImageCacheTag(episodeSeries, ImageType.Thumb);
-                    }
-
-                    if (options.GetImageLimit(ImageType.Primary) > 0)
+                //if (fields.Contains(ItemFields.SeriesPrimaryImage))
+                {
+                    episodeSeries = episodeSeries ?? episode.Series;
+                    if (episodeSeries != null)
                     {
                         dto.SeriesPrimaryImageTag = GetImageCacheTag(episodeSeries, ImageType.Primary);
                     }
+                }
 
-                    if (fields.Contains(ItemFields.SeriesStudio))
+                if (fields.Contains(ItemFields.SeriesStudio))
+                {
+                    episodeSeries = episodeSeries ?? episode.Series;
+                    if (episodeSeries != null)
                     {
                         dto.SeriesStudio = episodeSeries.Studios.FirstOrDefault();
                     }
@@ -1483,16 +1357,29 @@ namespace MediaBrowser.Server.Implementations.Dto
             var season = item as Season;
             if (season != null)
             {
-                series = season.Series;
+                dto.SeriesName = season.SeriesName;
+
+                var seriesId = season.SeriesId;
+                if (seriesId.HasValue)
+                {
+                    dto.SeriesId = seriesId.Value.ToString("N");
+                }
+
+                series = null;
 
-                if (series != null)
+                if (fields.Contains(ItemFields.SeriesStudio))
                 {
-                    dto.SeriesId = GetDtoId(series);
-                    dto.SeriesName = series.Name;
-                    dto.AirTime = series.AirTime;
-                    dto.SeriesStudio = series.Studios.FirstOrDefault();
+                    series = series ?? season.Series;
+                    if (series != null)
+                    {
+                        dto.SeriesStudio = series.Studios.FirstOrDefault();
+                    }
+                }
 
-                    if (options.GetImageLimit(ImageType.Primary) > 0)
+                if (fields.Contains(ItemFields.SeriesPrimaryImage))
+                {
+                    series = series ?? season.Series;
+                    if (series != null)
                     {
                         dto.SeriesPrimaryImageTag = GetImageCacheTag(series, ImageType.Primary);
                     }
@@ -1543,6 +1430,77 @@ namespace MediaBrowser.Server.Implementations.Dto
             }
         }
 
+        private void AddInheritedImages(BaseItemDto dto, BaseItem item, DtoOptions options, BaseItem owner)
+        {
+            var logoLimit = options.GetImageLimit(ImageType.Logo);
+            var artLimit = options.GetImageLimit(ImageType.Art);
+            var thumbLimit = options.GetImageLimit(ImageType.Thumb);
+            var backdropLimit = options.GetImageLimit(ImageType.Backdrop);
+
+            if (logoLimit == 0 && artLimit == 0 && thumbLimit == 0 && backdropLimit == 0)
+            {
+                return;
+            }
+
+            BaseItem parent = null;
+            var isFirst = true;
+
+            while (((!dto.HasLogo && logoLimit > 0) || (!dto.HasArtImage && artLimit > 0) || (!dto.HasThumb && thumbLimit > 0) || parent is Series) && 
+                (parent = parent ?? (isFirst ? item.GetParent() ?? owner : parent)) != null)
+            {
+                if (parent == null)
+                {
+                    break;
+                }
+
+                var allImages = parent.ImageInfos;
+
+                if (logoLimit > 0 && !dto.HasLogo && dto.ParentLogoItemId == null)
+                {
+                    var image = allImages.FirstOrDefault(i => i.Type == ImageType.Logo);
+
+                    if (image != null)
+                    {
+                        dto.ParentLogoItemId = GetDtoId(parent);
+                        dto.ParentLogoImageTag = GetImageCacheTag(parent, image);
+                    }
+                }
+                if (artLimit > 0 && !dto.HasArtImage && dto.ParentArtItemId == null)
+                {
+                    var image = allImages.FirstOrDefault(i => i.Type == ImageType.Art);
+
+                    if (image != null)
+                    {
+                        dto.ParentArtItemId = GetDtoId(parent);
+                        dto.ParentArtImageTag = GetImageCacheTag(parent, image);
+                    }
+                }
+                if (thumbLimit > 0 && !dto.HasThumb && (dto.ParentThumbItemId == null || parent is Series))
+                {
+                    var image = allImages.FirstOrDefault(i => i.Type == ImageType.Thumb);
+
+                    if (image != null)
+                    {
+                        dto.ParentThumbItemId = GetDtoId(parent);
+                        dto.ParentThumbImageTag = GetImageCacheTag(parent, image);
+                    }
+                }
+                if (backdropLimit > 0 && !dto.HasBackdrop)
+                {
+                    var images = allImages.Where(i => i.Type == ImageType.Backdrop).Take(backdropLimit).ToList();
+
+                    if (images.Count > 0)
+                    {
+                        dto.ParentBackdropItemId = GetDtoId(parent);
+                        dto.ParentBackdropImageTags = GetImageTags(parent, images);
+                    }
+                }
+
+                isFirst = false;
+                parent = parent.GetParent();
+            }
+        }
+
         private string GetMappedPath(IHasMetadata item)
         {
             var path = item.Path;

+ 56 - 0
MediaBrowser.Server.Implementations/HttpServer/AsyncStreamWriterFunc.cs

@@ -0,0 +1,56 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Threading.Tasks;
+using ServiceStack;
+using ServiceStack.Web;
+
+namespace MediaBrowser.Server.Implementations.HttpServer
+{
+    public class AsyncStreamWriterFunc : IStreamWriter, IAsyncStreamWriter, IHasOptions
+    {
+        /// <summary>
+        /// Gets or sets the source stream.
+        /// </summary>
+        /// <value>The source stream.</value>
+        private Func<Stream, Task> Writer { get; set; }
+
+        /// <summary>
+        /// Gets the options.
+        /// </summary>
+        /// <value>The options.</value>
+        public IDictionary<string, string> Options { get; private set; }
+
+        public Action OnComplete { get; set; }
+        public Action OnError { get; set; }
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="StreamWriter" /> class.
+        /// </summary>
+        public AsyncStreamWriterFunc(Func<Stream, Task> writer, IDictionary<string, string> headers)
+        {
+            Writer = writer;
+
+            if (headers == null)
+            {
+                headers = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+            }
+            Options = headers;
+        }
+
+        /// <summary>
+        /// Writes to.
+        /// </summary>
+        /// <param name="responseStream">The response stream.</param>
+        public void WriteTo(Stream responseStream)
+        {
+            var task = Writer(responseStream);
+            Task.WaitAll(task);
+        }
+
+        public async Task WriteToAsync(Stream responseStream)
+        {
+            await Writer(responseStream).ConfigureAwait(false);
+        }
+    }
+}

+ 19 - 22
MediaBrowser.Server.Implementations/HttpServer/HttpListenerHost.cs

@@ -335,7 +335,7 @@ namespace MediaBrowser.Server.Implementations.HttpServer
         /// <param name="httpReq">The HTTP req.</param>
         /// <param name="url">The URL.</param>
         /// <returns>Task.</returns>
-        protected Task RequestHandler(IHttpRequest httpReq, Uri url)
+        protected async Task RequestHandler(IHttpRequest httpReq, Uri url)
         {
             var date = DateTime.Now;
 
@@ -345,7 +345,7 @@ namespace MediaBrowser.Server.Implementations.HttpServer
             {
                 httpRes.StatusCode = 503;
                 httpRes.Close();
-                return Task.FromResult(true);
+                return ;
             }
 
             var operationName = httpReq.OperationName;
@@ -365,13 +365,13 @@ namespace MediaBrowser.Server.Implementations.HttpServer
                 string.Equals(localPath, "/mediabrowser/", StringComparison.OrdinalIgnoreCase))
             {
                 httpRes.RedirectToUrl(DefaultRedirectPath);
-                return Task.FromResult(true);
+                return;
             }
             if (string.Equals(localPath, "/emby", StringComparison.OrdinalIgnoreCase) ||
                 string.Equals(localPath, "/mediabrowser", StringComparison.OrdinalIgnoreCase))
             {
                 httpRes.RedirectToUrl("emby/" + DefaultRedirectPath);
-                return Task.FromResult(true);
+                return;
             }
 
             if (string.Equals(localPath, "/mediabrowser/", StringComparison.OrdinalIgnoreCase) ||
@@ -389,35 +389,35 @@ namespace MediaBrowser.Server.Implementations.HttpServer
                     httpRes.Write("<!doctype html><html><head><title>Emby</title></head><body>Please update your Emby bookmark to <a href=\"" + newUrl + "\">" + newUrl + "</a></body></html>");
 
                     httpRes.Close();
-                    return Task.FromResult(true);
+                    return;
                 }
             }
 
             if (string.Equals(localPath, "/web", StringComparison.OrdinalIgnoreCase))
             {
                 httpRes.RedirectToUrl(DefaultRedirectPath);
-                return Task.FromResult(true);
+                return;
             }
             if (string.Equals(localPath, "/web/", StringComparison.OrdinalIgnoreCase))
             {
                 httpRes.RedirectToUrl("../" + DefaultRedirectPath);
-                return Task.FromResult(true);
+                return;
             }
             if (string.Equals(localPath, "/", StringComparison.OrdinalIgnoreCase))
             {
                 httpRes.RedirectToUrl(DefaultRedirectPath);
-                return Task.FromResult(true);
+                return;
             }
             if (string.IsNullOrEmpty(localPath))
             {
                 httpRes.RedirectToUrl("/" + DefaultRedirectPath);
-                return Task.FromResult(true);
+                return;
             }
 
             if (string.Equals(localPath, "/emby/pin", StringComparison.OrdinalIgnoreCase))
             {
                 httpRes.RedirectToUrl("web/pin.html");
-                return Task.FromResult(true);
+                return;
             }
 
             if (!string.IsNullOrWhiteSpace(GlobalResponse))
@@ -427,7 +427,7 @@ namespace MediaBrowser.Server.Implementations.HttpServer
                 httpRes.Write(GlobalResponse);
 
                 httpRes.Close();
-                return Task.FromResult(true);
+                return;
             }
 
             var handler = HttpHandlerFactory.GetHandler(httpReq);
@@ -443,13 +443,13 @@ namespace MediaBrowser.Server.Implementations.HttpServer
                     httpReq.OperationName = operationName = restHandler.RestPath.RequestType.GetOperationName();
                 }
 
-                var task = serviceStackHandler.ProcessRequestAsync(httpReq, httpRes, operationName);
-
-                task.ContinueWith(x => httpRes.Close(), TaskContinuationOptions.OnlyOnRanToCompletion | TaskContinuationOptions.AttachedToParent);
-                //Matches Exceptions handled in HttpListenerBase.InitTask()
-
-                task.ContinueWith(x =>
+                try
+                {
+                    await serviceStackHandler.ProcessRequestAsync(httpReq, httpRes, operationName).ConfigureAwait(false);
+                }
+                finally
                 {
+                    httpRes.Close();
                     var statusCode = httpRes.StatusCode;
 
                     var duration = DateTime.Now - date;
@@ -458,13 +458,10 @@ namespace MediaBrowser.Server.Implementations.HttpServer
                     {
                         LoggerUtils.LogResponse(_logger, statusCode, urlToLog, remoteIp, duration);
                     }
-
-                }, TaskContinuationOptions.None);
-                return task;
+                }
             }
 
-            return new NotImplementedException("Cannot execute handler: " + handler + " at PathInfo: " + httpReq.PathInfo)
-                .AsTaskException();
+            throw new NotImplementedException("Cannot execute handler: " + handler + " at PathInfo: " + httpReq.PathInfo);
         }
 
         /// <summary>

+ 10 - 1
MediaBrowser.Server.Implementations/HttpServer/HttpResultFactory.cs

@@ -331,7 +331,11 @@ namespace MediaBrowser.Server.Implementations.HttpServer
                 options.ContentType = MimeTypes.GetMimeType(path);
             }
 
-            options.DateLastModified = _fileSystem.GetLastWriteTimeUtc(path);
+            if (!options.DateLastModified.HasValue)
+            {
+                options.DateLastModified = _fileSystem.GetLastWriteTimeUtc(path);
+            }
+
             var cacheKey = path + options.DateLastModified.Value.Ticks;
 
             options.CacheKey = cacheKey.GetMD5();
@@ -699,5 +703,10 @@ namespace MediaBrowser.Server.Implementations.HttpServer
 
             throw error;
         }
+
+        public object GetAsyncStreamWriter(Func<Stream, Task> streamWriter, IDictionary<string, string> responseHeaders = null)
+        {
+            return new AsyncStreamWriterFunc(streamWriter, responseHeaders);
+        }
     }
 }

+ 0 - 285
MediaBrowser.Server.Implementations/HttpServer/NetListener/HttpListenerServer.cs

@@ -1,285 +0,0 @@
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Logging;
-using ServiceStack;
-using ServiceStack.Host.HttpListener;
-using ServiceStack.Web;
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Net;
-using System.Text;
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace MediaBrowser.Server.Implementations.HttpServer.NetListener
-{
-    public class HttpListenerServer : IHttpListener
-    {
-        private readonly ILogger _logger;
-        private HttpListener _listener;
-        private readonly ManualResetEventSlim _listenForNextRequest = new ManualResetEventSlim(false);
-
-        public Action<Exception, IRequest> ErrorHandler { get; set; }
-        public Action<WebSocketConnectEventArgs> WebSocketHandler { get; set; }
-        public Func<IHttpRequest, Uri, Task> RequestHandler { get; set; }
-
-        private readonly Action<string> _endpointListener;
-
-        public HttpListenerServer(ILogger logger, Action<string> endpointListener)
-        {
-            _logger = logger;
-            _endpointListener = endpointListener;
-        }
-
-        private List<string> UrlPrefixes { get; set; }
-
-        public void Start(IEnumerable<string> urlPrefixes)
-        {
-            UrlPrefixes = urlPrefixes.ToList();
-
-            if (_listener == null)
-                _listener = new HttpListener();
-
-            //HostContext.Config.HandlerFactoryPath = ListenerRequest.GetHandlerPathIfAny(UrlPrefixes.First());
-
-            foreach (var prefix in UrlPrefixes)
-            {
-                _logger.Info("Adding HttpListener prefix " + prefix);
-                _listener.Prefixes.Add(prefix);
-            }
-
-            _listener.Start();
-
-            Task.Factory.StartNew(Listen, TaskCreationOptions.LongRunning);
-        }
-
-        private bool IsListening
-        {
-            get { return _listener != null && _listener.IsListening; }
-        }
-
-        // Loop here to begin processing of new requests.
-        private void Listen()
-        {
-            while (IsListening)
-            {
-                if (_listener == null) return;
-                _listenForNextRequest.Reset();
-
-                try
-                {
-                    _listener.BeginGetContext(ListenerCallback, _listener);
-                    _listenForNextRequest.Wait();
-                }
-                catch (Exception ex)
-                {
-                    _logger.Error("Listen()", ex);
-                    return;
-                }
-                if (_listener == null) return;
-            }
-        }
-
-        // Handle the processing of a request in here.
-        private void ListenerCallback(IAsyncResult asyncResult)
-        {
-            _listenForNextRequest.Set();
-
-            var listener = asyncResult.AsyncState as HttpListener;
-            HttpListenerContext context;
-
-            if (listener == null) return;
-            var isListening = listener.IsListening;
-
-            try
-            {
-                if (!isListening)
-                {
-                    _logger.Debug("Ignoring ListenerCallback() as HttpListener is no longer listening"); return;
-                }
-                // The EndGetContext() method, as with all Begin/End asynchronous methods in the .NET Framework,
-                // blocks until there is a request to be processed or some type of data is available.
-                context = listener.EndGetContext(asyncResult);
-            }
-            catch (Exception ex)
-            {
-                // You will get an exception when httpListener.Stop() is called
-                // because there will be a thread stopped waiting on the .EndGetContext()
-                // method, and again, that is just the way most Begin/End asynchronous
-                // methods of the .NET Framework work.
-                var errMsg = ex + ": " + IsListening;
-                _logger.Warn(errMsg);
-                return;
-            }
-
-            Task.Factory.StartNew(() => InitTask(context));
-        }
-
-        private void InitTask(HttpListenerContext context)
-        {
-            try
-            {
-                var task = this.ProcessRequestAsync(context);
-                task.ContinueWith(x => HandleError(x.Exception, context), TaskContinuationOptions.OnlyOnFaulted | TaskContinuationOptions.AttachedToParent);
-
-                if (task.Status == TaskStatus.Created)
-                {
-                    task.RunSynchronously();
-                }
-            }
-            catch (Exception ex)
-            {
-                HandleError(ex, context);
-            }
-        }
-
-        private Task ProcessRequestAsync(HttpListenerContext context)
-        {
-            var request = context.Request;
-
-            LogHttpRequest(request);
-
-            if (request.IsWebSocketRequest)
-            {
-                return ProcessWebSocketRequest(context);
-            }
-
-            if (string.IsNullOrEmpty(context.Request.RawUrl))
-                return ((object)null).AsTaskResult();
-
-            var operationName = context.Request.GetOperationName();
-
-            var httpReq = GetRequest(context, operationName);
-
-            return RequestHandler(httpReq, request.Url);
-        }
-
-        /// <summary>
-        /// Processes the web socket request.
-        /// </summary>
-        /// <param name="ctx">The CTX.</param>
-        /// <returns>Task.</returns>
-        private async Task ProcessWebSocketRequest(HttpListenerContext ctx)
-        {
-#if !__MonoCS__
-            try
-            {
-                var webSocketContext = await ctx.AcceptWebSocketAsync(null).ConfigureAwait(false);
-
-                if (WebSocketHandler != null)
-                {
-                    WebSocketHandler(new WebSocketConnectEventArgs
-                    {
-                        WebSocket = new NativeWebSocket(webSocketContext.WebSocket, _logger),
-                        Endpoint = ctx.Request.RemoteEndPoint.ToString()
-                    });
-                }
-            }
-            catch (Exception ex)
-            {
-                _logger.ErrorException("AcceptWebSocketAsync error", ex);
-                ctx.Response.StatusCode = 500;
-                ctx.Response.Close();
-            }
-#endif
-        }
-
-        private void HandleError(Exception ex, HttpListenerContext context)
-        {
-            var operationName = context.Request.GetOperationName();
-            var httpReq = GetRequest(context, operationName);
-
-            if (ErrorHandler != null)
-            {
-                ErrorHandler(ex, httpReq);
-            }
-        }
-
-        private static ListenerRequest GetRequest(HttpListenerContext httpContext, string operationName)
-        {
-            var req = new ListenerRequest(httpContext, operationName, RequestAttributes.None);
-            req.RequestAttributes = req.GetAttributes();
-
-            return req;
-        }
-
-        /// <summary>
-        /// Logs the HTTP request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        private void LogHttpRequest(HttpListenerRequest request)
-        {
-            var endpoint = request.LocalEndPoint;
-
-            if (endpoint != null)
-            {
-                var address = endpoint.ToString();
-
-                _endpointListener(address);
-            }
-
-            LogRequest(_logger, request);
-        }
-
-        /// <summary>
-        /// Logs the request.
-        /// </summary>
-        /// <param name="logger">The logger.</param>
-        /// <param name="request">The request.</param>
-        private static void LogRequest(ILogger logger, HttpListenerRequest request)
-        {
-            var log = new StringBuilder();
-
-            var logHeaders = true;
-
-            if (logHeaders)
-            {
-                var headers = string.Join(",", request.Headers.AllKeys.Where(i => !string.Equals(i, "cookie", StringComparison.OrdinalIgnoreCase) && !string.Equals(i, "Referer", StringComparison.OrdinalIgnoreCase)).Select(k => k + "=" + request.Headers[k]));
-
-                log.AppendLine("Ip: " + request.RemoteEndPoint + ". Headers: " + headers);
-            }
-
-            var type = request.IsWebSocketRequest ? "Web Socket" : "HTTP " + request.HttpMethod;
-
-            logger.LogMultiline(type + " " + request.Url, LogSeverity.Debug, log);
-        }
-
-        public void Stop()
-        {
-            if (_listener != null)
-            {
-                foreach (var prefix in UrlPrefixes)
-                {
-                    _listener.Prefixes.Remove(prefix);
-                }
-
-                _listener.Close();
-            }
-        }
-
-        public void Dispose()
-        {
-            Dispose(true);
-        }
-
-        private bool _disposed;
-        private readonly object _disposeLock = new object();
-        protected virtual void Dispose(bool disposing)
-        {
-            if (_disposed) return;
-
-            lock (_disposeLock)
-            {
-                if (_disposed) return;
-
-                if (disposing)
-                {
-                    Stop();
-                }
-
-                //release unmanaged resources here...
-                _disposed = true;
-            }
-        }
-    }
-}

+ 63 - 11
MediaBrowser.Server.Implementations/HttpServer/RangeRequestWriter.cs

@@ -5,10 +5,12 @@ using System.Collections.Generic;
 using System.Globalization;
 using System.IO;
 using System.Net;
+using System.Threading.Tasks;
+using ServiceStack;
 
 namespace MediaBrowser.Server.Implementations.HttpServer
 {
-    public class RangeRequestWriter : IStreamWriter, IHttpResult
+    public class RangeRequestWriter : IStreamWriter, IAsyncStreamWriter, IHttpResult
     {
         /// <summary>
         /// Gets or sets the source stream.
@@ -168,16 +170,6 @@ namespace MediaBrowser.Server.Implementations.HttpServer
         /// </summary>
         /// <param name="responseStream">The response stream.</param>
         public void WriteTo(Stream responseStream)
-        {
-            WriteToInternal(responseStream);
-        }
-
-        /// <summary>
-        /// Writes to async.
-        /// </summary>
-        /// <param name="responseStream">The response stream.</param>
-        /// <returns>Task.</returns>
-        private void WriteToInternal(Stream responseStream)
         {
             try
             {
@@ -237,6 +229,66 @@ namespace MediaBrowser.Server.Implementations.HttpServer
             }
         }
 
+        public async Task WriteToAsync(Stream responseStream)
+        {
+            try
+            {
+                // Headers only
+                if (IsHeadRequest)
+                {
+                    return;
+                }
+
+                using (var source = SourceStream)
+                {
+                    // If the requested range is "0-", we can optimize by just doing a stream copy
+                    if (RangeEnd >= TotalContentLength - 1)
+                    {
+                        await source.CopyToAsync(responseStream, BufferSize).ConfigureAwait(false);
+                    }
+                    else
+                    {
+                        await CopyToInternalAsync(source, responseStream, RangeLength).ConfigureAwait(false);
+                    }
+                }
+            }
+            catch (IOException ex)
+            {
+                throw;
+            }
+            catch (Exception ex)
+            {
+                _logger.ErrorException("Error in range request writer", ex);
+                throw;
+            }
+            finally
+            {
+                if (OnComplete != null)
+                {
+                    OnComplete();
+                }
+            }
+        }
+
+        private async Task CopyToInternalAsync(Stream source, Stream destination, long copyLength)
+        {
+            var array = new byte[BufferSize];
+            int count;
+            while ((count = await source.ReadAsync(array, 0, array.Length).ConfigureAwait(false)) != 0)
+            {
+                var bytesToCopy = Math.Min(count, copyLength);
+
+                await destination.WriteAsync(array, 0, Convert.ToInt32(bytesToCopy)).ConfigureAwait(false);
+
+                copyLength -= bytesToCopy;
+
+                if (copyLength <= 0)
+                {
+                    break;
+                }
+            }
+        }
+
         public string ContentType { get; set; }
 
         public IRequest RequestContext { get; set; }

+ 71 - 64
MediaBrowser.Server.Implementations/HttpServer/SocketSharp/RequestMono.cs

@@ -3,6 +3,7 @@ using System.Collections.Specialized;
 using System.Globalization;
 using System.IO;
 using System.Text;
+using System.Threading.Tasks;
 using System.Web;
 using ServiceStack;
 using ServiceStack.Web;
@@ -32,53 +33,54 @@ namespace MediaBrowser.Server.Implementations.HttpServer.SocketSharp
             return header.Substring(ap + 1, end - ap - 1);
         }
 
-        void LoadMultiPart()
+        async Task LoadMultiPart()
         {
             string boundary = GetParameter(ContentType, "; boundary=");
             if (boundary == null)
                 return;
 
-            var input = GetSubStream(InputStream);
+            using (var requestStream = GetSubStream(InputStream))
+            {
+                //DB: 30/01/11 - Hack to get around non-seekable stream and received HTTP request
+                //Not ending with \r\n?
+                var ms = new MemoryStream(32 * 1024);
+                await requestStream.CopyToAsync(ms).ConfigureAwait(false);
 
-            //DB: 30/01/11 - Hack to get around non-seekable stream and received HTTP request
-            //Not ending with \r\n?
-            var ms = new MemoryStream(32 * 1024);
-            input.CopyTo(ms);
-            input = ms;
-            ms.WriteByte((byte)'\r');
-            ms.WriteByte((byte)'\n');
+                var input = ms;
+                ms.WriteByte((byte)'\r');
+                ms.WriteByte((byte)'\n');
 
-            input.Position = 0;
+                input.Position = 0;
 
-            //Uncomment to debug
-            //var content = new StreamReader(ms).ReadToEnd();
-            //Console.WriteLine(boundary + "::" + content);
-            //input.Position = 0;
+                //Uncomment to debug
+                //var content = new StreamReader(ms).ReadToEnd();
+                //Console.WriteLine(boundary + "::" + content);
+                //input.Position = 0;
 
-            var multi_part = new HttpMultipart(input, boundary, ContentEncoding);
+                var multi_part = new HttpMultipart(input, boundary, ContentEncoding);
 
-            HttpMultipart.Element e;
-            while ((e = multi_part.ReadNextElement()) != null)
-            {
-                if (e.Filename == null)
+                HttpMultipart.Element e;
+                while ((e = multi_part.ReadNextElement()) != null)
                 {
-                    byte[] copy = new byte[e.Length];
+                    if (e.Filename == null)
+                    {
+                        byte[] copy = new byte[e.Length];
 
-                    input.Position = e.Start;
-                    input.Read(copy, 0, (int)e.Length);
+                        input.Position = e.Start;
+                        input.Read(copy, 0, (int)e.Length);
 
-                    form.Add(e.Name, (e.Encoding ?? ContentEncoding).GetString(copy));
-                }
-                else
-                {
-                    //
-                    // We use a substream, as in 2.x we will support large uploads streamed to disk,
-                    //
-                    HttpPostedFile sub = new HttpPostedFile(e.Filename, e.ContentType, input, e.Start, e.Length);
-                    files.AddFile(e.Name, sub);
+                        form.Add(e.Name, (e.Encoding ?? ContentEncoding).GetString(copy));
+                    }
+                    else
+                    {
+                        //
+                        // We use a substream, as in 2.x we will support large uploads streamed to disk,
+                        //
+                        HttpPostedFile sub = new HttpPostedFile(e.Filename, e.ContentType, input, e.Start, e.Length);
+                        files.AddFile(e.Name, sub);
+                    }
                 }
             }
-            EndSubStream(input);
         }
 
         public NameValueCollection Form
@@ -91,10 +93,15 @@ namespace MediaBrowser.Server.Implementations.HttpServer.SocketSharp
                     files = new HttpFileCollection();
 
                     if (IsContentType("multipart/form-data", true))
-                        LoadMultiPart();
-                    else if (
-                        IsContentType("application/x-www-form-urlencoded", true))
-                        LoadWwwForm();
+                    {
+                        var task = LoadMultiPart();
+                        Task.WaitAll(task);
+                    }
+                    else if (IsContentType("application/x-www-form-urlencoded", true))
+                    {
+                        var task = LoadWwwForm();
+                        Task.WaitAll(task);
+                    }
 
                     form.Protect();
                 }
@@ -220,50 +227,50 @@ namespace MediaBrowser.Server.Implementations.HttpServer.SocketSharp
             return String.Compare(ContentType, ct, true, Helpers.InvariantCulture) == 0;
         }
 
-
-
-
-
-        void LoadWwwForm()
+        async Task LoadWwwForm()
         {
             using (Stream input = GetSubStream(InputStream))
             {
-                using (StreamReader s = new StreamReader(input, ContentEncoding))
+                using (var ms = new MemoryStream())
                 {
-                    StringBuilder key = new StringBuilder();
-                    StringBuilder value = new StringBuilder();
-                    int c;
+                    await input.CopyToAsync(ms).ConfigureAwait(false);
+                    ms.Position = 0;
 
-                    while ((c = s.Read()) != -1)
+                    using (StreamReader s = new StreamReader(ms, ContentEncoding))
                     {
-                        if (c == '=')
+                        StringBuilder key = new StringBuilder();
+                        StringBuilder value = new StringBuilder();
+                        int c;
+
+                        while ((c = s.Read()) != -1)
                         {
-                            value.Length = 0;
-                            while ((c = s.Read()) != -1)
+                            if (c == '=')
                             {
-                                if (c == '&')
+                                value.Length = 0;
+                                while ((c = s.Read()) != -1)
+                                {
+                                    if (c == '&')
+                                    {
+                                        AddRawKeyValue(key, value);
+                                        break;
+                                    }
+                                    else
+                                        value.Append((char)c);
+                                }
+                                if (c == -1)
                                 {
                                     AddRawKeyValue(key, value);
-                                    break;
+                                    return;
                                 }
-                                else
-                                    value.Append((char)c);
                             }
-                            if (c == -1)
-                            {
+                            else if (c == '&')
                                 AddRawKeyValue(key, value);
-                                return;
-                            }
+                            else
+                                key.Append((char)c);
                         }
-                        else if (c == '&')
+                        if (c == -1)
                             AddRawKeyValue(key, value);
-                        else
-                            key.Append((char)c);
                     }
-                    if (c == -1)
-                        AddRawKeyValue(key, value);
-
-                    EndSubStream(input);
                 }
             }
         }

+ 79 - 6
MediaBrowser.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpRequest.cs

@@ -134,12 +134,89 @@ namespace MediaBrowser.Server.Implementations.HttpServer.SocketSharp
             get
             {
                 return remoteIp ??
-                    (remoteIp = XForwardedFor ??
-                                (NormalizeIp(XRealIp) ??
+                    (remoteIp = (CheckBadChars(XForwardedFor)) ??
+                                (NormalizeIp(CheckBadChars(XRealIp)) ??
                                 (request.RemoteEndPoint != null ? NormalizeIp(request.RemoteEndPoint.Address.ToString()) : null)));
             }
         }
 
+        private static readonly char[] HttpTrimCharacters = new char[] { (char)0x09, (char)0xA, (char)0xB, (char)0xC, (char)0xD, (char)0x20 };
+
+        //
+        // CheckBadChars - throws on invalid chars to be not found in header name/value
+        //
+        internal static string CheckBadChars(string name)
+        {
+            if (name == null || name.Length == 0)
+            {
+                return name;
+            }
+
+            // VALUE check
+            //Trim spaces from both ends
+            name = name.Trim(HttpTrimCharacters);
+
+            //First, check for correctly formed multi-line value
+            //Second, check for absenece of CTL characters
+            int crlf = 0;
+            for (int i = 0; i < name.Length; ++i)
+            {
+                char c = (char)(0x000000ff & (uint)name[i]);
+                switch (crlf)
+                {
+                    case 0:
+                        if (c == '\r')
+                        {
+                            crlf = 1;
+                        }
+                        else if (c == '\n')
+                        {
+                            // Technically this is bad HTTP.  But it would be a breaking change to throw here.
+                            // Is there an exploit?
+                            crlf = 2;
+                        }
+                        else if (c == 127 || (c < ' ' && c != '\t'))
+                        {
+                            throw new ArgumentException("net_WebHeaderInvalidControlChars");
+                        }
+                        break;
+
+                    case 1:
+                        if (c == '\n')
+                        {
+                            crlf = 2;
+                            break;
+                        }
+                        throw new ArgumentException("net_WebHeaderInvalidCRLFChars");
+
+                    case 2:
+                        if (c == ' ' || c == '\t')
+                        {
+                            crlf = 0;
+                            break;
+                        }
+                        throw new ArgumentException("net_WebHeaderInvalidCRLFChars");
+                }
+            }
+            if (crlf != 0)
+            {
+                throw new ArgumentException("net_WebHeaderInvalidCRLFChars");
+            }
+            return name;
+        }
+
+        internal static bool ContainsNonAsciiChars(string token)
+        {
+            for (int i = 0; i < token.Length; ++i)
+            {
+                if ((token[i] < 0x20) || (token[i] > 0x7e))
+                {
+                    return true;
+                }
+            }
+            return false;
+        }
+
         private string NormalizeIp(string ip)
         {
             if (!string.IsNullOrWhiteSpace(ip))
@@ -388,10 +465,6 @@ namespace MediaBrowser.Server.Implementations.HttpServer.SocketSharp
             return stream;
         }
 
-        static void EndSubStream(Stream stream)
-        {
-        }
-
         public static string GetHandlerPathIfAny(string listenerUrl)
         {
             if (listenerUrl == null) return null;

+ 34 - 13
MediaBrowser.Server.Implementations/HttpServer/StreamWriter.cs

@@ -4,13 +4,15 @@ using System;
 using System.Collections.Generic;
 using System.Globalization;
 using System.IO;
+using System.Threading.Tasks;
+using ServiceStack;
 
 namespace MediaBrowser.Server.Implementations.HttpServer
 {
     /// <summary>
     /// Class StreamWriter
     /// </summary>
-    public class StreamWriter : IStreamWriter, IHasOptions
+    public class StreamWriter : IStreamWriter, IAsyncStreamWriter, IHasOptions
     {
         private ILogger Logger { get; set; }
 
@@ -73,30 +75,49 @@ namespace MediaBrowser.Server.Implementations.HttpServer
         {
         }
 
+        // 256k
+        private const int BufferSize = 262144;
+
         /// <summary>
         /// Writes to.
         /// </summary>
         /// <param name="responseStream">The response stream.</param>
         public void WriteTo(Stream responseStream)
         {
-            WriteToInternal(responseStream);
+            try
+            {
+                using (var src = SourceStream)
+                {
+                    src.CopyTo(responseStream, BufferSize);
+                }
+            }
+            catch (Exception ex)
+            {
+                Logger.ErrorException("Error streaming data", ex);
+
+                if (OnError != null)
+                {
+                    OnError();
+                }
+
+                throw;
+            }
+            finally
+            {
+                if (OnComplete != null)
+                {
+                    OnComplete();
+                }
+            }
         }
 
-        // 256k
-        private const int BufferSize = 262144;
-        
-        /// <summary>
-        /// Writes to async.
-        /// </summary>
-        /// <param name="responseStream">The response stream.</param>
-        /// <returns>Task.</returns>
-        private void WriteToInternal(Stream responseStream)
+        public async Task WriteToAsync(Stream responseStream)
         {
             try
             {
                 using (var src = SourceStream)
                 {
-                    src.CopyTo(responseStream, BufferSize);
+                    await src.CopyToAsync(responseStream, BufferSize).ConfigureAwait(false);
                 }
             }
             catch (Exception ex)
@@ -107,7 +128,7 @@ namespace MediaBrowser.Server.Implementations.HttpServer
                 {
                     OnError();
                 }
-                
+
                 throw;
             }
             finally

+ 1 - 1
MediaBrowser.Server.Implementations/IO/LibraryMonitor.cs

@@ -84,7 +84,7 @@ namespace MediaBrowser.Server.Implementations.IO
             // This is an arbitraty amount of time, but delay it because file system writes often trigger events long after the file was actually written to.
             // Seeing long delays in some situations, especially over the network, sometimes up to 45 seconds
             // But if we make this delay too high, we risk missing legitimate changes, such as user adding a new file, or hand-editing metadata
-            await Task.Delay(25000).ConfigureAwait(false);
+            await Task.Delay(45000).ConfigureAwait(false);
 
             string val;
             _tempIgnoredPaths.TryRemove(path, out val);

+ 35 - 20
MediaBrowser.Server.Implementations/Library/LibraryManager.cs

@@ -33,6 +33,7 @@ using System.Net;
 using System.Threading;
 using System.Threading.Tasks;
 using CommonIO;
+using MediaBrowser.Controller.Channels;
 using MediaBrowser.Model.Channels;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Extensions;
@@ -353,10 +354,6 @@ namespace MediaBrowser.Server.Implementations.Library
 
         private void RegisterItem(Guid id, BaseItem item)
         {
-            if (item.SourceType != SourceType.Library)
-            {
-                return;
-            }
             if (item is IItemByName)
             {
                 if (!(item is MusicArtist))
@@ -364,14 +361,25 @@ namespace MediaBrowser.Server.Implementations.Library
                     return;
                 }
             }
-            if (item is Photo)
+
+            if (item.IsFolder)
             {
-                return;
+                if (!(item is ICollectionFolder) && !(item is UserView) && !(item is Channel))
+                {
+                    if (item.SourceType != SourceType.Library)
+                    {
+                        return;
+                    }
+                }
+            }
+            else
+            {
+                if (item is Photo)
+                {
+                    return;
+                }
             }
-            //if (!(item is Folder))
-            //{
-            //    return;
-            //}
+
             LibraryItemsCache.AddOrUpdate(id, item, delegate { return item; });
         }
 
@@ -782,19 +790,19 @@ namespace MediaBrowser.Server.Implementations.Library
 
         public BaseItem FindByPath(string path, bool? isFolder)
         {
+            // If this returns multiple items it could be tricky figuring out which one is correct. 
+            // In most cases, the newest one will be and the others obsolete but not yet cleaned up
+
             var query = new InternalItemsQuery
             {
                 Path = path,
-                IsFolder = isFolder
+                IsFolder = isFolder,
+                SortBy = new[] { ItemSortBy.DateCreated },
+                SortOrder = SortOrder.Descending,
+                Limit = 1
             };
 
-            // If this returns multiple items it could be tricky figuring out which one is correct. 
-            // In most cases, the newest one will be and the others obsolete but not yet cleaned up
-
-            return GetItemIds(query)
-                .Select(GetItemById)
-                .Where(i => i != null)
-                .OrderByDescending(i => i.DateCreated)
+            return GetItemList(query)
                 .FirstOrDefault();
         }
 
@@ -1258,6 +1266,8 @@ namespace MediaBrowser.Server.Implementations.Library
 
             item = RetrieveItem(id);
 
+            //_logger.Debug("GetitemById {0}", id);
+
             if (item != null)
             {
                 RegisterItem(item);
@@ -1508,7 +1518,7 @@ namespace MediaBrowser.Server.Implementations.Library
                         UserId = user.Id.ToString("N")
 
                     }, CancellationToken.None).Result;
-                    
+
                     return channelResult.Items;
                 }
 
@@ -1921,7 +1931,7 @@ namespace MediaBrowser.Server.Implementations.Library
 
         private string GetContentTypeOverride(string path, bool inherit)
         {
-            var nameValuePair = ConfigurationManager.Configuration.ContentTypes.FirstOrDefault(i => string.Equals(i.Name, path, StringComparison.OrdinalIgnoreCase) || (inherit && _fileSystem.ContainsSubPath(i.Name, path)));
+            var nameValuePair = ConfigurationManager.Configuration.ContentTypes.FirstOrDefault(i => string.Equals(i.Name, path, StringComparison.OrdinalIgnoreCase) || (inherit && !string.IsNullOrWhiteSpace(i.Name) && _fileSystem.ContainsSubPath(i.Name, path)));
             if (nameValuePair != null)
             {
                 return nameValuePair.Value;
@@ -2802,6 +2812,11 @@ namespace MediaBrowser.Server.Implementations.Library
 
         private void RemoveContentTypeOverrides(string path)
         {
+            if (string.IsNullOrWhiteSpace(path))
+            {
+                throw new ArgumentNullException("path");
+            }
+
             var removeList = new List<NameValuePair>();
 
             foreach (var contentType in ConfigurationManager.Configuration.ContentTypes)

+ 39 - 9
MediaBrowser.Server.Implementations/Library/Resolvers/PhotoResolver.cs

@@ -5,15 +5,19 @@ using MediaBrowser.Model.Entities;
 using System;
 using System.IO;
 using System.Linq;
+using CommonIO;
 
 namespace MediaBrowser.Server.Implementations.Library.Resolvers
 {
     public class PhotoResolver : ItemResolver<Photo>
     {
         private readonly IImageProcessor _imageProcessor;
-        public PhotoResolver(IImageProcessor imageProcessor)
+        private readonly ILibraryManager _libraryManager;
+
+        public PhotoResolver(IImageProcessor imageProcessor, ILibraryManager libraryManager)
         {
             _imageProcessor = imageProcessor;
+            _libraryManager = libraryManager;
         }
 
         /// <summary>
@@ -23,20 +27,45 @@ namespace MediaBrowser.Server.Implementations.Library.Resolvers
         /// <returns>Trailer.</returns>
         protected override Photo Resolve(ItemResolveArgs args)
         {
-            // Must be an image file within a photo collection
-            if (string.Equals(args.GetCollectionType(), CollectionType.Photos, StringComparison.OrdinalIgnoreCase) &&
-                !args.IsDirectory &&
-                IsImageFile(args.Path, _imageProcessor))
+            if (!args.IsDirectory)
             {
-                return new Photo
+                // Must be an image file within a photo collection
+                var collectionType = args.GetCollectionType();
+
+                if (string.Equals(collectionType, CollectionType.Photos, StringComparison.OrdinalIgnoreCase) ||
+                    string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase))
                 {
-                    Path = args.Path
-                };
+                    if (IsImageFile(args.Path, _imageProcessor))
+                    {
+                        var filename = Path.GetFileNameWithoutExtension(args.Path);
+
+                        // Make sure the image doesn't belong to a video file
+                        if (args.DirectoryService.GetFiles(Path.GetDirectoryName(args.Path)).Any(i => IsOwnedByMedia(i, filename)))
+                        {
+                            return null;
+                        }
+
+                        return new Photo
+                        {
+                            Path = args.Path
+                        };
+                    }
+                }
             }
 
             return null;
         }
 
+        private bool IsOwnedByMedia(FileSystemMetadata file, string imageFilename)
+        {
+            if (_libraryManager.IsVideoFile(file.FullName) && imageFilename.StartsWith(Path.GetFileNameWithoutExtension(file.Name), StringComparison.OrdinalIgnoreCase))
+            {
+                return true;
+            }
+
+            return false;
+        }
+
         private static readonly string[] IgnoreFiles =
         {
             "folder",
@@ -44,7 +73,8 @@ namespace MediaBrowser.Server.Implementations.Library.Resolvers
             "landscape",
             "fanart",
             "backdrop",
-            "poster"
+            "poster",
+            "cover"
         };
 
         internal static bool IsImageFile(string path, IImageProcessor imageProcessor)

+ 21 - 2
MediaBrowser.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs

@@ -31,7 +31,6 @@ namespace MediaBrowser.Server.Implementations.Library.Resolvers.TV
             }
 
             var season = parent as Season;
-
             // Just in case the user decided to nest episodes. 
             // Not officially supported but in some cases we can handle it.
             if (season == null)
@@ -41,10 +40,30 @@ namespace MediaBrowser.Server.Implementations.Library.Resolvers.TV
 
             // If the parent is a Season or Series, then this is an Episode if the VideoResolver returns something
             // Also handle flat tv folders
-            if (season != null || args.HasParent<Series>() || string.Equals(args.GetCollectionType(), CollectionType.TvShows, StringComparison.OrdinalIgnoreCase))
+            if (season != null || 
+                string.Equals(args.GetCollectionType(), CollectionType.TvShows, StringComparison.OrdinalIgnoreCase) || 
+                args.HasParent<Series>())
             {
                 var episode = ResolveVideo<Episode>(args, false);
 
+                if (episode != null)
+                {
+                    var series = parent as Series;
+                    if (series == null)
+                    {
+                        series = parent.GetParents().OfType<Series>().FirstOrDefault();
+                    }
+
+                    if (series != null)
+                    {
+                        episode.SeriesId = series.Id;
+                    }
+                    if (season != null)
+                    {
+                        episode.SeasonId = season.Id;
+                    }
+                }
+
                 return episode;
             }
 

+ 4 - 2
MediaBrowser.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs

@@ -38,10 +38,12 @@ namespace MediaBrowser.Server.Implementations.Library.Resolvers.TV
             if (args.Parent is Series && args.IsDirectory)
             {
                 var namingOptions = ((LibraryManager)_libraryManager).GetNamingOptions();
-                
+                var series = ((Series)args.Parent);
+
                 var season = new Season
                 {
-                    IndexNumber = new SeasonPathParser(namingOptions, new RegexProvider()).Parse(args.Path, true, true).SeasonNumber
+                    IndexNumber = new SeasonPathParser(namingOptions, new RegexProvider()).Parse(args.Path, true, true).SeasonNumber,
+                    SeriesId = series.Id
                 };
                 
                 if (season.IndexNumber.HasValue && season.IndexNumber.Value == 0)

+ 0 - 2
MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs

@@ -1214,8 +1214,6 @@ namespace MediaBrowser.Server.Implementations.LiveTv
                     var item = await GetChannel(channelInfo.Item2, channelInfo.Item1, parentFolderId, cancellationToken).ConfigureAwait(false);
 
                     list.Add(item);
-
-                    _libraryManager.RegisterItem(item);
                 }
                 catch (OperationCanceledException)
                 {

+ 43 - 16
MediaBrowser.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs

@@ -17,6 +17,7 @@ using System.Threading.Tasks;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Controller.MediaEncoding;
 using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.Net;
 
 namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun
 {
@@ -106,18 +107,31 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun
 
         private async Task<string> GetModelInfo(TunerHostInfo info, CancellationToken cancellationToken)
         {
-            using (var stream = await _httpClient.Get(new HttpRequestOptions()
+            try
             {
-                Url = string.Format("{0}/discover.json", GetApiUrl(info, false)),
-                CancellationToken = cancellationToken,
-                CacheLength = TimeSpan.FromDays(1),
-                CacheMode = CacheMode.Unconditional,
-                TimeoutMs = Convert.ToInt32(TimeSpan.FromSeconds(5).TotalMilliseconds)
-            }))
+                using (var stream = await _httpClient.Get(new HttpRequestOptions()
+                {
+                    Url = string.Format("{0}/discover.json", GetApiUrl(info, false)),
+                    CancellationToken = cancellationToken,
+                    CacheLength = TimeSpan.FromDays(1),
+                    CacheMode = CacheMode.Unconditional,
+                    TimeoutMs = Convert.ToInt32(TimeSpan.FromSeconds(5).TotalMilliseconds)
+                }))
+                {
+                    var response = JsonSerializer.DeserializeFromStream<DiscoverResponse>(stream);
+
+                    return response.ModelNumber;
+                }
+            }
+            catch (HttpException ex)
             {
-                var response = JsonSerializer.DeserializeFromStream<DiscoverResponse>(stream);
+                if (ex.StatusCode.HasValue && ex.StatusCode.Value == System.Net.HttpStatusCode.NotFound)
+                {
+                    // HDHR4 doesn't have this api
+                    return "HDHR";
+                }
 
-                return response.ModelNumber;
+                throw;
             }
         }
 
@@ -455,16 +469,29 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun
                 return;
             }
 
-            // Test it by pulling down the lineup
-            using (var stream = await _httpClient.Get(new HttpRequestOptions
+            try
             {
-                Url = string.Format("{0}/discover.json", GetApiUrl(info, false)),
-                CancellationToken = CancellationToken.None
-            }))
+                // Test it by pulling down the lineup
+                using (var stream = await _httpClient.Get(new HttpRequestOptions
+                {
+                    Url = string.Format("{0}/discover.json", GetApiUrl(info, false)),
+                    CancellationToken = CancellationToken.None
+                }))
+                {
+                    var response = JsonSerializer.DeserializeFromStream<DiscoverResponse>(stream);
+
+                    info.DeviceId = response.DeviceID;
+                }
+            }
+            catch (HttpException ex)
             {
-                var response = JsonSerializer.DeserializeFromStream<DiscoverResponse>(stream);
+                if (ex.StatusCode.HasValue && ex.StatusCode.Value == System.Net.HttpStatusCode.NotFound)
+                {
+                    // HDHR4 doesn't have this api
+                    return;
+                }
 
-                info.DeviceId = response.DeviceID;
+                throw;
             }
         }
 

+ 6 - 7
MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj

@@ -56,8 +56,8 @@
     <Reference Include="Interfaces.IO">
       <HintPath>..\packages\Interfaces.IO.1.0.0.5\lib\portable-net45+sl4+wp71+win8+wpa81\Interfaces.IO.dll</HintPath>
     </Reference>
-    <Reference Include="MediaBrowser.Naming, Version=1.0.6012.15754, Culture=neutral, processorArchitecture=MSIL">
-      <HintPath>..\packages\MediaBrowser.Naming.1.0.0.52\lib\portable-net45+sl4+wp71+win8+wpa81\MediaBrowser.Naming.dll</HintPath>
+    <Reference Include="MediaBrowser.Naming, Version=1.0.6046.32295, Culture=neutral, processorArchitecture=MSIL">
+      <HintPath>..\packages\MediaBrowser.Naming.1.0.0.53\lib\portable-net45+sl4+wp71+win8+wpa81\MediaBrowser.Naming.dll</HintPath>
       <Private>True</Private>
     </Reference>
     <Reference Include="MoreLinq">
@@ -73,8 +73,8 @@
       <HintPath>..\packages\SimpleInjector.3.2.0\lib\net45\SimpleInjector.dll</HintPath>
       <Private>True</Private>
     </Reference>
-    <Reference Include="SocketHttpListener, Version=1.0.5955.1537, Culture=neutral, processorArchitecture=MSIL">
-      <HintPath>..\packages\SocketHttpListener.1.0.0.30\lib\net45\SocketHttpListener.dll</HintPath>
+    <Reference Include="SocketHttpListener, Version=1.0.6046.26351, Culture=neutral, processorArchitecture=MSIL">
+      <HintPath>..\packages\SocketHttpListener.1.0.0.35\lib\net45\SocketHttpListener.dll</HintPath>
       <Private>True</Private>
     </Reference>
     <Reference Include="System" />
@@ -156,6 +156,7 @@
     <Compile Include="EntryPoints\ServerEventNotifier.cs" />
     <Compile Include="EntryPoints\UserDataChangeNotifier.cs" />
     <Compile Include="FileOrganization\OrganizerScheduledTask.cs" />
+    <Compile Include="HttpServer\AsyncStreamWriterFunc.cs" />
     <Compile Include="HttpServer\IHttpListener.cs" />
     <Compile Include="HttpServer\Security\AuthorizationContext.cs" />
     <Compile Include="HttpServer\ContainerAdapter.cs" />
@@ -757,9 +758,7 @@
     <EmbeddedResource Include="Localization\iso6392.txt" />
     <EmbeddedResource Include="Localization\Ratings\be.txt" />
   </ItemGroup>
-  <ItemGroup>
-    <Folder Include="HttpServer\NetListener\" />
-  </ItemGroup>
+  <ItemGroup />
   <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
   <!-- To modify your build process, add your task inside one of the targets below and uncomment it. 
        Other similar extension points exist, see Microsoft.Common.targets.

+ 2 - 0
MediaBrowser.Server.Implementations/MediaEncoder/EncodingManager.cs

@@ -152,6 +152,7 @@ namespace MediaBrowser.Server.Implementations.MediaEncoder
                             }
 
                             chapter.ImagePath = path;
+                            chapter.ImageDateModified = _fileSystem.GetLastWriteTimeUtc(path);
                             changesMade = true;
                         }
                         catch (Exception ex)
@@ -170,6 +171,7 @@ namespace MediaBrowser.Server.Implementations.MediaEncoder
                 else if (!string.Equals(path, chapter.ImagePath, StringComparison.OrdinalIgnoreCase))
                 {
                     chapter.ImagePath = path;
+                    chapter.ImageDateModified = _fileSystem.GetLastWriteTimeUtc(path);
                     changesMade = true;
                 }
             }

+ 37 - 12
MediaBrowser.Server.Implementations/Persistence/CleanDatabaseScheduledTask.cs

@@ -155,6 +155,8 @@ namespace MediaBrowser.Server.Implementations.Persistence
 
             _logger.Debug("Upgrading schema for {0} items", numItems);
 
+            var list = new List<BaseItem>();
+
             foreach (var itemId in itemIds)
             {
                 cancellationToken.ThrowIfCancellationRequested();
@@ -166,19 +168,26 @@ namespace MediaBrowser.Server.Implementations.Persistence
 
                     if (item != null)
                     {
-                        try
-                        {
-                            await _itemRepo.SaveItem(item, cancellationToken).ConfigureAwait(false);
-                        }
-                        catch (OperationCanceledException)
-                        {
-                            throw;
-                        }
-                        catch (Exception ex)
-                        {
-                            _logger.ErrorException("Error saving item", ex);
-                        }
+                        list.Add(item);
+                    }
+                }
+
+                if (list.Count >= 1000)
+                {
+                    try
+                    {
+                        await _itemRepo.SaveItems(list, cancellationToken).ConfigureAwait(false);
                     }
+                    catch (OperationCanceledException)
+                    {
+                        throw;
+                    }
+                    catch (Exception ex)
+                    {
+                        _logger.ErrorException("Error saving item", ex);
+                    }
+
+                    list.Clear();
                 }
 
                 numComplete++;
@@ -187,6 +196,22 @@ namespace MediaBrowser.Server.Implementations.Persistence
                 progress.Report(percent * 100);
             }
 
+            if (list.Count > 0)
+            {
+                try
+                {
+                    await _itemRepo.SaveItems(list, cancellationToken).ConfigureAwait(false);
+                }
+                catch (OperationCanceledException)
+                {
+                    throw;
+                }
+                catch (Exception ex)
+                {
+                    _logger.ErrorException("Error saving item", ex);
+                }
+            }
+
             progress.Report(100);
         }
 

+ 131 - 8
MediaBrowser.Server.Implementations/Persistence/SqliteItemRepository.cs

@@ -95,7 +95,7 @@ namespace MediaBrowser.Server.Implementations.Persistence
         private IDbCommand _updateInheritedRatingCommand;
         private IDbCommand _updateInheritedTagsCommand;
 
-        public const int LatestSchemaVersion = 97;
+        public const int LatestSchemaVersion = 108;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="SqliteItemRepository"/> class.
@@ -269,10 +269,16 @@ namespace MediaBrowser.Server.Implementations.Persistence
             _connection.AddColumn(Logger, "TypedBaseItems", "IsVirtualItem", "BIT");
             _connection.AddColumn(Logger, "TypedBaseItems", "SeriesName", "Text");
             _connection.AddColumn(Logger, "TypedBaseItems", "UserDataKey", "Text");
+            _connection.AddColumn(Logger, "TypedBaseItems", "SeasonName", "Text");
+            _connection.AddColumn(Logger, "TypedBaseItems", "SeasonId", "GUID");
+            _connection.AddColumn(Logger, "TypedBaseItems", "SeriesId", "GUID");
+            _connection.AddColumn(Logger, "TypedBaseItems", "SeriesSortName", "Text");
 
             _connection.AddColumn(Logger, "UserDataKeys", "Priority", "INT");
             _connection.AddColumn(Logger, "ItemValues", "CleanValue", "Text");
 
+            _connection.AddColumn(Logger, ChaptersTableName, "ImageDateModified", "DATETIME");
+
             string[] postQueries =
 
                                 {
@@ -403,7 +409,12 @@ namespace MediaBrowser.Server.Implementations.Persistence
             "Album",
             "CriticRating",
             "CriticRatingSummary",
-            "IsVirtualItem"
+            "IsVirtualItem",
+            "SeriesName",
+            "SeasonName",
+            "SeasonId",
+            "SeriesId",
+            "SeriesSortName"
         };
 
         private readonly string[] _mediaStreamSaveColumns =
@@ -523,7 +534,11 @@ namespace MediaBrowser.Server.Implementations.Persistence
                 "Album",
                 "IsVirtualItem",
                 "SeriesName",
-                "UserDataKey"
+                "UserDataKey",
+                "SeasonName",
+                "SeasonId",
+                "SeriesId",
+                "SeriesSortName"
             };
             _saveItemCommand = _connection.CreateCommand();
             _saveItemCommand.CommandText = "replace into TypedBaseItems (" + string.Join(",", saveColumns.ToArray()) + ") values (";
@@ -582,6 +597,7 @@ namespace MediaBrowser.Server.Implementations.Persistence
             _saveChapterCommand.Parameters.Add(_saveChapterCommand, "@StartPositionTicks");
             _saveChapterCommand.Parameters.Add(_saveChapterCommand, "@Name");
             _saveChapterCommand.Parameters.Add(_saveChapterCommand, "@ImagePath");
+            _saveChapterCommand.Parameters.Add(_saveChapterCommand, "@ImageDateModified");
 
             // MediaStreams
             _deleteStreamsCommand = _connection.CreateCommand();
@@ -945,7 +961,7 @@ namespace MediaBrowser.Server.Implementations.Persistence
                     var hasSeries = item as IHasSeries;
                     if (hasSeries != null)
                     {
-                        _saveItemCommand.GetParameter(index++).Value = hasSeries.SeriesName;
+                        _saveItemCommand.GetParameter(index++).Value = hasSeries.FindSeriesName();
                     }
                     else
                     {
@@ -954,6 +970,29 @@ namespace MediaBrowser.Server.Implementations.Persistence
 
                     _saveItemCommand.GetParameter(index++).Value = item.GetUserDataKeys().FirstOrDefault();
 
+                    var episode = item as Episode;
+                    if (episode != null)
+                    {
+                        _saveItemCommand.GetParameter(index++).Value = episode.FindSeasonName();
+                        _saveItemCommand.GetParameter(index++).Value = episode.FindSeasonId();
+                    }
+                    else
+                    {
+                        _saveItemCommand.GetParameter(index++).Value = null;
+                        _saveItemCommand.GetParameter(index++).Value = null;
+                    }
+
+                    if (hasSeries != null)
+                    {
+                        _saveItemCommand.GetParameter(index++).Value = hasSeries.FindSeriesId();
+                        _saveItemCommand.GetParameter(index++).Value = hasSeries.FindSeriesSortName();
+                    }
+                    else
+                    {
+                        _saveItemCommand.GetParameter(index++).Value = null;
+                        _saveItemCommand.GetParameter(index++).Value = null;
+                    }
+
                     _saveItemCommand.Transaction = transaction;
 
                     _saveItemCommand.ExecuteNonQuery();
@@ -1376,6 +1415,44 @@ namespace MediaBrowser.Server.Implementations.Persistence
                 item.IsVirtualItem = reader.GetBoolean(58);
             }
 
+            var hasSeries = item as IHasSeries;
+            if (hasSeries != null)
+            {
+                if (!reader.IsDBNull(59))
+                {
+                    hasSeries.SeriesName = reader.GetString(59);
+                }
+            }
+
+            var episode = item as Episode;
+            if (episode != null)
+            {
+                if (!reader.IsDBNull(60))
+                {
+                    episode.SeasonName = reader.GetString(60);
+                }
+                if (!reader.IsDBNull(61))
+                {
+                    episode.SeasonId = reader.GetGuid(61);
+                }
+            }
+
+            if (hasSeries != null)
+            {
+                if (!reader.IsDBNull(62))
+                {
+                    hasSeries.SeriesId = reader.GetGuid(62);
+                }
+            }
+
+            if (hasSeries != null)
+            {
+                if (!reader.IsDBNull(63))
+                {
+                    hasSeries.SeriesSortName = reader.GetString(63);
+                }
+            }
+
             return item;
         }
 
@@ -1437,7 +1514,7 @@ namespace MediaBrowser.Server.Implementations.Persistence
 
             using (var cmd = _connection.CreateCommand())
             {
-                cmd.CommandText = "select StartPositionTicks,Name,ImagePath from " + ChaptersTableName + " where ItemId = @ItemId order by ChapterIndex asc";
+                cmd.CommandText = "select StartPositionTicks,Name,ImagePath,ImageDateModified from " + ChaptersTableName + " where ItemId = @ItemId order by ChapterIndex asc";
 
                 cmd.Parameters.Add(cmd, "@ItemId", DbType.Guid).Value = id;
 
@@ -1470,7 +1547,7 @@ namespace MediaBrowser.Server.Implementations.Persistence
 
             using (var cmd = _connection.CreateCommand())
             {
-                cmd.CommandText = "select StartPositionTicks,Name,ImagePath from " + ChaptersTableName + " where ItemId = @ItemId and ChapterIndex=@ChapterIndex";
+                cmd.CommandText = "select StartPositionTicks,Name,ImagePath,ImageDateModified from " + ChaptersTableName + " where ItemId = @ItemId and ChapterIndex=@ChapterIndex";
 
                 cmd.Parameters.Add(cmd, "@ItemId", DbType.Guid).Value = id;
                 cmd.Parameters.Add(cmd, "@ChapterIndex", DbType.Int32).Value = index;
@@ -1508,6 +1585,11 @@ namespace MediaBrowser.Server.Implementations.Persistence
                 chapter.ImagePath = reader.GetString(2);
             }
 
+            if (!reader.IsDBNull(3))
+            {
+                chapter.ImageDateModified = reader.GetDateTime(3).ToUniversalTime();
+            }
+
             return chapter;
         }
 
@@ -1567,6 +1649,7 @@ namespace MediaBrowser.Server.Implementations.Persistence
                     _saveChapterCommand.GetParameter(2).Value = chapter.StartPositionTicks;
                     _saveChapterCommand.GetParameter(3).Value = chapter.Name;
                     _saveChapterCommand.GetParameter(4).Value = chapter.ImagePath;
+                    _saveChapterCommand.GetParameter(5).Value = chapter.ImageDateModified;
 
                     _saveChapterCommand.Transaction = transaction;
 
@@ -2061,7 +2144,7 @@ namespace MediaBrowser.Server.Implementations.Persistence
                 {
                     if (query.User != null)
                     {
-                        query.SortBy = new[] { "SimilarityScore", ItemSortBy.IsPlayed, ItemSortBy.Random };
+                        query.SortBy = new[] { ItemSortBy.IsPlayed, "SimilarityScore", ItemSortBy.Random };
                     }
                     else
                     {
@@ -2986,6 +3069,39 @@ namespace MediaBrowser.Server.Implementations.Persistence
                     whereClauses.Add("LocationType<>'Virtual'");
                 }
             }
+            if (query.IsUnaired.HasValue)
+            {
+                if (query.IsUnaired.Value)
+                {
+                    whereClauses.Add("PremiereDate >= DATETIME('now')");
+                }
+                else
+                {
+                    whereClauses.Add("PremiereDate < DATETIME('now')");
+                }
+            }
+            if (query.IsMissing.HasValue && _config.Configuration.SchemaVersion >= 90)
+            {
+                if (query.IsMissing.Value)
+                {
+                    whereClauses.Add("(IsVirtualItem=1 AND PremiereDate < DATETIME('now'))");
+                }
+                else
+                {
+                    whereClauses.Add("(IsVirtualItem=0 OR PremiereDate >= DATETIME('now'))");
+                }
+            }
+            if (query.IsVirtualUnaired.HasValue && _config.Configuration.SchemaVersion >= 90)
+            {
+                if (query.IsVirtualUnaired.Value)
+                {
+                    whereClauses.Add("(IsVirtualItem=1 AND PremiereDate >= DATETIME('now'))");
+                }
+                else
+                {
+                    whereClauses.Add("(IsVirtualItem=0 OR PremiereDate < DATETIME('now'))");
+                }
+            }
             if (query.MediaTypes.Length == 1)
             {
                 whereClauses.Add("MediaType=@MediaTypes");
@@ -4063,6 +4179,13 @@ namespace MediaBrowser.Server.Implementations.Persistence
                 throw new ArgumentNullException("values");
             }
 
+            // Just in case there might be case-insensitive duplicates, strip them out now
+            var newValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+            foreach (var pair in values)
+            {
+                newValues[pair.Key] = pair.Value;
+            }
+
             CheckDisposed();
 
             // First delete 
@@ -4071,7 +4194,7 @@ namespace MediaBrowser.Server.Implementations.Persistence
 
             _deleteProviderIdsCommand.ExecuteNonQuery();
 
-            foreach (var pair in values)
+            foreach (var pair in newValues)
             {
                 _saveProviderIdsCommand.GetParameter(0).Value = itemId;
                 _saveProviderIdsCommand.GetParameter(1).Value = pair.Key;

+ 9 - 6
MediaBrowser.Server.Implementations/Playlists/PlaylistManager.cs

@@ -247,15 +247,18 @@ namespace MediaBrowser.Server.Implementations.Playlists
                 return;
             }
 
-            if (newIndex > oldIndex)
-            {
-                newIndex--;
-            }
-
             var item = playlist.LinkedChildren[oldIndex];
 
             playlist.LinkedChildren.Remove(item);
-            playlist.LinkedChildren.Insert(newIndex, item);
+
+            if (newIndex >= playlist.LinkedChildren.Count)
+            {
+                playlist.LinkedChildren.Add(item);
+            }
+            else
+            {
+                playlist.LinkedChildren.Insert(newIndex, item);
+            }
 
             await playlist.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
         }

+ 2 - 22
MediaBrowser.Server.Implementations/Sorting/SeriesSortNameComparer.cs

@@ -1,5 +1,4 @@
 using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Sorting;
 using MediaBrowser.Model.Querying;
 using System;
@@ -21,28 +20,9 @@ namespace MediaBrowser.Server.Implementations.Sorting
 
         private string GetValue(BaseItem item)
         {
-            Series series = null;
+            var hasSeries = item as IHasSeries;
 
-            var season = item as Season;
-
-            if (season != null)
-            {
-                series = season.Series;
-            }
-
-            var episode = item as Episode;
-
-            if (episode != null)
-            {
-                series = episode.Series;
-            }
-
-            if (series == null)
-            {
-                series = item as Series;
-            }
-
-            return series != null ? series.SortName : null;
+            return hasSeries != null ? hasSeries.SeriesSortName : null;
         }
 
         /// <summary>

+ 12 - 2
MediaBrowser.Server.Implementations/TV/TVSeriesManager.cs

@@ -50,6 +50,11 @@ namespace MediaBrowser.Server.Implementations.TV
                 }
             }
 
+            if (string.IsNullOrWhiteSpace(presentationUniqueKey) && limit.HasValue)
+            {
+                limit = limit.Value + 10;
+            }
+
             var items = _libraryManager.GetItemList(new InternalItemsQuery(user)
             {
                 IncludeItemTypes = new[] { typeof(Series).Name },
@@ -89,6 +94,11 @@ namespace MediaBrowser.Server.Implementations.TV
                 }
             }
 
+            if (string.IsNullOrWhiteSpace(presentationUniqueKey) && limit.HasValue)
+            {
+                limit = limit.Value + 10;
+            }
+
             var items = _libraryManager.GetItemList(new InternalItemsQuery(user)
             {
                 IncludeItemTypes = new[] { typeof(Series).Name },
@@ -115,7 +125,8 @@ namespace MediaBrowser.Server.Implementations.TV
                 .Where(i => i.Item1 != null && (!i.Item3 || !string.IsNullOrWhiteSpace(request.SeriesId)))
                 .OrderByDescending(i => i.Item2)
                 .ThenByDescending(i => i.Item1.PremiereDate ?? DateTime.MinValue)
-                .Select(i => i.Item1);
+                .Select(i => i.Item1)
+                .Take(request.Limit ?? int.MaxValue);
         }
 
         private string GetUniqueSeriesKey(BaseItem series)
@@ -143,7 +154,6 @@ namespace MediaBrowser.Server.Implementations.TV
                 SortOrder = SortOrder.Descending,
                 IsPlayed = true,
                 Limit = 1,
-                IsVirtualItem = false,
                 ParentIndexNumberNotEquals = 0
 
             }).FirstOrDefault();

+ 0 - 5
MediaBrowser.Server.Implementations/UserViews/CollectionFolderImageProvider.cs

@@ -54,11 +54,6 @@ namespace MediaBrowser.Server.Implementations.UserViews
                     {
                         return series;
                     }
-                    var episodeSeason = episode.Season;
-                    if (episodeSeason != null)
-                    {
-                        return episodeSeason;
-                    }
 
                     return episode;
                 }

+ 0 - 5
MediaBrowser.Server.Implementations/UserViews/DynamicImageProvider.cs

@@ -86,11 +86,6 @@ namespace MediaBrowser.Server.Implementations.UserViews
                     {
                         return series;
                     }
-                    var episodeSeason = episode.Season;
-                    if (episodeSeason != null)
-                    {
-                        return episodeSeason;
-                    }
 
                     return episode;
                 }

+ 2 - 2
MediaBrowser.Server.Implementations/packages.config

@@ -4,10 +4,10 @@
   <package id="Emby.XmlTv" version="1.0.0.55" targetFramework="net45" />
   <package id="ini-parser" version="2.3.0" targetFramework="net45" />
   <package id="Interfaces.IO" version="1.0.0.5" targetFramework="net45" />
-  <package id="MediaBrowser.Naming" version="1.0.0.52" targetFramework="net45" />
+  <package id="MediaBrowser.Naming" version="1.0.0.53" targetFramework="net45" />
   <package id="Mono.Nat" version="1.2.24.0" targetFramework="net45" />
   <package id="morelinq" version="1.4.0" targetFramework="net45" />
   <package id="Patterns.Logging" version="1.0.0.2" targetFramework="net45" />
   <package id="SimpleInjector" version="3.2.0" targetFramework="net45" />
-  <package id="SocketHttpListener" version="1.0.0.30" targetFramework="net45" />
+  <package id="SocketHttpListener" version="1.0.0.35" targetFramework="net45" />
 </packages>

+ 1 - 1
MediaBrowser.Server.Startup.Common/ApplicationHost.cs

@@ -274,7 +274,7 @@ namespace MediaBrowser.Server.Startup.Common
         {
             get
             {
-                return "Media Browser Server";
+                return "Emby Server";
             }
         }
 

+ 76 - 7
MediaBrowser.ServerApplication/MainStartup.cs

@@ -12,6 +12,7 @@ using System.Configuration.Install;
 using System.Diagnostics;
 using System.IO;
 using System.Linq;
+using System.Management;
 using System.Runtime.InteropServices;
 using System.ServiceProcess;
 using System.Threading;
@@ -102,7 +103,7 @@ namespace MediaBrowser.ServerApplication
 
             if (IsAlreadyRunning(applicationPath, currentProcess))
             {
-                logger.Info("Shutting down because another instance of Media Browser Server is already running.");
+                logger.Info("Shutting down because another instance of Emby Server is already running.");
                 return;
             }
 
@@ -130,13 +131,28 @@ namespace MediaBrowser.ServerApplication
         /// <returns><c>true</c> if [is already running] [the specified current process]; otherwise, <c>false</c>.</returns>
         private static bool IsAlreadyRunning(string applicationPath, Process currentProcess)
         {
-            var filename = Path.GetFileName(applicationPath);
-
             var duplicate = Process.GetProcesses().FirstOrDefault(i =>
             {
                 try
                 {
-                    return string.Equals(filename, Path.GetFileName(i.MainModule.FileName)) && currentProcess.Id != i.Id;
+                    if (currentProcess.Id == i.Id)
+                    {
+                        return false;
+                    }
+                }
+                catch (Exception)
+                {
+                    return false;
+                }
+
+                try
+                {
+                    //_logger.Info("Module: {0}", i.MainModule.FileName);
+                    if (string.Equals(applicationPath, i.MainModule.FileName, StringComparison.OrdinalIgnoreCase))
+                    {
+                        return true;
+                    }
+                    return false;
                 }
                 catch (Exception)
                 {
@@ -155,6 +171,41 @@ namespace MediaBrowser.ServerApplication
                 }
             }
 
+            if (!_isRunningAsService)
+            {
+                return IsAlreadyRunningAsService(applicationPath);
+            }
+
+            return false;
+        }
+
+        private static bool IsAlreadyRunningAsService(string applicationPath)
+        {
+            var serviceName = BackgroundService.GetExistingServiceName();
+
+            WqlObjectQuery wqlObjectQuery = new WqlObjectQuery(string.Format("SELECT * FROM Win32_Service WHERE State = 'Running' AND Name = '{0}'", serviceName));
+            ManagementObjectSearcher managementObjectSearcher = new ManagementObjectSearcher(wqlObjectQuery);
+            ManagementObjectCollection managementObjectCollection = managementObjectSearcher.Get();
+
+            foreach (ManagementObject managementObject in managementObjectCollection)
+            {
+                var obj = managementObject.GetPropertyValue("PathName");
+                if (obj == null)
+                {
+                    continue;
+                }
+                var path = obj.ToString();
+
+                _logger.Info("Service path: {0}", path);
+                // Need to use indexOf instead of equality because the path will have the full service command line
+                if (path.IndexOf(applicationPath, StringComparison.OrdinalIgnoreCase) != -1)
+                {
+                    _logger.Info("The windows service is already running");
+                    MessageBox.Show("Emby Server is already running as a Windows Service. Only one instance is allowed at a time. To run as a tray icon, shut down the Windows Service.");
+                    return true;
+                }
+            }
+
             return false;
         }
 
@@ -593,14 +644,32 @@ namespace MediaBrowser.ServerApplication
 
         private static async Task InstallVcredist2013IfNeeded(ApplicationHost appHost, ILogger logger)
         {
+            // Reference 
+            // http://stackoverflow.com/questions/12206314/detect-if-visual-c-redistributable-for-visual-studio-2012-is-installed
+
             try
             {
-                var version = ImageMagickEncoder.GetVersion();
-                return;
+                var subkey = Environment.Is64BitProcess
+                    ? "SOFTWARE\\WOW6432Node\\Microsoft\\VisualStudio\\12.0\\VC\\Runtimes\\x64"
+                    : "SOFTWARE\\Microsoft\\VisualStudio\\12.0\\VC\\Runtimes\\x86";
+
+                using (RegistryKey ndpKey = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Default)
+                    .OpenSubKey(subkey))
+                {
+                    if (ndpKey != null && ndpKey.GetValue("Version") != null)
+                    {
+                        var installedVersion = ((string)ndpKey.GetValue("Version")).TrimStart('v');
+                        if (installedVersion.StartsWith("12", StringComparison.OrdinalIgnoreCase))
+                        {
+                            return;
+                        }
+                    }
+                }
             }
             catch (Exception ex)
             {
-                logger.ErrorException("Error loading ImageMagick", ex);
+                logger.ErrorException("Error getting .NET Framework version", ex);
+                return;
             }
 
             try

+ 1 - 0
MediaBrowser.ServerApplication/MediaBrowser.ServerApplication.csproj

@@ -88,6 +88,7 @@
       <Private>True</Private>
     </Reference>
     <Reference Include="System.Drawing" />
+    <Reference Include="System.Management" />
     <Reference Include="System.ServiceProcess" />
     <Reference Include="System.Windows.Forms" />
     <Reference Include="System.Xml.Linq" />

+ 5 - 10
MediaBrowser.WebDashboard/Api/DashboardService.cs

@@ -349,21 +349,16 @@ namespace MediaBrowser.WebDashboard.Api
             }
 
             _fileSystem.DeleteDirectory(Path.Combine(bowerPath, "jquery", "src"), true);
-            _fileSystem.DeleteDirectory(Path.Combine(bowerPath, "fingerprintjs2", "flash"), true);
-            _fileSystem.DeleteDirectory(Path.Combine(bowerPath, "fingerprintjs2", "specs"), true);
+            //_fileSystem.DeleteDirectory(Path.Combine(bowerPath, "fingerprintjs2", "flash"), true);
+            //_fileSystem.DeleteDirectory(Path.Combine(bowerPath, "fingerprintjs2", "specs"), true);
 
             DeleteCryptoFiles(Path.Combine(bowerPath, "cryptojslib", "components"));
 
             DeleteFoldersByName(Path.Combine(bowerPath, "jquery"), "src");
             DeleteFoldersByName(Path.Combine(bowerPath, "jstree"), "src");
-            DeleteFoldersByName(Path.Combine(bowerPath, "Sortable"), "meteor");
-            DeleteFoldersByName(Path.Combine(bowerPath, "Sortable"), "st");
-            DeleteFoldersByName(Path.Combine(bowerPath, "Swiper"), "src");
-
-            _fileSystem.DeleteDirectory(Path.Combine(bowerPath, "marked"), true);
-            _fileSystem.DeleteDirectory(Path.Combine(bowerPath, "marked-element"), true);
-            _fileSystem.DeleteDirectory(Path.Combine(bowerPath, "prism"), true);
-            _fileSystem.DeleteDirectory(Path.Combine(bowerPath, "prism-element"), true);
+            //DeleteFoldersByName(Path.Combine(bowerPath, "Sortable"), "meteor");
+            //DeleteFoldersByName(Path.Combine(bowerPath, "Sortable"), "st");
+            //DeleteFoldersByName(Path.Combine(bowerPath, "Swiper"), "src");
            
             if (string.Equals(mode, "cordova", StringComparison.OrdinalIgnoreCase))
             {

+ 3 - 48
MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj

@@ -104,6 +104,9 @@
     <Content Include="dashboard-ui\components\apphost.js">
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
     </Content>
+    <Content Include="dashboard-ui\components\categorysyncbuttons.js">
+      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+    </Content>
     <Content Include="dashboard-ui\components\channelmapper\channelmapper.js">
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
     </Content>
@@ -902,9 +905,6 @@
     <Content Include="dashboard-ui\scripts\favorites.js">
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
     </Content>
-    <Content Include="dashboard-ui\scripts\librarylist.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
     <Content Include="dashboard-ui\scripts\librarymenu.js">
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
     </Content>
@@ -1043,42 +1043,6 @@
     <Content Include="dashboard-ui\userpassword.html">
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
     </Content>
-    <Content Include="dashboard-ui\voice\commands\controlcommands.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\voice\commands\disablecommands.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\voice\commands\enablecommands.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\voice\commands\playcommands.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\voice\commands\searchcommands.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\voice\commands\showcommands.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\voice\commands\togglecommands.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\voice\grammarprocessor.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\voice\voicedialog.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\voice\voice.css">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\voice\voice.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\voice\voicecommands.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
     <Content Include="dashboard-ui\wizardagreement.html">
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
     </Content>
@@ -1674,15 +1638,6 @@
     <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\jqm.popup.js">
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
     </Content>
-    <None Include="dashboard-ui\voice\grammar\en-US.json">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </None>
-    <None Include="dashboard-ui\voice\grammar\grammar.json">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </None>
-    <None Include="dashboard-ui\voice\Readme.md">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </None>
     <None Include="packages.config" />
   </ItemGroup>
   <ItemGroup />