Browse Source

take photos into the core

Luke Pulverenti 11 years ago
parent
commit
eec9e04825
63 changed files with 1564 additions and 357 deletions
  1. 71 0
      MediaBrowser.Api/Playback/BaseStreamingService.cs
  2. 2 6
      MediaBrowser.Api/Playback/StreamRequest.cs
  3. 1 1
      MediaBrowser.Api/UserService.cs
  4. 22 0
      MediaBrowser.Controller/Entities/AdultVideo.cs
  5. 1 1
      MediaBrowser.Controller/Entities/AggregateFolder.cs
  6. 27 21
      MediaBrowser.Controller/Entities/BaseItem.cs
  7. 1 1
      MediaBrowser.Controller/Entities/CollectionFolder.cs
  8. 1 1
      MediaBrowser.Controller/Entities/Folder.cs
  9. 3 3
      MediaBrowser.Controller/Entities/IHasMetadata.cs
  10. 26 4
      MediaBrowser.Controller/Entities/Movies/Movie.cs
  11. 24 0
      MediaBrowser.Controller/Entities/Photo.cs
  12. 19 8
      MediaBrowser.Controller/Entities/TV/Episode.cs
  13. 6 6
      MediaBrowser.Controller/Entities/TV/Season.cs
  14. 21 0
      MediaBrowser.Controller/Entities/TV/Series.cs
  15. 5 6
      MediaBrowser.Controller/Entities/UserRootFolder.cs
  16. 3 3
      MediaBrowser.Controller/Entities/Video.cs
  17. 13 2
      MediaBrowser.Controller/Library/ILibraryManager.cs
  18. 5 1
      MediaBrowser.Controller/Library/ItemResolveArgs.cs
  19. 1 0
      MediaBrowser.Controller/MediaBrowser.Controller.csproj
  20. 1 10
      MediaBrowser.Controller/Providers/IRemoteImageProvider.cs
  21. 15 15
      MediaBrowser.Controller/Resolvers/EntityResolutionHelper.cs
  22. 1 8
      MediaBrowser.Providers/BoxSets/MovieDbBoxSetImageProvider.cs
  23. 1 6
      MediaBrowser.Providers/GameGenres/GameGenreImageProvider.cs
  24. 1 6
      MediaBrowser.Providers/Genres/GenreImageProvider.cs
  25. 11 2
      MediaBrowser.Providers/Manager/MetadataService.cs
  26. 8 13
      MediaBrowser.Providers/Manager/ProviderManager.cs
  27. 7 1
      MediaBrowser.Providers/Manager/ProviderUtils.cs
  28. 5 0
      MediaBrowser.Providers/MediaBrowser.Providers.csproj
  29. 1 8
      MediaBrowser.Providers/Movies/FanartMovieImageProvider.cs
  30. 1 8
      MediaBrowser.Providers/Movies/MovieDbImageProvider.cs
  31. 20 4
      MediaBrowser.Providers/Movies/MovieDbSearch.cs
  32. 1 8
      MediaBrowser.Providers/Music/AudioDbAlbumImageProvider.cs
  33. 1 8
      MediaBrowser.Providers/Music/AudioDbArtistImageProvider.cs
  34. 1 8
      MediaBrowser.Providers/Music/FanArtAlbumProvider.cs
  35. 1 8
      MediaBrowser.Providers/Music/FanArtArtistProvider.cs
  36. 1 8
      MediaBrowser.Providers/Music/LastFmImageProvider.cs
  37. 1 6
      MediaBrowser.Providers/MusicGenres/MusicGenreImageProvider.cs
  38. 0 2
      MediaBrowser.Providers/MusicGenres/MusicGenreMetadataService.cs
  39. 1 8
      MediaBrowser.Providers/People/MovieDbPersonImageProvider.cs
  40. 1 8
      MediaBrowser.Providers/People/TvdbPersonImageProvider.cs
  41. 613 0
      MediaBrowser.Providers/Photos/ExifReader.cs
  42. 132 0
      MediaBrowser.Providers/Photos/ExifTags.cs
  43. 113 0
      MediaBrowser.Providers/Photos/PhotoHelper.cs
  44. 32 0
      MediaBrowser.Providers/Photos/PhotoMetadataService.cs
  45. 137 0
      MediaBrowser.Providers/Photos/PhotoProvider.cs
  46. 1 6
      MediaBrowser.Providers/Studios/StudiosImageProvider.cs
  47. 1 8
      MediaBrowser.Providers/TV/FanArtSeasonProvider.cs
  48. 1 8
      MediaBrowser.Providers/TV/FanartSeriesProvider.cs
  49. 1 8
      MediaBrowser.Providers/TV/MovieDbSeriesImageProvider.cs
  50. 66 66
      MediaBrowser.Providers/TV/MovieDbSeriesProvider.cs
  51. 1 8
      MediaBrowser.Providers/TV/TvdbEpisodeImageProvider.cs
  52. 1 8
      MediaBrowser.Providers/TV/TvdbSeasonImageProvider.cs
  53. 1 8
      MediaBrowser.Providers/TV/TvdbSeriesImageProvider.cs
  54. 13 11
      MediaBrowser.Server.Implementations/Library/LibraryManager.cs
  55. 6 3
      MediaBrowser.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs
  56. 3 1
      MediaBrowser.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs
  57. 8 7
      MediaBrowser.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
  58. 50 0
      MediaBrowser.Server.Implementations/Library/Resolvers/PhotoResolver.cs
  59. 1 0
      MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj
  60. 46 1
      MediaBrowser.ServerApplication/ApplicationHost.cs
  61. 2 2
      Nuget/MediaBrowser.Common.Internal.nuspec
  62. 1 1
      Nuget/MediaBrowser.Common.nuspec
  63. 2 2
      Nuget/MediaBrowser.Server.Core.nuspec

+ 71 - 0
MediaBrowser.Api/Playback/BaseStreamingService.cs

@@ -1159,6 +1159,72 @@ namespace MediaBrowser.Api.Playback
             return null;
         }
 
+        /// <summary>
+        /// Parses the parameters.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        private void ParseParams(StreamRequest request)
+        {
+            var vals = request.Params.Split(';');
+
+            var videoRequest = request as VideoStreamRequest;
+
+            for (var i = 0; i < vals.Length; i++)
+            {
+                var val = vals[i];
+
+                if (string.IsNullOrWhiteSpace(val))
+                {
+                    continue;
+                }
+
+                if (i == 0)
+                {
+                    request.DeviceId = val;
+                }
+                else if (i == 1)
+                {
+                    if (videoRequest != null)
+                    {
+                        videoRequest.VideoCodec = (VideoCodecs)Enum.Parse(typeof(VideoCodecs), val, true);
+                    }
+                }
+                else if (i == 2)
+                {
+                    request.AudioCodec = (AudioCodecs)Enum.Parse(typeof(AudioCodecs), val, true);
+                }
+                else if (i == 3)
+                {
+                    if (videoRequest != null)
+                    {
+                        videoRequest.AudioStreamIndex = int.Parse(val, UsCulture);
+                    }
+                }
+                else if (i == 4)
+                {
+                    if (videoRequest != null)
+                    {
+                        videoRequest.SubtitleStreamIndex = int.Parse(val, UsCulture);
+                    }
+                }
+                else if (i == 5)
+                {
+                    if (videoRequest != null)
+                    {
+                        videoRequest.VideoBitRate = int.Parse(val, UsCulture);
+                    }
+                }
+                else if (i == 6)
+                {
+                    request.AudioBitRate = int.Parse(val, UsCulture);
+                }
+                else if (i == 7)
+                {
+                    request.AudioChannels = int.Parse(val, UsCulture);
+                }
+            }
+        }
+
         /// <summary>
         /// Gets the state.
         /// </summary>
