Pārlūkot izejas kodu

Merge pull request #1710 from MediaBrowser/dev

Dev
Luke 9 gadi atpakaļ
vecāks
revīzija
ab2476b9e0

+ 78 - 111
MediaBrowser.Api/BaseApiService.cs

@@ -79,7 +79,7 @@ namespace MediaBrowser.Api
                 }
                 }
             }
             }
         }
         }
-        
+
         /// <summary>
         /// <summary>
         /// To the optimized serialized result using cache.
         /// To the optimized serialized result using cache.
         /// </summary>
         /// </summary>
@@ -118,9 +118,6 @@ namespace MediaBrowser.Api
             return ResultFactory.GetStaticFileResult(Request, path);
             return ResultFactory.GetStaticFileResult(Request, path);
         }
         }
 
 
-        private readonly char[] _dashReplaceChars = { '?', '/', '&' };
-        private const char SlugChar = '-';
-
         protected DtoOptions GetDtoOptions(object request)
         protected DtoOptions GetDtoOptions(object request)
         {
         {
             var options = new DtoOptions();
             var options = new DtoOptions();
@@ -154,152 +151,122 @@ namespace MediaBrowser.Api
 
 
         protected MusicArtist GetArtist(string name, ILibraryManager libraryManager)
         protected MusicArtist GetArtist(string name, ILibraryManager libraryManager)
         {
         {
-            return libraryManager.GetArtist(DeSlugArtistName(name, libraryManager));
-        }
-
-        protected Studio GetStudio(string name, ILibraryManager libraryManager)
-        {
-            return libraryManager.GetStudio(DeSlugStudioName(name, libraryManager));
-        }
-
-        protected Genre GetGenre(string name, ILibraryManager libraryManager)
-        {
-            return libraryManager.GetGenre(DeSlugGenreName(name, libraryManager));
-        }
+            if (name.IndexOf(BaseItem.SlugChar) != -1)
+            {
+                var result = libraryManager.GetItemList(new InternalItemsQuery
+                {
+                    SlugName = name,
+                    IncludeItemTypes = new[] { typeof(MusicArtist).Name }
 
 
-        protected MusicGenre GetMusicGenre(string name, ILibraryManager libraryManager)
-        {
-            return libraryManager.GetMusicGenre(DeSlugGenreName(name, libraryManager));
-        }
+                }).OfType<MusicArtist>().FirstOrDefault();
 
 
-        protected GameGenre GetGameGenre(string name, ILibraryManager libraryManager)
-        {
-            return libraryManager.GetGameGenre(DeSlugGameGenreName(name, libraryManager));
-        }
+                if (result != null)
+                {
+                    return result;
+                }
+            }
 
 
-        protected Person GetPerson(string name, ILibraryManager libraryManager)
-        {
-            return libraryManager.GetPerson(DeSlugPersonName(name, libraryManager));
+            return libraryManager.GetArtist(name);
         }
         }
 
 
-        /// <summary>
-        /// Deslugs an artist name by finding the correct entry in the library
-        /// </summary>
-        /// <param name="name"></param>
-        /// <param name="libraryManager"></param>
-        /// <returns></returns>
-        protected string DeSlugArtistName(string name, ILibraryManager libraryManager)
+        protected Studio GetStudio(string name, ILibraryManager libraryManager)
         {
         {
-            if (name.IndexOf(SlugChar) == -1)
-            {
-                return name;
-            }
-
-            var items = libraryManager.GetItemList(new InternalItemsQuery
+            if (name.IndexOf(BaseItem.SlugChar) != -1)
             {
             {
-                IncludeItemTypes = new[] { typeof(Audio).Name, typeof(MusicVideo).Name, typeof(MusicAlbum).Name }
-            });
-
-            return items
-                .OfType<IHasArtist>()
-                .SelectMany(i => i.AllArtists)
-                .DistinctNames()
-                .FirstOrDefault(i =>
+                var result = libraryManager.GetItemList(new InternalItemsQuery
                 {
                 {
-                    i = _dashReplaceChars.Aggregate(i, (current, c) => current.Replace(c, SlugChar));
+                    SlugName = name,
+                    IncludeItemTypes = new[] { typeof(Studio).Name }
 
 
-                    return string.Equals(i, name, StringComparison.OrdinalIgnoreCase);
+                }).OfType<Studio>().FirstOrDefault();
+
+                if (result != null)
+                {
+                    return result;
+                }
+            }
 
 
-                }) ?? name;
+            return libraryManager.GetStudio(name);
         }
         }
 
 
-        /// <summary>
-        /// Deslugs a genre name by finding the correct entry in the library
-        /// </summary>
-        protected string DeSlugGenreName(string name, ILibraryManager libraryManager)
+        protected Genre GetGenre(string name, ILibraryManager libraryManager)
         {
         {
-            if (name.IndexOf(SlugChar) == -1)
+            if (name.IndexOf(BaseItem.SlugChar) != -1)
             {
             {
-                return name;
-            }
-
-            return libraryManager.RootFolder.GetRecursiveChildren()
-                .SelectMany(i => i.Genres)
-                .DistinctNames()
-                .FirstOrDefault(i =>
+                var result = libraryManager.GetItemList(new InternalItemsQuery
                 {
                 {
-                    i = _dashReplaceChars.Aggregate(i, (current, c) => current.Replace(c, SlugChar));
+                    SlugName = name,
+                    IncludeItemTypes = new[] { typeof(Genre).Name }
 
 
-                    return string.Equals(i, name, StringComparison.OrdinalIgnoreCase);
+                }).OfType<Genre>().FirstOrDefault();
+
+                if (result != null)
+                {
+                    return result;
+                }
+            }
 
 
-                }) ?? name;
+            return libraryManager.GetGenre(name);
         }
         }
 
 
-        protected string DeSlugGameGenreName(string name, ILibraryManager libraryManager)
+        protected MusicGenre GetMusicGenre(string name, ILibraryManager libraryManager)
         {
         {
-            if (name.IndexOf(SlugChar) == -1)
+            if (name.IndexOf(BaseItem.SlugChar) != -1)
             {
             {
-                return name;
-            }
+                var result = libraryManager.GetItemList(new InternalItemsQuery
+                {
+                    SlugName = name,
+                    IncludeItemTypes = new[] { typeof(MusicGenre).Name }
 
 
-            var items = libraryManager.GetItemList(new InternalItemsQuery
-            {
-                IncludeItemTypes = new[] { typeof(Game).Name }
-            });
+                }).OfType<MusicGenre>().FirstOrDefault();
 
 
-            return items
-                .SelectMany(i => i.Genres)
-                .DistinctNames()
-                .FirstOrDefault(i =>
+                if (result != null)
                 {
                 {
-                    i = _dashReplaceChars.Aggregate(i, (current, c) => current.Replace(c, SlugChar));
-
-                    return string.Equals(i, name, StringComparison.OrdinalIgnoreCase);
+                    return result;
+                }
+            }
 
 
-                }) ?? name;
+            return libraryManager.GetMusicGenre(name);
         }
         }
 
 
-        /// <summary>
-        /// Deslugs a studio name by finding the correct entry in the library
-        /// </summary>
-        protected string DeSlugStudioName(string name, ILibraryManager libraryManager)
+        protected GameGenre GetGameGenre(string name, ILibraryManager libraryManager)
         {
         {
-            if (name.IndexOf(SlugChar) == -1)
+            if (name.IndexOf(BaseItem.SlugChar) != -1)
             {
             {
-                return name;
-            }
-
-            return libraryManager.RootFolder
-                .GetRecursiveChildren()
-                .SelectMany(i => i.Studios)
-                .DistinctNames()
-                .FirstOrDefault(i =>
+                var result = libraryManager.GetItemList(new InternalItemsQuery
                 {
                 {
-                    i = _dashReplaceChars.Aggregate(i, (current, c) => current.Replace(c, SlugChar));
+                    SlugName = name,
+                    IncludeItemTypes = new[] { typeof(GameGenre).Name }
 
 
-                    return string.Equals(i, name, StringComparison.OrdinalIgnoreCase);
+                }).OfType<GameGenre>().FirstOrDefault();
 
 
-                }) ?? name;
+                if (result != null)
+                {
+                    return result;
+                }
+            }
+
+            return libraryManager.GetGameGenre(name);
         }
         }
 
 
-        /// <summary>
-        /// Deslugs a person name by finding the correct entry in the library
-        /// </summary>
-        protected string DeSlugPersonName(string name, ILibraryManager libraryManager)
+        protected Person GetPerson(string name, ILibraryManager libraryManager)
         {
         {
-            if (name.IndexOf(SlugChar) == -1)
+            if (name.IndexOf(BaseItem.SlugChar) != -1)
             {
             {
-                return name;
-            }
-
-            return libraryManager.GetPeopleNames(new InternalPeopleQuery())
-                .FirstOrDefault(i =>
+                var result = libraryManager.GetItemList(new InternalItemsQuery
                 {
                 {
-                    i = _dashReplaceChars.Aggregate(i, (current, c) => current.Replace(c, SlugChar));
+                    SlugName = name,
+                    IncludeItemTypes = new[] { typeof(Person).Name }
 
 
-                    return string.Equals(i, name, StringComparison.OrdinalIgnoreCase);
+                }).OfType<Person>().FirstOrDefault();
+
+                if (result != null)
+                {
+                    return result;
+                }
+            }
 
 
-                }) ?? name;
+            return libraryManager.GetPerson(name);
         }
         }
 
 
         protected string GetPathValue(int index)
         protected string GetPathValue(int index)

+ 0 - 54
MediaBrowser.Api/Library/LibraryHelpers.cs

@@ -1,54 +0,0 @@
-using MediaBrowser.Controller;
-using System;
-using System.IO;
-using System.Linq;
-using CommonIO;
-
-namespace MediaBrowser.Api.Library
-{
-    /// <summary>
-    /// Class LibraryHelpers
-    /// </summary>
-    public static class LibraryHelpers
-    {
-        /// <summary>
-        /// The shortcut file extension
-        /// </summary>
-        private const string ShortcutFileExtension = ".mblink";
-        /// <summary>
-        /// The shortcut file search
-        /// </summary>
-        private const string ShortcutFileSearch = "*" + ShortcutFileExtension;
-
-        /// <summary>
-        /// Deletes a shortcut from within a virtual folder, within either the default view or a user view
-        /// </summary>
-        /// <param name="fileSystem">The file system.</param>
-        /// <param name="virtualFolderName">Name of the virtual folder.</param>
-        /// <param name="mediaPath">The media path.</param>
-        /// <param name="appPaths">The app paths.</param>
-        /// <exception cref="System.IO.DirectoryNotFoundException">The media folder does not exist</exception>
-        public static void RemoveMediaPath(IFileSystem fileSystem, string virtualFolderName, string mediaPath, IServerApplicationPaths appPaths)
-        {
-            if (string.IsNullOrWhiteSpace(mediaPath))
-            {
-                throw new ArgumentNullException("mediaPath");
-            }
-
-            var rootFolderPath = appPaths.DefaultUserViewsPath;
-            var path = Path.Combine(rootFolderPath, virtualFolderName);
-
-            if (!fileSystem.DirectoryExists(path))
-            {
-                throw new DirectoryNotFoundException(string.Format("The media collection {0} does not exist", virtualFolderName));
-            }
-            
-            var shortcut = Directory.EnumerateFiles(path, ShortcutFileSearch, SearchOption.AllDirectories).FirstOrDefault(f => fileSystem.ResolveShortcut(f).Equals(mediaPath, StringComparison.OrdinalIgnoreCase));
-
-            if (!string.IsNullOrEmpty(shortcut))
-            {
-                fileSystem.DeleteFile(shortcut);
-            }
-        }
-    }
-}

+ 2 - 41
MediaBrowser.Api/Library/LibraryStructureService.cs

@@ -268,46 +268,7 @@ namespace MediaBrowser.Api.Library
         /// <param name="request">The request.</param>
         /// <param name="request">The request.</param>
         public void Delete(RemoveVirtualFolder request)
         public void Delete(RemoveVirtualFolder request)
         {
         {
-            if (string.IsNullOrWhiteSpace(request.Name))
-            {
-                throw new ArgumentNullException("request");
-            }
-
-            var rootFolderPath = _appPaths.DefaultUserViewsPath;
-
-            var path = Path.Combine(rootFolderPath, request.Name);
-
-			if (!_fileSystem.DirectoryExists(path))
-            {
-                throw new DirectoryNotFoundException("The media folder does not exist");
-            }
-
-            _libraryMonitor.Stop();
-
-            try
-            {
-                _fileSystem.DeleteDirectory(path, true);
-            }
-            finally
-            {
-                Task.Run(() =>
-                {
-                    // No need to start if scanning the library because it will handle it
-                    if (request.RefreshLibrary)
-                    {
-                        _libraryManager.ValidateMediaLibrary(new Progress<double>(), CancellationToken.None);
-                    }
-                    else
-                    {
-                        // Need to add a delay here or directory watchers may still pick up the changes
-                        var task = Task.Delay(1000);
-                        // Have to block here to allow exceptions to bubble
-                        Task.WaitAll(task);
-
-                        _libraryMonitor.Start();
-                    }
-                });
-            }
+            _libraryManager.RemoveVirtualFolder(request.Name, request.RefreshLibrary);
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -364,7 +325,7 @@ namespace MediaBrowser.Api.Library
 
 
             try
             try
             {
             {
-                LibraryHelpers.RemoveMediaPath(_fileSystem, request.Name, request.Path, _appPaths);
+                _libraryManager.RemoveMediaPath(request.Name, request.Path);
             }
             }
             finally
             finally
             {
             {

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

@@ -129,7 +129,6 @@
     <Compile Include="ItemUpdateService.cs" />
     <Compile Include="ItemUpdateService.cs" />
     <Compile Include="Library\LibraryService.cs" />
     <Compile Include="Library\LibraryService.cs" />
     <Compile Include="Library\FileOrganizationService.cs" />
     <Compile Include="Library\FileOrganizationService.cs" />
-    <Compile Include="Library\LibraryHelpers.cs" />
     <Compile Include="Library\LibraryStructureService.cs" />
     <Compile Include="Library\LibraryStructureService.cs" />
     <Compile Include="LiveTv\LiveTvService.cs" />
     <Compile Include="LiveTv\LiveTvService.cs" />
     <Compile Include="LocalizationService.cs" />
     <Compile Include="LocalizationService.cs" />

+ 1 - 1
MediaBrowser.Api/VideosService.cs

@@ -175,7 +175,7 @@ namespace MediaBrowser.Api
 
 
             foreach (var item in items.Where(i => i.Id != primaryVersion.Id))
             foreach (var item in items.Where(i => i.Id != primaryVersion.Id))
             {
             {
-                item.PrimaryVersionId = primaryVersion.Id;
+                item.PrimaryVersionId = primaryVersion.Id.ToString("N");
 
 
                 await item.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
                 await item.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
 
 

+ 20 - 0
MediaBrowser.Controller/Entities/BaseItem.cs

@@ -44,6 +44,9 @@ namespace MediaBrowser.Controller.Entities
             ImageInfos = new List<ItemImageInfo>();
             ImageInfos = new List<ItemImageInfo>();
         }
         }
 
 
+        public static readonly char[] SlugReplaceChars = { '?', '/', '&' };
+        public static char SlugChar = '-';
+
         /// <summary>
         /// <summary>
         /// The supported image extensions
         /// The supported image extensions
         /// </summary>
         /// </summary>
@@ -125,6 +128,21 @@ namespace MediaBrowser.Controller.Entities
             }
             }
         }
         }
 
 
+        [IgnoreDataMember]
+        public string SlugName
+        {
+            get
+            {
+                var name = Name;
+                if (string.IsNullOrWhiteSpace(name))
+                {
+                    return string.Empty;
+                }
+
+                return SlugReplaceChars.Aggregate(name, (current, c) => current.Replace(c, SlugChar));
+            }
+        }
+
         public string OriginalTitle { get; set; }
         public string OriginalTitle { get; set; }
 
 
         /// <summary>
         /// <summary>
@@ -728,12 +746,14 @@ namespace MediaBrowser.Controller.Entities
         /// Gets or sets the critic rating.
         /// Gets or sets the critic rating.
         /// </summary>
         /// </summary>
         /// <value>The critic rating.</value>
         /// <value>The critic rating.</value>
+        [IgnoreDataMember]
         public float? CriticRating { get; set; }
         public float? CriticRating { get; set; }
 
 
         /// <summary>
         /// <summary>
         /// Gets or sets the critic rating summary.
         /// Gets or sets the critic rating summary.
         /// </summary>
         /// </summary>
         /// <value>The critic rating summary.</value>
         /// <value>The critic rating summary.</value>
+        [IgnoreDataMember]
         public string CriticRatingSummary { get; set; }
         public string CriticRatingSummary { get; set; }
 
 
         /// <summary>
         /// <summary>

+ 2 - 2
MediaBrowser.Controller/Entities/Folder.cs

@@ -707,8 +707,8 @@ namespace MediaBrowser.Controller.Entities
         {
         {
             return ItemRepository.GetItemIdsList(new InternalItemsQuery
             return ItemRepository.GetItemIdsList(new InternalItemsQuery
             {
             {
-                ParentId = Id
-
+                ParentId = Id,
+                GroupByPresentationUniqueKey = false
             });
             });
         }
         }
 
 

+ 6 - 1
MediaBrowser.Controller/Entities/InternalItemsQuery.cs

@@ -49,6 +49,7 @@ namespace MediaBrowser.Controller.Entities
         public string PresentationUniqueKey { get; set; }
         public string PresentationUniqueKey { get; set; }
         public string Path { get; set; }
         public string Path { get; set; }
         public string Name { get; set; }
         public string Name { get; set; }
+        public string SlugName { get; set; }
 
 
         public string Person { get; set; }
         public string Person { get; set; }
         public string[] PersonIds { get; set; }
         public string[] PersonIds { get; set; }
@@ -133,9 +134,13 @@ namespace MediaBrowser.Controller.Entities
 
 
         public string[] AlbumNames { get; set; }
         public string[] AlbumNames { get; set; }
         public string[] ArtistNames { get; set; }
         public string[] ArtistNames { get; set; }
-        
+
+        public bool GroupByPresentationUniqueKey { get; set; }
+
         public InternalItemsQuery()
         public InternalItemsQuery()
         {
         {
+            GroupByPresentationUniqueKey = true;
+
             AlbumNames = new string[] { };
             AlbumNames = new string[] { };
             ArtistNames = new string[] { };
             ArtistNames = new string[] { };
             
             

+ 2 - 1
MediaBrowser.Controller/Entities/Movies/BoxSet.cs

@@ -8,6 +8,7 @@ using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.Linq;
 using System.Linq;
 using System.Runtime.Serialization;
 using System.Runtime.Serialization;
+using MediaBrowser.Controller.Entities.Audio;
 
 
 namespace MediaBrowser.Controller.Entities.Movies
 namespace MediaBrowser.Controller.Entities.Movies
 {
 {
@@ -118,7 +119,7 @@ namespace MediaBrowser.Controller.Entities.Movies
             // Gather all possible ratings
             // Gather all possible ratings
             var ratings = GetRecursiveChildren()
             var ratings = GetRecursiveChildren()
                 .Concat(GetLinkedChildren())
                 .Concat(GetLinkedChildren())
-                .Where(i => i is Movie || i is Series)
+                .Where(i => i is Movie || i is Series || i is MusicAlbum || i is Game)
                 .Select(i => i.OfficialRating)
                 .Select(i => i.OfficialRating)
                 .Where(i => !string.IsNullOrEmpty(i))
                 .Where(i => !string.IsNullOrEmpty(i))
                 .Distinct(StringComparer.OrdinalIgnoreCase)
                 .Distinct(StringComparer.OrdinalIgnoreCase)

+ 74 - 73
MediaBrowser.Controller/Entities/Video.cs

@@ -28,7 +28,8 @@ namespace MediaBrowser.Controller.Entities
         IThemeMedia,
         IThemeMedia,
         IArchivable
         IArchivable
     {
     {
-        public Guid? PrimaryVersionId { get; set; }
+        [IgnoreDataMember]
+        public string PrimaryVersionId { get; set; }
 
 
         public List<string> AdditionalParts { get; set; }
         public List<string> AdditionalParts { get; set; }
         public List<string> LocalAlternateVersions { get; set; }
         public List<string> LocalAlternateVersions { get; set; }
@@ -49,9 +50,9 @@ namespace MediaBrowser.Controller.Entities
         {
         {
             get
             get
             {
             {
-                if (PrimaryVersionId.HasValue)
+                if (!string.IsNullOrWhiteSpace(PrimaryVersionId))
                 {
                 {
-                    return PrimaryVersionId.Value.ToString("N");
+                    return PrimaryVersionId;
                 }
                 }
 
 
                 return base.PresentationUniqueKey;
                 return base.PresentationUniqueKey;
@@ -70,6 +71,72 @@ namespace MediaBrowser.Controller.Entities
         /// <value>The timestamp.</value>
         /// <value>The timestamp.</value>
         public TransportStreamTimestamp? Timestamp { get; set; }
         public TransportStreamTimestamp? Timestamp { get; set; }
 
 
+        /// <summary>
+        /// Gets or sets the subtitle paths.
+        /// </summary>
+        /// <value>The subtitle paths.</value>
+        public List<string> SubtitleFiles { get; set; }
+
+        /// <summary>
+        /// Gets or sets a value indicating whether this instance has subtitles.
+        /// </summary>
+        /// <value><c>true</c> if this instance has subtitles; otherwise, <c>false</c>.</value>
+        public bool HasSubtitles { get; set; }
+
+        public bool IsPlaceHolder { get; set; }
+        public bool IsShortcut { get; set; }
+        public string ShortcutPath { get; set; }
+
+        /// <summary>
+        /// Gets or sets the video bit rate.
+        /// </summary>
+        /// <value>The video bit rate.</value>
+        public int? VideoBitRate { get; set; }
+
+        /// <summary>
+        /// Gets or sets the default index of the video stream.
+        /// </summary>
+        /// <value>The default index of the video stream.</value>
+        public int? DefaultVideoStreamIndex { get; set; }
+
+        /// <summary>
+        /// Gets or sets the type of the video.
+        /// </summary>
+        /// <value>The type of the video.</value>
+        public VideoType VideoType { get; set; }
+
+        /// <summary>
+        /// Gets or sets the type of the iso.
+        /// </summary>
+        /// <value>The type of the iso.</value>
+        public IsoType? IsoType { get; set; }
+
+        /// <summary>
+        /// Gets or sets the video3 D format.
+        /// </summary>
+        /// <value>The video3 D format.</value>
+        public Video3DFormat? Video3DFormat { get; set; }
+
+        /// <summary>
+        /// If the video is a folder-rip, this will hold the file list for the largest playlist
+        /// </summary>
+        public List<string> PlayableStreamFileNames { get; set; }
+
+        /// <summary>
+        /// Gets the playable stream files.
+        /// </summary>
+        /// <returns>List{System.String}.</returns>
+        public List<string> GetPlayableStreamFiles()
+        {
+            return GetPlayableStreamFiles(Path);
+        }
+
+        /// <summary>
+        /// Gets or sets the aspect ratio.
+        /// </summary>
+        /// <value>The aspect ratio.</value>
+        public string AspectRatio { get; set; }
+
         public Video()
         public Video()
         {
         {
             PlayableStreamFileNames = new List<string>();
             PlayableStreamFileNames = new List<string>();
@@ -104,9 +171,9 @@ namespace MediaBrowser.Controller.Entities
         {
         {
             get
             get
             {
             {
-                if (PrimaryVersionId.HasValue)
+                if (!string.IsNullOrWhiteSpace(PrimaryVersionId))
                 {
                 {
-                    var item = LibraryManager.GetItemById(PrimaryVersionId.Value) as Video;
+                    var item = LibraryManager.GetItemById(PrimaryVersionId) as Video;
                     if (item != null)
                     if (item != null)
                     {
                     {
                         return item.MediaSourceCount;
                         return item.MediaSourceCount;
@@ -238,72 +305,6 @@ namespace MediaBrowser.Controller.Entities
                 .OrderBy(i => i.SortName);
                 .OrderBy(i => i.SortName);
         }
         }
 
 
-        /// <summary>
-        /// Gets or sets the subtitle paths.
-        /// </summary>
-        /// <value>The subtitle paths.</value>
-        public List<string> SubtitleFiles { get; set; }
-
-        /// <summary>
-        /// Gets or sets a value indicating whether this instance has subtitles.
-        /// </summary>
-        /// <value><c>true</c> if this instance has subtitles; otherwise, <c>false</c>.</value>
-        public bool HasSubtitles { get; set; }
-
-        public bool IsPlaceHolder { get; set; }
-        public bool IsShortcut { get; set; }
-        public string ShortcutPath { get; set; }
-
-        /// <summary>
-        /// Gets or sets the video bit rate.
-        /// </summary>
-        /// <value>The video bit rate.</value>
-        public int? VideoBitRate { get; set; }
-
-        /// <summary>
-        /// Gets or sets the default index of the video stream.
-        /// </summary>
-        /// <value>The default index of the video stream.</value>
-        public int? DefaultVideoStreamIndex { get; set; }
-
-        /// <summary>
-        /// Gets or sets the type of the video.
-        /// </summary>
-        /// <value>The type of the video.</value>
-        public VideoType VideoType { get; set; }
-
-        /// <summary>
-        /// Gets or sets the type of the iso.
-        /// </summary>
-        /// <value>The type of the iso.</value>
-        public IsoType? IsoType { get; set; }
-
-        /// <summary>
-        /// Gets or sets the video3 D format.
-        /// </summary>
-        /// <value>The video3 D format.</value>
-        public Video3DFormat? Video3DFormat { get; set; }
-
-        /// <summary>
-        /// If the video is a folder-rip, this will hold the file list for the largest playlist
-        /// </summary>
-        public List<string> PlayableStreamFileNames { get; set; }
-
-        /// <summary>
-        /// Gets the playable stream files.
-        /// </summary>
-        /// <returns>List{System.String}.</returns>
-        public List<string> GetPlayableStreamFiles()
-        {
-            return GetPlayableStreamFiles(Path);
-        }
-
-        /// <summary>
-        /// Gets or sets the aspect ratio.
-        /// </summary>
-        /// <value>The aspect ratio.</value>
-        public string AspectRatio { get; set; }
-
         [IgnoreDataMember]
         [IgnoreDataMember]
         public override string ContainingFolderPath
         public override string ContainingFolderPath
         {
         {
@@ -520,9 +521,9 @@ namespace MediaBrowser.Controller.Entities
             list.Add(new Tuple<Video, MediaSourceType>(this, MediaSourceType.Default));
             list.Add(new Tuple<Video, MediaSourceType>(this, MediaSourceType.Default));
             list.AddRange(GetLinkedAlternateVersions().Select(i => new Tuple<Video, MediaSourceType>(i, MediaSourceType.Grouping)));
             list.AddRange(GetLinkedAlternateVersions().Select(i => new Tuple<Video, MediaSourceType>(i, MediaSourceType.Grouping)));
 
 
-            if (PrimaryVersionId.HasValue)
+            if (!string.IsNullOrWhiteSpace(PrimaryVersionId))
             {
             {
-                var primary = LibraryManager.GetItemById(PrimaryVersionId.Value) as Video;
+                var primary = LibraryManager.GetItemById(PrimaryVersionId) as Video;
                 if (primary != null)
                 if (primary != null)
                 {
                 {
                     var existingIds = list.Select(i => i.Item1.Id).ToList();
                     var existingIds = list.Select(i => i.Item1.Id).ToList();

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

@@ -571,6 +571,8 @@ namespace MediaBrowser.Controller.Library
         bool IgnoreFile(FileSystemMetadata file, BaseItem parent);
         bool IgnoreFile(FileSystemMetadata file, BaseItem parent);
 
 
         void AddVirtualFolder(string name, string collectionType, string[] mediaPaths, bool refreshLibrary);
         void AddVirtualFolder(string name, string collectionType, string[] mediaPaths, bool refreshLibrary);
+        void RemoveVirtualFolder(string name, bool refreshLibrary);
         void AddMediaPath(string virtualFolderName, string path);
         void AddMediaPath(string virtualFolderName, string path);
+        void RemoveMediaPath(string virtualFolderName, string path);
     }
     }
 }
 }

+ 4 - 0
MediaBrowser.Model/LiveTv/LiveTvOptions.cs

@@ -7,8 +7,11 @@ namespace MediaBrowser.Model.LiveTv
         public int? GuideDays { get; set; }
         public int? GuideDays { get; set; }
         public bool EnableMovieProviders { get; set; }
         public bool EnableMovieProviders { get; set; }
         public string RecordingPath { get; set; }
         public string RecordingPath { get; set; }
+        public string MovieRecordingPath { get; set; }
+        public string SeriesRecordingPath { get; set; }
         public bool EnableAutoOrganize { get; set; }
         public bool EnableAutoOrganize { get; set; }
         public bool EnableRecordingEncoding { get; set; }
         public bool EnableRecordingEncoding { get; set; }
+        public bool EnableRecordingSubfolders { get; set; }
         public bool EnableOriginalAudioWithEncodedRecordings { get; set; }
         public bool EnableOriginalAudioWithEncodedRecordings { get; set; }
 
 
         public List<TunerHostInfo> TunerHosts { get; set; }
         public List<TunerHostInfo> TunerHosts { get; set; }
@@ -20,6 +23,7 @@ namespace MediaBrowser.Model.LiveTv
         public LiveTvOptions()
         public LiveTvOptions()
         {
         {
             EnableMovieProviders = true;
             EnableMovieProviders = true;
+            EnableRecordingSubfolders = true;
             TunerHosts = new List<TunerHostInfo>();
             TunerHosts = new List<TunerHostInfo>();
             ListingProviders = new List<ListingsProviderInfo>();
             ListingProviders = new List<ListingsProviderInfo>();
         }
         }

+ 0 - 45
MediaBrowser.Server.Implementations/Dto/DtoService.cs

@@ -397,12 +397,6 @@ namespace MediaBrowser.Server.Implementations.Dto
                     collectionFolder.GetViewType(user);
                     collectionFolder.GetViewType(user);
             }
             }
 
 
-            var playlist = item as Playlist;
-            if (playlist != null)
-            {
-                AttachLinkedChildImages(dto, playlist, user, options);
-            }
-
             if (fields.Contains(ItemFields.CanDelete))
             if (fields.Contains(ItemFields.CanDelete))
             {
             {
                 dto.CanDelete = user == null
                 dto.CanDelete = user == null
@@ -1564,45 +1558,6 @@ namespace MediaBrowser.Server.Implementations.Dto
             }
             }
         }
         }
 
 
-        private void AttachLinkedChildImages(BaseItemDto dto, Folder folder, User user, DtoOptions options)
-        {
-            List<BaseItem> linkedChildren = null;
-
-            var backdropLimit = options.GetImageLimit(ImageType.Backdrop);
-
-            if (backdropLimit > 0 && dto.BackdropImageTags.Count == 0)
-            {
-                linkedChildren = user == null
-                    ? folder.GetRecursiveChildren().ToList()
-                    : folder.GetRecursiveChildren(user).ToList();
-
-                var parentWithBackdrop = linkedChildren.FirstOrDefault(i => i.GetImages(ImageType.Backdrop).Any());
-
-                if (parentWithBackdrop != null)
-                {
-                    dto.ParentBackdropItemId = GetDtoId(parentWithBackdrop);
-                    dto.ParentBackdropImageTags = GetBackdropImageTags(parentWithBackdrop, backdropLimit);
-                }
-            }
-
-            if (!dto.ImageTags.ContainsKey(ImageType.Primary) && options.GetImageLimit(ImageType.Primary) > 0)
-            {
-                if (linkedChildren == null)
-                {
-                    linkedChildren = user == null
-                        ? folder.GetRecursiveChildren().ToList()
-                        : folder.GetRecursiveChildren(user).ToList();
-                }
-                var parentWithImage = linkedChildren.FirstOrDefault(i => i.GetImages(ImageType.Primary).Any());
-
-                if (parentWithImage != null)
-                {
-                    dto.ParentPrimaryImageItemId = GetDtoId(parentWithImage);
-                    dto.ParentPrimaryImageTag = GetImageCacheTag(parentWithImage, ImageType.Primary);
-                }
-            }
-        }
-
         private string GetMappedPath(IHasMetadata item)
         private string GetMappedPath(IHasMetadata item)
         {
         {
             var path = item.Path;
             var path = item.Path;

+ 68 - 0
MediaBrowser.Server.Implementations/Library/LibraryManager.cs

@@ -2640,7 +2640,52 @@ namespace MediaBrowser.Server.Implementations.Library
             }
             }
         }
         }
 
 
+        public void RemoveVirtualFolder(string name, bool refreshLibrary)
+        {
+            if (string.IsNullOrWhiteSpace(name))
+            {
+                throw new ArgumentNullException("name");
+            }
+
+            var rootFolderPath = ConfigurationManager.ApplicationPaths.DefaultUserViewsPath;
+
+            var path = Path.Combine(rootFolderPath, name);
+
+            if (!_fileSystem.DirectoryExists(path))
+            {
+                throw new DirectoryNotFoundException("The media folder does not exist");
+            }
+
+            _libraryMonitorFactory().Stop();
+
+            try
+            {
+                _fileSystem.DeleteDirectory(path, true);
+            }
+            finally
+            {
+                Task.Run(() =>
+                {
+                    // No need to start if scanning the library because it will handle it
+                    if (refreshLibrary)
+                    {
+                        ValidateMediaLibrary(new Progress<double>(), CancellationToken.None);
+                    }
+                    else
+                    {
+                        // Need to add a delay here or directory watchers may still pick up the changes
+                        var task = Task.Delay(1000);
+                        // Have to block here to allow exceptions to bubble
+                        Task.WaitAll(task);
+
+                        _libraryMonitorFactory().Start();
+                    }
+                });
+            }
+        }
+
         private const string ShortcutFileExtension = ".mblink";
         private const string ShortcutFileExtension = ".mblink";
+        private const string ShortcutFileSearch = "*" + ShortcutFileExtension;
         public void AddMediaPath(string virtualFolderName, string path)
         public void AddMediaPath(string virtualFolderName, string path)
         {
         {
             if (string.IsNullOrWhiteSpace(path))
             if (string.IsNullOrWhiteSpace(path))
@@ -2668,5 +2713,28 @@ namespace MediaBrowser.Server.Implementations.Library
 
 
             _fileSystem.CreateShortcut(lnk, path);
             _fileSystem.CreateShortcut(lnk, path);
         }
         }
+
+        public void RemoveMediaPath(string virtualFolderName, string mediaPath)
+        {
+            if (string.IsNullOrWhiteSpace(mediaPath))
+            {
+                throw new ArgumentNullException("mediaPath");
+            }
+
+            var rootFolderPath = ConfigurationManager.ApplicationPaths.DefaultUserViewsPath;
+            var path = Path.Combine(rootFolderPath, virtualFolderName);
+
+            if (!_fileSystem.DirectoryExists(path))
+            {
+                throw new DirectoryNotFoundException(string.Format("The media collection {0} does not exist", virtualFolderName));
+            }
+
+            var shortcut = Directory.EnumerateFiles(path, ShortcutFileSearch, SearchOption.AllDirectories).FirstOrDefault(f => _fileSystem.ResolveShortcut(f).Equals(mediaPath, StringComparison.OrdinalIgnoreCase));
+
+            if (!string.IsNullOrEmpty(shortcut))
+            {
+                _fileSystem.DeleteFile(shortcut);
+            }
+        }
     }
     }
 }
 }

+ 8 - 36
MediaBrowser.Server.Implementations/Library/Validators/StudiosValidator.cs

@@ -2,7 +2,6 @@
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Model.Logging;
 using MediaBrowser.Model.Logging;
 using System;
 using System;
-using System.Collections.Generic;
 using System.Linq;
 using System.Linq;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
@@ -35,25 +34,20 @@ namespace MediaBrowser.Server.Implementations.Library.Validators
         /// <returns>Task.</returns>
         /// <returns>Task.</returns>
         public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
         public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
         {
         {
-            var items = _libraryManager.RootFolder.GetRecursiveChildren()
-                .SelectMany(i => i.Studios)
-                .DistinctNames()
-                .ToList();
+            var items = _libraryManager.GetItemList(new InternalItemsQuery
+            {
+                IncludeItemTypes = new[] { typeof(Studio).Name }
+
+            }).ToList();
 
 
             var numComplete = 0;
             var numComplete = 0;
             var count = items.Count;
             var count = items.Count;
 
 
-            var validIds = new List<Guid>();
-
-            foreach (var name in items)
+            foreach (var item in items)
             {
             {
                 try
                 try
                 {
                 {
-                    var itemByName = _libraryManager.GetStudio(name);
-
-                    validIds.Add(itemByName.Id);
-
-                    await itemByName.RefreshMetadata(cancellationToken).ConfigureAwait(false);
+                    await item.RefreshMetadata(cancellationToken).ConfigureAwait(false);
                 }
                 }
                 catch (OperationCanceledException)
                 catch (OperationCanceledException)
                 {
                 {
@@ -62,7 +56,7 @@ namespace MediaBrowser.Server.Implementations.Library.Validators
                 }
                 }
                 catch (Exception ex)
                 catch (Exception ex)
                 {
                 {
-                    _logger.ErrorException("Error refreshing {0}", ex, name);
+                    _logger.ErrorException("Error refreshing {0}", ex, item.Name);
                 }
                 }
 
 
                 numComplete++;
                 numComplete++;
@@ -73,28 +67,6 @@ namespace MediaBrowser.Server.Implementations.Library.Validators
                 progress.Report(percent);
                 progress.Report(percent);
             }
             }
 
 
-            var allIds = _libraryManager.GetItemIds(new InternalItemsQuery
-            {
-                IncludeItemTypes = new[] { typeof(Studio).Name }
-            });
-
-            var invalidIds = allIds
-                .Except(validIds)
-                .ToList();
-
-            foreach (var id in invalidIds)
-            {
-                cancellationToken.ThrowIfCancellationRequested();
-                
-                var item = _libraryManager.GetItemById(id);
-
-                await _libraryManager.DeleteItem(item, new DeleteOptions
-                {
-                    DeleteFileLocation = false
-
-                }).ConfigureAwait(false);
-            }
-
             progress.Report(100);
             progress.Report(100);
         }
         }
     }
     }

+ 327 - 185
MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs

@@ -26,7 +26,10 @@ using System.Linq;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 using CommonIO;
 using CommonIO;
+using MediaBrowser.Common.Events;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Extensions;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Power;
 using MediaBrowser.Controller.Power;
 using Microsoft.Win32;
 using Microsoft.Win32;
 
 
@@ -40,7 +43,6 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
         private readonly IServerConfigurationManager _config;
         private readonly IServerConfigurationManager _config;
         private readonly IJsonSerializer _jsonSerializer;
         private readonly IJsonSerializer _jsonSerializer;
 
 
-        private readonly ItemDataProvider<RecordingInfo> _recordingProvider;
         private readonly ItemDataProvider<SeriesTimerInfo> _seriesTimerProvider;
         private readonly ItemDataProvider<SeriesTimerInfo> _seriesTimerProvider;
         private readonly TimerManager _timerProvider;
         private readonly TimerManager _timerProvider;
 
 
@@ -56,6 +58,12 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
 
 
         public static EmbyTV Current;
         public static EmbyTV Current;
 
 
+        public event EventHandler DataSourceChanged;
+        public event EventHandler<RecordingStatusChangedEventArgs> RecordingStatusChanged;
+
+        private readonly ConcurrentDictionary<string, ActiveRecordingInfo> _activeRecordings =
+            new ConcurrentDictionary<string, ActiveRecordingInfo>(StringComparer.OrdinalIgnoreCase);
+
         public EmbyTV(IApplicationHost appHost, ILogger logger, IJsonSerializer jsonSerializer, IHttpClient httpClient, IServerConfigurationManager config, ILiveTvManager liveTvManager, IFileSystem fileSystem, ISecurityManager security, ILibraryManager libraryManager, ILibraryMonitor libraryMonitor, IProviderManager providerManager, IFileOrganizationService organizationService, IMediaEncoder mediaEncoder, IPowerManagement powerManagement)
         public EmbyTV(IApplicationHost appHost, ILogger logger, IJsonSerializer jsonSerializer, IHttpClient httpClient, IServerConfigurationManager config, ILiveTvManager liveTvManager, IFileSystem fileSystem, ISecurityManager security, ILibraryManager libraryManager, ILibraryMonitor libraryMonitor, IProviderManager providerManager, IFileOrganizationService organizationService, IMediaEncoder mediaEncoder, IPowerManagement powerManagement)
         {
         {
             Current = this;
             Current = this;
@@ -74,10 +82,19 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
             _liveTvManager = (LiveTvManager)liveTvManager;
             _liveTvManager = (LiveTvManager)liveTvManager;
             _jsonSerializer = jsonSerializer;
             _jsonSerializer = jsonSerializer;
 
 
-            _recordingProvider = new ItemDataProvider<RecordingInfo>(fileSystem, jsonSerializer, _logger, Path.Combine(DataPath, "recordings"), (r1, r2) => string.Equals(r1.Id, r2.Id, StringComparison.OrdinalIgnoreCase));
             _seriesTimerProvider = new SeriesTimerManager(fileSystem, jsonSerializer, _logger, Path.Combine(DataPath, "seriestimers"));
             _seriesTimerProvider = new SeriesTimerManager(fileSystem, jsonSerializer, _logger, Path.Combine(DataPath, "seriestimers"));
             _timerProvider = new TimerManager(fileSystem, jsonSerializer, _logger, Path.Combine(DataPath, "timers"), powerManagement, _logger);
             _timerProvider = new TimerManager(fileSystem, jsonSerializer, _logger, Path.Combine(DataPath, "timers"), powerManagement, _logger);
             _timerProvider.TimerFired += _timerProvider_TimerFired;
             _timerProvider.TimerFired += _timerProvider_TimerFired;
+
+            _config.NamedConfigurationUpdated += _config_NamedConfigurationUpdated;
+        }
+
+        private void _config_NamedConfigurationUpdated(object sender, ConfigurationUpdateEventArgs e)
+        {
+            if (string.Equals(e.Key, "livetv", StringComparison.OrdinalIgnoreCase))
+            {
+                OnRecordingFoldersChanged();
+            }
         }
         }
 
 
         public void Start()
         public void Start()
@@ -85,6 +102,95 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
             _timerProvider.RestartTimers();
             _timerProvider.RestartTimers();
 
 
             SystemEvents.PowerModeChanged += SystemEvents_PowerModeChanged;
             SystemEvents.PowerModeChanged += SystemEvents_PowerModeChanged;
+
+            CreateRecordingFolders();
+        }
+
+        private void OnRecordingFoldersChanged()
+        {
+            CreateRecordingFolders();
+        }
+
+        private void CreateRecordingFolders()
+        {
+            var recordingFolders = GetRecordingFolders();
+
+            var defaultRecordingPath = DefaultRecordingPath;
+            if (!recordingFolders.Any(i => i.Locations.Contains(defaultRecordingPath, StringComparer.OrdinalIgnoreCase)))
+            {
+                RemovePathFromLibrary(defaultRecordingPath);
+            }
+
+            var virtualFolders = _libraryManager.GetVirtualFolders()
+                .ToList();
+
+            var allExistingPaths = virtualFolders.SelectMany(i => i.Locations).ToList();
+
+            foreach (var recordingFolder in recordingFolders)
+            {
+                var pathsToCreate = recordingFolder.Locations
+                    .Where(i => !allExistingPaths.Contains(i, StringComparer.OrdinalIgnoreCase))
+                    .ToList();
+
+                if (pathsToCreate.Count == 0)
+                {
+                    continue;
+                }
+
+                try
+                {
+                    _libraryManager.AddVirtualFolder(recordingFolder.Name, recordingFolder.CollectionType, pathsToCreate.ToArray(), true);
+                }
+                catch (Exception ex)
+                {
+                    _logger.ErrorException("Error creating virtual folder", ex);
+                }
+            }
+        }
+
+        private void RemovePathFromLibrary(string path)
+        {
+            var requiresRefresh = false;
+            var virtualFolders = _libraryManager.GetVirtualFolders()
+               .ToList();
+
+            foreach (var virtualFolder in virtualFolders)
+            {
+                if (!virtualFolder.Locations.Contains(path, StringComparer.OrdinalIgnoreCase))
+                {
+                    continue;
+                }
+
+                if (virtualFolder.Locations.Count == 1)
+                {
+                    // remove entire virtual folder
+                    try
+                    {
+                        _libraryManager.RemoveVirtualFolder(virtualFolder.Name, true);
+                    }
+                    catch (Exception ex)
+                    {
+                        _logger.ErrorException("Error removing virtual folder", ex);
+                    }
+                }
+                else
+                {
+                    try
+                    {
+                        _libraryManager.RemoveMediaPath(virtualFolder.Name, path);
+                        requiresRefresh = true;
+                    }
+                    catch (Exception ex)
+                    {
+                        _logger.ErrorException("Error removing media path", ex);
+                    }
+                }
+            }
+
+            if (requiresRefresh)
+            {
+                _libraryManager.ValidateMediaLibrary(new Progress<Double>(), CancellationToken.None);
+            }
         }
         }
 
 
         void SystemEvents_PowerModeChanged(object sender, PowerModeChangedEventArgs e)
         void SystemEvents_PowerModeChanged(object sender, PowerModeChangedEventArgs e)
@@ -97,13 +203,6 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
             }
             }
         }
         }
 
 
-        public event EventHandler DataSourceChanged;
-
-        public event EventHandler<RecordingStatusChangedEventArgs> RecordingStatusChanged;
-
-        private readonly ConcurrentDictionary<string, ActiveRecordingInfo> _activeRecordings =
-            new ConcurrentDictionary<string, ActiveRecordingInfo>(StringComparer.OrdinalIgnoreCase);
-
         public string Name
         public string Name
         {
         {
             get { return "Emby"; }
             get { return "Emby"; }
@@ -114,6 +213,26 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
             get { return Path.Combine(_config.CommonApplicationPaths.DataPath, "livetv"); }
             get { return Path.Combine(_config.CommonApplicationPaths.DataPath, "livetv"); }
         }
         }
 
 
+        private string DefaultRecordingPath
+        {
+            get
+            {
+                return Path.Combine(DataPath, "recordings");
+            }
+        }
+
+        private string RecordingPath
+        {
+            get
+            {
+                var path = GetConfiguration().RecordingPath;
+
+                return string.IsNullOrWhiteSpace(path)
+                    ? DefaultRecordingPath
+                    : path;
+            }
+        }
+
         public string HomePageUrl
         public string HomePageUrl
         {
         {
             get { return "http://emby.media"; }
             get { return "http://emby.media"; }
@@ -280,49 +399,9 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
             return Task.FromResult(true);
             return Task.FromResult(true);
         }
         }
 
 
-        public async Task DeleteRecordingAsync(string recordingId, CancellationToken cancellationToken)
+        public Task DeleteRecordingAsync(string recordingId, CancellationToken cancellationToken)
         {
         {
-            var remove = _recordingProvider.GetAll().FirstOrDefault(i => string.Equals(i.Id, recordingId, StringComparison.OrdinalIgnoreCase));
-            if (remove != null)
-            {
-                if (!string.IsNullOrWhiteSpace(remove.TimerId))
-                {
-                    var enableDelay = _activeRecordings.ContainsKey(remove.TimerId);
-
-                    CancelTimerInternal(remove.TimerId);
-
-                    if (enableDelay)
-                    {
-                        // A hack yes, but need to make sure the file is closed before attempting to delete it
-                        await Task.Delay(3000, cancellationToken).ConfigureAwait(false);
-                    }
-                }
-
-                if (!string.IsNullOrWhiteSpace(remove.Path))
-                {
-                    try
-                    {
-                        _fileSystem.DeleteFile(remove.Path);
-                    }
-                    catch (DirectoryNotFoundException)
-                    {
-
-                    }
-                    catch (FileNotFoundException)
-                    {
-
-                    }
-                    catch (Exception ex)
-                    {
-                        _logger.ErrorException("Error deleting recording file {0}", ex, remove.Path);
-                    }
-                }
-                _recordingProvider.Delete(remove);
-            }
-            else
-            {
-                throw new ResourceNotFoundException("Recording not found: " + recordingId);
-            }
+            return Task.FromResult(true);
         }
         }
 
 
         public Task CreateTimerAsync(TimerInfo info, CancellationToken cancellationToken)
         public Task CreateTimerAsync(TimerInfo info, CancellationToken cancellationToken)
@@ -424,29 +503,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
 
 
         public async Task<IEnumerable<RecordingInfo>> GetRecordingsAsync(CancellationToken cancellationToken)
         public async Task<IEnumerable<RecordingInfo>> GetRecordingsAsync(CancellationToken cancellationToken)
         {
         {
-            var recordings = _recordingProvider.GetAll().ToList();
-            var updated = false;
-
-            foreach (var recording in recordings)
-            {
-                if (recording.Status == RecordingStatus.InProgress)
-                {
-                    if (string.IsNullOrWhiteSpace(recording.TimerId) || !_activeRecordings.ContainsKey(recording.TimerId))
-                    {
-                        recording.Status = RecordingStatus.Cancelled;
-                        recording.DateLastUpdated = DateTime.UtcNow;
-                        _recordingProvider.Update(recording);
-                        updated = true;
-                    }
-                }
-            }
-
-            if (updated)
-            {
-                recordings = _recordingProvider.GetAll().ToList();
-            }
-
-            return recordings;
+            return new List<RecordingInfo>();
         }
         }
 
 
         public Task<IEnumerable<TimerInfo>> GetTimersAsync(CancellationToken cancellationToken)
         public Task<IEnumerable<TimerInfo>> GetTimersAsync(CancellationToken cancellationToken)
@@ -695,104 +752,124 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
             }
             }
         }
         }
 
 
