Jelajahi Sumber

add option to merge metadata and IBN paths

Luke Pulverenti 10 tahun lalu
induk
melakukan
63f3cf97da
33 mengubah file dengan 413 tambahan dan 381 penghapusan
  1. 9 9
      MediaBrowser.Api/Music/InstantMixService.cs
  2. 1 1
      MediaBrowser.Api/Playback/BaseStreamingService.cs
  3. 1 1
      MediaBrowser.Api/StartupWizardService.cs
  4. 9 87
      MediaBrowser.Api/UserLibrary/UserLibraryService.cs
  5. 36 36
      MediaBrowser.Common.Implementations/ScheduledTasks/ScheduledTaskWorker.cs
  6. 10 0
      MediaBrowser.Controller/Devices/CameraImageUploadInfo.cs
  7. 4 1
      MediaBrowser.Controller/Devices/IDeviceManager.cs
  8. 0 19
      MediaBrowser.Controller/Entities/AdultVideo.cs
  9. 1 7
      MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs
  10. 11 48
      MediaBrowser.Controller/Entities/Audio/MusicArtist.cs
  11. 11 23
      MediaBrowser.Controller/Entities/Folder.cs
  12. 7 26
      MediaBrowser.Controller/Entities/Movies/BoxSet.cs
  13. 1 2
      MediaBrowser.Controller/Entities/UserView.cs
  14. 15 6
      MediaBrowser.Controller/Entities/UserViewBuilder.cs
  15. 1 16
      MediaBrowser.Controller/Library/IMusicManager.cs
  16. 4 0
      MediaBrowser.Controller/Library/IUserViewManager.cs
  17. 1 1
      MediaBrowser.Controller/MediaBrowser.Controller.csproj
  18. 4 25
      MediaBrowser.Controller/Playlists/Playlist.cs
  19. 1 1
      MediaBrowser.Model/Configuration/ServerConfiguration.cs
  20. 2 1
      MediaBrowser.Model/Notifications/NotificationType.cs
  21. 11 3
      MediaBrowser.Server.Implementations/Configuration/ServerConfigurationManager.cs
  22. 22 3
      MediaBrowser.Server.Implementations/Devices/DeviceManager.cs
  23. 21 1
      MediaBrowser.Server.Implementations/EntryPoints/Notifications/Notifications.cs
  24. 4 1
      MediaBrowser.Server.Implementations/IO/LibraryMonitor.cs
  25. 10 19
      MediaBrowser.Server.Implementations/Library/LibraryManager.cs
  26. 35 0
      MediaBrowser.Server.Implementations/Library/MusicManager.cs
  27. 140 1
      MediaBrowser.Server.Implementations/Library/UserViewManager.cs
  28. 4 2
      MediaBrowser.Server.Implementations/Localization/Server/server.json
  29. 12 2
      MediaBrowser.Server.Implementations/Notifications/CoreNotificationTypes.cs
  30. 17 32
      MediaBrowser.Server.Implementations/Session/SessionManager.cs
  31. 5 4
      MediaBrowser.Server.Implementations/TV/TVSeriesManager.cs
  32. 1 1
      MediaBrowser.WebDashboard/Api/PackageCreator.cs
  33. 2 2
      MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj

+ 9 - 9
MediaBrowser.Api/Music/InstantMixService.cs

@@ -73,44 +73,44 @@ namespace MediaBrowser.Api.Music
 
         public object Get(GetInstantMixFromArtistId request)
         {
-            var item = (MusicArtist)_libraryManager.GetItemById(request.Id);
+            var item = _libraryManager.GetItemById(request.Id);
 
             var user = _userManager.GetUserById(request.UserId.Value);
 
-            var items = _musicManager.GetInstantMixFromArtist(item.Name, user);
+            var items = _musicManager.GetInstantMixFromItem(item, user);
 
             return GetResult(items, user, request);
         }
 
         public object Get(GetInstantMixFromMusicGenreId request)
         {
-            var item = (MusicGenre)_libraryManager.GetItemById(request.Id);
+            var item = _libraryManager.GetItemById(request.Id);
 
             var user = _userManager.GetUserById(request.UserId.Value);
 
-            var items = _musicManager.GetInstantMixFromGenres(new[] { item.Name }, user);
+            var items = _musicManager.GetInstantMixFromItem(item, user);
 
             return GetResult(items, user, request);
         }
 
         public object Get(GetInstantMixFromSong request)
         {
-            var item = (Audio)_libraryManager.GetItemById(request.Id);
+            var item = _libraryManager.GetItemById(request.Id);
 
             var user = _userManager.GetUserById(request.UserId.Value);
 
-            var items = _musicManager.GetInstantMixFromSong(item, user);
+            var items = _musicManager.GetInstantMixFromItem(item, user);
 
             return GetResult(items, user, request);
         }
 
         public object Get(GetInstantMixFromAlbum request)
         {
-            var album = (MusicAlbum)_libraryManager.GetItemById(request.Id);
+            var album = _libraryManager.GetItemById(request.Id);
 
             var user = _userManager.GetUserById(request.UserId.Value);
 
-            var items = _musicManager.GetInstantMixFromAlbum(album, user);
+            var items = _musicManager.GetInstantMixFromItem(album, user);
 
             return GetResult(items, user, request);
         }
@@ -121,7 +121,7 @@ namespace MediaBrowser.Api.Music
 
             var user = _userManager.GetUserById(request.UserId.Value);
 
-            var items = _musicManager.GetInstantMixFromPlaylist(playlist, user);
+            var items = _musicManager.GetInstantMixFromItem(playlist, user);
 
             return GetResult(items, user, request);
         }

+ 1 - 1
MediaBrowser.Api/Playback/BaseStreamingService.cs

@@ -824,7 +824,7 @@ namespace MediaBrowser.Api.Playback
         {
             get
             {
-                return true;
+                return false;
             }
         }
 

+ 1 - 1
MediaBrowser.Api/StartupWizardService.cs

