From 22377f2516b78d9eb6a2e5bf4e99958590bd5788 Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Fri, 4 Mar 2016 12:07:14 -0500 Subject: [PATCH 1/7] auto-organize fixes --- .../TV/TvdbSeasonIdentityProvider.cs | 5 +++ .../TV/TvdbSeriesProvider.cs | 34 +++++++++---------- .../FileOrganization/TvFolderOrganizer.cs | 8 ++--- 3 files changed, 26 insertions(+), 21 deletions(-) diff --git a/MediaBrowser.Providers/TV/TvdbSeasonIdentityProvider.cs b/MediaBrowser.Providers/TV/TvdbSeasonIdentityProvider.cs index edeea36e44..4198430c9f 100644 --- a/MediaBrowser.Providers/TV/TvdbSeasonIdentityProvider.cs +++ b/MediaBrowser.Providers/TV/TvdbSeasonIdentityProvider.cs @@ -27,6 +27,11 @@ namespace MediaBrowser.Providers.TV public static TvdbSeasonIdentity? ParseIdentity(string id) { + if (id == null) + { + return null; + } + try { var parts = id.Split(':'); diff --git a/MediaBrowser.Providers/TV/TvdbSeriesProvider.cs b/MediaBrowser.Providers/TV/TvdbSeriesProvider.cs index 4a7ae57fb0..593507fb2a 100644 --- a/MediaBrowser.Providers/TV/TvdbSeriesProvider.cs +++ b/MediaBrowser.Providers/TV/TvdbSeriesProvider.cs @@ -160,19 +160,19 @@ namespace MediaBrowser.Providers.TV var series = result.Item; string id; - if (seriesProviderIds.TryGetValue(MetadataProviders.Tvdb.ToString(), out id)) + if (seriesProviderIds.TryGetValue(MetadataProviders.Tvdb.ToString(), out id) && !string.IsNullOrEmpty(id)) { series.SetProviderId(MetadataProviders.Tvdb, id); } - if (seriesProviderIds.TryGetValue(MetadataProviders.Imdb.ToString(), out id)) + if (seriesProviderIds.TryGetValue(MetadataProviders.Imdb.ToString(), out id) && !string.IsNullOrEmpty(id)) { series.SetProviderId(MetadataProviders.Imdb, id); } var seriesDataPath = GetSeriesDataPath(_config.ApplicationPaths, seriesProviderIds); - var seriesXmlPath = GetSeriesXmlPath (seriesProviderIds, metadataLanguage); + var seriesXmlPath = GetSeriesXmlPath(seriesProviderIds, metadataLanguage); var actorsXmlPath = Path.Combine(seriesDataPath, "actors.xml"); FetchSeriesInfo(series, seriesXmlPath, cancellationToken); @@ -320,7 +320,7 @@ namespace MediaBrowser.Providers.TV internal static bool IsValidSeries(Dictionary seriesProviderIds) { string id; - if (seriesProviderIds.TryGetValue(MetadataProviders.Tvdb.ToString(), out id)) + if (seriesProviderIds.TryGetValue(MetadataProviders.Tvdb.ToString(), out id) && !string.IsNullOrEmpty(id)) { // This check should ideally never be necessary but we're seeing some cases of this and haven't tracked them down yet. if (!string.IsNullOrWhiteSpace(id)) @@ -329,7 +329,7 @@ namespace MediaBrowser.Providers.TV } } - if (seriesProviderIds.TryGetValue(MetadataProviders.Imdb.ToString(), out id)) + if (seriesProviderIds.TryGetValue(MetadataProviders.Imdb.ToString(), out id) && !string.IsNullOrEmpty(id)) { // This check should ideally never be necessary but we're seeing some cases of this and haven't tracked them down yet. if (!string.IsNullOrWhiteSpace(id)) @@ -340,7 +340,7 @@ namespace MediaBrowser.Providers.TV return false; } - private SemaphoreSlim _ensureSemaphore = new SemaphoreSlim(1,1); + private SemaphoreSlim _ensureSemaphore = new SemaphoreSlim(1, 1); internal async Task EnsureSeriesInfo(Dictionary seriesProviderIds, string preferredMetadataLanguage, CancellationToken cancellationToken) { await _ensureSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false); @@ -348,7 +348,7 @@ namespace MediaBrowser.Providers.TV try { string seriesId; - if (seriesProviderIds.TryGetValue(MetadataProviders.Tvdb.ToString(), out seriesId)) + if (seriesProviderIds.TryGetValue(MetadataProviders.Tvdb.ToString(), out seriesId) && !string.IsNullOrEmpty(seriesId)) { var seriesDataPath = GetSeriesDataPath(_config.ApplicationPaths, seriesProviderIds); @@ -362,7 +362,7 @@ namespace MediaBrowser.Providers.TV return seriesDataPath; } - if (seriesProviderIds.TryGetValue(MetadataProviders.Imdb.ToString(), out seriesId)) + if (seriesProviderIds.TryGetValue(MetadataProviders.Imdb.ToString(), out seriesId) && !string.IsNullOrEmpty(seriesId)) { var seriesDataPath = GetSeriesDataPath(_config.ApplicationPaths, seriesProviderIds); @@ -1300,14 +1300,14 @@ namespace MediaBrowser.Providers.TV internal static string GetSeriesDataPath(IApplicationPaths appPaths, Dictionary seriesProviderIds) { string seriesId; - if (seriesProviderIds.TryGetValue(MetadataProviders.Tvdb.ToString(), out seriesId)) + if (seriesProviderIds.TryGetValue(MetadataProviders.Tvdb.ToString(), out seriesId) && !string.IsNullOrEmpty(seriesId)) { var seriesDataPath = Path.Combine(GetSeriesDataPath(appPaths), seriesId); return seriesDataPath; } - if (seriesProviderIds.TryGetValue(MetadataProviders.Imdb.ToString(), out seriesId)) + if (seriesProviderIds.TryGetValue(MetadataProviders.Imdb.ToString(), out seriesId) && !string.IsNullOrEmpty(seriesId)) { var seriesDataPath = Path.Combine(GetSeriesDataPath(appPaths), seriesId); @@ -1317,14 +1317,14 @@ namespace MediaBrowser.Providers.TV return null; } - public string GetSeriesXmlPath(Dictionary seriesProviderIds, string language) - { - var seriesDataPath = GetSeriesDataPath(_config.ApplicationPaths, seriesProviderIds); + public string GetSeriesXmlPath(Dictionary seriesProviderIds, string language) + { + var seriesDataPath = GetSeriesDataPath(_config.ApplicationPaths, seriesProviderIds); - var seriesXmlFilename = language.ToLower() + ".xml"; + var seriesXmlFilename = language.ToLower() + ".xml"; - return Path.Combine (seriesDataPath, seriesXmlFilename); - } + return Path.Combine(seriesDataPath, seriesXmlFilename); + } /// /// Gets the series data path. @@ -1466,4 +1466,4 @@ namespace MediaBrowser.Providers.TV }; } } -} +} \ No newline at end of file diff --git a/MediaBrowser.Server.Implementations/FileOrganization/TvFolderOrganizer.cs b/MediaBrowser.Server.Implementations/FileOrganization/TvFolderOrganizer.cs index 845fcdff27..43bd2f29cf 100644 --- a/MediaBrowser.Server.Implementations/FileOrganization/TvFolderOrganizer.cs +++ b/MediaBrowser.Server.Implementations/FileOrganization/TvFolderOrganizer.cs @@ -84,7 +84,7 @@ namespace MediaBrowser.Server.Implementations.FileOrganization } catch (Exception ex) { - _logger.ErrorException("Error organizing episode {0}", ex, file); + _logger.ErrorException("Error organizing episode {0}", ex, file.FullName); } numComplete++; @@ -132,7 +132,7 @@ namespace MediaBrowser.Server.Implementations.FileOrganization { try { - return _fileSystem.GetFiles(path, true) + return _fileSystem.GetFiles(path, true) .ToList(); } catch (IOException ex) @@ -150,7 +150,7 @@ namespace MediaBrowser.Server.Implementations.FileOrganization /// The extensions. private void DeleteLeftOverFiles(string path, IEnumerable extensions) { - var eligibleFiles = _fileSystem.GetFiles(path, true) + var eligibleFiles = _fileSystem.GetFiles(path, true) .Where(i => extensions.Contains(i.Extension, StringComparer.OrdinalIgnoreCase)) .ToList(); @@ -206,4 +206,4 @@ namespace MediaBrowser.Server.Implementations.FileOrganization return watchLocations.Contains(path, StringComparer.OrdinalIgnoreCase); } } -} +} \ No newline at end of file From 21ebd05a0ef72c0b4c8f5d0c3d84077e0579b5ba Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Fri, 4 Mar 2016 12:38:51 -0500 Subject: [PATCH 2/7] update MovieDb providers to search for images using all languages --- .../BoxSets/MovieDbBoxSetImageProvider.cs | 2 +- .../BoxSets/MovieDbBoxSetProvider.cs | 23 +++++++------- .../Movies/MovieDbImageProvider.cs | 7 ++--- .../Movies/MovieDbProvider.cs | 31 ++++++++++--------- .../TV/MovieDbSeriesImageProvider.cs | 6 ++-- .../TV/MovieDbSeriesProvider.cs | 26 +++++++--------- 6 files changed, 45 insertions(+), 50 deletions(-) diff --git a/MediaBrowser.Providers/BoxSets/MovieDbBoxSetImageProvider.cs b/MediaBrowser.Providers/BoxSets/MovieDbBoxSetImageProvider.cs index e82c6a1b91..ff3d5a5b24 100644 --- a/MediaBrowser.Providers/BoxSets/MovieDbBoxSetImageProvider.cs +++ b/MediaBrowser.Providers/BoxSets/MovieDbBoxSetImageProvider.cs @@ -55,7 +55,7 @@ namespace MediaBrowser.Providers.BoxSets { var language = item.GetPreferredMetadataLanguage(); - var mainResult = await MovieDbBoxSetProvider.Current.GetMovieDbResult(tmdbId, language, cancellationToken).ConfigureAwait(false); + var mainResult = await MovieDbBoxSetProvider.Current.GetMovieDbResult(tmdbId, null, cancellationToken).ConfigureAwait(false); if (mainResult != null) { diff --git a/MediaBrowser.Providers/BoxSets/MovieDbBoxSetProvider.cs b/MediaBrowser.Providers/BoxSets/MovieDbBoxSetProvider.cs index ae6199a75d..37aaf038ff 100644 --- a/MediaBrowser.Providers/BoxSets/MovieDbBoxSetProvider.cs +++ b/MediaBrowser.Providers/BoxSets/MovieDbBoxSetProvider.cs @@ -169,11 +169,10 @@ namespace MediaBrowser.Providers.BoxSets if (!string.IsNullOrEmpty(language)) { url += string.Format("&language={0}", language); - } - var includeImageLanguageParam = MovieDbProvider.GetImageLanguagesParam(language); - // Get images in english and with no language - url += "&include_image_language=" + includeImageLanguageParam; + // Get images in english and with no language + url += "&include_image_language=" + MovieDbProvider.GetImageLanguagesParam(language); + } cancellationToken.ThrowIfCancellationRequested(); @@ -196,7 +195,13 @@ namespace MediaBrowser.Providers.BoxSets { if (!string.IsNullOrEmpty(language) && !string.Equals(language, "en", StringComparison.OrdinalIgnoreCase)) { - url = string.Format(GetCollectionInfo3, id, MovieDbSearch.ApiKey) + "&include_image_language=en,null&language=en"; + url = string.Format(GetCollectionInfo3, id, MovieDbSearch.ApiKey) + "&language=en"; + + if (!string.IsNullOrEmpty(language)) + { + // Get images in english and with no language + url += "&include_image_language=" + MovieDbProvider.GetImageLanguagesParam(language); + } using (var json = await MovieDbProvider.Current.GetMovieDbResponse(new HttpRequestOptions { @@ -239,15 +244,9 @@ namespace MediaBrowser.Providers.BoxSets private static string GetDataFilePath(IApplicationPaths appPaths, string tmdbId, string preferredLanguage) { - if (string.IsNullOrWhiteSpace(preferredLanguage)) - { - throw new ArgumentNullException("preferredLanguage"); - } - var path = GetDataPath(appPaths, tmdbId); - var filename = string.Format("all-{0}.json", - preferredLanguage); + var filename = string.Format("all-{0}.json", preferredLanguage ?? string.Empty); return Path.Combine(path, filename); } diff --git a/MediaBrowser.Providers/Movies/MovieDbImageProvider.cs b/MediaBrowser.Providers/Movies/MovieDbImageProvider.cs index b6f93392b3..e86ae4b047 100644 --- a/MediaBrowser.Providers/Movies/MovieDbImageProvider.cs +++ b/MediaBrowser.Providers/Movies/MovieDbImageProvider.cs @@ -82,7 +82,7 @@ namespace MediaBrowser.Providers.Movies { var list = new List(); - var results = await FetchImages((BaseItem)item, _jsonSerializer, cancellationToken).ConfigureAwait(false); + var results = await FetchImages((BaseItem)item, null, _jsonSerializer, cancellationToken).ConfigureAwait(false); if (results == null) { @@ -183,14 +183,13 @@ namespace MediaBrowser.Providers.Movies /// Fetches the images. /// /// The item. + /// The language. /// The json serializer. /// The cancellation token. /// Task{MovieImages}. - private async Task FetchImages(BaseItem item, IJsonSerializer jsonSerializer, - CancellationToken cancellationToken) + private async Task FetchImages(BaseItem item, string language, IJsonSerializer jsonSerializer, CancellationToken cancellationToken) { var tmdbId = item.GetProviderId(MetadataProviders.Tmdb); - var language = item.GetPreferredMetadataLanguage(); if (string.IsNullOrWhiteSpace(tmdbId)) { diff --git a/MediaBrowser.Providers/Movies/MovieDbProvider.cs b/MediaBrowser.Providers/Movies/MovieDbProvider.cs index 593c6f180e..9ab9e44fca 100644 --- a/MediaBrowser.Providers/Movies/MovieDbProvider.cs +++ b/MediaBrowser.Providers/Movies/MovieDbProvider.cs @@ -226,10 +226,6 @@ namespace MediaBrowser.Providers.Movies { throw new ArgumentNullException("tmdbId"); } - if (string.IsNullOrEmpty(language)) - { - throw new ArgumentNullException("language"); - } var path = GetDataFilePath(tmdbId, language); @@ -253,15 +249,15 @@ namespace MediaBrowser.Providers.Movies { throw new ArgumentNullException("tmdbId"); } - if (string.IsNullOrEmpty(preferredLanguage)) - { - throw new ArgumentNullException("preferredLanguage"); - } var path = GetMovieDataPath(_configurationManager.ApplicationPaths, tmdbId); - var filename = string.Format("all-{0}.json", - preferredLanguage); + if (string.IsNullOrWhiteSpace(preferredLanguage)) + { + preferredLanguage = "alllang"; + } + + var filename = string.Format("all-{0}.json", preferredLanguage); return Path.Combine(path, filename); } @@ -298,11 +294,10 @@ namespace MediaBrowser.Providers.Movies if (!string.IsNullOrEmpty(language)) { url += string.Format("&language={0}", language); - } - var includeImageLanguageParam = GetImageLanguagesParam(language); - // Get images in english and with no language - url += "&include_image_language=" + includeImageLanguageParam; + // Get images in english and with no language + url += "&include_image_language=" + GetImageLanguagesParam(language); + } CompleteMovieData mainResult; @@ -348,7 +343,13 @@ namespace MediaBrowser.Providers.Movies { _logger.Info("MovieDbProvider couldn't find meta for language " + language + ". Trying English..."); - url = string.Format(GetMovieInfo3, id, ApiKey) + "&include_image_language=" + includeImageLanguageParam + "&language=en"; + url = string.Format(GetMovieInfo3, id, ApiKey) + "&language=en"; + + if (!string.IsNullOrEmpty(language)) + { + // Get images in english and with no language + url += "&include_image_language=" + GetImageLanguagesParam(language); + } using (var json = await GetMovieDbResponse(new HttpRequestOptions { diff --git a/MediaBrowser.Providers/TV/MovieDbSeriesImageProvider.cs b/MediaBrowser.Providers/TV/MovieDbSeriesImageProvider.cs index 3516fcbd11..f7c19988c3 100644 --- a/MediaBrowser.Providers/TV/MovieDbSeriesImageProvider.cs +++ b/MediaBrowser.Providers/TV/MovieDbSeriesImageProvider.cs @@ -55,7 +55,7 @@ namespace MediaBrowser.Providers.TV { var list = new List(); - var results = await FetchImages((BaseItem)item, _jsonSerializer, cancellationToken).ConfigureAwait(false); + var results = await FetchImages((BaseItem)item, null, _jsonSerializer, cancellationToken).ConfigureAwait(false); if (results == null) { @@ -146,14 +146,14 @@ namespace MediaBrowser.Providers.TV /// Fetches the images. /// /// The item. + /// The language. /// The json serializer. /// The cancellation token. /// Task{MovieImages}. - private async Task FetchImages(BaseItem item, IJsonSerializer jsonSerializer, + private async Task FetchImages(BaseItem item, string language, IJsonSerializer jsonSerializer, CancellationToken cancellationToken) { var tmdbId = item.GetProviderId(MetadataProviders.Tmdb); - var language = item.GetPreferredMetadataLanguage(); if (string.IsNullOrEmpty(tmdbId)) { diff --git a/MediaBrowser.Providers/TV/MovieDbSeriesProvider.cs b/MediaBrowser.Providers/TV/MovieDbSeriesProvider.cs index 994343ee24..5b4153760d 100644 --- a/MediaBrowser.Providers/TV/MovieDbSeriesProvider.cs +++ b/MediaBrowser.Providers/TV/MovieDbSeriesProvider.cs @@ -302,11 +302,10 @@ namespace MediaBrowser.Providers.TV if (!string.IsNullOrEmpty(language)) { url += string.Format("&language={0}", language); - } - var includeImageLanguageParam = MovieDbProvider.GetImageLanguagesParam(language); - // Get images in english and with no language - url += "&include_image_language=" + includeImageLanguageParam; + // Get images in english and with no language + url += "&include_image_language=" + MovieDbProvider.GetImageLanguagesParam(language); + } cancellationToken.ThrowIfCancellationRequested(); @@ -333,7 +332,13 @@ namespace MediaBrowser.Providers.TV { _logger.Info("MovieDbSeriesProvider couldn't find meta for language " + language + ". Trying English..."); - url = string.Format(GetTvInfo3, id, MovieDbProvider.ApiKey) + "&include_image_language=" + includeImageLanguageParam + "&language=en"; + url = string.Format(GetTvInfo3, id, MovieDbProvider.ApiKey) + "&language=en"; + + if (!string.IsNullOrEmpty(language)) + { + // Get images in english and with no language + url += "&include_image_language=" + MovieDbProvider.GetImageLanguagesParam(language); + } using (var json = await MovieDbProvider.Current.GetMovieDbResponse(new HttpRequestOptions { @@ -359,10 +364,6 @@ namespace MediaBrowser.Providers.TV { throw new ArgumentNullException("tmdbId"); } - if (string.IsNullOrEmpty(language)) - { - throw new ArgumentNullException("language"); - } var path = GetDataFilePath(tmdbId, language); @@ -386,15 +387,10 @@ namespace MediaBrowser.Providers.TV { throw new ArgumentNullException("tmdbId"); } - if (string.IsNullOrEmpty(preferredLanguage)) - { - throw new ArgumentNullException("preferredLanguage"); - } var path = GetSeriesDataPath(_configurationManager.ApplicationPaths, tmdbId); - var filename = string.Format("series-{0}.json", - preferredLanguage ?? string.Empty); + var filename = string.Format("series-{0}.json", preferredLanguage ?? string.Empty); return Path.Combine(path, filename); } From ca83644e1cf936d3f95572cad8a35fba0148e1b7 Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Fri, 4 Mar 2016 23:16:55 -0500 Subject: [PATCH 3/7] handle no subs setting --- .../Library/MediaSourceManager.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/MediaBrowser.Server.Implementations/Library/MediaSourceManager.cs b/MediaBrowser.Server.Implementations/Library/MediaSourceManager.cs index e4a085f421..dfc6fc1259 100644 --- a/MediaBrowser.Server.Implementations/Library/MediaSourceManager.cs +++ b/MediaBrowser.Server.Implementations/Library/MediaSourceManager.cs @@ -17,6 +17,7 @@ using System.Threading; using System.Threading.Tasks; using CommonIO; using MediaBrowser.Common.IO; +using MediaBrowser.Model.Configuration; namespace MediaBrowser.Server.Implementations.Library { @@ -276,7 +277,7 @@ namespace MediaBrowser.Server.Implementations.Library private void SetDefaultSubtitleStreamIndex(MediaSourceInfo source, UserItemData userData, User user) { - if (userData.SubtitleStreamIndex.HasValue && user.Configuration.RememberSubtitleSelections) + if (userData.SubtitleStreamIndex.HasValue && user.Configuration.RememberSubtitleSelections && user.Configuration.SubtitleMode != SubtitlePlaybackMode.None) { var index = userData.SubtitleStreamIndex.Value; // Make sure the saved index is still valid From 63efe6ae987b363ca6c7789f576de4907debaab9 Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Fri, 4 Mar 2016 23:17:02 -0500 Subject: [PATCH 4/7] handle people image failures --- .../Manager/MetadataService.cs | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs index 416cc51bd2..b7991cb78d 100644 --- a/MediaBrowser.Providers/Manager/MetadataService.cs +++ b/MediaBrowser.Providers/Manager/MetadataService.cs @@ -97,7 +97,7 @@ namespace MediaBrowser.Providers.Manager var itemImageProvider = new ItemImageProvider(Logger, ProviderManager, ServerConfigurationManager, FileSystem); var localImagesFailed = false; - var allImageProviders = ((ProviderManager)ProviderManager).GetImageProviders(item, refreshOptions).ToList(); + var allImageProviders = ((ProviderManager)ProviderManager).GetImageProviders(item, refreshOptions).ToList(); // Start by validating images try @@ -301,17 +301,23 @@ namespace MediaBrowser.Providers.Manager { if (ServerConfigurationManager.Configuration.DownloadImagesInAdvance) { - await ProviderManager.SaveImage(personEntity, imageUrl, null, ImageType.Primary, null, cancellationToken).ConfigureAwait(false); - } - else - { - personEntity.SetImage(new ItemImageInfo + try { - Path = imageUrl, - Type = ImageType.Primary, - IsPlaceholder = true - }, 0); + await ProviderManager.SaveImage(personEntity, imageUrl, null, ImageType.Primary, null, cancellationToken).ConfigureAwait(false); + return; + } + catch (Exception ex) + { + Logger.ErrorException("Error in AddPersonImage", ex); + } } + + personEntity.SetImage(new ItemImageInfo + { + Path = imageUrl, + Type = ImageType.Primary, + IsPlaceholder = true + }, 0); } private readonly Task _cachedTask = Task.FromResult(true); From 62023e986aeba762caf9c9f2a2dbe5f5d3177a09 Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Sat, 5 Mar 2016 13:29:14 -0500 Subject: [PATCH 5/7] re-enable sat/ip --- .../LiveTv/TunerHosts/SatIp/SatIpDiscovery.cs | 438 +++++++++--------- .../LiveTv/TunerHosts/SatIp/SatIpHost.cs | 298 ++++++------ 2 files changed, 368 insertions(+), 368 deletions(-) diff --git a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/SatIpDiscovery.cs b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/SatIpDiscovery.cs index f6db2f5a8d..6781e498ac 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/SatIpDiscovery.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/SatIpDiscovery.cs @@ -21,225 +21,225 @@ using MediaBrowser.Model.Serialization; namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.SatIp { - //public class SatIpDiscovery : IServerEntryPoint - //{ - // private readonly IDeviceDiscovery _deviceDiscovery; - // private readonly IServerConfigurationManager _config; - // private readonly ILogger _logger; - // private readonly ILiveTvManager _liveTvManager; - // private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1); - // private readonly IHttpClient _httpClient; - // private readonly IJsonSerializer _json; - - // public static SatIpDiscovery Current; - - // private readonly List _discoveredHosts = new List(); - - // public List DiscoveredHosts - // { - // get { return _discoveredHosts.ToList(); } - // } - - // public SatIpDiscovery(IDeviceDiscovery deviceDiscovery, IServerConfigurationManager config, ILogger logger, ILiveTvManager liveTvManager, IHttpClient httpClient, IJsonSerializer json) - // { - // _deviceDiscovery = deviceDiscovery; - // _config = config; - // _logger = logger; - // _liveTvManager = liveTvManager; - // _httpClient = httpClient; - // _json = json; - // Current = this; - // } - - // public void Run() - // { - // _deviceDiscovery.DeviceDiscovered += _deviceDiscovery_DeviceDiscovered; - // } - - // void _deviceDiscovery_DeviceDiscovered(object sender, SsdpMessageEventArgs e) - // { - // string st = null; - // string nt = null; - // e.Headers.TryGetValue("ST", out st); - // e.Headers.TryGetValue("NT", out nt); - - // if (string.Equals(st, "urn:ses-com:device:SatIPServer:1", StringComparison.OrdinalIgnoreCase) || - // string.Equals(nt, "urn:ses-com:device:SatIPServer:1", StringComparison.OrdinalIgnoreCase)) - // { - // string location; - // if (e.Headers.TryGetValue("Location", out location) && !string.IsNullOrWhiteSpace(location)) - // { - // _logger.Debug("SAT IP found at {0}", location); - - // // Just get the beginning of the url - // AddDevice(location); - // } - // } - // } - - // private async void AddDevice(string location) - // { - // await _semaphore.WaitAsync().ConfigureAwait(false); - - // try - // { - // if (_discoveredHosts.Any(i => string.Equals(i.Type, SatIpHost.DeviceType, StringComparison.OrdinalIgnoreCase) && string.Equals(location, i.Url, StringComparison.OrdinalIgnoreCase))) - // { - // return; - // } - - // _logger.Debug("Will attempt to add SAT device {0}", location); - // var info = await GetInfo(location, CancellationToken.None).ConfigureAwait(false); - - // _discoveredHosts.Add(info); - // } - // catch (OperationCanceledException) - // { - - // } - // catch (NotImplementedException) - // { - - // } - // catch (Exception ex) - // { - // _logger.ErrorException("Error saving device", ex); - // } - // finally - // { - // _semaphore.Release(); - // } - // } - - // public void Dispose() - // { - // } - - // public async Task GetInfo(string url, CancellationToken cancellationToken) - // { - // var result = new SatIpTunerHostInfo - // { - // Url = url, - // IsEnabled = true, - // Type = SatIpHost.DeviceType, - // Tuners = 1, - // TunersAvailable = 1 - // }; - - // using (var stream = await _httpClient.Get(url, cancellationToken).ConfigureAwait(false)) - // { - // using (var streamReader = new StreamReader(stream)) - // { - // // Use XmlReader for best performance - // using (var reader = XmlReader.Create(streamReader)) - // { - // reader.MoveToContent(); - - // // Loop through each element - // while (reader.Read()) - // { - // if (reader.NodeType == XmlNodeType.Element) - // { - // switch (reader.Name) - // { - // case "device": - // using (var subtree = reader.ReadSubtree()) - // { - // FillFromDeviceNode(result, subtree); - // } - // break; - // default: - // reader.Skip(); - // break; - // } - // } - // } - // } - // } - // } - - // if (string.IsNullOrWhiteSpace(result.Id)) - // { - // throw new NotImplementedException(); - // } - - // // Device hasn't implemented an m3u list - // if (string.IsNullOrWhiteSpace(result.M3UUrl)) - // { - // result.IsEnabled = false; - // } - - // else if (!result.M3UUrl.StartsWith("http", StringComparison.OrdinalIgnoreCase)) - // { - // var fullM3uUrl = url.Substring(0, url.LastIndexOf('/')); - // result.M3UUrl = fullM3uUrl + "/" + result.M3UUrl.TrimStart('/'); - // } - - // _logger.Debug("SAT device result: {0}", _json.SerializeToString(result)); - - // return result; - // } - - // private void FillFromDeviceNode(SatIpTunerHostInfo info, XmlReader reader) - // { - // reader.MoveToContent(); - - // while (reader.Read()) - // { - // if (reader.NodeType == XmlNodeType.Element) - // { - // switch (reader.LocalName) - // { - // case "UDN": - // { - // info.Id = reader.ReadElementContentAsString(); - // break; - // } - - // case "friendlyName": - // { - // info.FriendlyName = reader.ReadElementContentAsString(); - // break; - // } - - // case "satip:X_SATIPCAP": - // case "X_SATIPCAP": - // { - // // DVBS2-2 - // var value = reader.ReadElementContentAsString() ?? string.Empty; - // var parts = value.Split(new[] { '-' }, StringSplitOptions.RemoveEmptyEntries); - // if (parts.Length == 2) - // { - // int intValue; - // if (int.TryParse(parts[1], NumberStyles.Any, CultureInfo.InvariantCulture, out intValue)) - // { - // info.TunersAvailable = intValue; - // } - - // if (int.TryParse(parts[0].Substring(parts[0].Length - 1), NumberStyles.Any, CultureInfo.InvariantCulture, out intValue)) - // { - // info.Tuners = intValue; - // } - // } - // break; - // } - - // case "satip:X_SATIPM3U": - // case "X_SATIPM3U": - // { - // // /channellist.lua?select=m3u - // info.M3UUrl = reader.ReadElementContentAsString(); - // break; - // } - - // default: - // reader.Skip(); - // break; - // } - // } - // } - // } - //} + public class SatIpDiscovery : IServerEntryPoint + { + private readonly IDeviceDiscovery _deviceDiscovery; + private readonly IServerConfigurationManager _config; + private readonly ILogger _logger; + private readonly ILiveTvManager _liveTvManager; + private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1); + private readonly IHttpClient _httpClient; + private readonly IJsonSerializer _json; + + public static SatIpDiscovery Current; + + private readonly List _discoveredHosts = new List(); + + public List DiscoveredHosts + { + get { return _discoveredHosts.ToList(); } + } + + public SatIpDiscovery(IDeviceDiscovery deviceDiscovery, IServerConfigurationManager config, ILogger logger, ILiveTvManager liveTvManager, IHttpClient httpClient, IJsonSerializer json) + { + _deviceDiscovery = deviceDiscovery; + _config = config; + _logger = logger; + _liveTvManager = liveTvManager; + _httpClient = httpClient; + _json = json; + Current = this; + } + + public void Run() + { + _deviceDiscovery.DeviceDiscovered += _deviceDiscovery_DeviceDiscovered; + } + + void _deviceDiscovery_DeviceDiscovered(object sender, SsdpMessageEventArgs e) + { + string st = null; + string nt = null; + e.Headers.TryGetValue("ST", out st); + e.Headers.TryGetValue("NT", out nt); + + if (string.Equals(st, "urn:ses-com:device:SatIPServer:1", StringComparison.OrdinalIgnoreCase) || + string.Equals(nt, "urn:ses-com:device:SatIPServer:1", StringComparison.OrdinalIgnoreCase)) + { + string location; + if (e.Headers.TryGetValue("Location", out location) && !string.IsNullOrWhiteSpace(location)) + { + _logger.Debug("SAT IP found at {0}", location); + + // Just get the beginning of the url + AddDevice(location); + } + } + } + + private async void AddDevice(string location) + { + await _semaphore.WaitAsync().ConfigureAwait(false); + + try + { + if (_discoveredHosts.Any(i => string.Equals(i.Type, SatIpHost.DeviceType, StringComparison.OrdinalIgnoreCase) && string.Equals(location, i.Url, StringComparison.OrdinalIgnoreCase))) + { + return; + } + + _logger.Debug("Will attempt to add SAT device {0}", location); + var info = await GetInfo(location, CancellationToken.None).ConfigureAwait(false); + + _discoveredHosts.Add(info); + } + catch (OperationCanceledException) + { + + } + catch (NotImplementedException) + { + + } + catch (Exception ex) + { + _logger.ErrorException("Error saving device", ex); + } + finally + { + _semaphore.Release(); + } + } + + public void Dispose() + { + } + + public async Task GetInfo(string url, CancellationToken cancellationToken) + { + var result = new SatIpTunerHostInfo + { + Url = url, + IsEnabled = true, + Type = SatIpHost.DeviceType, + Tuners = 1, + TunersAvailable = 1 + }; + + using (var stream = await _httpClient.Get(url, cancellationToken).ConfigureAwait(false)) + { + using (var streamReader = new StreamReader(stream)) + { + // Use XmlReader for best performance + using (var reader = XmlReader.Create(streamReader)) + { + reader.MoveToContent(); + + // Loop through each element + while (reader.Read()) + { + if (reader.NodeType == XmlNodeType.Element) + { + switch (reader.Name) + { + case "device": + using (var subtree = reader.ReadSubtree()) + { + FillFromDeviceNode(result, subtree); + } + break; + default: + reader.Skip(); + break; + } + } + } + } + } + } + + if (string.IsNullOrWhiteSpace(result.Id)) + { + throw new NotImplementedException(); + } + + // Device hasn't implemented an m3u list + if (string.IsNullOrWhiteSpace(result.M3UUrl)) + { + result.IsEnabled = false; + } + + else if (!result.M3UUrl.StartsWith("http", StringComparison.OrdinalIgnoreCase)) + { + var fullM3uUrl = url.Substring(0, url.LastIndexOf('/')); + result.M3UUrl = fullM3uUrl + "/" + result.M3UUrl.TrimStart('/'); + } + + _logger.Debug("SAT device result: {0}", _json.SerializeToString(result)); + + return result; + } + + private void FillFromDeviceNode(SatIpTunerHostInfo info, XmlReader reader) + { + reader.MoveToContent(); + + while (reader.Read()) + { + if (reader.NodeType == XmlNodeType.Element) + { + switch (reader.LocalName) + { + case "UDN": + { + info.Id = reader.ReadElementContentAsString(); + break; + } + + case "friendlyName": + { + info.FriendlyName = reader.ReadElementContentAsString(); + break; + } + + case "satip:X_SATIPCAP": + case "X_SATIPCAP": + { + // DVBS2-2 + var value = reader.ReadElementContentAsString() ?? string.Empty; + var parts = value.Split(new[] { '-' }, StringSplitOptions.RemoveEmptyEntries); + if (parts.Length == 2) + { + int intValue; + if (int.TryParse(parts[1], NumberStyles.Any, CultureInfo.InvariantCulture, out intValue)) + { + info.TunersAvailable = intValue; + } + + if (int.TryParse(parts[0].Substring(parts[0].Length - 1), NumberStyles.Any, CultureInfo.InvariantCulture, out intValue)) + { + info.Tuners = intValue; + } + } + break; + } + + case "satip:X_SATIPM3U": + case "X_SATIPM3U": + { + // /channellist.lua?select=m3u + info.M3UUrl = reader.ReadElementContentAsString(); + break; + } + + default: + reader.Skip(); + break; + } + } + } + } + } public class SatIpTunerHostInfo : TunerHostInfo { diff --git a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/SatIpHost.cs b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/SatIpHost.cs index 11213be232..d305a886a6 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/SatIpHost.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/SatIpHost.cs @@ -19,153 +19,153 @@ using MediaBrowser.Model.Serialization; namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.SatIp { - //public class SatIpHost : BaseTunerHost, ITunerHost - //{ - // private readonly IFileSystem _fileSystem; - // private readonly IHttpClient _httpClient; - - // public SatIpHost(IConfigurationManager config, ILogger logger, IJsonSerializer jsonSerializer, IMediaEncoder mediaEncoder, IFileSystem fileSystem, IHttpClient httpClient) - // : base(config, logger, jsonSerializer, mediaEncoder) - // { - // _fileSystem = fileSystem; - // _httpClient = httpClient; - // } - - // private const string ChannelIdPrefix = "sat_"; - - // protected override async Task> GetChannelsInternal(TunerHostInfo tuner, CancellationToken cancellationToken) - // { - // var satInfo = (SatIpTunerHostInfo) tuner; - - // return await new M3uParser(Logger, _fileSystem, _httpClient).Parse(satInfo.M3UUrl, ChannelIdPrefix, tuner.Id, cancellationToken).ConfigureAwait(false); - // } - - // public static string DeviceType - // { - // get { return "satip"; } - // } - - // public override string Type - // { - // get { return DeviceType; } - // } - - // protected override async Task> GetChannelStreamMediaSources(TunerHostInfo tuner, string channelId, CancellationToken cancellationToken) - // { - // var urlHash = tuner.Url.GetMD5().ToString("N"); - // var prefix = ChannelIdPrefix + urlHash; - // if (!channelId.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) - // { - // return null; - // } - - // var channels = await GetChannels(tuner, true, cancellationToken).ConfigureAwait(false); - // var m3uchannels = channels.Cast(); - // var channel = m3uchannels.FirstOrDefault(c => string.Equals(c.Id, channelId, StringComparison.OrdinalIgnoreCase)); - // if (channel != null) - // { - // var path = channel.Path; - // MediaProtocol protocol = MediaProtocol.File; - // if (path.StartsWith("http", StringComparison.OrdinalIgnoreCase)) - // { - // protocol = MediaProtocol.Http; - // } - // else if (path.StartsWith("rtmp", StringComparison.OrdinalIgnoreCase)) - // { - // protocol = MediaProtocol.Rtmp; - // } - // else if (path.StartsWith("rtsp", StringComparison.OrdinalIgnoreCase)) - // { - // protocol = MediaProtocol.Rtsp; - // } - - // var mediaSource = new MediaSourceInfo - // { - // Path = channel.Path, - // Protocol = protocol, - // MediaStreams = new List - // { - // new MediaStream - // { - // Type = MediaStreamType.Video, - // // Set the index to -1 because we don't know the exact index of the video stream within the container - // Index = -1, - // IsInterlaced = true - // }, - // new MediaStream - // { - // Type = MediaStreamType.Audio, - // // Set the index to -1 because we don't know the exact index of the audio stream within the container - // Index = -1 - - // } - // }, - // RequiresOpening = false, - // RequiresClosing = false - // }; - - // return new List { mediaSource }; - // } - // return new List { }; - // } - - // protected override async Task GetChannelStream(TunerHostInfo tuner, string channelId, string streamId, CancellationToken cancellationToken) - // { - // var sources = await GetChannelStreamMediaSources(tuner, channelId, cancellationToken).ConfigureAwait(false); - - // return sources.First(); - // } - - // protected override async Task IsAvailableInternal(TunerHostInfo tuner, string channelId, CancellationToken cancellationToken) - // { - // var updatedInfo = await SatIpDiscovery.Current.GetInfo(tuner.Url, cancellationToken).ConfigureAwait(false); - - // return updatedInfo.TunersAvailable > 0; - // } - - // protected override bool IsValidChannelId(string channelId) - // { - // return channelId.StartsWith(ChannelIdPrefix, StringComparison.OrdinalIgnoreCase); - // } - - // protected override List GetTunerHosts() - // { - // return SatIpDiscovery.Current.DiscoveredHosts; - // } - - // public string Name - // { - // get { return "Sat IP"; } - // } - - // public Task> GetTunerInfos(CancellationToken cancellationToken) - // { - // var list = GetTunerHosts() - // .SelectMany(i => GetTunerInfos(i, cancellationToken)) - // .ToList(); - - // return Task.FromResult(list); - // } - - // public List GetTunerInfos(TunerHostInfo info, CancellationToken cancellationToken) - // { - // var satInfo = (SatIpTunerHostInfo) info; - - // var list = new List(); - - // for (var i = 0; i < satInfo.Tuners; i++) - // { - // list.Add(new LiveTvTunerInfo - // { - // Name = satInfo.FriendlyName ?? Name, - // SourceType = Type, - // Status = LiveTvTunerStatus.Available, - // Id = info.Url.GetMD5().ToString("N") + i.ToString(CultureInfo.InvariantCulture), - // Url = info.Url - // }); - // } - - // return list; - // } - //} + public class SatIpHost : BaseTunerHost, ITunerHost + { + private readonly IFileSystem _fileSystem; + private readonly IHttpClient _httpClient; + + public SatIpHost(IConfigurationManager config, ILogger logger, IJsonSerializer jsonSerializer, IMediaEncoder mediaEncoder, IFileSystem fileSystem, IHttpClient httpClient) + : base(config, logger, jsonSerializer, mediaEncoder) + { + _fileSystem = fileSystem; + _httpClient = httpClient; + } + + private const string ChannelIdPrefix = "sat_"; + + protected override async Task> GetChannelsInternal(TunerHostInfo tuner, CancellationToken cancellationToken) + { + var satInfo = (SatIpTunerHostInfo)tuner; + + return await new M3uParser(Logger, _fileSystem, _httpClient).Parse(satInfo.M3UUrl, ChannelIdPrefix, tuner.Id, cancellationToken).ConfigureAwait(false); + } + + public static string DeviceType + { + get { return "satip"; } + } + + public override string Type + { + get { return DeviceType; } + } + + protected override async Task> GetChannelStreamMediaSources(TunerHostInfo tuner, string channelId, CancellationToken cancellationToken) + { + var urlHash = tuner.Url.GetMD5().ToString("N"); + var prefix = ChannelIdPrefix + urlHash; + if (!channelId.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) + { + return null; + } + + var channels = await GetChannels(tuner, true, cancellationToken).ConfigureAwait(false); + var m3uchannels = channels.Cast(); + var channel = m3uchannels.FirstOrDefault(c => string.Equals(c.Id, channelId, StringComparison.OrdinalIgnoreCase)); + if (channel != null) + { + var path = channel.Path; + MediaProtocol protocol = MediaProtocol.File; + if (path.StartsWith("http", StringComparison.OrdinalIgnoreCase)) + { + protocol = MediaProtocol.Http; + } + else if (path.StartsWith("rtmp", StringComparison.OrdinalIgnoreCase)) + { + protocol = MediaProtocol.Rtmp; + } + else if (path.StartsWith("rtsp", StringComparison.OrdinalIgnoreCase)) + { + protocol = MediaProtocol.Rtsp; + } + + var mediaSource = new MediaSourceInfo + { + Path = channel.Path, + Protocol = protocol, + MediaStreams = new List + { + new MediaStream + { + Type = MediaStreamType.Video, + // Set the index to -1 because we don't know the exact index of the video stream within the container + Index = -1, + IsInterlaced = true + }, + new MediaStream + { + Type = MediaStreamType.Audio, + // Set the index to -1 because we don't know the exact index of the audio stream within the container + Index = -1 + + } + }, + RequiresOpening = false, + RequiresClosing = false + }; + + return new List { mediaSource }; + } + return new List { }; + } + + protected override async Task GetChannelStream(TunerHostInfo tuner, string channelId, string streamId, CancellationToken cancellationToken) + { + var sources = await GetChannelStreamMediaSources(tuner, channelId, cancellationToken).ConfigureAwait(false); + + return sources.First(); + } + + protected override async Task IsAvailableInternal(TunerHostInfo tuner, string channelId, CancellationToken cancellationToken) + { + var updatedInfo = await SatIpDiscovery.Current.GetInfo(tuner.Url, cancellationToken).ConfigureAwait(false); + + return updatedInfo.TunersAvailable > 0; + } + + protected override bool IsValidChannelId(string channelId) + { + return channelId.StartsWith(ChannelIdPrefix, StringComparison.OrdinalIgnoreCase); + } + + protected override List GetTunerHosts() + { + return SatIpDiscovery.Current.DiscoveredHosts; + } + + public string Name + { + get { return "Sat IP"; } + } + + public Task> GetTunerInfos(CancellationToken cancellationToken) + { + var list = GetTunerHosts() + .SelectMany(i => GetTunerInfos(i, cancellationToken)) + .ToList(); + + return Task.FromResult(list); + } + + public List GetTunerInfos(TunerHostInfo info, CancellationToken cancellationToken) + { + var satInfo = (SatIpTunerHostInfo)info; + + var list = new List(); + + for (var i = 0; i < satInfo.Tuners; i++) + { + list.Add(new LiveTvTunerInfo + { + Name = satInfo.FriendlyName ?? Name, + SourceType = Type, + Status = LiveTvTunerStatus.Available, + Id = info.Url.GetMD5().ToString("N") + i.ToString(CultureInfo.InvariantCulture), + Url = info.Url + }); + } + + return list; + } + } } From 0aecb6e2963245c2d43d3450915a9d7561d94a8a Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Sat, 5 Mar 2016 13:29:32 -0500 Subject: [PATCH 6/7] fix opus transcoding --- MediaBrowser.Api/Playback/Progressive/AudioService.cs | 8 ++++++-- MediaBrowser.MediaEncoding/Encoder/AudioEncoder.cs | 8 ++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/MediaBrowser.Api/Playback/Progressive/AudioService.cs b/MediaBrowser.Api/Playback/Progressive/AudioService.cs index ada4761c75..1d8f5003f1 100644 --- a/MediaBrowser.Api/Playback/Progressive/AudioService.cs +++ b/MediaBrowser.Api/Playback/Progressive/AudioService.cs @@ -73,9 +73,13 @@ namespace MediaBrowser.Api.Playback.Progressive audioTranscodeParams.Add("-ac " + state.OutputAudioChannels.Value.ToString(UsCulture)); } - if (state.OutputAudioSampleRate.HasValue) + // opus will fail on 44100 + if (!string.Equals(state.OutputAudioCodec, "opus", global::System.StringComparison.OrdinalIgnoreCase)) { - audioTranscodeParams.Add("-ar " + state.OutputAudioSampleRate.Value.ToString(UsCulture)); + if (state.OutputAudioSampleRate.HasValue) + { + audioTranscodeParams.Add("-ar " + state.OutputAudioSampleRate.Value.ToString(UsCulture)); + } } const string vn = " -vn"; diff --git a/MediaBrowser.MediaEncoding/Encoder/AudioEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/AudioEncoder.cs index a4d4797ebc..c2754217f6 100644 --- a/MediaBrowser.MediaEncoding/Encoder/AudioEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/AudioEncoder.cs @@ -35,9 +35,13 @@ namespace MediaBrowser.MediaEncoding.Encoder audioTranscodeParams.Add("-ac " + state.OutputAudioChannels.Value.ToString(UsCulture)); } - if (state.OutputAudioSampleRate.HasValue) + // opus will fail on 44100 + if (!string.Equals(state.OutputAudioCodec, "opus", StringComparison.OrdinalIgnoreCase)) { - audioTranscodeParams.Add("-ar " + state.OutputAudioSampleRate.Value.ToString(UsCulture)); + if (state.OutputAudioSampleRate.HasValue) + { + audioTranscodeParams.Add("-ar " + state.OutputAudioSampleRate.Value.ToString(UsCulture)); + } } const string vn = " -vn"; From c481daeb204d66f3ce543b2b862a9bc9c7159ca1 Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Sat, 5 Mar 2016 13:51:19 -0500 Subject: [PATCH 7/7] use shared globalize --- MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj b/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj index b8bb397fc4..77365315c8 100644 --- a/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj +++ b/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj @@ -143,6 +143,9 @@ PreserveNewest + + PreserveNewest + PreserveNewest @@ -281,9 +284,6 @@ PreserveNewest - - PreserveNewest - PreserveNewest