浏览代码

Merge pull request #1087 from MediaBrowser/dev

3.0.5588.0
Luke 10 年之前
父节点
当前提交
7b67ec6854
共有 39 个文件被更改,包括 380 次插入200 次删除
  1. 11 21
      Emby.Drawing/ImageProcessor.cs
  2. 74 13
      MediaBrowser.Api/ApiEntryPoint.cs
  3. 1 0
      MediaBrowser.Api/Playback/BaseStreamingService.cs
  4. 3 1
      MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs
  5. 3 3
      MediaBrowser.Api/UserLibrary/UserViewsService.cs
  6. 5 17
      MediaBrowser.Controller/Entities/BaseItem.cs
  7. 0 6
      MediaBrowser.Controller/Entities/ItemImageInfo.cs
  8. 24 9
      MediaBrowser.Controller/Entities/UserViewBuilder.cs
  9. 2 0
      MediaBrowser.Controller/Library/ILibraryManager.cs
  10. 8 2
      MediaBrowser.Controller/LiveTv/LiveTvTunerInfo.cs
  11. 3 0
      MediaBrowser.Model.Portable/MediaBrowser.Model.Portable.csproj
  12. 3 0
      MediaBrowser.Model.net35/MediaBrowser.Model.net35.csproj
  13. 14 1
      MediaBrowser.Model/ApiClient/ServerCredentials.cs
  14. 1 1
      MediaBrowser.Model/Configuration/ServerConfiguration.cs
  15. 8 13
      MediaBrowser.Model/Dlna/StreamBuilder.cs
  16. 72 0
      MediaBrowser.Model/Dto/ItemLayout.cs
  17. 6 0
      MediaBrowser.Model/LiveTv/LiveTvTunerInfoDto.cs
  18. 1 0
      MediaBrowser.Model/MediaBrowser.Model.csproj
  19. 0 1
      MediaBrowser.Providers/Manager/ItemImageProvider.cs
  20. 5 0
      MediaBrowser.Providers/Manager/MetadataService.cs
  21. 47 24
      MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs
  22. 0 4
      MediaBrowser.Providers/Movies/MovieMetadataService.cs
  23. 0 4
      MediaBrowser.Providers/TV/SeriesMetadataService.cs
  24. 9 1
      MediaBrowser.Providers/TV/TvdbSeriesProvider.cs
  25. 1 4
      MediaBrowser.Server.Implementations/Dto/DtoService.cs
  26. 8 40
      MediaBrowser.Server.Implementations/EntryPoints/ExternalPortForwarding.cs
  27. 12 4
      MediaBrowser.Server.Implementations/Library/LibraryManager.cs
  28. 27 18
      MediaBrowser.Server.Implementations/Library/UserViewManager.cs
  29. 2 1
      MediaBrowser.Server.Implementations/LiveTv/LiveTvDtoService.cs
  30. 8 3
      MediaBrowser.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs
  31. 3 1
      MediaBrowser.Server.Implementations/Localization/Server/server.json
  32. 5 1
      MediaBrowser.Server.Implementations/Sync/SyncManager.cs
  33. 1 0
      MediaBrowser.WebDashboard/Api/PackageCreator.cs
  34. 6 0
      MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj
  35. 2 2
      Nuget/MediaBrowser.Common.Internal.nuspec
  36. 1 1
      Nuget/MediaBrowser.Common.nuspec
  37. 1 1
      Nuget/MediaBrowser.Model.Signed.nuspec
  38. 2 2
      Nuget/MediaBrowser.Server.Core.nuspec
  39. 1 1
      SharedVersion.cs

+ 11 - 21
Emby.Drawing/ImageProcessor.cs

@@ -158,22 +158,19 @@ namespace Emby.Drawing
             }
 
             var dateModified = options.Image.DateModified;