@@ -62,7 +62,7 @@ namespace MediaBrowser.Api
         {
             _config.Configuration.IsStartupWizardCompleted = true;
             _config.Configuration.EnableLocalizedGuids = true;
-            _config.Configuration.StoreArtistsInMetadata = true;
+            _config.Configuration.MergeMetadataAndImagesByName = true;
             _config.Configuration.EnableStandaloneMetadata = true;
             _config.Configuration.EnableLibraryMetadataSubFolder = true;
             _config.SaveConfiguration();

+ 9 - 87
MediaBrowser.Api/UserLibrary/UserLibraryService.cs

@@ -228,7 +228,7 @@ namespace MediaBrowser.Api.UserLibrary
         /// </summary>
         /// <value>The user id.</value>
         [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public Guid UserId { get; set; }
+        public string UserId { get; set; }
 
         [ApiMember(Name = "Limit", Description = "Limit", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
         public int Limit { get; set; }
@@ -304,81 +304,15 @@ namespace MediaBrowser.Api.UserLibrary
         {
             var user = _userManager.GetUserById(request.UserId);
 
-            var includeTypes = string.IsNullOrWhiteSpace(request.IncludeItemTypes)
-                ? new string[] { }
-                : request.IncludeItemTypes.Split(',');
-
-            var currentUser = user;
-
-            Func<BaseItem, bool> filter = i =>
-            {
-                if (includeTypes.Length > 0)
-                {
-                    if (!includeTypes.Contains(i.GetType().Name, StringComparer.OrdinalIgnoreCase))
-                    {
-                        return false;
-                    }
-                }
-
-                if (request.IsPlayed.HasValue)
-                {
-                    var val = request.IsPlayed.Value;
-                    if (i.IsPlayed(currentUser) != val)
-                    {
-                        return false;
-                    }
-                }
-
-                return i.LocationType != LocationType.Virtual && !i.IsFolder;
-            };
-
-            // Avoid implicitly captured closure
-            var libraryItems = string.IsNullOrEmpty(request.ParentId) && user != null ?
-                GetItemsConfiguredForLatest(user, filter) :
-                GetAllLibraryItems(request.UserId, _userManager, _libraryManager, request.ParentId, filter);
-
-            libraryItems = libraryItems.OrderByDescending(i => i.DateCreated);
-
-            if (request.IsPlayed.HasValue)
+            var list = _userViewManager.GetLatestItems(new LatestItemsQuery
             {
-                var takeLimit = request.Limit * 20;
-                libraryItems = libraryItems.Take(takeLimit);
-            }
-
-            // Avoid implicitly captured closure
-            var items = libraryItems
-                .ToList();
-
-            var list = new List<Tuple<BaseItem, List<BaseItem>>>();
-
-            foreach (var item in items)
-            {
-                // Only grab the index container for media
-                var container = item.IsFolder || !request.GroupItems ? null : item.LatestItemsIndexContainer;
-
-                if (container == null)
-                {
-                    list.Add(new Tuple<BaseItem, List<BaseItem>>(null, new List<BaseItem> { item }));
-                }
-                else
-                {
-                    var current = list.FirstOrDefault(i => i.Item1 != null && i.Item1.Id == container.Id);
-
-                    if (current != null)
-                    {
-                        current.Item2.Add(item);
-                    }
-                    else
-                    {
-                        list.Add(new Tuple<BaseItem, List<BaseItem>>(container, new List<BaseItem> { item }));
-                    }
-                }
-
-                if (list.Count >= request.Limit)
-                {
-                    break;
-                }
-            }
+                GroupItems = request.GroupItems,
+                IncludeItemTypes = (request.IncludeItemTypes ?? string.Empty).Split(',').Where(i => !string.IsNullOrWhiteSpace(i)).ToArray(),
+                IsPlayed = request.IsPlayed,
+                Limit = request.Limit,
+                ParentId = request.ParentId,
+                UserId = request.UserId
+            });
 
             var options = GetDtoOptions(request);
 
@@ -403,18 +337,6 @@ namespace MediaBrowser.Api.UserLibrary
             return ToOptimizedResult(dtos.ToList());
         }
 
-        private IEnumerable<BaseItem> GetItemsConfiguredForLatest(User user, Func<BaseItem,bool> filter)
-        {
-            // Avoid implicitly captured closure
-            var currentUser = user;
-
-            return user.RootFolder.GetChildren(user, true)
-                .OfType<Folder>()
-                .Where(i => !user.Configuration.LatestItemsExcludes.Contains(i.Id.ToString("N")))
-                .SelectMany(i => i.GetRecursiveChildren(currentUser, filter))
-                .DistinctBy(i => i.Id);
-        }
-
         public async Task<object> Get(GetUserViews request)
         {
             var user = _userManager.GetUserById(request.UserId);

+ 36 - 36
MediaBrowser.Common.Implementations/ScheduledTasks/ScheduledTaskWorker.cs

@@ -108,13 +108,9 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks
         /// </summary>
         private TaskResult _lastExecutionResult;
         /// <summary>
-        /// The _last execution resultinitialized
-        /// </summary>
-        private bool _lastExecutionResultinitialized;
-        /// <summary>
         /// The _last execution result sync lock
         /// </summary>
-        private object _lastExecutionResultSyncLock = new object();
+        private readonly object _lastExecutionResultSyncLock = new object();
         /// <summary>
         /// Gets the last execution result.
         /// </summary>
@@ -123,38 +119,39 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks
         {
             get
             {
-                LazyInitializer.EnsureInitialized(ref _lastExecutionResult, ref _lastExecutionResultinitialized, ref _lastExecutionResultSyncLock, () =>
+                if (_lastExecutionResult == null)
                 {
-                    var path = GetHistoryFilePath();
-
-                    try
-                    {
-                        return JsonSerializer.DeserializeFromFile<TaskResult>(path);
-                    }
-                    catch (DirectoryNotFoundException)
-                    {
-                        // File doesn't exist. No biggie
-                        return null;
-                    }
-                    catch (FileNotFoundException)
-                    {
-                        // File doesn't exist. No biggie
-                        return null;
-                    }
-                    catch (Exception ex)
+                    lock (_lastExecutionResultSyncLock)
                     {
-                        Logger.ErrorException("Error deserializing {0}", ex, path);
-                        return null;
+                        if (_lastExecutionResult == null)
+                        {
+                            var path = GetHistoryFilePath();
+
+                            try
+                            {
+                                return JsonSerializer.DeserializeFromFile<TaskResult>(path);
+                            }
+                            catch (DirectoryNotFoundException)
+                            {
+                                // File doesn't exist. No biggie
+                            }
+                            catch (FileNotFoundException)
+                            {
+                                // File doesn't exist. No biggie
+                            }
+                            catch (Exception ex)
+                            {
+                                Logger.ErrorException("Error deserializing {0}", ex, path);
+                            }
+                        }
                     }
-                });
+                }
 
                 return _lastExecutionResult;
             }
             private set
             {
                 _lastExecutionResult = value;
-
-                _lastExecutionResultinitialized = value != null;
             }
         }
 
@@ -227,13 +224,9 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks
         /// </summary>
         private IEnumerable<ITaskTrigger> _triggers;
         /// <summary>
-        /// The _triggers initialized
-        /// </summary>
-        private bool _triggersInitialized;
-        /// <summary>
         /// The _triggers sync lock
         /// </summary>
-        private object _triggersSyncLock = new object();
+        private readonly object _triggersSyncLock = new object();
         /// <summary>
         /// Gets the triggers that define when the task will run
         /// </summary>
@@ -243,7 +236,16 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks
         {
             get
             {
-                LazyInitializer.EnsureInitialized(ref _triggers, ref _triggersInitialized, ref _triggersSyncLock, LoadTriggers);
+                if (_triggers == null)
+                {
+                    lock (_triggersSyncLock)
+                    {
+                        if (_triggers == null)
+                        {
+                            _triggers = LoadTriggers();
+                        }
+                    }
+                }
 
                 return _triggers;
             }
@@ -262,8 +264,6 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks
 
                 _triggers = value.ToList();
 
-                _triggersInitialized = true;
-
                 ReloadTriggerEvents(false);
 
                 SaveTriggers(_triggers);

+ 10 - 0
MediaBrowser.Controller/Devices/CameraImageUploadInfo.cs

@@ -0,0 +1,10 @@
+using MediaBrowser.Model.Devices;
+
+namespace MediaBrowser.Controller.Devices
+{
+    public class CameraImageUploadInfo
+    {
+        public LocalFileInfo FileInfo { get; set; }
+        public DeviceInfo Device { get; set; }
+    }
+}

+ 4 - 1
MediaBrowser.Controller/Devices/IDeviceManager.cs

@@ -3,7 +3,6 @@ using MediaBrowser.Model.Events;
 using MediaBrowser.Model.Querying;
 using MediaBrowser.Model.Session;
 using System;
-using System.Collections.Generic;
 using System.IO;
 using System.Threading.Tasks;
 
@@ -15,6 +14,10 @@ namespace MediaBrowser.Controller.Devices
         /// Occurs when [device options updated].
         /// </summary>
         event EventHandler<GenericEventArgs<DeviceInfo>> DeviceOptionsUpdated;
+        /// <summary>
+        /// Occurs when [camera image uploaded].
+        /// </summary>
+        event EventHandler<GenericEventArgs<CameraImageUploadInfo>> CameraImageUploaded;
 
         /// <summary>
         /// Registers the device.

+ 0 - 19
MediaBrowser.Controller/Entities/AdultVideo.cs

@@ -1,19 +0,0 @@
-using System;
-using System.Collections.Generic;
-
-namespace MediaBrowser.Controller.Entities
-{
-    [Obsolete]
-    public class AdultVideo : Video, IHasProductionLocations, IHasTaglines
-    {
-        public List<string> ProductionLocations { get; set; }
-
-        public List<string> Taglines { get; set; }
-
-        public AdultVideo()
-        {
-            Taglines = new List<string>();
-            ProductionLocations = new List<string>();
-        }
-    }
-}

+ 1 - 7
MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs

@@ -1,11 +1,11 @@
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Configuration;
 using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Users;
 using System;
 using System.Collections.Generic;
 using System.Linq;
 using System.Runtime.Serialization;
-using MediaBrowser.Model.Users;
 
 namespace MediaBrowser.Controller.Entities.Audio
 {
@@ -181,10 +181,4 @@ namespace MediaBrowser.Controller.Entities.Audio
             return id;
         }
     }
-
-    [Obsolete]
-    public class MusicAlbumDisc : Folder
-    {
-
-    }
 }

+ 11 - 48
MediaBrowser.Controller/Entities/Audio/MusicArtist.cs

@@ -129,43 +129,20 @@ namespace MediaBrowser.Controller.Entities.Audio
             var others = items.Except(songs).ToList();
 
             var totalItems = songs.Count + others.Count;
-            var percentages = new Dictionary<Guid, double>(totalItems);
-
-            var tasks = new List<Task>();
+            var numComplete = 0;
 
             // Refresh songs
             foreach (var item in songs)
             {
-                if (tasks.Count >= 2)
-                {
-                    await Task.WhenAll(tasks).ConfigureAwait(false);
-                    tasks.Clear();
-                }
-
                 cancellationToken.ThrowIfCancellationRequested();
-                var innerProgress = new ActionableProgress<double>();
 
-                // Avoid implicitly captured closure
-                var currentChild = item;
-                innerProgress.RegisterAction(p =>
-                {
-                    lock (percentages)
-                    {
-                        percentages[currentChild.Id] = p / 100;
-
-                        var percent = percentages.Values.Sum();
-                        percent /= totalItems;
-                        percent *= 100;
-                        progress.Report(percent);
-                    }
-                });
-
-                var taskChild = item;
-                tasks.Add(Task.Run(async () => await RefreshItem(taskChild, refreshOptions, innerProgress, cancellationToken).ConfigureAwait(false), cancellationToken));
-            }
+                await item.RefreshMetadata(refreshOptions, cancellationToken).ConfigureAwait(false);
 
-            await Task.WhenAll(tasks).ConfigureAwait(false);
-            tasks.Clear();
+                numComplete++;
+                double percent = numComplete;
+                percent /= totalItems;
+                progress.Report(percent * 100);
+            }
 
             // Refresh current item
             await RefreshMetadata(refreshOptions, cancellationToken).ConfigureAwait(false);
@@ -175,31 +152,17 @@ namespace MediaBrowser.Controller.Entities.Audio
             {
                 cancellationToken.ThrowIfCancellationRequested();
 
-                // Avoid implicitly captured closure
-                var currentChild = item;
-
                 await item.RefreshMetadata(refreshOptions, cancellationToken).ConfigureAwait(false);
-                lock (percentages)
-                {
-                    percentages[currentChild.Id] = 1;
 
-                    var percent = percentages.Values.Sum();
-                    percent /= totalItems;
-                    percent *= 100;
-                    progress.Report(percent);
-                }
+                numComplete++;
+                double percent = numComplete;
+                percent /= totalItems;
+                progress.Report(percent * 100);
             }
 
             progress.Report(100);
         }
 
