Browse Source

add more methods to file system interface

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

+ 61 - 28
MediaBrowser.Api/EnvironmentService.cs

@@ -46,6 +46,18 @@ namespace MediaBrowser.Api
         public bool IncludeHidden { get; set; }
     }
 
+    [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>
     /// Class GetDrives
     /// </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>
     /// Class EnvironmentService
     /// </summary>
     public class EnvironmentService : BaseApiService
     {
+        const char UncSeparator = '\\';
+
         /// <summary>
         /// The _network manager
         /// </summary>
@@ -105,13 +131,9 @@ namespace MediaBrowser.Api
                 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());
             }
@@ -119,6 +141,15 @@ namespace MediaBrowser.Api
             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>
         /// Gets the specified request.
         /// </summary>
@@ -154,25 +185,13 @@ namespace MediaBrowser.Api
         /// <returns>System.Object.</returns>
         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);
         }
 
-        /// <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>
         /// Gets the name.
         /// </summary>
@@ -223,7 +242,7 @@ namespace MediaBrowser.Api
                 {
                     return false;
                 }
-                
+
                 return true;
             });
 
@@ -236,13 +255,27 @@ namespace MediaBrowser.Api
             }).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")]
         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")]
         public string BackgroundColor { get; set; }

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

@@ -91,6 +91,7 @@ namespace MediaBrowser.Api.Images
                 OutputFormat = Request.Format,
                 AddPlayedIndicator = Request.AddPlayedIndicator,
                 PercentPlayed = Request.PercentPlayed,
+                UnplayedCount = Request.UnplayedCount,
                 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.");
             }
 
-            // 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 virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName);
 
-            ValidateNewMediaPath(fileSystem, rootFolderPath, path, appPaths);
+            ValidateNewMediaPath(fileSystem, rootFolderPath, path);
 
             var shortcutFilename = Path.GetFileNameWithoutExtension(path);
 
@@ -96,25 +89,18 @@ namespace MediaBrowser.Api.Library
         /// <param name="fileSystem">The file system.</param>
         /// <param name="currentViewRootFolderPath">The current view root folder path.</param>
         /// <param name="mediaPath">The media path.</param>
-        /// <param name="appPaths">The app paths.</param>
         /// <exception cref="System.ArgumentException">
         /// </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
             // 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))
             {
@@ -122,9 +108,8 @@ namespace MediaBrowser.Api.Library
             }
             
             // 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))
             {
@@ -135,30 +120,30 @@ namespace MediaBrowser.Api.Library
         /// <summary>
         /// Validates that a new path can be added based on an existing path
         /// </summary>
+        /// <param name="fileSystem">The file system.</param>
         /// <param name="newPath">The new 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>
-        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
             // D:\ cannot be added
             // Neither can D:\Movies\Kids
             // 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;
             }
 
             // 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;
             }
 
             // Validate the D:\ scenario
-            if (existingPath.StartsWith(newPath.TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase))
+            if (fileSystem.ContainsSubPath(newPath, existingPath))
             {
                 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")]
         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")]
@@ -274,7 +277,8 @@ namespace MediaBrowser.Api.LiveTv
                 UserId = request.UserId,
                 GroupId = request.GroupId,
                 StartIndex = request.StartIndex,
-                Limit = request.Limit
+                Limit = request.Limit,
+                IsRecording = request.IsRecording
 
             }, 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);
             }
-            else if (!state.HasMediaStreams)
-            {
-                args += string.Format("-map 0:{0}", 0);
-            }
             else
             {
                 args += "-map -0:v";
@@ -210,10 +206,6 @@ namespace MediaBrowser.Api.Playback
             {
                 args += string.Format(" -map 0:{0}", state.AudioStream.Index);
             }
-            else if (!state.HasMediaStreams)
-            {
-                args += string.Format(" -map 0:{0}", 1);
-            }
 
             else
             {
@@ -871,7 +863,7 @@ namespace MediaBrowser.Api.Playback
                 RequestedUrl = url
             };
 
-            BaseItem item;
+            Guid itemId;
 
             if (string.Equals(request.Type, "Recording", StringComparison.OrdinalIgnoreCase))
             {
@@ -900,7 +892,7 @@ namespace MediaBrowser.Api.Playback
                     state.IsRemote = true;
                 }
 
-                item = recording;
+                itemId = recording.Id;
             }
             else if (string.Equals(request.Type, "Channel", StringComparison.OrdinalIgnoreCase))
             {
@@ -916,11 +908,11 @@ namespace MediaBrowser.Api.Playback
 
                 state.IsRemote = true;
 
-                item = channel;
+                itemId = channel.Id;
             }
             else
             {
-                item = DtoService.GetItemByDtoId(request.Id);
+                var item = DtoService.GetItemByDtoId(request.Id);
 
                 state.MediaPath = item.Path;
                 state.IsRemote = item.LocationType == LocationType.Remote;
@@ -937,13 +929,15 @@ namespace MediaBrowser.Api.Playback
                         ? new List<string>()
                         : video.PlayableStreamFileNames.ToList();
                 }
