Browse Source

add more methods to file system interface

Luke Pulverenti 12 năm trước cách đây
mục cha
commit
b9d17c9bc7
54 tập tin đã thay đổi với 704 bổ sung426 xóa
  1. 61 28
      MediaBrowser.Api/EnvironmentService.cs
  2. 5 2
      MediaBrowser.Api/Images/ImageRequest.cs
  3. 1 0
      MediaBrowser.Api/Images/ImageWriter.cs
  4. 14 29
      MediaBrowser.Api/Library/LibraryHelpers.cs
  5. 5 1
      MediaBrowser.Api/LiveTv/LiveTvService.cs
  6. 7 13
      MediaBrowser.Api/Playback/BaseStreamingService.cs
  7. 57 103
      MediaBrowser.Common.Implementations/IO/CommonFileSystem.cs
  8. 22 0
      MediaBrowser.Common/IO/IFileSystem.cs
  9. 2 1
      MediaBrowser.Common/Net/INetworkManager.cs
  10. 4 1
      MediaBrowser.Controller/Drawing/ImageProcessingOptions.cs
  11. 14 13
      MediaBrowser.Controller/Entities/BaseItem.cs
  12. 62 63
      MediaBrowser.Controller/Entities/Folder.cs
  13. 3 3
      MediaBrowser.Controller/Entities/IHasImages.cs
  14. 7 0
      MediaBrowser.Controller/Library/ILibraryManager.cs
  15. 1 1
      MediaBrowser.Controller/LiveTv/ILiveTvManager.cs
  16. 26 0
      MediaBrowser.Controller/LiveTv/ILiveTvRecording.cs
  17. 52 0
      MediaBrowser.Controller/LiveTv/LiveTvAudioRecording.cs
  18. 12 3
      MediaBrowser.Controller/LiveTv/LiveTvVideoRecording.cs
  19. 3 1
      MediaBrowser.Controller/MediaBrowser.Controller.csproj
  20. 12 0
      MediaBrowser.Model/LiveTv/RecordingInfoDto.cs
  21. 6 0
      MediaBrowser.Model/LiveTv/RecordingQuery.cs
  22. 3 2
      MediaBrowser.Mono.userprefs
  23. 1 1
      MediaBrowser.Providers/MediaInfo/AudioImageProvider.cs
  24. 0 9
      MediaBrowser.Providers/Music/FanArtArtistProvider.cs
  25. 0 8
      MediaBrowser.Providers/Music/LastfmBaseProvider.cs
  26. 0 21
      MediaBrowser.Providers/Music/LastfmHelper.cs
  27. 1 1
      MediaBrowser.Providers/Savers/AlbumXmlSaver.cs
  28. 1 1
      MediaBrowser.Providers/Savers/ArtistXmlSaver.cs
  29. 1 1
      MediaBrowser.Providers/Savers/BoxSetXmlSaver.cs
  30. 1 1
      MediaBrowser.Providers/Savers/EpisodeXmlSaver.cs
  31. 1 1
      MediaBrowser.Providers/Savers/FolderXmlSaver.cs
  32. 1 1
      MediaBrowser.Providers/Savers/GameSystemXmlSaver.cs
  33. 1 1
      MediaBrowser.Providers/Savers/GameXmlSaver.cs
  34. 1 1
      MediaBrowser.Providers/Savers/MovieXmlSaver.cs
  35. 1 1
      MediaBrowser.Providers/Savers/SeasonXmlSaver.cs
  36. 1 1
      MediaBrowser.Providers/Savers/SeriesXmlSaver.cs
  37. 34 9
      MediaBrowser.Server.Implementations/Drawing/ImageProcessor.cs
  38. 17 19
      MediaBrowser.Server.Implementations/Drawing/PercentPlayedDrawer.cs
  39. 8 10
      MediaBrowser.Server.Implementations/Drawing/PlayedIndicatorDrawer.cs
  40. 55 0
      MediaBrowser.Server.Implementations/Drawing/UnplayedCountIndicator.cs
  41. 11 14
      MediaBrowser.Server.Implementations/Library/CoreResolutionIgnoreRule.cs
  42. 22 8
      MediaBrowser.Server.Implementations/Library/LibraryManager.cs
  43. 24 11
      MediaBrowser.Server.Implementations/LiveTv/LiveTvDtoService.cs
  44. 51 30
      MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs
  45. 4 4
      MediaBrowser.Server.Implementations/LiveTv/RecordingImageProvider.cs
  46. 2 1
      MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj
  47. 1 1
      MediaBrowser.Server.Implementations/Providers/ImageSaver.cs
  48. 3 2
      MediaBrowser.Server.Mono/Networking/NetworkManager.cs
  49. 1 1
      MediaBrowser.ServerApplication/ApplicationHost.cs
  50. 32 2
      MediaBrowser.ServerApplication/Networking/NetworkManager.cs
  51. 1 0
      MediaBrowser.WebDashboard/Api/DashboardService.cs
  52. 41 0
      MediaBrowser.WebDashboard/ApiClient.js
  53. 6 0
      MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj
  54. 1 1
      MediaBrowser.WebDashboard/packages.config

+ 61 - 28
MediaBrowser.Api/EnvironmentService.cs

@@ -46,6 +46,18 @@ namespace MediaBrowser.Api
         public bool IncludeHidden { get; set; }
         public bool IncludeHidden { get; set; }
     }
     }
 
 
+    [Route("/Environment/NetworkShares", "GET")]
+    [Api(Description = "Gets shares from a network device")]
+    public class GetNetworkShares : IReturn<List<FileSystemEntryInfo>>
+    {
+        /// <summary>
+        /// Gets or sets the path.
+        /// </summary>
+        /// <value>The path.</value>
+        [ApiMember(Name = "Path", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
+        public string Path { get; set; }
+    }
+
     /// <summary>
     /// <summary>
     /// Class GetDrives
     /// Class GetDrives
     /// </summary>
     /// </summary>
@@ -64,11 +76,25 @@ namespace MediaBrowser.Api
     {
     {
     }
     }
 
 
+    [Route("/Environment/ParentPath", "GET")]
+    [Api(Description = "Gets the parent path of a given path")]
+    public class GetParentPath : IReturn<string>
+    {
+        /// <summary>
+        /// Gets or sets the path.
+        /// </summary>
+        /// <value>The path.</value>
+        [ApiMember(Name = "Path", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
+        public string Path { get; set; }
+    }
+
     /// <summary>
     /// <summary>
     /// Class EnvironmentService
     /// Class EnvironmentService
     /// </summary>
     /// </summary>
     public class EnvironmentService : BaseApiService
     public class EnvironmentService : BaseApiService
     {
     {
+        const char UncSeparator = '\\';
+
         /// <summary>
         /// <summary>
         /// The _network manager
         /// The _network manager
         /// </summary>
         /// </summary>
@@ -105,13 +131,9 @@ namespace MediaBrowser.Api
                 throw new ArgumentNullException("Path");
                 throw new ArgumentNullException("Path");
             }
             }
 
 
-            // If it's not a drive trim trailing slashes.
-            if (!path.EndsWith(":\\"))
-            {
-                path = path.TrimEnd('\\');
-            }
+            var networkPrefix = UncSeparator.ToString(CultureInfo.InvariantCulture) + UncSeparator.ToString(CultureInfo.InvariantCulture);
 
 
-            if (path.StartsWith(NetworkPrefix, StringComparison.OrdinalIgnoreCase) && path.LastIndexOf('\\') == 1)
+            if (path.StartsWith(networkPrefix, StringComparison.OrdinalIgnoreCase) && path.LastIndexOf(UncSeparator) == 1)
             {
             {
                 return ToOptimizedResult(GetNetworkShares(path).OrderBy(i => i.Path).ToList());
                 return ToOptimizedResult(GetNetworkShares(path).OrderBy(i => i.Path).ToList());
             }
             }
@@ -119,6 +141,15 @@ namespace MediaBrowser.Api
             return ToOptimizedResult(GetFileSystemEntries(request).OrderBy(i => i.Path).ToList());
             return ToOptimizedResult(GetFileSystemEntries(request).OrderBy(i => i.Path).ToList());
         }
         }
 
 
+        public object Get(GetNetworkShares request)
+        {
+            var path = request.Path;
+
+            var shares = GetNetworkShares(path).OrderBy(i => i.Path).ToList();
+
+            return ToOptimizedResult(shares);
+        }
+
         /// <summary>
         /// <summary>
         /// Gets the specified request.
         /// Gets the specified request.
         /// </summary>
         /// </summary>
@@ -154,25 +185,13 @@ namespace MediaBrowser.Api
         /// <returns>System.Object.</returns>
         /// <returns>System.Object.</returns>
         public object Get(GetNetworkDevices request)
         public object Get(GetNetworkDevices request)
         {
         {
-            var result = GetNetworkDevices().OrderBy(i => i.Path).ToList();
+            var result = _networkManager.GetNetworkDevices()
+                .OrderBy(i => i.Path)
+                .ToList();
 
 
             return ToOptimizedResult(result);
             return ToOptimizedResult(result);
         }
         }
 
 
-        /// <summary>
-        /// Gets the network computers.
-        /// </summary>
-        /// <returns>IEnumerable{FileSystemEntryInfo}.</returns>
-        private IEnumerable<FileSystemEntryInfo> GetNetworkDevices()
-        {
-            return _networkManager.GetNetworkDevices().Select(c => new FileSystemEntryInfo
-            {
-                Name = c,
-                Path = NetworkPrefix + c,
-                Type = FileSystemEntryType.NetworkComputer
-            });
-        }
-
         /// <summary>
         /// <summary>
         /// Gets the name.
         /// Gets the name.
         /// </summary>
         /// </summary>
@@ -223,7 +242,7 @@ namespace MediaBrowser.Api
                 {
                 {
                     return false;
                     return false;
                 }
                 }
-                
+
                 return true;
                 return true;
             });
             });
 
 
@@ -236,13 +255,27 @@ namespace MediaBrowser.Api
             }).ToList();
             }).ToList();
         }
         }
 
 
-        /// <summary>
-        /// Gets the network prefix.
-        /// </summary>
-        /// <value>The network prefix.</value>
-        private string NetworkPrefix
+        public object Get(GetParentPath request)
         {
         {
-            get { return Path.DirectorySeparatorChar.ToString(CultureInfo.InvariantCulture) + Path.DirectorySeparatorChar.ToString(CultureInfo.InvariantCulture); }
+            var parent = Path.GetDirectoryName(request.Path);
+
+            if (string.IsNullOrEmpty(parent))
+            {
+                // Check if unc share
+                var index = request.Path.LastIndexOf(UncSeparator);
+
+                if (index != -1 && request.Path.IndexOf(UncSeparator) == 0)
+                {
+                    parent = request.Path.Substring(0, index);
+
+                    if (string.IsNullOrWhiteSpace(parent.TrimStart(UncSeparator)))
+                    {
+                        parent = null;
+                    }
+                }
+            }
+
+            return parent;
         }
         }
     }
     }
 }
 }

+ 5 - 2
MediaBrowser.Api/Images/ImageRequest.cs