-        private async Task RefreshItem(BaseItem item, MetadataRefreshOptions refreshOptions, IProgress<double> progress, CancellationToken cancellationToken)
-        {
-            await item.RefreshMetadata(refreshOptions, cancellationToken).ConfigureAwait(false);
-
-            progress.Report(100);
-        }
-
         public ArtistInfo GetLookupInfo()
         {
             var info = GetItemLookupInfo<ArtistInfo>();

+ 11 - 23
MediaBrowser.Controller/Entities/Folder.cs

@@ -283,7 +283,17 @@ namespace MediaBrowser.Controller.Entities
         {
             get
             {
-                return _children ?? (_children = LoadChildrenInternal());
+                if (_children == null)
+                {
+                    lock (_childrenSyncLock)
+                    {
+                        if (_children == null)
+                        {
+                            _children = LoadChildrenInternal();
+                        }
+                    }
+                }
+                return _children;
             }
         }
 
@@ -749,28 +759,6 @@ namespace MediaBrowser.Controller.Entities
             return childrenItems;
         }
 
-        /// <summary>
-        /// Retrieves the child.
-        /// </summary>
-        /// <param name="child">The child.</param>
-        /// <returns>BaseItem.</returns>
-        private BaseItem RetrieveChild(Guid child)
-        {
-            var item = LibraryManager.GetItemById(child);
-
-            if (item != null)
-            {
-                if (item is IByReferenceItem)
-                {
-                    return LibraryManager.GetOrAddByReferenceItem(item);
-                }
-
-                item.Parent = this;
-            }
-
-            return item;
-        }
-
         private BaseItem RetrieveChild(BaseItem child)
         {
             if (child.Id == Guid.Empty)

+ 7 - 26
MediaBrowser.Controller/Entities/Movies/BoxSet.cs

@@ -143,31 +143,19 @@ namespace MediaBrowser.Controller.Entities.Movies
             var items = GetRecursiveChildren().ToList();
 
             var totalItems = items.Count;
-            var percentages = new Dictionary<Guid, double>(totalItems);
+            var numComplete = 0;
 
             // Refresh songs
             foreach (var item in items)
             {
                 cancellationToken.ThrowIfCancellationRequested();
-                var innerProgress = new ActionableProgress<double>();
 
-                // Avoid implicitly captured closure
-                var currentChild = item;
-                innerProgress.RegisterAction(p =>
-                {
-                    lock (percentages)
-                    {
-                        percentages[currentChild.Id] = p / 100;
-
-                        var percent = percentages.Values.Sum();
-                        percent /= totalItems;
-                        percent *= 100;
-                        progress.Report(percent);
-                    }
-                });
-
-                // Avoid implicitly captured closure
-                await RefreshItem(item, refreshOptions, innerProgress, cancellationToken).ConfigureAwait(false);
+                await item.RefreshMetadata(refreshOptions, cancellationToken).ConfigureAwait(false);
+
+                numComplete++;
+                double percent = numComplete;
+                percent /= totalItems;
+                progress.Report(percent * 100);
             }
 
             // Refresh current item
@@ -176,13 +164,6 @@ namespace MediaBrowser.Controller.Entities.Movies
             progress.Report(100);
         }
 
