From 3eb4091808735858b01855d298226d239be464af Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Thu, 3 Nov 2016 02:37:52 -0400 Subject: move additional classes to new server lib --- .../Library/Validators/ArtistsPostScanTask.cs | 44 ++++++ .../Library/Validators/ArtistsValidator.cs | 84 ++++++++++ .../Library/Validators/GameGenresPostScanTask.cs | 45 ++++++ .../Library/Validators/GameGenresValidator.cs | 74 +++++++++ .../Library/Validators/GenresPostScanTask.cs | 42 +++++ .../Library/Validators/GenresValidator.cs | 75 +++++++++ .../Library/Validators/MusicGenresPostScanTask.cs | 45 ++++++ .../Library/Validators/MusicGenresValidator.cs | 75 +++++++++ .../Library/Validators/PeopleValidator.cs | 172 +++++++++++++++++++++ .../Library/Validators/StudiosPostScanTask.cs | 45 ++++++ .../Library/Validators/StudiosValidator.cs | 74 +++++++++ .../Library/Validators/YearsPostScanTask.cs | 55 +++++++ 12 files changed, 830 insertions(+) create mode 100644 Emby.Server.Implementations/Library/Validators/ArtistsPostScanTask.cs create mode 100644 Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs create mode 100644 Emby.Server.Implementations/Library/Validators/GameGenresPostScanTask.cs create mode 100644 Emby.Server.Implementations/Library/Validators/GameGenresValidator.cs create mode 100644 Emby.Server.Implementations/Library/Validators/GenresPostScanTask.cs create mode 100644 Emby.Server.Implementations/Library/Validators/GenresValidator.cs create mode 100644 Emby.Server.Implementations/Library/Validators/MusicGenresPostScanTask.cs create mode 100644 Emby.Server.Implementations/Library/Validators/MusicGenresValidator.cs create mode 100644 Emby.Server.Implementations/Library/Validators/PeopleValidator.cs create mode 100644 Emby.Server.Implementations/Library/Validators/StudiosPostScanTask.cs create mode 100644 Emby.Server.Implementations/Library/Validators/StudiosValidator.cs create mode 100644 Emby.Server.Implementations/Library/Validators/YearsPostScanTask.cs (limited to 'Emby.Server.Implementations/Library/Validators') diff --git a/Emby.Server.Implementations/Library/Validators/ArtistsPostScanTask.cs b/Emby.Server.Implementations/Library/Validators/ArtistsPostScanTask.cs new file mode 100644 index 000000000..4d718dbee --- /dev/null +++ b/Emby.Server.Implementations/Library/Validators/ArtistsPostScanTask.cs @@ -0,0 +1,44 @@ +using MediaBrowser.Controller.Library; +using MediaBrowser.Model.Logging; +using System; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Controller.Persistence; + +namespace Emby.Server.Implementations.Library.Validators +{ + /// + /// Class ArtistsPostScanTask + /// + public class ArtistsPostScanTask : ILibraryPostScanTask + { + /// + /// The _library manager + /// + private readonly ILibraryManager _libraryManager; + private readonly ILogger _logger; + private readonly IItemRepository _itemRepo; + + /// + /// Initializes a new instance of the class. + /// + /// The library manager. + public ArtistsPostScanTask(ILibraryManager libraryManager, ILogger logger, IItemRepository itemRepo) + { + _libraryManager = libraryManager; + _logger = logger; + _itemRepo = itemRepo; + } + + /// + /// Runs the specified progress. + /// + /// The progress. + /// The cancellation token. + /// Task. + public Task Run(IProgress progress, CancellationToken cancellationToken) + { + return new ArtistsValidator(_libraryManager, _logger, _itemRepo).Run(progress, cancellationToken); + } + } +} diff --git a/Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs b/Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs new file mode 100644 index 000000000..643c5970e --- /dev/null +++ b/Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs @@ -0,0 +1,84 @@ +using MediaBrowser.Controller.Entities.Audio; +using MediaBrowser.Controller.Library; +using MediaBrowser.Model.Logging; +using System; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Persistence; + +namespace Emby.Server.Implementations.Library.Validators +{ + /// + /// Class ArtistsValidator + /// + public class ArtistsValidator + { + /// + /// The _library manager + /// + private readonly ILibraryManager _libraryManager; + + /// + /// The _logger + /// + private readonly ILogger _logger; + private readonly IItemRepository _itemRepo; + + /// + /// Initializes a new instance of the class. + /// + /// The library manager. + /// The logger. + public ArtistsValidator(ILibraryManager libraryManager, ILogger logger, IItemRepository itemRepo) + { + _libraryManager = libraryManager; + _logger = logger; + _itemRepo = itemRepo; + } + + /// + /// Runs the specified progress. + /// + /// The progress. + /// The cancellation token. + /// Task. + public async Task Run(IProgress progress, CancellationToken cancellationToken) + { + var names = _itemRepo.GetAllArtistNames(); + + var numComplete = 0; + var count = names.Count; + + foreach (var name in names) + { + try + { + var item = _libraryManager.GetArtist(name); + + await item.RefreshMetadata(cancellationToken).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + // Don't clutter the log + break; + } + catch (Exception ex) + { + _logger.ErrorException("Error refreshing {0}", ex, name); + } + + numComplete++; + double percent = numComplete; + percent /= count; + percent *= 100; + + progress.Report(percent); + } + + progress.Report(100); + } + } +} diff --git a/Emby.Server.Implementations/Library/Validators/GameGenresPostScanTask.cs b/Emby.Server.Implementations/Library/Validators/GameGenresPostScanTask.cs new file mode 100644 index 000000000..ee6c4461c --- /dev/null +++ b/Emby.Server.Implementations/Library/Validators/GameGenresPostScanTask.cs @@ -0,0 +1,45 @@ +using MediaBrowser.Controller.Library; +using MediaBrowser.Model.Logging; +using System; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Controller.Persistence; + +namespace Emby.Server.Implementations.Library.Validators +{ + /// + /// Class GameGenresPostScanTask + /// + public class GameGenresPostScanTask : ILibraryPostScanTask + { + /// + /// The _library manager + /// + private readonly ILibraryManager _libraryManager; + private readonly ILogger _logger; + private readonly IItemRepository _itemRepo; + + /// + /// Initializes a new instance of the class. + /// + /// The library manager. + /// The logger. + public GameGenresPostScanTask(ILibraryManager libraryManager, ILogger logger, IItemRepository itemRepo) + { + _libraryManager = libraryManager; + _logger = logger; + _itemRepo = itemRepo; + } + + /// + /// Runs the specified progress. + /// + /// The progress. + /// The cancellation token. + /// Task. + public Task Run(IProgress progress, CancellationToken cancellationToken) + { + return new GameGenresValidator(_libraryManager, _logger, _itemRepo).Run(progress, cancellationToken); + } + } +} diff --git a/Emby.Server.Implementations/Library/Validators/GameGenresValidator.cs b/Emby.Server.Implementations/Library/Validators/GameGenresValidator.cs new file mode 100644 index 000000000..b1820bb91 --- /dev/null +++ b/Emby.Server.Implementations/Library/Validators/GameGenresValidator.cs @@ -0,0 +1,74 @@ +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Library; +using MediaBrowser.Model.Logging; +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Controller.Persistence; + +namespace Emby.Server.Implementations.Library.Validators +{ + class GameGenresValidator + { + /// + /// The _library manager + /// + private readonly ILibraryManager _libraryManager; + + /// + /// The _logger + /// + private readonly ILogger _logger; + private readonly IItemRepository _itemRepo; + + public GameGenresValidator(ILibraryManager libraryManager, ILogger logger, IItemRepository itemRepo) + { + _libraryManager = libraryManager; + _logger = logger; + _itemRepo = itemRepo; + } + + /// + /// Runs the specified progress. + /// + /// The progress. + /// The cancellation token. + /// Task. + public async Task Run(IProgress progress, CancellationToken cancellationToken) + { + var names = _itemRepo.GetGameGenreNames(); + + var numComplete = 0; + var count = names.Count; + + foreach (var name in names) + { + try + { + var item = _libraryManager.GetGameGenre(name); + + await item.RefreshMetadata(cancellationToken).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + // Don't clutter the log + break; + } + catch (Exception ex) + { + _logger.ErrorException("Error refreshing {0}", ex, name); + } + + numComplete++; + double percent = numComplete; + percent /= count; + percent *= 100; + + progress.Report(percent); + } + + progress.Report(100); + } + } +} diff --git a/Emby.Server.Implementations/Library/Validators/GenresPostScanTask.cs b/Emby.Server.Implementations/Library/Validators/GenresPostScanTask.cs new file mode 100644 index 000000000..be46decfb --- /dev/null +++ b/Emby.Server.Implementations/Library/Validators/GenresPostScanTask.cs @@ -0,0 +1,42 @@ +using MediaBrowser.Controller.Library; +using System; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Controller.Persistence; +using MediaBrowser.Model.Logging; + +namespace Emby.Server.Implementations.Library.Validators +{ + public class GenresPostScanTask : ILibraryPostScanTask + { + /// + /// The _library manager + /// + private readonly ILibraryManager _libraryManager; + private readonly ILogger _logger; + private readonly IItemRepository _itemRepo; + + /// + /// Initializes a new instance of the class. + /// + /// The library manager. + /// The logger. + public GenresPostScanTask(ILibraryManager libraryManager, ILogger logger, IItemRepository itemRepo) + { + _libraryManager = libraryManager; + _logger = logger; + _itemRepo = itemRepo; + } + + /// + /// Runs the specified progress. + /// + /// The progress. + /// The cancellation token. + /// Task. + public Task Run(IProgress progress, CancellationToken cancellationToken) + { + return new GenresValidator(_libraryManager, _logger, _itemRepo).Run(progress, cancellationToken); + } + } +} diff --git a/Emby.Server.Implementations/Library/Validators/GenresValidator.cs b/Emby.Server.Implementations/Library/Validators/GenresValidator.cs new file mode 100644 index 000000000..d8956f78a --- /dev/null +++ b/Emby.Server.Implementations/Library/Validators/GenresValidator.cs @@ -0,0 +1,75 @@ +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Audio; +using MediaBrowser.Controller.Library; +using MediaBrowser.Model.Logging; +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Controller.Persistence; + +namespace Emby.Server.Implementations.Library.Validators +{ + class GenresValidator + { + /// + /// The _library manager + /// + private readonly ILibraryManager _libraryManager; + private readonly IItemRepository _itemRepo; + + /// + /// The _logger + /// + private readonly ILogger _logger; + + public GenresValidator(ILibraryManager libraryManager, ILogger logger, IItemRepository itemRepo) + { + _libraryManager = libraryManager; + _logger = logger; + _itemRepo = itemRepo; + } + + /// + /// Runs the specified progress. + /// + /// The progress. + /// The cancellation token. + /// Task. + public async Task Run(IProgress progress, CancellationToken cancellationToken) + { + var names = _itemRepo.GetGenreNames(); + + var numComplete = 0; + var count = names.Count; + + foreach (var name in names) + { + try + { + var item = _libraryManager.GetGenre(name); + + await item.RefreshMetadata(cancellationToken).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + // Don't clutter the log + break; + } + catch (Exception ex) + { + _logger.ErrorException("Error refreshing {0}", ex, name); + } + + numComplete++; + double percent = numComplete; + percent /= count; + percent *= 100; + + progress.Report(percent); + } + + progress.Report(100); + } + } +} diff --git a/Emby.Server.Implementations/Library/Validators/MusicGenresPostScanTask.cs b/Emby.Server.Implementations/Library/Validators/MusicGenresPostScanTask.cs new file mode 100644 index 000000000..cd4021548 --- /dev/null +++ b/Emby.Server.Implementations/Library/Validators/MusicGenresPostScanTask.cs @@ -0,0 +1,45 @@ +using MediaBrowser.Controller.Library; +using MediaBrowser.Model.Logging; +using System; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Controller.Persistence; + +namespace Emby.Server.Implementations.Library.Validators +{ + /// + /// Class MusicGenresPostScanTask + /// + public class MusicGenresPostScanTask : ILibraryPostScanTask + { + /// + /// The _library manager + /// + private readonly ILibraryManager _libraryManager; + private readonly ILogger _logger; + private readonly IItemRepository _itemRepo; + + /// + /// Initializes a new instance of the class. + /// + /// The library manager. + /// The logger. + public MusicGenresPostScanTask(ILibraryManager libraryManager, ILogger logger, IItemRepository itemRepo) + { + _libraryManager = libraryManager; + _logger = logger; + _itemRepo = itemRepo; + } + + /// + /// Runs the specified progress. + /// + /// The progress. + /// The cancellation token. + /// Task. + public Task Run(IProgress progress, CancellationToken cancellationToken) + { + return new MusicGenresValidator(_libraryManager, _logger, _itemRepo).Run(progress, cancellationToken); + } + } +} diff --git a/Emby.Server.Implementations/Library/Validators/MusicGenresValidator.cs b/Emby.Server.Implementations/Library/Validators/MusicGenresValidator.cs new file mode 100644 index 000000000..983c881b7 --- /dev/null +++ b/Emby.Server.Implementations/Library/Validators/MusicGenresValidator.cs @@ -0,0 +1,75 @@ +using MediaBrowser.Controller.Entities.Audio; +using MediaBrowser.Controller.Library; +using MediaBrowser.Model.Logging; +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Persistence; + +namespace Emby.Server.Implementations.Library.Validators +{ + class MusicGenresValidator + { + /// + /// The _library manager + /// + private readonly ILibraryManager _libraryManager; + + /// + /// The _logger + /// + private readonly ILogger _logger; + private readonly IItemRepository _itemRepo; + + public MusicGenresValidator(ILibraryManager libraryManager, ILogger logger, IItemRepository itemRepo) + { + _libraryManager = libraryManager; + _logger = logger; + _itemRepo = itemRepo; + } + + /// + /// Runs the specified progress. + /// + /// The progress. + /// The cancellation token. + /// Task. + public async Task Run(IProgress progress, CancellationToken cancellationToken) + { + var names = _itemRepo.GetMusicGenreNames(); + + var numComplete = 0; + var count = names.Count; + + foreach (var name in names) + { + try + { + var item = _libraryManager.GetMusicGenre(name); + + await item.RefreshMetadata(cancellationToken).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + // Don't clutter the log + break; + } + catch (Exception ex) + { + _logger.ErrorException("Error refreshing {0}", ex, name); + } + + numComplete++; + double percent = numComplete; + percent /= count; + percent *= 100; + + progress.Report(percent); + } + + progress.Report(100); + } + } +} diff --git a/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs b/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs new file mode 100644 index 000000000..813f07fff --- /dev/null +++ b/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs @@ -0,0 +1,172 @@ +using MediaBrowser.Common.Progress; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Configuration; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Logging; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Common.IO; +using MediaBrowser.Controller.IO; +using MediaBrowser.Model.IO; + +namespace Emby.Server.Implementations.Library.Validators +{ + /// + /// Class PeopleValidator + /// + public class PeopleValidator + { + /// + /// The _library manager + /// + private readonly ILibraryManager _libraryManager; + /// + /// The _logger + /// + private readonly ILogger _logger; + + private readonly IServerConfigurationManager _config; + private readonly IFileSystem _fileSystem; + + /// + /// Initializes a new instance of the class. + /// + /// The library manager. + /// The logger. + public PeopleValidator(ILibraryManager libraryManager, ILogger logger, IServerConfigurationManager config, IFileSystem fileSystem) + { + _libraryManager = libraryManager; + _logger = logger; + _config = config; + _fileSystem = fileSystem; + } + + private bool DownloadMetadata(PersonInfo i, PeopleMetadataOptions options) + { + if (i.IsType(PersonType.Actor)) + { + return options.DownloadActorMetadata; + } + if (i.IsType(PersonType.Director)) + { + return options.DownloadDirectorMetadata; + } + if (i.IsType(PersonType.Composer)) + { + return options.DownloadComposerMetadata; + } + if (i.IsType(PersonType.Writer)) + { + return options.DownloadWriterMetadata; + } + if (i.IsType(PersonType.Producer)) + { + return options.DownloadProducerMetadata; + } + if (i.IsType(PersonType.GuestStar)) + { + return options.DownloadGuestStarMetadata; + } + + return options.DownloadOtherPeopleMetadata; + } + + /// + /// Validates the people. + /// + /// The cancellation token. + /// The progress. + /// Task. + public async Task ValidatePeople(CancellationToken cancellationToken, IProgress progress) + { + var innerProgress = new ActionableProgress(); + + innerProgress.RegisterAction(pct => progress.Report(pct * .15)); + + var peopleOptions = _config.Configuration.PeopleMetadataOptions; + + var people = _libraryManager.GetPeople(new InternalPeopleQuery()); + + var dict = new Dictionary(StringComparer.OrdinalIgnoreCase); + + foreach (var person in people) + { + var isMetadataEnabled = DownloadMetadata(person, peopleOptions); + + bool currentValue; + if (dict.TryGetValue(person.Name, out currentValue)) + { + if (!currentValue && isMetadataEnabled) + { + dict[person.Name] = true; + } + } + else + { + dict[person.Name] = isMetadataEnabled; + } + } + + var numComplete = 0; + + _logger.Debug("Will refresh {0} people", dict.Count); + + var numPeople = dict.Count; + + foreach (var person in dict) + { + cancellationToken.ThrowIfCancellationRequested(); + + try + { + var item = _libraryManager.GetPerson(person.Key); + + var hasMetdata = !string.IsNullOrWhiteSpace(item.Overview); + var performFullRefresh = !hasMetdata && (DateTime.UtcNow - item.DateLastRefreshed).TotalDays >= 30; + + var defaultMetadataRefreshMode = performFullRefresh + ? MetadataRefreshMode.FullRefresh + : MetadataRefreshMode.Default; + + var imageRefreshMode = performFullRefresh + ? ImageRefreshMode.FullRefresh + : ImageRefreshMode.Default; + + var options = new MetadataRefreshOptions(_fileSystem) + { + MetadataRefreshMode = person.Value ? defaultMetadataRefreshMode : MetadataRefreshMode.ValidationOnly, + ImageRefreshMode = person.Value ? imageRefreshMode : ImageRefreshMode.ValidationOnly, + ForceSave = performFullRefresh + }; + + await item.RefreshMetadata(options, cancellationToken).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + throw; + } + catch (Exception ex) + { + _logger.ErrorException("Error validating IBN entry {0}", ex, person); + } + + // Update progress + numComplete++; + double percent = numComplete; + percent /= numPeople; + + progress.Report(100 * percent); + } + + progress.Report(100); + + _logger.Info("People validation complete"); + } + } +} diff --git a/Emby.Server.Implementations/Library/Validators/StudiosPostScanTask.cs b/Emby.Server.Implementations/Library/Validators/StudiosPostScanTask.cs new file mode 100644 index 000000000..d23efb6d3 --- /dev/null +++ b/Emby.Server.Implementations/Library/Validators/StudiosPostScanTask.cs @@ -0,0 +1,45 @@ +using MediaBrowser.Controller.Library; +using MediaBrowser.Model.Logging; +using System; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Controller.Persistence; + +namespace Emby.Server.Implementations.Library.Validators +{ + /// + /// Class MusicGenresPostScanTask + /// + public class StudiosPostScanTask : ILibraryPostScanTask + { + /// + /// The _library manager + /// + private readonly ILibraryManager _libraryManager; + + private readonly ILogger _logger; + private readonly IItemRepository _itemRepo; + + /// + /// Initializes a new instance of the class. + /// + /// The library manager. + public StudiosPostScanTask(ILibraryManager libraryManager, ILogger logger, IItemRepository itemRepo) + { + _libraryManager = libraryManager; + _logger = logger; + _itemRepo = itemRepo; + } + + /// + /// Runs the specified progress. + /// + /// The progress. + /// The cancellation token. + /// Task. + public Task Run(IProgress progress, CancellationToken cancellationToken) + { + return new StudiosValidator(_libraryManager, _logger, _itemRepo).Run(progress, cancellationToken); + } + } +} diff --git a/Emby.Server.Implementations/Library/Validators/StudiosValidator.cs b/Emby.Server.Implementations/Library/Validators/StudiosValidator.cs new file mode 100644 index 000000000..6faab7bb9 --- /dev/null +++ b/Emby.Server.Implementations/Library/Validators/StudiosValidator.cs @@ -0,0 +1,74 @@ +using MediaBrowser.Controller.Library; +using MediaBrowser.Model.Logging; +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Persistence; + +namespace Emby.Server.Implementations.Library.Validators +{ + class StudiosValidator + { + /// + /// The _library manager + /// + private readonly ILibraryManager _libraryManager; + + private readonly IItemRepository _itemRepo; + /// + /// The _logger + /// + private readonly ILogger _logger; + + public StudiosValidator(ILibraryManager libraryManager, ILogger logger, IItemRepository itemRepo) + { + _libraryManager = libraryManager; + _logger = logger; + _itemRepo = itemRepo; + } + + /// + /// Runs the specified progress. + /// + /// The progress. + /// The cancellation token. + /// Task. + public async Task Run(IProgress progress, CancellationToken cancellationToken) + { + var names = _itemRepo.GetStudioNames(); + + var numComplete = 0; + var count = names.Count; + + foreach (var name in names) + { + try + { + var item = _libraryManager.GetStudio(name); + + await item.RefreshMetadata(cancellationToken).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + // Don't clutter the log + break; + } + catch (Exception ex) + { + _logger.ErrorException("Error refreshing {0}", ex, name); + } + + numComplete++; + double percent = numComplete; + percent /= count; + percent *= 100; + + progress.Report(percent); + } + + progress.Report(100); + } + } +} diff --git a/Emby.Server.Implementations/Library/Validators/YearsPostScanTask.cs b/Emby.Server.Implementations/Library/Validators/YearsPostScanTask.cs new file mode 100644 index 000000000..ae43c77f0 --- /dev/null +++ b/Emby.Server.Implementations/Library/Validators/YearsPostScanTask.cs @@ -0,0 +1,55 @@ +using MediaBrowser.Controller.Library; +using MediaBrowser.Model.Logging; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Emby.Server.Implementations.Library.Validators +{ + public class YearsPostScanTask : ILibraryPostScanTask + { + private readonly ILibraryManager _libraryManager; + private readonly ILogger _logger; + + public YearsPostScanTask(ILibraryManager libraryManager, ILogger logger) + { + _libraryManager = libraryManager; + _logger = logger; + } + + public async Task Run(IProgress progress, CancellationToken cancellationToken) + { + var yearNumber = 1900; + var maxYear = DateTime.UtcNow.Year + 3; + var count = maxYear - yearNumber + 1; + var numComplete = 0; + + while (yearNumber < maxYear) + { + try + { + var year = _libraryManager.GetYear(yearNumber); + + await year.RefreshMetadata(cancellationToken).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + // Don't clutter the log + break; + } + catch (Exception ex) + { + _logger.ErrorException("Error refreshing year {0}", ex, yearNumber); + } + + numComplete++; + double percent = numComplete; + percent /= count; + percent *= 100; + + progress.Report(percent); + yearNumber++; + } + } + } +} -- cgit v1.2.3 From 01fc207b62151a858c0d7edb802a24690505bb95 Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Thu, 10 Nov 2016 20:37:20 -0500 Subject: update portable components --- .../Serialization/JsonSerializer.cs | 227 ++++++ Emby.Common.Implementations/project.json | 45 +- .../Emby.Server.Implementations.csproj | 4 + .../HttpServer/HttpResultFactory.cs | 847 +++++++++++++++++++++ .../Library/Validators/PeopleValidator.cs | 18 +- .../HttpServer/HttpListenerHost.cs | 60 +- .../HttpServer/HttpResultFactory.cs | 835 -------------------- .../SocketSharp/WebSocketSharpRequest.cs | 32 +- .../MediaBrowser.Server.Implementations.csproj | 13 +- .../Serialization/JsonSerializer.cs | 227 ------ .../packages.config | 1 + .../ApplicationHost.cs | 3 +- 12 files changed, 1173 insertions(+), 1139 deletions(-) create mode 100644 Emby.Common.Implementations/Serialization/JsonSerializer.cs create mode 100644 Emby.Server.Implementations/HttpServer/HttpResultFactory.cs delete mode 100644 MediaBrowser.Server.Implementations/HttpServer/HttpResultFactory.cs delete mode 100644 MediaBrowser.Server.Implementations/Serialization/JsonSerializer.cs (limited to 'Emby.Server.Implementations/Library/Validators') diff --git a/Emby.Common.Implementations/Serialization/JsonSerializer.cs b/Emby.Common.Implementations/Serialization/JsonSerializer.cs new file mode 100644 index 000000000..c9db33689 --- /dev/null +++ b/Emby.Common.Implementations/Serialization/JsonSerializer.cs @@ -0,0 +1,227 @@ +using System; +using System.IO; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Serialization; + +namespace Emby.Common.Implementations.Serialization +{ + /// + /// Provides a wrapper around third party json serialization. + /// + public class JsonSerializer : IJsonSerializer + { + private readonly IFileSystem _fileSystem; + private readonly ILogger _logger; + + public JsonSerializer(IFileSystem fileSystem, ILogger logger) + { + _fileSystem = fileSystem; + _logger = logger; + Configure(); + } + + /// + /// Serializes to stream. + /// + /// The obj. + /// The stream. + /// obj + public void SerializeToStream(object obj, Stream stream) + { + if (obj == null) + { + throw new ArgumentNullException("obj"); + } + + if (stream == null) + { + throw new ArgumentNullException("stream"); + } + + ServiceStack.Text.JsonSerializer.SerializeToStream(obj, obj.GetType(), stream); + } + + /// + /// Serializes to file. + /// + /// The obj. + /// The file. + /// obj + public void SerializeToFile(object obj, string file) + { + if (obj == null) + { + throw new ArgumentNullException("obj"); + } + + if (string.IsNullOrEmpty(file)) + { + throw new ArgumentNullException("file"); + } + + using (Stream stream = _fileSystem.GetFileStream(file, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read)) + { + SerializeToStream(obj, stream); + } + } + + private Stream OpenFile(string path) + { + _logger.Debug("Deserializing file {0}", path); + return new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, 131072); + } + + /// + /// Deserializes from file. + /// + /// The type. + /// The file. + /// System.Object. + /// type + public object DeserializeFromFile(Type type, string file) + { + if (type == null) + { + throw new ArgumentNullException("type"); + } + + if (string.IsNullOrEmpty(file)) + { + throw new ArgumentNullException("file"); + } + + using (Stream stream = OpenFile(file)) + { + return DeserializeFromStream(stream, type); + } + } + + /// + /// Deserializes from file. + /// + /// + /// The file. + /// ``0. + /// file + public T DeserializeFromFile(string file) + where T : class + { + if (string.IsNullOrEmpty(file)) + { + throw new ArgumentNullException("file"); + } + + using (Stream stream = OpenFile(file)) + { + return DeserializeFromStream(stream); + } + } + + /// + /// Deserializes from stream. + /// + /// + /// The stream. + /// ``0. + /// stream + public T DeserializeFromStream(Stream stream) + { + if (stream == null) + { + throw new ArgumentNullException("stream"); + } + + return ServiceStack.Text.JsonSerializer.DeserializeFromStream(stream); + } + + /// + /// Deserializes from string. + /// + /// + /// The text. + /// ``0. + /// text + public T DeserializeFromString(string text) + { + if (string.IsNullOrEmpty(text)) + { + throw new ArgumentNullException("text"); + } + + return ServiceStack.Text.JsonSerializer.DeserializeFromString(text); + } + + /// + /// Deserializes from stream. + /// + /// The stream. + /// The type. + /// System.Object. + /// stream + public object DeserializeFromStream(Stream stream, Type type) + { + if (stream == null) + { + throw new ArgumentNullException("stream"); + } + + if (type == null) + { + throw new ArgumentNullException("type"); + } + + return ServiceStack.Text.JsonSerializer.DeserializeFromStream(type, stream); + } + + /// + /// Configures this instance. + /// + private void Configure() + { + ServiceStack.Text.JsConfig.DateHandler = ServiceStack.Text.DateHandler.ISO8601; + ServiceStack.Text.JsConfig.ExcludeTypeInfo = true; + ServiceStack.Text.JsConfig.IncludeNullValues = false; + ServiceStack.Text.JsConfig.AlwaysUseUtc = true; + ServiceStack.Text.JsConfig.AssumeUtc = true; + } + + /// + /// Deserializes from string. + /// + /// The json. + /// The type. + /// System.Object. + /// json + public object DeserializeFromString(string json, Type type) + { + if (string.IsNullOrEmpty(json)) + { + throw new ArgumentNullException("json"); + } + + if (type == null) + { + throw new ArgumentNullException("type"); + } + + return ServiceStack.Text.JsonSerializer.DeserializeFromString(json, type); + } + + /// + /// Serializes to string. + /// + /// The obj. + /// System.String. + /// obj + public string SerializeToString(object obj) + { + if (obj == null) + { + throw new ArgumentNullException("obj"); + } + + return ServiceStack.Text.JsonSerializer.SerializeToString(obj, obj.GetType()); + } + } +} diff --git a/Emby.Common.Implementations/project.json b/Emby.Common.Implementations/project.json index 2b2357e38..dd304606b 100644 --- a/Emby.Common.Implementations/project.json +++ b/Emby.Common.Implementations/project.json @@ -2,7 +2,7 @@ "version": "1.0.0-*", "dependencies": { - + }, "frameworks": { @@ -19,46 +19,49 @@ "System.Text.Encoding": "4.0.0.0", "System.Threading": "4.0.0.0", "System.Threading.Tasks": "4.0.0.0", - "System.Xml.ReaderWriter": "4.0.0" + "System.Xml.ReaderWriter": "4.0.0" }, "dependencies": { "SimpleInjector": "3.2.4", + "ServiceStack.Text": "4.5.4", "NLog": "4.4.0-betaV15", "MediaBrowser.Model": { "target": "project" }, "MediaBrowser.Common": { "target": "project" - } - } + } + } }, "netstandard1.6": { "imports": "dnxcore50", "dependencies": { "NETStandard.Library": "1.6.0", - "System.IO.FileSystem.DriveInfo": "4.0.0", - "System.Diagnostics.Process": "4.1.0", - "System.Threading.Timer": "4.0.1", - "System.Net.Requests": "4.0.11", - "System.Xml.ReaderWriter": "4.0.11", - "System.Xml.XmlSerializer": "4.0.11", - "System.Net.Http": "4.1.0", - "System.Net.Primitives": "4.0.11", - "System.Net.Sockets": "4.1.0", - "System.Net.NetworkInformation": "4.1.0", - "System.Net.NameResolution": "4.0.0", - "System.Runtime.InteropServices.RuntimeInformation": "4.0.0", - "System.Reflection": "4.1.0", - "System.Reflection.Primitives": "4.0.1", - "System.Runtime.Loader": "4.0.0", - "SimpleInjector": "3.2.4", + "System.IO.FileSystem.DriveInfo": "4.0.0", + "System.Diagnostics.Process": "4.1.0", + "System.Threading.Timer": "4.0.1", + "System.Net.Requests": "4.0.11", + "System.Xml.ReaderWriter": "4.0.11", + "System.Xml.XmlSerializer": "4.0.11", + "System.Net.Http": "4.1.0", + "System.Net.Primitives": "4.0.11", + "System.Net.Sockets": "4.1.0", + "System.Net.NetworkInformation": "4.1.0", + "System.Net.NameResolution": "4.0.0", + "System.Runtime.InteropServices.RuntimeInformation": "4.0.0", + "System.Reflection": "4.1.0", + "System.Reflection.Primitives": "4.0.1", + "System.Runtime.Loader": "4.0.0", + "SimpleInjector": "3.2.4", + "ServiceStack.Text.Core": "1.0.27", "NLog": "4.4.0-betaV15", "MediaBrowser.Model": { "target": "project" }, "MediaBrowser.Common": { "target": "project" - } } + } + } } } } diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj index 7e885b779..3f55bd7fb 100644 --- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj +++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj @@ -69,6 +69,7 @@ + @@ -263,6 +264,9 @@ {442b5058-dcaf-4263-bb6a-f21e31120a1b} MediaBrowser.Providers + + ..\ThirdParty\ServiceStack\ServiceStack.dll + ..\ThirdParty\emby\SocketHttpListener.Portable.dll diff --git a/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs b/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs new file mode 100644 index 000000000..bbd556661 --- /dev/null +++ b/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs @@ -0,0 +1,847 @@ +using MediaBrowser.Common.Extensions; +using MediaBrowser.Controller.Net; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Serialization; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.IO.Compression; +using System.Net; +using System.Runtime.Serialization; +using System.Text; +using System.Threading.Tasks; +using System.Xml; +using Emby.Server.Implementations.HttpServer; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Services; +using ServiceStack; +using ServiceStack.Host; +using IRequest = MediaBrowser.Model.Services.IRequest; +using MimeTypes = MediaBrowser.Model.Net.MimeTypes; +using StreamWriter = Emby.Server.Implementations.HttpServer.StreamWriter; + +namespace Emby.Server.Implementations.HttpServer +{ + /// + /// Class HttpResultFactory + /// + public class HttpResultFactory : IHttpResultFactory + { + /// + /// The _logger + /// + private readonly ILogger _logger; + private readonly IFileSystem _fileSystem; + private readonly IJsonSerializer _jsonSerializer; + private readonly IXmlSerializer _xmlSerializer; + + /// + /// Initializes a new instance of the class. + /// + /// The log manager. + /// The file system. + /// The json serializer. + public HttpResultFactory(ILogManager logManager, IFileSystem fileSystem, IJsonSerializer jsonSerializer, IXmlSerializer xmlSerializer) + { + _fileSystem = fileSystem; + _jsonSerializer = jsonSerializer; + _xmlSerializer = xmlSerializer; + _logger = logManager.GetLogger("HttpResultFactory"); + } + + /// + /// Gets the result. + /// + /// The content. + /// Type of the content. + /// The response headers. + /// System.Object. + public object GetResult(object content, string contentType, IDictionary responseHeaders = null) + { + return GetHttpResult(content, contentType, responseHeaders); + } + + /// + /// Gets the HTTP result. + /// + /// The content. + /// Type of the content. + /// The response headers. + /// IHasHeaders. + private IHasHeaders GetHttpResult(object content, string contentType, IDictionary responseHeaders = null) + { + IHasHeaders result; + + var stream = content as Stream; + + if (stream != null) + { + result = new StreamWriter(stream, contentType, _logger); + } + + else + { + var bytes = content as byte[]; + + if (bytes != null) + { + result = new StreamWriter(bytes, contentType, _logger); + } + else + { + var text = content as string; + + if (text != null) + { + result = new StreamWriter(Encoding.UTF8.GetBytes(text), contentType, _logger); + } + else + { + result = new HttpResult(content, contentType); + } + } + } + if (responseHeaders == null) + { + responseHeaders = new Dictionary(); + } + + responseHeaders["Expires"] = "-1"; + AddResponseHeaders(result, responseHeaders); + + return result; + } + + /// + /// Gets the optimized result. + /// + /// + /// The request context. + /// The result. + /// The response headers. + /// System.Object. + /// result + public object GetOptimizedResult(IRequest requestContext, T result, IDictionary responseHeaders = null) + where T : class + { + return GetOptimizedResultInternal(requestContext, result, true, responseHeaders); + } + + private object GetOptimizedResultInternal(IRequest requestContext, T result, bool addCachePrevention, IDictionary responseHeaders = null) + where T : class + { + if (result == null) + { + throw new ArgumentNullException("result"); + } + + var optimizedResult = ToOptimizedResult(requestContext, result); + + if (responseHeaders == null) + { + responseHeaders = new Dictionary(StringComparer.OrdinalIgnoreCase); + } + + if (addCachePrevention) + { + responseHeaders["Expires"] = "-1"; + } + + // Apply headers + var hasHeaders = optimizedResult as IHasHeaders; + + if (hasHeaders != null) + { + AddResponseHeaders(hasHeaders, responseHeaders); + } + + return optimizedResult; + } + + public static string GetCompressionType(IRequest request) + { + var acceptEncoding = request.Headers["Accept-Encoding"]; + + if (!string.IsNullOrWhiteSpace(acceptEncoding)) + { + if (acceptEncoding.Contains("deflate")) + return "deflate"; + + if (acceptEncoding.Contains("gzip")) + return "gzip"; + } + + return null; + } + + /// + /// Returns the optimized result for the IRequestContext. + /// Does not use or store results in any cache. + /// + /// + /// + /// + public object ToOptimizedResult(IRequest request, T dto) + { + request.Response.Dto = dto; + + var compressionType = GetCompressionType(request); + if (compressionType == null) + { + var contentType = request.ResponseContentType; + + switch (GetRealContentType(contentType)) + { + case "application/xml": + case "text/xml": + case "text/xml; charset=utf-8": //"text/xml; charset=utf-8" also matches xml + return SerializeToXmlString(dto); + + case "application/json": + case "text/json": + return _jsonSerializer.SerializeToString(dto); + } + } + + using (var ms = new MemoryStream()) + { + using (var compressionStream = GetCompressionStream(ms, compressionType)) + { + ContentTypes.Instance.SerializeToStream(request, dto, compressionStream); + compressionStream.Dispose(); + + var compressedBytes = ms.ToArray(); + + var httpResult = new HttpResult(compressedBytes, request.ResponseContentType) + { + Status = request.Response.StatusCode + }; + + httpResult.Headers["Content-Length"] = compressedBytes.Length.ToString(UsCulture); + httpResult.Headers["Content-Encoding"] = compressionType; + + return httpResult; + } + } + } + + public static string GetRealContentType(string contentType) + { + return contentType == null + ? null + : contentType.Split(';')[0].ToLower().Trim(); + } + + public static string SerializeToXmlString(object from) + { + using (var ms = new MemoryStream()) + { + var xwSettings = new XmlWriterSettings(); + xwSettings.Encoding = new UTF8Encoding(false); + xwSettings.OmitXmlDeclaration = false; + + using (var xw = XmlWriter.Create(ms, xwSettings)) + { + var serializer = new DataContractSerializer(from.GetType()); + serializer.WriteObject(xw, from); + xw.Flush(); + ms.Seek(0, SeekOrigin.Begin); + var reader = new StreamReader(ms); + return reader.ReadToEnd(); + } + } + } + + private static Stream GetCompressionStream(Stream outputStream, string compressionType) + { + if (compressionType == "deflate") + return new DeflateStream(outputStream, CompressionMode.Compress); + if (compressionType == "gzip") + return new GZipStream(outputStream, CompressionMode.Compress); + + throw new NotSupportedException(compressionType); + } + + /// + /// Gets the optimized result using cache. + /// + /// + /// The request context. + /// The cache key. + /// The last date modified. + /// Duration of the cache. + /// The factory fn. + /// The response headers. + /// System.Object. + /// cacheKey + /// or + /// factoryFn + public object GetOptimizedResultUsingCache(IRequest requestContext, Guid cacheKey, DateTime? lastDateModified, TimeSpan? cacheDuration, Func factoryFn, IDictionary responseHeaders = null) + where T : class + { + if (cacheKey == Guid.Empty) + { + throw new ArgumentNullException("cacheKey"); + } + if (factoryFn == null) + { + throw new ArgumentNullException("factoryFn"); + } + + var key = cacheKey.ToString("N"); + + if (responseHeaders == null) + { + responseHeaders = new Dictionary(StringComparer.OrdinalIgnoreCase); + } + + // See if the result is already cached in the browser + var result = GetCachedResult(requestContext, responseHeaders, cacheKey, key, lastDateModified, cacheDuration, null); + + if (result != null) + { + return result; + } + + return GetOptimizedResultInternal(requestContext, factoryFn(), false, responseHeaders); + } + + /// + /// To the cached result. + /// + /// + /// The request context. + /// The cache key. + /// The last date modified. + /// Duration of the cache. + /// The factory fn. + /// Type of the content. + /// The response headers. + /// System.Object. + /// cacheKey + public object GetCachedResult(IRequest requestContext, Guid cacheKey, DateTime? lastDateModified, TimeSpan? cacheDuration, Func factoryFn, string contentType, IDictionary responseHeaders = null) + where T : class + { + if (cacheKey == Guid.Empty) + { + throw new ArgumentNullException("cacheKey"); + } + if (factoryFn == null) + { + throw new ArgumentNullException("factoryFn"); + } + + var key = cacheKey.ToString("N"); + + if (responseHeaders == null) + { + responseHeaders = new Dictionary(StringComparer.OrdinalIgnoreCase); + } + + // See if the result is already cached in the browser + var result = GetCachedResult(requestContext, responseHeaders, cacheKey, key, lastDateModified, cacheDuration, contentType); + + if (result != null) + { + return result; + } + + result = factoryFn(); + + // Apply caching headers + var hasHeaders = result as IHasHeaders; + + if (hasHeaders != null) + { + AddResponseHeaders(hasHeaders, responseHeaders); + return hasHeaders; + } + + IHasHeaders httpResult; + + var stream = result as Stream; + + if (stream != null) + { + httpResult = new StreamWriter(stream, contentType, _logger); + } + else + { + // Otherwise wrap into an HttpResult + httpResult = new HttpResult(result, contentType ?? "text/html", HttpStatusCode.NotModified); + } + + AddResponseHeaders(httpResult, responseHeaders); + + return httpResult; + } + + /// + /// Pres the process optimized result. + /// + /// The request context. + /// The responseHeaders. + /// The cache key. + /// The cache key string. + /// The last date modified. + /// Duration of the cache. + /// Type of the content. + /// System.Object. + private object GetCachedResult(IRequest requestContext, IDictionary responseHeaders, Guid cacheKey, string cacheKeyString, DateTime? lastDateModified, TimeSpan? cacheDuration, string contentType) + { + responseHeaders["ETag"] = string.Format("\"{0}\"", cacheKeyString); + + if (IsNotModified(requestContext, cacheKey, lastDateModified, cacheDuration)) + { + AddAgeHeader(responseHeaders, lastDateModified); + AddExpiresHeader(responseHeaders, cacheKeyString, cacheDuration); + + var result = new HttpResult(new byte[] { }, contentType ?? "text/html", HttpStatusCode.NotModified); + + AddResponseHeaders(result, responseHeaders); + + return result; + } + + AddCachingHeaders(responseHeaders, cacheKeyString, lastDateModified, cacheDuration); + + return null; + } + + public Task GetStaticFileResult(IRequest requestContext, + string path, + FileShareMode fileShare = FileShareMode.Read) + { + if (string.IsNullOrEmpty(path)) + { + throw new ArgumentNullException("path"); + } + + return GetStaticFileResult(requestContext, new StaticFileResultOptions + { + Path = path, + FileShare = fileShare + }); + } + + public Task GetStaticFileResult(IRequest requestContext, + StaticFileResultOptions options) + { + var path = options.Path; + var fileShare = options.FileShare; + + if (string.IsNullOrEmpty(path)) + { + throw new ArgumentNullException("path"); + } + + if (fileShare != FileShareMode.Read && fileShare != FileShareMode.ReadWrite) + { + throw new ArgumentException("FileShare must be either Read or ReadWrite"); + } + + if (string.IsNullOrWhiteSpace(options.ContentType)) + { + options.ContentType = MimeTypes.GetMimeType(path); + } + + if (!options.DateLastModified.HasValue) + { + options.DateLastModified = _fileSystem.GetLastWriteTimeUtc(path); + } + + var cacheKey = path + options.DateLastModified.Value.Ticks; + + options.CacheKey = cacheKey.GetMD5(); + options.ContentFactory = () => Task.FromResult(GetFileStream(path, fileShare)); + + return GetStaticResult(requestContext, options); + } + + /// + /// Gets the file stream. + /// + /// The path. + /// The file share. + /// Stream. + private Stream GetFileStream(string path, FileShareMode fileShare) + { + return _fileSystem.GetFileStream(path, FileOpenMode.Open, FileAccessMode.Read, fileShare); + } + + public Task GetStaticResult(IRequest requestContext, + Guid cacheKey, + DateTime? lastDateModified, + TimeSpan? cacheDuration, + string contentType, + Func> factoryFn, + IDictionary responseHeaders = null, + bool isHeadRequest = false) + { + return GetStaticResult(requestContext, new StaticResultOptions + { + CacheDuration = cacheDuration, + CacheKey = cacheKey, + ContentFactory = factoryFn, + ContentType = contentType, + DateLastModified = lastDateModified, + IsHeadRequest = isHeadRequest, + ResponseHeaders = responseHeaders + }); + } + + public async Task GetStaticResult(IRequest requestContext, StaticResultOptions options) + { + var cacheKey = options.CacheKey; + options.ResponseHeaders = options.ResponseHeaders ?? new Dictionary(StringComparer.OrdinalIgnoreCase); + var contentType = options.ContentType; + + if (cacheKey == Guid.Empty) + { + throw new ArgumentNullException("cacheKey"); + } + if (options.ContentFactory == null) + { + throw new ArgumentNullException("factoryFn"); + } + + var key = cacheKey.ToString("N"); + + // See if the result is already cached in the browser + var result = GetCachedResult(requestContext, options.ResponseHeaders, cacheKey, key, options.DateLastModified, options.CacheDuration, contentType); + + if (result != null) + { + return result; + } + + var compress = ShouldCompressResponse(requestContext, contentType); + var hasHeaders = await GetStaticResult(requestContext, options, compress).ConfigureAwait(false); + AddResponseHeaders(hasHeaders, options.ResponseHeaders); + + return hasHeaders; + } + + /// + /// Shoulds the compress response. + /// + /// The request context. + /// Type of the content. + /// true if XXXX, false otherwise + private bool ShouldCompressResponse(IRequest requestContext, string contentType) + { + // It will take some work to support compression with byte range requests + if (!string.IsNullOrEmpty(requestContext.Headers.Get("Range"))) + { + return false; + } + + // Don't compress media + if (contentType.StartsWith("audio/", StringComparison.OrdinalIgnoreCase) || contentType.StartsWith("video/", StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + // Don't compress images + if (contentType.StartsWith("image/", StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + if (contentType.StartsWith("font/", StringComparison.OrdinalIgnoreCase)) + { + return false; + } + if (contentType.StartsWith("application/", StringComparison.OrdinalIgnoreCase)) + { + if (string.Equals(contentType, "application/x-javascript", StringComparison.OrdinalIgnoreCase)) + { + return true; + } + if (string.Equals(contentType, "application/xml", StringComparison.OrdinalIgnoreCase)) + { + return true; + } + return false; + } + + return true; + } + + /// + /// The us culture + /// + private static readonly CultureInfo UsCulture = new CultureInfo("en-US"); + + private async Task GetStaticResult(IRequest requestContext, StaticResultOptions options, bool compress) + { + var isHeadRequest = options.IsHeadRequest; + var factoryFn = options.ContentFactory; + var contentType = options.ContentType; + var responseHeaders = options.ResponseHeaders; + + var requestedCompressionType = GetCompressionType(requestContext); + + if (!compress || string.IsNullOrEmpty(requestedCompressionType)) + { + var rangeHeader = requestContext.Headers.Get("Range"); + + var stream = await factoryFn().ConfigureAwait(false); + + if (!string.IsNullOrEmpty(rangeHeader)) + { + return new RangeRequestWriter(rangeHeader, stream, contentType, isHeadRequest, _logger) + { + OnComplete = options.OnComplete + }; + } + + responseHeaders["Content-Length"] = stream.Length.ToString(UsCulture); + + if (isHeadRequest) + { + stream.Dispose(); + + return GetHttpResult(new byte[] { }, contentType); + } + + return new StreamWriter(stream, contentType, _logger) + { + OnComplete = options.OnComplete, + OnError = options.OnError + }; + } + + string content; + + using (var stream = await factoryFn().ConfigureAwait(false)) + { + using (var reader = new StreamReader(stream)) + { + content = await reader.ReadToEndAsync().ConfigureAwait(false); + } + } + + var contents = Compress(content, requestedCompressionType); + + responseHeaders["Content-Length"] = contents.Length.ToString(UsCulture); + responseHeaders["Content-Encoding"] = requestedCompressionType; + + if (isHeadRequest) + { + return GetHttpResult(new byte[] { }, contentType); + } + + return GetHttpResult(contents, contentType, responseHeaders); + } + + public static byte[] Compress(string text, string compressionType) + { + if (compressionType == "deflate") + return Deflate(text); + + if (compressionType == "gzip") + return GZip(text); + + throw new NotSupportedException(compressionType); + } + + public static byte[] Deflate(string text) + { + return Deflate(Encoding.UTF8.GetBytes(text)); + } + + public static byte[] Deflate(byte[] bytes) + { + // In .NET FX incompat-ville, you can't access compressed bytes without closing DeflateStream + // Which means we must use MemoryStream since you have to use ToArray() on a closed Stream + using (var ms = new MemoryStream()) + using (var zipStream = new DeflateStream(ms, CompressionMode.Compress)) + { + zipStream.Write(bytes, 0, bytes.Length); + zipStream.Dispose(); + + return ms.ToArray(); + } + } + + public static byte[] GZip(string text) + { + return GZip(Encoding.UTF8.GetBytes(text)); + } + + public static byte[] GZip(byte[] buffer) + { + using (var ms = new MemoryStream()) + using (var zipStream = new GZipStream(ms, CompressionMode.Compress)) + { + zipStream.Write(buffer, 0, buffer.Length); + zipStream.Dispose(); + + return ms.ToArray(); + } + } + + /// + /// Adds the caching responseHeaders. + /// + /// The responseHeaders. + /// The cache key. + /// The last date modified. + /// Duration of the cache. + private void AddCachingHeaders(IDictionary responseHeaders, string cacheKey, DateTime? lastDateModified, TimeSpan? cacheDuration) + { + // Don't specify both last modified and Etag, unless caching unconditionally. They are redundant + // https://developers.google.com/speed/docs/best-practices/caching#LeverageBrowserCaching + if (lastDateModified.HasValue && (string.IsNullOrEmpty(cacheKey) || cacheDuration.HasValue)) + { + AddAgeHeader(responseHeaders, lastDateModified); + responseHeaders["Last-Modified"] = lastDateModified.Value.ToString("r"); + } + + if (cacheDuration.HasValue) + { + responseHeaders["Cache-Control"] = "public, max-age=" + Convert.ToInt32(cacheDuration.Value.TotalSeconds); + } + else if (!string.IsNullOrEmpty(cacheKey)) + { + responseHeaders["Cache-Control"] = "public"; + } + else + { + responseHeaders["Cache-Control"] = "no-cache, no-store, must-revalidate"; + responseHeaders["pragma"] = "no-cache, no-store, must-revalidate"; + } + + AddExpiresHeader(responseHeaders, cacheKey, cacheDuration); + } + + /// + /// Adds the expires header. + /// + /// The responseHeaders. + /// The cache key. + /// Duration of the cache. + private void AddExpiresHeader(IDictionary responseHeaders, string cacheKey, TimeSpan? cacheDuration) + { + if (cacheDuration.HasValue) + { + responseHeaders["Expires"] = DateTime.UtcNow.Add(cacheDuration.Value).ToString("r"); + } + else if (string.IsNullOrEmpty(cacheKey)) + { + responseHeaders["Expires"] = "-1"; + } + } + + /// + /// Adds the age header. + /// + /// The responseHeaders. + /// The last date modified. + private void AddAgeHeader(IDictionary responseHeaders, DateTime? lastDateModified) + { + if (lastDateModified.HasValue) + { + responseHeaders["Age"] = Convert.ToInt64((DateTime.UtcNow - lastDateModified.Value).TotalSeconds).ToString(CultureInfo.InvariantCulture); + } + } + /// + /// Determines whether [is not modified] [the specified cache key]. + /// + /// The request context. + /// The cache key. + /// The last date modified. + /// Duration of the cache. + /// true if [is not modified] [the specified cache key]; otherwise, false. + private bool IsNotModified(IRequest requestContext, Guid? cacheKey, DateTime? lastDateModified, TimeSpan? cacheDuration) + { + var isNotModified = true; + + var ifModifiedSinceHeader = requestContext.Headers.Get("If-Modified-Since"); + + if (!string.IsNullOrEmpty(ifModifiedSinceHeader)) + { + DateTime ifModifiedSince; + + if (DateTime.TryParse(ifModifiedSinceHeader, out ifModifiedSince)) + { + isNotModified = IsNotModified(ifModifiedSince.ToUniversalTime(), cacheDuration, lastDateModified); + } + } + + var ifNoneMatchHeader = requestContext.Headers.Get("If-None-Match"); + + // Validate If-None-Match + if (isNotModified && (cacheKey.HasValue || !string.IsNullOrEmpty(ifNoneMatchHeader))) + { + Guid ifNoneMatch; + + if (Guid.TryParse(ifNoneMatchHeader ?? string.Empty, out ifNoneMatch)) + { + if (cacheKey.HasValue && cacheKey.Value == ifNoneMatch) + { + return true; + } + } + } + + return false; + } + + /// + /// Determines whether [is not modified] [the specified if modified since]. + /// + /// If modified since. + /// Duration of the cache. + /// The date modified. + /// true if [is not modified] [the specified if modified since]; otherwise, false. + private bool IsNotModified(DateTime ifModifiedSince, TimeSpan? cacheDuration, DateTime? dateModified) + { + if (dateModified.HasValue) + { + var lastModified = NormalizeDateForComparison(dateModified.Value); + ifModifiedSince = NormalizeDateForComparison(ifModifiedSince); + + return lastModified <= ifModifiedSince; + } + + if (cacheDuration.HasValue) + { + var cacheExpirationDate = ifModifiedSince.Add(cacheDuration.Value); + + if (DateTime.UtcNow < cacheExpirationDate) + { + return true; + } + } + + return false; + } + + + /// + /// When the browser sends the IfModifiedDate, it's precision is limited to seconds, so this will account for that + /// + /// The date. + /// DateTime. + private DateTime NormalizeDateForComparison(DateTime date) + { + return new DateTime(date.Year, date.Month, date.Day, date.Hour, date.Minute, date.Second, date.Kind); + } + + /// + /// Adds the response headers. + /// + /// The has options. + /// The response headers. + private void AddResponseHeaders(IHasHeaders hasHeaders, IEnumerable> responseHeaders) + { + foreach (var item in responseHeaders) + { + hasHeaders.Headers[item.Key] = item.Value; + } + } + } +} \ No newline at end of file diff --git a/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs b/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs index 813f07fff..c32af8eb1 100644 --- a/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs +++ b/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs @@ -127,23 +127,7 @@ namespace Emby.Server.Implementations.Library.Validators { var item = _libraryManager.GetPerson(person.Key); - var hasMetdata = !string.IsNullOrWhiteSpace(item.Overview); - var performFullRefresh = !hasMetdata && (DateTime.UtcNow - item.DateLastRefreshed).TotalDays >= 30; - - var defaultMetadataRefreshMode = performFullRefresh - ? MetadataRefreshMode.FullRefresh - : MetadataRefreshMode.Default; - - var imageRefreshMode = performFullRefresh - ? ImageRefreshMode.FullRefresh - : ImageRefreshMode.Default; - - var options = new MetadataRefreshOptions(_fileSystem) - { - MetadataRefreshMode = person.Value ? defaultMetadataRefreshMode : MetadataRefreshMode.ValidationOnly, - ImageRefreshMode = person.Value ? imageRefreshMode : ImageRefreshMode.ValidationOnly, - ForceSave = performFullRefresh - }; + var options = new MetadataRefreshOptions(_fileSystem); await item.RefreshMetadata(options, cancellationToken).ConfigureAwait(false); } diff --git a/MediaBrowser.Server.Implementations/HttpServer/HttpListenerHost.cs b/MediaBrowser.Server.Implementations/HttpServer/HttpListenerHost.cs index 643d0c956..4218370ac 100644 --- a/MediaBrowser.Server.Implementations/HttpServer/HttpListenerHost.cs +++ b/MediaBrowser.Server.Implementations/HttpServer/HttpListenerHost.cs @@ -28,6 +28,7 @@ using MediaBrowser.Model.Net; using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Services; using MediaBrowser.Model.Text; +using ServiceStack.Text.Jsv; using SocketHttpListener.Net; using SocketHttpListener.Primitives; @@ -87,9 +88,7 @@ namespace MediaBrowser.Server.Implementations.HttpServer public override void Configure() { - HostConfig.Instance.DefaultRedirectPath = DefaultRedirectPath; - - HostConfig.Instance.MapExceptionToStatusCode = new Dictionary + var mapExceptionToStatusCode = new Dictionary { {typeof (InvalidOperationException), 500}, {typeof (NotImplementedException), 500}, @@ -126,21 +125,24 @@ namespace MediaBrowser.Server.Implementations.HttpServer return _appHost.Resolve(); } - public override T TryResolve() + public override Type[] GetGenericArguments(Type type) { - return _appHost.TryResolve(); + return type.GetGenericArguments(); } - public override object CreateInstance(Type type) + public override bool IsAssignableFrom(Type type1, Type type2) { - return _appHost.CreateInstance(type); + return type1.IsAssignableFrom(type2); } - public override void OnConfigLoad() + public override T TryResolve() { - base.OnConfigLoad(); + return _appHost.TryResolve(); + } - Config.HandlerFactoryPath = null; + public override object CreateInstance(Type type) + { + return _appHost.CreateInstance(type); } protected override ServiceController CreateServiceController(params Assembly[] assembliesWithServices) @@ -156,12 +158,14 @@ namespace MediaBrowser.Server.Implementations.HttpServer return this; } + public static string HandlerFactoryPath; + /// /// Starts the Web Service /// private void StartListener() { - HostContext.Config.HandlerFactoryPath = GetHandlerPathIfAny(UrlPrefixes.First()); + HandlerFactoryPath = GetHandlerPathIfAny(UrlPrefixes.First()); _listener = GetListener(); @@ -610,6 +614,40 @@ namespace MediaBrowser.Server.Implementations.HttpServer return routes.ToArray(); } + public override object GetTaskResult(Task task, string requestName) + { + try + { + var taskObject = task as Task; + if (taskObject != null) + { + return taskObject.Result; + } + + task.Wait(); + + var type = task.GetType(); + if (!type.IsGenericType) + { + return null; + } + + Logger.Warn("Getting task result from " + requestName + " using reflection. For better performance have your api return Task"); + return type.GetProperty("Result").GetValue(task); + } + catch (TypeAccessException) + { + return null; //return null for void Task's + } + } + + public override Func GetParseFn(Type propertyType) + { + var fn = JsvReader.GetParseFn(propertyType); + + return s => fn(s); + } + public override void SerializeToJson(object o, Stream stream) { _jsonSerializer.SerializeToStream(o, stream); diff --git a/MediaBrowser.Server.Implementations/HttpServer/HttpResultFactory.cs b/MediaBrowser.Server.Implementations/HttpServer/HttpResultFactory.cs deleted file mode 100644 index b013a0952..000000000 --- a/MediaBrowser.Server.Implementations/HttpServer/HttpResultFactory.cs +++ /dev/null @@ -1,835 +0,0 @@ -using MediaBrowser.Common.Extensions; -using MediaBrowser.Controller.Net; -using MediaBrowser.Model.Logging; -using MediaBrowser.Model.Serialization; -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.IO.Compression; -using System.Net; -using System.Runtime.Serialization; -using System.Text; -using System.Threading.Tasks; -using System.Xml; -using Emby.Server.Implementations.HttpServer; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.Services; -using ServiceStack; -using ServiceStack.Host; -using IRequest = MediaBrowser.Model.Services.IRequest; -using MimeTypes = MediaBrowser.Model.Net.MimeTypes; -using StreamWriter = Emby.Server.Implementations.HttpServer.StreamWriter; - -namespace MediaBrowser.Server.Implementations.HttpServer -{ - /// - /// Class HttpResultFactory - /// - public class HttpResultFactory : IHttpResultFactory - { - /// - /// The _logger - /// - private readonly ILogger _logger; - private readonly IFileSystem _fileSystem; - private readonly IJsonSerializer _jsonSerializer; - private readonly IXmlSerializer _xmlSerializer; - - /// - /// Initializes a new instance of the class. - /// - /// The log manager. - /// The file system. - /// The json serializer. - public HttpResultFactory(ILogManager logManager, IFileSystem fileSystem, IJsonSerializer jsonSerializer, IXmlSerializer xmlSerializer) - { - _fileSystem = fileSystem; - _jsonSerializer = jsonSerializer; - _xmlSerializer = xmlSerializer; - _logger = logManager.GetLogger("HttpResultFactory"); - } - - /// - /// Gets the result. - /// - /// The content. - /// Type of the content. - /// The response headers. - /// System.Object. - public object GetResult(object content, string contentType, IDictionary responseHeaders = null) - { - return GetHttpResult(content, contentType, responseHeaders); - } - - /// - /// Gets the HTTP result. - /// - /// The content. - /// Type of the content. - /// The response headers. - /// IHasHeaders. - private IHasHeaders GetHttpResult(object content, string contentType, IDictionary responseHeaders = null) - { - IHasHeaders result; - - var stream = content as Stream; - - if (stream != null) - { - result = new StreamWriter(stream, contentType, _logger); - } - - else - { - var bytes = content as byte[]; - - if (bytes != null) - { - result = new StreamWriter(bytes, contentType, _logger); - } - else - { - var text = content as string; - - if (text != null) - { - result = new StreamWriter(Encoding.UTF8.GetBytes(text), contentType, _logger); - } - else - { - result = new HttpResult(content, contentType); - } - } - } - if (responseHeaders == null) - { - responseHeaders = new Dictionary(); - } - - responseHeaders["Expires"] = "-1"; - AddResponseHeaders(result, responseHeaders); - - return result; - } - - /// - /// Gets the optimized result. - /// - /// - /// The request context. - /// The result. - /// The response headers. - /// System.Object. - /// result - public object GetOptimizedResult(IRequest requestContext, T result, IDictionary responseHeaders = null) - where T : class - { - return GetOptimizedResultInternal(requestContext, result, true, responseHeaders); - } - - private object GetOptimizedResultInternal(IRequest requestContext, T result, bool addCachePrevention, IDictionary responseHeaders = null) - where T : class - { - if (result == null) - { - throw new ArgumentNullException("result"); - } - - var optimizedResult = ToOptimizedResult(requestContext, result); - - if (responseHeaders == null) - { - responseHeaders = new Dictionary(StringComparer.OrdinalIgnoreCase); - } - - if (addCachePrevention) - { - responseHeaders["Expires"] = "-1"; - } - - // Apply headers - var hasHeaders = optimizedResult as IHasHeaders; - - if (hasHeaders != null) - { - AddResponseHeaders(hasHeaders, responseHeaders); - } - - return optimizedResult; - } - - public static string GetCompressionType(IRequest request) - { - var prefs = new RequestPreferences(request); - - if (prefs.AcceptsDeflate) - return "deflate"; - - if (prefs.AcceptsGzip) - return "gzip"; - - return null; - } - - /// - /// Returns the optimized result for the IRequestContext. - /// Does not use or store results in any cache. - /// - /// - /// - /// - public object ToOptimizedResult(IRequest request, T dto) - { - request.Response.Dto = dto; - - var compressionType = GetCompressionType(request); - if (compressionType == null) - { - var contentType = request.ResponseContentType; - var contentTypeAttr = ContentFormat.GetEndpointAttributes(contentType); - - switch (contentTypeAttr) - { - case RequestAttributes.Xml: - return SerializeToXmlString(dto); - - case RequestAttributes.Json: - return _jsonSerializer.SerializeToString(dto); - } - } - - using (var ms = new MemoryStream()) - { - using (var compressionStream = GetCompressionStream(ms, compressionType)) - { - ContentTypes.Instance.SerializeToStream(request, dto, compressionStream); - compressionStream.Close(); - - var compressedBytes = ms.ToArray(); - - var httpResult = new HttpResult(compressedBytes, request.ResponseContentType) - { - Status = request.Response.StatusCode - }; - - httpResult.Headers["Content-Length"] = compressedBytes.Length.ToString(UsCulture); - httpResult.Headers["Content-Encoding"] = compressionType; - - return httpResult; - } - } - } - - public static string SerializeToXmlString(object from) - { - using (var ms = new MemoryStream()) - { - var xwSettings = new XmlWriterSettings(); - xwSettings.Encoding = new UTF8Encoding(false); - xwSettings.OmitXmlDeclaration = false; - - using (var xw = XmlWriter.Create(ms, xwSettings)) - { - var serializer = new DataContractSerializer(from.GetType()); - serializer.WriteObject(xw, from); - xw.Flush(); - ms.Seek(0, SeekOrigin.Begin); - var reader = new StreamReader(ms); - return reader.ReadToEnd(); - } - } - } - - private static Stream GetCompressionStream(Stream outputStream, string compressionType) - { - if (compressionType == "deflate") - return new DeflateStream(outputStream, CompressionMode.Compress); - if (compressionType == "gzip") - return new GZipStream(outputStream, CompressionMode.Compress); - - throw new NotSupportedException(compressionType); - } - - /// - /// Gets the optimized result using cache. - /// - /// - /// The request context. - /// The cache key. - /// The last date modified. - /// Duration of the cache. - /// The factory fn. - /// The response headers. - /// System.Object. - /// cacheKey - /// or - /// factoryFn - public object GetOptimizedResultUsingCache(IRequest requestContext, Guid cacheKey, DateTime? lastDateModified, TimeSpan? cacheDuration, Func factoryFn, IDictionary responseHeaders = null) - where T : class - { - if (cacheKey == Guid.Empty) - { - throw new ArgumentNullException("cacheKey"); - } - if (factoryFn == null) - { - throw new ArgumentNullException("factoryFn"); - } - - var key = cacheKey.ToString("N"); - - if (responseHeaders == null) - { - responseHeaders = new Dictionary(StringComparer.OrdinalIgnoreCase); - } - - // See if the result is already cached in the browser - var result = GetCachedResult(requestContext, responseHeaders, cacheKey, key, lastDateModified, cacheDuration, null); - - if (result != null) - { - return result; - } - - return GetOptimizedResultInternal(requestContext, factoryFn(), false, responseHeaders); - } - - /// - /// To the cached result. - /// - /// - /// The request context. - /// The cache key. - /// The last date modified. - /// Duration of the cache. - /// The factory fn. - /// Type of the content. - /// The response headers. - /// System.Object. - /// cacheKey - public object GetCachedResult(IRequest requestContext, Guid cacheKey, DateTime? lastDateModified, TimeSpan? cacheDuration, Func factoryFn, string contentType, IDictionary responseHeaders = null) - where T : class - { - if (cacheKey == Guid.Empty) - { - throw new ArgumentNullException("cacheKey"); - } - if (factoryFn == null) - { - throw new ArgumentNullException("factoryFn"); - } - - var key = cacheKey.ToString("N"); - - if (responseHeaders == null) - { - responseHeaders = new Dictionary(StringComparer.OrdinalIgnoreCase); - } - - // See if the result is already cached in the browser - var result = GetCachedResult(requestContext, responseHeaders, cacheKey, key, lastDateModified, cacheDuration, contentType); - - if (result != null) - { - return result; - } - - result = factoryFn(); - - // Apply caching headers - var hasHeaders = result as IHasHeaders; - - if (hasHeaders != null) - { - AddResponseHeaders(hasHeaders, responseHeaders); - return hasHeaders; - } - - IHasHeaders httpResult; - - var stream = result as Stream; - - if (stream != null) - { - httpResult = new StreamWriter(stream, contentType, _logger); - } - else - { - // Otherwise wrap into an HttpResult - httpResult = new HttpResult(result, contentType ?? "text/html", HttpStatusCode.NotModified); - } - - AddResponseHeaders(httpResult, responseHeaders); - - return httpResult; - } - - /// - /// Pres the process optimized result. - /// - /// The request context. - /// The responseHeaders. - /// The cache key. - /// The cache key string. - /// The last date modified. - /// Duration of the cache. - /// Type of the content. - /// System.Object. - private object GetCachedResult(IRequest requestContext, IDictionary responseHeaders, Guid cacheKey, string cacheKeyString, DateTime? lastDateModified, TimeSpan? cacheDuration, string contentType) - { - responseHeaders["ETag"] = string.Format("\"{0}\"", cacheKeyString); - - if (IsNotModified(requestContext, cacheKey, lastDateModified, cacheDuration)) - { - AddAgeHeader(responseHeaders, lastDateModified); - AddExpiresHeader(responseHeaders, cacheKeyString, cacheDuration); - - var result = new HttpResult(new byte[] { }, contentType ?? "text/html", HttpStatusCode.NotModified); - - AddResponseHeaders(result, responseHeaders); - - return result; - } - - AddCachingHeaders(responseHeaders, cacheKeyString, lastDateModified, cacheDuration); - - return null; - } - - public Task GetStaticFileResult(IRequest requestContext, - string path, - FileShareMode fileShare = FileShareMode.Read) - { - if (string.IsNullOrEmpty(path)) - { - throw new ArgumentNullException("path"); - } - - return GetStaticFileResult(requestContext, new StaticFileResultOptions - { - Path = path, - FileShare = fileShare - }); - } - - public Task GetStaticFileResult(IRequest requestContext, - StaticFileResultOptions options) - { - var path = options.Path; - var fileShare = options.FileShare; - - if (string.IsNullOrEmpty(path)) - { - throw new ArgumentNullException("path"); - } - - if (fileShare != FileShareMode.Read && fileShare != FileShareMode.ReadWrite) - { - throw new ArgumentException("FileShare must be either Read or ReadWrite"); - } - - if (string.IsNullOrWhiteSpace(options.ContentType)) - { - options.ContentType = MimeTypes.GetMimeType(path); - } - - if (!options.DateLastModified.HasValue) - { - options.DateLastModified = _fileSystem.GetLastWriteTimeUtc(path); - } - - var cacheKey = path + options.DateLastModified.Value.Ticks; - - options.CacheKey = cacheKey.GetMD5(); - options.ContentFactory = () => Task.FromResult(GetFileStream(path, fileShare)); - - return GetStaticResult(requestContext, options); - } - - /// - /// Gets the file stream. - /// - /// The path. - /// The file share. - /// Stream. - private Stream GetFileStream(string path, FileShareMode fileShare) - { - return _fileSystem.GetFileStream(path, FileOpenMode.Open, FileAccessMode.Read, fileShare); - } - - public Task GetStaticResult(IRequest requestContext, - Guid cacheKey, - DateTime? lastDateModified, - TimeSpan? cacheDuration, - string contentType, - Func> factoryFn, - IDictionary responseHeaders = null, - bool isHeadRequest = false) - { - return GetStaticResult(requestContext, new StaticResultOptions - { - CacheDuration = cacheDuration, - CacheKey = cacheKey, - ContentFactory = factoryFn, - ContentType = contentType, - DateLastModified = lastDateModified, - IsHeadRequest = isHeadRequest, - ResponseHeaders = responseHeaders - }); - } - - public async Task GetStaticResult(IRequest requestContext, StaticResultOptions options) - { - var cacheKey = options.CacheKey; - options.ResponseHeaders = options.ResponseHeaders ?? new Dictionary(StringComparer.OrdinalIgnoreCase); - var contentType = options.ContentType; - - if (cacheKey == Guid.Empty) - { - throw new ArgumentNullException("cacheKey"); - } - if (options.ContentFactory == null) - { - throw new ArgumentNullException("factoryFn"); - } - - var key = cacheKey.ToString("N"); - - // See if the result is already cached in the browser - var result = GetCachedResult(requestContext, options.ResponseHeaders, cacheKey, key, options.DateLastModified, options.CacheDuration, contentType); - - if (result != null) - { - return result; - } - - var compress = ShouldCompressResponse(requestContext, contentType); - var hasHeaders = await GetStaticResult(requestContext, options, compress).ConfigureAwait(false); - AddResponseHeaders(hasHeaders, options.ResponseHeaders); - - return hasHeaders; - } - - /// - /// Shoulds the compress response. - /// - /// The request context. - /// Type of the content. - /// true if XXXX, false otherwise - private bool ShouldCompressResponse(IRequest requestContext, string contentType) - { - // It will take some work to support compression with byte range requests - if (!string.IsNullOrEmpty(requestContext.GetHeader("Range"))) - { - return false; - } - - // Don't compress media - if (contentType.StartsWith("audio/", StringComparison.OrdinalIgnoreCase) || contentType.StartsWith("video/", StringComparison.OrdinalIgnoreCase)) - { - return false; - } - - // Don't compress images - if (contentType.StartsWith("image/", StringComparison.OrdinalIgnoreCase)) - { - return false; - } - - if (contentType.StartsWith("font/", StringComparison.OrdinalIgnoreCase)) - { - return false; - } - if (contentType.StartsWith("application/", StringComparison.OrdinalIgnoreCase)) - { - if (string.Equals(contentType, "application/x-javascript", StringComparison.OrdinalIgnoreCase)) - { - return true; - } - if (string.Equals(contentType, "application/xml", StringComparison.OrdinalIgnoreCase)) - { - return true; - } - return false; - } - - return true; - } - - /// - /// The us culture - /// - private static readonly CultureInfo UsCulture = new CultureInfo("en-US"); - - private async Task GetStaticResult(IRequest requestContext, StaticResultOptions options, bool compress) - { - var isHeadRequest = options.IsHeadRequest; - var factoryFn = options.ContentFactory; - var contentType = options.ContentType; - var responseHeaders = options.ResponseHeaders; - - var requestedCompressionType = GetCompressionType(requestContext); - - if (!compress || string.IsNullOrEmpty(requestedCompressionType)) - { - var rangeHeader = requestContext.GetHeader("Range"); - - var stream = await factoryFn().ConfigureAwait(false); - - if (!string.IsNullOrEmpty(rangeHeader)) - { - return new RangeRequestWriter(rangeHeader, stream, contentType, isHeadRequest, _logger) - { - OnComplete = options.OnComplete - }; - } - - responseHeaders["Content-Length"] = stream.Length.ToString(UsCulture); - - if (isHeadRequest) - { - stream.Dispose(); - - return GetHttpResult(new byte[] { }, contentType); - } - - return new StreamWriter(stream, contentType, _logger) - { - OnComplete = options.OnComplete, - OnError = options.OnError - }; - } - - string content; - - using (var stream = await factoryFn().ConfigureAwait(false)) - { - using (var reader = new StreamReader(stream)) - { - content = await reader.ReadToEndAsync().ConfigureAwait(false); - } - } - - var contents = Compress(content, requestedCompressionType); - - responseHeaders["Content-Length"] = contents.Length.ToString(UsCulture); - responseHeaders["Content-Encoding"] = requestedCompressionType; - - if (isHeadRequest) - { - return GetHttpResult(new byte[] { }, contentType); - } - - return GetHttpResult(contents, contentType, responseHeaders); - } - - public static byte[] Compress(string text, string compressionType) - { - if (compressionType == "deflate") - return Deflate(text); - - if (compressionType == "gzip") - return GZip(text); - - throw new NotSupportedException(compressionType); - } - - public static byte[] Deflate(string text) - { - return Deflate(Encoding.UTF8.GetBytes(text)); - } - - public static byte[] Deflate(byte[] bytes) - { - // In .NET FX incompat-ville, you can't access compressed bytes without closing DeflateStream - // Which means we must use MemoryStream since you have to use ToArray() on a closed Stream - using (var ms = new MemoryStream()) - using (var zipStream = new DeflateStream(ms, CompressionMode.Compress)) - { - zipStream.Write(bytes, 0, bytes.Length); - zipStream.Close(); - - return ms.ToArray(); - } - } - - public static byte[] GZip(string text) - { - return GZip(Encoding.UTF8.GetBytes(text)); - } - - public static byte[] GZip(byte[] buffer) - { - using (var ms = new MemoryStream()) - using (var zipStream = new GZipStream(ms, CompressionMode.Compress)) - { - zipStream.Write(buffer, 0, buffer.Length); - zipStream.Close(); - - return ms.ToArray(); - } - } - - /// - /// Adds the caching responseHeaders. - /// - /// The responseHeaders. - /// The cache key. - /// The last date modified. - /// Duration of the cache. - private void AddCachingHeaders(IDictionary responseHeaders, string cacheKey, DateTime? lastDateModified, TimeSpan? cacheDuration) - { - // Don't specify both last modified and Etag, unless caching unconditionally. They are redundant - // https://developers.google.com/speed/docs/best-practices/caching#LeverageBrowserCaching - if (lastDateModified.HasValue && (string.IsNullOrEmpty(cacheKey) || cacheDuration.HasValue)) - { - AddAgeHeader(responseHeaders, lastDateModified); - responseHeaders["Last-Modified"] = lastDateModified.Value.ToString("r"); - } - - if (cacheDuration.HasValue) - { - responseHeaders["Cache-Control"] = "public, max-age=" + Convert.ToInt32(cacheDuration.Value.TotalSeconds); - } - else if (!string.IsNullOrEmpty(cacheKey)) - { - responseHeaders["Cache-Control"] = "public"; - } - else - { - responseHeaders["Cache-Control"] = "no-cache, no-store, must-revalidate"; - responseHeaders["pragma"] = "no-cache, no-store, must-revalidate"; - } - - AddExpiresHeader(responseHeaders, cacheKey, cacheDuration); - } - - /// - /// Adds the expires header. - /// - /// The responseHeaders. - /// The cache key. - /// Duration of the cache. - private void AddExpiresHeader(IDictionary responseHeaders, string cacheKey, TimeSpan? cacheDuration) - { - if (cacheDuration.HasValue) - { - responseHeaders["Expires"] = DateTime.UtcNow.Add(cacheDuration.Value).ToString("r"); - } - else if (string.IsNullOrEmpty(cacheKey)) - { - responseHeaders["Expires"] = "-1"; - } - } - - /// - /// Adds the age header. - /// - /// The responseHeaders. - /// The last date modified. - private void AddAgeHeader(IDictionary responseHeaders, DateTime? lastDateModified) - { - if (lastDateModified.HasValue) - { - responseHeaders["Age"] = Convert.ToInt64((DateTime.UtcNow - lastDateModified.Value).TotalSeconds).ToString(CultureInfo.InvariantCulture); - } - } - /// - /// Determines whether [is not modified] [the specified cache key]. - /// - /// The request context. - /// The cache key. - /// The last date modified. - /// Duration of the cache. - /// true if [is not modified] [the specified cache key]; otherwise, false. - private bool IsNotModified(IRequest requestContext, Guid? cacheKey, DateTime? lastDateModified, TimeSpan? cacheDuration) - { - var isNotModified = true; - - var ifModifiedSinceHeader = requestContext.GetHeader("If-Modified-Since"); - - if (!string.IsNullOrEmpty(ifModifiedSinceHeader)) - { - DateTime ifModifiedSince; - - if (DateTime.TryParse(ifModifiedSinceHeader, out ifModifiedSince)) - { - isNotModified = IsNotModified(ifModifiedSince.ToUniversalTime(), cacheDuration, lastDateModified); - } - } - - var ifNoneMatchHeader = requestContext.GetHeader("If-None-Match"); - - // Validate If-None-Match - if (isNotModified && (cacheKey.HasValue || !string.IsNullOrEmpty(ifNoneMatchHeader))) - { - Guid ifNoneMatch; - - if (Guid.TryParse(ifNoneMatchHeader ?? string.Empty, out ifNoneMatch)) - { - if (cacheKey.HasValue && cacheKey.Value == ifNoneMatch) - { - return true; - } - } - } - - return false; - } - - /// - /// Determines whether [is not modified] [the specified if modified since]. - /// - /// If modified since. - /// Duration of the cache. - /// The date modified. - /// true if [is not modified] [the specified if modified since]; otherwise, false. - private bool IsNotModified(DateTime ifModifiedSince, TimeSpan? cacheDuration, DateTime? dateModified) - { - if (dateModified.HasValue) - { - var lastModified = NormalizeDateForComparison(dateModified.Value); - ifModifiedSince = NormalizeDateForComparison(ifModifiedSince); - - return lastModified <= ifModifiedSince; - } - - if (cacheDuration.HasValue) - { - var cacheExpirationDate = ifModifiedSince.Add(cacheDuration.Value); - - if (DateTime.UtcNow < cacheExpirationDate) - { - return true; - } - } - - return false; - } - - - /// - /// When the browser sends the IfModifiedDate, it's precision is limited to seconds, so this will account for that - /// - /// The date. - /// DateTime. - private DateTime NormalizeDateForComparison(DateTime date) - { - return new DateTime(date.Year, date.Month, date.Day, date.Hour, date.Minute, date.Second, date.Kind); - } - - /// - /// Adds the response headers. - /// - /// The has options. - /// The response headers. - private void AddResponseHeaders(IHasHeaders hasHeaders, IEnumerable> responseHeaders) - { - foreach (var item in responseHeaders) - { - hasHeaders.Headers[item.Key] = item.Value; - } - } - } -} \ No newline at end of file diff --git a/MediaBrowser.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpRequest.cs b/MediaBrowser.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpRequest.cs index 95b2ccaba..628e5cc7e 100644 --- a/MediaBrowser.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpRequest.cs +++ b/MediaBrowser.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpRequest.cs @@ -2,12 +2,12 @@ using System.Collections.Generic; using System.IO; using System.Text; +using Emby.Server.Implementations.HttpServer; using Emby.Server.Implementations.HttpServer.SocketSharp; using MediaBrowser.Model.IO; using MediaBrowser.Model.Logging; using MediaBrowser.Model.Services; using ServiceStack; -using ServiceStack.Host; using SocketHttpListener.Net; using IHttpFile = MediaBrowser.Model.Services.IHttpFile; using IHttpRequest = MediaBrowser.Model.Services.IHttpRequest; @@ -244,14 +244,15 @@ namespace MediaBrowser.Server.Implementations.HttpServer.SocketSharp var specifiedContentType = GetQueryStringContentType(httpReq); if (!string.IsNullOrEmpty(specifiedContentType)) return specifiedContentType; + var serverDefaultContentType = "application/json"; + var acceptContentTypes = httpReq.AcceptTypes; var defaultContentType = httpReq.ContentType; if (HasAnyOfContentTypes(httpReq, FormUrlEncoded, MultiPartFormData)) { - defaultContentType = HostContext.Config.DefaultContentType; + defaultContentType = serverDefaultContentType; } - var customContentTypes = ContentTypes.Instance.ContentTypeFormats.Values; var preferredContentTypes = new string[] {}; var acceptsAnything = false; @@ -261,7 +262,7 @@ namespace MediaBrowser.Server.Implementations.HttpServer.SocketSharp var hasPreferredContentTypes = new bool[preferredContentTypes.Length]; foreach (var acceptsType in acceptContentTypes) { - var contentType = ContentFormat.GetRealContentType(acceptsType); + var contentType = HttpResultFactory.GetRealContentType(acceptsType); acceptsAnything = acceptsAnything || contentType == "*/*"; for (var i = 0; i < preferredContentTypes.Length; i++) @@ -285,17 +286,8 @@ namespace MediaBrowser.Server.Implementations.HttpServer.SocketSharp { if (hasDefaultContentType) return defaultContentType; - if (HostContext.Config.DefaultContentType != null) - return HostContext.Config.DefaultContentType; - } - - foreach (var contentType in acceptContentTypes) - { - foreach (var customContentType in customContentTypes) - { - if (contentType.StartsWith(customContentType, StringComparison.OrdinalIgnoreCase)) - return customContentType; - } + if (serverDefaultContentType != null) + return serverDefaultContentType; } } @@ -305,8 +297,9 @@ namespace MediaBrowser.Server.Implementations.HttpServer.SocketSharp } //We could also send a '406 Not Acceptable', but this is allowed also - return HostContext.Config.DefaultContentType; + return serverDefaultContentType; } + public const string Soap11 = "text/xml; charset=utf-8"; public static bool HasAnyOfContentTypes(IRequest request, params string[] contentTypes) @@ -342,10 +335,7 @@ namespace MediaBrowser.Server.Implementations.HttpServer.SocketSharp if (format.Contains("json")) return "application/json"; if (format.Contains("xml")) return Xml; - string contentType; - ContentTypes.Instance.ContentTypeFormats.TryGetValue(format, out contentType); - - return contentType; + return null; } public bool HasExplicitResponseContentType { get; private set; } @@ -357,7 +347,7 @@ namespace MediaBrowser.Server.Implementations.HttpServer.SocketSharp { if (this.pathInfo == null) { - var mode = HostContext.Config.HandlerFactoryPath; + var mode = HttpListenerHost.HandlerFactoryPath; var pos = request.RawUrl.IndexOf("?"); if (pos != -1) diff --git a/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj b/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj index a2edc7aa8..a253753c5 100644 --- a/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj +++ b/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj @@ -69,6 +69,10 @@ ..\packages\Patterns.Logging.1.0.0.6\lib\portable-net45+win8\Patterns.Logging.dll True + + ..\packages\ServiceStack.Text.4.5.4\lib\net45\ServiceStack.Text.dll + True + False ..\ThirdParty\SharpCompress\SharpCompress.dll @@ -88,9 +92,6 @@ ..\ThirdParty\ServiceStack\ServiceStack.dll - - ..\ThirdParty\ServiceStack.Text\ServiceStack.Text.dll - ..\packages\UniversalDetector.1.0.1\lib\portable-net45+sl4+wp71+win8+wpa81\UniversalDetector.dll @@ -109,7 +110,6 @@ - @@ -141,7 +141,6 @@ - @@ -351,7 +350,9 @@ - + + +