瀏覽代碼

Merge pull request #2080 from MediaBrowser/beta

Beta
Luke 8 年之前
父節點
當前提交
0780889c51
共有 100 個文件被更改,包括 1713 次插入635 次删除
  1. 5 3
      MediaBrowser.Api/ApiEntryPoint.cs
  2. 4 0
      MediaBrowser.Api/BaseApiService.cs
  3. 1 0
      MediaBrowser.Api/IHasDtoOptions.cs
  4. 1 3
      MediaBrowser.Api/Images/ImageService.cs
  5. 27 5
      MediaBrowser.Api/Library/LibraryStructureService.cs
  6. 26 1
      MediaBrowser.Api/LiveTv/LiveTvService.cs
  7. 162 14
      MediaBrowser.Api/Playback/BaseStreamingService.cs
  8. 14 0
      MediaBrowser.Api/Playback/MediaInfoService.cs
  9. 12 4
      MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs
  10. 23 6
      MediaBrowser.Api/Playback/Progressive/ProgressiveStreamWriter.cs
  11. 1 0
      MediaBrowser.Api/Playback/StreamRequest.cs
  12. 13 1
      MediaBrowser.Api/PlaylistService.cs
  13. 13 1
      MediaBrowser.Api/SimilarItemsHelper.cs
  14. 2 2
      MediaBrowser.Api/Sync/SyncHelper.cs
  15. 2 1
      MediaBrowser.Api/Sync/SyncService.cs
  16. 21 33
      MediaBrowser.Api/TvShowsService.cs
  17. 1 0
      MediaBrowser.Api/UserLibrary/BaseItemsByNameService.cs
  18. 3 0
      MediaBrowser.Api/UserLibrary/BaseItemsRequest.cs
  19. 15 19
      MediaBrowser.Api/UserLibrary/ItemsService.cs
  20. 32 2
      MediaBrowser.Api/UserLibrary/UserLibraryService.cs
  21. 4 0
      MediaBrowser.Controller/Dto/DtoOptions.cs
  22. 48 11
      MediaBrowser.Controller/Entities/AggregateFolder.cs
  23. 8 1
      MediaBrowser.Controller/Entities/Audio/Audio.cs
  24. 51 6
      MediaBrowser.Controller/Entities/Audio/MusicArtist.cs
  25. 45 6
      MediaBrowser.Controller/Entities/Audio/MusicGenre.cs
  26. 21 5
      MediaBrowser.Controller/Entities/BaseItem.cs
  27. 1 1
      MediaBrowser.Controller/Entities/Book.cs
  28. 71 0
      MediaBrowser.Controller/Entities/CollectionFolder.cs
  29. 26 7
      MediaBrowser.Controller/Entities/Folder.cs
  30. 1 1
      MediaBrowser.Controller/Entities/Game.cs
  31. 45 5
      MediaBrowser.Controller/Entities/GameGenre.cs
  32. 45 6
      MediaBrowser.Controller/Entities/Genre.cs
  33. 8 2
      MediaBrowser.Controller/Entities/IHasMetadata.cs
  34. 2 0
      MediaBrowser.Controller/Entities/InternalItemsQuery.cs
  35. 35 1
      MediaBrowser.Controller/Entities/Movies/BoxSet.cs
  36. 10 0
      MediaBrowser.Controller/Entities/Movies/Movie.cs
  37. 60 6
      MediaBrowser.Controller/Entities/Person.cs
  38. 1 1
      MediaBrowser.Controller/Entities/Photo.cs
  39. 45 6
      MediaBrowser.Controller/Entities/Studio.cs
  40. 23 38
      MediaBrowser.Controller/Entities/TV/Season.cs
  41. 135 88
      MediaBrowser.Controller/Entities/TV/Series.cs
  42. 10 0
      MediaBrowser.Controller/Entities/Trailer.cs
  43. 39 1
      MediaBrowser.Controller/Entities/UserRootFolder.cs
  44. 17 63
      MediaBrowser.Controller/Entities/UserViewBuilder.cs
  45. 16 11
      MediaBrowser.Controller/Entities/Video.cs
  46. 43 0
      MediaBrowser.Controller/Entities/Year.cs
  47. 2 0
      MediaBrowser.Controller/IServerApplicationPaths.cs
  48. 11 8
      MediaBrowser.Controller/Library/ILibraryManager.cs
  49. 9 0
      MediaBrowser.Controller/Library/ItemResolveArgs.cs
  50. 1 0
      MediaBrowser.Controller/MediaBrowser.Controller.csproj
  51. 18 0
      MediaBrowser.Controller/Net/IAsyncStreamSource.cs
  52. 1 1
      MediaBrowser.Controller/Net/IHttpResultFactory.cs
  53. 6 0
      MediaBrowser.Controller/Persistence/IItemRepository.cs
  54. 12 0
      MediaBrowser.Controller/Playlists/Playlist.cs
  55. 6 1
      MediaBrowser.Dlna/DlnaManager.cs
  56. 39 0
      MediaBrowser.Dlna/PlayTo/Device.cs
  57. 16 8
      MediaBrowser.Dlna/PlayTo/PlayToController.cs
  58. 16 4
      MediaBrowser.Dlna/Profiles/DefaultProfile.cs
  59. 2 0
      MediaBrowser.Dlna/Profiles/DenonAvrProfile.cs
  60. 2 0
      MediaBrowser.Dlna/Profiles/DirectTvProfile.cs
  61. 3 1
      MediaBrowser.Dlna/Profiles/Foobar2000Profile.cs
  62. 2 0
      MediaBrowser.Dlna/Profiles/LgTvProfile.cs
  63. 2 0
      MediaBrowser.Dlna/Profiles/LinksysDMA2100Profile.cs
  64. 2 0
      MediaBrowser.Dlna/Profiles/MediaMonkeyProfile.cs
  65. 2 0
      MediaBrowser.Dlna/Profiles/PopcornHourProfile.cs
  66. 2 0
      MediaBrowser.Dlna/Profiles/SonyBlurayPlayer2013Profile.cs
  67. 7 3
      MediaBrowser.Dlna/Profiles/Xml/Default.xml
  68. 4 1
      MediaBrowser.MediaEncoding/Encoder/AudioEncoder.cs
  69. 20 6
      MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
  70. 4 3
      MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
  71. 3 0
      MediaBrowser.Model.Portable/MediaBrowser.Model.Portable.csproj
  72. 3 0
      MediaBrowser.Model.net35/MediaBrowser.Model.net35.csproj
  73. 13 0
      MediaBrowser.Model/Configuration/LibraryOptions.cs
  74. 2 2
      MediaBrowser.Model/Configuration/ServerConfiguration.cs
  75. 3 2
      MediaBrowser.Model/Dlna/ResolutionNormalizer.cs
  76. 10 3
      MediaBrowser.Model/Dlna/StreamBuilder.cs
  77. 2 0
      MediaBrowser.Model/Dlna/StreamInfo.cs
  78. 12 2
      MediaBrowser.Model/Dto/BaseItemDto.cs
  79. 1 0
      MediaBrowser.Model/Dto/ItemCounts.cs
  80. 1 0
      MediaBrowser.Model/Dto/MediaSourceInfo.cs
  81. 3 0
      MediaBrowser.Model/Entities/VirtualFolderInfo.cs
  82. 6 0
      MediaBrowser.Model/LiveTv/LiveTvChannelQuery.cs
  83. 2 0
      MediaBrowser.Model/LiveTv/ProgramQuery.cs
  84. 1 0
      MediaBrowser.Model/MediaBrowser.Model.csproj
  85. 1 0
      MediaBrowser.Model/Sync/SyncJobQuery.cs
  86. 2 0
      MediaBrowser.Model/Users/UserPolicy.cs
  87. 16 6
      MediaBrowser.Providers/Manager/MetadataService.cs
  88. 6 3
      MediaBrowser.Providers/MediaInfo/FFProbeProvider.cs
  89. 6 3
      MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs
  90. 51 49
      MediaBrowser.Providers/Music/MusicBrainzAlbumProvider.cs
  91. 3 1
      MediaBrowser.Providers/TV/DummySeasonProvider.cs
  92. 6 15
      MediaBrowser.Providers/TV/SeasonMetadataService.cs
  93. 1 1
      MediaBrowser.Server.Implementations/Collections/CollectionsDynamicFolder.cs
  94. 50 114
      MediaBrowser.Server.Implementations/Dto/DtoService.cs
  95. 35 15
      MediaBrowser.Server.Implementations/FileOrganization/EpisodeFileOrganizer.cs
  96. 12 2
      MediaBrowser.Server.Implementations/FileOrganization/FileOrganizationService.cs
  97. 19 16
      MediaBrowser.Server.Implementations/HttpServer/AsyncStreamWriter.cs
  98. 50 0
      MediaBrowser.Server.Implementations/HttpServer/HttpListenerHost.cs
  99. 4 4
      MediaBrowser.Server.Implementations/HttpServer/HttpResultFactory.cs
  100. 12 0
      MediaBrowser.Server.Implementations/IO/FileRefresher.cs

+ 5 - 3
MediaBrowser.Api/ApiEntryPoint.cs

@@ -192,13 +192,13 @@ namespace MediaBrowser.Api
 
                 _activeTranscodingJobs.Add(job);
 
-                ReportTranscodingProgress(job, state, null, null, null, null);
+                ReportTranscodingProgress(job, state, null, null, null, null, null);
 
                 return job;
             }
         }
 
-        public void ReportTranscodingProgress(TranscodingJob job, StreamState state, TimeSpan? transcodingPosition, float? framerate, double? percentComplete, long? bytesTranscoded)
+        public void ReportTranscodingProgress(TranscodingJob job, StreamState state, TimeSpan? transcodingPosition, float? framerate, double? percentComplete, long? bytesTranscoded, int? bitRate)
         {
             var ticks = transcodingPosition.HasValue ? transcodingPosition.Value.Ticks : (long?)null;
 
@@ -208,6 +208,7 @@ namespace MediaBrowser.Api
                 job.CompletionPercentage = percentComplete;
                 job.TranscodingPositionTicks = ticks;
                 job.BytesTranscoded = bytesTranscoded;
+                job.BitRate = bitRate;
             }
 
             var deviceId = state.Request.DeviceId;
@@ -219,7 +220,7 @@ namespace MediaBrowser.Api
 
                 _sessionManager.ReportTranscodingInfo(deviceId, new TranscodingInfo
                 {
-                    Bitrate = state.TotalOutputBitrate,
+                    Bitrate = bitRate ?? state.TotalOutputBitrate,
                     AudioCodec = audioCodec,
                     VideoCodec = videoCodec,
                     Container = state.OutputContainer,
@@ -694,6 +695,7 @@ namespace MediaBrowser.Api
 
         public long? BytesDownloaded { get; set; }
         public long? BytesTranscoded { get; set; }
+        public int? BitRate { get; set; }
 
         public long? TranscodingPositionTicks { get; set; }
         public long? DownloadPositionTicks { get; set; }

+ 4 - 0
MediaBrowser.Api/BaseApiService.cs

@@ -139,6 +139,10 @@ namespace MediaBrowser.Api
                 {
                     options.ImageTypeLimit = hasDtoOptions.ImageTypeLimit.Value;
                 }
+                if (hasDtoOptions.EnableUserData.HasValue)
+                {
+                    options.EnableUserData = hasDtoOptions.EnableUserData.Value;
+                }
 
                 if (!string.IsNullOrWhiteSpace(hasDtoOptions.EnableImageTypes))
                 {

+ 1 - 0
MediaBrowser.Api/IHasDtoOptions.cs

@@ -4,6 +4,7 @@ namespace MediaBrowser.Api
     public interface IHasDtoOptions : IHasItemFields
     {
         bool? EnableImages { get; set; }
+        bool? EnableUserData { get; set; }
 
         int? ImageTypeLimit { get; set; }
 

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

@@ -573,11 +573,9 @@ namespace MediaBrowser.Api.Images
 
             var outputFormats = GetOutputFormats(request, imageInfo, cropwhitespace, supportedImageEnhancers);
 
-            var cacheGuid = new Guid(_imageProcessor.GetImageCacheTag(item, imageInfo, supportedImageEnhancers));
-
             TimeSpan? cacheDuration = null;
 
-            if (!string.IsNullOrEmpty(request.Tag) && cacheGuid == new Guid(request.Tag))
+            if (!string.IsNullOrEmpty(request.Tag))
             {
                 cacheDuration = TimeSpan.FromDays(365);
             }

+ 27 - 5
MediaBrowser.Api/Library/LibraryStructureService.cs

@@ -10,6 +10,9 @@ using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
 using CommonIO;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.Configuration;
 
 namespace MediaBrowser.Api.Library
 {
@@ -52,6 +55,8 @@ namespace MediaBrowser.Api.Library
         /// </summary>
         /// <value>The path.</value>
         public string[] Paths { get; set; }
+
+        public LibraryOptions LibraryOptions { get; set; }
     }
 
     [Route("/Library/VirtualFolders", "DELETE")]
@@ -136,6 +141,14 @@ namespace MediaBrowser.Api.Library
         public bool RefreshLibrary { get; set; }
     }
 
+    [Route("/Library/VirtualFolders/LibraryOptions", "POST")]
+    public class UpdateLibraryOptions : IReturnVoid
+    {
+        public string Id { get; set; }
+
+        public LibraryOptions LibraryOptions { get; set; }
+    }
+
     /// <summary>
     /// Class LibraryStructureService
     /// </summary>
@@ -184,13 +197,22 @@ namespace MediaBrowser.Api.Library
             return ToOptimizedSerializedResultUsingCache(result);
         }
 
+        public void Post(UpdateLibraryOptions request)
+        {
+            var collectionFolder = (CollectionFolder)_libraryManager.GetItemById(request.Id);
+
+            collectionFolder.UpdateLibraryOptions(request.LibraryOptions);
+        }
+
         /// <summary>
         /// Posts the specified request.
         /// </summary>
         /// <param name="request">The request.</param>
         public void Post(AddVirtualFolder request)
         {
-            _libraryManager.AddVirtualFolder(request.Name, request.CollectionType, request.Paths, request.RefreshLibrary);
+            var libraryOptions = request.LibraryOptions ?? new LibraryOptions();
+
+            _libraryManager.AddVirtualFolder(request.Name, request.CollectionType, request.Paths, libraryOptions, request.RefreshLibrary);
         }
 
         /// <summary>
@@ -214,12 +236,12 @@ namespace MediaBrowser.Api.Library
             var currentPath = Path.Combine(rootFolderPath, request.Name);
             var newPath = Path.Combine(rootFolderPath, request.NewName);
 
-			if (!_fileSystem.DirectoryExists(currentPath))
+            if (!_fileSystem.DirectoryExists(currentPath))
             {
                 throw new DirectoryNotFoundException("The media collection does not exist");
             }
 
-			if (!string.Equals(currentPath, newPath, StringComparison.OrdinalIgnoreCase) && _fileSystem.DirectoryExists(newPath))
+            if (!string.Equals(currentPath, newPath, StringComparison.OrdinalIgnoreCase) && _fileSystem.DirectoryExists(newPath))
             {
                 throw new ArgumentException("There is already a media collection with the name " + newPath + ".");
             }
@@ -234,11 +256,11 @@ namespace MediaBrowser.Api.Library
                     //Create an unique name
                     var temporaryName = Guid.NewGuid().ToString();
                     var temporaryPath = Path.Combine(rootFolderPath, temporaryName);
-					_fileSystem.MoveDirectory(currentPath, temporaryPath);
+                    _fileSystem.MoveDirectory(currentPath, temporaryPath);
                     currentPath = temporaryPath;
                 }
 
-				_fileSystem.MoveDirectory(currentPath, newPath);
+                _fileSystem.MoveDirectory(currentPath, newPath);
             }
             finally
             {

+ 26 - 1
MediaBrowser.Api/LiveTv/LiveTvService.cs

@@ -82,6 +82,9 @@ namespace MediaBrowser.Api.LiveTv
         [ApiMember(Name = "AddCurrentProgram", Description = "Optional. Adds current program info to each channel", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
         public bool AddCurrentProgram { get; set; }
 
+        [ApiMember(Name = "EnableUserData", Description = "Optional, include user data", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")]
+        public bool? EnableUserData { get; set; }
+
         public GetChannels()
         {
             AddCurrentProgram = true;
@@ -149,6 +152,9 @@ namespace MediaBrowser.Api.LiveTv
 
         public bool EnableTotalRecordCount { get; set; }
 
+        [ApiMember(Name = "EnableUserData", Description = "Optional, include user data", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")]
+        public bool? EnableUserData { get; set; }
+
         public GetRecordings()
         {
             EnableTotalRecordCount = true;
@@ -271,6 +277,9 @@ namespace MediaBrowser.Api.LiveTv
         [ApiMember(Name = "EnableImageTypes", Description = "Optional. The image types to include in the output.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
         public string EnableImageTypes { get; set; }
 
+        [ApiMember(Name = "EnableUserData", Description = "Optional, include user data", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")]
+        public bool? EnableUserData { get; set; }
+
         /// <summary>
         /// Fields to return within the items, in addition to basic information
         /// </summary>
@@ -331,6 +340,9 @@ namespace MediaBrowser.Api.LiveTv
         /// <value>The fields.</value>
         [ApiMember(Name = "Fields", Description = "Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, CriticRatingSummary, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
         public string Fields { get; set; }
+
+        [ApiMember(Name = "EnableUserData", Description = "Optional, include user data", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")]
+        public bool? EnableUserData { get; set; }
     }
 
     [Route("/LiveTv/Programs/{Id}", "GET", Summary = "Gets a live tv program")]
@@ -726,7 +738,12 @@ namespace MediaBrowser.Api.LiveTv
 
             var user = string.IsNullOrEmpty(request.UserId) ? null : _userManager.GetUserById(request.UserId);
 
-            var returnArray = (await _dtoService.GetBaseItemDtos(channelResult.Items, GetDtoOptions(Request), user).ConfigureAwait(false)).ToArray();
+            var options = GetDtoOptions(request);
+            RemoveFields(options);
+
+            options.AddCurrentProgram = request.AddCurrentProgram;
+
+            var returnArray = (await _dtoService.GetBaseItemDtos(channelResult.Items, options, user).ConfigureAwait(false)).ToArray();
 
             var result = new QueryResult<BaseItemDto>
             {
@@ -737,6 +754,14 @@ namespace MediaBrowser.Api.LiveTv
             return ToOptimizedSerializedResultUsingCache(result);
         }
 
+        private void RemoveFields(DtoOptions options)
+        {
+            options.Fields.Remove(ItemFields.CanDelete);
+            options.Fields.Remove(ItemFields.CanDownload);
+            options.Fields.Remove(ItemFields.DisplayPreferencesId);
+            options.Fields.Remove(ItemFields.Etag);
+        }
+
         public object Get(GetChannel request)
         {
             var user = string.IsNullOrWhiteSpace(request.UserId) ? null : _userManager.GetUserById(request.UserId);

+ 162 - 14
MediaBrowser.Api/Playback/BaseStreamingService.cs

@@ -22,6 +22,8 @@ using System.Text;
 using System.Threading;
 using System.Threading.Tasks;
 using CommonIO;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller;
 
 namespace MediaBrowser.Api.Playback
 {
@@ -69,6 +71,9 @@ namespace MediaBrowser.Api.Playback
         protected IZipClient ZipClient { get; private set; }
         protected IJsonSerializer JsonSerializer { get; private set; }
 
+        public static IServerApplicationHost AppHost;
+        public static IHttpClient HttpClient;
+
         /// <summary>
         /// Initializes a new instance of the <see cref="BaseStreamingService" /> class.
         /// </summary>
@@ -1055,14 +1060,14 @@ namespace MediaBrowser.Api.Playback
             var commandLineLogMessage = process.StartInfo.FileName + " " + process.StartInfo.Arguments;
             Logger.Info(commandLineLogMessage);
 
-            var logFilePrefix = "transcode";
+            var logFilePrefix = "ffmpeg-transcode";
             if (state.VideoRequest != null && string.Equals(state.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase) && string.Equals(state.OutputAudioCodec, "copy", StringComparison.OrdinalIgnoreCase))
             {
-                logFilePrefix = "directstream";
+                logFilePrefix = "ffmpeg-directstream";
             }
             else if (state.VideoRequest != null && string.Equals(state.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase))
             {
-                logFilePrefix = "remux";
+                logFilePrefix = "ffmpeg-remux";
             }
 
             var logFilePath = Path.Combine(ServerConfigurationManager.ApplicationPaths.LogDirectoryPath, logFilePrefix + "-" + Guid.NewGuid() + ".txt");
@@ -1112,28 +1117,30 @@ namespace MediaBrowser.Api.Playback
             }
 
             StartThrottler(state, transcodingJob);
+            ReportUsage(state);
 
             return transcodingJob;
         }
 
         private void StartThrottler(StreamState state, TranscodingJob transcodingJob)
         {
-            if (EnableThrottling(state) && state.InputProtocol == MediaProtocol.File &&
-                           state.RunTimeTicks.HasValue &&
-                           state.VideoType == VideoType.VideoFile &&
-                           !string.Equals(state.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase))
+            if (EnableThrottling(state) && !string.Equals(state.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase))
             {
-                if (state.RunTimeTicks.Value >= TimeSpan.FromMinutes(5).Ticks && state.IsInputVideo)
-                {
-                    transcodingJob.TranscodingThrottler = state.TranscodingThrottler = new TranscodingThrottler(transcodingJob, Logger, ServerConfigurationManager);
-                    state.TranscodingThrottler.Start();
-                }
+                transcodingJob.TranscodingThrottler = state.TranscodingThrottler = new TranscodingThrottler(transcodingJob, Logger, ServerConfigurationManager);
+                state.TranscodingThrottler.Start();
             }
         }
 
         protected virtual bool EnableThrottling(StreamState state)
         {
-            return true;
+            // do not use throttling with hardware encoders
+            return state.InputProtocol == MediaProtocol.File &&
+                state.RunTimeTicks.HasValue &&
+                state.RunTimeTicks.Value >= TimeSpan.FromMinutes(5).Ticks &&
+                state.IsInputVideo &&
+                state.VideoType == VideoType.VideoFile &&
+                !string.Equals(state.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase) &&
+                string.Equals(GetVideoEncoder(state), "libx264", StringComparison.OrdinalIgnoreCase);
         }
 
         private async Task StartStreamingLog(TranscodingJob transcodingJob, StreamState state, Stream source, Stream target)
@@ -1171,6 +1178,7 @@ namespace MediaBrowser.Api.Playback
             double? percent = null;
             TimeSpan? transcodingPosition = null;
             long? bytesTranscoded = null;
+            int? bitRate = null;
 
             var parts = line.Split(' ');
 
@@ -1234,11 +1242,32 @@ namespace MediaBrowser.Api.Playback
                         }
                     }
                 }
+                else if (part.StartsWith("bitrate=", StringComparison.OrdinalIgnoreCase))
+                {
+                    var rate = part.Split(new[] { '=' }, 2).Last();
+
+                    int? scale = null;
+                    if (rate.IndexOf("kbits/s", StringComparison.OrdinalIgnoreCase) != -1)
+                    {
+                        scale = 1024;
+                        rate = rate.Replace("kbits/s", string.Empty, StringComparison.OrdinalIgnoreCase);
+                    }
+
+                    if (scale.HasValue)
+                    {
+                        float val;
+
+                        if (float.TryParse(rate, NumberStyles.Any, UsCulture, out val))
+                        {
+                            bitRate = (int)Math.Ceiling(val * scale.Value);
+                        }
+                    }
+                }
             }
 
             if (framerate.HasValue || percent.HasValue)
             {
-                ApiEntryPoint.Instance.ReportTranscodingProgress(transcodingJob, state, transcodingPosition, framerate, percent, bytesTranscoded);
+                ApiEntryPoint.Instance.ReportTranscodingProgress(transcodingJob, state, transcodingPosition, framerate, percent, bytesTranscoded, bitRate);
             }
         }
 
@@ -1588,6 +1617,10 @@ namespace MediaBrowser.Api.Playback
                         videoRequest.EnableSubtitlesInManifest = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
                     }
                 }
+                else if (i == 29)
+                {
+                    request.Tag = val;
+                }
             }
         }
 
@@ -2192,6 +2225,121 @@ namespace MediaBrowser.Api.Playback
             }
         }
 
+        private async void ReportUsage(StreamState state)
+        {
+            try
+            {
+                await ReportUsageInternal(state).ConfigureAwait(false);
+            }
+            catch
+            {
+
+            }
+        }
+
+        private Task ReportUsageInternal(StreamState state)
+        {
+            if (!ServerConfigurationManager.Configuration.EnableAnonymousUsageReporting)
+            {
+                return Task.FromResult(true);
+            }
+
+            if (!string.Equals(MediaEncoder.EncoderLocationType, "Default", StringComparison.OrdinalIgnoreCase))
+            {
+                return Task.FromResult(true);
+            }
+
+            var dict = new Dictionary<string, string>();
+
+            var outputAudio = GetAudioEncoder(state);
+            if (!string.IsNullOrWhiteSpace(outputAudio))
+            {
+                dict["outputAudio"] = outputAudio;
+            }
+
+            var outputVideo = GetVideoEncoder(state);
+            if (!string.IsNullOrWhiteSpace(outputVideo))
+            {
+                dict["outputVideo"] = outputVideo;
+            }
+
+            if (ServerConfigurationManager.Configuration.CodecsUsed.Contains(outputAudio ?? string.Empty, StringComparer.OrdinalIgnoreCase) &&
+                ServerConfigurationManager.Configuration.CodecsUsed.Contains(outputVideo ?? string.Empty, StringComparer.OrdinalIgnoreCase))
+            {
+                return Task.FromResult(true);
+            }
+
+            dict["id"] = AppHost.SystemId;
+            dict["type"] = state.VideoRequest == null ? "Audio" : "Video";
+
+            var audioStream = state.AudioStream;
+            if (audioStream != null && !string.IsNullOrWhiteSpace(audioStream.Codec))
+            {
+                dict["inputAudio"] = audioStream.Codec;
+            }
+
+            var videoStream = state.VideoStream;
+            if (videoStream != null && !string.IsNullOrWhiteSpace(videoStream.Codec))
+            {
+                dict["inputVideo"] = videoStream.Codec;
+            }
+
+            var cert = GetType().Assembly.GetModules().First().GetSignerCertificate();
+            if (cert != null)
+            {
+                dict["assemblySig"] = cert.GetCertHashString();
+                dict["certSubject"] = cert.Subject ?? string.Empty;
+                dict["certIssuer"] = cert.Issuer ?? string.Empty;
+            }
+            else
+            {
+                return Task.FromResult(true);
+            }
+
+            if (state.SupportedAudioCodecs.Count > 0)
+            {
+                dict["supportedAudioCodecs"] = string.Join(",", state.SupportedAudioCodecs.ToArray());
+            }
+
+            var auth = AuthorizationContext.GetAuthorizationInfo(Request);
+
+            dict["appName"] = auth.Client ?? string.Empty;
+            dict["appVersion"] = auth.Version ?? string.Empty;
+            dict["device"] = auth.Device ?? string.Empty;
+            dict["deviceId"] = auth.DeviceId ?? string.Empty;
+            dict["context"] = "streaming";
+
+            //Logger.Info(JsonSerializer.SerializeToString(dict));
+            if (!ServerConfigurationManager.Configuration.CodecsUsed.Contains(outputAudio ?? string.Empty, StringComparer.OrdinalIgnoreCase))
+            {
+                var list = ServerConfigurationManager.Configuration.CodecsUsed.ToList();
+                list.Add(outputAudio);
+                ServerConfigurationManager.Configuration.CodecsUsed = list.ToArray();
+            }
+
+            if (!ServerConfigurationManager.Configuration.CodecsUsed.Contains(outputVideo ?? string.Empty, StringComparer.OrdinalIgnoreCase))
+            {
+                var list = ServerConfigurationManager.Configuration.CodecsUsed.ToList();
+                list.Add(outputVideo);
+                ServerConfigurationManager.Configuration.CodecsUsed = list.ToArray();
+            }
+
+            ServerConfigurationManager.SaveConfiguration();
+
+            //Logger.Info(JsonSerializer.SerializeToString(dict));
+            var options = new HttpRequestOptions()
+            {
+                Url = "https://mb3admin.com/admin/service/transcoding/report",
+                CancellationToken = CancellationToken.None,
+                LogRequest = false,
+                LogErrors = false
+            };
+            options.RequestContent = JsonSerializer.SerializeToString(dict);
+            options.RequestContentType = "application/json";
+
+            return HttpClient.Post(options);
+        }
+
         /// <summary>
         /// Adds the dlna headers.
         /// </summary>

+ 14 - 0
MediaBrowser.Api/Playback/MediaInfoService.cs

@@ -284,6 +284,13 @@ namespace MediaBrowser.Api.Playback
                         options.ForceDirectPlay = true;
                     }
                 }
+                else if (item is Video)
+                {
+                    if (!user.Policy.EnableAudioPlaybackTranscoding && !user.Policy.EnableVideoPlaybackTranscoding && !user.Policy.EnablePlaybackRemuxing)
+                    {
+                        options.ForceDirectPlay = true;
+                    }
+                }
 
                 // The MediaSource supports direct stream, now test to see if the client supports it
                 var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase) ?
@@ -315,6 +322,13 @@ namespace MediaBrowser.Api.Playback
                         options.ForceDirectStream = true;
                     }
                 }
