浏览代码

Merge pull request #1 from MediaBrowser/Repo

Repo
Luke 12 年之前
父节点
当前提交
845554722e
共有 100 个文件被更改,包括 9297 次插入0 次删除
  1. 43 0
      .hgignore
  2. 438 0
      MediaBrowser.Api/ApiService.cs
  3. 81 0
      MediaBrowser.Api/Drawing/DrawingUtils.cs
  4. 148 0
      MediaBrowser.Api/Drawing/ImageProcessor.cs
  5. 119 0
      MediaBrowser.Api/HttpHandlers/AudioHandler.cs
  6. 255 0
      MediaBrowser.Api/HttpHandlers/BaseMediaHandler.cs
  7. 38 0
      MediaBrowser.Api/HttpHandlers/FavoriteStatusHandler.cs
  8. 57 0
      MediaBrowser.Api/HttpHandlers/GenreHandler.cs
  9. 78 0
      MediaBrowser.Api/HttpHandlers/GenresHandler.cs
  10. 224 0
      MediaBrowser.Api/HttpHandlers/ImageHandler.cs
  11. 35 0
      MediaBrowser.Api/HttpHandlers/ItemHandler.cs
  12. 84 0
      MediaBrowser.Api/HttpHandlers/ItemListHandler.cs
  13. 46 0
      MediaBrowser.Api/HttpHandlers/MovieSpecialFeaturesHandler.cs
  14. 55 0
      MediaBrowser.Api/HttpHandlers/PersonHandler.cs
  15. 38 0
      MediaBrowser.Api/HttpHandlers/PlayedStatusHandler.cs
  16. 38 0
      MediaBrowser.Api/HttpHandlers/PluginAssemblyHandler.cs
  17. 53 0
      MediaBrowser.Api/HttpHandlers/PluginConfigurationHandler.cs
  18. 38 0
      MediaBrowser.Api/HttpHandlers/PluginsHandler.cs
  19. 37 0
      MediaBrowser.Api/HttpHandlers/ServerConfigurationHandler.cs
  20. 57 0
      MediaBrowser.Api/HttpHandlers/StudioHandler.cs
  21. 78 0
      MediaBrowser.Api/HttpHandlers/StudiosHandler.cs
  22. 29 0
      MediaBrowser.Api/HttpHandlers/UserAuthenticationHandler.cs
  23. 29 0
      MediaBrowser.Api/HttpHandlers/UserHandler.cs
  24. 46 0
      MediaBrowser.Api/HttpHandlers/UserItemRatingHandler.cs
  25. 25 0
      MediaBrowser.Api/HttpHandlers/UsersHandler.cs
  26. 424 0
      MediaBrowser.Api/HttpHandlers/VideoHandler.cs
  27. 43 0
      MediaBrowser.Api/HttpHandlers/WeatherHandler.cs
  28. 55 0
      MediaBrowser.Api/HttpHandlers/YearHandler.cs
  29. 75 0
      MediaBrowser.Api/HttpHandlers/YearsHandler.cs
  30. 117 0
      MediaBrowser.Api/MediaBrowser.Api.csproj
  31. 14 0
      MediaBrowser.Api/Plugin.cs
  32. 35 0
      MediaBrowser.Api/Properties/AssemblyInfo.cs
  33. 6 0
      MediaBrowser.Api/packages.config
  34. 12 0
      MediaBrowser.ApiInteraction.Metro/ApiClient.cs
  35. 78 0
      MediaBrowser.ApiInteraction.Metro/DataSerializer.cs
  36. 74 0
      MediaBrowser.ApiInteraction.Metro/MediaBrowser.ApiInteraction.Metro.csproj
  37. 30 0
      MediaBrowser.ApiInteraction.Metro/Properties/AssemblyInfo.cs
  38. 37 0
      MediaBrowser.ApiInteraction.sln
  39. 18 0
      MediaBrowser.ApiInteraction/ApiClient.cs
  40. 446 0
      MediaBrowser.ApiInteraction/BaseApiClient.cs
  41. 611 0
      MediaBrowser.ApiInteraction/BaseHttpApiClient.cs
  42. 77 0
      MediaBrowser.ApiInteraction/DataSerializer.cs
  43. 78 0
      MediaBrowser.ApiInteraction/MediaBrowser.ApiInteraction.csproj
  44. 35 0
      MediaBrowser.ApiInteraction/Properties/AssemblyInfo.cs
  45. 10 0
      MediaBrowser.ApiInteraction/SerializationFormats.cs
  46. 4 0
      MediaBrowser.ApiInteraction/packages.config
  47. 12 0
      MediaBrowser.Common/Events/GenericEventArgs.cs
  48. 63 0
      MediaBrowser.Common/Extensions/BaseExtensions.cs
  49. 154 0
      MediaBrowser.Common/Kernel/BaseApplicationPaths.cs
  50. 345 0
      MediaBrowser.Common/Kernel/BaseKernel.cs
  51. 9 0
      MediaBrowser.Common/Kernel/KernelContext.cs
  52. 16 0
      MediaBrowser.Common/Logging/BaseLogger.cs
  53. 44 0
      MediaBrowser.Common/Logging/LogRow.cs
  54. 14 0
      MediaBrowser.Common/Logging/LogSeverity.cs
  55. 93 0
      MediaBrowser.Common/Logging/Logger.cs
  56. 38 0
      MediaBrowser.Common/Logging/TraceFileLogger.cs
  57. 164 0
      MediaBrowser.Common/MediaBrowser.Common.csproj
  58. 43 0
      MediaBrowser.Common/Mef/MefUtils.cs
  59. 23 0
      MediaBrowser.Common/Net/Handlers/BaseEmbeddedResourceHandler.cs
  60. 430 0
      MediaBrowser.Common/Net/Handlers/BaseHandler.cs
  61. 90 0
      MediaBrowser.Common/Net/Handlers/BaseSerializationHandler.cs
  62. 249 0
      MediaBrowser.Common/Net/Handlers/StaticFileHandler.cs
  63. 40 0
      MediaBrowser.Common/Net/HttpServer.cs
  64. 160 0
      MediaBrowser.Common/Net/MimeTypes.cs
  65. 18 0
      MediaBrowser.Common/Net/Request.cs
  66. 247 0
      MediaBrowser.Common/Plugins/BasePlugin.cs
  67. 78 0
      MediaBrowser.Common/Plugins/BaseTheme.cs
  68. 35 0
      MediaBrowser.Common/Properties/AssemblyInfo.cs
  69. 63 0
      MediaBrowser.Common/Properties/Resources.Designer.cs
  70. 121 0
      MediaBrowser.Common/Properties/Resources.resx
  71. 二进制
      MediaBrowser.Common/Resources/Images/Icon.ico
  72. 二进制
      MediaBrowser.Common/Resources/Images/mblogoblack.png
  73. 二进制
      MediaBrowser.Common/Resources/Images/mblogowhite.png
  74. 二进制
      MediaBrowser.Common/Resources/Images/spinner.gif
  75. 74 0
      MediaBrowser.Common/Serialization/JsonSerializer.cs
  76. 44 0
      MediaBrowser.Common/Serialization/JsvSerializer.cs
  77. 53 0
      MediaBrowser.Common/Serialization/ProtobufSerializer.cs
  78. 58 0
      MediaBrowser.Common/Serialization/XmlSerializer.cs
  79. 123 0
      MediaBrowser.Common/UI/BaseApplication.cs
  80. 484 0
      MediaBrowser.Common/UI/SingleInstance.cs
  81. 33 0
      MediaBrowser.Common/UI/Splash.xaml
  82. 32 0
      MediaBrowser.Common/UI/Splash.xaml.cs
  83. 15 0
      MediaBrowser.Common/app.config
  84. 8 0
      MediaBrowser.Common/packages.config
  85. 81 0
      MediaBrowser.Controller/Drawing/DrawingUtils.cs
  86. 148 0
      MediaBrowser.Controller/Drawing/ImageProcessor.cs
  87. 14 0
      MediaBrowser.Controller/Entities/Audio.cs
  88. 94 0
      MediaBrowser.Controller/Entities/BaseEntity.cs
  89. 202 0
      MediaBrowser.Controller/Entities/BaseItem.cs
  90. 619 0
      MediaBrowser.Controller/Entities/Folder.cs
  91. 7 0
      MediaBrowser.Controller/Entities/Genre.cs
  92. 7 0
      MediaBrowser.Controller/Entities/Movies/BoxSet.cs
  93. 31 0
      MediaBrowser.Controller/Entities/Movies/Movie.cs
  94. 25 0
      MediaBrowser.Controller/Entities/Person.cs
  95. 7 0
      MediaBrowser.Controller/Entities/Studio.cs
  96. 7 0
      MediaBrowser.Controller/Entities/TV/Episode.cs
  97. 34 0
      MediaBrowser.Controller/Entities/TV/Season.cs
  98. 12 0
      MediaBrowser.Controller/Entities/TV/Series.cs
  99. 21 0
      MediaBrowser.Controller/Entities/User.cs
  100. 67 0
      MediaBrowser.Controller/Entities/UserItemData.cs

+ 43 - 0
.hgignore

@@ -0,0 +1,43 @@
+# use glob syntax
+syntax: glob
+
+*.obj
+*.pdb
+*.user
+*.aps
+*.pch
+*.vspscc
+*.vssscc
+*_i.c
+*_p.c
+*.ncb
+*.suo
+*.tlb
+*.tlh
+*.bak
+*.cache
+*.ilk
+*.log
+*.lib
+*.sbr
+*.scc
+*.psess
+*.vsp
+*.orig
+[Bb]in
+[Dd]ebug*/
+obj/
+[Rr]elease*/
+ProgramData*/
+ProgramData-Server*/
+ProgramData-UI*/
+_ReSharper*/
+[Tt]humbs.db
+[Tt]est[Rr]esult*
+[Bb]uild[Ll]og.*
+*.[Pp]ublish.xml
+*.resharper
+
+# ncrunch files
+*.ncrunchsolution
+*.ncrunchproject

+ 438 - 0
MediaBrowser.Api/ApiService.cs

@@ -0,0 +1,438 @@
+using MediaBrowser.Controller;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Movies;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Model.DTO;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Api
+{
+    /// <summary>
+    /// Contains some helpers for the api
+    /// </summary>
+    public static class ApiService
+    {
+        /// <summary>
+        /// Gets an Item by Id, or the root item if none is supplied
+        /// </summary>
+        public static BaseItem GetItemById(string id)
+        {
+            Guid guid = string.IsNullOrEmpty(id) ? Guid.Empty : new Guid(id);
+
+            return Kernel.Instance.GetItemById(guid);
+        }
+
+        /// <summary>
+        /// Gets a User by Id
+        /// </summary>
+        /// <param name="logActivity">Whether or not to update the user's LastActivityDate</param>
+        public static User GetUserById(string id, bool logActivity)
+        {
+            var guid = new Guid(id);
+
+            var user = Kernel.Instance.Users.FirstOrDefault(u => u.Id == guid);
+
+            if (logActivity)
+            {
+                LogUserActivity(user);
+            }
+
+            return user;
+        }
+
+        /// <summary>
+        /// Gets the default User
+        /// </summary>
+        /// <param name="logActivity">Whether or not to update the user's LastActivityDate</param>
+        public static User GetDefaultUser(bool logActivity)
+        {
+            User user = Kernel.Instance.GetDefaultUser();
+
+            if (logActivity)
+            {
+                LogUserActivity(user);
+            }
+
+            return user;
+        }
+
+        /// <summary>
+        /// Updates LastActivityDate for a given User
+        /// </summary>
+        public static void LogUserActivity(User user)
+        {
+            user.LastActivityDate = DateTime.UtcNow;
+            Kernel.Instance.SaveUser(user);
+        }
+
+        /// <summary>
+        /// Converts a BaseItem to a DTOBaseItem
+        /// </summary>
+        public async static Task<DtoBaseItem> GetDtoBaseItem(BaseItem item, User user,
+            bool includeChildren = true,
+            bool includePeople = true)
+        {
+            var dto = new DtoBaseItem();
+
+            var tasks = new List<Task>();
+
+            tasks.Add(AttachStudios(dto, item));
+
+            if (includeChildren)
+            {
+                tasks.Add(AttachChildren(dto, item, user));
+                tasks.Add(AttachLocalTrailers(dto, item, user));
+            }
+
+            if (includePeople)
+            {
+                tasks.Add(AttachPeople(dto, item));
+            }
+
+            AttachBasicFields(dto, item, user);
+
+            // Make sure all the tasks we kicked off have completed.
+            if (tasks.Count > 0)
+            {
+                await Task.WhenAll(tasks).ConfigureAwait(false);
+            }
+
+            return dto;
+        }
+
+        /// <summary>
+        /// Sets simple property values on a DTOBaseItem
+        /// </summary>
+        private static void AttachBasicFields(DtoBaseItem dto, BaseItem item, User user)
+        {
+            dto.AspectRatio = item.AspectRatio;
+            dto.BackdropCount = item.BackdropImagePaths == null ? 0 : item.BackdropImagePaths.Count();
+            dto.DateCreated = item.DateCreated;
+            dto.DisplayMediaType = item.DisplayMediaType;
+
+            if (item.Genres != null)
+            {
+                dto.Genres = item.Genres.ToArray();
+            }
+
+            dto.HasArt = !string.IsNullOrEmpty(item.ArtImagePath);
+            dto.HasBanner = !string.IsNullOrEmpty(item.BannerImagePath);
+            dto.HasLogo = !string.IsNullOrEmpty(item.LogoImagePath);
+            dto.HasPrimaryImage = !string.IsNullOrEmpty(item.PrimaryImagePath);
+            dto.HasThumb = !string.IsNullOrEmpty(item.ThumbnailImagePath);
+            dto.Id = item.Id;
+            dto.IsNew = item.IsRecentlyAdded(user);
+            dto.IndexNumber = item.IndexNumber;
+            dto.IsFolder = item.IsFolder;
+            dto.Language = item.Language;
+            dto.LocalTrailerCount = item.LocalTrailers == null ? 0 : item.LocalTrailers.Count();
+            dto.Name = item.Name;
+            dto.OfficialRating = item.OfficialRating;
+            dto.Overview = item.Overview;
+
+            // If there are no backdrops, indicate what parent has them in case the Ui wants to allow inheritance
+            if (dto.BackdropCount == 0)
+            {
+                int backdropCount;
+                dto.ParentBackdropItemId = GetParentBackdropItemId(item, out backdropCount);
+                dto.ParentBackdropCount = backdropCount;
+            }
+
+            if (item.Parent != null)
+            {
+                dto.ParentId = item.Parent.Id;
+            }
+
+            dto.ParentIndexNumber = item.ParentIndexNumber;
+
+            // If there is no logo, indicate what parent has one in case the Ui wants to allow inheritance
+            if (!dto.HasLogo)
+            {
+                dto.ParentLogoItemId = GetParentLogoItemId(item);
+            }
+
+            dto.Path = item.Path;
+
+            dto.PremiereDate = item.PremiereDate;
+            dto.ProductionYear = item.ProductionYear;
+            dto.ProviderIds = item.ProviderIds;
+            dto.RunTimeTicks = item.RunTimeTicks;
+            dto.SortName = item.SortName;
+
+            if (item.Taglines != null)
+            {
+                dto.Taglines = item.Taglines.ToArray();
+            }
+
+            dto.TrailerUrl = item.TrailerUrl;
+            dto.Type = item.GetType().Name;
+            dto.CommunityRating = item.CommunityRating;
+
+            dto.UserData = GetDtoUserItemData(item.GetUserData(user, false));
+
+            var folder = item as Folder;
+
+            if (folder != null)
+            {
+                dto.SpecialCounts = folder.GetSpecialCounts(user);
+
+                dto.IsRoot = folder.IsRoot;
+                dto.IsVirtualFolder = folder.IsVirtualFolder;
+            }
+
+            // Add AudioInfo
+            var audio = item as Audio;
+
+            if (audio != null)
+            {
+                dto.AudioInfo = new AudioInfo
+                {
+                    Album = audio.Album,
+                    AlbumArtist = audio.AlbumArtist,
+                    Artist = audio.Artist,
+                    BitRate = audio.BitRate,
+                    Channels = audio.Channels
+                };
+            }
+
+            // Add VideoInfo
+            var video = item as Video;
+
+            if (video != null)
+            {
+                dto.VideoInfo = new VideoInfo
+                {
+                    Height = video.Height,
+                    Width = video.Width,
+                    Codec = video.Codec,
+                    VideoType = video.VideoType,
+                    ScanType = video.ScanType
+                };
+
+                if (video.AudioStreams != null)
+                {
+                    dto.VideoInfo.AudioStreams = video.AudioStreams.ToArray();
+                }
+
+                if (video.Subtitles != null)
+                {
+                    dto.VideoInfo.Subtitles = video.Subtitles.ToArray();
+                }
+            }
+
+            // Add SeriesInfo
+            var series = item as Series;
+
+            if (series != null)
+            {
+                DayOfWeek[] airDays = series.AirDays == null ? new DayOfWeek[] { } : series.AirDays.ToArray(); 
+
+                dto.SeriesInfo = new SeriesInfo
+                {
+                    AirDays = airDays,
+                    AirTime = series.AirTime,
+                    Status = series.Status
+                };
+            }
+
+            // Add MovieInfo
+            var movie = item as Movie;
+
+            if (movie != null)
+            {
+                int specialFeatureCount = movie.SpecialFeatures == null ? 0 : movie.SpecialFeatures.Count();
+
+                dto.MovieInfo = new MovieInfo
+                {
+                    SpecialFeatureCount = specialFeatureCount
+                };
+            }
+        }
+
+        /// <summary>
+        /// Attaches Studio DTO's to a DTOBaseItem
+        /// </summary>
+        private static async Task AttachStudios(DtoBaseItem dto, BaseItem item)
+        {
+            // Attach Studios by transforming them into BaseItemStudio (DTO)
+            if (item.Studios != null)
+            {
+                Studio[] entities = await Task.WhenAll(item.Studios.Select(c => Kernel.Instance.ItemController.GetStudio(c))).ConfigureAwait(false);
+
+                dto.Studios = new BaseItemStudio[entities.Length];
+
+                for (int i = 0; i < entities.Length; i++)
+                {
+                    Studio entity = entities[i];
+                    var baseItemStudio = new BaseItemStudio{};
+
+                    baseItemStudio.Name = entity.Name;
+
+                    baseItemStudio.HasImage = !string.IsNullOrEmpty(entity.PrimaryImagePath);
+
+                    dto.Studios[i] = baseItemStudio;
+                }
+            }
+        }
+
+        /// <summary>
+        /// Attaches child DTO's to a DTOBaseItem
+        /// </summary>
+        private static async Task AttachChildren(DtoBaseItem dto, BaseItem item, User user)
+        {
+            var folder = item as Folder;
+
+            if (folder != null)
+            {
+                IEnumerable<BaseItem> children = folder.GetChildren(user);
+
+                dto.Children = await Task.WhenAll(children.Select(c => GetDtoBaseItem(c, user, false, false))).ConfigureAwait(false);
+            }
+        }
+
+        /// <summary>
+        /// Attaches trailer DTO's to a DTOBaseItem
+        /// </summary>
+        private static async Task AttachLocalTrailers(DtoBaseItem dto, BaseItem item, User user)
+        {
+            if (item.LocalTrailers != null && item.LocalTrailers.Any())
+            {
+                dto.LocalTrailers = await Task.WhenAll(item.LocalTrailers.Select(c => GetDtoBaseItem(c, user, false, false))).ConfigureAwait(false);
+            }
+        }
+
+        /// <summary>
+        /// Attaches People DTO's to a DTOBaseItem
+        /// </summary>
+        private static async Task AttachPeople(DtoBaseItem dto, BaseItem item)
+        {
+            // Attach People by transforming them into BaseItemPerson (DTO)
+            if (item.People != null)
+            {
+                IEnumerable<Person> entities = await Task.WhenAll(item.People.Select(c => Kernel.Instance.ItemController.GetPerson(c.Key))).ConfigureAwait(false);
+
+                dto.People = item.People.Select(p =>
+                {
+                    var baseItemPerson = new BaseItemPerson{};
+
+                    baseItemPerson.Name = p.Key;
+                    baseItemPerson.Overview = p.Value.Overview;
+                    baseItemPerson.Type = p.Value.Type;
+
+                    Person ibnObject = entities.First(i => i.Name.Equals(p.Key, StringComparison.OrdinalIgnoreCase));
+
+                    if (ibnObject != null)
+                    {
+                        baseItemPerson.HasImage = !string.IsNullOrEmpty(ibnObject.PrimaryImagePath);
+                    }
+
+                    return baseItemPerson;
+                }).ToArray();
+            }
+        }
+
+        /// <summary>
+        /// If an item does not any backdrops, this can be used to find the first parent that does have one
+        /// </summary>
+        private static Guid? GetParentBackdropItemId(BaseItem item, out int backdropCount)
+        {
+            backdropCount = 0;
+
+            var parent = item.Parent;
+
+            while (parent != null)
+            {
+                if (parent.BackdropImagePaths != null && parent.BackdropImagePaths.Any())
+                {
+                    backdropCount = parent.BackdropImagePaths.Count();
+                    return parent.Id;
+                }
+
+                parent = parent.Parent;
+            }
+
+            return null;
+        }
+
+        /// <summary>
+        /// If an item does not have a logo, this can be used to find the first parent that does have one
+        /// </summary>
+        private static Guid? GetParentLogoItemId(BaseItem item)
+        {
+            var parent = item.Parent;
+
+            while (parent != null)
+            {
+                if (!string.IsNullOrEmpty(parent.LogoImagePath))
+                {
+                    return parent.Id;
+                }
+
+                parent = parent.Parent;
+            }
+
+            return null;
+        }
+
+        /// <summary>
+        /// Gets an ImagesByName entity along with the number of items containing it
+        /// </summary>
+        public static IbnItem GetIbnItem(BaseEntity entity, int itemCount)
+        {
+            return new IbnItem
+            {
+                Id = entity.Id,
+                BaseItemCount = itemCount,
+                HasImage = !string.IsNullOrEmpty(entity.PrimaryImagePath),
+                Name = entity.Name
+            };
+        }
+
+        /// <summary>
+        /// Converts a User to a DTOUser
+        /// </summary>
+        public static DtoUser GetDtoUser(User user)
+        {
+            return new DtoUser
+            {
+                Id = user.Id,
+                Name = user.Name,
+                HasImage = !string.IsNullOrEmpty(user.PrimaryImagePath),
+                HasPassword = !string.IsNullOrEmpty(user.Password),
+                LastActivityDate = user.LastActivityDate,
+                LastLoginDate = user.LastLoginDate
+            };
+        }
+
+        /// <summary>
+        /// Converts a UserItemData to a DTOUserItemData
+        /// </summary>
+        public static DtoUserItemData GetDtoUserItemData(UserItemData data)
+        {
+            if (data == null)
+            {
+                return null;
+            }
+
+            return new DtoUserItemData
+            {
+                IsFavorite = data.IsFavorite,
+                Likes = data.Likes,
+                PlaybackPositionTicks = data.PlaybackPositionTicks,
+                PlayCount = data.PlayCount,
+                Rating = data.Rating
+            };
+        }
+
+        public static bool IsApiUrlMatch(string url, HttpListenerRequest request)
+        {
+            url = "/api/" + url;
+
+            return request.Url.LocalPath.EndsWith(url, StringComparison.OrdinalIgnoreCase);
+        }
+    }
+}

+ 81 - 0
MediaBrowser.Api/Drawing/DrawingUtils.cs

@@ -0,0 +1,81 @@
+using System;
+using System.Drawing;
+
+namespace MediaBrowser.Api.Drawing
+{
+    public static class DrawingUtils
+    {
+        /// <summary>
+        /// Resizes a set of dimensions
+        /// </summary>
+        public static Size Resize(int currentWidth, int currentHeight, int? width, int? height, int? maxWidth, int? maxHeight)
+        {
+            return Resize(new Size(currentWidth, currentHeight), width, height, maxWidth, maxHeight);
+        }
+
+        /// <summary>
+        /// Resizes a set of dimensions
+        /// </summary>
+        /// <param name="size">The original size object</param>
+        /// <param name="width">A new fixed width, if desired</param>
+        /// <param name="height">A new fixed neight, if desired</param>
+        /// <param name="maxWidth">A max fixed width, if desired</param>
+        /// <param name="maxHeight">A max fixed height, if desired</param>
+        /// <returns>A new size object</returns>
+        public static Size Resize(Size size, int? width, int? height, int? maxWidth, int? maxHeight)
+        {
+            decimal newWidth = size.Width;
+            decimal newHeight = size.Height;
+
+            if (width.HasValue && height.HasValue)
+            {
+                newWidth = width.Value;
+                newHeight = height.Value;
+            }
+
+            else if (height.HasValue)
+            {
+                newWidth = GetNewWidth(newHeight, newWidth, height.Value);
+                newHeight = height.Value;
+            }
+
+            else if (width.HasValue)
+            {
+                newHeight = GetNewHeight(newHeight, newWidth, width.Value);
+                newWidth = width.Value;
+            }
+
+            if (maxHeight.HasValue && maxHeight < newHeight)
+            {
+                newWidth = GetNewWidth(newHeight, newWidth, maxHeight.Value);
+                newHeight = maxHeight.Value;
+            }
+
+            if (maxWidth.HasValue && maxWidth < newWidth)
+            {
+                newHeight = GetNewHeight(newHeight, newWidth, maxWidth.Value);
+                newWidth = maxWidth.Value;
+            }
+
+            return new Size(Convert.ToInt32(newWidth), Convert.ToInt32(newHeight));
+        }
+
+        private static decimal GetNewWidth(decimal currentHeight, decimal currentWidth, int newHeight)
+        {
+            decimal scaleFactor = newHeight;
+            scaleFactor /= currentHeight;
+            scaleFactor *= currentWidth;
+
+            return scaleFactor;
+        }
+
+        private static decimal GetNewHeight(decimal currentHeight, decimal currentWidth, int newWidth)
+        {
+            decimal scaleFactor = newWidth;
+            scaleFactor /= currentWidth;
+            scaleFactor *= currentHeight;
+
+            return scaleFactor;
+        }
+    }
+}

+ 148 - 0
MediaBrowser.Api/Drawing/ImageProcessor.cs

@@ -0,0 +1,148 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.Entities;
+using System;
+using System.Drawing;
+using System.Drawing.Drawing2D;
+using System.Drawing.Imaging;
+using System.IO;
+using System.Linq;
+
+namespace MediaBrowser.Api.Drawing
+{
+    public static class ImageProcessor
+    {
+        /// <summary>
+        /// Processes an image by resizing to target dimensions
+        /// </summary>
+        /// <param name="entity">The entity that owns the image</param>
+        /// <param name="imageType">The image type</param>
+        /// <param name="imageIndex">The image index (currently only used with backdrops)</param>
+        /// <param name="toStream">The stream to save the new image to</param>
+        /// <param name="width">Use if a fixed width is required. Aspect ratio will be preserved.</param>
+        /// <param name="height">Use if a fixed height is required. Aspect ratio will be preserved.</param>
+        /// <param name="maxWidth">Use if a max width is required. Aspect ratio will be preserved.</param>
+        /// <param name="maxHeight">Use if a max height is required. Aspect ratio will be preserved.</param>
+        /// <param name="quality">Quality level, from 0-100. Currently only applies to JPG. The default value should suffice.</param>
+        public static void ProcessImage(BaseEntity entity, ImageType imageType, int imageIndex, Stream toStream, int? width, int? height, int? maxWidth, int? maxHeight, int? quality)
+        {
+            Image originalImage = Image.FromFile(GetImagePath(entity, imageType, imageIndex));
+
+            // Determine the output size based on incoming parameters
+            Size newSize = DrawingUtils.Resize(originalImage.Size, width, height, maxWidth, maxHeight);
+
+            Bitmap thumbnail;
+
+            // Graphics.FromImage will throw an exception if the PixelFormat is Indexed, so we need to handle that here
+            if (originalImage.PixelFormat.HasFlag(PixelFormat.Indexed))
+            {
+                thumbnail = new Bitmap(originalImage, newSize.Width, newSize.Height);
+            }
+            else
+            {
+                thumbnail = new Bitmap(newSize.Width, newSize.Height, originalImage.PixelFormat);
+            }
+
+            thumbnail.MakeTransparent();
+
+            // Preserve the original resolution
+            thumbnail.SetResolution(originalImage.HorizontalResolution, originalImage.VerticalResolution);
+
+            Graphics thumbnailGraph = Graphics.FromImage(thumbnail);
+
+            thumbnailGraph.CompositingQuality = CompositingQuality.HighQuality;
+            thumbnailGraph.SmoothingMode = SmoothingMode.HighQuality;
+            thumbnailGraph.InterpolationMode = InterpolationMode.HighQualityBicubic;
+            thumbnailGraph.PixelOffsetMode = PixelOffsetMode.HighQuality;
+            thumbnailGraph.CompositingMode = CompositingMode.SourceOver;
+
+            thumbnailGraph.DrawImage(originalImage, 0, 0, newSize.Width, newSize.Height);
+
+            ImageFormat outputFormat = originalImage.RawFormat;
+
+            // Write to the output stream
+            SaveImage(outputFormat, thumbnail, toStream, quality);
+
+            thumbnailGraph.Dispose();
+            thumbnail.Dispose();
+            originalImage.Dispose();
+        }
+
+        public static string GetImagePath(BaseEntity entity, ImageType imageType, int imageIndex)
+        {
+            var item = entity as BaseItem;
+
+            if (item != null)
+            {
+                if (imageType == ImageType.Logo)
+                {
+                    return item.LogoImagePath;
+                }
+                if (imageType == ImageType.Backdrop)
+                {
+                    return item.BackdropImagePaths.ElementAt(imageIndex);
+                }
+                if (imageType == ImageType.Banner)
+                {
+                    return item.BannerImagePath;
+                }
+                if (imageType == ImageType.Art)
+                {
+                    return item.ArtImagePath;
+                }
+                if (imageType == ImageType.Thumbnail)
+                {
+                    return item.ThumbnailImagePath;
+                }
+            }
+
+            return entity.PrimaryImagePath;
+        }
+
+        public static void SaveImage(ImageFormat outputFormat, Image newImage, Stream toStream, int? quality)
+        {
+            // Use special save methods for jpeg and png that will result in a much higher quality image
+            // All other formats use the generic Image.Save
+            if (ImageFormat.Jpeg.Equals(outputFormat))
+            {
+                SaveJpeg(newImage, toStream, quality);
+            }
+            else if (ImageFormat.Png.Equals(outputFormat))
+            {
+                newImage.Save(toStream, ImageFormat.Png);
+            }
+            else
+            {
+                newImage.Save(toStream, outputFormat);
+            }
+        }
+
+        public static void SaveJpeg(Image image, Stream target, int? quality)
+        {
+            if (!quality.HasValue)
+            {
+                quality = 90;
+            }
+
+            using (var encoderParameters = new EncoderParameters(1))
+            {
+                encoderParameters.Param[0] = new EncoderParameter(Encoder.Quality, quality.Value);
+                image.Save(target, GetImageCodecInfo("image/jpeg"), encoderParameters);
+            }
+        }
+
+        public static ImageCodecInfo GetImageCodecInfo(string mimeType)
+        {
+            ImageCodecInfo[] info = ImageCodecInfo.GetImageEncoders();
+
+            for (int i = 0; i < info.Length; i++)
+            {
+                ImageCodecInfo ici = info[i];
+                if (ici.MimeType.Equals(mimeType, StringComparison.OrdinalIgnoreCase))
+                {
+                    return ici;
+                }
+            }
+            return info[1];
+        }
+    }
+}

+ 119 - 0
MediaBrowser.Api/HttpHandlers/AudioHandler.cs

@@ -0,0 +1,119 @@
+using MediaBrowser.Common.Net.Handlers;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.DTO;
+using System.Collections.Generic;
+using System.ComponentModel.Composition;
+using System.IO;
+using System.Net;
+
+namespace MediaBrowser.Api.HttpHandlers
+{
+    /// <summary>
+    /// Supported output formats are: mp3,flac,ogg,wav,asf,wma,aac
+    /// </summary>
+    [Export(typeof(BaseHandler))]
+    public class AudioHandler : BaseMediaHandler<Audio, AudioOutputFormats>
+    {
+        public override bool HandlesRequest(HttpListenerRequest request)
+        {
+            return ApiService.IsApiUrlMatch("audio", request);
+        }
+
+        /// <summary>
+        /// We can output these formats directly, but we cannot encode to them.
+        /// </summary>
+        protected override IEnumerable<AudioOutputFormats> UnsupportedOutputEncodingFormats
+        {
+            get
+            {
+                return new AudioOutputFormats[] { AudioOutputFormats.Aac, AudioOutputFormats.Flac, AudioOutputFormats.Wma };
+            }
+        }
+
+        private int? GetMaxAcceptedBitRate(AudioOutputFormats audioFormat)
+        {
+            return GetMaxAcceptedBitRate(audioFormat.ToString());
+        }
+
+        private int? GetMaxAcceptedBitRate(string audioFormat)
+        {
+            if (audioFormat.Equals("mp3", System.StringComparison.OrdinalIgnoreCase))
+            {
+                return 320000;
+            }
+
+            return null;
+        }
+
+        /// <summary>
+        /// Determines whether or not the original file requires transcoding
+        /// </summary>
+        protected override bool RequiresConversion()
+        {
+            if (base.RequiresConversion())
+            {
+                return true;
+            }
+
+            string currentFormat = Path.GetExtension(LibraryItem.Path).Replace(".", string.Empty);
+
+            int? bitrate = GetMaxAcceptedBitRate(currentFormat);
+
+            // If the bitrate is greater than our desired bitrate, we need to transcode
+            if (bitrate.HasValue && bitrate.Value < LibraryItem.BitRate)
+            {
+                return true;
+            }
+
+            // If the number of channels is greater than our desired channels, we need to transcode
+            if (AudioChannels.HasValue && AudioChannels.Value < LibraryItem.Channels)
+            {
+                return true;
+            }
+
+            // If the sample rate is greater than our desired sample rate, we need to transcode
+            if (AudioSampleRate.HasValue && AudioSampleRate.Value < LibraryItem.SampleRate)
+            {
+                return true;
+            }
+
+            // Yay
+            return false;
+        }
+
+        /// <summary>
+        /// Creates arguments to pass to ffmpeg
+        /// </summary>
+        protected override string GetCommandLineArguments()
+        {
+            var audioTranscodeParams = new List<string>();
+
+            AudioOutputFormats outputFormat = GetConversionOutputFormat();
+
+            int? bitrate = GetMaxAcceptedBitRate(outputFormat);
+
+            if (bitrate.HasValue)
+            {
+                audioTranscodeParams.Add("-ab " + bitrate.Value);
+            }
+
+            int? channels = GetNumAudioChannelsParam(LibraryItem.Channels);
+
+            if (channels.HasValue)
+            {
+                audioTranscodeParams.Add("-ac " + channels.Value);
+            }
+
+            int? sampleRate = GetSampleRateParam(LibraryItem.SampleRate);
+
+            if (sampleRate.HasValue)
+            {
+                audioTranscodeParams.Add("-ar " + sampleRate.Value);
+            }
+
+            audioTranscodeParams.Add("-f " + outputFormat);
+
+            return "-i \"" + LibraryItem.Path + "\" -vn " + string.Join(" ", audioTranscodeParams.ToArray()) + " -";
+        }
+    }
+}

+ 255 - 0
MediaBrowser.Api/HttpHandlers/BaseMediaHandler.cs