+
+                itemId = item.Id;
             }
 
             var videoRequest = request as VideoStreamRequest;
 
             var mediaStreams = ItemRepository.GetMediaStreams(new MediaStreamQuery
             {
-                ItemId = item.Id
+                ItemId = itemId
 
             }).ToList();
 

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

@@ -82,6 +82,16 @@ namespace MediaBrowser.Common.Implementations.IO
                 throw new ArgumentNullException("target");
             }
 
+            if (string.IsNullOrEmpty(shortcutPath))
+            {
+                throw new ArgumentNullException("shortcutPath");
+            }
+
+            if (string.IsNullOrEmpty(target))
+            {
+                throw new ArgumentNullException("target");
+            }
+
             File.WriteAllText(shortcutPath, target);
         }
 
@@ -92,6 +102,11 @@ namespace MediaBrowser.Common.Implementations.IO
         /// <returns>FileSystemInfo.</returns>
         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
             if (Path.HasExtension(path))
             {
@@ -172,7 +187,6 @@ namespace MediaBrowser.Common.Implementations.IO
         /// Gets the creation time UTC.
         /// </summary>
         /// <param name="info">The info.</param>
-        /// <param name="logger">The logger.</param>
         /// <returns>DateTime.</returns>
         public DateTime GetLastWriteTimeUtc(FileSystemInfo info)
         {
@@ -224,6 +238,16 @@ namespace MediaBrowser.Common.Implementations.IO
         /// <param name="file2">The file2.</param>
         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 temp2 = Path.GetTempFileName();
 
@@ -247,6 +271,11 @@ namespace MediaBrowser.Common.Implementations.IO
         /// <param name="path">The path.</param>
         private void RemoveHiddenAttribute(string path)
         {
+            if (string.IsNullOrEmpty(path))
+            {
+                throw new ArgumentNullException("path");
+            }
+
             var currentFile = new FileInfo(path);
 
             // 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="file2">The file2.</param>
         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 System.Collections.Generic;
 using System.Net;
@@ -35,7 +36,7 @@ namespace MediaBrowser.Common.Net
         /// Gets available devices within the domain
         /// </summary>
         /// <returns>PC's in the Domain</returns>
-        IEnumerable<string> GetNetworkDevices();
+        IEnumerable<FileSystemEntryInfo> GetNetworkDevices();
 
         /// <summary>
         /// 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 int? PercentPlayed { get; set; }
+        public int? UnplayedCount { get; set; }
 
+        public double? PercentPlayed { get; set; }
+        
         public string BackgroundColor { get; set; }
 
         public bool HasDefaultOptions()
@@ -56,6 +58,7 @@ namespace MediaBrowser.Controller.Drawing
                 IsOutputFormatDefault &&
                 !AddPlayedIndicator &&
                 !PercentPlayed.HasValue &&
+                !UnplayedCount.HasValue &&
                 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 DateLastSaved { get; set; }
-        
+
         /// <summary>
         /// The logger
         /// </summary>
@@ -327,21 +327,18 @@ namespace MediaBrowser.Controller.Entities
                 // When resolving the root, we need it's grandchildren (children of user views)
                 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
                 // Example: if \\server\movies exists, then strip out \\server\movies\action
                 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
@@ -1016,14 +1013,18 @@ namespace MediaBrowser.Controller.Entities
             return lang;
         }
 
+        public virtual bool IsSaveLocalMetadataEnabled()
+        {
+            return ConfigurationManager.Configuration.SaveLocalMeta;
+        }
+
         /// <summary>
         /// Determines if a given user has access to this item
         /// </summary>
         /// <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>
         /// <exception cref="System.ArgumentNullException">user</exception>
-        public bool IsParentalAllowed(User user, ILocalizationManager localizationManager)
+        public bool IsParentalAllowed(User user)
         {
             if (user == null)
             {
@@ -1049,7 +1050,7 @@ namespace MediaBrowser.Controller.Entities
                 return !GetBlockUnratedValue(user.Configuration);
             }
 
-            var value = localizationManager.GetRatingLevel(rating);
+            var value = LocalizationManager.GetRatingLevel(rating);
 
             // Could not determine the integer value
             if (!value.HasValue)
@@ -1084,7 +1085,7 @@ namespace MediaBrowser.Controller.Entities
                 throw new ArgumentNullException("user");
             }
 
-            return IsParentalAllowed(user, LocalizationManager);
+            return IsParentalAllowed(user);
         }
 
         /// <summary>

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

@@ -519,85 +519,84 @@ namespace MediaBrowser.Controller.Entities
                     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>
@@ -646,7 +645,7 @@ namespace MediaBrowser.Controller.Entities
 
         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>

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

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

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

@@ -320,5 +320,12 @@ namespace MediaBrowser.Controller.Library
         /// <param name="items">The items.</param>
         /// <returns>IEnumerable{System.String}.</returns>
         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="cancellationToken">The cancellation token.</param>
         /// <returns>LiveTvRecording.</returns>
-        Task<LiveTvRecording> GetInternalRecording(string id, CancellationToken cancellationToken);
+        Task<ILiveTvRecording> GetInternalRecording(string id, CancellationToken cancellationToken);
 
         /// <summary>
         /// 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.Model.Entities;
-using MediaBrowser.Model.LiveTv;
 
 namespace MediaBrowser.Controller.LiveTv
 {
-    public class LiveTvRecording : BaseItem
+    public class LiveTvVideoRecording : Video, ILiveTvRecording
     {
         /// <summary>
         /// Gets the user data key.
@@ -23,7 +22,7 @@ namespace MediaBrowser.Controller.LiveTv
         {
             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
             {
+                if (!string.IsNullOrEmpty(Path))
+                {
+                    return base.LocationType;
+                }
+
                 return LocationType.Remote;
             }
         }
@@ -39,5 +43,10 @@ namespace MediaBrowser.Controller.LiveTv
         {
             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\IUserDataManager.cs" />
     <Compile Include="Library\UserDataSaveEventArgs.cs" />
+    <Compile Include="LiveTv\ILiveTvRecording.cs" />
+    <Compile Include="LiveTv\LiveTvAudioRecording.cs" />
     <Compile Include="LiveTv\LiveTvChannel.cs" />
     <Compile Include="LiveTv\ChannelInfo.cs" />
     <Compile Include="LiveTv\ILiveTvManager.cs" />
@@ -115,7 +117,7 @@
     <Compile Include="LiveTv\LiveTvException.cs" />
     <Compile Include="LiveTv\StreamResponseInfo.cs" />
     <Compile Include="LiveTv\LiveTvProgram.cs" />
-    <Compile Include="LiveTv\LiveTvRecording.cs" />
+    <Compile Include="LiveTv\LiveTvVideoRecording.cs" />
     <Compile Include="LiveTv\ProgramInfo.cs" />
     <Compile Include="LiveTv\RecordingInfo.cs" />
     <Compile Include="LiveTv\SeriesTimerInfo.cs" />

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

@@ -51,6 +51,18 @@ namespace MediaBrowser.Model.LiveTv
         /// </summary>
         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>
         /// Gets or sets the path.
         /// </summary>

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

@@ -40,6 +40,12 @@
         /// </summary>
         /// <value>The limit.</value>
         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

+ 3 - 2
MediaBrowser.Mono.userprefs

@@ -1,8 +1,9 @@
 <Properties>
   <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>
-      <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>
   </MonoDevelop.Ide.Workbench>
   <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;
             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";
 

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

@@ -85,15 +85,6 @@ namespace MediaBrowser.Providers.Music
             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>
         /// Gets a value indicating whether [refresh on version change].
         /// </summary>

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

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

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

@@ -31,11 +31,6 @@ namespace MediaBrowser.Providers.Music
 
                 artist.ProductionYear = yearFormed;
             }
-            
-            if (data.tags != null && !artist.LockedFields.Contains(MetadataFields.Tags))
-            {
-                AddTags(artist, data.tags);
-            }
 
             string 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;
 
             string imageSize;
@@ -112,16 +102,5 @@ namespace MediaBrowser.Providers.Music
             album.LastFmImageUrl = GetImageUrl(data, out 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;
 
             // 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;
             }

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

@@ -32,7 +32,7 @@ namespace MediaBrowser.Providers.Savers
             var wasMetadataDownloaded = (updateType & ItemUpdateType.MetadataDownload) == ItemUpdateType.MetadataDownload;
 
             // 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)
                 {

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

@@ -32,7 +32,7 @@ namespace MediaBrowser.Providers.Savers
             var wasMetadataDownloaded = (updateType & ItemUpdateType.MetadataDownload) == ItemUpdateType.MetadataDownload;
 
             // 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;
             }

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

@@ -29,7 +29,7 @@ namespace MediaBrowser.Providers.Savers
             var wasMetadataDownloaded = (updateType & ItemUpdateType.MetadataDownload) == ItemUpdateType.MetadataDownload;
 
             // 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;
             }

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

@@ -37,7 +37,7 @@ namespace MediaBrowser.Providers.Savers
             var wasMetadataDownloaded = (updateType & ItemUpdateType.MetadataDownload) == ItemUpdateType.MetadataDownload;
 
             // 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) &&
                     !(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;
 
             // 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;
             }

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

@@ -37,7 +37,7 @@ namespace MediaBrowser.Providers.Savers
             var wasMetadataDownloaded = (updateType & ItemUpdateType.MetadataDownload) == ItemUpdateType.MetadataDownload;
 
             // 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;
             }

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

@@ -39,7 +39,7 @@ namespace MediaBrowser.Providers.Savers
             var wasMetadataDownloaded = (updateType & ItemUpdateType.MetadataDownload) == ItemUpdateType.MetadataDownload;
 
             // 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;
 

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

@@ -30,7 +30,7 @@ namespace MediaBrowser.Providers.Savers
             var wasMetadataDownloaded = (updateType & ItemUpdateType.MetadataDownload) == ItemUpdateType.MetadataDownload;
 
             // 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;
             }

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