@@ -59,8 +59,11 @@ namespace MediaBrowser.Api.Images
         [ApiMember(Name = "AddPlayedIndicator", Description = "Optional. Add a played indicator", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
         [ApiMember(Name = "AddPlayedIndicator", Description = "Optional. Add a played indicator", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
         public bool AddPlayedIndicator { get; set; }
         public bool AddPlayedIndicator { get; set; }
 
 
-        [ApiMember(Name = "PercentPlayed", Description = "Optional percent to render for the percent played overlay", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
-        public int? PercentPlayed { get; set; }
+        [ApiMember(Name = "PercentPlayed", Description = "Optional percent to render for the percent played overlay", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
+        public double? PercentPlayed { get; set; }
+
+        [ApiMember(Name = "UnplayedCount", Description = "Optional unplayed count overlay to render", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
+        public int? UnplayedCount { get; set; }
 
 
         [ApiMember(Name = "BackgroundColor", Description = "Optional. Apply a background color for transparent images.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
         [ApiMember(Name = "BackgroundColor", Description = "Optional. Apply a background color for transparent images.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
         public string BackgroundColor { get; set; }
         public string BackgroundColor { get; set; }

+ 1 - 0
MediaBrowser.Api/Images/ImageWriter.cs

@@ -91,6 +91,7 @@ namespace MediaBrowser.Api.Images
                 OutputFormat = Request.Format,
                 OutputFormat = Request.Format,
                 AddPlayedIndicator = Request.AddPlayedIndicator,
                 AddPlayedIndicator = Request.AddPlayedIndicator,
                 PercentPlayed = Request.PercentPlayed,
                 PercentPlayed = Request.PercentPlayed,
+                UnplayedCount = Request.UnplayedCount,
                 BackgroundColor = Request.BackgroundColor
                 BackgroundColor = Request.BackgroundColor
             };
             };
 
 

+ 14 - 29
MediaBrowser.Api/Library/LibraryHelpers.cs

@@ -65,17 +65,10 @@ namespace MediaBrowser.Api.Library
                 throw new DirectoryNotFoundException("The path does not exist.");
                 throw new DirectoryNotFoundException("The path does not exist.");
             }
             }
 
 
-            // Strip off trailing slash, but not on drives
-            path = path.TrimEnd(Path.DirectorySeparatorChar);
-            if (path.EndsWith(":", StringComparison.OrdinalIgnoreCase))
-            {
-                path += Path.DirectorySeparatorChar;
-            }
-
             var rootFolderPath = user != null ? user.RootFolderPath : appPaths.DefaultUserViewsPath;
             var rootFolderPath = user != null ? user.RootFolderPath : appPaths.DefaultUserViewsPath;
             var virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName);
             var virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName);
 
 
-            ValidateNewMediaPath(fileSystem, rootFolderPath, path, appPaths);
+            ValidateNewMediaPath(fileSystem, rootFolderPath, path);
 
 
             var shortcutFilename = Path.GetFileNameWithoutExtension(path);
             var shortcutFilename = Path.GetFileNameWithoutExtension(path);
 
 
@@ -96,25 +89,18 @@ namespace MediaBrowser.Api.Library
         /// <param name="fileSystem">The file system.</param>
         /// <param name="fileSystem">The file system.</param>
         /// <param name="currentViewRootFolderPath">The current view root folder path.</param>
         /// <param name="currentViewRootFolderPath">The current view root folder path.</param>
         /// <param name="mediaPath">The media path.</param>
         /// <param name="mediaPath">The media path.</param>
-        /// <param name="appPaths">The app paths.</param>
         /// <exception cref="System.ArgumentException">
         /// <exception cref="System.ArgumentException">
         /// </exception>
         /// </exception>
-        private static void ValidateNewMediaPath(IFileSystem fileSystem, string currentViewRootFolderPath, string mediaPath, IServerApplicationPaths appPaths)
+        private static void ValidateNewMediaPath(IFileSystem fileSystem, string currentViewRootFolderPath, string mediaPath)
         {
         {
-            var duplicate = Directory.EnumerateFiles(appPaths.RootFolderPath, ShortcutFileSearch, SearchOption.AllDirectories)
-                .Select(fileSystem.ResolveShortcut)
-                .FirstOrDefault(p => !IsNewPathValid(mediaPath, p, false));
-
-            if (!string.IsNullOrEmpty(duplicate))
-            {
-                throw new ArgumentException(string.Format("The path cannot be added to the library because {0} already exists.", duplicate));
-            }
+            var pathsInCurrentVIew = Directory.EnumerateFiles(currentViewRootFolderPath, ShortcutFileSearch, SearchOption.AllDirectories)
+                    .Select(fileSystem.ResolveShortcut)
+                    .ToList();
 
 
             // Don't allow duplicate sub-paths within the same user library, or it will result in duplicate items
             // Don't allow duplicate sub-paths within the same user library, or it will result in duplicate items
             // See comments in IsNewPathValid
             // See comments in IsNewPathValid
-            duplicate = Directory.EnumerateFiles(currentViewRootFolderPath, ShortcutFileSearch, SearchOption.AllDirectories)
-              .Select(fileSystem.ResolveShortcut)
-              .FirstOrDefault(p => !IsNewPathValid(mediaPath, p, true));
+            var duplicate = pathsInCurrentVIew
+              .FirstOrDefault(p => !IsNewPathValid(fileSystem, mediaPath, p));
 
 
             if (!string.IsNullOrEmpty(duplicate))
             if (!string.IsNullOrEmpty(duplicate))
             {
             {
@@ -122,9 +108,8 @@ namespace MediaBrowser.Api.Library
             }
             }
             
             
             // Make sure the current root folder doesn't already have a shortcut to the same path
             // Make sure the current root folder doesn't already have a shortcut to the same path
-            duplicate = Directory.EnumerateFiles(currentViewRootFolderPath, ShortcutFileSearch, SearchOption.AllDirectories)
-                .Select(fileSystem.ResolveShortcut)
-                .FirstOrDefault(p => mediaPath.Equals(p, StringComparison.OrdinalIgnoreCase));
+            duplicate = pathsInCurrentVIew
+                .FirstOrDefault(p => string.Equals(mediaPath, p, StringComparison.OrdinalIgnoreCase));
 
 
             if (!string.IsNullOrEmpty(duplicate))
             if (!string.IsNullOrEmpty(duplicate))
             {
             {
@@ -135,30 +120,30 @@ namespace MediaBrowser.Api.Library
         /// <summary>
         /// <summary>
         /// Validates that a new path can be added based on an existing path
         /// Validates that a new path can be added based on an existing path
         /// </summary>
         /// </summary>
+        /// <param name="fileSystem">The file system.</param>
         /// <param name="newPath">The new path.</param>
         /// <param name="newPath">The new path.</param>
         /// <param name="existingPath">The existing path.</param>
         /// <param name="existingPath">The existing path.</param>
-        /// <param name="enforceSubPathRestriction">if set to <c>true</c> [enforce sub path restriction].</param>
         /// <returns><c>true</c> if [is new path valid] [the specified new path]; otherwise, <c>false</c>.</returns>
         /// <returns><c>true</c> if [is new path valid] [the specified new path]; otherwise, <c>false</c>.</returns>
-        private static bool IsNewPathValid(string newPath, string existingPath, bool enforceSubPathRestriction)
+        private static bool IsNewPathValid(IFileSystem fileSystem, string newPath, string existingPath)
         {
         {
             // Example: D:\Movies is the existing path
             // Example: D:\Movies is the existing path
             // D:\ cannot be added
             // D:\ cannot be added
             // Neither can D:\Movies\Kids
             // Neither can D:\Movies\Kids
             // A D:\Movies duplicate is ok here since that will be caught later
             // A D:\Movies duplicate is ok here since that will be caught later
 
 
-            if (newPath.Equals(existingPath, StringComparison.OrdinalIgnoreCase))
+            if (string.Equals(newPath, existingPath, StringComparison.OrdinalIgnoreCase))
             {
             {
                 return true;
                 return true;
             }
             }
 
 
             // If enforceSubPathRestriction is true, validate the D:\Movies\Kids scenario
             // If enforceSubPathRestriction is true, validate the D:\Movies\Kids scenario
-            if (enforceSubPathRestriction && newPath.StartsWith(existingPath.TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase))
+            if (fileSystem.ContainsSubPath(existingPath, newPath))
             {
             {
                 return false;
                 return false;
             }
             }
 
 
             // Validate the D:\ scenario
             // Validate the D:\ scenario
-            if (existingPath.StartsWith(newPath.TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase))
+            if (fileSystem.ContainsSubPath(newPath, existingPath))
             {
             {
                 return false;
                 return false;
             }
             }

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

@@ -61,6 +61,9 @@ namespace MediaBrowser.Api.LiveTv
 
 
         [ApiMember(Name = "Limit", Description = "Optional. The maximum number of records to return", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
         [ApiMember(Name = "Limit", Description = "Optional. The maximum number of records to return", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
         public int? Limit { get; set; }
         public int? Limit { get; set; }
+
+        [ApiMember(Name = "IsRecording", Description = "Optional filter by recordings that are currently active, or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
+        public bool? IsRecording { get; set; }
     }
     }
 
 
     [Route("/LiveTv/Recordings/Groups", "GET")]
     [Route("/LiveTv/Recordings/Groups", "GET")]
@@ -274,7 +277,8 @@ namespace MediaBrowser.Api.LiveTv
                 UserId = request.UserId,
                 UserId = request.UserId,
                 GroupId = request.GroupId,
                 GroupId = request.GroupId,
                 StartIndex = request.StartIndex,
                 StartIndex = request.StartIndex,
-                Limit = request.Limit
+                Limit = request.Limit,
+                IsRecording = request.IsRecording
 
 
             }, CancellationToken.None).Result;
             }, CancellationToken.None).Result;
 
 

+ 7 - 13
MediaBrowser.Api/Playback/BaseStreamingService.cs

@@ -197,10 +197,6 @@ namespace MediaBrowser.Api.Playback
             {
             {
                 args += string.Format("-map 0:{0}", state.VideoStream.Index);
                 args += string.Format("-map 0:{0}", state.VideoStream.Index);
             }
             }
-            else if (!state.HasMediaStreams)
-            {
-                args += string.Format("-map 0:{0}", 0);
-            }
             else
             else
             {
             {
                 args += "-map -0:v";
                 args += "-map -0:v";
@@ -210,10 +206,6 @@ namespace MediaBrowser.Api.Playback
             {
             {
                 args += string.Format(" -map 0:{0}", state.AudioStream.Index);
                 args += string.Format(" -map 0:{0}", state.AudioStream.Index);
             }
             }
-            else if (!state.HasMediaStreams)
-            {
-                args += string.Format(" -map 0:{0}", 1);
-            }
 
 
             else
             else
             {
             {
@@ -871,7 +863,7 @@ namespace MediaBrowser.Api.Playback
                 RequestedUrl = url
                 RequestedUrl = url
             };
             };
 
 
-            BaseItem item;
+            Guid itemId;
 
 
             if (string.Equals(request.Type, "Recording", StringComparison.OrdinalIgnoreCase))
             if (string.Equals(request.Type, "Recording", StringComparison.OrdinalIgnoreCase))
             {
             {
@@ -900,7 +892,7 @@ namespace MediaBrowser.Api.Playback
                     state.IsRemote = true;
                     state.IsRemote = true;
                 }
                 }
 
 
-                item = recording;
+                itemId = recording.Id;
             }
             }
             else if (string.Equals(request.Type, "Channel", StringComparison.OrdinalIgnoreCase))
             else if (string.Equals(request.Type, "Channel", StringComparison.OrdinalIgnoreCase))
             {
             {
@@ -916,11 +908,11 @@ namespace MediaBrowser.Api.Playback
 
 
                 state.IsRemote = true;
                 state.IsRemote = true;
 
 
-                item = channel;
+                itemId = channel.Id;
             }
             }
             else
             else
             {
             {
-                item = DtoService.GetItemByDtoId(request.Id);
+                var item = DtoService.GetItemByDtoId(request.Id);
 
 
                 state.MediaPath = item.Path;
                 state.MediaPath = item.Path;
                 state.IsRemote = item.LocationType == LocationType.Remote;
                 state.IsRemote = item.LocationType == LocationType.Remote;
@@ -937,13 +929,15 @@ namespace MediaBrowser.Api.Playback
                         ? new List<string>()
                         ? new List<string>()
                         : video.PlayableStreamFileNames.ToList();
                         : video.PlayableStreamFileNames.ToList();
                 }
                 }
+
+                itemId = item.Id;
             }
             }
 
 
             var videoRequest = request as VideoStreamRequest;
             var videoRequest = request as VideoStreamRequest;
 
 
             var mediaStreams = ItemRepository.GetMediaStreams(new MediaStreamQuery
             var mediaStreams = ItemRepository.GetMediaStreams(new MediaStreamQuery
             {
             {
-                ItemId = item.Id
+                ItemId = itemId
 
 
             }).ToList();
             }).ToList();
 
 

+ 57 - 103
MediaBrowser.Common.Implementations/IO/CommonFileSystem.cs

@@ -82,6 +82,16 @@ namespace MediaBrowser.Common.Implementations.IO
                 throw new ArgumentNullException("target");
                 throw new ArgumentNullException("target");
             }
             }
 
 
+            if (string.IsNullOrEmpty(shortcutPath))
+            {
+                throw new ArgumentNullException("shortcutPath");
+            }
+
+            if (string.IsNullOrEmpty(target))
+            {
+                throw new ArgumentNullException("target");
+            }
+
             File.WriteAllText(shortcutPath, target);
             File.WriteAllText(shortcutPath, target);
         }
         }
 
 
@@ -92,6 +102,11 @@ namespace MediaBrowser.Common.Implementations.IO
         /// <returns>FileSystemInfo.</returns>
         /// <returns>FileSystemInfo.</returns>
         public FileSystemInfo GetFileSystemInfo(string path)
         public FileSystemInfo GetFileSystemInfo(string path)
         {
         {
+            if (string.IsNullOrEmpty(path))
+            {
+                throw new ArgumentNullException("path");
+            }
+
             // Take a guess to try and avoid two file system hits, but we'll double-check by calling Exists
             // Take a guess to try and avoid two file system hits, but we'll double-check by calling Exists
             if (Path.HasExtension(path))
             if (Path.HasExtension(path))
             {
             {
@@ -172,7 +187,6 @@ namespace MediaBrowser.Common.Implementations.IO
         /// Gets the creation time UTC.
         /// Gets the creation time UTC.
         /// </summary>
         /// </summary>
         /// <param name="info">The info.</param>
         /// <param name="info">The info.</param>
-        /// <param name="logger">The logger.</param>
         /// <returns>DateTime.</returns>
         /// <returns>DateTime.</returns>
         public DateTime GetLastWriteTimeUtc(FileSystemInfo info)
         public DateTime GetLastWriteTimeUtc(FileSystemInfo info)
         {
         {
@@ -224,6 +238,16 @@ namespace MediaBrowser.Common.Implementations.IO
         /// <param name="file2">The file2.</param>
         /// <param name="file2">The file2.</param>
         public void SwapFiles(string file1, string file2)
         public void SwapFiles(string file1, string file2)
         {
         {
+            if (string.IsNullOrEmpty(file1))
+            {
+                throw new ArgumentNullException("file1");
+            }
+
+            if (string.IsNullOrEmpty(file2))
+            {
+                throw new ArgumentNullException("file2");
+            }
+
             var temp1 = Path.GetTempFileName();
             var temp1 = Path.GetTempFileName();
             var temp2 = Path.GetTempFileName();
             var temp2 = Path.GetTempFileName();
 
 
@@ -247,6 +271,11 @@ namespace MediaBrowser.Common.Implementations.IO
         /// <param name="path">The path.</param>
         /// <param name="path">The path.</param>
         private void RemoveHiddenAttribute(string path)
         private void RemoveHiddenAttribute(string path)
         {
         {
+            if (string.IsNullOrEmpty(path))
+            {
+                throw new ArgumentNullException("path");
+            }
+
             var currentFile = new FileInfo(path);
             var currentFile = new FileInfo(path);
 
 
             // This will fail if the file is hidden
             // This will fail if the file is hidden
@@ -258,127 +287,52 @@ namespace MediaBrowser.Common.Implementations.IO
                 }
                 }
             }
             }
         }
         }
-    }
 
 
-    /// <summary>
-    ///  Adapted from http://stackoverflow.com/questions/309495/windows-shortcut-lnk-parser-in-java
-    /// </summary>
-    internal class WindowsShortcut
-    {
-        public bool IsDirectory { get; private set; }
-        public bool IsLocal { get; private set; }
-        public string ResolvedPath { get; private set; }
-
-        public WindowsShortcut(string file)
+        public bool ContainsSubPath(string parentPath, string path)
         {
         {
-            ParseLink(File.ReadAllBytes(file), Encoding.UTF8);
-        }
-
-        private static bool isMagicPresent(byte[] link)
-        {
-
-            const int magic = 0x0000004C;
-            const int magic_offset = 0x00;
-
-            return link.Length >= 32 && bytesToDword(link, magic_offset) == magic;
-        }
-
-        /**
-         * Gobbles up link data by parsing it and storing info in member fields
-         * @param link all the bytes from the .lnk file
-         */
-        private void ParseLink(byte[] link, Encoding encoding)
-        {
-            if (!isMagicPresent(link))
-                throw new IOException("Invalid shortcut; magic is missing", 0);
-
-            // get the flags byte
-            byte flags = link[0x14];
-
-            // get the file attributes byte
-            const int file_atts_offset = 0x18;
-            byte file_atts = link[file_atts_offset];
-            byte is_dir_mask = (byte)0x10;
-            if ((file_atts & is_dir_mask) > 0)
-            {
-                IsDirectory = true;
-            }
-            else
+            if (string.IsNullOrEmpty(parentPath))
             {
             {
-                IsDirectory = false;
+                throw new ArgumentNullException("parentPath");
             }
             }
 
 
-            // if the shell settings are present, skip them
-            const int shell_offset = 0x4c;
-            const byte has_shell_mask = (byte)0x01;
-            int shell_len = 0;
-            if ((flags & has_shell_mask) > 0)
+            if (string.IsNullOrEmpty(path))
             {
             {
-                // the plus 2 accounts for the length marker itself
-                shell_len = bytesToWord(link, shell_offset) + 2;
+                throw new ArgumentNullException("path");
             }
             }
+            
+            return path.IndexOf(parentPath.TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase) != -1;
+        }
 
 
-            // get to the file settings
-            int file_start = 0x4c + shell_len;
-
-            const int file_location_info_flag_offset_offset = 0x08;
-            int file_location_info_flag = link[file_start + file_location_info_flag_offset_offset];
-            IsLocal = (file_location_info_flag & 2) == 0;
-            // get the local volume and local system values
-            //final int localVolumeTable_offset_offset = 0x0C;
-            const int basename_offset_offset = 0x10;
-            const int networkVolumeTable_offset_offset = 0x14;
-            const int finalname_offset_offset = 0x18;
-            int finalname_offset = link[file_start + finalname_offset_offset] + file_start;
-            String finalname = getNullDelimitedString(link, finalname_offset, encoding);
-            if (IsLocal)
+        public bool IsRootPath(string path)
+        {
+            if (string.IsNullOrEmpty(path))
             {
             {
-                int basename_offset = link[file_start + basename_offset_offset] + file_start;
-                String basename = getNullDelimitedString(link, basename_offset, encoding);
-                ResolvedPath = basename + finalname;
+                throw new ArgumentNullException("path");
             }
             }
-            else
+            
+            var parent = Path.GetDirectoryName(path);
+
+            if (!string.IsNullOrEmpty(parent))
             {
             {
-                int networkVolumeTable_offset = link[file_start + networkVolumeTable_offset_offset] + file_start;
-                int shareName_offset_offset = 0x08;
-                int shareName_offset = link[networkVolumeTable_offset + shareName_offset_offset]
-                    + networkVolumeTable_offset;
-                String shareName = getNullDelimitedString(link, shareName_offset, encoding);
-                ResolvedPath = shareName + "\\" + finalname;
+                return false;   
             }
             }
+
+            return true;
         }
         }
 
 
-        private static string getNullDelimitedString(byte[] bytes, int off, Encoding encoding)
+        public string NormalizePath(string path)
         {
         {
-            int len = 0;
-
-            // count bytes until the null character (0)
-            while (true)
+            if (string.IsNullOrEmpty(path))
             {
             {
-                if (bytes[off + len] == 0)
-                {
-                    break;
-                }
-                len++;
+                throw new ArgumentNullException("path");
             }
             }
 
 
-            return encoding.GetString(bytes, off, len);
-        }
-
-        /*
-         * convert two bytes into a short note, this is little endian because it's
-         * for an Intel only OS.
-         */
-        private static int bytesToWord(byte[] bytes, int off)
-        {
-            return ((bytes[off + 1] & 0xff) << 8) | (bytes[off] & 0xff);
-        }
+            if (path.EndsWith(":\\", StringComparison.OrdinalIgnoreCase))
+            {
+                return path;
+            }
 
 
-        private static int bytesToDword(byte[] bytes, int off)
-        {
-            return (bytesToWord(bytes, off + 2) << 16) | bytesToWord(bytes, off);
+            return path.TrimEnd(Path.DirectorySeparatorChar);
         }
         }
-
     }
     }
-
 }
 }

+ 22 - 0
MediaBrowser.Common/IO/IFileSystem.cs

@@ -81,5 +81,27 @@ namespace MediaBrowser.Common.IO
         /// <param name="file1">The file1.</param>
         /// <param name="file1">The file1.</param>
         /// <param name="file2">The file2.</param>
         /// <param name="file2">The file2.</param>
         void SwapFiles(string file1, string file2);
         void SwapFiles(string file1, string file2);
+
+        /// <summary>
+        /// Determines whether [contains sub path] [the specified parent path].
+        /// </summary>
+        /// <param name="parentPath">The parent path.</param>
+        /// <param name="path">The path.</param>
+        /// <returns><c>true</c> if [contains sub path] [the specified parent path]; otherwise, <c>false</c>.</returns>
+        bool ContainsSubPath(string parentPath, string path);
+
+        /// <summary>
+        /// Determines whether [is root path] [the specified path].
+        /// </summary>
+        /// <param name="path">The path.</param>
+        /// <returns><c>true</c> if [is root path] [the specified path]; otherwise, <c>false</c>.</returns>
+        bool IsRootPath(string path);
+
+        /// <summary>
+        /// Normalizes the path.
+        /// </summary>
+        /// <param name="path">The path.</param>
+        /// <returns>System.String.</returns>
+        string NormalizePath(string path);
     }
     }
 }
 }

+ 2 - 1
MediaBrowser.Common/Net/INetworkManager.cs

@@ -1,3 +1,4 @@
+using MediaBrowser.Model.IO;
 using MediaBrowser.Model.Net;
 using MediaBrowser.Model.Net;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.Net;
 using System.Net;
@@ -35,7 +36,7 @@ namespace MediaBrowser.Common.Net
         /// Gets available devices within the domain
         /// Gets available devices within the domain
         /// </summary>
         /// </summary>
         /// <returns>PC's in the Domain</returns>
         /// <returns>PC's in the Domain</returns>
-        IEnumerable<string> GetNetworkDevices();
+        IEnumerable<FileSystemEntryInfo> GetNetworkDevices();
 
 
         /// <summary>
         /// <summary>
         /// Parses the specified endpointstring.
         /// Parses the specified endpointstring.

+ 4 - 1
MediaBrowser.Controller/Drawing/ImageProcessingOptions.cs

@@ -37,8 +37,10 @@ namespace MediaBrowser.Controller.Drawing
 
 
         public bool AddPlayedIndicator { get; set; }
         public bool AddPlayedIndicator { get; set; }
 
 
-        public int? PercentPlayed { get; set; }
+        public int? UnplayedCount { get; set; }
 
 
+        public double? PercentPlayed { get; set; }
+        
         public string BackgroundColor { get; set; }
         public string BackgroundColor { get; set; }
 
 
         public bool HasDefaultOptions()
         public bool HasDefaultOptions()
@@ -56,6 +58,7 @@ namespace MediaBrowser.Controller.Drawing
                 IsOutputFormatDefault &&
                 IsOutputFormatDefault &&
                 !AddPlayedIndicator &&
                 !AddPlayedIndicator &&
                 !PercentPlayed.HasValue &&
                 !PercentPlayed.HasValue &&
+                !UnplayedCount.HasValue &&
                 string.IsNullOrEmpty(BackgroundColor);
                 string.IsNullOrEmpty(BackgroundColor);
         }
         }
 
 

+ 14 - 13
MediaBrowser.Controller/Entities/BaseItem.cs

@@ -156,7 +156,7 @@ namespace MediaBrowser.Controller.Entities
         public DateTime DateModified { get; set; }
         public DateTime DateModified { get; set; }
 
 
         public DateTime DateLastSaved { get; set; }
         public DateTime DateLastSaved { get; set; }
-        
+
         /// <summary>
         /// <summary>
         /// The logger
         /// The logger
         /// </summary>
         /// </summary>
@@ -327,21 +327,18 @@ namespace MediaBrowser.Controller.Entities
                 // When resolving the root, we need it's grandchildren (children of user views)
                 // When resolving the root, we need it's grandchildren (children of user views)
                 var flattenFolderDepth = isPhysicalRoot ? 2 : 0;
                 var flattenFolderDepth = isPhysicalRoot ? 2 : 0;
 
 
-                args.FileSystemDictionary = FileData.GetFilteredFileSystemEntries(args.Path, FileSystem, Logger, args, flattenFolderDepth: flattenFolderDepth, resolveShortcuts: isPhysicalRoot || args.IsVf);
+                var fileSystemDictionary = FileData.GetFilteredFileSystemEntries(args.Path, FileSystem, Logger, args, flattenFolderDepth: flattenFolderDepth, resolveShortcuts: isPhysicalRoot || args.IsVf);
 
 
                 // Need to remove subpaths that may have been resolved from shortcuts
                 // Need to remove subpaths that may have been resolved from shortcuts
                 // Example: if \\server\movies exists, then strip out \\server\movies\action
                 // Example: if \\server\movies exists, then strip out \\server\movies\action
                 if (isPhysicalRoot)
                 if (isPhysicalRoot)
                 {
                 {
-                    var paths = args.FileSystemDictionary.Keys.ToList();
+                    var paths = LibraryManager.NormalizeRootPathList(fileSystemDictionary.Keys);
 
 
-                    foreach (var subPath in paths
-                        .Where(subPath => !subPath.EndsWith(":\\", StringComparison.OrdinalIgnoreCase) && paths.Any(i => subPath.StartsWith(i.TrimEnd(System.IO.Path.DirectorySeparatorChar) + System.IO.Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase))))
-                    {
-                        Logger.Info("Ignoring duplicate path: {0}", subPath);
-                        args.FileSystemDictionary.Remove(subPath);
-                    }
+                    fileSystemDictionary = paths.Select(i => (FileSystemInfo)new DirectoryInfo(i)).ToDictionary(i => i.FullName);
                 }
                 }
+
+                args.FileSystemDictionary = fileSystemDictionary;
             }
             }
 
 
             //update our dates
             //update our dates
