Selaa lähdekoodia

Merge branch 'dev' of https://github.com/hatharry/Emby.git

hatharry 9 vuotta sitten
vanhempi
sitoutus
cabf2cdc1b
100 muutettua tiedostoa jossa 1749 lisäystä ja 719 poistoa
  1. 10 38
      Emby.Drawing/GDI/DynamicImageHelpers.cs
  2. 5 3
      Emby.Drawing/GDI/GDIImageEncoder.cs
  3. 21 9
      MediaBrowser.Api/ApiEntryPoint.cs
  4. 4 0
      MediaBrowser.Api/BaseApiService.cs
  5. 1 0
      MediaBrowser.Api/IHasDtoOptions.cs
  6. 1 3
      MediaBrowser.Api/Images/ImageService.cs
  7. 5 2
      MediaBrowser.Api/ItemRefreshService.cs
  8. 1 1
      MediaBrowser.Api/ItemUpdateService.cs
  9. 8 6
      MediaBrowser.Api/Library/FileOrganizationService.cs
  10. 27 5
      MediaBrowser.Api/Library/LibraryStructureService.cs
  11. 27 5
      MediaBrowser.Api/LiveTv/LiveTvService.cs
  12. 2 1
      MediaBrowser.Api/Movies/MoviesService.cs
  13. 291 71
      MediaBrowser.Api/Playback/BaseStreamingService.cs
  14. 0 6
      MediaBrowser.Api/Playback/Hls/BaseHlsService.cs
  15. 1 2
      MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs
  16. 14 0
      MediaBrowser.Api/Playback/MediaInfoService.cs
  17. 49 26
      MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs
  18. 46 21
      MediaBrowser.Api/Playback/Progressive/ProgressiveStreamWriter.cs
  19. 3 3
      MediaBrowser.Api/Playback/Progressive/VideoService.cs
  20. 1 2
      MediaBrowser.Api/Playback/StreamRequest.cs
  21. 5 2
      MediaBrowser.Api/Playback/StreamState.cs
  22. 13 1
      MediaBrowser.Api/PlaylistService.cs
  23. 1 1
      MediaBrowser.Api/PluginService.cs
  24. 0 2
      MediaBrowser.Api/Reports/ReportsService.cs
  25. 13 1
      MediaBrowser.Api/SimilarItemsHelper.cs
  26. 1 1
      MediaBrowser.Api/StartupWizardService.cs
  27. 15 2
      MediaBrowser.Api/Sync/SyncHelper.cs
  28. 4 2
      MediaBrowser.Api/Sync/SyncService.cs
  29. 21 33
      MediaBrowser.Api/TvShowsService.cs
  30. 0 2
      MediaBrowser.Api/UserLibrary/ArtistsService.cs
  31. 5 4
      MediaBrowser.Api/UserLibrary/BaseItemsByNameService.cs
  32. 3 0
      MediaBrowser.Api/UserLibrary/BaseItemsRequest.cs
  33. 0 2
      MediaBrowser.Api/UserLibrary/GameGenresService.cs
  34. 0 1
      MediaBrowser.Api/UserLibrary/GenresService.cs
  35. 2 27
      MediaBrowser.Api/UserLibrary/ItemsService.cs
  36. 0 2
      MediaBrowser.Api/UserLibrary/MusicGenresService.cs
  37. 32 2
      MediaBrowser.Api/UserLibrary/UserLibraryService.cs
  38. 13 5
      MediaBrowser.Api/VideosService.cs
  39. 1 1
      MediaBrowser.Common.Implementations/MediaBrowser.Common.Implementations.csproj
  40. 0 11
      MediaBrowser.Common.Implementations/ScheduledTasks/ScheduledTaskWorker.cs
  41. 13 3
      MediaBrowser.Common.Implementations/Serialization/XmlSerializer.cs
  42. 63 1
      MediaBrowser.Common.Implementations/Updates/GithubUpdater.cs
  43. 1 1
      MediaBrowser.Common.Implementations/packages.config
  44. 2 2
      MediaBrowser.Common/IO/StreamDefaults.cs
  45. 1 1
      MediaBrowser.Common/Plugins/BasePlugin.cs
  46. 1 4
      MediaBrowser.Controller/Channels/Channel.cs
  47. 4 2
      MediaBrowser.Controller/Channels/ChannelMediaInfo.cs
  48. 4 0
      MediaBrowser.Controller/Dto/DtoOptions.cs
  49. 5 16
      MediaBrowser.Controller/Dto/IDtoService.cs
  50. 49 12
      MediaBrowser.Controller/Entities/AggregateFolder.cs
  51. 8 1
      MediaBrowser.Controller/Entities/Audio/Audio.cs
  52. 51 6
      MediaBrowser.Controller/Entities/Audio/MusicArtist.cs
  53. 45 6
      MediaBrowser.Controller/Entities/Audio/MusicGenre.cs
  54. 43 10
      MediaBrowser.Controller/Entities/BaseItem.cs
  55. 1 1
      MediaBrowser.Controller/Entities/Book.cs
  56. 73 1
      MediaBrowser.Controller/Entities/CollectionFolder.cs
  57. 41 27
      MediaBrowser.Controller/Entities/Folder.cs
  58. 1 1
      MediaBrowser.Controller/Entities/Game.cs
  59. 45 5
      MediaBrowser.Controller/Entities/GameGenre.cs
  60. 45 6
      MediaBrowser.Controller/Entities/Genre.cs
  61. 1 2
      MediaBrowser.Controller/Entities/IHasImages.cs
  62. 11 2
      MediaBrowser.Controller/Entities/IHasMetadata.cs
  63. 1 2
      MediaBrowser.Controller/Entities/IItemByName.cs
  64. 2 0
      MediaBrowser.Controller/Entities/InternalItemsQuery.cs
  65. 2 0
      MediaBrowser.Controller/Entities/InternalPeopleQuery.cs
  66. 35 1
      MediaBrowser.Controller/Entities/Movies/BoxSet.cs
  67. 10 0
      MediaBrowser.Controller/Entities/Movies/Movie.cs
  68. 0 1
      MediaBrowser.Controller/Entities/MusicVideo.cs
  69. 60 6
      MediaBrowser.Controller/Entities/Person.cs
  70. 1 1
      MediaBrowser.Controller/Entities/Photo.cs
  71. 45 6
      MediaBrowser.Controller/Entities/Studio.cs
  72. 25 21
      MediaBrowser.Controller/Entities/TV/Episode.cs
  73. 29 41
      MediaBrowser.Controller/Entities/TV/Season.cs
  74. 135 88
      MediaBrowser.Controller/Entities/TV/Series.cs
  75. 10 2
      MediaBrowser.Controller/Entities/Trailer.cs
  76. 1 1
      MediaBrowser.Controller/Entities/User.cs
  77. 39 3
      MediaBrowser.Controller/Entities/UserRootFolder.cs
  78. 17 66
      MediaBrowser.Controller/Entities/UserViewBuilder.cs
  79. 16 11
      MediaBrowser.Controller/Entities/Video.cs
  80. 43 0
      MediaBrowser.Controller/Entities/Year.cs
  81. 23 1
      MediaBrowser.Controller/FileOrganization/IFileOrganizationService.cs
  82. 2 0
      MediaBrowser.Controller/IServerApplicationPaths.cs
  83. 12 14
      MediaBrowser.Controller/Library/ILibraryManager.cs
  84. 9 0
      MediaBrowser.Controller/Library/ItemResolveArgs.cs
  85. 2 3
      MediaBrowser.Controller/LiveTv/ILiveTvManager.cs
  86. 0 1
      MediaBrowser.Controller/LiveTv/LiveTvProgram.cs
  87. 0 1
      MediaBrowser.Controller/LiveTv/LiveTvVideoRecording.cs
  88. 1 7
      MediaBrowser.Controller/LiveTv/TimerEventInfo.cs
  89. 1 7
      MediaBrowser.Controller/LiveTv/TunerChannelMapping.cs
  90. 1 0
      MediaBrowser.Controller/MediaBrowser.Controller.csproj
  91. 1 2
      MediaBrowser.Controller/MediaEncoding/EncodingJobOptions.cs
  92. 2 1
      MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs
  93. 6 1
      MediaBrowser.Controller/MediaEncoding/MediaEncoderHelpers.cs
  94. 18 0
      MediaBrowser.Controller/Net/IAsyncStreamSource.cs
  95. 1 1
      MediaBrowser.Controller/Net/IHttpResultFactory.cs
  96. 7 0
      MediaBrowser.Controller/Persistence/IItemRepository.cs
  97. 12 0
      MediaBrowser.Controller/Playlists/Playlist.cs
  98. 9 0
      MediaBrowser.Controller/Providers/BaseItemXmlParser.cs
  99. 15 13
      MediaBrowser.Controller/Providers/DirectoryService.cs
  100. 2 1
      MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs

+ 10 - 38
Emby.Drawing/GDI/DynamicImageHelpers.cs

@@ -33,7 +33,9 @@ namespace Emby.Drawing.GDI
                     graphics.SmoothingMode = SmoothingMode.HighQuality;
                     graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
                     graphics.PixelOffsetMode = PixelOffsetMode.HighQuality;
-                    graphics.CompositingMode = CompositingMode.SourceCopy;
+
+                    // SourceCopy causes the image to be blank in OSX
+                    //graphics.CompositingMode = CompositingMode.SourceCopy;
 
                     for (var row = 0; row < rows; row++)
                     {
@@ -44,19 +46,9 @@ namespace Emby.Drawing.GDI
 
                             if (files.Count > index)
                             {
-                                using (var fileStream = fileSystem.GetFileStream(files[index], FileMode.Open, FileAccess.Read, FileShare.Read, true))
+                                using (var imgtemp = Image.FromFile(files[index]))
                                 {
-                                    using (var memoryStream = new MemoryStream())
-                                    {
-                                        fileStream.CopyTo(memoryStream);
-
-                                        memoryStream.Position = 0;
-
-                                        using (var imgtemp = Image.FromStream(memoryStream, true, false))
-                                        {
-                                            graphics.DrawImage(imgtemp, x, y, cellWidth, cellHeight);
-                                        }
-                                    }
+                                    graphics.DrawImage(imgtemp, x, y, cellWidth, cellHeight);
                                 }
                             }
 
@@ -90,7 +82,9 @@ namespace Emby.Drawing.GDI
                     graphics.SmoothingMode = SmoothingMode.HighQuality;
                     graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
                     graphics.PixelOffsetMode = PixelOffsetMode.HighQuality;
-                    graphics.CompositingMode = CompositingMode.SourceCopy;
+
+                    // SourceCopy causes the image to be blank in OSX
+                    //graphics.CompositingMode = CompositingMode.SourceCopy;
 
                     for (var row = 0; row < rows; row++)
                     {
@@ -99,21 +93,10 @@ namespace Emby.Drawing.GDI
                             var x = col * singleSize;
                             var y = row * singleSize;
 
-                            using (var fileStream = fileSystem.GetFileStream(files[index], FileMode.Open, FileAccess.Read, FileShare.Read, true))
+                            using (var imgtemp = Image.FromFile(files[index]))
                             {
-                                using (var memoryStream = new MemoryStream())
-                                {
-                                    fileStream.CopyTo(memoryStream);
-
-                                    memoryStream.Position = 0;
-
-                                    using (var imgtemp = Image.FromStream(memoryStream, true, false))
-                                    {
-                                        graphics.DrawImage(imgtemp, x, y, singleSize, singleSize);
-                                    }
-                                }
+                                graphics.DrawImage(imgtemp, x, y, singleSize, singleSize);
                             }
-
                             index++;
                         }
                     }