@@ -32,7 +32,7 @@ namespace MediaBrowser.Providers.Savers
             var wasMetadataDownloaded = (updateType & ItemUpdateType.MetadataDownload) == ItemUpdateType.MetadataDownload;
 
             // 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;
             }

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

@@ -172,7 +172,7 @@ namespace MediaBrowser.Server.Implementations.Drawing
 
             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
             {
@@ -241,7 +241,9 @@ namespace MediaBrowser.Server.Implementations.Drawing
                                     thumbnailGraph.SmoothingMode = SmoothingMode.HighQuality;
                                     thumbnailGraph.InterpolationMode = InterpolationMode.HighQualityBicubic;
                                     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);
 
@@ -347,28 +349,31 @@ namespace MediaBrowser.Server.Implementations.Drawing
         /// <param name="options">The options.</param>
         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;
             }
 
             try
             {
-                var percentOffset = 0;
-
                 if (options.AddPlayedIndicator)
                 {
                     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)
                 {
                     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)
@@ -465,10 +470,15 @@ namespace MediaBrowser.Server.Implementations.Drawing
             return new Tuple<string, DateTime>(croppedImagePath, _fileSystem.GetLastWriteTimeUtc(croppedImagePath));
         }
 
+        /// <summary>
+        /// Increment this when indicator drawings change
+        /// </summary>
+        private const string IndicatorVersion = "1";
+
         /// <summary>
         /// Gets the cache file path based on a set of parameters
         /// </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;
 