@@ -1016,14 +1013,18 @@ namespace MediaBrowser.Controller.Entities
             return lang;
             return lang;
         }
         }
 
 
+        public virtual bool IsSaveLocalMetadataEnabled()
+        {
+            return ConfigurationManager.Configuration.SaveLocalMeta;
+        }
+
         /// <summary>
         /// <summary>
         /// Determines if a given user has access to this item
         /// Determines if a given user has access to this item
         /// </summary>
         /// </summary>
         /// <param name="user">The user.</param>
         /// <param name="user">The user.</param>
-        /// <param name="localizationManager">The localization manager.</param>
         /// <returns><c>true</c> if [is parental allowed] [the specified user]; otherwise, <c>false</c>.</returns>
         /// <returns><c>true</c> if [is parental allowed] [the specified user]; otherwise, <c>false</c>.</returns>
         /// <exception cref="System.ArgumentNullException">user</exception>
         /// <exception cref="System.ArgumentNullException">user</exception>
-        public bool IsParentalAllowed(User user, ILocalizationManager localizationManager)
+        public bool IsParentalAllowed(User user)
         {
         {
             if (user == null)
             if (user == null)
             {
             {
@@ -1049,7 +1050,7 @@ namespace MediaBrowser.Controller.Entities
                 return !GetBlockUnratedValue(user.Configuration);
                 return !GetBlockUnratedValue(user.Configuration);
             }
             }
 
 
-            var value = localizationManager.GetRatingLevel(rating);
+            var value = LocalizationManager.GetRatingLevel(rating);
 
 
             // Could not determine the integer value
             // Could not determine the integer value
             if (!value.HasValue)
             if (!value.HasValue)
@@ -1084,7 +1085,7 @@ namespace MediaBrowser.Controller.Entities
                 throw new ArgumentNullException("user");
                 throw new ArgumentNullException("user");
             }
             }
 
 
-            return IsParentalAllowed(user, LocalizationManager);
+            return IsParentalAllowed(user);
         }
         }
 
 
         /// <summary>
         /// <summary>

+ 62 - 63
MediaBrowser.Controller/Entities/Folder.cs

@@ -519,85 +519,84 @@ namespace MediaBrowser.Controller.Entities
                     await Task.WhenAll(tasks).ConfigureAwait(false);
                     await Task.WhenAll(tasks).ConfigureAwait(false);
                 }
                 }
 
 
-                Tuple<BaseItem, bool> currentTuple = tuple;
-
-                tasks.Add(Task.Run(async () =>
-                {
-                    cancellationToken.ThrowIfCancellationRequested();
-
-                    var child = currentTuple.Item1;
-                    try
-                    {
-                        //refresh it
-                        await child.RefreshMetadata(cancellationToken, forceSave: currentTuple.Item2, forceRefresh: forceRefreshMetadata, resetResolveArgs: false).ConfigureAwait(false);
-                    }
-                    catch (IOException ex)
-                    {
-                        Logger.ErrorException("Error refreshing {0}", ex, child.Path ?? child.Name);
-                    }
+                tasks.Add(RefreshChild(tuple, progress, percentages, list.Count, cancellationToken, recursive, forceRefreshMetadata));
+            }
 
 
-                    // Refresh children if a folder and the item changed or recursive is set to true
-                    var refreshChildren = child.IsFolder && (currentTuple.Item2 || (recursive.HasValue && recursive.Value));
+            cancellationToken.ThrowIfCancellationRequested();
 
 
-                    if (refreshChildren)
-                    {
-                        // Don't refresh children if explicitly set to false
-                        if (recursive.HasValue && recursive.Value == false)
-                        {
-                            refreshChildren = false;
-                        }
-                    }
+            await Task.WhenAll(tasks).ConfigureAwait(false);
+        }
 
 
-                    if (refreshChildren)
-                    {
-                        cancellationToken.ThrowIfCancellationRequested();
+        private async Task RefreshChild(Tuple<BaseItem, bool> currentTuple, IProgress<double> progress, Dictionary<Guid, double> percentages, int childCount, CancellationToken cancellationToken, bool? recursive, bool forceRefreshMetadata = false)
+        {
+            cancellationToken.ThrowIfCancellationRequested();
 
 
-                        var innerProgress = new ActionableProgress<double>();
+            var child = currentTuple.Item1;
+            try
+            {
+                //refresh it
+                await child.RefreshMetadata(cancellationToken, forceSave: currentTuple.Item2, forceRefresh: forceRefreshMetadata, resetResolveArgs: false).ConfigureAwait(false);
+            }
+            catch (IOException ex)
+            {
+                Logger.ErrorException("Error refreshing {0}", ex, child.Path ?? child.Name);
+            }
 
 
-                        innerProgress.RegisterAction(p =>
-                        {
-                            lock (percentages)
-                            {
-                                percentages[child.Id] = p / 100;
+            // Refresh children if a folder and the item changed or recursive is set to true
+            var refreshChildren = child.IsFolder && (currentTuple.Item2 || (recursive.HasValue && recursive.Value));
 
 
-                                var percent = percentages.Values.Sum();
-                                percent /= list.Count;
+            if (refreshChildren)
+            {
+                // Don't refresh children if explicitly set to false
+                if (recursive.HasValue && recursive.Value == false)
+                {
+                    refreshChildren = false;
+                }
+            }
 
 
-                                progress.Report((90 * percent) + 10);
-                            }
-                        });
+            if (refreshChildren)
+            {
+                cancellationToken.ThrowIfCancellationRequested();
 
 
-                        await ((Folder)child).ValidateChildren(innerProgress, cancellationToken, recursive, forceRefreshMetadata).ConfigureAwait(false);
+                var innerProgress = new ActionableProgress<double>();
 
 
-                        try
-                        {
-                            // Some folder providers are unable to refresh until children have been refreshed.
-                            await child.RefreshMetadata(cancellationToken, resetResolveArgs: false).ConfigureAwait(false);
-                        }
-                        catch (IOException ex)
-                        {
-                            Logger.ErrorException("Error refreshing {0}", ex, child.Path ?? child.Name);
-                        }
-                    }
-                    else
+                innerProgress.RegisterAction(p =>
+                {
+                    lock (percentages)
                     {
                     {
-                        lock (percentages)
-                        {
-                            percentages[child.Id] = 1;
+                        percentages[child.Id] = p / 100;
 
 
-                            var percent = percentages.Values.Sum();
-                            percent /= list.Count;
+                        var percent = percentages.Values.Sum();
+                        percent /= childCount;
 
 
-                            progress.Report((90 * percent) + 10);
-                        }
+                        progress.Report((90 * percent) + 10);
                     }
                     }
+                });
 
 
-                }, cancellationToken));
+                await ((Folder)child).ValidateChildren(innerProgress, cancellationToken, recursive, forceRefreshMetadata).ConfigureAwait(false);
+
+                try
+                {
+                    // Some folder providers are unable to refresh until children have been refreshed.
+                    await child.RefreshMetadata(cancellationToken, resetResolveArgs: false).ConfigureAwait(false);
+                }
+                catch (IOException ex)
+                {
+                    Logger.ErrorException("Error refreshing {0}", ex, child.Path ?? child.Name);
+                }
             }
             }