@@ -121,16 +104,5 @@ namespace Emby.Drawing.GDI
                 }
             }
         }
-
-        private static Stream GetStream(Image image)
-        {
-            var ms = new MemoryStream();
-
-            image.Save(ms, ImageFormat.Png);
-
-            ms.Position = 0;
-
-            return ms;
-        }
     }
 }

+ 5 - 3
Emby.Drawing/GDI/GDIImageEncoder.cs

@@ -119,9 +119,11 @@ namespace Emby.Drawing.GDI
                         thumbnailGraph.SmoothingMode = SmoothingMode.HighQuality;
                         thumbnailGraph.InterpolationMode = InterpolationMode.HighQualityBicubic;
                         thumbnailGraph.PixelOffsetMode = PixelOffsetMode.HighQuality;
-                        thumbnailGraph.CompositingMode = !hasPostProcessing ?
-                            CompositingMode.SourceCopy :
-                            CompositingMode.SourceOver;
+
+                        // SourceCopy causes the image to be blank in OSX
+                        //thumbnailGraph.CompositingMode = !hasPostProcessing ?
+                        //    CompositingMode.SourceCopy :
+                        //    CompositingMode.SourceOver;
 
                         SetBackgroundColor(thumbnailGraph, options);
 

+ 21 - 9
MediaBrowser.Api/ApiEntryPoint.cs

@@ -63,6 +63,15 @@ namespace MediaBrowser.Api
 
             Instance = this;
             _sessionManager.PlaybackProgress += _sessionManager_PlaybackProgress;
+            _sessionManager.PlaybackStart += _sessionManager_PlaybackStart;
+        }
+
+        private void _sessionManager_PlaybackStart(object sender, PlaybackProgressEventArgs e)
+        {
+            if (!string.IsNullOrWhiteSpace(e.PlaySessionId))
+            {
+                PingTranscodingJob(e.PlaySessionId, e.IsPaused);
+            }
         }
 
         void _sessionManager_PlaybackProgress(object sender, PlaybackProgressEventArgs e)
@@ -126,9 +135,10 @@ namespace MediaBrowser.Api
         /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
         protected virtual void Dispose(bool dispose)
         {
-            var jobCount = _activeTranscodingJobs.Count;
+            var list = _activeTranscodingJobs.ToList();
+            var jobCount = list.Count;
 
-            Parallel.ForEach(_activeTranscodingJobs.ToList(), j => KillTranscodingJob(j, false, path => true));
+            Parallel.ForEach(list, j => KillTranscodingJob(j, false, path => true));
 
             // Try to allow for some time to kill the ffmpeg processes and delete the partial stream files
             if (jobCount > 0)
@@ -182,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;
 
@@ -198,6 +208,7 @@ namespace MediaBrowser.Api
                 job.CompletionPercentage = percentComplete;
                 job.TranscodingPositionTicks = ticks;
                 job.BytesTranscoded = bytesTranscoded;
+                job.BitRate = bitRate;
             }
 
             var deviceId = state.Request.DeviceId;
@@ -209,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,
@@ -348,7 +359,7 @@ namespace MediaBrowser.Api
                 return;
             }
 
-            var timerDuration = 1000;
+            var timerDuration = 10000;
 
             if (job.Type != TranscodingJobType.Progressive)
             {
@@ -400,7 +411,7 @@ namespace MediaBrowser.Api
                 }
             }
 
-            Logger.Debug("Transcoding kill timer stopped for JobId {0} PlaySessionId {1}. Killing transcoding", job.Id, job.PlaySessionId);
+            Logger.Info("Transcoding kill timer stopped for JobId {0} PlaySessionId {1}. Killing transcoding", job.Id, job.PlaySessionId);
 
             KillTranscodingJob(job, true, path => true);
         }
@@ -558,13 +569,13 @@ namespace MediaBrowser.Api
             {
 
             }
-            catch (IOException ex)
+            catch (IOException)
             {
                 //Logger.ErrorException("Error deleting partial stream file(s) {0}", ex, path);
 
                 DeletePartialStreamFiles(path, jobType, retryCount + 1, 500);
             }
-            catch (Exception ex)
+            catch
             {
                 //Logger.ErrorException("Error deleting partial stream file(s) {0}", ex, path);
             }
@@ -684,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);
             }

+ 5 - 2
MediaBrowser.Api/ItemRefreshService.cs

@@ -5,6 +5,7 @@ using MediaBrowser.Controller.Providers;
 using ServiceStack;
 using System.Threading;
 using CommonIO;
+using MediaBrowser.Model.Logging;
 
 namespace MediaBrowser.Api
 {
@@ -39,12 +40,14 @@ namespace MediaBrowser.Api
         private readonly ILibraryManager _libraryManager;
         private readonly IProviderManager _providerManager;
         private readonly IFileSystem _fileSystem;
+        private readonly ILogger _logger;
 
-        public ItemRefreshService(ILibraryManager libraryManager, IProviderManager providerManager, IFileSystem fileSystem)
+        public ItemRefreshService(ILibraryManager libraryManager, IProviderManager providerManager, IFileSystem fileSystem, ILogger logger)
         {
             _libraryManager = libraryManager;
             _providerManager = providerManager;
             _fileSystem = fileSystem;
+            _logger = logger;
         }
 
         /// <summary>
@@ -69,7 +72,7 @@ namespace MediaBrowser.Api
 
         private MetadataRefreshOptions GetRefreshOptions(BaseRefreshRequest request)
         {
-            return new MetadataRefreshOptions(new DirectoryService(_fileSystem))
+            return new MetadataRefreshOptions(new DirectoryService(_logger, _fileSystem))
             {
                 MetadataRefreshMode = request.MetadataRefreshMode,
                 ImageRefreshMode = request.ImageRefreshMode,

+ 1 - 1
MediaBrowser.Api/ItemUpdateService.cs

@@ -304,7 +304,7 @@ namespace MediaBrowser.Api
             item.EndDate = request.EndDate.HasValue ? NormalizeDateTime(request.EndDate.Value) : (DateTime?)null;
             item.PremiereDate = request.PremiereDate.HasValue ? NormalizeDateTime(request.PremiereDate.Value) : (DateTime?)null;
             item.ProductionYear = request.ProductionYear;
-            item.OfficialRating = request.OfficialRating;
+            item.OfficialRating = string.IsNullOrWhiteSpace(request.OfficialRating) ? null : request.OfficialRating;
             item.CustomRating = request.CustomRating;
 
             SetProductionLocations(item, request);

+ 8 - 6
MediaBrowser.Api/Library/FileOrganizationService.cs

@@ -154,9 +154,12 @@ namespace MediaBrowser.Api.Library
 
         public void Post(PerformOrganization request)
         {
+            // Don't await this
             var task = _iFileOrganizationService.PerformOrganization(request.Id);
 
-            Task.WaitAll(task);
+            // Async processing (close dialog early instead of waiting until the file has been copied)
+            // Wait 2s for exceptions that may occur to have them forwarded to the client for immediate error display
+            task.Wait(2000);
         }
 
         public void Post(OrganizeEpisode request)
@@ -168,6 +171,7 @@ namespace MediaBrowser.Api.Library
                 dicNewProviderIds = request.NewSeriesProviderIds;
             }
 
+            // Don't await this
             var task = _iFileOrganizationService.PerformEpisodeOrganization(new EpisodeFileOrganizationRequest
             {
                 EndingEpisodeNumber = request.EndingEpisodeNumber,
@@ -182,11 +186,9 @@ namespace MediaBrowser.Api.Library
                 TargetFolder = request.TargetFolder
             });
 
-            // For async processing (close dialog early instead of waiting until the file has been copied)
-            //var tasks = new Task[] { task };
-            //Task.WaitAll(tasks, 8000);
-
-            Task.WaitAll(task);
+            // Async processing (close dialog early instead of waiting until the file has been copied)
+            // Wait 2s for exceptions that may occur to have them forwarded to the client for immediate error display
+            task.Wait(2000);
         }
 
         public object Get(GetSmartMatchInfos request)

+ 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
             {

+ 27 - 5
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);
@@ -997,10 +1022,7 @@ namespace MediaBrowser.Api.LiveTv
 
         public async Task<object> Get(GetRecordingGroup request)
         {
-            var result = await _liveTvManager.GetRecordingGroups(new RecordingGroupQuery
-            {
-
-            }, CancellationToken.None).ConfigureAwait(false);
+            var result = await _liveTvManager.GetRecordingGroups(new RecordingGroupQuery(), CancellationToken.None).ConfigureAwait(false);
 
             var group = result.Items.FirstOrDefault(i => string.Equals(i.Id, request.Id, StringComparison.OrdinalIgnoreCase));
 

+ 2 - 1
MediaBrowser.Api/Movies/MoviesService.cs

@@ -192,7 +192,8 @@ namespace MediaBrowser.Api.Movies
                 SortOrder = SortOrder.Descending,
                 Limit = 7,
                 ParentId = parentIdGuid,
-                Recursive = true
+                Recursive = true,
+                IsPlayed = true
             };
 
             var recentlyPlayedMovies = _libraryManager.GetItemList(query).ToList();

+ 291 - 71
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>
@@ -286,28 +291,46 @@ namespace MediaBrowser.Api.Playback
 
         protected string GetH264Encoder(StreamState state)
         {
+            var defaultEncoder = "libx264";
+
             // Only use alternative encoders for video files.
             // When using concat with folder rips, if the mfx session fails to initialize, ffmpeg will be stuck retrying and will not exit gracefully
             // Since transcoding of folder rips is expiremental anyway, it's not worth adding additional variables such as this.
             if (state.VideoType == VideoType.VideoFile)
             {
-                if (string.Equals(ApiEntryPoint.Instance.GetEncodingOptions().HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase) ||
-                    string.Equals(ApiEntryPoint.Instance.GetEncodingOptions().HardwareAccelerationType, "h264_qsv", StringComparison.OrdinalIgnoreCase))
+                var encodingOptions = ApiEntryPoint.Instance.GetEncodingOptions();
+                var hwType = encodingOptions.HardwareAccelerationType;
+
+                if (string.Equals(hwType, "qsv", StringComparison.OrdinalIgnoreCase) ||
+                    string.Equals(hwType, "h264_qsv", StringComparison.OrdinalIgnoreCase))
                 {
-                    return "h264_qsv";
+                    return GetAvailableEncoder("h264_qsv", defaultEncoder);
                 }
 
-                if (string.Equals(ApiEntryPoint.Instance.GetEncodingOptions().HardwareAccelerationType, "nvenc", StringComparison.OrdinalIgnoreCase))
+                if (string.Equals(hwType, "nvenc", StringComparison.OrdinalIgnoreCase))
+                {
+                    return GetAvailableEncoder("h264_nvenc", defaultEncoder);
+                }
+                if (string.Equals(hwType, "h264_omx", StringComparison.OrdinalIgnoreCase))
                 {
-                    return "h264_nvenc";
+                    return GetAvailableEncoder("h264_omx", defaultEncoder);
                 }
-                if (string.Equals(ApiEntryPoint.Instance.GetEncodingOptions().HardwareAccelerationType, "h264_omx", StringComparison.OrdinalIgnoreCase))
+                if (string.Equals(hwType, "vaapi", StringComparison.OrdinalIgnoreCase) && !string.IsNullOrWhiteSpace(encodingOptions.VaapiDevice))
                 {
-                    return "h264_omx";
+                    return GetAvailableEncoder("h264_vaapi", defaultEncoder);
                 }
             }
 
-            return "libx264";
+            return defaultEncoder;
+        }
+
+        private string GetAvailableEncoder(string preferredEncoder, string defaultEncoder)
+        {
+            if (MediaEncoder.SupportsEncoder(preferredEncoder))
+            {
+                return preferredEncoder;
+            }
+            return defaultEncoder;
         }
 
         /// <summary>
@@ -409,7 +432,8 @@ namespace MediaBrowser.Api.Playback
 
             if (!string.IsNullOrEmpty(state.VideoRequest.Profile))
             {
-                if (!string.Equals(videoCodec, "h264_omx", StringComparison.OrdinalIgnoreCase))
+                if (!string.Equals(videoCodec, "h264_omx", StringComparison.OrdinalIgnoreCase) &&
+                    !string.Equals(videoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase))
                 {
                     // not supported by h264_omx
                     param += " -profile:v " + state.VideoRequest.Profile;
@@ -464,7 +488,8 @@ namespace MediaBrowser.Api.Playback
 
             if (!string.Equals(videoCodec, "h264_omx", StringComparison.OrdinalIgnoreCase) &&
                 !string.Equals(videoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase) &&
-                !string.Equals(videoCodec, "h264_nvenc", StringComparison.OrdinalIgnoreCase))
+                !string.Equals(videoCodec, "h264_nvenc", StringComparison.OrdinalIgnoreCase) &&
+                !string.Equals(videoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase))
             {
                 param = "-pix_fmt yuv420p " + param;
             }
@@ -530,59 +555,97 @@ namespace MediaBrowser.Api.Playback
 
             var filters = new List<string>();
 
-            if (state.DeInterlace)
+            if (string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase))
+            {
+                filters.Add("format=nv12|vaapi");
+                filters.Add("hwupload");
+            }
+            else if (state.DeInterlace && !string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase))
             {
                 filters.Add("yadif=0:-1:0");
             }
 