-            var length = options.Image.Length;
 
             if (options.CropWhiteSpace)
             {
-                var tuple = await GetWhitespaceCroppedImage(originalImagePath, dateModified, length).ConfigureAwait(false);
+                var tuple = await GetWhitespaceCroppedImage(originalImagePath, dateModified).ConfigureAwait(false);
 
                 originalImagePath = tuple.Item1;
                 dateModified = tuple.Item2;
-                length = tuple.Item3;
             }
 
             if (options.Enhancers.Count > 0)
             {
                 var tuple = await GetEnhancedImage(new ItemImageInfo
                 {
-                    Length = length,
                     DateModified = dateModified,
                     Type = options.Image.Type,
                     Path = originalImagePath
@@ -182,7 +179,6 @@ namespace Emby.Drawing
 
                 originalImagePath = tuple.Item1;
                 dateModified = tuple.Item2;
-                length = tuple.Item3;
             }
 
             var originalImageSize = GetImageSize(originalImagePath, dateModified);
@@ -199,7 +195,7 @@ namespace Emby.Drawing
             var quality = options.Quality ?? 90;
 
             var outputFormat = GetOutputFormat(options.OutputFormat);
-            var cacheFilePath = GetCacheFilePath(originalImagePath, newSize, quality, dateModified, length, outputFormat, options.AddPlayedIndicator, options.PercentPlayed, options.UnplayedCount, options.BackgroundColor);
+            var cacheFilePath = GetCacheFilePath(originalImagePath, newSize, quality, dateModified, outputFormat, options.AddPlayedIndicator, options.PercentPlayed, options.UnplayedCount, options.BackgroundColor);
 
             var semaphore = GetLock(cacheFilePath);
 
@@ -240,11 +236,10 @@ namespace Emby.Drawing
         /// <summary>
         /// Crops whitespace from an image, caches the result, and returns the cached path
         /// </summary>
-        private async Task<Tuple<string, DateTime, long>> GetWhitespaceCroppedImage(string originalImagePath, DateTime dateModified, long length)
+        private async Task<Tuple<string, DateTime>> GetWhitespaceCroppedImage(string originalImagePath, DateTime dateModified)
         {
             var name = originalImagePath;
             name += "datemodified=" + dateModified.Ticks;
-            name += "length=" + length;
 
             var croppedImagePath = GetCachePath(CroppedWhitespaceImageCachePath, name, Path.GetExtension(originalImagePath));
 
@@ -270,7 +265,7 @@ namespace Emby.Drawing
                 // We have to have a catch-all here because some of the .net image methods throw a plain old Exception
                 _logger.ErrorException("Error cropping image {0}", ex, originalImagePath);
 
-                return new Tuple<string, DateTime, long>(originalImagePath, dateModified, length);
+                return new Tuple<string, DateTime>(originalImagePath, dateModified);
             }
             finally
             {
@@ -280,11 +275,9 @@ namespace Emby.Drawing
             return GetResult(croppedImagePath);
         }
 
-        private Tuple<string, DateTime, long> GetResult(string path)
+        private Tuple<string, DateTime> GetResult(string path)
         {
-            var file = new FileInfo(path);
-
-            return new Tuple<string, DateTime, long>(path, _fileSystem.GetLastWriteTimeUtc(file), file.Length);
+            return new Tuple<string, DateTime>(path, _fileSystem.GetLastWriteTimeUtc(path));
         }
 
         /// <summary>
@@ -295,7 +288,7 @@ namespace Emby.Drawing
         /// <summary>
         /// Gets the cache file path based on a set of parameters
         /// </summary>
-        private string GetCacheFilePath(string originalPath, ImageSize outputSize, int quality, DateTime dateModified, long length, ImageFormat format, bool addPlayedIndicator, double percentPlayed, int? unwatchedCount, string backgroundColor)
+        private string GetCacheFilePath(string originalPath, ImageSize outputSize, int quality, DateTime dateModified, ImageFormat format, bool addPlayedIndicator, double percentPlayed, int? unwatchedCount, string backgroundColor)
         {
             var filename = originalPath;
 
@@ -306,7 +299,6 @@ namespace Emby.Drawing
             filename += "quality=" + quality;
 
             filename += "datemodified=" + dateModified.Ticks;
-            filename += "length=" + length;
 
             filename += "f=" + format;
 
@@ -492,17 +484,16 @@ namespace Emby.Drawing
             var originalImagePath = image.Path;
             var dateModified = image.DateModified;
             var imageType = image.Type;
-            var length = image.Length;
 
             // Optimization
             if (imageEnhancers.Count == 0)
             {
-                return (originalImagePath + dateModified.Ticks + string.Empty + length).GetMD5().ToString("N");
+                return (originalImagePath + dateModified.Ticks).GetMD5().ToString("N");
             }
 
             // Cache name is created with supported enhancers combined with the last config change so we pick up new config changes
             var cacheKeys = imageEnhancers.Select(i => i.GetConfigurationCacheKey(item, imageType)).ToList();
-            cacheKeys.Add(originalImagePath + dateModified.Ticks + string.Empty + length);
+            cacheKeys.Add(originalImagePath + dateModified.Ticks);
 
             return string.Join("|", cacheKeys.ToArray()).GetMD5().ToString("N");
         }
@@ -525,7 +516,7 @@ namespace Emby.Drawing
             return result.Item1;
         }
 
-        private async Task<Tuple<string, DateTime, long>> GetEnhancedImage(ItemImageInfo image,
+        private async Task<Tuple<string, DateTime>> GetEnhancedImage(ItemImageInfo image,
             IHasImages item,
             int imageIndex,
             List<IImageEnhancer> enhancers)
@@ -533,7 +524,6 @@ namespace Emby.Drawing
             var originalImagePath = image.Path;
             var dateModified = image.DateModified;
             var imageType = image.Type;
-            var length = image.Length;
 
             try
             {
@@ -553,7 +543,7 @@ namespace Emby.Drawing
                 _logger.Error("Error enhancing image", ex);
             }
 
-            return new Tuple<string, DateTime, long>(originalImagePath, dateModified, length);
+            return new Tuple<string, DateTime>(originalImagePath, dateModified);
         }
 
         /// <summary>

+ 74 - 13
MediaBrowser.Api/ApiEntryPoint.cs

@@ -2,6 +2,7 @@
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.IO;
 using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Plugins;
 using MediaBrowser.Controller.Session;
 using MediaBrowser.Model.Configuration;
@@ -40,6 +41,7 @@ namespace MediaBrowser.Api
 
         private readonly ISessionManager _sessionManager;
         private readonly IFileSystem _fileSystem;
+        private readonly IMediaSourceManager _mediaSourceManager;
 
         public readonly SemaphoreSlim TranscodingStartLock = new SemaphoreSlim(1, 1);
 
@@ -49,12 +51,15 @@ namespace MediaBrowser.Api
         /// <param name="logger">The logger.</param>
         /// <param name="sessionManager">The session manager.</param>
         /// <param name="config">The configuration.</param>
-        public ApiEntryPoint(ILogger logger, ISessionManager sessionManager, IServerConfigurationManager config, IFileSystem fileSystem)
+        /// <param name="fileSystem">The file system.</param>
+        /// <param name="mediaSourceManager">The media source manager.</param>
+        public ApiEntryPoint(ILogger logger, ISessionManager sessionManager, IServerConfigurationManager config, IFileSystem fileSystem, IMediaSourceManager mediaSourceManager)
         {
             Logger = logger;
             _sessionManager = sessionManager;
             _config = config;
             _fileSystem = fileSystem;
+            _mediaSourceManager = mediaSourceManager;
 
             Instance = this;
         }
@@ -114,7 +119,7 @@ namespace MediaBrowser.Api
         {
             var jobCount = _activeTranscodingJobs.Count;
 
-            Parallel.ForEach(_activeTranscodingJobs.ToList(), j => KillTranscodingJob(j, path => true));
+            Parallel.ForEach(_activeTranscodingJobs.ToList(), 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)
@@ -133,6 +138,7 @@ namespace MediaBrowser.Api
         /// </summary>
         /// <param name="path">The path.</param>
         /// <param name="playSessionId">The play session identifier.</param>
+        /// <param name="liveStreamId">The live stream identifier.</param>
         /// <param name="transcodingJobId">The transcoding job identifier.</param>
         /// <param name="type">The type.</param>
         /// <param name="process">The process.</param>
@@ -142,6 +148,7 @@ namespace MediaBrowser.Api
         /// <returns>TranscodingJob.</returns>
         public TranscodingJob OnTranscodeBeginning(string path,
             string playSessionId,
+            string liveStreamId,
             string transcodingJobId,
             TranscodingJobType type,
             Process process,
@@ -160,7 +167,8 @@ namespace MediaBrowser.Api
                     DeviceId = deviceId,
                     CancellationTokenSource = cancellationTokenSource,
                     Id = transcodingJobId,
-                    PlaySessionId = playSessionId
+                    PlaySessionId = playSessionId,
+                    LiveStreamId = liveStreamId
                 };
 
                 _activeTranscodingJobs.Add(job);
@@ -323,7 +331,7 @@ namespace MediaBrowser.Api
             }
         }
 
-        private void PingTimer(TranscodingJob job, bool isProgressCheckIn)
+        private async void PingTimer(TranscodingJob job, bool isProgressCheckIn)
         {
             if (job.HasExited)
             {
@@ -331,7 +339,6 @@ namespace MediaBrowser.Api
                 return;
             }
 
-            // TODO: Lower this hls timeout
             var timerDuration = job.Type == TranscodingJobType.Progressive ?
                 1000 :
                 1800000;
@@ -339,17 +346,32 @@ namespace MediaBrowser.Api
             // We can really reduce the timeout for apps that are using the newer api
             if (!string.IsNullOrWhiteSpace(job.PlaySessionId) && job.Type != TranscodingJobType.Progressive)
             {
-                timerDuration = 20000;
+                timerDuration = 60000;
             }
 
+            job.PingTimeout = timerDuration;
+            job.LastPingDate = DateTime.UtcNow;
+
             // Don't start the timer for playback checkins with progressive streaming
             if (job.Type != TranscodingJobType.Progressive || !isProgressCheckIn)
             {
-                job.StartKillTimer(timerDuration, OnTranscodeKillTimerStopped);
+                job.StartKillTimer(OnTranscodeKillTimerStopped);
             }
             else
             {
-                job.ChangeKillTimerIfStarted(timerDuration);
+                job.ChangeKillTimerIfStarted();
+            }
+
+            if (!string.IsNullOrWhiteSpace(job.LiveStreamId))
+            {
+                try
+                {
+                    await _mediaSourceManager.PingLiveStream(job.LiveStreamId, CancellationToken.None).ConfigureAwait(false);
+                }
+                catch (Exception ex)
+                {
+                    Logger.ErrorException("Error closing live stream", ex);
+                }
             }
         }
 
@@ -361,9 +383,20 @@ namespace MediaBrowser.Api
         {
             var job = (TranscodingJob)state;
 
+            if (!job.HasExited && job.Type != TranscodingJobType.Progressive)
+            {
+                var timeSinceLastPing = (DateTime.UtcNow - job.LastPingDate).TotalMilliseconds;
+
+                if (timeSinceLastPing < job.PingTimeout)
+                {
+                    job.StartKillTimer(OnTranscodeKillTimerStopped, job.PingTimeout);
+                    return;
+                }
+            }
+
             Logger.Debug("Transcoding kill timer stopped for JobId {0} PlaySessionId {1}. Killing transcoding", job.Id, job.PlaySessionId);
 
-            KillTranscodingJob(job, path => true);
+            KillTranscodingJob(job, true, path => true);
         }
 
         /// <summary>