-        private async Task RecordStream(TimerInfo timer, DateTime recordingEndDate, ActiveRecordingInfo activeRecordingInfo, CancellationToken cancellationToken)
+        private string GetRecordingPath(TimerInfo timer, ProgramInfo info)
         {
         {
-            if (timer == null)
-            {
-                throw new ArgumentNullException("timer");
-            }
-
-            ProgramInfo info = null;
-
-            if (string.IsNullOrWhiteSpace(timer.ProgramId))
-            {
-                _logger.Info("Timer {0} has null programId", timer.Id);
-            }
-            else
-            {
-                info = GetProgramInfoFromCache(timer.ChannelId, timer.ProgramId);
-            }
-
-            if (info == null)
-            {
-                _logger.Info("Unable to find program with Id {0}. Will search using start date", timer.ProgramId);
-                info = GetProgramInfoFromCache(timer.ChannelId, timer.StartDate);
-            }
-
-            if (info == null)
-            {
-                throw new InvalidOperationException(string.Format("Program with Id {0} not found", timer.ProgramId));
-            }
-
             var recordPath = RecordingPath;
             var recordPath = RecordingPath;
+            var config = GetConfiguration();
 
 
             if (info.IsMovie)
             if (info.IsMovie)
             {
             {
-                recordPath = Path.Combine(recordPath, "Movies", _fileSystem.GetValidFilename(info.Name).Trim());
+                var customRecordingPath = config.MovieRecordingPath;
+                if ((string.IsNullOrWhiteSpace(customRecordingPath) || string.Equals(customRecordingPath, recordPath, StringComparison.OrdinalIgnoreCase)) && config.EnableRecordingSubfolders)
+                {
+                    recordPath = Path.Combine(recordPath, "Movies");
+                }
+
+                var folderName = _fileSystem.GetValidFilename(info.Name).Trim();
+                if (info.ProductionYear.HasValue)
+                {
+                    folderName += " (" + info.ProductionYear.Value.ToString(CultureInfo.InvariantCulture) + ")";
+                }
+                recordPath = Path.Combine(recordPath, folderName);
             }
             }
             else if (info.IsSeries)
             else if (info.IsSeries)
             {
             {
-                recordPath = Path.Combine(recordPath, "Series", _fileSystem.GetValidFilename(info.Name).Trim());
+                var customRecordingPath = config.SeriesRecordingPath;
+                if ((string.IsNullOrWhiteSpace(customRecordingPath) || string.Equals(customRecordingPath, recordPath, StringComparison.OrdinalIgnoreCase)) && config.EnableRecordingSubfolders)
+                {
+                    recordPath = Path.Combine(recordPath, "Series");
+                }
+
+                var folderName = _fileSystem.GetValidFilename(info.Name).Trim();
+                var folderNameWithYear = folderName;
+                if (info.ProductionYear.HasValue)
+                {
+                    folderNameWithYear += " (" + info.ProductionYear.Value.ToString(CultureInfo.InvariantCulture) + ")";
+                }
+
+                if (Directory.Exists(Path.Combine(recordPath, folderName)))
+                {
+                    recordPath = Path.Combine(recordPath, folderName);
+                }
+                else
+                {
+                    recordPath = Path.Combine(recordPath, folderNameWithYear);
+                }
 
 
                 if (info.SeasonNumber.HasValue)
                 if (info.SeasonNumber.HasValue)
                 {
                 {
-                    var folderName = string.Format("Season {0}", info.SeasonNumber.Value.ToString(CultureInfo.InvariantCulture));
+                    folderName = string.Format("Season {0}", info.SeasonNumber.Value.ToString(CultureInfo.InvariantCulture));
                     recordPath = Path.Combine(recordPath, folderName);
                     recordPath = Path.Combine(recordPath, folderName);
                 }
                 }
             }
             }
             else if (info.IsKids)
             else if (info.IsKids)
             {
             {
-                recordPath = Path.Combine(recordPath, "Kids", _fileSystem.GetValidFilename(info.Name).Trim());
+                if (config.EnableRecordingSubfolders)
+                {
+                    recordPath = Path.Combine(recordPath, "Kids");
+                }
+
+                var folderName = _fileSystem.GetValidFilename(info.Name).Trim();
+                if (info.ProductionYear.HasValue)
+                {
+                    folderName += " (" + info.ProductionYear.Value.ToString(CultureInfo.InvariantCulture) + ")";
+                }
+                recordPath = Path.Combine(recordPath, folderName);
             }
             }
             else if (info.IsSports)
             else if (info.IsSports)
             {
             {
-                recordPath = Path.Combine(recordPath, "Sports", _fileSystem.GetValidFilename(info.Name).Trim());
+                if (config.EnableRecordingSubfolders)
+                {
+                    recordPath = Path.Combine(recordPath, "Sports");
+                }
+                recordPath = Path.Combine(recordPath, _fileSystem.GetValidFilename(info.Name).Trim());
             }
             }
             else
             else
             {
             {
-                recordPath = Path.Combine(recordPath, "Other", _fileSystem.GetValidFilename(info.Name).Trim());
+                if (config.EnableRecordingSubfolders)
+                {
+                    recordPath = Path.Combine(recordPath, "Other");
+                }
+                recordPath = Path.Combine(recordPath, _fileSystem.GetValidFilename(info.Name).Trim());
             }
             }
 
 
             var recordingFileName = _fileSystem.GetValidFilename(RecordingHelper.GetRecordingName(timer, info)).Trim() + ".ts";
             var recordingFileName = _fileSystem.GetValidFilename(RecordingHelper.GetRecordingName(timer, info)).Trim() + ".ts";
 
 
-            recordPath = Path.Combine(recordPath, recordingFileName);
+            return Path.Combine(recordPath, recordingFileName);
+        }
 
 
-            var recordingId = info.Id.GetMD5().ToString("N");
-            var recording = _recordingProvider.GetAll().FirstOrDefault(x => string.Equals(x.Id, recordingId, StringComparison.OrdinalIgnoreCase));
+        private async Task RecordStream(TimerInfo timer, DateTime recordingEndDate, ActiveRecordingInfo activeRecordingInfo, CancellationToken cancellationToken)
+        {
+            if (timer == null)
+            {
+                throw new ArgumentNullException("timer");
+            }
 
 
-            if (recording == null)
+            ProgramInfo info = null;
+
+            if (string.IsNullOrWhiteSpace(timer.ProgramId))
             {
             {
-                recording = new RecordingInfo
-                {
-                    ChannelId = info.ChannelId,
-                    Id = recordingId,
-                    StartDate = info.StartDate,
-                    EndDate = info.EndDate,
-                    Genres = info.Genres,
-                    IsKids = info.IsKids,
-                    IsLive = info.IsLive,
-                    IsMovie = info.IsMovie,
-                    IsHD = info.IsHD,
-                    IsNews = info.IsNews,
-                    IsPremiere = info.IsPremiere,
-                    IsSeries = info.IsSeries,
-                    IsSports = info.IsSports,
-                    IsRepeat = !info.IsPremiere,
-                    Name = info.Name,
-                    EpisodeTitle = info.EpisodeTitle,
-                    ProgramId = info.Id,
-                    ImagePath = info.ImagePath,
-                    ImageUrl = info.ImageUrl,
-                    OriginalAirDate = info.OriginalAirDate,
-                    Status = RecordingStatus.Scheduled,
-                    Overview = info.Overview,
-                    SeriesTimerId = timer.SeriesTimerId,
-                    TimerId = timer.Id,
-                    ShowId = info.ShowId
-                };
-                _recordingProvider.AddOrUpdate(recording);
+                _logger.Info("Timer {0} has null programId", timer.Id);
+            }
+            else
+            {
+                info = GetProgramInfoFromCache(timer.ChannelId, timer.ProgramId);
+            }
+
+            if (info == null)
+            {
+                _logger.Info("Unable to find program with Id {0}. Will search using start date", timer.ProgramId);
+                info = GetProgramInfoFromCache(timer.ChannelId, timer.StartDate);
             }
             }
 
 
+            if (info == null)
+            {
+                throw new InvalidOperationException(string.Format("Program with Id {0} not found", timer.ProgramId));
+            }
+
+            var recordPath = GetRecordingPath(timer, info);
+            var recordingStatus = RecordingStatus.New;
+
             try
             try
             {
             {
                 var result = await GetChannelStreamInternal(timer.ChannelId, null, CancellationToken.None).ConfigureAwait(false);
                 var result = await GetChannelStreamInternal(timer.ChannelId, null, CancellationToken.None).ConfigureAwait(false);
@@ -817,11 +894,6 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
 
 
                     _libraryMonitor.ReportFileSystemChangeBeginning(recordPath);
                     _libraryMonitor.ReportFileSystemChangeBeginning(recordPath);
 
 
-                    recording.Path = recordPath;
-                    recording.Status = RecordingStatus.InProgress;
-                    recording.DateLastUpdated = DateTime.UtcNow;
-                    _recordingProvider.AddOrUpdate(recording);
-
                     var duration = recordingEndDate - DateTime.UtcNow;
                     var duration = recordingEndDate - DateTime.UtcNow;
 
 
                     _logger.Info("Beginning recording. Will record for {0} minutes.", duration.TotalMinutes.ToString(CultureInfo.InvariantCulture));
                     _logger.Info("Beginning recording. Will record for {0} minutes.", duration.TotalMinutes.ToString(CultureInfo.InvariantCulture));
@@ -846,7 +918,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
 
 
                     await recorder.Record(mediaStreamInfo, recordPath, duration, onStarted, cancellationToken).ConfigureAwait(false);
                     await recorder.Record(mediaStreamInfo, recordPath, duration, onStarted, cancellationToken).ConfigureAwait(false);
 
 
-                    recording.Status = RecordingStatus.Completed;
+                    recordingStatus = RecordingStatus.Completed;
                     _logger.Info("Recording completed: {0}", recordPath);
                     _logger.Info("Recording completed: {0}", recordPath);
                 }
                 }
                 finally
                 finally
@@ -862,12 +934,12 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
             catch (OperationCanceledException)
             catch (OperationCanceledException)
             {
             {
                 _logger.Info("Recording stopped: {0}", recordPath);
                 _logger.Info("Recording stopped: {0}", recordPath);
-                recording.Status = RecordingStatus.Completed;
+                recordingStatus = RecordingStatus.Completed;
             }
             }
             catch (Exception ex)
             catch (Exception ex)
             {
             {
                 _logger.ErrorException("Error recording to {0}", ex, recordPath);
                 _logger.ErrorException("Error recording to {0}", ex, recordPath);
-                recording.Status = RecordingStatus.Error;
+                recordingStatus = RecordingStatus.Error;
             }
             }
             finally
             finally
             {
             {
@@ -875,12 +947,9 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
                 _activeRecordings.TryRemove(timer.Id, out removed);
                 _activeRecordings.TryRemove(timer.Id, out removed);
             }
             }
 
 
-            recording.DateLastUpdated = DateTime.UtcNow;
-            _recordingProvider.AddOrUpdate(recording);
-
-            if (recording.Status == RecordingStatus.Completed)
+            if (recordingStatus == RecordingStatus.Completed)
             {
             {
-                OnSuccessfulRecording(recording);
+                OnSuccessfulRecording(info.IsSeries, recordPath);
                 _timerProvider.Delete(timer);
                 _timerProvider.Delete(timer);
             }
             }
             else if (DateTime.UtcNow < timer.EndDate)
             else if (DateTime.UtcNow < timer.EndDate)
@@ -893,7 +962,6 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
             else
             else
             {
             {
                 _timerProvider.Delete(timer);
                 _timerProvider.Delete(timer);
-                _recordingProvider.Delete(recording);
             }
             }
         }
         }
 
 