@@ -1167,6 +1233,11 @@ namespace MediaBrowser.Api.Playback
         /// <returns>StreamState.</returns>
         protected async Task<StreamState> GetState(StreamRequest request, CancellationToken cancellationToken)
         {
+            if (!string.IsNullOrWhiteSpace(request.Params))
+            {
+                ParseParams(request);
+            }
+
             if (request.ThrowDebugError)
             {
                 throw new InvalidOperationException("You asked for a debug error, you got one.");

+ 2 - 6
MediaBrowser.Api/Playback/StreamRequest.cs

@@ -60,16 +60,12 @@ namespace MediaBrowser.Api.Playback
         [ApiMember(Name = "Static", Description = "Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
         public bool Static { get; set; }
 
-        /// <summary>
-        /// This is an xbox 360 param that is used with dlna. If true the item's image should be returned instead of audio or video.
-        /// No need to put this in api docs since it's dlna only
-        /// </summary>
-        public bool AlbumArt { get; set; }
-
         /// <summary>
         /// For testing purposes
         /// </summary>
         public bool ThrowDebugError { get; set; }
+
+        public string Params { get; set; }
     }
 
     public class VideoStreamRequest : StreamRequest

+ 1 - 1
MediaBrowser.Api/UserService.cs

@@ -189,7 +189,7 @@ namespace MediaBrowser.Api
             {
                 throw new ArgumentNullException("xmlSerializer");
             }
-
+            
             _xmlSerializer = xmlSerializer;
             _userManager = userManager;
             _dtoService = dtoService;

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

@@ -1,4 +1,5 @@
 using System.Collections.Generic;
+using MediaBrowser.Controller.Providers;
 
 namespace MediaBrowser.Controller.Entities
 {
@@ -22,5 +23,26 @@ namespace MediaBrowser.Controller.Entities
         {
             Taglines = new List<string>();
         }
+
+        public override bool BeforeMetadataRefresh()
+        {
+            var hasChanges = base.BeforeMetadataRefresh();
+
+            if (!ProductionYear.HasValue)
+            {
+                int? yearInName = null;
+                string name;
+
+                NameParser.ParseName(Name, out name, out yearInName);
+
+                if (yearInName.HasValue)
+                {
+                    ProductionYear = yearInName;
+                    hasChanges = true;
+                }
+            }
+
+            return hasChanges;
+        }
     }
 }

+ 1 - 1
MediaBrowser.Controller/Entities/AggregateFolder.cs

@@ -66,7 +66,7 @@ namespace MediaBrowser.Controller.Entities
         {
             var path = ContainingFolderPath;
 
-            var args = new ItemResolveArgs(ConfigurationManager.ApplicationPaths, LibraryManager)
+            var args = new ItemResolveArgs(ConfigurationManager.ApplicationPaths, LibraryManager, directoryService)
             {
                 FileInfo = new DirectoryInfo(path),
                 Path = path,

+ 27 - 21
MediaBrowser.Controller/Entities/BaseItem.cs

@@ -472,7 +472,7 @@ namespace MediaBrowser.Controller.Entities
         /// Loads local trailers from the file system
         /// </summary>
         /// <returns>List{Video}.</returns>
-        private IEnumerable<Trailer> LoadLocalTrailers(List<FileSystemInfo> fileSystemChildren)
+        private IEnumerable<Trailer> LoadLocalTrailers(List<FileSystemInfo> fileSystemChildren, IDirectoryService directoryService)
         {
             var files = fileSystemChildren.OfType<DirectoryInfo>()
                 .Where(i => string.Equals(i.Name, TrailerFolderName, StringComparison.OrdinalIgnoreCase))
@@ -484,7 +484,7 @@ namespace MediaBrowser.Controller.Entities
                 .Where(i => System.IO.Path.GetFileNameWithoutExtension(i.Name).EndsWith(XbmcTrailerFileSuffix, StringComparison.OrdinalIgnoreCase) && !string.Equals(Path, i.FullName, StringComparison.OrdinalIgnoreCase))
                 );
 
-            return LibraryManager.ResolvePaths<Trailer>(files, null).Select(video =>
+            return LibraryManager.ResolvePaths<Trailer>(files, directoryService, null).Select(video =>
             {
                 // Try to retrieve it from the db. If we don't find it, use the resolved version
                 var dbItem = LibraryManager.GetItemById(video.Id) as Trailer;
@@ -504,7 +504,7 @@ namespace MediaBrowser.Controller.Entities
         /// Loads the theme songs.
         /// </summary>
         /// <returns>List{Audio.Audio}.</returns>
-        private IEnumerable<Audio.Audio> LoadThemeSongs(List<FileSystemInfo> fileSystemChildren)
+        private IEnumerable<Audio.Audio> LoadThemeSongs(List<FileSystemInfo> fileSystemChildren, IDirectoryService directoryService)
         {
             var files = fileSystemChildren.OfType<DirectoryInfo>()
                 .Where(i => string.Equals(i.Name, ThemeSongsFolderName, StringComparison.OrdinalIgnoreCase))
@@ -516,7 +516,7 @@ namespace MediaBrowser.Controller.Entities
                 .Where(i => string.Equals(System.IO.Path.GetFileNameWithoutExtension(i.Name), ThemeSongFilename, StringComparison.OrdinalIgnoreCase))
                 );
 
-            return LibraryManager.ResolvePaths<Audio.Audio>(files, null).Select(audio =>
+            return LibraryManager.ResolvePaths<Audio.Audio>(files, directoryService, null).Select(audio =>
             {
                 // Try to retrieve it from the db. If we don't find it, use the resolved version
                 var dbItem = LibraryManager.GetItemById(audio.Id) as Audio.Audio;
@@ -536,13 +536,13 @@ namespace MediaBrowser.Controller.Entities
         /// Loads the video backdrops.
         /// </summary>
         /// <returns>List{Video}.</returns>
-        private IEnumerable<Video> LoadThemeVideos(IEnumerable<FileSystemInfo> fileSystemChildren)
+        private IEnumerable<Video> LoadThemeVideos(IEnumerable<FileSystemInfo> fileSystemChildren, IDirectoryService directoryService)
         {
             var files = fileSystemChildren.OfType<DirectoryInfo>()
                 .Where(i => string.Equals(i.Name, ThemeVideosFolderName, StringComparison.OrdinalIgnoreCase))
                 .SelectMany(i => i.EnumerateFiles("*", SearchOption.TopDirectoryOnly));
 
-            return LibraryManager.ResolvePaths<Video>(files, null).Select(item =>
+            return LibraryManager.ResolvePaths<Video>(files, directoryService, null).Select(item =>
             {
                 // Try to retrieve it from the db. If we don't find it, use the resolved version
                 var dbItem = LibraryManager.GetItemById(item.Id) as Video;
@@ -579,15 +579,22 @@ namespace MediaBrowser.Controller.Entities
             {
                 options.DirectoryService = options.DirectoryService ?? new DirectoryService(Logger);
 
-                var files = locationType == LocationType.FileSystem || locationType == LocationType.Offline ?
-                    GetFileSystemChildren(options.DirectoryService).ToList() :
-                    new List<FileSystemInfo>();
+                try
+                {
+                    var files = locationType == LocationType.FileSystem || locationType == LocationType.Offline ?
+                        GetFileSystemChildren(options.DirectoryService).ToList() :
+                        new List<FileSystemInfo>();
 
-                var ownedItemsChanged = await RefreshedOwnedItems(options, files, cancellationToken).ConfigureAwait(false);
+                    var ownedItemsChanged = await RefreshedOwnedItems(options, files, cancellationToken).ConfigureAwait(false);
 
-                if (ownedItemsChanged)
+                    if (ownedItemsChanged)
+                    {
+                        requiresSave = true;
+                    }
+                }
+                catch (Exception ex)
                 {
-                    requiresSave = true;
+                    Logger.ErrorException("Error refreshing owned items for {0}", ex, Path ?? Name);
                 }
             }
 
@@ -650,7 +657,7 @@ namespace MediaBrowser.Controller.Entities
 
         private async Task<bool> RefreshLocalTrailers(IHasTrailers item, MetadataRefreshOptions options, List<FileSystemInfo> fileSystemChildren, CancellationToken cancellationToken)
         {
-            var newItems = LoadLocalTrailers(fileSystemChildren).ToList();
+            var newItems = LoadLocalTrailers(fileSystemChildren, options.DirectoryService).ToList();
             var newItemIds = newItems.Select(i => i.Id).ToList();
 
             var itemsChanged = !item.LocalTrailerIds.SequenceEqual(newItemIds);
@@ -666,7 +673,7 @@ namespace MediaBrowser.Controller.Entities
 
         private async Task<bool> RefreshThemeVideos(IHasThemeMedia item, MetadataRefreshOptions options, IEnumerable<FileSystemInfo> fileSystemChildren, CancellationToken cancellationToken)
         {
-            var newThemeVideos = LoadThemeVideos(fileSystemChildren).ToList();
+            var newThemeVideos = LoadThemeVideos(fileSystemChildren, options.DirectoryService).ToList();
 
             var newThemeVideoIds = newThemeVideos.Select(i => i.Id).ToList();
 
@@ -686,7 +693,7 @@ namespace MediaBrowser.Controller.Entities
         /// </summary>
         private async Task<bool> RefreshThemeSongs(IHasThemeMedia item, MetadataRefreshOptions options, List<FileSystemInfo> fileSystemChildren, CancellationToken cancellationToken)
         {
-            var newThemeSongs = LoadThemeSongs(fileSystemChildren).ToList();
+            var newThemeSongs = LoadThemeSongs(fileSystemChildren, options.DirectoryService).ToList();
             var newThemeSongIds = newThemeSongs.Select(i => i.Id).ToList();
 
             var themeSongsChanged = !item.ThemeSongIds.SequenceEqual(newThemeSongIds);
@@ -1422,20 +1429,19 @@ namespace MediaBrowser.Controller.Entities
         }
 
         /// <summary>
-        /// This is called before any metadata refresh and returns ItemUpdateType indictating if changes were made, and what.
+        /// This is called before any metadata refresh and returns true or false indicating if changes were made
         /// </summary>
-        /// <returns>ItemUpdateType.</returns>
-        public virtual ItemUpdateType BeforeMetadataRefresh()
+        public virtual bool BeforeMetadataRefresh()
         {
-            var updateType = ItemUpdateType.None;
+            var hasChanges = false;
 
             if (string.IsNullOrEmpty(Name) && !string.IsNullOrEmpty(Path))
             {
                 Name = System.IO.Path.GetFileNameWithoutExtension(Path);
-                updateType = updateType | ItemUpdateType.MetadataEdit;
+                hasChanges = true;
             }
 
-            return updateType;
+            return hasChanges;
         }
     }
 }

+ 1 - 1
MediaBrowser.Controller/Entities/CollectionFolder.cs

@@ -70,7 +70,7 @@ namespace MediaBrowser.Controller.Entities
         {
             var path = ContainingFolderPath;
 
-            var args = new ItemResolveArgs(ConfigurationManager.ApplicationPaths, LibraryManager)
+            var args = new ItemResolveArgs(ConfigurationManager.ApplicationPaths, LibraryManager, directoryService)
             {
                 FileInfo = new DirectoryInfo(path),
                 Path = path,

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

@@ -681,7 +681,7 @@ namespace MediaBrowser.Controller.Entities
         /// <returns>IEnumerable{BaseItem}.</returns>
         protected virtual IEnumerable<BaseItem> GetNonCachedChildren(IDirectoryService directoryService)
         {
-            return LibraryManager.ResolvePaths<BaseItem>(GetFileSystemChildren(directoryService), this);
+            return LibraryManager.ResolvePaths<BaseItem>(GetFileSystemChildren(directoryService), directoryService, this);
         }
 
         /// <summary>

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

@@ -51,9 +51,9 @@ namespace MediaBrowser.Controller.Entities
         Task UpdateToRepository(ItemUpdateType updateReason, CancellationToken cancellationToken);
 
         /// <summary>
-        /// This is called before any metadata refresh and returns ItemUpdateType indictating if changes were made, and what.
+        /// This is called before any metadata refresh and returns true or false indicating if changes were made
         /// </summary>
-        /// <returns>ItemUpdateType.</returns>
-        ItemUpdateType BeforeMetadataRefresh();
+        /// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns>
+        bool BeforeMetadataRefresh();
     }
 }

+ 26 - 4
MediaBrowser.Controller/Entities/Movies/Movie.cs

@@ -1,4 +1,5 @@
-using MediaBrowser.Controller.Providers;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Configuration;
 using MediaBrowser.Model.Entities;
 using System;
@@ -117,7 +118,7 @@ namespace MediaBrowser.Controller.Entities.Movies
 
         private async Task<bool> RefreshSpecialFeatures(MetadataRefreshOptions options, IEnumerable<FileSystemInfo> fileSystemChildren, CancellationToken cancellationToken)
         {
-            var newItems = LoadSpecialFeatures(fileSystemChildren).ToList();
+            var newItems = LoadSpecialFeatures(fileSystemChildren, options.DirectoryService).ToList();
             var newItemIds = newItems.Select(i => i.Id).ToList();
 
             var itemsChanged = !SpecialFeatureIds.SequenceEqual(newItemIds);
@@ -135,13 +136,13 @@ namespace MediaBrowser.Controller.Entities.Movies
         /// Loads the special features.
         /// </summary>
         /// <returns>IEnumerable{Video}.</returns>
-        private IEnumerable<Video> LoadSpecialFeatures(IEnumerable<FileSystemInfo> fileSystemChildren)
+        private IEnumerable<Video> LoadSpecialFeatures(IEnumerable<FileSystemInfo> fileSystemChildren, IDirectoryService directoryService)
         {
             var files = fileSystemChildren.OfType<DirectoryInfo>()
                 .Where(i => string.Equals(i.Name, "extras", StringComparison.OrdinalIgnoreCase) || string.Equals(i.Name, "specials", StringComparison.OrdinalIgnoreCase))
                 .SelectMany(i => i.EnumerateFiles("*", SearchOption.TopDirectoryOnly));
 
-            return LibraryManager.ResolvePaths<Video>(files, null).Select(video =>
+            return LibraryManager.ResolvePaths<Video>(files, directoryService, null).Select(video =>
             {
                 // Try to retrieve it from the db. If we don't find it, use the resolved version
                 var dbItem = LibraryManager.GetItemById(video.Id) as Video;
@@ -166,5 +167,26 @@ namespace MediaBrowser.Controller.Entities.Movies
         {
             return GetItemLookupInfo<MovieInfo>();
         }
+
+        public override bool BeforeMetadataRefresh()
+        {
+            var hasChanges = base.BeforeMetadataRefresh();
+
+            if (!ProductionYear.HasValue)
+            {
+                int? yearInName = null;
+                string name;
+
+                NameParser.ParseName(Name, out name, out yearInName);
+
+                if (yearInName.HasValue)
+                {
+                    ProductionYear = yearInName;
+                    hasChanges = true;
+                }
+            }
+
+            return hasChanges;
+        }
     }
 }

+ 24 - 0
MediaBrowser.Controller/Entities/Photo.cs

@@ -0,0 +1,24 @@
+using System.Collections.Generic;
+
+namespace MediaBrowser.Controller.Entities
+{
+    public class Photo : BaseItem, IHasTags, IHasTaglines
+    {
+        public List<string> Tags { get; set; }
+        public List<string> Taglines { get; set; }
+
+        public Photo()
+        {
+            Tags = new List<string>();
+            Taglines = new List<string>();
+        }
+
+        public override string MediaType
+        {
+            get
+            {
+                return Model.Entities.MediaType.Photo;
+            }
+        }
+    }
+}

+ 19 - 8
MediaBrowser.Controller/Entities/TV/Episode.cs

@@ -263,32 +263,32 @@ namespace MediaBrowser.Controller.Entities.TV
             return id;
         }
 
-        public override ItemUpdateType BeforeMetadataRefresh()
+        public override bool BeforeMetadataRefresh()
         {
-            var updateType = base.BeforeMetadataRefresh();
+            var hasChanges = base.BeforeMetadataRefresh();
 
             var locationType = LocationType;
             if (locationType == LocationType.FileSystem || locationType == LocationType.Offline)
             {
                 if (!IndexNumber.HasValue && !string.IsNullOrEmpty(Path))
                 {
-                    IndexNumber = IndexNumber ?? TVUtils.GetEpisodeNumberFromFile(Path, Parent is Season);
+                    IndexNumber = TVUtils.GetEpisodeNumberFromFile(Path, Parent is Season);
 
                     // If a change was made record it
                     if (IndexNumber.HasValue)
                     {
-                        updateType = updateType | ItemUpdateType.MetadataImport;
+                        hasChanges = true;
                     }
                 }
 
                 if (!IndexNumberEnd.HasValue && !string.IsNullOrEmpty(Path))
                 {
-                    IndexNumberEnd = IndexNumberEnd ?? TVUtils.GetEndingEpisodeNumberFromFile(Path);
+                    IndexNumberEnd = TVUtils.GetEndingEpisodeNumberFromFile(Path);
 
                     // If a change was made record it
                     if (IndexNumberEnd.HasValue)
                     {
-                        updateType = updateType | ItemUpdateType.MetadataImport;
+                        hasChanges = true;
                     }
                 }
             }
@@ -302,14 +302,25 @@ namespace MediaBrowser.Controller.Entities.TV
                     ParentIndexNumber = season.IndexNumber;
                 }
 
+                if (!ParentIndexNumber.HasValue && !string.IsNullOrEmpty(Path))
+                {
+                    ParentIndexNumber = TVUtils.GetSeasonNumberFromPath(Path);
+
+                    // If a change was made record it
+                    if (ParentIndexNumber.HasValue)
+                    {
+                        hasChanges = true;
+                    }
+                }
+
                 // If a change was made record it
                 if (ParentIndexNumber.HasValue)
                 {
-                    updateType = updateType | ItemUpdateType.MetadataImport;
+                    hasChanges = true;
                 }
             }
 
-            return updateType;
+            return hasChanges;
         }
     }
 }

+ 6 - 6
MediaBrowser.Controller/Entities/TV/Season.cs

@@ -254,12 +254,12 @@ namespace MediaBrowser.Controller.Entities.TV
         }
 
         /// <summary>
-        /// This is called before any metadata refresh and returns ItemUpdateType indictating if changes were made, and what.
+        /// This is called before any metadata refresh and returns true or false indicating if changes were made
         /// </summary>
-        /// <returns>ItemUpdateType.</returns>
-        public override ItemUpdateType BeforeMetadataRefresh()
+        /// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns>
+        public override bool BeforeMetadataRefresh()
         {
-            var updateType = base.BeforeMetadataRefresh();
+            var hasChanges = base.BeforeMetadataRefresh();
 
             var locationType = LocationType;
 
@@ -272,12 +272,12 @@ namespace MediaBrowser.Controller.Entities.TV
                     // If a change was made record it
                     if (IndexNumber.HasValue)
                     {
-                        updateType = updateType | ItemUpdateType.MetadataImport;
+                        hasChanges = true;
                     }
                 }
             }
 
-            return updateType;
+            return hasChanges;
         }
     }
 }

+ 21 - 0
MediaBrowser.Controller/Entities/TV/Series.cs

@@ -228,5 +228,26 @@ namespace MediaBrowser.Controller.Entities.TV
         {
             return GetItemLookupInfo<SeriesInfo>();
         }
+
+        public override bool BeforeMetadataRefresh()
+        {
+            var hasChanges = base.BeforeMetadataRefresh();
+
+            if (!ProductionYear.HasValue)
+            {
+                int? yearInName = null;
+                string name;
+
+                NameParser.ParseName(Name, out name, out yearInName);
+
+                if (yearInName.HasValue)
+                {
+                    ProductionYear = yearInName;
+                    hasChanges = true;
+                }
+            }
+
+            return hasChanges;
+        }
     }
 }

+ 5 - 6
MediaBrowser.Controller/Entities/UserRootFolder.cs

@@ -1,5 +1,4 @@
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Providers;
+using MediaBrowser.Controller.Providers;
 using System.Collections.Generic;
 using System.Linq;
 
@@ -20,17 +19,17 @@ namespace MediaBrowser.Controller.Entities
             return base.GetNonCachedChildren(directoryService).Concat(LibraryManager.RootFolder.VirtualChildren);
         }
 
-        public override ItemUpdateType BeforeMetadataRefresh()
+        public override bool BeforeMetadataRefresh()
         {
-            var updateType = base.BeforeMetadataRefresh();
+            var hasChanges = base.BeforeMetadataRefresh();
 
             if (string.Equals("default", Name, System.StringComparison.OrdinalIgnoreCase))
             {
                 Name = "Default Media Library";
-                updateType = updateType | ItemUpdateType.MetadataEdit;
+                hasChanges = true;
             }
 
-            return updateType;
+            return hasChanges;
         }
     }
 }

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

@@ -192,7 +192,7 @@ namespace MediaBrowser.Controller.Entities
         /// <returns>Task{System.Boolean}.</returns>
         private async Task<bool> RefreshAdditionalParts(MetadataRefreshOptions options, IEnumerable<FileSystemInfo> fileSystemChildren, CancellationToken cancellationToken)
         {
-            var newItems = LoadAdditionalParts(fileSystemChildren).ToList();
+            var newItems = LoadAdditionalParts(fileSystemChildren, options.DirectoryService).ToList();
 
             var newItemIds = newItems.Select(i => i.Id).ToList();
 
@@ -211,7 +211,7 @@ namespace MediaBrowser.Controller.Entities
         /// Loads the additional parts.
         /// </summary>
         /// <returns>IEnumerable{Video}.</returns>
-        private IEnumerable<Video> LoadAdditionalParts(IEnumerable<FileSystemInfo> fileSystemChildren)
+        private IEnumerable<Video> LoadAdditionalParts(IEnumerable<FileSystemInfo> fileSystemChildren, IDirectoryService directoryService)
         {
             IEnumerable<FileSystemInfo> files;
 
@@ -242,7 +242,7 @@ namespace MediaBrowser.Controller.Entities
                 });
             }
 
-            return LibraryManager.ResolvePaths<Video>(files, null).Select(video =>
+            return LibraryManager.ResolvePaths<Video>(files, directoryService, null).Select(video =>
             {
                 // Try to retrieve it from the db. If we don't find it, use the resolved version
                 var dbItem = LibraryManager.GetItemById(video.Id) as Video;

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

@@ -1,5 +1,6 @@
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Providers;
 using MediaBrowser.Controller.Resolvers;
 using MediaBrowser.Controller.Sorting;
 using MediaBrowser.Model.Entities;
@@ -27,19 +28,29 @@ namespace MediaBrowser.Controller.Library
         /// Resolves a path into a BaseItem
         /// </summary>
         /// <param name="fileInfo">The file info.</param>
+        /// <param name="directoryService">The directory service.</param>
         /// <param name="parent">The parent.</param>
         /// <returns>BaseItem.</returns>
         /// <exception cref="System.ArgumentNullException"></exception>
-        BaseItem ResolvePath(FileSystemInfo fileInfo, Folder parent = null);
+        BaseItem ResolvePath(FileSystemInfo fileInfo, IDirectoryService directoryService, Folder parent = null);
 
+        /// <summary>
+        /// Resolves the path.
+        /// </summary>
+        /// <param name="fileInfo">The file information.</param>
+        /// <param name="parent">The parent.</param>
+        /// <returns>BaseItem.</returns>
+        BaseItem ResolvePath(FileSystemInfo fileInfo, Folder parent = null);
+        
         /// <summary>
         /// Resolves a set of files into a list of BaseItem
         /// </summary>
         /// <typeparam name="T"></typeparam>
         /// <param name="files">The files.</param>
+        /// <param name="directoryService">The directory service.</param>
         /// <param name="parent">The parent.</param>
         /// <returns>List{``0}.</returns>
-        List<T> ResolvePaths<T>(IEnumerable<FileSystemInfo> files, Folder parent)
+        List<T> ResolvePaths<T>(IEnumerable<FileSystemInfo> files, IDirectoryService directoryService, Folder parent)
             where T : BaseItem;
 
         /// <summary>

+ 5 - 1
MediaBrowser.Controller/Library/ItemResolveArgs.cs

@@ -1,4 +1,5 @@
 using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Providers;
 using System;
 using System.Collections.Generic;
 using System.IO;
@@ -18,15 +19,18 @@ namespace MediaBrowser.Controller.Library
         private readonly IServerApplicationPaths _appPaths;
         private readonly ILibraryManager _libraryManager;
 
+        public IDirectoryService DirectoryService { get; private set; }
+
         /// <summary>
         /// Initializes a new instance of the <see cref="ItemResolveArgs" /> class.
         /// </summary>
         /// <param name="appPaths">The app paths.</param>
         /// <param name="libraryManager">The library manager.</param>
-        public ItemResolveArgs(IServerApplicationPaths appPaths, ILibraryManager libraryManager)
+        public ItemResolveArgs(IServerApplicationPaths appPaths, ILibraryManager libraryManager, IDirectoryService directoryService)
         {
             _appPaths = appPaths;
             _libraryManager = libraryManager;
+            DirectoryService = directoryService;
         }
 
         /// <summary>

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

@@ -109,6 +109,7 @@
     <Compile Include="Entities\LinkedChild.cs" />
     <Compile Include="Entities\MusicVideo.cs" />
     <Compile Include="Entities\IHasAwards.cs" />
+    <Compile Include="Entities\Photo.cs" />
     <Compile Include="FileOrganization\IFileOrganizationService.cs" />
     <Compile Include="Library\ILibraryPostScanTask.cs" />
     <Compile Include="Library\IMetadataSaver.cs" />

+ 1 - 10
MediaBrowser.Controller/Providers/IRemoteImageProvider.cs

@@ -24,18 +24,9 @@ namespace MediaBrowser.Controller.Providers
         /// Gets the images.
         /// </summary>
         /// <param name="item">The item.</param>
-        /// <param name="imageType">Type of the image.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <returns>Task{IEnumerable{RemoteImageInfo}}.</returns>
-        Task<IEnumerable<RemoteImageInfo>> GetImages(IHasImages item, ImageType imageType, CancellationToken cancellationToken);
-
-        /// <summary>
-        /// Gets the images.
-        /// </summary>
-        /// <param name="item">The item.</param>
-        /// <param name="cancellationToken">The cancellation token.</param>
-        /// <returns>Task{IEnumerable{RemoteImageInfo}}.</returns>
-        Task<IEnumerable<RemoteImageInfo>> GetAllImages(IHasImages item, CancellationToken cancellationToken);
+        Task<IEnumerable<RemoteImageInfo>> GetImages(IHasImages item, CancellationToken cancellationToken);
 
         /// <summary>
         /// Gets the image response.

+ 15 - 15
MediaBrowser.Controller/Resolvers/EntityResolutionHelper.cs

@@ -77,21 +77,21 @@ namespace MediaBrowser.Controller.Resolvers
         /// <summary>
         /// The audio file extensions
         /// </summary>
-        public static readonly string[] AudioFileExtensions = new[]
-            {
-                ".mp3",
-                ".flac",
-                ".wma",
-                ".aac",
-                ".acc",
-                ".m4a",
-                ".m4b",
-                ".wav",
-                ".ape",
-                ".ogg",
-                ".oga"
-
-            };
+        public static readonly string[] AudioFileExtensions =
+        {
+            ".mp3",
+            ".flac",
+            ".wma",
+            ".aac",
+            ".acc",
+            ".m4a",
+            ".m4b",
+            ".wav",
+            ".ape",
+            ".ogg",
+            ".oga"
+
+        };
 
         private static readonly Dictionary<string, string> AudioFileExtensionsDictionary = AudioFileExtensions.ToDictionary(i => i, StringComparer.OrdinalIgnoreCase);
 

+ 1 - 8
MediaBrowser.Providers/BoxSets/MovieDbBoxSetImageProvider.cs

@@ -47,14 +47,7 @@ namespace MediaBrowser.Providers.BoxSets
             };
         }
 
-        public async Task<IEnumerable<RemoteImageInfo>> GetImages(IHasImages item, ImageType imageType, CancellationToken cancellationToken)
-        {
-            var images = await GetAllImages(item, cancellationToken).ConfigureAwait(false);
-
-            return images.Where(i => i.Type == imageType);
-        }
-
-        public async Task<IEnumerable<RemoteImageInfo>> GetAllImages(IHasImages item, CancellationToken cancellationToken)
+        public async Task<IEnumerable<RemoteImageInfo>> GetImages(IHasImages item, CancellationToken cancellationToken)
         {
             var tmdbId = item.GetProviderId(MetadataProviders.Tmdb);
 

+ 1 - 6
MediaBrowser.Providers/GameGenres/GameGenreImageProvider.cs

@@ -54,12 +54,7 @@ namespace MediaBrowser.Providers.GameGenres
             };
         }
 
-        public Task<IEnumerable<RemoteImageInfo>> GetImages(IHasImages item, ImageType imageType, CancellationToken cancellationToken)
-        {
-            return GetImages(item, imageType == ImageType.Primary, imageType == ImageType.Thumb, cancellationToken);
-        }
-
-        public Task<IEnumerable<RemoteImageInfo>> GetAllImages(IHasImages item, CancellationToken cancellationToken)
+        public Task<IEnumerable<RemoteImageInfo>> GetImages(IHasImages item, CancellationToken cancellationToken)
         {
             return GetImages(item, true, true, cancellationToken);
         }

+ 1 - 6
MediaBrowser.Providers/Genres/GenreImageProvider.cs

@@ -55,12 +55,7 @@ namespace MediaBrowser.Providers.Genres
             };
         }
 