@@ -411,7 +444,7 @@ namespace MediaBrowser.Api
 
             foreach (var job in jobs)
             {
-                KillTranscodingJob(job, deleteFiles);
+                KillTranscodingJob(job, false, deleteFiles);
             }
         }
 
@@ -419,8 +452,9 @@ namespace MediaBrowser.Api
         /// Kills the transcoding job.
         /// </summary>
         /// <param name="job">The job.</param>
+        /// <param name="closeLiveStream">if set to <c>true</c> [close live stream].</param>
         /// <param name="delete">The delete.</param>
-        private void KillTranscodingJob(TranscodingJob job, Func<string, bool> delete)
+        private async void KillTranscodingJob(TranscodingJob job, bool closeLiveStream, Func<string, bool> delete)
         {
             job.DisposeKillTimer();
 
@@ -470,6 +504,18 @@ namespace MediaBrowser.Api
             {
                 DeletePartialStreamFiles(job.Path, job.Type, 0, 1500);
             }
+
+            if (closeLiveStream && !string.IsNullOrWhiteSpace(job.LiveStreamId))
+            {
+                try
+                {
+                    await _mediaSourceManager.CloseLiveStream(job.LiveStreamId, CancellationToken.None).ConfigureAwait(false);
+                }
+                catch (Exception ex)
+                {
+                    Logger.ErrorException("Error closing live stream for {0}", ex, job.Path);
+                }
+            }
         }
 
         private async void DeletePartialStreamFiles(string path, TranscodingJobType jobType, int retryCount, int delayMs)
@@ -578,6 +624,11 @@ namespace MediaBrowser.Api
         /// <value>The play session identifier.</value>
         public string PlaySessionId { get; set; }
         /// <summary>
+        /// Gets or sets the live stream identifier.
+        /// </summary>
+        /// <value>The live stream identifier.</value>
+        public string LiveStreamId { get; set; }
+        /// <summary>
         /// Gets or sets the path.
         /// </summary>
         /// <value>The path.</value>
@@ -627,6 +678,9 @@ namespace MediaBrowser.Api
 
         private readonly object _timerLock = new object();
 
+        public DateTime LastPingDate { get; set; }
+        public int PingTimeout { get; set; }
+
         public TranscodingJob(ILogger logger)
         {
             Logger = logger;
@@ -655,7 +709,12 @@ namespace MediaBrowser.Api
             }
         }
 
-        public void StartKillTimer(int intervalMs, TimerCallback callback)
+        public void StartKillTimer(TimerCallback callback)
+        {
+            StartKillTimer(callback, PingTimeout);
+        }
+
+        public void StartKillTimer(TimerCallback callback, int intervalMs)
         {
             CheckHasExited();
 
@@ -674,7 +733,7 @@ namespace MediaBrowser.Api
             }
         }
 
-        public void ChangeKillTimerIfStarted(int intervalMs)
+        public void ChangeKillTimerIfStarted()
         {
             CheckHasExited();
 
@@ -682,6 +741,8 @@ namespace MediaBrowser.Api
             {
                 if (KillTimer != null)
                 {
+                    var intervalMs = PingTimeout;
+
                     Logger.Debug("Changing kill timer to {0}ms. JobId {1} PlaySessionId {2}", intervalMs, Id, PlaySessionId);
                     KillTimer.Change(intervalMs, Timeout.Infinite);
                 }

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

@@ -1010,6 +1010,7 @@ namespace MediaBrowser.Api.Playback
 
             var transcodingJob = ApiEntryPoint.Instance.OnTranscodeBeginning(outputPath,
                 state.Request.PlaySessionId,
+                state.MediaSource.LiveStreamId,
                 transcodingId,
                 TranscodingJobType,
                 process,

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

@@ -543,7 +543,9 @@ namespace MediaBrowser.Api.Playback.Hls
                 return false;
             }
 
-            return state.VideoRequest.VideoBitRate.HasValue;
+            // Having problems in android
+            return false;
+            //return state.VideoRequest.VideoBitRate.HasValue;
         }
 
         private void AppendPlaylist(StringBuilder builder, string url, int bitrate, string subtitleGroup)

+ 3 - 3
MediaBrowser.Api/UserLibrary/UserViewsService.cs

@@ -88,7 +88,7 @@ namespace MediaBrowser.Api.UserLibrary
 
             var views = user.RootFolder
                 .GetChildren(user, true)
-                .OfType<CollectionFolder>()
+                .OfType<ICollectionFolder>()
                 .Where(i => IsEligibleForSpecialView(i))
                 .ToList();
 
@@ -105,9 +105,9 @@ namespace MediaBrowser.Api.UserLibrary
             return ToOptimizedResult(list);
         }
 
-        private bool IsEligibleForSpecialView(CollectionFolder view)
+        private bool IsEligibleForSpecialView(ICollectionFolder view)
         {
-            var types = new[] { CollectionType.Movies, CollectionType.TvShows, CollectionType.Games, CollectionType.Music };
+            var types = new[] { CollectionType.Movies, CollectionType.TvShows, CollectionType.Games, CollectionType.Music, CollectionType.Photos };
 
             return types.Contains(view.CollectionType ?? string.Empty, StringComparer.OrdinalIgnoreCase);
         }

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

@@ -1512,7 +1512,6 @@ namespace MediaBrowser.Controller.Entities
 
                 image.Path = file.FullName;
                 image.DateModified = imageInfo.DateModified;
-                image.Length = imageInfo.Length;
             }
         }
 
@@ -1622,14 +1621,11 @@ namespace MediaBrowser.Controller.Entities
                     return null;
                 }
 
-                var fileInfo = new FileInfo(path);
-
                 return new ItemImageInfo
                 {
                     Path = path,
-                    DateModified = FileSystem.GetLastWriteTimeUtc(fileInfo),
-                    Type = imageType,
-                    Length = fileInfo.Length
+                    DateModified = FileSystem.GetLastWriteTimeUtc(path),
+                    Type = imageType
                 };
             }
 
@@ -1690,7 +1686,6 @@ namespace MediaBrowser.Controller.Entities
                 else
                 {
                     existing.DateModified = FileSystem.GetLastWriteTimeUtc(newImage);
-                    existing.Length = ((FileInfo)newImage).Length;
                 }
             }
 
@@ -1716,8 +1711,7 @@ namespace MediaBrowser.Controller.Entities
             {
                 Path = file.FullName,
                 Type = type,
-                DateModified = FileSystem.GetLastWriteTimeUtc(file),
-                Length = ((FileInfo)file).Length
+                DateModified = FileSystem.GetLastWriteTimeUtc(file)
             };
         }
 
@@ -1756,15 +1750,9 @@ namespace MediaBrowser.Controller.Entities
 
             FileSystem.SwapFiles(path1, path2);
 
-            var file1 = new FileInfo(info1.Path);
-            var file2 = new FileInfo(info2.Path);
-
             // Refresh these values
-            info1.DateModified = FileSystem.GetLastWriteTimeUtc(file1);
-            info2.DateModified = FileSystem.GetLastWriteTimeUtc(file2);
-
-            info1.Length = file1.Length;
-            info2.Length = file2.Length;
+            info1.DateModified = FileSystem.GetLastWriteTimeUtc(info1.Path);
+            info2.DateModified = FileSystem.GetLastWriteTimeUtc(info2.Path);
 
             return UpdateToRepository(ItemUpdateType.ImageUpdate, CancellationToken.None);
         }

+ 0 - 6
MediaBrowser.Controller/Entities/ItemImageInfo.cs

@@ -11,12 +11,6 @@ namespace MediaBrowser.Controller.Entities
         /// <value>The path.</value>
         public string Path { get; set; }
 
-        /// <summary>
-        /// Gets or sets the length.
-        /// </summary>
-        /// <value>The length.</value>
-        public long Length { get; set; }
-
         /// <summary>
         /// Gets or sets the type.
         /// </summary>