@@ -0,0 +1,255 @@
+using MediaBrowser.Common.Logging;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Common.Net.Handlers;
+using MediaBrowser.Controller;
+using MediaBrowser.Controller.Entities;
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Net;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Api.HttpHandlers
+{
+    public abstract class BaseMediaHandler<TBaseItemType, TOutputType> : BaseHandler
+        where TBaseItemType : BaseItem, new()
+    {
+        /// <summary>
+        /// Supported values: mp3,flac,ogg,wav,asf,wma,aac
+        /// </summary>
+        protected virtual IEnumerable<TOutputType> OutputFormats
+        {
+            get
+            {
+                return QueryString["outputformats"].Split(',').Select(o => (TOutputType)Enum.Parse(typeof(TOutputType), o, true));
+            }
+        }
+
+        /// <summary>
+        /// These formats can be outputted directly but cannot be encoded to
+        /// </summary>
+        protected virtual IEnumerable<TOutputType> UnsupportedOutputEncodingFormats
+        {
+            get
+            {
+                return new TOutputType[] { };
+            }
+        }
+
+        private TBaseItemType _libraryItem;
+        /// <summary>
+        /// Gets the library item that will be played, if any
+        /// </summary>
+        protected TBaseItemType LibraryItem
+        {
+            get
+            {
+                if (_libraryItem == null)
+                {
+                    string id = QueryString["id"];
+
+                    if (!string.IsNullOrEmpty(id))
+                    {
+                        _libraryItem = Kernel.Instance.GetItemById(Guid.Parse(id)) as TBaseItemType;
+                    }
+                }
+
+                return _libraryItem;
+            }
+        }
+
+        public int? AudioChannels
+        {
+            get
+            {
+                string val = QueryString["audiochannels"];
+
+                if (string.IsNullOrEmpty(val))
+                {
+                    return null;
+                }
+
+                return int.Parse(val);
+            }
+        }
+
+        public int? AudioSampleRate
+        {
+            get
+            {
+                string val = QueryString["audiosamplerate"];
+
+                if (string.IsNullOrEmpty(val))
+                {
+                    return 44100;
+                }
+
+                return int.Parse(val);
+            }
+        }
+
+        protected override Task<ResponseInfo> GetResponseInfo()
+        {
+            ResponseInfo info = new ResponseInfo
+            {
+                ContentType = MimeTypes.GetMimeType("." + GetConversionOutputFormat()),
+                CompressResponse = false
+            };
+
+            return Task.FromResult<ResponseInfo>(info);
+        }
+
+        public override Task ProcessRequest(HttpListenerContext ctx)
+        {
+            HttpListenerContext = ctx;
+
+            if (!RequiresConversion())
+            {
+                return new StaticFileHandler { Path = LibraryItem.Path }.ProcessRequest(ctx);
+            }
+
+            return base.ProcessRequest(ctx);
+        }
+
+        protected abstract string GetCommandLineArguments();
+
+        /// <summary>
+        /// Gets the format we'll be converting to
+        /// </summary>
+        protected virtual TOutputType GetConversionOutputFormat()
+        {
+            return OutputFormats.First(f => !UnsupportedOutputEncodingFormats.Any(s => s.ToString().Equals(f.ToString(), StringComparison.OrdinalIgnoreCase)));
+        }
+
+        protected virtual bool RequiresConversion()
+        {
+            string currentFormat = Path.GetExtension(LibraryItem.Path).Replace(".", string.Empty);
+
+            if (OutputFormats.Any(f => currentFormat.EndsWith(f.ToString(), StringComparison.OrdinalIgnoreCase)))
+            {
+                // We can output these files directly, but we can't encode them
+                if (UnsupportedOutputEncodingFormats.Any(f => currentFormat.EndsWith(f.ToString(), StringComparison.OrdinalIgnoreCase)))
+                {
+                    return false;
+                }
+            }
+            else
+            {
+                // If it's not in a format the consumer accepts, return true
+                return true;
+            }
+
+            return false;
+        }
+
+        private FileStream LogFileStream { get; set; }
+
+        protected async override Task WriteResponseToOutputStream(Stream stream)
+        {
+            var startInfo = new ProcessStartInfo{};
+
+            startInfo.CreateNoWindow = true;
+
+            startInfo.UseShellExecute = false;
+
+            // Must consume both or ffmpeg may hang due to deadlocks. See comments below.
+            startInfo.RedirectStandardOutput = true;
+            startInfo.RedirectStandardError = true;
+
+            startInfo.FileName = Kernel.Instance.ApplicationPaths.FFMpegPath;
+            startInfo.WorkingDirectory = Kernel.Instance.ApplicationPaths.FFMpegDirectory;
+            startInfo.Arguments = GetCommandLineArguments();
+
+            Logger.LogInfo(startInfo.FileName + " " + startInfo.Arguments);
+
+            var process = new Process{};
+            process.StartInfo = startInfo;
+
+            // FFMpeg writes debug/error info to stderr. This is useful when debugging so let's put it in the log directory.
+            LogFileStream = new FileStream(Path.Combine(Kernel.Instance.ApplicationPaths.LogDirectoryPath, "ffmpeg-" + Guid.NewGuid().ToString() + ".txt"), FileMode.Create);
+
+            process.EnableRaisingEvents = true;
+
+            process.Exited += ProcessExited;
+
+            try
+            {
+                process.Start();
+
+                // MUST read both stdout and stderr asynchronously or a deadlock may occurr
+
+                // Kick off two tasks
+                Task mediaTask = process.StandardOutput.BaseStream.CopyToAsync(stream);
+                Task debugLogTask = process.StandardError.BaseStream.CopyToAsync(LogFileStream);
+
+                await mediaTask.ConfigureAwait(false);
+                //await debugLogTask.ConfigureAwait(false);
+            }
+            catch (Exception ex)
+            {
+                Logger.LogException(ex);
+
+                // Hate having to do this
+                try
+                {
+                    process.Kill();
+                }
+                catch
+                {
+                }
+            }
+        }
+
+        void ProcessExited(object sender, EventArgs e)
+        {
+            if (LogFileStream != null)
+            {
+                LogFileStream.Dispose();
+            }
+
+            var process = sender as Process;
+
+            Logger.LogInfo("FFMpeg exited with code " + process.ExitCode);
+
+            process.Dispose();
+        }
+
+        /// <summary>
+        /// Gets the number of audio channels to specify on the command line
+        /// </summary>
+        protected int? GetNumAudioChannelsParam(int libraryItemChannels)
+        {
+            // If the user requested a max number of channels
+            if (AudioChannels.HasValue)
+            {
+                // Only specify the param if we're going to downmix
+                if (AudioChannels.Value < libraryItemChannels)
+                {
+                    return AudioChannels.Value;
+                }
+            }
+
+            return null;
+        }
+
+        /// <summary>
+        /// Gets the number of audio channels to specify on the command line
+        /// </summary>
+        protected int? GetSampleRateParam(int libraryItemSampleRate)
+        {
+            // If the user requested a max value
+            if (AudioSampleRate.HasValue)
+            {
+                // Only specify the param if we're going to downmix
+                if (AudioSampleRate.Value < libraryItemSampleRate)
+                {
+                    return AudioSampleRate.Value;
+                }
+            }
+
+            return null;
+        }
+    }
+}

+ 38 - 0
MediaBrowser.Api/HttpHandlers/FavoriteStatusHandler.cs

@@ -0,0 +1,38 @@
+using MediaBrowser.Common.Net.Handlers;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.DTO;
+using System.ComponentModel.Composition;
+using System.Net;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Api.HttpHandlers
+{
+    /// <summary>
+    /// Provides a handler to set user favorite status for an item
+    /// </summary>
+    [Export(typeof(BaseHandler))]
+    public class FavoriteStatusHandler : BaseSerializationHandler<DtoUserItemData>
+    {
+        public override bool HandlesRequest(HttpListenerRequest request)
+        {
+            return ApiService.IsApiUrlMatch("FavoriteStatus", request);
+        }
+
+        protected override Task<DtoUserItemData> GetObjectToSerialize()
+        {
+            // Get the item
+            BaseItem item = ApiService.GetItemById(QueryString["id"]);
+
+            // Get the user
+            User user = ApiService.GetUserById(QueryString["userid"], true);
+
+            // Get the user data for this item
+            UserItemData data = item.GetUserData(user, true);
+
+            // Set favorite status
+            data.IsFavorite = QueryString["isfavorite"] == "1";
+
+            return Task.FromResult(ApiService.GetDtoUserItemData(data));
+        }
+    }
+}

+ 57 - 0
MediaBrowser.Api/HttpHandlers/GenreHandler.cs

@@ -0,0 +1,57 @@
+using MediaBrowser.Common.Net.Handlers;
+using MediaBrowser.Controller;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.DTO;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.Composition;
+using System.Linq;
+using System.Net;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Api.HttpHandlers
+{
+    /// <summary>
+    /// Gets a single genre
+    /// </summary>
+    [Export(typeof(BaseHandler))]
+    public class GenreHandler : BaseSerializationHandler<IbnItem>
+    {
+        public override bool HandlesRequest(HttpListenerRequest request)
+        {
+            return ApiService.IsApiUrlMatch("genre", request);
+        }
+
+        protected override Task<IbnItem> GetObjectToSerialize()
+        {
+            var parent = ApiService.GetItemById(QueryString["id"]) as Folder;
+            var user = ApiService.GetUserById(QueryString["userid"], true);
+
+            string name = QueryString["name"];
+
+            return GetGenre(parent, user, name);
+        }
+
+        /// <summary>
+        /// Gets a Genre
+        /// </summary>
+        private async Task<IbnItem> GetGenre(Folder parent, User user, string name)
+        {
+            int count = 0;
+
+            // Get all the allowed recursive children
+            IEnumerable<BaseItem> allItems = parent.GetRecursiveChildren(user);
+
+            foreach (var item in allItems)
+            {
+                if (item.Genres != null && item.Genres.Any(s => s.Equals(name, StringComparison.OrdinalIgnoreCase)))
+                {
+                    count++;
+                }
+            }
+
+            // Get the original entity so that we can also supply the PrimaryImagePath
+            return ApiService.GetIbnItem(await Kernel.Instance.ItemController.GetGenre(name).ConfigureAwait(false), count);
+        }
+    }
+}

+ 78 - 0
MediaBrowser.Api/HttpHandlers/GenresHandler.cs

@@ -0,0 +1,78 @@
+using MediaBrowser.Common.Net.Handlers;
+using MediaBrowser.Controller;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.DTO;
+using System.Collections.Generic;
+using System.ComponentModel.Composition;
+using System.Linq;
+using System.Net;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Api.HttpHandlers
+{
+    [Export(typeof(BaseHandler))]
+    public class GenresHandler : BaseSerializationHandler<IbnItem[]>
+    {
+        public override bool HandlesRequest(HttpListenerRequest request)
+        {
+            return ApiService.IsApiUrlMatch("genres", request);
+        }
+
+        protected override Task<IbnItem[]> GetObjectToSerialize()
+        {
+            var parent = ApiService.GetItemById(QueryString["id"]) as Folder;
+            User user = ApiService.GetUserById(QueryString["userid"], true);
+
+            return GetAllGenres(parent, user);
+        }
+
+        /// <summary>
+        /// Gets all genres from all recursive children of a folder
+        /// The CategoryInfo class is used to keep track of the number of times each genres appears
+        /// </summary>
+        private async Task<IbnItem[]> GetAllGenres(Folder parent, User user)
+        {
+            var data = new Dictionary<string, int>();
+
+            // Get all the allowed recursive children
+            IEnumerable<BaseItem> allItems = parent.GetRecursiveChildren(user);
+
+            foreach (var item in allItems)
+            {
+                // Add each genre from the item to the data dictionary
+                // If the genre already exists, increment the count
+                if (item.Genres == null)
+                {
+                    continue;
+                }
+
+                foreach (string val in item.Genres)
+                {
+                    if (!data.ContainsKey(val))
+                    {
+                        data.Add(val, 1);
+                    }
+                    else
+                    {
+                        data[val]++;
+                    }
+                }
+            }
+
+            // Get the Genre objects
+            Genre[] entities = await Task.WhenAll(data.Keys.Select(key => Kernel.Instance.ItemController.GetGenre(key))).ConfigureAwait(false);
+
+            // Convert to an array of IBNItem
+            var items = new IbnItem[entities.Length];
+
+            for (int i = 0; i < entities.Length; i++)
+            {
+                Genre e = entities[i];
+
+                items[i] = ApiService.GetIbnItem(e, data[e.Name]);
+            }
+
+            return items;
+        }
+    }
+}

+ 224 - 0
MediaBrowser.Api/HttpHandlers/ImageHandler.cs

@@ -0,0 +1,224 @@
+using MediaBrowser.Common.Net;
+using MediaBrowser.Common.Net.Handlers;
+using MediaBrowser.Controller;
+using MediaBrowser.Controller.Drawing;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.Entities;
+using System;
+using System.ComponentModel.Composition;
+using System.IO;
+using System.Net;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Api.HttpHandlers
+{
+    [Export(typeof(BaseHandler))]
+    public class ImageHandler : BaseHandler
+    {
+        public override bool HandlesRequest(HttpListenerRequest request)
+        {
+            return ApiService.IsApiUrlMatch("image", request);
+        }
+
+        private string _imagePath;
+
+        private async Task<string> GetImagePath()
+        {
+            _imagePath = _imagePath ?? await DiscoverImagePath();
+
+            return _imagePath;
+        }
+
+        private BaseEntity _sourceEntity;
+
+        private async Task<BaseEntity> GetSourceEntity()
+        {
+            if (_sourceEntity == null)
+            {
+                if (!string.IsNullOrEmpty(QueryString["personname"]))
+                {
+                    _sourceEntity =
+                        await Kernel.Instance.ItemController.GetPerson(QueryString["personname"]).ConfigureAwait(false);
+                }
+
+                else if (!string.IsNullOrEmpty(QueryString["genre"]))
+                {
+                    _sourceEntity =
+                        await Kernel.Instance.ItemController.GetGenre(QueryString["genre"]).ConfigureAwait(false);
+                }
+
+                else if (!string.IsNullOrEmpty(QueryString["year"]))
+                {
+                    _sourceEntity =
+                        await
+                        Kernel.Instance.ItemController.GetYear(int.Parse(QueryString["year"])).ConfigureAwait(false);
+                }
+
+                else if (!string.IsNullOrEmpty(QueryString["studio"]))
+                {
+                    _sourceEntity =
+                        await Kernel.Instance.ItemController.GetStudio(QueryString["studio"]).ConfigureAwait(false);
+                }
+
+                else if (!string.IsNullOrEmpty(QueryString["userid"]))
+                {
+                    _sourceEntity = ApiService.GetUserById(QueryString["userid"], false);
+                }
+
+                else
+                {
+                    _sourceEntity = ApiService.GetItemById(QueryString["id"]);
+                }
+            }
+
+            return _sourceEntity;
+        }
+
+        private async Task<string> DiscoverImagePath()
+        {
+            var entity = await GetSourceEntity().ConfigureAwait(false);
+
+            return ImageProcessor.GetImagePath(entity, ImageType, ImageIndex);
+        }
+
+        protected async override Task<ResponseInfo> GetResponseInfo()
+        {
+            string path = await GetImagePath().ConfigureAwait(false);
+
+            ResponseInfo info = new ResponseInfo
+            {
+                CacheDuration = TimeSpan.FromDays(365),
+                ContentType = MimeTypes.GetMimeType(path)
+            };
+
+            DateTime? date = File.GetLastWriteTimeUtc(path);
+
+            // If the file does not exist it will return jan 1, 1601
+            // http://msdn.microsoft.com/en-us/library/system.io.file.getlastwritetimeutc.aspx
+            if (date.Value.Year == 1601)
+            {
+                if (!File.Exists(path))
+                {
+                    info.StatusCode = 404;
+                    date = null;
+                }
+            }
+
+            info.DateLastModified = date;
+
+            return info;
+        }
+
+        private int ImageIndex
+        {
+            get
+            {
+                string val = QueryString["index"];
+
+                if (string.IsNullOrEmpty(val))
+                {
+                    return 0;
+                }
+
+                return int.Parse(val);
+            }
+        }
+
+        private int? Height
+        {
+            get
+            {
+                string val = QueryString["height"];
+
+                if (string.IsNullOrEmpty(val))
+                {
+                    return null;
+                }
+
+                return int.Parse(val);
+            }
+        }
+
+        private int? Width
+        {
+            get
+            {
+                string val = QueryString["width"];
+
+                if (string.IsNullOrEmpty(val))
+                {
+                    return null;
+                }
+
+                return int.Parse(val);
+            }
+        }
+
+        private int? MaxHeight
+        {
+            get
+            {
+                string val = QueryString["maxheight"];
+
+                if (string.IsNullOrEmpty(val))
+                {
+                    return null;
+                }
+
+                return int.Parse(val);
+            }
+        }
+
+        private int? MaxWidth
+        {
+            get
+            {
+                string val = QueryString["maxwidth"];
+
+                if (string.IsNullOrEmpty(val))
+                {
+                    return null;
+                }
+
+                return int.Parse(val);
+            }
+        }
+
+        private int? Quality
+        {
+            get
+            {
+                string val = QueryString["quality"];
+
+                if (string.IsNullOrEmpty(val))
+                {
+                    return null;
+                }
+
+                return int.Parse(val);
+            }
+        }
+
+        private ImageType ImageType
+        {
+            get
+            {
+                string imageType = QueryString["type"];
+
+                if (string.IsNullOrEmpty(imageType))
+                {
+                    return ImageType.Primary;
+                }
+
+                return (ImageType)Enum.Parse(typeof(ImageType), imageType, true);
+            }
+        }
+
+        protected override async Task WriteResponseToOutputStream(Stream stream)
+        {
+            var entity = await GetSourceEntity().ConfigureAwait(false);
+
+            ImageProcessor.ProcessImage(entity, ImageType, ImageIndex, stream, Width, Height, MaxWidth, MaxHeight, Quality);
+        }
+    }
+}

+ 35 - 0
MediaBrowser.Api/HttpHandlers/ItemHandler.cs

@@ -0,0 +1,35 @@
+using MediaBrowser.Common.Net.Handlers;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.DTO;
+using System.ComponentModel.Composition;
+using System.Net;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Api.HttpHandlers
+{
+    /// <summary>
+    /// Provides a handler to retrieve a single item
+    /// </summary>
+    [Export(typeof(BaseHandler))]
+    public class ItemHandler : BaseSerializationHandler<DtoBaseItem>
+    {
+        public override bool HandlesRequest(HttpListenerRequest request)
+        {
+            return ApiService.IsApiUrlMatch("item", request);
+        }
+        
+        protected override Task<DtoBaseItem> GetObjectToSerialize()
+        {
+            User user = ApiService.GetUserById(QueryString["userid"], true);
+
+            BaseItem item = ApiService.GetItemById(QueryString["id"]);
+
+            if (item == null)
+            {
+                return null;
+            }
+
+            return ApiService.GetDtoBaseItem(item, user);
+        }
+    }
+}

+ 84 - 0
MediaBrowser.Api/HttpHandlers/ItemListHandler.cs

@@ -0,0 +1,84 @@
+using MediaBrowser.Common.Net.Handlers;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.DTO;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.Composition;
+using System.Linq;
+using System.Net;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Api.HttpHandlers
+{
+    [Export(typeof(BaseHandler))]
+    public class ItemListHandler : BaseSerializationHandler<DtoBaseItem[]>
+    {
+        public override bool HandlesRequest(HttpListenerRequest request)
+        {
+            return ApiService.IsApiUrlMatch("itemlist", request);
+        }
+
+        protected override Task<DtoBaseItem[]> GetObjectToSerialize()
+        {
+            User user = ApiService.GetUserById(QueryString["userid"], true);
+
+            return Task.WhenAll(GetItemsToSerialize(user).Select(i => ApiService.GetDtoBaseItem(i, user, includeChildren: false, includePeople: false)));
+        }
+
+        private IEnumerable<BaseItem> GetItemsToSerialize(User user)
+        {
+            var parent = ApiService.GetItemById(ItemId) as Folder;
+
+            if (ListType.Equals("inprogressitems", StringComparison.OrdinalIgnoreCase))
+            {
+                return parent.GetInProgressItems(user);
+            }
+            if (ListType.Equals("recentlyaddeditems", StringComparison.OrdinalIgnoreCase))
+            {
+                return parent.GetRecentlyAddedItems(user);
+            }
+            if (ListType.Equals("recentlyaddedunplayeditems", StringComparison.OrdinalIgnoreCase))
+            {
+                return parent.GetRecentlyAddedUnplayedItems(user);
+            }
+            if (ListType.Equals("itemswithgenre", StringComparison.OrdinalIgnoreCase))
+            {
+                return parent.GetItemsWithGenre(QueryString["name"], user);
+            }
+            if (ListType.Equals("itemswithyear", StringComparison.OrdinalIgnoreCase))
+            {
+                return parent.GetItemsWithYear(int.Parse(QueryString["year"]), user);
+            }
+            if (ListType.Equals("itemswithstudio", StringComparison.OrdinalIgnoreCase))
+            {
+                return parent.GetItemsWithStudio(QueryString["name"], user);
+            }
+            if (ListType.Equals("itemswithperson", StringComparison.OrdinalIgnoreCase))
+            {
+                return parent.GetItemsWithPerson(QueryString["name"], null, user);
+            }
+            if (ListType.Equals("favorites", StringComparison.OrdinalIgnoreCase))
+            {
+                return parent.GetFavoriteItems(user);
+            }
+
+            throw new InvalidOperationException();
+        }
+
+        protected string ItemId
+        {
+            get
+            {
+                return QueryString["id"];
+            }
+        }
+
+        private string ListType
+        {
+            get
+            {
+                return QueryString["listtype"] ?? string.Empty;
+            }
+        }
+    }
+}

+ 46 - 0
MediaBrowser.Api/HttpHandlers/MovieSpecialFeaturesHandler.cs

@@ -0,0 +1,46 @@
+using MediaBrowser.Common.Net.Handlers;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Movies;
+using MediaBrowser.Model.DTO;
+using System.ComponentModel.Composition;
+using System.Linq;
+using System.Net;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Api.HttpHandlers
+{
+    /// <summary>
+    /// This handler retrieves special features for movies
+    /// </summary>
+    [Export(typeof(BaseHandler))]
+    public class MovieSpecialFeaturesHandler : BaseSerializationHandler<DtoBaseItem[]>
+    {
+        public override bool HandlesRequest(HttpListenerRequest request)
+        {
+            return ApiService.IsApiUrlMatch("MovieSpecialFeatures", request);
+        }
+
+        protected override Task<DtoBaseItem[]> GetObjectToSerialize()
+        {
+            User user = ApiService.GetUserById(QueryString["userid"], true);
+
+            var movie = ApiService.GetItemById(ItemId) as Movie;
+
+            // If none
+            if (movie.SpecialFeatures == null)
+            {
+                return Task.FromResult(new DtoBaseItem[] { });
+            }
+
+            return Task.WhenAll(movie.SpecialFeatures.Select(i => ApiService.GetDtoBaseItem(i, user, includeChildren: false)));
+        }
+
+        protected string ItemId
+        {
+            get
+            {
+                return QueryString["id"];
+            }
+        }
+    }
+}

+ 55 - 0
MediaBrowser.Api/HttpHandlers/PersonHandler.cs

@@ -0,0 +1,55 @@
+using MediaBrowser.Common.Net.Handlers;
+using MediaBrowser.Controller;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.DTO;
+using System.Collections.Generic;
+using System.ComponentModel.Composition;
+using System.Net;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Api.HttpHandlers
+{
+    /// <summary>
+    /// Gets a single Person
+    /// </summary>
+    [Export(typeof(BaseHandler))]
+    public class PersonHandler : BaseSerializationHandler<IbnItem>
+    {
+        public override bool HandlesRequest(HttpListenerRequest request)
+        {
+            return ApiService.IsApiUrlMatch("person", request);
+        }
+
+        protected override Task<IbnItem> GetObjectToSerialize()
+        {
+            var parent = ApiService.GetItemById(QueryString["id"]) as Folder;
+            var user = ApiService.GetUserById(QueryString["userid"], true);
+
+            string name = QueryString["name"];
+
+            return GetPerson(parent, user, name);
+        }
+
+        /// <summary>
+        /// Gets a Person
+        /// </summary>
+        private async Task<IbnItem> GetPerson(Folder parent, User user, string name)
+        {
+            int count = 0;
+
+            // Get all the allowed recursive children
+            IEnumerable<BaseItem> allItems = parent.GetRecursiveChildren(user);
+
+            foreach (var item in allItems)
+            {
+                if (item.People != null && item.People.ContainsKey(name))
+                {
+                    count++;
+                }
+            }
+
+            // Get the original entity so that we can also supply the PrimaryImagePath
+            return ApiService.GetIbnItem(await Kernel.Instance.ItemController.GetPerson(name).ConfigureAwait(false), count);
+        }
+    }
+}

+ 38 - 0
MediaBrowser.Api/HttpHandlers/PlayedStatusHandler.cs

@@ -0,0 +1,38 @@
+using MediaBrowser.Common.Net.Handlers;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.DTO;
+using System.ComponentModel.Composition;
+using System.Net;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Api.HttpHandlers
+{
+    /// <summary>
+    /// Provides a handler to set played status for an item
+    /// </summary>
+    [Export(typeof(BaseHandler))]
+    public class PlayedStatusHandler : BaseSerializationHandler<DtoUserItemData>
+    {
+        public override bool HandlesRequest(HttpListenerRequest request)
+        {
+            return ApiService.IsApiUrlMatch("PlayedStatus", request);
+        }
+
+        protected override Task<DtoUserItemData> GetObjectToSerialize()
+        {
+            // Get the item
+            BaseItem item = ApiService.GetItemById(QueryString["id"]);
+
+            // Get the user
+            User user = ApiService.GetUserById(QueryString["userid"], true);
+
+            bool wasPlayed = QueryString["played"] == "1";
+
+            item.SetPlayedStatus(user, wasPlayed);
+
+            UserItemData data = item.GetUserData(user, true);
+
+            return Task.FromResult(ApiService.GetDtoUserItemData(data));
+        }
+    }
+}

+ 38 - 0
MediaBrowser.Api/HttpHandlers/PluginAssemblyHandler.cs

@@ -0,0 +1,38 @@
+using MediaBrowser.Common.Net.Handlers;
+using MediaBrowser.Controller;
+using System;
+using System.ComponentModel.Composition;
+using System.IO;
+using System.Net;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Api.HttpHandlers
+{
+    [Export(typeof(BaseHandler))]
+    class PluginAssemblyHandler : BaseHandler
+    {
+        public override bool HandlesRequest(HttpListenerRequest request)
+        {
+            return ApiService.IsApiUrlMatch("pluginassembly", request);
+        }
+
+        protected override Task<ResponseInfo> GetResponseInfo()
+        {
+            throw new NotImplementedException();
+        }
+
+        protected override Task WriteResponseToOutputStream(Stream stream)
+        {
+            throw new NotImplementedException();
+        }
+
+        public override Task ProcessRequest(HttpListenerContext ctx)
+        {
+            string filename = ctx.Request.QueryString["assemblyfilename"];
+
+            string path = Path.Combine(Kernel.Instance.ApplicationPaths.PluginsPath, filename);
+
+            return new StaticFileHandler { Path = path }.ProcessRequest(ctx);
+        }
+    }
+}

+ 53 - 0
MediaBrowser.Api/HttpHandlers/PluginConfigurationHandler.cs

@@ -0,0 +1,53 @@
+using MediaBrowser.Common.Net.Handlers;
+using MediaBrowser.Common.Plugins;
+using MediaBrowser.Controller;
+using MediaBrowser.Model.Plugins;
+using System;
+using System.ComponentModel.Composition;
+using System.Linq;
+using System.Net;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Api.HttpHandlers
+{
+    [Export(typeof(BaseHandler))]
+    public class PluginConfigurationHandler : BaseSerializationHandler<BasePluginConfiguration>
+    {
+        public override bool HandlesRequest(HttpListenerRequest request)
+        {
+            return ApiService.IsApiUrlMatch("pluginconfiguration", request);
+        }
+
+        private BasePlugin _plugin;
+        private BasePlugin Plugin
+        {
+            get
+            {
+                if (_plugin == null)
+                {
+                    string name = QueryString["assemblyfilename"];
+
+                    _plugin = Kernel.Instance.Plugins.First(p => p.AssemblyFileName.Equals(name, StringComparison.OrdinalIgnoreCase));
+                }
+
+                return _plugin;
+            }
+        }
+
+        protected override Task<BasePluginConfiguration> GetObjectToSerialize()
+        {
+            return Task.FromResult(Plugin.Configuration);
+        }
+
+        protected override async Task<ResponseInfo> GetResponseInfo()
+        {
+            var info = await base.GetResponseInfo().ConfigureAwait(false);
+
+            info.DateLastModified = Plugin.ConfigurationDateLastModified;
+
+            info.CacheDuration = TimeSpan.FromDays(7);
+
+            return info;
+        }
+    }
+}

+ 38 - 0
MediaBrowser.Api/HttpHandlers/PluginsHandler.cs

@@ -0,0 +1,38 @@
+using MediaBrowser.Common.Net.Handlers;
+using MediaBrowser.Controller;
+using MediaBrowser.Model.DTO;
+using System.Collections.Generic;
+using System.ComponentModel.Composition;
+using System.Linq;
+using System.Net;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Api.HttpHandlers
+{
+    /// <summary>
+    /// Provides information about installed plugins
+    /// </summary>
+    [Export(typeof(BaseHandler))]
+    public class PluginsHandler : BaseSerializationHandler<IEnumerable<PluginInfo>>
+    {
+        public override bool HandlesRequest(HttpListenerRequest request)
+        {
+            return ApiService.IsApiUrlMatch("plugins", request);
+        }
+
+        protected override Task<IEnumerable<PluginInfo>> GetObjectToSerialize()
+        {
+            var plugins = Kernel.Instance.Plugins.Select(p => new PluginInfo
+            {
+                Name = p.Name,
+                Enabled = p.Enabled,
+                DownloadToUI = p.DownloadToUi,
+                Version = p.Version.ToString(),
+                AssemblyFileName = p.AssemblyFileName,
+                ConfigurationDateLastModified = p.ConfigurationDateLastModified
+            });
+
+            return Task.FromResult(plugins);
+        }
+    }
+}

+ 37 - 0
MediaBrowser.Api/HttpHandlers/ServerConfigurationHandler.cs

@@ -0,0 +1,37 @@
+using MediaBrowser.Common.Net.Handlers;
+using MediaBrowser.Controller;
+using MediaBrowser.Model.Configuration;
+using System;
+using System.ComponentModel.Composition;
+using System.IO;
+using System.Net;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Api.HttpHandlers
+{
+    [Export(typeof(BaseHandler))]
+    class ServerConfigurationHandler : BaseSerializationHandler<ServerConfiguration>
+    {
+        public override bool HandlesRequest(HttpListenerRequest request)
+        {
+            return ApiService.IsApiUrlMatch("serverconfiguration", request);
+        }
+
+        protected override Task<ServerConfiguration> GetObjectToSerialize()
+        {
+            return Task.FromResult(Kernel.Instance.Configuration);
+        }
+
+        protected override async Task<ResponseInfo> GetResponseInfo()
+        {
+            var info = await base.GetResponseInfo().ConfigureAwait(false);
+
+            info.DateLastModified =
+                File.GetLastWriteTimeUtc(Kernel.Instance.ApplicationPaths.SystemConfigurationFilePath);
+
+            info.CacheDuration = TimeSpan.FromDays(7);
+
+            return info;
+        }
+    }
+}

+ 57 - 0
MediaBrowser.Api/HttpHandlers/StudioHandler.cs

@@ -0,0 +1,57 @@
+using MediaBrowser.Common.Net.Handlers;
+using MediaBrowser.Controller;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.DTO;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.Composition;
+using System.Linq;
+using System.Net;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Api.HttpHandlers
+{
+    /// <summary>
+    /// Gets a single studio
+    /// </summary>
+    [Export(typeof(BaseHandler))]
+    public class StudioHandler : BaseSerializationHandler<IbnItem>
+    {
+        public override bool HandlesRequest(HttpListenerRequest request)
+        {
+            return ApiService.IsApiUrlMatch("studio", request);
+        }
+
+        protected override Task<IbnItem> GetObjectToSerialize()
+        {
+            var parent = ApiService.GetItemById(QueryString["id"]) as Folder;
+            var user = ApiService.GetUserById(QueryString["userid"], true);
+
+            string name = QueryString["name"];
+
+            return GetStudio(parent, user, name);
+        }
+
+        /// <summary>
+        /// Gets a Studio
+        /// </summary>
+        private async Task<IbnItem> GetStudio(Folder parent, User user, string name)
+        {
+            int count = 0;
+
+            // Get all the allowed recursive children
+            IEnumerable<BaseItem> allItems = parent.GetRecursiveChildren(user);
+
+            foreach (var item in allItems)
+            {
+                if (item.Studios != null && item.Studios.Any(s => s.Equals(name, StringComparison.OrdinalIgnoreCase)))
+                {
+                    count++;
+                }
+            }
+
+            // Get the original entity so that we can also supply the PrimaryImagePath
+            return ApiService.GetIbnItem(await Kernel.Instance.ItemController.GetStudio(name).ConfigureAwait(false), count);
+        }
+    }
+}

+ 78 - 0
MediaBrowser.Api/HttpHandlers/StudiosHandler.cs

@@ -0,0 +1,78 @@
+using MediaBrowser.Common.Net.Handlers;
+using MediaBrowser.Controller;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.DTO;
+using System.Collections.Generic;
+using System.ComponentModel.Composition;
+using System.Linq;
+using System.Net;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Api.HttpHandlers
+{
+    [Export(typeof(BaseHandler))]
+    public class StudiosHandler : BaseSerializationHandler<IbnItem[]>
+    {
+        public override bool HandlesRequest(HttpListenerRequest request)
+        {
+            return ApiService.IsApiUrlMatch("studios", request);
+        }
+
+        protected override Task<IbnItem[]> GetObjectToSerialize()
+        {
+            var parent = ApiService.GetItemById(QueryString["id"]) as Folder;
+            var user = ApiService.GetUserById(QueryString["userid"], true);
+
+            return GetAllStudios(parent, user);
+        }
+
+        /// <summary>
+        /// Gets all studios from all recursive children of a folder
+        /// The CategoryInfo class is used to keep track of the number of times each studio appears
+        /// </summary>
+        private async Task<IbnItem[]> GetAllStudios(Folder parent, User user)
+        {
+            var data = new Dictionary<string, int>();
+
+            // Get all the allowed recursive children
+            IEnumerable<BaseItem> allItems = parent.GetRecursiveChildren(user);
+
+            foreach (var item in allItems)
+            {
+                // Add each studio from the item to the data dictionary
+                // If the studio already exists, increment the count
+                if (item.Studios == null)
+                {
+                    continue;
+                }
+
+                foreach (string val in item.Studios)
+                {
+                    if (!data.ContainsKey(val))
+                    {
+                        data.Add(val, 1);
+                    }
+                    else
+                    {
+                        data[val]++;
+                    }
+                }
+            }
+
+            // Get the Studio objects
+            Studio[] entities = await Task.WhenAll(data.Keys.Select(key => Kernel.Instance.ItemController.GetStudio(key))).ConfigureAwait(false);
+
+            // Convert to an array of IBNItem
+            var items = new IbnItem[entities.Length];
+
+            for (int i = 0; i < entities.Length; i++)
+            {
+                Studio e = entities[i];
+
+                items[i] = ApiService.GetIbnItem(e, data[e.Name]);
+            }
+
+            return items;
+        }
+    }
+}

+ 29 - 0
MediaBrowser.Api/HttpHandlers/UserAuthenticationHandler.cs

@@ -0,0 +1,29 @@
+using MediaBrowser.Common.Net.Handlers;
+using MediaBrowser.Controller;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.Authentication;
+using System.ComponentModel.Composition;
+using System.Net;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Api.HttpHandlers
+{
+    [Export(typeof(BaseHandler))]
+    class UserAuthenticationHandler : BaseSerializationHandler<AuthenticationResult>
+    {
+        public override bool HandlesRequest(HttpListenerRequest request)
+        {
+            return ApiService.IsApiUrlMatch("UserAuthentication", request);
+        }
+        
+        protected override async Task<AuthenticationResult> GetObjectToSerialize()
+        {
+            string userId = await GetFormValue("userid").ConfigureAwait(false);
+            User user = ApiService.GetUserById(userId, false);
+
+            string password = await GetFormValue("password").ConfigureAwait(false);
+
+            return Kernel.Instance.AuthenticateUser(user, password);
+        }
+    }
+}

+ 29 - 0
MediaBrowser.Api/HttpHandlers/UserHandler.cs

@@ -0,0 +1,29 @@
+using MediaBrowser.Common.Net.Handlers;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.DTO;
+using System.ComponentModel.Composition;
+using System.Net;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Api.HttpHandlers
+{
+    [Export(typeof(BaseHandler))]
+    class UserHandler : BaseSerializationHandler<DtoUser>
+    {
+        public override bool HandlesRequest(HttpListenerRequest request)
+        {
+            return ApiService.IsApiUrlMatch("user", request);
+        }
+
+        protected override Task<DtoUser> GetObjectToSerialize()
+        {
+            string id = QueryString["id"];
+
+            User user = string.IsNullOrEmpty(id) ? ApiService.GetDefaultUser(false) : ApiService.GetUserById(id, false);
+
+            DtoUser dto = ApiService.GetDtoUser(user);
+
+            return Task.FromResult(dto);
+        }
+    }
+}

+ 46 - 0
MediaBrowser.Api/HttpHandlers/UserItemRatingHandler.cs

@@ -0,0 +1,46 @@
+using MediaBrowser.Common.Net.Handlers;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.DTO;
+using System.ComponentModel.Composition;
+using System.Net;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Api.HttpHandlers
+{
+    /// <summary>
+    /// Provides a handler to set a user's rating for an item
+    /// </summary>
+    [Export(typeof(BaseHandler))]
+    public class UserItemRatingHandler : BaseSerializationHandler<DtoUserItemData>
+    {
+        public override bool HandlesRequest(HttpListenerRequest request)
+        {
+            return ApiService.IsApiUrlMatch("UserItemRating", request);
+        }
+
+        protected override Task<DtoUserItemData> GetObjectToSerialize()
+        {
+            // Get the item
+            BaseItem item = ApiService.GetItemById(QueryString["id"]);
+
+            // Get the user
+            User user = ApiService.GetUserById(QueryString["userid"], true);
+
+            // Get the user data for this item
+            UserItemData data = item.GetUserData(user, true);
+
+            // If clearing the rating, set it to null
+            if (QueryString["clear"] == "1")
+            {
+                data.Rating = null;
+            }
+
+            else
+            {
+                data.Likes = QueryString["likes"] == "1";
+            }
+
+            return Task.FromResult(ApiService.GetDtoUserItemData(data));
+        }
+    }
+}

+ 25 - 0
MediaBrowser.Api/HttpHandlers/UsersHandler.cs

@@ -0,0 +1,25 @@
+using MediaBrowser.Common.Net.Handlers;
+using MediaBrowser.Controller;
+using MediaBrowser.Model.DTO;
+using System.Collections.Generic;
+using System.ComponentModel.Composition;
+using System.Linq;
+using System.Net;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Api.HttpHandlers
+{
+    [Export(typeof(BaseHandler))]
+    class UsersHandler : BaseSerializationHandler<IEnumerable<DtoUser>>
+    {
+        public override bool HandlesRequest(HttpListenerRequest request)
+        {
+            return ApiService.IsApiUrlMatch("users", request);
+        }
+        
+        protected override Task<IEnumerable<DtoUser>> GetObjectToSerialize()
+        {
+            return Task.FromResult(Kernel.Instance.Users.Select(u => ApiService.GetDtoUser(u)));
+        }
+    }
+}

+ 424 - 0
MediaBrowser.Api/HttpHandlers/VideoHandler.cs