-            // If fixed dimensions were supplied
-            if (request.Width.HasValue && request.Height.HasValue)
+            if (string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase))
             {
-                var widthParam = request.Width.Value.ToString(UsCulture);
-                var heightParam = request.Height.Value.ToString(UsCulture);
+                // Work around vaapi's reduced scaling features
+                var scaler = "scale_vaapi";
 
-                filters.Add(string.Format("scale=trunc({0}/2)*2:trunc({1}/2)*2", widthParam, heightParam));
-            }
+                // Given the input dimensions (inputWidth, inputHeight), determine the output dimensions
+                // (outputWidth, outputHeight). The user may request precise output dimensions or maximum
+                // output dimensions. Output dimensions are guaranteed to be even.
+                decimal inputWidth = Convert.ToDecimal(state.VideoStream.Width);
+                decimal inputHeight = Convert.ToDecimal(state.VideoStream.Height);
+                decimal outputWidth = request.Width.HasValue ? Convert.ToDecimal(request.Width.Value) : inputWidth;
+                decimal outputHeight = request.Height.HasValue ? Convert.ToDecimal(request.Height.Value) : inputHeight;
+                decimal maximumWidth = request.MaxWidth.HasValue ? Convert.ToDecimal(request.MaxWidth.Value) : outputWidth;
+                decimal maximumHeight = request.MaxHeight.HasValue ? Convert.ToDecimal(request.MaxHeight.Value) : outputHeight;
 
-            // If Max dimensions were supplied, for width selects lowest even number between input width and width req size and selects lowest even number from in width*display aspect and requested size
-            else if (request.MaxWidth.HasValue && request.MaxHeight.HasValue)
-            {
-                var maxWidthParam = request.MaxWidth.Value.ToString(UsCulture);
-                var maxHeightParam = request.MaxHeight.Value.ToString(UsCulture);
+                if (outputWidth > maximumWidth || outputHeight > maximumHeight)
+                {
+                    var scale = Math.Min(maximumWidth / outputWidth, maximumHeight / outputHeight);
+                    outputWidth = Math.Min(maximumWidth, Math.Truncate(outputWidth * scale));
+                    outputHeight = Math.Min(maximumHeight, Math.Truncate(outputHeight * scale));
+                }
 
-                filters.Add(string.Format("scale=trunc(min(max(iw\\,ih*dar)\\,min({0}\\,{1}*dar))/2)*2:trunc(min(max(iw/dar\\,ih)\\,min({0}/dar\\,{1}))/2)*2", maxWidthParam, maxHeightParam));
-            }
+                outputWidth = 2 * Math.Truncate(outputWidth / 2);
+                outputHeight = 2 * Math.Truncate(outputHeight / 2);
 
-            // If a fixed width was requested
-            else if (request.Width.HasValue)
+                if (outputWidth != inputWidth || outputHeight != inputHeight)
+                {
+                    filters.Add(string.Format("{0}=w={1}:h={2}", scaler, outputWidth.ToString(UsCulture), outputHeight.ToString(UsCulture)));
+                }
+            }
+            else
             {
-                var widthParam = request.Width.Value.ToString(UsCulture);
+                // If fixed dimensions were supplied
+                if (request.Width.HasValue && request.Height.HasValue)
+                {
+                    var widthParam = request.Width.Value.ToString(UsCulture);
+                    var heightParam = request.Height.Value.ToString(UsCulture);
 
-                filters.Add(string.Format("scale={0}:trunc(ow/a/2)*2", widthParam));
-            }
+                    filters.Add(string.Format("scale=trunc({0}/2)*2:trunc({1}/2)*2", widthParam, heightParam));
+                }
 
-            // If a fixed height was requested
-            else if (request.Height.HasValue)
-            {
-                var heightParam = request.Height.Value.ToString(UsCulture);
+                // If Max dimensions were supplied, for width selects lowest even number between input width and width req size and selects lowest even number from in width*display aspect and requested size
+                else if (request.MaxWidth.HasValue && request.MaxHeight.HasValue)
+                {
+                    var maxWidthParam = request.MaxWidth.Value.ToString(UsCulture);
+                    var maxHeightParam = request.MaxHeight.Value.ToString(UsCulture);
 
-                filters.Add(string.Format("scale=trunc(oh*a/2)*2:{0}", heightParam));
-            }
+                    filters.Add(string.Format("scale=trunc(min(max(iw\\,ih*dar)\\,min({0}\\,{1}*dar))/2)*2:trunc(min(max(iw/dar\\,ih)\\,min({0}/dar\\,{1}))/2)*2", maxWidthParam, maxHeightParam));
+                }
 
-            // If a max width was requested
-            else if (request.MaxWidth.HasValue)
-            {
-                var maxWidthParam = request.MaxWidth.Value.ToString(UsCulture);
+                // If a fixed width was requested
+                else if (request.Width.HasValue)
+                {
+                    var widthParam = request.Width.Value.ToString(UsCulture);
 
-                filters.Add(string.Format("scale=trunc(min(max(iw\\,ih*dar)\\,{0})/2)*2:trunc(ow/dar/2)*2", maxWidthParam));
-            }
+                    filters.Add(string.Format("scale={0}:trunc(ow/a/2)*2", widthParam));
+                }
 
-            // If a max height was requested
-            else if (request.MaxHeight.HasValue)
-            {
-                var maxHeightParam = request.MaxHeight.Value.ToString(UsCulture);
+                // If a fixed height was requested
+                else if (request.Height.HasValue)
+                {
+                    var heightParam = request.Height.Value.ToString(UsCulture);
+
+                    filters.Add(string.Format("scale=trunc(oh*a/2)*2:{0}", heightParam));
+                }
+
+                // If a max width was requested
+                else if (request.MaxWidth.HasValue)
+                {
+                    var maxWidthParam = request.MaxWidth.Value.ToString(UsCulture);
+
+                    filters.Add(string.Format("scale=trunc(min(max(iw\\,ih*dar)\\,{0})/2)*2:trunc(ow/dar/2)*2", maxWidthParam));
+                }
+
+                // If a max height was requested
+                else if (request.MaxHeight.HasValue)
+                {
+                    var maxHeightParam = request.MaxHeight.Value.ToString(UsCulture);
 
-                filters.Add(string.Format("scale=trunc(oh*a/2)*2:min(ih\\,{0})", maxHeightParam));
+                    filters.Add(string.Format("scale=trunc(oh*a/2)*2:min(max(iw/dar\\,ih)\\,{0})", maxHeightParam));
+                }
             }
 
             var output = string.Empty;
@@ -917,6 +980,15 @@ namespace MediaBrowser.Api.Playback
                 }
             }
 
+            if (state.VideoRequest != null)
+            {
+                var encodingOptions = ApiEntryPoint.Instance.GetEncodingOptions();
+                if (GetVideoEncoder(state).IndexOf("vaapi", StringComparison.OrdinalIgnoreCase) != -1)
+                {
+                    arg = "-hwaccel vaapi -hwaccel_output_format vaapi -vaapi_device " + encodingOptions.VaapiDevice + " " + arg;
+                }
+            }
+
             return arg.Trim();
         }
 
@@ -1042,14 +1114,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");
@@ -1080,7 +1152,7 @@ namespace MediaBrowser.Api.Playback
             //process.BeginOutputReadLine();
 
             // Important - don't await the log task or we won't be able to kill ffmpeg when the user stops playback
-            Task.Run(() => StartStreamingLog(transcodingJob, state, process.StandardError.BaseStream, state.LogFileStream));
+            var task = Task.Run(() => StartStreamingLog(transcodingJob, state, process.StandardError.BaseStream, state.LogFileStream));
 
             // Wait for the file to exist before proceeeding
             while (!FileSystem.FileExists(state.WaitForPath ?? outputPath) && !transcodingJob.HasExited)
@@ -1099,28 +1171,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)
@@ -1158,6 +1232,7 @@ namespace MediaBrowser.Api.Playback
             double? percent = null;
             TimeSpan? transcodingPosition = null;
             long? bytesTranscoded = null;
+            int? bitRate = null;
 
             var parts = line.Split(' ');
 
@@ -1221,11 +1296,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);
             }
         }
 
@@ -1547,13 +1643,6 @@ namespace MediaBrowser.Api.Playback
                     }
                 }
                 else if (i == 25)