+ 24 - 9
MediaBrowser.Controller/Entities/UserViewBuilder.cs

@@ -121,7 +121,6 @@ namespace MediaBrowser.Controller.Entities
                     }
 
                 case CollectionType.Books:
-                case CollectionType.Photos:
                 case CollectionType.HomeVideos:
                 case CollectionType.MusicVideos:
                     return GetResult(queryParent.GetChildren(user, true), queryParent, query);
@@ -138,6 +137,9 @@ namespace MediaBrowser.Controller.Entities
                 case CollectionType.BoxSets:
                     return await GetBoxsetView(queryParent, user, query).ConfigureAwait(false);
 
+                case CollectionType.Photos:
+                    return await GetPhotosView(queryParent, user, query).ConfigureAwait(false);
+
                 case CollectionType.TvShows:
                     return await GetTvView(queryParent, user, query).ConfigureAwait(false);
 
@@ -247,16 +249,16 @@ namespace MediaBrowser.Controller.Entities
                     return GetFavoriteSongs(queryParent, user, query);
 
                 default:
-                {
-                    if (queryParent is UserView)
-                    {
-                        return GetResult(GetMediaFolders(user).SelectMany(i => i.GetChildren(user, true)), queryParent, query);
-                    }
-                    else
                     {
-                        return GetResult(queryParent.GetChildren(user, true), queryParent, query);
+                        if (queryParent is UserView)
+                        {
+                            return GetResult(GetMediaFolders(user).SelectMany(i => i.GetChildren(user, true)), queryParent, query);
+                        }
+                        else
+                        {
+                            return GetResult(queryParent.GetChildren(user, true), queryParent, query);
+                        }
                     }
-                }
             }
         }
 
@@ -645,6 +647,19 @@ namespace MediaBrowser.Controller.Entities
             }), parent, query);
         }
 
+        private async Task<QueryResult<BaseItem>> GetPhotosView(Folder queryParent, User user, InternalItemsQuery query)
+        {
+            if (query.Recursive)
+            {
+                var mediaTypes = new[] { MediaType.Video, MediaType.Photo };
+                var items = GetRecursiveChildren(queryParent, user, new[] { CollectionType.Photos, string.Empty }, i => (i is PhotoAlbum || mediaTypes.Contains(i.MediaType ?? string.Empty, StringComparer.OrdinalIgnoreCase)) && FilterItem(i, query));
+
+                return PostFilterAndSort(items, queryParent, null, query);
+            }
+
+            return GetResult(queryParent.GetChildren(user, true), queryParent, query);
+        }
+
         private async Task<QueryResult<BaseItem>> GetTvView(Folder parent, User user, InternalItemsQuery query)
         {
             if (query.Recursive)

+ 2 - 0
MediaBrowser.Controller/Library/ILibraryManager.cs

@@ -309,6 +309,7 @@ namespace MediaBrowser.Controller.Library
         /// <param name="parentId">The parent identifier.</param>
         /// <param name="viewType">Type of the view.</param>
         /// <param name="sortName">Name of the sort.</param>
+        /// <param name="uniqueId">The unique identifier.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <returns>Task&lt;UserView&gt;.</returns>
         Task<UserView> GetNamedView(User user,
@@ -316,6 +317,7 @@ namespace MediaBrowser.Controller.Library
             string parentId,
             string viewType, 
             string sortName, 
+            string uniqueId,
             CancellationToken cancellationToken);
 
         /// <summary>

+ 8 - 2
MediaBrowser.Controller/LiveTv/LiveTvTunerInfo.cs

@@ -1,5 +1,5 @@
-using System.Collections.Generic;
-using MediaBrowser.Model.LiveTv;
+using MediaBrowser.Model.LiveTv;
+using System.Collections.Generic;
 
 namespace MediaBrowser.Controller.LiveTv
 {
@@ -23,6 +23,12 @@ namespace MediaBrowser.Controller.LiveTv
         /// <value>The identifier.</value>
         public string Id { get; set; }
 
+        /// <summary>
+        /// Gets or sets the URL.
+        /// </summary>
+        /// <value>The URL.</value>
+        public string Url { get; set; }
+        
         /// <summary>
         /// Gets or sets the status.
         /// </summary>

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

@@ -488,6 +488,9 @@
     <Compile Include="..\MediaBrowser.Model\Dto\ItemIndex.cs">
       <Link>Dto\ItemIndex.cs</Link>
     </Compile>
+    <Compile Include="..\MediaBrowser.Model\Dto\ItemLayout.cs">
+      <Link>Dto\ItemLayout.cs</Link>
+    </Compile>
     <Compile Include="..\MediaBrowser.Model\Dto\MediaSourceInfo.cs">
       <Link>Dto\MediaSourceInfo.cs</Link>
     </Compile>

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

@@ -453,6 +453,9 @@
     <Compile Include="..\MediaBrowser.Model\Dto\ItemIndex.cs">
       <Link>Dto\ItemIndex.cs</Link>
     </Compile>
+    <Compile Include="..\MediaBrowser.Model\Dto\ItemLayout.cs">
+      <Link>Dto\ItemLayout.cs</Link>
+    </Compile>
     <Compile Include="..\MediaBrowser.Model\Dto\MediaSourceInfo.cs">
       <Link>Dto\MediaSourceInfo.cs</Link>
     </Compile>

+ 14 - 1
MediaBrowser.Model/ApiClient/ServerCredentials.cs

@@ -98,7 +98,7 @@ namespace MediaBrowser.Model.ApiClient
         {
             var index = 0;
 
-            foreach (var server in servers)
+            foreach (ServerInfo server in servers)
             {
                 if (StringHelper.EqualsIgnoreCase(id, server.Id))
                 {
@@ -110,5 +110,18 @@ namespace MediaBrowser.Model.ApiClient
 
             return -1;
         }
+
+        public ServerInfo GetServer(string id)
+        {
+            foreach (ServerInfo server in Servers)
+            {
+                if (StringHelper.EqualsIgnoreCase(id, server.Id))
+                {
+                    return server;
+                }
+            }
+
+            return null;
+        }
     }
 }

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

@@ -49,7 +49,7 @@ namespace MediaBrowser.Model.Configuration
         /// </summary>
         /// <value><c>true</c> if [enable user specific user views]; otherwise, <c>false</c>.</value>
         public bool EnableUserSpecificUserViews { get; set; }
-
+        
         /// <summary>
         /// Gets or sets the value pointing to the file system where the ssl certiifcate is located..
         /// </summary>

+ 8 - 13
MediaBrowser.Model/Dlna/StreamBuilder.cs

@@ -277,7 +277,7 @@ namespace MediaBrowser.Model.Dlna
                 
                 // The profile describes what the device supports
                 // If device requirements are satisfied then allow both direct stream and direct play
-                if (IsAudioEligibleForDirectPlay(item, GetBitrateForDirectPlayCheck(item, options)))
+                if (item.SupportsDirectPlay && IsAudioEligibleForDirectPlay(item, GetBitrateForDirectPlayCheck(item, options)))
                 {
                     playMethods.Add(PlayMethod.DirectPlay);
                 }
@@ -456,10 +456,8 @@ namespace MediaBrowser.Model.Dlna
                     playlistItem.MaxAudioChannels = Math.Min(options.MaxAudioChannels.Value, currentValue);
                 }
 
-                if (!playlistItem.AudioBitrate.HasValue)
-                {
-                    playlistItem.AudioBitrate = GetAudioBitrate(playlistItem.TargetAudioChannels, playlistItem.TargetAudioCodec);
-                }
+                int audioBitrate = GetAudioBitrate(playlistItem.TargetAudioChannels, playlistItem.TargetAudioCodec);
+                playlistItem.AudioBitrate = Math.Min(playlistItem.AudioBitrate ?? audioBitrate, audioBitrate);
 
                 int? maxBitrateSetting = options.GetMaxBitrate();
                 // Honor max rate
@@ -472,9 +470,9 @@ namespace MediaBrowser.Model.Dlna
                         videoBitrate -= playlistItem.AudioBitrate.Value;
                     }
 