+                else if (item is Video)
+                {
+                    if (!user.Policy.EnableAudioPlaybackTranscoding && !user.Policy.EnableVideoPlaybackTranscoding && !user.Policy.EnablePlaybackRemuxing)
+                    {
+                        options.ForceDirectStream = true;
+                    }
+                }
 
                 // The MediaSource supports direct stream, now test to see if the client supports it
                 var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase) ?

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

@@ -154,12 +154,20 @@ namespace MediaBrowser.Api.Playback.Progressive
 
                 using (state)
                 {
+                    TimeSpan? cacheDuration = null;
+
+                    if (!string.IsNullOrEmpty(request.Tag))
+                    {
+                        cacheDuration = TimeSpan.FromDays(365);
+                    }
+
                     return await ResultFactory.GetStaticFileResult(Request, new StaticFileResultOptions
                     {
                         ResponseHeaders = responseHeaders,
                         ContentType = contentType,
                         IsHeadRequest = isHeadRequest,
-                        Path = state.MediaPath
+                        Path = state.MediaPath,
+                        CacheDuration = cacheDuration
 
                     }).ConfigureAwait(false);
                 }
@@ -362,9 +370,9 @@ namespace MediaBrowser.Api.Playback.Progressive
                     outputHeaders[item.Key] = item.Value;
                 }
 
-                Func<Stream, Task> streamWriter = stream => new ProgressiveFileCopier(FileSystem, job, Logger).StreamFile(outputPath, stream, CancellationToken.None);
+                var streamSource = new ProgressiveFileCopier(FileSystem, outputPath, outputHeaders, job, Logger, CancellationToken.None);
 