@@ -948,11 +1016,11 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
             return new DirectRecorder(_logger, _httpClient, _fileSystem);
             return new DirectRecorder(_logger, _httpClient, _fileSystem);
         }
         }
 
 
-        private async void OnSuccessfulRecording(RecordingInfo recording)
+        private async void OnSuccessfulRecording(bool isSeries, string path)
         {
         {
             if (GetConfiguration().EnableAutoOrganize)
             if (GetConfiguration().EnableAutoOrganize)
             {
             {
-                if (recording.IsSeries)
+                if (isSeries)
                 {
                 {
                     try
                     try
                     {
                     {
@@ -962,12 +1030,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
 
 
                         var organize = new EpisodeFileOrganizer(_organizationService, _config, _fileSystem, _logger, _libraryManager, _libraryMonitor, _providerManager);
                         var organize = new EpisodeFileOrganizer(_organizationService, _config, _fileSystem, _logger, _libraryManager, _libraryMonitor, _providerManager);
 
 
-                        var result = await organize.OrganizeEpisodeFile(recording.Path, CancellationToken.None).ConfigureAwait(false);
-
-                        if (result.Status == FileSortingStatus.Success)
-                        {
-                            _recordingProvider.Delete(recording);
-                        }
+                        var result = await organize.OrganizeEpisodeFile(path, CancellationToken.None).ConfigureAwait(false);
                     }
                     }
                     catch (Exception ex)
                     catch (Exception ex)
                     {
                     {
@@ -991,18 +1054,6 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
             return epgData.FirstOrDefault(p => Math.Abs(startDateTicks - p.StartDate.Ticks) <= TimeSpan.FromMinutes(3).Ticks);
             return epgData.FirstOrDefault(p => Math.Abs(startDateTicks - p.StartDate.Ticks) <= TimeSpan.FromMinutes(3).Ticks);
         }
         }
 
 
-        private string RecordingPath
-        {
-            get
-            {
-                var path = GetConfiguration().RecordingPath;
-
-                return string.IsNullOrWhiteSpace(path)
-                    ? Path.Combine(DataPath, "recordings")
-                    : path;
-            }
-        }
-
         private LiveTvOptions GetConfiguration()
         private LiveTvOptions GetConfiguration()
         {
         {
             return _config.GetConfiguration<LiveTvOptions>("livetv");
             return _config.GetConfiguration<LiveTvOptions>("livetv");
@@ -1010,7 +1061,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
 
 
         private async Task UpdateTimersForSeriesTimer(List<ProgramInfo> epgData, SeriesTimerInfo seriesTimer, bool deleteInvalidTimers)
         private async Task UpdateTimersForSeriesTimer(List<ProgramInfo> epgData, SeriesTimerInfo seriesTimer, bool deleteInvalidTimers)
         {
         {
-            var newTimers = GetTimersForSeries(seriesTimer, epgData, _recordingProvider.GetAll()).ToList();
+            var newTimers = GetTimersForSeries(seriesTimer, epgData, true).ToList();
 
 
             var registration = await GetRegistrationInfo("seriesrecordings").ConfigureAwait(false);
             var registration = await GetRegistrationInfo("seriesrecordings").ConfigureAwait(false);
 
 
@@ -1024,7 +1075,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
 
 
             if (deleteInvalidTimers)
             if (deleteInvalidTimers)
             {
             {
-                var allTimers = GetTimersForSeries(seriesTimer, epgData, new List<RecordingInfo>())
+                var allTimers = GetTimersForSeries(seriesTimer, epgData, false)
                     .Select(i => i.Id)
                     .Select(i => i.Id)
                     .ToList();
                     .ToList();
 
 
@@ -1040,7 +1091,9 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
             }
             }
         }
         }
 
 
-        private IEnumerable<TimerInfo> GetTimersForSeries(SeriesTimerInfo seriesTimer, IEnumerable<ProgramInfo> allPrograms, IReadOnlyList<RecordingInfo> currentRecordings)
+        private IEnumerable<TimerInfo> GetTimersForSeries(SeriesTimerInfo seriesTimer,
+            IEnumerable<ProgramInfo> allPrograms,
+            bool filterByCurrentRecordings)
         {
         {
             if (seriesTimer == null)
             if (seriesTimer == null)
             {
             {
@@ -1050,23 +1103,71 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
             {
             {
                 throw new ArgumentNullException("allPrograms");
                 throw new ArgumentNullException("allPrograms");
             }
             }
-            if (currentRecordings == null)
-            {
-                throw new ArgumentNullException("currentRecordings");
-            }
 
 
             // Exclude programs that have already ended
             // Exclude programs that have already ended
             allPrograms = allPrograms.Where(i => i.EndDate > DateTime.UtcNow && i.StartDate > DateTime.UtcNow);
             allPrograms = allPrograms.Where(i => i.EndDate > DateTime.UtcNow && i.StartDate > DateTime.UtcNow);
 
 
             allPrograms = GetProgramsForSeries(seriesTimer, allPrograms);
             allPrograms = GetProgramsForSeries(seriesTimer, allPrograms);
 
 
-            var recordingShowIds = currentRecordings.Select(i => i.ProgramId).Where(i => !string.IsNullOrWhiteSpace(i)).ToList();
-
-            allPrograms = allPrograms.Where(i => !recordingShowIds.Contains(i.Id, StringComparer.OrdinalIgnoreCase));
+            if (filterByCurrentRecordings)
+            {
+                allPrograms = allPrograms.Where(i => !IsProgramAlreadyInLibrary(i));
+            }
 
 
             return allPrograms.Select(i => RecordingHelper.CreateTimer(i, seriesTimer));
             return allPrograms.Select(i => RecordingHelper.CreateTimer(i, seriesTimer));
         }
         }
 
 
+        private bool IsProgramAlreadyInLibrary(ProgramInfo program)
+        {
+            if ((program.EpisodeNumber.HasValue && program.SeasonNumber.HasValue) || !string.IsNullOrWhiteSpace(program.EpisodeTitle))
+            {
+                var seriesIds = _libraryManager.GetItemIds(new InternalItemsQuery
+                {
+                    IncludeItemTypes = new[] { typeof(Series).Name },
+                    Name = program.Name
+
+                }).Select(i => i.ToString("N")).ToArray();
+
+                if (seriesIds.Length == 0)
+                {
+                    return false;
+                }
+
+                if (program.EpisodeNumber.HasValue && program.SeasonNumber.HasValue)
+                {
+                    var result = _libraryManager.GetItemsResult(new InternalItemsQuery
+                    {
+                        IncludeItemTypes = new[] { typeof(Episode).Name },
+                        ParentIndexNumber = program.SeasonNumber.Value,
+                        IndexNumber = program.EpisodeNumber.Value,
+                        AncestorIds = seriesIds
+                    });
+
+                    if (result.TotalRecordCount > 0)
+                    {
+                        return true;
+                    }
+                }
+
+                if (!string.IsNullOrWhiteSpace(program.EpisodeTitle))
+                {
+                    var result = _libraryManager.GetItemsResult(new InternalItemsQuery
+                    {
+                        IncludeItemTypes = new[] { typeof(Episode).Name },
+                        Name = program.EpisodeTitle,
+                        AncestorIds = seriesIds
+                    });
+
+                    if (result.TotalRecordCount > 0)
+                    {
+                        return true;
+                    }
+                }
+            }
+
+            return false;
+        }
+
         private IEnumerable<ProgramInfo> GetProgramsForSeries(SeriesTimerInfo seriesTimer, IEnumerable<ProgramInfo> allPrograms)
         private IEnumerable<ProgramInfo> GetProgramsForSeries(SeriesTimerInfo seriesTimer, IEnumerable<ProgramInfo> allPrograms)
         {
         {
             if (!seriesTimer.RecordAnyTime)
             if (!seriesTimer.RecordAnyTime)
@@ -1151,6 +1252,47 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
             });
             });
         }
         }
 
 
+        public List<VirtualFolderInfo> GetRecordingFolders()
+        {
+            var list = new List<VirtualFolderInfo>();
+
+            var defaultFolder = RecordingPath;
+            var defaultName = "Recordings";
+
+            if (Directory.Exists(defaultFolder))
+            {
+                list.Add(new VirtualFolderInfo
+                {
+                    Locations = new List<string> { defaultFolder },
+                    Name = defaultName
+                });
+            }
+
+            var customPath = GetConfiguration().MovieRecordingPath;
+            if ((!string.IsNullOrWhiteSpace(customPath) && !string.Equals(customPath, defaultFolder, StringComparison.OrdinalIgnoreCase)) && Directory.Exists(customPath))
+            {
+                list.Add(new VirtualFolderInfo
+                {
+                    Locations = new List<string> { customPath },
+                    Name = "Recorded Movies",
+                    CollectionType = CollectionType.Movies
+                });
+            }
+
+            customPath = GetConfiguration().SeriesRecordingPath;
+            if ((!string.IsNullOrWhiteSpace(customPath) && !string.Equals(customPath, defaultFolder, StringComparison.OrdinalIgnoreCase)) && Directory.Exists(customPath))
+            {
+                list.Add(new VirtualFolderInfo
+                {
+                    Locations = new List<string> { customPath },
+                    Name = "Recorded Series",
+                    CollectionType = CollectionType.TvShows
+                });
+            }
+
+            return list;
+        }
+
         class ActiveRecordingInfo
         class ActiveRecordingInfo
         {
         {
             public string Path { get; set; }
             public string Path { get; set; }

+ 57 - 11
MediaBrowser.Server.Implementations/Persistence/SqliteItemRepository.cs

@@ -82,7 +82,7 @@ namespace MediaBrowser.Server.Implementations.Persistence
         private IDbCommand _updateInheritedRatingCommand;
         private IDbCommand _updateInheritedRatingCommand;
         private IDbCommand _updateInheritedTagsCommand;
         private IDbCommand _updateInheritedTagsCommand;
 
 
-        public const int LatestSchemaVersion = 69;
+        public const int LatestSchemaVersion = 71;
 
 
         /// <summary>
         /// <summary>
         /// Initializes a new instance of the <see cref="SqliteItemRepository"/> class.
         /// Initializes a new instance of the <see cref="SqliteItemRepository"/> class.
@@ -226,6 +226,9 @@ namespace MediaBrowser.Server.Implementations.Persistence
             _connection.AddColumn(Logger, "TypedBaseItems", "InheritedTags", "Text");
             _connection.AddColumn(Logger, "TypedBaseItems", "InheritedTags", "Text");
             _connection.AddColumn(Logger, "TypedBaseItems", "CleanName", "Text");
             _connection.AddColumn(Logger, "TypedBaseItems", "CleanName", "Text");
             _connection.AddColumn(Logger, "TypedBaseItems", "PresentationUniqueKey", "Text");
             _connection.AddColumn(Logger, "TypedBaseItems", "PresentationUniqueKey", "Text");
+            _connection.AddColumn(Logger, "TypedBaseItems", "SlugName", "Text");
+            _connection.AddColumn(Logger, "TypedBaseItems", "OriginalTitle", "Text");
+            _connection.AddColumn(Logger, "TypedBaseItems", "PrimaryVersionId", "Text");
 
 
             string[] postQueries =
             string[] postQueries =
                 {
                 {
@@ -367,7 +370,9 @@ namespace MediaBrowser.Server.Implementations.Persistence
             "Tags",
             "Tags",
             "SourceType",
             "SourceType",
             "TrailerTypes",
             "TrailerTypes",
-            "DateModifiedDuringLastRefresh"
+            "DateModifiedDuringLastRefresh",
+            "OriginalTitle",
+            "PrimaryVersionId"
         };
         };
 
 
         private readonly string[] _mediaStreamSaveColumns =
         private readonly string[] _mediaStreamSaveColumns =
@@ -476,7 +481,10 @@ namespace MediaBrowser.Server.Implementations.Persistence
                 "DateModifiedDuringLastRefresh",
                 "DateModifiedDuringLastRefresh",
                 "InheritedTags",
                 "InheritedTags",
                 "CleanName",
                 "CleanName",
-                "PresentationUniqueKey"
+                "PresentationUniqueKey",
+                "SlugName",
+                "OriginalTitle",
+                "PrimaryVersionId"
             };
             };
             _saveItemCommand = _connection.CreateCommand();
             _saveItemCommand = _connection.CreateCommand();
             _saveItemCommand.CommandText = "replace into TypedBaseItems (" + string.Join(",", saveColumns.ToArray()) + ") values (";
             _saveItemCommand.CommandText = "replace into TypedBaseItems (" + string.Join(",", saveColumns.ToArray()) + ") values (";
@@ -810,7 +818,20 @@ namespace MediaBrowser.Server.Implementations.Persistence
                     {
                     {
                         _saveItemCommand.GetParameter(index++).Value = item.Name.RemoveDiacritics();
                         _saveItemCommand.GetParameter(index++).Value = item.Name.RemoveDiacritics();
                     }
                     }
+
                     _saveItemCommand.GetParameter(index++).Value = item.PresentationUniqueKey;
                     _saveItemCommand.GetParameter(index++).Value = item.PresentationUniqueKey;
+                    _saveItemCommand.GetParameter(index++).Value = item.SlugName;
+                    _saveItemCommand.GetParameter(index++).Value = item.OriginalTitle;
+
+                    var video = item as Video;
+                    if (video != null)
+                    {
+                        _saveItemCommand.GetParameter(index++).Value = video.PrimaryVersionId;
+                    }
+                    else
+                    {
+                        _saveItemCommand.GetParameter(index++).Value = null;
+                    }
 
 
                     _saveItemCommand.Transaction = transaction;
                     _saveItemCommand.Transaction = transaction;
 
 
@@ -1189,6 +1210,20 @@ namespace MediaBrowser.Server.Implementations.Persistence
                 item.DateModifiedDuringLastRefresh = reader.GetDateTime(51).ToUniversalTime();
                 item.DateModifiedDuringLastRefresh = reader.GetDateTime(51).ToUniversalTime();
             }
             }
 
 
+            if (!reader.IsDBNull(52))
+            {
+                item.OriginalTitle = reader.GetString(52);
+            }
+
+            var video = item as Video;
+            if (video != null)
+            {
+                if (!reader.IsDBNull(53))
+                {
+                    video.PrimaryVersionId = reader.GetString(53);
+                }
+            }
+
             return item;
             return item;
         }
         }
 
 
@@ -2070,6 +2105,19 @@ namespace MediaBrowser.Server.Implementations.Persistence
                 cmd.Parameters.Add(cmd, "@PersonName", DbType.String).Value = query.Person;
                 cmd.Parameters.Add(cmd, "@PersonName", DbType.String).Value = query.Person;
             }
             }
 
 