+                    // Make sure the video bitrate is lower than bitrate settings but at least 64k
                     int currentValue = playlistItem.VideoBitrate ?? videoBitrate;
-
-                    playlistItem.VideoBitrate = Math.Min(videoBitrate, currentValue);
+                    playlistItem.VideoBitrate = Math.Max(Math.Min(videoBitrate, currentValue), 64000);
                 }
             }
 
@@ -640,7 +638,7 @@ namespace MediaBrowser.Model.Dlna
                 }
             }
 
-            if (isEligibleForDirectPlay)
+            if (isEligibleForDirectPlay && mediaSource.SupportsDirectPlay)
             {
                 if (mediaSource.Protocol == MediaProtocol.Http)
                 {
@@ -659,12 +657,9 @@ namespace MediaBrowser.Model.Dlna
                 }
             }
 
-            if (isEligibleForDirectStream)
+            if (isEligibleForDirectStream && mediaSource.SupportsDirectStream)
             {
-                if (mediaSource.SupportsDirectStream)
-                {
-                    return PlayMethod.DirectStream;
-                }
+                return PlayMethod.DirectStream;
             }
 
             return null;

+ 72 - 0
MediaBrowser.Model/Dto/ItemLayout.cs

@@ -0,0 +1,72 @@
+using System;
+using System.Collections.Generic;
+
+namespace MediaBrowser.Model.Dto
+{
+    public static class ItemLayout
+    {
+        public static double? GetDisplayAspectRatio(BaseItemDto item)
+        {
+            List<BaseItemDto> items = new List<BaseItemDto>();
+            items.Add(item);
+            return GetDisplayAspectRatio(items);
+        }
+
+        public static double? GetDisplayAspectRatio(List<BaseItemDto> items)
+        {
+            List<double> values = new List<double>();
+
+            foreach (BaseItemDto item in items)
+            {
+                if (item.PrimaryImageAspectRatio.HasValue)
+                {
+                    values.Add(item.PrimaryImageAspectRatio.Value);
+                }
+            }
+
+            if (values.Count == 0)
+            {
+                return null;
+            }
+
+            values.Sort();
+
+            double halfDouble = values.Count;
+            halfDouble /= 2;
+            int half = Convert.ToInt32(Math.Floor(halfDouble));
+
+            double result;
+
+            if (values.Count % 2 > 0)
+                result = values[half];
+            else
+                result = (values[half - 1] + values[half]) / 2.0;
+
+            // If really close to 2:3 (poster image), just return 2:3
+            if (Math.Abs(0.66666666667 - result) <= .15) 
+            {
+                return 0.66666666667;
+            }
+
+            // If really close to 16:9 (episode image), just return 16:9
+            if (Math.Abs(1.777777778 - result) <= .2)
+            {
+                return 1.777777778;
+            }
+
+            // If really close to 1 (square image), just return 1
+            if (Math.Abs(1 - result) <= .15)
+            {
+                return 1.0;
+            }
+
+            // If really close to 4:3 (poster image), just return 2:3
+            if (Math.Abs(1.33333333333 - result) <= .15)
+            {
+                return 1.33333333333;
+            }
+
+            return result;
+        }
+    }
+}

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

@@ -22,6 +22,12 @@ namespace MediaBrowser.Model.LiveTv
         /// <value>The identifier.</value>
         public string Id { get; set; }
 
+        /// <summary>
+        /// Gets or sets the URL.
+        /// </summary>
+        /// <value>The URL.</value>
+        public string Url { get; set; }
+        
         /// <summary>
         /// Gets or sets the status.
         /// </summary>

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

@@ -139,6 +139,7 @@
     <Compile Include="Drawing\ImageOrientation.cs" />
     <Compile Include="Dto\IHasServerId.cs" />
     <Compile Include="Dto\IHasSyncInfo.cs" />
+    <Compile Include="Dto\ItemLayout.cs" />
     <Compile Include="Dto\MetadataEditorInfo.cs" />
     <Compile Include="Dto\NameIdPair.cs" />
     <Compile Include="Dto\NameValuePair.cs" />

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

@@ -384,7 +384,6 @@ namespace MediaBrowser.Providers.Manager
                     else
                     {
                         currentImage.DateModified = _fileSystem.GetLastWriteTimeUtc(image.FileInfo);
-                        currentImage.Length = ((FileInfo) image.FileInfo).Length;
                     }
                 }
                 else

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

@@ -482,6 +482,11 @@ namespace MediaBrowser.Providers.Manager
 
         protected virtual bool IsFullLocalMetadata(TItemType item)
         {
+            if (string.IsNullOrWhiteSpace(item.Name))
+            {
+                return false;
+            }
+            
             return true;
         }
 

+ 47 - 24
MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs

@@ -218,7 +218,7 @@ namespace MediaBrowser.Providers.MediaInfo
 
             await AddExternalSubtitles(video, mediaStreams, options, cancellationToken).ConfigureAwait(false);
 
-            FetchEmbeddedInfo(video, mediaInfo);
+            FetchEmbeddedInfo(video, mediaInfo, options);
 
             video.IsHD = mediaStreams.Any(i => i.Type == MediaStreamType.Video && i.Width.HasValue && i.Width.Value >= 1270);
 
@@ -358,11 +358,13 @@ namespace MediaBrowser.Providers.MediaInfo
             return _blurayExaminer.GetDiscInfo(path);
         }
 
