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 + { + public override bool HandlesRequest(HttpListenerRequest request) + { + return ApiService.IsApiUrlMatch("audio", request); + } + + /// + /// We can output these formats directly, but we cannot encode to them. + /// + protected override IEnumerable 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; + } + + /// + /// Determines whether or not the original file requires transcoding + /// + 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; + } + + /// + /// Creates arguments to pass to ffmpeg + /// + protected override string GetCommandLineArguments() + { + var audioTranscodeParams = new List(); + + 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()) + " -"; + } + } +} diff --git a/MediaBrowser.Api/HttpHandlers/BaseMediaHandler.cs b/MediaBrowser.Api/HttpHandlers/BaseMediaHandler.cs new file mode 100644 index 0000000000..96ef606813 --- /dev/null +++ b/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 : BaseHandler + where TBaseItemType : BaseItem, new() + { + /// + /// Supported values: mp3,flac,ogg,wav,asf,wma,aac + /// + protected virtual IEnumerable OutputFormats + { + get + { + return QueryString["outputformats"].Split(',').Select(o => (TOutputType)Enum.Parse(typeof(TOutputType), o, true)); + } + } + + /// + /// These formats can be outputted directly but cannot be encoded to + /// + protected virtual IEnumerable UnsupportedOutputEncodingFormats + { + get + { + return new TOutputType[] { }; + } + } + + private TBaseItemType _libraryItem; + /// + /// Gets the library item that will be played, if any + /// + 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 GetResponseInfo() + { + ResponseInfo info = new ResponseInfo + { + ContentType = MimeTypes.GetMimeType("." + GetConversionOutputFormat()), + CompressResponse = false + }; + + return Task.FromResult(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(); + + /// + /// Gets the format we'll be converting to + /// + 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(); + } + + /// + /// Gets the number of audio channels to specify on the command line + /// + 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; + } + + /// + /// Gets the number of audio channels to specify on the command line + /// + 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; + } + } +} diff --git a/MediaBrowser.Api/HttpHandlers/FavoriteStatusHandler.cs b/MediaBrowser.Api/HttpHandlers/FavoriteStatusHandler.cs new file mode 100644 index 0000000000..19c175d8b8 --- /dev/null +++ b/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 +{ + /// + /// Provides a handler to set user favorite status for an item + /// + [Export(typeof(BaseHandler))] + public class FavoriteStatusHandler : BaseSerializationHandler + { + public override bool HandlesRequest(HttpListenerRequest request) + { + return ApiService.IsApiUrlMatch("FavoriteStatus", request); + } + + protected override Task 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)); + } + } +} \ No newline at end of file diff --git a/MediaBrowser.Api/HttpHandlers/GenreHandler.cs b/MediaBrowser.Api/HttpHandlers/GenreHandler.cs new file mode 100644 index 0000000000..7cca2aea76 --- /dev/null +++ b/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 +{ + /// + /// Gets a single genre + /// + [Export(typeof(BaseHandler))] + public class GenreHandler : BaseSerializationHandler + { + public override bool HandlesRequest(HttpListenerRequest request) + { + return ApiService.IsApiUrlMatch("genre", request); + } + + protected override Task 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); + } + + /// + /// Gets a Genre + /// + private async Task GetGenre(Folder parent, User user, string name) + { + int count = 0; + + // Get all the allowed recursive children + IEnumerable 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); + } + } +} diff --git a/MediaBrowser.Api/HttpHandlers/GenresHandler.cs b/MediaBrowser.Api/HttpHandlers/GenresHandler.cs new file mode 100644 index 0000000000..4c5a9f4b7f --- /dev/null +++ b/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 + { + public override bool HandlesRequest(HttpListenerRequest request) + { + return ApiService.IsApiUrlMatch("genres", request); + } + + protected override Task GetObjectToSerialize() + { + var parent = ApiService.GetItemById(QueryString["id"]) as Folder; + User user = ApiService.GetUserById(QueryString["userid"], true); + + return GetAllGenres(parent, user); + } + + /// + /// 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 + /// + private async Task GetAllGenres(Folder parent, User user) + { + var data = new Dictionary(); + + // Get all the allowed recursive children + IEnumerable 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; + } + } +} diff --git a/MediaBrowser.Api/HttpHandlers/ImageHandler.cs b/MediaBrowser.Api/HttpHandlers/ImageHandler.cs new file mode 100644 index 0000000000..4aa367fb7e --- /dev/null +++ b/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 GetImagePath() + { + _imagePath = _imagePath ?? await DiscoverImagePath(); + + return _imagePath; + } + + private BaseEntity _sourceEntity; + + private async Task 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 DiscoverImagePath() + { + var entity = await GetSourceEntity().ConfigureAwait(false); + + return ImageProcessor.GetImagePath(entity, ImageType, ImageIndex); + } + + protected async override Task 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); + } + } +} diff --git a/MediaBrowser.Api/HttpHandlers/ItemHandler.cs b/MediaBrowser.Api/HttpHandlers/ItemHandler.cs new file mode 100644 index 0000000000..60b328d1a3 --- /dev/null +++ b/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 +{ + /// + /// Provides a handler to retrieve a single item + /// + [Export(typeof(BaseHandler))] + public class ItemHandler : BaseSerializationHandler + { + public override bool HandlesRequest(HttpListenerRequest request) + { + return ApiService.IsApiUrlMatch("item", request); + } + + protected override Task GetObjectToSerialize() + { + User user = ApiService.GetUserById(QueryString["userid"], true); + + BaseItem item = ApiService.GetItemById(QueryString["id"]); + + if (item == null) + { + return null; + } + + return ApiService.GetDtoBaseItem(item, user); + } + } +} diff --git a/MediaBrowser.Api/HttpHandlers/ItemListHandler.cs b/MediaBrowser.Api/HttpHandlers/ItemListHandler.cs new file mode 100644 index 0000000000..d236e546b2 --- /dev/null +++ b/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 + { + public override bool HandlesRequest(HttpListenerRequest request) + { + return ApiService.IsApiUrlMatch("itemlist", request); + } + + protected override Task 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 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; + } + } + } +} diff --git a/MediaBrowser.Api/HttpHandlers/MovieSpecialFeaturesHandler.cs b/MediaBrowser.Api/HttpHandlers/MovieSpecialFeaturesHandler.cs new file mode 100644 index 0000000000..3ab78ee8d1 --- /dev/null +++ b/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 +{ + /// + /// This handler retrieves special features for movies + /// + [Export(typeof(BaseHandler))] + public class MovieSpecialFeaturesHandler : BaseSerializationHandler + { + public override bool HandlesRequest(HttpListenerRequest request) + { + return ApiService.IsApiUrlMatch("MovieSpecialFeatures", request); + } + + protected override Task 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"]; + } + } + } +} diff --git a/MediaBrowser.Api/HttpHandlers/PersonHandler.cs b/MediaBrowser.Api/HttpHandlers/PersonHandler.cs new file mode 100644 index 0000000000..fbbd88a11d --- /dev/null +++ b/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 +{ + /// + /// Gets a single Person + /// + [Export(typeof(BaseHandler))] + public class PersonHandler : BaseSerializationHandler + { + public override bool HandlesRequest(HttpListenerRequest request) + { + return ApiService.IsApiUrlMatch("person", request); + } + + protected override Task 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); + } + + /// + /// Gets a Person + /// + private async Task GetPerson(Folder parent, User user, string name) + { + int count = 0; + + // Get all the allowed recursive children + IEnumerable 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); + } + } +} diff --git a/MediaBrowser.Api/HttpHandlers/PlayedStatusHandler.cs b/MediaBrowser.Api/HttpHandlers/PlayedStatusHandler.cs new file mode 100644 index 0000000000..c010bcb023 --- /dev/null +++ b/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 +{ + /// + /// Provides a handler to set played status for an item + /// + [Export(typeof(BaseHandler))] + public class PlayedStatusHandler : BaseSerializationHandler + { + public override bool HandlesRequest(HttpListenerRequest request) + { + return ApiService.IsApiUrlMatch("PlayedStatus", request); + } + + protected override Task 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)); + } + } +} \ No newline at end of file diff --git a/MediaBrowser.Api/HttpHandlers/PluginAssemblyHandler.cs b/MediaBrowser.Api/HttpHandlers/PluginAssemblyHandler.cs new file mode 100644 index 0000000000..47f08c8c32 --- /dev/null +++ b/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 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); + } + } +} diff --git a/MediaBrowser.Api/HttpHandlers/PluginConfigurationHandler.cs b/MediaBrowser.Api/HttpHandlers/PluginConfigurationHandler.cs new file mode 100644 index 0000000000..dc363956fd --- /dev/null +++ b/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 + { + 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 GetObjectToSerialize() + { + return Task.FromResult(Plugin.Configuration); + } + + protected override async Task GetResponseInfo() + { + var info = await base.GetResponseInfo().ConfigureAwait(false); + + info.DateLastModified = Plugin.ConfigurationDateLastModified; + + info.CacheDuration = TimeSpan.FromDays(7); + + return info; + } + } +} diff --git a/MediaBrowser.Api/HttpHandlers/PluginsHandler.cs b/MediaBrowser.Api/HttpHandlers/PluginsHandler.cs new file mode 100644 index 0000000000..a1b37ecaba --- /dev/null +++ b/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 +{ + /// + /// Provides information about installed plugins + /// + [Export(typeof(BaseHandler))] + public class PluginsHandler : BaseSerializationHandler> + { + public override bool HandlesRequest(HttpListenerRequest request) + { + return ApiService.IsApiUrlMatch("plugins", request); + } + + protected override Task> 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); + } + } +} diff --git a/MediaBrowser.Api/HttpHandlers/ServerConfigurationHandler.cs b/MediaBrowser.Api/HttpHandlers/ServerConfigurationHandler.cs new file mode 100644 index 0000000000..48c6761b16 --- /dev/null +++ b/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 + { + public override bool HandlesRequest(HttpListenerRequest request) + { + return ApiService.IsApiUrlMatch("serverconfiguration", request); + } + + protected override Task GetObjectToSerialize() + { + return Task.FromResult(Kernel.Instance.Configuration); + } + + protected override async Task GetResponseInfo() + { + var info = await base.GetResponseInfo().ConfigureAwait(false); + + info.DateLastModified = + File.GetLastWriteTimeUtc(Kernel.Instance.ApplicationPaths.SystemConfigurationFilePath); + + info.CacheDuration = TimeSpan.FromDays(7); + + return info; + } + } +} diff --git a/MediaBrowser.Api/HttpHandlers/StudioHandler.cs b/MediaBrowser.Api/HttpHandlers/StudioHandler.cs new file mode 100644 index 0000000000..6576e2cfe5 --- /dev/null +++ b/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 +{ + /// + /// Gets a single studio + /// + [Export(typeof(BaseHandler))] + public class StudioHandler : BaseSerializationHandler + { + public override bool HandlesRequest(HttpListenerRequest request) + { + return ApiService.IsApiUrlMatch("studio", request); + } + + protected override Task 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); + } + + /// + /// Gets a Studio + /// + private async Task GetStudio(Folder parent, User user, string name) + { + int count = 0; + + // Get all the allowed recursive children + IEnumerable 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); + } + } +} diff --git a/MediaBrowser.Api/HttpHandlers/StudiosHandler.cs b/MediaBrowser.Api/HttpHandlers/StudiosHandler.cs new file mode 100644 index 0000000000..4377a0f432 --- /dev/null +++ b/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 + { + public override bool HandlesRequest(HttpListenerRequest request) + { + return ApiService.IsApiUrlMatch("studios", request); + } + + protected override Task GetObjectToSerialize() + { + var parent = ApiService.GetItemById(QueryString["id"]) as Folder; + var user = ApiService.GetUserById(QueryString["userid"], true); + + return GetAllStudios(parent, user); + } + + /// + /// 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 + /// + private async Task GetAllStudios(Folder parent, User user) + { + var data = new Dictionary(); + + // Get all the allowed recursive children + IEnumerable 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; + } + } +} diff --git a/MediaBrowser.Api/HttpHandlers/UserAuthenticationHandler.cs b/MediaBrowser.Api/HttpHandlers/UserAuthenticationHandler.cs new file mode 100644 index 0000000000..fa9d975983 --- /dev/null +++ b/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 + { + public override bool HandlesRequest(HttpListenerRequest request) + { + return ApiService.IsApiUrlMatch("UserAuthentication", request); + } + + protected override async Task 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); + } + } +} diff --git a/MediaBrowser.Api/HttpHandlers/UserHandler.cs b/MediaBrowser.Api/HttpHandlers/UserHandler.cs new file mode 100644 index 0000000000..bc92862040 --- /dev/null +++ b/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 + { + public override bool HandlesRequest(HttpListenerRequest request) + { + return ApiService.IsApiUrlMatch("user", request); + } + + protected override Task 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); + } + } +} diff --git a/MediaBrowser.Api/HttpHandlers/UserItemRatingHandler.cs b/MediaBrowser.Api/HttpHandlers/UserItemRatingHandler.cs new file mode 100644 index 0000000000..aed0804b6d --- /dev/null +++ b/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 +{ + /// + /// Provides a handler to set a user's rating for an item + /// + [Export(typeof(BaseHandler))] + public class UserItemRatingHandler : BaseSerializationHandler + { + public override bool HandlesRequest(HttpListenerRequest request) + { + return ApiService.IsApiUrlMatch("UserItemRating", request); + } + + protected override Task 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)); + } + } +} \ No newline at end of file diff --git a/MediaBrowser.Api/HttpHandlers/UsersHandler.cs b/MediaBrowser.Api/HttpHandlers/UsersHandler.cs new file mode 100644 index 0000000000..3fc3a7d585 --- /dev/null +++ b/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> + { + public override bool HandlesRequest(HttpListenerRequest request) + { + return ApiService.IsApiUrlMatch("users", request); + } + + protected override Task> GetObjectToSerialize() + { + return Task.FromResult(Kernel.Instance.Users.Select(u => ApiService.GetDtoUser(u))); + } + } +} diff --git a/MediaBrowser.Api/HttpHandlers/VideoHandler.cs b/MediaBrowser.Api/HttpHandlers/VideoHandler.cs new file mode 100644 index 0000000000..e34a1b41f7 --- /dev/null +++ b/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 +{ + /// + /// Supported output formats: mkv,m4v,mp4,asf,wmv,mov,webm,ogv,3gp,avi,ts,flv + /// + [Export(typeof(BaseHandler))] + class VideoHandler : BaseMediaHandler + { + public override bool HandlesRequest(HttpListenerRequest request) + { + return ApiService.IsApiUrlMatch("video", request); + } + + /// + /// We can output these files directly, but we can't encode them + /// + protected override IEnumerable 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 }; + } + } + + /// + /// Determines whether or not we can just output the original file directly + /// + 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()).FirstOrDefault(); + + if (audioStream != null) + { + if (RequiresAudioConversion(audioStream)) + { + return true; + } + } + + // Yay + return false; + } + + /// + /// Translates the output file extension to the format param that follows "-f" on the ffmpeg command line + /// + 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(); + } + + /// + /// Creates arguments to pass to ffmpeg + /// + 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) + ); + } + + /// + /// Gets video arguments to pass to ffmpeg + /// + 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; + } + + /// + /// Gets audio arguments to pass to ffmpeg + /// + private string GetAudioArguments(VideoOutputFormats outputFormat) + { + AudioStream audioStream = (LibraryItem.AudioStreams ?? new List()).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; + } + + /// + /// Gets the name of the output video codec + /// + 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"; + } + + /// + /// Gets the name of the output audio codec + /// + 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"; + } + + /// + /// Gets the number of audio channels to specify on the command line + /// + 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); + } + + /// + /// Determines if the video stream requires encoding + /// + 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; + } + + /// + /// Determines if the audio stream requires encoding + /// + 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; + } + + /// + /// Gets the fixed output video height, in pixels + /// + private int? Height + { + get + { + string val = QueryString["height"]; + + if (string.IsNullOrEmpty(val)) + { + return null; + } + + return int.Parse(val); + } + } + + /// + /// Gets the fixed output video width, in pixels + /// + private int? Width + { + get + { + string val = QueryString["width"]; + + if (string.IsNullOrEmpty(val)) + { + return null; + } + + return int.Parse(val); + } + } + + /// + /// Gets the maximum output video height, in pixels + /// + private int? MaxHeight + { + get + { + string val = QueryString["maxheight"]; + + if (string.IsNullOrEmpty(val)) + { + return null; + } + + return int.Parse(val); + } + } + + /// + /// Gets the maximum output video width, in pixels + /// + private int? MaxWidth + { + get + { + string val = QueryString["maxwidth"]; + + if (string.IsNullOrEmpty(val)) + { + return null; + } + + return int.Parse(val); + } + } + + } +} diff --git a/MediaBrowser.Api/HttpHandlers/WeatherHandler.cs b/MediaBrowser.Api/HttpHandlers/WeatherHandler.cs new file mode 100644 index 0000000000..378e89067d --- /dev/null +++ b/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 + { + public override bool HandlesRequest(HttpListenerRequest request) + { + return ApiService.IsApiUrlMatch("weather", request); + } + + protected override Task 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 GetResponseInfo() + { + var info = await base.GetResponseInfo().ConfigureAwait(false); + + info.CacheDuration = TimeSpan.FromMinutes(15); + + return info; + } + } +} diff --git a/MediaBrowser.Api/HttpHandlers/YearHandler.cs b/MediaBrowser.Api/HttpHandlers/YearHandler.cs new file mode 100644 index 0000000000..dbd1d25be0 --- /dev/null +++ b/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 +{ + /// + /// Gets a single year + /// + [Export(typeof(BaseHandler))] + public class YearHandler : BaseSerializationHandler + { + public override bool HandlesRequest(HttpListenerRequest request) + { + return ApiService.IsApiUrlMatch("year", request); + } + + protected override Task 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)); + } + + /// + /// Gets a Year + /// + private async Task GetYear(Folder parent, User user, int year) + { + int count = 0; + + // Get all the allowed recursive children + IEnumerable 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); + } + } +} diff --git a/MediaBrowser.Api/HttpHandlers/YearsHandler.cs b/MediaBrowser.Api/HttpHandlers/YearsHandler.cs new file mode 100644 index 0000000000..7c90768e84 --- /dev/null +++ b/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 + { + public override bool HandlesRequest(HttpListenerRequest request) + { + return ApiService.IsApiUrlMatch("years", request); + } + + protected override Task GetObjectToSerialize() + { + var parent = ApiService.GetItemById(QueryString["id"]) as Folder; + User user = ApiService.GetUserById(QueryString["userid"], true); + + return GetAllYears(parent, user); + } + + /// + /// 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 + /// + private async Task GetAllYears(Folder parent, User user) + { + var data = new Dictionary(); + + // Get all the allowed recursive children + IEnumerable 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; + } + } +} diff --git a/MediaBrowser.Api/MediaBrowser.Api.csproj b/MediaBrowser.Api/MediaBrowser.Api.csproj new file mode 100644 index 0000000000..1af7e71bd0 --- /dev/null +++ b/MediaBrowser.Api/MediaBrowser.Api.csproj @@ -0,0 +1,117 @@ + + + + + Debug + AnyCPU + {4FD51AC5-2C16-4308-A993-C3A84F3B4582} + Library + Properties + MediaBrowser.Api + MediaBrowser.Api + v4.5 + 512 + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + Always + + + + + + + + False + ..\packages\Rx-Core.2.0.20823\lib\Net45\System.Reactive.Core.dll + + + False + ..\packages\Rx-Interfaces.2.0.20823\lib\Net45\System.Reactive.Interfaces.dll + + + False + ..\packages\Rx-Linq.2.0.20823\lib\Net45\System.Reactive.Linq.dll + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {9142eefa-7570-41e1-bfcc-468bb571af2f} + MediaBrowser.Common + + + {17e1f4e6-8abd-4fe5-9ecf-43d4b6087ba2} + MediaBrowser.Controller + + + {7eeeb4bb-f3e8-48fc-b4c5-70f0fff8329b} + MediaBrowser.Model + + + + + + + + xcopy "$(TargetPath)" "$(SolutionDir)\ProgramData-Server\Plugins\" /y + + + \ No newline at end of file diff --git a/MediaBrowser.Api/Plugin.cs b/MediaBrowser.Api/Plugin.cs new file mode 100644 index 0000000000..8def96da8d --- /dev/null +++ b/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"; } + } + } +} diff --git a/MediaBrowser.Api/Properties/AssemblyInfo.cs b/MediaBrowser.Api/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..c92346bac1 --- /dev/null +++ b/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")] diff --git a/MediaBrowser.Api/packages.config b/MediaBrowser.Api/packages.config new file mode 100644 index 0000000000..42f16a2676 --- /dev/null +++ b/MediaBrowser.Api/packages.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/MediaBrowser.ApiInteraction.Metro/ApiClient.cs b/MediaBrowser.ApiInteraction.Metro/ApiClient.cs new file mode 100644 index 0000000000..bf49a896ee --- /dev/null +++ b/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) + { + } + } +} diff --git a/MediaBrowser.ApiInteraction.Metro/DataSerializer.cs b/MediaBrowser.ApiInteraction.Metro/DataSerializer.cs new file mode 100644 index 0000000000..92e3f7c2b7 --- /dev/null +++ b/MediaBrowser.ApiInteraction.Metro/DataSerializer.cs @@ -0,0 +1,78 @@ +using Newtonsoft.Json; +using System; +using System.IO; + +namespace MediaBrowser.ApiInteraction +{ + public static class DataSerializer + { + /// + /// 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. + /// + private static readonly ProtobufModelSerializer ProtobufModelSerializer = new ProtobufModelSerializer(); + + public static T DeserializeFromStream(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(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; + } + } + } +} diff --git a/MediaBrowser.ApiInteraction.Metro/MediaBrowser.ApiInteraction.Metro.csproj b/MediaBrowser.ApiInteraction.Metro/MediaBrowser.ApiInteraction.Metro.csproj new file mode 100644 index 0000000000..63a91ac498 --- /dev/null +++ b/MediaBrowser.ApiInteraction.Metro/MediaBrowser.ApiInteraction.Metro.csproj @@ -0,0 +1,74 @@ + + + + + Debug + AnyCPU + {94CEA07A-307C-4663-AA43-7BD852808574} + Library + Properties + MediaBrowser.ApiInteraction.Metro + MediaBrowser.ApiInteraction.Metro + v4.5 + Profile7 + 512 + {786C830F-07A1-408B-BD7F-6EE04809D6DB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + {7eeeb4bb-f3e8-48fc-b4c5-70f0fff8329b} + MediaBrowser.Model + + + + + BaseApiClient.cs + + + BaseHttpApiClient.cs + + + SerializationFormats.cs + + + + + + + + ..\Json.Net\Portable\Newtonsoft.Json.dll + + + ..\protobuf-net\Full\portable\protobuf-net.dll + + + ..\MediaBrowser.Model\bin\ProtobufModelSerializer.dll + + + + + \ No newline at end of file diff --git a/MediaBrowser.ApiInteraction.Metro/Properties/AssemblyInfo.cs b/MediaBrowser.ApiInteraction.Metro/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..c9c876bc88 --- /dev/null +++ b/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")] diff --git a/MediaBrowser.ApiInteraction.sln b/MediaBrowser.ApiInteraction.sln new file mode 100644 index 0000000000..4484801a2b --- /dev/null +++ b/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 diff --git a/MediaBrowser.ApiInteraction/ApiClient.cs b/MediaBrowser.ApiInteraction/ApiClient.cs new file mode 100644 index 0000000000..3d5ecde22b --- /dev/null +++ b/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) }) + { + } + } +} diff --git a/MediaBrowser.ApiInteraction/BaseApiClient.cs b/MediaBrowser.ApiInteraction/BaseApiClient.cs new file mode 100644 index 0000000000..466869c765 --- /dev/null +++ b/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 +{ + /// + /// Provides api methods that are usable on all platforms + /// + public abstract class BaseApiClient : IDisposable + { + protected BaseApiClient() + { + DataSerializer.Configure(); + } + + /// + /// Gets or sets the server host name (myserver or 192.168.x.x) + /// + public string ServerHostName { get; set; } + + /// + /// Gets or sets the port number used by the API + /// + public int ServerApiPort { get; set; } + + /// + /// Gets the current api url based on hostname and port. + /// + protected string ApiUrl + { + get + { + return string.Format("http://{0}:{1}/mediabrowser/api", ServerHostName, ServerApiPort); + } + } + + /// + /// Gets the default data format to request from the server + /// + protected SerializationFormats SerializationFormat + { + get + { + return SerializationFormats.Protobuf; + } + } + + /// + /// Gets an image url that can be used to download an image from the api + /// + /// The Id of the item + /// The type of image requested + /// The image index, if there are multiple. Currently only applies to backdrops. Supply null or 0 for first backdrop. + /// 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 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; + } + + /// + /// Gets an image url that can be used to download an image from the api + /// + /// The Id of the user + /// 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 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; + } + + /// + /// Gets an image url that can be used to download an image from the api + /// + /// The name of the person + /// 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 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; + } + + /// + /// Gets an image url that can be used to download an image from the api + /// + /// The year + /// 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 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; + } + + /// + /// Gets an image url that can be used to download an image from the api + /// + /// The name of the genre + /// 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 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; + } + + /// + /// Gets an image url that can be used to download an image from the api + /// + /// The name of the studio + /// 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 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; + } + + /// + /// 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. + /// + /// A given item. + /// 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 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; + } + + /// + /// 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. + /// + /// A given item. + /// 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 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; + } + + /// + /// Gets the url needed to stream an audio file + /// + /// The id of the item + /// 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. + /// The maximum number of channels that the device can play. Omit this if it doesn't matter. Phones and tablets should generally specify 2. + /// 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. + public string GetAudioStreamUrl(Guid itemId, IEnumerable 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; + } + + /// + /// Gets the url needed to stream a video file + /// + /// The id of the item + /// 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. + /// The maximum number of channels that the device can play. Omit this if it doesn't matter. Phones and tablets should generally specify 2. + /// 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. + /// Specify this is a fixed video width is required + /// Specify this is a fixed video height is required + /// Specify this is a max video width is required + /// Specify this is a max video height is required + public string GetVideoStreamUrl(Guid itemId, + IEnumerable 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(Stream stream) + where T : class + { + return DataSerializer.DeserializeFromStream(stream, SerializationFormat); + } + + public virtual void Dispose() + { + } + } +} diff --git a/MediaBrowser.ApiInteraction/BaseHttpApiClient.cs b/MediaBrowser.ApiInteraction/BaseHttpApiClient.cs new file mode 100644 index 0000000000..8c6c1c2971 --- /dev/null +++ b/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 +{ + /// + /// Provides api methods centered around an HttpClient + /// + 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 + + /// + /// Gets an image stream based on a url + /// + public Task GetImageStreamAsync(string url) + { + return GetStreamAsync(url); + } + + /// + /// Gets a BaseItem + /// + public async Task 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(stream); + } + } + + /// + /// Gets all Users + /// + public async Task GetAllUsersAsync() + { + string url = ApiUrl + "/users"; + + using (Stream stream = await GetSerializedStreamAsync(url).ConfigureAwait(false)) + { + return DeserializeFromStream(stream); + } + } + + /// + /// Gets all Genres + /// + public async Task GetAllGenresAsync(Guid userId) + { + string url = ApiUrl + "/genres?userId=" + userId.ToString(); + + using (Stream stream = await GetSerializedStreamAsync(url).ConfigureAwait(false)) + { + return DeserializeFromStream(stream); + } + } + + /// + /// Gets in-progress items + /// + /// The user id. + /// (Optional) Specify a folder Id to localize the search to a specific folder. + public async Task 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(stream); + } + } + + /// + /// Gets recently added items + /// + /// The user id. + /// (Optional) Specify a folder Id to localize the search to a specific folder. + public async Task 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(stream); + } + } + + /// + /// Gets favorite items + /// + /// The user id. + /// (Optional) Specify a folder Id to localize the search to a specific folder. + public async Task 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(stream); + } + } + + /// + /// Gets recently added items that are unplayed. + /// + /// The user id. + /// (Optional) Specify a folder Id to localize the search to a specific folder. + public async Task 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(stream); + } + } + + /// + /// Gets all Years + /// + public async Task GetAllYearsAsync(Guid userId) + { + string url = ApiUrl + "/years?userId=" + userId.ToString(); + + using (Stream stream = await GetSerializedStreamAsync(url).ConfigureAwait(false)) + { + return DeserializeFromStream(stream); + } + } + + /// + /// Gets all items that contain a given Year + /// + /// (Optional) Specify a folder Id to localize the search to a specific folder. + public async Task 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(stream); + } + } + + /// + /// Gets all items that contain a given Genre + /// + /// (Optional) Specify a folder Id to localize the search to a specific folder. + public async Task 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(stream); + } + } + + /// + /// Gets all items that contain a given Person + /// + /// (Optional) Specify a folder Id to localize the search to a specific folder. + public async Task 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(stream); + } + } + + /// + /// Gets all items that contain a given Person + /// + /// (Optional) Specify a folder Id to localize the search to a specific folder. + public async Task 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(stream); + } + } + + /// + /// Gets all studious + /// + public async Task GetAllStudiosAsync(Guid userId) + { + string url = ApiUrl + "/studios?userId=" + userId.ToString(); + + using (Stream stream = await GetSerializedStreamAsync(url).ConfigureAwait(false)) + { + return DeserializeFromStream(stream); + } + } + + /// + /// Gets all items that contain a given Studio + /// + /// (Optional) Specify a folder Id to localize the search to a specific folder. + public async Task 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(stream); + } + } + + /// + /// Gets a studio + /// + public async Task GetStudioAsync(Guid userId, string name) + { + string url = ApiUrl + "/studio?userId=" + userId.ToString() + "&name=" + name; + + using (Stream stream = await GetSerializedStreamAsync(url).ConfigureAwait(false)) + { + return DeserializeFromStream(stream); + } + } + + /// + /// Gets a genre + /// + public async Task GetGenreAsync(Guid userId, string name) + { + string url = ApiUrl + "/genre?userId=" + userId.ToString() + "&name=" + name; + + using (Stream stream = await GetSerializedStreamAsync(url).ConfigureAwait(false)) + { + return DeserializeFromStream(stream); + } + } + + /// + /// Gets a person + /// + public async Task GetPersonAsync(Guid userId, string name) + { + string url = ApiUrl + "/person?userId=" + userId.ToString() + "&name=" + name; + + using (Stream stream = await GetSerializedStreamAsync(url).ConfigureAwait(false)) + { + return DeserializeFromStream(stream); + } + } + + /// + /// Gets a year + /// + public async Task GetYearAsync(Guid userId, int year) + { + string url = ApiUrl + "/year?userId=" + userId.ToString() + "&year=" + year; + + using (Stream stream = await GetSerializedStreamAsync(url).ConfigureAwait(false)) + { + return DeserializeFromStream(stream); + } + } + + /// + /// Gets a list of plugins installed on the server + /// + public async Task GetInstalledPluginsAsync() + { + string url = ApiUrl + "/plugins"; + + using (Stream stream = await GetSerializedStreamAsync(url).ConfigureAwait(false)) + { + return DeserializeFromStream(stream); + } + } + + /// + /// Gets a list of plugins installed on the server + /// + public Task GetPluginAssemblyAsync(PluginInfo plugin) + { + string url = ApiUrl + "/pluginassembly?assemblyfilename=" + plugin.AssemblyFileName; + + return GetStreamAsync(url); + } + + /// + /// Gets the current server configuration + /// + public async Task GetServerConfigurationAsync() + { + string url = ApiUrl + "/ServerConfiguration"; + + using (Stream stream = await GetSerializedStreamAsync(url).ConfigureAwait(false)) + { + return DeserializeFromStream(stream); + } + } + + /// + /// Gets weather information for the default location as set in configuration + /// + public async Task 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); + } + } + + /// + /// Gets the default user + /// + public async Task GetDefaultUserAsync() + { + string url = ApiUrl + "/user"; + + using (Stream stream = await GetSerializedStreamAsync(url).ConfigureAwait(false)) + { + return DeserializeFromStream(stream); + } + } + + /// + /// Gets a user by id + /// + public async Task GetUserAsync(Guid id) + { + string url = ApiUrl + "/user?id=" + id.ToString(); + + using (Stream stream = await GetSerializedStreamAsync(url).ConfigureAwait(false)) + { + return DeserializeFromStream(stream); + } + } + + /// + /// Gets weather information for the default location as set in configuration + /// + public async Task GetWeatherInfoAsync() + { + string url = ApiUrl + "/weather"; + + using (Stream stream = await GetSerializedStreamAsync(url).ConfigureAwait(false)) + { + return DeserializeFromStream(stream); + } + } + + /// + /// Gets weather information for a specific zip code + /// + public async Task GetWeatherInfoAsync(string zipCode) + { + string url = ApiUrl + "/weather?zipcode=" + zipCode; + + using (Stream stream = await GetSerializedStreamAsync(url).ConfigureAwait(false)) + { + return DeserializeFromStream(stream); + } + } + + /// + /// Gets special features for a Movie + /// + public async Task GetMovieSpecialFeaturesAsync(Guid itemId, Guid userId) + { + string url = ApiUrl + "/MovieSpecialFeatures?id=" + itemId; + url += "&userid=" + userId; + + using (Stream stream = await GetSerializedStreamAsync(url).ConfigureAwait(false)) + { + return DeserializeFromStream(stream); + } + } + + /// + /// Updates played status for an item + /// + public async Task 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(stream); + } + } + + /// + /// Updates a user's favorite status for an item and returns the updated UserItemData object. + /// + public async Task 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(stream); + } + } + + /// + /// Clears a user's rating for an item + /// + public async Task 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(stream); + } + } + + /// + /// Updates a user's rating for an item, based on likes or dislikes + /// + public async Task 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(stream); + } + } + + /// + /// Authenticates a user and returns the result + /// + public async Task 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(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(stream); + } +#endif + } + + /// + /// This is a helper around getting a stream from the server that contains serialized data + /// + private Task GetSerializedStreamAsync(string url) + { + return GetSerializedStreamAsync(url, SerializationFormat); + } + + /// + /// This is a helper around getting a stream from the server that contains serialized data + /// + private Task GetSerializedStreamAsync(string url, SerializationFormats serializationFormat) + { + if (url.IndexOf('?') == -1) + { + url += "?dataformat=" + serializationFormat.ToString(); + } + else + { + url += "&dataformat=" + serializationFormat.ToString(); + } + + return GetStreamAsync(url); + } + + /// + /// This is just a helper around HttpClient + /// + private Task 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 + } + } +} diff --git a/MediaBrowser.ApiInteraction/DataSerializer.cs b/MediaBrowser.ApiInteraction/DataSerializer.cs new file mode 100644 index 0000000000..3c3f8fae2f --- /dev/null +++ b/MediaBrowser.ApiInteraction/DataSerializer.cs @@ -0,0 +1,77 @@ +using ServiceStack.Text; +using System; +using System.IO; + +namespace MediaBrowser.ApiInteraction +{ + public static class DataSerializer + { + /// + /// 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. + /// + private static readonly ProtobufModelSerializer ProtobufModelSerializer = new ProtobufModelSerializer(); + + /// + /// Deserializes an object using generics + /// + public static T DeserializeFromStream(Stream stream, SerializationFormats format) + where T : class + { + if (format == SerializationFormats.Protobuf) + { + //return Serializer.Deserialize(stream); + return ProtobufModelSerializer.Deserialize(stream, null, typeof(T)) as T; + } + if (format == SerializationFormats.Jsv) + { + return TypeSerializer.DeserializeFromStream(stream); + } + if (format == SerializationFormats.Json) + { + return JsonSerializer.DeserializeFromStream(stream); + } + + throw new NotImplementedException(); + } + + /// + /// Deserializes an object + /// + 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; + } + } + } +} diff --git a/MediaBrowser.ApiInteraction/MediaBrowser.ApiInteraction.csproj b/MediaBrowser.ApiInteraction/MediaBrowser.ApiInteraction.csproj new file mode 100644 index 0000000000..d38a25a074 --- /dev/null +++ b/MediaBrowser.ApiInteraction/MediaBrowser.ApiInteraction.csproj @@ -0,0 +1,78 @@ + + + + + Debug + AnyCPU + {921C0F64-FDA7-4E9F-9E73-0CB0EEDB2422} + Library + Properties + MediaBrowser.ApiInteraction + MediaBrowser.ApiInteraction + v4.5 + 512 + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + ..\protobuf-net\Full\net30\protobuf-net.dll + + + ..\MediaBrowser.Model\bin\ProtobufModelSerializer.dll + + + False + ..\packages\ServiceStack.Text.3.9.9\lib\net35\ServiceStack.Text.dll + + + + + + + + + + + + + + + + + + + + + + {7eeeb4bb-f3e8-48fc-b4c5-70f0fff8329b} + MediaBrowser.Model + + + + + + + + \ No newline at end of file diff --git a/MediaBrowser.ApiInteraction/Properties/AssemblyInfo.cs b/MediaBrowser.ApiInteraction/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..74742759f3 --- /dev/null +++ b/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")] diff --git a/MediaBrowser.ApiInteraction/SerializationFormats.cs b/MediaBrowser.ApiInteraction/SerializationFormats.cs new file mode 100644 index 0000000000..21eb210d05 --- /dev/null +++ b/MediaBrowser.ApiInteraction/SerializationFormats.cs @@ -0,0 +1,10 @@ + +namespace MediaBrowser.ApiInteraction +{ + public enum SerializationFormats + { + Json, + Jsv, + Protobuf + } +} diff --git a/MediaBrowser.ApiInteraction/packages.config b/MediaBrowser.ApiInteraction/packages.config new file mode 100644 index 0000000000..05294421d4 --- /dev/null +++ b/MediaBrowser.ApiInteraction/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/MediaBrowser.Common/Events/GenericEventArgs.cs b/MediaBrowser.Common/Events/GenericEventArgs.cs new file mode 100644 index 0000000000..98e072816f --- /dev/null +++ b/MediaBrowser.Common/Events/GenericEventArgs.cs @@ -0,0 +1,12 @@ +using System; + +namespace MediaBrowser.Common.Events +{ + /// + /// Provides a generic EventArgs subclass that can hold any kind of object + /// + public class GenericEventArgs : EventArgs + { + public T Argument { get; set; } + } +} diff --git a/MediaBrowser.Common/Extensions/BaseExtensions.cs b/MediaBrowser.Common/Extensions/BaseExtensions.cs new file mode 100644 index 0000000000..77eb9fbb4a --- /dev/null +++ b/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))); + } + } + + /// + /// Examine a list of strings assumed to be file paths to see if it contains a parent of + /// the provided path. + /// + /// + /// + /// + public static bool ContainsParentFolder(this List 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; + } + + /// + /// Helper method for Dictionaries since they throw on not-found keys + /// + /// + /// + /// + /// + /// + /// + public static U GetValueOrDefault(this Dictionary dictionary, T key, U defaultValue) + { + U val; + if (!dictionary.TryGetValue(key, out val)) + { + val = defaultValue; + } + return val; + + } + + } +} diff --git a/MediaBrowser.Common/Kernel/BaseApplicationPaths.cs b/MediaBrowser.Common/Kernel/BaseApplicationPaths.cs new file mode 100644 index 0000000000..fefbd354a1 --- /dev/null +++ b/MediaBrowser.Common/Kernel/BaseApplicationPaths.cs @@ -0,0 +1,154 @@ +using System.Configuration; +using System.IO; +using System.Reflection; + +namespace MediaBrowser.Common.Kernel +{ + /// + /// 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. + /// + public abstract class BaseApplicationPaths + { + private string _programDataPath; + /// + /// Gets the path to the program data folder + /// + public string ProgramDataPath + { + get + { + if (_programDataPath == null) + { + _programDataPath = GetProgramDataPath(); + } + + return _programDataPath; + } + } + + private string _pluginsPath; + /// + /// Gets the path to the plugin directory + /// + public string PluginsPath + { + get + { + if (_pluginsPath == null) + { + _pluginsPath = Path.Combine(ProgramDataPath, "plugins"); + if (!Directory.Exists(_pluginsPath)) + { + Directory.CreateDirectory(_pluginsPath); + } + } + + return _pluginsPath; + } + } + + private string _pluginConfigurationsPath; + /// + /// Gets the path to the plugin configurations directory + /// + public string PluginConfigurationsPath + { + get + { + if (_pluginConfigurationsPath == null) + { + _pluginConfigurationsPath = Path.Combine(PluginsPath, "configurations"); + if (!Directory.Exists(_pluginConfigurationsPath)) + { + Directory.CreateDirectory(_pluginConfigurationsPath); + } + } + + return _pluginConfigurationsPath; + } + } + + private string _logDirectoryPath; + /// + /// Gets the path to the log directory + /// + public string LogDirectoryPath + { + get + { + if (_logDirectoryPath == null) + { + _logDirectoryPath = Path.Combine(ProgramDataPath, "logs"); + if (!Directory.Exists(_logDirectoryPath)) + { + Directory.CreateDirectory(_logDirectoryPath); + } + } + return _logDirectoryPath; + } + } + + private string _configurationDirectoryPath; + /// + /// Gets the path to the application configuration root directory + /// + public string ConfigurationDirectoryPath + { + get + { + if (_configurationDirectoryPath == null) + { + _configurationDirectoryPath = Path.Combine(ProgramDataPath, "config"); + if (!Directory.Exists(_configurationDirectoryPath)) + { + Directory.CreateDirectory(_configurationDirectoryPath); + } + } + return _configurationDirectoryPath; + } + } + + private string _systemConfigurationFilePath; + /// + /// Gets the path to the system configuration file + /// + public string SystemConfigurationFilePath + { + get + { + if (_systemConfigurationFilePath == null) + { + _systemConfigurationFilePath = Path.Combine(ConfigurationDirectoryPath, "system.xml"); + } + return _systemConfigurationFilePath; + } + } + + /// + /// Gets the path to the application's ProgramDataFolder + /// + 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; + } + } +} diff --git a/MediaBrowser.Common/Kernel/BaseKernel.cs b/MediaBrowser.Common/Kernel/BaseKernel.cs new file mode 100644 index 0000000000..a6081a6881 --- /dev/null +++ b/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 +{ + /// + /// Represents a shared base kernel for both the Ui and server apps + /// + public abstract class BaseKernel : IDisposable, IKernel + where TConfigurationType : BaseApplicationConfiguration, new() + where TApplicationPathsType : BaseApplicationPaths, new() + { + #region ReloadBeginning Event + /// + /// Fires whenever the kernel begins reloading + /// + public event EventHandler>> ReloadBeginning; + private void OnReloadBeginning(IProgress progress) + { + if (ReloadBeginning != null) + { + ReloadBeginning(this, new GenericEventArgs> { Argument = progress }); + } + } + #endregion + + #region ReloadCompleted Event + /// + /// Fires whenever the kernel completes reloading + /// + public event EventHandler>> ReloadCompleted; + private void OnReloadCompleted(IProgress progress) + { + if (ReloadCompleted != null) + { + ReloadCompleted(this, new GenericEventArgs> { Argument = progress }); + } + } + #endregion + + /// + /// Gets the current configuration + /// + public TConfigurationType Configuration { get; private set; } + + public TApplicationPathsType ApplicationPaths { get; private set; } + + /// + /// Gets the list of currently loaded plugins + /// + [ImportMany(typeof(BasePlugin))] + public IEnumerable Plugins { get; private set; } + + /// + /// Gets the list of currently registered http handlers + /// + [ImportMany(typeof(BaseHandler))] + private IEnumerable HttpHandlers { get; set; } + + /// + /// Gets the list of currently registered Loggers + /// + [ImportMany(typeof(BaseLogger))] + public IEnumerable Loggers { get; set; } + + /// + /// 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. + /// + public HttpServer HttpServer { get; private set; } + + /// + /// This subscribes to HttpListener requests and finds the appropate BaseHandler to process it + /// + private IDisposable HttpListener { get; set; } + + /// + /// Gets the MEF CompositionContainer + /// + private CompositionContainer CompositionContainer { get; set; } + + protected virtual string HttpServerUrlPrefix + { + get + { + return "http://+:" + Configuration.HttpServerPortNumber + "/mediabrowser/"; + } + } + + /// + /// Gets the kernel context. Subclasses will have to override. + /// + public abstract KernelContext KernelContext { get; } + + /// + /// Initializes the Kernel + /// + public async Task Init(IProgress 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); + } + + /// + /// Performs initializations that only occur once + /// + protected virtual void InitializeInternal(IProgress progress) + { + ApplicationPaths = new TApplicationPathsType(); + + ReportProgress(progress, "Loading Configuration"); + ReloadConfiguration(); + + ReportProgress(progress, "Loading Http Server"); + ReloadHttpServer(); + } + + /// + /// Performs initializations that can be reloaded at anytime + /// + public async Task Reload(IProgress progress) + { + OnReloadBeginning(progress); + + await ReloadInternal(progress).ConfigureAwait(false); + + OnReloadCompleted(progress); + + ReportProgress(progress, "Kernel.Reload Complete"); + } + + /// + /// Performs initializations that can be reloaded at anytime + /// + protected virtual async Task ReloadInternal(IProgress progress) + { + await Task.Run(() => + { + ReportProgress(progress, "Loading Plugins"); + ReloadComposableParts(); + + }).ConfigureAwait(false); + } + + /// + /// Uses MEF to locate plugins + /// Subclasses can use this to locate types within plugins + /// + private void ReloadComposableParts() + { + DisposeComposableParts(); + + CompositionContainer = GetCompositionContainer(includeCurrentAssembly: true); + + CompositionContainer.ComposeParts(this); + + OnComposablePartsLoaded(); + + CompositionContainer.Catalog.Dispose(); + } + + /// + /// Constructs an MEF CompositionContainer based on the current running assembly and all plugin assemblies + /// + 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 pluginAssemblies = Directory.GetFiles(ApplicationPaths.PluginsPath, "*.dll", SearchOption.TopDirectoryOnly).Select(f => Assembly.Load(File.ReadAllBytes((f)))); + + var catalogs = new List(); + + 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); + } + + /// + /// Fires after MEF finishes finding composable parts within plugin assemblies + /// + protected virtual void OnComposablePartsLoaded() + { + foreach (var logger in Loggers) + { + logger.Initialize(this); + } + + // Start-up each plugin + foreach (var plugin in Plugins) + { + plugin.Initialize(this); + } + } + + /// + /// Reloads application configuration from the config file + /// + 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(ApplicationPaths.SystemConfigurationFilePath); + } + catch (FileNotFoundException) + { + Configuration = new TConfigurationType(); + XmlSerializer.SerializeToFile(Configuration, ApplicationPaths.SystemConfigurationFilePath); + } + } + + /// + /// Restarts the Http Server, or starts it if not currently running + /// + 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); + } + }); + } + + /// + /// Disposes all resources currently in use. + /// + public virtual void Dispose() + { + Logger.LogInfo("Beginning Kernel.Dispose"); + + DisposeHttpServer(); + + DisposeComposableParts(); + } + + /// + /// Disposes all objects gathered through MEF composable parts + /// + protected virtual void DisposeComposableParts() + { + if (CompositionContainer != null) + { + CompositionContainer.Dispose(); + } + } + + /// + /// Disposes the current HttpServer + /// + private void DisposeHttpServer() + { + if (HttpServer != null) + { + Logger.LogInfo("Disposing Http Server"); + + HttpServer.Dispose(); + } + + if (HttpListener != null) + { + HttpListener.Dispose(); + } + } + + /// + /// Gets the current application version + /// + public Version ApplicationVersion + { + get + { + return GetType().Assembly.GetName().Version; + } + } + + protected void ReportProgress(IProgress 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 progress); + Task Reload(IProgress progress); + IEnumerable Loggers { get; } + void Dispose(); + } +} diff --git a/MediaBrowser.Common/Kernel/KernelContext.cs b/MediaBrowser.Common/Kernel/KernelContext.cs new file mode 100644 index 0000000000..4d13ebb7b0 --- /dev/null +++ b/MediaBrowser.Common/Kernel/KernelContext.cs @@ -0,0 +1,9 @@ + +namespace MediaBrowser.Common.Kernel +{ + public enum KernelContext + { + Server, + Ui + } +} diff --git a/MediaBrowser.Common/Logging/BaseLogger.cs b/MediaBrowser.Common/Logging/BaseLogger.cs new file mode 100644 index 0000000000..a97bc201f2 --- /dev/null +++ b/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); + } + } +} diff --git a/MediaBrowser.Common/Logging/LogRow.cs b/MediaBrowser.Common/Logging/LogRow.cs new file mode 100644 index 0000000000..6fecef59cc --- /dev/null +++ b/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(); + + 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] "); + } + } +} diff --git a/MediaBrowser.Common/Logging/LogSeverity.cs b/MediaBrowser.Common/Logging/LogSeverity.cs new file mode 100644 index 0000000000..97abfe7b58 --- /dev/null +++ b/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 + } +} \ No newline at end of file diff --git a/MediaBrowser.Common/Logging/Logger.cs b/MediaBrowser.Common/Logging/Logger.cs new file mode 100644 index 0000000000..9ac02fe3ea --- /dev/null +++ b/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; + } + } +} diff --git a/MediaBrowser.Common/Logging/TraceFileLogger.cs b/MediaBrowser.Common/Logging/TraceFileLogger.cs new file mode 100644 index 0000000000..7ab67a137e --- /dev/null +++ b/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()); + } + } +} diff --git a/MediaBrowser.Common/MediaBrowser.Common.csproj b/MediaBrowser.Common/MediaBrowser.Common.csproj new file mode 100644 index 0000000000..c08716614f --- /dev/null +++ b/MediaBrowser.Common/MediaBrowser.Common.csproj @@ -0,0 +1,164 @@ + + + + + Debug + AnyCPU + {9142EEFA-7570-41E1-BFCC-468BB571AF2F} + Library + Properties + MediaBrowser.Common + MediaBrowser.Common + v4.5 + {60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + 512 + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + Resources\Images\Icon.ico + + + + ..\packages\MahApps.Metro.0.9.0.0\lib\net40\MahApps.Metro.dll + + + + + ..\protobuf-net\Full\net30\protobuf-net.dll + + + ..\MediaBrowser.Model\bin\ProtobufModelSerializer.dll + + + False + ..\packages\ServiceStack.Text.3.9.9\lib\net35\ServiceStack.Text.dll + + + + + + + + False + ..\packages\Rx-Core.2.0.20823\lib\Net45\System.Reactive.Core.dll + + + False + ..\packages\Rx-Interfaces.2.0.20823\lib\Net45\System.Reactive.Interfaces.dll + + + False + ..\packages\Rx-Linq.2.0.20823\lib\Net45\System.Reactive.Linq.dll + + + + + + ..\packages\MahApps.Metro.0.9.0.0\lib\net40\System.Windows.Interactivity.dll + + + + + + + + + + + + + + + + + + + + + + True + True + Resources.resx + + + + + + + + + + + + + + + + + + + Splash.xaml + + + + + + + + + + {7eeeb4bb-f3e8-48fc-b4c5-70f0fff8329b} + MediaBrowser.Model + + + + + Designer + MSBuild:Compile + + + + + ResXFileCodeGenerator + Resources.Designer.cs + Designer + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/MediaBrowser.Common/Mef/MefUtils.cs b/MediaBrowser.Common/Mef/MefUtils.cs new file mode 100644 index 0000000000..55d8886978 --- /dev/null +++ b/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 + { + /// + /// 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. + /// + public static CompositionContainer GetSafeCompositionContainer(IEnumerable catalogs) + { + var newList = new List(); + + // 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)); + } + } +} diff --git a/MediaBrowser.Common/Net/Handlers/BaseEmbeddedResourceHandler.cs b/MediaBrowser.Common/Net/Handlers/BaseEmbeddedResourceHandler.cs new file mode 100644 index 0000000000..579e341fec --- /dev/null +++ b/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(); + } +} diff --git a/MediaBrowser.Common/Net/Handlers/BaseHandler.cs b/MediaBrowser.Common/Net/Handlers/BaseHandler.cs new file mode 100644 index 0000000000..a5058e6caf --- /dev/null +++ b/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; + } + } + + /// + /// The original HttpListenerContext + /// + protected HttpListenerContext HttpListenerContext { get; set; } + + /// + /// The original QueryString + /// + protected NameValueCollection QueryString + { + get + { + return HttpListenerContext.Request.QueryString; + } + } + + private List> _requestedRanges; + protected IEnumerable> RequestedRanges + { + get + { + if (_requestedRanges == null) + { + _requestedRanges = new List>(); + + 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(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; + } + + /// + /// When the browser sends the IfModifiedDate, it's precision is limited to seconds, so this will account for that + /// + 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 GetResponseInfo(); + + private Hashtable _formValues; + + /// + /// Gets a value from form POST data + /// + protected async Task GetFormValue(string name) + { + if (_formValues == null) + { + _formValues = await GetFormValues(HttpListenerContext.Request).ConfigureAwait(false); + } + + if (_formValues.ContainsKey(name)) + { + return _formValues[name].ToString(); + } + + return null; + } + + /// + /// Extracts form POST data from a request + /// + private async Task 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; + } + } + } +} \ No newline at end of file diff --git a/MediaBrowser.Common/Net/Handlers/BaseSerializationHandler.cs b/MediaBrowser.Common/Net/Handlers/BaseSerializationHandler.cs new file mode 100644 index 0000000000..53b3ee817f --- /dev/null +++ b/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 : 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 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 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 + } + +} diff --git a/MediaBrowser.Common/Net/Handlers/StaticFileHandler.cs b/MediaBrowser.Common/Net/Handlers/StaticFileHandler.cs new file mode 100644 index 0000000000..11438b164b --- /dev/null +++ b/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 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(info); + } + + protected override Task WriteResponseToOutputStream(Stream stream) + { + if (IsRangeRequest) + { + KeyValuePair 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(); + } + } + + /// + /// Handles a range request of "bytes=0-" + /// This will serve the complete content and add the content-range header + /// + private Task ServeCompleteRangeRequest(KeyValuePair 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); + } + + /// + /// Serves a partial range request where the total content length is not known + /// + private async Task ServePartialRangeRequestWithUnknownTotalContentLength(KeyValuePair 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); + } + + /// + /// Serves a partial range request where the total content length is already known + /// + private async Task ServePartialRangeRequestWithKnownTotalContentLength(KeyValuePair 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); + } + + /// + /// Reads bytes from a stream + /// + /// The input stream + /// The starting position + /// The number of bytes to read, or null to read to the end. + private async Task 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(); + } + } + + } + } +} diff --git a/MediaBrowser.Common/Net/HttpServer.cs b/MediaBrowser.Common/Net/HttpServer.cs new file mode 100644 index 0000000000..276e14eb31 --- /dev/null +++ b/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, IDisposable + { + private readonly HttpListener _listener; + private readonly IObservable _stream; + + public HttpServer(string url) + { + _listener = new HttpListener(); + _listener.Prefixes.Add(url); + _listener.Start(); + _stream = ObservableHttpContext(); + } + + private IObservable ObservableHttpContext() + { + return Observable.Create(obs => + Observable.FromAsync(() => _listener.GetContextAsync()) + .Subscribe(obs)) + .Repeat() + .Retry() + .Publish() + .RefCount(); + } + public void Dispose() + { + _listener.Stop(); + } + + public IDisposable Subscribe(IObserver observer) + { + return _stream.Subscribe(observer); + } + } +} \ No newline at end of file diff --git a/MediaBrowser.Common/Net/MimeTypes.cs b/MediaBrowser.Common/Net/MimeTypes.cs new file mode 100644 index 0000000000..fb85b0f2a3 --- /dev/null +++ b/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); + } + } +} diff --git a/MediaBrowser.Common/Net/Request.cs b/MediaBrowser.Common/Net/Request.cs new file mode 100644 index 0000000000..795c9c36ba --- /dev/null +++ b/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> Headers { get; set; } + public Stream InputStream { get; set; } + public string RawUrl { get; set; } + public int ContentLength + { + get { return int.Parse(Headers["Content-Length"].First()); } + } + } +} \ No newline at end of file diff --git a/MediaBrowser.Common/Plugins/BasePlugin.cs b/MediaBrowser.Common/Plugins/BasePlugin.cs new file mode 100644 index 0000000000..70e573817b --- /dev/null +++ b/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 +{ + /// + /// Provides a common base class for all plugins + /// + public abstract class BasePlugin : IDisposable + { + protected IKernel Kernel { get; private set; } + + /// + /// Gets or sets the plugin's current context + /// + protected KernelContext Context { get { return Kernel.KernelContext; } } + + /// + /// Gets the name of the plugin + /// + public abstract string Name { get; } + + /// + /// Gets the type of configuration this plugin uses + /// + public virtual Type ConfigurationType + { + get { return typeof (BasePluginConfiguration); } + } + + /// + /// Gets the plugin version + /// + public Version Version + { + get + { + return GetType().Assembly.GetName().Version; + } + } + + /// + /// Gets the name the assembly file + /// + 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; + } + } + + /// + /// Gets the path to the assembly file + /// + public string AssemblyFilePath + { + get + { + return Path.Combine(Kernel.ApplicationPaths.PluginsPath, AssemblyFileName); + } + } + + /// + /// Gets or sets the current plugin configuration + /// + public BasePluginConfiguration Configuration { get; protected set; } + + /// + /// Gets the name of the configuration file. Subclasses should override + /// + public virtual string ConfigurationFileName + { + get + { + return Name.Replace(" ", string.Empty) + ".xml"; + } + } + + /// + /// Gets the full path to the configuration file + /// + public string ConfigurationFilePath + { + get + { + return Path.Combine(Kernel.ApplicationPaths.PluginConfigurationsPath, ConfigurationFileName); + } + } + + private string _dataFolderPath; + /// + /// Gets the full path to the data folder, where the plugin can store any miscellaneous files needed + /// + 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; + } + } + + /// + /// Returns true or false indicating if the plugin should be downloaded and run within the Ui. + /// + public virtual bool DownloadToUi + { + get + { + return false; + } + } + + public void Initialize(IKernel kernel) + { + Initialize(kernel, true); + } + + /// + /// Starts the plugin. + /// + 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(); + } + } + } + } + + /// + /// Starts the plugin on the server + /// + protected virtual void InitializeOnServer() + { + } + + /// + /// Starts the plugin in the Ui + /// + protected virtual void InitializeInUi() + { + } + + /// + /// Disposes the plugins. Undos all actions performed during Init. + /// + public void Dispose() + { + Logger.LogInfo("Disposing {0} Plugin", Name); + + if (Context == KernelContext.Server) + { + DisposeOnServer(); + } + else if (Context == KernelContext.Ui) + { + InitializeInUi(); + } + } + + /// + /// Disposes the plugin on the server + /// + protected virtual void DisposeOnServer() + { + } + + /// + /// Disposes the plugin in the Ui + /// + 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; + } + } +} diff --git a/MediaBrowser.Common/Plugins/BaseTheme.cs b/MediaBrowser.Common/Plugins/BaseTheme.cs new file mode 100644 index 0000000000..32a28258bc --- /dev/null +++ b/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; + } + } + + /// + /// Gets the MEF CompositionContainer + /// + private CompositionContainer CompositionContainer { get; set; } + + /// + /// Gets the list of global resources + /// + [ImportMany(typeof(ResourceDictionary))] + public IEnumerable GlobalResources { get; private set; } + + /// + /// Gets the list of pages + /// + [ImportMany(typeof(Page))] + public IEnumerable Pages { get; private set; } + + /// + /// Gets the pack Uri of the Login page + /// + 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); + } + } +} diff --git a/MediaBrowser.Common/Properties/AssemblyInfo.cs b/MediaBrowser.Common/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..ff70e8db7f --- /dev/null +++ b/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")] diff --git a/MediaBrowser.Common/Properties/Resources.Designer.cs b/MediaBrowser.Common/Properties/Resources.Designer.cs new file mode 100644 index 0000000000..f39a1c1d97 --- /dev/null +++ b/MediaBrowser.Common/Properties/Resources.Designer.cs @@ -0,0 +1,63 @@ +//------------------------------------------------------------------------------ +// +// 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. +// +//------------------------------------------------------------------------------ + +namespace MediaBrowser.Common.Properties { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // 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() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [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; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + } +} diff --git a/MediaBrowser.Common/Properties/Resources.resx b/MediaBrowser.Common/Properties/Resources.resx new file mode 100644 index 0000000000..7c0911ec14 --- /dev/null +++ b/MediaBrowser.Common/Properties/Resources.resx @@ -0,0 +1,121 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + \ No newline at end of file diff --git a/MediaBrowser.Common/Resources/Images/Icon.ico b/MediaBrowser.Common/Resources/Images/Icon.ico new file mode 100644 index 0000000000..1541dabdc4 Binary files /dev/null and b/MediaBrowser.Common/Resources/Images/Icon.ico differ diff --git a/MediaBrowser.Common/Resources/Images/mblogoblack.png b/MediaBrowser.Common/Resources/Images/mblogoblack.png new file mode 100644 index 0000000000..84323fe525 Binary files /dev/null and b/MediaBrowser.Common/Resources/Images/mblogoblack.png differ diff --git a/MediaBrowser.Common/Resources/Images/mblogowhite.png b/MediaBrowser.Common/Resources/Images/mblogowhite.png new file mode 100644 index 0000000000..a39812e35c Binary files /dev/null and b/MediaBrowser.Common/Resources/Images/mblogowhite.png differ diff --git a/MediaBrowser.Common/Resources/Images/spinner.gif b/MediaBrowser.Common/Resources/Images/spinner.gif new file mode 100644 index 0000000000..d0bce15423 Binary files /dev/null and b/MediaBrowser.Common/Resources/Images/spinner.gif differ diff --git a/MediaBrowser.Common/Serialization/JsonSerializer.cs b/MediaBrowser.Common/Serialization/JsonSerializer.cs new file mode 100644 index 0000000000..f5d2abe33f --- /dev/null +++ b/MediaBrowser.Common/Serialization/JsonSerializer.cs @@ -0,0 +1,74 @@ +using System; +using System.IO; + +namespace MediaBrowser.Common.Serialization +{ + /// + /// Provides a wrapper around third party json serialization. + /// + public class JsonSerializer + { + public static void SerializeToStream(T obj, Stream stream) + { + Configure(); + + ServiceStack.Text.JsonSerializer.SerializeToStream(obj, stream); + } + + public static void SerializeToFile(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(string file) + { + Configure(); + + using (Stream stream = File.OpenRead(file)) + { + return ServiceStack.Text.JsonSerializer.DeserializeFromStream(stream); + } + } + + public static T DeserializeFromStream(Stream stream) + { + Configure(); + + return ServiceStack.Text.JsonSerializer.DeserializeFromStream(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; + } + } + } +} diff --git a/MediaBrowser.Common/Serialization/JsvSerializer.cs b/MediaBrowser.Common/Serialization/JsvSerializer.cs new file mode 100644 index 0000000000..41e5ea800a --- /dev/null +++ b/MediaBrowser.Common/Serialization/JsvSerializer.cs @@ -0,0 +1,44 @@ +using System; +using System.IO; + +namespace MediaBrowser.Common.Serialization +{ + /// + /// 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 + /// + public static class JsvSerializer + { + public static void SerializeToStream(T obj, Stream stream) + { + ServiceStack.Text.TypeSerializer.SerializeToStream(obj, stream); + } + + public static T DeserializeFromStream(Stream stream) + { + return ServiceStack.Text.TypeSerializer.DeserializeFromStream(stream); + } + + public static object DeserializeFromStream(Stream stream, Type type) + { + return ServiceStack.Text.TypeSerializer.DeserializeFromStream(type, stream); + } + + public static void SerializeToFile(T obj, string file) + { + using (Stream stream = File.Open(file, FileMode.Create)) + { + SerializeToStream(obj, stream); + } + } + + public static T DeserializeFromFile(string file) + { + using (Stream stream = File.OpenRead(file)) + { + return DeserializeFromStream(stream); + } + } + } +} diff --git a/MediaBrowser.Common/Serialization/ProtobufSerializer.cs b/MediaBrowser.Common/Serialization/ProtobufSerializer.cs new file mode 100644 index 0000000000..1c79a272d5 --- /dev/null +++ b/MediaBrowser.Common/Serialization/ProtobufSerializer.cs @@ -0,0 +1,53 @@ +using System; +using System.IO; + +namespace MediaBrowser.Common.Serialization +{ + /// + /// 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. + /// + public static class ProtobufSerializer + { + /// + /// 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. + /// + private static readonly ProtobufModelSerializer ProtobufModelSerializer = new ProtobufModelSerializer(); + + public static void SerializeToStream(T obj, Stream stream) + { + ProtobufModelSerializer.Serialize(stream, obj); + } + + public static T DeserializeFromStream(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 obj, string file) + { + using (Stream stream = File.Open(file, FileMode.Create)) + { + SerializeToStream(obj, stream); + } + } + + public static T DeserializeFromFile(string file) + where T : class + { + using (Stream stream = File.OpenRead(file)) + { + return DeserializeFromStream(stream); + } + } + } +} diff --git a/MediaBrowser.Common/Serialization/XmlSerializer.cs b/MediaBrowser.Common/Serialization/XmlSerializer.cs new file mode 100644 index 0000000000..11ef17c3de --- /dev/null +++ b/MediaBrowser.Common/Serialization/XmlSerializer.cs @@ -0,0 +1,58 @@ +using System; +using System.IO; + +namespace MediaBrowser.Common.Serialization +{ + /// + /// Provides a wrapper around third party xml serialization. + /// + public class XmlSerializer + { + public static void SerializeToStream(T obj, Stream stream) + { + ServiceStack.Text.XmlSerializer.SerializeToStream(obj, stream); + } + + public static T DeserializeFromStream(Stream stream) + { + return ServiceStack.Text.XmlSerializer.DeserializeFromStream(stream); + } + + public static object DeserializeFromStream(Type type, Stream stream) + { + return ServiceStack.Text.XmlSerializer.DeserializeFromStream(type, stream); + } + + public static void SerializeToFile(T obj, string file) + { + using (var stream = new FileStream(file, FileMode.Create)) + { + SerializeToStream(obj, stream); + } + } + + public static T DeserializeFromFile(string file) + { + using (Stream stream = File.OpenRead(file)) + { + return DeserializeFromStream(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); + } + } + } +} diff --git a/MediaBrowser.Common/UI/BaseApplication.cs b/MediaBrowser.Common/UI/BaseApplication.cs new file mode 100644 index 0000000000..c3792c714a --- /dev/null +++ b/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 +{ + /// + /// Serves as a base Application class for both the UI and Server apps. + /// + 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(); + + 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 args) + { + OnSecondInstanceLaunched(args); + + return true; + } + + protected virtual void OnSecondInstanceLaunched(IList args) + { + if (this.MainWindow.WindowState == WindowState.Minimized) + { + this.MainWindow.WindowState = WindowState.Maximized; + } + } + + public static void RunApplication(string uniqueKey) + where TApplicationType : BaseApplication, IApplication, new() + { + if (SingleInstance.InitializeAsFirstInstance(uniqueKey)) + { + var application = new TApplicationType(); + application.InitializeComponent(); + + application.Run(); + + // Allow single instance code to perform cleanup operations + SingleInstance.Cleanup(); + } + } + } + + public interface IApplication + { + void InitializeComponent(); + } +} diff --git a/MediaBrowser.Common/UI/SingleInstance.cs b/MediaBrowser.Common/UI/SingleInstance.cs new file mode 100644 index 0000000000..3fc85a74ec --- /dev/null +++ b/MediaBrowser.Common/UI/SingleInstance.cs @@ -0,0 +1,484 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +// +// This class checks to make sure that only one instance of +// this application is running at a time. +// +//----------------------------------------------------------------------- + +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 + { + /// + /// Delegate declaration that matches WndProc signatures. + /// + 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 args); + } + + /// + /// This class checks to make sure that only one instance of + /// this application is running at a time. + /// + /// + /// 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. + /// + public static class SingleInstance + where TApplication : Application, ISingleInstanceApp + { + #region Private Fields + + /// + /// String delimiter used in channel names. + /// + private const string Delimiter = ":"; + + /// + /// Suffix to the channel name. + /// + private const string ChannelNameSuffix = "SingeInstanceIPCChannel"; + + /// + /// Remote service name. + /// + private const string RemoteServiceName = "SingleInstanceApplicationService"; + + /// + /// IPC protocol used (string). + /// + private const string IpcProtocol = "ipc://"; + + /// + /// Application mutex. + /// + private static Mutex singleInstanceMutex; + + /// + /// IPC channel for communications. + /// + private static IpcServerChannel channel; + + /// + /// List of command line arguments for the application. + /// + private static IList commandLineArgs; + + #endregion + + #region Public Properties + + /// + /// Gets list of command line arguments for the application. + /// + public static IList CommandLineArgs + { + get { return commandLineArgs; } + } + + #endregion + + #region Public Methods + + /// + /// Checks if the instance of the application attempting to start is the first instance. + /// If not, activates the first instance. + /// + /// True if this is the first instance of the application. + 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; + } + + /// + /// Cleans up single-instance code, clearing shared resources, mutexes, etc. + /// + public static void Cleanup() + { + if (singleInstanceMutex != null) + { + singleInstanceMutex.Close(); + singleInstanceMutex = null; + } + + if (channel != null) + { + ChannelServices.UnregisterChannel(channel); + channel = null; + } + } + + #endregion + + #region Private Methods + + /// + /// Gets command line args - for ClickOnce deployed applications, command line args may not be passed directly, they have to be retrieved. + /// + /// List of command line arg strings. + private static IList 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(args); + } + + /// + /// Creates a remote service for communication. + /// + /// Application's IPC channel name. + private static void CreateRemoteService(string channelName) + { + var serverProvider = new BinaryServerFormatterSinkProvider { }; + serverProvider.TypeFilterLevel = TypeFilterLevel.Full; + IDictionary props = new Dictionary(); + + 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); + } + + /// + /// 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. + /// + /// Application's IPC channel name. + /// + /// Command line arguments for the second instance, passed to the first instance to take appropriate action. + /// + private static void SignalFirstInstance(string channelName, IList 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); + } + } + + /// + /// Callback for activating first instance of the application. + /// + /// Callback argument. + /// Always null. + private static object ActivateFirstInstanceCallback(object arg) + { + // Get command line args to be passed to first instance + var args = arg as IList; + ActivateFirstInstance(args); + return null; + } + + /// + /// Activates the first instance of the application with arguments from a second instance. + /// + /// List of arguments to supply the first instance of the application. + private static void ActivateFirstInstance(IList args) + { + // Set main window state and process command line args + if (Application.Current == null) + { + return; + } + + ((TApplication)Application.Current).SignalExternalCommandLineArgs(args); + } + + #endregion + + #region Private Classes + + /// + /// 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. + /// + private class IPCRemoteService : MarshalByRefObject + { + /// + /// Activates the first instance of the application. + /// + /// List of arguments to pass to the first instance. + public void InvokeFirstInstance(IList args) + { + if (Application.Current != null) + { + // Do an asynchronous call to ActivateFirstInstance function + Application.Current.Dispatcher.BeginInvoke( + DispatcherPriority.Normal, new DispatcherOperationCallback(SingleInstance.ActivateFirstInstanceCallback), args); + } + } + + /// + /// Remoting Object's ease expires after every 5 minutes by default. We need to override the InitializeLifetimeService class + /// to ensure that lease never expires. + /// + /// Always null. + public override object InitializeLifetimeService() + { + return null; + } + } + + #endregion + } +} diff --git a/MediaBrowser.Common/UI/Splash.xaml b/MediaBrowser.Common/UI/Splash.xaml new file mode 100644 index 0000000000..7781841b26 --- /dev/null +++ b/MediaBrowser.Common/UI/Splash.xaml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/MediaBrowser.Common/UI/Splash.xaml.cs b/MediaBrowser.Common/UI/Splash.xaml.cs new file mode 100644 index 0000000000..b9764c05fd --- /dev/null +++ b/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 +{ + /// + /// Interaction logic for Splash.xaml + /// + public partial class Splash : MetroWindow + { + public Splash(Progress 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; + } + } +} diff --git a/MediaBrowser.Common/app.config b/MediaBrowser.Common/app.config new file mode 100644 index 0000000000..037800f7f2 --- /dev/null +++ b/MediaBrowser.Common/app.config @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/MediaBrowser.Common/packages.config b/MediaBrowser.Common/packages.config new file mode 100644 index 0000000000..d3043e27a8 --- /dev/null +++ b/MediaBrowser.Common/packages.config @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/MediaBrowser.Controller/Drawing/DrawingUtils.cs b/MediaBrowser.Controller/Drawing/DrawingUtils.cs new file mode 100644 index 0000000000..8e2f829b98 --- /dev/null +++ b/MediaBrowser.Controller/Drawing/DrawingUtils.cs @@ -0,0 +1,81 @@ +using System; +using System.Drawing; + +namespace MediaBrowser.Controller.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.Controller/Drawing/ImageProcessor.cs b/MediaBrowser.Controller/Drawing/ImageProcessor.cs new file mode 100644 index 0000000000..29e40d17d7 --- /dev/null +++ b/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 + { + /// + /// 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.Controller/Entities/Audio.cs b/MediaBrowser.Controller/Entities/Audio.cs new file mode 100644 index 0000000000..61e901dd22 --- /dev/null +++ b/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; } + } +} diff --git a/MediaBrowser.Controller/Entities/BaseEntity.cs b/MediaBrowser.Controller/Entities/BaseEntity.cs new file mode 100644 index 0000000000..5b4a360c1f --- /dev/null +++ b/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 +{ + /// + /// Provides a base entity for all of our types + /// + 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 _providerData; + /// + /// 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. + /// + public Dictionary ProviderData + { + get + { + if (_providerData == null) _providerData = new Dictionary(); + return _providerData; + } + set + { + _providerData = value; + } + } + + protected ItemResolveEventArgs _resolveArgs; + /// + /// 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 + /// + 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; + } + } + + /// + /// Refresh metadata on us by execution our provider chain + /// + /// true if a provider reports we changed + public bool RefreshMetadata() + { + Kernel.Instance.ExecuteMetadataProviders(this).ConfigureAwait(false); + return true; + } + + } +} diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs new file mode 100644 index 0000000000..4c9008b22c --- /dev/null +++ b/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 PhysicalLocations + { + get + { + return _resolveArgs.PhysicalLocations; + } + } + + public string SortName { get; set; } + + /// + /// When the item first debuted. For movies this could be premiere date, episodes would be first aired + /// + 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 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 Taglines { get; set; } + + /// + /// Using a Dictionary to prevent duplicates + /// + public Dictionary People { get; set; } + + public List Studios { get; set; } + + public List 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; } + + /// + /// 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. + /// + public int? IndexNumber { get; set; } + + /// + /// For an episode this could be the season number, or for a song this could be the disc number. + /// + public int? ParentIndexNumber { get; set; } + + public IEnumerable