-                return ResultFactory.GetAsyncStreamWriter(streamWriter, outputHeaders);
+                return ResultFactory.GetAsyncStreamWriter(streamSource);
             }
             finally
             {
@@ -383,7 +391,7 @@ namespace MediaBrowser.Api.Playback.Progressive
 
             if (totalBitrate > 0 && state.RunTimeTicks.HasValue)
             {
-                return Convert.ToInt64(totalBitrate * TimeSpan.FromTicks(state.RunTimeTicks.Value).TotalSeconds);
+                return Convert.ToInt64(totalBitrate * TimeSpan.FromTicks(state.RunTimeTicks.Value).TotalSeconds / 8);
             }
 
             return null;

+ 23 - 6
MediaBrowser.Api/Playback/Progressive/ProgressiveStreamWriter.cs

@@ -4,38 +4,55 @@ using System.IO;
 using System.Threading;
 using System.Threading.Tasks;
 using CommonIO;
+using MediaBrowser.Controller.Net;
+using System.Collections.Generic;
+using ServiceStack.Web;
 
 namespace MediaBrowser.Api.Playback.Progressive
 {
-    public class ProgressiveFileCopier
+    public class ProgressiveFileCopier : IAsyncStreamSource, IHasOptions
     {
         private readonly IFileSystem _fileSystem;
         private readonly TranscodingJob _job;
         private readonly ILogger _logger;
+        private readonly string _path;
+        private readonly CancellationToken _cancellationToken;
+        private readonly Dictionary<string, string> _outputHeaders;
 
         // 256k
         private const int BufferSize = 81920;
 
         private long _bytesWritten = 0;
 
-        public ProgressiveFileCopier(IFileSystem fileSystem, TranscodingJob job, ILogger logger)
+        public ProgressiveFileCopier(IFileSystem fileSystem, string path, Dictionary<string, string> outputHeaders, TranscodingJob job, ILogger logger, CancellationToken cancellationToken)
         {
             _fileSystem = fileSystem;
+            _path = path;
+            _outputHeaders = outputHeaders;
             _job = job;
             _logger = logger;
+            _cancellationToken = cancellationToken;
         }
 
-        public async Task StreamFile(string path, Stream outputStream, CancellationToken cancellationToken)
+        public IDictionary<string, string> Options
+        {
+            get
+            {
+                return _outputHeaders;
+            }
+        }
+
+        public async Task WriteToAsync(Stream outputStream)
         {
             try
             {
                 var eofCount = 0;
 
-                using (var fs = _fileSystem.GetFileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, true))
+                using (var fs = _fileSystem.GetFileStream(_path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, true))
                 {
                     while (eofCount < 15)
                     {
-                        var bytesRead = await CopyToAsyncInternal(fs, outputStream, BufferSize, cancellationToken).ConfigureAwait(false);
+                        var bytesRead = await CopyToAsyncInternal(fs, outputStream, BufferSize, _cancellationToken).ConfigureAwait(false);
 
                         //var position = fs.Position;
                         //_logger.Debug("Streamed {0} bytes to position {1} from file {2}", bytesRead, position, path);
@@ -46,7 +63,7 @@ namespace MediaBrowser.Api.Playback.Progressive
                             {
                                 eofCount++;
                             }
-                            await Task.Delay(100, cancellationToken).ConfigureAwait(false);
+                            await Task.Delay(100, _cancellationToken).ConfigureAwait(false);
                         }
                         else
                         {

+ 1 - 0
MediaBrowser.Api/Playback/StreamRequest.cs

@@ -74,6 +74,7 @@ namespace MediaBrowser.Api.Playback
         public string Params { get; set; }
         public string PlaySessionId { get; set; }
         public string LiveStreamId { get; set; }
+        public string Tag { get; set; }
     }
 
     public class VideoStreamRequest : StreamRequest

+ 13 - 1
MediaBrowser.Api/PlaylistService.cs

@@ -72,7 +72,7 @@ namespace MediaBrowser.Api
     }
 
     [Route("/Playlists/{Id}/Items", "GET", Summary = "Gets the original items of a playlist")]
-    public class GetPlaylistItems : IReturn<QueryResult<BaseItemDto>>, IHasItemFields
+    public class GetPlaylistItems : IReturn<QueryResult<BaseItemDto>>, IHasDtoOptions
     {
         [ApiMember(Name = "Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")]
         public string Id { get; set; }
@@ -104,6 +104,18 @@ namespace MediaBrowser.Api
         /// <value>The fields.</value>
         [ApiMember(Name = "Fields", Description = "Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, CriticRatingSummary, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
         public string Fields { get; set; }
+
+        [ApiMember(Name = "EnableImages", Description = "Optional, include image information in output", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")]
+        public bool? EnableImages { get; set; }
+
+        [ApiMember(Name = "EnableUserData", Description = "Optional, include user data", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")]
+        public bool? EnableUserData { get; set; }
+
+        [ApiMember(Name = "ImageTypeLimit", Description = "Optional, the max number of images to return, per image type", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
+        public int? ImageTypeLimit { get; set; }
+
+        [ApiMember(Name = "EnableImageTypes", Description = "Optional. The image types to include in the output.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
+        public string EnableImageTypes { get; set; }
     }
 
     [Authenticated]

+ 13 - 1
MediaBrowser.Api/SimilarItemsHelper.cs

@@ -29,8 +29,20 @@ namespace MediaBrowser.Api
         public string ExcludeArtistIds { get; set; }
     }
 
-    public class BaseGetSimilarItems : IReturn<ItemsResult>, IHasItemFields
+    public class BaseGetSimilarItems : IReturn<ItemsResult>, IHasDtoOptions
     {
+        [ApiMember(Name = "EnableImages", Description = "Optional, include image information in output", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")]
+        public bool? EnableImages { get; set; }
+
+        [ApiMember(Name = "EnableUserData", Description = "Optional, include user data", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")]
+        public bool? EnableUserData { get; set; }
+
+        [ApiMember(Name = "ImageTypeLimit", Description = "Optional, the max number of images to return, per image type", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
+        public int? ImageTypeLimit { get; set; }
+
+        [ApiMember(Name = "EnableImageTypes", Description = "Optional. The image types to include in the output.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
+        public string EnableImageTypes { get; set; }
+
         /// <summary>
         /// Gets or sets the user id.
         /// </summary>

+ 2 - 2
MediaBrowser.Api/Sync/SyncHelper.cs

@@ -24,7 +24,7 @@ namespace MediaBrowser.Api.Sync
                         }
                         break;
                     }
-                    if (item.IsFolder && !item.IsMusicGenre && !item.IsArtist && !item.IsType("musicalbum") && !item.IsGameGenre)
+                    if (item.IsFolderItem && !item.IsMusicGenre && !item.IsArtist && !item.IsType("musicalbum") && !item.IsGameGenre)
                     {
                         options.Add(SyncJobOption.Quality);
                         options.Add(SyncJobOption.Profile);
@@ -44,7 +44,7 @@ namespace MediaBrowser.Api.Sync
             {
                 if (item.SupportsSync ?? false)
                 {
-                    if (item.IsFolder || item.IsGameGenre || item.IsMusicGenre || item.IsGenre || item.IsArtist || item.IsStudio || item.IsPerson)
+                    if (item.IsFolderItem || item.IsGameGenre || item.IsMusicGenre || item.IsGenre || item.IsArtist || item.IsStudio || item.IsPerson)
                     {
                         options.Add(SyncJobOption.SyncNewContent);
                         options.Add(SyncJobOption.ItemLimit);

+ 2 - 1
MediaBrowser.Api/Sync/SyncService.cs

@@ -66,6 +66,7 @@ namespace MediaBrowser.Api.Sync
         public string Id { get; set; }
     }
 
+    [Route("/Sync/Items/Cancel", "POST", Summary = "Cancels items from a sync target")]
     [Route("/Sync/{TargetId}/Items", "DELETE", Summary = "Cancels items from a sync target")]
     public class CancelItems : IReturnVoid
     {
@@ -211,7 +212,7 @@ namespace MediaBrowser.Api.Sync
             return ToOptimizedResult(result);
         }
 
-        public void Delete(CancelItems request)
+        public void Any(CancelItems request)
         {
             var itemIds = request.ItemIds.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
 

+ 21 - 33
MediaBrowser.Api/TvShowsService.cs

@@ -69,6 +69,9 @@ namespace MediaBrowser.Api
 
         [ApiMember(Name = "EnableImageTypes", Description = "Optional. The image types to include in the output.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
         public string EnableImageTypes { get; set; }
+
+        [ApiMember(Name = "EnableUserData", Description = "Optional, include user data", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")]
+        public bool? EnableUserData { get; set; }
     }
 
     [Route("/Shows/Upcoming", "GET", Summary = "Gets a list of upcoming episodes")]
@@ -117,6 +120,9 @@ namespace MediaBrowser.Api
 
         [ApiMember(Name = "EnableImageTypes", Description = "Optional. The image types to include in the output.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
         public string EnableImageTypes { get; set; }
+
+        [ApiMember(Name = "EnableUserData", Description = "Optional, include user data", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")]
+        public bool? EnableUserData { get; set; }
     }
 
     [Route("/Shows/{Id}/Similar", "GET", Summary = "Finds tv shows similar to a given one.")]
@@ -184,6 +190,10 @@ namespace MediaBrowser.Api
 
         [ApiMember(Name = "EnableImageTypes", Description = "Optional. The image types to include in the output.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
         public string EnableImageTypes { get; set; }
+
+        [ApiMember(Name = "EnableUserData", Description = "Optional, include user data", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")]
+        public bool? EnableUserData { get; set; }
+
     }
 
     [Route("/Shows/{Id}/Seasons", "GET", Summary = "Gets seasons for a tv series")]
@@ -226,6 +236,10 @@ namespace MediaBrowser.Api
 
         [ApiMember(Name = "EnableImageTypes", Description = "Optional. The image types to include in the output.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
         public string EnableImageTypes { get; set; }
+
+        [ApiMember(Name = "EnableUserData", Description = "Optional, include user data", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")]
+        public bool? EnableUserData { get; set; }
+
     }
 
     /// <summary>
@@ -409,23 +423,14 @@ namespace MediaBrowser.Api
                 throw new ResourceNotFoundException("No series exists with Id " + request.Id);
             }
 
-            var seasons = series.GetSeasons(user);
-
-            if (request.IsSpecialSeason.HasValue)
+            var seasons = (await series.GetItems(new InternalItemsQuery(user)
             {
-                var val = request.IsSpecialSeason.Value;
+                IsMissing = request.IsMissing,
+                IsVirtualUnaired = request.IsVirtualUnaired,
+                IsSpecialSeason = request.IsSpecialSeason,
+                AdjacentTo = request.AdjacentTo
 
-                seasons = seasons.Where(i => i.IsSpecialSeason == val);
-            }
-
-            seasons = FilterVirtualSeasons(request, seasons);
-
-            // This must be the last filter
-            if (!string.IsNullOrEmpty(request.AdjacentTo))
-            {
-                seasons = UserViewBuilder.FilterForAdjacency(seasons, request.AdjacentTo)
-                    .Cast<Season>();
-            }
+            }).ConfigureAwait(false)).Items.OfType<Season>();
 
             var dtoOptions = GetDtoOptions(request);
 
@@ -439,23 +444,6 @@ namespace MediaBrowser.Api
             };
         }
 
-        private IEnumerable<Season> FilterVirtualSeasons(GetSeasons request, IEnumerable<Season> items)
-        {
-            if (request.IsMissing.HasValue)
-            {
-                var val = request.IsMissing.Value;
-                items = items.Where(i => (i.IsMissingSeason) == val);
-            }
-
-            if (request.IsVirtualUnaired.HasValue)
-            {
-                var val = request.IsVirtualUnaired.Value;
-                items = items.Where(i => i.IsVirtualUnaired == val);
-            }
-
-            return items;
-        }
-
         public async Task<object> Get(GetEpisodes request)
         {
             var user = _userManager.GetUserById(request.UserId);
@@ -490,7 +478,7 @@ namespace MediaBrowser.Api
                 }
                 else
                 {
-                    episodes = series.GetEpisodes(user, season);
+                    episodes = series.GetSeasonEpisodes(user, season);
                 }
             }
             else

+ 1 - 0
MediaBrowser.Api/UserLibrary/BaseItemsByNameService.cs

@@ -214,6 +214,7 @@ namespace MediaBrowser.Api.UserLibrary
             dto.AlbumCount = counts.AlbumCount;
             dto.SongCount = counts.SongCount;
             dto.GameCount = counts.GameCount;
+            dto.ArtistCount = counts.ArtistCount;
         }
 
         /// <summary>

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

@@ -226,6 +226,9 @@ namespace MediaBrowser.Api.UserLibrary
         [ApiMember(Name = "EnableImages", Description = "Optional, include image information in output", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")]
         public bool? EnableImages { get; set; }
 
+        [ApiMember(Name = "EnableUserData", Description = "Optional, include user data", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")]
+        public bool? EnableUserData { get; set; }
+
         [ApiMember(Name = "ImageTypeLimit", Description = "Optional, the max number of images to return, per image type", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
         public int? ImageTypeLimit { get; set; }
 

+ 15 - 19
MediaBrowser.Api/UserLibrary/ItemsService.cs

@@ -149,39 +149,35 @@ namespace MediaBrowser.Api.UserLibrary
                 item = user == null ? _libraryManager.RootFolder : user.RootFolder;
             }
 
-            // Default list type = children
-
-            var folder = item as Folder;
-            if (folder == null)
-            {
-                folder = user == null ? _libraryManager.RootFolder : _libraryManager.GetUserRootFolder();
-            }
-
             if (!string.IsNullOrEmpty(request.Ids))
             {
-                request.Recursive = true;
                 var query = GetItemsQuery(request, user);
-                var result = await folder.GetItems(query).ConfigureAwait(false);
-
-                if (string.IsNullOrWhiteSpace(request.SortBy))
+                var specificItems = _libraryManager.GetItemList(query).ToArray();
+                if (query.SortBy.Length == 0)
                 {
                     var ids = query.ItemIds.ToList();
 
                     // Try to preserve order
-                    result.Items = result.Items.OrderBy(i => ids.IndexOf(i.Id.ToString("N"))).ToArray();
+                    specificItems = specificItems.OrderBy(i => ids.IndexOf(i.Id.ToString("N"))).ToArray();
                 }
-
-                return result;
+                return new QueryResult<BaseItem>
+                {
+                    Items = specificItems.ToArray(),
+                    TotalRecordCount = specificItems.Length
+                };
             }
 
-            if (request.Recursive)
+            // Default list type = children
+
+            var folder = item as Folder;
+            if (folder == null)
             {
-                return await folder.GetItems(GetItemsQuery(request, user)).ConfigureAwait(false);
+                folder = user == null ? _libraryManager.RootFolder : _libraryManager.GetUserRootFolder();
             }
 
-            if (user == null)
+            if (request.Recursive || !string.IsNullOrEmpty(request.Ids) || user == null)
             {
-                return await folder.GetItems(GetItemsQuery(request, null)).ConfigureAwait(false);
+                return await folder.GetItems(GetItemsQuery(request, user)).ConfigureAwait(false);
             }
 
             var userRoot = item as UserRootFolder;

+ 32 - 2
MediaBrowser.Api/UserLibrary/UserLibraryService.cs

@@ -12,6 +12,8 @@ using System.Collections.Generic;
 using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
+using CommonIO;
+using MediaBrowser.Controller.Providers;
 
 namespace MediaBrowser.Api.UserLibrary
 {
@@ -244,6 +246,9 @@ namespace MediaBrowser.Api.UserLibrary
         [ApiMember(Name = "EnableImageTypes", Description = "Optional. The image types to include in the output.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
         public string EnableImageTypes { get; set; }
 
+        [ApiMember(Name = "EnableUserData", Description = "Optional, include user data", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")]
+        public bool? EnableUserData { get; set; }
+
         public GetLatestMedia()
         {
             Limit = 20;
@@ -262,14 +267,16 @@ namespace MediaBrowser.Api.UserLibrary
         private readonly ILibraryManager _libraryManager;
         private readonly IDtoService _dtoService;
         private readonly IUserViewManager _userViewManager;
+        private readonly IFileSystem _fileSystem;
 
-        public UserLibraryService(IUserManager userManager, ILibraryManager libraryManager, IUserDataManager userDataRepository, IDtoService dtoService, IUserViewManager userViewManager)
+        public UserLibraryService(IUserManager userManager, ILibraryManager libraryManager, IUserDataManager userDataRepository, IDtoService dtoService, IUserViewManager userViewManager, IFileSystem fileSystem)
         {
             _userManager = userManager;
             _libraryManager = libraryManager;
             _userDataRepository = userDataRepository;
             _dtoService = dtoService;
             _userViewManager = userViewManager;
+            _fileSystem = fileSystem;
         }
 
         /// <summary>
@@ -426,12 +433,14 @@ namespace MediaBrowser.Api.UserLibrary
         /// </summary>
         /// <param name="request">The request.</param>
         /// <returns>System.Object.</returns>
-        public object Get(GetItem request)
+        public async Task<object> Get(GetItem request)
         {
             var user = _userManager.GetUserById(request.UserId);
 
             var item = string.IsNullOrEmpty(request.Id) ? user.RootFolder : _libraryManager.GetItemById(request.Id);
 
+            await RefreshItemOnDemandIfNeeded(item).ConfigureAwait(false);
+
             var dtoOptions = GetDtoOptions(request);
 
             var result = _dtoService.GetBaseItemDto(item, dtoOptions, user);
@@ -439,6 +448,27 @@ namespace MediaBrowser.Api.UserLibrary
             return ToOptimizedSerializedResultUsingCache(result);
         }
 
+        private async Task RefreshItemOnDemandIfNeeded(BaseItem item)
+        {
+            if (item is Person)
+            {
+                var hasMetdata = !string.IsNullOrWhiteSpace(item.Overview) && item.HasImage(ImageType.Primary);
+                var performFullRefresh = !hasMetdata && (DateTime.UtcNow - item.DateLastRefreshed).TotalDays >= 3;
+
+                if (!hasMetdata)
+                {
+                    var options = new MetadataRefreshOptions(_fileSystem)
+                    {
+                        MetadataRefreshMode = MetadataRefreshMode.FullRefresh,
+                        ImageRefreshMode = ImageRefreshMode.FullRefresh,
+                        ForceSave = performFullRefresh
+                    };
+
+                    await item.RefreshMetadata(options, CancellationToken.None).ConfigureAwait(false);
+                }
+            }
+        }
+
         /// <summary>
         /// Gets the specified request.
         /// </summary>

+ 4 - 0
MediaBrowser.Controller/Dto/DtoOptions.cs

@@ -19,12 +19,16 @@ namespace MediaBrowser.Controller.Dto
         public bool EnableImages { get; set; }
         public bool AddProgramRecordingInfo { get; set; }
         public string DeviceId { get; set; }
+        public bool EnableUserData { get; set; }
+        public bool AddCurrentProgram { get; set; }
 
         public DtoOptions()
         {
             Fields = new List<ItemFields>();
             ImageTypeLimit = int.MaxValue;
             EnableImages = true;
+            EnableUserData = true;
+            AddCurrentProgram = true;
 
             Fields = Enum.GetNames(typeof (ItemFields))
                     .Select(i => (ItemFields) Enum.Parse(typeof (ItemFields), i, true))

+ 48 - 11
MediaBrowser.Controller/Entities/AggregateFolder.cs

@@ -5,6 +5,8 @@ using System.Collections.Concurrent;
 using System.Collections.Generic;
 using System.Linq;
 using System.Runtime.Serialization;
+using System.Threading;
+using System.Threading.Tasks;
 using CommonIO;
 using MediaBrowser.Controller.Providers;
 
@@ -67,6 +69,31 @@ namespace MediaBrowser.Controller.Entities
             return CreateResolveArgs(directoryService, true).FileSystemChildren;
         }
 
+        private List<Guid> _childrenIds = null;
+        private readonly object _childIdsLock = new object();
+        protected override IEnumerable<BaseItem> LoadChildren()
+        {
+            lock (_childIdsLock)
+            {
+                if (_childrenIds == null || _childrenIds.Count == 0)
+                {
+                    var list = base.LoadChildren().ToList();
+                    _childrenIds = list.Select(i => i.Id).ToList();
+                    return list;
+                }
+
+                return _childrenIds.Select(LibraryManager.GetItemById).Where(i => i != null).ToList();
+            }
+        }
+
+        private void ClearCache()
+        {
+            lock (_childIdsLock)
+            {
+                _childrenIds = null;
+            }
+        }
+
         private bool _requiresRefresh;
         public override bool RequiresRefresh()
         {
@@ -89,6 +116,8 @@ namespace MediaBrowser.Controller.Entities
 
         public override bool BeforeMetadataRefresh()
         {
+            ClearCache();
+
             var changed = base.BeforeMetadataRefresh() || _requiresRefresh;
             _requiresRefresh = false;
             return changed;
@@ -96,9 +125,11 @@ namespace MediaBrowser.Controller.Entities
 
         private ItemResolveArgs CreateResolveArgs(IDirectoryService directoryService, bool setPhysicalLocations)
         {
+            ClearCache();
+
             var path = ContainingFolderPath;
 
-            var args = new ItemResolveArgs(ConfigurationManager.ApplicationPaths , directoryService)
+            var args = new ItemResolveArgs(ConfigurationManager.ApplicationPaths, directoryService)
             {
                 FileInfo = FileSystem.GetDirectoryInfo(path),
                 Path = path,
@@ -135,7 +166,22 @@ namespace MediaBrowser.Controller.Entities
 
             return args;
         }
-        
+
+        protected override IEnumerable<BaseItem> GetNonCachedChildren(IDirectoryService directoryService)
+        {
+            return base.GetNonCachedChildren(directoryService).Concat(_virtualChildren);
+        }
+
+        protected override async Task ValidateChildrenInternal(IProgress<double> progress, CancellationToken cancellationToken, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService)
+        {
+            ClearCache();
+
+            await base.ValidateChildrenInternal(progress, cancellationToken, recursive, refreshChildMetadata, refreshOptions, directoryService)
+                .ConfigureAwait(false);
+
+            ClearCache();
+        }
+
         /// <summary>
         /// Adds the virtual child.
         /// </summary>
@@ -151,15 +197,6 @@ namespace MediaBrowser.Controller.Entities
             _virtualChildren.Add(child);
         }
 
-        /// <summary>
-        /// Get the children of this folder from the actual file system
-        /// </summary>
-        /// <returns>IEnumerable{BaseItem}.</returns>
-        protected override IEnumerable<BaseItem> GetNonCachedChildren(IDirectoryService directoryService)
-        {
-            return base.GetNonCachedChildren(directoryService).Concat(_virtualChildren);
-        }
-
         /// <summary>
         /// Finds the virtual child.
         /// </summary>

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

@@ -5,9 +5,11 @@ using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.MediaInfo;
 using System;
 using System.Collections.Generic;
+using System.Globalization;
 using System.Linq;
 using System.Runtime.Serialization;
 using System.Threading;
+using MediaBrowser.Common.Extensions;
 using MediaBrowser.Controller.Channels;
 
 namespace MediaBrowser.Controller.Entities.Audio
@@ -47,7 +49,7 @@ namespace MediaBrowser.Controller.Entities.Audio
         }
 
         [IgnoreDataMember]
-        public override bool EnableForceSaveOnDateModifiedChange
+        public override bool EnableRefreshOnDateModifiedChange
         {
             get { return true; }
         }
@@ -266,6 +268,11 @@ namespace MediaBrowser.Controller.Entities.Audio
                 Size = i.Size
             };
 
+            if (info.Protocol == MediaProtocol.File)
+            {
+                info.ETag = i.DateModified.Ticks.ToString(CultureInfo.InvariantCulture).GetMD5().ToString("N");
+            }
+
             if (string.IsNullOrEmpty(info.Container))
             {
                 if (!string.IsNullOrWhiteSpace(i.Path) && locationType != LocationType.Remote && locationType != LocationType.Virtual)

+ 51 - 6
MediaBrowser.Controller/Entities/Audio/MusicArtist.cs

@@ -169,13 +169,9 @@ namespace MediaBrowser.Controller.Entities.Audio
             list.Add("Artist-" + (item.Name ?? string.Empty).RemoveDiacritics());
             return list;
         }
-
-        public override string PresentationUniqueKey
+        public override string CreatePresentationUniqueKey()
         {
-            get
-            {
-                return "Artist-" + (Name ?? string.Empty).RemoveDiacritics();
-            }
+            return "Artist-" + (Name ?? string.Empty).RemoveDiacritics();
         }
         protected override bool GetBlockUnratedValue(UserPolicy config)
         {
@@ -274,5 +270,54 @@ namespace MediaBrowser.Controller.Entities.Audio
                 return false;
             }
         }
+
+        public static string GetPath(string name, bool normalizeName = true)
+        {
+            // Trim the period at the end because windows will have a hard time with that
+            var validName = normalizeName ?
+                FileSystem.GetValidFilename(name).Trim().TrimEnd('.') :
+                name;
+
+            return System.IO.Path.Combine(ConfigurationManager.ApplicationPaths.ArtistsPath, validName);
+        }
+
+        private string GetRebasedPath()
+        {
+            return GetPath(System.IO.Path.GetFileName(Path), false);
+        }
+
+        public override bool RequiresRefresh()
+        {
+            if (IsAccessedByName)
+            {
+                var newPath = GetRebasedPath();
+                if (!string.Equals(Path, newPath, StringComparison.Ordinal))
+                {
+                    Logger.Debug("{0} path has changed from {1} to {2}", GetType().Name, Path, newPath);
+                    return true;
+                }
+            }
+            return base.RequiresRefresh();
+        }
+
+        /// <summary>
+        /// This is called before any metadata refresh and returns true or false indicating if changes were made
+        /// </summary>
+        public override bool BeforeMetadataRefresh()
+        {
+            var hasChanges = base.BeforeMetadataRefresh();
+
+            if (IsAccessedByName)
+            {
+                var newPath = GetRebasedPath();
+                if (!string.Equals(Path, newPath, StringComparison.Ordinal))
+                {
+                    Path = newPath;
+                    hasChanges = true;
+                }
+            }
+
+            return hasChanges;
+        }
     }
 }

+ 45 - 6
MediaBrowser.Controller/Entities/Audio/MusicGenre.cs

@@ -18,13 +18,9 @@ namespace MediaBrowser.Controller.Entities.Audio
             list.Insert(0, GetType().Name + "-" + (Name ?? string.Empty).RemoveDiacritics());
             return list;
         }
-
-        public override string PresentationUniqueKey
+        public override string CreatePresentationUniqueKey()
         {
-            get
-            {
-                return GetUserDataKeys()[0];
-            }
+            return GetUserDataKeys()[0];
         }
 
         [IgnoreDataMember]
@@ -96,5 +92,48 @@ namespace MediaBrowser.Controller.Entities.Audio
 
             return LibraryManager.GetItemList(query);
         }
+
+        public static string GetPath(string name, bool normalizeName = true)
+        {
+            // Trim the period at the end because windows will have a hard time with that
+            var validName = normalizeName ?
+                FileSystem.GetValidFilename(name).Trim().TrimEnd('.') :
+                name;
+
+            return System.IO.Path.Combine(ConfigurationManager.ApplicationPaths.MusicGenrePath, validName);
+        }
+
+        private string GetRebasedPath()
+        {
+            return GetPath(System.IO.Path.GetFileName(Path), false);
+        }
+
+        public override bool RequiresRefresh()
+        {
+            var newPath = GetRebasedPath();
+            if (!string.Equals(Path, newPath, StringComparison.Ordinal))
+            {
+                Logger.Debug("{0} path has changed from {1} to {2}", GetType().Name, Path, newPath);
+                return true;
+            }
+            return base.RequiresRefresh();
+        }
+
+        /// <summary>
+        /// This is called before any metadata refresh and returns true or false indicating if changes were made
+        /// </summary>
+        public override bool BeforeMetadataRefresh()
+        {
+            var hasChanges = base.BeforeMetadataRefresh();
+
+            var newPath = GetRebasedPath();
+            if (!string.Equals(Path, newPath, StringComparison.Ordinal))
+            {
+                Path = newPath;
+                hasChanges = true;
+            }
+
+            return hasChanges;
+        }
     }
 }

+ 21 - 5
MediaBrowser.Controller/Entities/BaseItem.cs

@@ -455,7 +455,7 @@ namespace MediaBrowser.Controller.Entities
         public DateTime DateLastRefreshed { get; set; }
 
         [IgnoreDataMember]
-        public virtual bool EnableForceSaveOnDateModifiedChange
+        public virtual bool EnableRefreshOnDateModifiedChange
         {
             get { return false; }
         }
@@ -951,7 +951,7 @@ namespace MediaBrowser.Controller.Entities
                 .Where(i => !i.IsDirectory && string.Equals(FileSystem.GetFileNameWithoutExtension(i), ThemeSongFilename, StringComparison.OrdinalIgnoreCase))
                 );
 
-            return LibraryManager.ResolvePaths(files, directoryService, null)
+            return LibraryManager.ResolvePaths(files, directoryService, null, new LibraryOptions())
                 .OfType<Audio.Audio>()
                 .Select(audio =>
                 {
@@ -981,7 +981,7 @@ namespace MediaBrowser.Controller.Entities
                 .Where(i => string.Equals(i.Name, ThemeVideosFolderName, StringComparison.OrdinalIgnoreCase))
                 .SelectMany(i => directoryService.GetFiles(i.FullName));
 
-            return LibraryManager.ResolvePaths(files, directoryService, null)
+            return LibraryManager.ResolvePaths(files, directoryService, null, new LibraryOptions())
                 .OfType<Video>()
                 .Select(item =>
                 {
@@ -1194,10 +1194,17 @@ namespace MediaBrowser.Controller.Entities
             get { return null; }
         }
 
+        public virtual string CreatePresentationUniqueKey()
+        {
+            return Id.ToString("N");
+        }
+
         [IgnoreDataMember]
-        public virtual string PresentationUniqueKey
+        public string PresentationUniqueKey { get; set; }
+
+        public string GetPresentationUniqueKey()
         {
-            get { return Id.ToString("N"); }
+            return PresentationUniqueKey ?? CreatePresentationUniqueKey();
         }
 
         public virtual bool RequiresRefresh()
@@ -2206,6 +2213,15 @@ namespace MediaBrowser.Controller.Entities
             }
         }
 
+        [IgnoreDataMember]
+        public virtual bool StopRefreshIfLocalMetadataFound
+        {
+            get
+            {
+                return true;
+            }
+        }
+
         public virtual IEnumerable<Guid> GetIdsForAncestorQuery()
         {
             return new[] { Id };

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

@@ -35,7 +35,7 @@ namespace MediaBrowser.Controller.Entities
         }
 
         [IgnoreDataMember]
-        public override bool EnableForceSaveOnDateModifiedChange
+        public override bool EnableRefreshOnDateModifiedChange
         {
             get { return true; }
         }

+ 71 - 0
MediaBrowser.Controller/Entities/CollectionFolder.cs

@@ -3,11 +3,15 @@ using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Providers;
 using System;
 using System.Collections.Generic;
+using System.IO;
 using System.Linq;
 using System.Runtime.Serialization;
 using System.Threading;
 using System.Threading.Tasks;
 using CommonIO;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.Serialization;
 using MoreLinq;
 
 namespace MediaBrowser.Controller.Entities
@@ -18,6 +22,8 @@ namespace MediaBrowser.Controller.Entities
     /// </summary>
     public class CollectionFolder : Folder, ICollectionFolder
     {
+        public static IXmlSerializer XmlSerializer { get; set; }
+
         public CollectionFolder()
         {
             PhysicalLocationsList = new List<string>();
@@ -39,6 +45,71 @@ namespace MediaBrowser.Controller.Entities
 
         public string CollectionType { get; set; }
 
+        private static readonly Dictionary<string, LibraryOptions> LibraryOptions = new Dictionary<string, LibraryOptions>();
+        public LibraryOptions GetLibraryOptions()
+        {
+            lock (LibraryOptions)
+            {
+                LibraryOptions options;
+                if (!LibraryOptions.TryGetValue(Path, out options))
+                {
+                    options = LoadLibraryOptions();
+                    LibraryOptions[Path] = options;
+                }
+
+                return options;
+            }
+        }
+
+        private LibraryOptions LoadLibraryOptions()
+        {
+            try
+            {
+                var result = XmlSerializer.DeserializeFromFile(typeof(LibraryOptions), GetLibraryOptionsPath(Path)) as LibraryOptions;
+
+                if (result == null)
+                {
+                    return new LibraryOptions();
+                }
+
+                return result;
+            }
+            catch (FileNotFoundException)
+            {
+                return new LibraryOptions();
+            }
+            catch (DirectoryNotFoundException)
+            {
+                return new LibraryOptions();
+            }
+            catch (Exception ex)
+            {
+                Logger.ErrorException("Error loading library options", ex);
+
+                return new LibraryOptions();
+            }
+        }
+
+        private static string GetLibraryOptionsPath(string path)
+        {
+            return System.IO.Path.Combine(path, "options.xml");
+        }
+
+        public void UpdateLibraryOptions(LibraryOptions options)
+        {
+            SaveLibraryOptions(Path, options);
+        }
+
+        public static void SaveLibraryOptions(string path, LibraryOptions options)
+        {
+            lock (LibraryOptions)
+            {
+                LibraryOptions[path] = options;
+
+                XmlSerializer.SerializeToFile(options, GetLibraryOptionsPath(path));
+            }
+        }
+
         /// <summary>
         /// Allow different display preferences for each collection folder
         /// </summary>

+ 26 - 7
MediaBrowser.Controller/Entities/Folder.cs

@@ -13,6 +13,7 @@ using System.Threading;
 using System.Threading.Tasks;
 using CommonIO;
 using MediaBrowser.Controller.Channels;
+using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Model.Channels;
 
@@ -273,6 +274,7 @@ namespace MediaBrowser.Controller.Entities
         /// </summary>
         protected virtual IEnumerable<BaseItem> LoadChildren()
         {
+            //Logger.Debug("Loading children from {0} {1} {2}", GetType().Name, Id, Path);
             //just load our children from the repo - the library will be validated and maintained in other processes
             return GetCachedChildren();
         }
@@ -643,8 +645,9 @@ namespace MediaBrowser.Controller.Entities
         protected virtual IEnumerable<BaseItem> GetNonCachedChildren(IDirectoryService directoryService)
         {
             var collectionType = LibraryManager.GetContentType(this);
+            var libraryOptions = LibraryManager.GetLibraryOptions(this);
 
-            return LibraryManager.ResolvePaths(GetFileSystemChildren(directoryService), directoryService, this, collectionType);
+            return LibraryManager.ResolvePaths(GetFileSystemChildren(directoryService), directoryService, this, libraryOptions, collectionType);
         }
 
         /// <summary>
@@ -699,7 +702,7 @@ namespace MediaBrowser.Controller.Entities
                     items = GetRecursiveChildren(user, query);
                 }
 
-                return PostFilterAndSort(items, query);
+                return PostFilterAndSort(items, query, true, true);
             }
 
             if (!(this is UserRootFolder) && !(this is AggregateFolder))
@@ -900,7 +903,15 @@ namespace MediaBrowser.Controller.Entities
             if (query.ItemIds.Length > 0)
             {
                 var specificItems = query.ItemIds.Select(LibraryManager.GetItemById).Where(i => i != null).ToList();
-                return Task.FromResult(PostFilterAndSort(specificItems, query));
+
+                if (query.SortBy.Length == 0)
+                {
+                    var ids = query.ItemIds.ToList();
+
+                    // Try to preserve order
+                    specificItems = specificItems.OrderBy(i => ids.IndexOf(i.Id.ToString("N"))).ToList();
+                }
+                return Task.FromResult(PostFilterAndSort(specificItems, query, true, true));
             }
 
             return GetItemsInternal(query);
@@ -956,12 +967,12 @@ namespace MediaBrowser.Controller.Entities
                    : GetChildren(user, true).Where(filter);
             }
 
-            return PostFilterAndSort(items, query);
+            return PostFilterAndSort(items, query, true, true);
         }
 
-        protected QueryResult<BaseItem> PostFilterAndSort(IEnumerable<BaseItem> items, InternalItemsQuery query)
+        protected QueryResult<BaseItem> PostFilterAndSort(IEnumerable<BaseItem> items, InternalItemsQuery query, bool collapseBoxSetItems, bool enableSorting)
         {
-            return UserViewBuilder.PostFilterAndSort(items, this, null, query, LibraryManager, ConfigurationManager);
+            return UserViewBuilder.PostFilterAndSort(items, this, null, query, LibraryManager, ConfigurationManager, collapseBoxSetItems, enableSorting);
         }
 
         public virtual IEnumerable<BaseItem> GetChildren(User user, bool includeLinkedChildren)
@@ -1425,7 +1436,7 @@ namespace MediaBrowser.Controller.Entities
                 itemDto.RecursiveItemCount = allItemsQueryResult.TotalRecordCount;
             }
 
-            double recursiveItemCount = allItemsQueryResult.TotalRecordCount;
+            var recursiveItemCount = allItemsQueryResult.TotalRecordCount;
             double unplayedCount = unplayedQueryResult.TotalRecordCount;
 
             if (recursiveItemCount > 0)
@@ -1435,6 +1446,14 @@ namespace MediaBrowser.Controller.Entities
                 dto.Played = dto.PlayedPercentage.Value >= 100;
                 dto.UnplayedItemCount = unplayedQueryResult.TotalRecordCount;
             }
+
+            if (itemDto != null)
+            {
+                if (this is Season || this is MusicAlbum)
+                {
+                    itemDto.ChildCount = recursiveItemCount;
+                }
+            }
         }
     }
 }

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

@@ -34,7 +34,7 @@ namespace MediaBrowser.Controller.Entities
         }
 
         [IgnoreDataMember]
-        public override bool EnableForceSaveOnDateModifiedChange
+        public override bool EnableRefreshOnDateModifiedChange
         {
             get { return true; }
         }

+ 45 - 5
MediaBrowser.Controller/Entities/GameGenre.cs

@@ -16,12 +16,9 @@ namespace MediaBrowser.Controller.Entities
             return list;
         }
 
-        public override string PresentationUniqueKey
+        public override string CreatePresentationUniqueKey()
         {
-            get
-            {
-                return GetUserDataKeys()[0];
-            }
+            return GetUserDataKeys()[0];
         }
 
         /// <summary>
@@ -87,5 +84,48 @@ namespace MediaBrowser.Controller.Entities
                 return false;
             }
         }
+
+        public static string GetPath(string name, bool normalizeName = true)
+        {
+            // Trim the period at the end because windows will have a hard time with that
+            var validName = normalizeName ?
+                FileSystem.GetValidFilename(name).Trim().TrimEnd('.') :
+                name;
+
+            return System.IO.Path.Combine(ConfigurationManager.ApplicationPaths.GameGenrePath, validName);
+        }
+
+        private string GetRebasedPath()
+        {
+            return GetPath(System.IO.Path.GetFileName(Path), false);
+        }
+
+        public override bool RequiresRefresh()
+        {
+            var newPath = GetRebasedPath();
+            if (!string.Equals(Path, newPath, StringComparison.Ordinal))
+            {
+                Logger.Debug("{0} path has changed from {1} to {2}", GetType().Name, Path, newPath);
+                return true;
+            }
+            return base.RequiresRefresh();
+        }
+
+        /// <summary>
+        /// This is called before any metadata refresh and returns true or false indicating if changes were made
+        /// </summary>
+        public override bool BeforeMetadataRefresh()
+        {
+            var hasChanges = base.BeforeMetadataRefresh();
+
+            var newPath = GetRebasedPath();
+            if (!string.Equals(Path, newPath, StringComparison.Ordinal))
+            {
+                Path = newPath;
+                hasChanges = true;
+            }
+
+            return hasChanges;
+        }
     }
 }

+ 45 - 6
MediaBrowser.Controller/Entities/Genre.cs

@@ -19,13 +19,9 @@ namespace MediaBrowser.Controller.Entities
             list.Insert(0, GetType().Name + "-" + (Name ?? string.Empty).RemoveDiacritics());
             return list;
         }
-
-        public override string PresentationUniqueKey
+        public override string CreatePresentationUniqueKey()
         {
-            get
-            {
-                return GetUserDataKeys()[0];
-            }
+            return GetUserDataKeys()[0];
         }
 
         /// <summary>
@@ -91,5 +87,48 @@ namespace MediaBrowser.Controller.Entities
                 return false;
             }
         }
+
+        public static string GetPath(string name, bool normalizeName = true)
+        {
+            // Trim the period at the end because windows will have a hard time with that
+            var validName = normalizeName ?
+                FileSystem.GetValidFilename(name).Trim().TrimEnd('.') :
+                name;
+
+            return System.IO.Path.Combine(ConfigurationManager.ApplicationPaths.GenrePath, validName);
+        }
+
+        private string GetRebasedPath()
+        {
+            return GetPath(System.IO.Path.GetFileName(Path), false);
+        }
+
+        public override bool RequiresRefresh()
+        {
+            var newPath = GetRebasedPath();
+            if (!string.Equals(Path, newPath, StringComparison.Ordinal))
+            {
+                Logger.Debug("{0} path has changed from {1} to {2}", GetType().Name, Path, newPath);
+                return true;
+            }
+            return base.RequiresRefresh();
+        }
+
+        /// <summary>
+        /// This is called before any metadata refresh and returns true or false indicating if changes were made
+        /// </summary>
+        public override bool BeforeMetadataRefresh()
+        {
+            var hasChanges = base.BeforeMetadataRefresh();
+
+            var newPath = GetRebasedPath();
+            if (!string.Equals(Path, newPath, StringComparison.Ordinal))
+            {
+                Path = newPath;
+                hasChanges = true;
+            }
+
+            return hasChanges;
+        }
     }
 }

+ 8 - 2
MediaBrowser.Controller/Entities/IHasMetadata.cs

@@ -32,7 +32,7 @@ namespace MediaBrowser.Controller.Entities
         /// </summary>
         /// <value>The date last refreshed.</value>
         DateTime DateLastRefreshed { get; set; }
-        
+
         /// <summary>
         /// This is called before any metadata refresh and returns true or false indicating if changes were made
         /// </summary>
@@ -52,6 +52,12 @@ namespace MediaBrowser.Controller.Entities
 
         bool RequiresRefresh();
 
-        bool EnableForceSaveOnDateModifiedChange { get; }
+        bool EnableRefreshOnDateModifiedChange { get; }
+
+        string PresentationUniqueKey { get; set; }
+
+        string GetPresentationUniqueKey();
+        string CreatePresentationUniqueKey();
+        bool StopRefreshIfLocalMetadataFound { get; }
     }
 }

+ 2 - 0
MediaBrowser.Controller/Entities/InternalItemsQuery.cs

@@ -37,6 +37,7 @@ namespace MediaBrowser.Controller.Entities
         public string[] Genres { get; set; }
         public string[] Keywords { get; set; }
 
+        public bool? IsSpecialSeason { get; set; }
         public bool? IsMissing { get; set; }
         public bool? IsUnaired { get; set; }
         public bool? IsVirtualUnaired { get; set; }
@@ -50,6 +51,7 @@ namespace MediaBrowser.Controller.Entities
 
         public string PresentationUniqueKey { get; set; }
         public string Path { get; set; }
+        public string PathNotStartsWith { get; set; }
         public string Name { get; set; }
         public string SlugName { get; set; }
 

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

@@ -62,6 +62,26 @@ namespace MediaBrowser.Controller.Entities.Movies
             return UnratedItem.Movie;
         }
 
+        protected override IEnumerable<BaseItem> GetNonCachedChildren(IDirectoryService directoryService)
+        {
+            if (IsLegacyBoxSet)
+            {
+                return base.GetNonCachedChildren(directoryService);
+            }
+            return new List<BaseItem>();
+        }
+
+        protected override IEnumerable<BaseItem> LoadChildren()
+        {
+            if (IsLegacyBoxSet)
+            {
+                return base.LoadChildren();
+            }
+
+            // Save a trip to the database
+            return new List<BaseItem>();
+        }
+
         [IgnoreDataMember]
         public override bool IsPreSorted
         {
@@ -76,7 +96,21 @@ namespace MediaBrowser.Controller.Entities.Movies
         {
             get
             {
-                return true;
+                if (IsLegacyBoxSet)
+                {
+                    return true;
+                }
+
+                return false;
+            }
+        }
+
+        [IgnoreDataMember]
+        private bool IsLegacyBoxSet
+        {
+            get
+            {
+                return !FileSystem.ContainsSubPath(ConfigurationManager.ApplicationPaths.DataPath, Path);
             }
         }
 

+ 10 - 0
MediaBrowser.Controller/Entities/Movies/Movie.cs

@@ -179,5 +179,15 @@ namespace MediaBrowser.Controller.Entities.Movies
 
             return list;
         }
+
+        [IgnoreDataMember]
+        public override bool StopRefreshIfLocalMetadataFound
+        {
+            get
+            {
+                // Need people id's from internet metadata
+                return false;
+            }
+        }
     }
 }

+ 60 - 6
MediaBrowser.Controller/Entities/Person.cs

@@ -26,13 +26,9 @@ namespace MediaBrowser.Controller.Entities
             list.Insert(0, GetType().Name + "-" + (Name ?? string.Empty).RemoveDiacritics());
             return list;
         }
-
-        public override string PresentationUniqueKey
+        public override string CreatePresentationUniqueKey()
         {
-            get
-            {
-                return GetUserDataKeys()[0];
-            }
+            return GetUserDataKeys()[0];
         }
 
         public PersonLookupInfo GetLookupInfo()
@@ -126,6 +122,64 @@ namespace MediaBrowser.Controller.Entities
                 return false;
             }
         }
+
+        public static string GetPath(string name, bool normalizeName = true)
+        {
+            // Trim the period at the end because windows will have a hard time with that
+            var validFilename = normalizeName ?
+                FileSystem.GetValidFilename(name).Trim().TrimEnd('.') :
+                name;
+
+            string subFolderPrefix = null;
+
+            foreach (char c in validFilename)
+            {
+                if (char.IsLetterOrDigit(c))
+                {
+                    subFolderPrefix = c.ToString();
+                    break;
+                }
+            }
+
+            var path = ConfigurationManager.ApplicationPaths.PeoplePath;
+
+            return string.IsNullOrEmpty(subFolderPrefix) ?
+                System.IO.Path.Combine(path, validFilename) :
+                System.IO.Path.Combine(path, subFolderPrefix, validFilename);
+        }
+
+        private string GetRebasedPath()
+        {
+            return GetPath(System.IO.Path.GetFileName(Path), false);
+        }
+
+        public override bool RequiresRefresh()
+        {
+            var newPath = GetRebasedPath();
+            if (!string.Equals(Path, newPath, StringComparison.Ordinal))
+            {
+                Logger.Debug("{0} path has changed from {1} to {2}", GetType().Name, Path, newPath);
+                return true;
+            }
+            return base.RequiresRefresh();
+        }
+
+        /// <summary>
+        /// This is called before any metadata refresh and returns true or false indicating if changes were made
+        /// </summary>
+        public override bool BeforeMetadataRefresh()
+        {
+            var hasChanges = base.BeforeMetadataRefresh();
+
+            var newPath = GetRebasedPath();
+            if (!string.Equals(Path, newPath, StringComparison.Ordinal))
+            {
+                Path = newPath;
+                hasChanges = true;
+            }
+
+            return hasChanges;
+        }
     }
 
     /// <summary>

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

@@ -52,7 +52,7 @@ namespace MediaBrowser.Controller.Entities
         }
 
         [IgnoreDataMember]
-        public override bool EnableForceSaveOnDateModifiedChange
+        public override bool EnableRefreshOnDateModifiedChange
         {
             get { return true; }
         }

+ 45 - 6
MediaBrowser.Controller/Entities/Studio.cs

@@ -18,13 +18,9 @@ namespace MediaBrowser.Controller.Entities
             list.Insert(0, GetType().Name + "-" + (Name ?? string.Empty).RemoveDiacritics());
             return list;
         }
-
-        public override string PresentationUniqueKey
+        public override string CreatePresentationUniqueKey()
         {
-            get
-            {
-                return GetUserDataKeys()[0];
-            }
+            return GetUserDataKeys()[0];
         }
 
         /// <summary>
@@ -89,5 +85,48 @@ namespace MediaBrowser.Controller.Entities
                 return false;
             }
         }
+
+        public static string GetPath(string name, bool normalizeName = true)
+        {
+            // Trim the period at the end because windows will have a hard time with that
+            var validName = normalizeName ?
+                FileSystem.GetValidFilename(name).Trim().TrimEnd('.') :
+                name;
+
+            return System.IO.Path.Combine(ConfigurationManager.ApplicationPaths.StudioPath, validName);
+        }
+
+        private string GetRebasedPath()
+        {
+            return GetPath(System.IO.Path.GetFileName(Path), false);
+        }
+
+        public override bool RequiresRefresh()
+        {
+            var newPath = GetRebasedPath();
+            if (!string.Equals(Path, newPath, StringComparison.Ordinal))
+            {
+                Logger.Debug("{0} path has changed from {1} to {2}", GetType().Name, Path, newPath);
+                return true;
+            }
+            return base.RequiresRefresh();
+        }
+
+        /// <summary>
+        /// This is called before any metadata refresh and returns true or false indicating if changes were made
+        /// </summary>
+        public override bool BeforeMetadataRefresh()
+        {
+            var hasChanges = base.BeforeMetadataRefresh();
+
+            var newPath = GetRebasedPath();
+            if (!string.Equals(Path, newPath, StringComparison.Ordinal))
+            {
+                Path = newPath;
+                hasChanges = true;
+            }
+
+            return hasChanges;
+        }
     }
 }

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

@@ -85,7 +85,11 @@ namespace MediaBrowser.Controller.Entities.TV
 
         public override int GetChildCount(User user)
         {
-            return GetChildren(user, true).Count();
+            Logger.Debug("Season {0} getting child cound", (Path ?? Name));
+            var result = GetChildren(user, true).Count();
+            Logger.Debug("Season {0} child cound: ", result);
+
+            return result;
         }
 
         /// <summary>
@@ -114,22 +118,18 @@ namespace MediaBrowser.Controller.Entities.TV
             }
         }
 