+            if (!string.IsNullOrWhiteSpace(query.SlugName))
+            {
+                if (_config.Configuration.SchemaVersion >= 70)
+                {
+                    whereClauses.Add("SlugName=@SlugName");
+                }
+                else
+                {
+                    whereClauses.Add("Name=@SlugName");
+                }
+                cmd.Parameters.Add(cmd, "@SlugName", DbType.String).Value = query.SlugName;
+            }
+
             if (!string.IsNullOrWhiteSpace(query.Name))
             if (!string.IsNullOrWhiteSpace(query.Name))
             {
             {
                 if (_config.Configuration.SchemaVersion >= 66)
                 if (_config.Configuration.SchemaVersion >= 66)
@@ -2097,14 +2145,7 @@ namespace MediaBrowser.Server.Implementations.Persistence
             }
             }
             if (!string.IsNullOrWhiteSpace(query.NameStartsWith))
             if (!string.IsNullOrWhiteSpace(query.NameStartsWith))
             {
             {
-                if (_config.Configuration.SchemaVersion >= 66)
-                {
-                    whereClauses.Add("CleanName like @NameStartsWith");
-                }
-                else
-                {
-                    whereClauses.Add("Name like @NameStartsWith");
-                }
+                whereClauses.Add("SortName like @NameStartsWith");
                 cmd.Parameters.Add(cmd, "@NameStartsWith", DbType.String).Value = query.NameStartsWith + "%";
                 cmd.Parameters.Add(cmd, "@NameStartsWith", DbType.String).Value = query.NameStartsWith + "%";
             }
             }
             if (!string.IsNullOrWhiteSpace(query.NameStartsWithOrGreater))
             if (!string.IsNullOrWhiteSpace(query.NameStartsWithOrGreater))