@@ -485,16 +495,31 @@ namespace MediaBrowser.Server.Implementations.Drawing
                 filename += "f=" + format;
             }
 
+            var hasIndicator = false;
+
             if (addPlayedIndicator)
             {
                 filename += "pl=true";
+                hasIndicator = true;
             }
 
             if (percentPlayed.HasValue)
             {
                 filename += "p=" + percentPlayed.Value;
+                hasIndicator = true;
             }
 
+            if (unwatchedCount.HasValue)
+            {
+                filename += "p=" + unwatchedCount.Value;
+                hasIndicator = true;
+            }
+
+            if (hasIndicator)
+            {
+                filename += "iv=" + IndicatorVersion;
+            }
+            
             if (!string.IsNullOrEmpty(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
 {
     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
 {
-    public class WatchedIndicatorDrawer
+    public class PlayedIndicatorDrawer
     {
         private const int IndicatorHeight = 50;
         public const int IndicatorWidth = 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 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.Resolvers;
 using System;
@@ -30,6 +31,13 @@ namespace MediaBrowser.Server.Implementations.Library
 
         }.ToDictionary(i => i, StringComparer.OrdinalIgnoreCase);
 
+        private readonly IFileSystem _fileSystem;
+
+        public CoreResolutionIgnoreRule(IFileSystem fileSystem)
+        {
+            _fileSystem = fileSystem;
+        }
+
         /// <summary>
         /// Shoulds the ignore.
         /// </summary>
@@ -60,23 +68,12 @@ namespace MediaBrowser.Server.Implementations.Library
                     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;
                 }
 
-                // 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;
             }
 

+ 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)
                 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
                 // Example: if \\server\movies exists, then strip out \\server\movies\action
                 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
@@ -525,6 +522,23 @@ namespace MediaBrowser.Server.Implementations.Library
             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>
         /// Determines whether a path should be ignored based on its contents - called after the contents have been read
         /// </summary>

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

@@ -4,11 +4,13 @@ using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.LiveTv;
+using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.LiveTv;
 using MediaBrowser.Model.Logging;
 using System;
 using System.Collections.Generic;
+using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
 
@@ -21,13 +23,15 @@ namespace MediaBrowser.Server.Implementations.LiveTv
 
         private readonly IUserDataManager _userDataManager;
         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;
             _userDataManager = userDataManager;
             _imageProcessor = imageProcessor;
             _logger = logger;
+            _itemRepo = itemRepo;
         }
 
         public TimerInfoDto GetTimerInfoDto(TimerInfo info, ILiveTvService service, LiveTvProgram program, LiveTvChannel channel)