-        private async Task RefreshItem(BaseItem item, MetadataRefreshOptions refreshOptions, IProgress<double> progress, CancellationToken cancellationToken)
-        {
-            await item.RefreshMetadata(refreshOptions, cancellationToken).ConfigureAwait(false);
-
-            progress.Report(100);
-        }
-
         public override bool IsVisible(User user)
         {
             if (base.IsVisible(user))

+ 1 - 2
MediaBrowser.Controller/Entities/UserView.cs

@@ -64,8 +64,7 @@ namespace MediaBrowser.Controller.Entities
             {
                 CollectionType.Books,
                 CollectionType.HomeVideos,
-                CollectionType.Photos,
-                string.Empty
+                CollectionType.Photos
             };
 
             var collectionFolder = folder as ICollectionFolder;

+ 15 - 6
MediaBrowser.Controller/Entities/UserViewBuilder.cs

@@ -409,12 +409,21 @@ namespace MediaBrowser.Controller.Entities
 
         private QueryResult<BaseItem> GetMusicLatest(Folder parent, User user, InternalItemsQuery query)
         {
-            query.SortBy = new[] { ItemSortBy.DateCreated, ItemSortBy.SortName };
-            query.SortOrder = SortOrder.Descending;
+            var items = _userViewManager.GetLatestItems(new LatestItemsQuery
+            {
+                UserId = user.Id.ToString("N"),
+                Limit = GetSpecialItemsLimit(),
+                IncludeItemTypes = new[] { typeof(Audio.Audio).Name },
+                ParentId = (parent == null ? null : parent.Id.ToString("N")),
+                GroupItems = true
 
-            var items = GetRecursiveChildren(parent, user, new[] { CollectionType.Music, CollectionType.MusicVideos }, i => i is MusicVideo || i is Audio.Audio && FilterItem(i, query));
+            }).Select(i => i.Item1);
 
-            return PostFilterAndSort(items, parent, GetSpecialItemsLimit(), query);
+            query.SortBy = new string[] { };
+
+            //var items = GetRecursiveChildren(parent, user, new[] { CollectionType.Music, CollectionType.MusicVideos }, i => i is MusicVideo || i is Audio.Audio && FilterItem(i, query));
+
+            return PostFilterAndSort(items, parent, null, query);
         }
 
         private async Task<QueryResult<BaseItem>> GetMovieFolders(Folder parent, User user, InternalItemsQuery query)
@@ -741,7 +750,7 @@ namespace MediaBrowser.Controller.Entities
 
         private async Task<QueryResult<BaseItem>> GetGameGenreItems(Folder queryParent, Folder displayParent, User user, InternalItemsQuery query)
         {
-            var items = GetRecursiveChildren(queryParent, user, new[] {CollectionType.Games},
+            var items = GetRecursiveChildren(queryParent, user, new[] { CollectionType.Games },
                 i => i is Game && i.Genres.Contains(displayParent.Name, StringComparer.OrdinalIgnoreCase));
 
             return GetResult(items, queryParent, query);
@@ -1686,7 +1695,7 @@ namespace MediaBrowser.Controller.Entities
             return parent.GetRecursiveChildren(user);
         }
 
-        private IEnumerable<BaseItem> GetRecursiveChildren(Folder parent, User user, IEnumerable<string> viewTypes, Func<BaseItem,bool> filter)
+        private IEnumerable<BaseItem> GetRecursiveChildren(Folder parent, User user, IEnumerable<string> viewTypes, Func<BaseItem, bool> filter)
         {
             if (parent == null || parent is UserView)
             {

+ 1 - 16
MediaBrowser.Controller/Library/IMusicManager.cs

@@ -1,6 +1,5 @@
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;
-using MediaBrowser.Controller.Playlists;
 using System.Collections.Generic;
 
 namespace MediaBrowser.Controller.Library
@@ -13,7 +12,7 @@ namespace MediaBrowser.Controller.Library
         /// <param name="item">The item.</param>
         /// <param name="user">The user.</param>
         /// <returns>IEnumerable{Audio}.</returns>
-        IEnumerable<Audio> GetInstantMixFromSong(Audio item, User user);
+        IEnumerable<Audio> GetInstantMixFromItem(BaseItem item, User user);
         /// <summary>
         /// Gets the instant mix from artist.
         /// </summary>
@@ -22,20 +21,6 @@ namespace MediaBrowser.Controller.Library
         /// <returns>IEnumerable{Audio}.</returns>
         IEnumerable<Audio> GetInstantMixFromArtist(string name, User user);
         /// <summary>
-        /// Gets the instant mix from album.
-        /// </summary>
-        /// <param name="item">The item.</param>
-        /// <param name="user">The user.</param>
-        /// <returns>IEnumerable{Audio}.</returns>
-        IEnumerable<Audio> GetInstantMixFromAlbum(MusicAlbum item, User user);
-        /// <summary>
-        /// Gets the instant mix from playlist.
-        /// </summary>
-        /// <param name="item">The item.</param>
-        /// <param name="user">The user.</param>
-        /// <returns>IEnumerable&lt;Audio&gt;.</returns>
-        IEnumerable<Audio> GetInstantMixFromPlaylist(Playlist item, User user);
-        /// <summary>
         /// Gets the instant mix from genre.
         /// </summary>
         /// <param name="genres">The genres.</param>

+ 4 - 0
MediaBrowser.Controller/Library/IUserViewManager.cs

@@ -1,5 +1,7 @@
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Model.Library;
+using MediaBrowser.Model.Querying;
+using System;
 using System.Collections.Generic;
 using System.Threading;
 using System.Threading.Tasks;
@@ -16,5 +18,7 @@ namespace MediaBrowser.Controller.Library
         Task<UserView> GetUserView(string type, string sortName, CancellationToken cancellationToken);
 
         Task<UserView> GetUserView(string category, string type, User user, string sortName, CancellationToken cancellationToken);
+
+        List<Tuple<BaseItem, List<BaseItem>>> GetLatestItems(LatestItemsQuery request);
     }
 }

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

@@ -101,6 +101,7 @@
     <Compile Include="Collections\ICollectionManager.cs" />
     <Compile Include="Connect\IConnectManager.cs" />
     <Compile Include="Connect\UserLinkResult.cs" />
+    <Compile Include="Devices\CameraImageUploadInfo.cs" />
     <Compile Include="Devices\IDeviceManager.cs" />
     <Compile Include="Devices\IDeviceRepository.cs" />
     <Compile Include="Dlna\ControlRequest.cs" />
@@ -117,7 +118,6 @@
     <Compile Include="Drawing\ImageStream.cs" />
     <Compile Include="Dto\DtoOptions.cs" />
     <Compile Include="Dto\IDtoService.cs" />
-    <Compile Include="Entities\AdultVideo.cs" />
     <Compile Include="Entities\Audio\IHasAlbumArtist.cs" />
     <Compile Include="Entities\Audio\IHasMusicGenres.cs" />
     <Compile Include="Entities\Book.cs" />

+ 4 - 25
MediaBrowser.Controller/Playlists/Playlist.cs

@@ -113,38 +113,17 @@ namespace MediaBrowser.Controller.Playlists
                 return LibraryManager.Sort(items, user, new[] { ItemSortBy.AlbumArtist, ItemSortBy.Album, ItemSortBy.SortName }, SortOrder.Ascending);
             }
 
-            // Grab these explicitly to avoid the sorting that will happen below
-            var collection = item as BoxSet;
-            if (collection != null)
-            {
-                var items = user == null
-                    ? collection.Children
-                    : collection.GetChildren(user, true);
-
-                return items
-                   .Where(m => !m.IsFolder);
-            }
-
-            // Grab these explicitly to avoid the sorting that will happen below
-            var season = item as Season;
-            if (season != null)
-            {
-                var items = user == null
-                    ? season.Children
-                    : season.GetChildren(user, true);
-
-                return items
-                   .Where(m => !m.IsFolder);
-            }
-
             var folder = item as Folder;
-
             if (folder != null)
             {
                 var items = user == null
                     ? folder.GetRecursiveChildren(m => !m.IsFolder)
                     : folder.GetRecursiveChildren(user, m => !m.IsFolder);
 
+                if (folder.IsPreSorted)
+                {
+                    return items;
+                }
                 return LibraryManager.Sort(items, user, new[] { ItemSortBy.SortName }, SortOrder.Ascending);
             }
 

+ 1 - 1
MediaBrowser.Model/Configuration/ServerConfiguration.cs

@@ -168,7 +168,7 @@ namespace MediaBrowser.Model.Configuration
         /// <value>The dashboard source path.</value>
         public string DashboardSourcePath { get; set; }
 
-        public bool StoreArtistsInMetadata { get; set; }
+        public bool MergeMetadataAndImagesByName { get; set; }
         public bool EnableStandaloneMetadata { get; set; }
 
         /// <summary>

+ 2 - 1
MediaBrowser.Model/Notifications/NotificationType.cs

@@ -18,6 +18,7 @@ namespace MediaBrowser.Model.Notifications
         NewLibraryContent,
         NewLibraryContentMultiple,
         ServerRestartRequired,
-        TaskFailed
+        TaskFailed,
+        CameraImageUploaded
     }
 }