-        public Task<IEnumerable<RemoteImageInfo>> GetImages(IHasImages item, ImageType imageType, CancellationToken cancellationToken)
-        {
-            return GetImages(item, imageType == ImageType.Primary, imageType == ImageType.Thumb, cancellationToken);
-        }
-
-        public Task<IEnumerable<RemoteImageInfo>> GetAllImages(IHasImages item, CancellationToken cancellationToken)
+        public Task<IEnumerable<RemoteImageInfo>> GetImages(IHasImages item, CancellationToken cancellationToken)
         {
             return GetImages(item, true, true, cancellationToken);
         }

+ 11 - 2
MediaBrowser.Providers/Manager/MetadataService.cs

@@ -106,7 +106,10 @@ namespace MediaBrowser.Providers.Manager
 
                 if (providers.Count > 0 || !refreshResult.DateLastMetadataRefresh.HasValue)
                 {
-                    updateType = updateType | item.BeforeMetadataRefresh();
+                    if (item.BeforeMetadataRefresh())
+                    {
+                        updateType = updateType | ItemUpdateType.MetadataImport;
+                    }
                 }
 
                 if (providers.Count > 0)
@@ -416,7 +419,13 @@ namespace MediaBrowser.Providers.Manager
             // Copy new provider id's that may have been obtained
             foreach (var providerId in source.ProviderIds)
             {
-                lookupInfo.ProviderIds[providerId.Key] = providerId.Value;
+                var key = providerId.Key;
+
+                // Don't replace existing Id's.
+                if (!lookupInfo.ProviderIds.ContainsKey(key))
+                {
+                    lookupInfo.ProviderIds[key] = providerId.Value;
+                }
             }
         }
 

+ 8 - 13
MediaBrowser.Providers/Manager/ProviderManager.cs

@@ -237,32 +237,27 @@ namespace MediaBrowser.Providers.Manager
         /// </summary>
         /// <param name="item">The item.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
-        /// <param name="i">The i.</param>
+        /// <param name="provider">The provider.</param>
         /// <param name="preferredLanguage">The preferred language.</param>
         /// <param name="type">The type.</param>
         /// <returns>Task{IEnumerable{RemoteImageInfo}}.</returns>
-        private async Task<IEnumerable<RemoteImageInfo>> GetImages(IHasImages item, CancellationToken cancellationToken, IRemoteImageProvider i, string preferredLanguage, ImageType? type = null)
+        private async Task<IEnumerable<RemoteImageInfo>> GetImages(IHasImages item, CancellationToken cancellationToken, IRemoteImageProvider provider, string preferredLanguage, ImageType? type = null)
         {
             try
             {
+                var result = await provider.GetImages(item, cancellationToken).ConfigureAwait(false);
+
                 if (type.HasValue)
                 {
-                    var result = await i.GetImages(item, type.Value, cancellationToken).ConfigureAwait(false);
-
-                    return string.IsNullOrEmpty(preferredLanguage) ? result :
-                        FilterImages(result, preferredLanguage);
+                    result = result.Where(i => i.Type == type.Value);
                 }
-                else
-                {
-                    var result = await i.GetAllImages(item, cancellationToken).ConfigureAwait(false);
 
-                    return string.IsNullOrEmpty(preferredLanguage) ? result :
-                        FilterImages(result, preferredLanguage);
-                }
+                return string.IsNullOrEmpty(preferredLanguage) ? result :
+                    FilterImages(result, preferredLanguage);
             }
             catch (Exception ex)
             {
-                _logger.ErrorException("{0} failed in GetImageInfos for type {1}", ex, i.GetType().Name, item.GetType().Name);
+                _logger.ErrorException("{0} failed in GetImageInfos for type {1}", ex, provider.GetType().Name, item.GetType().Name);
                 return new List<RemoteImageInfo>();
             }
         }

+ 7 - 1
MediaBrowser.Providers/Manager/ProviderUtils.cs

@@ -157,7 +157,13 @@ namespace MediaBrowser.Providers.Manager
 
             foreach (var id in source.ProviderIds)
             {
-                target.ProviderIds[id.Key] = id.Value;
+                var key = id.Key;
+
+                // Don't replace existing Id's.
+                if (!target.ProviderIds.ContainsKey(key))
+                {
+                    target.ProviderIds[key] = id.Value;
+                }
             }
 
             MergeAlbumArtist(source, target, lockedFields, replaceData);

+ 5 - 0
MediaBrowser.Providers/MediaBrowser.Providers.csproj

@@ -151,6 +151,11 @@
     <Compile Include="People\PersonMetadataService.cs" />
     <Compile Include="People\PersonXmlProvider.cs" />
     <Compile Include="People\MovieDbPersonProvider.cs" />
+    <Compile Include="Photos\ExifReader.cs" />
+    <Compile Include="Photos\ExifTags.cs" />
+    <Compile Include="Photos\PhotoHelper.cs" />
+    <Compile Include="Photos\PhotoMetadataService.cs" />
+    <Compile Include="Photos\PhotoProvider.cs" />
     <Compile Include="Properties\AssemblyInfo.cs" />
     <Compile Include="Manager\ProviderUtils.cs" />
     <Compile Include="Savers\AlbumXmlSaver.cs" />

+ 1 - 8
MediaBrowser.Providers/Movies/FanartMovieImageProvider.cs

@@ -78,14 +78,7 @@ namespace MediaBrowser.Providers.Movies
             };
         }
 
-        public async Task<IEnumerable<RemoteImageInfo>> GetImages(IHasImages item, ImageType imageType, CancellationToken cancellationToken)
-        {
-            var images = await GetAllImages(item, cancellationToken).ConfigureAwait(false);
-
-            return images.Where(i => i.Type == imageType);
-        }
-
-        public async Task<IEnumerable<RemoteImageInfo>> GetAllImages(IHasImages item, CancellationToken cancellationToken)
+        public async Task<IEnumerable<RemoteImageInfo>> GetImages(IHasImages item, CancellationToken cancellationToken)
         {
             var baseItem = (BaseItem)item;
             var list = new List<RemoteImageInfo>();

+ 1 - 8
MediaBrowser.Providers/Movies/MovieDbImageProvider.cs

@@ -58,14 +58,7 @@ namespace MediaBrowser.Providers.Movies
             };
         }
 
