Browse Source

Merge pull request #1710 from MediaBrowser/dev

Dev
Luke 9 years ago
parent
commit
ab2476b9e0

+ 78 - 111
MediaBrowser.Api/BaseApiService.cs

@@ -79,7 +79,7 @@ namespace MediaBrowser.Api
                 }
             }
         }
-        
+
         /// <summary>
         /// To the optimized serialized result using cache.
         /// </summary>
@@ -118,9 +118,6 @@ namespace MediaBrowser.Api
             return ResultFactory.GetStaticFileResult(Request, path);
         }
 
-        private readonly char[] _dashReplaceChars = { '?', '/', '&' };
-        private const char SlugChar = '-';
-
         protected DtoOptions GetDtoOptions(object request)
         {
             var options = new DtoOptions();
@@ -154,152 +151,122 @@ namespace MediaBrowser.Api
 
         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)

+ 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>
         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>
@@ -364,7 +325,7 @@ namespace MediaBrowser.Api.Library
 
             try
             {
-                LibraryHelpers.RemoveMediaPath(_fileSystem, request.Name, request.Path, _appPaths);
+                _libraryManager.RemoveMediaPath(request.Name, request.Path);
             }
             finally
             {

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

@@ -129,7 +129,6 @@
     <Compile Include="ItemUpdateService.cs" />
     <Compile Include="Library\LibraryService.cs" />
     <Compile Include="Library\FileOrganizationService.cs" />
-    <Compile Include="Library\LibraryHelpers.cs" />
     <Compile Include="Library\LibraryStructureService.cs" />
     <Compile Include="LiveTv\LiveTvService.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))
             {
-                item.PrimaryVersionId = primaryVersion.Id;
+                item.PrimaryVersionId = primaryVersion.Id.ToString("N");
 
                 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>();
         }
 
+        public static readonly char[] SlugReplaceChars = { '?', '/', '&' };
+        public static char SlugChar = '-';
+
         /// <summary>
         /// The supported image extensions
         /// </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; }
 
         /// <summary>
@@ -728,12 +746,14 @@ namespace MediaBrowser.Controller.Entities
         /// Gets or sets the critic rating.
         /// </summary>
         /// <value>The critic rating.</value>
+        [IgnoreDataMember]
         public float? CriticRating { get; set; }
 
         /// <summary>
         /// Gets or sets the critic rating summary.
         /// </summary>
         /// <value>The critic rating summary.</value>
+        [IgnoreDataMember]
         public string CriticRatingSummary { get; set; }
 
         /// <summary>

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

@@ -707,8 +707,8 @@ namespace MediaBrowser.Controller.Entities
         {
             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 Path { get; set; }
         public string Name { get; set; }
+        public string SlugName { get; set; }
 
         public string Person { get; set; }
         public string[] PersonIds { get; set; }
@@ -133,9 +134,13 @@ namespace MediaBrowser.Controller.Entities
 
         public string[] AlbumNames { get; set; }
         public string[] ArtistNames { get; set; }
-        
+
+        public bool GroupByPresentationUniqueKey { get; set; }
+
         public InternalItemsQuery()
         {
+            GroupByPresentationUniqueKey = true;
+
             AlbumNames = 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.Linq;
 using System.Runtime.Serialization;
+using MediaBrowser.Controller.Entities.Audio;
 
 namespace MediaBrowser.Controller.Entities.Movies
 {
@@ -118,7 +119,7 @@ namespace MediaBrowser.Controller.Entities.Movies
             // Gather all possible ratings
             var ratings = GetRecursiveChildren()
                 .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)
                 .Where(i => !string.IsNullOrEmpty(i))
                 .Distinct(StringComparer.OrdinalIgnoreCase)

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

@@ -28,7 +28,8 @@ namespace MediaBrowser.Controller.Entities
         IThemeMedia,
         IArchivable
     {
-        public Guid? PrimaryVersionId { get; set; }
+        [IgnoreDataMember]
+        public string PrimaryVersionId { get; set; }
 
         public List<string> AdditionalParts { get; set; }
         public List<string> LocalAlternateVersions { get; set; }
@@ -49,9 +50,9 @@ namespace MediaBrowser.Controller.Entities
         {
             get
             {
-                if (PrimaryVersionId.HasValue)
+                if (!string.IsNullOrWhiteSpace(PrimaryVersionId))
                 {
-                    return PrimaryVersionId.Value.ToString("N");
+                    return PrimaryVersionId;
                 }
 
                 return base.PresentationUniqueKey;
@@ -70,6 +71,72 @@ namespace MediaBrowser.Controller.Entities
         /// <value>The timestamp.</value>
         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()
         {
             PlayableStreamFileNames = new List<string>();
@@ -104,9 +171,9 @@ namespace MediaBrowser.Controller.Entities
         {
             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)
                     {
                         return item.MediaSourceCount;
@@ -238,72 +305,6 @@ namespace MediaBrowser.Controller.Entities
                 .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]
         public override string ContainingFolderPath
         {
@@ -520,9 +521,9 @@ namespace MediaBrowser.Controller.Entities
             list.Add(new Tuple<Video, MediaSourceType>(this, MediaSourceType.Default));
             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)
                 {
                     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);
 
         void AddVirtualFolder(string name, string collectionType, string[] mediaPaths, bool refreshLibrary);
+        void RemoveVirtualFolder(string name, bool refreshLibrary);
         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 bool EnableMovieProviders { get; set; }
         public string RecordingPath { get; set; }
+        public string MovieRecordingPath { get; set; }
+        public string SeriesRecordingPath { get; set; }
         public bool EnableAutoOrganize { get; set; }
         public bool EnableRecordingEncoding { get; set; }
+        public bool EnableRecordingSubfolders { get; set; }
         public bool EnableOriginalAudioWithEncodedRecordings { get; set; }
 
         public List<TunerHostInfo> TunerHosts { get; set; }
@@ -20,6 +23,7 @@ namespace MediaBrowser.Model.LiveTv
         public LiveTvOptions()
         {
             EnableMovieProviders = true;
+            EnableRecordingSubfolders = true;
             TunerHosts = new List<TunerHostInfo>();
             ListingProviders = new List<ListingsProviderInfo>();
         }

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

@@ -397,12 +397,6 @@ namespace MediaBrowser.Server.Implementations.Dto
                     collectionFolder.GetViewType(user);
             }
 
-            var playlist = item as Playlist;
-            if (playlist != null)
-            {
-                AttachLinkedChildImages(dto, playlist, user, options);
-            }
-
             if (fields.Contains(ItemFields.CanDelete))
             {
                 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)
         {
             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 ShortcutFileSearch = "*" + ShortcutFileExtension;
         public void AddMediaPath(string virtualFolderName, string path)
         {
             if (string.IsNullOrWhiteSpace(path))
@@ -2668,5 +2713,28 @@ namespace MediaBrowser.Server.Implementations.Library
 
             _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.Model.Logging;
 using System;
-using System.Collections.Generic;
 using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
@@ -35,25 +34,20 @@ namespace MediaBrowser.Server.Implementations.Library.Validators
         /// <returns>Task.</returns>
         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 count = items.Count;
 
-            var validIds = new List<Guid>();
-
-            foreach (var name in items)
+            foreach (var item in items)
             {
                 try
                 {
-                    var itemByName = _libraryManager.GetStudio(name);
-
-                    validIds.Add(itemByName.Id);
-
-                    await itemByName.RefreshMetadata(cancellationToken).ConfigureAwait(false);
+                    await item.RefreshMetadata(cancellationToken).ConfigureAwait(false);
                 }
                 catch (OperationCanceledException)
                 {
@@ -62,7 +56,7 @@ namespace MediaBrowser.Server.Implementations.Library.Validators
                 }
                 catch (Exception ex)
                 {
-                    _logger.ErrorException("Error refreshing {0}", ex, name);
+                    _logger.ErrorException("Error refreshing {0}", ex, item.Name);
                 }
 
                 numComplete++;
@@ -73,28 +67,6 @@ namespace MediaBrowser.Server.Implementations.Library.Validators
                 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);
         }
     }

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

@@ -26,7 +26,10 @@ using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
 using CommonIO;
+using MediaBrowser.Common.Events;
 using MediaBrowser.Common.Extensions;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Power;
 using Microsoft.Win32;
 
@@ -40,7 +43,6 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
         private readonly IServerConfigurationManager _config;
         private readonly IJsonSerializer _jsonSerializer;
 
-        private readonly ItemDataProvider<RecordingInfo> _recordingProvider;
         private readonly ItemDataProvider<SeriesTimerInfo> _seriesTimerProvider;
         private readonly TimerManager _timerProvider;
 
@@ -56,6 +58,12 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
 
         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)
         {
             Current = this;
@@ -74,10 +82,19 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
             _liveTvManager = (LiveTvManager)liveTvManager;
             _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"));
             _timerProvider = new TimerManager(fileSystem, jsonSerializer, _logger, Path.Combine(DataPath, "timers"), powerManagement, _logger);
             _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()
@@ -85,6 +102,95 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
             _timerProvider.RestartTimers();
 
             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)
@@ -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
         {
             get { return "Emby"; }
@@ -114,6 +213,26 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
             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
         {
             get { return "http://emby.media"; }
@@ -280,49 +399,9 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
             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)
@@ -424,29 +503,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
 
         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)
@@ -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 config = GetConfiguration();
 
             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)
             {
-                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)
                 {
-                    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);
                 }
             }
             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)
             {
-                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
             {
-                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";
 
-            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
             {
                 var result = await GetChannelStreamInternal(timer.ChannelId, null, CancellationToken.None).ConfigureAwait(false);
@@ -817,11 +894,6 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
 
                     _libraryMonitor.ReportFileSystemChangeBeginning(recordPath);
 
-                    recording.Path = recordPath;
-                    recording.Status = RecordingStatus.InProgress;
-                    recording.DateLastUpdated = DateTime.UtcNow;
-                    _recordingProvider.AddOrUpdate(recording);
-
                     var duration = recordingEndDate - DateTime.UtcNow;
 
                     _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);
 
-                    recording.Status = RecordingStatus.Completed;
+                    recordingStatus = RecordingStatus.Completed;
                     _logger.Info("Recording completed: {0}", recordPath);
                 }
                 finally
@@ -862,12 +934,12 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
             catch (OperationCanceledException)
             {
                 _logger.Info("Recording stopped: {0}", recordPath);
-                recording.Status = RecordingStatus.Completed;
+                recordingStatus = RecordingStatus.Completed;
             }
             catch (Exception ex)
             {
                 _logger.ErrorException("Error recording to {0}", ex, recordPath);
-                recording.Status = RecordingStatus.Error;
+                recordingStatus = RecordingStatus.Error;
             }
             finally
             {
@@ -875,12 +947,9 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
                 _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);
             }
             else if (DateTime.UtcNow < timer.EndDate)
@@ -893,7 +962,6 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
             else
             {
                 _timerProvider.Delete(timer);
-                _recordingProvider.Delete(recording);
             }
         }
 