-                {
-                    if (videoRequest != null)
-                    {
-                        videoRequest.ForceLiveStream = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
-                    }
-                }
-                else if (i == 26)
                 {
                     if (!string.IsNullOrWhiteSpace(val) && videoRequest != null)
                     {
@@ -1564,17 +1653,21 @@ namespace MediaBrowser.Api.Playback
                         }
                     }
                 }
-                else if (i == 27)
+                else if (i == 26)
                 {
                     request.TranscodingMaxAudioChannels = int.Parse(val, UsCulture);
                 }
-                else if (i == 28)
+                else if (i == 27)
                 {
                     if (videoRequest != null)
                     {
                         videoRequest.EnableSubtitlesInManifest = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
                     }
                 }
+                else if (i == 28)
+                {
+                    request.Tag = val;
+                }
             }
         }
 
@@ -1788,6 +1881,19 @@ namespace MediaBrowser.Api.Playback
             {
                 state.OutputAudioCodec = "copy";
             }
+            else
+            {
+                // If the user doesn't have access to transcoding, then force stream copy, regardless of whether it will be compatible or not
+                var auth = AuthorizationContext.GetAuthorizationInfo(Request);
+                if (!string.IsNullOrWhiteSpace(auth.UserId))
+                {
+                    var user = UserManager.GetUserById(auth.UserId);
+                    if (!user.Policy.EnableAudioPlaybackTranscoding)
+                    {
+                        state.OutputAudioCodec = "copy";
+                    }
+                }
+            }
         }
 
         private void AttachMediaSourceInfo(StreamState state,
@@ -2159,13 +2265,127 @@ namespace MediaBrowser.Api.Playback
                     if (state.VideoRequest != null)
                     {
                         state.VideoRequest.CopyTimestamps = transcodingProfile.CopyTimestamps;
-                        state.VideoRequest.ForceLiveStream = transcodingProfile.ForceLiveStream;
                         state.VideoRequest.EnableSubtitlesInManifest = transcodingProfile.EnableSubtitlesInManifest;
                     }
                 }
             }
         }
 
+        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 (!MediaEncoder.IsDefaultEncoderPath)
+            {
+                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>

+ 0 - 6
MediaBrowser.Api/Playback/Hls/BaseHlsService.cs

@@ -3,7 +3,6 @@ using MediaBrowser.Controller.Devices;
 using MediaBrowser.Controller.Dlna;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.MediaEncoding;
-using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Extensions;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.Net;
@@ -282,11 +281,6 @@ namespace MediaBrowser.Api.Playback.Hls
         {
             var isLiveStream = (state.RunTimeTicks ?? 0) == 0;
 
-            if (state.VideoRequest.ForceLiveStream)
-            {
-                return true;
-            }
-
             return isLiveStream;
         }
     }

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

@@ -257,8 +257,7 @@ namespace MediaBrowser.Api.Playback.Hls
             return await GetSegmentResult(state, playlistPath, segmentPath, requestedIndex, job, cancellationToken).ConfigureAwait(false);
         }
 
-        // 256k
-        private const int BufferSize = 262144;
+        private const int BufferSize = 81920;
 
         private long GetStartPositionTicks(StreamState state, int requestedIndex)
         {

+ 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) ?

+ 49 - 26
MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs

@@ -142,7 +142,8 @@ namespace MediaBrowser.Api.Playback.Progressive
             var outputPath = state.OutputFilePath;
             var outputPathExists = FileSystem.FileExists(outputPath);
 
-            var isTranscodeCached = outputPathExists && !ApiEntryPoint.Instance.HasActiveTranscodingJob(outputPath, TranscodingJobType.Progressive);
+            var transcodingJob = ApiEntryPoint.Instance.GetTranscodingJob(outputPath, TranscodingJobType.Progressive);
+            var isTranscodeCached = outputPathExists && transcodingJob != null;
 
             AddDlnaHeaders(state, responseHeaders, request.Static || isTranscodeCached);
 
@@ -153,42 +154,64 @@ namespace MediaBrowser.Api.Playback.Progressive
 
                 using (state)
                 {
-                    return await ResultFactory.GetStaticFileResult(Request, new StaticFileResultOptions
-                    {
-                        ResponseHeaders = responseHeaders,
-                        ContentType = contentType,
-                        IsHeadRequest = isHeadRequest,
-                        Path = state.MediaPath
-                    }).ConfigureAwait(false);
-                }
-            }
+                    TimeSpan? cacheDuration = null;
 
-            // Not static but transcode cache file exists
-            if (isTranscodeCached)
-            {
-                var contentType = state.GetMimeType(outputPath);
+                    if (!string.IsNullOrEmpty(request.Tag))
+                    {
+                        cacheDuration = TimeSpan.FromDays(365);
+                    }
 
-                try
-                {
                     return await ResultFactory.GetStaticFileResult(Request, new StaticFileResultOptions
                     {
                         ResponseHeaders = responseHeaders,
                         ContentType = contentType,
                         IsHeadRequest = isHeadRequest,
-                        Path = outputPath
+                        Path = state.MediaPath,
+                        CacheDuration = cacheDuration
+
                     }).ConfigureAwait(false);
                 }
-                finally
-                {
-                    state.Dispose();
-                }
             }
 
+            //// Not static but transcode cache file exists
+            //if (isTranscodeCached && state.VideoRequest == null)
+            //{
+            //    var contentType = state.GetMimeType(outputPath);
+
+            //    try
+            //    {
+            //        if (transcodingJob != null)
+            //        {
+            //            ApiEntryPoint.Instance.OnTranscodeBeginRequest(transcodingJob);
+            //        }
+
+            //        return await ResultFactory.GetStaticFileResult(Request, new StaticFileResultOptions
+            //        {
+            //            ResponseHeaders = responseHeaders,
+            //            ContentType = contentType,
+            //            IsHeadRequest = isHeadRequest,
+            //            Path = outputPath,
+            //            FileShare = FileShare.ReadWrite,
+            //            OnComplete = () =>
+            //            {
+            //                if (transcodingJob != null)
+            //                {
+            //                    ApiEntryPoint.Instance.OnTranscodeEndRequest(transcodingJob);
+            //                }
+            //            }
+
+            //        }).ConfigureAwait(false);
+            //    }
+            //    finally
+            //    {
+            //        state.Dispose();
+            //    }
+            //}
+
             // Need to start ffmpeg
             try
             {
-                return await GetStreamResult(state, responseHeaders, isHeadRequest, cancellationTokenSource)
-                            .ConfigureAwait(false);
+                return await GetStreamResult(state, responseHeaders, isHeadRequest, cancellationTokenSource).ConfigureAwait(false);
             }
             catch
             {
@@ -347,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
             {
@@ -368,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;

+ 46 - 21
MediaBrowser.Api/Playback/Progressive/ProgressiveStreamWriter.cs

@@ -1,59 +1,84 @@
 using MediaBrowser.Model.Logging;
-using ServiceStack.Web;
 using System;
-using System.Collections.Generic;
 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 = 262144;
+        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
         {
-            var eofCount = 0;
+            get
+            {
+                return _outputHeaders;
+            }
+        }
 
-            using (var fs = _fileSystem.GetFileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, true))
+        public async Task WriteToAsync(Stream outputStream)
+        {
+            try
             {
-                while (eofCount < 15)
+                var eofCount = 0;
+
+                using (var fs = _fileSystem.GetFileStream(_path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, true))
                 {
-                    var bytesRead = await CopyToAsyncInternal(fs, outputStream, BufferSize, cancellationToken).ConfigureAwait(false);
+                    while (eofCount < 15)
+                    {
+                        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);
+                        //var position = fs.Position;
+                        //_logger.Debug("Streamed {0} bytes to position {1} from file {2}", bytesRead, position, path);
 
-                    if (bytesRead == 0)
-                    {
-                        if (_job == null || _job.HasExited)
+                        if (bytesRead == 0)
                         {
-                            eofCount++;
+                            if (_job == null || _job.HasExited)
+                            {
+                                eofCount++;
+                            }
+                            await Task.Delay(100, _cancellationToken).ConfigureAwait(false);
+                        }
+                        else
+                        {
+                            eofCount = 0;
                         }
-                        await Task.Delay(100, cancellationToken).ConfigureAwait(false);
-                    }
-                    else
-                    {
-                        eofCount = 0;
                     }
                 }
             }
+            finally
+            {
+                if (_job != null)
+                {
+                    ApiEntryPoint.Instance.OnTranscodeEndRequest(_job);
+                }
+            }
         }
 
         private async Task<int> CopyToAsyncInternal(Stream source, Stream destination, Int32 bufferSize, CancellationToken cancellationToken)

+ 3 - 3
MediaBrowser.Api/Playback/Progressive/VideoService.cs

@@ -149,11 +149,11 @@ namespace MediaBrowser.Api.Playback.Progressive
                 {
                     args += " -copyts -avoid_negative_ts disabled -start_at_zero";
                 }
-                
+
                 return args;
             }
 
-            var keyFrameArg = string.Format(" -force_key_frames expr:gte(t,n_forced*{0})",
+            var keyFrameArg = string.Format(" -force_key_frames \"expr:gte(t,n_forced*{0})\"",
                 5.ToString(UsCulture));
 
             args += keyFrameArg;
@@ -237,4 +237,4 @@ namespace MediaBrowser.Api.Playback.Progressive
             return args;
         }
     }
-}
+}

+ 1 - 2
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
@@ -192,8 +193,6 @@ namespace MediaBrowser.Api.Playback
         [ApiMember(Name = "CopyTimestamps", Description = "Whether or not to copy timestamps when transcoding with an offset. Defaults to false.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
         public bool CopyTimestamps { get; set; }
 
-        public bool ForceLiveStream { get; set; }
-
         public bool EnableSubtitlesInManifest { get; set; }
 
         public VideoStreamRequest()

+ 5 - 2
MediaBrowser.Api/Playback/StreamState.cs

@@ -80,7 +80,10 @@ namespace MediaBrowser.Api.Playback
                     {
                         return 10;
                     }
-                    if (userAgent.IndexOf("cfnetwork", StringComparison.OrdinalIgnoreCase) != -1)
+                    if (userAgent.IndexOf("cfnetwork", StringComparison.OrdinalIgnoreCase) != -1 ||
+                        userAgent.IndexOf("ipad", StringComparison.OrdinalIgnoreCase) != -1 ||
+                        userAgent.IndexOf("iphone", StringComparison.OrdinalIgnoreCase) != -1 ||
+                        userAgent.IndexOf("ipod", StringComparison.OrdinalIgnoreCase) != -1)
                     {
                         return 10;
                     }
@@ -208,7 +211,7 @@ namespace MediaBrowser.Api.Playback
 
         private async void DisposeLiveStream()
         {
-            if (MediaSource.RequiresClosing && string.IsNullOrWhiteSpace(Request.LiveStreamId))
+            if (MediaSource.RequiresClosing && string.IsNullOrWhiteSpace(Request.LiveStreamId) && !string.IsNullOrWhiteSpace(MediaSource.LiveStreamId))
             {
                 try
                 {

+ 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]

+ 1 - 1
MediaBrowser.Api/PluginService.cs

@@ -227,7 +227,7 @@ namespace MediaBrowser.Api
                         .ToList();
                 }
             }
-            catch (Exception ex)
+            catch
             {
                 //Logger.ErrorException("Error getting plugin list", ex);
                 // Play it safe here

+ 0 - 2
MediaBrowser.Api/Reports/ReportsService.cs

@@ -275,8 +275,6 @@ namespace MediaBrowser.Api.Reports
                     case ItemFilter.IsPlayed:
                         query.IsPlayed = true;
                         break;
-                    case ItemFilter.IsRecentlyAdded:
-                        break;
                     case ItemFilter.IsResumable:
                         query.IsResumable = true;
                         break;

+ 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>

+ 1 - 1
MediaBrowser.Api/StartupWizardService.cs

@@ -117,7 +117,7 @@ namespace MediaBrowser.Api
             config.EnableStandaloneMusicKeys = true;
             config.EnableCaseSensitiveItemIds = true;
             //config.EnableFolderView = true;
-            config.SchemaVersion = 108;
+            config.SchemaVersion = 109;
         }
 
         public void Post(UpdateStartupConfiguration request)

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

@@ -24,7 +24,20 @@ namespace MediaBrowser.Api.Sync
                         }
                         break;
                     }