+            else
+            {
+                lock (percentages)
+                {
+                    percentages[child.Id] = 1;
 
 
-            cancellationToken.ThrowIfCancellationRequested();
+                    var percent = percentages.Values.Sum();
+                    percent /= childCount;
 
 
-            await Task.WhenAll(tasks).ConfigureAwait(false);
+                    progress.Report((90 * percent) + 10);
+                }
+            }
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -646,7 +645,7 @@ namespace MediaBrowser.Controller.Entities
 
 
         private bool ContainsPath(string parent, string path)
         private bool ContainsPath(string parent, string path)
         {
         {
-            return string.Equals(parent, path, StringComparison.OrdinalIgnoreCase) || path.IndexOf(parent.TrimEnd(System.IO.Path.DirectorySeparatorChar) + System.IO.Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase) != -1;
+            return string.Equals(parent, path, StringComparison.OrdinalIgnoreCase) || FileSystem.ContainsSubPath(parent, path);
         }
         }
 
 
         /// <summary>
         /// <summary>

+ 3 - 3
MediaBrowser.Controller/Entities/IHasImages.cs

@@ -16,8 +16,8 @@ namespace MediaBrowser.Controller.Entities
         /// Gets the path.
         /// Gets the path.
         /// </summary>
         /// </summary>
         /// <value>The path.</value>
         /// <value>The path.</value>
-        string Path { get; }
-        
+        string Path { get; set; }
+
         /// <summary>
         /// <summary>
         /// Gets the identifier.
         /// Gets the identifier.
         /// </summary>
         /// </summary>
@@ -100,7 +100,7 @@ namespace MediaBrowser.Controller.Entities
         {
         {
             return item.HasImage(imageType, 0);
             return item.HasImage(imageType, 0);
         }
         }
-        
+
         /// <summary>
         /// <summary>
         /// Sets the image path.
         /// Sets the image path.
         /// </summary>
         /// </summary>

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

@@ -320,5 +320,12 @@ namespace MediaBrowser.Controller.Library
         /// <param name="items">The items.</param>
         /// <param name="items">The items.</param>
         /// <returns>IEnumerable{System.String}.</returns>
         /// <returns>IEnumerable{System.String}.</returns>
         IEnumerable<string> GetAllArtists(IEnumerable<BaseItem> items);
         IEnumerable<string> GetAllArtists(IEnumerable<BaseItem> items);
+
+        /// <summary>
+        /// Normalizes the root path list.
+        /// </summary>
+        /// <param name="paths">The paths.</param>
+        /// <returns>IEnumerable{System.String}.</returns>
+        IEnumerable<string> NormalizeRootPathList(IEnumerable<string> paths);
     }
     }
 }
 }

+ 1 - 1
MediaBrowser.Controller/LiveTv/ILiveTvManager.cs

@@ -153,7 +153,7 @@ namespace MediaBrowser.Controller.LiveTv
         /// <param name="id">The identifier.</param>
         /// <param name="id">The identifier.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <returns>LiveTvRecording.</returns>
         /// <returns>LiveTvRecording.</returns>
-        Task<LiveTvRecording> GetInternalRecording(string id, CancellationToken cancellationToken);
+        Task<ILiveTvRecording> GetInternalRecording(string id, CancellationToken cancellationToken);
 
 
         /// <summary>
         /// <summary>
         /// Gets the recording stream.
         /// Gets the recording stream.

+ 26 - 0
MediaBrowser.Controller/LiveTv/ILiveTvRecording.cs

@@ -0,0 +1,26 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.Entities;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.LiveTv
+{
+    public interface ILiveTvRecording : IHasImages, IHasMediaStreams
+    {
+        string ServiceName { get; set; }
+
+        string MediaType { get; }
+
+        LocationType LocationType { get; }
+
+        RecordingInfo RecordingInfo { get; set; }
+
+        string GetClientTypeName();
+
+        string GetUserDataKey();
+
+        bool IsParentalAllowed(User user);
+
+        Task<bool> RefreshMetadata(CancellationToken cancellationToken, bool forceSave = false, bool forceRefresh = false, bool allowSlowProviders = true, bool resetResolveArgs = true);
+    }
+}

+ 52 - 0
MediaBrowser.Controller/LiveTv/LiveTvAudioRecording.cs

@@ -0,0 +1,52 @@
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Model.Entities;
+
+namespace MediaBrowser.Controller.LiveTv
+{
+    public class LiveTvAudioRecording : Audio, ILiveTvRecording
+    {
+        /// <summary>
+        /// Gets the user data key.
+        /// </summary>
+        /// <returns>System.String.</returns>
+        public override string GetUserDataKey()
+        {
+            return GetClientTypeName() + "-" + Name;
+        }
+
+        public RecordingInfo RecordingInfo { get; set; }
+
+        public string ServiceName { get; set; }
+
+        public override string MediaType
+        {
+            get
+            {
+                return Model.Entities.MediaType.Audio;
+            }
+        }
+
+        public override LocationType LocationType
+        {
+            get
+            {
+                if (!string.IsNullOrEmpty(Path))
+                {
+                    return base.LocationType;
+                }
+
+                return LocationType.Remote;
+            }
+        }
+
+        public override string GetClientTypeName()
+        {
+            return "Recording";
+        }
+
+        public override bool IsSaveLocalMetadataEnabled()
+        {
+            return false;
+        }
+    }
+}

+ 12 - 3
MediaBrowser.Controller/LiveTv/LiveTvRecording.cs → MediaBrowser.Controller/LiveTv/LiveTvVideoRecording.cs

@@ -1,10 +1,9 @@
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.LiveTv;
 
 
 namespace MediaBrowser.Controller.LiveTv
 namespace MediaBrowser.Controller.LiveTv
 {
 {
-    public class LiveTvRecording : BaseItem
+    public class LiveTvVideoRecording : Video, ILiveTvRecording
     {
     {
         /// <summary>
         /// <summary>
         /// Gets the user data key.
         /// Gets the user data key.
@@ -23,7 +22,7 @@ namespace MediaBrowser.Controller.LiveTv
         {
         {
             get
             get
             {
             {
-                return RecordingInfo.ChannelType == ChannelType.Radio ? Model.Entities.MediaType.Audio : Model.Entities.MediaType.Video;
+                return Model.Entities.MediaType.Video;
             }
             }
         }
         }
 
 
@@ -31,6 +30,11 @@ namespace MediaBrowser.Controller.LiveTv
         {
         {
             get
             get
             {
             {
+                if (!string.IsNullOrEmpty(Path))
+                {
+                    return base.LocationType;
+                }
+
                 return LocationType.Remote;
                 return LocationType.Remote;
             }
             }
         }
         }
@@ -39,5 +43,10 @@ namespace MediaBrowser.Controller.LiveTv
         {
         {
             return "Recording";
             return "Recording";
         }
         }
+
+        public override bool IsSaveLocalMetadataEnabled()
+        {
+            return false;
+        }
     }
     }
 }
 }

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

@@ -108,6 +108,8 @@
     <Compile Include="Library\ItemUpdateType.cs" />
     <Compile Include="Library\ItemUpdateType.cs" />
     <Compile Include="Library\IUserDataManager.cs" />
     <Compile Include="Library\IUserDataManager.cs" />
     <Compile Include="Library\UserDataSaveEventArgs.cs" />
     <Compile Include="Library\UserDataSaveEventArgs.cs" />
+    <Compile Include="LiveTv\ILiveTvRecording.cs" />
+    <Compile Include="LiveTv\LiveTvAudioRecording.cs" />
     <Compile Include="LiveTv\LiveTvChannel.cs" />
     <Compile Include="LiveTv\LiveTvChannel.cs" />
     <Compile Include="LiveTv\ChannelInfo.cs" />
     <Compile Include="LiveTv\ChannelInfo.cs" />
     <Compile Include="LiveTv\ILiveTvManager.cs" />
     <Compile Include="LiveTv\ILiveTvManager.cs" />
@@ -115,7 +117,7 @@
     <Compile Include="LiveTv\LiveTvException.cs" />
     <Compile Include="LiveTv\LiveTvException.cs" />
     <Compile Include="LiveTv\StreamResponseInfo.cs" />
     <Compile Include="LiveTv\StreamResponseInfo.cs" />
     <Compile Include="LiveTv\LiveTvProgram.cs" />
     <Compile Include="LiveTv\LiveTvProgram.cs" />
-    <Compile Include="LiveTv\LiveTvRecording.cs" />
+    <Compile Include="LiveTv\LiveTvVideoRecording.cs" />
     <Compile Include="LiveTv\ProgramInfo.cs" />
     <Compile Include="LiveTv\ProgramInfo.cs" />
     <Compile Include="LiveTv\RecordingInfo.cs" />
     <Compile Include="LiveTv\RecordingInfo.cs" />
     <Compile Include="LiveTv\SeriesTimerInfo.cs" />
     <Compile Include="LiveTv\SeriesTimerInfo.cs" />

+ 12 - 0
MediaBrowser.Model/LiveTv/RecordingInfoDto.cs

@@ -51,6 +51,18 @@ namespace MediaBrowser.Model.LiveTv
         /// </summary>
         /// </summary>
         public string Name { get; set; }
         public string Name { get; set; }
 
 
+        /// <summary>
+        /// Gets or sets the type of the location.
+        /// </summary>
+        /// <value>The type of the location.</value>
+        public LocationType LocationType { get; set; }
+
+        /// <summary>
+        /// Gets or sets the media streams.
+        /// </summary>
+        /// <value>The media streams.</value>
+        public List<MediaStream> MediaStreams { get; set; }
+
         /// <summary>
         /// <summary>
         /// Gets or sets the path.
         /// Gets or sets the path.
         /// </summary>
         /// </summary>

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

@@ -40,6 +40,12 @@
         /// </summary>
         /// </summary>
         /// <value>The limit.</value>
         /// <value>The limit.</value>
         public int? Limit { get; set; }
         public int? Limit { get; set; }
+
+        /// <summary>
+        /// Gets or sets a value indicating whether this instance is recording.
+        /// </summary>
+        /// <value><c>null</c> if [is recording] contains no value, <c>true</c> if [is recording]; otherwise, <c>false</c>.</value>
+        public bool? IsRecording { get; set; }
     }
     }
 
 
     public class RecordingGroupQuery
     public class RecordingGroupQuery

+ 3 - 2
MediaBrowser.Mono.userprefs

@@ -1,8 +1,9 @@
 <Properties>
 <Properties>
   <MonoDevelop.Ide.Workspace ActiveConfiguration="Release Mono" />
   <MonoDevelop.Ide.Workspace ActiveConfiguration="Release Mono" />
-  <MonoDevelop.Ide.Workbench ActiveDocument="MediaBrowser.Server.Mono\app.config">
+  <MonoDevelop.Ide.Workbench ActiveDocument="MediaBrowser.Server.Mono\Networking\NetworkManager.cs">
     <Files>
     <Files>
-      <File FileName="MediaBrowser.Server.Mono\app.config" Line="5" Column="20" />
+      <File FileName="MediaBrowser.Server.Mono\app.config" Line="1" Column="1" />
+      <File FileName="MediaBrowser.Server.Mono\Networking\NetworkManager.cs" Line="6" Column="34" />
     </Files>
     </Files>
   </MonoDevelop.Ide.Workbench>
   </MonoDevelop.Ide.Workbench>
   <MonoDevelop.Ide.DebuggingService.Breakpoints>
   <MonoDevelop.Ide.DebuggingService.Breakpoints>

+ 1 - 1
MediaBrowser.Providers/MediaInfo/AudioImageProvider.cs

@@ -191,7 +191,7 @@ namespace MediaBrowser.Providers.MediaInfo
 
 
             var filename = item.Album ?? string.Empty;
             var filename = item.Album ?? string.Empty;
             filename += item.Artists.FirstOrDefault() ?? string.Empty;
             filename += item.Artists.FirstOrDefault() ?? string.Empty;