-        public async Task<IEnumerable<RemoteImageInfo>> GetImages(IHasImages item, ImageType imageType, CancellationToken cancellationToken)
-        {
-            var images = await GetAllImages(item, cancellationToken).ConfigureAwait(false);
-
-            return images.Where(i => i.Type == imageType);
-        }
-
-        public async Task<IEnumerable<RemoteImageInfo>> GetAllImages(IHasImages item, CancellationToken cancellationToken)
+        public async Task<IEnumerable<RemoteImageInfo>> GetImages(IHasImages item, CancellationToken cancellationToken)
         {
             var list = new List<RemoteImageInfo>();
 

+ 20 - 4
MediaBrowser.Providers/Movies/MovieDbSearch.cs

@@ -46,11 +46,8 @@ namespace MediaBrowser.Providers.Movies
 
         private async Task<string> FindId(ItemLookupInfo idInfo, string searchType, CancellationToken cancellationToken)
         {
-            int? yearInName;
             var name = idInfo.Name;
-            NameParser.ParseName(name, out name, out yearInName);
-
-            var year = idInfo.Year ?? yearInName;
+            var year = idInfo.Year;
 
             _logger.Info("MovieDbProvider: Finding id for item: " + name);
             var language = idInfo.MetadataLanguage.ToLower();
@@ -266,5 +263,24 @@ namespace MediaBrowser.Providers.Movies
             public int total_results { get; set; }
         }
 
+        public class TvResult
+        {
+            public string backdrop_path { get; set; }
+            public int id { get; set; }
+            public string original_name { get; set; }
+            public string first_air_date { get; set; }
+            public string poster_path { get; set; }
+            public double popularity { get; set; }
+            public string name { get; set; }
+            public double vote_average { get; set; }
+            public int vote_count { get; set; }
+        }
+
+        public class ExternalIdLookupResult
+        {
+            public List<object> movie_results { get; set; }
+            public List<object> person_results { get; set; }
+            public List<TvResult> tv_results { get; set; }
+        }
     }
 }

+ 1 - 8
MediaBrowser.Providers/Music/AudioDbAlbumImageProvider.cs

@@ -35,14 +35,7 @@ namespace MediaBrowser.Providers.Music
             };
         }
 
-        public async Task<IEnumerable<RemoteImageInfo>> GetImages(IHasImages item, ImageType imageType, CancellationToken cancellationToken)
-        {
-            var images = await GetAllImages(item, cancellationToken).ConfigureAwait(false);
-
-            return images.Where(i => i.Type == imageType);
-        }
-
-        public async Task<IEnumerable<RemoteImageInfo>> GetAllImages(IHasImages item, CancellationToken cancellationToken)
+        public async Task<IEnumerable<RemoteImageInfo>> GetImages(IHasImages item, CancellationToken cancellationToken)
         {
             var id = item.GetProviderId(MetadataProviders.MusicBrainzReleaseGroup);
 

+ 1 - 8
MediaBrowser.Providers/Music/AudioDbArtistImageProvider.cs

@@ -37,14 +37,7 @@ namespace MediaBrowser.Providers.Music
             };
         }
 
-        public async Task<IEnumerable<RemoteImageInfo>> GetImages(IHasImages item, ImageType imageType, CancellationToken cancellationToken)
-        {
-            var images = await GetAllImages(item, cancellationToken).ConfigureAwait(false);
-
-            return images.Where(i => i.Type == imageType);
-        }
-
-        public async Task<IEnumerable<RemoteImageInfo>> GetAllImages(IHasImages item, CancellationToken cancellationToken)
+        public async Task<IEnumerable<RemoteImageInfo>> GetImages(IHasImages item, CancellationToken cancellationToken)
         {
             var id = item.GetProviderId(MetadataProviders.MusicBrainzArtist);
 

+ 1 - 8
MediaBrowser.Providers/Music/FanArtAlbumProvider.cs

@@ -57,14 +57,7 @@ namespace MediaBrowser.Providers.Music
             };
         }
 
-        public async Task<IEnumerable<RemoteImageInfo>> GetImages(IHasImages item, ImageType imageType, CancellationToken cancellationToken)
-        {
-            var images = await GetAllImages(item, cancellationToken).ConfigureAwait(false);
-
-            return images.Where(i => i.Type == imageType);
-        }
-
-        public async Task<IEnumerable<RemoteImageInfo>> GetAllImages(IHasImages item, CancellationToken cancellationToken)
+        public async Task<IEnumerable<RemoteImageInfo>> GetImages(IHasImages item, CancellationToken cancellationToken)
         {
             var album = (MusicAlbum)item;
 

+ 1 - 8
MediaBrowser.Providers/Music/FanArtArtistProvider.cs

@@ -69,14 +69,7 @@ namespace MediaBrowser.Providers.Music
             };
         }
 
-        public async Task<IEnumerable<RemoteImageInfo>> GetImages(IHasImages item, ImageType imageType, CancellationToken cancellationToken)
-        {
-            var images = await GetAllImages(item, cancellationToken).ConfigureAwait(false);
-
-            return images.Where(i => i.Type == imageType);
-        }
-
-        public async Task<IEnumerable<RemoteImageInfo>> GetAllImages(IHasImages item, CancellationToken cancellationToken)
+        public async Task<IEnumerable<RemoteImageInfo>> GetImages(IHasImages item, CancellationToken cancellationToken)
         {
             var artist = (MusicArtist)item;
 

+ 1 - 8
MediaBrowser.Providers/Music/LastFmImageProvider.cs

@@ -48,14 +48,7 @@ namespace MediaBrowser.Providers.Music
             };
         }
 
-        public async Task<IEnumerable<RemoteImageInfo>> GetImages(IHasImages item, ImageType imageType, CancellationToken cancellationToken)
-        {
-            var images = await GetAllImages(item, cancellationToken).ConfigureAwait(false);
-
-            return images.Where(i => i.Type == imageType);
-        }
-
-        public Task<IEnumerable<RemoteImageInfo>> GetAllImages(IHasImages item, CancellationToken cancellationToken)
+        public Task<IEnumerable<RemoteImageInfo>> GetImages(IHasImages item, CancellationToken cancellationToken)
         {
             var list = new List<RemoteImageInfo>();
 

+ 1 - 6
MediaBrowser.Providers/MusicGenres/MusicGenreImageProvider.cs

@@ -55,12 +55,7 @@ namespace MediaBrowser.Providers.MusicGenres
             };
         }
 
-        public Task<IEnumerable<RemoteImageInfo>> GetImages(IHasImages item, ImageType imageType, CancellationToken cancellationToken)
-        {
-            return GetImages(item, imageType == ImageType.Primary, imageType == ImageType.Thumb, cancellationToken);
-        }
-
-        public Task<IEnumerable<RemoteImageInfo>> GetAllImages(IHasImages item, CancellationToken cancellationToken)
+        public Task<IEnumerable<RemoteImageInfo>> GetImages(IHasImages item, CancellationToken cancellationToken)
         {
             return GetImages(item, true, true, cancellationToken);
         }

+ 0 - 2
MediaBrowser.Providers/MusicGenres/MusicGenreMetadataService.cs

@@ -7,8 +7,6 @@ using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Logging;
 using MediaBrowser.Providers.Manager;
 using System.Collections.Generic;
-using System.Threading;
-using System.Threading.Tasks;
 
 namespace MediaBrowser.Providers.MusicGenres
 {

+ 1 - 8
MediaBrowser.Providers/People/MovieDbPersonImageProvider.cs

@@ -50,14 +50,7 @@ namespace MediaBrowser.Providers.People
             };
         }
 
-        public async Task<IEnumerable<RemoteImageInfo>> GetImages(IHasImages item, ImageType imageType, CancellationToken cancellationToken)
-        {
-            var images = await GetAllImages(item, cancellationToken).ConfigureAwait(false);
-
-            return images.Where(i => i.Type == imageType);
-        }
-
-        public async Task<IEnumerable<RemoteImageInfo>> GetAllImages(IHasImages item, CancellationToken cancellationToken)
+        public async Task<IEnumerable<RemoteImageInfo>> GetImages(IHasImages item, CancellationToken cancellationToken)
         {
             var person = (Person)item;
             var id = person.GetProviderId(MetadataProviders.Tmdb);

+ 1 - 8
MediaBrowser.Providers/People/TvdbPersonImageProvider.cs

@@ -54,14 +54,7 @@ namespace MediaBrowser.Providers.People
             };
         }
 
