using MediaBrowser.ApiInteraction; using MediaBrowser.ClickOnce; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.IO; using MediaBrowser.Common.Kernel; using MediaBrowser.IsoMounter; using MediaBrowser.Logging.Nlog; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Logging; using MediaBrowser.Model.Net; using MediaBrowser.Model.Updates; using MediaBrowser.Model.Weather; using MediaBrowser.UI.Controller; using MediaBrowser.UI.Controls; using MediaBrowser.UI.Pages; using MediaBrowser.UI.Uninstall; using Microsoft.Win32; using System; using System.Collections.Generic; using System.ComponentModel; using System.IO; using System.Linq; using System.Net.Cache; using System.Threading; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; using System.Windows.Media; using System.Windows.Media.Imaging; namespace MediaBrowser.UI { /// /// Interaction logic for App.xaml /// public partial class App : Application, IApplicationHost { /// /// Gets or sets a value indicating whether [last run at startup value]. /// /// null if [last run at startup value] contains no value, true if [last run at startup value]; otherwise, false. private bool? LastRunAtStartupValue { get; set; } /// /// Gets or sets the clock timer. /// /// The clock timer. private Timer ClockTimer { get; set; } /// /// Gets or sets the server configuration timer. /// /// The server configuration timer. private Timer ServerConfigurationTimer { get; set; } /// /// The single instance mutex /// private Mutex SingleInstanceMutex; /// /// Gets or sets the kernel. /// /// The kernel. protected IKernel Kernel { get; set; } /// /// Gets or sets the logger. /// /// The logger. protected ILogger Logger { get; set; } /// /// Gets or sets the log file path. /// /// The log file path. public string LogFilePath { get; private set; } /// /// Occurs when [property changed]. /// public event PropertyChangedEventHandler PropertyChanged; /// /// Gets the name of the product. /// /// The name of the product. protected string ProductName { get { return Globals.ProductName; } } /// /// Gets the name of the publisher. /// /// The name of the publisher. protected string PublisherName { get { return Globals.PublisherName; } } /// /// Gets the name of the suite. /// /// The name of the suite. protected string SuiteName { get { return Globals.SuiteName; } } /// /// Gets the name of the uninstaller file. /// /// The name of the uninstaller file. protected string UninstallerFileName { get { return "MediaBrowser.UI.Uninstall.exe"; } } /// /// Gets the instance. /// /// The instance. public static App Instance { get { return Current as App; } } /// /// Gets the API client. /// /// The API client. public ApiClient ApiClient { get { return UIKernel.Instance.ApiClient; } } /// /// Gets the application window. /// /// The application window. public MainWindow ApplicationWindow { get; private set; } /// /// Gets the hidden window. /// /// The hidden window. public HiddenWindow HiddenWindow { get; private set; } /// /// The _current user /// private UserDto _currentUser; /// /// Gets or sets the current user. /// /// The current user. public UserDto CurrentUser { get { return _currentUser; } set { _currentUser = value; if (UIKernel.Instance.ApiClient != null) { if (value == null) { UIKernel.Instance.ApiClient.CurrentUserId = null; } else { UIKernel.Instance.ApiClient.CurrentUserId = value.Id; } } OnPropertyChanged("CurrentUser"); } } /// /// The _server configuration /// private ServerConfiguration _serverConfiguration; /// /// Gets or sets the server configuration. /// /// The server configuration. public ServerConfiguration ServerConfiguration { get { return _serverConfiguration; } set { _serverConfiguration = value; OnPropertyChanged("ServerConfiguration"); } } /// /// The _current time /// private DateTime _currentTime = DateTime.Now; /// /// Gets the current time. /// /// The current time. public DateTime CurrentTime { get { return _currentTime; } private set { _currentTime = value; OnPropertyChanged("CurrentTime"); } } /// /// The _current weather /// private WeatherInfo _currentWeather; /// /// Gets the current weather. /// /// The current weather. public WeatherInfo CurrentWeather { get { return _currentWeather; } private set { _currentWeather = value; OnPropertyChanged("CurrentWeather"); } } /// /// The _current theme /// private BaseTheme _currentTheme; /// /// Gets the current theme. /// /// The current theme. public BaseTheme CurrentTheme { get { return _currentTheme; } private set { _currentTheme = value; OnPropertyChanged("CurrentTheme"); } } /// /// Defines the entry point of the application. /// [STAThread] public static void Main() { var application = new App(new NLogger("App")); application.InitializeComponent(); application.Run(); } /// /// Initializes a new instance of the class. /// /// The logger. public App(ILogger logger) { Logger = logger; InitializeComponent(); } /// /// Instantiates the kernel. /// /// IKernel. protected IKernel InstantiateKernel() { return new UIKernel(this, new PismoIsoManager(Logger), Logger); } /// /// Instantiates the main window. /// /// Window. protected Window InstantiateMainWindow() { HiddenWindow = new HiddenWindow { }; return HiddenWindow; } /// /// Shows the application window. /// private void ShowApplicationWindow() { var win = new MainWindow(Logger); var config = UIKernel.Instance.Configuration; // Restore window position/size if (config.WindowState.HasValue) { // Set window state win.WindowState = config.WindowState.Value; // Set position if not maximized if (config.WindowState.Value != WindowState.Maximized) { double left = 0; double top = 0; // Set left if (config.WindowLeft.HasValue) { win.WindowStartupLocation = WindowStartupLocation.Manual; win.Left = left = Math.Max(config.WindowLeft.Value, 0); } // Set top if (config.WindowTop.HasValue) { win.WindowStartupLocation = WindowStartupLocation.Manual; win.Top = top = Math.Max(config.WindowTop.Value, 0); } // Set width if (config.WindowWidth.HasValue) { win.Width = Math.Min(config.WindowWidth.Value, SystemParameters.VirtualScreenWidth - left); } // Set height if (config.WindowHeight.HasValue) { win.Height = Math.Min(config.WindowHeight.Value, SystemParameters.VirtualScreenHeight - top); } } } win.LocationChanged += ApplicationWindow_LocationChanged; win.StateChanged += ApplicationWindow_LocationChanged; win.SizeChanged += ApplicationWindow_LocationChanged; ApplicationWindow = win; ApplicationWindow.Show(); ApplicationWindow.Owner = HiddenWindow; SyncHiddenWindowLocation(); } /// /// Handles the LocationChanged event of the ApplicationWindow control. /// /// The source of the event. /// The instance containing the event data. void ApplicationWindow_LocationChanged(object sender, EventArgs e) { SyncHiddenWindowLocation(); } /// /// Syncs the hidden window location. /// public void SyncHiddenWindowLocation() { HiddenWindow.Width = ApplicationWindow.Width; HiddenWindow.Height = ApplicationWindow.Height; HiddenWindow.Top = ApplicationWindow.Top; HiddenWindow.Left = ApplicationWindow.Left; HiddenWindow.WindowState = ApplicationWindow.WindowState; ApplicationWindow.Activate(); } /// /// Loads the kernel. /// protected async void LoadKernel() { // Without this the app will shutdown after the splash screen closes ShutdownMode = ShutdownMode.OnExplicitShutdown; Kernel = InstantiateKernel(); try { var now = DateTime.UtcNow; await Kernel.Init(); Logger.Info("Kernel.Init completed in {0} seconds.", (DateTime.UtcNow - now).TotalSeconds); ShutdownMode = System.Windows.ShutdownMode.OnLastWindowClose; OnKernelLoaded(); InstantiateMainWindow().Show(); ShowApplicationWindow(); await ApplicationWindow.LoadInitialUI().ConfigureAwait(false); } catch (Exception ex) { Logger.ErrorException("Error launching application", ex); MessageBox.Show("There was an error launching Media Browser: " + ex.Message); // Shutdown the app with an error code Shutdown(1); } } /// /// Called when [kernel loaded]. /// /// Task. protected void OnKernelLoaded() { Kernel.ConfigurationUpdated += Kernel_ConfigurationUpdated; ConfigureClickOnceStartup(); PropertyChanged += AppPropertyChanged; // Update every 10 seconds ClockTimer = new Timer(ClockTimerCallback, null, 0, 10000); // Update every 30 minutes ServerConfigurationTimer = new Timer(ServerConfigurationTimerCallback, null, 0, 1800000); CurrentTheme = UIKernel.Instance.Themes.First(); foreach (var resource in CurrentTheme.GetGlobalResources()) { Resources.MergedDictionaries.Add(resource); } } /// /// Raises the event. /// /// A that contains the event data. protected override void OnStartup(StartupEventArgs e) { bool createdNew; SingleInstanceMutex = new Mutex(true, @"Local\" + GetType().Assembly.GetName().Name, out createdNew); if (!createdNew) { SingleInstanceMutex = null; Shutdown(); return; } AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException; LoadKernel(); SystemEvents.SessionEnding += SystemEvents_SessionEnding; } /// /// Handles the UnhandledException event of the CurrentDomain control. /// /// The source of the event. /// The instance containing the event data. void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e) { var exception = (Exception)e.ExceptionObject; Logger.ErrorException("UnhandledException", exception); MessageBox.Show("Unhandled exception: " + exception.Message); } /// /// Called when [property changed]. /// /// The info. public void OnPropertyChanged(String info) { if (PropertyChanged != null) { try { PropertyChanged(this, new PropertyChangedEventArgs(info)); } catch (Exception ex) { Logger.ErrorException("Error in event handler", ex); } } } /// /// Handles the SessionEnding event of the SystemEvents control. /// /// The source of the event. /// The instance containing the event data. void SystemEvents_SessionEnding(object sender, SessionEndingEventArgs e) { // Try to shut down gracefully Shutdown(); } /// /// Raises the event. /// /// An that contains the event data. protected override void OnExit(ExitEventArgs e) { var win = ApplicationWindow; if (win != null) { // Save window position var config = UIKernel.Instance.Configuration; config.WindowState = win.WindowState; config.WindowTop = win.Top; config.WindowLeft = win.Left; config.WindowWidth = win.Width; config.WindowHeight = win.Height; UIKernel.Instance.SaveConfiguration(); } ReleaseMutex(); base.OnExit(e); Kernel.Dispose(); } /// /// Releases the mutex. /// private void ReleaseMutex() { if (SingleInstanceMutex == null) { return; } SingleInstanceMutex.ReleaseMutex(); SingleInstanceMutex.Close(); SingleInstanceMutex.Dispose(); SingleInstanceMutex = null; } /// /// Apps the property changed. /// /// The sender. /// The instance containing the event data. async void AppPropertyChanged(object sender, PropertyChangedEventArgs e) { if (e.PropertyName.Equals("ServerConfiguration")) { if (string.IsNullOrEmpty(ServerConfiguration.WeatherLocation)) { CurrentWeather = null; } else { try { CurrentWeather = await ApiClient.GetWeatherInfoAsync(ServerConfiguration.WeatherLocation); } catch (HttpException ex) { Logger.ErrorException("Error downloading weather information", ex); } } } } /// /// Clocks the timer callback. /// /// The state info. private void ClockTimerCallback(object stateInfo) { CurrentTime = DateTime.Now; } /// /// Servers the configuration timer callback. /// /// The state info. private async void ServerConfigurationTimerCallback(object stateInfo) { try { ServerConfiguration = await ApiClient.GetServerConfigurationAsync(); } catch (HttpException ex) { Logger.ErrorException("Error refreshing server configuration", ex); } } /// /// Logouts the user. /// /// Task. public async Task LogoutUser() { CurrentUser = null; await Dispatcher.InvokeAsync(() => Navigate(CurrentTheme.GetLoginPage())); } /// /// Navigates the specified page. /// /// The page. public void Navigate(Page page) { _remoteImageCache = new FileSystemRepository(UIKernel.Instance.ApplicationPaths.RemoteImageCachePath); ApplicationWindow.Navigate(page); } /// /// Navigates to settings page. /// public void NavigateToSettingsPage() { Navigate(new SettingsPage()); } /// /// Navigates to internal player page. /// public void NavigateToInternalPlayerPage() { Navigate(CurrentTheme.GetInternalPlayerPage()); } /// /// Navigates to image viewer. /// /// The image URL. /// The caption. public void OpenImageViewer(Uri imageUrl, string caption) { var tuple = new Tuple(imageUrl, caption); OpenImageViewer(new[] { tuple }); } /// /// Navigates to image viewer. /// /// The images. public void OpenImageViewer(IEnumerable> images) { new ImageViewerWindow(images).ShowModal(ApplicationWindow); } /// /// Navigates to item. /// /// The item. public void NavigateToItem(BaseItemDto item) { if (item.IsRoot.HasValue && item.IsRoot.Value) { NavigateToHomePage(); } else if (item.IsFolder) { Navigate(CurrentTheme.GetListPage(item)); } else { Navigate(CurrentTheme.GetDetailPage(item)); } } /// /// Displays the weather. /// public void DisplayWeather() { CurrentTheme.DisplayWeather(); } /// /// Navigates to home page. /// public void NavigateToHomePage() { Navigate(CurrentTheme.GetHomePage()); } /// /// Shows a notification message that will disappear on it's own /// /// The text. /// The caption. /// The icon. public void ShowNotificationMessage(string text, string caption = null, MessageBoxIcon icon = MessageBoxIcon.None) { ApplicationWindow.ShowModalMessage(text, caption: caption, icon: icon); } /// /// Shows a notification message that will disappear on it's own /// /// The text. /// The caption. /// The icon. public void ShowNotificationMessage(UIElement text, string caption = null, MessageBoxIcon icon = MessageBoxIcon.None) { ApplicationWindow.ShowModalMessage(text, caption: caption, icon: icon); } /// /// Shows a modal message box and asynchronously returns a MessageBoxResult /// /// The text. /// The caption. /// The button. /// The icon. /// MessageBoxResult. public MessageBoxResult ShowModalMessage(string text, string caption = null, MessageBoxButton button = MessageBoxButton.OK, MessageBoxIcon icon = MessageBoxIcon.None) { return ApplicationWindow.ShowModalMessage(text, caption: caption, button: button, icon: icon); } /// /// Shows a modal message box and asynchronously returns a MessageBoxResult /// /// The text. /// The caption. /// The button. /// The icon. /// MessageBoxResult. public MessageBoxResult ShowModalMessage(UIElement text, string caption = null, MessageBoxButton button = MessageBoxButton.OK, MessageBoxIcon icon = MessageBoxIcon.None) { return ApplicationWindow.ShowModalMessage(text, caption: caption, button: button, icon: icon); } /// /// Shows the error message. /// /// The message. /// The caption. public void ShowErrorMessage(string message, string caption = null) { caption = caption ?? "Error"; ShowModalMessage(message, caption: caption, button: MessageBoxButton.OK, icon: MessageBoxIcon.Error); } /// /// Shows the default error message. /// public void ShowDefaultErrorMessage() { ShowErrorMessage("There was an error processing the request", "Error"); } /// /// The _remote image cache /// private FileSystemRepository _remoteImageCache; /// /// Gets the remote image async. /// /// The URL. /// Task{Image}. public async Task GetRemoteImageAsync(string url) { var bitmap = await GetRemoteBitmapAsync(url); return new Image { Source = bitmap }; } /// /// Gets the remote image async. /// /// The URL. /// Task{BitmapImage}. /// url public Task GetRemoteBitmapAsync(string url) { if (string.IsNullOrEmpty(url)) { throw new ArgumentNullException("url"); } Logger.Info("Image url: " + url); return Task.Run(async () => { var cachePath = _remoteImageCache.GetResourcePath(url.GetMD5().ToString()); await _remoteImageCache.WaitForLockAsync(cachePath).ConfigureAwait(false); var releaseLock = true; try { using (var stream = new FileStream(cachePath, FileMode.Open, FileAccess.Read, FileShare.Read, StreamDefaults.DefaultFileStreamBufferSize, true)) { return await GetRemoteBitmapAsync(stream).ConfigureAwait(false); } } catch (FileNotFoundException) { // Doesn't exist. No biggie releaseLock = false; } finally { if (releaseLock) { _remoteImageCache.ReleaseLock(cachePath); } } try { using (var httpStream = await UIKernel.Instance.ApiClient.GetImageStreamAsync(url + "&x=1")) { return await GetRemoteBitmapAsync(httpStream, cachePath); } } finally { _remoteImageCache.ReleaseLock(cachePath); } }); } /// /// Gets the image async. /// /// The source stream. /// The cache path. /// Task{BitmapImage}. private async Task GetRemoteBitmapAsync(Stream sourceStream, string cachePath = null) { byte[] bytes; using (var ms = new MemoryStream()) { await sourceStream.CopyToAsync(ms).ConfigureAwait(false); bytes = ms.ToArray(); } if (!string.IsNullOrEmpty(cachePath)) { using (var fileStream = new FileStream(cachePath, FileMode.Create, FileAccess.Write, FileShare.Read, StreamDefaults.DefaultFileStreamBufferSize, true)) { await fileStream.WriteAsync(bytes, 0, bytes.Length).ConfigureAwait(false); } } using (Stream stream = new MemoryStream(bytes)) { var bitmapImage = new BitmapImage { CreateOptions = BitmapCreateOptions.DelayCreation }; bitmapImage.BeginInit(); bitmapImage.CacheOption = BitmapCacheOption.OnLoad; bitmapImage.StreamSource = stream; bitmapImage.EndInit(); bitmapImage.Freeze(); return bitmapImage; } } /// /// Handles the ConfigurationUpdated event of the Kernel control. /// /// The source of the event. /// The instance containing the event data. void Kernel_ConfigurationUpdated(object sender, EventArgs e) { if (!LastRunAtStartupValue.HasValue || LastRunAtStartupValue.Value != Kernel.Configuration.RunAtStartup) { ConfigureClickOnceStartup(); } } /// /// Configures the click once startup. /// private void ConfigureClickOnceStartup() { try { ClickOnceHelper.ConfigureClickOnceStartupIfInstalled(PublisherName, ProductName, SuiteName, Kernel.Configuration.RunAtStartup, UninstallerFileName); LastRunAtStartupValue = Kernel.Configuration.RunAtStartup; } catch (Exception ex) { Logger.ErrorException("Error configuring ClickOnce", ex); } } public void Restart() { Dispatcher.Invoke(ReleaseMutex); Kernel.Dispose(); System.Windows.Forms.Application.Restart(); Dispatcher.Invoke(Shutdown); } public void ReloadLogger() { LogFilePath = Path.Combine(Kernel.ApplicationPaths.LogDirectoryPath, "Server-" + DateTime.Now.Ticks + ".log"); NlogManager.AddFileTarget(LogFilePath, Kernel.Configuration.EnableDebugLevelLogging); } /// /// Gets the bitmap image. /// /// The URI. /// BitmapImage. /// uri public BitmapImage GetBitmapImage(string uri) { if (string.IsNullOrEmpty(uri)) { throw new ArgumentNullException("uri"); } return GetBitmapImage(new Uri(uri)); } /// /// Gets the bitmap image. /// /// The URI. /// BitmapImage. /// uri public BitmapImage GetBitmapImage(Uri uri) { if (uri == null) { throw new ArgumentNullException("uri"); } var bitmap = new BitmapImage { CreateOptions = BitmapCreateOptions.DelayCreation, CacheOption = BitmapCacheOption.OnDemand, UriCachePolicy = new RequestCachePolicy(RequestCacheLevel.CacheIfAvailable) }; bitmap.BeginInit(); bitmap.UriSource = uri; bitmap.EndInit(); RenderOptions.SetBitmapScalingMode(bitmap, BitmapScalingMode.Fant); return bitmap; } /// /// Gets or sets a value indicating whether this instance can self update. /// /// true if this instance can self update; otherwise, false. public bool CanSelfUpdate { get { return ClickOnceHelper.IsNetworkDeployed; } } /// /// Checks for update. /// /// The cancellation token. /// The progress. /// Task{CheckForUpdateResult}. public Task CheckForApplicationUpdate(CancellationToken cancellationToken, IProgress progress) { return new ApplicationUpdateCheck().CheckForApplicationUpdate(cancellationToken, progress); } /// /// Updates the application. /// /// The cancellation token. /// The progress. /// Task. public Task UpdateApplication(CancellationToken cancellationToken, IProgress progress) { return new ApplicationUpdater().UpdateApplication(cancellationToken, progress); } } }