-            filename += album == null ? item.Id.ToString("N") + item.DateModified.Ticks : album.Id.ToString("N") + album.DateModified.Ticks + "_primary";
+            filename += album == null ? item.Id.ToString("N") + "_primary" + item.DateModified.Ticks : album.Id.ToString("N") + album.DateModified.Ticks + "_primary";
 
 
             filename = filename.GetMD5() + ".jpg";
             filename = filename.GetMD5() + ".jpg";
 
 

+ 0 - 9
MediaBrowser.Providers/Music/FanArtArtistProvider.cs

@@ -85,15 +85,6 @@ namespace MediaBrowser.Providers.Music
             return item is MusicArtist;
             return item is MusicArtist;
         }
         }
 
 
-        /// <summary>
-        /// Gets a value indicating whether [save local meta].
-        /// </summary>
-        /// <value><c>true</c> if [save local meta]; otherwise, <c>false</c>.</value>
-        protected virtual bool SaveLocalMeta
-        {
-            get { return ConfigurationManager.Configuration.SaveLocalMeta; }
-        }
-
         /// <summary>
         /// <summary>
         /// Gets a value indicating whether [refresh on version change].
         /// Gets a value indicating whether [refresh on version change].
         /// </summary>
         /// </summary>

+ 0 - 8
MediaBrowser.Providers/Music/LastfmBaseProvider.cs

@@ -68,14 +68,6 @@ namespace MediaBrowser.Providers.Music
         /// <value>The HTTP client.</value>
         /// <value>The HTTP client.</value>
         protected IHttpClient HttpClient { get; private set; }
         protected IHttpClient HttpClient { get; private set; }
 
 
-        protected virtual bool SaveLocalMeta
-        {
-            get
-            {
-                return ConfigurationManager.Configuration.SaveLocalMeta;
-            }
-        }
-
         /// <summary>
         /// <summary>
         /// Gets a value indicating whether [requires internet].
         /// Gets a value indicating whether [requires internet].
         /// </summary>
         /// </summary>

+ 0 - 21
MediaBrowser.Providers/Music/LastfmHelper.cs

@@ -31,11 +31,6 @@ namespace MediaBrowser.Providers.Music
 
 
                 artist.ProductionYear = yearFormed;
                 artist.ProductionYear = yearFormed;
             }
             }
-            
-            if (data.tags != null && !artist.LockedFields.Contains(MetadataFields.Tags))
-            {
-                AddTags(artist, data.tags);
-            }
 
 
             string imageSize;
             string imageSize;
             artist.LastFmImageUrl = GetImageUrl(data, out imageSize);
             artist.LastFmImageUrl = GetImageUrl(data, out imageSize);
@@ -100,11 +95,6 @@ namespace MediaBrowser.Providers.Music
                 }
                 }
             }
             }
 
 
-            if (data.toptags != null && !item.LockedFields.Contains(MetadataFields.Tags))
-            {
-                AddTags(item, data.toptags);
-            }
-
             var album = (MusicAlbum)item;
             var album = (MusicAlbum)item;
 
 
             string imageSize;
             string imageSize;
@@ -112,16 +102,5 @@ namespace MediaBrowser.Providers.Music
             album.LastFmImageUrl = GetImageUrl(data, out imageSize);
             album.LastFmImageUrl = GetImageUrl(data, out imageSize);
             album.LastFmImageSize = imageSize;
             album.LastFmImageSize = imageSize;
         }
         }
-
-        private static void AddTags(BaseItem item, LastfmTags tags)
-        {
-            var itemTags = (from tag in tags.tag where !string.IsNullOrEmpty(tag.name) select tag.name).ToList();
-
-            var hasTags = item as IHasTags;
-            if (hasTags != null)
-            {
-                hasTags.Tags = itemTags;
-            }
-        }
     }
     }
 }
 }

+ 1 - 1
MediaBrowser.Providers/Savers/AlbumXmlSaver.cs

@@ -32,7 +32,7 @@ namespace MediaBrowser.Providers.Savers
             var wasMetadataDownloaded = (updateType & ItemUpdateType.MetadataDownload) == ItemUpdateType.MetadataDownload;
             var wasMetadataDownloaded = (updateType & ItemUpdateType.MetadataDownload) == ItemUpdateType.MetadataDownload;
 
 
             // If new metadata has been downloaded and save local is on
             // If new metadata has been downloaded and save local is on
-            if (_config.Configuration.SaveLocalMeta && (wasMetadataEdited || wasMetadataDownloaded))
+            if (item.IsSaveLocalMetadataEnabled() && (wasMetadataEdited || wasMetadataDownloaded))
             {
             {
                 return item is MusicAlbum;
                 return item is MusicAlbum;
             }
             }

+ 1 - 1
MediaBrowser.Providers/Savers/ArtistXmlSaver.cs

@@ -32,7 +32,7 @@ namespace MediaBrowser.Providers.Savers
             var wasMetadataDownloaded = (updateType & ItemUpdateType.MetadataDownload) == ItemUpdateType.MetadataDownload;
             var wasMetadataDownloaded = (updateType & ItemUpdateType.MetadataDownload) == ItemUpdateType.MetadataDownload;
 
 
             // If new metadata has been downloaded and save local is on
             // If new metadata has been downloaded and save local is on