@@ -0,0 +1,424 @@
+using MediaBrowser.Common.Net.Handlers;
+using MediaBrowser.Controller.Drawing;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.DTO;
+using MediaBrowser.Model.Entities;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.Composition;
+using System.Drawing;
+using System.Linq;
+using System.Net;
+
+namespace MediaBrowser.Api.HttpHandlers
+{
+    /// <summary>
+    /// Supported output formats: mkv,m4v,mp4,asf,wmv,mov,webm,ogv,3gp,avi,ts,flv
+    /// </summary>
+    [Export(typeof(BaseHandler))]
+    class VideoHandler : BaseMediaHandler<Video, VideoOutputFormats>
+    {
+        public override bool HandlesRequest(HttpListenerRequest request)
+        {
+            return ApiService.IsApiUrlMatch("video", request);
+        }
+
+        /// <summary>
+        /// We can output these files directly, but we can't encode them
+        /// </summary>
+        protected override IEnumerable<VideoOutputFormats> UnsupportedOutputEncodingFormats
+        {
+            get
+            {
+                // mp4, 3gp, mov - muxer does not support non-seekable output
+                // avi, mov, mkv, m4v - can't stream these when encoding. the player will try to download them completely before starting playback.
+                // wmv - can't seem to figure out the output format name
+                return new VideoOutputFormats[] { VideoOutputFormats.Mp4, VideoOutputFormats.ThreeGp, VideoOutputFormats.M4V, VideoOutputFormats.Mkv, VideoOutputFormats.Avi, VideoOutputFormats.Mov, VideoOutputFormats.Wmv };
+            }
+        }
+
+        /// <summary>
+        /// Determines whether or not we can just output the original file directly
+        /// </summary>
+        protected override bool RequiresConversion()
+        {
+            if (base.RequiresConversion())
+            {
+                return true;
+            }
+
+            // See if the video requires conversion
+            if (RequiresVideoConversion())
+            {
+                return true;
+            }
+
+            // See if the audio requires conversion
+            AudioStream audioStream = (LibraryItem.AudioStreams ?? new List<AudioStream>()).FirstOrDefault();
+
+            if (audioStream != null)
+            {
+                if (RequiresAudioConversion(audioStream))
+                {
+                    return true;
+                }
+            }
+
+            // Yay
+            return false;
+        }
+
+        /// <summary>
+        /// Translates the output file extension to the format param that follows "-f" on the ffmpeg command line
+        /// </summary>
+        private string GetFfMpegOutputFormat(VideoOutputFormats outputFormat)
+        {
+            if (outputFormat == VideoOutputFormats.Mkv)
+            {
+                return "matroska";
+            }
+            if (outputFormat == VideoOutputFormats.Ts)
+            {
+                return "mpegts";
+            }
+            if (outputFormat == VideoOutputFormats.Ogv)
+            {
+                return "ogg";
+            }
+
+            return outputFormat.ToString().ToLower();
+        }
+
+        /// <summary>
+        /// Creates arguments to pass to ffmpeg
+        /// </summary>
+        protected override string GetCommandLineArguments()
+        {
+            VideoOutputFormats outputFormat = GetConversionOutputFormat();
+
+            return string.Format("-i \"{0}\" -threads 0 {1} {2} -f {3} -",
+                LibraryItem.Path,
+                GetVideoArguments(outputFormat),
+                GetAudioArguments(outputFormat),
+                GetFfMpegOutputFormat(outputFormat)
+                );
+        }
+
+        /// <summary>
+        /// Gets video arguments to pass to ffmpeg
+        /// </summary>
+        private string GetVideoArguments(VideoOutputFormats outputFormat)
+        {
+            // Get the output codec name
+            string codec = GetVideoCodec(outputFormat);
+
+            string args = "-vcodec " + codec;
+
+            // If we're encoding video, add additional params
+            if (!codec.Equals("copy", StringComparison.OrdinalIgnoreCase))
+            {
+                // Add resolution params, if specified
+                if (Width.HasValue || Height.HasValue || MaxHeight.HasValue || MaxWidth.HasValue)
+                {
+                    Size size = DrawingUtils.Resize(LibraryItem.Width, LibraryItem.Height, Width, Height, MaxWidth, MaxHeight);
+
+                    args += string.Format(" -s {0}x{1}", size.Width, size.Height);
+                }
+            }
+
+            return args;
+        }
+
+        /// <summary>
+        /// Gets audio arguments to pass to ffmpeg
+        /// </summary>
+        private string GetAudioArguments(VideoOutputFormats outputFormat)
+        {
+            AudioStream audioStream = (LibraryItem.AudioStreams ?? new List<AudioStream>()).FirstOrDefault();
+
+            // If the video doesn't have an audio stream, return empty
+            if (audioStream == null)
+            {
+                return string.Empty;
+            }
+
+            // Get the output codec name
+            string codec = GetAudioCodec(audioStream, outputFormat);
+
+            string args = "-acodec " + codec;
+
+            // If we're encoding audio, add additional params
+            if (!codec.Equals("copy", StringComparison.OrdinalIgnoreCase))
+            {
+                // Add the number of audio channels
+                int? channels = GetNumAudioChannelsParam(codec, audioStream.Channels);
+
+                if (channels.HasValue)
+                {
+                    args += " -ac " + channels.Value;
+                }
+
+                // Add the audio sample rate
+                int? sampleRate = GetSampleRateParam(audioStream.SampleRate);
+
+                if (sampleRate.HasValue)
+                {
+                    args += " -ar " + sampleRate.Value;
+                }
+
+            }
+
+            return args;
+        }
+
+        /// <summary>
+        /// Gets the name of the output video codec
+        /// </summary>
+        private string GetVideoCodec(VideoOutputFormats outputFormat)
+        {
+            // Some output containers require specific codecs
+
+            if (outputFormat == VideoOutputFormats.Webm)
+            {
+                // Per webm specification, it must be vpx
+                return "libvpx";
+            }
+            if (outputFormat == VideoOutputFormats.Asf)
+            {
+                return "wmv2";
+            }
+            if (outputFormat == VideoOutputFormats.Wmv)
+            {
+                return "wmv2";
+            }
+            if (outputFormat == VideoOutputFormats.Ogv)
+            {
+                return "libtheora";
+            }
+
+            // Skip encoding when possible
+            if (!RequiresVideoConversion())
+            {
+                return "copy";
+            }
+
+            return "libx264";
+        }
+
+        /// <summary>
+        /// Gets the name of the output audio codec
+        /// </summary>
+        private string GetAudioCodec(AudioStream audioStream, VideoOutputFormats outputFormat)
+        {
+            // Some output containers require specific codecs
+
+            if (outputFormat == VideoOutputFormats.Webm)
+            {
+                // Per webm specification, it must be vorbis
+                return "libvorbis";
+            }
+            if (outputFormat == VideoOutputFormats.Asf)
+            {
+                return "wmav2";
+            }
+            if (outputFormat == VideoOutputFormats.Wmv)
+            {
+                return "wmav2";
+            }
+            if (outputFormat == VideoOutputFormats.Ogv)
+            {
+                return "libvorbis";
+            }
+
+            // Skip encoding when possible
+            if (!RequiresAudioConversion(audioStream))
+            {
+                return "copy";
+            }
+
+            return "libvo_aacenc";
+        }
+
+        /// <summary>
+        /// Gets the number of audio channels to specify on the command line
+        /// </summary>
+        private int? GetNumAudioChannelsParam(string audioCodec, int libraryItemChannels)
+        {
+            if (libraryItemChannels > 2)
+            {
+                if (audioCodec.Equals("libvo_aacenc"))
+                {
+                    // libvo_aacenc currently only supports two channel output
+                    return 2;
+                }
+                if (audioCodec.Equals("wmav2"))
+                {
+                    // wmav2 currently only supports two channel output
+                    return 2;
+                }
+            }
+
+            return GetNumAudioChannelsParam(libraryItemChannels);
+        }
+
+        /// <summary>
+        /// Determines if the video stream requires encoding
+        /// </summary>
+        private bool RequiresVideoConversion()
+        {
+            // Check dimensions
+
+            // If a specific width is required, validate that
+            if (Width.HasValue)
+            {
+                if (Width.Value != LibraryItem.Width)
+                {
+                    return true;
+                }
+            }
+
+            // If a specific height is required, validate that
+            if (Height.HasValue)
+            {
+                if (Height.Value != LibraryItem.Height)
+                {
+                    return true;
+                }
+            }
+
+            // If a max width is required, validate that
+            if (MaxWidth.HasValue)
+            {
+                if (MaxWidth.Value < LibraryItem.Width)
+                {
+                    return true;
+                }
+            }
+
+            // If a max height is required, validate that
+            if (MaxHeight.HasValue)
+            {
+                if (MaxHeight.Value < LibraryItem.Height)
+                {
+                    return true;
+                }
+            }
+
+            // If the codec is already h264, don't encode
+            if (LibraryItem.Codec.IndexOf("264", StringComparison.OrdinalIgnoreCase) != -1 || LibraryItem.Codec.IndexOf("avc", StringComparison.OrdinalIgnoreCase) != -1)
+            {
+                return false;
+            }
+
+            return false;
+        }
+
+        /// <summary>
+        /// Determines if the audio stream requires encoding
+        /// </summary>
+        private bool RequiresAudioConversion(AudioStream audio)
+        {
+
+            // If the input stream has more audio channels than the client can handle, we need to encode
+            if (AudioChannels.HasValue)
+            {
+                if (audio.Channels > AudioChannels.Value)
+                {
+                    return true;
+                }
+            }
+
+            // Aac, ac-3 and mp3 are all pretty much universally supported. No need to encode them
+
+            if (audio.Codec.IndexOf("aac", StringComparison.OrdinalIgnoreCase) != -1)
+            {
+                return false;
+            }
+
+            if (audio.Codec.IndexOf("ac-3", StringComparison.OrdinalIgnoreCase) != -1 || audio.Codec.IndexOf("ac3", StringComparison.OrdinalIgnoreCase) != -1)
+            {
+                return false;
+            }
+
+            if (audio.Codec.IndexOf("mpeg", StringComparison.OrdinalIgnoreCase) != -1 || audio.Codec.IndexOf("mp3", StringComparison.OrdinalIgnoreCase) != -1)
+            {
+                return false;
+            }
+
+            return true;
+        }
+
+        /// <summary>
+        /// Gets the fixed output video height, in pixels
+        /// </summary>
+        private int? Height
+        {
+            get
+            {
+                string val = QueryString["height"];
+
+                if (string.IsNullOrEmpty(val))
+                {
+                    return null;
+                }
+
+                return int.Parse(val);
+            }
+        }
+
+        /// <summary>
+        /// Gets the fixed output video width, in pixels
+        /// </summary>
+        private int? Width
+        {
+            get
+            {
+                string val = QueryString["width"];
+
+                if (string.IsNullOrEmpty(val))
+                {
+                    return null;
+                }
+
+                return int.Parse(val);
+            }
+        }
+
+        /// <summary>
+        /// Gets the maximum output video height, in pixels
+        /// </summary>
+        private int? MaxHeight
+        {
+            get
+            {
+                string val = QueryString["maxheight"];
+
+                if (string.IsNullOrEmpty(val))
+                {
+                    return null;
+                }
+
+                return int.Parse(val);
+            }
+        }
+
+        /// <summary>
+        /// Gets the maximum output video width, in pixels
+        /// </summary>
+        private int? MaxWidth
+        {
+            get
+            {
+                string val = QueryString["maxwidth"];
+
+                if (string.IsNullOrEmpty(val))
+                {
+                    return null;
+                }
+
+                return int.Parse(val);
+            }
+        }
+
+    }
+}

+ 43 - 0
MediaBrowser.Api/HttpHandlers/WeatherHandler.cs

@@ -0,0 +1,43 @@
+using MediaBrowser.Common.Net.Handlers;
+using MediaBrowser.Controller;
+using MediaBrowser.Model.Weather;
+using System;
+using System.ComponentModel.Composition;
+using System.Linq;
+using System.Net;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Api.HttpHandlers
+{
+    [Export(typeof(BaseHandler))]
+    class WeatherHandler : BaseSerializationHandler<WeatherInfo>
+    {
+        public override bool HandlesRequest(HttpListenerRequest request)
+        {
+            return ApiService.IsApiUrlMatch("weather", request);
+        }
+
+        protected override Task<WeatherInfo> GetObjectToSerialize()
+        {
+            // If a specific zip code was requested on the query string, use that. Otherwise use the value from configuration
+
+            string zipCode = QueryString["zipcode"];
+
+            if (string.IsNullOrWhiteSpace(zipCode))
+            {
+                zipCode = Kernel.Instance.Configuration.WeatherZipCode;
+            }
+
+            return Kernel.Instance.WeatherProviders.First().GetWeatherInfoAsync(zipCode);
+        }
+
+        protected override async Task<ResponseInfo> GetResponseInfo()
+        {
+            var info = await base.GetResponseInfo().ConfigureAwait(false);
+
+            info.CacheDuration = TimeSpan.FromMinutes(15);
+
+            return info;
+        }
+    }
+}

+ 55 - 0
MediaBrowser.Api/HttpHandlers/YearHandler.cs

@@ -0,0 +1,55 @@
+using MediaBrowser.Common.Net.Handlers;
+using MediaBrowser.Controller;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.DTO;
+using System.Collections.Generic;
+using System.ComponentModel.Composition;
+using System.Net;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Api.HttpHandlers
+{
+    /// <summary>
+    /// Gets a single year
+    /// </summary>
+    [Export(typeof(BaseHandler))]
+    public class YearHandler : BaseSerializationHandler<IbnItem>
+    {
+        public override bool HandlesRequest(HttpListenerRequest request)
+        {
+            return ApiService.IsApiUrlMatch("year", request);
+        }
+
+        protected override Task<IbnItem> GetObjectToSerialize()
+        {
+            var parent = ApiService.GetItemById(QueryString["id"]) as Folder;
+            var user = ApiService.GetUserById(QueryString["userid"], true);
+
+            string year = QueryString["year"];
+
+            return GetYear(parent, user, int.Parse(year));
+        }
+
+        /// <summary>
+        /// Gets a Year
+        /// </summary>
+        private async Task<IbnItem> GetYear(Folder parent, User user, int year)
+        {
+            int count = 0;
+
+            // Get all the allowed recursive children
+            IEnumerable<BaseItem> allItems = parent.GetRecursiveChildren(user);
+
+            foreach (var item in allItems)
+            {
+                if (item.ProductionYear.HasValue && item.ProductionYear.Value == year)
+                {
+                    count++;
+                }
+            }
+
+            // Get the original entity so that we can also supply the PrimaryImagePath
+            return ApiService.GetIbnItem(await Kernel.Instance.ItemController.GetYear(year).ConfigureAwait(false), count);
+        }
+    }
+}

+ 75 - 0
MediaBrowser.Api/HttpHandlers/YearsHandler.cs

@@ -0,0 +1,75 @@
+using MediaBrowser.Common.Net.Handlers;
+using MediaBrowser.Controller;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.DTO;
+using System.Collections.Generic;
+using System.ComponentModel.Composition;
+using System.Linq;
+using System.Net;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Api.HttpHandlers
+{
+    [Export(typeof(BaseHandler))]
+    public class YearsHandler : BaseSerializationHandler<IbnItem[]>
+    {
+        public override bool HandlesRequest(HttpListenerRequest request)
+        {
+            return ApiService.IsApiUrlMatch("years", request);
+        }
+
+        protected override Task<IbnItem[]> GetObjectToSerialize()
+        {
+            var parent = ApiService.GetItemById(QueryString["id"]) as Folder;
+            User user = ApiService.GetUserById(QueryString["userid"], true);
+
+            return GetAllYears(parent, user);
+        }
+
+        /// <summary>
+        /// Gets all years from all recursive children of a folder
+        /// The CategoryInfo class is used to keep track of the number of times each year appears
+        /// </summary>
+        private async Task<IbnItem[]> GetAllYears(Folder parent, User user)
+        {
+            var data = new Dictionary<int, int>();
+
+            // Get all the allowed recursive children
+            IEnumerable<BaseItem> allItems = parent.GetRecursiveChildren(user);
+
+            foreach (var item in allItems)
+            {
+                // Add the year from the item to the data dictionary
+                // If the year already exists, increment the count
+                if (item.ProductionYear == null)
+                {
+                    continue;
+                }
+
+                if (!data.ContainsKey(item.ProductionYear.Value))
+                {
+                    data.Add(item.ProductionYear.Value, 1);
+                }
+                else
+                {
+                    data[item.ProductionYear.Value]++;
+                }
+            }
+
+            // Get the Year objects
+            Year[] entities = await Task.WhenAll(data.Keys.Select(key => Kernel.Instance.ItemController.GetYear(key))).ConfigureAwait(false);
+
+            // Convert to an array of IBNItem
+            var items = new IbnItem[entities.Length];
+
+            for (int i = 0; i < entities.Length; i++)
+            {
+                Year e = entities[i];
+
+                items[i] = ApiService.GetIbnItem(e, data[int.Parse(e.Name)]);
+            }
+
+            return items;
+        }
+    }
+}

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

@@ -0,0 +1,117 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+  <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
+  <PropertyGroup>
+    <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+    <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+    <ProjectGuid>{4FD51AC5-2C16-4308-A993-C3A84F3B4582}</ProjectGuid>
+    <OutputType>Library</OutputType>
+    <AppDesignerFolder>Properties</AppDesignerFolder>
+    <RootNamespace>MediaBrowser.Api</RootNamespace>
+    <AssemblyName>MediaBrowser.Api</AssemblyName>
+    <TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
+    <FileAlignment>512</FileAlignment>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
+    <DebugSymbols>true</DebugSymbols>
+    <DebugType>full</DebugType>
+    <Optimize>false</Optimize>
+    <OutputPath>bin\Debug\</OutputPath>
+    <DefineConstants>DEBUG;TRACE</DefineConstants>
+    <ErrorReport>prompt</ErrorReport>
+    <WarningLevel>4</WarningLevel>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
+    <DebugType>pdbonly</DebugType>
+    <Optimize>true</Optimize>
+    <OutputPath>bin\Release\</OutputPath>
+    <DefineConstants>TRACE</DefineConstants>
+    <ErrorReport>prompt</ErrorReport>
+    <WarningLevel>4</WarningLevel>
+  </PropertyGroup>
+  <PropertyGroup>
+    <RunPostBuildEvent>Always</RunPostBuildEvent>
+  </PropertyGroup>
+  <ItemGroup>
+    <Reference Include="System" />
+    <Reference Include="System.ComponentModel.Composition" />
+    <Reference Include="System.Core" />
+    <Reference Include="System.Drawing" />
+    <Reference Include="System.Reactive.Core, Version=2.0.20823.0, Culture=neutral, PublicKeyToken=f300afd708cefcd3, processorArchitecture=MSIL">
+      <SpecificVersion>False</SpecificVersion>
+      <HintPath>..\packages\Rx-Core.2.0.20823\lib\Net45\System.Reactive.Core.dll</HintPath>
+    </Reference>
+    <Reference Include="System.Reactive.Interfaces, Version=2.0.20823.0, Culture=neutral, PublicKeyToken=f300afd708cefcd3, processorArchitecture=MSIL">
+      <SpecificVersion>False</SpecificVersion>
+      <HintPath>..\packages\Rx-Interfaces.2.0.20823\lib\Net45\System.Reactive.Interfaces.dll</HintPath>
+    </Reference>
+    <Reference Include="System.Reactive.Linq, Version=2.0.20823.0, Culture=neutral, PublicKeyToken=f300afd708cefcd3, processorArchitecture=MSIL">
+      <SpecificVersion>False</SpecificVersion>
+      <HintPath>..\packages\Rx-Linq.2.0.20823\lib\Net45\System.Reactive.Linq.dll</HintPath>
+    </Reference>
+    <Reference Include="System.Runtime.Serialization" />
+    <Reference Include="System.Xml.Linq" />
+    <Reference Include="System.Data.DataSetExtensions" />
+    <Reference Include="Microsoft.CSharp" />
+    <Reference Include="System.Data" />
+    <Reference Include="System.Xml" />
+  </ItemGroup>
+  <ItemGroup>
+    <Compile Include="ApiService.cs" />
+    <Compile Include="HttpHandlers\AudioHandler.cs" />
+    <Compile Include="HttpHandlers\BaseMediaHandler.cs" />
+    <Compile Include="HttpHandlers\FavoriteStatusHandler.cs" />
+    <Compile Include="HttpHandlers\MovieSpecialFeaturesHandler.cs" />
+    <Compile Include="HttpHandlers\PlayedStatusHandler.cs" />
+    <Compile Include="HttpHandlers\UserHandler.cs" />
+    <Compile Include="HttpHandlers\GenreHandler.cs" />
+    <Compile Include="HttpHandlers\GenresHandler.cs" />
+    <Compile Include="HttpHandlers\ImageHandler.cs" />
+    <Compile Include="HttpHandlers\ItemHandler.cs" />
+    <Compile Include="HttpHandlers\ItemListHandler.cs" />
+    <Compile Include="HttpHandlers\PersonHandler.cs" />
+    <Compile Include="HttpHandlers\PluginAssemblyHandler.cs" />
+    <Compile Include="HttpHandlers\PluginConfigurationHandler.cs" />
+    <Compile Include="HttpHandlers\PluginsHandler.cs" />
+    <Compile Include="HttpHandlers\ServerConfigurationHandler.cs" />
+    <Compile Include="HttpHandlers\StudioHandler.cs" />
+    <Compile Include="HttpHandlers\StudiosHandler.cs" />
+    <Compile Include="HttpHandlers\UserAuthenticationHandler.cs" />
+    <Compile Include="HttpHandlers\UserItemRatingHandler.cs" />
+    <Compile Include="HttpHandlers\UsersHandler.cs" />
+    <Compile Include="HttpHandlers\VideoHandler.cs" />
+    <Compile Include="HttpHandlers\WeatherHandler.cs" />
+    <Compile Include="HttpHandlers\YearHandler.cs" />
+    <Compile Include="HttpHandlers\YearsHandler.cs" />
+    <Compile Include="Plugin.cs" />
+    <Compile Include="Properties\AssemblyInfo.cs" />
+  </ItemGroup>
+  <ItemGroup>
+    <ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj">
+      <Project>{9142eefa-7570-41e1-bfcc-468bb571af2f}</Project>
+      <Name>MediaBrowser.Common</Name>
+    </ProjectReference>
+    <ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj">
+      <Project>{17e1f4e6-8abd-4fe5-9ecf-43d4b6087ba2}</Project>
+      <Name>MediaBrowser.Controller</Name>
+    </ProjectReference>
+    <ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj">
+      <Project>{7eeeb4bb-f3e8-48fc-b4c5-70f0fff8329b}</Project>
+      <Name>MediaBrowser.Model</Name>
+    </ProjectReference>
+  </ItemGroup>
+  <ItemGroup>
+    <None Include="packages.config" />
+  </ItemGroup>
+  <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
+  <PropertyGroup>
+    <PostBuildEvent>xcopy "$(TargetPath)" "$(SolutionDir)\ProgramData-Server\Plugins\" /y</PostBuildEvent>
+  </PropertyGroup>
+  <!-- To modify your build process, add your task inside one of the targets below and uncomment it. 
+       Other similar extension points exist, see Microsoft.Common.targets.
+  <Target Name="BeforeBuild">
+  </Target>
+  <Target Name="AfterBuild">
+  </Target>
+  -->
+</Project>

+ 14 - 0
MediaBrowser.Api/Plugin.cs

@@ -0,0 +1,14 @@
+using MediaBrowser.Common.Plugins;
+using System.ComponentModel.Composition;
+
+namespace MediaBrowser.Api
+{
+    [Export(typeof(BasePlugin))]
+    public class Plugin : BasePlugin
+    {
+        public override string Name
+        {
+            get { return "Media Browser API"; }
+        }
+    }
+}

+ 35 - 0
MediaBrowser.Api/Properties/AssemblyInfo.cs