-                    if (item.IsFolder && !item.IsMusicGenre && !item.IsArtist && !item.IsType("musicalbum") && !item.IsGameGenre)
+                    if (item.IsAudio)
+                    {
+                        options.Add(SyncJobOption.Quality);
+                        options.Add(SyncJobOption.Profile);
+                        break;
+                    }
+                    if (item.IsMusicGenre || item.IsArtist|| item.IsType("musicalbum"))
+                    {
+                        options.Add(SyncJobOption.Quality);
+                        options.Add(SyncJobOption.Profile);
+                        options.Add(SyncJobOption.ItemLimit);
+                        break;
+                    }
+                    if (item.IsFolderItem && !item.IsMusicGenre && !item.IsArtist && !item.IsType("musicalbum") && !item.IsGameGenre)
                     {
                         options.Add(SyncJobOption.Quality);
                         options.Add(SyncJobOption.Profile);
@@ -44,7 +57,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);

+ 4 - 2
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);
 
@@ -290,7 +291,8 @@ namespace MediaBrowser.Api.Sync
                 {
                     Fields = new List<ItemFields>
                     {
-                        ItemFields.SyncInfo
+                        ItemFields.SyncInfo,
+                        ItemFields.BasicSyncInfo
                     }
                 };
 

+ 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

+ 0 - 2
MediaBrowser.Api/UserLibrary/ArtistsService.cs

@@ -8,8 +8,6 @@ using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Model.Dto;
 using ServiceStack;
 using System.Collections.Generic;
-using System.Linq;
-using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Querying;
 
 namespace MediaBrowser.Api.UserLibrary

+ 5 - 4
MediaBrowser.Api/UserLibrary/BaseItemsByNameService.cs

@@ -164,8 +164,6 @@ namespace MediaBrowser.Api.UserLibrary
                     case ItemFilter.IsPlayed:
                         query.IsPlayed = true;
                         break;
-                    case ItemFilter.IsRecentlyAdded:
-                        break;
                     case ItemFilter.IsResumable:
                         query.IsResumable = true;
                         break;
@@ -180,9 +178,10 @@ namespace MediaBrowser.Api.UserLibrary
 
             var result = GetItems(request, query);
 
+            var syncProgess = DtoService.GetSyncedItemProgress(dtoOptions);
             var dtos = result.Items.Select(i =>
             {
-                var dto = DtoService.GetItemByNameDto(i.Item1, dtoOptions, null, user);
+                var dto = DtoService.GetItemByNameDto(i.Item1, dtoOptions, null, syncProgess, user);
 
                 if (!string.IsNullOrWhiteSpace(request.IncludeItemTypes))
                 {
@@ -213,6 +212,7 @@ namespace MediaBrowser.Api.UserLibrary
             dto.AlbumCount = counts.AlbumCount;
             dto.SongCount = counts.SongCount;
             dto.GameCount = counts.GameCount;
+            dto.ArtistCount = counts.ArtistCount;
         }
 
         /// <summary>
@@ -325,7 +325,8 @@ namespace MediaBrowser.Api.UserLibrary
                 tuples = ibnItems.Select(i => new Tuple<BaseItem, List<BaseItem>>(i, new List<BaseItem>()));
             }
 
-            var dtos = tuples.Select(i => DtoService.GetItemByNameDto(i.Item1, dtoOptions, i.Item2, user));
+            var syncProgess = DtoService.GetSyncedItemProgress(dtoOptions);
+            var dtos = tuples.Select(i => DtoService.GetItemByNameDto(i.Item1, dtoOptions, i.Item2, syncProgess, user));
 
             result.Items = dtos.Where(i => i != null).ToArray();
 

+ 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; }
 

+ 0 - 2
MediaBrowser.Api/UserLibrary/GameGenresService.cs

@@ -4,11 +4,9 @@ using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Entities;
 using ServiceStack;
 using System;
 using System.Collections.Generic;
-using System.Linq;
 using MediaBrowser.Model.Querying;
 
 namespace MediaBrowser.Api.UserLibrary

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

@@ -8,7 +8,6 @@ using MediaBrowser.Model.Entities;
 using ServiceStack;
 using System;
 using System.Collections.Generic;
-using System.Linq;
 using MediaBrowser.Model.Querying;
 
 namespace MediaBrowser.Api.UserLibrary

+ 2 - 27
MediaBrowser.Api/UserLibrary/ItemsService.cs

@@ -1,5 +1,4 @@
-using MediaBrowser.Controller.Collections;
-using MediaBrowser.Controller.Dto;
+using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Localization;
@@ -158,33 +157,11 @@ namespace MediaBrowser.Api.UserLibrary
                 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 ids = query.ItemIds.ToList();
-
-                    // Try to preserve order
-                    result.Items = result.Items.OrderBy(i => ids.IndexOf(i.Id.ToString("N"))).ToArray();
-                }
-
-                return result;
-            }
-
-            if (request.Recursive)
+            if (request.Recursive || !string.IsNullOrEmpty(request.Ids) || user == null)
             {
                 return await folder.GetItems(GetItemsQuery(request, user)).ConfigureAwait(false);
             }
 
-            if (user == null)
-            {
-                return await folder.GetItems(GetItemsQuery(request, null)).ConfigureAwait(false);
-            }
-
             var userRoot = item as UserRootFolder;
 
             if (userRoot == null)
@@ -294,8 +271,6 @@ namespace MediaBrowser.Api.UserLibrary
                     case ItemFilter.IsPlayed:
                         query.IsPlayed = true;
                         break;
-                    case ItemFilter.IsRecentlyAdded:
-                        break;
                     case ItemFilter.IsResumable:
                         query.IsResumable = true;
                         break;

+ 0 - 2
MediaBrowser.Api/UserLibrary/MusicGenresService.cs

@@ -8,8 +8,6 @@ using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Model.Dto;
 using ServiceStack;
 using System.Collections.Generic;
-using System.Linq;
-using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Querying;
 
 namespace MediaBrowser.Api.UserLibrary

+ 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>

+ 13 - 5
MediaBrowser.Api/VideosService.cs

@@ -11,6 +11,7 @@ using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
 using CommonIO;
+using MediaBrowser.Model.Dto;
 
 namespace MediaBrowser.Api
 {
@@ -81,11 +82,18 @@ namespace MediaBrowser.Api
 
             var dtoOptions = GetDtoOptions(request);
 
-            var video = (Video)item;
-
-            var items = video.GetAdditionalParts()
-                         .Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, video))
-                         .ToArray();
+            var video = item as Video;
+            BaseItemDto[] items;
+            if (video != null)
+            {
+                items = video.GetAdditionalParts()
+                    .Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, video))
+                    .ToArray();
+            }
+            else
+            {
+                items = new BaseItemDto[] { };
+            }
 
             var result = new ItemsResult
             {

+ 1 - 1
MediaBrowser.Common.Implementations/MediaBrowser.Common.Implementations.csproj

@@ -55,7 +55,7 @@
       <HintPath>..\packages\morelinq.1.4.0\lib\net35\MoreLinq.dll</HintPath>
     </Reference>
     <Reference Include="NLog, Version=4.0.0.0, Culture=neutral, PublicKeyToken=5120e14c03d0593c, processorArchitecture=MSIL">
-      <HintPath>..\packages\NLog.4.3.5\lib\net45\NLog.dll</HintPath>
+      <HintPath>..\packages\NLog.4.3.6\lib\net45\NLog.dll</HintPath>
       <Private>True</Private>
     </Reference>
     <Reference Include="Patterns.Logging">

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

@@ -429,17 +429,6 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks
             GC.Collect(2, GCCollectionMode.Forced, true);
         }
 
-        /// <summary>
-        /// Executes the task.
-        /// </summary>
-        /// <param name="cancellationToken">The cancellation token.</param>
-        /// <param name="progress">The progress.</param>
-        /// <returns>Task.</returns>
-        private Task ExecuteTask(CancellationToken cancellationToken, IProgress<double> progress)
-        {
-            return Task.Run(async () => await ScheduledTask.Execute(cancellationToken, progress).ConfigureAwait(false), cancellationToken);
-        }
-
         /// <summary>
         /// Progress_s the progress changed.
         /// </summary>

+ 13 - 3
MediaBrowser.Common.Implementations/Serialization/XmlSerializer.cs

@@ -1,6 +1,7 @@
 using MediaBrowser.Model.Serialization;
 using System;
 using System.Collections.Concurrent;
+using System.Collections.Generic;
 using System.IO;
 using System.Xml;
 using CommonIO;
@@ -24,13 +25,22 @@ namespace MediaBrowser.Common.Implementations.Serialization
 
         // Need to cache these
         // http://dotnetcodebox.blogspot.com/2013/01/xmlserializer-class-may-result-in.html
-        private readonly ConcurrentDictionary<string, System.Xml.Serialization.XmlSerializer> _serializers =
-            new ConcurrentDictionary<string, System.Xml.Serialization.XmlSerializer>();
+        private readonly Dictionary<string, System.Xml.Serialization.XmlSerializer> _serializers =
+            new Dictionary<string, System.Xml.Serialization.XmlSerializer>();
 
         private System.Xml.Serialization.XmlSerializer GetSerializer(Type type)
         {
             var key = type.FullName;
-            return _serializers.GetOrAdd(key, k => new System.Xml.Serialization.XmlSerializer(type));
+            lock (_serializers)
+            {
+                System.Xml.Serialization.XmlSerializer serializer;
+                if (!_serializers.TryGetValue(key, out serializer))
+                {
+                    serializer = new System.Xml.Serialization.XmlSerializer(type);
+                    _serializers[key] = serializer;
+                }
+                return serializer;
+            }
         }
 
         /// <summary>