-        [IgnoreDataMember]
-        public override string PresentationUniqueKey
+        public override string CreatePresentationUniqueKey()
         {
-            get
+            if (IndexNumber.HasValue)
             {
-                if (IndexNumber.HasValue)
+                var series = Series;
+                if (series != null)
                 {
-                    var series = Series;
-                    if (series != null)
-                    {
-                        return series.PresentationUniqueKey + "-" + (IndexNumber ?? 0).ToString("000");
-                    }
+                    return series.PresentationUniqueKey + "-" + (IndexNumber ?? 0).ToString("000");
                 }
-
-                return base.PresentationUniqueKey;
             }
+
+            return base.CreatePresentationUniqueKey();
         }
 
         /// <summary>
@@ -141,24 +141,6 @@ namespace MediaBrowser.Controller.Entities.TV
             return IndexNumber != null ? IndexNumber.Value.ToString("0000") : Name;
         }
 
-        [IgnoreDataMember]
-        public bool IsMissingSeason
-        {
-            get { return (IsVirtualItem) && !IsUnaired; }
-        }
-
-        [IgnoreDataMember]
-        public bool IsVirtualUnaired
-        {
-            get { return (IsVirtualItem) && IsUnaired; }
-        }
-
-        [IgnoreDataMember]
-        public bool IsSpecialSeason
-        {
-            get { return (IndexNumber ?? -1) == 0; }
-        }
-
         protected override Task<QueryResult<BaseItem>> GetItemsInternal(InternalItemsQuery query)
         {
             if (query.User == null)
@@ -170,10 +152,15 @@ namespace MediaBrowser.Controller.Entities.TV
 
             Func<BaseItem, bool> filter = i => UserViewBuilder.Filter(i, user, query, UserDataManager, LibraryManager);
 
+            var id = Guid.NewGuid().ToString("N");
+
+            Logger.Debug("Season.GetItemsInternal entering GetEpisodes. Request id: " + id);
             var items = GetEpisodes(user).Where(filter);
 
-            var result = PostFilterAndSort(items, query);
+            Logger.Debug("Season.GetItemsInternal entering PostFilterAndSort. Request id: " + id);
+            var result = PostFilterAndSort(items, query, false, false);
 
+            Logger.Debug("Season.GetItemsInternal complete. Request id: " + id);
             return Task.FromResult(result);
         }
 
@@ -184,19 +171,17 @@ namespace MediaBrowser.Controller.Entities.TV
         /// <returns>IEnumerable{Episode}.</returns>
         public IEnumerable<Episode> GetEpisodes(User user)
         {
-            var config = user.Configuration;
-
-            return GetEpisodes(Series, user, config.DisplayMissingEpisodes, config.DisplayUnairedEpisodes);
+            return GetEpisodes(Series, user);
         }
 
-        public IEnumerable<Episode> GetEpisodes(Series series, User user, bool includeMissingEpisodes, bool includeVirtualUnairedEpisodes)
+        public IEnumerable<Episode> GetEpisodes(Series series, User user)
         {
-            return GetEpisodes(series, user, includeMissingEpisodes, includeVirtualUnairedEpisodes, null);
+            return GetEpisodes(series, user, null);
         }
 
-        public IEnumerable<Episode> GetEpisodes(Series series, User user, bool includeMissingEpisodes, bool includeVirtualUnairedEpisodes, IEnumerable<Episode> allSeriesEpisodes)
+        public IEnumerable<Episode> GetEpisodes(Series series, User user, IEnumerable<Episode> allSeriesEpisodes)
         {
-            return series.GetEpisodes(user, this, includeMissingEpisodes, includeVirtualUnairedEpisodes, allSeriesEpisodes);
+            return series.GetSeasonEpisodes(user, this, allSeriesEpisodes);
         }
 
         public IEnumerable<Episode> GetEpisodes()

+ 135 - 88
MediaBrowser.Controller/Entities/TV/Series.cs

@@ -96,19 +96,29 @@ namespace MediaBrowser.Controller.Entities.TV
             }
         }
 
-        [IgnoreDataMember]
-        public override string PresentationUniqueKey
+        public override string CreatePresentationUniqueKey()
         {
-            get
+            var userdatakeys = GetUserDataKeys();
+
+            if (userdatakeys.Count > 1)
             {
-                var userdatakeys = GetUserDataKeys();
+                return AddLibrariesToPresentationUniqueKey(userdatakeys[0]);
+            }
+            return base.CreatePresentationUniqueKey();
+        }
 
-                if (userdatakeys.Count > 1)
-                {
-                    return userdatakeys[0];
-                }
-                return base.PresentationUniqueKey;
+        private string AddLibrariesToPresentationUniqueKey(string key)
+        {
+            var folders = LibraryManager.GetCollectionFolders(this)
+                .Select(i => i.Id.ToString("N"))
+                .ToArray();
+
+            if (folders.Length == 0)
+            {
+                return key;
             }
+
+            return key + "-" + string.Join("-", folders);
         }
 
         private static string GetUniqueSeriesKey(BaseItem series)
@@ -117,7 +127,7 @@ namespace MediaBrowser.Controller.Entities.TV
             {
                 return series.Id.ToString("N");
             }
-            return series.PresentationUniqueKey;
+            return series.GetPresentationUniqueKey();
         }
 
         public override int GetChildCount(User user)
@@ -197,7 +207,30 @@ namespace MediaBrowser.Controller.Entities.TV
         {
             var config = user.Configuration;
 
-            return GetSeasons(user, config.DisplayMissingEpisodes, config.DisplayUnairedEpisodes);
+            var seriesKey = GetUniqueSeriesKey(this);
+
+            Logger.Debug("GetSeasons SeriesKey: {0}", seriesKey);
+            var query = new InternalItemsQuery(user)
+            {
+                AncestorWithPresentationUniqueKey = seriesKey,
+                IncludeItemTypes = new[] {typeof (Season).Name},
+                SortBy = new[] {ItemSortBy.SortName}
+            };
+
+            if (!config.DisplayMissingEpisodes && !config.DisplayUnairedEpisodes)
+            {
+                query.IsVirtualItem = false;
+            }
+            else if (!config.DisplayMissingEpisodes)
+            {
+                query.IsMissing = false;
+            }
+            else if (!config.DisplayUnairedEpisodes)
+            {
+                query.IsVirtualUnaired = false;
+            }
+
+            return LibraryManager.GetItemList(query).Cast<Season>();
         }
 
         protected override Task<QueryResult<BaseItem>> GetItemsInternal(InternalItemsQuery query)
@@ -227,55 +260,43 @@ namespace MediaBrowser.Controller.Entities.TV
             Func<BaseItem, bool> filter = i => UserViewBuilder.Filter(i, user, query, UserDataManager, LibraryManager);
 
             var items = GetSeasons(user).Where(filter);
-            var result = PostFilterAndSort(items, query);
+            var result = PostFilterAndSort(items, query, false, true);
             return Task.FromResult(result);
         }
 
-        public IEnumerable<Season> GetSeasons(User user, bool includeMissingSeasons, bool includeVirtualUnaired)
+        public IEnumerable<Episode> GetEpisodes(User user)
         {
-            IEnumerable<Season> seasons;
+            var seriesKey = GetUniqueSeriesKey(this);
+            Logger.Debug("GetEpisodes seriesKey: {0}", seriesKey);
 
-            seasons = LibraryManager.GetItemList(new InternalItemsQuery(user)
+            var query = new InternalItemsQuery(user)
             {
-                AncestorWithPresentationUniqueKey = GetUniqueSeriesKey(this),
-                IncludeItemTypes = new[] { typeof(Season).Name },
-                SortBy = new[] { ItemSortBy.SortName }
-
-            }).Cast<Season>();
-
-            if (!includeMissingSeasons)
+                AncestorWithPresentationUniqueKey = seriesKey,
+                IncludeItemTypes = new[] {typeof (Episode).Name, typeof (Season).Name},
+                SortBy = new[] {ItemSortBy.SortName}
+            };
+            var config = user.Configuration;
+            if (!config.DisplayMissingEpisodes && !config.DisplayUnairedEpisodes)
             {
-                seasons = seasons.Where(i => !(i.IsMissingSeason));
+                query.IsVirtualItem = false;
             }