@@ -0,0 +1,35 @@
+using System.Reflection;
+using System.Runtime.InteropServices;
+
+// General Information about an assembly is controlled through the following 
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: AssemblyTitle("MediaBrowser.Api")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("MediaBrowser.Api")]
+[assembly: AssemblyCopyright("Copyright ©  2012")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// Setting ComVisible to false makes the types in this assembly not visible 
+// to COM components.  If you need to access a type in this assembly from 
+// COM, set the ComVisible attribute to true on that type.
+[assembly: ComVisible(false)]
+
+// The following GUID is for the ID of the typelib if this project is exposed to COM
+[assembly: Guid("13464b02-f033-48b8-9e1c-d071f8860935")]
+
+// Version information for an assembly consists of the following four values:
+//
+//      Major Version
+//      Minor Version 
+//      Build Number
+//      Revision
+//
+// You can specify all the values or you can default the Build and Revision Numbers 
+// by using the '*' as shown below:
+// [assembly: AssemblyVersion("1.0.*")]
+[assembly: AssemblyVersion("1.0.*")]
+[assembly: AssemblyFileVersion("1.0.0.0")]

+ 6 - 0
MediaBrowser.Api/packages.config

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<packages>
+  <package id="Rx-Core" version="2.0.20823" targetFramework="net45" />
+  <package id="Rx-Interfaces" version="2.0.20823" targetFramework="net45" />
+  <package id="Rx-Linq" version="2.0.20823" targetFramework="net45" />
+</packages>

+ 12 - 0
MediaBrowser.ApiInteraction.Metro/ApiClient.cs

@@ -0,0 +1,12 @@
+using System.Net.Http;
+
+namespace MediaBrowser.ApiInteraction
+{
+    public class ApiClient : BaseHttpApiClient
+    {
+        public ApiClient(HttpClientHandler handler)
+            : base(handler)
+        {
+        }
+    }
+}

+ 78 - 0
MediaBrowser.ApiInteraction.Metro/DataSerializer.cs

@@ -0,0 +1,78 @@
+using Newtonsoft.Json;
+using System;
+using System.IO;
+
+namespace MediaBrowser.ApiInteraction
+{
+    public static class DataSerializer
+    {
+        /// <summary>
+        /// This is an auto-generated Protobuf Serialization assembly for best performance.
+        /// It is created during the Model project's post-build event.
+        /// This means that this class can currently only handle types within the Model project.
+        /// If we need to, we can always add a param indicating whether or not the model serializer should be used.
+        /// </summary>
+        private static readonly ProtobufModelSerializer ProtobufModelSerializer = new ProtobufModelSerializer();
+        
+        public static T DeserializeFromStream<T>(Stream stream, SerializationFormats format)
+            where T : class
+        {
+            if (format == ApiInteraction.SerializationFormats.Protobuf)
+            {
+                return ProtobufModelSerializer.Deserialize(stream, null, typeof(T)) as T;
+            }
+            else if (format == ApiInteraction.SerializationFormats.Jsv)
+            {
+                throw new NotImplementedException();
+            }
+            else if (format == ApiInteraction.SerializationFormats.Json)
+            {
+                using (StreamReader streamReader = new StreamReader(stream))
+                {
+                    using (JsonReader jsonReader = new JsonTextReader(streamReader))
+                    {
+                        return JsonSerializer.Create(new JsonSerializerSettings()).Deserialize<T>(jsonReader);
+                    }
+                }
+            }
+
+            throw new NotImplementedException();
+        }
+
+        public static object DeserializeFromStream(Stream stream, SerializationFormats format, Type type)
+        {
+            if (format == ApiInteraction.SerializationFormats.Protobuf)
+            {
+                return ProtobufModelSerializer.Deserialize(stream, null, type);
+            }
+            else if (format == ApiInteraction.SerializationFormats.Jsv)
+            {
+                throw new NotImplementedException();
+            }
+            else if (format == ApiInteraction.SerializationFormats.Json)
+            {
+                using (StreamReader streamReader = new StreamReader(stream))
+                {
+                    using (JsonReader jsonReader = new JsonTextReader(streamReader))
+                    {
+                        return JsonSerializer.Create(new JsonSerializerSettings()).Deserialize(jsonReader, type);
+                    }
+                }
+            }
+
+            throw new NotImplementedException();
+        }
+
+        public static void Configure()
+        {
+        }
+
+        public static bool CanDeSerializeJsv
+        {
+            get
+            {
+                return false;
+            }
+        }
+    }
+}

+ 74 - 0
MediaBrowser.ApiInteraction.Metro/MediaBrowser.ApiInteraction.Metro.csproj

@@ -0,0 +1,74 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+  <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
+  <PropertyGroup>
+    <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+    <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+    <ProjectGuid>{94CEA07A-307C-4663-AA43-7BD852808574}</ProjectGuid>
+    <OutputType>Library</OutputType>
+    <AppDesignerFolder>Properties</AppDesignerFolder>
+    <RootNamespace>MediaBrowser.ApiInteraction.Metro</RootNamespace>
+    <AssemblyName>MediaBrowser.ApiInteraction.Metro</AssemblyName>
+    <TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
+    <TargetFrameworkProfile>Profile7</TargetFrameworkProfile>
+    <FileAlignment>512</FileAlignment>
+    <ProjectTypeGuids>{786C830F-07A1-408B-BD7F-6EE04809D6DB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
+    <DebugSymbols>true</DebugSymbols>
+    <DebugType>full</DebugType>
+    <Optimize>false</Optimize>
+    <OutputPath>bin\Debug\</OutputPath>
+    <DefineConstants>DEBUG;TRACE</DefineConstants>
+    <ErrorReport>prompt</ErrorReport>
+    <WarningLevel>4</WarningLevel>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
+    <DebugType>pdbonly</DebugType>
+    <Optimize>true</Optimize>
+    <OutputPath>bin\Release\</OutputPath>
+    <DefineConstants>TRACE</DefineConstants>
+    <ErrorReport>prompt</ErrorReport>
+    <WarningLevel>4</WarningLevel>
+  </PropertyGroup>
+  <ItemGroup>
+    <!-- A reference to the entire .NET Framework is automatically included -->
+    <ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj">
+      <Project>{7eeeb4bb-f3e8-48fc-b4c5-70f0fff8329b}</Project>
+      <Name>MediaBrowser.Model</Name>
+    </ProjectReference>
+  </ItemGroup>
+  <ItemGroup>
+    <Compile Include="..\MediaBrowser.ApiInteraction\BaseApiClient.cs">
+      <Link>BaseApiClient.cs</Link>
+    </Compile>
+    <Compile Include="..\MediaBrowser.ApiInteraction\BaseHttpApiClient.cs">
+      <Link>BaseHttpApiClient.cs</Link>
+    </Compile>
+    <Compile Include="..\MediaBrowser.ApiInteraction\SerializationFormats.cs">
+      <Link>SerializationFormats.cs</Link>
+    </Compile>
+    <Compile Include="ApiClient.cs" />
+    <Compile Include="DataSerializer.cs" />
+    <Compile Include="Properties\AssemblyInfo.cs" />
+  </ItemGroup>
+  <ItemGroup>
+    <Reference Include="Newtonsoft.Json">
+      <HintPath>..\Json.Net\Portable\Newtonsoft.Json.dll</HintPath>
+    </Reference>
+    <Reference Include="protobuf-net">
+      <HintPath>..\protobuf-net\Full\portable\protobuf-net.dll</HintPath>
+    </Reference>
+    <Reference Include="ProtobufModelSerializer">
+      <HintPath>..\MediaBrowser.Model\bin\ProtobufModelSerializer.dll</HintPath>
+    </Reference>
+  </ItemGroup>
+  <Import Project="$(MSBuildExtensionsPath32)\Microsoft\Portable\$(TargetFrameworkVersion)\Microsoft.Portable.CSharp.targets" />
+  <!-- To modify your build process, add your task inside one of the targets below and uncomment it. 
+       Other similar extension points exist, see Microsoft.Common.targets.
+  <Target Name="BeforeBuild">
+  </Target>
+  <Target Name="AfterBuild">
+  </Target>
+  -->
+</Project>

+ 30 - 0
MediaBrowser.ApiInteraction.Metro/Properties/AssemblyInfo.cs

@@ -0,0 +1,30 @@
+using System.Resources;
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+// General Information about an assembly is controlled through the following 
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: AssemblyTitle("MediaBrowser.ApiInteraction.Metro")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("MediaBrowser.ApiInteraction.Metro")]
+[assembly: AssemblyCopyright("Copyright ©  2012")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+[assembly: NeutralResourcesLanguage("en")]
+
+// Version information for an assembly consists of the following four values:
+//
+//      Major Version
+//      Minor Version 
+//      Build Number
+//      Revision
+//
+// You can specify all the values or you can default the Build and Revision Numbers 
+// by using the '*' as shown below:
+// [assembly: AssemblyVersion("1.0.*")]
+[assembly: AssemblyVersion("1.0.0.0")]
+[assembly: AssemblyFileVersion("1.0.0.0")]

+ 37 - 0
MediaBrowser.ApiInteraction.sln

@@ -0,0 +1,37 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio 2012
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MediaBrowser.Model", "MediaBrowser.Model\MediaBrowser.Model.csproj", "{7EEEB4BB-F3E8-48FC-B4C5-70F0FFF8329B}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MediaBrowser.ApiInteraction", "MediaBrowser.ApiInteraction\MediaBrowser.ApiInteraction.csproj", "{921C0F64-FDA7-4E9F-9E73-0CB0EEDB2422}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".nuget", ".nuget", "{F0E0E64C-2A6F-4E35-9533-D53AC07C2CD1}"
+	ProjectSection(SolutionItems) = preProject
+		.nuget\packages.config = .nuget\packages.config
+	EndProjectSection
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MediaBrowser.ApiInteraction.Metro", "MediaBrowser.ApiInteraction.Metro\MediaBrowser.ApiInteraction.Metro.csproj", "{94CEA07A-307C-4663-AA43-7BD852808574}"
+EndProject
+Global
+	GlobalSection(SolutionConfigurationPlatforms) = preSolution
+		Debug|Any CPU = Debug|Any CPU
+		Release|Any CPU = Release|Any CPU
+	EndGlobalSection
+	GlobalSection(ProjectConfigurationPlatforms) = postSolution
+		{7EEEB4BB-F3E8-48FC-B4C5-70F0FFF8329B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{7EEEB4BB-F3E8-48FC-B4C5-70F0FFF8329B}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{7EEEB4BB-F3E8-48FC-B4C5-70F0FFF8329B}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{7EEEB4BB-F3E8-48FC-B4C5-70F0FFF8329B}.Release|Any CPU.Build.0 = Release|Any CPU
+		{921C0F64-FDA7-4E9F-9E73-0CB0EEDB2422}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{921C0F64-FDA7-4E9F-9E73-0CB0EEDB2422}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{921C0F64-FDA7-4E9F-9E73-0CB0EEDB2422}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{921C0F64-FDA7-4E9F-9E73-0CB0EEDB2422}.Release|Any CPU.Build.0 = Release|Any CPU
+		{94CEA07A-307C-4663-AA43-7BD852808574}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{94CEA07A-307C-4663-AA43-7BD852808574}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{94CEA07A-307C-4663-AA43-7BD852808574}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{94CEA07A-307C-4663-AA43-7BD852808574}.Release|Any CPU.Build.0 = Release|Any CPU
+	EndGlobalSection
+	GlobalSection(SolutionProperties) = preSolution
+		HideSolutionNode = FALSE
+	EndGlobalSection
+EndGlobal

+ 18 - 0
MediaBrowser.ApiInteraction/ApiClient.cs

@@ -0,0 +1,18 @@
+using System.Net.Cache;
+using System.Net.Http;
+
+namespace MediaBrowser.ApiInteraction
+{
+    public class ApiClient : BaseHttpApiClient
+    {
+        public ApiClient(HttpClientHandler handler)
+            : base(handler)
+        {
+        }
+
+        public ApiClient()
+            : this(new WebRequestHandler { CachePolicy = new RequestCachePolicy(RequestCacheLevel.Revalidate) })
+        {
+        }
+    }
+}

+ 446 - 0
MediaBrowser.ApiInteraction/BaseApiClient.cs

@@ -0,0 +1,446 @@
+using MediaBrowser.Model.DTO;
+using MediaBrowser.Model.Entities;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+
+namespace MediaBrowser.ApiInteraction
+{
+    /// <summary>
+    /// Provides api methods that are usable on all platforms
+    /// </summary>
+    public abstract class BaseApiClient : IDisposable
+    {
+        protected BaseApiClient()
+        {
+            DataSerializer.Configure();
+        }
+
+        /// <summary>
+        /// Gets or sets the server host name (myserver or 192.168.x.x)
+        /// </summary>
+        public string ServerHostName { get; set; }
+
+        /// <summary>
+        /// Gets or sets the port number used by the API
+        /// </summary>
+        public int ServerApiPort { get; set; }
+
+        /// <summary>
+        /// Gets the current api url based on hostname and port.
+        /// </summary>
+        protected string ApiUrl
+        {
+            get
+            {
+                return string.Format("http://{0}:{1}/mediabrowser/api", ServerHostName, ServerApiPort);
+            }
+        }
+
+        /// <summary>
+        /// Gets the default data format to request from the server
+        /// </summary>
+        protected SerializationFormats SerializationFormat
+        {
+            get
+            {
+                return SerializationFormats.Protobuf;
+            }
+        }
+
+        /// <summary>
+        /// Gets an image url that can be used to download an image from the api
+        /// </summary>
+        /// <param name="itemId">The Id of the item</param>
+        /// <param name="imageType">The type of image requested</param>
+        /// <param name="imageIndex">The image index, if there are multiple. Currently only applies to backdrops. Supply null or 0 for first backdrop.</param>
+        /// <param name="width">Use if a fixed width is required. Aspect ratio will be preserved.</param>
+        /// <param name="height">Use if a fixed height is required. Aspect ratio will be preserved.</param>
+        /// <param name="maxWidth">Use if a max width is required. Aspect ratio will be preserved.</param>
+        /// <param name="maxHeight">Use if a max height is required. Aspect ratio will be preserved.</param>
+        /// <param name="quality">Quality level, from 0-100. Currently only applies to JPG. The default value should suffice.</param>
+        public string GetImageUrl(Guid itemId, ImageType imageType, int? imageIndex = null, int? width = null, int? height = null, int? maxWidth = null, int? maxHeight = null, int? quality = null)
+        {
+            string url = ApiUrl + "/image";
+
+            url += "?id=" + itemId.ToString();
+            url += "&type=" + imageType.ToString();
+
+            if (imageIndex.HasValue)
+            {
+                url += "&index=" + imageIndex;
+            }
+            if (width.HasValue)
+            {
+                url += "&width=" + width;
+            }
+            if (height.HasValue)
+            {
+                url += "&height=" + height;
+            }
+            if (maxWidth.HasValue)
+            {
+                url += "&maxWidth=" + maxWidth;
+            }
+            if (maxHeight.HasValue)
+            {
+                url += "&maxHeight=" + maxHeight;
+            }
+            if (quality.HasValue)
+            {
+                url += "&quality=" + quality;
+            }
+
+            return url;
+        }
+
+        /// <summary>
+        /// Gets an image url that can be used to download an image from the api
+        /// </summary>
+        /// <param name="userId">The Id of the user</param>
+        /// <param name="width">Use if a fixed width is required. Aspect ratio will be preserved.</param>
+        /// <param name="height">Use if a fixed height is required. Aspect ratio will be preserved.</param>
+        /// <param name="maxWidth">Use if a max width is required. Aspect ratio will be preserved.</param>
+        /// <param name="maxHeight">Use if a max height is required. Aspect ratio will be preserved.</param>
+        /// <param name="quality">Quality level, from 0-100. Currently only applies to JPG. The default value should suffice.</param>
+        public string GetUserImageUrl(Guid userId, int? width = null, int? height = null, int? maxWidth = null, int? maxHeight = null, int? quality = null)
+        {
+            string url = ApiUrl + "/image";
+
+            url += "?userId=" + userId.ToString();
+
+            if (width.HasValue)
+            {
+                url += "&width=" + width;
+            }
+            if (height.HasValue)
+            {
+                url += "&height=" + height;
+            }
+            if (maxWidth.HasValue)
+            {
+                url += "&maxWidth=" + maxWidth;
+            }
+            if (maxHeight.HasValue)
+            {
+                url += "&maxHeight=" + maxHeight;
+            }
+            if (quality.HasValue)
+            {
+                url += "&quality=" + quality;
+            }
+
+            return url;
+        }
+
+        /// <summary>
+        /// Gets an image url that can be used to download an image from the api
+        /// </summary>
+        /// <param name="name">The name of the person</param>
+        /// <param name="width">Use if a fixed width is required. Aspect ratio will be preserved.</param>
+        /// <param name="height">Use if a fixed height is required. Aspect ratio will be preserved.</param>
+        /// <param name="maxWidth">Use if a max width is required. Aspect ratio will be preserved.</param>
+        /// <param name="maxHeight">Use if a max height is required. Aspect ratio will be preserved.</param>
+        /// <param name="quality">Quality level, from 0-100. Currently only applies to JPG. The default value should suffice.</param>
+        public string GetPersonImageUrl(string name, int? width = null, int? height = null, int? maxWidth = null, int? maxHeight = null, int? quality = null)
+        {
+            string url = ApiUrl + "/image";
+
+            url += "?personname=" + name;
+
+            if (width.HasValue)
+            {
+                url += "&width=" + width;
+            }
+            if (height.HasValue)
+            {
+                url += "&height=" + height;
+            }
+            if (maxWidth.HasValue)
+            {
+                url += "&maxWidth=" + maxWidth;
+            }
+            if (maxHeight.HasValue)
+            {
+                url += "&maxHeight=" + maxHeight;
+            }
+            if (quality.HasValue)
+            {
+                url += "&quality=" + quality;
+            }
+
+            return url;
+        }
+
+        /// <summary>
+        /// Gets an image url that can be used to download an image from the api
+        /// </summary>
+        /// <param name="year">The year</param>
+        /// <param name="width">Use if a fixed width is required. Aspect ratio will be preserved.</param>
+        /// <param name="height">Use if a fixed height is required. Aspect ratio will be preserved.</param>
+        /// <param name="maxWidth">Use if a max width is required. Aspect ratio will be preserved.</param>
+        /// <param name="maxHeight">Use if a max height is required. Aspect ratio will be preserved.</param>
+        /// <param name="quality">Quality level, from 0-100. Currently only applies to JPG. The default value should suffice.</param>
+        public string GetYearImageUrl(int year, int? width = null, int? height = null, int? maxWidth = null, int? maxHeight = null, int? quality = null)
+        {
+            string url = ApiUrl + "/image";
+
+            url += "?year=" + year;
+
+            if (width.HasValue)
+            {
+                url += "&width=" + width;
+            }
+            if (height.HasValue)
+            {
+                url += "&height=" + height;
+            }
+            if (maxWidth.HasValue)
+            {
+                url += "&maxWidth=" + maxWidth;
+            }
+            if (maxHeight.HasValue)
+            {
+                url += "&maxHeight=" + maxHeight;
+            }
+            if (quality.HasValue)
+            {
+                url += "&quality=" + quality;
+            }
+
+            return url;
+        }
+
+        /// <summary>
+        /// Gets an image url that can be used to download an image from the api
+        /// </summary>
+        /// <param name="name">The name of the genre</param>
+        /// <param name="width">Use if a fixed width is required. Aspect ratio will be preserved.</param>
+        /// <param name="height">Use if a fixed height is required. Aspect ratio will be preserved.</param>
+        /// <param name="maxWidth">Use if a max width is required. Aspect ratio will be preserved.</param>
+        /// <param name="maxHeight">Use if a max height is required. Aspect ratio will be preserved.</param>
+        /// <param name="quality">Quality level, from 0-100. Currently only applies to JPG. The default value should suffice.</param>
+        public string GetGenreImageUrl(string name, int? width = null, int? height = null, int? maxWidth = null, int? maxHeight = null, int? quality = null)
+        {
+            string url = ApiUrl + "/image";
+
+            url += "?genre=" + name;
+
+            if (width.HasValue)
+            {
+                url += "&width=" + width;
+            }
+            if (height.HasValue)
+            {
+                url += "&height=" + height;
+            }
+            if (maxWidth.HasValue)
+            {
+                url += "&maxWidth=" + maxWidth;
+            }
+            if (maxHeight.HasValue)
+            {
+                url += "&maxHeight=" + maxHeight;
+            }
+            if (quality.HasValue)
+            {
+                url += "&quality=" + quality;
+            }
+
+            return url;
+        }
+
+        /// <summary>
+        /// Gets an image url that can be used to download an image from the api
+        /// </summary>
+        /// <param name="name">The name of the studio</param>
+        /// <param name="width">Use if a fixed width is required. Aspect ratio will be preserved.</param>
+        /// <param name="height">Use if a fixed height is required. Aspect ratio will be preserved.</param>
+        /// <param name="maxWidth">Use if a max width is required. Aspect ratio will be preserved.</param>
+        /// <param name="maxHeight">Use if a max height is required. Aspect ratio will be preserved.</param>
+        /// <param name="quality">Quality level, from 0-100. Currently only applies to JPG. The default value should suffice.</param>
+        public string GetStudioImageUrl(string name, int? width = null, int? height = null, int? maxWidth = null, int? maxHeight = null, int? quality = null)
+        {
+            string url = ApiUrl + "/image";
+
+            url += "?studio=" + name;
+
+            if (width.HasValue)
+            {
+                url += "&width=" + width;
+            }
+            if (height.HasValue)
+            {
+                url += "&height=" + height;
+            }
+            if (maxWidth.HasValue)
+            {
+                url += "&maxWidth=" + maxWidth;
+            }
+            if (maxHeight.HasValue)
+            {
+                url += "&maxHeight=" + maxHeight;
+            }
+            if (quality.HasValue)
+            {
+                url += "&quality=" + quality;
+            }
+
+            return url;
+        }
+
+        /// <summary>
+        /// This is a helper to get a list of backdrop url's from a given ApiBaseItemWrapper. If the actual item does not have any backdrops it will return backdrops from the first parent that does.
+        /// </summary>
+        /// <param name="item">A given item.</param>
+        /// <param name="width">Use if a fixed width is required. Aspect ratio will be preserved.</param>
+        /// <param name="height">Use if a fixed height is required. Aspect ratio will be preserved.</param>
+        /// <param name="maxWidth">Use if a max width is required. Aspect ratio will be preserved.</param>
+        /// <param name="maxHeight">Use if a max height is required. Aspect ratio will be preserved.</param>
+        /// <param name="quality">Quality level, from 0-100. Currently only applies to JPG. The default value should suffice.</param>
+        public string[] GetBackdropImageUrls(DtoBaseItem item, int? width = null, int? height = null, int? maxWidth = null, int? maxHeight = null, int? quality = null)
+        {
+            Guid? backdropItemId;
+            int backdropCount;
+
+            if (item.BackdropCount == 0)
+            {
+                backdropItemId = item.ParentBackdropItemId;
+                backdropCount = item.ParentBackdropCount ?? 0;
+            }
+            else
+            {
+                backdropItemId = item.Id;
+                backdropCount = item.BackdropCount;
+            }
+
+            if (backdropItemId == null)
+            {
+                return new string[] { };
+            }
+
+            var files = new string[backdropCount];
+
+            for (int i = 0; i < backdropCount; i++)
+            {
+                files[i] = GetImageUrl(backdropItemId.Value, ImageType.Backdrop, i, width, height, maxWidth, maxHeight, quality);
+            }
+
+            return files;
+        }
+
+        /// <summary>
+        /// This is a helper to get the logo image url from a given ApiBaseItemWrapper. If the actual item does not have a logo, it will return the logo from the first parent that does, or null.
+        /// </summary>
+        /// <param name="item">A given item.</param>
+        /// <param name="width">Use if a fixed width is required. Aspect ratio will be preserved.</param>
+        /// <param name="height">Use if a fixed height is required. Aspect ratio will be preserved.</param>
+        /// <param name="maxWidth">Use if a max width is required. Aspect ratio will be preserved.</param>
+        /// <param name="maxHeight">Use if a max height is required. Aspect ratio will be preserved.</param>
+        /// <param name="quality">Quality level, from 0-100. Currently only applies to JPG. The default value should suffice.</param>
+        public string GetLogoImageUrl(DtoBaseItem item, int? width = null, int? height = null, int? maxWidth = null, int? maxHeight = null, int? quality = null)
+        {
+            Guid? logoItemId = item.HasLogo ? item.Id : item.ParentLogoItemId;
+
+            if (logoItemId.HasValue)
+            {
+                return GetImageUrl(logoItemId.Value, ImageType.Logo, null, width, height, maxWidth, maxHeight, quality);
+            }
+
+            return null;
+        }
+
+        /// <summary>
+        /// Gets the url needed to stream an audio file
+        /// </summary>
+        /// <param name="itemId">The id of the item</param>
+        /// <param name="supportedOutputFormats">List all the output formats the decice is capable of playing. The more, the better, as it will decrease the likelyhood of having to encode, which will put a load on the server.</param>
+        /// <param name="maxAudioChannels">The maximum number of channels that the device can play. Omit this if it doesn't matter. Phones and tablets should generally specify 2.</param>
+        /// <param name="maxAudioSampleRate">The maximum sample rate that the device can play. This should generally be omitted. The server will default this to 44100, so only override if a different max is needed.</param>
+        public string GetAudioStreamUrl(Guid itemId, IEnumerable<AudioOutputFormats> supportedOutputFormats, int? maxAudioChannels = null, int? maxAudioSampleRate = null)
+        {
+            string url = ApiUrl + "/audio?id=" + itemId;
+
+            url += "&outputformats=" + string.Join(",", supportedOutputFormats.Select(s => s.ToString()).ToArray());
+
+            if (maxAudioChannels.HasValue)
+            {
+                url += "&audiochannels=" + maxAudioChannels.Value;
+            }
+
+            if (maxAudioSampleRate.HasValue)
+            {
+                url += "&audiosamplerate=" + maxAudioSampleRate.Value;
+            }
+
+            return url;
+        }
+
+        /// <summary>
+        /// Gets the url needed to stream a video file
+        /// </summary>
+        /// <param name="itemId">The id of the item</param>
+        /// <param name="supportedOutputFormats">List all the output formats the decice is capable of playing. The more, the better, as it will decrease the likelyhood of having to encode, which will put a load on the server.</param>
+        /// <param name="maxAudioChannels">The maximum number of channels that the device can play. Omit this if it doesn't matter. Phones and tablets should generally specify 2.</param>
+        /// <param name="maxAudioSampleRate">The maximum sample rate that the device can play. This should generally be omitted. The server will default this to 44100, so only override if a different max is needed.</param>
+        /// <param name="width">Specify this is a fixed video width is required</param>
+        /// <param name="height">Specify this is a fixed video height is required</param>
+        /// <param name="maxWidth">Specify this is a max video width is required</param>
+        /// <param name="maxHeight">Specify this is a max video height is required</param>
+        public string GetVideoStreamUrl(Guid itemId, 
+            IEnumerable<VideoOutputFormats> supportedOutputFormats, 
+            int? maxAudioChannels = null, 
+            int? maxAudioSampleRate = null, 
+            int? width = null, 
+            int? height = null, 
+            int? maxWidth = null, 
+            int? maxHeight = null)
+        {
+            string url = ApiUrl + "/video?id=" + itemId;
+
+            url += "&outputformats=" + string.Join(",", supportedOutputFormats.Select(s => s.ToString()).ToArray());
+
+            if (maxAudioChannels.HasValue)
+            {
+                url += "&audiochannels=" + maxAudioChannels.Value;
+            }
+
+            if (maxAudioSampleRate.HasValue)
+            {
+                url += "&audiosamplerate=" + maxAudioSampleRate.Value;
+            }
+
+            if (width.HasValue)
+            {
+                url += "&width=" + width.Value;
+            }
+
+            if (height.HasValue)
+            {
+                url += "&height=" + height.Value;
+            }
+
+            if (maxWidth.HasValue)
+            {
+                url += "&maxWidth=" + maxWidth.Value;
+            }
+
+            if (maxHeight.HasValue)
+            {
+                url += "&maxHeight=" + maxHeight.Value;
+            }
+            return url;
+        }
+
+        protected T DeserializeFromStream<T>(Stream stream)
+            where T : class
+        {
+            return DataSerializer.DeserializeFromStream<T>(stream, SerializationFormat);
+        }
+
+        public virtual void Dispose()
+        {
+        }
+    }
+}

+ 611 - 0
MediaBrowser.ApiInteraction/BaseHttpApiClient.cs

@@ -0,0 +1,611 @@
+using MediaBrowser.Model.Authentication;
+using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.DTO;
+using MediaBrowser.Model.Weather;
+using System;
+using System.IO;
+using System.Net;
+using System.Text;
+using System.Threading.Tasks;
+#if WINDOWS_PHONE
+using SharpGIS;
+#else
+using System.Net.Http;
+#endif
+
+namespace MediaBrowser.ApiInteraction
+{
+    /// <summary>
+    /// Provides api methods centered around an HttpClient
+    /// </summary>
+    public abstract class BaseHttpApiClient : BaseApiClient
+    {
+#if WINDOWS_PHONE
+        public BaseHttpApiClient()
+        {
+            HttpClient = new GZipWebClient();
+        }
+
+        private WebClient HttpClient { get; set; }
+#else
+        protected BaseHttpApiClient(HttpClientHandler handler)
+            : base()
+        {
+            handler.AutomaticDecompression = DecompressionMethods.Deflate;
+
+            HttpClient = new HttpClient(handler);
+        }
+
+        private HttpClient HttpClient { get; set; }
+#endif
+
+        /// <summary>
+        /// Gets an image stream based on a url
+        /// </summary>
+        public Task<Stream> GetImageStreamAsync(string url)
+        {
+            return GetStreamAsync(url);
+        }
+
+        /// <summary>
+        /// Gets a BaseItem
+        /// </summary>
+        public async Task<DtoBaseItem> GetItemAsync(Guid id, Guid userId)
+        {
+            string url = ApiUrl + "/item?userId=" + userId.ToString();
+
+            if (id != Guid.Empty)
+            {
+                url += "&id=" + id.ToString();
+            }
+
+            using (Stream stream = await GetSerializedStreamAsync(url).ConfigureAwait(false))
+            {
+                return DeserializeFromStream<DtoBaseItem>(stream);
+            }
+        }
+
+        /// <summary>
+        /// Gets all Users
+        /// </summary>
+        public async Task<DtoUser[]> GetAllUsersAsync()
+        {
+            string url = ApiUrl + "/users";
+
+            using (Stream stream = await GetSerializedStreamAsync(url).ConfigureAwait(false))
+            {
+                return DeserializeFromStream<DtoUser[]>(stream);
+            }
+        }
+
+        /// <summary>
+        /// Gets all Genres
+        /// </summary>
+        public async Task<IbnItem[]> GetAllGenresAsync(Guid userId)
+        {
+            string url = ApiUrl + "/genres?userId=" + userId.ToString();
+
+            using (Stream stream = await GetSerializedStreamAsync(url).ConfigureAwait(false))
+            {
+                return DeserializeFromStream<IbnItem[]>(stream);
+            }
+        }
+
+        /// <summary>
+        /// Gets in-progress items
+        /// </summary>
+        /// <param name="userId">The user id.</param>
+        /// <param name="folderId">(Optional) Specify a folder Id to localize the search to a specific folder.</param>
+        public async Task<DtoBaseItem[]> GetInProgressItemsItemsAsync(Guid userId, Guid? folderId = null)
+        {
+            string url = ApiUrl + "/itemlist?listtype=inprogressitems&userId=" + userId.ToString();
+
+            if (folderId.HasValue)
+            {
+                url += "&id=" + folderId.ToString();
+            }
+
+            using (Stream stream = await GetSerializedStreamAsync(url).ConfigureAwait(false))
+            {
+                return DeserializeFromStream<DtoBaseItem[]>(stream);
+            }
+        }
+
+        /// <summary>
+        /// Gets recently added items
+        /// </summary>
+        /// <param name="userId">The user id.</param>
+        /// <param name="folderId">(Optional) Specify a folder Id to localize the search to a specific folder.</param>
+        public async Task<DtoBaseItem[]> GetRecentlyAddedItemsAsync(Guid userId, Guid? folderId = null)
+        {
+            string url = ApiUrl + "/itemlist?listtype=recentlyaddeditems&userId=" + userId.ToString();
+
+            if (folderId.HasValue)
+            {
+                url += "&id=" + folderId.ToString();
+            }
+
+            using (Stream stream = await GetSerializedStreamAsync(url).ConfigureAwait(false))
+            {
+                return DeserializeFromStream<DtoBaseItem[]>(stream);
+            }
+        }
+
+        /// <summary>
+        /// Gets favorite items
+        /// </summary>
+        /// <param name="userId">The user id.</param>
+        /// <param name="folderId">(Optional) Specify a folder Id to localize the search to a specific folder.</param>
+        public async Task<DtoBaseItem[]> GetFavoriteItemsAsync(Guid userId, Guid? folderId = null)
+        {
+            string url = ApiUrl + "/itemlist?listtype=favorites&userId=" + userId.ToString();
+
+            if (folderId.HasValue)
+            {
+                url += "&id=" + folderId.ToString();
+            }
+
+            using (Stream stream = await GetSerializedStreamAsync(url).ConfigureAwait(false))
+            {
+                return DeserializeFromStream<DtoBaseItem[]>(stream);
+            }
+        }
+
+        /// <summary>
+        /// Gets recently added items that are unplayed.
+        /// </summary>
+        /// <param name="userId">The user id.</param>
+        /// <param name="folderId">(Optional) Specify a folder Id to localize the search to a specific folder.</param>
+        public async Task<DtoBaseItem[]> GetRecentlyAddedUnplayedItemsAsync(Guid userId, Guid? folderId = null)
+        {
+            string url = ApiUrl + "/itemlist?listtype=recentlyaddedunplayeditems&userId=" + userId.ToString();
+
+            if (folderId.HasValue)
+            {
+                url += "&id=" + folderId.ToString();
+            }
+
+            using (Stream stream = await GetSerializedStreamAsync(url).ConfigureAwait(false))
+            {
+                return DeserializeFromStream<DtoBaseItem[]>(stream);
+            }
+        }
+
+        /// <summary>
+        /// Gets all Years
+        /// </summary>
+        public async Task<IbnItem[]> GetAllYearsAsync(Guid userId)
+        {
+            string url = ApiUrl + "/years?userId=" + userId.ToString();
+
+            using (Stream stream = await GetSerializedStreamAsync(url).ConfigureAwait(false))
+            {
+                return DeserializeFromStream<IbnItem[]>(stream);
+            }
+        }
+
+        /// <summary>
+        /// Gets all items that contain a given Year
+        /// </summary>
+        /// <param name="folderId">(Optional) Specify a folder Id to localize the search to a specific folder.</param>
+        public async Task<DtoBaseItem[]> GetItemsWithYearAsync(string name, Guid userId, Guid? folderId = null)
+        {
+            string url = ApiUrl + "/itemlist?listtype=itemswithyear&userId=" + userId.ToString() + "&name=" + name;
+
+            if (folderId.HasValue)
+            {
+                url += "&id=" + folderId.ToString();
+            }
+
+            using (Stream stream = await GetSerializedStreamAsync(url).ConfigureAwait(false))
+            {
+                return DeserializeFromStream<DtoBaseItem[]>(stream);
+            }
+        }
+
+        /// <summary>
+        /// Gets all items that contain a given Genre
+        /// </summary>
+        /// <param name="folderId">(Optional) Specify a folder Id to localize the search to a specific folder.</param>
+        public async Task<DtoBaseItem[]> GetItemsWithGenreAsync(string name, Guid userId, Guid? folderId = null)
+        {
+            string url = ApiUrl + "/itemlist?listtype=itemswithgenre&userId=" + userId.ToString() + "&name=" + name;
+
+            if (folderId.HasValue)
+            {
+                url += "&id=" + folderId.ToString();
+            }
+
+            using (Stream stream = await GetSerializedStreamAsync(url).ConfigureAwait(false))
+            {
+                return DeserializeFromStream<DtoBaseItem[]>(stream);
+            }
+        }
+
+        /// <summary>
+        /// Gets all items that contain a given Person
+        /// </summary>
+        /// <param name="folderId">(Optional) Specify a folder Id to localize the search to a specific folder.</param>
+        public async Task<DtoBaseItem[]> GetItemsWithPersonAsync(string name, Guid userId, Guid? folderId = null)
+        {
+            string url = ApiUrl + "/itemlist?listtype=itemswithperson&userId=" + userId.ToString() + "&name=" + name;
+
+            if (folderId.HasValue)
+            {
+                url += "&id=" + folderId.ToString();
+            }
+
+            using (Stream stream = await GetSerializedStreamAsync(url).ConfigureAwait(false))
+            {
+                return DeserializeFromStream<DtoBaseItem[]>(stream);
+            }
+        }
+
+        /// <summary>
+        /// Gets all items that contain a given Person
+        /// </summary>
+        /// <param name="folderId">(Optional) Specify a folder Id to localize the search to a specific folder.</param>
+        public async Task<DtoBaseItem[]> GetItemsWithPersonAsync(string name, string personType, Guid userId, Guid? folderId = null)
+        {
+            string url = ApiUrl + "/itemlist?listtype=itemswithperson&userId=" + userId.ToString() + "&name=" + name;
+
+            url += "&persontype=" + personType;
+
+            if (folderId.HasValue)
+            {
+                url += "&id=" + folderId.ToString();
+            }
+
+            using (Stream stream = await GetSerializedStreamAsync(url).ConfigureAwait(false))
+            {
+                return DeserializeFromStream<DtoBaseItem[]>(stream);
+            }
+        }
+
+        /// <summary>
+        /// Gets all studious
+        /// </summary>
+        public async Task<IbnItem[]> GetAllStudiosAsync(Guid userId)
+        {
+            string url = ApiUrl + "/studios?userId=" + userId.ToString();
+
+            using (Stream stream = await GetSerializedStreamAsync(url).ConfigureAwait(false))
+            {
+                return DeserializeFromStream<IbnItem[]>(stream);
+            }
+        }
+
+        /// <summary>
+        /// Gets all items that contain a given Studio
+        /// </summary>
+        /// <param name="folderId">(Optional) Specify a folder Id to localize the search to a specific folder.</param>
+        public async Task<DtoBaseItem[]> GetItemsWithStudioAsync(string name, Guid userId, Guid? folderId = null)
+        {
+            string url = ApiUrl + "/itemlist?listtype=itemswithstudio&userId=" + userId.ToString() + "&name=" + name;
+
+            if (folderId.HasValue)
+            {
+                url += "&id=" + folderId.ToString();
+            }
+
+            using (Stream stream = await GetSerializedStreamAsync(url).ConfigureAwait(false))
+            {
+                return DeserializeFromStream<DtoBaseItem[]>(stream);
+            }
+        }
+
+        /// <summary>
+        /// Gets a studio
+        /// </summary>
+        public async Task<IbnItem> GetStudioAsync(Guid userId, string name)
+        {
+            string url = ApiUrl + "/studio?userId=" + userId.ToString() + "&name=" + name;
+
+            using (Stream stream = await GetSerializedStreamAsync(url).ConfigureAwait(false))
+            {
+                return DeserializeFromStream<IbnItem>(stream);
+            }
+        }
+
+        /// <summary>
+        /// Gets a genre
+        /// </summary>
+        public async Task<IbnItem> GetGenreAsync(Guid userId, string name)
+        {
+            string url = ApiUrl + "/genre?userId=" + userId.ToString() + "&name=" + name;
+
+            using (Stream stream = await GetSerializedStreamAsync(url).ConfigureAwait(false))
+            {
+                return DeserializeFromStream<IbnItem>(stream);
+            }
+        }
+
+        /// <summary>
+        /// Gets a person
+        /// </summary>
+        public async Task<IbnItem> GetPersonAsync(Guid userId, string name)
+        {
+            string url = ApiUrl + "/person?userId=" + userId.ToString() + "&name=" + name;
+
+            using (Stream stream = await GetSerializedStreamAsync(url).ConfigureAwait(false))
+            {
+                return DeserializeFromStream<IbnItem>(stream);
+            }
+        }
+
+        /// <summary>
+        /// Gets a year
+        /// </summary>
+        public async Task<IbnItem> GetYearAsync(Guid userId, int year)
+        {
+            string url = ApiUrl + "/year?userId=" + userId.ToString() + "&year=" + year;
+
+            using (Stream stream = await GetSerializedStreamAsync(url).ConfigureAwait(false))
+            {
+                return DeserializeFromStream<IbnItem>(stream);
+            }
+        }
+
+        /// <summary>
+        /// Gets a list of plugins installed on the server
+        /// </summary>
+        public async Task<PluginInfo[]> GetInstalledPluginsAsync()
+        {
+            string url = ApiUrl + "/plugins";
+
+            using (Stream stream = await GetSerializedStreamAsync(url).ConfigureAwait(false))
+            {
+                return DeserializeFromStream<PluginInfo[]>(stream);
+            }
+        }
+
+        /// <summary>
+        /// Gets a list of plugins installed on the server
+        /// </summary>
+        public Task<Stream> GetPluginAssemblyAsync(PluginInfo plugin)
+        {
+            string url = ApiUrl + "/pluginassembly?assemblyfilename=" + plugin.AssemblyFileName;
+
+            return GetStreamAsync(url);
+        }
+
+        /// <summary>
+        /// Gets the current server configuration
+        /// </summary>
+        public async Task<ServerConfiguration> GetServerConfigurationAsync()
+        {
+            string url = ApiUrl + "/ServerConfiguration";
+
+            using (Stream stream = await GetSerializedStreamAsync(url).ConfigureAwait(false))
+            {
+                return DeserializeFromStream<ServerConfiguration>(stream);
+            }
+        }
+
+        /// <summary>
+        /// Gets weather information for the default location as set in configuration
+        /// </summary>
+        public async Task<object> GetPluginConfigurationAsync(PluginInfo plugin, Type configurationType)
+        {
+            string url = ApiUrl + "/PluginConfiguration?assemblyfilename=" + plugin.AssemblyFileName;
+
+            // At the moment this can't be retrieved in protobuf format
+            SerializationFormats format = DataSerializer.CanDeSerializeJsv ? SerializationFormats.Jsv : SerializationFormats.Json;
+
+            using (Stream stream = await GetSerializedStreamAsync(url, format).ConfigureAwait(false))
+            {
+                return DataSerializer.DeserializeFromStream(stream, format, configurationType);
+            }
+        }
+
+        /// <summary>
+        /// Gets the default user
+        /// </summary>
+        public async Task<DtoUser> GetDefaultUserAsync()
+        {
+            string url = ApiUrl + "/user";
+
+            using (Stream stream = await GetSerializedStreamAsync(url).ConfigureAwait(false))
+            {
+                return DeserializeFromStream<DtoUser>(stream);
+            }
+        }
+
+        /// <summary>
+        /// Gets a user by id
+        /// </summary>
+        public async Task<DtoUser> GetUserAsync(Guid id)
+        {
+            string url = ApiUrl + "/user?id=" + id.ToString();
+
+            using (Stream stream = await GetSerializedStreamAsync(url).ConfigureAwait(false))
+            {
+                return DeserializeFromStream<DtoUser>(stream);
+            }
+        }
+
+        /// <summary>
+        /// Gets weather information for the default location as set in configuration
+        /// </summary>
+        public async Task<WeatherInfo> GetWeatherInfoAsync()
+        {
+            string url = ApiUrl + "/weather";
+
+            using (Stream stream = await GetSerializedStreamAsync(url).ConfigureAwait(false))
+            {
+                return DeserializeFromStream<WeatherInfo>(stream);
+            }
+        }
+
+        /// <summary>
+        /// Gets weather information for a specific zip code
+        /// </summary>
+        public async Task<WeatherInfo> GetWeatherInfoAsync(string zipCode)
+        {
+            string url = ApiUrl + "/weather?zipcode=" + zipCode;
+
+            using (Stream stream = await GetSerializedStreamAsync(url).ConfigureAwait(false))
+            {
+                return DeserializeFromStream<WeatherInfo>(stream);
+            }
+        }
+
+        /// <summary>
+        /// Gets special features for a Movie
+        /// </summary>
+        public async Task<DtoBaseItem[]> GetMovieSpecialFeaturesAsync(Guid itemId, Guid userId)
+        {
+            string url = ApiUrl + "/MovieSpecialFeatures?id=" + itemId;
+            url += "&userid=" + userId;
+
+            using (Stream stream = await GetSerializedStreamAsync(url).ConfigureAwait(false))
+            {
+                return DeserializeFromStream<DtoBaseItem[]>(stream);
+            }
+        }
+
+        /// <summary>
+        /// Updates played status for an item
+        /// </summary>
+        public async Task<DtoUserItemData> UpdatePlayedStatusAsync(Guid itemId, Guid userId, bool wasPlayed)
+        {
+            string url = ApiUrl + "/PlayedStatus?id=" + itemId;
+
+            url += "&userid=" + userId;
+            url += "&played=" + (wasPlayed ? "1" : "0");
+
+            using (Stream stream = await GetSerializedStreamAsync(url).ConfigureAwait(false))
+            {
+                return DeserializeFromStream<DtoUserItemData>(stream);
+            }
+        }
+
+        /// <summary>
+        /// Updates a user's favorite status for an item and returns the updated UserItemData object.
+        /// </summary>
+        public async Task<DtoUserItemData> UpdateFavoriteStatusAsync(Guid itemId, Guid userId, bool isFavorite)
+        {
+            string url = ApiUrl + "/favoritestatus?id=" + itemId;
+
+            url += "&userid=" + userId;
+            url += "&isfavorite=" + (isFavorite ? "1" : "0");
+
+            using (Stream stream = await GetSerializedStreamAsync(url).ConfigureAwait(false))
+            {
+                return DeserializeFromStream<DtoUserItemData>(stream);
+            }
+        }
+
+        /// <summary>
+        /// Clears a user's rating for an item
+        /// </summary>
+        public async Task<DtoUserItemData> ClearUserItemRatingAsync(Guid itemId, Guid userId)
+        {
+            string url = ApiUrl + "/UserItemRating?id=" + itemId;
+
+            url += "&userid=" + userId;
+            url += "&clear=1";
+
+            using (Stream stream = await GetSerializedStreamAsync(url).ConfigureAwait(false))
+            {
+                return DeserializeFromStream<DtoUserItemData>(stream);
+            }
+        }
+
+        /// <summary>
+        /// Updates a user's rating for an item, based on likes or dislikes
+        /// </summary>
+        public async Task<DtoUserItemData> UpdateUserItemRatingAsync(Guid itemId, Guid userId, bool likes)
+        {
+            string url = ApiUrl + "/UserItemRating?id=" + itemId;
+
+            url += "&userid=" + userId;
+            url += "&likes=" + (likes ? "1" : "0");
+
+            using (Stream stream = await GetSerializedStreamAsync(url).ConfigureAwait(false))
+            {
+                return DeserializeFromStream<DtoUserItemData>(stream);
+            }
+        }
+
+        /// <summary>
+        /// Authenticates a user and returns the result
+        /// </summary>
+        public async Task<AuthenticationResult> AuthenticateUserAsync(Guid userId, string password)
+        {
+            string url = ApiUrl + "/UserAuthentication?dataformat=" + SerializationFormat.ToString();
+
+            // Create the post body
+            string postContent = string.Format("userid={0}", userId);
+
+            if (!string.IsNullOrEmpty(password))
+            {
+                postContent += "&password=" + password;
+            }
+
+#if WINDOWS_PHONE
+            HttpClient.Headers["Content-Type"] = "application/x-www-form-urlencoded";
+            var result = await HttpClient.UploadStringTaskAsync(url, "POST", postContent);
+
+            var byteArray = Encoding.UTF8.GetBytes(result);
+            using (MemoryStream stream = new MemoryStream(byteArray))
+            {
+                return DeserializeFromStream<AuthenticationResult>(stream);
+            }
+#else
+            HttpContent content = new StringContent(postContent, Encoding.UTF8, "application/x-www-form-urlencoded");
+
+            HttpResponseMessage msg = await HttpClient.PostAsync(url, content).ConfigureAwait(false);
+
+            using (Stream stream = await msg.Content.ReadAsStreamAsync().ConfigureAwait(false))
+            {
+                return DeserializeFromStream<AuthenticationResult>(stream);
+            }
+#endif
+        }
+
+        /// <summary>
+        /// This is a helper around getting a stream from the server that contains serialized data
+        /// </summary>
+        private Task<Stream> GetSerializedStreamAsync(string url)
+        {
+            return GetSerializedStreamAsync(url, SerializationFormat);
+        }
+
+        /// <summary>
+        /// This is a helper around getting a stream from the server that contains serialized data
+        /// </summary>
+        private Task<Stream> GetSerializedStreamAsync(string url, SerializationFormats serializationFormat)
+        {
+            if (url.IndexOf('?') == -1)
+            {
+                url += "?dataformat=" + serializationFormat.ToString();
+            }
+            else
+            {
+                url += "&dataformat=" + serializationFormat.ToString();
+            }
+
+            return GetStreamAsync(url);
+        }
+
+        /// <summary>
+        /// This is just a helper around HttpClient
+        /// </summary>
+        private Task<Stream> GetStreamAsync(string url)
+        {
+#if WINDOWS_PHONE
+            return HttpClient.OpenReadTaskAsync(url);
+#else
+            return HttpClient.GetStreamAsync(url);
+#endif
+        }
+
+        public override void Dispose()
+        {
+#if !WINDOWS_PHONE
+            HttpClient.Dispose();
+#endif
+        }
+    }
+}

+ 77 - 0
MediaBrowser.ApiInteraction/DataSerializer.cs

@@ -0,0 +1,77 @@
+using ServiceStack.Text;
+using System;
+using System.IO;
+
+namespace MediaBrowser.ApiInteraction
+{
+    public static class DataSerializer
+    {
+        /// <summary>
+        /// This is an auto-generated Protobuf Serialization assembly for best performance.
+        /// It is created during the Model project's post-build event.
+        /// This means that this class can currently only handle types within the Model project.
+        /// If we need to, we can always add a param indicating whether or not the model serializer should be used.
+        /// </summary>
+        private static readonly ProtobufModelSerializer ProtobufModelSerializer = new ProtobufModelSerializer();
+        
+        /// <summary>
+        /// Deserializes an object using generics
+        /// </summary>
+        public static T DeserializeFromStream<T>(Stream stream, SerializationFormats format)
+            where T : class
+        {
+            if (format == SerializationFormats.Protobuf)
+            {
+                //return Serializer.Deserialize<T>(stream);
+                return ProtobufModelSerializer.Deserialize(stream, null, typeof(T)) as T;
+            }
+            if (format == SerializationFormats.Jsv)
+            {
+                return TypeSerializer.DeserializeFromStream<T>(stream);
+            }
+            if (format == SerializationFormats.Json)
+            {
+                return JsonSerializer.DeserializeFromStream<T>(stream);
+            }
+
+            throw new NotImplementedException();
+        }
+
+        /// <summary>
+        /// Deserializes an object
+        /// </summary>
+        public static object DeserializeFromStream(Stream stream, SerializationFormats format, Type type)
+        {
+            if (format == SerializationFormats.Protobuf)
+            {
+                //throw new NotImplementedException();
+                return ProtobufModelSerializer.Deserialize(stream, null, type);
+            }
+            if (format == SerializationFormats.Jsv)
+            {
+                return TypeSerializer.DeserializeFromStream(type, stream);
+            }
+            if (format == SerializationFormats.Json)
+            {
+                return JsonSerializer.DeserializeFromStream(type, stream);
+            }
+
+            throw new NotImplementedException();
+        }
+
+        public static void Configure()
+        {
+            JsConfig.DateHandler = JsonDateHandler.ISO8601;
+            JsConfig.ExcludeTypeInfo = true;
+            JsConfig.IncludeNullValues = false;
+        }
+
+        public static bool CanDeSerializeJsv
+        {
+            get
+            {
+                return true;
+            }
+        }
+    }
+}

+ 78 - 0
MediaBrowser.ApiInteraction/MediaBrowser.ApiInteraction.csproj

@@ -0,0 +1,78 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+  <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
+  <PropertyGroup>
+    <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+    <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+    <ProjectGuid>{921C0F64-FDA7-4E9F-9E73-0CB0EEDB2422}</ProjectGuid>
+    <OutputType>Library</OutputType>
+    <AppDesignerFolder>Properties</AppDesignerFolder>
+    <RootNamespace>MediaBrowser.ApiInteraction</RootNamespace>
+    <AssemblyName>MediaBrowser.ApiInteraction</AssemblyName>
+    <TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
+    <FileAlignment>512</FileAlignment>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
+    <DebugSymbols>true</DebugSymbols>
+    <DebugType>full</DebugType>
+    <Optimize>false</Optimize>
+    <OutputPath>bin\Debug\</OutputPath>
+    <DefineConstants>DEBUG;TRACE</DefineConstants>
+    <ErrorReport>prompt</ErrorReport>
+    <WarningLevel>4</WarningLevel>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
+    <DebugType>pdbonly</DebugType>
+    <Optimize>true</Optimize>
+    <OutputPath>bin\Release\</OutputPath>
+    <DefineConstants>TRACE</DefineConstants>
+    <ErrorReport>prompt</ErrorReport>
+    <WarningLevel>4</WarningLevel>
+  </PropertyGroup>
+  <ItemGroup>
+    <Reference Include="protobuf-net">
+      <HintPath>..\protobuf-net\Full\net30\protobuf-net.dll</HintPath>
+    </Reference>
+    <Reference Include="ProtobufModelSerializer">
+      <HintPath>..\MediaBrowser.Model\bin\ProtobufModelSerializer.dll</HintPath>
+    </Reference>
+    <Reference Include="ServiceStack.Text, Version=3.9.9.0, Culture=neutral, processorArchitecture=MSIL">
+      <SpecificVersion>False</SpecificVersion>
+      <HintPath>..\packages\ServiceStack.Text.3.9.9\lib\net35\ServiceStack.Text.dll</HintPath>
+    </Reference>
+    <Reference Include="System" />
+    <Reference Include="System.Core" />
+    <Reference Include="System.Net.Http" />
+    <Reference Include="System.Net.Http.WebRequest" />
+    <Reference Include="System.Xml.Linq" />
+    <Reference Include="System.Data.DataSetExtensions" />
+    <Reference Include="Microsoft.CSharp" />
+    <Reference Include="System.Data" />
+    <Reference Include="System.Xml" />
+  </ItemGroup>
+  <ItemGroup>
+    <Compile Include="ApiClient.cs" />
+    <Compile Include="BaseApiClient.cs" />
+    <Compile Include="BaseHttpApiClient.cs" />
+    <Compile Include="DataSerializer.cs" />
+    <Compile Include="Properties\AssemblyInfo.cs" />
+    <Compile Include="SerializationFormats.cs" />
+  </ItemGroup>
+  <ItemGroup>
+    <ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj">
+      <Project>{7eeeb4bb-f3e8-48fc-b4c5-70f0fff8329b}</Project>
+      <Name>MediaBrowser.Model</Name>
+    </ProjectReference>
+  </ItemGroup>
+  <ItemGroup>
+    <None Include="packages.config" />
+  </ItemGroup>
+  <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
+  <!-- To modify your build process, add your task inside one of the targets below and uncomment it. 
+       Other similar extension points exist, see Microsoft.Common.targets.
+  <Target Name="BeforeBuild">
+  </Target>
+  <Target Name="AfterBuild">
+  </Target>
+  -->
+</Project>

+ 35 - 0
MediaBrowser.ApiInteraction/Properties/AssemblyInfo.cs

@@ -0,0 +1,35 @@
+using System.Reflection;
+using System.Runtime.InteropServices;
+
+// General Information about an assembly is controlled through the following 
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: AssemblyTitle("MediaBrowser.ApiInteraction")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("MediaBrowser.ApiInteraction")]
+[assembly: AssemblyCopyright("Copyright ©  2012")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// Setting ComVisible to false makes the types in this assembly not visible 
+// to COM components.  If you need to access a type in this assembly from 
+// COM, set the ComVisible attribute to true on that type.
+[assembly: ComVisible(false)]
+
+// The following GUID is for the ID of the typelib if this project is exposed to COM
+[assembly: Guid("677618f2-de4b-44f4-8dfd-a90176297ee2")]
+
+// Version information for an assembly consists of the following four values:
+//
+//      Major Version
+//      Minor Version 
+//      Build Number
+//      Revision
+//
+// You can specify all the values or you can default the Build and Revision Numbers 
+// by using the '*' as shown below:
+// [assembly: AssemblyVersion("1.0.*")]
+[assembly: AssemblyVersion("1.0.0.0")]
+[assembly: AssemblyFileVersion("1.0.0.0")]

+ 10 - 0
MediaBrowser.ApiInteraction/SerializationFormats.cs

@@ -0,0 +1,10 @@
+
+namespace MediaBrowser.ApiInteraction
+{
+    public enum SerializationFormats
+    {
+        Json,
+        Jsv,
+        Protobuf
+    }
+}

+ 4 - 0
MediaBrowser.ApiInteraction/packages.config

@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<packages>
+  <package id="ServiceStack.Text" version="3.9.9" targetFramework="net45" />
+</packages>

+ 12 - 0
MediaBrowser.Common/Events/GenericEventArgs.cs

@@ -0,0 +1,12 @@
+using System;
+
+namespace MediaBrowser.Common.Events
+{
+    /// <summary>
+    /// Provides a generic EventArgs subclass that can hold any kind of object
+    /// </summary>
+    public class GenericEventArgs<T> : EventArgs
+    {
+        public T Argument { get; set; }
+    }
+}

+ 63 - 0
MediaBrowser.Common/Extensions/BaseExtensions.cs

@@ -0,0 +1,63 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Security.Cryptography;
+
+namespace MediaBrowser.Common.Extensions
+{
+    public static class BaseExtensions
+    {
+        static MD5CryptoServiceProvider md5Provider = new MD5CryptoServiceProvider();
+
+        public static Guid GetMD5(this string str)
+        {
+            lock (md5Provider)
+            {
+                return new Guid(md5Provider.ComputeHash(Encoding.Unicode.GetBytes(str)));
+            }
+        }
+
+        /// <summary>
+        /// Examine a list of strings assumed to be file paths to see if it contains a parent of 
+        /// the provided path.
+        /// </summary>
+        /// <param name="lst"></param>
+        /// <param name="path"></param>
+        /// <returns></returns>
+        public static bool ContainsParentFolder(this List<string> lst, string path)
+        {
+            path = path.TrimEnd('\\');
+            foreach (var str in lst)
+            {
+                //this should be a little quicker than examining each actual parent folder...
+                var compare = str.TrimEnd('\\');
+                if (path.Equals(compare,StringComparison.OrdinalIgnoreCase) 
+                    || (path.StartsWith(compare, StringComparison.OrdinalIgnoreCase) && path[compare.Length] == '\\')) return true;
+            }
+            return false;
+        }
+
+        /// <summary>
+        /// Helper method for Dictionaries since they throw on not-found keys
+        /// </summary>
+        /// <typeparam name="T"></typeparam>
+        /// <typeparam name="U"></typeparam>
+        /// <param name="dictionary"></param>
+        /// <param name="key"></param>
+        /// <param name="defaultValue"></param>
+        /// <returns></returns>
+        public static U GetValueOrDefault<T, U>(this Dictionary<T, U> dictionary, T key, U defaultValue)
+        {
+            U val;
+            if (!dictionary.TryGetValue(key, out val))
+            {
+                val = defaultValue;
+            }
+            return val;
+
+        }
+
+    }
+}

+ 154 - 0
MediaBrowser.Common/Kernel/BaseApplicationPaths.cs

@@ -0,0 +1,154 @@
+using System.Configuration;
+using System.IO;
+using System.Reflection;
+
+namespace MediaBrowser.Common.Kernel
+{
+    /// <summary>
+    /// Provides a base class to hold common application paths used by both the Ui and Server.
+    /// This can be subclassed to add application-specific paths.
+    /// </summary>
+    public abstract class BaseApplicationPaths
+    {
+        private string _programDataPath;
+        /// <summary>
+        /// Gets the path to the program data folder
+        /// </summary>
+        public string ProgramDataPath
+        {
+            get
+            {
+                if (_programDataPath == null)
+                {
+                    _programDataPath = GetProgramDataPath();
+                }
+                
+                return _programDataPath;
+            }
+        }
+
+        private string _pluginsPath;
+        /// <summary>
+        /// Gets the path to the plugin directory
+        /// </summary>
+        public string PluginsPath
+        {
+            get
+            {
+                if (_pluginsPath == null)
+                {
+                    _pluginsPath = Path.Combine(ProgramDataPath, "plugins");
+                    if (!Directory.Exists(_pluginsPath))
+                    {
+                        Directory.CreateDirectory(_pluginsPath);
+                    }
+                }
+
+                return _pluginsPath;
+            }
+        }
+
+        private string _pluginConfigurationsPath;
+        /// <summary>
+        /// Gets the path to the plugin configurations directory
+        /// </summary>
+        public string PluginConfigurationsPath
+        {
+            get
+            {
+                if (_pluginConfigurationsPath == null)
+                {
+                    _pluginConfigurationsPath = Path.Combine(PluginsPath, "configurations");
+                    if (!Directory.Exists(_pluginConfigurationsPath))
+                    {
+                        Directory.CreateDirectory(_pluginConfigurationsPath);
+                    }
+                }
+
+                return _pluginConfigurationsPath;
+            }
+        }
+
+        private string _logDirectoryPath;
+        /// <summary>
+        /// Gets the path to the log directory
+        /// </summary>
+        public string LogDirectoryPath
+        {
+            get
+            {
+                if (_logDirectoryPath == null)
+                {
+                    _logDirectoryPath = Path.Combine(ProgramDataPath, "logs");
+                    if (!Directory.Exists(_logDirectoryPath))
+                    {
+                        Directory.CreateDirectory(_logDirectoryPath);
+                    }
+                }
+                return _logDirectoryPath;
+            }
+        }
+
+        private string _configurationDirectoryPath;
+        /// <summary>
+        /// Gets the path to the application configuration root directory
+        /// </summary>
+        public string ConfigurationDirectoryPath
+        {
+            get
+            {
+                if (_configurationDirectoryPath == null)
+                {
+                    _configurationDirectoryPath = Path.Combine(ProgramDataPath, "config");
+                    if (!Directory.Exists(_configurationDirectoryPath))
+                    {
+                        Directory.CreateDirectory(_configurationDirectoryPath);
+                    }
+                }
+                return _configurationDirectoryPath;
+            }
+        }
+
+        private string _systemConfigurationFilePath;
+        /// <summary>
+        /// Gets the path to the system configuration file
+        /// </summary>
+        public string SystemConfigurationFilePath
+        {
+            get
+            {
+                if (_systemConfigurationFilePath == null)
+                {
+                    _systemConfigurationFilePath = Path.Combine(ConfigurationDirectoryPath, "system.xml");
+                }
+                return _systemConfigurationFilePath;
+            }
+        }
+
+        /// <summary>
+        /// Gets the path to the application's ProgramDataFolder
+        /// </summary>
+        private static string GetProgramDataPath()
+        {
+            string programDataPath = ConfigurationManager.AppSettings["ProgramDataPath"];
+
+            // If it's a relative path, e.g. "..\"
+            if (!Path.IsPathRooted(programDataPath))
+            {
+                string path = Assembly.GetExecutingAssembly().Location;
+                path = Path.GetDirectoryName(path);
+
+                programDataPath = Path.Combine(path, programDataPath);
+
+                programDataPath = Path.GetFullPath(programDataPath);
+            }
+
+            if (!Directory.Exists(programDataPath))
+            {
+                Directory.CreateDirectory(programDataPath);
+            }
+
+            return programDataPath;
+        }
+    }
+}

+ 345 - 0
MediaBrowser.Common/Kernel/BaseKernel.cs

@@ -0,0 +1,345 @@
+using MediaBrowser.Common.Events;
+using MediaBrowser.Common.Logging;
+using MediaBrowser.Common.Mef;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Common.Net.Handlers;
+using MediaBrowser.Common.Plugins;
+using MediaBrowser.Common.Serialization;
+using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.Progress;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.Composition;
+using System.ComponentModel.Composition.Hosting;
+using System.ComponentModel.Composition.Primitives;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Common.Kernel
+{
+    /// <summary>
+    /// Represents a shared base kernel for both the Ui and server apps
+    /// </summary>
+    public abstract class BaseKernel<TConfigurationType, TApplicationPathsType> : IDisposable, IKernel
+        where TConfigurationType : BaseApplicationConfiguration, new()
+        where TApplicationPathsType : BaseApplicationPaths, new()
+    {
+        #region ReloadBeginning Event
+        /// <summary>
+        /// Fires whenever the kernel begins reloading
+        /// </summary>
+        public event EventHandler<GenericEventArgs<IProgress<TaskProgress>>> ReloadBeginning;
+        private void OnReloadBeginning(IProgress<TaskProgress> progress)
+        {
+            if (ReloadBeginning != null)
+            {
+                ReloadBeginning(this, new GenericEventArgs<IProgress<TaskProgress>> { Argument = progress });
+            }
+        }
+        #endregion
+
+        #region ReloadCompleted Event
+        /// <summary>
+        /// Fires whenever the kernel completes reloading
+        /// </summary>
+        public event EventHandler<GenericEventArgs<IProgress<TaskProgress>>> ReloadCompleted;
+        private void OnReloadCompleted(IProgress<TaskProgress> progress)
+        {
+            if (ReloadCompleted != null)
+            {
+                ReloadCompleted(this, new GenericEventArgs<IProgress<TaskProgress>> { Argument = progress });
+            }
+        }
+        #endregion
+
+        /// <summary>
+        /// Gets the current configuration
+        /// </summary>
+        public TConfigurationType Configuration { get; private set; }
+
+        public TApplicationPathsType ApplicationPaths { get; private set; }
+
+        /// <summary>
+        /// Gets the list of currently loaded plugins
+        /// </summary>
+        [ImportMany(typeof(BasePlugin))]
+        public IEnumerable<BasePlugin> Plugins { get; private set; }
+
+        /// <summary>
+        /// Gets the list of currently registered http handlers
+        /// </summary>
+        [ImportMany(typeof(BaseHandler))]
+        private IEnumerable<BaseHandler> HttpHandlers { get; set; }
+
+        /// <summary>
+        /// Gets the list of currently registered Loggers
+        /// </summary>
+        [ImportMany(typeof(BaseLogger))]
+        public IEnumerable<BaseLogger> Loggers { get; set; }
+
+        /// <summary>
+        /// Both the Ui and server will have a built-in HttpServer.
+        /// People will inevitably want remote control apps so it's needed in the Ui too.
+        /// </summary>
+        public HttpServer HttpServer { get; private set; }
+
+        /// <summary>
+        /// This subscribes to HttpListener requests and finds the appropate BaseHandler to process it
+        /// </summary>
+        private IDisposable HttpListener { get; set; }
+
+        /// <summary>
+        /// Gets the MEF CompositionContainer
+        /// </summary>
+        private CompositionContainer CompositionContainer { get; set; }
+
+        protected virtual string HttpServerUrlPrefix
+        {
+            get
+            {
+                return "http://+:" + Configuration.HttpServerPortNumber + "/mediabrowser/";
+            }
+        }
+
+        /// <summary>
+        /// Gets the kernel context. Subclasses will have to override.
+        /// </summary>
+        public abstract KernelContext KernelContext { get; }
+
+        /// <summary>
+        /// Initializes the Kernel
+        /// </summary>
+        public async Task Init(IProgress<TaskProgress> progress)
+        {
+            Logger.Kernel = this;
+
+            // Performs initializations that only occur once
+            InitializeInternal(progress);
+
+            // Performs initializations that can be reloaded at anytime
+            await Reload(progress).ConfigureAwait(false);
+        }
+
+        /// <summary>
+        /// Performs initializations that only occur once
+        /// </summary>
+        protected virtual void InitializeInternal(IProgress<TaskProgress> progress)
+        {
+            ApplicationPaths = new TApplicationPathsType();
+
+            ReportProgress(progress, "Loading Configuration");
+            ReloadConfiguration();
+
+            ReportProgress(progress, "Loading Http Server");
+            ReloadHttpServer();
+        }
+
+        /// <summary>
+        /// Performs initializations that can be reloaded at anytime
+        /// </summary>
+        public async Task Reload(IProgress<TaskProgress> progress)
+        {
+            OnReloadBeginning(progress);
+
+            await ReloadInternal(progress).ConfigureAwait(false);
+
+            OnReloadCompleted(progress);
+
+            ReportProgress(progress, "Kernel.Reload Complete");
+        }
+
+        /// <summary>
+        /// Performs initializations that can be reloaded at anytime
+        /// </summary>
+        protected virtual async Task ReloadInternal(IProgress<TaskProgress> progress)
+        {
+            await Task.Run(() =>
+            {
+                ReportProgress(progress, "Loading Plugins");
+                ReloadComposableParts();
+
+            }).ConfigureAwait(false);
+        }
+
+        /// <summary>
+        /// Uses MEF to locate plugins
+        /// Subclasses can use this to locate types within plugins
+        /// </summary>
+        private void ReloadComposableParts()
+        {
+            DisposeComposableParts();
+
+            CompositionContainer = GetCompositionContainer(includeCurrentAssembly: true);
+
+            CompositionContainer.ComposeParts(this);
+
+            OnComposablePartsLoaded();
+
+            CompositionContainer.Catalog.Dispose();
+        }
+
+        /// <summary>
+        /// Constructs an MEF CompositionContainer based on the current running assembly and all plugin assemblies
+        /// </summary>
+        public CompositionContainer GetCompositionContainer(bool includeCurrentAssembly = false)
+        {
+            // Gets all plugin assemblies by first reading all bytes of the .dll and calling Assembly.Load against that
+            // This will prevent the .dll file from getting locked, and allow us to replace it when needed
+            IEnumerable<Assembly> pluginAssemblies = Directory.GetFiles(ApplicationPaths.PluginsPath, "*.dll", SearchOption.TopDirectoryOnly).Select(f => Assembly.Load(File.ReadAllBytes((f))));
+
+            var catalogs = new List<ComposablePartCatalog>();
+
+            catalogs.AddRange(pluginAssemblies.Select(a => new AssemblyCatalog(a)));
+
+            // Include composable parts in the Common assembly 
+            catalogs.Add(new AssemblyCatalog(Assembly.GetExecutingAssembly()));
+
+            if (includeCurrentAssembly)
+            {
+                // Include composable parts in the subclass assembly
+                catalogs.Add(new AssemblyCatalog(GetType().Assembly));
+            }
+
+            return MefUtils.GetSafeCompositionContainer(catalogs);
+        }
+
+        /// <summary>
+        /// Fires after MEF finishes finding composable parts within plugin assemblies
+        /// </summary>
+        protected virtual void OnComposablePartsLoaded()
+        {
+            foreach (var logger in Loggers)
+            {
+                logger.Initialize(this);
+            }
+
+            // Start-up each plugin
+            foreach (var plugin in Plugins)
+            {
+                plugin.Initialize(this);
+            }
+        }
+
+        /// <summary>
+        /// Reloads application configuration from the config file
+        /// </summary>
+        private void ReloadConfiguration()
+        {
+            //Configuration information for anything other than server-specific configuration will have to come via the API... -ebr
+
+            // Deserialize config
+            // Use try/catch to avoid the extra file system lookup using File.Exists
+            try
+            {
+                Configuration = XmlSerializer.DeserializeFromFile<TConfigurationType>(ApplicationPaths.SystemConfigurationFilePath);
+            }
+            catch (FileNotFoundException)
+            {
+                Configuration = new TConfigurationType();
+                XmlSerializer.SerializeToFile(Configuration, ApplicationPaths.SystemConfigurationFilePath);
+            }
+        }
+
+        /// <summary>
+        /// Restarts the Http Server, or starts it if not currently running
+        /// </summary>
+        private void ReloadHttpServer()
+        {
+            DisposeHttpServer();
+
+            HttpServer = new HttpServer(HttpServerUrlPrefix);
+
+            HttpListener = HttpServer.Subscribe(ctx =>
+            {
+                BaseHandler handler = HttpHandlers.FirstOrDefault(h => h.HandlesRequest(ctx.Request));
+
+                // Find the appropiate http handler
+                if (handler != null)
+                {
+                    // Need to create a new instance because handlers are currently stateful
+                    handler = Activator.CreateInstance(handler.GetType()) as BaseHandler;
+
+                    // No need to await this, despite the compiler warning
+                    handler.ProcessRequest(ctx);
+                }
+            });
+        }
+
+        /// <summary>
+        /// Disposes all resources currently in use.
+        /// </summary>
+        public virtual void Dispose()
+        {
+            Logger.LogInfo("Beginning Kernel.Dispose");
+
+            DisposeHttpServer();
+
+            DisposeComposableParts();
+        }
+
+        /// <summary>
+        /// Disposes all objects gathered through MEF composable parts
+        /// </summary>
+        protected virtual void DisposeComposableParts()
+        {
+            if (CompositionContainer != null)
+            {
+                CompositionContainer.Dispose();
+            }
+        }
+
+        /// <summary>
+        /// Disposes the current HttpServer
+        /// </summary>
+        private void DisposeHttpServer()
+        {
+            if (HttpServer != null)
+            {
+                Logger.LogInfo("Disposing Http Server");
+
+                HttpServer.Dispose();
+            }
+
+            if (HttpListener != null)
+            {
+                HttpListener.Dispose();
+            }
+        }
+
+        /// <summary>
+        /// Gets the current application version
+        /// </summary>
+        public Version ApplicationVersion
+        {
+            get
+            {
+                return GetType().Assembly.GetName().Version;
+            }
+        }
+
+        protected void ReportProgress(IProgress<TaskProgress> progress, string message)
+        {
+            progress.Report(new TaskProgress { Description = message });
+
+            Logger.LogInfo(message);
+        }
+
+        BaseApplicationPaths IKernel.ApplicationPaths
+        {
+            get { return ApplicationPaths; }
+        }
+    }
+
+    public interface IKernel
+    {
+        BaseApplicationPaths ApplicationPaths { get; }
+        KernelContext KernelContext { get; }
+
+        Task Init(IProgress<TaskProgress> progress);
+        Task Reload(IProgress<TaskProgress> progress);
+        IEnumerable<BaseLogger> Loggers { get; }
+        void Dispose();
+    }
+}

+ 9 - 0
MediaBrowser.Common/Kernel/KernelContext.cs

@@ -0,0 +1,9 @@
+
+namespace MediaBrowser.Common.Kernel
+{
+    public enum KernelContext
+    {
+        Server,
+        Ui
+    }
+}

+ 16 - 0
MediaBrowser.Common/Logging/BaseLogger.cs

@@ -0,0 +1,16 @@
+using MediaBrowser.Common.Kernel;
+using System;
+
+namespace MediaBrowser.Common.Logging
+{
+    public abstract class BaseLogger : IDisposable
+    {
+        public abstract void Initialize(IKernel kernel);
+        public abstract void LogEntry(LogRow row);
+
+        public virtual void Dispose()
+        {
+            Logger.LogInfo("Disposing " + GetType().Name);
+        }
+    }
+}

+ 44 - 0
MediaBrowser.Common/Logging/LogRow.cs

@@ -0,0 +1,44 @@
+using System;
+using System.Collections.Generic;
+
+namespace MediaBrowser.Common.Logging
+{
+    public struct LogRow
+    {
+        const string TimePattern = "h:mm:ss.fff tt d/M/yyyy";
+        
+        public LogSeverity Severity { get; set; }
+        public string Message { get; set; }
+        public int ThreadId { get; set; }
+        public string ThreadName { get; set; }
+        public DateTime Time { get; set; }
+
+        public override string ToString()
+        {
+            var data = new List<string>();
+
+            data.Add(Time.ToString(TimePattern));
+
+            data.Add(Severity.ToString());
+
+            if (!string.IsNullOrEmpty(Message))
+            {
+                data.Add(Encode(Message));
+            }
+
+            data.Add(ThreadId.ToString());
+
+            if (!string.IsNullOrEmpty(ThreadName))
+            {
+                data.Add(Encode(ThreadName));
+            }
+
+            return string.Join(" , ", data.ToArray());
+        }
+
+        private string Encode(string str)
+        {
+            return (str ?? "").Replace(",", ",,").Replace(Environment.NewLine, " [n] ");
+        }
+    }
+}

+ 14 - 0
MediaBrowser.Common/Logging/LogSeverity.cs

@@ -0,0 +1,14 @@
+using System;
+
+namespace MediaBrowser.Common.Logging
+{
+    [Flags]
+    public enum LogSeverity
+    {
+        None = 0,
+        Debug = 1,
+        Info = 2,
+        Warning = 4,
+        Error = 8
+    }
+}

+ 93 - 0
MediaBrowser.Common/Logging/Logger.cs

@@ -0,0 +1,93 @@
+using System;
+using System.Diagnostics;
+using System.Text;
+using System.Threading;
+using MediaBrowser.Common.Kernel;
+
+namespace MediaBrowser.Common.Logging
+{
+    public static class Logger
+    {
+        internal static IKernel Kernel { get; set; }
+
+        public static void LogInfo(string message, params object[] paramList)
+        {
+            LogEntry(message, LogSeverity.Info, paramList);
+        }
+
+        public static void LogDebugInfo(string message, params object[] paramList)
+        {
+            LogEntry(message, LogSeverity.Debug, paramList);
+        }
+
+        public static void LogError(string message, params object[] paramList)
+        {
+            LogEntry(message, LogSeverity.Error, paramList);
+        }
+
+        public static void LogException(Exception ex, params object[] paramList)
+        {
+            LogException(string.Empty, ex, paramList);
+        }
+
+        public static void LogException(string message, Exception ex, params object[] paramList)
+        {
+            var builder = new StringBuilder();
+
+            if (ex != null)
+            {
+                builder.AppendFormat("Exception.  Type={0} Msg={1} StackTrace={3}{2}",
+                    ex.GetType().FullName,
+                    ex.Message,
+                    ex.StackTrace,
+                    Environment.NewLine);
+            }
+
+            message = FormatMessage(message, paramList);
+
+            LogError(string.Format("{0} ( {1} )", message, builder));
+        }
+
+        public static void LogWarning(string message, params object[] paramList)
+        {
+            LogEntry(message, LogSeverity.Warning, paramList);
+        }
+
+        private static void LogEntry(string message, LogSeverity severity, params object[] paramList)
+        {
+            message = FormatMessage(message, paramList);
+
+            Thread currentThread = Thread.CurrentThread;
+
+            var row = new LogRow
+            {
+                Severity = severity,
+                Message = message,
+                ThreadId = currentThread.ManagedThreadId,
+                ThreadName = currentThread.Name,
+                Time = DateTime.Now
+            };
+
+            if (Kernel.Loggers != null)
+            {
+                foreach (var logger in Kernel.Loggers)
+                {
+                    logger.LogEntry(row);
+                }
+            }
+        }
+
+        private static string FormatMessage(string message, params object[] paramList)
+        {
+            if (paramList != null)
+            {
+                for (int i = 0; i < paramList.Length; i++)
+                {
+                    message = message.Replace("{" + i + "}", paramList[i].ToString());
+                }
+            }
+
+            return message;
+        }
+    }
+}

+ 38 - 0
MediaBrowser.Common/Logging/TraceFileLogger.cs

@@ -0,0 +1,38 @@
+using MediaBrowser.Common.Kernel;
+using System;
+using System.ComponentModel.Composition;
+using System.Diagnostics;
+using System.IO;
+
+namespace MediaBrowser.Common.Logging
+{
+    [Export(typeof(BaseLogger))]
+    public class TraceFileLogger : BaseLogger
+    {
+        private TraceListener Listener { get; set; }
+
+        public override void Initialize(IKernel kernel)
+        {
+            DateTime now = DateTime.Now;
+
+            string logFilePath = Path.Combine(kernel.ApplicationPaths.LogDirectoryPath, "log-" + now.ToString("dMyyyy") + "-" + now.Ticks + ".log");
+
+            Listener = new TextWriterTraceListener(logFilePath);
+            Trace.Listeners.Add(Listener);
+            Trace.AutoFlush = true;
+        }
+
+        public override void Dispose()
+        {
+            base.Dispose();
+
+            Trace.Listeners.Remove(Listener);
+            Listener.Dispose();
+        }
+
+        public override void LogEntry(LogRow row)
+        {
+            Trace.WriteLine(row.ToString());
+        }
+    }
+}

+ 164 - 0
MediaBrowser.Common/MediaBrowser.Common.csproj

@@ -0,0 +1,164 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+  <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
+  <PropertyGroup>
+    <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+    <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+    <ProjectGuid>{9142EEFA-7570-41E1-BFCC-468BB571AF2F}</ProjectGuid>
+    <OutputType>Library</OutputType>
+    <AppDesignerFolder>Properties</AppDesignerFolder>
+    <RootNamespace>MediaBrowser.Common</RootNamespace>
+    <AssemblyName>MediaBrowser.Common</AssemblyName>
+    <TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
+    <ProjectTypeGuids>{60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
+    <FileAlignment>512</FileAlignment>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
+    <DebugSymbols>true</DebugSymbols>
+    <DebugType>full</DebugType>
+    <Optimize>false</Optimize>
+    <OutputPath>bin\Debug\</OutputPath>
+    <DefineConstants>DEBUG;TRACE</DefineConstants>
+    <ErrorReport>prompt</ErrorReport>
+    <WarningLevel>4</WarningLevel>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
+    <DebugType>pdbonly</DebugType>
+    <Optimize>true</Optimize>
+    <OutputPath>bin\Release\</OutputPath>
+    <DefineConstants>TRACE</DefineConstants>
+    <ErrorReport>prompt</ErrorReport>
+    <WarningLevel>4</WarningLevel>
+  </PropertyGroup>
+  <PropertyGroup>
+    <ApplicationIcon>Resources\Images\Icon.ico</ApplicationIcon>
+  </PropertyGroup>
+  <ItemGroup>
+    <Reference Include="MahApps.Metro">
+      <HintPath>..\packages\MahApps.Metro.0.9.0.0\lib\net40\MahApps.Metro.dll</HintPath>
+    </Reference>
+    <Reference Include="PresentationCore" />
+    <Reference Include="PresentationFramework" />
+    <Reference Include="protobuf-net">
+      <HintPath>..\protobuf-net\Full\net30\protobuf-net.dll</HintPath>
+    </Reference>
+    <Reference Include="ProtobufModelSerializer">
+      <HintPath>..\MediaBrowser.Model\bin\ProtobufModelSerializer.dll</HintPath>
+    </Reference>
+    <Reference Include="ServiceStack.Text, Version=3.9.9.0, Culture=neutral, processorArchitecture=MSIL">
+      <SpecificVersion>False</SpecificVersion>
+      <HintPath>..\packages\ServiceStack.Text.3.9.9\lib\net35\ServiceStack.Text.dll</HintPath>
+    </Reference>
+    <Reference Include="System" />
+    <Reference Include="System.ComponentModel.Composition" />
+    <Reference Include="System.Configuration" />
+    <Reference Include="System.Core" />
+    <Reference Include="System.Drawing" />
+    <Reference Include="System.Reactive.Core, Version=2.0.20823.0, Culture=neutral, PublicKeyToken=f300afd708cefcd3, processorArchitecture=MSIL">
+      <SpecificVersion>False</SpecificVersion>
+      <HintPath>..\packages\Rx-Core.2.0.20823\lib\Net45\System.Reactive.Core.dll</HintPath>
+    </Reference>
+    <Reference Include="System.Reactive.Interfaces, Version=2.0.20823.0, Culture=neutral, PublicKeyToken=f300afd708cefcd3, processorArchitecture=MSIL">
+      <SpecificVersion>False</SpecificVersion>
+      <HintPath>..\packages\Rx-Interfaces.2.0.20823\lib\Net45\System.Reactive.Interfaces.dll</HintPath>
+    </Reference>
+    <Reference Include="System.Reactive.Linq, Version=2.0.20823.0, Culture=neutral, PublicKeyToken=f300afd708cefcd3, processorArchitecture=MSIL">
+      <SpecificVersion>False</SpecificVersion>
+      <HintPath>..\packages\Rx-Linq.2.0.20823\lib\Net45\System.Reactive.Linq.dll</HintPath>
+    </Reference>
+    <Reference Include="System.Runtime.Remoting" />
+    <Reference Include="System.Runtime.Serialization" />
+    <Reference Include="System.Web" />
+    <Reference Include="System.Windows.Interactivity, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
+      <HintPath>..\packages\MahApps.Metro.0.9.0.0\lib\net40\System.Windows.Interactivity.dll</HintPath>
+    </Reference>
+    <Reference Include="System.Xaml" />
+    <Reference Include="System.Xml.Linq" />
+    <Reference Include="System.Data.DataSetExtensions" />
+    <Reference Include="Microsoft.CSharp" />
+    <Reference Include="System.Data" />
+    <Reference Include="System.Xml" />
+    <Reference Include="WindowsBase" />
+  </ItemGroup>
+  <ItemGroup>
+    <Compile Include="Extensions\BaseExtensions.cs" />
+    <Compile Include="Events\GenericEventArgs.cs" />
+    <Compile Include="Kernel\BaseApplicationPaths.cs" />
+    <Compile Include="Logging\BaseLogger.cs" />
+    <Compile Include="Logging\LogSeverity.cs" />
+    <Compile Include="Logging\TraceFileLogger.cs" />
+    <Compile Include="Mef\MefUtils.cs" />
+    <Compile Include="Net\Handlers\StaticFileHandler.cs" />
+    <Compile Include="Net\MimeTypes.cs" />
+    <Compile Include="Plugins\BaseTheme.cs" />
+    <Compile Include="Properties\Resources.Designer.cs">
+      <AutoGen>True</AutoGen>
+      <DesignTime>True</DesignTime>
+      <DependentUpon>Resources.resx</DependentUpon>
+    </Compile>
+    <Compile Include="Serialization\JsonSerializer.cs" />
+    <Compile Include="Kernel\BaseKernel.cs" />
+    <Compile Include="Kernel\KernelContext.cs" />
+    <Compile Include="Logging\Logger.cs" />
+    <Compile Include="Logging\LogRow.cs" />
+    <Compile Include="Net\Handlers\BaseEmbeddedResourceHandler.cs" />
+    <Compile Include="Net\Handlers\BaseHandler.cs" />
+    <Compile Include="Net\Handlers\BaseSerializationHandler.cs" />
+    <Compile Include="Net\HttpServer.cs" />
+    <Compile Include="Net\Request.cs" />
+    <Compile Include="Plugins\BasePlugin.cs" />
+    <Compile Include="Properties\AssemblyInfo.cs" />
+    <Compile Include="Serialization\JsvSerializer.cs" />
+    <Compile Include="Serialization\ProtobufSerializer.cs" />
+    <Compile Include="Serialization\XmlSerializer.cs" />
+    <Compile Include="UI\BaseApplication.cs" />
+    <Compile Include="UI\Splash.xaml.cs">
+      <DependentUpon>Splash.xaml</DependentUpon>
+    </Compile>
+    <Compile Include="UI\SingleInstance.cs" />
+  </ItemGroup>
+  <ItemGroup>
+    <None Include="app.config" />
+    <None Include="packages.config" />
+  </ItemGroup>
+  <ItemGroup>
+    <ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj">
+      <Project>{7eeeb4bb-f3e8-48fc-b4c5-70f0fff8329b}</Project>
+      <Name>MediaBrowser.Model</Name>
+    </ProjectReference>
+  </ItemGroup>
+  <ItemGroup>
+    <Page Include="UI\Splash.xaml">
+      <SubType>Designer</SubType>
+      <Generator>MSBuild:Compile</Generator>
+    </Page>
+  </ItemGroup>
+  <ItemGroup>
+    <EmbeddedResource Include="Properties\Resources.resx">
+      <Generator>ResXFileCodeGenerator</Generator>
+      <LastGenOutput>Resources.Designer.cs</LastGenOutput>
+      <SubType>Designer</SubType>
+    </EmbeddedResource>
+  </ItemGroup>
+  <ItemGroup>
+    <Resource Include="Resources\Images\mblogoblack.png" />
+  </ItemGroup>
+  <ItemGroup>
+    <Resource Include="Resources\Images\Icon.ico" />
+  </ItemGroup>
+  <ItemGroup>
+    <Resource Include="Resources\Images\mblogowhite.png" />
+  </ItemGroup>
+  <ItemGroup>
+    <Resource Include="Resources\Images\spinner.gif" />
+  </ItemGroup>
+  <ItemGroup />
+  <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
+  <!-- To modify your build process, add your task inside one of the targets below and uncomment it. 
+       Other similar extension points exist, see Microsoft.Common.targets.
+  <Target Name="BeforeBuild">
+  </Target>
+  <Target Name="AfterBuild">
+  </Target>
+  -->
+</Project>

+ 43 - 0
MediaBrowser.Common/Mef/MefUtils.cs

@@ -0,0 +1,43 @@
+using System.Collections.Generic;
+using System.ComponentModel.Composition.Hosting;
+using System.ComponentModel.Composition.Primitives;
+using System.Linq;
+using System.Reflection;
+
+namespace MediaBrowser.Common.Mef
+{
+    public static class MefUtils
+    {
+        /// <summary>
+        /// Plugins that live on both the server and UI are going to have references to assemblies from both sides.
+        /// But looks for Parts on one side, it will throw an exception when it seems Types from the other side that it doesn't have a reference to.
+        /// For example, a plugin provides a Resolver. When MEF runs in the UI, it will throw an exception when it sees the resolver because there won't be a reference to the base class.
+        /// This method will catch those exceptions while retining the list of Types that MEF is able to resolve.
+        /// </summary>
+        public static CompositionContainer GetSafeCompositionContainer(IEnumerable<ComposablePartCatalog> catalogs)
+        {
+            var newList = new List<ComposablePartCatalog>();
+
+            // Go through each Catalog
+            foreach (var catalog in catalogs)
+            {
+                try
+                {
+                    // Try to have MEF find Parts
+                    catalog.Parts.ToArray();
+
+                    // If it succeeds we can use the entire catalog
+                    newList.Add(catalog);
+                }
+                catch (ReflectionTypeLoadException ex)
+                {
+                    // If it fails we can still get a list of the Types it was able to resolve and create TypeCatalogs
+                    var typeCatalogs = ex.Types.Where(t => t != null).Select(t => new TypeCatalog(t));
+                    newList.AddRange(typeCatalogs);
+                }
+            }
+
+            return new CompositionContainer(new AggregateCatalog(newList));
+        }
+    }
+}

+ 23 - 0
MediaBrowser.Common/Net/Handlers/BaseEmbeddedResourceHandler.cs

@@ -0,0 +1,23 @@
+using System.IO;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Common.Net.Handlers
+{
+    public abstract class BaseEmbeddedResourceHandler : BaseHandler
+    {
+        protected BaseEmbeddedResourceHandler(string resourcePath)
+            : base()
+        {
+            ResourcePath = resourcePath;
+        }
+
+        protected string ResourcePath { get; set; }
+
+        protected override Task WriteResponseToOutputStream(Stream stream)
+        {
+            return GetEmbeddedResourceStream().CopyToAsync(stream);
+        }
+
+        protected abstract Stream GetEmbeddedResourceStream();
+    }
+}

+ 430 - 0
MediaBrowser.Common/Net/Handlers/BaseHandler.cs

@@ -0,0 +1,430 @@
+using MediaBrowser.Common.Logging;
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.IO;
+using System.IO.Compression;
+using System.Linq;
+using System.Net;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Common.Net.Handlers
+{
+    public abstract class BaseHandler
+    {
+        public abstract bool HandlesRequest(HttpListenerRequest request);
+
+        private Stream CompressedStream { get; set; }
+
+        public virtual bool? UseChunkedEncoding
+        {
+            get
+            {
+                return null;
+            }
+        }
+
+        private bool _totalContentLengthDiscovered;
+        private long? _totalContentLength;
+        public long? TotalContentLength
+        {
+            get
+            {
+                if (!_totalContentLengthDiscovered)
+                {
+                    _totalContentLength = GetTotalContentLength();
+                    _totalContentLengthDiscovered = true;
+                }
+
+                return _totalContentLength;
+            }
+        }
+
+        protected virtual bool SupportsByteRangeRequests
+        {
+            get
+            {
+                return false;
+            }
+        }
+
+        /// <summary>
+        /// The original HttpListenerContext
+        /// </summary>
+        protected HttpListenerContext HttpListenerContext { get; set; }
+
+        /// <summary>
+        /// The original QueryString
+        /// </summary>
+        protected NameValueCollection QueryString
+        {
+            get
+            {
+                return HttpListenerContext.Request.QueryString;
+            }
+        }
+
+        private List<KeyValuePair<long, long?>> _requestedRanges;
+        protected IEnumerable<KeyValuePair<long, long?>> RequestedRanges
+        {
+            get
+            {
+                if (_requestedRanges == null)
+                {
+                    _requestedRanges = new List<KeyValuePair<long, long?>>();
+
+                    if (IsRangeRequest)
+                    {
+                        // Example: bytes=0-,32-63
+                        string[] ranges = HttpListenerContext.Request.Headers["Range"].Split('=')[1].Split(',');
+
+                        foreach (string range in ranges)
+                        {
+                            string[] vals = range.Split('-');
+
+                            long start = 0;
+                            long? end = null;
+
+                            if (!string.IsNullOrEmpty(vals[0]))
+                            {
+                                start = long.Parse(vals[0]);
+                            }
+                            if (!string.IsNullOrEmpty(vals[1]))
+                            {
+                                end = long.Parse(vals[1]);
+                            }
+
+                            _requestedRanges.Add(new KeyValuePair<long, long?>(start, end));
+                        }
+                    }
+                }
+
+                return _requestedRanges;
+            }
+        }
+
+        protected bool IsRangeRequest
+        {
+            get
+            {
+                return HttpListenerContext.Request.Headers.AllKeys.Contains("Range");
+            }
+        }
+
+        private bool ClientSupportsCompression
+        {
+            get
+            {
+                string enc = HttpListenerContext.Request.Headers["Accept-Encoding"] ?? string.Empty;
+
+                return enc.IndexOf("deflate", StringComparison.OrdinalIgnoreCase) != -1 || enc.IndexOf("gzip", StringComparison.OrdinalIgnoreCase) != -1;
+            }
+        }
+
+        private string CompressionMethod
+        {
+            get
+            {
+                string enc = HttpListenerContext.Request.Headers["Accept-Encoding"] ?? string.Empty;
+
+                if (enc.IndexOf("deflate", StringComparison.OrdinalIgnoreCase) != -1)
+                {
+                    return "deflate";
+                }
+                if (enc.IndexOf("gzip", StringComparison.OrdinalIgnoreCase) != -1)
+                {
+                    return "gzip";
+                }
+
+                return null;
+            }
+        }
+
+        public virtual async Task ProcessRequest(HttpListenerContext ctx)
+        {
+            HttpListenerContext = ctx;
+
+            string url = ctx.Request.Url.ToString();
+            Logger.LogInfo("Http Server received request at: " + url);
+            Logger.LogInfo("Http Headers: " + string.Join(",", ctx.Request.Headers.AllKeys.Select(k => k + "=" + ctx.Request.Headers[k])));
+
+            ctx.Response.AddHeader("Access-Control-Allow-Origin", "*");
+
+            ctx.Response.KeepAlive = true;
+
+            try
+            {
+                if (SupportsByteRangeRequests && IsRangeRequest)
+                {
+                    ctx.Response.Headers["Accept-Ranges"] = "bytes";
+                }
+
+                ResponseInfo responseInfo = await GetResponseInfo().ConfigureAwait(false);
+
+                if (responseInfo.IsResponseValid)
+                {
+                    // Set the initial status code
+                    // When serving a range request, we need to return status code 206 to indicate a partial response body
+                    responseInfo.StatusCode = SupportsByteRangeRequests && IsRangeRequest ? 206 : 200;
+                }
+
+                ctx.Response.ContentType = responseInfo.ContentType;
+
+                if (!string.IsNullOrEmpty(responseInfo.Etag))
+                {
+                    ctx.Response.Headers["ETag"] = responseInfo.Etag;
+                }
+
+                if (ctx.Request.Headers.AllKeys.Contains("If-Modified-Since"))
+                {
+                    DateTime ifModifiedSince;
+
+                    if (DateTime.TryParse(ctx.Request.Headers["If-Modified-Since"], out ifModifiedSince))
+                    {
+                        // If the cache hasn't expired yet just return a 304
+                        if (IsCacheValid(ifModifiedSince.ToUniversalTime(), responseInfo.CacheDuration, responseInfo.DateLastModified))
+                        {
+                            // ETag must also match (if supplied)
+                            if ((responseInfo.Etag ?? string.Empty).Equals(ctx.Request.Headers["If-None-Match"] ?? string.Empty))
+                            {
+                                responseInfo.StatusCode = 304;
+                            }
+                        }
+                    }
+                }
+
+                Logger.LogInfo("Responding with status code {0} for url {1}", responseInfo.StatusCode, url);
+
+                if (responseInfo.IsResponseValid)
+                {
+                    await ProcessUncachedRequest(ctx, responseInfo).ConfigureAwait(false);
+                }
+                else
+                {
+                    ctx.Response.StatusCode = responseInfo.StatusCode;
+                    ctx.Response.SendChunked = false;
+                }
+            }
+            catch (Exception ex)
+            {
+                // It might be too late if some response data has already been transmitted, but try to set this
+                ctx.Response.StatusCode = 500;
+
+                Logger.LogException(ex);
+            }
+            finally
+            {
+                DisposeResponseStream();
+            }
+        }
+
+        private async Task ProcessUncachedRequest(HttpListenerContext ctx, ResponseInfo responseInfo)
+        {
+            long? totalContentLength = TotalContentLength;
+
+            // By default, use chunked encoding if we don't know the content length
+            bool useChunkedEncoding = UseChunkedEncoding == null ? (totalContentLength == null) : UseChunkedEncoding.Value;
+
+            // Don't force this to true. HttpListener will default it to true if supported by the client.
+            if (!useChunkedEncoding)
+            {
+                ctx.Response.SendChunked = false;
+            }
+
+            // Set the content length, if we know it
+            if (totalContentLength.HasValue)
+            {
+                ctx.Response.ContentLength64 = totalContentLength.Value;
+            }
+
+            var compressResponse = responseInfo.CompressResponse && ClientSupportsCompression;
+
+            // Add the compression header
+            if (compressResponse)
+            {
+                ctx.Response.AddHeader("Content-Encoding", CompressionMethod);
+            }
+
+            if (responseInfo.DateLastModified.HasValue)
+            {
+                ctx.Response.Headers[HttpResponseHeader.LastModified] = responseInfo.DateLastModified.Value.ToString("r");
+            }
+
+            // Add caching headers
+            if (responseInfo.CacheDuration.Ticks > 0)
+            {
+                CacheResponse(ctx.Response, responseInfo.CacheDuration);
+            }
+
+            // Set the status code
+            ctx.Response.StatusCode = responseInfo.StatusCode;
+
+            if (responseInfo.IsResponseValid)
+            {
+                // Finally, write the response data
+                Stream outputStream = ctx.Response.OutputStream;
+
+                if (compressResponse)
+                {
+                    if (CompressionMethod.Equals("deflate", StringComparison.OrdinalIgnoreCase))
+                    {
+                        CompressedStream = new DeflateStream(outputStream, CompressionLevel.Fastest, false);
+                    }
+                    else
+                    {
+                        CompressedStream = new GZipStream(outputStream, CompressionLevel.Fastest, false);
+                    }
+
+                    outputStream = CompressedStream;
+                }
+
+                await WriteResponseToOutputStream(outputStream).ConfigureAwait(false);
+            }
+            else
+            {
+                ctx.Response.SendChunked = false;
+            }
+        }
+
+        private void CacheResponse(HttpListenerResponse response, TimeSpan duration)
+        {
+            response.Headers[HttpResponseHeader.CacheControl] = "public, max-age=" + Convert.ToInt32(duration.TotalSeconds);
+            response.Headers[HttpResponseHeader.Expires] = DateTime.UtcNow.Add(duration).ToString("r");
+        }
+
+        protected abstract Task WriteResponseToOutputStream(Stream stream);
+
+        protected virtual void DisposeResponseStream()
+        {
+            if (CompressedStream != null)
+            {
+                CompressedStream.Dispose();
+            }
+
+            HttpListenerContext.Response.OutputStream.Dispose();
+        }
+
+        private bool IsCacheValid(DateTime ifModifiedSince, TimeSpan cacheDuration, DateTime? dateModified)
+        {
+            if (dateModified.HasValue)
+            {
+                DateTime lastModified = NormalizeDateForComparison(dateModified.Value);
+                ifModifiedSince = NormalizeDateForComparison(ifModifiedSince);
+
+                return lastModified <= ifModifiedSince;
+            }
+
+            DateTime cacheExpirationDate = ifModifiedSince.Add(cacheDuration);
+
+            if (DateTime.UtcNow < cacheExpirationDate)
+            {
+                return true;
+            }
+
+            return false;
+        }
+
+        /// <summary>
+        /// When the browser sends the IfModifiedDate, it's precision is limited to seconds, so this will account for that
+        /// </summary>
+        private DateTime NormalizeDateForComparison(DateTime date)
+        {
+            return new DateTime(date.Year, date.Month, date.Day, date.Hour, date.Minute, date.Second, date.Kind);
+        }
+
+        protected virtual long? GetTotalContentLength()
+        {
+            return null;
+        }
+
+        protected abstract Task<ResponseInfo> GetResponseInfo();
+
+        private Hashtable _formValues;
+
+        /// <summary>
+        /// Gets a value from form POST data
+        /// </summary>
+        protected async Task<string> GetFormValue(string name)
+        {
+            if (_formValues == null)
+            {
+                _formValues = await GetFormValues(HttpListenerContext.Request).ConfigureAwait(false);
+            }
+
+            if (_formValues.ContainsKey(name))
+            {
+                return _formValues[name].ToString();
+            }
+
+            return null;
+        }
+
+        /// <summary>
+        /// Extracts form POST data from a request
+        /// </summary>
+        private async Task<Hashtable> GetFormValues(HttpListenerRequest request)
+        {
+            var formVars = new Hashtable();
+
+            if (request.HasEntityBody)
+            {
+                if (request.ContentType.IndexOf("application/x-www-form-urlencoded", StringComparison.OrdinalIgnoreCase) != -1)
+                {
+                    using (Stream requestBody = request.InputStream)
+                    {
+                        using (var reader = new StreamReader(requestBody, request.ContentEncoding))
+                        {
+                            string s = await reader.ReadToEndAsync().ConfigureAwait(false);
+
+                            string[] pairs = s.Split('&');
+
+                            for (int x = 0; x < pairs.Length; x++)
+                            {
+                                string pair = pairs[x];
+
+                                int index = pair.IndexOf('=');
+
+                                if (index != -1)
+                                {
+                                    string name = pair.Substring(0, index);
+                                    string value = pair.Substring(index + 1);
+                                    formVars.Add(name, value);
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+
+            return formVars;
+        }
+    }
+
+    public class ResponseInfo
+    {
+        public string ContentType { get; set; }
+        public string Etag { get; set; }
+        public DateTime? DateLastModified { get; set; }
+        public TimeSpan CacheDuration { get; set; }
+        public bool CompressResponse { get; set; }
+        public int StatusCode { get; set; }
+
+        public ResponseInfo()
+        {
+            CacheDuration = TimeSpan.FromTicks(0);
+
+            CompressResponse = true;
+
+            StatusCode = 200;
+        }
+
+        public bool IsResponseValid
+        {
+            get
+            {
+                return StatusCode == 200 || StatusCode == 206;
+            }
+        }
+    }
+}

+ 90 - 0
MediaBrowser.Common/Net/Handlers/BaseSerializationHandler.cs

@@ -0,0 +1,90 @@
+using MediaBrowser.Common.Serialization;
+using System;
+using System.IO;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Common.Net.Handlers
+{
+    public abstract class BaseSerializationHandler<T> : BaseHandler
+        where T : class
+    {
+        public SerializationFormat SerializationFormat
+        {
+            get
+            {
+                string format = QueryString["dataformat"];
+
+                if (string.IsNullOrEmpty(format))
+                {
+                    return SerializationFormat.Json;
+                }
+
+                return (SerializationFormat)Enum.Parse(typeof(SerializationFormat), format, true);
+            }
+        }
+
+        protected string ContentType
+        {
+            get
+            {
+                switch (SerializationFormat)
+                {
+                    case SerializationFormat.Jsv:
+                        return "text/plain";
+                    case SerializationFormat.Protobuf:
+                        return "application/x-protobuf";
+                    default:
+                        return MimeTypes.JsonMimeType;
+                }
+            }
+        }
+
+        protected override async Task<ResponseInfo> GetResponseInfo()
+        {
+            ResponseInfo info = new ResponseInfo
+            {
+                ContentType = ContentType
+            };
+
+            _objectToSerialize = await GetObjectToSerialize().ConfigureAwait(false);
+
+            if (_objectToSerialize == null)
+            {
+                info.StatusCode = 404;
+            }
+
+            return info;
+        }
+
+        private T _objectToSerialize;
+
+        protected abstract Task<T> GetObjectToSerialize();
+
+        protected override Task WriteResponseToOutputStream(Stream stream)
+        {
+            return Task.Run(() =>
+            {
+                switch (SerializationFormat)
+                {
+                    case SerializationFormat.Jsv:
+                        JsvSerializer.SerializeToStream(_objectToSerialize, stream);
+                        break;
+                    case SerializationFormat.Protobuf:
+                        ProtobufSerializer.SerializeToStream(_objectToSerialize, stream);
+                        break;
+                    default:
+                        JsonSerializer.SerializeToStream(_objectToSerialize, stream);
+                        break;
+                }
+            });
+        }
+    }
+
+    public enum SerializationFormat
+    {
+        Json,
+        Jsv,
+        Protobuf
+    }
+
+}

+ 249 - 0
MediaBrowser.Common/Net/Handlers/StaticFileHandler.cs

@@ -0,0 +1,249 @@
+using MediaBrowser.Common.Logging;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Net;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Common.Net.Handlers
+{
+    public class StaticFileHandler : BaseHandler
+    {
+        public override bool HandlesRequest(HttpListenerRequest request)
+        {
+            return false;
+        }
+
+        private string _path;
+        public virtual string Path
+        {
+            get
+            {
+                if (!string.IsNullOrWhiteSpace(_path))
+                {
+                    return _path;
+                }
+
+                return QueryString["path"];
+            }
+            set
+            {
+                _path = value;
+            }
+        }
+
+        private Stream SourceStream { get; set; }
+
+        protected override bool SupportsByteRangeRequests
+        {
+            get
+            {
+                return true;
+            }
+        }
+
+        private bool ShouldCompressResponse(string contentType)
+        {
+            // Can't compress these
+            if (IsRangeRequest)
+            {
+                return false;
+            }
+
+            // Don't compress media
+            if (contentType.StartsWith("audio/", StringComparison.OrdinalIgnoreCase) || contentType.StartsWith("video/", StringComparison.OrdinalIgnoreCase))
+            {
+                return false;
+            }
+
+            // It will take some work to support compression within this handler
+            return false;
+        }
+
+        protected override long? GetTotalContentLength()
+        {
+            return SourceStream.Length;
+        }
+
+        protected override Task<ResponseInfo> GetResponseInfo()
+        {
+            ResponseInfo info = new ResponseInfo
+            {
+                ContentType = MimeTypes.GetMimeType(Path),
+            };
+
+            try
+            {
+                SourceStream = File.OpenRead(Path);
+            }
+            catch (FileNotFoundException ex)
+            {
+                info.StatusCode = 404;
+                Logger.LogException(ex);
+            }
+            catch (DirectoryNotFoundException ex)
+            {
+                info.StatusCode = 404;
+                Logger.LogException(ex);
+            }
+            catch (UnauthorizedAccessException ex)
+            {
+                info.StatusCode = 403;
+                Logger.LogException(ex);
+            }
+
+            info.CompressResponse = ShouldCompressResponse(info.ContentType);
+
+            if (SourceStream != null)
+            {
+                info.DateLastModified = File.GetLastWriteTimeUtc(Path);
+            }
+
+            return Task.FromResult<ResponseInfo>(info);
+        }
+
+        protected override Task WriteResponseToOutputStream(Stream stream)
+        {
+            if (IsRangeRequest)
+            {
+                KeyValuePair<long, long?> requestedRange = RequestedRanges.First();
+
+                // If the requested range is "0-" and we know the total length, we can optimize by avoiding having to buffer the content into memory
+                if (requestedRange.Value == null && TotalContentLength != null)
+                {
+                    return ServeCompleteRangeRequest(requestedRange, stream);
+                }
+                if (TotalContentLength.HasValue)
+                {
+                    // This will have to buffer a portion of the content into memory
+                    return ServePartialRangeRequestWithKnownTotalContentLength(requestedRange, stream);
+                }
+
+                // This will have to buffer the entire content into memory
+                return ServePartialRangeRequestWithUnknownTotalContentLength(requestedRange, stream);
+            }
+
+            return SourceStream.CopyToAsync(stream);
+        }
+
+        protected override void DisposeResponseStream()
+        {
+            base.DisposeResponseStream();
+
+            if (SourceStream != null)
+            {
+                SourceStream.Dispose();
+            }
+        }
+
+        /// <summary>
+        /// Handles a range request of "bytes=0-"
+        /// This will serve the complete content and add the content-range header
+        /// </summary>
+        private Task ServeCompleteRangeRequest(KeyValuePair<long, long?> requestedRange, Stream responseStream)
+        {
+            long totalContentLength = TotalContentLength.Value;
+
+            long rangeStart = requestedRange.Key;
+            long rangeEnd = totalContentLength - 1;
+            long rangeLength = 1 + rangeEnd - rangeStart;
+
+            // Content-Length is the length of what we're serving, not the original content
+            HttpListenerContext.Response.ContentLength64 = rangeLength;
+            HttpListenerContext.Response.Headers["Content-Range"] = string.Format("bytes {0}-{1}/{2}", rangeStart, rangeEnd, totalContentLength);
+
+            if (rangeStart > 0)
+            {
+                SourceStream.Position = rangeStart;
+            }
+
+            return SourceStream.CopyToAsync(responseStream);
+        }
+
+        /// <summary>
+        /// Serves a partial range request where the total content length is not known
+        /// </summary>
+        private async Task ServePartialRangeRequestWithUnknownTotalContentLength(KeyValuePair<long, long?> requestedRange, Stream responseStream)
+        {
+            // Read the entire stream so that we can determine the length
+            byte[] bytes = await ReadBytes(SourceStream, 0, null).ConfigureAwait(false);
+
+            long totalContentLength = bytes.LongLength;
+
+            long rangeStart = requestedRange.Key;
+            long rangeEnd = requestedRange.Value ?? (totalContentLength - 1);
+            long rangeLength = 1 + rangeEnd - rangeStart;
+
+            // Content-Length is the length of what we're serving, not the original content
+            HttpListenerContext.Response.ContentLength64 = rangeLength;
+            HttpListenerContext.Response.Headers["Content-Range"] = string.Format("bytes {0}-{1}/{2}", rangeStart, rangeEnd, totalContentLength);
+
+            await responseStream.WriteAsync(bytes, Convert.ToInt32(rangeStart), Convert.ToInt32(rangeLength)).ConfigureAwait(false);
+        }
+
+        /// <summary>
+        /// Serves a partial range request where the total content length is already known
+        /// </summary>
+        private async Task ServePartialRangeRequestWithKnownTotalContentLength(KeyValuePair<long, long?> requestedRange, Stream responseStream)
+        {
+            long totalContentLength = TotalContentLength.Value;
+            long rangeStart = requestedRange.Key;
+            long rangeEnd = requestedRange.Value ?? (totalContentLength - 1);
+            long rangeLength = 1 + rangeEnd - rangeStart;
+
+            // Only read the bytes we need
+            byte[] bytes = await ReadBytes(SourceStream, Convert.ToInt32(rangeStart), Convert.ToInt32(rangeLength)).ConfigureAwait(false);
+
+            // Content-Length is the length of what we're serving, not the original content
+            HttpListenerContext.Response.ContentLength64 = rangeLength;
+
+            HttpListenerContext.Response.Headers["Content-Range"] = string.Format("bytes {0}-{1}/{2}", rangeStart, rangeEnd, totalContentLength);
+
+            await responseStream.WriteAsync(bytes, 0, Convert.ToInt32(rangeLength)).ConfigureAwait(false);
+        }
+
+        /// <summary>
+        /// Reads bytes from a stream
+        /// </summary>
+        /// <param name="input">The input stream</param>
+        /// <param name="start">The starting position</param>
+        /// <param name="count">The number of bytes to read, or null to read to the end.</param>
+        private async Task<byte[]> ReadBytes(Stream input, int start, int? count)
+        {
+            if (start > 0)
+            {
+                input.Position = start;
+            }
+
+            if (count == null)
+            {
+                var buffer = new byte[16 * 1024];
+
+                using (var ms = new MemoryStream())
+                {
+                    int read;
+                    while ((read = await input.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false)) > 0)
+                    {
+                        await ms.WriteAsync(buffer, 0, read).ConfigureAwait(false);
+                    }
+                    return ms.ToArray();
+                }
+            }
+            else
+            {
+                var buffer = new byte[count.Value];
+
+                using (var ms = new MemoryStream())
+                {
+                    int read = await input.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false);
+
+                    await ms.WriteAsync(buffer, 0, read).ConfigureAwait(false);
+
+                    return ms.ToArray();
+                }
+            }
+
+        }
+    }
+}

+ 40 - 0
MediaBrowser.Common/Net/HttpServer.cs

@@ -0,0 +1,40 @@
+using System;
+using System.Net;
+using System.Reactive.Linq;
+
+namespace MediaBrowser.Common.Net
+{
+    public class HttpServer : IObservable<HttpListenerContext>, IDisposable
+    {
+        private readonly HttpListener _listener;
+        private readonly IObservable<HttpListenerContext> _stream;
+
+        public HttpServer(string url)
+        {
+            _listener = new HttpListener();
+            _listener.Prefixes.Add(url);
+            _listener.Start();
+            _stream = ObservableHttpContext();
+        }
+
+        private IObservable<HttpListenerContext> ObservableHttpContext()
+        {
+            return Observable.Create<HttpListenerContext>(obs =>
+                                Observable.FromAsync(() => _listener.GetContextAsync())
+                                          .Subscribe(obs))
+                             .Repeat()
+                             .Retry()
+                             .Publish()
+                             .RefCount();
+        }
+        public void Dispose()
+        {
+            _listener.Stop();
+        }
+
+        public IDisposable Subscribe(IObserver<HttpListenerContext> observer)
+        {
+            return _stream.Subscribe(observer);
+        }
+    }
+}

+ 160 - 0
MediaBrowser.Common/Net/MimeTypes.cs

@@ -0,0 +1,160 @@
+using System;
+using System.IO;
+
+namespace MediaBrowser.Common.Net
+{
+    public static class MimeTypes
+    {
+        public static string JsonMimeType = "application/json";
+
+        public static string GetMimeType(string path)
+        {
+            var ext = Path.GetExtension(path);
+
+            // http://en.wikipedia.org/wiki/Internet_media_type
+            // Add more as needed
+
+            // Type video
+            if (ext.EndsWith("mpg", StringComparison.OrdinalIgnoreCase) || ext.EndsWith("mpeg", StringComparison.OrdinalIgnoreCase))
+            {
+                return "video/mpeg";
+            }
+            if (ext.EndsWith("mp4", StringComparison.OrdinalIgnoreCase))
+            {
+                return "video/mp4";
+            }
+            if (ext.EndsWith("ogv", StringComparison.OrdinalIgnoreCase))
+            {
+                return "video/ogg";
+            }
+            if (ext.EndsWith("mov", StringComparison.OrdinalIgnoreCase))
+            {
+                return "video/quicktime";
+            }
+            if (ext.EndsWith("webm", StringComparison.OrdinalIgnoreCase))
+            {
+                return "video/webm";
+            }
+            if (ext.EndsWith("mkv", StringComparison.OrdinalIgnoreCase))
+            {
+                return "video/x-matroska";
+            }
+            if (ext.EndsWith("wmv", StringComparison.OrdinalIgnoreCase))
+            {
+                return "video/x-ms-wmv";
+            }
+            if (ext.EndsWith("flv", StringComparison.OrdinalIgnoreCase))
+            {
+                return "video/x-flv";
+            }
+            if (ext.EndsWith("avi", StringComparison.OrdinalIgnoreCase))
+            {
+                return "video/avi";
+            }
+            if (ext.EndsWith("m4v", StringComparison.OrdinalIgnoreCase))
+            {
+                return "video/x-m4v";
+            }
+            if (ext.EndsWith("asf", StringComparison.OrdinalIgnoreCase))
+            {
+                return "video/x-ms-asf";
+            }
+            if (ext.EndsWith("3gp", StringComparison.OrdinalIgnoreCase))
+            {
+                return "video/3gpp";
+            }
+            if (ext.EndsWith("3g2", StringComparison.OrdinalIgnoreCase))
+            {
+                return "video/3gpp2";
+            }
+            if (ext.EndsWith("ts", StringComparison.OrdinalIgnoreCase))
+            {
+                return "video/mp2t";
+            }
+
+            // Type text
+            if (ext.EndsWith("css", StringComparison.OrdinalIgnoreCase))
+            {
+                return "text/css";
+            }
+            if (ext.EndsWith("csv", StringComparison.OrdinalIgnoreCase))
+            {
+                return "text/csv";
+            }
+            if (ext.EndsWith("html", StringComparison.OrdinalIgnoreCase) || ext.EndsWith("html", StringComparison.OrdinalIgnoreCase))
+            {
+                return "text/html";
+            }
+            if (ext.EndsWith("txt", StringComparison.OrdinalIgnoreCase))
+            {
+                return "text/plain";
+            }
+
+            // Type image
+            if (ext.EndsWith("gif", StringComparison.OrdinalIgnoreCase))
+            {
+                return "image/gif";
+            }
+            if (ext.EndsWith("jpg", StringComparison.OrdinalIgnoreCase) || ext.EndsWith("jpeg", StringComparison.OrdinalIgnoreCase))
+            {
+                return "image/jpeg";
+            }
+            if (ext.EndsWith("png", StringComparison.OrdinalIgnoreCase))
+            {
+                return "image/png";
+            }
+            if (ext.EndsWith("ico", StringComparison.OrdinalIgnoreCase))
+            {
+                return "image/vnd.microsoft.icon";
+            }
+
+             // Type audio
+            if (ext.EndsWith("mp3", StringComparison.OrdinalIgnoreCase))
+            {
+                return "audio/mpeg";
+            }
+            if (ext.EndsWith("m4a", StringComparison.OrdinalIgnoreCase) || ext.EndsWith("aac", StringComparison.OrdinalIgnoreCase))
+            {
+                return "audio/mp4";
+            }
+            if (ext.EndsWith("webma", StringComparison.OrdinalIgnoreCase))
+            {
+                return "audio/webm";
+            }
+            if (ext.EndsWith("wav", StringComparison.OrdinalIgnoreCase))
+            {
+                return "audio/wav";
+            }
+            if (ext.EndsWith("wma", StringComparison.OrdinalIgnoreCase))
+            {
+                return "audio/x-ms-wma";
+            }
+            if (ext.EndsWith("flac", StringComparison.OrdinalIgnoreCase))
+            {
+                return "audio/flac";
+            }
+            if (ext.EndsWith("aac", StringComparison.OrdinalIgnoreCase))
+            {
+                return "audio/x-aac";
+            }
+            if (ext.EndsWith("ogg", StringComparison.OrdinalIgnoreCase) || ext.EndsWith("oga", StringComparison.OrdinalIgnoreCase))
+            {
+                return "audio/ogg";
+            }
+
+            // Playlists
+            if (ext.EndsWith("m3u8", StringComparison.OrdinalIgnoreCase))
+            {
+                return "application/x-mpegURL";
+            }
+
+            // Misc
+            if (ext.EndsWith("dll", StringComparison.OrdinalIgnoreCase))
+            {
+                return "application/x-msdownload";
+            }
+
+            throw new InvalidOperationException("Argument not supported: " + path);
+        }
+    }
+}

+ 18 - 0
MediaBrowser.Common/Net/Request.cs

@@ -0,0 +1,18 @@
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+
+namespace MediaBrowser.Common.Net
+{
+    public class Request
+    {
+        public string HttpMethod { get; set; }
+        public IDictionary<string, IEnumerable<string>> Headers { get; set; }
+        public Stream InputStream { get; set; }
+        public string RawUrl { get; set; }
+        public int ContentLength
+        {
+            get { return int.Parse(Headers["Content-Length"].First()); }
+        }
+    }
+}

+ 247 - 0
MediaBrowser.Common/Plugins/BasePlugin.cs

@@ -0,0 +1,247 @@
+using MediaBrowser.Common.Kernel;
+using MediaBrowser.Common.Logging;
+using MediaBrowser.Common.Serialization;
+using MediaBrowser.Model.Plugins;
+using System;
+using System.IO;
+using System.Reflection;
+
+namespace MediaBrowser.Common.Plugins
+{
+    /// <summary>
+    /// Provides a common base class for all plugins
+    /// </summary>
+    public abstract class BasePlugin : IDisposable
+    {
+        protected IKernel Kernel { get; private set; }
+
+        /// <summary>
+        /// Gets or sets the plugin's current context
+        /// </summary>
+        protected KernelContext Context { get { return Kernel.KernelContext; } }
+
+        /// <summary>
+        /// Gets the name of the plugin
+        /// </summary>
+        public abstract string Name { get; }
+
+        /// <summary>
+        /// Gets the type of configuration this plugin uses
+        /// </summary>
+        public virtual Type ConfigurationType
+        {
+            get { return typeof (BasePluginConfiguration); }
+        }
+
+        /// <summary>
+        /// Gets the plugin version
+        /// </summary>
+        public Version Version
+        {
+            get
+            {
+                return GetType().Assembly.GetName().Version;
+            }
+        }
+
+        /// <summary>
+        /// Gets the name the assembly file
+        /// </summary>
+        public string AssemblyFileName
+        {
+            get
+            {
+                return GetType().Assembly.GetName().Name + ".dll";
+            }
+        }
+
+        private DateTime? _configurationDateLastModified;
+        public DateTime ConfigurationDateLastModified
+        {
+            get
+            {
+                if (_configurationDateLastModified == null)
+                {
+                    if (File.Exists(ConfigurationFilePath))
+                    {
+                        _configurationDateLastModified = File.GetLastWriteTimeUtc(ConfigurationFilePath);
+                    }
+                }
+
+                return _configurationDateLastModified ?? DateTime.MinValue;
+            }
+        }
+
+        /// <summary>
+        /// Gets the path to the assembly file
+        /// </summary>
+        public string AssemblyFilePath
+        {
+            get
+            {
+                return Path.Combine(Kernel.ApplicationPaths.PluginsPath, AssemblyFileName);
+            }
+        }
+
+        /// <summary>
+        /// Gets or sets the current plugin configuration
+        /// </summary>
+        public BasePluginConfiguration Configuration { get; protected set; }
+
+        /// <summary>
+        /// Gets the name of the configuration file. Subclasses should override
+        /// </summary>
+        public virtual string ConfigurationFileName
+        {
+            get
+            {
+                return Name.Replace(" ", string.Empty) + ".xml";
+            }
+        }
+
+        /// <summary>
+        /// Gets the full path to the configuration file
+        /// </summary>
+        public string ConfigurationFilePath
+        {
+            get
+            {
+                return Path.Combine(Kernel.ApplicationPaths.PluginConfigurationsPath, ConfigurationFileName);
+            }
+        }
+
+        private string _dataFolderPath;
+        /// <summary>
+        /// Gets the full path to the data folder, where the plugin can store any miscellaneous files needed
+        /// </summary>
+        public string DataFolderPath
+        {
+            get
+            {
+                if (_dataFolderPath == null)
+                {
+                    // Give the folder name the same name as the config file name
+                    // We can always make this configurable if/when needed
+                    _dataFolderPath = Path.Combine(Kernel.ApplicationPaths.PluginsPath, Path.GetFileNameWithoutExtension(ConfigurationFileName));
+
+                    if (!Directory.Exists(_dataFolderPath))
+                    {
+                        Directory.CreateDirectory(_dataFolderPath);
+                    }
+                }
+
+                return _dataFolderPath;
+            }
+        }
+
+        public bool Enabled
+        {
+            get
+            {
+                return Configuration.Enabled;
+            }
+        }
+
+        /// <summary>
+        /// Returns true or false indicating if the plugin should be downloaded and run within the Ui.
+        /// </summary>
+        public virtual bool DownloadToUi
+        {
+            get
+            {
+                return false;
+            }
+        }
+
+        public void Initialize(IKernel kernel)
+        {
+            Initialize(kernel, true);
+        }
+
+        /// <summary>
+        /// Starts the plugin.
+        /// </summary>
+        public void Initialize(IKernel kernel, bool loadFeatures)
+        {
+            Kernel = kernel;
+
+            if (loadFeatures)
+            {
+                ReloadConfiguration();
+
+                if (Enabled)
+                {
+                    if (kernel.KernelContext == KernelContext.Server)
+                    {
+                        InitializeOnServer();
+                    }
+                    else if (kernel.KernelContext == KernelContext.Ui)
+                    {
+                        InitializeInUi();
+                    }
+                }
+            }
+        }
+
+        /// <summary>
+        /// Starts the plugin on the server
+        /// </summary>
+        protected virtual void InitializeOnServer()
+        {
+        }
+
+        /// <summary>
+        /// Starts the plugin in the Ui
+        /// </summary>
+        protected virtual void InitializeInUi()
+        {
+        }
+
+        /// <summary>
+        /// Disposes the plugins. Undos all actions performed during Init.
+        /// </summary>
+        public void Dispose()
+        {
+            Logger.LogInfo("Disposing {0} Plugin", Name);
+
+            if (Context == KernelContext.Server)
+            {
+                DisposeOnServer();
+            }
+            else if (Context == KernelContext.Ui)
+            {
+                InitializeInUi();
+            }
+        }
+
+        /// <summary>
+        /// Disposes the plugin on the server
+        /// </summary>
+        protected virtual void DisposeOnServer()
+        {
+        }
+
+        /// <summary>
+        /// Disposes the plugin in the Ui
+        /// </summary>
+        protected virtual void DisposeInUi()
+        {
+        }
+
+        public void ReloadConfiguration()
+        {
+            if (!File.Exists(ConfigurationFilePath))
+            {
+                Configuration = Activator.CreateInstance(ConfigurationType) as BasePluginConfiguration;
+                XmlSerializer.SerializeToFile(Configuration, ConfigurationFilePath);
+            }
+            else
+            {
+                Configuration = XmlSerializer.DeserializeFromFile(ConfigurationType, ConfigurationFilePath) as BasePluginConfiguration;
+            }
+
+            // Reset this so it will be loaded again next time it's accessed
+            _configurationDateLastModified = null;
+        }
+    }
+}

+ 78 - 0
MediaBrowser.Common/Plugins/BaseTheme.cs

@@ -0,0 +1,78 @@
+using MediaBrowser.Common.Mef;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.Composition;
+using System.ComponentModel.Composition.Hosting;
+using System.ComponentModel.Composition.Primitives;
+using System.Windows;
+using System.Windows.Controls;
+
+namespace MediaBrowser.Common.Plugins
+{
+    public abstract class BaseTheme : BasePlugin
+    {
+        public sealed override bool DownloadToUi
+        {
+            get
+            {
+                return true;
+            }
+        }
+
+        /// <summary>
+        /// Gets the MEF CompositionContainer
+        /// </summary>
+        private CompositionContainer CompositionContainer { get; set; }
+
+        /// <summary>
+        /// Gets the list of global resources
+        /// </summary>
+        [ImportMany(typeof(ResourceDictionary))]
+        public IEnumerable<ResourceDictionary> GlobalResources { get; private set; }
+
+        /// <summary>
+        /// Gets the list of pages
+        /// </summary>
+        [ImportMany(typeof(Page))]
+        public IEnumerable<Page> Pages { get; private set; }
+
+        /// <summary>
+        /// Gets the pack Uri of the Login page
+        /// </summary>
+        public abstract Uri LoginPageUri { get; }
+
+        protected override void InitializeInUi()
+        {
+            base.InitializeInUi();
+
+            ComposeParts();
+        }
+
+        private void ComposeParts()
+        {
+            var catalog = new AssemblyCatalog(GetType().Assembly);
+
+            CompositionContainer = MefUtils.GetSafeCompositionContainer(new ComposablePartCatalog[] { catalog });
+
+            CompositionContainer.ComposeParts(this);
+
+            CompositionContainer.Catalog.Dispose();
+        }
+
+        protected override void DisposeInUi()
+        {
+            base.DisposeInUi();
+
+            CompositionContainer.Dispose();
+        }
+
+        protected Uri GeneratePackUri(string relativePath)
+        {
+            string assemblyName = GetType().Assembly.GetName().Name;
+
+            string uri = string.Format("pack://application:,,,/{0};component/{1}", assemblyName, relativePath);
+
+            return new Uri(uri, UriKind.Absolute);
+        }
+    }
+}

+ 35 - 0
MediaBrowser.Common/Properties/AssemblyInfo.cs

@@ -0,0 +1,35 @@
+using System.Reflection;
+using System.Runtime.InteropServices;
+
+// General Information about an assembly is controlled through the following 
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: AssemblyTitle("MediaBrowser.Common")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("MediaBrowser.Common")]
+[assembly: AssemblyCopyright("Copyright ©  2012")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// Setting ComVisible to false makes the types in this assembly not visible 
+// to COM components.  If you need to access a type in this assembly from 
+// COM, set the ComVisible attribute to true on that type.
+[assembly: ComVisible(false)]
+
+// The following GUID is for the ID of the typelib if this project is exposed to COM
+[assembly: Guid("cdec1bb7-6ffd-409f-b41f-0524a73df9be")]
+
+// Version information for an assembly consists of the following four values:
+//
+//      Major Version
+//      Minor Version 
+//      Build Number
+//      Revision
+//
+// You can specify all the values or you can default the Build and Revision Numbers 
+// by using the '*' as shown below:
+// [assembly: AssemblyVersion("1.0.*")]
+[assembly: AssemblyVersion("1.0.0.0")]
+[assembly: AssemblyFileVersion("1.0.0.0")]

+ 63 - 0
MediaBrowser.Common/Properties/Resources.Designer.cs

@@ -0,0 +1,63 @@
+//------------------------------------------------------------------------------
+// <auto-generated>
+//     This code was generated by a tool.
+//     Runtime Version:4.0.30319.17929
+//
+//     Changes to this file may cause incorrect behavior and will be lost if
+//     the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace MediaBrowser.Common.Properties {
+    using System;
+    
+    
+    /// <summary>
+    ///   A strongly-typed resource class, for looking up localized strings, etc.
+    /// </summary>
+    // This class was auto-generated by the StronglyTypedResourceBuilder
+    // class via a tool like ResGen or Visual Studio.
+    // To add or remove a member, edit your .ResX file then rerun ResGen
+    // with the /str option, or rebuild your VS project.
+    [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
+    [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+    [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+    internal class Resources {
+        
+        private static global::System.Resources.ResourceManager resourceMan;
+        
+        private static global::System.Globalization.CultureInfo resourceCulture;
+        
+        [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+        internal Resources() {
+        }
+        
+        /// <summary>
+        ///   Returns the cached ResourceManager instance used by this class.
+        /// </summary>
+        [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+        internal static global::System.Resources.ResourceManager ResourceManager {
+            get {
+                if (object.ReferenceEquals(resourceMan, null)) {
+                    global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("MediaBrowser.Common.Properties.Resources", typeof(Resources).Assembly);
+                    resourceMan = temp;
+                }
+                return resourceMan;
+            }
+        }
+        
+        /// <summary>
+        ///   Overrides the current thread's CurrentUICulture property for all
+        ///   resource lookups using this strongly typed resource class.
+        /// </summary>
+        [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+        internal static global::System.Globalization.CultureInfo Culture {
+            get {
+                return resourceCulture;
+            }
+            set {
+                resourceCulture = value;
+            }
+        }
+    }
+}

+ 121 - 0
MediaBrowser.Common/Properties/Resources.resx

@@ -0,0 +1,121 @@
+<?xml version="1.0" encoding="utf-8"?>
+<root>
+  <!-- 
+    Microsoft ResX Schema 
+    
+    Version 2.0
+    
+    The primary goals of this format is to allow a simple XML format 
+    that is mostly human readable. The generation and parsing of the 
+    various data types are done through the TypeConverter classes 
+    associated with the data types.
+    
+    Example:
+    
+    ... ado.net/XML headers & schema ...
+    <resheader name="resmimetype">text/microsoft-resx</resheader>
+    <resheader name="version">2.0</resheader>
+    <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+    <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+    <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+    <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+    <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+        <value>[base64 mime encoded serialized .NET Framework object]</value>
+    </data>
+    <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+        <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+        <comment>This is a comment</comment>
+    </data>
+                
+    There are any number of "resheader" rows that contain simple 
+    name/value pairs.
+    
+    Each data row contains a name, and value. The row also contains a 
+    type or mimetype. Type corresponds to a .NET class that support 
+    text/value conversion through the TypeConverter architecture. 
+    Classes that don't support this are serialized and stored with the 
+    mimetype set.
+    
+    The mimetype is used for serialized objects, and tells the 
+    ResXResourceReader how to depersist the object. This is currently not 
+    extensible. For a given mimetype the value must be set accordingly:
+    
+    Note - application/x-microsoft.net.object.binary.base64 is the format 
+    that the ResXResourceWriter will generate, however the reader can 
+    read any of the formats listed below.
+    
+    mimetype: application/x-microsoft.net.object.binary.base64
+    value   : The object must be serialized with 
+            : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+            : and then encoded with base64 encoding.
+    
+    mimetype: application/x-microsoft.net.object.soap.base64
+    value   : The object must be serialized with 
+            : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+            : and then encoded with base64 encoding.
+
+    mimetype: application/x-microsoft.net.object.bytearray.base64
+    value   : The object must be serialized into a byte array 
+            : using a System.ComponentModel.TypeConverter
+            : and then encoded with base64 encoding.
+    -->
+  <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+    <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+    <xsd:element name="root" msdata:IsDataSet="true">
+      <xsd:complexType>
+        <xsd:choice maxOccurs="unbounded">
+          <xsd:element name="metadata">
+            <xsd:complexType>
+              <xsd:sequence>
+                <xsd:element name="value" type="xsd:string" minOccurs="0" />
+              </xsd:sequence>
+              <xsd:attribute name="name" use="required" type="xsd:string" />
+              <xsd:attribute name="type" type="xsd:string" />
+              <xsd:attribute name="mimetype" type="xsd:string" />
+              <xsd:attribute ref="xml:space" />
+            </xsd:complexType>
+          </xsd:element>
+          <xsd:element name="assembly">
+            <xsd:complexType>
+              <xsd:attribute name="alias" type="xsd:string" />
+              <xsd:attribute name="name" type="xsd:string" />
+            </xsd:complexType>
+          </xsd:element>
+          <xsd:element name="data">
+            <xsd:complexType>
+              <xsd:sequence>
+                <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+                <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+              </xsd:sequence>
+              <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+              <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+              <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+              <xsd:attribute ref="xml:space" />
+            </xsd:complexType>
+          </xsd:element>
+          <xsd:element name="resheader">
+            <xsd:complexType>
+              <xsd:sequence>
+                <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+              </xsd:sequence>
+              <xsd:attribute name="name" type="xsd:string" use="required" />
+            </xsd:complexType>
+          </xsd:element>
+        </xsd:choice>
+      </xsd:complexType>
+    </xsd:element>
+  </xsd:schema>
+  <resheader name="resmimetype">
+    <value>text/microsoft-resx</value>
+  </resheader>
+  <resheader name="version">
+    <value>2.0</value>
+  </resheader>
+  <resheader name="reader">
+    <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+  </resheader>
+  <resheader name="writer">
+    <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+  </resheader>
+  <assembly alias="System.Windows.Forms" name="System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
+</root>

二进制
MediaBrowser.Common/Resources/Images/Icon.ico


二进制
MediaBrowser.Common/Resources/Images/mblogoblack.png


二进制
MediaBrowser.Common/Resources/Images/mblogowhite.png


二进制
MediaBrowser.Common/Resources/Images/spinner.gif


+ 74 - 0
MediaBrowser.Common/Serialization/JsonSerializer.cs

@@ -0,0 +1,74 @@
+using System;
+using System.IO;
+
+namespace MediaBrowser.Common.Serialization
+{
+    /// <summary>
+    /// Provides a wrapper around third party json serialization.
+    /// </summary>
+    public class JsonSerializer
+    {
+        public static void SerializeToStream<T>(T obj, Stream stream)
+        {
+            Configure();
+
+            ServiceStack.Text.JsonSerializer.SerializeToStream(obj, stream);
+        }
+
+        public static void SerializeToFile<T>(T obj, string file)
+        {
+            Configure();
+
+            using (Stream stream = File.Open(file, FileMode.Create))
+            {
+                ServiceStack.Text.JsonSerializer.SerializeToStream(obj, stream);
+            }
+        }
+
+        public static object DeserializeFromFile(Type type, string file)
+        {
+            Configure();
+
+            using (Stream stream = File.OpenRead(file))
+            {
+                return ServiceStack.Text.JsonSerializer.DeserializeFromStream(type, stream);
+            }
+        }
+
+        public static T DeserializeFromFile<T>(string file)
+        {
+            Configure();
+
+            using (Stream stream = File.OpenRead(file))
+            {
+                return ServiceStack.Text.JsonSerializer.DeserializeFromStream<T>(stream);
+            }
+        }
+
+        public static T DeserializeFromStream<T>(Stream stream)
+        {
+            Configure();
+
+            return ServiceStack.Text.JsonSerializer.DeserializeFromStream<T>(stream);
+        }
+
+        public static object DeserializeFromStream(Stream stream, Type type)
+        {
+            Configure();
+
+            return ServiceStack.Text.JsonSerializer.DeserializeFromStream(type, stream);
+        }
+
+        private static bool _isConfigured;
+        private static void Configure()
+        {
+            if (!_isConfigured)
+            {
+                ServiceStack.Text.JsConfig.DateHandler = ServiceStack.Text.JsonDateHandler.ISO8601;
+                ServiceStack.Text.JsConfig.ExcludeTypeInfo = true;
+                ServiceStack.Text.JsConfig.IncludeNullValues = false;
+                _isConfigured = true;
+            }
+        }
+    }
+}

+ 44 - 0
MediaBrowser.Common/Serialization/JsvSerializer.cs

@@ -0,0 +1,44 @@
+using System;
+using System.IO;
+
+namespace MediaBrowser.Common.Serialization
+{
+    /// <summary>
+    /// This adds support for ServiceStack's proprietary JSV output format.
+    /// It's a hybrid of Json and Csv but the serializer performs about 25% faster and output runs about 10% smaller
+    /// http://www.servicestack.net/benchmarks/NorthwindDatabaseRowsSerialization.100000-times.2010-08-17.html
+    /// </summary>
+    public static class JsvSerializer
+    {
+        public static void SerializeToStream<T>(T obj, Stream stream)
+        {
+            ServiceStack.Text.TypeSerializer.SerializeToStream(obj, stream);
+        }
+
+        public static T DeserializeFromStream<T>(Stream stream)
+        {
+            return ServiceStack.Text.TypeSerializer.DeserializeFromStream<T>(stream);
+        }
+
+        public static object DeserializeFromStream(Stream stream, Type type)
+        {
+            return ServiceStack.Text.TypeSerializer.DeserializeFromStream(type, stream);
+        }
+
+        public static void SerializeToFile<T>(T obj, string file)
+        {
+            using (Stream stream = File.Open(file, FileMode.Create))
+            {
+                SerializeToStream(obj, stream);
+            }
+        }
+
+        public static T DeserializeFromFile<T>(string file)
+        {
+            using (Stream stream = File.OpenRead(file))
+            {
+                return DeserializeFromStream<T>(stream);
+            }
+        }
+    }
+}

+ 53 - 0
MediaBrowser.Common/Serialization/ProtobufSerializer.cs

@@ -0,0 +1,53 @@
+using System;
+using System.IO;
+
+namespace MediaBrowser.Common.Serialization
+{
+    /// <summary>
+    /// Protocol buffers is google's binary serialization format. This is a .NET implementation of it.
+    /// You have to tag your classes with some annoying attributes, but in return you get the fastest serialization around with the smallest possible output.
+    /// </summary>
+    public static class ProtobufSerializer
+    {
+        /// <summary>
+        /// This is an auto-generated Protobuf Serialization assembly for best performance.
+        /// It is created during the Model project's post-build event.
+        /// This means that this class can currently only handle types within the Model project.
+        /// If we need to, we can always add a param indicating whether or not the model serializer should be used.
+        /// </summary>
+        private static readonly ProtobufModelSerializer ProtobufModelSerializer = new ProtobufModelSerializer();
+
+        public static void SerializeToStream<T>(T obj, Stream stream)
+        {
+            ProtobufModelSerializer.Serialize(stream, obj);
+        }
+
+        public static T DeserializeFromStream<T>(Stream stream)
+            where T : class
+        {
+            return ProtobufModelSerializer.Deserialize(stream, null, typeof(T)) as T;
+        }
+
+        public static object DeserializeFromStream(Stream stream, Type type)
+        {
+            return ProtobufModelSerializer.Deserialize(stream, null, type);
+        }
+
+        public static void SerializeToFile<T>(T obj, string file)
+        {
+            using (Stream stream = File.Open(file, FileMode.Create))
+            {
+                SerializeToStream(obj, stream);
+            }
+        }
+
+        public static T DeserializeFromFile<T>(string file)
+            where T : class
+        {
+            using (Stream stream = File.OpenRead(file))
+            {
+                return DeserializeFromStream<T>(stream);
+            }
+        }
+    }
+}

+ 58 - 0
MediaBrowser.Common/Serialization/XmlSerializer.cs

@@ -0,0 +1,58 @@
+using System;
+using System.IO;
+
+namespace MediaBrowser.Common.Serialization
+{
+    /// <summary>
+    /// Provides a wrapper around third party xml serialization.
+    /// </summary>
+    public class XmlSerializer
+    {
+        public static void SerializeToStream<T>(T obj, Stream stream)
+        {
+            ServiceStack.Text.XmlSerializer.SerializeToStream(obj, stream);
+        }
+
+        public static T DeserializeFromStream<T>(Stream stream)
+        {
+            return ServiceStack.Text.XmlSerializer.DeserializeFromStream<T>(stream);
+        }
+
+        public static object DeserializeFromStream(Type type, Stream stream)
+        {
+            return ServiceStack.Text.XmlSerializer.DeserializeFromStream(type, stream);
+        }
+
+        public static void SerializeToFile<T>(T obj, string file)
+        {
+            using (var stream = new FileStream(file, FileMode.Create))
+            {
+                SerializeToStream(obj, stream);
+            }
+        }
+
+        public static T DeserializeFromFile<T>(string file)
+        {
+            using (Stream stream = File.OpenRead(file))
+            {
+                return DeserializeFromStream<T>(stream);
+            }
+        }
+
+        public static void SerializeToFile(object obj, string file)
+        {
+            using (var stream = new FileStream(file, FileMode.Create))
+            {
+                ServiceStack.Text.XmlSerializer.SerializeToStream(obj, stream);
+            }
+        }
+        
+        public static object DeserializeFromFile(Type type, string file)
+        {
+            using (Stream stream = File.OpenRead(file))
+            {
+                return DeserializeFromStream(type, stream);
+            }
+        }
+    }
+}

+ 123 - 0
MediaBrowser.Common/UI/BaseApplication.cs

@@ -0,0 +1,123 @@
+using MediaBrowser.Common.Kernel;
+using MediaBrowser.Common.Logging;
+using MediaBrowser.Model.Progress;
+using Microsoft.Shell;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Windows;
+
+namespace MediaBrowser.Common.UI
+{
+    /// <summary>
+    /// Serves as a base Application class for both the UI and Server apps.
+    /// </summary>
+    public abstract class BaseApplication : Application, INotifyPropertyChanged, ISingleInstanceApp
+    {
+        private IKernel Kernel { get; set; }
+
+        protected abstract IKernel InstantiateKernel();
+        protected abstract Window InstantiateMainWindow();
+
+        public event PropertyChangedEventHandler PropertyChanged;
+
+        public void OnPropertyChanged(String info)
+        {
+            if (PropertyChanged != null)
+            {
+                PropertyChanged(this, new PropertyChangedEventArgs(info));
+            }
+        }
+
+        protected override void OnStartup(StartupEventArgs e)
+        {
+            // Without this the app will shutdown after the splash screen closes
+            ShutdownMode = ShutdownMode.OnExplicitShutdown;
+
+            LoadKernel();
+        }
+
+        private async void LoadKernel()
+        {
+            Kernel = InstantiateKernel();
+
+            var progress = new Progress<TaskProgress>();
+
+            var splash = new Splash(progress);
+
+            splash.Show();
+
+            try
+            {
+                DateTime now = DateTime.UtcNow;
+
+                await Kernel.Init(progress);
+
+                Logger.LogInfo("Kernel.Init completed in {0} seconds.", (DateTime.UtcNow - now).TotalSeconds);
+                splash.Close();
+
+                ShutdownMode = System.Windows.ShutdownMode.OnLastWindowClose;
+
+                OnKernelLoaded();
+
+                InstantiateMainWindow().Show();
+            }
+            catch (Exception ex)
+            {
+                Logger.LogException(ex);
+
+                MessageBox.Show("There was an error launching Media Browser: " + ex.Message);
+                splash.Close();
+
+                // Shutdown the app with an error code
+                Shutdown(1);
+            }
+        }
+
+        protected virtual void OnKernelLoaded()
+        {
+        }
+
+        protected override void OnExit(ExitEventArgs e)
+        {
+            base.OnExit(e);
+
+            Kernel.Dispose();
+        }
+
+        public bool SignalExternalCommandLineArgs(IList<string> args)
+        {
+            OnSecondInstanceLaunched(args);
+
+            return true;
+        }
+
+        protected virtual void OnSecondInstanceLaunched(IList<string> args)
+        {
+            if (this.MainWindow.WindowState == WindowState.Minimized)
+            {
+                this.MainWindow.WindowState = WindowState.Maximized;
+            }            
+        }
+
+        public static void RunApplication<TApplicationType>(string uniqueKey)
+            where TApplicationType : BaseApplication, IApplication, new()
+        {
+            if (SingleInstance<TApplicationType>.InitializeAsFirstInstance(uniqueKey))
+            {
+                var application = new TApplicationType();
+                application.InitializeComponent();
+
+                application.Run();
+
+                // Allow single instance code to perform cleanup operations
+                SingleInstance<TApplicationType>.Cleanup();
+            }
+        }
+    }
+
+    public interface IApplication
+    {
+        void InitializeComponent();
+    }
+}

+ 484 - 0
MediaBrowser.Common/UI/SingleInstance.cs

@@ -0,0 +1,484 @@
+//-----------------------------------------------------------------------
+// <copyright file="SingleInstance.cs" company="Microsoft">
+//     Copyright (c) Microsoft Corporation.  All rights reserved.
+// </copyright>
+// <summary>
+//     This class checks to make sure that only one instance of 
+//     this application is running at a time.
+// </summary>
+//-----------------------------------------------------------------------
+
+namespace Microsoft.Shell
+{
+    using System;
+    using System.Collections;
+    using System.Collections.Generic;
+    using System.ComponentModel;
+    using System.IO;
+    using System.Runtime.InteropServices;
+    using System.Runtime.Remoting;
+    using System.Runtime.Remoting.Channels;
+    using System.Runtime.Remoting.Channels.Ipc;
+    using System.Runtime.Serialization.Formatters;
+    using System.Security;
+    using System.Threading;
+    using System.Windows;
+    using System.Windows.Threading;
+
+    internal enum WM
+    {
+        NULL = 0x0000,
+        CREATE = 0x0001,
+        DESTROY = 0x0002,
+        MOVE = 0x0003,
+        SIZE = 0x0005,
+        ACTIVATE = 0x0006,
+        SETFOCUS = 0x0007,
+        KILLFOCUS = 0x0008,
+        ENABLE = 0x000A,
+        SETREDRAW = 0x000B,
+        SETTEXT = 0x000C,
+        GETTEXT = 0x000D,
+        GETTEXTLENGTH = 0x000E,
+        PAINT = 0x000F,
+        CLOSE = 0x0010,
+        QUERYENDSESSION = 0x0011,
+        QUIT = 0x0012,
+        QUERYOPEN = 0x0013,
+        ERASEBKGND = 0x0014,
+        SYSCOLORCHANGE = 0x0015,
+        SHOWWINDOW = 0x0018,
+        ACTIVATEAPP = 0x001C,
+        SETCURSOR = 0x0020,
+        MOUSEACTIVATE = 0x0021,
+        CHILDACTIVATE = 0x0022,
+        QUEUESYNC = 0x0023,
+        GETMINMAXINFO = 0x0024,
+
+        WINDOWPOSCHANGING = 0x0046,
+        WINDOWPOSCHANGED = 0x0047,
+
+        CONTEXTMENU = 0x007B,
+        STYLECHANGING = 0x007C,
+        STYLECHANGED = 0x007D,
+        DISPLAYCHANGE = 0x007E,
+        GETICON = 0x007F,
+        SETICON = 0x0080,
+        NCCREATE = 0x0081,
+        NCDESTROY = 0x0082,
+        NCCALCSIZE = 0x0083,
+        NCHITTEST = 0x0084,
+        NCPAINT = 0x0085,
+        NCACTIVATE = 0x0086,
+        GETDLGCODE = 0x0087,
+        SYNCPAINT = 0x0088,
+        NCMOUSEMOVE = 0x00A0,
+        NCLBUTTONDOWN = 0x00A1,
+        NCLBUTTONUP = 0x00A2,
+        NCLBUTTONDBLCLK = 0x00A3,
+        NCRBUTTONDOWN = 0x00A4,
+        NCRBUTTONUP = 0x00A5,
+        NCRBUTTONDBLCLK = 0x00A6,
+        NCMBUTTONDOWN = 0x00A7,
+        NCMBUTTONUP = 0x00A8,
+        NCMBUTTONDBLCLK = 0x00A9,
+
+        SYSKEYDOWN = 0x0104,
+        SYSKEYUP = 0x0105,
+        SYSCHAR = 0x0106,
+        SYSDEADCHAR = 0x0107,
+        COMMAND = 0x0111,
+        SYSCOMMAND = 0x0112,
+
+        MOUSEMOVE = 0x0200,
+        LBUTTONDOWN = 0x0201,
+        LBUTTONUP = 0x0202,
+        LBUTTONDBLCLK = 0x0203,
+        RBUTTONDOWN = 0x0204,
+        RBUTTONUP = 0x0205,
+        RBUTTONDBLCLK = 0x0206,
+        MBUTTONDOWN = 0x0207,
+        MBUTTONUP = 0x0208,
+        MBUTTONDBLCLK = 0x0209,
+        MOUSEWHEEL = 0x020A,
+        XBUTTONDOWN = 0x020B,
+        XBUTTONUP = 0x020C,
+        XBUTTONDBLCLK = 0x020D,
+        MOUSEHWHEEL = 0x020E,
+
+
+        CAPTURECHANGED = 0x0215,
+
+        ENTERSIZEMOVE = 0x0231,
+        EXITSIZEMOVE = 0x0232,
+
+        IME_SETCONTEXT = 0x0281,
+        IME_NOTIFY = 0x0282,
+        IME_CONTROL = 0x0283,
+        IME_COMPOSITIONFULL = 0x0284,
+        IME_SELECT = 0x0285,
+        IME_CHAR = 0x0286,
+        IME_REQUEST = 0x0288,
+        IME_KEYDOWN = 0x0290,
+        IME_KEYUP = 0x0291,
+
+        NCMOUSELEAVE = 0x02A2,
+
+        DWMCOMPOSITIONCHANGED = 0x031E,
+        DWMNCRENDERINGCHANGED = 0x031F,
+        DWMCOLORIZATIONCOLORCHANGED = 0x0320,
+        DWMWINDOWMAXIMIZEDCHANGE = 0x0321,
+
+        #region Windows 7
+        DWMSENDICONICTHUMBNAIL = 0x0323,
+        DWMSENDICONICLIVEPREVIEWBITMAP = 0x0326,
+        #endregion
+
+        USER = 0x0400,
+
+        // This is the hard-coded message value used by WinForms for Shell_NotifyIcon.
+        // It's relatively safe to reuse.
+        TRAYMOUSEMESSAGE = 0x800, //WM_USER + 1024
+        APP = 0x8000,
+    }
+
+    [SuppressUnmanagedCodeSecurity]
+    internal static class NativeMethods
+    {
+        /// <summary>
+        /// Delegate declaration that matches WndProc signatures.
+        /// </summary>
+        public delegate IntPtr MessageHandler(WM uMsg, IntPtr wParam, IntPtr lParam, out bool handled);
+
+        [DllImport("shell32.dll", EntryPoint = "CommandLineToArgvW", CharSet = CharSet.Unicode)]
+        private static extern IntPtr _CommandLineToArgvW([MarshalAs(UnmanagedType.LPWStr)] string cmdLine, out int numArgs);
+
+
+        [DllImport("kernel32.dll", EntryPoint = "LocalFree", SetLastError = true)]
+        private static extern IntPtr _LocalFree(IntPtr hMem);
+
+
+        public static string[] CommandLineToArgvW(string cmdLine)
+        {
+            IntPtr argv = IntPtr.Zero;
+            try
+            {
+                int numArgs = 0;
+
+                argv = _CommandLineToArgvW(cmdLine, out numArgs);
+                if (argv == IntPtr.Zero)
+                {
+                    throw new Win32Exception();
+                }
+                var result = new string[numArgs];
+
+                for (int i = 0; i < numArgs; i++)
+                {
+                    IntPtr currArg = Marshal.ReadIntPtr(argv, i * Marshal.SizeOf(typeof(IntPtr)));
+                    result[i] = Marshal.PtrToStringUni(currArg);
+                }
+
+                return result;
+            }
+            finally
+            {
+
+                _LocalFree(argv);
+                // Otherwise LocalFree failed.
+                // Assert.AreEqual(IntPtr.Zero, p);
+            }
+        }
+
+    }
+
+    public interface ISingleInstanceApp
+    {
+        bool SignalExternalCommandLineArgs(IList<string> args);
+    }
+
+    /// <summary>
+    /// This class checks to make sure that only one instance of 
+    /// this application is running at a time.
+    /// </summary>
+    /// <remarks>
+    /// Note: this class should be used with some caution, because it does no
+    /// security checking. For example, if one instance of an app that uses this class
+    /// is running as Administrator, any other instance, even if it is not
+    /// running as Administrator, can activate it with command line arguments.
+    /// For most apps, this will not be much of an issue.
+    /// </remarks>
+    public static class SingleInstance<TApplication>
+                where TApplication : Application, ISingleInstanceApp
+    {
+        #region Private Fields
+
+        /// <summary>
+        /// String delimiter used in channel names.
+        /// </summary>
+        private const string Delimiter = ":";
+
+        /// <summary>
+        /// Suffix to the channel name.
+        /// </summary>
+        private const string ChannelNameSuffix = "SingeInstanceIPCChannel";
+
+        /// <summary>
+        /// Remote service name.
+        /// </summary>
+        private const string RemoteServiceName = "SingleInstanceApplicationService";
+
+        /// <summary>
+        /// IPC protocol used (string).
+        /// </summary>
+        private const string IpcProtocol = "ipc://";
+
+        /// <summary>
+        /// Application mutex.
+        /// </summary>
+        private static Mutex singleInstanceMutex;
+
+        /// <summary>
+        /// IPC channel for communications.
+        /// </summary>
+        private static IpcServerChannel channel;
+
+        /// <summary>
+        /// List of command line arguments for the application.
+        /// </summary>
+        private static IList<string> commandLineArgs;
+
+        #endregion
+
+        #region Public Properties
+
+        /// <summary>
+        /// Gets list of command line arguments for the application.
+        /// </summary>
+        public static IList<string> CommandLineArgs
+        {
+            get { return commandLineArgs; }
+        }
+
+        #endregion
+
+        #region Public Methods
+
+        /// <summary>
+        /// Checks if the instance of the application attempting to start is the first instance. 
+        /// If not, activates the first instance.
+        /// </summary>
+        /// <returns>True if this is the first instance of the application.</returns>
+        public static bool InitializeAsFirstInstance(string uniqueName)
+        {
+            commandLineArgs = GetCommandLineArgs(uniqueName);
+
+            // Build unique application Id and the IPC channel name.
+            string applicationIdentifier = uniqueName + Environment.UserName;
+
+            string channelName = String.Concat(applicationIdentifier, Delimiter, ChannelNameSuffix);
+
+            // Create mutex based on unique application Id to check if this is the first instance of the application. 
+            bool firstInstance;
+            singleInstanceMutex = new Mutex(true, applicationIdentifier, out firstInstance);
+            if (firstInstance)
+            {
+                CreateRemoteService(channelName);
+            }
+            else
+            {
+                SignalFirstInstance(channelName, commandLineArgs);
+            }
+
+            return firstInstance;
+        }
+
+        /// <summary>
+        /// Cleans up single-instance code, clearing shared resources, mutexes, etc.
+        /// </summary>
+        public static void Cleanup()
+        {
+            if (singleInstanceMutex != null)
+            {
+                singleInstanceMutex.Close();
+                singleInstanceMutex = null;
+            }
+
+            if (channel != null)
+            {
+                ChannelServices.UnregisterChannel(channel);
+                channel = null;
+            }
+        }
+
+        #endregion
+
+        #region Private Methods
+
+        /// <summary>
+        /// Gets command line args - for ClickOnce deployed applications, command line args may not be passed directly, they have to be retrieved.
+        /// </summary>
+        /// <returns>List of command line arg strings.</returns>
+        private static IList<string> GetCommandLineArgs(string uniqueApplicationName)
+        {
+            string[] args = null;
+            if (AppDomain.CurrentDomain.ActivationContext == null)
+            {
+                // The application was not clickonce deployed, get args from standard API's
+                args = Environment.GetCommandLineArgs();
+            }
+            else
+            {
+                // The application was clickonce deployed
+                // Clickonce deployed apps cannot recieve traditional commandline arguments
+                // As a workaround commandline arguments can be written to a shared location before 
+                // the app is launched and the app can obtain its commandline arguments from the 
+                // shared location               
+                string appFolderPath = Path.Combine(
+                    Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), uniqueApplicationName);
+
+                string cmdLinePath = Path.Combine(appFolderPath, "cmdline.txt");
+                if (File.Exists(cmdLinePath))
+                {
+                    try
+                    {
+                        using (TextReader reader = new StreamReader(cmdLinePath, System.Text.Encoding.Unicode))
+                        {
+                            args = NativeMethods.CommandLineToArgvW(reader.ReadToEnd());
+                        }
+
+                        File.Delete(cmdLinePath);
+                    }
+                    catch (IOException)
+                    {
+                    }
+                }
+            }
+
+            if (args == null)
+            {
+                args = new string[] { };
+            }
+
+            return new List<string>(args);
+        }
+
+        /// <summary>
+        /// Creates a remote service for communication.
+        /// </summary>
+        /// <param name="channelName">Application's IPC channel name.</param>
+        private static void CreateRemoteService(string channelName)
+        {
+            var serverProvider = new BinaryServerFormatterSinkProvider { };
+            serverProvider.TypeFilterLevel = TypeFilterLevel.Full;
+            IDictionary props = new Dictionary<string, string>();
+
+            props["name"] = channelName;
+            props["portName"] = channelName;
+            props["exclusiveAddressUse"] = "false";
+
+            // Create the IPC Server channel with the channel properties
+            channel = new IpcServerChannel(props, serverProvider);
+
+            // Register the channel with the channel services
+            ChannelServices.RegisterChannel(channel, true);
+
+            // Expose the remote service with the REMOTE_SERVICE_NAME
+            var remoteService = new IPCRemoteService();
+            RemotingServices.Marshal(remoteService, RemoteServiceName);
+        }
+
+        /// <summary>
+        /// Creates a client channel and obtains a reference to the remoting service exposed by the server - 
+        /// in this case, the remoting service exposed by the first instance. Calls a function of the remoting service 
+        /// class to pass on command line arguments from the second instance to the first and cause it to activate itself.
+        /// </summary>
+        /// <param name="channelName">Application's IPC channel name.</param>
+        /// <param name="args">
+        /// Command line arguments for the second instance, passed to the first instance to take appropriate action.
+        /// </param>
+        private static void SignalFirstInstance(string channelName, IList<string> args)
+        {
+            var secondInstanceChannel = new IpcClientChannel();
+            ChannelServices.RegisterChannel(secondInstanceChannel, true);
+
+            string remotingServiceUrl = IpcProtocol + channelName + "/" + RemoteServiceName;
+
+            // Obtain a reference to the remoting service exposed by the server i.e the first instance of the application
+            var firstInstanceRemoteServiceReference = (IPCRemoteService)RemotingServices.Connect(typeof(IPCRemoteService), remotingServiceUrl);
+
+            // Check that the remote service exists, in some cases the first instance may not yet have created one, in which case
+            // the second instance should just exit
+            if (firstInstanceRemoteServiceReference != null)
+            {
+                // Invoke a method of the remote service exposed by the first instance passing on the command line
+                // arguments and causing the first instance to activate itself
+                firstInstanceRemoteServiceReference.InvokeFirstInstance(args);
+            }
+        }
+
+        /// <summary>
+        /// Callback for activating first instance of the application.
+        /// </summary>
+        /// <param name="arg">Callback argument.</param>
+        /// <returns>Always null.</returns>
+        private static object ActivateFirstInstanceCallback(object arg)
+        {
+            // Get command line args to be passed to first instance
+            var args = arg as IList<string>;
+            ActivateFirstInstance(args);
+            return null;
+        }
+
+        /// <summary>
+        /// Activates the first instance of the application with arguments from a second instance.
+        /// </summary>
+        /// <param name="args">List of arguments to supply the first instance of the application.</param>
+        private static void ActivateFirstInstance(IList<string> args)
+        {
+            // Set main window state and process command line args
+            if (Application.Current == null)
+            {
+                return;
+            }
+
+            ((TApplication)Application.Current).SignalExternalCommandLineArgs(args);
+        }
+
+        #endregion
+
+        #region Private Classes
+
+        /// <summary>
+        /// Remoting service class which is exposed by the server i.e the first instance and called by the second instance
+        /// to pass on the command line arguments to the first instance and cause it to activate itself.
+        /// </summary>
+        private class IPCRemoteService : MarshalByRefObject
+        {
+            /// <summary>
+            /// Activates the first instance of the application.
+            /// </summary>
+            /// <param name="args">List of arguments to pass to the first instance.</param>
+            public void InvokeFirstInstance(IList<string> args)
+            {
+                if (Application.Current != null)
+                {
+                    // Do an asynchronous call to ActivateFirstInstance function
+                    Application.Current.Dispatcher.BeginInvoke(
+                        DispatcherPriority.Normal, new DispatcherOperationCallback(SingleInstance<TApplication>.ActivateFirstInstanceCallback), args);
+                }
+            }
+
+            /// <summary>
+            /// Remoting Object's ease expires after every 5 minutes by default. We need to override the InitializeLifetimeService class
+            /// to ensure that lease never expires.
+            /// </summary>
+            /// <returns>Always null.</returns>
+            public override object InitializeLifetimeService()
+            {
+                return null;
+            }
+        }
+
+        #endregion
+    }
+}

+ 33 - 0
MediaBrowser.Common/UI/Splash.xaml

@@ -0,0 +1,33 @@
+<Controls:MetroWindow x:Class="MediaBrowser.Common.UI.Splash"
+        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+        xmlns:Controls="clr-namespace:MahApps.Metro.Controls;assembly=MahApps.Metro"
+        Title="MediaBrowser" 
+        Height="230" 
+        Width="520" 
+        ShowInTaskbar="True" 
+        ResizeMode="NoResize" 
+        WindowStartupLocation="CenterScreen"
+        WindowState="Normal" 
+        FontSize="14">
+    <Window.Resources>
+        <ResourceDictionary>
+            <Style TargetType="{x:Type Controls:WindowCommands}">
+                <Setter Property="Visibility" Value="Hidden" />
+            </Style>
+        </ResourceDictionary>
+    </Window.Resources>
+    <Window.Background>
+        <RadialGradientBrush RadiusX=".75" RadiusY=".75">
+            <GradientStop Color="White" Offset="0.0"/>
+            <GradientStop Color="WhiteSmoke" Offset="0.65"/>
+            <GradientStop Color="#cfcfcf" Offset="1.0"/>
+        </RadialGradientBrush>
+    </Window.Background>
+    <Grid Name="splashGrid">
+        <Image x:Name="imgLogo" HorizontalAlignment="Left" VerticalAlignment="Top" Stretch="Uniform" Grid.Row="0" Margin="10 10 10 10" Source="../Resources/Images/mblogoblack.png"/>
+        <StackPanel Margin="0,130,10,0" VerticalAlignment="Center" HorizontalAlignment="Center" Grid.Row="2" Orientation="Horizontal">
+            <TextBlock Name="lblProgress" FontSize="18" Foreground="Black" Text="Label"></TextBlock>
+        </StackPanel>
+    </Grid>
+</Controls:MetroWindow>

+ 32 - 0
MediaBrowser.Common/UI/Splash.xaml.cs

@@ -0,0 +1,32 @@
+using MahApps.Metro.Controls;
+using MediaBrowser.Model.Progress;
+using System;
+using System.Windows;
+
+namespace MediaBrowser.Common.UI
+{
+    /// <summary>
+    /// Interaction logic for Splash.xaml
+    /// </summary>
+    public partial class Splash : MetroWindow
+    {
+        public Splash(Progress<TaskProgress> progress)
+        {
+            InitializeComponent();
+            
+            progress.ProgressChanged += ProgressChanged;
+            Loaded+=SplashLoaded;
+        }
+
+        void ProgressChanged(object sender, TaskProgress e)
+        {
+            lblProgress.Text = e.Description + "...";
+        }
+
+        private void SplashLoaded(object sender, RoutedEventArgs e)
+        {
+            // Setting this in markup throws an exception at runtime
+            ShowTitleBar = false;
+        }
+    }
+}

+ 15 - 0
MediaBrowser.Common/app.config

@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<configuration>
+  <runtime>
+    <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
+      <dependentAssembly>
+        <assemblyIdentity name="System.Reactive.Core" publicKeyToken="f300afd708cefcd3" culture="neutral" />
+        <bindingRedirect oldVersion="0.0.0.0-2.0.20823.0" newVersion="2.0.20823.0" />
+      </dependentAssembly>
+      <dependentAssembly>
+        <assemblyIdentity name="System.Reactive.Interfaces" publicKeyToken="f300afd708cefcd3" culture="neutral" />
+        <bindingRedirect oldVersion="0.0.0.0-2.0.20823.0" newVersion="2.0.20823.0" />
+      </dependentAssembly>
+    </assemblyBinding>
+  </runtime>
+</configuration>

+ 8 - 0
MediaBrowser.Common/packages.config

@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<packages>
+  <package id="MahApps.Metro" version="0.9.0.0" targetFramework="net45" />
+  <package id="Rx-Core" version="2.0.20823" targetFramework="net45" />
+  <package id="Rx-Interfaces" version="2.0.20823" targetFramework="net45" />
+  <package id="Rx-Linq" version="2.0.20823" targetFramework="net45" />
+  <package id="ServiceStack.Text" version="3.9.9" targetFramework="net45" />
+</packages>

+ 81 - 0
MediaBrowser.Controller/Drawing/DrawingUtils.cs

@@ -0,0 +1,81 @@
+using System;
+using System.Drawing;
+
+namespace MediaBrowser.Controller.Drawing
+{
+    public static class DrawingUtils
+    {
+        /// <summary>
+        /// Resizes a set of dimensions
+        /// </summary>
+        public static Size Resize(int currentWidth, int currentHeight, int? width, int? height, int? maxWidth, int? maxHeight)
+        {
+            return Resize(new Size(currentWidth, currentHeight), width, height, maxWidth, maxHeight);
+        }
+
+        /// <summary>
+        /// Resizes a set of dimensions
+        /// </summary>
+        /// <param name="size">The original size object</param>
+        /// <param name="width">A new fixed width, if desired</param>
+        /// <param name="height">A new fixed neight, if desired</param>
+        /// <param name="maxWidth">A max fixed width, if desired</param>
+        /// <param name="maxHeight">A max fixed height, if desired</param>
+        /// <returns>A new size object</returns>
+        public static Size Resize(Size size, int? width, int? height, int? maxWidth, int? maxHeight)
+        {
+            decimal newWidth = size.Width;
+            decimal newHeight = size.Height;
+
+            if (width.HasValue && height.HasValue)
+            {
+                newWidth = width.Value;
+                newHeight = height.Value;
+            }
+
+            else if (height.HasValue)
+            {
+                newWidth = GetNewWidth(newHeight, newWidth, height.Value);
+                newHeight = height.Value;
+            }
+
+            else if (width.HasValue)
+            {
+                newHeight = GetNewHeight(newHeight, newWidth, width.Value);
+                newWidth = width.Value;
+            }
+
+            if (maxHeight.HasValue && maxHeight < newHeight)
+            {
+                newWidth = GetNewWidth(newHeight, newWidth, maxHeight.Value);
+                newHeight = maxHeight.Value;
+            }
+
+            if (maxWidth.HasValue && maxWidth < newWidth)
+            {
+                newHeight = GetNewHeight(newHeight, newWidth, maxWidth.Value);
+                newWidth = maxWidth.Value;
+            }
+
+            return new Size(Convert.ToInt32(newWidth), Convert.ToInt32(newHeight));
+        }
+
+        private static decimal GetNewWidth(decimal currentHeight, decimal currentWidth, int newHeight)
+        {
+            decimal scaleFactor = newHeight;
+            scaleFactor /= currentHeight;
+            scaleFactor *= currentWidth;
+
+            return scaleFactor;
+        }
+
+        private static decimal GetNewHeight(decimal currentHeight, decimal currentWidth, int newWidth)
+        {
+            decimal scaleFactor = newWidth;
+            scaleFactor /= currentWidth;
+            scaleFactor *= currentHeight;
+
+            return scaleFactor;
+        }
+    }
+}

+ 148 - 0
MediaBrowser.Controller/Drawing/ImageProcessor.cs

@@ -0,0 +1,148 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.Entities;
+using System;
+using System.Drawing;
+using System.Drawing.Drawing2D;
+using System.Drawing.Imaging;
+using System.IO;
+using System.Linq;
+
+namespace MediaBrowser.Controller.Drawing
+{
+    public static class ImageProcessor
+    {
+        /// <summary>
+        /// Processes an image by resizing to target dimensions
+        /// </summary>
+        /// <param name="entity">The entity that owns the image</param>
+        /// <param name="imageType">The image type</param>
+        /// <param name="imageIndex">The image index (currently only used with backdrops)</param>
+        /// <param name="toStream">The stream to save the new image to</param>
+        /// <param name="width">Use if a fixed width is required. Aspect ratio will be preserved.</param>
+        /// <param name="height">Use if a fixed height is required. Aspect ratio will be preserved.</param>
+        /// <param name="maxWidth">Use if a max width is required. Aspect ratio will be preserved.</param>
+        /// <param name="maxHeight">Use if a max height is required. Aspect ratio will be preserved.</param>
+        /// <param name="quality">Quality level, from 0-100. Currently only applies to JPG. The default value should suffice.</param>
+        public static void ProcessImage(BaseEntity entity, ImageType imageType, int imageIndex, Stream toStream, int? width, int? height, int? maxWidth, int? maxHeight, int? quality)
+        {
+            Image originalImage = Image.FromFile(GetImagePath(entity, imageType, imageIndex));
+
+            // Determine the output size based on incoming parameters
+            Size newSize = DrawingUtils.Resize(originalImage.Size, width, height, maxWidth, maxHeight);
+
+            Bitmap thumbnail;
+
+            // Graphics.FromImage will throw an exception if the PixelFormat is Indexed, so we need to handle that here
+            if (originalImage.PixelFormat.HasFlag(PixelFormat.Indexed))
+            {
+                thumbnail = new Bitmap(originalImage, newSize.Width, newSize.Height);
+            }
+            else
+            {
+                thumbnail = new Bitmap(newSize.Width, newSize.Height, originalImage.PixelFormat);
+            }
+
+            thumbnail.MakeTransparent();
+
+            // Preserve the original resolution
+            thumbnail.SetResolution(originalImage.HorizontalResolution, originalImage.VerticalResolution);
+
+            Graphics thumbnailGraph = Graphics.FromImage(thumbnail);
+
+            thumbnailGraph.CompositingQuality = CompositingQuality.HighQuality;
+            thumbnailGraph.SmoothingMode = SmoothingMode.HighQuality;
+            thumbnailGraph.InterpolationMode = InterpolationMode.HighQualityBicubic;
+            thumbnailGraph.PixelOffsetMode = PixelOffsetMode.HighQuality;
+            thumbnailGraph.CompositingMode = CompositingMode.SourceOver;
+
+            thumbnailGraph.DrawImage(originalImage, 0, 0, newSize.Width, newSize.Height);
+
+            ImageFormat outputFormat = originalImage.RawFormat;
+
+            // Write to the output stream
+            SaveImage(outputFormat, thumbnail, toStream, quality);
+
+            thumbnailGraph.Dispose();
+            thumbnail.Dispose();
+            originalImage.Dispose();
+        }
+
+        public static string GetImagePath(BaseEntity entity, ImageType imageType, int imageIndex)
+        {
+            var item = entity as BaseItem;
+
+            if (item != null)
+            {
+                if (imageType == ImageType.Logo)
+                {
+                    return item.LogoImagePath;
+                }
+                if (imageType == ImageType.Backdrop)
+                {
+                    return item.BackdropImagePaths.ElementAt(imageIndex);
+                }
+                if (imageType == ImageType.Banner)
+                {
+                    return item.BannerImagePath;
+                }
+                if (imageType == ImageType.Art)
+                {
+                    return item.ArtImagePath;
+                }
+                if (imageType == ImageType.Thumbnail)
+                {
+                    return item.ThumbnailImagePath;
+                }
+            }
+
+            return entity.PrimaryImagePath;
+        }
+
+        public static void SaveImage(ImageFormat outputFormat, Image newImage, Stream toStream, int? quality)
+        {
+            // Use special save methods for jpeg and png that will result in a much higher quality image
+            // All other formats use the generic Image.Save
+            if (ImageFormat.Jpeg.Equals(outputFormat))
+            {
+                SaveJpeg(newImage, toStream, quality);
+            }
+            else if (ImageFormat.Png.Equals(outputFormat))
+            {
+                newImage.Save(toStream, ImageFormat.Png);
+            }
+            else
+            {
+                newImage.Save(toStream, outputFormat);
+            }
+        }
+
+        public static void SaveJpeg(Image image, Stream target, int? quality)
+        {
+            if (!quality.HasValue)
+            {
+                quality = 90;
+            }
+
+            using (var encoderParameters = new EncoderParameters(1))
+            {
+                encoderParameters.Param[0] = new EncoderParameter(Encoder.Quality, quality.Value);
+                image.Save(target, GetImageCodecInfo("image/jpeg"), encoderParameters);
+            }
+        }
+
+        public static ImageCodecInfo GetImageCodecInfo(string mimeType)
+        {
+            ImageCodecInfo[] info = ImageCodecInfo.GetImageEncoders();
+
+            for (int i = 0; i < info.Length; i++)
+            {
+                ImageCodecInfo ici = info[i];
+                if (ici.MimeType.Equals(mimeType, StringComparison.OrdinalIgnoreCase))
+                {
+                    return ici;
+                }
+            }
+            return info[1];
+        }
+    }
+}

+ 14 - 0
MediaBrowser.Controller/Entities/Audio.cs

@@ -0,0 +1,14 @@
+
+namespace MediaBrowser.Controller.Entities
+{
+    public class Audio : BaseItem
+    {
+        public int BitRate { get; set; }
+        public int Channels { get; set; }
+        public int SampleRate { get; set; }
+
+        public string Artist { get; set; }
+        public string Album { get; set; }
+        public string AlbumArtist { get; set; }
+    }
+}

+ 94 - 0
MediaBrowser.Controller/Entities/BaseEntity.cs

@@ -0,0 +1,94 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.IO;
+using MediaBrowser.Controller.Providers;
+
+namespace MediaBrowser.Controller.Entities
+{
+    /// <summary>
+    /// Provides a base entity for all of our types
+    /// </summary>
+    public abstract class BaseEntity
+    {
+        public string Name { get; set; }
+
+        public Guid Id { get; set; }
+
+        public string Path { get; set; }
+
+        public Folder Parent { get; set; }
+
+        public string PrimaryImagePath { get; set; }
+
+        public DateTime DateCreated { get; set; }
+
+        public DateTime DateModified { get; set; }
+
+        public override string ToString()
+        {
+            return Name;
+        }
+        protected Dictionary<Guid, BaseProviderInfo> _providerData;
+        /// <summary>
+        /// Holds persistent data for providers like last refresh date.
+        /// Providers can use this to determine if they need to refresh.
+        /// The BaseProviderInfo class can be extended to hold anything a provider may need.
+        /// 
+        /// Keyed by a unique provider ID.
+        /// </summary>
+        public Dictionary<Guid, BaseProviderInfo> ProviderData
+        {
+            get
+            {
+                if (_providerData == null) _providerData = new Dictionary<Guid, BaseProviderInfo>();
+                return _providerData;
+            }
+            set
+            {
+                _providerData = value;
+            }
+        }
+
+        protected ItemResolveEventArgs _resolveArgs;
+        /// <summary>
+        /// We attach these to the item so that we only ever have to hit the file system once
+        /// (this includes the children of the containing folder)
+        /// Use ResolveArgs.FileSystemChildren to check for the existence of files instead of File.Exists
+        /// </summary>
+        public ItemResolveEventArgs ResolveArgs
+        {
+            get
+            {
+                if (_resolveArgs == null)
+                {
+                    _resolveArgs = new ItemResolveEventArgs()
+                    {
+                        FileInfo = FileData.GetFileData(this.Path),
+                        Parent = this.Parent,
+                        Cancel = false,
+                        Path = this.Path
+                    };
+                    _resolveArgs = FileSystemHelper.FilterChildFileSystemEntries(_resolveArgs, (this.Parent != null && this.Parent.IsRoot));
+                }
+                return _resolveArgs;
+            }
+            set
+            {
+                _resolveArgs = value;
+            }
+        }
+
+        /// <summary>
+        /// Refresh metadata on us by execution our provider chain
+        /// </summary>
+        /// <returns>true if a provider reports we changed</returns>
+        public bool RefreshMetadata()
+        {
+            Kernel.Instance.ExecuteMetadataProviders(this).ConfigureAwait(false);
+            return true;
+        }
+
+    }
+}

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

@@ -0,0 +1,202 @@
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.IO;
+using System;
+using System.Threading.Tasks;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace MediaBrowser.Controller.Entities
+{
+    public abstract class BaseItem : BaseEntity, IHasProviderIds
+    {
+
+        public IEnumerable<string> PhysicalLocations
+        {
+            get
+            {
+                return _resolveArgs.PhysicalLocations;
+            }
+        }
+
+        public string SortName { get; set; }
+
+        /// <summary>
+        /// When the item first debuted. For movies this could be premiere date, episodes would be first aired
+        /// </summary>
+        public DateTime? PremiereDate { get; set; }
+
+        public string LogoImagePath { get; set; }
+
+        public string ArtImagePath { get; set; }
+
+        public string ThumbnailImagePath { get; set; }
+
+        public string BannerImagePath { get; set; }
+
+        public IEnumerable<string> BackdropImagePaths { get; set; }
+
+        public string OfficialRating { get; set; }
+        
+        public string CustomRating { get; set; }
+        public string CustomPin { get; set; }
+
+        public string Language { get; set; }
+        public string Overview { get; set; }
+        public List<string> Taglines { get; set; }
+
+        /// <summary>
+        /// Using a Dictionary to prevent duplicates
+        /// </summary>
+        public Dictionary<string,PersonInfo> People { get; set; }
+
+        public List<string> Studios { get; set; }
+
+        public List<string> Genres { get; set; }
+
+        public string DisplayMediaType { get; set; }
+
+        public float? CommunityRating { get; set; }
+        public long? RunTimeTicks { get; set; }
+
+        public string AspectRatio { get; set; }
+        public int? ProductionYear { get; set; }
+
+        /// <summary>
+        /// If the item is part of a series, this is it's number in the series.
+        /// This could be episode number, album track number, etc.
+        /// </summary>
+        public int? IndexNumber { get; set; }
+
+        /// <summary>
+        /// For an episode this could be the season number, or for a song this could be the disc number.
+        /// </summary>
+        public int? ParentIndexNumber { get; set; }
+
+        public IEnumerable<Video> LocalTrailers { get; set; }
+
+        public string TrailerUrl { get; set; }
+
+        public Dictionary<string, string> ProviderIds { get; set; }
+
+        public Dictionary<Guid, UserItemData> UserData { get; set; }
+
+        public UserItemData GetUserData(User user, bool createIfNull)
+        {
+            if (UserData == null || !UserData.ContainsKey(user.Id))
+            {
+                if (createIfNull)
+                {
+                    AddUserData(user, new UserItemData());
+                }
+                else
+                {
+                    return null;
+                }
+            }
+
+            return UserData[user.Id];
+        }
+
+        private void AddUserData(User user, UserItemData data)
+        {
+            if (UserData == null)
+            {
+                UserData = new Dictionary<Guid, UserItemData>();
+            }
+
+            UserData[user.Id] = data;
+        }
+
+        /// <summary>
+        /// Determines if a given user has access to this item
+        /// </summary>
+        internal bool IsParentalAllowed(User user)
+        {
+            return true;
+        }
+
+        /// <summary>
+        /// Finds an item by ID, recursively
+        /// </summary>
+        public virtual BaseItem FindItemById(Guid id)
+        {
+            if (Id == id)
+            {
+                return this;
+            }
+
+            if (LocalTrailers != null)
+            {
+                return LocalTrailers.FirstOrDefault(i => i.Id == id);
+            }
+
+            return null;
+        }
+
+        public virtual bool IsFolder
+        {
+            get
+            {
+                return false;
+            }
+        }
+
+        /// <summary>
+        /// Determine if we have changed vs the passed in copy
+        /// </summary>
+        /// <param name="copy"></param>
+        /// <returns></returns>
+        public virtual bool IsChanged(BaseItem copy)
+        {
+            bool changed = copy.DateModified != this.DateModified;
+            if (changed) MediaBrowser.Common.Logging.Logger.LogDebugInfo(this.Name + " changed - original creation: " + this.DateCreated + " new creation: " + copy.DateCreated + " original modified: " + this.DateModified + " new modified: " + copy.DateModified);
+            return changed;
+        }
+
+        /// <summary>
+        /// Determines if the item is considered new based on user settings
+        /// </summary>
+        public bool IsRecentlyAdded(User user)
+        {
+            return (DateTime.UtcNow - DateCreated).TotalDays < user.RecentItemDays;
+        }
+
+        public void AddPerson(PersonInfo person)
+        {
+            if (People == null)
+            {
+                People = new Dictionary<string, PersonInfo>(StringComparer.OrdinalIgnoreCase);
+            }
+
+            People[person.Name] = person;
+        }
+
+        /// <summary>
+        /// Marks the item as either played or unplayed
+        /// </summary>
+        public virtual void SetPlayedStatus(User user, bool wasPlayed)
+        {
+            UserItemData data = GetUserData(user, true);
+
+            if (wasPlayed)
+            {
+                data.PlayCount = Math.Max(data.PlayCount, 1);
+            }
+            else
+            {
+                data.PlayCount = 0;
+                data.PlaybackPositionTicks = 0;
+            }
+        }
+
+        /// <summary>
+        /// Do whatever refreshing is necessary when the filesystem pertaining to this item has changed.
+        /// </summary>
+        /// <returns></returns>
+        public virtual Task ChangedExternally()
+        {
+            return Task.Run(() => RefreshMetadata());
+        }
+    }
+}

+ 619 - 0
MediaBrowser.Controller/Entities/Folder.cs

@@ -0,0 +1,619 @@
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Controller.IO;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Common.Logging;
+using MediaBrowser.Controller.Resolvers;
+using System;
+using System.Threading.Tasks;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace MediaBrowser.Controller.Entities
+{
+    public class Folder : BaseItem
+    {
+        #region Events
+        /// <summary>
+        /// Fires whenever a validation routine updates our children.  The added and removed children are properties of the args.
+        /// *** Will fire asynchronously. ***
+        /// </summary>
+        public event EventHandler<ChildrenChangedEventArgs> ChildrenChanged;
+        protected void OnChildrenChanged(ChildrenChangedEventArgs args)
+        {
+            if (ChildrenChanged != null)
+            {
+                Task.Run( () => 
+                    {
+                        ChildrenChanged(this, args);
+                        Kernel.Instance.OnLibraryChanged(args);
+                    });
+            }
+        }
+
+        #endregion
+
+        public override bool IsFolder
+        {
+            get
+            {
+                return true;
+            }
+        }
+
+        public bool IsRoot { get; set; }
+
+        public bool IsVirtualFolder
+        {
+            get
+            {
+                return Parent != null && Parent.IsRoot;
+            }
+        }
+        protected object childLock = new object();
+        protected List<BaseItem> children;
+        protected virtual List<BaseItem> ActualChildren
+        {
+            get
+            {
+                if (children == null)
+                {
+                    LoadChildren();
+                }
+                return children;
+            }
+
+            set
+            {
+                children = value;
+            }
+        }
+
+        /// <summary>
+        /// thread-safe access to the actual children of this folder - without regard to user
+        /// </summary>
+        public IEnumerable<BaseItem> Children
+        {
+            get
+            {
+                lock (childLock)
+                    return ActualChildren.ToList();
+            }
+        }
+
+        /// <summary>
+        /// thread-safe access to all recursive children of this folder - without regard to user
+        /// </summary>
+        public IEnumerable<BaseItem> RecursiveChildren
+        {
+            get
+            {
+                foreach (var item in Children)
+                {
+                    yield return item;
+
+                    var subFolder = item as Folder;
+
+                    if (subFolder != null)
+                    {
+                        foreach (var subitem in subFolder.RecursiveChildren)
+                        {
+                            yield return subitem;
+                        }
+                    }
+                }
+            }
+        }
+                
+
+        /// <summary>
+        /// Loads and validates our children
+        /// </summary>
+        protected virtual void LoadChildren()
+        {
+            //first - load our children from the repo
+            lock (childLock)
+                children = GetCachedChildren();
+
+            //then kick off a validation against the actual file system
+            Task.Run(() => ValidateChildren());
+        }
+
+        protected bool ChildrenValidating = false;
+
+        /// <summary>
+        /// Compare our current children (presumably just read from the repo) with the current state of the file system and adjust for any changes
+        /// ***Currently does not contain logic to maintain items that are unavailable in the file system***
+        /// </summary>
+        /// <returns></returns>
+        protected async virtual void ValidateChildren()
+        {
+            if (ChildrenValidating) return; //only ever want one of these going at once and don't want them to fire off in sequence so don't use lock
+            ChildrenValidating = true;
+            bool changed = false; //this will save us a little time at the end if nothing changes
+            var changedArgs = new ChildrenChangedEventArgs(this);
+            //get the current valid children from filesystem (or wherever)
+            var nonCachedChildren = await GetNonCachedChildren();
+            if (nonCachedChildren == null) return; //nothing to validate
+            //build a dictionary of the current children we have now by Id so we can compare quickly and easily
+            Dictionary<Guid, BaseItem> currentChildren;
+            lock (childLock)
+                currentChildren =  ActualChildren.ToDictionary(i => i.Id);
+            
+            //create a list for our validated children
+            var validChildren = new List<BaseItem>();
+            //now traverse the valid children and find any changed or new items
+            foreach (var child in nonCachedChildren)
+            {
+                BaseItem currentChild;
+                currentChildren.TryGetValue(child.Id, out currentChild);
+                if (currentChild == null)
+                {
+                    //brand new item - needs to be added
+                    changed = true;
+                    changedArgs.ItemsAdded.Add(child);
+                    //refresh it
+                    child.RefreshMetadata();
+                    Logger.LogInfo("New Item Added to Library: ("+child.GetType().Name+") "+ child.Name + " (" + child.Path + ")");
+                    //save it in repo...
+
+                    //and add it to our valid children
+                    validChildren.Add(child);
+                    //fire an added event...?
+                    //if it is a folder we need to validate its children as well
+                    Folder folder = child as Folder;
+                    if (folder != null)
+                    {
+                        folder.ValidateChildren();
+                        //probably need to refresh too...
+                    }
+                }
+                else
+                {
+                    //existing item - check if it has changed
+                    if (currentChild.IsChanged(child))
+                    {
+                        changed = true;
+                        //update resolve args and refresh meta
+                        //  Note - we are refreshing the existing child instead of the newly found one so the "Except" operation below
+                        //  will identify this item as the same one
+                        currentChild.ResolveArgs = child.ResolveArgs;
+                        currentChild.RefreshMetadata();
+                        Logger.LogInfo("Item Changed: ("+currentChild.GetType().Name+") "+ currentChild.Name + " (" + currentChild.Path + ")");
+                        //save it in repo...
+                        validChildren.Add(currentChild);
+                    }
+                    else
+                    {
+                        //current child that didn't change - just put it in the valid children
+                        validChildren.Add(currentChild);
+                    }
+                }
+            }
+
+            //that's all the new and changed ones - now see if there are any that are missing
+            changedArgs.ItemsRemoved = currentChildren.Values.Except(validChildren);
+            changed |= changedArgs.ItemsRemoved != null;
+
+            //now, if anything changed - replace our children
+            if (changed)
+            {
+                if (changedArgs.ItemsRemoved != null) foreach (var item in changedArgs.ItemsRemoved) Logger.LogDebugInfo("** " + item.Name + " Removed from library.");
+
+                lock (childLock)
+                    ActualChildren = validChildren;
+                //and save children in repo...
+
+                //and fire event
+                this.OnChildrenChanged(changedArgs);
+            }
+            ChildrenValidating = false;
+
+        }
+
+        /// <summary>
+        /// Get the children of this folder from the actual file system
+        /// </summary>
+        /// <returns></returns>
+        protected async virtual Task<IEnumerable<BaseItem>> GetNonCachedChildren()
+        {
+            ItemResolveEventArgs args = new ItemResolveEventArgs()
+            {
+                FileInfo = FileData.GetFileData(this.Path),
+                Parent = this.Parent,
+                Cancel = false,
+                Path = this.Path
+            };
+
+            // Gather child folder and files
+            if (args.IsDirectory)
+            {
+                args.FileSystemChildren = FileData.GetFileSystemEntries(this.Path, "*").ToArray();
+
+                bool isVirtualFolder = Parent != null && Parent.IsRoot;
+                args = FileSystemHelper.FilterChildFileSystemEntries(args, isVirtualFolder);
+            }
+            else
+            {
+                Logger.LogError("Folder has a path that is not a directory: " + this.Path);
+                return null;
+            }
+
+            if (!EntityResolutionHelper.ShouldResolvePathContents(args))
+            {
+                return null;
+            }
+            return (await Task.WhenAll<BaseItem>(GetChildren(args.FileSystemChildren)).ConfigureAwait(false))
+                        .Where(i => i != null).OrderBy(f =>
+                        {
+                            return string.IsNullOrEmpty(f.SortName) ? f.Name : f.SortName;
+
+                        });
+
+        }
+
+        /// <summary>
+        /// Resolves a path into a BaseItem
+        /// </summary>
+        protected async Task<BaseItem> GetChild(string path,  WIN32_FIND_DATA? fileInfo = null)
+        {
+            ItemResolveEventArgs args = new ItemResolveEventArgs()
+            {
+                FileInfo = fileInfo ?? FileData.GetFileData(path),
+                Parent = this,
+                Cancel = false,
+                Path = path
+            };
+
+            args.FileSystemChildren = FileData.GetFileSystemEntries(path, "*").ToArray();
+            args = FileSystemHelper.FilterChildFileSystemEntries(args, false);
+
+            return Kernel.Instance.ResolveItem(args);
+
+        }
+
+        /// <summary>
+        /// Finds child BaseItems for us
+        /// </summary>
+        protected Task<BaseItem>[] GetChildren(WIN32_FIND_DATA[] fileSystemChildren)
+        {
+            Task<BaseItem>[] tasks = new Task<BaseItem>[fileSystemChildren.Length];
+
+            for (int i = 0; i < fileSystemChildren.Length; i++)
+            {
+                var child = fileSystemChildren[i];
+
+                tasks[i] = GetChild(child.Path, child);
+            }
+
+            return tasks;
+        }
+
+
+        /// <summary>
+        /// Get our children from the repo - stubbed for now
+        /// </summary>
+        /// <returns></returns>
+        protected virtual List<BaseItem> GetCachedChildren()
+        {
+            return new List<BaseItem>();
+        }
+
+        /// <summary>
+        /// Gets allowed children of an item
+        /// </summary>
+        public IEnumerable<BaseItem> GetChildren(User user)
+        {
+            lock(childLock)
+                return ActualChildren.Where(c => c.IsParentalAllowed(user));
+        }
+
+        /// <summary>
+        /// Gets allowed recursive children of an item
+        /// </summary>
+        public IEnumerable<BaseItem> GetRecursiveChildren(User user)
+        {
+            foreach (var item in GetChildren(user))
+            {
+                yield return item;
+
+                var subFolder = item as Folder;
+
+                if (subFolder != null)
+                {
+                    foreach (var subitem in subFolder.GetRecursiveChildren(user))
+                    {
+                        yield return subitem;
+                    }
+                }
+            }
+        }
+
+        /// <summary>
+        /// Folders need to validate and refresh
+        /// </summary>
+        /// <returns></returns>
+        public override Task ChangedExternally()
+        {
+            return Task.Run(() =>
+                {
+                    if (this.IsRoot)
+                    {
+                        Kernel.Instance.ReloadRoot().ConfigureAwait(false);
+                    }
+                    else
+                    {
+                        RefreshMetadata();
+                        ValidateChildren();
+                    }
+                });
+        }
+
+        /// <summary>
+        /// Since it can be slow to make all of these calculations at once, this method will provide a way to get them all back together
+        /// </summary>
+        public ItemSpecialCounts GetSpecialCounts(User user)
+        {
+            var counts = new ItemSpecialCounts();
+
+            IEnumerable<BaseItem> recursiveChildren = GetRecursiveChildren(user);
+
+            var recentlyAddedItems = GetRecentlyAddedItems(recursiveChildren, user);
+
+            counts.RecentlyAddedItemCount = recentlyAddedItems.Count;
+            counts.RecentlyAddedUnPlayedItemCount = GetRecentlyAddedUnplayedItems(recentlyAddedItems, user).Count;
+            counts.InProgressItemCount = GetInProgressItems(recursiveChildren, user).Count;
+            counts.PlayedPercentage = GetPlayedPercentage(recursiveChildren, user);
+
+            return counts;
+        }
+
+        /// <summary>
+        /// Finds all recursive items within a top-level parent that contain the given genre and are allowed for the current user
+        /// </summary>
+        public IEnumerable<BaseItem> GetItemsWithGenre(string genre, User user)
+        {
+            return GetRecursiveChildren(user).Where(f => f.Genres != null && f.Genres.Any(s => s.Equals(genre, StringComparison.OrdinalIgnoreCase)));
+        }
+
+        /// <summary>
+        /// Finds all recursive items within a top-level parent that contain the given year and are allowed for the current user
+        /// </summary>
+        public IEnumerable<BaseItem> GetItemsWithYear(int year, User user)
+        {
+            return GetRecursiveChildren(user).Where(f => f.ProductionYear.HasValue && f.ProductionYear == year);
+        }
+
+        /// <summary>
+        /// Finds all recursive items within a top-level parent that contain the given studio and are allowed for the current user
+        /// </summary>
+        public IEnumerable<BaseItem> GetItemsWithStudio(string studio, User user)
+        {
+            return GetRecursiveChildren(user).Where(f => f.Studios != null && f.Studios.Any(s => s.Equals(studio, StringComparison.OrdinalIgnoreCase)));
+        }
+
+        /// <summary>
+        /// Finds all recursive items within a top-level parent that the user has marked as a favorite
+        /// </summary>
+        public IEnumerable<BaseItem> GetFavoriteItems(User user)
+        {
+            return GetRecursiveChildren(user).Where(c =>
+            {
+                UserItemData data = c.GetUserData(user, false);
+
+                if (data != null)
+                {
+                    return data.IsFavorite;
+                }
+
+                return false;
+            });
+        }
+
+        /// <summary>
+        /// Finds all recursive items within a top-level parent that contain the given person and are allowed for the current user
+        /// </summary>
+        public IEnumerable<BaseItem> GetItemsWithPerson(string person, User user)
+        {
+            return GetRecursiveChildren(user).Where(c =>
+            {
+                if (c.People != null)
+                {
+                    return c.People.ContainsKey(person);
+                }
+
+                return false;
+            });
+        }
+
+        /// <summary>
+        /// Finds all recursive items within a top-level parent that contain the given person and are allowed for the current user
+        /// </summary>
+        /// <param name="personType">Specify this to limit results to a specific PersonType</param>
+        public IEnumerable<BaseItem> GetItemsWithPerson(string person, string personType, User user)
+        {
+            return GetRecursiveChildren(user).Where(c =>
+            {
+                if (c.People != null)
+                {
+                    return c.People.ContainsKey(person) && c.People[person].Type.Equals(personType, StringComparison.OrdinalIgnoreCase);
+                }
+
+                return false;
+            });
+        }
+
+        /// <summary>
+        /// Gets all recently added items (recursive) within a folder, based on configuration and parental settings
+        /// </summary>
+        public List<BaseItem> GetRecentlyAddedItems(User user)
+        {
+            return GetRecentlyAddedItems(GetRecursiveChildren(user), user);
+        }
+
+        /// <summary>
+        /// Gets all recently added unplayed items (recursive) within a folder, based on configuration and parental settings
+        /// </summary>
+        public List<BaseItem> GetRecentlyAddedUnplayedItems(User user)
+        {
+            return GetRecentlyAddedUnplayedItems(GetRecursiveChildren(user), user);
+        }
+
+        /// <summary>
+        /// Gets all in-progress items (recursive) within a folder
+        /// </summary>
+        public List<BaseItem> GetInProgressItems(User user)
+        {
+            return GetInProgressItems(GetRecursiveChildren(user), user);
+        }
+
+        /// <summary>
+        /// Takes a list of items and returns the ones that are recently added
+        /// </summary>
+        private static List<BaseItem> GetRecentlyAddedItems(IEnumerable<BaseItem> itemSet, User user)
+        {
+            var list = new List<BaseItem>();
+
+            foreach (var item in itemSet)
+            {
+                if (!item.IsFolder && item.IsRecentlyAdded(user))
+                {
+                    list.Add(item);
+                }
+            }
+
+            return list;
+        }
+
+        /// <summary>
+        /// Takes a list of items and returns the ones that are recently added and unplayed
+        /// </summary>
+        private static List<BaseItem> GetRecentlyAddedUnplayedItems(IEnumerable<BaseItem> itemSet, User user)
+        {
+            var list = new List<BaseItem>();
+
+            foreach (var item in itemSet)
+            {
+                if (!item.IsFolder && item.IsRecentlyAdded(user))
+                {
+                    var userdata = item.GetUserData(user, false);
+
+                    if (userdata == null || userdata.PlayCount == 0)
+                    {
+                        list.Add(item);
+                    }
+                }
+            }
+
+            return list;
+        }
+
+        /// <summary>
+        /// Takes a list of items and returns the ones that are in progress
+        /// </summary>
+        private static List<BaseItem> GetInProgressItems(IEnumerable<BaseItem> itemSet, User user)
+        {
+            var list = new List<BaseItem>();
+
+            foreach (var item in itemSet)
+            {
+                if (!item.IsFolder)
+                {
+                    var userdata = item.GetUserData(user, false);
+
+                    if (userdata != null && userdata.PlaybackPositionTicks > 0)
+                    {
+                        list.Add(item);
+                    }
+                }
+            }
+
+            return list;
+        }
+
+        /// <summary>
+        /// Gets the total played percentage for a set of items
+        /// </summary>
+        private static decimal GetPlayedPercentage(IEnumerable<BaseItem> itemSet, User user)
+        {
+            itemSet = itemSet.Where(i => !(i.IsFolder));
+
+            decimal totalPercent = 0;
+
+            int count = 0;
+
+            foreach (BaseItem item in itemSet)
+            {
+                count++;
+                
+                UserItemData data = item.GetUserData(user, false);
+
+                if (data == null)
+                {
+                    continue;
+                }
+
+                if (data.PlayCount > 0)
+                {
+                    totalPercent += 100;
+                }
+                else if (data.PlaybackPositionTicks > 0 && item.RunTimeTicks.HasValue)
+                {
+                    decimal itemPercent = data.PlaybackPositionTicks;
+                    itemPercent /= item.RunTimeTicks.Value;
+                    totalPercent += itemPercent;
+                }
+            }
+
+            if (count == 0)
+            {
+                return 0;
+            }
+
+            return totalPercent / count;
+        }
+
+        /// <summary>
+        /// Marks the item as either played or unplayed
+        /// </summary>
+        public override void SetPlayedStatus(User user, bool wasPlayed)
+        {
+            base.SetPlayedStatus(user, wasPlayed);
+
+            // Now sweep through recursively and update status
+            foreach (BaseItem item in GetChildren(user))
+            {
+                item.SetPlayedStatus(user, wasPlayed);
+            }
+        }
+
+        /// <summary>
+        /// Finds an item by ID, recursively
+        /// </summary>
+        public override BaseItem FindItemById(Guid id)
+        {
+            var result = base.FindItemById(id);
+
+            if (result != null)
+            {
+                return result;
+            }
+
+            //this should be functionally equivilent to what was here since it is IEnum and works on a thread-safe copy
+            return RecursiveChildren.FirstOrDefault(i => i.Id == id);
+        }
+
+        /// <summary>
+        /// Finds an item by path, recursively
+        /// </summary>
+        public BaseItem FindByPath(string path)
+        {
+            if (PhysicalLocations.Contains(path, StringComparer.OrdinalIgnoreCase))
+            {
+                return this;
+            }
+
+            //this should be functionally equivilent to what was here since it is IEnum and works on a thread-safe copy
+            return RecursiveChildren.FirstOrDefault(i => i.PhysicalLocations.Contains(path, StringComparer.OrdinalIgnoreCase));
+        }
+    }
+}

+ 7 - 0
MediaBrowser.Controller/Entities/Genre.cs

@@ -0,0 +1,7 @@
+
+namespace MediaBrowser.Controller.Entities
+{
+    public class Genre : BaseEntity
+    {
+    }
+}

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

@@ -0,0 +1,7 @@
+
+namespace MediaBrowser.Controller.Entities.Movies
+{
+    public class BoxSet : Folder
+    {
+    }
+}

+ 31 - 0
MediaBrowser.Controller/Entities/Movies/Movie.cs

@@ -0,0 +1,31 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace MediaBrowser.Controller.Entities.Movies
+{
+    public class Movie : Video
+    {
+        public IEnumerable<Video> SpecialFeatures { get; set; }
+
+        /// <summary>
+        /// Finds an item by ID, recursively
+        /// </summary>
+        public override BaseItem FindItemById(Guid id)
+        {
+            var item = base.FindItemById(id);
+
+            if (item != null)
+            {
+                return item;
+            }
+
+            if (SpecialFeatures != null)
+            {
+                return SpecialFeatures.FirstOrDefault(i => i.Id == id);
+            }
+
+            return null;
+        }
+    }
+}

+ 25 - 0
MediaBrowser.Controller/Entities/Person.cs

@@ -0,0 +1,25 @@
+
+namespace MediaBrowser.Controller.Entities
+{
+    /// <summary>
+    /// This is the full Person object that can be retrieved with all of it's data.
+    /// </summary>
+    public class Person : BaseEntity
+    {
+    }
+
+    /// <summary>
+    /// This is the small Person stub that is attached to BaseItems
+    /// </summary>
+    public class PersonInfo
+    {
+        public string Name { get; set; }
+        public string Overview { get; set; }
+        public string Type { get; set; }
+
+        public override string ToString()
+        {
+            return Name;
+        }
+    }
+}

+ 7 - 0
MediaBrowser.Controller/Entities/Studio.cs

@@ -0,0 +1,7 @@
+
+namespace MediaBrowser.Controller.Entities
+{
+    public class Studio : BaseEntity
+    {
+    }
+}

+ 7 - 0
MediaBrowser.Controller/Entities/TV/Episode.cs

@@ -0,0 +1,7 @@
+
+namespace MediaBrowser.Controller.Entities.TV
+{
+    public class Episode : Video
+    {
+    }
+}

+ 34 - 0
MediaBrowser.Controller/Entities/TV/Season.cs

@@ -0,0 +1,34 @@
+using System;
+
+namespace MediaBrowser.Controller.Entities.TV
+{
+    public class Season : Folder
+    {
+        /// <summary>
+        /// Store these to reduce disk access in Episode Resolver
+        /// </summary>
+        public string[] MetadataFiles
+        {
+            get
+            {
+                return ResolveArgs.MetadataFiles ?? new string[] { };
+            }
+        }
+
+        /// <summary>
+        /// Determines if the metafolder contains a given file
+        /// </summary>
+        public bool ContainsMetadataFile(string file)
+        {
+            for (int i = 0; i < MetadataFiles.Length; i++)
+            {
+                if (MetadataFiles[i].Equals(file, StringComparison.OrdinalIgnoreCase))
+                {
+                    return true;
+                }
+            }
+
+            return false;
+        }
+    }
+}

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

@@ -0,0 +1,12 @@
+using System;
+using System.Collections.Generic;
+
+namespace MediaBrowser.Controller.Entities.TV
+{
+    public class Series : Folder
+    {
+        public string Status { get; set; }
+        public IEnumerable<DayOfWeek> AirDays { get; set; }
+        public string AirTime { get; set; }
+    }
+}

+ 21 - 0
MediaBrowser.Controller/Entities/User.cs

@@ -0,0 +1,21 @@
+using System;
+
+namespace MediaBrowser.Controller.Entities
+{
+    public class User : BaseEntity
+    {
+        public string Password { get; set; }
+        
+        public string MaxParentalRating { get; set; }
+
+        public int RecentItemDays { get; set; }
+
+        public User()
+        {
+            RecentItemDays = 14;
+        }
+
+        public DateTime? LastLoginDate { get; set; }
+        public DateTime? LastActivityDate { get; set; }
+    }
+}

+ 67 - 0
MediaBrowser.Controller/Entities/UserItemData.cs

@@ -0,0 +1,67 @@
+using System;
+using System.Runtime.Serialization;
+
+namespace MediaBrowser.Controller.Entities
+{
+    public class UserItemData
+    {
+        private float? _rating;
+        /// <summary>
+        /// Gets or sets the users 0-10 rating
+        /// </summary>
+        public float? Rating
+        {
+            get
+            {
+                return _rating;
+            }
+            set
+            {
+                if (value.HasValue)
+                {
+                    if (value.Value < 0 || value.Value > 10)
+                    {
+                        throw new InvalidOperationException("A 0-10 rating is required for UserItemData.");
+                    }
+                }
+
+                _rating = value;
+            }
+        }
+
+        public long PlaybackPositionTicks { get; set; }
+
+        public int PlayCount { get; set; }
+
+        public bool IsFavorite { get; set; }
+
+        /// <summary>
+        /// This is an interpreted property to indicate likes or dislikes
+        /// This should never be serialized.
+        /// </summary>
+        [IgnoreDataMember]
+        public bool? Likes
+        {
+            get
+            {
+                if (Rating != null)
+                {
+                    return Rating >= 6.5;
+                }
+
+                return null;
+            }
+            set
+            {
+                if (value.HasValue)
+                {
+                    Rating = value.Value ? 10 : 1;
+                }
+                else
+                {
+                    Rating = null;
+                }
+            }
+        }
+    }
+}

部分文件因为文件数量过多而无法显示