-        public async Task<IEnumerable<RemoteImageInfo>> GetImages(IHasImages item, ImageType imageType, CancellationToken cancellationToken)
-        {
-            var images = await GetAllImages(item, cancellationToken).ConfigureAwait(false);
-
-            return images.Where(i => i.Type == imageType);
-        }
-
-        public Task<IEnumerable<RemoteImageInfo>> GetAllImages(IHasImages item, CancellationToken cancellationToken)
+        public Task<IEnumerable<RemoteImageInfo>> GetImages(IHasImages item, CancellationToken cancellationToken)
         {
             var seriesWithPerson = _library.RootFolder
                 .RecursiveChildren

+ 613 - 0
MediaBrowser.Providers/Photos/ExifReader.cs

@@ -0,0 +1,613 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Text;
+
+namespace MediaBrowser.Providers.Photos
+{
+    /// <summary>
+    /// A class for reading Exif data from a JPEG file. The file will be open for reading for as long as the class exists.
+    /// <seealso cref="http://gvsoft.homedns.org/exif/Exif-explanation.html"/>
+    /// </summary>
+    public class ExifReader : IDisposable
+    {
+        private readonly FileStream fileStream = null;
+        private readonly BinaryReader reader = null;
+
+        /// <summary>
+        /// The catalogue of tag ids and their absolute offsets within the
+        /// file
+        /// </summary>
+        private Dictionary<ushort, long> catalogue;
+
+        /// <summary>
+        /// Indicates whether to read data using big or little endian byte aligns
+        /// </summary>
+        private bool isLittleEndian;
+
+        /// <summary>
+        /// The position in the filestream at which the TIFF header starts
+        /// </summary>
+        private long tiffHeaderStart;
+
+        public ExifReader(string fileName)
+        {
+            // JPEG encoding uses big endian (i.e. Motorola) byte aligns. The TIFF encoding
+            // found later in the document will specify the byte aligns used for the
+            // rest of the document.
+            isLittleEndian = false;
+
+            try
+            {
+                // Open the file in a stream
+                fileStream = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
+                reader = new BinaryReader(fileStream);
+
+                // Make sure the file's a JPEG.
+                if (ReadUShort() != 0xFFD8)
+                    throw new Exception("File is not a valid JPEG");
+
+                // Scan to the start of the Exif content
+                ReadToExifStart();
+
+                // Create an index of all Exif tags found within the document
+                CreateTagIndex();
+            }
+            catch (Exception)
+            {
+                // If instantiation fails, make sure there's no mess left behind
+                Dispose();
+
+                throw;
+            }
+        }
+
+        #region TIFF methods
+
+        /// <summary>
+        /// Returns the length (in bytes) per component of the specified TIFF data type
+        /// </summary>
+        /// <returns></returns>
+        private byte GetTIFFFieldLength(ushort tiffDataType)
+        {
+            switch (tiffDataType)
+            {
+                case 1:
+                case 2:
+                case 6:
+                    return 1;
+                case 3:
+                case 8:
+                    return 2;
+                case 4:
+                case 7:
+                case 9:
+                case 11:
+                    return 4;
+                case 5:
+                case 10:
+                case 12:
+                    return 8;
+                default:
+                    throw new Exception(string.Format("Unknown TIFF datatype: {0}", tiffDataType));
+            }
+        }
+
+        #endregion
+
+        #region Methods for reading data directly from the filestream
+
+        /// <summary>
+        /// Gets a 2 byte unsigned integer from the file
+        /// </summary>
+        /// <returns></returns>
+        private ushort ReadUShort()
+        {
+            return ToUShort(ReadBytes(2));
+        }
+
+        /// <summary>
+        /// Gets a 4 byte unsigned integer from the file
+        /// </summary>
+        /// <returns></returns>
+        private uint ReadUint()
+        {
+            return ToUint(ReadBytes(4));
+        }
+
+        private string ReadString(int chars)
+        {
+            return Encoding.ASCII.GetString(ReadBytes(chars));
+        }
+
+        private byte[] ReadBytes(int byteCount)
+        {
+            return reader.ReadBytes(byteCount);
+        }
+
+        /// <summary>
+        /// Reads some bytes from the specified TIFF offset
+        /// </summary>
+        /// <param name="tiffOffset"></param>
+        /// <param name="byteCount"></param>
+        /// <returns></returns>
+        private byte[] ReadBytes(ushort tiffOffset, int byteCount)
+        {
+            // Keep the current file offset
+            long originalOffset = fileStream.Position;
+
+            // Move to the TIFF offset and retrieve the data
+            fileStream.Seek(tiffOffset + tiffHeaderStart, SeekOrigin.Begin);
+
+            byte[] data = reader.ReadBytes(byteCount);
+
+            // Restore the file offset
+            fileStream.Position = originalOffset;
+
+            return data;
+        }
+
+        #endregion
+
+        #region Data conversion methods for interpreting datatypes from a byte array
+
+        /// <summary>
+        /// Converts 2 bytes to a ushort using the current byte aligns
+        /// </summary>
+        /// <returns></returns>
+        private ushort ToUShort(byte[] data)
+        {
+            if (isLittleEndian != BitConverter.IsLittleEndian)
+                Array.Reverse(data);
+
+            return BitConverter.ToUInt16(data, 0);
+        }
+
+        /// <summary>
+        /// Converts 8 bytes to an unsigned rational using the current byte aligns.
+        /// </summary>
+        /// <param name="data"></param>
+        /// <returns></returns>
+        /// <seealso cref="ToRational"/>
+        private double ToURational(byte[] data)
+        {
+            var numeratorData = new byte[4];
+            var denominatorData = new byte[4];
+
+            Array.Copy(data, numeratorData, 4);
+            Array.Copy(data, 4, denominatorData, 0, 4);
+
+            uint numerator = ToUint(numeratorData);
+            uint denominator = ToUint(denominatorData);
+
+            return numerator / (double)denominator;
+        }
+
+        /// <summary>
+        /// Converts 8 bytes to a signed rational using the current byte aligns.
+        /// </summary>
+        /// <remarks>
+        /// A TIFF rational contains 2 4-byte integers, the first of which is
+        /// the numerator, and the second of which is the denominator.
+        /// </remarks>
+        /// <param name="data"></param>
+        /// <returns></returns>
+        private double ToRational(byte[] data)
+        {
+            var numeratorData = new byte[4];
+            var denominatorData = new byte[4];
+
+            Array.Copy(data, numeratorData, 4);
+            Array.Copy(data, 4, denominatorData, 0, 4);
+
+            int numerator = ToInt(numeratorData);
+            int denominator = ToInt(denominatorData);
+
+            return numerator / (double)denominator;
+        }
+
+        /// <summary>
+        /// Converts 4 bytes to a uint using the current byte aligns
+        /// </summary>
+        /// <returns></returns>
+        private uint ToUint(byte[] data)
+        {
+            if (isLittleEndian != BitConverter.IsLittleEndian)
+                Array.Reverse(data);
+
+            return BitConverter.ToUInt32(data, 0);
+        }
+
+        /// <summary>
+        /// Converts 4 bytes to an int using the current byte aligns
+        /// </summary>
+        /// <returns></returns>
+        private int ToInt(byte[] data)
+        {
+            if (isLittleEndian != BitConverter.IsLittleEndian)
+                Array.Reverse(data);
+
+            return BitConverter.ToInt32(data, 0);
+        }
+
+        private double ToDouble(byte[] data)
+        {
+            if (isLittleEndian != BitConverter.IsLittleEndian)
+                Array.Reverse(data);
+
+            return BitConverter.ToDouble(data, 0);
+        }
+
+        private float ToSingle(byte[] data)
+        {
+            if (isLittleEndian != BitConverter.IsLittleEndian)
+                Array.Reverse(data);
+
+            return BitConverter.ToSingle(data, 0);
+        }
+
+        private short ToShort(byte[] data)
+        {
+            if (isLittleEndian != BitConverter.IsLittleEndian)
+                Array.Reverse(data);
+
+            return BitConverter.ToInt16(data, 0);
+        }
+
+        private sbyte ToSByte(byte[] data)
+        {
+            // An sbyte should just be a byte with an offset range.
+            return (sbyte)(data[0] - byte.MaxValue);
+        }
+
+        /// <summary>
+        /// Retrieves an array from a byte array using the supplied converter
+        /// to read each individual element from the supplied byte array
+        /// </summary>
+        /// <param name="data"></param>
+        /// <param name="elementLengthBytes"></param>
+        /// <param name="converter"></param>
+        /// <returns></returns>
+        private Array GetArray<T>(byte[] data, int elementLengthBytes, ConverterMethod<T> converter)
+        {
+            Array convertedData = Array.CreateInstance(typeof(T), data.Length / elementLengthBytes);
+
+            var buffer = new byte[elementLengthBytes];
+
+            // Read each element from the array
+            for (int elementCount = 0; elementCount < data.Length / elementLengthBytes; elementCount++)
+            {
+                // Place the data for the current element into the buffer
+                Array.Copy(data, elementCount * elementLengthBytes, buffer, 0, elementLengthBytes);
+
+                // Process the data and place it into the output array
+                convertedData.SetValue(converter(buffer), elementCount);
+            }
+
+            return convertedData;
+        }
+
+        /// <summary>
+        /// A delegate used to invoke any of the data conversion methods
+        /// </summary>
+        /// <param name="data"></param>
+        /// <returns></returns>
+        private delegate T ConverterMethod<out T>(byte[] data);
+
+        #endregion
+
+        #region Stream seek methods - used to get to locations within the JPEG
+
+        /// <summary>
+        /// Scans to the Exif block
+        /// </summary>
+        private void ReadToExifStart()
+        {
+            // The file has a number of blocks (Exif/JFIF), each of which
+            // has a tag number followed by a length. We scan the document until the required tag (0xFFE1)
+            // is found. All tags start with FF, so a non FF tag indicates an error.
+
+            // Get the next tag.
+            byte markerStart;
+            byte markerNumber = 0;
+            while (((markerStart = reader.ReadByte()) == 0xFF) && (markerNumber = reader.ReadByte()) != 0xE1)
+            {
+                // Get the length of the data.
+                ushort dataLength = ReadUShort();
+
+                // Jump to the end of the data (note that the size field includes its own size)!
+                reader.BaseStream.Seek(dataLength - 2, SeekOrigin.Current);
+            }
+
+            // It's only success if we found the 0xFFE1 marker
+            if (markerStart != 0xFF || markerNumber != 0xE1)
+                throw new Exception("Could not find Exif data block");
+        }
+
+        /// <summary>
+        /// Reads through the Exif data and builds an index of all Exif tags in the document
+        /// </summary>
+        /// <returns></returns>
+        private void CreateTagIndex()
+        {
+            // The next 4 bytes are the size of the Exif data.
+            ReadUShort();
+
+            // Next is the Exif data itself. It starts with the ASCII "Exif" followed by 2 zero bytes.
+            if (ReadString(4) != "Exif")
+                throw new Exception("Exif data not found");
+
+            // 2 zero bytes
+            if (ReadUShort() != 0)
+                throw new Exception("Malformed Exif data");
+
+            // We're now into the TIFF format
+            tiffHeaderStart = reader.BaseStream.Position;
+
+            // What byte align will be used for the TIFF part of the document? II for Intel, MM for Motorola
+            isLittleEndian = ReadString(2) == "II";
+
+            // Next 2 bytes are always the same.
+            if (ReadUShort() != 0x002A)
+                throw new Exception("Error in TIFF data");
+
+            // Get the offset to the IFD (image file directory)
+            uint ifdOffset = ReadUint();
+
+            // Note that this offset is from the first byte of the TIFF header. Jump to the IFD.
+            fileStream.Position = ifdOffset + tiffHeaderStart;
+
+            // Catalogue this first IFD (there will be another IFD)
+            CatalogueIFD();
+
+            // There's more data stored in the subifd, the offset to which is found in tag 0x8769.
+            // As with all TIFF offsets, it will be relative to the first byte of the TIFF header.
+            uint offset;
+            if (!GetTagValue(0x8769, out offset))
+                throw new Exception("Unable to locate Exif data");
+
+            // Jump to the exif SubIFD
+            fileStream.Position = offset + tiffHeaderStart;
+
+            // Add the subIFD to the catalogue too
+            CatalogueIFD();
+
+            // Go to the GPS IFD and catalogue that too. It's an optional
+            // section.
+            if (GetTagValue(0x8825, out offset))
+            {
+                // Jump to the GPS SubIFD
+                fileStream.Position = offset + tiffHeaderStart;
+
+                // Add the subIFD to the catalogue too
+                CatalogueIFD();
+            }
+        }
+
+        #endregion
+
+        #region Exif data catalog and retrieval methods
+
+        public bool GetTagValue<T>(ExifTags tag, out T result)
+        {
+            return GetTagValue((ushort)tag, out result);
+        }
+
+        /// <summary>
+        /// Retrieves an Exif value with the requested tag ID
+        /// </summary>
+        /// <param name="tagID"></param>
+        /// <param name="result"></param>
+        /// <returns></returns>
+        public bool GetTagValue<T>(ushort tagID, out T result)
+        {
+            ushort tiffDataType;
+            uint numberOfComponents;
+            byte[] tagData = GetTagBytes(tagID, out tiffDataType, out numberOfComponents);
+
+            if (tagData == null)
+            {
+                result = default(T);
+                return false;
+            }
+
+            byte fieldLength = GetTIFFFieldLength(tiffDataType);
+
+            // Convert the data to the appropriate datatype. Note the weird boxing via object.
+            // The compiler doesn't like it otherwise.
+            switch (tiffDataType)
+            {
+                case 1:
+                    // unsigned byte
+                    if (numberOfComponents == 1)
+                        result = (T)(object)tagData[0];
+                    else
+                        result = (T)(object)tagData;
+                    return true;
+                case 2:
+                    // ascii string
+                    string str = Encoding.ASCII.GetString(tagData);
+
+                    // There may be a null character within the string
+                    int nullCharIndex = str.IndexOf('\0');
+                    if (nullCharIndex != -1)
+                        str = str.Substring(0, nullCharIndex);
+
+                    // Special processing for dates.
+                    if (typeof(T) == typeof(DateTime))
+                    {
+                        result =
+                            (T)(object)DateTime.ParseExact(str, "yyyy:MM:dd HH:mm:ss", CultureInfo.InvariantCulture);
+                        return true;
+                    }
+
+                    result = (T)(object)str;
+                    return true;
+                case 3:
+                    // unsigned short
+                    if (numberOfComponents == 1)
+                        result = (T)(object)ToUShort(tagData);
+                    else
+                        result = (T)(object)GetArray(tagData, fieldLength, ToUShort);
+                    return true;
+                case 4:
+                    // unsigned long
+                    if (numberOfComponents == 1)
+                        result = (T)(object)ToUint(tagData);
+                    else
+                        result = (T)(object)GetArray(tagData, fieldLength, ToUint);
+                    return true;
+                case 5:
+                    // unsigned rational
+                    if (numberOfComponents == 1)
+                        result = (T)(object)ToURational(tagData);
+                    else
+                        result = (T)(object)GetArray(tagData, fieldLength, ToURational);
+                    return true;
+                case 6:
+                    // signed byte
+                    if (numberOfComponents == 1)
+                        result = (T)(object)ToSByte(tagData);
+                    else
+                        result = (T)(object)GetArray(tagData, fieldLength, ToSByte);
+                    return true;
+                case 7:
+                    // undefined. Treat it as an unsigned integer.
+                    if (numberOfComponents == 1)
+                        result = (T)(object)ToUint(tagData);
+                    else
+                        result = (T)(object)GetArray(tagData, fieldLength, ToUint);
+                    return true;
+                case 8:
+                    // Signed short
+                    if (numberOfComponents == 1)
+                        result = (T)(object)ToShort(tagData);
+                    else
+                        result = (T)(object)GetArray(tagData, fieldLength, ToShort);
+                    return true;
+                case 9:
+                    // Signed long
+                    if (numberOfComponents == 1)
+                        result = (T)(object)ToInt(tagData);
+                    else
+                        result = (T)(object)GetArray(tagData, fieldLength, ToInt);
+                    return true;
+                case 10:
+                    // signed rational
+                    if (numberOfComponents == 1)
+                        result = (T)(object)ToRational(tagData);
+                    else
+                        result = (T)(object)GetArray(tagData, fieldLength, ToRational);
+                    return true;
+                case 11:
+                    // single float
+                    if (numberOfComponents == 1)
+                        result = (T)(object)ToSingle(tagData);
+                    else
+                        result = (T)(object)GetArray(tagData, fieldLength, ToSingle);
+                    return true;
+                case 12:
+                    // double float
+                    if (numberOfComponents == 1)
+                        result = (T)(object)ToDouble(tagData);
+                    else
+                        result = (T)(object)GetArray(tagData, fieldLength, ToDouble);
+                    return true;
+                default:
+                    throw new Exception(string.Format("Unknown TIFF datatype: {0}", tiffDataType));
+            }
+        }
+
+        /// <summary>
+        /// Gets the data in the specified tag ID, starting from before the IFD block.
+        /// </summary>
+        /// <param name="tiffDataType"></param>
+        /// <param name="numberOfComponents">The number of items which make up the data item - i.e. for a string, this will be the
+        /// number of characters in the string</param>
+        /// <param name="tagID"></param>
+        private byte[] GetTagBytes(ushort tagID, out ushort tiffDataType, out uint numberOfComponents)
+        {
+            // Get the tag's offset from the catalogue and do some basic error checks
+            if (fileStream == null || reader == null || catalogue == null || !catalogue.ContainsKey(tagID))
+            {
+                tiffDataType = 0;
+                numberOfComponents = 0;
+                return null;
+            }
+
+            long tagOffset = catalogue[tagID];
+
+            // Jump to the TIFF offset
+            fileStream.Position = tagOffset;
+
+            // Read the tag number from the file
+            ushort currentTagID = ReadUShort();
+
+            if (currentTagID != tagID)
+                throw new Exception("Tag number not at expected offset");
+
+            // Read the offset to the Exif IFD
+            tiffDataType = ReadUShort();
+            numberOfComponents = ReadUint();
+            byte[] tagData = ReadBytes(4);
+
+            // If the total space taken up by the field is longer than the
+            // 2 bytes afforded by the tagData, tagData will contain an offset
+            // to the actual data.
+            var dataSize = (int)(numberOfComponents * GetTIFFFieldLength(tiffDataType));
+
+            if (dataSize > 4)
+            {
+                ushort offsetAddress = ToUShort(tagData);
+                return ReadBytes(offsetAddress, dataSize);
+            }
+
+            // The value is stored in the tagData starting from the left
+            Array.Resize(ref tagData, dataSize);
+
+            return tagData;
+        }
+
+        /// <summary>
+        /// Records all Exif tags and their offsets within
+        /// the file from the current IFD
+        /// </summary>
+        private void CatalogueIFD()
+        {
+            if (catalogue == null)
+                catalogue = new Dictionary<ushort, long>();
+
+            // Assume we're just before the IFD.
+
+            // First 2 bytes is the number of entries in this IFD
+            ushort entryCount = ReadUShort();
+
+            for (ushort currentEntry = 0; currentEntry < entryCount; currentEntry++)
+            {
+                ushort currentTagNumber = ReadUShort();
+
+                // Record this in the catalogue
+                catalogue[currentTagNumber] = fileStream.Position - 2;
+
+                // Go to the end of this item (10 bytes, as each entry is 12 bytes long)
+                reader.BaseStream.Seek(10, SeekOrigin.Current);
+            }
+        }
+
+        #endregion
+
+        #region IDisposable Members
+
+        public void Dispose()
+        {
+            // Make sure the file handle is released
+            if (reader != null)
+                reader.Close();
+            if (fileStream != null)
+                fileStream.Close();
+        }
+
+        #endregion
+    }
+}

+ 132 - 0
MediaBrowser.Providers/Photos/ExifTags.cs

@@ -0,0 +1,132 @@
+
+namespace MediaBrowser.Providers.Photos
+{
+    /// <summary>
+    /// All exif tags as per the Exif standard 2.2, JEITA CP-2451
+    /// </summary>
+    public enum ExifTags : ushort
+    {
+        // IFD0 items
+        ImageWidth = 0x100,
+        ImageLength = 0x101,
+        BitsPerSample = 0x102,
+        Compression = 0x103,
+        PhotometricInterpretation = 0x106,
+        ImageDescription = 0x10E,
+        Make = 0x10F,
+        Model = 0x110,
+        StripOffsets = 0x111,
+        Orientation = 0x112,
+        SamplesPerPixel = 0x115,
+        RowsPerStrip = 0x116,
+        StripByteCounts = 0x117,
+        XResolution = 0x11A,
+        YResolution = 0x11B,
+        PlanarConfiguration = 0x11C,
+        ResolutionUnit = 0x128,
+        TransferFunction = 0x12D,
+        Software = 0x131,
+        DateTime = 0x132,
+        Artist = 0x13B,
+        WhitePoint = 0x13E,
+        PrimaryChromaticities = 0x13F,
+        JPEGInterchangeFormat = 0x201,
+        JPEGInterchangeFormatLength = 0x202,
+        YCbCrCoefficients = 0x211,
+        YCbCrSubSampling = 0x212,
+        YCbCrPositioning = 0x213,
+        ReferenceBlackWhite = 0x214,
+        Copyright = 0x8298,
+
+        // SubIFD items
+        ExposureTime = 0x829A,
+        FNumber = 0x829D,
+        ExposureProgram = 0x8822,
+        SpectralSensitivity = 0x8824,
+        ISOSpeedRatings = 0x8827,
+        OECF = 0x8828,
+        ExifVersion = 0x9000,
+        DateTimeOriginal = 0x9003,
+        DateTimeDigitized = 0x9004,
+        ComponentsConfiguration = 0x9101,
+        CompressedBitsPerPixel = 0x9102,
+        ShutterSpeedValue = 0x9201,
+        ApertureValue = 0x9202,
+        BrightnessValue = 0x9203,
+        ExposureBiasValue = 0x9204,
+        MaxApertureValue = 0x9205,
+        SubjectDistance = 0x9206,
+        MeteringMode = 0x9207,
+        LightSource = 0x9208,
+        Flash = 0x9209,
+        FocalLength = 0x920A,
+        SubjectArea = 0x9214,
+        MakerNote = 0x927C,
+        UserComment = 0x9286,
+        SubsecTime = 0x9290,
+        SubsecTimeOriginal = 0x9291,
+        SubsecTimeDigitized = 0x9292,
+        FlashpixVersion = 0xA000,
+        ColorSpace = 0xA001,
+        PixelXDimension = 0xA002,
+        PixelYDimension = 0xA003,
+        RelatedSoundFile = 0xA004,
+        FlashEnergy = 0xA20B,
+        SpatialFrequencyResponse = 0xA20C,
+        FocalPlaneXResolution = 0xA20E,
+        FocalPlaneYResolution = 0xA20F,
+        FocalPlaneResolutionUnit = 0xA210,
+        SubjectLocation = 0xA214,
+        ExposureIndex = 0xA215,
+        SensingMethod = 0xA217,
+        FileSource = 0xA300,
+        SceneType = 0xA301,
+        CFAPattern = 0xA302,
+        CustomRendered = 0xA401,
+        ExposureMode = 0xA402,
+        WhiteBalance = 0xA403,
+        DigitalZoomRatio = 0xA404,
+        FocalLengthIn35mmFilm = 0xA405,
+        SceneCaptureType = 0xA406,
+        GainControl = 0xA407,
+        Contrast = 0xA408,
+        Saturation = 0xA409,
+        Sharpness = 0xA40A,
+        DeviceSettingDescription = 0xA40B,
+        SubjectDistanceRange = 0xA40C,
+        ImageUniqueID = 0xA420,
+
+        // GPS subifd items
+        GPSVersionID = 0x0,
+        GPSLatitudeRef = 0x1,
+        GPSLatitude = 0x2,
+        GPSLongitudeRef = 0x3,
+        GPSLongitude = 0x4,
+        GPSAltitudeRef = 0x5,
+        GPSAltitude = 0x6,
+        GPSTimeStamp = 0x7,
+        GPSSatellites = 0x8,
+        GPSStatus = 0x9,
+        GPSMeasureMode = 0xA,
+        GPSDOP = 0xB,
+        GPSSpeedRef = 0xC,
+        GPSSpeed = 0xD,
+        GPSTrackRef = 0xE,
+        GPSTrack = 0xF,
+        GPSImgDirectionRef = 0x10,
+        GPSImgDirection = 0x11,
+        GPSMapDatum = 0x12,
+        GPSDestLatitudeRef = 0x13,
+        GPSDestLatitude = 0x14,
+        GPSDestLongitudeRef = 0x15,
+        GPSDestLongitude = 0x16,
+        GPSDestBearingRef = 0x17,
+        GPSDestBearing = 0x18,
+        GPSDestDistanceRef = 0x19,
+        GPSDestDistance = 0x1A,
+        GPSProcessingMethod = 0x1B,
+        GPSAreaInformation = 0x1C,
+        GPSDateStamp = 0x1D,
+        GPSDifferential = 0x1E
+    }
+}

+ 113 - 0
MediaBrowser.Providers/Photos/PhotoHelper.cs

@@ -0,0 +1,113 @@
+using MediaBrowser.Controller.Entities;
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace MediaBrowser.Providers.Photos
+{
+    public static class PhotoHelper
+    {
+        public static List<BaseItem> ShuffleList(List<BaseItem> list)
+        {
+            var rnd = new Random(DateTime.Now.Second);
+            for (var i = 1; i < list.Count; i++)
+            {
+                var pos = rnd.Next(i + 1);
+                var x = list[i];
+                list[i] = list[pos];
+                list[pos] = x;
+            }
+            return list;
+        }
+
+        public static string Dec2Frac(double dbl)
+        {
+            char neg = ' ';
+            double dblDecimal = dbl;
+            if (dblDecimal == (int)dblDecimal) return dblDecimal.ToString(); //return no if it's not a decimal
+            if (dblDecimal < 0)
+            {
+                dblDecimal = Math.Abs(dblDecimal);
+                neg = '-';
+            }
+            var whole = (int)Math.Truncate(dblDecimal);
+            string decpart = dblDecimal.ToString().Replace(Math.Truncate(dblDecimal) + ".", "");
+            double rN = Convert.ToDouble(decpart);
+            double rD = Math.Pow(10, decpart.Length);
+
+            string rd = Recur(decpart);
+            int rel = Convert.ToInt32(rd);
+            if (rel != 0)
+            {
+                rN = rel;
+                rD = (int)Math.Pow(10, rd.Length) - 1;
+            }
+            //just a few prime factors for testing purposes
+            var primes = new[] { 47, 43, 37, 31, 29, 23, 19, 17, 13, 11, 7, 5, 3, 2 };
+            foreach (int i in primes) ReduceNo(i, ref rD, ref rN);
+
+            rN = rN + (whole * rD);
+            return string.Format("{0}{1}/{2}", neg, rN, rD);
+        }
+
+        /// <summary>
+        /// Finds out the recurring decimal in a specified number
+        /// </summary>
+        /// <param name="db">Number to check</param>
+        /// <returns></returns>
+        private static string Recur(string db)
+        {
+            if (db.Length < 13) return "0";
+            var sb = new StringBuilder();
+            for (int i = 0; i < 7; i++)
+            {
+                sb.Append(db[i]);
+                int dlength = (db.Length / sb.ToString().Length);
+                int occur = Occurence(sb.ToString(), db);
+                if (dlength == occur || dlength == occur - sb.ToString().Length)
+                {
+                    return sb.ToString();
+                }
+            }
+            return "0";
+        }
+
+        /// <summary>
+        /// Checks for number of occurence of specified no in a number
+        /// </summary>
+        /// <param name="s">The no to check occurence times</param>
+        /// <param name="check">The number where to check this</param>
+        /// <returns></returns>
+        private static int Occurence(string s, string check)
+        {
+            int i = 0;
+            int d = s.Length;
+            string ds = check;
+            for (int n = (ds.Length / d); n > 0; n--)
+            {
+                if (ds.Contains(s))
+                {
+                    i++;
+                    ds = ds.Remove(ds.IndexOf(s, System.StringComparison.Ordinal), d);
+                }
+            }
+            return i;
+        }
+
+        /// <summary>
+        /// Reduces a fraction given the numerator and denominator
+        /// </summary>
+        /// <param name="i">Number to use in an attempt to reduce fraction</param>
+        /// <param name="rD">the Denominator</param>
+        /// <param name="rN">the Numerator</param>
+        private static void ReduceNo(int i, ref double rD, ref double rN)
+        {
+            //keep reducing until divisibility ends
+            while ((rD % i) < 1e-10 && (rN % i) < 1e-10)
+            {
+                rN = rN / i;
+                rD = rD / i;
+            }
+        }
+    }
+}

+ 32 - 0
MediaBrowser.Providers/Photos/PhotoMetadataService.cs

@@ -0,0 +1,32 @@
+using MediaBrowser.Common.IO;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Logging;
+using MediaBrowser.Providers.Manager;
+using System.Collections.Generic;
+
+namespace MediaBrowser.Providers.Photos
+{
+    class PhotoMetadataService : MetadataService<Photo, ItemLookupInfo>
+    {
+        public PhotoMetadataService(IServerConfigurationManager serverConfigurationManager, ILogger logger, IProviderManager providerManager, IProviderRepository providerRepo, IFileSystem fileSystem)
+            : base(serverConfigurationManager, logger, providerManager, providerRepo, fileSystem)
+        {
+        }
+
+        /// <summary>
+        /// Merges the specified source.
+        /// </summary>
+        /// <param name="source">The source.</param>
+        /// <param name="target">The target.</param>
+        /// <param name="lockedFields">The locked fields.</param>
+        /// <param name="replaceData">if set to <c>true</c> [replace data].</param>
+        /// <param name="mergeMetadataSettings">if set to <c>true</c> [merge metadata settings].</param>
+        protected override void MergeData(Photo source, Photo target, List<MetadataFields> lockedFields, bool replaceData, bool mergeMetadataSettings)
+        {
+            ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings);
+        }
+    }
+}

