Prechádzať zdrojové kódy

add more methods to file system interface

Luke Pulverenti 11 rokov pred
rodič
commit
b9d17c9bc7
54 zmenil súbory, kde vykonal 704 pridanie a 426 odobranie
  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>