-            if (!includeVirtualUnaired)
+            else if (!config.DisplayMissingEpisodes)
             {
-                seasons = seasons.Where(i => !i.IsVirtualUnaired);
+                query.IsMissing = false;
             }
-
-            return seasons;
-        }
-
-        public IEnumerable<Episode> GetEpisodes(User user)
-        {
-            var config = user.Configuration;
-
-            return GetEpisodes(user, config.DisplayMissingEpisodes, config.DisplayUnairedEpisodes);
-        }
-
-        public IEnumerable<Episode> GetEpisodes(User user, bool includeMissing, bool includeVirtualUnaired)
-        {
-            var allItems = LibraryManager.GetItemList(new InternalItemsQuery(user)
+            else if (!config.DisplayUnairedEpisodes)
             {
-                AncestorWithPresentationUniqueKey = GetUniqueSeriesKey(this),
-                IncludeItemTypes = new[] { typeof(Episode).Name, typeof(Season).Name },
-                SortBy = new[] { ItemSortBy.SortName }
+                query.IsVirtualUnaired = false;
+            }
 
-            }).ToList();
+            var allItems = LibraryManager.GetItemList(query).ToList();
+
+            Logger.Debug("GetEpisodes return {0} items from database", allItems.Count);
 
             var allSeriesEpisodes = allItems.OfType<Episode>().ToList();
 
             var allEpisodes = allItems.OfType<Season>()
-                .SelectMany(i => i.GetEpisodes(this, user, includeMissing, includeVirtualUnaired, allSeriesEpisodes))
+                .SelectMany(i => i.GetEpisodes(this, user, allSeriesEpisodes))
                 .Reverse()
                 .ToList();
 
@@ -352,78 +373,68 @@ namespace MediaBrowser.Controller.Entities.TV
             progress.Report(100);
         }
 
-        public IEnumerable<Episode> GetEpisodes(User user, Season season)
-        {
-            var config = user.Configuration;
-
-            return GetEpisodes(user, season, config.DisplayMissingEpisodes, config.DisplayUnairedEpisodes);
-        }
-
         private IEnumerable<Episode> GetAllEpisodes(User user)
         {
-            return LibraryManager.GetItemList(new InternalItemsQuery(user)
+            Logger.Debug("Series.GetAllEpisodes entering GetItemList");
+
+            var result =  LibraryManager.GetItemList(new InternalItemsQuery(user)
             {
                 AncestorWithPresentationUniqueKey = GetUniqueSeriesKey(this),
                 IncludeItemTypes = new[] { typeof(Episode).Name },
                 SortBy = new[] { ItemSortBy.SortName }
 
-            }).Cast<Episode>();
-        }
+            }).Cast<Episode>().ToList();
 
-        public IEnumerable<Episode> GetEpisodes(User user, Season parentSeason, bool includeMissingEpisodes, bool includeVirtualUnairedEpisodes)
-        {
-            IEnumerable<Episode> episodes = GetAllEpisodes(user);
+            Logger.Debug("Series.GetAllEpisodes returning {0} episodes", result.Count);
 
-            return GetEpisodes(user, parentSeason, includeMissingEpisodes, includeVirtualUnairedEpisodes, episodes);
+            return result;
         }
 
-        public IEnumerable<Episode> GetEpisodes(User user, Season parentSeason, bool includeMissingEpisodes, bool includeVirtualUnairedEpisodes, IEnumerable<Episode> allSeriesEpisodes)
+        public IEnumerable<Episode> GetSeasonEpisodes(User user, Season parentSeason)
         {
-            if (allSeriesEpisodes == null)
+            var seriesKey = GetUniqueSeriesKey(this);
+            Logger.Debug("GetSeasonEpisodes seriesKey: {0}", seriesKey);
+
+            var query = new InternalItemsQuery(user)
             {
-                return GetEpisodes(user, parentSeason, includeMissingEpisodes, includeVirtualUnairedEpisodes);
+                AncestorWithPresentationUniqueKey = seriesKey,
+                IncludeItemTypes = new[] { typeof(Episode).Name },
+                SortBy = new[] { ItemSortBy.SortName }
+            };
+            var config = user.Configuration;
+            if (!config.DisplayMissingEpisodes && !config.DisplayUnairedEpisodes)
+            {
+                query.IsVirtualItem = false;
             }
-
-            var episodes = FilterEpisodesBySeason(allSeriesEpisodes, parentSeason, ConfigurationManager.Configuration.DisplaySpecialsWithinSeasons);
-
-            if (!includeMissingEpisodes)
+            else if (!config.DisplayMissingEpisodes)
             {
-                episodes = episodes.Where(i => !i.IsMissingEpisode);
+                query.IsMissing = false;
             }
-            if (!includeVirtualUnairedEpisodes)
+            else if (!config.DisplayUnairedEpisodes)
             {
-                episodes = episodes.Where(i => !i.IsVirtualUnaired);
+                query.IsVirtualUnaired = false;
             }
 
-            var sortBy = (parentSeason.IndexNumber ?? -1) == 0 ? ItemSortBy.SortName : ItemSortBy.AiredEpisodeOrder;
+            var allItems = LibraryManager.GetItemList(query).OfType<Episode>();
 
-            return LibraryManager.Sort(episodes, user, new[] { sortBy }, SortOrder.Ascending)
-                .Cast<Episode>();
+            return GetSeasonEpisodes(user, parentSeason, allItems);
         }
 
-        /// <summary>
-        /// Filters the episodes by season.
-        /// </summary>
-        public static IEnumerable<Episode> FilterEpisodesBySeason(IEnumerable<Episode> episodes, int seasonNumber, bool includeSpecials)
+        public IEnumerable<Episode> GetSeasonEpisodes(User user, Season parentSeason, IEnumerable<Episode> allSeriesEpisodes)
         {
-            if (!includeSpecials || seasonNumber < 1)
+            if (allSeriesEpisodes == null)
             {
-                return episodes.Where(i => (i.ParentIndexNumber ?? -1) == seasonNumber);
+                Logger.Debug("GetSeasonEpisodes allSeriesEpisodes is null");
+                return GetSeasonEpisodes(user, parentSeason);
             }
 
-            return episodes.Where(i =>
-            {
-                var episode = i;
-
-                if (episode != null)
-                {
-                    var currentSeasonNumber = episode.AiredSeasonNumber;
+            Logger.Debug("GetSeasonEpisodes FilterEpisodesBySeason");
+            var episodes = FilterEpisodesBySeason(allSeriesEpisodes, parentSeason, ConfigurationManager.Configuration.DisplaySpecialsWithinSeasons);
 
-                    return currentSeasonNumber.HasValue && currentSeasonNumber.Value == seasonNumber;
-                }
+            var sortBy = (parentSeason.IndexNumber ?? -1) == 0 ? ItemSortBy.SortName : ItemSortBy.AiredEpisodeOrder;
 
-                return false;
-            });
+            return LibraryManager.Sort(episodes, user, new[] { sortBy }, SortOrder.Ascending)
+                .Cast<Episode>();
         }
 
         /// <summary>
@@ -454,6 +465,32 @@ namespace MediaBrowser.Controller.Entities.TV
             });
         }
 
+        /// <summary>
+        /// Filters the episodes by season.
+        /// </summary>
+        public static IEnumerable<Episode> FilterEpisodesBySeason(IEnumerable<Episode> episodes, int seasonNumber, bool includeSpecials)
+        {
+            if (!includeSpecials || seasonNumber < 1)
+            {
+                return episodes.Where(i => (i.ParentIndexNumber ?? -1) == seasonNumber);
+            }
+
+            return episodes.Where(i =>
+            {
+                var episode = i;
+
+                if (episode != null)
+                {
+                    var currentSeasonNumber = episode.AiredSeasonNumber;
+
+                    return currentSeasonNumber.HasValue && currentSeasonNumber.Value == seasonNumber;
+                }
+
+                return false;
+            });
+        }
+
+
         protected override bool GetBlockUnratedValue(UserPolicy config)
         {
             return config.BlockUnratedItems.Contains(UnratedItem.Series);
@@ -509,5 +546,15 @@ namespace MediaBrowser.Controller.Entities.TV
 
             return list;
         }
+
+        [IgnoreDataMember]
+        public override bool StopRefreshIfLocalMetadataFound
+        {
+            get
+            {
+                // Need people id's from internet metadata
+                return false;
+            }
+        }
     }
 }

+ 10 - 0
MediaBrowser.Controller/Entities/Trailer.cs

@@ -124,5 +124,15 @@ namespace MediaBrowser.Controller.Entities
 
             return list;
         }
+
+        [IgnoreDataMember]
+        public override bool StopRefreshIfLocalMetadataFound
+        {
+            get
+            {
+                // Need people id's from internet metadata
+                return false;
+            }
+        }
     }
 }

+ 39 - 1
MediaBrowser.Controller/Entities/UserRootFolder.cs

@@ -16,6 +16,31 @@ namespace MediaBrowser.Controller.Entities
     /// </summary>
     public class UserRootFolder : Folder
     {
+        private List<Guid> _childrenIds = null;
+        private readonly object _childIdsLock = new object();
+        protected override IEnumerable<BaseItem> LoadChildren()
+        {
+            lock (_childIdsLock)
+            {
+                if (_childrenIds == null)
+                {
+                    var list = base.LoadChildren().ToList();
+                    _childrenIds = list.Select(i => i.Id).ToList();
+                    return list;
+                }
+
+                return _childrenIds.Select(LibraryManager.GetItemById).Where(i => i != null).ToList();
+            }
+        }
+
+        private void ClearCache()
+        {
+            lock (_childIdsLock)
+            {
+                _childrenIds = null;
+            }
+        }
+
         protected override async Task<QueryResult<BaseItem>> GetItemsInternal(InternalItemsQuery query)
         {
             if (query.Recursive)
@@ -33,7 +58,7 @@ namespace MediaBrowser.Controller.Entities
             var user = query.User;
             Func<BaseItem, bool> filter = i => UserViewBuilder.Filter(i, user, query, UserDataManager, LibraryManager);
             
-            return PostFilterAndSort(result.Where(filter), query);
+            return PostFilterAndSort(result.Where(filter), query, true, true);
         }
 
         public override int GetChildCount(User user)
@@ -69,6 +94,8 @@ namespace MediaBrowser.Controller.Entities
 
         public override bool BeforeMetadataRefresh()
         {
+            ClearCache();
+
             var hasChanges = base.BeforeMetadataRefresh();
 
             if (string.Equals("default", Name, StringComparison.OrdinalIgnoreCase))
@@ -80,11 +107,22 @@ namespace MediaBrowser.Controller.Entities
             return hasChanges;
         }
 
+        protected override IEnumerable<BaseItem> GetNonCachedChildren(IDirectoryService directoryService)
+        {
+            ClearCache();
+
+            return base.GetNonCachedChildren(directoryService);
+        }
+
         protected override async Task ValidateChildrenInternal(IProgress<double> progress, CancellationToken cancellationToken, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService)
         {
+            ClearCache();
+
             await base.ValidateChildrenInternal(progress, cancellationToken, recursive, refreshChildMetadata, refreshOptions, directoryService)
                 .ConfigureAwait(false);
 
+            ClearCache();
+
             // Not the best way to handle this, but it solves an issue
             // CollectionFolders aren't always getting saved after changes
             // This means that grabbing the item by Id may end up returning the old one

+ 17 - 63
MediaBrowser.Controller/Entities/UserViewBuilder.cs

@@ -424,7 +424,7 @@ namespace MediaBrowser.Controller.Entities
 
             query.SortBy = new string[] { };
 
-            return PostFilterAndSort(items, parent, null, query);
+            return PostFilterAndSort(items, parent, null, query, false, true);
         }
 
         private QueryResult<BaseItem> GetFavoriteSongs(Folder parent, User user, InternalItemsQuery query)
@@ -780,7 +780,7 @@ namespace MediaBrowser.Controller.Entities
         {
             items = items.Where(i => Filter(i, query.User, query, _userDataManager, _libraryManager));
 
-            return PostFilterAndSort(items, queryParent, null, query, _libraryManager, _config);
+            return PostFilterAndSort(items, queryParent, null, query, _libraryManager, _config, true, true);
         }
 
         public static bool FilterItem(BaseItem item, InternalItemsQuery query)
@@ -791,9 +791,11 @@ namespace MediaBrowser.Controller.Entities
         private QueryResult<BaseItem> PostFilterAndSort(IEnumerable<BaseItem> items,
             BaseItem queryParent,
             int? totalRecordLimit,
-            InternalItemsQuery query)
+            InternalItemsQuery query,
+            bool collapseBoxSetItems,
+            bool enableSorting)
         {
-            return PostFilterAndSort(items, queryParent, totalRecordLimit, query, _libraryManager, _config);
+            return PostFilterAndSort(items, queryParent, totalRecordLimit, query, _libraryManager, _config, collapseBoxSetItems, enableSorting);
         }
 
         public static QueryResult<BaseItem> PostFilterAndSort(IEnumerable<BaseItem> items,
@@ -801,7 +803,9 @@ namespace MediaBrowser.Controller.Entities
             int? totalRecordLimit,
             InternalItemsQuery query,
             ILibraryManager libraryManager,
-            IServerConfigurationManager configurationManager)
+            IServerConfigurationManager configurationManager,
+            bool collapseBoxSetItems,
+            bool enableSorting)
         {
             var user = query.User;
 
@@ -810,7 +814,10 @@ namespace MediaBrowser.Controller.Entities
                 query.IsVirtualUnaired,
                 query.IsUnaired);
 
-            items = CollapseBoxSetItemsIfNeeded(items, query, queryParent, user, configurationManager);
+            if (collapseBoxSetItems)
+            {
+                items = CollapseBoxSetItemsIfNeeded(items, query, queryParent, user, configurationManager);
+            }
 
             // This must be the last filter
             if (!string.IsNullOrEmpty(query.AdjacentTo))
@@ -818,7 +825,7 @@ namespace MediaBrowser.Controller.Entities
                 items = FilterForAdjacency(items, query.AdjacentTo);
             }
 
-            return Sort(items, totalRecordLimit, query, libraryManager);
+            return SortAndPage(items, totalRecordLimit, query, libraryManager, enableSorting);
         }
 
         public static IEnumerable<BaseItem> CollapseBoxSetItemsIfNeeded(IEnumerable<BaseItem> items,
@@ -1093,8 +1100,6 @@ namespace MediaBrowser.Controller.Entities
             bool? isVirtualUnaired,
             bool? isUnaired)
         {
-            items = FilterVirtualSeasons(items, isMissing, isVirtualUnaired, isUnaired);
-
             if (isMissing.HasValue)
             {
                 var val = isMissing.Value;
@@ -1140,65 +1145,14 @@ namespace MediaBrowser.Controller.Entities
             return items;
         }
 
-        private static IEnumerable<BaseItem> FilterVirtualSeasons(
-            IEnumerable<BaseItem> items,
-            bool? isMissing,
-            bool? isVirtualUnaired,
-            bool? isUnaired)
-        {
-            if (isMissing.HasValue)
-            {
-                var val = isMissing.Value;
-                items = items.Where(i =>
-                {
-                    var e = i as Season;
-                    if (e != null)
-                    {
-                        return (e.IsMissingSeason) == val;
-                    }
-                    return true;
-                });
-            }
-
-            if (isUnaired.HasValue)
-            {
-                var val = isUnaired.Value;
-                items = items.Where(i =>
-                {
-                    var e = i as Season;
-                    if (e != null)
-                    {
-                        return e.IsUnaired == val;
-                    }
-                    return true;
-                });
-            }
-
-            if (isVirtualUnaired.HasValue)
-            {
-                var val = isVirtualUnaired.Value;
-                items = items.Where(i =>
-                {
-                    var e = i as Season;
-                    if (e != null)
-                    {
-                        return e.IsVirtualUnaired == val;
-                    }
-                    return true;
-                });
-            }
-
-            return items;
-        }
-
-        public static QueryResult<BaseItem> Sort(IEnumerable<BaseItem> items,
+        public static QueryResult<BaseItem> SortAndPage(IEnumerable<BaseItem> items,
             int? totalRecordLimit,
             InternalItemsQuery query,
-            ILibraryManager libraryManager)
+            ILibraryManager libraryManager, bool enableSorting)
         {
             var user = query.User;
 
-            items = items.DistinctBy(i => i.PresentationUniqueKey, StringComparer.OrdinalIgnoreCase);
+            items = items.DistinctBy(i => i.GetPresentationUniqueKey(), StringComparer.OrdinalIgnoreCase);
 
             if (query.SortBy.Length > 0)
             {

+ 16 - 11
MediaBrowser.Controller/Entities/Video.cs

@@ -12,6 +12,7 @@ using System.Runtime.Serialization;
 using System.Threading;
 using System.Threading.Tasks;
 using CommonIO;
+using MediaBrowser.Common.Extensions;
 using MediaBrowser.Controller.Channels;
 
 namespace MediaBrowser.Controller.Entities
@@ -44,24 +45,23 @@ namespace MediaBrowser.Controller.Entities
             }
         }
 
-        [IgnoreDataMember]
-        public override string PresentationUniqueKey
+        public override string CreatePresentationUniqueKey()
         {
-            get
+            if (!string.IsNullOrWhiteSpace(PrimaryVersionId))
             {
-                if (!string.IsNullOrWhiteSpace(PrimaryVersionId))
-                {
-                    return PrimaryVersionId;
-                }
-
-                return base.PresentationUniqueKey;
+                return PrimaryVersionId;
             }
+
+            return base.CreatePresentationUniqueKey();
         }
 
         [IgnoreDataMember]
-        public override bool EnableForceSaveOnDateModifiedChange
+        public override bool EnableRefreshOnDateModifiedChange
         {
-            get { return true; }
+            get
+            {
+                return VideoType == VideoType.VideoFile || VideoType == VideoType.Iso;
+            }
         }
 
         public int? TotalBitrate { get; set; }
@@ -612,6 +612,11 @@ namespace MediaBrowser.Controller.Entities
                 SupportsDirectStream = i.VideoType == VideoType.VideoFile
             };
 
+            if (info.Protocol == MediaProtocol.File)
+            {
+                info.ETag = i.DateModified.Ticks.ToString(CultureInfo.InvariantCulture).GetMD5().ToString("N");
+            }
+
             if (i.IsShortcut)
             {
                 info.Path = i.ShortcutPath;

+ 43 - 0
MediaBrowser.Controller/Entities/Year.cs

@@ -112,5 +112,48 @@ namespace MediaBrowser.Controller.Entities
                 return false;
             }
         }
+
+        public static string GetPath(string name, bool normalizeName = true)
+        {
+            // Trim the period at the end because windows will have a hard time with that
+            var validName = normalizeName ?
+                FileSystem.GetValidFilename(name).Trim().TrimEnd('.') :
+                name;
+
+            return System.IO.Path.Combine(ConfigurationManager.ApplicationPaths.YearPath, validName);
+        }
+
+        private string GetRebasedPath()
+        {
+            return GetPath(System.IO.Path.GetFileName(Path), false);
+        }
+
+        public override bool RequiresRefresh()
+        {
+            var newPath = GetRebasedPath();
+            if (!string.Equals(Path, newPath, StringComparison.Ordinal))
+            {
+                Logger.Debug("{0} path has changed from {1} to {2}", GetType().Name, Path, newPath);
+                return true;
+            }
+            return base.RequiresRefresh();
+        }
+
+        /// <summary>
+        /// This is called before any metadata refresh and returns true or false indicating if changes were made
+        /// </summary>
+        public override bool BeforeMetadataRefresh()
+        {
+            var hasChanges = base.BeforeMetadataRefresh();
+
+            var newPath = GetRebasedPath();
+            if (!string.Equals(Path, newPath, StringComparison.Ordinal))
+            {
+                Path = newPath;
+                hasChanges = true;
+            }
+
+            return hasChanges;
+        }
     }
 }

+ 2 - 0
MediaBrowser.Controller/IServerApplicationPaths.cs

@@ -106,5 +106,7 @@ namespace MediaBrowser.Controller
         /// </summary>
         /// <value>The internal metadata path.</value>
         string InternalMetadataPath { get; }
+
+        string ArtistsPath { get; }
     }
 }

+ 11 - 8
MediaBrowser.Controller/Library/ILibraryManager.cs

@@ -11,6 +11,8 @@ using System.Collections.Generic;
 using System.Threading;
 using System.Threading.Tasks;
 using CommonIO;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Model.Configuration;
 using MediaBrowser.Model.Dto;
 
 namespace MediaBrowser.Controller.Library
@@ -32,15 +34,11 @@ namespace MediaBrowser.Controller.Library
         /// <summary>
         /// Resolves a set of files into a list of BaseItem
         /// </summary>
-        /// <param name="files">The files.</param>
-        /// <param name="directoryService">The directory service.</param>
-        /// <param name="parent">The parent.</param>
-        /// <param name="collectionType">Type of the collection.</param>
-        /// <returns>List{``0}.</returns>
         IEnumerable<BaseItem> ResolvePaths(IEnumerable<FileSystemMetadata> files,
             IDirectoryService directoryService,
-            Folder parent, string
-            collectionType = null);
+            Folder parent,
+            LibraryOptions libraryOptions,
+            string collectionType = null);
 
         /// <summary>
         /// Gets the root folder.
@@ -397,6 +395,9 @@ namespace MediaBrowser.Controller.Library
         /// <returns><c>true</c> if [is audio file] [the specified path]; otherwise, <c>false</c>.</returns>
         bool IsAudioFile(string path);
 
+        bool IsAudioFile(string path, LibraryOptions libraryOptions);
+        bool IsVideoFile(string path, LibraryOptions libraryOptions);
+
         /// <summary>
         /// Gets the season number from path.
         /// </summary>
@@ -453,6 +454,8 @@ namespace MediaBrowser.Controller.Library
         /// <returns>IEnumerable&lt;Folder&gt;.</returns>
         IEnumerable<Folder> GetCollectionFolders(BaseItem item);
 
+        LibraryOptions GetLibraryOptions(BaseItem item);
+
         /// <summary>
         /// Gets the people.
         /// </summary>
@@ -551,7 +554,7 @@ namespace MediaBrowser.Controller.Library
         /// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns>
         bool IgnoreFile(FileSystemMetadata file, BaseItem parent);
 
