From 3fda8ec5c264f077b625247ab29ce504be25774c Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Thu, 23 Jul 2015 09:23:22 -0400 Subject: [PATCH] update tuner setup --- MediaBrowser.Api/LiveTv/LiveTvService.cs | 56 +++++++++-- .../LiveTv/IListingsProvider.cs | 6 +- .../LiveTv/ILiveTvManager.cs | 25 ++++- MediaBrowser.Controller/LiveTv/ITunerHost.cs | 6 ++ MediaBrowser.Model/LiveTv/LiveTvOptions.cs | 5 +- .../LiveTv/EmbyTV/EmbyTV.cs | 14 ++- .../LiveTv/Listings/SchedulesDirect.cs | 85 +++++++++------- .../LiveTv/LiveTvManager.cs | 98 ++++++++++++++++++- .../LiveTv/TunerHosts/HdHomerun.cs | 8 +- .../LiveTv/TunerHosts/M3UTunerHost.cs | 8 ++ .../Localization/JavaScript/javascript.json | 5 +- .../Localization/Server/server.json | 5 +- .../ApplicationHost.cs | 2 +- .../MediaBrowser.WebDashboard.csproj | 4 +- 14 files changed, 262 insertions(+), 65 deletions(-) diff --git a/MediaBrowser.Api/LiveTv/LiveTvService.cs b/MediaBrowser.Api/LiveTv/LiveTvService.cs index 49ba001cef..f40048ce40 100644 --- a/MediaBrowser.Api/LiveTv/LiveTvService.cs +++ b/MediaBrowser.Api/LiveTv/LiveTvService.cs @@ -1,4 +1,5 @@ -using MediaBrowser.Common.Configuration; +using System.Collections.Generic; +using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.LiveTv; @@ -345,6 +346,31 @@ namespace MediaBrowser.Api.LiveTv public string Id { get; set; } } + [Route("/LiveTv/ListingProviders", "POST", Summary = "Adds a listing provider")] + [Authenticated] + public class AddListingProvider : ListingsProviderInfo, IReturn + { + } + + [Route("/LiveTv/ListingProviders", "DELETE", Summary = "Deletes a listing provider")] + [Authenticated] + public class DeleteListingProvider : IReturnVoid + { + [ApiMember(Name = "Id", Description = "Provider id", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "DELETE")] + public string Id { get; set; } + } + + [Route("/LiveTv/ListingProviders/Lineups", "GET", Summary = "Gets available lineups")] + [Authenticated] + public class GetLineups : IReturn> + { + [ApiMember(Name = "Id", Description = "Provider id", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] + public string Id { get; set; } + + [ApiMember(Name = "Location", Description = "Location", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] + public string Location { get; set; } + } + public class LiveTvService : BaseApiService { private readonly ILiveTvManager _liveTvManager; @@ -373,20 +399,27 @@ namespace MediaBrowser.Api.LiveTv } } - public void Post(AddTunerHost request) + public object Post(AddListingProvider request) + { + var result = _liveTvManager.SaveListingProvider(request).Result; + return ToOptimizedResult(result); + } + + public void Delete(DeleteListingProvider request) { var config = GetConfiguration(); - config.TunerHosts.Add(new TunerHostInfo - { - Id = Guid.NewGuid().ToString("N"), - Url = request.Url, - Type = request.Type - }); + config.ListingProviders = config.ListingProviders.Where(i => !string.Equals(request.Id, i.Id, StringComparison.OrdinalIgnoreCase)).ToList(); _config.SaveConfiguration("livetv", config); } + public void Post(AddTunerHost request) + { + var task = _liveTvManager.SaveTunerHost(request); + Task.WaitAll(task); + } + public void Delete(DeleteTunerHost request) { var config = GetConfiguration(); @@ -401,6 +434,13 @@ namespace MediaBrowser.Api.LiveTv return _config.GetConfiguration("livetv"); } + public async Task Get(GetLineups request) + { + var info = await _liveTvManager.GetLineups(request.Id, request.Location).ConfigureAwait(false); + + return ToOptimizedSerializedResultUsingCache(info); + } + public async Task Get(GetLiveTvInfo request) { var info = await _liveTvManager.GetLiveTvInfo(CancellationToken.None).ConfigureAwait(false); diff --git a/MediaBrowser.Controller/LiveTv/IListingsProvider.cs b/MediaBrowser.Controller/LiveTv/IListingsProvider.cs index beaa4eeafa..e509728bfa 100644 --- a/MediaBrowser.Controller/LiveTv/IListingsProvider.cs +++ b/MediaBrowser.Controller/LiveTv/IListingsProvider.cs @@ -1,4 +1,5 @@ -using MediaBrowser.Model.LiveTv; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.LiveTv; using System; using System.Collections.Generic; using System.Threading; @@ -9,7 +10,10 @@ namespace MediaBrowser.Controller.LiveTv public interface IListingsProvider { string Name { get; } + string Type { get; } Task> GetProgramsAsync(ListingsProviderInfo info, ChannelInfo channel, DateTime startDateUtc, DateTime endDateUtc, CancellationToken cancellationToken); Task AddMetadata(ListingsProviderInfo info, List channels, CancellationToken cancellationToken); + Task Validate(ListingsProviderInfo info); + Task> GetLineups(ListingsProviderInfo info, string location); } } diff --git a/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs b/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs index 3aa1f66efb..3dbf9a9e90 100644 --- a/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs +++ b/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs @@ -56,12 +56,14 @@ namespace MediaBrowser.Controller.LiveTv /// The identifier. /// Task. Task CancelSeriesTimer(string id); - + /// /// Adds the parts. /// /// The services. - void AddParts(IEnumerable services); + /// The tuner hosts. + /// The listing providers. + void AddParts(IEnumerable services, IEnumerable tunerHosts, IEnumerable listingProviders); /// /// Gets the channels. @@ -337,5 +339,24 @@ namespace MediaBrowser.Controller.LiveTv /// The dto. /// The user. void AddInfoToProgramDto(BaseItem item, BaseItemDto dto, User user = null); + /// + /// Saves the tuner host. + /// + /// The information. + /// Task. + Task SaveTunerHost(TunerHostInfo info); + /// + /// Saves the listing provider. + /// + /// The information. + /// Task. + Task SaveListingProvider(ListingsProviderInfo info); + /// + /// Gets the lineups. + /// + /// The provider identifier. + /// The location. + /// Task<List<NameIdPair>>. + Task> GetLineups(string providerId, string location); } } diff --git a/MediaBrowser.Controller/LiveTv/ITunerHost.cs b/MediaBrowser.Controller/LiveTv/ITunerHost.cs index 2fa538c605..38ed43bd97 100644 --- a/MediaBrowser.Controller/LiveTv/ITunerHost.cs +++ b/MediaBrowser.Controller/LiveTv/ITunerHost.cs @@ -46,5 +46,11 @@ namespace MediaBrowser.Controller.LiveTv /// The cancellation token. /// Task<MediaSourceInfo>. Task GetChannelStream(TunerHostInfo info, string channelId, string streamId, CancellationToken cancellationToken); + /// + /// Validates the specified information. + /// + /// The information. + /// Task. + Task Validate(TunerHostInfo info); } } diff --git a/MediaBrowser.Model/LiveTv/LiveTvOptions.cs b/MediaBrowser.Model/LiveTv/LiveTvOptions.cs index c78fbe630a..90cd8924d2 100644 --- a/MediaBrowser.Model/LiveTv/LiveTvOptions.cs +++ b/MediaBrowser.Model/LiveTv/LiveTvOptions.cs @@ -28,10 +28,11 @@ namespace MediaBrowser.Model.LiveTv public class ListingsProviderInfo { - public string ProviderName { get; set; } + public string Id { get; set; } + public string Type { get; set; } public string Username { get; set; } public string Password { get; set; } - public string ZipCode { get; set; } public string ListingsId { get; set; } + public string ZipCode { get; set; } } } \ No newline at end of file diff --git a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs index 26e0709215..6e8213ee09 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs @@ -30,18 +30,16 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV private readonly ItemDataProvider _seriesTimerProvider; private readonly TimerManager _timerProvider; - private readonly List _tunerHosts = new List(); - private readonly List _listingProviders = new List(); + private readonly LiveTvManager _liveTvManager; - public EmbyTV(IApplicationHost appHost, ILogger logger, IJsonSerializer jsonSerializer, IHttpClient httpClient, IConfigurationManager config) + public EmbyTV(IApplicationHost appHost, ILogger logger, IJsonSerializer jsonSerializer, IHttpClient httpClient, IConfigurationManager config, ILiveTvManager liveTvManager) { _appHpst = appHost; _logger = logger; _httpClient = httpClient; _config = config; + _liveTvManager = (LiveTvManager)liveTvManager; _jsonSerializer = jsonSerializer; - _tunerHosts.AddRange(appHost.GetExports()); - _listingProviders.AddRange(appHost.GetExports()); _recordingProvider = new ItemDataProvider(jsonSerializer, _logger, Path.Combine(DataPath, "recordings"), (r1, r2) => string.Equals(r1.Id, r2.Id, StringComparison.OrdinalIgnoreCase)); _seriesTimerProvider = new SeriesTimerManager(jsonSerializer, _logger, Path.Combine(DataPath, "seriestimers")); @@ -76,7 +74,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV var status = new LiveTvServiceStatusInfo(); var list = new List(); - foreach (var host in _tunerHosts) + foreach (var host in _liveTvManager.TunerHosts) { foreach (var hostInstance in host.GetTunerHosts()) { @@ -104,7 +102,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV { var list = new List(); - foreach (var host in _tunerHosts) + foreach (var host in _liveTvManager.TunerHosts) { foreach (var hostInstance in host.GetTunerHosts()) { @@ -288,7 +286,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV return GetConfiguration().ListingProviders .Select(i => { - var provider = _listingProviders.FirstOrDefault(l => string.Equals(l.Name, i.ProviderName, StringComparison.OrdinalIgnoreCase)); + var provider = _liveTvManager.ListingProviders.FirstOrDefault(l => string.Equals(l.Type, i.Type, StringComparison.OrdinalIgnoreCase)); return provider == null ? null : new Tuple(provider, i); }) diff --git a/MediaBrowser.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs b/MediaBrowser.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs index 3b7564983e..f5b41f3df7 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs @@ -422,7 +422,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.Listings return images; } - public async Task> GetHeadends(ListingsProviderInfo info, CancellationToken cancellationToken) + public async Task> GetHeadends(ListingsProviderInfo info, string location, CancellationToken cancellationToken) { var token = await GetToken(info, cancellationToken); @@ -437,7 +437,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.Listings var options = new HttpRequestOptions() { - Url = ApiUrl + "/headends?country=USA&postalcode=" + info.ZipCode, + Url = ApiUrl + "/headends?country=USA&postalcode=" + location, UserAgent = UserAgent, CancellationToken = cancellationToken }; @@ -484,43 +484,43 @@ namespace MediaBrowser.Server.Implementations.LiveTv.Listings private async Task GetToken(ListingsProviderInfo info, CancellationToken cancellationToken) { + var username = info.Username; + + // Reset the token if there's no username + if (string.IsNullOrWhiteSpace(username)) + { + return null; + } + + var password = info.Password; + if (string.IsNullOrWhiteSpace(password)) + { + return null; + } + + NameValuePair savedToken = null; + if (!_tokens.TryGetValue(username, out savedToken)) + { + savedToken = new NameValuePair(); + _tokens.TryAdd(username, savedToken); + } + + if (!string.IsNullOrWhiteSpace(savedToken.Name) && !string.IsNullOrWhiteSpace(savedToken.Value)) + { + long ticks; + if (long.TryParse(savedToken.Value, NumberStyles.Any, CultureInfo.InvariantCulture, out ticks)) + { + // If it's under 24 hours old we can still use it + if ((DateTime.UtcNow.Ticks - ticks) < TimeSpan.FromHours(24).Ticks) + { + return savedToken.Name; + } + } + } + await _tokenSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false); try { - var username = info.Username; - - // Reset the token if there's no username - if (string.IsNullOrWhiteSpace(username)) - { - return null; - } - - var password = info.Password; - if (string.IsNullOrWhiteSpace(password)) - { - return null; - } - - NameValuePair savedToken = null; - if (!_tokens.TryGetValue(username, out savedToken)) - { - savedToken = new NameValuePair(); - _tokens.TryAdd(username, savedToken); - } - - if (!string.IsNullOrWhiteSpace(savedToken.Name) && !string.IsNullOrWhiteSpace(savedToken.Value)) - { - long ticks; - if (long.TryParse(savedToken.Value, NumberStyles.Any, CultureInfo.InvariantCulture, out ticks)) - { - // If it's under 24 hours old we can still use it - if ((DateTime.UtcNow.Ticks - ticks) < TimeSpan.FromHours(24).Ticks) - { - return savedToken.Name; - } - } - } - var result = await GetTokenInternal(username, password, cancellationToken).ConfigureAwait(false); savedToken.Name = result; savedToken.Value = DateTime.UtcNow.Ticks.ToString(CultureInfo.InvariantCulture); @@ -563,6 +563,11 @@ namespace MediaBrowser.Server.Implementations.LiveTv.Listings get { return "Schedules Direct"; } } + public string Type + { + get { return "SchedulesDirect"; } + } + public class ScheduleDirect { public class Token @@ -842,5 +847,13 @@ namespace MediaBrowser.Server.Implementations.LiveTv.Listings } + public async Task Validate(ListingsProviderInfo info) + { + } + + public Task> GetLineups(ListingsProviderInfo info, string location) + { + return GetHeadends(info, location, CancellationToken.None); + } } } diff --git a/MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs b/MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs index 05bc276b86..a64ce31cf8 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs @@ -57,6 +57,9 @@ namespace MediaBrowser.Server.Implementations.LiveTv private readonly ConcurrentDictionary _refreshedPrograms = new ConcurrentDictionary(); + private readonly List _tunerHosts = new List(); + private readonly List _listingProviders = new List(); + public LiveTvManager(IApplicationHost appHost, IServerConfigurationManager config, ILogger logger, IItemRepository itemRepo, IImageProcessor imageProcessor, IUserDataManager userDataManager, IDtoService dtoService, IUserManager userManager, ILibraryManager libraryManager, ITaskManager taskManager, ILocalizationManager localization, IJsonSerializer jsonSerializer, IProviderManager providerManager) { _config = config; @@ -92,9 +95,13 @@ namespace MediaBrowser.Server.Implementations.LiveTv /// Adds the parts. /// /// The services. - public void AddParts(IEnumerable services) + /// The tuner hosts. + /// The listing providers. + public void AddParts(IEnumerable services, IEnumerable tunerHosts, IEnumerable listingProviders) { _services.AddRange(services); + _tunerHosts.AddRange(tunerHosts); + _listingProviders.AddRange(listingProviders); foreach (var service in _services) { @@ -102,6 +109,16 @@ namespace MediaBrowser.Server.Implementations.LiveTv } } + public List TunerHosts + { + get { return _tunerHosts; } + } + + public List ListingProviders + { + get { return _listingProviders; } + } + void service_DataSourceChanged(object sender, EventArgs e) { _taskManager.CancelIfRunningAndQueue(); @@ -2154,5 +2171,84 @@ namespace MediaBrowser.Server.Implementations.LiveTv var user = _userManager.GetUserById(userId); return await _libraryManager.GetNamedView(user, name, "livetv", "zz_" + name, cancellationToken).ConfigureAwait(false); } + + public async Task SaveTunerHost(TunerHostInfo info) + { + info = (TunerHostInfo)_jsonSerializer.DeserializeFromString(_jsonSerializer.SerializeToString(info), typeof(TunerHostInfo)); + + var provider = _tunerHosts.FirstOrDefault(i => string.Equals(info.Type, i.Type, StringComparison.OrdinalIgnoreCase)); + + if (provider == null) + { + throw new ResourceNotFoundException(); + } + + await provider.Validate(info).ConfigureAwait(false); + + var config = GetConfiguration(); + + var index = config.TunerHosts.FindIndex(i => string.Equals(i.Id, info.Id, StringComparison.OrdinalIgnoreCase)); + + if (index == -1 || string.IsNullOrWhiteSpace(info.Id)) + { + info.Id = Guid.NewGuid().ToString("N"); + config.TunerHosts.Add(info); + } + else + { + config.TunerHosts[index] = info; + } + + _config.SaveConfiguration("livetv", config); + } + + public async Task SaveListingProvider(ListingsProviderInfo info) + { + info = (ListingsProviderInfo)_jsonSerializer.DeserializeFromString(_jsonSerializer.SerializeToString(info), typeof(ListingsProviderInfo)); + + var provider = _listingProviders.FirstOrDefault(i => string.Equals(info.Type, i.Type, StringComparison.OrdinalIgnoreCase)); + + if (provider == null) + { + throw new ResourceNotFoundException(); + } + + await provider.Validate(info).ConfigureAwait(false); + + var config = GetConfiguration(); + + var index = config.ListingProviders.FindIndex(i => string.Equals(i.Id, info.Id, StringComparison.OrdinalIgnoreCase)); + + if (index == -1 || string.IsNullOrWhiteSpace(info.Id)) + { + info.Id = Guid.NewGuid().ToString("N"); + config.ListingProviders.Add(info); + } + else + { + config.ListingProviders[index] = info; + } + + _config.SaveConfiguration("livetv", config); + + return info; + } + + + public Task> GetLineups(string providerId, string location) + { + var config = GetConfiguration(); + + var info = config.ListingProviders.FirstOrDefault(i => string.Equals(i.Id, providerId, StringComparison.OrdinalIgnoreCase)); + + var provider = _listingProviders.FirstOrDefault(i => string.Equals(info.Type, i.Type, StringComparison.OrdinalIgnoreCase)); + + if (provider == null) + { + throw new ResourceNotFoundException(); + } + + return provider.GetLineups(info, location); + } } } diff --git a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/HdHomerun.cs b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/HdHomerun.cs index b5b588682a..9aacd0c1ea 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/HdHomerun.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/HdHomerun.cs @@ -3,7 +3,6 @@ using MediaBrowser.Common.Net; using MediaBrowser.Controller.LiveTv; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Extensions; using MediaBrowser.Model.LiveTv; using MediaBrowser.Model.Logging; using MediaBrowser.Model.MediaInfo; @@ -15,7 +14,6 @@ using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; -using MediaBrowser.Server.Implementations.LiveTv.EmbyTV; namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts { @@ -222,5 +220,11 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts } throw new ApplicationException("Channel not found."); } + + + public async Task Validate(TunerHostInfo info) + { + await GetChannels(info, CancellationToken.None).ConfigureAwait(false); + } } } diff --git a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs index 615e07a822..e91bd0f0bb 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs @@ -185,5 +185,13 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts { } } + + public async Task Validate(TunerHostInfo info) + { + if (!File.Exists(info.Url)) + { + throw new FileNotFoundException(); + } + } } } diff --git a/MediaBrowser.Server.Implementations/Localization/JavaScript/javascript.json b/MediaBrowser.Server.Implementations/Localization/JavaScript/javascript.json index d2b998ae8e..cea0363f49 100644 --- a/MediaBrowser.Server.Implementations/Localization/JavaScript/javascript.json +++ b/MediaBrowser.Server.Implementations/Localization/JavaScript/javascript.json @@ -820,5 +820,8 @@ "MessageConfirmDeleteTunerDevice": "Are you sure you wish to delete this device?", "MessageConfirmDeleteGuideProvider": "Are you sure you wish to delete this guide provider?", "HeaderDeleteProvider": "Delete Provider", - "HeaderAddProvider": "Add Provider" + "HeaderAddProvider": "Add Provider", + "ErrorAddingTunerDevice": "There was an error adding the tuner device. Please ensure it is accessible and try again.", + "ErrorSavingTvProvider": "There was an error saving the TV provider. Please ensure it is accessible and try again.", + "ErrorGettingTvLineups": "There was an error downloading tv lineups. Please ensure your username and password are correct and try again." } diff --git a/MediaBrowser.Server.Implementations/Localization/Server/server.json b/MediaBrowser.Server.Implementations/Localization/Server/server.json index 83c2ccf5d6..fe099dc672 100644 --- a/MediaBrowser.Server.Implementations/Localization/Server/server.json +++ b/MediaBrowser.Server.Implementations/Localization/Server/server.json @@ -1483,5 +1483,8 @@ "TabTuners": "Tuners", "HeaderGuideProviders": "Guide Providers", "AddGuideProviderHelp": "Add a source for TV Guide information", - "LabelZipCode": "Zip Code" + "LabelZipCode": "Zip Code:", + "GuideProviderListingsStep": "Step 2: Select Listings", + "GuideProviderLoginStep": "Step 1: Login", + "LabelLineup": "Lineup" } diff --git a/MediaBrowser.Server.Startup.Common/ApplicationHost.cs b/MediaBrowser.Server.Startup.Common/ApplicationHost.cs index fab6682d78..adef0a45fa 100644 --- a/MediaBrowser.Server.Startup.Common/ApplicationHost.cs +++ b/MediaBrowser.Server.Startup.Common/ApplicationHost.cs @@ -784,7 +784,7 @@ namespace MediaBrowser.Server.Startup.Common ImageProcessor.AddParts(GetExports()); - LiveTvManager.AddParts(GetExports()); + LiveTvManager.AddParts(GetExports(), GetExports(), GetExports()); SubtitleManager.AddParts(GetExports()); ChapterManager.AddParts(GetExports()); diff --git a/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj b/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj index 1920a9ed79..bc2b8eb7a1 100644 --- a/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj +++ b/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj @@ -183,7 +183,7 @@ PreserveNewest - + PreserveNewest @@ -207,7 +207,7 @@ PreserveNewest - + PreserveNewest