aboutsummaryrefslogtreecommitdiff
path: root/MediaBrowser.Common
diff options
context:
space:
mode:
Diffstat (limited to 'MediaBrowser.Common')
-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
38 files changed, 3473 insertions, 0 deletions
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