+ 63 - 1
MediaBrowser.Common.Implementations/Updates/GithubUpdater.cs

@@ -33,7 +33,6 @@ namespace MediaBrowser.Common.Implementations.Updates
                 EnableKeepAlive = false,
                 CancellationToken = cancellationToken,
                 UserAgent = "Emby/3.0"
-
             };
 
             if (_cacheLength.Ticks > 0)
@@ -79,6 +78,69 @@ namespace MediaBrowser.Common.Implementations.Updates
             };
         }
 
+        private bool MatchesUpdateLevel(RootObject i, PackageVersionClass updateLevel)
+        {
+            if (updateLevel == PackageVersionClass.Beta)
+            {
+                return !i.prerelease || i.name.EndsWith("-beta", StringComparison.OrdinalIgnoreCase);
+            }
+            if (updateLevel == PackageVersionClass.Dev)
+            {
+                return !i.prerelease || i.name.EndsWith("-beta", StringComparison.OrdinalIgnoreCase) ||
+                       i.name.EndsWith("-dev", StringComparison.OrdinalIgnoreCase);
+            }
+
+            // Technically all we need to do is check that it's not pre-release
+            // But let's addititional checks for -beta and -dev to handle builds that might be temporarily tagged incorrectly.
+            return !i.prerelease && !i.name.EndsWith("-beta", StringComparison.OrdinalIgnoreCase) &&
+                   !i.name.EndsWith("-dev", StringComparison.OrdinalIgnoreCase);
+        }
+
+        public async Task<List<RootObject>> GetLatestReleases(string organzation, string repository, string assetFilename, CancellationToken cancellationToken)
+        {
+            var list = new List<RootObject>();
+
+            var url = string.Format("https://api.github.com/repos/{0}/{1}/releases", organzation, repository);
+
+            var options = new HttpRequestOptions
+            {
+                Url = url,
+                EnableKeepAlive = false,
+                CancellationToken = cancellationToken,
+                UserAgent = "Emby/3.0"
+            };
+
+            if (_cacheLength.Ticks > 0)
+            {
+                options.CacheMode = CacheMode.Unconditional;
+                options.CacheLength = _cacheLength;
+            }
+
+            using (var stream = await _httpClient.Get(options).ConfigureAwait(false))
+            {
+                var obj = _jsonSerializer.DeserializeFromStream<RootObject[]>(stream);
+
+                obj = obj.Where(i => (i.assets ?? new List<Asset>()).Any(a => IsAsset(a, assetFilename))).ToArray();
+
+                list.AddRange(obj.Where(i => MatchesUpdateLevel(i, PackageVersionClass.Release)).OrderByDescending(GetVersion).Take(1));
+                list.AddRange(obj.Where(i => MatchesUpdateLevel(i, PackageVersionClass.Beta)).OrderByDescending(GetVersion).Take(1));
+                list.AddRange(obj.Where(i => MatchesUpdateLevel(i, PackageVersionClass.Dev)).OrderByDescending(GetVersion).Take(1));
+
+                return list;
+            }
+        }
+
+        public Version GetVersion(RootObject obj)
+        {
+            Version version;
+            if (!Version.TryParse(obj.tag_name, out version))
+            {
+                return new Version(1, 0);
+            }
+
+            return version;
+        }
+
         private CheckForUpdateResult CheckForUpdateResult(RootObject obj, Version minVersion, string assetFilename, string packageName, string targetFilename)
         {
             Version version;

+ 1 - 1
MediaBrowser.Common.Implementations/packages.config

@@ -2,7 +2,7 @@
 <packages>
   <package id="CommonIO" version="1.0.0.9" targetFramework="net45" />
   <package id="morelinq" version="1.4.0" targetFramework="net45" />
-  <package id="NLog" version="4.3.5" targetFramework="net45" />
+  <package id="NLog" version="4.3.6" targetFramework="net45" />
   <package id="Patterns.Logging" version="1.0.0.2" targetFramework="net45" />
   <package id="SimpleInjector" version="3.2.0" targetFramework="net45" />
 </packages>

+ 2 - 2
MediaBrowser.Common/IO/StreamDefaults.cs

@@ -9,11 +9,11 @@ namespace MediaBrowser.Common.IO
         /// <summary>
         /// The default copy to buffer size
         /// </summary>
-        public const int DefaultCopyToBufferSize = 262144;
+        public const int DefaultCopyToBufferSize = 81920;
 
         /// <summary>
         /// The default file stream buffer size
         /// </summary>
-        public const int DefaultFileStreamBufferSize = 262144;
+        public const int DefaultFileStreamBufferSize = 81920;
     }
 }

+ 1 - 1
MediaBrowser.Common/Plugins/BasePlugin.cs

@@ -211,7 +211,7 @@ namespace MediaBrowser.Common.Plugins
             {
                 return (TConfigurationType)Activator.CreateInstance(typeof(TConfigurationType));
             }
-            catch (Exception ex)
+            catch
             {
                 return (TConfigurationType)Activator.CreateInstance(typeof(TConfigurationType));
             }

+ 1 - 4
MediaBrowser.Controller/Channels/Channel.cs

@@ -57,10 +57,7 @@ namespace MediaBrowser.Controller.Channels
             catch
             {
                 // Already logged at lower levels
-                return new QueryResult<BaseItem>
-                {
-
-                };
+                return new QueryResult<BaseItem>();
             }
         }
 

+ 4 - 2
MediaBrowser.Controller/Channels/ChannelMediaInfo.cs

@@ -83,8 +83,7 @@ namespace MediaBrowser.Controller.Channels
         {
             var list = new List<MediaStream>();
 
-            if (!string.IsNullOrWhiteSpace(info.VideoCodec) &&
-                !string.IsNullOrWhiteSpace(info.AudioCodec))
+            if (!string.IsNullOrWhiteSpace(info.VideoCodec))
             {
                 list.Add(new MediaStream
                 {
@@ -99,7 +98,10 @@ namespace MediaBrowser.Controller.Channels
                     BitRate = info.VideoBitrate,
                     AverageFrameRate = info.Framerate
                 });
+            }
 
+            if (!string.IsNullOrWhiteSpace(info.AudioCodec))
+            {
                 list.Add(new MediaStream
                 {
                     Type = MediaStreamType.Audio,

+ 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))

+ 5 - 16
MediaBrowser.Controller/Dto/IDtoService.cs

@@ -1,9 +1,9 @@
-using System;
-using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Querying;
 using System.Collections.Generic;
 using System.Threading.Tasks;
+using MediaBrowser.Controller.Sync;
 
 namespace MediaBrowser.Controller.Dto
 {
@@ -43,14 +43,6 @@ namespace MediaBrowser.Controller.Dto
         /// <returns>Task{BaseItemDto}.</returns>
         BaseItemDto GetBaseItemDto(BaseItem item, List<ItemFields> fields, User user = null, BaseItem owner = null);
 
-        /// <summary>
-        /// Fills the synchronize information.
-        /// </summary>
-        /// <param name="tuples">The tuples.</param>
-        /// <param name="options">The options.</param>
-        /// <param name="user">The user.</param>
-        void FillSyncInfo(IEnumerable<Tuple<BaseItem, BaseItemDto>> tuples, DtoOptions options, User user);
-
         /// <summary>
         /// Gets the base item dto.
         /// </summary>
@@ -89,11 +81,8 @@ namespace MediaBrowser.Controller.Dto
         /// <summary>
         /// Gets the item by name dto.
         /// </summary>
-        /// <param name="item">The item.</param>
-        /// <param name="options">The options.</param>
-        /// <param name="taggedItems">The tagged items.</param>
-        /// <param name="user">The user.</param>
-        /// <returns>BaseItemDto.</returns>
-        BaseItemDto GetItemByNameDto(BaseItem item, DtoOptions options, List<BaseItem> taggedItems, User user = null);
+        BaseItemDto GetItemByNameDto(BaseItem item, DtoOptions options, List<BaseItem> taggedItems, Dictionary<string, SyncedItemProgress> syncProgress, User user = null);
+
+        Dictionary<string, SyncedItemProgress> GetSyncedItemProgress(DtoOptions options);
     }
 }

+ 49 - 12
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()
         {
@@ -76,7 +103,7 @@ namespace MediaBrowser.Controller.Entities
             {
                 var locations = PhysicalLocations.ToList();
 
-                var newLocations = CreateResolveArgs(new DirectoryService(BaseItem.FileSystem), false).PhysicalLocations.ToList();
+                var newLocations = CreateResolveArgs(new DirectoryService(Logger, FileSystem), false).PhysicalLocations.ToList();
 
                 if (!locations.SequenceEqual(newLocations))
                 {
@@ -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;
+        }
     }
 }

+ 43 - 10
MediaBrowser.Controller/Entities/BaseItem.cs

@@ -281,6 +281,20 @@ namespace MediaBrowser.Controller.Entities
             }
         }
 
+        public Task UpdateIsOffline(bool newValue)
+        {
+            var item = this;
+
+            if (item.IsOffline != newValue)
+            {
+                item.IsOffline = newValue;
+                // this is creating too many repeated db updates
+                //return item.UpdateToRepository(ItemUpdateType.None, CancellationToken.None);
+            }
+
+            return Task.FromResult(true);
+        }
+
         /// <summary>
         /// Gets or sets the type of the location.
         /// </summary>
@@ -290,10 +304,10 @@ namespace MediaBrowser.Controller.Entities
         {
             get
             {
-                if (IsOffline)
-                {
-                    return LocationType.Offline;
-                }
+                //if (IsOffline)
+                //{
+                //    return LocationType.Offline;
+                //}
 
                 if (string.IsNullOrWhiteSpace(Path))
                 {
@@ -455,7 +469,7 @@ namespace MediaBrowser.Controller.Entities
         public DateTime DateLastRefreshed { get; set; }
 
         [IgnoreDataMember]
-        public virtual bool EnableForceSaveOnDateModifiedChange
+        public virtual bool EnableRefreshOnDateModifiedChange
         {
             get { return false; }
         }
@@ -767,6 +781,9 @@ namespace MediaBrowser.Controller.Entities
         [IgnoreDataMember]
         public string OfficialRating { get; set; }
 
+        [IgnoreDataMember]
+        public int InheritedParentalRatingValue { get; set; }
+
         /// <summary>
         /// Gets or sets the critic rating.
         /// </summary>
@@ -951,7 +968,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 +998,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 =>
                 {
@@ -1003,7 +1020,7 @@ namespace MediaBrowser.Controller.Entities
 
         public Task RefreshMetadata(CancellationToken cancellationToken)
         {
-            return RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(FileSystem)), cancellationToken);
+            return RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(Logger, FileSystem)), cancellationToken);
         }
 
         /// <summary>
@@ -1194,10 +1211,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 +2230,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; }
         }

+ 73 - 1
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,72 @@ 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;
+
+                options.SchemaVersion = 1;
+                XmlSerializer.SerializeToFile(options, GetLibraryOptionsPath(path));
+            }
+        }
+
         /// <summary>
         /// Allow different display preferences for each collection folder
         /// </summary>
