diff options
| author | Bond_009 <bond.009@outlook.com> | 2020-05-02 00:54:04 +0200 |
|---|---|---|
| committer | Bond_009 <bond.009@outlook.com> | 2020-05-02 00:54:04 +0200 |
| commit | 15634a1913e8c1ad4e921364f30055794644d0bd (patch) | |
| tree | f0dfb3502728e4786de32c0cb4eaabd06f8e2d87 /MediaBrowser.Controller | |
| parent | 407f54e7764a6bfd8e4ccc0df897fac7e8c658b4 (diff) | |
| parent | 62e251663fce8216cea529f85382299ac2f39fbc (diff) | |
Merge branch 'master' into websocket
Diffstat (limited to 'MediaBrowser.Controller')
76 files changed, 812 insertions, 768 deletions
diff --git a/MediaBrowser.Controller/Authentication/AuthenticationException.cs b/MediaBrowser.Controller/Authentication/AuthenticationException.cs index 62eca3ea9f..081f877f72 100644 --- a/MediaBrowser.Controller/Authentication/AuthenticationException.cs +++ b/MediaBrowser.Controller/Authentication/AuthenticationException.cs @@ -7,23 +7,29 @@ namespace MediaBrowser.Controller.Authentication /// </summary> public class AuthenticationException : Exception { - /// <inheritdoc /> + /// <summary> + /// Initializes a new instance of the <see cref="AuthenticationException"/> class. + /// </summary> public AuthenticationException() : base() { - } - /// <inheritdoc /> + /// <summary> + /// Initializes a new instance of the <see cref="AuthenticationException"/> class. + /// </summary> + /// <param name="message">The message that describes the error.</param> public AuthenticationException(string message) : base(message) { - } - /// <inheritdoc /> + /// <summary> + /// Initializes a new instance of the <see cref="AuthenticationException"/> class. + /// </summary> + /// <param name="message">The message that describes the error.</param> + /// <param name="innerException">The exception that is the cause of the current exception, or a null reference if no inner exception is specified.</param> public AuthenticationException(string message, Exception innerException) : base(message, innerException) { - } } } diff --git a/MediaBrowser.Controller/Authentication/AuthenticationResult.cs b/MediaBrowser.Controller/Authentication/AuthenticationResult.cs index 5248ea4c13..4249a9a667 100644 --- a/MediaBrowser.Controller/Authentication/AuthenticationResult.cs +++ b/MediaBrowser.Controller/Authentication/AuthenticationResult.cs @@ -1,5 +1,4 @@ #pragma warning disable CS1591 -#pragma warning disable SA1600 using MediaBrowser.Controller.Session; using MediaBrowser.Model.Dto; diff --git a/MediaBrowser.Controller/Channels/IChannel.cs b/MediaBrowser.Controller/Channels/IChannel.cs index f8ed98a45d..c44e20d1ab 100644 --- a/MediaBrowser.Controller/Channels/IChannel.cs +++ b/MediaBrowser.Controller/Channels/IChannel.cs @@ -64,7 +64,7 @@ namespace MediaBrowser.Controller.Channels /// </summary> /// <param name="type">The type.</param> /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task{DynamicImageInfo}.</returns> + /// <returns>Task{DynamicImageResponse}.</returns> Task<DynamicImageResponse> GetChannelImage(ImageType type, CancellationToken cancellationToken); /// <summary> diff --git a/MediaBrowser.Controller/Chapters/IChapterManager.cs b/MediaBrowser.Controller/Chapters/IChapterManager.cs index d061898a14..f82e5b41a2 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 { /// <summary> - /// Interface IChapterManager + /// Interface IChapterManager. /// </summary> public interface IChapterManager { /// <summary> /// Saves the chapters. /// </summary> - void SaveChapters(string itemId, List<ChapterInfo> chapters); + void SaveChapters(Guid itemId, IReadOnlyList<ChapterInfo> chapters); } } diff --git a/MediaBrowser.Controller/Drawing/IImageProcessor.cs b/MediaBrowser.Controller/Drawing/IImageProcessor.cs index a58a11bd1f..36c746624e 100644 --- a/MediaBrowser.Controller/Drawing/IImageProcessor.cs +++ b/MediaBrowser.Controller/Drawing/IImageProcessor.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.IO; using System.Threading.Tasks; using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Drawing; using MediaBrowser.Model.Entities; @@ -21,19 +20,11 @@ namespace MediaBrowser.Controller.Drawing IReadOnlyCollection<string> SupportedInputFormats { get; } /// <summary> - /// Gets the image enhancers. - /// </summary> - /// <value>The image enhancers.</value> - IReadOnlyCollection<IImageEnhancer> ImageEnhancers { get; set; } - - /// <summary> /// Gets a value indicating whether [supports image collage creation]. /// </summary> /// <value><c>true</c> if [supports image collage creation]; otherwise, <c>false</c>.</value> bool SupportsImageCollageCreation { get; } - IImageEncoder ImageEncoder { get; set; } - /// <summary> /// Gets the dimensions of the image. /// </summary> @@ -50,23 +41,6 @@ namespace MediaBrowser.Controller.Drawing ImageDimensions GetImageDimensions(BaseItem item, ItemImageInfo info); /// <summary> - /// Gets the dimensions of the image. - /// </summary> - /// <param name="item">The base item.</param> - /// <param name="info">The information.</param> - /// <param name="updateItem">Whether or not the item info should be updated.</param> - /// <returns>ImageDimensions</returns> - ImageDimensions GetImageDimensions(BaseItem item, ItemImageInfo info, bool updateItem); - - /// <summary> - /// Gets the supported enhancers. - /// </summary> - /// <param name="item">The item.</param> - /// <param name="imageType">Type of the image.</param> - /// <returns>IEnumerable{IImageEnhancer}.</returns> - IEnumerable<IImageEnhancer> GetSupportedEnhancers(BaseItem item, ImageType imageType); - - /// <summary> /// Gets the image cache tag. /// </summary> /// <param name="item">The item.</param> @@ -76,15 +50,6 @@ namespace MediaBrowser.Controller.Drawing string GetImageCacheTag(BaseItem item, ChapterInfo info); /// <summary> - /// Gets the image cache tag. - /// </summary> - /// <param name="item">The item.</param> - /// <param name="image">The image.</param> - /// <param name="imageEnhancers">The image enhancers.</param> - /// <returns>Guid.</returns> - string GetImageCacheTag(BaseItem item, ItemImageInfo image, IReadOnlyCollection<IImageEnhancer> imageEnhancers); - - /// <summary> /// Processes the image. /// </summary> /// <param name="options">The options.</param> @@ -100,15 +65,6 @@ namespace MediaBrowser.Controller.Drawing Task<(string path, string mimeType, DateTime dateModified)> ProcessImage(ImageProcessingOptions options); /// <summary> - /// Gets the enhanced image. - /// </summary> - /// <param name="item">The item.</param> - /// <param name="imageType">Type of the image.</param> - /// <param name="imageIndex">Index of the image.</param> - /// <returns>Task{System.String}.</returns> - Task<string> GetEnhancedImage(BaseItem item, ImageType imageType, int imageIndex); - - /// <summary> /// Gets the supported image output formats. /// </summary> /// <returns><see cref="IReadOnlyCollection{ImageOutput}" />.</returns> diff --git a/MediaBrowser.Controller/Drawing/ImageHelper.cs b/MediaBrowser.Controller/Drawing/ImageHelper.cs index 432cf80423..d5a5f547ea 100644 --- a/MediaBrowser.Controller/Drawing/ImageHelper.cs +++ b/MediaBrowser.Controller/Drawing/ImageHelper.cs @@ -19,8 +19,6 @@ namespace MediaBrowser.Controller.Drawing return GetSizeEstimate(options); } - public static IImageProcessor ImageProcessor { get; set; } - private static ImageDimensions GetSizeEstimate(ImageProcessingOptions options) { if (options.Width.HasValue && options.Height.HasValue) diff --git a/MediaBrowser.Controller/Drawing/ImageProcessingOptions.cs b/MediaBrowser.Controller/Drawing/ImageProcessingOptions.cs index 29addf6e65..870e0278e4 100644 --- a/MediaBrowser.Controller/Drawing/ImageProcessingOptions.cs +++ b/MediaBrowser.Controller/Drawing/ImageProcessingOptions.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.IO; using System.Linq; using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Drawing; namespace MediaBrowser.Controller.Drawing @@ -34,8 +33,6 @@ namespace MediaBrowser.Controller.Drawing public int Quality { get; set; } - public IReadOnlyCollection<IImageEnhancer> Enhancers { get; set; } - public IReadOnlyCollection<ImageFormat> SupportedOutputFormats { get; set; } public bool AddPlayedIndicator { get; set; } diff --git a/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs b/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs index efe0d3cf70..5e3056ccb0 100644 --- a/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs +++ b/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs @@ -198,6 +198,7 @@ namespace MediaBrowser.Controller.Entities.Audio return true; } } + return base.RequiresRefresh(); } diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index 353c675cb3..7ed8fa7671 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -15,7 +15,6 @@ using MediaBrowser.Controller.Extensions; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Providers; -using MediaBrowser.Controller.Sorting; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; @@ -33,7 +32,7 @@ namespace MediaBrowser.Controller.Entities /// <summary> /// Class BaseItem /// </summary> - public abstract class BaseItem : IHasProviderIds, IHasLookupInfo<ItemLookupInfo> + public abstract class BaseItem : IHasProviderIds, IHasLookupInfo<ItemLookupInfo>, IEquatable<BaseItem> { /// <summary> /// The supported image extensions @@ -178,6 +177,7 @@ namespace MediaBrowser.Controller.Entities [JsonIgnore] public int? TotalBitrate { get; set; } + [JsonIgnore] public ExtraType? ExtraType { get; set; } @@ -387,15 +387,12 @@ namespace MediaBrowser.Controller.Entities while (thisMarker < s1.Length) { - if (thisMarker >= s1.Length) - { - break; - } char thisCh = s1[thisMarker]; var thisChunk = new StringBuilder(); + bool isNumeric = char.IsDigit(thisCh); - while ((thisMarker < s1.Length) && (thisChunk.Length == 0 || SortHelper.InChunk(thisCh, thisChunk[0]))) + while (thisMarker < s1.Length && char.IsDigit(thisCh) == isNumeric) { thisChunk.Append(thisCh); thisMarker++; @@ -406,7 +403,6 @@ namespace MediaBrowser.Controller.Entities } } - var isNumeric = thisChunk.Length > 0 && char.IsDigit(thisChunk[0]); list.Add(new Tuple<StringBuilder, bool>(thisChunk, isNumeric)); } @@ -553,7 +549,6 @@ namespace MediaBrowser.Controller.Entities [JsonIgnore] public DateTime DateModified { get; set; } - [JsonIgnore] public DateTime DateLastSaved { get; set; } [JsonIgnore] @@ -1330,8 +1325,9 @@ namespace MediaBrowser.Controller.Entities } // Use some hackery to get the extra type based on foldername - Enum.TryParse(extraFolderName.Replace(" ", ""), true, out ExtraType extraType); - item.ExtraType = extraType; + item.ExtraType = Enum.TryParse(extraFolderName.Replace(" ", string.Empty), true, out ExtraType extraType) + ? extraType + : Model.Entities.ExtraType.Unknown; return item; @@ -2194,13 +2190,9 @@ namespace MediaBrowser.Controller.Entities /// <summary> /// Do whatever refreshing is necessary when the filesystem pertaining to this item has changed. /// </summary> - /// <returns>Task.</returns> public virtual void ChangedExternally() { - ProviderManager.QueueRefresh(Id, new MetadataRefreshOptions(new DirectoryService(FileSystem)) - { - - }, RefreshPriority.High); + ProviderManager.QueueRefresh(Id, new MetadataRefreshOptions(new DirectoryService(FileSystem)), RefreshPriority.High); } /// <summary> @@ -2231,7 +2223,6 @@ namespace MediaBrowser.Controller.Entities existingImage.Width = image.Width; existingImage.Height = image.Height; } - else { var current = ImageInfos; @@ -2274,7 +2265,6 @@ namespace MediaBrowser.Controller.Entities /// </summary> /// <param name="type">The type.</param> /// <param name="index">The index.</param> - /// <returns>Task.</returns> public void DeleteImage(ImageType type, int index) { var info = GetImageInfo(type, index); @@ -2312,7 +2302,7 @@ namespace MediaBrowser.Controller.Entities } /// <summary> - /// Validates that images within the item are still on the file system + /// Validates that images within the item are still on the filesystem. /// </summary> public bool ValidateImages(IDirectoryService directoryService) { @@ -2606,7 +2596,7 @@ namespace MediaBrowser.Controller.Entities } /// <summary> - /// This is called before any metadata refresh and returns true or false indicating if changes were made + /// This is called before any metadata refresh and returns true if changes were made. /// </summary> public virtual bool BeforeMetadataRefresh(bool replaceAllMetdata) { @@ -2666,36 +2656,43 @@ namespace MediaBrowser.Controller.Entities newOptions.ForceSave = true; ownedItem.Genres = item.Genres; } + if (!item.Studios.SequenceEqual(ownedItem.Studios, StringComparer.Ordinal)) { newOptions.ForceSave = true; ownedItem.Studios = item.Studios; } + if (!item.ProductionLocations.SequenceEqual(ownedItem.ProductionLocations, StringComparer.Ordinal)) { newOptions.ForceSave = true; ownedItem.ProductionLocations = item.ProductionLocations; } + if (item.CommunityRating != ownedItem.CommunityRating) { ownedItem.CommunityRating = item.CommunityRating; newOptions.ForceSave = true; } + if (item.CriticRating != ownedItem.CriticRating) { ownedItem.CriticRating = item.CriticRating; newOptions.ForceSave = true; } + if (!string.Equals(item.Overview, ownedItem.Overview, StringComparison.Ordinal)) { ownedItem.Overview = item.Overview; newOptions.ForceSave = true; } + if (!string.Equals(item.OfficialRating, ownedItem.OfficialRating, StringComparison.Ordinal)) { ownedItem.OfficialRating = item.OfficialRating; newOptions.ForceSave = true; } + if (!string.Equals(item.CustomRating, ownedItem.CustomRating, StringComparison.Ordinal)) { ownedItem.CustomRating = item.CustomRating; @@ -2743,7 +2740,7 @@ namespace MediaBrowser.Controller.Entities { var list = GetEtagValues(user); - return string.Join("|", list.ToArray()).GetMD5().ToString("N", CultureInfo.InvariantCulture); + return string.Join("|", list).GetMD5().ToString("N", CultureInfo.InvariantCulture); } protected virtual List<string> GetEtagValues(User user) @@ -2786,8 +2783,7 @@ namespace MediaBrowser.Controller.Entities return true; } - var view = this as IHasCollectionType; - if (view != null) + if (this is IHasCollectionType view) { if (string.Equals(view.CollectionType, CollectionType.LiveTv, StringComparison.OrdinalIgnoreCase)) { @@ -2880,14 +2876,29 @@ namespace MediaBrowser.Controller.Entities /// <value>The remote trailers.</value> public IReadOnlyList<MediaUrl> RemoteTrailers { get; set; } + /// <summary> + /// Get all extras associated with this item, sorted by <see cref="SortName"/>. + /// </summary> + /// <returns>An enumerable containing the items.</returns> public IEnumerable<BaseItem> GetExtras() { - return ExtraIds.Select(LibraryManager.GetItemById).Where(i => i != null).OrderBy(i => i.SortName); + return ExtraIds + .Select(LibraryManager.GetItemById) + .Where(i => i != null) + .OrderBy(i => i.SortName); } + /// <summary> + /// Get all extras with specific types that are associated with this item. + /// </summary> + /// <param name="extraTypes">The types of extras to retrieve.</param> + /// <returns>An enumerable containing the extras.</returns> public IEnumerable<BaseItem> GetExtras(IReadOnlyCollection<ExtraType> extraTypes) { - return ExtraIds.Select(LibraryManager.GetItemById).Where(i => i != null && extraTypes.Contains(i.ExtraType.Value)); + return ExtraIds + .Select(LibraryManager.GetItemById) + .Where(i => i != null) + .Where(i => i.ExtraType.HasValue && extraTypes.Contains(i.ExtraType.Value)); } public IEnumerable<BaseItem> GetTrailers() @@ -2898,25 +2909,49 @@ namespace MediaBrowser.Controller.Entities return Array.Empty<BaseItem>(); } - public IEnumerable<BaseItem> GetDisplayExtras() - { - return GetExtras(DisplayExtraTypes); - } - public virtual bool IsHD => Height >= 720; + public bool IsShortcut { get; set; } + public string ShortcutPath { get; set; } + public int Width { get; set; } + public int Height { get; set; } + public Guid[] ExtraIds { get; set; } + public virtual long GetRunTimeTicksForPlayState() { return RunTimeTicks ?? 0; } - // Possible types of extra videos - public static readonly IReadOnlyCollection<ExtraType> DisplayExtraTypes = new[] { Model.Entities.ExtraType.BehindTheScenes, Model.Entities.ExtraType.Clip, Model.Entities.ExtraType.DeletedScene, Model.Entities.ExtraType.Interview, Model.Entities.ExtraType.Sample, Model.Entities.ExtraType.Scene }; + /// <summary> + /// Extra types that should be counted and displayed as "Special Features" in the UI. + /// </summary> + public static readonly IReadOnlyCollection<ExtraType> DisplayExtraTypes = new HashSet<ExtraType> + { + Model.Entities.ExtraType.Unknown, + Model.Entities.ExtraType.BehindTheScenes, + Model.Entities.ExtraType.Clip, + Model.Entities.ExtraType.DeletedScene, + Model.Entities.ExtraType.Interview, + Model.Entities.ExtraType.Sample, + Model.Entities.ExtraType.Scene + }; public virtual bool SupportsExternalTransfer => false; + + /// <inheritdoc /> + public override bool Equals(object obj) + { + return obj is BaseItem baseItem && this.Equals(baseItem); + } + + /// <inheritdoc /> + public bool Equals(BaseItem item) => Object.Equals(Id, item?.Id); + + /// <inheritdoc /> + public override int GetHashCode() => HashCode.Combine(Id); } } diff --git a/MediaBrowser.Controller/Entities/Book.cs b/MediaBrowser.Controller/Entities/Book.cs index 44c35374d7..dcad2554bd 100644 --- a/MediaBrowser.Controller/Entities/Book.cs +++ b/MediaBrowser.Controller/Entities/Book.cs @@ -13,8 +13,10 @@ namespace MediaBrowser.Controller.Entities [JsonIgnore] public string SeriesPresentationUniqueKey { get; set; } + [JsonIgnore] public string SeriesName { get; set; } + [JsonIgnore] public Guid SeriesId { get; set; } @@ -22,10 +24,12 @@ namespace MediaBrowser.Controller.Entities { return SeriesName; } + public string FindSeriesName() { return SeriesName; } + public string FindSeriesPresentationUniqueKey() { return SeriesPresentationUniqueKey; diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs index 07fbe60350..a468e0c35a 100644 --- a/MediaBrowser.Controller/Entities/Folder.cs +++ b/MediaBrowser.Controller/Entities/Folder.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Globalization; @@ -28,7 +30,6 @@ namespace MediaBrowser.Controller.Entities /// </summary> public class Folder : BaseItem { - public static IUserManager UserManager { get; set; } public static IUserViewManager UserViewManager { get; set; } /// <summary> @@ -322,10 +323,10 @@ namespace MediaBrowser.Controller.Entities ProviderManager.OnRefreshProgress(this, 5); } - //build a dictionary of the current children we have now by Id so we can compare quickly and easily + // Build a dictionary of the current children we have now by Id so we can compare quickly and easily var currentChildren = GetActualChildrenDictionary(); - //create a list for our validated children + // Create a list for our validated children var newItems = new List<BaseItem>(); cancellationToken.ThrowIfCancellationRequested(); @@ -391,7 +392,7 @@ namespace MediaBrowser.Controller.Entities var folder = this; innerProgress.RegisterAction(p => { - double newPct = .80 * p + 10; + double newPct = 0.80 * p + 10; progress.Report(newPct); ProviderManager.OnRefreshProgress(folder, newPct); }); @@ -421,7 +422,7 @@ namespace MediaBrowser.Controller.Entities var folder = this; innerProgress.RegisterAction(p => { - double newPct = .10 * p + 90; + double newPct = 0.10 * p + 90; progress.Report(newPct); if (recursive) { @@ -620,7 +621,6 @@ namespace MediaBrowser.Controller.Entities { EnableImages = false } - }).TotalRecordCount; } @@ -807,11 +807,45 @@ namespace MediaBrowser.Controller.Entities return false; } + private static BaseItem[] SortItemsByRequest(InternalItemsQuery query, IReadOnlyList<BaseItem> items) + { + var ids = query.ItemIds; + int size = items.Count; + + // ids can potentially contain non-unique guids, but query result cannot, + // so we include only first occurrence of each guid + var positions = new Dictionary<Guid, int>(size); + int index = 0; + for (int i = 0; i < ids.Length; i++) + { + if (positions.TryAdd(ids[i], index)) + { + index++; + } + } + + var newItems = new BaseItem[size]; + for (int i = 0; i < size; i++) + { + var item = items[i]; + newItems[positions[item.Id]] = item; + } + + return newItems; + } + public QueryResult<BaseItem> GetItems(InternalItemsQuery query) { if (query.ItemIds.Length > 0) { - return LibraryManager.GetItemsResult(query); + var result = LibraryManager.GetItemsResult(query); + + if (query.OrderBy.Count == 0 && query.ItemIds.Length > 1) + { + result.Items = SortItemsByRequest(query, result.Items); + } + + return result; } return GetItemsInternal(query); @@ -823,7 +857,14 @@ namespace MediaBrowser.Controller.Entities if (query.ItemIds.Length > 0) { - return LibraryManager.GetItemList(query); + var result = LibraryManager.GetItemList(query); + + if (query.OrderBy.Count == 0 && query.ItemIds.Length > 1) + { + return SortItemsByRequest(query, result); + } + + return result; } return GetItemsInternal(query).Items; @@ -1672,7 +1713,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 89b5dfee36..8ef5c8d961 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 { /// <summary> - /// Marker interface + /// Marker interface. /// </summary> public interface IItemByName { diff --git a/MediaBrowser.Controller/Entities/InternalPeopleQuery.cs b/MediaBrowser.Controller/Entities/InternalPeopleQuery.cs index 1613531b5c..011975dd2c 100644 --- a/MediaBrowser.Controller/Entities/InternalPeopleQuery.cs +++ b/MediaBrowser.Controller/Entities/InternalPeopleQuery.cs @@ -4,11 +4,21 @@ namespace MediaBrowser.Controller.Entities { public class InternalPeopleQuery { + /// <summary> + /// Gets or sets the maximum number of items the query should return. + /// </summary> + public int Limit { get; set; } + public Guid ItemId { get; set; } + public string[] PersonTypes { get; set; } + public string[] ExcludePersonTypes { get; set; } + public int? MaxListOrder { get; set; } + public Guid AppearsInItemId { get; set; } + public string NameContains { get; set; } public InternalPeopleQuery() diff --git a/MediaBrowser.Controller/Entities/Person.cs b/MediaBrowser.Controller/Entities/Person.cs index d9b4b22067..9e4f9d47ed 100644 --- a/MediaBrowser.Controller/Entities/Person.cs +++ b/MediaBrowser.Controller/Entities/Person.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Text.Json.Serialization; using MediaBrowser.Controller.Extensions; using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Entities; using Microsoft.Extensions.Logging; namespace MediaBrowser.Controller.Entities @@ -135,57 +134,4 @@ namespace MediaBrowser.Controller.Entities return hasChanges; } } - - /// <summary> - /// This is the small Person stub that is attached to BaseItems - /// </summary> - public class PersonInfo : IHasProviderIds - { - public PersonInfo() - { - ProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); - } - - public Guid ItemId { get; set; } - - /// <summary> - /// Gets or sets the name. - /// </summary> - /// <value>The name.</value> - public string Name { get; set; } - /// <summary> - /// Gets or sets the role. - /// </summary> - /// <value>The role.</value> - public string Role { get; set; } - /// <summary> - /// Gets or sets the type. - /// </summary> - /// <value>The type.</value> - public string Type { get; set; } - - /// <summary> - /// Gets or sets the sort order - ascending - /// </summary> - /// <value>The sort order.</value> - public int? SortOrder { get; set; } - - public string ImageUrl { get; set; } - - public Dictionary<string, string> ProviderIds { get; set; } - - /// <summary> - /// Returns a <see cref="string" /> that represents this instance. - /// </summary> - /// <returns>A <see cref="string" /> that represents this instance.</returns> - public override string ToString() - { - return Name; - } - - public bool IsType(string type) - { - return string.Equals(Type, type, StringComparison.OrdinalIgnoreCase) || string.Equals(Role, type, StringComparison.OrdinalIgnoreCase); - } - } } diff --git a/MediaBrowser.Controller/Entities/PersonInfo.cs b/MediaBrowser.Controller/Entities/PersonInfo.cs new file mode 100644 index 0000000000..f3ec73b322 --- /dev/null +++ b/MediaBrowser.Controller/Entities/PersonInfo.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; +using MediaBrowser.Model.Entities; + +namespace MediaBrowser.Controller.Entities +{ + /// <summary> + /// This is a small Person stub that is attached to BaseItems. + /// </summary> + public sealed class PersonInfo : IHasProviderIds + { + public PersonInfo() + { + ProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); + } + + public Guid ItemId { get; set; } + + /// <summary> + /// Gets or sets the name. + /// </summary> + /// <value>The name.</value> + public string Name { get; set; } + + /// <summary> + /// Gets or sets the role. + /// </summary> + /// <value>The role.</value> + public string Role { get; set; } + + /// <summary> + /// Gets or sets the type. + /// </summary> + /// <value>The type.</value> + public string Type { get; set; } + + /// <summary> + /// Gets or sets the ascending sort order. + /// </summary> + /// <value>The sort order.</value> + public int? SortOrder { get; set; } + + public string ImageUrl { get; set; } + + public Dictionary<string, string> ProviderIds { get; set; } + + /// <summary> + /// Returns a <see cref="string" /> that represents this instance. + /// </summary> + /// <returns>A <see cref="string" /> that represents this instance.</returns> + public override string ToString() + { + return Name; + } + + public bool IsType(string type) + { + return string.Equals(Type, type, StringComparison.OrdinalIgnoreCase) + || string.Equals(Role, type, StringComparison.OrdinalIgnoreCase); + } + } +} diff --git a/MediaBrowser.Controller/Entities/Photo.cs b/MediaBrowser.Controller/Entities/Photo.cs index 86d62add97..5ebc9f16ad 100644 --- a/MediaBrowser.Controller/Entities/Photo.cs +++ b/MediaBrowser.Controller/Entities/Photo.cs @@ -41,10 +41,10 @@ namespace MediaBrowser.Controller.Entities public override double GetDefaultPrimaryImageAspectRatio() { // REVIEW: @bond - if (Width.HasValue && Height.HasValue) + if (Width != 0 && Height != 0) { - double width = Width.Value; - double height = Height.Value; + double width = Width; + double height = Height; if (Orientation.HasValue) { @@ -67,8 +67,6 @@ namespace MediaBrowser.Controller.Entities return base.GetDefaultPrimaryImageAspectRatio(); } - public new int? Width { get; set; } - public new int? Height { get; set; } public string CameraMake { get; set; } public string CameraModel { get; set; } public string Software { get; set; } diff --git a/MediaBrowser.Controller/Entities/UserRootFolder.cs b/MediaBrowser.Controller/Entities/UserRootFolder.cs index 7fcf48a48f..8a68f830cc 100644 --- a/MediaBrowser.Controller/Entities/UserRootFolder.cs +++ b/MediaBrowser.Controller/Entities/UserRootFolder.cs @@ -60,14 +60,7 @@ namespace MediaBrowser.Controller.Entities PresetViews = query.PresetViews }); - var itemsArray = result; - var totalCount = itemsArray.Length; - - return new QueryResult<BaseItem> - { - TotalRecordCount = totalCount, - Items = itemsArray //TODO Fix The co-variant conversion between Folder[] and BaseItem[], this can generate runtime issues. - }; + return UserViewBuilder.SortAndPage(result, null, query, LibraryManager, true); } public override int GetChildCount(User user) diff --git a/MediaBrowser.Controller/Entities/Video.cs b/MediaBrowser.Controller/Entities/Video.cs index af4d227bc8..c3ea7f347a 100644 --- a/MediaBrowser.Controller/Entities/Video.cs +++ b/MediaBrowser.Controller/Entities/Video.cs @@ -7,7 +7,6 @@ using System.Threading; using System.Threading.Tasks; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.LiveTv; -using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Dto; diff --git a/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs b/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs index 76c9b4b26c..c0043c0efa 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 @@ -8,29 +9,56 @@ namespace MediaBrowser.Controller.Extensions public static class ConfigurationExtensions { /// <summary> + /// The key for a setting that indicates whether the application should host web client content. + /// </summary> + public const string HostWebClientKey = "hostwebclient"; + + /// <summary> /// The key for the FFmpeg probe size option. /// </summary> public const string FfmpegProbeSizeKey = "FFmpeg:probesize"; /// <summary> - /// The key for the FFmpeg analyse duration option. + /// The key for the FFmpeg analyze duration option. /// </summary> public const string FfmpegAnalyzeDurationKey = "FFmpeg:analyzeduration"; /// <summary> - /// Retrieves the FFmpeg probe size from the <see cref="IConfiguration" />. + /// The key for a setting that indicates whether playlists should allow duplicate entries. + /// </summary> + public const string PlaylistsAllowDuplicatesKey = "playlists:allowDuplicates"; + + /// <summary> + /// Gets a value indicating whether the application should host static web content from the <see cref="IConfiguration"/>. /// </summary> - /// <param name="configuration">This configuration.</param> + /// <param name="configuration">The configuration to retrieve the value from.</param> + /// <returns>The parsed config value.</returns> + /// <exception cref="FormatException">The config value is not a valid bool string. See <see cref="bool.Parse(string)"/>.</exception> + public static bool HostWebClient(this IConfiguration configuration) + => configuration.GetValue<bool>(HostWebClientKey); + + /// <summary> + /// Gets the FFmpeg probe size from the <see cref="IConfiguration" />. + /// </summary> + /// <param name="configuration">The configuration to read the setting from.</param> /// <returns>The FFmpeg probe size option.</returns> public static string GetFFmpegProbeSize(this IConfiguration configuration) => configuration[FfmpegProbeSizeKey]; /// <summary> - /// Retrieves the FFmpeg analyse duration from the <see cref="IConfiguration" />. + /// Gets the FFmpeg analyze duration from the <see cref="IConfiguration" />. /// </summary> - /// <param name="configuration">This configuration.</param> - /// <returns>The FFmpeg analyse duration option.</returns> + /// <param name="configuration">The configuration to read the setting from.</param> + /// <returns>The FFmpeg analyze duration option.</returns> public static string GetFFmpegAnalyzeDuration(this IConfiguration configuration) => configuration[FfmpegAnalyzeDurationKey]; + + /// <summary> + /// Gets a value indicating whether playlists should allow duplicate entries from the <see cref="IConfiguration"/>. + /// </summary> + /// <param name="configuration">The configuration to read the setting from.</param> + /// <returns>True if playlists should allow duplicates, otherwise false.</returns> + public static bool DoPlaylistsAllowDuplicates(this IConfiguration configuration) + => configuration.GetValue<bool>(PlaylistsAllowDuplicatesKey); } } diff --git a/MediaBrowser.Controller/IServerApplicationHost.cs b/MediaBrowser.Controller/IServerApplicationHost.cs index 0790b6cd60..b35e539abe 100644 --- a/MediaBrowser.Controller/IServerApplicationHost.cs +++ b/MediaBrowser.Controller/IServerApplicationHost.cs @@ -65,23 +65,32 @@ namespace MediaBrowser.Controller /// <summary> /// Gets the local API URL. /// </summary> + /// <param name="cancellationToken">Token to cancel the request if needed.</param> + /// <param name="forceHttp">Whether to force usage of plain HTTP protocol.</param> /// <value>The local API URL.</value> - Task<string> GetLocalApiUrl(CancellationToken cancellationToken); + Task<string> GetLocalApiUrl(CancellationToken cancellationToken, bool forceHttp = false); /// <summary> /// Gets the local API URL. /// </summary> /// <param name="hostname">The hostname.</param> + /// <param name="forceHttp">Whether to force usage of plain HTTP protocol.</param> /// <returns>The local API URL.</returns> - string GetLocalApiUrl(ReadOnlySpan<char> hostname); + string GetLocalApiUrl(ReadOnlySpan<char> hostname, bool forceHttp = false); /// <summary> /// Gets the local API URL. /// </summary> /// <param name="address">The IP address.</param> + /// <param name="forceHttp">Whether to force usage of plain HTTP protocol.</param> /// <returns>The local API URL.</returns> - string GetLocalApiUrl(IPAddress address); + string GetLocalApiUrl(IPAddress address, bool forceHttp = false); + /// <summary> + /// Open a URL in an external browser window. + /// </summary> + /// <param name="url">The URL to open.</param> + /// <exception cref="NotSupportedException"><see cref="CanLaunchWebBrowser"/> is false.</exception> void LaunchUrl(string url); void EnableLoopback(string appName); diff --git a/MediaBrowser.Controller/IServerApplicationPaths.cs b/MediaBrowser.Controller/IServerApplicationPaths.cs index 5d7c60910a..c35a22ac70 100644 --- a/MediaBrowser.Controller/IServerApplicationPaths.cs +++ b/MediaBrowser.Controller/IServerApplicationPaths.cs @@ -71,7 +71,12 @@ namespace MediaBrowser.Controller string UserConfigurationDirectoryPath { get; } /// <summary> - /// Gets the internal metadata path. + /// Gets the default internal metadata path. + /// </summary> + string DefaultInternalMetadataPath { get; } + + /// <summary> + /// Gets the internal metadata path, either a custom path or the default. /// </summary> /// <value>The internal metadata path.</value> string InternalMetadataPath { get; } diff --git a/MediaBrowser.Controller/Library/ILibraryManager.cs b/MediaBrowser.Controller/Library/ILibraryManager.cs index 511356aa4e..2e1c97f674 100644 --- a/MediaBrowser.Controller/Library/ILibraryManager.cs +++ b/MediaBrowser.Controller/Library/ILibraryManager.cs @@ -157,7 +157,8 @@ namespace MediaBrowser.Controller.Library /// <param name="introProviders">The intro providers.</param> /// <param name="itemComparers">The item comparers.</param> /// <param name="postscanTasks">The postscan tasks.</param> - void AddParts(IEnumerable<IResolverIgnoreRule> rules, + void AddParts( + IEnumerable<IResolverIgnoreRule> rules, IEnumerable<IItemResolver> resolvers, IEnumerable<IIntroProvider> introProviders, IEnumerable<IBaseItemComparer> itemComparers, @@ -349,9 +350,6 @@ namespace MediaBrowser.Controller.Library /// <returns><c>true</c> if [is audio file] [the specified path]; otherwise, <c>false</c>.</returns> bool IsAudioFile(string path); - bool IsAudioFile(string path, LibraryOptions libraryOptions); - bool IsVideoFile(string path, LibraryOptions libraryOptions); - /// <summary> /// Gets the season number from path. /// </summary> diff --git a/MediaBrowser.Controller/Library/IMediaSourceProvider.cs b/MediaBrowser.Controller/Library/IMediaSourceProvider.cs index 9e74879fc3..5bf4acebb4 100644 --- a/MediaBrowser.Controller/Library/IMediaSourceProvider.cs +++ b/MediaBrowser.Controller/Library/IMediaSourceProvider.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; diff --git a/MediaBrowser.Controller/Library/NameExtensions.cs b/MediaBrowser.Controller/Library/NameExtensions.cs index 6b0b7e53ac..24d0347e9d 100644 --- a/MediaBrowser.Controller/Library/NameExtensions.cs +++ b/MediaBrowser.Controller/Library/NameExtensions.cs @@ -1,6 +1,6 @@ using System; -using System.Linq; using System.Collections.Generic; +using System.Linq; using MediaBrowser.Controller.Extensions; namespace MediaBrowser.Controller.Library diff --git a/MediaBrowser.Controller/Library/Profiler.cs b/MediaBrowser.Controller/Library/Profiler.cs index 9fe175a7c4..46a97d181f 100644 --- a/MediaBrowser.Controller/Library/Profiler.cs +++ b/MediaBrowser.Controller/Library/Profiler.cs @@ -28,7 +28,7 @@ namespace MediaBrowser.Controller.Library /// </summary> /// <param name="name">The name.</param> /// <param name="logger">The logger.</param> - public Profiler(string name, ILogger logger) + public Profiler(string name, ILogger<Profiler> logger) { this._name = name; diff --git a/MediaBrowser.Controller/MediaBrowser.Controller.csproj b/MediaBrowser.Controller/MediaBrowser.Controller.csproj index 60c76ef7db..4e7d027374 100644 --- a/MediaBrowser.Controller/MediaBrowser.Controller.csproj +++ b/MediaBrowser.Controller/MediaBrowser.Controller.csproj @@ -1,5 +1,10 @@ <Project Sdk="Microsoft.NET.Sdk"> + <!-- ProjectGuid is only included as a requirement for SonarQube analysis --> + <PropertyGroup> + <ProjectGuid>{17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}</ProjectGuid> + </PropertyGroup> + <PropertyGroup> <Authors>Jellyfin Contributors</Authors> <PackageId>Jellyfin.Controller</PackageId> @@ -8,7 +13,8 @@ </PropertyGroup> <ItemGroup> - <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.0.0" /> + <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.3" /> + <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="3.1.3" /> </ItemGroup> <ItemGroup> @@ -26,4 +32,16 @@ <GenerateDocumentationFile>true</GenerateDocumentationFile> </PropertyGroup> + <!-- Code Analyzers--> + <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> + <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" /> + <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> + <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" /> + <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> + </ItemGroup> + + <PropertyGroup Condition=" '$(Configuration)' == 'Debug' "> + <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet> + </PropertyGroup> + </Project> diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 020f0553ed..61a3306756 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -78,8 +78,7 @@ namespace MediaBrowser.Controller.MediaEncoding if (!string.IsNullOrEmpty(hwType) && encodingOptions.EnableHardwareEncoding - && codecMap.ContainsKey(hwType) - && CheckVaapi(state, hwType, encodingOptions)) + && codecMap.ContainsKey(hwType)) { var preferredEncoder = codecMap[hwType]; @@ -93,23 +92,6 @@ namespace MediaBrowser.Controller.MediaEncoding return defaultEncoder; } - private bool CheckVaapi(EncodingJobInfo state, string hwType, EncodingOptions encodingOptions) - { - if (!string.Equals(hwType, "vaapi", StringComparison.OrdinalIgnoreCase)) - { - // No vaapi requested, return OK. - return true; - } - - if (string.IsNullOrEmpty(encodingOptions.VaapiDevice)) - { - // No device specified, return OK. - return true; - } - - return IsVaapiSupported(state); - } - private bool IsVaapiSupported(EncodingJobInfo state) { var videoStream = state.VideoStream; @@ -424,7 +406,13 @@ namespace MediaBrowser.Controller.MediaEncoding if (string.Equals(codec, "aac", StringComparison.OrdinalIgnoreCase)) { - return "aac -strict experimental"; + // Use libfdk_aac for better audio quality if using custom build of FFmpeg which has fdk_aac support + if (_mediaEncoder.SupportsEncoder("libfdk_aac")) + { + return "libfdk_aac"; + } + + return "aac"; } if (string.Equals(codec, "mp3", StringComparison.OrdinalIgnoreCase)) @@ -460,40 +448,37 @@ namespace MediaBrowser.Controller.MediaEncoding if (state.IsVideoRequest && string.Equals(encodingOptions.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase)) { - var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; - var hwOutputFormat = "vaapi"; - - if (hasGraphicalSubs) - { - hwOutputFormat = "yuv420p"; - } - - arg.Append("-hwaccel vaapi -hwaccel_output_format ") - .Append(hwOutputFormat) + arg.Append("-hwaccel vaapi -hwaccel_output_format vaapi") .Append(" -vaapi_device ") .Append(encodingOptions.VaapiDevice) .Append(' '); } - if (state.IsVideoRequest + if (state.IsVideoRequest && string.Equals(encodingOptions.HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase)) { var videoDecoder = GetHardwareAcceleratedVideoDecoder(state, encodingOptions); var outputVideoCodec = GetVideoEncoder(state, encodingOptions); - if (encodingOptions.EnableHardwareEncoding && outputVideoCodec.Contains("qsv", StringComparison.OrdinalIgnoreCase)) + var hasTextSubs = state.SubtitleStream != null && state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; + + if (!hasTextSubs) { - if (!string.IsNullOrEmpty(videoDecoder) && videoDecoder.Contains("qsv", StringComparison.OrdinalIgnoreCase)) + // While using QSV encoder + if ((outputVideoCodec ?? string.Empty).IndexOf("qsv", StringComparison.OrdinalIgnoreCase) != -1) { - arg.Append("-hwaccel qsv "); - } - else - { - arg.Append("-init_hw_device qsv=hw -filter_hw_device hw "); - } + // While using QSV decoder + if ((videoDecoder ?? string.Empty).IndexOf("qsv", StringComparison.OrdinalIgnoreCase) != -1) + { + arg.Append("-hwaccel qsv "); + } + // While using SW decoder + else + { + arg.Append("-init_hw_device qsv=hw -filter_hw_device hw "); + } + } } - - arg.Append(videoDecoder + " "); } arg.Append("-i ") @@ -503,17 +488,6 @@ namespace MediaBrowser.Controller.MediaEncoding && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode && state.SubtitleStream.IsExternal && !state.SubtitleStream.IsTextSubtitleStream) { - if (state.VideoStream != null && state.VideoStream.Width.HasValue) - { - // This is hacky but not sure how to get the exact subtitle resolution - int height = Convert.ToInt32(state.VideoStream.Width.Value / 16.0 * 9.0); - - arg.Append(" -canvas_size ") - .Append(state.VideoStream.Width.Value.ToString(CultureInfo.InvariantCulture)) - .Append(':') - .Append(height.ToString(CultureInfo.InvariantCulture)); - } - var subtitlePath = state.SubtitleStream.Path; if (string.Equals(Path.GetExtension(subtitlePath), ".sub", StringComparison.OrdinalIgnoreCase)) @@ -653,7 +627,7 @@ namespace MediaBrowser.Controller.MediaEncoding // _fileSystem.CreateDirectory(_fileSystem.GetDirectoryName(fallbackFontPath)); // using (var stream = _assemblyInfo.GetManifestResourceStream(GetType(), GetType().Namespace + ".DroidSansFallback.ttf")) // { - // using (var fileStream = _fileSystem.GetFileStream(fallbackFontPath, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read)) + // using (var fileStream = new FileStream(fallbackFontPath, FileMode.Create, FileAccess.Write, FileShare.Read)) // { // stream.CopyTo(fileStream); // } @@ -1546,9 +1520,12 @@ namespace MediaBrowser.Controller.MediaEncoding } /// <summary> - /// Gets the internal graphical subtitle param. + /// Gets the graphical subtitle param. /// </summary> - public string GetGraphicalSubtitleParam(EncodingJobInfo state, EncodingOptions options, string outputVideoCodec) + public string GetGraphicalSubtitleParam( + EncodingJobInfo state, + EncodingOptions options, + string outputVideoCodec) { var outputSizeParam = string.Empty; @@ -1562,53 +1539,77 @@ namespace MediaBrowser.Controller.MediaEncoding { outputSizeParam = GetOutputSizeParam(state, options, outputVideoCodec).TrimEnd('"'); - if (string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase)) + var index = outputSizeParam.IndexOf("hwdownload", StringComparison.OrdinalIgnoreCase); + if (index != -1) { - var index = outputSizeParam.IndexOf("format", StringComparison.OrdinalIgnoreCase); - if (index != -1) - { - outputSizeParam = "," + outputSizeParam.Substring(index); - } + outputSizeParam = "," + outputSizeParam.Substring(index); } else { - var index = outputSizeParam.IndexOf("scale", StringComparison.OrdinalIgnoreCase); + index = outputSizeParam.IndexOf("format", StringComparison.OrdinalIgnoreCase); if (index != -1) { outputSizeParam = "," + outputSizeParam.Substring(index); } - } - } - - if (string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase) - && outputSizeParam.Length == 0) - { - outputSizeParam = ",format=nv12|vaapi,hwupload"; - - // Add parameters to use VAAPI with burn-in subttiles (GH issue #642) - if (state.SubtitleStream != null - && state.SubtitleStream.IsTextSubtitleStream - && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode) { - outputSizeParam += ",hwmap=mode=read+write+direct"; + else + { + index = outputSizeParam.IndexOf("yadif", StringComparison.OrdinalIgnoreCase); + if (index != -1) + { + outputSizeParam = "," + outputSizeParam.Substring(index); + } + else + { + index = outputSizeParam.IndexOf("scale", StringComparison.OrdinalIgnoreCase); + if (index != -1) + { + outputSizeParam = "," + outputSizeParam.Substring(index); + } + } + } } } var videoSizeParam = string.Empty; + var videoDecoder = GetHardwareAcceleratedVideoDecoder(state, options); // Setup subtitle scaling if (state.VideoStream != null && state.VideoStream.Width.HasValue && state.VideoStream.Height.HasValue) { + // force_original_aspect_ratio=decrease + // Enable decreasing output video width or height if necessary to keep the original aspect ratio videoSizeParam = string.Format( CultureInfo.InvariantCulture, - "scale={0}:{1}", + "scale={0}:{1}:force_original_aspect_ratio=decrease", state.VideoStream.Width.Value, state.VideoStream.Height.Value); - //For QSV, feed it into hardware encoder now + // For QSV, feed it into hardware encoder now if (string.Equals(outputVideoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase)) { videoSizeParam += ",hwupload=extra_hw_frames=64"; } + + // For VAAPI and CUVID decoder + // these encoders cannot automatically adjust the size of graphical subtitles to fit the output video, + // thus needs to be manually adjusted. + if ((IsVaapiSupported(state) && string.Equals(options.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase)) + || (videoDecoder ?? string.Empty).IndexOf("cuvid", StringComparison.OrdinalIgnoreCase) != -1) + { + var videoStream = state.VideoStream; + var inputWidth = videoStream?.Width; + var inputHeight = videoStream?.Height; + var (width, height) = GetFixedOutputSize(inputWidth, inputHeight, request.Width, request.Height, request.MaxWidth, request.MaxHeight); + + if (width.HasValue && height.HasValue) + { + videoSizeParam = string.Format( + CultureInfo.InvariantCulture, + "scale={0}:{1}:force_original_aspect_ratio=decrease", + width.Value, + height.Value); + } + } } var mapPrefix = state.SubtitleStream.IsExternal ? @@ -1619,27 +1620,50 @@ namespace MediaBrowser.Controller.MediaEncoding ? 0 : state.SubtitleStream.Index; - var videoDecoder = GetHardwareAcceleratedVideoDecoder(state, options); - // Setup default filtergraph utilizing FFMpeg overlay() and FFMpeg scale() (see the return of this function for index reference) var retStr = " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}][sub]overlay{3}\""; - if (string.Equals(outputVideoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase)) + // When the input may or may not be hardware VAAPI decodable + if (string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase)) + { + /* + [base]: HW scaling video to OutputSize + [sub]: SW scaling subtitle to FixedOutputSize + [base][sub]: SW overlay + */ + outputSizeParam = outputSizeParam.TrimStart(','); + retStr = " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}]{3},hwdownload[base];[base][sub]overlay,format=nv12,hwupload\""; + } + + // If we're hardware VAAPI decoding and software encoding, download frames from the decoder first + else if (IsVaapiSupported(state) && string.Equals(options.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase) + && string.Equals(outputVideoCodec, "libx264", StringComparison.OrdinalIgnoreCase)) + { + /* + [base]: SW scaling video to OutputSize + [sub]: SW scaling subtitle to FixedOutputSize + [base][sub]: SW overlay + */ + outputSizeParam = outputSizeParam.TrimStart(','); + retStr = " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}]{3}[base];[base][sub]overlay\""; + } + + else if (string.Equals(outputVideoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase)) { /* QSV in FFMpeg can now setup hardware overlay for transcodes. For software decoding and hardware encoding option, frames must be hwuploaded into hardware - with fixed frame size. + with fixed frame size. */ if (!string.IsNullOrEmpty(videoDecoder) && videoDecoder.Contains("qsv", StringComparison.OrdinalIgnoreCase)) { retStr = " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}][sub]overlay_qsv=x=(W-w)/2:y=(H-h)/2{3}\""; - } - else + } + else { retStr = " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}]hwupload=extra_hw_frames=64[v];[v][sub]overlay_qsv=x=(W-w)/2:y=(H-h)/2{3}\""; } - } + } return string.Format( CultureInfo.InvariantCulture, @@ -1688,7 +1712,8 @@ namespace MediaBrowser.Controller.MediaEncoding return (Convert.ToInt32(outputWidth), Convert.ToInt32(outputHeight)); } - public List<string> GetScalingFilters(int? videoWidth, + public List<string> GetScalingFilters(EncodingJobInfo state, + int? videoWidth, int? videoHeight, Video3DFormat? threedFormat, string videoDecoder, @@ -1707,7 +1732,9 @@ namespace MediaBrowser.Controller.MediaEncoding requestedMaxWidth, requestedMaxHeight); - if ((string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase) || string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase)) + var hasTextSubs = state.SubtitleStream != null && state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; + + if (string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase) || (string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase) && !hasTextSubs) && width.HasValue && height.HasValue) { @@ -1731,13 +1758,13 @@ namespace MediaBrowser.Controller.MediaEncoding vaapi_or_qsv, outputWidth, outputHeight)); - } - else + } + else { filters.Add(string.Format(CultureInfo.InvariantCulture, "scale_{0}=format=nv12", vaapi_or_qsv)); } } - else if ((videoDecoder ?? string.Empty).IndexOf("_cuvid", StringComparison.OrdinalIgnoreCase) != -1 + else if ((videoDecoder ?? string.Empty).IndexOf("cuvid", StringComparison.OrdinalIgnoreCase) != -1 && width.HasValue && height.HasValue) { @@ -1941,8 +1968,7 @@ namespace MediaBrowser.Controller.MediaEncoding public string GetOutputSizeParam( EncodingJobInfo state, EncodingOptions options, - string outputVideoCodec, - bool allowTimeStampCopy = true) + string outputVideoCodec) { // http://sonnati.wordpress.com/2012/10/19/ffmpeg-the-swiss-army-knife-of-internet-streaming-part-vi/ @@ -1951,42 +1977,61 @@ namespace MediaBrowser.Controller.MediaEncoding var videoStream = state.VideoStream; var filters = new List<string>(); - // If we're hardware VAAPI decoding and software encoding, download frames from the decoder first - var hwType = options.HardwareAccelerationType ?? string.Empty; - if (string.Equals(hwType, "vaapi", StringComparison.OrdinalIgnoreCase) && !options.EnableHardwareEncoding ) - { - filters.Add("hwdownload"); + var videoDecoder = GetHardwareAcceleratedVideoDecoder(state, options); + var inputWidth = videoStream?.Width; + var inputHeight = videoStream?.Height; + var threeDFormat = state.MediaSource.Video3DFormat; - // If transcoding from 10 bit, transform colour spaces too - if (!string.IsNullOrEmpty(videoStream.PixelFormat) - && videoStream.PixelFormat.IndexOf("p10", StringComparison.OrdinalIgnoreCase) != -1 - && string.Equals(outputVideoCodec, "libx264", StringComparison.OrdinalIgnoreCase)) - { - filters.Add("format=p010le"); - filters.Add("format=nv12"); - } - else - { - filters.Add("format=nv12"); - } - } + var hasTextSubs = state.SubtitleStream != null && state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; + // When the input may or may not be hardware VAAPI decodable if (string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase)) { filters.Add("format=nv12|vaapi"); filters.Add("hwupload"); } - var videoDecoder = GetHardwareAcceleratedVideoDecoder(state, options); - - // If we are software decoding, and hardware encoding - if (string.Equals(outputVideoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase) - && (string.IsNullOrEmpty(videoDecoder) || !videoDecoder.Contains("qsv", StringComparison.OrdinalIgnoreCase))) + // When the input may or may not be hardware QSV decodable + else if (string.Equals(outputVideoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase)) { - filters.Add("format=nv12|qsv"); - filters.Add("hwupload=extra_hw_frames=64"); + if (!hasTextSubs) + { + filters.Add("format=nv12|qsv"); + filters.Add("hwupload=extra_hw_frames=64"); + } } + // If we're hardware VAAPI decoding and software encoding, download frames from the decoder first + else if (IsVaapiSupported(state) && string.Equals(options.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase) + && string.Equals(outputVideoCodec, "libx264", StringComparison.OrdinalIgnoreCase)) + { + var codec = videoStream.Codec.ToLowerInvariant(); + var isColorDepth10 = !string.IsNullOrEmpty(videoStream.Profile) && (videoStream.Profile.Contains("Main 10", StringComparison.OrdinalIgnoreCase) + || videoStream.Profile.Contains("High 10", StringComparison.OrdinalIgnoreCase)); + + // Assert 10-bit hardware VAAPI decodable + if (isColorDepth10 && (string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase) + || string.Equals(codec, "h265", StringComparison.OrdinalIgnoreCase) + || string.Equals(codec, "vp9", StringComparison.OrdinalIgnoreCase))) + { + /* + Download data from GPU to CPU as p010le format. + Colorspace conversion is unnecessary here as libx264 will handle it. + If this step is missing, it will fail on AMD but not on intel. + */ + filters.Add("hwdownload"); + filters.Add("format=p010le"); + } + + // Assert 8-bit hardware VAAPI decodable + else if (!isColorDepth10) + { + filters.Add("hwdownload"); + filters.Add("format=nv12"); + } + } + + // Add hardware deinterlace filter before scaling filter if (state.DeInterlace("h264", true)) { if (string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase)) @@ -1995,17 +2040,23 @@ namespace MediaBrowser.Controller.MediaEncoding } else if (string.Equals(outputVideoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase)) { - filters.Add(string.Format(CultureInfo.InvariantCulture, "deinterlace_qsv")); + if (!hasTextSubs) + { + filters.Add(string.Format(CultureInfo.InvariantCulture, "deinterlace_qsv")); + } } } - if ((state.DeInterlace("h264", true) || state.DeInterlace("h265", true) || state.DeInterlace("hevc", true)) - && !string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase)) + // Add software deinterlace filter before scaling filter + if (((state.DeInterlace("h264", true) || state.DeInterlace("h265", true) || state.DeInterlace("hevc", true)) + && !string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase) + && !string.Equals(outputVideoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase)) + || (hasTextSubs && state.DeInterlace("h264", true) && string.Equals(outputVideoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase))) { var inputFramerate = videoStream?.RealFrameRate; // If it is already 60fps then it will create an output framerate that is much too high for roku and others to handle - if (string.Equals(options.DeinterlaceMethod, "bobandweave", StringComparison.OrdinalIgnoreCase) && (inputFramerate ?? 60) <= 30) + if (string.Equals(options.DeinterlaceMethod, "yadif_bob", StringComparison.OrdinalIgnoreCase) && (inputFramerate ?? 60) <= 30) { filters.Add("yadif=1:-1:0"); } @@ -2015,11 +2066,21 @@ namespace MediaBrowser.Controller.MediaEncoding } } - var inputWidth = videoStream?.Width; - var inputHeight = videoStream?.Height; - var threeDFormat = state.MediaSource.Video3DFormat; + // Add scaling filter: scale_*=format=nv12 or scale_*=w=*:h=*:format=nv12 or scale=expr + filters.AddRange(GetScalingFilters(state, inputWidth, inputHeight, threeDFormat, videoDecoder, outputVideoCodec, request.Width, request.Height, request.MaxWidth, request.MaxHeight)); - filters.AddRange(GetScalingFilters(inputWidth, inputHeight, threeDFormat, videoDecoder, outputVideoCodec, request.Width, request.Height, request.MaxWidth, request.MaxHeight)); + // Add parameters to use VAAPI with burn-in text subttiles (GH issue #642) + if (string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase)) + { + if (state.SubtitleStream != null + && state.SubtitleStream.IsTextSubtitleStream + && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode) + { + // Test passed on Intel and AMD gfx + filters.Add("hwmap=mode=read+write"); + filters.Add("format=nv12"); + } + } var output = string.Empty; @@ -2037,11 +2098,6 @@ namespace MediaBrowser.Controller.MediaEncoding { filters.Add("hwmap"); } - - if (allowTimeStampCopy) - { - output += " -copyts"; - } } if (filters.Count > 0) @@ -2218,7 +2274,7 @@ namespace MediaBrowser.Controller.MediaEncoding { inputModifier += " " + videoDecoder; - if ((videoDecoder ?? string.Empty).IndexOf("_cuvid", StringComparison.OrdinalIgnoreCase) != -1) + if ((videoDecoder ?? string.Empty).IndexOf("cuvid", StringComparison.OrdinalIgnoreCase) != -1) { var videoStream = state.VideoStream; var inputWidth = videoStream?.Width; @@ -2227,7 +2283,7 @@ namespace MediaBrowser.Controller.MediaEncoding var (width, height) = GetFixedOutputSize(inputWidth, inputHeight, request.Width, request.Height, request.MaxWidth, request.MaxHeight); - if ((videoDecoder ?? string.Empty).IndexOf("_cuvid", StringComparison.OrdinalIgnoreCase) != -1 + if ((videoDecoder ?? string.Empty).IndexOf("cuvid", StringComparison.OrdinalIgnoreCase) != -1 && width.HasValue && height.HasValue) { @@ -2491,7 +2547,7 @@ namespace MediaBrowser.Controller.MediaEncoding encodingOptions.HardwareDecodingCodecs = Array.Empty<string>(); return null; } - return "-c:v h264_qsv "; + return "-c:v h264_qsv"; } break; case "hevc": @@ -2499,19 +2555,19 @@ namespace MediaBrowser.Controller.MediaEncoding if (_mediaEncoder.SupportsDecoder("hevc_qsv") && encodingOptions.HardwareDecodingCodecs.Contains("hevc", StringComparer.OrdinalIgnoreCase)) { //return "-c:v hevc_qsv -load_plugin hevc_hw "; - return "-c:v hevc_qsv "; + return "-c:v hevc_qsv"; } break; case "mpeg2video": if (_mediaEncoder.SupportsDecoder("mpeg2_qsv") && encodingOptions.HardwareDecodingCodecs.Contains("mpeg2video", StringComparer.OrdinalIgnoreCase)) { - return "-c:v mpeg2_qsv "; + return "-c:v mpeg2_qsv"; } break; case "vc1": if (_mediaEncoder.SupportsDecoder("vc1_qsv") && encodingOptions.HardwareDecodingCodecs.Contains("vc1", StringComparer.OrdinalIgnoreCase)) { - return "-c:v vc1_qsv "; + return "-c:v vc1_qsv"; } break; } @@ -2525,32 +2581,38 @@ namespace MediaBrowser.Controller.MediaEncoding case "h264": if (_mediaEncoder.SupportsDecoder("h264_cuvid") && encodingOptions.HardwareDecodingCodecs.Contains("h264", StringComparer.OrdinalIgnoreCase)) { - return "-c:v h264_cuvid "; + // cuvid decoder does not support 10-bit input + if ((videoStream.BitDepth ?? 8) > 8) + { + encodingOptions.HardwareDecodingCodecs = Array.Empty<string>(); + return null; + } + return "-c:v h264_cuvid"; } break; case "hevc": case "h265": if (_mediaEncoder.SupportsDecoder("hevc_cuvid") && encodingOptions.HardwareDecodingCodecs.Contains("hevc", StringComparer.OrdinalIgnoreCase)) { - return "-c:v hevc_cuvid "; + return "-c:v hevc_cuvid"; } break; case "mpeg2video": if (_mediaEncoder.SupportsDecoder("mpeg2_cuvid") && encodingOptions.HardwareDecodingCodecs.Contains("mpeg2video", StringComparer.OrdinalIgnoreCase)) { - return "-c:v mpeg2_cuvid "; + return "-c:v mpeg2_cuvid"; } break; case "vc1": if (_mediaEncoder.SupportsDecoder("vc1_cuvid") && encodingOptions.HardwareDecodingCodecs.Contains("vc1", StringComparer.OrdinalIgnoreCase)) { - return "-c:v vc1_cuvid "; + return "-c:v vc1_cuvid"; } break; case "mpeg4": if (_mediaEncoder.SupportsDecoder("mpeg4_cuvid") && encodingOptions.HardwareDecodingCodecs.Contains("mpeg4", StringComparer.OrdinalIgnoreCase)) { - return "-c:v mpeg4_cuvid "; + return "-c:v mpeg4_cuvid"; } break; } @@ -2564,38 +2626,38 @@ namespace MediaBrowser.Controller.MediaEncoding case "h264": if (_mediaEncoder.SupportsDecoder("h264_mediacodec") && encodingOptions.HardwareDecodingCodecs.Contains("h264", StringComparer.OrdinalIgnoreCase)) { - return "-c:v h264_mediacodec "; + return "-c:v h264_mediacodec"; } break; case "hevc": case "h265": if (_mediaEncoder.SupportsDecoder("hevc_mediacodec") && encodingOptions.HardwareDecodingCodecs.Contains("hevc", StringComparer.OrdinalIgnoreCase)) { - return "-c:v hevc_mediacodec "; + return "-c:v hevc_mediacodec"; } break; case "mpeg2video": if (_mediaEncoder.SupportsDecoder("mpeg2_mediacodec") && encodingOptions.HardwareDecodingCodecs.Contains("mpeg2video", StringComparer.OrdinalIgnoreCase)) { - return "-c:v mpeg2_mediacodec "; + return "-c:v mpeg2_mediacodec"; } break; case "mpeg4": if (_mediaEncoder.SupportsDecoder("mpeg4_mediacodec") && encodingOptions.HardwareDecodingCodecs.Contains("mpeg4", StringComparer.OrdinalIgnoreCase)) { - return "-c:v mpeg4_mediacodec "; + return "-c:v mpeg4_mediacodec"; } break; case "vp8": if (_mediaEncoder.SupportsDecoder("vp8_mediacodec") && encodingOptions.HardwareDecodingCodecs.Contains("vp8", StringComparer.OrdinalIgnoreCase)) { - return "-c:v vp8_mediacodec "; + return "-c:v vp8_mediacodec"; } break; case "vp9": if (_mediaEncoder.SupportsDecoder("vp9_mediacodec") && encodingOptions.HardwareDecodingCodecs.Contains("vp9", StringComparer.OrdinalIgnoreCase)) { - return "-c:v vp9_mediacodec "; + return "-c:v vp9_mediacodec"; } break; } @@ -2609,25 +2671,25 @@ namespace MediaBrowser.Controller.MediaEncoding case "h264": if (_mediaEncoder.SupportsDecoder("h264_mmal") && encodingOptions.HardwareDecodingCodecs.Contains("h264", StringComparer.OrdinalIgnoreCase)) { - return "-c:v h264_mmal "; + return "-c:v h264_mmal"; } break; case "mpeg2video": if (_mediaEncoder.SupportsDecoder("mpeg2_mmal") && encodingOptions.HardwareDecodingCodecs.Contains("mpeg2video", StringComparer.OrdinalIgnoreCase)) { - return "-c:v mpeg2_mmal "; + return "-c:v mpeg2_mmal"; } break; case "mpeg4": if (_mediaEncoder.SupportsDecoder("mpeg4_mmal") && encodingOptions.HardwareDecodingCodecs.Contains("mpeg4", StringComparer.OrdinalIgnoreCase)) { - return "-c:v mpeg4_mmal "; + return "-c:v mpeg4_mmal"; } break; case "vc1": if (_mediaEncoder.SupportsDecoder("vc1_mmal") && encodingOptions.HardwareDecodingCodecs.Contains("vc1", StringComparer.OrdinalIgnoreCase)) { - return "-c:v vc1_mmal "; + return "-c:v vc1_mmal"; } break; } @@ -2637,27 +2699,14 @@ namespace MediaBrowser.Controller.MediaEncoding { if (Environment.OSVersion.Platform == PlatformID.Win32NT) { - if(Environment.OSVersion.Version.Major > 6 || (Environment.OSVersion.Version.Major == 6 && Environment.OSVersion.Version.Minor > 1)) + if (Environment.OSVersion.Version.Major > 6 || (Environment.OSVersion.Version.Major == 6 && Environment.OSVersion.Version.Minor > 1)) return "-hwaccel d3d11va"; else return "-hwaccel dxva2"; } - - switch (videoStream.Codec.ToLowerInvariant()) + else { - case "avc": - case "h264": - if (_mediaEncoder.SupportsDecoder("h264_amf") && encodingOptions.HardwareDecodingCodecs.Contains("h264", StringComparer.OrdinalIgnoreCase)) - { - return "-c:v h264_amf"; - } - break; - case "mpeg2video": - if (_mediaEncoder.SupportsDecoder("hevc_amf") && encodingOptions.HardwareDecodingCodecs.Contains("mpeg2video", StringComparer.OrdinalIgnoreCase)) - { - return "-c:v mpeg2_mmal"; - } - break; + return "-hwaccel vaapi"; } } } @@ -2770,7 +2819,7 @@ namespace MediaBrowser.Controller.MediaEncoding if (!state.RunTimeTicks.HasValue) { - args += " -flags -global_header -fflags +genpts"; + args += " -fflags +genpts"; } } else @@ -2785,14 +2834,27 @@ namespace MediaBrowser.Controller.MediaEncoding var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; var hasCopyTs = false; + // Add resolution params, if specified if (!hasGraphicalSubs) { var outputSizeParam = GetOutputSizeParam(state, encodingOptions, videoCodec); + args += outputSizeParam; + hasCopyTs = outputSizeParam.IndexOf("copyts", StringComparison.OrdinalIgnoreCase) != -1; } + // This is for graphical subs + if (hasGraphicalSubs) + { + var graphicalSubtitleParam = GetGraphicalSubtitleParam(state, encodingOptions, videoCodec); + + args += graphicalSubtitleParam; + + hasCopyTs = graphicalSubtitleParam.IndexOf("copyts", StringComparison.OrdinalIgnoreCase) != -1; + } + if (state.RunTimeTicks.HasValue && state.BaseRequest.CopyTimestamps) { if (!hasCopyTs) @@ -2800,13 +2862,12 @@ namespace MediaBrowser.Controller.MediaEncoding args += " -copyts"; } - args += " -avoid_negative_ts disabled -start_at_zero"; - } + args += " -avoid_negative_ts disabled"; - // This is for internal graphical subs - if (hasGraphicalSubs) - { - args += GetGraphicalSubtitleParam(state, encodingOptions, videoCodec); + if (!(state.SubtitleStream != null && state.SubtitleStream.IsExternal && !state.SubtitleStream.IsTextSubtitleStream)) + { + args += " -start_at_zero"; + } } var qualityParam = GetVideoQualityParam(state, videoCodec, encodingOptions, defaultPreset); @@ -2815,11 +2876,6 @@ namespace MediaBrowser.Controller.MediaEncoding { args += " " + qualityParam.Trim(); } - - if (!state.RunTimeTicks.HasValue) - { - args += " -flags -global_header"; - } } if (!string.IsNullOrEmpty(state.OutputVideoSync)) @@ -2917,6 +2973,5 @@ namespace MediaBrowser.Controller.MediaEncoding string.Empty, string.Empty).Trim(); } - } } diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs index 34af3b1568..1127a08ded 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs @@ -9,8 +9,8 @@ using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; using MediaBrowser.Model.MediaInfo; -using MediaBrowser.Model.Session; using MediaBrowser.Model.Net; +using MediaBrowser.Model.Session; namespace MediaBrowser.Controller.MediaEncoding { @@ -316,11 +316,7 @@ namespace MediaBrowser.Controller.MediaEncoding { if (VideoStream != null && VideoStream.Width.HasValue && VideoStream.Height.HasValue) { - var size = new ImageDimensions - { - Width = VideoStream.Width.Value, - Height = VideoStream.Height.Value - }; + var size = new ImageDimensions(VideoStream.Width.Value, VideoStream.Height.Value); var newSize = DrawingUtils.Resize(size, BaseRequest.Width ?? 0, @@ -346,11 +342,7 @@ namespace MediaBrowser.Controller.MediaEncoding { if (VideoStream != null && VideoStream.Width.HasValue && VideoStream.Height.HasValue) { - var size = new ImageDimensions - { - Width = VideoStream.Width.Value, - Height = VideoStream.Height.Value - }; + var size = new ImageDimensions(VideoStream.Width.Value, VideoStream.Height.Value); var newSize = DrawingUtils.Resize(size, BaseRequest.Width ?? 0, @@ -665,7 +657,7 @@ namespace MediaBrowser.Controller.MediaEncoding } public IProgress<double> Progress { get; set; } - public virtual void ReportTranscodingProgress(TimeSpan? transcodingPosition, float framerate, double? percentComplete, long bytesTranscoded, int? bitRate) + public virtual void ReportTranscodingProgress(TimeSpan? transcodingPosition, float? framerate, double? percentComplete, long? bytesTranscoded, int? bitRate) { Progress.Report(percentComplete.Value); } diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingJobOptions.cs b/MediaBrowser.Controller/MediaEncoding/EncodingJobOptions.cs index d64feb2f7c..addc88174f 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingJobOptions.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingJobOptions.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Globalization; using System.Linq; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Services; diff --git a/MediaBrowser.Controller/MediaEncoding/IEncodingManager.cs b/MediaBrowser.Controller/MediaEncoding/IEncodingManager.cs index e560999e8f..15a2580afd 100644 --- a/MediaBrowser.Controller/MediaEncoding/IEncodingManager.cs +++ b/MediaBrowser.Controller/MediaEncoding/IEncodingManager.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; @@ -12,6 +14,6 @@ namespace MediaBrowser.Controller.MediaEncoding /// <summary> /// Refreshes the chapter images. /// </summary> - Task<bool> RefreshChapterImages(Video video, IDirectoryService directoryService, List<ChapterInfo> chapters, bool extractImages, bool saveChapters, CancellationToken cancellationToken); + Task<bool> RefreshChapterImages(Video video, IDirectoryService directoryService, IReadOnlyList<ChapterInfo> chapters, bool extractImages, bool saveChapters, CancellationToken cancellationToken); } } diff --git a/MediaBrowser.Controller/MediaEncoding/JobLogger.cs b/MediaBrowser.Controller/MediaEncoding/JobLogger.cs index ac989f6ba1..c9f64c7075 100644 --- a/MediaBrowser.Controller/MediaEncoding/JobLogger.cs +++ b/MediaBrowser.Controller/MediaEncoding/JobLogger.cs @@ -4,7 +4,6 @@ using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; -using MediaBrowser.Model.Extensions; using Microsoft.Extensions.Logging; namespace MediaBrowser.Controller.MediaEncoding @@ -90,6 +89,15 @@ namespace MediaBrowser.Controller.MediaEncoding framerate = val; } } + else if (part.StartsWith("fps=", StringComparison.OrdinalIgnoreCase)) + { + var rate = part.Split(new[] { '=' }, 2)[^1]; + + if (float.TryParse(rate, NumberStyles.Any, _usCulture, out var val)) + { + framerate = val; + } + } else if (state.RunTimeTicks.HasValue && part.StartsWith("time=", StringComparison.OrdinalIgnoreCase)) { @@ -146,7 +154,7 @@ namespace MediaBrowser.Controller.MediaEncoding if (framerate.HasValue || percent.HasValue) { - state.ReportTranscodingProgress(transcodingPosition, 0, percent, 0, bitRate); + state.ReportTranscodingProgress(transcodingPosition, framerate, percent, bytesTranscoded, bitRate); } } } diff --git a/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs b/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs index b193cbb55a..1162bff130 100644 --- a/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs +++ b/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs @@ -22,7 +22,7 @@ namespace MediaBrowser.Controller.Net /// <summary> /// The _active connections /// </summary> - protected readonly List<Tuple<IWebSocketConnection, CancellationTokenSource, TStateType>> ActiveConnections = + private readonly List<Tuple<IWebSocketConnection, CancellationTokenSource, TStateType>> _activeConnections = new List<Tuple<IWebSocketConnection, CancellationTokenSource, TStateType>>(); /// <summary> @@ -98,9 +98,9 @@ namespace MediaBrowser.Controller.Net InitialDelayMs = dueTimeMs }; - lock (ActiveConnections) + lock (_activeConnections) { - ActiveConnections.Add(new Tuple<IWebSocketConnection, CancellationTokenSource, TStateType>(message.Connection, cancellationTokenSource, state)); + _activeConnections.Add(new Tuple<IWebSocketConnection, CancellationTokenSource, TStateType>(message.Connection, cancellationTokenSource, state)); } } @@ -108,9 +108,9 @@ namespace MediaBrowser.Controller.Net { Tuple<IWebSocketConnection, CancellationTokenSource, TStateType>[] tuples; - lock (ActiveConnections) + lock (_activeConnections) { - tuples = ActiveConnections + tuples = _activeConnections .Where(c => { if (c.Item1.State == WebSocketState.Open && !c.Item2.IsCancellationRequested) @@ -177,9 +177,9 @@ namespace MediaBrowser.Controller.Net /// <param name="message">The message.</param> private void Stop(WebSocketMessageInfo message) { - lock (ActiveConnections) + lock (_activeConnections) { - var connection = ActiveConnections.FirstOrDefault(c => c.Item1 == message.Connection); + var connection = _activeConnections.FirstOrDefault(c => c.Item1 == message.Connection); if (connection != null) { @@ -209,9 +209,9 @@ namespace MediaBrowser.Controller.Net //TODO Investigate and properly fix. } - lock (ActiveConnections) + lock (_activeConnections) { - ActiveConnections.Remove(connection); + _activeConnections.Remove(connection); } } @@ -223,9 +223,9 @@ namespace MediaBrowser.Controller.Net { if (dispose) { - lock (ActiveConnections) + lock (_activeConnections) { - foreach (var connection in ActiveConnections.ToArray()) + foreach (var connection in _activeConnections.ToArray()) { DisposeConnection(connection); } diff --git a/MediaBrowser.Controller/Net/IAuthService.cs b/MediaBrowser.Controller/Net/IAuthService.cs index 4c9120e0c9..9132404a08 100644 --- a/MediaBrowser.Controller/Net/IAuthService.cs +++ b/MediaBrowser.Controller/Net/IAuthService.cs @@ -1,3 +1,5 @@ +#nullable enable + using MediaBrowser.Controller.Entities; using MediaBrowser.Model.Services; using Microsoft.AspNetCore.Http; @@ -7,6 +9,6 @@ namespace MediaBrowser.Controller.Net public interface IAuthService { void Authenticate(IRequest request, IAuthenticationAttributes authAttribtues); - User Authenticate(HttpRequest request, IAuthenticationAttributes authAttribtues); + User? Authenticate(HttpRequest request, IAuthenticationAttributes authAttribtues); } } diff --git a/MediaBrowser.Controller/Net/IHttpResultFactory.cs b/MediaBrowser.Controller/Net/IHttpResultFactory.cs index ff9ecf8af0..25404fa78d 100644 --- a/MediaBrowser.Controller/Net/IHttpResultFactory.cs +++ b/MediaBrowser.Controller/Net/IHttpResultFactory.cs @@ -2,8 +2,6 @@ using System; using System.Collections.Generic; using System.IO; using System.Threading.Tasks; - -using MediaBrowser.Model.IO; using MediaBrowser.Model.Services; namespace MediaBrowser.Controller.Net @@ -66,7 +64,7 @@ namespace MediaBrowser.Controller.Net /// <param name="path">The path.</param> /// <param name="fileShare">The file share.</param> /// <returns>System.Object.</returns> - Task<object> GetStaticFileResult(IRequest requestContext, string path, FileShareMode fileShare = FileShareMode.Read); + Task<object> GetStaticFileResult(IRequest requestContext, string path, FileShare fileShare = FileShare.Read); /// <summary> /// Gets the static file result. diff --git a/MediaBrowser.Controller/Net/IHttpServer.cs b/MediaBrowser.Controller/Net/IHttpServer.cs index 694e3954ed..666ac1cfef 100644 --- a/MediaBrowser.Controller/Net/IHttpServer.cs +++ b/MediaBrowser.Controller/Net/IHttpServer.cs @@ -26,7 +26,7 @@ namespace MediaBrowser.Controller.Net /// <summary> /// Inits this instance. /// </summary> - void Init(IEnumerable<IService> services, IEnumerable<IWebSocketListener> listener, IEnumerable<string> urlPrefixes); + void Init(IEnumerable<Type> serviceTypes, IEnumerable<IWebSocketListener> listener, IEnumerable<string> urlPrefixes); /// <summary> /// If set, all requests will respond with this message diff --git a/MediaBrowser.Controller/Net/SecurityException.cs b/MediaBrowser.Controller/Net/SecurityException.cs index 3ccecf0eb8..a5b94ea5e3 100644 --- a/MediaBrowser.Controller/Net/SecurityException.cs +++ b/MediaBrowser.Controller/Net/SecurityException.cs @@ -2,20 +2,36 @@ using System; namespace MediaBrowser.Controller.Net { + /// <summary> + /// The exception that is thrown when a user is authenticated, but not authorized to access a requested resource. + /// </summary> public class SecurityException : Exception { + /// <summary> + /// Initializes a new instance of the <see cref="SecurityException"/> class. + /// </summary> + public SecurityException() + : base() + { + } + + /// <summary> + /// Initializes a new instance of the <see cref="SecurityException"/> class. + /// </summary> + /// <param name="message">The message that describes the error.</param> public SecurityException(string message) : base(message) { - } - public SecurityExceptionType SecurityExceptionType { get; set; } - } - - public enum SecurityExceptionType - { - Unauthenticated = 0, - ParentalControl = 1 + /// <summary> + /// Initializes a new instance of the <see cref="SecurityException"/> class. + /// </summary> + /// <param name="message">The message that describes the error</param> + /// <param name="innerException">The exception that is the cause of the current exception, or a null reference if no inner exception is specified.</param> + public SecurityException(string message, Exception innerException) + : base(message, innerException) + { + } } } diff --git a/MediaBrowser.Controller/Net/StaticResultOptions.cs b/MediaBrowser.Controller/Net/StaticResultOptions.cs index 7a179913ac..071beaed19 100644 --- a/MediaBrowser.Controller/Net/StaticResultOptions.cs +++ b/MediaBrowser.Controller/Net/StaticResultOptions.cs @@ -3,8 +3,6 @@ using System.Collections.Generic; using System.IO; using System.Threading.Tasks; -using MediaBrowser.Model.IO; - namespace MediaBrowser.Controller.Net { public class StaticResultOptions @@ -24,12 +22,12 @@ namespace MediaBrowser.Controller.Net public string Path { get; set; } public long? ContentLength { get; set; } - public FileShareMode FileShare { get; set; } + public FileShare FileShare { get; set; } public StaticResultOptions() { ResponseHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); - FileShare = FileShareMode.Read; + FileShare = FileShare.Read; } } diff --git a/MediaBrowser.Controller/Persistence/IDisplayPreferencesRepository.cs b/MediaBrowser.Controller/Persistence/IDisplayPreferencesRepository.cs index 4424e044bb..c2dcb66d7c 100644 --- a/MediaBrowser.Controller/Persistence/IDisplayPreferencesRepository.cs +++ b/MediaBrowser.Controller/Persistence/IDisplayPreferencesRepository.cs @@ -6,30 +6,34 @@ using MediaBrowser.Model.Entities; namespace MediaBrowser.Controller.Persistence { /// <summary> - /// Interface IDisplayPreferencesRepository + /// Interface IDisplayPreferencesRepository. /// </summary> public interface IDisplayPreferencesRepository : IRepository { /// <summary> - /// Saves display preferences for an item + /// Saves display preferences for an item. /// </summary> /// <param name="displayPreferences">The display preferences.</param> /// <param name="userId">The user id.</param> /// <param name="client">The client.</param> /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task.</returns> - void SaveDisplayPreferences(DisplayPreferences displayPreferences, string userId, string client, - CancellationToken cancellationToken); + void SaveDisplayPreferences( + DisplayPreferences displayPreferences, + string userId, + string client, + CancellationToken cancellationToken); /// <summary> - /// Saves all display preferences for a user + /// Saves all display preferences for a user. /// </summary> /// <param name="displayPreferences">The display preferences.</param> /// <param name="userId">The user id.</param> /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task.</returns> - void SaveAllDisplayPreferences(IEnumerable<DisplayPreferences> displayPreferences, Guid userId, - CancellationToken cancellationToken); + void SaveAllDisplayPreferences( + IEnumerable<DisplayPreferences> displayPreferences, + Guid userId, + CancellationToken cancellationToken); + /// <summary> /// Gets the display preferences. /// </summary> diff --git a/MediaBrowser.Controller/Persistence/IItemRepository.cs b/MediaBrowser.Controller/Persistence/IItemRepository.cs index 5a5b7f58f0..75fc43a047 100644 --- a/MediaBrowser.Controller/Persistence/IItemRepository.cs +++ b/MediaBrowser.Controller/Persistence/IItemRepository.cs @@ -24,8 +24,7 @@ namespace MediaBrowser.Controller.Persistence /// Deletes the item. /// </summary> /// <param name="id">The identifier.</param> - /// <param name="cancellationToken">The cancellation token.</param> - void DeleteItem(Guid id, CancellationToken cancellationToken); + void DeleteItem(Guid id); /// <summary> /// Saves the items. @@ -61,7 +60,7 @@ namespace MediaBrowser.Controller.Persistence /// <summary> /// Saves the chapters. /// </summary> - void SaveChapters(Guid id, List<ChapterInfo> chapters); + void SaveChapters(Guid id, IReadOnlyList<ChapterInfo> chapters); /// <summary> /// Gets the media streams. @@ -169,4 +168,3 @@ namespace MediaBrowser.Controller.Persistence List<string> GetAllArtistNames(); } } - diff --git a/MediaBrowser.Controller/Persistence/MediaAttachmentQuery.cs b/MediaBrowser.Controller/Persistence/MediaAttachmentQuery.cs index 91ab34aab6..e3b2d46650 100644 --- a/MediaBrowser.Controller/Persistence/MediaAttachmentQuery.cs +++ b/MediaBrowser.Controller/Persistence/MediaAttachmentQuery.cs @@ -1,5 +1,4 @@ using System; -using MediaBrowser.Model.Entities; namespace MediaBrowser.Controller.Persistence { diff --git a/MediaBrowser.Controller/Playlists/IPlaylistManager.cs b/MediaBrowser.Controller/Playlists/IPlaylistManager.cs index 5001f68420..544cd2643f 100644 --- a/MediaBrowser.Controller/Playlists/IPlaylistManager.cs +++ b/MediaBrowser.Controller/Playlists/IPlaylistManager.cs @@ -29,7 +29,7 @@ namespace MediaBrowser.Controller.Playlists /// <param name="itemIds">The item ids.</param> /// <param name="userId">The user identifier.</param> /// <returns>Task.</returns> - void AddToPlaylist(string playlistId, IEnumerable<Guid> itemIds, Guid userId); + void AddToPlaylist(string playlistId, ICollection<Guid> itemIds, Guid userId); /// <summary> /// Removes from playlist. diff --git a/MediaBrowser.Controller/Plugins/IServerEntryPoint.cs b/MediaBrowser.Controller/Plugins/IServerEntryPoint.cs index e57929989a..1e8654c4dc 100644 --- a/MediaBrowser.Controller/Plugins/IServerEntryPoint.cs +++ b/MediaBrowser.Controller/Plugins/IServerEntryPoint.cs @@ -4,16 +4,22 @@ using System.Threading.Tasks; namespace MediaBrowser.Controller.Plugins { /// <summary> - /// Interface IServerEntryPoint + /// Represents an entry point for a module in the application. This interface is scanned for automatically and + /// provides a hook to initialize the module at application start. + /// The entry point can additionally be flagged as a pre-startup task by implementing the + /// <see cref="IRunBeforeStartup"/> interface. /// </summary> public interface IServerEntryPoint : IDisposable { /// <summary> - /// Runs this instance. + /// Run the initialization for this module. This method is invoked at application start. /// </summary> Task RunAsync(); } + /// <summary> + /// Indicates that a <see cref="IServerEntryPoint"/> should be invoked as a pre-startup task. + /// </summary> public interface IRunBeforeStartup { diff --git a/MediaBrowser.Controller/Providers/AlbumInfo.cs b/MediaBrowser.Controller/Providers/AlbumInfo.cs index ac6b86c1d8..dbda4843f9 100644 --- a/MediaBrowser.Controller/Providers/AlbumInfo.cs +++ b/MediaBrowser.Controller/Providers/AlbumInfo.cs @@ -16,6 +16,7 @@ namespace MediaBrowser.Controller.Providers /// </summary> /// <value>The artist provider ids.</value> public Dictionary<string, string> ArtistProviderIds { get; set; } + public List<SongInfo> SongInfos { get; set; } public AlbumInfo() diff --git a/MediaBrowser.Controller/Providers/BoxSetInfo.cs b/MediaBrowser.Controller/Providers/BoxSetInfo.cs index 4cbe2d6efd..d23f2b9bf3 100644 --- a/MediaBrowser.Controller/Providers/BoxSetInfo.cs +++ b/MediaBrowser.Controller/Providers/BoxSetInfo.cs @@ -2,6 +2,5 @@ namespace MediaBrowser.Controller.Providers { public class BoxSetInfo : ItemLookupInfo { - } } diff --git a/MediaBrowser.Controller/Providers/DirectoryService.cs b/MediaBrowser.Controller/Providers/DirectoryService.cs index 303b03a216..b7640c2050 100644 --- a/MediaBrowser.Controller/Providers/DirectoryService.cs +++ b/MediaBrowser.Controller/Providers/DirectoryService.cs @@ -26,7 +26,6 @@ namespace MediaBrowser.Controller.Providers { entries = _fileSystem.GetFileSystemEntries(path).ToArray(); - //_cache.TryAdd(path, entries); _cache[path] = entries; } @@ -56,7 +55,6 @@ namespace MediaBrowser.Controller.Providers if (file != null && file.Exists) { - //_fileCache.TryAdd(path, file); _fileCache[path] = file; } else @@ -66,15 +64,12 @@ namespace MediaBrowser.Controller.Providers } return file; - //return _fileSystem.GetFileInfo(path); } - public List<string> GetFilePaths(string path) - { - return GetFilePaths(path, false); - } + public IReadOnlyList<string> GetFilePaths(string path) + => GetFilePaths(path, false); - public List<string> GetFilePaths(string path, bool clearCache) + public IReadOnlyList<string> GetFilePaths(string path, bool clearCache) { if (clearCache || !_filePathCache.TryGetValue(path, out List<string> result)) { diff --git a/MediaBrowser.Controller/Providers/DynamicImageInfo.cs b/MediaBrowser.Controller/Providers/DynamicImageInfo.cs deleted file mode 100644 index 0791783c63..0000000000 --- a/MediaBrowser.Controller/Providers/DynamicImageInfo.cs +++ /dev/null @@ -1,10 +0,0 @@ -using MediaBrowser.Model.Entities; - -namespace MediaBrowser.Controller.Providers -{ - public class DynamicImageInfo - { - public string ImageId { get; set; } - public ImageType Type { get; set; } - } -} diff --git a/MediaBrowser.Controller/Providers/DynamicImageResponse.cs b/MediaBrowser.Controller/Providers/DynamicImageResponse.cs index 11c7ccbe50..7c1371702e 100644 --- a/MediaBrowser.Controller/Providers/DynamicImageResponse.cs +++ b/MediaBrowser.Controller/Providers/DynamicImageResponse.cs @@ -8,9 +8,13 @@ namespace MediaBrowser.Controller.Providers public class DynamicImageResponse { public string Path { get; set; } + public MediaProtocol Protocol { get; set; } + public Stream Stream { get; set; } + public ImageFormat Format { get; set; } + public bool HasImage { get; set; } public void SetFormatFromMimeType(string mimeType) diff --git a/MediaBrowser.Controller/Providers/EpisodeInfo.cs b/MediaBrowser.Controller/Providers/EpisodeInfo.cs index 6ecf4edc53..55c41ff82c 100644 --- a/MediaBrowser.Controller/Providers/EpisodeInfo.cs +++ b/MediaBrowser.Controller/Providers/EpisodeInfo.cs @@ -10,6 +10,7 @@ namespace MediaBrowser.Controller.Providers public int? IndexNumberEnd { get; set; } public bool IsMissingEpisode { get; set; } + public string SeriesDisplayOrder { get; set; } public EpisodeInfo() diff --git a/MediaBrowser.Controller/Providers/ExtraInfo.cs b/MediaBrowser.Controller/Providers/ExtraInfo.cs deleted file mode 100644 index 413ff2e789..0000000000 --- a/MediaBrowser.Controller/Providers/ExtraInfo.cs +++ /dev/null @@ -1,15 +0,0 @@ -using MediaBrowser.Model.Entities; - -namespace MediaBrowser.Controller.Providers -{ - public class ExtraInfo - { - public string Path { get; set; } - - public LocationType LocationType { get; set; } - - public bool IsDownloadable { get; set; } - - public ExtraType ExtraType { get; set; } - } -} diff --git a/MediaBrowser.Controller/Providers/ExtraSource.cs b/MediaBrowser.Controller/Providers/ExtraSource.cs deleted file mode 100644 index 46b246fbc5..0000000000 --- a/MediaBrowser.Controller/Providers/ExtraSource.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace MediaBrowser.Controller.Providers -{ - public enum ExtraSource - { - Local = 1, - Metadata = 2, - Remote = 3 - } -} diff --git a/MediaBrowser.Controller/Providers/ICustomMetadataProvider.cs b/MediaBrowser.Controller/Providers/ICustomMetadataProvider.cs index 2d5a0b364a..6b4c9feb51 100644 --- a/MediaBrowser.Controller/Providers/ICustomMetadataProvider.cs +++ b/MediaBrowser.Controller/Providers/ICustomMetadataProvider.cs @@ -13,7 +13,7 @@ namespace MediaBrowser.Controller.Providers where TItemType : BaseItem { /// <summary> - /// Fetches the asynchronous. + /// Fetches the metadata asynchronously. /// </summary> /// <param name="item">The item.</param> /// <param name="options">The options.</param> diff --git a/MediaBrowser.Controller/Providers/IDirectoryService.cs b/MediaBrowser.Controller/Providers/IDirectoryService.cs index 8059d2bd13..949a17740b 100644 --- a/MediaBrowser.Controller/Providers/IDirectoryService.cs +++ b/MediaBrowser.Controller/Providers/IDirectoryService.cs @@ -6,10 +6,13 @@ namespace MediaBrowser.Controller.Providers public interface IDirectoryService { FileSystemMetadata[] GetFileSystemEntries(string path); + List<FileSystemMetadata> GetFiles(string path); + FileSystemMetadata GetFile(string path); - List<string> GetFilePaths(string path); - List<string> GetFilePaths(string path, bool clearCache); + IReadOnlyList<string> GetFilePaths(string path); + + IReadOnlyList<string> GetFilePaths(string path, bool clearCache); } } diff --git a/MediaBrowser.Controller/Providers/IExtrasProvider.cs b/MediaBrowser.Controller/Providers/IExtrasProvider.cs deleted file mode 100644 index fa31635cb1..0000000000 --- a/MediaBrowser.Controller/Providers/IExtrasProvider.cs +++ /dev/null @@ -1,20 +0,0 @@ -using MediaBrowser.Controller.Entities; - -namespace MediaBrowser.Controller.Providers -{ - public interface IExtrasProvider - { - /// <summary> - /// Gets the name. - /// </summary> - /// <value>The name.</value> - string Name { get; } - - /// <summary> - /// Supportses the specified item. - /// </summary> - /// <param name="item">The item.</param> - /// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns> - bool Supports(BaseItem item); - } -} diff --git a/MediaBrowser.Controller/Providers/IForcedProvider.cs b/MediaBrowser.Controller/Providers/IForcedProvider.cs index 35fa29d94d..5ae4a56efc 100644 --- a/MediaBrowser.Controller/Providers/IForcedProvider.cs +++ b/MediaBrowser.Controller/Providers/IForcedProvider.cs @@ -1,7 +1,7 @@ namespace MediaBrowser.Controller.Providers { /// <summary> - /// This is a marker interface that will cause a provider to run even if IsLocked=true + /// This is a marker interface that will cause a provider to run even if an item is locked from changes. /// </summary> public interface IForcedProvider { diff --git a/MediaBrowser.Controller/Providers/IImageEnhancer.cs b/MediaBrowser.Controller/Providers/IImageEnhancer.cs deleted file mode 100644 index c27c00ca26..0000000000 --- a/MediaBrowser.Controller/Providers/IImageEnhancer.cs +++ /dev/null @@ -1,61 +0,0 @@ -using System.Threading.Tasks; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Model.Drawing; -using MediaBrowser.Model.Entities; - -namespace MediaBrowser.Controller.Providers -{ - public interface IImageEnhancer - { - /// <summary> - /// Return true only if the given image for the given item will be enhanced by this enhancer. - /// </summary> - /// <param name="item">The item.</param> - /// <param name="imageType">Type of the image.</param> - /// <returns><c>true</c> if this enhancer will enhance the supplied image for the supplied item, <c>false</c> otherwise</returns> - bool Supports(BaseItem item, ImageType imageType); - - /// <summary> - /// Gets the priority or order in which this enhancer should be run. - /// </summary> - /// <value>The priority.</value> - MetadataProviderPriority Priority { get; } - - /// <summary> - /// Return a key incorporating all configuration information related to this item - /// </summary> - /// <param name="item">The item.</param> - /// <param name="imageType">Type of the image.</param> - /// <returns>Cache key relating to the current state of this item and configuration</returns> - string GetConfigurationCacheKey(BaseItem item, ImageType imageType); - - /// <summary> - /// Gets the size of the enhanced image. - /// </summary> - /// <param name="item">The item.</param> - /// <param name="imageType">Type of the image.</param> - /// <param name="imageIndex">Index of the image.</param> - /// <param name="originalImageSize">Size of the original image.</param> - /// <returns>ImageSize.</returns> - ImageDimensions GetEnhancedImageSize(BaseItem item, ImageType imageType, int imageIndex, ImageDimensions originalImageSize); - - EnhancedImageInfo GetEnhancedImageInfo(BaseItem item, string inputFile, ImageType imageType, int imageIndex); - - /// <summary> - /// Enhances the image async. - /// </summary> - /// <param name="item">The item.</param> - /// <param name="inputFile">The input file.</param> - /// <param name="outputFile">The output file.</param> - /// <param name="imageType">Type of the image.</param> - /// <param name="imageIndex">Index of the image.</param> - /// <returns>Task{Image}.</returns> - /// <exception cref="System.ArgumentNullException"></exception> - Task EnhanceImageAsync(BaseItem item, string inputFile, string outputFile, ImageType imageType, int imageIndex); - } - - public class EnhancedImageInfo - { - public bool RequiresTransparency { get; set; } - } -} diff --git a/MediaBrowser.Controller/Providers/IImageProvider.cs b/MediaBrowser.Controller/Providers/IImageProvider.cs index 2df3d5ff8d..29ab323f8d 100644 --- a/MediaBrowser.Controller/Providers/IImageProvider.cs +++ b/MediaBrowser.Controller/Providers/IImageProvider.cs @@ -3,7 +3,7 @@ using MediaBrowser.Controller.Entities; namespace MediaBrowser.Controller.Providers { /// <summary> - /// Interface IImageProvider + /// Interface IImageProvider. /// </summary> public interface IImageProvider { @@ -14,10 +14,10 @@ namespace MediaBrowser.Controller.Providers string Name { get; } /// <summary> - /// Supportses the specified item. + /// Supports the specified item. /// </summary> /// <param name="item">The item.</param> - /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns> + /// <returns><c>true</c> if the provider supports the item.</returns> bool Supports(BaseItem item); } } diff --git a/MediaBrowser.Controller/Providers/ILocalImageFileProvider.cs b/MediaBrowser.Controller/Providers/ILocalImageFileProvider.cs deleted file mode 100644 index 72bc563909..0000000000 --- a/MediaBrowser.Controller/Providers/ILocalImageFileProvider.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Collections.Generic; -using MediaBrowser.Controller.Entities; - -namespace MediaBrowser.Controller.Providers -{ - public interface ILocalImageFileProvider : ILocalImageProvider - { - List<LocalImageInfo> GetImages(BaseItem item, IDirectoryService directoryService); - } -} diff --git a/MediaBrowser.Controller/Providers/ILocalImageProvider.cs b/MediaBrowser.Controller/Providers/ILocalImageProvider.cs index 09aaab3de9..463c81376d 100644 --- a/MediaBrowser.Controller/Providers/ILocalImageProvider.cs +++ b/MediaBrowser.Controller/Providers/ILocalImageProvider.cs @@ -1,9 +1,13 @@ +using System.Collections.Generic; +using MediaBrowser.Controller.Entities; + namespace MediaBrowser.Controller.Providers { /// <summary> - /// This is just a marker interface + /// This is just a marker interface. /// </summary> public interface ILocalImageProvider : IImageProvider { + List<LocalImageInfo> GetImages(BaseItem item, IDirectoryService directoryService); } } diff --git a/MediaBrowser.Controller/Providers/ILocalMetadataProvider.cs b/MediaBrowser.Controller/Providers/ILocalMetadataProvider.cs index 2a2c379f60..44fb1b394f 100644 --- a/MediaBrowser.Controller/Providers/ILocalMetadataProvider.cs +++ b/MediaBrowser.Controller/Providers/ILocalMetadataProvider.cs @@ -17,8 +17,9 @@ namespace MediaBrowser.Controller.Providers /// <param name="info">The information.</param> /// <param name="directoryService">The directory service.</param> /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task{MetadataResult{`0}}.</returns> - Task<MetadataResult<TItemType>> GetMetadata(ItemInfo info, + /// <returns>Task{MetadataResult{0}}.</returns> + Task<MetadataResult<TItemType>> GetMetadata( + ItemInfo info, IDirectoryService directoryService, CancellationToken cancellationToken); } diff --git a/MediaBrowser.Controller/Providers/IMetadataService.cs b/MediaBrowser.Controller/Providers/IMetadataService.cs index 49f6a78306..21204e6d31 100644 --- a/MediaBrowser.Controller/Providers/IMetadataService.cs +++ b/MediaBrowser.Controller/Providers/IMetadataService.cs @@ -12,8 +12,9 @@ namespace MediaBrowser.Controller.Providers /// Determines whether this instance can refresh the specified item. /// </summary> /// <param name="item">The item.</param> - /// <returns><c>true</c> if this instance can refresh the specified item; otherwise, <c>false</c>.</returns> + /// <returns><c>true</c> if this instance can refresh the specified item.</returns> bool CanRefresh(BaseItem item); + bool CanRefreshPrimary(Type type); /// <summary> diff --git a/MediaBrowser.Controller/Providers/IPreRefreshProvider.cs b/MediaBrowser.Controller/Providers/IPreRefreshProvider.cs index 058010e1a4..28da27ae77 100644 --- a/MediaBrowser.Controller/Providers/IPreRefreshProvider.cs +++ b/MediaBrowser.Controller/Providers/IPreRefreshProvider.cs @@ -2,6 +2,5 @@ namespace MediaBrowser.Controller.Providers { public interface IPreRefreshProvider : ICustomMetadataProvider { - } } diff --git a/MediaBrowser.Controller/Providers/IProviderManager.cs b/MediaBrowser.Controller/Providers/IProviderManager.cs index 925ace8952..254b274601 100644 --- a/MediaBrowser.Controller/Providers/IProviderManager.cs +++ b/MediaBrowser.Controller/Providers/IProviderManager.cs @@ -14,7 +14,7 @@ using MediaBrowser.Model.Providers; namespace MediaBrowser.Controller.Providers { /// <summary> - /// Interface IProviderManager + /// Interface IProviderManager. /// </summary> public interface IProviderManager { @@ -159,13 +159,17 @@ namespace MediaBrowser.Controller.Providers Dictionary<Guid, Guid> GetRefreshQueue(); void OnRefreshStart(BaseItem item); + void OnRefreshProgress(BaseItem item, double progress); + void OnRefreshComplete(BaseItem item); double? GetRefreshProgress(Guid id); event EventHandler<GenericEventArgs<BaseItem>> RefreshStarted; + event EventHandler<GenericEventArgs<BaseItem>> RefreshCompleted; + event EventHandler<GenericEventArgs<Tuple<BaseItem, double>>> RefreshProgress; } diff --git a/MediaBrowser.Controller/Providers/IRemoteImageProvider.cs b/MediaBrowser.Controller/Providers/IRemoteImageProvider.cs index e56bba3e3f..68a968f901 100644 --- a/MediaBrowser.Controller/Providers/IRemoteImageProvider.cs +++ b/MediaBrowser.Controller/Providers/IRemoteImageProvider.cs @@ -9,7 +9,7 @@ using MediaBrowser.Model.Providers; namespace MediaBrowser.Controller.Providers { /// <summary> - /// Interface IImageProvider + /// Interface IImageProvider. /// </summary> public interface IRemoteImageProvider : IImageProvider { diff --git a/MediaBrowser.Controller/Providers/ItemInfo.cs b/MediaBrowser.Controller/Providers/ItemInfo.cs index f29a8aa706..d61153dfa6 100644 --- a/MediaBrowser.Controller/Providers/ItemInfo.cs +++ b/MediaBrowser.Controller/Providers/ItemInfo.cs @@ -23,10 +23,15 @@ namespace MediaBrowser.Controller.Providers } public Type ItemType { get; set; } + public string Path { get; set; } + public string ContainingFolderPath { get; set; } + public VideoType VideoType { get; set; } + public bool IsInMixedFolder { get; set; } + public bool IsPlaceHolder { get; set; } } } diff --git a/MediaBrowser.Controller/Providers/ItemLookupInfo.cs b/MediaBrowser.Controller/Providers/ItemLookupInfo.cs index 0aaab9a944..4707b0c7fc 100644 --- a/MediaBrowser.Controller/Providers/ItemLookupInfo.cs +++ b/MediaBrowser.Controller/Providers/ItemLookupInfo.cs @@ -11,29 +11,37 @@ namespace MediaBrowser.Controller.Providers /// </summary> /// <value>The name.</value> public string Name { get; set; } + /// <summary> /// Gets or sets the metadata language. /// </summary> /// <value>The metadata language.</value> public string MetadataLanguage { get; set; } + /// <summary> /// Gets or sets the metadata country code. /// </summary> /// <value>The metadata country code.</value> public string MetadataCountryCode { get; set; } + /// <summary> /// Gets or sets the provider ids. /// </summary> /// <value>The provider ids.</value> public Dictionary<string, string> ProviderIds { get; set; } + /// <summary> /// Gets or sets the year. /// </summary> /// <value>The year.</value> public int? Year { get; set; } + public int? IndexNumber { get; set; } + public int? ParentIndexNumber { get; set; } + public DateTime? PremiereDate { get; set; } + public bool IsAutomated { get; set; } public ItemLookupInfo() diff --git a/MediaBrowser.Controller/Providers/LocalImageInfo.cs b/MediaBrowser.Controller/Providers/LocalImageInfo.cs index 24cded79ba..1842810255 100644 --- a/MediaBrowser.Controller/Providers/LocalImageInfo.cs +++ b/MediaBrowser.Controller/Providers/LocalImageInfo.cs @@ -6,6 +6,7 @@ namespace MediaBrowser.Controller.Providers public class LocalImageInfo { public FileSystemMetadata FileInfo { get; set; } + public ImageType Type { get; set; } } } diff --git a/MediaBrowser.Controller/Providers/MetadataProviderPriority.cs b/MediaBrowser.Controller/Providers/MetadataProviderPriority.cs deleted file mode 100644 index 0076bb9721..0000000000 --- a/MediaBrowser.Controller/Providers/MetadataProviderPriority.cs +++ /dev/null @@ -1,39 +0,0 @@ -namespace MediaBrowser.Controller.Providers -{ - /// <summary> - /// Determines when a provider should execute, relative to others - /// </summary> - public enum MetadataProviderPriority - { - // Run this provider at the beginning - /// <summary> - /// The first - /// </summary> - First = 1, - - // Run this provider after all first priority providers - /// <summary> - /// The second - /// </summary> - Second = 2, - - // Run this provider after all second priority providers - /// <summary> - /// The third - /// </summary> - Third = 3, - - /// <summary> - /// The fourth - /// </summary> - Fourth = 4, - - Fifth = 5, - - // Run this provider last - /// <summary> - /// The last - /// </summary> - Last = 999 - } -} diff --git a/MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs b/MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs index b3eb8cdd16..0a473b80ca 100644 --- a/MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs +++ b/MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs @@ -13,11 +13,13 @@ namespace MediaBrowser.Controller.Providers public bool ReplaceAllMetadata { get; set; } public MetadataRefreshMode MetadataRefreshMode { get; set; } + public RemoteSearchResult SearchResult { get; set; } public string[] RefreshPaths { get; set; } public bool ForceSave { get; set; } + public bool EnableRemoteContentProbe { get; set; } public MetadataRefreshOptions(IDirectoryService directoryService) diff --git a/MediaBrowser.Controller/Providers/MetadataResult.cs b/MediaBrowser.Controller/Providers/MetadataResult.cs index ebff81b7f8..59adaedfa9 100644 --- a/MediaBrowser.Controller/Providers/MetadataResult.cs +++ b/MediaBrowser.Controller/Providers/MetadataResult.cs @@ -8,6 +8,7 @@ namespace MediaBrowser.Controller.Providers public class MetadataResult<T> { public List<LocalImageInfo> Images { get; set; } + public List<UserItemData> UserDataList { get; set; } public MetadataResult() @@ -19,10 +20,15 @@ namespace MediaBrowser.Controller.Providers public List<PersonInfo> People { get; set; } public bool HasMetadata { get; set; } + public T Item { get; set; } + public string ResultLanguage { get; set; } + public string Provider { get; set; } + public bool QueriedById { get; set; } + public void AddPerson(PersonInfo p) { if (People == null) diff --git a/MediaBrowser.Controller/Providers/MovieInfo.cs b/MediaBrowser.Controller/Providers/MovieInfo.cs index c9a766fe7a..5b2c3ed03b 100644 --- a/MediaBrowser.Controller/Providers/MovieInfo.cs +++ b/MediaBrowser.Controller/Providers/MovieInfo.cs @@ -2,6 +2,5 @@ namespace MediaBrowser.Controller.Providers { public class MovieInfo : ItemLookupInfo { - } } diff --git a/MediaBrowser.Controller/Providers/PersonLookupInfo.cs b/MediaBrowser.Controller/Providers/PersonLookupInfo.cs index 3fa6e50b7f..a6218c039a 100644 --- a/MediaBrowser.Controller/Providers/PersonLookupInfo.cs +++ b/MediaBrowser.Controller/Providers/PersonLookupInfo.cs @@ -2,6 +2,5 @@ namespace MediaBrowser.Controller.Providers { public class PersonLookupInfo : ItemLookupInfo { - } } diff --git a/MediaBrowser.Controller/Providers/RemoteSearchQuery.cs b/MediaBrowser.Controller/Providers/RemoteSearchQuery.cs index 078125673f..a2ac6c9ae0 100644 --- a/MediaBrowser.Controller/Providers/RemoteSearchQuery.cs +++ b/MediaBrowser.Controller/Providers/RemoteSearchQuery.cs @@ -10,14 +10,14 @@ namespace MediaBrowser.Controller.Providers public Guid ItemId { get; set; } /// <summary> - /// If set will only search within the given provider + /// Will only search within the given provider when set. /// </summary> public string SearchProviderName { get; set; } /// <summary> - /// Gets or sets a value indicating whether [include disabled providers]. + /// Gets or sets a value indicating whether disabled providers should be included. /// </summary> - /// <value><c>true</c> if [include disabled providers]; otherwise, <c>false</c>.</value> + /// <value><c>true</c> if disabled providers should be included.</value> public bool IncludeDisabledProviders { get; set; } } } diff --git a/MediaBrowser.Controller/Session/SessionInfo.cs b/MediaBrowser.Controller/Session/SessionInfo.cs index 63ed43a833..2ba7c9fec0 100644 --- a/MediaBrowser.Controller/Session/SessionInfo.cs +++ b/MediaBrowser.Controller/Session/SessionInfo.cs @@ -115,6 +115,8 @@ namespace MediaBrowser.Controller.Session public BaseItem FullNowPlayingItem { get; set; } + public BaseItemDto NowViewingItem { get; set; } + /// <summary> /// Gets or sets the device id. /// </summary> @@ -239,11 +241,6 @@ namespace MediaBrowser.Controller.Session SessionControllers = controllers.ToArray(); } - public bool ContainsUser(string userId) - { - return ContainsUser(new Guid(userId)); - } - public bool ContainsUser(Guid userId) { if (UserId.Equals(userId)) @@ -253,7 +250,7 @@ namespace MediaBrowser.Controller.Session foreach (var additionalUser in AdditionalUsers) { - if (userId.Equals(userId)) + if (additionalUser.UserId.Equals(userId)) { return true; } @@ -308,7 +305,7 @@ namespace MediaBrowser.Controller.Session var newPositionTicks = positionTicks + ProgressIncrement; var item = progressInfo.Item; - long? runtimeTicks = item == null ? null : item.RunTimeTicks; + long? runtimeTicks = item?.RunTimeTicks; // Don't report beyond the runtime if (runtimeTicks.HasValue && newPositionTicks >= runtimeTicks.Value) diff --git a/MediaBrowser.Controller/Sorting/AlphanumComparator.cs b/MediaBrowser.Controller/Sorting/AlphanumComparator.cs new file mode 100644 index 0000000000..de7f72d1c2 --- /dev/null +++ b/MediaBrowser.Controller/Sorting/AlphanumComparator.cs @@ -0,0 +1,135 @@ +#nullable enable + +using System; +using System.Collections.Generic; + +namespace MediaBrowser.Controller.Sorting +{ + public class AlphanumComparator : IComparer<string?> + { + public static int CompareValues(string? s1, string? s2) + { + if (s1 == null && s2 == null) + { + return 0; + } + else if (s1 == null) + { + return -1; + } + else if (s2 == null) + { + return 1; + } + + int len1 = s1.Length; + int len2 = s2.Length; + + // Early return for empty strings + if (len1 == 0 && len2 == 0) + { + return 0; + } + else if (len1 == 0) + { + return -1; + } + else if (len2 == 0) + { + return 1; + } + + int pos1 = 0; + int pos2 = 0; + + do + { + int start1 = pos1; + int start2 = pos2; + + bool isNum1 = char.IsDigit(s1[pos1++]); + bool isNum2 = char.IsDigit(s2[pos2++]); + + while (pos1 < len1 && char.IsDigit(s1[pos1]) == isNum1) + { + pos1++; + } + + while (pos2 < len2 && char.IsDigit(s2[pos2]) == isNum2) + { + pos2++; + } + + var span1 = s1.AsSpan(start1, pos1 - start1); + var span2 = s2.AsSpan(start2, pos2 - start2); + + if (isNum1 && isNum2) + { + // Trim leading zeros so we can compare the length + // of the strings to find the largest number + span1 = span1.TrimStart('0'); + span2 = span2.TrimStart('0'); + var span1Len = span1.Length; + var span2Len = span2.Length; + if (span1Len < span2Len) + { + return -1; + } + else if (span1Len > span2Len) + { + return 1; + } + else if (span1Len >= 20) // Number is probably too big for a ulong + { + // Trim all the first digits that are the same + int i = 0; + while (i < span1Len && span1[i] == span2[i]) + { + i++; + } + + // If there are no more digits it's the same number + if (i == span1Len) + { + continue; + } + + // Only need to compare the most significant digit + span1 = span1.Slice(i, 1); + span2 = span2.Slice(i, 1); + } + + if (!ulong.TryParse(span1, out var num1) + || !ulong.TryParse(span2, out var num2)) + { + return 0; + } + else if (num1 < num2) + { + return -1; + } + else if (num1 > num2) + { + return 1; + } + } + else + { + int result = span1.CompareTo(span2, StringComparison.InvariantCulture); + if (result != 0) + { + return result; + } + } + } while (pos1 < len1 && pos2 < len2); + + return len1 - len2; + } + + /// <inheritdoc /> + public int Compare(string x, string y) + { + return CompareValues(x, y); + } + } +} diff --git a/MediaBrowser.Controller/Sorting/SortExtensions.cs b/MediaBrowser.Controller/Sorting/SortExtensions.cs index 111f4f17fc..2a68f46789 100644 --- a/MediaBrowser.Controller/Sorting/SortExtensions.cs +++ b/MediaBrowser.Controller/Sorting/SortExtensions.cs @@ -1,143 +1,30 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Text; namespace MediaBrowser.Controller.Sorting { public static class SortExtensions { + private static readonly AlphanumComparator _comparer = new AlphanumComparator(); public static IEnumerable<T> OrderByString<T>(this IEnumerable<T> list, Func<T, string> getName) { - return list.OrderBy(getName, new AlphanumComparator()); + return list.OrderBy(getName, _comparer); } public static IEnumerable<T> OrderByStringDescending<T>(this IEnumerable<T> list, Func<T, string> getName) { - return list.OrderByDescending(getName, new AlphanumComparator()); + return list.OrderByDescending(getName, _comparer); } public static IOrderedEnumerable<T> ThenByString<T>(this IOrderedEnumerable<T> list, Func<T, string> getName) { - return list.ThenBy(getName, new AlphanumComparator()); + return list.ThenBy(getName, _comparer); } public static IOrderedEnumerable<T> ThenByStringDescending<T>(this IOrderedEnumerable<T> list, Func<T, string> getName) { - return list.ThenByDescending(getName, new AlphanumComparator()); - } - - private class AlphanumComparator : IComparer<string> - { - private enum ChunkType { Alphanumeric, Numeric }; - - private static bool InChunk(char ch, char otherCh) - { - var type = ChunkType.Alphanumeric; - - if (char.IsDigit(otherCh)) - { - type = ChunkType.Numeric; - } - - if ((type == ChunkType.Alphanumeric && char.IsDigit(ch)) - || (type == ChunkType.Numeric && !char.IsDigit(ch))) - { - return false; - } - - return true; - } - - public static int CompareValues(string s1, string s2) - { - if (s1 == null || s2 == null) - { - return 0; - } - - int thisMarker = 0, thisNumericChunk = 0; - int thatMarker = 0, thatNumericChunk = 0; - - while ((thisMarker < s1.Length) || (thatMarker < s2.Length)) - { - if (thisMarker >= s1.Length) - { - return -1; - } - else if (thatMarker >= s2.Length) - { - return 1; - } - char thisCh = s1[thisMarker]; - char thatCh = s2[thatMarker]; - - var thisChunk = new StringBuilder(); - var thatChunk = new StringBuilder(); - - while ((thisMarker < s1.Length) && (thisChunk.Length == 0 || InChunk(thisCh, thisChunk[0]))) - { - thisChunk.Append(thisCh); - thisMarker++; - - if (thisMarker < s1.Length) - { - thisCh = s1[thisMarker]; - } - } - - while ((thatMarker < s2.Length) && (thatChunk.Length == 0 || InChunk(thatCh, thatChunk[0]))) - { - thatChunk.Append(thatCh); - thatMarker++; - - if (thatMarker < s2.Length) - { - thatCh = s2[thatMarker]; - } - } - - int result = 0; - // If both chunks contain numeric characters, sort them numerically - if (char.IsDigit(thisChunk[0]) && char.IsDigit(thatChunk[0])) - { - if (!int.TryParse(thisChunk.ToString(), out thisNumericChunk)) - { - return 0; - } - if (!int.TryParse(thatChunk.ToString(), out thatNumericChunk)) - { - return 0; - } - - if (thisNumericChunk < thatNumericChunk) - { - result = -1; - } - - if (thisNumericChunk > thatNumericChunk) - { - result = 1; - } - } - else - { - result = thisChunk.ToString().CompareTo(thatChunk.ToString()); - } - - if (result != 0) - { - return result; - } - } - - return 0; - } - - public int Compare(string x, string y) - { - return CompareValues(x, y); - } + return list.ThenByDescending(getName, _comparer); } } } diff --git a/MediaBrowser.Controller/Sorting/SortHelper.cs b/MediaBrowser.Controller/Sorting/SortHelper.cs deleted file mode 100644 index 05981d9757..0000000000 --- a/MediaBrowser.Controller/Sorting/SortHelper.cs +++ /dev/null @@ -1,25 +0,0 @@ -namespace MediaBrowser.Controller.Sorting -{ - public static class SortHelper - { - private enum ChunkType { Alphanumeric, Numeric }; - - public static bool InChunk(char ch, char otherCh) - { - var type = ChunkType.Alphanumeric; - - if (char.IsDigit(otherCh)) - { - type = ChunkType.Numeric; - } - - if ((type == ChunkType.Alphanumeric && char.IsDigit(ch)) - || (type == ChunkType.Numeric && !char.IsDigit(ch))) - { - return false; - } - - return true; - } - } -} |