-            if (_config.Configuration.SaveLocalMeta && (wasMetadataEdited || wasMetadataDownloaded))
+            if (item.IsSaveLocalMetadataEnabled() && (wasMetadataEdited || wasMetadataDownloaded))
             {
             {
                 if (item is MusicArtist)
                 if (item is MusicArtist)
                 {
                 {

+ 1 - 1
MediaBrowser.Providers/Savers/BoxSetXmlSaver.cs

@@ -32,7 +32,7 @@ namespace MediaBrowser.Providers.Savers
             var wasMetadataDownloaded = (updateType & ItemUpdateType.MetadataDownload) == ItemUpdateType.MetadataDownload;
             var wasMetadataDownloaded = (updateType & ItemUpdateType.MetadataDownload) == ItemUpdateType.MetadataDownload;
 
 
             // If new metadata has been downloaded and save local is on
             // If new metadata has been downloaded and save local is on
-            if (_config.Configuration.SaveLocalMeta && (wasMetadataEdited || wasMetadataDownloaded))
+            if (item.IsSaveLocalMetadataEnabled() && (wasMetadataEdited || wasMetadataDownloaded))
             {
             {
                 return item is BoxSet;
                 return item is BoxSet;
             }
             }

+ 1 - 1
MediaBrowser.Providers/Savers/EpisodeXmlSaver.cs

@@ -29,7 +29,7 @@ namespace MediaBrowser.Providers.Savers
             var wasMetadataDownloaded = (updateType & ItemUpdateType.MetadataDownload) == ItemUpdateType.MetadataDownload;
             var wasMetadataDownloaded = (updateType & ItemUpdateType.MetadataDownload) == ItemUpdateType.MetadataDownload;
 
 
             // If new metadata has been downloaded and save local is on
             // If new metadata has been downloaded and save local is on
-            if (_config.Configuration.SaveLocalMeta && (wasMetadataEdited || wasMetadataDownloaded))
+            if (item.IsSaveLocalMetadataEnabled() && (wasMetadataEdited || wasMetadataDownloaded))
             {
             {
                 return item is Episode;
                 return item is Episode;
             }
             }

+ 1 - 1
MediaBrowser.Providers/Savers/FolderXmlSaver.cs

@@ -37,7 +37,7 @@ namespace MediaBrowser.Providers.Savers
             var wasMetadataDownloaded = (updateType & ItemUpdateType.MetadataDownload) == ItemUpdateType.MetadataDownload;
             var wasMetadataDownloaded = (updateType & ItemUpdateType.MetadataDownload) == ItemUpdateType.MetadataDownload;
 
 
             // If new metadata has been downloaded and save local is on
             // If new metadata has been downloaded and save local is on
-            if (_config.Configuration.SaveLocalMeta && (wasMetadataEdited || wasMetadataDownloaded))
+            if (item.IsSaveLocalMetadataEnabled() && (wasMetadataEdited || wasMetadataDownloaded))
             {
             {
                 if (!(item is Series) && !(item is BoxSet) && !(item is MusicArtist) && !(item is MusicAlbum) &&
                 if (!(item is Series) && !(item is BoxSet) && !(item is MusicArtist) && !(item is MusicAlbum) &&
                     !(item is Season))
                     !(item is Season))

+ 1 - 1
MediaBrowser.Providers/Savers/GameSystemXmlSaver.cs

@@ -31,7 +31,7 @@ namespace MediaBrowser.Providers.Savers
             var wasMetadataDownloaded = (updateType & ItemUpdateType.MetadataDownload) == ItemUpdateType.MetadataDownload;
             var wasMetadataDownloaded = (updateType & ItemUpdateType.MetadataDownload) == ItemUpdateType.MetadataDownload;
 
 
             // If new metadata has been downloaded and save local is on
             // If new metadata has been downloaded and save local is on
-            if (_config.Configuration.SaveLocalMeta && (wasMetadataEdited || wasMetadataDownloaded))
+            if (item.IsSaveLocalMetadataEnabled() && (wasMetadataEdited || wasMetadataDownloaded))
             {
             {
                 return item is GameSystem;
                 return item is GameSystem;
             }
             }

+ 1 - 1
MediaBrowser.Providers/Savers/GameXmlSaver.cs

@@ -37,7 +37,7 @@ namespace MediaBrowser.Providers.Savers
             var wasMetadataDownloaded = (updateType & ItemUpdateType.MetadataDownload) == ItemUpdateType.MetadataDownload;
             var wasMetadataDownloaded = (updateType & ItemUpdateType.MetadataDownload) == ItemUpdateType.MetadataDownload;
 
 
             // If new metadata has been downloaded and save local is on
             // If new metadata has been downloaded and save local is on
-            if (_config.Configuration.SaveLocalMeta && (wasMetadataEdited || wasMetadataDownloaded))
+            if (item.IsSaveLocalMetadataEnabled() && (wasMetadataEdited || wasMetadataDownloaded))
             {
             {
                 return item is Game;
                 return item is Game;
             }
             }

+ 1 - 1
MediaBrowser.Providers/Savers/MovieXmlSaver.cs

@@ -39,7 +39,7 @@ namespace MediaBrowser.Providers.Savers
             var wasMetadataDownloaded = (updateType & ItemUpdateType.MetadataDownload) == ItemUpdateType.MetadataDownload;
             var wasMetadataDownloaded = (updateType & ItemUpdateType.MetadataDownload) == ItemUpdateType.MetadataDownload;
 
 
             // If new metadata has been downloaded and save local is on
             // If new metadata has been downloaded and save local is on
-            if (_config.Configuration.SaveLocalMeta && (wasMetadataEdited || wasMetadataDownloaded))
+            if (item.IsSaveLocalMetadataEnabled() && (wasMetadataEdited || wasMetadataDownloaded))
             {
             {
                 var trailer = item as Trailer;
                 var trailer = item as Trailer;
 
 

+ 1 - 1
MediaBrowser.Providers/Savers/SeasonXmlSaver.cs

@@ -30,7 +30,7 @@ namespace MediaBrowser.Providers.Savers
             var wasMetadataDownloaded = (updateType & ItemUpdateType.MetadataDownload) == ItemUpdateType.MetadataDownload;
             var wasMetadataDownloaded = (updateType & ItemUpdateType.MetadataDownload) == ItemUpdateType.MetadataDownload;
 
 
             // If new metadata has been downloaded and save local is on
             // If new metadata has been downloaded and save local is on
-            if (_config.Configuration.SaveLocalMeta && (wasMetadataEdited || wasMetadataDownloaded))
+            if (item.IsSaveLocalMetadataEnabled() && (wasMetadataEdited || wasMetadataDownloaded))
             {
             {
                 return item is Season;
                 return item is Season;
             }
             }

+ 1 - 1
MediaBrowser.Providers/Savers/SeriesXmlSaver.cs

@@ -32,7 +32,7 @@ namespace MediaBrowser.Providers.Savers
             var wasMetadataDownloaded = (updateType & ItemUpdateType.MetadataDownload) == ItemUpdateType.MetadataDownload;
             var wasMetadataDownloaded = (updateType & ItemUpdateType.MetadataDownload) == ItemUpdateType.MetadataDownload;
 
 
             // If new metadata has been downloaded and save local is on
             // If new metadata has been downloaded and save local is on
-            if (_config.Configuration.SaveLocalMeta && (wasMetadataEdited || wasMetadataDownloaded))
+            if (item.IsSaveLocalMetadataEnabled() && (wasMetadataEdited || wasMetadataDownloaded))
             {
             {
                 return item is Series;
                 return item is Series;
             }
             }

+ 34 - 9
MediaBrowser.Server.Implementations/Drawing/ImageProcessor.cs

@@ -172,7 +172,7 @@ namespace MediaBrowser.Server.Implementations.Drawing
 
 
             var quality = options.Quality ?? 90;
             var quality = options.Quality ?? 90;
 
 
-            var cacheFilePath = GetCacheFilePath(originalImagePath, newSize, quality, dateModified, options.OutputFormat, options.AddPlayedIndicator, options.PercentPlayed, options.BackgroundColor);
+            var cacheFilePath = GetCacheFilePath(originalImagePath, newSize, quality, dateModified, options.OutputFormat, options.AddPlayedIndicator, options.PercentPlayed, options.UnplayedCount, options.BackgroundColor);
 
 
             try
             try
             {
             {
@@ -241,7 +241,9 @@ namespace MediaBrowser.Server.Implementations.Drawing
                                     thumbnailGraph.SmoothingMode = SmoothingMode.HighQuality;
                                     thumbnailGraph.SmoothingMode = SmoothingMode.HighQuality;
                                     thumbnailGraph.InterpolationMode = InterpolationMode.HighQualityBicubic;
                                     thumbnailGraph.InterpolationMode = InterpolationMode.HighQualityBicubic;
                                     thumbnailGraph.PixelOffsetMode = PixelOffsetMode.HighQuality;
                                     thumbnailGraph.PixelOffsetMode = PixelOffsetMode.HighQuality;
-                                    thumbnailGraph.CompositingMode = string.IsNullOrEmpty(options.BackgroundColor) && !options.PercentPlayed.HasValue && !options.AddPlayedIndicator ? CompositingMode.SourceCopy : CompositingMode.SourceOver;
+                                    thumbnailGraph.CompositingMode = string.IsNullOrEmpty(options.BackgroundColor) && !options.UnplayedCount.HasValue && !options.AddPlayedIndicator && !options.PercentPlayed.HasValue ? 
+                                        CompositingMode.SourceCopy : 
+                                        CompositingMode.SourceOver;
 
 
                                     SetBackgroundColor(thumbnailGraph, options);
                                     SetBackgroundColor(thumbnailGraph, options);
 
 
@@ -347,28 +349,31 @@ namespace MediaBrowser.Server.Implementations.Drawing
         /// <param name="options">The options.</param>
         /// <param name="options">The options.</param>
         private void DrawIndicator(Graphics graphics, int imageWidth, int imageHeight, ImageProcessingOptions options)
         private void DrawIndicator(Graphics graphics, int imageWidth, int imageHeight, ImageProcessingOptions options)
         {
         {
-            if (!options.AddPlayedIndicator && !options.PercentPlayed.HasValue)
+            if (!options.AddPlayedIndicator && !options.UnplayedCount.HasValue && !options.PercentPlayed.HasValue)
             {
             {
                 return;
                 return;
             }
             }
 
 
             try
             try
             {
             {
-                var percentOffset = 0;
-
                 if (options.AddPlayedIndicator)
                 if (options.AddPlayedIndicator)
                 {
                 {
                     var currentImageSize = new Size(imageWidth, imageHeight);
                     var currentImageSize = new Size(imageWidth, imageHeight);
 
 
-                    new WatchedIndicatorDrawer().Process(graphics, currentImageSize);
+                    new PlayedIndicatorDrawer().DrawPlayedIndicator(graphics, currentImageSize);
+                }
+                else if (options.UnplayedCount.HasValue)
+                {
+                    var currentImageSize = new Size(imageWidth, imageHeight);
 
 
-                    percentOffset = 0 - WatchedIndicatorDrawer.IndicatorWidth;
+                    new UnplayedCountIndicator().DrawUnplayedCountIndicator(graphics, currentImageSize, options.UnplayedCount.Value);
                 }
                 }
+
                 if (options.PercentPlayed.HasValue)
                 if (options.PercentPlayed.HasValue)
                 {
                 {
                     var currentImageSize = new Size(imageWidth, imageHeight);
                     var currentImageSize = new Size(imageWidth, imageHeight);
 
 
-                    new PercentPlayedDrawer().Process(graphics, currentImageSize, options.PercentPlayed.Value, percentOffset);
+                    new PercentPlayedDrawer().Process(graphics, currentImageSize, options.PercentPlayed.Value);
                 }
                 }
             }
             }
             catch (Exception ex)
             catch (Exception ex)
@@ -465,10 +470,15 @@ namespace MediaBrowser.Server.Implementations.Drawing
             return new Tuple<string, DateTime>(croppedImagePath, _fileSystem.GetLastWriteTimeUtc(croppedImagePath));
             return new Tuple<string, DateTime>(croppedImagePath, _fileSystem.GetLastWriteTimeUtc(croppedImagePath));
         }
         }
 
 
+        /// <summary>
+        /// Increment this when indicator drawings change
+        /// </summary>
+        private const string IndicatorVersion = "1";
+
         /// <summary>
         /// <summary>
         /// Gets the cache file path based on a set of parameters
         /// Gets the cache file path based on a set of parameters
         /// </summary>
         /// </summary>
-        private string GetCacheFilePath(string originalPath, ImageSize outputSize, int quality, DateTime dateModified, ImageOutputFormat format, bool addPlayedIndicator, int? percentPlayed, string backgroundColor)
+        private string GetCacheFilePath(string originalPath, ImageSize outputSize, int quality, DateTime dateModified, ImageOutputFormat format, bool addPlayedIndicator, double? percentPlayed, int? unwatchedCount, string backgroundColor)
         {
         {
             var filename = originalPath;
             var filename = originalPath;
 
 
@@ -485,16 +495,31 @@ namespace MediaBrowser.Server.Implementations.Drawing
                 filename += "f=" + format;
                 filename += "f=" + format;
             }
             }
 
 
+            var hasIndicator = false;
+
             if (addPlayedIndicator)
             if (addPlayedIndicator)
             {
             {
                 filename += "pl=true";
                 filename += "pl=true";
+                hasIndicator = true;
             }
             }
 
 
             if (percentPlayed.HasValue)
             if (percentPlayed.HasValue)
             {
             {
                 filename += "p=" + percentPlayed.Value;
                 filename += "p=" + percentPlayed.Value;
+                hasIndicator = true;
             }
             }
 
 
+            if (unwatchedCount.HasValue)
+            {
+                filename += "p=" + unwatchedCount.Value;
+                hasIndicator = true;
+            }
+
+            if (hasIndicator)
+            {
+                filename += "iv=" + IndicatorVersion;
+            }
+            
             if (!string.IsNullOrEmpty(backgroundColor))
             if (!string.IsNullOrEmpty(backgroundColor))
             {
             {
                 filename += "b=" + backgroundColor;
                 filename += "b=" + backgroundColor;

+ 17 - 19
MediaBrowser.Server.Implementations/Drawing/PercentPlayedDrawer.cs

@@ -1,36 +1,34 @@
-using System.Drawing;
-using System.Globalization;
+using System;
+using System.Drawing;
 
 
 namespace MediaBrowser.Server.Implementations.Drawing
 namespace MediaBrowser.Server.Implementations.Drawing
 {
 {
     public class PercentPlayedDrawer
     public class PercentPlayedDrawer
     {
     {
-        private const int IndicatorWidth = 80;
-        private const int IndicatorHeight = 50;
-        private const int FontSize = 30;
-        private readonly CultureInfo _usCulture = new CultureInfo("en-US");
+        private const int IndicatorHeight = 10;
 
 
-        public void Process(Graphics graphics, Size imageSize, int percent, int rightOffset)
+        public void Process(Graphics graphics, Size imageSize, double percent)
         {
         {
-            var x = imageSize.Width - IndicatorWidth + rightOffset;
+            var y = imageSize.Height - IndicatorHeight;
 
 
-            using (var backdroundBrush = new SolidBrush(Color.FromArgb(225, 102, 192, 16)))
+            using (var backdroundBrush = new SolidBrush(Color.FromArgb(225, 0, 0, 0)))
             {
             {
-                graphics.FillRectangle(backdroundBrush, x, 0, IndicatorWidth, IndicatorHeight);
+                const int innerX = 0;
+                var innerY = y;
+                var innerWidth = imageSize.Width;
+                var innerHeight = imageSize.Height;
 
 
-                var text = string.Format("{0}%", percent.ToString(_usCulture));
+                graphics.FillRectangle(backdroundBrush, innerX, innerY, innerWidth, innerHeight);
 
 
-                x = imageSize.Width - (percent < 10 ? 66 : 75) + rightOffset;
-
-                using (var font = new Font(FontFamily.GenericSansSerif, FontSize, FontStyle.Regular, GraphicsUnit.Pixel))
+                using (var foregroundBrush = new SolidBrush(Color.FromArgb(82, 181, 75)))
                 {
                 {
-                    using (var fontBrush = new SolidBrush(Color.White))
-                    {
-                        graphics.DrawString(text, font, fontBrush, x, 6);
-                    }
+                    double foregroundWidth = innerWidth;
+                    foregroundWidth *= percent;
+                    foregroundWidth /= 100;
+
+                    graphics.FillRectangle(foregroundBrush, innerX, innerY, Convert.ToInt32(Math.Round(foregroundWidth)), innerHeight);
                 }
                 }
             }
             }
-
         }
         }
     }
     }
 }
 }

+ 8 - 10
MediaBrowser.Server.Implementations/Drawing/WatchedIndicatorDrawer.cs → MediaBrowser.Server.Implementations/Drawing/PlayedIndicatorDrawer.cs

@@ -2,33 +2,31 @@
 
 
 namespace MediaBrowser.Server.Implementations.Drawing
 namespace MediaBrowser.Server.Implementations.Drawing
 {
 {
-    public class WatchedIndicatorDrawer
+    public class PlayedIndicatorDrawer
     {
     {
         private const int IndicatorHeight = 50;
         private const int IndicatorHeight = 50;
         public const int IndicatorWidth = 50;
         public const int IndicatorWidth = 50;
         private const int FontSize = 50;
         private const int FontSize = 50;
+        private const int OffsetFromTopRightCorner = 10;
 
 
-        public void Process(Graphics graphics, Size imageSize)
+        public void DrawPlayedIndicator(Graphics graphics, Size imageSize)
         {
         {
-            var x = imageSize.Width - IndicatorWidth;
+            var x = imageSize.Width - IndicatorWidth - OffsetFromTopRightCorner;
 
 
-            using (var backdroundBrush = new SolidBrush(Color.FromArgb(225, 204, 51, 51)))
+            using (var backdroundBrush = new SolidBrush(Color.FromArgb(225, 82, 181, 75)))
             {
             {
-                graphics.FillRectangle(backdroundBrush, x, 0, IndicatorWidth, IndicatorHeight);
+                graphics.FillEllipse(backdroundBrush, x, OffsetFromTopRightCorner, IndicatorWidth, IndicatorHeight);
 
 
-                const string text = "a";
-
-                x = imageSize.Width - 55;
+                x = imageSize.Width - 55 - OffsetFromTopRightCorner;
 
 
                 using (var font = new Font("Webdings", FontSize, FontStyle.Regular, GraphicsUnit.Pixel))
                 using (var font = new Font("Webdings", FontSize, FontStyle.Regular, GraphicsUnit.Pixel))
                 {
                 {
                     using (var fontBrush = new SolidBrush(Color.White))
                     using (var fontBrush = new SolidBrush(Color.White))
                     {
                     {
-                        graphics.DrawString(text, font, fontBrush, x, -2);
+                        graphics.DrawString("a", font, fontBrush, x, OffsetFromTopRightCorner - 2);
                     }
                     }
                 }
                 }
             }
             }
-
         }
         }
     }
     }
 }
 }

+ 55 - 0
MediaBrowser.Server.Implementations/Drawing/UnplayedCountIndicator.cs

@@ -0,0 +1,55 @@
+using System;
+using System.Collections.Generic;
+using System.Drawing;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Server.Implementations.Drawing
+{
+    public class UnplayedCountIndicator
+    {
+        private const int IndicatorHeight = 50;
+        public const int IndicatorWidth = 50;
+        private const int OffsetFromTopRightCorner = 10;
+
+        public void DrawUnplayedCountIndicator(Graphics graphics, Size imageSize, int count)
+        {
+            var x = imageSize.Width - IndicatorWidth - OffsetFromTopRightCorner;
+
+            using (var backdroundBrush = new SolidBrush(Color.FromArgb(225, 82, 181, 75)))
+            {
+                graphics.FillEllipse(backdroundBrush, x, OffsetFromTopRightCorner, IndicatorWidth, IndicatorHeight);
+
+                var text = count.ToString();
+
+                x = imageSize.Width - 50 - OffsetFromTopRightCorner;
+                var y = OffsetFromTopRightCorner + 7;
+                var fontSize = 30;
+
+                if (text.Length == 1)
+                {
+                    x += 11;
+                }
+                else if (text.Length == 2)
+                {
+                    x += 3;
+                }
+                else if (text.Length == 3)
+                {
+                    //x += 1;
+                    y += 3;
+                    fontSize = 24;
+                }
+
+                using (var font = new Font("Sans-Serif", fontSize, FontStyle.Regular, GraphicsUnit.Pixel))
+                {
+                    using (var fontBrush = new SolidBrush(Color.White))
+                    {
+                        graphics.DrawString(text, font, fontBrush, x, y);
+                    }
+                }
+            }
+        }
+    }
+}

+ 11 - 14
MediaBrowser.Server.Implementations/Library/CoreResolutionIgnoreRule.cs

@@ -1,4 +1,5 @@
-using MediaBrowser.Controller.Entities;
+using MediaBrowser.Common.IO;
+using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Resolvers;
 using MediaBrowser.Controller.Resolvers;
 using System;
 using System;
@@ -30,6 +31,13 @@ namespace MediaBrowser.Server.Implementations.Library
 
 
         }.ToDictionary(i => i, StringComparer.OrdinalIgnoreCase);
         }.ToDictionary(i => i, StringComparer.OrdinalIgnoreCase);
 
 
+        private readonly IFileSystem _fileSystem;
+
+        public CoreResolutionIgnoreRule(IFileSystem fileSystem)
+        {
+            _fileSystem = fileSystem;
+        }
+
         /// <summary>
         /// <summary>
         /// Shoulds the ignore.
         /// Shoulds the ignore.
         /// </summary>
         /// </summary>
@@ -60,23 +68,12 @@ namespace MediaBrowser.Server.Implementations.Library
                     return false;
                     return false;
                 }
                 }
 
 
-                // Drives will sometimes be hidden
-                if (args.Path.EndsWith(Path.VolumeSeparatorChar + "\\", StringComparison.OrdinalIgnoreCase))
+                // Sometimes these are marked hidden
+                if (_fileSystem.IsRootPath(args.Path))
                 {
                 {
                     return false;
                     return false;
                 }
                 }
 
 
-                // Shares will sometimes be hidden
-                if (args.Path.StartsWith("\\", StringComparison.OrdinalIgnoreCase))
-                {
-                    // Look for a share, e.g. \\server\movies
-                    // Is there a better way to detect if a path is a share without using native code?
-                    if (args.Path.Substring(2).Split(Path.DirectorySeparatorChar).Length == 2)
-                    {
-                        return false;
-                    }
-                }
-
                 return true;
                 return true;
             }
             }
 
 

+ 22 - 8
MediaBrowser.Server.Implementations/Library/LibraryManager.cs

@@ -499,21 +499,18 @@ namespace MediaBrowser.Server.Implementations.Library
                 // When resolving the root, we need it's grandchildren (children of user views)
                 // When resolving the root, we need it's grandchildren (children of user views)
                 var flattenFolderDepth = isPhysicalRoot ? 2 : 0;
                 var flattenFolderDepth = isPhysicalRoot ? 2 : 0;
 
 
-                args.FileSystemDictionary = FileData.GetFilteredFileSystemEntries(args.Path, _fileSystem, _logger, args, flattenFolderDepth: flattenFolderDepth, resolveShortcuts: isPhysicalRoot || args.IsVf);
+                var fileSystemDictionary = FileData.GetFilteredFileSystemEntries(args.Path, _fileSystem, _logger, args, flattenFolderDepth: flattenFolderDepth, resolveShortcuts: isPhysicalRoot || args.IsVf);
 
 
                 // Need to remove subpaths that may have been resolved from shortcuts
                 // Need to remove subpaths that may have been resolved from shortcuts
                 // Example: if \\server\movies exists, then strip out \\server\movies\action
                 // Example: if \\server\movies exists, then strip out \\server\movies\action
                 if (isPhysicalRoot)
                 if (isPhysicalRoot)
                 {
                 {
-                    var paths = args.FileSystemDictionary.Keys.ToList();
+                    var paths = NormalizeRootPathList(fileSystemDictionary.Keys);
 
 
-                    foreach (var subPath in paths
-                        .Where(subPath => !subPath.EndsWith(":\\", StringComparison.OrdinalIgnoreCase) && paths.Any(i => subPath.StartsWith(i.TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase))))
-                    {
-                        _logger.Info("Ignoring duplicate path: {0}", subPath);
-                        args.FileSystemDictionary.Remove(subPath);
-                    }
+                    fileSystemDictionary = paths.Select(i => (FileSystemInfo)new DirectoryInfo(i)).ToDictionary(i => i.FullName);
                 }
                 }
+
+                args.FileSystemDictionary = fileSystemDictionary;
             }
             }
 
 
             // Check to see if we should resolve based on our contents
             // Check to see if we should resolve based on our contents
@@ -525,6 +522,23 @@ namespace MediaBrowser.Server.Implementations.Library
             return ResolveItem(args);
             return ResolveItem(args);
         }
         }
 
 
+        public IEnumerable<string> NormalizeRootPathList(IEnumerable<string> paths)
+        {
+            var list = paths.Select(_fileSystem.NormalizePath)
+                .Distinct(StringComparer.OrdinalIgnoreCase)
+                .ToList();
+
+            var dupes = list.Where(subPath => !subPath.EndsWith(":\\", StringComparison.OrdinalIgnoreCase) && list.Any(i => _fileSystem.ContainsSubPath(i, subPath)))
+                .ToList();
+
+            foreach (var dupe in dupes)
+            {
+                _logger.Info("Found duplicate path: {0}", dupe);
+            }
+
+            return list.Except(dupes, StringComparer.OrdinalIgnoreCase);
+        }
+
         /// <summary>
         /// <summary>
         /// Determines whether a path should be ignored based on its contents - called after the contents have been read
         /// Determines whether a path should be ignored based on its contents - called after the contents have been read
         /// </summary>
         /// </summary>

+ 24 - 11
MediaBrowser.Server.Implementations/LiveTv/LiveTvDtoService.cs

@@ -4,11 +4,13 @@ using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.LiveTv;
 using MediaBrowser.Controller.LiveTv;
+using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.LiveTv;
 using MediaBrowser.Model.LiveTv;
 using MediaBrowser.Model.Logging;
 using MediaBrowser.Model.Logging;
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
+using System.Linq;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 
 
@@ -21,13 +23,15 @@ namespace MediaBrowser.Server.Implementations.LiveTv
 
 
         private readonly IUserDataManager _userDataManager;
         private readonly IUserDataManager _userDataManager;
         private readonly IDtoService _dtoService;
         private readonly IDtoService _dtoService;
+        private readonly IItemRepository _itemRepo;
 
 
-        public LiveTvDtoService(IDtoService dtoService, IUserDataManager userDataManager, IImageProcessor imageProcessor, ILogger logger)
+        public LiveTvDtoService(IDtoService dtoService, IUserDataManager userDataManager, IImageProcessor imageProcessor, ILogger logger, IItemRepository itemRepo)
         {
         {
             _dtoService = dtoService;
             _dtoService = dtoService;
             _userDataManager = userDataManager;
             _userDataManager = userDataManager;
             _imageProcessor = imageProcessor;
             _imageProcessor = imageProcessor;
             _logger = logger;
             _logger = logger;
+            _itemRepo = itemRepo;
         }
         }
 
 
         public TimerInfoDto GetTimerInfoDto(TimerInfo info, ILiveTvService service, LiveTvProgram program, LiveTvChannel channel)
         public TimerInfoDto GetTimerInfoDto(TimerInfo info, ILiveTvService service, LiveTvProgram program, LiveTvChannel channel)
@@ -180,7 +184,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
             return status.ToString();
             return status.ToString();
         }
         }
 
 