@@ -180,7 +184,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
             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;
 
@@ -216,7 +220,14 @@ namespace MediaBrowser.Server.Implementations.LiveTv
                 IsNews = info.IsNews,
                 IsKids = info.IsKids,
                 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);
@@ -330,7 +341,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
             return dto;
         }
 
-        private Guid? GetImageTag(BaseItem info)
+        private Guid? GetImageTag(IHasImages info)
         {
             var path = info.PrimaryImagePath;
 
@@ -351,39 +362,41 @@ namespace MediaBrowser.Server.Implementations.LiveTv
             return null;
         }
 
+        private const string InternalVersionNumber = "2";
+
         public Guid GetInternalChannelId(string serviceName, string externalId)
         {
-            var name = serviceName + externalId;
+            var name = serviceName + externalId + InternalVersionNumber;
 
             return name.ToLower().GetMBId(typeof(LiveTvChannel));
         }
 
         public Guid GetInternalTimerId(string serviceName, string externalId)
         {
-            var name = serviceName + externalId;
+            var name = serviceName + externalId + InternalVersionNumber;
 
             return name.ToLower().GetMD5();
         }
 
         public Guid GetInternalSeriesTimerId(string serviceName, string externalId)
         {
-            var name = serviceName + externalId;
+            var name = serviceName + externalId + InternalVersionNumber;
 
             return name.ToLower().GetMD5();
         }
 
         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)
         {
-            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)

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

@@ -6,8 +6,8 @@ using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.LiveTv;
-using MediaBrowser.Controller.Localization;
 using MediaBrowser.Controller.Persistence;
+using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.LiveTv;
 using MediaBrowser.Model.Logging;
 using MediaBrowser.Model.Querying;
@@ -31,7 +31,6 @@ namespace MediaBrowser.Server.Implementations.LiveTv
         private readonly IItemRepository _itemRepo;
         private readonly IUserManager _userManager;
 
