From a07ee6536567a048b85d4bdc7d17e1c46f292572 Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Sun, 23 Feb 2020 10:53:51 +0100 Subject: [PATCH 01/40] Minor improvements --- .../ApplicationHost.cs | 9 +- .../Data/SqliteItemRepository.cs | 11 +- .../MediaEncoder/EncodingManager.cs | 40 +++--- .../Chapters/IChapterManager.cs | 5 +- MediaBrowser.Controller/Entities/Folder.cs | 6 +- .../Entities/IItemByName.cs | 2 +- .../MediaEncoding/IEncodingManager.cs | 5 +- .../Persistence/IItemRepository.cs | 2 +- .../Providers/DirectoryService.cs | 8 +- .../Providers/IDirectoryService.cs | 4 +- .../Encoder/MediaEncoder.cs | 2 +- .../Probing/ProbeResultNormalizer.cs | 7 +- .../Chapters/ChapterManager.cs | 23 +--- .../Manager/ProviderManager.cs | 80 ++++++----- .../MediaBrowser.Providers.csproj | 13 ++ .../MediaInfo/FFProbeProvider.cs | 14 +- .../MediaInfo/FFProbeVideoInfo.cs | 128 ++++++++++-------- .../Music/MusicBrainzAlbumProvider.cs | 3 +- .../Tmdb/Movies/TmdbMovieProvider.cs | 22 ++- 19 files changed, 214 insertions(+), 170 deletions(-) diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index fd01122669e..a8f457390db 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -852,10 +852,15 @@ namespace Emby.Server.Implementations serviceCollection.AddSingleton(new DeviceDiscovery(ServerConfigurationManager)); - ChapterManager = new ChapterManager(LibraryManager, LoggerFactory, ServerConfigurationManager, ItemRepository); + ChapterManager = new ChapterManager(ItemRepository); serviceCollection.AddSingleton(ChapterManager); - EncodingManager = new MediaEncoder.EncodingManager(FileSystemManager, LoggerFactory, MediaEncoder, ChapterManager, LibraryManager); + EncodingManager = new MediaEncoder.EncodingManager( + LoggerFactory.CreateLogger(), + FileSystemManager, + MediaEncoder, + ChapterManager, + LibraryManager); serviceCollection.AddSingleton(EncodingManager); var activityLogRepo = GetActivityLogRepository(); diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index 44f38504abb..b73609acbcc 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -2006,7 +2006,7 @@ namespace Emby.Server.Implementations.Data /// /// Saves the chapters. /// - public void SaveChapters(Guid id, List chapters) + public void SaveChapters(Guid id, IReadOnlyList chapters) { CheckDisposed(); @@ -2035,22 +2035,24 @@ namespace Emby.Server.Implementations.Data } } - private void InsertChapters(byte[] idBlob, List chapters, IDatabaseConnection db) + private void InsertChapters(byte[] idBlob, IReadOnlyList chapters, IDatabaseConnection db) { var startIndex = 0; var limit = 100; var chapterIndex = 0; + const string StartInsertText = "insert into " + ChaptersTableName + " (ItemId, ChapterIndex, StartPositionTicks, Name, ImagePath, ImageDateModified) values "; + var insertText = new StringBuilder(StartInsertText, 256); + while (startIndex < chapters.Count) { - var insertText = new StringBuilder("insert into " + ChaptersTableName + " (ItemId, ChapterIndex, StartPositionTicks, Name, ImagePath, ImageDateModified) values "); - var endIndex = Math.Min(chapters.Count, startIndex + limit); for (var i = startIndex; i < endIndex; i++) { insertText.AppendFormat("(@ItemId, @ChapterIndex{0}, @StartPositionTicks{0}, @Name{0}, @ImagePath{0}, @ImageDateModified{0}),", i.ToString(CultureInfo.InvariantCulture)); } + insertText.Length -= 1; // Remove last , using (var statement = PrepareStatement(db, insertText.ToString())) @@ -2077,6 +2079,7 @@ namespace Emby.Server.Implementations.Data } startIndex += limit; + insertText.Length = StartInsertText.Length; } } diff --git a/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs b/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs index 840aca7a6ba..069c02b8544 100644 --- a/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs +++ b/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs @@ -26,14 +26,20 @@ namespace Emby.Server.Implementations.MediaEncoder private readonly IChapterManager _chapterManager; private readonly ILibraryManager _libraryManager; + /// + /// The first chapter ticks. + /// + private static readonly long _firstChapterTicks = TimeSpan.FromSeconds(15).Ticks; + public EncodingManager( + ILogger logger, IFileSystem fileSystem, - ILoggerFactory loggerFactory, IMediaEncoder encoder, - IChapterManager chapterManager, ILibraryManager libraryManager) + IChapterManager chapterManager, + ILibraryManager libraryManager) { + _logger = logger; _fileSystem = fileSystem; - _logger = loggerFactory.CreateLogger(nameof(EncodingManager)); _encoder = encoder; _chapterManager = chapterManager; _libraryManager = libraryManager; @@ -97,12 +103,7 @@ namespace Emby.Server.Implementations.MediaEncoder return video.DefaultVideoStreamIndex.HasValue; } - /// - /// The first chapter ticks - /// - private static readonly long FirstChapterTicks = TimeSpan.FromSeconds(15).Ticks; - - public async Task RefreshChapterImages(Video video, IDirectoryService directoryService, List chapters, bool extractImages, bool saveChapters, CancellationToken cancellationToken) + public async Task RefreshChapterImages(Video video, IDirectoryService directoryService, IReadOnlyList chapters, bool extractImages, bool saveChapters, CancellationToken cancellationToken) { if (!IsEligibleForChapterImageExtraction(video)) { @@ -135,7 +136,7 @@ namespace Emby.Server.Implementations.MediaEncoder try { // Add some time for the first chapter to make sure we don't end up with a black image - var time = chapter.StartPositionTicks == 0 ? TimeSpan.FromTicks(Math.Min(FirstChapterTicks, video.RunTimeTicks ?? 0)) : TimeSpan.FromTicks(chapter.StartPositionTicks); + var time = chapter.StartPositionTicks == 0 ? TimeSpan.FromTicks(Math.Min(_firstChapterTicks, video.RunTimeTicks ?? 0)) : TimeSpan.FromTicks(chapter.StartPositionTicks); var protocol = MediaProtocol.File; @@ -152,9 +153,9 @@ namespace Emby.Server.Implementations.MediaEncoder { _fileSystem.DeleteFile(tempFile); } - catch + catch (IOException ex) { - + _logger.LogError(ex, "Error deleting {Path}", tempFile); } chapter.ImagePath = path; @@ -184,7 +185,7 @@ namespace Emby.Server.Implementations.MediaEncoder if (saveChapters && changesMade) { - _chapterManager.SaveChapters(video.Id.ToString(), chapters); + _chapterManager.SaveChapters(video.Id, chapters); } DeleteDeadImages(currentImages, chapters); @@ -199,22 +200,21 @@ namespace Emby.Server.Implementations.MediaEncoder return Path.Combine(GetChapterImagesPath(video), filename); } - private static List GetSavedChapterImages(Video video, IDirectoryService directoryService) + private static IReadOnlyList GetSavedChapterImages(Video video, IDirectoryService directoryService) { var path = GetChapterImagesPath(video); if (!Directory.Exists(path)) { - return new List(); + return Array.Empty(); } try { - return directoryService.GetFilePaths(path) - .ToList(); + return directoryService.GetFilePaths(path); } catch (IOException) { - return new List(); + return Array.Empty(); } } @@ -227,7 +227,7 @@ namespace Emby.Server.Implementations.MediaEncoder foreach (var image in deadImages) { - _logger.LogDebug("Deleting dead chapter image {path}", image); + _logger.LogDebug("Deleting dead chapter image {Path}", image); try { @@ -235,7 +235,7 @@ namespace Emby.Server.Implementations.MediaEncoder } catch (IOException ex) { - _logger.LogError(ex, "Error deleting {path}.", image); + _logger.LogError(ex, "Error deleting {Path}.", image); } } } diff --git a/MediaBrowser.Controller/Chapters/IChapterManager.cs b/MediaBrowser.Controller/Chapters/IChapterManager.cs index d061898a141..f82e5b41a20 100644 --- a/MediaBrowser.Controller/Chapters/IChapterManager.cs +++ b/MediaBrowser.Controller/Chapters/IChapterManager.cs @@ -1,16 +1,17 @@ +using System; using System.Collections.Generic; using MediaBrowser.Model.Entities; namespace MediaBrowser.Controller.Chapters { /// - /// Interface IChapterManager + /// Interface IChapterManager. /// public interface IChapterManager { /// /// Saves the chapters. /// - void SaveChapters(string itemId, List chapters); + void SaveChapters(Guid itemId, IReadOnlyList chapters); } } diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs index 07fbe60350d..9296ae4fd49 100644 --- a/MediaBrowser.Controller/Entities/Folder.cs +++ b/MediaBrowser.Controller/Entities/Folder.cs @@ -1,3 +1,6 @@ +#pragma warning disable CS1591 +#pragma warning disable SA1600 + using System; using System.Collections.Generic; using System.Globalization; @@ -28,7 +31,6 @@ namespace MediaBrowser.Controller.Entities /// public class Folder : BaseItem { - public static IUserManager UserManager { get; set; } public static IUserViewManager UserViewManager { get; set; } /// @@ -620,7 +622,6 @@ namespace MediaBrowser.Controller.Entities { EnableImages = false } - }).TotalRecordCount; } @@ -1672,7 +1673,6 @@ namespace MediaBrowser.Controller.Entities { EnableImages = false } - }); double unplayedCount = unplayedQueryResult.TotalRecordCount; diff --git a/MediaBrowser.Controller/Entities/IItemByName.cs b/MediaBrowser.Controller/Entities/IItemByName.cs index 89b5dfee36f..8ef5c8d9613 100644 --- a/MediaBrowser.Controller/Entities/IItemByName.cs +++ b/MediaBrowser.Controller/Entities/IItemByName.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; namespace MediaBrowser.Controller.Entities { /// - /// Marker interface + /// Marker interface. /// public interface IItemByName { diff --git a/MediaBrowser.Controller/MediaEncoding/IEncodingManager.cs b/MediaBrowser.Controller/MediaEncoding/IEncodingManager.cs index e560999e8f1..7063d39398d 100644 --- a/MediaBrowser.Controller/MediaEncoding/IEncodingManager.cs +++ b/MediaBrowser.Controller/MediaEncoding/IEncodingManager.cs @@ -1,3 +1,6 @@ +#pragma warning disable CS1591 +#pragma warning disable SA1600 + using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; @@ -12,6 +15,6 @@ namespace MediaBrowser.Controller.MediaEncoding /// /// Refreshes the chapter images. /// - Task RefreshChapterImages(Video video, IDirectoryService directoryService, List chapters, bool extractImages, bool saveChapters, CancellationToken cancellationToken); + Task RefreshChapterImages(Video video, IDirectoryService directoryService, IReadOnlyList chapters, bool extractImages, bool saveChapters, CancellationToken cancellationToken); } } diff --git a/MediaBrowser.Controller/Persistence/IItemRepository.cs b/MediaBrowser.Controller/Persistence/IItemRepository.cs index 5a5b7f58f0a..eb5a8ded84a 100644 --- a/MediaBrowser.Controller/Persistence/IItemRepository.cs +++ b/MediaBrowser.Controller/Persistence/IItemRepository.cs @@ -61,7 +61,7 @@ namespace MediaBrowser.Controller.Persistence /// /// Saves the chapters. /// - void SaveChapters(Guid id, List chapters); + void SaveChapters(Guid id, IReadOnlyList chapters); /// /// Gets the media streams. diff --git a/MediaBrowser.Controller/Providers/DirectoryService.cs b/MediaBrowser.Controller/Providers/DirectoryService.cs index 303b03a216e..b4dcb54415f 100644 --- a/MediaBrowser.Controller/Providers/DirectoryService.cs +++ b/MediaBrowser.Controller/Providers/DirectoryService.cs @@ -69,12 +69,10 @@ namespace MediaBrowser.Controller.Providers //return _fileSystem.GetFileInfo(path); } - public List GetFilePaths(string path) - { - return GetFilePaths(path, false); - } + public IReadOnlyList GetFilePaths(string path) + => GetFilePaths(path, false); - public List GetFilePaths(string path, bool clearCache) + public IReadOnlyList GetFilePaths(string path, bool clearCache) { if (clearCache || !_filePathCache.TryGetValue(path, out List result)) { diff --git a/MediaBrowser.Controller/Providers/IDirectoryService.cs b/MediaBrowser.Controller/Providers/IDirectoryService.cs index 8059d2bd138..e3815facc43 100644 --- a/MediaBrowser.Controller/Providers/IDirectoryService.cs +++ b/MediaBrowser.Controller/Providers/IDirectoryService.cs @@ -9,7 +9,7 @@ namespace MediaBrowser.Controller.Providers List GetFiles(string path); FileSystemMetadata GetFile(string path); - List GetFilePaths(string path); - List GetFilePaths(string path, bool clearCache); + IReadOnlyList GetFilePaths(string path); + IReadOnlyList GetFilePaths(string path, bool clearCache); } } diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index 4123f0203ad..c3a5e21d941 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -429,7 +429,7 @@ namespace MediaBrowser.MediaEncoding.Encoder } } - return new ProbeResultNormalizer(_logger, _fileSystem, _localization).GetMediaInfo(result, videoType, isAudio, primaryPath, protocol); + return new ProbeResultNormalizer(_logger, _localization).GetMediaInfo(result, videoType, isAudio, primaryPath, protocol); } } diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs index f8047af4265..ac626f9f813 100644 --- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs +++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs @@ -9,7 +9,6 @@ using MediaBrowser.Controller.Library; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Globalization; -using MediaBrowser.Model.IO; using MediaBrowser.Model.MediaInfo; using Microsoft.Extensions.Logging; @@ -19,13 +18,11 @@ namespace MediaBrowser.MediaEncoding.Probing { private readonly CultureInfo _usCulture = new CultureInfo("en-US"); private readonly ILogger _logger; - private readonly IFileSystem _fileSystem; private readonly ILocalizationManager _localization; - public ProbeResultNormalizer(ILogger logger, IFileSystem fileSystem, ILocalizationManager localization) + public ProbeResultNormalizer(ILogger logger, ILocalizationManager localization) { _logger = logger; - _fileSystem = fileSystem; _localization = localization; } @@ -40,7 +37,7 @@ namespace MediaBrowser.MediaEncoding.Probing FFProbeHelpers.NormalizeFFProbeResult(data); SetSize(data, info); - var internalStreams = data.Streams ?? new MediaStreamInfo[] { }; + var internalStreams = data.Streams ?? Array.Empty(); info.MediaStreams = internalStreams.Select(s => GetMediaStream(isAudio, s, data.Format)) .Where(i => i != null) diff --git a/MediaBrowser.Providers/Chapters/ChapterManager.cs b/MediaBrowser.Providers/Chapters/ChapterManager.cs index 45e87f137e1..242fc84f6ea 100644 --- a/MediaBrowser.Providers/Chapters/ChapterManager.cs +++ b/MediaBrowser.Providers/Chapters/ChapterManager.cs @@ -1,36 +1,27 @@ +#pragma warning disable CS1591 +#pragma warning disable SA1600 + using System; using System.Collections.Generic; using MediaBrowser.Controller.Chapters; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Persistence; using MediaBrowser.Model.Entities; -using Microsoft.Extensions.Logging; namespace MediaBrowser.Providers.Chapters { public class ChapterManager : IChapterManager { - private readonly ILibraryManager _libraryManager; - private readonly ILogger _logger; - private readonly IServerConfigurationManager _config; private readonly IItemRepository _itemRepo; - public ChapterManager( - ILibraryManager libraryManager, - ILoggerFactory loggerFactory, - IServerConfigurationManager config, - IItemRepository itemRepo) + public ChapterManager(IItemRepository itemRepo) { - _libraryManager = libraryManager; - _logger = loggerFactory.CreateLogger(nameof(ChapterManager)); - _config = config; _itemRepo = itemRepo; } - public void SaveChapters(string itemId, List chapters) + /// + public void SaveChapters(Guid itemId, IReadOnlyList chapters) { - _itemRepo.SaveChapters(new Guid(itemId), chapters); + _itemRepo.SaveChapters(itemId, chapters); } } } diff --git a/MediaBrowser.Providers/Manager/ProviderManager.cs b/MediaBrowser.Providers/Manager/ProviderManager.cs index e7b349f67bc..ae1e0492bf1 100644 --- a/MediaBrowser.Providers/Manager/ProviderManager.cs +++ b/MediaBrowser.Providers/Manager/ProviderManager.cs @@ -1,5 +1,10 @@ +#pragma warning disable CS1591 +#pragma warning disable SA1600 + using System; +using System.Collections.Concurrent; using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Linq; using System.Threading; @@ -897,7 +902,10 @@ namespace MediaBrowser.Providers.Manager return new ExternalUrl { Name = i.Name, - Url = string.Format(i.UrlFormatString, value) + Url = string.Format( + CultureInfo.InvariantCulture, + i.UrlFormatString, + value) }; }).Where(i => i != null).Concat(item.GetRelatedUrls()); @@ -911,11 +919,10 @@ namespace MediaBrowser.Providers.Manager Name = i.Name, Key = i.Key, UrlFormatString = i.UrlFormatString - }); } - private Dictionary _activeRefreshes = new Dictionary(); + private ConcurrentDictionary _activeRefreshes = new ConcurrentDictionary(); public Dictionary GetRefreshQueue() { @@ -927,65 +934,56 @@ namespace MediaBrowser.Providers.Manager { dict[item.Item1] = item.Item1; } + return dict; } } public void OnRefreshStart(BaseItem item) { - //_logger.LogInformation("OnRefreshStart {0}", item.Id.ToString("N", CultureInfo.InvariantCulture)); - var id = item.Id; - - lock (_activeRefreshes) - { - _activeRefreshes[id] = 0; - } - + _logger.LogInformation("OnRefreshStart {0}", item.Id.ToString("N", CultureInfo.InvariantCulture)); + _activeRefreshes[item.Id] = 0; RefreshStarted?.Invoke(this, new GenericEventArgs(item)); } public void OnRefreshComplete(BaseItem item) { - //_logger.LogInformation("OnRefreshComplete {0}", item.Id.ToString("N", CultureInfo.InvariantCulture)); - lock (_activeRefreshes) - { - _activeRefreshes.Remove(item.Id); - } + _logger.LogInformation("OnRefreshComplete {0}", item.Id.ToString("N", CultureInfo.InvariantCulture)); + + _activeRefreshes.Remove(item.Id, out _); RefreshCompleted?.Invoke(this, new GenericEventArgs(item)); } public double? GetRefreshProgress(Guid id) { - lock (_activeRefreshes) + if (_activeRefreshes.TryGetValue(id, out double value)) { - if (_activeRefreshes.TryGetValue(id, out double value)) - { - return value; - } - - return null; + return value; } + + return null; } public void OnRefreshProgress(BaseItem item, double progress) { - //_logger.LogInformation("OnRefreshProgress {0} {1}", item.Id.ToString("N", CultureInfo.InvariantCulture), progress); var id = item.Id; + _logger.LogInformation("OnRefreshProgress {0} {1}", id.ToString("N", CultureInfo.InvariantCulture), progress); - lock (_activeRefreshes) + if (_activeRefreshes.ContainsKey(id)) { - if (_activeRefreshes.ContainsKey(id)) - { - _activeRefreshes[id] = progress; - - RefreshProgress?.Invoke(this, new GenericEventArgs>(new Tuple(item, progress))); - } - else - { - // TODO: Need to hunt down the conditions for this happening - //throw new Exception(string.Format("Refresh for item {0} {1} is not in progress", item.GetType().Name, item.Id.ToString("N", CultureInfo.InvariantCulture))); - } + _activeRefreshes[id] = progress; + RefreshProgress?.Invoke(this, new GenericEventArgs>(new Tuple(item, progress))); + } + else + { + // TODO: Need to hunt down the conditions for this happening + throw new Exception( + string.Format( + CultureInfo.InvariantCulture, + "Refresh for item {0} {1} is not in progress", + item.GetType().Name, + item.Id.ToString("N", CultureInfo.InvariantCulture))); } } @@ -1040,10 +1038,9 @@ namespace MediaBrowser.Providers.Manager // Try to throttle this a little bit. await Task.Delay(100).ConfigureAwait(false); - var artist = item as MusicArtist; - var task = artist == null - ? RefreshItem(item, refreshItem.Item2, cancellationToken) - : RefreshArtist(artist, refreshItem.Item2, cancellationToken); + var task = item is MusicArtist artist + ? RefreshArtist(artist, refreshItem.Item2, cancellationToken) + : RefreshItem(item, refreshItem.Item2, cancellationToken); await task.ConfigureAwait(false); } @@ -1125,8 +1122,7 @@ namespace MediaBrowser.Providers.Manager } } - public Task RefreshFullItem(BaseItem item, MetadataRefreshOptions options, - CancellationToken cancellationToken) + public Task RefreshFullItem(BaseItem item, MetadataRefreshOptions options, CancellationToken cancellationToken) { return RefreshItem(item, options, cancellationToken); } diff --git a/MediaBrowser.Providers/MediaBrowser.Providers.csproj b/MediaBrowser.Providers/MediaBrowser.Providers.csproj index 5593c5036c4..4e33d0048ef 100644 --- a/MediaBrowser.Providers/MediaBrowser.Providers.csproj +++ b/MediaBrowser.Providers/MediaBrowser.Providers.csproj @@ -22,6 +22,19 @@ netstandard2.1 false true + true + + + + + + + + + + + + ../jellyfin.ruleset diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeProvider.cs b/MediaBrowser.Providers/MediaInfo/FFProbeProvider.cs index 4e11fcbb2a0..95808eb1a6d 100644 --- a/MediaBrowser.Providers/MediaInfo/FFProbeProvider.cs +++ b/MediaBrowser.Providers/MediaInfo/FFProbeProvider.cs @@ -177,7 +177,19 @@ namespace MediaBrowser.Providers.MediaInfo FetchShortcutInfo(item); } - var prober = new FFProbeVideoInfo(_logger, _mediaSourceManager, _isoManager, _mediaEncoder, _itemRepo, _blurayExaminer, _localization, _appPaths, _json, _encodingManager, _fileSystem, _config, _subtitleManager, _chapterManager, _libraryManager); + var prober = new FFProbeVideoInfo( + _logger, + _mediaSourceManager, + _mediaEncoder, + _itemRepo, + _blurayExaminer, + _localization, + _encodingManager, + _fileSystem, + _config, + _subtitleManager, + _chapterManager, + _libraryManager); return prober.ProbeVideo(item, options, cancellationToken); } diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs index 2b178d4d4e6..ea1db88ba4d 100644 --- a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs +++ b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs @@ -1,3 +1,6 @@ +#pragma warning disable CS1591 +#pragma warning disable SA1600 + using System; using System.Collections.Generic; using System.Globalization; @@ -25,7 +28,6 @@ using MediaBrowser.Model.Globalization; using MediaBrowser.Model.IO; using MediaBrowser.Model.MediaInfo; using MediaBrowser.Model.Providers; -using MediaBrowser.Model.Serialization; using Microsoft.Extensions.Logging; namespace MediaBrowser.Providers.MediaInfo @@ -33,13 +35,10 @@ namespace MediaBrowser.Providers.MediaInfo public class FFProbeVideoInfo { private readonly ILogger _logger; - private readonly IIsoManager _isoManager; private readonly IMediaEncoder _mediaEncoder; private readonly IItemRepository _itemRepo; private readonly IBlurayExaminer _blurayExaminer; private readonly ILocalizationManager _localization; - private readonly IApplicationPaths _appPaths; - private readonly IJsonSerializer _json; private readonly IEncodingManager _encodingManager; private readonly IFileSystem _fileSystem; private readonly IServerConfigurationManager _config; @@ -48,16 +47,30 @@ namespace MediaBrowser.Providers.MediaInfo private readonly ILibraryManager _libraryManager; private readonly IMediaSourceManager _mediaSourceManager; - public FFProbeVideoInfo(ILogger logger, IMediaSourceManager mediaSourceManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IItemRepository itemRepo, IBlurayExaminer blurayExaminer, ILocalizationManager localization, IApplicationPaths appPaths, IJsonSerializer json, IEncodingManager encodingManager, IFileSystem fileSystem, IServerConfigurationManager config, ISubtitleManager subtitleManager, IChapterManager chapterManager, ILibraryManager libraryManager) + /// + /// The dummy chapter duration. + /// + private readonly long _dummyChapterDuration = TimeSpan.FromMinutes(5).Ticks; + + public FFProbeVideoInfo( + ILogger logger, + IMediaSourceManager mediaSourceManager, + IMediaEncoder mediaEncoder, + IItemRepository itemRepo, + IBlurayExaminer blurayExaminer, + ILocalizationManager localization, + IEncodingManager encodingManager, + IFileSystem fileSystem, + IServerConfigurationManager config, + ISubtitleManager subtitleManager, + IChapterManager chapterManager, + ILibraryManager libraryManager) { _logger = logger; - _isoManager = isoManager; _mediaEncoder = mediaEncoder; _itemRepo = itemRepo; _blurayExaminer = blurayExaminer; _localization = localization; - _appPaths = appPaths; - _json = json; _encodingManager = encodingManager; _fileSystem = fileSystem; _config = config; @@ -159,7 +172,7 @@ namespace MediaBrowser.Providers.MediaInfo { List mediaStreams; IReadOnlyList mediaAttachments; - List chapters; + ChapterInfo[] chapters; if (mediaInfo != null) { @@ -191,17 +204,17 @@ namespace MediaBrowser.Providers.MediaInfo } video.Container = mediaInfo.Container; - chapters = mediaInfo.Chapters == null ? new List() : mediaInfo.Chapters.ToList(); + chapters = mediaInfo.Chapters == null ? Array.Empty() : mediaInfo.Chapters; if (blurayInfo != null) { - FetchBdInfo(video, chapters, mediaStreams, blurayInfo); + FetchBdInfo(video, ref chapters, mediaStreams, blurayInfo); } } else { mediaStreams = new List(); mediaAttachments = Array.Empty(); - chapters = new List(); + chapters = Array.Empty(); } await AddExternalSubtitles(video, mediaStreams, options, cancellationToken).ConfigureAwait(false); @@ -231,9 +244,9 @@ namespace MediaBrowser.Providers.MediaInfo if (options.MetadataRefreshMode == MetadataRefreshMode.FullRefresh || options.MetadataRefreshMode == MetadataRefreshMode.Default) { - if (chapters.Count == 0 && mediaStreams.Any(i => i.Type == MediaStreamType.Video)) + if (chapters.Length == 0 && mediaStreams.Any(i => i.Type == MediaStreamType.Video)) { - AddDummyChapters(video, chapters); + CreateDummyChapters(video, ref chapters); } NormalizeChapterNames(chapters); @@ -246,28 +259,29 @@ namespace MediaBrowser.Providers.MediaInfo await _encodingManager.RefreshChapterImages(video, options.DirectoryService, chapters, extractDuringScan, false, cancellationToken).ConfigureAwait(false); - _chapterManager.SaveChapters(video.Id.ToString(), chapters); + _chapterManager.SaveChapters(video.Id, chapters); } } - private void NormalizeChapterNames(List chapters) + private void NormalizeChapterNames(ChapterInfo[] chapters) { - var index = 1; - - foreach (var chapter in chapters) + for (int i = 0; i < chapters.Length; i++) { + string name = chapters[i].Name; // Check if the name is empty and/or if the name is a time // Some ripping programs do that. - if (string.IsNullOrWhiteSpace(chapter.Name) || - TimeSpan.TryParse(chapter.Name, out var time)) + if (string.IsNullOrWhiteSpace(name) || + TimeSpan.TryParse(name, out _)) { - chapter.Name = string.Format(_localization.GetLocalizedString("ChapterNameValue"), index.ToString(CultureInfo.InvariantCulture)); + chapters[i].Name = string.Format( + CultureInfo.InvariantCulture, + _localization.GetLocalizedString("ChapterNameValue"), + (i + 1).ToString(CultureInfo.InvariantCulture)); } - index++; } } - private void FetchBdInfo(BaseItem item, List chapters, List mediaStreams, BlurayDiscInfo blurayInfo) + private void FetchBdInfo(BaseItem item, ref ChapterInfo[] chapters, List mediaStreams, BlurayDiscInfo blurayInfo) { var video = (Video)item; @@ -301,13 +315,15 @@ namespace MediaBrowser.Providers.MediaInfo if (blurayInfo.Chapters != null) { - chapters.Clear(); - - chapters.AddRange(blurayInfo.Chapters.Select(c => new ChapterInfo + double[] brChapter = blurayInfo.Chapters; + chapters = new ChapterInfo[brChapter.Length]; + for (int i = 0; i < brChapter.Length; i++) { - StartPositionTicks = TimeSpan.FromSeconds(c).Ticks - - })); + chapters[i] = new ChapterInfo + { + StartPositionTicks = TimeSpan.FromSeconds(brChapter[i]).Ticks + }; + } } videoStream = mediaStreams.FirstOrDefault(s => s.Type == MediaStreamType.Video); @@ -495,17 +511,17 @@ namespace MediaBrowser.Providers.MediaInfo var libraryOptions = _libraryManager.GetLibraryOptions(video); string[] subtitleDownloadLanguages; - bool SkipIfEmbeddedSubtitlesPresent; - bool SkipIfAudioTrackMatches; - bool RequirePerfectMatch; + bool skipIfEmbeddedSubtitlesPresent; + bool skipIfAudioTrackMatches; + bool requirePerfectMatch; bool enabled; if (libraryOptions.SubtitleDownloadLanguages == null) { subtitleDownloadLanguages = subtitleOptions.DownloadLanguages; - SkipIfEmbeddedSubtitlesPresent = subtitleOptions.SkipIfEmbeddedSubtitlesPresent; - SkipIfAudioTrackMatches = subtitleOptions.SkipIfAudioTrackMatches; - RequirePerfectMatch = subtitleOptions.RequirePerfectMatch; + skipIfEmbeddedSubtitlesPresent = subtitleOptions.SkipIfEmbeddedSubtitlesPresent; + skipIfAudioTrackMatches = subtitleOptions.SkipIfAudioTrackMatches; + requirePerfectMatch = subtitleOptions.RequirePerfectMatch; enabled = (subtitleOptions.DownloadEpisodeSubtitles && video is Episode) || (subtitleOptions.DownloadMovieSubtitles && @@ -514,9 +530,9 @@ namespace MediaBrowser.Providers.MediaInfo else { subtitleDownloadLanguages = libraryOptions.SubtitleDownloadLanguages; - SkipIfEmbeddedSubtitlesPresent = libraryOptions.SkipSubtitlesIfEmbeddedSubtitlesPresent; - SkipIfAudioTrackMatches = libraryOptions.SkipSubtitlesIfAudioTrackMatches; - RequirePerfectMatch = libraryOptions.RequirePerfectSubtitleMatch; + skipIfEmbeddedSubtitlesPresent = libraryOptions.SkipSubtitlesIfEmbeddedSubtitlesPresent; + skipIfAudioTrackMatches = libraryOptions.SkipSubtitlesIfAudioTrackMatches; + requirePerfectMatch = libraryOptions.RequirePerfectSubtitleMatch; enabled = true; } @@ -526,9 +542,9 @@ namespace MediaBrowser.Providers.MediaInfo _subtitleManager) .DownloadSubtitles(video, currentStreams.Concat(externalSubtitleStreams).ToList(), - SkipIfEmbeddedSubtitlesPresent, - SkipIfAudioTrackMatches, - RequirePerfectMatch, + skipIfEmbeddedSubtitlesPresent, + skipIfAudioTrackMatches, + requirePerfectMatch, subtitleDownloadLanguages, libraryOptions.DisabledSubtitleFetchers, libraryOptions.SubtitleFetcherOrder, @@ -546,23 +562,23 @@ namespace MediaBrowser.Providers.MediaInfo currentStreams.AddRange(externalSubtitleStreams); } - /// - /// The dummy chapter duration - /// - private readonly long _dummyChapterDuration = TimeSpan.FromMinutes(5).Ticks; - /// /// Adds the dummy chapters. /// /// The video. /// The chapters. - private void AddDummyChapters(Video video, List chapters) + private void CreateDummyChapters(Video video, ref ChapterInfo[] chapters) { var runtime = video.RunTimeTicks ?? 0; if (runtime < 0) { - throw new ArgumentException(string.Format("{0} has invalid runtime of {1}", video.Name, runtime)); + throw new ArgumentException( + string.Format( + CultureInfo.InvariantCulture, + "{0} has invalid runtime of {1}", + video.Name, + runtime)); } if (runtime < _dummyChapterDuration) @@ -570,18 +586,18 @@ namespace MediaBrowser.Providers.MediaInfo return; } - long currentChapterTicks = 0; - var index = 1; - // Limit to 100 chapters just in case there's some incorrect metadata here - while (currentChapterTicks < runtime && index < 100) + int chapterCount = (int)Math.Min(runtime / _dummyChapterDuration, 100); + chapters = new ChapterInfo[chapterCount]; + + long currentChapterTicks = 0; + for (int i = 0; i < chapterCount; i++) { - chapters.Add(new ChapterInfo + chapters[i] = new ChapterInfo { StartPositionTicks = currentChapterTicks - }); + }; - index++; currentChapterTicks += _dummyChapterDuration; } } diff --git a/MediaBrowser.Providers/Music/MusicBrainzAlbumProvider.cs b/MediaBrowser.Providers/Music/MusicBrainzAlbumProvider.cs index 8e71b625ee0..3e276ba3761 100644 --- a/MediaBrowser.Providers/Music/MusicBrainzAlbumProvider.cs +++ b/MediaBrowser.Providers/Music/MusicBrainzAlbumProvider.cs @@ -5,6 +5,7 @@ using System.Globalization; using System.IO; using System.Linq; using System.Net; +using System.Net.Http; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -776,7 +777,7 @@ namespace MediaBrowser.Providers.Music _logger.LogDebug("GetMusicBrainzResponse: Time since previous request: {0} ms", _stopWatchMusicBrainz.ElapsedMilliseconds); _stopWatchMusicBrainz.Restart(); - response = await _httpClient.SendAsync(options, "GET").ConfigureAwait(false); + response = await _httpClient.SendAsync(options, HttpMethod.Get).ConfigureAwait(false); // We retry a finite number of times, and only whilst MB is indcating 503 (throttling) } diff --git a/MediaBrowser.Providers/Tmdb/Movies/TmdbMovieProvider.cs b/MediaBrowser.Providers/Tmdb/Movies/TmdbMovieProvider.cs index 861847f71f2..e2fd5b9e308 100644 --- a/MediaBrowser.Providers/Tmdb/Movies/TmdbMovieProvider.cs +++ b/MediaBrowser.Providers/Tmdb/Movies/TmdbMovieProvider.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Globalization; using System.IO; using System.Net; +using System.Net.Http; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common; @@ -36,20 +37,25 @@ namespace MediaBrowser.Providers.Tmdb.Movies private readonly IFileSystem _fileSystem; private readonly IServerConfigurationManager _configurationManager; private readonly ILogger _logger; - private readonly ILocalizationManager _localization; private readonly ILibraryManager _libraryManager; private readonly IApplicationHost _appHost; private readonly CultureInfo _usCulture = new CultureInfo("en-US"); - public TmdbMovieProvider(IJsonSerializer jsonSerializer, IHttpClient httpClient, IFileSystem fileSystem, IServerConfigurationManager configurationManager, ILogger logger, ILocalizationManager localization, ILibraryManager libraryManager, IApplicationHost appHost) + public TmdbMovieProvider( + IJsonSerializer jsonSerializer, + IHttpClient httpClient, + IFileSystem fileSystem, + IServerConfigurationManager configurationManager, + ILogger logger, + ILibraryManager libraryManager, + IApplicationHost appHost) { _jsonSerializer = jsonSerializer; _httpClient = httpClient; _fileSystem = fileSystem; _configurationManager = configurationManager; _logger = logger; - _localization = localization; _libraryManager = libraryManager; _appHost = appHost; Current = this; @@ -401,15 +407,15 @@ namespace MediaBrowser.Providers.Tmdb.Movies private static long _lastRequestTicks; // The limit is 40 requests per 10 seconds - private static int requestIntervalMs = 300; + private const int RequestIntervalMs = 300; /// /// Gets the movie db response. /// internal async Task GetMovieDbResponse(HttpRequestOptions options) { - var delayTicks = (requestIntervalMs * 10000) - (DateTime.UtcNow.Ticks - _lastRequestTicks); - var delayMs = Math.Min(delayTicks / 10000, requestIntervalMs); + var delayTicks = (RequestIntervalMs * 10000) - (DateTime.UtcNow.Ticks - _lastRequestTicks); + var delayMs = Math.Min(delayTicks / 10000, RequestIntervalMs); if (delayMs > 0) { @@ -422,11 +428,13 @@ namespace MediaBrowser.Providers.Tmdb.Movies options.BufferContent = true; options.UserAgent = _appHost.ApplicationUserAgent; - return await _httpClient.SendAsync(options, "GET").ConfigureAwait(false); + return await _httpClient.SendAsync(options, HttpMethod.Get).ConfigureAwait(false); } + /// public int Order => 1; + /// public Task GetImageResponse(string url, CancellationToken cancellationToken) { return _httpClient.GetResponse(new HttpRequestOptions From 6d6fa1355063e9ee2773c579b2c4fdbf8370b3ec Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Sun, 23 Feb 2020 10:58:45 +0100 Subject: [PATCH 02/40] Fix release build --- MediaBrowser.Providers/MediaBrowser.Providers.csproj | 1 - 1 file changed, 1 deletion(-) diff --git a/MediaBrowser.Providers/MediaBrowser.Providers.csproj b/MediaBrowser.Providers/MediaBrowser.Providers.csproj index 4e33d0048ef..d0ed61c8332 100644 --- a/MediaBrowser.Providers/MediaBrowser.Providers.csproj +++ b/MediaBrowser.Providers/MediaBrowser.Providers.csproj @@ -22,7 +22,6 @@ netstandard2.1 false true - true From 26af5ea45a8ea02b7a3f20b0ebc31ef19f850dea Mon Sep 17 00:00:00 2001 From: Mark Monteiro Date: Tue, 25 Feb 2020 16:51:36 +0100 Subject: [PATCH 03/40] Do not set a static content root if the jellyfin-web directory does not exist or is empty --- .../AppBase/BaseApplicationPaths.cs | 2 +- .../ApplicationHost.cs | 2 +- Jellyfin.Server/Program.cs | 20 ++++++++++++++++--- 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs b/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs index c3cdcc22222..be2d198efcb 100644 --- a/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs +++ b/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs @@ -40,7 +40,7 @@ namespace Emby.Server.Implementations.AppBase /// /// Gets the path to the web UI resources folder. /// - /// The web UI resources path. + /// The web UI resources path, or null if the server is not hosting any web content. public string WebPath { get; } /// diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 8ea188724a0..a5b88f64f2c 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -240,7 +240,7 @@ namespace Emby.Server.Implementations public int HttpsPort { get; private set; } /// - /// Gets the content root for the webhost. + /// Gets the content root for the webhost. If the webhost is not serving static web content, this will be null. /// public string ContentRoot { get; private set; } diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index 1dd598236be..2ce7379fbae 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -222,7 +222,7 @@ namespace Jellyfin.Server private static IWebHostBuilder CreateWebHostBuilder(ApplicationHost appHost, IServiceCollection serviceCollection) { - return new WebHostBuilder() + var webhostBuilder = new WebHostBuilder() .UseKestrel(options => { var addresses = appHost.ServerConfigurationManager @@ -260,13 +260,20 @@ namespace Jellyfin.Server } } }) - .UseContentRoot(appHost.ContentRoot) .ConfigureServices(services => { // Merge the external ServiceCollection into ASP.NET DI services.TryAdd(serviceCollection); }) .UseStartup(); + + // Set the root directory for static content, if one exists + if (!string.IsNullOrEmpty(appHost.ContentRoot)) + { + webhostBuilder.UseContentRoot(appHost.ContentRoot); + } + + return webhostBuilder; } /// @@ -383,7 +390,7 @@ namespace Jellyfin.Server // webDir // IF --webdir // ELSE IF $JELLYFIN_WEB_DIR - // ELSE use /jellyfin-web + // ELSE /jellyfin-web var webDir = options.WebDir; if (string.IsNullOrEmpty(webDir)) @@ -397,6 +404,13 @@ namespace Jellyfin.Server } } + // Reset webDir if the directory does not exist, or is empty + if (!Directory.Exists(webDir) || !Directory.GetFiles(webDir).Any()) + { + _logger.LogInformation("Server will not host static content because the web content directory does not exist or is empty: {ContentRoot}", webDir); + webDir = null; + } + // logDir // IF --logdir // ELSE IF $JELLYFIN_LOG_DIR From 1b5999a1bc11ee386131e4bbce561896c0e1bced Mon Sep 17 00:00:00 2001 From: Mark Monteiro Date: Tue, 25 Feb 2020 17:01:57 +0100 Subject: [PATCH 04/40] Open the Swagger API page on server start if not hosting the static web content --- Emby.Server.Implementations/ApplicationHost.cs | 3 +++ Emby.Server.Implementations/Browser/BrowserLauncher.cs | 10 ++++++++++ .../EntryPoints/StartupWizard.cs | 6 +++++- MediaBrowser.Controller/IServerApplicationHost.cs | 5 +++++ 4 files changed, 23 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index a5b88f64f2c..789b8724c3a 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -244,6 +244,9 @@ namespace Emby.Server.Implementations /// public string ContentRoot { get; private set; } + /// + public bool IsHostingContent => ContentRoot != null; + /// /// Gets the server configuration manager. /// diff --git a/Emby.Server.Implementations/Browser/BrowserLauncher.cs b/Emby.Server.Implementations/Browser/BrowserLauncher.cs index f5da0d01836..b17c2b27005 100644 --- a/Emby.Server.Implementations/Browser/BrowserLauncher.cs +++ b/Emby.Server.Implementations/Browser/BrowserLauncher.cs @@ -29,6 +29,16 @@ namespace Emby.Server.Implementations.Browser OpenDashboardPage("index.html", appHost); } + /// + /// Opens the swagger API page. + /// + /// The app host. + public static void OpenSwaggerPage(IServerApplicationHost appHost) + { + var url = appHost.GetLocalApiUrl("localhost") + "/swagger/index.html"; + OpenUrl(appHost, url); + } + /// /// Opens the URL. /// diff --git a/Emby.Server.Implementations/EntryPoints/StartupWizard.cs b/Emby.Server.Implementations/EntryPoints/StartupWizard.cs index 5f2d629fead..6b7e2805bd5 100644 --- a/Emby.Server.Implementations/EntryPoints/StartupWizard.cs +++ b/Emby.Server.Implementations/EntryPoints/StartupWizard.cs @@ -36,7 +36,11 @@ namespace Emby.Server.Implementations.EntryPoints return Task.CompletedTask; } - if (!_config.Configuration.IsStartupWizardCompleted) + if (!_appHost.IsHostingContent) + { + BrowserLauncher.OpenSwaggerPage(_appHost); + } + else if (!_config.Configuration.IsStartupWizardCompleted) { BrowserLauncher.OpenWebApp(_appHost); } diff --git a/MediaBrowser.Controller/IServerApplicationHost.cs b/MediaBrowser.Controller/IServerApplicationHost.cs index 25f0905eb83..134ed110ba9 100644 --- a/MediaBrowser.Controller/IServerApplicationHost.cs +++ b/MediaBrowser.Controller/IServerApplicationHost.cs @@ -16,6 +16,11 @@ namespace MediaBrowser.Controller { event EventHandler HasUpdateAvailableChanged; + /// + /// Gets a value indicating whether the server is hosting the static web content from jellyfin-web. + /// + bool IsHostingContent { get; } + /// /// Gets the system info. /// From f3be93a4de86177d87a3be919b7aaa6e394b819d Mon Sep 17 00:00:00 2001 From: Mark Monteiro Date: Tue, 25 Feb 2020 17:02:51 +0100 Subject: [PATCH 05/40] Use the swagger API page as the default redirect path if not hosting the jellyfin-web content --- Emby.Server.Implementations/ConfigurationOptions.cs | 8 +++++++- Jellyfin.Server/Program.cs | 9 ++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/Emby.Server.Implementations/ConfigurationOptions.cs b/Emby.Server.Implementations/ConfigurationOptions.cs index 2ea7ff6e911..b733bd2d57d 100644 --- a/Emby.Server.Implementations/ConfigurationOptions.cs +++ b/Emby.Server.Implementations/ConfigurationOptions.cs @@ -3,9 +3,15 @@ using static MediaBrowser.Controller.Extensions.ConfigurationExtensions; namespace Emby.Server.Implementations { + /// + /// Static class containing the default configuration options for the web server. + /// public static class ConfigurationOptions { - public static Dictionary Configuration => new Dictionary + /// + /// Gets the default configuration options. + /// + public static Dictionary DefaultConfiguration => new Dictionary { { "HttpListenerHost:DefaultRedirectPath", "web/index.html" }, { "MusicBrainz:BaseUrl", "https://www.musicbrainz.org" }, diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index 2ce7379fbae..8c133576ca5 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -468,9 +468,16 @@ namespace Jellyfin.Server await resource.CopyToAsync(dst).ConfigureAwait(false); } + // Use the swagger API page as the default redirect path if not hosting the jellyfin-web content + var inMemoryDefaultConfig = ConfigurationOptions.DefaultConfiguration; + if (string.IsNullOrEmpty(appPaths.WebPath)) + { + inMemoryDefaultConfig["HttpListenerHost:DefaultRedirectPath"] = "swagger/index.html"; + } + return new ConfigurationBuilder() .SetBasePath(appPaths.ConfigurationDirectoryPath) - .AddInMemoryCollection(ConfigurationOptions.Configuration) + .AddInMemoryCollection(inMemoryDefaultConfig) .AddJsonFile("logging.json", false, true) .AddEnvironmentVariables("JELLYFIN_") .Build(); From 192ec57b608a2cd1f8b337fb416b84abccd7a2a0 Mon Sep 17 00:00:00 2001 From: Mark Monteiro Date: Tue, 25 Feb 2020 17:22:21 +0100 Subject: [PATCH 06/40] Remove log message executed before logger is initialized --- Emby.Server.Implementations/ConfigurationOptions.cs | 2 +- Jellyfin.Server/Program.cs | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/Emby.Server.Implementations/ConfigurationOptions.cs b/Emby.Server.Implementations/ConfigurationOptions.cs index b733bd2d57d..456484813d7 100644 --- a/Emby.Server.Implementations/ConfigurationOptions.cs +++ b/Emby.Server.Implementations/ConfigurationOptions.cs @@ -9,7 +9,7 @@ namespace Emby.Server.Implementations public static class ConfigurationOptions { /// - /// Gets the default configuration options. + /// Gets a new copy of the default configuration options. /// public static Dictionary DefaultConfiguration => new Dictionary { diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index 8c133576ca5..c4e3cd72687 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -392,7 +392,6 @@ namespace Jellyfin.Server // ELSE IF $JELLYFIN_WEB_DIR // ELSE /jellyfin-web var webDir = options.WebDir; - if (string.IsNullOrEmpty(webDir)) { webDir = Environment.GetEnvironmentVariable("JELLYFIN_WEB_DIR"); @@ -407,7 +406,6 @@ namespace Jellyfin.Server // Reset webDir if the directory does not exist, or is empty if (!Directory.Exists(webDir) || !Directory.GetFiles(webDir).Any()) { - _logger.LogInformation("Server will not host static content because the web content directory does not exist or is empty: {ContentRoot}", webDir); webDir = null; } From d95ccbacac7df20ab445e73dd9405ec1391c7a7a Mon Sep 17 00:00:00 2001 From: Mark Monteiro Date: Wed, 26 Feb 2020 16:11:09 +0100 Subject: [PATCH 07/40] Use IsHostingContent instead of explicitly checking ContentRoot --- Jellyfin.Server/Program.cs | 2 +- MediaBrowser.Model/Configuration/ServerConfiguration.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index c4e3cd72687..4048a0d0310 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -268,7 +268,7 @@ namespace Jellyfin.Server .UseStartup(); // Set the root directory for static content, if one exists - if (!string.IsNullOrEmpty(appHost.ContentRoot)) + if (appHost.IsHostingContent) { webhostBuilder.UseContentRoot(appHost.ContentRoot); } diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs index 5280d455c4f..43a84d38152 100644 --- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs +++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs @@ -149,9 +149,9 @@ namespace MediaBrowser.Model.Configuration public bool EnableDashboardResponseCaching { get; set; } /// - /// Allows the dashboard to be served from a custom path. + /// Gets or sets a custom path to serve the dashboard from. /// - /// The dashboard source path. + /// The dashboard source path, or null if the default path should be used. public string DashboardSourcePath { get; set; } /// From d1e1aef5f796ff5c25338d44cb4f6a7aadf24af4 Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Wed, 26 Feb 2020 18:19:34 +0100 Subject: [PATCH 08/40] Fix possible race condition --- MediaBrowser.Providers/Manager/ProviderManager.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/MediaBrowser.Providers/Manager/ProviderManager.cs b/MediaBrowser.Providers/Manager/ProviderManager.cs index ae1e0492bf1..4f1513df344 100644 --- a/MediaBrowser.Providers/Manager/ProviderManager.cs +++ b/MediaBrowser.Providers/Manager/ProviderManager.cs @@ -970,9 +970,8 @@ namespace MediaBrowser.Providers.Manager var id = item.Id; _logger.LogInformation("OnRefreshProgress {0} {1}", id.ToString("N", CultureInfo.InvariantCulture), progress); - if (_activeRefreshes.ContainsKey(id)) + if (_activeRefreshes.TryAdd(id, progress)) { - _activeRefreshes[id] = progress; RefreshProgress?.Invoke(this, new GenericEventArgs>(new Tuple(item, progress))); } else From 3043b7323b1a12e888ba8c4eb71f904724f43e3a Mon Sep 17 00:00:00 2001 From: Mark Monteiro Date: Fri, 28 Feb 2020 17:57:38 +0100 Subject: [PATCH 09/40] Use constants for settings keys --- Emby.Server.Implementations/ConfigurationOptions.cs | 6 ++++-- .../HttpServer/HttpListenerHost.cs | 8 +++++++- Jellyfin.Server/Program.cs | 3 ++- MediaBrowser.Providers/Music/MusicBrainzAlbumProvider.cs | 7 ++++++- 4 files changed, 19 insertions(+), 5 deletions(-) diff --git a/Emby.Server.Implementations/ConfigurationOptions.cs b/Emby.Server.Implementations/ConfigurationOptions.cs index 456484813d7..cc0d314edc6 100644 --- a/Emby.Server.Implementations/ConfigurationOptions.cs +++ b/Emby.Server.Implementations/ConfigurationOptions.cs @@ -1,4 +1,6 @@ using System.Collections.Generic; +using Emby.Server.Implementations.HttpServer; +using MediaBrowser.Providers.Music; using static MediaBrowser.Controller.Extensions.ConfigurationExtensions; namespace Emby.Server.Implementations @@ -13,8 +15,8 @@ namespace Emby.Server.Implementations /// public static Dictionary DefaultConfiguration => new Dictionary { - { "HttpListenerHost:DefaultRedirectPath", "web/index.html" }, - { "MusicBrainz:BaseUrl", "https://www.musicbrainz.org" }, + { HttpListenerHost.DefaultRedirectKey, "web/index.html" }, + { MusicBrainzAlbumProvider.BaseUrlKey, "https://www.musicbrainz.org" }, { FfmpegProbeSizeKey, "1G" }, { FfmpegAnalyzeDurationKey, "200M" } }; diff --git a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs index 85602a67f0e..7f16c683438 100644 --- a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs +++ b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs @@ -30,6 +30,12 @@ namespace Emby.Server.Implementations.HttpServer { public class HttpListenerHost : IHttpServer, IDisposable { + /// + /// The settings key for a setting that specifies the default redirect path + /// to use for requests where the URL base prefix is invalid or missing. + /// + public const string DefaultRedirectKey = "HttpListenerHost:DefaultRedirectPath"; + private readonly ILogger _logger; private readonly IServerConfigurationManager _config; private readonly INetworkManager _networkManager; @@ -58,7 +64,7 @@ namespace Emby.Server.Implementations.HttpServer _appHost = applicationHost; _logger = logger; _config = config; - _defaultRedirectPath = configuration["HttpListenerHost:DefaultRedirectPath"]; + _defaultRedirectPath = configuration[DefaultRedirectKey]; _baseUrlPrefix = _config.Configuration.BaseUrl; _networkManager = networkManager; _jsonSerializer = jsonSerializer; diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index 4048a0d0310..7e03b0c9ca6 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -13,6 +13,7 @@ using System.Threading.Tasks; using CommandLine; using Emby.Drawing; using Emby.Server.Implementations; +using Emby.Server.Implementations.HttpServer; using Emby.Server.Implementations.IO; using Emby.Server.Implementations.Networking; using Jellyfin.Drawing.Skia; @@ -470,7 +471,7 @@ namespace Jellyfin.Server var inMemoryDefaultConfig = ConfigurationOptions.DefaultConfiguration; if (string.IsNullOrEmpty(appPaths.WebPath)) { - inMemoryDefaultConfig["HttpListenerHost:DefaultRedirectPath"] = "swagger/index.html"; + inMemoryDefaultConfig[HttpListenerHost.DefaultRedirectKey] = "swagger/index.html"; } return new ConfigurationBuilder() diff --git a/MediaBrowser.Providers/Music/MusicBrainzAlbumProvider.cs b/MediaBrowser.Providers/Music/MusicBrainzAlbumProvider.cs index 8e71b625ee0..ed7688e5a9e 100644 --- a/MediaBrowser.Providers/Music/MusicBrainzAlbumProvider.cs +++ b/MediaBrowser.Providers/Music/MusicBrainzAlbumProvider.cs @@ -22,6 +22,11 @@ namespace MediaBrowser.Providers.Music { public class MusicBrainzAlbumProvider : IRemoteMetadataProvider, IHasOrder { + /// + /// The settings key for a setting that specifies the base URL to use for sending requests to MusicBrainz. + /// + public const string BaseUrlKey = "MusicBrainz:BaseUrl"; + /// /// The Jellyfin user-agent is unrestricted but source IP must not exceed /// one request per second, therefore we rate limit to avoid throttling. @@ -57,7 +62,7 @@ namespace MediaBrowser.Providers.Music _appHost = appHost; _logger = logger; - _musicBrainzBaseUrl = configuration["MusicBrainz:BaseUrl"]; + _musicBrainzBaseUrl = configuration[BaseUrlKey]; // Use a stopwatch to ensure we don't exceed the MusicBrainz rate limit _stopWatchMusicBrainz.Start(); From e699e5d405792a598fdf6d0e723db52555af0492 Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Fri, 28 Feb 2020 20:34:10 +0100 Subject: [PATCH 10/40] Fix possible race condition --- .../Manager/ProviderManager.cs | 18 ++++++++---------- .../MediaBrowser.Providers.csproj | 1 + 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/MediaBrowser.Providers/Manager/ProviderManager.cs b/MediaBrowser.Providers/Manager/ProviderManager.cs index 4f1513df344..17d612199fe 100644 --- a/MediaBrowser.Providers/Manager/ProviderManager.cs +++ b/MediaBrowser.Providers/Manager/ProviderManager.cs @@ -970,20 +970,18 @@ namespace MediaBrowser.Providers.Manager var id = item.Id; _logger.LogInformation("OnRefreshProgress {0} {1}", id.ToString("N", CultureInfo.InvariantCulture), progress); - if (_activeRefreshes.TryAdd(id, progress)) - { - RefreshProgress?.Invoke(this, new GenericEventArgs>(new Tuple(item, progress))); - } - else - { - // TODO: Need to hunt down the conditions for this happening - throw new Exception( + // TODO: Need to hunt down the conditions for this happening + _activeRefreshes.AddOrUpdate( + id, + (_) => throw new Exception( string.Format( CultureInfo.InvariantCulture, "Refresh for item {0} {1} is not in progress", item.GetType().Name, - item.Id.ToString("N", CultureInfo.InvariantCulture))); - } + item.Id.ToString("N", CultureInfo.InvariantCulture))), + (_, _) => progress); + + RefreshProgress?.Invoke(this, new GenericEventArgs>(new Tuple(item, progress))); } private readonly SimplePriorityQueue> _refreshQueue = diff --git a/MediaBrowser.Providers/MediaBrowser.Providers.csproj b/MediaBrowser.Providers/MediaBrowser.Providers.csproj index d0ed61c8332..9c986382f2d 100644 --- a/MediaBrowser.Providers/MediaBrowser.Providers.csproj +++ b/MediaBrowser.Providers/MediaBrowser.Providers.csproj @@ -22,6 +22,7 @@ netstandard2.1 false true + preview From 3f4b9e9a81278c6f5c817036a7ae52b6f70557e4 Mon Sep 17 00:00:00 2001 From: Mark Monteiro Date: Fri, 28 Feb 2020 20:40:45 +0100 Subject: [PATCH 11/40] Add new 'nowebcontent' configuration flag --- .../ConfigurationOptions.cs | 1 + .../HttpServer/HttpListenerHost.cs | 2 +- .../Extensions/ConfigurationExtensions.cs | 31 +++++++++++++++++++ .../Music/MusicBrainzAlbumProvider.cs | 2 +- 4 files changed, 34 insertions(+), 2 deletions(-) diff --git a/Emby.Server.Implementations/ConfigurationOptions.cs b/Emby.Server.Implementations/ConfigurationOptions.cs index cc0d314edc6..08a493cb794 100644 --- a/Emby.Server.Implementations/ConfigurationOptions.cs +++ b/Emby.Server.Implementations/ConfigurationOptions.cs @@ -15,6 +15,7 @@ namespace Emby.Server.Implementations /// public static Dictionary DefaultConfiguration => new Dictionary { + { NoWebContentKey, bool.FalseString }, { HttpListenerHost.DefaultRedirectKey, "web/index.html" }, { MusicBrainzAlbumProvider.BaseUrlKey, "https://www.musicbrainz.org" }, { FfmpegProbeSizeKey, "1G" }, diff --git a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs index 7f16c683438..546a59517d3 100644 --- a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs +++ b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs @@ -31,7 +31,7 @@ namespace Emby.Server.Implementations.HttpServer public class HttpListenerHost : IHttpServer, IDisposable { /// - /// The settings key for a setting that specifies the default redirect path + /// The key for a setting that specifies the default redirect path /// to use for requests where the URL base prefix is invalid or missing. /// public const string DefaultRedirectKey = "HttpListenerHost:DefaultRedirectPath"; diff --git a/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs b/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs index 76c9b4b26c8..9dbc1a243f0 100644 --- a/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs +++ b/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs @@ -1,3 +1,4 @@ +using System; using Microsoft.Extensions.Configuration; namespace MediaBrowser.Controller.Extensions @@ -7,6 +8,11 @@ namespace MediaBrowser.Controller.Extensions /// public static class ConfigurationExtensions { + /// + /// The key for a setting that indicates whether the application should host static web content. + /// + public const string NoWebContentKey = "nowebcontent"; + /// /// The key for the FFmpeg probe size option. /// @@ -17,6 +23,16 @@ namespace MediaBrowser.Controller.Extensions /// public const string FfmpegAnalyzeDurationKey = "FFmpeg:analyzeduration"; + /// + /// Retrieves a config value indicating whether the application should not host + /// static web content from the . + /// + /// The configuration to retrieve the value from. + /// The parsed config value. + /// The config value is not a valid bool string. See . + public static bool IsNoWebContent(this IConfiguration configuration) + => configuration.ParseBoolean(NoWebContentKey); + /// /// Retrieves the FFmpeg probe size from the . /// @@ -32,5 +48,20 @@ namespace MediaBrowser.Controller.Extensions /// The FFmpeg analyse duration option. public static string GetFFmpegAnalyzeDuration(this IConfiguration configuration) => configuration[FfmpegAnalyzeDurationKey]; + + /// + /// Convert the specified configuration string value its equivalent. + /// + /// The configuration to retrieve and parse the setting from. + /// The key to use to retrieve the string value from the configuration. + /// The parsed boolean value. + /// The config value is not a valid bool string. See . + public static bool ParseBoolean(this IConfiguration configuration, string key) + { + string configValue = configuration[key]; + return bool.TryParse(configValue, out bool result) ? + result : + throw new FormatException($"Invalid value for configuration option '{key}' (expected a boolean): {configValue}"); + } } } diff --git a/MediaBrowser.Providers/Music/MusicBrainzAlbumProvider.cs b/MediaBrowser.Providers/Music/MusicBrainzAlbumProvider.cs index ed7688e5a9e..d217c6ad209 100644 --- a/MediaBrowser.Providers/Music/MusicBrainzAlbumProvider.cs +++ b/MediaBrowser.Providers/Music/MusicBrainzAlbumProvider.cs @@ -23,7 +23,7 @@ namespace MediaBrowser.Providers.Music public class MusicBrainzAlbumProvider : IRemoteMetadataProvider, IHasOrder { /// - /// The settings key for a setting that specifies the base URL to use for sending requests to MusicBrainz. + /// The key for a setting that specifies the base URL to use for sending requests to MusicBrainz. /// public const string BaseUrlKey = "MusicBrainz:BaseUrl"; From 29bad073ebeb10813c6468b6159ce3bd06398134 Mon Sep 17 00:00:00 2001 From: Mark Monteiro Date: Fri, 28 Feb 2020 20:49:04 +0100 Subject: [PATCH 12/40] Use config setting to decide if web content should be hosted Also fail server startup if web content is expected but missing --- .../ApplicationHost.cs | 5 +--- .../EntryPoints/StartupWizard.cs | 4 ++- Jellyfin.Server/Program.cs | 25 +++++++++++-------- .../IServerApplicationHost.cs | 5 ---- 4 files changed, 18 insertions(+), 21 deletions(-) diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 789b8724c3a..8ea188724a0 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -240,13 +240,10 @@ namespace Emby.Server.Implementations public int HttpsPort { get; private set; } /// - /// Gets the content root for the webhost. If the webhost is not serving static web content, this will be null. + /// Gets the content root for the webhost. /// public string ContentRoot { get; private set; } - /// - public bool IsHostingContent => ContentRoot != null; - /// /// Gets the server configuration manager. /// diff --git a/Emby.Server.Implementations/EntryPoints/StartupWizard.cs b/Emby.Server.Implementations/EntryPoints/StartupWizard.cs index 6b7e2805bd5..7c2b3cd5f25 100644 --- a/Emby.Server.Implementations/EntryPoints/StartupWizard.cs +++ b/Emby.Server.Implementations/EntryPoints/StartupWizard.cs @@ -2,7 +2,9 @@ using System.Threading.Tasks; using Emby.Server.Implementations.Browser; using MediaBrowser.Controller; using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Extensions; using MediaBrowser.Controller.Plugins; +using Microsoft.Extensions.Configuration; namespace Emby.Server.Implementations.EntryPoints { @@ -36,7 +38,7 @@ namespace Emby.Server.Implementations.EntryPoints return Task.CompletedTask; } - if (!_appHost.IsHostingContent) + if (_appHost.Resolve().IsNoWebContent()) { BrowserLauncher.OpenSwaggerPage(_appHost); } diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index 7e03b0c9ca6..843eb9ea94a 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -19,7 +19,7 @@ using Emby.Server.Implementations.Networking; using Jellyfin.Drawing.Skia; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Drawing; -using MediaBrowser.Model.Globalization; +using MediaBrowser.Controller.Extensions; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -177,7 +177,7 @@ namespace Jellyfin.Server ServiceCollection serviceCollection = new ServiceCollection(); await appHost.InitAsync(serviceCollection).ConfigureAwait(false); - var host = CreateWebHostBuilder(appHost, serviceCollection).Build(); + var host = CreateWebHostBuilder(appHost, serviceCollection, appConfig).Build(); // A bit hacky to re-use service provider since ASP.NET doesn't allow a custom service collection. appHost.ServiceProvider = host.Services; @@ -221,7 +221,7 @@ namespace Jellyfin.Server } } - private static IWebHostBuilder CreateWebHostBuilder(ApplicationHost appHost, IServiceCollection serviceCollection) + private static IWebHostBuilder CreateWebHostBuilder(ApplicationHost appHost, IServiceCollection serviceCollection, IConfiguration appConfig) { var webhostBuilder = new WebHostBuilder() .UseKestrel(options => @@ -268,9 +268,18 @@ namespace Jellyfin.Server }) .UseStartup(); - // Set the root directory for static content, if one exists - if (appHost.IsHostingContent) + if (!appConfig.IsNoWebContent()) { + // Fail startup if the web content does not exist + if (!Directory.Exists(appHost.ContentRoot) || !Directory.GetFiles(appHost.ContentRoot).Any()) + { + throw new InvalidOperationException( + "The server is expected to host web content, but the provided content directory is either " + + $"invalid or empty: {appHost.ContentRoot}. If you do not want to host web content with the " + + $"server, you may set the '{MediaBrowser.Controller.Extensions.ConfigurationExtensions.NoWebContentKey}' flag."); + } + + // Configure the web host to host the static web content webhostBuilder.UseContentRoot(appHost.ContentRoot); } @@ -404,12 +413,6 @@ namespace Jellyfin.Server } } - // Reset webDir if the directory does not exist, or is empty - if (!Directory.Exists(webDir) || !Directory.GetFiles(webDir).Any()) - { - webDir = null; - } - // logDir // IF --logdir // ELSE IF $JELLYFIN_LOG_DIR diff --git a/MediaBrowser.Controller/IServerApplicationHost.cs b/MediaBrowser.Controller/IServerApplicationHost.cs index 134ed110ba9..25f0905eb83 100644 --- a/MediaBrowser.Controller/IServerApplicationHost.cs +++ b/MediaBrowser.Controller/IServerApplicationHost.cs @@ -16,11 +16,6 @@ namespace MediaBrowser.Controller { event EventHandler HasUpdateAvailableChanged; - /// - /// Gets a value indicating whether the server is hosting the static web content from jellyfin-web. - /// - bool IsHostingContent { get; } - /// /// Gets the system info. /// From d437950ac30ee294ab275362abe711ae3c14ac32 Mon Sep 17 00:00:00 2001 From: Mark Monteiro Date: Wed, 11 Mar 2020 22:55:10 +0100 Subject: [PATCH 13/40] Parse config value correctly --- .../Extensions/ConfigurationExtensions.cs | 17 +---------------- .../MediaBrowser.Controller.csproj | 1 + 2 files changed, 2 insertions(+), 16 deletions(-) diff --git a/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs b/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs index 9dbc1a243f0..1a9ac09ee91 100644 --- a/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs +++ b/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs @@ -31,7 +31,7 @@ namespace MediaBrowser.Controller.Extensions /// The parsed config value. /// The config value is not a valid bool string. See . public static bool IsNoWebContent(this IConfiguration configuration) - => configuration.ParseBoolean(NoWebContentKey); + => configuration.GetValue(NoWebContentKey); /// /// Retrieves the FFmpeg probe size from the . @@ -48,20 +48,5 @@ namespace MediaBrowser.Controller.Extensions /// The FFmpeg analyse duration option. public static string GetFFmpegAnalyzeDuration(this IConfiguration configuration) => configuration[FfmpegAnalyzeDurationKey]; - - /// - /// Convert the specified configuration string value its equivalent. - /// - /// The configuration to retrieve and parse the setting from. - /// The key to use to retrieve the string value from the configuration. - /// The parsed boolean value. - /// The config value is not a valid bool string. See . - public static bool ParseBoolean(this IConfiguration configuration, string key) - { - string configValue = configuration[key]; - return bool.TryParse(configValue, out bool result) ? - result : - throw new FormatException($"Invalid value for configuration option '{key}' (expected a boolean): {configValue}"); - } } } diff --git a/MediaBrowser.Controller/MediaBrowser.Controller.csproj b/MediaBrowser.Controller/MediaBrowser.Controller.csproj index 88e9055e84e..bcca9e4a18a 100644 --- a/MediaBrowser.Controller/MediaBrowser.Controller.csproj +++ b/MediaBrowser.Controller/MediaBrowser.Controller.csproj @@ -9,6 +9,7 @@ + From 547f248a620c2de61d72703590f9c5c4720ba6e2 Mon Sep 17 00:00:00 2001 From: Mark Monteiro Date: Sun, 15 Mar 2020 12:59:34 +0100 Subject: [PATCH 14/40] Update XML documentation for WebPath --- .../AppBase/BaseApplicationPaths.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs b/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs index be2d198efcb..8bcd0874528 100644 --- a/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs +++ b/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs @@ -1,11 +1,13 @@ using System; using System.IO; using MediaBrowser.Common.Configuration; +using MediaBrowser.Controller.Extensions; +using Microsoft.Extensions.Configuration; namespace Emby.Server.Implementations.AppBase { /// - /// Provides a base class to hold common application paths used by both the Ui and Server. + /// Provides a base class to hold common application paths used by both the UI and Server. /// This can be subclassed to add application-specific paths. /// public abstract class BaseApplicationPaths : IApplicationPaths @@ -40,7 +42,10 @@ namespace Emby.Server.Implementations.AppBase /// /// Gets the path to the web UI resources folder. /// - /// The web UI resources path, or null if the server is not hosting any web content. + /// The web UI resources path. + /// + /// This value is not relevant if is true. + /// public string WebPath { get; } /// From 4102e3afd0a9df29faea598769db2212a00d64ce Mon Sep 17 00:00:00 2001 From: Mark Monteiro Date: Sun, 15 Mar 2020 13:00:14 +0100 Subject: [PATCH 15/40] Rename IsNoWebContent to NoWebContent --- Emby.Server.Implementations/EntryPoints/StartupWizard.cs | 2 +- Jellyfin.Server/Program.cs | 3 ++- MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Emby.Server.Implementations/EntryPoints/StartupWizard.cs b/Emby.Server.Implementations/EntryPoints/StartupWizard.cs index 7c2b3cd5f25..bc6b8c956ae 100644 --- a/Emby.Server.Implementations/EntryPoints/StartupWizard.cs +++ b/Emby.Server.Implementations/EntryPoints/StartupWizard.cs @@ -38,7 +38,7 @@ namespace Emby.Server.Implementations.EntryPoints return Task.CompletedTask; } - if (_appHost.Resolve().IsNoWebContent()) + if (_appHost.Resolve().NoWebContent()) { BrowserLauncher.OpenSwaggerPage(_appHost); } diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index 9450fee7056..6412db75187 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -287,7 +287,8 @@ namespace Jellyfin.Server }) .UseStartup(); - if (!startupConfig.IsNoWebContent()) + // Set up static content hosting unless it has been disabled via config + if (!startupConfig.NoWebContent()) { // Fail startup if the web content does not exist if (!Directory.Exists(appHost.ContentRoot) || !Directory.GetFiles(appHost.ContentRoot).Any()) diff --git a/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs b/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs index 1a9ac09ee91..e802eeed2ac 100644 --- a/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs +++ b/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs @@ -30,7 +30,7 @@ namespace MediaBrowser.Controller.Extensions /// The configuration to retrieve the value from. /// The parsed config value. /// The config value is not a valid bool string. See . - public static bool IsNoWebContent(this IConfiguration configuration) + public static bool NoWebContent(this IConfiguration configuration) => configuration.GetValue(NoWebContentKey); /// From a9c1ff91193d8f645225ee9f7001370fe304ae8d Mon Sep 17 00:00:00 2001 From: Mark Monteiro Date: Sun, 15 Mar 2020 13:00:34 +0100 Subject: [PATCH 16/40] Remove unnecessary method in BrowserLauncher --- .../Browser/BrowserLauncher.cs | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/Emby.Server.Implementations/Browser/BrowserLauncher.cs b/Emby.Server.Implementations/Browser/BrowserLauncher.cs index b17c2b27005..752650ae18d 100644 --- a/Emby.Server.Implementations/Browser/BrowserLauncher.cs +++ b/Emby.Server.Implementations/Browser/BrowserLauncher.cs @@ -8,25 +8,14 @@ namespace Emby.Server.Implementations.Browser /// public static class BrowserLauncher { - /// - /// Opens the dashboard page. - /// - /// The page. - /// The app host. - private static void OpenDashboardPage(string page, IServerApplicationHost appHost) - { - var url = appHost.GetLocalApiUrl("localhost") + "/web/" + page; - - OpenUrl(appHost, url); - } - /// /// Opens the web client. /// /// The app host. public static void OpenWebApp(IServerApplicationHost appHost) { - OpenDashboardPage("index.html", appHost); + var url = appHost.GetLocalApiUrl("localhost") + "/web/index.html"; + OpenUrl(appHost, url); } /// From 420e3619fb062727a914a4f73bf67edb9e9aaf6c Mon Sep 17 00:00:00 2001 From: Mark Monteiro Date: Sun, 15 Mar 2020 13:01:05 +0100 Subject: [PATCH 17/40] Use startup configuration to set the default redirect path --- Jellyfin.Server/Program.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index 6412db75187..168ee081eed 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -278,7 +278,7 @@ namespace Jellyfin.Server } } }) - .ConfigureAppConfiguration(config => config.ConfigureAppConfiguration(appPaths)) + .ConfigureAppConfiguration(config => config.ConfigureAppConfiguration(appPaths, startupConfig)) .UseSerilog() .ConfigureServices(services => { @@ -499,11 +499,11 @@ namespace Jellyfin.Server .Build(); } - private static IConfigurationBuilder ConfigureAppConfiguration(this IConfigurationBuilder config, IApplicationPaths appPaths) + private static IConfigurationBuilder ConfigureAppConfiguration(this IConfigurationBuilder config, IApplicationPaths appPaths, IConfiguration startupConfig = null) { // Use the swagger API page as the default redirect path if not hosting the jellyfin-web content var inMemoryDefaultConfig = ConfigurationOptions.DefaultConfiguration; - if (string.IsNullOrEmpty(appPaths.WebPath)) + if (startupConfig != null && startupConfig.NoWebContent()) { inMemoryDefaultConfig[HttpListenerHost.DefaultRedirectKey] = "swagger/index.html"; } From a67e32f8ecd46dfe2193ab3c81160bc830cd9349 Mon Sep 17 00:00:00 2001 From: Mark Monteiro Date: Sun, 15 Mar 2020 13:14:57 +0100 Subject: [PATCH 18/40] Clean up and document BrowserLauncher correctly --- .../Browser/BrowserLauncher.cs | 28 +++++++++---------- .../IServerApplicationHost.cs | 5 ++++ 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/Emby.Server.Implementations/Browser/BrowserLauncher.cs b/Emby.Server.Implementations/Browser/BrowserLauncher.cs index 752650ae18d..96096e142aa 100644 --- a/Emby.Server.Implementations/Browser/BrowserLauncher.cs +++ b/Emby.Server.Implementations/Browser/BrowserLauncher.cs @@ -1,21 +1,21 @@ using System; using MediaBrowser.Controller; +using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.Browser { /// - /// Class BrowserLauncher. + /// Assists in opening application URLs in an external browser. /// public static class BrowserLauncher { /// - /// Opens the web client. + /// Opens the home page of the web client. /// /// The app host. public static void OpenWebApp(IServerApplicationHost appHost) { - var url = appHost.GetLocalApiUrl("localhost") + "/web/index.html"; - OpenUrl(appHost, url); + TryOpenUrl(appHost, "/web/index.html"); } /// @@ -24,27 +24,25 @@ namespace Emby.Server.Implementations.Browser /// The app host. public static void OpenSwaggerPage(IServerApplicationHost appHost) { - var url = appHost.GetLocalApiUrl("localhost") + "/swagger/index.html"; - OpenUrl(appHost, url); + TryOpenUrl(appHost, "/swagger/index.html"); } /// - /// Opens the URL. + /// Opens the specified URL in an external browser window. Any exceptions will be logged, but ignored. /// - /// The application host instance. + /// The application host. /// The URL. - private static void OpenUrl(IServerApplicationHost appHost, string url) + private static void TryOpenUrl(IServerApplicationHost appHost, string url) { try { - appHost.LaunchUrl(url); + string baseUrl = appHost.GetLocalApiUrl("localhost"); + appHost.LaunchUrl(baseUrl + url); } - catch (NotSupportedException) - { - - } - catch (Exception) + catch (Exception ex) { + var logger = appHost.Resolve(); + logger?.LogError(ex, "Failed to open browser window with URL {URL}", url); } } } diff --git a/MediaBrowser.Controller/IServerApplicationHost.cs b/MediaBrowser.Controller/IServerApplicationHost.cs index 25f0905eb83..608ffc61c29 100644 --- a/MediaBrowser.Controller/IServerApplicationHost.cs +++ b/MediaBrowser.Controller/IServerApplicationHost.cs @@ -82,6 +82,11 @@ namespace MediaBrowser.Controller /// The local API URL. string GetLocalApiUrl(IPAddress address); + /// + /// Open a URL in an external browser window. + /// + /// The URL to open. + /// is false. void LaunchUrl(string url); void EnableLoopback(string appName); From 6a6293afc774001743985d615d651ca4a877bfd7 Mon Sep 17 00:00:00 2001 From: Mark Monteiro Date: Sun, 15 Mar 2020 13:15:47 +0100 Subject: [PATCH 19/40] Make startup config nullable when configuring app configuration --- Jellyfin.Server/Program.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index 168ee081eed..c839867f12d 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -499,7 +499,7 @@ namespace Jellyfin.Server .Build(); } - private static IConfigurationBuilder ConfigureAppConfiguration(this IConfigurationBuilder config, IApplicationPaths appPaths, IConfiguration startupConfig = null) + private static IConfigurationBuilder ConfigureAppConfiguration(this IConfigurationBuilder config, IApplicationPaths appPaths, IConfiguration? startupConfig = null) { // Use the swagger API page as the default redirect path if not hosting the jellyfin-web content var inMemoryDefaultConfig = ConfigurationOptions.DefaultConfiguration; From 6fbdf0d6a20d8bf230cbf2ba002612b49fa45b76 Mon Sep 17 00:00:00 2001 From: Mark Monteiro Date: Sun, 15 Mar 2020 15:23:50 +0100 Subject: [PATCH 20/40] Construct ApplicationHost with DI framework instead of manually --- .../ApplicationHost.cs | 32 ++++++++----------- .../HttpServer/HttpListenerHost.cs | 5 ++- Jellyfin.Server/Program.cs | 3 +- 3 files changed, 19 insertions(+), 21 deletions(-) diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 3d79cae1e56..7262268a972 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -613,7 +613,7 @@ namespace Emby.Server.Implementations DiscoverTypes(); - await RegisterResources(serviceCollection, startupConfig).ConfigureAwait(false); + await RegisterServices(serviceCollection, startupConfig).ConfigureAwait(false); ContentRoot = ServerConfigurationManager.Configuration.DashboardSourcePath; if (string.IsNullOrEmpty(ContentRoot)) @@ -650,9 +650,9 @@ namespace Emby.Server.Implementations } /// - /// Registers resources that classes will depend on + /// Registers services/resources with the service collection that will be available via DI. /// - protected async Task RegisterResources(IServiceCollection serviceCollection, IConfiguration startupConfig) + protected async Task RegisterServices(IServiceCollection serviceCollection, IConfiguration startupConfig) { serviceCollection.AddMemoryCache(); @@ -770,20 +770,8 @@ namespace Emby.Server.Implementations CertificateInfo = GetCertificateInfo(true); Certificate = GetCertificate(CertificateInfo); - HttpServer = new HttpListenerHost( - this, - LoggerFactory.CreateLogger(), - ServerConfigurationManager, - startupConfig, - NetworkManager, - JsonSerializer, - XmlSerializer, - CreateHttpListener()) - { - GlobalResponse = LocalizationManager.GetLocalizedString("StartupEmbyServerIsLoading") - }; - - serviceCollection.AddSingleton(HttpServer); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); ImageProcessor = new ImageProcessor(LoggerFactory.CreateLogger(), ServerConfigurationManager.ApplicationPaths, FileSystemManager, ImageEncoder, () => LibraryManager, () => MediaEncoder); serviceCollection.AddSingleton(ImageProcessor); @@ -891,6 +879,14 @@ namespace Emby.Server.Implementations ((LibraryManager)LibraryManager).ItemRepository = ItemRepository; } + /// + /// Create services registered with the service container that need to be initialized at application startup. + /// + public void InitializeServices() + { + HttpServer = Resolve(); + } + public static void LogEnvironmentInfo(ILogger logger, IApplicationPaths appPaths) { // Distinct these to prevent users from reporting problems that aren't actually problems @@ -1196,8 +1192,6 @@ namespace Emby.Server.Implementations }); } - protected IHttpListener CreateHttpListener() => new WebSocketSharpListener(LoggerFactory.CreateLogger()); - private CertificateInfo GetCertificateInfo(bool generateCertificate) { // Custom cert diff --git a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs index 546a59517d3..7633eec4eac 100644 --- a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs +++ b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs @@ -18,6 +18,7 @@ using MediaBrowser.Controller; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Net; using MediaBrowser.Model.Events; +using MediaBrowser.Model.Globalization; using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Services; using Microsoft.AspNetCore.Http; @@ -59,7 +60,8 @@ namespace Emby.Server.Implementations.HttpServer INetworkManager networkManager, IJsonSerializer jsonSerializer, IXmlSerializer xmlSerializer, - IHttpListener socketListener) + IHttpListener socketListener, + ILocalizationManager localizationManager) { _appHost = applicationHost; _logger = logger; @@ -76,6 +78,7 @@ namespace Emby.Server.Implementations.HttpServer Instance = this; ResponseFilters = Array.Empty>(); + GlobalResponse = localizationManager.GetLocalizedString("StartupEmbyServerIsLoading"); } public event EventHandler> WebSocketConnected; diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index c839867f12d..cb886b8ce07 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -191,8 +191,9 @@ namespace Jellyfin.Server var webHost = CreateWebHostBuilder(appHost, serviceCollection, startupConfig, appPaths).Build(); - // A bit hacky to re-use service provider since ASP.NET doesn't allow a custom service collection. + // Re-use the web host service provider in the app host since ASP.NET doesn't allow a custom service collection. appHost.ServiceProvider = webHost.Services; + appHost.InitializeServices(); appHost.FindParts(); Migrations.MigrationRunner.Run(appHost, _loggerFactory); From 7e3caec5836c0afeec58215ac33d625fa7f6fefa Mon Sep 17 00:00:00 2001 From: Mark Monteiro Date: Sun, 15 Mar 2020 15:29:39 +0100 Subject: [PATCH 21/40] Make PackageCreator.ModifyHtml() static and clean up XML documentation This eliminates the need to create a dummy instance to call this method --- .../Api/DashboardService.cs | 2 +- MediaBrowser.WebDashboard/Api/PackageCreator.cs | 16 +++++++++------- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/MediaBrowser.WebDashboard/Api/DashboardService.cs b/MediaBrowser.WebDashboard/Api/DashboardService.cs index 9e0aae7820a..83cc6c68209 100644 --- a/MediaBrowser.WebDashboard/Api/DashboardService.cs +++ b/MediaBrowser.WebDashboard/Api/DashboardService.cs @@ -203,7 +203,7 @@ namespace MediaBrowser.WebDashboard.Api return _resultFactory.GetStaticResult(Request, plugin.Version.ToString().GetMD5(), null, null, MimeTypes.GetMimeType("page.html"), () => Task.FromResult(stream)); } - return _resultFactory.GetStaticResult(Request, plugin.Version.ToString().GetMD5(), null, null, MimeTypes.GetMimeType("page.html"), () => GetPackageCreator(DashboardUIPath).ModifyHtml("dummy.html", stream, null, _appHost.ApplicationVersionString, null)); + return _resultFactory.GetStaticResult(Request, plugin.Version.ToString().GetMD5(), null, null, MimeTypes.GetMimeType("page.html"), () => PackageCreator.ModifyHtml(false, stream, null, _appHost.ApplicationVersionString, null)); } throw new ResourceNotFoundException(); diff --git a/MediaBrowser.WebDashboard/Api/PackageCreator.cs b/MediaBrowser.WebDashboard/Api/PackageCreator.cs index 133bf61e8ca..e8eb0389f96 100644 --- a/MediaBrowser.WebDashboard/Api/PackageCreator.cs +++ b/MediaBrowser.WebDashboard/Api/PackageCreator.cs @@ -28,7 +28,8 @@ namespace MediaBrowser.WebDashboard.Api if (resourceStream != null && IsCoreHtml(virtualPath)) { - resourceStream = await ModifyHtml(virtualPath, resourceStream, mode, appVersion, localizationCulture).ConfigureAwait(false); + bool isMainIndexPage = string.Equals(virtualPath, "index.html", StringComparison.OrdinalIgnoreCase); + resourceStream = await ModifyHtml(isMainIndexPage, resourceStream, mode, appVersion, localizationCulture).ConfigureAwait(false); } return resourceStream; @@ -45,18 +46,19 @@ namespace MediaBrowser.WebDashboard.Api } /// - /// Modifies the HTML by adding common meta tags, css and js. + /// Modifies the source HTML stream by adding common meta tags, css and js. /// - /// Task{Stream}. - public async Task ModifyHtml( - string path, + /// + /// A task that represents the async operation to read and modify the input stream. + /// The task result contains a stream containing the modified HTML content. + /// + public static async Task ModifyHtml( + bool isMainIndexPage, Stream sourceStream, string mode, string appVersion, string localizationCulture) { - var isMainIndexPage = string.Equals(path, "index.html", StringComparison.OrdinalIgnoreCase); - string html; using (var reader = new StreamReader(sourceStream, Encoding.UTF8)) { From 0996ce28987027afe5943ca9787d4a5695c2ee0a Mon Sep 17 00:00:00 2001 From: Mark Monteiro Date: Sun, 15 Mar 2020 15:31:43 +0100 Subject: [PATCH 22/40] Add --nowebcontent command line flag Added without any functionality --- Jellyfin.Server/StartupOptions.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Jellyfin.Server/StartupOptions.cs b/Jellyfin.Server/StartupOptions.cs index 1fb1c5af8e1..42c5fa86e39 100644 --- a/Jellyfin.Server/StartupOptions.cs +++ b/Jellyfin.Server/StartupOptions.cs @@ -15,6 +15,12 @@ namespace Jellyfin.Server [Option('d', "datadir", Required = false, HelpText = "Path to use for the data folder (database files, etc.).")] public string? DataDir { get; set; } + /// + /// Gets or sets a value indicating whether the server should not host static web content. + /// + [Option("nowebcontent", Required = false, HelpText = "Indicates that the web server should not host any static web content.")] + public bool NoWebContent { get; set; } + /// /// Gets or sets the path to the web directory. /// From 2a0153737131a404a8ccd386ad09ff4a1a697dfc Mon Sep 17 00:00:00 2001 From: Mark Monteiro Date: Sun, 15 Mar 2020 15:34:09 +0100 Subject: [PATCH 23/40] Merge command line options into framework configuration --- Jellyfin.Server/Program.cs | 23 +++++++++++++++-------- Jellyfin.Server/StartupOptions.cs | 13 +++++++++++++ 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index cb886b8ce07..bfb2fc6b57f 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -113,9 +113,10 @@ namespace Jellyfin.Server // $JELLYFIN_LOG_DIR needs to be set for the logger configuration manager Environment.SetEnvironmentVariable("JELLYFIN_LOG_DIR", appPaths.LogDirectoryPath); - // Create an instance of the application configuration to use for application startup await InitLoggingConfigFile(appPaths).ConfigureAwait(false); - IConfiguration startupConfig = CreateAppConfiguration(appPaths); + + // Create an instance of the application configuration to use for application startup + IConfiguration startupConfig = CreateAppConfiguration(options, appPaths); // Initialize logging framework InitializeLoggingFramework(startupConfig, appPaths); @@ -189,7 +190,7 @@ namespace Jellyfin.Server ServiceCollection serviceCollection = new ServiceCollection(); await appHost.InitAsync(serviceCollection, startupConfig).ConfigureAwait(false); - var webHost = CreateWebHostBuilder(appHost, serviceCollection, startupConfig, appPaths).Build(); + var webHost = CreateWebHostBuilder(appHost, serviceCollection, options, startupConfig, appPaths).Build(); // Re-use the web host service provider in the app host since ASP.NET doesn't allow a custom service collection. appHost.ServiceProvider = webHost.Services; @@ -238,6 +239,7 @@ namespace Jellyfin.Server private static IWebHostBuilder CreateWebHostBuilder( ApplicationHost appHost, IServiceCollection serviceCollection, + StartupOptions commandLineOpts, IConfiguration startupConfig, IApplicationPaths appPaths) { @@ -279,7 +281,7 @@ namespace Jellyfin.Server } } }) - .ConfigureAppConfiguration(config => config.ConfigureAppConfiguration(appPaths, startupConfig)) + .ConfigureAppConfiguration(config => config.ConfigureAppConfiguration(commandLineOpts, appPaths, startupConfig)) .UseSerilog() .ConfigureServices(services => { @@ -493,14 +495,18 @@ namespace Jellyfin.Server await resource.CopyToAsync(dst).ConfigureAwait(false); } - private static IConfiguration CreateAppConfiguration(IApplicationPaths appPaths) + private static IConfiguration CreateAppConfiguration(StartupOptions commandLineOpts, IApplicationPaths appPaths) { return new ConfigurationBuilder() - .ConfigureAppConfiguration(appPaths) + .ConfigureAppConfiguration(commandLineOpts, appPaths) .Build(); } - private static IConfigurationBuilder ConfigureAppConfiguration(this IConfigurationBuilder config, IApplicationPaths appPaths, IConfiguration? startupConfig = null) + private static IConfigurationBuilder ConfigureAppConfiguration( + this IConfigurationBuilder config, + StartupOptions commandLineOpts, + IApplicationPaths appPaths, + IConfiguration? startupConfig = null) { // Use the swagger API page as the default redirect path if not hosting the jellyfin-web content var inMemoryDefaultConfig = ConfigurationOptions.DefaultConfiguration; @@ -514,7 +520,8 @@ namespace Jellyfin.Server .AddInMemoryCollection(inMemoryDefaultConfig) .AddJsonFile(LoggingConfigFileDefault, optional: false, reloadOnChange: true) .AddJsonFile(LoggingConfigFileSystem, optional: true, reloadOnChange: true) - .AddEnvironmentVariables("JELLYFIN_"); + .AddEnvironmentVariables("JELLYFIN_") + .AddInMemoryCollection(commandLineOpts.ConvertToConfig()); } /// diff --git a/Jellyfin.Server/StartupOptions.cs b/Jellyfin.Server/StartupOptions.cs index 42c5fa86e39..0f15488f40d 100644 --- a/Jellyfin.Server/StartupOptions.cs +++ b/Jellyfin.Server/StartupOptions.cs @@ -1,5 +1,8 @@ +using System.Collections.Generic; +using System.Globalization; using CommandLine; using Emby.Server.Implementations; +using MediaBrowser.Controller.Extensions; namespace Jellyfin.Server { @@ -72,5 +75,15 @@ namespace Jellyfin.Server /// [Option("restartargs", Required = false, HelpText = "Arguments for restart script.")] public string? RestartArgs { get; set; } + + /// + /// Gets the command line options as a dictionary that can be used in the .NET configuration system. + /// + /// The configuration dictionary. + public Dictionary ConvertToConfig() + => new Dictionary + { + { ConfigurationExtensions.NoWebContentKey, NoWebContent.ToString(CultureInfo.InvariantCulture) } + }; } } From 602112fba433fa8919133d7ff292b0cd5a912aa2 Mon Sep 17 00:00:00 2001 From: Mark Monteiro Date: Sun, 15 Mar 2020 16:24:04 +0100 Subject: [PATCH 24/40] Return 404 for static web resources when the 'nowebcontent' flag is set --- .../Api/DashboardService.cs | 26 ++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/MediaBrowser.WebDashboard/Api/DashboardService.cs b/MediaBrowser.WebDashboard/Api/DashboardService.cs index 83cc6c68209..11901c25fb1 100644 --- a/MediaBrowser.WebDashboard/Api/DashboardService.cs +++ b/MediaBrowser.WebDashboard/Api/DashboardService.cs @@ -7,12 +7,14 @@ using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Plugins; using MediaBrowser.Controller; using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Extensions; using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Plugins; using MediaBrowser.Model.IO; using MediaBrowser.Model.Net; using MediaBrowser.Model.Plugins; using MediaBrowser.Model.Services; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; namespace MediaBrowser.WebDashboard.Api @@ -113,6 +115,7 @@ namespace MediaBrowser.WebDashboard.Api private readonly IFileSystem _fileSystem; private IResourceFileManager _resourceFileManager; + private readonly IConfiguration _appConfig; /// /// Initializes a new instance of the class. @@ -123,7 +126,8 @@ namespace MediaBrowser.WebDashboard.Api IServerConfigurationManager serverConfigurationManager, IFileSystem fileSystem, ILogger logger, - IHttpResultFactory resultFactory) + IHttpResultFactory resultFactory, + IConfiguration appConfig) { _appHost = appHost; _serverConfigurationManager = serverConfigurationManager; @@ -131,16 +135,22 @@ namespace MediaBrowser.WebDashboard.Api _logger = logger; _resultFactory = resultFactory; _resourceFileManager = resourceFileManager; + _appConfig = appConfig; } /// - /// Gets the path for the web interface. + /// Gets the path of the directory containing the static web interface content, or null if the server is not + /// hosting the static web content. /// - /// The path for the web interface. public string DashboardUIPath { get { + if (_appConfig.NoWebContent()) + { + return null; + } + if (!string.IsNullOrEmpty(_serverConfigurationManager.Configuration.DashboardSourcePath)) { return _serverConfigurationManager.Configuration.DashboardSourcePath; @@ -301,6 +311,11 @@ namespace MediaBrowser.WebDashboard.Api /// System.Object. public async Task Get(GetDashboardResource request) { + if (_appConfig.NoWebContent() || DashboardUIPath == null) + { + throw new ResourceNotFoundException(); + } + var path = request.ResourceName; var contentType = MimeTypes.GetMimeType(path); @@ -372,6 +387,11 @@ namespace MediaBrowser.WebDashboard.Api public async Task Get(GetDashboardPackage request) { + if (_appConfig.NoWebContent() || DashboardUIPath == null) + { + throw new ResourceNotFoundException(); + } + var mode = request.Mode; var inputPath = string.IsNullOrWhiteSpace(mode) ? From 05ab61200a181ff39034a5c3ba30a4ee2672971d Mon Sep 17 00:00:00 2001 From: Mark Monteiro Date: Sun, 15 Mar 2020 17:42:57 +0100 Subject: [PATCH 25/40] Do not call UseContentRoot() on the web host builder This call has nothing to do with static web content and should not have ever been called with the web content path: https://docs.microsoft.com/en-us/aspnet/core/fundamentals/?view=aspnetcore-3.1&tabs=windows#content-root Since this call is removed, the ContentRoot property is also removed from ApplicationHost as it is no longer needed. Finally, move validation of the static content path to the DashboardService where it is actually used. --- .../AppBase/BaseApplicationPaths.cs | 10 +--------- .../ApplicationHost.cs | 11 ---------- Jellyfin.Server/Program.cs | 20 +------------------ .../Configuration/IApplicationPaths.cs | 9 +++++++-- .../Api/DashboardService.cs | 11 ++++++++++ 5 files changed, 20 insertions(+), 41 deletions(-) diff --git a/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs b/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs index 8bcd0874528..bc478174387 100644 --- a/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs +++ b/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs @@ -1,8 +1,6 @@ using System; using System.IO; using MediaBrowser.Common.Configuration; -using MediaBrowser.Controller.Extensions; -using Microsoft.Extensions.Configuration; namespace Emby.Server.Implementations.AppBase { @@ -39,13 +37,7 @@ namespace Emby.Server.Implementations.AppBase /// The program data path. public string ProgramDataPath { get; } - /// - /// Gets the path to the web UI resources folder. - /// - /// The web UI resources path. - /// - /// This value is not relevant if is true. - /// + /// public string WebPath { get; } /// diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 7262268a972..52c51762714 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -236,11 +236,6 @@ namespace Emby.Server.Implementations /// public int HttpsPort { get; private set; } - /// - /// Gets the content root for the webhost. - /// - public string ContentRoot { get; private set; } - /// /// Gets the server configuration manager. /// @@ -614,12 +609,6 @@ namespace Emby.Server.Implementations DiscoverTypes(); await RegisterServices(serviceCollection, startupConfig).ConfigureAwait(false); - - ContentRoot = ServerConfigurationManager.Configuration.DashboardSourcePath; - if (string.IsNullOrEmpty(ContentRoot)) - { - ContentRoot = ServerConfigurationManager.ApplicationPaths.WebPath; - } } public async Task ExecuteWebsocketHandlerAsync(HttpContext context, Func next) diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index bfb2fc6b57f..67251eb2491 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -243,7 +243,7 @@ namespace Jellyfin.Server IConfiguration startupConfig, IApplicationPaths appPaths) { - var webhostBuilder = new WebHostBuilder() + return new WebHostBuilder() .UseKestrel(options => { var addresses = appHost.ServerConfigurationManager @@ -289,24 +289,6 @@ namespace Jellyfin.Server services.TryAdd(serviceCollection); }) .UseStartup(); - - // Set up static content hosting unless it has been disabled via config - if (!startupConfig.NoWebContent()) - { - // Fail startup if the web content does not exist - if (!Directory.Exists(appHost.ContentRoot) || !Directory.GetFiles(appHost.ContentRoot).Any()) - { - throw new InvalidOperationException( - "The server is expected to host web content, but the provided content directory is either " + - $"invalid or empty: {appHost.ContentRoot}. If you do not want to host web content with the " + - $"server, you may set the '{MediaBrowser.Controller.Extensions.ConfigurationExtensions.NoWebContentKey}' flag."); - } - - // Configure the web host to host the static web content - webhostBuilder.UseContentRoot(appHost.ContentRoot); - } - - return webhostBuilder; } /// diff --git a/MediaBrowser.Common/Configuration/IApplicationPaths.cs b/MediaBrowser.Common/Configuration/IApplicationPaths.cs index 5bdea7d8b30..870b90796cb 100644 --- a/MediaBrowser.Common/Configuration/IApplicationPaths.cs +++ b/MediaBrowser.Common/Configuration/IApplicationPaths.cs @@ -1,3 +1,5 @@ +using MediaBrowser.Model.Configuration; + namespace MediaBrowser.Common.Configuration { /// @@ -12,9 +14,12 @@ namespace MediaBrowser.Common.Configuration string ProgramDataPath { get; } /// - /// Gets the path to the web UI resources folder + /// Gets the path to the web UI resources folder. /// - /// The web UI resources path. + /// + /// This value is not relevant if the server is configured to not host any static web content. Additionally, + /// the value for takes precedence over this one. + /// string WebPath { get; } /// diff --git a/MediaBrowser.WebDashboard/Api/DashboardService.cs b/MediaBrowser.WebDashboard/Api/DashboardService.cs index 11901c25fb1..0114f5d6027 100644 --- a/MediaBrowser.WebDashboard/Api/DashboardService.cs +++ b/MediaBrowser.WebDashboard/Api/DashboardService.cs @@ -136,6 +136,17 @@ namespace MediaBrowser.WebDashboard.Api _resultFactory = resultFactory; _resourceFileManager = resourceFileManager; _appConfig = appConfig; + + // Validate web content path + string webContentPath = DashboardUIPath; + bool webContentPathValid = appConfig.NoWebContent() || (Directory.Exists(webContentPath) && Directory.GetFiles(webContentPath).Any()); + if (!webContentPathValid) + { + throw new InvalidOperationException( + "The server is expected to host web content, but the provided content directory is either " + + $"invalid or empty: {webContentPath}. If you do not want to host web content with the server, " + + $"you may set the '{Controller.Extensions.ConfigurationExtensions.NoWebContentKey}' flag."); + } } /// From 5d482590463232d1462b2f4738920bbd05c25165 Mon Sep 17 00:00:00 2001 From: Mark Monteiro Date: Sun, 15 Mar 2020 17:43:52 +0100 Subject: [PATCH 26/40] Use the same key constant for command line config and all other config --- Jellyfin.Server/StartupOptions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jellyfin.Server/StartupOptions.cs b/Jellyfin.Server/StartupOptions.cs index 0f15488f40d..ac5df3925ef 100644 --- a/Jellyfin.Server/StartupOptions.cs +++ b/Jellyfin.Server/StartupOptions.cs @@ -21,7 +21,7 @@ namespace Jellyfin.Server /// /// Gets or sets a value indicating whether the server should not host static web content. /// - [Option("nowebcontent", Required = false, HelpText = "Indicates that the web server should not host any static web content.")] + [Option(ConfigurationExtensions.NoWebContentKey, Required = false, HelpText = "Indicates that the web server should not host any static web content.")] public bool NoWebContent { get; set; } /// From 923313bb1cda2f265c6fa62f8b2b3f053cafd5a4 Mon Sep 17 00:00:00 2001 From: Mark Monteiro Date: Sun, 15 Mar 2020 17:49:13 +0100 Subject: [PATCH 27/40] Add debugging profile for running the server without web content --- Jellyfin.Server/Properties/launchSettings.json | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 Jellyfin.Server/Properties/launchSettings.json diff --git a/Jellyfin.Server/Properties/launchSettings.json b/Jellyfin.Server/Properties/launchSettings.json new file mode 100644 index 00000000000..d68a611c1bd --- /dev/null +++ b/Jellyfin.Server/Properties/launchSettings.json @@ -0,0 +1,11 @@ +{ + "profiles": { + "Jellyfin.Server": { + "commandName": "Project" + }, + "Jellyfin.Server (nowebcontent)": { + "commandName": "Project", + "commandLineArgs": "--nowebcontent" + } + } +} \ No newline at end of file From 7dd2f3a8f2dc6ea4db5ef9e1db4684b39cbcb6d3 Mon Sep 17 00:00:00 2001 From: Mark Monteiro Date: Tue, 17 Mar 2020 14:19:43 +0100 Subject: [PATCH 28/40] Do not add command line flag to the config hierarchy unless it is explicitly set --- Jellyfin.Server/StartupOptions.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/Jellyfin.Server/StartupOptions.cs b/Jellyfin.Server/StartupOptions.cs index ac5df3925ef..0abc0fd9115 100644 --- a/Jellyfin.Server/StartupOptions.cs +++ b/Jellyfin.Server/StartupOptions.cs @@ -81,9 +81,15 @@ namespace Jellyfin.Server /// /// The configuration dictionary. public Dictionary ConvertToConfig() - => new Dictionary + { + var config = new Dictionary(); + + if (NoWebContent) { - { ConfigurationExtensions.NoWebContentKey, NoWebContent.ToString(CultureInfo.InvariantCulture) } - }; + config.Add(ConfigurationExtensions.NoWebContentKey, bool.TrueString); + } + + return config; + } } } From 1a63c3f3644f8144ac7761724476891f4dc1d2cd Mon Sep 17 00:00:00 2001 From: Mark Monteiro Date: Fri, 20 Mar 2020 13:13:20 +0100 Subject: [PATCH 29/40] Inject IConfiguration instead of resolving manually --- Emby.Server.Implementations/EntryPoints/StartupWizard.cs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/Emby.Server.Implementations/EntryPoints/StartupWizard.cs b/Emby.Server.Implementations/EntryPoints/StartupWizard.cs index bc6b8c956ae..9a41396ce07 100644 --- a/Emby.Server.Implementations/EntryPoints/StartupWizard.cs +++ b/Emby.Server.Implementations/EntryPoints/StartupWizard.cs @@ -13,10 +13,8 @@ namespace Emby.Server.Implementations.EntryPoints /// public sealed class StartupWizard : IServerEntryPoint { - /// - /// The app host. - /// private readonly IServerApplicationHost _appHost; + private readonly IConfiguration _appConfig; private readonly IServerConfigurationManager _config; /// @@ -24,9 +22,10 @@ namespace Emby.Server.Implementations.EntryPoints /// /// The application host. /// The configuration manager. - public StartupWizard(IServerApplicationHost appHost, IServerConfigurationManager config) + public StartupWizard(IServerApplicationHost appHost, IConfiguration appConfig, IServerConfigurationManager config) { _appHost = appHost; + _appConfig = appConfig; _config = config; } @@ -38,7 +37,7 @@ namespace Emby.Server.Implementations.EntryPoints return Task.CompletedTask; } - if (_appHost.Resolve().NoWebContent()) + if (_appConfig.NoWebContent()) { BrowserLauncher.OpenSwaggerPage(_appHost); } From aa546dd36abb688cb3a5d10e589521ebf79ef610 Mon Sep 17 00:00:00 2001 From: Mark Monteiro Date: Sat, 21 Mar 2020 18:25:09 +0100 Subject: [PATCH 30/40] Rename command line option to --nowebclient and config setting to HostWebClient --- .../ConfigurationOptions.cs | 2 +- .../EntryPoints/StartupWizard.cs | 2 +- Jellyfin.Server/Program.cs | 4 +-- .../Properties/launchSettings.json | 6 ++--- Jellyfin.Server/StartupOptions.cs | 10 +++---- .../Extensions/ConfigurationExtensions.cs | 8 +++--- .../Api/DashboardService.cs | 27 ++++++++++--------- 7 files changed, 31 insertions(+), 28 deletions(-) diff --git a/Emby.Server.Implementations/ConfigurationOptions.cs b/Emby.Server.Implementations/ConfigurationOptions.cs index 814d4b8b57e..4574a64fdb4 100644 --- a/Emby.Server.Implementations/ConfigurationOptions.cs +++ b/Emby.Server.Implementations/ConfigurationOptions.cs @@ -15,7 +15,7 @@ namespace Emby.Server.Implementations /// public static Dictionary DefaultConfiguration => new Dictionary { - { NoWebContentKey, bool.FalseString }, + { HostWebClientKey, bool.TrueString }, { HttpListenerHost.DefaultRedirectKey, "web/index.html" }, { FfmpegProbeSizeKey, "1G" }, { FfmpegAnalyzeDurationKey, "200M" }, diff --git a/Emby.Server.Implementations/EntryPoints/StartupWizard.cs b/Emby.Server.Implementations/EntryPoints/StartupWizard.cs index 9a41396ce07..8e97719311f 100644 --- a/Emby.Server.Implementations/EntryPoints/StartupWizard.cs +++ b/Emby.Server.Implementations/EntryPoints/StartupWizard.cs @@ -37,7 +37,7 @@ namespace Emby.Server.Implementations.EntryPoints return Task.CompletedTask; } - if (_appConfig.NoWebContent()) + if (!_appConfig.HostWebClient()) { BrowserLauncher.OpenSwaggerPage(_appHost); } diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index 67251eb2491..d9ca1413639 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -490,9 +490,9 @@ namespace Jellyfin.Server IApplicationPaths appPaths, IConfiguration? startupConfig = null) { - // Use the swagger API page as the default redirect path if not hosting the jellyfin-web content + // Use the swagger API page as the default redirect path if not hosting the web client var inMemoryDefaultConfig = ConfigurationOptions.DefaultConfiguration; - if (startupConfig != null && startupConfig.NoWebContent()) + if (startupConfig != null && !startupConfig.HostWebClient()) { inMemoryDefaultConfig[HttpListenerHost.DefaultRedirectKey] = "swagger/index.html"; } diff --git a/Jellyfin.Server/Properties/launchSettings.json b/Jellyfin.Server/Properties/launchSettings.json index d68a611c1bd..53d9fe1656a 100644 --- a/Jellyfin.Server/Properties/launchSettings.json +++ b/Jellyfin.Server/Properties/launchSettings.json @@ -3,9 +3,9 @@ "Jellyfin.Server": { "commandName": "Project" }, - "Jellyfin.Server (nowebcontent)": { + "Jellyfin.Server (nowebclient)": { "commandName": "Project", - "commandLineArgs": "--nowebcontent" + "commandLineArgs": "--nowebclient" } } -} \ No newline at end of file +} diff --git a/Jellyfin.Server/StartupOptions.cs b/Jellyfin.Server/StartupOptions.cs index 0abc0fd9115..c93577d3ea7 100644 --- a/Jellyfin.Server/StartupOptions.cs +++ b/Jellyfin.Server/StartupOptions.cs @@ -19,10 +19,10 @@ namespace Jellyfin.Server public string? DataDir { get; set; } /// - /// Gets or sets a value indicating whether the server should not host static web content. + /// Gets or sets a value indicating whether the server should not host the web client. /// - [Option(ConfigurationExtensions.NoWebContentKey, Required = false, HelpText = "Indicates that the web server should not host any static web content.")] - public bool NoWebContent { get; set; } + [Option("nowebclient", Required = false, HelpText = "Indicates that the web server should not host the web client.")] + public bool NoWebClient { get; set; } /// /// Gets or sets the path to the web directory. @@ -84,9 +84,9 @@ namespace Jellyfin.Server { var config = new Dictionary(); - if (NoWebContent) + if (NoWebClient) { - config.Add(ConfigurationExtensions.NoWebContentKey, bool.TrueString); + config.Add(ConfigurationExtensions.HostWebClientKey, bool.FalseString); } return config; diff --git a/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs b/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs index 900cc6cb573..c9514998478 100644 --- a/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs +++ b/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs @@ -9,9 +9,9 @@ namespace MediaBrowser.Controller.Extensions public static class ConfigurationExtensions { /// - /// The key for a setting that indicates whether the application should host static web content. + /// The key for a setting that indicates whether the application should host web client content. /// - public const string NoWebContentKey = "nowebcontent"; + public const string HostWebClientKey = "hostwebclient"; /// /// The key for the FFmpeg probe size option. @@ -34,8 +34,8 @@ namespace MediaBrowser.Controller.Extensions /// The configuration to retrieve the value from. /// The parsed config value. /// The config value is not a valid bool string. See . - public static bool NoWebContent(this IConfiguration configuration) - => configuration.GetValue(NoWebContentKey); + public static bool HostWebClient(this IConfiguration configuration) + => configuration.GetValue(HostWebClientKey); /// /// Gets the FFmpeg probe size from the . diff --git a/MediaBrowser.WebDashboard/Api/DashboardService.cs b/MediaBrowser.WebDashboard/Api/DashboardService.cs index 3e47ce68292..a71d685fbbe 100644 --- a/MediaBrowser.WebDashboard/Api/DashboardService.cs +++ b/MediaBrowser.WebDashboard/Api/DashboardService.cs @@ -136,15 +136,18 @@ namespace MediaBrowser.WebDashboard.Api _fileSystem = fileSystem; _resultFactory = resultFactory; - // Validate web content path - string webContentPath = DashboardUIPath; - bool webContentPathValid = appConfig.NoWebContent() || (Directory.Exists(webContentPath) && Directory.GetFiles(webContentPath).Any()); - if (!webContentPathValid) + // If hosting the web client, validate the client content path + if (appConfig.HostWebClient()) { - throw new InvalidOperationException( - "The server is expected to host web content, but the provided content directory is either " + - $"invalid or empty: {webContentPath}. If you do not want to host web content with the server, " + - $"you may set the '{Controller.Extensions.ConfigurationExtensions.NoWebContentKey}' flag."); + string webContentPath = DashboardUIPath; + if (!Directory.Exists(webContentPath) || !Directory.GetFiles(webContentPath).Any()) + { + throw new InvalidOperationException( + "The server is expected to host the web client, but the provided content directory is either " + + $"invalid or empty: {webContentPath}. If you do not want to host the web client with the " + + "server, you may set the '--nowebclient' command line flag, or set" + + $"'{Controller.Extensions.ConfigurationExtensions.HostWebClientKey}=false' in your config settings."); + } } } @@ -156,13 +159,13 @@ namespace MediaBrowser.WebDashboard.Api /// /// Gets the path of the directory containing the static web interface content, or null if the server is not - /// hosting the static web content. + /// hosting the web client. /// public string DashboardUIPath { get { - if (_appConfig.NoWebContent()) + if (!_appConfig.HostWebClient()) { return null; } @@ -329,7 +332,7 @@ namespace MediaBrowser.WebDashboard.Api /// System.Object. public async Task Get(GetDashboardResource request) { - if (_appConfig.NoWebContent() || DashboardUIPath == null) + if (!_appConfig.HostWebClient() || DashboardUIPath == null) { throw new ResourceNotFoundException(); } @@ -405,7 +408,7 @@ namespace MediaBrowser.WebDashboard.Api public async Task Get(GetDashboardPackage request) { - if (_appConfig.NoWebContent() || DashboardUIPath == null) + if (!_appConfig.HostWebClient() || DashboardUIPath == null) { throw new ResourceNotFoundException(); } From 4bccaafb57abbc75858896374d42819f1c73fb20 Mon Sep 17 00:00:00 2001 From: Mark Monteiro Date: Tue, 24 Mar 2020 16:27:54 +0100 Subject: [PATCH 31/40] Use 'Length' instead of 'Any()' for an array --- MediaBrowser.WebDashboard/Api/DashboardService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MediaBrowser.WebDashboard/Api/DashboardService.cs b/MediaBrowser.WebDashboard/Api/DashboardService.cs index a71d685fbbe..1ca035f7bdf 100644 --- a/MediaBrowser.WebDashboard/Api/DashboardService.cs +++ b/MediaBrowser.WebDashboard/Api/DashboardService.cs @@ -140,7 +140,7 @@ namespace MediaBrowser.WebDashboard.Api if (appConfig.HostWebClient()) { string webContentPath = DashboardUIPath; - if (!Directory.Exists(webContentPath) || !Directory.GetFiles(webContentPath).Any()) + if (!Directory.Exists(webContentPath) || Directory.GetFiles(webContentPath).Length == 0) { throw new InvalidOperationException( "The server is expected to host the web client, but the provided content directory is either " + From ca85bef7c57108977d0f9813940f1ca942902d30 Mon Sep 17 00:00:00 2001 From: Mark Monteiro Date: Wed, 25 Mar 2020 18:52:14 +0100 Subject: [PATCH 32/40] Move check for web client directory to application startup in Program.cs --- Jellyfin.Server/Program.cs | 16 ++++++++++ .../Api/DashboardService.cs | 31 +++++++++++-------- 2 files changed, 34 insertions(+), 13 deletions(-) diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index d9ca1413639..2939d33782d 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -20,6 +20,7 @@ using Jellyfin.Drawing.Skia; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Extensions; +using MediaBrowser.WebDashboard.Api; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -185,8 +186,23 @@ namespace Jellyfin.Server new ManagedFileSystem(_loggerFactory.CreateLogger(), appPaths), GetImageEncoder(appPaths), new NetworkManager(_loggerFactory.CreateLogger())); + try { + // If hosting the web client, validate the client content path + if (startupConfig.HostWebClient()) + { + string webContentPath = DashboardService.GetDashboardUIPath(startupConfig, appHost.ServerConfigurationManager); + if (!Directory.Exists(webContentPath) || Directory.GetFiles(webContentPath).Length == 0) + { + throw new InvalidOperationException( + "The server is expected to host the web client, but the provided content directory is either " + + $"invalid or empty: {webContentPath}. If you do not want to host the web client with the " + + "server, you may set the '--nowebclient' command line flag, or set" + + $"'{MediaBrowser.Controller.Extensions.ConfigurationExtensions.HostWebClientKey}=false' in your config settings."); + } + } + ServiceCollection serviceCollection = new ServiceCollection(); await appHost.InitAsync(serviceCollection, startupConfig).ConfigureAwait(false); diff --git a/MediaBrowser.WebDashboard/Api/DashboardService.cs b/MediaBrowser.WebDashboard/Api/DashboardService.cs index 1ca035f7bdf..938ab513b5c 100644 --- a/MediaBrowser.WebDashboard/Api/DashboardService.cs +++ b/MediaBrowser.WebDashboard/Api/DashboardService.cs @@ -161,22 +161,27 @@ namespace MediaBrowser.WebDashboard.Api /// Gets the path of the directory containing the static web interface content, or null if the server is not /// hosting the web client. /// - public string DashboardUIPath + public string DashboardUIPath => GetDashboardUIPath(_appConfig, _serverConfigurationManager); + + /// + /// Gets the path of the directory containing the static web interface content. + /// + /// The app configuration. + /// The server configuration manager. + /// The directory path, or null if the server is not hosting the web client. + public static string GetDashboardUIPath(IConfiguration appConfig, IServerConfigurationManager serverConfigManager) { - get + if (!appConfig.HostWebClient()) { - if (!_appConfig.HostWebClient()) - { - return null; - } - - if (!string.IsNullOrEmpty(_serverConfigurationManager.Configuration.DashboardSourcePath)) - { - return _serverConfigurationManager.Configuration.DashboardSourcePath; - } - - return _serverConfigurationManager.ApplicationPaths.WebPath; + return null; } + + if (!string.IsNullOrEmpty(serverConfigManager.Configuration.DashboardSourcePath)) + { + return serverConfigManager.Configuration.DashboardSourcePath; + } + + return serverConfigManager.ApplicationPaths.WebPath; } [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")] From b10f268c43a6a0f49ded5e0175815d2c3f66631f Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Thu, 26 Mar 2020 17:15:35 +0000 Subject: [PATCH 33/40] Bump Microsoft.NET.Test.Sdk from 16.4.0 to 16.5.0 Bumps [Microsoft.NET.Test.Sdk](https://github.com/microsoft/vstest) from 16.4.0 to 16.5.0. - [Release notes](https://github.com/microsoft/vstest/releases) - [Commits](https://github.com/microsoft/vstest/compare/v16.4.0...v16.5.0) Signed-off-by: dependabot-preview[bot] --- tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj | 2 +- tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj | 2 +- .../Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj | 2 +- .../Jellyfin.MediaEncoding.Tests.csproj | 2 +- tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj index 77b0561efd5..bb0357c0678 100644 --- a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj +++ b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj @@ -12,7 +12,7 @@ - + diff --git a/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj b/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj index 86bb11bd4cb..c81b820d9a2 100644 --- a/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj +++ b/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj @@ -8,7 +8,7 @@ - + diff --git a/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj b/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj index c63f2e8c63b..06c10afe1bc 100644 --- a/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj +++ b/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj @@ -8,7 +8,7 @@ - + diff --git a/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj b/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj index b5e4a1287b3..52d28206d8a 100644 --- a/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj +++ b/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj @@ -14,7 +14,7 @@ - + diff --git a/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj b/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj index 9602d9e58ae..4a583bcc767 100644 --- a/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj +++ b/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj @@ -7,7 +7,7 @@ - + From 861bad1edaec10fa03d2f7cf12bb8b7b6ae1802c Mon Sep 17 00:00:00 2001 From: Mark Monteiro Date: Wed, 1 Apr 2020 13:26:47 +0200 Subject: [PATCH 34/40] Apply suggestions from code review --- .../Extensions/ConfigurationExtensions.cs | 2 +- MediaBrowser.WebDashboard/Api/DashboardService.cs | 14 -------------- 2 files changed, 1 insertion(+), 15 deletions(-) diff --git a/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs b/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs index c9514998478..c0043c0efab 100644 --- a/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs +++ b/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs @@ -29,7 +29,7 @@ namespace MediaBrowser.Controller.Extensions public const string PlaylistsAllowDuplicatesKey = "playlists:allowDuplicates"; /// - /// Gets a value indicating whether the application should not host static web content from the . + /// Gets a value indicating whether the application should host static web content from the . /// /// The configuration to retrieve the value from. /// The parsed config value. diff --git a/MediaBrowser.WebDashboard/Api/DashboardService.cs b/MediaBrowser.WebDashboard/Api/DashboardService.cs index 938ab513b5c..133a35527d7 100644 --- a/MediaBrowser.WebDashboard/Api/DashboardService.cs +++ b/MediaBrowser.WebDashboard/Api/DashboardService.cs @@ -135,20 +135,6 @@ namespace MediaBrowser.WebDashboard.Api _serverConfigurationManager = serverConfigurationManager; _fileSystem = fileSystem; _resultFactory = resultFactory; - - // If hosting the web client, validate the client content path - if (appConfig.HostWebClient()) - { - string webContentPath = DashboardUIPath; - if (!Directory.Exists(webContentPath) || Directory.GetFiles(webContentPath).Length == 0) - { - throw new InvalidOperationException( - "The server is expected to host the web client, but the provided content directory is either " + - $"invalid or empty: {webContentPath}. If you do not want to host the web client with the " + - "server, you may set the '--nowebclient' command line flag, or set" + - $"'{Controller.Extensions.ConfigurationExtensions.HostWebClientKey}=false' in your config settings."); - } - } } /// From 147e434634bda67ffeebd6db0790f5fd652b1fc9 Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Wed, 1 Apr 2020 14:50:18 +0200 Subject: [PATCH 35/40] Try to not crash on unsupported plugin load --- Emby.Server.Implementations/ApplicationHost.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index d6a572818a0..f3540e25eee 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -1167,7 +1167,7 @@ namespace Emby.Server.Implementations { exportedTypes = ass.GetExportedTypes(); } - catch (TypeLoadException ex) + catch (FileNotFoundException ex) { Logger.LogError(ex, "Error getting exported types from {Assembly}", ass.FullName); continue; From 3ab50f5a3ff1d1e081b12cf7226fa23771e2f552 Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Wed, 1 Apr 2020 19:05:41 +0200 Subject: [PATCH 36/40] Address comments --- .../Api/NotificationsService.cs | 1 - Emby.Notifications/CoreNotificationTypes.cs | 1 - .../NotificationConfigurationFactory.cs | 1 - .../LiveTv/EmbyTV/DirectRecorder.cs | 1 - .../LiveTv/EmbyTV/EncodedRecorder.cs | 1 - .../LiveTv/EmbyTV/EntryPoint.cs | 1 - .../LiveTv/EmbyTV/IRecorder.cs | 1 - .../LiveTv/EmbyTV/ItemDataProvider.cs | 1 - .../LiveTv/EmbyTV/RecordingHelper.cs | 1 - .../LiveTv/EmbyTV/SeriesTimerManager.cs | 1 - .../LiveTv/EmbyTV/TimerManager.cs | 1 - .../LiveTv/Listings/SchedulesDirect.cs | 1 - .../LiveTv/Listings/XmlTvListingsProvider.cs | 1 - .../LiveTv/LiveTvConfigurationFactory.cs | 1 - .../LiveTv/LiveTvDtoService.cs | 1 - .../LiveTv/LiveTvManager.cs | 1 - .../LiveTv/LiveTvMediaSourceProvider.cs | 1 - .../LiveTv/TunerHosts/BaseTunerHost.cs | 1 - .../TunerHosts/HdHomerun/HdHomerunHost.cs | 1 - .../TunerHosts/HdHomerun/HdHomerunManager.cs | 1 - .../HdHomerun/HdHomerunUdpStream.cs | 1 - .../LiveTv/TunerHosts/LiveStream.cs | 1 - .../LiveTv/TunerHosts/M3UTunerHost.cs | 1 - .../LiveTv/TunerHosts/M3uParser.cs | 1 - .../LiveTv/TunerHosts/SharedHttpStream.cs | 1 - .../MediaEncoder/EncodingManager.cs | 2 +- MediaBrowser.Controller/Entities/Folder.cs | 1 - .../Library/IMediaSourceProvider.cs | 1 - .../MediaEncoding/IEncodingManager.cs | 1 - .../Chapters/ChapterManager.cs | 1 - .../Manager/ProviderManager.cs | 5 ++--- .../MediaBrowser.Providers.csproj | 1 - .../MediaInfo/FFProbeVideoInfo.cs | 20 +++++++++---------- 33 files changed, 13 insertions(+), 44 deletions(-) diff --git a/Emby.Notifications/Api/NotificationsService.cs b/Emby.Notifications/Api/NotificationsService.cs index f2f38183815..67401c1f5bb 100644 --- a/Emby.Notifications/Api/NotificationsService.cs +++ b/Emby.Notifications/Api/NotificationsService.cs @@ -1,6 +1,5 @@ #pragma warning disable CS1591 #pragma warning disable SA1402 -#pragma warning disable SA1600 #pragma warning disable SA1649 using System; diff --git a/Emby.Notifications/CoreNotificationTypes.cs b/Emby.Notifications/CoreNotificationTypes.cs index 73e0b0256ac..a602b72213b 100644 --- a/Emby.Notifications/CoreNotificationTypes.cs +++ b/Emby.Notifications/CoreNotificationTypes.cs @@ -1,5 +1,4 @@ #pragma warning disable CS1591 -#pragma warning disable SA1600 using System; using System.Collections.Generic; diff --git a/Emby.Notifications/NotificationConfigurationFactory.cs b/Emby.Notifications/NotificationConfigurationFactory.cs index b168ed221b0..3fb3553d0ec 100644 --- a/Emby.Notifications/NotificationConfigurationFactory.cs +++ b/Emby.Notifications/NotificationConfigurationFactory.cs @@ -1,5 +1,4 @@ #pragma warning disable CS1591 -#pragma warning disable SA1600 using System.Collections.Generic; using MediaBrowser.Common.Configuration; diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs index 9c4f5fe3d84..e2bff97c813 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs @@ -1,5 +1,4 @@ #pragma warning disable CS1591 -#pragma warning disable SA1600 using System; using System.IO; diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs index 8590c56dfd4..d24fc679225 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs @@ -1,5 +1,4 @@ #pragma warning disable CS1591 -#pragma warning disable SA1600 using System; using System.Collections.Generic; diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EntryPoint.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EntryPoint.cs index a716b6240f4..69a9cb78aad 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/EntryPoint.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EntryPoint.cs @@ -1,5 +1,4 @@ #pragma warning disable CS1591 -#pragma warning disable SA1600 using System.Threading.Tasks; using MediaBrowser.Controller.Plugins; diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/IRecorder.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/IRecorder.cs index d6a1aee38b2..4712724d672 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/IRecorder.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/IRecorder.cs @@ -1,5 +1,4 @@ #pragma warning disable CS1591 -#pragma warning disable SA1600 using System; using System.Threading; diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs index 6d42a58f42d..fc543dc5515 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs @@ -1,5 +1,4 @@ #pragma warning disable CS1591 -#pragma warning disable SA1600 using System; using System.Collections.Generic; diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/RecordingHelper.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/RecordingHelper.cs index 4cb9f6fe88f..0b0ff6cb31f 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/RecordingHelper.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/RecordingHelper.cs @@ -1,5 +1,4 @@ #pragma warning disable CS1591 -#pragma warning disable SA1600 using System; using System.Globalization; diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/SeriesTimerManager.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/SeriesTimerManager.cs index 9cc53fddcd9..194e4606deb 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/SeriesTimerManager.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/SeriesTimerManager.cs @@ -1,5 +1,4 @@ #pragma warning disable CS1591 -#pragma warning disable SA1600 using System; using MediaBrowser.Controller.LiveTv; diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs index 330e881ef5d..7ebb043d8ec 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs @@ -1,5 +1,4 @@ #pragma warning disable CS1591 -#pragma warning disable SA1600 using System; using System.Collections.Concurrent; diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs index e9d3105bf10..00f469d8389 100644 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs +++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs @@ -1,5 +1,4 @@ #pragma warning disable CS1591 -#pragma warning disable SA1600 using System; using System.Collections.Concurrent; diff --git a/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs b/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs index c159b60a91b..609b397da80 100644 --- a/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs +++ b/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs @@ -1,5 +1,4 @@ #pragma warning disable CS1591 -#pragma warning disable SA1600 using System; using System.Collections.Generic; diff --git a/Emby.Server.Implementations/LiveTv/LiveTvConfigurationFactory.cs b/Emby.Server.Implementations/LiveTv/LiveTvConfigurationFactory.cs index 222fed9d92f..ba916af389e 100644 --- a/Emby.Server.Implementations/LiveTv/LiveTvConfigurationFactory.cs +++ b/Emby.Server.Implementations/LiveTv/LiveTvConfigurationFactory.cs @@ -1,5 +1,4 @@ #pragma warning disable CS1591 -#pragma warning disable SA1600 using System.Collections.Generic; using MediaBrowser.Common.Configuration; diff --git a/Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs b/Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs index 14b627f82ea..6e903a18eff 100644 --- a/Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs +++ b/Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs @@ -1,5 +1,4 @@ #pragma warning disable CS1591 -#pragma warning disable SA1600 using System; using System.Globalization; diff --git a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs index f20f6140e50..b64fe8634cf 100644 --- a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs +++ b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs @@ -1,5 +1,4 @@ #pragma warning disable CS1591 -#pragma warning disable SA1600 using System; using System.Collections.Generic; diff --git a/Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs b/Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs index 33887bbfd20..7f63991d0c4 100644 --- a/Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs +++ b/Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs @@ -1,5 +1,4 @@ #pragma warning disable CS1591 -#pragma warning disable SA1600 using System; using System.Collections.Generic; diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs index 419ec3635a9..80ee1ee33a6 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs @@ -1,5 +1,4 @@ #pragma warning disable CS1591 -#pragma warning disable SA1600 using System; using System.Collections.Concurrent; diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs index a2d972d19c0..25b2c674c58 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs @@ -1,5 +1,4 @@ #pragma warning disable CS1591 -#pragma warning disable SA1600 using System; using System.Collections.Generic; diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs index 56864ab116d..57c5b75002c 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs @@ -1,5 +1,4 @@ #pragma warning disable CS1591 -#pragma warning disable SA1600 using System; using System.Buffers; diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs index 77669da39f1..01f0dd885db 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs @@ -1,5 +1,4 @@ #pragma warning disable CS1591 -#pragma warning disable SA1600 using System; using System.Collections.Generic; diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs index 5354489f9cc..93bd8034fba 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs @@ -1,5 +1,4 @@ #pragma warning disable CS1591 -#pragma warning disable SA1600 using System; using System.Collections.Generic; diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs index 46c77e7b0ed..f5dda79db3b 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs @@ -1,5 +1,4 @@ #pragma warning disable CS1591 -#pragma warning disable SA1600 using System; using System.Collections.Generic; diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs index 511af150bb6..59451fccd24 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs @@ -1,5 +1,4 @@ #pragma warning disable CS1591 -#pragma warning disable SA1600 using System; using System.Collections.Generic; diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs index 86151838710..9ced65cca9e 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs @@ -1,5 +1,4 @@ #pragma warning disable CS1591 -#pragma warning disable SA1600 using System; using System.Collections.Generic; diff --git a/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs b/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs index 069c02b8544..677d68b4c9c 100644 --- a/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs +++ b/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs @@ -155,7 +155,7 @@ namespace Emby.Server.Implementations.MediaEncoder } catch (IOException ex) { - _logger.LogError(ex, "Error deleting {Path}", tempFile); + _logger.LogError(ex, "Error deleting temporary chapter image encoding file {Path}", tempFile); } chapter.ImagePath = path; diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs index 89ac8aea4b3..bb48605e553 100644 --- a/MediaBrowser.Controller/Entities/Folder.cs +++ b/MediaBrowser.Controller/Entities/Folder.cs @@ -1,5 +1,4 @@ #pragma warning disable CS1591 -#pragma warning disable SA1600 using System; using System.Collections.Generic; diff --git a/MediaBrowser.Controller/Library/IMediaSourceProvider.cs b/MediaBrowser.Controller/Library/IMediaSourceProvider.cs index ec7798551b8..5bf4acebb4b 100644 --- a/MediaBrowser.Controller/Library/IMediaSourceProvider.cs +++ b/MediaBrowser.Controller/Library/IMediaSourceProvider.cs @@ -1,5 +1,4 @@ #pragma warning disable CS1591 -#pragma warning disable SA1600 using System.Collections.Generic; using System.Threading; diff --git a/MediaBrowser.Controller/MediaEncoding/IEncodingManager.cs b/MediaBrowser.Controller/MediaEncoding/IEncodingManager.cs index 7063d39398d..15a2580afd8 100644 --- a/MediaBrowser.Controller/MediaEncoding/IEncodingManager.cs +++ b/MediaBrowser.Controller/MediaEncoding/IEncodingManager.cs @@ -1,5 +1,4 @@ #pragma warning disable CS1591 -#pragma warning disable SA1600 using System.Collections.Generic; using System.Threading; diff --git a/MediaBrowser.Providers/Chapters/ChapterManager.cs b/MediaBrowser.Providers/Chapters/ChapterManager.cs index 242fc84f6ea..3cbfe7d4d7a 100644 --- a/MediaBrowser.Providers/Chapters/ChapterManager.cs +++ b/MediaBrowser.Providers/Chapters/ChapterManager.cs @@ -1,5 +1,4 @@ #pragma warning disable CS1591 -#pragma warning disable SA1600 using System; using System.Collections.Generic; diff --git a/MediaBrowser.Providers/Manager/ProviderManager.cs b/MediaBrowser.Providers/Manager/ProviderManager.cs index 17d612199fe..7125f34c554 100644 --- a/MediaBrowser.Providers/Manager/ProviderManager.cs +++ b/MediaBrowser.Providers/Manager/ProviderManager.cs @@ -1,5 +1,4 @@ #pragma warning disable CS1591 -#pragma warning disable SA1600 using System; using System.Collections.Concurrent; @@ -976,10 +975,10 @@ namespace MediaBrowser.Providers.Manager (_) => throw new Exception( string.Format( CultureInfo.InvariantCulture, - "Refresh for item {0} {1} is not in progress", + "Cannot update refresh progress of item '{0}' ({1}) because a refresh for this item is not running", item.GetType().Name, item.Id.ToString("N", CultureInfo.InvariantCulture))), - (_, _) => progress); + (_, __) => progress); RefreshProgress?.Invoke(this, new GenericEventArgs>(new Tuple(item, progress))); } diff --git a/MediaBrowser.Providers/MediaBrowser.Providers.csproj b/MediaBrowser.Providers/MediaBrowser.Providers.csproj index 1d11a3cc216..330a4d1e534 100644 --- a/MediaBrowser.Providers/MediaBrowser.Providers.csproj +++ b/MediaBrowser.Providers/MediaBrowser.Providers.csproj @@ -22,7 +22,6 @@ netstandard2.1 false true - preview diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs index ea1db88ba4d..d2e98a5a94c 100644 --- a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs +++ b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs @@ -1,5 +1,4 @@ #pragma warning disable CS1591 -#pragma warning disable SA1600 using System; using System.Collections.Generic; @@ -47,9 +46,6 @@ namespace MediaBrowser.Providers.MediaInfo private readonly ILibraryManager _libraryManager; private readonly IMediaSourceManager _mediaSourceManager; - /// - /// The dummy chapter duration. - /// private readonly long _dummyChapterDuration = TimeSpan.FromMinutes(5).Ticks; public FFProbeVideoInfo( @@ -190,6 +186,7 @@ namespace MediaBrowser.Providers.MediaInfo { video.RunTimeTicks = mediaInfo.RunTimeTicks; } + video.Size = mediaInfo.Size; if (video.VideoType == VideoType.VideoFile) @@ -202,6 +199,7 @@ namespace MediaBrowser.Providers.MediaInfo { video.Container = null; } + video.Container = mediaInfo.Container; chapters = mediaInfo.Chapters == null ? Array.Empty() : mediaInfo.Chapters; @@ -246,7 +244,7 @@ namespace MediaBrowser.Providers.MediaInfo { if (chapters.Length == 0 && mediaStreams.Any(i => i.Type == MediaStreamType.Video)) { - CreateDummyChapters(video, ref chapters); + chapters = CreateDummyChapters(video); } NormalizeChapterNames(chapters); @@ -563,11 +561,11 @@ namespace MediaBrowser.Providers.MediaInfo } /// - /// Adds the dummy chapters. + /// Creates dummy chapters. /// /// The video. - /// The chapters. - private void CreateDummyChapters(Video video, ref ChapterInfo[] chapters) + /// An array of dummy chapters. + private ChapterInfo[] CreateDummyChapters(Video video) { var runtime = video.RunTimeTicks ?? 0; @@ -583,12 +581,12 @@ namespace MediaBrowser.Providers.MediaInfo if (runtime < _dummyChapterDuration) { - return; + return Array.Empty(); } // Limit to 100 chapters just in case there's some incorrect metadata here int chapterCount = (int)Math.Min(runtime / _dummyChapterDuration, 100); - chapters = new ChapterInfo[chapterCount]; + var chapters = new ChapterInfo[chapterCount]; long currentChapterTicks = 0; for (int i = 0; i < chapterCount; i++) @@ -600,6 +598,8 @@ namespace MediaBrowser.Providers.MediaInfo currentChapterTicks += _dummyChapterDuration; } + + return chapters; } private string[] FetchFromDvdLib(Video item) From 3c666aed4cfc622cf48d018c1854a7125cbda8ae Mon Sep 17 00:00:00 2001 From: Medzhnun Date: Wed, 1 Apr 2020 11:29:50 +0000 Subject: [PATCH 37/40] Translated using Weblate (Bulgarian) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/bg/ --- .../Localization/Core/bg-BG.json | 28 +++++++++++++++++-- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/bg-BG.json b/Emby.Server.Implementations/Localization/Core/bg-BG.json index 345f384605d..3fc7c7dc0fe 100644 --- a/Emby.Server.Implementations/Localization/Core/bg-BG.json +++ b/Emby.Server.Implementations/Localization/Core/bg-BG.json @@ -1,8 +1,8 @@ { "Albums": "Албуми", - "AppDeviceValues": "Програма: {0}, устройство: {1}", + "AppDeviceValues": "Програма: {0}, Устройство: {1}", "Application": "Програма", - "Artists": "Изпълнители", + "Artists": "Артисти", "AuthenticationSucceededWithUserName": "{0} се удостовери успешно", "Books": "Книги", "CameraImageUploadedFrom": "Нова снимка от камера беше качена от {0}", @@ -92,5 +92,27 @@ "UserStoppedPlayingItemWithValues": "{0} спря {1}", "ValueHasBeenAddedToLibrary": "{0} беше добавен във Вашата библиотека", "ValueSpecialEpisodeName": "Специални - {0}", - "VersionNumber": "Версия {0}" + "VersionNumber": "Версия {0}", + "TaskDownloadMissingSubtitlesDescription": "Търси Интернет за липсващи поднадписи, на база конфигурацията за мета-данни.", + "TaskDownloadMissingSubtitles": "Изтегляне на липсващи поднадписи", + "TaskRefreshChannelsDescription": "Обновява информацията за интернет канала.", + "TaskRefreshChannels": "Обновяване на Канали", + "TaskCleanTranscodeDescription": "Изтрива прекодирани файлове по-стари от един ден.", + "TaskCleanTranscode": "Изчиства директорията за прекодиране", + "TaskUpdatePluginsDescription": "Изтегля и инсталира актуализации за добавките, които са настроени за автоматична актуализация.", + "TaskUpdatePlugins": "Актуализира добавките", + "TaskRefreshPeopleDescription": "Актуализира мета-данните за артистите и режисьорите за Вашата медийна библиотека.", + "TaskRefreshPeople": "Обновяване на участниците", + "TaskCleanLogsDescription": "Изтрива лог файлове по-стари от {0} дни.", + "TaskCleanLogs": "Изчисти директорията с логове", + "TaskRefreshLibraryDescription": "Сканира Вашата библиотека с медия за нови файлове и обновява мета-данните.", + "TaskRefreshLibrary": "Сканиране на библиотеката с медия", + "TaskRefreshChapterImagesDescription": "Създава иконки за видеа, които имат епизоди.", + "TaskRefreshChapterImages": "Извличане на изображения за епизода", + "TaskCleanCacheDescription": "Изтриване на ненужните от системата файлове.", + "TaskCleanCache": "Изчистване на Кеш-директорията", + "TasksChannelsCategory": "Интернет Канали", + "TasksApplicationCategory": "Приложение", + "TasksLibraryCategory": "Библиотека", + "TasksMaintenanceCategory": "Поддръжка" } From ab7771941aac54fde51f4c9870849f35c8a02b9f Mon Sep 17 00:00:00 2001 From: Louis Hermier Date: Wed, 1 Apr 2020 16:59:12 +0000 Subject: [PATCH 38/40] Translated using Weblate (French) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/fr/ --- .../Localization/Core/fr.json | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/fr.json b/Emby.Server.Implementations/Localization/Core/fr.json index d93c803a352..88a7ac19064 100644 --- a/Emby.Server.Implementations/Localization/Core/fr.json +++ b/Emby.Server.Implementations/Localization/Core/fr.json @@ -92,5 +92,27 @@ "UserStoppedPlayingItemWithValues": "{0} vient d'arrêter la lecture de {1} sur {2}", "ValueHasBeenAddedToLibrary": "{0} a été ajouté à votre médiathèque", "ValueSpecialEpisodeName": "Spécial - {0}", - "VersionNumber": "Version {0}" + "VersionNumber": "Version {0}", + "TasksChannelsCategory": "Chaines en ligne", + "TaskDownloadMissingSubtitlesDescription": "Cherche les sous-titres manquant sur internet en se basant sur la configuration des métadonnées.", + "TaskDownloadMissingSubtitles": "Télécharge les sous-titres manquant", + "TaskRefreshChannelsDescription": "Rafraîchit les informations des chaines en ligne.", + "TaskRefreshChannels": "Rafraîchit les chaines", + "TaskCleanTranscodeDescription": "Supprime les fichiers transcodés de plus d'un jour.", + "TaskCleanTranscode": "Nettoie les dossier des transcodages", + "TaskUpdatePluginsDescription": "Télécharge et installe les mises à jours des plugins configurés pour être mis à jour automatiquement.", + "TaskUpdatePlugins": "Mettre à jour les plugins", + "TaskRefreshPeopleDescription": "Met à jour les métadonnées pour les acteurs et directeurs dans votre bibliothèque.", + "TaskRefreshPeople": "Rafraîchit les acteurs", + "TaskCleanLogsDescription": "Supprime les journaux de plus de {0} jours.", + "TaskCleanLogs": "Nettoie le répertoire des journaux", + "TaskRefreshLibraryDescription": "Scanne toute les bibliothèques pour trouver les nouveaux fichiers et rafraîchit les métadonnées.", + "TaskRefreshLibrary": "Scanne toute les Bibliothèques", + "TaskRefreshChapterImagesDescription": "Crée des images de miniature pour les vidéos ayant des chapitres.", + "TaskRefreshChapterImages": "Extrait les images de chapitre", + "TaskCleanCacheDescription": "Supprime les fichiers de cache dont le système n'a plus besoin.", + "TaskCleanCache": "Vider le répertoire cache", + "TasksApplicationCategory": "Application", + "TasksLibraryCategory": "Bibliothèque", + "TasksMaintenanceCategory": "Maintenance" } From cd5289efcf4877b50bdf0bb031573bf345edf193 Mon Sep 17 00:00:00 2001 From: MG Date: Wed, 1 Apr 2020 16:03:44 +0000 Subject: [PATCH 39/40] Translated using Weblate (Italian) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/it/ --- .../Localization/Core/it.json | 28 +++++++++++++++++-- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/it.json b/Emby.Server.Implementations/Localization/Core/it.json index b9348e058f5..0758bbe9ceb 100644 --- a/Emby.Server.Implementations/Localization/Core/it.json +++ b/Emby.Server.Implementations/Localization/Core/it.json @@ -5,7 +5,7 @@ "Artists": "Artisti", "AuthenticationSucceededWithUserName": "{0} autenticato con successo", "Books": "Libri", - "CameraImageUploadedFrom": "È stata caricata una nuova immagine della fotocamera da {0}", + "CameraImageUploadedFrom": "È stata caricata una nuova immagine della fotocamera dal device {0}", "Channels": "Canali", "ChapterNameValue": "Capitolo {0}", "Collections": "Collezioni", @@ -15,7 +15,7 @@ "Favorites": "Preferiti", "Folders": "Cartelle", "Genres": "Generi", - "HeaderAlbumArtists": "Artisti dell' Album", + "HeaderAlbumArtists": "Artisti degli Album", "HeaderCameraUploads": "Caricamenti Fotocamera", "HeaderContinueWatching": "Continua a guardare", "HeaderFavoriteAlbums": "Album Preferiti", @@ -92,5 +92,27 @@ "UserStoppedPlayingItemWithValues": "{0} ha interrotto la riproduzione di {1} su {2}", "ValueHasBeenAddedToLibrary": "{0} è stato aggiunto alla tua libreria multimediale", "ValueSpecialEpisodeName": "Speciale - {0}", - "VersionNumber": "Versione {0}" + "VersionNumber": "Versione {0}", + "TaskRefreshChannelsDescription": "Aggiorna le informazioni dei canali Internet.", + "TaskDownloadMissingSubtitlesDescription": "Cerca su internet i sottotitoli mancanti basandosi sulle configurazioni dei metadati.", + "TaskDownloadMissingSubtitles": "Scarica i sottotitoli mancanti", + "TaskRefreshChannels": "Aggiorna i canali", + "TaskCleanTranscodeDescription": "Cancella i file di transcode più vecchi di un giorno.", + "TaskCleanTranscode": "Svuota la cartella del transcoding", + "TaskUpdatePluginsDescription": "Scarica e installa gli aggiornamenti per i plugin che sono stati configurati per essere aggiornati contemporaneamente.", + "TaskUpdatePlugins": "Aggiorna i Plugin", + "TaskRefreshPeopleDescription": "Aggiorna i metadati per gli attori e registi nella tua libreria multimediale.", + "TaskRefreshPeople": "Aggiorna persone", + "TaskCleanLogsDescription": "Rimuovi i file di log più vecchi di {0} giorni.", + "TaskCleanLogs": "Pulisci la cartella dei log", + "TaskRefreshLibraryDescription": "Analizza la tua libreria multimediale per nuovi file e rinnova i metadati.", + "TaskRefreshLibrary": "Analizza la libreria dei contenuti multimediali", + "TaskRefreshChapterImagesDescription": "Crea le thumbnail per i video che hanno capitoli.", + "TaskRefreshChapterImages": "Estrai immagini capitolo", + "TaskCleanCacheDescription": "Cancella i file di cache non più necessari al sistema.", + "TaskCleanCache": "Pulisci la directory della cache", + "TasksChannelsCategory": "Canali su Internet", + "TasksApplicationCategory": "Applicazione", + "TasksLibraryCategory": "Libreria", + "TasksMaintenanceCategory": "Manutenzione" } From 5804873d3875e1034858d90e48d9a9865511c087 Mon Sep 17 00:00:00 2001 From: Vitorvlv Date: Wed, 1 Apr 2020 18:25:46 +0000 Subject: [PATCH 40/40] Translated using Weblate (Portuguese (Brazil)) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/pt_BR/ --- .../Localization/Core/pt-BR.json | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/pt-BR.json b/Emby.Server.Implementations/Localization/Core/pt-BR.json index 10ca4f9326d..3a69b6d7a5d 100644 --- a/Emby.Server.Implementations/Localization/Core/pt-BR.json +++ b/Emby.Server.Implementations/Localization/Core/pt-BR.json @@ -92,5 +92,27 @@ "UserStoppedPlayingItemWithValues": "{0} parou de reproduzir {1} em {2}", "ValueHasBeenAddedToLibrary": "{0} foi adicionado à sua biblioteca de mídia", "ValueSpecialEpisodeName": "Especial - {0}", - "VersionNumber": "Versão {0}" + "VersionNumber": "Versão {0}", + "TaskDownloadMissingSubtitlesDescription": "Procurar na internet por legendas faltando baseado na configuração de metadados.", + "TaskDownloadMissingSubtitles": "Baixar legendas que estão faltando", + "TaskRefreshChannelsDescription": "Atualizar informação de canais da internet .", + "TaskRefreshChannels": "Atualizar Canais", + "TaskCleanTranscodeDescription": "Deletar arquivos de transcodificação com mais de um dia de criação.", + "TaskCleanTranscode": "Limpar pasta de transcodificação", + "TaskUpdatePluginsDescription": "Baixa e instala atualizações para plugins que estão configurados para atualizar automaticamente.", + "TaskUpdatePlugins": "Atualizar Plugins", + "TaskRefreshPeopleDescription": "Atualiza metadados para atores e diretores na sua biblioteca de mídia.", + "TaskRefreshPeople": "Atualizar pessoas", + "TaskCleanLogsDescription": "Deletar arquivos temporários com mais de {0} dias.", + "TaskCleanLogs": "Limpar pasta de logs", + "TaskRefreshLibraryDescription": "Escaneie a sua biblioteca de mídia para arquivos novos e atualize os metadados.", + "TaskRefreshLibrary": "Escanear a Biblioteca de Mídia", + "TaskRefreshChapterImagesDescription": "Criar miniaturas para vídeos que tem capítulos.", + "TaskRefreshChapterImages": "Extrair imagens dos capítulos", + "TaskCleanCacheDescription": "Deletar arquivos temporários que não são mais necessários para o sistema.", + "TaskCleanCache": "Limpar Arquivos Temporários", + "TasksChannelsCategory": "Canais da Internet", + "TasksApplicationCategory": "Aplicativo", + "TasksLibraryCategory": "Biblioteca", + "TasksMaintenanceCategory": "Manutenção" }