aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.hgignore43
-rw-r--r--MediaBrowser.Api/ApiService.cs438
-rw-r--r--MediaBrowser.Api/Drawing/DrawingUtils.cs81
-rw-r--r--MediaBrowser.Api/Drawing/ImageProcessor.cs148
-rw-r--r--MediaBrowser.Api/HttpHandlers/AudioHandler.cs119
-rw-r--r--MediaBrowser.Api/HttpHandlers/BaseMediaHandler.cs255
-rw-r--r--MediaBrowser.Api/HttpHandlers/FavoriteStatusHandler.cs38
-rw-r--r--MediaBrowser.Api/HttpHandlers/GenreHandler.cs57
-rw-r--r--MediaBrowser.Api/HttpHandlers/GenresHandler.cs78
-rw-r--r--MediaBrowser.Api/HttpHandlers/ImageHandler.cs224
-rw-r--r--MediaBrowser.Api/HttpHandlers/ItemHandler.cs35
-rw-r--r--MediaBrowser.Api/HttpHandlers/ItemListHandler.cs84
-rw-r--r--MediaBrowser.Api/HttpHandlers/MovieSpecialFeaturesHandler.cs46
-rw-r--r--MediaBrowser.Api/HttpHandlers/PersonHandler.cs55
-rw-r--r--MediaBrowser.Api/HttpHandlers/PlayedStatusHandler.cs38
-rw-r--r--MediaBrowser.Api/HttpHandlers/PluginAssemblyHandler.cs38
-rw-r--r--MediaBrowser.Api/HttpHandlers/PluginConfigurationHandler.cs53
-rw-r--r--MediaBrowser.Api/HttpHandlers/PluginsHandler.cs38
-rw-r--r--MediaBrowser.Api/HttpHandlers/ServerConfigurationHandler.cs37
-rw-r--r--MediaBrowser.Api/HttpHandlers/StudioHandler.cs57
-rw-r--r--MediaBrowser.Api/HttpHandlers/StudiosHandler.cs78
-rw-r--r--MediaBrowser.Api/HttpHandlers/UserAuthenticationHandler.cs29
-rw-r--r--MediaBrowser.Api/HttpHandlers/UserHandler.cs29
-rw-r--r--MediaBrowser.Api/HttpHandlers/UserItemRatingHandler.cs46
-rw-r--r--MediaBrowser.Api/HttpHandlers/UsersHandler.cs25
-rw-r--r--MediaBrowser.Api/HttpHandlers/VideoHandler.cs424
-rw-r--r--MediaBrowser.Api/HttpHandlers/WeatherHandler.cs43
-rw-r--r--MediaBrowser.Api/HttpHandlers/YearHandler.cs55
-rw-r--r--MediaBrowser.Api/HttpHandlers/YearsHandler.cs75
-rw-r--r--MediaBrowser.Api/MediaBrowser.Api.csproj117
-rw-r--r--MediaBrowser.Api/Plugin.cs14
-rw-r--r--MediaBrowser.Api/Properties/AssemblyInfo.cs35
-rw-r--r--MediaBrowser.Api/packages.config6
-rw-r--r--MediaBrowser.ApiInteraction.Metro/ApiClient.cs12
-rw-r--r--MediaBrowser.ApiInteraction.Metro/DataSerializer.cs78
-rw-r--r--MediaBrowser.ApiInteraction.Metro/MediaBrowser.ApiInteraction.Metro.csproj74
-rw-r--r--MediaBrowser.ApiInteraction.Metro/Properties/AssemblyInfo.cs30
-rw-r--r--MediaBrowser.ApiInteraction.sln37
-rw-r--r--MediaBrowser.ApiInteraction/ApiClient.cs18
-rw-r--r--MediaBrowser.ApiInteraction/BaseApiClient.cs446
-rw-r--r--MediaBrowser.ApiInteraction/BaseHttpApiClient.cs611
-rw-r--r--MediaBrowser.ApiInteraction/DataSerializer.cs77
-rw-r--r--MediaBrowser.ApiInteraction/MediaBrowser.ApiInteraction.csproj78
-rw-r--r--MediaBrowser.ApiInteraction/Properties/AssemblyInfo.cs35
-rw-r--r--MediaBrowser.ApiInteraction/SerializationFormats.cs10
-rw-r--r--MediaBrowser.ApiInteraction/packages.config4
-rw-r--r--MediaBrowser.Common/Events/GenericEventArgs.cs12
-rw-r--r--MediaBrowser.Common/Extensions/BaseExtensions.cs63
-rw-r--r--MediaBrowser.Common/Kernel/BaseApplicationPaths.cs154
-rw-r--r--MediaBrowser.Common/Kernel/BaseKernel.cs345
-rw-r--r--MediaBrowser.Common/Kernel/KernelContext.cs9
-rw-r--r--MediaBrowser.Common/Logging/BaseLogger.cs16
-rw-r--r--MediaBrowser.Common/Logging/LogRow.cs44
-rw-r--r--MediaBrowser.Common/Logging/LogSeverity.cs14
-rw-r--r--MediaBrowser.Common/Logging/Logger.cs93
-rw-r--r--MediaBrowser.Common/Logging/TraceFileLogger.cs38
-rw-r--r--MediaBrowser.Common/MediaBrowser.Common.csproj164
-rw-r--r--MediaBrowser.Common/Mef/MefUtils.cs43
-rw-r--r--MediaBrowser.Common/Net/Handlers/BaseEmbeddedResourceHandler.cs23
-rw-r--r--MediaBrowser.Common/Net/Handlers/BaseHandler.cs430
-rw-r--r--MediaBrowser.Common/Net/Handlers/BaseSerializationHandler.cs90
-rw-r--r--MediaBrowser.Common/Net/Handlers/StaticFileHandler.cs249
-rw-r--r--MediaBrowser.Common/Net/HttpServer.cs40
-rw-r--r--MediaBrowser.Common/Net/MimeTypes.cs160
-rw-r--r--MediaBrowser.Common/Net/Request.cs18
-rw-r--r--MediaBrowser.Common/Plugins/BasePlugin.cs247
-rw-r--r--MediaBrowser.Common/Plugins/BaseTheme.cs78
-rw-r--r--MediaBrowser.Common/Properties/AssemblyInfo.cs35
-rw-r--r--MediaBrowser.Common/Properties/Resources.Designer.cs63
-rw-r--r--MediaBrowser.Common/Properties/Resources.resx121
-rw-r--r--MediaBrowser.Common/Resources/Images/Icon.icobin0 -> 146168 bytes
-rw-r--r--MediaBrowser.Common/Resources/Images/mblogoblack.pngbin0 -> 32983 bytes
-rw-r--r--MediaBrowser.Common/Resources/Images/mblogowhite.pngbin0 -> 27029 bytes
-rw-r--r--MediaBrowser.Common/Resources/Images/spinner.gifbin0 -> 673 bytes
-rw-r--r--MediaBrowser.Common/Serialization/JsonSerializer.cs74
-rw-r--r--MediaBrowser.Common/Serialization/JsvSerializer.cs44
-rw-r--r--MediaBrowser.Common/Serialization/ProtobufSerializer.cs53
-rw-r--r--MediaBrowser.Common/Serialization/XmlSerializer.cs58
-rw-r--r--MediaBrowser.Common/UI/BaseApplication.cs123
-rw-r--r--MediaBrowser.Common/UI/SingleInstance.cs484
-rw-r--r--MediaBrowser.Common/UI/Splash.xaml33
-rw-r--r--MediaBrowser.Common/UI/Splash.xaml.cs32
-rw-r--r--MediaBrowser.Common/app.config15
-rw-r--r--MediaBrowser.Common/packages.config8
-rw-r--r--MediaBrowser.Controller/Drawing/DrawingUtils.cs81
-rw-r--r--MediaBrowser.Controller/Drawing/ImageProcessor.cs148
-rw-r--r--MediaBrowser.Controller/Entities/Audio.cs14
-rw-r--r--MediaBrowser.Controller/Entities/BaseEntity.cs94
-rw-r--r--MediaBrowser.Controller/Entities/BaseItem.cs202
-rw-r--r--MediaBrowser.Controller/Entities/Folder.cs619
-rw-r--r--MediaBrowser.Controller/Entities/Genre.cs7
-rw-r--r--MediaBrowser.Controller/Entities/Movies/BoxSet.cs7
-rw-r--r--MediaBrowser.Controller/Entities/Movies/Movie.cs31
-rw-r--r--MediaBrowser.Controller/Entities/Person.cs25
-rw-r--r--MediaBrowser.Controller/Entities/Studio.cs7
-rw-r--r--MediaBrowser.Controller/Entities/TV/Episode.cs7
-rw-r--r--MediaBrowser.Controller/Entities/TV/Season.cs34
-rw-r--r--MediaBrowser.Controller/Entities/TV/Series.cs12
-rw-r--r--MediaBrowser.Controller/Entities/User.cs21
-rw-r--r--MediaBrowser.Controller/Entities/UserItemData.cs67
-rw-r--r--MediaBrowser.Controller/Entities/Video.cs20
-rw-r--r--MediaBrowser.Controller/Entities/Year.cs7
-rw-r--r--MediaBrowser.Controller/FFMpeg/FFProbe.cs137
-rw-r--r--MediaBrowser.Controller/FFMpeg/FFProbeResult.cs119
-rw-r--r--MediaBrowser.Controller/FFMpeg/ffmpeg.exe.REMOVED.git-id1
-rw-r--r--MediaBrowser.Controller/FFMpeg/ffprobe.exe.REMOVED.git-id1
-rw-r--r--MediaBrowser.Controller/FFMpeg/readme.txt3
-rw-r--r--MediaBrowser.Controller/IO/DirectoryWatchers.cs172
-rw-r--r--MediaBrowser.Controller/IO/FileData.cs251
-rw-r--r--MediaBrowser.Controller/IO/FileSystemHelper.cs132
-rw-r--r--MediaBrowser.Controller/IO/Shortcut.cs185
-rw-r--r--MediaBrowser.Controller/Kernel.cs386
-rw-r--r--MediaBrowser.Controller/Library/ChildrenChangedEventArgs.cs34
-rw-r--r--MediaBrowser.Controller/Library/ItemController.cs136
-rw-r--r--MediaBrowser.Controller/Library/ItemResolveEventArgs.cs104
-rw-r--r--MediaBrowser.Controller/MediaBrowser.Controller.csproj150
-rw-r--r--MediaBrowser.Controller/Properties/AssemblyInfo.cs35
-rw-r--r--MediaBrowser.Controller/Providers/AudioInfoProvider.cs262
-rw-r--r--MediaBrowser.Controller/Providers/BaseItemXmlParser.cs724
-rw-r--r--MediaBrowser.Controller/Providers/BaseMetadataProvider.cs104
-rw-r--r--MediaBrowser.Controller/Providers/BaseProviderInfo.cs15
-rw-r--r--MediaBrowser.Controller/Providers/FolderProviderFromXml.cs38
-rw-r--r--MediaBrowser.Controller/Providers/ImageFromMediaLocationProvider.cs128
-rw-r--r--MediaBrowser.Controller/Providers/LocalTrailerProvider.cs47
-rw-r--r--MediaBrowser.Controller/Providers/Movies/MovieProviderFromXml.cs43
-rw-r--r--MediaBrowser.Controller/Providers/Movies/MovieSpecialFeaturesProvider.cs45
-rw-r--r--MediaBrowser.Controller/Providers/TV/EpisodeImageFromMediaLocationProvider.cs67
-rw-r--r--MediaBrowser.Controller/Providers/TV/EpisodeProviderFromXml.cs59
-rw-r--r--MediaBrowser.Controller/Providers/TV/EpisodeXmlParser.cs60
-rw-r--r--MediaBrowser.Controller/Providers/TV/SeriesProviderFromXml.cs36
-rw-r--r--MediaBrowser.Controller/Providers/TV/SeriesXmlParser.cs69
-rw-r--r--MediaBrowser.Controller/Providers/VideoInfoProvider.cs168
-rw-r--r--MediaBrowser.Controller/Resolvers/AudioResolver.cs54
-rw-r--r--MediaBrowser.Controller/Resolvers/BaseItemResolver.cs126
-rw-r--r--MediaBrowser.Controller/Resolvers/EntityResolutionHelper.cs70
-rw-r--r--MediaBrowser.Controller/Resolvers/FolderResolver.cs36
-rw-r--r--MediaBrowser.Controller/Resolvers/Movies/BoxSetResolver.cs28
-rw-r--r--MediaBrowser.Controller/Resolvers/Movies/MovieResolver.cs116
-rw-r--r--MediaBrowser.Controller/Resolvers/TV/EpisodeResolver.cs21
-rw-r--r--MediaBrowser.Controller/Resolvers/TV/SeasonResolver.cs25
-rw-r--r--MediaBrowser.Controller/Resolvers/TV/SeriesResolver.cs64
-rw-r--r--MediaBrowser.Controller/Resolvers/TV/TVUtils.cs164
-rw-r--r--MediaBrowser.Controller/Resolvers/VideoResolver.cs100
-rw-r--r--MediaBrowser.Controller/ServerApplicationPaths.cs278
-rw-r--r--MediaBrowser.Controller/Weather/BaseWeatherProvider.cs34
-rw-r--r--MediaBrowser.Controller/Weather/WeatherProvider.cs189
-rw-r--r--MediaBrowser.Controller/Xml/XmlExtensions.cs46
-rw-r--r--MediaBrowser.Controller/packages.config6
-rw-r--r--MediaBrowser.Model/Authentication/AuthenticationResult.cs11
-rw-r--r--MediaBrowser.Model/Configuration/BaseApplicationConfiguration.cs24
-rw-r--r--MediaBrowser.Model/Configuration/ServerConfiguration.cs30
-rw-r--r--MediaBrowser.Model/DTO/AudioInfo.cs23
-rw-r--r--MediaBrowser.Model/DTO/AudioOutputFormats.cs15
-rw-r--r--MediaBrowser.Model/DTO/DTOBaseItem.cs178
-rw-r--r--MediaBrowser.Model/DTO/DTOUser.cs27
-rw-r--r--MediaBrowser.Model/DTO/DTOUserItemData.cs23
-rw-r--r--MediaBrowser.Model/DTO/IBNItem.cs65
-rw-r--r--MediaBrowser.Model/DTO/MovieInfo.cs11
-rw-r--r--MediaBrowser.Model/DTO/PluginInfo.cs33
-rw-r--r--MediaBrowser.Model/DTO/SeriesInfo.cs18
-rw-r--r--MediaBrowser.Model/DTO/VideoInfo.cs30
-rw-r--r--MediaBrowser.Model/DTO/VideoOutputFormats.cs22
-rw-r--r--MediaBrowser.Model/Entities/AudioStream.cs26
-rw-r--r--MediaBrowser.Model/Entities/IHasProviderIds.cs57
-rw-r--r--MediaBrowser.Model/Entities/ImageType.cs13
-rw-r--r--MediaBrowser.Model/Entities/ItemSpecialCounts.cs23
-rw-r--r--MediaBrowser.Model/Entities/MetadataProviders.cs11
-rw-r--r--MediaBrowser.Model/Entities/SubtitleStream.cs17
-rw-r--r--MediaBrowser.Model/Entities/VideoType.cs12
-rw-r--r--MediaBrowser.Model/MediaBrowser.Model.csproj80
-rw-r--r--MediaBrowser.Model/Plugins/BasePluginConfiguration.cs13
-rw-r--r--MediaBrowser.Model/Progress/TaskProgress.cs19
-rw-r--r--MediaBrowser.Model/Properties/AssemblyInfo.cs28
-rw-r--r--MediaBrowser.Model/Weather/WeatherForecast.cs30
-rw-r--r--MediaBrowser.Model/Weather/WeatherInfo.cs14
-rw-r--r--MediaBrowser.Model/Weather/WeatherStatus.cs38
-rw-r--r--MediaBrowser.Model/Weather/WeatherUnits.cs9
-rw-r--r--MediaBrowser.Plugins.DefaultTheme/Converters/TileBackgroundConverter.cs43
-rw-r--r--MediaBrowser.Plugins.DefaultTheme/Converters/WeatherImageConverter.cs43
-rw-r--r--MediaBrowser.Plugins.DefaultTheme/MediaBrowser.Plugins.DefaultTheme.csproj132
-rw-r--r--MediaBrowser.Plugins.DefaultTheme/Pages/LoginPage.xaml64
-rw-r--r--MediaBrowser.Plugins.DefaultTheme/Pages/LoginPage.xaml.cs15
-rw-r--r--MediaBrowser.Plugins.DefaultTheme/Plugin.cs20
-rw-r--r--MediaBrowser.Plugins.DefaultTheme/Properties/AssemblyInfo.cs55
-rw-r--r--MediaBrowser.Plugins.DefaultTheme/Properties/Resources.Designer.cs62
-rw-r--r--MediaBrowser.Plugins.DefaultTheme/Properties/Resources.resx117
-rw-r--r--MediaBrowser.Plugins.DefaultTheme/Properties/Settings.Designer.cs30
-rw-r--r--MediaBrowser.Plugins.DefaultTheme/Properties/Settings.settings7
-rw-r--r--MediaBrowser.Plugins.DefaultTheme/Resources/AppResources.cs14
-rw-r--r--MediaBrowser.Plugins.DefaultTheme/Resources/AppResources.xaml81
-rw-r--r--MediaBrowser.Plugins.DefaultTheme/Resources/Images/CurrentUserDefault.pngbin0 -> 968 bytes
-rw-r--r--MediaBrowser.Plugins.DefaultTheme/Resources/Images/UserLoginDefault.pngbin0 -> 3179 bytes
-rw-r--r--MediaBrowser.Plugins.DefaultTheme/Resources/Images/Weather/Overcast.pngbin0 -> 1578 bytes
-rw-r--r--MediaBrowser.Plugins.DefaultTheme/Resources/Images/Weather/Rain.pngbin0 -> 1147 bytes
-rw-r--r--MediaBrowser.Plugins.DefaultTheme/Resources/Images/Weather/Snow.pngbin0 -> 1458 bytes
-rw-r--r--MediaBrowser.Plugins.DefaultTheme/Resources/Images/Weather/Sunny.pngbin0 -> 1302 bytes
-rw-r--r--MediaBrowser.Plugins.DefaultTheme/Resources/Images/Weather/Thunder.pngbin0 -> 1166 bytes
-rw-r--r--MediaBrowser.ServerApplication/App.config25
-rw-r--r--MediaBrowser.ServerApplication/App.xaml8
-rw-r--r--MediaBrowser.ServerApplication/App.xaml.cs67
-rw-r--r--MediaBrowser.ServerApplication/MainWindow.xaml41
-rw-r--r--MediaBrowser.ServerApplication/MainWindow.xaml.cs109
-rw-r--r--MediaBrowser.ServerApplication/MediaBrowser.ServerApplication.csproj143
-rw-r--r--MediaBrowser.ServerApplication/Properties/AssemblyInfo.cs53
-rw-r--r--MediaBrowser.ServerApplication/Properties/Resources.Designer.cs71
-rw-r--r--MediaBrowser.ServerApplication/Properties/Resources.resx117
-rw-r--r--MediaBrowser.ServerApplication/Properties/Settings.Designer.cs30
-rw-r--r--MediaBrowser.ServerApplication/Properties/Settings.settings7
-rw-r--r--MediaBrowser.ServerApplication/Resources/Images/Icon.icobin0 -> 146168 bytes
-rw-r--r--MediaBrowser.ServerApplication/Resources/Images/loadingIcon1.icobin0 -> 179144 bytes
-rw-r--r--MediaBrowser.ServerApplication/Resources/Images/loadingIcon2.icobin0 -> 176716 bytes
-rw-r--r--MediaBrowser.ServerApplication/Resources/Images/loadingIcon3.icobin0 -> 175427 bytes
-rw-r--r--MediaBrowser.ServerApplication/Resources/Images/loadingIcon4.icobin0 -> 176666 bytes
-rw-r--r--MediaBrowser.ServerApplication/packages.config4
-rw-r--r--MediaBrowser.UI.sln58
-rw-r--r--MediaBrowser.UI/App.config9
-rw-r--r--MediaBrowser.UI/App.xaml14
-rw-r--r--MediaBrowser.UI/App.xaml.cs213
-rw-r--r--MediaBrowser.UI/Configuration/UIApplicationConfiguration.cs27
-rw-r--r--MediaBrowser.UI/Configuration/UIApplicationPaths.cs8
-rw-r--r--MediaBrowser.UI/Controller/PluginUpdater.cs231
-rw-r--r--MediaBrowser.UI/Controller/UIKernel.cs97
-rw-r--r--MediaBrowser.UI/Controls/EnhancedScrollViewer.cs73
-rw-r--r--MediaBrowser.UI/Controls/ExtendedImage.cs92
-rw-r--r--MediaBrowser.UI/Controls/TreeHelper.cs226
-rw-r--r--MediaBrowser.UI/Controls/WindowCommands.xaml91
-rw-r--r--MediaBrowser.UI/Controls/WindowCommands.xaml.cs50
-rw-r--r--MediaBrowser.UI/Converters/CurrentUserVisibilityConverter.cs26
-rw-r--r--MediaBrowser.UI/Converters/DateTimeToStringConverter.cs34
-rw-r--r--MediaBrowser.UI/Converters/LastSeenTextConverter.cs86
-rw-r--r--MediaBrowser.UI/Converters/UserImageConverter.cs60
-rw-r--r--MediaBrowser.UI/Converters/WeatherTemperatureConverter.cs31
-rw-r--r--MediaBrowser.UI/Converters/WeatherVisibilityConverter.cs20
-rw-r--r--MediaBrowser.UI/MainWindow.xaml50
-rw-r--r--MediaBrowser.UI/MainWindow.xaml.cs368
-rw-r--r--MediaBrowser.UI/MediaBrowser.UI.csproj196
-rw-r--r--MediaBrowser.UI/Pages/BaseLoginPage.cs33
-rw-r--r--MediaBrowser.UI/Pages/BasePage.cs79
-rw-r--r--MediaBrowser.UI/Properties/AssemblyInfo.cs53
-rw-r--r--MediaBrowser.UI/Properties/Resources.Designer.cs71
-rw-r--r--MediaBrowser.UI/Properties/Resources.resx117
-rw-r--r--MediaBrowser.UI/Properties/Settings.Designer.cs30
-rw-r--r--MediaBrowser.UI/Properties/Settings.settings7
-rw-r--r--MediaBrowser.UI/Resources/AppResources.xaml122
-rw-r--r--MediaBrowser.UI/Resources/Images/BackButton.pngbin0 -> 1461 bytes
-rw-r--r--MediaBrowser.UI/Resources/Images/ExitButton.pngbin0 -> 1486 bytes
-rw-r--r--MediaBrowser.UI/Resources/Images/ForwardButton.pngbin0 -> 1442 bytes
-rw-r--r--MediaBrowser.UI/Resources/Images/Icon.icobin0 -> 32038 bytes
-rw-r--r--MediaBrowser.UI/Resources/Images/MuteButton.pngbin0 -> 1550 bytes
-rw-r--r--MediaBrowser.UI/Resources/Images/SettingsButton.pngbin0 -> 1690 bytes
-rw-r--r--MediaBrowser.UI/Resources/Images/VolumeDownButton.pngbin0 -> 1363 bytes
-rw-r--r--MediaBrowser.UI/Resources/Images/VolumeUpButton.pngbin0 -> 1450 bytes
-rw-r--r--MediaBrowser.UI/Resources/Images/mblogoblack.pngbin0 -> 32983 bytes
-rw-r--r--MediaBrowser.UI/Resources/Images/mblogowhite.pngbin0 -> 27029 bytes
-rw-r--r--MediaBrowser.UI/Resources/MainWindowResources.xaml43
-rw-r--r--MediaBrowser.UI/Resources/NavBarResources.xaml122
-rw-r--r--MediaBrowser.UI/Themes/Generic.xaml32
-rw-r--r--MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj70
-rw-r--r--MediaBrowser.WebDashboard/Plugin.cs15
-rw-r--r--MediaBrowser.WebDashboard/Properties/AssemblyInfo.cs35
-rw-r--r--MediaBrowser.sln55
261 files changed, 19594 insertions, 0 deletions
diff --git a/.hgignore b/.hgignore
new file mode 100644
index 000000000..c8162e4c8
--- /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 000000000..0fef1cb57
--- /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
+{
+ /// <summary>
+ /// Contains some helpers for the api
+ /// </summary>
+ public static class ApiService
+ {
+ /// <summary>
+ /// Gets an Item by Id, or the root item if none is supplied
+ /// </summary>
+ public static BaseItem GetItemById(string id)
+ {
+ Guid guid = string.IsNullOrEmpty(id) ? Guid.Empty : new Guid(id);
+
+ return Kernel.Instance.GetItemById(guid);
+ }
+
+ /// <summary>
+ /// Gets a User by Id
+ /// </summary>
+ /// <param name="logActivity">Whether or not to update the user's LastActivityDate</param>
+ public static User GetUserById(string id, bool logActivity)
+ {
+ var guid = new Guid(id);
+
+ var user = Kernel.Instance.Users.FirstOrDefault(u => u.Id == guid);
+
+ if (logActivity)
+ {
+ LogUserActivity(user);
+ }
+
+ return user;
+ }
+
+ /// <summary>
+ /// Gets the default User
+ /// </summary>
+ /// <param name="logActivity">Whether or not to update the user's LastActivityDate</param>
+ public static User GetDefaultUser(bool logActivity)
+ {
+ User user = Kernel.Instance.GetDefaultUser();
+
+ if (logActivity)
+ {
+ LogUserActivity(user);
+ }
+
+ return user;
+ }
+
+ /// <summary>
+ /// Updates LastActivityDate for a given User
+ /// </summary>
+ public static void LogUserActivity(User user)
+ {
+ user.LastActivityDate = DateTime.UtcNow;
+ Kernel.Instance.SaveUser(user);
+ }
+
+ /// <summary>
+ /// Converts a BaseItem to a DTOBaseItem
+ /// </summary>
+ public async static Task<DtoBaseItem> GetDtoBaseItem(BaseItem item, User user,
+ bool includeChildren = true,
+ bool includePeople = true)
+ {
+ var dto = new DtoBaseItem();
+
+ var tasks = new List<Task>();
+
+ tasks.Add(AttachStudios(dto, item));
+
+ if (includeChildren)
+ {
+ tasks.Add(AttachChildren(dto, item, user));
+ tasks.Add(AttachLocalTrailers(dto, item, user));
+ }
+
+ if (includePeople)
+ {
+ tasks.Add(AttachPeople(dto, item));
+ }
+
+ AttachBasicFields(dto, item, user);
+
+ // Make sure all the tasks we kicked off have completed.
+ if (tasks.Count > 0)
+ {
+ await Task.WhenAll(tasks).ConfigureAwait(false);
+ }
+
+ return dto;
+ }
+
+ /// <summary>
+ /// Sets simple property values on a DTOBaseItem
+ /// </summary>
+ private static void AttachBasicFields(DtoBaseItem dto, BaseItem item, User user)
+ {
+ dto.AspectRatio = item.AspectRatio;
+ dto.BackdropCount = item.BackdropImagePaths == null ? 0 : item.BackdropImagePaths.Count();
+ dto.DateCreated = item.DateCreated;
+ dto.DisplayMediaType = item.DisplayMediaType;
+
+ if (item.Genres != null)
+ {
+ dto.Genres = item.Genres.ToArray();
+ }
+
+ dto.HasArt = !string.IsNullOrEmpty(item.ArtImagePath);
+ dto.HasBanner = !string.IsNullOrEmpty(item.BannerImagePath);
+ dto.HasLogo = !string.IsNullOrEmpty(item.LogoImagePath);
+ dto.HasPrimaryImage = !string.IsNullOrEmpty(item.PrimaryImagePath);
+ dto.HasThumb = !string.IsNullOrEmpty(item.ThumbnailImagePath);
+ dto.Id = item.Id;
+ dto.IsNew = item.IsRecentlyAdded(user);
+ dto.IndexNumber = item.IndexNumber;
+ dto.IsFolder = item.IsFolder;
+ dto.Language = item.Language;
+ dto.LocalTrailerCount = item.LocalTrailers == null ? 0 : item.LocalTrailers.Count();
+ dto.Name = item.Name;
+ dto.OfficialRating = item.OfficialRating;
+ dto.Overview = item.Overview;
+
+ // If there are no backdrops, indicate what parent has them in case the Ui wants to allow inheritance
+ if (dto.BackdropCount == 0)
+ {
+ int backdropCount;
+ dto.ParentBackdropItemId = GetParentBackdropItemId(item, out backdropCount);
+ dto.ParentBackdropCount = backdropCount;
+ }
+
+ if (item.Parent != null)
+ {
+ dto.ParentId = item.Parent.Id;
+ }
+
+ dto.ParentIndexNumber = item.ParentIndexNumber;
+
+ // If there is no logo, indicate what parent has one in case the Ui wants to allow inheritance
+ if (!dto.HasLogo)
+ {
+ dto.ParentLogoItemId = GetParentLogoItemId(item);
+ }
+
+ dto.Path = item.Path;
+
+ dto.PremiereDate = item.PremiereDate;
+ dto.ProductionYear = item.ProductionYear;
+ dto.ProviderIds = item.ProviderIds;
+ dto.RunTimeTicks = item.RunTimeTicks;
+ dto.SortName = item.SortName;
+
+ if (item.Taglines != null)
+ {
+ dto.Taglines = item.Taglines.ToArray();
+ }
+
+ dto.TrailerUrl = item.TrailerUrl;
+ dto.Type = item.GetType().Name;
+ dto.CommunityRating = item.CommunityRating;
+
+ dto.UserData = GetDtoUserItemData(item.GetUserData(user, false));
+
+ var folder = item as Folder;
+
+ if (folder != null)
+ {
+ dto.SpecialCounts = folder.GetSpecialCounts(user);
+
+ dto.IsRoot = folder.IsRoot;
+ dto.IsVirtualFolder = folder.IsVirtualFolder;
+ }
+
+ // Add AudioInfo
+ var audio = item as Audio;
+
+ if (audio != null)
+ {
+ dto.AudioInfo = new AudioInfo
+ {
+ Album = audio.Album,
+ AlbumArtist = audio.AlbumArtist,
+ Artist = audio.Artist,
+ BitRate = audio.BitRate,
+ Channels = audio.Channels
+ };
+ }
+
+ // Add VideoInfo
+ var video = item as Video;
+
+ if (video != null)
+ {
+ dto.VideoInfo = new VideoInfo
+ {
+ Height = video.Height,
+ Width = video.Width,
+ Codec = video.Codec,
+ VideoType = video.VideoType,
+ ScanType = video.ScanType
+ };
+
+ if (video.AudioStreams != null)
+ {
+ dto.VideoInfo.AudioStreams = video.AudioStreams.ToArray();
+ }
+
+ if (video.Subtitles != null)
+ {
+ dto.VideoInfo.Subtitles = video.Subtitles.ToArray();
+ }
+ }
+
+ // Add SeriesInfo
+ var series = item as Series;
+
+ if (series != null)
+ {
+ DayOfWeek[] airDays = series.AirDays == null ? new DayOfWeek[] { } : series.AirDays.ToArray();
+
+ dto.SeriesInfo = new SeriesInfo
+ {
+ AirDays = airDays,
+ AirTime = series.AirTime,
+ Status = series.Status
+ };
+ }
+
+ // Add MovieInfo
+ var movie = item as Movie;
+
+ if (movie != null)
+ {
+ int specialFeatureCount = movie.SpecialFeatures == null ? 0 : movie.SpecialFeatures.Count();
+
+ dto.MovieInfo = new MovieInfo
+ {
+ SpecialFeatureCount = specialFeatureCount
+ };
+ }
+ }
+
+ /// <summary>
+ /// Attaches Studio DTO's to a DTOBaseItem
+ /// </summary>
+ private static async Task AttachStudios(DtoBaseItem dto, BaseItem item)
+ {
+ // Attach Studios by transforming them into BaseItemStudio (DTO)
+ if (item.Studios != null)
+ {
+ Studio[] entities = await Task.WhenAll(item.Studios.Select(c => Kernel.Instance.ItemController.GetStudio(c))).ConfigureAwait(false);
+
+ dto.Studios = new BaseItemStudio[entities.Length];
+
+ for (int i = 0; i < entities.Length; i++)
+ {
+ Studio entity = entities[i];
+ var baseItemStudio = new BaseItemStudio{};
+
+ baseItemStudio.Name = entity.Name;
+
+ baseItemStudio.HasImage = !string.IsNullOrEmpty(entity.PrimaryImagePath);
+
+ dto.Studios[i] = baseItemStudio;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Attaches child DTO's to a DTOBaseItem
+ /// </summary>
+ private static async Task AttachChildren(DtoBaseItem dto, BaseItem item, User user)
+ {
+ var folder = item as Folder;
+
+ if (folder != null)
+ {
+ IEnumerable<BaseItem> children = folder.GetChildren(user);
+
+ dto.Children = await Task.WhenAll(children.Select(c => GetDtoBaseItem(c, user, false, false))).ConfigureAwait(false);
+ }
+ }
+
+ /// <summary>
+ /// Attaches trailer DTO's to a DTOBaseItem
+ /// </summary>
+ private static async Task AttachLocalTrailers(DtoBaseItem dto, BaseItem item, User user)
+ {
+ if (item.LocalTrailers != null && item.LocalTrailers.Any())
+ {
+ dto.LocalTrailers = await Task.WhenAll(item.LocalTrailers.Select(c => GetDtoBaseItem(c, user, false, false))).ConfigureAwait(false);
+ }
+ }
+
+ /// <summary>
+ /// Attaches People DTO's to a DTOBaseItem
+ /// </summary>
+ private static async Task AttachPeople(DtoBaseItem dto, BaseItem item)
+ {
+ // Attach People by transforming them into BaseItemPerson (DTO)
+ if (item.People != null)
+ {
+ IEnumerable<Person> entities = await Task.WhenAll(item.People.Select(c => Kernel.Instance.ItemController.GetPerson(c.Key))).ConfigureAwait(false);
+
+ dto.People = item.People.Select(p =>
+ {
+ var baseItemPerson = new BaseItemPerson{};
+
+ baseItemPerson.Name = p.Key;
+ baseItemPerson.Overview = p.Value.Overview;
+ baseItemPerson.Type = p.Value.Type;
+
+ Person ibnObject = entities.First(i => i.Name.Equals(p.Key, StringComparison.OrdinalIgnoreCase));
+
+ if (ibnObject != null)
+ {
+ baseItemPerson.HasImage = !string.IsNullOrEmpty(ibnObject.PrimaryImagePath);
+ }
+
+ return baseItemPerson;
+ }).ToArray();
+ }
+ }
+
+ /// <summary>
+ /// If an item does not any backdrops, this can be used to find the first parent that does have one
+ /// </summary>
+ private static Guid? GetParentBackdropItemId(BaseItem item, out int backdropCount)
+ {
+ backdropCount = 0;
+
+ var parent = item.Parent;
+
+ while (parent != null)
+ {
+ if (parent.BackdropImagePaths != null && parent.BackdropImagePaths.Any())
+ {
+ backdropCount = parent.BackdropImagePaths.Count();
+ return parent.Id;
+ }
+
+ parent = parent.Parent;
+ }
+
+ return null;
+ }
+
+ /// <summary>
+ /// If an item does not have a logo, this can be used to find the first parent that does have one
+ /// </summary>
+ private static Guid? GetParentLogoItemId(BaseItem item)
+ {
+ var parent = item.Parent;
+
+ while (parent != null)
+ {
+ if (!string.IsNullOrEmpty(parent.LogoImagePath))
+ {
+ return parent.Id;
+ }
+
+ parent = parent.Parent;
+ }
+
+ return null;
+ }
+
+ /// <summary>
+ /// Gets an ImagesByName entity along with the number of items containing it
+ /// </summary>
+ public static IbnItem GetIbnItem(BaseEntity entity, int itemCount)
+ {
+ return new IbnItem
+ {
+ Id = entity.Id,
+ BaseItemCount = itemCount,
+ HasImage = !string.IsNullOrEmpty(entity.PrimaryImagePath),
+ Name = entity.Name
+ };
+ }
+
+ /// <summary>
+ /// Converts a User to a DTOUser
+ /// </summary>
+ public static DtoUser GetDtoUser(User user)
+ {
+ return new DtoUser
+ {
+ Id = user.Id,
+ Name = user.Name,
+ HasImage = !string.IsNullOrEmpty(user.PrimaryImagePath),
+ HasPassword = !string.IsNullOrEmpty(user.Password),
+ LastActivityDate = user.LastActivityDate,
+ LastLoginDate = user.LastLoginDate
+ };
+ }
+
+ /// <summary>
+ /// Converts a UserItemData to a DTOUserItemData
+ /// </summary>
+ public static DtoUserItemData GetDtoUserItemData(UserItemData data)
+ {
+ if (data == null)
+ {
+ return null;
+ }
+
+ return new DtoUserItemData
+ {
+ IsFavorite = data.IsFavorite,
+ Likes = data.Likes,
+ PlaybackPositionTicks = data.PlaybackPositionTicks,
+ PlayCount = data.PlayCount,
+ Rating = data.Rating
+ };
+ }
+
+ public static bool IsApiUrlMatch(string url, HttpListenerRequest request)
+ {
+ url = "/api/" + url;
+
+ return request.Url.LocalPath.EndsWith(url, StringComparison.OrdinalIgnoreCase);
+ }
+ }
+}
diff --git a/MediaBrowser.Api/Drawing/DrawingUtils.cs b/MediaBrowser.Api/Drawing/DrawingUtils.cs
new file mode 100644
index 000000000..f76a74218
--- /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
+ {
+ /// <summary>
+ /// Resizes a set of dimensions
+ /// </summary>
+ public static Size Resize(int currentWidth, int currentHeight, int? width, int? height, int? maxWidth, int? maxHeight)
+ {
+ return Resize(new Size(currentWidth, currentHeight), width, height, maxWidth, maxHeight);
+ }
+
+ /// <summary>
+ /// Resizes a set of dimensions
+ /// </summary>
+ /// <param name="size">The original size object</param>
+ /// <param name="width">A new fixed width, if desired</param>
+ /// <param name="height">A new fixed neight, if desired</param>
+ /// <param name="maxWidth">A max fixed width, if desired</param>
+ /// <param name="maxHeight">A max fixed height, if desired</param>
+ /// <returns>A new size object</returns>
+ public static Size Resize(Size size, int? width, int? height, int? maxWidth, int? maxHeight)
+ {
+ decimal newWidth = size.Width;
+ decimal newHeight = size.Height;
+
+ if (width.HasValue && height.HasValue)
+ {
+ newWidth = width.Value;
+ newHeight = height.Value;
+ }
+
+ else if (height.HasValue)
+ {
+ newWidth = GetNewWidth(newHeight, newWidth, height.Value);
+ newHeight = height.Value;
+ }
+
+ else if (width.HasValue)
+ {
+ newHeight = GetNewHeight(newHeight, newWidth, width.Value);
+ newWidth = width.Value;
+ }
+
+ if (maxHeight.HasValue && maxHeight < newHeight)
+ {
+ newWidth = GetNewWidth(newHeight, newWidth, maxHeight.Value);
+ newHeight = maxHeight.Value;
+ }
+
+ if (maxWidth.HasValue && maxWidth < newWidth)
+ {
+ newHeight = GetNewHeight(newHeight, newWidth, maxWidth.Value);
+ newWidth = maxWidth.Value;
+ }
+
+ return new Size(Convert.ToInt32(newWidth), Convert.ToInt32(newHeight));
+ }
+
+ private static decimal GetNewWidth(decimal currentHeight, decimal currentWidth, int newHeight)
+ {
+ decimal scaleFactor = newHeight;
+ scaleFactor /= currentHeight;
+ scaleFactor *= currentWidth;
+
+ return scaleFactor;
+ }
+
+ private static decimal GetNewHeight(decimal currentHeight, decimal currentWidth, int newWidth)
+ {
+ decimal scaleFactor = newWidth;
+ scaleFactor /= currentWidth;
+ scaleFactor *= currentHeight;
+
+ return scaleFactor;
+ }
+ }
+}
diff --git a/MediaBrowser.Api/Drawing/ImageProcessor.cs b/MediaBrowser.Api/Drawing/ImageProcessor.cs
new file mode 100644
index 000000000..1a471acf5
--- /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
+ {
+ /// <summary>
+ /// Processes an image by resizing to target dimensions
+ /// </summary>
+ /// <param name="entity">The entity that owns the image</param>
+ /// <param name="imageType">The image type</param>
+ /// <param name="imageIndex">The image index (currently only used with backdrops)</param>
+ /// <param name="toStream">The stream to save the new image to</param>
+ /// <param name="width">Use if a fixed width is required. Aspect ratio will be preserved.</param>
+ /// <param name="height">Use if a fixed height is required. Aspect ratio will be preserved.</param>
+ /// <param name="maxWidth">Use if a max width is required. Aspect ratio will be preserved.</param>
+ /// <param name="maxHeight">Use if a max height is required. Aspect ratio will be preserved.</param>
+ /// <param name="quality">Quality level, from 0-100. Currently only applies to JPG. The default value should suffice.</param>
+ public static void ProcessImage(BaseEntity entity, ImageType imageType, int imageIndex, Stream toStream, int? width, int? height, int? maxWidth, int? maxHeight, int? quality)
+ {
+ Image originalImage = Image.FromFile(GetImagePath(entity, imageType, imageIndex));
+
+ // Determine the output size based on incoming parameters
+ Size newSize = DrawingUtils.Resize(originalImage.Size, width, height, maxWidth, maxHeight);
+
+ Bitmap thumbnail;
+
+ // Graphics.FromImage will throw an exception if the PixelFormat is Indexed, so we need to handle that here
+ if (originalImage.PixelFormat.HasFlag(PixelFormat.Indexed))
+ {
+ thumbnail = new Bitmap(originalImage, newSize.Width, newSize.Height);
+ }
+ else
+ {
+ thumbnail = new Bitmap(newSize.Width, newSize.Height, originalImage.PixelFormat);
+ }
+
+ thumbnail.MakeTransparent();
+
+ // Preserve the original resolution
+ thumbnail.SetResolution(originalImage.HorizontalResolution, originalImage.VerticalResolution);
+
+ Graphics thumbnailGraph = Graphics.FromImage(thumbnail);
+
+ thumbnailGraph.CompositingQuality = CompositingQuality.HighQuality;
+ thumbnailGraph.SmoothingMode = SmoothingMode.HighQuality;
+ thumbnailGraph.InterpolationMode = InterpolationMode.HighQualityBicubic;
+ thumbnailGraph.PixelOffsetMode = PixelOffsetMode.HighQuality;
+ thumbnailGraph.CompositingMode = CompositingMode.SourceOver;
+
+ thumbnailGraph.DrawImage(originalImage, 0, 0, newSize.Width, newSize.Height);
+
+ ImageFormat outputFormat = originalImage.RawFormat;
+
+ // Write to the output stream
+ SaveImage(outputFormat, thumbnail, toStream, quality);
+
+ thumbnailGraph.Dispose();
+ thumbnail.Dispose();
+ originalImage.Dispose();
+ }
+
+ public static string GetImagePath(BaseEntity entity, ImageType imageType, int imageIndex)
+ {
+ var item = entity as BaseItem;
+
+ if (item != null)
+ {
+ if (imageType == ImageType.Logo)
+ {
+ return item.LogoImagePath;
+ }
+ if (imageType == ImageType.Backdrop)
+ {
+ return item.BackdropImagePaths.ElementAt(imageIndex);
+ }
+ if (imageType == ImageType.Banner)
+ {
+ return item.BannerImagePath;
+ }
+ if (imageType == ImageType.Art)
+ {
+ return item.ArtImagePath;
+ }
+ if (imageType == ImageType.Thumbnail)
+ {
+ return item.ThumbnailImagePath;
+ }
+ }
+
+ return entity.PrimaryImagePath;
+ }
+
+ public static void SaveImage(ImageFormat outputFormat, Image newImage, Stream toStream, int? quality)
+ {
+ // Use special save methods for jpeg and png that will result in a much higher quality image
+ // All other formats use the generic Image.Save
+ if (ImageFormat.Jpeg.Equals(outputFormat))
+ {
+ SaveJpeg(newImage, toStream, quality);
+ }
+ else if (ImageFormat.Png.Equals(outputFormat))
+ {
+ newImage.Save(toStream, ImageFormat.Png);
+ }
+ else
+ {
+ newImage.Save(toStream, outputFormat);
+ }
+ }
+
+ public static void SaveJpeg(Image image, Stream target, int? quality)
+ {
+ if (!quality.HasValue)
+ {
+ quality = 90;
+ }
+
+ using (var encoderParameters = new EncoderParameters(1))
+ {
+ encoderParameters.Param[0] = new EncoderParameter(Encoder.Quality, quality.Value);
+ image.Save(target, GetImageCodecInfo("image/jpeg"), encoderParameters);
+ }
+ }
+
+ public static ImageCodecInfo GetImageCodecInfo(string mimeType)
+ {
+ ImageCodecInfo[] info = ImageCodecInfo.GetImageEncoders();
+
+ for (int i = 0; i < info.Length; i++)
+ {
+ ImageCodecInfo ici = info[i];
+ if (ici.MimeType.Equals(mimeType, StringComparison.OrdinalIgnoreCase))
+ {
+ return ici;
+ }
+ }
+ return info[1];
+ }
+ }
+}
diff --git a/MediaBrowser.Api/HttpHandlers/AudioHandler.cs b/MediaBrowser.Api/HttpHandlers/AudioHandler.cs
new file mode 100644
index 000000000..9c16acd2e
--- /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
+{
+ /// <summary>
+ /// Supported output formats are: mp3,flac,ogg,wav,asf,wma,aac
+ /// </summary>
+ [Export(typeof(BaseHandler))]
+ public class AudioHandler : BaseMediaHandler<Audio, AudioOutputFormats>
+ {
+ public override bool HandlesRequest(HttpListenerRequest request)
+ {
+ return ApiService.IsApiUrlMatch("audio", request);
+ }
+
+ /// <summary>
+ /// We can output these formats directly, but we cannot encode to them.
+ /// </summary>
+ protected override IEnumerable<AudioOutputFormats> UnsupportedOutputEncodingFormats
+ {
+ get
+ {
+ return new AudioOutputFormats[] { AudioOutputFormats.Aac, AudioOutputFormats.Flac, AudioOutputFormats.Wma };
+ }
+ }
+
+ private int? GetMaxAcceptedBitRate(AudioOutputFormats audioFormat)
+ {
+ return GetMaxAcceptedBitRate(audioFormat.ToString());
+ }
+
+ private int? GetMaxAcceptedBitRate(string audioFormat)
+ {
+ if (audioFormat.Equals("mp3", System.StringComparison.OrdinalIgnoreCase))
+ {
+ return 320000;
+ }
+
+ return null;
+ }
+
+ /// <summary>
+ /// Determines whether or not the original file requires transcoding
+ /// </summary>
+ protected override bool RequiresConversion()
+ {
+ if (base.RequiresConversion())
+ {
+ return true;
+ }
+
+ string currentFormat = Path.GetExtension(LibraryItem.Path).Replace(".", string.Empty);
+
+ int? bitrate = GetMaxAcceptedBitRate(currentFormat);
+
+ // If the bitrate is greater than our desired bitrate, we need to transcode
+ if (bitrate.HasValue && bitrate.Value < LibraryItem.BitRate)
+ {
+ return true;
+ }
+
+ // If the number of channels is greater than our desired channels, we need to transcode
+ if (AudioChannels.HasValue && AudioChannels.Value < LibraryItem.Channels)
+ {
+ return true;
+ }
+
+ // If the sample rate is greater than our desired sample rate, we need to transcode
+ if (AudioSampleRate.HasValue && AudioSampleRate.Value < LibraryItem.SampleRate)
+ {
+ return true;
+ }
+
+ // Yay
+ return false;
+ }
+
+ /// <summary>
+ /// Creates arguments to pass to ffmpeg
+ /// </summary>
+ protected override string GetCommandLineArguments()
+ {
+ var audioTranscodeParams = new List<string>();
+
+ AudioOutputFormats outputFormat = GetConversionOutputFormat();
+
+ int? bitrate = GetMaxAcceptedBitRate(outputFormat);
+
+ if (bitrate.HasValue)
+ {
+ audioTranscodeParams.Add("-ab " + bitrate.Value);
+ }
+
+ int? channels = GetNumAudioChannelsParam(LibraryItem.Channels);
+
+ if (channels.HasValue)
+ {
+ audioTranscodeParams.Add("-ac " + channels.Value);
+ }
+
+ int? sampleRate = GetSampleRateParam(LibraryItem.SampleRate);
+
+ if (sampleRate.HasValue)
+ {
+ audioTranscodeParams.Add("-ar " + sampleRate.Value);
+ }
+
+ audioTranscodeParams.Add("-f " + outputFormat);
+
+ return "-i \"" + LibraryItem.Path + "\" -vn " + string.Join(" ", audioTranscodeParams.ToArray()) + " -";
+ }
+ }
+}
diff --git a/MediaBrowser.Api/HttpHandlers/BaseMediaHandler.cs b/MediaBrowser.Api/HttpHandlers/BaseMediaHandler.cs
new file mode 100644
index 000000000..96ef60681
--- /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<TBaseItemType, TOutputType> : BaseHandler
+ where TBaseItemType : BaseItem, new()
+ {
+ /// <summary>
+ /// Supported values: mp3,flac,ogg,wav,asf,wma,aac
+ /// </summary>
+ protected virtual IEnumerable<TOutputType> OutputFormats
+ {
+ get
+ {
+ return QueryString["outputformats"].Split(',').Select(o => (TOutputType)Enum.Parse(typeof(TOutputType), o, true));
+ }
+ }
+
+ /// <summary>
+ /// These formats can be outputted directly but cannot be encoded to
+ /// </summary>
+ protected virtual IEnumerable<TOutputType> UnsupportedOutputEncodingFormats
+ {
+ get
+ {
+ return new TOutputType[] { };
+ }
+ }
+
+ private TBaseItemType _libraryItem;
+ /// <summary>
+ /// Gets the library item that will be played, if any
+ /// </summary>
+ protected TBaseItemType LibraryItem
+ {
+ get
+ {
+ if (_libraryItem == null)
+ {
+ string id = QueryString["id"];
+
+ if (!string.IsNullOrEmpty(id))
+ {
+ _libraryItem = Kernel.Instance.GetItemById(Guid.Parse(id)) as TBaseItemType;
+ }
+ }
+
+ return _libraryItem;
+ }
+ }
+
+ public int? AudioChannels
+ {
+ get
+ {
+ string val = QueryString["audiochannels"];
+
+ if (string.IsNullOrEmpty(val))
+ {
+ return null;
+ }
+
+ return int.Parse(val);
+ }
+ }
+
+ public int? AudioSampleRate
+ {
+ get
+ {
+ string val = QueryString["audiosamplerate"];
+
+ if (string.IsNullOrEmpty(val))
+ {
+ return 44100;
+ }
+
+ return int.Parse(val);
+ }
+ }
+
+ protected override Task<ResponseInfo> GetResponseInfo()
+ {
+ ResponseInfo info = new ResponseInfo
+ {
+ ContentType = MimeTypes.GetMimeType("." + GetConversionOutputFormat()),
+ CompressResponse = false
+ };
+
+ return Task.FromResult<ResponseInfo>(info);
+ }
+
+ public override Task ProcessRequest(HttpListenerContext ctx)
+ {
+ HttpListenerContext = ctx;
+
+ if (!RequiresConversion())
+ {
+ return new StaticFileHandler { Path = LibraryItem.Path }.ProcessRequest(ctx);
+ }
+
+ return base.ProcessRequest(ctx);
+ }
+
+ protected abstract string GetCommandLineArguments();
+
+ /// <summary>
+ /// Gets the format we'll be converting to
+ /// </summary>
+ protected virtual TOutputType GetConversionOutputFormat()
+ {
+ return OutputFormats.First(f => !UnsupportedOutputEncodingFormats.Any(s => s.ToString().Equals(f.ToString(), StringComparison.OrdinalIgnoreCase)));
+ }
+
+ protected virtual bool RequiresConversion()
+ {
+ string currentFormat = Path.GetExtension(LibraryItem.Path).Replace(".", string.Empty);
+
+ if (OutputFormats.Any(f => currentFormat.EndsWith(f.ToString(), StringComparison.OrdinalIgnoreCase)))
+ {
+ // We can output these files directly, but we can't encode them
+ if (UnsupportedOutputEncodingFormats.Any(f => currentFormat.EndsWith(f.ToString(), StringComparison.OrdinalIgnoreCase)))
+ {
+ return false;
+ }
+ }
+ else
+ {
+ // If it's not in a format the consumer accepts, return true
+ return true;
+ }
+
+ return false;
+ }
+
+ private FileStream LogFileStream { get; set; }
+
+ protected async override Task WriteResponseToOutputStream(Stream stream)
+ {
+ var startInfo = new ProcessStartInfo{};
+
+ startInfo.CreateNoWindow = true;
+
+ startInfo.UseShellExecute = false;
+
+ // Must consume both or ffmpeg may hang due to deadlocks. See comments below.
+ startInfo.RedirectStandardOutput = true;
+ startInfo.RedirectStandardError = true;
+
+ startInfo.FileName = Kernel.Instance.ApplicationPaths.FFMpegPath;
+ startInfo.WorkingDirectory = Kernel.Instance.ApplicationPaths.FFMpegDirectory;
+ startInfo.Arguments = GetCommandLineArguments();
+
+ Logger.LogInfo(startInfo.FileName + " " + startInfo.Arguments);
+
+ var process = new Process{};
+ process.StartInfo = startInfo;
+
+ // FFMpeg writes debug/error info to stderr. This is useful when debugging so let's put it in the log directory.
+ LogFileStream = new FileStream(Path.Combine(Kernel.Instance.ApplicationPaths.LogDirectoryPath, "ffmpeg-" + Guid.NewGuid().ToString() + ".txt"), FileMode.Create);
+
+ process.EnableRaisingEvents = true;
+
+ process.Exited += ProcessExited;
+
+ try
+ {
+ process.Start();
+
+ // MUST read both stdout and stderr asynchronously or a deadlock may occurr
+
+ // Kick off two tasks
+ Task mediaTask = process.StandardOutput.BaseStream.CopyToAsync(stream);
+ Task debugLogTask = process.StandardError.BaseStream.CopyToAsync(LogFileStream);
+
+ await mediaTask.ConfigureAwait(false);
+ //await debugLogTask.ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ Logger.LogException(ex);
+
+ // Hate having to do this
+ try
+ {
+ process.Kill();
+ }
+ catch
+ {
+ }
+ }
+ }
+
+ void ProcessExited(object sender, EventArgs e)
+ {
+ if (LogFileStream != null)
+ {
+ LogFileStream.Dispose();
+ }
+
+ var process = sender as Process;
+
+ Logger.LogInfo("FFMpeg exited with code " + process.ExitCode);
+
+ process.Dispose();
+ }
+
+ /// <summary>
+ /// Gets the number of audio channels to specify on the command line
+ /// </summary>
+ protected int? GetNumAudioChannelsParam(int libraryItemChannels)
+ {
+ // If the user requested a max number of channels
+ if (AudioChannels.HasValue)
+ {
+ // Only specify the param if we're going to downmix
+ if (AudioChannels.Value < libraryItemChannels)
+ {
+ return AudioChannels.Value;
+ }
+ }
+
+ return null;
+ }
+
+ /// <summary>
+ /// Gets the number of audio channels to specify on the command line
+ /// </summary>
+ protected int? GetSampleRateParam(int libraryItemSampleRate)
+ {
+ // If the user requested a max value
+ if (AudioSampleRate.HasValue)
+ {
+ // Only specify the param if we're going to downmix
+ if (AudioSampleRate.Value < libraryItemSampleRate)
+ {
+ return AudioSampleRate.Value;
+ }
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/MediaBrowser.Api/HttpHandlers/FavoriteStatusHandler.cs b/MediaBrowser.Api/HttpHandlers/FavoriteStatusHandler.cs
new file mode 100644
index 000000000..19c175d8b
--- /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
+{
+ /// <summary>
+ /// Provides a handler to set user favorite status for an item
+ /// </summary>
+ [Export(typeof(BaseHandler))]
+ public class FavoriteStatusHandler : BaseSerializationHandler<DtoUserItemData>
+ {
+ public override bool HandlesRequest(HttpListenerRequest request)
+ {
+ return ApiService.IsApiUrlMatch("FavoriteStatus", request);
+ }
+
+ protected override Task<DtoUserItemData> GetObjectToSerialize()
+ {
+ // Get the item
+ BaseItem item = ApiService.GetItemById(QueryString["id"]);
+
+ // Get the user
+ User user = ApiService.GetUserById(QueryString["userid"], true);
+
+ // Get the user data for this item
+ UserItemData data = item.GetUserData(user, true);
+
+ // Set favorite status
+ data.IsFavorite = QueryString["isfavorite"] == "1";
+
+ return Task.FromResult(ApiService.GetDtoUserItemData(data));
+ }
+ }
+} \ 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 000000000..7cca2aea7
--- /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
+{
+ /// <summary>
+ /// Gets a single genre
+ /// </summary>
+ [Export(typeof(BaseHandler))]
+ public class GenreHandler : BaseSerializationHandler<IbnItem>
+ {
+ public override bool HandlesRequest(HttpListenerRequest request)
+ {
+ return ApiService.IsApiUrlMatch("genre", request);
+ }
+
+ protected override Task<IbnItem> GetObjectToSerialize()
+ {
+ var parent = ApiService.GetItemById(QueryString["id"]) as Folder;
+ var user = ApiService.GetUserById(QueryString["userid"], true);
+
+ string name = QueryString["name"];
+
+ return GetGenre(parent, user, name);
+ }
+
+ /// <summary>
+ /// Gets a Genre
+ /// </summary>
+ private async Task<IbnItem> GetGenre(Folder parent, User user, string name)
+ {
+ int count = 0;
+
+ // Get all the allowed recursive children
+ IEnumerable<BaseItem> allItems = parent.GetRecursiveChildren(user);
+
+ foreach (var item in allItems)
+ {
+ if (item.Genres != null && item.Genres.Any(s => s.Equals(name, StringComparison.OrdinalIgnoreCase)))
+ {
+ count++;
+ }
+ }
+
+ // Get the original entity so that we can also supply the PrimaryImagePath
+ return ApiService.GetIbnItem(await Kernel.Instance.ItemController.GetGenre(name).ConfigureAwait(false), count);
+ }
+ }
+}
diff --git a/MediaBrowser.Api/HttpHandlers/GenresHandler.cs b/MediaBrowser.Api/HttpHandlers/GenresHandler.cs
new file mode 100644
index 000000000..4c5a9f4b7
--- /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<IbnItem[]>
+ {
+ public override bool HandlesRequest(HttpListenerRequest request)
+ {
+ return ApiService.IsApiUrlMatch("genres", request);
+ }
+
+ protected override Task<IbnItem[]> GetObjectToSerialize()
+ {
+ var parent = ApiService.GetItemById(QueryString["id"]) as Folder;
+ User user = ApiService.GetUserById(QueryString["userid"], true);
+
+ return GetAllGenres(parent, user);
+ }
+
+ /// <summary>
+ /// Gets all genres from all recursive children of a folder
+ /// The CategoryInfo class is used to keep track of the number of times each genres appears
+ /// </summary>
+ private async Task<IbnItem[]> GetAllGenres(Folder parent, User user)
+ {
+ var data = new Dictionary<string, int>();
+
+ // Get all the allowed recursive children
+ IEnumerable<BaseItem> allItems = parent.GetRecursiveChildren(user);
+
+ foreach (var item in allItems)
+ {
+ // Add each genre from the item to the data dictionary
+ // If the genre already exists, increment the count
+ if (item.Genres == null)
+ {
+ continue;
+ }
+
+ foreach (string val in item.Genres)
+ {
+ if (!data.ContainsKey(val))
+ {
+ data.Add(val, 1);
+ }
+ else
+ {
+ data[val]++;
+ }
+ }
+ }
+
+ // Get the Genre objects
+ Genre[] entities = await Task.WhenAll(data.Keys.Select(key => Kernel.Instance.ItemController.GetGenre(key))).ConfigureAwait(false);
+
+ // Convert to an array of IBNItem
+ var items = new IbnItem[entities.Length];
+
+ for (int i = 0; i < entities.Length; i++)
+ {
+ Genre e = entities[i];
+
+ items[i] = ApiService.GetIbnItem(e, data[e.Name]);
+ }
+
+ return items;
+ }
+ }
+}
diff --git a/MediaBrowser.Api/HttpHandlers/ImageHandler.cs b/MediaBrowser.Api/HttpHandlers/ImageHandler.cs
new file mode 100644
index 000000000..4aa367fb7
--- /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<string> GetImagePath()
+ {
+ _imagePath = _imagePath ?? await DiscoverImagePath();
+
+ return _imagePath;
+ }
+
+ private BaseEntity _sourceEntity;
+
+ private async Task<BaseEntity> GetSourceEntity()
+ {
+ if (_sourceEntity == null)
+ {
+ if (!string.IsNullOrEmpty(QueryString["personname"]))
+ {
+ _sourceEntity =
+ await Kernel.Instance.ItemController.GetPerson(QueryString["personname"]).ConfigureAwait(false);
+ }
+
+ else if (!string.IsNullOrEmpty(QueryString["genre"]))
+ {
+ _sourceEntity =
+ await Kernel.Instance.ItemController.GetGenre(QueryString["genre"]).ConfigureAwait(false);
+ }
+
+ else if (!string.IsNullOrEmpty(QueryString["year"]))
+ {
+ _sourceEntity =
+ await
+ Kernel.Instance.ItemController.GetYear(int.Parse(QueryString["year"])).ConfigureAwait(false);
+ }
+
+ else if (!string.IsNullOrEmpty(QueryString["studio"]))
+ {
+ _sourceEntity =
+ await Kernel.Instance.ItemController.GetStudio(QueryString["studio"]).ConfigureAwait(false);
+ }
+
+ else if (!string.IsNullOrEmpty(QueryString["userid"]))
+ {
+ _sourceEntity = ApiService.GetUserById(QueryString["userid"], false);
+ }
+
+ else
+ {
+ _sourceEntity = ApiService.GetItemById(QueryString["id"]);
+ }
+ }
+
+ return _sourceEntity;
+ }
+
+ private async Task<string> DiscoverImagePath()
+ {
+ var entity = await GetSourceEntity().ConfigureAwait(false);
+
+ return ImageProcessor.GetImagePath(entity, ImageType, ImageIndex);
+ }
+
+ protected async override Task<ResponseInfo> GetResponseInfo()
+ {
+ string path = await GetImagePath().ConfigureAwait(false);
+
+ ResponseInfo info = new ResponseInfo
+ {
+ CacheDuration = TimeSpan.FromDays(365),
+ ContentType = MimeTypes.GetMimeType(path)
+ };
+
+ DateTime? date = File.GetLastWriteTimeUtc(path);
+
+ // If the file does not exist it will return jan 1, 1601
+ // http://msdn.microsoft.com/en-us/library/system.io.file.getlastwritetimeutc.aspx
+ if (date.Value.Year == 1601)
+ {
+ if (!File.Exists(path))
+ {
+ info.StatusCode = 404;
+ date = null;
+ }
+ }
+
+ info.DateLastModified = date;
+
+ return info;
+ }
+
+ private int ImageIndex
+ {
+ get
+ {
+ string val = QueryString["index"];
+
+ if (string.IsNullOrEmpty(val))
+ {
+ return 0;
+ }
+
+ return int.Parse(val);
+ }
+ }
+
+ private int? Height
+ {
+ get
+ {
+ string val = QueryString["height"];
+
+ if (string.IsNullOrEmpty(val))
+ {
+ return null;
+ }
+
+ return int.Parse(val);
+ }
+ }
+
+ private int? Width
+ {
+ get
+ {
+ string val = QueryString["width"];
+
+ if (string.IsNullOrEmpty(val))
+ {
+ return null;
+ }
+
+ return int.Parse(val);
+ }
+ }
+
+ private int? MaxHeight
+ {
+ get
+ {
+ string val = QueryString["maxheight"];
+
+ if (string.IsNullOrEmpty(val))
+ {
+ return null;
+ }
+
+ return int.Parse(val);
+ }
+ }
+
+ private int? MaxWidth
+ {
+ get
+ {
+ string val = QueryString["maxwidth"];
+
+ if (string.IsNullOrEmpty(val))
+ {
+ return null;
+ }
+
+ return int.Parse(val);
+ }
+ }
+
+ private int? Quality
+ {
+ get
+ {
+ string val = QueryString["quality"];
+
+ if (string.IsNullOrEmpty(val))
+ {
+ return null;
+ }
+
+ return int.Parse(val);
+ }
+ }
+
+ private ImageType ImageType
+ {
+ get
+ {
+ string imageType = QueryString["type"];
+
+ if (string.IsNullOrEmpty(imageType))
+ {
+ return ImageType.Primary;
+ }
+
+ return (ImageType)Enum.Parse(typeof(ImageType), imageType, true);
+ }
+ }
+
+ protected override async Task WriteResponseToOutputStream(Stream stream)
+ {
+ var entity = await GetSourceEntity().ConfigureAwait(false);
+
+ ImageProcessor.ProcessImage(entity, ImageType, ImageIndex, stream, Width, Height, MaxWidth, MaxHeight, Quality);
+ }
+ }
+}
diff --git a/MediaBrowser.Api/HttpHandlers/ItemHandler.cs b/MediaBrowser.Api/HttpHandlers/ItemHandler.cs
new file mode 100644
index 000000000..60b328d1a
--- /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
+{
+ /// <summary>
+ /// Provides a handler to retrieve a single item
+ /// </summary>
+ [Export(typeof(BaseHandler))]
+ public class ItemHandler : BaseSerializationHandler<DtoBaseItem>
+ {
+ public override bool HandlesRequest(HttpListenerRequest request)
+ {
+ return ApiService.IsApiUrlMatch("item", request);
+ }
+
+ protected override Task<DtoBaseItem> GetObjectToSerialize()
+ {
+ User user = ApiService.GetUserById(QueryString["userid"], true);
+
+ BaseItem item = ApiService.GetItemById(QueryString["id"]);
+
+ if (item == null)
+ {
+ return null;
+ }
+
+ return ApiService.GetDtoBaseItem(item, user);
+ }
+ }
+}
diff --git a/MediaBrowser.Api/HttpHandlers/ItemListHandler.cs b/MediaBrowser.Api/HttpHandlers/ItemListHandler.cs
new file mode 100644
index 000000000..d236e546b
--- /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<DtoBaseItem[]>
+ {
+ public override bool HandlesRequest(HttpListenerRequest request)
+ {
+ return ApiService.IsApiUrlMatch("itemlist", request);
+ }
+
+ protected override Task<DtoBaseItem[]> GetObjectToSerialize()
+ {
+ User user = ApiService.GetUserById(QueryString["userid"], true);
+
+ return Task.WhenAll(GetItemsToSerialize(user).Select(i => ApiService.GetDtoBaseItem(i, user, includeChildren: false, includePeople: false)));
+ }
+
+ private IEnumerable<BaseItem> GetItemsToSerialize(User user)
+ {
+ var parent = ApiService.GetItemById(ItemId) as Folder;
+
+ if (ListType.Equals("inprogressitems", StringComparison.OrdinalIgnoreCase))
+ {
+ return parent.GetInProgressItems(user);
+ }
+ if (ListType.Equals("recentlyaddeditems", StringComparison.OrdinalIgnoreCase))
+ {
+ return parent.GetRecentlyAddedItems(user);
+ }
+ if (ListType.Equals("recentlyaddedunplayeditems", StringComparison.OrdinalIgnoreCase))
+ {
+ return parent.GetRecentlyAddedUnplayedItems(user);
+ }
+ if (ListType.Equals("itemswithgenre", StringComparison.OrdinalIgnoreCase))
+ {
+ return parent.GetItemsWithGenre(QueryString["name"], user);
+ }
+ if (ListType.Equals("itemswithyear", StringComparison.OrdinalIgnoreCase))
+ {
+ return parent.GetItemsWithYear(int.Parse(QueryString["year"]), user);
+ }
+ if (ListType.Equals("itemswithstudio", StringComparison.OrdinalIgnoreCase))
+ {
+ return parent.GetItemsWithStudio(QueryString["name"], user);
+ }
+ if (ListType.Equals("itemswithperson", StringComparison.OrdinalIgnoreCase))
+ {
+ return parent.GetItemsWithPerson(QueryString["name"], null, user);
+ }
+ if (ListType.Equals("favorites", StringComparison.OrdinalIgnoreCase))
+ {
+ return parent.GetFavoriteItems(user);
+ }
+
+ throw new InvalidOperationException();
+ }
+
+ protected string ItemId
+ {
+ get
+ {
+ return QueryString["id"];
+ }
+ }
+
+ private string ListType
+ {
+ get
+ {
+ return QueryString["listtype"] ?? string.Empty;
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Api/HttpHandlers/MovieSpecialFeaturesHandler.cs b/MediaBrowser.Api/HttpHandlers/MovieSpecialFeaturesHandler.cs
new file mode 100644
index 000000000..3ab78ee8d
--- /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
+{
+ /// <summary>
+ /// This handler retrieves special features for movies
+ /// </summary>
+ [Export(typeof(BaseHandler))]
+ public class MovieSpecialFeaturesHandler : BaseSerializationHandler<DtoBaseItem[]>
+ {
+ public override bool HandlesRequest(HttpListenerRequest request)
+ {
+ return ApiService.IsApiUrlMatch("MovieSpecialFeatures", request);
+ }
+
+ protected override Task<DtoBaseItem[]> GetObjectToSerialize()
+ {
+ User user = ApiService.GetUserById(QueryString["userid"], true);
+
+ var movie = ApiService.GetItemById(ItemId) as Movie;
+
+ // If none
+ if (movie.SpecialFeatures == null)
+ {
+ return Task.FromResult(new DtoBaseItem[] { });
+ }
+
+ return Task.WhenAll(movie.SpecialFeatures.Select(i => ApiService.GetDtoBaseItem(i, user, includeChildren: false)));
+ }
+
+ protected string ItemId
+ {
+ get
+ {
+ return QueryString["id"];
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Api/HttpHandlers/PersonHandler.cs b/MediaBrowser.Api/HttpHandlers/PersonHandler.cs
new file mode 100644
index 000000000..fbbd88a11
--- /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
+{
+ /// <summary>
+ /// Gets a single Person
+ /// </summary>
+ [Export(typeof(BaseHandler))]
+ public class PersonHandler : BaseSerializationHandler<IbnItem>
+ {
+ public override bool HandlesRequest(HttpListenerRequest request)
+ {
+ return ApiService.IsApiUrlMatch("person", request);
+ }
+
+ protected override Task<IbnItem> GetObjectToSerialize()
+ {
+ var parent = ApiService.GetItemById(QueryString["id"]) as Folder;
+ var user = ApiService.GetUserById(QueryString["userid"], true);
+
+ string name = QueryString["name"];
+
+ return GetPerson(parent, user, name);
+ }
+
+ /// <summary>
+ /// Gets a Person
+ /// </summary>
+ private async Task<IbnItem> GetPerson(Folder parent, User user, string name)
+ {
+ int count = 0;
+
+ // Get all the allowed recursive children
+ IEnumerable<BaseItem> allItems = parent.GetRecursiveChildren(user);
+
+ foreach (var item in allItems)
+ {
+ if (item.People != null && item.People.ContainsKey(name))
+ {
+ count++;
+ }
+ }
+
+ // Get the original entity so that we can also supply the PrimaryImagePath
+ return ApiService.GetIbnItem(await Kernel.Instance.ItemController.GetPerson(name).ConfigureAwait(false), count);
+ }
+ }
+}
diff --git a/MediaBrowser.Api/HttpHandlers/PlayedStatusHandler.cs b/MediaBrowser.Api/HttpHandlers/PlayedStatusHandler.cs
new file mode 100644
index 000000000..c010bcb02
--- /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
+{
+ /// <summary>
+ /// Provides a handler to set played status for an item
+ /// </summary>
+ [Export(typeof(BaseHandler))]
+ public class PlayedStatusHandler : BaseSerializationHandler<DtoUserItemData>
+ {
+ public override bool HandlesRequest(HttpListenerRequest request)
+ {
+ return ApiService.IsApiUrlMatch("PlayedStatus", request);
+ }
+
+ protected override Task<DtoUserItemData> GetObjectToSerialize()
+ {
+ // Get the item
+ BaseItem item = ApiService.GetItemById(QueryString["id"]);
+
+ // Get the user
+ User user = ApiService.GetUserById(QueryString["userid"], true);
+
+ bool wasPlayed = QueryString["played"] == "1";
+
+ item.SetPlayedStatus(user, wasPlayed);
+
+ UserItemData data = item.GetUserData(user, true);
+
+ return Task.FromResult(ApiService.GetDtoUserItemData(data));
+ }
+ }
+} \ 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 000000000..47f08c8c3
--- /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<ResponseInfo> GetResponseInfo()
+ {
+ throw new NotImplementedException();
+ }
+
+ protected override Task WriteResponseToOutputStream(Stream stream)
+ {
+ throw new NotImplementedException();
+ }
+
+ public override Task ProcessRequest(HttpListenerContext ctx)
+ {
+ string filename = ctx.Request.QueryString["assemblyfilename"];
+
+ string path = Path.Combine(Kernel.Instance.ApplicationPaths.PluginsPath, filename);
+
+ return new StaticFileHandler { Path = path }.ProcessRequest(ctx);
+ }
+ }
+}
diff --git a/MediaBrowser.Api/HttpHandlers/PluginConfigurationHandler.cs b/MediaBrowser.Api/HttpHandlers/PluginConfigurationHandler.cs
new file mode 100644
index 000000000..dc363956f
--- /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<BasePluginConfiguration>
+ {
+ public override bool HandlesRequest(HttpListenerRequest request)
+ {
+ return ApiService.IsApiUrlMatch("pluginconfiguration", request);
+ }
+
+ private BasePlugin _plugin;
+ private BasePlugin Plugin
+ {
+ get
+ {
+ if (_plugin == null)
+ {
+ string name = QueryString["assemblyfilename"];
+
+ _plugin = Kernel.Instance.Plugins.First(p => p.AssemblyFileName.Equals(name, StringComparison.OrdinalIgnoreCase));
+ }
+
+ return _plugin;
+ }
+ }
+
+ protected override Task<BasePluginConfiguration> GetObjectToSerialize()
+ {
+ return Task.FromResult(Plugin.Configuration);
+ }
+
+ protected override async Task<ResponseInfo> GetResponseInfo()
+ {
+ var info = await base.GetResponseInfo().ConfigureAwait(false);
+
+ info.DateLastModified = Plugin.ConfigurationDateLastModified;
+
+ info.CacheDuration = TimeSpan.FromDays(7);
+
+ return info;
+ }
+ }
+}
diff --git a/MediaBrowser.Api/HttpHandlers/PluginsHandler.cs b/MediaBrowser.Api/HttpHandlers/PluginsHandler.cs
new file mode 100644
index 000000000..a1b37ecab
--- /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
+{
+ /// <summary>
+ /// Provides information about installed plugins
+ /// </summary>
+ [Export(typeof(BaseHandler))]
+ public class PluginsHandler : BaseSerializationHandler<IEnumerable<PluginInfo>>
+ {
+ public override bool HandlesRequest(HttpListenerRequest request)
+ {
+ return ApiService.IsApiUrlMatch("plugins", request);
+ }
+
+ protected override Task<IEnumerable<PluginInfo>> GetObjectToSerialize()
+ {
+ var plugins = Kernel.Instance.Plugins.Select(p => new PluginInfo
+ {
+ Name = p.Name,
+ Enabled = p.Enabled,
+ DownloadToUI = p.DownloadToUi,
+ Version = p.Version.ToString(),
+ AssemblyFileName = p.AssemblyFileName,
+ ConfigurationDateLastModified = p.ConfigurationDateLastModified
+ });
+
+ return Task.FromResult(plugins);
+ }
+ }
+}
diff --git a/MediaBrowser.Api/HttpHandlers/ServerConfigurationHandler.cs b/MediaBrowser.Api/HttpHandlers/ServerConfigurationHandler.cs
new file mode 100644
index 000000000..48c6761b1
--- /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<ServerConfiguration>
+ {
+ public override bool HandlesRequest(HttpListenerRequest request)
+ {
+ return ApiService.IsApiUrlMatch("serverconfiguration", request);
+ }
+
+ protected override Task<ServerConfiguration> GetObjectToSerialize()
+ {
+ return Task.FromResult(Kernel.Instance.Configuration);
+ }
+
+ protected override async Task<ResponseInfo> GetResponseInfo()
+ {
+ var info = await base.GetResponseInfo().ConfigureAwait(false);
+
+ info.DateLastModified =
+ File.GetLastWriteTimeUtc(Kernel.Instance.ApplicationPaths.SystemConfigurationFilePath);
+
+ info.CacheDuration = TimeSpan.FromDays(7);
+
+ return info;
+ }
+ }
+}
diff --git a/MediaBrowser.Api/HttpHandlers/StudioHandler.cs b/MediaBrowser.Api/HttpHandlers/StudioHandler.cs
new file mode 100644
index 000000000..6576e2cfe
--- /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
+{
+ /// <summary>
+ /// Gets a single studio
+ /// </summary>
+ [Export(typeof(BaseHandler))]
+ public class StudioHandler : BaseSerializationHandler<IbnItem>
+ {
+ public override bool HandlesRequest(HttpListenerRequest request)
+ {
+ return ApiService.IsApiUrlMatch("studio", request);
+ }
+
+ protected override Task<IbnItem> GetObjectToSerialize()
+ {
+ var parent = ApiService.GetItemById(QueryString["id"]) as Folder;
+ var user = ApiService.GetUserById(QueryString["userid"], true);
+
+ string name = QueryString["name"];
+
+ return GetStudio(parent, user, name);
+ }
+
+ /// <summary>
+ /// Gets a Studio
+ /// </summary>
+ private async Task<IbnItem> GetStudio(Folder parent, User user, string name)
+ {
+ int count = 0;
+
+ // Get all the allowed recursive children
+ IEnumerable<BaseItem> allItems = parent.GetRecursiveChildren(user);
+
+ foreach (var item in allItems)
+ {
+ if (item.Studios != null && item.Studios.Any(s => s.Equals(name, StringComparison.OrdinalIgnoreCase)))
+ {
+ count++;
+ }
+ }
+
+ // Get the original entity so that we can also supply the PrimaryImagePath
+ return ApiService.GetIbnItem(await Kernel.Instance.ItemController.GetStudio(name).ConfigureAwait(false), count);
+ }
+ }
+}
diff --git a/MediaBrowser.Api/HttpHandlers/StudiosHandler.cs b/MediaBrowser.Api/HttpHandlers/StudiosHandler.cs
new file mode 100644
index 000000000..4377a0f43
--- /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<IbnItem[]>
+ {
+ public override bool HandlesRequest(HttpListenerRequest request)
+ {
+ return ApiService.IsApiUrlMatch("studios", request);
+ }
+
+ protected override Task<IbnItem[]> GetObjectToSerialize()
+ {
+ var parent = ApiService.GetItemById(QueryString["id"]) as Folder;
+ var user = ApiService.GetUserById(QueryString["userid"], true);
+
+ return GetAllStudios(parent, user);
+ }
+
+ /// <summary>
+ /// Gets all studios from all recursive children of a folder
+ /// The CategoryInfo class is used to keep track of the number of times each studio appears
+ /// </summary>
+ private async Task<IbnItem[]> GetAllStudios(Folder parent, User user)
+ {
+ var data = new Dictionary<string, int>();
+
+ // Get all the allowed recursive children
+ IEnumerable<BaseItem> allItems = parent.GetRecursiveChildren(user);
+
+ foreach (var item in allItems)
+ {
+ // Add each studio from the item to the data dictionary
+ // If the studio already exists, increment the count
+ if (item.Studios == null)
+ {
+ continue;
+ }
+
+ foreach (string val in item.Studios)
+ {
+ if (!data.ContainsKey(val))
+ {
+ data.Add(val, 1);
+ }
+ else
+ {
+ data[val]++;
+ }
+ }
+ }
+
+ // Get the Studio objects
+ Studio[] entities = await Task.WhenAll(data.Keys.Select(key => Kernel.Instance.ItemController.GetStudio(key))).ConfigureAwait(false);
+
+ // Convert to an array of IBNItem
+ var items = new IbnItem[entities.Length];
+
+ for (int i = 0; i < entities.Length; i++)
+ {
+ Studio e = entities[i];
+
+ items[i] = ApiService.GetIbnItem(e, data[e.Name]);
+ }
+
+ return items;
+ }
+ }
+}
diff --git a/MediaBrowser.Api/HttpHandlers/UserAuthenticationHandler.cs b/MediaBrowser.Api/HttpHandlers/UserAuthenticationHandler.cs
new file mode 100644
index 000000000..fa9d97598
--- /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<AuthenticationResult>
+ {
+ public override bool HandlesRequest(HttpListenerRequest request)
+ {
+ return ApiService.IsApiUrlMatch("UserAuthentication", request);
+ }
+
+ protected override async Task<AuthenticationResult> GetObjectToSerialize()
+ {
+ string userId = await GetFormValue("userid").ConfigureAwait(false);
+ User user = ApiService.GetUserById(userId, false);
+
+ string password = await GetFormValue("password").ConfigureAwait(false);
+
+ return Kernel.Instance.AuthenticateUser(user, password);
+ }
+ }
+}
diff --git a/MediaBrowser.Api/HttpHandlers/UserHandler.cs b/MediaBrowser.Api/HttpHandlers/UserHandler.cs
new file mode 100644
index 000000000..bc9286204
--- /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<DtoUser>
+ {
+ public override bool HandlesRequest(HttpListenerRequest request)
+ {
+ return ApiService.IsApiUrlMatch("user", request);
+ }
+
+ protected override Task<DtoUser> GetObjectToSerialize()
+ {
+ string id = QueryString["id"];
+
+ User user = string.IsNullOrEmpty(id) ? ApiService.GetDefaultUser(false) : ApiService.GetUserById(id, false);
+
+ DtoUser dto = ApiService.GetDtoUser(user);
+
+ return Task.FromResult(dto);
+ }
+ }
+}
diff --git a/MediaBrowser.Api/HttpHandlers/UserItemRatingHandler.cs b/MediaBrowser.Api/HttpHandlers/UserItemRatingHandler.cs
new file mode 100644
index 000000000..aed0804b6
--- /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
+{
+ /// <summary>
+ /// Provides a handler to set a user's rating for an item
+ /// </summary>
+ [Export(typeof(BaseHandler))]
+ public class UserItemRatingHandler : BaseSerializationHandler<DtoUserItemData>
+ {
+ public override bool HandlesRequest(HttpListenerRequest request)
+ {
+ return ApiService.IsApiUrlMatch("UserItemRating", request);
+ }
+
+ protected override Task<DtoUserItemData> GetObjectToSerialize()
+ {
+ // Get the item
+ BaseItem item = ApiService.GetItemById(QueryString["id"]);
+
+ // Get the user
+ User user = ApiService.GetUserById(QueryString["userid"], true);
+
+ // Get the user data for this item
+ UserItemData data = item.GetUserData(user, true);
+
+ // If clearing the rating, set it to null
+ if (QueryString["clear"] == "1")
+ {
+ data.Rating = null;
+ }
+
+ else
+ {
+ data.Likes = QueryString["likes"] == "1";
+ }
+
+ return Task.FromResult(ApiService.GetDtoUserItemData(data));
+ }
+ }
+} \ 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 000000000..3fc3a7d58
--- /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<IEnumerable<DtoUser>>
+ {
+ public override bool HandlesRequest(HttpListenerRequest request)
+ {
+ return ApiService.IsApiUrlMatch("users", request);
+ }
+
+ protected override Task<IEnumerable<DtoUser>> GetObjectToSerialize()
+ {
+ return Task.FromResult(Kernel.Instance.Users.Select(u => ApiService.GetDtoUser(u)));
+ }
+ }
+}
diff --git a/MediaBrowser.Api/HttpHandlers/VideoHandler.cs b/MediaBrowser.Api/HttpHandlers/VideoHandler.cs
new file mode 100644
index 000000000..e34a1b41f
--- /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
+{
+ /// <summary>
+ /// Supported output formats: mkv,m4v,mp4,asf,wmv,mov,webm,ogv,3gp,avi,ts,flv
+ /// </summary>
+ [Export(typeof(BaseHandler))]
+ class VideoHandler : BaseMediaHandler<Video, VideoOutputFormats>
+ {
+ public override bool HandlesRequest(HttpListenerRequest request)
+ {
+ return ApiService.IsApiUrlMatch("video", request);
+ }
+
+ /// <summary>
+ /// We can output these files directly, but we can't encode them
+ /// </summary>
+ protected override IEnumerable<VideoOutputFormats> UnsupportedOutputEncodingFormats
+ {
+ get
+ {
+ // mp4, 3gp, mov - muxer does not support non-seekable output
+ // avi, mov, mkv, m4v - can't stream these when encoding. the player will try to download them completely before starting playback.
+ // wmv - can't seem to figure out the output format name
+ return new VideoOutputFormats[] { VideoOutputFormats.Mp4, VideoOutputFormats.ThreeGp, VideoOutputFormats.M4V, VideoOutputFormats.Mkv, VideoOutputFormats.Avi, VideoOutputFormats.Mov, VideoOutputFormats.Wmv };
+ }
+ }
+
+ /// <summary>
+ /// Determines whether or not we can just output the original file directly
+ /// </summary>
+ protected override bool RequiresConversion()
+ {
+ if (base.RequiresConversion())
+ {
+ return true;
+ }
+
+ // See if the video requires conversion
+ if (RequiresVideoConversion())
+ {
+ return true;
+ }
+
+ // See if the audio requires conversion
+ AudioStream audioStream = (LibraryItem.AudioStreams ?? new List<AudioStream>()).FirstOrDefault();
+
+ if (audioStream != null)
+ {
+ if (RequiresAudioConversion(audioStream))
+ {
+ return true;
+ }
+ }
+
+ // Yay
+ return false;
+ }
+
+ /// <summary>
+ /// Translates the output file extension to the format param that follows "-f" on the ffmpeg command line
+ /// </summary>
+ private string GetFfMpegOutputFormat(VideoOutputFormats outputFormat)
+ {
+ if (outputFormat == VideoOutputFormats.Mkv)
+ {
+ return "matroska";
+ }
+ if (outputFormat == VideoOutputFormats.Ts)
+ {
+ return "mpegts";
+ }
+ if (outputFormat == VideoOutputFormats.Ogv)
+ {
+ return "ogg";
+ }
+
+ return outputFormat.ToString().ToLower();
+ }
+
+ /// <summary>
+ /// Creates arguments to pass to ffmpeg
+ /// </summary>
+ protected override string GetCommandLineArguments()
+ {
+ VideoOutputFormats outputFormat = GetConversionOutputFormat();
+
+ return string.Format("-i \"{0}\" -threads 0 {1} {2} -f {3} -",
+ LibraryItem.Path,
+ GetVideoArguments(outputFormat),
+ GetAudioArguments(outputFormat),
+ GetFfMpegOutputFormat(outputFormat)
+ );
+ }
+
+ /// <summary>
+ /// Gets video arguments to pass to ffmpeg
+ /// </summary>
+ private string GetVideoArguments(VideoOutputFormats outputFormat)
+ {
+ // Get the output codec name
+ string codec = GetVideoCodec(outputFormat);
+
+ string args = "-vcodec " + codec;
+
+ // If we're encoding video, add additional params
+ if (!codec.Equals("copy", StringComparison.OrdinalIgnoreCase))
+ {
+ // Add resolution params, if specified
+ if (Width.HasValue || Height.HasValue || MaxHeight.HasValue || MaxWidth.HasValue)
+ {
+ Size size = DrawingUtils.Resize(LibraryItem.Width, LibraryItem.Height, Width, Height, MaxWidth, MaxHeight);
+
+ args += string.Format(" -s {0}x{1}", size.Width, size.Height);
+ }
+ }
+
+ return args;
+ }
+
+ /// <summary>
+ /// Gets audio arguments to pass to ffmpeg
+ /// </summary>
+ private string GetAudioArguments(VideoOutputFormats outputFormat)
+ {
+ AudioStream audioStream = (LibraryItem.AudioStreams ?? new List<AudioStream>()).FirstOrDefault();
+
+ // If the video doesn't have an audio stream, return empty
+ if (audioStream == null)
+ {
+ return string.Empty;
+ }
+
+ // Get the output codec name
+ string codec = GetAudioCodec(audioStream, outputFormat);
+
+ string args = "-acodec " + codec;
+
+ // If we're encoding audio, add additional params
+ if (!codec.Equals("copy", StringComparison.OrdinalIgnoreCase))
+ {
+ // Add the number of audio channels
+ int? channels = GetNumAudioChannelsParam(codec, audioStream.Channels);
+
+ if (channels.HasValue)
+ {
+ args += " -ac " + channels.Value;
+ }
+
+ // Add the audio sample rate
+ int? sampleRate = GetSampleRateParam(audioStream.SampleRate);
+
+ if (sampleRate.HasValue)
+ {
+ args += " -ar " + sampleRate.Value;
+ }
+
+ }
+
+ return args;
+ }
+
+ /// <summary>
+ /// Gets the name of the output video codec
+ /// </summary>
+ private string GetVideoCodec(VideoOutputFormats outputFormat)
+ {
+ // Some output containers require specific codecs
+
+ if (outputFormat == VideoOutputFormats.Webm)
+ {
+ // Per webm specification, it must be vpx
+ return "libvpx";
+ }
+ if (outputFormat == VideoOutputFormats.Asf)
+ {
+ return "wmv2";
+ }
+ if (outputFormat == VideoOutputFormats.Wmv)
+ {
+ return "wmv2";
+ }
+ if (outputFormat == VideoOutputFormats.Ogv)
+ {
+ return "libtheora";
+ }
+
+ // Skip encoding when possible
+ if (!RequiresVideoConversion())
+ {
+ return "copy";
+ }
+
+ return "libx264";
+ }
+
+ /// <summary>
+ /// Gets the name of the output audio codec
+ /// </summary>
+ private string GetAudioCodec(AudioStream audioStream, VideoOutputFormats outputFormat)
+ {
+ // Some output containers require specific codecs
+
+ if (outputFormat == VideoOutputFormats.Webm)
+ {
+ // Per webm specification, it must be vorbis
+ return "libvorbis";
+ }
+ if (outputFormat == VideoOutputFormats.Asf)
+ {
+ return "wmav2";
+ }
+ if (outputFormat == VideoOutputFormats.Wmv)
+ {
+ return "wmav2";
+ }
+ if (outputFormat == VideoOutputFormats.Ogv)
+ {
+ return "libvorbis";
+ }
+
+ // Skip encoding when possible
+ if (!RequiresAudioConversion(audioStream))
+ {
+ return "copy";
+ }
+
+ return "libvo_aacenc";
+ }
+
+ /// <summary>
+ /// Gets the number of audio channels to specify on the command line
+ /// </summary>
+ private int? GetNumAudioChannelsParam(string audioCodec, int libraryItemChannels)
+ {
+ if (libraryItemChannels > 2)
+ {
+ if (audioCodec.Equals("libvo_aacenc"))
+ {
+ // libvo_aacenc currently only supports two channel output
+ return 2;
+ }
+ if (audioCodec.Equals("wmav2"))
+ {
+ // wmav2 currently only supports two channel output
+ return 2;
+ }
+ }
+
+ return GetNumAudioChannelsParam(libraryItemChannels);
+ }
+
+ /// <summary>
+ /// Determines if the video stream requires encoding
+ /// </summary>
+ private bool RequiresVideoConversion()
+ {
+ // Check dimensions
+
+ // If a specific width is required, validate that
+ if (Width.HasValue)
+ {
+ if (Width.Value != LibraryItem.Width)
+ {
+ return true;
+ }
+ }
+
+ // If a specific height is required, validate that
+ if (Height.HasValue)
+ {
+ if (Height.Value != LibraryItem.Height)
+ {
+ return true;
+ }
+ }
+
+ // If a max width is required, validate that
+ if (MaxWidth.HasValue)
+ {
+ if (MaxWidth.Value < LibraryItem.Width)
+ {
+ return true;
+ }
+ }
+
+ // If a max height is required, validate that
+ if (MaxHeight.HasValue)
+ {
+ if (MaxHeight.Value < LibraryItem.Height)
+ {
+ return true;
+ }
+ }
+
+ // If the codec is already h264, don't encode
+ if (LibraryItem.Codec.IndexOf("264", StringComparison.OrdinalIgnoreCase) != -1 || LibraryItem.Codec.IndexOf("avc", StringComparison.OrdinalIgnoreCase) != -1)
+ {
+ return false;
+ }
+
+ return false;
+ }
+
+ /// <summary>
+ /// Determines if the audio stream requires encoding
+ /// </summary>
+ private bool RequiresAudioConversion(AudioStream audio)
+ {
+
+ // If the input stream has more audio channels than the client can handle, we need to encode
+ if (AudioChannels.HasValue)
+ {
+ if (audio.Channels > AudioChannels.Value)
+ {
+ return true;
+ }
+ }
+
+ // Aac, ac-3 and mp3 are all pretty much universally supported. No need to encode them
+
+ if (audio.Codec.IndexOf("aac", StringComparison.OrdinalIgnoreCase) != -1)
+ {
+ return false;
+ }
+
+ if (audio.Codec.IndexOf("ac-3", StringComparison.OrdinalIgnoreCase) != -1 || audio.Codec.IndexOf("ac3", StringComparison.OrdinalIgnoreCase) != -1)
+ {
+ return false;
+ }
+
+ if (audio.Codec.IndexOf("mpeg", StringComparison.OrdinalIgnoreCase) != -1 || audio.Codec.IndexOf("mp3", StringComparison.OrdinalIgnoreCase) != -1)
+ {
+ return false;
+ }
+
+ return true;
+ }
+
+ /// <summary>
+ /// Gets the fixed output video height, in pixels
+ /// </summary>
+ private int? Height
+ {
+ get
+ {
+ string val = QueryString["height"];
+
+ if (string.IsNullOrEmpty(val))
+ {
+ return null;
+ }
+
+ return int.Parse(val);
+ }
+ }
+
+ /// <summary>
+ /// Gets the fixed output video width, in pixels
+ /// </summary>
+ private int? Width
+ {
+ get
+ {
+ string val = QueryString["width"];
+
+ if (string.IsNullOrEmpty(val))
+ {
+ return null;
+ }
+
+ return int.Parse(val);
+ }
+ }
+
+ /// <summary>
+ /// Gets the maximum output video height, in pixels
+ /// </summary>
+ private int? MaxHeight
+ {
+ get
+ {
+ string val = QueryString["maxheight"];
+
+ if (string.IsNullOrEmpty(val))
+ {
+ return null;
+ }
+
+ return int.Parse(val);
+ }
+ }
+
+ /// <summary>
+ /// Gets the maximum output video width, in pixels
+ /// </summary>
+ private int? MaxWidth
+ {
+ get
+ {
+ string val = QueryString["maxwidth"];
+
+ if (string.IsNullOrEmpty(val))
+ {
+ return null;
+ }
+
+ return int.Parse(val);
+ }
+ }
+
+ }
+}
diff --git a/MediaBrowser.Api/HttpHandlers/WeatherHandler.cs b/MediaBrowser.Api/HttpHandlers/WeatherHandler.cs
new file mode 100644
index 000000000..378e89067
--- /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<WeatherInfo>
+ {
+ public override bool HandlesRequest(HttpListenerRequest request)
+ {
+ return ApiService.IsApiUrlMatch("weather", request);
+ }
+
+ protected override Task<WeatherInfo> GetObjectToSerialize()
+ {
+ // If a specific zip code was requested on the query string, use that. Otherwise use the value from configuration
+
+ string zipCode = QueryString["zipcode"];
+
+ if (string.IsNullOrWhiteSpace(zipCode))
+ {
+ zipCode = Kernel.Instance.Configuration.WeatherZipCode;
+ }
+
+ return Kernel.Instance.WeatherProviders.First().GetWeatherInfoAsync(zipCode);
+ }
+
+ protected override async Task<ResponseInfo> GetResponseInfo()
+ {
+ var info = await base.GetResponseInfo().ConfigureAwait(false);
+
+ info.CacheDuration = TimeSpan.FromMinutes(15);
+
+ return info;
+ }
+ }
+}
diff --git a/MediaBrowser.Api/HttpHandlers/YearHandler.cs b/MediaBrowser.Api/HttpHandlers/YearHandler.cs
new file mode 100644
index 000000000..dbd1d25be
--- /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
+{
+ /// <summary>
+ /// Gets a single year
+ /// </summary>
+ [Export(typeof(BaseHandler))]
+ public class YearHandler : BaseSerializationHandler<IbnItem>
+ {
+ public override bool HandlesRequest(HttpListenerRequest request)
+ {
+ return ApiService.IsApiUrlMatch("year", request);
+ }
+
+ protected override Task<IbnItem> GetObjectToSerialize()
+ {
+ var parent = ApiService.GetItemById(QueryString["id"]) as Folder;
+ var user = ApiService.GetUserById(QueryString["userid"], true);
+
+ string year = QueryString["year"];
+
+ return GetYear(parent, user, int.Parse(year));
+ }
+
+ /// <summary>
+ /// Gets a Year
+ /// </summary>
+ private async Task<IbnItem> GetYear(Folder parent, User user, int year)
+ {
+ int count = 0;
+
+ // Get all the allowed recursive children
+ IEnumerable<BaseItem> allItems = parent.GetRecursiveChildren(user);
+
+ foreach (var item in allItems)
+ {
+ if (item.ProductionYear.HasValue && item.ProductionYear.Value == year)
+ {
+ count++;
+ }
+ }
+
+ // Get the original entity so that we can also supply the PrimaryImagePath
+ return ApiService.GetIbnItem(await Kernel.Instance.ItemController.GetYear(year).ConfigureAwait(false), count);
+ }
+ }
+}
diff --git a/MediaBrowser.Api/HttpHandlers/YearsHandler.cs b/MediaBrowser.Api/HttpHandlers/YearsHandler.cs
new file mode 100644
index 000000000..7c90768e8
--- /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<IbnItem[]>
+ {
+ public override bool HandlesRequest(HttpListenerRequest request)
+ {
+ return ApiService.IsApiUrlMatch("years", request);
+ }
+
+ protected override Task<IbnItem[]> GetObjectToSerialize()
+ {
+ var parent = ApiService.GetItemById(QueryString["id"]) as Folder;
+ User user = ApiService.GetUserById(QueryString["userid"], true);
+
+ return GetAllYears(parent, user);
+ }
+
+ /// <summary>
+ /// Gets all years from all recursive children of a folder
+ /// The CategoryInfo class is used to keep track of the number of times each year appears
+ /// </summary>
+ private async Task<IbnItem[]> GetAllYears(Folder parent, User user)
+ {
+ var data = new Dictionary<int, int>();
+
+ // Get all the allowed recursive children
+ IEnumerable<BaseItem> allItems = parent.GetRecursiveChildren(user);
+
+ foreach (var item in allItems)
+ {
+ // Add the year from the item to the data dictionary
+ // If the year already exists, increment the count
+ if (item.ProductionYear == null)
+ {
+ continue;
+ }
+
+ if (!data.ContainsKey(item.ProductionYear.Value))
+ {
+ data.Add(item.ProductionYear.Value, 1);
+ }
+ else
+ {
+ data[item.ProductionYear.Value]++;
+ }
+ }
+
+ // Get the Year objects
+ Year[] entities = await Task.WhenAll(data.Keys.Select(key => Kernel.Instance.ItemController.GetYear(key))).ConfigureAwait(false);
+
+ // Convert to an array of IBNItem
+ var items = new IbnItem[entities.Length];
+
+ for (int i = 0; i < entities.Length; i++)
+ {
+ Year e = entities[i];
+
+ items[i] = ApiService.GetIbnItem(e, data[int.Parse(e.Name)]);
+ }
+
+ return items;
+ }
+ }
+}
diff --git a/MediaBrowser.Api/MediaBrowser.Api.csproj b/MediaBrowser.Api/MediaBrowser.Api.csproj
new file mode 100644
index 000000000..1af7e71bd
--- /dev/null
+++ b/MediaBrowser.Api/MediaBrowser.Api.csproj
@@ -0,0 +1,117 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
+ <PropertyGroup>
+ <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+ <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+ <ProjectGuid>{4FD51AC5-2C16-4308-A993-C3A84F3B4582}</ProjectGuid>
+ <OutputType>Library</OutputType>
+ <AppDesignerFolder>Properties</AppDesignerFolder>
+ <RootNamespace>MediaBrowser.Api</RootNamespace>
+ <AssemblyName>MediaBrowser.Api</AssemblyName>
+ <TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
+ <FileAlignment>512</FileAlignment>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
+ <DebugSymbols>true</DebugSymbols>
+ <DebugType>full</DebugType>
+ <Optimize>false</Optimize>
+ <OutputPath>bin\Debug\</OutputPath>
+ <DefineConstants>DEBUG;TRACE</DefineConstants>
+ <ErrorReport>prompt</ErrorReport>
+ <WarningLevel>4</WarningLevel>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
+ <DebugType>pdbonly</DebugType>
+ <Optimize>true</Optimize>
+ <OutputPath>bin\Release\</OutputPath>
+ <DefineConstants>TRACE</DefineConstants>
+ <ErrorReport>prompt</ErrorReport>
+ <WarningLevel>4</WarningLevel>
+ </PropertyGroup>
+ <PropertyGroup>
+ <RunPostBuildEvent>Always</RunPostBuildEvent>
+ </PropertyGroup>
+ <ItemGroup>
+ <Reference Include="System" />
+ <Reference Include="System.ComponentModel.Composition" />
+ <Reference Include="System.Core" />
+ <Reference Include="System.Drawing" />
+ <Reference Include="System.Reactive.Core, Version=2.0.20823.0, Culture=neutral, PublicKeyToken=f300afd708cefcd3, processorArchitecture=MSIL">
+ <SpecificVersion>False</SpecificVersion>
+ <HintPath>..\packages\Rx-Core.2.0.20823\lib\Net45\System.Reactive.Core.dll</HintPath>
+ </Reference>
+ <Reference Include="System.Reactive.Interfaces, Version=2.0.20823.0, Culture=neutral, PublicKeyToken=f300afd708cefcd3, processorArchitecture=MSIL">
+ <SpecificVersion>False</SpecificVersion>
+ <HintPath>..\packages\Rx-Interfaces.2.0.20823\lib\Net45\System.Reactive.Interfaces.dll</HintPath>
+ </Reference>
+ <Reference Include="System.Reactive.Linq, Version=2.0.20823.0, Culture=neutral, PublicKeyToken=f300afd708cefcd3, processorArchitecture=MSIL">
+ <SpecificVersion>False</SpecificVersion>
+ <HintPath>..\packages\Rx-Linq.2.0.20823\lib\Net45\System.Reactive.Linq.dll</HintPath>
+ </Reference>
+ <Reference Include="System.Runtime.Serialization" />
+ <Reference Include="System.Xml.Linq" />
+ <Reference Include="System.Data.DataSetExtensions" />
+ <Reference Include="Microsoft.CSharp" />
+ <Reference Include="System.Data" />
+ <Reference Include="System.Xml" />
+ </ItemGroup>
+ <ItemGroup>
+ <Compile Include="ApiService.cs" />
+ <Compile Include="HttpHandlers\AudioHandler.cs" />
+ <Compile Include="HttpHandlers\BaseMediaHandler.cs" />
+ <Compile Include="HttpHandlers\FavoriteStatusHandler.cs" />
+ <Compile Include="HttpHandlers\MovieSpecialFeaturesHandler.cs" />
+ <Compile Include="HttpHandlers\PlayedStatusHandler.cs" />
+ <Compile Include="HttpHandlers\UserHandler.cs" />
+ <Compile Include="HttpHandlers\GenreHandler.cs" />
+ <Compile Include="HttpHandlers\GenresHandler.cs" />
+ <Compile Include="HttpHandlers\ImageHandler.cs" />
+ <Compile Include="HttpHandlers\ItemHandler.cs" />
+ <Compile Include="HttpHandlers\ItemListHandler.cs" />
+ <Compile Include="HttpHandlers\PersonHandler.cs" />
+ <Compile Include="HttpHandlers\PluginAssemblyHandler.cs" />
+ <Compile Include="HttpHandlers\PluginConfigurationHandler.cs" />
+ <Compile Include="HttpHandlers\PluginsHandler.cs" />
+ <Compile Include="HttpHandlers\ServerConfigurationHandler.cs" />
+ <Compile Include="HttpHandlers\StudioHandler.cs" />
+ <Compile Include="HttpHandlers\StudiosHandler.cs" />
+ <Compile Include="HttpHandlers\UserAuthenticationHandler.cs" />
+ <Compile Include="HttpHandlers\UserItemRatingHandler.cs" />
+ <Compile Include="HttpHandlers\UsersHandler.cs" />
+ <Compile Include="HttpHandlers\VideoHandler.cs" />
+ <Compile Include="HttpHandlers\WeatherHandler.cs" />
+ <Compile Include="HttpHandlers\YearHandler.cs" />
+ <Compile Include="HttpHandlers\YearsHandler.cs" />
+ <Compile Include="Plugin.cs" />
+ <Compile Include="Properties\AssemblyInfo.cs" />
+ </ItemGroup>
+ <ItemGroup>
+ <ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj">
+ <Project>{9142eefa-7570-41e1-bfcc-468bb571af2f}</Project>
+ <Name>MediaBrowser.Common</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj">
+ <Project>{17e1f4e6-8abd-4fe5-9ecf-43d4b6087ba2}</Project>
+ <Name>MediaBrowser.Controller</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj">
+ <Project>{7eeeb4bb-f3e8-48fc-b4c5-70f0fff8329b}</Project>
+ <Name>MediaBrowser.Model</Name>
+ </ProjectReference>
+ </ItemGroup>
+ <ItemGroup>
+ <None Include="packages.config" />
+ </ItemGroup>
+ <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
+ <PropertyGroup>
+ <PostBuildEvent>xcopy "$(TargetPath)" "$(SolutionDir)\ProgramData-Server\Plugins\" /y</PostBuildEvent>
+ </PropertyGroup>
+ <!-- To modify your build process, add your task inside one of the targets below and uncomment it.
+ Other similar extension points exist, see Microsoft.Common.targets.
+ <Target Name="BeforeBuild">
+ </Target>
+ <Target Name="AfterBuild">
+ </Target>
+ -->
+</Project> \ No newline at end of file
diff --git a/MediaBrowser.Api/Plugin.cs b/MediaBrowser.Api/Plugin.cs
new file mode 100644
index 000000000..8def96da8
--- /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 000000000..c92346bac
--- /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 000000000..42f16a267
--- /dev/null
+++ b/MediaBrowser.Api/packages.config
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<packages>
+ <package id="Rx-Core" version="2.0.20823" targetFramework="net45" />
+ <package id="Rx-Interfaces" version="2.0.20823" targetFramework="net45" />
+ <package id="Rx-Linq" version="2.0.20823" targetFramework="net45" />
+</packages> \ 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 000000000..bf49a896e
--- /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 000000000..92e3f7c2b
--- /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
+ {
+ /// <summary>
+ /// This is an auto-generated Protobuf Serialization assembly for best performance.
+ /// It is created during the Model project's post-build event.
+ /// This means that this class can currently only handle types within the Model project.
+ /// If we need to, we can always add a param indicating whether or not the model serializer should be used.
+ /// </summary>
+ private static readonly ProtobufModelSerializer ProtobufModelSerializer = new ProtobufModelSerializer();
+
+ public static T DeserializeFromStream<T>(Stream stream, SerializationFormats format)
+ where T : class
+ {
+ if (format == ApiInteraction.SerializationFormats.Protobuf)
+ {
+ return ProtobufModelSerializer.Deserialize(stream, null, typeof(T)) as T;
+ }
+ else if (format == ApiInteraction.SerializationFormats.Jsv)
+ {
+ throw new NotImplementedException();
+ }
+ else if (format == ApiInteraction.SerializationFormats.Json)
+ {
+ using (StreamReader streamReader = new StreamReader(stream))
+ {
+ using (JsonReader jsonReader = new JsonTextReader(streamReader))
+ {
+ return JsonSerializer.Create(new JsonSerializerSettings()).Deserialize<T>(jsonReader);
+ }
+ }
+ }
+
+ throw new NotImplementedException();
+ }
+
+ public static object DeserializeFromStream(Stream stream, SerializationFormats format, Type type)
+ {
+ if (format == ApiInteraction.SerializationFormats.Protobuf)
+ {
+ return ProtobufModelSerializer.Deserialize(stream, null, type);
+ }
+ else if (format == ApiInteraction.SerializationFormats.Jsv)
+ {
+ throw new NotImplementedException();
+ }
+ else if (format == ApiInteraction.SerializationFormats.Json)
+ {
+ using (StreamReader streamReader = new StreamReader(stream))
+ {
+ using (JsonReader jsonReader = new JsonTextReader(streamReader))
+ {
+ return JsonSerializer.Create(new JsonSerializerSettings()).Deserialize(jsonReader, type);
+ }
+ }
+ }
+
+ throw new NotImplementedException();
+ }
+
+ public static void Configure()
+ {
+ }
+
+ public static bool CanDeSerializeJsv
+ {
+ get
+ {
+ return false;
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.ApiInteraction.Metro/MediaBrowser.ApiInteraction.Metro.csproj b/MediaBrowser.ApiInteraction.Metro/MediaBrowser.ApiInteraction.Metro.csproj
new file mode 100644
index 000000000..63a91ac49
--- /dev/null
+++ b/MediaBrowser.ApiInteraction.Metro/MediaBrowser.ApiInteraction.Metro.csproj
@@ -0,0 +1,74 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
+ <PropertyGroup>
+ <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+ <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+ <ProjectGuid>{94CEA07A-307C-4663-AA43-7BD852808574}</ProjectGuid>
+ <OutputType>Library</OutputType>
+ <AppDesignerFolder>Properties</AppDesignerFolder>
+ <RootNamespace>MediaBrowser.ApiInteraction.Metro</RootNamespace>
+ <AssemblyName>MediaBrowser.ApiInteraction.Metro</AssemblyName>
+ <TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
+ <TargetFrameworkProfile>Profile7</TargetFrameworkProfile>
+ <FileAlignment>512</FileAlignment>
+ <ProjectTypeGuids>{786C830F-07A1-408B-BD7F-6EE04809D6DB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
+ <DebugSymbols>true</DebugSymbols>
+ <DebugType>full</DebugType>
+ <Optimize>false</Optimize>
+ <OutputPath>bin\Debug\</OutputPath>
+ <DefineConstants>DEBUG;TRACE</DefineConstants>
+ <ErrorReport>prompt</ErrorReport>
+ <WarningLevel>4</WarningLevel>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
+ <DebugType>pdbonly</DebugType>
+ <Optimize>true</Optimize>
+ <OutputPath>bin\Release\</OutputPath>
+ <DefineConstants>TRACE</DefineConstants>
+ <ErrorReport>prompt</ErrorReport>
+ <WarningLevel>4</WarningLevel>
+ </PropertyGroup>
+ <ItemGroup>
+ <!-- A reference to the entire .NET Framework is automatically included -->
+ <ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj">
+ <Project>{7eeeb4bb-f3e8-48fc-b4c5-70f0fff8329b}</Project>
+ <Name>MediaBrowser.Model</Name>
+ </ProjectReference>
+ </ItemGroup>
+ <ItemGroup>
+ <Compile Include="..\MediaBrowser.ApiInteraction\BaseApiClient.cs">
+ <Link>BaseApiClient.cs</Link>
+ </Compile>
+ <Compile Include="..\MediaBrowser.ApiInteraction\BaseHttpApiClient.cs">
+ <Link>BaseHttpApiClient.cs</Link>
+ </Compile>
+ <Compile Include="..\MediaBrowser.ApiInteraction\SerializationFormats.cs">
+ <Link>SerializationFormats.cs</Link>
+ </Compile>
+ <Compile Include="ApiClient.cs" />
+ <Compile Include="DataSerializer.cs" />
+ <Compile Include="Properties\AssemblyInfo.cs" />
+ </ItemGroup>
+ <ItemGroup>
+ <Reference Include="Newtonsoft.Json">
+ <HintPath>..\Json.Net\Portable\Newtonsoft.Json.dll</HintPath>
+ </Reference>
+ <Reference Include="protobuf-net">
+ <HintPath>..\protobuf-net\Full\portable\protobuf-net.dll</HintPath>
+ </Reference>
+ <Reference Include="ProtobufModelSerializer">
+ <HintPath>..\MediaBrowser.Model\bin\ProtobufModelSerializer.dll</HintPath>
+ </Reference>
+ </ItemGroup>
+ <Import Project="$(MSBuildExtensionsPath32)\Microsoft\Portable\$(TargetFrameworkVersion)\Microsoft.Portable.CSharp.targets" />
+ <!-- To modify your build process, add your task inside one of the targets below and uncomment it.
+ Other similar extension points exist, see Microsoft.Common.targets.
+ <Target Name="BeforeBuild">
+ </Target>
+ <Target Name="AfterBuild">
+ </Target>
+ -->
+</Project> \ 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 000000000..c9c876bc8
--- /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 000000000..4484801a2
--- /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 000000000..3d5ecde22
--- /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 000000000..466869c76
--- /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
+{
+ /// <summary>
+ /// Provides api methods that are usable on all platforms
+ /// </summary>
+ public abstract class BaseApiClient : IDisposable
+ {
+ protected BaseApiClient()
+ {
+ DataSerializer.Configure();
+ }
+
+ /// <summary>
+ /// Gets or sets the server host name (myserver or 192.168.x.x)
+ /// </summary>
+ public string ServerHostName { get; set; }
+
+ /// <summary>
+ /// Gets or sets the port number used by the API
+ /// </summary>
+ public int ServerApiPort { get; set; }
+
+ /// <summary>
+ /// Gets the current api url based on hostname and port.
+ /// </summary>
+ protected string ApiUrl
+ {
+ get
+ {
+ return string.Format("http://{0}:{1}/mediabrowser/api", ServerHostName, ServerApiPort);
+ }
+ }
+
+ /// <summary>
+ /// Gets the default data format to request from the server
+ /// </summary>
+ protected SerializationFormats SerializationFormat
+ {
+ get
+ {
+ return SerializationFormats.Protobuf;
+ }
+ }
+
+ /// <summary>
+ /// Gets an image url that can be used to download an image from the api
+ /// </summary>
+ /// <param name="itemId">The Id of the item</param>
+ /// <param name="imageType">The type of image requested</param>
+ /// <param name="imageIndex">The image index, if there are multiple. Currently only applies to backdrops. Supply null or 0 for first backdrop.</param>
+ /// <param name="width">Use if a fixed width is required. Aspect ratio will be preserved.</param>
+ /// <param name="height">Use if a fixed height is required. Aspect ratio will be preserved.</param>
+ /// <param name="maxWidth">Use if a max width is required. Aspect ratio will be preserved.</param>
+ /// <param name="maxHeight">Use if a max height is required. Aspect ratio will be preserved.</param>
+ /// <param name="quality">Quality level, from 0-100. Currently only applies to JPG. The default value should suffice.</param>
+ public string GetImageUrl(Guid itemId, ImageType imageType, int? imageIndex = null, int? width = null, int? height = null, int? maxWidth = null, int? maxHeight = null, int? quality = null)
+ {
+ string url = ApiUrl + "/image";
+
+ url += "?id=" + itemId.ToString();
+ url += "&type=" + imageType.ToString();
+
+ if (imageIndex.HasValue)
+ {
+ url += "&index=" + imageIndex;
+ }
+ if (width.HasValue)
+ {
+ url += "&width=" + width;
+ }
+ if (height.HasValue)
+ {
+ url += "&height=" + height;
+ }
+ if (maxWidth.HasValue)
+ {
+ url += "&maxWidth=" + maxWidth;
+ }
+ if (maxHeight.HasValue)
+ {
+ url += "&maxHeight=" + maxHeight;
+ }
+ if (quality.HasValue)
+ {
+ url += "&quality=" + quality;
+ }
+
+ return url;
+ }
+
+ /// <summary>
+ /// Gets an image url that can be used to download an image from the api
+ /// </summary>
+ /// <param name="userId">The Id of the user</param>
+ /// <param name="width">Use if a fixed width is required. Aspect ratio will be preserved.</param>
+ /// <param name="height">Use if a fixed height is required. Aspect ratio will be preserved.</param>
+ /// <param name="maxWidth">Use if a max width is required. Aspect ratio will be preserved.</param>
+ /// <param name="maxHeight">Use if a max height is required. Aspect ratio will be preserved.</param>
+ /// <param name="quality">Quality level, from 0-100. Currently only applies to JPG. The default value should suffice.</param>
+ public string GetUserImageUrl(Guid userId, int? width = null, int? height = null, int? maxWidth = null, int? maxHeight = null, int? quality = null)
+ {
+ string url = ApiUrl + "/image";
+
+ url += "?userId=" + userId.ToString();
+
+ if (width.HasValue)
+ {
+ url += "&width=" + width;
+ }
+ if (height.HasValue)
+ {
+ url += "&height=" + height;
+ }
+ if (maxWidth.HasValue)
+ {
+ url += "&maxWidth=" + maxWidth;
+ }
+ if (maxHeight.HasValue)
+ {
+ url += "&maxHeight=" + maxHeight;
+ }
+ if (quality.HasValue)
+ {
+ url += "&quality=" + quality;
+ }
+
+ return url;
+ }
+
+ /// <summary>
+ /// Gets an image url that can be used to download an image from the api
+ /// </summary>
+ /// <param name="name">The name of the person</param>
+ /// <param name="width">Use if a fixed width is required. Aspect ratio will be preserved.</param>
+ /// <param name="height">Use if a fixed height is required. Aspect ratio will be preserved.</param>
+ /// <param name="maxWidth">Use if a max width is required. Aspect ratio will be preserved.</param>
+ /// <param name="maxHeight">Use if a max height is required. Aspect ratio will be preserved.</param>
+ /// <param name="quality">Quality level, from 0-100. Currently only applies to JPG. The default value should suffice.</param>
+ public string GetPersonImageUrl(string name, int? width = null, int? height = null, int? maxWidth = null, int? maxHeight = null, int? quality = null)
+ {
+ string url = ApiUrl + "/image";
+
+ url += "?personname=" + name;
+
+ if (width.HasValue)
+ {
+ url += "&width=" + width;
+ }
+ if (height.HasValue)
+ {
+ url += "&height=" + height;
+ }
+ if (maxWidth.HasValue)
+ {
+ url += "&maxWidth=" + maxWidth;
+ }
+ if (maxHeight.HasValue)
+ {
+ url += "&maxHeight=" + maxHeight;
+ }
+ if (quality.HasValue)
+ {
+ url += "&quality=" + quality;
+ }
+
+ return url;
+ }
+
+ /// <summary>
+ /// Gets an image url that can be used to download an image from the api
+ /// </summary>
+ /// <param name="year">The year</param>
+ /// <param name="width">Use if a fixed width is required. Aspect ratio will be preserved.</param>
+ /// <param name="height">Use if a fixed height is required. Aspect ratio will be preserved.</param>
+ /// <param name="maxWidth">Use if a max width is required. Aspect ratio will be preserved.</param>
+ /// <param name="maxHeight">Use if a max height is required. Aspect ratio will be preserved.</param>
+ /// <param name="quality">Quality level, from 0-100. Currently only applies to JPG. The default value should suffice.</param>
+ public string GetYearImageUrl(int year, int? width = null, int? height = null, int? maxWidth = null, int? maxHeight = null, int? quality = null)
+ {
+ string url = ApiUrl + "/image";
+
+ url += "?year=" + year;
+
+ if (width.HasValue)
+ {
+ url += "&width=" + width;
+ }
+ if (height.HasValue)
+ {
+ url += "&height=" + height;
+ }
+ if (maxWidth.HasValue)
+ {
+ url += "&maxWidth=" + maxWidth;
+ }
+ if (maxHeight.HasValue)
+ {
+ url += "&maxHeight=" + maxHeight;
+ }
+ if (quality.HasValue)
+ {
+ url += "&quality=" + quality;
+ }
+
+ return url;
+ }
+
+ /// <summary>
+ /// Gets an image url that can be used to download an image from the api
+ /// </summary>
+ /// <param name="name">The name of the genre</param>
+ /// <param name="width">Use if a fixed width is required. Aspect ratio will be preserved.</param>
+ /// <param name="height">Use if a fixed height is required. Aspect ratio will be preserved.</param>
+ /// <param name="maxWidth">Use if a max width is required. Aspect ratio will be preserved.</param>
+ /// <param name="maxHeight">Use if a max height is required. Aspect ratio will be preserved.</param>
+ /// <param name="quality">Quality level, from 0-100. Currently only applies to JPG. The default value should suffice.</param>
+ public string GetGenreImageUrl(string name, int? width = null, int? height = null, int? maxWidth = null, int? maxHeight = null, int? quality = null)
+ {
+ string url = ApiUrl + "/image";
+
+ url += "?genre=" + name;
+
+ if (width.HasValue)
+ {
+ url += "&width=" + width;
+ }
+ if (height.HasValue)
+ {
+ url += "&height=" + height;
+ }
+ if (maxWidth.HasValue)
+ {
+ url += "&maxWidth=" + maxWidth;
+ }
+ if (maxHeight.HasValue)
+ {
+ url += "&maxHeight=" + maxHeight;
+ }
+ if (quality.HasValue)
+ {
+ url += "&quality=" + quality;
+ }
+
+ return url;
+ }
+
+ /// <summary>
+ /// Gets an image url that can be used to download an image from the api
+ /// </summary>
+ /// <param name="name">The name of the studio</param>
+ /// <param name="width">Use if a fixed width is required. Aspect ratio will be preserved.</param>
+ /// <param name="height">Use if a fixed height is required. Aspect ratio will be preserved.</param>
+ /// <param name="maxWidth">Use if a max width is required. Aspect ratio will be preserved.</param>
+ /// <param name="maxHeight">Use if a max height is required. Aspect ratio will be preserved.</param>
+ /// <param name="quality">Quality level, from 0-100. Currently only applies to JPG. The default value should suffice.</param>
+ public string GetStudioImageUrl(string name, int? width = null, int? height = null, int? maxWidth = null, int? maxHeight = null, int? quality = null)
+ {
+ string url = ApiUrl + "/image";
+
+ url += "?studio=" + name;
+
+ if (width.HasValue)
+ {
+ url += "&width=" + width;
+ }
+ if (height.HasValue)
+ {
+ url += "&height=" + height;
+ }
+ if (maxWidth.HasValue)
+ {
+ url += "&maxWidth=" + maxWidth;
+ }
+ if (maxHeight.HasValue)
+ {
+ url += "&maxHeight=" + maxHeight;
+ }
+ if (quality.HasValue)
+ {
+ url += "&quality=" + quality;
+ }
+
+ return url;
+ }
+
+ /// <summary>
+ /// This is a helper to get a list of backdrop url's from a given ApiBaseItemWrapper. If the actual item does not have any backdrops it will return backdrops from the first parent that does.
+ /// </summary>
+ /// <param name="item">A given item.</param>
+ /// <param name="width">Use if a fixed width is required. Aspect ratio will be preserved.</param>
+ /// <param name="height">Use if a fixed height is required. Aspect ratio will be preserved.</param>
+ /// <param name="maxWidth">Use if a max width is required. Aspect ratio will be preserved.</param>
+ /// <param name="maxHeight">Use if a max height is required. Aspect ratio will be preserved.</param>
+ /// <param name="quality">Quality level, from 0-100. Currently only applies to JPG. The default value should suffice.</param>
+ public string[] GetBackdropImageUrls(DtoBaseItem item, int? width = null, int? height = null, int? maxWidth = null, int? maxHeight = null, int? quality = null)
+ {
+ Guid? backdropItemId;
+ int backdropCount;
+
+ if (item.BackdropCount == 0)
+ {
+ backdropItemId = item.ParentBackdropItemId;
+ backdropCount = item.ParentBackdropCount ?? 0;
+ }
+ else
+ {
+ backdropItemId = item.Id;
+ backdropCount = item.BackdropCount;
+ }
+
+ if (backdropItemId == null)
+ {
+ return new string[] { };
+ }
+
+ var files = new string[backdropCount];
+
+ for (int i = 0; i < backdropCount; i++)
+ {
+ files[i] = GetImageUrl(backdropItemId.Value, ImageType.Backdrop, i, width, height, maxWidth, maxHeight, quality);
+ }
+
+ return files;
+ }
+
+ /// <summary>
+ /// This is a helper to get the logo image url from a given ApiBaseItemWrapper. If the actual item does not have a logo, it will return the logo from the first parent that does, or null.
+ /// </summary>
+ /// <param name="item">A given item.</param>
+ /// <param name="width">Use if a fixed width is required. Aspect ratio will be preserved.</param>
+ /// <param name="height">Use if a fixed height is required. Aspect ratio will be preserved.</param>
+ /// <param name="maxWidth">Use if a max width is required. Aspect ratio will be preserved.</param>
+ /// <param name="maxHeight">Use if a max height is required. Aspect ratio will be preserved.</param>
+ /// <param name="quality">Quality level, from 0-100. Currently only applies to JPG. The default value should suffice.</param>
+ public string GetLogoImageUrl(DtoBaseItem item, int? width = null, int? height = null, int? maxWidth = null, int? maxHeight = null, int? quality = null)
+ {
+ Guid? logoItemId = item.HasLogo ? item.Id : item.ParentLogoItemId;
+
+ if (logoItemId.HasValue)
+ {
+ return GetImageUrl(logoItemId.Value, ImageType.Logo, null, width, height, maxWidth, maxHeight, quality);
+ }
+
+ return null;
+ }
+
+ /// <summary>
+ /// Gets the url needed to stream an audio file
+ /// </summary>
+ /// <param name="itemId">The id of the item</param>
+ /// <param name="supportedOutputFormats">List all the output formats the decice is capable of playing. The more, the better, as it will decrease the likelyhood of having to encode, which will put a load on the server.</param>
+ /// <param name="maxAudioChannels">The maximum number of channels that the device can play. Omit this if it doesn't matter. Phones and tablets should generally specify 2.</param>
+ /// <param name="maxAudioSampleRate">The maximum sample rate that the device can play. This should generally be omitted. The server will default this to 44100, so only override if a different max is needed.</param>
+ public string GetAudioStreamUrl(Guid itemId, IEnumerable<AudioOutputFormats> supportedOutputFormats, int? maxAudioChannels = null, int? maxAudioSampleRate = null)
+ {
+ string url = ApiUrl + "/audio?id=" + itemId;
+
+ url += "&outputformats=" + string.Join(",", supportedOutputFormats.Select(s => s.ToString()).ToArray());
+
+ if (maxAudioChannels.HasValue)
+ {
+ url += "&audiochannels=" + maxAudioChannels.Value;
+ }
+
+ if (maxAudioSampleRate.HasValue)
+ {
+ url += "&audiosamplerate=" + maxAudioSampleRate.Value;
+ }
+
+ return url;
+ }
+
+ /// <summary>
+ /// Gets the url needed to stream a video file
+ /// </summary>
+ /// <param name="itemId">The id of the item</param>
+ /// <param name="supportedOutputFormats">List all the output formats the decice is capable of playing. The more, the better, as it will decrease the likelyhood of having to encode, which will put a load on the server.</param>
+ /// <param name="maxAudioChannels">The maximum number of channels that the device can play. Omit this if it doesn't matter. Phones and tablets should generally specify 2.</param>
+ /// <param name="maxAudioSampleRate">The maximum sample rate that the device can play. This should generally be omitted. The server will default this to 44100, so only override if a different max is needed.</param>
+ /// <param name="width">Specify this is a fixed video width is required</param>
+ /// <param name="height">Specify this is a fixed video height is required</param>
+ /// <param name="maxWidth">Specify this is a max video width is required</param>
+ /// <param name="maxHeight">Specify this is a max video height is required</param>
+ public string GetVideoStreamUrl(Guid itemId,
+ IEnumerable<VideoOutputFormats> supportedOutputFormats,
+ int? maxAudioChannels = null,
+ int? maxAudioSampleRate = null,
+ int? width = null,
+ int? height = null,
+ int? maxWidth = null,
+ int? maxHeight = null)
+ {
+ string url = ApiUrl + "/video?id=" + itemId;
+
+ url += "&outputformats=" + string.Join(",", supportedOutputFormats.Select(s => s.ToString()).ToArray());
+
+ if (maxAudioChannels.HasValue)
+ {
+ url += "&audiochannels=" + maxAudioChannels.Value;
+ }
+
+ if (maxAudioSampleRate.HasValue)
+ {
+ url += "&audiosamplerate=" + maxAudioSampleRate.Value;
+ }
+
+ if (width.HasValue)
+ {
+ url += "&width=" + width.Value;
+ }
+
+ if (height.HasValue)
+ {
+ url += "&height=" + height.Value;
+ }
+
+ if (maxWidth.HasValue)
+ {
+ url += "&maxWidth=" + maxWidth.Value;
+ }
+
+ if (maxHeight.HasValue)
+ {
+ url += "&maxHeight=" + maxHeight.Value;
+ }
+ return url;
+ }
+
+ protected T DeserializeFromStream<T>(Stream stream)
+ where T : class
+ {
+ return DataSerializer.DeserializeFromStream<T>(stream, SerializationFormat);
+ }
+
+ public virtual void Dispose()
+ {
+ }
+ }
+}
diff --git a/MediaBrowser.ApiInteraction/BaseHttpApiClient.cs b/MediaBrowser.ApiInteraction/BaseHttpApiClient.cs
new file mode 100644
index 000000000..8c6c1c297
--- /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
+{
+ /// <summary>
+ /// Provides api methods centered around an HttpClient
+ /// </summary>
+ public abstract class BaseHttpApiClient : BaseApiClient
+ {
+#if WINDOWS_PHONE
+ public BaseHttpApiClient()
+ {
+ HttpClient = new GZipWebClient();
+ }
+
+ private WebClient HttpClient { get; set; }
+#else
+ protected BaseHttpApiClient(HttpClientHandler handler)
+ : base()
+ {
+ handler.AutomaticDecompression = DecompressionMethods.Deflate;
+
+ HttpClient = new HttpClient(handler);
+ }
+
+ private HttpClient HttpClient { get; set; }
+#endif
+
+ /// <summary>
+ /// Gets an image stream based on a url
+ /// </summary>
+ public Task<Stream> GetImageStreamAsync(string url)
+ {
+ return GetStreamAsync(url);
+ }
+
+ /// <summary>
+ /// Gets a BaseItem
+ /// </summary>
+ public async Task<DtoBaseItem> GetItemAsync(Guid id, Guid userId)
+ {
+ string url = ApiUrl + "/item?userId=" + userId.ToString();
+
+ if (id != Guid.Empty)
+ {
+ url += "&id=" + id.ToString();
+ }
+
+ using (Stream stream = await GetSerializedStreamAsync(url).ConfigureAwait(false))
+ {
+ return DeserializeFromStream<DtoBaseItem>(stream);
+ }
+ }
+
+ /// <summary>
+ /// Gets all Users
+ /// </summary>
+ public async Task<DtoUser[]> GetAllUsersAsync()
+ {
+ string url = ApiUrl + "/users";
+
+ using (Stream stream = await GetSerializedStreamAsync(url).ConfigureAwait(false))
+ {
+ return DeserializeFromStream<DtoUser[]>(stream);
+ }
+ }
+
+ /// <summary>
+ /// Gets all Genres
+ /// </summary>
+ public async Task<IbnItem[]> GetAllGenresAsync(Guid userId)
+ {
+ string url = ApiUrl + "/genres?userId=" + userId.ToString();
+
+ using (Stream stream = await GetSerializedStreamAsync(url).ConfigureAwait(false))
+ {
+ return DeserializeFromStream<IbnItem[]>(stream);
+ }
+ }
+
+ /// <summary>
+ /// Gets in-progress items
+ /// </summary>
+ /// <param name="userId">The user id.</param>
+ /// <param name="folderId">(Optional) Specify a folder Id to localize the search to a specific folder.</param>
+ public async Task<DtoBaseItem[]> GetInProgressItemsItemsAsync(Guid userId, Guid? folderId = null)
+ {
+ string url = ApiUrl + "/itemlist?listtype=inprogressitems&userId=" + userId.ToString();
+
+ if (folderId.HasValue)
+ {
+ url += "&id=" + folderId.ToString();
+ }
+
+ using (Stream stream = await GetSerializedStreamAsync(url).ConfigureAwait(false))
+ {
+ return DeserializeFromStream<DtoBaseItem[]>(stream);
+ }
+ }
+
+ /// <summary>
+ /// Gets recently added items
+ /// </summary>
+ /// <param name="userId">The user id.</param>
+ /// <param name="folderId">(Optional) Specify a folder Id to localize the search to a specific folder.</param>
+ public async Task<DtoBaseItem[]> GetRecentlyAddedItemsAsync(Guid userId, Guid? folderId = null)
+ {
+ string url = ApiUrl + "/itemlist?listtype=recentlyaddeditems&userId=" + userId.ToString();
+
+ if (folderId.HasValue)
+ {
+ url += "&id=" + folderId.ToString();
+ }
+
+ using (Stream stream = await GetSerializedStreamAsync(url).ConfigureAwait(false))
+ {
+ return DeserializeFromStream<DtoBaseItem[]>(stream);
+ }
+ }
+
+ /// <summary>
+ /// Gets favorite items
+ /// </summary>
+ /// <param name="userId">The user id.</param>
+ /// <param name="folderId">(Optional) Specify a folder Id to localize the search to a specific folder.</param>
+ public async Task<DtoBaseItem[]> GetFavoriteItemsAsync(Guid userId, Guid? folderId = null)
+ {
+ string url = ApiUrl + "/itemlist?listtype=favorites&userId=" + userId.ToString();
+
+ if (folderId.HasValue)
+ {
+ url += "&id=" + folderId.ToString();
+ }
+
+ using (Stream stream = await GetSerializedStreamAsync(url).ConfigureAwait(false))
+ {
+ return DeserializeFromStream<DtoBaseItem[]>(stream);
+ }
+ }
+
+ /// <summary>
+ /// Gets recently added items that are unplayed.
+ /// </summary>
+ /// <param name="userId">The user id.</param>
+ /// <param name="folderId">(Optional) Specify a folder Id to localize the search to a specific folder.</param>
+ public async Task<DtoBaseItem[]> GetRecentlyAddedUnplayedItemsAsync(Guid userId, Guid? folderId = null)
+ {
+ string url = ApiUrl + "/itemlist?listtype=recentlyaddedunplayeditems&userId=" + userId.ToString();
+
+ if (folderId.HasValue)
+ {
+ url += "&id=" + folderId.ToString();
+ }
+
+ using (Stream stream = await GetSerializedStreamAsync(url).ConfigureAwait(false))
+ {
+ return DeserializeFromStream<DtoBaseItem[]>(stream);
+ }
+ }
+
+ /// <summary>
+ /// Gets all Years
+ /// </summary>
+ public async Task<IbnItem[]> GetAllYearsAsync(Guid userId)
+ {
+ string url = ApiUrl + "/years?userId=" + userId.ToString();
+
+ using (Stream stream = await GetSerializedStreamAsync(url).ConfigureAwait(false))
+ {
+ return DeserializeFromStream<IbnItem[]>(stream);
+ }
+ }
+
+ /// <summary>
+ /// Gets all items that contain a given Year
+ /// </summary>
+ /// <param name="folderId">(Optional) Specify a folder Id to localize the search to a specific folder.</param>
+ public async Task<DtoBaseItem[]> GetItemsWithYearAsync(string name, Guid userId, Guid? folderId = null)
+ {
+ string url = ApiUrl + "/itemlist?listtype=itemswithyear&userId=" + userId.ToString() + "&name=" + name;
+
+ if (folderId.HasValue)
+ {
+ url += "&id=" + folderId.ToString();
+ }
+
+ using (Stream stream = await GetSerializedStreamAsync(url).ConfigureAwait(false))
+ {
+ return DeserializeFromStream<DtoBaseItem[]>(stream);
+ }
+ }
+
+ /// <summary>
+ /// Gets all items that contain a given Genre
+ /// </summary>
+ /// <param name="folderId">(Optional) Specify a folder Id to localize the search to a specific folder.</param>
+ public async Task<DtoBaseItem[]> GetItemsWithGenreAsync(string name, Guid userId, Guid? folderId = null)
+ {
+ string url = ApiUrl + "/itemlist?listtype=itemswithgenre&userId=" + userId.ToString() + "&name=" + name;
+
+ if (folderId.HasValue)
+ {
+ url += "&id=" + folderId.ToString();
+ }
+
+ using (Stream stream = await GetSerializedStreamAsync(url).ConfigureAwait(false))
+ {
+ return DeserializeFromStream<DtoBaseItem[]>(stream);
+ }
+ }
+
+ /// <summary>
+ /// Gets all items that contain a given Person
+ /// </summary>
+ /// <param name="folderId">(Optional) Specify a folder Id to localize the search to a specific folder.</param>
+ public async Task<DtoBaseItem[]> GetItemsWithPersonAsync(string name, Guid userId, Guid? folderId = null)
+ {
+ string url = ApiUrl + "/itemlist?listtype=itemswithperson&userId=" + userId.ToString() + "&name=" + name;
+
+ if (folderId.HasValue)
+ {
+ url += "&id=" + folderId.ToString();
+ }
+
+ using (Stream stream = await GetSerializedStreamAsync(url).ConfigureAwait(false))
+ {
+ return DeserializeFromStream<DtoBaseItem[]>(stream);
+ }
+ }
+
+ /// <summary>
+ /// Gets all items that contain a given Person
+ /// </summary>
+ /// <param name="folderId">(Optional) Specify a folder Id to localize the search to a specific folder.</param>
+ public async Task<DtoBaseItem[]> GetItemsWithPersonAsync(string name, string personType, Guid userId, Guid? folderId = null)
+ {
+ string url = ApiUrl + "/itemlist?listtype=itemswithperson&userId=" + userId.ToString() + "&name=" + name;
+
+ url += "&persontype=" + personType;
+
+ if (folderId.HasValue)
+ {
+ url += "&id=" + folderId.ToString();
+ }
+
+ using (Stream stream = await GetSerializedStreamAsync(url).ConfigureAwait(false))
+ {
+ return DeserializeFromStream<DtoBaseItem[]>(stream);
+ }
+ }
+
+ /// <summary>
+ /// Gets all studious
+ /// </summary>
+ public async Task<IbnItem[]> GetAllStudiosAsync(Guid userId)
+ {
+ string url = ApiUrl + "/studios?userId=" + userId.ToString();
+
+ using (Stream stream = await GetSerializedStreamAsync(url).ConfigureAwait(false))
+ {
+ return DeserializeFromStream<IbnItem[]>(stream);
+ }
+ }
+
+ /// <summary>
+ /// Gets all items that contain a given Studio
+ /// </summary>
+ /// <param name="folderId">(Optional) Specify a folder Id to localize the search to a specific folder.</param>
+ public async Task<DtoBaseItem[]> GetItemsWithStudioAsync(string name, Guid userId, Guid? folderId = null)
+ {
+ string url = ApiUrl + "/itemlist?listtype=itemswithstudio&userId=" + userId.ToString() + "&name=" + name;
+
+ if (folderId.HasValue)
+ {
+ url += "&id=" + folderId.ToString();
+ }
+
+ using (Stream stream = await GetSerializedStreamAsync(url).ConfigureAwait(false))
+ {
+ return DeserializeFromStream<DtoBaseItem[]>(stream);
+ }
+ }
+
+ /// <summary>
+ /// Gets a studio
+ /// </summary>
+ public async Task<IbnItem> GetStudioAsync(Guid userId, string name)
+ {
+ string url = ApiUrl + "/studio?userId=" + userId.ToString() + "&name=" + name;
+
+ using (Stream stream = await GetSerializedStreamAsync(url).ConfigureAwait(false))
+ {
+ return DeserializeFromStream<IbnItem>(stream);
+ }
+ }
+
+ /// <summary>
+ /// Gets a genre
+ /// </summary>
+ public async Task<IbnItem> GetGenreAsync(Guid userId, string name)
+ {
+ string url = ApiUrl + "/genre?userId=" + userId.ToString() + "&name=" + name;
+
+ using (Stream stream = await GetSerializedStreamAsync(url).ConfigureAwait(false))
+ {
+ return DeserializeFromStream<IbnItem>(stream);
+ }
+ }
+
+ /// <summary>
+ /// Gets a person
+ /// </summary>
+ public async Task<IbnItem> GetPersonAsync(Guid userId, string name)
+ {
+ string url = ApiUrl + "/person?userId=" + userId.ToString() + "&name=" + name;
+
+ using (Stream stream = await GetSerializedStreamAsync(url).ConfigureAwait(false))
+ {
+ return DeserializeFromStream<IbnItem>(stream);
+ }
+ }
+
+ /// <summary>
+ /// Gets a year
+ /// </summary>
+ public async Task<IbnItem> GetYearAsync(Guid userId, int year)
+ {
+ string url = ApiUrl + "/year?userId=" + userId.ToString() + "&year=" + year;
+
+ using (Stream stream = await GetSerializedStreamAsync(url).ConfigureAwait(false))
+ {
+ return DeserializeFromStream<IbnItem>(stream);
+ }
+ }
+
+ /// <summary>
+ /// Gets a list of plugins installed on the server
+ /// </summary>
+ public async Task<PluginInfo[]> GetInstalledPluginsAsync()
+ {
+ string url = ApiUrl + "/plugins";
+
+ using (Stream stream = await GetSerializedStreamAsync(url).ConfigureAwait(false))
+ {
+ return DeserializeFromStream<PluginInfo[]>(stream);
+ }
+ }
+
+ /// <summary>
+ /// Gets a list of plugins installed on the server
+ /// </summary>
+ public Task<Stream> GetPluginAssemblyAsync(PluginInfo plugin)
+ {
+ string url = ApiUrl + "/pluginassembly?assemblyfilename=" + plugin.AssemblyFileName;
+
+ return GetStreamAsync(url);
+ }
+
+ /// <summary>
+ /// Gets the current server configuration
+ /// </summary>
+ public async Task<ServerConfiguration> GetServerConfigurationAsync()
+ {
+ string url = ApiUrl + "/ServerConfiguration";
+
+ using (Stream stream = await GetSerializedStreamAsync(url).ConfigureAwait(false))
+ {
+ return DeserializeFromStream<ServerConfiguration>(stream);
+ }
+ }
+
+ /// <summary>
+ /// Gets weather information for the default location as set in configuration
+ /// </summary>
+ public async Task<object> GetPluginConfigurationAsync(PluginInfo plugin, Type configurationType)
+ {
+ string url = ApiUrl + "/PluginConfiguration?assemblyfilename=" + plugin.AssemblyFileName;
+
+ // At the moment this can't be retrieved in protobuf format
+ SerializationFormats format = DataSerializer.CanDeSerializeJsv ? SerializationFormats.Jsv : SerializationFormats.Json;
+
+ using (Stream stream = await GetSerializedStreamAsync(url, format).ConfigureAwait(false))
+ {
+ return DataSerializer.DeserializeFromStream(stream, format, configurationType);
+ }
+ }
+
+ /// <summary>
+ /// Gets the default user
+ /// </summary>
+ public async Task<DtoUser> GetDefaultUserAsync()
+ {
+ string url = ApiUrl + "/user";
+
+ using (Stream stream = await GetSerializedStreamAsync(url).ConfigureAwait(false))
+ {
+ return DeserializeFromStream<DtoUser>(stream);
+ }
+ }
+
+ /// <summary>
+ /// Gets a user by id
+ /// </summary>
+ public async Task<DtoUser> GetUserAsync(Guid id)
+ {
+ string url = ApiUrl + "/user?id=" + id.ToString();
+
+ using (Stream stream = await GetSerializedStreamAsync(url).ConfigureAwait(false))
+ {
+ return DeserializeFromStream<DtoUser>(stream);
+ }
+ }
+
+ /// <summary>
+ /// Gets weather information for the default location as set in configuration
+ /// </summary>
+ public async Task<WeatherInfo> GetWeatherInfoAsync()
+ {
+ string url = ApiUrl + "/weather";
+
+ using (Stream stream = await GetSerializedStreamAsync(url).ConfigureAwait(false))
+ {
+ return DeserializeFromStream<WeatherInfo>(stream);
+ }
+ }
+
+ /// <summary>
+ /// Gets weather information for a specific zip code
+ /// </summary>
+ public async Task<WeatherInfo> GetWeatherInfoAsync(string zipCode)
+ {
+ string url = ApiUrl + "/weather?zipcode=" + zipCode;
+
+ using (Stream stream = await GetSerializedStreamAsync(url).ConfigureAwait(false))
+ {
+ return DeserializeFromStream<WeatherInfo>(stream);
+ }
+ }
+
+ /// <summary>
+ /// Gets special features for a Movie
+ /// </summary>
+ public async Task<DtoBaseItem[]> GetMovieSpecialFeaturesAsync(Guid itemId, Guid userId)
+ {
+ string url = ApiUrl + "/MovieSpecialFeatures?id=" + itemId;
+ url += "&userid=" + userId;
+
+ using (Stream stream = await GetSerializedStreamAsync(url).ConfigureAwait(false))
+ {
+ return DeserializeFromStream<DtoBaseItem[]>(stream);
+ }
+ }
+
+ /// <summary>
+ /// Updates played status for an item
+ /// </summary>
+ public async Task<DtoUserItemData> UpdatePlayedStatusAsync(Guid itemId, Guid userId, bool wasPlayed)
+ {
+ string url = ApiUrl + "/PlayedStatus?id=" + itemId;
+
+ url += "&userid=" + userId;
+ url += "&played=" + (wasPlayed ? "1" : "0");
+
+ using (Stream stream = await GetSerializedStreamAsync(url).ConfigureAwait(false))
+ {
+ return DeserializeFromStream<DtoUserItemData>(stream);
+ }
+ }
+
+ /// <summary>
+ /// Updates a user's favorite status for an item and returns the updated UserItemData object.
+ /// </summary>
+ public async Task<DtoUserItemData> UpdateFavoriteStatusAsync(Guid itemId, Guid userId, bool isFavorite)
+ {
+ string url = ApiUrl + "/favoritestatus?id=" + itemId;
+
+ url += "&userid=" + userId;
+ url += "&isfavorite=" + (isFavorite ? "1" : "0");
+
+ using (Stream stream = await GetSerializedStreamAsync(url).ConfigureAwait(false))
+ {
+ return DeserializeFromStream<DtoUserItemData>(stream);
+ }
+ }
+
+ /// <summary>
+ /// Clears a user's rating for an item
+ /// </summary>
+ public async Task<DtoUserItemData> ClearUserItemRatingAsync(Guid itemId, Guid userId)
+ {
+ string url = ApiUrl + "/UserItemRating?id=" + itemId;
+
+ url += "&userid=" + userId;
+ url += "&clear=1";
+
+ using (Stream stream = await GetSerializedStreamAsync(url).ConfigureAwait(false))
+ {
+ return DeserializeFromStream<DtoUserItemData>(stream);
+ }
+ }
+
+ /// <summary>
+ /// Updates a user's rating for an item, based on likes or dislikes
+ /// </summary>
+ public async Task<DtoUserItemData> UpdateUserItemRatingAsync(Guid itemId, Guid userId, bool likes)
+ {
+ string url = ApiUrl + "/UserItemRating?id=" + itemId;
+
+ url += "&userid=" + userId;
+ url += "&likes=" + (likes ? "1" : "0");
+
+ using (Stream stream = await GetSerializedStreamAsync(url).ConfigureAwait(false))
+ {
+ return DeserializeFromStream<DtoUserItemData>(stream);
+ }
+ }
+
+ /// <summary>
+ /// Authenticates a user and returns the result
+ /// </summary>
+ public async Task<AuthenticationResult> AuthenticateUserAsync(Guid userId, string password)
+ {
+ string url = ApiUrl + "/UserAuthentication?dataformat=" + SerializationFormat.ToString();
+
+ // Create the post body
+ string postContent = string.Format("userid={0}", userId);
+
+ if (!string.IsNullOrEmpty(password))
+ {
+ postContent += "&password=" + password;
+ }
+
+#if WINDOWS_PHONE
+ HttpClient.Headers["Content-Type"] = "application/x-www-form-urlencoded";
+ var result = await HttpClient.UploadStringTaskAsync(url, "POST", postContent);
+
+ var byteArray = Encoding.UTF8.GetBytes(result);
+ using (MemoryStream stream = new MemoryStream(byteArray))
+ {
+ return DeserializeFromStream<AuthenticationResult>(stream);
+ }
+#else
+ HttpContent content = new StringContent(postContent, Encoding.UTF8, "application/x-www-form-urlencoded");
+
+ HttpResponseMessage msg = await HttpClient.PostAsync(url, content).ConfigureAwait(false);
+
+ using (Stream stream = await msg.Content.ReadAsStreamAsync().ConfigureAwait(false))
+ {
+ return DeserializeFromStream<AuthenticationResult>(stream);
+ }
+#endif
+ }
+
+ /// <summary>
+ /// This is a helper around getting a stream from the server that contains serialized data
+ /// </summary>
+ private Task<Stream> GetSerializedStreamAsync(string url)
+ {
+ return GetSerializedStreamAsync(url, SerializationFormat);
+ }
+
+ /// <summary>
+ /// This is a helper around getting a stream from the server that contains serialized data
+ /// </summary>
+ private Task<Stream> GetSerializedStreamAsync(string url, SerializationFormats serializationFormat)
+ {
+ if (url.IndexOf('?') == -1)
+ {
+ url += "?dataformat=" + serializationFormat.ToString();
+ }
+ else
+ {
+ url += "&dataformat=" + serializationFormat.ToString();
+ }
+
+ return GetStreamAsync(url);
+ }
+
+ /// <summary>
+ /// This is just a helper around HttpClient
+ /// </summary>
+ private Task<Stream> GetStreamAsync(string url)
+ {
+#if WINDOWS_PHONE
+ return HttpClient.OpenReadTaskAsync(url);
+#else
+ return HttpClient.GetStreamAsync(url);
+#endif
+ }
+
+ public override void Dispose()
+ {
+#if !WINDOWS_PHONE
+ HttpClient.Dispose();
+#endif
+ }
+ }
+}
diff --git a/MediaBrowser.ApiInteraction/DataSerializer.cs b/MediaBrowser.ApiInteraction/DataSerializer.cs
new file mode 100644
index 000000000..3c3f8fae2
--- /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
+ {
+ /// <summary>
+ /// This is an auto-generated Protobuf Serialization assembly for best performance.
+ /// It is created during the Model project's post-build event.
+ /// This means that this class can currently only handle types within the Model project.
+ /// If we need to, we can always add a param indicating whether or not the model serializer should be used.
+ /// </summary>
+ private static readonly ProtobufModelSerializer ProtobufModelSerializer = new ProtobufModelSerializer();
+
+ /// <summary>
+ /// Deserializes an object using generics
+ /// </summary>
+ public static T DeserializeFromStream<T>(Stream stream, SerializationFormats format)
+ where T : class
+ {
+ if (format == SerializationFormats.Protobuf)
+ {
+ //return Serializer.Deserialize<T>(stream);
+ return ProtobufModelSerializer.Deserialize(stream, null, typeof(T)) as T;
+ }
+ if (format == SerializationFormats.Jsv)
+ {
+ return TypeSerializer.DeserializeFromStream<T>(stream);
+ }
+ if (format == SerializationFormats.Json)
+ {
+ return JsonSerializer.DeserializeFromStream<T>(stream);
+ }
+
+ throw new NotImplementedException();
+ }
+
+ /// <summary>
+ /// Deserializes an object
+ /// </summary>
+ public static object DeserializeFromStream(Stream stream, SerializationFormats format, Type type)
+ {
+ if (format == SerializationFormats.Protobuf)
+ {
+ //throw new NotImplementedException();
+ return ProtobufModelSerializer.Deserialize(stream, null, type);
+ }
+ if (format == SerializationFormats.Jsv)
+ {
+ return TypeSerializer.DeserializeFromStream(type, stream);
+ }
+ if (format == SerializationFormats.Json)
+ {
+ return JsonSerializer.DeserializeFromStream(type, stream);
+ }
+
+ throw new NotImplementedException();
+ }
+
+ public static void Configure()
+ {
+ JsConfig.DateHandler = JsonDateHandler.ISO8601;
+ JsConfig.ExcludeTypeInfo = true;
+ JsConfig.IncludeNullValues = false;
+ }
+
+ public static bool CanDeSerializeJsv
+ {
+ get
+ {
+ return true;
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.ApiInteraction/MediaBrowser.ApiInteraction.csproj b/MediaBrowser.ApiInteraction/MediaBrowser.ApiInteraction.csproj
new file mode 100644
index 000000000..d38a25a07
--- /dev/null
+++ b/MediaBrowser.ApiInteraction/MediaBrowser.ApiInteraction.csproj
@@ -0,0 +1,78 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
+ <PropertyGroup>
+ <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+ <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+ <ProjectGuid>{921C0F64-FDA7-4E9F-9E73-0CB0EEDB2422}</ProjectGuid>
+ <OutputType>Library</OutputType>
+ <AppDesignerFolder>Properties</AppDesignerFolder>
+ <RootNamespace>MediaBrowser.ApiInteraction</RootNamespace>
+ <AssemblyName>MediaBrowser.ApiInteraction</AssemblyName>
+ <TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
+ <FileAlignment>512</FileAlignment>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
+ <DebugSymbols>true</DebugSymbols>
+ <DebugType>full</DebugType>
+ <Optimize>false</Optimize>
+ <OutputPath>bin\Debug\</OutputPath>
+ <DefineConstants>DEBUG;TRACE</DefineConstants>
+ <ErrorReport>prompt</ErrorReport>
+ <WarningLevel>4</WarningLevel>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
+ <DebugType>pdbonly</DebugType>
+ <Optimize>true</Optimize>
+ <OutputPath>bin\Release\</OutputPath>
+ <DefineConstants>TRACE</DefineConstants>
+ <ErrorReport>prompt</ErrorReport>
+ <WarningLevel>4</WarningLevel>
+ </PropertyGroup>
+ <ItemGroup>
+ <Reference Include="protobuf-net">
+ <HintPath>..\protobuf-net\Full\net30\protobuf-net.dll</HintPath>
+ </Reference>
+ <Reference Include="ProtobufModelSerializer">
+ <HintPath>..\MediaBrowser.Model\bin\ProtobufModelSerializer.dll</HintPath>
+ </Reference>
+ <Reference Include="ServiceStack.Text, Version=3.9.9.0, Culture=neutral, processorArchitecture=MSIL">
+ <SpecificVersion>False</SpecificVersion>
+ <HintPath>..\packages\ServiceStack.Text.3.9.9\lib\net35\ServiceStack.Text.dll</HintPath>
+ </Reference>
+ <Reference Include="System" />
+ <Reference Include="System.Core" />
+ <Reference Include="System.Net.Http" />
+ <Reference Include="System.Net.Http.WebRequest" />
+ <Reference Include="System.Xml.Linq" />
+ <Reference Include="System.Data.DataSetExtensions" />
+ <Reference Include="Microsoft.CSharp" />
+ <Reference Include="System.Data" />
+ <Reference Include="System.Xml" />
+ </ItemGroup>
+ <ItemGroup>
+ <Compile Include="ApiClient.cs" />
+ <Compile Include="BaseApiClient.cs" />
+ <Compile Include="BaseHttpApiClient.cs" />
+ <Compile Include="DataSerializer.cs" />
+ <Compile Include="Properties\AssemblyInfo.cs" />
+ <Compile Include="SerializationFormats.cs" />
+ </ItemGroup>
+ <ItemGroup>
+ <ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj">
+ <Project>{7eeeb4bb-f3e8-48fc-b4c5-70f0fff8329b}</Project>
+ <Name>MediaBrowser.Model</Name>
+ </ProjectReference>
+ </ItemGroup>
+ <ItemGroup>
+ <None Include="packages.config" />
+ </ItemGroup>
+ <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
+ <!-- To modify your build process, add your task inside one of the targets below and uncomment it.
+ Other similar extension points exist, see Microsoft.Common.targets.
+ <Target Name="BeforeBuild">
+ </Target>
+ <Target Name="AfterBuild">
+ </Target>
+ -->
+</Project> \ 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 000000000..74742759f
--- /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 000000000..21eb210d0
--- /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 000000000..05294421d
--- /dev/null
+++ b/MediaBrowser.ApiInteraction/packages.config
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<packages>
+ <package id="ServiceStack.Text" version="3.9.9" targetFramework="net45" />
+</packages> \ 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 000000000..98e072816
--- /dev/null
+++ b/MediaBrowser.Common/Events/GenericEventArgs.cs
@@ -0,0 +1,12 @@
+using System;
+
+namespace MediaBrowser.Common.Events
+{
+ /// <summary>
+ /// Provides a generic EventArgs subclass that can hold any kind of object
+ /// </summary>
+ public class GenericEventArgs<T> : EventArgs
+ {
+ public T Argument { get; set; }
+ }
+}
diff --git a/MediaBrowser.Common/Extensions/BaseExtensions.cs b/MediaBrowser.Common/Extensions/BaseExtensions.cs
new file mode 100644
index 000000000..77eb9fbb4
--- /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)));
+ }
+ }
+
+ /// <summary>
+ /// Examine a list of strings assumed to be file paths to see if it contains a parent of
+ /// the provided path.
+ /// </summary>
+ /// <param name="lst"></param>
+ /// <param name="path"></param>
+ /// <returns></returns>
+ public static bool ContainsParentFolder(this List<string> lst, string path)
+ {
+ path = path.TrimEnd('\\');
+ foreach (var str in lst)
+ {
+ //this should be a little quicker than examining each actual parent folder...
+ var compare = str.TrimEnd('\\');
+ if (path.Equals(compare,StringComparison.OrdinalIgnoreCase)
+ || (path.StartsWith(compare, StringComparison.OrdinalIgnoreCase) && path[compare.Length] == '\\')) return true;
+ }
+ return false;
+ }
+
+ /// <summary>
+ /// Helper method for Dictionaries since they throw on not-found keys
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <typeparam name="U"></typeparam>
+ /// <param name="dictionary"></param>
+ /// <param name="key"></param>
+ /// <param name="defaultValue"></param>
+ /// <returns></returns>
+ public static U GetValueOrDefault<T, U>(this Dictionary<T, U> dictionary, T key, U defaultValue)
+ {
+ U val;
+ if (!dictionary.TryGetValue(key, out val))
+ {
+ val = defaultValue;
+ }
+ return val;
+
+ }
+
+ }
+}
diff --git a/MediaBrowser.Common/Kernel/BaseApplicationPaths.cs b/MediaBrowser.Common/Kernel/BaseApplicationPaths.cs
new file mode 100644
index 000000000..fefbd354a
--- /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
+{
+ /// <summary>
+ /// Provides a base class to hold common application paths used by both the Ui and Server.
+ /// This can be subclassed to add application-specific paths.
+ /// </summary>
+ public abstract class BaseApplicationPaths
+ {
+ private string _programDataPath;
+ /// <summary>
+ /// Gets the path to the program data folder
+ /// </summary>
+ public string ProgramDataPath
+ {
+ get
+ {
+ if (_programDataPath == null)
+ {
+ _programDataPath = GetProgramDataPath();
+ }
+
+ return _programDataPath;
+ }
+ }
+
+ private string _pluginsPath;
+ /// <summary>
+ /// Gets the path to the plugin directory
+ /// </summary>
+ public string PluginsPath
+ {
+ get
+ {
+ if (_pluginsPath == null)
+ {
+ _pluginsPath = Path.Combine(ProgramDataPath, "plugins");
+ if (!Directory.Exists(_pluginsPath))
+ {
+ Directory.CreateDirectory(_pluginsPath);
+ }
+ }
+
+ return _pluginsPath;
+ }
+ }
+
+ private string _pluginConfigurationsPath;
+ /// <summary>
+ /// Gets the path to the plugin configurations directory
+ /// </summary>
+ public string PluginConfigurationsPath
+ {
+ get
+ {
+ if (_pluginConfigurationsPath == null)
+ {
+ _pluginConfigurationsPath = Path.Combine(PluginsPath, "configurations");
+ if (!Directory.Exists(_pluginConfigurationsPath))
+ {
+ Directory.CreateDirectory(_pluginConfigurationsPath);
+ }
+ }
+
+ return _pluginConfigurationsPath;
+ }
+ }
+
+ private string _logDirectoryPath;
+ /// <summary>
+ /// Gets the path to the log directory
+ /// </summary>
+ public string LogDirectoryPath
+ {
+ get
+ {
+ if (_logDirectoryPath == null)
+ {
+ _logDirectoryPath = Path.Combine(ProgramDataPath, "logs");
+ if (!Directory.Exists(_logDirectoryPath))
+ {
+ Directory.CreateDirectory(_logDirectoryPath);
+ }
+ }
+ return _logDirectoryPath;
+ }
+ }
+
+ private string _configurationDirectoryPath;
+ /// <summary>
+ /// Gets the path to the application configuration root directory
+ /// </summary>
+ public string ConfigurationDirectoryPath
+ {
+ get
+ {
+ if (_configurationDirectoryPath == null)
+ {
+ _configurationDirectoryPath = Path.Combine(ProgramDataPath, "config");
+ if (!Directory.Exists(_configurationDirectoryPath))
+ {
+ Directory.CreateDirectory(_configurationDirectoryPath);
+ }
+ }
+ return _configurationDirectoryPath;
+ }
+ }
+
+ private string _systemConfigurationFilePath;
+ /// <summary>
+ /// Gets the path to the system configuration file
+ /// </summary>
+ public string SystemConfigurationFilePath
+ {
+ get
+ {
+ if (_systemConfigurationFilePath == null)
+ {
+ _systemConfigurationFilePath = Path.Combine(ConfigurationDirectoryPath, "system.xml");
+ }
+ return _systemConfigurationFilePath;
+ }
+ }
+
+ /// <summary>
+ /// Gets the path to the application's ProgramDataFolder
+ /// </summary>
+ private static string GetProgramDataPath()
+ {
+ string programDataPath = ConfigurationManager.AppSettings["ProgramDataPath"];
+
+ // If it's a relative path, e.g. "..\"
+ if (!Path.IsPathRooted(programDataPath))
+ {
+ string path = Assembly.GetExecutingAssembly().Location;
+ path = Path.GetDirectoryName(path);
+
+ programDataPath = Path.Combine(path, programDataPath);
+
+ programDataPath = Path.GetFullPath(programDataPath);
+ }
+
+ if (!Directory.Exists(programDataPath))
+ {
+ Directory.CreateDirectory(programDataPath);
+ }
+
+ return programDataPath;
+ }
+ }
+}
diff --git a/MediaBrowser.Common/Kernel/BaseKernel.cs b/MediaBrowser.Common/Kernel/BaseKernel.cs
new file mode 100644
index 000000000..a6081a688
--- /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
+{
+ /// <summary>
+ /// Represents a shared base kernel for both the Ui and server apps
+ /// </summary>
+ public abstract class BaseKernel<TConfigurationType, TApplicationPathsType> : IDisposable, IKernel
+ where TConfigurationType : BaseApplicationConfiguration, new()
+ where TApplicationPathsType : BaseApplicationPaths, new()
+ {
+ #region ReloadBeginning Event
+ /// <summary>
+ /// Fires whenever the kernel begins reloading
+ /// </summary>
+ public event EventHandler<GenericEventArgs<IProgress<TaskProgress>>> ReloadBeginning;
+ private void OnReloadBeginning(IProgress<TaskProgress> progress)
+ {
+ if (ReloadBeginning != null)
+ {
+ ReloadBeginning(this, new GenericEventArgs<IProgress<TaskProgress>> { Argument = progress });
+ }
+ }
+ #endregion
+
+ #region ReloadCompleted Event
+ /// <summary>
+ /// Fires whenever the kernel completes reloading
+ /// </summary>
+ public event EventHandler<GenericEventArgs<IProgress<TaskProgress>>> ReloadCompleted;
+ private void OnReloadCompleted(IProgress<TaskProgress> progress)
+ {
+ if (ReloadCompleted != null)
+ {
+ ReloadCompleted(this, new GenericEventArgs<IProgress<TaskProgress>> { Argument = progress });
+ }
+ }
+ #endregion
+
+ /// <summary>
+ /// Gets the current configuration
+ /// </summary>
+ public TConfigurationType Configuration { get; private set; }
+
+ public TApplicationPathsType ApplicationPaths { get; private set; }
+
+ /// <summary>
+ /// Gets the list of currently loaded plugins
+ /// </summary>
+ [ImportMany(typeof(BasePlugin))]
+ public IEnumerable<BasePlugin> Plugins { get; private set; }
+
+ /// <summary>
+ /// Gets the list of currently registered http handlers
+ /// </summary>
+ [ImportMany(typeof(BaseHandler))]
+ private IEnumerable<BaseHandler> HttpHandlers { get; set; }
+
+ /// <summary>
+ /// Gets the list of currently registered Loggers
+ /// </summary>
+ [ImportMany(typeof(BaseLogger))]
+ public IEnumerable<BaseLogger> Loggers { get; set; }
+
+ /// <summary>
+ /// Both the Ui and server will have a built-in HttpServer.
+ /// People will inevitably want remote control apps so it's needed in the Ui too.
+ /// </summary>
+ public HttpServer HttpServer { get; private set; }
+
+ /// <summary>
+ /// This subscribes to HttpListener requests and finds the appropate BaseHandler to process it
+ /// </summary>
+ private IDisposable HttpListener { get; set; }
+
+ /// <summary>
+ /// Gets the MEF CompositionContainer
+ /// </summary>
+ private CompositionContainer CompositionContainer { get; set; }
+
+ protected virtual string HttpServerUrlPrefix
+ {
+ get
+ {
+ return "http://+:" + Configuration.HttpServerPortNumber + "/mediabrowser/";
+ }
+ }
+
+ /// <summary>
+ /// Gets the kernel context. Subclasses will have to override.
+ /// </summary>
+ public abstract KernelContext KernelContext { get; }
+
+ /// <summary>
+ /// Initializes the Kernel
+ /// </summary>
+ public async Task Init(IProgress<TaskProgress> progress)
+ {
+ Logger.Kernel = this;
+
+ // Performs initializations that only occur once
+ InitializeInternal(progress);
+
+ // Performs initializations that can be reloaded at anytime
+ await Reload(progress).ConfigureAwait(false);
+ }
+
+ /// <summary>
+ /// Performs initializations that only occur once
+ /// </summary>
+ protected virtual void InitializeInternal(IProgress<TaskProgress> progress)
+ {
+ ApplicationPaths = new TApplicationPathsType();
+
+ ReportProgress(progress, "Loading Configuration");
+ ReloadConfiguration();
+
+ ReportProgress(progress, "Loading Http Server");
+ ReloadHttpServer();
+ }
+
+ /// <summary>
+ /// Performs initializations that can be reloaded at anytime
+ /// </summary>
+ public async Task Reload(IProgress<TaskProgress> progress)
+ {
+ OnReloadBeginning(progress);
+
+ await ReloadInternal(progress).ConfigureAwait(false);
+
+ OnReloadCompleted(progress);
+
+ ReportProgress(progress, "Kernel.Reload Complete");
+ }
+
+ /// <summary>
+ /// Performs initializations that can be reloaded at anytime
+ /// </summary>
+ protected virtual async Task ReloadInternal(IProgress<TaskProgress> progress)
+ {
+ await Task.Run(() =>
+ {
+ ReportProgress(progress, "Loading Plugins");
+ ReloadComposableParts();
+
+ }).ConfigureAwait(false);
+ }
+
+ /// <summary>
+ /// Uses MEF to locate plugins
+ /// Subclasses can use this to locate types within plugins
+ /// </summary>
+ private void ReloadComposableParts()
+ {
+ DisposeComposableParts();
+
+ CompositionContainer = GetCompositionContainer(includeCurrentAssembly: true);
+
+ CompositionContainer.ComposeParts(this);
+
+ OnComposablePartsLoaded();
+
+ CompositionContainer.Catalog.Dispose();
+ }
+
+ /// <summary>
+ /// Constructs an MEF CompositionContainer based on the current running assembly and all plugin assemblies
+ /// </summary>
+ public CompositionContainer GetCompositionContainer(bool includeCurrentAssembly = false)
+ {
+ // Gets all plugin assemblies by first reading all bytes of the .dll and calling Assembly.Load against that
+ // This will prevent the .dll file from getting locked, and allow us to replace it when needed
+ IEnumerable<Assembly> pluginAssemblies = Directory.GetFiles(ApplicationPaths.PluginsPath, "*.dll", SearchOption.TopDirectoryOnly).Select(f => Assembly.Load(File.ReadAllBytes((f))));
+
+ var catalogs = new List<ComposablePartCatalog>();
+
+ catalogs.AddRange(pluginAssemblies.Select(a => new AssemblyCatalog(a)));
+
+ // Include composable parts in the Common assembly
+ catalogs.Add(new AssemblyCatalog(Assembly.GetExecutingAssembly()));
+
+ if (includeCurrentAssembly)
+ {
+ // Include composable parts in the subclass assembly
+ catalogs.Add(new AssemblyCatalog(GetType().Assembly));
+ }
+
+ return MefUtils.GetSafeCompositionContainer(catalogs);
+ }
+
+ /// <summary>
+ /// Fires after MEF finishes finding composable parts within plugin assemblies
+ /// </summary>
+ protected virtual void OnComposablePartsLoaded()
+ {
+ foreach (var logger in Loggers)
+ {
+ logger.Initialize(this);
+ }
+
+ // Start-up each plugin
+ foreach (var plugin in Plugins)
+ {
+ plugin.Initialize(this);
+ }
+ }
+
+ /// <summary>
+ /// Reloads application configuration from the config file
+ /// </summary>
+ private void ReloadConfiguration()
+ {
+ //Configuration information for anything other than server-specific configuration will have to come via the API... -ebr
+
+ // Deserialize config
+ // Use try/catch to avoid the extra file system lookup using File.Exists
+ try
+ {
+ Configuration = XmlSerializer.DeserializeFromFile<TConfigurationType>(ApplicationPaths.SystemConfigurationFilePath);
+ }
+ catch (FileNotFoundException)
+ {
+ Configuration = new TConfigurationType();
+ XmlSerializer.SerializeToFile(Configuration, ApplicationPaths.SystemConfigurationFilePath);
+ }
+ }
+
+ /// <summary>
+ /// Restarts the Http Server, or starts it if not currently running
+ /// </summary>
+ private void ReloadHttpServer()
+ {
+ DisposeHttpServer();
+
+ HttpServer = new HttpServer(HttpServerUrlPrefix);
+
+ HttpListener = HttpServer.Subscribe(ctx =>
+ {
+ BaseHandler handler = HttpHandlers.FirstOrDefault(h => h.HandlesRequest(ctx.Request));
+
+ // Find the appropiate http handler
+ if (handler != null)
+ {
+ // Need to create a new instance because handlers are currently stateful
+ handler = Activator.CreateInstance(handler.GetType()) as BaseHandler;
+
+ // No need to await this, despite the compiler warning
+ handler.ProcessRequest(ctx);
+ }
+ });
+ }
+
+ /// <summary>
+ /// Disposes all resources currently in use.
+ /// </summary>
+ public virtual void Dispose()
+ {
+ Logger.LogInfo("Beginning Kernel.Dispose");
+
+ DisposeHttpServer();
+
+ DisposeComposableParts();
+ }
+
+ /// <summary>
+ /// Disposes all objects gathered through MEF composable parts
+ /// </summary>
+ protected virtual void DisposeComposableParts()
+ {
+ if (CompositionContainer != null)
+ {
+ CompositionContainer.Dispose();
+ }
+ }
+
+ /// <summary>
+ /// Disposes the current HttpServer
+ /// </summary>
+ private void DisposeHttpServer()
+ {
+ if (HttpServer != null)
+ {
+ Logger.LogInfo("Disposing Http Server");
+
+ HttpServer.Dispose();
+ }
+
+ if (HttpListener != null)
+ {
+ HttpListener.Dispose();
+ }
+ }
+
+ /// <summary>
+ /// Gets the current application version
+ /// </summary>
+ public Version ApplicationVersion
+ {
+ get
+ {
+ return GetType().Assembly.GetName().Version;
+ }
+ }
+
+ protected void ReportProgress(IProgress<TaskProgress> progress, string message)
+ {
+ progress.Report(new TaskProgress { Description = message });
+
+ Logger.LogInfo(message);
+ }
+
+ BaseApplicationPaths IKernel.ApplicationPaths
+ {
+ get { return ApplicationPaths; }
+ }
+ }
+
+ public interface IKernel
+ {
+ BaseApplicationPaths ApplicationPaths { get; }
+ KernelContext KernelContext { get; }
+
+ Task Init(IProgress<TaskProgress> progress);
+ Task Reload(IProgress<TaskProgress> progress);
+ IEnumerable<BaseLogger> Loggers { get; }
+ void Dispose();
+ }
+}
diff --git a/MediaBrowser.Common/Kernel/KernelContext.cs b/MediaBrowser.Common/Kernel/KernelContext.cs
new file mode 100644
index 000000000..4d13ebb7b
--- /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 000000000..a97bc201f
--- /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 000000000..6fecef59c
--- /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<string>();
+
+ data.Add(Time.ToString(TimePattern));
+
+ data.Add(Severity.ToString());
+
+ if (!string.IsNullOrEmpty(Message))
+ {
+ data.Add(Encode(Message));
+ }
+
+ data.Add(ThreadId.ToString());
+
+ if (!string.IsNullOrEmpty(ThreadName))
+ {
+ data.Add(Encode(ThreadName));
+ }
+
+ return string.Join(" , ", data.ToArray());
+ }
+
+ private string Encode(string str)
+ {
+ return (str ?? "").Replace(",", ",,").Replace(Environment.NewLine, " [n] ");
+ }
+ }
+}
diff --git a/MediaBrowser.Common/Logging/LogSeverity.cs b/MediaBrowser.Common/Logging/LogSeverity.cs
new file mode 100644
index 000000000..97abfe7b5
--- /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 000000000..9ac02fe3e
--- /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 000000000..7ab67a137
--- /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 000000000..c08716614
--- /dev/null
+++ b/MediaBrowser.Common/MediaBrowser.Common.csproj
@@ -0,0 +1,164 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
+ <PropertyGroup>
+ <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+ <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+ <ProjectGuid>{9142EEFA-7570-41E1-BFCC-468BB571AF2F}</ProjectGuid>
+ <OutputType>Library</OutputType>
+ <AppDesignerFolder>Properties</AppDesignerFolder>
+ <RootNamespace>MediaBrowser.Common</RootNamespace>
+ <AssemblyName>MediaBrowser.Common</AssemblyName>
+ <TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
+ <ProjectTypeGuids>{60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
+ <FileAlignment>512</FileAlignment>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
+ <DebugSymbols>true</DebugSymbols>
+ <DebugType>full</DebugType>
+ <Optimize>false</Optimize>
+ <OutputPath>bin\Debug\</OutputPath>
+ <DefineConstants>DEBUG;TRACE</DefineConstants>
+ <ErrorReport>prompt</ErrorReport>
+ <WarningLevel>4</WarningLevel>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
+ <DebugType>pdbonly</DebugType>
+ <Optimize>true</Optimize>
+ <OutputPath>bin\Release\</OutputPath>
+ <DefineConstants>TRACE</DefineConstants>
+ <ErrorReport>prompt</ErrorReport>
+ <WarningLevel>4</WarningLevel>
+ </PropertyGroup>
+ <PropertyGroup>
+ <ApplicationIcon>Resources\Images\Icon.ico</ApplicationIcon>
+ </PropertyGroup>
+ <ItemGroup>
+ <Reference Include="MahApps.Metro">
+ <HintPath>..\packages\MahApps.Metro.0.9.0.0\lib\net40\MahApps.Metro.dll</HintPath>
+ </Reference>
+ <Reference Include="PresentationCore" />
+ <Reference Include="PresentationFramework" />
+ <Reference Include="protobuf-net">
+ <HintPath>..\protobuf-net\Full\net30\protobuf-net.dll</HintPath>
+ </Reference>
+ <Reference Include="ProtobufModelSerializer">
+ <HintPath>..\MediaBrowser.Model\bin\ProtobufModelSerializer.dll</HintPath>
+ </Reference>
+ <Reference Include="ServiceStack.Text, Version=3.9.9.0, Culture=neutral, processorArchitecture=MSIL">
+ <SpecificVersion>False</SpecificVersion>
+ <HintPath>..\packages\ServiceStack.Text.3.9.9\lib\net35\ServiceStack.Text.dll</HintPath>
+ </Reference>
+ <Reference Include="System" />
+ <Reference Include="System.ComponentModel.Composition" />
+ <Reference Include="System.Configuration" />
+ <Reference Include="System.Core" />
+ <Reference Include="System.Drawing" />
+ <Reference Include="System.Reactive.Core, Version=2.0.20823.0, Culture=neutral, PublicKeyToken=f300afd708cefcd3, processorArchitecture=MSIL">
+ <SpecificVersion>False</SpecificVersion>
+ <HintPath>..\packages\Rx-Core.2.0.20823\lib\Net45\System.Reactive.Core.dll</HintPath>
+ </Reference>
+ <Reference Include="System.Reactive.Interfaces, Version=2.0.20823.0, Culture=neutral, PublicKeyToken=f300afd708cefcd3, processorArchitecture=MSIL">
+ <SpecificVersion>False</SpecificVersion>
+ <HintPath>..\packages\Rx-Interfaces.2.0.20823\lib\Net45\System.Reactive.Interfaces.dll</HintPath>
+ </Reference>
+ <Reference Include="System.Reactive.Linq, Version=2.0.20823.0, Culture=neutral, PublicKeyToken=f300afd708cefcd3, processorArchitecture=MSIL">
+ <SpecificVersion>False</SpecificVersion>
+ <HintPath>..\packages\Rx-Linq.2.0.20823\lib\Net45\System.Reactive.Linq.dll</HintPath>
+ </Reference>
+ <Reference Include="System.Runtime.Remoting" />
+ <Reference Include="System.Runtime.Serialization" />
+ <Reference Include="System.Web" />
+ <Reference Include="System.Windows.Interactivity, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
+ <HintPath>..\packages\MahApps.Metro.0.9.0.0\lib\net40\System.Windows.Interactivity.dll</HintPath>
+ </Reference>
+ <Reference Include="System.Xaml" />
+ <Reference Include="System.Xml.Linq" />
+ <Reference Include="System.Data.DataSetExtensions" />
+ <Reference Include="Microsoft.CSharp" />
+ <Reference Include="System.Data" />
+ <Reference Include="System.Xml" />
+ <Reference Include="WindowsBase" />
+ </ItemGroup>
+ <ItemGroup>
+ <Compile Include="Extensions\BaseExtensions.cs" />
+ <Compile Include="Events\GenericEventArgs.cs" />
+ <Compile Include="Kernel\BaseApplicationPaths.cs" />
+ <Compile Include="Logging\BaseLogger.cs" />
+ <Compile Include="Logging\LogSeverity.cs" />
+ <Compile Include="Logging\TraceFileLogger.cs" />
+ <Compile Include="Mef\MefUtils.cs" />
+ <Compile Include="Net\Handlers\StaticFileHandler.cs" />
+ <Compile Include="Net\MimeTypes.cs" />
+ <Compile Include="Plugins\BaseTheme.cs" />
+ <Compile Include="Properties\Resources.Designer.cs">
+ <AutoGen>True</AutoGen>
+ <DesignTime>True</DesignTime>
+ <DependentUpon>Resources.resx</DependentUpon>
+ </Compile>
+ <Compile Include="Serialization\JsonSerializer.cs" />
+ <Compile Include="Kernel\BaseKernel.cs" />
+ <Compile Include="Kernel\KernelContext.cs" />
+ <Compile Include="Logging\Logger.cs" />
+ <Compile Include="Logging\LogRow.cs" />
+ <Compile Include="Net\Handlers\BaseEmbeddedResourceHandler.cs" />
+ <Compile Include="Net\Handlers\BaseHandler.cs" />
+ <Compile Include="Net\Handlers\BaseSerializationHandler.cs" />
+ <Compile Include="Net\HttpServer.cs" />
+ <Compile Include="Net\Request.cs" />
+ <Compile Include="Plugins\BasePlugin.cs" />
+ <Compile Include="Properties\AssemblyInfo.cs" />
+ <Compile Include="Serialization\JsvSerializer.cs" />
+ <Compile Include="Serialization\ProtobufSerializer.cs" />
+ <Compile Include="Serialization\XmlSerializer.cs" />
+ <Compile Include="UI\BaseApplication.cs" />
+ <Compile Include="UI\Splash.xaml.cs">
+ <DependentUpon>Splash.xaml</DependentUpon>
+ </Compile>
+ <Compile Include="UI\SingleInstance.cs" />
+ </ItemGroup>
+ <ItemGroup>
+ <None Include="app.config" />
+ <None Include="packages.config" />
+ </ItemGroup>
+ <ItemGroup>
+ <ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj">
+ <Project>{7eeeb4bb-f3e8-48fc-b4c5-70f0fff8329b}</Project>
+ <Name>MediaBrowser.Model</Name>
+ </ProjectReference>
+ </ItemGroup>
+ <ItemGroup>
+ <Page Include="UI\Splash.xaml">
+ <SubType>Designer</SubType>
+ <Generator>MSBuild:Compile</Generator>
+ </Page>
+ </ItemGroup>
+ <ItemGroup>
+ <EmbeddedResource Include="Properties\Resources.resx">
+ <Generator>ResXFileCodeGenerator</Generator>
+ <LastGenOutput>Resources.Designer.cs</LastGenOutput>
+ <SubType>Designer</SubType>
+ </EmbeddedResource>
+ </ItemGroup>
+ <ItemGroup>
+ <Resource Include="Resources\Images\mblogoblack.png" />
+ </ItemGroup>
+ <ItemGroup>
+ <Resource Include="Resources\Images\Icon.ico" />
+ </ItemGroup>
+ <ItemGroup>
+ <Resource Include="Resources\Images\mblogowhite.png" />
+ </ItemGroup>
+ <ItemGroup>
+ <Resource Include="Resources\Images\spinner.gif" />
+ </ItemGroup>
+ <ItemGroup />
+ <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
+ <!-- To modify your build process, add your task inside one of the targets below and uncomment it.
+ Other similar extension points exist, see Microsoft.Common.targets.
+ <Target Name="BeforeBuild">
+ </Target>
+ <Target Name="AfterBuild">
+ </Target>
+ -->
+</Project> \ 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 000000000..55d888697
--- /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
+ {
+ /// <summary>
+ /// Plugins that live on both the server and UI are going to have references to assemblies from both sides.
+ /// But looks for Parts on one side, it will throw an exception when it seems Types from the other side that it doesn't have a reference to.
+ /// For example, a plugin provides a Resolver. When MEF runs in the UI, it will throw an exception when it sees the resolver because there won't be a reference to the base class.
+ /// This method will catch those exceptions while retining the list of Types that MEF is able to resolve.
+ /// </summary>
+ public static CompositionContainer GetSafeCompositionContainer(IEnumerable<ComposablePartCatalog> catalogs)
+ {
+ var newList = new List<ComposablePartCatalog>();
+
+ // Go through each Catalog
+ foreach (var catalog in catalogs)
+ {
+ try
+ {
+ // Try to have MEF find Parts
+ catalog.Parts.ToArray();
+
+ // If it succeeds we can use the entire catalog
+ newList.Add(catalog);
+ }
+ catch (ReflectionTypeLoadException ex)
+ {
+ // If it fails we can still get a list of the Types it was able to resolve and create TypeCatalogs
+ var typeCatalogs = ex.Types.Where(t => t != null).Select(t => new TypeCatalog(t));
+ newList.AddRange(typeCatalogs);
+ }
+ }
+
+ return new CompositionContainer(new AggregateCatalog(newList));
+ }
+ }
+}
diff --git a/MediaBrowser.Common/Net/Handlers/BaseEmbeddedResourceHandler.cs b/MediaBrowser.Common/Net/Handlers/BaseEmbeddedResourceHandler.cs
new file mode 100644
index 000000000..579e341fe
--- /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 000000000..a5058e6ca
--- /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;
+ }
+ }
+
+ /// <summary>
+ /// The original HttpListenerContext
+ /// </summary>
+ protected HttpListenerContext HttpListenerContext { get; set; }
+
+ /// <summary>
+ /// The original QueryString
+ /// </summary>
+ protected NameValueCollection QueryString
+ {
+ get
+ {
+ return HttpListenerContext.Request.QueryString;
+ }
+ }
+
+ private List<KeyValuePair<long, long?>> _requestedRanges;
+ protected IEnumerable<KeyValuePair<long, long?>> RequestedRanges
+ {
+ get
+ {
+ if (_requestedRanges == null)
+ {
+ _requestedRanges = new List<KeyValuePair<long, long?>>();
+
+ if (IsRangeRequest)
+ {
+ // Example: bytes=0-,32-63
+ string[] ranges = HttpListenerContext.Request.Headers["Range"].Split('=')[1].Split(',');
+
+ foreach (string range in ranges)
+ {
+ string[] vals = range.Split('-');
+
+ long start = 0;
+ long? end = null;
+
+ if (!string.IsNullOrEmpty(vals[0]))
+ {
+ start = long.Parse(vals[0]);
+ }
+ if (!string.IsNullOrEmpty(vals[1]))
+ {
+ end = long.Parse(vals[1]);
+ }
+
+ _requestedRanges.Add(new KeyValuePair<long, long?>(start, end));
+ }
+ }
+ }
+
+ return _requestedRanges;
+ }
+ }
+
+ protected bool IsRangeRequest
+ {
+ get
+ {
+ return HttpListenerContext.Request.Headers.AllKeys.Contains("Range");
+ }
+ }
+
+ private bool ClientSupportsCompression
+ {
+ get
+ {
+ string enc = HttpListenerContext.Request.Headers["Accept-Encoding"] ?? string.Empty;
+
+ return enc.IndexOf("deflate", StringComparison.OrdinalIgnoreCase) != -1 || enc.IndexOf("gzip", StringComparison.OrdinalIgnoreCase) != -1;
+ }
+ }
+
+ private string CompressionMethod
+ {
+ get
+ {
+ string enc = HttpListenerContext.Request.Headers["Accept-Encoding"] ?? string.Empty;
+
+ if (enc.IndexOf("deflate", StringComparison.OrdinalIgnoreCase) != -1)
+ {
+ return "deflate";
+ }
+ if (enc.IndexOf("gzip", StringComparison.OrdinalIgnoreCase) != -1)
+ {
+ return "gzip";
+ }
+
+ return null;
+ }
+ }
+
+ public virtual async Task ProcessRequest(HttpListenerContext ctx)
+ {
+ HttpListenerContext = ctx;
+
+ string url = ctx.Request.Url.ToString();
+ Logger.LogInfo("Http Server received request at: " + url);
+ Logger.LogInfo("Http Headers: " + string.Join(",", ctx.Request.Headers.AllKeys.Select(k => k + "=" + ctx.Request.Headers[k])));
+
+ ctx.Response.AddHeader("Access-Control-Allow-Origin", "*");
+
+ ctx.Response.KeepAlive = true;
+
+ try
+ {
+ if (SupportsByteRangeRequests && IsRangeRequest)
+ {
+ ctx.Response.Headers["Accept-Ranges"] = "bytes";
+ }
+
+ ResponseInfo responseInfo = await GetResponseInfo().ConfigureAwait(false);
+
+ if (responseInfo.IsResponseValid)
+ {
+ // Set the initial status code
+ // When serving a range request, we need to return status code 206 to indicate a partial response body
+ responseInfo.StatusCode = SupportsByteRangeRequests && IsRangeRequest ? 206 : 200;
+ }
+
+ ctx.Response.ContentType = responseInfo.ContentType;
+
+ if (!string.IsNullOrEmpty(responseInfo.Etag))
+ {
+ ctx.Response.Headers["ETag"] = responseInfo.Etag;
+ }
+
+ if (ctx.Request.Headers.AllKeys.Contains("If-Modified-Since"))
+ {
+ DateTime ifModifiedSince;
+
+ if (DateTime.TryParse(ctx.Request.Headers["If-Modified-Since"], out ifModifiedSince))
+ {
+ // If the cache hasn't expired yet just return a 304
+ if (IsCacheValid(ifModifiedSince.ToUniversalTime(), responseInfo.CacheDuration, responseInfo.DateLastModified))
+ {
+ // ETag must also match (if supplied)
+ if ((responseInfo.Etag ?? string.Empty).Equals(ctx.Request.Headers["If-None-Match"] ?? string.Empty))
+ {
+ responseInfo.StatusCode = 304;
+ }
+ }
+ }
+ }
+
+ Logger.LogInfo("Responding with status code {0} for url {1}", responseInfo.StatusCode, url);
+
+ if (responseInfo.IsResponseValid)
+ {
+ await ProcessUncachedRequest(ctx, responseInfo).ConfigureAwait(false);
+ }
+ else
+ {
+ ctx.Response.StatusCode = responseInfo.StatusCode;
+ ctx.Response.SendChunked = false;
+ }
+ }
+ catch (Exception ex)
+ {
+ // It might be too late if some response data has already been transmitted, but try to set this
+ ctx.Response.StatusCode = 500;
+
+ Logger.LogException(ex);
+ }
+ finally
+ {
+ DisposeResponseStream();
+ }
+ }
+
+ private async Task ProcessUncachedRequest(HttpListenerContext ctx, ResponseInfo responseInfo)
+ {
+ long? totalContentLength = TotalContentLength;
+
+ // By default, use chunked encoding if we don't know the content length
+ bool useChunkedEncoding = UseChunkedEncoding == null ? (totalContentLength == null) : UseChunkedEncoding.Value;
+
+ // Don't force this to true. HttpListener will default it to true if supported by the client.
+ if (!useChunkedEncoding)
+ {
+ ctx.Response.SendChunked = false;
+ }
+
+ // Set the content length, if we know it
+ if (totalContentLength.HasValue)
+ {
+ ctx.Response.ContentLength64 = totalContentLength.Value;
+ }
+
+ var compressResponse = responseInfo.CompressResponse && ClientSupportsCompression;
+
+ // Add the compression header
+ if (compressResponse)
+ {
+ ctx.Response.AddHeader("Content-Encoding", CompressionMethod);
+ }
+
+ if (responseInfo.DateLastModified.HasValue)
+ {
+ ctx.Response.Headers[HttpResponseHeader.LastModified] = responseInfo.DateLastModified.Value.ToString("r");
+ }
+
+ // Add caching headers
+ if (responseInfo.CacheDuration.Ticks > 0)
+ {
+ CacheResponse(ctx.Response, responseInfo.CacheDuration);
+ }
+
+ // Set the status code
+ ctx.Response.StatusCode = responseInfo.StatusCode;
+
+ if (responseInfo.IsResponseValid)
+ {
+ // Finally, write the response data
+ Stream outputStream = ctx.Response.OutputStream;
+
+ if (compressResponse)
+ {
+ if (CompressionMethod.Equals("deflate", StringComparison.OrdinalIgnoreCase))
+ {
+ CompressedStream = new DeflateStream(outputStream, CompressionLevel.Fastest, false);
+ }
+ else
+ {
+ CompressedStream = new GZipStream(outputStream, CompressionLevel.Fastest, false);
+ }
+
+ outputStream = CompressedStream;
+ }
+
+ await WriteResponseToOutputStream(outputStream).ConfigureAwait(false);
+ }
+ else
+ {
+ ctx.Response.SendChunked = false;
+ }
+ }
+
+ private void CacheResponse(HttpListenerResponse response, TimeSpan duration)
+ {
+ response.Headers[HttpResponseHeader.CacheControl] = "public, max-age=" + Convert.ToInt32(duration.TotalSeconds);
+ response.Headers[HttpResponseHeader.Expires] = DateTime.UtcNow.Add(duration).ToString("r");
+ }
+
+ protected abstract Task WriteResponseToOutputStream(Stream stream);
+
+ protected virtual void DisposeResponseStream()
+ {
+ if (CompressedStream != null)
+ {
+ CompressedStream.Dispose();
+ }
+
+ HttpListenerContext.Response.OutputStream.Dispose();
+ }
+
+ private bool IsCacheValid(DateTime ifModifiedSince, TimeSpan cacheDuration, DateTime? dateModified)
+ {
+ if (dateModified.HasValue)
+ {
+ DateTime lastModified = NormalizeDateForComparison(dateModified.Value);
+ ifModifiedSince = NormalizeDateForComparison(ifModifiedSince);
+
+ return lastModified <= ifModifiedSince;
+ }
+
+ DateTime cacheExpirationDate = ifModifiedSince.Add(cacheDuration);
+
+ if (DateTime.UtcNow < cacheExpirationDate)
+ {
+ return true;
+ }
+
+ return false;
+ }
+
+ /// <summary>
+ /// When the browser sends the IfModifiedDate, it's precision is limited to seconds, so this will account for that
+ /// </summary>
+ private DateTime NormalizeDateForComparison(DateTime date)
+ {
+ return new DateTime(date.Year, date.Month, date.Day, date.Hour, date.Minute, date.Second, date.Kind);
+ }
+
+ protected virtual long? GetTotalContentLength()
+ {
+ return null;
+ }
+
+ protected abstract Task<ResponseInfo> GetResponseInfo();
+
+ private Hashtable _formValues;
+
+ /// <summary>
+ /// Gets a value from form POST data
+ /// </summary>
+ protected async Task<string> GetFormValue(string name)
+ {
+ if (_formValues == null)
+ {
+ _formValues = await GetFormValues(HttpListenerContext.Request).ConfigureAwait(false);
+ }
+
+ if (_formValues.ContainsKey(name))
+ {
+ return _formValues[name].ToString();
+ }
+
+ return null;
+ }
+
+ /// <summary>
+ /// Extracts form POST data from a request
+ /// </summary>
+ private async Task<Hashtable> GetFormValues(HttpListenerRequest request)
+ {
+ var formVars = new Hashtable();
+
+ if (request.HasEntityBody)
+ {
+ if (request.ContentType.IndexOf("application/x-www-form-urlencoded", StringComparison.OrdinalIgnoreCase) != -1)
+ {
+ using (Stream requestBody = request.InputStream)
+ {
+ using (var reader = new StreamReader(requestBody, request.ContentEncoding))
+ {
+ string s = await reader.ReadToEndAsync().ConfigureAwait(false);
+
+ string[] pairs = s.Split('&');
+
+ for (int x = 0; x < pairs.Length; x++)
+ {
+ string pair = pairs[x];
+
+ int index = pair.IndexOf('=');
+
+ if (index != -1)
+ {
+ string name = pair.Substring(0, index);
+ string value = pair.Substring(index + 1);
+ formVars.Add(name, value);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ return formVars;
+ }
+ }
+
+ public class ResponseInfo
+ {
+ public string ContentType { get; set; }
+ public string Etag { get; set; }
+ public DateTime? DateLastModified { get; set; }
+ public TimeSpan CacheDuration { get; set; }
+ public bool CompressResponse { get; set; }
+ public int StatusCode { get; set; }
+
+ public ResponseInfo()
+ {
+ CacheDuration = TimeSpan.FromTicks(0);
+
+ CompressResponse = true;
+
+ StatusCode = 200;
+ }
+
+ public bool IsResponseValid
+ {
+ get
+ {
+ return StatusCode == 200 || StatusCode == 206;
+ }
+ }
+ }
+} \ 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 000000000..53b3ee817
--- /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<T> : BaseHandler
+ where T : class
+ {
+ public SerializationFormat SerializationFormat
+ {
+ get
+ {
+ string format = QueryString["dataformat"];
+
+ if (string.IsNullOrEmpty(format))
+ {
+ return SerializationFormat.Json;
+ }
+
+ return (SerializationFormat)Enum.Parse(typeof(SerializationFormat), format, true);
+ }
+ }
+
+ protected string ContentType
+ {
+ get
+ {
+ switch (SerializationFormat)
+ {
+ case SerializationFormat.Jsv:
+ return "text/plain";
+ case SerializationFormat.Protobuf:
+ return "application/x-protobuf";
+ default:
+ return MimeTypes.JsonMimeType;
+ }
+ }
+ }
+
+ protected override async Task<ResponseInfo> GetResponseInfo()
+ {
+ ResponseInfo info = new ResponseInfo
+ {
+ ContentType = ContentType
+ };
+
+ _objectToSerialize = await GetObjectToSerialize().ConfigureAwait(false);
+
+ if (_objectToSerialize == null)
+ {
+ info.StatusCode = 404;
+ }
+
+ return info;
+ }
+
+ private T _objectToSerialize;
+
+ protected abstract Task<T> GetObjectToSerialize();
+
+ protected override Task WriteResponseToOutputStream(Stream stream)
+ {
+ return Task.Run(() =>
+ {
+ switch (SerializationFormat)
+ {
+ case SerializationFormat.Jsv:
+ JsvSerializer.SerializeToStream(_objectToSerialize, stream);
+ break;
+ case SerializationFormat.Protobuf:
+ ProtobufSerializer.SerializeToStream(_objectToSerialize, stream);
+ break;
+ default:
+ JsonSerializer.SerializeToStream(_objectToSerialize, stream);
+ break;
+ }
+ });
+ }
+ }
+
+ public enum SerializationFormat
+ {
+ Json,
+ Jsv,
+ Protobuf
+ }
+
+}
diff --git a/MediaBrowser.Common/Net/Handlers/StaticFileHandler.cs b/MediaBrowser.Common/Net/Handlers/StaticFileHandler.cs
new file mode 100644
index 000000000..11438b164
--- /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<ResponseInfo> GetResponseInfo()
+ {
+ ResponseInfo info = new ResponseInfo
+ {
+ ContentType = MimeTypes.GetMimeType(Path),
+ };
+
+ try
+ {
+ SourceStream = File.OpenRead(Path);
+ }
+ catch (FileNotFoundException ex)
+ {
+ info.StatusCode = 404;
+ Logger.LogException(ex);
+ }
+ catch (DirectoryNotFoundException ex)
+ {
+ info.StatusCode = 404;
+ Logger.LogException(ex);
+ }
+ catch (UnauthorizedAccessException ex)
+ {
+ info.StatusCode = 403;
+ Logger.LogException(ex);
+ }
+
+ info.CompressResponse = ShouldCompressResponse(info.ContentType);
+
+ if (SourceStream != null)
+ {
+ info.DateLastModified = File.GetLastWriteTimeUtc(Path);
+ }
+
+ return Task.FromResult<ResponseInfo>(info);
+ }
+
+ protected override Task WriteResponseToOutputStream(Stream stream)
+ {
+ if (IsRangeRequest)
+ {
+ KeyValuePair<long, long?> requestedRange = RequestedRanges.First();
+
+ // If the requested range is "0-" and we know the total length, we can optimize by avoiding having to buffer the content into memory
+ if (requestedRange.Value == null && TotalContentLength != null)
+ {
+ return ServeCompleteRangeRequest(requestedRange, stream);
+ }
+ if (TotalContentLength.HasValue)
+ {
+ // This will have to buffer a portion of the content into memory
+ return ServePartialRangeRequestWithKnownTotalContentLength(requestedRange, stream);
+ }
+
+ // This will have to buffer the entire content into memory
+ return ServePartialRangeRequestWithUnknownTotalContentLength(requestedRange, stream);
+ }
+
+ return SourceStream.CopyToAsync(stream);
+ }
+
+ protected override void DisposeResponseStream()
+ {
+ base.DisposeResponseStream();
+
+ if (SourceStream != null)
+ {
+ SourceStream.Dispose();
+ }
+ }
+
+ /// <summary>
+ /// Handles a range request of "bytes=0-"
+ /// This will serve the complete content and add the content-range header
+ /// </summary>
+ private Task ServeCompleteRangeRequest(KeyValuePair<long, long?> requestedRange, Stream responseStream)
+ {
+ long totalContentLength = TotalContentLength.Value;
+
+ long rangeStart = requestedRange.Key;
+ long rangeEnd = totalContentLength - 1;
+ long rangeLength = 1 + rangeEnd - rangeStart;
+
+ // Content-Length is the length of what we're serving, not the original content
+ HttpListenerContext.Response.ContentLength64 = rangeLength;
+ HttpListenerContext.Response.Headers["Content-Range"] = string.Format("bytes {0}-{1}/{2}", rangeStart, rangeEnd, totalContentLength);
+
+ if (rangeStart > 0)
+ {
+ SourceStream.Position = rangeStart;
+ }
+
+ return SourceStream.CopyToAsync(responseStream);
+ }
+
+ /// <summary>
+ /// Serves a partial range request where the total content length is not known
+ /// </summary>
+ private async Task ServePartialRangeRequestWithUnknownTotalContentLength(KeyValuePair<long, long?> requestedRange, Stream responseStream)
+ {
+ // Read the entire stream so that we can determine the length
+ byte[] bytes = await ReadBytes(SourceStream, 0, null).ConfigureAwait(false);
+
+ long totalContentLength = bytes.LongLength;
+
+ long rangeStart = requestedRange.Key;
+ long rangeEnd = requestedRange.Value ?? (totalContentLength - 1);
+ long rangeLength = 1 + rangeEnd - rangeStart;
+
+ // Content-Length is the length of what we're serving, not the original content
+ HttpListenerContext.Response.ContentLength64 = rangeLength;
+ HttpListenerContext.Response.Headers["Content-Range"] = string.Format("bytes {0}-{1}/{2}", rangeStart, rangeEnd, totalContentLength);
+
+ await responseStream.WriteAsync(bytes, Convert.ToInt32(rangeStart), Convert.ToInt32(rangeLength)).ConfigureAwait(false);
+ }
+
+ /// <summary>
+ /// Serves a partial range request where the total content length is already known
+ /// </summary>
+ private async Task ServePartialRangeRequestWithKnownTotalContentLength(KeyValuePair<long, long?> requestedRange, Stream responseStream)
+ {
+ long totalContentLength = TotalContentLength.Value;
+ long rangeStart = requestedRange.Key;
+ long rangeEnd = requestedRange.Value ?? (totalContentLength - 1);
+ long rangeLength = 1 + rangeEnd - rangeStart;
+
+ // Only read the bytes we need
+ byte[] bytes = await ReadBytes(SourceStream, Convert.ToInt32(rangeStart), Convert.ToInt32(rangeLength)).ConfigureAwait(false);
+
+ // Content-Length is the length of what we're serving, not the original content
+ HttpListenerContext.Response.ContentLength64 = rangeLength;
+
+ HttpListenerContext.Response.Headers["Content-Range"] = string.Format("bytes {0}-{1}/{2}", rangeStart, rangeEnd, totalContentLength);
+
+ await responseStream.WriteAsync(bytes, 0, Convert.ToInt32(rangeLength)).ConfigureAwait(false);
+ }
+
+ /// <summary>
+ /// Reads bytes from a stream
+ /// </summary>
+ /// <param name="input">The input stream</param>
+ /// <param name="start">The starting position</param>
+ /// <param name="count">The number of bytes to read, or null to read to the end.</param>
+ private async Task<byte[]> ReadBytes(Stream input, int start, int? count)
+ {
+ if (start > 0)
+ {
+ input.Position = start;
+ }
+
+ if (count == null)
+ {
+ var buffer = new byte[16 * 1024];
+
+ using (var ms = new MemoryStream())
+ {
+ int read;
+ while ((read = await input.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false)) > 0)
+ {
+ await ms.WriteAsync(buffer, 0, read).ConfigureAwait(false);
+ }
+ return ms.ToArray();
+ }
+ }
+ else
+ {
+ var buffer = new byte[count.Value];
+
+ using (var ms = new MemoryStream())
+ {
+ int read = await input.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false);
+
+ await ms.WriteAsync(buffer, 0, read).ConfigureAwait(false);
+
+ return ms.ToArray();
+ }
+ }
+
+ }
+ }
+}
diff --git a/MediaBrowser.Common/Net/HttpServer.cs b/MediaBrowser.Common/Net/HttpServer.cs
new file mode 100644
index 000000000..276e14eb3
--- /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<HttpListenerContext>, IDisposable
+ {
+ private readonly HttpListener _listener;
+ private readonly IObservable<HttpListenerContext> _stream;
+
+ public HttpServer(string url)
+ {
+ _listener = new HttpListener();
+ _listener.Prefixes.Add(url);
+ _listener.Start();
+ _stream = ObservableHttpContext();
+ }
+
+ private IObservable<HttpListenerContext> ObservableHttpContext()
+ {
+ return Observable.Create<HttpListenerContext>(obs =>
+ Observable.FromAsync(() => _listener.GetContextAsync())
+ .Subscribe(obs))
+ .Repeat()
+ .Retry()
+ .Publish()
+ .RefCount();
+ }
+ public void Dispose()
+ {
+ _listener.Stop();
+ }
+
+ public IDisposable Subscribe(IObserver<HttpListenerContext> observer)
+ {
+ return _stream.Subscribe(observer);
+ }
+ }
+} \ 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 000000000..fb85b0f2a
--- /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 000000000..795c9c36b
--- /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<string, IEnumerable<string>> Headers { get; set; }
+ public Stream InputStream { get; set; }
+ public string RawUrl { get; set; }
+ public int ContentLength
+ {
+ get { return int.Parse(Headers["Content-Length"].First()); }
+ }
+ }
+} \ 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 000000000..70e573817
--- /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
+{
+ /// <summary>
+ /// Provides a common base class for all plugins
+ /// </summary>
+ public abstract class BasePlugin : IDisposable
+ {
+ protected IKernel Kernel { get; private set; }
+
+ /// <summary>
+ /// Gets or sets the plugin's current context
+ /// </summary>
+ protected KernelContext Context { get { return Kernel.KernelContext; } }
+
+ /// <summary>
+ /// Gets the name of the plugin
+ /// </summary>
+ public abstract string Name { get; }
+
+ /// <summary>
+ /// Gets the type of configuration this plugin uses
+ /// </summary>
+ public virtual Type ConfigurationType
+ {
+ get { return typeof (BasePluginConfiguration); }
+ }
+
+ /// <summary>
+ /// Gets the plugin version
+ /// </summary>
+ public Version Version
+ {
+ get
+ {
+ return GetType().Assembly.GetName().Version;
+ }
+ }
+
+ /// <summary>
+ /// Gets the name the assembly file
+ /// </summary>
+ public string AssemblyFileName
+ {
+ get
+ {
+ return GetType().Assembly.GetName().Name + ".dll";
+ }
+ }
+
+ private DateTime? _configurationDateLastModified;
+ public DateTime ConfigurationDateLastModified
+ {
+ get
+ {
+ if (_configurationDateLastModified == null)
+ {
+ if (File.Exists(ConfigurationFilePath))
+ {
+ _configurationDateLastModified = File.GetLastWriteTimeUtc(ConfigurationFilePath);
+ }
+ }
+
+ return _configurationDateLastModified ?? DateTime.MinValue;
+ }
+ }
+
+ /// <summary>
+ /// Gets the path to the assembly file
+ /// </summary>
+ public string AssemblyFilePath
+ {
+ get
+ {
+ return Path.Combine(Kernel.ApplicationPaths.PluginsPath, AssemblyFileName);
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets the current plugin configuration
+ /// </summary>
+ public BasePluginConfiguration Configuration { get; protected set; }
+
+ /// <summary>
+ /// Gets the name of the configuration file. Subclasses should override
+ /// </summary>
+ public virtual string ConfigurationFileName
+ {
+ get
+ {
+ return Name.Replace(" ", string.Empty) + ".xml";
+ }
+ }
+
+ /// <summary>
+ /// Gets the full path to the configuration file
+ /// </summary>
+ public string ConfigurationFilePath
+ {
+ get
+ {
+ return Path.Combine(Kernel.ApplicationPaths.PluginConfigurationsPath, ConfigurationFileName);
+ }
+ }
+
+ private string _dataFolderPath;
+ /// <summary>
+ /// Gets the full path to the data folder, where the plugin can store any miscellaneous files needed
+ /// </summary>
+ public string DataFolderPath
+ {
+ get
+ {
+ if (_dataFolderPath == null)
+ {
+ // Give the folder name the same name as the config file name
+ // We can always make this configurable if/when needed
+ _dataFolderPath = Path.Combine(Kernel.ApplicationPaths.PluginsPath, Path.GetFileNameWithoutExtension(ConfigurationFileName));
+
+ if (!Directory.Exists(_dataFolderPath))
+ {
+ Directory.CreateDirectory(_dataFolderPath);
+ }
+ }
+
+ return _dataFolderPath;
+ }
+ }
+
+ public bool Enabled
+ {
+ get
+ {
+ return Configuration.Enabled;
+ }
+ }
+
+ /// <summary>
+ /// Returns true or false indicating if the plugin should be downloaded and run within the Ui.
+ /// </summary>
+ public virtual bool DownloadToUi
+ {
+ get
+ {
+ return false;
+ }
+ }
+
+ public void Initialize(IKernel kernel)
+ {
+ Initialize(kernel, true);
+ }
+
+ /// <summary>
+ /// Starts the plugin.
+ /// </summary>
+ public void Initialize(IKernel kernel, bool loadFeatures)
+ {
+ Kernel = kernel;
+
+ if (loadFeatures)
+ {
+ ReloadConfiguration();
+
+ if (Enabled)
+ {
+ if (kernel.KernelContext == KernelContext.Server)
+ {
+ InitializeOnServer();
+ }
+ else if (kernel.KernelContext == KernelContext.Ui)
+ {
+ InitializeInUi();
+ }
+ }
+ }
+ }
+
+ /// <summary>
+ /// Starts the plugin on the server
+ /// </summary>
+ protected virtual void InitializeOnServer()
+ {
+ }
+
+ /// <summary>
+ /// Starts the plugin in the Ui
+ /// </summary>
+ protected virtual void InitializeInUi()
+ {
+ }
+
+ /// <summary>
+ /// Disposes the plugins. Undos all actions performed during Init.
+ /// </summary>
+ public void Dispose()
+ {
+ Logger.LogInfo("Disposing {0} Plugin", Name);
+
+ if (Context == KernelContext.Server)
+ {
+ DisposeOnServer();
+ }
+ else if (Context == KernelContext.Ui)
+ {
+ InitializeInUi();
+ }
+ }
+
+ /// <summary>
+ /// Disposes the plugin on the server
+ /// </summary>
+ protected virtual void DisposeOnServer()
+ {
+ }
+
+ /// <summary>
+ /// Disposes the plugin in the Ui
+ /// </summary>
+ protected virtual void DisposeInUi()
+ {
+ }
+
+ public void ReloadConfiguration()
+ {
+ if (!File.Exists(ConfigurationFilePath))
+ {
+ Configuration = Activator.CreateInstance(ConfigurationType) as BasePluginConfiguration;
+ XmlSerializer.SerializeToFile(Configuration, ConfigurationFilePath);
+ }
+ else
+ {
+ Configuration = XmlSerializer.DeserializeFromFile(ConfigurationType, ConfigurationFilePath) as BasePluginConfiguration;
+ }
+
+ // Reset this so it will be loaded again next time it's accessed
+ _configurationDateLastModified = null;
+ }
+ }
+}
diff --git a/MediaBrowser.Common/Plugins/BaseTheme.cs b/MediaBrowser.Common/Plugins/BaseTheme.cs
new file mode 100644
index 000000000..32a28258b
--- /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;
+ }
+ }
+
+ /// <summary>
+ /// Gets the MEF CompositionContainer
+ /// </summary>
+ private CompositionContainer CompositionContainer { get; set; }
+
+ /// <summary>
+ /// Gets the list of global resources
+ /// </summary>
+ [ImportMany(typeof(ResourceDictionary))]
+ public IEnumerable<ResourceDictionary> GlobalResources { get; private set; }
+
+ /// <summary>
+ /// Gets the list of pages
+ /// </summary>
+ [ImportMany(typeof(Page))]
+ public IEnumerable<Page> Pages { get; private set; }
+
+ /// <summary>
+ /// Gets the pack Uri of the Login page
+ /// </summary>
+ public abstract Uri LoginPageUri { get; }
+
+ protected override void InitializeInUi()
+ {
+ base.InitializeInUi();
+
+ ComposeParts();
+ }
+
+ private void ComposeParts()
+ {
+ var catalog = new AssemblyCatalog(GetType().Assembly);
+
+ CompositionContainer = MefUtils.GetSafeCompositionContainer(new ComposablePartCatalog[] { catalog });
+
+ CompositionContainer.ComposeParts(this);
+
+ CompositionContainer.Catalog.Dispose();
+ }
+
+ protected override void DisposeInUi()
+ {
+ base.DisposeInUi();
+
+ CompositionContainer.Dispose();
+ }
+
+ protected Uri GeneratePackUri(string relativePath)
+ {
+ string assemblyName = GetType().Assembly.GetName().Name;
+
+ string uri = string.Format("pack://application:,,,/{0};component/{1}", assemblyName, relativePath);
+
+ return new Uri(uri, UriKind.Absolute);
+ }
+ }
+}
diff --git a/MediaBrowser.Common/Properties/AssemblyInfo.cs b/MediaBrowser.Common/Properties/AssemblyInfo.cs
new file mode 100644
index 000000000..ff70e8db7
--- /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 000000000..f39a1c1d9
--- /dev/null
+++ b/MediaBrowser.Common/Properties/Resources.Designer.cs
@@ -0,0 +1,63 @@
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.17929
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace MediaBrowser.Common.Properties {
+ using System;
+
+
+ /// <summary>
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ /// </summary>
+ // This class was auto-generated by the StronglyTypedResourceBuilder
+ // class via a tool like ResGen or Visual Studio.
+ // To add or remove a member, edit your .ResX file then rerun ResGen
+ // with the /str option, or rebuild your VS project.
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ internal class Resources {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal Resources() {
+ }
+
+ /// <summary>
+ /// Returns the cached ResourceManager instance used by this class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("MediaBrowser.Common.Properties.Resources", typeof(Resources).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ /// <summary>
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Common/Properties/Resources.resx b/MediaBrowser.Common/Properties/Resources.resx
new file mode 100644
index 000000000..7c0911ec1
--- /dev/null
+++ b/MediaBrowser.Common/Properties/Resources.resx
@@ -0,0 +1,121 @@
+<?xml version="1.0" encoding="utf-8"?>
+<root>
+ <!--
+ Microsoft ResX Schema
+
+ Version 2.0
+
+ The primary goals of this format is to allow a simple XML format
+ that is mostly human readable. The generation and parsing of the
+ various data types are done through the TypeConverter classes
+ associated with the data types.
+
+ Example:
+
+ ... ado.net/XML headers & schema ...
+ <resheader name="resmimetype">text/microsoft-resx</resheader>
+ <resheader name="version">2.0</resheader>
+ <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+ <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+ <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+ <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+ <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+ <value>[base64 mime encoded serialized .NET Framework object]</value>
+ </data>
+ <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+ <comment>This is a comment</comment>
+ </data>
+
+ There are any number of "resheader" rows that contain simple
+ name/value pairs.
+
+ Each data row contains a name, and value. The row also contains a
+ type or mimetype. Type corresponds to a .NET class that support
+ text/value conversion through the TypeConverter architecture.
+ Classes that don't support this are serialized and stored with the
+ mimetype set.
+
+ The mimetype is used for serialized objects, and tells the
+ ResXResourceReader how to depersist the object. This is currently not
+ extensible. For a given mimetype the value must be set accordingly:
+
+ Note - application/x-microsoft.net.object.binary.base64 is the format
+ that the ResXResourceWriter will generate, however the reader can
+ read any of the formats listed below.
+
+ mimetype: application/x-microsoft.net.object.binary.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.soap.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.bytearray.base64
+ value : The object must be serialized into a byte array
+ : using a System.ComponentModel.TypeConverter
+ : and then encoded with base64 encoding.
+ -->
+ <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+ <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+ <xsd:element name="root" msdata:IsDataSet="true">
+ <xsd:complexType>
+ <xsd:choice maxOccurs="unbounded">
+ <xsd:element name="metadata">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" />
+ </xsd:sequence>
+ <xsd:attribute name="name" use="required" type="xsd:string" />
+ <xsd:attribute name="type" type="xsd:string" />
+ <xsd:attribute name="mimetype" type="xsd:string" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="assembly">
+ <xsd:complexType>
+ <xsd:attribute name="alias" type="xsd:string" />
+ <xsd:attribute name="name" type="xsd:string" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="data">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+ <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+ <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="resheader">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" />
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:choice>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:schema>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>2.0</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <assembly alias="System.Windows.Forms" name="System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
+</root> \ 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 000000000..1541dabdc
--- /dev/null
+++ b/MediaBrowser.Common/Resources/Images/Icon.ico
Binary files differ
diff --git a/MediaBrowser.Common/Resources/Images/mblogoblack.png b/MediaBrowser.Common/Resources/Images/mblogoblack.png
new file mode 100644
index 000000000..84323fe52
--- /dev/null
+++ b/MediaBrowser.Common/Resources/Images/mblogoblack.png
Binary files differ
diff --git a/MediaBrowser.Common/Resources/Images/mblogowhite.png b/MediaBrowser.Common/Resources/Images/mblogowhite.png
new file mode 100644
index 000000000..a39812e35
--- /dev/null
+++ b/MediaBrowser.Common/Resources/Images/mblogowhite.png
Binary files differ
diff --git a/MediaBrowser.Common/Resources/Images/spinner.gif b/MediaBrowser.Common/Resources/Images/spinner.gif
new file mode 100644
index 000000000..d0bce1542
--- /dev/null
+++ b/MediaBrowser.Common/Resources/Images/spinner.gif
Binary files differ
diff --git a/MediaBrowser.Common/Serialization/JsonSerializer.cs b/MediaBrowser.Common/Serialization/JsonSerializer.cs
new file mode 100644
index 000000000..f5d2abe33
--- /dev/null
+++ b/MediaBrowser.Common/Serialization/JsonSerializer.cs
@@ -0,0 +1,74 @@
+using System;
+using System.IO;
+
+namespace MediaBrowser.Common.Serialization
+{
+ /// <summary>
+ /// Provides a wrapper around third party json serialization.
+ /// </summary>
+ public class JsonSerializer
+ {
+ public static void SerializeToStream<T>(T obj, Stream stream)
+ {
+ Configure();
+
+ ServiceStack.Text.JsonSerializer.SerializeToStream(obj, stream);
+ }
+
+ public static void SerializeToFile<T>(T obj, string file)
+ {
+ Configure();
+
+ using (Stream stream = File.Open(file, FileMode.Create))
+ {
+ ServiceStack.Text.JsonSerializer.SerializeToStream(obj, stream);
+ }
+ }
+
+ public static object DeserializeFromFile(Type type, string file)
+ {
+ Configure();
+
+ using (Stream stream = File.OpenRead(file))
+ {
+ return ServiceStack.Text.JsonSerializer.DeserializeFromStream(type, stream);
+ }
+ }
+
+ public static T DeserializeFromFile<T>(string file)
+ {
+ Configure();
+
+ using (Stream stream = File.OpenRead(file))
+ {
+ return ServiceStack.Text.JsonSerializer.DeserializeFromStream<T>(stream);
+ }
+ }
+
+ public static T DeserializeFromStream<T>(Stream stream)
+ {
+ Configure();
+
+ return ServiceStack.Text.JsonSerializer.DeserializeFromStream<T>(stream);
+ }
+
+ public static object DeserializeFromStream(Stream stream, Type type)
+ {
+ Configure();
+
+ return ServiceStack.Text.JsonSerializer.DeserializeFromStream(type, stream);
+ }
+
+ private static bool _isConfigured;
+ private static void Configure()
+ {
+ if (!_isConfigured)
+ {
+ ServiceStack.Text.JsConfig.DateHandler = ServiceStack.Text.JsonDateHandler.ISO8601;
+ ServiceStack.Text.JsConfig.ExcludeTypeInfo = true;
+ ServiceStack.Text.JsConfig.IncludeNullValues = false;
+ _isConfigured = true;
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Common/Serialization/JsvSerializer.cs b/MediaBrowser.Common/Serialization/JsvSerializer.cs
new file mode 100644
index 000000000..41e5ea800
--- /dev/null
+++ b/MediaBrowser.Common/Serialization/JsvSerializer.cs
@@ -0,0 +1,44 @@
+using System;
+using System.IO;
+
+namespace MediaBrowser.Common.Serialization
+{
+ /// <summary>
+ /// This adds support for ServiceStack's proprietary JSV output format.
+ /// It's a hybrid of Json and Csv but the serializer performs about 25% faster and output runs about 10% smaller
+ /// http://www.servicestack.net/benchmarks/NorthwindDatabaseRowsSerialization.100000-times.2010-08-17.html
+ /// </summary>
+ public static class JsvSerializer
+ {
+ public static void SerializeToStream<T>(T obj, Stream stream)
+ {
+ ServiceStack.Text.TypeSerializer.SerializeToStream(obj, stream);
+ }
+
+ public static T DeserializeFromStream<T>(Stream stream)
+ {
+ return ServiceStack.Text.TypeSerializer.DeserializeFromStream<T>(stream);
+ }
+
+ public static object DeserializeFromStream(Stream stream, Type type)
+ {
+ return ServiceStack.Text.TypeSerializer.DeserializeFromStream(type, stream);
+ }
+
+ public static void SerializeToFile<T>(T obj, string file)
+ {
+ using (Stream stream = File.Open(file, FileMode.Create))
+ {
+ SerializeToStream(obj, stream);
+ }
+ }
+
+ public static T DeserializeFromFile<T>(string file)
+ {
+ using (Stream stream = File.OpenRead(file))
+ {
+ return DeserializeFromStream<T>(stream);
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Common/Serialization/ProtobufSerializer.cs b/MediaBrowser.Common/Serialization/ProtobufSerializer.cs
new file mode 100644
index 000000000..1c79a272d
--- /dev/null
+++ b/MediaBrowser.Common/Serialization/ProtobufSerializer.cs
@@ -0,0 +1,53 @@
+using System;
+using System.IO;
+
+namespace MediaBrowser.Common.Serialization
+{
+ /// <summary>
+ /// Protocol buffers is google's binary serialization format. This is a .NET implementation of it.
+ /// You have to tag your classes with some annoying attributes, but in return you get the fastest serialization around with the smallest possible output.
+ /// </summary>
+ public static class ProtobufSerializer
+ {
+ /// <summary>
+ /// This is an auto-generated Protobuf Serialization assembly for best performance.
+ /// It is created during the Model project's post-build event.
+ /// This means that this class can currently only handle types within the Model project.
+ /// If we need to, we can always add a param indicating whether or not the model serializer should be used.
+ /// </summary>
+ private static readonly ProtobufModelSerializer ProtobufModelSerializer = new ProtobufModelSerializer();
+
+ public static void SerializeToStream<T>(T obj, Stream stream)
+ {
+ ProtobufModelSerializer.Serialize(stream, obj);
+ }
+
+ public static T DeserializeFromStream<T>(Stream stream)
+ where T : class
+ {
+ return ProtobufModelSerializer.Deserialize(stream, null, typeof(T)) as T;
+ }
+
+ public static object DeserializeFromStream(Stream stream, Type type)
+ {
+ return ProtobufModelSerializer.Deserialize(stream, null, type);
+ }
+
+ public static void SerializeToFile<T>(T obj, string file)
+ {
+ using (Stream stream = File.Open(file, FileMode.Create))
+ {
+ SerializeToStream(obj, stream);
+ }
+ }
+
+ public static T DeserializeFromFile<T>(string file)
+ where T : class
+ {
+ using (Stream stream = File.OpenRead(file))
+ {
+ return DeserializeFromStream<T>(stream);
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Common/Serialization/XmlSerializer.cs b/MediaBrowser.Common/Serialization/XmlSerializer.cs
new file mode 100644
index 000000000..11ef17c3d
--- /dev/null
+++ b/MediaBrowser.Common/Serialization/XmlSerializer.cs
@@ -0,0 +1,58 @@
+using System;
+using System.IO;
+
+namespace MediaBrowser.Common.Serialization
+{
+ /// <summary>
+ /// Provides a wrapper around third party xml serialization.
+ /// </summary>
+ public class XmlSerializer
+ {
+ public static void SerializeToStream<T>(T obj, Stream stream)
+ {
+ ServiceStack.Text.XmlSerializer.SerializeToStream(obj, stream);
+ }
+
+ public static T DeserializeFromStream<T>(Stream stream)
+ {
+ return ServiceStack.Text.XmlSerializer.DeserializeFromStream<T>(stream);
+ }
+
+ public static object DeserializeFromStream(Type type, Stream stream)
+ {
+ return ServiceStack.Text.XmlSerializer.DeserializeFromStream(type, stream);
+ }
+
+ public static void SerializeToFile<T>(T obj, string file)
+ {
+ using (var stream = new FileStream(file, FileMode.Create))
+ {
+ SerializeToStream(obj, stream);
+ }
+ }
+
+ public static T DeserializeFromFile<T>(string file)
+ {
+ using (Stream stream = File.OpenRead(file))
+ {
+ return DeserializeFromStream<T>(stream);
+ }
+ }
+
+ public static void SerializeToFile(object obj, string file)
+ {
+ using (var stream = new FileStream(file, FileMode.Create))
+ {
+ ServiceStack.Text.XmlSerializer.SerializeToStream(obj, stream);
+ }
+ }
+
+ public static object DeserializeFromFile(Type type, string file)
+ {
+ using (Stream stream = File.OpenRead(file))
+ {
+ return DeserializeFromStream(type, stream);
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Common/UI/BaseApplication.cs b/MediaBrowser.Common/UI/BaseApplication.cs
new file mode 100644
index 000000000..c3792c714
--- /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
+{
+ /// <summary>
+ /// Serves as a base Application class for both the UI and Server apps.
+ /// </summary>
+ public abstract class BaseApplication : Application, INotifyPropertyChanged, ISingleInstanceApp
+ {
+ private IKernel Kernel { get; set; }
+
+ protected abstract IKernel InstantiateKernel();
+ protected abstract Window InstantiateMainWindow();
+
+ public event PropertyChangedEventHandler PropertyChanged;
+
+ public void OnPropertyChanged(String info)
+ {
+ if (PropertyChanged != null)
+ {
+ PropertyChanged(this, new PropertyChangedEventArgs(info));
+ }
+ }
+
+ protected override void OnStartup(StartupEventArgs e)
+ {
+ // Without this the app will shutdown after the splash screen closes
+ ShutdownMode = ShutdownMode.OnExplicitShutdown;
+
+ LoadKernel();
+ }
+
+ private async void LoadKernel()
+ {
+ Kernel = InstantiateKernel();
+
+ var progress = new Progress<TaskProgress>();
+
+ var splash = new Splash(progress);
+
+ splash.Show();
+
+ try
+ {
+ DateTime now = DateTime.UtcNow;
+
+ await Kernel.Init(progress);
+
+ Logger.LogInfo("Kernel.Init completed in {0} seconds.", (DateTime.UtcNow - now).TotalSeconds);
+ splash.Close();
+
+ ShutdownMode = System.Windows.ShutdownMode.OnLastWindowClose;
+
+ OnKernelLoaded();
+
+ InstantiateMainWindow().Show();
+ }
+ catch (Exception ex)
+ {
+ Logger.LogException(ex);
+
+ MessageBox.Show("There was an error launching Media Browser: " + ex.Message);
+ splash.Close();
+
+ // Shutdown the app with an error code
+ Shutdown(1);
+ }
+ }
+
+ protected virtual void OnKernelLoaded()
+ {
+ }
+
+ protected override void OnExit(ExitEventArgs e)
+ {
+ base.OnExit(e);
+
+ Kernel.Dispose();
+ }
+
+ public bool SignalExternalCommandLineArgs(IList<string> args)
+ {
+ OnSecondInstanceLaunched(args);
+
+ return true;
+ }
+
+ protected virtual void OnSecondInstanceLaunched(IList<string> args)
+ {
+ if (this.MainWindow.WindowState == WindowState.Minimized)
+ {
+ this.MainWindow.WindowState = WindowState.Maximized;
+ }
+ }
+
+ public static void RunApplication<TApplicationType>(string uniqueKey)
+ where TApplicationType : BaseApplication, IApplication, new()
+ {
+ if (SingleInstance<TApplicationType>.InitializeAsFirstInstance(uniqueKey))
+ {
+ var application = new TApplicationType();
+ application.InitializeComponent();
+
+ application.Run();
+
+ // Allow single instance code to perform cleanup operations
+ SingleInstance<TApplicationType>.Cleanup();
+ }
+ }
+ }
+
+ public interface IApplication
+ {
+ void InitializeComponent();
+ }
+}
diff --git a/MediaBrowser.Common/UI/SingleInstance.cs b/MediaBrowser.Common/UI/SingleInstance.cs
new file mode 100644
index 000000000..3fc85a74e
--- /dev/null
+++ b/MediaBrowser.Common/UI/SingleInstance.cs
@@ -0,0 +1,484 @@
+//-----------------------------------------------------------------------
+// <copyright file="SingleInstance.cs" company="Microsoft">
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// </copyright>
+// <summary>
+// This class checks to make sure that only one instance of
+// this application is running at a time.
+// </summary>
+//-----------------------------------------------------------------------
+
+namespace Microsoft.Shell
+{
+ using System;
+ using System.Collections;
+ using System.Collections.Generic;
+ using System.ComponentModel;
+ using System.IO;
+ using System.Runtime.InteropServices;
+ using System.Runtime.Remoting;
+ using System.Runtime.Remoting.Channels;
+ using System.Runtime.Remoting.Channels.Ipc;
+ using System.Runtime.Serialization.Formatters;
+ using System.Security;
+ using System.Threading;
+ using System.Windows;
+ using System.Windows.Threading;
+
+ internal enum WM
+ {
+ NULL = 0x0000,
+ CREATE = 0x0001,
+ DESTROY = 0x0002,
+ MOVE = 0x0003,
+ SIZE = 0x0005,
+ ACTIVATE = 0x0006,
+ SETFOCUS = 0x0007,
+ KILLFOCUS = 0x0008,
+ ENABLE = 0x000A,
+ SETREDRAW = 0x000B,
+ SETTEXT = 0x000C,
+ GETTEXT = 0x000D,
+ GETTEXTLENGTH = 0x000E,
+ PAINT = 0x000F,
+ CLOSE = 0x0010,
+ QUERYENDSESSION = 0x0011,
+ QUIT = 0x0012,
+ QUERYOPEN = 0x0013,
+ ERASEBKGND = 0x0014,
+ SYSCOLORCHANGE = 0x0015,
+ SHOWWINDOW = 0x0018,
+ ACTIVATEAPP = 0x001C,
+ SETCURSOR = 0x0020,
+ MOUSEACTIVATE = 0x0021,
+ CHILDACTIVATE = 0x0022,
+ QUEUESYNC = 0x0023,
+ GETMINMAXINFO = 0x0024,
+
+ WINDOWPOSCHANGING = 0x0046,
+ WINDOWPOSCHANGED = 0x0047,
+
+ CONTEXTMENU = 0x007B,
+ STYLECHANGING = 0x007C,
+ STYLECHANGED = 0x007D,
+ DISPLAYCHANGE = 0x007E,
+ GETICON = 0x007F,
+ SETICON = 0x0080,
+ NCCREATE = 0x0081,
+ NCDESTROY = 0x0082,
+ NCCALCSIZE = 0x0083,
+ NCHITTEST = 0x0084,
+ NCPAINT = 0x0085,
+ NCACTIVATE = 0x0086,
+ GETDLGCODE = 0x0087,
+ SYNCPAINT = 0x0088,
+ NCMOUSEMOVE = 0x00A0,
+ NCLBUTTONDOWN = 0x00A1,
+ NCLBUTTONUP = 0x00A2,
+ NCLBUTTONDBLCLK = 0x00A3,
+ NCRBUTTONDOWN = 0x00A4,
+ NCRBUTTONUP = 0x00A5,
+ NCRBUTTONDBLCLK = 0x00A6,
+ NCMBUTTONDOWN = 0x00A7,
+ NCMBUTTONUP = 0x00A8,
+ NCMBUTTONDBLCLK = 0x00A9,
+
+ SYSKEYDOWN = 0x0104,
+ SYSKEYUP = 0x0105,
+ SYSCHAR = 0x0106,
+ SYSDEADCHAR = 0x0107,
+ COMMAND = 0x0111,
+ SYSCOMMAND = 0x0112,
+
+ MOUSEMOVE = 0x0200,
+ LBUTTONDOWN = 0x0201,
+ LBUTTONUP = 0x0202,
+ LBUTTONDBLCLK = 0x0203,
+ RBUTTONDOWN = 0x0204,
+ RBUTTONUP = 0x0205,
+ RBUTTONDBLCLK = 0x0206,
+ MBUTTONDOWN = 0x0207,
+ MBUTTONUP = 0x0208,
+ MBUTTONDBLCLK = 0x0209,
+ MOUSEWHEEL = 0x020A,
+ XBUTTONDOWN = 0x020B,
+ XBUTTONUP = 0x020C,
+ XBUTTONDBLCLK = 0x020D,
+ MOUSEHWHEEL = 0x020E,
+
+
+ CAPTURECHANGED = 0x0215,
+
+ ENTERSIZEMOVE = 0x0231,
+ EXITSIZEMOVE = 0x0232,
+
+ IME_SETCONTEXT = 0x0281,
+ IME_NOTIFY = 0x0282,
+ IME_CONTROL = 0x0283,
+ IME_COMPOSITIONFULL = 0x0284,
+ IME_SELECT = 0x0285,
+ IME_CHAR = 0x0286,
+ IME_REQUEST = 0x0288,
+ IME_KEYDOWN = 0x0290,
+ IME_KEYUP = 0x0291,
+
+ NCMOUSELEAVE = 0x02A2,
+
+ DWMCOMPOSITIONCHANGED = 0x031E,
+ DWMNCRENDERINGCHANGED = 0x031F,
+ DWMCOLORIZATIONCOLORCHANGED = 0x0320,
+ DWMWINDOWMAXIMIZEDCHANGE = 0x0321,
+
+ #region Windows 7
+ DWMSENDICONICTHUMBNAIL = 0x0323,
+ DWMSENDICONICLIVEPREVIEWBITMAP = 0x0326,
+ #endregion
+
+ USER = 0x0400,
+
+ // This is the hard-coded message value used by WinForms for Shell_NotifyIcon.
+ // It's relatively safe to reuse.
+ TRAYMOUSEMESSAGE = 0x800, //WM_USER + 1024
+ APP = 0x8000,
+ }
+
+ [SuppressUnmanagedCodeSecurity]
+ internal static class NativeMethods
+ {
+ /// <summary>
+ /// Delegate declaration that matches WndProc signatures.
+ /// </summary>
+ public delegate IntPtr MessageHandler(WM uMsg, IntPtr wParam, IntPtr lParam, out bool handled);
+
+ [DllImport("shell32.dll", EntryPoint = "CommandLineToArgvW", CharSet = CharSet.Unicode)]
+ private static extern IntPtr _CommandLineToArgvW([MarshalAs(UnmanagedType.LPWStr)] string cmdLine, out int numArgs);
+
+
+ [DllImport("kernel32.dll", EntryPoint = "LocalFree", SetLastError = true)]
+ private static extern IntPtr _LocalFree(IntPtr hMem);
+
+
+ public static string[] CommandLineToArgvW(string cmdLine)
+ {
+ IntPtr argv = IntPtr.Zero;
+ try
+ {
+ int numArgs = 0;
+
+ argv = _CommandLineToArgvW(cmdLine, out numArgs);
+ if (argv == IntPtr.Zero)
+ {
+ throw new Win32Exception();
+ }
+ var result = new string[numArgs];
+
+ for (int i = 0; i < numArgs; i++)
+ {
+ IntPtr currArg = Marshal.ReadIntPtr(argv, i * Marshal.SizeOf(typeof(IntPtr)));
+ result[i] = Marshal.PtrToStringUni(currArg);
+ }
+
+ return result;
+ }
+ finally
+ {
+
+ _LocalFree(argv);
+ // Otherwise LocalFree failed.
+ // Assert.AreEqual(IntPtr.Zero, p);
+ }
+ }
+
+ }
+
+ public interface ISingleInstanceApp
+ {
+ bool SignalExternalCommandLineArgs(IList<string> args);
+ }
+
+ /// <summary>
+ /// This class checks to make sure that only one instance of
+ /// this application is running at a time.
+ /// </summary>
+ /// <remarks>
+ /// Note: this class should be used with some caution, because it does no
+ /// security checking. For example, if one instance of an app that uses this class
+ /// is running as Administrator, any other instance, even if it is not
+ /// running as Administrator, can activate it with command line arguments.
+ /// For most apps, this will not be much of an issue.
+ /// </remarks>
+ public static class SingleInstance<TApplication>
+ where TApplication : Application, ISingleInstanceApp
+ {
+ #region Private Fields
+
+ /// <summary>
+ /// String delimiter used in channel names.
+ /// </summary>
+ private const string Delimiter = ":";
+
+ /// <summary>
+ /// Suffix to the channel name.
+ /// </summary>
+ private const string ChannelNameSuffix = "SingeInstanceIPCChannel";
+
+ /// <summary>
+ /// Remote service name.
+ /// </summary>
+ private const string RemoteServiceName = "SingleInstanceApplicationService";
+
+ /// <summary>
+ /// IPC protocol used (string).
+ /// </summary>
+ private const string IpcProtocol = "ipc://";
+
+ /// <summary>
+ /// Application mutex.
+ /// </summary>
+ private static Mutex singleInstanceMutex;
+
+ /// <summary>
+ /// IPC channel for communications.
+ /// </summary>
+ private static IpcServerChannel channel;
+
+ /// <summary>
+ /// List of command line arguments for the application.
+ /// </summary>
+ private static IList<string> commandLineArgs;
+
+ #endregion
+
+ #region Public Properties
+
+ /// <summary>
+ /// Gets list of command line arguments for the application.
+ /// </summary>
+ public static IList<string> CommandLineArgs
+ {
+ get { return commandLineArgs; }
+ }
+
+ #endregion
+
+ #region Public Methods
+
+ /// <summary>
+ /// Checks if the instance of the application attempting to start is the first instance.
+ /// If not, activates the first instance.
+ /// </summary>
+ /// <returns>True if this is the first instance of the application.</returns>
+ public static bool InitializeAsFirstInstance(string uniqueName)
+ {
+ commandLineArgs = GetCommandLineArgs(uniqueName);
+
+ // Build unique application Id and the IPC channel name.
+ string applicationIdentifier = uniqueName + Environment.UserName;
+
+ string channelName = String.Concat(applicationIdentifier, Delimiter, ChannelNameSuffix);
+
+ // Create mutex based on unique application Id to check if this is the first instance of the application.
+ bool firstInstance;
+ singleInstanceMutex = new Mutex(true, applicationIdentifier, out firstInstance);
+ if (firstInstance)
+ {
+ CreateRemoteService(channelName);
+ }
+ else
+ {
+ SignalFirstInstance(channelName, commandLineArgs);
+ }
+
+ return firstInstance;
+ }
+
+ /// <summary>
+ /// Cleans up single-instance code, clearing shared resources, mutexes, etc.
+ /// </summary>
+ public static void Cleanup()
+ {
+ if (singleInstanceMutex != null)
+ {
+ singleInstanceMutex.Close();
+ singleInstanceMutex = null;
+ }
+
+ if (channel != null)
+ {
+ ChannelServices.UnregisterChannel(channel);
+ channel = null;
+ }
+ }
+
+ #endregion
+
+ #region Private Methods
+
+ /// <summary>
+ /// Gets command line args - for ClickOnce deployed applications, command line args may not be passed directly, they have to be retrieved.
+ /// </summary>
+ /// <returns>List of command line arg strings.</returns>
+ private static IList<string> GetCommandLineArgs(string uniqueApplicationName)
+ {
+ string[] args = null;
+ if (AppDomain.CurrentDomain.ActivationContext == null)
+ {
+ // The application was not clickonce deployed, get args from standard API's
+ args = Environment.GetCommandLineArgs();
+ }
+ else
+ {
+ // The application was clickonce deployed
+ // Clickonce deployed apps cannot recieve traditional commandline arguments
+ // As a workaround commandline arguments can be written to a shared location before
+ // the app is launched and the app can obtain its commandline arguments from the
+ // shared location
+ string appFolderPath = Path.Combine(
+ Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), uniqueApplicationName);
+
+ string cmdLinePath = Path.Combine(appFolderPath, "cmdline.txt");
+ if (File.Exists(cmdLinePath))
+ {
+ try
+ {
+ using (TextReader reader = new StreamReader(cmdLinePath, System.Text.Encoding.Unicode))
+ {
+ args = NativeMethods.CommandLineToArgvW(reader.ReadToEnd());
+ }
+
+ File.Delete(cmdLinePath);
+ }
+ catch (IOException)
+ {
+ }
+ }
+ }
+
+ if (args == null)
+ {
+ args = new string[] { };
+ }
+
+ return new List<string>(args);
+ }
+
+ /// <summary>
+ /// Creates a remote service for communication.
+ /// </summary>
+ /// <param name="channelName">Application's IPC channel name.</param>
+ private static void CreateRemoteService(string channelName)
+ {
+ var serverProvider = new BinaryServerFormatterSinkProvider { };
+ serverProvider.TypeFilterLevel = TypeFilterLevel.Full;
+ IDictionary props = new Dictionary<string, string>();
+
+ props["name"] = channelName;
+ props["portName"] = channelName;
+ props["exclusiveAddressUse"] = "false";
+
+ // Create the IPC Server channel with the channel properties
+ channel = new IpcServerChannel(props, serverProvider);
+
+ // Register the channel with the channel services
+ ChannelServices.RegisterChannel(channel, true);
+
+ // Expose the remote service with the REMOTE_SERVICE_NAME
+ var remoteService = new IPCRemoteService();
+ RemotingServices.Marshal(remoteService, RemoteServiceName);
+ }
+
+ /// <summary>
+ /// Creates a client channel and obtains a reference to the remoting service exposed by the server -
+ /// in this case, the remoting service exposed by the first instance. Calls a function of the remoting service
+ /// class to pass on command line arguments from the second instance to the first and cause it to activate itself.
+ /// </summary>
+ /// <param name="channelName">Application's IPC channel name.</param>
+ /// <param name="args">
+ /// Command line arguments for the second instance, passed to the first instance to take appropriate action.
+ /// </param>
+ private static void SignalFirstInstance(string channelName, IList<string> args)
+ {
+ var secondInstanceChannel = new IpcClientChannel();
+ ChannelServices.RegisterChannel(secondInstanceChannel, true);
+
+ string remotingServiceUrl = IpcProtocol + channelName + "/" + RemoteServiceName;
+
+ // Obtain a reference to the remoting service exposed by the server i.e the first instance of the application
+ var firstInstanceRemoteServiceReference = (IPCRemoteService)RemotingServices.Connect(typeof(IPCRemoteService), remotingServiceUrl);
+
+ // Check that the remote service exists, in some cases the first instance may not yet have created one, in which case
+ // the second instance should just exit
+ if (firstInstanceRemoteServiceReference != null)
+ {
+ // Invoke a method of the remote service exposed by the first instance passing on the command line
+ // arguments and causing the first instance to activate itself
+ firstInstanceRemoteServiceReference.InvokeFirstInstance(args);
+ }
+ }
+
+ /// <summary>
+ /// Callback for activating first instance of the application.
+ /// </summary>
+ /// <param name="arg">Callback argument.</param>
+ /// <returns>Always null.</returns>
+ private static object ActivateFirstInstanceCallback(object arg)
+ {
+ // Get command line args to be passed to first instance
+ var args = arg as IList<string>;
+ ActivateFirstInstance(args);
+ return null;
+ }
+
+ /// <summary>
+ /// Activates the first instance of the application with arguments from a second instance.
+ /// </summary>
+ /// <param name="args">List of arguments to supply the first instance of the application.</param>
+ private static void ActivateFirstInstance(IList<string> args)
+ {
+ // Set main window state and process command line args
+ if (Application.Current == null)
+ {
+ return;
+ }
+
+ ((TApplication)Application.Current).SignalExternalCommandLineArgs(args);
+ }
+
+ #endregion
+
+ #region Private Classes
+
+ /// <summary>
+ /// Remoting service class which is exposed by the server i.e the first instance and called by the second instance
+ /// to pass on the command line arguments to the first instance and cause it to activate itself.
+ /// </summary>
+ private class IPCRemoteService : MarshalByRefObject
+ {
+ /// <summary>
+ /// Activates the first instance of the application.
+ /// </summary>
+ /// <param name="args">List of arguments to pass to the first instance.</param>
+ public void InvokeFirstInstance(IList<string> args)
+ {
+ if (Application.Current != null)
+ {
+ // Do an asynchronous call to ActivateFirstInstance function
+ Application.Current.Dispatcher.BeginInvoke(
+ DispatcherPriority.Normal, new DispatcherOperationCallback(SingleInstance<TApplication>.ActivateFirstInstanceCallback), args);
+ }
+ }
+
+ /// <summary>
+ /// Remoting Object's ease expires after every 5 minutes by default. We need to override the InitializeLifetimeService class
+ /// to ensure that lease never expires.
+ /// </summary>
+ /// <returns>Always null.</returns>
+ public override object InitializeLifetimeService()
+ {
+ return null;
+ }
+ }
+
+ #endregion
+ }
+}
diff --git a/MediaBrowser.Common/UI/Splash.xaml b/MediaBrowser.Common/UI/Splash.xaml
new file mode 100644
index 000000000..7781841b2
--- /dev/null
+++ b/MediaBrowser.Common/UI/Splash.xaml
@@ -0,0 +1,33 @@
+<Controls:MetroWindow x:Class="MediaBrowser.Common.UI.Splash"
+ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ xmlns:Controls="clr-namespace:MahApps.Metro.Controls;assembly=MahApps.Metro"
+ Title="MediaBrowser"
+ Height="230"
+ Width="520"
+ ShowInTaskbar="True"
+ ResizeMode="NoResize"
+ WindowStartupLocation="CenterScreen"
+ WindowState="Normal"
+ FontSize="14">
+ <Window.Resources>
+ <ResourceDictionary>
+ <Style TargetType="{x:Type Controls:WindowCommands}">
+ <Setter Property="Visibility" Value="Hidden" />
+ </Style>
+ </ResourceDictionary>
+ </Window.Resources>
+ <Window.Background>
+ <RadialGradientBrush RadiusX=".75" RadiusY=".75">
+ <GradientStop Color="White" Offset="0.0"/>
+ <GradientStop Color="WhiteSmoke" Offset="0.65"/>
+ <GradientStop Color="#cfcfcf" Offset="1.0"/>
+ </RadialGradientBrush>
+ </Window.Background>
+ <Grid Name="splashGrid">
+ <Image x:Name="imgLogo" HorizontalAlignment="Left" VerticalAlignment="Top" Stretch="Uniform" Grid.Row="0" Margin="10 10 10 10" Source="../Resources/Images/mblogoblack.png"/>
+ <StackPanel Margin="0,130,10,0" VerticalAlignment="Center" HorizontalAlignment="Center" Grid.Row="2" Orientation="Horizontal">
+ <TextBlock Name="lblProgress" FontSize="18" Foreground="Black" Text="Label"></TextBlock>
+ </StackPanel>
+ </Grid>
+</Controls:MetroWindow>
diff --git a/MediaBrowser.Common/UI/Splash.xaml.cs b/MediaBrowser.Common/UI/Splash.xaml.cs
new file mode 100644
index 000000000..b9764c05f
--- /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
+{
+ /// <summary>
+ /// Interaction logic for Splash.xaml
+ /// </summary>
+ public partial class Splash : MetroWindow
+ {
+ public Splash(Progress<TaskProgress> progress)
+ {
+ InitializeComponent();
+
+ progress.ProgressChanged += ProgressChanged;
+ Loaded+=SplashLoaded;
+ }
+
+ void ProgressChanged(object sender, TaskProgress e)
+ {
+ lblProgress.Text = e.Description + "...";
+ }
+
+ private void SplashLoaded(object sender, RoutedEventArgs e)
+ {
+ // Setting this in markup throws an exception at runtime
+ ShowTitleBar = false;
+ }
+ }
+}
diff --git a/MediaBrowser.Common/app.config b/MediaBrowser.Common/app.config
new file mode 100644
index 000000000..037800f7f
--- /dev/null
+++ b/MediaBrowser.Common/app.config
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<configuration>
+ <runtime>
+ <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
+ <dependentAssembly>
+ <assemblyIdentity name="System.Reactive.Core" publicKeyToken="f300afd708cefcd3" culture="neutral" />
+ <bindingRedirect oldVersion="0.0.0.0-2.0.20823.0" newVersion="2.0.20823.0" />
+ </dependentAssembly>
+ <dependentAssembly>
+ <assemblyIdentity name="System.Reactive.Interfaces" publicKeyToken="f300afd708cefcd3" culture="neutral" />
+ <bindingRedirect oldVersion="0.0.0.0-2.0.20823.0" newVersion="2.0.20823.0" />
+ </dependentAssembly>
+ </assemblyBinding>
+ </runtime>
+</configuration> \ No newline at end of file
diff --git a/MediaBrowser.Common/packages.config b/MediaBrowser.Common/packages.config
new file mode 100644
index 000000000..d3043e27a
--- /dev/null
+++ b/MediaBrowser.Common/packages.config
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<packages>
+ <package id="MahApps.Metro" version="0.9.0.0" targetFramework="net45" />
+ <package id="Rx-Core" version="2.0.20823" targetFramework="net45" />
+ <package id="Rx-Interfaces" version="2.0.20823" targetFramework="net45" />
+ <package id="Rx-Linq" version="2.0.20823" targetFramework="net45" />
+ <package id="ServiceStack.Text" version="3.9.9" targetFramework="net45" />
+</packages> \ 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 000000000..8e2f829b9
--- /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
+ {
+ /// <summary>
+ /// Resizes a set of dimensions
+ /// </summary>
+ public static Size Resize(int currentWidth, int currentHeight, int? width, int? height, int? maxWidth, int? maxHeight)
+ {
+ return Resize(new Size(currentWidth, currentHeight), width, height, maxWidth, maxHeight);
+ }
+
+ /// <summary>
+ /// Resizes a set of dimensions
+ /// </summary>
+ /// <param name="size">The original size object</param>
+ /// <param name="width">A new fixed width, if desired</param>
+ /// <param name="height">A new fixed neight, if desired</param>
+ /// <param name="maxWidth">A max fixed width, if desired</param>
+ /// <param name="maxHeight">A max fixed height, if desired</param>
+ /// <returns>A new size object</returns>
+ public static Size Resize(Size size, int? width, int? height, int? maxWidth, int? maxHeight)
+ {
+ decimal newWidth = size.Width;
+ decimal newHeight = size.Height;
+
+ if (width.HasValue && height.HasValue)
+ {
+ newWidth = width.Value;
+ newHeight = height.Value;
+ }
+
+ else if (height.HasValue)
+ {
+ newWidth = GetNewWidth(newHeight, newWidth, height.Value);
+ newHeight = height.Value;
+ }
+
+ else if (width.HasValue)
+ {
+ newHeight = GetNewHeight(newHeight, newWidth, width.Value);
+ newWidth = width.Value;
+ }
+
+ if (maxHeight.HasValue && maxHeight < newHeight)
+ {
+ newWidth = GetNewWidth(newHeight, newWidth, maxHeight.Value);
+ newHeight = maxHeight.Value;
+ }
+
+ if (maxWidth.HasValue && maxWidth < newWidth)
+ {
+ newHeight = GetNewHeight(newHeight, newWidth, maxWidth.Value);
+ newWidth = maxWidth.Value;
+ }
+
+ return new Size(Convert.ToInt32(newWidth), Convert.ToInt32(newHeight));
+ }
+
+ private static decimal GetNewWidth(decimal currentHeight, decimal currentWidth, int newHeight)
+ {
+ decimal scaleFactor = newHeight;
+ scaleFactor /= currentHeight;
+ scaleFactor *= currentWidth;
+
+ return scaleFactor;
+ }
+
+ private static decimal GetNewHeight(decimal currentHeight, decimal currentWidth, int newWidth)
+ {
+ decimal scaleFactor = newWidth;
+ scaleFactor /= currentWidth;
+ scaleFactor *= currentHeight;
+
+ return scaleFactor;
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Drawing/ImageProcessor.cs b/MediaBrowser.Controller/Drawing/ImageProcessor.cs
new file mode 100644
index 000000000..29e40d17d
--- /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
+ {
+ /// <summary>
+ /// Processes an image by resizing to target dimensions
+ /// </summary>
+ /// <param name="entity">The entity that owns the image</param>
+ /// <param name="imageType">The image type</param>
+ /// <param name="imageIndex">The image index (currently only used with backdrops)</param>
+ /// <param name="toStream">The stream to save the new image to</param>
+ /// <param name="width">Use if a fixed width is required. Aspect ratio will be preserved.</param>
+ /// <param name="height">Use if a fixed height is required. Aspect ratio will be preserved.</param>
+ /// <param name="maxWidth">Use if a max width is required. Aspect ratio will be preserved.</param>
+ /// <param name="maxHeight">Use if a max height is required. Aspect ratio will be preserved.</param>
+ /// <param name="quality">Quality level, from 0-100. Currently only applies to JPG. The default value should suffice.</param>
+ public static void ProcessImage(BaseEntity entity, ImageType imageType, int imageIndex, Stream toStream, int? width, int? height, int? maxWidth, int? maxHeight, int? quality)
+ {
+ Image originalImage = Image.FromFile(GetImagePath(entity, imageType, imageIndex));
+
+ // Determine the output size based on incoming parameters
+ Size newSize = DrawingUtils.Resize(originalImage.Size, width, height, maxWidth, maxHeight);
+
+ Bitmap thumbnail;
+
+ // Graphics.FromImage will throw an exception if the PixelFormat is Indexed, so we need to handle that here
+ if (originalImage.PixelFormat.HasFlag(PixelFormat.Indexed))
+ {
+ thumbnail = new Bitmap(originalImage, newSize.Width, newSize.Height);
+ }
+ else
+ {
+ thumbnail = new Bitmap(newSize.Width, newSize.Height, originalImage.PixelFormat);
+ }
+
+ thumbnail.MakeTransparent();
+
+ // Preserve the original resolution
+ thumbnail.SetResolution(originalImage.HorizontalResolution, originalImage.VerticalResolution);
+
+ Graphics thumbnailGraph = Graphics.FromImage(thumbnail);
+
+ thumbnailGraph.CompositingQuality = CompositingQuality.HighQuality;
+ thumbnailGraph.SmoothingMode = SmoothingMode.HighQuality;
+ thumbnailGraph.InterpolationMode = InterpolationMode.HighQualityBicubic;
+ thumbnailGraph.PixelOffsetMode = PixelOffsetMode.HighQuality;
+ thumbnailGraph.CompositingMode = CompositingMode.SourceOver;
+
+ thumbnailGraph.DrawImage(originalImage, 0, 0, newSize.Width, newSize.Height);
+
+ ImageFormat outputFormat = originalImage.RawFormat;
+
+ // Write to the output stream
+ SaveImage(outputFormat, thumbnail, toStream, quality);
+
+ thumbnailGraph.Dispose();
+ thumbnail.Dispose();
+ originalImage.Dispose();
+ }
+
+ public static string GetImagePath(BaseEntity entity, ImageType imageType, int imageIndex)
+ {
+ var item = entity as BaseItem;
+
+ if (item != null)
+ {
+ if (imageType == ImageType.Logo)
+ {
+ return item.LogoImagePath;
+ }
+ if (imageType == ImageType.Backdrop)
+ {
+ return item.BackdropImagePaths.ElementAt(imageIndex);
+ }
+ if (imageType == ImageType.Banner)
+ {
+ return item.BannerImagePath;
+ }
+ if (imageType == ImageType.Art)
+ {
+ return item.ArtImagePath;
+ }
+ if (imageType == ImageType.Thumbnail)
+ {
+ return item.ThumbnailImagePath;
+ }
+ }
+
+ return entity.PrimaryImagePath;
+ }
+
+ public static void SaveImage(ImageFormat outputFormat, Image newImage, Stream toStream, int? quality)
+ {
+ // Use special save methods for jpeg and png that will result in a much higher quality image
+ // All other formats use the generic Image.Save
+ if (ImageFormat.Jpeg.Equals(outputFormat))
+ {
+ SaveJpeg(newImage, toStream, quality);
+ }
+ else if (ImageFormat.Png.Equals(outputFormat))
+ {
+ newImage.Save(toStream, ImageFormat.Png);
+ }
+ else
+ {
+ newImage.Save(toStream, outputFormat);
+ }
+ }
+
+ public static void SaveJpeg(Image image, Stream target, int? quality)
+ {
+ if (!quality.HasValue)
+ {
+ quality = 90;
+ }
+
+ using (var encoderParameters = new EncoderParameters(1))
+ {
+ encoderParameters.Param[0] = new EncoderParameter(Encoder.Quality, quality.Value);
+ image.Save(target, GetImageCodecInfo("image/jpeg"), encoderParameters);
+ }
+ }
+
+ public static ImageCodecInfo GetImageCodecInfo(string mimeType)
+ {
+ ImageCodecInfo[] info = ImageCodecInfo.GetImageEncoders();
+
+ for (int i = 0; i < info.Length; i++)
+ {
+ ImageCodecInfo ici = info[i];
+ if (ici.MimeType.Equals(mimeType, StringComparison.OrdinalIgnoreCase))
+ {
+ return ici;
+ }
+ }
+ return info[1];
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Entities/Audio.cs b/MediaBrowser.Controller/Entities/Audio.cs
new file mode 100644
index 000000000..61e901dd2
--- /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 000000000..5b4a360c1
--- /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
+{
+ /// <summary>
+ /// Provides a base entity for all of our types
+ /// </summary>
+ public abstract class BaseEntity
+ {
+ public string Name { get; set; }
+
+ public Guid Id { get; set; }
+
+ public string Path { get; set; }
+
+ public Folder Parent { get; set; }
+
+ public string PrimaryImagePath { get; set; }
+
+ public DateTime DateCreated { get; set; }
+
+ public DateTime DateModified { get; set; }
+
+ public override string ToString()
+ {
+ return Name;
+ }
+ protected Dictionary<Guid, BaseProviderInfo> _providerData;
+ /// <summary>
+ /// Holds persistent data for providers like last refresh date.
+ /// Providers can use this to determine if they need to refresh.
+ /// The BaseProviderInfo class can be extended to hold anything a provider may need.
+ ///
+ /// Keyed by a unique provider ID.
+ /// </summary>
+ public Dictionary<Guid, BaseProviderInfo> ProviderData
+ {
+ get
+ {
+ if (_providerData == null) _providerData = new Dictionary<Guid, BaseProviderInfo>();
+ return _providerData;
+ }
+ set
+ {
+ _providerData = value;
+ }
+ }
+
+ protected ItemResolveEventArgs _resolveArgs;
+ /// <summary>
+ /// We attach these to the item so that we only ever have to hit the file system once
+ /// (this includes the children of the containing folder)
+ /// Use ResolveArgs.FileSystemChildren to check for the existence of files instead of File.Exists
+ /// </summary>
+ public ItemResolveEventArgs ResolveArgs
+ {
+ get
+ {
+ if (_resolveArgs == null)
+ {
+ _resolveArgs = new ItemResolveEventArgs()
+ {
+ FileInfo = FileData.GetFileData(this.Path),
+ Parent = this.Parent,
+ Cancel = false,
+ Path = this.Path
+ };
+ _resolveArgs = FileSystemHelper.FilterChildFileSystemEntries(_resolveArgs, (this.Parent != null && this.Parent.IsRoot));
+ }
+ return _resolveArgs;
+ }
+ set
+ {
+ _resolveArgs = value;
+ }
+ }
+
+ /// <summary>
+ /// Refresh metadata on us by execution our provider chain
+ /// </summary>
+ /// <returns>true if a provider reports we changed</returns>
+ public bool RefreshMetadata()
+ {
+ Kernel.Instance.ExecuteMetadataProviders(this).ConfigureAwait(false);
+ return true;
+ }
+
+ }
+}
diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs
new file mode 100644
index 000000000..4c9008b22
--- /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<string> PhysicalLocations
+ {
+ get
+ {
+ return _resolveArgs.PhysicalLocations;
+ }
+ }
+
+ public string SortName { get; set; }
+
+ /// <summary>
+ /// When the item first debuted. For movies this could be premiere date, episodes would be first aired
+ /// </summary>
+ public DateTime? PremiereDate { get; set; }
+
+ public string LogoImagePath { get; set; }
+
+ public string ArtImagePath { get; set; }
+
+ public string ThumbnailImagePath { get; set; }
+
+ public string BannerImagePath { get; set; }
+
+ public IEnumerable<string> BackdropImagePaths { get; set; }
+
+ public string OfficialRating { get; set; }
+
+ public string CustomRating { get; set; }
+ public string CustomPin { get; set; }
+
+ public string Language { get; set; }
+ public string Overview { get; set; }
+ public List<string> Taglines { get; set; }
+
+ /// <summary>
+ /// Using a Dictionary to prevent duplicates
+ /// </summary>
+ public Dictionary<string,PersonInfo> People { get; set; }
+
+ public List<string> Studios { get; set; }
+
+ public List<string> Genres { get; set; }
+
+ public string DisplayMediaType { get; set; }
+
+ public float? CommunityRating { get; set; }
+ public long? RunTimeTicks { get; set; }
+
+ public string AspectRatio { get; set; }
+ public int? ProductionYear { get; set; }
+
+ /// <summary>
+ /// If the item is part of a series, this is it's number in the series.
+ /// This could be episode number, album track number, etc.
+ /// </summary>
+ public int? IndexNumber { get; set; }
+
+ /// <summary>
+ /// For an episode this could be the season number, or for a song this could be the disc number.
+ /// </summary>
+ public int? ParentIndexNumber { get; set; }
+
+ public IEnumerable<Video> LocalTrailers { get; set; }
+
+ public string TrailerUrl { get; set; }
+
+ public Dictionary<string, string> ProviderIds { get; set; }
+
+ public Dictionary<Guid, UserItemData> UserData { get; set; }
+
+ public UserItemData GetUserData(User user, bool createIfNull)
+ {
+ if (UserData == null || !UserData.ContainsKey(user.Id))
+ {
+ if (createIfNull)
+ {
+ AddUserData(user, new UserItemData());
+ }
+ else
+ {
+ return null;
+ }
+ }
+
+ return UserData[user.Id];
+ }
+
+ private void AddUserData(User user, UserItemData data)
+ {
+ if (UserData == null)
+ {
+ UserData = new Dictionary<Guid, UserItemData>();
+ }
+
+ UserData[user.Id] = data;
+ }
+
+ /// <summary>
+ /// Determines if a given user has access to this item
+ /// </summary>
+ internal bool IsParentalAllowed(User user)
+ {
+ return true;
+ }
+
+ /// <summary>
+ /// Finds an item by ID, recursively
+ /// </summary>
+ public virtual BaseItem FindItemById(Guid id)
+ {
+ if (Id == id)
+ {
+ return this;
+ }
+
+ if (LocalTrailers != null)
+ {
+ return LocalTrailers.FirstOrDefault(i => i.Id == id);
+ }
+
+ return null;
+ }
+
+ public virtual bool IsFolder
+ {
+ get
+ {
+ return false;
+ }
+ }
+
+ /// <summary>
+ /// Determine if we have changed vs the passed in copy
+ /// </summary>
+ /// <param name="copy"></param>
+ /// <returns></returns>
+ public virtual bool IsChanged(BaseItem copy)
+ {
+ bool changed = copy.DateModified != this.DateModified;
+ if (changed) MediaBrowser.Common.Logging.Logger.LogDebugInfo(this.Name + " changed - original creation: " + this.DateCreated + " new creation: " + copy.DateCreated + " original modified: " + this.DateModified + " new modified: " + copy.DateModified);
+ return changed;
+ }
+
+ /// <summary>
+ /// Determines if the item is considered new based on user settings
+ /// </summary>
+ public bool IsRecentlyAdded(User user)
+ {
+ return (DateTime.UtcNow - DateCreated).TotalDays < user.RecentItemDays;
+ }
+
+ public void AddPerson(PersonInfo person)
+ {
+ if (People == null)
+ {
+ People = new Dictionary<string, PersonInfo>(StringComparer.OrdinalIgnoreCase);
+ }
+
+ People[person.Name] = person;
+ }
+
+ /// <summary>
+ /// Marks the item as either played or unplayed
+ /// </summary>
+ public virtual void SetPlayedStatus(User user, bool wasPlayed)
+ {
+ UserItemData data = GetUserData(user, true);
+
+ if (wasPlayed)
+ {
+ data.PlayCount = Math.Max(data.PlayCount, 1);
+ }
+ else
+ {
+ data.PlayCount = 0;
+ data.PlaybackPositionTicks = 0;
+ }
+ }
+
+ /// <summary>
+ /// Do whatever refreshing is necessary when the filesystem pertaining to this item has changed.
+ /// </summary>
+ /// <returns></returns>
+ public virtual Task ChangedExternally()
+ {
+ return Task.Run(() => RefreshMetadata());
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs
new file mode 100644
index 000000000..07529c80f
--- /dev/null
+++ b/MediaBrowser.Controller/Entities/Folder.cs
@@ -0,0 +1,619 @@
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Controller.IO;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Common.Logging;
+using MediaBrowser.Controller.Resolvers;
+using System;
+using System.Threading.Tasks;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace MediaBrowser.Controller.Entities
+{
+ public class Folder : BaseItem
+ {
+ #region Events
+ /// <summary>
+ /// Fires whenever a validation routine updates our children. The added and removed children are properties of the args.
+ /// *** Will fire asynchronously. ***
+ /// </summary>
+ public event EventHandler<ChildrenChangedEventArgs> ChildrenChanged;
+ protected void OnChildrenChanged(ChildrenChangedEventArgs args)
+ {
+ if (ChildrenChanged != null)
+ {
+ Task.Run( () =>
+ {
+ ChildrenChanged(this, args);
+ Kernel.Instance.OnLibraryChanged(args);
+ });
+ }
+ }
+
+ #endregion
+
+ public override bool IsFolder
+ {
+ get
+ {
+ return true;
+ }
+ }
+
+ public bool IsRoot { get; set; }
+
+ public bool IsVirtualFolder
+ {
+ get
+ {
+ return Parent != null && Parent.IsRoot;
+ }
+ }
+ protected object childLock = new object();
+ protected List<BaseItem> children;
+ protected virtual List<BaseItem> ActualChildren
+ {
+ get
+ {
+ if (children == null)
+ {
+ LoadChildren();
+ }
+ return children;
+ }
+
+ set
+ {
+ children = value;
+ }
+ }
+
+ /// <summary>
+ /// thread-safe access to the actual children of this folder - without regard to user
+ /// </summary>
+ public IEnumerable<BaseItem> Children
+ {
+ get
+ {
+ lock (childLock)
+ return ActualChildren.ToList();
+ }
+ }
+
+ /// <summary>
+ /// thread-safe access to all recursive children of this folder - without regard to user
+ /// </summary>
+ public IEnumerable<BaseItem> RecursiveChildren
+ {
+ get
+ {
+ foreach (var item in Children)
+ {
+ yield return item;
+
+ var subFolder = item as Folder;
+
+ if (subFolder != null)
+ {
+ foreach (var subitem in subFolder.RecursiveChildren)
+ {
+ yield return subitem;
+ }
+ }
+ }
+ }
+ }
+
+
+ /// <summary>
+ /// Loads and validates our children
+ /// </summary>
+ protected virtual void LoadChildren()
+ {
+ //first - load our children from the repo
+ lock (childLock)
+ children = GetCachedChildren();
+
+ //then kick off a validation against the actual file system
+ Task.Run(() => ValidateChildren());
+ }
+
+ protected bool ChildrenValidating = false;
+
+ /// <summary>
+ /// Compare our current children (presumably just read from the repo) with the current state of the file system and adjust for any changes
+ /// ***Currently does not contain logic to maintain items that are unavailable in the file system***
+ /// </summary>
+ /// <returns></returns>
+ protected async virtual void ValidateChildren()
+ {
+ if (ChildrenValidating) return; //only ever want one of these going at once and don't want them to fire off in sequence so don't use lock
+ ChildrenValidating = true;
+ bool changed = false; //this will save us a little time at the end if nothing changes
+ var changedArgs = new ChildrenChangedEventArgs(this);
+ //get the current valid children from filesystem (or wherever)
+ var nonCachedChildren = await GetNonCachedChildren();
+ if (nonCachedChildren == null) return; //nothing to validate
+ //build a dictionary of the current children we have now by Id so we can compare quickly and easily
+ Dictionary<Guid, BaseItem> currentChildren;
+ lock (childLock)
+ currentChildren = ActualChildren.ToDictionary(i => i.Id);
+
+ //create a list for our validated children
+ var validChildren = new List<BaseItem>();
+ //now traverse the valid children and find any changed or new items
+ foreach (var child in nonCachedChildren)
+ {
+ BaseItem currentChild;
+ currentChildren.TryGetValue(child.Id, out currentChild);
+ if (currentChild == null)
+ {
+ //brand new item - needs to be added
+ changed = true;
+ changedArgs.ItemsAdded.Add(child);
+ //refresh it
+ child.RefreshMetadata();
+ Logger.LogInfo("New Item Added to Library: ("+child.GetType().Name+") "+ child.Name + " (" + child.Path + ")");
+ //save it in repo...
+
+ //and add it to our valid children
+ validChildren.Add(child);
+ //fire an added event...?
+ //if it is a folder we need to validate its children as well
+ Folder folder = child as Folder;
+ if (folder != null)
+ {
+ folder.ValidateChildren();
+ //probably need to refresh too...
+ }
+ }
+ else
+ {
+ //existing item - check if it has changed
+ if (currentChild.IsChanged(child))
+ {
+ changed = true;
+ //update resolve args and refresh meta
+ // Note - we are refreshing the existing child instead of the newly found one so the "Except" operation below
+ // will identify this item as the same one
+ currentChild.ResolveArgs = child.ResolveArgs;
+ currentChild.RefreshMetadata();
+ Logger.LogInfo("Item Changed: ("+currentChild.GetType().Name+") "+ currentChild.Name + " (" + currentChild.Path + ")");
+ //save it in repo...
+ validChildren.Add(currentChild);
+ }
+ else
+ {
+ //current child that didn't change - just put it in the valid children
+ validChildren.Add(currentChild);
+ }
+ }
+ }
+
+ //that's all the new and changed ones - now see if there are any that are missing
+ changedArgs.ItemsRemoved = currentChildren.Values.Except(validChildren);
+ changed |= changedArgs.ItemsRemoved != null;
+
+ //now, if anything changed - replace our children
+ if (changed)
+ {
+ if (changedArgs.ItemsRemoved != null) foreach (var item in changedArgs.ItemsRemoved) Logger.LogDebugInfo("** " + item.Name + " Removed from library.");
+
+ lock (childLock)
+ ActualChildren = validChildren;
+ //and save children in repo...
+
+ //and fire event
+ this.OnChildrenChanged(changedArgs);
+ }
+ ChildrenValidating = false;
+
+ }
+
+ /// <summary>
+ /// Get the children of this folder from the actual file system
+ /// </summary>
+ /// <returns></returns>
+ protected async virtual Task<IEnumerable<BaseItem>> GetNonCachedChildren()
+ {
+ ItemResolveEventArgs args = new ItemResolveEventArgs()
+ {
+ FileInfo = FileData.GetFileData(this.Path),
+ Parent = this.Parent,
+ Cancel = false,
+ Path = this.Path
+ };
+
+ // Gather child folder and files
+ if (args.IsDirectory)
+ {
+ args.FileSystemChildren = FileData.GetFileSystemEntries(this.Path, "*").ToArray();
+
+ bool isVirtualFolder = Parent != null && Parent.IsRoot;
+ args = FileSystemHelper.FilterChildFileSystemEntries(args, isVirtualFolder);
+ }
+ else
+ {
+ Logger.LogError("Folder has a path that is not a directory: " + this.Path);
+ return null;
+ }
+
+ if (!EntityResolutionHelper.ShouldResolvePathContents(args))
+ {
+ return null;
+ }
+ return (await Task.WhenAll<BaseItem>(GetChildren(args.FileSystemChildren)).ConfigureAwait(false))
+ .Where(i => i != null).OrderBy(f =>
+ {
+ return string.IsNullOrEmpty(f.SortName) ? f.Name : f.SortName;
+
+ });
+
+ }
+
+ /// <summary>
+ /// Resolves a path into a BaseItem
+ /// </summary>
+ protected async Task<BaseItem> GetChild(string path, WIN32_FIND_DATA? fileInfo = null)
+ {
+ ItemResolveEventArgs args = new ItemResolveEventArgs()
+ {
+ FileInfo = fileInfo ?? FileData.GetFileData(path),
+ Parent = this,
+ Cancel = false,
+ Path = path
+ };
+
+ args.FileSystemChildren = FileData.GetFileSystemEntries(path, "*").ToArray();
+ args = FileSystemHelper.FilterChildFileSystemEntries(args, false);
+
+ return Kernel.Instance.ResolveItem(args);
+
+ }
+
+ /// <summary>
+ /// Finds child BaseItems for us
+ /// </summary>
+ protected Task<BaseItem>[] GetChildren(WIN32_FIND_DATA[] fileSystemChildren)
+ {
+ Task<BaseItem>[] tasks = new Task<BaseItem>[fileSystemChildren.Length];
+
+ for (int i = 0; i < fileSystemChildren.Length; i++)
+ {
+ var child = fileSystemChildren[i];
+
+ tasks[i] = GetChild(child.Path, child);
+ }
+
+ return tasks;
+ }
+
+
+ /// <summary>
+ /// Get our children from the repo - stubbed for now
+ /// </summary>
+ /// <returns></returns>
+ protected virtual List<BaseItem> GetCachedChildren()
+ {
+ return new List<BaseItem>();
+ }
+
+ /// <summary>
+ /// Gets allowed children of an item
+ /// </summary>
+ public IEnumerable<BaseItem> GetChildren(User user)
+ {
+ lock(childLock)
+ return ActualChildren.Where(c => c.IsParentalAllowed(user));
+ }
+
+ /// <summary>
+ /// Gets allowed recursive children of an item
+ /// </summary>
+ public IEnumerable<BaseItem> GetRecursiveChildren(User user)
+ {
+ foreach (var item in GetChildren(user))
+ {
+ yield return item;
+
+ var subFolder = item as Folder;
+
+ if (subFolder != null)
+ {
+ foreach (var subitem in subFolder.GetRecursiveChildren(user))
+ {
+ yield return subitem;
+ }
+ }
+ }
+ }
+
+ /// <summary>
+ /// Folders need to validate and refresh
+ /// </summary>
+ /// <returns></returns>
+ public override Task ChangedExternally()
+ {
+ return Task.Run(() =>
+ {
+ if (this.IsRoot)
+ {
+ Kernel.Instance.ReloadRoot().ConfigureAwait(false);
+ }
+ else
+ {
+ RefreshMetadata();
+ ValidateChildren();
+ }
+ });
+ }
+
+ /// <summary>
+ /// Since it can be slow to make all of these calculations at once, this method will provide a way to get them all back together
+ /// </summary>
+ public ItemSpecialCounts GetSpecialCounts(User user)
+ {
+ var counts = new ItemSpecialCounts();
+
+ IEnumerable<BaseItem> recursiveChildren = GetRecursiveChildren(user);
+
+ var recentlyAddedItems = GetRecentlyAddedItems(recursiveChildren, user);
+
+ counts.RecentlyAddedItemCount = recentlyAddedItems.Count;
+ counts.RecentlyAddedUnPlayedItemCount = GetRecentlyAddedUnplayedItems(recentlyAddedItems, user).Count;
+ counts.InProgressItemCount = GetInProgressItems(recursiveChildren, user).Count;
+ counts.PlayedPercentage = GetPlayedPercentage(recursiveChildren, user);
+
+ return counts;
+ }
+
+ /// <summary>
+ /// Finds all recursive items within a top-level parent that contain the given genre and are allowed for the current user
+ /// </summary>
+ public IEnumerable<BaseItem> GetItemsWithGenre(string genre, User user)
+ {
+ return GetRecursiveChildren(user).Where(f => f.Genres != null && f.Genres.Any(s => s.Equals(genre, StringComparison.OrdinalIgnoreCase)));
+ }
+
+ /// <summary>
+ /// Finds all recursive items within a top-level parent that contain the given year and are allowed for the current user
+ /// </summary>
+ public IEnumerable<BaseItem> GetItemsWithYear(int year, User user)
+ {
+ return GetRecursiveChildren(user).Where(f => f.ProductionYear.HasValue && f.ProductionYear == year);
+ }
+
+ /// <summary>
+ /// Finds all recursive items within a top-level parent that contain the given studio and are allowed for the current user
+ /// </summary>
+ public IEnumerable<BaseItem> GetItemsWithStudio(string studio, User user)
+ {
+ return GetRecursiveChildren(user).Where(f => f.Studios != null && f.Studios.Any(s => s.Equals(studio, StringComparison.OrdinalIgnoreCase)));
+ }
+
+ /// <summary>
+ /// Finds all recursive items within a top-level parent that the user has marked as a favorite
+ /// </summary>
+ public IEnumerable<BaseItem> GetFavoriteItems(User user)
+ {
+ return GetRecursiveChildren(user).Where(c =>
+ {
+ UserItemData data = c.GetUserData(user, false);
+
+ if (data != null)
+ {
+ return data.IsFavorite;
+ }
+
+ return false;
+ });
+ }
+
+ /// <summary>
+ /// Finds all recursive items within a top-level parent that contain the given person and are allowed for the current user
+ /// </summary>
+ public IEnumerable<BaseItem> GetItemsWithPerson(string person, User user)
+ {
+ return GetRecursiveChildren(user).Where(c =>
+ {
+ if (c.People != null)
+ {
+ return c.People.ContainsKey(person);
+ }
+
+ return false;
+ });
+ }
+
+ /// <summary>
+ /// Finds all recursive items within a top-level parent that contain the given person and are allowed for the current user
+ /// </summary>
+ /// <param name="personType">Specify this to limit results to a specific PersonType</param>
+ public IEnumerable<BaseItem> GetItemsWithPerson(string person, string personType, User user)
+ {
+ return GetRecursiveChildren(user).Where(c =>
+ {
+ if (c.People != null)
+ {
+ return c.People.ContainsKey(person) && c.People[person].Type.Equals(personType, StringComparison.OrdinalIgnoreCase);
+ }
+
+ return false;
+ });
+ }
+
+ /// <summary>
+ /// Gets all recently added items (recursive) within a folder, based on configuration and parental settings
+ /// </summary>
+ public List<BaseItem> GetRecentlyAddedItems(User user)
+ {
+ return GetRecentlyAddedItems(GetRecursiveChildren(user), user);
+ }
+
+ /// <summary>
+ /// Gets all recently added unplayed items (recursive) within a folder, based on configuration and parental settings
+ /// </summary>
+ public List<BaseItem> GetRecentlyAddedUnplayedItems(User user)
+ {
+ return GetRecentlyAddedUnplayedItems(GetRecursiveChildren(user), user);
+ }
+
+ /// <summary>
+ /// Gets all in-progress items (recursive) within a folder
+ /// </summary>
+ public List<BaseItem> GetInProgressItems(User user)
+ {
+ return GetInProgressItems(GetRecursiveChildren(user), user);
+ }
+
+ /// <summary>
+ /// Takes a list of items and returns the ones that are recently added
+ /// </summary>
+ private static List<BaseItem> GetRecentlyAddedItems(IEnumerable<BaseItem> itemSet, User user)
+ {
+ var list = new List<BaseItem>();
+
+ foreach (var item in itemSet)
+ {
+ if (!item.IsFolder && item.IsRecentlyAdded(user))
+ {
+ list.Add(item);
+ }
+ }
+
+ return list;
+ }
+
+ /// <summary>
+ /// Takes a list of items and returns the ones that are recently added and unplayed
+ /// </summary>
+ private static List<BaseItem> GetRecentlyAddedUnplayedItems(IEnumerable<BaseItem> itemSet, User user)
+ {
+ var list = new List<BaseItem>();
+
+ foreach (var item in itemSet)
+ {
+ if (!item.IsFolder && item.IsRecentlyAdded(user))
+ {
+ var userdata = item.GetUserData(user, false);
+
+ if (userdata == null || userdata.PlayCount == 0)
+ {
+ list.Add(item);
+ }
+ }
+ }
+
+ return list;
+ }
+
+ /// <summary>
+ /// Takes a list of items and returns the ones that are in progress
+ /// </summary>
+ private static List<BaseItem> GetInProgressItems(IEnumerable<BaseItem> itemSet, User user)
+ {
+ var list = new List<BaseItem>();
+
+ foreach (var item in itemSet)
+ {
+ if (!item.IsFolder)
+ {
+ var userdata = item.GetUserData(user, false);
+
+ if (userdata != null && userdata.PlaybackPositionTicks > 0)
+ {
+ list.Add(item);
+ }
+ }
+ }
+
+ return list;
+ }
+
+ /// <summary>
+ /// Gets the total played percentage for a set of items
+ /// </summary>
+ private static decimal GetPlayedPercentage(IEnumerable<BaseItem> itemSet, User user)
+ {
+ itemSet = itemSet.Where(i => !(i.IsFolder));
+
+ decimal totalPercent = 0;
+
+ int count = 0;
+
+ foreach (BaseItem item in itemSet)
+ {
+ count++;
+
+ UserItemData data = item.GetUserData(user, false);
+
+ if (data == null)
+ {
+ continue;
+ }
+
+ if (data.PlayCount > 0)
+ {
+ totalPercent += 100;
+ }
+ else if (data.PlaybackPositionTicks > 0 && item.RunTimeTicks.HasValue)
+ {
+ decimal itemPercent = data.PlaybackPositionTicks;
+ itemPercent /= item.RunTimeTicks.Value;
+ totalPercent += itemPercent;
+ }
+ }
+
+ if (count == 0)
+ {
+ return 0;
+ }
+
+ return totalPercent / count;
+ }
+
+ /// <summary>
+ /// Marks the item as either played or unplayed
+ /// </summary>
+ public override void SetPlayedStatus(User user, bool wasPlayed)
+ {
+ base.SetPlayedStatus(user, wasPlayed);
+
+ // Now sweep through recursively and update status
+ foreach (BaseItem item in GetChildren(user))
+ {
+ item.SetPlayedStatus(user, wasPlayed);
+ }
+ }
+
+ /// <summary>
+ /// Finds an item by ID, recursively
+ /// </summary>
+ public override BaseItem FindItemById(Guid id)
+ {
+ var result = base.FindItemById(id);
+
+ if (result != null)
+ {
+ return result;
+ }
+
+ //this should be functionally equivilent to what was here since it is IEnum and works on a thread-safe copy
+ return RecursiveChildren.FirstOrDefault(i => i.Id == id);
+ }
+
+ /// <summary>
+ /// Finds an item by path, recursively
+ /// </summary>
+ public BaseItem FindByPath(string path)
+ {
+ if (PhysicalLocations.Contains(path, StringComparer.OrdinalIgnoreCase))
+ {
+ return this;
+ }
+
+ //this should be functionally equivilent to what was here since it is IEnum and works on a thread-safe copy
+ return RecursiveChildren.FirstOrDefault(i => i.PhysicalLocations.Contains(path, StringComparer.OrdinalIgnoreCase));
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Entities/Genre.cs b/MediaBrowser.Controller/Entities/Genre.cs
new file mode 100644
index 000000000..ba343a2bc
--- /dev/null
+++ b/MediaBrowser.Controller/Entities/Genre.cs
@@ -0,0 +1,7 @@
+
+namespace MediaBrowser.Controller.Entities
+{
+ public class Genre : BaseEntity
+ {
+ }
+}
diff --git a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs
new file mode 100644
index 000000000..cb841530e
--- /dev/null
+++ b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs
@@ -0,0 +1,7 @@
+
+namespace MediaBrowser.Controller.Entities.Movies
+{
+ public class BoxSet : Folder
+ {
+ }
+}
diff --git a/MediaBrowser.Controller/Entities/Movies/Movie.cs b/MediaBrowser.Controller/Entities/Movies/Movie.cs
new file mode 100644
index 000000000..2d98fa06e
--- /dev/null
+++ b/MediaBrowser.Controller/Entities/Movies/Movie.cs
@@ -0,0 +1,31 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace MediaBrowser.Controller.Entities.Movies
+{
+ public class Movie : Video
+ {
+ public IEnumerable<Video> SpecialFeatures { get; set; }
+
+ /// <summary>
+ /// Finds an item by ID, recursively
+ /// </summary>
+ public override BaseItem FindItemById(Guid id)
+ {
+ var item = base.FindItemById(id);
+
+ if (item != null)
+ {
+ return item;
+ }
+
+ if (SpecialFeatures != null)
+ {
+ return SpecialFeatures.FirstOrDefault(i => i.Id == id);
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Entities/Person.cs b/MediaBrowser.Controller/Entities/Person.cs
new file mode 100644
index 000000000..a12b9e38e
--- /dev/null
+++ b/MediaBrowser.Controller/Entities/Person.cs
@@ -0,0 +1,25 @@
+
+namespace MediaBrowser.Controller.Entities
+{
+ /// <summary>
+ /// This is the full Person object that can be retrieved with all of it's data.
+ /// </summary>
+ public class Person : BaseEntity
+ {
+ }
+
+ /// <summary>
+ /// This is the small Person stub that is attached to BaseItems
+ /// </summary>
+ public class PersonInfo
+ {
+ public string Name { get; set; }
+ public string Overview { get; set; }
+ public string Type { get; set; }
+
+ public override string ToString()
+ {
+ return Name;
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Entities/Studio.cs b/MediaBrowser.Controller/Entities/Studio.cs
new file mode 100644
index 000000000..b7c6e6aa4
--- /dev/null
+++ b/MediaBrowser.Controller/Entities/Studio.cs
@@ -0,0 +1,7 @@
+
+namespace MediaBrowser.Controller.Entities
+{
+ public class Studio : BaseEntity
+ {
+ }
+}
diff --git a/MediaBrowser.Controller/Entities/TV/Episode.cs b/MediaBrowser.Controller/Entities/TV/Episode.cs
new file mode 100644
index 000000000..5d599fca7
--- /dev/null
+++ b/MediaBrowser.Controller/Entities/TV/Episode.cs
@@ -0,0 +1,7 @@
+
+namespace MediaBrowser.Controller.Entities.TV
+{
+ public class Episode : Video
+ {
+ }
+}
diff --git a/MediaBrowser.Controller/Entities/TV/Season.cs b/MediaBrowser.Controller/Entities/TV/Season.cs
new file mode 100644
index 000000000..f9c7fecb3
--- /dev/null
+++ b/MediaBrowser.Controller/Entities/TV/Season.cs
@@ -0,0 +1,34 @@
+using System;
+
+namespace MediaBrowser.Controller.Entities.TV
+{
+ public class Season : Folder
+ {
+ /// <summary>
+ /// Store these to reduce disk access in Episode Resolver
+ /// </summary>
+ public string[] MetadataFiles
+ {
+ get
+ {
+ return ResolveArgs.MetadataFiles ?? new string[] { };
+ }
+ }
+
+ /// <summary>
+ /// Determines if the metafolder contains a given file
+ /// </summary>
+ public bool ContainsMetadataFile(string file)
+ {
+ for (int i = 0; i < MetadataFiles.Length; i++)
+ {
+ if (MetadataFiles[i].Equals(file, StringComparison.OrdinalIgnoreCase))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Entities/TV/Series.cs b/MediaBrowser.Controller/Entities/TV/Series.cs
new file mode 100644
index 000000000..7c228a53d
--- /dev/null
+++ b/MediaBrowser.Controller/Entities/TV/Series.cs
@@ -0,0 +1,12 @@
+using System;
+using System.Collections.Generic;
+
+namespace MediaBrowser.Controller.Entities.TV
+{
+ public class Series : Folder
+ {
+ public string Status { get; set; }
+ public IEnumerable<DayOfWeek> AirDays { get; set; }
+ public string AirTime { get; set; }
+ }
+}
diff --git a/MediaBrowser.Controller/Entities/User.cs b/MediaBrowser.Controller/Entities/User.cs
new file mode 100644
index 000000000..01eadfafb
--- /dev/null
+++ b/MediaBrowser.Controller/Entities/User.cs
@@ -0,0 +1,21 @@
+using System;
+
+namespace MediaBrowser.Controller.Entities
+{
+ public class User : BaseEntity
+ {
+ public string Password { get; set; }
+
+ public string MaxParentalRating { get; set; }
+
+ public int RecentItemDays { get; set; }
+
+ public User()
+ {
+ RecentItemDays = 14;
+ }
+
+ public DateTime? LastLoginDate { get; set; }
+ public DateTime? LastActivityDate { get; set; }
+ }
+}
diff --git a/MediaBrowser.Controller/Entities/UserItemData.cs b/MediaBrowser.Controller/Entities/UserItemData.cs
new file mode 100644
index 000000000..bb4950046
--- /dev/null
+++ b/MediaBrowser.Controller/Entities/UserItemData.cs
@@ -0,0 +1,67 @@
+using System;
+using System.Runtime.Serialization;
+
+namespace MediaBrowser.Controller.Entities
+{
+ public class UserItemData
+ {
+ private float? _rating;
+ /// <summary>
+ /// Gets or sets the users 0-10 rating
+ /// </summary>
+ public float? Rating
+ {
+ get
+ {
+ return _rating;
+ }
+ set
+ {
+ if (value.HasValue)
+ {
+ if (value.Value < 0 || value.Value > 10)
+ {
+ throw new InvalidOperationException("A 0-10 rating is required for UserItemData.");
+ }
+ }
+
+ _rating = value;
+ }
+ }
+
+ public long PlaybackPositionTicks { get; set; }
+
+ public int PlayCount { get; set; }
+
+ public bool IsFavorite { get; set; }
+
+ /// <summary>
+ /// This is an interpreted property to indicate likes or dislikes
+ /// This should never be serialized.
+ /// </summary>
+ [IgnoreDataMember]
+ public bool? Likes
+ {
+ get
+ {
+ if (Rating != null)
+ {
+ return Rating >= 6.5;
+ }
+
+ return null;
+ }
+ set
+ {
+ if (value.HasValue)
+ {
+ Rating = value.Value ? 10 : 1;
+ }
+ else
+ {
+ Rating = null;
+ }
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Entities/Video.cs b/MediaBrowser.Controller/Entities/Video.cs
new file mode 100644
index 000000000..8dd82fab9
--- /dev/null
+++ b/MediaBrowser.Controller/Entities/Video.cs
@@ -0,0 +1,20 @@
+using MediaBrowser.Model.Entities;
+using System.Collections.Generic;
+
+namespace MediaBrowser.Controller.Entities
+{
+ public class Video : BaseItem
+ {
+ public VideoType VideoType { get; set; }
+
+ public List<SubtitleStream> Subtitles { get; set; }
+ public List<AudioStream> AudioStreams { get; set; }
+
+ public int Height { get; set; }
+ public int Width { get; set; }
+ public string ScanType { get; set; }
+ public float FrameRate { get; set; }
+ public int BitRate { get; set; }
+ public string Codec { get; set; }
+ }
+}
diff --git a/MediaBrowser.Controller/Entities/Year.cs b/MediaBrowser.Controller/Entities/Year.cs
new file mode 100644
index 000000000..d0b29de56
--- /dev/null
+++ b/MediaBrowser.Controller/Entities/Year.cs
@@ -0,0 +1,7 @@
+
+namespace MediaBrowser.Controller.Entities
+{
+ public class Year : BaseEntity
+ {
+ }
+}
diff --git a/MediaBrowser.Controller/FFMpeg/FFProbe.cs b/MediaBrowser.Controller/FFMpeg/FFProbe.cs
new file mode 100644
index 000000000..f16f0142d
--- /dev/null
+++ b/MediaBrowser.Controller/FFMpeg/FFProbe.cs
@@ -0,0 +1,137 @@
+using MediaBrowser.Common.Logging;
+using MediaBrowser.Common.Serialization;
+using MediaBrowser.Controller.Entities;
+using System;
+using System.Diagnostics;
+using System.IO;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.FFMpeg
+{
+ /// <summary>
+ /// Runs FFProbe against a media file and returns metadata.
+ /// </summary>
+ public static class FFProbe
+ {
+ /// <summary>
+ /// Runs FFProbe against an Audio file, caches the result and returns the output
+ /// </summary>
+ public static FFProbeResult Run(BaseItem item, string cacheDirectory)
+ {
+ string cachePath = GetFfProbeCachePath(item, cacheDirectory);
+
+ // Use try catch to avoid having to use File.Exists
+ try
+ {
+ return GetCachedResult(cachePath);
+ }
+ catch (FileNotFoundException)
+ {
+ }
+ catch (Exception ex)
+ {
+ Logger.LogException(ex);
+ }
+
+ FFProbeResult result = Run(item.Path);
+
+ if (result != null)
+ {
+ // Fire and forget
+ CacheResult(result, cachePath);
+ }
+
+ return result;
+ }
+
+ /// <summary>
+ /// Gets the cached result of an FFProbe operation
+ /// </summary>
+ private static FFProbeResult GetCachedResult(string path)
+ {
+ return ProtobufSerializer.DeserializeFromFile<FFProbeResult>(path);
+ }
+
+ /// <summary>
+ /// Caches the result of an FFProbe operation
+ /// </summary>
+ private static async void CacheResult(FFProbeResult result, string outputCachePath)
+ {
+ await Task.Run(() =>
+ {
+ try
+ {
+ ProtobufSerializer.SerializeToFile(result, outputCachePath);
+ }
+ catch (Exception ex)
+ {
+ Logger.LogException(ex);
+ }
+ }).ConfigureAwait(false);
+ }
+
+ private static FFProbeResult Run(string input)
+ {
+ 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.FFProbePath;
+ startInfo.WorkingDirectory = Kernel.Instance.ApplicationPaths.FFMpegDirectory;
+ startInfo.Arguments = string.Format("\"{0}\" -v quiet -print_format json -show_streams -show_format", input);
+
+ //Logger.LogInfo(startInfo.FileName + " " + startInfo.Arguments);
+
+ var process = new Process { };
+ process.StartInfo = startInfo;
+
+ process.EnableRaisingEvents = true;
+
+ process.Exited += ProcessExited;
+
+ try
+ {
+ process.Start();
+
+ // MUST read both stdout and stderr asynchronously or a deadlock may occurr
+ // If we ever decide to disable the ffmpeg log then you must uncomment the below line.
+ process.BeginErrorReadLine();
+
+ return JsonSerializer.DeserializeFromStream<FFProbeResult>(process.StandardOutput.BaseStream);
+ }
+ catch (Exception ex)
+ {
+ Logger.LogException(ex);
+
+ // Hate having to do this
+ try
+ {
+ process.Kill();
+ }
+ catch
+ {
+ }
+
+ return null;
+ }
+ }
+
+ static void ProcessExited(object sender, EventArgs e)
+ {
+ (sender as Process).Dispose();
+ }
+
+ private static string GetFfProbeCachePath(BaseItem item, string cacheDirectory)
+ {
+ string outputDirectory = Path.Combine(cacheDirectory, item.Id.ToString().Substring(0, 1));
+
+ return Path.Combine(outputDirectory, item.Id + "-" + item.DateModified.Ticks + ".pb");
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/FFMpeg/FFProbeResult.cs b/MediaBrowser.Controller/FFMpeg/FFProbeResult.cs
new file mode 100644
index 000000000..db7c9dd3c
--- /dev/null
+++ b/MediaBrowser.Controller/FFMpeg/FFProbeResult.cs
@@ -0,0 +1,119 @@
+using System.Collections.Generic;
+using ProtoBuf;
+
+namespace MediaBrowser.Controller.FFMpeg
+{
+ /// <summary>
+ /// Provides a class that we can use to deserialize the ffprobe json output
+ /// Sample output:
+ /// http://stackoverflow.com/questions/7708373/get-ffmpeg-information-in-friendly-way
+ /// </summary>
+ [ProtoContract]
+ public class FFProbeResult
+ {
+ [ProtoMember(1)]
+ public MediaStream[] streams { get; set; }
+
+ [ProtoMember(2)]
+ public MediaFormat format { get; set; }
+ }
+
+ /// <summary>
+ /// Represents a stream within the output
+ /// A number of properties are commented out to improve deserialization performance
+ /// Enable them as needed.
+ /// </summary>
+ [ProtoContract]
+ public class MediaStream
+ {
+ [ProtoMember(1)]
+ public int index { get; set; }
+
+ [ProtoMember(2)]
+ public string profile { get; set; }
+
+ [ProtoMember(3)]
+ public string codec_name { get; set; }
+
+ [ProtoMember(4)]
+ public string codec_long_name { get; set; }
+
+ [ProtoMember(5)]
+ public string codec_type { get; set; }
+
+ //public string codec_time_base { get; set; }
+ //public string codec_tag { get; set; }
+ //public string codec_tag_string { get; set; }
+ //public string sample_fmt { get; set; }
+
+ [ProtoMember(6)]
+ public string sample_rate { get; set; }
+
+ [ProtoMember(7)]
+ public int channels { get; set; }
+
+ //public int bits_per_sample { get; set; }
+ //public string r_frame_rate { get; set; }
+
+ [ProtoMember(8)]
+ public string avg_frame_rate { get; set; }
+
+ //public string time_base { get; set; }
+ //public string start_time { get; set; }
+
+ [ProtoMember(9)]
+ public string duration { get; set; }
+
+ [ProtoMember(10)]
+ public string bit_rate { get; set; }
+
+ [ProtoMember(11)]
+ public int width { get; set; }
+
+ [ProtoMember(12)]
+ public int height { get; set; }
+
+ //public int has_b_frames { get; set; }
+ //public string sample_aspect_ratio { get; set; }
+
+ [ProtoMember(13)]
+ public string display_aspect_ratio { get; set; }
+
+ //public string pix_fmt { get; set; }
+ //public int level { get; set; }
+
+ [ProtoMember(14)]
+ public Dictionary<string, string> tags { get; set; }
+ }
+
+ [ProtoContract]
+ public class MediaFormat
+ {
+ [ProtoMember(1)]
+ public string filename { get; set; }
+
+ [ProtoMember(2)]
+ public int nb_streams { get; set; }
+
+ [ProtoMember(3)]
+ public string format_name { get; set; }
+
+ [ProtoMember(4)]
+ public string format_long_name { get; set; }
+
+ [ProtoMember(5)]
+ public string start_time { get; set; }
+
+ [ProtoMember(6)]
+ public string duration { get; set; }
+
+ [ProtoMember(7)]
+ public string size { get; set; }
+
+ [ProtoMember(8)]
+ public string bit_rate { get; set; }
+
+ [ProtoMember(9)]
+ public Dictionary<string, string> tags { get; set; }
+ }
+}
diff --git a/MediaBrowser.Controller/FFMpeg/ffmpeg.exe.REMOVED.git-id b/MediaBrowser.Controller/FFMpeg/ffmpeg.exe.REMOVED.git-id
new file mode 100644
index 000000000..73a37bd55
--- /dev/null
+++ b/MediaBrowser.Controller/FFMpeg/ffmpeg.exe.REMOVED.git-id
@@ -0,0 +1 @@
+84ac1c51e84cfbfb20e7b96c9f1a4442a8cfadf2 \ No newline at end of file
diff --git a/MediaBrowser.Controller/FFMpeg/ffprobe.exe.REMOVED.git-id b/MediaBrowser.Controller/FFMpeg/ffprobe.exe.REMOVED.git-id
new file mode 100644
index 000000000..682ead74d
--- /dev/null
+++ b/MediaBrowser.Controller/FFMpeg/ffprobe.exe.REMOVED.git-id
@@ -0,0 +1 @@
+331e241e29f1b015e303b301c17c37883e39f39d \ No newline at end of file
diff --git a/MediaBrowser.Controller/FFMpeg/readme.txt b/MediaBrowser.Controller/FFMpeg/readme.txt
new file mode 100644
index 000000000..cdb039bdc
--- /dev/null
+++ b/MediaBrowser.Controller/FFMpeg/readme.txt
@@ -0,0 +1,3 @@
+This is the 32-bit static build of ffmpeg, located at:
+
+http://ffmpeg.zeranoe.com/builds/ \ No newline at end of file
diff --git a/MediaBrowser.Controller/IO/DirectoryWatchers.cs b/MediaBrowser.Controller/IO/DirectoryWatchers.cs
new file mode 100644
index 000000000..eb1358e16
--- /dev/null
+++ b/MediaBrowser.Controller/IO/DirectoryWatchers.cs
@@ -0,0 +1,172 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Common.Logging;
+using MediaBrowser.Common.Extensions;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.IO
+{
+ public class DirectoryWatchers
+ {
+ private readonly List<FileSystemWatcher> FileSystemWatchers = new List<FileSystemWatcher>();
+ private Timer updateTimer;
+ private List<string> affectedPaths = new List<string>();
+
+ private const int TimerDelayInSeconds = 30;
+
+ public void Start()
+ {
+ var pathsToWatch = new List<string>();
+
+ var rootFolder = Kernel.Instance.RootFolder;
+
+ pathsToWatch.Add(rootFolder.Path);
+
+ foreach (Folder folder in rootFolder.Children.OfType<Folder>())
+ {
+ foreach (string path in folder.PhysicalLocations)
+ {
+ if (Path.IsPathRooted(path) && !pathsToWatch.ContainsParentFolder(path))
+ {
+ pathsToWatch.Add(path);
+ }
+ }
+ }
+
+ foreach (string path in pathsToWatch)
+ {
+ Logger.LogInfo("Watching directory " + path + " for changes.");
+
+ var watcher = new FileSystemWatcher(path, "*") { };
+ watcher.IncludeSubdirectories = true;
+
+ //watcher.Changed += watcher_Changed;
+
+ // All the others seem to trigger change events on the parent, so let's keep it simple for now.
+ // Actually, we really need to only watch created, deleted and renamed as changed fires too much -ebr
+ watcher.Created += watcher_Changed;
+ watcher.Deleted += watcher_Changed;
+ watcher.Renamed += watcher_Changed;
+
+ watcher.EnableRaisingEvents = true;
+ FileSystemWatchers.Add(watcher);
+ }
+ }
+
+ void watcher_Changed(object sender, FileSystemEventArgs e)
+ {
+ Logger.LogDebugInfo("****** Watcher sees change of type " + e.ChangeType.ToString() + " to " + e.FullPath);
+ lock (affectedPaths)
+ {
+ //Since we're watching created, deleted and renamed we always want the parent of the item to be the affected path
+ var affectedPath = Path.GetDirectoryName(e.FullPath);
+
+ if (e.ChangeType == WatcherChangeTypes.Renamed)
+ {
+ var renamedArgs = e as RenamedEventArgs;
+ if (affectedPaths.Contains(renamedArgs.OldFullPath))
+ {
+ Logger.LogDebugInfo("****** Removing " + renamedArgs.OldFullPath + " from affected paths.");
+ affectedPaths.Remove(renamedArgs.OldFullPath);
+ }
+ }
+
+ //If anything underneath this path was already marked as affected - remove it as it will now get captured by this one
+ affectedPaths.RemoveAll(p => p.StartsWith(e.FullPath, StringComparison.OrdinalIgnoreCase));
+
+ if (!affectedPaths.ContainsParentFolder(affectedPath))
+ {
+ Logger.LogDebugInfo("****** Adding " + affectedPath + " to affected paths.");
+ affectedPaths.Add(affectedPath);
+ }
+ }
+
+ if (updateTimer == null)
+ {
+ updateTimer = new Timer(TimerStopped, null, TimeSpan.FromSeconds(TimerDelayInSeconds), TimeSpan.FromMilliseconds(-1));
+ }
+ else
+ {
+ updateTimer.Change(TimeSpan.FromSeconds(TimerDelayInSeconds), TimeSpan.FromMilliseconds(-1));
+ }
+ }
+
+ private async void TimerStopped(object stateInfo)
+ {
+ updateTimer.Dispose();
+ updateTimer = null;
+ List<string> paths;
+ lock (affectedPaths)
+ {
+ paths = affectedPaths;
+ affectedPaths = new List<string>();
+ }
+
+ await ProcessPathChanges(paths).ConfigureAwait(false);
+ }
+
+ private Task ProcessPathChanges(IEnumerable<string> paths)
+ {
+ var itemsToRefresh = new List<BaseItem>();
+
+ foreach (BaseItem item in paths.Select(p => GetAffectedBaseItem(p)))
+ {
+ if (item != null && !itemsToRefresh.Contains(item))
+ {
+ itemsToRefresh.Add(item);
+ }
+ }
+
+ if (itemsToRefresh.Any(i =>
+ {
+ var folder = i as Folder;
+
+ return folder != null && folder.IsRoot;
+ }))
+ {
+ return Kernel.Instance.ReloadRoot();
+ }
+
+ foreach (var p in paths) Logger.LogDebugInfo("********* "+ p + " reports change.");
+ foreach (var i in itemsToRefresh) Logger.LogDebugInfo("********* "+i.Name + " ("+ i.Path + ") will be refreshed.");
+ return Task.WhenAll(itemsToRefresh.Select(i => i.ChangedExternally()));
+ }
+
+ private BaseItem GetAffectedBaseItem(string path)
+ {
+ BaseItem item = null;
+
+ while (item == null && !string.IsNullOrEmpty(path))
+ {
+ item = Kernel.Instance.RootFolder.FindByPath(path);
+
+ path = Path.GetDirectoryName(path);
+ }
+
+ return item;
+ }
+
+ public void Stop()
+ {
+ foreach (FileSystemWatcher watcher in FileSystemWatchers)
+ {
+ watcher.Changed -= watcher_Changed;
+ watcher.EnableRaisingEvents = false;
+ watcher.Dispose();
+ }
+
+ if (updateTimer != null)
+ {
+ updateTimer.Dispose();
+ updateTimer = null;
+ }
+
+ FileSystemWatchers.Clear();
+ affectedPaths.Clear();
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/IO/FileData.cs b/MediaBrowser.Controller/IO/FileData.cs
new file mode 100644
index 000000000..4ae2ee72f
--- /dev/null
+++ b/MediaBrowser.Controller/IO/FileData.cs
@@ -0,0 +1,251 @@
+using MediaBrowser.Common.Logging;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Runtime.InteropServices;
+
+namespace MediaBrowser.Controller.IO
+{
+ /// <summary>
+ /// Provides low level File access that is much faster than the File/Directory api's
+ /// </summary>
+ public static class FileData
+ {
+ public const int MAX_PATH = 260;
+ public const int MAX_ALTERNATE = 14;
+ public static IntPtr INVALID_HANDLE_VALUE = new IntPtr(-1);
+
+ /// <summary>
+ /// Gets information about a path
+ /// </summary>
+ public static WIN32_FIND_DATA GetFileData(string path)
+ {
+ WIN32_FIND_DATA data;
+ IntPtr handle = FindFirstFile(path, out data);
+ bool getFilename = false;
+
+ if (handle == INVALID_HANDLE_VALUE && !Path.HasExtension(path))
+ {
+ if (!path.EndsWith("*"))
+ {
+ Logger.LogInfo("Handle came back invalid for {0}. Since this is a directory we'll try appending \\*.", path);
+
+ FindClose(handle);
+
+ handle = FindFirstFile(Path.Combine(path, "*"), out data);
+
+ getFilename = true;
+ }
+ }
+
+ if (handle == IntPtr.Zero)
+ {
+ throw new IOException("FindFirstFile failed");
+ }
+
+ if (getFilename)
+ {
+ data.cFileName = Path.GetFileName(path);
+ }
+
+ FindClose(handle);
+
+ data.Path = path;
+ return data;
+ }
+
+ /// <summary>
+ /// Gets all file system entries within a foler
+ /// </summary>
+ public static IEnumerable<WIN32_FIND_DATA> GetFileSystemEntries(string path, string searchPattern)
+ {
+ return GetFileSystemEntries(path, searchPattern, true, true);
+ }
+
+ /// <summary>
+ /// Gets all files within a folder
+ /// </summary>
+ public static IEnumerable<WIN32_FIND_DATA> GetFiles(string path, string searchPattern)
+ {
+ return GetFileSystemEntries(path, searchPattern, true, false);
+ }
+
+ /// <summary>
+ /// Gets all sub-directories within a folder
+ /// </summary>
+ public static IEnumerable<WIN32_FIND_DATA> GetDirectories(string path, string searchPattern)
+ {
+ return GetFileSystemEntries(path, searchPattern, false, true);
+ }
+
+ /// <summary>
+ /// Gets all file system entries within a foler
+ /// </summary>
+ public static IEnumerable<WIN32_FIND_DATA> GetFileSystemEntries(string path, string searchPattern, bool includeFiles, bool includeDirectories)
+ {
+ string lpFileName = Path.Combine(path, searchPattern);
+
+ WIN32_FIND_DATA lpFindFileData;
+ var handle = FindFirstFile(lpFileName, out lpFindFileData);
+
+ if (handle == IntPtr.Zero)
+ {
+ int hr = Marshal.GetLastWin32Error();
+ if (hr != 2 && hr != 0x12)
+ {
+ throw new IOException("GetFileSystemEntries failed");
+ }
+ yield break;
+ }
+
+ if (IncludeInOutput(lpFindFileData.cFileName, lpFindFileData.dwFileAttributes, includeFiles, includeDirectories))
+ {
+ yield return lpFindFileData;
+ }
+
+ while (FindNextFile(handle, out lpFindFileData) != IntPtr.Zero)
+ {
+ if (IncludeInOutput(lpFindFileData.cFileName, lpFindFileData.dwFileAttributes, includeFiles, includeDirectories))
+ {
+ lpFindFileData.Path = Path.Combine(path, lpFindFileData.cFileName);
+ yield return lpFindFileData;
+ }
+ }
+
+ FindClose(handle);
+ }
+
+ private static bool IncludeInOutput(string cFileName, FileAttributes attributes, bool includeFiles, bool includeDirectories)
+ {
+ if (cFileName.Equals(".", StringComparison.OrdinalIgnoreCase))
+ {
+ return false;
+ }
+ if (cFileName.Equals("..", StringComparison.OrdinalIgnoreCase))
+ {
+ return false;
+ }
+
+ if (!includeFiles && !attributes.HasFlag(FileAttributes.Directory))
+ {
+ return false;
+ }
+
+ if (!includeDirectories && attributes.HasFlag(FileAttributes.Directory))
+ {
+ return false;
+ }
+
+ return true;
+ }
+
+ [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
+ private static extern IntPtr FindFirstFile(string fileName, out WIN32_FIND_DATA data);
+
+ [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
+ private static extern IntPtr FindNextFile(IntPtr hFindFile, out WIN32_FIND_DATA data);
+
+ [DllImport("kernel32")]
+ private static extern bool FindClose(IntPtr hFindFile);
+
+ private const char SpaceChar = ' ';
+ private static readonly char[] InvalidFileNameChars = Path.GetInvalidFileNameChars();
+
+ /// <summary>
+ /// Takes a filename and removes invalid characters
+ /// </summary>
+ public static string GetValidFilename(string filename)
+ {
+ foreach (char c in InvalidFileNameChars)
+ {
+ filename = filename.Replace(c, SpaceChar);
+ }
+
+ return filename;
+ }
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ public struct FILETIME
+ {
+ public uint dwLowDateTime;
+ public uint dwHighDateTime;
+ }
+
+ [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
+ public struct WIN32_FIND_DATA
+ {
+ public FileAttributes dwFileAttributes;
+ public FILETIME ftCreationTime;
+ public FILETIME ftLastAccessTime;
+ public FILETIME ftLastWriteTime;
+ public int nFileSizeHigh;
+ public int nFileSizeLow;
+ public int dwReserved0;
+ public int dwReserved1;
+
+ [MarshalAs(UnmanagedType.ByValTStr, SizeConst = FileData.MAX_PATH)]
+ public string cFileName;
+
+ [MarshalAs(UnmanagedType.ByValTStr, SizeConst = FileData.MAX_ALTERNATE)]
+ public string cAlternate;
+
+ public bool IsHidden
+ {
+ get
+ {
+ return dwFileAttributes.HasFlag(FileAttributes.Hidden);
+ }
+ }
+
+ public bool IsSystemFile
+ {
+ get
+ {
+ return dwFileAttributes.HasFlag(FileAttributes.System);
+ }
+ }
+
+ public bool IsDirectory
+ {
+ get
+ {
+ return dwFileAttributes.HasFlag(FileAttributes.Directory);
+ }
+ }
+
+ public DateTime CreationTimeUtc
+ {
+ get
+ {
+ return ParseFileTime(ftCreationTime);
+ }
+ }
+
+ public DateTime LastAccessTimeUtc
+ {
+ get
+ {
+ return ParseFileTime(ftLastAccessTime);
+ }
+ }
+
+ public DateTime LastWriteTimeUtc
+ {
+ get
+ {
+ return ParseFileTime(ftLastWriteTime);
+ }
+ }
+
+ private DateTime ParseFileTime(FILETIME filetime)
+ {
+ long highBits = filetime.dwHighDateTime;
+ highBits = highBits << 32;
+ return DateTime.FromFileTimeUtc(highBits + (long)filetime.dwLowDateTime);
+ }
+
+ public string Path { get; set; }
+ }
+
+}
diff --git a/MediaBrowser.Controller/IO/FileSystemHelper.cs b/MediaBrowser.Controller/IO/FileSystemHelper.cs
new file mode 100644
index 000000000..732cf0803
--- /dev/null
+++ b/MediaBrowser.Controller/IO/FileSystemHelper.cs
@@ -0,0 +1,132 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.IO;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Resolvers;
+using MediaBrowser.Controller.Library;
+
+namespace MediaBrowser.Controller.IO
+{
+ public static class FileSystemHelper
+ {
+ /// <summary>
+ /// Transforms shortcuts into their actual paths and filters out items that should be ignored
+ /// </summary>
+ public static ItemResolveEventArgs FilterChildFileSystemEntries(ItemResolveEventArgs args, bool flattenShortcuts)
+ {
+
+ List<WIN32_FIND_DATA> returnChildren = new List<WIN32_FIND_DATA>();
+ List<WIN32_FIND_DATA> resolvedShortcuts = new List<WIN32_FIND_DATA>();
+
+ foreach (var file in args.FileSystemChildren)
+ {
+ // If it's a shortcut, resolve it
+ if (Shortcut.IsShortcut(file.Path))
+ {
+ string newPath = Shortcut.ResolveShortcut(file.Path);
+ WIN32_FIND_DATA newPathData = FileData.GetFileData(newPath);
+
+ // Find out if the shortcut is pointing to a directory or file
+ if (newPathData.IsDirectory)
+ {
+ // add to our physical locations
+ args.AdditionalLocations.Add(newPath);
+
+ // If we're flattening then get the shortcut's children
+ if (flattenShortcuts)
+ {
+ returnChildren.Add(file);
+ ItemResolveEventArgs newArgs = new ItemResolveEventArgs()
+ {
+ FileSystemChildren = FileData.GetFileSystemEntries(newPath, "*").ToArray()
+ };
+
+ resolvedShortcuts.AddRange(FilterChildFileSystemEntries(newArgs, false).FileSystemChildren);
+ }
+ else
+ {
+ returnChildren.Add(newPathData);
+ }
+ }
+ else
+ {
+ returnChildren.Add(newPathData);
+ }
+ }
+ else
+ {
+ //not a shortcut check to see if we should filter it out
+ if (EntityResolutionHelper.ShouldResolvePath(file))
+ {
+ returnChildren.Add(file);
+ }
+ else
+ {
+ //filtered - see if it is one of our "indicator" folders and mark it now - no reason to search for it again
+ args.IsBDFolder |= file.cFileName.Equals("bdmv", StringComparison.OrdinalIgnoreCase);
+ args.IsDVDFolder |= file.cFileName.Equals("video_ts", StringComparison.OrdinalIgnoreCase);
+ args.IsHDDVDFolder |= file.cFileName.Equals("hvdvd_ts", StringComparison.OrdinalIgnoreCase);
+
+ //and check to see if it is a metadata folder and collect contents now if so
+ if (IsMetadataFolder(file.cFileName))
+ {
+ args.MetadataFiles = Directory.GetFiles(Path.Combine(args.Path, "metadata"), "*", SearchOption.TopDirectoryOnly);
+ }
+ }
+ }
+ }
+
+ if (resolvedShortcuts.Count > 0)
+ {
+ resolvedShortcuts.InsertRange(0, returnChildren);
+ args.FileSystemChildren = resolvedShortcuts.ToArray();
+ }
+ else
+ {
+ args.FileSystemChildren = returnChildren.ToArray();
+ }
+ return args;
+ }
+
+ public static bool IsMetadataFolder(string path)
+ {
+ return path.TrimEnd('\\').EndsWith("metadata", StringComparison.OrdinalIgnoreCase);
+ }
+
+ public static bool IsVideoFile(string path)
+ {
+ string extension = System.IO.Path.GetExtension(path).ToLower();
+
+ switch (extension)
+ {
+ case ".mkv":
+ case ".m2ts":
+ case ".iso":
+ case ".ts":
+ case ".rmvb":
+ case ".mov":
+ case ".avi":
+ case ".mpg":
+ case ".mpeg":
+ case ".wmv":
+ case ".mp4":
+ case ".divx":
+ case ".dvr-ms":
+ case ".wtv":
+ case ".ogm":
+ case ".ogv":
+ case ".asf":
+ case ".m4v":
+ case ".flv":
+ case ".f4v":
+ case ".3gp":
+ return true;
+
+ default:
+ return false;
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/IO/Shortcut.cs b/MediaBrowser.Controller/IO/Shortcut.cs
new file mode 100644
index 000000000..e9ea21f17
--- /dev/null
+++ b/MediaBrowser.Controller/IO/Shortcut.cs
@@ -0,0 +1,185 @@
+using System;
+using System.IO;
+using System.Runtime.InteropServices;
+using System.Text;
+
+namespace MediaBrowser.Controller.IO
+{
+ /// <summary>
+ /// Contains helpers to interact with shortcut files (.lnk)
+ /// </summary>
+ public static class Shortcut
+ {
+ #region Signitures were imported from http://pinvoke.net
+ [Flags()]
+ enum SLGP_FLAGS
+ {
+ /// <summary>Retrieves the standard short (8.3 format) file name</summary>
+ SLGP_SHORTPATH = 0x1,
+ /// <summary>Retrieves the Universal Naming Convention (UNC) path name of the file</summary>
+ SLGP_UNCPRIORITY = 0x2,
+ /// <summary>Retrieves the raw path name. A raw path is something that might not exist and may include environment variables that need to be expanded</summary>
+ SLGP_RAWPATH = 0x4
+ }
+
+ [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
+ struct WIN32_FIND_DATAW
+ {
+ public uint dwFileAttributes;
+ public long ftCreationTime;
+ public long ftLastAccessTime;
+ public long ftLastWriteTime;
+ public uint nFileSizeHigh;
+ public uint nFileSizeLow;
+ public uint dwReserved0;
+ public uint dwReserved1;
+ [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)]
+ public string cFileName;
+ [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 14)]
+ public string cAlternateFileName;
+ }
+
+ [Flags()]
+
+ enum SLR_FLAGS
+ {
+ /// <summary>
+ /// Do not display a dialog box if the link cannot be resolved. When SLR_NO_UI is set,
+ /// the high-order word of fFlags can be set to a time-out value that specifies the
+ /// maximum amount of time to be spent resolving the link. The function returns if the
+ /// link cannot be resolved within the time-out duration. If the high-order word is set
+ /// to zero, the time-out duration will be set to the default value of 3,000 milliseconds
+ /// (3 seconds). To specify a value, set the high word of fFlags to the desired time-out
+ /// duration, in milliseconds.
+ /// </summary>
+ SLR_NO_UI = 0x1,
+ /// <summary>Obsolete and no longer used</summary>
+ SLR_ANY_MATCH = 0x2,
+ /// <summary>If the link object has changed, update its path and list of identifiers.
+ /// If SLR_UPDATE is set, you do not need to call IPersistFile::IsDirty to determine
+ /// whether or not the link object has changed.</summary>
+ SLR_UPDATE = 0x4,
+ /// <summary>Do not update the link information</summary>
+ SLR_NOUPDATE = 0x8,
+ /// <summary>Do not execute the search heuristics</summary>
+ SLR_NOSEARCH = 0x10,
+ /// <summary>Do not use distributed link tracking</summary>
+ SLR_NOTRACK = 0x20,
+ /// <summary>Disable distributed link tracking. By default, distributed link tracking tracks
+ /// removable media across multiple devices based on the volume name. It also uses the
+ /// Universal Naming Convention (UNC) path to track remote file systems whose drive letter
+ /// has changed. Setting SLR_NOLINKINFO disables both types of tracking.</summary>
+ SLR_NOLINKINFO = 0x40,
+ /// <summary>Call the Microsoft Windows Installer</summary>
+ SLR_INVOKE_MSI = 0x80
+ }
+
+
+ /// <summary>The IShellLink interface allows Shell links to be created, modified, and resolved</summary>
+ [ComImport(), InterfaceType(ComInterfaceType.InterfaceIsIUnknown), Guid("000214F9-0000-0000-C000-000000000046")]
+ interface IShellLinkW
+ {
+ /// <summary>Retrieves the path and file name of a Shell link object</summary>
+ void GetPath([Out(), MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszFile, int cchMaxPath, out WIN32_FIND_DATAW pfd, SLGP_FLAGS fFlags);
+ /// <summary>Retrieves the list of item identifiers for a Shell link object</summary>
+ void GetIDList(out IntPtr ppidl);
+ /// <summary>Sets the pointer to an item identifier list (PIDL) for a Shell link object.</summary>
+ void SetIDList(IntPtr pidl);
+ /// <summary>Retrieves the description string for a Shell link object</summary>
+ void GetDescription([Out(), MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszName, int cchMaxName);
+ /// <summary>Sets the description for a Shell link object. The description can be any application-defined string</summary>
+ void SetDescription([MarshalAs(UnmanagedType.LPWStr)] string pszName);
+ /// <summary>Retrieves the name of the working directory for a Shell link object</summary>
+ void GetWorkingDirectory([Out(), MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszDir, int cchMaxPath);
+ /// <summary>Sets the name of the working directory for a Shell link object</summary>
+ void SetWorkingDirectory([MarshalAs(UnmanagedType.LPWStr)] string pszDir);
+ /// <summary>Retrieves the command-line arguments associated with a Shell link object</summary>
+ void GetArguments([Out(), MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszArgs, int cchMaxPath);
+ /// <summary>Sets the command-line arguments for a Shell link object</summary>
+ void SetArguments([MarshalAs(UnmanagedType.LPWStr)] string pszArgs);
+ /// <summary>Retrieves the hot key for a Shell link object</summary>
+ void GetHotkey(out short pwHotkey);
+ /// <summary>Sets a hot key for a Shell link object</summary>
+ void SetHotkey(short wHotkey);
+ /// <summary>Retrieves the show command for a Shell link object</summary>
+ void GetShowCmd(out int piShowCmd);
+ /// <summary>Sets the show command for a Shell link object. The show command sets the initial show state of the window.</summary>
+ void SetShowCmd(int iShowCmd);
+ /// <summary>Retrieves the location (path and index) of the icon for a Shell link object</summary>
+ void GetIconLocation([Out(), MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszIconPath,
+ int cchIconPath, out int piIcon);
+ /// <summary>Sets the location (path and index) of the icon for a Shell link object</summary>
+ void SetIconLocation([MarshalAs(UnmanagedType.LPWStr)] string pszIconPath, int iIcon);
+ /// <summary>Sets the relative path to the Shell link object</summary>
+ void SetRelativePath([MarshalAs(UnmanagedType.LPWStr)] string pszPathRel, int dwReserved);
+ /// <summary>Attempts to find the target of a Shell link, even if it has been moved or renamed</summary>
+ void Resolve(IntPtr hwnd, SLR_FLAGS fFlags);
+ /// <summary>Sets the path and file name of a Shell link object</summary>
+ void SetPath([MarshalAs(UnmanagedType.LPWStr)] string pszFile);
+
+ }
+
+ [ComImport, Guid("0000010c-0000-0000-c000-000000000046"),
+ InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
+ public interface IPersist
+ {
+ [PreserveSig]
+ void GetClassID(out Guid pClassID);
+ }
+
+
+ [ComImport, Guid("0000010b-0000-0000-C000-000000000046"),
+ InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
+ public interface IPersistFile : IPersist
+ {
+ new void GetClassID(out Guid pClassID);
+ [PreserveSig]
+ int IsDirty();
+
+ [PreserveSig]
+ void Load([In, MarshalAs(UnmanagedType.LPWStr)]
+ string pszFileName, uint dwMode);
+
+ [PreserveSig]
+ void Save([In, MarshalAs(UnmanagedType.LPWStr)] string pszFileName,
+ [In, MarshalAs(UnmanagedType.Bool)] bool remember);
+
+ [PreserveSig]
+ void SaveCompleted([In, MarshalAs(UnmanagedType.LPWStr)] string pszFileName);
+
+ [PreserveSig]
+ void GetCurFile([In, MarshalAs(UnmanagedType.LPWStr)] string ppszFileName);
+ }
+
+ const uint STGM_READ = 0;
+ const int MAX_PATH = 260;
+
+ // CLSID_ShellLink from ShlGuid.h
+ [
+ ComImport(),
+ Guid("00021401-0000-0000-C000-000000000046")
+ ]
+ public class ShellLink
+ {
+ }
+
+ #endregion
+
+ public static string ResolveShortcut(string filename)
+ {
+ var link = new ShellLink();
+ ((IPersistFile)link).Load(filename, STGM_READ);
+ // TODO: if I can get hold of the hwnd call resolve first. This handles moved and renamed files.
+ // ((IShellLinkW)link).Resolve(hwnd, 0)
+ var sb = new StringBuilder(MAX_PATH);
+ var data = new WIN32_FIND_DATAW();
+ ((IShellLinkW)link).GetPath(sb, sb.Capacity, out data, 0);
+ return sb.ToString();
+ }
+
+ public static bool IsShortcut(string filename)
+ {
+ return filename != null ? Path.GetExtension(filename).EndsWith("lnk", StringComparison.OrdinalIgnoreCase) : false;
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Kernel.cs b/MediaBrowser.Controller/Kernel.cs
new file mode 100644
index 000000000..2430260dd
--- /dev/null
+++ b/MediaBrowser.Controller/Kernel.cs
@@ -0,0 +1,386 @@
+using MediaBrowser.Common.Kernel;
+using MediaBrowser.Common.Logging;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.IO;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Controller.Resolvers;
+using MediaBrowser.Controller.Weather;
+using MediaBrowser.Model.Authentication;
+using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.Progress;
+using MediaBrowser.Common.Extensions;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.Composition;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Security.Cryptography;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller
+{
+ public class Kernel : BaseKernel<ServerConfiguration, ServerApplicationPaths>
+ {
+ #region Events
+ /// <summary>
+ /// Fires whenever any validation routine adds or removes items. The added and removed items are properties of the args.
+ /// *** Will fire asynchronously. ***
+ /// </summary>
+ public event EventHandler<ChildrenChangedEventArgs> LibraryChanged;
+ public void OnLibraryChanged(ChildrenChangedEventArgs args)
+ {
+ if (LibraryChanged != null)
+ {
+ Task.Run(() => LibraryChanged(this, args));
+ }
+ }
+
+ #endregion
+ public static Kernel Instance { get; private set; }
+
+ public ItemController ItemController { get; private set; }
+
+ public IEnumerable<User> Users { get; private set; }
+ public Folder RootFolder { get; private set; }
+
+ private DirectoryWatchers DirectoryWatchers { get; set; }
+
+ private string MediaRootFolderPath
+ {
+ get
+ {
+ return ApplicationPaths.RootFolderPath;
+ }
+ }
+
+ public override KernelContext KernelContext
+ {
+ get { return KernelContext.Server; }
+ }
+
+ /// <summary>
+ /// Gets the list of currently registered weather prvoiders
+ /// </summary>
+ [ImportMany(typeof(BaseWeatherProvider))]
+ public IEnumerable<BaseWeatherProvider> WeatherProviders { get; private set; }
+
+ /// <summary>
+ /// Gets the list of currently registered metadata prvoiders
+ /// </summary>
+ [ImportMany(typeof(BaseMetadataProvider))]
+ private IEnumerable<BaseMetadataProvider> MetadataProvidersEnumerable { get; set; }
+
+ /// <summary>
+ /// Once MEF has loaded the resolvers, sort them by priority and store them in this array
+ /// Given the sheer number of times they'll be iterated over it'll be faster to loop through an array
+ /// </summary>
+ private BaseMetadataProvider[] MetadataProviders { get; set; }
+
+ /// <summary>
+ /// Gets the list of currently registered entity resolvers
+ /// </summary>
+ [ImportMany(typeof(IBaseItemResolver))]
+ private IEnumerable<IBaseItemResolver> EntityResolversEnumerable { get; set; }
+
+ /// <summary>
+ /// Once MEF has loaded the resolvers, sort them by priority and store them in this array
+ /// Given the sheer number of times they'll be iterated over it'll be faster to loop through an array
+ /// </summary>
+ internal IBaseItemResolver[] EntityResolvers { get; private set; }
+
+ /// <summary>
+ /// Creates a kernel based on a Data path, which is akin to our current programdata path
+ /// </summary>
+ public Kernel()
+ : base()
+ {
+ Instance = this;
+ }
+
+ /// <summary>
+ /// Performs initializations that only occur once
+ /// </summary>
+ protected override void InitializeInternal(IProgress<TaskProgress> progress)
+ {
+ base.InitializeInternal(progress);
+
+ ItemController = new ItemController();
+ DirectoryWatchers = new DirectoryWatchers();
+
+
+ ExtractFFMpeg();
+ }
+
+ /// <summary>
+ /// Performs initializations that can be reloaded at anytime
+ /// </summary>
+ protected override async Task ReloadInternal(IProgress<TaskProgress> progress)
+ {
+ await base.ReloadInternal(progress).ConfigureAwait(false);
+
+ ReportProgress(progress, "Loading Users");
+ ReloadUsers();
+
+ ReportProgress(progress, "Loading Media Library");
+
+ await ReloadRoot(allowInternetProviders: false).ConfigureAwait(false);
+
+ }
+
+ /// <summary>
+ /// Completely disposes the Kernel
+ /// </summary>
+ public override void Dispose()
+ {
+ base.Dispose();
+
+ DirectoryWatchers.Stop();
+
+ }
+
+ protected override void OnComposablePartsLoaded()
+ {
+ // The base class will start up all the plugins
+ base.OnComposablePartsLoaded();
+
+ // Sort the resolvers by priority
+ EntityResolvers = EntityResolversEnumerable.OrderBy(e => e.Priority).ToArray();
+
+ // Sort the providers by priority
+ MetadataProviders = MetadataProvidersEnumerable.OrderBy(e => e.Priority).ToArray();
+ }
+
+ public BaseItem ResolveItem(ItemResolveEventArgs args)
+ {
+ // Try first priority resolvers
+ for (int i = 0; i < EntityResolvers.Length; i++)
+ {
+ var item = EntityResolvers[i].ResolvePath(args);
+
+ if (item != null)
+ {
+ item.ResolveArgs = args;
+ return item;
+ }
+ }
+
+ return null;
+ }
+
+ private void ReloadUsers()
+ {
+ Users = GetAllUsers();
+ }
+
+ /// <summary>
+ /// Reloads the root media folder
+ /// </summary>
+ public async Task ReloadRoot(bool allowInternetProviders = true)
+ {
+ if (!Directory.Exists(MediaRootFolderPath))
+ {
+ Directory.CreateDirectory(MediaRootFolderPath);
+ }
+
+ DirectoryWatchers.Stop();
+
+ RootFolder = await ItemController.GetItem(MediaRootFolderPath, allowInternetProviders: allowInternetProviders).ConfigureAwait(false) as Folder;
+ RootFolder.ChildrenChanged += RootFolder_ChildrenChanged;
+
+ DirectoryWatchers.Start();
+ }
+
+ void RootFolder_ChildrenChanged(object sender, ChildrenChangedEventArgs e)
+ {
+ Logger.LogDebugInfo("Root Folder Children Changed. Added: " + e.ItemsAdded.Count + " Removed: " + e.ItemsRemoved.Count());
+ //re-start the directory watchers
+ DirectoryWatchers.Stop();
+ DirectoryWatchers.Start();
+ //Task.Delay(30000); //let's wait and see if more data gets filled in...
+ var allChildren = RootFolder.RecursiveChildren;
+ Logger.LogDebugInfo(string.Format("Loading complete. Movies: {0} Episodes: {1} Folders: {2}", allChildren.OfType<Entities.Movies.Movie>().Count(), allChildren.OfType<Entities.TV.Episode>().Count(), allChildren.Where(i => i is Folder && !(i is Series || i is Season)).Count()));
+ //foreach (var child in allChildren)
+ //{
+ // Logger.LogDebugInfo("(" + child.GetType().Name + ") " + child.Name + " (" + child.Path + ")");
+ //}
+ }
+
+ /// <summary>
+ /// Gets the default user to use when EnableUserProfiles is false
+ /// </summary>
+ public User GetDefaultUser()
+ {
+ User user = Users.FirstOrDefault();
+
+ return user;
+ }
+
+ /// <summary>
+ /// Persists a User
+ /// </summary>
+ public void SaveUser(User user)
+ {
+
+ }
+
+ /// <summary>
+ /// Authenticates a User and returns a result indicating whether or not it succeeded
+ /// </summary>
+ public AuthenticationResult AuthenticateUser(User user, string password)
+ {
+ var result = new AuthenticationResult();
+
+ // When EnableUserProfiles is false, only the default User can login
+ if (!Configuration.EnableUserProfiles)
+ {
+ result.Success = user.Id == GetDefaultUser().Id;
+ }
+ else if (string.IsNullOrEmpty(user.Password))
+ {
+ result.Success = true;
+ }
+ else
+ {
+ password = password ?? string.Empty;
+ result.Success = password.GetMD5().ToString().Equals(user.Password);
+ }
+
+ // Update LastActivityDate and LastLoginDate, then save
+ if (result.Success)
+ {
+ user.LastActivityDate = user.LastLoginDate = DateTime.UtcNow;
+ SaveUser(user);
+ }
+
+ return result;
+ }
+
+ /// <summary>
+ /// Finds a library item by Id
+ /// </summary>
+ public BaseItem GetItemById(Guid id)
+ {
+ if (id == Guid.Empty)
+ {
+ return RootFolder;
+ }
+
+ return RootFolder.FindItemById(id);
+ }
+
+ /// <summary>
+ /// Gets all users within the system
+ /// </summary>
+ private IEnumerable<User> GetAllUsers()
+ {
+ var list = new List<User>();
+
+ // Return a dummy user for now since all calls to get items requre a userId
+ var user = new User { };
+
+ user.Name = "Default User";
+ user.Id = Guid.Parse("5d1cf7fce25943b790d140095457a42b");
+ user.PrimaryImagePath = "D:\\Video\\TV\\Archer (2009)\\backdrop.jpg";
+ list.Add(user);
+
+ user = new User { };
+ user.Name = "Abobader";
+ user.Id = Guid.NewGuid();
+ user.LastLoginDate = DateTime.UtcNow.AddDays(-1);
+ user.LastActivityDate = DateTime.UtcNow.AddHours(-3);
+ user.Password = ("1234").GetMD5().ToString();
+ list.Add(user);
+
+ user = new User { };
+ user.Name = "Scottisafool";
+ user.Id = Guid.NewGuid();
+ list.Add(user);
+
+ user = new User { };
+ user.Name = "Redshirt";
+ user.Id = Guid.NewGuid();
+ list.Add(user);
+
+ /*user = new User();
+ user.Name = "Test User 4";
+ user.Id = Guid.NewGuid();
+ list.Add(user);
+
+ user = new User();
+ user.Name = "Test User 5";
+ user.Id = Guid.NewGuid();
+ list.Add(user);
+
+ user = new User();
+ user.Name = "Test User 6";
+ user.Id = Guid.NewGuid();
+ list.Add(user);*/
+
+ return list;
+ }
+
+ /// <summary>
+ /// Runs all metadata providers for an entity
+ /// </summary>
+ internal async Task ExecuteMetadataProviders(BaseEntity item, bool allowInternetProviders = true)
+ {
+ // Run them sequentially in order of priority
+ for (int i = 0; i < MetadataProviders.Length; i++)
+ {
+ var provider = MetadataProviders[i];
+
+ // Skip if internet providers are currently disabled
+ if (provider.RequiresInternet && (!Configuration.EnableInternetProviders || !allowInternetProviders))
+ {
+ continue;
+ }
+
+ // Skip if the provider doesn't support the current item
+ if (!provider.Supports(item))
+ {
+ continue;
+ }
+
+ try
+ {
+ await provider.FetchIfNeededAsync(item).ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ Logger.LogException(ex);
+ }
+ }
+ }
+
+ private void ExtractFFMpeg()
+ {
+ ExtractFFMpeg(ApplicationPaths.FFMpegPath);
+ ExtractFFMpeg(ApplicationPaths.FFProbePath);
+ }
+
+ /// <summary>
+ /// Run these during Init.
+ /// Can't run do this on-demand because there will be multiple workers accessing them at once and we'd have to lock them
+ /// </summary>
+ private void ExtractFFMpeg(string exe)
+ {
+ if (File.Exists(exe))
+ {
+ File.Delete(exe);
+ }
+
+ // Extract exe
+ using (Stream stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("MediaBrowser.Controller.FFMpeg." + Path.GetFileName(exe)))
+ {
+ using (var fileStream = new FileStream(exe, FileMode.Create))
+ {
+ stream.CopyTo(fileStream);
+ }
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Library/ChildrenChangedEventArgs.cs b/MediaBrowser.Controller/Library/ChildrenChangedEventArgs.cs
new file mode 100644
index 000000000..462fcc6d6
--- /dev/null
+++ b/MediaBrowser.Controller/Library/ChildrenChangedEventArgs.cs
@@ -0,0 +1,34 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Entities;
+
+namespace MediaBrowser.Controller.Library
+{
+ public class ChildrenChangedEventArgs : EventArgs
+ {
+ public Folder Folder { get; set; }
+ public List<BaseItem> ItemsAdded { get; set; }
+ public IEnumerable<BaseItem> ItemsRemoved { get; set; }
+
+ public ChildrenChangedEventArgs()
+ {
+ //initialize the list
+ ItemsAdded = new List<BaseItem>();
+ }
+
+ /// <summary>
+ /// Create the args and set the folder property
+ /// </summary>
+ /// <param name="folder"></param>
+ public ChildrenChangedEventArgs(Folder folder)
+ {
+ //init the folder property
+ this.Folder = folder;
+ //init the list
+ ItemsAdded = new List<BaseItem>();
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Library/ItemController.cs b/MediaBrowser.Controller/Library/ItemController.cs
new file mode 100644
index 000000000..54673e538
--- /dev/null
+++ b/MediaBrowser.Controller/Library/ItemController.cs
@@ -0,0 +1,136 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.IO;
+using MediaBrowser.Controller.Resolvers;
+using MediaBrowser.Common.Extensions;
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Library
+{
+ public class ItemController
+ {
+
+ /// <summary>
+ /// Resolves a path into a BaseItem
+ /// </summary>
+ public async Task<BaseItem> GetItem(string path, Folder parent = null, WIN32_FIND_DATA? fileInfo = null, bool allowInternetProviders = true)
+ {
+ var args = new ItemResolveEventArgs
+ {
+ FileInfo = fileInfo ?? FileData.GetFileData(path),
+ Parent = parent,
+ Cancel = false,
+ Path = path
+ };
+
+ // Gather child folder and files
+ if (args.IsDirectory)
+ {
+ args.FileSystemChildren = FileData.GetFileSystemEntries(path, "*").ToArray();
+
+ bool isVirtualFolder = parent != null && parent.IsRoot;
+ args = FileSystemHelper.FilterChildFileSystemEntries(args, isVirtualFolder);
+ }
+ else
+ {
+ args.FileSystemChildren = new WIN32_FIND_DATA[] { };
+ }
+
+
+ // Check to see if we should resolve based on our contents
+ if (!EntityResolutionHelper.ShouldResolvePathContents(args))
+ {
+ return null;
+ }
+
+ BaseItem item = Kernel.Instance.ResolveItem(args);
+
+ return item;
+ }
+
+ /// <summary>
+ /// Gets a Person
+ /// </summary>
+ public Task<Person> GetPerson(string name)
+ {
+ return GetImagesByNameItem<Person>(Kernel.Instance.ApplicationPaths.PeoplePath, name);
+ }
+
+ /// <summary>
+ /// Gets a Studio
+ /// </summary>
+ public Task<Studio> GetStudio(string name)
+ {
+ return GetImagesByNameItem<Studio>(Kernel.Instance.ApplicationPaths.StudioPath, name);
+ }
+
+ /// <summary>
+ /// Gets a Genre
+ /// </summary>
+ public Task<Genre> GetGenre(string name)
+ {
+ return GetImagesByNameItem<Genre>(Kernel.Instance.ApplicationPaths.GenrePath, name);
+ }
+
+ /// <summary>
+ /// Gets a Year
+ /// </summary>
+ public Task<Year> GetYear(int value)
+ {
+ return GetImagesByNameItem<Year>(Kernel.Instance.ApplicationPaths.YearPath, value.ToString());
+ }
+
+ private readonly ConcurrentDictionary<string, object> ImagesByNameItemCache = new ConcurrentDictionary<string, object>(StringComparer.OrdinalIgnoreCase);
+
+ /// <summary>
+ /// Generically retrieves an IBN item
+ /// </summary>
+ private Task<T> GetImagesByNameItem<T>(string path, string name)
+ where T : BaseEntity, new()
+ {
+ name = FileData.GetValidFilename(name);
+
+ path = Path.Combine(path, name);
+
+ // Look for it in the cache, if it's not there, create it
+ if (!ImagesByNameItemCache.ContainsKey(path))
+ {
+ ImagesByNameItemCache[path] = CreateImagesByNameItem<T>(path, name);
+ }
+
+ return ImagesByNameItemCache[path] as Task<T>;
+ }
+
+ /// <summary>
+ /// Creates an IBN item based on a given path
+ /// </summary>
+ private async Task<T> CreateImagesByNameItem<T>(string path, string name)
+ where T : BaseEntity, new()
+ {
+ var item = new T { };
+
+ item.Name = name;
+ item.Id = path.GetMD5();
+
+ if (!Directory.Exists(path))
+ {
+ Directory.CreateDirectory(path);
+ }
+
+ item.DateCreated = Directory.GetCreationTimeUtc(path);
+ item.DateModified = Directory.GetLastWriteTimeUtc(path);
+
+ var args = new ItemResolveEventArgs { };
+ args.FileInfo = FileData.GetFileData(path);
+ args.FileSystemChildren = FileData.GetFileSystemEntries(path, "*").ToArray();
+
+ await Kernel.Instance.ExecuteMetadataProviders(item).ConfigureAwait(false);
+
+ return item;
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Library/ItemResolveEventArgs.cs b/MediaBrowser.Controller/Library/ItemResolveEventArgs.cs
new file mode 100644
index 000000000..32b8783df
--- /dev/null
+++ b/MediaBrowser.Controller/Library/ItemResolveEventArgs.cs
@@ -0,0 +1,104 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.IO;
+using System.Collections.Generic;
+using System.Linq;
+using System;
+using System.IO;
+
+namespace MediaBrowser.Controller.Library
+{
+ /// <summary>
+ /// This is an EventArgs object used when resolving a Path into a BaseItem
+ /// </summary>
+ public class ItemResolveEventArgs : PreBeginResolveEventArgs
+ {
+ public WIN32_FIND_DATA[] FileSystemChildren { get; set; }
+
+ protected List<string> _additionalLocations = new List<string>();
+ public List<string> AdditionalLocations
+ {
+ get
+ {
+ return _additionalLocations;
+ }
+ set
+ {
+ _additionalLocations = value;
+ }
+ }
+
+ public IEnumerable<string> PhysicalLocations
+ {
+ get
+ {
+ return (new List<string>() {this.Path}).Concat(AdditionalLocations);
+ }
+ }
+
+ public bool IsBDFolder { get; set; }
+ public bool IsDVDFolder { get; set; }
+ public bool IsHDDVDFolder { get; set; }
+
+ /// <summary>
+ /// Store these to reduce disk access in Resolvers
+ /// </summary>
+ public string[] MetadataFiles { get; set; }
+
+ public WIN32_FIND_DATA? GetFileSystemEntry(string path)
+ {
+ WIN32_FIND_DATA entry = FileSystemChildren.FirstOrDefault(f => f.Path.Equals(path, StringComparison.OrdinalIgnoreCase));
+ return entry.cFileName != null ? (WIN32_FIND_DATA?)entry : null;
+ }
+
+ public bool ContainsFile(string name)
+ {
+ return FileSystemChildren.FirstOrDefault(f => f.cFileName.Equals(name, StringComparison.OrdinalIgnoreCase)).cFileName != null;
+ }
+
+ public bool ContainsFolder(string name)
+ {
+ return ContainsFile(name);
+ }
+ }
+
+ /// <summary>
+ /// This is an EventArgs object used before we begin resolving a Path into a BaseItem
+ /// File system children have not been collected yet, but consuming events will
+ /// have a chance to cancel resolution based on the Path, Parent and FileAttributes
+ /// </summary>
+ public class PreBeginResolveEventArgs : EventArgs
+ {
+ public Folder Parent { get; set; }
+
+ public bool Cancel { get; set; }
+
+ public WIN32_FIND_DATA FileInfo { get; set; }
+
+ public string Path { get; set; }
+
+ public bool IsDirectory
+ {
+ get
+ {
+ return FileInfo.dwFileAttributes.HasFlag(FileAttributes.Directory);
+ }
+ }
+
+ public bool IsHidden
+ {
+ get
+ {
+ return FileInfo.IsHidden;
+ }
+ }
+
+ public bool IsSystemFile
+ {
+ get
+ {
+ return FileInfo.IsSystemFile;
+ }
+ }
+
+ }
+}
diff --git a/MediaBrowser.Controller/MediaBrowser.Controller.csproj b/MediaBrowser.Controller/MediaBrowser.Controller.csproj
new file mode 100644
index 000000000..fc1e578e9
--- /dev/null
+++ b/MediaBrowser.Controller/MediaBrowser.Controller.csproj
@@ -0,0 +1,150 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
+ <PropertyGroup>
+ <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+ <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+ <ProjectGuid>{17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}</ProjectGuid>
+ <OutputType>Library</OutputType>
+ <AppDesignerFolder>Properties</AppDesignerFolder>
+ <RootNamespace>MediaBrowser.Controller</RootNamespace>
+ <AssemblyName>MediaBrowser.Controller</AssemblyName>
+ <TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
+ <FileAlignment>512</FileAlignment>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
+ <DebugSymbols>true</DebugSymbols>
+ <DebugType>full</DebugType>
+ <Optimize>false</Optimize>
+ <OutputPath>bin\Debug\</OutputPath>
+ <DefineConstants>DEBUG;TRACE</DefineConstants>
+ <ErrorReport>prompt</ErrorReport>
+ <WarningLevel>4</WarningLevel>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
+ <DebugType>pdbonly</DebugType>
+ <Optimize>true</Optimize>
+ <OutputPath>bin\Release\</OutputPath>
+ <DefineConstants>TRACE</DefineConstants>
+ <ErrorReport>prompt</ErrorReport>
+ <WarningLevel>4</WarningLevel>
+ </PropertyGroup>
+ <ItemGroup>
+ <Reference Include="protobuf-net">
+ <HintPath>..\protobuf-net\Full\net30\protobuf-net.dll</HintPath>
+ </Reference>
+ <Reference Include="System" />
+ <Reference Include="System.ComponentModel.Composition" />
+ <Reference Include="System.Core" />
+ <Reference Include="System.Drawing" />
+ <Reference Include="System.Net.Http" />
+ <Reference Include="System.Net.Http.WebRequest" />
+ <Reference Include="System.Reactive.Core, Version=2.0.20823.0, Culture=neutral, PublicKeyToken=f300afd708cefcd3, processorArchitecture=MSIL">
+ <SpecificVersion>False</SpecificVersion>
+ <HintPath>..\packages\Rx-Core.2.0.20823\lib\Net45\System.Reactive.Core.dll</HintPath>
+ </Reference>
+ <Reference Include="System.Reactive.Interfaces, Version=2.0.20823.0, Culture=neutral, PublicKeyToken=f300afd708cefcd3, processorArchitecture=MSIL">
+ <SpecificVersion>False</SpecificVersion>
+ <HintPath>..\packages\Rx-Interfaces.2.0.20823\lib\Net45\System.Reactive.Interfaces.dll</HintPath>
+ </Reference>
+ <Reference Include="System.Reactive.Linq, Version=2.0.20823.0, Culture=neutral, PublicKeyToken=f300afd708cefcd3, processorArchitecture=MSIL">
+ <SpecificVersion>False</SpecificVersion>
+ <HintPath>..\packages\Rx-Linq.2.0.20823\lib\Net45\System.Reactive.Linq.dll</HintPath>
+ </Reference>
+ <Reference Include="System.Runtime.Serialization" />
+ <Reference Include="System.Xml.Linq" />
+ <Reference Include="System.Data.DataSetExtensions" />
+ <Reference Include="Microsoft.CSharp" />
+ <Reference Include="System.Data" />
+ <Reference Include="System.Xml" />
+ </ItemGroup>
+ <ItemGroup>
+ <Compile Include="Drawing\DrawingUtils.cs" />
+ <Compile Include="Drawing\ImageProcessor.cs" />
+ <Compile Include="Entities\Audio.cs" />
+ <Compile Include="Entities\BaseEntity.cs" />
+ <Compile Include="Entities\BaseItem.cs" />
+ <Compile Include="Entities\Folder.cs" />
+ <Compile Include="Entities\Genre.cs" />
+ <Compile Include="Entities\Movies\BoxSet.cs" />
+ <Compile Include="Entities\Movies\Movie.cs" />
+ <Compile Include="Entities\Person.cs" />
+ <Compile Include="Entities\Studio.cs" />
+ <Compile Include="Entities\TV\Episode.cs" />
+ <Compile Include="Entities\TV\Season.cs" />
+ <Compile Include="Entities\TV\Series.cs" />
+ <Compile Include="Entities\User.cs" />
+ <Compile Include="Entities\UserItemData.cs" />
+ <Compile Include="Entities\Video.cs" />
+ <Compile Include="Entities\Year.cs" />
+ <Compile Include="IO\FileSystemHelper.cs" />
+ <Compile Include="Library\ChildrenChangedEventArgs.cs" />
+ <Compile Include="Providers\BaseProviderInfo.cs" />
+ <Compile Include="Providers\Movies\MovieProviderFromXml.cs" />
+ <Compile Include="Providers\Movies\MovieSpecialFeaturesProvider.cs" />
+ <Compile Include="Providers\TV\EpisodeImageFromMediaLocationProvider.cs" />
+ <Compile Include="Providers\TV\EpisodeProviderFromXml.cs" />
+ <Compile Include="Providers\TV\EpisodeXmlParser.cs" />
+ <Compile Include="Providers\TV\SeriesProviderFromXml.cs" />
+ <Compile Include="Providers\TV\SeriesXmlParser.cs" />
+ <Compile Include="Resolvers\EntityResolutionHelper.cs" />
+ <Compile Include="Resolvers\Movies\BoxSetResolver.cs" />
+ <Compile Include="Resolvers\Movies\MovieResolver.cs" />
+ <Compile Include="Resolvers\TV\EpisodeResolver.cs" />
+ <Compile Include="Resolvers\TV\SeasonResolver.cs" />
+ <Compile Include="Resolvers\TV\SeriesResolver.cs" />
+ <Compile Include="Resolvers\TV\TVUtils.cs" />
+ <Compile Include="ServerApplicationPaths.cs" />
+ <Compile Include="Library\ItemResolveEventArgs.cs" />
+ <Compile Include="FFMpeg\FFProbe.cs" />
+ <Compile Include="FFMpeg\FFProbeResult.cs" />
+ <Compile Include="IO\DirectoryWatchers.cs" />
+ <Compile Include="IO\FileData.cs" />
+ <Compile Include="IO\Shortcut.cs" />
+ <Compile Include="Library\ItemController.cs" />
+ <Compile Include="Kernel.cs" />
+ <Compile Include="Properties\AssemblyInfo.cs" />
+ <Compile Include="Providers\BaseMetadataProvider.cs" />
+ <Compile Include="Providers\AudioInfoProvider.cs" />
+ <Compile Include="Providers\FolderProviderFromXml.cs" />
+ <Compile Include="Providers\ImageFromMediaLocationProvider.cs" />
+ <Compile Include="Providers\LocalTrailerProvider.cs" />
+ <Compile Include="Providers\VideoInfoProvider.cs" />
+ <Compile Include="Resolvers\AudioResolver.cs" />
+ <Compile Include="Resolvers\BaseItemResolver.cs" />
+ <Compile Include="Resolvers\FolderResolver.cs" />
+ <Compile Include="Resolvers\VideoResolver.cs" />
+ <Compile Include="Weather\BaseWeatherProvider.cs" />
+ <Compile Include="Weather\WeatherProvider.cs" />
+ <Compile Include="Providers\BaseItemXmlParser.cs" />
+ <Compile Include="Xml\XmlExtensions.cs" />
+ </ItemGroup>
+ <ItemGroup>
+ <ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj">
+ <Project>{9142eefa-7570-41e1-bfcc-468bb571af2f}</Project>
+ <Name>MediaBrowser.Common</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj">
+ <Project>{7eeeb4bb-f3e8-48fc-b4c5-70f0fff8329b}</Project>
+ <Name>MediaBrowser.Model</Name>
+ </ProjectReference>
+ </ItemGroup>
+ <ItemGroup>
+ <None Include="packages.config" />
+ </ItemGroup>
+ <ItemGroup>
+ <EmbeddedResource Include="FFMpeg\ffmpeg.exe" />
+ </ItemGroup>
+ <ItemGroup>
+ <EmbeddedResource Include="FFMpeg\ffprobe.exe" />
+ <Content Include="FFMpeg\readme.txt" />
+ </ItemGroup>
+ <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
+ <!-- To modify your build process, add your task inside one of the targets below and uncomment it.
+ Other similar extension points exist, see Microsoft.Common.targets.
+ <Target Name="BeforeBuild">
+ </Target>
+ <Target Name="AfterBuild">
+ </Target>
+ -->
+</Project> \ No newline at end of file
diff --git a/MediaBrowser.Controller/Properties/AssemblyInfo.cs b/MediaBrowser.Controller/Properties/AssemblyInfo.cs
new file mode 100644
index 000000000..63cc65d7a
--- /dev/null
+++ b/MediaBrowser.Controller/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.Controller")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("MediaBrowser.Controller")]
+[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("bc09905a-04ed-497d-b39b-27593401e715")]
+
+// 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.Controller/Providers/AudioInfoProvider.cs b/MediaBrowser.Controller/Providers/AudioInfoProvider.cs
new file mode 100644
index 000000000..302902646
--- /dev/null
+++ b/MediaBrowser.Controller/Providers/AudioInfoProvider.cs
@@ -0,0 +1,262 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.FFMpeg;
+using MediaBrowser.Controller.Library;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.Composition;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Providers
+{
+ [Export(typeof(BaseMetadataProvider))]
+ public class AudioInfoProvider : BaseMediaInfoProvider<Audio>
+ {
+ public override MetadataProviderPriority Priority
+ {
+ get { return MetadataProviderPriority.First; }
+ }
+
+ protected override string CacheDirectory
+ {
+ get { return Kernel.Instance.ApplicationPaths.FFProbeAudioCacheDirectory; }
+ }
+
+ protected override void Fetch(Audio audio, FFProbeResult data)
+ {
+ MediaStream stream = data.streams.First(s => s.codec_type.Equals("audio", StringComparison.OrdinalIgnoreCase));
+
+ audio.Channels = stream.channels;
+
+ if (!string.IsNullOrEmpty(stream.sample_rate))
+ {
+ audio.SampleRate = int.Parse(stream.sample_rate);
+ }
+
+ string bitrate = stream.bit_rate;
+ string duration = stream.duration;
+
+ if (string.IsNullOrEmpty(bitrate))
+ {
+ bitrate = data.format.bit_rate;
+ }
+
+ if (string.IsNullOrEmpty(duration))
+ {
+ duration = data.format.duration;
+ }
+
+ if (!string.IsNullOrEmpty(bitrate))
+ {
+ audio.BitRate = int.Parse(bitrate);
+ }
+
+ if (!string.IsNullOrEmpty(duration))
+ {
+ audio.RunTimeTicks = TimeSpan.FromSeconds(double.Parse(duration)).Ticks;
+ }
+
+ if (data.format.tags != null)
+ {
+ FetchDataFromTags(audio, data.format.tags);
+ }
+ }
+
+ private void FetchDataFromTags(Audio audio, Dictionary<string, string> tags)
+ {
+ string title = GetDictionaryValue(tags, "title");
+
+ if (!string.IsNullOrEmpty(title))
+ {
+ audio.Name = title;
+ }
+
+ string composer = GetDictionaryValue(tags, "composer");
+
+ if (!string.IsNullOrEmpty(composer))
+ {
+ audio.AddPerson(new PersonInfo { Name = composer, Type = "Composer" });
+ }
+
+ audio.Album = GetDictionaryValue(tags, "album");
+ audio.Artist = GetDictionaryValue(tags, "artist");
+ audio.AlbumArtist = GetDictionaryValue(tags, "albumartist") ?? GetDictionaryValue(tags, "album artist") ?? GetDictionaryValue(tags, "album_artist");
+
+ audio.IndexNumber = GetDictionaryNumericValue(tags, "track");
+ audio.ParentIndexNumber = GetDictionaryDiscValue(tags);
+
+ audio.Language = GetDictionaryValue(tags, "language");
+
+ audio.ProductionYear = GetDictionaryNumericValue(tags, "date");
+
+ audio.PremiereDate = GetDictionaryDateTime(tags, "retaildate") ?? GetDictionaryDateTime(tags, "retail date") ?? GetDictionaryDateTime(tags, "retail_date");
+
+ FetchGenres(audio, tags);
+
+ FetchStudios(audio, tags, "organization");
+ FetchStudios(audio, tags, "ensemble");
+ FetchStudios(audio, tags, "publisher");
+ }
+
+ private void FetchStudios(Audio audio, Dictionary<string, string> tags, string tagName)
+ {
+ string val = GetDictionaryValue(tags, tagName);
+
+ if (!string.IsNullOrEmpty(val))
+ {
+ var list = audio.Studios ?? new List<string>();
+ list.AddRange(val.Split('/'));
+ audio.Studios = list;
+ }
+ }
+
+ private void FetchGenres(Audio audio, Dictionary<string, string> tags)
+ {
+ string val = GetDictionaryValue(tags, "genre");
+
+ if (!string.IsNullOrEmpty(val))
+ {
+ var list = audio.Genres ?? new List<string>();
+ list.AddRange(val.Split('/'));
+ audio.Genres = list;
+ }
+ }
+
+ private int? GetDictionaryDiscValue(Dictionary<string, string> tags)
+ {
+ string disc = GetDictionaryValue(tags, "disc");
+
+ if (!string.IsNullOrEmpty(disc))
+ {
+ disc = disc.Split('/')[0];
+
+ int num;
+
+ if (int.TryParse(disc, out num))
+ {
+ return num;
+ }
+ }
+
+ return null;
+ }
+ }
+
+ public abstract class BaseMediaInfoProvider<T> : BaseMetadataProvider
+ where T : BaseItem
+ {
+ protected abstract string CacheDirectory { get; }
+
+ public override bool Supports(BaseEntity item)
+ {
+ return item is T;
+ }
+
+ public override async Task FetchAsync(BaseEntity item, ItemResolveEventArgs args)
+ {
+ await Task.Run(() =>
+ {
+ /*T myItem = item as T;
+
+ if (CanSkipFFProbe(myItem))
+ {
+ return;
+ }
+
+ FFProbeResult result = FFProbe.Run(myItem, CacheDirectory);
+
+ if (result == null)
+ {
+ Logger.LogInfo("Null FFProbeResult for {0} {1}", item.Id, item.Name);
+ return;
+ }
+
+ if (result.format != null && result.format.tags != null)
+ {
+ result.format.tags = ConvertDictionaryToCaseInSensitive(result.format.tags);
+ }
+
+ if (result.streams != null)
+ {
+ foreach (MediaStream stream in result.streams)
+ {
+ if (stream.tags != null)
+ {
+ stream.tags = ConvertDictionaryToCaseInSensitive(stream.tags);
+ }
+ }
+ }
+
+ Fetch(myItem, result);*/
+ });
+ }
+
+ protected abstract void Fetch(T item, FFProbeResult result);
+
+ protected virtual bool CanSkipFFProbe(T item)
+ {
+ return false;
+ }
+
+ protected string GetDictionaryValue(Dictionary<string, string> tags, string key)
+ {
+ if (tags == null)
+ {
+ return null;
+ }
+
+ if (!tags.ContainsKey(key))
+ {
+ return null;
+ }
+
+ return tags[key];
+ }
+
+ protected int? GetDictionaryNumericValue(Dictionary<string, string> tags, string key)
+ {
+ string val = GetDictionaryValue(tags, key);
+
+ if (!string.IsNullOrEmpty(val))
+ {
+ int i;
+
+ if (int.TryParse(val, out i))
+ {
+ return i;
+ }
+ }
+
+ return null;
+ }
+
+ protected DateTime? GetDictionaryDateTime(Dictionary<string, string> tags, string key)
+ {
+ string val = GetDictionaryValue(tags, key);
+
+ if (!string.IsNullOrEmpty(val))
+ {
+ DateTime i;
+
+ if (DateTime.TryParse(val, out i))
+ {
+ return i.ToUniversalTime();
+ }
+ }
+
+ return null;
+ }
+
+ private Dictionary<string, string> ConvertDictionaryToCaseInSensitive(Dictionary<string, string> dict)
+ {
+ var newDict = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+
+ foreach (string key in dict.Keys)
+ {
+ newDict[key] = dict[key];
+ }
+
+ return newDict;
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Providers/BaseItemXmlParser.cs b/MediaBrowser.Controller/Providers/BaseItemXmlParser.cs
new file mode 100644
index 000000000..38afb2b52
--- /dev/null
+++ b/MediaBrowser.Controller/Providers/BaseItemXmlParser.cs
@@ -0,0 +1,724 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Xml;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Common.Logging;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Xml;
+
+namespace MediaBrowser.Controller.Providers
+{
+ /// <summary>
+ /// Provides a base class for parsing metadata xml
+ /// </summary>
+ public class BaseItemXmlParser<T>
+ where T : BaseItem, new()
+ {
+ /// <summary>
+ /// Fetches metadata for an item from one xml file
+ /// </summary>
+ public void Fetch(T item, string metadataFile)
+ {
+ // Use XmlReader for best performance
+ using (XmlReader reader = XmlReader.Create(metadataFile))
+ {
+ reader.MoveToContent();
+
+ // Loop through each element
+ while (reader.Read())
+ {
+ if (reader.NodeType == XmlNodeType.Element)
+ {
+ FetchDataFromXmlNode(reader, item);
+ }
+ }
+ }
+ }
+
+ /// <summary>
+ /// Fetches metadata from one Xml Element
+ /// </summary>
+ protected virtual void FetchDataFromXmlNode(XmlReader reader, T item)
+ {
+ switch (reader.Name)
+ {
+ // DateCreated
+ case "Added":
+ DateTime added;
+ if (DateTime.TryParse(reader.ReadElementContentAsString() ?? string.Empty, out added))
+ {
+ item.DateCreated = added.ToUniversalTime();
+ }
+ break;
+
+ // DisplayMediaType
+ case "Type":
+ {
+ item.DisplayMediaType = reader.ReadElementContentAsString();
+
+ switch (item.DisplayMediaType.ToLower())
+ {
+ case "blu-ray":
+ item.DisplayMediaType = VideoType.BluRay.ToString();
+ break;
+ case "dvd":
+ item.DisplayMediaType = VideoType.Dvd.ToString();
+ break;
+ case "":
+ item.DisplayMediaType = null;
+ break;
+ }
+
+ break;
+ }
+
+ // TODO: Do we still need this?
+ case "banner":
+ item.BannerImagePath = reader.ReadElementContentAsString();
+ break;
+
+ case "LocalTitle":
+ item.Name = reader.ReadElementContentAsString();
+ break;
+
+ case "SortTitle":
+ item.SortName = reader.ReadElementContentAsString();
+ break;
+
+ case "Overview":
+ case "Description":
+ item.Overview = reader.ReadElementContentAsString();
+ break;
+
+ case "TagLine":
+ {
+ var list = item.Taglines ?? new List<string>();
+ var tagline = reader.ReadElementContentAsString();
+
+ if (!list.Contains(tagline))
+ {
+ list.Add(tagline);
+ }
+
+ item.Taglines = list;
+ break;
+ }
+
+ case "TagLines":
+ {
+ FetchFromTaglinesNode(reader.ReadSubtree(), item);
+ break;
+ }
+
+ case "ContentRating":
+ case "MPAARating":
+ item.OfficialRating = reader.ReadElementContentAsString();
+ break;
+
+ case "CustomRating":
+ item.CustomRating = reader.ReadElementContentAsString();
+ break;
+
+ case "CustomPin":
+ item.CustomPin = reader.ReadElementContentAsString();
+ break;
+
+ case "Runtime":
+ case "RunningTime":
+ {
+ string text = reader.ReadElementContentAsString();
+
+ if (!string.IsNullOrWhiteSpace(text))
+ {
+ int runtime;
+ if (int.TryParse(text.Split(' ')[0], out runtime))
+ {
+ item.RunTimeTicks = TimeSpan.FromMinutes(runtime).Ticks;
+ }
+ }
+ break;
+ }
+
+ case "Genre":
+ {
+ var list = item.Genres ?? new List<string>();
+ list.AddRange(GetSplitValues(reader.ReadElementContentAsString(), '|'));
+
+ item.Genres = list;
+ break;
+ }
+
+ case "AspectRatio":
+ item.AspectRatio = reader.ReadElementContentAsString();
+ break;
+
+ case "Network":
+ {
+ var list = item.Studios ?? new List<string>();
+ list.AddRange(GetSplitValues(reader.ReadElementContentAsString(), '|'));
+
+ item.Studios = list;
+ break;
+ }
+
+ case "Director":
+ {
+ foreach (PersonInfo p in GetSplitValues(reader.ReadElementContentAsString(), '|').Select(v => new PersonInfo { Name = v, Type = "Director" }))
+ {
+ item.AddPerson(p);
+ }
+ break;
+ }
+ case "Writer":
+ {
+ foreach (PersonInfo p in GetSplitValues(reader.ReadElementContentAsString(), '|').Select(v => new PersonInfo { Name = v, Type = "Writer" }))
+ {
+ item.AddPerson(p);
+ }
+ break;
+ }
+
+ case "Actors":
+ case "GuestStars":
+ {
+ foreach (PersonInfo p in GetSplitValues(reader.ReadElementContentAsString(), '|').Select(v => new PersonInfo { Name = v, Type = "Actor" }))
+ {
+ item.AddPerson(p);
+ }
+ break;
+ }
+
+ case "Trailer":
+ item.TrailerUrl = reader.ReadElementContentAsString();
+ break;
+
+ case "ProductionYear":
+ {
+ string val = reader.ReadElementContentAsString();
+
+ if (!string.IsNullOrWhiteSpace(val))
+ {
+ int ProductionYear;
+ if (int.TryParse(val, out ProductionYear) && ProductionYear > 1850)
+ {
+ item.ProductionYear = ProductionYear;
+ }
+ }
+
+ break;
+ }
+
+ case "Rating":
+ case "IMDBrating":
+
+ string rating = reader.ReadElementContentAsString();
+
+ if (!string.IsNullOrWhiteSpace(rating))
+ {
+ float val;
+
+ if (float.TryParse(rating, out val))
+ {
+ item.CommunityRating = val;
+ }
+ }
+ break;
+
+ case "FirstAired":
+ {
+ string firstAired = reader.ReadElementContentAsString();
+
+ if (!string.IsNullOrWhiteSpace(firstAired))
+ {
+ DateTime airDate;
+
+ if (DateTime.TryParse(firstAired, out airDate) && airDate.Year > 1850)
+ {
+ item.PremiereDate = airDate.ToUniversalTime();
+ item.ProductionYear = airDate.Year;
+ }
+ }
+
+ break;
+ }
+
+ case "TMDbId":
+ string tmdb = reader.ReadElementContentAsString();
+ if (!string.IsNullOrWhiteSpace(tmdb))
+ {
+ item.SetProviderId(MetadataProviders.Tmdb, tmdb);
+ }
+ break;
+
+ case "TVcomId":
+ string TVcomId = reader.ReadElementContentAsString();
+ if (!string.IsNullOrWhiteSpace(TVcomId))
+ {
+ item.SetProviderId(MetadataProviders.Tvcom, TVcomId);
+ }
+ break;
+
+ case "IMDB_ID":
+ case "IMDB":
+ case "IMDbId":
+ string IMDbId = reader.ReadElementContentAsString();
+ if (!string.IsNullOrWhiteSpace(IMDbId))
+ {
+ item.SetProviderId(MetadataProviders.Imdb, IMDbId);
+ }
+ break;
+
+ case "Genres":
+ FetchFromGenresNode(reader.ReadSubtree(), item);
+ break;
+
+ case "Persons":
+ FetchDataFromPersonsNode(reader.ReadSubtree(), item);
+ break;
+
+ case "ParentalRating":
+ FetchFromParentalRatingNode(reader.ReadSubtree(), item);
+ break;
+
+ case "Studios":
+ FetchFromStudiosNode(reader.ReadSubtree(), item);
+ break;
+
+ case "MediaInfo":
+ {
+ var video = item as Video;
+
+ if (video != null)
+ {
+ FetchMediaInfo(reader.ReadSubtree(), video);
+ }
+ break;
+ }
+
+ default:
+ reader.Skip();
+ break;
+ }
+ }
+
+ private void FetchMediaInfo(XmlReader reader, Video item)
+ {
+ reader.MoveToContent();
+
+ while (reader.Read())
+ {
+ if (reader.NodeType == XmlNodeType.Element)
+ {
+ switch (reader.Name)
+ {
+ case "Audio":
+ {
+ AudioStream stream = FetchMediaInfoAudio(reader.ReadSubtree());
+
+ List<AudioStream> streams = item.AudioStreams ?? new List<AudioStream>();
+ streams.Add(stream);
+ item.AudioStreams = streams;
+
+ break;
+ }
+
+ case "Video":
+ FetchMediaInfoVideo(reader.ReadSubtree(), item);
+ break;
+
+ case "Subtitle":
+ {
+ SubtitleStream stream = FetchMediaInfoSubtitles(reader.ReadSubtree());
+
+ List<SubtitleStream> streams = item.Subtitles ?? new List<SubtitleStream>();
+ streams.Add(stream);
+ item.Subtitles = streams;
+
+ break;
+ }
+
+ default:
+ reader.Skip();
+ break;
+ }
+ }
+ }
+ }
+
+ private AudioStream FetchMediaInfoAudio(XmlReader reader)
+ {
+ var stream = new AudioStream();
+
+ reader.MoveToContent();
+
+ while (reader.Read())
+ {
+ if (reader.NodeType == XmlNodeType.Element)
+ {
+ switch (reader.Name)
+ {
+ case "Default":
+ stream.IsDefault = reader.ReadElementContentAsString() == "True";
+ break;
+
+ case "SamplingRate":
+ stream.SampleRate = reader.ReadIntSafe();
+ break;
+
+ case "BitRate":
+ stream.BitRate = reader.ReadIntSafe();
+ break;
+
+ case "Channels":
+ stream.Channels = reader.ReadIntSafe();
+ break;
+
+ case "Language":
+ stream.Language = reader.ReadElementContentAsString();
+ break;
+
+ case "Codec":
+ stream.Codec = reader.ReadElementContentAsString();
+ break;
+
+ default:
+ reader.Skip();
+ break;
+ }
+ }
+ }
+
+ return stream;
+ }
+
+ private void FetchMediaInfoVideo(XmlReader reader, Video item)
+ {
+ reader.MoveToContent();
+
+ while (reader.Read())
+ {
+ if (reader.NodeType == XmlNodeType.Element)
+ {
+ switch (reader.Name)
+ {
+ case "Width":
+ item.Width = reader.ReadIntSafe();
+ break;
+
+ case "Height":
+ item.Height = reader.ReadIntSafe();
+ break;
+
+ case "BitRate":
+ item.BitRate = reader.ReadIntSafe();
+ break;
+
+ case "FrameRate":
+ item.FrameRate = reader.ReadFloatSafe();
+ break;
+
+ case "ScanType":
+ item.ScanType = reader.ReadElementContentAsString();
+ break;
+
+ case "Duration":
+ item.RunTimeTicks = TimeSpan.FromMinutes(reader.ReadIntSafe()).Ticks;
+ break;
+
+ case "DurationSeconds":
+ int seconds = reader.ReadIntSafe();
+ if (seconds > 0)
+ {
+ item.RunTimeTicks = TimeSpan.FromSeconds(seconds).Ticks;
+ }
+ break;
+
+ case "Codec":
+ {
+ string videoCodec = reader.ReadElementContentAsString();
+
+ switch (videoCodec.ToLower())
+ {
+ case "sorenson h.263":
+ item.Codec = "Sorenson H263";
+ break;
+ case "h.262":
+ item.Codec = "MPEG-2 Video";
+ break;
+ case "h.264":
+ item.Codec = "AVC";
+ break;
+ default:
+ item.Codec = videoCodec;
+ break;
+ }
+
+ break;
+ }
+
+ default:
+ reader.Skip();
+ break;
+ }
+ }
+ }
+ }
+
+ private SubtitleStream FetchMediaInfoSubtitles(XmlReader reader)
+ {
+ var stream = new SubtitleStream();
+
+ reader.MoveToContent();
+
+ while (reader.Read())
+ {
+ if (reader.NodeType == XmlNodeType.Element)
+ {
+ switch (reader.Name)
+ {
+ case "Language":
+ stream.Language = reader.ReadElementContentAsString();
+ break;
+
+ case "Default":
+ stream.IsDefault = reader.ReadElementContentAsString() == "True";
+ break;
+
+ case "Forced":
+ stream.IsForced = reader.ReadElementContentAsString() == "True";
+ break;
+
+ default:
+ reader.Skip();
+ break;
+ }
+ }
+ }
+
+ return stream;
+ }
+
+ private void FetchFromTaglinesNode(XmlReader reader, T item)
+ {
+ var list = item.Taglines ?? new List<string>();
+
+ reader.MoveToContent();
+
+ while (reader.Read())
+ {
+ if (reader.NodeType == XmlNodeType.Element)
+ {
+ switch (reader.Name)
+ {
+ case "Tagline":
+ {
+ string val = reader.ReadElementContentAsString();
+
+ if (!string.IsNullOrWhiteSpace(val) && !list.Contains(val))
+ {
+ list.Add(val);
+ }
+ break;
+ }
+
+ default:
+ reader.Skip();
+ break;
+ }
+ }
+ }
+
+ item.Taglines = list;
+ }
+
+ private void FetchFromGenresNode(XmlReader reader, T item)
+ {
+ var list = item.Genres ?? new List<string>();
+
+ reader.MoveToContent();
+
+ while (reader.Read())
+ {
+ if (reader.NodeType == XmlNodeType.Element)
+ {
+ switch (reader.Name)
+ {
+ case "Genre":
+ {
+ string genre = reader.ReadElementContentAsString();
+
+ if (!string.IsNullOrWhiteSpace(genre))
+ {
+ list.Add(genre);
+ }
+ break;
+ }
+
+ default:
+ reader.Skip();
+ break;
+ }
+ }
+ }
+
+ item.Genres = list;
+ }
+
+ private void FetchDataFromPersonsNode(XmlReader reader, T item)
+ {
+ reader.MoveToContent();
+
+ while (reader.Read())
+ {
+ if (reader.NodeType == XmlNodeType.Element)
+ {
+ switch (reader.Name)
+ {
+ case "Person":
+ {
+ item.AddPerson(GetPersonFromXmlNode(reader.ReadSubtree()));
+ break;
+ }
+
+ default:
+ reader.Skip();
+ break;
+ }
+ }
+ }
+ }
+
+ private void FetchFromStudiosNode(XmlReader reader, T item)
+ {
+ var list = item.Studios ?? new List<string>();
+
+ reader.MoveToContent();
+
+ while (reader.Read())
+ {
+ if (reader.NodeType == XmlNodeType.Element)
+ {
+ switch (reader.Name)
+ {
+ case "Studio":
+ {
+ string studio = reader.ReadElementContentAsString();
+
+ if (!string.IsNullOrWhiteSpace(studio))
+ {
+ list.Add(studio);
+ }
+ break;
+ }
+
+ default:
+ reader.Skip();
+ break;
+ }
+ }
+ }
+
+ item.Studios = list;
+ }
+
+ private void FetchFromParentalRatingNode(XmlReader reader, T item)
+ {
+ reader.MoveToContent();
+
+ while (reader.Read())
+ {
+ if (reader.NodeType == XmlNodeType.Element)
+ {
+ switch (reader.Name)
+ {
+ case "Value":
+ {
+ string ratingString = reader.ReadElementContentAsString();
+
+ int rating = 7;
+
+ if (!string.IsNullOrWhiteSpace(ratingString))
+ {
+ int.TryParse(ratingString, out rating);
+ }
+
+ switch (rating)
+ {
+ case -1:
+ item.OfficialRating = "NR";
+ break;
+ case 0:
+ item.OfficialRating = "UR";
+ break;
+ case 1:
+ item.OfficialRating = "G";
+ break;
+ case 3:
+ item.OfficialRating = "PG";
+ break;
+ case 4:
+ item.OfficialRating = "PG-13";
+ break;
+ case 5:
+ item.OfficialRating = "NC-17";
+ break;
+ case 6:
+ item.OfficialRating = "R";
+ break;
+ default:
+ break;
+ }
+ break;
+ }
+
+ default:
+ reader.Skip();
+ break;
+ }
+ }
+ }
+ }
+
+ private PersonInfo GetPersonFromXmlNode(XmlReader reader)
+ {
+ var person = new PersonInfo();
+
+ reader.MoveToContent();
+
+ while (reader.Read())
+ {
+ if (reader.NodeType == XmlNodeType.Element)
+ {
+ switch (reader.Name)
+ {
+ case "Name":
+ person.Name = reader.ReadElementContentAsString();
+ break;
+
+ case "Type":
+ person.Type = reader.ReadElementContentAsString();
+ break;
+
+ case "Role":
+ person.Overview = reader.ReadElementContentAsString();
+ break;
+
+ default:
+ reader.Skip();
+ break;
+ }
+ }
+ }
+
+ return person;
+ }
+
+ protected IEnumerable<string> GetSplitValues(string value, char deliminator)
+ {
+ value = (value ?? string.Empty).Trim(deliminator);
+
+ return string.IsNullOrWhiteSpace(value) ? new string[] { } : value.Split(deliminator);
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Providers/BaseMetadataProvider.cs b/MediaBrowser.Controller/Providers/BaseMetadataProvider.cs
new file mode 100644
index 000000000..50004be44
--- /dev/null
+++ b/MediaBrowser.Controller/Providers/BaseMetadataProvider.cs
@@ -0,0 +1,104 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Common.Extensions;
+using System.Threading.Tasks;
+using System;
+
+namespace MediaBrowser.Controller.Providers
+{
+ public abstract class BaseMetadataProvider
+ {
+ protected Guid _id;
+ public virtual Guid Id
+ {
+ get
+ {
+ if (_id == null) _id = this.GetType().FullName.GetMD5();
+ return _id;
+ }
+ }
+
+ public abstract bool Supports(BaseEntity item);
+
+ public virtual bool RequiresInternet
+ {
+ get
+ {
+ return false;
+ }
+ }
+
+ /// <summary>
+ /// Returns the last refresh time of this provider for this item. Providers that care should
+ /// call SetLastRefreshed to update this value.
+ /// </summary>
+ /// <param name="item"></param>
+ /// <returns></returns>
+ protected virtual DateTime LastRefreshed(BaseEntity item)
+ {
+ return (item.ProviderData.GetValueOrDefault(this.Id, new BaseProviderInfo())).LastRefreshed;
+ }
+
+ /// <summary>
+ /// Sets the persisted last refresh date on the item for this provider.
+ /// </summary>
+ /// <param name="item"></param>
+ /// <param name="value"></param>
+ protected virtual void SetLastRefreshed(BaseEntity item, DateTime value)
+ {
+ var data = item.ProviderData.GetValueOrDefault(this.Id, new BaseProviderInfo());
+ data.LastRefreshed = value;
+ item.ProviderData[this.Id] = data;
+ }
+
+ /// <summary>
+ /// Returns whether or not this provider should be re-fetched. Default functionality can
+ /// compare a provided date with a last refresh time. This can be overridden for more complex
+ /// determinations.
+ /// </summary>
+ /// <returns></returns>
+ public virtual bool NeedsRefresh(BaseEntity item)
+ {
+ return CompareDate(item) > LastRefreshed(item);
+ }
+
+ /// <summary>
+ /// Override this to return the date that should be compared to the last refresh date
+ /// to determine if this provider should be re-fetched.
+ /// </summary>
+ protected virtual DateTime CompareDate(BaseEntity item)
+ {
+ return DateTime.MinValue.AddMinutes(1); // want this to be greater than mindate so new items will refresh
+ }
+
+ public virtual Task FetchIfNeededAsync(BaseEntity item)
+ {
+ if (this.NeedsRefresh(item))
+ return FetchAsync(item, item.ResolveArgs);
+ else
+ return new Task(() => { });
+ }
+
+ public abstract Task FetchAsync(BaseEntity item, ItemResolveEventArgs args);
+
+ public abstract MetadataProviderPriority Priority { get; }
+ }
+
+ /// <summary>
+ /// Determines when a provider should execute, relative to others
+ /// </summary>
+ public enum MetadataProviderPriority
+ {
+ // Run this provider at the beginning
+ First = 1,
+
+ // Run this provider after all first priority providers
+ Second = 2,
+
+ // Run this provider after all second priority providers
+ Third = 3,
+
+ // Run this provider last
+ Last = 4
+ }
+}
diff --git a/MediaBrowser.Controller/Providers/BaseProviderInfo.cs b/MediaBrowser.Controller/Providers/BaseProviderInfo.cs
new file mode 100644
index 000000000..1538b2262
--- /dev/null
+++ b/MediaBrowser.Controller/Providers/BaseProviderInfo.cs
@@ -0,0 +1,15 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Providers
+{
+ public class BaseProviderInfo
+ {
+ public Guid ProviderId { get; set; }
+ public DateTime LastRefreshed { get; set; }
+
+ }
+}
diff --git a/MediaBrowser.Controller/Providers/FolderProviderFromXml.cs b/MediaBrowser.Controller/Providers/FolderProviderFromXml.cs
new file mode 100644
index 000000000..b7d9b7189
--- /dev/null
+++ b/MediaBrowser.Controller/Providers/FolderProviderFromXml.cs
@@ -0,0 +1,38 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using System.ComponentModel.Composition;
+using System.IO;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Providers
+{
+ /// <summary>
+ /// Provides metadata for Folders and all subclasses by parsing folder.xml
+ /// </summary>
+ [Export(typeof(BaseMetadataProvider))]
+ public class FolderProviderFromXml : BaseMetadataProvider
+ {
+ public override bool Supports(BaseEntity item)
+ {
+ return item is Folder;
+ }
+
+ public override MetadataProviderPriority Priority
+ {
+ get { return MetadataProviderPriority.First; }
+ }
+
+ public async override Task FetchAsync(BaseEntity item, ItemResolveEventArgs args)
+ {
+ if (args.ContainsFile("folder.xml"))
+ {
+ await Task.Run(() => Fetch(item, args)).ConfigureAwait(false);
+ }
+ }
+
+ private void Fetch(BaseEntity item, ItemResolveEventArgs args)
+ {
+ new BaseItemXmlParser<Folder>().Fetch(item as Folder, Path.Combine(args.Path, "folder.xml"));
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Providers/ImageFromMediaLocationProvider.cs b/MediaBrowser.Controller/Providers/ImageFromMediaLocationProvider.cs
new file mode 100644
index 000000000..d6fd26d1c
--- /dev/null
+++ b/MediaBrowser.Controller/Providers/ImageFromMediaLocationProvider.cs
@@ -0,0 +1,128 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.Composition;
+using System.IO;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Providers
+{
+ /// <summary>
+ /// Provides images for all types by looking for standard images - folder, backdrop, logo, etc.
+ /// </summary>
+ [Export(typeof(BaseMetadataProvider))]
+ public class ImageFromMediaLocationProvider : BaseMetadataProvider
+ {
+ public override bool Supports(BaseEntity item)
+ {
+ return true;
+ }
+
+ public override MetadataProviderPriority Priority
+ {
+ get { return MetadataProviderPriority.First; }
+ }
+
+ public override Task FetchAsync(BaseEntity item, ItemResolveEventArgs args)
+ {
+ if (args.IsDirectory)
+ {
+ var baseItem = item as BaseItem;
+
+ if (baseItem != null)
+ {
+ return Task.Run(() => PopulateBaseItemImages(baseItem, args));
+ }
+
+ return Task.Run(() => PopulateImages(item, args));
+ }
+
+ return Task.FromResult<object>(null);
+ }
+
+ /// <summary>
+ /// Fills in image paths based on files win the folder
+ /// </summary>
+ private void PopulateImages(BaseEntity item, ItemResolveEventArgs args)
+ {
+ for (int i = 0; i < args.FileSystemChildren.Length; i++)
+ {
+ var file = args.FileSystemChildren[i];
+
+ string filePath = file.Path;
+
+ string ext = Path.GetExtension(filePath);
+
+ // Only support png and jpg files
+ if (!ext.EndsWith("png", StringComparison.OrdinalIgnoreCase) && !ext.EndsWith("jpg", StringComparison.OrdinalIgnoreCase))
+ {
+ continue;
+ }
+
+ string name = Path.GetFileNameWithoutExtension(filePath);
+
+ if (name.Equals("folder", StringComparison.OrdinalIgnoreCase))
+ {
+ item.PrimaryImagePath = filePath;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Fills in image paths based on files win the folder
+ /// </summary>
+ private void PopulateBaseItemImages(BaseItem item, ItemResolveEventArgs args)
+ {
+ var backdropFiles = new List<string>();
+
+ for (int i = 0; i < args.FileSystemChildren.Length; i++)
+ {
+ var file = args.FileSystemChildren[i];
+
+ string filePath = file.Path;
+
+ string ext = Path.GetExtension(filePath);
+
+ // Only support png and jpg files
+ if (!ext.EndsWith("png", StringComparison.OrdinalIgnoreCase) && !ext.EndsWith("jpg", StringComparison.OrdinalIgnoreCase))
+ {
+ continue;
+ }
+
+ string name = Path.GetFileNameWithoutExtension(filePath);
+
+ if (name.Equals("folder", StringComparison.OrdinalIgnoreCase))
+ {
+ item.PrimaryImagePath = filePath;
+ }
+ else if (name.StartsWith("backdrop", StringComparison.OrdinalIgnoreCase))
+ {
+ backdropFiles.Add(filePath);
+ }
+ if (name.Equals("logo", StringComparison.OrdinalIgnoreCase))
+ {
+ item.LogoImagePath = filePath;
+ }
+ if (name.Equals("banner", StringComparison.OrdinalIgnoreCase))
+ {
+ item.BannerImagePath = filePath;
+ }
+ if (name.Equals("clearart", StringComparison.OrdinalIgnoreCase))
+ {
+ item.ArtImagePath = filePath;
+ }
+ if (name.Equals("thumb", StringComparison.OrdinalIgnoreCase))
+ {
+ item.ThumbnailImagePath = filePath;
+ }
+ }
+
+ if (backdropFiles.Count > 0)
+ {
+ item.BackdropImagePaths = backdropFiles;
+ }
+ }
+
+ }
+}
diff --git a/MediaBrowser.Controller/Providers/LocalTrailerProvider.cs b/MediaBrowser.Controller/Providers/LocalTrailerProvider.cs
new file mode 100644
index 000000000..8823da691
--- /dev/null
+++ b/MediaBrowser.Controller/Providers/LocalTrailerProvider.cs
@@ -0,0 +1,47 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.IO;
+using MediaBrowser.Controller.Library;
+using System.Collections.Generic;
+using System.ComponentModel.Composition;
+using System.IO;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Providers
+{
+ /// <summary>
+ /// Provides local trailers by checking the trailers subfolder
+ /// </summary>
+ [Export(typeof(BaseMetadataProvider))]
+ public class LocalTrailerProvider : BaseMetadataProvider
+ {
+ public override bool Supports(BaseEntity item)
+ {
+ return item is BaseItem;
+ }
+
+ public override MetadataProviderPriority Priority
+ {
+ get { return MetadataProviderPriority.First; }
+ }
+
+ public async override Task FetchAsync(BaseEntity item, ItemResolveEventArgs args)
+ {
+ if (args.ContainsFolder("trailers"))
+ {
+ var items = new List<Video>();
+
+ foreach (WIN32_FIND_DATA file in FileData.GetFileSystemEntries(Path.Combine(args.Path, "trailers"), "*"))
+ {
+ var video = await Kernel.Instance.ItemController.GetItem(file.Path, fileInfo: file).ConfigureAwait(false) as Video;
+
+ if (video != null)
+ {
+ items.Add(video);
+ }
+ }
+
+ (item as BaseItem).LocalTrailers = items;
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Providers/Movies/MovieProviderFromXml.cs b/MediaBrowser.Controller/Providers/Movies/MovieProviderFromXml.cs
new file mode 100644
index 000000000..7ef53d546
--- /dev/null
+++ b/MediaBrowser.Controller/Providers/Movies/MovieProviderFromXml.cs
@@ -0,0 +1,43 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Movies;
+using MediaBrowser.Controller.Library;
+using System.ComponentModel.Composition;
+using System.IO;
+using System.Threading.Tasks;
+using System;
+
+namespace MediaBrowser.Controller.Providers.Movies
+{
+ [Export(typeof(BaseMetadataProvider))]
+ public class MovieProviderFromXml : BaseMetadataProvider
+ {
+ public override bool Supports(BaseEntity item)
+ {
+ return item is Movie;
+ }
+
+ public override MetadataProviderPriority Priority
+ {
+ get { return MetadataProviderPriority.First; }
+ }
+
+ protected override DateTime CompareDate(BaseEntity item)
+ {
+ var entry = item.ResolveArgs.GetFileSystemEntry(Path.Combine(item.Path, "movie.xml"));
+ return entry != null ? entry.Value.LastWriteTimeUtc : DateTime.MinValue;
+ }
+
+ public override async Task FetchAsync(BaseEntity item, ItemResolveEventArgs args)
+ {
+ await Task.Run(() => Fetch(item, args)).ConfigureAwait(false);
+ }
+
+ private void Fetch(BaseEntity item, ItemResolveEventArgs args)
+ {
+ if (args.ContainsFile("movie.xml"))
+ {
+ new BaseItemXmlParser<Movie>().Fetch(item as Movie, Path.Combine(args.Path, "movie.xml"));
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Providers/Movies/MovieSpecialFeaturesProvider.cs b/MediaBrowser.Controller/Providers/Movies/MovieSpecialFeaturesProvider.cs
new file mode 100644
index 000000000..b6b856d29
--- /dev/null
+++ b/MediaBrowser.Controller/Providers/Movies/MovieSpecialFeaturesProvider.cs
@@ -0,0 +1,45 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Movies;
+using MediaBrowser.Controller.IO;
+using MediaBrowser.Controller.Library;
+using System.Collections.Generic;
+using System.ComponentModel.Composition;
+using System.IO;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Providers.Movies
+{
+ [Export(typeof(BaseMetadataProvider))]
+ public class MovieSpecialFeaturesProvider : BaseMetadataProvider
+ {
+ public override bool Supports(BaseEntity item)
+ {
+ return item is Movie;
+ }
+
+ public override MetadataProviderPriority Priority
+ {
+ get { return MetadataProviderPriority.First; }
+ }
+
+ public async override Task FetchAsync(BaseEntity item, ItemResolveEventArgs args)
+ {
+ if (args.ContainsFolder("specials"))
+ {
+ var items = new List<Video>();
+
+ foreach (WIN32_FIND_DATA file in FileData.GetFileSystemEntries(Path.Combine(args.Path, "specials"), "*"))
+ {
+ var video = await Kernel.Instance.ItemController.GetItem(file.Path, fileInfo: file).ConfigureAwait(false) as Video;
+
+ if (video != null)
+ {
+ items.Add(video);
+ }
+ }
+
+ (item as Movie).SpecialFeatures = items;
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Providers/TV/EpisodeImageFromMediaLocationProvider.cs b/MediaBrowser.Controller/Providers/TV/EpisodeImageFromMediaLocationProvider.cs
new file mode 100644
index 000000000..0b9cf85eb
--- /dev/null
+++ b/MediaBrowser.Controller/Providers/TV/EpisodeImageFromMediaLocationProvider.cs
@@ -0,0 +1,67 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Library;
+using System.ComponentModel.Composition;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Providers.TV
+{
+ [Export(typeof(BaseMetadataProvider))]
+ public class EpisodeImageFromMediaLocationProvider : BaseMetadataProvider
+ {
+ public override bool Supports(BaseEntity item)
+ {
+ return item is Episode;
+ }
+
+ public override MetadataProviderPriority Priority
+ {
+ get { return MetadataProviderPriority.First; }
+ }
+
+ public override Task FetchAsync(BaseEntity item, ItemResolveEventArgs args)
+ {
+ return Task.Run(() =>
+ {
+ var episode = item as Episode;
+
+ string metadataFolder = Path.Combine(args.Parent.Path, "metadata");
+
+ string episodeFileName = Path.GetFileName(episode.Path);
+
+ var season = args.Parent as Season;
+
+ SetPrimaryImagePath(episode, season, metadataFolder, episodeFileName);
+ });
+ }
+
+ private void SetPrimaryImagePath(Episode item, Season season, string metadataFolder, string episodeFileName)
+ {
+ // Look for the image file in the metadata folder, and if found, set PrimaryImagePath
+ var imageFiles = new string[] {
+ Path.Combine(metadataFolder, Path.ChangeExtension(episodeFileName, ".jpg")),
+ Path.Combine(metadataFolder, Path.ChangeExtension(episodeFileName, ".png"))
+ };
+
+ string image;
+
+ if (season == null)
+ {
+ // Epsiode directly in Series folder. Gotta do this the slow way
+ image = imageFiles.FirstOrDefault(f => File.Exists(f));
+ }
+ else
+ {
+ image = imageFiles.FirstOrDefault(f => season.ContainsMetadataFile(f));
+ }
+
+ // If we found something, set PrimaryImagePath
+ if (!string.IsNullOrEmpty(image))
+ {
+ item.PrimaryImagePath = image;
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Providers/TV/EpisodeProviderFromXml.cs b/MediaBrowser.Controller/Providers/TV/EpisodeProviderFromXml.cs
new file mode 100644
index 000000000..f3c19a704
--- /dev/null
+++ b/MediaBrowser.Controller/Providers/TV/EpisodeProviderFromXml.cs
@@ -0,0 +1,59 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Library;
+using System.ComponentModel.Composition;
+using System.IO;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Providers.TV
+{
+ [Export(typeof(BaseMetadataProvider))]
+ public class EpisodeProviderFromXml : BaseMetadataProvider
+ {
+ public override bool Supports(BaseEntity item)
+ {
+ return item is Episode;
+ }
+
+ public override MetadataProviderPriority Priority
+ {
+ get { return MetadataProviderPriority.First; }
+ }
+
+ public override async Task FetchAsync(BaseEntity item, ItemResolveEventArgs args)
+ {
+ await Task.Run(() => Fetch(item, args)).ConfigureAwait(false);
+ }
+
+ private void Fetch(BaseEntity item, ItemResolveEventArgs args)
+ {
+ string metadataFolder = Path.Combine(args.Parent.Path, "metadata");
+
+ string metadataFile = Path.Combine(metadataFolder, Path.ChangeExtension(Path.GetFileName(args.Path), ".xml"));
+
+ FetchMetadata(item as Episode, args.Parent as Season, metadataFile);
+ }
+
+ private void FetchMetadata(Episode item, Season season, string metadataFile)
+ {
+ if (season == null)
+ {
+ // Episode directly in Series folder
+ // Need to validate it the slow way
+ if (!File.Exists(metadataFile))
+ {
+ return;
+ }
+ }
+ else
+ {
+ if (!season.ContainsMetadataFile(metadataFile))
+ {
+ return;
+ }
+ }
+
+ new EpisodeXmlParser().Fetch(item, metadataFile);
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Providers/TV/EpisodeXmlParser.cs b/MediaBrowser.Controller/Providers/TV/EpisodeXmlParser.cs
new file mode 100644
index 000000000..1cb604a51
--- /dev/null
+++ b/MediaBrowser.Controller/Providers/TV/EpisodeXmlParser.cs
@@ -0,0 +1,60 @@
+using MediaBrowser.Controller.Entities.TV;
+using System.IO;
+using System.Xml;
+
+namespace MediaBrowser.Controller.Providers.TV
+{
+ public class EpisodeXmlParser : BaseItemXmlParser<Episode>
+ {
+ protected override void FetchDataFromXmlNode(XmlReader reader, Episode item)
+ {
+ switch (reader.Name)
+ {
+ case "filename":
+ {
+ string filename = reader.ReadElementContentAsString();
+
+ if (!string.IsNullOrWhiteSpace(filename))
+ {
+ // Strip off everything but the filename. Some metadata tools like MetaBrowser v1.0 will have an 'episodes' prefix
+ // even though it's actually using the metadata folder.
+ filename = Path.GetFileName(filename);
+
+ string seasonFolder = Path.GetDirectoryName(item.Path);
+ item.PrimaryImagePath = Path.Combine(seasonFolder, "metadata", filename);
+ }
+ break;
+ }
+ case "SeasonNumber":
+ {
+ string number = reader.ReadElementContentAsString();
+
+ if (!string.IsNullOrWhiteSpace(number))
+ {
+ item.ParentIndexNumber = int.Parse(number);
+ }
+ break;
+ }
+
+ case "EpisodeNumber":
+ {
+ string number = reader.ReadElementContentAsString();
+
+ if (!string.IsNullOrWhiteSpace(number))
+ {
+ item.IndexNumber = int.Parse(number);
+ }
+ break;
+ }
+
+ case "EpisodeName":
+ item.Name = reader.ReadElementContentAsString();
+ break;
+
+ default:
+ base.FetchDataFromXmlNode(reader, item);
+ break;
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Providers/TV/SeriesProviderFromXml.cs b/MediaBrowser.Controller/Providers/TV/SeriesProviderFromXml.cs
new file mode 100644
index 000000000..76d7e7ac1
--- /dev/null
+++ b/MediaBrowser.Controller/Providers/TV/SeriesProviderFromXml.cs
@@ -0,0 +1,36 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Library;
+using System.ComponentModel.Composition;
+using System.IO;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Providers.TV
+{
+ [Export(typeof(BaseMetadataProvider))]
+ public class SeriesProviderFromXml : BaseMetadataProvider
+ {
+ public override bool Supports(BaseEntity item)
+ {
+ return item is Series;
+ }
+
+ public override MetadataProviderPriority Priority
+ {
+ get { return MetadataProviderPriority.First; }
+ }
+
+ public override async Task FetchAsync(BaseEntity item, ItemResolveEventArgs args)
+ {
+ await Task.Run(() => Fetch(item, args)).ConfigureAwait(false);
+ }
+
+ private void Fetch(BaseEntity item, ItemResolveEventArgs args)
+ {
+ if (args.ContainsFile("series.xml"))
+ {
+ new SeriesXmlParser().Fetch(item as Series, Path.Combine(args.Path, "series.xml"));
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Providers/TV/SeriesXmlParser.cs b/MediaBrowser.Controller/Providers/TV/SeriesXmlParser.cs
new file mode 100644
index 000000000..36c0a99ef
--- /dev/null
+++ b/MediaBrowser.Controller/Providers/TV/SeriesXmlParser.cs
@@ -0,0 +1,69 @@
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Model.Entities;
+using System;
+using System.Xml;
+
+namespace MediaBrowser.Controller.Providers.TV
+{
+ public class SeriesXmlParser : BaseItemXmlParser<Series>
+ {
+ protected override void FetchDataFromXmlNode(XmlReader reader, Series item)
+ {
+ switch (reader.Name)
+ {
+ case "id":
+ string id = reader.ReadElementContentAsString();
+ if (!string.IsNullOrWhiteSpace(id))
+ {
+ item.SetProviderId(MetadataProviders.Tvdb, id);
+ }
+ break;
+
+ case "Airs_DayOfWeek":
+ {
+ string day = reader.ReadElementContentAsString();
+
+ if (!string.IsNullOrWhiteSpace(day))
+ {
+ if (day.Equals("Daily", StringComparison.OrdinalIgnoreCase))
+ {
+ item.AirDays = new DayOfWeek[] {
+ DayOfWeek.Sunday,
+ DayOfWeek.Monday,
+ DayOfWeek.Tuesday,
+ DayOfWeek.Wednesday,
+ DayOfWeek.Thursday,
+ DayOfWeek.Friday,
+ DayOfWeek.Saturday
+ };
+ }
+ else
+ {
+ item.AirDays = new DayOfWeek[] {
+ (DayOfWeek)Enum.Parse(typeof(DayOfWeek), day, true)
+ };
+ }
+ }
+
+ break;
+ }
+
+ case "Airs_Time":
+ item.AirTime = reader.ReadElementContentAsString();
+ break;
+
+ case "SeriesName":
+ item.Name = reader.ReadElementContentAsString();
+ break;
+
+ case "Status":
+ item.Status = reader.ReadElementContentAsString();
+ break;
+
+ default:
+ base.FetchDataFromXmlNode(reader, item);
+ break;
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Providers/VideoInfoProvider.cs b/MediaBrowser.Controller/Providers/VideoInfoProvider.cs
new file mode 100644
index 000000000..264825fe0
--- /dev/null
+++ b/MediaBrowser.Controller/Providers/VideoInfoProvider.cs
@@ -0,0 +1,168 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.FFMpeg;
+using MediaBrowser.Model.Entities;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.Composition;
+using System.Linq;
+
+namespace MediaBrowser.Controller.Providers
+{
+ [Export(typeof(BaseMetadataProvider))]
+ public class VideoInfoProvider : BaseMediaInfoProvider<Video>
+ {
+ public override MetadataProviderPriority Priority
+ {
+ // Give this second priority
+ // Give metadata xml providers a chance to fill in data first, so that we can skip this whenever possible
+ get { return MetadataProviderPriority.Second; }
+ }
+
+ protected override string CacheDirectory
+ {
+ get { return Kernel.Instance.ApplicationPaths.FFProbeVideoCacheDirectory; }
+ }
+
+ protected override void Fetch(Video video, FFProbeResult data)
+ {
+ if (data.format != null)
+ {
+ if (!string.IsNullOrEmpty(data.format.duration))
+ {
+ video.RunTimeTicks = TimeSpan.FromSeconds(double.Parse(data.format.duration)).Ticks;
+ }
+
+ if (!string.IsNullOrEmpty(data.format.bit_rate))
+ {
+ video.BitRate = int.Parse(data.format.bit_rate);
+ }
+ }
+
+ if (data.streams != null)
+ {
+ // For now, only read info about first video stream
+ // Files with multiple video streams are possible, but extremely rare
+ bool foundVideo = false;
+
+ foreach (MediaStream stream in data.streams)
+ {
+ if (stream.codec_type.Equals("video", StringComparison.OrdinalIgnoreCase))
+ {
+ if (!foundVideo)
+ {
+ FetchFromVideoStream(video, stream);
+ }
+
+ foundVideo = true;
+ }
+ else if (stream.codec_type.Equals("audio", StringComparison.OrdinalIgnoreCase))
+ {
+ FetchFromAudioStream(video, stream);
+ }
+ }
+ }
+ }
+
+ private void FetchFromVideoStream(Video video, MediaStream stream)
+ {
+ video.Codec = stream.codec_name;
+ video.Width = stream.width;
+ video.Height = stream.height;
+ video.AspectRatio = stream.display_aspect_ratio;
+
+ if (!string.IsNullOrEmpty(stream.avg_frame_rate))
+ {
+ string[] parts = stream.avg_frame_rate.Split('/');
+
+ if (parts.Length == 2)
+ {
+ video.FrameRate = float.Parse(parts[0]) / float.Parse(parts[1]);
+ }
+ else
+ {
+ video.FrameRate = float.Parse(parts[0]);
+ }
+ }
+ }
+
+ private void FetchFromAudioStream(Video video, MediaStream stream)
+ {
+ var audio = new AudioStream{};
+
+ audio.Codec = stream.codec_name;
+
+ if (!string.IsNullOrEmpty(stream.bit_rate))
+ {
+ audio.BitRate = int.Parse(stream.bit_rate);
+ }
+
+ audio.Channels = stream.channels;
+
+ if (!string.IsNullOrEmpty(stream.sample_rate))
+ {
+ audio.SampleRate = int.Parse(stream.sample_rate);
+ }
+
+ audio.Language = GetDictionaryValue(stream.tags, "language");
+
+ List<AudioStream> streams = video.AudioStreams ?? new List<AudioStream>();
+ streams.Add(audio);
+ video.AudioStreams = streams;
+ }
+
+ private void FetchFromSubtitleStream(Video video, MediaStream stream)
+ {
+ var subtitle = new SubtitleStream{};
+
+ subtitle.Language = GetDictionaryValue(stream.tags, "language");
+
+ List<SubtitleStream> streams = video.Subtitles ?? new List<SubtitleStream>();
+ streams.Add(subtitle);
+ video.Subtitles = streams;
+ }
+
+ /// <summary>
+ /// Determines if there's already enough info in the Video object to allow us to skip running ffprobe
+ /// </summary>
+ protected override bool CanSkipFFProbe(Video video)
+ {
+ if (video.VideoType != VideoType.VideoFile)
+ {
+ // Not supported yet
+ return true;
+ }
+
+ if (video.AudioStreams == null || !video.AudioStreams.Any())
+ {
+ return false;
+ }
+
+ if (string.IsNullOrEmpty(video.AspectRatio))
+ {
+ return false;
+ }
+
+ if (string.IsNullOrEmpty(video.Codec))
+ {
+ return false;
+ }
+
+ if (string.IsNullOrEmpty(video.ScanType))
+ {
+ return false;
+ }
+
+ if (!video.RunTimeTicks.HasValue || video.RunTimeTicks.Value == 0)
+ {
+ return false;
+ }
+
+ if (Convert.ToInt32(video.FrameRate) == 0 || video.Height == 0 || video.Width == 0 || video.BitRate == 0)
+ {
+ return false;
+ }
+
+ return true;
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Resolvers/AudioResolver.cs b/MediaBrowser.Controller/Resolvers/AudioResolver.cs
new file mode 100644
index 000000000..8f10e45e5
--- /dev/null
+++ b/MediaBrowser.Controller/Resolvers/AudioResolver.cs
@@ -0,0 +1,54 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using System.ComponentModel.Composition;
+using System.IO;
+
+namespace MediaBrowser.Controller.Resolvers
+{
+ [Export(typeof(IBaseItemResolver))]
+ public class AudioResolver : BaseItemResolver<Audio>
+ {
+ public override ResolverPriority Priority
+ {
+ get { return ResolverPriority.Last; }
+ }
+
+ protected override Audio Resolve(ItemResolveEventArgs args)
+ {
+ // Return audio if the path is a file and has a matching extension
+
+ if (!args.IsDirectory)
+ {
+ if (IsAudioFile(args.Path))
+ {
+ return new Audio();
+ }
+ }
+
+ return null;
+ }
+
+ private static bool IsAudioFile(string path)
+ {
+ string extension = Path.GetExtension(path).ToLower();
+
+ switch (extension)
+ {
+ case ".mp3":
+ case ".wma":
+ case ".aac":
+ case ".acc":
+ case ".flac":
+ case ".m4a":
+ case ".m4b":
+ case ".wav":
+ case ".ape":
+ return true;
+
+ default:
+ return false;
+ }
+
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Resolvers/BaseItemResolver.cs b/MediaBrowser.Controller/Resolvers/BaseItemResolver.cs
new file mode 100644
index 000000000..7c9677e4e
--- /dev/null
+++ b/MediaBrowser.Controller/Resolvers/BaseItemResolver.cs
@@ -0,0 +1,126 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.IO;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Common.Extensions;
+using System;
+using System.IO;
+
+namespace MediaBrowser.Controller.Resolvers
+{
+ public abstract class BaseItemResolver<T> : IBaseItemResolver
+ where T : BaseItem, new()
+ {
+ protected virtual T Resolve(ItemResolveEventArgs args)
+ {
+ return null;
+ }
+
+ public virtual ResolverPriority Priority
+ {
+ get
+ {
+ return ResolverPriority.First;
+ }
+ }
+
+ /// <summary>
+ /// Sets initial values on the newly resolved item
+ /// </summary>
+ protected virtual void SetInitialItemValues(T item, ItemResolveEventArgs args)
+ {
+ // If the subclass didn't specify this
+ if (string.IsNullOrEmpty(item.Path))
+ {
+ item.Path = args.Path;
+ }
+
+ // If the subclass didn't specify this
+ if (args.Parent != null)
+ {
+ item.Parent = args.Parent;
+ }
+
+ item.Id = (item.GetType().FullName + item.Path).GetMD5();
+ }
+
+ public BaseItem ResolvePath(ItemResolveEventArgs args)
+ {
+ T item = Resolve(args);
+
+ if (item != null)
+ {
+ // Set initial values on the newly resolved item
+ SetInitialItemValues(item, args);
+
+ // Make sure the item has a name
+ EnsureName(item);
+
+ // Make sure DateCreated and DateModified have values
+ EnsureDates(item, args);
+ }
+
+ return item;
+ }
+
+ private void EnsureName(T item)
+ {
+ // If the subclass didn't supply a name, add it here
+ if (string.IsNullOrEmpty(item.Name))
+ {
+ item.Name = Path.GetFileNameWithoutExtension(item.Path);
+ }
+
+ }
+
+ /// <summary>
+ /// Ensures DateCreated and DateModified have values
+ /// </summary>
+ private void EnsureDates(T item, ItemResolveEventArgs args)
+ {
+ if (!Path.IsPathRooted(item.Path))
+ {
+ return;
+ }
+
+ // See if a different path came out of the resolver than what went in
+ if (!args.Path.Equals(item.Path, StringComparison.OrdinalIgnoreCase))
+ {
+ WIN32_FIND_DATA? childData = args.GetFileSystemEntry(item.Path);
+
+ if (childData != null)
+ {
+ item.DateCreated = childData.Value.CreationTimeUtc;
+ item.DateModified = childData.Value.LastWriteTimeUtc;
+ }
+ else
+ {
+ WIN32_FIND_DATA fileData = FileData.GetFileData(item.Path);
+ item.DateCreated = fileData.CreationTimeUtc;
+ item.DateModified = fileData.LastWriteTimeUtc;
+ }
+ }
+ else
+ {
+ item.DateCreated = args.FileInfo.CreationTimeUtc;
+ item.DateModified = args.FileInfo.LastWriteTimeUtc;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Weed this to keep a list of resolvers, since Resolvers are built with generics
+ /// </summary>
+ public interface IBaseItemResolver
+ {
+ BaseItem ResolvePath(ItemResolveEventArgs args);
+ ResolverPriority Priority { get; }
+ }
+
+ public enum ResolverPriority
+ {
+ First = 1,
+ Second = 2,
+ Third = 3,
+ Last = 4
+ }
+}
diff --git a/MediaBrowser.Controller/Resolvers/EntityResolutionHelper.cs b/MediaBrowser.Controller/Resolvers/EntityResolutionHelper.cs
new file mode 100644
index 000000000..b821f8801
--- /dev/null
+++ b/MediaBrowser.Controller/Resolvers/EntityResolutionHelper.cs
@@ -0,0 +1,70 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.IO;
+using MediaBrowser.Controller.IO;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Entities.TV;
+
+namespace MediaBrowser.Controller.Resolvers
+{
+ public static class EntityResolutionHelper
+ {
+ /// <summary>
+ /// Any folder named in this list will be ignored - can be added to at runtime for extensibility
+ /// </summary>
+ public static List<string> IgnoreFolders = new List<string>()
+ {
+ "trailers",
+ "metadata",
+ "bdmv",
+ "certificate",
+ "backup",
+ "video_ts",
+ "audio_ts",
+ "ps3_update",
+ "ps3_vprm",
+ "adv_obj",
+ "hvdvd_ts"
+ };
+ /// <summary>
+ /// Determines whether a path should be resolved or ignored entirely - called before we even look at the contents
+ /// </summary>
+ /// <param name="path"></param>
+ /// <returns>false if the path should be ignored</returns>
+ public static bool ShouldResolvePath(WIN32_FIND_DATA path)
+ {
+ bool resolve = true;
+ // Ignore hidden files and folders
+ if (path.IsHidden || path.IsSystemFile)
+ {
+ resolve = false;
+ }
+
+ // Ignore any folders in our list
+ else if (path.IsDirectory && IgnoreFolders.Contains(Path.GetFileName(path.Path), StringComparer.OrdinalIgnoreCase))
+ {
+ resolve = false;
+ }
+
+ return resolve;
+ }
+
+ /// <summary>
+ /// Determines whether a path should be ignored based on its contents - called after the contents have been read
+ /// </summary>
+ public static bool ShouldResolvePathContents(ItemResolveEventArgs args)
+ {
+ bool resolve = true;
+ if (args.ContainsFile(".ignore"))
+ {
+ // Ignore any folders containing a file called .ignore
+ resolve = false;
+ }
+
+ return resolve;
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Resolvers/FolderResolver.cs b/MediaBrowser.Controller/Resolvers/FolderResolver.cs
new file mode 100644
index 000000000..028c85f86
--- /dev/null
+++ b/MediaBrowser.Controller/Resolvers/FolderResolver.cs
@@ -0,0 +1,36 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using System.ComponentModel.Composition;
+
+namespace MediaBrowser.Controller.Resolvers
+{
+ [Export(typeof(IBaseItemResolver))]
+ public class FolderResolver : BaseFolderResolver<Folder>
+ {
+ public override ResolverPriority Priority
+ {
+ get { return ResolverPriority.Last; }
+ }
+
+ protected override Folder Resolve(ItemResolveEventArgs args)
+ {
+ if (args.IsDirectory)
+ {
+ return new Folder();
+ }
+
+ return null;
+ }
+ }
+
+ public abstract class BaseFolderResolver<TItemType> : BaseItemResolver<TItemType>
+ where TItemType : Folder, new()
+ {
+ protected override void SetInitialItemValues(TItemType item, ItemResolveEventArgs args)
+ {
+ base.SetInitialItemValues(item, args);
+
+ item.IsRoot = args.Parent == null;
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Resolvers/Movies/BoxSetResolver.cs b/MediaBrowser.Controller/Resolvers/Movies/BoxSetResolver.cs
new file mode 100644
index 000000000..069068067
--- /dev/null
+++ b/MediaBrowser.Controller/Resolvers/Movies/BoxSetResolver.cs
@@ -0,0 +1,28 @@
+using MediaBrowser.Controller.Entities.Movies;
+using MediaBrowser.Controller.Library;
+using System;
+using System.ComponentModel.Composition;
+using System.IO;
+
+namespace MediaBrowser.Controller.Resolvers.Movies
+{
+ [Export(typeof(IBaseItemResolver))]
+ public class BoxSetResolver : BaseFolderResolver<BoxSet>
+ {
+ protected override BoxSet Resolve(ItemResolveEventArgs args)
+ {
+ // It's a boxset if all of the following conditions are met:
+ // Is a Directory
+ // Contains [boxset] in the path
+ if (args.IsDirectory)
+ {
+ if (Path.GetFileName(args.Path).IndexOf("[boxset]", StringComparison.OrdinalIgnoreCase) != -1)
+ {
+ return new BoxSet();
+ }
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Resolvers/Movies/MovieResolver.cs b/MediaBrowser.Controller/Resolvers/Movies/MovieResolver.cs
new file mode 100644
index 000000000..825850b20
--- /dev/null
+++ b/MediaBrowser.Controller/Resolvers/Movies/MovieResolver.cs
@@ -0,0 +1,116 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Movies;
+using MediaBrowser.Controller.IO;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Entities;
+using System.ComponentModel.Composition;
+using System.Collections.Generic;
+
+namespace MediaBrowser.Controller.Resolvers.Movies
+{
+ [Export(typeof(IBaseItemResolver))]
+ public class MovieResolver : BaseVideoResolver<Movie>
+ {
+ protected override Movie Resolve(ItemResolveEventArgs args)
+ {
+ // Must be a directory and under a 'Movies' VF
+ if (args.IsDirectory)
+ {
+ // If the parent is not a boxset, the only other allowed parent type is Folder
+ if (!(args.Parent is BoxSet))
+ {
+ if (args.Parent != null && args.Parent.GetType() != typeof(Folder))
+ {
+ return null;
+ }
+ }
+
+ // Optimization to avoid running all these tests against VF's
+ if (args.Parent != null && args.Parent.IsRoot)
+ {
+ return null;
+ }
+
+ // Return a movie if the video resolver finds something in the folder
+ return GetMovie(args);
+ }
+
+ return null;
+ }
+
+ protected override void SetInitialItemValues(Movie item, ItemResolveEventArgs args)
+ {
+ base.SetInitialItemValues(item, args);
+
+ SetProviderIdFromPath(item);
+ }
+
+ private void SetProviderIdFromPath(Movie item)
+ {
+ const string srch = "[tmdbid=";
+ int index = item.Path.IndexOf(srch, System.StringComparison.OrdinalIgnoreCase);
+
+ if (index != -1)
+ {
+ string id = item.Path.Substring(index + srch.Length);
+
+ id = id.Substring(0, id.IndexOf(']'));
+
+ item.SetProviderId(MetadataProviders.Tmdb, id);
+ }
+ }
+
+ private Movie GetMovie(ItemResolveEventArgs args)
+ {
+ //first see if the discovery process has already determined we are a DVD or BD
+ if (args.IsDVDFolder)
+ {
+ return new Movie()
+ {
+ Path = args.Path,
+ VideoType = VideoType.Dvd
+ };
+ }
+ else if (args.IsBDFolder)
+ {
+ return new Movie()
+ {
+ Path = args.Path,
+ VideoType = VideoType.BluRay
+ };
+ }
+ else if (args.IsHDDVDFolder)
+ {
+ return new Movie()
+ {
+ Path = args.Path,
+ VideoType = VideoType.HdDvd
+ };
+ }
+
+ // Loop through each child file/folder and see if we find a video
+ foreach (var child in args.FileSystemChildren)
+ {
+ var childArgs = new ItemResolveEventArgs
+ {
+ FileInfo = child,
+ FileSystemChildren = new WIN32_FIND_DATA[] { },
+ Path = child.Path
+ };
+
+ var item = base.Resolve(childArgs);
+
+ if (item != null)
+ {
+ return new Movie
+ {
+ Path = item.Path,
+ VideoType = item.VideoType
+ };
+ }
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Resolvers/TV/EpisodeResolver.cs b/MediaBrowser.Controller/Resolvers/TV/EpisodeResolver.cs
new file mode 100644
index 000000000..0961edd1a
--- /dev/null
+++ b/MediaBrowser.Controller/Resolvers/TV/EpisodeResolver.cs
@@ -0,0 +1,21 @@
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Library;
+using System.ComponentModel.Composition;
+
+namespace MediaBrowser.Controller.Resolvers.TV
+{
+ [Export(typeof(IBaseItemResolver))]
+ public class EpisodeResolver : BaseVideoResolver<Episode>
+ {
+ protected override Episode Resolve(ItemResolveEventArgs args)
+ {
+ // If the parent is a Season or Series, then this is an Episode if the VideoResolver returns something
+ if (args.Parent is Season || args.Parent is Series)
+ {
+ return base.Resolve(args);
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Resolvers/TV/SeasonResolver.cs b/MediaBrowser.Controller/Resolvers/TV/SeasonResolver.cs
new file mode 100644
index 000000000..0ad0782e0
--- /dev/null
+++ b/MediaBrowser.Controller/Resolvers/TV/SeasonResolver.cs
@@ -0,0 +1,25 @@
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Library;
+using System.ComponentModel.Composition;
+using System.IO;
+
+namespace MediaBrowser.Controller.Resolvers.TV
+{
+ [Export(typeof(IBaseItemResolver))]
+ public class SeasonResolver : BaseFolderResolver<Season>
+ {
+ protected override Season Resolve(ItemResolveEventArgs args)
+ {
+ if (args.Parent is Series && args.IsDirectory)
+ {
+ var season = new Season { };
+
+ season.IndexNumber = TVUtils.GetSeasonNumberFromPath(args.Path);
+
+ return season;
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Resolvers/TV/SeriesResolver.cs b/MediaBrowser.Controller/Resolvers/TV/SeriesResolver.cs
new file mode 100644
index 000000000..b8ff2c37b
--- /dev/null
+++ b/MediaBrowser.Controller/Resolvers/TV/SeriesResolver.cs
@@ -0,0 +1,64 @@
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Entities;
+using System;
+using System.ComponentModel.Composition;
+using System.IO;
+
+namespace MediaBrowser.Controller.Resolvers.TV
+{
+ [Export(typeof(IBaseItemResolver))]
+ public class SeriesResolver : BaseFolderResolver<Series>
+ {
+ protected override Series Resolve(ItemResolveEventArgs args)
+ {
+ if (args.IsDirectory)
+ {
+ // Optimization to avoid running all these tests against VF's
+ if (args.Parent != null && args.Parent.IsRoot)
+ {
+ return null;
+ }
+
+ // Optimization to avoid running these tests against Seasons
+ if (args.Parent is Series)
+ {
+ return null;
+ }
+
+ // It's a Series if any of the following conditions are met:
+ // series.xml exists
+ // [tvdbid= is present in the path
+ // TVUtils.IsSeriesFolder returns true
+ if (args.ContainsFile("series.xml") || Path.GetFileName(args.Path).IndexOf("[tvdbid=", StringComparison.OrdinalIgnoreCase) != -1 || TVUtils.IsSeriesFolder(args.Path, args.FileSystemChildren))
+ {
+ return new Series();
+ }
+ }
+
+ return null;
+ }
+
+ protected override void SetInitialItemValues(Series item, ItemResolveEventArgs args)
+ {
+ base.SetInitialItemValues(item, args);
+
+ SetProviderIdFromPath(item);
+ }
+
+ private void SetProviderIdFromPath(Series item)
+ {
+ const string srch = "[tvdbid=";
+ int index = item.Path.IndexOf(srch, StringComparison.OrdinalIgnoreCase);
+
+ if (index != -1)
+ {
+ string id = item.Path.Substring(index + srch.Length);
+
+ id = id.Substring(0, id.IndexOf(']'));
+
+ item.SetProviderId(MetadataProviders.Tvdb, id);
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Resolvers/TV/TVUtils.cs b/MediaBrowser.Controller/Resolvers/TV/TVUtils.cs
new file mode 100644
index 000000000..ec3305e16
--- /dev/null
+++ b/MediaBrowser.Controller/Resolvers/TV/TVUtils.cs
@@ -0,0 +1,164 @@
+using MediaBrowser.Controller.IO;
+using System;
+using System.Text.RegularExpressions;
+
+namespace MediaBrowser.Controller.Resolvers.TV
+{
+ public static class TVUtils
+ {
+ /// <summary>
+ /// A season folder must contain one of these somewhere in the name
+ /// </summary>
+ private static readonly string[] SeasonFolderNames = new string[] {
+ "season",
+ "sæson",
+ "temporada",
+ "saison",
+ "staffel"
+ };
+
+ /// <summary>
+ /// Used to detect paths that represent episodes, need to make sure they don't also
+ /// match movie titles like "2001 A Space..."
+ /// Currently we limit the numbers here to 2 digits to try and avoid this
+ /// </summary>
+ /// <remarks>
+ /// The order here is important, if the order is changed some of the later
+ /// ones might incorrectly match things that higher ones would have caught.
+ /// The most restrictive expressions should appear first
+ /// </remarks>
+ private static readonly Regex[] episodeExpressions = new Regex[] {
+ new Regex(@".*\\[s|S]?(?<seasonnumber>\d{1,2})[x|X](?<epnumber>\d{1,3})[^\\]*$", RegexOptions.Compiled), // 01x02 blah.avi S01x01 balh.avi
+ new Regex(@".*\\[s|S](?<seasonnumber>\d{1,2})x?[e|E](?<epnumber>\d{1,3})[^\\]*$", RegexOptions.Compiled), // S01E02 blah.avi, S01xE01 blah.avi
+ new Regex(@".*\\(?<seriesname>[^\\]*)[s|S]?(?<seasonnumber>\d{1,2})[x|X](?<epnumber>\d{1,3})[^\\]*$", RegexOptions.Compiled), // 01x02 blah.avi S01x01 balh.avi
+ new Regex(@".*\\(?<seriesname>[^\\]*)[s|S](?<seasonnumber>\d{1,2})[x|X|\.]?[e|E](?<epnumber>\d{1,3})[^\\]*$", RegexOptions.Compiled) // S01E02 blah.avi, S01xE01 blah.avi
+ };
+ /// <summary>
+ /// To avoid the following matching movies they are only valid when contained in a folder which has been matched as a being season
+ /// </summary>
+ private static readonly Regex[] episodeExpressionsInASeasonFolder = new Regex[] {
+ new Regex(@".*\\(?<epnumber>\d{1,2})\s?-\s?[^\\]*$", RegexOptions.Compiled), // 01 - blah.avi, 01-blah.avi
+ new Regex(@".*\\(?<epnumber>\d{1,2})[^\d\\]*[^\\]*$", RegexOptions.Compiled), // 01.avi, 01.blah.avi "01 - 22 blah.avi"
+ new Regex(@".*\\(?<seasonnumber>\d)(?<epnumber>\d{1,2})[^\d\\]+[^\\]*$", RegexOptions.Compiled), // 01.avi, 01.blah.avi
+ new Regex(@".*\\\D*\d+(?<epnumber>\d{2})", RegexOptions.Compiled) // hell0 - 101 - hello.avi
+
+ };
+
+ public static int? GetSeasonNumberFromPath(string path)
+ {
+ // Look for one of the season folder names
+ foreach (string name in SeasonFolderNames)
+ {
+ int index = path.IndexOf(name, StringComparison.OrdinalIgnoreCase);
+
+ if (index != -1)
+ {
+ return GetSeasonNumberFromPathSubstring(path.Substring(index + name.Length));
+ }
+ }
+
+ return null;
+ }
+
+ /// <summary>
+ /// Extracts the season number from the second half of the Season folder name (everything after "Season", or "Staffel")
+ /// </summary>
+ private static int? GetSeasonNumberFromPathSubstring(string path)
+ {
+ int numericStart = -1;
+ int length = 0;
+
+ // Find out where the numbers start, and then keep going until they end
+ for (int i = 0; i < path.Length; i++)
+ {
+ if (char.IsNumber(path, i))
+ {
+ if (numericStart == -1)
+ {
+ numericStart = i;
+ }
+ length++;
+ }
+ else if (numericStart != -1)
+ {
+ break;
+ }
+ }
+
+ if (numericStart == -1)
+ {
+ return null;
+ }
+
+ return int.Parse(path.Substring(numericStart, length));
+ }
+
+ public static bool IsSeasonFolder(string path)
+ {
+ return GetSeasonNumberFromPath(path) != null;
+ }
+
+ public static bool IsSeriesFolder(string path, WIN32_FIND_DATA[] fileSystemChildren)
+ {
+ // A folder with more than 3 non-season folders in will not becounted as a series
+ int nonSeriesFolders = 0;
+
+ for (int i = 0; i < fileSystemChildren.Length; i++)
+ {
+ var child = fileSystemChildren[i];
+
+ if (child.IsHidden || child.IsSystemFile)
+ {
+ continue;
+ }
+
+ if (child.IsDirectory)
+ {
+ if (IsSeasonFolder(child.Path))
+ {
+ return true;
+ }
+
+ nonSeriesFolders++;
+
+ if (nonSeriesFolders >= 3)
+ {
+ return false;
+ }
+ }
+ else
+ {
+ if (FileSystemHelper.IsVideoFile(child.Path) && !string.IsNullOrEmpty(EpisodeNumberFromFile(child.Path, false)))
+ {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ public static string EpisodeNumberFromFile(string fullPath, bool isInSeason)
+ {
+ string fl = fullPath.ToLower();
+ foreach (Regex r in episodeExpressions)
+ {
+ Match m = r.Match(fl);
+ if (m.Success)
+ return m.Groups["epnumber"].Value;
+ }
+ if (isInSeason)
+ {
+ foreach (Regex r in episodeExpressionsInASeasonFolder)
+ {
+ Match m = r.Match(fl);
+ if (m.Success)
+ return m.Groups["epnumber"].Value;
+ }
+
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Resolvers/VideoResolver.cs b/MediaBrowser.Controller/Resolvers/VideoResolver.cs
new file mode 100644
index 000000000..bc3be5e43
--- /dev/null
+++ b/MediaBrowser.Controller/Resolvers/VideoResolver.cs
@@ -0,0 +1,100 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Controller.IO;
+using System.ComponentModel.Composition;
+using System.IO;
+
+namespace MediaBrowser.Controller.Resolvers
+{
+ /// <summary>
+ /// Resolves a Path into a Video
+ /// </summary>
+ [Export(typeof(IBaseItemResolver))]
+ public class VideoResolver : BaseVideoResolver<Video>
+ {
+ public override ResolverPriority Priority
+ {
+ get { return ResolverPriority.Last; }
+ }
+ }
+
+ /// <summary>
+ /// Resolves a Path into a Video or Video subclass
+ /// </summary>
+ public abstract class BaseVideoResolver<T> : BaseItemResolver<T>
+ where T : Video, new()
+ {
+ protected override T Resolve(ItemResolveEventArgs args)
+ {
+ // If the path is a file check for a matching extensions
+ if (!args.IsDirectory)
+ {
+ if (FileSystemHelper.IsVideoFile(args.Path))
+ {
+ VideoType type = Path.GetExtension(args.Path).EndsWith("iso", System.StringComparison.OrdinalIgnoreCase) ? VideoType.Iso : VideoType.VideoFile;
+
+ return new T
+ {
+ VideoType = type,
+ Path = args.Path
+ };
+ }
+ }
+
+ else
+ {
+ // If the path is a folder, check if it's bluray or dvd
+ T item = ResolveFromFolderName(args.Path);
+
+ if (item != null)
+ {
+ return item;
+ }
+
+ // Also check the subfolders for bluray or dvd
+ for (int i = 0; i < args.FileSystemChildren.Length; i++)
+ {
+ var folder = args.FileSystemChildren[i];
+
+ if (!folder.IsDirectory)
+ {
+ continue;
+ }
+
+ item = ResolveFromFolderName(folder.Path);
+
+ if (item != null)
+ {
+ return item;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ private T ResolveFromFolderName(string folder)
+ {
+ if (folder.IndexOf("video_ts", System.StringComparison.OrdinalIgnoreCase) != -1)
+ {
+ return new T
+ {
+ VideoType = VideoType.Dvd,
+ Path = Path.GetDirectoryName(folder)
+ };
+ }
+ if (folder.IndexOf("bdmv", System.StringComparison.OrdinalIgnoreCase) != -1)
+ {
+ return new T
+ {
+ VideoType = VideoType.BluRay,
+ Path = Path.GetDirectoryName(folder)
+ };
+ }
+
+ return null;
+ }
+
+ }
+}
diff --git a/MediaBrowser.Controller/ServerApplicationPaths.cs b/MediaBrowser.Controller/ServerApplicationPaths.cs
new file mode 100644
index 000000000..f657ee259
--- /dev/null
+++ b/MediaBrowser.Controller/ServerApplicationPaths.cs
@@ -0,0 +1,278 @@
+using System.IO;
+using MediaBrowser.Common.Kernel;
+
+namespace MediaBrowser.Controller
+{
+ /// <summary>
+ /// Extends BaseApplicationPaths to add paths that are only applicable on the server
+ /// </summary>
+ public class ServerApplicationPaths : BaseApplicationPaths
+ {
+ private string _rootFolderPath;
+ /// <summary>
+ /// Gets the path to the root media directory
+ /// </summary>
+ public string RootFolderPath
+ {
+ get
+ {
+ if (_rootFolderPath == null)
+ {
+ _rootFolderPath = Path.Combine(ProgramDataPath, "root");
+ if (!Directory.Exists(_rootFolderPath))
+ {
+ Directory.CreateDirectory(_rootFolderPath);
+ }
+ }
+ return _rootFolderPath;
+ }
+ }
+
+ private string _ibnPath;
+ /// <summary>
+ /// Gets the path to the Images By Name directory
+ /// </summary>
+ public string ImagesByNamePath
+ {
+ get
+ {
+ if (_ibnPath == null)
+ {
+ _ibnPath = Path.Combine(ProgramDataPath, "ImagesByName");
+ if (!Directory.Exists(_ibnPath))
+ {
+ Directory.CreateDirectory(_ibnPath);
+ }
+ }
+
+ return _ibnPath;
+ }
+ }
+
+ private string _PeoplePath;
+ /// <summary>
+ /// Gets the path to the People directory
+ /// </summary>
+ public string PeoplePath
+ {
+ get
+ {
+ if (_PeoplePath == null)
+ {
+ _PeoplePath = Path.Combine(ImagesByNamePath, "People");
+ if (!Directory.Exists(_PeoplePath))
+ {
+ Directory.CreateDirectory(_PeoplePath);
+ }
+ }
+
+ return _PeoplePath;
+ }
+ }
+
+ private string _GenrePath;
+ /// <summary>
+ /// Gets the path to the Genre directory
+ /// </summary>
+ public string GenrePath
+ {
+ get
+ {
+ if (_GenrePath == null)
+ {
+ _GenrePath = Path.Combine(ImagesByNamePath, "Genre");
+ if (!Directory.Exists(_GenrePath))
+ {
+ Directory.CreateDirectory(_GenrePath);
+ }
+ }
+
+ return _GenrePath;
+ }
+ }
+
+ private string _StudioPath;
+ /// <summary>
+ /// Gets the path to the Studio directory
+ /// </summary>
+ public string StudioPath
+ {
+ get
+ {
+ if (_StudioPath == null)
+ {
+ _StudioPath = Path.Combine(ImagesByNamePath, "Studio");
+ if (!Directory.Exists(_StudioPath))
+ {
+ Directory.CreateDirectory(_StudioPath);
+ }
+ }
+
+ return _StudioPath;
+ }
+ }
+
+ private string _yearPath;
+ /// <summary>
+ /// Gets the path to the Year directory
+ /// </summary>
+ public string YearPath
+ {
+ get
+ {
+ if (_yearPath == null)
+ {
+ _yearPath = Path.Combine(ImagesByNamePath, "Year");
+ if (!Directory.Exists(_yearPath))
+ {
+ Directory.CreateDirectory(_yearPath);
+ }
+ }
+
+ return _yearPath;
+ }
+ }
+
+ private string _userConfigurationDirectoryPath;
+ /// <summary>
+ /// Gets the path to the user configuration directory
+ /// </summary>
+ public string UserConfigurationDirectoryPath
+ {
+ get
+ {
+ if (_userConfigurationDirectoryPath == null)
+ {
+ _userConfigurationDirectoryPath = Path.Combine(ConfigurationDirectoryPath, "user");
+ if (!Directory.Exists(_userConfigurationDirectoryPath))
+ {
+ Directory.CreateDirectory(_userConfigurationDirectoryPath);
+ }
+ }
+ return _userConfigurationDirectoryPath;
+ }
+ }
+
+ private string _CacheDirectory;
+ /// <summary>
+ /// Gets the folder path to the cache directory
+ /// </summary>
+ public string CacheDirectory
+ {
+ get
+ {
+ if (_CacheDirectory == null)
+ {
+ _CacheDirectory = Path.Combine(Kernel.Instance.ApplicationPaths.ProgramDataPath, "cache");
+
+ if (!Directory.Exists(_CacheDirectory))
+ {
+ Directory.CreateDirectory(_CacheDirectory);
+ }
+ }
+
+ return _CacheDirectory;
+ }
+ }
+
+ private string _FFProbeAudioCacheDirectory;
+ /// <summary>
+ /// Gets the folder path to the ffprobe audio cache directory
+ /// </summary>
+ public string FFProbeAudioCacheDirectory
+ {
+ get
+ {
+ if (_FFProbeAudioCacheDirectory == null)
+ {
+ _FFProbeAudioCacheDirectory = Path.Combine(Kernel.Instance.ApplicationPaths.CacheDirectory, "ffprobe-audio");
+
+ if (!Directory.Exists(_FFProbeAudioCacheDirectory))
+ {
+ Directory.CreateDirectory(_FFProbeAudioCacheDirectory);
+ }
+ }
+
+ return _FFProbeAudioCacheDirectory;
+ }
+ }
+
+ private string _FFProbeVideoCacheDirectory;
+ /// <summary>
+ /// Gets the folder path to the ffprobe video cache directory
+ /// </summary>
+ public string FFProbeVideoCacheDirectory
+ {
+ get
+ {
+ if (_FFProbeVideoCacheDirectory == null)
+ {
+ _FFProbeVideoCacheDirectory = Path.Combine(Kernel.Instance.ApplicationPaths.CacheDirectory, "ffprobe-video");
+
+ if (!Directory.Exists(_FFProbeVideoCacheDirectory))
+ {
+ Directory.CreateDirectory(_FFProbeVideoCacheDirectory);
+ }
+ }
+
+ return _FFProbeVideoCacheDirectory;
+ }
+ }
+
+ private string _FFMpegDirectory;
+ /// <summary>
+ /// Gets the folder path to ffmpeg
+ /// </summary>
+ public string FFMpegDirectory
+ {
+ get
+ {
+ if (_FFMpegDirectory == null)
+ {
+ _FFMpegDirectory = Path.Combine(Kernel.Instance.ApplicationPaths.ProgramDataPath, "FFMpeg");
+
+ if (!Directory.Exists(_FFMpegDirectory))
+ {
+ Directory.CreateDirectory(_FFMpegDirectory);
+ }
+ }
+
+ return _FFMpegDirectory;
+ }
+ }
+
+ private string _FFMpegPath;
+ /// <summary>
+ /// Gets the path to ffmpeg.exe
+ /// </summary>
+ public string FFMpegPath
+ {
+ get
+ {
+ if (_FFMpegPath == null)
+ {
+ _FFMpegPath = Path.Combine(FFMpegDirectory, "ffmpeg.exe");
+ }
+
+ return _FFMpegPath;
+ }
+ }
+
+ private string _FFProbePath;
+ /// <summary>
+ /// Gets the path to ffprobe.exe
+ /// </summary>
+ public string FFProbePath
+ {
+ get
+ {
+ if (_FFProbePath == null)
+ {
+ _FFProbePath = Path.Combine(FFMpegDirectory, "ffprobe.exe");
+ }
+
+ return _FFProbePath;
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Weather/BaseWeatherProvider.cs b/MediaBrowser.Controller/Weather/BaseWeatherProvider.cs
new file mode 100644
index 000000000..c3d436e66
--- /dev/null
+++ b/MediaBrowser.Controller/Weather/BaseWeatherProvider.cs
@@ -0,0 +1,34 @@
+using MediaBrowser.Common.Logging;
+using MediaBrowser.Model.Weather;
+using System;
+using System.Net;
+using System.Net.Cache;
+using System.Net.Http;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Weather
+{
+ public abstract class BaseWeatherProvider : IDisposable
+ {
+ protected HttpClient HttpClient { get; private set; }
+
+ protected BaseWeatherProvider()
+ {
+ var handler = new WebRequestHandler { };
+
+ handler.AutomaticDecompression = DecompressionMethods.Deflate;
+ handler.CachePolicy = new RequestCachePolicy(RequestCacheLevel.Revalidate);
+
+ HttpClient = new HttpClient(handler);
+ }
+
+ public virtual void Dispose()
+ {
+ Logger.LogInfo("Disposing " + GetType().Name);
+
+ HttpClient.Dispose();
+ }
+
+ public abstract Task<WeatherInfo> GetWeatherInfoAsync(string zipCode);
+ }
+}
diff --git a/MediaBrowser.Controller/Weather/WeatherProvider.cs b/MediaBrowser.Controller/Weather/WeatherProvider.cs
new file mode 100644
index 000000000..0fc728879
--- /dev/null
+++ b/MediaBrowser.Controller/Weather/WeatherProvider.cs
@@ -0,0 +1,189 @@
+using MediaBrowser.Common.Logging;
+using MediaBrowser.Common.Serialization;
+using MediaBrowser.Model.Weather;
+using System;
+using System.ComponentModel.Composition;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Weather
+{
+ /// <summary>
+ /// Based on http://www.worldweatheronline.com/free-weather-feed.aspx
+ /// The classes in this file are a reproduction of the json output, which will then be converted to our weather model classes
+ /// </summary>
+ [Export(typeof(BaseWeatherProvider))]
+ public class WeatherProvider : BaseWeatherProvider
+ {
+ public override async Task<WeatherInfo> GetWeatherInfoAsync(string zipCode)
+ {
+ if (string.IsNullOrWhiteSpace(zipCode))
+ {
+ return null;
+ }
+
+ const int numDays = 5;
+ const string apiKey = "24902f60f1231941120109";
+
+ string url = "http://free.worldweatheronline.com/feed/weather.ashx?q=" + zipCode + "&format=json&num_of_days=" + numDays + "&key=" + apiKey;
+
+ Logger.LogInfo("Accessing weather from " + url);
+
+ using (Stream stream = await HttpClient.GetStreamAsync(url).ConfigureAwait(false))
+ {
+ WeatherData data = JsonSerializer.DeserializeFromStream<WeatherResult>(stream).data;
+
+ return GetWeatherInfo(data);
+ }
+ }
+
+ /// <summary>
+ /// Converst the json output to our WeatherInfo model class
+ /// </summary>
+ private WeatherInfo GetWeatherInfo(WeatherData data)
+ {
+ var info = new WeatherInfo();
+
+ if (data.current_condition != null)
+ {
+ if (data.current_condition.Any())
+ {
+ info.CurrentWeather = data.current_condition.First().ToWeatherStatus();
+ }
+ }
+
+ if (data.weather != null)
+ {
+ info.Forecasts = data.weather.Select(w => w.ToWeatherForecast()).ToArray();
+ }
+
+ return info;
+ }
+ }
+
+ class WeatherResult
+ {
+ public WeatherData data { get; set; }
+ }
+
+ public class WeatherData
+ {
+ public WeatherCondition[] current_condition { get; set; }
+ public DailyWeatherInfo[] weather { get; set; }
+ }
+
+ public class WeatherCondition
+ {
+ public string temp_C { get; set; }
+ public string temp_F { get; set; }
+ public string humidity { get; set; }
+ public string weatherCode { get; set; }
+
+ public WeatherStatus ToWeatherStatus()
+ {
+ return new WeatherStatus
+ {
+ TemperatureCelsius = int.Parse(temp_C),
+ TemperatureFahrenheit = int.Parse(temp_F),
+ Humidity = int.Parse(humidity),
+ Condition = DailyWeatherInfo.GetCondition(weatherCode)
+ };
+ }
+ }
+
+ public class DailyWeatherInfo
+ {
+ public string date { get; set; }
+ public string precipMM { get; set; }
+ public string tempMaxC { get; set; }
+ public string tempMaxF { get; set; }
+ public string tempMinC { get; set; }
+ public string tempMinF { get; set; }
+ public string weatherCode { get; set; }
+ public string winddir16Point { get; set; }
+ public string winddirDegree { get; set; }
+ public string winddirection { get; set; }
+ public string windspeedKmph { get; set; }
+ public string windspeedMiles { get; set; }
+
+ public WeatherForecast ToWeatherForecast()
+ {
+ return new WeatherForecast
+ {
+ Date = DateTime.Parse(date),
+ HighTemperatureCelsius = int.Parse(tempMaxC),
+ HighTemperatureFahrenheit = int.Parse(tempMaxF),
+ LowTemperatureCelsius = int.Parse(tempMinC),
+ LowTemperatureFahrenheit = int.Parse(tempMinF),
+ Condition = GetCondition(weatherCode)
+ };
+ }
+
+ public static WeatherConditions GetCondition(string weatherCode)
+ {
+ switch (weatherCode)
+ {
+ case "362":
+ case "365":
+ case "320":
+ case "317":
+ case "182":
+ return WeatherConditions.Sleet;
+ case "338":
+ case "335":
+ case "332":
+ case "329":
+ case "326":
+ case "323":
+ case "377":
+ case "374":
+ case "371":
+ case "368":
+ case "395":
+ case "392":
+ case "350":
+ case "227":
+ case "179":
+ return WeatherConditions.Snow;
+ case "314":
+ case "311":
+ case "308":
+ case "305":
+ case "302":
+ case "299":
+ case "296":
+ case "293":
+ case "284":
+ case "281":
+ case "266":
+ case "263":
+ case "359":
+ case "356":
+ case "353":
+ case "185":
+ case "176":
+ return WeatherConditions.Rain;
+ case "260":
+ case "248":
+ return WeatherConditions.Fog;
+ case "389":
+ case "386":
+ case "200":
+ return WeatherConditions.Thunderstorm;
+ case "230":
+ return WeatherConditions.Blizzard;
+ case "143":
+ return WeatherConditions.Mist;
+ case "122":
+ return WeatherConditions.Overcast;
+ case "119":
+ return WeatherConditions.Cloudy;
+ case "115":
+ return WeatherConditions.PartlyCloudy;
+ default:
+ return WeatherConditions.Sunny;
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Xml/XmlExtensions.cs b/MediaBrowser.Controller/Xml/XmlExtensions.cs
new file mode 100644
index 000000000..d2e8e1983
--- /dev/null
+++ b/MediaBrowser.Controller/Xml/XmlExtensions.cs
@@ -0,0 +1,46 @@
+using System.Globalization;
+using System.Xml;
+
+namespace MediaBrowser.Controller.Xml
+{
+ public static class XmlExtensions
+ {
+ private static readonly CultureInfo _usCulture = new CultureInfo("en-US");
+
+ /// <summary>
+ /// Reads a float from the current element of an XmlReader
+ /// </summary>
+ public static float ReadFloatSafe(this XmlReader reader)
+ {
+ string valueString = reader.ReadElementContentAsString();
+
+ float value = 0;
+
+ if (!string.IsNullOrWhiteSpace(valueString))
+ {
+ // float.TryParse is local aware, so it can be probamatic, force us culture
+ float.TryParse(valueString, NumberStyles.AllowDecimalPoint, _usCulture, out value);
+ }
+
+ return value;
+ }
+
+ /// <summary>
+ /// Reads an int from the current element of an XmlReader
+ /// </summary>
+ public static int ReadIntSafe(this XmlReader reader)
+ {
+ string valueString = reader.ReadElementContentAsString();
+
+ int value = 0;
+
+ if (!string.IsNullOrWhiteSpace(valueString))
+ {
+
+ int.TryParse(valueString, out value);
+ }
+
+ return value;
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/packages.config b/MediaBrowser.Controller/packages.config
new file mode 100644
index 000000000..42f16a267
--- /dev/null
+++ b/MediaBrowser.Controller/packages.config
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<packages>
+ <package id="Rx-Core" version="2.0.20823" targetFramework="net45" />
+ <package id="Rx-Interfaces" version="2.0.20823" targetFramework="net45" />
+ <package id="Rx-Linq" version="2.0.20823" targetFramework="net45" />
+</packages> \ No newline at end of file
diff --git a/MediaBrowser.Model/Authentication/AuthenticationResult.cs b/MediaBrowser.Model/Authentication/AuthenticationResult.cs
new file mode 100644
index 000000000..ebc253172
--- /dev/null
+++ b/MediaBrowser.Model/Authentication/AuthenticationResult.cs
@@ -0,0 +1,11 @@
+using ProtoBuf;
+
+namespace MediaBrowser.Model.Authentication
+{
+ [ProtoContract]
+ public class AuthenticationResult
+ {
+ [ProtoMember(1)]
+ public bool Success { get; set; }
+ }
+}
diff --git a/MediaBrowser.Model/Configuration/BaseApplicationConfiguration.cs b/MediaBrowser.Model/Configuration/BaseApplicationConfiguration.cs
new file mode 100644
index 000000000..41eb1da2c
--- /dev/null
+++ b/MediaBrowser.Model/Configuration/BaseApplicationConfiguration.cs
@@ -0,0 +1,24 @@
+using ProtoBuf;
+
+namespace MediaBrowser.Model.Configuration
+{
+ /// <summary>
+ /// Serves as a common base class for the Server and UI application Configurations
+ /// ProtoInclude tells Protobuf about subclasses,
+ /// The number 50 can be any number, so long as it doesn't clash with any of the ProtoMember numbers either here or in subclasses.
+ /// </summary>
+ [ProtoContract, ProtoInclude(50, typeof(ServerConfiguration))]
+ public class BaseApplicationConfiguration
+ {
+ [ProtoMember(1)]
+ public bool EnableDebugLevelLogging { get; set; }
+
+ [ProtoMember(2)]
+ public int HttpServerPortNumber { get; set; }
+
+ public BaseApplicationConfiguration()
+ {
+ HttpServerPortNumber = 8096;
+ }
+ }
+}
diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs
new file mode 100644
index 000000000..5bcd09ef0
--- /dev/null
+++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs
@@ -0,0 +1,30 @@
+using MediaBrowser.Model.Weather;
+using ProtoBuf;
+
+namespace MediaBrowser.Model.Configuration
+{
+ /// <summary>
+ /// Represents the server configuration.
+ /// </summary>
+ [ProtoContract]
+ public class ServerConfiguration : BaseApplicationConfiguration
+ {
+ [ProtoMember(3)]
+ public bool EnableInternetProviders { get; set; }
+
+ [ProtoMember(4)]
+ public bool EnableUserProfiles { get; set; }
+
+ [ProtoMember(5)]
+ public string WeatherZipCode { get; set; }
+
+ [ProtoMember(6)]
+ public WeatherUnits WeatherUnit { get; set; }
+
+ public ServerConfiguration()
+ : base()
+ {
+ EnableUserProfiles = true;
+ }
+ }
+}
diff --git a/MediaBrowser.Model/DTO/AudioInfo.cs b/MediaBrowser.Model/DTO/AudioInfo.cs
new file mode 100644
index 000000000..9f7675e17
--- /dev/null
+++ b/MediaBrowser.Model/DTO/AudioInfo.cs
@@ -0,0 +1,23 @@
+using ProtoBuf;
+
+namespace MediaBrowser.Model.DTO
+{
+ [ProtoContract]
+ public class AudioInfo
+ {
+ [ProtoMember(1)]
+ public int BitRate { get; set; }
+
+ [ProtoMember(2)]
+ public int Channels { get; set; }
+
+ [ProtoMember(3)]
+ public string Artist { get; set; }
+
+ [ProtoMember(4)]
+ public string Album { get; set; }
+
+ [ProtoMember(5)]
+ public string AlbumArtist { get; set; }
+ }
+}
diff --git a/MediaBrowser.Model/DTO/AudioOutputFormats.cs b/MediaBrowser.Model/DTO/AudioOutputFormats.cs
new file mode 100644
index 000000000..1ae044473
--- /dev/null
+++ b/MediaBrowser.Model/DTO/AudioOutputFormats.cs
@@ -0,0 +1,15 @@
+
+namespace MediaBrowser.Model.DTO
+{
+ /// <summary>
+ /// These are the audio output formats that the api is cabaple of streaming
+ /// This does not limit the inputs, only the outputs.
+ /// </summary>
+ public enum AudioOutputFormats
+ {
+ Aac,
+ Flac,
+ Mp3,
+ Wma
+ }
+}
diff --git a/MediaBrowser.Model/DTO/DTOBaseItem.cs b/MediaBrowser.Model/DTO/DTOBaseItem.cs
new file mode 100644
index 000000000..61607ab02
--- /dev/null
+++ b/MediaBrowser.Model/DTO/DTOBaseItem.cs
@@ -0,0 +1,178 @@
+using MediaBrowser.Model.Entities;
+using ProtoBuf;
+using System;
+using System.Collections.Generic;
+
+namespace MediaBrowser.Model.DTO
+{
+ /// <summary>
+ /// This is strictly used as a data transfer object from the api layer.
+ /// This holds information about a BaseItem in a format that is convenient for the client.
+ /// </summary>
+ [ProtoContract]
+ public class DtoBaseItem : IHasProviderIds
+ {
+ [ProtoMember(1)]
+ public string Name { get; set; }
+
+ [ProtoMember(2)]
+ public Guid Id { get; set; }
+
+ [ProtoMember(3)]
+ public DateTime DateCreated { get; set; }
+
+ [ProtoMember(4)]
+ public string SortName { get; set; }
+
+ [ProtoMember(5)]
+ public DateTime? PremiereDate { get; set; }
+
+ [ProtoMember(6)]
+ public string Path { get; set; }
+
+ [ProtoMember(7)]
+ public string OfficialRating { get; set; }
+
+ [ProtoMember(8)]
+ public string Overview { get; set; }
+
+ [ProtoMember(9)]
+ public string[] Taglines { get; set; }
+
+ [ProtoMember(10)]
+ public string[] Genres { get; set; }
+
+ [ProtoMember(11)]
+ public string DisplayMediaType { get; set; }
+
+ [ProtoMember(12)]
+ public float? CommunityRating { get; set; }
+
+ [ProtoMember(13)]
+ public long? RunTimeTicks { get; set; }
+
+ [ProtoMember(14)]
+ public string AspectRatio { get; set; }
+
+ [ProtoMember(15)]
+ public int? ProductionYear { get; set; }
+
+ [ProtoMember(16)]
+ public int? IndexNumber { get; set; }
+
+ [ProtoMember(17)]
+ public int? ParentIndexNumber { get; set; }
+
+ [ProtoMember(18)]
+ public string TrailerUrl { get; set; }
+
+ [ProtoMember(19)]
+ public Dictionary<string, string> ProviderIds { get; set; }
+
+ [ProtoMember(20)]
+ public bool HasBanner { get; set; }
+
+ [ProtoMember(21)]
+ public bool HasArt { get; set; }
+
+ [ProtoMember(22)]
+ public bool HasLogo { get; set; }
+
+ [ProtoMember(23)]
+ public bool HasThumb { get; set; }
+
+ [ProtoMember(24)]
+ public bool HasPrimaryImage { get; set; }
+
+ [ProtoMember(25)]
+ public string Language { get; set; }
+
+ [ProtoMember(26)]
+ public int BackdropCount { get; set; }
+
+ [ProtoMember(27)]
+ public DtoBaseItem[] Children { get; set; }
+
+ [ProtoMember(28)]
+ public bool IsFolder { get; set; }
+
+ /// <summary>
+ /// If the item is a Folder this will determine if it's the Root or not
+ /// </summary>
+ [ProtoMember(29)]
+ public bool? IsRoot { get; set; }
+
+ /// <summary>
+ /// If the item is a Folder this will determine if it's a VF or not
+ /// </summary>
+ [ProtoMember(30)]
+ public bool? IsVirtualFolder { get; set; }
+
+ [ProtoMember(31)]
+ public Guid? ParentId { get; set; }
+
+ [ProtoMember(32)]
+ public string Type { get; set; }
+
+ [ProtoMember(33)]
+ public BaseItemPerson[] People { get; set; }
+
+ [ProtoMember(34)]
+ public BaseItemStudio[] Studios { get; set; }
+
+ /// <summary>
+ /// If the item does not have a logo, this will hold the Id of the Parent that has one.
+ /// </summary>
+ [ProtoMember(35)]
+ public Guid? ParentLogoItemId { get; set; }
+
+ /// <summary>
+ /// If the item does not have any backdrops, this will hold the Id of the Parent that has one.
+ /// </summary>
+ [ProtoMember(36)]
+ public Guid? ParentBackdropItemId { get; set; }
+
+ [ProtoMember(37)]
+ public int? ParentBackdropCount { get; set; }
+
+ [ProtoMember(38)]
+ public DtoBaseItem[] LocalTrailers { get; set; }
+
+ [ProtoMember(39)]
+ public int LocalTrailerCount { get; set; }
+
+ /// <summary>
+ /// User data for this item based on the user it's being requested for
+ /// </summary>
+ [ProtoMember(40)]
+ public DtoUserItemData UserData { get; set; }
+
+ [ProtoMember(41)]
+ public ItemSpecialCounts SpecialCounts { get; set; }
+
+ [ProtoMember(42)]
+ public AudioInfo AudioInfo { get; set; }
+
+ [ProtoMember(43)]
+ public VideoInfo VideoInfo { get; set; }
+
+ [ProtoMember(44)]
+ public SeriesInfo SeriesInfo { get; set; }
+
+ [ProtoMember(45)]
+ public MovieInfo MovieInfo { get; set; }
+
+ [ProtoMember(46)]
+ public bool IsNew { get; set; }
+
+ public bool IsType(Type type)
+ {
+ return IsType(type.Name);
+ }
+
+ public bool IsType(string type)
+ {
+ return Type.Equals(type, StringComparison.OrdinalIgnoreCase);
+ }
+ }
+}
diff --git a/MediaBrowser.Model/DTO/DTOUser.cs b/MediaBrowser.Model/DTO/DTOUser.cs
new file mode 100644
index 000000000..766cd741e
--- /dev/null
+++ b/MediaBrowser.Model/DTO/DTOUser.cs
@@ -0,0 +1,27 @@
+using ProtoBuf;
+using System;
+
+namespace MediaBrowser.Model.DTO
+{
+ [ProtoContract]
+ public class DtoUser
+ {
+ [ProtoMember(1)]
+ public string Name { get; set; }
+
+ [ProtoMember(2)]
+ public Guid Id { get; set; }
+
+ [ProtoMember(3)]
+ public bool HasImage { get; set; }
+
+ [ProtoMember(4)]
+ public bool HasPassword { get; set; }
+
+ [ProtoMember(5)]
+ public DateTime? LastLoginDate { get; set; }
+
+ [ProtoMember(6)]
+ public DateTime? LastActivityDate { get; set; }
+ }
+}
diff --git a/MediaBrowser.Model/DTO/DTOUserItemData.cs b/MediaBrowser.Model/DTO/DTOUserItemData.cs
new file mode 100644
index 000000000..ce258f16f
--- /dev/null
+++ b/MediaBrowser.Model/DTO/DTOUserItemData.cs
@@ -0,0 +1,23 @@
+using ProtoBuf;
+
+namespace MediaBrowser.Model.DTO
+{
+ [ProtoContract]
+ public class DtoUserItemData
+ {
+ [ProtoMember(1)]
+ public float? Rating { get; set; }
+
+ [ProtoMember(2)]
+ public long PlaybackPositionTicks { get; set; }
+
+ [ProtoMember(3)]
+ public int PlayCount { get; set; }
+
+ [ProtoMember(4)]
+ public bool IsFavorite { get; set; }
+
+ [ProtoMember(5)]
+ public bool? Likes { get; set; }
+ }
+}
diff --git a/MediaBrowser.Model/DTO/IBNItem.cs b/MediaBrowser.Model/DTO/IBNItem.cs
new file mode 100644
index 000000000..507a37272
--- /dev/null
+++ b/MediaBrowser.Model/DTO/IBNItem.cs
@@ -0,0 +1,65 @@
+using System;
+using ProtoBuf;
+
+namespace MediaBrowser.Model.DTO
+{
+ /// <summary>
+ /// This is a stub class used by the api to get IBN types along with their item counts
+ /// </summary>
+ [ProtoContract]
+ public class IbnItem
+ {
+ /// <summary>
+ /// The name of the person, genre, etc
+ /// </summary>
+ [ProtoMember(1)]
+ public string Name { get; set; }
+
+ /// <summary>
+ /// The id of the person, genre, etc
+ /// </summary>
+ [ProtoMember(2)]
+ public Guid Id { get; set; }
+
+ [ProtoMember(3)]
+ public bool HasImage { get; set; }
+
+ /// <summary>
+ /// The number of items that have the genre, year, studio, etc
+ /// </summary>
+ [ProtoMember(4)]
+ public int BaseItemCount { get; set; }
+ }
+
+ /// <summary>
+ /// This is used by the api to get information about a Person within a BaseItem
+ /// </summary>
+ [ProtoContract]
+ public class BaseItemPerson
+ {
+ [ProtoMember(1)]
+ public string Name { get; set; }
+
+ [ProtoMember(2)]
+ public string Overview { get; set; }
+
+ [ProtoMember(3)]
+ public string Type { get; set; }
+
+ [ProtoMember(4)]
+ public bool HasImage { get; set; }
+ }
+
+ /// <summary>
+ /// This is used by the api to get information about a studio within a BaseItem
+ /// </summary>
+ [ProtoContract]
+ public class BaseItemStudio
+ {
+ [ProtoMember(1)]
+ public string Name { get; set; }
+
+ [ProtoMember(2)]
+ public bool HasImage { get; set; }
+ }
+}
diff --git a/MediaBrowser.Model/DTO/MovieInfo.cs b/MediaBrowser.Model/DTO/MovieInfo.cs
new file mode 100644
index 000000000..192c80565
--- /dev/null
+++ b/MediaBrowser.Model/DTO/MovieInfo.cs
@@ -0,0 +1,11 @@
+using ProtoBuf;
+
+namespace MediaBrowser.Model.DTO
+{
+ [ProtoContract]
+ public class MovieInfo
+ {
+ [ProtoMember(1)]
+ public int SpecialFeatureCount { get; set; }
+ }
+}
diff --git a/MediaBrowser.Model/DTO/PluginInfo.cs b/MediaBrowser.Model/DTO/PluginInfo.cs
new file mode 100644
index 000000000..12a22b98f
--- /dev/null
+++ b/MediaBrowser.Model/DTO/PluginInfo.cs
@@ -0,0 +1,33 @@
+using System;
+using ProtoBuf;
+
+namespace MediaBrowser.Model.DTO
+{
+ /// <summary>
+ /// This is a serializable stub class that is used by the api to provide information about installed plugins.
+ /// </summary>
+ [ProtoContract]
+ public class PluginInfo
+ {
+ [ProtoMember(1)]
+ public string Name { get; set; }
+
+ [ProtoMember(2)]
+ public bool Enabled { get; set; }
+
+ [ProtoMember(3)]
+ public bool DownloadToUI { get; set; }
+
+ [ProtoMember(4)]
+ public DateTime ConfigurationDateLastModified { get; set; }
+
+ [ProtoMember(5)]
+ public string Version { get; set; }
+
+ [ProtoMember(6)]
+ public string AssemblyFileName { get; set; }
+
+ [ProtoMember(7)]
+ public string ConfigurationFileName { get; set; }
+ }
+}
diff --git a/MediaBrowser.Model/DTO/SeriesInfo.cs b/MediaBrowser.Model/DTO/SeriesInfo.cs
new file mode 100644
index 000000000..ebb39c8c4
--- /dev/null
+++ b/MediaBrowser.Model/DTO/SeriesInfo.cs
@@ -0,0 +1,18 @@
+using ProtoBuf;
+using System;
+
+namespace MediaBrowser.Model.DTO
+{
+ [ProtoContract]
+ public class SeriesInfo
+ {
+ [ProtoMember(1)]
+ public string Status { get; set; }
+
+ [ProtoMember(2)]
+ public string AirTime { get; set; }
+
+ [ProtoMember(3)]
+ public DayOfWeek[] AirDays { get; set; }
+ }
+}
diff --git a/MediaBrowser.Model/DTO/VideoInfo.cs b/MediaBrowser.Model/DTO/VideoInfo.cs
new file mode 100644
index 000000000..8e0d6f38f
--- /dev/null
+++ b/MediaBrowser.Model/DTO/VideoInfo.cs
@@ -0,0 +1,30 @@
+using MediaBrowser.Model.Entities;
+using ProtoBuf;
+
+namespace MediaBrowser.Model.DTO
+{
+ [ProtoContract]
+ public class VideoInfo
+ {
+ [ProtoMember(1)]
+ public string Codec { get; set; }
+
+ [ProtoMember(2)]
+ public int Height { get; set; }
+
+ [ProtoMember(3)]
+ public int Width { get; set; }
+
+ [ProtoMember(4)]
+ public string ScanType { get; set; }
+
+ [ProtoMember(5)]
+ public VideoType VideoType { get; set; }
+
+ [ProtoMember(6)]
+ public SubtitleStream[] Subtitles { get; set; }
+
+ [ProtoMember(7)]
+ public AudioStream[] AudioStreams { get; set; }
+ }
+}
diff --git a/MediaBrowser.Model/DTO/VideoOutputFormats.cs b/MediaBrowser.Model/DTO/VideoOutputFormats.cs
new file mode 100644
index 000000000..c3840bff6
--- /dev/null
+++ b/MediaBrowser.Model/DTO/VideoOutputFormats.cs
@@ -0,0 +1,22 @@
+
+namespace MediaBrowser.Model.DTO
+{
+ /// <summary>
+ /// These are the video output formats that the api is cabaple of streaming
+ /// This does not limit the inputs, only the outputs.
+ /// </summary>
+ public enum VideoOutputFormats
+ {
+ Avi,
+ Asf,
+ M4V,
+ Mkv,
+ Mov,
+ Mp4,
+ Ogv,
+ ThreeGp,
+ Ts,
+ Webm,
+ Wmv
+ }
+}
diff --git a/MediaBrowser.Model/Entities/AudioStream.cs b/MediaBrowser.Model/Entities/AudioStream.cs
new file mode 100644
index 000000000..8a4cea4ee
--- /dev/null
+++ b/MediaBrowser.Model/Entities/AudioStream.cs
@@ -0,0 +1,26 @@
+using ProtoBuf;
+
+namespace MediaBrowser.Model.Entities
+{
+ [ProtoContract]
+ public class AudioStream
+ {
+ [ProtoMember(1)]
+ public string Codec { get; set; }
+
+ [ProtoMember(2)]
+ public string Language { get; set; }
+
+ [ProtoMember(3)]
+ public int BitRate { get; set; }
+
+ [ProtoMember(4)]
+ public int Channels { get; set; }
+
+ [ProtoMember(5)]
+ public int SampleRate { get; set; }
+
+ [ProtoMember(6)]
+ public bool IsDefault { get; set; }
+ }
+}
diff --git a/MediaBrowser.Model/Entities/IHasProviderIds.cs b/MediaBrowser.Model/Entities/IHasProviderIds.cs
new file mode 100644
index 000000000..96eb78f24
--- /dev/null
+++ b/MediaBrowser.Model/Entities/IHasProviderIds.cs
@@ -0,0 +1,57 @@
+using System.Collections.Generic;
+
+namespace MediaBrowser.Model.Entities
+{
+ /// <summary>
+ /// Since BaseItem and DTOBaseItem both have ProviderIds, this interface helps avoid code repition by using extension methods
+ /// </summary>
+ public interface IHasProviderIds
+ {
+ Dictionary<string, string> ProviderIds { get; set; }
+ }
+
+ public static class ProviderIdsExtensions
+ {
+ /// <summary>
+ /// Gets a provider id
+ /// </summary>
+ public static string GetProviderId(this IHasProviderIds instance, MetadataProviders provider)
+ {
+ return instance.GetProviderId(provider.ToString());
+ }
+
+ /// <summary>
+ /// Gets a provider id
+ /// </summary>
+ public static string GetProviderId(this IHasProviderIds instance, string name)
+ {
+ if (instance.ProviderIds == null)
+ {
+ return null;
+ }
+
+ return instance.ProviderIds[name];
+ }
+
+ /// <summary>
+ /// Sets a provider id
+ /// </summary>
+ public static void SetProviderId(this IHasProviderIds instance, string name, string value)
+ {
+ if (instance.ProviderIds == null)
+ {
+ instance.ProviderIds = new Dictionary<string, string>();
+ }
+
+ instance.ProviderIds[name] = value;
+ }
+
+ /// <summary>
+ /// Sets a provider id
+ /// </summary>
+ public static void SetProviderId(this IHasProviderIds instance, MetadataProviders provider, string value)
+ {
+ instance.SetProviderId(provider.ToString(), value);
+ }
+ }
+}
diff --git a/MediaBrowser.Model/Entities/ImageType.cs b/MediaBrowser.Model/Entities/ImageType.cs
new file mode 100644
index 000000000..d9bb06cbc
--- /dev/null
+++ b/MediaBrowser.Model/Entities/ImageType.cs
@@ -0,0 +1,13 @@
+
+namespace MediaBrowser.Model.Entities
+{
+ public enum ImageType
+ {
+ Primary,
+ Art,
+ Backdrop,
+ Banner,
+ Logo,
+ Thumbnail
+ }
+}
diff --git a/MediaBrowser.Model/Entities/ItemSpecialCounts.cs b/MediaBrowser.Model/Entities/ItemSpecialCounts.cs
new file mode 100644
index 000000000..b57be6ca8
--- /dev/null
+++ b/MediaBrowser.Model/Entities/ItemSpecialCounts.cs
@@ -0,0 +1,23 @@
+using ProtoBuf;
+
+namespace MediaBrowser.Model.Entities
+{
+ /// <summary>
+ /// Since it can be slow to collect this data, this class helps provide a way to calculate them all at once.
+ /// </summary>
+ [ProtoContract]
+ public class ItemSpecialCounts
+ {
+ [ProtoMember(1)]
+ public int RecentlyAddedItemCount { get; set; }
+
+ [ProtoMember(2)]
+ public int RecentlyAddedUnPlayedItemCount { get; set; }
+
+ [ProtoMember(3)]
+ public int InProgressItemCount { get; set; }
+
+ [ProtoMember(4)]
+ public decimal PlayedPercentage { get; set; }
+ }
+}
diff --git a/MediaBrowser.Model/Entities/MetadataProviders.cs b/MediaBrowser.Model/Entities/MetadataProviders.cs
new file mode 100644
index 000000000..b32ec2039
--- /dev/null
+++ b/MediaBrowser.Model/Entities/MetadataProviders.cs
@@ -0,0 +1,11 @@
+
+namespace MediaBrowser.Model.Entities
+{
+ public enum MetadataProviders
+ {
+ Imdb,
+ Tmdb,
+ Tvdb,
+ Tvcom
+ }
+}
diff --git a/MediaBrowser.Model/Entities/SubtitleStream.cs b/MediaBrowser.Model/Entities/SubtitleStream.cs
new file mode 100644
index 000000000..7a59d9302
--- /dev/null
+++ b/MediaBrowser.Model/Entities/SubtitleStream.cs
@@ -0,0 +1,17 @@
+using ProtoBuf;
+
+namespace MediaBrowser.Model.Entities
+{
+ [ProtoContract]
+ public class SubtitleStream
+ {
+ [ProtoMember(1)]
+ public string Language { get; set; }
+
+ [ProtoMember(2)]
+ public bool IsDefault { get; set; }
+
+ [ProtoMember(3)]
+ public bool IsForced { get; set; }
+ }
+}
diff --git a/MediaBrowser.Model/Entities/VideoType.cs b/MediaBrowser.Model/Entities/VideoType.cs
new file mode 100644
index 000000000..0d46ff770
--- /dev/null
+++ b/MediaBrowser.Model/Entities/VideoType.cs
@@ -0,0 +1,12 @@
+
+namespace MediaBrowser.Model.Entities
+{
+ public enum VideoType
+ {
+ VideoFile,
+ Iso,
+ Dvd,
+ BluRay,
+ HdDvd
+ }
+}
diff --git a/MediaBrowser.Model/MediaBrowser.Model.csproj b/MediaBrowser.Model/MediaBrowser.Model.csproj
new file mode 100644
index 000000000..f7bd3a3ba
--- /dev/null
+++ b/MediaBrowser.Model/MediaBrowser.Model.csproj
@@ -0,0 +1,80 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
+ <PropertyGroup>
+ <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+ <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+ <ProjectGuid>{7EEEB4BB-F3E8-48FC-B4C5-70F0FFF8329B}</ProjectGuid>
+ <OutputType>Library</OutputType>
+ <AppDesignerFolder>Properties</AppDesignerFolder>
+ <RootNamespace>MediaBrowser.Model</RootNamespace>
+ <AssemblyName>MediaBrowser.Model</AssemblyName>
+ <TargetFrameworkVersion>v4.0</TargetFrameworkVersion>
+ <TargetFrameworkProfile>Profile4</TargetFrameworkProfile>
+ <FileAlignment>512</FileAlignment>
+ <ProjectTypeGuids>{786C830F-07A1-408B-BD7F-6EE04809D6DB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
+ <DebugSymbols>true</DebugSymbols>
+ <DebugType>full</DebugType>
+ <Optimize>false</Optimize>
+ <OutputPath>bin\Debug\</OutputPath>
+ <DefineConstants>DEBUG;TRACE</DefineConstants>
+ <ErrorReport>prompt</ErrorReport>
+ <WarningLevel>4</WarningLevel>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
+ <DebugType>pdbonly</DebugType>
+ <Optimize>true</Optimize>
+ <OutputPath>bin\Release\</OutputPath>
+ <DefineConstants>TRACE</DefineConstants>
+ <ErrorReport>prompt</ErrorReport>
+ <WarningLevel>4</WarningLevel>
+ </PropertyGroup>
+ <ItemGroup>
+ <Compile Include="Configuration\BaseApplicationConfiguration.cs" />
+ <Compile Include="Configuration\ServerConfiguration.cs" />
+ <Compile Include="DTO\AudioInfo.cs" />
+ <Compile Include="DTO\AudioOutputFormats.cs" />
+ <Compile Include="DTO\DtoUserItemData.cs" />
+ <Compile Include="DTO\MovieInfo.cs" />
+ <Compile Include="DTO\SeriesInfo.cs" />
+ <Compile Include="Authentication\AuthenticationResult.cs" />
+ <Compile Include="DTO\DtoBaseItem.cs" />
+ <Compile Include="DTO\DtoUser.cs" />
+ <Compile Include="DTO\VideoInfo.cs" />
+ <Compile Include="DTO\VideoOutputFormats.cs" />
+ <Compile Include="DTO\IbnItem.cs" />
+ <Compile Include="Entities\AudioStream.cs" />
+ <Compile Include="Entities\ImageType.cs" />
+ <Compile Include="Entities\IHasProviderIds.cs" />
+ <Compile Include="Entities\ItemSpecialCounts.cs" />
+ <Compile Include="Entities\MetadataProviders.cs" />
+ <Compile Include="Entities\SubtitleStream.cs" />
+ <Compile Include="Entities\VideoType.cs" />
+ <Compile Include="Plugins\BasePluginConfiguration.cs" />
+ <Compile Include="DTO\PluginInfo.cs" />
+ <Compile Include="Progress\TaskProgress.cs" />
+ <Compile Include="Properties\AssemblyInfo.cs" />
+ <Compile Include="Weather\WeatherForecast.cs" />
+ <Compile Include="Weather\WeatherInfo.cs" />
+ <Compile Include="Weather\WeatherStatus.cs" />
+ <Compile Include="Weather\WeatherUnits.cs" />
+ </ItemGroup>
+ <ItemGroup>
+ <Reference Include="protobuf-net">
+ <HintPath>..\protobuf-net\Full\portable\protobuf-net.dll</HintPath>
+ </Reference>
+ </ItemGroup>
+ <Import Project="$(MSBuildExtensionsPath32)\Microsoft\Portable\$(TargetFrameworkVersion)\Microsoft.Portable.CSharp.targets" />
+ <PropertyGroup>
+ <PostBuildEvent>"$(ProjectDir)..\protobuf-net\Precompile\precompile.exe" "$(TargetPath)" -o:"$(ProjectDir)bin\ProtobufModelSerializer.dll" -t:ProtobufModelSerializer</PostBuildEvent>
+ </PropertyGroup>
+ <!-- To modify your build process, add your task inside one of the targets below and uncomment it.
+ Other similar extension points exist, see Microsoft.Common.targets.
+ <Target Name="BeforeBuild">
+ </Target>
+ <Target Name="AfterBuild">
+ </Target>
+ -->
+</Project> \ No newline at end of file
diff --git a/MediaBrowser.Model/Plugins/BasePluginConfiguration.cs b/MediaBrowser.Model/Plugins/BasePluginConfiguration.cs
new file mode 100644
index 000000000..e33ebcce7
--- /dev/null
+++ b/MediaBrowser.Model/Plugins/BasePluginConfiguration.cs
@@ -0,0 +1,13 @@
+
+namespace MediaBrowser.Model.Plugins
+{
+ public class BasePluginConfiguration
+ {
+ public bool Enabled { get; set; }
+
+ public BasePluginConfiguration()
+ {
+ Enabled = true;
+ }
+ }
+}
diff --git a/MediaBrowser.Model/Progress/TaskProgress.cs b/MediaBrowser.Model/Progress/TaskProgress.cs
new file mode 100644
index 000000000..211875fff
--- /dev/null
+++ b/MediaBrowser.Model/Progress/TaskProgress.cs
@@ -0,0 +1,19 @@
+
+namespace MediaBrowser.Model.Progress
+{
+ /// <summary>
+ /// Represents a generic progress class that can be used with IProgress
+ /// </summary>
+ public class TaskProgress
+ {
+ /// <summary>
+ /// Gets or sets a description of the actions currently executing
+ /// </summary>
+ public string Description { get; set; }
+
+ /// <summary>
+ /// Gets or sets the current completion percentage
+ /// </summary>
+ public decimal? PercentComplete { get; set; }
+ }
+}
diff --git a/MediaBrowser.Model/Properties/AssemblyInfo.cs b/MediaBrowser.Model/Properties/AssemblyInfo.cs
new file mode 100644
index 000000000..a67dc993f
--- /dev/null
+++ b/MediaBrowser.Model/Properties/AssemblyInfo.cs
@@ -0,0 +1,28 @@
+using System.Reflection;
+using System.Resources;
+
+// 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.Model")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("MediaBrowser.Model")]
+[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.Model/Weather/WeatherForecast.cs b/MediaBrowser.Model/Weather/WeatherForecast.cs
new file mode 100644
index 000000000..f77d92366
--- /dev/null
+++ b/MediaBrowser.Model/Weather/WeatherForecast.cs
@@ -0,0 +1,30 @@
+using System;
+using ProtoBuf;
+
+namespace MediaBrowser.Model.Weather
+{
+ /// <summary>
+ /// Represents a weather forecase for a specific date
+ /// </summary>
+ [ProtoContract]
+ public class WeatherForecast
+ {
+ [ProtoMember(1)]
+ public DateTime Date { get; set; }
+
+ [ProtoMember(2)]
+ public int HighTemperatureFahrenheit { get; set; }
+
+ [ProtoMember(3)]
+ public int LowTemperatureFahrenheit { get; set; }
+
+ [ProtoMember(4)]
+ public int HighTemperatureCelsius { get; set; }
+
+ [ProtoMember(5)]
+ public int LowTemperatureCelsius { get; set; }
+
+ [ProtoMember(6)]
+ public WeatherConditions Condition { get; set; }
+ }
+}
diff --git a/MediaBrowser.Model/Weather/WeatherInfo.cs b/MediaBrowser.Model/Weather/WeatherInfo.cs
new file mode 100644
index 000000000..7cad4d248
--- /dev/null
+++ b/MediaBrowser.Model/Weather/WeatherInfo.cs
@@ -0,0 +1,14 @@
+using ProtoBuf;
+
+namespace MediaBrowser.Model.Weather
+{
+ [ProtoContract]
+ public class WeatherInfo
+ {
+ [ProtoMember(1)]
+ public WeatherStatus CurrentWeather { get; set; }
+
+ [ProtoMember(2)]
+ public WeatherForecast[] Forecasts { get; set; }
+ }
+}
diff --git a/MediaBrowser.Model/Weather/WeatherStatus.cs b/MediaBrowser.Model/Weather/WeatherStatus.cs
new file mode 100644
index 000000000..7019d319b
--- /dev/null
+++ b/MediaBrowser.Model/Weather/WeatherStatus.cs
@@ -0,0 +1,38 @@
+using ProtoBuf;
+
+namespace MediaBrowser.Model.Weather
+{
+ /// <summary>
+ /// Represents the current weather status
+ /// </summary>
+ [ProtoContract]
+ public class WeatherStatus
+ {
+ [ProtoMember(1)]
+ public int TemperatureFahrenheit { get; set; }
+
+ [ProtoMember(2)]
+ public int TemperatureCelsius { get; set; }
+
+ [ProtoMember(3)]
+ public int Humidity { get; set; }
+
+ [ProtoMember(4)]
+ public WeatherConditions Condition { get; set; }
+ }
+
+ public enum WeatherConditions
+ {
+ Sunny,
+ PartlyCloudy,
+ Cloudy,
+ Overcast,
+ Mist,
+ Snow,
+ Rain,
+ Sleet,
+ Fog,
+ Blizzard,
+ Thunderstorm
+ }
+}
diff --git a/MediaBrowser.Model/Weather/WeatherUnits.cs b/MediaBrowser.Model/Weather/WeatherUnits.cs
new file mode 100644
index 000000000..3bea67b9a
--- /dev/null
+++ b/MediaBrowser.Model/Weather/WeatherUnits.cs
@@ -0,0 +1,9 @@
+
+namespace MediaBrowser.Model.Weather
+{
+ public enum WeatherUnits
+ {
+ Fahrenheit,
+ Celsius
+ }
+}
diff --git a/MediaBrowser.Plugins.DefaultTheme/Converters/TileBackgroundConverter.cs b/MediaBrowser.Plugins.DefaultTheme/Converters/TileBackgroundConverter.cs
new file mode 100644
index 000000000..a7a964693
--- /dev/null
+++ b/MediaBrowser.Plugins.DefaultTheme/Converters/TileBackgroundConverter.cs
@@ -0,0 +1,43 @@
+using System;
+using System.Globalization;
+using System.Windows.Data;
+using System.Windows.Media;
+
+namespace MediaBrowser.Plugins.DefaultTheme.Converters
+{
+ public class TileBackgroundConverter : IValueConverter
+ {
+ private static readonly Brush[] TileColors = new Brush[] {
+ new SolidColorBrush(Color.FromRgb((byte)111,(byte)189,(byte)69)),
+ new SolidColorBrush(Color.FromRgb((byte)75,(byte)179,(byte)221)),
+ new SolidColorBrush(Color.FromRgb((byte)65,(byte)100,(byte)165)),
+ new SolidColorBrush(Color.FromRgb((byte)225,(byte)32,(byte)38)),
+ new SolidColorBrush(Color.FromRgb((byte)128,(byte)0,(byte)128)),
+ new SolidColorBrush(Color.FromRgb((byte)0,(byte)128,(byte)64)),
+ new SolidColorBrush(Color.FromRgb((byte)0,(byte)148,(byte)255)),
+ new SolidColorBrush(Color.FromRgb((byte)255,(byte)0,(byte)199)),
+ new SolidColorBrush(Color.FromRgb((byte)255,(byte)135,(byte)15)),
+ new SolidColorBrush(Color.FromRgb((byte)127,(byte)0,(byte)55))
+
+ };
+
+ private static int _currentIndex = new Random(DateTime.Now.Millisecond).Next(0, TileColors.Length);
+
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ int index;
+
+ lock (TileColors)
+ {
+ index = (_currentIndex++) % TileColors.Length;
+ }
+
+ return TileColors[index++];
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ throw new System.NotImplementedException();
+ }
+ }
+}
diff --git a/MediaBrowser.Plugins.DefaultTheme/Converters/WeatherImageConverter.cs b/MediaBrowser.Plugins.DefaultTheme/Converters/WeatherImageConverter.cs
new file mode 100644
index 000000000..0d73a1a6f
--- /dev/null
+++ b/MediaBrowser.Plugins.DefaultTheme/Converters/WeatherImageConverter.cs
@@ -0,0 +1,43 @@
+using MediaBrowser.Model.Weather;
+using System;
+using System.ComponentModel.Composition;
+using System.Globalization;
+using System.Windows.Data;
+
+namespace MediaBrowser.Plugins.DefaultTheme.Converters
+{
+ [PartNotDiscoverable]
+ public class WeatherImageConverter : IValueConverter
+ {
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ var weather = value as WeatherInfo;
+
+ if (weather != null)
+ {
+ switch (weather.CurrentWeather.Condition)
+ {
+ case WeatherConditions.Thunderstorm:
+ return "../Images/Weather/Thunder.png";
+ case WeatherConditions.Overcast:
+ return "../Images/Weather/Overcast.png";
+ case WeatherConditions.Mist:
+ case WeatherConditions.Sleet:
+ case WeatherConditions.Rain:
+ return "../Images/Weather/Rain.png";
+ case WeatherConditions.Blizzard:
+ case WeatherConditions.Snow:
+ return "../Images/Weather/Snow.png";
+ default:
+ return "../Images/Weather/Sunny.png";
+ }
+ }
+ return null;
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
+ }
+}
diff --git a/MediaBrowser.Plugins.DefaultTheme/MediaBrowser.Plugins.DefaultTheme.csproj b/MediaBrowser.Plugins.DefaultTheme/MediaBrowser.Plugins.DefaultTheme.csproj
new file mode 100644
index 000000000..2fe9914ab
--- /dev/null
+++ b/MediaBrowser.Plugins.DefaultTheme/MediaBrowser.Plugins.DefaultTheme.csproj
@@ -0,0 +1,132 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
+ <PropertyGroup>
+ <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+ <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+ <ProjectGuid>{6E892999-711D-4E24-8BAC-DACF5BFA783A}</ProjectGuid>
+ <OutputType>library</OutputType>
+ <AppDesignerFolder>Properties</AppDesignerFolder>
+ <RootNamespace>MediaBrowser.Plugins.DefaultTheme</RootNamespace>
+ <AssemblyName>MediaBrowser.Plugins.DefaultTheme</AssemblyName>
+ <TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
+ <FileAlignment>512</FileAlignment>
+ <ProjectTypeGuids>{60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
+ <WarningLevel>4</WarningLevel>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
+ <DebugSymbols>true</DebugSymbols>
+ <DebugType>full</DebugType>
+ <Optimize>false</Optimize>
+ <OutputPath>bin\Debug\</OutputPath>
+ <DefineConstants>DEBUG;TRACE</DefineConstants>
+ <ErrorReport>prompt</ErrorReport>
+ <WarningLevel>4</WarningLevel>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
+ <DebugType>pdbonly</DebugType>
+ <Optimize>true</Optimize>
+ <OutputPath>bin\Release\</OutputPath>
+ <DefineConstants>TRACE</DefineConstants>
+ <ErrorReport>prompt</ErrorReport>
+ <WarningLevel>4</WarningLevel>
+ </PropertyGroup>
+ <PropertyGroup>
+ <RunPostBuildEvent>Always</RunPostBuildEvent>
+ </PropertyGroup>
+ <ItemGroup>
+ <Reference Include="System" />
+ <Reference Include="System.ComponentModel.Composition" />
+ <Reference Include="System.Data" />
+ <Reference Include="System.Xml" />
+ <Reference Include="Microsoft.CSharp" />
+ <Reference Include="System.Core" />
+ <Reference Include="System.Xml.Linq" />
+ <Reference Include="System.Data.DataSetExtensions" />
+ <Reference Include="System.Xaml">
+ <RequiredTargetFramework>4.0</RequiredTargetFramework>
+ </Reference>
+ <Reference Include="WindowsBase" />
+ <Reference Include="PresentationCore" />
+ <Reference Include="PresentationFramework" />
+ </ItemGroup>
+ <ItemGroup>
+ <Compile Include="Pages\LoginPage.xaml.cs">
+ <DependentUpon>LoginPage.xaml</DependentUpon>
+ </Compile>
+ <Compile Include="Resources\AppResources.cs" />
+ <Compile Include="Converters\TileBackgroundConverter.cs" />
+ <Compile Include="Converters\WeatherImageConverter.cs" />
+ <Compile Include="Plugin.cs" />
+ <Compile Include="Properties\AssemblyInfo.cs">
+ <SubType>Code</SubType>
+ </Compile>
+ <Compile Include="Properties\Resources.Designer.cs">
+ <AutoGen>True</AutoGen>
+ <DesignTime>True</DesignTime>
+ <DependentUpon>Resources.resx</DependentUpon>
+ </Compile>
+ <Compile Include="Properties\Settings.Designer.cs">
+ <AutoGen>True</AutoGen>
+ <DependentUpon>Settings.settings</DependentUpon>
+ <DesignTimeSharedInput>True</DesignTimeSharedInput>
+ </Compile>
+ <EmbeddedResource Include="Properties\Resources.resx">
+ <Generator>ResXFileCodeGenerator</Generator>
+ <LastGenOutput>Resources.Designer.cs</LastGenOutput>
+ </EmbeddedResource>
+ <None Include="Properties\Settings.settings">
+ <Generator>SettingsSingleFileGenerator</Generator>
+ <LastGenOutput>Settings.Designer.cs</LastGenOutput>
+ </None>
+ <AppDesigner Include="Properties\" />
+ </ItemGroup>
+ <ItemGroup>
+ <ProjectReference Include="..\..\MediaBrowserServer\MediaBrowser.Common\MediaBrowser.Common.csproj">
+ <Project>{9142eefa-7570-41e1-bfcc-468bb571af2f}</Project>
+ <Name>MediaBrowser.Common</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\..\MediaBrowserServer\MediaBrowser.Controller\MediaBrowser.Controller.csproj">
+ <Project>{17e1f4e6-8abd-4fe5-9ecf-43d4b6087ba2}</Project>
+ <Name>MediaBrowser.Controller</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\..\MediaBrowserServer\MediaBrowser.Model\MediaBrowser.Model.csproj">
+ <Project>{7eeeb4bb-f3e8-48fc-b4c5-70f0fff8329b}</Project>
+ <Name>MediaBrowser.Model</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\..\MediaBrowserUI\MediaBrowser.UI\MediaBrowser.UI.csproj">
+ <Project>{b5ece1fb-618e-420b-9a99-8e972d76920a}</Project>
+ <Name>MediaBrowser.UI</Name>
+ </ProjectReference>
+ </ItemGroup>
+ <ItemGroup>
+ <Page Include="Pages\LoginPage.xaml">
+ <SubType>Designer</SubType>
+ <Generator>MSBuild:Compile</Generator>
+ </Page>
+ <Page Include="Resources\AppResources.xaml">
+ <SubType>Designer</SubType>
+ <Generator>MSBuild:Compile</Generator>
+ </Page>
+ </ItemGroup>
+ <ItemGroup>
+ <Resource Include="Resources\Images\CurrentUserDefault.png" />
+ <Resource Include="Resources\Images\UserLoginDefault.png" />
+ <Resource Include="Resources\Images\Weather\Overcast.png" />
+ <Resource Include="Resources\Images\Weather\Rain.png" />
+ <Resource Include="Resources\Images\Weather\Snow.png" />
+ <Resource Include="Resources\Images\Weather\Sunny.png" />
+ <Resource Include="Resources\Images\Weather\Thunder.png" />
+ </ItemGroup>
+ <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
+ <PropertyGroup>
+ <PostBuildEvent>xcopy "$(TargetPath)" "$(SolutionDir)\ProgramData-Server\Plugins\" /y</PostBuildEvent>
+ </PropertyGroup>
+ <!-- To modify your build process, add your task inside one of the targets below and uncomment it.
+ Other similar extension points exist, see Microsoft.Common.targets.
+ <Target Name="BeforeBuild">
+ </Target>
+ <Target Name="AfterBuild">
+ </Target>
+ -->
+</Project> \ No newline at end of file
diff --git a/MediaBrowser.Plugins.DefaultTheme/Pages/LoginPage.xaml b/MediaBrowser.Plugins.DefaultTheme/Pages/LoginPage.xaml
new file mode 100644
index 000000000..4b1552bd8
--- /dev/null
+++ b/MediaBrowser.Plugins.DefaultTheme/Pages/LoginPage.xaml
@@ -0,0 +1,64 @@
+<base:BaseLoginPage x:Class="MediaBrowser.Plugins.DefaultTheme.Pages.LoginPage"
+ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+ xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+ xmlns:base="clr-namespace:MediaBrowser.UI.Pages;assembly=MediaBrowser.UI"
+ xmlns:DTO="clr-namespace:MediaBrowser.Model.DTO;assembly=MediaBrowser.Model"
+ xmlns:controls="clr-namespace:MediaBrowser.UI.Controls;assembly=MediaBrowser.UI" mc:Ignorable="d"
+ d:DesignHeight="300"
+ d:DesignWidth="300"
+ Title="LoginPage">
+
+ <Page.Resources>
+ <ResourceDictionary>
+ <DataTemplate DataType="{x:Type DTO:DtoUser}">
+ <Grid HorizontalAlignment="Left" Margin="3">
+
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition Width="auto"></ColumnDefinition>
+ <ColumnDefinition Width="475"></ColumnDefinition>
+ </Grid.ColumnDefinitions>
+
+ <controls:ExtendedImage HasImage="{Binding HasImage}"
+ PlaceHolderSource="../Resources/Images/UserLoginDefault.png"
+ Source="{Binding Converter={StaticResource UserImageConverter}, ConverterParameter='225,225,0,0'}"
+ Stretch="Uniform"
+ Width="225"
+ Height="225"
+ Background="{Binding Converter={StaticResource TileBackgroundConverter}}"/>
+ <TextBlock Text="{Binding Name}" VerticalAlignment="Top" HorizontalAlignment="Left" Grid.Column="1" Grid.Row="0" Margin="25 30 0 0" FontSize="{StaticResource Heading2FontSize}"></TextBlock>
+ <TextBlock Text="{Binding Converter={StaticResource LastSeenTextConverter}}" VerticalAlignment="Center" HorizontalAlignment="Left" Grid.Column="1" Grid.Row="0" Margin="25 80 0 0"></TextBlock>
+ </Grid>
+ </DataTemplate>
+
+ </ResourceDictionary>
+ </Page.Resources>
+ <Grid>
+
+ <Grid.RowDefinitions>
+ <RowDefinition Height="auto"></RowDefinition>
+ <RowDefinition Height="*"></RowDefinition>
+ </Grid.RowDefinitions>
+
+ <Image Style="{StaticResource MBLogoImageBlack}" Margin="0 0 0 10" Height="125" Stretch="Uniform" HorizontalAlignment="Left"></Image>
+
+ <Grid VerticalAlignment="Stretch" HorizontalAlignment="Center" Grid.Row="1">
+
+ <Grid.RowDefinitions>
+ <RowDefinition Height="auto"></RowDefinition>
+ <RowDefinition Height="*"></RowDefinition>
+ </Grid.RowDefinitions>
+
+ <TextBlock FontSize="{StaticResource Heading2FontSize}" Grid.Row="0" Margin="0 0 0 30">Select Profile</TextBlock>
+
+ <ListView HorizontalAlignment="Center" Grid.Row="1" ScrollViewer.VerticalScrollBarVisibility="Disabled" ScrollViewer.HorizontalScrollBarVisibility="Hidden" ItemsSource="{Binding Path=Users}" Style="{StaticResource ListViewStyle}" ItemContainerStyle="{StaticResource ListViewItemStyle}">
+ <ListView.ItemsPanel>
+ <ItemsPanelTemplate>
+ <WrapPanel Orientation="Vertical" />
+ </ItemsPanelTemplate>
+ </ListView.ItemsPanel>
+ </ListView>
+ </Grid>
+ </Grid>
+</base:BaseLoginPage>
diff --git a/MediaBrowser.Plugins.DefaultTheme/Pages/LoginPage.xaml.cs b/MediaBrowser.Plugins.DefaultTheme/Pages/LoginPage.xaml.cs
new file mode 100644
index 000000000..547443086
--- /dev/null
+++ b/MediaBrowser.Plugins.DefaultTheme/Pages/LoginPage.xaml.cs
@@ -0,0 +1,15 @@
+using MediaBrowser.UI.Pages;
+
+namespace MediaBrowser.Plugins.DefaultTheme.Pages
+{
+ /// <summary>
+ /// Interaction logic for LoginPage.xaml
+ /// </summary>
+ public partial class LoginPage : BaseLoginPage
+ {
+ public LoginPage()
+ {
+ InitializeComponent();
+ }
+ }
+}
diff --git a/MediaBrowser.Plugins.DefaultTheme/Plugin.cs b/MediaBrowser.Plugins.DefaultTheme/Plugin.cs
new file mode 100644
index 000000000..f12d53af1
--- /dev/null
+++ b/MediaBrowser.Plugins.DefaultTheme/Plugin.cs
@@ -0,0 +1,20 @@
+using MediaBrowser.Common.Plugins;
+using System;
+using System.ComponentModel.Composition;
+
+namespace MediaBrowser.Plugins.DefaultTheme
+{
+ [Export(typeof(BasePlugin))]
+ public class Plugin : BaseTheme
+ {
+ public override string Name
+ {
+ get { return "Default Theme"; }
+ }
+
+ public override Uri LoginPageUri
+ {
+ get { return GeneratePackUri("Pages/LoginPage.xaml"); }
+ }
+ }
+}
diff --git a/MediaBrowser.Plugins.DefaultTheme/Properties/AssemblyInfo.cs b/MediaBrowser.Plugins.DefaultTheme/Properties/AssemblyInfo.cs
new file mode 100644
index 000000000..3d92517da
--- /dev/null
+++ b/MediaBrowser.Plugins.DefaultTheme/Properties/AssemblyInfo.cs
@@ -0,0 +1,55 @@
+using System.Reflection;
+using System.Resources;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using System.Windows;
+
+// 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.Plugins.DefaultTheme")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("MediaBrowser.Plugins.DefaultTheme")]
+[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)]
+
+//In order to begin building localizable applications, set
+//<UICulture>CultureYouAreCodingWith</UICulture> in your .csproj file
+//inside a <PropertyGroup>. For example, if you are using US english
+//in your source files, set the <UICulture> to en-US. Then uncomment
+//the NeutralResourceLanguage attribute below. Update the "en-US" in
+//the line below to match the UICulture setting in the project file.
+
+//[assembly: NeutralResourcesLanguage("en-US", UltimateResourceFallbackLocation.Satellite)]
+
+
+[assembly:ThemeInfo(
+ ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
+ //(used if a resource is not found in the page,
+ // or application resource dictionaries)
+ ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
+ //(used if a resource is not found in the page,
+ // app, or any theme specific resource dictionaries)
+)]
+
+
+// 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.Plugins.DefaultTheme/Properties/Resources.Designer.cs b/MediaBrowser.Plugins.DefaultTheme/Properties/Resources.Designer.cs
new file mode 100644
index 000000000..da735391a
--- /dev/null
+++ b/MediaBrowser.Plugins.DefaultTheme/Properties/Resources.Designer.cs
@@ -0,0 +1,62 @@
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.17929
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace MediaBrowser.Plugins.DefaultTheme.Properties {
+
+
+ /// <summary>
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ /// </summary>
+ // This class was auto-generated by the StronglyTypedResourceBuilder
+ // class via a tool like ResGen or Visual Studio.
+ // To add or remove a member, edit your .ResX file then rerun ResGen
+ // with the /str option, or rebuild your VS project.
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ internal class Resources {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal Resources() {
+ }
+
+ /// <summary>
+ /// Returns the cached ResourceManager instance used by this class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if ((resourceMan == null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("MediaBrowser.Plugins.DefaultTheme.Properties.Resources", typeof(Resources).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ /// <summary>
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Plugins.DefaultTheme/Properties/Resources.resx b/MediaBrowser.Plugins.DefaultTheme/Properties/Resources.resx
new file mode 100644
index 000000000..ffecec851
--- /dev/null
+++ b/MediaBrowser.Plugins.DefaultTheme/Properties/Resources.resx
@@ -0,0 +1,117 @@
+<?xml version="1.0" encoding="utf-8"?>
+<root>
+ <!--
+ Microsoft ResX Schema
+
+ Version 2.0
+
+ The primary goals of this format is to allow a simple XML format
+ that is mostly human readable. The generation and parsing of the
+ various data types are done through the TypeConverter classes
+ associated with the data types.
+
+ Example:
+
+ ... ado.net/XML headers & schema ...
+ <resheader name="resmimetype">text/microsoft-resx</resheader>
+ <resheader name="version">2.0</resheader>
+ <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+ <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+ <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+ <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+ <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+ <value>[base64 mime encoded serialized .NET Framework object]</value>
+ </data>
+ <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+ <comment>This is a comment</comment>
+ </data>
+
+ There are any number of "resheader" rows that contain simple
+ name/value pairs.
+
+ Each data row contains a name, and value. The row also contains a
+ type or mimetype. Type corresponds to a .NET class that support
+ text/value conversion through the TypeConverter architecture.
+ Classes that don't support this are serialized and stored with the
+ mimetype set.
+
+ The mimetype is used for serialized objects, and tells the
+ ResXResourceReader how to depersist the object. This is currently not
+ extensible. For a given mimetype the value must be set accordingly:
+
+ Note - application/x-microsoft.net.object.binary.base64 is the format
+ that the ResXResourceWriter will generate, however the reader can
+ read any of the formats listed below.
+
+ mimetype: application/x-microsoft.net.object.binary.base64
+ value : The object must be serialized with
+ : System.Serialization.Formatters.Binary.BinaryFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.soap.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.bytearray.base64
+ value : The object must be serialized into a byte array
+ : using a System.ComponentModel.TypeConverter
+ : and then encoded with base64 encoding.
+ -->
+ <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+ <xsd:element name="root" msdata:IsDataSet="true">
+ <xsd:complexType>
+ <xsd:choice maxOccurs="unbounded">
+ <xsd:element name="metadata">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" />
+ <xsd:attribute name="type" type="xsd:string" />
+ <xsd:attribute name="mimetype" type="xsd:string" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="assembly">
+ <xsd:complexType>
+ <xsd:attribute name="alias" type="xsd:string" />
+ <xsd:attribute name="name" type="xsd:string" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="data">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" msdata:Ordinal="1" />
+ <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+ <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="resheader">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" />
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:choice>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:schema>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>2.0</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+</root> \ No newline at end of file
diff --git a/MediaBrowser.Plugins.DefaultTheme/Properties/Settings.Designer.cs b/MediaBrowser.Plugins.DefaultTheme/Properties/Settings.Designer.cs
new file mode 100644
index 000000000..b99760e3f
--- /dev/null
+++ b/MediaBrowser.Plugins.DefaultTheme/Properties/Settings.Designer.cs
@@ -0,0 +1,30 @@
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.17929
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace MediaBrowser.Plugins.DefaultTheme.Properties
+{
+
+
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "11.0.0.0")]
+ internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase
+ {
+
+ private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));
+
+ public static Settings Default
+ {
+ get
+ {
+ return defaultInstance;
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Plugins.DefaultTheme/Properties/Settings.settings b/MediaBrowser.Plugins.DefaultTheme/Properties/Settings.settings
new file mode 100644
index 000000000..8f2fd95d6
--- /dev/null
+++ b/MediaBrowser.Plugins.DefaultTheme/Properties/Settings.settings
@@ -0,0 +1,7 @@
+<?xml version='1.0' encoding='utf-8'?>
+<SettingsFile xmlns="uri:settings" CurrentProfile="(Default)">
+ <Profiles>
+ <Profile Name="(Default)" />
+ </Profiles>
+ <Settings />
+</SettingsFile> \ No newline at end of file
diff --git a/MediaBrowser.Plugins.DefaultTheme/Resources/AppResources.cs b/MediaBrowser.Plugins.DefaultTheme/Resources/AppResources.cs
new file mode 100644
index 000000000..28128c75b
--- /dev/null
+++ b/MediaBrowser.Plugins.DefaultTheme/Resources/AppResources.cs
@@ -0,0 +1,14 @@
+using System.ComponentModel.Composition;
+using System.Windows;
+
+namespace MediaBrowser.Plugins.DefaultTheme.Resources
+{
+ [Export(typeof(ResourceDictionary))]
+ public partial class AppResources : ResourceDictionary
+ {
+ public AppResources()
+ {
+ InitializeComponent();
+ }
+ }
+}
diff --git a/MediaBrowser.Plugins.DefaultTheme/Resources/AppResources.xaml b/MediaBrowser.Plugins.DefaultTheme/Resources/AppResources.xaml
new file mode 100644
index 000000000..c1e5adeb5
--- /dev/null
+++ b/MediaBrowser.Plugins.DefaultTheme/Resources/AppResources.xaml
@@ -0,0 +1,81 @@
+<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ xmlns:themeconverters="clr-namespace:MediaBrowser.Plugins.DefaultTheme.Converters"
+ x:Class="MediaBrowser.Plugins.DefaultTheme.Resources.AppResources">
+
+ <themeconverters:WeatherImageConverter x:Key="WeatherImageConverter"></themeconverters:WeatherImageConverter>
+ <themeconverters:TileBackgroundConverter x:Key="TileBackgroundConverter"></themeconverters:TileBackgroundConverter>
+
+ <Style x:Key="ListViewItemStyle" TargetType="{x:Type ListViewItem}" BasedOn="{StaticResource BaseListViewItemStyle}">
+
+ <Style.Resources>
+ <SolidColorBrush x:Key="{x:Static SystemColors.HighlightBrushKey}" Color="LightBlue"/>
+ </Style.Resources>
+
+ <Style.Triggers>
+ <Trigger Property="IsMouseOver" Value="True">
+ <Setter Property="Opacity" Value=".85" />
+ </Trigger>
+ </Style.Triggers>
+ </Style>
+
+ <!--Override MainWindow style-->
+ <Style TargetType="Window" x:Key="MainWindow" BasedOn="{StaticResource BaseWindow}">
+ <Setter Property="Background">
+ <Setter.Value>
+ <RadialGradientBrush RadiusX=".75" RadiusY=".75">
+ <GradientStop Color="White" Offset="0.0"/>
+ <GradientStop Color="WhiteSmoke" Offset="0.5"/>
+ <GradientStop Color="#cfcfcf" Offset="1.0"/>
+ </RadialGradientBrush>
+ </Setter.Value>
+ </Setter>
+ </Style>
+
+ <!--Override PageContentTemplate-->
+ <ControlTemplate x:Key="PageContentTemplate">
+
+ <Grid Margin="20 15 20 20">
+
+ <StackPanel Orientation="Horizontal" VerticalAlignment="Top" HorizontalAlignment="Right">
+
+ <!--Display CurrentUser-->
+ <StackPanel Orientation="Horizontal" Margin="0 0 30 0" Visibility="{Binding Path=CurrentUser,Converter={StaticResource CurrentUserVisibilityConverter}}">
+ <TextBlock FontSize="{StaticResource Heading2FontSize}" Text="{Binding Path=CurrentUser.Name}" Margin="0 0 5 0">
+ </TextBlock>
+ <Image>
+ <Image.Style>
+ <Style TargetType="{x:Type Image}">
+ <Setter Property="Image.Source" Value="Images\CurrentUserDefault.png" />
+ <Setter Property="Stretch" Value="None" />
+ <Style.Triggers>
+ <DataTrigger Binding="{Binding Path=CurrentUser.HasImage}" Value="true">
+ <Setter Property="Image.Source" Value="{Binding Path=CurrentUser,Converter={StaticResource UserImageConverter}, ConverterParameter='0,64,0,0'}" />
+ </DataTrigger>
+ </Style.Triggers>
+ </Style>
+ </Image.Style>
+ </Image>
+ </StackPanel>
+
+ <!--Display Weather-->
+ <StackPanel Orientation="Horizontal" Margin="0 0 30 0" Visibility="{Binding Path=CurrentWeather,Converter={StaticResource WeatherVisibilityConverter}}">
+
+ <TextBlock FontSize="{StaticResource Heading2FontSize}" Text="{Binding Path=CurrentWeather,Converter={StaticResource WeatherTemperatureConverter}}" Margin="0 0 5 0">
+ </TextBlock>
+ <Image Stretch="None" Source="{Binding Path=CurrentWeather,Converter={StaticResource WeatherImageConverter}}"></Image>
+ </StackPanel>
+
+ <!--Display Clock-->
+ <TextBlock FontSize="{StaticResource Heading2FontSize}">
+ <TextBlock.Text>
+ <Binding Path="CurrentTime" Converter="{StaticResource DateTimeToStringConverter}" ConverterParameter="h:mm" />
+ </TextBlock.Text>
+ </TextBlock>
+ </StackPanel>
+
+ <Frame x:Name="PageFrame"></Frame>
+ </Grid>
+ </ControlTemplate>
+
+</ResourceDictionary> \ No newline at end of file
diff --git a/MediaBrowser.Plugins.DefaultTheme/Resources/Images/CurrentUserDefault.png b/MediaBrowser.Plugins.DefaultTheme/Resources/Images/CurrentUserDefault.png
new file mode 100644
index 000000000..f272ed92a
--- /dev/null
+++ b/MediaBrowser.Plugins.DefaultTheme/Resources/Images/CurrentUserDefault.png
Binary files differ
diff --git a/MediaBrowser.Plugins.DefaultTheme/Resources/Images/UserLoginDefault.png b/MediaBrowser.Plugins.DefaultTheme/Resources/Images/UserLoginDefault.png
new file mode 100644
index 000000000..93a06e308
--- /dev/null
+++ b/MediaBrowser.Plugins.DefaultTheme/Resources/Images/UserLoginDefault.png
Binary files differ
diff --git a/MediaBrowser.Plugins.DefaultTheme/Resources/Images/Weather/Overcast.png b/MediaBrowser.Plugins.DefaultTheme/Resources/Images/Weather/Overcast.png
new file mode 100644
index 000000000..b9b6765c7
--- /dev/null
+++ b/MediaBrowser.Plugins.DefaultTheme/Resources/Images/Weather/Overcast.png
Binary files differ
diff --git a/MediaBrowser.Plugins.DefaultTheme/Resources/Images/Weather/Rain.png b/MediaBrowser.Plugins.DefaultTheme/Resources/Images/Weather/Rain.png
new file mode 100644
index 000000000..2e526f895
--- /dev/null
+++ b/MediaBrowser.Plugins.DefaultTheme/Resources/Images/Weather/Rain.png
Binary files differ
diff --git a/MediaBrowser.Plugins.DefaultTheme/Resources/Images/Weather/Snow.png b/MediaBrowser.Plugins.DefaultTheme/Resources/Images/Weather/Snow.png
new file mode 100644
index 000000000..94131ed2d
--- /dev/null
+++ b/MediaBrowser.Plugins.DefaultTheme/Resources/Images/Weather/Snow.png
Binary files differ
diff --git a/MediaBrowser.Plugins.DefaultTheme/Resources/Images/Weather/Sunny.png b/MediaBrowser.Plugins.DefaultTheme/Resources/Images/Weather/Sunny.png
new file mode 100644
index 000000000..2a51cd544
--- /dev/null
+++ b/MediaBrowser.Plugins.DefaultTheme/Resources/Images/Weather/Sunny.png
Binary files differ
diff --git a/MediaBrowser.Plugins.DefaultTheme/Resources/Images/Weather/Thunder.png b/MediaBrowser.Plugins.DefaultTheme/Resources/Images/Weather/Thunder.png
new file mode 100644
index 000000000..f413a2ed7
--- /dev/null
+++ b/MediaBrowser.Plugins.DefaultTheme/Resources/Images/Weather/Thunder.png
Binary files differ
diff --git a/MediaBrowser.ServerApplication/App.config b/MediaBrowser.ServerApplication/App.config
new file mode 100644
index 000000000..a5c945338
--- /dev/null
+++ b/MediaBrowser.ServerApplication/App.config
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<configuration>
+ <appSettings>
+ <add key="ProgramDataPath" value="..\..\..\ProgramData-Server" />
+ </appSettings>
+ <startup>
+ <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
+ </startup>
+ <runtime>
+ <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
+ <dependentAssembly>
+ <assemblyIdentity name="System.Net.Http" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
+ <bindingRedirect oldVersion="0.0.0.0-4.0.0.0" newVersion="4.0.0.0" />
+ </dependentAssembly>
+ <dependentAssembly>
+ <assemblyIdentity name="System.Reactive.Core" publicKeyToken="f300afd708cefcd3" culture="neutral" />
+ <bindingRedirect oldVersion="0.0.0.0-2.0.20823.0" newVersion="2.0.20823.0" />
+ </dependentAssembly>
+ <dependentAssembly>
+ <assemblyIdentity name="System.Reactive.Interfaces" publicKeyToken="f300afd708cefcd3" culture="neutral" />
+ <bindingRedirect oldVersion="0.0.0.0-2.0.20823.0" newVersion="2.0.20823.0" />
+ </dependentAssembly>
+ </assemblyBinding>
+ </runtime>
+</configuration> \ No newline at end of file
diff --git a/MediaBrowser.ServerApplication/App.xaml b/MediaBrowser.ServerApplication/App.xaml
new file mode 100644
index 000000000..6b7f6c38f
--- /dev/null
+++ b/MediaBrowser.ServerApplication/App.xaml
@@ -0,0 +1,8 @@
+<z:BaseApplication x:Class="MediaBrowser.ServerApplication.App"
+ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ xmlns:z="clr-namespace:MediaBrowser.Common.UI;assembly=MediaBrowser.Common">
+ <Application.Resources>
+
+ </Application.Resources>
+</z:BaseApplication>
diff --git a/MediaBrowser.ServerApplication/App.xaml.cs b/MediaBrowser.ServerApplication/App.xaml.cs
new file mode 100644
index 000000000..41fb8f24e
--- /dev/null
+++ b/MediaBrowser.ServerApplication/App.xaml.cs
@@ -0,0 +1,67 @@
+using MediaBrowser.Common.Kernel;
+using MediaBrowser.Common.UI;
+using MediaBrowser.Controller;
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Windows;
+
+namespace MediaBrowser.ServerApplication
+{
+ /// <summary>
+ /// Interaction logic for App.xaml
+ /// </summary>
+ public partial class App : BaseApplication, IApplication
+ {
+ [STAThread]
+ public static void Main()
+ {
+ RunApplication<App>("MediaBrowserServer");
+ }
+
+ protected override void OnSecondInstanceLaunched(IList<string> args)
+ {
+ base.OnSecondInstanceLaunched(args);
+
+ OpenDashboard();
+ InitializeComponent();
+ }
+
+ public static void OpenDashboard()
+ {
+ OpenUrl("http://localhost:" + Kernel.Instance.Configuration.HttpServerPortNumber + "/mediabrowser/dashboard/index.html");
+ }
+
+ public static void OpenUrl(string url)
+ {
+ var process = new Process
+ {
+ StartInfo = new ProcessStartInfo
+ {
+ FileName = url
+ },
+
+ EnableRaisingEvents = true
+ };
+
+ process.Exited += ProcessExited;
+
+ process.Start();
+ }
+
+ static void ProcessExited(object sender, EventArgs e)
+ {
+ (sender as Process).Dispose();
+ }
+
+ protected override IKernel InstantiateKernel()
+ {
+ return new Kernel();
+ }
+
+ protected override Window InstantiateMainWindow()
+ {
+ return new MainWindow();
+ }
+ }
+}
diff --git a/MediaBrowser.ServerApplication/MainWindow.xaml b/MediaBrowser.ServerApplication/MainWindow.xaml
new file mode 100644
index 000000000..ade1caee9
--- /dev/null
+++ b/MediaBrowser.ServerApplication/MainWindow.xaml
@@ -0,0 +1,41 @@
+<Window x:Class="MediaBrowser.ServerApplication.MainWindow"
+ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ xmlns:tb="http://www.hardcodet.net/taskbar"
+ Title="MainWindow" Height="350" Width="525" AllowsTransparency="True" Background="Transparent" WindowStyle="None" ShowInTaskbar="False">
+ <Grid>
+ <tb:TaskbarIcon Name="MbTaskbarIcon" ToolTipText="MediaBrowser Server">
+
+ <tb:TaskbarIcon.ContextMenu>
+ <ContextMenu Background="White">
+ <MenuItem Name="cmOpenDashboard" Header="Open Dashboard" Click="cmOpenDashboard_click"/>
+ <MenuItem Name="cmdReloadServer" Header="Reload Server" Click="cmdReloadServer_click"/>
+ <MenuItem Name="cmVisitCT" Header="Visit Community Tracker" Click="cmVisitCT_click"/>
+ <Separator/>
+ <MenuItem Name="cmExit" Header="Exit" Click="cmExit_click"/>
+ </ContextMenu>
+ </tb:TaskbarIcon.ContextMenu>
+
+ <tb:TaskbarIcon.Style>
+ <Style TargetType="{x:Type tb:TaskbarIcon}">
+ <Setter Property="IconSource" Value="/Resources/Images/icon.ico" />
+ <Style.Triggers>
+ <DataTrigger Binding="{Binding LoadingImageIndex}" Value="1">
+ <Setter Property="IconSource" Value="/Resources/Images/loadingIcon1.ico" />
+ </DataTrigger>
+ <DataTrigger Binding="{Binding LoadingImageIndex}" Value="2">
+ <Setter Property="IconSource" Value="/Resources/Images/loadingIcon2.ico" />
+ </DataTrigger>
+ <DataTrigger Binding="{Binding LoadingImageIndex}" Value="3">
+ <Setter Property="IconSource" Value="/Resources/Images/loadingIcon3.ico" />
+ </DataTrigger>
+ <DataTrigger Binding="{Binding LoadingImageIndex}" Value="4">
+ <Setter Property="IconSource" Value="/Resources/Images/loadingIcon4.ico" />
+ </DataTrigger>
+ </Style.Triggers>
+ </Style>
+ </tb:TaskbarIcon.Style>
+
+ </tb:TaskbarIcon>
+ </Grid>
+</Window>
diff --git a/MediaBrowser.ServerApplication/MainWindow.xaml.cs b/MediaBrowser.ServerApplication/MainWindow.xaml.cs
new file mode 100644
index 000000000..6180482d7
--- /dev/null
+++ b/MediaBrowser.ServerApplication/MainWindow.xaml.cs
@@ -0,0 +1,109 @@
+using Hardcodet.Wpf.TaskbarNotification;
+using MediaBrowser.Common.Events;
+using MediaBrowser.Controller;
+using MediaBrowser.Model.Progress;
+using System;
+using System.ComponentModel;
+using System.Threading;
+using System.Windows;
+
+namespace MediaBrowser.ServerApplication
+{
+ /// <summary>
+ /// Interaction logic for MainWindow.xaml
+ /// </summary>
+ public partial class MainWindow : Window, INotifyPropertyChanged
+ {
+ private Timer LoadingIconTimer { get; set; }
+
+ public MainWindow()
+ {
+ InitializeComponent();
+ Loaded += MainWindowLoaded;
+ }
+
+ void MainWindowLoaded(object sender, RoutedEventArgs e)
+ {
+ DataContext = this;
+
+ Kernel.Instance.ReloadBeginning += KernelReloadBeginning;
+ Kernel.Instance.ReloadCompleted += KernelReloadCompleted;
+ }
+
+ void KernelReloadBeginning(object sender, GenericEventArgs<IProgress<TaskProgress>> e)
+ {
+ MbTaskbarIcon.ShowBalloonTip("Media Browser is reloading", "Please wait...", BalloonIcon.Info);
+
+ LoadingImageIndex = 0;
+
+ LoadingIconTimer = new Timer(LoadingIconTimerCallback, null, 0, 250);
+ }
+
+ void KernelReloadCompleted(object sender, GenericEventArgs<IProgress<TaskProgress>> e)
+ {
+ LoadingIconTimer.Dispose();
+
+ LoadingImageIndex = 0;
+ }
+
+ public event PropertyChangedEventHandler PropertyChanged;
+
+ public void OnPropertyChanged(String info)
+ {
+ if (PropertyChanged != null)
+ {
+ PropertyChanged(this, new PropertyChangedEventArgs(info));
+ }
+ }
+
+ private int _loadingImageIndex;
+ public int LoadingImageIndex
+ {
+ get { return _loadingImageIndex; }
+ set
+ {
+ _loadingImageIndex = value;
+ OnPropertyChanged("LoadingImageIndex");
+ }
+ }
+
+ #region Context Menu events
+
+ private void cmOpenDashboard_click(object sender, RoutedEventArgs e)
+ {
+ App.OpenDashboard();
+ }
+
+ private void cmVisitCT_click(object sender, RoutedEventArgs e)
+ {
+ App.OpenUrl("http://community.mediabrowser.tv/");
+ }
+
+ private void cmExit_click(object sender, RoutedEventArgs e)
+ {
+ Close();
+ }
+
+ private async void cmdReloadServer_click(object sender, RoutedEventArgs e)
+ {
+ await Kernel.Instance.Reload(new Progress<TaskProgress>()).ConfigureAwait(false);
+ }
+
+ private void LoadingIconTimerCallback(object stateInfo)
+ {
+ const int numImages = 4;
+
+ if (LoadingImageIndex < numImages)
+ {
+ LoadingImageIndex++;
+ }
+ else
+ {
+ LoadingImageIndex = 1;
+ }
+ }
+
+ #endregion
+
+ }
+}
diff --git a/MediaBrowser.ServerApplication/MediaBrowser.ServerApplication.csproj b/MediaBrowser.ServerApplication/MediaBrowser.ServerApplication.csproj
new file mode 100644
index 000000000..de20c9256
--- /dev/null
+++ b/MediaBrowser.ServerApplication/MediaBrowser.ServerApplication.csproj
@@ -0,0 +1,143 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
+ <PropertyGroup>
+ <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+ <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+ <ProjectGuid>{156EA256-AD2D-4D2F-B116-2ED4B9EFD869}</ProjectGuid>
+ <OutputType>WinExe</OutputType>
+ <AppDesignerFolder>Properties</AppDesignerFolder>
+ <RootNamespace>MediaBrowser.ServerApplication</RootNamespace>
+ <AssemblyName>MediaBrowser.ServerApplication</AssemblyName>
+ <TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
+ <FileAlignment>512</FileAlignment>
+ <ProjectTypeGuids>{60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
+ <WarningLevel>4</WarningLevel>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
+ <PlatformTarget>AnyCPU</PlatformTarget>
+ <DebugSymbols>true</DebugSymbols>
+ <DebugType>full</DebugType>
+ <Optimize>false</Optimize>
+ <OutputPath>bin\Debug\</OutputPath>
+ <DefineConstants>DEBUG;TRACE</DefineConstants>
+ <ErrorReport>prompt</ErrorReport>
+ <WarningLevel>4</WarningLevel>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
+ <PlatformTarget>AnyCPU</PlatformTarget>
+ <DebugType>pdbonly</DebugType>
+ <Optimize>true</Optimize>
+ <OutputPath>bin\Release\</OutputPath>
+ <DefineConstants>TRACE</DefineConstants>
+ <ErrorReport>prompt</ErrorReport>
+ <WarningLevel>4</WarningLevel>
+ </PropertyGroup>
+ <PropertyGroup>
+ <StartupObject>MediaBrowser.ServerApplication.App</StartupObject>
+ </PropertyGroup>
+ <PropertyGroup>
+ <ApplicationIcon>Resources\Images\icon.ico</ApplicationIcon>
+ </PropertyGroup>
+ <ItemGroup>
+ <Reference Include="Hardcodet.Wpf.TaskbarNotification">
+ <HintPath>..\packages\Hardcodet.Wpf.TaskbarNotification.1.0.4.0\lib\net40\Hardcodet.Wpf.TaskbarNotification.dll</HintPath>
+ </Reference>
+ <Reference Include="System" />
+ <Reference Include="System.Data" />
+ <Reference Include="System.Drawing" />
+ <Reference Include="System.Runtime.Remoting" />
+ <Reference Include="System.Xml" />
+ <Reference Include="Microsoft.CSharp" />
+ <Reference Include="System.Core" />
+ <Reference Include="System.Xml.Linq" />
+ <Reference Include="System.Data.DataSetExtensions" />
+ <Reference Include="System.Xaml">
+ <RequiredTargetFramework>4.0</RequiredTargetFramework>
+ </Reference>
+ <Reference Include="WindowsBase" />
+ <Reference Include="PresentationCore" />
+ <Reference Include="PresentationFramework" />
+ </ItemGroup>
+ <ItemGroup>
+ <Page Include="App.xaml">
+ <Generator>MSBuild:Compile</Generator>
+ <SubType>Designer</SubType>
+ </Page>
+ <Page Include="MainWindow.xaml">
+ <Generator>MSBuild:Compile</Generator>
+ <SubType>Designer</SubType>
+ </Page>
+ <Compile Include="App.xaml.cs">
+ <DependentUpon>App.xaml</DependentUpon>
+ <SubType>Code</SubType>
+ </Compile>
+ <Compile Include="MainWindow.xaml.cs">
+ <DependentUpon>MainWindow.xaml</DependentUpon>
+ <SubType>Code</SubType>
+ </Compile>
+ </ItemGroup>
+ <ItemGroup>
+ <Compile Include="Properties\AssemblyInfo.cs">
+ <SubType>Code</SubType>
+ </Compile>
+ <Compile Include="Properties\Resources.Designer.cs">
+ <AutoGen>True</AutoGen>
+ <DesignTime>True</DesignTime>
+ <DependentUpon>Resources.resx</DependentUpon>
+ </Compile>
+ <Compile Include="Properties\Settings.Designer.cs">
+ <AutoGen>True</AutoGen>
+ <DependentUpon>Settings.settings</DependentUpon>
+ <DesignTimeSharedInput>True</DesignTimeSharedInput>
+ </Compile>
+ <EmbeddedResource Include="Properties\Resources.resx">
+ <Generator>ResXFileCodeGenerator</Generator>
+ <LastGenOutput>Resources.Designer.cs</LastGenOutput>
+ </EmbeddedResource>
+ <None Include="packages.config" />
+ <None Include="Properties\Settings.settings">
+ <Generator>SettingsSingleFileGenerator</Generator>
+ <LastGenOutput>Settings.Designer.cs</LastGenOutput>
+ </None>
+ <AppDesigner Include="Properties\" />
+ </ItemGroup>
+ <ItemGroup>
+ <None Include="App.config">
+ <SubType>Designer</SubType>
+ </None>
+ </ItemGroup>
+ <ItemGroup>
+ <ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj">
+ <Project>{9142eefa-7570-41e1-bfcc-468bb571af2f}</Project>
+ <Name>MediaBrowser.Common</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj">
+ <Project>{17e1f4e6-8abd-4fe5-9ecf-43d4b6087ba2}</Project>
+ <Name>MediaBrowser.Controller</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj">
+ <Project>{7eeeb4bb-f3e8-48fc-b4c5-70f0fff8329b}</Project>
+ <Name>MediaBrowser.Model</Name>
+ </ProjectReference>
+ </ItemGroup>
+ <ItemGroup>
+ <Resource Include="Resources\Images\icon.ico" />
+ </ItemGroup>
+ <ItemGroup>
+ <Resource Include="Resources\Images\loadingIcon1.ico" />
+ <Resource Include="Resources\Images\loadingIcon2.ico" />
+ </ItemGroup>
+ <ItemGroup>
+ <Resource Include="Resources\Images\loadingIcon3.ico" />
+ <Resource Include="Resources\Images\loadingIcon4.ico" />
+ </ItemGroup>
+ <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
+ <!-- To modify your build process, add your task inside one of the targets below and uncomment it.
+ Other similar extension points exist, see Microsoft.Common.targets.
+ <Target Name="BeforeBuild">
+ </Target>
+ <Target Name="AfterBuild">
+ </Target>
+ -->
+</Project> \ No newline at end of file
diff --git a/MediaBrowser.ServerApplication/Properties/AssemblyInfo.cs b/MediaBrowser.ServerApplication/Properties/AssemblyInfo.cs
new file mode 100644
index 000000000..35b3c6f98
--- /dev/null
+++ b/MediaBrowser.ServerApplication/Properties/AssemblyInfo.cs
@@ -0,0 +1,53 @@
+using System.Reflection;
+using System.Runtime.InteropServices;
+using System.Windows;
+
+// 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.ServerApplication")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("MediaBrowser.ServerApplication")]
+[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)]
+
+//In order to begin building localizable applications, set
+//<UICulture>CultureYouAreCodingWith</UICulture> in your .csproj file
+//inside a <PropertyGroup>. For example, if you are using US english
+//in your source files, set the <UICulture> to en-US. Then uncomment
+//the NeutralResourceLanguage attribute below. Update the "en-US" in
+//the line below to match the UICulture setting in the project file.
+
+//[assembly: NeutralResourcesLanguage("en-US", UltimateResourceFallbackLocation.Satellite)]
+
+
+[assembly: ThemeInfo(
+ ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
+ //(used if a resource is not found in the page,
+ // or application resource dictionaries)
+ ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
+ //(used if a resource is not found in the page,
+ // app, or any theme specific resource dictionaries)
+)]
+
+
+// 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.ServerApplication/Properties/Resources.Designer.cs b/MediaBrowser.ServerApplication/Properties/Resources.Designer.cs
new file mode 100644
index 000000000..18c997522
--- /dev/null
+++ b/MediaBrowser.ServerApplication/Properties/Resources.Designer.cs
@@ -0,0 +1,71 @@
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.17626
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace MediaBrowser.ServerApplication.Properties
+{
+
+
+ /// <summary>
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ /// </summary>
+ // This class was auto-generated by the StronglyTypedResourceBuilder
+ // class via a tool like ResGen or Visual Studio.
+ // To add or remove a member, edit your .ResX file then rerun ResGen
+ // with the /str option, or rebuild your VS project.
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ internal class Resources
+ {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal Resources()
+ {
+ }
+
+ /// <summary>
+ /// Returns the cached ResourceManager instance used by this class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Resources.ResourceManager ResourceManager
+ {
+ get
+ {
+ if ((resourceMan == null))
+ {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("MediaBrowser.ServerApplication.Properties.Resources", typeof(Resources).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ /// <summary>
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Globalization.CultureInfo Culture
+ {
+ get
+ {
+ return resourceCulture;
+ }
+ set
+ {
+ resourceCulture = value;
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.ServerApplication/Properties/Resources.resx b/MediaBrowser.ServerApplication/Properties/Resources.resx
new file mode 100644
index 000000000..ffecec851
--- /dev/null
+++ b/MediaBrowser.ServerApplication/Properties/Resources.resx
@@ -0,0 +1,117 @@
+<?xml version="1.0" encoding="utf-8"?>
+<root>
+ <!--
+ Microsoft ResX Schema
+
+ Version 2.0
+
+ The primary goals of this format is to allow a simple XML format
+ that is mostly human readable. The generation and parsing of the
+ various data types are done through the TypeConverter classes
+ associated with the data types.
+
+ Example:
+
+ ... ado.net/XML headers & schema ...
+ <resheader name="resmimetype">text/microsoft-resx</resheader>
+ <resheader name="version">2.0</resheader>
+ <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+ <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+ <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+ <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+ <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+ <value>[base64 mime encoded serialized .NET Framework object]</value>
+ </data>
+ <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+ <comment>This is a comment</comment>
+ </data>
+
+ There are any number of "resheader" rows that contain simple
+ name/value pairs.
+
+ Each data row contains a name, and value. The row also contains a
+ type or mimetype. Type corresponds to a .NET class that support
+ text/value conversion through the TypeConverter architecture.
+ Classes that don't support this are serialized and stored with the
+ mimetype set.
+
+ The mimetype is used for serialized objects, and tells the
+ ResXResourceReader how to depersist the object. This is currently not
+ extensible. For a given mimetype the value must be set accordingly:
+
+ Note - application/x-microsoft.net.object.binary.base64 is the format
+ that the ResXResourceWriter will generate, however the reader can
+ read any of the formats listed below.
+
+ mimetype: application/x-microsoft.net.object.binary.base64
+ value : The object must be serialized with
+ : System.Serialization.Formatters.Binary.BinaryFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.soap.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.bytearray.base64
+ value : The object must be serialized into a byte array
+ : using a System.ComponentModel.TypeConverter
+ : and then encoded with base64 encoding.
+ -->
+ <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+ <xsd:element name="root" msdata:IsDataSet="true">
+ <xsd:complexType>
+ <xsd:choice maxOccurs="unbounded">
+ <xsd:element name="metadata">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" />
+ <xsd:attribute name="type" type="xsd:string" />
+ <xsd:attribute name="mimetype" type="xsd:string" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="assembly">
+ <xsd:complexType>
+ <xsd:attribute name="alias" type="xsd:string" />
+ <xsd:attribute name="name" type="xsd:string" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="data">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" msdata:Ordinal="1" />
+ <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+ <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="resheader">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" />
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:choice>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:schema>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>2.0</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+</root> \ No newline at end of file
diff --git a/MediaBrowser.ServerApplication/Properties/Settings.Designer.cs b/MediaBrowser.ServerApplication/Properties/Settings.Designer.cs
new file mode 100644
index 000000000..f7b055389
--- /dev/null
+++ b/MediaBrowser.ServerApplication/Properties/Settings.Designer.cs
@@ -0,0 +1,30 @@
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.17626
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace MediaBrowser.ServerApplication.Properties
+{
+
+
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "11.0.0.0")]
+ internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase
+ {
+
+ private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));
+
+ public static Settings Default
+ {
+ get
+ {
+ return defaultInstance;
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.ServerApplication/Properties/Settings.settings b/MediaBrowser.ServerApplication/Properties/Settings.settings
new file mode 100644
index 000000000..8f2fd95d6
--- /dev/null
+++ b/MediaBrowser.ServerApplication/Properties/Settings.settings
@@ -0,0 +1,7 @@
+<?xml version='1.0' encoding='utf-8'?>
+<SettingsFile xmlns="uri:settings" CurrentProfile="(Default)">
+ <Profiles>
+ <Profile Name="(Default)" />
+ </Profiles>
+ <Settings />
+</SettingsFile> \ No newline at end of file
diff --git a/MediaBrowser.ServerApplication/Resources/Images/Icon.ico b/MediaBrowser.ServerApplication/Resources/Images/Icon.ico
new file mode 100644
index 000000000..1541dabdc
--- /dev/null
+++ b/MediaBrowser.ServerApplication/Resources/Images/Icon.ico
Binary files differ
diff --git a/MediaBrowser.ServerApplication/Resources/Images/loadingIcon1.ico b/MediaBrowser.ServerApplication/Resources/Images/loadingIcon1.ico
new file mode 100644
index 000000000..f7c2f2783
--- /dev/null
+++ b/MediaBrowser.ServerApplication/Resources/Images/loadingIcon1.ico
Binary files differ
diff --git a/MediaBrowser.ServerApplication/Resources/Images/loadingIcon2.ico b/MediaBrowser.ServerApplication/Resources/Images/loadingIcon2.ico
new file mode 100644
index 000000000..0f20c2b7a
--- /dev/null
+++ b/MediaBrowser.ServerApplication/Resources/Images/loadingIcon2.ico
Binary files differ
diff --git a/MediaBrowser.ServerApplication/Resources/Images/loadingIcon3.ico b/MediaBrowser.ServerApplication/Resources/Images/loadingIcon3.ico
new file mode 100644
index 000000000..bc915ea9f
--- /dev/null
+++ b/MediaBrowser.ServerApplication/Resources/Images/loadingIcon3.ico
Binary files differ
diff --git a/MediaBrowser.ServerApplication/Resources/Images/loadingIcon4.ico b/MediaBrowser.ServerApplication/Resources/Images/loadingIcon4.ico
new file mode 100644
index 000000000..183049b06
--- /dev/null
+++ b/MediaBrowser.ServerApplication/Resources/Images/loadingIcon4.ico
Binary files differ
diff --git a/MediaBrowser.ServerApplication/packages.config b/MediaBrowser.ServerApplication/packages.config
new file mode 100644
index 000000000..bd2832dce
--- /dev/null
+++ b/MediaBrowser.ServerApplication/packages.config
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<packages>
+ <package id="Hardcodet.Wpf.TaskbarNotification" version="1.0.4.0" targetFramework="net45" />
+</packages> \ No newline at end of file
diff --git a/MediaBrowser.UI.sln b/MediaBrowser.UI.sln
new file mode 100644
index 000000000..65130e3d9
--- /dev/null
+++ b/MediaBrowser.UI.sln
@@ -0,0 +1,58 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio 2012
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MediaBrowser.UI", "MediaBrowser.UI\MediaBrowser.UI.csproj", "{B5ECE1FB-618E-420B-9A99-8E972D76920A}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MediaBrowser.Common", "MediaBrowser.Common\MediaBrowser.Common.csproj", "{9142EEFA-7570-41E1-BFCC-468BB571AF2F}"
+EndProject
+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("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MediaBrowser.Plugins.DefaultTheme", "MediaBrowser.Plugins.DefaultTheme\MediaBrowser.Plugins.DefaultTheme.csproj", "{6E892999-711D-4E24-8BAC-DACF5BFA783A}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MediaBrowser.Controller", "MediaBrowser.Controller\MediaBrowser.Controller.csproj", "{17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Debug|x86 = Debug|x86
+ Release|Any CPU = Release|Any CPU
+ Release|x86 = Release|x86
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {B5ECE1FB-618E-420B-9A99-8E972D76920A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {B5ECE1FB-618E-420B-9A99-8E972D76920A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {B5ECE1FB-618E-420B-9A99-8E972D76920A}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {B5ECE1FB-618E-420B-9A99-8E972D76920A}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {B5ECE1FB-618E-420B-9A99-8E972D76920A}.Release|Any CPU.Build.0 = Release|Any CPU
+ {B5ECE1FB-618E-420B-9A99-8E972D76920A}.Release|x86.ActiveCfg = Release|Any CPU
+ {9142EEFA-7570-41E1-BFCC-468BB571AF2F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {9142EEFA-7570-41E1-BFCC-468BB571AF2F}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {9142EEFA-7570-41E1-BFCC-468BB571AF2F}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {9142EEFA-7570-41E1-BFCC-468BB571AF2F}.Release|x86.ActiveCfg = Release|Any CPU
+ {7EEEB4BB-F3E8-48FC-B4C5-70F0FFF8329B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {7EEEB4BB-F3E8-48FC-B4C5-70F0FFF8329B}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {7EEEB4BB-F3E8-48FC-B4C5-70F0FFF8329B}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {7EEEB4BB-F3E8-48FC-B4C5-70F0FFF8329B}.Release|x86.ActiveCfg = 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}.Debug|x86.ActiveCfg = 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
+ {921C0F64-FDA7-4E9F-9E73-0CB0EEDB2422}.Release|x86.ActiveCfg = Release|Any CPU
+ {6E892999-711D-4E24-8BAC-DACF5BFA783A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {6E892999-711D-4E24-8BAC-DACF5BFA783A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {6E892999-711D-4E24-8BAC-DACF5BFA783A}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {6E892999-711D-4E24-8BAC-DACF5BFA783A}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {6E892999-711D-4E24-8BAC-DACF5BFA783A}.Release|Any CPU.Build.0 = Release|Any CPU
+ {6E892999-711D-4E24-8BAC-DACF5BFA783A}.Release|x86.ActiveCfg = Release|Any CPU
+ {17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}.Release|x86.ActiveCfg = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+EndGlobal
diff --git a/MediaBrowser.UI/App.config b/MediaBrowser.UI/App.config
new file mode 100644
index 000000000..018d3790f
--- /dev/null
+++ b/MediaBrowser.UI/App.config
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<configuration>
+ <appSettings>
+ <add key="ProgramDataPath" value="..\..\..\ProgramData-UI" />
+ </appSettings>
+ <startup>
+ <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
+ </startup>
+</configuration> \ No newline at end of file
diff --git a/MediaBrowser.UI/App.xaml b/MediaBrowser.UI/App.xaml
new file mode 100644
index 000000000..75318985c
--- /dev/null
+++ b/MediaBrowser.UI/App.xaml
@@ -0,0 +1,14 @@
+<z:BaseApplication x:Class="MediaBrowser.UI.App"
+ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ xmlns:z="clr-namespace:MediaBrowser.Common.UI;assembly=MediaBrowser.Common">
+ <Application.Resources>
+ <ResourceDictionary>
+ <ResourceDictionary.MergedDictionaries>
+ <ResourceDictionary Source="Resources/AppResources.xaml" />
+ <ResourceDictionary Source="Resources/MainWindowResources.xaml" />
+ <ResourceDictionary Source="Resources/NavBarResources.xaml"/>
+ </ResourceDictionary.MergedDictionaries>
+ </ResourceDictionary>
+ </Application.Resources>
+</z:BaseApplication> \ No newline at end of file
diff --git a/MediaBrowser.UI/App.xaml.cs b/MediaBrowser.UI/App.xaml.cs
new file mode 100644
index 000000000..6f2afa91c
--- /dev/null
+++ b/MediaBrowser.UI/App.xaml.cs
@@ -0,0 +1,213 @@
+using MediaBrowser.Common.Kernel;
+using MediaBrowser.Common.Plugins;
+using MediaBrowser.Common.UI;
+using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.DTO;
+using MediaBrowser.Model.Weather;
+using MediaBrowser.UI.Controller;
+using System;
+using System.ComponentModel;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Media.Imaging;
+
+namespace MediaBrowser.UI
+{
+ /// <summary>
+ /// Interaction logic for App.xaml
+ /// </summary>
+ public partial class App : BaseApplication, IApplication
+ {
+ private Timer ClockTimer { get; set; }
+ private Timer ServerConfigurationTimer { get; set; }
+
+ public static App Instance
+ {
+ get
+ {
+ return Application.Current as App;
+ }
+ }
+
+ public DtoUser CurrentUser
+ {
+ get
+ {
+ return UIKernel.Instance.CurrentUser;
+ }
+ set
+ {
+ UIKernel.Instance.CurrentUser = value;
+ OnPropertyChanged("CurrentUser");
+ }
+ }
+
+ public ServerConfiguration ServerConfiguration
+ {
+ get
+ {
+ return UIKernel.Instance.ServerConfiguration;
+ }
+ set
+ {
+ UIKernel.Instance.ServerConfiguration = value;
+ OnPropertyChanged("ServerConfiguration");
+ }
+ }
+
+ private DateTime _currentTime = DateTime.Now;
+ public DateTime CurrentTime
+ {
+ get
+ {
+ return _currentTime;
+ }
+ private set
+ {
+ _currentTime = value;
+ OnPropertyChanged("CurrentTime");
+ }
+ }
+
+ private WeatherInfo _currentWeather;
+ public WeatherInfo CurrentWeather
+ {
+ get
+ {
+ return _currentWeather;
+ }
+ private set
+ {
+ _currentWeather = value;
+ OnPropertyChanged("CurrentWeather");
+ }
+ }
+
+ private BaseTheme _currentTheme;
+ public BaseTheme CurrentTheme
+ {
+ get
+ {
+ return _currentTheme;
+ }
+ private set
+ {
+ _currentTheme = value;
+ OnPropertyChanged("CurrentTheme");
+ }
+ }
+
+ [STAThread]
+ public static void Main()
+ {
+ RunApplication<App>("MediaBrowserUI");
+ }
+
+ #region BaseApplication Overrides
+ protected override IKernel InstantiateKernel()
+ {
+ return new UIKernel();
+ }
+
+ protected override Window InstantiateMainWindow()
+ {
+ return new MainWindow();
+ }
+
+ protected override void OnKernelLoaded()
+ {
+ base.OnKernelLoaded();
+
+ PropertyChanged += AppPropertyChanged;
+
+ // Update every 10 seconds
+ ClockTimer = new Timer(ClockTimerCallback, null, 0, 10000);
+
+ // Update every 30 minutes
+ ServerConfigurationTimer = new Timer(ServerConfigurationTimerCallback, null, 0, 1800000);
+
+ CurrentTheme = UIKernel.Instance.Plugins.OfType<BaseTheme>().First();
+
+ foreach (var resource in CurrentTheme.GlobalResources)
+ {
+ Resources.MergedDictionaries.Add(resource);
+ }
+ }
+ #endregion
+
+ async void AppPropertyChanged(object sender, PropertyChangedEventArgs e)
+ {
+ if (e.PropertyName.Equals("ServerConfiguration"))
+ {
+ if (string.IsNullOrEmpty(ServerConfiguration.WeatherZipCode))
+ {
+ CurrentWeather = null;
+ }
+ else
+ {
+ CurrentWeather = await UIKernel.Instance.ApiClient.GetWeatherInfoAsync(ServerConfiguration.WeatherZipCode);
+ }
+ }
+ }
+
+ private void ClockTimerCallback(object stateInfo)
+ {
+ CurrentTime = DateTime.Now;
+ }
+
+ private async void ServerConfigurationTimerCallback(object stateInfo)
+ {
+ ServerConfiguration = await UIKernel.Instance.ApiClient.GetServerConfigurationAsync();
+ }
+
+ public async Task<Image> GetImage(string url)
+ {
+ var image = new Image();
+
+ image.Source = await GetBitmapImage(url);
+
+ return image;
+ }
+
+ public async Task<BitmapImage> GetBitmapImage(string url)
+ {
+ Stream stream = await UIKernel.Instance.ApiClient.GetImageStreamAsync(url);
+
+ BitmapImage bitmap = new BitmapImage();
+
+ bitmap.CacheOption = BitmapCacheOption.Default;
+
+ bitmap.BeginInit();
+ bitmap.StreamSource = stream;
+ bitmap.EndInit();
+
+ return bitmap;
+ }
+
+ public async Task LogoutUser()
+ {
+ CurrentUser = null;
+
+ if (ServerConfiguration.EnableUserProfiles)
+ {
+ Navigate(CurrentTheme.LoginPageUri);
+ }
+ else
+ {
+ DtoUser defaultUser = await UIKernel.Instance.ApiClient.GetDefaultUserAsync();
+ CurrentUser = defaultUser;
+
+ Navigate(new Uri("/Pages/HomePage.xaml", UriKind.Relative));
+ }
+ }
+
+ public void Navigate(Uri uri)
+ {
+ (MainWindow as MainWindow).Navigate(uri);
+ }
+ }
+}
diff --git a/MediaBrowser.UI/Configuration/UIApplicationConfiguration.cs b/MediaBrowser.UI/Configuration/UIApplicationConfiguration.cs
new file mode 100644
index 000000000..59c625178
--- /dev/null
+++ b/MediaBrowser.UI/Configuration/UIApplicationConfiguration.cs
@@ -0,0 +1,27 @@
+using MediaBrowser.Model.Configuration;
+
+namespace MediaBrowser.UI.Configuration
+{
+ /// <summary>
+ /// This is the UI's device configuration that applies regardless of which user is logged in.
+ /// </summary>
+ public class UIApplicationConfiguration : BaseApplicationConfiguration
+ {
+ /// <summary>
+ /// Gets or sets the server host name (myserver or 192.168.x.x)
+ /// </summary>
+ public string ServerHostName { get; set; }
+
+ /// <summary>
+ /// Gets or sets the port number used by the API
+ /// </summary>
+ public int ServerApiPort { get; set; }
+
+ public UIApplicationConfiguration()
+ : base()
+ {
+ ServerHostName = "localhost";
+ ServerApiPort = 8096;
+ }
+ }
+}
diff --git a/MediaBrowser.UI/Configuration/UIApplicationPaths.cs b/MediaBrowser.UI/Configuration/UIApplicationPaths.cs
new file mode 100644
index 000000000..07cb54fc1
--- /dev/null
+++ b/MediaBrowser.UI/Configuration/UIApplicationPaths.cs
@@ -0,0 +1,8 @@
+using MediaBrowser.Common.Kernel;
+
+namespace MediaBrowser.UI.Configuration
+{
+ public class UIApplicationPaths : BaseApplicationPaths
+ {
+ }
+}
diff --git a/MediaBrowser.UI/Controller/PluginUpdater.cs b/MediaBrowser.UI/Controller/PluginUpdater.cs
new file mode 100644
index 000000000..d9fa48749
--- /dev/null
+++ b/MediaBrowser.UI/Controller/PluginUpdater.cs
@@ -0,0 +1,231 @@
+using MediaBrowser.Common.Logging;
+using MediaBrowser.Common.Plugins;
+using MediaBrowser.Common.Serialization;
+using MediaBrowser.Model.DTO;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.Composition;
+using System.ComponentModel.Composition.Hosting;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.UI.Controller
+{
+ /// <summary>
+ /// This keeps ui plugin assemblies in sync with plugins installed on the server
+ /// </summary>
+ public class PluginUpdater
+ {
+ /// <summary>
+ /// Gets the list of currently installed UI plugins
+ /// </summary>
+ [ImportMany(typeof(BasePlugin))]
+ private IEnumerable<BasePlugin> CurrentPlugins { get; set; }
+
+ private CompositionContainer CompositionContainer { get; set; }
+
+ public async Task<PluginUpdateResult> UpdatePlugins()
+ {
+ // First load the plugins that are currently installed
+ ReloadComposableParts();
+
+ Logger.LogInfo("Downloading list of installed plugins");
+ PluginInfo[] allInstalledPlugins = await UIKernel.Instance.ApiClient.GetInstalledPluginsAsync().ConfigureAwait(false);
+
+ IEnumerable<PluginInfo> uiPlugins = allInstalledPlugins.Where(p => p.DownloadToUI);
+
+ PluginUpdateResult result = new PluginUpdateResult();
+
+ result.DeletedPlugins = DeleteUninstalledPlugins(uiPlugins);
+
+ await DownloadPluginAssemblies(uiPlugins, result).ConfigureAwait(false);
+
+ // If any new assemblies were downloaded we'll have to reload the CurrentPlugins list
+ if (result.NewlyInstalledPlugins.Any())
+ {
+ ReloadComposableParts();
+ }
+
+ result.UpdatedConfigurations = await DownloadPluginConfigurations(uiPlugins).ConfigureAwait(false);
+
+ CompositionContainer.Dispose();
+
+ return result;
+ }
+
+ /// <summary>
+ /// Downloads plugin assemblies from the server, if they need to be installed or updated.
+ /// </summary>
+ private async Task DownloadPluginAssemblies(IEnumerable<PluginInfo> uiPlugins, PluginUpdateResult result)
+ {
+ List<PluginInfo> newlyInstalledPlugins = new List<PluginInfo>();
+ List<PluginInfo> updatedPlugins = new List<PluginInfo>();
+
+ // Loop through the list of plugins that are on the server
+ foreach (PluginInfo pluginInfo in uiPlugins)
+ {
+ // See if it is already installed in the UI
+ BasePlugin installedPlugin = CurrentPlugins.FirstOrDefault(p => p.AssemblyFileName.Equals(pluginInfo.AssemblyFileName, StringComparison.OrdinalIgnoreCase));
+
+ // Download the plugin if it is not present, or if the current version is out of date
+ bool downloadPlugin = installedPlugin == null;
+
+ if (installedPlugin != null)
+ {
+ Version serverVersion = Version.Parse(pluginInfo.Version);
+
+ downloadPlugin = serverVersion > installedPlugin.Version;
+ }
+
+ if (downloadPlugin)
+ {
+ await DownloadPlugin(pluginInfo).ConfigureAwait(false);
+
+ if (installedPlugin == null)
+ {
+ newlyInstalledPlugins.Add(pluginInfo);
+ }
+ else
+ {
+ updatedPlugins.Add(pluginInfo);
+ }
+ }
+ }
+
+ result.NewlyInstalledPlugins = newlyInstalledPlugins;
+ result.UpdatedPlugins = updatedPlugins;
+ }
+
+ /// <summary>
+ /// Downloads plugin configurations from the server.
+ /// </summary>
+ private async Task<List<PluginInfo>> DownloadPluginConfigurations(IEnumerable<PluginInfo> uiPlugins)
+ {
+ List<PluginInfo> updatedPlugins = new List<PluginInfo>();
+
+ // Loop through the list of plugins that are on the server
+ foreach (PluginInfo pluginInfo in uiPlugins)
+ {
+ // See if it is already installed in the UI
+ BasePlugin installedPlugin = CurrentPlugins.First(p => p.AssemblyFileName.Equals(pluginInfo.AssemblyFileName, StringComparison.OrdinalIgnoreCase));
+
+ if (installedPlugin.ConfigurationDateLastModified < pluginInfo.ConfigurationDateLastModified)
+ {
+ await DownloadPluginConfiguration(installedPlugin, pluginInfo).ConfigureAwait(false);
+
+ updatedPlugins.Add(pluginInfo);
+ }
+ }
+
+ return updatedPlugins;
+ }
+
+ /// <summary>
+ /// Downloads a plugin assembly from the server
+ /// </summary>
+ private async Task DownloadPlugin(PluginInfo plugin)
+ {
+ Logger.LogInfo("Downloading {0} Plugin", plugin.Name);
+
+ string path = Path.Combine(UIKernel.Instance.ApplicationPaths.PluginsPath, plugin.AssemblyFileName);
+
+ // First download to a MemoryStream. This way if the download is cut off, we won't be left with a partial file
+ using (MemoryStream memoryStream = new MemoryStream())
+ {
+ Stream assemblyStream = await UIKernel.Instance.ApiClient.GetPluginAssemblyAsync(plugin).ConfigureAwait(false);
+
+ await assemblyStream.CopyToAsync(memoryStream).ConfigureAwait(false);
+
+ memoryStream.Position = 0;
+
+ using (FileStream fileStream = new FileStream(path, FileMode.Create))
+ {
+ await memoryStream.CopyToAsync(fileStream).ConfigureAwait(false);
+ }
+ }
+ }
+
+ /// <summary>
+ /// Downloads the latest configuration for a plugin
+ /// </summary>
+ private async Task DownloadPluginConfiguration(BasePlugin plugin, PluginInfo pluginInfo)
+ {
+ Logger.LogInfo("Downloading {0} Configuration", plugin.Name);
+
+ object config = await UIKernel.Instance.ApiClient.GetPluginConfigurationAsync(pluginInfo, plugin.ConfigurationType).ConfigureAwait(false);
+
+ XmlSerializer.SerializeToFile(config, plugin.ConfigurationFilePath);
+
+ File.SetLastWriteTimeUtc(plugin.ConfigurationFilePath, pluginInfo.ConfigurationDateLastModified);
+ }
+
+ /// <summary>
+ /// Deletes any plugins that have been uninstalled from the server
+ /// </summary>
+ private IEnumerable<string> DeleteUninstalledPlugins(IEnumerable<PluginInfo> uiPlugins)
+ {
+ var deletedPlugins = new List<string>();
+
+ foreach (BasePlugin plugin in CurrentPlugins)
+ {
+ PluginInfo latest = uiPlugins.FirstOrDefault(p => p.AssemblyFileName.Equals(plugin.AssemblyFileName, StringComparison.OrdinalIgnoreCase));
+
+ if (latest == null)
+ {
+ DeletePlugin(plugin);
+
+ deletedPlugins.Add(plugin.Name);
+ }
+ }
+
+ return deletedPlugins;
+ }
+
+ /// <summary>
+ /// Deletes an installed ui plugin.
+ /// Leaves config and data behind in the event it is later re-installed
+ /// </summary>
+ private void DeletePlugin(BasePlugin plugin)
+ {
+ Logger.LogInfo("Deleting {0} Plugin", plugin.Name);
+
+ string path = plugin.AssemblyFilePath;
+
+ if (File.Exists(path))
+ {
+ File.Delete(path);
+ }
+ }
+
+ /// <summary>
+ /// Re-uses MEF within the kernel to discover installed plugins
+ /// </summary>
+ private void ReloadComposableParts()
+ {
+ if (CompositionContainer != null)
+ {
+ CompositionContainer.Dispose();
+ }
+
+ CompositionContainer = UIKernel.Instance.GetCompositionContainer();
+
+ CompositionContainer.ComposeParts(this);
+
+ CompositionContainer.Catalog.Dispose();
+
+ foreach (BasePlugin plugin in CurrentPlugins)
+ {
+ plugin.Initialize(UIKernel.Instance, false);
+ }
+ }
+ }
+
+ public class PluginUpdateResult
+ {
+ public IEnumerable<string> DeletedPlugins { get; set; }
+ public IEnumerable<PluginInfo> NewlyInstalledPlugins { get; set; }
+ public IEnumerable<PluginInfo> UpdatedPlugins { get; set; }
+ public IEnumerable<PluginInfo> UpdatedConfigurations { get; set; }
+ }
+}
diff --git a/MediaBrowser.UI/Controller/UIKernel.cs b/MediaBrowser.UI/Controller/UIKernel.cs
new file mode 100644
index 000000000..ca24b7852
--- /dev/null
+++ b/MediaBrowser.UI/Controller/UIKernel.cs
@@ -0,0 +1,97 @@
+using MediaBrowser.ApiInteraction;
+using MediaBrowser.Common.Kernel;
+using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.DTO;
+using MediaBrowser.Model.Progress;
+using MediaBrowser.UI.Configuration;
+using System;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.UI.Controller
+{
+ /// <summary>
+ /// This controls application logic as well as server interaction within the UI.
+ /// </summary>
+ public class UIKernel : BaseKernel<UIApplicationConfiguration, UIApplicationPaths>
+ {
+ public static UIKernel Instance { get; private set; }
+
+ public ApiClient ApiClient { get; private set; }
+ public DtoUser CurrentUser { get; set; }
+ public ServerConfiguration ServerConfiguration { get; set; }
+
+ public UIKernel()
+ : base()
+ {
+ Instance = this;
+ }
+
+ public override KernelContext KernelContext
+ {
+ get { return KernelContext.Ui; }
+ }
+
+ /// <summary>
+ /// Give the UI a different url prefix so that they can share the same port, in case they are installed on the same machine.
+ /// </summary>
+ protected override string HttpServerUrlPrefix
+ {
+ get
+ {
+ return "http://+:" + Configuration.HttpServerPortNumber + "/mediabrowser/ui/";
+ }
+ }
+
+ /// <summary>
+ /// Performs initializations that can be reloaded at anytime
+ /// </summary>
+ protected override async Task ReloadInternal(IProgress<TaskProgress> progress)
+ {
+ ReloadApiClient();
+
+ await new PluginUpdater().UpdatePlugins().ConfigureAwait(false);
+
+ await base.ReloadInternal(progress).ConfigureAwait(false);
+ }
+
+ /// <summary>
+ /// Updates and installs new plugin assemblies and configurations from the server
+ /// </summary>
+ protected async Task<PluginUpdateResult> UpdatePlugins()
+ {
+ return await new PluginUpdater().UpdatePlugins().ConfigureAwait(false);
+ }
+
+ /// <summary>
+ /// Disposes the current ApiClient and creates a new one
+ /// </summary>
+ private void ReloadApiClient()
+ {
+ DisposeApiClient();
+
+ ApiClient = new ApiClient
+ {
+ ServerHostName = Configuration.ServerHostName,
+ ServerApiPort = Configuration.ServerApiPort
+ };
+ }
+
+ /// <summary>
+ /// Disposes the current ApiClient
+ /// </summary>
+ private void DisposeApiClient()
+ {
+ if (ApiClient != null)
+ {
+ ApiClient.Dispose();
+ }
+ }
+
+ public override void Dispose()
+ {
+ base.Dispose();
+
+ DisposeApiClient();
+ }
+ }
+}
diff --git a/MediaBrowser.UI/Controls/EnhancedScrollViewer.cs b/MediaBrowser.UI/Controls/EnhancedScrollViewer.cs
new file mode 100644
index 000000000..188715e1e
--- /dev/null
+++ b/MediaBrowser.UI/Controls/EnhancedScrollViewer.cs
@@ -0,0 +1,73 @@
+using System;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Input;
+
+namespace MediaBrowser.UI.Controls
+{
+ /// <summary>
+ /// Provides a ScrollViewer that can be scrolled by dragging the mouse
+ /// </summary>
+ public class EnhancedScrollViewer : ScrollViewer
+ {
+ private Point _scrollTarget;
+ private Point _scrollStartPoint;
+ private Point _scrollStartOffset;
+ private const int PixelsToMoveToBeConsideredScroll = 5;
+
+ protected override void OnPreviewMouseDown(MouseButtonEventArgs e)
+ {
+ if (IsMouseOver)
+ {
+ // Save starting point, used later when determining how much to scroll.
+ _scrollStartPoint = e.GetPosition(this);
+ _scrollStartOffset.X = HorizontalOffset;
+ _scrollStartOffset.Y = VerticalOffset;
+
+ // Update the cursor if can scroll or not.
+ Cursor = (ExtentWidth > ViewportWidth) ||
+ (ExtentHeight > ViewportHeight) ?
+ Cursors.ScrollAll : Cursors.Arrow;
+
+ CaptureMouse();
+ }
+
+ base.OnPreviewMouseDown(e);
+ }
+
+ protected override void OnPreviewMouseMove(MouseEventArgs e)
+ {
+ if (IsMouseCaptured)
+ {
+ Point currentPoint = e.GetPosition(this);
+
+ // Determine the new amount to scroll.
+ var delta = new Point(_scrollStartPoint.X - currentPoint.X, _scrollStartPoint.Y - currentPoint.Y);
+
+ if (Math.Abs(delta.X) < PixelsToMoveToBeConsideredScroll &&
+ Math.Abs(delta.Y) < PixelsToMoveToBeConsideredScroll)
+ return;
+
+ _scrollTarget.X = _scrollStartOffset.X + delta.X;
+ _scrollTarget.Y = _scrollStartOffset.Y + delta.Y;
+
+ // Scroll to the new position.
+ ScrollToHorizontalOffset(_scrollTarget.X);
+ ScrollToVerticalOffset(_scrollTarget.Y);
+ }
+
+ base.OnPreviewMouseMove(e);
+ }
+
+ protected override void OnPreviewMouseUp(MouseButtonEventArgs e)
+ {
+ if (IsMouseCaptured)
+ {
+ Cursor = Cursors.Arrow;
+ ReleaseMouseCapture();
+ }
+
+ base.OnPreviewMouseUp(e);
+ }
+ }
+}
diff --git a/MediaBrowser.UI/Controls/ExtendedImage.cs b/MediaBrowser.UI/Controls/ExtendedImage.cs
new file mode 100644
index 000000000..9d6ee3a7a
--- /dev/null
+++ b/MediaBrowser.UI/Controls/ExtendedImage.cs
@@ -0,0 +1,92 @@
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Media;
+
+namespace MediaBrowser.UI.Controls
+{
+ /// <summary>
+ /// Follow steps 1a or 1b and then 2 to use this custom control in a XAML file.
+ ///
+ /// Step 1a) Using this custom control in a XAML file that exists in the current project.
+ /// Add this XmlNamespace attribute to the root element of the markup file where it is
+ /// to be used:
+ ///
+ /// xmlns:MyNamespace="clr-namespace:MediaBrowser.UI.Controls"
+ ///
+ ///
+ /// Step 1b) Using this custom control in a XAML file that exists in a different project.
+ /// Add this XmlNamespace attribute to the root element of the markup file where it is
+ /// to be used:
+ ///
+ /// xmlns:MyNamespace="clr-namespace:MediaBrowser.UI.Controls;assembly=MediaBrowser.UI.Controls"
+ ///
+ /// You will also need to add a project reference from the project where the XAML file lives
+ /// to this project and Rebuild to avoid compilation errors:
+ ///
+ /// Right click on the target project in the Solution Explorer and
+ /// "Add Reference"->"Projects"->[Browse to and select this project]
+ ///
+ ///
+ /// Step 2)
+ /// Go ahead and use your control in the XAML file.
+ ///
+ /// <MyNamespace:ExtendedImage/>
+ ///
+ /// </summary>
+ public class ExtendedImage : Control
+ {
+ public static readonly DependencyProperty HasImageProperty = DependencyProperty.Register(
+ "HasImage",
+ typeof (bool),
+ typeof (ExtendedImage),
+ new PropertyMetadata(default(bool)));
+
+ public bool HasImage
+ {
+ get { return (bool)GetValue(HasImageProperty); }
+ set { SetValue(HasImageProperty, value); }
+ }
+
+ public static readonly DependencyProperty SourceProperty = DependencyProperty.Register(
+ "Source",
+ typeof(ImageSource),
+ typeof(ExtendedImage),
+ new PropertyMetadata(default(ImageBrush)));
+
+ public ImageSource Source
+ {
+ get { return (ImageSource)GetValue(SourceProperty); }
+ set { SetValue(SourceProperty, value); }
+ }
+
+ public static readonly DependencyProperty StretchProperty = DependencyProperty.Register(
+ "Stretch",
+ typeof (Stretch),
+ typeof (ExtendedImage),
+ new PropertyMetadata(default(Stretch)));
+
+ public Stretch Stretch
+ {
+ get { return (Stretch) GetValue(StretchProperty); }
+ set { SetValue(StretchProperty, value); }
+ }
+
+ public static readonly DependencyProperty PlaceHolderSourceProperty = DependencyProperty.Register(
+ "PlaceHolderSource",
+ typeof(ImageSource),
+ typeof(ExtendedImage),
+ new PropertyMetadata(default(ImageBrush)));
+
+ public ImageSource PlaceHolderSource
+ {
+ get { return (ImageSource)GetValue(PlaceHolderSourceProperty); }
+ set { SetValue(PlaceHolderSourceProperty, value); }
+ }
+
+ static ExtendedImage()
+ {
+ DefaultStyleKeyProperty.OverrideMetadata(typeof(ExtendedImage),
+ new FrameworkPropertyMetadata(typeof(ExtendedImage)));
+ }
+ }
+}
diff --git a/MediaBrowser.UI/Controls/TreeHelper.cs b/MediaBrowser.UI/Controls/TreeHelper.cs
new file mode 100644
index 000000000..bbe489572
--- /dev/null
+++ b/MediaBrowser.UI/Controls/TreeHelper.cs
@@ -0,0 +1,226 @@
+using System.Collections.Generic;
+using System.Windows;
+using System.Windows.Media;
+
+namespace MediaBrowser.UI.Controls
+{
+ /// <summary>
+ /// Helper methods for UI-related tasks.
+ /// </summary>
+ public static class TreeHelper
+ {
+ /// <summary>
+ /// Finds a Child of a given item in the visual tree.
+ /// </summary>
+ /// <param name="parent">A direct parent of the queried item.</param>
+ /// <typeparam name="T">The type of the queried item.</typeparam>
+ /// <param name="childName">x:Name or Name of child. </param>
+ /// <returns>The first parent item that matches the submitted type parameter.
+ /// If not matching item can be found,
+ /// a null parent is being returned.</returns>
+ public static T FindChild<T>(DependencyObject parent, string childName)
+ where T : DependencyObject
+ {
+ // Confirm parent and childName are valid.
+ if (parent == null) return null;
+
+ T foundChild = null;
+
+ int childrenCount = VisualTreeHelper.GetChildrenCount(parent);
+ for (int i = 0; i < childrenCount; i++)
+ {
+ var child = VisualTreeHelper.GetChild(parent, i);
+ // If the child is not of the request child type child
+ T childType = child as T;
+ if (childType == null)
+ {
+ // recursively drill down the tree
+ foundChild = FindChild<T>(child, childName);
+
+ // If the child is found, break so we do not overwrite the found child.
+ if (foundChild != null) break;
+ }
+ else if (!string.IsNullOrEmpty(childName))
+ {
+ var frameworkElement = child as FrameworkElement;
+ // If the child's name is set for search
+ if (frameworkElement != null && frameworkElement.Name == childName)
+ {
+ // if the child's name is of the request name
+ foundChild = (T)child;
+ break;
+ }
+ }
+ else
+ {
+ // child element found.
+ foundChild = (T)child;
+ break;
+ }
+ }
+
+ return foundChild;
+ }
+
+ #region find parent
+
+ /// <summary>
+ /// Finds a parent of a given item on the visual tree.
+ /// </summary>
+ /// <typeparam name="T">The type of the queried item.</typeparam>
+ /// <param name="child">A direct or indirect child of the
+ /// queried item.</param>
+ /// <returns>The first parent item that matches the submitted
+ /// type parameter. If not matching item can be found, a null
+ /// reference is being returned.</returns>
+ public static T TryFindParent<T>(this DependencyObject child)
+ where T : DependencyObject
+ {
+ //get parent item
+ DependencyObject parentObject = GetParentObject(child);
+
+ //we've reached the end of the tree
+ if (parentObject == null) return null;
+
+ //check if the parent matches the type we're looking for
+ T parent = parentObject as T;
+ if (parent != null)
+ {
+ return parent;
+ }
+
+ //use recursion to proceed with next level
+ return TryFindParent<T>(parentObject);
+ }
+
+ /// <summary>
+ /// This method is an alternative to WPF's
+ /// <see cref="VisualTreeHelper.GetParent"/> method, which also
+ /// supports content elements. Keep in mind that for content element,
+ /// this method falls back to the logical tree of the element!
+ /// </summary>
+ /// <param name="child">The item to be processed.</param>
+ /// <returns>The submitted item's parent, if available. Otherwise
+ /// null.</returns>
+ public static DependencyObject GetParentObject(this DependencyObject child)
+ {
+ if (child == null) return null;
+
+ //handle content elements separately
+ ContentElement contentElement = child as ContentElement;
+ if (contentElement != null)
+ {
+ DependencyObject parent = ContentOperations.GetParent(contentElement);
+ if (parent != null) return parent;
+
+ FrameworkContentElement fce = contentElement as FrameworkContentElement;
+ return fce != null ? fce.Parent : null;
+ }
+
+ //also try searching for parent in framework elements (such as DockPanel, etc)
+ FrameworkElement frameworkElement = child as FrameworkElement;
+ if (frameworkElement != null)
+ {
+ DependencyObject parent = frameworkElement.Parent;
+ if (parent != null) return parent;
+ }
+
+ //if it's not a ContentElement/FrameworkElement, rely on VisualTreeHelper
+ return VisualTreeHelper.GetParent(child);
+ }
+
+ #endregion
+
+ #region find children
+
+ /// <summary>
+ /// Analyzes both visual and logical tree in order to find all elements of a given
+ /// type that are descendants of the <paramref name="source"/> item.
+ /// </summary>
+ /// <typeparam name="T">The type of the queried items.</typeparam>
+ /// <param name="source">The root element that marks the source of the search. If the
+ /// source is already of the requested type, it will not be included in the result.</param>
+ /// <returns>All descendants of <paramref name="source"/> that match the requested type.</returns>
+ public static IEnumerable<T> FindChildren<T>(this DependencyObject source) where T : DependencyObject
+ {
+ if (source != null)
+ {
+ var childs = GetChildObjects(source);
+ foreach (DependencyObject child in childs)
+ {
+ //analyze if children match the requested type
+ if (child is T)
+ {
+ yield return (T)child;
+ }
+
+ //recurse tree
+ foreach (T descendant in FindChildren<T>(child))
+ {
+ yield return descendant;
+ }
+ }
+ }
+ }
+
+
+ /// <summary>
+ /// This method is an alternative to WPF's
+ /// <see cref="VisualTreeHelper.GetChild"/> method, which also
+ /// supports content elements. Keep in mind that for content elements,
+ /// this method falls back to the logical tree of the element.
+ /// </summary>
+ /// <param name="parent">The item to be processed.</param>
+ /// <returns>The submitted item's child elements, if available.</returns>
+ public static IEnumerable<DependencyObject> GetChildObjects(this DependencyObject parent)
+ {
+ if (parent == null) yield break;
+
+ if (parent is ContentElement || parent is FrameworkElement)
+ {
+ //use the logical tree for content / framework elements
+ foreach (object obj in LogicalTreeHelper.GetChildren(parent))
+ {
+ var depObj = obj as DependencyObject;
+ if (depObj != null) yield return (DependencyObject)obj;
+ }
+ }
+ else
+ {
+ //use the visual tree per default
+ int count = VisualTreeHelper.GetChildrenCount(parent);
+ for (int i = 0; i < count; i++)
+ {
+ yield return VisualTreeHelper.GetChild(parent, i);
+ }
+ }
+ }
+
+ #endregion
+
+ #region find from point
+
+ /// <summary>
+ /// Tries to locate a given item within the visual tree,
+ /// starting with the dependency object at a given position.
+ /// </summary>
+ /// <typeparam name="T">The type of the element to be found
+ /// on the visual tree of the element at the given location.</typeparam>
+ /// <param name="reference">The main element which is used to perform
+ /// hit testing.</param>
+ /// <param name="point">The position to be evaluated on the origin.</param>
+ public static T TryFindFromPoint<T>(UIElement reference, Point point)
+ where T : DependencyObject
+ {
+ DependencyObject element = reference.InputHitTest(point) as DependencyObject;
+
+ if (element == null) return null;
+
+ if (element is T) return (T)element;
+
+ return TryFindParent<T>(element);
+ }
+
+ #endregion
+ }
+}
diff --git a/MediaBrowser.UI/Controls/WindowCommands.xaml b/MediaBrowser.UI/Controls/WindowCommands.xaml
new file mode 100644
index 000000000..920954918
--- /dev/null
+++ b/MediaBrowser.UI/Controls/WindowCommands.xaml
@@ -0,0 +1,91 @@
+<UserControl x:Class="MediaBrowser.UI.Controls.WindowCommands"
+ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+ xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+ mc:Ignorable="d"
+ d:DesignHeight="300" d:DesignWidth="300">
+
+ <UserControl.Resources>
+
+ <Style TargetType="StackPanel" x:Key="WindowCommandsPanel">
+ <Setter Property="Orientation" Value="Horizontal"/>
+ <Setter Property="HorizontalAlignment" Value="Right"/>
+ </Style>
+
+ <Style TargetType="Button" x:Key="WebdingsButton" BasedOn="{StaticResource ImageButton}">
+ <Setter Property="Margin" Value="0 0 15 0"/>
+ <Setter Property="KeyboardNavigation.IsTabStop" Value="false"/>
+ </Style>
+
+ <Style TargetType="TextBlock" x:Key="WebdingsTextBlock">
+ <Setter Property="FontFamily" Value="Webdings"/>
+ <Setter Property="FontSize" Value="14"/>
+ <Setter Property="Foreground" Value="{StaticResource DefaultForeground}"/>
+ </Style>
+
+ <Style TargetType="Button" x:Key="MinimizeApplicationButton" BasedOn="{StaticResource WebdingsButton}">
+ <Setter Property="ToolTip" Value="Minimize"/>
+ <Setter Property="Template">
+ <Setter.Value>
+ <ControlTemplate>
+ <TextBlock Style="{StaticResource WebdingsTextBlock}">0</TextBlock>
+ </ControlTemplate>
+ </Setter.Value>
+ </Setter>
+ </Style>
+
+ <Style TargetType="Button" x:Key="MaximizeApplicationButton" BasedOn="{StaticResource WebdingsButton}">
+ <Setter Property="ToolTip" Value="Maximize"/>
+ <Setter Property="Template">
+ <Setter.Value>
+ <ControlTemplate>
+ <TextBlock Style="{StaticResource WebdingsTextBlock}">1</TextBlock>
+ </ControlTemplate>
+ </Setter.Value>
+ </Setter>
+ <Style.Triggers>
+ <DataTrigger Binding="{Binding Path=WindowState, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}}" Value="Maximized">
+ <Setter Property="Visibility" Value="Collapsed" />
+ </DataTrigger>
+ </Style.Triggers>
+ </Style>
+
+ <Style TargetType="Button" x:Key="UndoMaximizeApplicationButton" BasedOn="{StaticResource WebdingsButton}">
+ <Setter Property="Visibility" Value="Collapsed"/>
+ <Setter Property="ToolTip" Value="Restore"/>
+ <Setter Property="Template">
+ <Setter.Value>
+ <ControlTemplate>
+ <TextBlock Style="{StaticResource WebdingsTextBlock}">2</TextBlock>
+ </ControlTemplate>
+ </Setter.Value>
+ </Setter>
+ <Style.Triggers>
+ <DataTrigger Binding="{Binding Path=WindowState, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}}" Value="Maximized">
+ <Setter Property="Visibility" Value="Visible" />
+ </DataTrigger>
+ </Style.Triggers>
+ </Style>
+
+ <Style TargetType="Button" x:Key="CloseApplicationButton" BasedOn="{StaticResource WebdingsButton}">
+ <Setter Property="ToolTip" Value="Close"/>
+ <Setter Property="Template">
+ <Setter.Value>
+ <ControlTemplate>
+ <TextBlock Style="{StaticResource WebdingsTextBlock}">r</TextBlock>
+ </ControlTemplate>
+ </Setter.Value>
+ </Setter>
+ </Style>
+
+ </UserControl.Resources>
+
+ <StackPanel Style="{StaticResource WindowCommandsPanel}">
+ <Button x:Name="MinimizeApplicationButton" Style="{StaticResource MinimizeApplicationButton}"></Button>
+ <Button x:Name="MaximizeApplicationButton" Style="{StaticResource MaximizeApplicationButton}"></Button>
+ <Button x:Name="UndoMaximizeApplicationButton" Style="{StaticResource UndoMaximizeApplicationButton}"></Button>
+ <Button x:Name="CloseApplicationButton" Style="{StaticResource CloseApplicationButton}"></Button>
+ </StackPanel>
+
+</UserControl>
diff --git a/MediaBrowser.UI/Controls/WindowCommands.xaml.cs b/MediaBrowser.UI/Controls/WindowCommands.xaml.cs
new file mode 100644
index 000000000..1810c5bf3
--- /dev/null
+++ b/MediaBrowser.UI/Controls/WindowCommands.xaml.cs
@@ -0,0 +1,50 @@
+using System.Windows;
+using System.Windows.Controls;
+
+namespace MediaBrowser.UI.Controls
+{
+ /// <summary>
+ /// Interaction logic for WindowCommands.xaml
+ /// </summary>
+ public partial class WindowCommands : UserControl
+ {
+ public Window ParentWindow
+ {
+ get { return TreeHelper.TryFindParent<Window>(this); }
+ }
+
+ public WindowCommands()
+ {
+ InitializeComponent();
+ Loaded += WindowCommandsLoaded;
+ }
+
+ void WindowCommandsLoaded(object sender, RoutedEventArgs e)
+ {
+ CloseApplicationButton.Click += CloseApplicationButtonClick;
+ MinimizeApplicationButton.Click += MinimizeApplicationButtonClick;
+ MaximizeApplicationButton.Click += MaximizeApplicationButtonClick;
+ UndoMaximizeApplicationButton.Click += UndoMaximizeApplicationButtonClick;
+ }
+
+ void UndoMaximizeApplicationButtonClick(object sender, RoutedEventArgs e)
+ {
+ ParentWindow.WindowState = WindowState.Normal;
+ }
+
+ void MaximizeApplicationButtonClick(object sender, RoutedEventArgs e)
+ {
+ ParentWindow.WindowState = WindowState.Maximized;
+ }
+
+ void MinimizeApplicationButtonClick(object sender, RoutedEventArgs e)
+ {
+ ParentWindow.WindowState = WindowState.Minimized;
+ }
+
+ void CloseApplicationButtonClick(object sender, RoutedEventArgs e)
+ {
+ ParentWindow.Close();
+ }
+ }
+}
diff --git a/MediaBrowser.UI/Converters/CurrentUserVisibilityConverter.cs b/MediaBrowser.UI/Converters/CurrentUserVisibilityConverter.cs
new file mode 100644
index 000000000..a5dd5013b
--- /dev/null
+++ b/MediaBrowser.UI/Converters/CurrentUserVisibilityConverter.cs
@@ -0,0 +1,26 @@
+using System;
+using System.Globalization;
+using System.Windows;
+using System.Windows.Data;
+
+namespace MediaBrowser.UI.Converters
+{
+ public class CurrentUserVisibilityConverter : IValueConverter
+ {
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ if (App.Instance.ServerConfiguration == null || !App.Instance.ServerConfiguration.EnableUserProfiles)
+ {
+ return Visibility.Collapsed;
+ }
+
+ return value == null ? Visibility.Collapsed : Visibility.Visible;
+ }
+
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
+ }
+}
diff --git a/MediaBrowser.UI/Converters/DateTimeToStringConverter.cs b/MediaBrowser.UI/Converters/DateTimeToStringConverter.cs
new file mode 100644
index 000000000..6c568c061
--- /dev/null
+++ b/MediaBrowser.UI/Converters/DateTimeToStringConverter.cs
@@ -0,0 +1,34 @@
+using System;
+using System.Globalization;
+using System.Windows.Data;
+
+namespace MediaBrowser.UI.Converters
+{
+ public class DateTimeToStringConverter : IValueConverter
+ {
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ var date = (DateTime)value;
+
+ string format = parameter as string;
+
+ if (string.IsNullOrEmpty(format))
+ {
+ return date.ToString();
+ }
+
+ if (format.Equals("shorttime", StringComparison.OrdinalIgnoreCase))
+ {
+ return date.ToShortTimeString();
+ }
+
+ return date.ToString(format);
+ }
+
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
+ }
+}
diff --git a/MediaBrowser.UI/Converters/LastSeenTextConverter.cs b/MediaBrowser.UI/Converters/LastSeenTextConverter.cs
new file mode 100644
index 000000000..746260210
--- /dev/null
+++ b/MediaBrowser.UI/Converters/LastSeenTextConverter.cs
@@ -0,0 +1,86 @@
+using MediaBrowser.Model.DTO;
+using System;
+using System.Globalization;
+using System.Windows.Data;
+
+namespace MediaBrowser.UI.Converters
+{
+ public class LastSeenTextConverter : IValueConverter
+ {
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ var user = value as DtoUser;
+
+ if (user != null)
+ {
+ if (user.LastActivityDate.HasValue)
+ {
+ DateTime date = user.LastActivityDate.Value.ToLocalTime();
+
+ return "Last seen " + GetRelativeTimeText(date);
+ }
+ }
+
+ return null;
+ }
+
+ private static string GetRelativeTimeText(DateTime date)
+ {
+ TimeSpan ts = DateTime.Now - date;
+
+ const int second = 1;
+ const int minute = 60 * second;
+ const int hour = 60 * minute;
+ const int day = 24 * hour;
+ const int month = 30 * day;
+
+ int delta = System.Convert.ToInt32(ts.TotalSeconds);
+
+ if (delta < 0)
+ {
+ return "not yet";
+ }
+ if (delta < 1 * minute)
+ {
+ return ts.Seconds == 1 ? "one second ago" : ts.Seconds + " seconds ago";
+ }
+ if (delta < 2 * minute)
+ {
+ return "a minute ago";
+ }
+ if (delta < 45 * minute)
+ {
+ return ts.Minutes + " minutes ago";
+ }
+ if (delta < 90 * minute)
+ {
+ return "an hour ago";
+ }
+ if (delta < 24 * hour)
+ {
+ return ts.Hours + " hours ago";
+ }
+ if (delta < 48 * hour)
+ {
+ return "yesterday";
+ }
+ if (delta < 30 * day)
+ {
+ return ts.Days + " days ago";
+ }
+ if (delta < 12 * month)
+ {
+ int months = System.Convert.ToInt32(Math.Floor((double)ts.Days / 30));
+ return months <= 1 ? "one month ago" : months + " months ago";
+ }
+
+ int years = System.Convert.ToInt32(Math.Floor((double)ts.Days / 365));
+ return years <= 1 ? "one year ago" : years + " years ago";
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
+ }
+}
diff --git a/MediaBrowser.UI/Converters/UserImageConverter.cs b/MediaBrowser.UI/Converters/UserImageConverter.cs
new file mode 100644
index 000000000..a9ef4b862
--- /dev/null
+++ b/MediaBrowser.UI/Converters/UserImageConverter.cs
@@ -0,0 +1,60 @@
+using MediaBrowser.Model.DTO;
+using MediaBrowser.UI.Controller;
+using System;
+using System.Globalization;
+using System.Net.Cache;
+using System.Windows.Data;
+using System.Windows.Media.Imaging;
+
+namespace MediaBrowser.UI.Converters
+{
+ public class UserImageConverter : IValueConverter
+ {
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ var user = value as DtoUser;
+
+ if (user != null && user.HasImage)
+ {
+ var config = parameter as string;
+
+ int? maxWidth = null;
+ int? maxHeight = null;
+ int? width = null;
+ int? height = null;
+
+ if (!string.IsNullOrEmpty(config))
+ {
+ var vals = config.Split(',');
+
+ width = GetSize(vals[0]);
+ height = GetSize(vals[1]);
+ maxWidth = GetSize(vals[2]);
+ maxHeight = GetSize(vals[3]);
+ }
+
+ var uri = UIKernel.Instance.ApiClient.GetUserImageUrl(user.Id, width, height, maxWidth, maxHeight, 100);
+
+ return new BitmapImage(new Uri(uri), new RequestCachePolicy(RequestCacheLevel.Revalidate));
+ }
+
+ return null;
+ }
+
+ private int? GetSize(string val)
+ {
+ if (string.IsNullOrEmpty(val) || val == "0")
+ {
+ return null;
+ }
+
+ return int.Parse(val);
+ }
+
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
+ }
+}
diff --git a/MediaBrowser.UI/Converters/WeatherTemperatureConverter.cs b/MediaBrowser.UI/Converters/WeatherTemperatureConverter.cs
new file mode 100644
index 000000000..cab4c595c
--- /dev/null
+++ b/MediaBrowser.UI/Converters/WeatherTemperatureConverter.cs
@@ -0,0 +1,31 @@
+using MediaBrowser.Model.Weather;
+using System;
+using System.Globalization;
+using System.Windows.Data;
+
+namespace MediaBrowser.UI.Converters
+{
+ public class WeatherTemperatureConverter : IValueConverter
+ {
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ var weather = value as WeatherInfo;
+
+ if (weather != null)
+ {
+ if (App.Instance.ServerConfiguration.WeatherUnit == WeatherUnits.Celsius)
+ {
+ return weather.CurrentWeather.TemperatureCelsius + "°C";
+ }
+
+ return weather.CurrentWeather.TemperatureFahrenheit + "°F";
+ }
+ return null;
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
+ }
+}
diff --git a/MediaBrowser.UI/Converters/WeatherVisibilityConverter.cs b/MediaBrowser.UI/Converters/WeatherVisibilityConverter.cs
new file mode 100644
index 000000000..5706ecec9
--- /dev/null
+++ b/MediaBrowser.UI/Converters/WeatherVisibilityConverter.cs
@@ -0,0 +1,20 @@
+using System;
+using System.Globalization;
+using System.Windows;
+using System.Windows.Data;
+
+namespace MediaBrowser.UI.Converters
+{
+ public class WeatherVisibilityConverter : IValueConverter
+ {
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ return value == null ? Visibility.Collapsed : Visibility.Visible;
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
+ }
+}
diff --git a/MediaBrowser.UI/MainWindow.xaml b/MediaBrowser.UI/MainWindow.xaml
new file mode 100644
index 000000000..b3c36915e
--- /dev/null
+++ b/MediaBrowser.UI/MainWindow.xaml
@@ -0,0 +1,50 @@
+<Window x:Class="MediaBrowser.UI.MainWindow"
+ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ xmlns:controls="clr-namespace:MediaBrowser.UI.Controls"
+ Title="media browser"
+ Style="{StaticResource MainWindow}"
+ WindowStartupLocation="CenterScreen"
+ AllowsTransparency="True"
+ WindowStyle="None"
+ ResizeMode="CanResizeWithGrip"
+ KeyboardNavigation.DirectionalNavigation="Contained">
+
+ <!--The window itself is a tabstop, and it can't be disabled. So this is a workaround.-->
+ <Grid>
+
+ <Grid x:Name="BackdropGrid" Style="{StaticResource BackdropGrid}">
+ </Grid>
+
+ <!--This allows the user to drag the window.-->
+ <Grid x:Name="DragBar" Style="{StaticResource DragBar}"></Grid>
+
+ <!--This allows the user to drag the window.-->
+ <controls:WindowCommands x:Name="WindowCommands" Style="{StaticResource WindowCommands}"></controls:WindowCommands>
+
+ <!--Themes will supply this template to outline the window structure.-->
+ <ContentControl x:Name="PageContent" Template="{StaticResource PageContentTemplate}"></ContentControl>
+
+ <Grid x:Name="NavBarGrid" Style="{StaticResource NavBarGrid}">
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition Width="auto"></ColumnDefinition>
+ <ColumnDefinition Width="*"></ColumnDefinition>
+ <ColumnDefinition Width="auto"></ColumnDefinition>
+ </Grid.ColumnDefinitions>
+
+ <StackPanel Style="{StaticResource NavBarGridLeftPanel}">
+ <Button x:Name="BackButton" Style="{StaticResource BackButton}"></Button>
+ <Button x:Name="ForwardButton" Style="{StaticResource ForwardButton}"></Button>
+ </StackPanel>
+ <StackPanel Style="{StaticResource NavBarGridCenterPanel}">
+ <Button x:Name="MuteButton" Style="{StaticResource MuteButton}"></Button>
+ <Button x:Name="VolumeDownButton" Style="{StaticResource VolumeDownButton}"></Button>
+ <Button x:Name="VolumeUpButton" Style="{StaticResource VolumeUpButton}"></Button>
+ </StackPanel>
+ <StackPanel Style="{StaticResource NavBarGridRightPanel}">
+ <Button x:Name="SettingsButton" Style="{StaticResource SettingsButton}"></Button>
+ <Button x:Name="ExitButton" Style="{StaticResource ExitButton}"></Button>
+ </StackPanel>
+ </Grid>
+ </Grid>
+</Window>
diff --git a/MediaBrowser.UI/MainWindow.xaml.cs b/MediaBrowser.UI/MainWindow.xaml.cs
new file mode 100644
index 000000000..07e8e9433
--- /dev/null
+++ b/MediaBrowser.UI/MainWindow.xaml.cs
@@ -0,0 +1,368 @@
+using MediaBrowser.Model.DTO;
+using MediaBrowser.UI.Controller;
+using MediaBrowser.UI.Controls;
+using System;
+using System.ComponentModel;
+using System.Linq;
+using System.Threading;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Input;
+using System.Windows.Media.Animation;
+using System.Windows.Media.Imaging;
+
+namespace MediaBrowser.UI
+{
+ /// <summary>
+ /// Interaction logic for MainWindow.xaml
+ /// </summary>
+ public partial class MainWindow : Window, INotifyPropertyChanged
+ {
+ private Timer MouseIdleTimer { get; set; }
+ private Timer BackdropTimer { get; set; }
+ private Image BackdropImage { get; set; }
+ private string[] CurrentBackdrops { get; set; }
+ private int CurrentBackdropIndex { get; set; }
+
+ public MainWindow()
+ {
+ InitializeComponent();
+
+ BackButton.Click += BtnApplicationBackClick;
+ ExitButton.Click += ExitButtonClick;
+ ForwardButton.Click += ForwardButtonClick;
+ DragBar.MouseDown += DragableGridMouseDown;
+ Loaded += MainWindowLoaded;
+ }
+
+ public event PropertyChangedEventHandler PropertyChanged;
+
+ public void OnPropertyChanged(String info)
+ {
+ if (PropertyChanged != null)
+ {
+ PropertyChanged(this, new PropertyChangedEventArgs(info));
+ }
+ }
+
+ private bool _isMouseIdle = true;
+ public bool IsMouseIdle
+ {
+ get { return _isMouseIdle; }
+ set
+ {
+ _isMouseIdle = value;
+ OnPropertyChanged("IsMouseIdle");
+ }
+ }
+
+ void MainWindowLoaded(object sender, RoutedEventArgs e)
+ {
+ DataContext = App.Instance;
+
+ if (App.Instance.ServerConfiguration == null)
+ {
+ App.Instance.PropertyChanged += ApplicationPropertyChanged;
+ }
+ else
+ {
+ LoadInitialPage();
+ }
+ }
+
+ void ForwardButtonClick(object sender, RoutedEventArgs e)
+ {
+ NavigateForward();
+ }
+
+ void ExitButtonClick(object sender, RoutedEventArgs e)
+ {
+ Close();
+ }
+
+ void ApplicationPropertyChanged(object sender, PropertyChangedEventArgs e)
+ {
+ if (e.PropertyName.Equals("ServerConfiguration"))
+ {
+ App.Instance.PropertyChanged -= ApplicationPropertyChanged;
+ LoadInitialPage();
+ }
+ }
+
+ private async void LoadInitialPage()
+ {
+ await App.Instance.LogoutUser().ConfigureAwait(false);
+ }
+
+ private void DragableGridMouseDown(object sender, MouseButtonEventArgs e)
+ {
+ if (e.ClickCount == 2)
+ {
+ WindowState = WindowState == WindowState.Maximized ? WindowState.Normal : WindowState.Maximized;
+ }
+ else if (e.LeftButton == MouseButtonState.Pressed)
+ {
+ DragMove();
+ }
+ }
+
+ void BtnApplicationBackClick(object sender, RoutedEventArgs e)
+ {
+ NavigateBack();
+ }
+
+ private Frame PageFrame
+ {
+ get
+ {
+ // Finding the grid that is generated by the ControlTemplate of the Button
+ return TreeHelper.FindChild<Frame>(PageContent, "PageFrame");
+ }
+ }
+
+ public void Navigate(Uri uri)
+ {
+ PageFrame.Navigate(uri);
+ }
+
+ /// <summary>
+ /// Sets the backdrop based on an ApiBaseItemWrapper
+ /// </summary>
+ public void SetBackdrops(DtoBaseItem item)
+ {
+ SetBackdrops(UIKernel.Instance.ApiClient.GetBackdropImageUrls(item, null, null, 1920, 1080));
+ }
+
+ /// <summary>
+ /// Sets the backdrop based on a list of image files
+ /// </summary>
+ public async void SetBackdrops(string[] backdrops)
+ {
+ // Don't reload the same backdrops
+ if (CurrentBackdrops != null && backdrops.SequenceEqual(CurrentBackdrops))
+ {
+ return;
+ }
+
+ if (BackdropTimer != null)
+ {
+ BackdropTimer.Dispose();
+ }
+
+ BackdropGrid.Children.Clear();
+
+ if (backdrops.Length == 0)
+ {
+ CurrentBackdrops = null;
+ return;
+ }
+
+ CurrentBackdropIndex = GetFirstBackdropIndex();
+
+ Image image = await App.Instance.GetImage(backdrops.ElementAt(CurrentBackdropIndex));
+ image.SetResourceReference(Image.StyleProperty, "BackdropImage");
+
+ BackdropGrid.Children.Add(image);
+
+ CurrentBackdrops = backdrops;
+ BackdropImage = image;
+
+ const int backdropRotationTime = 7000;
+
+ if (backdrops.Count() > 1)
+ {
+ BackdropTimer = new Timer(BackdropTimerCallback, null, backdropRotationTime, backdropRotationTime);
+ }
+ }
+
+ public void ClearBackdrops()
+ {
+ if (BackdropTimer != null)
+ {
+ BackdropTimer.Dispose();
+ }
+
+ BackdropGrid.Children.Clear();
+
+ CurrentBackdrops = null;
+ }
+
+ private void BackdropTimerCallback(object stateInfo)
+ {
+ // Need to do this on the UI thread
+ Application.Current.Dispatcher.InvokeAsync(() =>
+ {
+ var animFadeOut = new Storyboard();
+ animFadeOut.Completed += AnimFadeOutCompleted;
+
+ var fadeOut = new DoubleAnimation();
+ fadeOut.From = 1.0;
+ fadeOut.To = 0.5;
+ fadeOut.Duration = new Duration(TimeSpan.FromSeconds(1));
+
+ animFadeOut.Children.Add(fadeOut);
+ Storyboard.SetTarget(fadeOut, BackdropImage);
+ Storyboard.SetTargetProperty(fadeOut, new PropertyPath(Image.OpacityProperty));
+
+ animFadeOut.Begin(this);
+ });
+ }
+
+ async void AnimFadeOutCompleted(object sender, System.EventArgs e)
+ {
+ if (CurrentBackdrops == null)
+ {
+ return;
+ }
+
+ int backdropIndex = GetNextBackdropIndex();
+
+ BitmapImage image = await App.Instance.GetBitmapImage(CurrentBackdrops[backdropIndex]);
+ CurrentBackdropIndex = backdropIndex;
+
+ // Need to do this on the UI thread
+ BackdropImage.Source = image;
+ Storyboard imageFadeIn = new Storyboard();
+
+ DoubleAnimation fadeIn = new DoubleAnimation();
+
+ fadeIn.From = 0.25;
+ fadeIn.To = 1.0;
+ fadeIn.Duration = new Duration(TimeSpan.FromSeconds(1));
+
+ imageFadeIn.Children.Add(fadeIn);
+ Storyboard.SetTarget(fadeIn, BackdropImage);
+ Storyboard.SetTargetProperty(fadeIn, new PropertyPath(Image.OpacityProperty));
+ imageFadeIn.Begin(this);
+ }
+
+ private int GetFirstBackdropIndex()
+ {
+ return 0;
+ }
+
+ private int GetNextBackdropIndex()
+ {
+ if (CurrentBackdropIndex < CurrentBackdrops.Length - 1)
+ {
+ return CurrentBackdropIndex + 1;
+ }
+
+ return 0;
+ }
+
+ public void NavigateBack()
+ {
+ if (PageFrame.NavigationService.CanGoBack)
+ {
+ PageFrame.NavigationService.GoBack();
+ }
+ }
+
+ public void NavigateForward()
+ {
+ if (PageFrame.NavigationService.CanGoForward)
+ {
+ PageFrame.NavigationService.GoForward();
+ }
+ }
+
+ /// <summary>
+ /// Shows the control bar then starts a timer to hide it
+ /// </summary>
+ private void StartMouseIdleTimer()
+ {
+ IsMouseIdle = false;
+
+ const int duration = 10000;
+
+ // Start the timer if it's null, otherwise reset it
+ if (MouseIdleTimer == null)
+ {
+ MouseIdleTimer = new Timer(MouseIdleTimerCallback, null, duration, Timeout.Infinite);
+ }
+ else
+ {
+ MouseIdleTimer.Change(duration, Timeout.Infinite);
+ }
+ }
+
+ /// <summary>
+ /// This is the Timer callback method to hide the control bar
+ /// </summary>
+ private void MouseIdleTimerCallback(object stateInfo)
+ {
+ IsMouseIdle = true;
+
+ if (MouseIdleTimer != null)
+ {
+ MouseIdleTimer.Dispose();
+ MouseIdleTimer = null;
+ }
+ }
+
+ /// <summary>
+ /// Handles OnMouseMove to show the control box
+ /// </summary>
+ protected override void OnMouseMove(MouseEventArgs e)
+ {
+ base.OnMouseMove(e);
+
+ StartMouseIdleTimer();
+ }
+
+ /// <summary>
+ /// Handles OnKeyUp to provide keyboard based navigation
+ /// </summary>
+ protected override void OnKeyUp(KeyEventArgs e)
+ {
+ base.OnKeyUp(e);
+
+ if (IsBackPress(e))
+ {
+ NavigateBack();
+ }
+
+ else if (IsForwardPress(e))
+ {
+ NavigateForward();
+ }
+ }
+
+ /// <summary>
+ /// Determines if a keypress should be treated as a backward press
+ /// </summary>
+ private bool IsBackPress(KeyEventArgs e)
+ {
+ if (e.Key == Key.BrowserBack || e.Key == Key.Back)
+ {
+ return true;
+ }
+
+ if (e.SystemKey == Key.Left && e.KeyboardDevice.Modifiers.HasFlag(ModifierKeys.Alt))
+ {
+ return true;
+ }
+
+ return false;
+ }
+
+ /// <summary>
+ /// Determines if a keypress should be treated as a forward press
+ /// </summary>
+ private bool IsForwardPress(KeyEventArgs e)
+ {
+ if (e.Key == Key.BrowserForward)
+ {
+ return true;
+ }
+
+ if (e.SystemKey == Key.RightAlt && e.KeyboardDevice.Modifiers.HasFlag(ModifierKeys.Alt))
+ {
+ return true;
+ }
+
+ return false;
+ }
+ }
+}
diff --git a/MediaBrowser.UI/MediaBrowser.UI.csproj b/MediaBrowser.UI/MediaBrowser.UI.csproj
new file mode 100644
index 000000000..b099d0f83
--- /dev/null
+++ b/MediaBrowser.UI/MediaBrowser.UI.csproj
@@ -0,0 +1,196 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
+ <PropertyGroup>
+ <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+ <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+ <ProjectGuid>{B5ECE1FB-618E-420B-9A99-8E972D76920A}</ProjectGuid>
+ <OutputType>WinExe</OutputType>
+ <AppDesignerFolder>Properties</AppDesignerFolder>
+ <RootNamespace>MediaBrowser.UI</RootNamespace>
+ <AssemblyName>MediaBrowser.UI</AssemblyName>
+ <TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
+ <FileAlignment>512</FileAlignment>
+ <ProjectTypeGuids>{60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
+ <WarningLevel>4</WarningLevel>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
+ <PlatformTarget>AnyCPU</PlatformTarget>
+ <DebugSymbols>true</DebugSymbols>
+ <DebugType>full</DebugType>
+ <Optimize>false</Optimize>
+ <OutputPath>bin\Debug\</OutputPath>
+ <DefineConstants>DEBUG;TRACE</DefineConstants>
+ <ErrorReport>prompt</ErrorReport>
+ <WarningLevel>4</WarningLevel>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
+ <PlatformTarget>AnyCPU</PlatformTarget>
+ <DebugType>pdbonly</DebugType>
+ <Optimize>true</Optimize>
+ <OutputPath>bin\Release\</OutputPath>
+ <DefineConstants>TRACE</DefineConstants>
+ <ErrorReport>prompt</ErrorReport>
+ <WarningLevel>4</WarningLevel>
+ </PropertyGroup>
+ <PropertyGroup>
+ <StartupObject>MediaBrowser.UI.App</StartupObject>
+ </PropertyGroup>
+ <PropertyGroup>
+ <ApplicationIcon>Resources\Images\Icon.ico</ApplicationIcon>
+ </PropertyGroup>
+ <ItemGroup>
+ <Reference Include="System" />
+ <Reference Include="System.ComponentModel.Composition" />
+ <Reference Include="System.Data" />
+ <Reference Include="System.Drawing" />
+ <Reference Include="System.Net.Http" />
+ <Reference Include="System.Net.Http.WebRequest" />
+ <Reference Include="System.Web" />
+ <Reference Include="System.Windows.Forms" />
+ <Reference Include="System.Xml" />
+ <Reference Include="Microsoft.CSharp" />
+ <Reference Include="System.Core" />
+ <Reference Include="System.Xml.Linq" />
+ <Reference Include="System.Data.DataSetExtensions" />
+ <Reference Include="System.Xaml">
+ <RequiredTargetFramework>4.0</RequiredTargetFramework>
+ </Reference>
+ <Reference Include="WindowsBase" />
+ <Reference Include="PresentationCore" />
+ <Reference Include="PresentationFramework" />
+ </ItemGroup>
+ <ItemGroup>
+ <Compile Include="Configuration\UIApplicationConfiguration.cs" />
+ <Compile Include="Configuration\UIApplicationPaths.cs" />
+ <Compile Include="Controller\PluginUpdater.cs" />
+ <Compile Include="Controls\EnhancedScrollViewer.cs" />
+ <Compile Include="Controls\ExtendedImage.cs" />
+ <Compile Include="Controls\TreeHelper.cs" />
+ <Compile Include="Controls\WindowCommands.xaml.cs">
+ <DependentUpon>WindowCommands.xaml</DependentUpon>
+ </Compile>
+ <Compile Include="Converters\CurrentUserVisibilityConverter.cs" />
+ <Compile Include="Converters\DateTimeToStringConverter.cs" />
+ <Compile Include="Converters\LastSeenTextConverter.cs" />
+ <Compile Include="Converters\WeatherTemperatureConverter.cs" />
+ <Compile Include="Converters\WeatherVisibilityConverter.cs" />
+ <Compile Include="Controller\UIKernel.cs" />
+ <Compile Include="Pages\BaseLoginPage.cs" />
+ <Page Include="App.xaml">
+ <Generator>MSBuild:Compile</Generator>
+ <SubType>Designer</SubType>
+ </Page>
+ <Page Include="Controls\WindowCommands.xaml">
+ <Generator>MSBuild:Compile</Generator>
+ <SubType>Designer</SubType>
+ </Page>
+ <Page Include="MainWindow.xaml">
+ <Generator>MSBuild:Compile</Generator>
+ <SubType>Designer</SubType>
+ </Page>
+ <Compile Include="App.xaml.cs">
+ <DependentUpon>App.xaml</DependentUpon>
+ <SubType>Code</SubType>
+ </Compile>
+ <Compile Include="Pages\BasePage.cs" />
+ <Compile Include="Converters\UserImageConverter.cs" />
+ <Compile Include="MainWindow.xaml.cs">
+ <DependentUpon>MainWindow.xaml</DependentUpon>
+ <SubType>Code</SubType>
+ </Compile>
+ <Page Include="Resources\AppResources.xaml">
+ <SubType>Designer</SubType>
+ <Generator>MSBuild:Compile</Generator>
+ </Page>
+ <Page Include="Resources\MainWindowResources.xaml">
+ <SubType>Designer</SubType>
+ <Generator>MSBuild:Compile</Generator>
+ </Page>
+ <Page Include="Resources\NavBarResources.xaml">
+ <SubType>Designer</SubType>
+ <Generator>MSBuild:Compile</Generator>
+ </Page>
+ <Page Include="Themes\Generic.xaml">
+ <Generator>MSBuild:Compile</Generator>
+ <SubType>Designer</SubType>
+ </Page>
+ </ItemGroup>
+ <ItemGroup>
+ <Compile Include="Properties\AssemblyInfo.cs">
+ <SubType>Code</SubType>
+ </Compile>
+ <Compile Include="Properties\Resources.Designer.cs">
+ <AutoGen>True</AutoGen>
+ <DesignTime>True</DesignTime>
+ <DependentUpon>Resources.resx</DependentUpon>
+ </Compile>
+ <Compile Include="Properties\Settings.Designer.cs">
+ <AutoGen>True</AutoGen>
+ <DependentUpon>Settings.settings</DependentUpon>
+ <DesignTimeSharedInput>True</DesignTimeSharedInput>
+ </Compile>
+ <EmbeddedResource Include="Properties\Resources.resx">
+ <Generator>ResXFileCodeGenerator</Generator>
+ <LastGenOutput>Resources.Designer.cs</LastGenOutput>
+ </EmbeddedResource>
+ <None Include="Properties\Settings.settings">
+ <Generator>SettingsSingleFileGenerator</Generator>
+ <LastGenOutput>Settings.Designer.cs</LastGenOutput>
+ </None>
+ <AppDesigner Include="Properties\" />
+ </ItemGroup>
+ <ItemGroup>
+ <None Include="App.config">
+ <SubType>Designer</SubType>
+ </None>
+ </ItemGroup>
+ <ItemGroup>
+ <ProjectReference Include="..\..\MediaBrowserServer\MediaBrowser.ApiInteraction\MediaBrowser.ApiInteraction.csproj">
+ <Project>{921c0f64-fda7-4e9f-9e73-0cb0eedb2422}</Project>
+ <Name>MediaBrowser.ApiInteraction</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\..\MediaBrowserServer\MediaBrowser.Common\MediaBrowser.Common.csproj">
+ <Project>{9142eefa-7570-41e1-bfcc-468bb571af2f}</Project>
+ <Name>MediaBrowser.Common</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\..\MediaBrowserServer\MediaBrowser.Model\MediaBrowser.Model.csproj">
+ <Project>{7eeeb4bb-f3e8-48fc-b4c5-70f0fff8329b}</Project>
+ <Name>MediaBrowser.Model</Name>
+ </ProjectReference>
+ </ItemGroup>
+ <ItemGroup>
+ <Resource Include="Resources\Images\BackButton.png" />
+ </ItemGroup>
+ <ItemGroup>
+ <Resource Include="Resources\Images\ForwardButton.png" />
+ </ItemGroup>
+ <ItemGroup>
+ <Resource Include="Resources\Images\ExitButton.png" />
+ </ItemGroup>
+ <ItemGroup>
+ <Resource Include="Resources\Images\SettingsButton.png" />
+ </ItemGroup>
+ <ItemGroup>
+ <Resource Include="Resources\Images\VolumeUpButton.png" />
+ <Resource Include="Resources\Images\VolumeDownButton.png" />
+ </ItemGroup>
+ <ItemGroup>
+ <Resource Include="Resources\Images\MuteButton.png" />
+ </ItemGroup>
+ <ItemGroup>
+ <Resource Include="Resources\Images\Icon.ico" />
+ </ItemGroup>
+ <ItemGroup>
+ <Resource Include="Resources\Images\mblogoblack.png" />
+ <Resource Include="Resources\Images\mblogowhite.png" />
+ </ItemGroup>
+ <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
+ <!-- To modify your build process, add your task inside one of the targets below and uncomment it.
+ Other similar extension points exist, see Microsoft.Common.targets.
+ <Target Name="BeforeBuild">
+ </Target>
+ <Target Name="AfterBuild">
+ </Target>
+ -->
+</Project> \ No newline at end of file
diff --git a/MediaBrowser.UI/Pages/BaseLoginPage.cs b/MediaBrowser.UI/Pages/BaseLoginPage.cs
new file mode 100644
index 000000000..cd3151df0
--- /dev/null
+++ b/MediaBrowser.UI/Pages/BaseLoginPage.cs
@@ -0,0 +1,33 @@
+using MediaBrowser.Model.DTO;
+using MediaBrowser.UI.Controller;
+using System;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.UI.Pages
+{
+ public class BaseLoginPage : BasePage
+ {
+ private DtoUser[] _users;
+ public DtoUser[] Users
+ {
+ get { return _users; }
+
+ set
+ {
+ _users = value;
+ OnPropertyChanged("Users");
+ }
+ }
+
+ protected override async Task LoadData()
+ {
+ Users = await UIKernel.Instance.ApiClient.GetAllUsersAsync().ConfigureAwait(false);
+ }
+
+ protected void UserClicked(DtoUser user)
+ {
+ App.Instance.CurrentUser = user;
+ //App.Instance.Navigate(new Uri("/Pages/HomePage.xaml", UriKind.Relative));
+ }
+ }
+}
diff --git a/MediaBrowser.UI/Pages/BasePage.cs b/MediaBrowser.UI/Pages/BasePage.cs
new file mode 100644
index 000000000..800f6e215
--- /dev/null
+++ b/MediaBrowser.UI/Pages/BasePage.cs
@@ -0,0 +1,79 @@
+using System;
+using System.Collections.Specialized;
+using System.ComponentModel;
+using System.Threading.Tasks;
+using System.Web;
+using System.Windows;
+using System.Windows.Controls;
+
+namespace MediaBrowser.UI.Pages
+{
+ public abstract class BasePage : Page, INotifyPropertyChanged
+ {
+ public event PropertyChangedEventHandler PropertyChanged;
+
+ public void OnPropertyChanged(String info)
+ {
+ if (PropertyChanged != null)
+ {
+ PropertyChanged(this, new PropertyChangedEventArgs(info));
+ }
+ }
+
+ protected Uri Uri
+ {
+ get
+ {
+ return NavigationService.CurrentSource;
+ }
+ }
+
+ protected MainWindow MainWindow
+ {
+ get
+ {
+ return App.Instance.MainWindow as MainWindow;
+ }
+ }
+
+ private NameValueCollection _queryString;
+ protected NameValueCollection QueryString
+ {
+ get
+ {
+ if (_queryString == null)
+ {
+ string url = Uri.ToString();
+
+ int index = url.IndexOf('?');
+
+ if (index == -1)
+ {
+ _queryString = new NameValueCollection();
+ }
+ else
+ {
+ _queryString = HttpUtility.ParseQueryString(url.Substring(index + 1));
+ }
+ }
+
+ return _queryString;
+ }
+ }
+
+ protected BasePage()
+ : base()
+ {
+ Loaded += BasePageLoaded;
+ }
+
+ async void BasePageLoaded(object sender, RoutedEventArgs e)
+ {
+ await LoadData();
+
+ DataContext = this;
+ }
+
+ protected abstract Task LoadData();
+ }
+}
diff --git a/MediaBrowser.UI/Properties/AssemblyInfo.cs b/MediaBrowser.UI/Properties/AssemblyInfo.cs
new file mode 100644
index 000000000..565b1801e
--- /dev/null
+++ b/MediaBrowser.UI/Properties/AssemblyInfo.cs
@@ -0,0 +1,53 @@
+using System.Reflection;
+using System.Runtime.InteropServices;
+using System.Windows;
+
+// 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.UI")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("MediaBrowser.UI")]
+[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)]
+
+//In order to begin building localizable applications, set
+//<UICulture>CultureYouAreCodingWith</UICulture> in your .csproj file
+//inside a <PropertyGroup>. For example, if you are using US english
+//in your source files, set the <UICulture> to en-US. Then uncomment
+//the NeutralResourceLanguage attribute below. Update the "en-US" in
+//the line below to match the UICulture setting in the project file.
+
+//[assembly: NeutralResourcesLanguage("en-US", UltimateResourceFallbackLocation.Satellite)]
+
+
+[assembly: ThemeInfo(
+ ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
+ //(used if a resource is not found in the page,
+ // or application resource dictionaries)
+ ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
+ //(used if a resource is not found in the page,
+ // app, or any theme specific resource dictionaries)
+)]
+
+
+// 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.UI/Properties/Resources.Designer.cs b/MediaBrowser.UI/Properties/Resources.Designer.cs
new file mode 100644
index 000000000..b9d742620
--- /dev/null
+++ b/MediaBrowser.UI/Properties/Resources.Designer.cs
@@ -0,0 +1,71 @@
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.17626
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace MediaBrowser.UI.Properties
+{
+
+
+ /// <summary>
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ /// </summary>
+ // This class was auto-generated by the StronglyTypedResourceBuilder
+ // class via a tool like ResGen or Visual Studio.
+ // To add or remove a member, edit your .ResX file then rerun ResGen
+ // with the /str option, or rebuild your VS project.
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ internal class Resources
+ {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal Resources()
+ {
+ }
+
+ /// <summary>
+ /// Returns the cached ResourceManager instance used by this class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Resources.ResourceManager ResourceManager
+ {
+ get
+ {
+ if ((resourceMan == null))
+ {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("MediaBrowser.UI.Properties.Resources", typeof(Resources).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ /// <summary>
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Globalization.CultureInfo Culture
+ {
+ get
+ {
+ return resourceCulture;
+ }
+ set
+ {
+ resourceCulture = value;
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.UI/Properties/Resources.resx b/MediaBrowser.UI/Properties/Resources.resx
new file mode 100644
index 000000000..ffecec851
--- /dev/null
+++ b/MediaBrowser.UI/Properties/Resources.resx
@@ -0,0 +1,117 @@
+<?xml version="1.0" encoding="utf-8"?>
+<root>
+ <!--
+ Microsoft ResX Schema
+
+ Version 2.0
+
+ The primary goals of this format is to allow a simple XML format
+ that is mostly human readable. The generation and parsing of the
+ various data types are done through the TypeConverter classes
+ associated with the data types.
+
+ Example:
+
+ ... ado.net/XML headers & schema ...
+ <resheader name="resmimetype">text/microsoft-resx</resheader>
+ <resheader name="version">2.0</resheader>
+ <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+ <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+ <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+ <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+ <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+ <value>[base64 mime encoded serialized .NET Framework object]</value>
+ </data>
+ <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+ <comment>This is a comment</comment>
+ </data>
+
+ There are any number of "resheader" rows that contain simple
+ name/value pairs.
+
+ Each data row contains a name, and value. The row also contains a
+ type or mimetype. Type corresponds to a .NET class that support
+ text/value conversion through the TypeConverter architecture.
+ Classes that don't support this are serialized and stored with the
+ mimetype set.
+
+ The mimetype is used for serialized objects, and tells the
+ ResXResourceReader how to depersist the object. This is currently not
+ extensible. For a given mimetype the value must be set accordingly:
+
+ Note - application/x-microsoft.net.object.binary.base64 is the format
+ that the ResXResourceWriter will generate, however the reader can
+ read any of the formats listed below.
+
+ mimetype: application/x-microsoft.net.object.binary.base64
+ value : The object must be serialized with
+ : System.Serialization.Formatters.Binary.BinaryFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.soap.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.bytearray.base64
+ value : The object must be serialized into a byte array
+ : using a System.ComponentModel.TypeConverter
+ : and then encoded with base64 encoding.
+ -->
+ <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+ <xsd:element name="root" msdata:IsDataSet="true">
+ <xsd:complexType>
+ <xsd:choice maxOccurs="unbounded">
+ <xsd:element name="metadata">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" />
+ <xsd:attribute name="type" type="xsd:string" />
+ <xsd:attribute name="mimetype" type="xsd:string" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="assembly">
+ <xsd:complexType>
+ <xsd:attribute name="alias" type="xsd:string" />
+ <xsd:attribute name="name" type="xsd:string" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="data">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" msdata:Ordinal="1" />
+ <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+ <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="resheader">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" />
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:choice>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:schema>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>2.0</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+</root> \ No newline at end of file
diff --git a/MediaBrowser.UI/Properties/Settings.Designer.cs b/MediaBrowser.UI/Properties/Settings.Designer.cs
new file mode 100644
index 000000000..4d9ddf50d
--- /dev/null
+++ b/MediaBrowser.UI/Properties/Settings.Designer.cs
@@ -0,0 +1,30 @@
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.17626
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace MediaBrowser.UI.Properties
+{
+
+
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "11.0.0.0")]
+ internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase
+ {
+
+ private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));
+
+ public static Settings Default
+ {
+ get
+ {
+ return defaultInstance;
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.UI/Properties/Settings.settings b/MediaBrowser.UI/Properties/Settings.settings
new file mode 100644
index 000000000..8f2fd95d6
--- /dev/null
+++ b/MediaBrowser.UI/Properties/Settings.settings
@@ -0,0 +1,7 @@
+<?xml version='1.0' encoding='utf-8'?>
+<SettingsFile xmlns="uri:settings" CurrentProfile="(Default)">
+ <Profiles>
+ <Profile Name="(Default)" />
+ </Profiles>
+ <Settings />
+</SettingsFile> \ No newline at end of file
diff --git a/MediaBrowser.UI/Resources/AppResources.xaml b/MediaBrowser.UI/Resources/AppResources.xaml
new file mode 100644
index 000000000..8d4f36d4f
--- /dev/null
+++ b/MediaBrowser.UI/Resources/AppResources.xaml
@@ -0,0 +1,122 @@
+<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ xmlns:System="clr-namespace:System;assembly=mscorlib"
+ xmlns:converters="clr-namespace:MediaBrowser.UI.Converters">
+
+ <!--Themes should override these as needed-->
+ <FontFamily x:Key="DefaultFontFamily">Segoe UI, Lucida Sans Unicode, Verdana</FontFamily>
+ <FontWeight x:Key="DefaultFontWeight">Thin</FontWeight>
+ <Brush x:Key="DefaultForeground">Black</Brush>
+ <System:Double x:Key="DefaultFontSize">36</System:Double>
+ <System:Double x:Key="Heading1FontSize">84</System:Double>
+ <System:Double x:Key="Heading2FontSize">60</System:Double>
+
+ <!--Define all the standard converters here in one place-->
+ <converters:DateTimeToStringConverter x:Key="DateTimeToStringConverter"></converters:DateTimeToStringConverter>
+ <converters:UserImageConverter x:Key="UserImageConverter"></converters:UserImageConverter>
+ <converters:WeatherTemperatureConverter x:Key="WeatherTemperatureConverter"></converters:WeatherTemperatureConverter>
+ <converters:LastSeenTextConverter x:Key="LastSeenTextConverter"></converters:LastSeenTextConverter>
+ <converters:WeatherVisibilityConverter x:Key="WeatherVisibilityConverter"></converters:WeatherVisibilityConverter>
+ <converters:CurrentUserVisibilityConverter x:Key="CurrentUserVisibilityConverter"></converters:CurrentUserVisibilityConverter>
+
+ <!--Default Frame style. -->
+ <Style TargetType="Frame">
+ <Setter Property="NavigationUIVisibility" Value="Hidden"/>
+ <Setter Property="KeyboardNavigation.IsTabStop" Value="false"/>
+ </Style>
+
+ <!--Default Frame style. -->
+ <Style TargetType="ContentControl">
+ <Setter Property="KeyboardNavigation.IsTabStop" Value="false"/>
+ </Style>
+
+ <!--Default Window style. -->
+ <Style TargetType="Window" x:Key="BaseWindow">
+ <Setter Property="FontSize" Value="{StaticResource DefaultFontSize}"/>
+ <Setter Property="FontFamily" Value="{StaticResource DefaultFontFamily}"/>
+ <Setter Property="FontWeight" Value="{StaticResource DefaultFontWeight}"/>
+ <Setter Property="Foreground" Value="{StaticResource DefaultForeground}"/>
+ <Setter Property="BorderBrush" Value="#cccccc"/>
+ <Setter Property="BorderThickness" Value="1"/>
+ </Style>
+
+ <!--Default TextBlock style. -->
+ <Style TargetType="TextBlock">
+ <Setter Property="FontSize" Value="{StaticResource DefaultFontSize}"/>
+ <Setter Property="FontFamily" Value="{StaticResource DefaultFontFamily}"/>
+ <Setter Property="FontWeight" Value="{StaticResource DefaultFontWeight}"/>
+ <Setter Property="Foreground" Value="{StaticResource DefaultForeground}"/>
+ <Setter Property="TextWrapping" Value="Wrap" />
+ </Style>
+
+ <!--Default Label style. -->
+ <Style TargetType="Label">
+ <Setter Property="FontSize" Value="{StaticResource DefaultFontSize}"/>
+ <Setter Property="FontFamily" Value="{StaticResource DefaultFontFamily}"/>
+ <Setter Property="FontWeight" Value="{StaticResource DefaultFontWeight}"/>
+ <Setter Property="Foreground" Value="{StaticResource DefaultForeground}"/>
+ </Style>
+
+ <!--Default Button style. -->
+ <Style TargetType="Button">
+ <Setter Property="FontSize" Value="{StaticResource DefaultFontSize}"/>
+ <Setter Property="FontFamily" Value="{StaticResource DefaultFontFamily}"/>
+ <Setter Property="FontWeight" Value="{StaticResource DefaultFontWeight}"/>
+ <Setter Property="Foreground" Value="{StaticResource DefaultForeground}"/>
+ </Style>
+
+ <!--Default style for buttons that have images. -->
+ <Style TargetType="Button" x:Key="ImageButton" BasedOn="{StaticResource {x:Static ToolBar.ButtonStyleKey}}">
+ <Setter Property="Margin" Value="0"/>
+ <Setter Property="Padding" Value="0"/>
+ <Setter Property="BorderThickness" Value="0"/>
+ <Setter Property="Cursor" Value="Hand"/>
+ <Style.Triggers>
+ <Trigger Property="IsMouseOver" Value="True">
+ <Setter Property="Opacity" Value=".5" />
+ </Trigger>
+ </Style.Triggers>
+ </Style>
+
+ <!--Default ListViewItem style. -->
+ <Style x:Key="BaseListViewItemStyle" TargetType="{x:Type ListViewItem}">
+
+ <Setter Property="Padding" Value="0" />
+ <Setter Property="Margin" Value="0" />
+ <Setter Property="Cursor" Value="Hand"/>
+
+ <Style.Triggers>
+ <Trigger Property="IsKeyboardFocusWithin" Value="True">
+ <Setter Property="IsSelected" Value="True" />
+ </Trigger>
+ </Style.Triggers>
+ </Style>
+
+ <!--Themes should override this -->
+ <Style x:Key="ListViewItemStyle" TargetType="{x:Type ListViewItem}" BasedOn="{StaticResource BaseListViewItemStyle}">
+ </Style>
+
+ <!--Default ListView style. -->
+ <Style TargetType="ListView" x:Key="BaseListViewStyle">
+ <Setter Property="BorderThickness" Value="0"/>
+ <Setter Property="Background" Value="Transparent"/>
+ <Setter Property="KeyboardNavigation.IsTabStop" Value="False"/>
+ <Setter Property="KeyboardNavigation.DirectionalNavigation" Value="Continue"/>
+ <Setter Property="VirtualizingPanel.IsVirtualizing" Value="True"/>
+ <Setter Property="IsSynchronizedWithCurrentItem" Value="True"/>
+ </Style>
+
+ <!--Themes should override this -->
+ <Style x:Key="ListViewStyle" TargetType="{x:Type ListView}" BasedOn="{StaticResource BaseListViewStyle}">
+ </Style>
+
+ <!--MB Logo, black text. -->
+ <Style TargetType="Image" x:Key="MBLogoImageBlack">
+ <Setter Property="Source" Value="Images/mblogoblack.png"/>
+ </Style>
+
+ <!--MB Logo, white text. -->
+ <Style TargetType="Image" x:Key="MBLogoImageWhite">
+ <Setter Property="Source" Value="Images/mblogowhite.png"/>
+ </Style>
+</ResourceDictionary> \ No newline at end of file
diff --git a/MediaBrowser.UI/Resources/Images/BackButton.png b/MediaBrowser.UI/Resources/Images/BackButton.png
new file mode 100644
index 000000000..263eceadb
--- /dev/null
+++ b/MediaBrowser.UI/Resources/Images/BackButton.png
Binary files differ
diff --git a/MediaBrowser.UI/Resources/Images/ExitButton.png b/MediaBrowser.UI/Resources/Images/ExitButton.png
new file mode 100644
index 000000000..c7d5c0f76
--- /dev/null
+++ b/MediaBrowser.UI/Resources/Images/ExitButton.png
Binary files differ
diff --git a/MediaBrowser.UI/Resources/Images/ForwardButton.png b/MediaBrowser.UI/Resources/Images/ForwardButton.png
new file mode 100644
index 000000000..a9548b309
--- /dev/null
+++ b/MediaBrowser.UI/Resources/Images/ForwardButton.png
Binary files differ
diff --git a/MediaBrowser.UI/Resources/Images/Icon.ico b/MediaBrowser.UI/Resources/Images/Icon.ico
new file mode 100644
index 000000000..f8accfab2
--- /dev/null
+++ b/MediaBrowser.UI/Resources/Images/Icon.ico
Binary files differ
diff --git a/MediaBrowser.UI/Resources/Images/MuteButton.png b/MediaBrowser.UI/Resources/Images/MuteButton.png
new file mode 100644
index 000000000..fa454b8f3
--- /dev/null
+++ b/MediaBrowser.UI/Resources/Images/MuteButton.png
Binary files differ
diff --git a/MediaBrowser.UI/Resources/Images/SettingsButton.png b/MediaBrowser.UI/Resources/Images/SettingsButton.png
new file mode 100644
index 000000000..04ca4d32b
--- /dev/null
+++ b/MediaBrowser.UI/Resources/Images/SettingsButton.png
Binary files differ
diff --git a/MediaBrowser.UI/Resources/Images/VolumeDownButton.png b/MediaBrowser.UI/Resources/Images/VolumeDownButton.png
new file mode 100644
index 000000000..c7ff252ce
--- /dev/null
+++ b/MediaBrowser.UI/Resources/Images/VolumeDownButton.png
Binary files differ
diff --git a/MediaBrowser.UI/Resources/Images/VolumeUpButton.png b/MediaBrowser.UI/Resources/Images/VolumeUpButton.png
new file mode 100644
index 000000000..c89d25691
--- /dev/null
+++ b/MediaBrowser.UI/Resources/Images/VolumeUpButton.png
Binary files differ
diff --git a/MediaBrowser.UI/Resources/Images/mblogoblack.png b/MediaBrowser.UI/Resources/Images/mblogoblack.png
new file mode 100644
index 000000000..84323fe52
--- /dev/null
+++ b/MediaBrowser.UI/Resources/Images/mblogoblack.png
Binary files differ
diff --git a/MediaBrowser.UI/Resources/Images/mblogowhite.png b/MediaBrowser.UI/Resources/Images/mblogowhite.png
new file mode 100644
index 000000000..a39812e35
--- /dev/null
+++ b/MediaBrowser.UI/Resources/Images/mblogowhite.png
Binary files differ
diff --git a/MediaBrowser.UI/Resources/MainWindowResources.xaml b/MediaBrowser.UI/Resources/MainWindowResources.xaml
new file mode 100644
index 000000000..624e7a633
--- /dev/null
+++ b/MediaBrowser.UI/Resources/MainWindowResources.xaml
@@ -0,0 +1,43 @@
+<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
+
+ <!--Themes should override this to style the window-->
+ <Style TargetType="Window" x:Key="MainWindow" BasedOn="{StaticResource BaseWindow}">
+ </Style>
+
+ <!--Themes may want to override this to adjust the backdrop container style-->
+ <Style TargetType="Grid" x:Key="BackdropGrid">
+ <Setter Property="Background" Value="Transparent"/>
+ <Setter Property="Opacity" Value=".15"/>
+ </Style>
+
+ <!--Themes may want to override this to adjust the backdrop image style-->
+ <Style TargetType="Image" x:Key="BackdropImage">
+ <Setter Property="Stretch" Value="UniformToFill"/>
+ </Style>
+
+ <Style TargetType="Grid" x:Key="DragBar">
+ <Setter Property="Background" Value="Transparent"/>
+ <Setter Property="Height" Value="50"/>
+ <Setter Property="VerticalAlignment" Value="Top"/>
+ <Setter Property="Panel.ZIndex" Value="1"/>
+ </Style>
+ <Style TargetType="UserControl" x:Key="WindowCommands">
+ <Setter Property="Margin" Value="0 10 0 0"/>
+ <Setter Property="HorizontalAlignment" Value="Right"/>
+ <Setter Property="VerticalAlignment" Value="Top"/>
+ <Setter Property="Panel.ZIndex" Value="2"/>
+ <Setter Property="Visibility" Value="Collapsed" />
+ <Style.Triggers>
+ <DataTrigger Binding="{Binding Path=MainWindow.IsMouseIdle}" Value="false">
+ <Setter Property="Visibility" Value="Visible" />
+ </DataTrigger>
+ </Style.Triggers>
+ </Style>
+
+ <!--Themes should override this to layout window content-->
+ <ControlTemplate x:Key="PageContentTemplate">
+ <Frame x:Name="PageFrame"></Frame>
+ </ControlTemplate>
+
+</ResourceDictionary> \ No newline at end of file
diff --git a/MediaBrowser.UI/Resources/NavBarResources.xaml b/MediaBrowser.UI/Resources/NavBarResources.xaml
new file mode 100644
index 000000000..c2181c16f
--- /dev/null
+++ b/MediaBrowser.UI/Resources/NavBarResources.xaml
@@ -0,0 +1,122 @@
+<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
+ <Style TargetType="Button" x:Key="NavBarButton" BasedOn="{StaticResource ImageButton}">
+ <Setter Property="Margin" Value="10 0 10 0"/>
+ <Setter Property="VerticalAlignment" Value="Center"/>
+ <Setter Property="HorizontalAlignment" Value="Left"/>
+ <Setter Property="KeyboardNavigation.IsTabStop" Value="False"/>
+ </Style>
+
+ <Style TargetType="Button" x:Key="BackButton" BasedOn="{StaticResource NavBarButton}">
+ <Setter Property="ToolTip" Value="Back"/>
+ <Setter Property="Template">
+ <Setter.Value>
+ <ControlTemplate>
+ <Image x:Name="img" Source="..\Resources\Images\BackButton.png" Stretch="None" />
+ </ControlTemplate>
+ </Setter.Value>
+ </Setter>
+ </Style>
+
+ <Style TargetType="Button" x:Key="ForwardButton" BasedOn="{StaticResource NavBarButton}">
+ <Setter Property="ToolTip" Value="Forward"/>
+ <Setter Property="Template">
+ <Setter.Value>
+ <ControlTemplate>
+ <Image x:Name="img" Source="..\Resources\Images\ForwardButton.png" Stretch="None" />
+ </ControlTemplate>
+ </Setter.Value>
+ </Setter>
+ </Style>
+
+ <Style TargetType="Button" x:Key="ExitButton" BasedOn="{StaticResource NavBarButton}">
+ <Setter Property="ToolTip" Value="Exit"/>
+ <Setter Property="Template">
+ <Setter.Value>
+ <ControlTemplate>
+ <Image x:Name="img" Source="..\Resources\Images\ExitButton.png" Stretch="None" />
+ </ControlTemplate>
+ </Setter.Value>
+ </Setter>
+ </Style>
+
+ <Style TargetType="Button" x:Key="SettingsButton" BasedOn="{StaticResource NavBarButton}">
+ <Setter Property="ToolTip" Value="Settings"/>
+ <Setter Property="Template">
+ <Setter.Value>
+ <ControlTemplate>
+ <Image x:Name="img" Source="..\Resources\Images\SettingsButton.png" Stretch="None" />
+ </ControlTemplate>
+ </Setter.Value>
+ </Setter>
+ </Style>
+
+ <Style TargetType="Button" x:Key="VolumeUpButton" BasedOn="{StaticResource NavBarButton}">
+ <Setter Property="ToolTip" Value="Increase Volume"/>
+ <Setter Property="Template">
+ <Setter.Value>
+ <ControlTemplate>
+ <Image x:Name="img" Source="..\Resources\Images\VolumeUpButton.png" Stretch="None" />
+ </ControlTemplate>
+ </Setter.Value>
+ </Setter>
+ </Style>
+
+ <Style TargetType="Button" x:Key="VolumeDownButton" BasedOn="{StaticResource NavBarButton}">
+ <Setter Property="ToolTip" Value="Decrease Volume"/>
+ <Setter Property="Template">
+ <Setter.Value>
+ <ControlTemplate>
+ <Image x:Name="img" Source="..\Resources\Images\VolumeDownButton.png" Stretch="None" />
+ </ControlTemplate>
+ </Setter.Value>
+ </Setter>
+ </Style>
+
+ <Style TargetType="Button" x:Key="MuteButton" BasedOn="{StaticResource NavBarButton}">
+ <Setter Property="ToolTip" Value="Mute"/>
+ <Setter Property="Template">
+ <Setter.Value>
+ <ControlTemplate>
+ <Image x:Name="img" Source="..\Resources\Images\MuteButton.png" Stretch="None" />
+ </ControlTemplate>
+ </Setter.Value>
+ </Setter>
+ </Style>
+
+ <Style TargetType="Grid" x:Key="NavBarGrid">
+ <Setter Property="VerticalAlignment" Value="Bottom"/>
+ <Setter Property="Background">
+ <Setter.Value>
+ <LinearGradientBrush StartPoint="0,0" EndPoint="0,1" Opacity=".8">
+ <GradientStop Color="#333333" Offset="0.0"/>
+ <GradientStop Color="Black" Offset="1.0"/>
+ </LinearGradientBrush>
+ </Setter.Value>
+ </Setter>
+ <Setter Property="Visibility" Value="Collapsed" />
+ <Style.Triggers>
+ <DataTrigger Binding="{Binding Path=MainWindow.IsMouseIdle}" Value="false">
+ <Setter Property="Visibility" Value="Visible" />
+ </DataTrigger>
+ </Style.Triggers>
+ </Style>
+ <Style TargetType="StackPanel" x:Key="NavBarGridLeftPanel">
+ <Setter Property="Grid.Column" Value="0"/>
+ <Setter Property="HorizontalAlignment" Value="Left"/>
+ <Setter Property="Orientation" Value="Horizontal"/>
+ <Setter Property="Margin" Value="15 20 15 20"/>
+ </Style>
+ <Style TargetType="StackPanel" x:Key="NavBarGridCenterPanel">
+ <Setter Property="Grid.Column" Value="1"/>
+ <Setter Property="HorizontalAlignment" Value="Center"/>
+ <Setter Property="Orientation" Value="Horizontal"/>
+ <Setter Property="Margin" Value="15 20 15 20"/>
+ </Style>
+ <Style TargetType="StackPanel" x:Key="NavBarGridRightPanel">
+ <Setter Property="Grid.Column" Value="2"/>
+ <Setter Property="HorizontalAlignment" Value="Right"/>
+ <Setter Property="Orientation" Value="Horizontal"/>
+ <Setter Property="Margin" Value="15 20 15 20"/>
+ </Style>
+</ResourceDictionary> \ No newline at end of file
diff --git a/MediaBrowser.UI/Themes/Generic.xaml b/MediaBrowser.UI/Themes/Generic.xaml
new file mode 100644
index 000000000..c34489b4e
--- /dev/null
+++ b/MediaBrowser.UI/Themes/Generic.xaml
@@ -0,0 +1,32 @@
+<ResourceDictionary
+ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ xmlns:local="clr-namespace:MediaBrowser.UI.Controls">
+
+ <Style TargetType="{x:Type local:ExtendedImage}">
+ <Setter Property="Template">
+ <Setter.Value>
+ <ControlTemplate TargetType="{x:Type local:ExtendedImage}">
+ <Border Background="{TemplateBinding Background}">
+ <Image x:Name="theImage">
+ <Image.Style>
+ <Style TargetType="{x:Type Image}">
+ <Setter Property="Source"
+ Value="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=PlaceHolderSource}" />
+ <Setter Property="Stretch"
+ Value="{Binding RelativeSource={RelativeSource TemplatedParent}, Path= Stretch}" />
+ <Style.Triggers>
+ <DataTrigger Binding="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=HasImage}" Value="True">
+ <Setter Property="Source"
+ Value="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=Source}" />
+ </DataTrigger>
+ </Style.Triggers>
+ </Style>
+ </Image.Style>
+ </Image>
+ </Border>
+ </ControlTemplate>
+ </Setter.Value>
+ </Setter>
+ </Style>
+</ResourceDictionary>
diff --git a/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj b/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj
new file mode 100644
index 000000000..7f1015591
--- /dev/null
+++ b/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj
@@ -0,0 +1,70 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
+ <PropertyGroup>
+ <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+ <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+ <ProjectGuid>{5624B7B5-B5A7-41D8-9F10-CC5611109619}</ProjectGuid>
+ <OutputType>Library</OutputType>
+ <AppDesignerFolder>Properties</AppDesignerFolder>
+ <RootNamespace>MediaBrowser.WebDashboard</RootNamespace>
+ <AssemblyName>MediaBrowser.WebDashboard</AssemblyName>
+ <TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
+ <FileAlignment>512</FileAlignment>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
+ <DebugSymbols>true</DebugSymbols>
+ <DebugType>full</DebugType>
+ <Optimize>false</Optimize>
+ <OutputPath>bin\Debug\</OutputPath>
+ <DefineConstants>DEBUG;TRACE</DefineConstants>
+ <ErrorReport>prompt</ErrorReport>
+ <WarningLevel>4</WarningLevel>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
+ <DebugType>pdbonly</DebugType>
+ <Optimize>true</Optimize>
+ <OutputPath>bin\Release\</OutputPath>
+ <DefineConstants>TRACE</DefineConstants>
+ <ErrorReport>prompt</ErrorReport>
+ <WarningLevel>4</WarningLevel>
+ </PropertyGroup>
+ <PropertyGroup>
+ <RunPostBuildEvent>Always</RunPostBuildEvent>
+ </PropertyGroup>
+ <ItemGroup>
+ <Reference Include="System" />
+ <Reference Include="System.ComponentModel.Composition" />
+ <Reference Include="System.Core" />
+ <Reference Include="System.Xml.Linq" />
+ <Reference Include="System.Data.DataSetExtensions" />
+ <Reference Include="Microsoft.CSharp" />
+ <Reference Include="System.Data" />
+ <Reference Include="System.Xml" />
+ </ItemGroup>
+ <ItemGroup>
+ <Compile Include="Plugin.cs" />
+ <Compile Include="Properties\AssemblyInfo.cs" />
+ </ItemGroup>
+ <ItemGroup>
+ <ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj">
+ <Project>{9142eefa-7570-41e1-bfcc-468bb571af2f}</Project>
+ <Name>MediaBrowser.Common</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj">
+ <Project>{7eeeb4bb-f3e8-48fc-b4c5-70f0fff8329b}</Project>
+ <Name>MediaBrowser.Model</Name>
+ </ProjectReference>
+ </ItemGroup>
+ <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
+ <PropertyGroup>
+ <PostBuildEvent>xcopy "$(TargetPath)" "$(SolutionDir)\ProgramData-Server\Plugins\" /y</PostBuildEvent>
+ </PropertyGroup>
+ <!-- To modify your build process, add your task inside one of the targets below and uncomment it.
+ Other similar extension points exist, see Microsoft.Common.targets.
+ <Target Name="BeforeBuild">
+ </Target>
+ <Target Name="AfterBuild">
+ </Target>
+ -->
+</Project> \ No newline at end of file
diff --git a/MediaBrowser.WebDashboard/Plugin.cs b/MediaBrowser.WebDashboard/Plugin.cs
new file mode 100644
index 000000000..95727a06f
--- /dev/null
+++ b/MediaBrowser.WebDashboard/Plugin.cs
@@ -0,0 +1,15 @@
+using MediaBrowser.Common.Plugins;
+using MediaBrowser.Model.Plugins;
+using System.ComponentModel.Composition;
+
+namespace MediaBrowser.WebDashboard
+{
+ [Export(typeof(BasePlugin))]
+ public class Plugin : BasePlugin
+ {
+ public override string Name
+ {
+ get { return "Dashboard"; }
+ }
+ }
+}
diff --git a/MediaBrowser.WebDashboard/Properties/AssemblyInfo.cs b/MediaBrowser.WebDashboard/Properties/AssemblyInfo.cs
new file mode 100644
index 000000000..a33fd4462
--- /dev/null
+++ b/MediaBrowser.WebDashboard/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.WebDashboard")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("MediaBrowser.WebDashboard")]
+[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("c6c73d12-933d-4389-be56-e59973c9ed00")]
+
+// 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.sln b/MediaBrowser.sln
new file mode 100644
index 000000000..51f450743
--- /dev/null
+++ b/MediaBrowser.sln
@@ -0,0 +1,55 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio 2012
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MediaBrowser.Controller", "MediaBrowser.Controller\MediaBrowser.Controller.csproj", "{17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MediaBrowser.Api", "MediaBrowser.Api\MediaBrowser.Api.csproj", "{4FD51AC5-2C16-4308-A993-C3A84F3B4582}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MediaBrowser.Common", "MediaBrowser.Common\MediaBrowser.Common.csproj", "{9142EEFA-7570-41E1-BFCC-468BB571AF2F}"
+EndProject
+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.ServerApplication", "MediaBrowser.ServerApplication\MediaBrowser.ServerApplication.csproj", "{156EA256-AD2D-4D2F-B116-2ED4B9EFD869}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MediaBrowser.WebDashboard", "MediaBrowser.WebDashboard\MediaBrowser.WebDashboard.csproj", "{5624B7B5-B5A7-41D8-9F10-CC5611109619}"
+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
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {156EA256-AD2D-4D2F-B116-2ED4B9EFD869}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {156EA256-AD2D-4D2F-B116-2ED4B9EFD869}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {156EA256-AD2D-4D2F-B116-2ED4B9EFD869}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {156EA256-AD2D-4D2F-B116-2ED4B9EFD869}.Release|Any CPU.Build.0 = Release|Any CPU
+ {17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}.Release|Any CPU.Build.0 = Release|Any CPU
+ {4FD51AC5-2C16-4308-A993-C3A84F3B4582}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {4FD51AC5-2C16-4308-A993-C3A84F3B4582}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {4FD51AC5-2C16-4308-A993-C3A84F3B4582}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {4FD51AC5-2C16-4308-A993-C3A84F3B4582}.Release|Any CPU.Build.0 = Release|Any CPU
+ {5624B7B5-B5A7-41D8-9F10-CC5611109619}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {5624B7B5-B5A7-41D8-9F10-CC5611109619}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {5624B7B5-B5A7-41D8-9F10-CC5611109619}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {5624B7B5-B5A7-41D8-9F10-CC5611109619}.Release|Any CPU.Build.0 = Release|Any CPU
+ {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
+ {9142EEFA-7570-41E1-BFCC-468BB571AF2F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {9142EEFA-7570-41E1-BFCC-468BB571AF2F}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {9142EEFA-7570-41E1-BFCC-468BB571AF2F}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {9142EEFA-7570-41E1-BFCC-468BB571AF2F}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+EndGlobal