-        private readonly ILocalizationManager _localization;
         private readonly LiveTvDtoService _tvDtoService;
 
         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, 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;
             _fileSystem = fileSystem;
             _logger = logger;
             _itemRepo = itemRepo;
-            _localization = localization;
             _userManager = userManager;
 
-            _tvDtoService = new LiveTvDtoService(dtoService, userDataManager, imageProcessor, logger);
+            _tvDtoService = new LiveTvDtoService(dtoService, userDataManager, imageProcessor, logger, _itemRepo);
         }
 
         /// <summary>
@@ -82,7 +80,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
             if (user != null)
             {
                 channels = channels
-                    .Where(i => i.IsParentalAllowed(user, _localization))
+                    .Where(i => i.IsParentalAllowed(user))
                     .OrderBy(i =>
                     {
                         double number = 0;
@@ -144,7 +142,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
             return obj;
         }
 
-        public async Task<LiveTvRecording> GetInternalRecording(string id, CancellationToken cancellationToken)
+        public async Task<ILiveTvRecording> GetInternalRecording(string id, CancellationToken cancellationToken)
         {
             var service = ActiveService;
 
@@ -255,23 +253,46 @@ namespace MediaBrowser.Server.Implementations.LiveTv
             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 id = _tvDtoService.GetInternalRecordingId(serviceName, info.Id);
 
-            var item = _itemRepo.RetrieveItem(id) as LiveTvRecording;
+            var item = _itemRepo.RetrieveItem(id) as ILiveTvRecording;
 
             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;
             }
@@ -331,7 +352,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
 
             if (user != null)
             {
-                programs = programs.Where(i => i.IsParentalAllowed(user, _localization));
+                programs = programs.Where(i => i.IsParentalAllowed(user));
             }
 
             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 list = new List<RecordingInfo>();
-
             var recordings = await service.GetRecordingsAsync(cancellationToken).ConfigureAwait(false);
-            list.AddRange(recordings);
 
             if (!string.IsNullOrEmpty(query.ChannelId))
             {
@@ -461,9 +479,8 @@ namespace MediaBrowser.Server.Implementations.LiveTv
 
                 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))
@@ -472,27 +489,31 @@ namespace MediaBrowser.Server.Implementations.LiveTv
 
                 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))
             {
                 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);
 
             if (user != null)
             {
                 var currentUser = user;
-                entities = entities.Where(i => i.IsParentalAllowed(currentUser, _localization));
+                entities = entities.Where(i => i.IsParentalAllowed(currentUser));
             }
 
             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));
 

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

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

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

@@ -100,7 +100,8 @@
     <Compile Include="Configuration\ServerConfigurationManager.cs" />
     <Compile Include="Drawing\ImageHeader.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="EntryPoints\LibraryChangedNotifier.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");
             }
 
-            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)
             {

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

@@ -1,5 +1,6 @@
 using MediaBrowser.Common.Implementations.Networking;
 using MediaBrowser.Common.Net;
+using MediaBrowser.Model.IO;
 using MediaBrowser.Model.Net;
 using System;
 using System.Collections.Generic;
@@ -26,9 +27,9 @@ namespace MediaBrowser.ServerApplication.Networking
         /// Gets a list of network devices
         /// </summary>
         /// 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);
             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);
             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.Model.IO;
 using MediaBrowser.Model.Net;
 using System;
 using System.Collections.Generic;
@@ -79,7 +82,7 @@ namespace MediaBrowser.ServerApplication.Networking
         /// </summary>
         /// <returns>Arraylist that represents all the SV_TYPE_WORKSTATION and SV_TYPE_SERVER
         /// PC's in the Domain</returns>
-        public IEnumerable<string> GetNetworkDevices()
+        private IEnumerable<string> GetNetworkDevicesInternal()
         {
             //local fields
             const int MAX_PREFERRED_LENGTH = -1;
@@ -131,6 +134,33 @@ namespace MediaBrowser.ServerApplication.Networking
                 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",
                                       "livetvprogram.js",
                                       "livetvrecording.js",
+                                      "livetvrecordinglist.js",
                                       "livetvrecordings.js",
                                       "livetvtimer.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
          */

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

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

+ 1 - 1
MediaBrowser.WebDashboard/packages.config

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