+ 11 - 3
MediaBrowser.Server.Implementations/Configuration/ServerConfigurationManager.cs

@@ -88,9 +88,12 @@ namespace MediaBrowser.Server.Implementations.Configuration
         /// </summary>
         private void UpdateItemsByNamePath()
         {
-            ((ServerApplicationPaths)ApplicationPaths).ItemsByNamePath = string.IsNullOrEmpty(Configuration.ItemsByNamePath) ?
-                null :
-                Configuration.ItemsByNamePath;
+            if (!Configuration.MergeMetadataAndImagesByName)
+            {
+                ((ServerApplicationPaths)ApplicationPaths).ItemsByNamePath = string.IsNullOrEmpty(Configuration.ItemsByNamePath) ?
+                    null :
+                    Configuration.ItemsByNamePath;
+            }
         }
 
         /// <summary>
@@ -101,6 +104,11 @@ namespace MediaBrowser.Server.Implementations.Configuration
             ((ServerApplicationPaths)ApplicationPaths).InternalMetadataPath = string.IsNullOrEmpty(Configuration.MetadataPath) ?
                 GetInternalMetadataPath() :
                 Configuration.MetadataPath;
+
+            if (Configuration.MergeMetadataAndImagesByName)
+            {
+                ((ServerApplicationPaths)ApplicationPaths).ItemsByNamePath = ((ServerApplicationPaths)ApplicationPaths).InternalMetadataPath;
+            }
         }
 
         private string GetInternalMetadataPath()

+ 22 - 3
MediaBrowser.Server.Implementations/Devices/DeviceManager.cs

@@ -27,6 +27,8 @@ namespace MediaBrowser.Server.Implementations.Devices
         private readonly IConfigurationManager _config;
         private readonly ILogger _logger;
 
+        public event EventHandler<GenericEventArgs<CameraImageUploadInfo>> CameraImageUploaded;
+
         /// <summary>
         /// Occurs when [device options updated].
         /// </summary>
@@ -116,7 +118,7 @@ namespace MediaBrowser.Server.Implementations.Devices
             {
                 devices = devices.Where(i => CanAccessDevice(query.UserId, i.Id));
             }