+ 137 - 0
MediaBrowser.Providers/Photos/PhotoProvider.cs

@@ -0,0 +1,137 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Logging;
+using System;
+using System.Globalization;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Providers.Photos
+{
+    public class PhotoProvider : ICustomMetadataProvider<Photo>, IHasChangeMonitor
+    {
+        private readonly ILogger _logger;
+
+        public PhotoProvider(ILogger logger)
+        {
+            _logger = logger;
+        }
+
+        public Task<ItemUpdateType> FetchAsync(Photo item, IDirectoryService directoryService, CancellationToken cancellationToken)
+        {
+            item.SetImagePath(ImageType.Primary, item.Path);
+
+            if (item.Path.EndsWith(".jpg", StringComparison.OrdinalIgnoreCase) || item.Path.EndsWith(".jpeg", StringComparison.OrdinalIgnoreCase))
+            {
+                try
+                {
+                    using (var reader = new ExifReader(item.Path))
+                    {
+                        double aperture = 0;
+                        double shutterSpeed = 0;
+
+                        DateTime dateTaken;
+
+                        string manufacturer;
+                        string model;
+
+                        int xResolution;
+                        int yResolution;
+
+                        reader.GetTagValue(ExifTags.FNumber, out aperture);
+                        reader.GetTagValue(ExifTags.ExposureTime, out shutterSpeed);
+                        reader.GetTagValue(ExifTags.DateTimeOriginal, out dateTaken);
+
+                        reader.GetTagValue(ExifTags.Make, out manufacturer);
+                        reader.GetTagValue(ExifTags.Model, out model);
+
+                        reader.GetTagValue(ExifTags.XResolution, out xResolution);
+                        reader.GetTagValue(ExifTags.YResolution, out yResolution);
+                        
+                        if (dateTaken > DateTime.MinValue)
+                        {
+                            item.DateCreated = dateTaken;
+                            item.PremiereDate = dateTaken;
+                            item.ProductionYear = dateTaken.Year;
+                        }
+
+                        var cameraModel = manufacturer ?? string.Empty;
+                        cameraModel += " ";
+                        cameraModel += model ?? string.Empty;
+
+                        item.Overview = "Taken " + dateTaken.ToString("F") + "\n" +
+                                        (!string.IsNullOrWhiteSpace(cameraModel) ? "With a " + cameraModel : "") +
+                                        (aperture > 0 && shutterSpeed > 0 ? " at f" + aperture.ToString(CultureInfo.InvariantCulture) + " and " + PhotoHelper.Dec2Frac(shutterSpeed) + "s" : "") + "\n"
+                                        + (xResolution > 0 ? "\n<br/>Resolution: " + xResolution + "x" + yResolution : "");
+                    }
+
+                }
+                catch (Exception e)
+                {
+                    _logger.ErrorException("Image Provider - Error reading image tag for {0}", e, item.Path);
+                }
+            }
+
+            //// Get additional tags from xmp
+            //try
+            //{
+            //    using (var fs = new FileStream(item.Path, FileMode.Open, FileAccess.Read))
+            //    {
+            //        var bf = BitmapFrame.Create(fs);
+
+            //        if (bf != null)
+            //        {
+            //            var data = (BitmapMetadata)bf.Metadata;
+            //            if (data != null)
+            //            {
+
+            //                DateTime dateTaken;
+            //                var cameraModel = "";
+
+            //                DateTime.TryParse(data.DateTaken, out dateTaken);
+            //                if (dateTaken > DateTime.MinValue) item.DateCreated = dateTaken;
+            //                cameraModel = data.CameraModel;
+
+            //                item.PremiereDate = dateTaken;
+            //                item.ProductionYear = dateTaken.Year;
+            //                item.Overview = "Taken " + dateTaken.ToString("F") + "\n" +
+            //                                (cameraModel != "" ? "With a " + cameraModel : "") +
+            //                                (aperture > 0 && shutterSpeed > 0 ? " at f" + aperture.ToString(CultureInfo.InvariantCulture) + " and " + PhotoHelper.Dec2Frac(shutterSpeed) + "s" : "") + "\n"
+            //                                + (bf.Width > 0 ? "\n<br/>Resolution: " + (int)bf.Width + "x" + (int)bf.Height : "");
+
+            //                var photo = item as Photo;
+            //                if (data.Keywords != null) item.Genres = photo.Tags = new List<string>(data.Keywords);
+            //                item.Name = !string.IsNullOrWhiteSpace(data.Title) ? data.Title : item.Name;
+            //                item.CommunityRating = data.Rating;
+            //                if (!string.IsNullOrWhiteSpace(data.Subject)) photo.AddTagline(data.Subject);
+            //            }
+            //        }
+
+            //    }
+            //}
+            //catch (NotSupportedException)
+            //{
+            //    // No problem - move on
+            //}
+            //catch (Exception e)
+            //{
+            //    _logger.ErrorException("Error trying to read extended data from {0}", e, item.Path);
+            //}
+
+            const ItemUpdateType result = ItemUpdateType.ImageUpdate | ItemUpdateType.MetadataImport;
+            return Task.FromResult(result);
+        }
+
+        public string Name
+        {
+            get { return "Embedded Information"; }
+        }
+
+        public bool HasChanged(IHasMetadata item, IDirectoryService directoryService, DateTime date)
+        {
+            return item.DateModified > date;
+        }
+    }
+}

+ 1 - 6
MediaBrowser.Providers/Studios/StudiosImageProvider.cs

@@ -54,12 +54,7 @@ namespace MediaBrowser.Providers.Studios
             };
         }
 