-        void AddVirtualFolder(string name, string collectionType, string[] mediaPaths, bool refreshLibrary);
+        void AddVirtualFolder(string name, string collectionType, string[] mediaPaths, LibraryOptions options, bool refreshLibrary);
         void RemoveVirtualFolder(string name, bool refreshLibrary);
         void AddMediaPath(string virtualFolderName, string path);
         void RemoveMediaPath(string virtualFolderName, string path);

+ 9 - 0
MediaBrowser.Controller/Library/ItemResolveArgs.cs

@@ -5,6 +5,8 @@ using System.Collections.Generic;
 using System.IO;
 using System.Linq;
 using CommonIO;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Model.Configuration;
 
 namespace MediaBrowser.Controller.Library
 {
@@ -51,6 +53,13 @@ namespace MediaBrowser.Controller.Library
             }
         }
 
+        public LibraryOptions LibraryOptions { get; set; }
+
+        public LibraryOptions GetLibraryOptions()
+        {
+            return LibraryOptions ?? (LibraryOptions = (Parent == null ? new LibraryOptions() : BaseItem.LibraryManager.GetLibraryOptions(Parent)));
+        }
+
         /// <summary>
         /// Gets or sets the file system dictionary.
         /// </summary>

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

@@ -236,6 +236,7 @@
     <Compile Include="Net\IAuthorizationContext.cs" />
     <Compile Include="Net\IAuthService.cs" />
     <Compile Include="Net\IHasAuthorization.cs" />
+    <Compile Include="Net\IAsyncStreamSource.cs" />
     <Compile Include="Net\IHasResultFactory.cs" />
     <Compile Include="Net\IHasSession.cs" />
     <Compile Include="Net\IHttpResultFactory.cs" />

+ 18 - 0
MediaBrowser.Controller/Net/IAsyncStreamSource.cs

@@ -0,0 +1,18 @@
+using ServiceStack.Web;
+using System.IO;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Net
+{
+    /// <summary>
+    /// Interface IAsyncStreamSource
+    /// Enables asynchronous writing to http resonse streams
+    /// </summary>
+    public interface IAsyncStreamSource
+    {
+        /// <summary>
+        /// Asynchronously write to the response stream.
+        /// </summary>
+        Task WriteToAsync(Stream responseStream);
+    }
+}

+ 1 - 1
MediaBrowser.Controller/Net/IHttpResultFactory.cs

@@ -28,7 +28,7 @@ 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);
+        object GetAsyncStreamWriter(IAsyncStreamSource streamSource);
 
         /// <summary>
         /// Gets the optimized result.

+ 6 - 0
MediaBrowser.Controller/Persistence/IItemRepository.cs

@@ -170,6 +170,12 @@ namespace MediaBrowser.Controller.Persistence
         QueryResult<Tuple<BaseItem, ItemCounts>> GetArtists(InternalItemsQuery query);
         QueryResult<Tuple<BaseItem, ItemCounts>> GetAlbumArtists(InternalItemsQuery query);
         QueryResult<Tuple<BaseItem, ItemCounts>> GetAllArtists(InternalItemsQuery query);
+
+        List<string> GetGameGenreNames();
+        List<string> GetMusicGenreNames();
+        List<string> GetStudioNames();
+        List<string> GetGenreNames();
+        List<string> GetAllArtistNames();
     }
 }
 

+ 12 - 0
MediaBrowser.Controller/Playlists/Playlist.cs

@@ -7,6 +7,7 @@ using System.Collections.Generic;
 using System.Linq;
 using System.Runtime.Serialization;
 using System.Threading.Tasks;
+using MediaBrowser.Controller.Providers;
 
 namespace MediaBrowser.Controller.Playlists
 {
@@ -58,11 +59,22 @@ namespace MediaBrowser.Controller.Playlists
             return true;
         }
 
+        protected override IEnumerable<BaseItem> LoadChildren()
+        {
+            // Save a trip to the database
+            return new List<BaseItem>();
+        }
+
         public override IEnumerable<BaseItem> GetChildren(User user, bool includeLinkedChildren)
         {
             return GetPlayableItems(user).Result;
         }
 
+        protected override IEnumerable<BaseItem> GetNonCachedChildren(IDirectoryService directoryService)
+        {
+            return new List<BaseItem>();
+        }
+
         public override IEnumerable<BaseItem> GetRecursiveChildren(User user, InternalItemsQuery query)
         {
             var items = GetPlayableItems(user).Result;

+ 6 - 1
MediaBrowser.Dlna/DlnaManager.cs

@@ -73,8 +73,13 @@ namespace MediaBrowser.Dlna
             lock (_profiles)
             {
                 var list = _profiles.Values.ToList();
-                return list.Select(i => i.Item2).OrderBy(i => i.Name);
+                return list
+                    .OrderBy(i => i.Item1.Info.Type == DeviceProfileType.User ? 0 : 1)
+                    .ThenBy(i => i.Item1.Info.Name)
+                    .Select(i => i.Item2)
+                    .ToList();
             }
+
         }
 
         public DeviceProfile GetDefaultProfile()

+ 39 - 0
MediaBrowser.Dlna/PlayTo/Device.cs

@@ -7,6 +7,7 @@ using System;
 using System.Collections.Generic;
 using System.Globalization;
 using System.Linq;
+using System.Net;
 using System.Security;
 using System.Threading;
 using System.Threading.Tasks;
@@ -91,6 +92,7 @@ namespace MediaBrowser.Dlna.PlayTo
         private readonly IServerConfigurationManager _config;
 
         public DateTime DateLastActivity { get; private set; }
+        public Action OnDeviceUnavailable { get; set; }
 
         public Device(DeviceInfo deviceProperties, IHttpClient httpClient, ILogger logger, IServerConfigurationManager config)
         {
@@ -134,6 +136,9 @@ namespace MediaBrowser.Dlna.PlayTo
 
         private async void RefreshVolume()
         {
+            if (_disposed)
+                return;
+
             try
             {
                 await GetVolume().ConfigureAwait(false);
@@ -149,6 +154,9 @@ namespace MediaBrowser.Dlna.PlayTo
         private bool _timerActive;
         private void RestartTimer()
         {
+            if (_disposed)
+                return;
+
             if (!_timerActive)
             {
                 lock (_timerLock)
@@ -169,6 +177,9 @@ namespace MediaBrowser.Dlna.PlayTo
         /// </summary>
         private void RestartTimerInactive()
         {
+            if (_disposed)
+                return;
+
             if (_timerActive)
             {
                 lock (_timerLock)
@@ -398,6 +409,7 @@ namespace MediaBrowser.Dlna.PlayTo
         #region Get data
 
         private int _successiveStopCount;
+        private int _connectFailureCount;
         private async void TimerCallback(object sender)
         {
             if (_disposed)
@@ -435,6 +447,8 @@ namespace MediaBrowser.Dlna.PlayTo
                         }
                     }
 
+                    _connectFailureCount = 0;
+
                     if (_disposed)
                         return;
 
@@ -455,8 +469,33 @@ namespace MediaBrowser.Dlna.PlayTo
                     }
                 }
             }
+            catch (WebException ex)
+            {
+                if (_disposed)
+                    return;
+
+                _logger.ErrorException("Error updating device info for {0}", ex, Properties.Name);
+
+                _successiveStopCount++;
+                _connectFailureCount++;
+
+                if (_successiveStopCount >= maxSuccessiveStopReturns)
+                {
+                    RestartTimerInactive();
+                }
+                if (_connectFailureCount >= maxSuccessiveStopReturns)
+                {
+                    if (OnDeviceUnavailable != null)
+                    {
+                        OnDeviceUnavailable();
+                    }
+                }
+            }
             catch (Exception ex)
             {
+                if (_disposed)
+                    return;
+
                 _logger.ErrorException("Error updating device info for {0}", ex, Properties.Name);
 
                 _successiveStopCount++;

+ 16 - 8
MediaBrowser.Dlna/PlayTo/PlayToController.cs

@@ -103,11 +103,25 @@ namespace MediaBrowser.Dlna.PlayTo
             _device.PlaybackProgress += _device_PlaybackProgress;
             _device.PlaybackStopped += _device_PlaybackStopped;
             _device.MediaChanged += _device_MediaChanged;
+            _device.OnDeviceUnavailable = OnDeviceUnavailable;
+
             _device.Start();
 
             _deviceDiscovery.DeviceLeft += _deviceDiscovery_DeviceLeft;
         }
 
+        private void OnDeviceUnavailable()
+        {
+            try
+            {
+                _sessionManager.ReportSessionEnded(_session.Id);
+            }
+            catch
+            {
+                // Could throw if the session is already gone
+            }
+        }
+
         void _deviceDiscovery_DeviceLeft(object sender, SsdpMessageEventArgs e)
         {
             string nts;
@@ -125,14 +139,7 @@ namespace MediaBrowser.Dlna.PlayTo
                 if (usn.IndexOf("MediaRenderer:", StringComparison.OrdinalIgnoreCase) != -1 ||
                     nt.IndexOf("MediaRenderer:", StringComparison.OrdinalIgnoreCase) != -1)
                 {
-                    try
-                    {
-                        _sessionManager.ReportSessionEnded(_session.Id);
-                    }
-                    catch
-                    {
-                        // Could throw if the session is already gone
-                    }
+                    OnDeviceUnavailable();
                 }
             }
         }
@@ -647,6 +654,7 @@ namespace MediaBrowser.Dlna.PlayTo
                 _device.PlaybackStopped -= _device_PlaybackStopped;
                 _device.MediaChanged -= _device_MediaChanged;
                 _deviceDiscovery.DeviceLeft -= _deviceDiscovery_DeviceLeft;
+                _device.OnDeviceUnavailable = null;
 
                 _device.Dispose();
             }

+ 16 - 4
MediaBrowser.Dlna/Profiles/DefaultProfile.cs

@@ -65,14 +65,26 @@ namespace MediaBrowser.Dlna.Profiles
             {
                 new DirectPlayProfile
                 {
-                    Container = "mp3,wma",
-                    Type = DlnaProfileType.Audio
+                    Container = "m4v,ts,mkv,avi,mpg,mpeg,mp4",
+                    VideoCodec = "h264",
+                    AudioCodec = "aac,mp3,ac3",
+                    Type = DlnaProfileType.Video
                 },
 
                 new DirectPlayProfile
                 {
-                    Container = "avi,mp4",
-                    Type = DlnaProfileType.Video
+                    Container = "mp3,wma,aac,wav",
+                    Type = DlnaProfileType.Audio
+                }
+            };
+
+            ResponseProfiles = new[]
+            {
+                new ResponseProfile
+                {
+                    Container = "m4v",
+                    Type = DlnaProfileType.Video,
+                    MimeType = "video/mp4"
                 }
             };
         }

+ 2 - 0
MediaBrowser.Dlna/Profiles/DenonAvrProfile.cs

@@ -24,6 +24,8 @@ namespace MediaBrowser.Dlna.Profiles
                     Type = DlnaProfileType.Audio
                 },
             };
+
+            ResponseProfiles = new ResponseProfile[] { };
         }
     }
 }

+ 2 - 0
MediaBrowser.Dlna/Profiles/DirectTvProfile.cs

@@ -112,6 +112,8 @@ namespace MediaBrowser.Dlna.Profiles
                     }
                 }
             };
+
+            ResponseProfiles = new ResponseProfile[] { };
         }
     }
 }

+ 3 - 1
MediaBrowser.Dlna/Profiles/Foobar2000Profile.cs

@@ -11,7 +11,7 @@ namespace MediaBrowser.Dlna.Profiles
             Name = "foobar2000";
 
             SupportedMediaTypes = "Audio";
-            
+
             Identification = new DeviceIdentification
             {
                 FriendlyName = @"foobar",
@@ -70,6 +70,8 @@ namespace MediaBrowser.Dlna.Profiles
                     Type = DlnaProfileType.Audio
                 }
             };
+
+            ResponseProfiles = new ResponseProfile[] { };
         }
     }
 }

+ 2 - 0
MediaBrowser.Dlna/Profiles/LgTvProfile.cs

@@ -198,6 +198,8 @@ namespace MediaBrowser.Dlna.Profiles
                     Method = SubtitleDeliveryMethod.External
                 }
             };
+
+            ResponseProfiles = new ResponseProfile[] { };
         }
     }
 }

+ 2 - 0
MediaBrowser.Dlna/Profiles/LinksysDMA2100Profile.cs

@@ -30,6 +30,8 @@ namespace MediaBrowser.Dlna.Profiles
                     Type = DlnaProfileType.Video
                 }
             };
+
+            ResponseProfiles = new ResponseProfile[] { };
         }
     }
 }

+ 2 - 0
MediaBrowser.Dlna/Profiles/MediaMonkeyProfile.cs

@@ -70,6 +70,8 @@ namespace MediaBrowser.Dlna.Profiles
                     Type = DlnaProfileType.Audio
                 }
             };
+
+            ResponseProfiles = new ResponseProfile[] { };
         }
     }
 }

+ 2 - 0
MediaBrowser.Dlna/Profiles/PopcornHourProfile.cs

@@ -200,6 +200,8 @@ namespace MediaBrowser.Dlna.Profiles
                     }
                 }
             };
+
+            ResponseProfiles = new ResponseProfile[] { };
         }
     }
 }

+ 2 - 0
MediaBrowser.Dlna/Profiles/SonyBlurayPlayer2013Profile.cs

@@ -181,6 +181,8 @@ namespace MediaBrowser.Dlna.Profiles
                     }
                 }
             };
+
+            ResponseProfiles = new ResponseProfile[] { };
         }
     }
 }

+ 7 - 3
MediaBrowser.Dlna/Profiles/Xml/Default.xml

@@ -29,8 +29,8 @@
   <IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests>
   <XmlRootAttributes />
   <DirectPlayProfiles>
-    <DirectPlayProfile container="mp3,wma" type="Audio" />
-    <DirectPlayProfile container="avi,mp4" type="Video" />
+    <DirectPlayProfile container="m4v,ts,mkv,avi,mpg,mpeg,mp4" audioCodec="aac,mp3,ac3" videoCodec="h264" type="Video" />
+    <DirectPlayProfile container="mp3,wma,aac,wav" type="Audio" />
   </DirectPlayProfiles>
   <TranscodingProfiles>
     <TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" forceLiveStream="false" enableSubtitlesInManifest="false" />
@@ -39,6 +39,10 @@
   </TranscodingProfiles>
   <ContainerProfiles />
   <CodecProfiles />
-  <ResponseProfiles />
+  <ResponseProfiles>
+    <ResponseProfile container="m4v" type="Video" mimeType="video/mp4">
+      <Conditions />
+    </ResponseProfile>
+  </ResponseProfiles>
   <SubtitleProfiles />
 </Profile>

+ 4 - 1
MediaBrowser.MediaEncoding/Encoder/AudioEncoder.cs

@@ -51,7 +51,10 @@ namespace MediaBrowser.MediaEncoding.Encoder
             var metadata = string.Empty;
             var vn = string.Empty;
 
-            if (!string.IsNullOrWhiteSpace(state.AlbumCoverPath))
+            var hasArt = !string.IsNullOrWhiteSpace(state.AlbumCoverPath);
+            hasArt = false;
+
+            if (hasArt)
             {
                 albumCoverInput = " -i \"" + state.AlbumCoverPath + "\"";
                 mapArgs = " -map 0:a -map 1:v -c:v copy";

+ 20 - 6
MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs

@@ -123,10 +123,22 @@ namespace MediaBrowser.MediaEncoding.Encoder
                     return "System";
                 }
 
+                if (IsDefaultPath(FFMpegPath))
+                {
+                    return "Default";
+                }
+
                 return "Custom";
             }
         }
 