-        public RecordingInfoDto GetRecordingInfoDto(LiveTvRecording recording, LiveTvChannel channel, ILiveTvService service, User user = null)
+        public RecordingInfoDto GetRecordingInfoDto(ILiveTvRecording recording, LiveTvChannel channel, ILiveTvService service, User user = null)
         {
         {
             var info = recording.RecordingInfo;
             var info = recording.RecordingInfo;
 
 
@@ -216,7 +220,14 @@ namespace MediaBrowser.Server.Implementations.LiveTv
                 IsNews = info.IsNews,
                 IsNews = info.IsNews,
                 IsKids = info.IsKids,
                 IsKids = info.IsKids,
                 IsPremiere = info.IsPremiere,
                 IsPremiere = info.IsPremiere,
-                RunTimeTicks = (info.EndDate - info.StartDate).Ticks
+                RunTimeTicks = (info.EndDate - info.StartDate).Ticks,
+                LocationType = recording.LocationType,
+
+                MediaStreams = _itemRepo.GetMediaStreams(new MediaStreamQuery
+                {
+                     ItemId = recording.Id
+
+                }).ToList()
             };
             };
 
 
             var imageTag = GetImageTag(recording);
             var imageTag = GetImageTag(recording);
@@ -330,7 +341,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
             return dto;
             return dto;
         }
         }
 
 
-        private Guid? GetImageTag(BaseItem info)
+        private Guid? GetImageTag(IHasImages info)
         {
         {
             var path = info.PrimaryImagePath;
             var path = info.PrimaryImagePath;
 
 
@@ -351,39 +362,41 @@ namespace MediaBrowser.Server.Implementations.LiveTv
             return null;
             return null;
         }
         }
 
 
+        private const string InternalVersionNumber = "2";
+
         public Guid GetInternalChannelId(string serviceName, string externalId)
         public Guid GetInternalChannelId(string serviceName, string externalId)
         {
         {
-            var name = serviceName + externalId;
+            var name = serviceName + externalId + InternalVersionNumber;
 
 
             return name.ToLower().GetMBId(typeof(LiveTvChannel));
             return name.ToLower().GetMBId(typeof(LiveTvChannel));
         }
         }
 
 
         public Guid GetInternalTimerId(string serviceName, string externalId)
         public Guid GetInternalTimerId(string serviceName, string externalId)
         {
         {
-            var name = serviceName + externalId;
+            var name = serviceName + externalId + InternalVersionNumber;
 
 
             return name.ToLower().GetMD5();
             return name.ToLower().GetMD5();
         }
         }
 
 
         public Guid GetInternalSeriesTimerId(string serviceName, string externalId)
         public Guid GetInternalSeriesTimerId(string serviceName, string externalId)
         {
         {
-            var name = serviceName + externalId;
+            var name = serviceName + externalId + InternalVersionNumber;
 
 
             return name.ToLower().GetMD5();
             return name.ToLower().GetMD5();
         }
         }
 
 
         public Guid GetInternalProgramId(string serviceName, string externalId)
         public Guid GetInternalProgramId(string serviceName, string externalId)
         {
         {
-            var name = serviceName + externalId;
+            var name = serviceName + externalId + InternalVersionNumber;
 
 
-            return name.ToLower().GetMD5();
+            return name.ToLower().GetMBId(typeof(LiveTvProgram));
         }
         }
 
 
         public Guid GetInternalRecordingId(string serviceName, string externalId)
         public Guid GetInternalRecordingId(string serviceName, string externalId)
         {
         {
-            var name = serviceName + externalId;
+            var name = serviceName + externalId + InternalVersionNumber;
 
 
-            return name.ToLower().GetMD5();
+            return name.ToLower().GetMBId(typeof(ILiveTvRecording));
         }
         }
 
 
         public async Task<TimerInfo> GetTimerInfo(TimerInfoDto dto, bool isNew, ILiveTvManager liveTv, CancellationToken cancellationToken)
         public async Task<TimerInfo> GetTimerInfo(TimerInfoDto dto, bool isNew, ILiveTvManager liveTv, CancellationToken cancellationToken)

+ 51 - 30
MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs

@@ -6,8 +6,8 @@ using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.LiveTv;
 using MediaBrowser.Controller.LiveTv;
-using MediaBrowser.Controller.Localization;
 using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Controller.Persistence;
+using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.LiveTv;
 using MediaBrowser.Model.LiveTv;
 using MediaBrowser.Model.Logging;
 using MediaBrowser.Model.Logging;
 using MediaBrowser.Model.Querying;
 using MediaBrowser.Model.Querying;
@@ -31,7 +31,6 @@ namespace MediaBrowser.Server.Implementations.LiveTv
         private readonly IItemRepository _itemRepo;
         private readonly IItemRepository _itemRepo;
         private readonly IUserManager _userManager;
         private readonly IUserManager _userManager;
 
 
-        private readonly ILocalizationManager _localization;
         private readonly LiveTvDtoService _tvDtoService;
         private readonly LiveTvDtoService _tvDtoService;
 
 
         private readonly List<ILiveTvService> _services = new List<ILiveTvService>();
         private readonly List<ILiveTvService> _services = new List<ILiveTvService>();
@@ -39,16 +38,15 @@ namespace MediaBrowser.Server.Implementations.LiveTv
         private Dictionary<Guid, LiveTvChannel> _channels = new Dictionary<Guid, LiveTvChannel>();
         private Dictionary<Guid, LiveTvChannel> _channels = new Dictionary<Guid, LiveTvChannel>();
         private Dictionary<Guid, LiveTvProgram> _programs = new Dictionary<Guid, LiveTvProgram>();
         private Dictionary<Guid, LiveTvProgram> _programs = new Dictionary<Guid, LiveTvProgram>();
 
 