-            
+
             var array = devices.ToArray();
             return new QueryResult<DeviceInfo>
             {
@@ -137,7 +139,8 @@ namespace MediaBrowser.Server.Implementations.Devices
 
         public async Task AcceptCameraUpload(string deviceId, Stream stream, LocalFileInfo file)
         {
-            var path = GetUploadPath(deviceId);
+            var device = GetDevice(deviceId);
+            var path = GetUploadPath(device);
 
             if (!string.IsNullOrWhiteSpace(file.Album))
             {
@@ -163,11 +166,27 @@ namespace MediaBrowser.Server.Implementations.Devices
             {
                 _libraryMonitor.ReportFileSystemChangeComplete(path, true);
             }
+
+            if (CameraImageUploaded != null)
+            {
+                EventHelper.FireEventIfNotNull(CameraImageUploaded, this, new GenericEventArgs<CameraImageUploadInfo>
+                {
+                    Argument = new CameraImageUploadInfo
+                    {
+                        Device = device,
+                        FileInfo = file
+                    }
+                }, _logger);
+            }
         }
 
         private string GetUploadPath(string deviceId)
         {
-            var device = GetDevice(deviceId);
+            return GetUploadPath(GetDevice(deviceId));
+        }
+
+        private string GetUploadPath(DeviceInfo device)
+        {
             if (!string.IsNullOrWhiteSpace(device.CameraUploadPath))
             {
                 return device.CameraUploadPath;

+ 21 - 1
MediaBrowser.Server.Implementations/EntryPoints/Notifications/Notifications.cs

@@ -3,6 +3,7 @@ using MediaBrowser.Common.Plugins;
 using MediaBrowser.Common.ScheduledTasks;
 using MediaBrowser.Common.Updates;
 using MediaBrowser.Controller;
+using MediaBrowser.Controller.Devices;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Library;
@@ -44,8 +45,9 @@ namespace MediaBrowser.Server.Implementations.EntryPoints.Notifications
         private readonly object _libraryChangedSyncLock = new object();
 
         private readonly IConfigurationManager _config;
+        private readonly IDeviceManager _deviceManager;
 
-        public Notifications(IInstallationManager installationManager, IUserManager userManager, ILogger logger, ITaskManager taskManager, INotificationManager notificationManager, ILibraryManager libraryManager, ISessionManager sessionManager, IServerApplicationHost appHost, IConfigurationManager config)
+        public Notifications(IInstallationManager installationManager, IUserManager userManager, ILogger logger, ITaskManager taskManager, INotificationManager notificationManager, ILibraryManager libraryManager, ISessionManager sessionManager, IServerApplicationHost appHost, IConfigurationManager config, IDeviceManager deviceManager)
         {
             _installationManager = installationManager;
             _userManager = userManager;
@@ -56,6 +58,7 @@ namespace MediaBrowser.Server.Implementations.EntryPoints.Notifications
             _sessionManager = sessionManager;
             _appHost = appHost;
             _config = config;
+            _deviceManager = deviceManager;
         }
 
         public void Run()
@@ -74,6 +77,21 @@ namespace MediaBrowser.Server.Implementations.EntryPoints.Notifications
             _appHost.HasPendingRestartChanged += _appHost_HasPendingRestartChanged;
             _appHost.HasUpdateAvailableChanged += _appHost_HasUpdateAvailableChanged;
             _appHost.ApplicationUpdated += _appHost_ApplicationUpdated;
+            _deviceManager.CameraImageUploaded +=_deviceManager_CameraImageUploaded;
+        }
+
+        async void _deviceManager_CameraImageUploaded(object sender, GenericEventArgs<CameraImageUploadInfo> e)
+        {
+            var type = NotificationType.CameraImageUploaded.ToString();
+
+            var notification = new NotificationRequest
+            {
+                NotificationType = type
+            };
+
+            notification.Variables["DeviceName"] = e.Argument.Device.Name;
+
+            await SendNotification(notification).ConfigureAwait(false);
         }
 
         async void _appHost_ApplicationUpdated(object sender, GenericEventArgs<PackageVersionInfo> e)
@@ -451,6 +469,8 @@ namespace MediaBrowser.Server.Implementations.EntryPoints.Notifications
             _appHost.HasPendingRestartChanged -= _appHost_HasPendingRestartChanged;
             _appHost.HasUpdateAvailableChanged -= _appHost_HasUpdateAvailableChanged;
             _appHost.ApplicationUpdated -= _appHost_ApplicationUpdated;
+
+            _deviceManager.CameraImageUploaded -= _deviceManager_CameraImageUploaded;
         }
 
         private void DisposeLibraryUpdateTimer()

+ 4 - 1
MediaBrowser.Server.Implementations/IO/LibraryMonitor.cs

@@ -566,7 +566,10 @@ namespace MediaBrowser.Server.Implementations.IO
                 .Distinct()
                 .ToList();
 
-            foreach (var p in paths) Logger.Info(p + " reports change.");
+            foreach (var p in paths)
+            {
+                Logger.Info(p + " reports change.");
+            }
 
             // If the root folder changed, run the library task so the user can see it
             if (itemsToRefresh.Any(i => i is AggregateFolder))

+ 10 - 19
MediaBrowser.Server.Implementations/Library/LibraryManager.cs

@@ -219,11 +219,7 @@ namespace MediaBrowser.Server.Implementations.Library
         /// <summary>
         /// The _root folder sync lock
         /// </summary>
-        private object _rootFolderSyncLock = new object();
-        /// <summary>
-        /// The _root folder initialized
-        /// </summary>
-        private bool _rootFolderInitialized;
+        private readonly object _rootFolderSyncLock = new object();
         /// <summary>
         /// Gets the root folder.
         /// </summary>
@@ -232,17 +228,17 @@ namespace MediaBrowser.Server.Implementations.Library
         {
             get
             {
-                LazyInitializer.EnsureInitialized(ref _rootFolder, ref _rootFolderInitialized, ref _rootFolderSyncLock, CreateRootFolder);
-                return _rootFolder;
-            }
-            private set
-            {
-                _rootFolder = value;
-
-                if (value == null)
+                if (_rootFolder == null)
                 {
-                    _rootFolderInitialized = false;
+                    lock (_rootFolderSyncLock)
+                    {
+                        if (_rootFolder == null)
+                        {
+                            _rootFolder = CreateRootFolder();
+                        }
+                    }
                 }
+                return _rootFolder;
             }
         }
 
@@ -849,11 +845,6 @@ namespace MediaBrowser.Server.Implementations.Library
         {
             get
             {
-                if (ConfigurationManager.Configuration.StoreArtistsInMetadata)
-                {
-                    return Path.Combine(ConfigurationManager.ApplicationPaths.InternalMetadataPath, "artists");
-                }
-
                 return Path.Combine(ConfigurationManager.ApplicationPaths.ItemsByNamePath, "artists");
             }
         }

+ 35 - 0
MediaBrowser.Server.Implementations/Library/MusicManager.cs

@@ -83,5 +83,40 @@ namespace MediaBrowser.Server.Implementations.Library
                 .Take(100)
                 .OrderBy(i => Guid.NewGuid());
         }
+
+        public IEnumerable<Audio> GetInstantMixFromItem(BaseItem item, User user)
+        {
+            var genre = item as MusicGenre;
+            if (genre != null)
+            {
+                return GetInstantMixFromGenres(new[] { item.Name }, user);
+            }
+
+            var playlist = item as Playlist;
+            if (playlist != null)
+            {
+                return GetInstantMixFromPlaylist(playlist, user);
+            }
+
+            var album = item as MusicAlbum;
+            if (album != null)
+            {
+                return GetInstantMixFromAlbum(album, user);
+            }
+
+            var artist = item as MusicArtist;
+            if (artist != null)
+            {
+                return GetInstantMixFromArtist(artist.Name, user);
+            }
+
+            var song = item as Audio;
+            if (song != null)
+            {
+                return GetInstantMixFromSong(song, user);
+            }
+            
+            return new Audio[] { };
+        }
     }
 }

+ 140 - 1
MediaBrowser.Server.Implementations/Library/UserViewManager.cs

@@ -16,6 +16,7 @@ using System.Collections.Generic;
 using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
+using MoreLinq;
 
 namespace MediaBrowser.Server.Implementations.Library
 {
@@ -56,7 +57,9 @@ namespace MediaBrowser.Server.Implementations.Library
 
             var excludeFolderIds = user.Configuration.ExcludeFoldersFromGrouping.Select(i => new Guid(i)).ToList();
 
-            var standaloneFolders = folders.Where(i => UserView.IsExcludedFromGrouping(i) || excludeFolderIds.Contains(i.Id)).ToList();
+            var standaloneFolders = folders
+                .Where(i => UserView.IsExcludedFromGrouping(i) || excludeFolderIds.Contains(i.Id))
+                .ToList();
 
             var foldersWithViewTypes = folders
                 .Except(standaloneFolders)
@@ -164,5 +167,141 @@ namespace MediaBrowser.Server.Implementations.Library
 
             return _libraryManager.GetNamedView(name, type, sortName, cancellationToken);
         }
