diff --git a/.hgignore b/.hgignore
new file mode 100644
index 0000000000..c8162e4c87
--- /dev/null
+++ b/.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
diff --git a/MediaBrowser.Api/ApiService.cs b/MediaBrowser.Api/ApiService.cs
new file mode 100644
index 0000000000..0fef1cb574
--- /dev/null
+++ b/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
+{
+ ///
+ /// Contains some helpers for the api
+ ///
+ public static class ApiService
+ {
+ ///
+ /// Gets an Item by Id, or the root item if none is supplied
+ ///
+ public static BaseItem GetItemById(string id)
+ {
+ Guid guid = string.IsNullOrEmpty(id) ? Guid.Empty : new Guid(id);
+
+ return Kernel.Instance.GetItemById(guid);
+ }
+
+ ///
+ /// Gets a User by Id
+ ///
+ /// Whether or not to update the user's LastActivityDate
+ 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;
+ }
+
+ ///
+ /// Gets the default User
+ ///
+ /// Whether or not to update the user's LastActivityDate
+ public static User GetDefaultUser(bool logActivity)
+ {
+ User user = Kernel.Instance.GetDefaultUser();
+
+ if (logActivity)
+ {
+ LogUserActivity(user);
+ }
+
+ return user;
+ }
+
+ ///
+ /// Updates LastActivityDate for a given User
+ ///
+ public static void LogUserActivity(User user)
+ {
+ user.LastActivityDate = DateTime.UtcNow;
+ Kernel.Instance.SaveUser(user);
+ }
+
+ ///
+ /// Converts a BaseItem to a DTOBaseItem
+ ///
+ public async static Task GetDtoBaseItem(BaseItem item, User user,
+ bool includeChildren = true,
+ bool includePeople = true)
+ {
+ var dto = new DtoBaseItem();
+
+ var tasks = new List();
+
+ 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;
+ }
+
+ ///
+ /// Sets simple property values on a DTOBaseItem
+ ///
+ 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
+ };
+ }
+ }
+
+ ///
+ /// Attaches Studio DTO's to a DTOBaseItem
+ ///
+ 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;
+ }
+ }
+ }
+
+ ///
+ /// Attaches child DTO's to a DTOBaseItem
+ ///
+ private static async Task AttachChildren(DtoBaseItem dto, BaseItem item, User user)
+ {
+ var folder = item as Folder;
+
+ if (folder != null)
+ {
+ IEnumerable children = folder.GetChildren(user);
+
+ dto.Children = await Task.WhenAll(children.Select(c => GetDtoBaseItem(c, user, false, false))).ConfigureAwait(false);
+ }
+ }
+
+ ///
+ /// Attaches trailer DTO's to a DTOBaseItem
+ ///
+ 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);
+ }
+ }
+
+ ///
+ /// Attaches People DTO's to a DTOBaseItem
+ ///
+ private static async Task AttachPeople(DtoBaseItem dto, BaseItem item)
+ {
+ // Attach People by transforming them into BaseItemPerson (DTO)
+ if (item.People != null)
+ {
+ IEnumerable 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();
+ }
+ }
+
+ ///
+ /// If an item does not any backdrops, this can be used to find the first parent that does have one
+ ///
+ 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;
+ }
+
+ ///
+ /// If an item does not have a logo, this can be used to find the first parent that does have one
+ ///
+ 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;
+ }
+
+ ///
+ /// Gets an ImagesByName entity along with the number of items containing it
+ ///
+ public static IbnItem GetIbnItem(BaseEntity entity, int itemCount)
+ {
+ return new IbnItem
+ {
+ Id = entity.Id,
+ BaseItemCount = itemCount,
+ HasImage = !string.IsNullOrEmpty(entity.PrimaryImagePath),
+ Name = entity.Name
+ };
+ }
+
+ ///
+ /// Converts a User to a DTOUser
+ ///
+ 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
+ };
+ }
+
+ ///
+ /// Converts a UserItemData to a DTOUserItemData
+ ///
+ 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);
+ }
+ }
+}
diff --git a/MediaBrowser.Api/Drawing/DrawingUtils.cs b/MediaBrowser.Api/Drawing/DrawingUtils.cs
new file mode 100644
index 0000000000..f76a74218f
--- /dev/null
+++ b/MediaBrowser.Api/Drawing/DrawingUtils.cs
@@ -0,0 +1,81 @@
+using System;
+using System.Drawing;
+
+namespace MediaBrowser.Api.Drawing
+{
+ public static class DrawingUtils
+ {
+ ///
+ /// Resizes a set of dimensions
+ ///
+ 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);
+ }
+
+ ///
+ /// Resizes a set of dimensions
+ ///
+ /// The original size object
+ /// A new fixed width, if desired
+ /// A new fixed neight, if desired
+ /// A max fixed width, if desired
+ /// A max fixed height, if desired
+ /// A new size object
+ 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;
+ }
+ }
+}
diff --git a/MediaBrowser.Api/Drawing/ImageProcessor.cs b/MediaBrowser.Api/Drawing/ImageProcessor.cs
new file mode 100644
index 0000000000..1a471acf54
--- /dev/null
+++ b/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
+ {
+ ///
+ /// Processes an image by resizing to target dimensions
+ ///
+ /// The entity that owns the image
+ /// The image type
+ /// The image index (currently only used with backdrops)
+ /// The stream to save the new image to
+ /// Use if a fixed width is required. Aspect ratio will be preserved.
+ /// Use if a fixed height is required. Aspect ratio will be preserved.
+ /// Use if a max width is required. Aspect ratio will be preserved.
+ /// Use if a max height is required. Aspect ratio will be preserved.
+ /// Quality level, from 0-100. Currently only applies to JPG. The default value should suffice.
+ 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];
+ }
+ }
+}
diff --git a/MediaBrowser.Api/HttpHandlers/AudioHandler.cs b/MediaBrowser.Api/HttpHandlers/AudioHandler.cs
new file mode 100644
index 0000000000..9c16acd2ef
--- /dev/null
+++ b/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
+{
+ ///
+ /// Supported output formats are: mp3,flac,ogg,wav,asf,wma,aac
+ ///
+ [Export(typeof(BaseHandler))]
+ public class AudioHandler : BaseMediaHandler