-        public LiveTvManager(IServerApplicationPaths appPaths, IFileSystem fileSystem, ILogger logger, IItemRepository itemRepo, IImageProcessor imageProcessor, ILocalizationManager localization, IUserDataManager userDataManager, IDtoService dtoService, IUserManager userManager)
+        public LiveTvManager(IServerApplicationPaths appPaths, IFileSystem fileSystem, ILogger logger, IItemRepository itemRepo, IImageProcessor imageProcessor, IUserDataManager userDataManager, IDtoService dtoService, IUserManager userManager)
         {
         {
             _appPaths = appPaths;
             _appPaths = appPaths;
             _fileSystem = fileSystem;
             _fileSystem = fileSystem;
             _logger = logger;
             _logger = logger;
             _itemRepo = itemRepo;
             _itemRepo = itemRepo;
-            _localization = localization;
             _userManager = userManager;
             _userManager = userManager;
 
 
-            _tvDtoService = new LiveTvDtoService(dtoService, userDataManager, imageProcessor, logger);
+            _tvDtoService = new LiveTvDtoService(dtoService, userDataManager, imageProcessor, logger, _itemRepo);
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -82,7 +80,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
             if (user != null)
             if (user != null)
             {
             {
                 channels = channels
                 channels = channels
-                    .Where(i => i.IsParentalAllowed(user, _localization))
+                    .Where(i => i.IsParentalAllowed(user))
                     .OrderBy(i =>
                     .OrderBy(i =>
                     {
                     {
                         double number = 0;
                         double number = 0;
@@ -144,7 +142,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
             return obj;
             return obj;
         }
         }
 
 
-        public async Task<LiveTvRecording> GetInternalRecording(string id, CancellationToken cancellationToken)
+        public async Task<ILiveTvRecording> GetInternalRecording(string id, CancellationToken cancellationToken)
         {
         {
             var service = ActiveService;
             var service = ActiveService;
 
 
@@ -255,23 +253,46 @@ namespace MediaBrowser.Server.Implementations.LiveTv
             return item;
             return item;
         }
         }
 
 
-        private async Task<LiveTvRecording> GetRecording(RecordingInfo info, string serviceName, CancellationToken cancellationToken)
+        private async Task<ILiveTvRecording> GetRecording(RecordingInfo info, string serviceName, CancellationToken cancellationToken)
         {
         {
             var isNew = false;
             var isNew = false;
 
 
             var id = _tvDtoService.GetInternalRecordingId(serviceName, info.Id);
             var id = _tvDtoService.GetInternalRecordingId(serviceName, info.Id);
 
 
-            var item = _itemRepo.RetrieveItem(id) as LiveTvRecording;
+            var item = _itemRepo.RetrieveItem(id) as ILiveTvRecording;
 
 
             if (item == null)
             if (item == null)
             {
             {
-                item = new LiveTvRecording
+                if (info.ChannelType == ChannelType.TV)
                 {
                 {
-                    Name = info.Name,
-                    Id = id,
-                    DateCreated = DateTime.UtcNow,
-                    DateModified = DateTime.UtcNow
-                };
+                    item = new LiveTvVideoRecording
+                    {
+                        Name = info.Name,
+                        Id = id,
+                        DateCreated = DateTime.UtcNow,
+                        DateModified = DateTime.UtcNow,
+                        VideoType = VideoType.VideoFile
+                    };
+                }
+                else
+                {
+                    item = new LiveTvAudioRecording
+                    {
+                        Name = info.Name,
+                        Id = id,
+                        DateCreated = DateTime.UtcNow,
+                        DateModified = DateTime.UtcNow
+                    };
+                }
+
+                if (!string.IsNullOrEmpty(info.Path))
+                {
+                    item.Path = info.Path;
+                }
+                else if (!string.IsNullOrEmpty(info.Url))
+                {
+                    item.Path = info.Url;
+                }
 
 
                 isNew = true;
                 isNew = true;
             }
             }
@@ -331,7 +352,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
 
 
             if (user != null)
             if (user != null)
             {
             {
-                programs = programs.Where(i => i.IsParentalAllowed(user, _localization));
+                programs = programs.Where(i => i.IsParentalAllowed(user));
             }
             }
 
 
             var returnArray = programs
             var returnArray = programs
@@ -450,10 +471,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
 
 
             var user = string.IsNullOrEmpty(query.UserId) ? null : _userManager.GetUserById(new Guid(query.UserId));
             var user = string.IsNullOrEmpty(query.UserId) ? null : _userManager.GetUserById(new Guid(query.UserId));
 
 
-            var list = new List<RecordingInfo>();
-
             var recordings = await service.GetRecordingsAsync(cancellationToken).ConfigureAwait(false);
             var recordings = await service.GetRecordingsAsync(cancellationToken).ConfigureAwait(false);
-            list.AddRange(recordings);
 
 
             if (!string.IsNullOrEmpty(query.ChannelId))
             if (!string.IsNullOrEmpty(query.ChannelId))
             {
             {
@@ -461,9 +479,8 @@ namespace MediaBrowser.Server.Implementations.LiveTv
 
 
                 var currentServiceName = service.Name;
                 var currentServiceName = service.Name;
 
 
-                list = list
-                    .Where(i => _tvDtoService.GetInternalChannelId(currentServiceName, i.ChannelId) == guid)
-                    .ToList();
+                recordings = recordings
+                    .Where(i => _tvDtoService.GetInternalChannelId(currentServiceName, i.ChannelId) == guid);
             }
             }
 
 
             if (!string.IsNullOrEmpty(query.Id))
             if (!string.IsNullOrEmpty(query.Id))
@@ -472,27 +489,31 @@ namespace MediaBrowser.Server.Implementations.LiveTv
 
 
                 var currentServiceName = service.Name;
                 var currentServiceName = service.Name;
 
 
-                list = list
-                    .Where(i => _tvDtoService.GetInternalRecordingId(currentServiceName, i.Id) == guid)
-                    .ToList();
+                recordings = recordings
+                    .Where(i => _tvDtoService.GetInternalRecordingId(currentServiceName, i.Id) == guid);
             }
             }
 
 
             if (!string.IsNullOrEmpty(query.GroupId))
             if (!string.IsNullOrEmpty(query.GroupId))
             {
             {
                 var guid = new Guid(query.GroupId);
                 var guid = new Guid(query.GroupId);
 
 
-                list = list.Where(i => GetRecordingGroupIds(i).Contains(guid))
-                    .ToList();
+                recordings = recordings.Where(i => GetRecordingGroupIds(i).Contains(guid));
+            }
+
+            if (query.IsRecording.HasValue)
+            {
+                var val = query.IsRecording.Value;
+                recordings = recordings.Where(i => (i.Status == RecordingStatus.InProgress) == val);
             }
             }
 
 
-            IEnumerable<LiveTvRecording> entities = await GetEntities(list, service.Name, cancellationToken).ConfigureAwait(false);
+            IEnumerable<ILiveTvRecording> entities = await GetEntities(recordings, service.Name, cancellationToken).ConfigureAwait(false);
 
 
             entities = entities.OrderByDescending(i => i.RecordingInfo.StartDate);
             entities = entities.OrderByDescending(i => i.RecordingInfo.StartDate);
 
 
             if (user != null)
             if (user != null)
             {
             {
                 var currentUser = user;
                 var currentUser = user;
-                entities = entities.Where(i => i.IsParentalAllowed(currentUser, _localization));
+                entities = entities.Where(i => i.IsParentalAllowed(currentUser));
             }
             }
 
 
             if (query.StartIndex.HasValue)
             if (query.StartIndex.HasValue)
@@ -520,7 +541,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
             };
             };
         }
         }
 
 
-        private Task<LiveTvRecording[]> GetEntities(IEnumerable<RecordingInfo> recordings, string serviceName, CancellationToken cancellationToken)
+        private Task<ILiveTvRecording[]> GetEntities(IEnumerable<RecordingInfo> recordings, string serviceName, CancellationToken cancellationToken)
         {
         {
             var tasks = recordings.Select(i => GetRecording(i, serviceName, cancellationToken));
             var tasks = recordings.Select(i => GetRecording(i, serviceName, cancellationToken));
 
 

+ 4 - 4
MediaBrowser.Server.Implementations/LiveTv/RecordingImageProvider.cs

@@ -35,7 +35,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
 
 
         public override bool Supports(BaseItem item)
         public override bool Supports(BaseItem item)
         {
         {
-            return item is LiveTvRecording;
+            return item is ILiveTvRecording;
         }
         }
 
 
         protected override bool NeedsRefreshInternal(BaseItem item, BaseProviderInfo providerInfo)
         protected override bool NeedsRefreshInternal(BaseItem item, BaseProviderInfo providerInfo)
@@ -55,7 +55,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
 
 
             try
             try
             {
             {
-                changed = await DownloadImage((LiveTvRecording)item, cancellationToken).ConfigureAwait(false);
+                changed = await DownloadImage((ILiveTvRecording)item, cancellationToken).ConfigureAwait(false);
             }
             }
             catch (HttpException ex)
             catch (HttpException ex)
             {
             {
@@ -74,7 +74,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
             return changed;
             return changed;
         }
         }
 
 
-        private async Task<bool> DownloadImage(LiveTvRecording item, CancellationToken cancellationToken)
+        private async Task<bool> DownloadImage(ILiveTvRecording item, CancellationToken cancellationToken)
         {
         {
             var recordingInfo = item.RecordingInfo;
             var recordingInfo = item.RecordingInfo;
 
 
@@ -133,7 +133,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
                 // Dummy up the original url
                 // Dummy up the original url
                 var url = item.ServiceName + recordingInfo.Id;
                 var url = item.ServiceName + recordingInfo.Id;
 
 
-                await _providerManager.SaveImage(item, imageStream, contentType, ImageType.Primary, null, url, cancellationToken).ConfigureAwait(false);
+                await _providerManager.SaveImage((BaseItem)item, imageStream, contentType, ImageType.Primary, null, url, cancellationToken).ConfigureAwait(false);
                 return true;
                 return true;
             }
             }
 
 

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

@@ -100,7 +100,8 @@
     <Compile Include="Configuration\ServerConfigurationManager.cs" />
     <Compile Include="Configuration\ServerConfigurationManager.cs" />
     <Compile Include="Drawing\ImageHeader.cs" />
     <Compile Include="Drawing\ImageHeader.cs" />
     <Compile Include="Drawing\PercentPlayedDrawer.cs" />
     <Compile Include="Drawing\PercentPlayedDrawer.cs" />
-    <Compile Include="Drawing\WatchedIndicatorDrawer.cs" />
+    <Compile Include="Drawing\PlayedIndicatorDrawer.cs" />
+    <Compile Include="Drawing\UnplayedCountIndicator.cs" />
     <Compile Include="Dto\DtoService.cs" />
     <Compile Include="Dto\DtoService.cs" />
     <Compile Include="EntryPoints\LibraryChangedNotifier.cs" />
     <Compile Include="EntryPoints\LibraryChangedNotifier.cs" />
     <Compile Include="EntryPoints\LoadRegistrations.cs" />
     <Compile Include="EntryPoints\LoadRegistrations.cs" />

+ 1 - 1
MediaBrowser.Server.Implementations/Providers/ImageSaver.cs

@@ -73,7 +73,7 @@ namespace MediaBrowser.Server.Implementations.Providers
                 throw new ArgumentNullException("mimeType");
                 throw new ArgumentNullException("mimeType");
             }
             }
 
 
-            var saveLocally = _config.Configuration.SaveLocalMeta && item.Parent != null && !(item is Audio);
+            var saveLocally = item.IsSaveLocalMetadataEnabled() && item.Parent != null && !(item is Audio);
 
 
             if (item is IItemByName || item is User)
             if (item is IItemByName || item is User)
             {
             {

+ 3 - 2
MediaBrowser.Server.Mono/Networking/NetworkManager.cs

@@ -1,5 +1,6 @@
 using MediaBrowser.Common.Implementations.Networking;
 using MediaBrowser.Common.Implementations.Networking;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Common.Net;
+using MediaBrowser.Model.IO;
 using MediaBrowser.Model.Net;
 using MediaBrowser.Model.Net;
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
@@ -26,9 +27,9 @@ namespace MediaBrowser.ServerApplication.Networking
         /// Gets a list of network devices
         /// Gets a list of network devices
         /// </summary>
         /// </summary>
         /// PC's in the Domain</returns>
         /// PC's in the Domain</returns>
-        public IEnumerable<string> GetNetworkDevices()
+		public IEnumerable<FileSystemEntryInfo> GetNetworkDevices()
         {
         {
-			return new List<string> ();
+			return new List<FileSystemEntryInfo> ();
         }
         }
     }
     }
 }
 }

+ 1 - 1
MediaBrowser.ServerApplication/ApplicationHost.cs

@@ -272,7 +272,7 @@ namespace MediaBrowser.ServerApplication
             DtoService = new DtoService(Logger, LibraryManager, UserManager, UserDataManager, ItemRepository, ImageProcessor);
             DtoService = new DtoService(Logger, LibraryManager, UserManager, UserDataManager, ItemRepository, ImageProcessor);
             RegisterSingleInstance(DtoService);
             RegisterSingleInstance(DtoService);
 
 
-            LiveTvManager = new LiveTvManager(ApplicationPaths, FileSystemManager, Logger, ItemRepository, ImageProcessor, LocalizationManager, UserDataManager, DtoService, UserManager);
+            LiveTvManager = new LiveTvManager(ApplicationPaths, FileSystemManager, Logger, ItemRepository, ImageProcessor, UserDataManager, DtoService, UserManager);
             RegisterSingleInstance(LiveTvManager);
             RegisterSingleInstance(LiveTvManager);
             progress.Report(15);
             progress.Report(15);
 
 

+ 32 - 2
MediaBrowser.ServerApplication/Networking/NetworkManager.cs

@@ -1,5 +1,8 @@
-using MediaBrowser.Common.Implementations.Networking;
+using System.Globalization;
+using System.IO;
+using MediaBrowser.Common.Implementations.Networking;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Common.Net;
+using MediaBrowser.Model.IO;
 using MediaBrowser.Model.Net;
 using MediaBrowser.Model.Net;
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
@@ -79,7 +82,7 @@ namespace MediaBrowser.ServerApplication.Networking
         /// </summary>
         /// </summary>
         /// <returns>Arraylist that represents all the SV_TYPE_WORKSTATION and SV_TYPE_SERVER
         /// <returns>Arraylist that represents all the SV_TYPE_WORKSTATION and SV_TYPE_SERVER
         /// PC's in the Domain</returns>
         /// PC's in the Domain</returns>
-        public IEnumerable<string> GetNetworkDevices()
+        private IEnumerable<string> GetNetworkDevicesInternal()
         {
         {
             //local fields
             //local fields
             const int MAX_PREFERRED_LENGTH = -1;
             const int MAX_PREFERRED_LENGTH = -1;
@@ -131,6 +134,33 @@ namespace MediaBrowser.ServerApplication.Networking
                 NativeMethods.NetApiBufferFree(buffer);
                 NativeMethods.NetApiBufferFree(buffer);
             }
             }
         }
         }
+
+        /// <summary>
+        /// Gets available devices within the domain
+        /// </summary>
+        /// <returns>PC's in the Domain</returns>
+        public IEnumerable<FileSystemEntryInfo> GetNetworkDevices()
+        {
+            return GetNetworkDevicesInternal().Select(c => new FileSystemEntryInfo
+            {
+                Name = c,
+                Path = NetworkPrefix + c,
+                Type = FileSystemEntryType.NetworkComputer
+            });
+        }
+
+        /// <summary>
+        /// Gets the network prefix.
+        /// </summary>
+        /// <value>The network prefix.</value>
+        private string NetworkPrefix
+        {
+            get
+            {
+                var separator = Path.DirectorySeparatorChar.ToString(CultureInfo.InvariantCulture);
+                return separator + separator;
+            }
+        }
     }
     }
 
 
 }
 }

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

@@ -498,6 +498,7 @@ namespace MediaBrowser.WebDashboard.Api
                                       "livetvnewrecording.js",
                                       "livetvnewrecording.js",
                                       "livetvprogram.js",
                                       "livetvprogram.js",
                                       "livetvrecording.js",
                                       "livetvrecording.js",
+                                      "livetvrecordinglist.js",
                                       "livetvrecordings.js",
                                       "livetvrecordings.js",
                                       "livetvtimer.js",
                                       "livetvtimer.js",
                                       "livetvseriestimer.js",
                                       "livetvseriestimer.js",

+ 41 - 0
MediaBrowser.WebDashboard/ApiClient.js

@@ -872,6 +872,47 @@ MediaBrowser.ApiClient = function ($, navigator, JSON, WebSocket, setTimeout, wi
             });
             });
         };
         };
 
 
+        /**
+         * Gets shares from a network device
+         */
+        self.getNetworkShares = function (path) {
+
+            if (!path) {
+                throw new Error("null path");
+            }
+
+            var options = {};
+            options.path = path;
+
+            var url = self.getUrl("Environment/NetworkShares", options);
+
+            return self.ajax({
+                type: "GET",
+                url: url,
+                dataType: "json"
+            });
+        };
+
+        /**
+         * Gets the parent of a given path
+         */
+        self.getParentPath = function (path) {
+
+            if (!path) {
+                throw new Error("null path");
+            }
+
+            var options = {};
+            options.path = path;
+
+            var url = self.getUrl("Environment/ParentPath", options);
+
+            return self.ajax({
+                type: "GET",
+                url: url
+            });
+        };
+
         /**
         /**
          * Gets a list of physical drives from the server
          * Gets a list of physical drives from the server
          */
          */

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

@@ -154,6 +154,9 @@
     <Content Include="dashboard-ui\livetvrecording.html">
     <Content Include="dashboard-ui\livetvrecording.html">
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
     </Content>
     </Content>
+    <Content Include="dashboard-ui\livetvrecordinglist.html">
+      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+    </Content>
     <Content Include="dashboard-ui\livetvseriestimer.html">
     <Content Include="dashboard-ui\livetvseriestimer.html">
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
     </Content>
     </Content>
@@ -424,6 +427,9 @@
     <Content Include="dashboard-ui\scripts\livetvrecording.js">
     <Content Include="dashboard-ui\scripts\livetvrecording.js">
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
     </Content>
     </Content>
+    <Content Include="dashboard-ui\scripts\livetvrecordinglist.js">
+      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+    </Content>
     <Content Include="dashboard-ui\scripts\livetvseriestimer.js">
     <Content Include="dashboard-ui\scripts\livetvseriestimer.js">
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
     </Content>
     </Content>

+ 1 - 1
MediaBrowser.WebDashboard/packages.config

@@ -1,4 +1,4 @@
 <?xml version="1.0" encoding="utf-8"?>
 <?xml version="1.0" encoding="utf-8"?>
 <packages>
 <packages>
-  <package id="MediaBrowser.ApiClient.Javascript" version="3.0.213" targetFramework="net45" />
+  <package id="MediaBrowser.ApiClient.Javascript" version="3.0.216" targetFramework="net45" />
 </packages>
 </packages>