-        public Task<IEnumerable<RemoteImageInfo>> GetImages(IHasImages item, ImageType imageType, CancellationToken cancellationToken)
-        {
-            return GetImages(item, imageType == ImageType.Primary, imageType == ImageType.Thumb, cancellationToken);
-        }
-
-        public Task<IEnumerable<RemoteImageInfo>> GetAllImages(IHasImages item, CancellationToken cancellationToken)
+        public Task<IEnumerable<RemoteImageInfo>> GetImages(IHasImages item, CancellationToken cancellationToken)
         {
             return GetImages(item, true, true, cancellationToken);
         }

+ 1 - 8
MediaBrowser.Providers/TV/FanArtSeasonProvider.cs

@@ -58,14 +58,7 @@ namespace MediaBrowser.Providers.TV
             };
         }
 
-        public async Task<IEnumerable<RemoteImageInfo>> GetImages(IHasImages item, ImageType imageType, CancellationToken cancellationToken)
-        {
-            var images = await GetAllImages(item, cancellationToken).ConfigureAwait(false);
-
-            return images.Where(i => i.Type == imageType);
-        }
-
-        public async Task<IEnumerable<RemoteImageInfo>> GetAllImages(IHasImages item, CancellationToken cancellationToken)
+        public async Task<IEnumerable<RemoteImageInfo>> GetImages(IHasImages item, CancellationToken cancellationToken)
         {
             var list = new List<RemoteImageInfo>();
 

+ 1 - 8
MediaBrowser.Providers/TV/FanartSeriesProvider.cs

@@ -69,14 +69,7 @@ namespace MediaBrowser.Providers.TV
             };
         }
 
-        public async Task<IEnumerable<RemoteImageInfo>> GetImages(IHasImages item, ImageType imageType, CancellationToken cancellationToken)
-        {
-            var images = await GetAllImages(item, cancellationToken).ConfigureAwait(false);
-
-            return images.Where(i => i.Type == imageType);
-        }
-
-        public async Task<IEnumerable<RemoteImageInfo>> GetAllImages(IHasImages item, CancellationToken cancellationToken)
+        public async Task<IEnumerable<RemoteImageInfo>> GetImages(IHasImages item, CancellationToken cancellationToken)
         {
             var list = new List<RemoteImageInfo>();
 

+ 1 - 8
MediaBrowser.Providers/TV/MovieDbSeriesImageProvider.cs

@@ -51,14 +51,7 @@ namespace MediaBrowser.Providers.TV
             };
         }
 
-        public async Task<IEnumerable<RemoteImageInfo>> GetImages(IHasImages item, ImageType imageType, CancellationToken cancellationToken)
-        {
-            var images = await GetAllImages(item, cancellationToken).ConfigureAwait(false);
-
-            return images.Where(i => i.Type == imageType);
-        }
-
-        public async Task<IEnumerable<RemoteImageInfo>> GetAllImages(IHasImages item, CancellationToken cancellationToken)
+        public async Task<IEnumerable<RemoteImageInfo>> GetImages(IHasImages item, CancellationToken cancellationToken)
         {
             var list = new List<RemoteImageInfo>();
 

+ 66 - 66
MediaBrowser.Providers/TV/MovieDbSeriesProvider.cs

@@ -50,22 +50,37 @@ namespace MediaBrowser.Providers.TV
             var result = new MetadataResult<Series>();
 
             var tmdbId = info.GetProviderId(MetadataProviders.Tmdb);
-            var imdbId = info.GetProviderId(MetadataProviders.Imdb);
-            var tvdbId = info.GetProviderId(MetadataProviders.Tvdb);
 
-            // Commenting our searching by imdb/tvdb because as of now it's not supported. 
-            // But this is how movies work so most likely this can eventually be enabled.
+            if (string.IsNullOrEmpty(tmdbId))
+            {
+                var imdbId = info.GetProviderId(MetadataProviders.Imdb);
 
-            if (string.IsNullOrEmpty(tmdbId) /*&& string.IsNullOrEmpty(imdbId) && string.IsNullOrEmpty(tvdbId)*/)
+                if (!string.IsNullOrEmpty(imdbId))
+                {
+                    tmdbId = await FindIdByExternalId(imdbId, "imdb_id", cancellationToken).ConfigureAwait(false);
+                }
+            }
+
+            if (string.IsNullOrEmpty(tmdbId))
+            {
+                var tvdbId = info.GetProviderId(MetadataProviders.Tvdb);
+
+                if (!string.IsNullOrEmpty(tvdbId))
+                {
+                    tmdbId = await FindIdByExternalId(tvdbId, "tvdb_id", cancellationToken).ConfigureAwait(false);
+                }
+            }
+
+            if (string.IsNullOrEmpty(tmdbId))
             {
                 tmdbId = await new MovieDbSearch(_logger, _jsonSerializer).FindSeriesId(info, cancellationToken).ConfigureAwait(false);
             }
 
-            if (!string.IsNullOrEmpty(tmdbId) /*|| !string.IsNullOrEmpty(imdbId) || !string.IsNullOrEmpty(tvdbId)*/)
+            if (!string.IsNullOrEmpty(tmdbId))
             {
                 cancellationToken.ThrowIfCancellationRequested();
 
-                result.Item = await FetchMovieData(tmdbId, imdbId, tvdbId, info.MetadataLanguage, info.MetadataCountryCode, cancellationToken).ConfigureAwait(false);
+                result.Item = await FetchMovieData(tmdbId, info.MetadataLanguage, info.MetadataCountryCode, cancellationToken).ConfigureAwait(false);
 
                 result.HasMetadata = result.Item != null;
             }
@@ -73,43 +88,29 @@ namespace MediaBrowser.Providers.TV
             return result;
         }
 
-        private async Task<Series> FetchMovieData(string tmdbId, string imdbId, string tvdbId, string language, string preferredCountryCode, CancellationToken cancellationToken)
+        private async Task<Series> FetchMovieData(string tmdbId, string language, string preferredCountryCode, CancellationToken cancellationToken)
         {
             string dataFilePath = null;
             RootObject seriesInfo = null;
 
-            // Id could be ImdbId or TmdbId
-            if (string.IsNullOrEmpty(tmdbId))
+            if (!string.IsNullOrEmpty(tmdbId))
             {
-                if (string.IsNullOrWhiteSpace(imdbId))
-                {
-                    seriesInfo = await FetchMainResult(imdbId, language, cancellationToken).ConfigureAwait(false);
-                }
-                if (seriesInfo == null)
-                {
-                    if (string.IsNullOrWhiteSpace(imdbId))
-                    {
-                        seriesInfo = await FetchMainResult(tvdbId, language, cancellationToken).ConfigureAwait(false);
-                    }
-                }
+                seriesInfo = await FetchMainResult(tmdbId, language, cancellationToken).ConfigureAwait(false);
+            }
 
-                if (seriesInfo == null)
-                {
-                    return null;
-                }
+            if (seriesInfo == null)
+            {
+                return null;
+            }
 
-                tmdbId = seriesInfo.id.ToString(_usCulture);
+            tmdbId = seriesInfo.id.ToString(_usCulture);
 
-                dataFilePath = MovieDbProvider.Current.GetDataFilePath(tmdbId, language);
-                Directory.CreateDirectory(Path.GetDirectoryName(dataFilePath));
-                _jsonSerializer.SerializeToFile(seriesInfo, dataFilePath);
-            }
+            dataFilePath = MovieDbProvider.Current.GetDataFilePath(tmdbId, language);
+            Directory.CreateDirectory(Path.GetDirectoryName(dataFilePath));
+            _jsonSerializer.SerializeToFile(seriesInfo, dataFilePath);
 
             await EnsureSeriesInfo(tmdbId, language, cancellationToken).ConfigureAwait(false);
 
-            dataFilePath = dataFilePath ?? GetDataFilePath(tmdbId, language);
-            seriesInfo = seriesInfo ?? _jsonSerializer.DeserializeFromFile<RootObject>(dataFilePath);
-
             var item = new Series();
 
             ProcessMainInfo(item, preferredCountryCode, seriesInfo);
@@ -223,8 +224,6 @@ namespace MediaBrowser.Providers.TV
                 url += string.Format("&language={0}", language);
             }
 
-            RootObject mainResult;
-
             cancellationToken.ThrowIfCancellationRequested();
 
             using (var json = await MovieDbProvider.Current.GetMovieDbResponse(new HttpRequestOptions
@@ -235,38 +234,8 @@ namespace MediaBrowser.Providers.TV
 
             }).ConfigureAwait(false))
             {
-                mainResult = _jsonSerializer.DeserializeFromStream<RootObject>(json);
-            }
-
-            cancellationToken.ThrowIfCancellationRequested();
-
-            if (mainResult != null && string.IsNullOrEmpty(mainResult.overview))
-            {
-                if (!string.IsNullOrEmpty(language) && !string.Equals(language, "en", StringComparison.OrdinalIgnoreCase))
-                {
-                    _logger.Info("Couldn't find meta for language " + language + ". Trying English...");
-
-                    url = string.Format(GetTvInfo3, id, MovieDbProvider.ApiKey) + "&include_image_language=en,null&language=en";
-
-                    using (var json = await MovieDbProvider.Current.GetMovieDbResponse(new HttpRequestOptions
-                    {
-                        Url = url,
-                        CancellationToken = cancellationToken,
-                        AcceptHeader = MovieDbProvider.AcceptHeader
-
-                    }).ConfigureAwait(false))
-                    {
-                        mainResult = _jsonSerializer.DeserializeFromStream<RootObject>(json);
-                    }
-
-                    if (String.IsNullOrEmpty(mainResult.overview))
-                    {
-                        _logger.Error("Unable to find information for (id:" + id + ")");
-                        return null;
-                    }
-                }
+                return _jsonSerializer.DeserializeFromStream<RootObject>(json);
             }
-            return mainResult;
         }
         
         private readonly Task _cachedTask = Task.FromResult(true);
@@ -338,6 +307,37 @@ namespace MediaBrowser.Providers.TV
             return false;
         }
 
+        private async Task<string> FindIdByExternalId(string id, string externalSource, CancellationToken cancellationToken)
+        {
+            var url = string.Format("http://api.themoviedb.org/3/tv/find/{0}?api_key={1}&external_source={2}", 
+                id, 
+                MovieDbProvider.ApiKey,
+                externalSource);
+
+            using (var json = await MovieDbProvider.Current.GetMovieDbResponse(new HttpRequestOptions
+            {
+                Url = url,
+                CancellationToken = cancellationToken,
+                AcceptHeader = MovieDbProvider.AcceptHeader
+
+            }).ConfigureAwait(false))
+            {
+                var result = _jsonSerializer.DeserializeFromStream<MovieDbSearch.ExternalIdLookupResult>(json);
+
+                if (result != null && result.tv_results != null)
+                {
+                    var tv = result.tv_results.FirstOrDefault();
+
+                    if (tv != null)
+                    {
+                        return tv.id.ToString(_usCulture);
+                    }
+                }
+            }
+
+            return null;
+        }
+
         public class CreatedBy
         {
             public int id { get; set; }

+ 1 - 8
MediaBrowser.Providers/TV/TvdbEpisodeImageProvider.cs

@@ -51,14 +51,7 @@ namespace MediaBrowser.Providers.TV
             };
         }
 
-        public async Task<IEnumerable<RemoteImageInfo>> GetImages(IHasImages item, ImageType imageType, CancellationToken cancellationToken)
-        {
-            var images = await GetAllImages(item, cancellationToken).ConfigureAwait(false);
-
-            return images.Where(i => i.Type == imageType);
-        }
-
-        public Task<IEnumerable<RemoteImageInfo>> GetAllImages(IHasImages item, CancellationToken cancellationToken)
+        public Task<IEnumerable<RemoteImageInfo>> GetImages(IHasImages item, CancellationToken cancellationToken)
         {
             var episode = (Episode)item;
             var series = episode.Series;

+ 1 - 8
MediaBrowser.Providers/TV/TvdbSeasonImageProvider.cs

@@ -59,14 +59,7 @@ namespace MediaBrowser.Providers.TV
             };
         }
 
-        public async Task<IEnumerable<RemoteImageInfo>> GetImages(IHasImages item, ImageType imageType, CancellationToken cancellationToken)
-        {
-            var images = await GetAllImages(item, cancellationToken).ConfigureAwait(false);
-
-            return images.Where(i => i.Type == imageType);
-        }
-
-        public async Task<IEnumerable<RemoteImageInfo>> GetAllImages(IHasImages item, CancellationToken cancellationToken)
+        public async Task<IEnumerable<RemoteImageInfo>> GetImages(IHasImages item, CancellationToken cancellationToken)
         {
             var season = (Season)item;
             var series = season.Series;

+ 1 - 8
MediaBrowser.Providers/TV/TvdbSeriesImageProvider.cs

@@ -59,14 +59,7 @@ namespace MediaBrowser.Providers.TV
             };
         }
 
-        public async Task<IEnumerable<RemoteImageInfo>> GetImages(IHasImages item, ImageType imageType, CancellationToken cancellationToken)
-        {
-            var images = await GetAllImages(item, cancellationToken).ConfigureAwait(false);
-
-            return images.Where(i => i.Type == imageType);
-        }
-
-        public async Task<IEnumerable<RemoteImageInfo>> GetAllImages(IHasImages item, CancellationToken cancellationToken)
+        public async Task<IEnumerable<RemoteImageInfo>> GetImages(IHasImages item, CancellationToken cancellationToken)
         {
             var series = (Series)item;
             var seriesId = series.GetProviderId(MetadataProviders.Tvdb);

+ 13 - 11
MediaBrowser.Server.Implementations/Library/LibraryManager.cs

@@ -462,21 +462,27 @@ namespace MediaBrowser.Server.Implementations.Library
             return item;
         }
 
+        public BaseItem ResolvePath(FileSystemInfo fileInfo, Folder parent = null)
+        {
+            return ResolvePath(fileInfo, new DirectoryService(_logger), parent);
+        }
+
         /// <summary>
         /// Resolves a path into a BaseItem
         /// </summary>
         /// <param name="fileInfo">The file info.</param>
+        /// <param name="directoryService">The directory service.</param>
         /// <param name="parent">The parent.</param>
         /// <returns>BaseItem.</returns>