+        private bool IsDefaultPath(string path)
+        {
+            var parentPath = Path.Combine(ConfigurationManager.ApplicationPaths.ProgramDataPath, "ffmpeg", "20160410");
+
+            return FileSystem.ContainsSubPath(parentPath, path);
+        }
+
         private bool IsSystemInstalledPath(string path)
         {
             if (path.IndexOf("/", StringComparison.Ordinal) == -1 && path.IndexOf("\\", StringComparison.Ordinal) == -1)
@@ -343,14 +355,16 @@ namespace MediaBrowser.MediaEncoding.Encoder
             // If that doesn't pan out, then do a recursive search
             var files = Directory.GetFiles(path);
 
-            var ffmpegPath = files.FirstOrDefault(i => string.Equals(Path.GetFileNameWithoutExtension(i), "ffmpeg", StringComparison.OrdinalIgnoreCase));
-            var ffprobePath = files.FirstOrDefault(i => string.Equals(Path.GetFileNameWithoutExtension(i), "ffprobe", StringComparison.OrdinalIgnoreCase));
+            var excludeExtensions = new[] { ".c" };
+
+            var ffmpegPath = files.FirstOrDefault(i => string.Equals(Path.GetFileNameWithoutExtension(i), "ffmpeg", StringComparison.OrdinalIgnoreCase) && !excludeExtensions.Contains(Path.GetExtension(i) ?? string.Empty));
+            var ffprobePath = files.FirstOrDefault(i => string.Equals(Path.GetFileNameWithoutExtension(i), "ffprobe", StringComparison.OrdinalIgnoreCase) && !excludeExtensions.Contains(Path.GetExtension(i) ?? string.Empty));
 
             if (string.IsNullOrWhiteSpace(ffmpegPath) || !File.Exists(ffmpegPath))
             {
                 files = Directory.GetFiles(path, "*", SearchOption.AllDirectories);
 
-                ffmpegPath = files.FirstOrDefault(i => string.Equals(Path.GetFileNameWithoutExtension(i), "ffmpeg", StringComparison.OrdinalIgnoreCase));
+                ffmpegPath = files.FirstOrDefault(i => string.Equals(Path.GetFileNameWithoutExtension(i), "ffmpeg", StringComparison.OrdinalIgnoreCase) && !excludeExtensions.Contains(Path.GetExtension(i) ?? string.Empty));
 
                 if (!string.IsNullOrWhiteSpace(ffmpegPath))
                 {
@@ -874,8 +888,8 @@ namespace MediaBrowser.MediaEncoding.Encoder
             var mapArg = imageStreamIndex.HasValue ? (" -map 0:v:" + imageStreamIndex.Value.ToString(CultureInfo.InvariantCulture)) : string.Empty;
 
             // Use ffmpeg to sample 100 (we can drop this if required using thumbnail=50 for 50 frames) frames and pick the best thumbnail. Have a fall back just in case.
-            var args = useIFrame ? string.Format("-i {0}{3} -threads 1 -v quiet -vframes 1 -vf \"{2},thumbnail=30\" -f image2 \"{1}\"", inputPath, tempExtractPath, vf, mapArg) :
-                string.Format("-i {0}{3} -threads 1 -v quiet -vframes 1 -vf \"{2}\" -f image2 \"{1}\"", inputPath, tempExtractPath, vf, mapArg);
+            var args = useIFrame ? string.Format("-i {0}{3} -threads 0 -v quiet -vframes 1 -vf \"{2},thumbnail=30\" -f image2 \"{1}\"", inputPath, tempExtractPath, vf, mapArg) :
+                string.Format("-i {0}{3} -threads 0 -v quiet -vframes 1 -vf \"{2}\" -f image2 \"{1}\"", inputPath, tempExtractPath, vf, mapArg);
 
             var probeSize = GetProbeSizeArgument(new[] { inputPath }, protocol);
 
@@ -980,7 +994,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
             FileSystem.CreateDirectory(targetDirectory);
             var outputPath = Path.Combine(targetDirectory, filenamePrefix + "%05d.jpg");
 
-            var args = string.Format("-i {0} -threads 1 -v quiet -vf \"{2}\" -f image2 \"{1}\"", inputArgument, outputPath, vf);
+            var args = string.Format("-i {0} -threads 0 -v quiet -vf \"{2}\" -f image2 \"{1}\"", inputArgument, outputPath, vf);
 
             var probeSize = GetProbeSizeArgument(new[] { inputArgument }, protocol);
 

+ 4 - 3
MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs

@@ -9,6 +9,7 @@ using System.Linq;
 using System.Text;
 using System.Xml;
 using CommonIO;
+using MediaBrowser.Controller.Library;
 using MediaBrowser.Model.Logging;
 using MediaBrowser.Model.MediaInfo;
 
@@ -793,7 +794,7 @@ namespace MediaBrowser.MediaEncoding.Probing
             if (!string.IsNullOrWhiteSpace(artists))
             {
                 audio.Artists = SplitArtists(artists, new[] { '/', ';' }, false)
-                    .Distinct(StringComparer.OrdinalIgnoreCase)
+                    .DistinctNames()
                     .ToList();
             }
             else
@@ -806,7 +807,7 @@ namespace MediaBrowser.MediaEncoding.Probing
                 else
                 {
                     audio.Artists = SplitArtists(artist, _nameDelimiters, true)
-                        .Distinct(StringComparer.OrdinalIgnoreCase)
+                    .DistinctNames()
                         .ToList();
                 }
             }
@@ -828,7 +829,7 @@ namespace MediaBrowser.MediaEncoding.Probing
             else
             {
                 audio.AlbumArtists = SplitArtists(albumArtist, _nameDelimiters, true)
-                    .Distinct(StringComparer.OrdinalIgnoreCase)
+                    .DistinctNames()
                     .ToList();
 
             }

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

@@ -205,6 +205,9 @@
     <Compile Include="..\MediaBrowser.Model\Configuration\ImageSavingConvention.cs">
       <Link>Configuration\ImageSavingConvention.cs</Link>
     </Compile>
+    <Compile Include="..\MediaBrowser.Model\Configuration\LibraryOptions.cs">
+      <Link>Configuration\LibraryOptions.cs</Link>
+    </Compile>
     <Compile Include="..\MediaBrowser.Model\Configuration\MetadataConfiguration.cs">
       <Link>Configuration\MetadataConfiguration.cs</Link>
     </Compile>

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

@@ -177,6 +177,9 @@
     <Compile Include="..\MediaBrowser.Model\Configuration\ImageSavingConvention.cs">
       <Link>Configuration\ImageSavingConvention.cs</Link>
     </Compile>
+    <Compile Include="..\MediaBrowser.Model\Configuration\LibraryOptions.cs">
+      <Link>Configuration\LibraryOptions.cs</Link>
+    </Compile>
     <Compile Include="..\MediaBrowser.Model\Configuration\MetadataConfiguration.cs">
       <Link>Configuration\MetadataConfiguration.cs</Link>
     </Compile>

+ 13 - 0
MediaBrowser.Model/Configuration/LibraryOptions.cs

@@ -0,0 +1,13 @@
+namespace MediaBrowser.Model.Configuration
+{
+    public class LibraryOptions
+    {
+        public bool EnableArchiveMediaFiles { get; set; }
+        public bool EnablePhotos { get; set; }
+
+        public LibraryOptions()
+        {
+            EnablePhotos = true;
+        }
+    }
+}

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

@@ -180,8 +180,6 @@ namespace MediaBrowser.Model.Configuration
 
         public NameValuePair[] ContentTypes { get; set; }
 
-        public bool EnableAudioArchiveFiles { get; set; }
-        public bool EnableVideoArchiveFiles { get; set; }
         public int RemoteClientBitrateLimit { get; set; }
 
         public AutoOnOff EnableLibraryMonitor { get; set; }
@@ -204,6 +202,7 @@ namespace MediaBrowser.Model.Configuration
         public bool DisplaySpecialsWithinSeasons { get; set; }
         public bool DisplayCollectionsView { get; set; }
         public string[] LocalNetworkAddresses { get; set; }
+        public string[] CodecsUsed { get; set; }
 
         /// <summary>
         /// Initializes a new instance of the <see cref="ServerConfiguration" /> class.
@@ -212,6 +211,7 @@ namespace MediaBrowser.Model.Configuration
         {
             LocalNetworkAddresses = new string[] { };
             Migrations = new string[] { };
+            CodecsUsed = new string[] { };
             SqliteCacheSize = 0;
 
             EnableLocalizedGuids = true;

+ 3 - 2
MediaBrowser.Model/Dlna/ResolutionNormalizer.cs

@@ -1,5 +1,6 @@
 using System;
 using System.Collections.Generic;
+using MediaBrowser.Model.Extensions;
 
 namespace MediaBrowser.Model.Dlna
 {
@@ -59,8 +60,8 @@ namespace MediaBrowser.Model.Dlna
 
         private static double GetVideoBitrateScaleFactor(string codec)
         {
-            if (string.Equals(codec, "h265", StringComparison.OrdinalIgnoreCase) ||
-                string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase))
+            if (StringHelper.EqualsIgnoreCase(codec, "h265") ||
+                StringHelper.EqualsIgnoreCase(codec, "hevc"))
             {
                 return .5;
             }

+ 10 - 3
MediaBrowser.Model/Dlna/StreamBuilder.cs

@@ -570,7 +570,7 @@ namespace MediaBrowser.Model.Dlna
                     playlistItem.MaxAudioChannels = Math.Min(options.MaxAudioChannels.Value, currentValue);
                 }
 
-                int audioBitrate = GetAudioBitrate(options.GetMaxBitrate(), playlistItem.TargetAudioChannels, playlistItem.TargetAudioCodec, audioStream);
+                int audioBitrate = GetAudioBitrate(playlistItem.SubProtocol, options.GetMaxBitrate(), playlistItem.TargetAudioChannels, playlistItem.TargetAudioCodec, audioStream);
                 playlistItem.AudioBitrate = Math.Min(playlistItem.AudioBitrate ?? audioBitrate, audioBitrate);
 
                 int? maxBitrateSetting = options.GetMaxBitrate();
@@ -593,7 +593,7 @@ namespace MediaBrowser.Model.Dlna
             return playlistItem;
         }
 
-        private int GetAudioBitrate(int? maxTotalBitrate, int? targetAudioChannels, string targetAudioCodec, MediaStream audioStream)
+        private int GetAudioBitrate(string subProtocol, int? maxTotalBitrate, int? targetAudioChannels, string targetAudioCodec, MediaStream audioStream)
         {
             var defaultBitrate = 128000;
             if (StringHelper.EqualsIgnoreCase(targetAudioCodec, "ac3"))
@@ -611,7 +611,14 @@ namespace MediaBrowser.Model.Dlna
                 {
                     if (StringHelper.EqualsIgnoreCase(targetAudioCodec, "ac3"))
                     {
-                        defaultBitrate = Math.Max(448000, defaultBitrate);
+                        if (string.Equals(subProtocol, "hls", StringComparison.OrdinalIgnoreCase))
+                        {
+                            defaultBitrate = Math.Max(384000, defaultBitrate);
+                        }
+                        else
+                        {
+                            defaultBitrate = Math.Max(448000, defaultBitrate);
+                        }
                     }
                     else
                     {

+ 2 - 0
MediaBrowser.Model/Dlna/StreamInfo.cs

@@ -252,6 +252,8 @@ namespace MediaBrowser.Model.Dlna
             list.Add(new NameValuePair("TranscodingMaxAudioChannels", item.TranscodingMaxAudioChannels.HasValue ? StringHelper.ToStringCultureInvariant(item.TranscodingMaxAudioChannels.Value) : string.Empty));
             list.Add(new NameValuePair("EnableSubtitlesInManifest", item.EnableSubtitlesInManifest.ToString().ToLower()));
 
+            list.Add(new NameValuePair("Tag", item.MediaSource.ETag ?? string.Empty));
+
             return list;
         }
 

+ 12 - 2
MediaBrowser.Model/Dto/BaseItemDto.cs

@@ -346,7 +346,16 @@ namespace MediaBrowser.Model.Dto
         /// Gets or sets a value indicating whether this instance is folder.
         /// </summary>
         /// <value><c>true</c> if this instance is folder; otherwise, <c>false</c>.</value>
-        public bool IsFolder { get; set; }
+        public bool? IsFolder { get; set; }
+
+        [IgnoreDataMember]
+        public bool IsFolderItem
+        {
+            get
+            {
+                return IsFolder ?? false;
+            }
+        }
 
         /// <summary>
         /// Gets or sets the parent id.
@@ -656,7 +665,7 @@ namespace MediaBrowser.Model.Dto
         {
             get
             {
-                return RunTimeTicks.HasValue || IsFolder || IsGenre || IsMusicGenre || IsArtist;
+                return RunTimeTicks.HasValue || IsFolderItem || IsGenre || IsMusicGenre || IsArtist;
             }
         }
 
@@ -837,6 +846,7 @@ namespace MediaBrowser.Model.Dto
         /// </summary>
         /// <value>The album count.</value>
         public int? AlbumCount { get; set; }
+        public int? ArtistCount { get; set; }
         /// <summary>
         /// Gets or sets the music video count.
         /// </summary>

+ 1 - 0
MediaBrowser.Model/Dto/ItemCounts.cs

@@ -25,6 +25,7 @@
         /// </summary>
         /// <value>The game count.</value>
         public int GameCount { get; set; }
+        public int ArtistCount { get; set; }
         /// <summary>
         /// Gets or sets the game system count.
         /// </summary>

+ 1 - 0
MediaBrowser.Model/Dto/MediaSourceInfo.cs

@@ -20,6 +20,7 @@ namespace MediaBrowser.Model.Dto
 
         public string Name { get; set; }
 
+        public string ETag { get; set; }
         public long? RunTimeTicks { get; set; }
         public bool ReadAtNativeFramerate { get; set; }
         public bool SupportsTranscoding { get; set; }

+ 3 - 0
MediaBrowser.Model/Entities/VirtualFolderInfo.cs

@@ -1,4 +1,5 @@
 using System.Collections.Generic;
+using MediaBrowser.Model.Configuration;
 
 namespace MediaBrowser.Model.Entities
 {
@@ -25,6 +26,8 @@ namespace MediaBrowser.Model.Entities
         /// <value>The type of the collection.</value>
         public string CollectionType { get; set; }
 
+        public LibraryOptions LibraryOptions { get; set; }
+
         /// <summary>
         /// Initializes a new instance of the <see cref="VirtualFolderInfo"/> class.
         /// </summary>

+ 6 - 0
MediaBrowser.Model/LiveTv/LiveTvChannelQuery.cs

@@ -59,5 +59,11 @@ namespace MediaBrowser.Model.LiveTv
         /// </summary>
         /// <value><c>true</c> if [add current program]; otherwise, <c>false</c>.</value>
         public bool AddCurrentProgram { get; set; }
+        public bool EnableUserData { get; set; }
+
+        public LiveTvChannelQuery()
+        {
+            EnableUserData = true;
+        }
     }
 }

+ 2 - 0
MediaBrowser.Model/LiveTv/ProgramQuery.cs

@@ -15,9 +15,11 @@ namespace MediaBrowser.Model.LiveTv
             SortBy = new string[] { };
             Genres = new string[] { };
             EnableTotalRecordCount = true;
+            EnableUserData = true;
         }
 
         public bool EnableTotalRecordCount { get; set; }
+        public bool EnableUserData { get; set; }
 
         /// <summary>
         /// Fields to return within the items, in addition to basic information

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

@@ -95,6 +95,7 @@
     <Compile Include="Configuration\CinemaModeConfiguration.cs" />
     <Compile Include="Configuration\EncodingOptions.cs" />
     <Compile Include="Configuration\FanartOptions.cs" />
+    <Compile Include="Configuration\LibraryOptions.cs" />
     <Compile Include="Configuration\MetadataConfiguration.cs" />
     <Compile Include="Configuration\PeopleMetadataOptions.cs" />
     <Compile Include="Configuration\XbmcMetadataOptions.cs" />

+ 1 - 0
MediaBrowser.Model/Sync/SyncJobQuery.cs

@@ -23,6 +23,7 @@ namespace MediaBrowser.Model.Sync
         /// </summary>
         /// <value>The user identifier.</value>
         public string UserId { get; set; }
+        public string ExcludeTargetIds { get; set; }
         /// <summary>
         /// Gets or sets the status.
         /// </summary>

+ 2 - 0
MediaBrowser.Model/Users/UserPolicy.cs

@@ -41,6 +41,7 @@ namespace MediaBrowser.Model.Users
         public bool EnableMediaPlayback { get; set; }
         public bool EnableAudioPlaybackTranscoding { get; set; }
         public bool EnableVideoPlaybackTranscoding { get; set; }
+        public bool EnablePlaybackRemuxing { get; set; }
 
         public bool EnableContentDeletion { get; set; }
         public bool EnableContentDownloading { get; set; }
@@ -76,6 +77,7 @@ namespace MediaBrowser.Model.Users
             EnableMediaPlayback = true;
             EnableAudioPlaybackTranscoding = true;
             EnableVideoPlaybackTranscoding = true;
+            EnablePlaybackRemuxing = true;
 
             EnableLiveTvManagement = true;
             EnableLiveTvAccess = true;

+ 16 - 6
MediaBrowser.Providers/Manager/MetadataService.cs

@@ -42,6 +42,13 @@ namespace MediaBrowser.Providers.Manager
             var config = ProviderManager.GetMetadataOptions(item);
 
             var updateType = ItemUpdateType.None;
+            var requiresRefresh = false;
+
+            if (refreshOptions.MetadataRefreshMode != MetadataRefreshMode.None)
+            {
+                // TODO: If this returns true, should we instead just change metadata refresh mode to Full?
+                requiresRefresh = item.RequiresRefresh();
+            }
 
             var itemImageProvider = new ItemImageProvider(Logger, ProviderManager, ServerConfigurationManager, FileSystem);
             var localImagesFailed = false;
@@ -70,14 +77,10 @@ namespace MediaBrowser.Providers.Manager
 
             bool hasRefreshedMetadata = true;
             bool hasRefreshedImages = true;
-            var requiresRefresh = false;
 
             // Next run metadata providers
             if (refreshOptions.MetadataRefreshMode != MetadataRefreshMode.None)
             {
-                // TODO: If this returns true, should we instead just change metadata refresh mode to Full?
-                requiresRefresh = item.RequiresRefresh();
-
                 var providers = GetProviders(item, refreshOptions, requiresRefresh)
                     .ToList();
 
@@ -149,7 +152,7 @@ namespace MediaBrowser.Providers.Manager
                 if (file != null)
                 {
                     var fileLastWriteTime = file.LastWriteTimeUtc;
-                    if (item.EnableForceSaveOnDateModifiedChange && fileLastWriteTime != item.DateModified)
+                    if (item.EnableRefreshOnDateModifiedChange && fileLastWriteTime != item.DateModified)
                     {
                         Logger.Debug("Date modified for {0}. Old date {1} new date {2} Id {3}", item.Path, item.DateModified, fileLastWriteTime, item.Id);
                         requiresRefresh = true;
@@ -284,6 +287,13 @@ namespace MediaBrowser.Providers.Manager
             updateType |= SaveCumulativeRunTimeTicks(item, isFullRefresh, currentUpdateType);
             updateType |= SaveDateLastMediaAdded(item, isFullRefresh, currentUpdateType);
 
+            var presentationUniqueKey = item.CreatePresentationUniqueKey();
+            if (!string.Equals(item.PresentationUniqueKey, presentationUniqueKey, StringComparison.Ordinal))
+            {
+                item.PresentationUniqueKey = presentationUniqueKey;
+                updateType |= ItemUpdateType.MetadataImport;
+            }
+
             return updateType;
         }
 
@@ -525,7 +535,7 @@ namespace MediaBrowser.Providers.Manager
             }
 
             // Local metadata is king - if any is found don't run remote providers
-            if (!options.ReplaceAllMetadata && (!hasLocalMetadata || options.MetadataRefreshMode == MetadataRefreshMode.FullRefresh))
+            if (!options.ReplaceAllMetadata && (!hasLocalMetadata || options.MetadataRefreshMode == MetadataRefreshMode.FullRefresh || !item.StopRefreshIfLocalMetadataFound))
             {
                 var remoteResult = await ExecuteRemoteProviders(temp, logName, id, providers.OfType<IRemoteMetadataProvider<TItemType, TIdType>>(), cancellationToken)
                     .ConfigureAwait(false);

+ 6 - 3
MediaBrowser.Providers/MediaInfo/FFProbeProvider.cs

@@ -171,10 +171,13 @@ namespace MediaBrowser.Providers.MediaInfo
 
         public bool HasChanged(IHasMetadata item, IDirectoryService directoryService)
         {
-            var file = directoryService.GetFile(item.Path);
-            if (file != null && file.LastWriteTimeUtc != item.DateModified)
+            if (item.EnableRefreshOnDateModifiedChange && !string.IsNullOrWhiteSpace(item.Path))
             {
-                return true;
+                var file = directoryService.GetFile(item.Path);
+                if (file != null && file.LastWriteTimeUtc != item.DateModified)
+                {
+                    return true;
+                }
             }
 
             if (item.SupportsLocalMetadata)

+ 6 - 3
MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs

@@ -194,10 +194,13 @@ namespace MediaBrowser.Providers.MediaInfo
 
         public bool HasChanged(IHasMetadata item, IDirectoryService directoryService)
         {
-            var file = directoryService.GetFile(item.Path);
-            if (file != null && file.LastWriteTimeUtc != item.DateModified)
+            if (item.EnableRefreshOnDateModifiedChange)
             {
-                return true;
+                var file = directoryService.GetFile(item.Path);
+                if (file != null && file.LastWriteTimeUtc != item.DateModified)
+                {
+                    return true;
+                }
             }
 
             return false;

+ 51 - 49
MediaBrowser.Providers/Music/MusicBrainzAlbumProvider.cs

@@ -81,46 +81,24 @@ namespace MediaBrowser.Providers.Music
 
         private IEnumerable<RemoteSearchResult> GetResultsFromResponse(XmlDocument doc)
         {
-            var ns = new XmlNamespaceManager(doc.NameTable);
-            ns.AddNamespace("mb", MusicBrainzBaseUrl + "/ns/mmd-2.0#");
-
-            var list = new List<RemoteSearchResult>();
-
-            var nodes = doc.SelectNodes("//mb:release-list/mb:release", ns);
-
-            if (nodes != null)
+            return ReleaseResult.Parse(doc).Select(i =>
             {
-                foreach (var node in nodes.Cast<XmlNode>())
+                var result = new RemoteSearchResult
                 {
-                    if (node.Attributes != null)
-                    {
-                        string name = null;
-
-                        string mbzId = node.Attributes["id"].Value;
-
-                        var nameNode = node.SelectSingleNode("//mb:title", ns);
-
-                        if (nameNode != null)
-                        {
-                            name = nameNode.InnerText;
-                        }
-
-                        if (!string.IsNullOrWhiteSpace(mbzId) && !string.IsNullOrWhiteSpace(name))
-                        {
-                            var result = new RemoteSearchResult
-                            {
-                                Name = name
-                            };
-
-                            result.SetProviderId(MetadataProviders.MusicBrainzAlbum, mbzId);
+                    Name = i.Title
+                };
 
-                            list.Add(result);
-                        }
-                    }
+                if (!string.IsNullOrWhiteSpace(i.ReleaseId))
+                {
+                    result.SetProviderId(MetadataProviders.MusicBrainzAlbum, i.ReleaseId);
+                }
+                if (!string.IsNullOrWhiteSpace(i.ReleaseGroupId))
+                {
+                    result.SetProviderId(MetadataProviders.MusicBrainzAlbum, i.ReleaseGroupId);
                 }
-            }
 
-            return list;
+                return result;
+            });
         }
 
         public async Task<MetadataResult<MusicAlbum>> GetMetadata(AlbumInfo id, CancellationToken cancellationToken)
@@ -208,7 +186,7 @@ namespace MediaBrowser.Providers.Music
 
             var doc = await GetMusicBrainzResponse(url, true, cancellationToken).ConfigureAwait(false);
 
-            return ReleaseResult.Parse(doc);
+            return ReleaseResult.Parse(doc, 1).FirstOrDefault();
         }
 
         private async Task<ReleaseResult> GetReleaseResultByArtistName(string albumName, string artistName, CancellationToken cancellationToken)
@@ -219,32 +197,32 @@ namespace MediaBrowser.Providers.Music
 
             var doc = await GetMusicBrainzResponse(url, true, cancellationToken).ConfigureAwait(false);
 
-            return ReleaseResult.Parse(doc);
+            return ReleaseResult.Parse(doc, 1).FirstOrDefault();
         }
 
         private class ReleaseResult
         {
             public string ReleaseId;
             public string ReleaseGroupId;
+            public string Title;
 
-            public static ReleaseResult Parse(XmlDocument doc)
+            public static List<ReleaseResult> Parse(XmlDocument doc, int? limit = null)
             {
                 var docElem = doc.DocumentElement;
+                var list = new List<ReleaseResult>();
 
                 if (docElem == null)
                 {
-                    return new ReleaseResult();
+                    return list;
                 }
 
                 var releaseList = docElem.FirstChild;
                 if (releaseList == null)
                 {
-                    return new ReleaseResult();
+                    return list;
                 }
 
                 var nodes = releaseList.ChildNodes;
-                string releaseId = null;
-                string releaseGroupId = null;
 
                 if (nodes != null)
                 {
@@ -252,18 +230,42 @@ namespace MediaBrowser.Providers.Music
                     {
                         if (string.Equals(node.Name, "release", StringComparison.OrdinalIgnoreCase))
                         {
-                            releaseId = node.Attributes["id"].Value;
-                            releaseGroupId = GetReleaseGroupIdFromReleaseNode(node);
-                            break;
+                            var releaseId = node.Attributes["id"].Value;
+                            var releaseGroupId = GetReleaseGroupIdFromReleaseNode(node);
+
+                            list.Add(new ReleaseResult
+                            {
+                                ReleaseId = releaseId,
+                                ReleaseGroupId = releaseGroupId,
+                                Title = GetTitleFromReleaseNode(node)
+                            });
+
+                            if (limit.HasValue && list.Count >= limit.Value)
+                            {
+                                break;
+                            }
                         }
                     }
                 }
 
-                return new ReleaseResult
+                return list;
+            }
+
+            private static string GetTitleFromReleaseNode(XmlNode node)
+            {
+                var subNodes = node.ChildNodes;
+                if (subNodes != null)
                 {
-                    ReleaseId = releaseId,
-                    ReleaseGroupId = releaseGroupId
-                };
+                    foreach (var subNode in subNodes.Cast<XmlNode>())
+                    {
+                        if (string.Equals(subNode.Name, "title", StringComparison.OrdinalIgnoreCase))
+                        {
+                            return subNode.InnerText;
+                        }
+                    }
+                }
+
+                return null;
             }
 
             private static string GetReleaseGroupIdFromReleaseNode(XmlNode node)

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

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

+ 6 - 15
MediaBrowser.Providers/TV/SeasonMetadataService.cs

@@ -35,26 +35,17 @@ namespace MediaBrowser.Providers.TV
                 updateType |= SaveIsVirtualItem(item, episodes);
             }
 
-            if (updateType <= ItemUpdateType.None)
+            if (!string.Equals(item.SeriesName, item.FindSeriesName(), StringComparison.Ordinal))
             {
-                if (!string.Equals(item.SeriesName, item.FindSeriesName(), StringComparison.Ordinal))
-                {
-                    updateType |= ItemUpdateType.MetadataImport;
-                }
+                updateType |= ItemUpdateType.MetadataImport;
             }
-            if (updateType <= ItemUpdateType.None)
+            if (!string.Equals(item.SeriesSortName, item.FindSeriesSortName(), StringComparison.Ordinal))
             {
-                if (!string.Equals(item.SeriesSortName, item.FindSeriesSortName(), StringComparison.Ordinal))
-                {
-                    updateType |= ItemUpdateType.MetadataImport;
-                }
+                updateType |= ItemUpdateType.MetadataImport;
             }
-            if (updateType <= ItemUpdateType.None)
+            if (item.SeriesId != item.FindSeriesId())
             {
-                if (item.SeriesId != item.FindSeriesId())
-                {
-                    updateType |= ItemUpdateType.MetadataImport;
-                }
+                updateType |= ItemUpdateType.MetadataImport;
             }
 
             return updateType;

+ 1 - 1
MediaBrowser.Server.Implementations/Collections/CollectionsDynamicFolder.cs

@@ -8,7 +8,7 @@ namespace MediaBrowser.Server.Implementations.Collections
     public class CollectionsDynamicFolder : IVirtualFolderCreator
     {
         private readonly IApplicationPaths _appPaths;
-        private IFileSystem _fileSystem;
+        private readonly IFileSystem _fileSystem;
 
         public CollectionsDynamicFolder(IApplicationPaths appPaths, IFileSystem fileSystem)
         {

+ 50 - 114
MediaBrowser.Server.Implementations/Dto/DtoService.cs

@@ -329,7 +329,7 @@ namespace MediaBrowser.Server.Implementations.Dto
 
             if (user != null)
             {
-                await AttachUserSpecificInfo(dto, item, user, fields).ConfigureAwait(false);
+                await AttachUserSpecificInfo(dto, item, user, options).ConfigureAwait(false);
             }
 
             var hasMediaSources = item as IHasMediaSources;
@@ -408,12 +408,19 @@ namespace MediaBrowser.Server.Implementations.Dto
 
         private void SetItemByNameInfo(BaseItem item, BaseItemDto dto, List<BaseItem> taggedItems, User user = null)
         {
-            if (item is MusicArtist || item is MusicGenre)
+            if (item is MusicArtist)
             {
                 dto.AlbumCount = taggedItems.Count(i => i is MusicAlbum);
                 dto.MusicVideoCount = taggedItems.Count(i => i is MusicVideo);
                 dto.SongCount = taggedItems.Count(i => i is Audio);
             }
+            else if (item is MusicGenre)
+            {
+                dto.ArtistCount = taggedItems.Count(i => i is MusicArtist);
+                dto.AlbumCount = taggedItems.Count(i => i is MusicAlbum);
+                dto.MusicVideoCount = taggedItems.Count(i => i is MusicVideo);
+                dto.SongCount = taggedItems.Count(i => i is Audio);
+            }
             else if (item is GameGenre)
             {
                 dto.GameCount = taggedItems.Count(i => i is Game);
@@ -422,6 +429,7 @@ namespace MediaBrowser.Server.Implementations.Dto
             {
                 // This populates them all and covers Genre, Person, Studio, Year
 
+                dto.ArtistCount = taggedItems.Count(i => i is MusicArtist);
                 dto.AlbumCount = taggedItems.Count(i => i is MusicAlbum);
                 dto.EpisodeCount = taggedItems.Count(i => i is Episode);
                 dto.GameCount = taggedItems.Count(i => i is Game);
@@ -438,19 +446,20 @@ namespace MediaBrowser.Server.Implementations.Dto
         /// <summary>
         /// Attaches the user specific info.
         /// </summary>
-        /// <param name="dto">The dto.</param>
-        /// <param name="item">The item.</param>
-        /// <param name="user">The user.</param>
-        /// <param name="fields">The fields.</param>
-        private async Task AttachUserSpecificInfo(BaseItemDto dto, BaseItem item, User user, List<ItemFields> fields)
+        private async Task AttachUserSpecificInfo(BaseItemDto dto, BaseItem item, User user, DtoOptions dtoOptions)
         {
+            var fields = dtoOptions.Fields;
+
             if (item.IsFolder)
             {
                 var folder = (Folder)item;
 
-                dto.UserData = await _userDataRepository.GetUserDataDto(item, dto, user).ConfigureAwait(false);
+                if (dtoOptions.EnableUserData)
+                {
+                    dto.UserData = await _userDataRepository.GetUserDataDto(item, dto, user).ConfigureAwait(false);
+                }
 
-                if (item.SourceType == SourceType.Library)
+                if (!dto.ChildCount.HasValue && item.SourceType == SourceType.Library)
                 {
                     dto.ChildCount = GetChildCount(folder, user);
                 }
@@ -468,7 +477,10 @@ namespace MediaBrowser.Server.Implementations.Dto
 
             else
             {
-                dto.UserData = _userDataRepository.GetUserDataDto(item, user).Result;
+                if (dtoOptions.EnableUserData)
+                {
+                    dto.UserData = _userDataRepository.GetUserDataDto(item, user).Result;
+                }
             }
 
             dto.PlayAccess = item.GetPlayAccess(user);
@@ -476,7 +488,10 @@ namespace MediaBrowser.Server.Implementations.Dto
             if (fields.Contains(ItemFields.BasicSyncInfo) || fields.Contains(ItemFields.SyncInfo))
             {
                 var userCanSync = user != null && user.Policy.EnableSync;
-                dto.SupportsSync = userCanSync && _syncManager.SupportsSync(item);
+                if (userCanSync && _syncManager.SupportsSync(item))
+                {
+                    dto.SupportsSync = true;
+                }
             }
 
             if (fields.Contains(ItemFields.SeasonUserData))
@@ -940,20 +955,23 @@ namespace MediaBrowser.Server.Implementations.Dto
                 dto.Genres = item.Genres;
             }
 
-            dto.ImageTags = new Dictionary<ImageType, string>();
-
-            // Prevent implicitly captured closure
-            var currentItem = item;
-            foreach (var image in currentItem.ImageInfos.Where(i => !currentItem.AllowsMultipleImages(i.Type))
-                .ToList())
+            if (options.EnableImages)
             {
-                if (options.GetImageLimit(image.Type) > 0)
-                {
-                    var tag = GetImageCacheTag(item, image);
+                dto.ImageTags = new Dictionary<ImageType, string>();
 
-                    if (tag != null)
+                // Prevent implicitly captured closure
+                var currentItem = item;
+                foreach (var image in currentItem.ImageInfos.Where(i => !currentItem.AllowsMultipleImages(i.Type))
+                    .ToList())
+                {
+                    if (options.GetImageLimit(image.Type) > 0)
                     {
-                        dto.ImageTags[image.Type] = tag;
+                        var tag = GetImageCacheTag(item, image);
+
+                        if (tag != null)
+                        {
+                            dto.ImageTags[image.Type] = tag;
+                        }
                     }
                 }
             }
@@ -961,7 +979,16 @@ namespace MediaBrowser.Server.Implementations.Dto
             dto.Id = GetDtoId(item);
             dto.IndexNumber = item.IndexNumber;
             dto.ParentIndexNumber = item.ParentIndexNumber;
-            dto.IsFolder = item.IsFolder;
+
+            if (item.IsFolder)
+            {
+                dto.IsFolder = true;
+            }
+            else if (item is IHasMediaSources)
+            {
+                dto.IsFolder = false;
+            }
+
             dto.MediaType = item.MediaType;
             dto.LocationType = item.LocationType;
             if (item.IsHD.HasValue && item.IsHD.Value)
@@ -1503,97 +1530,6 @@ namespace MediaBrowser.Server.Implementations.Dto
             }
         }
 
-        /// <summary>
-        /// Since it can be slow to make all of these calculations independently, this method will provide a way to do them all at once
-        /// </summary>
-        /// <param name="folder">The folder.</param>
-        /// <param name="user">The user.</param>
-        /// <param name="dto">The dto.</param>
-        /// <param name="fields">The fields.</param>
-        /// <param name="syncProgress">The synchronize progress.</param>
-        /// <returns>Task.</returns>
-        private async Task SetSpecialCounts(Folder folder, User user, BaseItemDto dto, List<ItemFields> fields, Dictionary<string, SyncJobItemStatus> syncProgress)
-        {
-            var recursiveItemCount = 0;
-            var unplayed = 0;
-
-            double totalPercentPlayed = 0;
-            double totalSyncPercent = 0;
-
-            var children = await folder.GetItems(new InternalItemsQuery
-            {
-                IsFolder = false,
-                Recursive = true,
-                ExcludeLocationTypes = new[] { LocationType.Virtual },
-                User = user
-
-            }).ConfigureAwait(false);
-
-            // Loop through each recursive child
-            foreach (var child in children.Items)
-            {
-                var userdata = _userDataRepository.GetUserData(user, child);
-
-                recursiveItemCount++;
-
-                var isUnplayed = true;
-
-                // Incrememt totalPercentPlayed
-                if (userdata != null)
-                {
-                    if (userdata.Played)
-                    {
-                        totalPercentPlayed += 100;
-
-                        isUnplayed = false;
-                    }
-                    else if (userdata.PlaybackPositionTicks > 0 && child.RunTimeTicks.HasValue && child.RunTimeTicks.Value > 0)
-                    {
-                        double itemPercent = userdata.PlaybackPositionTicks;
-                        itemPercent /= child.RunTimeTicks.Value;
-                        totalPercentPlayed += itemPercent;
-                    }
-                }
-
-                if (isUnplayed)
-                {
-                    unplayed++;
-                }
-
-                double percent = 0;
-                SyncJobItemStatus syncItemProgress;
-                if (syncProgress.TryGetValue(child.Id.ToString("N"), out syncItemProgress))
-                {
-                    switch (syncItemProgress)
-                    {
-                        case SyncJobItemStatus.Synced:
-                            percent = 100;
-                            break;
-                        case SyncJobItemStatus.Converting:
-                        case SyncJobItemStatus.ReadyToTransfer:
-                        case SyncJobItemStatus.Transferring:
-                            percent = 50;
-                            break;
-                    }
-                }
-                totalSyncPercent += percent;
-            }
-
-            dto.RecursiveItemCount = recursiveItemCount;
-            dto.UserData.UnplayedItemCount = unplayed;
-
-            if (recursiveItemCount > 0)
-            {
-                dto.UserData.PlayedPercentage = totalPercentPlayed / recursiveItemCount;
-
-                var pct = totalSyncPercent / recursiveItemCount;
-                if (pct > 0)
-                {
-                    dto.SyncPercent = pct;
-                }
-            }
-        }
-
         /// <summary>
         /// Attaches the primary image aspect ratio.
         /// </summary>

+ 35 - 15
MediaBrowser.Server.Implementations/FileOrganization/EpisodeFileOrganizer.cs

@@ -43,13 +43,6 @@ namespace MediaBrowser.Server.Implementations.FileOrganization
             _providerManager = providerManager;
         }
 
-        public Task<FileOrganizationResult> OrganizeEpisodeFile(string path, CancellationToken cancellationToken)
-        {
-            var options = _config.GetAutoOrganizeOptions();
-
-            return OrganizeEpisodeFile(path, options, false, cancellationToken);
-        }
-
         public async Task<FileOrganizationResult> OrganizeEpisodeFile(string path, AutoOrganizeOptions options, bool overwriteExisting, CancellationToken cancellationToken)
         {
             _logger.Info("Sorting file {0}", path);
@@ -63,6 +56,8 @@ namespace MediaBrowser.Server.Implementations.FileOrganization
                 FileSize = new FileInfo(path).Length
             };
 
+            try
+            { 
             if (_libraryMonitor.IsPathLocked(path))
             {
                 result.Status = FileSortingStatus.Failure;
@@ -148,6 +143,12 @@ namespace MediaBrowser.Server.Implementations.FileOrganization
             }
 
             await _organizationService.SaveResult(result, CancellationToken.None).ConfigureAwait(false);
+            }
+            catch (Exception ex)
+            {
+                result.Status = FileSortingStatus.Failure;
+                result.StatusMessage = ex.Message;
+            }
 
             return result;
         }
@@ -156,6 +157,8 @@ namespace MediaBrowser.Server.Implementations.FileOrganization
         {
             var result = _organizationService.GetResult(request.ResultId);
 
+            try
+            { 
             Series series = null;
 
             if (request.NewSeriesProviderIds.Count > 0)
@@ -207,6 +210,12 @@ namespace MediaBrowser.Server.Implementations.FileOrganization
                 cancellationToken).ConfigureAwait(false);
 
             await _organizationService.SaveResult(result, CancellationToken.None).ConfigureAwait(false);
+            }
+            catch (Exception ex)
+            {
+                result.Status = FileSortingStatus.Failure;
+                result.StatusMessage = ex.Message;
+            }
 
             return result;
         }
@@ -263,16 +272,15 @@ namespace MediaBrowser.Server.Implementations.FileOrganization
 
             var originalExtractedSeriesString = result.ExtractedName;
 
+            try
+            {
             // Proceed to sort the file
             var newPath = await GetNewPath(sourcePath, series, seasonNumber, episodeNumber, endingEpiosdeNumber, premiereDate, options.TvOptions, cancellationToken).ConfigureAwait(false);
 
             if (string.IsNullOrEmpty(newPath))
             {
                 var msg = string.Format("Unable to sort {0} because target path could not be determined.", sourcePath);
-                result.Status = FileSortingStatus.Failure;
-                result.StatusMessage = msg;
-                _logger.Warn(msg);
-                return;
+                throw new Exception(msg);
             }
 
             _logger.Info("Sorting file {0} to new path {1}", sourcePath, newPath);
@@ -347,6 +355,14 @@ namespace MediaBrowser.Server.Implementations.FileOrganization
                     }
                 }
             }
+            }
+            catch (Exception ex)
+            {
+                result.Status = FileSortingStatus.Failure;
+                result.StatusMessage = ex.Message;
+                _logger.Warn(ex.Message);
+                return;
+            }
 
             if (rememberCorrection)
             {
@@ -505,7 +521,7 @@ namespace MediaBrowser.Server.Implementations.FileOrganization
             }
             catch (Exception ex)
             {
-                var errorMsg = string.Format("Failed to move file from {0} to {1}", result.OriginalPath, result.TargetPath);
+                var errorMsg = string.Format("Failed to move file from {0} to {1}: {2}", result.OriginalPath, result.TargetPath, ex.Message);
 
                 result.Status = FileSortingStatus.Failure;
                 result.StatusMessage = errorMsg;
@@ -616,7 +632,7 @@ namespace MediaBrowser.Server.Implementations.FileOrganization
             {
                 var msg = string.Format("No provider metadata found for {0} season {1} episode {2}", series.Name, seasonNumber, episodeNumber);
                 _logger.Warn(msg);
-                return null;
+                throw new Exception(msg);
             }
 
             var episodeName = episode.Name;
@@ -715,6 +731,11 @@ namespace MediaBrowser.Server.Implementations.FileOrganization
 
             var pattern = endingEpisodeNumber.HasValue ? options.MultiEpisodeNamePattern : options.EpisodeNamePattern;
 
+            if (string.IsNullOrWhiteSpace(pattern))
+            {
+                throw new Exception("GetEpisodeFileName: Configured episode name pattern is empty!");
+            }
+
             var result = pattern.Replace("%sn", seriesName)
                 .Replace("%s.n", seriesName.Replace(" ", "."))
                 .Replace("%s_n", seriesName.Replace(" ", "_"))
@@ -759,8 +780,7 @@ namespace MediaBrowser.Server.Implementations.FileOrganization
                 // There may be cases where reducing the title length may still not be sufficient to
                 // stay below maxLength
                 var msg = string.Format("Unable to generate an episode file name shorter than {0} characters to constrain to the max path limit", maxLength);
-                _logger.Warn(msg);
-                return string.Empty;
+                throw new Exception(msg);
             }
 
             return result;

+ 12 - 2
MediaBrowser.Server.Implementations/FileOrganization/FileOrganizationService.cs

@@ -112,8 +112,13 @@ namespace MediaBrowser.Server.Implementations.FileOrganization
             var organizer = new EpisodeFileOrganizer(this, _config, _fileSystem, _logger, _libraryManager,
                 _libraryMonitor, _providerManager);
 
-            await organizer.OrganizeEpisodeFile(result.OriginalPath, GetAutoOrganizeOptions(), true, CancellationToken.None)
+            var organizeResult = await organizer.OrganizeEpisodeFile(result.OriginalPath, GetAutoOrganizeOptions(), true, CancellationToken.None)
                     .ConfigureAwait(false);
+
+            if (organizeResult.Status != FileSortingStatus.Success)
+            {
+                throw new Exception(result.StatusMessage);
+            }
         }
 
         public Task ClearLog()