+
+        public List<Tuple<BaseItem, List<BaseItem>>> GetLatestItems(LatestItemsQuery request)
+        {
+            var user = _userManager.GetUserById(request.UserId);
+
+            var includeTypes = request.IncludeItemTypes;
+
+            var currentUser = user;
+
+            Func<BaseItem, bool> filter = i =>
+            {
+                if (includeTypes.Length > 0)
+                {
+                    if (!includeTypes.Contains(i.GetType().Name, StringComparer.OrdinalIgnoreCase))
+                    {
+                        return false;
+                    }
+                }
+
+                if (request.IsPlayed.HasValue)
+                {
+                    var val = request.IsPlayed.Value;
+                    if (i.IsPlayed(currentUser) != val)
+                    {
+                        return false;
+                    }
+                }
+
+                return i.LocationType != LocationType.Virtual && !i.IsFolder;
+            };
+
+            // Avoid implicitly captured closure
+            var libraryItems = string.IsNullOrEmpty(request.ParentId) && user != null ?
+                GetItemsConfiguredForLatest(user, filter) :
+                GetAllLibraryItems(request.UserId, _userManager, _libraryManager, request.ParentId, filter);
+
+            libraryItems = libraryItems.OrderByDescending(i => i.DateCreated);
+
+            if (request.IsPlayed.HasValue)
+            {
+                var takeLimit = (request.Limit ?? 20) * 20;
+                libraryItems = libraryItems.Take(takeLimit);
+            }
+
+            // Avoid implicitly captured closure
+            var items = libraryItems
+                .ToList();
+
+            var list = new List<Tuple<BaseItem, List<BaseItem>>>();
+
+            foreach (var item in items)
+            {
+                // Only grab the index container for media
+                var container = item.IsFolder || !request.GroupItems ? null : item.LatestItemsIndexContainer;
+
+                if (container == null)
+                {
+                    list.Add(new Tuple<BaseItem, List<BaseItem>>(null, new List<BaseItem> { item }));
+                }
+                else
+                {
+                    var current = list.FirstOrDefault(i => i.Item1 != null && i.Item1.Id == container.Id);
+
+                    if (current != null)
+                    {
+                        current.Item2.Add(item);
+                    }
+                    else
+                    {
+                        list.Add(new Tuple<BaseItem, List<BaseItem>>(container, new List<BaseItem> { item }));
+                    }
+                }
+
+                if (list.Count >= request.Limit)
+                {
+                    break;
+                }
+            }
+
+            return list;
+        }
+
+        protected IList<BaseItem> GetAllLibraryItems(string userId, IUserManager userManager, ILibraryManager libraryManager, string parentId, Func<BaseItem, bool> filter)
+        {
+            if (!string.IsNullOrEmpty(parentId))
+            {
+                var folder = (Folder)libraryManager.GetItemById(new Guid(parentId));
+
+                if (!string.IsNullOrWhiteSpace(userId))
+                {
+                    var user = userManager.GetUserById(userId);
+
+                    if (user == null)
+                    {
+                        throw new ArgumentException("User not found");
+                    }
+
+                    return folder
+                        .GetRecursiveChildren(user, filter)
+                        .ToList();
+                }
+
+                return folder
+                    .GetRecursiveChildren(filter);
+            }
+            if (!string.IsNullOrWhiteSpace(userId))
+            {
+                var user = userManager.GetUserById(userId);
+
+                if (user == null)
+                {
+                    throw new ArgumentException("User not found");
+                }
+
+                return user
+                    .RootFolder
+                    .GetRecursiveChildren(user, filter)
+                    .ToList();
+            }
+
+            return libraryManager
+                .RootFolder
+                .GetRecursiveChildren(filter);
+        }
+        
+        private IEnumerable<BaseItem> GetItemsConfiguredForLatest(User user, Func<BaseItem, bool> filter)
+        {
+            // Avoid implicitly captured closure
+            var currentUser = user;
+
+            return user.RootFolder.GetChildren(user, true)
+                .OfType<Folder>()
+                .Where(i => !user.Configuration.LatestItemsExcludes.Contains(i.Id.ToString("N")))
+                .SelectMany(i => i.GetRecursiveChildren(currentUser, filter))
+                .DistinctBy(i => i.Id);
+        }
     }
 }

+ 4 - 2
MediaBrowser.Server.Implementations/Localization/Server/server.json

@@ -55,6 +55,7 @@
     "HeaderAudio": "Audio",
     "HeaderVideo": "Video",
     "HeaderPaths": "Paths",
+    "CategorySync": "Sync",
     "HeaderSyncRequiresSupporterMembership": "Sync Requires a Supporter Membership",
     "HeaderEnjoyDayTrial": "Enjoy a 14 Day Free Trial",
     "LabelSyncTempPath": "Temporary file path:",
@@ -669,6 +670,7 @@
     "NotificationOptionInstallationFailed": "Installation failure",
     "NotificationOptionNewLibraryContent": "New content added",
     "NotificationOptionNewLibraryContentMultiple": "New content added (multiple)",
+    "NotificationOptionCameraImageUploaded": "Camera image uploaded",
     "SendNotificationHelp": "By default, notifications are delivered to the dashboard inbox. Browse the plugin catalog to install additional notification options.",
     "NotificationOptionServerRestartRequired": "Server restart required",
     "LabelNotificationEnabled": "Enable this notification",
@@ -893,7 +895,7 @@
     "OptionCommunityMostWatchedSort": "Most Watched",
     "TabNextUp": "Next Up",
     "HeaderBecomeMediaBrowserSupporter": "Become a Media Browser Supporter",
-    "TextAccessPremiumFeatures": "Enjoy Premium Features",
+    "TextEnjoyBonusFeatures": "Enjoy Bonus Features",
     "MessageNoMovieSuggestionsAvailable": "No movie suggestions are currently available. Start watching and rating your movies, and then come back to view your recommendations.",
     "MessageNoCollectionsAvailable": "Collections allow you to enjoy personalized groupings of Movies, Series, Albums, Books and Games. Click the + button to start creating Collections.",
     "MessageNoPlaylistsAvailable": "Playlists allow you to create lists of content to play consecutively at a time. To add items to playlists, right click or tap and hold, then select Add to Playlist.",
@@ -957,7 +959,7 @@
     "OptionLatestTvRecordings": "Latest recordings",
     "LabelProtocolInfo": "Protocol info:",
     "LabelProtocolInfoHelp": "The value that will be used when responding to GetProtocolInfo requests from the device.",
-    "TabKodiMetadata": "Kodi",
+    "TabNfo": "Nfo",
     "HeaderKodiMetadataHelp": "Media Browser includes native support for Kodi Nfo metadata and images. To enable or disable Kodi metadata, use the Advanced tab to configure options for your media types.",
     "LabelKodiMetadataUser": "Sync user watch data to nfo's for:",
     "LabelKodiMetadataUserHelp": "Enable this to keep watch data in sync between Media Browser and Kodi.",

+ 12 - 2
MediaBrowser.Server.Implementations/Notifications/CoreNotificationTypes.cs

@@ -1,7 +1,6 @@
 using MediaBrowser.Controller;
 using MediaBrowser.Controller.Localization;
 using MediaBrowser.Controller.Notifications;
-using MediaBrowser.Model.Configuration;
 using MediaBrowser.Model.Notifications;
 using System;
 using System.Collections.Generic;
@@ -137,6 +136,13 @@ namespace MediaBrowser.Server.Implementations.Notifications
                      Type = NotificationType.VideoPlaybackStopped.ToString(),
                      DefaultTitle = "{UserName} has finished playing {ItemName} on {DeviceName}.",
                      Variables = new List<string>{"UserName", "ItemName", "DeviceName", "AppName"}
