From a02333fb0cbda8227dd6a5662c6a880eafd5074c Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Mon, 23 Sep 2013 12:00:22 -0400 Subject: [PATCH 01/21] #429 - Extract ffmpeg from core product --- .../ApplicationHost.cs | 6 +- .../Implementations/FFMpegDownloader.cs | 55 +++++++++---------- 2 files changed, 29 insertions(+), 32 deletions(-) diff --git a/MediaBrowser.ServerApplication/ApplicationHost.cs b/MediaBrowser.ServerApplication/ApplicationHost.cs index 5cae99785..a2965d4ea 100644 --- a/MediaBrowser.ServerApplication/ApplicationHost.cs +++ b/MediaBrowser.ServerApplication/ApplicationHost.cs @@ -258,6 +258,8 @@ namespace MediaBrowser.ServerApplication ZipClient = new DotNetZipClient(); RegisterSingleInstance(ZipClient); + var mediaEncoderTask = RegisterMediaEncoder(); + UserDataRepository = new SqliteUserDataRepository(ApplicationPaths, JsonSerializer, LogManager); RegisterSingleInstance(UserDataRepository); @@ -284,8 +286,6 @@ namespace MediaBrowser.ServerApplication RegisterSingleInstance(() => new LuceneSearchEngine(ApplicationPaths, LogManager, LibraryManager)); - await RegisterMediaEncoder().ConfigureAwait(false); - var clientConnectionManager = new SessionManager(UserDataRepository, ServerConfigurationManager, Logger, UserRepository); RegisterSingleInstance(clientConnectionManager); @@ -310,7 +310,7 @@ namespace MediaBrowser.ServerApplication await ConfigureNotificationsRepository().ConfigureAwait(false); - await Task.WhenAll(itemsTask, displayPreferencesTask, userdataTask).ConfigureAwait(false); + await Task.WhenAll(itemsTask, displayPreferencesTask, userdataTask, mediaEncoderTask).ConfigureAwait(false); SetKernelProperties(); } diff --git a/MediaBrowser.ServerApplication/Implementations/FFMpegDownloader.cs b/MediaBrowser.ServerApplication/Implementations/FFMpegDownloader.cs index 7fd0acddd..861ca7f3b 100644 --- a/MediaBrowser.ServerApplication/Implementations/FFMpegDownloader.cs +++ b/MediaBrowser.ServerApplication/Implementations/FFMpegDownloader.cs @@ -29,33 +29,37 @@ namespace MediaBrowser.ServerApplication.Implementations public async Task GetFFMpegInfo() { - var assembly = GetType().Assembly; + var version = "ffmpeg20130904"; - var prefix = GetType().Namespace + "."; + var versionedDirectoryPath = Path.Combine(GetMediaToolsPath(true), version); - var srch = prefix + "ffmpeg"; - - var resource = assembly.GetManifestResourceNames().First(r => r.StartsWith(srch)); - - var filename = - resource.Substring(resource.IndexOf(prefix, StringComparison.OrdinalIgnoreCase) + prefix.Length); - - var versionedDirectoryPath = Path.Combine(GetMediaToolsPath(true), - Path.GetFileNameWithoutExtension(filename)); + var info = new FFMpegInfo + { + ProbePath = Path.Combine(versionedDirectoryPath, "ffprobe.exe"), + Path = Path.Combine(versionedDirectoryPath, "ffmpeg.exe"), + Version = version + }; if (!Directory.Exists(versionedDirectoryPath)) { Directory.CreateDirectory(versionedDirectoryPath); } - await ExtractTools(assembly, resource, versionedDirectoryPath).ConfigureAwait(false); - - return new FFMpegInfo + if (!File.Exists(info.ProbePath) || !File.Exists(info.Path)) { - ProbePath = Path.Combine(versionedDirectoryPath, "ffprobe.exe"), - Path = Path.Combine(versionedDirectoryPath, "ffmpeg.exe"), - Version = Path.GetFileNameWithoutExtension(versionedDirectoryPath) - }; + ExtractTools(version, versionedDirectoryPath); + } + + try + { + await DownloadFonts(versionedDirectoryPath).ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.ErrorException("Error getting ffmpeg font files", ex); + } + + return info; } /// @@ -64,21 +68,14 @@ namespace MediaBrowser.ServerApplication.Implementations /// The assembly. /// The zip file resource path. /// The target path. - private async Task ExtractTools(Assembly assembly, string zipFileResourcePath, string targetPath) + private void ExtractTools(string version, string targetPath) { - using (var resourceStream = assembly.GetManifestResourceStream(zipFileResourcePath)) + var zipFileResourcePath = GetType().Namespace + "." + version + ".zip"; + + using (var resourceStream = GetType().Assembly.GetManifestResourceStream(zipFileResourcePath)) { _zipClient.ExtractAll(resourceStream, targetPath, false); } - - try - { - await DownloadFonts(targetPath).ConfigureAwait(false); - } - catch (Exception ex) - { - _logger.ErrorException("Error getting ffmpeg font files", ex); - } } private const string FontUrl = "https://www.dropbox.com/s/9nb76tybcsw5xrk/ARIALUNI.zip?dl=1"; From baa5be2157f70095be319bcfe4a0f5a1cbdd5d1c Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Mon, 23 Sep 2013 12:00:36 -0400 Subject: [PATCH 02/21] rename images to force cache bust --- MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj b/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj index 06f930238..1fbc01952 100644 --- a/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj +++ b/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj @@ -255,10 +255,10 @@ PreserveNewest - + PreserveNewest - + PreserveNewest From c905051b163946ab70bae94468d071fdb5925fae Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Mon, 23 Sep 2013 12:48:35 -0400 Subject: [PATCH 03/21] switch to sharp compress to add 7z support --- .../Implementations/DotNetZipClient.cs | 21 ++++++++++++------- .../Implementations/FFMpegDownloader.cs | 1 - .../MediaBrowser.ServerApplication.csproj | 3 +++ .../packages.config | 1 + 4 files changed, 17 insertions(+), 9 deletions(-) diff --git a/MediaBrowser.ServerApplication/Implementations/DotNetZipClient.cs b/MediaBrowser.ServerApplication/Implementations/DotNetZipClient.cs index 3b174a9b2..4a9afac3f 100644 --- a/MediaBrowser.ServerApplication/Implementations/DotNetZipClient.cs +++ b/MediaBrowser.ServerApplication/Implementations/DotNetZipClient.cs @@ -1,5 +1,6 @@ -using Ionic.Zip; -using MediaBrowser.Model.IO; +using MediaBrowser.Model.IO; +using SharpCompress.Common; +using SharpCompress.Reader; using System.IO; namespace MediaBrowser.ServerApplication.Implementations @@ -19,10 +20,7 @@ namespace MediaBrowser.ServerApplication.Implementations { using (var fileStream = File.OpenRead(sourceFile)) { - using (var zipFile = ZipFile.Read(fileStream)) - { - zipFile.ExtractAll(targetPath, overwriteExistingFiles ? ExtractExistingFileAction.OverwriteSilently : ExtractExistingFileAction.DoNotOverwrite); - } + ExtractAll(fileStream, targetPath, overwriteExistingFiles); } } @@ -34,9 +32,16 @@ namespace MediaBrowser.ServerApplication.Implementations /// if set to true [overwrite existing files]. public void ExtractAll(Stream source, string targetPath, bool overwriteExistingFiles) { - using (var zipFile = ZipFile.Read(source)) + using (var reader = ReaderFactory.Open(source)) { - zipFile.ExtractAll(targetPath, overwriteExistingFiles ? ExtractExistingFileAction.OverwriteSilently : ExtractExistingFileAction.DoNotOverwrite); + var options = ExtractOptions.ExtractFullPath; + + if (overwriteExistingFiles) + { + options = options | ExtractOptions.Overwrite; + } + + reader.WriteAllToDirectory(targetPath, options); } } } diff --git a/MediaBrowser.ServerApplication/Implementations/FFMpegDownloader.cs b/MediaBrowser.ServerApplication/Implementations/FFMpegDownloader.cs index 861ca7f3b..becb8d8ab 100644 --- a/MediaBrowser.ServerApplication/Implementations/FFMpegDownloader.cs +++ b/MediaBrowser.ServerApplication/Implementations/FFMpegDownloader.cs @@ -6,7 +6,6 @@ using MediaBrowser.Model.Logging; using System; using System.IO; using System.Linq; -using System.Reflection; using System.Text; using System.Threading.Tasks; diff --git a/MediaBrowser.ServerApplication/MediaBrowser.ServerApplication.csproj b/MediaBrowser.ServerApplication/MediaBrowser.ServerApplication.csproj index 043d5c18f..965e9f873 100644 --- a/MediaBrowser.ServerApplication/MediaBrowser.ServerApplication.csproj +++ b/MediaBrowser.ServerApplication/MediaBrowser.ServerApplication.csproj @@ -168,6 +168,9 @@ False ..\packages\ServiceStack.Text.3.9.62\lib\net35\ServiceStack.Text.dll + + ..\packages\sharpcompress.0.10.1.3\lib\net40\SharpCompress.dll + False ..\packages\SimpleInjector.2.3.5\lib\net40-client\SimpleInjector.dll diff --git a/MediaBrowser.ServerApplication/packages.config b/MediaBrowser.ServerApplication/packages.config index 8c1821ca5..137483ef1 100644 --- a/MediaBrowser.ServerApplication/packages.config +++ b/MediaBrowser.ServerApplication/packages.config @@ -11,6 +11,7 @@ + \ No newline at end of file From db0264e80fa84aba2263f22b799e5a9f4359b490 Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Mon, 23 Sep 2013 13:14:17 -0400 Subject: [PATCH 04/21] fixes #429 - Extract ffmpeg from core product --- .../ApplicationHost.cs | 2 +- .../Implementations/FFMpegDownloader.cs | 119 ++++++++++++++---- 2 files changed, 98 insertions(+), 23 deletions(-) diff --git a/MediaBrowser.ServerApplication/ApplicationHost.cs b/MediaBrowser.ServerApplication/ApplicationHost.cs index a2965d4ea..eeea9392c 100644 --- a/MediaBrowser.ServerApplication/ApplicationHost.cs +++ b/MediaBrowser.ServerApplication/ApplicationHost.cs @@ -321,7 +321,7 @@ namespace MediaBrowser.ServerApplication /// Task. private async Task RegisterMediaEncoder() { - var info = await new FFMpegDownloader(Logger, ApplicationPaths, HttpClient, ZipClient).GetFFMpegInfo().ConfigureAwait(false); + var info = await new FFMpegDownloader(Logger, ApplicationPaths, HttpClient).GetFFMpegInfo().ConfigureAwait(false); MediaEncoder = new MediaEncoder(LogManager.GetLogger("MediaEncoder"), ApplicationPaths, JsonSerializer, info.Path, info.ProbePath, info.Version); RegisterSingleInstance(MediaEncoder); diff --git a/MediaBrowser.ServerApplication/Implementations/FFMpegDownloader.cs b/MediaBrowser.ServerApplication/Implementations/FFMpegDownloader.cs index becb8d8ab..ef33518cd 100644 --- a/MediaBrowser.ServerApplication/Implementations/FFMpegDownloader.cs +++ b/MediaBrowser.ServerApplication/Implementations/FFMpegDownloader.cs @@ -1,42 +1,52 @@ using MediaBrowser.Common.Configuration; using MediaBrowser.Common.IO; using MediaBrowser.Common.Net; -using MediaBrowser.Model.IO; using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Net; +using SharpCompress.Archive.SevenZip; +using SharpCompress.Common; +using SharpCompress.Reader; using System; using System.IO; using System.Linq; using System.Text; +using System.Threading; using System.Threading.Tasks; namespace MediaBrowser.ServerApplication.Implementations { public class FFMpegDownloader { - private readonly IZipClient _zipClient; private readonly IHttpClient _httpClient; private readonly IApplicationPaths _appPaths; private readonly ILogger _logger; - public FFMpegDownloader(ILogger logger, IApplicationPaths appPaths, IHttpClient httpClient, IZipClient zipClient) + private const string Version = "ffmpeg20130904"; + + private const string FontUrl = "https://www.dropbox.com/s/pj847twf7riq0j7/ARIALUNI.7z?dl=1"; + + private readonly string[] _ffMpegUrls = new[] + { + "http://ffmpeg.zeranoe.com/builds/win32/static/ffmpeg-20130904-git-f974289-win32-static.7z", + "https://www.dropbox.com/s/a81cb2ob23fwcfs/ffmpeg-20130904-git-f974289-win32-static.7z?dl=1" + }; + + public FFMpegDownloader(ILogger logger, IApplicationPaths appPaths, IHttpClient httpClient) { _logger = logger; _appPaths = appPaths; _httpClient = httpClient; - _zipClient = zipClient; } public async Task GetFFMpegInfo() { - var version = "ffmpeg20130904"; - - var versionedDirectoryPath = Path.Combine(GetMediaToolsPath(true), version); + var versionedDirectoryPath = Path.Combine(GetMediaToolsPath(true), Version); var info = new FFMpegInfo { ProbePath = Path.Combine(versionedDirectoryPath, "ffprobe.exe"), Path = Path.Combine(versionedDirectoryPath, "ffmpeg.exe"), - Version = version + Version = Version }; if (!Directory.Exists(versionedDirectoryPath)) @@ -46,7 +56,7 @@ namespace MediaBrowser.ServerApplication.Implementations if (!File.Exists(info.ProbePath) || !File.Exists(info.Path)) { - ExtractTools(version, versionedDirectoryPath); + await DownloadFFMpeg(info).ConfigureAwait(false); } try @@ -61,23 +71,88 @@ namespace MediaBrowser.ServerApplication.Implementations return info; } - /// - /// Extracts the tools. - /// - /// The assembly. - /// The zip file resource path. - /// The target path. - private void ExtractTools(string version, string targetPath) + private async Task DownloadFFMpeg(FFMpegInfo info) { - var zipFileResourcePath = GetType().Namespace + "." + version + ".zip"; - - using (var resourceStream = GetType().Assembly.GetManifestResourceStream(zipFileResourcePath)) + foreach (var url in _ffMpegUrls) { - _zipClient.ExtractAll(resourceStream, targetPath, false); + try + { + var tempFile = await DownloadFFMpeg(info, url).ConfigureAwait(false); + + ExtractFFMpeg(tempFile, Path.GetDirectoryName(info.Path)); + return; + } + catch (HttpException ex) + { + + } } } - private const string FontUrl = "https://www.dropbox.com/s/9nb76tybcsw5xrk/ARIALUNI.zip?dl=1"; + private Task DownloadFFMpeg(FFMpegInfo info, string url) + { + return _httpClient.GetTempFile(new HttpRequestOptions + { + Url = url, + CancellationToken = CancellationToken.None, + Progress = new Progress(), + + // Make it look like a browser + // Try to hide that we're direct linking + UserAgent = "Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/30.0.1599.47 Safari/537.36" + }); + } + + private void ExtractFFMpeg(string tempFile, string targetFolder) + { + _logger.Debug("Extracting ffmpeg from {0}", tempFile); + + var tempFolder = Path.Combine(_appPaths.TempDirectory, Guid.NewGuid().ToString()); + + if (!Directory.Exists(tempFolder)) + { + Directory.CreateDirectory(tempFolder); + } + + try + { + Extract7zArchive(tempFile, tempFolder); + + var files = Directory.EnumerateFiles(tempFolder, "*.exe", SearchOption.AllDirectories).ToList(); + + foreach (var file in files) + { + File.Copy(file, Path.Combine(targetFolder, Path.GetFileName(file))); + } + } + finally + { + DeleteFile(tempFile); + } + } + + private void Extract7zArchive(string archivePath, string targetPath) + { + using (var archive = SevenZipArchive.Open(archivePath)) + { + using (var reader = archive.ExtractAllEntries()) + { + reader.WriteAllToDirectory(targetPath, ExtractOptions.ExtractFullPath | ExtractOptions.Overwrite); + } + } + } + + private void DeleteFile(string path) + { + try + { + File.Delete(path); + } + catch (IOException ex) + { + _logger.ErrorException("Error deleting temp file {0}", ex, path); + } + } /// /// Extracts the fonts. @@ -136,7 +211,7 @@ namespace MediaBrowser.ServerApplication.Implementations Progress = new Progress() }); - _zipClient.ExtractAll(tempFile, fontsDirectory, true); + Extract7zArchive(tempFile, fontsDirectory); try { From 1c2be7ba67d2cb5b98155e839983a6666f92961d Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Mon, 23 Sep 2013 13:16:23 -0400 Subject: [PATCH 05/21] fixes #429 - extract ffmpeg --- .../Implementations/ffmpeg20130904.zip.REMOVED.git-id | 1 - MediaBrowser.ServerApplication/Implementations/readme.txt | 5 ----- .../MediaBrowser.ServerApplication.csproj | 4 ---- 3 files changed, 10 deletions(-) delete mode 100644 MediaBrowser.ServerApplication/Implementations/ffmpeg20130904.zip.REMOVED.git-id delete mode 100644 MediaBrowser.ServerApplication/Implementations/readme.txt diff --git a/MediaBrowser.ServerApplication/Implementations/ffmpeg20130904.zip.REMOVED.git-id b/MediaBrowser.ServerApplication/Implementations/ffmpeg20130904.zip.REMOVED.git-id deleted file mode 100644 index e99d115a4..000000000 --- a/MediaBrowser.ServerApplication/Implementations/ffmpeg20130904.zip.REMOVED.git-id +++ /dev/null @@ -1 +0,0 @@ -3496b2cde22e7c4cb56b480dd2da637167d51e78 \ No newline at end of file diff --git a/MediaBrowser.ServerApplication/Implementations/readme.txt b/MediaBrowser.ServerApplication/Implementations/readme.txt deleted file mode 100644 index b32dd9aec..000000000 --- a/MediaBrowser.ServerApplication/Implementations/readme.txt +++ /dev/null @@ -1,5 +0,0 @@ -This is the 32-bit static build of ffmpeg, located at: - -http://ffmpeg.zeranoe.com/builds/ - -The zip file contains both ffmpeg and ffprobe, and is suffixed with the date of the build. \ No newline at end of file diff --git a/MediaBrowser.ServerApplication/MediaBrowser.ServerApplication.csproj b/MediaBrowser.ServerApplication/MediaBrowser.ServerApplication.csproj index 965e9f873..d76fa25f4 100644 --- a/MediaBrowser.ServerApplication/MediaBrowser.ServerApplication.csproj +++ b/MediaBrowser.ServerApplication/MediaBrowser.ServerApplication.csproj @@ -281,7 +281,6 @@ Resources.Designer.cs - SettingsSingleFileGenerator @@ -393,9 +392,6 @@ - - - if $(ConfigurationName) == Release ( From 1dbce6bff23b406d3d14c9ceb2af4dc42e5740c4 Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Mon, 23 Sep 2013 13:27:38 -0400 Subject: [PATCH 06/21] perform downloads in parallel --- .../Implementations/FFMpegDownloader.cs | 57 ++++++++++++------- 1 file changed, 35 insertions(+), 22 deletions(-) diff --git a/MediaBrowser.ServerApplication/Implementations/FFMpegDownloader.cs b/MediaBrowser.ServerApplication/Implementations/FFMpegDownloader.cs index ef33518cd..75c47c10d 100644 --- a/MediaBrowser.ServerApplication/Implementations/FFMpegDownloader.cs +++ b/MediaBrowser.ServerApplication/Implementations/FFMpegDownloader.cs @@ -7,6 +7,7 @@ using SharpCompress.Archive.SevenZip; using SharpCompress.Common; using SharpCompress.Reader; using System; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; @@ -54,19 +55,16 @@ namespace MediaBrowser.ServerApplication.Implementations Directory.CreateDirectory(versionedDirectoryPath); } + var tasks = new List(); + if (!File.Exists(info.ProbePath) || !File.Exists(info.Path)) { - await DownloadFFMpeg(info).ConfigureAwait(false); + tasks.Add(DownloadFFMpeg(info)); } - try - { - await DownloadFonts(versionedDirectoryPath).ConfigureAwait(false); - } - catch (Exception ex) - { - _logger.ErrorException("Error getting ffmpeg font files", ex); - } + tasks.Add(DownloadFonts(versionedDirectoryPath)); + + await Task.WhenAll(tasks).ConfigureAwait(false); return info; } @@ -87,6 +85,8 @@ namespace MediaBrowser.ServerApplication.Implementations } } + + throw new ApplicationException("Unable to download required components. Please try again later."); } private Task DownloadFFMpeg(FFMpegInfo info, string url) @@ -160,23 +160,36 @@ namespace MediaBrowser.ServerApplication.Implementations /// The target path. private async Task DownloadFonts(string targetPath) { - var fontsDirectory = Path.Combine(targetPath, "fonts"); - - if (!Directory.Exists(fontsDirectory)) + try { - Directory.CreateDirectory(fontsDirectory); + var fontsDirectory = Path.Combine(targetPath, "fonts"); + + if (!Directory.Exists(fontsDirectory)) + { + Directory.CreateDirectory(fontsDirectory); + } + + const string fontFilename = "ARIALUNI.TTF"; + + var fontFile = Path.Combine(fontsDirectory, fontFilename); + + if (!File.Exists(fontFile)) + { + await DownloadFontFile(fontsDirectory, fontFilename).ConfigureAwait(false); + } + + await WriteFontConfigFile(fontsDirectory).ConfigureAwait(false); } - - const string fontFilename = "ARIALUNI.TTF"; - - var fontFile = Path.Combine(fontsDirectory, fontFilename); - - if (!File.Exists(fontFile)) + catch (HttpException ex) { - await DownloadFontFile(fontsDirectory, fontFilename).ConfigureAwait(false); + // Don't let the server crash because of this + _logger.ErrorException("Error downloading ffmpeg font files", ex); + } + catch (Exception ex) + { + // Don't let the server crash because of this + _logger.ErrorException("Error writing ffmpeg font files", ex); } - - await WriteFontConfigFile(fontsDirectory).ConfigureAwait(false); } /// From 0fd0c9bd4e9cbb1c7501b851e1765f546fb8cb0e Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Mon, 23 Sep 2013 14:05:07 -0400 Subject: [PATCH 07/21] add ffmpeg files to project --- .../ffmpeg-20130904-git-f974289-win32-static.7z.REMOVED.git-id | 1 + .../MediaBrowser.ServerApplication.csproj | 2 ++ 2 files changed, 3 insertions(+) create mode 100644 MediaBrowser.ServerApplication/Implementations/ffmpeg-20130904-git-f974289-win32-static.7z.REMOVED.git-id diff --git a/MediaBrowser.ServerApplication/Implementations/ffmpeg-20130904-git-f974289-win32-static.7z.REMOVED.git-id b/MediaBrowser.ServerApplication/Implementations/ffmpeg-20130904-git-f974289-win32-static.7z.REMOVED.git-id new file mode 100644 index 000000000..9f83b949b --- /dev/null +++ b/MediaBrowser.ServerApplication/Implementations/ffmpeg-20130904-git-f974289-win32-static.7z.REMOVED.git-id @@ -0,0 +1 @@ +8f1dfd62d31e48c31bef4b9ccc0e514f46650a79 \ No newline at end of file diff --git a/MediaBrowser.ServerApplication/MediaBrowser.ServerApplication.csproj b/MediaBrowser.ServerApplication/MediaBrowser.ServerApplication.csproj index d76fa25f4..cc799f6ba 100644 --- a/MediaBrowser.ServerApplication/MediaBrowser.ServerApplication.csproj +++ b/MediaBrowser.ServerApplication/MediaBrowser.ServerApplication.csproj @@ -281,6 +281,8 @@ Resources.Designer.cs + + SettingsSingleFileGenerator From 4d3bef9f4f04fc8e05e0c0e8160954faa714a43f Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Mon, 23 Sep 2013 14:14:07 -0400 Subject: [PATCH 08/21] use github raw hosting --- .../Implementations/FFMpegDownloader.cs | 36 ++++++++++++++++--- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/MediaBrowser.ServerApplication/Implementations/FFMpegDownloader.cs b/MediaBrowser.ServerApplication/Implementations/FFMpegDownloader.cs index 75c47c10d..1bc0b90f0 100644 --- a/MediaBrowser.ServerApplication/Implementations/FFMpegDownloader.cs +++ b/MediaBrowser.ServerApplication/Implementations/FFMpegDownloader.cs @@ -24,10 +24,15 @@ namespace MediaBrowser.ServerApplication.Implementations private const string Version = "ffmpeg20130904"; - private const string FontUrl = "https://www.dropbox.com/s/pj847twf7riq0j7/ARIALUNI.7z?dl=1"; + private readonly string[] _fontUrls = new[] + { + "https://www.dropbox.com/s/pj847twf7riq0j7/ARIALUNI.7z?dl=1" + }; private readonly string[] _ffMpegUrls = new[] { + "https://raw.github.com/MediaBrowser/MediaBrowser/master/MediaBrowser.ServerApplication/Implementations/ffmpeg-20130904-git-f974289-win32-static.7z", + "http://ffmpeg.zeranoe.com/builds/win32/static/ffmpeg-20130904-git-f974289-win32-static.7z", "https://www.dropbox.com/s/a81cb2ob23fwcfs/ffmpeg-20130904-git-f974289-win32-static.7z?dl=1" }; @@ -218,11 +223,32 @@ namespace MediaBrowser.ServerApplication.Implementations } } - var tempFile = await _httpClient.GetTempFile(new HttpRequestOptions + string tempFile = null; + + foreach (var url in _fontUrls) { - Url = FontUrl, - Progress = new Progress() - }); + try + { + tempFile = await _httpClient.GetTempFile(new HttpRequestOptions + { + Url = url, + Progress = new Progress() + + }).ConfigureAwait(false); + + break; + } + catch (Exception ex) + { + // The core can function without the font file, so handle this + _logger.ErrorException("Failed to download ffmpeg font file from {0}", ex, url); + } + } + + if (string.IsNullOrEmpty(tempFile)) + { + return; + } Extract7zArchive(tempFile, fontsDirectory); From d43fd8c51dcd801959ed0581598efe2cd9314a9f Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Mon, 23 Sep 2013 14:28:07 -0400 Subject: [PATCH 09/21] updated nuget --- MediaBrowser.Controller/Entities/Folder.cs | 24 ++++++++++++++++++++++ Nuget/MediaBrowser.Common.Internal.nuspec | 4 ++-- Nuget/MediaBrowser.Common.nuspec | 2 +- Nuget/MediaBrowser.Server.Core.nuspec | 4 ++-- 4 files changed, 29 insertions(+), 5 deletions(-) diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs index 0d91a2e86..326d30bd7 100644 --- a/MediaBrowser.Controller/Entities/Folder.cs +++ b/MediaBrowser.Controller/Entities/Folder.cs @@ -171,6 +171,25 @@ namespace MediaBrowser.Controller.Entities return ItemRepository.SaveChildren(Id, ActualChildren.Select(i => i.Id).ToList(), cancellationToken); } + /// + /// Clears the children. + /// + /// The cancellation token. + /// Task. + public Task ClearChildren(CancellationToken cancellationToken) + { + var items = ActualChildren.ToList(); + + ClearChildrenInternal(); + + foreach (var item in items) + { + LibraryManager.ReportItemRemoved(item); + } + + return ItemRepository.SaveChildren(Id, ActualChildren.Select(i => i.Id).ToList(), cancellationToken); + } + #region Indexing /// @@ -733,6 +752,11 @@ namespace MediaBrowser.Controller.Entities if (actualRemovals.Count > 0) { RemoveChildrenInternal(actualRemovals); + + foreach (var item in actualRemovals) + { + LibraryManager.ReportItemRemoved(item); + } } await LibraryManager.CreateItems(newItems, cancellationToken).ConfigureAwait(false); diff --git a/Nuget/MediaBrowser.Common.Internal.nuspec b/Nuget/MediaBrowser.Common.Internal.nuspec index 1d104c45b..d3fd7a35e 100644 --- a/Nuget/MediaBrowser.Common.Internal.nuspec +++ b/Nuget/MediaBrowser.Common.Internal.nuspec @@ -2,7 +2,7 @@ MediaBrowser.Common.Internal - 3.0.206 + 3.0.207 MediaBrowser.Common.Internal Luke ebr,Luke,scottisafool @@ -12,7 +12,7 @@ Contains common components shared by Media Browser Theater and Media Browser Server. Not intended for plugin developer consumption. Copyright © Media Browser 2013 - + diff --git a/Nuget/MediaBrowser.Common.nuspec b/Nuget/MediaBrowser.Common.nuspec index 0670bcacf..5cddfbc76 100644 --- a/Nuget/MediaBrowser.Common.nuspec +++ b/Nuget/MediaBrowser.Common.nuspec @@ -2,7 +2,7 @@ MediaBrowser.Common - 3.0.206 + 3.0.207 MediaBrowser.Common Media Browser Team ebr,Luke,scottisafool diff --git a/Nuget/MediaBrowser.Server.Core.nuspec b/Nuget/MediaBrowser.Server.Core.nuspec index f5aa2d460..0c738ce93 100644 --- a/Nuget/MediaBrowser.Server.Core.nuspec +++ b/Nuget/MediaBrowser.Server.Core.nuspec @@ -2,7 +2,7 @@ MediaBrowser.Server.Core - 3.0.206 + 3.0.207 Media Browser.Server.Core Media Browser Team ebr,Luke,scottisafool @@ -12,7 +12,7 @@ Contains core components required to build plugins for Media Browser Server. Copyright © Media Browser 2013 - + From 51b01d0f6d21bf31a093cf7d8f88e31aba884288 Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Mon, 23 Sep 2013 14:53:06 -0400 Subject: [PATCH 10/21] updated nuget --- .../ScheduledTasks/TaskManager.cs | 15 +++++++++++++-- .../ScheduledTasks/ITaskManager.cs | 7 +++++++ MediaBrowser.ServerApplication/ApplicationHost.cs | 2 +- .../{DotNetZipClient.cs => ZipClient.cs} | 2 +- .../MediaBrowser.ServerApplication.csproj | 2 +- Nuget/MediaBrowser.Common.Internal.nuspec | 4 ++-- Nuget/MediaBrowser.Common.nuspec | 2 +- Nuget/MediaBrowser.Server.Core.nuspec | 4 ++-- 8 files changed, 28 insertions(+), 10 deletions(-) rename MediaBrowser.ServerApplication/Implementations/{DotNetZipClient.cs => ZipClient.cs} (97%) diff --git a/MediaBrowser.Common.Implementations/ScheduledTasks/TaskManager.cs b/MediaBrowser.Common.Implementations/ScheduledTasks/TaskManager.cs index 8278c8a28..6605432fa 100644 --- a/MediaBrowser.Common.Implementations/ScheduledTasks/TaskManager.cs +++ b/MediaBrowser.Common.Implementations/ScheduledTasks/TaskManager.cs @@ -1,5 +1,4 @@ -using System.Threading.Tasks; -using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Events; using MediaBrowser.Common.ScheduledTasks; using MediaBrowser.Model.Logging; @@ -8,6 +7,7 @@ using MediaBrowser.Model.Tasks; using System; using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; namespace MediaBrowser.Common.Implementations.ScheduledTasks { @@ -77,6 +77,17 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks QueueScheduledTask(); } + /// + /// Cancels if running + /// + /// + public void CancelIfRunning() + where T : IScheduledTask + { + var task = ScheduledTasks.First(t => t.ScheduledTask.GetType() == typeof(T)); + ((ScheduledTaskWorker)task).CancelIfRunning(); + } + /// /// Queues the scheduled task. /// diff --git a/MediaBrowser.Common/ScheduledTasks/ITaskManager.cs b/MediaBrowser.Common/ScheduledTasks/ITaskManager.cs index ec0e7c1c9..394872783 100644 --- a/MediaBrowser.Common/ScheduledTasks/ITaskManager.cs +++ b/MediaBrowser.Common/ScheduledTasks/ITaskManager.cs @@ -21,6 +21,13 @@ namespace MediaBrowser.Common.ScheduledTasks void CancelIfRunningAndQueue() where T : IScheduledTask; + /// + /// Cancels if running. + /// + /// + void CancelIfRunning() + where T : IScheduledTask; + /// /// Queues the scheduled task. /// diff --git a/MediaBrowser.ServerApplication/ApplicationHost.cs b/MediaBrowser.ServerApplication/ApplicationHost.cs index eeea9392c..7a99693a6 100644 --- a/MediaBrowser.ServerApplication/ApplicationHost.cs +++ b/MediaBrowser.ServerApplication/ApplicationHost.cs @@ -255,7 +255,7 @@ namespace MediaBrowser.ServerApplication RegisterSingleInstance(() => new BdInfoExaminer()); - ZipClient = new DotNetZipClient(); + ZipClient = new ZipClient(); RegisterSingleInstance(ZipClient); var mediaEncoderTask = RegisterMediaEncoder(); diff --git a/MediaBrowser.ServerApplication/Implementations/DotNetZipClient.cs b/MediaBrowser.ServerApplication/Implementations/ZipClient.cs similarity index 97% rename from MediaBrowser.ServerApplication/Implementations/DotNetZipClient.cs rename to MediaBrowser.ServerApplication/Implementations/ZipClient.cs index 4a9afac3f..e9e8645e9 100644 --- a/MediaBrowser.ServerApplication/Implementations/DotNetZipClient.cs +++ b/MediaBrowser.ServerApplication/Implementations/ZipClient.cs @@ -8,7 +8,7 @@ namespace MediaBrowser.ServerApplication.Implementations /// /// Class DotNetZipClient /// - public class DotNetZipClient : IZipClient + public class ZipClient : IZipClient { /// /// Extracts all. diff --git a/MediaBrowser.ServerApplication/MediaBrowser.ServerApplication.csproj b/MediaBrowser.ServerApplication/MediaBrowser.ServerApplication.csproj index cc799f6ba..793489c1f 100644 --- a/MediaBrowser.ServerApplication/MediaBrowser.ServerApplication.csproj +++ b/MediaBrowser.ServerApplication/MediaBrowser.ServerApplication.csproj @@ -245,7 +245,7 @@ Code - + LibraryExplorer.xaml diff --git a/Nuget/MediaBrowser.Common.Internal.nuspec b/Nuget/MediaBrowser.Common.Internal.nuspec index d3fd7a35e..eb846cd2f 100644 --- a/Nuget/MediaBrowser.Common.Internal.nuspec +++ b/Nuget/MediaBrowser.Common.Internal.nuspec @@ -2,7 +2,7 @@ MediaBrowser.Common.Internal - 3.0.207 + 3.0.208 MediaBrowser.Common.Internal Luke ebr,Luke,scottisafool @@ -12,7 +12,7 @@ Contains common components shared by Media Browser Theater and Media Browser Server. Not intended for plugin developer consumption. Copyright © Media Browser 2013 - + diff --git a/Nuget/MediaBrowser.Common.nuspec b/Nuget/MediaBrowser.Common.nuspec index 5cddfbc76..ba211c3d6 100644 --- a/Nuget/MediaBrowser.Common.nuspec +++ b/Nuget/MediaBrowser.Common.nuspec @@ -2,7 +2,7 @@ MediaBrowser.Common - 3.0.207 + 3.0.208 MediaBrowser.Common Media Browser Team ebr,Luke,scottisafool diff --git a/Nuget/MediaBrowser.Server.Core.nuspec b/Nuget/MediaBrowser.Server.Core.nuspec index 0c738ce93..c6f801a9b 100644 --- a/Nuget/MediaBrowser.Server.Core.nuspec +++ b/Nuget/MediaBrowser.Server.Core.nuspec @@ -2,7 +2,7 @@ MediaBrowser.Server.Core - 3.0.207 + 3.0.208 Media Browser.Server.Core Media Browser Team ebr,Luke,scottisafool @@ -12,7 +12,7 @@ Contains core components required to build plugins for Media Browser Server. Copyright © Media Browser 2013 - + From bb281d4bcc56f0ea923e14d352d057a74587c33d Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Mon, 23 Sep 2013 18:13:19 -0400 Subject: [PATCH 11/21] fixes #546 - Show runtime being overwritten --- MediaBrowser.Providers/Savers/SeriesXmlSaver.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/MediaBrowser.Providers/Savers/SeriesXmlSaver.cs b/MediaBrowser.Providers/Savers/SeriesXmlSaver.cs index 4e2f538c7..6b9828576 100644 --- a/MediaBrowser.Providers/Savers/SeriesXmlSaver.cs +++ b/MediaBrowser.Providers/Savers/SeriesXmlSaver.cs @@ -113,7 +113,10 @@ namespace MediaBrowser.Providers.Savers "Network", "Airs_Time", "Airs_DayOfWeek", - "FirstAired" + "FirstAired", + + // Don't preserve old series node + "Series" }); // Set last refreshed so that the provider doesn't trigger after the file save From 98fefb18bcf2d98309d31a0f226b02a00a01db61 Mon Sep 17 00:00:00 2001 From: tikuf Date: Tue, 24 Sep 2013 09:48:55 +1000 Subject: [PATCH 12/21] Images changes --- CONTRIBUTORS.md | 1 + .../Resources/Images/mb3logo800.png | Bin 35600 -> 28816 bytes 2 files changed, 1 insertion(+) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index ee6870271..18cef5819 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -57,3 +57,4 @@ - [Detector1](https://github.com/Detector1) - [BlackIce013](https://github.com/blackice013) - [mporcas] (https://github.com/mporcas) + - [tikuf] (https://github.com/tikuf/) diff --git a/MediaBrowser.ServerApplication/Resources/Images/mb3logo800.png b/MediaBrowser.ServerApplication/Resources/Images/mb3logo800.png index fbc769a6f1bfa0de8f334237c6b215f0e1d8c3bc..12db846790a2edc8886d906f20507df099244fc3 100644 GIT binary patch literal 28816 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 literal 35600 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 From cbd767ddcef7a857fb48d1cdb13e79e0ebf201b7 Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Mon, 23 Sep 2013 20:04:02 -0400 Subject: [PATCH 13/21] improve image serving performance slightly --- .../Drawing/ImageProcessor.cs | 94 +++++++++++-------- .../HttpServer/HttpServer.cs | 15 ++- 2 files changed, 67 insertions(+), 42 deletions(-) diff --git a/MediaBrowser.Server.Implementations/Drawing/ImageProcessor.cs b/MediaBrowser.Server.Implementations/Drawing/ImageProcessor.cs index 1f7361d2f..305bede56 100644 --- a/MediaBrowser.Server.Implementations/Drawing/ImageProcessor.cs +++ b/MediaBrowser.Server.Implementations/Drawing/ImageProcessor.cs @@ -121,7 +121,7 @@ namespace MediaBrowser.Server.Implementations.Drawing } catch (IOException) { - // Cache file doesn't exist or is currently being written ro + // Cache file doesn't exist or is currently being written to } var semaphore = GetLock(cacheFilePath); @@ -129,21 +129,24 @@ namespace MediaBrowser.Server.Implementations.Drawing await semaphore.WaitAsync().ConfigureAwait(false); // Check again in case of lock contention - if (File.Exists(cacheFilePath)) + try { - try - { - using (var fileStream = new FileStream(cacheFilePath, FileMode.Open, FileAccess.Read, FileShare.Read, StreamDefaults.DefaultFileStreamBufferSize, FileOptions.Asynchronous)) - { - await fileStream.CopyToAsync(toStream).ConfigureAwait(false); - return; - } - } - finally + using (var fileStream = new FileStream(cacheFilePath, FileMode.Open, FileAccess.Read, FileShare.Read, StreamDefaults.DefaultFileStreamBufferSize, FileOptions.Asynchronous)) { + await fileStream.CopyToAsync(toStream).ConfigureAwait(false); semaphore.Release(); + return; } } + catch (IOException) + { + // Cache file doesn't exist or is currently being written to + } + catch + { + semaphore.Release(); + throw; + } try { @@ -188,12 +191,10 @@ namespace MediaBrowser.Server.Implementations.Drawing var bytes = outputMemoryStream.ToArray(); - var outputTask = toStream.WriteAsync(bytes, 0, bytes.Length); + await toStream.WriteAsync(bytes, 0, bytes.Length).ConfigureAwait(false); // kick off a task to cache the result - var cacheTask = CacheResizedImage(cacheFilePath, bytes); - - await Task.WhenAll(outputTask, cacheTask).ConfigureAwait(false); + CacheResizedImage(cacheFilePath, bytes, semaphore); } } } @@ -202,12 +203,51 @@ namespace MediaBrowser.Server.Implementations.Drawing } } } - finally + catch { semaphore.Release(); + + throw; } } + /// + /// Caches the resized image. + /// + /// The cache file path. + /// The bytes. + /// The semaphore. + private void CacheResizedImage(string cacheFilePath, byte[] bytes, SemaphoreSlim semaphore) + { + Task.Run(async () => + { + try + { + var parentPath = Path.GetDirectoryName(cacheFilePath); + + if (!Directory.Exists(parentPath)) + { + Directory.CreateDirectory(parentPath); + } + + // Save to the cache location + using (var cacheFileStream = new FileStream(cacheFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, StreamDefaults.DefaultFileStreamBufferSize, FileOptions.Asynchronous)) + { + // Save to the filestream + await cacheFileStream.WriteAsync(bytes, 0, bytes.Length).ConfigureAwait(false); + } + } + catch (Exception ex) + { + _logger.ErrorException("Error writing to image cache file {0}", ex, cacheFilePath); + } + finally + { + semaphore.Release(); + } + }); + } + /// /// Sets the color of the background. /// @@ -363,28 +403,6 @@ namespace MediaBrowser.Server.Implementations.Drawing return croppedImagePath; } - /// - /// Caches the resized image. - /// - /// The cache file path. - /// The bytes. - private async Task CacheResizedImage(string cacheFilePath, byte[] bytes) - { - var parentPath = Path.GetDirectoryName(cacheFilePath); - - if (!Directory.Exists(parentPath)) - { - Directory.CreateDirectory(parentPath); - } - - // Save to the cache location - using (var cacheFileStream = new FileStream(cacheFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, StreamDefaults.DefaultFileStreamBufferSize, FileOptions.Asynchronous)) - { - // Save to the filestream - await cacheFileStream.WriteAsync(bytes, 0, bytes.Length).ConfigureAwait(false); - } - } - /// /// Gets the cache file path based on a set of parameters /// diff --git a/MediaBrowser.Server.Implementations/HttpServer/HttpServer.cs b/MediaBrowser.Server.Implementations/HttpServer/HttpServer.cs index f6547dec1..4f795fdd5 100644 --- a/MediaBrowser.Server.Implementations/HttpServer/HttpServer.cs +++ b/MediaBrowser.Server.Implementations/HttpServer/HttpServer.cs @@ -314,6 +314,8 @@ namespace MediaBrowser.Server.Implementations.HttpServer /// The CTX. private async void ProcessHttpRequestAsync(HttpListenerContext context) { + var date = DateTime.Now; + LogHttpRequest(context); if (context.Request.IsWebSocketRequest) @@ -360,7 +362,9 @@ namespace MediaBrowser.Server.Implementations.HttpServer var url = context.Request.Url.ToString(); var endPoint = context.Request.RemoteEndPoint; - LogResponse(context, url, endPoint); + var duration = DateTime.Now - date; + + LogResponse(context, url, endPoint, duration); } catch (Exception ex) @@ -461,14 +465,15 @@ namespace MediaBrowser.Server.Implementations.HttpServer /// The CTX. /// The URL. /// The end point. - private void LogResponse(HttpListenerContext ctx, string url, IPEndPoint endPoint) + /// The duration. + private void LogResponse(HttpListenerContext ctx, string url, IPEndPoint endPoint, TimeSpan duration) { if (!EnableHttpRequestLogging) { return; } - var statusode = ctx.Response.StatusCode; + var statusCode = ctx.Response.StatusCode; var log = new StringBuilder(); @@ -476,7 +481,9 @@ namespace MediaBrowser.Server.Implementations.HttpServer log.AppendLine("Headers: " + string.Join(",", ctx.Response.Headers.AllKeys.Select(k => k + "=" + ctx.Response.Headers[k]))); - var msg = "Http Response Sent (" + statusode + ") to " + endPoint; + var responseTime = string.Format(". Response time: {0} ms", duration.TotalMilliseconds); + + var msg = "Response code " + statusCode + " sent to " + endPoint + responseTime; _logger.LogMultiline(msg, LogSeverity.Debug, log); } From 14c464c28a3b9cac207ef741711e31cef1c15378 Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Mon, 23 Sep 2013 20:04:18 -0400 Subject: [PATCH 14/21] check attributes before saving over image file --- .../Providers/ImageSaver.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/MediaBrowser.Server.Implementations/Providers/ImageSaver.cs b/MediaBrowser.Server.Implementations/Providers/ImageSaver.cs index 608738f7f..d8872f318 100644 --- a/MediaBrowser.Server.Implementations/Providers/ImageSaver.cs +++ b/MediaBrowser.Server.Implementations/Providers/ImageSaver.cs @@ -102,6 +102,18 @@ namespace MediaBrowser.Server.Implementations.Providers using (source) { + // If the file is currently hidden we'll have to remove that or the save will fail + var file = new FileInfo(path); + + // This will fail if the file is hidden + if (file.Exists) + { + if ((file.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden) + { + file.Attributes &= ~FileAttributes.Hidden; + } + } + using (var fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read, StreamDefaults.DefaultFileStreamBufferSize, FileOptions.Asynchronous)) { await source.CopyToAsync(fs, StreamDefaults.DefaultCopyToBufferSize, cancellationToken).ConfigureAwait(false); From b49764dbaafbbf11b6308ec675355696b9e58379 Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Tue, 24 Sep 2013 11:08:51 -0400 Subject: [PATCH 15/21] fixes #555 - Have clients report seek and queuing capabilities --- .../UserLibrary/UserLibraryService.cs | 37 +++++++++++- .../Logging/NlogManager.cs | 18 +++--- .../ScheduledTasks/Tasks/DeleteLogFileTask.cs | 39 ++++++------- .../ScheduledTasks/Tasks/ReloadLoggerTask.cs | 6 +- MediaBrowser.Controller/Entities/Folder.cs | 5 +- .../MediaBrowser.Controller.csproj | 1 + .../Session/ISessionManager.cs | 7 +-- .../Session/PlaybackInfo.cs | 38 ++++++++++++ .../Session/SessionInfo.cs | 13 +++++ MediaBrowser.Model/ApiClient/IApiClient.cs | 4 +- MediaBrowser.Model/Session/SessionInfoDto.cs | 17 +++++- .../Movies/FanArtMovieProvider.cs | 1 - .../Music/SoundtrackPostScanTask.cs | 4 +- .../TV/SeriesPostScanTask.cs | 4 +- .../Dto/DtoService.cs | 4 +- .../Library/LibraryManager.cs | 58 ++++++------------- .../Library/Validators/PeoplePostScanTask.cs | 4 +- .../Persistence/SqliteItemRepository.cs | 15 +++-- .../Session/SessionManager.cs | 22 ++++--- .../Session/SessionWebSocketListener.cs | 52 +++++++++++++---- .../WebSocket/AlchemyWebSocket.cs | 4 +- MediaBrowser.WebDashboard/ApiClient.js | 15 ++++- MediaBrowser.WebDashboard/packages.config | 2 +- 23 files changed, 251 insertions(+), 119 deletions(-) create mode 100644 MediaBrowser.Controller/Session/PlaybackInfo.cs diff --git a/MediaBrowser.Api/UserLibrary/UserLibraryService.cs b/MediaBrowser.Api/UserLibrary/UserLibraryService.cs index ab3e2af19..9085a3ecf 100644 --- a/MediaBrowser.Api/UserLibrary/UserLibraryService.cs +++ b/MediaBrowser.Api/UserLibrary/UserLibraryService.cs @@ -186,7 +186,7 @@ namespace MediaBrowser.Api.UserLibrary [ApiMember(Name = "DatePlayed", Description = "The date the item was played (if any)", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")] public DateTime? DatePlayed { get; set; } - + /// /// Gets or sets the id. /// @@ -224,6 +224,13 @@ namespace MediaBrowser.Api.UserLibrary [Api(Description = "Reports that a user has begun playing an item")] public class OnPlaybackStart : IReturnVoid { + public OnPlaybackStart() + { + // Have to default these until all clients have a chance to incorporate them + CanSeek = true; + QueueableMediaTypes = "Audio,Video,Book,Game"; + } + /// /// Gets or sets the user id. /// @@ -237,6 +244,20 @@ namespace MediaBrowser.Api.UserLibrary /// The id. [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] public string Id { get; set; } + + /// + /// Gets or sets a value indicating whether this is likes. + /// + /// true if likes; otherwise, false. + [ApiMember(Name = "CanSeek", Description = "Indicates if the client can seek", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "POST")] + public bool CanSeek { get; set; } + + /// + /// Gets or sets the id. + /// + /// The id. + [ApiMember(Name = "QueueableMediaTypes", Description = "A list of media types that can be queued from this item, comma delimited. Audio,Video,Book,Game", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST", AllowMultiple = true)] + public string QueueableMediaTypes { get; set; } } /// @@ -378,6 +399,8 @@ namespace MediaBrowser.Api.UserLibrary /// The library manager. /// The user data repository. /// The item repo. + /// The session manager. + /// The dto service. /// jsonSerializer public UserLibraryService(IUserManager userManager, ILibraryManager libraryManager, IUserDataRepository userDataRepository, IItemRepository itemRepo, ISessionManager sessionManager, IDtoService dtoService) { @@ -665,7 +688,17 @@ namespace MediaBrowser.Api.UserLibrary var item = _dtoService.GetItemByDtoId(request.Id, user.Id); - _sessionManager.OnPlaybackStart(item, GetSession().Id); + var queueableMediaTypes = (request.QueueableMediaTypes ?? string.Empty); + + var info = new PlaybackInfo + { + CanSeek = request.CanSeek, + Item = item, + SessionId = GetSession().Id, + QueueableMediaTypes = queueableMediaTypes.Split(',').ToList() + }; + + _sessionManager.OnPlaybackStart(info); } /// diff --git a/MediaBrowser.Common.Implementations/Logging/NlogManager.cs b/MediaBrowser.Common.Implementations/Logging/NlogManager.cs index 109e85d80..e20f9bc13 100644 --- a/MediaBrowser.Common.Implementations/Logging/NlogManager.cs +++ b/MediaBrowser.Common.Implementations/Logging/NlogManager.cs @@ -5,7 +5,6 @@ using NLog.Targets; using System; using System.IO; using System.Linq; -using System.Threading.Tasks; namespace MediaBrowser.Common.Implementations.Logging { @@ -193,17 +192,14 @@ namespace MediaBrowser.Common.Implementations.Logging if (LoggerLoaded != null) { - Task.Run(() => + try { - try - { - LoggerLoaded(this, EventArgs.Empty); - } - catch (Exception ex) - { - GetLogger("Logger").ErrorException("Error in LoggerLoaded event", ex); - } - }); + LoggerLoaded(this, EventArgs.Empty); + } + catch (Exception ex) + { + GetLogger("Logger").ErrorException("Error in LoggerLoaded event", ex); + } } } } diff --git a/MediaBrowser.Common.Implementations/ScheduledTasks/Tasks/DeleteLogFileTask.cs b/MediaBrowser.Common.Implementations/ScheduledTasks/Tasks/DeleteLogFileTask.cs index 15f955723..bfd626adb 100644 --- a/MediaBrowser.Common.Implementations/ScheduledTasks/Tasks/DeleteLogFileTask.cs +++ b/MediaBrowser.Common.Implementations/ScheduledTasks/Tasks/DeleteLogFileTask.cs @@ -54,33 +54,32 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks.Tasks /// Task. public Task Execute(CancellationToken cancellationToken, IProgress progress) { - return Task.Run(() => + // Delete log files more than n days old + var minDateModified = DateTime.UtcNow.AddDays(-(ConfigurationManager.CommonConfiguration.LogFileRetentionDays)); + + var filesToDelete = new DirectoryInfo(ConfigurationManager.CommonApplicationPaths.LogDirectoryPath).EnumerateFileSystemInfos("*", SearchOption.AllDirectories) + .Where(f => f.LastWriteTimeUtc < minDateModified) + .ToList(); + + var index = 0; + + foreach (var file in filesToDelete) { - // Delete log files more than n days old - var minDateModified = DateTime.UtcNow.AddDays(-(ConfigurationManager.CommonConfiguration.LogFileRetentionDays)); + double percent = index; + percent /= filesToDelete.Count; - var filesToDelete = new DirectoryInfo(ConfigurationManager.CommonApplicationPaths.LogDirectoryPath).EnumerateFileSystemInfos("*", SearchOption.AllDirectories) - .Where(f => f.LastWriteTimeUtc < minDateModified) - .ToList(); + progress.Report(100 * percent); - var index = 0; + cancellationToken.ThrowIfCancellationRequested(); - foreach (var file in filesToDelete) - { - double percent = index; - percent /= filesToDelete.Count; + File.Delete(file.FullName); - progress.Report(100 * percent); + index++; + } - cancellationToken.ThrowIfCancellationRequested(); + progress.Report(100); - File.Delete(file.FullName); - - index++; - } - - progress.Report(100); - }); + return Task.FromResult(true); } /// diff --git a/MediaBrowser.Common.Implementations/ScheduledTasks/Tasks/ReloadLoggerTask.cs b/MediaBrowser.Common.Implementations/ScheduledTasks/Tasks/ReloadLoggerTask.cs index e860834ec..00928255c 100644 --- a/MediaBrowser.Common.Implementations/ScheduledTasks/Tasks/ReloadLoggerTask.cs +++ b/MediaBrowser.Common.Implementations/ScheduledTasks/Tasks/ReloadLoggerTask.cs @@ -58,7 +58,11 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks.Tasks progress.Report(0); - return Task.Run(() => LogManager.ReloadLogger(ConfigurationManager.CommonConfiguration.EnableDebugLevelLogging ? LogSeverity.Debug : LogSeverity.Info)); + LogManager.ReloadLogger(ConfigurationManager.CommonConfiguration.EnableDebugLevelLogging + ? LogSeverity.Debug + : LogSeverity.Info); + + return Task.FromResult(true); } /// diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs index 326d30bd7..0f090f587 100644 --- a/MediaBrowser.Controller/Entities/Folder.cs +++ b/MediaBrowser.Controller/Entities/Folder.cs @@ -16,7 +16,6 @@ using System.Linq; using System.Runtime.Serialization; using System.Threading; using System.Threading.Tasks; -using MoreLinq; namespace MediaBrowser.Controller.Entities { @@ -690,7 +689,7 @@ namespace MediaBrowser.Controller.Entities var options = new ParallelOptions { - MaxDegreeOfParallelism = 20 + MaxDegreeOfParallelism = 10 }; Parallel.ForEach(nonCachedChildren, options, child => @@ -805,7 +804,7 @@ namespace MediaBrowser.Controller.Entities foreach (var tuple in list) { - if (tasks.Count > 8) + if (tasks.Count > 5) { await Task.WhenAll(tasks).ConfigureAwait(false); } diff --git a/MediaBrowser.Controller/MediaBrowser.Controller.csproj b/MediaBrowser.Controller/MediaBrowser.Controller.csproj index f49bd8cf0..80cf82da1 100644 --- a/MediaBrowser.Controller/MediaBrowser.Controller.csproj +++ b/MediaBrowser.Controller/MediaBrowser.Controller.csproj @@ -165,6 +165,7 @@ + diff --git a/MediaBrowser.Controller/Session/ISessionManager.cs b/MediaBrowser.Controller/Session/ISessionManager.cs index 1976c653a..fba1d26e8 100644 --- a/MediaBrowser.Controller/Session/ISessionManager.cs +++ b/MediaBrowser.Controller/Session/ISessionManager.cs @@ -47,11 +47,9 @@ namespace MediaBrowser.Controller.Session /// /// Used to report that playback has started for an item /// - /// The item. - /// The session id. + /// The info. /// Task. - /// - Task OnPlaybackStart(BaseItem item, Guid sessionId); + Task OnPlaybackStart(PlaybackInfo info); /// /// Used to report playback progress for an item @@ -59,6 +57,7 @@ namespace MediaBrowser.Controller.Session /// The item. /// The position ticks. /// if set to true [is paused]. + /// if set to true [is muted]. /// The session id. /// Task. /// diff --git a/MediaBrowser.Controller/Session/PlaybackInfo.cs b/MediaBrowser.Controller/Session/PlaybackInfo.cs new file mode 100644 index 000000000..ab3111e76 --- /dev/null +++ b/MediaBrowser.Controller/Session/PlaybackInfo.cs @@ -0,0 +1,38 @@ +using MediaBrowser.Controller.Entities; +using System; +using System.Collections.Generic; + +namespace MediaBrowser.Controller.Session +{ + public class PlaybackInfo + { + public PlaybackInfo() + { + QueueableMediaTypes = new List(); + } + + /// + /// Gets or sets a value indicating whether this instance can seek. + /// + /// true if this instance can seek; otherwise, false. + public bool CanSeek { get; set; } + + /// + /// Gets or sets the queueable media types. + /// + /// The queueable media types. + public List QueueableMediaTypes { get; set; } + + /// + /// Gets or sets the item. + /// + /// The item. + public BaseItem Item { get; set; } + + /// + /// Gets or sets the session id. + /// + /// The session id. + public Guid SessionId { get; set; } + } +} diff --git a/MediaBrowser.Controller/Session/SessionInfo.cs b/MediaBrowser.Controller/Session/SessionInfo.cs index 6c0f1a085..ba6d3d0ac 100644 --- a/MediaBrowser.Controller/Session/SessionInfo.cs +++ b/MediaBrowser.Controller/Session/SessionInfo.cs @@ -15,8 +15,21 @@ namespace MediaBrowser.Controller.Session public SessionInfo() { WebSockets = new List(); + QueueableMediaTypes = new List(); } + /// + /// Gets or sets a value indicating whether this instance can seek. + /// + /// true if this instance can seek; otherwise, false. + public bool CanSeek { get; set; } + + /// + /// Gets or sets the queueable media types. + /// + /// The queueable media types. + public List QueueableMediaTypes { get; set; } + /// /// Gets or sets the id. /// diff --git a/MediaBrowser.Model/ApiClient/IApiClient.cs b/MediaBrowser.Model/ApiClient/IApiClient.cs index 03ea79b3b..4a459cac8 100644 --- a/MediaBrowser.Model/ApiClient/IApiClient.cs +++ b/MediaBrowser.Model/ApiClient/IApiClient.cs @@ -497,9 +497,11 @@ namespace MediaBrowser.Model.ApiClient /// /// The item id. /// The user id. + /// if set to true [is seekable]. + /// The list of media types that the client is capable of queuing onto the playlist. See MediaType class. /// Task{UserItemDataDto}. /// itemId - Task ReportPlaybackStartAsync(string itemId, string userId); + Task ReportPlaybackStartAsync(string itemId, string userId, bool isSeekable, List queueableMediaTypes); /// /// Reports playback progress to the server diff --git a/MediaBrowser.Model/Session/SessionInfoDto.cs b/MediaBrowser.Model/Session/SessionInfoDto.cs index f9b0e0abd..02b7f0226 100644 --- a/MediaBrowser.Model/Session/SessionInfoDto.cs +++ b/MediaBrowser.Model/Session/SessionInfoDto.cs @@ -1,11 +1,24 @@ -using System.ComponentModel; -using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Entities; using System; +using System.Collections.Generic; +using System.ComponentModel; namespace MediaBrowser.Model.Session { public class SessionInfoDto : INotifyPropertyChanged { + /// + /// Gets or sets a value indicating whether this instance can seek. + /// + /// true if this instance can seek; otherwise, false. + public bool CanSeek { get; set; } + + /// + /// Gets or sets the queueable media types. + /// + /// The queueable media types. + public List QueueableMediaTypes { get; set; } + /// /// Gets or sets the id. /// diff --git a/MediaBrowser.Providers/Movies/FanArtMovieProvider.cs b/MediaBrowser.Providers/Movies/FanArtMovieProvider.cs index fefcd8371..adc013699 100644 --- a/MediaBrowser.Providers/Movies/FanArtMovieProvider.cs +++ b/MediaBrowser.Providers/Movies/FanArtMovieProvider.cs @@ -1,5 +1,4 @@ using MediaBrowser.Common.Configuration; -using MediaBrowser.Common.Extensions; using MediaBrowser.Common.IO; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; diff --git a/MediaBrowser.Providers/Music/SoundtrackPostScanTask.cs b/MediaBrowser.Providers/Music/SoundtrackPostScanTask.cs index 18868d3ea..e18351248 100644 --- a/MediaBrowser.Providers/Music/SoundtrackPostScanTask.cs +++ b/MediaBrowser.Providers/Music/SoundtrackPostScanTask.cs @@ -23,7 +23,9 @@ namespace MediaBrowser.Providers.Music public Task Run(IProgress progress, CancellationToken cancellationToken) { - return Task.Run(() => RunInternal(progress, cancellationToken)); + RunInternal(progress, cancellationToken); + + return Task.FromResult(true); } private void RunInternal(IProgress progress, CancellationToken cancellationToken) diff --git a/MediaBrowser.Providers/TV/SeriesPostScanTask.cs b/MediaBrowser.Providers/TV/SeriesPostScanTask.cs index a781551de..2b73ba1f7 100644 --- a/MediaBrowser.Providers/TV/SeriesPostScanTask.cs +++ b/MediaBrowser.Providers/TV/SeriesPostScanTask.cs @@ -21,7 +21,9 @@ namespace MediaBrowser.Providers.TV public Task Run(IProgress progress, CancellationToken cancellationToken) { - return Task.Run(() => RunInternal(progress, cancellationToken)); + RunInternal(progress, cancellationToken); + + return Task.FromResult(true); } private void RunInternal(IProgress progress, CancellationToken cancellationToken) diff --git a/MediaBrowser.Server.Implementations/Dto/DtoService.cs b/MediaBrowser.Server.Implementations/Dto/DtoService.cs index e5260004a..a5f54b938 100644 --- a/MediaBrowser.Server.Implementations/Dto/DtoService.cs +++ b/MediaBrowser.Server.Implementations/Dto/DtoService.cs @@ -237,7 +237,9 @@ namespace MediaBrowser.Server.Implementations.Dto NowViewingItemId = session.NowViewingItemId, NowViewingItemName = session.NowViewingItemName, NowViewingItemType = session.NowViewingItemType, - ApplicationVersion = session.ApplicationVersion + ApplicationVersion = session.ApplicationVersion, + CanSeek = session.CanSeek, + QueueableMediaTypes = session.QueueableMediaTypes }; if (session.NowPlayingItem != null) diff --git a/MediaBrowser.Server.Implementations/Library/LibraryManager.cs b/MediaBrowser.Server.Implementations/Library/LibraryManager.cs index a5b792726..1bc3f1094 100644 --- a/MediaBrowser.Server.Implementations/Library/LibraryManager.cs +++ b/MediaBrowser.Server.Implementations/Library/LibraryManager.cs @@ -829,10 +829,6 @@ namespace MediaBrowser.Server.Implementations.Library /// Task. public async Task ValidatePeople(CancellationToken cancellationToken, IProgress progress) { - const int maxTasks = 3; - - var tasks = new List(); - var people = RootFolder.RecursiveChildren .SelectMany(c => c.People) .DistinctBy(p => p.Name, StringComparer.OrdinalIgnoreCase) @@ -842,47 +838,27 @@ namespace MediaBrowser.Server.Implementations.Library foreach (var person in people) { - if (tasks.Count > maxTasks) - { - await Task.WhenAll(tasks).ConfigureAwait(false); - tasks.Clear(); + cancellationToken.ThrowIfCancellationRequested(); - // Safe cancellation point, when there are no pending tasks - cancellationToken.ThrowIfCancellationRequested(); + try + { + var item = GetPerson(person.Name); + + await item.RefreshMetadata(cancellationToken).ConfigureAwait(false); + } + catch (IOException ex) + { + _logger.ErrorException("Error validating IBN entry {0}", ex, person.Name); } - // Avoid accessing the foreach variable within the closure - var currentPerson = person; + // Update progress + numComplete++; + double percent = numComplete; + percent /= people.Count; - tasks.Add(Task.Run(async () => - { - cancellationToken.ThrowIfCancellationRequested(); - - try - { - var item = GetPerson(currentPerson.Name); - - await item.RefreshMetadata(cancellationToken).ConfigureAwait(false); - } - catch (IOException ex) - { - _logger.ErrorException("Error validating IBN entry {0}", ex, currentPerson.Name); - } - - // Update progress - lock (progress) - { - numComplete++; - double percent = numComplete; - percent /= people.Count; - - progress.Report(100 * percent); - } - })); + progress.Report(100 * percent); } - await Task.WhenAll(tasks).ConfigureAwait(false); - progress.Report(100); _logger.Info("People validation complete"); @@ -956,7 +932,9 @@ namespace MediaBrowser.Server.Implementations.Library public Task ValidateMediaLibrary(IProgress progress, CancellationToken cancellationToken) { // Just run the scheduled task so that the user can see it - return Task.Run(() => _taskManager.CancelIfRunningAndQueue()); + _taskManager.CancelIfRunningAndQueue(); + + return Task.FromResult(true); } /// diff --git a/MediaBrowser.Server.Implementations/Library/Validators/PeoplePostScanTask.cs b/MediaBrowser.Server.Implementations/Library/Validators/PeoplePostScanTask.cs index efefaeba3..dc96632f6 100644 --- a/MediaBrowser.Server.Implementations/Library/Validators/PeoplePostScanTask.cs +++ b/MediaBrowser.Server.Implementations/Library/Validators/PeoplePostScanTask.cs @@ -41,7 +41,9 @@ namespace MediaBrowser.Server.Implementations.Library.Validators /// Task. public Task Run(IProgress progress, CancellationToken cancellationToken) { - return Task.Run(() => RunInternal(progress, cancellationToken)); + RunInternal(progress, cancellationToken); + + return Task.FromResult(true); } private void RunInternal(IProgress progress, CancellationToken cancellationToken) diff --git a/MediaBrowser.Server.Implementations/Persistence/SqliteItemRepository.cs b/MediaBrowser.Server.Implementations/Persistence/SqliteItemRepository.cs index f4f5f08e4..9c5cf6f1c 100644 --- a/MediaBrowser.Server.Implementations/Persistence/SqliteItemRepository.cs +++ b/MediaBrowser.Server.Implementations/Persistence/SqliteItemRepository.cs @@ -333,17 +333,16 @@ namespace MediaBrowser.Server.Implementations.Persistence /// Task. public Task SaveCriticReviews(Guid itemId, IEnumerable criticReviews) { - return Task.Run(() => + if (!Directory.Exists(_criticReviewsPath)) { - if (!Directory.Exists(_criticReviewsPath)) - { - Directory.CreateDirectory(_criticReviewsPath); - } + Directory.CreateDirectory(_criticReviewsPath); + } - var path = Path.Combine(_criticReviewsPath, itemId + ".json"); + var path = Path.Combine(_criticReviewsPath, itemId + ".json"); - _jsonSerializer.SerializeToFile(criticReviews.ToList(), path); - }); + _jsonSerializer.SerializeToFile(criticReviews.ToList(), path); + + return Task.FromResult(true); } /// diff --git a/MediaBrowser.Server.Implementations/Session/SessionManager.cs b/MediaBrowser.Server.Implementations/Session/SessionManager.cs index ce757d142..65ec02d12 100644 --- a/MediaBrowser.Server.Implementations/Session/SessionManager.cs +++ b/MediaBrowser.Server.Implementations/Session/SessionManager.cs @@ -207,21 +207,29 @@ namespace MediaBrowser.Server.Implementations.Session /// /// Used to report that playback has started for an item /// - /// The item. - /// The session id. + /// The info. /// Task. - /// - public async Task OnPlaybackStart(BaseItem item, Guid sessionId) + /// info + public async Task OnPlaybackStart(PlaybackInfo info) { - if (item == null) + if (info == null) { - throw new ArgumentNullException(); + throw new ArgumentNullException("info"); + } + if (info.SessionId == Guid.Empty) + { + throw new ArgumentNullException("info"); } - var session = Sessions.First(i => i.Id.Equals(sessionId)); + var session = Sessions.First(i => i.Id.Equals(info.SessionId)); + + var item = info.Item; UpdateNowPlayingItem(session, item, false, false); + session.CanSeek = info.CanSeek; + session.QueueableMediaTypes = info.QueueableMediaTypes; + var key = item.GetUserDataKey(); var user = session.User; diff --git a/MediaBrowser.Server.Implementations/Session/SessionWebSocketListener.cs b/MediaBrowser.Server.Implementations/Session/SessionWebSocketListener.cs index 2a4361e61..95eb5948f 100644 --- a/MediaBrowser.Server.Implementations/Session/SessionWebSocketListener.cs +++ b/MediaBrowser.Server.Implementations/Session/SessionWebSocketListener.cs @@ -101,16 +101,7 @@ namespace MediaBrowser.Server.Implementations.Session } else if (string.Equals(message.MessageType, "PlaybackStart", StringComparison.OrdinalIgnoreCase)) { - _logger.Debug("Received PlaybackStart message"); - - var session = _sessionManager.Sessions.FirstOrDefault(i => i.WebSockets.Contains(message.Connection)); - - if (session != null && session.User != null) - { - var item = _dtoService.GetItemByDtoId(message.Data); - - _sessionManager.OnPlaybackStart(item, session.Id); - } + ReportPlaybackStart(message); } else if (string.Equals(message.MessageType, "PlaybackProgress", StringComparison.OrdinalIgnoreCase)) { @@ -170,5 +161,46 @@ namespace MediaBrowser.Server.Implementations.Session return _trueTaskResult; } + + /// + /// Reports the playback start. + /// + /// The message. + private void ReportPlaybackStart(WebSocketMessageInfo message) + { + _logger.Debug("Received PlaybackStart message"); + + var session = _sessionManager.Sessions + .FirstOrDefault(i => i.WebSockets.Contains(message.Connection)); + + if (session != null && session.User != null) + { + var vals = message.Data.Split('|'); + + var item = _dtoService.GetItemByDtoId(vals[0]); + + var queueableMediaTypes = string.Empty; + var canSeek = true; + + if (vals.Length > 1) + { + canSeek = string.Equals(vals[1], "true", StringComparison.OrdinalIgnoreCase); + } + if (vals.Length > 2) + { + queueableMediaTypes = vals[2]; + } + + var info = new PlaybackInfo + { + CanSeek = canSeek, + Item = item, + SessionId = session.Id, + QueueableMediaTypes = queueableMediaTypes.Split(',').ToList() + }; + + _sessionManager.OnPlaybackStart(info); + } + } } } diff --git a/MediaBrowser.Server.Implementations/WebSocket/AlchemyWebSocket.cs b/MediaBrowser.Server.Implementations/WebSocket/AlchemyWebSocket.cs index 958201625..de998254c 100644 --- a/MediaBrowser.Server.Implementations/WebSocket/AlchemyWebSocket.cs +++ b/MediaBrowser.Server.Implementations/WebSocket/AlchemyWebSocket.cs @@ -92,7 +92,9 @@ namespace MediaBrowser.Server.Implementations.WebSocket /// Task. public Task SendAsync(byte[] bytes, WebSocketMessageType type, bool endOfMessage, CancellationToken cancellationToken) { - return Task.Run(() => UserContext.Send(bytes)); + UserContext.Send(bytes); + + return Task.FromResult(true); } /// diff --git a/MediaBrowser.WebDashboard/ApiClient.js b/MediaBrowser.WebDashboard/ApiClient.js index d139adfc3..189812a3c 100644 --- a/MediaBrowser.WebDashboard/ApiClient.js +++ b/MediaBrowser.WebDashboard/ApiClient.js @@ -3200,7 +3200,7 @@ MediaBrowser.ApiClient = function ($, navigator, JSON, WebSocket, setTimeout, wi * @param {String} userId * @param {String} itemId */ - self.reportPlaybackStart = function (userId, itemId) { + self.reportPlaybackStart = function (userId, itemId, canSeek, queueableMediaTypes) { if (!userId) { throw new Error("null userId"); @@ -3210,17 +3210,26 @@ MediaBrowser.ApiClient = function ($, navigator, JSON, WebSocket, setTimeout, wi throw new Error("null itemId"); } + canSeek = canSeek || false; + queueableMediaTypes = queueableMediaTypes || ''; + if (self.isWebSocketOpen()) { var deferred = $.Deferred(); - self.sendWebSocketMessage("PlaybackStart", itemId); + var msg = [itemId, canSeek, queueableMediaTypes]; + + self.sendWebSocketMessage("PlaybackStart", msg.join('|')); deferred.resolveWith(null, []); return deferred.promise(); } - var url = self.getUrl("Users/" + userId + "/PlayingItems/" + itemId); + var url = self.getUrl("Users/" + userId + "/PlayingItems/" + itemId, { + + CanSeek: canSeek, + QueueableMediaTypes: queueableMediaTypes + }); return self.ajax({ type: "POST", diff --git a/MediaBrowser.WebDashboard/packages.config b/MediaBrowser.WebDashboard/packages.config index 25b4f7b47..1c5a0f818 100644 --- a/MediaBrowser.WebDashboard/packages.config +++ b/MediaBrowser.WebDashboard/packages.config @@ -1,6 +1,6 @@  - + \ No newline at end of file From c61cc4a304978a59f66c86c3618f8f6dd8ccca7b Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Tue, 24 Sep 2013 11:42:30 -0400 Subject: [PATCH 16/21] support more kinds of remote control besides web socket --- MediaBrowser.Api/SessionsService.cs | 40 +----------- .../MediaBrowser.Controller.csproj | 1 + .../Session/ISessionManager.cs | 17 +++++ .../Session/ISessionRemoteController.cs | 25 +++++++ ...MediaBrowser.Server.Implementations.csproj | 1 + .../Session/SessionManager.cs | 65 +++++++++++++++++-- .../Session/WebSocketController.cs | 52 +++++++++++++++ .../ApplicationHost.cs | 7 +- 8 files changed, 163 insertions(+), 45 deletions(-) create mode 100644 MediaBrowser.Controller/Session/ISessionRemoteController.cs create mode 100644 MediaBrowser.Server.Implementations/Session/WebSocketController.cs diff --git a/MediaBrowser.Api/SessionsService.cs b/MediaBrowser.Api/SessionsService.cs index cad3c4384..b93b5326e 100644 --- a/MediaBrowser.Api/SessionsService.cs +++ b/MediaBrowser.Api/SessionsService.cs @@ -325,49 +325,11 @@ namespace MediaBrowser.Api /// The request. public void Post(SendSystemCommand request) { - var task = SendSystemCommand(request); + var task = _sessionManager.SendSystemCommand(request.Id, request.Command, CancellationToken.None); Task.WaitAll(task); } - private async Task SendSystemCommand(SendSystemCommand request) - { - var session = _sessionManager.Sessions.FirstOrDefault(i => i.Id == request.Id); - - if (session == null) - { - throw new ResourceNotFoundException(string.Format("Session {0} not found.", request.Id)); - } - - if (!session.SupportsRemoteControl) - { - throw new ArgumentException(string.Format("Session {0} does not support remote control.", session.Id)); - } - - var socket = session.WebSockets.OrderByDescending(i => i.LastActivityDate).FirstOrDefault(i => i.State == WebSocketState.Open); - - if (socket != null) - { - try - { - await socket.SendAsync(new WebSocketMessage - { - MessageType = "SystemCommand", - Data = request.Command.ToString() - - }, CancellationToken.None).ConfigureAwait(false); - } - catch (Exception ex) - { - Logger.ErrorException("Error sending web socket message", ex); - } - } - else - { - throw new InvalidOperationException("The requested session does not have an open web socket."); - } - } - /// /// Posts the specified request. /// diff --git a/MediaBrowser.Controller/MediaBrowser.Controller.csproj b/MediaBrowser.Controller/MediaBrowser.Controller.csproj index 80cf82da1..b5ad862be 100644 --- a/MediaBrowser.Controller/MediaBrowser.Controller.csproj +++ b/MediaBrowser.Controller/MediaBrowser.Controller.csproj @@ -165,6 +165,7 @@ + diff --git a/MediaBrowser.Controller/Session/ISessionManager.cs b/MediaBrowser.Controller/Session/ISessionManager.cs index fba1d26e8..f8f7ded2b 100644 --- a/MediaBrowser.Controller/Session/ISessionManager.cs +++ b/MediaBrowser.Controller/Session/ISessionManager.cs @@ -1,7 +1,9 @@ using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; +using MediaBrowser.Model.Session; using System; using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; namespace MediaBrowser.Controller.Session @@ -11,6 +13,12 @@ namespace MediaBrowser.Controller.Session /// public interface ISessionManager { + /// + /// Adds the parts. + /// + /// The remote controllers. + void AddParts(IEnumerable remoteControllers); + /// /// Occurs when [playback start]. /// @@ -72,5 +80,14 @@ namespace MediaBrowser.Controller.Session /// Task. /// Task OnPlaybackStopped(BaseItem item, long? positionTicks, Guid sessionId); + + /// + /// Sends the system command. + /// + /// The session id. + /// The command. + /// The cancellation token. + /// Task. + Task SendSystemCommand(Guid sessionId, SystemCommand command, CancellationToken cancellationToken); } } \ No newline at end of file diff --git a/MediaBrowser.Controller/Session/ISessionRemoteController.cs b/MediaBrowser.Controller/Session/ISessionRemoteController.cs new file mode 100644 index 000000000..1f6faeb9c --- /dev/null +++ b/MediaBrowser.Controller/Session/ISessionRemoteController.cs @@ -0,0 +1,25 @@ +using MediaBrowser.Model.Session; +using System.Threading; +using System.Threading.Tasks; + +namespace MediaBrowser.Controller.Session +{ + public interface ISessionRemoteController + { + /// + /// Supportses the specified session. + /// + /// The session. + /// true if XXXX, false otherwise + bool Supports(SessionInfo session); + + /// + /// Sends the system command. + /// + /// The session. + /// The command. + /// The cancellation token. + /// Task. + Task SendSystemCommand(SessionInfo session, SystemCommand command, CancellationToken cancellationToken); + } +} diff --git a/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj b/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj index 9d2fc8c6b..3c2021750 100644 --- a/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj +++ b/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj @@ -183,6 +183,7 @@ Code + diff --git a/MediaBrowser.Server.Implementations/Session/SessionManager.cs b/MediaBrowser.Server.Implementations/Session/SessionManager.cs index 65ec02d12..5b0d957ae 100644 --- a/MediaBrowser.Server.Implementations/Session/SessionManager.cs +++ b/MediaBrowser.Server.Implementations/Session/SessionManager.cs @@ -1,4 +1,5 @@ using MediaBrowser.Common.Events; +using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; @@ -6,6 +7,7 @@ using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Session; using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Session; using System; using System.Collections.Concurrent; using System.Collections.Generic; @@ -75,6 +77,12 @@ namespace MediaBrowser.Server.Implementations.Session _userRepository = userRepository; } + private List _remoteControllers; + public void AddParts(IEnumerable remoteControllers) + { + _remoteControllers = remoteControllers.ToList(); + } + /// /// Gets all connections. /// @@ -122,7 +130,7 @@ namespace MediaBrowser.Server.Implementations.Session var activityDate = DateTime.UtcNow; var session = GetSessionInfo(clientType, appVersion, deviceId, deviceName, user); - + session.LastActivityDate = activityDate; if (user == null) @@ -233,7 +241,7 @@ namespace MediaBrowser.Server.Implementations.Session var key = item.GetUserDataKey(); var user = session.User; - + var data = _userDataRepository.GetUserData(user.Id, key); data.PlayCount++; @@ -321,7 +329,7 @@ namespace MediaBrowser.Server.Implementations.Session { throw new ArgumentOutOfRangeException("positionTicks"); } - + var session = Sessions.First(i => i.Id.Equals(sessionId)); RemoveNowPlayingItem(session, item); @@ -329,7 +337,7 @@ namespace MediaBrowser.Server.Implementations.Session var key = item.GetUserDataKey(); var user = session.User; - + var data = _userDataRepository.GetUserData(user.Id, key); if (positionTicks.HasValue) @@ -408,5 +416,54 @@ namespace MediaBrowser.Server.Implementations.Session data.PlaybackPositionTicks = positionTicks; } + + /// + /// Gets the session for remote control. + /// + /// The session id. + /// SessionInfo. + /// + private SessionInfo GetSessionForRemoteControl(Guid sessionId) + { + var session = Sessions.First(i => i.Id.Equals(sessionId)); + + if (session == null) + { + throw new ResourceNotFoundException(string.Format("Session {0} not found.", sessionId)); + } + + if (!session.SupportsRemoteControl) + { + throw new ArgumentException(string.Format("Session {0} does not support remote control.", session.Id)); + } + + return session; + } + + /// + /// Gets the controllers. + /// + /// The session. + /// IEnumerable{ISessionRemoteController}. + private IEnumerable GetControllers(SessionInfo session) + { + return _remoteControllers.Where(i => i.Supports(session)); + } + + /// + /// Sends the system command. + /// + /// The session id. + /// The command. + /// The cancellation token. + /// Task. + public Task SendSystemCommand(Guid sessionId, SystemCommand command, CancellationToken cancellationToken) + { + var session = GetSessionForRemoteControl(sessionId); + + var tasks = GetControllers(session).Select(i => i.SendSystemCommand(session, command, cancellationToken)); + + return Task.WhenAll(tasks); + } } } diff --git a/MediaBrowser.Server.Implementations/Session/WebSocketController.cs b/MediaBrowser.Server.Implementations/Session/WebSocketController.cs new file mode 100644 index 000000000..daa4c7d81 --- /dev/null +++ b/MediaBrowser.Server.Implementations/Session/WebSocketController.cs @@ -0,0 +1,52 @@ +using MediaBrowser.Controller.Session; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Net; +using MediaBrowser.Model.Session; +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace MediaBrowser.Server.Implementations.Session +{ + public class WebSocketController : ISessionRemoteController + { + private readonly ILogger _logger; + + public WebSocketController(ILogger logger) + { + _logger = logger; + } + + public bool Supports(SessionInfo session) + { + return session.WebSockets.Any(i => i.State == WebSocketState.Open); + } + + public async Task SendSystemCommand(SessionInfo session, SystemCommand command, CancellationToken cancellationToken) + { + var socket = session.WebSockets.OrderByDescending(i => i.LastActivityDate).FirstOrDefault(i => i.State == WebSocketState.Open); + + if (socket != null) + { + try + { + await socket.SendAsync(new WebSocketMessage + { + MessageType = "SystemCommand", + Data = command.ToString() + + }, cancellationToken).ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.ErrorException("Error sending web socket message", ex); + } + } + else + { + throw new InvalidOperationException("The requested session does not have an open web socket."); + } + } + } +} diff --git a/MediaBrowser.ServerApplication/ApplicationHost.cs b/MediaBrowser.ServerApplication/ApplicationHost.cs index 7a99693a6..e96516603 100644 --- a/MediaBrowser.ServerApplication/ApplicationHost.cs +++ b/MediaBrowser.ServerApplication/ApplicationHost.cs @@ -161,6 +161,7 @@ namespace MediaBrowser.ServerApplication private IMediaEncoder MediaEncoder { get; set; } private IIsoManager IsoManager { get; set; } + private ISessionManager SessionManager { get; set; } private ILocalizationManager LocalizationManager { get; set; } @@ -286,8 +287,8 @@ namespace MediaBrowser.ServerApplication RegisterSingleInstance(() => new LuceneSearchEngine(ApplicationPaths, LogManager, LibraryManager)); - var clientConnectionManager = new SessionManager(UserDataRepository, ServerConfigurationManager, Logger, UserRepository); - RegisterSingleInstance(clientConnectionManager); + SessionManager = new SessionManager(UserDataRepository, ServerConfigurationManager, Logger, UserRepository); + RegisterSingleInstance(SessionManager); HttpServer = await _httpServerCreationTask.ConfigureAwait(false); RegisterSingleInstance(HttpServer, false); @@ -477,6 +478,8 @@ namespace MediaBrowser.ServerApplication IsoManager.AddParts(GetExports()); + SessionManager.AddParts(GetExports()); + ImageProcessor.AddParts(GetExports()); } From f176307e591dc8cd4fd1dabe1ebc5e22fba26d51 Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Tue, 24 Sep 2013 15:54:42 -0400 Subject: [PATCH 17/21] support multiple remote control outputs --- MediaBrowser.Api/ApiEntryPoint.cs | 48 +++- .../AuthorizationRequestFilterAttribute.cs | 181 ++++++++++++++ MediaBrowser.Api/BaseApiService.cs | 147 +---------- MediaBrowser.Api/MediaBrowser.Api.csproj | 7 +- .../Playback/BaseStreamingService.cs | 2 +- .../Playback/Hls/AudioHlsService.cs | 36 --- .../Playback/Hls/BaseHlsService.cs | 24 -- .../Playback/Hls/HlsSegmentResponseFilter.cs | 53 ++++ .../Playback/Hls/HlsSegmentService.cs | 147 +++++++++++ .../Playback/Hls/VideoHlsService.cs | 67 +---- MediaBrowser.Api/SessionsService.cs | 228 +++--------------- .../UserLibrary/UserLibraryService.cs | 23 +- .../Session/ISessionManager.cs | 36 +++ .../Session/ISessionRemoteController.cs | 36 +++ .../Session/SessionManager.cs | 64 +++++ .../Session/WebSocketController.cs | 92 +++++-- 16 files changed, 681 insertions(+), 510 deletions(-) create mode 100644 MediaBrowser.Api/AuthorizationRequestFilterAttribute.cs create mode 100644 MediaBrowser.Api/Playback/Hls/HlsSegmentResponseFilter.cs create mode 100644 MediaBrowser.Api/Playback/Hls/HlsSegmentService.cs diff --git a/MediaBrowser.Api/ApiEntryPoint.cs b/MediaBrowser.Api/ApiEntryPoint.cs index 52707c3c6..273d9a7a9 100644 --- a/MediaBrowser.Api/ApiEntryPoint.cs +++ b/MediaBrowser.Api/ApiEntryPoint.cs @@ -62,7 +62,7 @@ namespace MediaBrowser.Api { var jobCount = _activeTranscodingJobs.Count; - Parallel.ForEach(_activeTranscodingJobs, OnTranscodeKillTimerStopped); + Parallel.ForEach(_activeTranscodingJobs, KillTranscodingJob); // Try to allow for some time to kill the ffmpeg processes and delete the partial stream files if (jobCount > 0) @@ -84,7 +84,8 @@ namespace MediaBrowser.Api /// The process. /// if set to true [is video]. /// The start time ticks. - public void OnTranscodeBeginning(string path, TranscodingJobType type, Process process, bool isVideo, long? startTimeTicks) + /// The source path. + public void OnTranscodeBeginning(string path, TranscodingJobType type, Process process, bool isVideo, long? startTimeTicks, string sourcePath) { lock (_activeTranscodingJobs) { @@ -95,7 +96,8 @@ namespace MediaBrowser.Api Process = process, ActiveRequestCount = 1, IsVideo = isVideo, - StartTimeTicks = startTimeTicks + StartTimeTicks = startTimeTicks, + SourcePath = sourcePath }); } } @@ -196,10 +198,47 @@ namespace MediaBrowser.Api /// Called when [transcode kill timer stopped]. /// /// The state. - private async void OnTranscodeKillTimerStopped(object state) + private void OnTranscodeKillTimerStopped(object state) { var job = (TranscodingJob)state; + KillTranscodingJob(job); + } + + /// + /// Kills the single transcoding job. + /// + /// The source path. + internal void KillSingleTranscodingJob(string sourcePath) + { + if (string.IsNullOrEmpty(sourcePath)) + { + throw new ArgumentNullException("sourcePath"); + } + + var jobs = new List(); + + lock (_activeTranscodingJobs) + { + // This is really only needed for HLS. + // Progressive streams can stop on their own reliably + jobs.AddRange(_activeTranscodingJobs.Where(i => string.Equals(sourcePath, i.SourcePath) && i.Type == TranscodingJobType.Hls)); + } + + // This method of killing is a bit of a shortcut, but it saves clients from having to send a request just for that + // But we can only kill if there's one active job. If there are more we won't know which one to stop + if (jobs.Count == 1) + { + KillTranscodingJob(jobs.First()); + } + } + + /// + /// Kills the transcoding job. + /// + /// The job. + private async void KillTranscodingJob(TranscodingJob job) + { lock (_activeTranscodingJobs) { _activeTranscodingJobs.Remove(job); @@ -373,6 +412,7 @@ namespace MediaBrowser.Api public bool IsVideo { get; set; } public long? StartTimeTicks { get; set; } + public string SourcePath { get; set; } } /// diff --git a/MediaBrowser.Api/AuthorizationRequestFilterAttribute.cs b/MediaBrowser.Api/AuthorizationRequestFilterAttribute.cs new file mode 100644 index 000000000..d225bdd99 --- /dev/null +++ b/MediaBrowser.Api/AuthorizationRequestFilterAttribute.cs @@ -0,0 +1,181 @@ +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Session; +using MediaBrowser.Model.Logging; +using ServiceStack.Common.Web; +using ServiceStack.ServiceHost; +using System; +using System.Collections.Generic; + +namespace MediaBrowser.Api +{ + public class AuthorizationRequestFilterAttribute : Attribute, IHasRequestFilter + { + //This property will be resolved by the IoC container + /// + /// Gets or sets the user manager. + /// + /// The user manager. + public IUserManager UserManager { get; set; } + + public ISessionManager SessionManager { get; set; } + + /// + /// Gets or sets the logger. + /// + /// The logger. + public ILogger Logger { get; set; } + + /// + /// The request filter is executed before the service. + /// + /// The http request wrapper + /// The http response wrapper + /// The request DTO + public void RequestFilter(IHttpRequest request, IHttpResponse response, object requestDto) + { + //This code is executed before the service + + var auth = GetAuthorization(request); + + if (auth != null) + { + User user = null; + + if (auth.ContainsKey("UserId")) + { + var userId = auth["UserId"]; + + if (!string.IsNullOrEmpty(userId)) + { + user = UserManager.GetUserById(new Guid(userId)); + } + } + + string deviceId; + string device; + string client; + string version; + + auth.TryGetValue("DeviceId", out deviceId); + auth.TryGetValue("Device", out device); + auth.TryGetValue("Client", out client); + auth.TryGetValue("Version", out version); + + if (!string.IsNullOrEmpty(client) && !string.IsNullOrEmpty(deviceId) && !string.IsNullOrEmpty(device) && !string.IsNullOrEmpty(version)) + { + SessionManager.LogConnectionActivity(client, version, deviceId, device, user); + } + } + } + + /// + /// Gets the auth. + /// + /// The HTTP req. + /// Dictionary{System.StringSystem.String}. + public static Dictionary GetAuthorization(IHttpRequest httpReq) + { + var auth = httpReq.Headers[HttpHeaders.Authorization]; + + return GetAuthorization(auth); + } + + /// + /// Gets the authorization. + /// + /// The HTTP req. + /// Dictionary{System.StringSystem.String}. + public static AuthorizationInfo GetAuthorization(IRequestContext httpReq) + { + var header = httpReq.GetHeader("Authorization"); + + var auth = GetAuthorization(header); + + string userId; + string deviceId; + string device; + string client; + string version; + + auth.TryGetValue("UserId", out userId); + auth.TryGetValue("DeviceId", out deviceId); + auth.TryGetValue("Device", out device); + auth.TryGetValue("Client", out client); + auth.TryGetValue("Version", out version); + + return new AuthorizationInfo + { + Client = client, + Device = device, + DeviceId = deviceId, + UserId = userId, + Version = version + }; + } + + /// + /// Gets the authorization. + /// + /// The authorization header. + /// Dictionary{System.StringSystem.String}. + private static Dictionary GetAuthorization(string authorizationHeader) + { + if (authorizationHeader == null) return null; + + var parts = authorizationHeader.Split(' '); + + // There should be at least to parts + if (parts.Length < 2) return null; + + // It has to be a digest request + if (!string.Equals(parts[0], "MediaBrowser", StringComparison.OrdinalIgnoreCase)) + { + return null; + } + + // Remove uptil the first space + authorizationHeader = authorizationHeader.Substring(authorizationHeader.IndexOf(' ')); + parts = authorizationHeader.Split(','); + + var result = new Dictionary(StringComparer.OrdinalIgnoreCase); + + foreach (var item in parts) + { + var param = item.Trim().Split(new[] { '=' }, 2); + result.Add(param[0], param[1].Trim(new[] { '"' })); + } + + return result; + } + + /// + /// A new shallow copy of this filter is used on every request. + /// + /// IHasRequestFilter. + public IHasRequestFilter Copy() + { + return this; + } + + /// + /// Order in which Request Filters are executed. + /// <0 Executed before global request filters + /// >0 Executed after global request filters + /// + /// The priority. + public int Priority + { + get { return 0; } + } + } + + public class AuthorizationInfo + { + public string UserId; + public string DeviceId; + public string Device; + public string Client; + public string Version; + } +} diff --git a/MediaBrowser.Api/BaseApiService.cs b/MediaBrowser.Api/BaseApiService.cs index b3f5027e0..069bc0fe1 100644 --- a/MediaBrowser.Api/BaseApiService.cs +++ b/MediaBrowser.Api/BaseApiService.cs @@ -2,9 +2,7 @@ using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Session; using MediaBrowser.Model.Logging; -using ServiceStack.Common.Web; using ServiceStack.ServiceHost; using System; using System.Collections.Generic; @@ -15,7 +13,7 @@ namespace MediaBrowser.Api /// /// Class BaseApiService /// - [RequestFilter] + [AuthorizationRequestFilter] public class BaseApiService : IHasResultFactory, IRestfulService { /// @@ -308,147 +306,4 @@ namespace MediaBrowser.Api return item; } } - - /// - /// Class RequestFilterAttribute - /// - public class RequestFilterAttribute : Attribute, IHasRequestFilter - { - //This property will be resolved by the IoC container - /// - /// Gets or sets the user manager. - /// - /// The user manager. - public IUserManager UserManager { get; set; } - - public ISessionManager SessionManager { get; set; } - - /// - /// Gets or sets the logger. - /// - /// The logger. - public ILogger Logger { get; set; } - - /// - /// The request filter is executed before the service. - /// - /// The http request wrapper - /// The http response wrapper - /// The request DTO - public void RequestFilter(IHttpRequest request, IHttpResponse response, object requestDto) - { - //This code is executed before the service - - var auth = GetAuthorization(request); - - if (auth != null) - { - User user = null; - - if (auth.ContainsKey("UserId")) - { - var userId = auth["UserId"]; - - if (!string.IsNullOrEmpty(userId)) - { - user = UserManager.GetUserById(new Guid(userId)); - } - } - - string deviceId; - string device; - string client; - string version; - - auth.TryGetValue("DeviceId", out deviceId); - auth.TryGetValue("Device", out device); - auth.TryGetValue("Client", out client); - auth.TryGetValue("Version", out version); - - if (!string.IsNullOrEmpty(client) && !string.IsNullOrEmpty(deviceId) && !string.IsNullOrEmpty(device) && !string.IsNullOrEmpty(version)) - { - SessionManager.LogConnectionActivity(client, version, deviceId, device, user); - } - } - } - - /// - /// Gets the auth. - /// - /// The HTTP req. - /// Dictionary{System.StringSystem.String}. - public static Dictionary GetAuthorization(IHttpRequest httpReq) - { - var auth = httpReq.Headers[HttpHeaders.Authorization]; - - return GetAuthorization(auth); - } - - /// - /// Gets the authorization. - /// - /// The HTTP req. - /// Dictionary{System.StringSystem.String}. - public static Dictionary GetAuthorization(IRequestContext httpReq) - { - var auth = httpReq.GetHeader("Authorization"); - - return GetAuthorization(auth); - } - - /// - /// Gets the authorization. - /// - /// The authorization header. - /// Dictionary{System.StringSystem.String}. - private static Dictionary GetAuthorization(string authorizationHeader) - { - if (authorizationHeader == null) return null; - - var parts = authorizationHeader.Split(' '); - - // There should be at least to parts - if (parts.Length < 2) return null; - - // It has to be a digest request - if (!string.Equals(parts[0], "MediaBrowser", StringComparison.OrdinalIgnoreCase)) - { - return null; - } - - // Remove uptil the first space - authorizationHeader = authorizationHeader.Substring(authorizationHeader.IndexOf(' ')); - parts = authorizationHeader.Split(','); - - var result = new Dictionary(StringComparer.OrdinalIgnoreCase); - - foreach (var item in parts) - { - var param = item.Trim().Split(new[] { '=' }, 2); - result.Add(param[0], param[1].Trim(new[] { '"' })); - } - - return result; - } - - /// - /// A new shallow copy of this filter is used on every request. - /// - /// IHasRequestFilter. - public IHasRequestFilter Copy() - { - return this; - } - - /// - /// Order in which Request Filters are executed. - /// <0 Executed before global request filters - /// >0 Executed after global request filters - /// - /// The priority. - public int Priority - { - get { return 0; } - } - } } diff --git a/MediaBrowser.Api/MediaBrowser.Api.csproj b/MediaBrowser.Api/MediaBrowser.Api.csproj index c7cca812f..995b5cdf1 100644 --- a/MediaBrowser.Api/MediaBrowser.Api.csproj +++ b/MediaBrowser.Api/MediaBrowser.Api.csproj @@ -70,6 +70,7 @@ + @@ -88,6 +89,8 @@ + + @@ -143,7 +146,9 @@ - + + + diff --git a/MediaBrowser.Api/Playback/BaseStreamingService.cs b/MediaBrowser.Api/Playback/BaseStreamingService.cs index e31a112d5..c782c243d 100644 --- a/MediaBrowser.Api/Playback/BaseStreamingService.cs +++ b/MediaBrowser.Api/Playback/BaseStreamingService.cs @@ -613,7 +613,7 @@ namespace MediaBrowser.Api.Playback EnableRaisingEvents = true }; - ApiEntryPoint.Instance.OnTranscodeBeginning(outputPath, TranscodingJobType, process, video != null, state.Request.StartTimeTicks); + ApiEntryPoint.Instance.OnTranscodeBeginning(outputPath, TranscodingJobType, process, video != null, state.Request.StartTimeTicks, state.Item.Path); Logger.Info(process.StartInfo.FileName + " " + process.StartInfo.Arguments); diff --git a/MediaBrowser.Api/Playback/Hls/AudioHlsService.cs b/MediaBrowser.Api/Playback/Hls/AudioHlsService.cs index d7ee73a9e..6e36ba0ad 100644 --- a/MediaBrowser.Api/Playback/Hls/AudioHlsService.cs +++ b/MediaBrowser.Api/Playback/Hls/AudioHlsService.cs @@ -6,7 +6,6 @@ using MediaBrowser.Model.Dto; using MediaBrowser.Model.IO; using ServiceStack.ServiceHost; using System; -using System.IO; namespace MediaBrowser.Api.Playback.Hls { @@ -20,27 +19,6 @@ namespace MediaBrowser.Api.Playback.Hls } - /// - /// Class GetHlsAudioSegment - /// - [Route("/Audio/{Id}/hls/{SegmentId}/stream.mp3", "GET")] - [Route("/Audio/{Id}/hls/{SegmentId}/stream.aac", "GET")] - [Api(Description = "Gets an Http live streaming segment file. Internal use only.")] - public class GetHlsAudioSegment - { - /// - /// Gets or sets the id. - /// - /// The id. - public string Id { get; set; } - - /// - /// Gets or sets the segment id. - /// - /// The segment id. - public string SegmentId { get; set; } - } - /// /// Class AudioHlsService /// @@ -59,20 +37,6 @@ namespace MediaBrowser.Api.Playback.Hls { } - /// - /// Gets the specified request. - /// - /// The request. - /// System.Object. - public object Get(GetHlsAudioSegment request) - { - var file = request.SegmentId + Path.GetExtension(RequestContext.PathInfo); - - file = Path.Combine(ApplicationPaths.EncodedMediaCachePath, file); - - return ResultFactory.GetStaticFileResult(RequestContext, file, FileShare.ReadWrite); - } - /// /// Gets the specified request. /// diff --git a/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs b/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs index e680546b0..05441bba7 100644 --- a/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs +++ b/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs @@ -10,7 +10,6 @@ using MediaBrowser.Model.IO; using System; using System.Collections.Generic; using System.IO; -using System.Linq; using System.Text; using System.Threading.Tasks; @@ -213,29 +212,6 @@ namespace MediaBrowser.Api.Playback.Hls return count; } - protected void ExtendHlsTimer(string itemId, string playlistId) - { - var normalizedPlaylistId = playlistId.Replace("-low", string.Empty); - - foreach (var playlist in Directory.EnumerateFiles(ApplicationPaths.EncodedMediaCachePath, "*.m3u8") - .Where(i => i.IndexOf(normalizedPlaylistId, StringComparison.OrdinalIgnoreCase) != -1) - .ToList()) - { - ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlist, TranscodingJobType.Hls); - - // Avoid implicitly captured closure - var playlist1 = playlist; - - Task.Run(async () => - { - // This is an arbitrary time period corresponding to when the request completes. - await Task.Delay(30000).ConfigureAwait(false); - - ApiEntryPoint.Instance.OnTranscodeEndRequest(playlist1, TranscodingJobType.Hls); - }); - } - } - /// /// Gets the command line arguments. /// diff --git a/MediaBrowser.Api/Playback/Hls/HlsSegmentResponseFilter.cs b/MediaBrowser.Api/Playback/Hls/HlsSegmentResponseFilter.cs new file mode 100644 index 000000000..44996c99f --- /dev/null +++ b/MediaBrowser.Api/Playback/Hls/HlsSegmentResponseFilter.cs @@ -0,0 +1,53 @@ +using MediaBrowser.Controller; +using MediaBrowser.Model.Logging; +using ServiceStack.ServiceHost; +using ServiceStack.Text.Controller; +using System; +using System.IO; +using System.Linq; + +namespace MediaBrowser.Api.Playback.Hls +{ + public class HlsSegmentResponseFilter : Attribute, IHasResponseFilter + { + public ILogger Logger { get; set; } + public IServerApplicationPaths ApplicationPaths { get; set; } + + public void ResponseFilter(IHttpRequest req, IHttpResponse res, object response) + { + var pathInfo = PathInfo.Parse(req.PathInfo); + var itemId = pathInfo.GetArgumentValue(1); + var playlistId = pathInfo.GetArgumentValue(3); + + OnEndRequest(itemId, playlistId); + } + + public IHasResponseFilter Copy() + { + return this; + } + + public int Priority + { + get { return -1; } + } + + /// + /// Called when [end request]. + /// + /// The item id. + /// The playlist id. + protected void OnEndRequest(string itemId, string playlistId) + { + Logger.Info("OnEndRequest " + playlistId); + var normalizedPlaylistId = playlistId.Replace("-low", string.Empty); + + foreach (var playlist in Directory.EnumerateFiles(ApplicationPaths.EncodedMediaCachePath, "*.m3u8") + .Where(i => i.IndexOf(normalizedPlaylistId, StringComparison.OrdinalIgnoreCase) != -1) + .ToList()) + { + ApiEntryPoint.Instance.OnTranscodeEndRequest(playlist, TranscodingJobType.Hls); + } + } + } +} diff --git a/MediaBrowser.Api/Playback/Hls/HlsSegmentService.cs b/MediaBrowser.Api/Playback/Hls/HlsSegmentService.cs new file mode 100644 index 000000000..f1fa86f78 --- /dev/null +++ b/MediaBrowser.Api/Playback/Hls/HlsSegmentService.cs @@ -0,0 +1,147 @@ +using MediaBrowser.Controller; +using ServiceStack.ServiceHost; +using System; +using System.IO; +using System.Linq; +using System.Threading.Tasks; + +namespace MediaBrowser.Api.Playback.Hls +{ + /// + /// Class GetHlsAudioSegment + /// + [Route("/Audio/{Id}/hls/{SegmentId}/stream.mp3", "GET")] + [Route("/Audio/{Id}/hls/{SegmentId}/stream.aac", "GET")] + [Api(Description = "Gets an Http live streaming segment file. Internal use only.")] + public class GetHlsAudioSegment + { + /// + /// Gets or sets the id. + /// + /// The id. + public string Id { get; set; } + + /// + /// Gets or sets the segment id. + /// + /// The segment id. + public string SegmentId { get; set; } + } + + /// + /// Class GetHlsVideoSegment + /// + [Route("/Videos/{Id}/hls/{PlaylistId}/{SegmentId}.ts", "GET")] + [Api(Description = "Gets an Http live streaming segment file. Internal use only.")] + public class GetHlsVideoSegment + { + /// + /// Gets or sets the id. + /// + /// The id. + public string Id { get; set; } + + public string PlaylistId { get; set; } + + /// + /// Gets or sets the segment id. + /// + /// The segment id. + public string SegmentId { get; set; } + } + + /// + /// Class GetHlsVideoSegment + /// + [Route("/Videos/{Id}/hls/{PlaylistId}/stream.m3u8", "GET")] + [Api(Description = "Gets an Http live streaming segment file. Internal use only.")] + public class GetHlsPlaylist + { + /// + /// Gets or sets the id. + /// + /// The id. + public string Id { get; set; } + + public string PlaylistId { get; set; } + } + + public class HlsSegmentService : BaseApiService + { + private readonly IServerApplicationPaths _appPaths; + + public HlsSegmentService(IServerApplicationPaths appPaths) + { + _appPaths = appPaths; + } + + public object Get(GetHlsPlaylist request) + { + OnBeginRequest(request.PlaylistId); + + var file = request.PlaylistId + Path.GetExtension(RequestContext.PathInfo); + + file = Path.Combine(_appPaths.EncodedMediaCachePath, file); + + return ResultFactory.GetStaticFileResult(RequestContext, file, FileShare.ReadWrite); + } + + /// + /// Gets the specified request. + /// + /// The request. + /// System.Object. + public object Get(GetHlsVideoSegment request) + { + var file = request.SegmentId + Path.GetExtension(RequestContext.PathInfo); + + file = Path.Combine(_appPaths.EncodedMediaCachePath, file); + + OnBeginRequest(request.PlaylistId); + + return ResultFactory.GetStaticFileResult(RequestContext, file); + } + + /// + /// Gets the specified request. + /// + /// The request. + /// System.Object. + public object Get(GetHlsAudioSegment request) + { + var file = request.SegmentId + Path.GetExtension(RequestContext.PathInfo); + + file = Path.Combine(_appPaths.EncodedMediaCachePath, file); + + return ResultFactory.GetStaticFileResult(RequestContext, file, FileShare.ReadWrite); + } + + /// + /// Called when [begin request]. + /// + /// The playlist id. + protected void OnBeginRequest(string playlistId) + { + var normalizedPlaylistId = playlistId.Replace("-low", string.Empty); + + foreach (var playlist in Directory.EnumerateFiles(_appPaths.EncodedMediaCachePath, "*.m3u8") + .Where(i => i.IndexOf(normalizedPlaylistId, StringComparison.OrdinalIgnoreCase) != -1) + .ToList()) + { + ExtendPlaylistTimer(playlist); + } + } + + private void ExtendPlaylistTimer(string playlist) + { + ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlist, TranscodingJobType.Hls); + + Task.Run(async () => + { + await Task.Delay(20000).ConfigureAwait(false); + + ApiEntryPoint.Instance.OnTranscodeEndRequest(playlist, TranscodingJobType.Hls); + }); + } + } +} diff --git a/MediaBrowser.Api/Playback/Hls/VideoHlsService.cs b/MediaBrowser.Api/Playback/Hls/VideoHlsService.cs index 901b27688..4694b68a1 100644 --- a/MediaBrowser.Api/Playback/Hls/VideoHlsService.cs +++ b/MediaBrowser.Api/Playback/Hls/VideoHlsService.cs @@ -5,7 +5,6 @@ using MediaBrowser.Controller.Library; using MediaBrowser.Model.IO; using ServiceStack.ServiceHost; using System; -using System.IO; namespace MediaBrowser.Api.Playback.Hls { @@ -31,44 +30,6 @@ namespace MediaBrowser.Api.Playback.Hls } } - /// - /// Class GetHlsVideoSegment - /// - [Route("/Videos/{Id}/hls/{PlaylistId}/{SegmentId}.ts", "GET")] - [Api(Description = "Gets an Http live streaming segment file. Internal use only.")] - public class GetHlsVideoSegment - { - /// - /// Gets or sets the id. - /// - /// The id. - public string Id { get; set; } - - public string PlaylistId { get; set; } - - /// - /// Gets or sets the segment id. - /// - /// The segment id. - public string SegmentId { get; set; } - } - - /// - /// Class GetHlsVideoSegment - /// - [Route("/Videos/{Id}/hls/{PlaylistId}/stream.m3u8", "GET")] - [Api(Description = "Gets an Http live streaming segment file. Internal use only.")] - public class GetHlsPlaylist - { - /// - /// Gets or sets the id. - /// - /// The id. - public string Id { get; set; } - - public string PlaylistId { get; set; } - } - /// /// Class VideoHlsService /// @@ -82,38 +43,12 @@ namespace MediaBrowser.Api.Playback.Hls /// The library manager. /// The iso manager. /// The media encoder. + /// The dto service. public VideoHlsService(IServerApplicationPaths appPaths, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IDtoService dtoService) : base(appPaths, userManager, libraryManager, isoManager, mediaEncoder, dtoService) { } - /// - /// Gets the specified request. - /// - /// The request. - /// System.Object. - public object Get(GetHlsVideoSegment request) - { - ExtendHlsTimer(request.Id, request.PlaylistId); - - var file = request.SegmentId + Path.GetExtension(RequestContext.PathInfo); - - file = Path.Combine(ApplicationPaths.EncodedMediaCachePath, file); - - return ResultFactory.GetStaticFileResult(RequestContext, file); - } - - public object Get(GetHlsPlaylist request) - { - ExtendHlsTimer(request.Id, request.PlaylistId); - - var file = request.PlaylistId + Path.GetExtension(RequestContext.PathInfo); - - file = Path.Combine(ApplicationPaths.EncodedMediaCachePath, file); - - return ResultFactory.GetStaticFileResult(RequestContext, file, FileShare.ReadWrite); - } - /// /// Gets the specified request. /// diff --git a/MediaBrowser.Api/SessionsService.cs b/MediaBrowser.Api/SessionsService.cs index b93b5326e..5888d9fba 100644 --- a/MediaBrowser.Api/SessionsService.cs +++ b/MediaBrowser.Api/SessionsService.cs @@ -1,7 +1,5 @@ -using MediaBrowser.Common.Extensions; -using MediaBrowser.Controller.Dto; +using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Session; -using MediaBrowser.Model.Net; using MediaBrowser.Model.Session; using ServiceStack.ServiceHost; using System; @@ -189,6 +187,7 @@ namespace MediaBrowser.Api /// Initializes a new instance of the class. /// /// The session manager. + /// The dto service. public SessionsService(ISessionManager sessionManager, IDtoService dtoService) { _sessionManager = sessionManager; @@ -214,111 +213,36 @@ namespace MediaBrowser.Api public void Post(SendPlaystateCommand request) { - var task = SendPlaystateCommand(request); + var command = new PlaystateRequest + { + Command = request.Command, + SeekPositionTicks = request.SeekPositionTicks + }; + + var task = _sessionManager.SendPlaystateCommand(request.Id, command, CancellationToken.None); Task.WaitAll(task); } - private async Task SendPlaystateCommand(SendPlaystateCommand request) - { - var session = _sessionManager.Sessions.FirstOrDefault(i => i.Id == request.Id); - - if (session == null) - { - throw new ResourceNotFoundException(string.Format("Session {0} not found.", request.Id)); - } - - if (!session.SupportsRemoteControl) - { - throw new ArgumentException(string.Format("Session {0} does not support remote control.", session.Id)); - } - - var socket = session.WebSockets.OrderByDescending(i => i.LastActivityDate).FirstOrDefault(i => i.State == WebSocketState.Open); - - if (socket != null) - { - try - { - await socket.SendAsync(new WebSocketMessage - { - MessageType = "Playstate", - - Data = new PlaystateRequest - { - Command = request.Command, - SeekPositionTicks = request.SeekPositionTicks - } - - }, CancellationToken.None).ConfigureAwait(false); - } - catch (Exception ex) - { - Logger.ErrorException("Error sending web socket message", ex); - } - } - else - { - throw new InvalidOperationException("The requested session does not have an open web socket."); - } - } - /// /// Posts the specified request. /// /// The request. public void Post(BrowseTo request) { - var task = BrowseTo(request); + var command = new BrowseRequest + { + Context = request.Context, + ItemId = request.ItemId, + ItemName = request.ItemName, + ItemType = request.ItemType + }; + + var task = _sessionManager.SendBrowseCommand(request.Id, command, CancellationToken.None); Task.WaitAll(task); } - /// - /// Browses to. - /// - /// The request. - /// Task. - /// - /// - /// The requested session does not have an open web socket. - private async Task BrowseTo(BrowseTo request) - { - var session = _sessionManager.Sessions.FirstOrDefault(i => i.Id == request.Id); - - if (session == null) - { - throw new ResourceNotFoundException(string.Format("Session {0} not found.", request.Id)); - } - - if (!session.SupportsRemoteControl) - { - throw new ArgumentException(string.Format("Session {0} does not support remote control.", session.Id)); - } - - var socket = session.WebSockets.OrderByDescending(i => i.LastActivityDate).FirstOrDefault(i => i.State == WebSocketState.Open); - - if (socket != null) - { - try - { - await socket.SendAsync(new WebSocketMessage - { - MessageType = "Browse", - Data = request - - }, CancellationToken.None).ConfigureAwait(false); - } - catch (Exception ex) - { - Logger.ErrorException("Error sending web socket message", ex); - } - } - else - { - throw new InvalidOperationException("The requested session does not have an open web socket."); - } - } - /// /// Posts the specified request. /// @@ -336,117 +260,35 @@ namespace MediaBrowser.Api /// The request. public void Post(SendMessageCommand request) { - var task = SendMessageCommand(request); + var command = new MessageCommand + { + Header = string.IsNullOrEmpty(request.Header) ? "Message from Server" : request.Header, + TimeoutMs = request.TimeoutMs, + Text = request.Text + }; + + var task = _sessionManager.SendMessageCommand(request.Id, command, CancellationToken.None); Task.WaitAll(task); } - private async Task SendMessageCommand(SendMessageCommand request) - { - var session = _sessionManager.Sessions.FirstOrDefault(i => i.Id == request.Id); - - if (session == null) - { - throw new ResourceNotFoundException(string.Format("Session {0} not found.", request.Id)); - } - - if (!session.SupportsRemoteControl) - { - throw new ArgumentException(string.Format("Session {0} does not support remote control.", session.Id)); - } - - var socket = session.WebSockets.OrderByDescending(i => i.LastActivityDate).FirstOrDefault(i => i.State == WebSocketState.Open); - - if (socket != null) - { - try - { - await socket.SendAsync(new WebSocketMessage - { - MessageType = "MessageCommand", - - Data = new MessageCommand - { - Header = string.IsNullOrEmpty(request.Header) ? "Message from Server" : request.Header, - TimeoutMs = request.TimeoutMs, - Text = request.Text - } - - }, CancellationToken.None).ConfigureAwait(false); - } - catch (Exception ex) - { - Logger.ErrorException("Error sending web socket message", ex); - } - } - else - { - throw new InvalidOperationException("The requested session does not have an open web socket."); - } - } - /// /// Posts the specified request. /// /// The request. public void Post(Play request) { - var task = Play(request); + var command = new PlayRequest + { + ItemIds = request.ItemIds.Split(',').ToArray(), + + PlayCommand = request.PlayCommand, + StartPositionTicks = request.StartPositionTicks + }; + + var task = _sessionManager.SendPlayCommand(request.Id, command, CancellationToken.None); Task.WaitAll(task); } - - /// - /// Plays the specified request. - /// - /// The request. - /// Task. - /// - /// - /// The requested session does not have an open web socket. - private async Task Play(Play request) - { - var session = _sessionManager.Sessions.FirstOrDefault(i => i.Id == request.Id); - - if (session == null) - { - throw new ResourceNotFoundException(string.Format("Session {0} not found.", request.Id)); - } - - if (!session.SupportsRemoteControl) - { - throw new ArgumentException(string.Format("Session {0} does not support remote control.", session.Id)); - } - - var socket = session.WebSockets.OrderByDescending(i => i.LastActivityDate).FirstOrDefault(i => i.State == WebSocketState.Open); - - if (socket != null) - { - try - { - await socket.SendAsync(new WebSocketMessage - { - MessageType = "Play", - - Data = new PlayRequest - { - ItemIds = request.ItemIds.Split(',').ToArray(), - - PlayCommand = request.PlayCommand, - StartPositionTicks = request.StartPositionTicks - } - - }, CancellationToken.None).ConfigureAwait(false); - } - catch (Exception ex) - { - Logger.ErrorException("Error sending web socket message", ex); - } - } - else - { - throw new InvalidOperationException("The requested session does not have an open web socket."); - } - } } } diff --git a/MediaBrowser.Api/UserLibrary/UserLibraryService.cs b/MediaBrowser.Api/UserLibrary/UserLibraryService.cs index 9085a3ecf..abd42910f 100644 --- a/MediaBrowser.Api/UserLibrary/UserLibraryService.cs +++ b/MediaBrowser.Api/UserLibrary/UserLibraryService.cs @@ -663,19 +663,11 @@ namespace MediaBrowser.Api.UserLibrary private SessionInfo GetSession() { - var auth = RequestFilterAttribute.GetAuthorization(RequestContext); + var auth = AuthorizationRequestFilterAttribute.GetAuthorization(RequestContext); - string deviceId; - string client; - string version; - - auth.TryGetValue("DeviceId", out deviceId); - auth.TryGetValue("Client", out client); - auth.TryGetValue("Version", out version); - - return _sessionManager.Sessions.First(i => string.Equals(i.DeviceId, deviceId) && - string.Equals(i.Client, client) && - string.Equals(i.ApplicationVersion, version)); + return _sessionManager.Sessions.First(i => string.Equals(i.DeviceId, auth.DeviceId) && + string.Equals(i.Client, auth.Client) && + string.Equals(i.ApplicationVersion, auth.Version)); } /// @@ -726,7 +718,12 @@ namespace MediaBrowser.Api.UserLibrary var item = _dtoService.GetItemByDtoId(request.Id, user.Id); - var task = _sessionManager.OnPlaybackStopped(item, request.PositionTicks, GetSession().Id); + // Kill the encoding + ApiEntryPoint.Instance.KillSingleTranscodingJob(item.Path); + + var session = GetSession(); + + var task = _sessionManager.OnPlaybackStopped(item, request.PositionTicks, session.Id); Task.WaitAll(task); } diff --git a/MediaBrowser.Controller/Session/ISessionManager.cs b/MediaBrowser.Controller/Session/ISessionManager.cs index f8f7ded2b..0932ee52d 100644 --- a/MediaBrowser.Controller/Session/ISessionManager.cs +++ b/MediaBrowser.Controller/Session/ISessionManager.cs @@ -89,5 +89,41 @@ namespace MediaBrowser.Controller.Session /// The cancellation token. /// Task. Task SendSystemCommand(Guid sessionId, SystemCommand command, CancellationToken cancellationToken); + + /// + /// Sends the message command. + /// + /// The session id. + /// The command. + /// The cancellation token. + /// Task. + Task SendMessageCommand(Guid sessionId, MessageCommand command, CancellationToken cancellationToken); + + /// + /// Sends the play command. + /// + /// The session id. + /// The command. + /// The cancellation token. + /// Task. + Task SendPlayCommand(Guid sessionId, PlayRequest command, CancellationToken cancellationToken); + + /// + /// Sends the browse command. + /// + /// The session id. + /// The command. + /// The cancellation token. + /// Task. + Task SendBrowseCommand(Guid sessionId, BrowseRequest command, CancellationToken cancellationToken); + + /// + /// Sends the playstate command. + /// + /// The session id. + /// The command. + /// The cancellation token. + /// Task. + Task SendPlaystateCommand(Guid sessionId, PlaystateRequest command, CancellationToken cancellationToken); } } \ No newline at end of file diff --git a/MediaBrowser.Controller/Session/ISessionRemoteController.cs b/MediaBrowser.Controller/Session/ISessionRemoteController.cs index 1f6faeb9c..9ba5c983d 100644 --- a/MediaBrowser.Controller/Session/ISessionRemoteController.cs +++ b/MediaBrowser.Controller/Session/ISessionRemoteController.cs @@ -21,5 +21,41 @@ namespace MediaBrowser.Controller.Session /// The cancellation token. /// Task. Task SendSystemCommand(SessionInfo session, SystemCommand command, CancellationToken cancellationToken); + + /// + /// Sends the message command. + /// + /// The session. + /// The command. + /// The cancellation token. + /// Task. + Task SendMessageCommand(SessionInfo session, MessageCommand command, CancellationToken cancellationToken); + + /// + /// Sends the play command. + /// + /// The session. + /// The command. + /// The cancellation token. + /// Task. + Task SendPlayCommand(SessionInfo session, PlayRequest command, CancellationToken cancellationToken); + + /// + /// Sends the browse command. + /// + /// The session. + /// The command. + /// The cancellation token. + /// Task. + Task SendBrowseCommand(SessionInfo session, BrowseRequest command, CancellationToken cancellationToken); + + /// + /// Sends the playstate command. + /// + /// The session. + /// The command. + /// The cancellation token. + /// Task. + Task SendPlaystateCommand(SessionInfo session, PlaystateRequest command, CancellationToken cancellationToken); } } diff --git a/MediaBrowser.Server.Implementations/Session/SessionManager.cs b/MediaBrowser.Server.Implementations/Session/SessionManager.cs index 5b0d957ae..79dfbc8a5 100644 --- a/MediaBrowser.Server.Implementations/Session/SessionManager.cs +++ b/MediaBrowser.Server.Implementations/Session/SessionManager.cs @@ -465,5 +465,69 @@ namespace MediaBrowser.Server.Implementations.Session return Task.WhenAll(tasks); } + + /// + /// Sends the message command. + /// + /// The session id. + /// The command. + /// The cancellation token. + /// Task. + public Task SendMessageCommand(Guid sessionId, MessageCommand command, CancellationToken cancellationToken) + { + var session = GetSessionForRemoteControl(sessionId); + + var tasks = GetControllers(session).Select(i => i.SendMessageCommand(session, command, cancellationToken)); + + return Task.WhenAll(tasks); + } + + /// + /// Sends the play command. + /// + /// The session id. + /// The command. + /// The cancellation token. + /// Task. + public Task SendPlayCommand(Guid sessionId, PlayRequest command, CancellationToken cancellationToken) + { + var session = GetSessionForRemoteControl(sessionId); + + var tasks = GetControllers(session).Select(i => i.SendPlayCommand(session, command, cancellationToken)); + + return Task.WhenAll(tasks); + } + + /// + /// Sends the browse command. + /// + /// The session id. + /// The command. + /// The cancellation token. + /// Task. + public Task SendBrowseCommand(Guid sessionId, BrowseRequest command, CancellationToken cancellationToken) + { + var session = GetSessionForRemoteControl(sessionId); + + var tasks = GetControllers(session).Select(i => i.SendBrowseCommand(session, command, cancellationToken)); + + return Task.WhenAll(tasks); + } + + /// + /// Sends the playstate command. + /// + /// The session id. + /// The command. + /// The cancellation token. + /// Task. + public Task SendPlaystateCommand(Guid sessionId, PlaystateRequest command, CancellationToken cancellationToken) + { + var session = GetSessionForRemoteControl(sessionId); + + var tasks = GetControllers(session).Select(i => i.SendPlaystateCommand(session, command, cancellationToken)); + + return Task.WhenAll(tasks); + } } } diff --git a/MediaBrowser.Server.Implementations/Session/WebSocketController.cs b/MediaBrowser.Server.Implementations/Session/WebSocketController.cs index daa4c7d81..6915cfc64 100644 --- a/MediaBrowser.Server.Implementations/Session/WebSocketController.cs +++ b/MediaBrowser.Server.Implementations/Session/WebSocketController.cs @@ -1,5 +1,5 @@ -using MediaBrowser.Controller.Session; -using MediaBrowser.Model.Logging; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller.Session; using MediaBrowser.Model.Net; using MediaBrowser.Model.Session; using System; @@ -11,42 +11,82 @@ namespace MediaBrowser.Server.Implementations.Session { public class WebSocketController : ISessionRemoteController { - private readonly ILogger _logger; - - public WebSocketController(ILogger logger) - { - _logger = logger; - } - public bool Supports(SessionInfo session) { return session.WebSockets.Any(i => i.State == WebSocketState.Open); } - public async Task SendSystemCommand(SessionInfo session, SystemCommand command, CancellationToken cancellationToken) + private IWebSocketConnection GetSocket(SessionInfo session) { var socket = session.WebSockets.OrderByDescending(i => i.LastActivityDate).FirstOrDefault(i => i.State == WebSocketState.Open); - if (socket != null) - { - try - { - await socket.SendAsync(new WebSocketMessage - { - MessageType = "SystemCommand", - Data = command.ToString() - }, cancellationToken).ConfigureAwait(false); - } - catch (Exception ex) - { - _logger.ErrorException("Error sending web socket message", ex); - } - } - else + if (socket == null) { throw new InvalidOperationException("The requested session does not have an open web socket."); } + + return socket; + } + + public Task SendSystemCommand(SessionInfo session, SystemCommand command, CancellationToken cancellationToken) + { + var socket = GetSocket(session); + + return socket.SendAsync(new WebSocketMessage + { + MessageType = "SystemCommand", + Data = command.ToString() + + }, cancellationToken); + } + + public Task SendMessageCommand(SessionInfo session, MessageCommand command, CancellationToken cancellationToken) + { + var socket = GetSocket(session); + + return socket.SendAsync(new WebSocketMessage + { + MessageType = "MessageCommand", + Data = command + + }, cancellationToken); + } + + public Task SendPlayCommand(SessionInfo session, PlayRequest command, CancellationToken cancellationToken) + { + var socket = GetSocket(session); + + return socket.SendAsync(new WebSocketMessage + { + MessageType = "Play", + Data = command + + }, cancellationToken); + } + + public Task SendBrowseCommand(SessionInfo session, BrowseRequest command, CancellationToken cancellationToken) + { + var socket = GetSocket(session); + + return socket.SendAsync(new WebSocketMessage + { + MessageType = "Browse", + Data = command + + }, cancellationToken); + } + + public Task SendPlaystateCommand(SessionInfo session, PlaystateRequest command, CancellationToken cancellationToken) + { + var socket = GetSocket(session); + + return socket.SendAsync(new WebSocketMessage + { + MessageType = "Playstate", + Data = command + + }, cancellationToken); } } } From 0ab379e271afe69372806ab0e24a874d3f085456 Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Tue, 24 Sep 2013 17:06:21 -0400 Subject: [PATCH 18/21] adding mono solution --- MediaBrowser.Api/MediaBrowser.Api.csproj | 38 ++++---- ...MediaBrowser.Common.Implementations.csproj | 27 +++--- .../MediaBrowser.Common.csproj | 29 +++--- .../MediaBrowser.Controller.csproj | 16 ++-- MediaBrowser.Model/MediaBrowser.Model.csproj | 11 ++- MediaBrowser.Mono.sln | 68 ++++++++++++++ MediaBrowser.Mono.userprefs | 8 ++ .../MediaBrowser.Providers.csproj | 15 +-- .../Library/Validators/GameGenresValidator.cs | 6 +- .../Library/Validators/GenresValidator.cs | 10 +- .../Validators/MusicGenresValidator.cs | 10 +- .../Library/Validators/StudiosValidator.cs | 8 +- ...MediaBrowser.Server.Implementations.csproj | 94 +++++++++---------- MediaBrowser.Server.Mono/MainWindow.cs | 16 ++++ .../MediaBrowser.Server.Mono.csproj | 91 ++++++++++++++++++ MediaBrowser.Server.Mono/Program.cs | 16 ++++ .../Properties/AssemblyInfo.cs | 22 +++++ .../gtk-gui/MainWindow.cs | 20 ++++ MediaBrowser.Server.Mono/gtk-gui/generated.cs | 29 ++++++ MediaBrowser.Server.Mono/gtk-gui/gui.stetic | 19 ++++ .../MediaBrowser.WebDashboard.csproj | 29 +++--- 21 files changed, 428 insertions(+), 154 deletions(-) create mode 100644 MediaBrowser.Mono.sln create mode 100644 MediaBrowser.Mono.userprefs create mode 100644 MediaBrowser.Server.Mono/MainWindow.cs create mode 100644 MediaBrowser.Server.Mono/MediaBrowser.Server.Mono.csproj create mode 100644 MediaBrowser.Server.Mono/Program.cs create mode 100644 MediaBrowser.Server.Mono/Properties/AssemblyInfo.cs create mode 100644 MediaBrowser.Server.Mono/gtk-gui/MainWindow.cs create mode 100644 MediaBrowser.Server.Mono/gtk-gui/generated.cs create mode 100644 MediaBrowser.Server.Mono/gtk-gui/gui.stetic diff --git a/MediaBrowser.Api/MediaBrowser.Api.csproj b/MediaBrowser.Api/MediaBrowser.Api.csproj index 995b5cdf1..4f54b5249 100644 --- a/MediaBrowser.Api/MediaBrowser.Api.csproj +++ b/MediaBrowser.Api/MediaBrowser.Api.csproj @@ -13,6 +13,8 @@ 512 ..\ true + 10.0.0 + 2.0 true @@ -36,29 +38,25 @@ Always - - False - ..\packages\morelinq.1.0.16006\lib\net35\MoreLinq.dll - - - False - ..\packages\ServiceStack.Common.3.9.62\lib\net35\ServiceStack.Common.dll - - - False - ..\packages\ServiceStack.Common.3.9.62\lib\net35\ServiceStack.Interfaces.dll - - - False - ..\packages\ServiceStack.Text.3.9.62\lib\net35\ServiceStack.Text.dll - - + + ..\packages\morelinq.1.0.16006\lib\net35\MoreLinq.dll + + + ..\packages\ServiceStack.Common.3.9.62\lib\net35\ServiceStack.Common.dll + + + ..\packages\ServiceStack.Common.3.9.62\lib\net35\ServiceStack.Interfaces.dll + + + ..\packages\ServiceStack.Text.3.9.62\lib\net35\ServiceStack.Text.dll + + @@ -131,15 +129,15 @@ - {9142eefa-7570-41e1-bfcc-468bb571af2f} + {9142EEFA-7570-41E1-BFCC-468BB571AF2F} MediaBrowser.Common - {17e1f4e6-8abd-4fe5-9ecf-43d4b6087ba2} + {17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2} MediaBrowser.Controller - {7eeeb4bb-f3e8-48fc-b4c5-70f0fff8329b} + {7EEEB4BB-F3E8-48FC-B4C5-70F0FFF8329B} MediaBrowser.Model diff --git a/MediaBrowser.Common.Implementations/MediaBrowser.Common.Implementations.csproj b/MediaBrowser.Common.Implementations/MediaBrowser.Common.Implementations.csproj index a96f2c354..79514b5cb 100644 --- a/MediaBrowser.Common.Implementations/MediaBrowser.Common.Implementations.csproj +++ b/MediaBrowser.Common.Implementations/MediaBrowser.Common.Implementations.csproj @@ -13,6 +13,8 @@ 512 ..\ true + 10.0.0 + 2.0 true @@ -35,18 +37,6 @@ Always - - False - ..\packages\NLog.2.0.1.2\lib\net45\NLog.dll - - - False - ..\packages\ServiceStack.Text.3.9.62\lib\net35\ServiceStack.Text.dll - - - False - ..\packages\SimpleInjector.2.3.5\lib\net40-client\SimpleInjector.dll - @@ -54,6 +44,15 @@ + + ..\packages\NLog.2.0.1.2\lib\net45\NLog.dll + + + ..\packages\ServiceStack.Text.3.9.62\lib\net35\ServiceStack.Text.dll + + + ..\packages\SimpleInjector.2.3.5\lib\net40-client\SimpleInjector.dll + @@ -88,11 +87,11 @@ - {9142eefa-7570-41e1-bfcc-468bb571af2f} + {9142EEFA-7570-41E1-BFCC-468BB571AF2F} MediaBrowser.Common - {7eeeb4bb-f3e8-48fc-b4c5-70f0fff8329b} + {7EEEB4BB-F3E8-48FC-B4C5-70F0FFF8329B} MediaBrowser.Model diff --git a/MediaBrowser.Common/MediaBrowser.Common.csproj b/MediaBrowser.Common/MediaBrowser.Common.csproj index 1611c55da..8acd1a83c 100644 --- a/MediaBrowser.Common/MediaBrowser.Common.csproj +++ b/MediaBrowser.Common/MediaBrowser.Common.csproj @@ -13,6 +13,8 @@ 512 ..\ true + 10.0.0 + 2.0 true @@ -32,26 +34,19 @@ prompt 4 - - - - - - False - ..\packages\ServiceStack.Common.3.9.62\lib\net35\ServiceStack.Common.dll - - - False - ..\packages\ServiceStack.Common.3.9.62\lib\net35\ServiceStack.Interfaces.dll - - - False - ..\packages\ServiceStack.Text.3.9.62\lib\net35\ServiceStack.Text.dll - + + ..\packages\ServiceStack.Common.3.9.62\lib\net35\ServiceStack.Common.dll + + + ..\packages\ServiceStack.Common.3.9.62\lib\net35\ServiceStack.Interfaces.dll + + + ..\packages\ServiceStack.Text.3.9.62\lib\net35\ServiceStack.Text.dll + @@ -113,7 +108,7 @@ - {7eeeb4bb-f3e8-48fc-b4c5-70f0fff8329b} + {7EEEB4BB-F3E8-48FC-B4C5-70F0FFF8329B} MediaBrowser.Model diff --git a/MediaBrowser.Controller/MediaBrowser.Controller.csproj b/MediaBrowser.Controller/MediaBrowser.Controller.csproj index b5ad862be..0b27a350b 100644 --- a/MediaBrowser.Controller/MediaBrowser.Controller.csproj +++ b/MediaBrowser.Controller/MediaBrowser.Controller.csproj @@ -13,6 +13,8 @@ 512 ..\ true + 10.0.0 + 2.0 true @@ -42,6 +44,8 @@ x86 prompt MinimumRecommendedRules.ruleset + 4 + false bin\x86\Release\ @@ -51,12 +55,9 @@ x86 prompt MinimumRecommendedRules.ruleset + 4 - - False - ..\packages\morelinq.1.0.16006\lib\net35\MoreLinq.dll - @@ -66,6 +67,9 @@ + + ..\packages\morelinq.1.0.16006\lib\net35\MoreLinq.dll + @@ -174,11 +178,11 @@ - {9142eefa-7570-41e1-bfcc-468bb571af2f} + {9142EEFA-7570-41E1-BFCC-468BB571AF2F} MediaBrowser.Common - {7eeeb4bb-f3e8-48fc-b4c5-70f0fff8329b} + {7EEEB4BB-F3E8-48FC-B4C5-70F0FFF8329B} MediaBrowser.Model diff --git a/MediaBrowser.Model/MediaBrowser.Model.csproj b/MediaBrowser.Model/MediaBrowser.Model.csproj index 8fb471c2d..fa4fc2986 100644 --- a/MediaBrowser.Model/MediaBrowser.Model.csproj +++ b/MediaBrowser.Model/MediaBrowser.Model.csproj @@ -14,6 +14,8 @@ ..\ true ..\packages\Fody.1.17.0.0 + 10.0.0 + 2.0 true @@ -162,14 +164,13 @@ - - False - ..\packages\PropertyChanged.Fody.1.41.0.0\Lib\NET35\PropertyChanged.dll - False - + + ..\packages\PropertyChanged.Fody.1.41.0.0\Lib\NET35\PropertyChanged.dll + False + diff --git a/MediaBrowser.Mono.sln b/MediaBrowser.Mono.sln new file mode 100644 index 000000000..0dc78ca2a --- /dev/null +++ b/MediaBrowser.Mono.sln @@ -0,0 +1,68 @@ + +Microsoft Visual Studio Solution File, Format Version 11.00 +# Visual Studio 2010 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MediaBrowser.Model", "MediaBrowser.Model\MediaBrowser.Model.csproj", "{7EEEB4BB-F3E8-48FC-B4C5-70F0FFF8329B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MediaBrowser.Common", "MediaBrowser.Common\MediaBrowser.Common.csproj", "{9142EEFA-7570-41E1-BFCC-468BB571AF2F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MediaBrowser.Common.Implementations", "MediaBrowser.Common.Implementations\MediaBrowser.Common.Implementations.csproj", "{C4D2573A-3FD3-441F-81AF-174AC4CD4E1D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MediaBrowser.Controller", "MediaBrowser.Controller\MediaBrowser.Controller.csproj", "{17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MediaBrowser.Providers", "MediaBrowser.Providers\MediaBrowser.Providers.csproj", "{442B5058-DCAF-4263-BB6A-F21E31120A1B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MediaBrowser.Server.Implementations", "MediaBrowser.Server.Implementations\MediaBrowser.Server.Implementations.csproj", "{2E781478-814D-4A48-9D80-BFF206441A65}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MediaBrowser.WebDashboard", "MediaBrowser.WebDashboard\MediaBrowser.WebDashboard.csproj", "{5624B7B5-B5A7-41D8-9F10-CC5611109619}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MediaBrowser.Api", "MediaBrowser.Api\MediaBrowser.Api.csproj", "{4FD51AC5-2C16-4308-A993-C3A84F3B4582}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MediaBrowser.Server.Mono", "MediaBrowser.Server.Mono\MediaBrowser.Server.Mono.csproj", "{A7FE75CD-3CB4-4E71-A5BF-5347721EC8E0}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|x86 = Debug|x86 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}.Debug|x86.ActiveCfg = Debug|x86 + {17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}.Debug|x86.Build.0 = Debug|x86 + {17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}.Release|x86.ActiveCfg = Release|x86 + {17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}.Release|x86.Build.0 = Release|x86 + {2E781478-814D-4A48-9D80-BFF206441A65}.Debug|x86.ActiveCfg = Debug|Any CPU + {2E781478-814D-4A48-9D80-BFF206441A65}.Debug|x86.Build.0 = Debug|Any CPU + {2E781478-814D-4A48-9D80-BFF206441A65}.Release|x86.ActiveCfg = Release|Any CPU + {2E781478-814D-4A48-9D80-BFF206441A65}.Release|x86.Build.0 = Release|Any CPU + {442B5058-DCAF-4263-BB6A-F21E31120A1B}.Debug|x86.ActiveCfg = Debug|Any CPU + {442B5058-DCAF-4263-BB6A-F21E31120A1B}.Debug|x86.Build.0 = Debug|Any CPU + {442B5058-DCAF-4263-BB6A-F21E31120A1B}.Release|x86.ActiveCfg = Release|Any CPU + {442B5058-DCAF-4263-BB6A-F21E31120A1B}.Release|x86.Build.0 = Release|Any CPU + {4FD51AC5-2C16-4308-A993-C3A84F3B4582}.Debug|x86.ActiveCfg = Debug|Any CPU + {4FD51AC5-2C16-4308-A993-C3A84F3B4582}.Debug|x86.Build.0 = Debug|Any CPU + {4FD51AC5-2C16-4308-A993-C3A84F3B4582}.Release|x86.ActiveCfg = Release|Any CPU + {4FD51AC5-2C16-4308-A993-C3A84F3B4582}.Release|x86.Build.0 = Release|Any CPU + {5624B7B5-B5A7-41D8-9F10-CC5611109619}.Debug|x86.ActiveCfg = Debug|Any CPU + {5624B7B5-B5A7-41D8-9F10-CC5611109619}.Debug|x86.Build.0 = Debug|Any CPU + {5624B7B5-B5A7-41D8-9F10-CC5611109619}.Release|x86.ActiveCfg = Release|Any CPU + {5624B7B5-B5A7-41D8-9F10-CC5611109619}.Release|x86.Build.0 = Release|Any CPU + {7EEEB4BB-F3E8-48FC-B4C5-70F0FFF8329B}.Debug|x86.ActiveCfg = Debug|Any CPU + {7EEEB4BB-F3E8-48FC-B4C5-70F0FFF8329B}.Debug|x86.Build.0 = Debug|Any CPU + {7EEEB4BB-F3E8-48FC-B4C5-70F0FFF8329B}.Release|x86.ActiveCfg = Release|Any CPU + {7EEEB4BB-F3E8-48FC-B4C5-70F0FFF8329B}.Release|x86.Build.0 = Release|Any CPU + {9142EEFA-7570-41E1-BFCC-468BB571AF2F}.Debug|x86.ActiveCfg = Debug|Any CPU + {9142EEFA-7570-41E1-BFCC-468BB571AF2F}.Debug|x86.Build.0 = Debug|Any CPU + {9142EEFA-7570-41E1-BFCC-468BB571AF2F}.Release|x86.ActiveCfg = Release|Any CPU + {9142EEFA-7570-41E1-BFCC-468BB571AF2F}.Release|x86.Build.0 = Release|Any CPU + {A7FE75CD-3CB4-4E71-A5BF-5347721EC8E0}.Debug|x86.ActiveCfg = Debug|x86 + {A7FE75CD-3CB4-4E71-A5BF-5347721EC8E0}.Debug|x86.Build.0 = Debug|x86 + {A7FE75CD-3CB4-4E71-A5BF-5347721EC8E0}.Release|x86.ActiveCfg = Release|x86 + {A7FE75CD-3CB4-4E71-A5BF-5347721EC8E0}.Release|x86.Build.0 = Release|x86 + {C4D2573A-3FD3-441F-81AF-174AC4CD4E1D}.Debug|x86.ActiveCfg = Debug|Any CPU + {C4D2573A-3FD3-441F-81AF-174AC4CD4E1D}.Debug|x86.Build.0 = Debug|Any CPU + {C4D2573A-3FD3-441F-81AF-174AC4CD4E1D}.Release|x86.ActiveCfg = Release|Any CPU + {C4D2573A-3FD3-441F-81AF-174AC4CD4E1D}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(MonoDevelopProperties) = preSolution + StartupItem = MediaBrowser.Server.Mono\MediaBrowser.Server.Mono.csproj + EndGlobalSection +EndGlobal diff --git a/MediaBrowser.Mono.userprefs b/MediaBrowser.Mono.userprefs new file mode 100644 index 000000000..95fb57a89 --- /dev/null +++ b/MediaBrowser.Mono.userprefs @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/MediaBrowser.Providers/MediaBrowser.Providers.csproj b/MediaBrowser.Providers/MediaBrowser.Providers.csproj index 139c622fc..ef94d77d1 100644 --- a/MediaBrowser.Providers/MediaBrowser.Providers.csproj +++ b/MediaBrowser.Providers/MediaBrowser.Providers.csproj @@ -13,6 +13,8 @@ 512 ..\ true + 10.0.0 + 2.0 true @@ -32,10 +34,6 @@ 4 - - False - ..\packages\morelinq.1.0.16006\lib\net35\MoreLinq.dll - @@ -44,6 +42,9 @@ + + ..\packages\morelinq.1.0.16006\lib\net35\MoreLinq.dll + @@ -116,15 +117,15 @@ - {9142eefa-7570-41e1-bfcc-468bb571af2f} + {9142EEFA-7570-41E1-BFCC-468BB571AF2F} MediaBrowser.Common - {17e1f4e6-8abd-4fe5-9ecf-43d4b6087ba2} + {17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2} MediaBrowser.Controller - {7eeeb4bb-f3e8-48fc-b4c5-70f0fff8329b} + {7EEEB4BB-F3E8-48FC-B4C5-70F0FFF8329B} MediaBrowser.Model diff --git a/MediaBrowser.Server.Implementations/Library/Validators/GameGenresValidator.cs b/MediaBrowser.Server.Implementations/Library/Validators/GameGenresValidator.cs index eb89210ff..b9e033d23 100644 --- a/MediaBrowser.Server.Implementations/Library/Validators/GameGenresValidator.cs +++ b/MediaBrowser.Server.Implementations/Library/Validators/GameGenresValidator.cs @@ -41,8 +41,6 @@ namespace MediaBrowser.Server.Implementations.Library.Validators /// Task. public async Task Run(IProgress progress, CancellationToken cancellationToken) { - var allItems = _libraryManager.RootFolder.RecursiveChildren.OfType().ToList(); - var userLibraries = _userManager.Users .Select(i => new Tuple>(i.Id, i.RootFolder.GetRecursiveChildren(i).OfType().ToList())) .ToList(); @@ -79,6 +77,10 @@ namespace MediaBrowser.Server.Implementations.Library.Validators { await UpdateItemByNameCounts(name, cancellationToken, masterDictionary[name]).ConfigureAwait(false); } + catch (OperationCanceledException) + { + // Don't clutter the log + } catch (Exception ex) { _logger.ErrorException("Error updating counts for {0}", ex, name); diff --git a/MediaBrowser.Server.Implementations/Library/Validators/GenresValidator.cs b/MediaBrowser.Server.Implementations/Library/Validators/GenresValidator.cs index 9a34dd1b0..e4d989c33 100644 --- a/MediaBrowser.Server.Implementations/Library/Validators/GenresValidator.cs +++ b/MediaBrowser.Server.Implementations/Library/Validators/GenresValidator.cs @@ -42,16 +42,10 @@ namespace MediaBrowser.Server.Implementations.Library.Validators /// Task. public async Task Run(IProgress progress, CancellationToken cancellationToken) { - var allItems = _libraryManager.RootFolder.RecursiveChildren - .Where(i => !(i is IHasMusicGenres) && !(i is Game)) - .ToList(); - var userLibraries = _userManager.Users .Select(i => new Tuple>(i.Id, i.RootFolder.GetRecursiveChildren(i).Where(m => !(m is IHasMusicGenres) && !(m is Game)).ToList())) .ToList(); - var allLibraryItems = allItems; - var masterDictionary = new Dictionary>>(StringComparer.OrdinalIgnoreCase); // Populate counts of items @@ -84,6 +78,10 @@ namespace MediaBrowser.Server.Implementations.Library.Validators { await UpdateItemByNameCounts(name, cancellationToken, masterDictionary[name]).ConfigureAwait(false); } + catch (OperationCanceledException) + { + // Don't clutter the log + } catch (Exception ex) { _logger.ErrorException("Error updating counts for {0}", ex, name); diff --git a/MediaBrowser.Server.Implementations/Library/Validators/MusicGenresValidator.cs b/MediaBrowser.Server.Implementations/Library/Validators/MusicGenresValidator.cs index 1b211d5f4..1edc24762 100644 --- a/MediaBrowser.Server.Implementations/Library/Validators/MusicGenresValidator.cs +++ b/MediaBrowser.Server.Implementations/Library/Validators/MusicGenresValidator.cs @@ -42,16 +42,10 @@ namespace MediaBrowser.Server.Implementations.Library.Validators /// Task. public async Task Run(IProgress progress, CancellationToken cancellationToken) { - var allItems = _libraryManager.RootFolder.RecursiveChildren - .Where(i => i is IHasMusicGenres) - .ToList(); - var userLibraries = _userManager.Users .Select(i => new Tuple>(i.Id, i.RootFolder.GetRecursiveChildren(i).Where(m => m is IHasMusicGenres).ToList())) .ToList(); - var allLibraryItems = allItems; - var masterDictionary = new Dictionary>>(StringComparer.OrdinalIgnoreCase); // Populate counts of items @@ -84,6 +78,10 @@ namespace MediaBrowser.Server.Implementations.Library.Validators { await UpdateItemByNameCounts(name, cancellationToken, masterDictionary[name]).ConfigureAwait(false); } + catch (OperationCanceledException) + { + // Don't clutter the log + } catch (Exception ex) { _logger.ErrorException("Error updating counts for {0}", ex, name); diff --git a/MediaBrowser.Server.Implementations/Library/Validators/StudiosValidator.cs b/MediaBrowser.Server.Implementations/Library/Validators/StudiosValidator.cs index a4d880329..05689f8e5 100644 --- a/MediaBrowser.Server.Implementations/Library/Validators/StudiosValidator.cs +++ b/MediaBrowser.Server.Implementations/Library/Validators/StudiosValidator.cs @@ -41,14 +41,10 @@ namespace MediaBrowser.Server.Implementations.Library.Validators /// Task. public async Task Run(IProgress progress, CancellationToken cancellationToken) { - var allItems = _libraryManager.RootFolder.RecursiveChildren.ToList(); - var userLibraries = _userManager.Users .Select(i => new Tuple>(i.Id, i.RootFolder.GetRecursiveChildren(i).ToList())) .ToList(); - var allLibraryItems = allItems; - var masterDictionary = new Dictionary>>(StringComparer.OrdinalIgnoreCase); // Populate counts of items @@ -81,6 +77,10 @@ namespace MediaBrowser.Server.Implementations.Library.Validators { await UpdateItemByNameCounts(name, cancellationToken, masterDictionary[name]).ConfigureAwait(false); } + catch (OperationCanceledException) + { + // Don't clutter the log + } catch (Exception ex) { _logger.ErrorException("Error updating counts for {0}", ex, name); diff --git a/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj b/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj index 3c2021750..e44089cc1 100644 --- a/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj +++ b/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj @@ -13,6 +13,8 @@ 512 ..\ true + 10.0.0 + 2.0 true @@ -35,62 +37,14 @@ ..\packages\Alchemy.2.2.1\lib\net40\Alchemy.dll - - False - ..\packages\MediaBrowser.BdInfo.1.0.0.2\lib\net45\BdInfo.dll - ..\packages\SharpZipLib.0.86.0\lib\20\ICSharpCode.SharpZipLib.dll ..\packages\Lucene.Net.3.0.3\lib\NET40\Lucene.Net.dll - - False - ..\packages\morelinq.1.0.16006\lib\net35\MoreLinq.dll - - - False - ..\packages\ServiceStack.3.9.62\lib\net35\ServiceStack.dll - - - False - ..\packages\ServiceStack.Api.Swagger.3.9.59\lib\net35\ServiceStack.Api.Swagger.dll - - - False - ..\packages\ServiceStack.Common.3.9.62\lib\net35\ServiceStack.Common.dll - - - False - ..\packages\ServiceStack.Common.3.9.62\lib\net35\ServiceStack.Interfaces.dll - - - False - ..\packages\ServiceStack.OrmLite.SqlServer.3.9.43\lib\ServiceStack.OrmLite.SqlServer.dll - - - False - ..\packages\ServiceStack.Redis.3.9.43\lib\net35\ServiceStack.Redis.dll - - - False - ..\packages\ServiceStack.3.9.62\lib\net35\ServiceStack.ServiceInterface.dll - - - False - ..\packages\ServiceStack.Text.3.9.62\lib\net35\ServiceStack.Text.dll - - - False - ..\packages\System.Data.SQLite.x86.1.0.88.0\lib\net45\System.Data.SQLite.dll - - - False - ..\packages\System.Data.SQLite.x86.1.0.88.0\lib\net45\System.Data.SQLite.Linq.dll - ..\packages\Rx-Core.2.1.30214.0\lib\Net45\System.Reactive.Core.dll @@ -106,6 +60,42 @@ + + ..\packages\MediaBrowser.BdInfo.1.0.0.2\lib\net45\BdInfo.dll + + + ..\packages\morelinq.1.0.16006\lib\net35\MoreLinq.dll + + + ..\packages\ServiceStack.3.9.62\lib\net35\ServiceStack.dll + + + ..\packages\ServiceStack.Api.Swagger.3.9.59\lib\net35\ServiceStack.Api.Swagger.dll + + + ..\packages\ServiceStack.Common.3.9.62\lib\net35\ServiceStack.Common.dll + + + ..\packages\ServiceStack.Common.3.9.62\lib\net35\ServiceStack.Interfaces.dll + + + ..\packages\ServiceStack.OrmLite.SqlServer.3.9.43\lib\ServiceStack.OrmLite.SqlServer.dll + + + ..\packages\ServiceStack.Redis.3.9.43\lib\net35\ServiceStack.Redis.dll + + + ..\packages\ServiceStack.3.9.62\lib\net35\ServiceStack.ServiceInterface.dll + + + ..\packages\ServiceStack.Text.3.9.62\lib\net35\ServiceStack.Text.dll + + + ..\packages\System.Data.SQLite.x86.1.0.88.0\lib\net45\System.Data.SQLite.dll + + + ..\packages\System.Data.SQLite.x86.1.0.88.0\lib\net45\System.Data.SQLite.Linq.dll + @@ -223,19 +213,19 @@ - {c4d2573a-3fd3-441f-81af-174ac4cd4e1d} + {C4D2573A-3FD3-441F-81AF-174AC4CD4E1D} MediaBrowser.Common.Implementations - {9142eefa-7570-41e1-bfcc-468bb571af2f} + {9142EEFA-7570-41E1-BFCC-468BB571AF2F} MediaBrowser.Common - {17e1f4e6-8abd-4fe5-9ecf-43d4b6087ba2} + {17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2} MediaBrowser.Controller - {7eeeb4bb-f3e8-48fc-b4c5-70f0fff8329b} + {7EEEB4BB-F3E8-48FC-B4C5-70F0FFF8329B} MediaBrowser.Model diff --git a/MediaBrowser.Server.Mono/MainWindow.cs b/MediaBrowser.Server.Mono/MainWindow.cs new file mode 100644 index 000000000..229f44dab --- /dev/null +++ b/MediaBrowser.Server.Mono/MainWindow.cs @@ -0,0 +1,16 @@ +using System; +using Gtk; + +public partial class MainWindow: Gtk.Window +{ + public MainWindow (): base (Gtk.WindowType.Toplevel) + { + Build (); + } + + protected void OnDeleteEvent (object sender, DeleteEventArgs a) + { + Application.Quit (); + a.RetVal = true; + } +} diff --git a/MediaBrowser.Server.Mono/MediaBrowser.Server.Mono.csproj b/MediaBrowser.Server.Mono/MediaBrowser.Server.Mono.csproj new file mode 100644 index 000000000..a97ab4fac --- /dev/null +++ b/MediaBrowser.Server.Mono/MediaBrowser.Server.Mono.csproj @@ -0,0 +1,91 @@ + + + + Debug + x86 + 10.0.0 + 2.0 + {A7FE75CD-3CB4-4E71-A5BF-5347721EC8E0} + WinExe + MediaBrowser.Server.Mono + MediaBrowser.Server.Mono + v4.5 + + + true + full + false + bin\Debug + DEBUG; + prompt + 4 + x86 + false + + + full + true + bin\Release + prompt + 4 + x86 + false + + + + + + + + + + + + + + gui.stetic + + + + + + + + + + + + + {5624B7B5-B5A7-41D8-9F10-CC5611109619} + MediaBrowser.WebDashboard + + + {2E781478-814D-4A48-9D80-BFF206441A65} + MediaBrowser.Server.Implementations + + + {442B5058-DCAF-4263-BB6A-F21E31120A1B} + MediaBrowser.Providers + + + {7EEEB4BB-F3E8-48FC-B4C5-70F0FFF8329B} + MediaBrowser.Model + + + {17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2} + MediaBrowser.Controller + + + {C4D2573A-3FD3-441F-81AF-174AC4CD4E1D} + MediaBrowser.Common.Implementations + + + {9142EEFA-7570-41E1-BFCC-468BB571AF2F} + MediaBrowser.Common + + + {4FD51AC5-2C16-4308-A993-C3A84F3B4582} + MediaBrowser.Api + + + \ No newline at end of file diff --git a/MediaBrowser.Server.Mono/Program.cs b/MediaBrowser.Server.Mono/Program.cs new file mode 100644 index 000000000..72dee1162 --- /dev/null +++ b/MediaBrowser.Server.Mono/Program.cs @@ -0,0 +1,16 @@ +using System; +using Gtk; + +namespace MediaBrowser.Server.Mono +{ + class MainClass + { + public static void Main (string[] args) + { + Application.Init (); + MainWindow win = new MainWindow (); + win.Show (); + Application.Run (); + } + } +} diff --git a/MediaBrowser.Server.Mono/Properties/AssemblyInfo.cs b/MediaBrowser.Server.Mono/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..0a2e93220 --- /dev/null +++ b/MediaBrowser.Server.Mono/Properties/AssemblyInfo.cs @@ -0,0 +1,22 @@ +using System.Reflection; +using System.Runtime.CompilerServices; + +// Information about this assembly is defined by the following attributes. +// Change them to the values specific to your project. +[assembly: AssemblyTitle ("MediaBrowser.Server.Mono")] +[assembly: AssemblyDescription ("")] +[assembly: AssemblyConfiguration ("")] +[assembly: AssemblyCompany ("")] +[assembly: AssemblyProduct ("")] +[assembly: AssemblyCopyright ("Luke")] +[assembly: AssemblyTrademark ("")] +[assembly: AssemblyCulture ("")] +// The assembly version has the format "{Major}.{Minor}.{Build}.{Revision}". +// The form "{Major}.{Minor}.*" will automatically update the build and revision, +// and "{Major}.{Minor}.{Build}.*" will update just the revision. +[assembly: AssemblyVersion ("1.0.*")] +// The following attributes are used to specify the signing key for the assembly, +// if desired. See the Mono documentation for more information about signing. +//[assembly: AssemblyDelaySign(false)] +//[assembly: AssemblyKeyFile("")] + diff --git a/MediaBrowser.Server.Mono/gtk-gui/MainWindow.cs b/MediaBrowser.Server.Mono/gtk-gui/MainWindow.cs new file mode 100644 index 000000000..c481dfc8c --- /dev/null +++ b/MediaBrowser.Server.Mono/gtk-gui/MainWindow.cs @@ -0,0 +1,20 @@ + +// This file has been generated by the GUI designer. Do not modify. +public partial class MainWindow +{ + protected virtual void Build () + { + global::Stetic.Gui.Initialize (this); + // Widget MainWindow + this.Name = "MainWindow"; + this.Title = global::Mono.Unix.Catalog.GetString ("MainWindow"); + this.WindowPosition = ((global::Gtk.WindowPosition)(4)); + if ((this.Child != null)) { + this.Child.ShowAll (); + } + this.DefaultWidth = 400; + this.DefaultHeight = 300; + this.Show (); + this.DeleteEvent += new global::Gtk.DeleteEventHandler (this.OnDeleteEvent); + } +} diff --git a/MediaBrowser.Server.Mono/gtk-gui/generated.cs b/MediaBrowser.Server.Mono/gtk-gui/generated.cs new file mode 100644 index 000000000..9ef336398 --- /dev/null +++ b/MediaBrowser.Server.Mono/gtk-gui/generated.cs @@ -0,0 +1,29 @@ + +// This file has been generated by the GUI designer. Do not modify. +namespace Stetic +{ + internal class Gui + { + private static bool initialized; + + internal static void Initialize (Gtk.Widget iconRenderer) + { + if ((Stetic.Gui.initialized == false)) { + Stetic.Gui.initialized = true; + } + } + } + + internal class ActionGroups + { + public static Gtk.ActionGroup GetActionGroup (System.Type type) + { + return Stetic.ActionGroups.GetActionGroup (type.FullName); + } + + public static Gtk.ActionGroup GetActionGroup (string name) + { + return null; + } + } +} diff --git a/MediaBrowser.Server.Mono/gtk-gui/gui.stetic b/MediaBrowser.Server.Mono/gtk-gui/gui.stetic new file mode 100644 index 000000000..d564b4446 --- /dev/null +++ b/MediaBrowser.Server.Mono/gtk-gui/gui.stetic @@ -0,0 +1,19 @@ + + + + 2.12 + + + + + + + + MainWindow + CenterOnParent + + + + + + \ No newline at end of file diff --git a/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj b/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj index 1fbc01952..6a599da45 100644 --- a/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj +++ b/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj @@ -13,6 +13,8 @@ 512 ..\ true + 10.0.0 + 2.0 true @@ -35,24 +37,21 @@ Always - - False - ..\packages\ServiceStack.Common.3.9.62\lib\net35\ServiceStack.Common.dll - - - False - ..\packages\ServiceStack.Common.3.9.62\lib\net35\ServiceStack.Interfaces.dll - - - False - ..\packages\ServiceStack.Text.3.9.62\lib\net35\ServiceStack.Text.dll - + + ..\packages\ServiceStack.Common.3.9.62\lib\net35\ServiceStack.Common.dll + + + ..\packages\ServiceStack.Common.3.9.62\lib\net35\ServiceStack.Interfaces.dll + + + ..\packages\ServiceStack.Text.3.9.62\lib\net35\ServiceStack.Text.dll + @@ -67,15 +66,15 @@ - {9142eefa-7570-41e1-bfcc-468bb571af2f} + {9142EEFA-7570-41E1-BFCC-468BB571AF2F} MediaBrowser.Common - {17e1f4e6-8abd-4fe5-9ecf-43d4b6087ba2} + {17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2} MediaBrowser.Controller - {7eeeb4bb-f3e8-48fc-b4c5-70f0fff8329b} + {7EEEB4BB-F3E8-48FC-B4C5-70F0FFF8329B} MediaBrowser.Model From fe5a9232c84cea113336005b3c7cbd7fe77a2d80 Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Tue, 24 Sep 2013 20:54:51 -0400 Subject: [PATCH 19/21] moved a few things for mono --- .../Archiving/ZipClient.cs | 87 +++++++++++ .../BaseApplicationHost.cs | 59 ++++---- ...MediaBrowser.Common.Implementations.csproj | 4 + .../packages.config | 1 + MediaBrowser.Model/IO/IZipClient.cs | 16 +++ MediaBrowser.Mono.userprefs | 13 +- .../EntryPoints/UdpServerEntryPoint.cs | 6 +- ...MediaBrowser.Server.Implementations.csproj | 1 + .../FFMpeg/FFMpegDownloader.cs | 36 +++++ .../MediaBrowser.Server.Mono.csproj | 28 ++++ MediaBrowser.Server.Mono/Native/Assemblies.cs | 22 +++ MediaBrowser.Server.Mono/Native/Autorun.cs | 20 +++ .../Native/HttpMessageHandlerFactory.cs | 25 ++++ MediaBrowser.Server.Mono/Native/NativeApp.cs | 25 ++++ .../Native/ServerAuthorization.cs | 26 ++++ MediaBrowser.Server.Mono/Native/Sqlite.cs | 36 +++++ MediaBrowser.Server.Mono/gtk-gui/gui.stetic | 1 + MediaBrowser.ServerApplication/App.xaml.cs | 53 ------- .../ApplicationHost.cs | 136 ++++++------------ .../EntryPoints/StartupWizard.cs | 14 +- .../FFMpegDownloader.cs | 25 +--- .../FFMpeg/FFMpegInfo.cs | 24 ++++ ...git-f974289-win32-static.7z.REMOVED.git-id | 0 .../Implementations/ZipClient.cs | 48 ------- MediaBrowser.ServerApplication/MainStartup.cs | 1 - .../MainWindow.xaml.cs | 17 +-- .../MediaBrowser.ServerApplication.csproj | 26 ++-- .../Native/Assemblies.cs | 25 ++++ .../Native/Autorun.cs | 31 ++++ .../Native/BrowserLauncher.cs | 68 +++++++++ .../Native/HttpMessageHandlerFactory.cs | 26 ++++ .../Native/NativeApp.cs | 25 ++++ .../{ => Native}/RegisterServer.bat | 0 .../Native/ServerAuthorization.cs | 56 ++++++++ .../Native/Sqlite.cs | 36 +++++ .../packages.config | 2 - 36 files changed, 740 insertions(+), 279 deletions(-) create mode 100644 MediaBrowser.Common.Implementations/Archiving/ZipClient.cs rename {MediaBrowser.ServerApplication => MediaBrowser.Server.Implementations}/EntryPoints/UdpServerEntryPoint.cs (95%) create mode 100644 MediaBrowser.Server.Mono/FFMpeg/FFMpegDownloader.cs create mode 100644 MediaBrowser.Server.Mono/Native/Assemblies.cs create mode 100644 MediaBrowser.Server.Mono/Native/Autorun.cs create mode 100644 MediaBrowser.Server.Mono/Native/HttpMessageHandlerFactory.cs create mode 100644 MediaBrowser.Server.Mono/Native/NativeApp.cs create mode 100644 MediaBrowser.Server.Mono/Native/ServerAuthorization.cs create mode 100644 MediaBrowser.Server.Mono/Native/Sqlite.cs rename MediaBrowser.ServerApplication/{Implementations => FFMpeg}/FFMpegDownloader.cs (93%) create mode 100644 MediaBrowser.ServerApplication/FFMpeg/FFMpegInfo.cs rename MediaBrowser.ServerApplication/{Implementations => FFMpeg}/ffmpeg-20130904-git-f974289-win32-static.7z.REMOVED.git-id (100%) delete mode 100644 MediaBrowser.ServerApplication/Implementations/ZipClient.cs create mode 100644 MediaBrowser.ServerApplication/Native/Assemblies.cs create mode 100644 MediaBrowser.ServerApplication/Native/Autorun.cs create mode 100644 MediaBrowser.ServerApplication/Native/BrowserLauncher.cs create mode 100644 MediaBrowser.ServerApplication/Native/HttpMessageHandlerFactory.cs create mode 100644 MediaBrowser.ServerApplication/Native/NativeApp.cs rename MediaBrowser.ServerApplication/{ => Native}/RegisterServer.bat (100%) create mode 100644 MediaBrowser.ServerApplication/Native/ServerAuthorization.cs create mode 100644 MediaBrowser.ServerApplication/Native/Sqlite.cs diff --git a/MediaBrowser.Common.Implementations/Archiving/ZipClient.cs b/MediaBrowser.Common.Implementations/Archiving/ZipClient.cs new file mode 100644 index 000000000..39690eb07 --- /dev/null +++ b/MediaBrowser.Common.Implementations/Archiving/ZipClient.cs @@ -0,0 +1,87 @@ +using MediaBrowser.Model.IO; +using SharpCompress.Archive.SevenZip; +using SharpCompress.Common; +using SharpCompress.Reader; +using System.IO; + +namespace MediaBrowser.Common.Implementations.Archiving +{ + /// + /// Class DotNetZipClient + /// + public class ZipClient : IZipClient + { + /// + /// Extracts all. + /// + /// The source file. + /// The target path. + /// if set to true [overwrite existing files]. + public void ExtractAll(string sourceFile, string targetPath, bool overwriteExistingFiles) + { + using (var fileStream = File.OpenRead(sourceFile)) + { + ExtractAll(fileStream, targetPath, overwriteExistingFiles); + } + } + + /// + /// Extracts all. + /// + /// The source. + /// The target path. + /// if set to true [overwrite existing files]. + public void ExtractAll(Stream source, string targetPath, bool overwriteExistingFiles) + { + using (var reader = ReaderFactory.Open(source)) + { + var options = ExtractOptions.ExtractFullPath; + + if (overwriteExistingFiles) + { + options = options | ExtractOptions.Overwrite; + } + + reader.WriteAllToDirectory(targetPath, options); + } + } + + /// + /// Extracts all from7z. + /// + /// The source file. + /// The target path. + /// if set to true [overwrite existing files]. + public void ExtractAllFrom7z(string sourceFile, string targetPath, bool overwriteExistingFiles) + { + using (var fileStream = File.OpenRead(sourceFile)) + { + ExtractAllFrom7z(fileStream, targetPath, overwriteExistingFiles); + } + } + + /// + /// Extracts all from7z. + /// + /// The source. + /// The target path. + /// if set to true [overwrite existing files]. + public void ExtractAllFrom7z(Stream source, string targetPath, bool overwriteExistingFiles) + { + using (var archive = SevenZipArchive.Open(source)) + { + using (var reader = archive.ExtractAllEntries()) + { + var options = ExtractOptions.ExtractFullPath; + + if (overwriteExistingFiles) + { + options = options | ExtractOptions.Overwrite; + } + + reader.WriteAllToDirectory(targetPath, options); + } + } + } + } +} diff --git a/MediaBrowser.Common.Implementations/BaseApplicationHost.cs b/MediaBrowser.Common.Implementations/BaseApplicationHost.cs index c0ac6a4b3..0d96df9a2 100644 --- a/MediaBrowser.Common.Implementations/BaseApplicationHost.cs +++ b/MediaBrowser.Common.Implementations/BaseApplicationHost.cs @@ -1,5 +1,6 @@ using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Events; +using MediaBrowser.Common.Implementations.Archiving; using MediaBrowser.Common.Implementations.NetworkManagement; using MediaBrowser.Common.Implementations.ScheduledTasks; using MediaBrowser.Common.Implementations.Security; @@ -10,6 +11,7 @@ using MediaBrowser.Common.Plugins; using MediaBrowser.Common.ScheduledTasks; using MediaBrowser.Common.Security; using MediaBrowser.Common.Updates; +using MediaBrowser.Model.IO; using MediaBrowser.Model.Logging; using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Updates; @@ -149,6 +151,12 @@ namespace MediaBrowser.Common.Implementations /// The installation manager. protected IInstallationManager InstallationManager { get; set; } + /// + /// Gets or sets the zip client. + /// + /// The zip client. + protected IZipClient ZipClient { get; set; } + /// /// Initializes a new instance of the class. /// @@ -202,12 +210,27 @@ namespace MediaBrowser.Common.Implementations { Resolve().AddTasks(GetExports(false)); - Task.Run(() => ConfigureAutoRunAtStartup()); + Task.Run(() => ConfigureAutorun()); ConfigurationManager.ConfigurationUpdated += OnConfigurationUpdated; }); } + /// + /// Configures the autorun. + /// + private void ConfigureAutorun() + { + try + { + ConfigureAutoRunAtStartup(ConfigurationManager.CommonConfiguration.RunAtStartup); + } + catch (Exception ex) + { + Logger.ErrorException("Error configuring autorun", ex); + } + } + /// /// Gets the composable part assemblies. /// @@ -281,6 +304,9 @@ namespace MediaBrowser.Common.Implementations InstallationManager = new InstallationManager(Logger, this, ApplicationPaths, HttpClient, JsonSerializer, SecurityManager, NetworkManager, ConfigurationManager); RegisterSingleInstance(InstallationManager); + + ZipClient = new ZipClient(); + RegisterSingleInstance(ZipClient); }); } @@ -453,11 +479,6 @@ namespace MediaBrowser.Common.Implementations } } - /// - /// Defines the full path to our shortcut in the start menu - /// - protected abstract string ProductShortcutPath { get; } - /// /// Handles the ConfigurationUpdated event of the ConfigurationManager control. /// @@ -466,32 +487,10 @@ namespace MediaBrowser.Common.Implementations /// protected virtual void OnConfigurationUpdated(object sender, EventArgs e) { - ConfigureAutoRunAtStartup(); + ConfigureAutorun(); } - /// - /// Configures the auto run at startup. - /// - private void ConfigureAutoRunAtStartup() - { - if (ConfigurationManager.CommonConfiguration.RunAtStartup) - { - //Copy our shortut into the startup folder for this user - File.Copy(ProductShortcutPath, Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Startup), Path.GetFileName(ProductShortcutPath) ?? "MBstartup.lnk"), true); - } - else - { - //Remove our shortcut from the startup folder for this user - try - { - File.Delete(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Startup), Path.GetFileName(ProductShortcutPath) ?? "MBstartup.lnk")); - } - catch (FileNotFoundException) - { - //This is okay - trying to remove it anyway - } - } - } + protected abstract void ConfigureAutoRunAtStartup(bool autorun); /// /// Removes the plugin. diff --git a/MediaBrowser.Common.Implementations/MediaBrowser.Common.Implementations.csproj b/MediaBrowser.Common.Implementations/MediaBrowser.Common.Implementations.csproj index 79514b5cb..11da950f7 100644 --- a/MediaBrowser.Common.Implementations/MediaBrowser.Common.Implementations.csproj +++ b/MediaBrowser.Common.Implementations/MediaBrowser.Common.Implementations.csproj @@ -37,6 +37,9 @@ Always + + ..\packages\sharpcompress.0.10.1.3\lib\net40\SharpCompress.dll + @@ -58,6 +61,7 @@ Properties\SharedVersion.cs + diff --git a/MediaBrowser.Common.Implementations/packages.config b/MediaBrowser.Common.Implementations/packages.config index 4be861cce..d03cb14e0 100644 --- a/MediaBrowser.Common.Implementations/packages.config +++ b/MediaBrowser.Common.Implementations/packages.config @@ -2,5 +2,6 @@ + \ No newline at end of file diff --git a/MediaBrowser.Model/IO/IZipClient.cs b/MediaBrowser.Model/IO/IZipClient.cs index c9e7e0db6..694c393aa 100644 --- a/MediaBrowser.Model/IO/IZipClient.cs +++ b/MediaBrowser.Model/IO/IZipClient.cs @@ -22,5 +22,21 @@ namespace MediaBrowser.Model.IO /// The target path. /// if set to true [overwrite existing files]. void ExtractAll(Stream source, string targetPath, bool overwriteExistingFiles); + + /// + /// Extracts all from7z. + /// + /// The source file. + /// The target path. + /// if set to true [overwrite existing files]. + void ExtractAllFrom7z(string sourceFile, string targetPath, bool overwriteExistingFiles); + + /// + /// Extracts all from7z. + /// + /// The source. + /// The target path. + /// if set to true [overwrite existing files]. + void ExtractAllFrom7z(Stream source, string targetPath, bool overwriteExistingFiles); } } diff --git a/MediaBrowser.Mono.userprefs b/MediaBrowser.Mono.userprefs index 95fb57a89..80da5915d 100644 --- a/MediaBrowser.Mono.userprefs +++ b/MediaBrowser.Mono.userprefs @@ -1,6 +1,17 @@  - + + + + + + + + + + + + diff --git a/MediaBrowser.ServerApplication/EntryPoints/UdpServerEntryPoint.cs b/MediaBrowser.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs similarity index 95% rename from MediaBrowser.ServerApplication/EntryPoints/UdpServerEntryPoint.cs rename to MediaBrowser.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs index 595d5c89f..9c1a953b1 100644 --- a/MediaBrowser.ServerApplication/EntryPoints/UdpServerEntryPoint.cs +++ b/MediaBrowser.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs @@ -5,7 +5,7 @@ using MediaBrowser.Model.Logging; using MediaBrowser.Server.Implementations.Udp; using System.Net.Sockets; -namespace MediaBrowser.ServerApplication.EntryPoints +namespace MediaBrowser.Server.Implementations.EntryPoints { /// /// Class UdpServerEntryPoint @@ -35,6 +35,8 @@ namespace MediaBrowser.ServerApplication.EntryPoints /// private readonly IHttpServer _httpServer; + public const int PortNumber = 7359; + /// /// Initializes a new instance of the class. /// @@ -59,7 +61,7 @@ namespace MediaBrowser.ServerApplication.EntryPoints try { - udpServer.Start(ApplicationHost.UdpServerPort); + udpServer.Start(PortNumber); UdpServer = udpServer; } diff --git a/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj b/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj index e44089cc1..f409b7205 100644 --- a/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj +++ b/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj @@ -113,6 +113,7 @@ + diff --git a/MediaBrowser.Server.Mono/FFMpeg/FFMpegDownloader.cs b/MediaBrowser.Server.Mono/FFMpeg/FFMpegDownloader.cs new file mode 100644 index 000000000..cc268ef07 --- /dev/null +++ b/MediaBrowser.Server.Mono/FFMpeg/FFMpegDownloader.cs @@ -0,0 +1,36 @@ +using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.IO; +using MediaBrowser.Common.Net; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Net; +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace MediaBrowser.ServerApplication.FFMpeg +{ + public class FFMpegDownloader + { + private readonly IHttpClient _httpClient; + private readonly IApplicationPaths _appPaths; + private readonly ILogger _logger; + private readonly IZipClient _zipClient; + + public FFMpegDownloader(ILogger logger, IApplicationPaths appPaths, IHttpClient httpClient, IZipClient zipClient) + { + _logger = logger; + _appPaths = appPaths; + _httpClient = httpClient; + _zipClient = zipClient; + } + + public Task GetFFMpegInfo() + { + return Task.FromResult (new FFMpegInfo()); + } + } +} diff --git a/MediaBrowser.Server.Mono/MediaBrowser.Server.Mono.csproj b/MediaBrowser.Server.Mono/MediaBrowser.Server.Mono.csproj index a97ab4fac..1c369daac 100644 --- a/MediaBrowser.Server.Mono/MediaBrowser.Server.Mono.csproj +++ b/MediaBrowser.Server.Mono/MediaBrowser.Server.Mono.csproj @@ -40,6 +40,9 @@ + + + @@ -52,6 +55,25 @@ + + EntryPoints\StartupWizard.cs + + + Native\BrowserLauncher.cs + + + + + + FFMpeg\FFMpegInfo.cs + + + ApplicationHost.cs + + + + + @@ -88,4 +110,10 @@ MediaBrowser.Api + + + + + + \ No newline at end of file diff --git a/MediaBrowser.Server.Mono/Native/Assemblies.cs b/MediaBrowser.Server.Mono/Native/Assemblies.cs new file mode 100644 index 000000000..eae6366e1 --- /dev/null +++ b/MediaBrowser.Server.Mono/Native/Assemblies.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; +using System.Reflection; + +namespace MediaBrowser.ServerApplication.Native +{ + /// + /// Class Assemblies + /// + public static class Assemblies + { + /// + /// Gets the assemblies with parts. + /// + /// List{Assembly}. + public static List GetAssembliesWithParts() + { + var list = new List(); + + return list; + } + } +} diff --git a/MediaBrowser.Server.Mono/Native/Autorun.cs b/MediaBrowser.Server.Mono/Native/Autorun.cs new file mode 100644 index 000000000..ee33c5967 --- /dev/null +++ b/MediaBrowser.Server.Mono/Native/Autorun.cs @@ -0,0 +1,20 @@ +using System; +using System.IO; + +namespace MediaBrowser.ServerApplication.Native +{ + /// + /// Class Autorun + /// + public static class Autorun + { + /// + /// Configures the specified autorun. + /// + /// if set to true [autorun]. + public static void Configure(bool autorun) + { + + } + } +} diff --git a/MediaBrowser.Server.Mono/Native/HttpMessageHandlerFactory.cs b/MediaBrowser.Server.Mono/Native/HttpMessageHandlerFactory.cs new file mode 100644 index 000000000..5823a7e51 --- /dev/null +++ b/MediaBrowser.Server.Mono/Native/HttpMessageHandlerFactory.cs @@ -0,0 +1,25 @@ +using System.Net; +using System.Net.Cache; +using System.Net.Http; + +namespace MediaBrowser.ServerApplication.Native +{ + /// + /// Class HttpMessageHandlerFactory + /// + public static class HttpMessageHandlerFactory + { + /// + /// Gets the HTTP message handler. + /// + /// if set to true [enable HTTP compression]. + /// HttpMessageHandler. + public static HttpMessageHandler GetHttpMessageHandler(bool enableHttpCompression) + { + return new HttpClientHandler + { + AutomaticDecompression = enableHttpCompression ? DecompressionMethods.Deflate : DecompressionMethods.None + }; + } + } +} diff --git a/MediaBrowser.Server.Mono/Native/NativeApp.cs b/MediaBrowser.Server.Mono/Native/NativeApp.cs new file mode 100644 index 000000000..bb47f6ea4 --- /dev/null +++ b/MediaBrowser.Server.Mono/Native/NativeApp.cs @@ -0,0 +1,25 @@ + +namespace MediaBrowser.ServerApplication.Native +{ + /// + /// Class NativeApp + /// + public static class NativeApp + { + /// + /// Shutdowns this instance. + /// + public static void Shutdown() + { + + } + + /// + /// Restarts this instance. + /// + public static void Restart() + { + + } + } +} diff --git a/MediaBrowser.Server.Mono/Native/ServerAuthorization.cs b/MediaBrowser.Server.Mono/Native/ServerAuthorization.cs new file mode 100644 index 000000000..6f43a12c0 --- /dev/null +++ b/MediaBrowser.Server.Mono/Native/ServerAuthorization.cs @@ -0,0 +1,26 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Reflection; + +namespace MediaBrowser.ServerApplication.Native +{ + /// + /// Class Authorization + /// + public static class ServerAuthorization + { + /// + /// Authorizes the server. + /// + /// The HTTP server port. + /// The HTTP server URL prefix. + /// The web socket port. + /// The UDP port. + /// The temp directory. + public static void AuthorizeServer(int httpServerPort, string httpServerUrlPrefix, int webSocketPort, int udpPort, string tempDirectory) + { + + } + } +} diff --git a/MediaBrowser.Server.Mono/Native/Sqlite.cs b/MediaBrowser.Server.Mono/Native/Sqlite.cs new file mode 100644 index 000000000..cc20952d7 --- /dev/null +++ b/MediaBrowser.Server.Mono/Native/Sqlite.cs @@ -0,0 +1,36 @@ +using System.Data; +using System.Data.SQLite; +using System.Threading.Tasks; + +namespace MediaBrowser.ServerApplication.Native +{ + /// + /// Class Sqlite + /// + public static class Sqlite + { + /// + /// Connects to db. + /// + /// The db path. + /// Task{IDbConnection}. + /// dbPath + public static async Task OpenDatabase(string dbPath) + { + var connectionstr = new SQLiteConnectionStringBuilder + { + PageSize = 4096, + CacheSize = 4096, + SyncMode = SynchronizationModes.Normal, + DataSource = dbPath, + JournalMode = SQLiteJournalModeEnum.Wal + }; + + var connection = new SQLiteConnection(connectionstr.ConnectionString); + + await connection.OpenAsync().ConfigureAwait(false); + + return connection; + } + } +} diff --git a/MediaBrowser.Server.Mono/gtk-gui/gui.stetic b/MediaBrowser.Server.Mono/gtk-gui/gui.stetic index d564b4446..81685442c 100644 --- a/MediaBrowser.Server.Mono/gtk-gui/gui.stetic +++ b/MediaBrowser.Server.Mono/gtk-gui/gui.stetic @@ -1,6 +1,7 @@  + .. 2.12 diff --git a/MediaBrowser.ServerApplication/App.xaml.cs b/MediaBrowser.ServerApplication/App.xaml.cs index 69de391a4..706206d3a 100644 --- a/MediaBrowser.ServerApplication/App.xaml.cs +++ b/MediaBrowser.ServerApplication/App.xaml.cs @@ -154,58 +154,5 @@ namespace MediaBrowser.ServerApplication { Dispatcher.Invoke(Shutdown); } - - /// - /// Opens the dashboard page. - /// - /// The page. - /// The logged in user. - /// The configuration manager. - /// The app host. - public static void OpenDashboardPage(string page, User loggedInUser, IServerConfigurationManager configurationManager, IServerApplicationHost appHost) - { - var url = "http://localhost:" + configurationManager.Configuration.HttpServerPortNumber + "/" + - appHost.WebApplicationName + "/dashboard/" + page; - - OpenUrl(url); - } - - /// - /// Opens the URL. - /// - /// The URL. - public static void OpenUrl(string url) - { - var process = new Process - { - StartInfo = new ProcessStartInfo - { - FileName = url - }, - - EnableRaisingEvents = true - }; - - process.Exited += ProcessExited; - - try - { - process.Start(); - } - catch (Exception ex) - { - MessageBox.Show("There was an error launching your web browser. Please check your defualt browser settings."); - } - } - - /// - /// Processes the exited. - /// - /// The sender. - /// The instance containing the event data. - static void ProcessExited(object sender, EventArgs e) - { - ((Process)sender).Dispose(); - } } } diff --git a/MediaBrowser.ServerApplication/ApplicationHost.cs b/MediaBrowser.ServerApplication/ApplicationHost.cs index e96516603..d0f7da73d 100644 --- a/MediaBrowser.ServerApplication/ApplicationHost.cs +++ b/MediaBrowser.ServerApplication/ApplicationHost.cs @@ -24,7 +24,6 @@ using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Resolvers; using MediaBrowser.Controller.Session; using MediaBrowser.Controller.Sorting; -using MediaBrowser.IsoMounter; using MediaBrowser.Model.IO; using MediaBrowser.Model.Logging; using MediaBrowser.Model.MediaInfo; @@ -36,6 +35,7 @@ using MediaBrowser.Server.Implementations.BdInfo; using MediaBrowser.Server.Implementations.Configuration; using MediaBrowser.Server.Implementations.Drawing; using MediaBrowser.Server.Implementations.Dto; +using MediaBrowser.Server.Implementations.EntryPoints; using MediaBrowser.Server.Implementations.HttpServer; using MediaBrowser.Server.Implementations.IO; using MediaBrowser.Server.Implementations.Library; @@ -46,16 +46,14 @@ using MediaBrowser.Server.Implementations.Providers; using MediaBrowser.Server.Implementations.ServerManager; using MediaBrowser.Server.Implementations.Session; using MediaBrowser.Server.Implementations.WebSocket; -using MediaBrowser.ServerApplication.Implementations; +using MediaBrowser.ServerApplication.FFMpeg; +using MediaBrowser.ServerApplication.Native; using MediaBrowser.WebDashboard.Api; using System; using System.Collections.Generic; -using System.Data.SQLite; -using System.Diagnostics; +using System.Data; using System.IO; using System.Linq; -using System.Net; -using System.Net.Cache; using System.Net.Http; using System.Reflection; using System.Threading; @@ -68,8 +66,6 @@ namespace MediaBrowser.ServerApplication /// public class ApplicationHost : BaseApplicationHost, IServerApplicationHost { - internal const int UdpServerPort = 7359; - /// /// Gets the server kernel. /// @@ -142,11 +138,6 @@ namespace MediaBrowser.ServerApplication /// The provider manager. private IProviderManager ProviderManager { get; set; } /// - /// Gets or sets the zip client. - /// - /// The zip client. - private IZipClient ZipClient { get; set; } - /// /// Gets or sets the HTTP server. /// /// The HTTP server. @@ -175,14 +166,6 @@ namespace MediaBrowser.ServerApplication private IItemRepository ItemRepository { get; set; } private INotificationsRepository NotificationsRepository { get; set; } - /// - /// The full path to our startmenu shortcut - /// - protected override string ProductShortcutPath - { - get { return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.StartMenu), "Media Browser 3", "Media Browser Server.lnk"); } - } - private Task _httpServerCreationTask; /// @@ -256,9 +239,6 @@ namespace MediaBrowser.ServerApplication RegisterSingleInstance(() => new BdInfoExaminer()); - ZipClient = new ZipClient(); - RegisterSingleInstance(ZipClient); - var mediaEncoderTask = RegisterMediaEncoder(); UserDataRepository = new SqliteUserDataRepository(ApplicationPaths, JsonSerializer, LogManager); @@ -322,7 +302,7 @@ namespace MediaBrowser.ServerApplication /// Task. private async Task RegisterMediaEncoder() { - var info = await new FFMpegDownloader(Logger, ApplicationPaths, HttpClient).GetFFMpegInfo().ConfigureAwait(false); + var info = await new FFMpegDownloader(Logger, ApplicationPaths, HttpClient, ZipClient).GetFFMpegInfo().ConfigureAwait(false); MediaEncoder = new MediaEncoder(LogManager.GetLogger("MediaEncoder"), ApplicationPaths, JsonSerializer, info.Path, info.ProbePath, info.Version); RegisterSingleInstance(MediaEncoder); @@ -407,27 +387,14 @@ namespace MediaBrowser.ServerApplication /// The db path. /// Task{IDbConnection}. /// dbPath - private static async Task ConnectToDb(string dbPath) + private static Task ConnectToDb(string dbPath) { if (string.IsNullOrEmpty(dbPath)) { throw new ArgumentNullException("dbPath"); } - var connectionstr = new SQLiteConnectionStringBuilder - { - PageSize = 4096, - CacheSize = 4096, - SyncMode = SynchronizationModes.Normal, - DataSource = dbPath, - JournalMode = SQLiteJournalModeEnum.Wal - }; - - var connection = new SQLiteConnection(connectionstr.ConnectionString); - - await connection.OpenAsync().ConfigureAwait(false); - - return connection; + return Sqlite.OpenDatabase(dbPath); } /// @@ -479,7 +446,7 @@ namespace MediaBrowser.ServerApplication IsoManager.AddParts(GetExports()); SessionManager.AddParts(GetExports()); - + ImageProcessor.AddParts(GetExports()); } @@ -530,7 +497,6 @@ namespace MediaBrowser.ServerApplication { NotifyPendingRestart(); } - } /// @@ -547,7 +513,7 @@ namespace MediaBrowser.ServerApplication Logger.ErrorException("Error sending server restart web socket message", ex); } - MainStartup.Restart(); + NativeApp.Restart(); } /// @@ -571,44 +537,44 @@ namespace MediaBrowser.ServerApplication /// IEnumerable{Assembly}. protected override IEnumerable GetComposablePartAssemblies() { + var list = Directory.EnumerateFiles(ApplicationPaths.PluginsPath, "*.dll", SearchOption.TopDirectoryOnly) + .Select(LoadAssembly) + .Where(a => a != null) + .ToList(); + // 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 - foreach (var pluginAssembly in Directory - .EnumerateFiles(ApplicationPaths.PluginsPath, "*.dll", SearchOption.TopDirectoryOnly) - .Select(LoadAssembly).Where(a => a != null)) - { - yield return pluginAssembly; - } // Include composable parts in the Api assembly - yield return typeof(ApiEntryPoint).Assembly; + list.Add(typeof(ApiEntryPoint).Assembly); // Include composable parts in the Dashboard assembly - yield return typeof(DashboardInfo).Assembly; + list.Add(typeof(DashboardInfo).Assembly); // Include composable parts in the Model assembly - yield return typeof(SystemInfo).Assembly; + list.Add(typeof(SystemInfo).Assembly); // Include composable parts in the Common assembly - yield return typeof(IApplicationHost).Assembly; + list.Add(typeof(IApplicationHost).Assembly); // Include composable parts in the Controller assembly - yield return typeof(Kernel).Assembly; + list.Add(typeof(Kernel).Assembly); // Include composable parts in the Providers assembly - yield return typeof(ImagesByNameProvider).Assembly; + list.Add(typeof(ImagesByNameProvider).Assembly); // Common implementations - yield return typeof(TaskManager).Assembly; + list.Add(typeof(TaskManager).Assembly); // Server implementations - yield return typeof(ServerApplicationPaths).Assembly; + list.Add(typeof(ServerApplicationPaths).Assembly); - // Pismo - yield return typeof(PismoIsoManager).Assembly; + list.AddRange(Assemblies.GetAssembliesWithParts()); // Include composable parts in the running assembly - yield return GetType().Assembly; + list.Add(GetType().Assembly); + + return list; } private readonly string _systemId = Environment.MachineName.GetMD5().ToString(); @@ -667,7 +633,7 @@ namespace MediaBrowser.ServerApplication Logger.ErrorException("Error sending server shutdown web socket message", ex); } - MainStartup.Shutdown(); + NativeApp.Shutdown(); } /// @@ -677,36 +643,16 @@ namespace MediaBrowser.ServerApplication { Logger.Info("Requesting administrative access to authorize http server"); - // Create a temp file path to extract the bat file to - var tmpFile = Path.Combine(ConfigurationManager.CommonApplicationPaths.TempDirectory, Guid.NewGuid() + ".bat"); - - // Extract the bat file - using (var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("MediaBrowser.ServerApplication.RegisterServer.bat")) + try { - using (var fileStream = File.Create(tmpFile)) - { - stream.CopyTo(fileStream); - } + ServerAuthorization.AuthorizeServer(ServerConfigurationManager.Configuration.HttpServerPortNumber, + HttpServerUrlPrefix, ServerConfigurationManager.Configuration.LegacyWebSocketPortNumber, + UdpServerEntryPoint.PortNumber, + ConfigurationManager.CommonApplicationPaths.TempDirectory); } - - var startInfo = new ProcessStartInfo + catch (Exception ex) { - FileName = tmpFile, - - Arguments = string.Format("{0} {1} {2} {3}", ServerConfigurationManager.Configuration.HttpServerPortNumber, - HttpServerUrlPrefix, - UdpServerPort, - ServerConfigurationManager.Configuration.LegacyWebSocketPortNumber), - - CreateNoWindow = true, - WindowStyle = ProcessWindowStyle.Hidden, - Verb = "runas", - ErrorDialog = false - }; - - using (var process = Process.Start(startInfo)) - { - process.WaitForExit(); + Logger.ErrorException("Error authorizing server", ex); } } @@ -716,8 +662,7 @@ namespace MediaBrowser.ServerApplication /// The cancellation token. /// The progress. /// Task{CheckForUpdateResult}. - public override async Task CheckForApplicationUpdate(CancellationToken cancellationToken, - IProgress progress) + public override async Task CheckForApplicationUpdate(CancellationToken cancellationToken, IProgress progress) { var availablePackages = await InstallationManager.GetAvailablePackagesWithoutRegistrationInfo(cancellationToken).ConfigureAwait(false); @@ -748,11 +693,12 @@ namespace MediaBrowser.ServerApplication /// HttpMessageHandler. protected override HttpMessageHandler GetHttpMessageHandler(bool enableHttpCompression) { - return new WebRequestHandler - { - CachePolicy = new RequestCachePolicy(RequestCacheLevel.Revalidate), - AutomaticDecompression = enableHttpCompression ? DecompressionMethods.Deflate : DecompressionMethods.None - }; + return HttpMessageHandlerFactory.GetHttpMessageHandler(enableHttpCompression); + } + + protected override void ConfigureAutoRunAtStartup(bool autorun) + { + Autorun.Configure(autorun); } } } diff --git a/MediaBrowser.ServerApplication/EntryPoints/StartupWizard.cs b/MediaBrowser.ServerApplication/EntryPoints/StartupWizard.cs index 87578ef84..1a5f9e2c3 100644 --- a/MediaBrowser.ServerApplication/EntryPoints/StartupWizard.cs +++ b/MediaBrowser.ServerApplication/EntryPoints/StartupWizard.cs @@ -3,9 +3,10 @@ using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Plugins; using MediaBrowser.Model.Logging; -using System.ComponentModel; +using System; using System.Linq; -using System.Windows; +using System.Windows.Forms; +using MediaBrowser.ServerApplication.Native; namespace MediaBrowser.ServerApplication.EntryPoints { @@ -31,9 +32,10 @@ namespace MediaBrowser.ServerApplication.EntryPoints /// /// The app host. /// The user manager. - public StartupWizard(IServerApplicationHost appHost, IUserManager userManager, IServerConfigurationManager configurationManager) + public StartupWizard(IServerApplicationHost appHost, IUserManager userManager, IServerConfigurationManager configurationManager, ILogger logger) { _appHost = appHost; + _logger = logger; _userManager = userManager; _configurationManager = configurationManager; } @@ -58,9 +60,9 @@ namespace MediaBrowser.ServerApplication.EntryPoints try { - App.OpenDashboardPage("wizardstart.html", user, _configurationManager, _appHost); + BrowserLauncher.OpenDashboardPage("wizardstart.html", user, _configurationManager, _appHost, _logger); } - catch (Win32Exception ex) + catch (Exception ex) { _logger.ErrorException("Error launching startup wizard", ex); @@ -75,4 +77,4 @@ namespace MediaBrowser.ServerApplication.EntryPoints { } } -} +} \ No newline at end of file diff --git a/MediaBrowser.ServerApplication/Implementations/FFMpegDownloader.cs b/MediaBrowser.ServerApplication/FFMpeg/FFMpegDownloader.cs similarity index 93% rename from MediaBrowser.ServerApplication/Implementations/FFMpegDownloader.cs rename to MediaBrowser.ServerApplication/FFMpeg/FFMpegDownloader.cs index 1bc0b90f0..c43a85c87 100644 --- a/MediaBrowser.ServerApplication/Implementations/FFMpegDownloader.cs +++ b/MediaBrowser.ServerApplication/FFMpeg/FFMpegDownloader.cs @@ -1,11 +1,9 @@ using MediaBrowser.Common.Configuration; using MediaBrowser.Common.IO; using MediaBrowser.Common.Net; +using MediaBrowser.Model.IO; using MediaBrowser.Model.Logging; using MediaBrowser.Model.Net; -using SharpCompress.Archive.SevenZip; -using SharpCompress.Common; -using SharpCompress.Reader; using System; using System.Collections.Generic; using System.IO; @@ -14,13 +12,14 @@ using System.Text; using System.Threading; using System.Threading.Tasks; -namespace MediaBrowser.ServerApplication.Implementations +namespace MediaBrowser.ServerApplication.FFMpeg { public class FFMpegDownloader { private readonly IHttpClient _httpClient; private readonly IApplicationPaths _appPaths; private readonly ILogger _logger; + private readonly IZipClient _zipClient; private const string Version = "ffmpeg20130904"; @@ -37,11 +36,12 @@ namespace MediaBrowser.ServerApplication.Implementations "https://www.dropbox.com/s/a81cb2ob23fwcfs/ffmpeg-20130904-git-f974289-win32-static.7z?dl=1" }; - public FFMpegDownloader(ILogger logger, IApplicationPaths appPaths, IHttpClient httpClient) + public FFMpegDownloader(ILogger logger, IApplicationPaths appPaths, IHttpClient httpClient, IZipClient zipClient) { _logger = logger; _appPaths = appPaths; _httpClient = httpClient; + _zipClient = zipClient; } public async Task GetFFMpegInfo() @@ -138,13 +138,7 @@ namespace MediaBrowser.ServerApplication.Implementations private void Extract7zArchive(string archivePath, string targetPath) { - using (var archive = SevenZipArchive.Open(archivePath)) - { - using (var reader = archive.ExtractAllEntries()) - { - reader.WriteAllToDirectory(targetPath, ExtractOptions.ExtractFullPath | ExtractOptions.Overwrite); - } - } + _zipClient.ExtractAllFrom7z(archivePath, targetPath, true); } private void DeleteFile(string path) @@ -305,11 +299,4 @@ namespace MediaBrowser.ServerApplication.Implementations return path; } } - - public class FFMpegInfo - { - public string Path { get; set; } - public string ProbePath { get; set; } - public string Version { get; set; } - } } diff --git a/MediaBrowser.ServerApplication/FFMpeg/FFMpegInfo.cs b/MediaBrowser.ServerApplication/FFMpeg/FFMpegInfo.cs new file mode 100644 index 000000000..147a9f771 --- /dev/null +++ b/MediaBrowser.ServerApplication/FFMpeg/FFMpegInfo.cs @@ -0,0 +1,24 @@ +namespace MediaBrowser.ServerApplication.FFMpeg +{ + /// + /// Class FFMpegInfo + /// + public class FFMpegInfo + { + /// + /// Gets or sets the path. + /// + /// The path. + public string Path { get; set; } + /// + /// Gets or sets the probe path. + /// + /// The probe path. + public string ProbePath { get; set; } + /// + /// Gets or sets the version. + /// + /// The version. + public string Version { get; set; } + } +} \ No newline at end of file diff --git a/MediaBrowser.ServerApplication/Implementations/ffmpeg-20130904-git-f974289-win32-static.7z.REMOVED.git-id b/MediaBrowser.ServerApplication/FFMpeg/ffmpeg-20130904-git-f974289-win32-static.7z.REMOVED.git-id similarity index 100% rename from MediaBrowser.ServerApplication/Implementations/ffmpeg-20130904-git-f974289-win32-static.7z.REMOVED.git-id rename to MediaBrowser.ServerApplication/FFMpeg/ffmpeg-20130904-git-f974289-win32-static.7z.REMOVED.git-id diff --git a/MediaBrowser.ServerApplication/Implementations/ZipClient.cs b/MediaBrowser.ServerApplication/Implementations/ZipClient.cs deleted file mode 100644 index e9e8645e9..000000000 --- a/MediaBrowser.ServerApplication/Implementations/ZipClient.cs +++ /dev/null @@ -1,48 +0,0 @@ -using MediaBrowser.Model.IO; -using SharpCompress.Common; -using SharpCompress.Reader; -using System.IO; - -namespace MediaBrowser.ServerApplication.Implementations -{ - /// - /// Class DotNetZipClient - /// - public class ZipClient : IZipClient - { - /// - /// Extracts all. - /// - /// The source file. - /// The target path. - /// if set to true [overwrite existing files]. - public void ExtractAll(string sourceFile, string targetPath, bool overwriteExistingFiles) - { - using (var fileStream = File.OpenRead(sourceFile)) - { - ExtractAll(fileStream, targetPath, overwriteExistingFiles); - } - } - - /// - /// Extracts all. - /// - /// The source. - /// The target path. - /// if set to true [overwrite existing files]. - public void ExtractAll(Stream source, string targetPath, bool overwriteExistingFiles) - { - using (var reader = ReaderFactory.Open(source)) - { - var options = ExtractOptions.ExtractFullPath; - - if (overwriteExistingFiles) - { - options = options | ExtractOptions.Overwrite; - } - - reader.WriteAllToDirectory(targetPath, options); - } - } - } -} diff --git a/MediaBrowser.ServerApplication/MainStartup.cs b/MediaBrowser.ServerApplication/MainStartup.cs index e9c1fdc99..55fa60ed2 100644 --- a/MediaBrowser.ServerApplication/MainStartup.cs +++ b/MediaBrowser.ServerApplication/MainStartup.cs @@ -11,7 +11,6 @@ using System.IO; using System.Linq; using System.ServiceProcess; using System.Threading; -using System.Threading.Tasks; using System.Windows; namespace MediaBrowser.ServerApplication diff --git a/MediaBrowser.ServerApplication/MainWindow.xaml.cs b/MediaBrowser.ServerApplication/MainWindow.xaml.cs index 4c9c065e6..c22c35be8 100644 --- a/MediaBrowser.ServerApplication/MainWindow.xaml.cs +++ b/MediaBrowser.ServerApplication/MainWindow.xaml.cs @@ -12,6 +12,7 @@ using System.Diagnostics; using System.Linq; using System.Windows; using System.Windows.Threading; +using MediaBrowser.ServerApplication.Native; namespace MediaBrowser.ServerApplication { @@ -188,19 +189,19 @@ namespace MediaBrowser.ServerApplication /// The instance containing the event data. void cmdApiDocs_Click(object sender, EventArgs e) { - App.OpenUrl("http://localhost:" + _configurationManager.Configuration.HttpServerPortNumber + "/" + - _appHost.WebApplicationName + "/metadata"); + BrowserLauncher.OpenUrl("http://localhost:" + _configurationManager.Configuration.HttpServerPortNumber + "/" + + _appHost.WebApplicationName + "/metadata", _logger); } void cmdSwaggerApiDocs_Click(object sender, EventArgs e) { - App.OpenUrl("http://localhost:" + _configurationManager.Configuration.HttpServerPortNumber + "/" + - _appHost.WebApplicationName + "/swagger-ui/index.html"); + BrowserLauncher.OpenUrl("http://localhost:" + _configurationManager.Configuration.HttpServerPortNumber + "/" + + _appHost.WebApplicationName + "/swagger-ui/index.html", _logger); } void cmdGithubWiki_Click(object sender, EventArgs e) { - App.OpenUrl("https://github.com/MediaBrowser/MediaBrowser/wiki"); + BrowserLauncher.OpenUrl("https://github.com/MediaBrowser/MediaBrowser/wiki", _logger); } /// @@ -254,7 +255,7 @@ namespace MediaBrowser.ServerApplication /// private void OpenDashboard(User loggedInUser) { - App.OpenDashboardPage("dashboard.html", loggedInUser, _configurationManager, _appHost); + BrowserLauncher.OpenDashboardPage("dashboard.html", loggedInUser, _configurationManager, _appHost, _logger); } /// @@ -264,7 +265,7 @@ namespace MediaBrowser.ServerApplication /// The instance containing the event data. private void cmVisitCT_click(object sender, RoutedEventArgs e) { - App.OpenUrl("http://community.mediabrowser.tv/"); + BrowserLauncher.OpenUrl("http://community.mediabrowser.tv/", _logger); } /// @@ -275,7 +276,7 @@ namespace MediaBrowser.ServerApplication private void cmdBrowseLibrary_click(object sender, RoutedEventArgs e) { var user = _userManager.Users.FirstOrDefault(u => u.Configuration.IsAdministrator); - App.OpenDashboardPage("index.html", user, _configurationManager, _appHost); + BrowserLauncher.OpenDashboardPage("index.html", user, _configurationManager, _appHost, _logger); } /// diff --git a/MediaBrowser.ServerApplication/MediaBrowser.ServerApplication.csproj b/MediaBrowser.ServerApplication/MediaBrowser.ServerApplication.csproj index 793489c1f..61ec19dd5 100644 --- a/MediaBrowser.ServerApplication/MediaBrowser.ServerApplication.csproj +++ b/MediaBrowser.ServerApplication/MediaBrowser.ServerApplication.csproj @@ -130,10 +130,6 @@ False ..\packages\MediaBrowser.IsoMounting.3.0.56\lib\net45\MediaBrowser.IsoMounter.dll - - False - ..\packages\morelinq.1.0.16006\lib\net35\MoreLinq.dll - False ..\packages\NLog.2.0.1.2\lib\net45\NLog.dll @@ -168,9 +164,6 @@ False ..\packages\ServiceStack.Text.3.9.62\lib\net35\ServiceStack.Text.dll - - ..\packages\sharpcompress.0.10.1.3\lib\net40\SharpCompress.dll - False ..\packages\SimpleInjector.2.3.5\lib\net40-client\SimpleInjector.dll @@ -190,7 +183,6 @@ - @@ -212,12 +204,19 @@ Component - - + + + + + + + + Component + SplashWindow.xaml @@ -245,7 +244,6 @@ Code - LibraryExplorer.xaml @@ -281,15 +279,15 @@ Resources.Designer.cs - - + + SettingsSingleFileGenerator Settings.Designer.cs - + diff --git a/MediaBrowser.ServerApplication/Native/Assemblies.cs b/MediaBrowser.ServerApplication/Native/Assemblies.cs new file mode 100644 index 000000000..b43dc1a10 --- /dev/null +++ b/MediaBrowser.ServerApplication/Native/Assemblies.cs @@ -0,0 +1,25 @@ +using MediaBrowser.IsoMounter; +using System.Collections.Generic; +using System.Reflection; + +namespace MediaBrowser.ServerApplication.Native +{ + /// + /// Class Assemblies + /// + public static class Assemblies + { + /// + /// Gets the assemblies with parts. + /// + /// List{Assembly}. + public static List GetAssembliesWithParts() + { + var list = new List(); + + list.Add(typeof(PismoIsoManager).Assembly); + + return list; + } + } +} diff --git a/MediaBrowser.ServerApplication/Native/Autorun.cs b/MediaBrowser.ServerApplication/Native/Autorun.cs new file mode 100644 index 000000000..d1c02db84 --- /dev/null +++ b/MediaBrowser.ServerApplication/Native/Autorun.cs @@ -0,0 +1,31 @@ +using System; +using System.IO; + +namespace MediaBrowser.ServerApplication.Native +{ + /// + /// Class Autorun + /// + public static class Autorun + { + /// + /// Configures the specified autorun. + /// + /// if set to true [autorun]. + public static void Configure(bool autorun) + { + var shortcutPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.StartMenu), "Media Browser 3", "Media Browser Server.lnk"); + + if (autorun) + { + //Copy our shortut into the startup folder for this user + File.Copy(shortcutPath, Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Startup), Path.GetFileName(shortcutPath) ?? "MBstartup.lnk"), true); + } + else + { + //Remove our shortcut from the startup folder for this user + File.Delete(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Startup), Path.GetFileName(shortcutPath) ?? "MBstartup.lnk")); + } + } + } +} diff --git a/MediaBrowser.ServerApplication/Native/BrowserLauncher.cs b/MediaBrowser.ServerApplication/Native/BrowserLauncher.cs new file mode 100644 index 000000000..e7d041d15 --- /dev/null +++ b/MediaBrowser.ServerApplication/Native/BrowserLauncher.cs @@ -0,0 +1,68 @@ +using MediaBrowser.Controller; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Model.Logging; +using System; +using System.Diagnostics; +using System.Windows.Forms; + +namespace MediaBrowser.ServerApplication.Native +{ + public static class BrowserLauncher + { + /// + /// Opens the dashboard page. + /// + /// The page. + /// The logged in user. + /// The configuration manager. + /// The app host. + public static void OpenDashboardPage(string page, User loggedInUser, IServerConfigurationManager configurationManager, IServerApplicationHost appHost, ILogger logger) + { + var url = "http://localhost:" + configurationManager.Configuration.HttpServerPortNumber + "/" + + appHost.WebApplicationName + "/dashboard/" + page; + + OpenUrl(url, logger); + } + + /// + /// Opens the URL. + /// + /// The URL. + public static void OpenUrl(string url, ILogger logger) + { + var process = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = url + }, + + EnableRaisingEvents = true + }; + + process.Exited += ProcessExited; + + try + { + process.Start(); + } + catch (Exception ex) + { + logger.ErrorException("Error launching url: {0}", ex, url); + + MessageBox.Show("There was an error launching your web browser. Please check your default browser settings."); + } + } + + /// + /// Processes the exited. + /// + /// The sender. + /// The instance containing the event data. + private static void ProcessExited(object sender, EventArgs e) + { + ((Process)sender).Dispose(); + } + } +} diff --git a/MediaBrowser.ServerApplication/Native/HttpMessageHandlerFactory.cs b/MediaBrowser.ServerApplication/Native/HttpMessageHandlerFactory.cs new file mode 100644 index 000000000..4bbcc9ea0 --- /dev/null +++ b/MediaBrowser.ServerApplication/Native/HttpMessageHandlerFactory.cs @@ -0,0 +1,26 @@ +using System.Net; +using System.Net.Cache; +using System.Net.Http; + +namespace MediaBrowser.ServerApplication.Native +{ + /// + /// Class HttpMessageHandlerFactory + /// + public static class HttpMessageHandlerFactory + { + /// + /// Gets the HTTP message handler. + /// + /// if set to true [enable HTTP compression]. + /// HttpMessageHandler. + public static HttpMessageHandler GetHttpMessageHandler(bool enableHttpCompression) + { + return new WebRequestHandler + { + CachePolicy = new RequestCachePolicy(RequestCacheLevel.Revalidate), + AutomaticDecompression = enableHttpCompression ? DecompressionMethods.Deflate : DecompressionMethods.None + }; + } + } +} diff --git a/MediaBrowser.ServerApplication/Native/NativeApp.cs b/MediaBrowser.ServerApplication/Native/NativeApp.cs new file mode 100644 index 000000000..ea4218afc --- /dev/null +++ b/MediaBrowser.ServerApplication/Native/NativeApp.cs @@ -0,0 +1,25 @@ + +namespace MediaBrowser.ServerApplication.Native +{ + /// + /// Class NativeApp + /// + public static class NativeApp + { + /// + /// Shutdowns this instance. + /// + public static void Shutdown() + { + MainStartup.Shutdown(); + } + + /// + /// Restarts this instance. + /// + public static void Restart() + { + MainStartup.Restart(); + } + } +} diff --git a/MediaBrowser.ServerApplication/RegisterServer.bat b/MediaBrowser.ServerApplication/Native/RegisterServer.bat similarity index 100% rename from MediaBrowser.ServerApplication/RegisterServer.bat rename to MediaBrowser.ServerApplication/Native/RegisterServer.bat diff --git a/MediaBrowser.ServerApplication/Native/ServerAuthorization.cs b/MediaBrowser.ServerApplication/Native/ServerAuthorization.cs new file mode 100644 index 000000000..91f0974eb --- /dev/null +++ b/MediaBrowser.ServerApplication/Native/ServerAuthorization.cs @@ -0,0 +1,56 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Reflection; + +namespace MediaBrowser.ServerApplication.Native +{ + /// + /// Class Authorization + /// + public static class ServerAuthorization + { + /// + /// Authorizes the server. + /// + /// The HTTP server port. + /// The HTTP server URL prefix. + /// The web socket port. + /// The UDP port. + /// The temp directory. + public static void AuthorizeServer(int httpServerPort, string httpServerUrlPrefix, int webSocketPort, int udpPort, string tempDirectory) + { + // Create a temp file path to extract the bat file to + var tmpFile = Path.Combine(tempDirectory, Guid.NewGuid() + ".bat"); + + // Extract the bat file + using (var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(typeof(ServerAuthorization).Namespace + ".RegisterServer.bat")) + { + using (var fileStream = File.Create(tmpFile)) + { + stream.CopyTo(fileStream); + } + } + + var startInfo = new ProcessStartInfo + { + FileName = tmpFile, + + Arguments = string.Format("{0} {1} {2} {3}", httpServerPort, + httpServerUrlPrefix, + udpPort, + webSocketPort), + + CreateNoWindow = true, + WindowStyle = ProcessWindowStyle.Hidden, + Verb = "runas", + ErrorDialog = false + }; + + using (var process = Process.Start(startInfo)) + { + process.WaitForExit(); + } + } + } +} diff --git a/MediaBrowser.ServerApplication/Native/Sqlite.cs b/MediaBrowser.ServerApplication/Native/Sqlite.cs new file mode 100644 index 000000000..cc20952d7 --- /dev/null +++ b/MediaBrowser.ServerApplication/Native/Sqlite.cs @@ -0,0 +1,36 @@ +using System.Data; +using System.Data.SQLite; +using System.Threading.Tasks; + +namespace MediaBrowser.ServerApplication.Native +{ + /// + /// Class Sqlite + /// + public static class Sqlite + { + /// + /// Connects to db. + /// + /// The db path. + /// Task{IDbConnection}. + /// dbPath + public static async Task OpenDatabase(string dbPath) + { + var connectionstr = new SQLiteConnectionStringBuilder + { + PageSize = 4096, + CacheSize = 4096, + SyncMode = SynchronizationModes.Normal, + DataSource = dbPath, + JournalMode = SQLiteJournalModeEnum.Wal + }; + + var connection = new SQLiteConnection(connectionstr.ConnectionString); + + await connection.OpenAsync().ConfigureAwait(false); + + return connection; + } + } +} diff --git a/MediaBrowser.ServerApplication/packages.config b/MediaBrowser.ServerApplication/packages.config index 137483ef1..e680b556f 100644 --- a/MediaBrowser.ServerApplication/packages.config +++ b/MediaBrowser.ServerApplication/packages.config @@ -4,14 +4,12 @@ - - \ No newline at end of file From 3ee73bb7c21c3f4831bc294e6c697b2018855fa3 Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Tue, 24 Sep 2013 20:57:03 -0400 Subject: [PATCH 20/21] fixed ffmpeg url --- MediaBrowser.ServerApplication/FFMpeg/FFMpegDownloader.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MediaBrowser.ServerApplication/FFMpeg/FFMpegDownloader.cs b/MediaBrowser.ServerApplication/FFMpeg/FFMpegDownloader.cs index c43a85c87..926767f5b 100644 --- a/MediaBrowser.ServerApplication/FFMpeg/FFMpegDownloader.cs +++ b/MediaBrowser.ServerApplication/FFMpeg/FFMpegDownloader.cs @@ -30,7 +30,7 @@ namespace MediaBrowser.ServerApplication.FFMpeg private readonly string[] _ffMpegUrls = new[] { - "https://raw.github.com/MediaBrowser/MediaBrowser/master/MediaBrowser.ServerApplication/Implementations/ffmpeg-20130904-git-f974289-win32-static.7z", + "https://github.com/MediaBrowser/MediaBrowser/raw/master/MediaBrowser.ServerApplication/FFMpeg/ffmpeg-20130904-git-f974289-win32-static.7z", "http://ffmpeg.zeranoe.com/builds/win32/static/ffmpeg-20130904-git-f974289-win32-static.7z", "https://www.dropbox.com/s/a81cb2ob23fwcfs/ffmpeg-20130904-git-f974289-win32-static.7z?dl=1" From 2d9b48d00fd31aaa96676c82a054b2794493fbf9 Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Wed, 25 Sep 2013 11:11:23 -0400 Subject: [PATCH 21/21] fixed ffprobe running over and over --- MediaBrowser.Api/ApiEntryPoint.cs | 2 +- .../Providers/BaseMetadataProvider.cs | 2 +- .../Session/SessionInfo.cs | 6 +-- .../MediaInfo/FFProbeVideoInfoProvider.cs | 1 + .../Savers/AlbumXmlSaver.cs | 5 +- .../Savers/ArtistXmlSaver.cs | 5 +- .../Savers/BoxSetXmlSaver.cs | 5 +- .../Savers/EpisodeXmlSaver.cs | 5 +- .../Savers/FolderXmlSaver.cs | 3 +- MediaBrowser.Providers/Savers/GameXmlSaver.cs | 7 +-- .../Savers/MovieXmlSaver.cs | 5 +- .../Savers/PersonXmlSaver.cs | 5 +- .../Savers/SeasonXmlSaver.cs | 3 +- .../Savers/SeriesXmlSaver.cs | 5 +- .../Savers/XmlSaverHelpers.cs | 53 ++++++++++++++----- .../MediaEncoder/MediaEncoder.cs | 2 +- .../ScheduledTasks/ChapterImagesTask.cs | 4 ++ 17 files changed, 80 insertions(+), 38 deletions(-) diff --git a/MediaBrowser.Api/ApiEntryPoint.cs b/MediaBrowser.Api/ApiEntryPoint.cs index 273d9a7a9..8754e57a1 100644 --- a/MediaBrowser.Api/ApiEntryPoint.cs +++ b/MediaBrowser.Api/ApiEntryPoint.cs @@ -180,7 +180,7 @@ namespace MediaBrowser.Api if (job.ActiveRequestCount == 0) { - var timerDuration = type == TranscodingJobType.Progressive ? 1000 : 60000; + var timerDuration = type == TranscodingJobType.Progressive ? 1000 : 180000; if (job.KillTimer == null) { diff --git a/MediaBrowser.Controller/Providers/BaseMetadataProvider.cs b/MediaBrowser.Controller/Providers/BaseMetadataProvider.cs index a8dc8788f..2364debed 100644 --- a/MediaBrowser.Controller/Providers/BaseMetadataProvider.cs +++ b/MediaBrowser.Controller/Providers/BaseMetadataProvider.cs @@ -385,7 +385,7 @@ namespace MediaBrowser.Controller.Providers var sb = new StringBuilder(); var extensions = FileStampExtensionsDictionary; - var numExtensions = extensions.Count; + var numExtensions = FilestampExtensions.Length; // Record the name of each file // Need to sort these because accoring to msdn docs, our i/o methods are not guaranteed in any order diff --git a/MediaBrowser.Controller/Session/SessionInfo.cs b/MediaBrowser.Controller/Session/SessionInfo.cs index ba6d3d0ac..dc934b70a 100644 --- a/MediaBrowser.Controller/Session/SessionInfo.cs +++ b/MediaBrowser.Controller/Session/SessionInfo.cs @@ -83,7 +83,7 @@ namespace MediaBrowser.Controller.Session /// /// The name of the now viewing item. public string NowViewingItemName { get; set; } - + /// /// Gets or sets the now playing item. /// @@ -107,7 +107,7 @@ namespace MediaBrowser.Controller.Session /// /// true if this instance is muted; otherwise, false. public bool IsMuted { get; set; } - + /// /// Gets or sets the device id. /// @@ -139,7 +139,7 @@ namespace MediaBrowser.Controller.Session return WebSockets.Any(i => i.State == WebSocketState.Open); } - return (DateTime.UtcNow - LastActivityDate).TotalMinutes <= 5; + return (DateTime.UtcNow - LastActivityDate).TotalMinutes <= 10; } } diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfoProvider.cs b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfoProvider.cs index 690c9b3ff..c28d06cbb 100644 --- a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfoProvider.cs +++ b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfoProvider.cs @@ -178,6 +178,7 @@ namespace MediaBrowser.Providers.MediaInfo } } + SetLastRefreshed(item, DateTime.UtcNow); return true; } diff --git a/MediaBrowser.Providers/Savers/AlbumXmlSaver.cs b/MediaBrowser.Providers/Savers/AlbumXmlSaver.cs index e6195c03e..bd63b5fbd 100644 --- a/MediaBrowser.Providers/Savers/AlbumXmlSaver.cs +++ b/MediaBrowser.Providers/Savers/AlbumXmlSaver.cs @@ -1,4 +1,5 @@ -using MediaBrowser.Controller.Configuration; +using System.Collections.Generic; +using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Library; @@ -58,7 +59,7 @@ namespace MediaBrowser.Providers.Savers var xmlFilePath = GetSavePath(item); - XmlSaverHelpers.Save(builder, xmlFilePath, new string[] { }); + XmlSaverHelpers.Save(builder, xmlFilePath, new List { }); // Set last refreshed so that the provider doesn't trigger after the file save PersonProviderFromXml.Current.SetLastRefreshed(item, DateTime.UtcNow); diff --git a/MediaBrowser.Providers/Savers/ArtistXmlSaver.cs b/MediaBrowser.Providers/Savers/ArtistXmlSaver.cs index 795e824fc..a27fb7363 100644 --- a/MediaBrowser.Providers/Savers/ArtistXmlSaver.cs +++ b/MediaBrowser.Providers/Savers/ArtistXmlSaver.cs @@ -1,4 +1,5 @@ -using MediaBrowser.Controller.Configuration; +using System.Collections.Generic; +using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Library; @@ -70,7 +71,7 @@ namespace MediaBrowser.Providers.Savers var xmlFilePath = GetSavePath(item); - XmlSaverHelpers.Save(builder, xmlFilePath, new string[] { }); + XmlSaverHelpers.Save(builder, xmlFilePath, new List { }); // Set last refreshed so that the provider doesn't trigger after the file save ArtistProviderFromXml.Current.SetLastRefreshed(item, DateTime.UtcNow); diff --git a/MediaBrowser.Providers/Savers/BoxSetXmlSaver.cs b/MediaBrowser.Providers/Savers/BoxSetXmlSaver.cs index f5fc37fe7..f09b34570 100644 --- a/MediaBrowser.Providers/Savers/BoxSetXmlSaver.cs +++ b/MediaBrowser.Providers/Savers/BoxSetXmlSaver.cs @@ -1,4 +1,5 @@ -using MediaBrowser.Controller.Configuration; +using System.Collections.Generic; +using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Library; @@ -57,7 +58,7 @@ namespace MediaBrowser.Providers.Savers var xmlFilePath = GetSavePath(item); - XmlSaverHelpers.Save(builder, xmlFilePath, new string[] { }); + XmlSaverHelpers.Save(builder, xmlFilePath, new List { }); BoxSetProviderFromXml.Current.SetLastRefreshed(item, DateTime.UtcNow); } diff --git a/MediaBrowser.Providers/Savers/EpisodeXmlSaver.cs b/MediaBrowser.Providers/Savers/EpisodeXmlSaver.cs index d90cb94c2..854c508b9 100644 --- a/MediaBrowser.Providers/Savers/EpisodeXmlSaver.cs +++ b/MediaBrowser.Providers/Savers/EpisodeXmlSaver.cs @@ -1,4 +1,5 @@ -using MediaBrowser.Controller.Configuration; +using System.Collections.Generic; +using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; @@ -87,7 +88,7 @@ namespace MediaBrowser.Providers.Savers var xmlFilePath = GetSavePath(item); - XmlSaverHelpers.Save(builder, xmlFilePath, new[] + XmlSaverHelpers.Save(builder, xmlFilePath, new List { "FirstAired", "SeasonNumber", diff --git a/MediaBrowser.Providers/Savers/FolderXmlSaver.cs b/MediaBrowser.Providers/Savers/FolderXmlSaver.cs index 23339ec75..6e95cc8c5 100644 --- a/MediaBrowser.Providers/Savers/FolderXmlSaver.cs +++ b/MediaBrowser.Providers/Savers/FolderXmlSaver.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; @@ -77,7 +78,7 @@ namespace MediaBrowser.Providers.Savers var xmlFilePath = GetSavePath(item); - XmlSaverHelpers.Save(builder, xmlFilePath, new string[] { }); + XmlSaverHelpers.Save(builder, xmlFilePath, new List { }); FolderProviderFromXml.Current.SetLastRefreshed(item, DateTime.UtcNow); } diff --git a/MediaBrowser.Providers/Savers/GameXmlSaver.cs b/MediaBrowser.Providers/Savers/GameXmlSaver.cs index 41bd364c8..1e654f72f 100644 --- a/MediaBrowser.Providers/Savers/GameXmlSaver.cs +++ b/MediaBrowser.Providers/Savers/GameXmlSaver.cs @@ -1,4 +1,5 @@ -using MediaBrowser.Controller.Configuration; +using System.Collections.Generic; +using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Providers.Movies; @@ -66,7 +67,7 @@ namespace MediaBrowser.Providers.Savers if (!string.IsNullOrEmpty(game.GameSystem)) { - builder.Append(""); + builder.Append("" + SecurityElement.Escape(game.GameSystem) + ""); } XmlSaverHelpers.AddCommonNodes(item, builder); @@ -75,7 +76,7 @@ namespace MediaBrowser.Providers.Savers var xmlFilePath = GetSavePath(item); - XmlSaverHelpers.Save(builder, xmlFilePath, new[] + XmlSaverHelpers.Save(builder, xmlFilePath, new List { "Players", "GameSystem" diff --git a/MediaBrowser.Providers/Savers/MovieXmlSaver.cs b/MediaBrowser.Providers/Savers/MovieXmlSaver.cs index 2402bcd7f..761bcefd1 100644 --- a/MediaBrowser.Providers/Savers/MovieXmlSaver.cs +++ b/MediaBrowser.Providers/Savers/MovieXmlSaver.cs @@ -1,4 +1,5 @@ -using MediaBrowser.Controller.Configuration; +using System.Collections.Generic; +using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Library; @@ -103,7 +104,7 @@ namespace MediaBrowser.Providers.Savers var xmlFilePath = GetSavePath(item); - XmlSaverHelpers.Save(builder, xmlFilePath, new[] + XmlSaverHelpers.Save(builder, xmlFilePath, new List { "IMDBrating", "Description", diff --git a/MediaBrowser.Providers/Savers/PersonXmlSaver.cs b/MediaBrowser.Providers/Savers/PersonXmlSaver.cs index 1b1377ac8..92f6db29b 100644 --- a/MediaBrowser.Providers/Savers/PersonXmlSaver.cs +++ b/MediaBrowser.Providers/Savers/PersonXmlSaver.cs @@ -1,4 +1,5 @@ -using System.Security; +using System.Collections.Generic; +using System.Security; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Providers.Movies; @@ -57,7 +58,7 @@ namespace MediaBrowser.Providers.Savers var xmlFilePath = GetSavePath(item); - XmlSaverHelpers.Save(builder, xmlFilePath, new[] + XmlSaverHelpers.Save(builder, xmlFilePath, new List { "PlaceOfBirth" }); diff --git a/MediaBrowser.Providers/Savers/SeasonXmlSaver.cs b/MediaBrowser.Providers/Savers/SeasonXmlSaver.cs index 97e8b671f..e484b3d39 100644 --- a/MediaBrowser.Providers/Savers/SeasonXmlSaver.cs +++ b/MediaBrowser.Providers/Savers/SeasonXmlSaver.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.TV; @@ -57,7 +58,7 @@ namespace MediaBrowser.Providers.Savers var xmlFilePath = GetSavePath(item); - XmlSaverHelpers.Save(builder, xmlFilePath, new string[] { }); + XmlSaverHelpers.Save(builder, xmlFilePath, new List { }); SeasonProviderFromXml.Current.SetLastRefreshed(item, DateTime.UtcNow); } diff --git a/MediaBrowser.Providers/Savers/SeriesXmlSaver.cs b/MediaBrowser.Providers/Savers/SeriesXmlSaver.cs index 6b9828576..a4ff9c7d8 100644 --- a/MediaBrowser.Providers/Savers/SeriesXmlSaver.cs +++ b/MediaBrowser.Providers/Savers/SeriesXmlSaver.cs @@ -1,4 +1,5 @@ -using MediaBrowser.Controller.Configuration; +using System.Collections.Generic; +using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; @@ -105,7 +106,7 @@ namespace MediaBrowser.Providers.Savers var xmlFilePath = GetSavePath(item); - XmlSaverHelpers.Save(builder, xmlFilePath, new[] + XmlSaverHelpers.Save(builder, xmlFilePath, new List { "id", "SeriesName", diff --git a/MediaBrowser.Providers/Savers/XmlSaverHelpers.cs b/MediaBrowser.Providers/Savers/XmlSaverHelpers.cs index cea7cf926..338447c10 100644 --- a/MediaBrowser.Providers/Savers/XmlSaverHelpers.cs +++ b/MediaBrowser.Providers/Savers/XmlSaverHelpers.cs @@ -29,13 +29,11 @@ namespace MediaBrowser.Providers.Savers /// The XML. /// The path. /// The XML tags used. - public static void Save(StringBuilder xml, string path, IEnumerable xmlTagsUsed) + public static void Save(StringBuilder xml, string path, List xmlTagsUsed) { if (File.Exists(path)) { - var tags = xmlTagsUsed.ToList(); - - tags.AddRange(new[] + xmlTagsUsed.AddRange(new[] { "MediaInfo", "ContentRating", @@ -88,7 +86,7 @@ namespace MediaBrowser.Providers.Savers }); var position = xml.ToString().LastIndexOf("The path. /// The XML tags used. /// System.String. - private static string GetCustomTags(string path, ICollection xmlTagsUsed) + private static string GetCustomTags(string path, IEnumerable xmlTagsUsed) { - var doc = new XmlDocument(); - doc.Load(path); + var settings = new XmlReaderSettings + { + CheckCharacters = false, + IgnoreProcessingInstructions = true, + IgnoreComments = true, + ValidationType = ValidationType.None + }; - var nodes = doc.DocumentElement.ChildNodes.Cast() - .Where(i => !xmlTagsUsed.Contains(i.Name)) - .Select(i => i.OuterXml) - .ToArray(); + var tagsDictionary = xmlTagsUsed.ToDictionary(i => i, StringComparer.OrdinalIgnoreCase); - return string.Join(Environment.NewLine, nodes); + var builder = new StringBuilder(); + + using (var streamReader = new StreamReader(path, Encoding.UTF8)) + { + // Use XmlReader for best performance + using (var reader = XmlReader.Create(streamReader, settings)) + { + reader.MoveToContent(); + + // Loop through each element + while (reader.Read()) + { + if (reader.NodeType == XmlNodeType.Element) + { + if (!tagsDictionary.ContainsKey(reader.Name)) + { + builder.AppendLine(reader.ReadOuterXml()); + } + else + { + reader.Skip(); + } + } + } + } + } + + return builder.ToString(); } /// diff --git a/MediaBrowser.Server.Implementations/MediaEncoder/MediaEncoder.cs b/MediaBrowser.Server.Implementations/MediaEncoder/MediaEncoder.cs index b24c9a5ca..2f353b8c0 100644 --- a/MediaBrowser.Server.Implementations/MediaEncoder/MediaEncoder.cs +++ b/MediaBrowser.Server.Implementations/MediaEncoder/MediaEncoder.cs @@ -57,7 +57,7 @@ namespace MediaBrowser.Server.Implementations.MediaEncoder /// /// The FF probe resource pool /// - private readonly SemaphoreSlim _ffProbeResourcePool = new SemaphoreSlim(2, 2); + private readonly SemaphoreSlim _ffProbeResourcePool = new SemaphoreSlim(1, 1); public string FFMpegPath { get; private set; } diff --git a/MediaBrowser.Server.Implementations/ScheduledTasks/ChapterImagesTask.cs b/MediaBrowser.Server.Implementations/ScheduledTasks/ChapterImagesTask.cs index d9b88368b..4829dc405 100644 --- a/MediaBrowser.Server.Implementations/ScheduledTasks/ChapterImagesTask.cs +++ b/MediaBrowser.Server.Implementations/ScheduledTasks/ChapterImagesTask.cs @@ -159,6 +159,10 @@ namespace MediaBrowser.Server.Implementations.ScheduledTasks { previouslyFailedImages = new List(); } + catch (DirectoryNotFoundException) + { + previouslyFailedImages = new List(); + } foreach (var video in videos) {