-        /// <exception cref="System.ArgumentNullException"></exception>
-        public BaseItem ResolvePath(FileSystemInfo fileInfo, Folder parent = null)
+        /// <exception cref="System.ArgumentNullException">fileInfo</exception>
+        public BaseItem ResolvePath(FileSystemInfo fileInfo, IDirectoryService directoryService, Folder parent = null)
         {
             if (fileInfo == null)
             {
                 throw new ArgumentNullException("fileInfo");
             }
 
-            var args = new ItemResolveArgs(ConfigurationManager.ApplicationPaths, this)
+            var args = new ItemResolveArgs(ConfigurationManager.ApplicationPaths, this, directoryService)
             {
                 Parent = parent,
                 Path = fileInfo.FullName,
@@ -497,8 +503,6 @@ namespace MediaBrowser.Server.Implementations.Library
                 // When resolving the root, we need it's grandchildren (children of user views)
                 var flattenFolderDepth = isPhysicalRoot ? 2 : 0;
 
-                var directoryService = new DirectoryService(_logger);
-
                 var fileSystemDictionary = FileData.GetFilteredFileSystemEntries(directoryService, args.Path, _fileSystem, _logger, args, flattenFolderDepth: flattenFolderDepth, resolveShortcuts: isPhysicalRoot || args.IsVf);
 
                 // Need to remove subpaths that may have been resolved from shortcuts
@@ -555,9 +559,10 @@ namespace MediaBrowser.Server.Implementations.Library
         /// </summary>
         /// <typeparam name="T"></typeparam>
         /// <param name="files">The files.</param>
+        /// <param name="directoryService">The directory service.</param>
         /// <param name="parent">The parent.</param>
         /// <returns>List{``0}.</returns>
-        public List<T> ResolvePaths<T>(IEnumerable<FileSystemInfo> files, Folder parent)
+        public List<T> ResolvePaths<T>(IEnumerable<FileSystemInfo> files, IDirectoryService directoryService, Folder parent)
             where T : BaseItem
         {
             var list = new List<T>();
@@ -566,7 +571,7 @@ namespace MediaBrowser.Server.Implementations.Library
             {
                 try
                 {
-                    var item = ResolvePath(f, parent) as T;
+                    var item = ResolvePath(f, directoryService, parent) as T;
 
                     if (item != null)
                     {
@@ -594,10 +599,7 @@ namespace MediaBrowser.Server.Implementations.Library
         {
             var rootFolderPath = ConfigurationManager.ApplicationPaths.RootFolderPath;
 
-            if (!Directory.Exists(rootFolderPath))
-            {
-                Directory.CreateDirectory(rootFolderPath);
-            }
+            Directory.CreateDirectory(rootFolderPath);
 
             var rootFolder = RetrieveItem(rootFolderPath.GetMBId(typeof(AggregateFolder))) as AggregateFolder ?? (AggregateFolder)ResolvePath(new DirectoryInfo(rootFolderPath));
 

+ 6 - 3
MediaBrowser.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs

@@ -3,6 +3,7 @@ using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Entities.Movies;
 using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Providers;
 using MediaBrowser.Controller.Resolvers;
 using MediaBrowser.Model.Entities;
 using System;
@@ -62,14 +63,17 @@ namespace MediaBrowser.Server.Implementations.Library.Resolvers.Audio
         /// Determine if the supplied file data points to a music album
         /// </summary>
         /// <param name="path">The path.</param>
+        /// <param name="directoryService">The directory service.</param>
         /// <returns><c>true</c> if [is music album] [the specified data]; otherwise, <c>false</c>.</returns>
-        public static bool IsMusicAlbum(string path)
+        public static bool IsMusicAlbum(string path, IDirectoryService directoryService)
         {
             // If list contains at least 2 audio files or at least one and no video files consider it to contain music
             var foundAudio = 0;
 
-            foreach (var fullName in Directory.EnumerateFiles(path))
+            foreach (var file in directoryService.GetFiles(path))
             {
+                var fullName = file.FullName;
+
                 if (EntityResolutionHelper.IsAudioFile(fullName))
                 {
                     // Don't resolve these into audio files
@@ -105,7 +109,6 @@ namespace MediaBrowser.Server.Implementations.Library.Resolvers.Audio
                 if (ContainsMusic(args.FileSystemChildren)) return true;
             }
 
-
             return false;
         }
 

+ 3 - 1
MediaBrowser.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs

@@ -57,9 +57,11 @@ namespace MediaBrowser.Server.Implementations.Library.Resolvers.Audio
             {
                 return null;
             }
+
+            var directoryService = args.DirectoryService;
             
             // If we contain an album assume we are an artist folder
-            return args.FileSystemChildren.Where(i => (i.Attributes & FileAttributes.Directory) == FileAttributes.Directory).Any(i => MusicAlbumResolver.IsMusicAlbum(i.FullName)) ? new MusicArtist() : null;
+            return args.FileSystemChildren.Where(i => (i.Attributes & FileAttributes.Directory) == FileAttributes.Directory).Any(i => MusicAlbumResolver.IsMusicAlbum(i.FullName, directoryService)) ? new MusicArtist() : null;
         }
 
     }

+ 8 - 7
MediaBrowser.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs

@@ -4,6 +4,7 @@ using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Entities.Movies;
 using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Providers;
 using MediaBrowser.Controller.Resolvers;
 using MediaBrowser.Model.Entities;
 using System;
@@ -92,31 +93,31 @@ namespace MediaBrowser.Server.Implementations.Library.Resolvers.Movies
                 if (args.Path.IndexOf("[trailers]", StringComparison.OrdinalIgnoreCase) != -1 ||
                     string.Equals(collectionType, CollectionType.Trailers, StringComparison.OrdinalIgnoreCase))
                 {
-                    return FindMovie<Trailer>(args.Path, args.Parent, args.FileSystemChildren);
+                    return FindMovie<Trailer>(args.Path, args.Parent, args.FileSystemChildren, args.DirectoryService);
                 }
 
                 if (args.Path.IndexOf("[musicvideos]", StringComparison.OrdinalIgnoreCase) != -1 ||
                     string.Equals(collectionType, CollectionType.MusicVideos, StringComparison.OrdinalIgnoreCase))
                 {
-                    return FindMovie<MusicVideo>(args.Path, args.Parent, args.FileSystemChildren);
+                    return FindMovie<MusicVideo>(args.Path, args.Parent, args.FileSystemChildren, args.DirectoryService);
                 }
 
                 if (args.Path.IndexOf("[adultvideos]", StringComparison.OrdinalIgnoreCase) != -1 ||
                     string.Equals(collectionType, CollectionType.AdultVideos, StringComparison.OrdinalIgnoreCase))
                 {
-                    return FindMovie<AdultVideo>(args.Path, args.Parent, args.FileSystemChildren);
+                    return FindMovie<AdultVideo>(args.Path, args.Parent, args.FileSystemChildren, args.DirectoryService);
                 }
 
                 if (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase))
                 {
-                    return FindMovie<Video>(args.Path, args.Parent, args.FileSystemChildren);
+                    return FindMovie<Video>(args.Path, args.Parent, args.FileSystemChildren, args.DirectoryService);
                 }
                 
                 if (string.IsNullOrEmpty(collectionType) ||
                     string.Equals(collectionType, CollectionType.Movies, StringComparison.OrdinalIgnoreCase) ||
                     string.Equals(collectionType, CollectionType.BoxSets, StringComparison.OrdinalIgnoreCase))
                 {
-                    return FindMovie<Movie>(args.Path, args.Parent, args.FileSystemChildren);
+                    return FindMovie<Movie>(args.Path, args.Parent, args.FileSystemChildren, args.DirectoryService);
                 }
 
                 return null;
@@ -203,7 +204,7 @@ namespace MediaBrowser.Server.Implementations.Library.Resolvers.Movies
         /// <param name="parent">The parent.</param>
         /// <param name="fileSystemEntries">The file system entries.</param>
         /// <returns>Movie.</returns>
-        private T FindMovie<T>(string path, Folder parent, IEnumerable<FileSystemInfo> fileSystemEntries)
+        private T FindMovie<T>(string path, Folder parent, IEnumerable<FileSystemInfo> fileSystemEntries, IDirectoryService directoryService)
             where T : Video, new()
         {
             var movies = new List<T>();
@@ -248,7 +249,7 @@ namespace MediaBrowser.Server.Implementations.Library.Resolvers.Movies
                     continue;
                 }
 
-                var childArgs = new ItemResolveArgs(_applicationPaths, _libraryManager)
+                var childArgs = new ItemResolveArgs(_applicationPaths, _libraryManager, directoryService)
                 {
                     FileInfo = child,
                     Path = child.FullName,

+ 50 - 0
MediaBrowser.Server.Implementations/Library/Resolvers/PhotoResolver.cs

@@ -0,0 +1,50 @@
+using MediaBrowser.Controller;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using System;
+using System.Linq;
+
+namespace MediaBrowser.Server.Implementations.Library.Resolvers
+{
+    public class PhotoResolver : ItemResolver<Photo>
+    {
+        private readonly IServerApplicationPaths _applicationPaths;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="PhotoResolver" /> class.
+        /// </summary>
+        /// <param name="applicationPaths">The application paths.</param>
+        public PhotoResolver(IServerApplicationPaths applicationPaths)
+        {
+            _applicationPaths = applicationPaths;
+        }
+
+        /// <summary>
+        /// Resolves the specified args.
+        /// </summary>
+        /// <param name="args">The args.</param>
+        /// <returns>Trailer.</returns>
+        protected override Photo Resolve(ItemResolveArgs args)
+        {
+            // Must be an image file within a photo collection
+            if (!args.IsDirectory && IsImageFile(args.Path) && string.Equals(args.GetCollectionType(), "photos", StringComparison.OrdinalIgnoreCase))
+            {
+                return new Photo
+                {
+                    Path = args.Path
+                };
+            }
+
+            return null;
+        }
+
+        protected static string[] ImageExtensions = { ".tiff", ".jpg", ".png", ".aiff" };
+        protected bool IsImageFile(string path)
+        {
+            return !path.EndsWith("folder.jpg", StringComparison.OrdinalIgnoreCase)
+                && !path.EndsWith("backdrop.jpg", StringComparison.OrdinalIgnoreCase)
+                && ImageExtensions.Any(p => path.EndsWith(p, StringComparison.OrdinalIgnoreCase));
+        }
+
+    }
+}

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

@@ -140,6 +140,7 @@
     <Compile Include="IO\LibraryMonitor.cs" />
     <Compile Include="Library\CoreResolutionIgnoreRule.cs" />
     <Compile Include="Library\LibraryManager.cs" />
+    <Compile Include="Library\Resolvers\PhotoResolver.cs" />
     <Compile Include="Library\SearchEngine.cs" />
     <Compile Include="Library\ResolverHelper.cs" />
     <Compile Include="Library\Resolvers\Audio\AudioResolver.cs" />

+ 46 - 1
MediaBrowser.ServerApplication/ApplicationHost.cs

@@ -31,7 +31,6 @@ using MediaBrowser.Model.Logging;
 using MediaBrowser.Model.MediaInfo;
 using MediaBrowser.Model.System;
 using MediaBrowser.Model.Updates;
-using MediaBrowser.Providers;
 using MediaBrowser.Providers.Manager;
 using MediaBrowser.Server.Implementations;
 using MediaBrowser.Server.Implementations.BdInfo;
@@ -242,6 +241,52 @@ namespace MediaBrowser.ServerApplication
             LogManager.RemoveConsoleOutput();
         }
 
+        public override Task Init(IProgress<double> progress)
+        {
+            DeleteDeprecatedModules();
+
+            return base.Init(progress);
+        }
+
+        private void DeleteDeprecatedModules()
+        {
+            try
+            {
+                File.Delete(Path.Combine(ApplicationPaths.PluginsPath, "MBPhoto.dll"));
+            }
+            catch (IOException)
+            {
+                // Not there, no big deal
+            }
+
+            try
+            {
+                Directory.Delete(Path.Combine(ApplicationPaths.DataPath, "remote-images"), true);
+            }
+            catch (IOException)
+            {
+                // Not there, no big deal
+            }
+
+            try
+            {
+                Directory.Delete(Path.Combine(ApplicationPaths.DataPath, "extracted-video-images"), true);
+            }
+            catch (IOException)
+            {
+                // Not there, no big deal
+            }
+
+            try
+            {
+                Directory.Delete(Path.Combine(ApplicationPaths.DataPath, "extracted-audio-images"), true);
+            }
+            catch (IOException)
+            {
+                // Not there, no big deal
+            }
+        }
+
         /// <summary>
         /// Registers resources that classes will depend on
         /// </summary>

+ 2 - 2
Nuget/MediaBrowser.Common.Internal.nuspec

@@ -2,7 +2,7 @@
 <package xmlns="http://schemas.microsoft.com/packaging/2011/08/nuspec.xsd">
     <metadata>
         <id>MediaBrowser.Common.Internal</id>
-        <version>3.0.326</version>
+        <version>3.0.327</version>
         <title>MediaBrowser.Common.Internal</title>
         <authors>Luke</authors>
         <owners>ebr,Luke,scottisafool</owners>
@@ -12,7 +12,7 @@
         <description>Contains common components shared by Media Browser Theater and Media Browser Server. Not intended for plugin developer consumption.</description>
         <copyright>Copyright © Media Browser 2013</copyright>
         <dependencies>
-            <dependency id="MediaBrowser.Common" version="3.0.326" />
+            <dependency id="MediaBrowser.Common" version="3.0.327" />
             <dependency id="NLog" version="2.1.0" />
             <dependency id="SimpleInjector" version="2.4.1" />
             <dependency id="sharpcompress" version="0.10.2" />

+ 1 - 1
Nuget/MediaBrowser.Common.nuspec

@@ -2,7 +2,7 @@
 <package xmlns="http://schemas.microsoft.com/packaging/2011/08/nuspec.xsd">
     <metadata>
         <id>MediaBrowser.Common</id>
-        <version>3.0.326</version>
+        <version>3.0.327</version>
         <title>MediaBrowser.Common</title>
         <authors>Media Browser Team</authors>
         <owners>ebr,Luke,scottisafool</owners>

+ 2 - 2
Nuget/MediaBrowser.Server.Core.nuspec

@@ -2,7 +2,7 @@
 <package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
     <metadata>
         <id>MediaBrowser.Server.Core</id>
-        <version>3.0.326</version>
+        <version>3.0.327</version>
         <title>Media Browser.Server.Core</title>
         <authors>Media Browser Team</authors>
         <owners>ebr,Luke,scottisafool</owners>
@@ -12,7 +12,7 @@
         <description>Contains core components required to build plugins for Media Browser Server.</description>
         <copyright>Copyright © Media Browser 2013</copyright>
         <dependencies>
-            <dependency id="MediaBrowser.Common" version="3.0.326" />
+            <dependency id="MediaBrowser.Common" version="3.0.327" />
         </dependencies>
     </metadata>
     <files>