+                },
+
+                new NotificationTypeInfo
+                {
+                     Type = NotificationType.CameraImageUploaded.ToString(),
+                     DefaultTitle = "A new camera image has been uploaded from {DeviceName}.",
+                     Variables = new List<string>{"DeviceName"}
                 }
             };
 
@@ -171,10 +177,14 @@ namespace MediaBrowser.Server.Implementations.Notifications
             {
                 note.Category = _localization.GetLocalizedString("CategoryUser");
             }
-            else   if (note.Type.IndexOf("Plugin", StringComparison.OrdinalIgnoreCase) != -1)
+            else if (note.Type.IndexOf("Plugin", StringComparison.OrdinalIgnoreCase) != -1)
             {
                 note.Category = _localization.GetLocalizedString("CategoryPlugin");
             }
+            else if (note.Type.IndexOf("CameraImageUploaded", StringComparison.OrdinalIgnoreCase) != -1)
+            {
+                note.Category = _localization.GetLocalizedString("CategorySync");
+            }
             else
             {
                 note.Category = _localization.GetLocalizedString("CategorySystem");

+ 17 - 32
MediaBrowser.Server.Implementations/Session/SessionManager.cs

@@ -870,14 +870,14 @@ namespace MediaBrowser.Server.Implementations.Session
             {
                 if (items.Any(i => !session.QueueableMediaTypes.Contains(i.MediaType, StringComparer.OrdinalIgnoreCase)))
                 {
-                    throw new ArgumentException(string.Format("{0} is unable to queue the requested media type.", session.DeviceName ?? session.Id.ToString()));
+                    throw new ArgumentException(string.Format("{0} is unable to queue the requested media type.", session.DeviceName ?? session.Id));
                 }
             }
             else
             {
                 if (items.Any(i => !session.PlayableMediaTypes.Contains(i.MediaType, StringComparer.OrdinalIgnoreCase)))
                 {
-                    throw new ArgumentException(string.Format("{0} is unable to play the requested media type.", session.DeviceName ?? session.Id.ToString()));
+                    throw new ArgumentException(string.Format("{0} is unable to play the requested media type.", session.DeviceName ?? session.Id));
                 }
             }
 
@@ -895,6 +895,19 @@ namespace MediaBrowser.Server.Implementations.Session
         {
             var item = _libraryManager.GetItemById(new Guid(id));
 
+            var byName = item as IItemByName;
+
+            if (byName != null)
+            {
+                var items = user == null ?
+                    _libraryManager.RootFolder.GetRecursiveChildren(i => !i.IsFolder && byName.ItemFilter(i)) :
+                    user.RootFolder.GetRecursiveChildren(user, i => !i.IsFolder && byName.ItemFilter(i));
+
+                items = items.OrderBy(i => i.SortName);
+
+                return items;
+            }
+            
             if (item.IsFolder)
             {
                 var folder = (Folder)item;
@@ -913,37 +926,9 @@ namespace MediaBrowser.Server.Implementations.Session
 
         private IEnumerable<BaseItem> TranslateItemForInstantMix(string id, User user)
         {
-            var item = _libraryManager.GetItemById(new Guid(id));
-
-            var audio = item as Audio;
-
-            if (audio != null)
-            {
-                return _musicManager.GetInstantMixFromSong(audio, user);
-            }
-
-            var artist = item as MusicArtist;
-
-            if (artist != null)
-            {
-                return _musicManager.GetInstantMixFromArtist(artist.Name, user);
-            }
-
-            var album = item as MusicAlbum;
-
-            if (album != null)
-            {
-                return _musicManager.GetInstantMixFromAlbum(album, user);
-            }
-
-            var genre = item as MusicGenre;
-
-            if (genre != null)
-            {
-                return _musicManager.GetInstantMixFromGenres(new[] { genre.Name }, user);
-            }
+            var item = _libraryManager.GetItemById(id);
 
-            return new BaseItem[] { };
+            return _musicManager.GetInstantMixFromItem(item, user);
         }
 
         public Task SendBrowseCommand(string controllingSessionId, string sessionId, BrowseRequest command, CancellationToken cancellationToken)

+ 5 - 4
MediaBrowser.Server.Implementations/TV/TVSeriesManager.cs

@@ -93,7 +93,8 @@ namespace MediaBrowser.Server.Implementations.TV
             return FilterSeries(request, series)
                 .AsParallel()
                 .Select(i => GetNextUp(i, currentUser))
-                .Where(i => i.Item1 != null)
+                // Include if an episode was found, and either the series is not unwatched or the specific series was requested
+                .Where(i => i.Item1 != null && (!i.Item3 || !string.IsNullOrWhiteSpace(request.SeriesId)))
                 .OrderByDescending(i =>
                 {
                     var episode = i.Item1;
@@ -123,7 +124,7 @@ namespace MediaBrowser.Server.Implementations.TV
         /// <param name="series">The series.</param>
         /// <param name="user">The user.</param>
         /// <returns>Task{Episode}.</returns>
-        private Tuple<Episode, DateTime> GetNextUp(Series series, User user)
+        private Tuple<Episode, DateTime, bool> GetNextUp(Series series, User user)
         {
             // Get them in display order, then reverse
             var allEpisodes = series.GetSeasons(user, true, true)
@@ -162,13 +163,13 @@ namespace MediaBrowser.Server.Implementations.TV
 
             if (lastWatched != null)
             {
-                return new Tuple<Episode, DateTime>(nextUp, lastWatchedDate);
+                return new Tuple<Episode, DateTime, bool>(nextUp, lastWatchedDate, false);
             }
 
             var firstEpisode = allEpisodes.LastOrDefault(i => i.LocationType != LocationType.Virtual && !i.IsPlayed(user));
 
             // Return the first episode
-            return new Tuple<Episode, DateTime>(firstEpisode, DateTime.MinValue);
+            return new Tuple<Episode, DateTime, bool>(firstEpisode, DateTime.MinValue, true);
         }
 
         private IEnumerable<Series> FilterSeries(NextUpQuery request, IEnumerable<Series> items)

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

@@ -441,7 +441,7 @@ namespace MediaBrowser.WebDashboard.Api
                                 "metadataconfigurationpage.js",
                                 "metadataimagespage.js",
                                 "metadatasubtitles.js",
-                                "metadatakodi.js",
+                                "metadatanfo.js",
                                 "moviegenres.js",
                                 "moviecollections.js",
                                 "movies.js",

+ 2 - 2
MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj

@@ -480,7 +480,7 @@
     <Content Include="dashboard-ui\librarypathmapping.html">
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
     </Content>
-    <Content Include="dashboard-ui\metadatakodi.html">
+    <Content Include="dashboard-ui\metadatanfo.html">
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
     </Content>
     <Content Include="dashboard-ui\mypreferencesdisplay.html">
@@ -795,7 +795,7 @@
     <Content Include="dashboard-ui\scripts\librarypathmapping.js">
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
     </Content>
-    <Content Include="dashboard-ui\scripts\metadatakodi.js">
+    <Content Include="dashboard-ui\scripts\metadatanfo.js">
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
     </Content>
     <Content Include="dashboard-ui\scripts\mypreferencesdisplay.js">