@@ -948,11 +1016,11 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
             return new DirectRecorder(_logger, _httpClient, _fileSystem);
         }
 
-        private async void OnSuccessfulRecording(RecordingInfo recording)
+        private async void OnSuccessfulRecording(bool isSeries, string path)
         {
             if (GetConfiguration().EnableAutoOrganize)
             {
-                if (recording.IsSeries)
+                if (isSeries)
                 {
                     try
                     {
@@ -962,12 +1030,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
 
                         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)
                     {
@@ -991,18 +1054,6 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
             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()
         {
             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)
         {
-            var newTimers = GetTimersForSeries(seriesTimer, epgData, _recordingProvider.GetAll()).ToList();
+            var newTimers = GetTimersForSeries(seriesTimer, epgData, true).ToList();
 
             var registration = await GetRegistrationInfo("seriesrecordings").ConfigureAwait(false);
 
@@ -1024,7 +1075,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
 
             if (deleteInvalidTimers)
             {
-                var allTimers = GetTimersForSeries(seriesTimer, epgData, new List<RecordingInfo>())
+                var allTimers = GetTimersForSeries(seriesTimer, epgData, false)
                     .Select(i => i.Id)
                     .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)
             {
@@ -1050,23 +1103,71 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
             {
                 throw new ArgumentNullException("allPrograms");
             }
-            if (currentRecordings == null)
-            {
-                throw new ArgumentNullException("currentRecordings");
-            }
 
             // Exclude programs that have already ended
             allPrograms = allPrograms.Where(i => i.EndDate > DateTime.UtcNow && i.StartDate > DateTime.UtcNow);
 
             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));
         }
 
+        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)
         {
             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
         {
             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 _updateInheritedTagsCommand;
 
-        public const int LatestSchemaVersion = 69;
+        public const int LatestSchemaVersion = 71;
 
         /// <summary>
         /// 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", "CleanName", "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 =
                 {
@@ -367,7 +370,9 @@ namespace MediaBrowser.Server.Implementations.Persistence
             "Tags",
             "SourceType",
             "TrailerTypes",
-            "DateModifiedDuringLastRefresh"
+            "DateModifiedDuringLastRefresh",
+            "OriginalTitle",
+            "PrimaryVersionId"
         };
 
         private readonly string[] _mediaStreamSaveColumns =
@@ -476,7 +481,10 @@ namespace MediaBrowser.Server.Implementations.Persistence
                 "DateModifiedDuringLastRefresh",
                 "InheritedTags",
                 "CleanName",
-                "PresentationUniqueKey"
+                "PresentationUniqueKey",
+                "SlugName",
+                "OriginalTitle",
+                "PrimaryVersionId"
             };
             _saveItemCommand = _connection.CreateCommand();
             _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.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;
 
@@ -1189,6 +1210,20 @@ namespace MediaBrowser.Server.Implementations.Persistence
                 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;
         }
 
@@ -2070,6 +2105,19 @@ namespace MediaBrowser.Server.Implementations.Persistence
                 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 (_config.Configuration.SchemaVersion >= 66)
@@ -2097,14 +2145,7 @@ namespace MediaBrowser.Server.Implementations.Persistence
             }
             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 + "%";
             }
             if (!string.IsNullOrWhiteSpace(query.NameStartsWithOrGreater))
@@ -2347,6 +2388,11 @@ namespace MediaBrowser.Server.Implementations.Persistence
 
         private bool EnableGroupByPresentationUniqueKey(InternalItemsQuery query)
         {
+            if (!query.GroupByPresentationUniqueKey)
+            {
+                return false;
+            }
+
             if (!string.IsNullOrWhiteSpace(query.PresentationUniqueKey))
             {
                 return false;

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

@@ -384,7 +384,7 @@ namespace MediaBrowser.WebDashboard.Api
 
             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\">");