-        private void FetchEmbeddedInfo(Video video, Model.MediaInfo.MediaInfo data)
+        private void FetchEmbeddedInfo(Video video, Model.MediaInfo.MediaInfo data, MetadataRefreshOptions options)
         {
+            var isFullRefresh = options.MetadataRefreshMode == MetadataRefreshMode.FullRefresh;
+
             if (!video.LockedFields.Contains(MetadataFields.OfficialRating))
             {
-                if (!string.IsNullOrWhiteSpace(data.OfficialRating))
+                if (!string.IsNullOrWhiteSpace(data.OfficialRating) || isFullRefresh)
                 {
                     video.OfficialRating = data.OfficialRating;
                 }
@@ -370,54 +372,75 @@ namespace MediaBrowser.Providers.MediaInfo
 
             if (!video.LockedFields.Contains(MetadataFields.Cast))
             {
-                video.People.Clear();
-
-                foreach (var person in data.People)
+                if (video.People.Count == 0 || isFullRefresh)
                 {
-                    video.AddPerson(new PersonInfo
+                    video.People.Clear();
+
+                    foreach (var person in data.People)
                     {
-                        Name = person.Name,
-                        Type = person.Type,
-                        Role = person.Role
-                    });
+                        video.AddPerson(new PersonInfo
+                        {
+                            Name = person.Name,
+                            Type = person.Type,
+                            Role = person.Role
+                        });
+                    }
                 }
             }
 
             if (!video.LockedFields.Contains(MetadataFields.Genres))
             {
-                video.Genres.Clear();
-
-                foreach (var genre in data.Genres)
+                if (video.Genres.Count == 0 || isFullRefresh)
                 {
-                    video.AddGenre(genre);
+                    video.Genres.Clear();
+
+                    foreach (var genre in data.Genres)
+                    {
+                        video.AddGenre(genre);
+                    }
                 }
             }
 
             if (!video.LockedFields.Contains(MetadataFields.Studios))
             {
-                video.Studios.Clear();
-
-                foreach (var studio in data.Studios)
+                if (video.Studios.Count == 0 || isFullRefresh)
                 {
-                    video.AddStudio(studio);
+                    video.Studios.Clear();
+
+                    foreach (var studio in data.Studios)
+                    {
+                        video.AddStudio(studio);
+                    }
                 }
             }
 
             if (data.ProductionYear.HasValue)
             {
-                video.ProductionYear = data.ProductionYear;
+                if (!video.ProductionYear.HasValue || isFullRefresh)
+                {
+                    video.ProductionYear = data.ProductionYear;
+                }
             }
             if (data.PremiereDate.HasValue)
             {
-                video.PremiereDate = data.PremiereDate;
+                if (!video.PremiereDate.HasValue || isFullRefresh)
+                {
+                    video.PremiereDate = data.PremiereDate;
+                }
             }
             if (data.IndexNumber.HasValue)
             {
-                video.IndexNumber = data.IndexNumber;
+                if (!video.IndexNumber.HasValue || isFullRefresh)
+                {
+                    video.IndexNumber = data.IndexNumber;
+                }
             }
             if (data.ParentIndexNumber.HasValue)
             {
-                video.ParentIndexNumber = data.ParentIndexNumber;
+                if (!video.ParentIndexNumber.HasValue || isFullRefresh)
+                {
+                    video.ParentIndexNumber = data.ParentIndexNumber;
+                }
             }
 
             // If we don't have a ProductionYear try and get it from PremiereDate
@@ -428,7 +451,7 @@ namespace MediaBrowser.Providers.MediaInfo
 
             if (!video.LockedFields.Contains(MetadataFields.Overview))
             {
-                if (!string.IsNullOrWhiteSpace(data.Overview))
+                if (string.IsNullOrWhiteSpace(video.Overview) || isFullRefresh)
                 {
                     video.Overview = data.Overview;
                 }

+ 0 - 4
MediaBrowser.Providers/Movies/MovieMetadataService.cs

@@ -36,10 +36,6 @@ namespace MediaBrowser.Providers.Movies
 
         protected override bool IsFullLocalMetadata(Movie item)
         {
-            if (string.IsNullOrWhiteSpace(item.Name))
-            {
-                return false;
-            }
             if (string.IsNullOrWhiteSpace(item.Overview))
             {
                 return false;

+ 0 - 4
MediaBrowser.Providers/TV/SeriesMetadataService.cs

@@ -77,10 +77,6 @@ namespace MediaBrowser.Providers.TV
 
         protected override bool IsFullLocalMetadata(Series item)
         {
-            if (string.IsNullOrWhiteSpace(item.Name))
-            {
-                return false;
-            }
             if (string.IsNullOrWhiteSpace(item.Overview))
             {
                 return false;

+ 9 - 1
MediaBrowser.Providers/TV/TvdbSeriesProvider.cs

@@ -243,7 +243,15 @@ namespace MediaBrowser.Providers.TV
                 await SanitizeXmlFile(file).ConfigureAwait(false);
             }
 
-            await ExtractEpisodes(seriesDataPath, Path.Combine(seriesDataPath, saveAsMetadataLanguage + ".xml"), lastTvDbUpdateTime).ConfigureAwait(false);
+            var downloadLangaugeXmlFile = Path.Combine(seriesDataPath, preferredMetadataLanguage + ".xml");
+            var saveAsLanguageXmlFile = Path.Combine(seriesDataPath, saveAsMetadataLanguage + ".xml");
+
+            if (!string.Equals(downloadLangaugeXmlFile, saveAsLanguageXmlFile, StringComparison.OrdinalIgnoreCase))
+            {
+                File.Copy(downloadLangaugeXmlFile, saveAsLanguageXmlFile, true);
+            }
+
+            await ExtractEpisodes(seriesDataPath, downloadLangaugeXmlFile, lastTvDbUpdateTime).ConfigureAwait(false);
         }
 
         public TvdbOptions GetTvDbOptions()

+ 1 - 4
MediaBrowser.Server.Implementations/Dto/DtoService.cs

@@ -828,14 +828,11 @@ namespace MediaBrowser.Server.Implementations.Dto
 
             if (!string.IsNullOrEmpty(chapterInfo.ImagePath))
             {
-                var file = new FileInfo(chapterInfo.ImagePath);
-
                 dto.ImageTag = GetImageCacheTag(item, new ItemImageInfo
                 {
                     Path = chapterInfo.ImagePath,
                     Type = ImageType.Chapter,
-                    DateModified = _fileSystem.GetLastWriteTimeUtc(file),
-                    Length = file.Length
+                    DateModified = _fileSystem.GetLastWriteTimeUtc(chapterInfo.ImagePath)
                 });
             }
 

+ 8 - 40
MediaBrowser.Server.Implementations/EntryPoints/ExternalPortForwarding.cs

@@ -1,5 +1,4 @@
-using System.Linq;
-using MediaBrowser.Controller;
+using MediaBrowser.Controller;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Plugins;
 using MediaBrowser.Model.Logging;
@@ -139,55 +138,24 @@ namespace MediaBrowser.Server.Implementations.EntryPoints
             // On some systems the device discovered event seems to fire repeatedly
             // This check will help ensure we're not trying to port map the same device over and over
 
-            List<Mapping> currentMappings = null;
-
-            try
-            {
-                currentMappings = device.GetAllMappings().ToList();
-            }
-            catch (NotSupportedException)
-            {
-            }
-
             var address = device.LocalAddress.ToString();
 
             if (!_createdRules.Contains(address))
             {
                 _createdRules.Add(address);
 
-                CreatePortMap(device, currentMappings, _appHost.HttpPort, _config.Configuration.PublicPort);
-                CreatePortMap(device, currentMappings, _appHost.HttpsPort, _config.Configuration.PublicHttpsPort);
+                CreatePortMap(device, _appHost.HttpPort, _config.Configuration.PublicPort);
+                CreatePortMap(device, _appHost.HttpsPort, _config.Configuration.PublicHttpsPort);
             }
         }
 
-        private void CreatePortMap(INatDevice device, List<Mapping> currentMappings, int privatePort, int publicPort)
+        private void CreatePortMap(INatDevice device, int privatePort, int publicPort)
         {
-            var hasMapping = false;
-
-            if (currentMappings != null)
-            {
-                hasMapping = currentMappings.Any(i => i.PublicPort == publicPort && i.PrivatePort == privatePort);
-            }
-            else
+            _logger.Debug("Creating port map on port {0}", privatePort);
+            device.CreatePortMap(new Mapping(Protocol.Tcp, privatePort, publicPort)
             {
-                try
-                {
-                    var mapping = device.GetSpecificMapping(Protocol.Tcp, publicPort);
-                    hasMapping = mapping != null;
-                }
-                catch (NotSupportedException)
-                {
-                }
-            }
-
-            if (!hasMapping)
-            {
-                _logger.Debug("Creating port map on port {0}", privatePort);
-                device.CreatePortMap(new Mapping(Protocol.Tcp, privatePort, publicPort)
-                {
-                    Description = _appHost.Name
-                });
-            }
+                Description = _appHost.Name
+            });
         }
 
         // As I said before, this method will be never invoked. You can remove it.

+ 12 - 4
MediaBrowser.Server.Implementations/Library/LibraryManager.cs

@@ -1602,7 +1602,7 @@ namespace MediaBrowser.Server.Implementations.Library
         {
             if (ConfigurationManager.Configuration.EnableUserSpecificUserViews)
             {
-                return await GetNamedViewInternal(user, name, null, viewType, sortName, cancellationToken)
+                return await GetNamedViewInternal(user, name, null, viewType, sortName, null, cancellationToken)
                             .ConfigureAwait(false);
             }
 
@@ -1662,6 +1662,7 @@ namespace MediaBrowser.Server.Implementations.Library
             string parentId,
             string viewType,
             string sortName,
+            string uniqueId,
             CancellationToken cancellationToken)
         {
             if (string.IsNullOrWhiteSpace(parentId))
@@ -1669,7 +1670,7 @@ namespace MediaBrowser.Server.Implementations.Library
                 throw new ArgumentNullException("parentId");
             }
 
-            return GetNamedViewInternal(user, name, parentId, viewType, sortName, cancellationToken);
+            return GetNamedViewInternal(user, name, parentId, viewType, sortName, uniqueId, cancellationToken);
         }
 
         private async Task<UserView> GetNamedViewInternal(User user,
@@ -1677,6 +1678,7 @@ namespace MediaBrowser.Server.Implementations.Library
             string parentId,
             string viewType,
             string sortName,
+            string uniqueId,
             CancellationToken cancellationToken)
         {
             if (string.IsNullOrWhiteSpace(name))
@@ -1684,7 +1686,13 @@ namespace MediaBrowser.Server.Implementations.Library
                 throw new ArgumentNullException("name");
             }
 
-            var id = GetNewItemId("37_namedview_" + name + user.Id.ToString("N") + (parentId ?? string.Empty), typeof(UserView));
+            var idValues = "37_namedview_" + name + user.Id.ToString("N") + (parentId ?? string.Empty);
+            if (!string.IsNullOrWhiteSpace(uniqueId))
+            {
+                idValues += uniqueId;
+            }
+
+            var id = GetNewItemId(idValues, typeof(UserView));
 
             var path = Path.Combine(ConfigurationManager.ApplicationPaths.InternalMetadataPath, "views", id.ToString("N"));
 
@@ -1723,7 +1731,7 @@ namespace MediaBrowser.Server.Implementations.Library
                 await item.UpdateToRepository(ItemUpdateType.MetadataEdit, cancellationToken).ConfigureAwait(false);
             }
 