@@ -2347,6 +2388,11 @@ namespace MediaBrowser.Server.Implementations.Persistence
 
 
         private bool EnableGroupByPresentationUniqueKey(InternalItemsQuery query)
         private bool EnableGroupByPresentationUniqueKey(InternalItemsQuery query)
         {
         {
+            if (!query.GroupByPresentationUniqueKey)
+            {
+                return false;
+            }
+
             if (!string.IsNullOrWhiteSpace(query.PresentationUniqueKey))
             if (!string.IsNullOrWhiteSpace(query.PresentationUniqueKey))
             {
             {
                 return false;
                 return false;

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

@@ -384,7 +384,7 @@ namespace MediaBrowser.WebDashboard.Api
 
 
             if (string.Equals(mode, "cordova", StringComparison.OrdinalIgnoreCase))
             if (string.Equals(mode, "cordova", StringComparison.OrdinalIgnoreCase))
             {
             {
-                sb.Append("<meta http-equiv=\"Content-Security-Policy\" content=\"default-src * 'unsafe-inline' 'unsafe-eval' data:;\">");
+                sb.Append("<meta http-equiv=\"Content-Security-Policy\" content=\"default-src * 'unsafe-inline' 'unsafe-eval' data: filesystem:;\">");
             }
             }
 
 
             sb.Append("<link rel=\"manifest\" href=\"manifest.json\">");
             sb.Append("<link rel=\"manifest\" href=\"manifest.json\">");