@@ -82,7 +154,7 @@ namespace MediaBrowser.Controller.Entities
             {
                 var locations = PhysicalLocations.ToList();
 
-                var newLocations = CreateResolveArgs(new DirectoryService(BaseItem.FileSystem), false).PhysicalLocations.ToList();
+                var newLocations = CreateResolveArgs(new DirectoryService(Logger, FileSystem), false).PhysicalLocations.ToList();
 
                 if (!locations.SequenceEqual(newLocations))
                 {

+ 41 - 27
MediaBrowser.Controller/Entities/Folder.cs

@@ -1,5 +1,4 @@
 using MediaBrowser.Common.Progress;
-using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Dto;
@@ -14,6 +13,8 @@ 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;
 
 namespace MediaBrowser.Controller.Entities
@@ -273,13 +274,14 @@ 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();
         }
 
         public Task ValidateChildren(IProgress<double> progress, CancellationToken cancellationToken)
         {
-            return ValidateChildren(progress, cancellationToken, new MetadataRefreshOptions(new DirectoryService(FileSystem)));
+            return ValidateChildren(progress, cancellationToken, new MetadataRefreshOptions(new DirectoryService(Logger, FileSystem)));
         }
 
         /// <summary>
@@ -373,7 +375,7 @@ namespace MediaBrowser.Controller.Entities
 
                     if (currentChildren.TryGetValue(child.Id, out currentChild) && IsValidFromResolver(currentChild, child))
                     {
-                        await UpdateIsOffline(currentChild, false).ConfigureAwait(false);
+                        await currentChild.UpdateIsOffline(false).ConfigureAwait(false);
                         validChildren.Add(currentChild);
 
                         continue;
@@ -402,7 +404,7 @@ namespace MediaBrowser.Controller.Entities
 
                         else if (!string.IsNullOrEmpty(item.Path) && IsPathOffline(item.Path))
                         {
-                            await UpdateIsOffline(item, true).ConfigureAwait(false);
+                            await item.UpdateIsOffline(true).ConfigureAwait(false);
                         }
                         else
                         {
@@ -459,17 +461,6 @@ namespace MediaBrowser.Controller.Entities
             progress.Report(100);
         }
 
-        private Task UpdateIsOffline(BaseItem item, bool newValue)
-        {
-            if (item.IsOffline != newValue)
-            {
-                item.IsOffline = newValue;
-                return item.UpdateToRepository(ItemUpdateType.None, CancellationToken.None);
-            }
-
-            return Task.FromResult(true);
-        }
-
         private async Task RefreshMetadataRecursive(MetadataRefreshOptions refreshOptions, bool recursive, IProgress<double> progress, CancellationToken cancellationToken)
         {
             var children = ActualChildren.ToList();
@@ -643,8 +634,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 +691,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))
@@ -883,6 +875,15 @@ namespace MediaBrowser.Controller.Entities
                 return true;
             }
 
+            if (query.IsPlayed.HasValue)
+            {
+                if (query.IncludeItemTypes.Length == 1 && query.IncludeItemTypes.Contains(typeof(Series).Name))
+                {
+                    Logger.Debug("Query requires post-filtering due to IsPlayed");
+                    return true;
+                }
+            }
+
             return false;
         }
 
@@ -890,8 +891,16 @@ 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));
+                var result = LibraryManager.GetItemsResult(query);
+
+                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();
+                }
+                return Task.FromResult(result);
             }
 
             return GetItemsInternal(query);
@@ -919,10 +928,7 @@ namespace MediaBrowser.Controller.Entities
                 catch
                 {
                     // Already logged at lower levels
-                    return new QueryResult<BaseItem>
-                    {
-
-                    };
+                    return new QueryResult<BaseItem>();
                 }
             }
 
@@ -950,12 +956,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)
@@ -1419,7 +1425,7 @@ namespace MediaBrowser.Controller.Entities
                 itemDto.RecursiveItemCount = allItemsQueryResult.TotalRecordCount;
             }
 
-            double recursiveItemCount = allItemsQueryResult.TotalRecordCount;
+            var recursiveItemCount = allItemsQueryResult.TotalRecordCount;
             double unplayedCount = unplayedQueryResult.TotalRecordCount;
 
             if (recursiveItemCount > 0)
@@ -1429,6 +1435,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;
+        }
     }
 }

+ 1 - 2
MediaBrowser.Controller/Entities/IHasImages.cs

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

+ 11 - 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,15 @@ namespace MediaBrowser.Controller.Entities
 
         bool RequiresRefresh();
 
-        bool EnableForceSaveOnDateModifiedChange { get; }
+        bool EnableRefreshOnDateModifiedChange { get; }
+
+        string PresentationUniqueKey { get; set; }
+
+        string GetPresentationUniqueKey();
+        string CreatePresentationUniqueKey();
+        bool StopRefreshIfLocalMetadataFound { get; }
+
+        int? GetInheritedParentalRatingValue();
+        int InheritedParentalRatingValue { get; set; }
     }
 }

+ 1 - 2
MediaBrowser.Controller/Entities/IItemByName.cs

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

+ 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; }
 

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

@@ -11,11 +11,13 @@ namespace MediaBrowser.Controller.Entities
         public int? MaxListOrder { get; set; }
         public Guid AppearsInItemId { get; set; }
         public string NameContains { get; set; }
+        public SourceType[] SourceTypes { get; set; }
 
         public InternalPeopleQuery()
         {
             PersonTypes = new List<string>();
             ExcludePersonTypes = new List<string>();
+            SourceTypes = new SourceType[] { };
         }
     }
 }

+ 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;
+            }
+        }
     }
 }

+ 0 - 1
MediaBrowser.Controller/Entities/MusicVideo.cs

@@ -1,7 +1,6 @@
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Configuration;
-using MediaBrowser.Model.Entities;
 using System.Collections.Generic;
 using System.Runtime.Serialization;
 

+ 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;
+        }
     }
 }

+ 25 - 21
MediaBrowser.Controller/Entities/TV/Episode.cs

@@ -135,7 +135,11 @@ namespace MediaBrowser.Controller.Entities.TV
         [IgnoreDataMember]
         public Series Series
         {
-            get { return FindParent<Series>(); }
+            get
+            {
+                var seriesId = SeriesId ?? FindSeriesId();
+                return seriesId.HasValue ? (LibraryManager.GetItemById(seriesId.Value) as Series) : null;
+            }
         }
 
         [IgnoreDataMember]
@@ -143,24 +147,8 @@ namespace MediaBrowser.Controller.Entities.TV
         {
             get
             {
-                var season = FindParent<Season>();
-
-                // Episodes directly in series folder
-                if (season == null)
-                {
-                    var series = Series;
-
-                    if (series != null && ParentIndexNumber.HasValue)
-                    {
-                        var findNumber = ParentIndexNumber.Value;
-
-                        season = series.Children
-                            .OfType<Season>()
-                            .FirstOrDefault(i => i.IndexNumber.HasValue && i.IndexNumber.Value == findNumber);
-                    }
-                }
-
-                return season;
+                var seasonId = SeasonId ?? FindSeasonId();
+                return seasonId.HasValue ? (LibraryManager.GetItemById(seasonId.Value) as Season) : null;
             }
         }
 
@@ -193,7 +181,23 @@ namespace MediaBrowser.Controller.Entities.TV
 
         public Guid? FindSeasonId()
         {
-            var season = Season;
+            var season = FindParent<Season>();
+
+            // Episodes directly in series folder
+            if (season == null)
+            {
+                var series = Series;
+
+                if (series != null && ParentIndexNumber.HasValue)
+                {
+                    var findNumber = ParentIndexNumber.Value;
+
+                    season = series.Children
+                        .OfType<Season>()
+                        .FirstOrDefault(i => i.IndexNumber.HasValue && i.IndexNumber.Value == findNumber);
+                }
+            }
+
             return season == null ? (Guid?)null : season.Id;
         }
 
@@ -263,7 +267,7 @@ namespace MediaBrowser.Controller.Entities.TV
 
         public Guid? FindSeriesId()
         {
-            var series = Series;
+            var series = FindParent<Series>();
             return series == null ? (Guid?)null : series.Id;
         }
 

+ 29 - 41
MediaBrowser.Controller/Entities/TV/Season.cs

@@ -1,6 +1,5 @@
 using System;
 using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Querying;
 using MediaBrowser.Model.Users;
 using MoreLinq;
@@ -86,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>
@@ -96,7 +99,11 @@ namespace MediaBrowser.Controller.Entities.TV
         [IgnoreDataMember]
         public Series Series
         {
-            get { return FindParent<Series>(); }
+            get
+            {
+                var seriesId = SeriesId ?? FindSeriesId();
+                return seriesId.HasValue ? (LibraryManager.GetItemById(seriesId.Value) as Series) : null;
+            }
         }
 
         [IgnoreDataMember]
@@ -115,22 +122,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>
@@ -142,24 +145,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)
@@ -171,10 +156,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);
         }
 
@@ -185,19 +175,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()
@@ -257,7 +245,7 @@ namespace MediaBrowser.Controller.Entities.TV
 
         public Guid? FindSeriesId()
         {
-            var series = Series;
+            var series = FindParent<Series>();
             return series == null ? (Guid?)null : series.Id;
         }
 

+ 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 - 2
MediaBrowser.Controller/Entities/Trailer.cs

@@ -2,9 +2,7 @@
 using MediaBrowser.Model.Configuration;
 using MediaBrowser.Model.Entities;
 using System.Collections.Generic;
-using System.Globalization;
 using System.Runtime.Serialization;
-using MediaBrowser.Controller.Entities.Movies;
 using MediaBrowser.Model.Providers;
 
 namespace MediaBrowser.Controller.Entities
@@ -126,5 +124,15 @@ namespace MediaBrowser.Controller.Entities
 
             return list;
         }
+
+        [IgnoreDataMember]
+        public override bool StopRefreshIfLocalMetadataFound
+        {
+            get
+            {
+                // Need people id's from internet metadata
+                return false;
+            }
+        }
     }
 }

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

@@ -213,7 +213,7 @@ namespace MediaBrowser.Controller.Entities
 
             Name = newName;
 
-			return RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(FileSystem))
+			return RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(Logger, FileSystem))
             {
                 ReplaceAllMetadata = true,
                 ImageRefreshMode = ImageRefreshMode.FullRefresh,

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

@@ -1,6 +1,5 @@
 using System.Runtime.Serialization;
 using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Library;
 using MediaBrowser.Model.Querying;
 using System;
@@ -8,7 +7,6 @@ using System.Collections.Generic;
 using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
-using MediaBrowser.Controller.Library;
 
 namespace MediaBrowser.Controller.Entities
 {
@@ -18,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)
@@ -35,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)
@@ -71,6 +94,8 @@ namespace MediaBrowser.Controller.Entities
 
         public override bool BeforeMetadataRefresh()
         {
+            ClearCache();
+
             var hasChanges = base.BeforeMetadataRefresh();
 
             if (string.Equals("default", Name, StringComparison.OrdinalIgnoreCase))
@@ -82,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 - 66
MediaBrowser.Controller/Entities/UserViewBuilder.cs

@@ -1,5 +1,4 @@
 using MediaBrowser.Controller.Channels;
-using MediaBrowser.Controller.Collections;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Entities.Movies;
 using MediaBrowser.Controller.Entities.TV;
@@ -14,12 +13,10 @@ using MediaBrowser.Model.Logging;
 using MediaBrowser.Model.Querying;
 using System;
 using System.Collections.Generic;
-using System.IO;
 using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
 using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Model.Configuration;
 using MoreLinq;
 
 namespace MediaBrowser.Controller.Entities
@@ -427,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)
@@ -783,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)
@@ -794,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,
@@ -804,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;
 