-            var refresh = isNew || (DateTime.UtcNow - item.DateLastSaved).TotalHours >= 12;
+            var refresh = isNew || (DateTime.UtcNow - item.DateLastSaved).TotalHours >= 24;
 
             if (refresh)
             {

+ 27 - 18
MediaBrowser.Server.Implementations/Library/UserViewManager.cs

@@ -97,7 +97,7 @@ namespace MediaBrowser.Server.Implementations.Library
 
             if (parents.Count > 0)
             {
-                list.Add(await GetUserView(parents, CollectionType.TvShows, string.Empty, user, cancellationToken).ConfigureAwait(false));
+                list.Add(await GetUserView(parents, list, CollectionType.TvShows, string.Empty, user, cancellationToken).ConfigureAwait(false));
             }
 
             parents = foldersWithViewTypes.Where(i => string.Equals(i.CollectionType, CollectionType.Music, StringComparison.OrdinalIgnoreCase) || string.Equals(i.CollectionType, CollectionType.MusicVideos, StringComparison.OrdinalIgnoreCase) || string.IsNullOrWhiteSpace(i.CollectionType))
@@ -105,7 +105,7 @@ namespace MediaBrowser.Server.Implementations.Library
 
             if (parents.Count > 0)
             {
-                list.Add(await GetUserView(parents, CollectionType.Music, string.Empty, user, cancellationToken).ConfigureAwait(false));
+                list.Add(await GetUserView(parents, list, CollectionType.Music, string.Empty, user, cancellationToken).ConfigureAwait(false));
             }
 
             parents = foldersWithViewTypes.Where(i => string.Equals(i.CollectionType, CollectionType.Movies, StringComparison.OrdinalIgnoreCase) || string.IsNullOrWhiteSpace(i.CollectionType))
@@ -113,7 +113,7 @@ namespace MediaBrowser.Server.Implementations.Library
 
             if (parents.Count > 0)
             {
-                list.Add(await GetUserView(parents, CollectionType.Movies, string.Empty, user, cancellationToken).ConfigureAwait(false));
+                list.Add(await GetUserView(parents, list, CollectionType.Movies, string.Empty, user, cancellationToken).ConfigureAwait(false));
             }
 
             parents = foldersWithViewTypes.Where(i => string.Equals(i.CollectionType, CollectionType.Games, StringComparison.OrdinalIgnoreCase))
@@ -121,7 +121,7 @@ namespace MediaBrowser.Server.Implementations.Library
 
             if (parents.Count > 0)
             {
-                list.Add(await GetUserView(parents, CollectionType.Games, string.Empty, user, cancellationToken).ConfigureAwait(false));
+                list.Add(await GetUserView(parents, list, CollectionType.Games, string.Empty, user, cancellationToken).ConfigureAwait(false));
             }
 
             parents = foldersWithViewTypes.Where(i => string.Equals(i.CollectionType, CollectionType.BoxSets, StringComparison.OrdinalIgnoreCase))
@@ -129,7 +129,7 @@ namespace MediaBrowser.Server.Implementations.Library
 
             if (parents.Count > 0)
             {
-                list.Add(await GetUserView(parents, CollectionType.BoxSets, string.Empty, user, cancellationToken).ConfigureAwait(false));
+                list.Add(await GetUserView(parents, list, CollectionType.BoxSets, string.Empty, user, cancellationToken).ConfigureAwait(false));
             }
 
             parents = foldersWithViewTypes.Where(i => string.Equals(i.CollectionType, CollectionType.Playlists, StringComparison.OrdinalIgnoreCase))
@@ -137,12 +137,12 @@ namespace MediaBrowser.Server.Implementations.Library
 
             if (parents.Count > 0)
             {
-                list.Add(await GetUserView(parents, CollectionType.Playlists, string.Empty, user, cancellationToken).ConfigureAwait(false));
+                list.Add(await GetUserView(parents, list, CollectionType.Playlists, string.Empty, user, cancellationToken).ConfigureAwait(false));
             }
 
             if (user.Configuration.DisplayFoldersView)
             {
-                list.Add(await GetUserView(new List<ICollectionFolder>(), CollectionType.Folders, "zz_" + CollectionType.Folders, user, cancellationToken).ConfigureAwait(false));
+                list.Add(await GetUserView(new List<ICollectionFolder>(), list, CollectionType.Folders, "zz_" + CollectionType.Folders, user, cancellationToken).ConfigureAwait(false));
             }
 
             if (query.IncludeExternalContent)
@@ -169,7 +169,7 @@ namespace MediaBrowser.Server.Implementations.Library
                 if (_liveTvManager.GetEnabledUsers().Select(i => i.Id.ToString("N")).Contains(query.UserId))
                 {
                     //list.Add(await _liveTvManager.GetInternalLiveTvFolder(query.UserId, cancellationToken).ConfigureAwait(false));
-                    list.Add(await GetUserView(new List<ICollectionFolder>(), CollectionType.LiveTv, string.Empty, user, cancellationToken).ConfigureAwait(false));
+                    list.Add(await GetUserView(new List<ICollectionFolder>(), list, CollectionType.LiveTv, string.Empty, user, cancellationToken).ConfigureAwait(false));
                 }
             }
 
@@ -190,7 +190,9 @@ namespace MediaBrowser.Server.Implementations.Library
 
         public Task<UserView> GetUserSubView(string name, string parentId, string type, User user, string sortName, CancellationToken cancellationToken)
         {
-            return _libraryManager.GetNamedView(user, name, parentId, type, sortName, cancellationToken);
+            var uniqueId = parentId + "subview" + type;
+
+            return _libraryManager.GetNamedView(user, name, parentId, type, sortName, uniqueId, cancellationToken);
         }
 
         public Task<UserView> GetUserSubView(string parentId, string type, User user, string sortName, CancellationToken cancellationToken)
@@ -200,16 +202,27 @@ namespace MediaBrowser.Server.Implementations.Library
             return GetUserSubView(name, parentId, type, user, sortName, cancellationToken);
         }
 
