Browse Source

take photos into the core

Luke Pulverenti 11 năm trước cách đây
mục cha
commit
eec9e04825
63 tập tin đã thay đổi với 1564 bổ sung357 xóa
  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>