@@ -126,7 +131,12 @@ namespace MediaBrowser.Server.Implementations.FileOrganization
             var organizer = new EpisodeFileOrganizer(this, _config, _fileSystem, _logger, _libraryManager,
                 _libraryMonitor, _providerManager);
 
-            await organizer.OrganizeWithCorrection(request, GetAutoOrganizeOptions(), CancellationToken.None).ConfigureAwait(false);
+            var result = await organizer.OrganizeWithCorrection(request, GetAutoOrganizeOptions(), CancellationToken.None).ConfigureAwait(false);
+
+            if (result.Status != FileSortingStatus.Success)
+            {
+                throw new Exception(result.StatusMessage);
+            }
         }
 
         public QueryResult<SmartMatchInfo> GetSmartMatchInfos(FileOrganizationResultQuery query)

+ 19 - 16
MediaBrowser.Server.Implementations/HttpServer/AsyncStreamWriterFunc.cs → MediaBrowser.Server.Implementations/HttpServer/AsyncStreamWriter.cs

@@ -4,38 +4,41 @@ using System.IO;
 using System.Threading.Tasks;
 using ServiceStack;
 using ServiceStack.Web;
+using MediaBrowser.Controller.Net;
 
 namespace MediaBrowser.Server.Implementations.HttpServer
 {
-    public class AsyncStreamWriterFunc : IStreamWriter, IAsyncStreamWriter, IHasOptions
+    public class AsyncStreamWriter : 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; }
+        private IAsyncStreamSource _source;
 
         public Action OnComplete { get; set; }
         public Action OnError { get; set; }
 
         /// <summary>
-        /// Initializes a new instance of the <see cref="StreamWriter" /> class.
+        /// Initializes a new instance of the <see cref="AsyncStreamWriter" /> class.
         /// </summary>
-        public AsyncStreamWriterFunc(Func<Stream, Task> writer, IDictionary<string, string> headers)
+        public AsyncStreamWriter(IAsyncStreamSource source)
         {
-            Writer = writer;
+            _source = source;
+        }
 
-            if (headers == null)
+        public IDictionary<string, string> Options
+        {
+            get
             {
-                headers = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+                var hasOptions = _source as IHasOptions;
+                if (hasOptions != null)
+                {
+                    return hasOptions.Options;
+                }
+
+                return new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
             }
-            Options = headers;
         }
 
         /// <summary>
@@ -44,13 +47,13 @@ namespace MediaBrowser.Server.Implementations.HttpServer
         /// <param name="responseStream">The response stream.</param>
         public void WriteTo(Stream responseStream)
         {
-            var task = Writer(responseStream);
+            var task = _source.WriteToAsync(responseStream);
             Task.WaitAll(task);
         }
 
         public async Task WriteToAsync(Stream responseStream)
         {
-            await Writer(responseStream).ConfigureAwait(false);
+            await _source.WriteToAsync(responseStream).ConfigureAwait(false);
         }
     }
 }

+ 50 - 0
MediaBrowser.Server.Implementations/HttpServer/HttpListenerHost.cs

@@ -331,6 +331,46 @@ namespace MediaBrowser.Server.Implementations.HttpServer
             return url;
         }
 
+        private string NormalizeConfiguredLocalAddress(string address)
+        {
+            var index = address.Trim('/').IndexOf('/');
+
+            if (index != -1)
+            {
+                address = address.Substring(index + 1);
+            }
+
+            return address.Trim('/');
+        }
+
+        private bool ValidateHost(Uri url)
+        {
+            var hosts = _config
+                .Configuration
+                .LocalNetworkAddresses
+                .Select(NormalizeConfiguredLocalAddress)
+                .ToList();
+
+            if (hosts.Count == 0)
+            {
+                return true;
+            }
+
+            var host = url.Host ?? string.Empty;
+
+            _logger.Debug("Validating host {0}", host);
+
+            if (_networkManager.IsInPrivateAddressSpace(host))
+            {
+                hosts.Add("localhost");
+                hosts.Add("127.0.0.1");
+
+                return hosts.Any(i => host.IndexOf(i, StringComparison.OrdinalIgnoreCase) != -1);
+            }
+
+            return true;
+        }
+
         /// <summary>
         /// Overridable method that can be used to implement a custom hnandler
         /// </summary>
@@ -350,6 +390,16 @@ namespace MediaBrowser.Server.Implementations.HttpServer
                 return ;
             }
 
+            if (!ValidateHost(url))
+            {
+                httpRes.StatusCode = 400;
+                httpRes.ContentType = "text/plain";
+                httpRes.Write("Invalid host");
+
+                httpRes.Close();
+                return;
+            }
+
             var operationName = httpReq.OperationName;
             var localPath = url.LocalPath;
 

+ 4 - 4
MediaBrowser.Server.Implementations/HttpServer/HttpResultFactory.cs

@@ -275,7 +275,7 @@ namespace MediaBrowser.Server.Implementations.HttpServer
         /// <returns>System.Object.</returns>
         private object GetCachedResult(IRequest requestContext, IDictionary<string, string> responseHeaders, Guid cacheKey, string cacheKeyString, DateTime? lastDateModified, TimeSpan? cacheDuration, string contentType)
         {
-            responseHeaders["ETag"] = cacheKeyString;
+            responseHeaders["ETag"] = string.Format("\"{0}\"", cacheKeyString);
 
             if (IsNotModified(requestContext, cacheKey, lastDateModified, cacheDuration))
             {
@@ -534,7 +534,7 @@ namespace MediaBrowser.Server.Implementations.HttpServer
             if (lastDateModified.HasValue && (string.IsNullOrEmpty(cacheKey) || cacheDuration.HasValue))
             {
                 AddAgeHeader(responseHeaders, lastDateModified);
-                responseHeaders["LastModified"] = lastDateModified.Value.ToString("r");
+                responseHeaders["Last-Modified"] = lastDateModified.Value.ToString("r");
             }
 
             if (cacheDuration.HasValue)
@@ -704,9 +704,9 @@ namespace MediaBrowser.Server.Implementations.HttpServer
             throw error;
         }
 
-        public object GetAsyncStreamWriter(Func<Stream, Task> streamWriter, IDictionary<string, string> responseHeaders = null)
+        public object GetAsyncStreamWriter(IAsyncStreamSource streamSource)
         {
-            return new AsyncStreamWriterFunc(streamWriter, responseHeaders);
+            return new AsyncStreamWriter(streamSource);
         }
     }
 }

+ 12 - 0
MediaBrowser.Server.Implementations/IO/FileRefresher.cs

@@ -61,6 +61,11 @@ namespace MediaBrowser.Server.Implementations.IO
 
         public void RestartTimer()
         {
+            if (_disposed)
+            {
+                return;
+            }
+
             lock (_timerLock)
             {
                 if (_timer == null)
@@ -254,6 +259,11 @@ namespace MediaBrowser.Server.Implementations.IO
                 // File may have been deleted
                 return false;
             }
+            catch (UnauthorizedAccessException)
+            {
+                Logger.Debug("No write permission for: {0}.", path);
+                return false;
+            }
             catch (IOException)
             {
                 //the file is unavailable because it is:
@@ -281,8 +291,10 @@ namespace MediaBrowser.Server.Implementations.IO
             }
         }
 
+        private bool _disposed;
         public void Dispose()
         {
+            _disposed = true;
             DisposeTimer();
         }
     }

部分文件因文件數量過多而無法顯示