-        public async Task<UserView> GetUserView(List<ICollectionFolder> parents, string viewType, string sortName, User user, CancellationToken cancellationToken)
+        public async Task<UserView> GetUserView(List<ICollectionFolder> parents, List<Folder> currentViews, string viewType, string sortName, User user, CancellationToken cancellationToken)
         {
+            var name = _localizationManager.GetLocalizedString("ViewType" + viewType);
+
             if (parents.Count == 1 && parents.All(i => string.Equals(i.CollectionType, viewType, StringComparison.OrdinalIgnoreCase)))
             {
-                var name = parents[0].Name;
+                if (!string.IsNullOrWhiteSpace(parents[0].Name))
+                {
+                    name = parents[0].Name;
+                }
+
                 var parentId = parents[0].Id;
 
                 var enableRichView = !user.Configuration.PlainFolderViews.Contains(parentId.ToString("N"), StringComparer.OrdinalIgnoreCase);
 
-                if (_config.Configuration.EnableUserSpecificUserViews || !enableRichView)
+                if (!enableRichView || currentViews.OfType<UserView>().Any(i => string.Equals(i.ViewType, viewType, StringComparison.OrdinalIgnoreCase) || string.Equals(i.Name, name, StringComparison.OrdinalIgnoreCase)))
+                {
+                    return await GetUserView(parentId, name, viewType, enableRichView, sortName, user, cancellationToken).ConfigureAwait(false);
+                }
+
+                if (_config.Configuration.EnableUserSpecificUserViews)
                 {
                     viewType = enableRichView ? viewType : null;
                     var view = await _libraryManager.GetNamedView(user, name, viewType, sortName, cancellationToken).ConfigureAwait(false);
@@ -224,18 +237,14 @@ namespace MediaBrowser.Server.Implementations.Library
 
                 return await _libraryManager.GetNamedView(user, name, viewType, sortName, cancellationToken).ConfigureAwait(false);
             }
-            else
-            {
-                var name = _localizationManager.GetLocalizedString("ViewType" + viewType);
 
-                return await _libraryManager.GetNamedView(user, name, viewType, sortName, cancellationToken).ConfigureAwait(false);
-            }
+            return await _libraryManager.GetNamedView(user, name, viewType, sortName, cancellationToken).ConfigureAwait(false);
         }
 
         public Task<UserView> GetUserView(Guid parentId, string name, string viewType, bool enableRichView, string sortName, User user, CancellationToken cancellationToken)
         {
             viewType = enableRichView ? viewType : null;
-            return _libraryManager.GetNamedView(user, name, parentId.ToString("N"), viewType, sortName, cancellationToken);
+            return _libraryManager.GetNamedView(user, name, parentId.ToString("N"), viewType, sortName, null, cancellationToken);
         }
 
         public List<Tuple<BaseItem, List<BaseItem>>> GetLatestItems(LatestItemsQuery request)

+ 2 - 1
MediaBrowser.Server.Implementations/LiveTv/LiveTvDtoService.cs

@@ -293,7 +293,8 @@ namespace MediaBrowser.Server.Implementations.LiveTv
                 ProgramName = info.ProgramName,
                 SourceType = info.SourceType,
                 Status = info.Status,
-                ChannelName = channelName
+                ChannelName = channelName,
+                Url = info.Url
             };
 
             if (!string.IsNullOrEmpty(info.ChannelId))

+ 8 - 3
MediaBrowser.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs

@@ -122,7 +122,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
 
         private async Task AddMediaInfo(MediaSourceInfo mediaSource, bool isAudio, CancellationToken cancellationToken)
         {
-            var inputPaths = new[] { mediaSource.Path };
+            var originalRuntime = mediaSource.RunTimeTicks;
 
             var info = await _mediaEncoder.GetMediaInfo(new MediaInfoRequest
             {
@@ -131,8 +131,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
                 MediaType = isAudio ? DlnaProfileType.Audio : DlnaProfileType.Video,
                 ExtractChapters = false
 
-            }, cancellationToken)
-                        .ConfigureAwait(false);
+            }, cancellationToken).ConfigureAwait(false);
 
             mediaSource.Bitrate = info.Bitrate;
             mediaSource.Container = info.Container;
@@ -146,6 +145,12 @@ namespace MediaBrowser.Server.Implementations.LiveTv
 
             mediaSource.DefaultSubtitleStreamIndex = null;
 
+            // Null this out so that it will be treated like a live stream
+            if (!originalRuntime.HasValue)
+            {
+                mediaSource.RunTimeTicks = null;
+            }
+
             var audioStream = mediaSource.MediaStreams.FirstOrDefault(i => i.Type == Model.Entities.MediaStreamType.Audio);
 
             if (audioStream == null || audioStream.Index == -1)

+ 3 - 1
MediaBrowser.Server.Implementations/Localization/Server/server.json

@@ -1433,5 +1433,7 @@
     "ToAccessPreferencesHelp": "To access your preferences later, click your user icon in the top right header and select My Preferences.",
     "HeaderViewStyles": "View Styles",
     "LabelSelectViewStyles": "Enable rich presentations for:",
-    "LabelSelectViewStylesHelp": "If enabled, views will be built with metadata to offer categories such as Suggestions, Latest, Genres, and more. If disabled, they'll be displayed with simple folders."
+    "LabelSelectViewStylesHelp": "If enabled, views will be built with metadata to offer categories such as Suggestions, Latest, Genres, and more. If disabled, they'll be displayed with simple folders.",
+    "TabPhotos": "Photos",
+    "TabVideos": "Videos"
 }

+ 5 - 1
MediaBrowser.Server.Implementations/Sync/SyncManager.cs

@@ -727,10 +727,14 @@ namespace MediaBrowser.Server.Implementations.Sync
                 }
             });
 
-            return jobItemResult.Items
+            var readyItems = jobItemResult.Items
                 .Select(GetJobItemInfo)
                 .Where(i => i != null)
                 .ToList();
+
+            _logger.Debug("Returning {0} ready sync items for targetId {1}", readyItems.Count, targetId);
+
+            return readyItems;
         }
 
         public async Task<SyncDataResponse> SyncData(SyncDataRequest request)

+ 1 - 0
MediaBrowser.WebDashboard/Api/PackageCreator.cs

@@ -470,6 +470,7 @@ namespace MediaBrowser.WebDashboard.Api
                                 "notificationlist.js",
                                 "notificationsetting.js",
                                 "notificationsettings.js",
+                                "photos.js",
                                 "playlists.js",
                                 "playlistedit.js",
 

+ 6 - 0
MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj

@@ -132,6 +132,9 @@
     <Content Include="dashboard-ui\mysyncjob.html">
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
     </Content>
+    <Content Include="dashboard-ui\photos.html">
+      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+    </Content>
     <Content Include="dashboard-ui\scripts\dashboardhosting.js">
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
     </Content>
@@ -147,6 +150,9 @@
     <Content Include="dashboard-ui\scripts\livetvitems.js">
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
     </Content>
+    <Content Include="dashboard-ui\scripts\photos.js">
+      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+    </Content>
     <Content Include="dashboard-ui\scripts\selectserver.js">
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
     </Content>

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

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

+ 1 - 1
Nuget/MediaBrowser.Common.nuspec

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

+ 1 - 1
Nuget/MediaBrowser.Model.Signed.nuspec

@@ -2,7 +2,7 @@
 <package xmlns="http://schemas.microsoft.com/packaging/2011/08/nuspec.xsd">
     <metadata>
         <id>MediaBrowser.Model.Signed</id>
-        <version>3.0.620</version>
+        <version>3.0.621</version>
         <title>MediaBrowser.Model - Signed Edition</title>
         <authors>Emby Team</authors>
         <owners>ebr,Luke,scottisafool</owners>

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

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

+ 1 - 1
SharedVersion.cs

@@ -1,4 +1,4 @@
 using System.Reflection;
 
 //[assembly: AssemblyVersion("3.0.*")]
-[assembly: AssemblyVersion("3.0.5582.4")]
+[assembly: AssemblyVersion("3.0.5588.0")]