@@ -813,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))
@@ -821,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,
@@ -1096,8 +1100,6 @@ namespace MediaBrowser.Controller.Entities
             bool? isVirtualUnaired,
             bool? isUnaired)
         {
-            items = FilterVirtualSeasons(items, isMissing, isVirtualUnaired, isUnaired);
-
             if (isMissing.HasValue)
             {
                 var val = isMissing.Value;
@@ -1143,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;
+        }
     }
 }

+ 23 - 1
MediaBrowser.Controller/FileOrganization/IFileOrganizationService.cs

@@ -1,5 +1,7 @@
-using MediaBrowser.Model.FileOrganization;
+using MediaBrowser.Model.Events;
+using MediaBrowser.Model.FileOrganization;
 using MediaBrowser.Model.Querying;
+using System;
 using System.Threading;
 using System.Threading.Tasks;
 
@@ -7,6 +9,11 @@ namespace MediaBrowser.Controller.FileOrganization
 {
     public interface IFileOrganizationService
     {
+        event EventHandler<GenericEventArgs<FileOrganizationResult>> ItemAdded;
+        event EventHandler<GenericEventArgs<FileOrganizationResult>> ItemUpdated;
+        event EventHandler<GenericEventArgs<FileOrganizationResult>> ItemRemoved;
+        event EventHandler LogReset;
+
         /// <summary>
         /// Processes the new files.
         /// </summary>
@@ -81,5 +88,20 @@ namespace MediaBrowser.Controller.FileOrganization
         /// <param name="ItemName">Item name.</param>
         /// <param name="matchString">The match string to delete.</param>
         void DeleteSmartMatchEntry(string ItemName, string matchString);
+
+        /// <summary>
+        /// Attempts to add a an item to the list of currently processed items.
+        /// </summary>
+        /// <param name="result">The result item.</param>
+        /// <param name="fullClientRefresh">Passing true will notify the client to reload all items, otherwise only a single item will be refreshed.</param>
+        /// <returns>True if the item was added, False if the item is already contained in the list.</returns>
+        bool AddToInProgressList(FileOrganizationResult result, bool fullClientRefresh);
+
+        /// <summary>
+        /// Removes an item from the list of currently processed items.
+        /// </summary>
+        /// <param name="result">The result item.</param>
+        /// <returns>True if the item was removed, False if the item was not contained in the list.</returns>
+        bool RemoveFromInprogressList(FileOrganizationResult result);
     }
 }

+ 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; }
     }
 }

+ 12 - 14
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>
@@ -474,12 +477,6 @@ namespace MediaBrowser.Controller.Library
         /// <returns>List&lt;Person&gt;.</returns>
         List<Person> GetPeopleItems(InternalPeopleQuery query);
 
-        /// <summary>
-        /// Gets all people names.
-        /// </summary>
-        /// <returns>List&lt;System.String&gt;.</returns>
-        List<PersonInfo> GetAllPeople();
-
         /// <summary>
         /// Updates the people.
         /// </summary>
@@ -557,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);
@@ -568,5 +565,6 @@ namespace MediaBrowser.Controller.Library
         QueryResult<Tuple<BaseItem, ItemCounts>> GetStudios(InternalItemsQuery query);
         QueryResult<Tuple<BaseItem, ItemCounts>> GetArtists(InternalItemsQuery query);
         QueryResult<Tuple<BaseItem, ItemCounts>> GetAlbumArtists(InternalItemsQuery query);
+        QueryResult<Tuple<BaseItem, ItemCounts>> GetAllArtists(InternalItemsQuery query);
     }
 }

+ 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>

+ 2 - 3
MediaBrowser.Controller/LiveTv/ILiveTvManager.cs

@@ -331,12 +331,11 @@ namespace MediaBrowser.Controller.LiveTv
         /// <param name="user">The user.</param>
         /// <returns>Task.</returns>
         Task AddInfoToProgramDto(List<Tuple<BaseItem,BaseItemDto>> programs, List<ItemFields> fields, User user = null);
+      
         /// <summary>
         /// Saves the tuner host.
         /// </summary>
-        /// <param name="info">The information.</param>
-        /// <returns>Task.</returns>
-        Task<TunerHostInfo> SaveTunerHost(TunerHostInfo info);
+        Task<TunerHostInfo> SaveTunerHost(TunerHostInfo info, bool dataSourceChanged = true);
         /// <summary>
         /// Saves the listing provider.
         /// </summary>

+ 0 - 1
MediaBrowser.Controller/LiveTv/LiveTvProgram.cs

@@ -1,5 +1,4 @@
 using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.Movies;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Configuration;
 using MediaBrowser.Model.LiveTv;

+ 0 - 1
MediaBrowser.Controller/LiveTv/LiveTvVideoRecording.cs

@@ -1,5 +1,4 @@
 using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.Movies;
 using MediaBrowser.Model.Configuration;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Entities;

+ 1 - 7
MediaBrowser.Controller/LiveTv/TimerEventInfo.cs

@@ -1,10 +1,4 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-
-namespace MediaBrowser.Controller.LiveTv
+namespace MediaBrowser.Controller.LiveTv
 {
     public class TimerEventInfo
     {

+ 1 - 7
MediaBrowser.Controller/LiveTv/TunerChannelMapping.cs

@@ -1,10 +1,4 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-
-namespace MediaBrowser.Controller.LiveTv
+namespace MediaBrowser.Controller.LiveTv
 {
     public class TunerChannelMapping
     {

+ 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" />

+ 1 - 2
MediaBrowser.Controller/MediaEncoding/EncodingJobOptions.cs

@@ -1,5 +1,4 @@
-using System.Linq;
-using MediaBrowser.Model.Dlna;
+using MediaBrowser.Model.Dlna;
 
 namespace MediaBrowser.Controller.MediaEncoding
 {

+ 2 - 1
MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs

@@ -1,7 +1,6 @@
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.MediaInfo;
 using System;
-using System.IO;
 using System.Threading;
 using System.Threading.Tasks;
 using MediaBrowser.Model.Dlna;
@@ -134,5 +133,7 @@ namespace MediaBrowser.Controller.MediaEncoding
         Task Init();
 
         Task UpdateEncoderPath(string path, string pathType);
+        bool SupportsEncoder(string encoder);
+        bool IsDefaultEncoderPath { get; }
     }
 }

+ 6 - 1
MediaBrowser.Controller/MediaEncoding/MediaEncoderHelpers.cs

@@ -36,8 +36,13 @@ namespace MediaBrowser.Controller.MediaEncoding
             return new[] {videoPath};
         }
 
-        public static List<string> GetPlayableStreamFiles(IFileSystem fileSystem, string rootPath, IEnumerable<string> filenames)
+        private static List<string> GetPlayableStreamFiles(IFileSystem fileSystem, string rootPath, List<string> filenames)
         {
+            if (filenames.Count == 0)
+            {
+                return new List<string>();
+            }
+
             var allFiles = fileSystem
                 .GetFilePaths(rootPath, true)
                 .ToList();

+ 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.

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

@@ -169,6 +169,13 @@ namespace MediaBrowser.Controller.Persistence
         QueryResult<Tuple<BaseItem, ItemCounts>> GetStudios(InternalItemsQuery query);
         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;

+ 9 - 0
MediaBrowser.Controller/Providers/BaseItemXmlParser.cs

@@ -724,6 +724,15 @@ namespace MediaBrowser.Controller.Providers
                         }
                         break;
                     }
+                case "TvMazeId":
+                    {
+                        var id = reader.ReadElementContentAsString();
+                        if (!string.IsNullOrWhiteSpace(id))
+                        {
+                            item.SetProviderId(MetadataProviders.TvMaze, id);
+                        }
+                        break;
+                    }
                 case "AudioDbArtistId":
                     {
                         var id = reader.ReadElementContentAsString();

+ 15 - 13
MediaBrowser.Controller/Providers/DirectoryService.cs

@@ -16,13 +16,16 @@ namespace MediaBrowser.Controller.Providers
         private readonly ConcurrentDictionary<string, Dictionary<string, FileSystemMetadata>> _cache =
             new ConcurrentDictionary<string, Dictionary<string, FileSystemMetadata>>(StringComparer.OrdinalIgnoreCase);
 
-		public DirectoryService(ILogger logger, IFileSystem fileSystem)
+        private readonly ConcurrentDictionary<string, FileSystemMetadata> _fileCache =
+        new ConcurrentDictionary<string, FileSystemMetadata>(StringComparer.OrdinalIgnoreCase);
+
+        public DirectoryService(ILogger logger, IFileSystem fileSystem)
         {
             _logger = logger;
 			_fileSystem = fileSystem;
         }
 
-		public DirectoryService(IFileSystem fileSystem)
+        public DirectoryService(IFileSystem fileSystem)
             : this(new NullLogger(), fileSystem)
         {
         }
@@ -100,20 +103,19 @@ namespace MediaBrowser.Controller.Providers
 
         public FileSystemMetadata GetFile(string path)
         {
-            var directory = Path.GetDirectoryName(path);
-
-            if (string.IsNullOrWhiteSpace(directory))
+            FileSystemMetadata file;
+            if (!_fileCache.TryGetValue(path, out file))
             {
-                _logger.Debug("Parent path is null for {0}", path);
-                return null;
-            }
-
-            var dict = GetFileSystemDictionary(directory, false);
+                file = _fileSystem.GetFileInfo(path);
 
-            FileSystemMetadata entry;
-            dict.TryGetValue(path, out entry);
+                if (file != null)
+                {
+                    _fileCache.TryAdd(path, file);
+                }
+            }
 
-            return entry;
+            return file;
+            //return _fileSystem.GetFileInfo(path);
         }
 
         public IEnumerable<FileSystemMetadata> GetDirectories(string path)

+ 2 - 1
MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs

@@ -1,5 +1,6 @@
 using System.Linq;
 using CommonIO;
+using MediaBrowser.Model.Logging;
 using MediaBrowser.Model.Providers;
 
 namespace MediaBrowser.Controller.Providers
@@ -19,7 +20,7 @@ namespace MediaBrowser.Controller.Providers
         public bool ForceSave { get; set; }
 
         public MetadataRefreshOptions(IFileSystem fileSystem)
-			: this(new DirectoryService(fileSystem))
+			: this(new DirectoryService(new NullLogger(), fileSystem))
         {
         }
 

Kaikkia tiedostoja ei voida näyttää, sillä liian monta tiedostoa muuttui tässä diffissä