diff --git a/.ci/azure-pipelines-package.yml b/.ci/azure-pipelines-package.yml index 81693452f..89f7137fd 100644 --- a/.ci/azure-pipelines-package.yml +++ b/.ci/azure-pipelines-package.yml @@ -39,6 +39,14 @@ jobs: vmImage: 'ubuntu-latest' steps: + - script: echo "##vso[task.setvariable variable=JellyfinVersion]$( awk -F '/' '{ print $NF }' <<<'$(Build.SourceBranch)' | sed 's/^v//' )" + displayName: Set release version (stable) + condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v') + + - script: './bump-version $(JellyfinVersion)' + displayName: Bump internal version (stable) + condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v') + - script: 'docker build -f deployment/Dockerfile.$(BuildConfiguration) -t jellyfin-server-$(BuildConfiguration) deployment' displayName: 'Build Dockerfile' @@ -80,6 +88,14 @@ jobs: vmImage: 'ubuntu-latest' steps: + - script: echo "##vso[task.setvariable variable=JellyfinVersion]$( awk -F '/' '{ print $NF }' <<<'$(Build.SourceBranch)' | sed 's/^v//' )" + displayName: Set release version (stable) + condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v') + + - script: './bump-version $(JellyfinVersion)' + displayName: Bump internal version (stable) + condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v') + - task: DownloadPipelineArtifact@2 displayName: 'Download OpenAPI Spec' inputs: @@ -127,6 +143,10 @@ jobs: displayName: Set release version (stable) condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v') + - script: './bump-version $(JellyfinVersion)' + displayName: Bump internal version (stable) + condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v') + - task: Docker@2 displayName: 'Push Unstable Image' condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master') diff --git a/.copr b/.copr new file mode 120000 index 000000000..100fe0cd7 --- /dev/null +++ b/.copr @@ -0,0 +1 @@ +fedora \ No newline at end of file diff --git a/.copr/Makefile b/.copr/Makefile deleted file mode 120000 index ec3c90dfd..000000000 --- a/.copr/Makefile +++ /dev/null @@ -1 +0,0 @@ -../fedora/Makefile \ No newline at end of file diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 5c4a031bc..d52e13324 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -150,6 +150,7 @@ - [ianjazz246](https://github.com/ianjazz246) - [peterspenler](https://github.com/peterspenler) - [MBR-0001](https://github.com/MBR-0001) + - [jonas-resch](https://github.com/jonas-resch) # Emby Contributors diff --git a/Emby.Dlna/ContentDirectory/ControlHandler.cs b/Emby.Dlna/ContentDirectory/ControlHandler.cs index 26a816107..fde3f2f89 100644 --- a/Emby.Dlna/ContentDirectory/ControlHandler.cs +++ b/Emby.Dlna/ContentDirectory/ControlHandler.cs @@ -18,11 +18,8 @@ using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; -using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.MediaEncoding; -using MediaBrowser.Controller.Playlists; using MediaBrowser.Controller.TV; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Dto; @@ -30,12 +27,7 @@ using MediaBrowser.Model.Entities; using MediaBrowser.Model.Globalization; using MediaBrowser.Model.Querying; using Microsoft.Extensions.Logging; -using Book = MediaBrowser.Controller.Entities.Book; -using Episode = MediaBrowser.Controller.Entities.TV.Episode; using Genre = MediaBrowser.Controller.Entities.Genre; -using Movie = MediaBrowser.Controller.Entities.Movies.Movie; -using MusicAlbum = MediaBrowser.Controller.Entities.Audio.MusicAlbum; -using Series = MediaBrowser.Controller.Entities.TV.Series; namespace Emby.Dlna.ContentDirectory { @@ -539,7 +531,7 @@ namespace Emby.Dlna.ContentDirectory User = user, Recursive = true, IsMissing = false, - ExcludeItemTypes = new[] { nameof(Book) }, + ExcludeItemTypes = new[] { BaseItemKind.Book }, IsFolder = isFolder, MediaTypes = mediaTypes, DtoOptions = GetDtoOptions() @@ -619,7 +611,7 @@ namespace Emby.Dlna.ContentDirectory Limit = limit, StartIndex = startIndex, IsVirtualItem = false, - ExcludeItemTypes = new[] { nameof(Book) }, + ExcludeItemTypes = new[] { BaseItemKind.Book }, IsPlaceHolder = false, DtoOptions = GetDtoOptions(), OrderBy = GetOrderBy(sort, folder.IsPreSorted) @@ -644,7 +636,7 @@ namespace Emby.Dlna.ContentDirectory { StartIndex = startIndex, Limit = limit, - IncludeItemTypes = new[] { nameof(LiveTvChannel) }, + IncludeItemTypes = new[] { BaseItemKind.LiveTvChannel }, OrderBy = GetOrderBy(sort, false) }; @@ -675,23 +667,23 @@ namespace Emby.Dlna.ContentDirectory switch (stubType) { case StubType.Latest: - return GetLatest(item, query, nameof(Audio)); + return GetLatest(item, query, BaseItemKind.Audio); case StubType.Playlists: return GetMusicPlaylists(query); case StubType.Albums: - return GetChildrenOfItem(item, query, nameof(MusicAlbum)); + return GetChildrenOfItem(item, query, BaseItemKind.MusicAlbum); case StubType.Artists: return GetMusicArtists(item, query); case StubType.AlbumArtists: return GetMusicAlbumArtists(item, query); case StubType.FavoriteAlbums: - return GetChildrenOfItem(item, query, nameof(MusicAlbum), true); + return GetChildrenOfItem(item, query, BaseItemKind.MusicAlbum, true); case StubType.FavoriteArtists: return GetFavoriteArtists(item, query); case StubType.FavoriteSongs: - return GetChildrenOfItem(item, query, nameof(Audio), true); + return GetChildrenOfItem(item, query, BaseItemKind.Audio, true); case StubType.Songs: - return GetChildrenOfItem(item, query, nameof(Audio)); + return GetChildrenOfItem(item, query, BaseItemKind.Audio); case StubType.Genres: return GetMusicGenres(item, query); } @@ -746,13 +738,13 @@ namespace Emby.Dlna.ContentDirectory case StubType.ContinueWatching: return GetMovieContinueWatching(item, query); case StubType.Latest: - return GetLatest(item, query, nameof(Movie)); + return GetLatest(item, query, BaseItemKind.Movie); case StubType.Movies: - return GetChildrenOfItem(item, query, nameof(Movie)); + return GetChildrenOfItem(item, query, BaseItemKind.Movie); case StubType.Collections: return GetMovieCollections(query); case StubType.Favorites: - return GetChildrenOfItem(item, query, nameof(Movie), true); + return GetChildrenOfItem(item, query, BaseItemKind.Movie, true); case StubType.Genres: return GetGenres(item, query); } @@ -831,13 +823,13 @@ namespace Emby.Dlna.ContentDirectory case StubType.NextUp: return GetNextUp(item, query); case StubType.Latest: - return GetLatest(item, query, nameof(Episode)); + return GetLatest(item, query, BaseItemKind.Episode); case StubType.Series: - return GetChildrenOfItem(item, query, nameof(Series)); + return GetChildrenOfItem(item, query, BaseItemKind.Series); case StubType.FavoriteSeries: - return GetChildrenOfItem(item, query, nameof(Series), true); + return GetChildrenOfItem(item, query, BaseItemKind.Series, true); case StubType.FavoriteEpisodes: - return GetChildrenOfItem(item, query, nameof(Episode), true); + return GetChildrenOfItem(item, query, BaseItemKind.Episode, true); case StubType.Genres: return GetGenres(item, query); } @@ -898,7 +890,7 @@ namespace Emby.Dlna.ContentDirectory private QueryResult GetMovieCollections(InternalItemsQuery query) { query.Recursive = true; - query.IncludeItemTypes = new[] { nameof(BoxSet) }; + query.IncludeItemTypes = new[] { BaseItemKind.BoxSet }; var result = _libraryManager.GetItemsResult(query); @@ -913,7 +905,7 @@ namespace Emby.Dlna.ContentDirectory /// The item type. /// A value indicating whether to only fetch favorite items. /// The . - private QueryResult GetChildrenOfItem(BaseItem parent, InternalItemsQuery query, string itemType, bool isFavorite = false) + private QueryResult GetChildrenOfItem(BaseItem parent, InternalItemsQuery query, BaseItemKind itemType, bool isFavorite = false) { query.Recursive = true; query.Parent = parent; @@ -1013,7 +1005,7 @@ namespace Emby.Dlna.ContentDirectory private QueryResult GetMusicPlaylists(InternalItemsQuery query) { query.Parent = null; - query.IncludeItemTypes = new[] { nameof(Playlist) }; + query.IncludeItemTypes = new[] { BaseItemKind.Playlist }; query.Recursive = true; var result = _libraryManager.GetItemsResult(query); @@ -1052,7 +1044,7 @@ namespace Emby.Dlna.ContentDirectory /// The . /// The item type. /// The . - private QueryResult GetLatest(BaseItem parent, InternalItemsQuery query, string itemType) + private QueryResult GetLatest(BaseItem parent, InternalItemsQuery query, BaseItemKind itemType) { query.OrderBy = Array.Empty<(string, SortOrder)>(); @@ -1086,7 +1078,7 @@ namespace Emby.Dlna.ContentDirectory { Recursive = true, ArtistIds = new[] { item.Id }, - IncludeItemTypes = new[] { nameof(MusicAlbum) }, + IncludeItemTypes = new[] { BaseItemKind.MusicAlbum }, Limit = limit, StartIndex = startIndex, DtoOptions = GetDtoOptions(), @@ -1115,8 +1107,8 @@ namespace Emby.Dlna.ContentDirectory GenreIds = new[] { item.Id }, IncludeItemTypes = new[] { - nameof(Movie), - nameof(Series) + BaseItemKind.Movie, + BaseItemKind.Series }, Limit = limit, StartIndex = startIndex, @@ -1144,7 +1136,7 @@ namespace Emby.Dlna.ContentDirectory { Recursive = true, GenreIds = new[] { item.Id }, - IncludeItemTypes = new[] { nameof(MusicAlbum) }, + IncludeItemTypes = new[] { BaseItemKind.MusicAlbum }, Limit = limit, StartIndex = startIndex, DtoOptions = GetDtoOptions(), diff --git a/Emby.Dlna/DlnaManager.cs b/Emby.Dlna/DlnaManager.cs index 93efa4b38..d9d2a345a 100644 --- a/Emby.Dlna/DlnaManager.cs +++ b/Emby.Dlna/DlnaManager.cs @@ -5,7 +5,6 @@ using System.Globalization; using System.IO; using System.Linq; using System.Reflection; -using System.Text; using System.Text.Json; using System.Text.RegularExpressions; using System.Threading.Tasks; diff --git a/Emby.Dlna/Main/DlnaEntryPoint.cs b/Emby.Dlna/Main/DlnaEntryPoint.cs index f35d90f21..08f639d93 100644 --- a/Emby.Dlna/Main/DlnaEntryPoint.cs +++ b/Emby.Dlna/Main/DlnaEntryPoint.cs @@ -124,7 +124,7 @@ namespace Emby.Dlna.Main config); Current = this; - var netConfig = config.GetConfiguration("network"); + var netConfig = config.GetConfiguration(NetworkConfigurationStore.StoreKey); _disabled = appHost.ListenWithHttps && netConfig.RequireHttps; if (_disabled && _config.GetDlnaConfiguration().EnableServer) diff --git a/Emby.Naming/AudioBook/AudioBookListResolver.cs b/Emby.Naming/AudioBook/AudioBookListResolver.cs index 1e4a8d2ed..dd8a05bb3 100644 --- a/Emby.Naming/AudioBook/AudioBookListResolver.cs +++ b/Emby.Naming/AudioBook/AudioBookListResolver.cs @@ -14,6 +14,7 @@ namespace Emby.Naming.AudioBook public class AudioBookListResolver { private readonly NamingOptions _options; + private readonly AudioBookResolver _audioBookResolver; /// /// Initializes a new instance of the class. @@ -22,6 +23,7 @@ namespace Emby.Naming.AudioBook public AudioBookListResolver(NamingOptions options) { _options = options; + _audioBookResolver = new AudioBookResolver(_options); } /// @@ -31,21 +33,19 @@ namespace Emby.Naming.AudioBook /// Returns IEnumerable of . public IEnumerable Resolve(IEnumerable files) { - var audioBookResolver = new AudioBookResolver(_options); // File with empty fullname will be sorted out here. var audiobookFileInfos = files - .Select(i => audioBookResolver.Resolve(i.FullName)) + .Select(i => _audioBookResolver.Resolve(i.FullName)) .OfType() .ToList(); - var stackResult = new StackResolver(_options) - .ResolveAudioBooks(audiobookFileInfos); + var stackResult = StackResolver.ResolveAudioBooks(audiobookFileInfos); foreach (var stack in stackResult) { var stackFiles = stack.Files - .Select(i => audioBookResolver.Resolve(i)) + .Select(i => _audioBookResolver.Resolve(i)) .OfType() .ToList(); diff --git a/Emby.Naming/AudioBook/AudioBookResolver.cs b/Emby.Naming/AudioBook/AudioBookResolver.cs index f6ad3601d..183b6c3b1 100644 --- a/Emby.Naming/AudioBook/AudioBookResolver.cs +++ b/Emby.Naming/AudioBook/AudioBookResolver.cs @@ -1,7 +1,7 @@ using System; using System.IO; -using System.Linq; using Emby.Naming.Common; +using Jellyfin.Extensions; namespace Emby.Naming.AudioBook { @@ -37,7 +37,7 @@ namespace Emby.Naming.AudioBook var extension = Path.GetExtension(path); // Check supported extensions - if (!_options.AudioFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase)) + if (!_options.AudioFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase)) { return null; } diff --git a/Emby.Naming/Common/NamingOptions.cs b/Emby.Naming/Common/NamingOptions.cs index 7bc9fbce8..aa62a47f1 100644 --- a/Emby.Naming/Common/NamingOptions.cs +++ b/Emby.Naming/Common/NamingOptions.cs @@ -1,6 +1,7 @@ #pragma warning disable CA1819 using System; +using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; using Emby.Naming.Video; @@ -124,11 +125,11 @@ namespace Emby.Naming.Common token: "DSR") }; - VideoFileStackingExpressions = new[] + VideoFileStackingRules = new[] { - "(?.*?)(?<volume>[ _.-]*(?:cd|dvd|p(?:ar)?t|dis[ck])[ _.-]*[0-9]+)(?<ignore>.*?)(?<extension>\\.[^.]+)$", - "(?<title>.*?)(?<volume>[ _.-]*(?:cd|dvd|p(?:ar)?t|dis[ck])[ _.-]*[a-d])(?<ignore>.*?)(?<extension>\\.[^.]+)$", - "(?<title>.*?)(?<volume>[ ._-]*[a-d])(?<ignore>.*?)(?<extension>\\.[^.]+)$" + new FileStackRule(@"^(?<filename>.*?)(?:(?<=[\]\)\}])|[ _.-]+)[\(\[]?(?<parttype>cd|dvd|part|pt|dis[ck])[ _.-]*(?<number>[0-9]+)[\)\]]?(?:\.[^.]+)?$", true), + new FileStackRule(@"^(?<filename>.*?)(?:(?<=[\]\)\}])|[ _.-]+)[\(\[]?(?<parttype>cd|dvd|part|pt|dis[ck])[ _.-]*(?<number>[a-d])[\)\]]?(?:\.[^.]+)?$", false), + new FileStackRule(@"^(?<filename>.*?)(?:(?<=[\]\)\}])|[ _.-]?)(?<number>[a-d])(?:\.[^.]+)?$", false) }; CleanDateTimes = new[] @@ -403,6 +404,12 @@ namespace Emby.Naming.Common VideoExtraRules = new[] { + new ExtraRule( + ExtraType.Trailer, + ExtraRuleType.DirectoryName, + "trailers", + MediaType.Video), + new ExtraRule( ExtraType.Trailer, ExtraRuleType.Filename, @@ -469,6 +476,12 @@ namespace Emby.Naming.Common "theme", MediaType.Audio), + new ExtraRule( + ExtraType.ThemeSong, + ExtraRuleType.DirectoryName, + "theme-music", + MediaType.Audio), + new ExtraRule( ExtraType.Scene, ExtraRuleType.Suffix, @@ -563,7 +576,7 @@ namespace Emby.Naming.Common ExtraType.Unknown, ExtraRuleType.DirectoryName, "extras", - MediaType.Video), + MediaType.Video) }; Format3DRules = new[] @@ -675,9 +688,29 @@ namespace Emby.Naming.Common .Distinct(StringComparer.OrdinalIgnoreCase) .ToArray(); + AllExtrasTypesFolderNames = new Dictionary<string, ExtraType>(StringComparer.OrdinalIgnoreCase) + { + ["trailers"] = ExtraType.Trailer, + ["theme-music"] = ExtraType.ThemeSong, + ["backdrops"] = ExtraType.ThemeVideo, + ["extras"] = ExtraType.Unknown, + ["behind the scenes"] = ExtraType.BehindTheScenes, + ["deleted scenes"] = ExtraType.DeletedScene, + ["interviews"] = ExtraType.Interview, + ["scenes"] = ExtraType.Scene, + ["samples"] = ExtraType.Sample, + ["shorts"] = ExtraType.Clip, + ["featurettes"] = ExtraType.Clip + }; + Compile(); } + /// <summary> + /// Gets or sets the folder name to extra types mapping. + /// </summary> + public Dictionary<string, ExtraType> AllExtrasTypesFolderNames { get; set; } + /// <summary> /// Gets or sets list of audio file extensions. /// </summary> @@ -759,9 +792,9 @@ namespace Emby.Naming.Common public Format3DRule[] Format3DRules { get; set; } /// <summary> - /// Gets or sets list of raw video file-stacking expressions strings. + /// Gets the file stacking rules. /// </summary> - public string[] VideoFileStackingExpressions { get; set; } + public FileStackRule[] VideoFileStackingRules { get; } /// <summary> /// Gets or sets list of raw clean DateTimes regular expressions strings. @@ -783,11 +816,6 @@ namespace Emby.Naming.Common /// </summary> public ExtraRule[] VideoExtraRules { get; set; } - /// <summary> - /// Gets list of video file-stack regular expressions. - /// </summary> - public Regex[] VideoFileStackingRegexes { get; private set; } = Array.Empty<Regex>(); - /// <summary> /// Gets list of clean datetime regular expressions. /// </summary> @@ -813,7 +841,6 @@ namespace Emby.Naming.Common /// </summary> public void Compile() { - VideoFileStackingRegexes = VideoFileStackingExpressions.Select(Compile).ToArray(); CleanDateTimeRegexes = CleanDateTimes.Select(Compile).ToArray(); CleanStringRegexes = CleanStrings.Select(Compile).ToArray(); EpisodeWithoutSeasonRegexes = EpisodeWithoutSeasonExpressions.Select(Compile).ToArray(); diff --git a/Emby.Naming/Subtitles/SubtitleParser.cs b/Emby.Naming/Subtitles/SubtitleParser.cs index a19340ef6..5809c512a 100644 --- a/Emby.Naming/Subtitles/SubtitleParser.cs +++ b/Emby.Naming/Subtitles/SubtitleParser.cs @@ -2,6 +2,7 @@ using System; using System.IO; using System.Linq; using Emby.Naming.Common; +using Jellyfin.Extensions; namespace Emby.Naming.Subtitles { @@ -34,7 +35,7 @@ namespace Emby.Naming.Subtitles } var extension = Path.GetExtension(path); - if (!_options.SubtitleFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase)) + if (!_options.SubtitleFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase)) { return null; } @@ -42,11 +43,11 @@ namespace Emby.Naming.Subtitles var flags = GetFlags(path); var info = new SubtitleInfo( path, - _options.SubtitleDefaultFlags.Any(i => flags.Contains(i, StringComparer.OrdinalIgnoreCase)), - _options.SubtitleForcedFlags.Any(i => flags.Contains(i, StringComparer.OrdinalIgnoreCase))); + _options.SubtitleDefaultFlags.Any(i => flags.Contains(i, StringComparison.OrdinalIgnoreCase)), + _options.SubtitleForcedFlags.Any(i => flags.Contains(i, StringComparison.OrdinalIgnoreCase))); - var parts = flags.Where(i => !_options.SubtitleDefaultFlags.Contains(i, StringComparer.OrdinalIgnoreCase) - && !_options.SubtitleForcedFlags.Contains(i, StringComparer.OrdinalIgnoreCase)) + var parts = flags.Where(i => !_options.SubtitleDefaultFlags.Contains(i, StringComparison.OrdinalIgnoreCase) + && !_options.SubtitleForcedFlags.Contains(i, StringComparison.OrdinalIgnoreCase)) .ToList(); // Should have a name, language and file extension diff --git a/Emby.Naming/TV/EpisodeResolver.cs b/Emby.Naming/TV/EpisodeResolver.cs index 5e952e47b..6cebc40c2 100644 --- a/Emby.Naming/TV/EpisodeResolver.cs +++ b/Emby.Naming/TV/EpisodeResolver.cs @@ -1,8 +1,8 @@ using System; using System.IO; -using System.Linq; using Emby.Naming.Common; using Emby.Naming.Video; +using Jellyfin.Extensions; namespace Emby.Naming.TV { @@ -48,7 +48,7 @@ namespace Emby.Naming.TV { var extension = Path.GetExtension(path); // Check supported extensions - if (!_options.VideoFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase)) + if (!_options.VideoFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase)) { // It's not supported. Check stub extensions if (!StubResolver.TryResolveFile(path, _options, out stubType)) diff --git a/Emby.Naming/TV/SeriesPathParser.cs b/Emby.Naming/TV/SeriesPathParser.cs index a62e5f4d6..4dfbb36a3 100644 --- a/Emby.Naming/TV/SeriesPathParser.cs +++ b/Emby.Naming/TV/SeriesPathParser.cs @@ -1,4 +1,3 @@ -using System.Globalization; using Emby.Naming.Common; namespace Emby.Naming.TV diff --git a/Emby.Naming/Video/CleanStringParser.cs b/Emby.Naming/Video/CleanStringParser.cs index b81333500..a336f8fbd 100644 --- a/Emby.Naming/Video/CleanStringParser.cs +++ b/Emby.Naming/Video/CleanStringParser.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Text.RegularExpressions; diff --git a/Emby.Naming/Video/ExtraResolver.cs b/Emby.Naming/Video/ExtraResolver.cs index 7bc226614..fbdca859f 100644 --- a/Emby.Naming/Video/ExtraResolver.cs +++ b/Emby.Naming/Video/ExtraResolver.cs @@ -1,5 +1,7 @@ using System; +using System.Collections.Generic; using System.IO; +using System.Linq; using System.Text.RegularExpressions; using Emby.Naming.Audio; using Emby.Naming.Common; @@ -9,45 +11,27 @@ namespace Emby.Naming.Video /// <summary> /// Resolve if file is extra for video. /// </summary> - public class ExtraResolver + public static class ExtraResolver { - private static readonly char[] _digits = new[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' }; - private readonly NamingOptions _options; - - /// <summary> - /// Initializes a new instance of the <see cref="ExtraResolver"/> class. - /// </summary> - /// <param name="options"><see cref="NamingOptions"/> object containing VideoExtraRules and passed to <see cref="AudioFileParser"/> and <see cref="VideoResolver"/>.</param> - public ExtraResolver(NamingOptions options) - { - _options = options; - } + private static readonly char[] _digits = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' }; /// <summary> /// Attempts to resolve if file is extra. /// </summary> /// <param name="path">Path to file.</param> + /// <param name="namingOptions">The naming options.</param> /// <returns>Returns <see cref="ExtraResult"/> object.</returns> - public ExtraResult GetExtraInfo(string path) + public static ExtraResult GetExtraInfo(string path, NamingOptions namingOptions) { var result = new ExtraResult(); - for (var i = 0; i < _options.VideoExtraRules.Length; i++) + for (var i = 0; i < namingOptions.VideoExtraRules.Length; i++) { - var rule = _options.VideoExtraRules[i]; - if (rule.MediaType == MediaType.Audio) + var rule = namingOptions.VideoExtraRules[i]; + if ((rule.MediaType == MediaType.Audio && !AudioFileParser.IsAudioFile(path, namingOptions)) + || (rule.MediaType == MediaType.Video && !VideoResolver.IsVideoFile(path, namingOptions))) { - if (!AudioFileParser.IsAudioFile(path, _options)) - { - continue; - } - } - else if (rule.MediaType == MediaType.Video) - { - if (!VideoResolver.IsVideoFile(path, _options)) - { - continue; - } + continue; } var pathSpan = path.AsSpan(); @@ -76,9 +60,9 @@ namespace Emby.Naming.Video { var filename = Path.GetFileName(path); - var regex = new Regex(rule.Token, RegexOptions.IgnoreCase); + var isMatch = Regex.IsMatch(filename, rule.Token, RegexOptions.IgnoreCase | RegexOptions.Compiled); - if (regex.IsMatch(filename)) + if (isMatch) { result.ExtraType = rule.ExtraType; result.Rule = rule; @@ -102,5 +86,66 @@ namespace Emby.Naming.Video return result; } + + /// <summary> + /// Finds extras matching the video info. + /// </summary> + /// <param name="files">The list of file video infos.</param> + /// <param name="videoInfo">The video to compare against.</param> + /// <param name="videoFlagDelimiters">The video flag delimiters.</param> + /// <returns>A list of video extras for [videoInfo].</returns> + public static IReadOnlyList<VideoFileInfo> GetExtras(IReadOnlyList<VideoInfo> files, VideoFileInfo videoInfo, ReadOnlySpan<char> videoFlagDelimiters) + { + var parentDir = videoInfo.IsDirectory ? videoInfo.Path : Path.GetDirectoryName(videoInfo.Path.AsSpan()); + + var trimmedFileNameWithoutExtension = TrimFilenameDelimiters(videoInfo.FileNameWithoutExtension, videoFlagDelimiters); + var trimmedVideoInfoName = TrimFilenameDelimiters(videoInfo.Name, videoFlagDelimiters); + + var result = new List<VideoFileInfo>(); + for (var pos = files.Count - 1; pos >= 0; pos--) + { + var current = files[pos]; + // ignore non-extras and multi-file (can this happen?) + if (current.ExtraType == null || current.Files.Count > 1) + { + continue; + } + + var currentFile = current.Files[0]; + var trimmedCurrentFileName = TrimFilenameDelimiters(currentFile.Name, videoFlagDelimiters); + + // first check filenames + bool isValid = StartsWith(trimmedCurrentFileName, trimmedFileNameWithoutExtension) + || (StartsWith(trimmedCurrentFileName, trimmedVideoInfoName) && currentFile.Year == videoInfo.Year); + + // then by directory + if (!isValid) + { + // When the extra rule type is DirectoryName we must go one level higher to get the "real" dir name + var currentParentDir = currentFile.ExtraRule?.RuleType == ExtraRuleType.DirectoryName + ? Path.GetDirectoryName(Path.GetDirectoryName(currentFile.Path.AsSpan())) + : Path.GetDirectoryName(currentFile.Path.AsSpan()); + + isValid = !currentParentDir.IsEmpty && !parentDir.IsEmpty && currentParentDir.Equals(parentDir, StringComparison.OrdinalIgnoreCase); + } + + if (isValid) + { + result.Add(currentFile); + } + } + + return result.OrderBy(r => r.Path).ToArray(); + } + + private static ReadOnlySpan<char> TrimFilenameDelimiters(ReadOnlySpan<char> name, ReadOnlySpan<char> videoFlagDelimiters) + { + return name.IsEmpty ? name : name.TrimEnd().TrimEnd(videoFlagDelimiters).TrimEnd(); + } + + private static bool StartsWith(ReadOnlySpan<char> fileName, ReadOnlySpan<char> baseName) + { + return !baseName.IsEmpty && fileName.StartsWith(baseName, StringComparison.OrdinalIgnoreCase); + } } } diff --git a/Emby.Naming/Video/FileStack.cs b/Emby.Naming/Video/FileStack.cs index 6519db57c..4902e6728 100644 --- a/Emby.Naming/Video/FileStack.cs +++ b/Emby.Naming/Video/FileStack.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Generic; -using System.Linq; +using Jellyfin.Extensions; namespace Emby.Naming.Video { @@ -12,25 +12,30 @@ namespace Emby.Naming.Video /// <summary> /// Initializes a new instance of the <see cref="FileStack"/> class. /// </summary> - public FileStack() + /// <param name="name">The stack name.</param> + /// <param name="isDirectory">Whether the stack files are directories.</param> + /// <param name="files">The stack files.</param> + public FileStack(string name, bool isDirectory, IReadOnlyList<string> files) { - Files = new List<string>(); + Name = name; + IsDirectoryStack = isDirectory; + Files = files; } /// <summary> - /// Gets or sets name of file stack. + /// Gets the name of file stack. /// </summary> - public string Name { get; set; } = string.Empty; + public string Name { get; } /// <summary> - /// Gets or sets list of paths in stack. + /// Gets the list of paths in stack. /// </summary> - public List<string> Files { get; set; } + public IReadOnlyList<string> Files { get; } /// <summary> - /// Gets or sets a value indicating whether stack is directory stack. + /// Gets a value indicating whether stack is directory stack. /// </summary> - public bool IsDirectoryStack { get; set; } + public bool IsDirectoryStack { get; } /// <summary> /// Helper function to determine if path is in the stack. @@ -40,12 +45,12 @@ namespace Emby.Naming.Video /// <returns>True if file is in the stack.</returns> public bool ContainsFile(string file, bool isDirectory) { - if (IsDirectoryStack == isDirectory) + if (string.IsNullOrEmpty(file)) { - return Files.Contains(file, StringComparer.OrdinalIgnoreCase); + return false; } - return false; + return IsDirectoryStack == isDirectory && Files.Contains(file, StringComparison.OrdinalIgnoreCase); } } } diff --git a/Emby.Naming/Video/FileStackRule.cs b/Emby.Naming/Video/FileStackRule.cs new file mode 100644 index 000000000..76b487f42 --- /dev/null +++ b/Emby.Naming/Video/FileStackRule.cs @@ -0,0 +1,48 @@ +using System.Diagnostics.CodeAnalysis; +using System.Text.RegularExpressions; + +namespace Emby.Naming.Video; + +/// <summary> +/// Regex based rule for file stacking (eg. disc1, disc2). +/// </summary> +public class FileStackRule +{ + private readonly Regex _tokenRegex; + + /// <summary> + /// Initializes a new instance of the <see cref="FileStackRule"/> class. + /// </summary> + /// <param name="token">Token.</param> + /// <param name="isNumerical">Whether the file stack rule uses numerical or alphabetical numbering.</param> + public FileStackRule(string token, bool isNumerical) + { + _tokenRegex = new Regex(token, RegexOptions.IgnoreCase); + IsNumerical = isNumerical; + } + + /// <summary> + /// Gets a value indicating whether the rule uses numerical or alphabetical numbering. + /// </summary> + public bool IsNumerical { get; } + + /// <summary> + /// Match the input against the rule regex. + /// </summary> + /// <param name="input">The input.</param> + /// <param name="result">The part type and number or <c>null</c>.</param> + /// <returns>A value indicating whether the input matched the rule.</returns> + public bool Match(string input, [NotNullWhen(true)] out (string StackName, string PartType, string PartNumber)? result) + { + result = null; + var match = _tokenRegex.Match(input); + if (!match.Success) + { + return false; + } + + var partType = match.Groups["parttype"].Success ? match.Groups["parttype"].Value : "unknown"; + result = (match.Groups["filename"].Value, partType, match.Groups["number"].Value); + return true; + } +} diff --git a/Emby.Naming/Video/StackResolver.cs b/Emby.Naming/Video/StackResolver.cs index 36f65a562..8119a0267 100644 --- a/Emby.Naming/Video/StackResolver.cs +++ b/Emby.Naming/Video/StackResolver.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; -using System.Text.RegularExpressions; using Emby.Naming.AudioBook; using Emby.Naming.Common; using MediaBrowser.Model.IO; @@ -12,37 +11,28 @@ namespace Emby.Naming.Video /// <summary> /// Resolve <see cref="FileStack"/> from list of paths. /// </summary> - public class StackResolver + public static class StackResolver { - private readonly NamingOptions _options; - - /// <summary> - /// Initializes a new instance of the <see cref="StackResolver"/> class. - /// </summary> - /// <param name="options"><see cref="NamingOptions"/> object containing VideoFileStackingRegexes and passes options to <see cref="VideoResolver"/>.</param> - public StackResolver(NamingOptions options) - { - _options = options; - } - /// <summary> /// Resolves only directories from paths. /// </summary> /// <param name="files">List of paths.</param> + /// <param name="namingOptions">The naming options.</param> /// <returns>Enumerable <see cref="FileStack"/> of directories.</returns> - public IEnumerable<FileStack> ResolveDirectories(IEnumerable<string> files) + public static IEnumerable<FileStack> ResolveDirectories(IEnumerable<string> files, NamingOptions namingOptions) { - return Resolve(files.Select(i => new FileSystemMetadata { FullName = i, IsDirectory = true })); + return Resolve(files.Select(i => new FileSystemMetadata { FullName = i, IsDirectory = true }), namingOptions); } /// <summary> /// Resolves only files from paths. /// </summary> /// <param name="files">List of paths.</param> + /// <param name="namingOptions">The naming options.</param> /// <returns>Enumerable <see cref="FileStack"/> of files.</returns> - public IEnumerable<FileStack> ResolveFiles(IEnumerable<string> files) + public static IEnumerable<FileStack> ResolveFiles(IEnumerable<string> files, NamingOptions namingOptions) { - return Resolve(files.Select(i => new FileSystemMetadata { FullName = i, IsDirectory = false })); + return Resolve(files.Select(i => new FileSystemMetadata { FullName = i, IsDirectory = false }), namingOptions); } /// <summary> @@ -50,7 +40,7 @@ namespace Emby.Naming.Video /// </summary> /// <param name="files">List of paths.</param> /// <returns>Enumerable <see cref="FileStack"/> of directories.</returns> - public IEnumerable<FileStack> ResolveAudioBooks(IEnumerable<AudioBookFileInfo> files) + public static IEnumerable<FileStack> ResolveAudioBooks(IEnumerable<AudioBookFileInfo> files) { var groupedDirectoryFiles = files.GroupBy(file => Path.GetDirectoryName(file.Path)); @@ -60,19 +50,13 @@ namespace Emby.Naming.Video { foreach (var file in directory) { - var stack = new FileStack { Name = Path.GetFileNameWithoutExtension(file.Path), IsDirectoryStack = false }; - stack.Files.Add(file.Path); + var stack = new FileStack(Path.GetFileNameWithoutExtension(file.Path), false, new[] { file.Path }); yield return stack; } } else { - var stack = new FileStack { Name = Path.GetFileName(directory.Key), IsDirectoryStack = false }; - foreach (var file in directory) - { - stack.Files.Add(file.Path); - } - + var stack = new FileStack(Path.GetFileName(directory.Key), false, directory.Select(f => f.Path).ToArray()); yield return stack; } } @@ -82,158 +66,91 @@ namespace Emby.Naming.Video /// Resolves videos from paths. /// </summary> /// <param name="files">List of paths.</param> + /// <param name="namingOptions">The naming options.</param> /// <returns>Enumerable <see cref="FileStack"/> of videos.</returns> - public IEnumerable<FileStack> Resolve(IEnumerable<FileSystemMetadata> files) + public static IEnumerable<FileStack> Resolve(IEnumerable<FileSystemMetadata> files, NamingOptions namingOptions) { - var list = files - .Where(i => i.IsDirectory || VideoResolver.IsVideoFile(i.FullName, _options) || VideoResolver.IsStubFile(i.FullName, _options)) - .OrderBy(i => i.FullName) - .ToList(); + var potentialFiles = files + .Where(i => i.IsDirectory || VideoResolver.IsVideoFile(i.FullName, namingOptions) || VideoResolver.IsStubFile(i.FullName, namingOptions)) + .OrderBy(i => i.FullName); - var expressions = _options.VideoFileStackingRegexes; - - for (var i = 0; i < list.Count; i++) + var potentialStacks = new Dictionary<string, StackMetadata>(); + foreach (var file in potentialFiles) { - var offset = 0; - - var file1 = list[i]; - - var expressionIndex = 0; - while (expressionIndex < expressions.Length) + var name = file.Name; + if (string.IsNullOrEmpty(name)) { - var exp = expressions[expressionIndex]; - var stack = new FileStack(); + name = Path.GetFileName(file.FullName); + } - // (Title)(Volume)(Ignore)(Extension) - var match1 = FindMatch(file1, exp, offset); - - if (match1.Success) + for (var i = 0; i < namingOptions.VideoFileStackingRules.Length; i++) + { + var rule = namingOptions.VideoFileStackingRules[i]; + if (!rule.Match(name, out var stackParsingResult)) { - var title1 = match1.Groups["title"].Value; - var volume1 = match1.Groups["volume"].Value; - var ignore1 = match1.Groups["ignore"].Value; - var extension1 = match1.Groups["extension"].Value; + continue; + } - var j = i + 1; - while (j < list.Count) + var stackName = stackParsingResult.Value.StackName; + var partNumber = stackParsingResult.Value.PartNumber; + var partType = stackParsingResult.Value.PartType; + + if (!potentialStacks.TryGetValue(stackName, out var stackResult)) + { + stackResult = new StackMetadata(file.IsDirectory, rule.IsNumerical, partType); + potentialStacks[stackName] = stackResult; + } + + if (stackResult.Parts.Count > 0) + { + if (stackResult.IsDirectory != file.IsDirectory + || !string.Equals(partType, stackResult.PartType, StringComparison.OrdinalIgnoreCase) + || stackResult.ContainsPart(partNumber)) { - var file2 = list[j]; - - if (file1.IsDirectory != file2.IsDirectory) - { - j++; - continue; - } - - // (Title)(Volume)(Ignore)(Extension) - var match2 = FindMatch(file2, exp, offset); - - if (match2.Success) - { - var title2 = match2.Groups[1].Value; - var volume2 = match2.Groups[2].Value; - var ignore2 = match2.Groups[3].Value; - var extension2 = match2.Groups[4].Value; - - if (string.Equals(title1, title2, StringComparison.OrdinalIgnoreCase)) - { - if (!string.Equals(volume1, volume2, StringComparison.OrdinalIgnoreCase)) - { - if (string.Equals(ignore1, ignore2, StringComparison.OrdinalIgnoreCase) - && string.Equals(extension1, extension2, StringComparison.OrdinalIgnoreCase)) - { - if (stack.Files.Count == 0) - { - stack.Name = title1 + ignore1; - stack.IsDirectoryStack = file1.IsDirectory; - stack.Files.Add(file1.FullName); - } - - stack.Files.Add(file2.FullName); - } - else - { - // Sequel - offset = 0; - expressionIndex++; - break; - } - } - else if (!string.Equals(ignore1, ignore2, StringComparison.OrdinalIgnoreCase)) - { - // False positive, try again with offset - offset = match1.Groups[3].Index; - break; - } - else - { - // Extension mismatch - offset = 0; - expressionIndex++; - break; - } - } - else - { - // Title mismatch - offset = 0; - expressionIndex++; - break; - } - } - else - { - // No match 2, next expression - offset = 0; - expressionIndex++; - break; - } - - j++; + continue; } - if (j == list.Count) + if (rule.IsNumerical != stackResult.IsNumerical) { - expressionIndex = expressions.Length; + break; } } - else - { - // No match 1 - offset = 0; - expressionIndex++; - } - if (stack.Files.Count > 1) - { - yield return stack; - i += stack.Files.Count - 1; - break; - } + stackResult.Parts.Add(partNumber, file); + break; } } - } - private static string GetRegexInput(FileSystemMetadata file) - { - // For directories, dummy up an extension otherwise the expressions will fail - var input = !file.IsDirectory - ? file.FullName - : file.FullName + ".mkv"; - - return Path.GetFileName(input); - } - - private static Match FindMatch(FileSystemMetadata input, Regex regex, int offset) - { - var regexInput = GetRegexInput(input); - - if (offset < 0 || offset >= regexInput.Length) + foreach (var (fileName, stack) in potentialStacks) { - return Match.Empty; + if (stack.Parts.Count < 2) + { + continue; + } + + yield return new FileStack(fileName, stack.IsDirectory, stack.Parts.Select(kv => kv.Value.FullName).ToArray()); + } + } + + private class StackMetadata + { + public StackMetadata(bool isDirectory, bool isNumerical, string partType) + { + Parts = new Dictionary<string, FileSystemMetadata>(StringComparer.OrdinalIgnoreCase); + IsDirectory = isDirectory; + IsNumerical = isNumerical; + PartType = partType; } - return regex.Match(regexInput, offset); + public Dictionary<string, FileSystemMetadata> Parts { get; } + + public bool IsDirectory { get; } + + public bool IsNumerical { get; } + + public string PartType { get; } + + public bool ContainsPart(string partNumber) => Parts.ContainsKey(partNumber); } } } diff --git a/Emby.Naming/Video/StubResolver.cs b/Emby.Naming/Video/StubResolver.cs index 079987fe8..f7ba606e3 100644 --- a/Emby.Naming/Video/StubResolver.cs +++ b/Emby.Naming/Video/StubResolver.cs @@ -1,7 +1,7 @@ using System; using System.IO; -using System.Linq; using Emby.Naming.Common; +using Jellyfin.Extensions; namespace Emby.Naming.Video { @@ -28,7 +28,7 @@ namespace Emby.Naming.Video var extension = Path.GetExtension(path); - if (!options.StubFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase)) + if (!options.StubFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase)) { return false; } diff --git a/Emby.Naming/Video/VideoInfo.cs b/Emby.Naming/Video/VideoInfo.cs index 930fdb33f..8847ee9bc 100644 --- a/Emby.Naming/Video/VideoInfo.cs +++ b/Emby.Naming/Video/VideoInfo.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using MediaBrowser.Model.Entities; namespace Emby.Naming.Video { @@ -17,7 +18,6 @@ namespace Emby.Naming.Video Name = name; Files = Array.Empty<VideoFileInfo>(); - Extras = Array.Empty<VideoFileInfo>(); AlternateVersions = Array.Empty<VideoFileInfo>(); } @@ -39,16 +39,15 @@ namespace Emby.Naming.Video /// <value>The files.</value> public IReadOnlyList<VideoFileInfo> Files { get; set; } - /// <summary> - /// Gets or sets the extras. - /// </summary> - /// <value>The extras.</value> - public IReadOnlyList<VideoFileInfo> Extras { get; set; } - /// <summary> /// Gets or sets the alternate versions. /// </summary> /// <value>The alternate versions.</value> public IReadOnlyList<VideoFileInfo> AlternateVersions { get; set; } + + /// <summary> + /// Gets or sets the extra type. + /// </summary> + public ExtraType? ExtraType { get; set; } } } diff --git a/Emby.Naming/Video/VideoListResolver.cs b/Emby.Naming/Video/VideoListResolver.cs index ed7d511a3..4fc849256 100644 --- a/Emby.Naming/Video/VideoListResolver.cs +++ b/Emby.Naming/Video/VideoListResolver.cs @@ -4,7 +4,6 @@ using System.IO; using System.Linq; using System.Text.RegularExpressions; using Emby.Naming.Common; -using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; namespace Emby.Naming.Video @@ -17,29 +16,38 @@ namespace Emby.Naming.Video /// <summary> /// Resolves alternative versions and extras from list of video files. /// </summary> - /// <param name="files">List of related video files.</param> + /// <param name="videoInfos">List of related video files.</param> /// <param name="namingOptions">The naming options.</param> /// <param name="supportMultiVersion">Indication we should consider multi-versions of content.</param> + /// <param name="parseName">Whether to parse the name or use the filename.</param> /// <returns>Returns enumerable of <see cref="VideoInfo"/> which groups files together when related.</returns> - public static IEnumerable<VideoInfo> Resolve(IEnumerable<FileSystemMetadata> files, NamingOptions namingOptions, bool supportMultiVersion = true) + public static IReadOnlyList<VideoInfo> Resolve(IReadOnlyList<VideoFileInfo> videoInfos, NamingOptions namingOptions, bool supportMultiVersion = true, bool parseName = true) { - var videoInfos = files - .Select(i => VideoResolver.Resolve(i.FullName, i.IsDirectory, namingOptions)) - .OfType<VideoFileInfo>() - .ToList(); - // Filter out all extras, otherwise they could cause stacks to not be resolved // See the unit test TestStackedWithTrailer var nonExtras = videoInfos .Where(i => i.ExtraType == null) .Select(i => new FileSystemMetadata { FullName = i.Path, IsDirectory = i.IsDirectory }); - var stackResult = new StackResolver(namingOptions) - .Resolve(nonExtras).ToList(); + var stackResult = StackResolver.Resolve(nonExtras, namingOptions).ToList(); - var remainingFiles = videoInfos - .Where(i => !stackResult.Any(s => i.Path != null && s.ContainsFile(i.Path, i.IsDirectory))) - .ToList(); + var remainingFiles = new List<VideoFileInfo>(); + var standaloneMedia = new List<VideoFileInfo>(); + + for (var i = 0; i < videoInfos.Count; i++) + { + var current = videoInfos[i]; + if (stackResult.Any(s => s.ContainsFile(current.Path, current.IsDirectory))) + { + continue; + } + + remainingFiles.Add(current); + if (current.ExtraType == null) + { + standaloneMedia.Add(current); + } + } var list = new List<VideoInfo>(); @@ -47,27 +55,15 @@ namespace Emby.Naming.Video { var info = new VideoInfo(stack.Name) { - Files = stack.Files.Select(i => VideoResolver.Resolve(i, stack.IsDirectoryStack, namingOptions)) + Files = stack.Files.Select(i => VideoResolver.Resolve(i, stack.IsDirectoryStack, namingOptions, parseName)) .OfType<VideoFileInfo>() .ToList() }; info.Year = info.Files[0].Year; - - var extras = ExtractExtras(remainingFiles, stack.Name, Path.GetFileNameWithoutExtension(stack.Files[0].AsSpan()), namingOptions.VideoFlagDelimiters); - - if (extras.Count > 0) - { - info.Extras = extras; - } - list.Add(info); } - var standaloneMedia = remainingFiles - .Where(i => i.ExtraType == null) - .ToList(); - foreach (var media in standaloneMedia) { var info = new VideoInfo(media.Name) { Files = new[] { media } }; @@ -75,10 +71,6 @@ namespace Emby.Naming.Video info.Year = info.Files[0].Year; remainingFiles.Remove(media); - var extras = ExtractExtras(remainingFiles, media.FileNameWithoutExtension, namingOptions.VideoFlagDelimiters); - - info.Extras = extras; - list.Add(info); } @@ -87,58 +79,12 @@ namespace Emby.Naming.Video list = GetVideosGroupedByVersion(list, namingOptions); } - // If there's only one resolved video, use the folder name as well to find extras - if (list.Count == 1) - { - var info = list[0]; - var videoPath = list[0].Files[0].Path; - var parentPath = Path.GetDirectoryName(videoPath.AsSpan()); - - if (!parentPath.IsEmpty) - { - var folderName = Path.GetFileName(parentPath); - if (!folderName.IsEmpty) - { - var extras = ExtractExtras(remainingFiles, folderName, namingOptions.VideoFlagDelimiters); - extras.AddRange(info.Extras); - info.Extras = extras; - } - } - - // Add the extras that are just based on file name as well - var extrasByFileName = remainingFiles - .Where(i => i.ExtraRule != null && i.ExtraRule.RuleType == ExtraRuleType.Filename) - .ToList(); - - remainingFiles = remainingFiles - .Except(extrasByFileName) - .ToList(); - - extrasByFileName.AddRange(info.Extras); - info.Extras = extrasByFileName; - } - - // If there's only one video, accept all trailers - // Be lenient because people use all kinds of mishmash conventions with trailers. - if (list.Count == 1) - { - var trailers = remainingFiles - .Where(i => i.ExtraType == ExtraType.Trailer) - .ToList(); - - trailers.AddRange(list[0].Extras); - list[0].Extras = trailers; - - remainingFiles = remainingFiles - .Except(trailers) - .ToList(); - } - // Whatever files are left, just add them list.AddRange(remainingFiles.Select(i => new VideoInfo(i.Name) { Files = new[] { i }, - Year = i.Year + Year = i.Year, + ExtraType = i.ExtraType })); return list; @@ -162,6 +108,11 @@ namespace Emby.Naming.Video for (var i = 0; i < videos.Count; i++) { var video = videos[i]; + if (video.ExtraType != null) + { + continue; + } + if (!IsEligibleForMultiVersion(folderName, video.Files[0].Path, namingOptions)) { return videos; @@ -178,17 +129,14 @@ namespace Emby.Naming.Video var alternateVersionsLen = videos.Count - 1; var alternateVersions = new VideoFileInfo[alternateVersionsLen]; - var extras = new List<VideoFileInfo>(list[0].Extras); for (int i = 0; i < alternateVersionsLen; i++) { var video = videos[i + 1]; alternateVersions[i] = video.Files[0]; - extras.AddRange(video.Extras); } list[0].AlternateVersions = alternateVersions; list[0].Name = folderName.ToString(); - list[0].Extras = extras; return list; } @@ -230,7 +178,7 @@ namespace Emby.Naming.Video var tmpTestFilename = testFilename.ToString(); if (CleanStringParser.TryClean(tmpTestFilename, namingOptions.CleanStringRegexes, out var cleanName)) { - tmpTestFilename = cleanName.Trim().ToString(); + tmpTestFilename = cleanName.Trim(); } // The CleanStringParser should have removed common keywords etc. @@ -238,67 +186,5 @@ namespace Emby.Naming.Video || testFilename[0] == '-' || Regex.IsMatch(tmpTestFilename, @"^\[([^]]*)\]", RegexOptions.Compiled); } - - private static ReadOnlySpan<char> TrimFilenameDelimiters(ReadOnlySpan<char> name, ReadOnlySpan<char> videoFlagDelimiters) - { - return name.IsEmpty ? name : name.TrimEnd().TrimEnd(videoFlagDelimiters).TrimEnd(); - } - - private static bool StartsWith(ReadOnlySpan<char> fileName, ReadOnlySpan<char> baseName, ReadOnlySpan<char> trimmedBaseName) - { - if (baseName.IsEmpty) - { - return false; - } - - return fileName.StartsWith(baseName, StringComparison.OrdinalIgnoreCase) - || (!trimmedBaseName.IsEmpty && fileName.StartsWith(trimmedBaseName, StringComparison.OrdinalIgnoreCase)); - } - - /// <summary> - /// Finds similar filenames to that of [baseName] and removes any matches from [remainingFiles]. - /// </summary> - /// <param name="remainingFiles">The list of remaining filenames.</param> - /// <param name="baseName">The base name to use for the comparison.</param> - /// <param name="videoFlagDelimiters">The video flag delimiters.</param> - /// <returns>A list of video extras for [baseName].</returns> - private static List<VideoFileInfo> ExtractExtras(IList<VideoFileInfo> remainingFiles, ReadOnlySpan<char> baseName, ReadOnlySpan<char> videoFlagDelimiters) - { - return ExtractExtras(remainingFiles, baseName, ReadOnlySpan<char>.Empty, videoFlagDelimiters); - } - - /// <summary> - /// Finds similar filenames to that of [firstBaseName] and [secondBaseName] and removes any matches from [remainingFiles]. - /// </summary> - /// <param name="remainingFiles">The list of remaining filenames.</param> - /// <param name="firstBaseName">The first base name to use for the comparison.</param> - /// <param name="secondBaseName">The second base name to use for the comparison.</param> - /// <param name="videoFlagDelimiters">The video flag delimiters.</param> - /// <returns>A list of video extras for [firstBaseName] and [secondBaseName].</returns> - private static List<VideoFileInfo> ExtractExtras(IList<VideoFileInfo> remainingFiles, ReadOnlySpan<char> firstBaseName, ReadOnlySpan<char> secondBaseName, ReadOnlySpan<char> videoFlagDelimiters) - { - var trimmedFirstBaseName = TrimFilenameDelimiters(firstBaseName, videoFlagDelimiters); - var trimmedSecondBaseName = TrimFilenameDelimiters(secondBaseName, videoFlagDelimiters); - - var result = new List<VideoFileInfo>(); - for (var pos = remainingFiles.Count - 1; pos >= 0; pos--) - { - var file = remainingFiles[pos]; - if (file.ExtraType == null) - { - continue; - } - - var filename = file.FileNameWithoutExtension; - if (StartsWith(filename, firstBaseName, trimmedFirstBaseName) - || StartsWith(filename, secondBaseName, trimmedSecondBaseName)) - { - result.Add(file); - remainingFiles.RemoveAt(pos); - } - } - - return result; - } } } diff --git a/Emby.Naming/Video/VideoResolver.cs b/Emby.Naming/Video/VideoResolver.cs index 4c9df27f5..9cadc1465 100644 --- a/Emby.Naming/Video/VideoResolver.cs +++ b/Emby.Naming/Video/VideoResolver.cs @@ -16,10 +16,11 @@ namespace Emby.Naming.Video /// </summary> /// <param name="path">The path.</param> /// <param name="namingOptions">The naming options.</param> + /// <param name="parseName">Whether to parse the name or use the filename.</param> /// <returns>VideoFileInfo.</returns> - public static VideoFileInfo? ResolveDirectory(string? path, NamingOptions namingOptions) + public static VideoFileInfo? ResolveDirectory(string? path, NamingOptions namingOptions, bool parseName = true) { - return Resolve(path, true, namingOptions); + return Resolve(path, true, namingOptions, parseName); } /// <summary> @@ -74,7 +75,7 @@ namespace Emby.Naming.Video var format3DResult = Format3DParser.Parse(path, namingOptions); - var extraResult = new ExtraResolver(namingOptions).GetExtraInfo(path); + var extraResult = ExtraResolver.GetExtraInfo(path, namingOptions); var name = Path.GetFileNameWithoutExtension(path); diff --git a/Emby.Notifications/NotificationEntryPoint.cs b/Emby.Notifications/NotificationEntryPoint.cs index e8ae14ff2..a56df7031 100644 --- a/Emby.Notifications/NotificationEntryPoint.cs +++ b/Emby.Notifications/NotificationEntryPoint.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using Jellyfin.Data.Events; +using Jellyfin.Extensions; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller; using MediaBrowser.Controller.Entities; @@ -104,7 +105,7 @@ namespace Emby.Notifications var type = entry.Type; - if (string.IsNullOrEmpty(type) || !_coreNotificationTypes.Contains(type, StringComparer.OrdinalIgnoreCase)) + if (string.IsNullOrEmpty(type) || !_coreNotificationTypes.Contains(type, StringComparison.OrdinalIgnoreCase)) { return; } diff --git a/Emby.Photos/PhotoProvider.cs b/Emby.Photos/PhotoProvider.cs index 4071e4e54..cef82b4d6 100644 --- a/Emby.Photos/PhotoProvider.cs +++ b/Emby.Photos/PhotoProvider.cs @@ -3,6 +3,7 @@ using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Extensions; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; @@ -60,7 +61,7 @@ namespace Emby.Photos item.SetImagePath(ImageType.Primary, item.Path); // Examples: https://github.com/mono/taglib-sharp/blob/a5f6949a53d09ce63ee7495580d6802921a21f14/tests/fixtures/TagLib.Tests.Images/NullOrientationTest.cs - if (_includeExtensions.Contains(Path.GetExtension(item.Path), StringComparer.OrdinalIgnoreCase)) + if (_includeExtensions.Contains(Path.GetExtension(item.Path), StringComparison.OrdinalIgnoreCase)) { try { diff --git a/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs b/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs index d38535634..5ba4749a6 100644 --- a/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs +++ b/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs @@ -301,7 +301,7 @@ namespace Emby.Server.Implementations.AppBase { return _configurations.GetOrAdd( key, - (k, configurationManager) => + static (k, configurationManager) => { var file = configurationManager.GetConfigurationFile(k); diff --git a/Emby.Server.Implementations/AppBase/ConfigurationHelper.cs b/Emby.Server.Implementations/AppBase/ConfigurationHelper.cs index 3a916e5c0..f923e59ef 100644 --- a/Emby.Server.Implementations/AppBase/ConfigurationHelper.cs +++ b/Emby.Server.Implementations/AppBase/ConfigurationHelper.cs @@ -1,6 +1,5 @@ using System; using System.IO; -using System.Linq; using MediaBrowser.Model.Serialization; namespace Emby.Server.Implementations.AppBase diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 903c31133..8892f7f40 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -313,22 +313,6 @@ namespace Emby.Server.Implementations ? Environment.MachineName : ConfigurationManager.Configuration.ServerName; - /// <summary> - /// Temporary function to migration network settings out of system.xml and into network.xml. - /// TODO: remove at the point when a fixed migration path has been decided upon. - /// </summary> - private void MigrateNetworkConfiguration() - { - string path = Path.Combine(ConfigurationManager.CommonApplicationPaths.ConfigurationDirectoryPath, "network.xml"); - if (!File.Exists(path)) - { - var networkSettings = new NetworkConfiguration(); - ClassMigrationHelper.CopyProperties(ConfigurationManager.Configuration, networkSettings); - _xmlSerializer.SerializeToFile(networkSettings, path); - Logger.LogDebug("Successfully migrated network settings."); - } - } - public string ExpandVirtualPath(string path) { var appPaths = ApplicationPaths; @@ -513,8 +497,6 @@ namespace Emby.Server.Implementations ConfigurationManager.AddParts(GetExports<IConfigurationFactory>()); - // Have to migrate settings here as migration subsystem not yet initialised. - MigrateNetworkConfiguration(); NetManager = new NetworkManager(ConfigurationManager, LoggerFactory.CreateLogger<NetworkManager>()); // Initialize runtime stat collection diff --git a/Emby.Server.Implementations/Archiving/ZipClient.cs b/Emby.Server.Implementations/Archiving/ZipClient.cs index 9e1d550eb..6a3b250d2 100644 --- a/Emby.Server.Implementations/Archiving/ZipClient.cs +++ b/Emby.Server.Implementations/Archiving/ZipClient.cs @@ -1,11 +1,8 @@ using System.IO; using MediaBrowser.Model.IO; -using SharpCompress.Archives.SevenZip; -using SharpCompress.Archives.Tar; using SharpCompress.Common; using SharpCompress.Readers; using SharpCompress.Readers.GZip; -using SharpCompress.Readers.Zip; namespace Emby.Server.Implementations.Archiving { @@ -14,55 +11,6 @@ namespace Emby.Server.Implementations.Archiving /// </summary> public class ZipClient : IZipClient { - /// <summary> - /// Extracts all. - /// </summary> - /// <param name="sourceFile">The source file.</param> - /// <param name="targetPath">The target path.</param> - /// <param name="overwriteExistingFiles">if set to <c>true</c> [overwrite existing files].</param> - public void ExtractAll(string sourceFile, string targetPath, bool overwriteExistingFiles) - { - using var fileStream = File.OpenRead(sourceFile); - ExtractAll(fileStream, targetPath, overwriteExistingFiles); - } - - /// <summary> - /// Extracts all. - /// </summary> - /// <param name="source">The source.</param> - /// <param name="targetPath">The target path.</param> - /// <param name="overwriteExistingFiles">if set to <c>true</c> [overwrite existing files].</param> - public void ExtractAll(Stream source, string targetPath, bool overwriteExistingFiles) - { - using var reader = ReaderFactory.Open(source); - var options = new ExtractionOptions - { - ExtractFullPath = true - }; - - if (overwriteExistingFiles) - { - options.Overwrite = true; - } - - Directory.CreateDirectory(targetPath); - reader.WriteAllToDirectory(targetPath, options); - } - - /// <inheritdoc /> - public void ExtractAllFromZip(Stream source, string targetPath, bool overwriteExistingFiles) - { - using var reader = ZipReader.Open(source); - var options = new ExtractionOptions - { - ExtractFullPath = true, - Overwrite = overwriteExistingFiles - }; - - Directory.CreateDirectory(targetPath); - reader.WriteAllToDirectory(targetPath, options); - } - /// <inheritdoc /> public void ExtractAllFromGz(Stream source, string targetPath, bool overwriteExistingFiles) { @@ -94,69 +42,5 @@ namespace Emby.Server.Implementations.Archiving reader.WriteEntryToFile(Path.Combine(targetPath, filename)); } } - - /// <summary> - /// Extracts all from7z. - /// </summary> - /// <param name="sourceFile">The source file.</param> - /// <param name="targetPath">The target path.</param> - /// <param name="overwriteExistingFiles">if set to <c>true</c> [overwrite existing files].</param> - public void ExtractAllFrom7z(string sourceFile, string targetPath, bool overwriteExistingFiles) - { - using var fileStream = File.OpenRead(sourceFile); - ExtractAllFrom7z(fileStream, targetPath, overwriteExistingFiles); - } - - /// <summary> - /// Extracts all from7z. - /// </summary> - /// <param name="source">The source.</param> - /// <param name="targetPath">The target path.</param> - /// <param name="overwriteExistingFiles">if set to <c>true</c> [overwrite existing files].</param> - public void ExtractAllFrom7z(Stream source, string targetPath, bool overwriteExistingFiles) - { - using var archive = SevenZipArchive.Open(source); - using var reader = archive.ExtractAllEntries(); - var options = new ExtractionOptions - { - ExtractFullPath = true, - Overwrite = overwriteExistingFiles - }; - - Directory.CreateDirectory(targetPath); - reader.WriteAllToDirectory(targetPath, options); - } - - /// <summary> - /// Extracts all from tar. - /// </summary> - /// <param name="sourceFile">The source file.</param> - /// <param name="targetPath">The target path.</param> - /// <param name="overwriteExistingFiles">if set to <c>true</c> [overwrite existing files].</param> - public void ExtractAllFromTar(string sourceFile, string targetPath, bool overwriteExistingFiles) - { - using var fileStream = File.OpenRead(sourceFile); - ExtractAllFromTar(fileStream, targetPath, overwriteExistingFiles); - } - - /// <summary> - /// Extracts all from tar. - /// </summary> - /// <param name="source">The source.</param> - /// <param name="targetPath">The target path.</param> - /// <param name="overwriteExistingFiles">if set to <c>true</c> [overwrite existing files].</param> - public void ExtractAllFromTar(Stream source, string targetPath, bool overwriteExistingFiles) - { - using var archive = TarArchive.Open(source); - using var reader = archive.ExtractAllEntries(); - var options = new ExtractionOptions - { - ExtractFullPath = true, - Overwrite = overwriteExistingFiles - }; - - Directory.CreateDirectory(targetPath); - reader.WriteAllToDirectory(targetPath, options); - } } } diff --git a/Emby.Server.Implementations/Channels/ChannelManager.cs b/Emby.Server.Implementations/Channels/ChannelManager.cs index f65eaec1c..8702691d1 100644 --- a/Emby.Server.Implementations/Channels/ChannelManager.cs +++ b/Emby.Server.Implementations/Channels/ChannelManager.cs @@ -10,6 +10,7 @@ using System.Threading; using System.Threading.Tasks; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Extensions; using Jellyfin.Extensions.Json; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Progress; @@ -179,7 +180,7 @@ namespace Emby.Server.Implementations.Channels try { return (GetChannelProvider(i) is IHasFolderAttributes hasAttributes - && hasAttributes.Attributes.Contains("Recordings", StringComparer.OrdinalIgnoreCase)) == val; + && hasAttributes.Attributes.Contains("Recordings", StringComparison.OrdinalIgnoreCase)) == val; } catch { @@ -541,7 +542,7 @@ namespace Emby.Server.Implementations.Channels return _libraryManager.GetItemIds( new InternalItemsQuery { - IncludeItemTypes = new[] { nameof(Channel) }, + IncludeItemTypes = new[] { BaseItemKind.Channel }, OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) } }).Select(i => GetChannelFeatures(i)).ToArray(); } @@ -1135,7 +1136,7 @@ namespace Emby.Server.Implementations.Channels if (!info.IsLiveStream) { - if (item.Tags.Contains("livestream", StringComparer.OrdinalIgnoreCase)) + if (item.Tags.Contains("livestream", StringComparison.OrdinalIgnoreCase)) { item.Tags = item.Tags.Except(new[] { "livestream" }, StringComparer.OrdinalIgnoreCase).ToArray(); _logger.LogDebug("Forcing update due to Tags {0}", item.Name); @@ -1144,7 +1145,7 @@ namespace Emby.Server.Implementations.Channels } else { - if (!item.Tags.Contains("livestream", StringComparer.OrdinalIgnoreCase)) + if (!item.Tags.Contains("livestream", StringComparison.OrdinalIgnoreCase)) { item.Tags = item.Tags.Concat(new[] { "livestream" }).ToArray(); _logger.LogDebug("Forcing update due to Tags {0}", item.Name); diff --git a/Emby.Server.Implementations/Channels/ChannelPostScanTask.cs b/Emby.Server.Implementations/Channels/ChannelPostScanTask.cs index 2391eed42..b358ba4d5 100644 --- a/Emby.Server.Implementations/Channels/ChannelPostScanTask.cs +++ b/Emby.Server.Implementations/Channels/ChannelPostScanTask.cs @@ -2,6 +2,7 @@ using System; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; @@ -51,7 +52,7 @@ namespace Emby.Server.Implementations.Channels var uninstalledChannels = _libraryManager.GetItemList(new InternalItemsQuery { - IncludeItemTypes = new[] { nameof(Channel) }, + IncludeItemTypes = new[] { BaseItemKind.Channel }, ExcludeItemIds = installedChannelIds.ToArray() }); diff --git a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs index 73c31f49d..5030cbacb 100644 --- a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs +++ b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs @@ -4,8 +4,8 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Threading; +using Jellyfin.Extensions; using Microsoft.Extensions.Logging; using SQLitePCL.pretty; @@ -194,7 +194,7 @@ namespace Emby.Server.Implementations.Data protected void AddColumn(IDatabaseConnection connection, string table, string columnName, string type, List<string> existingColumnNames) { - if (existingColumnNames.Contains(columnName, StringComparer.OrdinalIgnoreCase)) + if (existingColumnNames.Contains(columnName, StringComparison.OrdinalIgnoreCase)) { return; } diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index 13f1df7c8..beae7e243 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -62,7 +62,7 @@ namespace Emby.Server.Implementations.Data private readonly ItemFields[] _allItemFields = Enum.GetValues<ItemFields>(); - private static readonly string[] _retriveItemColumns = + private static readonly string[] _retrieveItemColumns = { "type", "data", @@ -133,7 +133,7 @@ namespace Emby.Server.Implementations.Data "OwnerId" }; - private static readonly string _retriveItemColumnsSelectQuery = $"select {string.Join(',', _retriveItemColumns)} from TypedBaseItems where guid = @guid"; + private static readonly string _retrieveItemColumnsSelectQuery = $"select {string.Join(',', _retrieveItemColumns)} from TypedBaseItems where guid = @guid"; private static readonly string[] _mediaStreamSaveColumns = { @@ -196,57 +196,56 @@ namespace Emby.Server.Implementations.Data private static readonly string _mediaAttachmentInsertPrefix; - private static readonly HashSet<string> _programTypes = new HashSet<string>(StringComparer.OrdinalIgnoreCase) + private static readonly BaseItemKind[] _programTypes = new[] { - "Program", - "TvChannel", - "LiveTvProgram", - "LiveTvTvChannel" + BaseItemKind.Program, + BaseItemKind.TvChannel, + BaseItemKind.LiveTvProgram, + BaseItemKind.LiveTvChannel }; - private static readonly HashSet<string> _programExcludeParentTypes = new HashSet<string>(StringComparer.OrdinalIgnoreCase) + private static readonly BaseItemKind[] _programExcludeParentTypes = new[] { - "Series", - "Season", - "MusicAlbum", - "MusicArtist", - "PhotoAlbum" + BaseItemKind.Series, + BaseItemKind.Season, + BaseItemKind.MusicAlbum, + BaseItemKind.MusicArtist, + BaseItemKind.PhotoAlbum }; - private static readonly HashSet<string> _serviceTypes = new HashSet<string>(StringComparer.OrdinalIgnoreCase) + private static readonly BaseItemKind[] _serviceTypes = new[] { - "TvChannel", - "LiveTvTvChannel" + BaseItemKind.TvChannel, + BaseItemKind.LiveTvChannel }; - private static readonly HashSet<string> _startDateTypes = new HashSet<string>(StringComparer.OrdinalIgnoreCase) + private static readonly BaseItemKind[] _startDateTypes = new[] { - "Program", - "LiveTvProgram" + BaseItemKind.Program, + BaseItemKind.LiveTvProgram }; - private static readonly HashSet<string> _seriesTypes = new HashSet<string>(StringComparer.OrdinalIgnoreCase) + private static readonly BaseItemKind[] _seriesTypes = new[] { - "Book", - "AudioBook", - "Episode", - "Season" + BaseItemKind.Book, + BaseItemKind.AudioBook, + BaseItemKind.Episode, + BaseItemKind.Season }; - private static readonly HashSet<string> _artistExcludeParentTypes = new HashSet<string>(StringComparer.OrdinalIgnoreCase) + private static readonly BaseItemKind[] _artistExcludeParentTypes = new[] { - "Series", - "Season", - "PhotoAlbum" + BaseItemKind.Series, + BaseItemKind.Season, + BaseItemKind.PhotoAlbum }; - private static readonly HashSet<string> _artistsTypes = new HashSet<string>(StringComparer.OrdinalIgnoreCase) + private static readonly BaseItemKind[] _artistsTypes = new[] { - "Audio", - "MusicAlbum", - "MusicVideo", - "AudioBook", - "AudioPodcast" + BaseItemKind.Audio, + BaseItemKind.MusicAlbum, + BaseItemKind.MusicVideo, + BaseItemKind.AudioBook }; private static readonly Type[] _knownTypes = @@ -285,6 +284,43 @@ namespace Emby.Server.Implementations.Data private readonly Dictionary<string, string> _types = GetTypeMapDictionary(); + private static readonly Dictionary<BaseItemKind, string> _baseItemKindNames = new() + { + { BaseItemKind.AggregateFolder, typeof(AggregateFolder).FullName }, + { BaseItemKind.Audio, typeof(Audio).FullName }, + { BaseItemKind.AudioBook, typeof(AudioBook).FullName }, + { BaseItemKind.BasePluginFolder, typeof(BasePluginFolder).FullName }, + { BaseItemKind.Book, typeof(Book).FullName }, + { BaseItemKind.BoxSet, typeof(BoxSet).FullName }, + { BaseItemKind.Channel, typeof(Channel).FullName }, + { BaseItemKind.CollectionFolder, typeof(CollectionFolder).FullName }, + { BaseItemKind.Episode, typeof(Episode).FullName }, + { BaseItemKind.Folder, typeof(Folder).FullName }, + { BaseItemKind.Genre, typeof(Genre).FullName }, + { BaseItemKind.Movie, typeof(Movie).FullName }, + { BaseItemKind.LiveTvChannel, typeof(LiveTvChannel).FullName }, + { BaseItemKind.LiveTvProgram, typeof(LiveTvProgram).FullName }, + { BaseItemKind.MusicAlbum, typeof(MusicAlbum).FullName }, + { BaseItemKind.MusicArtist, typeof(MusicArtist).FullName }, + { BaseItemKind.MusicGenre, typeof(MusicGenre).FullName }, + { BaseItemKind.MusicVideo, typeof(MusicVideo).FullName }, + { BaseItemKind.Person, typeof(Person).FullName }, + { BaseItemKind.Photo, typeof(Photo).FullName }, + { BaseItemKind.PhotoAlbum, typeof(PhotoAlbum).FullName }, + { BaseItemKind.Playlist, typeof(Playlist).FullName }, + { BaseItemKind.PlaylistsFolder, typeof(PlaylistsFolder).FullName }, + { BaseItemKind.Season, typeof(Season).FullName }, + { BaseItemKind.Series, typeof(Series).FullName }, + { BaseItemKind.Studio, typeof(Studio).FullName }, + { BaseItemKind.Trailer, typeof(Trailer).FullName }, + { BaseItemKind.TvChannel, typeof(LiveTvChannel).FullName }, + { BaseItemKind.TvProgram, typeof(LiveTvProgram).FullName }, + { BaseItemKind.UserRootFolder, typeof(UserRootFolder).FullName }, + { BaseItemKind.UserView, typeof(UserView).FullName }, + { BaseItemKind.Video, typeof(Video).FullName }, + { BaseItemKind.Year, typeof(Year).FullName } + }; + static SqliteItemRepository() { var queryPrefixText = new StringBuilder(); @@ -1320,7 +1356,7 @@ namespace Emby.Server.Implementations.Data using (var connection = GetConnection(true)) { - using (var statement = PrepareStatement(connection, _retriveItemColumnsSelectQuery)) + using (var statement = PrepareStatement(connection, _retrieveItemColumnsSelectQuery)) { statement.TryBind("@guid", id); @@ -2212,7 +2248,7 @@ namespace Emby.Server.Implementations.Data private bool HasProgramAttributes(InternalItemsQuery query) { - if (_programExcludeParentTypes.Contains(query.ParentType)) + if (query.ParentType != null && _programExcludeParentTypes.Contains(query.ParentType.Value)) { return false; } @@ -2227,7 +2263,7 @@ namespace Emby.Server.Implementations.Data private bool HasServiceName(InternalItemsQuery query) { - if (_programExcludeParentTypes.Contains(query.ParentType)) + if (query.ParentType != null && _programExcludeParentTypes.Contains(query.ParentType.Value)) { return false; } @@ -2242,7 +2278,7 @@ namespace Emby.Server.Implementations.Data private bool HasStartDate(InternalItemsQuery query) { - if (_programExcludeParentTypes.Contains(query.ParentType)) + if (query.ParentType != null && _programExcludeParentTypes.Contains(query.ParentType.Value)) { return false; } @@ -2262,7 +2298,7 @@ namespace Emby.Server.Implementations.Data return true; } - return query.IncludeItemTypes.Contains("Episode", StringComparer.OrdinalIgnoreCase); + return query.IncludeItemTypes.Contains(BaseItemKind.Episode); } private bool HasTrailerTypes(InternalItemsQuery query) @@ -2272,12 +2308,12 @@ namespace Emby.Server.Implementations.Data return true; } - return query.IncludeItemTypes.Contains("Trailer", StringComparer.OrdinalIgnoreCase); + return query.IncludeItemTypes.Contains(BaseItemKind.Trailer); } private bool HasArtistFields(InternalItemsQuery query) { - if (_artistExcludeParentTypes.Contains(query.ParentType)) + if (query.ParentType != null && _artistExcludeParentTypes.Contains(query.ParentType.Value)) { return false; } @@ -2292,7 +2328,7 @@ namespace Emby.Server.Implementations.Data private bool HasSeriesFields(InternalItemsQuery query) { - if (string.Equals(query.ParentType, "PhotoAlbum", StringComparison.OrdinalIgnoreCase)) + if (query.ParentType == BaseItemKind.PhotoAlbum) { return false; } @@ -2630,7 +2666,7 @@ namespace Emby.Server.Implementations.Data query.Limit = query.Limit.Value + 4; } - var columns = _retriveItemColumns.ToList(); + var columns = _retrieveItemColumns.ToList(); SetFinalColumnsToSelect(query, columns); var commandTextBuilder = new StringBuilder("select ", 1024) .AppendJoin(',', columns) @@ -2821,7 +2857,7 @@ namespace Emby.Server.Implementations.Data query.Limit = query.Limit.Value + 4; } - var columns = _retriveItemColumns.ToList(); + var columns = _retrieveItemColumns.ToList(); SetFinalColumnsToSelect(query, columns); var commandTextBuilder = new StringBuilder("select ", 512) .AppendJoin(',', columns) @@ -3487,8 +3523,8 @@ namespace Emby.Server.Implementations.Data if (query.IsMovie == true) { if (query.IncludeItemTypes.Length == 0 - || query.IncludeItemTypes.Contains(nameof(Movie)) - || query.IncludeItemTypes.Contains(nameof(Trailer))) + || query.IncludeItemTypes.Contains(BaseItemKind.Movie) + || query.IncludeItemTypes.Contains(BaseItemKind.Trailer)) { whereClauses.Add("(IsMovie is null OR IsMovie=@IsMovie)"); } @@ -3563,31 +3599,81 @@ namespace Emby.Server.Implementations.Data statement?.TryBind("@IsFolder", query.IsFolder); } - var includeTypes = query.IncludeItemTypes.Select(MapIncludeItemTypes).Where(x => x != null).ToArray(); + var includeTypes = query.IncludeItemTypes; // Only specify excluded types if no included types are specified - if (includeTypes.Length == 0) + if (query.IncludeItemTypes.Length == 0) { - var excludeTypes = query.ExcludeItemTypes.Select(MapIncludeItemTypes).Where(x => x != null).ToArray(); + var excludeTypes = query.ExcludeItemTypes; if (excludeTypes.Length == 1) { - whereClauses.Add("type<>@type"); - statement?.TryBind("@type", excludeTypes[0]); + if (_baseItemKindNames.TryGetValue(excludeTypes[0], out var excludeTypeName)) + { + whereClauses.Add("type<>@type"); + statement?.TryBind("@type", excludeTypeName); + } + else + { + Logger.LogWarning("Undefined BaseItemKind to Type mapping: {BaseItemKind}", excludeTypes[0]); + } } else if (excludeTypes.Length > 1) { - var inClause = string.Join(',', excludeTypes.Select(i => "'" + i + "'")); - whereClauses.Add($"type not in ({inClause})"); + var whereBuilder = new StringBuilder("type not in ("); + foreach (var excludeType in excludeTypes) + { + if (_baseItemKindNames.TryGetValue(excludeType, out var baseItemKindName)) + { + whereBuilder + .Append('\'') + .Append(baseItemKindName) + .Append("',"); + } + else + { + Logger.LogWarning("Undefined BaseItemKind to Type mapping: {BaseItemKind}", excludeType); + } + } + + // Remove trailing comma. + whereBuilder.Length--; + whereBuilder.Append(')'); + whereClauses.Add(whereBuilder.ToString()); } } else if (includeTypes.Length == 1) { - whereClauses.Add("type=@type"); - statement?.TryBind("@type", includeTypes[0]); + if (_baseItemKindNames.TryGetValue(includeTypes[0], out var includeTypeName)) + { + whereClauses.Add("type=@type"); + statement?.TryBind("@type", includeTypeName); + } + else + { + Logger.LogWarning("Undefined BaseItemKind to Type mapping: {BaseItemKind}", includeTypes[0]); + } } else if (includeTypes.Length > 1) { - var inClause = string.Join(',', includeTypes.Select(i => "'" + i + "'")); - whereClauses.Add($"type in ({inClause})"); + var whereBuilder = new StringBuilder("type in ("); + foreach (var includeType in includeTypes) + { + if (_baseItemKindNames.TryGetValue(includeType, out var baseItemKindName)) + { + whereBuilder + .Append('\'') + .Append(baseItemKindName) + .Append("',"); + } + else + { + Logger.LogWarning("Undefined BaseItemKind to Type mapping: {BaseItemKind}", includeType); + } + } + + // Remove trailing comma. + whereBuilder.Length--; + whereBuilder.Append(')'); + whereClauses.Add(whereBuilder.ToString()); } if (query.ChannelIds.Count == 1) @@ -3911,7 +3997,7 @@ namespace Emby.Server.Implementations.Data if (query.IsPlayed.HasValue) { // We should probably figure this out for all folders, but for right now, this is the only place where we need it - if (query.IncludeItemTypes.Length == 1 && string.Equals(query.IncludeItemTypes[0], nameof(Series), StringComparison.OrdinalIgnoreCase)) + if (query.IncludeItemTypes.Length == 1 && query.IncludeItemTypes[0] == BaseItemKind.Series) { if (query.IsPlayed.Value) { @@ -4761,27 +4847,27 @@ namespace Emby.Server.Implementations.Data { var list = new List<string>(); - if (IsTypeInQuery(nameof(Person), query)) + if (IsTypeInQuery(BaseItemKind.Person, query)) { list.Add(typeof(Person).FullName); } - if (IsTypeInQuery(nameof(Genre), query)) + if (IsTypeInQuery(BaseItemKind.Genre, query)) { list.Add(typeof(Genre).FullName); } - if (IsTypeInQuery(nameof(MusicGenre), query)) + if (IsTypeInQuery(BaseItemKind.MusicGenre, query)) { list.Add(typeof(MusicGenre).FullName); } - if (IsTypeInQuery(nameof(MusicArtist), query)) + if (IsTypeInQuery(BaseItemKind.MusicArtist, query)) { list.Add(typeof(MusicArtist).FullName); } - if (IsTypeInQuery(nameof(Studio), query)) + if (IsTypeInQuery(BaseItemKind.Studio, query)) { list.Add(typeof(Studio).FullName); } @@ -4789,14 +4875,14 @@ namespace Emby.Server.Implementations.Data return list; } - private bool IsTypeInQuery(string type, InternalItemsQuery query) + private bool IsTypeInQuery(BaseItemKind type, InternalItemsQuery query) { - if (query.ExcludeItemTypes.Contains(type, StringComparer.OrdinalIgnoreCase)) + if (query.ExcludeItemTypes.Contains(type)) { return false; } - return query.IncludeItemTypes.Length == 0 || query.IncludeItemTypes.Contains(type, StringComparer.OrdinalIgnoreCase); + return query.IncludeItemTypes.Length == 0 || query.IncludeItemTypes.Contains(type); } private string GetCleanValue(string value) @@ -4836,12 +4922,12 @@ namespace Emby.Server.Implementations.Data return true; } - if (query.IncludeItemTypes.Contains(nameof(Episode), StringComparer.OrdinalIgnoreCase) - || query.IncludeItemTypes.Contains(nameof(Video), StringComparer.OrdinalIgnoreCase) - || query.IncludeItemTypes.Contains(nameof(Movie), StringComparer.OrdinalIgnoreCase) - || query.IncludeItemTypes.Contains(nameof(MusicVideo), StringComparer.OrdinalIgnoreCase) - || query.IncludeItemTypes.Contains(nameof(Series), StringComparer.OrdinalIgnoreCase) - || query.IncludeItemTypes.Contains(nameof(Season), StringComparer.OrdinalIgnoreCase)) + if (query.IncludeItemTypes.Contains(BaseItemKind.Episode) + || query.IncludeItemTypes.Contains(BaseItemKind.Video) + || query.IncludeItemTypes.Contains(BaseItemKind.Movie) + || query.IncludeItemTypes.Contains(BaseItemKind.MusicVideo) + || query.IncludeItemTypes.Contains(BaseItemKind.Series) + || query.IncludeItemTypes.Contains(BaseItemKind.Season)) { return true; } @@ -4890,22 +4976,6 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type return dict; } - private string MapIncludeItemTypes(string value) - { - if (_types.TryGetValue(value, out string result)) - { - return result; - } - - if (IsValidType(value)) - { - return value; - } - - Logger.LogWarning("Unknown item type: {ItemType}", value); - return null; - } - public void DeleteItem(Guid id) { if (id == Guid.Empty) @@ -5351,7 +5421,7 @@ AND Type = @InternalPersonType)"); stringBuilder.Clear(); } - List<string> columns = _retriveItemColumns.ToList(); + List<string> columns = _retrieveItemColumns.ToList(); // Unfortunately we need to add it to columns to ensure the order of the columns in the select if (!string.IsNullOrEmpty(itemCountColumns)) { @@ -5569,7 +5639,7 @@ AND Type = @InternalPersonType)"); return result; } - private static ItemCounts GetItemCounts(IReadOnlyList<ResultSetValue> reader, int countStartColumn, string[] typesToCount) + private static ItemCounts GetItemCounts(IReadOnlyList<ResultSetValue> reader, int countStartColumn, BaseItemKind[] typesToCount) { var counts = new ItemCounts(); diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs index b91ff6408..a6406827c 100644 --- a/Emby.Server.Implementations/Dto/DtoService.cs +++ b/Emby.Server.Implementations/Dto/DtoService.cs @@ -7,9 +7,9 @@ using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; -using System.Threading.Tasks; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Extensions; using MediaBrowser.Common; using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.Drawing; @@ -294,7 +294,7 @@ namespace Emby.Server.Implementations.Dto path = path.TrimStart('.'); } - if (!string.IsNullOrEmpty(path) && containers.Contains(path, StringComparer.OrdinalIgnoreCase)) + if (!string.IsNullOrEmpty(path) && containers.Contains(path, StringComparison.OrdinalIgnoreCase)) { fileExtensionContainer = path; } @@ -470,7 +470,7 @@ namespace Emby.Server.Implementations.Dto { var parentAlbumIds = _libraryManager.GetItemIds(new InternalItemsQuery { - IncludeItemTypes = new[] { nameof(MusicAlbum) }, + IncludeItemTypes = new[] { BaseItemKind.MusicAlbum }, Name = item.Album, Limit = 1 }); @@ -1404,44 +1404,27 @@ namespace Emby.Server.Implementations.Dto return null; } - ImageDimensions size; - - var defaultAspectRatio = item.GetDefaultPrimaryImageAspectRatio(); - - if (defaultAspectRatio > 0) - { - return defaultAspectRatio; - } - if (!imageInfo.IsLocalFile) { - return null; + return item.GetDefaultPrimaryImageAspectRatio(); } try { - size = _imageProcessor.GetImageDimensions(item, imageInfo); - - if (size.Width <= 0 || size.Height <= 0) + var size = _imageProcessor.GetImageDimensions(item, imageInfo); + var width = size.Width; + var height = size.Height; + if (width > 0 && height > 0) { - return null; + return (double)width / height; } } catch (Exception ex) { - _logger.LogError(ex, "Failed to determine primary image aspect ratio for {0}", imageInfo.Path); - return null; + _logger.LogError(ex, "Failed to determine primary image aspect ratio for {ImagePath}", imageInfo.Path); } - var width = size.Width; - var height = size.Height; - - if (width <= 0 || height <= 0) - { - return null; - } - - return (double)width / height; + return item.GetDefaultPrimaryImageAspectRatio(); } } } diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj index 95acd216d..329a84acb 100644 --- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj +++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj @@ -29,7 +29,7 @@ <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="6.0.0" /> <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="6.0.0" /> <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="6.0.0" /> - <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.0" /> + <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.1" /> <PackageReference Include="Mono.Nat" Version="3.0.2" /> <PackageReference Include="prometheus-net.DotNetRuntime" Version="4.2.2" /> <PackageReference Include="sharpcompress" Version="0.30.1" /> diff --git a/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs b/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs index d325fa14f..06e57ad12 100644 --- a/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs +++ b/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs @@ -13,7 +13,6 @@ using Jellyfin.Networking.Configuration; using MediaBrowser.Controller; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Plugins; -using MediaBrowser.Model.Dlna; using Microsoft.Extensions.Logging; using Mono.Nat; diff --git a/Emby.Server.Implementations/HttpServer/Security/AuthService.cs b/Emby.Server.Implementations/HttpServer/Security/AuthService.cs index e7103ec95..1d04f3da3 100644 --- a/Emby.Server.Implementations/HttpServer/Security/AuthService.cs +++ b/Emby.Server.Implementations/HttpServer/Security/AuthService.cs @@ -2,7 +2,6 @@ using System.Threading.Tasks; using Jellyfin.Data.Enums; -using MediaBrowser.Controller.Authentication; using MediaBrowser.Controller.Net; using Microsoft.AspNetCore.Http; diff --git a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs index 5f25f6980..0eca2a608 100644 --- a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs +++ b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs @@ -180,7 +180,7 @@ namespace Emby.Server.Implementations.HttpServer } WebSocketMessage<object>? stub; - long bytesConsumed = 0; + long bytesConsumed; try { stub = DeserializeWebSocketMessage(buffer, out bytesConsumed); diff --git a/Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs b/Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs index 0229fbae7..7e12ebb08 100644 --- a/Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs +++ b/Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs @@ -28,35 +28,35 @@ namespace Emby.Server.Implementations.Images var view = (CollectionFolder)item; var viewType = view.CollectionType; - string[] includeItemTypes; + BaseItemKind[] includeItemTypes; if (string.Equals(viewType, CollectionType.Movies, StringComparison.Ordinal)) { - includeItemTypes = new string[] { "Movie" }; + includeItemTypes = new[] { BaseItemKind.Movie }; } else if (string.Equals(viewType, CollectionType.TvShows, StringComparison.Ordinal)) { - includeItemTypes = new string[] { "Series" }; + includeItemTypes = new[] { BaseItemKind.Series }; } else if (string.Equals(viewType, CollectionType.Music, StringComparison.Ordinal)) { - includeItemTypes = new string[] { "MusicAlbum" }; + includeItemTypes = new[] { BaseItemKind.MusicAlbum }; } else if (string.Equals(viewType, CollectionType.Books, StringComparison.Ordinal)) { - includeItemTypes = new string[] { "Book", "AudioBook" }; + includeItemTypes = new[] { BaseItemKind.Book, BaseItemKind.AudioBook }; } else if (string.Equals(viewType, CollectionType.BoxSets, StringComparison.Ordinal)) { - includeItemTypes = new string[] { "BoxSet" }; + includeItemTypes = new[] { BaseItemKind.BoxSet }; } else if (string.Equals(viewType, CollectionType.HomeVideos, StringComparison.Ordinal) || string.Equals(viewType, CollectionType.Photos, StringComparison.Ordinal)) { - includeItemTypes = new string[] { "Video", "Photo" }; + includeItemTypes = new[] { BaseItemKind.Video, BaseItemKind.Photo }; } else { - includeItemTypes = new string[] { "Video", "Audio", "Photo", "Movie", "Series" }; + includeItemTypes = new[] { BaseItemKind.Video, BaseItemKind.Audio, BaseItemKind.Photo, BaseItemKind.Movie, BaseItemKind.Series }; } var recursive = !string.Equals(CollectionType.Playlists, viewType, StringComparison.OrdinalIgnoreCase); diff --git a/Emby.Server.Implementations/Images/DynamicImageProvider.cs b/Emby.Server.Implementations/Images/DynamicImageProvider.cs index 900b3fd9c..575680653 100644 --- a/Emby.Server.Implementations/Images/DynamicImageProvider.cs +++ b/Emby.Server.Implementations/Images/DynamicImageProvider.cs @@ -6,6 +6,8 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using Jellyfin.Data.Enums; +using Jellyfin.Extensions; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Dto; @@ -34,14 +36,14 @@ namespace Emby.Server.Implementations.Images var view = (UserView)item; var isUsingCollectionStrip = IsUsingCollectionStrip(view); - var recursive = isUsingCollectionStrip && !new[] { CollectionType.BoxSets, CollectionType.Playlists }.Contains(view.ViewType ?? string.Empty, StringComparer.OrdinalIgnoreCase); + var recursive = isUsingCollectionStrip && !new[] { CollectionType.BoxSets, CollectionType.Playlists }.Contains(view.ViewType ?? string.Empty, StringComparison.OrdinalIgnoreCase); var result = view.GetItemList(new InternalItemsQuery { User = view.UserId.HasValue ? _userManager.GetUserById(view.UserId.Value) : null, CollapseBoxSetItems = false, Recursive = recursive, - ExcludeItemTypes = new[] { "UserView", "CollectionFolder", "Person" }, + ExcludeItemTypes = new[] { BaseItemKind.UserView, BaseItemKind.CollectionFolder, BaseItemKind.Person }, DtoOptions = new DtoOptions(false) }); diff --git a/Emby.Server.Implementations/Images/GenreImageProvider.cs b/Emby.Server.Implementations/Images/GenreImageProvider.cs index 1f5090f7f..968bf5fa3 100644 --- a/Emby.Server.Implementations/Images/GenreImageProvider.cs +++ b/Emby.Server.Implementations/Images/GenreImageProvider.cs @@ -8,8 +8,6 @@ using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Entities.Movies; -using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; @@ -43,7 +41,7 @@ namespace Emby.Server.Implementations.Images return _libraryManager.GetItemList(new InternalItemsQuery { Genres = new[] { item.Name }, - IncludeItemTypes = new[] { nameof(Series), nameof(Movie) }, + IncludeItemTypes = new[] { BaseItemKind.Series, BaseItemKind.Movie }, OrderBy = new[] { (ItemSortBy.Random, SortOrder.Ascending) }, Limit = 4, Recursive = true, diff --git a/Emby.Server.Implementations/Images/MusicGenreImageProvider.cs b/Emby.Server.Implementations/Images/MusicGenreImageProvider.cs index baf1c9051..31f053f06 100644 --- a/Emby.Server.Implementations/Images/MusicGenreImageProvider.cs +++ b/Emby.Server.Implementations/Images/MusicGenreImageProvider.cs @@ -44,9 +44,9 @@ namespace Emby.Server.Implementations.Images Genres = new[] { item.Name }, IncludeItemTypes = new[] { - nameof(MusicAlbum), - nameof(MusicVideo), - nameof(Audio) + BaseItemKind.MusicAlbum, + BaseItemKind.MusicVideo, + BaseItemKind.Audio }, OrderBy = new[] { (ItemSortBy.Random, SortOrder.Ascending) }, Limit = 4, diff --git a/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs b/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs index 29758a078..e558fbe27 100644 --- a/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs +++ b/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs @@ -54,20 +54,10 @@ namespace Emby.Server.Implementations.Library { if (parent != null) { - // Ignore trailer folders but allow it at the collection level - if (string.Equals(filename, BaseItem.TrailersFolderName, StringComparison.OrdinalIgnoreCase) - && !(parent is AggregateFolder) - && !(parent is UserRootFolder)) - { - return true; - } - - if (string.Equals(filename, BaseItem.ThemeVideosFolderName, StringComparison.OrdinalIgnoreCase)) - { - return true; - } - - if (string.Equals(filename, BaseItem.ThemeSongsFolderName, StringComparison.OrdinalIgnoreCase)) + // Ignore extras folders but allow it at the collection level + if (_namingOptions.AllExtrasTypesFolderNames.ContainsKey(filename) + && parent is not AggregateFolder + && parent is not UserRootFolder) { return true; } diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index 778b6225e..270264dba 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -11,11 +11,9 @@ using System.Net; using System.Net.Http; using System.Threading; using System.Threading.Tasks; -using Emby.Naming.Audio; using Emby.Naming.Common; using Emby.Naming.TV; using Emby.Naming.Video; -using Emby.Server.Implementations.Library.Resolvers; using Emby.Server.Implementations.Library.Validators; using Emby.Server.Implementations.Playlists; using Emby.Server.Implementations.ScheduledTasks; @@ -533,8 +531,8 @@ namespace Emby.Server.Implementations.Library return key.GetMD5(); } - public BaseItem ResolvePath(FileSystemMetadata fileInfo, Folder parent = null) - => ResolvePath(fileInfo, new DirectoryService(_fileSystem), null, parent); + public BaseItem ResolvePath(FileSystemMetadata fileInfo, Folder parent = null, IDirectoryService directoryService = null) + => ResolvePath(fileInfo, directoryService ?? new DirectoryService(_fileSystem), null, parent); private BaseItem ResolvePath( FileSystemMetadata fileInfo, @@ -654,7 +652,7 @@ namespace Emby.Server.Implementations.Library return !args.ContainsFileSystemEntryByName(".ignore"); } - public IEnumerable<BaseItem> ResolvePaths(IEnumerable<FileSystemMetadata> files, IDirectoryService directoryService, Folder parent, LibraryOptions libraryOptions, string collectionType) + public IEnumerable<BaseItem> ResolvePaths(IEnumerable<FileSystemMetadata> files, IDirectoryService directoryService, Folder parent, LibraryOptions libraryOptions, string collectionType = null) { return ResolvePaths(files, directoryService, parent, libraryOptions, collectionType, EntityResolvers); } @@ -677,7 +675,7 @@ namespace Emby.Server.Implementations.Library { var result = resolver.ResolveMultiple(parent, fileList, collectionType, directoryService); - if (result != null && result.Items.Count > 0) + if (result?.Items.Count > 0) { var items = new List<BaseItem>(); items.AddRange(result.Items); @@ -965,7 +963,7 @@ namespace Emby.Server.Implementations.Library { var existing = GetItemList(new InternalItemsQuery { - IncludeItemTypes = new[] { nameof(MusicArtist) }, + IncludeItemTypes = new[] { BaseItemKind.MusicArtist }, Name = name, DtoOptions = options }).Cast<MusicArtist>() @@ -2685,89 +2683,105 @@ namespace Emby.Server.Implementations.Library }; } - public IEnumerable<Video> FindTrailers(BaseItem owner, List<FileSystemMetadata> fileSystemChildren, IDirectoryService directoryService) + public IEnumerable<BaseItem> FindExtras(BaseItem owner, List<FileSystemMetadata> fileSystemChildren, IDirectoryService directoryService) { - var namingOptions = _namingOptions; - - var files = owner.IsInMixedFolder ? new List<FileSystemMetadata>() : fileSystemChildren.Where(i => i.IsDirectory) - .Where(i => string.Equals(i.Name, BaseItem.TrailersFolderName, StringComparison.OrdinalIgnoreCase)) - .SelectMany(i => _fileSystem.GetFiles(i.FullName, namingOptions.VideoFileExtensions, false, false)) - .ToList(); - - var videos = VideoListResolver.Resolve(fileSystemChildren, namingOptions); - - var currentVideo = videos.FirstOrDefault(i => string.Equals(owner.Path, i.Files[0].Path, StringComparison.OrdinalIgnoreCase)); - - if (currentVideo != null) + var ownerVideoInfo = VideoResolver.Resolve(owner.Path, owner.IsFolder, _namingOptions); + if (ownerVideoInfo == null) { - files.AddRange(currentVideo.Extras.Where(i => i.ExtraType == ExtraType.Trailer).Select(i => _fileSystem.GetFileInfo(i.Path))); + yield break; } - var resolvers = new IItemResolver[] + var count = fileSystemChildren.Count; + var files = new List<VideoFileInfo>(); + var nonVideoFiles = new List<FileSystemMetadata>(); + for (var i = 0; i < count; i++) { - new GenericVideoResolver<Trailer>(_namingOptions) - }; - - return ResolvePaths(files, directoryService, null, new LibraryOptions(), null, resolvers) - .OfType<Trailer>() - .Select(video => + var current = fileSystemChildren[i]; + if (current.IsDirectory && _namingOptions.AllExtrasTypesFolderNames.ContainsKey(current.Name)) { - // Try to retrieve it from the db. If we don't find it, use the resolved version - if (GetItemById(video.Id) is Trailer dbItem) + var filesInSubFolder = _fileSystem.GetFiles(current.FullName, _namingOptions.VideoFileExtensions, false, false); + foreach (var file in filesInSubFolder) { - video = dbItem; + var videoInfo = VideoResolver.Resolve(file.FullName, file.IsDirectory, _namingOptions); + if (videoInfo == null) + { + nonVideoFiles.Add(file); + continue; + } + + files.Add(videoInfo); + } + } + else if (!current.IsDirectory) + { + var videoInfo = VideoResolver.Resolve(current.FullName, current.IsDirectory, _namingOptions); + if (videoInfo == null) + { + nonVideoFiles.Add(current); + continue; } - video.ParentId = Guid.Empty; - video.OwnerId = owner.Id; - video.ExtraType = ExtraType.Trailer; - video.TrailerTypes = new[] { TrailerType.LocalTrailer }; - - return video; - - // Sort them so that the list can be easily compared for changes - }).OrderBy(i => i.Path); - } - - public IEnumerable<Video> FindExtras(BaseItem owner, List<FileSystemMetadata> fileSystemChildren, IDirectoryService directoryService) - { - var namingOptions = _namingOptions; - - var files = owner.IsInMixedFolder ? new List<FileSystemMetadata>() : fileSystemChildren.Where(i => i.IsDirectory) - .Where(i => BaseItem.AllExtrasTypesFolderNames.ContainsKey(i.Name ?? string.Empty)) - .SelectMany(i => _fileSystem.GetFiles(i.FullName, namingOptions.VideoFileExtensions, false, false)) - .ToList(); - - var videos = VideoListResolver.Resolve(fileSystemChildren, namingOptions); - - var currentVideo = videos.FirstOrDefault(i => string.Equals(owner.Path, i.Files[0].Path, StringComparison.OrdinalIgnoreCase)); - - if (currentVideo != null) - { - files.AddRange(currentVideo.Extras.Where(i => i.ExtraType != ExtraType.Trailer).Select(i => _fileSystem.GetFileInfo(i.Path))); + files.Add(videoInfo); + } } - return ResolvePaths(files, directoryService, null, new LibraryOptions(), null) - .OfType<Video>() - .Select(video => + if (files.Count == 0) + { + yield break; + } + + var videos = VideoListResolver.Resolve(files, _namingOptions); + // owner video info cannot be null as that implies it has no path + var extras = ExtraResolver.GetExtras(videos, ownerVideoInfo, _namingOptions.VideoFlagDelimiters); + for (var i = 0; i < extras.Count; i++) + { + var currentExtra = extras[i]; + var resolved = ResolvePath(_fileSystem.GetFileInfo(currentExtra.Path), null, directoryService); + if (resolved is not Video video) { - // Try to retrieve it from the db. If we don't find it, use the resolved version - var dbItem = GetItemById(video.Id) as Video; + continue; + } - if (dbItem != null) - { - video = dbItem; - } + // Try to retrieve it from the db. If we don't find it, use the resolved version + if (GetItemById(resolved.Id) is Video dbItem) + { + video = dbItem; + } - video.ParentId = Guid.Empty; - video.OwnerId = owner.Id; + video.ExtraType = currentExtra.ExtraType; + video.ParentId = Guid.Empty; + video.OwnerId = owner.Id; + yield return video; + } - SetExtraTypeFromFilename(video); + // TODO: theme songs must be handled "manually" (but should we?) since they aren't video files + for (var i = 0; i < nonVideoFiles.Count; i++) + { + var current = nonVideoFiles[i]; + var extraInfo = ExtraResolver.GetExtraInfo(current.FullName, _namingOptions); + if (extraInfo.ExtraType != ExtraType.ThemeSong) + { + continue; + } - return video; + var resolved = ResolvePath(current, null, directoryService); + if (resolved is not Audio themeSong) + { + continue; + } - // Sort them so that the list can be easily compared for changes - }).OrderBy(i => i.Path); + // Try to retrieve it from the db. If we don't find it, use the resolved version + if (GetItemById(themeSong.Id) is Audio dbItem) + { + themeSong = dbItem; + } + + themeSong.ExtraType = ExtraType.ThemeSong; + themeSong.OwnerId = owner.Id; + themeSong.ParentId = Guid.Empty; + + yield return themeSong; + } } public string GetPathAfterNetworkSubstitution(string path, BaseItem ownerItem) @@ -2817,15 +2831,6 @@ namespace Emby.Server.Implementations.Library return path; } - private void SetExtraTypeFromFilename(Video item) - { - var resolver = new ExtraResolver(_namingOptions); - - var result = resolver.GetExtraInfo(item.Path); - - item.ExtraType = result.ExtraType; - } - public List<PersonInfo> GetPeople(InternalPeopleQuery query) { return _itemRepository.GetPeople(query); @@ -2932,11 +2937,12 @@ namespace Emby.Server.Implementations.Library var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath; + var existingNameCount = 1; // first numbered name will be 2 var virtualFolderPath = Path.Combine(rootFolderPath, name); while (Directory.Exists(virtualFolderPath)) { - name += "1"; - virtualFolderPath = Path.Combine(rootFolderPath, name); + existingNameCount++; + virtualFolderPath = Path.Combine(rootFolderPath, name + " " + existingNameCount); } var mediaPathInfos = options.PathInfos; diff --git a/Emby.Server.Implementations/Library/MediaStreamSelector.cs b/Emby.Server.Implementations/Library/MediaStreamSelector.cs index b3837fedb..da0c89c13 100644 --- a/Emby.Server.Implementations/Library/MediaStreamSelector.cs +++ b/Emby.Server.Implementations/Library/MediaStreamSelector.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; using System.Linq; using Jellyfin.Data.Enums; +using Jellyfin.Extensions; using MediaBrowser.Model.Entities; namespace Emby.Server.Implementations.Library @@ -64,18 +65,18 @@ namespace Emby.Server.Implementations.Library stream = sortedStreams.FirstOrDefault(s => s.IsExternal || s.IsForced || s.IsDefault); // if the audio language is not understood by the user, load their preferred subs, if there are any - if (stream == null && !preferredLanguages.Contains(audioTrackLanguage, StringComparer.OrdinalIgnoreCase)) + if (stream == null && !preferredLanguages.Contains(audioTrackLanguage, StringComparison.OrdinalIgnoreCase)) { - stream = sortedStreams.FirstOrDefault(s => !s.IsForced && preferredLanguages.Contains(s.Language, StringComparer.OrdinalIgnoreCase)); + stream = sortedStreams.FirstOrDefault(s => !s.IsForced && preferredLanguages.Contains(s.Language, StringComparison.OrdinalIgnoreCase)); } } else if (mode == SubtitlePlaybackMode.Smart) { // if the audio language is not understood by the user, load their preferred subs, if there are any - if (!preferredLanguages.Contains(audioTrackLanguage, StringComparer.OrdinalIgnoreCase)) + if (!preferredLanguages.Contains(audioTrackLanguage, StringComparison.OrdinalIgnoreCase)) { - stream = streams.FirstOrDefault(s => !s.IsForced && preferredLanguages.Contains(s.Language, StringComparer.OrdinalIgnoreCase)) ?? - streams.FirstOrDefault(s => preferredLanguages.Contains(s.Language, StringComparer.OrdinalIgnoreCase)); + stream = streams.FirstOrDefault(s => !s.IsForced && preferredLanguages.Contains(s.Language, StringComparison.OrdinalIgnoreCase)) ?? + streams.FirstOrDefault(s => preferredLanguages.Contains(s.Language, StringComparison.OrdinalIgnoreCase)); } } else if (mode == SubtitlePlaybackMode.Always) @@ -136,9 +137,9 @@ namespace Emby.Server.Implementations.Library else if (mode == SubtitlePlaybackMode.Smart) { // Prefer smart logic over embedded metadata - if (!preferredLanguages.Contains(audioTrackLanguage, StringComparer.OrdinalIgnoreCase)) + if (!preferredLanguages.Contains(audioTrackLanguage, StringComparison.OrdinalIgnoreCase)) { - filteredStreams = streams.Where(s => !s.IsForced && preferredLanguages.Contains(s.Language, StringComparer.OrdinalIgnoreCase)) + filteredStreams = streams.Where(s => !s.IsForced && preferredLanguages.Contains(s.Language, StringComparison.OrdinalIgnoreCase)) .ToList(); } } diff --git a/Emby.Server.Implementations/Library/MusicManager.cs b/Emby.Server.Implementations/Library/MusicManager.cs index e2f1fb0ad..d33213564 100644 --- a/Emby.Server.Implementations/Library/MusicManager.cs +++ b/Emby.Server.Implementations/Library/MusicManager.cs @@ -52,7 +52,7 @@ namespace Emby.Server.Implementations.Library var genres = item .GetRecursiveChildren(user, new InternalItemsQuery(user) { - IncludeItemTypes = new[] { nameof(Audio) }, + IncludeItemTypes = new[] { BaseItemKind.Audio }, DtoOptions = dtoOptions }) .Cast<Audio>() @@ -89,7 +89,7 @@ namespace Emby.Server.Implementations.Library { return _libraryManager.GetItemList(new InternalItemsQuery(user) { - IncludeItemTypes = new[] { nameof(Audio) }, + IncludeItemTypes = new[] { BaseItemKind.Audio }, GenreIds = genreIds.ToArray(), diff --git a/Emby.Server.Implementations/Library/PathExtensions.cs b/Emby.Server.Implementations/Library/PathExtensions.cs index d5b855cdf..6f61dc713 100644 --- a/Emby.Server.Implementations/Library/PathExtensions.cs +++ b/Emby.Server.Implementations/Library/PathExtensions.cs @@ -16,7 +16,7 @@ namespace Emby.Server.Implementations.Library /// <param name="attribute">The attrib.</param> /// <returns>System.String.</returns> /// <exception cref="ArgumentException"><paramref name="str" /> or <paramref name="attribute" /> is empty.</exception> - public static string? GetAttributeValue(this string str, string attribute) + public static string? GetAttributeValue(this ReadOnlySpan<char> str, ReadOnlySpan<char> attribute) { if (str.Length == 0) { @@ -28,17 +28,31 @@ namespace Emby.Server.Implementations.Library throw new ArgumentException("String can't be empty.", nameof(attribute)); } - string srch = "[" + attribute + "="; - int start = str.IndexOf(srch, StringComparison.OrdinalIgnoreCase); - if (start != -1) + var attributeIndex = str.IndexOf(attribute, StringComparison.OrdinalIgnoreCase); + + // Must be at least 3 characters after the attribute =, ], any character. + var maxIndex = str.Length - attribute.Length - 3; + while (attributeIndex > -1 && attributeIndex < maxIndex) { - start += srch.Length; - int end = str.IndexOf(']', start); - return str.Substring(start, end - start); + var attributeEnd = attributeIndex + attribute.Length; + if (attributeIndex > 0 + && str[attributeIndex - 1] == '[' + && str[attributeEnd] == '=') + { + var closingIndex = str[attributeEnd..].IndexOf(']'); + // Must be at least 1 character before the closing bracket. + if (closingIndex > 1) + { + return str[(attributeEnd + 1)..(attributeEnd + closingIndex)].Trim().ToString(); + } + } + + str = str[attributeEnd..]; + attributeIndex = str.IndexOf(attribute, StringComparison.OrdinalIgnoreCase); } // for imdbid we also accept pattern matching - if (string.Equals(attribute, "imdbid", StringComparison.OrdinalIgnoreCase)) + if (attribute.Equals("imdbid", StringComparison.OrdinalIgnoreCase)) { var match = ProviderIdParsers.TryFindImdbId(str, out var imdbId); return match ? imdbId.ToString() : null; diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs index 27e18be42..210ed0953 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs @@ -4,12 +4,10 @@ using System; using System.Linq; using System.Threading.Tasks; using Emby.Naming.Common; -using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Resolvers; using MediaBrowser.Model.Entities; -using MediaBrowser.Model.IO; using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.Library.Resolvers.Audio diff --git a/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs b/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs index 0ebf0e530..9222a9479 100644 --- a/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs @@ -49,120 +49,71 @@ namespace Emby.Server.Implementations.Library.Resolvers protected virtual TVideoType ResolveVideo<TVideoType>(ItemResolveArgs args, bool parseName) where TVideoType : Video, new() { - var namingOptions = NamingOptions; + VideoFileInfo videoInfo = null; + VideoType? videoType = null; // If the path is a file check for a matching extensions if (args.IsDirectory) { - TVideoType video = null; - VideoFileInfo videoInfo = null; - // Loop through each child file/folder and see if we find a video foreach (var child in args.FileSystemChildren) { var filename = child.Name; - if (child.IsDirectory) { if (IsDvdDirectory(child.FullName, filename, args.DirectoryService)) { - videoInfo = VideoResolver.ResolveDirectory(args.Path, namingOptions); - - if (videoInfo == null) - { - return null; - } - - video = new TVideoType - { - Path = args.Path, - VideoType = VideoType.Dvd, - ProductionYear = videoInfo.Year - }; - break; + videoType = VideoType.Dvd; } - - if (IsBluRayDirectory(filename)) + else if (IsBluRayDirectory(filename)) { - videoInfo = VideoResolver.ResolveDirectory(args.Path, namingOptions); - - if (videoInfo == null) - { - return null; - } - - video = new TVideoType - { - Path = args.Path, - VideoType = VideoType.BluRay, - ProductionYear = videoInfo.Year - }; - break; + videoType = VideoType.BluRay; } } else if (IsDvdFile(filename)) { - videoInfo = VideoResolver.ResolveDirectory(args.Path, namingOptions); - - if (videoInfo == null) - { - return null; - } - - video = new TVideoType - { - Path = args.Path, - VideoType = VideoType.Dvd, - ProductionYear = videoInfo.Year - }; - break; + videoType = VideoType.Dvd; } + + if (videoType == null) + { + continue; + } + + videoInfo = VideoResolver.ResolveDirectory(args.Path, NamingOptions, parseName); + break; } - - if (video != null) - { - video.Name = parseName ? - videoInfo.Name : - Path.GetFileName(args.Path); - - Set3DFormat(video, videoInfo); - } - - return video; } else { - var videoInfo = VideoResolver.Resolve(args.Path, false, namingOptions, false); - - if (videoInfo == null) - { - return null; - } - - if (VideoResolver.IsVideoFile(args.Path, NamingOptions) || videoInfo.IsStub) - { - var path = args.Path; - - var video = new TVideoType - { - Path = path, - IsInMixedFolder = true, - ProductionYear = videoInfo.Year - }; - - SetVideoType(video, videoInfo); - - video.Name = parseName ? - videoInfo.Name : - Path.GetFileNameWithoutExtension(args.Path); - - Set3DFormat(video, videoInfo); - - return video; - } + videoInfo = VideoResolver.Resolve(args.Path, false, NamingOptions, parseName); } - return null; + if (videoInfo == null || (!videoInfo.IsStub && !VideoResolver.IsVideoFile(args.Path, NamingOptions))) + { + return null; + } + + var video = new TVideoType + { + Name = videoInfo.Name, + Path = args.Path, + ProductionYear = videoInfo.Year, + ExtraType = videoInfo.ExtraType + }; + + if (videoType.HasValue) + { + video.VideoType = videoType.Value; + } + else + { + SetVideoType(video, videoInfo); + } + + Set3DFormat(video, videoInfo); + + return video; } protected void SetVideoType(Video video, VideoFileInfo videoInfo) @@ -207,8 +158,8 @@ namespace Emby.Server.Implementations.Library.Resolvers { // use disc-utils, both DVDs and BDs use UDF filesystem using (var videoFileStream = File.Open(video.Path, FileMode.Open, FileAccess.Read)) + using (UdfReader udfReader = new UdfReader(videoFileStream)) { - UdfReader udfReader = new UdfReader(videoFileStream); if (udfReader.DirectoryExists("VIDEO_TS")) { video.IsoType = IsoType.Dvd; diff --git a/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs index e685c87f1..8f224f547 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs @@ -5,6 +5,7 @@ using System; using System.IO; using System.Linq; +using Jellyfin.Extensions; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Model.Entities; @@ -32,7 +33,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books var extension = Path.GetExtension(args.Path); - if (extension != null && _validExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase)) + if (extension != null && _validExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase)) { // It's a book return new Book diff --git a/Emby.Server.Implementations/Library/Resolvers/Movies/BoxSetResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Movies/BoxSetResolver.cs index e7abe1e6d..6cc04ea81 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Movies/BoxSetResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Movies/BoxSetResolver.cs @@ -65,7 +65,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies private static void SetProviderIdFromPath(BaseItem item) { // we need to only look at the name of this actual item (not parents) - var justName = Path.GetFileName(item.Path); + var justName = Path.GetFileName(item.Path.AsSpan()); var id = justName.GetAttributeValue("tmdbid"); diff --git a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs index 732be0fe5..4feaf3fb4 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs @@ -26,7 +26,6 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies public class MovieResolver : BaseVideoResolver<Video>, IMultiItemResolver { private readonly IImageProcessor _imageProcessor; - private readonly StackResolver _stackResolver; private string[] _validCollectionTypes = new[] { @@ -46,7 +45,6 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies : base(namingOptions) { _imageProcessor = imageProcessor; - _stackResolver = new StackResolver(NamingOptions); } /// <summary> @@ -62,7 +60,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies string collectionType, IDirectoryService directoryService) { - var result = ResolveMultipleInternal(parent, files, collectionType, directoryService); + var result = ResolveMultipleInternal(parent, files, collectionType); if (result != null) { @@ -92,16 +90,17 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies return null; } + Video movie = null; var files = args.GetActualFileSystemChildren().ToList(); if (string.Equals(collectionType, CollectionType.MusicVideos, StringComparison.OrdinalIgnoreCase)) { - return FindMovie<MusicVideo>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, false); + movie = FindMovie<MusicVideo>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, false); } if (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase)) { - return FindMovie<Video>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, false); + movie = FindMovie<Video>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, false); } if (string.IsNullOrEmpty(collectionType)) @@ -118,17 +117,16 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies return null; } - { - return FindMovie<Movie>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, true); - } + movie = FindMovie<Movie>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, true); } if (string.Equals(collectionType, CollectionType.Movies, StringComparison.OrdinalIgnoreCase)) { - return FindMovie<Movie>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, true); + movie = FindMovie<Movie>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, true); } - return null; + // ignore extras + return movie?.ExtraType == null ? movie : null; } // Handle owned items @@ -169,6 +167,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies item = ResolveVideo<Video>(args, false); } + // Ignore extras + if (item?.ExtraType != null) + { + return null; + } + if (item != null) { item.IsInMixedFolder = true; @@ -180,8 +184,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies private MultiItemResolverResult ResolveMultipleInternal( Folder parent, List<FileSystemMetadata> files, - string collectionType, - IDirectoryService directoryService) + string collectionType) { if (IsInvalid(parent, collectionType)) { @@ -190,13 +193,13 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies if (string.Equals(collectionType, CollectionType.MusicVideos, StringComparison.OrdinalIgnoreCase)) { - return ResolveVideos<MusicVideo>(parent, files, directoryService, true, collectionType, false); + return ResolveVideos<MusicVideo>(parent, files, true, collectionType, false); } if (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase) || string.Equals(collectionType, CollectionType.Photos, StringComparison.OrdinalIgnoreCase)) { - return ResolveVideos<Video>(parent, files, directoryService, false, collectionType, false); + return ResolveVideos<Video>(parent, files, false, collectionType, false); } if (string.IsNullOrEmpty(collectionType)) @@ -204,7 +207,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies // Owned items should just use the plain video type if (parent == null) { - return ResolveVideos<Video>(parent, files, directoryService, false, collectionType, false); + return ResolveVideos<Video>(parent, files, false, collectionType, false); } if (parent is Series || parent.GetParents().OfType<Series>().Any()) @@ -212,12 +215,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies return null; } - return ResolveVideos<Movie>(parent, files, directoryService, false, collectionType, true); + return ResolveVideos<Movie>(parent, files, false, collectionType, true); } if (string.Equals(collectionType, CollectionType.Movies, StringComparison.OrdinalIgnoreCase)) { - return ResolveVideos<Movie>(parent, files, directoryService, true, collectionType, true); + return ResolveVideos<Movie>(parent, files, true, collectionType, true); } return null; @@ -226,21 +229,20 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies private MultiItemResolverResult ResolveVideos<T>( Folder parent, IEnumerable<FileSystemMetadata> fileSystemEntries, - IDirectoryService directoryService, - bool suppportMultiEditions, + bool supportMultiEditions, string collectionType, bool parseName) where T : Video, new() { var files = new List<FileSystemMetadata>(); - var videos = new List<BaseItem>(); var leftOver = new List<FileSystemMetadata>(); + var hasCollectionType = !string.IsNullOrEmpty(collectionType); // Loop through each child file/folder and see if we find a video foreach (var child in fileSystemEntries) { // This is a hack but currently no better way to resolve a sometimes ambiguous situation - if (string.IsNullOrEmpty(collectionType)) + if (!hasCollectionType) { if (string.Equals(child.Name, "tvshow.nfo", StringComparison.OrdinalIgnoreCase) || string.Equals(child.Name, "season.nfo", StringComparison.OrdinalIgnoreCase)) @@ -259,29 +261,39 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies } } - var resolverResult = VideoListResolver.Resolve(files, NamingOptions, suppportMultiEditions).ToList(); + var videoInfos = files + .Select(i => VideoResolver.Resolve(i.FullName, i.IsDirectory, NamingOptions, parseName)) + .Where(f => f != null) + .ToList(); + + var resolverResult = VideoListResolver.Resolve(videoInfos, NamingOptions, supportMultiEditions, parseName); var result = new MultiItemResolverResult { - ExtraFiles = leftOver, - Items = videos + ExtraFiles = leftOver }; - var isInMixedFolder = resolverResult.Count > 1 || (parent != null && parent.IsTopParent); + var isInMixedFolder = resolverResult.Count > 1 || parent?.IsTopParent == true; foreach (var video in resolverResult) { var firstVideo = video.Files[0]; + var path = firstVideo.Path; + if (video.ExtraType != null) + { + result.ExtraFiles.Add(files.Find(f => string.Equals(f.FullName, path, StringComparison.OrdinalIgnoreCase))); + continue; + } + + var additionalParts = video.Files.Count > 1 ? video.Files.Skip(1).Select(i => i.Path).ToArray() : Array.Empty<string>(); var videoItem = new T { - Path = video.Files[0].Path, + Path = path, IsInMixedFolder = isInMixedFolder, ProductionYear = video.Year, - Name = parseName ? - video.Name : - Path.GetFileNameWithoutExtension(video.Files[0].Path), - AdditionalParts = video.Files.Skip(1).Select(i => i.Path).ToArray(), + Name = parseName ? video.Name : firstVideo.Name, + AdditionalParts = additionalParts, LocalAlternateVersions = video.AlternateVersions.Select(i => i.Path).ToArray() }; @@ -299,21 +311,34 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies private static bool IsIgnored(string filename) { // Ignore samples - Match m = Regex.Match(filename, @"\bsample\b", RegexOptions.IgnoreCase); + Match m = Regex.Match(filename, @"\bsample\b", RegexOptions.IgnoreCase | RegexOptions.Compiled); return m.Success; } - private bool ContainsFile(List<VideoInfo> result, FileSystemMetadata file) + private static bool ContainsFile(IReadOnlyList<VideoInfo> result, FileSystemMetadata file) { - return result.Any(i => ContainsFile(i, file)); - } + for (var i = 0; i < result.Count; i++) + { + var current = result[i]; + for (var j = 0; j < current.Files.Count; j++) + { + if (ContainsFile(current.Files[j], file)) + { + return true; + } + } - private bool ContainsFile(VideoInfo result, FileSystemMetadata file) - { - return result.Files.Any(i => ContainsFile(i, file)) || - result.AlternateVersions.Any(i => ContainsFile(i, file)) || - result.Extras.Any(i => ContainsFile(i, file)); + for (var j = 0; j < current.AlternateVersions.Count; j++) + { + if (ContainsFile(current.AlternateVersions[j], file)) + { + return true; + } + } + } + + return false; } private static bool ContainsFile(VideoFileInfo result, FileSystemMetadata file) @@ -342,9 +367,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies if (item is Movie || item is MusicVideo) { // We need to only look at the name of this actual item (not parents) - var justName = item.IsInMixedFolder ? Path.GetFileName(item.Path) : Path.GetFileName(item.ContainingFolderPath); + var justName = item.IsInMixedFolder ? Path.GetFileName(item.Path.AsSpan()) : Path.GetFileName(item.ContainingFolderPath.AsSpan()); - if (!string.IsNullOrEmpty(justName)) + if (!justName.IsEmpty) { // check for tmdb id var tmdbid = justName.GetAttributeValue("tmdbid"); @@ -358,7 +383,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies if (!string.IsNullOrEmpty(item.Path)) { // check for imdb id - we use full media path, as we can assume, that this will match in any use case (wither id in parent dir or in file name) - var imdbid = item.Path.GetAttributeValue("imdbid"); + var imdbid = item.Path.AsSpan().GetAttributeValue("imdbid"); if (!string.IsNullOrWhiteSpace(imdbid)) { @@ -431,7 +456,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies // TODO: Allow GetMultiDiscMovie in here const bool SupportsMultiVersion = true; - var result = ResolveVideos<T>(parent, fileSystemEntries, directoryService, SupportsMultiVersion, collectionType, parseName) ?? + var result = ResolveVideos<T>(parent, fileSystemEntries, SupportsMultiVersion, collectionType, parseName) ?? new MultiItemResolverResult(); if (result.Items.Count == 1) @@ -510,7 +535,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies return null; } - var result = _stackResolver.ResolveDirectories(folderPaths).ToList(); + var result = StackResolver.ResolveDirectories(folderPaths, NamingOptions).ToList(); if (result.Count != 1) { diff --git a/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs b/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs index 51d819303..e52b43050 100644 --- a/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs @@ -8,6 +8,7 @@ using System.IO; using System.Linq; using Emby.Naming.Common; using Emby.Naming.Video; +using Jellyfin.Extensions; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; @@ -109,7 +110,7 @@ namespace Emby.Server.Implementations.Library.Resolvers } string extension = Path.GetExtension(path).TrimStart('.'); - return imageProcessor.SupportedInputFormats.Contains(extension, StringComparer.OrdinalIgnoreCase); + return imageProcessor.SupportedInputFormats.Contains(extension, StringComparison.OrdinalIgnoreCase); } } } diff --git a/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs b/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs index 8ce59717d..6b0dfe986 100644 --- a/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs @@ -5,6 +5,7 @@ using System; using System.IO; using System.Linq; +using Jellyfin.Extensions; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Playlists; using MediaBrowser.Controller.Resolvers; @@ -57,10 +58,10 @@ namespace Emby.Server.Implementations.Library.Resolvers // Check if this is a music playlist file // It should have the correct collection type and a supported file extension - else if (_musicPlaylistCollectionTypes.Contains(args.CollectionType ?? string.Empty, StringComparer.OrdinalIgnoreCase)) + else if (_musicPlaylistCollectionTypes.Contains(args.CollectionType ?? string.Empty, StringComparison.OrdinalIgnoreCase)) { var extension = Path.GetExtension(args.Path); - if (Playlist.SupportedExtensions.Contains(extension ?? string.Empty, StringComparer.OrdinalIgnoreCase)) + if (Playlist.SupportedExtensions.Contains(extension ?? string.Empty, StringComparison.OrdinalIgnoreCase)) { return new Playlist { diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs index f72da3617..be9905647 100644 --- a/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs @@ -3,7 +3,6 @@ using System; using System.Linq; using Emby.Naming.Common; -using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; using MediaBrowser.Model.Entities; @@ -45,34 +44,36 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV // If the parent is a Season or Series and the parent is not an extras folder, then this is an Episode if the VideoResolver returns something // Also handle flat tv folders - if ((season != null || - string.Equals(args.GetCollectionType(), CollectionType.TvShows, StringComparison.OrdinalIgnoreCase) || - args.HasParent<Series>()) - && (parent is Series || !BaseItem.AllExtrasTypesFolderNames.ContainsKey(parent.Name))) + if (season != null || + string.Equals(args.GetCollectionType(), CollectionType.TvShows, StringComparison.OrdinalIgnoreCase) || + args.HasParent<Series>()) { var episode = ResolveVideo<Episode>(args, false); - if (episode != null) + // Ignore extras + if (episode == null || episode.ExtraType != null) { - var series = parent as Series ?? parent.GetParents().OfType<Series>().FirstOrDefault(); + return null; + } - if (series != null) - { - episode.SeriesId = series.Id; - episode.SeriesName = series.Name; - } + var series = parent as Series ?? parent.GetParents().OfType<Series>().FirstOrDefault(); - if (season != null) - { - episode.SeasonId = season.Id; - episode.SeasonName = season.Name; - } + if (series != null) + { + episode.SeriesId = series.Id; + episode.SeriesName = series.Name; + } - // Assume season 1 if there's no season folder and a season number could not be determined - if (season == null && !episode.ParentIndexNumber.HasValue && (episode.IndexNumber.HasValue || episode.PremiereDate.HasValue)) - { - episode.ParentIndexNumber = 1; - } + if (season != null) + { + episode.SeasonId = season.Id; + episode.SeasonName = season.Name; + } + + // Assume season 1 if there's no season folder and a season number could not be determined + if (season == null && !episode.ParentIndexNumber.HasValue && (episode.IndexNumber.HasValue || episode.PremiereDate.HasValue)) + { + episode.ParentIndexNumber = 1; } return episode; diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs index 4e15acd18..f5ac3c665 100644 --- a/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs @@ -5,12 +5,9 @@ using System; using System.Collections.Generic; using System.IO; -using System.Linq; using Emby.Naming.Common; using Emby.Naming.TV; using Emby.Naming.Video; -using MediaBrowser.Common.Configuration; -using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Resolvers; @@ -185,13 +182,42 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV /// <param name="path">The path.</param> private static void SetProviderIdFromPath(Series item, string path) { - var justName = Path.GetFileName(path); + var justName = Path.GetFileName(path.AsSpan()); - var id = justName.GetAttributeValue("tvdbid"); - - if (!string.IsNullOrEmpty(id)) + var tvdbId = justName.GetAttributeValue("tvdbid"); + if (!string.IsNullOrEmpty(tvdbId)) { - item.SetProviderId(MetadataProvider.Tvdb, id); + item.SetProviderId(MetadataProvider.Tvdb, tvdbId); + } + + var tvmazeId = justName.GetAttributeValue("tvmazeid"); + if (!string.IsNullOrEmpty(tvmazeId)) + { + item.SetProviderId(MetadataProvider.TvMaze, tvmazeId); + } + + var tmdbId = justName.GetAttributeValue("tmdbid"); + if (!string.IsNullOrEmpty(tmdbId)) + { + item.SetProviderId(MetadataProvider.Tmdb, tmdbId); + } + + var anidbId = justName.GetAttributeValue("anidbid"); + if (!string.IsNullOrEmpty(anidbId)) + { + item.SetProviderId("AniDB", anidbId); + } + + var aniListId = justName.GetAttributeValue("anilistid"); + if (!string.IsNullOrEmpty(aniListId)) + { + item.SetProviderId("AniList", aniListId); + } + + var aniSearchId = justName.GetAttributeValue("anisearchid"); + if (!string.IsNullOrEmpty(aniSearchId)) + { + item.SetProviderId("AniSearch", aniSearchId); } } } diff --git a/Emby.Server.Implementations/Library/SearchEngine.cs b/Emby.Server.Implementations/Library/SearchEngine.cs index bebc25b77..4aacf7774 100644 --- a/Emby.Server.Implementations/Library/SearchEngine.cs +++ b/Emby.Server.Implementations/Library/SearchEngine.cs @@ -10,12 +10,9 @@ using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Library; using MediaBrowser.Model.Querying; using MediaBrowser.Model.Search; -using Genre = MediaBrowser.Controller.Entities.Genre; -using Person = MediaBrowser.Controller.Entities.Person; namespace Emby.Server.Implementations.Library { @@ -59,9 +56,9 @@ namespace Emby.Server.Implementations.Library }; } - private static void AddIfMissing(List<string> list, string value) + private static void AddIfMissing(List<BaseItemKind> list, BaseItemKind value) { - if (!list.Contains(value, StringComparer.OrdinalIgnoreCase)) + if (!list.Contains(value)) { list.Add(value); } @@ -86,63 +83,63 @@ namespace Emby.Server.Implementations.Library searchTerm = searchTerm.Trim().RemoveDiacritics(); var excludeItemTypes = query.ExcludeItemTypes.ToList(); - var includeItemTypes = (query.IncludeItemTypes ?? Array.Empty<string>()).ToList(); + var includeItemTypes = (query.IncludeItemTypes ?? Array.Empty<BaseItemKind>()).ToList(); - excludeItemTypes.Add(nameof(Year)); - excludeItemTypes.Add(nameof(Folder)); + excludeItemTypes.Add(BaseItemKind.Year); + excludeItemTypes.Add(BaseItemKind.Folder); - if (query.IncludeGenres && (includeItemTypes.Count == 0 || includeItemTypes.Contains("Genre", StringComparer.OrdinalIgnoreCase))) + if (query.IncludeGenres && (includeItemTypes.Count == 0 || includeItemTypes.Contains(BaseItemKind.Genre))) { if (!query.IncludeMedia) { - AddIfMissing(includeItemTypes, nameof(Genre)); - AddIfMissing(includeItemTypes, nameof(MusicGenre)); + AddIfMissing(includeItemTypes, BaseItemKind.Genre); + AddIfMissing(includeItemTypes, BaseItemKind.MusicGenre); } } else { - AddIfMissing(excludeItemTypes, nameof(Genre)); - AddIfMissing(excludeItemTypes, nameof(MusicGenre)); + AddIfMissing(excludeItemTypes, BaseItemKind.Genre); + AddIfMissing(excludeItemTypes, BaseItemKind.MusicGenre); } - if (query.IncludePeople && (includeItemTypes.Count == 0 || includeItemTypes.Contains("People", StringComparer.OrdinalIgnoreCase) || includeItemTypes.Contains("Person", StringComparer.OrdinalIgnoreCase))) + if (query.IncludePeople && (includeItemTypes.Count == 0 || includeItemTypes.Contains(BaseItemKind.Person))) { if (!query.IncludeMedia) { - AddIfMissing(includeItemTypes, nameof(Person)); + AddIfMissing(includeItemTypes, BaseItemKind.Person); } } else { - AddIfMissing(excludeItemTypes, nameof(Person)); + AddIfMissing(excludeItemTypes, BaseItemKind.Person); } - if (query.IncludeStudios && (includeItemTypes.Count == 0 || includeItemTypes.Contains("Studio", StringComparer.OrdinalIgnoreCase))) + if (query.IncludeStudios && (includeItemTypes.Count == 0 || includeItemTypes.Contains(BaseItemKind.Studio))) { if (!query.IncludeMedia) { - AddIfMissing(includeItemTypes, nameof(Studio)); + AddIfMissing(includeItemTypes, BaseItemKind.Studio); } } else { - AddIfMissing(excludeItemTypes, nameof(Studio)); + AddIfMissing(excludeItemTypes, BaseItemKind.Studio); } - if (query.IncludeArtists && (includeItemTypes.Count == 0 || includeItemTypes.Contains("MusicArtist", StringComparer.OrdinalIgnoreCase))) + if (query.IncludeArtists && (includeItemTypes.Count == 0 || includeItemTypes.Contains(BaseItemKind.MusicArtist))) { if (!query.IncludeMedia) { - AddIfMissing(includeItemTypes, nameof(MusicArtist)); + AddIfMissing(includeItemTypes, BaseItemKind.MusicArtist); } } else { - AddIfMissing(excludeItemTypes, nameof(MusicArtist)); + AddIfMissing(excludeItemTypes, BaseItemKind.MusicArtist); } - AddIfMissing(excludeItemTypes, nameof(CollectionFolder)); - AddIfMissing(excludeItemTypes, nameof(Folder)); + AddIfMissing(excludeItemTypes, BaseItemKind.CollectionFolder); + AddIfMissing(excludeItemTypes, BaseItemKind.Folder); var mediaTypes = query.MediaTypes.ToList(); if (includeItemTypes.Count > 0) @@ -183,7 +180,7 @@ namespace Emby.Server.Implementations.Library List<BaseItem> mediaItems; - if (searchQuery.IncludeItemTypes.Length == 1 && string.Equals(searchQuery.IncludeItemTypes[0], "MusicArtist", StringComparison.OrdinalIgnoreCase)) + if (searchQuery.IncludeItemTypes.Length == 1 && searchQuery.IncludeItemTypes[0] == BaseItemKind.MusicArtist) { if (!searchQuery.ParentId.Equals(Guid.Empty)) { @@ -192,7 +189,7 @@ namespace Emby.Server.Implementations.Library searchQuery.ParentId = Guid.Empty; searchQuery.IncludeItemsByName = true; - searchQuery.IncludeItemTypes = Array.Empty<string>(); + searchQuery.IncludeItemTypes = Array.Empty<BaseItemKind>(); mediaItems = _libraryManager.GetAllArtists(searchQuery).Items.Select(i => i.Item1).ToList(); } else diff --git a/Emby.Server.Implementations/Library/UserViewManager.cs b/Emby.Server.Implementations/Library/UserViewManager.cs index 711647e7a..ab8bc6328 100644 --- a/Emby.Server.Implementations/Library/UserViewManager.cs +++ b/Emby.Server.Implementations/Library/UserViewManager.cs @@ -8,11 +8,11 @@ using System.Linq; using System.Threading; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Extensions; using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.LiveTv; using MediaBrowser.Model.Channels; @@ -20,8 +20,6 @@ using MediaBrowser.Model.Entities; using MediaBrowser.Model.Globalization; using MediaBrowser.Model.Library; using MediaBrowser.Model.Querying; -using Genre = MediaBrowser.Controller.Entities.Genre; -using Person = MediaBrowser.Controller.Entities.Person; namespace Emby.Server.Implementations.Library { @@ -80,7 +78,7 @@ namespace Emby.Server.Implementations.Library continue; } - if (query.PresetViews.Contains(folderViewType ?? string.Empty, StringComparer.OrdinalIgnoreCase)) + if (query.PresetViews.Contains(folderViewType ?? string.Empty, StringComparison.OrdinalIgnoreCase)) { list.Add(GetUserView(folder, folderViewType, string.Empty)); } @@ -180,7 +178,7 @@ namespace Emby.Server.Implementations.Library { if (parents.Count == 1 && parents.All(i => string.Equals(i.CollectionType, viewType, StringComparison.OrdinalIgnoreCase))) { - if (!presetViews.Contains(viewType, StringComparer.OrdinalIgnoreCase)) + if (!presetViews.Contains(viewType, StringComparison.OrdinalIgnoreCase)) { return (Folder)parents[0]; } @@ -300,11 +298,11 @@ namespace Emby.Server.Implementations.Library { if (hasCollectionType.All(i => string.Equals(i.CollectionType, CollectionType.Movies, StringComparison.OrdinalIgnoreCase))) { - includeItemTypes = new string[] { "Movie" }; + includeItemTypes = new[] { BaseItemKind.Movie }; } else if (hasCollectionType.All(i => string.Equals(i.CollectionType, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase))) { - includeItemTypes = new string[] { "Episode" }; + includeItemTypes = new[] { BaseItemKind.Episode }; } } } @@ -344,13 +342,13 @@ namespace Emby.Server.Implementations.Library var excludeItemTypes = includeItemTypes.Length == 0 && mediaTypes.Count == 0 ? new[] { - nameof(Person), - nameof(Studio), - nameof(Year), - nameof(MusicGenre), - nameof(Genre) + BaseItemKind.Person, + BaseItemKind.Studio, + BaseItemKind.Year, + BaseItemKind.MusicGenre, + BaseItemKind.Genre } - : Array.Empty<string>(); + : Array.Empty<BaseItemKind>(); var query = new InternalItemsQuery(user) { diff --git a/Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs b/Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs index 40436d9c5..7591e8391 100644 --- a/Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs +++ b/Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs @@ -3,6 +3,7 @@ using System.Globalization; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Library; @@ -81,7 +82,7 @@ namespace Emby.Server.Implementations.Library.Validators var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery { - IncludeItemTypes = new[] { nameof(MusicArtist) }, + IncludeItemTypes = new[] { BaseItemKind.MusicArtist }, IsDeadArtist = true, IsLocked = false }).Cast<MusicArtist>().ToList(); diff --git a/Emby.Server.Implementations/Library/Validators/CollectionPostScanTask.cs b/Emby.Server.Implementations/Library/Validators/CollectionPostScanTask.cs index 945b559ad..73e58d16c 100644 --- a/Emby.Server.Implementations/Library/Validators/CollectionPostScanTask.cs +++ b/Emby.Server.Implementations/Library/Validators/CollectionPostScanTask.cs @@ -64,7 +64,7 @@ namespace Emby.Server.Implementations.Library.Validators var movies = _libraryManager.GetItemList(new InternalItemsQuery { MediaTypes = new string[] { MediaType.Video }, - IncludeItemTypes = new[] { nameof(Movie) }, + IncludeItemTypes = new[] { BaseItemKind.Movie }, IsVirtualItem = false, OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) }, Parent = library, @@ -108,7 +108,7 @@ namespace Emby.Server.Implementations.Library.Validators var boxSets = _libraryManager.GetItemList(new InternalItemsQuery { - IncludeItemTypes = new[] { nameof(BoxSet) }, + IncludeItemTypes = new[] { BaseItemKind.BoxSet }, CollapseBoxSetItems = false, Recursive = true }); diff --git a/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs b/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs index 8a9a4b865..601aab5b9 100644 --- a/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs +++ b/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs @@ -2,6 +2,7 @@ using System; using System.Globalization; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; @@ -91,7 +92,7 @@ namespace Emby.Server.Implementations.Library.Validators var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery { - IncludeItemTypes = new[] { nameof(Person) }, + IncludeItemTypes = new[] { BaseItemKind.Person }, IsDeadPerson = true, IsLocked = false }); diff --git a/Emby.Server.Implementations/Library/Validators/StudiosValidator.cs b/Emby.Server.Implementations/Library/Validators/StudiosValidator.cs index 8577d722e..26bc49c1f 100644 --- a/Emby.Server.Implementations/Library/Validators/StudiosValidator.cs +++ b/Emby.Server.Implementations/Library/Validators/StudiosValidator.cs @@ -2,6 +2,7 @@ using System; using System.Globalization; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Persistence; @@ -80,7 +81,7 @@ namespace Emby.Server.Implementations.Library.Validators var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery { - IncludeItemTypes = new[] { nameof(Studio) }, + IncludeItemTypes = new[] { BaseItemKind.Studio }, IsDeadStudio = true, IsLocked = false }); diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs index 644f9050d..7ef93d166 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs @@ -17,6 +17,7 @@ using System.Xml; using Emby.Server.Implementations.Library; using Jellyfin.Data.Enums; using Jellyfin.Data.Events; +using Jellyfin.Extensions; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Progress; @@ -227,7 +228,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV foreach (var virtualFolder in virtualFolders) { - if (!virtualFolder.Locations.Contains(path, StringComparer.OrdinalIgnoreCase)) + if (!virtualFolder.Locations.Contains(path, StringComparison.OrdinalIgnoreCase)) { continue; } @@ -891,7 +892,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV throw new ArgumentNullException(nameof(tunerHostId)); } - return info.EnabledTuners.Contains(tunerHostId, StringComparer.OrdinalIgnoreCase); + return info.EnabledTuners.Contains(tunerHostId, StringComparison.OrdinalIgnoreCase); } public async Task<IEnumerable<ProgramInfo>> GetProgramsAsync(string channelId, DateTime startDateUtc, DateTime endDateUtc, CancellationToken cancellationToken) @@ -1778,7 +1779,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV { var program = string.IsNullOrWhiteSpace(timer.ProgramId) ? null : _libraryManager.GetItemList(new InternalItemsQuery { - IncludeItemTypes = new[] { nameof(LiveTvProgram) }, + IncludeItemTypes = new[] { BaseItemKind.LiveTvProgram }, Limit = 1, ExternalId = timer.ProgramId, DtoOptions = new DtoOptions(true) @@ -2137,7 +2138,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV { var query = new InternalItemsQuery { - IncludeItemTypes = new string[] { nameof(LiveTvProgram) }, + IncludeItemTypes = new[] { BaseItemKind.LiveTvProgram }, Limit = 1, DtoOptions = new DtoOptions(true) { @@ -2332,7 +2333,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV var deletes = _timerProvider.GetAll() .Where(i => string.Equals(i.SeriesTimerId, seriesTimer.Id, StringComparison.OrdinalIgnoreCase)) - .Where(i => !allTimerIds.Contains(i.Id, StringComparer.OrdinalIgnoreCase) && i.StartDate > DateTime.UtcNow) + .Where(i => !allTimerIds.Contains(i.Id, StringComparison.OrdinalIgnoreCase) && i.StartDate > DateTime.UtcNow) .Where(i => deleteStatuses.Contains(i.Status)) .ToList(); @@ -2352,7 +2353,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV var query = new InternalItemsQuery { - IncludeItemTypes = new string[] { nameof(LiveTvProgram) }, + IncludeItemTypes = new[] { BaseItemKind.LiveTvProgram }, ExternalSeriesId = seriesTimer.SeriesId, DtoOptions = new DtoOptions(true) { @@ -2387,7 +2388,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV channel = _libraryManager.GetItemList( new InternalItemsQuery { - IncludeItemTypes = new string[] { nameof(LiveTvChannel) }, + IncludeItemTypes = new[] { BaseItemKind.LiveTvChannel }, ItemIds = new[] { parent.ChannelId }, DtoOptions = new DtoOptions() }).FirstOrDefault() as LiveTvChannel; @@ -2446,7 +2447,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV channel = _libraryManager.GetItemList( new InternalItemsQuery { - IncludeItemTypes = new string[] { nameof(LiveTvChannel) }, + IncludeItemTypes = new[] { BaseItemKind.LiveTvChannel }, ItemIds = new[] { programInfo.ChannelId }, DtoOptions = new DtoOptions() }).FirstOrDefault() as LiveTvChannel; @@ -2511,7 +2512,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV var seriesIds = _libraryManager.GetItemIds( new InternalItemsQuery { - IncludeItemTypes = new[] { nameof(Series) }, + IncludeItemTypes = new[] { BaseItemKind.Series }, Name = program.Name }).ToArray(); @@ -2524,7 +2525,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV { var result = _libraryManager.GetItemIds(new InternalItemsQuery { - IncludeItemTypes = new[] { nameof(Episode) }, + IncludeItemTypes = new[] { BaseItemKind.Episode }, ParentIndexNumber = program.SeasonNumber.Value, IndexNumber = program.EpisodeNumber.Value, AncestorIds = seriesIds, @@ -2621,7 +2622,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV if (newDevicesOnly) { - discoveredDevices = discoveredDevices.Where(d => !configuredDeviceIds.Contains(d.DeviceId, StringComparer.OrdinalIgnoreCase)) + discoveredDevices = discoveredDevices.Where(d => !configuredDeviceIds.Contains(d.DeviceId, StringComparison.OrdinalIgnoreCase)) .ToList(); } diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs index 08aa0cfd7..dd0cb6c5d 100644 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs +++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs @@ -9,7 +9,7 @@ using System.Globalization; using System.Linq; using System.Net; using System.Net.Http; -using System.Net.Http.Headers; +using System.Net.Http.Json; using System.Net.Mime; using System.Security.Cryptography; using System.Text; @@ -101,11 +101,10 @@ namespace Emby.Server.Implementations.LiveTv.Listings } }; - var requestString = JsonSerializer.Serialize(requestList, _jsonOptions); - _logger.LogDebug("Request string for schedules is: {RequestString}", requestString); + _logger.LogDebug("Request string for schedules is: {@RequestString}", requestList); using var options = new HttpRequestMessage(HttpMethod.Post, ApiUrl + "/schedules"); - options.Content = new StringContent(requestString, Encoding.UTF8, MediaTypeNames.Application.Json); + options.Content = JsonContent.Create(requestList, options: _jsonOptions); options.Headers.TryAddWithoutValidation("token", token); using var response = await Send(options, true, info, cancellationToken).ConfigureAwait(false); await using var responseStream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); @@ -121,8 +120,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings programRequestOptions.Headers.TryAddWithoutValidation("token", token); var programIds = dailySchedules.SelectMany(d => d.Programs.Select(s => s.ProgramId)).Distinct(); - programRequestOptions.Content = new ByteArrayContent(JsonSerializer.SerializeToUtf8Bytes(programIds, _jsonOptions)); - programRequestOptions.Content.Headers.ContentType = MediaTypeHeaderValue.Parse(MediaTypeNames.Application.Json); + programRequestOptions.Content = JsonContent.Create(programIds, options: _jsonOptions); using var innerResponse = await Send(programRequestOptions, true, info, cancellationToken).ConfigureAwait(false); await using var innerResponseStream = await innerResponse.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); @@ -243,19 +241,19 @@ namespace Emby.Server.Implementations.LiveTv.Listings if (programInfo.AudioProperties.Count != 0) { - if (programInfo.AudioProperties.Contains("atmos", StringComparer.OrdinalIgnoreCase)) + if (programInfo.AudioProperties.Contains("atmos", StringComparison.OrdinalIgnoreCase)) { audioType = ProgramAudio.Atmos; } - else if (programInfo.AudioProperties.Contains("dd 5.1", StringComparer.OrdinalIgnoreCase)) + else if (programInfo.AudioProperties.Contains("dd 5.1", StringComparison.OrdinalIgnoreCase)) { audioType = ProgramAudio.DolbyDigital; } - else if (programInfo.AudioProperties.Contains("dd", StringComparer.OrdinalIgnoreCase)) + else if (programInfo.AudioProperties.Contains("dd", StringComparison.OrdinalIgnoreCase)) { audioType = ProgramAudio.DolbyDigital; } - else if (programInfo.AudioProperties.Contains("stereo", StringComparer.OrdinalIgnoreCase)) + else if (programInfo.AudioProperties.Contains("stereo", StringComparison.OrdinalIgnoreCase)) { audioType = ProgramAudio.Stereo; } @@ -317,8 +315,8 @@ namespace Emby.Server.Implementations.LiveTv.Listings if (programInfo.VideoProperties != null) { - info.IsHD = programInfo.VideoProperties.Contains("hdtv", StringComparer.OrdinalIgnoreCase); - info.Is3D = programInfo.VideoProperties.Contains("3d", StringComparer.OrdinalIgnoreCase); + info.IsHD = programInfo.VideoProperties.Contains("hdtv", StringComparison.OrdinalIgnoreCase); + info.Is3D = programInfo.VideoProperties.Contains("3d", StringComparison.OrdinalIgnoreCase); } if (details.ContentRating != null && details.ContentRating.Count > 0) @@ -327,7 +325,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings .Replace("--", "-", StringComparison.Ordinal); var invalid = new[] { "N/A", "Approved", "Not Rated", "Passed" }; - if (invalid.Contains(info.OfficialRating, StringComparer.OrdinalIgnoreCase)) + if (invalid.Contains(info.OfficialRating, StringComparison.OrdinalIgnoreCase)) { info.OfficialRating = null; } @@ -389,9 +387,9 @@ namespace Emby.Server.Implementations.LiveTv.Listings if (details.Genres != null) { info.Genres = details.Genres.Where(g => !string.IsNullOrWhiteSpace(g)).ToList(); - info.IsNews = details.Genres.Contains("news", StringComparer.OrdinalIgnoreCase); + info.IsNews = details.Genres.Contains("news", StringComparison.OrdinalIgnoreCase); - if (info.Genres.Contains("children", StringComparer.OrdinalIgnoreCase)) + if (info.Genres.Contains("children", StringComparison.OrdinalIgnoreCase)) { info.IsKids = true; } diff --git a/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs b/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs index 0c0ec48d9..3da9d02b8 100644 --- a/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs +++ b/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs @@ -190,10 +190,10 @@ namespace Emby.Server.Implementations.LiveTv.Listings IsSeries = program.Episode != null, IsRepeat = program.IsPreviouslyShown && !program.IsNew, IsPremiere = program.Premiere != null, - IsKids = program.Categories.Any(c => info.KidsCategories.Contains(c, StringComparer.OrdinalIgnoreCase)), - IsMovie = program.Categories.Any(c => info.MovieCategories.Contains(c, StringComparer.OrdinalIgnoreCase)), - IsNews = program.Categories.Any(c => info.NewsCategories.Contains(c, StringComparer.OrdinalIgnoreCase)), - IsSports = program.Categories.Any(c => info.SportsCategories.Contains(c, StringComparer.OrdinalIgnoreCase)), + IsKids = program.Categories.Any(c => info.KidsCategories.Contains(c, StringComparison.OrdinalIgnoreCase)), + IsMovie = program.Categories.Any(c => info.MovieCategories.Contains(c, StringComparison.OrdinalIgnoreCase)), + IsNews = program.Categories.Any(c => info.NewsCategories.Contains(c, StringComparison.OrdinalIgnoreCase)), + IsSports = program.Categories.Any(c => info.SportsCategories.Contains(c, StringComparison.OrdinalIgnoreCase)), ImageUrl = program.Icon != null && !string.IsNullOrEmpty(program.Icon.Source) ? program.Icon.Source : null, HasImage = program.Icon != null && !string.IsNullOrEmpty(program.Icon.Source), OfficialRating = program.Rating != null && !string.IsNullOrEmpty(program.Rating.Value) ? program.Rating.Value : null, diff --git a/Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs b/Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs index 598e3f88a..323b96021 100644 --- a/Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs +++ b/Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs @@ -7,12 +7,12 @@ using System.Globalization; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Data.Enums; using MediaBrowser.Common; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.LiveTv; using MediaBrowser.Model.Dto; @@ -161,7 +161,7 @@ namespace Emby.Server.Implementations.LiveTv { var librarySeries = _libraryManager.GetItemList(new InternalItemsQuery { - IncludeItemTypes = new string[] { nameof(Series) }, + IncludeItemTypes = new[] { BaseItemKind.Series }, Name = seriesName, Limit = 1, ImageTypes = new ImageType[] { ImageType.Thumb }, @@ -204,7 +204,7 @@ namespace Emby.Server.Implementations.LiveTv var program = _libraryManager.GetItemList(new InternalItemsQuery { - IncludeItemTypes = new string[] { nameof(LiveTvProgram) }, + IncludeItemTypes = new[] { BaseItemKind.LiveTvProgram }, ExternalSeriesId = programSeriesId, Limit = 1, ImageTypes = new ImageType[] { ImageType.Primary }, @@ -255,7 +255,7 @@ namespace Emby.Server.Implementations.LiveTv { var librarySeries = _libraryManager.GetItemList(new InternalItemsQuery { - IncludeItemTypes = new string[] { nameof(Series) }, + IncludeItemTypes = new[] { BaseItemKind.Series }, Name = seriesName, Limit = 1, ImageTypes = new ImageType[] { ImageType.Thumb }, @@ -298,7 +298,7 @@ namespace Emby.Server.Implementations.LiveTv var program = _libraryManager.GetItemList(new InternalItemsQuery { - IncludeItemTypes = new string[] { nameof(Series) }, + IncludeItemTypes = new[] { BaseItemKind.Series }, Name = seriesName, Limit = 1, ImageTypes = new ImageType[] { ImageType.Primary }, @@ -309,7 +309,7 @@ namespace Emby.Server.Implementations.LiveTv { program = _libraryManager.GetItemList(new InternalItemsQuery { - IncludeItemTypes = new string[] { nameof(LiveTvProgram) }, + IncludeItemTypes = new[] { BaseItemKind.LiveTvProgram }, ExternalSeriesId = programSeriesId, Limit = 1, ImageTypes = new ImageType[] { ImageType.Primary }, diff --git a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs index a41b63f28..047d8e98c 100644 --- a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs +++ b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs @@ -33,8 +33,6 @@ using MediaBrowser.Model.LiveTv; using MediaBrowser.Model.Querying; using MediaBrowser.Model.Tasks; using Microsoft.Extensions.Logging; -using Episode = MediaBrowser.Controller.Entities.TV.Episode; -using Movie = MediaBrowser.Controller.Entities.Movies.Movie; namespace Emby.Server.Implementations.LiveTv { @@ -191,7 +189,7 @@ namespace Emby.Server.Implementations.LiveTv IsKids = query.IsKids, IsSports = query.IsSports, IsSeries = query.IsSeries, - IncludeItemTypes = new[] { nameof(LiveTvChannel) }, + IncludeItemTypes = new[] { BaseItemKind.LiveTvChannel }, TopParentIds = new[] { topFolder.Id }, IsFavorite = query.IsFavorite, IsLiked = query.IsLiked, @@ -810,7 +808,7 @@ namespace Emby.Server.Implementations.LiveTv var internalQuery = new InternalItemsQuery(user) { - IncludeItemTypes = new[] { nameof(LiveTvProgram) }, + IncludeItemTypes = new[] { BaseItemKind.LiveTvProgram }, MinEndDate = query.MinEndDate, MinStartDate = query.MinStartDate, MaxEndDate = query.MaxEndDate, @@ -874,7 +872,7 @@ namespace Emby.Server.Implementations.LiveTv var internalQuery = new InternalItemsQuery(user) { - IncludeItemTypes = new[] { nameof(LiveTvProgram) }, + IncludeItemTypes = new[] { BaseItemKind.LiveTvProgram }, IsAiring = query.IsAiring, HasAired = query.HasAired, IsNews = query.IsNews, @@ -1085,8 +1083,8 @@ namespace Emby.Server.Implementations.LiveTv if (cleanDatabase) { - CleanDatabaseInternal(newChannelIdList.ToArray(), new[] { nameof(LiveTvChannel) }, progress, cancellationToken); - CleanDatabaseInternal(newProgramIdList.ToArray(), new[] { nameof(LiveTvProgram) }, progress, cancellationToken); + CleanDatabaseInternal(newChannelIdList.ToArray(), new[] { BaseItemKind.LiveTvChannel }, progress, cancellationToken); + CleanDatabaseInternal(newProgramIdList.ToArray(), new[] { BaseItemKind.LiveTvProgram }, progress, cancellationToken); } var coreService = _services.OfType<EmbyTV.EmbyTV>().FirstOrDefault(); @@ -1177,7 +1175,7 @@ namespace Emby.Server.Implementations.LiveTv var existingPrograms = _libraryManager.GetItemList(new InternalItemsQuery { - IncludeItemTypes = new string[] { nameof(LiveTvProgram) }, + IncludeItemTypes = new[] { BaseItemKind.LiveTvProgram }, ChannelIds = new Guid[] { currentChannel.Id }, DtoOptions = new DtoOptions(true) }).Cast<LiveTvProgram>().ToDictionary(i => i.Id); @@ -1261,7 +1259,7 @@ namespace Emby.Server.Implementations.LiveTv return new Tuple<List<Guid>, List<Guid>>(channels, programs); } - private void CleanDatabaseInternal(Guid[] currentIdList, string[] validTypes, IProgress<double> progress, CancellationToken cancellationToken) + private void CleanDatabaseInternal(Guid[] currentIdList, BaseItemKind[] validTypes, IProgress<double> progress, CancellationToken cancellationToken) { var list = _itemRepo.GetItemIdsList(new InternalItemsQuery { @@ -1328,25 +1326,25 @@ namespace Emby.Server.Implementations.LiveTv .Select(i => i.Id) .ToList(); - var excludeItemTypes = new List<string>(); + var excludeItemTypes = new List<BaseItemKind>(); if (folderIds.Count == 0) { return new QueryResult<BaseItem>(); } - var includeItemTypes = new List<string>(); + var includeItemTypes = new List<BaseItemKind>(); var genres = new List<string>(); if (query.IsMovie.HasValue) { if (query.IsMovie.Value) { - includeItemTypes.Add(nameof(Movie)); + includeItemTypes.Add(BaseItemKind.Movie); } else { - excludeItemTypes.Add(nameof(Movie)); + excludeItemTypes.Add(BaseItemKind.Movie); } } @@ -1354,11 +1352,11 @@ namespace Emby.Server.Implementations.LiveTv { if (query.IsSeries.Value) { - includeItemTypes.Add(nameof(Episode)); + includeItemTypes.Add(BaseItemKind.Episode); } else { - excludeItemTypes.Add(nameof(Episode)); + excludeItemTypes.Add(BaseItemKind.Episode); } } @@ -1878,7 +1876,7 @@ namespace Emby.Server.Implementations.LiveTv var programs = options.AddCurrentProgram ? _libraryManager.GetItemList(new InternalItemsQuery(user) { - IncludeItemTypes = new[] { nameof(LiveTvProgram) }, + IncludeItemTypes = new[] { BaseItemKind.LiveTvProgram }, ChannelIds = channelIds, MaxStartDate = now, MinEndDate = now, diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs index 9fba17ca3..d2f033439 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs @@ -3,7 +3,6 @@ #pragma warning disable CS1591 using System; -using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; @@ -85,7 +84,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun Logger.LogInformation("Opening HDHR UDP Live stream from {Host}", uri.Host); var remoteAddress = IPAddress.Parse(uri.Host); - IPAddress localAddress = null; + IPAddress localAddress; using (var tcpClient = new TcpClient()) { try diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs index 08b9260b9..99486f25c 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs @@ -10,6 +10,7 @@ using System.Linq; using System.Net.Http; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Extensions; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller; @@ -119,7 +120,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts { var extension = Path.GetExtension(mediaSource.Path) ?? string.Empty; - if (!_disallowedSharedStreamExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase)) + if (!_disallowedSharedStreamExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase)) { return new SharedHttpStream(mediaSource, tunerHost, streamId, FileSystem, _httpClientFactory, Logger, Config, _appHost, _streamHelper); } diff --git a/Emby.Server.Implementations/Localization/Core/ar.json b/Emby.Server.Implementations/Localization/Core/ar.json index a83a453b4..9d4d40e51 100644 --- a/Emby.Server.Implementations/Localization/Core/ar.json +++ b/Emby.Server.Implementations/Localization/Core/ar.json @@ -11,11 +11,11 @@ "Collections": "التجميعات", "DeviceOfflineWithName": "قُطِع الاتصال ب{0}", "DeviceOnlineWithName": "{0} متصل", - "FailedLoginAttemptWithUserName": "عملية تسجيل الدخول فشلت من {0}", + "FailedLoginAttemptWithUserName": "محاولة تسجيل الدخول فشلت من {0}", "Favorites": "مفضلات", "Folders": "المجلدات", "Genres": "التضنيفات", - "HeaderAlbumArtists": "ألبوم الفنان", + "HeaderAlbumArtists": "فناني الألبوم", "HeaderContinueWatching": "استمر بالمشاهدة", "HeaderFavoriteAlbums": "الألبومات المفضلة", "HeaderFavoriteArtists": "الفنانون المفضلون", diff --git a/Emby.Server.Implementations/Localization/Core/cy.json b/Emby.Server.Implementations/Localization/Core/cy.json index 0fa72dea4..1192f5c88 100644 --- a/Emby.Server.Implementations/Localization/Core/cy.json +++ b/Emby.Server.Implementations/Localization/Core/cy.json @@ -10,5 +10,49 @@ "AuthenticationSucceededWithUserName": "{0} wedi’i ddilysu’n llwyddiannus", "Artists": "Artistiaid", "AppDeviceValues": "Ap: {0}, Dyfais: {1}", - "Albums": "Albwmau" + "Albums": "Albwmau", + "Genres": "Genres", + "Folders": "Ffolderi", + "Favorites": "Ffefrynnau", + "LabelRunningTimeValue": "Amser rhedeg: {0}", + "TaskOptimizeDatabase": "Cronfa ddata Optimeiddio", + "TaskRefreshChannels": "Adnewyddu Sianeli", + "TaskRefreshPeople": "Adnewyddu Pobl", + "TasksChannelsCategory": "Sianeli Internet", + "VersionNumber": "Fersiwn {0}", + "ScheduledTaskStartedWithName": "{0} wedi dechrau", + "ScheduledTaskFailedWithName": "{0} wedi methu", + "ProviderValue": "Darparwr: {0}", + "NotificationOptionInstallationFailed": "Fethu Gosod", + "NameSeasonUnknown": "Tymor Anhysbys", + "NameSeasonNumber": "Tymor {0}", + "MusicVideos": "Fideos Cerddoriaeth", + "MixedContent": "Cynnwys amrywiol", + "HomeVideos": "Fideos Cartref", + "HeaderNextUp": "Nesaf i Fyny", + "HeaderFavoriteArtists": "Ffefryn Artistiaid", + "HeaderFavoriteAlbums": "Ffefryn Albwmau", + "HeaderContinueWatching": "Parhewch i Weithio", + "TasksApplicationCategory": "Rhaglen", + "TasksLibraryCategory": "Llyfrgell", + "TasksMaintenanceCategory": "Cynnal a Chadw", + "System": "System", + "Plugin": "Ategyn", + "Music": "Cerddoriaeth", + "Latest": "Diweddaraf", + "Inherit": "Etifeddu", + "Forced": "Orfodi", + "Application": "Rhaglen", + "HeaderAlbumArtists": "Artistiaid albwm", + "Sync": "Cysoni", + "Songs": "Caneuon", + "Shows": "Rhaglenni", + "Playlists": "Rhestri Chwarae", + "Photos": "Lluniau", + "ValueSpecialEpisodeName": "Arbennig - {0}", + "Movies": "Ffilmiau", + "Undefined": "Heb ddiffiniad", + "TvShows": "Rhaglenni teledu", + "HeaderLiveTV": "Teledu Byw", + "User": "Defnyddiwr" } diff --git a/Emby.Server.Implementations/Localization/Core/eo.json b/Emby.Server.Implementations/Localization/Core/eo.json index 3cadde0a0..8abf7fa66 100644 --- a/Emby.Server.Implementations/Localization/Core/eo.json +++ b/Emby.Server.Implementations/Localization/Core/eo.json @@ -104,7 +104,7 @@ "TaskRefreshChannelsDescription": "Refreŝigas informon pri interretaj kanaloj.", "TaskDownloadMissingSubtitles": "Elŝuti mankantajn subtekstojn", "TaskCleanTranscode": "Malplenigi Transkodadan Katalogon", - "TaskRefreshChapterImages": "Eltiri Ĉapitraj Bildojn", + "TaskRefreshChapterImages": "Eltiri Ĉapitrajn Bildojn", "TaskCleanCache": "Malplenigi Staplan Katalogon", "TaskCleanActivityLog": "Malplenigi Aktivecan Ĵurnalon", "PluginUpdatedWithName": "{0} estis ĝisdatigita", diff --git a/Emby.Server.Implementations/Localization/Core/es.json b/Emby.Server.Implementations/Localization/Core/es.json index d3d9d2703..f8c69712e 100644 --- a/Emby.Server.Implementations/Localization/Core/es.json +++ b/Emby.Server.Implementations/Localization/Core/es.json @@ -15,8 +15,8 @@ "Favorites": "Favoritos", "Folders": "Carpetas", "Genres": "Géneros", - "HeaderAlbumArtists": "Artista del álbum", - "HeaderContinueWatching": "Continuar viendo", + "HeaderAlbumArtists": "Artistas del álbum", + "HeaderContinueWatching": "Seguir viendo", "HeaderFavoriteAlbums": "Álbumes favoritos", "HeaderFavoriteArtists": "Artistas favoritos", "HeaderFavoriteEpisodes": "Episodios favoritos", diff --git a/Emby.Server.Implementations/Localization/Core/es_419.json b/Emby.Server.Implementations/Localization/Core/es_419.json index 010531c9b..2ca736ad9 100644 --- a/Emby.Server.Implementations/Localization/Core/es_419.json +++ b/Emby.Server.Implementations/Localization/Core/es_419.json @@ -15,7 +15,7 @@ "HeaderFavoriteEpisodes": "Episodios favoritos", "HeaderFavoriteShows": "Programas favoritos", "HeaderContinueWatching": "Continuar viendo", - "HeaderAlbumArtists": "Artistas del álbum", + "HeaderAlbumArtists": "Artistas de álbum", "Genres": "Géneros", "Folders": "Carpetas", "Favorites": "Favoritos", @@ -29,7 +29,7 @@ "TaskRefreshChannelsDescription": "Actualiza la información de canales de Internet.", "TaskRefreshChannels": "Actualizar canales", "TaskCleanTranscodeDescription": "Elimina archivos transcodificados que tengan más de un día.", - "TaskCleanTranscode": "Limpiar directorio de transcodificado", + "TaskCleanTranscode": "Limpiar el directorio de transcodificaciones", "TaskUpdatePluginsDescription": "Descarga e instala actualizaciones para complementos que están configurados para actualizarse automáticamente.", "TaskUpdatePlugins": "Actualizar complementos", "TaskRefreshPeopleDescription": "Actualiza metadatos de actores y directores en tu biblioteca de medios.", @@ -91,7 +91,7 @@ "NameSeasonUnknown": "Temporada desconocida", "NameSeasonNumber": "Temporada {0}", "NameInstallFailed": "Falló la instalación de {0}", - "MusicVideos": "Videos Musicales", + "MusicVideos": "Videos musicales", "Music": "Música", "MixedContent": "Contenido mezclado", "MessageServerConfigurationUpdated": "Se ha actualizado la configuración del servidor", @@ -105,7 +105,7 @@ "Inherit": "Heredar", "HomeVideos": "Videos caseros", "HeaderRecordingGroups": "Grupos de grabación", - "FailedLoginAttemptWithUserName": "Intento fallido de inicio de sesión desde {0}", + "FailedLoginAttemptWithUserName": "Intento de inicio de sesión fallido desde {0}", "DeviceOnlineWithName": "{0} está conectado", "DeviceOfflineWithName": "{0} se ha desconectado", "ChapterNameValue": "Capítulo {0}", @@ -114,10 +114,10 @@ "Application": "Aplicación", "AppDeviceValues": "App: {0}, Dispositivo: {1}", "TaskCleanActivityLogDescription": "Elimina las entradas del registro de actividad anteriores al periodo configurado.", - "TaskCleanActivityLog": "Limpiar Registro de Actividades", + "TaskCleanActivityLog": "Limpiar registro de actividades", "Undefined": "Sin definir", "Forced": "Forzado", "Default": "Por defecto", - "TaskOptimizeDatabaseDescription": "Compacta la base de datos y restaura el espacio libre. Ejecutar esta tarea después de actualizar las librerías o realizar otros cambios que impliquen modificar las bases de datos puede mejorar la performance.", - "TaskOptimizeDatabase": "Optimización de base de datos" + "TaskOptimizeDatabaseDescription": "Compacta la base de datos y libera espacio. Ejecutar esta tarea después de escanear la biblioteca o hacer otros cambios que impliquen modificaciones en la base de datos puede mejorar el rendimiento.", + "TaskOptimizeDatabase": "Optimizar base de datos" } diff --git a/Emby.Server.Implementations/Localization/Core/et.json b/Emby.Server.Implementations/Localization/Core/et.json index 71f4a97e5..8db6a0b38 100644 --- a/Emby.Server.Implementations/Localization/Core/et.json +++ b/Emby.Server.Implementations/Localization/Core/et.json @@ -102,7 +102,7 @@ "Forced": "Sunnitud", "Folders": "Kaustad", "Favorites": "Lemmikud", - "FailedLoginAttemptWithUserName": "Ebaõnnestunud sisselogimiskatse kasutajalt {0}", + "FailedLoginAttemptWithUserName": "{0} - sisselogimine nurjus", "DeviceOnlineWithName": "{0} on ühendatud", "DeviceOfflineWithName": "{0} katkestas ühenduse", "Default": "Vaikimisi", diff --git a/Emby.Server.Implementations/Localization/Core/fa.json b/Emby.Server.Implementations/Localization/Core/fa.json index 8ab657e5b..6960ff007 100644 --- a/Emby.Server.Implementations/Localization/Core/fa.json +++ b/Emby.Server.Implementations/Localization/Core/fa.json @@ -6,7 +6,7 @@ "AuthenticationSucceededWithUserName": "{0} با موفقیت تایید اعتبار شد", "Books": "کتاب‌ها", "CameraImageUploadedFrom": "یک عکس جدید از دوربین ارسال شده است {0}", - "Channels": "کانال‌ها", + "Channels": "کانالها", "ChapterNameValue": "قسمت {0}", "Collections": "مجموعه‌ها", "DeviceOfflineWithName": "ارتباط {0} قطع شد", @@ -37,7 +37,7 @@ "MessageNamedServerConfigurationUpdatedWithValue": "پکربندی بخش {0} سرور بروزرسانی شد", "MessageServerConfigurationUpdated": "پیکربندی سرور بروزرسانی شد", "MixedContent": "محتوای مخلوط", - "Movies": "فیلم‌ها", + "Movies": "فیلم ها", "Music": "موسیقی", "MusicVideos": "موزیک ویدیوها", "NameInstallFailed": "{0} نصب با مشکل مواجه شد", @@ -118,5 +118,6 @@ "Default": "پیشفرض", "TaskCleanActivityLogDescription": "ورودی‌های قدیمی‌تر از سن تنظیم شده در سیاهه فعالیت را حذف می‌کند.", "TaskCleanActivityLog": "پاکسازی سیاهه فعالیت", - "Undefined": "تعریف نشده" + "Undefined": "تعریف نشده", + "TaskOptimizeDatabase": "بهینه سازی پایگاه داده" } diff --git a/Emby.Server.Implementations/Localization/Core/fil.json b/Emby.Server.Implementations/Localization/Core/fil.json index f18a1c030..99839ae6e 100644 --- a/Emby.Server.Implementations/Localization/Core/fil.json +++ b/Emby.Server.Implementations/Localization/Core/fil.json @@ -117,5 +117,7 @@ "TaskCleanActivityLogDescription": "Tanggalin ang mga tala ng aktibidad na mas luma sa nakatakda na edad.", "Default": "Default", "Undefined": "Hindi tiyak", - "Forced": "Sapilitan" + "Forced": "Sapilitan", + "TaskOptimizeDatabaseDescription": "Iko-compact ang database at ita-truncate ang free space. Ang pagpapatakbo ng gawaing ito pagkatapos ng pag-scan sa library o paggawa ng iba pang mga pagbabago na nagpapahiwatig ng mga pagbabago sa database ay maaaring magpa-improve ng performance.", + "TaskOptimizeDatabase": "I-optimize ang database" } diff --git a/Emby.Server.Implementations/Localization/Core/hr.json b/Emby.Server.Implementations/Localization/Core/hr.json index d7cda61da..4df0444e6 100644 --- a/Emby.Server.Implementations/Localization/Core/hr.json +++ b/Emby.Server.Implementations/Localization/Core/hr.json @@ -15,7 +15,7 @@ "Favorites": "Favoriti", "Folders": "Mape", "Genres": "Žanrovi", - "HeaderAlbumArtists": "Album od izvođača", + "HeaderAlbumArtists": "Izvođači albuma", "HeaderContinueWatching": "Nastavi gledati", "HeaderFavoriteAlbums": "Omiljeni albumi", "HeaderFavoriteArtists": "Omiljeni izvođači", diff --git a/Emby.Server.Implementations/Localization/Core/ja.json b/Emby.Server.Implementations/Localization/Core/ja.json index c689bc58a..2588f1e8c 100644 --- a/Emby.Server.Implementations/Localization/Core/ja.json +++ b/Emby.Server.Implementations/Localization/Core/ja.json @@ -11,12 +11,12 @@ "Collections": "コレクション", "DeviceOfflineWithName": "{0} が切断されました", "DeviceOnlineWithName": "{0} が接続されました", - "FailedLoginAttemptWithUserName": "ログインを試行しましたが {0}によって失敗しました", + "FailedLoginAttemptWithUserName": "ログインを試行しましたが {0} によって失敗しました", "Favorites": "お気に入り", "Folders": "フォルダー", "Genres": "ジャンル", - "HeaderAlbumArtists": "アーティストのアルバム", - "HeaderContinueWatching": "視聴を続ける", + "HeaderAlbumArtists": "アルバムアーティスト", + "HeaderContinueWatching": "続きを見る", "HeaderFavoriteAlbums": "お気に入りのアルバム", "HeaderFavoriteArtists": "お気に入りのアーティスト", "HeaderFavoriteEpisodes": "お気に入りのエピソード", diff --git a/Emby.Server.Implementations/Localization/Core/lt-LT.json b/Emby.Server.Implementations/Localization/Core/lt-LT.json index f3a131d40..f0a07f604 100644 --- a/Emby.Server.Implementations/Localization/Core/lt-LT.json +++ b/Emby.Server.Implementations/Localization/Core/lt-LT.json @@ -1,7 +1,7 @@ { "Albums": "Albumai", "AppDeviceValues": "Programa: {0}, Įrenginys: {1}", - "Application": "Programa", + "Application": "Programėlė", "Artists": "Atlikėjai", "AuthenticationSucceededWithUserName": "{0} sėkmingai autentifikuota", "Books": "Knygos", diff --git a/Emby.Server.Implementations/Localization/Core/mk.json b/Emby.Server.Implementations/Localization/Core/mk.json index 6baedcb2d..279734c5e 100644 --- a/Emby.Server.Implementations/Localization/Core/mk.json +++ b/Emby.Server.Implementations/Localization/Core/mk.json @@ -5,7 +5,7 @@ "PluginUninstalledWithName": "{0} беше успешно деинсталирано", "PluginInstalledWithName": "{0} беше успешно инсталирано", "Plugin": "Додатоци", - "Playlists": "Листи", + "Playlists": "Плејлисти", "Photos": "Слики", "NotificationOptionVideoPlaybackStopped": "Видео стопирано", "NotificationOptionVideoPlayback": "Видео пуштено", @@ -97,5 +97,8 @@ "TasksChannelsCategory": "Интернет Канали", "TasksApplicationCategory": "Апликација", "TasksLibraryCategory": "Библиотека", - "TasksMaintenanceCategory": "Одржување" + "TasksMaintenanceCategory": "Одржување", + "Undefined": "Недефинирано", + "Forced": "Принудно", + "Default": "Зададено" } diff --git a/Emby.Server.Implementations/Localization/Core/mn.json b/Emby.Server.Implementations/Localization/Core/mn.json index ab7d423b5..7421d42fb 100644 --- a/Emby.Server.Implementations/Localization/Core/mn.json +++ b/Emby.Server.Implementations/Localization/Core/mn.json @@ -9,5 +9,6 @@ "Genres": "Төрөл зүйл", "Favorites": "Дуртай", "Collections": "Багц", - "Artists": "Жүжигчин" + "Artists": "Зураачуд", + "Albums": "Цомгууд" } diff --git a/Emby.Server.Implementations/Localization/Core/ms.json b/Emby.Server.Implementations/Localization/Core/ms.json index b2dcf270c..deb28970c 100644 --- a/Emby.Server.Implementations/Localization/Core/ms.json +++ b/Emby.Server.Implementations/Localization/Core/ms.json @@ -2,7 +2,7 @@ "Albums": "Album-album", "AppDeviceValues": "Apl: {0}, Peranti: {1}", "Application": "Aplikasi", - "Artists": "Artis", + "Artists": "Artis-artis", "AuthenticationSucceededWithUserName": "{0} berjaya disahkan", "Books": "Buku-buku", "CameraImageUploadedFrom": "Gambar baharu telah dimuat naik melalui {0}", @@ -37,9 +37,9 @@ "MessageNamedServerConfigurationUpdatedWithValue": "Konfigurasi pelayan di bahagian {0} telah dikemas kini", "MessageServerConfigurationUpdated": "Konfigurasi pelayan telah dikemas kini", "MixedContent": "Kandungan campuran", - "Movies": "Filem", + "Movies": "Filem-filem", "Music": "Muzik", - "MusicVideos": "Muzik video", + "MusicVideos": "Video muzik", "NameInstallFailed": "{0} pemasangan gagal", "NameSeasonNumber": "Musim {0}", "NameSeasonUnknown": "Musim Tidak Diketahui", @@ -53,43 +53,43 @@ "NotificationOptionNewLibraryContent": "Kandungan baru telah ditambah", "NotificationOptionPluginError": "Kegagalan plugin", "NotificationOptionPluginInstalled": "Plugin telah dipasang", - "NotificationOptionPluginUninstalled": "Plugin uninstalled", - "NotificationOptionPluginUpdateInstalled": "Plugin update installed", - "NotificationOptionServerRestartRequired": "Server restart required", - "NotificationOptionTaskFailed": "Scheduled task failure", - "NotificationOptionUserLockedOut": "User locked out", - "NotificationOptionVideoPlayback": "Video playback started", + "NotificationOptionPluginUninstalled": "Plugin telah dinyahpasang", + "NotificationOptionPluginUpdateInstalled": "Kemaskini plugin telah dipasang", + "NotificationOptionServerRestartRequired": "", + "NotificationOptionTaskFailed": "Kegagalan tugas berjadual", + "NotificationOptionUserLockedOut": "Pengguna telah dikunci", + "NotificationOptionVideoPlayback": "Ulangmain video bermula", "NotificationOptionVideoPlaybackStopped": "Ulangmain video dihentikan", "Photos": "Gambar-gambar", "Playlists": "Senarai main", "Plugin": "Plugin", - "PluginInstalledWithName": "{0} was installed", - "PluginUninstalledWithName": "{0} was uninstalled", - "PluginUpdatedWithName": "{0} was updated", - "ProviderValue": "Provider: {0}", + "PluginInstalledWithName": "{0} telah dipasang", + "PluginUninstalledWithName": "{0} telah dinyahpasang", + "PluginUpdatedWithName": "{0} telah dikemaskini", + "ProviderValue": "Pembekal: {0}", "ScheduledTaskFailedWithName": "{0} gagal", "ScheduledTaskStartedWithName": "{0} bermula", - "ServerNameNeedsToBeRestarted": "{0} needs to be restarted", - "Shows": "Series", + "ServerNameNeedsToBeRestarted": "{0} perlu di ulangmula", + "Shows": "Tayangan", "Songs": "Lagu-lagu", "StartupEmbyServerIsLoading": "Pelayan Jellyfin sedang dimuatkan. Sila cuba sebentar lagi.", "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}", "SubtitleDownloadFailureFromForItem": "Muat turun sarikata gagal dari {0} untuk {1}", - "Sync": "Sync", + "Sync": "Segerak", "System": "Sistem", - "TvShows": "TV Shows", - "User": "User", - "UserCreatedWithName": "User {0} has been created", - "UserDeletedWithName": "User {0} has been deleted", - "UserDownloadingItemWithValues": "{0} is downloading {1}", + "TvShows": "Tayangan TV", + "User": "Pengguna", + "UserCreatedWithName": "Pengguna {0} telah diwujudkan", + "UserDeletedWithName": "Pengguna {0} telah dipadamkan", + "UserDownloadingItemWithValues": "{0} sedang memuat turun {1}", "UserLockedOutWithName": "Pengguna {0} telah dikunci", "UserOfflineFromDevice": "{0} telah terputus dari {1}", "UserOnlineFromDevice": "{0} berada dalam talian dari {1}", "UserPasswordChangedWithName": "Kata laluan telah ditukar bagi pengguna {0}", "UserPolicyUpdatedWithName": "Dasar pengguna telah dikemas kini untuk {0}", - "UserStartedPlayingItemWithValues": "{0} is playing {1} on {2}", - "UserStoppedPlayingItemWithValues": "{0} has finished playing {1} on {2}", - "ValueHasBeenAddedToLibrary": "{0} has been added to your media library", + "UserStartedPlayingItemWithValues": "{0} sedang dimainkan {1} pada {2}", + "UserStoppedPlayingItemWithValues": "{0} telah tamat dimainkan {1} pada {2}", + "ValueHasBeenAddedToLibrary": "{0} telah ditambah ke media library anda", "ValueSpecialEpisodeName": "Khas - {0}", "VersionNumber": "Versi {0}", "TaskCleanActivityLog": "Log Aktiviti Bersih", diff --git a/Emby.Server.Implementations/Localization/Core/ne.json b/Emby.Server.Implementations/Localization/Core/ne.json index 8e820d40c..8584fc065 100644 --- a/Emby.Server.Implementations/Localization/Core/ne.json +++ b/Emby.Server.Implementations/Localization/Core/ne.json @@ -69,7 +69,7 @@ "UserDeletedWithName": "प्रयोगकर्ता {0} हटाइएको छ", "UserCreatedWithName": "प्रयोगकर्ता {0} सिर्जना गरिएको छ", "User": "प्रयोगकर्ता", - "PluginInstalledWithName": "", + "PluginInstalledWithName": "{0} सभएको थियो", "StartupEmbyServerIsLoading": "Jellyfin सर्भर लोड हुँदैछ। कृपया छिट्टै फेरि प्रयास गर्नुहोस्।", "Songs": "गीतहरू", "Shows": "शोहरू", diff --git a/Emby.Server.Implementations/Localization/Core/nl.json b/Emby.Server.Implementations/Localization/Core/nl.json index 79f921bcb..9d512dea1 100644 --- a/Emby.Server.Implementations/Localization/Core/nl.json +++ b/Emby.Server.Implementations/Localization/Core/nl.json @@ -15,7 +15,7 @@ "Favorites": "Favorieten", "Folders": "Mappen", "Genres": "Genres", - "HeaderAlbumArtists": "Artiests Album", + "HeaderAlbumArtists": "Album Artiesten", "HeaderContinueWatching": "Kijken hervatten", "HeaderFavoriteAlbums": "Favoriete albums", "HeaderFavoriteArtists": "Favoriete artiesten", diff --git a/Emby.Server.Implementations/Localization/Core/pa.json b/Emby.Server.Implementations/Localization/Core/pa.json index d1db09232..4ac57b630 100644 --- a/Emby.Server.Implementations/Localization/Core/pa.json +++ b/Emby.Server.Implementations/Localization/Core/pa.json @@ -24,7 +24,7 @@ "TasksLibraryCategory": "ਲਾਇਬ੍ਰੇਰੀ", "TasksMaintenanceCategory": "ਰੱਖ-ਰਖਾਅ", "VersionNumber": "ਵਰਜਨ {0}", - "ValueSpecialEpisodeName": "ਵਿਸ਼ੇਸ਼ - {0}", + "ValueSpecialEpisodeName": "ਖਾਸ - {0}", "ValueHasBeenAddedToLibrary": "{0} ਤੁਹਾਡੀ ਮੀਡੀਆ ਲਾਇਬ੍ਰੇਰੀ ਵਿੱਚ ਸ਼ਾਮਲ ਕੀਤਾ ਗਿਆ ਹੈ", "UserStoppedPlayingItemWithValues": "{0} ਨੇ {2} 'ਤੇ {1} ਖੇਡਣਾ ਪੂਰਾ ਕਰ ਲਿਆ ਹੈ", "UserStartedPlayingItemWithValues": "{0} {2} 'ਤੇ {1} ਖੇਡ ਰਿਹਾ ਹੈ", @@ -43,8 +43,8 @@ "Sync": "ਸਿੰਕ", "SubtitleDownloadFailureFromForItem": "ਉਪਸਿਰਲੇਖ {1} ਲਈ {0} ਤੋਂ ਡਾ toਨਲੋਡ ਕਰਨ ਵਿੱਚ ਅਸਫਲ ਰਹੇ", "StartupEmbyServerIsLoading": "ਜੈਲੀਫਿਨ ਸਰਵਰ ਲੋਡ ਹੋ ਰਿਹਾ ਹੈ. ਕਿਰਪਾ ਕਰਕੇ ਜਲਦੀ ਹੀ ਦੁਬਾਰਾ ਕੋਸ਼ਿਸ਼ ਕਰੋ.", - "Songs": "ਗਾਣੇ", - "Shows": "ਸ਼ੋਅਜ਼", + "Songs": "ਗਾਣੇਂ", + "Shows": "ਸ਼ੋਅ", "ServerNameNeedsToBeRestarted": "{0} ਮੁੜ ਚਾਲੂ ਕਰਨ ਦੀ ਲੋੜ ਹੈ", "ScheduledTaskStartedWithName": "{0} ਸ਼ੁਰੂ ਹੋਇਆ", "ScheduledTaskFailedWithName": "{0} ਅਸਫਲ", @@ -53,7 +53,7 @@ "PluginUninstalledWithName": "{0} ਅਣਇੰਸਟੌਲ ਕੀਤਾ ਗਿਆ ਸੀ", "PluginInstalledWithName": "{0} ਲਗਾਇਆ ਗਿਆ ਸੀ", "Plugin": "ਪਲੱਗਇਨ", - "Playlists": "ਪਲੇਲਿਸਟਸ", + "Playlists": "ਪਲੇਸੂਚੀਆਂ", "Photos": "ਫੋਟੋਆਂ", "NotificationOptionVideoPlaybackStopped": "ਵੀਡੀਓ ਪਲੇਬੈਕ ਰੋਕਿਆ ਗਿਆ", "NotificationOptionVideoPlayback": "ਵੀਡੀਓ ਪਲੇਬੈਕ ਸ਼ੁਰੂ ਹੋਇਆ", @@ -102,13 +102,13 @@ "HeaderAlbumArtists": "ਐਲਬਮ ਕਲਾਕਾਰ", "Genres": "ਸ਼ੈਲੀਆਂ", "Forced": "ਮਜਬੂਰ", - "Folders": "ਫੋਲਡਰ", + "Folders": "ਫੋਲਡਰਸ", "Favorites": "ਮਨਪਸੰਦ", "FailedLoginAttemptWithUserName": "ਤੋਂ ਲਾਗਇਨ ਕੋਸ਼ਿਸ਼ ਫੇਲ ਹੋਈ {0}", "DeviceOnlineWithName": "{0} ਜੁੜਿਆ ਹੋਇਆ ਹੈ", "DeviceOfflineWithName": "{0} ਡਿਸਕਨੈਕਟ ਹੋ ਗਿਆ ਹੈ", - "Default": "ਮੂਲ", - "Collections": "ਸੰਗ੍ਰਹਿ", + "Default": "ਡਿਫੌਲਟ", + "Collections": "ਸੰਗ੍ਰਹਿਣ", "ChapterNameValue": "ਅਧਿਆਇ {0}", "Channels": "ਚੈਨਲ", "CameraImageUploadedFrom": "ਤੋਂ ਇੱਕ ਨਵਾਂ ਕੈਮਰਾ ਚਿੱਤਰ ਅਪਲੋਡ ਕੀਤਾ ਗਿਆ ਹੈ {0}", diff --git a/Emby.Server.Implementations/Localization/Core/pl.json b/Emby.Server.Implementations/Localization/Core/pl.json index e8a32a13e..4fa8d2bb4 100644 --- a/Emby.Server.Implementations/Localization/Core/pl.json +++ b/Emby.Server.Implementations/Localization/Core/pl.json @@ -15,7 +15,7 @@ "Favorites": "Ulubione", "Folders": "Foldery", "Genres": "Gatunki", - "HeaderAlbumArtists": "Album artysty", + "HeaderAlbumArtists": "Wykonawcy albumów", "HeaderContinueWatching": "Kontynuuj odtwarzanie", "HeaderFavoriteAlbums": "Ulubione albumy", "HeaderFavoriteArtists": "Ulubieni wykonawcy", @@ -47,7 +47,7 @@ "NotificationOptionApplicationUpdateAvailable": "Dostępna aktualizacja aplikacji", "NotificationOptionApplicationUpdateInstalled": "Zaktualizowano aplikację", "NotificationOptionAudioPlayback": "Rozpoczęto odtwarzanie muzyki", - "NotificationOptionAudioPlaybackStopped": "Odtwarzane dźwięku zatrzymane", + "NotificationOptionAudioPlaybackStopped": "Odtwarzanie dźwięku zatrzymane", "NotificationOptionCameraImageUploaded": "Przekazano obraz z urządzenia przenośnego", "NotificationOptionInstallationFailed": "Nieudana instalacja", "NotificationOptionNewLibraryContent": "Dodano nową zawartość", @@ -98,7 +98,7 @@ "TaskRefreshChannels": "Odśwież kanały", "TaskCleanTranscodeDescription": "Usuwa transkodowane pliki starsze niż 1 dzień.", "TaskCleanTranscode": "Wyczyść folder transkodowania", - "TaskUpdatePluginsDescription": "Pobiera i instaluje aktualizacje dla pluginów które są skonfigurowane do automatycznej aktualizacji.", + "TaskUpdatePluginsDescription": "Pobiera i instaluje aktualizacje dla pluginów, które są skonfigurowane do automatycznej aktualizacji.", "TaskUpdatePlugins": "Aktualizuj pluginy", "TaskRefreshPeopleDescription": "Odświeża metadane o aktorów i reżyserów w Twojej bibliotece mediów.", "TaskRefreshPeople": "Odśwież obsadę", diff --git a/Emby.Server.Implementations/Localization/Core/pr.json b/Emby.Server.Implementations/Localization/Core/pr.json index e3a3bfaf1..81aa996d9 100644 --- a/Emby.Server.Implementations/Localization/Core/pr.json +++ b/Emby.Server.Implementations/Localization/Core/pr.json @@ -1,5 +1,7 @@ { "Books": "Libros", "AuthenticationSucceededWithUserName": "{0} autentificado correctamente", - "Artists": "Artistas" + "Artists": "Artistas", + "Songs": "Shantees", + "Albums": "Ships" } diff --git a/Emby.Server.Implementations/Localization/Core/pt.json b/Emby.Server.Implementations/Localization/Core/pt.json index 3c9ac2c24..a9dbd53ea 100644 --- a/Emby.Server.Implementations/Localization/Core/pt.json +++ b/Emby.Server.Implementations/Localization/Core/pt.json @@ -11,7 +11,7 @@ "HeaderFavoriteEpisodes": "Episódios Favoritos", "HeaderFavoriteShows": "Séries Favoritas", "HeaderContinueWatching": "Continuar assistindo", - "HeaderAlbumArtists": "Discos do Artista", + "HeaderAlbumArtists": "Artistas do Álbum", "Genres": "Gêneros", "Folders": "Diretórios", "Favorites": "Favoritos", diff --git a/Emby.Server.Implementations/Localization/Core/ro.json b/Emby.Server.Implementations/Localization/Core/ro.json index 510aac11c..f8fad7b63 100644 --- a/Emby.Server.Implementations/Localization/Core/ro.json +++ b/Emby.Server.Implementations/Localization/Core/ro.json @@ -74,7 +74,7 @@ "HeaderFavoriteArtists": "Artiști Favoriți", "HeaderFavoriteAlbums": "Albume Favorite", "HeaderContinueWatching": "Vizionează în continuare", - "HeaderAlbumArtists": "Album Artiști", + "HeaderAlbumArtists": "Albume Artiști", "Genres": "Genuri", "Folders": "Dosare", "Favorites": "Favorite", diff --git a/Emby.Server.Implementations/Localization/Core/sq.json b/Emby.Server.Implementations/Localization/Core/sq.json index 066ef5587..2766dab06 100644 --- a/Emby.Server.Implementations/Localization/Core/sq.json +++ b/Emby.Server.Implementations/Localization/Core/sq.json @@ -74,7 +74,7 @@ "NameSeasonUnknown": "Sezon i panjohur", "NameSeasonNumber": "Sezoni {0}", "NameInstallFailed": "Instalimi i {0} dështoi", - "MusicVideos": "Video muzikore", + "MusicVideos": "Video Muzikore", "Music": "Muzikë", "Movies": "Filmat", "MixedContent": "Përmbajtje e përzier", @@ -96,7 +96,7 @@ "HeaderFavoriteArtists": "Artistët e preferuar", "HeaderFavoriteAlbums": "Albumet e preferuar", "HeaderContinueWatching": "Vazhdo të shikosh", - "HeaderAlbumArtists": "Artistët e Albumeve", + "HeaderAlbumArtists": "Artistët e albumeve", "Genres": "Zhanret", "Folders": "Skedarët", "Favorites": "Të preferuarat", diff --git a/Emby.Server.Implementations/Localization/Core/sr.json b/Emby.Server.Implementations/Localization/Core/sr.json index 15fb34186..e31208e80 100644 --- a/Emby.Server.Implementations/Localization/Core/sr.json +++ b/Emby.Server.Implementations/Localization/Core/sr.json @@ -50,7 +50,7 @@ "NameSeasonUnknown": "Непозната сезона", "NameSeasonNumber": "Сезона {0}", "NameInstallFailed": "Инсталација {0} није успела", - "MusicVideos": "Музички спотови", + "MusicVideos": "Музички видео", "Music": "Музика", "Movies": "Филмови", "MixedContent": "Мешовит садржај", @@ -64,7 +64,7 @@ "ItemRemovedWithName": "{0} уклоњено из библиотеке", "ItemAddedWithName": "{0} додато у библиотеку", "Inherit": "Наследи", - "HomeVideos": "Кућни видео", + "HomeVideos": "Кућни Видео", "HeaderRecordingGroups": "Групе снимања", "HeaderNextUp": "Следи", "HeaderLiveTV": "ТВ уживо", @@ -117,5 +117,6 @@ "TaskCleanActivityLog": "Очисти историју активности", "Undefined": "Недефинисано", "Forced": "Принудно", - "Default": "Подразумевано" + "Default": "Подразумевано", + "TaskOptimizeDatabase": "Оптимизуј датабазу" } diff --git a/Emby.Server.Implementations/Localization/Core/sv.json b/Emby.Server.Implementations/Localization/Core/sv.json index 6c772c6a2..f3f601661 100644 --- a/Emby.Server.Implementations/Localization/Core/sv.json +++ b/Emby.Server.Implementations/Localization/Core/sv.json @@ -15,7 +15,7 @@ "Favorites": "Favoriter", "Folders": "Mappar", "Genres": "Genrer", - "HeaderAlbumArtists": "Artistens album", + "HeaderAlbumArtists": "Albumsartister", "HeaderContinueWatching": "Fortsätt kolla", "HeaderFavoriteAlbums": "Favoritalbum", "HeaderFavoriteArtists": "Favoritartister", @@ -96,8 +96,8 @@ "TaskDownloadMissingSubtitles": "Ladda ned saknade undertexter", "TaskRefreshChannelsDescription": "Uppdaterar information för internetkanaler.", "TaskRefreshChannels": "Uppdatera kanaler", - "TaskCleanTranscodeDescription": "Raderar transkodningsfiler som är mer än en dag gamla.", - "TaskCleanTranscode": "Töm transkodningskatalog", + "TaskCleanTranscodeDescription": "Raderar omkodningsfiler som är mer än en dag gamla.", + "TaskCleanTranscode": "Töm omkodningskatalog", "TaskUpdatePluginsDescription": "Laddar ned och installerar uppdateringar till insticksprogram som är konfigurerade att uppdateras automatiskt.", "TaskUpdatePlugins": "Uppdatera insticksprogram", "TaskRefreshPeopleDescription": "Uppdaterar metadata för skådespelare och regissörer i ditt mediabibliotek.", diff --git a/Emby.Server.Implementations/Localization/Core/ta.json b/Emby.Server.Implementations/Localization/Core/ta.json index d6e9aa8e5..98d763fcd 100644 --- a/Emby.Server.Implementations/Localization/Core/ta.json +++ b/Emby.Server.Implementations/Localization/Core/ta.json @@ -21,7 +21,7 @@ "Inherit": "மரபுரிமையாகப் பெறு", "HeaderRecordingGroups": "பதிவு குழுக்கள்", "Folders": "கோப்புறைகள்", - "FailedLoginAttemptWithUserName": "{0} இல் இருந்து உள்நுழைவு முயற்சி தோல்வியடைந்தது", + "FailedLoginAttemptWithUserName": "{0} இன் உள்நுழைவு முயற்சி தோல்வியடைந்தது", "DeviceOnlineWithName": "{0} இணைக்கப்பட்டது", "DeviceOfflineWithName": "{0} துண்டிக்கப்பட்டது", "Collections": "தொகுப்புகள்", @@ -85,7 +85,7 @@ "HeaderFavoriteArtists": "பிடித்த கலைஞர்கள்", "HeaderFavoriteAlbums": "பிடித்த ஆல்பங்கள்", "HeaderContinueWatching": "தொடர்ந்து பார்", - "HeaderAlbumArtists": "இசைக் கலைஞர்கள்", + "HeaderAlbumArtists": "கலைஞரின் ஆல்பம்", "Genres": "வகைகள்", "Favorites": "பிடித்தவை", "ChapterNameValue": "அத்தியாயம் {0}", diff --git a/Emby.Server.Implementations/Localization/Core/te.json b/Emby.Server.Implementations/Localization/Core/te.json index 0967ef424..a9a8ceae0 100644 --- a/Emby.Server.Implementations/Localization/Core/te.json +++ b/Emby.Server.Implementations/Localization/Core/te.json @@ -1 +1,23 @@ -{} +{ + "ValueSpecialEpisodeName": "ప్రత్యేక - {0}", + "Sync": "సమకాలీకరించు", + "Songs": "పాటలు", + "Shows": "ప్రదర్శనలు", + "Playlists": "ప్లేజాబితాలు", + "Photos": "ఫోటోలు", + "MusicVideos": "మ్యూజిక్ వీడియోలు", + "Music": "సంగీతం", + "Movies": "సినిమాలు", + "HeaderContinueWatching": "చూడటం కొనసాగించండి", + "HeaderAlbumArtists": "ఆల్బమ్ కళాకారులు", + "Genres": "శైలులు", + "Forced": "బలవంతంగా", + "Folders": "ఫోల్డర్లు", + "Favorites": "ఇష్టమైనవి", + "Default": "డిఫాల్ట్", + "Collections": "సేకరణలు", + "Channels": "ఛానెల్‌లు", + "Books": "పుస్తకాలు", + "Artists": "కళాకారులు", + "Albums": "ఆల్బమ్‌లు" +} diff --git a/Emby.Server.Implementations/Localization/Core/vi.json b/Emby.Server.Implementations/Localization/Core/vi.json index a5ff3031d..b7ece8d5f 100644 --- a/Emby.Server.Implementations/Localization/Core/vi.json +++ b/Emby.Server.Implementations/Localization/Core/vi.json @@ -3,7 +3,7 @@ "Favorites": "Yêu Thích", "Folders": "Thư Mục", "Genres": "Thể Loại", - "HeaderAlbumArtists": "Album Nghệ sĩ", + "HeaderAlbumArtists": "Album nghệ sĩ", "HeaderContinueWatching": "Xem Tiếp", "HeaderLiveTV": "TV Trực Tiếp", "Movies": "Phim", @@ -103,7 +103,7 @@ "HeaderFavoriteEpisodes": "Tập Phim Yêu Thích", "HeaderFavoriteArtists": "Nghệ Sĩ Yêu Thích", "HeaderFavoriteAlbums": "Album Ưa Thích", - "FailedLoginAttemptWithUserName": "Nỗ lực đăng nhập thất bại từ {0}", + "FailedLoginAttemptWithUserName": "Đăng nhập không thành công thử từ {0}", "DeviceOnlineWithName": "{0} đã kết nối", "DeviceOfflineWithName": "{0} đã ngắt kết nối", "ChapterNameValue": "Phân Cảnh {0}", diff --git a/Emby.Server.Implementations/Localization/Core/zh-CN.json b/Emby.Server.Implementations/Localization/Core/zh-CN.json index f9df62724..ac4eb644b 100644 --- a/Emby.Server.Implementations/Localization/Core/zh-CN.json +++ b/Emby.Server.Implementations/Localization/Core/zh-CN.json @@ -11,7 +11,7 @@ "Collections": "合集", "DeviceOfflineWithName": "{0} 已断开", "DeviceOnlineWithName": "{0} 已连接", - "FailedLoginAttemptWithUserName": "来自 {0} 的失败登入", + "FailedLoginAttemptWithUserName": "从 {0} 尝试登录失败", "Favorites": "我的最爱", "Folders": "文件夹", "Genres": "风格", diff --git a/Emby.Server.Implementations/Localization/Core/zu.json b/Emby.Server.Implementations/Localization/Core/zu.json index 0967ef424..b5f4b920f 100644 --- a/Emby.Server.Implementations/Localization/Core/zu.json +++ b/Emby.Server.Implementations/Localization/Core/zu.json @@ -1 +1,29 @@ -{} +{ + "TasksApplicationCategory": "Ukusetshenziswa", + "TasksLibraryCategory": "Umtapo", + "TasksMaintenanceCategory": "Ukunakekela", + "User": "Umsebenzisi", + "Undefined": "Akuchaziwe", + "System": "Isistimu", + "Sync": "Vumelanisa", + "Songs": "Amaculo", + "Shows": "Izinhlelo", + "Plugin": "Isijobelelo", + "Playlists": "Izinhla Zokudlalayo", + "Photos": "Izithombe", + "Music": "Umculo", + "Movies": "Amamuvi", + "Latest": "lwakamuva", + "Inherit": "Ngefa", + "Forced": "Kuphoqiwe", + "Application": "Ukusetshenziswa", + "Genres": "Izinhlobo", + "Folders": "Izikhwama", + "Favorites": "Izintandokazi", + "Default": "Okumisiwe", + "Collections": "Amaqoqo", + "Channels": "Amashaneli", + "Books": "Izincwadi", + "Artists": "Abadlali", + "Albums": "Ama-albhamu" +} diff --git a/Emby.Server.Implementations/Localization/LocalizationManager.cs b/Emby.Server.Implementations/Localization/LocalizationManager.cs index 9cdbbb6a3..dbd70342a 100644 --- a/Emby.Server.Implementations/Localization/LocalizationManager.cs +++ b/Emby.Server.Implementations/Localization/LocalizationManager.cs @@ -310,7 +310,7 @@ namespace Emby.Server.Implementations.Localization return _dictionaries.GetOrAdd( culture, - (key, localizationManager) => localizationManager.GetDictionary(Prefix, key, DefaultCulture + ".json").GetAwaiter().GetResult(), + static (key, localizationManager) => localizationManager.GetDictionary(Prefix, key, DefaultCulture + ".json").GetAwaiter().GetResult(), this); } diff --git a/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs b/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs index ac6606d39..6e1dc725d 100644 --- a/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs +++ b/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs @@ -9,6 +9,7 @@ using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Extensions; using MediaBrowser.Controller.Chapters; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; @@ -120,7 +121,7 @@ namespace Emby.Server.Implementations.MediaEncoder var path = GetChapterImagePath(video, chapter.StartPositionTicks); - if (!currentImages.Contains(path, StringComparer.OrdinalIgnoreCase)) + if (!currentImages.Contains(path, StringComparison.OrdinalIgnoreCase)) { if (extractImages) { @@ -219,7 +220,7 @@ namespace Emby.Server.Implementations.MediaEncoder { var deadImages = images .Except(chapters.Select(i => i.ImagePath).Where(i => !string.IsNullOrEmpty(i)), StringComparer.OrdinalIgnoreCase) - .Where(i => BaseItem.SupportedImageExtensions.Contains(Path.GetExtension(i), StringComparer.OrdinalIgnoreCase)) + .Where(i => BaseItem.SupportedImageExtensions.Contains(Path.GetExtension(i), StringComparison.OrdinalIgnoreCase)) .ToList(); foreach (var image in deadImages) diff --git a/Emby.Server.Implementations/Playlists/PlaylistsFolder.cs b/Emby.Server.Implementations/Playlists/PlaylistsFolder.cs index 8b1cee89d..8ec9f6161 100644 --- a/Emby.Server.Implementations/Playlists/PlaylistsFolder.cs +++ b/Emby.Server.Implementations/Playlists/PlaylistsFolder.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Linq; using System.Text.Json.Serialization; using Jellyfin.Data.Entities; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Playlists; using MediaBrowser.Model.Querying; @@ -45,7 +46,7 @@ namespace Emby.Server.Implementations.Playlists } query.Recursive = true; - query.IncludeItemTypes = new[] { "Playlist" }; + query.IncludeItemTypes = new[] { BaseItemKind.Playlist }; query.Parent = null; return LibraryManager.GetItemsResult(query); } diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs index 09ea6271d..8b185419f 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs @@ -4,6 +4,7 @@ using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Extensions; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; @@ -143,7 +144,7 @@ namespace Emby.Server.Implementations.ScheduledTasks var key = video.Path + video.DateModified.Ticks; - var extract = !previouslyFailedImages.Contains(key, StringComparer.OrdinalIgnoreCase); + var extract = !previouslyFailedImages.Contains(key, StringComparison.OrdinalIgnoreCase); try { diff --git a/Emby.Server.Implementations/Serialization/MyXmlSerializer.cs b/Emby.Server.Implementations/Serialization/MyXmlSerializer.cs index 059211a0b..1bac2600c 100644 --- a/Emby.Server.Implementations/Serialization/MyXmlSerializer.cs +++ b/Emby.Server.Implementations/Serialization/MyXmlSerializer.cs @@ -21,7 +21,7 @@ namespace Emby.Server.Implementations.Serialization private static XmlSerializer GetSerializer(Type type) => _serializers.GetOrAdd( type.FullName ?? throw new ArgumentException($"Invalid type {type}."), - (_, t) => new XmlSerializer(t), + static (_, t) => new XmlSerializer(t), type); /// <summary> diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index 341d2c8e6..d10a24bbc 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -1292,7 +1292,7 @@ namespace Emby.Server.Implementations.Session { ["ItemId"] = command.ItemId, ["ItemName"] = command.ItemName, - ["ItemType"] = command.ItemType + ["ItemType"] = command.ItemType.ToString() } }; diff --git a/Emby.Server.Implementations/TV/TVSeriesManager.cs b/Emby.Server.Implementations/TV/TVSeriesManager.cs index 4d990c871..a87831294 100644 --- a/Emby.Server.Implementations/TV/TVSeriesManager.cs +++ b/Emby.Server.Implementations/TV/TVSeriesManager.cs @@ -116,7 +116,7 @@ namespace Emby.Server.Implementations.TV .GetItemList( new InternalItemsQuery(user) { - IncludeItemTypes = new[] { nameof(Episode) }, + IncludeItemTypes = new[] { BaseItemKind.Episode }, OrderBy = new[] { new ValueTuple<string, SortOrder>(ItemSortBy.DatePlayed, SortOrder.Descending) }, SeriesPresentationUniqueKey = presentationUniqueKey, Limit = limit, @@ -191,7 +191,7 @@ namespace Emby.Server.Implementations.TV { AncestorWithPresentationUniqueKey = null, SeriesPresentationUniqueKey = seriesKey, - IncludeItemTypes = new[] { nameof(Episode) }, + IncludeItemTypes = new[] { BaseItemKind.Episode }, OrderBy = new[] { new ValueTuple<string, SortOrder>(ItemSortBy.SortName, SortOrder.Descending) }, IsPlayed = true, Limit = 1, @@ -209,7 +209,7 @@ namespace Emby.Server.Implementations.TV { AncestorWithPresentationUniqueKey = null, SeriesPresentationUniqueKey = seriesKey, - IncludeItemTypes = new[] { nameof(Episode) }, + IncludeItemTypes = new[] { BaseItemKind.Episode }, OrderBy = new[] { new ValueTuple<string, SortOrder>(ItemSortBy.SortName, SortOrder.Ascending) }, Limit = 1, IsPlayed = false, @@ -226,7 +226,7 @@ namespace Emby.Server.Implementations.TV AncestorWithPresentationUniqueKey = null, SeriesPresentationUniqueKey = seriesKey, ParentIndexNumber = 0, - IncludeItemTypes = new[] { nameof(Episode) }, + IncludeItemTypes = new[] { BaseItemKind.Episode }, IsPlayed = false, IsVirtualItem = false, DtoOptions = dtoOptions diff --git a/Emby.Server.Implementations/Updates/InstallationManager.cs b/Emby.Server.Implementations/Updates/InstallationManager.cs index ef95ebf94..24d592525 100644 --- a/Emby.Server.Implementations/Updates/InstallationManager.cs +++ b/Emby.Server.Implementations/Updates/InstallationManager.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; +using System.IO.Compression; using System.Linq; using System.Net.Http; using System.Net.Http.Json; @@ -19,7 +20,6 @@ using MediaBrowser.Controller; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Events; using MediaBrowser.Controller.Events.Updates; -using MediaBrowser.Model.IO; using MediaBrowser.Model.Plugins; using MediaBrowser.Model.Updates; using Microsoft.Extensions.Logging; @@ -47,7 +47,6 @@ namespace Emby.Server.Implementations.Updates /// </summary> /// <value>The application host.</value> private readonly IServerApplicationHost _applicationHost; - private readonly IZipClient _zipClient; private readonly object _currentInstallationsLock = new object(); /// <summary> @@ -69,7 +68,6 @@ namespace Emby.Server.Implementations.Updates /// <param name="eventManager">The <see cref="IEventManager"/>.</param> /// <param name="httpClientFactory">The <see cref="IHttpClientFactory"/>.</param> /// <param name="config">The <see cref="IServerConfigurationManager"/>.</param> - /// <param name="zipClient">The <see cref="IZipClient"/>.</param> /// <param name="pluginManager">The <see cref="IPluginManager"/>.</param> public InstallationManager( ILogger<InstallationManager> logger, @@ -78,7 +76,6 @@ namespace Emby.Server.Implementations.Updates IEventManager eventManager, IHttpClientFactory httpClientFactory, IServerConfigurationManager config, - IZipClient zipClient, IPluginManager pluginManager) { _currentInstallations = new List<(InstallationInfo, CancellationTokenSource)>(); @@ -90,7 +87,6 @@ namespace Emby.Server.Implementations.Updates _eventManager = eventManager; _httpClientFactory = httpClientFactory; _config = config; - _zipClient = zipClient; _jsonSerializerOptions = JsonDefaults.Options; _pluginManager = pluginManager; } @@ -560,7 +556,8 @@ namespace Emby.Server.Implementations.Updates } stream.Position = 0; - _zipClient.ExtractAllFromZip(stream, targetDir, true); + using var reader = new ZipArchive(stream); + reader.ExtractToDirectory(targetDir, true); await _pluginManager.GenerateManifest(package.PackageInfo, package.Version, targetDir, status).ConfigureAwait(false); _pluginManager.ImportPluginFrom(targetDir); } diff --git a/Jellyfin.Api/Controllers/ArtistsController.cs b/Jellyfin.Api/Controllers/ArtistsController.cs index 154a56702..3df975563 100644 --- a/Jellyfin.Api/Controllers/ArtistsController.cs +++ b/Jellyfin.Api/Controllers/ArtistsController.cs @@ -133,8 +133,8 @@ namespace Jellyfin.Api.Controllers var query = new InternalItemsQuery(user) { - ExcludeItemTypes = RequestHelpers.GetItemTypeStrings(excludeItemTypes), - IncludeItemTypes = RequestHelpers.GetItemTypeStrings(includeItemTypes), + ExcludeItemTypes = excludeItemTypes, + IncludeItemTypes = includeItemTypes, MediaTypes = mediaTypes, StartIndex = startIndex, Limit = limit, @@ -337,8 +337,8 @@ namespace Jellyfin.Api.Controllers var query = new InternalItemsQuery(user) { - ExcludeItemTypes = RequestHelpers.GetItemTypeStrings(excludeItemTypes), - IncludeItemTypes = RequestHelpers.GetItemTypeStrings(includeItemTypes), + ExcludeItemTypes = excludeItemTypes, + IncludeItemTypes = includeItemTypes, MediaTypes = mediaTypes, StartIndex = startIndex, Limit = limit, diff --git a/Jellyfin.Api/Controllers/FilterController.cs b/Jellyfin.Api/Controllers/FilterController.cs index 223b2a2b6..e170436d1 100644 --- a/Jellyfin.Api/Controllers/FilterController.cs +++ b/Jellyfin.Api/Controllers/FilterController.cs @@ -1,7 +1,6 @@ using System; using System.Linq; using Jellyfin.Api.Constants; -using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; using Jellyfin.Data.Enums; using MediaBrowser.Controller.Dto; @@ -71,7 +70,7 @@ namespace Jellyfin.Api.Controllers { User = user, MediaTypes = mediaTypes, - IncludeItemTypes = RequestHelpers.GetItemTypeStrings(includeItemTypes), + IncludeItemTypes = includeItemTypes, Recursive = true, EnableTotalRecordCount = false, DtoOptions = new DtoOptions @@ -166,7 +165,7 @@ namespace Jellyfin.Api.Controllers var filters = new QueryFilters(); var genreQuery = new InternalItemsQuery(user) { - IncludeItemTypes = RequestHelpers.GetItemTypeStrings(includeItemTypes), + IncludeItemTypes = includeItemTypes, DtoOptions = new DtoOptions { Fields = Array.Empty<ItemFields>(), diff --git a/Jellyfin.Api/Controllers/GenresController.cs b/Jellyfin.Api/Controllers/GenresController.cs index 5aa457153..37e6ae184 100644 --- a/Jellyfin.Api/Controllers/GenresController.cs +++ b/Jellyfin.Api/Controllers/GenresController.cs @@ -101,8 +101,8 @@ namespace Jellyfin.Api.Controllers var query = new InternalItemsQuery(user) { - ExcludeItemTypes = RequestHelpers.GetItemTypeStrings(excludeItemTypes), - IncludeItemTypes = RequestHelpers.GetItemTypeStrings(includeItemTypes), + ExcludeItemTypes = excludeItemTypes, + IncludeItemTypes = includeItemTypes, StartIndex = startIndex, Limit = limit, IsFavorite = isFavorite, @@ -160,7 +160,7 @@ namespace Jellyfin.Api.Controllers Genre item = new Genre(); if (genreName.IndexOf(BaseItem.SlugChar, StringComparison.OrdinalIgnoreCase) != -1) { - var result = GetItemFromSlugName<Genre>(_libraryManager, genreName, dtoOptions); + var result = GetItemFromSlugName<Genre>(_libraryManager, genreName, dtoOptions, BaseItemKind.Genre); if (result != null) { @@ -182,27 +182,27 @@ namespace Jellyfin.Api.Controllers return _dtoService.GetBaseItemDto(item, dtoOptions); } - private T? GetItemFromSlugName<T>(ILibraryManager libraryManager, string name, DtoOptions dtoOptions) + private T? GetItemFromSlugName<T>(ILibraryManager libraryManager, string name, DtoOptions dtoOptions, BaseItemKind baseItemKind) where T : BaseItem, new() { var result = libraryManager.GetItemList(new InternalItemsQuery { Name = name.Replace(BaseItem.SlugChar, '&'), - IncludeItemTypes = new[] { typeof(T).Name }, + IncludeItemTypes = new[] { baseItemKind }, DtoOptions = dtoOptions }).OfType<T>().FirstOrDefault(); result ??= libraryManager.GetItemList(new InternalItemsQuery { Name = name.Replace(BaseItem.SlugChar, '/'), - IncludeItemTypes = new[] { typeof(T).Name }, + IncludeItemTypes = new[] { baseItemKind }, DtoOptions = dtoOptions }).OfType<T>().FirstOrDefault(); result ??= libraryManager.GetItemList(new InternalItemsQuery { Name = name.Replace(BaseItem.SlugChar, '?'), - IncludeItemTypes = new[] { typeof(T).Name }, + IncludeItemTypes = new[] { baseItemKind }, DtoOptions = dtoOptions }).OfType<T>().FirstOrDefault(); diff --git a/Jellyfin.Api/Controllers/ItemsController.cs b/Jellyfin.Api/Controllers/ItemsController.cs index 45a36c8fe..65c0662d2 100644 --- a/Jellyfin.Api/Controllers/ItemsController.cs +++ b/Jellyfin.Api/Controllers/ItemsController.cs @@ -8,7 +8,6 @@ using Jellyfin.Api.ModelBinders; using Jellyfin.Data.Enums; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Session; using MediaBrowser.Model.Dto; @@ -296,8 +295,8 @@ namespace Jellyfin.Api.Controllers { IsPlayed = isPlayed, MediaTypes = mediaTypes, - IncludeItemTypes = RequestHelpers.GetItemTypeStrings(includeItemTypes), - ExcludeItemTypes = RequestHelpers.GetItemTypeStrings(excludeItemTypes), + IncludeItemTypes = includeItemTypes, + ExcludeItemTypes = excludeItemTypes, Recursive = recursive ?? false, OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder), IsFavorite = isFavorite, @@ -459,7 +458,7 @@ namespace Jellyfin.Api.Controllers { query.AlbumIds = albums.SelectMany(i => { - return _libraryManager.GetItemIds(new InternalItemsQuery { IncludeItemTypes = new[] { nameof(MusicAlbum) }, Name = i, Limit = 1 }); + return _libraryManager.GetItemIds(new InternalItemsQuery { IncludeItemTypes = new[] { BaseItemKind.MusicAlbum }, Name = i, Limit = 1 }); }).ToArray(); } @@ -483,7 +482,7 @@ namespace Jellyfin.Api.Controllers if (query.OrderBy.Count == 0) { // Albums by artist - if (query.ArtistIds.Length > 0 && query.IncludeItemTypes.Length == 1 && string.Equals(query.IncludeItemTypes[0], "MusicAlbum", StringComparison.OrdinalIgnoreCase)) + if (query.ArtistIds.Length > 0 && query.IncludeItemTypes.Length == 1 && query.IncludeItemTypes[0] == BaseItemKind.MusicAlbum) { query.OrderBy = new[] { new ValueTuple<string, SortOrder>(ItemSortBy.ProductionYear, SortOrder.Descending), new ValueTuple<string, SortOrder>(ItemSortBy.SortName, SortOrder.Ascending) }; } @@ -831,8 +830,8 @@ namespace Jellyfin.Api.Controllers CollapseBoxSetItems = false, EnableTotalRecordCount = enableTotalRecordCount, AncestorIds = ancestorIds, - IncludeItemTypes = RequestHelpers.GetItemTypeStrings(includeItemTypes), - ExcludeItemTypes = RequestHelpers.GetItemTypeStrings(excludeItemTypes), + IncludeItemTypes = includeItemTypes, + ExcludeItemTypes = excludeItemTypes, SearchTerm = searchTerm, ExcludeItemIds = excludeItemIds }); diff --git a/Jellyfin.Api/Controllers/LibraryController.cs b/Jellyfin.Api/Controllers/LibraryController.cs index 0be853ca4..f1b9c2f67 100644 --- a/Jellyfin.Api/Controllers/LibraryController.cs +++ b/Jellyfin.Api/Controllers/LibraryController.cs @@ -14,6 +14,8 @@ using Jellyfin.Api.Extensions; using Jellyfin.Api.ModelBinders; using Jellyfin.Api.Models.LibraryDtos; using Jellyfin.Data.Entities; +using Jellyfin.Data.Enums; +using Jellyfin.Extensions; using MediaBrowser.Common.Progress; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Dto; @@ -22,7 +24,6 @@ using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Activity; @@ -36,7 +37,6 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; -using Book = MediaBrowser.Controller.Entities.Book; namespace Jellyfin.Api.Controllers { @@ -413,14 +413,14 @@ namespace Jellyfin.Api.Controllers var counts = new ItemCounts { - AlbumCount = GetCount(typeof(MusicAlbum), user, isFavorite), - EpisodeCount = GetCount(typeof(Episode), user, isFavorite), - MovieCount = GetCount(typeof(Movie), user, isFavorite), - SeriesCount = GetCount(typeof(Series), user, isFavorite), - SongCount = GetCount(typeof(Audio), user, isFavorite), - MusicVideoCount = GetCount(typeof(MusicVideo), user, isFavorite), - BoxSetCount = GetCount(typeof(BoxSet), user, isFavorite), - BookCount = GetCount(typeof(Book), user, isFavorite) + AlbumCount = GetCount(BaseItemKind.MusicAlbum, user, isFavorite), + EpisodeCount = GetCount(BaseItemKind.Episode, user, isFavorite), + MovieCount = GetCount(BaseItemKind.Movie, user, isFavorite), + SeriesCount = GetCount(BaseItemKind.Series, user, isFavorite), + SongCount = GetCount(BaseItemKind.Audio, user, isFavorite), + MusicVideoCount = GetCount(BaseItemKind.MusicVideo, user, isFavorite), + BoxSetCount = GetCount(BaseItemKind.BoxSet, user, isFavorite), + BookCount = GetCount(BaseItemKind.Book, user, isFavorite) }; return counts; @@ -529,7 +529,7 @@ namespace Jellyfin.Api.Controllers { var series = _libraryManager.GetItemList(new InternalItemsQuery { - IncludeItemTypes = new[] { nameof(Series) }, + IncludeItemTypes = new[] { BaseItemKind.Series }, DtoOptions = new DtoOptions(false) { EnableImages = false @@ -559,7 +559,7 @@ namespace Jellyfin.Api.Controllers { var movies = _libraryManager.GetItemList(new InternalItemsQuery { - IncludeItemTypes = new[] { nameof(Movie) }, + IncludeItemTypes = new[] { BaseItemKind.Movie }, DtoOptions = new DtoOptions(false) { EnableImages = false @@ -715,30 +715,31 @@ namespace Jellyfin.Api.Controllers bool? isMovie = item is Movie || (program != null && program.IsMovie) || item is Trailer; bool? isSeries = item is Series || (program != null && program.IsSeries); - var includeItemTypes = new List<string>(); + var includeItemTypes = new List<BaseItemKind>(); if (isMovie.Value) { - includeItemTypes.Add(nameof(Movie)); + includeItemTypes.Add(BaseItemKind.Movie); if (_serverConfigurationManager.Configuration.EnableExternalContentInSuggestions) { - includeItemTypes.Add(nameof(Trailer)); - includeItemTypes.Add(nameof(LiveTvProgram)); + includeItemTypes.Add(BaseItemKind.Trailer); + includeItemTypes.Add(BaseItemKind.LiveTvProgram); } } else if (isSeries.Value) { - includeItemTypes.Add(nameof(Series)); + includeItemTypes.Add(BaseItemKind.Series); } else { // For non series and movie types these columns are typically null - isSeries = null; + // isSeries = null; isMovie = null; - includeItemTypes.Add(item.GetType().Name); + includeItemTypes.Add(item.GetBaseItemKind()); } var query = new InternalItemsQuery(user) { + Genres = item.Genres, Limit = limit, IncludeItemTypes = includeItemTypes.ToArray(), SimilarTo = item, @@ -785,7 +786,7 @@ namespace Jellyfin.Api.Controllers var typesList = types.ToList(); var plugins = _providerManager.GetAllMetadataPlugins() - .Where(i => types.Contains(i.ItemType, StringComparer.OrdinalIgnoreCase)) + .Where(i => types.Contains(i.ItemType, StringComparison.OrdinalIgnoreCase)) .OrderBy(i => typesList.IndexOf(i.ItemType)) .ToList(); @@ -871,11 +872,11 @@ namespace Jellyfin.Api.Controllers return result; } - private int GetCount(Type type, User? user, bool? isFavorite) + private int GetCount(BaseItemKind itemKind, User? user, bool? isFavorite) { var query = new InternalItemsQuery(user) { - IncludeItemTypes = new[] { type.Name }, + IncludeItemTypes = new[] { itemKind }, Limit = 0, Recursive = true, IsVirtualItem = false, @@ -940,10 +941,10 @@ namespace Jellyfin.Api.Controllers } var metadataOptions = _serverConfigurationManager.Configuration.MetadataOptions - .Where(i => itemTypes.Contains(i.ItemType ?? string.Empty, StringComparer.OrdinalIgnoreCase)) + .Where(i => itemTypes.Contains(i.ItemType ?? string.Empty, StringComparison.OrdinalIgnoreCase)) .ToArray(); - return metadataOptions.Length == 0 || metadataOptions.Any(i => !i.DisabledMetadataSavers.Contains(name, StringComparer.OrdinalIgnoreCase)); + return metadataOptions.Length == 0 || metadataOptions.Any(i => !i.DisabledMetadataSavers.Contains(name, StringComparison.OrdinalIgnoreCase)); } private bool IsMetadataFetcherEnabledByDefault(string name, string type, bool isNewLibrary) @@ -967,7 +968,7 @@ namespace Jellyfin.Api.Controllers .ToArray(); return metadataOptions.Length == 0 - || metadataOptions.Any(i => !i.DisabledMetadataFetchers.Contains(name, StringComparer.OrdinalIgnoreCase)); + || metadataOptions.Any(i => !i.DisabledMetadataFetchers.Contains(name, StringComparison.OrdinalIgnoreCase)); } private bool IsImageFetcherEnabledByDefault(string name, string type, bool isNewLibrary) @@ -997,7 +998,7 @@ namespace Jellyfin.Api.Controllers return true; } - return metadataOptions.Any(i => !i.DisabledImageFetchers.Contains(name, StringComparer.OrdinalIgnoreCase)); + return metadataOptions.Any(i => !i.DisabledImageFetchers.Contains(name, StringComparison.OrdinalIgnoreCase)); } } } diff --git a/Jellyfin.Api/Controllers/MoviesController.cs b/Jellyfin.Api/Controllers/MoviesController.cs index 99c90d19e..db72ff2f8 100644 --- a/Jellyfin.Api/Controllers/MoviesController.cs +++ b/Jellyfin.Api/Controllers/MoviesController.cs @@ -11,9 +11,7 @@ using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.LiveTv; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Querying; @@ -84,7 +82,7 @@ namespace Jellyfin.Api.Controllers { IncludeItemTypes = new[] { - nameof(Movie), + BaseItemKind.Movie, // nameof(Trailer), // nameof(LiveTvProgram) }, @@ -99,11 +97,11 @@ namespace Jellyfin.Api.Controllers var recentlyPlayedMovies = _libraryManager.GetItemList(query); - var itemTypes = new List<string> { nameof(Movie) }; + var itemTypes = new List<BaseItemKind> { BaseItemKind.Movie }; if (_serverConfigurationManager.Configuration.EnableExternalContentInSuggestions) { - itemTypes.Add(nameof(Trailer)); - itemTypes.Add(nameof(LiveTvProgram)); + itemTypes.Add(BaseItemKind.Trailer); + itemTypes.Add(BaseItemKind.LiveTvProgram); } var likedMovies = _libraryManager.GetItemList(new InternalItemsQuery(user) @@ -182,11 +180,11 @@ namespace Jellyfin.Api.Controllers DtoOptions dtoOptions, RecommendationType type) { - var itemTypes = new List<string> { nameof(Movie) }; + var itemTypes = new List<BaseItemKind> { BaseItemKind.Movie }; if (_serverConfigurationManager.Configuration.EnableExternalContentInSuggestions) { - itemTypes.Add(nameof(Trailer)); - itemTypes.Add(nameof(LiveTvProgram)); + itemTypes.Add(BaseItemKind.Trailer); + itemTypes.Add(BaseItemKind.LiveTvProgram); } foreach (var name in names) @@ -224,11 +222,11 @@ namespace Jellyfin.Api.Controllers private IEnumerable<RecommendationDto> GetWithActor(User? user, IEnumerable<string> names, int itemLimit, DtoOptions dtoOptions, RecommendationType type) { - var itemTypes = new List<string> { nameof(Movie) }; + var itemTypes = new List<BaseItemKind> { BaseItemKind.Movie }; if (_serverConfigurationManager.Configuration.EnableExternalContentInSuggestions) { - itemTypes.Add(nameof(Trailer)); - itemTypes.Add(nameof(LiveTvProgram)); + itemTypes.Add(BaseItemKind.Trailer); + itemTypes.Add(BaseItemKind.LiveTvProgram); } foreach (var name in names) @@ -264,11 +262,11 @@ namespace Jellyfin.Api.Controllers private IEnumerable<RecommendationDto> GetSimilarTo(User? user, IEnumerable<BaseItem> baselineItems, int itemLimit, DtoOptions dtoOptions, RecommendationType type) { - var itemTypes = new List<string> { nameof(Movie) }; + var itemTypes = new List<BaseItemKind> { BaseItemKind.Movie }; if (_serverConfigurationManager.Configuration.EnableExternalContentInSuggestions) { - itemTypes.Add(nameof(Trailer)); - itemTypes.Add(nameof(LiveTvProgram)); + itemTypes.Add(BaseItemKind.Trailer); + itemTypes.Add(BaseItemKind.LiveTvProgram); } foreach (var item in baselineItems) diff --git a/Jellyfin.Api/Controllers/MusicGenresController.cs b/Jellyfin.Api/Controllers/MusicGenresController.cs index 27eec2b9a..c4c03aa4f 100644 --- a/Jellyfin.Api/Controllers/MusicGenresController.cs +++ b/Jellyfin.Api/Controllers/MusicGenresController.cs @@ -101,8 +101,8 @@ namespace Jellyfin.Api.Controllers var query = new InternalItemsQuery(user) { - ExcludeItemTypes = RequestHelpers.GetItemTypeStrings(excludeItemTypes), - IncludeItemTypes = RequestHelpers.GetItemTypeStrings(includeItemTypes), + ExcludeItemTypes = excludeItemTypes, + IncludeItemTypes = includeItemTypes, StartIndex = startIndex, Limit = limit, IsFavorite = isFavorite, @@ -149,7 +149,7 @@ namespace Jellyfin.Api.Controllers if (genreName.IndexOf(BaseItem.SlugChar, StringComparison.OrdinalIgnoreCase) != -1) { - item = GetItemFromSlugName<MusicGenre>(_libraryManager, genreName, dtoOptions); + item = GetItemFromSlugName<MusicGenre>(_libraryManager, genreName, dtoOptions, BaseItemKind.MusicGenre); } else { @@ -166,27 +166,27 @@ namespace Jellyfin.Api.Controllers return _dtoService.GetBaseItemDto(item, dtoOptions); } - private T? GetItemFromSlugName<T>(ILibraryManager libraryManager, string name, DtoOptions dtoOptions) + private T? GetItemFromSlugName<T>(ILibraryManager libraryManager, string name, DtoOptions dtoOptions, BaseItemKind baseItemKind) where T : BaseItem, new() { var result = libraryManager.GetItemList(new InternalItemsQuery { Name = name.Replace(BaseItem.SlugChar, '&'), - IncludeItemTypes = new[] { typeof(T).Name }, + IncludeItemTypes = new[] { baseItemKind }, DtoOptions = dtoOptions }).OfType<T>().FirstOrDefault(); result ??= libraryManager.GetItemList(new InternalItemsQuery { Name = name.Replace(BaseItem.SlugChar, '/'), - IncludeItemTypes = new[] { typeof(T).Name }, + IncludeItemTypes = new[] { baseItemKind }, DtoOptions = dtoOptions }).OfType<T>().FirstOrDefault(); result ??= libraryManager.GetItemList(new InternalItemsQuery { Name = name.Replace(BaseItem.SlugChar, '?'), - IncludeItemTypes = new[] { typeof(T).Name }, + IncludeItemTypes = new[] { baseItemKind }, DtoOptions = dtoOptions }).OfType<T>().FirstOrDefault(); diff --git a/Jellyfin.Api/Controllers/PluginsController.cs b/Jellyfin.Api/Controllers/PluginsController.cs index 0778ea3fc..b41df1abb 100644 --- a/Jellyfin.Api/Controllers/PluginsController.cs +++ b/Jellyfin.Api/Controllers/PluginsController.cs @@ -9,7 +9,6 @@ using Jellyfin.Api.Attributes; using Jellyfin.Api.Constants; using Jellyfin.Api.Models.PluginDtos; using Jellyfin.Extensions.Json; -using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Plugins; using MediaBrowser.Common.Updates; using MediaBrowser.Model.Net; diff --git a/Jellyfin.Api/Controllers/RemoteImageController.cs b/Jellyfin.Api/Controllers/RemoteImageController.cs index 773cff1ac..9f57a5cdb 100644 --- a/Jellyfin.Api/Controllers/RemoteImageController.cs +++ b/Jellyfin.Api/Controllers/RemoteImageController.cs @@ -3,18 +3,13 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.IO; using System.Linq; -using System.Net.Http; using System.Threading; using System.Threading.Tasks; using Jellyfin.Api.Constants; -using Jellyfin.Extensions; -using MediaBrowser.Common.Extensions; -using MediaBrowser.Common.Net; using MediaBrowser.Controller; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; -using MediaBrowser.Model.IO; using MediaBrowser.Model.Providers; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; diff --git a/Jellyfin.Api/Controllers/SearchController.cs b/Jellyfin.Api/Controllers/SearchController.cs index 73bdf9018..26acb4cdc 100644 --- a/Jellyfin.Api/Controllers/SearchController.cs +++ b/Jellyfin.Api/Controllers/SearchController.cs @@ -4,7 +4,6 @@ using System.ComponentModel.DataAnnotations; using System.Globalization; using System.Linq; using Jellyfin.Api.Constants; -using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; using Jellyfin.Data.Enums; using MediaBrowser.Controller.Drawing; @@ -110,8 +109,8 @@ namespace Jellyfin.Api.Controllers IncludeStudios = includeStudios, StartIndex = startIndex, UserId = userId ?? Guid.Empty, - IncludeItemTypes = RequestHelpers.GetItemTypeStrings(includeItemTypes), - ExcludeItemTypes = RequestHelpers.GetItemTypeStrings(excludeItemTypes), + IncludeItemTypes = includeItemTypes, + ExcludeItemTypes = excludeItemTypes, MediaTypes = mediaTypes, ParentId = parentId, diff --git a/Jellyfin.Api/Controllers/SessionController.cs b/Jellyfin.Api/Controllers/SessionController.cs index 3a04cb3a4..a6bbd40cc 100644 --- a/Jellyfin.Api/Controllers/SessionController.cs +++ b/Jellyfin.Api/Controllers/SessionController.cs @@ -127,7 +127,7 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status204NoContent)] public async Task<ActionResult> DisplayContent( [FromRoute, Required] string sessionId, - [FromQuery, Required] string itemType, + [FromQuery, Required] BaseItemKind itemType, [FromQuery, Required] string itemId, [FromQuery, Required] string itemName) { diff --git a/Jellyfin.Api/Controllers/StartupController.cs b/Jellyfin.Api/Controllers/StartupController.cs index a01a617fc..c49bde93f 100644 --- a/Jellyfin.Api/Controllers/StartupController.cs +++ b/Jellyfin.Api/Controllers/StartupController.cs @@ -93,7 +93,7 @@ namespace Jellyfin.Api.Controllers NetworkConfiguration settings = _config.GetNetworkConfiguration(); settings.EnableRemoteAccess = startupRemoteAccessDto.EnableRemoteAccess; settings.EnableUPnP = startupRemoteAccessDto.EnableAutomaticPortMapping; - _config.SaveConfiguration("network", settings); + _config.SaveConfiguration(NetworkConfigurationStore.StoreKey, settings); return NoContent(); } diff --git a/Jellyfin.Api/Controllers/StudiosController.cs b/Jellyfin.Api/Controllers/StudiosController.cs index da8f8b199..4422ef32c 100644 --- a/Jellyfin.Api/Controllers/StudiosController.cs +++ b/Jellyfin.Api/Controllers/StudiosController.cs @@ -97,8 +97,8 @@ namespace Jellyfin.Api.Controllers var query = new InternalItemsQuery(user) { - ExcludeItemTypes = RequestHelpers.GetItemTypeStrings(excludeItemTypes), - IncludeItemTypes = RequestHelpers.GetItemTypeStrings(includeItemTypes), + ExcludeItemTypes = excludeItemTypes, + IncludeItemTypes = includeItemTypes, StartIndex = startIndex, Limit = limit, IsFavorite = isFavorite, diff --git a/Jellyfin.Api/Controllers/SuggestionsController.cs b/Jellyfin.Api/Controllers/SuggestionsController.cs index 97eec4bd2..af77c801f 100644 --- a/Jellyfin.Api/Controllers/SuggestionsController.cs +++ b/Jellyfin.Api/Controllers/SuggestionsController.cs @@ -58,7 +58,7 @@ namespace Jellyfin.Api.Controllers public ActionResult<QueryResult<BaseItemDto>> GetSuggestions( [FromRoute, Required] Guid userId, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaType, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] type, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] type, [FromQuery] int? startIndex, [FromQuery] int? limit, [FromQuery] bool enableTotalRecordCount = false) diff --git a/Jellyfin.Api/Controllers/SystemController.cs b/Jellyfin.Api/Controllers/SystemController.cs index 2ff85fd2a..411c987f3 100644 --- a/Jellyfin.Api/Controllers/SystemController.cs +++ b/Jellyfin.Api/Controllers/SystemController.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.IO; using System.Linq; -using System.Net; using System.Net.Mime; using System.Threading.Tasks; using Jellyfin.Api.Attributes; diff --git a/Jellyfin.Api/Controllers/TvShowsController.cs b/Jellyfin.Api/Controllers/TvShowsController.cs index 5dd773331..e20bcd7a7 100644 --- a/Jellyfin.Api/Controllers/TvShowsController.cs +++ b/Jellyfin.Api/Controllers/TvShowsController.cs @@ -61,7 +61,7 @@ namespace Jellyfin.Api.Controllers /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param> /// <param name="seriesId">Optional. Filter by series id.</param> /// <param name="parentId">Optional. Specify this to localize the search to a specific item or folder. Omit to use the root.</param> - /// <param name="enableImges">Optional. Include image information in output.</param> + /// <param name="enableImages">Optional. Include image information in output.</param> /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param> /// <param name="enableImageTypes">Optional. The image types to include in the output.</param> /// <param name="enableUserData">Optional. Include user data.</param> @@ -78,7 +78,7 @@ namespace Jellyfin.Api.Controllers [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, [FromQuery] string? seriesId, [FromQuery] Guid? parentId, - [FromQuery] bool? enableImges, + [FromQuery] bool? enableImages, [FromQuery] int? imageTypeLimit, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, [FromQuery] bool? enableUserData, @@ -88,7 +88,7 @@ namespace Jellyfin.Api.Controllers { var options = new DtoOptions { Fields = fields } .AddClientFields(Request) - .AddAdditionalDtoOptions(enableImges, enableUserData, imageTypeLimit, enableImageTypes); + .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); var result = _tvSeriesManager.GetNextUp( new NextUpQuery @@ -125,7 +125,7 @@ namespace Jellyfin.Api.Controllers /// <param name="limit">Optional. The maximum number of records to return.</param> /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param> /// <param name="parentId">Optional. Specify this to localize the search to a specific item or folder. Omit to use the root.</param> - /// <param name="enableImges">Optional. Include image information in output.</param> + /// <param name="enableImages">Optional. Include image information in output.</param> /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param> /// <param name="enableImageTypes">Optional. The image types to include in the output.</param> /// <param name="enableUserData">Optional. Include user data.</param> @@ -138,7 +138,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? limit, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, [FromQuery] Guid? parentId, - [FromQuery] bool? enableImges, + [FromQuery] bool? enableImages, [FromQuery] int? imageTypeLimit, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, [FromQuery] bool? enableUserData) @@ -153,11 +153,11 @@ namespace Jellyfin.Api.Controllers var options = new DtoOptions { Fields = fields } .AddClientFields(Request) - .AddAdditionalDtoOptions(enableImges, enableUserData, imageTypeLimit, enableImageTypes); + .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); var itemsResult = _libraryManager.GetItemList(new InternalItemsQuery(user) { - IncludeItemTypes = new[] { nameof(Episode) }, + IncludeItemTypes = new[] { BaseItemKind.Episode }, OrderBy = new[] { (ItemSortBy.PremiereDate, SortOrder.Ascending), (ItemSortBy.SortName, SortOrder.Ascending) }, MinPremiereDate = minPremiereDate, StartIndex = startIndex, diff --git a/Jellyfin.Api/Controllers/UserLibraryController.cs b/Jellyfin.Api/Controllers/UserLibraryController.cs index a33a0826c..f6fbdc302 100644 --- a/Jellyfin.Api/Controllers/UserLibraryController.cs +++ b/Jellyfin.Api/Controllers/UserLibraryController.cs @@ -6,7 +6,6 @@ using System.Threading; using System.Threading.Tasks; using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; -using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; using Jellyfin.Data.Enums; using Jellyfin.Extensions; @@ -213,7 +212,7 @@ namespace Jellyfin.Api.Controllers if (item is IHasTrailers hasTrailers) { - var trailers = hasTrailers.GetTrailers(); + var trailers = hasTrailers.LocalTrailers; var dtosTrailers = _dtoService.GetBaseItemDtos(trailers, dtoOptions, user, item); var allTrailers = new BaseItemDto[dtosExtras.Length + dtosTrailers.Count]; dtosExtras.CopyTo(allTrailers, 0); @@ -297,7 +296,7 @@ namespace Jellyfin.Api.Controllers new LatestItemsQuery { GroupItems = groupItems, - IncludeItemTypes = RequestHelpers.GetItemTypeStrings(includeItemTypes), + IncludeItemTypes = includeItemTypes, IsPlayed = isPlayed, Limit = limit, ParentId = parentId ?? Guid.Empty, diff --git a/Jellyfin.Api/Controllers/YearsController.cs b/Jellyfin.Api/Controllers/YearsController.cs index d6dc6650c..8be6fd1b5 100644 --- a/Jellyfin.Api/Controllers/YearsController.cs +++ b/Jellyfin.Api/Controllers/YearsController.cs @@ -8,6 +8,7 @@ using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Extensions; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; @@ -101,8 +102,8 @@ namespace Jellyfin.Api.Controllers var query = new InternalItemsQuery(user) { - ExcludeItemTypes = RequestHelpers.GetItemTypeStrings(excludeItemTypes), - IncludeItemTypes = RequestHelpers.GetItemTypeStrings(includeItemTypes), + ExcludeItemTypes = excludeItemTypes, + IncludeItemTypes = includeItemTypes, MediaTypes = mediaTypes, DtoOptions = dtoOptions }; @@ -209,7 +210,7 @@ namespace Jellyfin.Api.Controllers } // Include MediaTypes - if (mediaTypes.Count > 0 && !mediaTypes.Contains(f.MediaType ?? string.Empty, StringComparer.OrdinalIgnoreCase)) + if (mediaTypes.Count > 0 && !mediaTypes.Contains(f.MediaType ?? string.Empty, StringComparison.OrdinalIgnoreCase)) { return false; } diff --git a/Jellyfin.Api/Helpers/ClassMigrationHelper.cs b/Jellyfin.Api/Helpers/ClassMigrationHelper.cs deleted file mode 100644 index 76fb27bcc..000000000 --- a/Jellyfin.Api/Helpers/ClassMigrationHelper.cs +++ /dev/null @@ -1,71 +0,0 @@ -using System; -using System.Reflection; - -namespace Jellyfin.Api.Helpers -{ - /// <summary> - /// A static class for copying matching properties from one object to another. - /// TODO: remove at the point when a fixed migration path has been decided upon. - /// </summary> - public static class ClassMigrationHelper - { - /// <summary> - /// Extension for 'Object' that copies the properties to a destination object. - /// </summary> - /// <param name="source">The source.</param> - /// <param name="destination">The destination.</param> - public static void CopyProperties(this object source, object destination) - { - // If any this null throw an exception. - if (source == null || destination == null) - { - throw new ArgumentException("Source or/and Destination Objects are null"); - } - - // Getting the Types of the objects. - Type typeDest = destination.GetType(); - Type typeSrc = source.GetType(); - - // Iterate the Properties of the source instance and populate them from their destination counterparts. - PropertyInfo[] srcProps = typeSrc.GetProperties(); - foreach (PropertyInfo srcProp in srcProps) - { - if (!srcProp.CanRead) - { - continue; - } - - var targetProperty = typeDest.GetProperty(srcProp.Name); - if (targetProperty == null) - { - continue; - } - - if (!targetProperty.CanWrite) - { - continue; - } - - var obj = targetProperty.GetSetMethod(true); - if (obj != null && obj.IsPrivate) - { - continue; - } - - var target = targetProperty.GetSetMethod(); - if (target != null && (target.Attributes & MethodAttributes.Static) != 0) - { - continue; - } - - if (!targetProperty.PropertyType.IsAssignableFrom(srcProp.PropertyType)) - { - continue; - } - - // Passed all tests, lets set the value. - targetProperty.SetValue(destination, srcProp.GetValue(source, null), null); - } - } - } -} diff --git a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs index 4abe4c5d5..02af2e435 100644 --- a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs +++ b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Globalization; using System.Linq; @@ -462,6 +462,11 @@ namespace Jellyfin.Api.Helpers private void AddSubtitles(StreamState state, IEnumerable<MediaStream> subtitles, StringBuilder builder, ClaimsPrincipal user) { + if (state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Drop) + { + return; + } + var selectedIndex = state.SubtitleStream == null || state.SubtitleDeliveryMethod != SubtitleDeliveryMethod.Hls ? (int?)null : state.SubtitleStream.Index; const string Format = "#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID=\"subs\",NAME=\"{0}\",DEFAULT={1},FORCED={2},AUTOSELECT=YES,URI=\"{3}\",LANGUAGE=\"{4}\""; diff --git a/Jellyfin.Api/Helpers/RequestHelpers.cs b/Jellyfin.Api/Helpers/RequestHelpers.cs index 0efd3443b..ca8bc0bdd 100644 --- a/Jellyfin.Api/Helpers/RequestHelpers.cs +++ b/Jellyfin.Api/Helpers/RequestHelpers.cs @@ -137,21 +137,5 @@ namespace Jellyfin.Api.Helpers TotalRecordCount = result.TotalRecordCount }; } - - internal static string[] GetItemTypeStrings(IReadOnlyList<BaseItemKind> itemKinds) - { - if (itemKinds.Count == 0) - { - return Array.Empty<string>(); - } - - var itemTypes = new string[itemKinds.Count]; - for (var i = 0; i < itemKinds.Count; i++) - { - itemTypes[i] = itemKinds[i].ToString(); - } - - return itemTypes; - } } } diff --git a/Jellyfin.Api/Helpers/StreamingHelpers.cs b/Jellyfin.Api/Helpers/StreamingHelpers.cs index 1b8f24c27..ed071bcd7 100644 --- a/Jellyfin.Api/Helpers/StreamingHelpers.cs +++ b/Jellyfin.Api/Helpers/StreamingHelpers.cs @@ -90,6 +90,7 @@ namespace Jellyfin.Api.Helpers } var enableDlnaHeaders = !string.IsNullOrWhiteSpace(streamingRequest.Params) || + streamingRequest.StreamOptions.ContainsKey("dlnaheaders") || string.Equals(httpRequest.Headers["GetContentFeatures.DLNA.ORG"], "1", StringComparison.OrdinalIgnoreCase); var state = new StreamState(mediaSourceManager, transcodingJobType, transcodingJobHelper) diff --git a/Jellyfin.Api/Jellyfin.Api.csproj b/Jellyfin.Api/Jellyfin.Api.csproj index a3598edfa..ccd647ebe 100644 --- a/Jellyfin.Api/Jellyfin.Api.csproj +++ b/Jellyfin.Api/Jellyfin.Api.csproj @@ -13,7 +13,7 @@ </PropertyGroup> <ItemGroup> - <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="6.0.0" /> + <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="6.0.1" /> <PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="6.2.3" /> <PackageReference Include="Swashbuckle.AspNetCore.ReDoc" Version="6.2.3" /> diff --git a/Jellyfin.Networking/Configuration/NetworkConfigurationFactory.cs b/Jellyfin.Networking/Configuration/NetworkConfigurationFactory.cs index ac0485d87..14726565a 100644 --- a/Jellyfin.Networking/Configuration/NetworkConfigurationFactory.cs +++ b/Jellyfin.Networking/Configuration/NetworkConfigurationFactory.cs @@ -16,11 +16,7 @@ namespace Jellyfin.Networking.Configuration { return new[] { - new ConfigurationStore - { - Key = "network", - ConfigurationType = typeof(NetworkConfiguration) - } + new NetworkConfigurationStore() }; } } diff --git a/Jellyfin.Networking/Configuration/NetworkConfigurationStore.cs b/Jellyfin.Networking/Configuration/NetworkConfigurationStore.cs new file mode 100644 index 000000000..a268ebb68 --- /dev/null +++ b/Jellyfin.Networking/Configuration/NetworkConfigurationStore.cs @@ -0,0 +1,24 @@ +using MediaBrowser.Common.Configuration; + +namespace Jellyfin.Networking.Configuration +{ + /// <summary> + /// A configuration that stores network related settings. + /// </summary> + public class NetworkConfigurationStore : ConfigurationStore + { + /// <summary> + /// The name of the configuration in the storage. + /// </summary> + public const string StoreKey = "network"; + + /// <summary> + /// Initializes a new instance of the <see cref="NetworkConfigurationStore"/> class. + /// </summary> + public NetworkConfigurationStore() + { + ConfigurationType = typeof(NetworkConfiguration); + Key = StoreKey; + } + } +} diff --git a/Jellyfin.Networking/Manager/NetworkManager.cs b/Jellyfin.Networking/Manager/NetworkManager.cs index cf002dc73..58b30ad2d 100644 --- a/Jellyfin.Networking/Manager/NetworkManager.cs +++ b/Jellyfin.Networking/Manager/NetworkManager.cs @@ -727,7 +727,7 @@ namespace Jellyfin.Networking.Manager private void ConfigurationUpdated(object? sender, ConfigurationUpdateEventArgs evt) { - if (evt.Key.Equals("network", StringComparison.Ordinal)) + if (evt.Key.Equals(NetworkConfigurationStore.StoreKey, StringComparison.Ordinal)) { UpdateSettings((NetworkConfiguration)evt.NewConfiguration); } diff --git a/Jellyfin.Server.Implementations/Devices/DeviceManager.cs b/Jellyfin.Server.Implementations/Devices/DeviceManager.cs index 0655c9813..a55949df8 100644 --- a/Jellyfin.Server.Implementations/Devices/DeviceManager.cs +++ b/Jellyfin.Server.Implementations/Devices/DeviceManager.cs @@ -7,6 +7,7 @@ using Jellyfin.Data.Entities.Security; using Jellyfin.Data.Enums; using Jellyfin.Data.Events; using Jellyfin.Data.Queries; +using Jellyfin.Extensions; using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Library; using MediaBrowser.Model.Devices; @@ -219,7 +220,7 @@ namespace Jellyfin.Server.Implementations.Devices return true; } - return user.GetPreference(PreferenceKind.EnabledDevices).Contains(deviceId, StringComparer.OrdinalIgnoreCase) + return user.GetPreference(PreferenceKind.EnabledDevices).Contains(deviceId, StringComparison.OrdinalIgnoreCase) || !GetCapabilities(deviceId).SupportsPersistentIdentifier; } diff --git a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj index 73ee69424..d22757c03 100644 --- a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj +++ b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj @@ -18,14 +18,14 @@ </ItemGroup> <ItemGroup> - <PackageReference Include="System.Linq.Async" Version="5.0.0" /> - <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.0" /> - <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.0" /> - <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.0"> + <PackageReference Include="System.Linq.Async" Version="5.1.0" /> + <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.1" /> + <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.1" /> + <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.1"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> </PackageReference> - <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.0"> + <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.1"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> </PackageReference> diff --git a/Jellyfin.Server/Jellyfin.Server.csproj b/Jellyfin.Server/Jellyfin.Server.csproj index bfe8e82e8..1638310fd 100644 --- a/Jellyfin.Server/Jellyfin.Server.csproj +++ b/Jellyfin.Server/Jellyfin.Server.csproj @@ -33,8 +33,8 @@ <PackageReference Include="CommandLineParser" Version="2.8.0" /> <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="6.0.0" /> <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="6.0.0" /> - <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="6.0.0" /> - <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="6.0.0" /> + <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="6.0.1" /> + <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="6.0.1" /> <PackageReference Include="prometheus-net" Version="5.0.2" /> <PackageReference Include="prometheus-net.AspNetCore" Version="5.0.2" /> <PackageReference Include="Serilog.AspNetCore" Version="4.1.0" /> diff --git a/Jellyfin.Server/Migrations/MigrationRunner.cs b/Jellyfin.Server/Migrations/MigrationRunner.cs index 7365c8dbc..e9a45c140 100644 --- a/Jellyfin.Server/Migrations/MigrationRunner.cs +++ b/Jellyfin.Server/Migrations/MigrationRunner.cs @@ -1,6 +1,11 @@ using System; +using System.Collections.Generic; +using System.IO; using System.Linq; +using Emby.Server.Implementations; +using Emby.Server.Implementations.Serialization; using MediaBrowser.Common.Configuration; +using MediaBrowser.Model.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -11,6 +16,14 @@ namespace Jellyfin.Server.Migrations /// </summary> public sealed class MigrationRunner { + /// <summary> + /// The list of known pre-startup migrations, in order of applicability. + /// </summary> + private static readonly Type[] _preStartupMigrationTypes = + { + typeof(PreStartupRoutines.CreateNetworkConfiguration) + }; + /// <summary> /// The list of known migrations, in order of applicability. /// </summary> @@ -41,17 +54,55 @@ namespace Jellyfin.Server.Migrations .Select(m => ActivatorUtilities.CreateInstance(host.ServiceProvider, m)) .OfType<IMigrationRoutine>() .ToArray(); - var migrationOptions = host.ConfigurationManager.GetConfiguration<MigrationOptions>(MigrationsListStore.StoreKey); - if (!host.ConfigurationManager.Configuration.IsStartupWizardCompleted && migrationOptions.Applied.Count == 0) + var migrationOptions = host.ConfigurationManager.GetConfiguration<MigrationOptions>(MigrationsListStore.StoreKey); + HandleStartupWizardCondition(migrations, migrationOptions, host.ConfigurationManager.Configuration.IsStartupWizardCompleted, logger); + PerformMigrations(migrations, migrationOptions, options => host.ConfigurationManager.SaveConfiguration(MigrationsListStore.StoreKey, options), logger); + } + + /// <summary> + /// Run all needed pre-startup migrations. + /// </summary> + /// <param name="appPaths">Application paths.</param> + /// <param name="loggerFactory">Factory for making the logger.</param> + public static void RunPreStartup(ServerApplicationPaths appPaths, ILoggerFactory loggerFactory) + { + var logger = loggerFactory.CreateLogger<MigrationRunner>(); + var migrations = _preStartupMigrationTypes + .Select(m => Activator.CreateInstance(m, appPaths, loggerFactory)) + .OfType<IMigrationRoutine>() + .ToArray(); + + var xmlSerializer = new MyXmlSerializer(); + var migrationConfigPath = Path.Join(appPaths.ConfigurationDirectoryPath, MigrationsListStore.StoreKey.ToLowerInvariant() + ".xml"); + var migrationOptions = File.Exists(migrationConfigPath) + ? (MigrationOptions)xmlSerializer.DeserializeFromFile(typeof(MigrationOptions), migrationConfigPath)! + : new MigrationOptions(); + + // We have to deserialize it manually since the configuration manager may overwrite it + var serverConfig = File.Exists(appPaths.SystemConfigurationFilePath) + ? (ServerConfiguration)xmlSerializer.DeserializeFromFile(typeof(ServerConfiguration), appPaths.SystemConfigurationFilePath)! + : new ServerConfiguration(); + + HandleStartupWizardCondition(migrations, migrationOptions, serverConfig.IsStartupWizardCompleted, logger); + PerformMigrations(migrations, migrationOptions, options => xmlSerializer.SerializeToFile(options, migrationConfigPath), logger); + } + + private static void HandleStartupWizardCondition(IEnumerable<IMigrationRoutine> migrations, MigrationOptions migrationOptions, bool isStartWizardCompleted, ILogger logger) + { + if (isStartWizardCompleted || migrationOptions.Applied.Count != 0) { - // If startup wizard is not finished, this is a fresh install. - // Don't run any migrations, just mark all of them as applied. - logger.LogInformation("Marking all known migrations as applied because this is a fresh install"); - migrationOptions.Applied.AddRange(migrations.Where(m => !m.PerformOnNewInstall).Select(m => (m.Id, m.Name))); - host.ConfigurationManager.SaveConfiguration(MigrationsListStore.StoreKey, migrationOptions); + return; } + // If startup wizard is not finished, this is a fresh install. + var onlyOldInstalls = migrations.Where(m => !m.PerformOnNewInstall).ToArray(); + logger.LogInformation("Marking following migrations as applied because this is a fresh install: {@OnlyOldInstalls}", onlyOldInstalls.Select(m => m.Name)); + migrationOptions.Applied.AddRange(onlyOldInstalls.Select(m => (m.Id, m.Name))); + } + + private static void PerformMigrations(IMigrationRoutine[] migrations, MigrationOptions migrationOptions, Action<MigrationOptions> saveConfiguration, ILogger logger) + { var appliedMigrationIds = migrationOptions.Applied.Select(m => m.Id).ToHashSet(); for (var i = 0; i < migrations.Length; i++) @@ -78,7 +129,7 @@ namespace Jellyfin.Server.Migrations // Mark the migration as completed logger.LogInformation("Migration '{Name}' applied successfully", migrationRoutine.Name); migrationOptions.Applied.Add((migrationRoutine.Id, migrationRoutine.Name)); - host.ConfigurationManager.SaveConfiguration(MigrationsListStore.StoreKey, migrationOptions); + saveConfiguration(migrationOptions); logger.LogDebug("Migration '{Name}' marked as applied in configuration.", migrationRoutine.Name); } } diff --git a/Jellyfin.Server/Migrations/PreStartupRoutines/CreateNetworkConfiguration.cs b/Jellyfin.Server/Migrations/PreStartupRoutines/CreateNetworkConfiguration.cs new file mode 100644 index 000000000..a951f751e --- /dev/null +++ b/Jellyfin.Server/Migrations/PreStartupRoutines/CreateNetworkConfiguration.cs @@ -0,0 +1,138 @@ +using System; +using System.IO; +using System.Xml; +using System.Xml.Serialization; +using Emby.Server.Implementations; +using Microsoft.Extensions.Logging; + +namespace Jellyfin.Server.Migrations.PreStartupRoutines; + +/// <inheritdoc /> +public class CreateNetworkConfiguration : IMigrationRoutine +{ + private readonly ServerApplicationPaths _applicationPaths; + private readonly ILogger<CreateNetworkConfiguration> _logger; + + /// <summary> + /// Initializes a new instance of the <see cref="CreateNetworkConfiguration"/> class. + /// </summary> + /// <param name="applicationPaths">An instance of <see cref="ServerApplicationPaths"/>.</param> + /// <param name="loggerFactory">An instance of the <see cref="ILoggerFactory"/> interface.</param> + public CreateNetworkConfiguration(ServerApplicationPaths applicationPaths, ILoggerFactory loggerFactory) + { + _applicationPaths = applicationPaths; + _logger = loggerFactory.CreateLogger<CreateNetworkConfiguration>(); + } + + /// <inheritdoc /> + public Guid Id => Guid.Parse("9B354818-94D5-4B68-AC49-E35CB85F9D84"); + + /// <inheritdoc /> + public string Name => nameof(CreateNetworkConfiguration); + + /// <inheritdoc /> + public bool PerformOnNewInstall => false; + + /// <inheritdoc /> + public void Perform() + { + string path = Path.Combine(_applicationPaths.ConfigurationDirectoryPath, "network.xml"); + if (File.Exists(path)) + { + _logger.LogDebug("Network configuration file already exists, skipping"); + return; + } + + var serverConfigSerializer = new XmlSerializer(typeof(OldNetworkConfiguration), new XmlRootAttribute("ServerConfiguration")); + using var xmlReader = XmlReader.Create(_applicationPaths.SystemConfigurationFilePath); + var networkSettings = serverConfigSerializer.Deserialize(xmlReader); + + var networkConfigSerializer = new XmlSerializer(typeof(OldNetworkConfiguration), new XmlRootAttribute("NetworkConfiguration")); + var xmlWriterSettings = new XmlWriterSettings { Indent = true }; + using var xmlWriter = XmlWriter.Create(path, xmlWriterSettings); + networkConfigSerializer.Serialize(xmlWriter, networkSettings); + } + +#pragma warning disable CS1591 + public sealed class OldNetworkConfiguration + { + public const int DefaultHttpPort = 8096; + + public const int DefaultHttpsPort = 8920; + + private string _baseUrl = string.Empty; + + public bool RequireHttps { get; set; } + + public string CertificatePath { get; set; } = string.Empty; + + public string CertificatePassword { get; set; } = string.Empty; + + public string BaseUrl + { + get => _baseUrl; + set + { + // Normalize the start of the string + if (string.IsNullOrWhiteSpace(value)) + { + // If baseUrl is empty, set an empty prefix string + _baseUrl = string.Empty; + return; + } + + if (value[0] != '/') + { + // If baseUrl was not configured with a leading slash, append one for consistency + value = "/" + value; + } + + // Normalize the end of the string + if (value[^1] == '/') + { + // If baseUrl was configured with a trailing slash, remove it for consistency + value = value.Remove(value.Length - 1); + } + + _baseUrl = value; + } + } + + public int PublicHttpsPort { get; set; } = DefaultHttpsPort; + + public int HttpServerPortNumber { get; set; } = DefaultHttpPort; + + public int HttpsPortNumber { get; set; } = DefaultHttpsPort; + + public bool EnableHttps { get; set; } + + public int PublicPort { get; set; } = DefaultHttpPort; + + public bool EnableIPV6 { get; set; } + + public bool EnableIPV4 { get; set; } = true; + + public bool IgnoreVirtualInterfaces { get; set; } = true; + + public string VirtualInterfaceNames { get; set; } = "vEthernet*"; + + public bool TrustAllIP6Interfaces { get; set; } + + public string[] PublishedServerUriBySubnet { get; set; } = Array.Empty<string>(); + + public string[] RemoteIPFilter { get; set; } = Array.Empty<string>(); + + public bool IsRemoteIPFilterBlacklist { get; set; } + + public bool EnableUPnP { get; set; } + + public bool EnableRemoteAccess { get; set; } = true; + + public string[] LocalNetworkSubnets { get; set; } = Array.Empty<string>(); + + public string[] LocalNetworkAddresses { get; set; } = Array.Empty<string>(); + + public string[] KnownProxies { get; set; } = Array.Empty<string>(); + } +#pragma warning restore CS1591 +} diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index 7f158aebb..f40526e22 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -175,6 +175,7 @@ namespace Jellyfin.Server } PerformStaticInitialization(); + Migrations.MigrationRunner.RunPreStartup(appPaths, _loggerFactory); var appHost = new CoreAppHost( appPaths, diff --git a/MediaBrowser.Controller/BaseItemManager/BaseItemManager.cs b/MediaBrowser.Controller/BaseItemManager/BaseItemManager.cs index ba2f419a2..d273b54fc 100644 --- a/MediaBrowser.Controller/BaseItemManager/BaseItemManager.cs +++ b/MediaBrowser.Controller/BaseItemManager/BaseItemManager.cs @@ -55,11 +55,6 @@ namespace MediaBrowser.Controller.BaseItemManager return typeOptions.MetadataFetchers.Contains(name.AsSpan(), StringComparison.OrdinalIgnoreCase); } - if (!libraryOptions.EnableInternetProviders) - { - return false; - } - var itemConfig = _serverConfigurationManager.Configuration.MetadataOptions.FirstOrDefault(i => string.Equals(i.ItemType, baseItem.GetType().Name, StringComparison.OrdinalIgnoreCase)); return itemConfig == null || !itemConfig.DisabledMetadataFetchers.Contains(name.AsSpan(), StringComparison.OrdinalIgnoreCase); @@ -86,11 +81,6 @@ namespace MediaBrowser.Controller.BaseItemManager return typeOptions.ImageFetchers.Contains(name.AsSpan(), StringComparison.OrdinalIgnoreCase); } - if (!libraryOptions.EnableInternetProviders) - { - return false; - } - var itemConfig = _serverConfigurationManager.Configuration.MetadataOptions.FirstOrDefault(i => string.Equals(i.ItemType, baseItem.GetType().Name, StringComparison.OrdinalIgnoreCase)); return itemConfig == null || !itemConfig.DisabledImageFetchers.Contains(name.AsSpan(), StringComparison.OrdinalIgnoreCase); diff --git a/MediaBrowser.Controller/Entities/Audio/Audio.cs b/MediaBrowser.Controller/Entities/Audio/Audio.cs index e90a2f56a..9d0187c8c 100644 --- a/MediaBrowser.Controller/Entities/Audio/Audio.cs +++ b/MediaBrowser.Controller/Entities/Audio/Audio.cs @@ -8,10 +8,8 @@ using System.Globalization; using System.Linq; using System.Text.Json.Serialization; using Jellyfin.Data.Enums; -using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Dto; -using MediaBrowser.Model.Entities; namespace MediaBrowser.Controller.Entities.Audio { diff --git a/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs b/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs index f30f8ce7f..11b95b94b 100644 --- a/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs +++ b/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs @@ -88,7 +88,7 @@ namespace MediaBrowser.Controller.Entities.Audio { if (query.IncludeItemTypes.Length == 0) { - query.IncludeItemTypes = new[] { nameof(Audio), nameof(MusicVideo), nameof(MusicAlbum) }; + query.IncludeItemTypes = new[] { BaseItemKind.Audio, BaseItemKind.MusicVideo, BaseItemKind.MusicAlbum }; query.ArtistIds = new[] { Id }; } diff --git a/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs b/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs index dc6fcc55a..73a25232e 100644 --- a/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs +++ b/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; using System.Text.Json.Serialization; using Diacritics.Extensions; +using Jellyfin.Data.Enums; using Microsoft.Extensions.Logging; namespace MediaBrowser.Controller.Entities.Audio @@ -66,7 +67,7 @@ namespace MediaBrowser.Controller.Entities.Audio public IList<BaseItem> GetTaggedItems(InternalItemsQuery query) { query.GenreIds = new[] { Id }; - query.IncludeItemTypes = new[] { nameof(MusicVideo), nameof(Audio), nameof(MusicAlbum), nameof(MusicArtist) }; + query.IncludeItemTypes = new[] { BaseItemKind.MusicVideo, BaseItemKind.Audio, BaseItemKind.MusicAlbum, BaseItemKind.MusicArtist }; return LibraryManager.GetItemList(query); } diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index b1ac2fe8e..95d49508f 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -22,7 +22,6 @@ using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Globalization; @@ -40,13 +39,9 @@ namespace MediaBrowser.Controller.Entities /// </summary> public abstract class BaseItem : IHasProviderIds, IHasLookupInfo<ItemLookupInfo>, IEquatable<BaseItem> { - /// <summary> - /// The trailer folder name. - /// </summary> - public const string TrailersFolderName = "trailers"; - public const string ThemeSongsFolderName = "theme-music"; + private BaseItemKind? _baseItemKind; + public const string ThemeSongFileName = "theme"; - public const string ThemeVideosFolderName = "backdrops"; /// <summary> /// The supported image extensions. @@ -83,24 +78,7 @@ namespace MediaBrowser.Controller.Entities Model.Entities.ExtraType.Scene }; - /// <summary> - /// The supported extra folder names and types. See <see cref="Emby.Naming.Common.NamingOptions" />. - /// </summary> - public static readonly Dictionary<string, ExtraType> AllExtrasTypesFolderNames = new Dictionary<string, ExtraType>(StringComparer.OrdinalIgnoreCase) - { - ["extras"] = MediaBrowser.Model.Entities.ExtraType.Unknown, - ["behind the scenes"] = MediaBrowser.Model.Entities.ExtraType.BehindTheScenes, - ["deleted scenes"] = MediaBrowser.Model.Entities.ExtraType.DeletedScene, - ["interviews"] = MediaBrowser.Model.Entities.ExtraType.Interview, - ["scenes"] = MediaBrowser.Model.Entities.ExtraType.Scene, - ["samples"] = MediaBrowser.Model.Entities.ExtraType.Sample, - ["shorts"] = MediaBrowser.Model.Entities.ExtraType.Clip, - ["featurettes"] = MediaBrowser.Model.Entities.ExtraType.Clip - }; - private string _sortName; - private Guid[] _themeSongIds; - private Guid[] _themeVideoIds; private string _forcedSortName; @@ -121,40 +99,6 @@ namespace MediaBrowser.Controller.Entities ExtraIds = Array.Empty<Guid>(); } - [JsonIgnore] - public Guid[] ThemeSongIds - { - get - { - return _themeSongIds ??= GetExtras() - .Where(extra => extra.ExtraType == Model.Entities.ExtraType.ThemeSong) - .Select(song => song.Id) - .ToArray(); - } - - private set - { - _themeSongIds = value; - } - } - - [JsonIgnore] - public Guid[] ThemeVideoIds - { - get - { - return _themeVideoIds ??= GetExtras() - .Where(extra => extra.ExtraType == Model.Entities.ExtraType.ThemeVideo) - .Select(song => song.Id) - .ToArray(); - } - - private set - { - _themeVideoIds = value; - } - } - [JsonIgnore] public string PreferredMetadataCountryCode { get; set; } @@ -1311,96 +1255,6 @@ namespace MediaBrowser.Controller.Entities return string.Join('/', terms); } - /// <summary> - /// Loads the theme songs. - /// </summary> - /// <returns>List{Audio.Audio}.</returns> - private static Audio.Audio[] LoadThemeSongs(List<FileSystemMetadata> fileSystemChildren, IDirectoryService directoryService) - { - var files = fileSystemChildren.Where(i => i.IsDirectory) - .Where(i => string.Equals(i.Name, ThemeSongsFolderName, StringComparison.OrdinalIgnoreCase)) - .SelectMany(i => FileSystem.GetFiles(i.FullName)) - .ToList(); - - // Support plex/xbmc convention - files.AddRange(fileSystemChildren - .Where(i => !i.IsDirectory && System.IO.Path.GetFileNameWithoutExtension(i.FullName.AsSpan()).Equals(ThemeSongFileName, StringComparison.OrdinalIgnoreCase))); - - return LibraryManager.ResolvePaths(files, directoryService, null, new LibraryOptions()) - .OfType<Audio.Audio>() - .Select(audio => - { - // Try to retrieve it from the db. If we don't find it, use the resolved version - if (LibraryManager.GetItemById(audio.Id) is Audio.Audio dbItem) - { - audio = dbItem; - } - else - { - // item is new - audio.ExtraType = MediaBrowser.Model.Entities.ExtraType.ThemeSong; - } - - return audio; - - // Sort them so that the list can be easily compared for changes - }).OrderBy(i => i.Path).ToArray(); - } - - /// <summary> - /// Loads the video backdrops. - /// </summary> - /// <returns>List{Video}.</returns> - private static Video[] LoadThemeVideos(IEnumerable<FileSystemMetadata> fileSystemChildren, IDirectoryService directoryService) - { - var files = fileSystemChildren.Where(i => i.IsDirectory) - .Where(i => string.Equals(i.Name, ThemeVideosFolderName, StringComparison.OrdinalIgnoreCase)) - .SelectMany(i => FileSystem.GetFiles(i.FullName)); - - return LibraryManager.ResolvePaths(files, directoryService, null, new LibraryOptions()) - .OfType<Video>() - .Select(item => - { - // Try to retrieve it from the db. If we don't find it, use the resolved version - - if (LibraryManager.GetItemById(item.Id) is Video dbItem) - { - item = dbItem; - } - else - { - // item is new - item.ExtraType = Model.Entities.ExtraType.ThemeVideo; - } - - return item; - - // Sort them so that the list can be easily compared for changes - }).OrderBy(i => i.Path).ToArray(); - } - - protected virtual BaseItem[] LoadExtras(List<FileSystemMetadata> fileSystemChildren, IDirectoryService directoryService) - { - return fileSystemChildren - .Where(child => child.IsDirectory && AllExtrasTypesFolderNames.ContainsKey(child.Name)) - .SelectMany(folder => LibraryManager - .ResolvePaths(FileSystem.GetFiles(folder.FullName), directoryService, null, new LibraryOptions()) - .OfType<Video>() - .Select(video => - { - // Try to retrieve it from the db. If we don't find it, use the resolved version - if (LibraryManager.GetItemById(video.Id) is Video dbItem) - { - video = dbItem; - } - - video.ExtraType = AllExtrasTypesFolderNames[folder.Name]; - return video; - }) - .OrderBy(video => video.Path)) // Sort them so that the list can be easily compared for changes - .ToArray(); - } - public Task RefreshMetadata(CancellationToken cancellationToken) { return RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(FileSystem)), cancellationToken); @@ -1430,17 +1284,12 @@ namespace MediaBrowser.Controller.Entities { try { - var files = IsFileProtocol ? - GetFileSystemChildren(options.DirectoryService).ToList() : - new List<FileSystemMetadata>(); - - var ownedItemsChanged = await RefreshedOwnedItems(options, files, cancellationToken).ConfigureAwait(false); - await LibraryManager.UpdateImagesAsync(this).ConfigureAwait(false); // ensure all image properties in DB are fresh - - if (ownedItemsChanged) + if (IsFileProtocol) { - requiresSave = true; + requiresSave = await RefreshedOwnedItems(options, GetFileSystemChildren(options.DirectoryService).ToList(), cancellationToken).ConfigureAwait(false); } + + await LibraryManager.UpdateImagesAsync(this).ConfigureAwait(false); // ensure all image properties in DB are fresh } catch (Exception ex) { @@ -1516,35 +1365,12 @@ namespace MediaBrowser.Controller.Entities /// <returns><c>true</c> if any items have changed, else <c>false</c>.</returns> protected virtual async Task<bool> RefreshedOwnedItems(MetadataRefreshOptions options, List<FileSystemMetadata> fileSystemChildren, CancellationToken cancellationToken) { - var themeSongsChanged = false; - - var themeVideosChanged = false; - - var extrasChanged = false; - - var localTrailersChanged = false; - - if (IsFileProtocol && SupportsOwnedItems) + if (!IsFileProtocol || !SupportsOwnedItems || IsInMixedFolder || this is ICollectionFolder or UserRootFolder or AggregateFolder || this.GetType() == typeof(Folder)) { - if (SupportsThemeMedia) - { - if (!IsInMixedFolder) - { - themeSongsChanged = await RefreshThemeSongs(this, options, fileSystemChildren, cancellationToken).ConfigureAwait(false); - - themeVideosChanged = await RefreshThemeVideos(this, options, fileSystemChildren, cancellationToken).ConfigureAwait(false); - - extrasChanged = await RefreshExtras(this, options, fileSystemChildren, cancellationToken).ConfigureAwait(false); - } - } - - if (this is IHasTrailers hasTrailers) - { - localTrailersChanged = await RefreshLocalTrailers(hasTrailers, options, fileSystemChildren, cancellationToken).ConfigureAwait(false); - } + return false; } - return themeSongsChanged || themeVideosChanged || extrasChanged || localTrailersChanged; + return await RefreshExtras(this, options, fileSystemChildren, cancellationToken).ConfigureAwait(false); } protected virtual FileSystemMetadata[] GetFileSystemChildren(IDirectoryService directoryService) @@ -1554,98 +1380,24 @@ namespace MediaBrowser.Controller.Entities return directoryService.GetFileSystemEntries(path); } - private async Task<bool> RefreshLocalTrailers(IHasTrailers item, MetadataRefreshOptions options, List<FileSystemMetadata> fileSystemChildren, CancellationToken cancellationToken) - { - var newItems = LibraryManager.FindTrailers(this, fileSystemChildren, options.DirectoryService); - - var newItemIds = newItems.Select(i => i.Id); - - var itemsChanged = !item.LocalTrailerIds.SequenceEqual(newItemIds); - var ownerId = item.Id; - - var tasks = newItems.Select(i => - { - var subOptions = new MetadataRefreshOptions(options); - - if (i.ExtraType != Model.Entities.ExtraType.Trailer || - i.OwnerId != ownerId || - !i.ParentId.Equals(Guid.Empty)) - { - i.ExtraType = Model.Entities.ExtraType.Trailer; - i.OwnerId = ownerId; - i.ParentId = Guid.Empty; - subOptions.ForceSave = true; - } - - return RefreshMetadataForOwnedItem(i, true, subOptions, cancellationToken); - }); - - await Task.WhenAll(tasks).ConfigureAwait(false); - - item.LocalTrailerIds = newItemIds.ToArray(); - - return itemsChanged; - } - private async Task<bool> RefreshExtras(BaseItem item, MetadataRefreshOptions options, List<FileSystemMetadata> fileSystemChildren, CancellationToken cancellationToken) { - var extras = LoadExtras(fileSystemChildren, options.DirectoryService); - var themeVideos = LoadThemeVideos(fileSystemChildren, options.DirectoryService); - var themeSongs = LoadThemeSongs(fileSystemChildren, options.DirectoryService); - var newExtras = new BaseItem[extras.Length + themeVideos.Length + themeSongs.Length]; - extras.CopyTo(newExtras, 0); - themeVideos.CopyTo(newExtras, extras.Length); - themeSongs.CopyTo(newExtras, extras.Length + themeVideos.Length); - - var newExtraIds = newExtras.Select(i => i.Id).ToArray(); - + var extras = LibraryManager.FindExtras(item, fileSystemChildren, options.DirectoryService).ToArray(); + var newExtraIds = extras.Select(i => i.Id).ToArray(); var extrasChanged = !item.ExtraIds.SequenceEqual(newExtraIds); - if (extrasChanged) + if (!extrasChanged && !options.ReplaceAllMetadata && options.MetadataRefreshMode != MetadataRefreshMode.FullRefresh) { - var ownerId = item.Id; - - var tasks = newExtras.Select(i => - { - var subOptions = new MetadataRefreshOptions(options); - if (i.OwnerId != ownerId || i.ParentId != Guid.Empty) - { - i.OwnerId = ownerId; - i.ParentId = Guid.Empty; - subOptions.ForceSave = true; - } - - return RefreshMetadataForOwnedItem(i, true, subOptions, cancellationToken); - }); - - await Task.WhenAll(tasks).ConfigureAwait(false); - - item.ExtraIds = newExtraIds; + return false; } - return extrasChanged; - } - - private async Task<bool> RefreshThemeVideos(BaseItem item, MetadataRefreshOptions options, IEnumerable<FileSystemMetadata> fileSystemChildren, CancellationToken cancellationToken) - { - var newThemeVideos = LoadThemeVideos(fileSystemChildren, options.DirectoryService); - - var newThemeVideoIds = newThemeVideos.Select(i => i.Id).ToArray(); - - var themeVideosChanged = !item.ThemeVideoIds.SequenceEqual(newThemeVideoIds); - var ownerId = item.Id; - var tasks = newThemeVideos.Select(i => + var tasks = extras.Select(i => { var subOptions = new MetadataRefreshOptions(options); - - if (!i.ExtraType.HasValue || - i.ExtraType.Value != Model.Entities.ExtraType.ThemeVideo || - i.OwnerId != ownerId || - !i.ParentId.Equals(Guid.Empty)) + if (i.OwnerId != ownerId || i.ParentId != Guid.Empty) { - i.ExtraType = Model.Entities.ExtraType.ThemeVideo; i.OwnerId = ownerId; i.ParentId = Guid.Empty; subOptions.ForceSave = true; @@ -1656,48 +1408,9 @@ namespace MediaBrowser.Controller.Entities await Task.WhenAll(tasks).ConfigureAwait(false); - // They are expected to be sorted by SortName - item.ThemeVideoIds = newThemeVideos.OrderBy(i => i.SortName).Select(i => i.Id).ToArray(); + item.ExtraIds = newExtraIds; - return themeVideosChanged; - } - - /// <summary> - /// Refreshes the theme songs. - /// </summary> - private async Task<bool> RefreshThemeSongs(BaseItem item, MetadataRefreshOptions options, List<FileSystemMetadata> fileSystemChildren, CancellationToken cancellationToken) - { - var newThemeSongs = LoadThemeSongs(fileSystemChildren, options.DirectoryService); - var newThemeSongIds = newThemeSongs.Select(i => i.Id).ToArray(); - - var themeSongsChanged = !item.ThemeSongIds.SequenceEqual(newThemeSongIds); - - var ownerId = item.Id; - - var tasks = newThemeSongs.Select(i => - { - var subOptions = new MetadataRefreshOptions(options); - - if (!i.ExtraType.HasValue || - i.ExtraType.Value != Model.Entities.ExtraType.ThemeSong || - i.OwnerId != ownerId || - !i.ParentId.Equals(Guid.Empty)) - { - i.ExtraType = Model.Entities.ExtraType.ThemeSong; - i.OwnerId = ownerId; - i.ParentId = Guid.Empty; - subOptions.ForceSave = true; - } - - return RefreshMetadataForOwnedItem(i, true, subOptions, cancellationToken); - }); - - await Task.WhenAll(tasks).ConfigureAwait(false); - - // They are expected to be sorted by SortName - item.ThemeSongIds = newThemeSongs.OrderBy(i => i.SortName).Select(i => i.Id).ToArray(); - - return themeSongsChanged; + return true; } public string GetPresentationUniqueKey() @@ -1930,7 +1643,7 @@ namespace MediaBrowser.Controller.Entities private bool IsVisibleViaTags(User user) { - if (user.GetPreference(PreferenceKind.BlockedTags).Any(i => Tags.Contains(i, StringComparer.OrdinalIgnoreCase))) + if (user.GetPreference(PreferenceKind.BlockedTags).Any(i => Tags.Contains(i, StringComparison.OrdinalIgnoreCase))) { return false; } @@ -2009,7 +1722,7 @@ namespace MediaBrowser.Controller.Entities public BaseItemKind GetBaseItemKind() { - return Enum.Parse<BaseItemKind>(GetClientTypeName()); + return _baseItemKind ??= Enum.Parse<BaseItemKind>(GetClientTypeName()); } /// <summary> @@ -2099,7 +1812,7 @@ namespace MediaBrowser.Controller.Entities var current = Studios; - if (!current.Contains(name, StringComparer.OrdinalIgnoreCase)) + if (!current.Contains(name, StringComparison.OrdinalIgnoreCase)) { int curLen = current.Length; if (curLen == 0) @@ -2134,7 +1847,7 @@ namespace MediaBrowser.Controller.Entities } var genres = Genres; - if (!genres.Contains(name, StringComparer.OrdinalIgnoreCase)) + if (!genres.Contains(name, StringComparison.OrdinalIgnoreCase)) { var list = genres.ToList(); list.Add(name); @@ -2340,7 +2053,7 @@ namespace MediaBrowser.Controller.Entities .ToList(); var deletedImages = ImageInfos - .Where(image => image.IsLocalFile && !allFiles.Contains(image.Path, StringComparer.OrdinalIgnoreCase)) + .Where(image => image.IsLocalFile && !allFiles.Contains(image.Path, StringComparison.OrdinalIgnoreCase)) .ToList(); if (deletedImages.Count > 0) @@ -2891,14 +2604,14 @@ namespace MediaBrowser.Controller.Entities StringComparison.OrdinalIgnoreCase); } - public IEnumerable<BaseItem> GetThemeSongs() + public IReadOnlyList<BaseItem> GetThemeSongs() { - return ThemeSongIds.Select(LibraryManager.GetItemById); + return GetExtras().Where(e => e.ExtraType == Model.Entities.ExtraType.ThemeSong).ToArray(); } - public IEnumerable<BaseItem> GetThemeVideos() + public IReadOnlyList<BaseItem> GetThemeVideos() { - return ThemeVideoIds.Select(LibraryManager.GetItemById); + return GetExtras().Where(e => e.ExtraType == Model.Entities.ExtraType.ThemeVideo).ToArray(); } /// <summary> @@ -2938,7 +2651,7 @@ namespace MediaBrowser.Controller.Entities } /// <inheritdoc /> - public bool Equals(BaseItem other) => Equals(Id, other?.Id); + public bool Equals(BaseItem other) => Id == other?.Id; /// <inheritdoc /> public override int GetHashCode() => HashCode.Combine(Id); diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs index ec1ebaabe..55551e70e 100644 --- a/MediaBrowser.Controller/Entities/Folder.cs +++ b/MediaBrowser.Controller/Entities/Folder.cs @@ -792,7 +792,7 @@ namespace MediaBrowser.Controller.Entities private bool RequiresPostFiltering2(InternalItemsQuery query) { - if (query.IncludeItemTypes.Length == 1 && string.Equals(query.IncludeItemTypes[0], nameof(BoxSet), StringComparison.OrdinalIgnoreCase)) + if (query.IncludeItemTypes.Length == 1 && query.IncludeItemTypes[0] == BaseItemKind.BoxSet) { Logger.LogDebug("Query requires post-filtering due to BoxSet query"); return true; @@ -882,7 +882,7 @@ namespace MediaBrowser.Controller.Entities if (query.IsPlayed.HasValue) { - if (query.IncludeItemTypes.Length == 1 && query.IncludeItemTypes.Contains(nameof(Series))) + if (query.IncludeItemTypes.Length == 1 && query.IncludeItemTypes.Contains(BaseItemKind.Series)) { Logger.LogDebug("Query requires post-filtering due to IsPlayed"); return true; @@ -1101,7 +1101,7 @@ namespace MediaBrowser.Controller.Entities return false; } - if (query.IncludeItemTypes.Length == 0 || query.IncludeItemTypes.Contains("Movie", StringComparer.OrdinalIgnoreCase)) + if (query.IncludeItemTypes.Length == 0 || query.IncludeItemTypes.Contains(BaseItemKind.Movie)) { param = true; } diff --git a/MediaBrowser.Controller/Entities/Genre.cs b/MediaBrowser.Controller/Entities/Genre.cs index 338f96204..4be673237 100644 --- a/MediaBrowser.Controller/Entities/Genre.cs +++ b/MediaBrowser.Controller/Entities/Genre.cs @@ -6,7 +6,7 @@ using System; using System.Collections.Generic; using System.Text.Json.Serialization; using Diacritics.Extensions; -using MediaBrowser.Controller.Entities.Audio; +using Jellyfin.Data.Enums; using Microsoft.Extensions.Logging; namespace MediaBrowser.Controller.Entities @@ -66,10 +66,10 @@ namespace MediaBrowser.Controller.Entities query.GenreIds = new[] { Id }; query.ExcludeItemTypes = new[] { - nameof(MusicVideo), - nameof(Entities.Audio.Audio), - nameof(MusicAlbum), - nameof(MusicArtist) + BaseItemKind.MusicVideo, + BaseItemKind.Audio, + BaseItemKind.MusicAlbum, + BaseItemKind.MusicArtist }; return LibraryManager.GetItemList(query); diff --git a/MediaBrowser.Controller/Entities/IHasSpecialFeatures.cs b/MediaBrowser.Controller/Entities/IHasSpecialFeatures.cs index f317a02ff..f47d2162f 100644 --- a/MediaBrowser.Controller/Entities/IHasSpecialFeatures.cs +++ b/MediaBrowser.Controller/Entities/IHasSpecialFeatures.cs @@ -10,9 +10,9 @@ namespace MediaBrowser.Controller.Entities public interface IHasSpecialFeatures { /// <summary> - /// Gets or sets the special feature ids. + /// Gets the special feature ids. /// </summary> /// <value>The special feature ids.</value> - IReadOnlyList<Guid> SpecialFeatureIds { get; set; } + IReadOnlyList<Guid> SpecialFeatureIds { get; } } } diff --git a/MediaBrowser.Controller/Entities/IHasTrailers.cs b/MediaBrowser.Controller/Entities/IHasTrailers.cs index f4271678d..bb4a6ea94 100644 --- a/MediaBrowser.Controller/Entities/IHasTrailers.cs +++ b/MediaBrowser.Controller/Entities/IHasTrailers.cs @@ -2,7 +2,6 @@ #pragma warning disable CS1591 -using System; using System.Collections.Generic; using MediaBrowser.Model.Entities; @@ -17,18 +16,10 @@ namespace MediaBrowser.Controller.Entities IReadOnlyList<MediaUrl> RemoteTrailers { get; set; } /// <summary> - /// Gets or sets the local trailer ids. + /// Gets the local trailers. /// </summary> - /// <value>The local trailer ids.</value> - IReadOnlyList<Guid> LocalTrailerIds { get; set; } - - /// <summary> - /// Gets or sets the remote trailer ids. - /// </summary> - /// <value>The remote trailer ids.</value> - IReadOnlyList<Guid> RemoteTrailerIds { get; set; } - - Guid Id { get; set; } + /// <value>The local trailers.</value> + IReadOnlyList<BaseItem> LocalTrailers { get; } } /// <summary> @@ -42,57 +33,6 @@ namespace MediaBrowser.Controller.Entities /// <param name="item">Media item.</param> /// <returns><see cref="IReadOnlyList{Guid}" />.</returns> public static int GetTrailerCount(this IHasTrailers item) - => item.LocalTrailerIds.Count + item.RemoteTrailerIds.Count; - - /// <summary> - /// Gets the trailer ids. - /// </summary> - /// <param name="item">Media item.</param> - /// <returns><see cref="IReadOnlyList{Guid}" />.</returns> - public static IReadOnlyList<Guid> GetTrailerIds(this IHasTrailers item) - { - var localIds = item.LocalTrailerIds; - var remoteIds = item.RemoteTrailerIds; - - var all = new Guid[localIds.Count + remoteIds.Count]; - var index = 0; - foreach (var id in localIds) - { - all[index++] = id; - } - - foreach (var id in remoteIds) - { - all[index++] = id; - } - - return all; - } - - /// <summary> - /// Gets the trailers. - /// </summary> - /// <param name="item">Media item.</param> - /// <returns><see cref="IReadOnlyList{BaseItem}" />.</returns> - public static IReadOnlyList<BaseItem> GetTrailers(this IHasTrailers item) - { - var localIds = item.LocalTrailerIds; - var remoteIds = item.RemoteTrailerIds; - var libraryManager = BaseItem.LibraryManager; - - var all = new BaseItem[localIds.Count + remoteIds.Count]; - var index = 0; - foreach (var id in localIds) - { - all[index++] = libraryManager.GetItemById(id); - } - - foreach (var id in remoteIds) - { - all[index++] = libraryManager.GetItemById(id); - } - - return all; - } + => item.LocalTrailers.Count + item.RemoteTrailers.Count; } } diff --git a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs index 0baa7725e..f06b5c787 100644 --- a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs +++ b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs @@ -27,13 +27,13 @@ namespace MediaBrowser.Controller.Entities ExcludeArtistIds = Array.Empty<Guid>(); ExcludeInheritedTags = Array.Empty<string>(); ExcludeItemIds = Array.Empty<Guid>(); - ExcludeItemTypes = Array.Empty<string>(); + ExcludeItemTypes = Array.Empty<BaseItemKind>(); ExcludeTags = Array.Empty<string>(); GenreIds = Array.Empty<Guid>(); Genres = Array.Empty<string>(); GroupByPresentationUniqueKey = true; ImageTypes = Array.Empty<ImageType>(); - IncludeItemTypes = Array.Empty<string>(); + IncludeItemTypes = Array.Empty<BaseItemKind>(); ItemIds = Array.Empty<Guid>(); MediaTypes = Array.Empty<string>(); MinSimilarityScore = 20; @@ -87,9 +87,9 @@ namespace MediaBrowser.Controller.Entities public string[] MediaTypes { get; set; } - public string[] IncludeItemTypes { get; set; } + public BaseItemKind[] IncludeItemTypes { get; set; } - public string[] ExcludeItemTypes { get; set; } + public BaseItemKind[] ExcludeItemTypes { get; set; } public string[] ExcludeTags { get; set; } @@ -229,7 +229,7 @@ namespace MediaBrowser.Controller.Entities public Guid ParentId { get; set; } - public string? ParentType { get; set; } + public BaseItemKind? ParentType { get; set; } public Guid[] AncestorIds { get; set; } @@ -314,7 +314,7 @@ namespace MediaBrowser.Controller.Entities else { ParentId = value.Id; - ParentType = value.GetType().Name; + ParentType = value.GetBaseItemKind(); } } } diff --git a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs index e46f99cd5..6b93d8d87 100644 --- a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs +++ b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs @@ -9,7 +9,6 @@ using System.Text.Json.Serialization; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Entities; using MediaBrowser.Model.Querying; namespace MediaBrowser.Controller.Entities.Movies @@ -21,10 +20,6 @@ namespace MediaBrowser.Controller.Entities.Movies { public BoxSet() { - RemoteTrailers = Array.Empty<MediaUrl>(); - LocalTrailerIds = Array.Empty<Guid>(); - RemoteTrailerIds = Array.Empty<Guid>(); - DisplayOrder = ItemSortBy.PremiereDate; } @@ -38,10 +33,9 @@ namespace MediaBrowser.Controller.Entities.Movies public override bool SupportsPeople => true; /// <inheritdoc /> - public IReadOnlyList<Guid> LocalTrailerIds { get; set; } - - /// <inheritdoc /> - public IReadOnlyList<Guid> RemoteTrailerIds { get; set; } + public IReadOnlyList<BaseItem> LocalTrailers => GetExtras() + .Where(extra => extra.ExtraType == Model.Entities.ExtraType.Trailer) + .ToArray(); /// <summary> /// Gets or sets the display order. diff --git a/MediaBrowser.Controller/Entities/Movies/Movie.cs b/MediaBrowser.Controller/Entities/Movies/Movie.cs index b54bbf5eb..dfaf03fda 100644 --- a/MediaBrowser.Controller/Entities/Movies/Movie.cs +++ b/MediaBrowser.Controller/Entities/Movies/Movie.cs @@ -7,12 +7,9 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Text.Json.Serialization; -using System.Threading; -using System.Threading.Tasks; using Jellyfin.Data.Enums; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; -using MediaBrowser.Model.IO; using MediaBrowser.Model.Providers; namespace MediaBrowser.Controller.Entities.Movies @@ -22,22 +19,16 @@ namespace MediaBrowser.Controller.Entities.Movies /// </summary> public class Movie : Video, IHasSpecialFeatures, IHasTrailers, IHasLookupInfo<MovieInfo>, ISupportsBoxSetGrouping { - public Movie() - { - SpecialFeatureIds = Array.Empty<Guid>(); - RemoteTrailers = Array.Empty<MediaUrl>(); - LocalTrailerIds = Array.Empty<Guid>(); - RemoteTrailerIds = Array.Empty<Guid>(); - } + /// <inheritdoc /> + public IReadOnlyList<Guid> SpecialFeatureIds => GetExtras() + .Where(extra => extra.ExtraType != null && extra is Video) + .Select(extra => extra.Id) + .ToArray(); /// <inheritdoc /> - public IReadOnlyList<Guid> SpecialFeatureIds { get; set; } - - /// <inheritdoc /> - public IReadOnlyList<Guid> LocalTrailerIds { get; set; } - - /// <inheritdoc /> - public IReadOnlyList<Guid> RemoteTrailerIds { get; set; } + public IReadOnlyList<BaseItem> LocalTrailers => GetExtras() + .Where(extra => extra.ExtraType == Model.Entities.ExtraType.Trailer) + .ToArray(); /// <summary> /// Gets or sets the name of the TMDB collection. @@ -66,54 +57,6 @@ namespace MediaBrowser.Controller.Entities.Movies return 2.0 / 3; } - protected override async Task<bool> RefreshedOwnedItems(MetadataRefreshOptions options, List<FileSystemMetadata> fileSystemChildren, CancellationToken cancellationToken) - { - var hasChanges = await base.RefreshedOwnedItems(options, fileSystemChildren, cancellationToken).ConfigureAwait(false); - - // Must have a parent to have special features - // In other words, it must be part of the Parent/Child tree - if (IsFileProtocol && SupportsOwnedItems && !IsInMixedFolder) - { - var specialFeaturesChanged = await RefreshSpecialFeatures(options, fileSystemChildren, cancellationToken).ConfigureAwait(false); - - if (specialFeaturesChanged) - { - hasChanges = true; - } - } - - return hasChanges; - } - - private async Task<bool> RefreshSpecialFeatures(MetadataRefreshOptions options, List<FileSystemMetadata> fileSystemChildren, CancellationToken cancellationToken) - { - var newItems = LibraryManager.FindExtras(this, fileSystemChildren, options.DirectoryService).ToList(); - var newItemIds = newItems.Select(i => i.Id).ToArray(); - - var itemsChanged = !SpecialFeatureIds.SequenceEqual(newItemIds); - - var ownerId = Id; - - var tasks = newItems.Select(i => - { - var subOptions = new MetadataRefreshOptions(options); - - if (i.OwnerId != ownerId) - { - i.OwnerId = ownerId; - subOptions.ForceSave = true; - } - - return RefreshMetadataForOwnedItem(i, false, subOptions, cancellationToken); - }); - - await Task.WhenAll(tasks).ConfigureAwait(false); - - SpecialFeatureIds = newItemIds; - - return itemsChanged; - } - /// <inheritdoc /> public override UnratedItem GetBlockUnratedType() { diff --git a/MediaBrowser.Controller/Entities/TV/Episode.cs b/MediaBrowser.Controller/Entities/TV/Episode.cs index 27c3ff81b..dcc752f8c 100644 --- a/MediaBrowser.Controller/Entities/TV/Episode.cs +++ b/MediaBrowser.Controller/Entities/TV/Episode.cs @@ -20,18 +20,10 @@ namespace MediaBrowser.Controller.Entities.TV /// </summary> public class Episode : Video, IHasTrailers, IHasLookupInfo<EpisodeInfo>, IHasSeries { - public Episode() - { - RemoteTrailers = Array.Empty<MediaUrl>(); - LocalTrailerIds = Array.Empty<Guid>(); - RemoteTrailerIds = Array.Empty<Guid>(); - } - /// <inheritdoc /> - public IReadOnlyList<Guid> LocalTrailerIds { get; set; } - - /// <inheritdoc /> - public IReadOnlyList<Guid> RemoteTrailerIds { get; set; } + public IReadOnlyList<BaseItem> LocalTrailers => GetExtras() + .Where(extra => extra.ExtraType == Model.Entities.ExtraType.Trailer) + .ToArray(); /// <summary> /// Gets or sets the season in which it aired. diff --git a/MediaBrowser.Controller/Entities/TV/Series.cs b/MediaBrowser.Controller/Entities/TV/Series.cs index e4933e968..bdadc2775 100644 --- a/MediaBrowser.Controller/Entities/TV/Series.cs +++ b/MediaBrowser.Controller/Entities/TV/Series.cs @@ -27,9 +27,6 @@ namespace MediaBrowser.Controller.Entities.TV { public Series() { - RemoteTrailers = Array.Empty<MediaUrl>(); - LocalTrailerIds = Array.Empty<Guid>(); - RemoteTrailerIds = Array.Empty<Guid>(); AirDays = Array.Empty<DayOfWeek>(); } @@ -53,10 +50,9 @@ namespace MediaBrowser.Controller.Entities.TV public override bool SupportsPeople => true; /// <inheritdoc /> - public IReadOnlyList<Guid> LocalTrailerIds { get; set; } - - /// <inheritdoc /> - public IReadOnlyList<Guid> RemoteTrailerIds { get; set; } + public IReadOnlyList<BaseItem> LocalTrailers => GetExtras() + .Where(extra => extra.ExtraType == Model.Entities.ExtraType.Trailer) + .ToArray(); /// <summary> /// Gets or sets the display order. @@ -131,7 +127,7 @@ namespace MediaBrowser.Controller.Entities.TV { AncestorWithPresentationUniqueKey = null, SeriesPresentationUniqueKey = seriesKey, - IncludeItemTypes = new[] { nameof(Season) }, + IncludeItemTypes = new[] { BaseItemKind.Season }, IsVirtualItem = false, Limit = 0, DtoOptions = new DtoOptions(false) @@ -159,7 +155,7 @@ namespace MediaBrowser.Controller.Entities.TV if (query.IncludeItemTypes.Length == 0) { - query.IncludeItemTypes = new[] { nameof(Episode) }; + query.IncludeItemTypes = new[] { BaseItemKind.Episode }; } query.IsVirtualItem = false; @@ -213,7 +209,7 @@ namespace MediaBrowser.Controller.Entities.TV query.AncestorWithPresentationUniqueKey = null; query.SeriesPresentationUniqueKey = seriesKey; - query.IncludeItemTypes = new[] { nameof(Season) }; + query.IncludeItemTypes = new[] { BaseItemKind.Season }; query.OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) }; if (user != null && !user.DisplayMissingEpisodes) @@ -239,7 +235,7 @@ namespace MediaBrowser.Controller.Entities.TV if (query.IncludeItemTypes.Length == 0) { - query.IncludeItemTypes = new[] { nameof(Episode), nameof(Season) }; + query.IncludeItemTypes = new[] { BaseItemKind.Episode, BaseItemKind.Season }; } query.IsVirtualItem = false; @@ -259,7 +255,7 @@ namespace MediaBrowser.Controller.Entities.TV { AncestorWithPresentationUniqueKey = null, SeriesPresentationUniqueKey = seriesKey, - IncludeItemTypes = new[] { nameof(Episode), nameof(Season) }, + IncludeItemTypes = new[] { BaseItemKind.Episode, BaseItemKind.Season }, OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) }, DtoOptions = options }; @@ -363,7 +359,7 @@ namespace MediaBrowser.Controller.Entities.TV { AncestorWithPresentationUniqueKey = queryFromSeries ? null : seriesKey, SeriesPresentationUniqueKey = queryFromSeries ? seriesKey : null, - IncludeItemTypes = new[] { nameof(Episode) }, + IncludeItemTypes = new[] { BaseItemKind.Episode }, OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) }, DtoOptions = options }; diff --git a/MediaBrowser.Controller/Entities/TagExtensions.cs b/MediaBrowser.Controller/Entities/TagExtensions.cs index 2ce396daf..ec3eb0f70 100644 --- a/MediaBrowser.Controller/Entities/TagExtensions.cs +++ b/MediaBrowser.Controller/Entities/TagExtensions.cs @@ -2,6 +2,7 @@ using System; using System.Linq; +using Jellyfin.Extensions; namespace MediaBrowser.Controller.Entities { @@ -16,7 +17,7 @@ namespace MediaBrowser.Controller.Entities var current = item.Tags; - if (!current.Contains(name, StringComparer.OrdinalIgnoreCase)) + if (!current.Contains(name, StringComparison.OrdinalIgnoreCase)) { if (current.Length == 0) { diff --git a/MediaBrowser.Controller/Entities/UserView.cs b/MediaBrowser.Controller/Entities/UserView.cs index a6f107849..5c9be7337 100644 --- a/MediaBrowser.Controller/Entities/UserView.cs +++ b/MediaBrowser.Controller/Entities/UserView.cs @@ -8,6 +8,7 @@ using System.Linq; using System.Text.Json.Serialization; using System.Threading.Tasks; using Jellyfin.Data.Entities; +using Jellyfin.Extensions; using MediaBrowser.Controller.TV; using MediaBrowser.Model.Querying; @@ -170,12 +171,12 @@ namespace MediaBrowser.Controller.Entities public static bool IsEligibleForGrouping(string viewType) { - return _viewTypesEligibleForGrouping.Contains(viewType ?? string.Empty, StringComparer.OrdinalIgnoreCase); + return _viewTypesEligibleForGrouping.Contains(viewType ?? string.Empty, StringComparison.OrdinalIgnoreCase); } public static bool EnableOriginalFolder(string viewType) { - return _originalFolderViewTypes.Contains(viewType ?? string.Empty, StringComparer.OrdinalIgnoreCase); + return _originalFolderViewTypes.Contains(viewType ?? string.Empty, StringComparison.OrdinalIgnoreCase); } protected override Task ValidateChildrenInternal(IProgress<double> progress, bool recursive, bool refreshChildMetadata, Providers.MetadataRefreshOptions refreshOptions, Providers.IDirectoryService directoryService, System.Threading.CancellationToken cancellationToken) diff --git a/MediaBrowser.Controller/Entities/UserViewBuilder.cs b/MediaBrowser.Controller/Entities/UserViewBuilder.cs index 1cff72037..fe44f1169 100644 --- a/MediaBrowser.Controller/Entities/UserViewBuilder.cs +++ b/MediaBrowser.Controller/Entities/UserViewBuilder.cs @@ -8,7 +8,7 @@ using System.Globalization; using System.Linq; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; -using MediaBrowser.Controller.Entities.Movies; +using Jellyfin.Extensions; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.TV; using MediaBrowser.Model.Entities; @@ -16,8 +16,6 @@ using MediaBrowser.Model.Querying; using Microsoft.Extensions.Logging; using Episode = MediaBrowser.Controller.Entities.TV.Episode; using MetadataProvider = MediaBrowser.Model.Entities.MetadataProvider; -using Movie = MediaBrowser.Controller.Entities.Movies.Movie; -using Season = MediaBrowser.Controller.Entities.TV.Season; using Series = MediaBrowser.Controller.Entities.TV.Series; namespace MediaBrowser.Controller.Entities @@ -140,7 +138,7 @@ namespace MediaBrowser.Controller.Entities if (query.IncludeItemTypes.Length == 0) { - query.IncludeItemTypes = new[] { nameof(Movie) }; + query.IncludeItemTypes = new[] { BaseItemKind.Movie }; } return parent.QueryRecursive(query); @@ -165,7 +163,7 @@ namespace MediaBrowser.Controller.Entities query.Parent = parent; query.SetUser(user); query.IsFavorite = true; - query.IncludeItemTypes = new[] { nameof(Movie) }; + query.IncludeItemTypes = new[] { BaseItemKind.Movie }; return _libraryManager.GetItemsResult(query); } @@ -176,7 +174,7 @@ namespace MediaBrowser.Controller.Entities query.Parent = parent; query.SetUser(user); query.IsFavorite = true; - query.IncludeItemTypes = new[] { nameof(Series) }; + query.IncludeItemTypes = new[] { BaseItemKind.Series }; return _libraryManager.GetItemsResult(query); } @@ -187,7 +185,7 @@ namespace MediaBrowser.Controller.Entities query.Parent = parent; query.SetUser(user); query.IsFavorite = true; - query.IncludeItemTypes = new[] { nameof(Episode) }; + query.IncludeItemTypes = new[] { BaseItemKind.Episode }; return _libraryManager.GetItemsResult(query); } @@ -198,7 +196,7 @@ namespace MediaBrowser.Controller.Entities query.Parent = parent; query.SetUser(user); - query.IncludeItemTypes = new[] { nameof(Movie) }; + query.IncludeItemTypes = new[] { BaseItemKind.Movie }; return _libraryManager.GetItemsResult(query); } @@ -206,7 +204,7 @@ namespace MediaBrowser.Controller.Entities private QueryResult<BaseItem> GetMovieCollections(User user, InternalItemsQuery query) { query.Parent = null; - query.IncludeItemTypes = new[] { nameof(BoxSet) }; + query.IncludeItemTypes = new[] { BaseItemKind.BoxSet }; query.SetUser(user); query.Recursive = true; @@ -220,7 +218,7 @@ namespace MediaBrowser.Controller.Entities query.Parent = parent; query.SetUser(user); query.Limit = GetSpecialItemsLimit(); - query.IncludeItemTypes = new[] { nameof(Movie) }; + query.IncludeItemTypes = new[] { BaseItemKind.Movie }; return ConvertToResult(_libraryManager.GetItemList(query)); } @@ -233,7 +231,7 @@ namespace MediaBrowser.Controller.Entities query.Parent = parent; query.SetUser(user); query.Limit = GetSpecialItemsLimit(); - query.IncludeItemTypes = new[] { nameof(Movie) }; + query.IncludeItemTypes = new[] { BaseItemKind.Movie }; return ConvertToResult(_libraryManager.GetItemList(query)); } @@ -252,7 +250,7 @@ namespace MediaBrowser.Controller.Entities { var genres = parent.QueryRecursive(new InternalItemsQuery(user) { - IncludeItemTypes = new[] { nameof(Movie) }, + IncludeItemTypes = new[] { BaseItemKind.Movie }, Recursive = true, EnableTotalRecordCount = false }).Items @@ -283,7 +281,7 @@ namespace MediaBrowser.Controller.Entities query.GenreIds = new[] { displayParent.Id }; query.SetUser(user); - query.IncludeItemTypes = new[] { nameof(Movie) }; + query.IncludeItemTypes = new[] { BaseItemKind.Movie }; return _libraryManager.GetItemsResult(query); } @@ -299,9 +297,9 @@ namespace MediaBrowser.Controller.Entities { query.IncludeItemTypes = new[] { - nameof(Series), - nameof(Season), - nameof(Episode) + BaseItemKind.Series, + BaseItemKind.Season, + BaseItemKind.Episode }; } @@ -329,7 +327,7 @@ namespace MediaBrowser.Controller.Entities query.Parent = parent; query.SetUser(user); query.Limit = GetSpecialItemsLimit(); - query.IncludeItemTypes = new[] { nameof(Episode) }; + query.IncludeItemTypes = new[] { BaseItemKind.Episode }; query.IsVirtualItem = false; return ConvertToResult(_libraryManager.GetItemList(query)); @@ -360,7 +358,7 @@ namespace MediaBrowser.Controller.Entities query.Parent = parent; query.SetUser(user); query.Limit = GetSpecialItemsLimit(); - query.IncludeItemTypes = new[] { nameof(Episode) }; + query.IncludeItemTypes = new[] { BaseItemKind.Episode }; return ConvertToResult(_libraryManager.GetItemList(query)); } @@ -371,7 +369,7 @@ namespace MediaBrowser.Controller.Entities query.Parent = parent; query.SetUser(user); - query.IncludeItemTypes = new[] { nameof(Series) }; + query.IncludeItemTypes = new[] { BaseItemKind.Series }; return _libraryManager.GetItemsResult(query); } @@ -380,7 +378,7 @@ namespace MediaBrowser.Controller.Entities { var genres = parent.QueryRecursive(new InternalItemsQuery(user) { - IncludeItemTypes = new[] { nameof(Series) }, + IncludeItemTypes = new[] { BaseItemKind.Series }, Recursive = true, EnableTotalRecordCount = false }).Items @@ -411,7 +409,7 @@ namespace MediaBrowser.Controller.Entities query.GenreIds = new[] { displayParent.Id }; query.SetUser(user); - query.IncludeItemTypes = new[] { nameof(Series) }; + query.IncludeItemTypes = new[] { BaseItemKind.Series }; return _libraryManager.GetItemsResult(query); } @@ -494,17 +492,17 @@ namespace MediaBrowser.Controller.Entities public static bool Filter(BaseItem item, User user, InternalItemsQuery query, IUserDataManager userDataManager, ILibraryManager libraryManager) { - if (query.MediaTypes.Length > 0 && !query.MediaTypes.Contains(item.MediaType ?? string.Empty, StringComparer.OrdinalIgnoreCase)) + if (query.MediaTypes.Length > 0 && !query.MediaTypes.Contains(item.MediaType ?? string.Empty, StringComparison.OrdinalIgnoreCase)) { return false; } - if (query.IncludeItemTypes.Length > 0 && !query.IncludeItemTypes.Contains(item.GetClientTypeName(), StringComparer.OrdinalIgnoreCase)) + if (query.IncludeItemTypes.Length > 0 && !query.IncludeItemTypes.Contains(item.GetBaseItemKind())) { return false; } - if (query.ExcludeItemTypes.Length > 0 && query.ExcludeItemTypes.Contains(item.GetClientTypeName(), StringComparer.OrdinalIgnoreCase)) + if (query.ExcludeItemTypes.Length > 0 && query.ExcludeItemTypes.Contains(item.GetBaseItemKind())) { return false; } @@ -745,10 +743,9 @@ namespace MediaBrowser.Controller.Entities var val = query.HasTrailer.Value; var trailerCount = 0; - var hasTrailers = item as IHasTrailers; - if (hasTrailers != null) + if (item is IHasTrailers hasTrailers) { - trailerCount = hasTrailers.GetTrailerIds().Count; + trailerCount = hasTrailers.GetTrailerCount(); } var ok = val ? trailerCount > 0 : trailerCount == 0; @@ -763,7 +760,7 @@ namespace MediaBrowser.Controller.Entities { var filterValue = query.HasThemeSong.Value; - var themeCount = item.ThemeSongIds.Length; + var themeCount = item.GetThemeSongs().Count; var ok = filterValue ? themeCount > 0 : themeCount == 0; if (!ok) @@ -776,7 +773,7 @@ namespace MediaBrowser.Controller.Entities { var filterValue = query.HasThemeVideo.Value; - var themeCount = item.ThemeVideoIds.Length; + var themeCount = item.GetThemeVideos().Count; var ok = filterValue ? themeCount > 0 : themeCount == 0; if (!ok) @@ -786,7 +783,7 @@ namespace MediaBrowser.Controller.Entities } // Apply genre filter - if (query.Genres.Count > 0 && !query.Genres.Any(v => item.Genres.Contains(v, StringComparer.OrdinalIgnoreCase))) + if (query.Genres.Count > 0 && !query.Genres.Any(v => item.Genres.Contains(v, StringComparison.OrdinalIgnoreCase))) { return false; } @@ -810,7 +807,7 @@ namespace MediaBrowser.Controller.Entities if (query.StudioIds.Length > 0 && !query.StudioIds.Any(id => { var studioItem = libraryManager.GetItemById(id); - return studioItem != null && item.Studios.Contains(studioItem.Name, StringComparer.OrdinalIgnoreCase); + return studioItem != null && item.Studios.Contains(studioItem.Name, StringComparison.OrdinalIgnoreCase); })) { return false; @@ -820,7 +817,7 @@ namespace MediaBrowser.Controller.Entities if (query.GenreIds.Count > 0 && !query.GenreIds.Any(id => { var genreItem = libraryManager.GetItemById(id); - return genreItem != null && item.Genres.Contains(genreItem.Name, StringComparer.OrdinalIgnoreCase); + return genreItem != null && item.Genres.Contains(genreItem.Name, StringComparison.OrdinalIgnoreCase); })) { return false; @@ -853,7 +850,7 @@ namespace MediaBrowser.Controller.Entities var tags = query.Tags; if (tags.Length > 0) { - if (!tags.Any(v => item.Tags.Contains(v, StringComparer.OrdinalIgnoreCase))) + if (!tags.Any(v => item.Tags.Contains(v, StringComparison.OrdinalIgnoreCase))) { return false; } @@ -971,7 +968,7 @@ namespace MediaBrowser.Controller.Entities { var folder = i as ICollectionFolder; - return folder != null && viewTypes.Contains(folder.CollectionType ?? string.Empty, StringComparer.OrdinalIgnoreCase); + return folder != null && viewTypes.Contains(folder.CollectionType ?? string.Empty, StringComparison.OrdinalIgnoreCase); }).ToArray(); } @@ -980,7 +977,7 @@ namespace MediaBrowser.Controller.Entities { var folder = i as ICollectionFolder; - return folder != null && viewTypes.Contains(folder.CollectionType ?? string.Empty, StringComparer.OrdinalIgnoreCase); + return folder != null && viewTypes.Contains(folder.CollectionType ?? string.Empty, StringComparison.OrdinalIgnoreCase); }).ToArray(); } diff --git a/MediaBrowser.Controller/Entities/Video.cs b/MediaBrowser.Controller/Entities/Video.cs index de42c67d3..4f7614f96 100644 --- a/MediaBrowser.Controller/Entities/Video.cs +++ b/MediaBrowser.Controller/Entities/Video.cs @@ -9,6 +9,7 @@ using System.Linq; using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Extensions; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.Persistence; @@ -33,6 +34,7 @@ namespace MediaBrowser.Controller.Entities AdditionalParts = Array.Empty<string>(); LocalAlternateVersions = Array.Empty<string>(); SubtitleFiles = Array.Empty<string>(); + AudioFiles = Array.Empty<string>(); LinkedAlternateVersions = Array.Empty<LinkedChild>(); } @@ -97,6 +99,12 @@ namespace MediaBrowser.Controller.Entities /// <value>The subtitle paths.</value> public string[] SubtitleFiles { get; set; } + /// <summary> + /// Gets or sets the audio paths. + /// </summary> + /// <value>The audio paths.</value> + public string[] AudioFiles { get; set; } + /// <summary> /// Gets or sets a value indicating whether this instance has subtitles. /// </summary> @@ -185,7 +193,7 @@ namespace MediaBrowser.Controller.Entities { if (SourceType == SourceType.Channel) { - return !Tags.Contains("livestream", StringComparer.OrdinalIgnoreCase); + return !Tags.Contains("livestream", StringComparison.OrdinalIgnoreCase); } return !IsActiveRecording(); diff --git a/MediaBrowser.Controller/IServerApplicationHost.cs b/MediaBrowser.Controller/IServerApplicationHost.cs index 8f8cf75a6..75ec5f213 100644 --- a/MediaBrowser.Controller/IServerApplicationHost.cs +++ b/MediaBrowser.Controller/IServerApplicationHost.cs @@ -2,7 +2,6 @@ #pragma warning disable CS1591 -using System.Collections.Generic; using System.Net; using MediaBrowser.Common; using MediaBrowser.Model.System; diff --git a/MediaBrowser.Controller/Library/ILibraryManager.cs b/MediaBrowser.Controller/Library/ILibraryManager.cs index 1e1e2adb8..eba92695e 100644 --- a/MediaBrowser.Controller/Library/ILibraryManager.cs +++ b/MediaBrowser.Controller/Library/ILibraryManager.cs @@ -6,7 +6,6 @@ using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -using Emby.Naming.Common; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; using MediaBrowser.Controller.Dto; @@ -59,10 +58,12 @@ namespace MediaBrowser.Controller.Library /// </summary> /// <param name="fileInfo">The file information.</param> /// <param name="parent">The parent.</param> + /// <param name="directoryService">An instance of <see cref="IDirectoryService"/>.</param> /// <returns>BaseItem.</returns> BaseItem ResolvePath( FileSystemMetadata fileInfo, - Folder parent = null); + Folder parent = null, + IDirectoryService directoryService = null); /// <summary> /// Resolves a set of files into a list of BaseItem. @@ -426,29 +427,14 @@ namespace MediaBrowser.Controller.Library /// <returns>Guid.</returns> Guid GetNewItemId(string key, Type type); - /// <summary> - /// Finds the trailers. - /// </summary> - /// <param name="owner">The owner.</param> - /// <param name="fileSystemChildren">The file system children.</param> - /// <param name="directoryService">The directory service.</param> - /// <returns>IEnumerable<Trailer>.</returns> - IEnumerable<Video> FindTrailers( - BaseItem owner, - List<FileSystemMetadata> fileSystemChildren, - IDirectoryService directoryService); - /// <summary> /// Finds the extras. /// </summary> /// <param name="owner">The owner.</param> /// <param name="fileSystemChildren">The file system children.</param> - /// <param name="directoryService">The directory service.</param> - /// <returns>IEnumerable<Video>.</returns> - IEnumerable<Video> FindExtras( - BaseItem owner, - List<FileSystemMetadata> fileSystemChildren, - IDirectoryService directoryService); + /// <param name="directoryService">An instance of <see cref="IDirectoryService"/>.</param> + /// <returns>IEnumerable<BaseItem>.</returns> + IEnumerable<BaseItem> FindExtras(BaseItem owner, List<FileSystemMetadata> fileSystemChildren, IDirectoryService directoryService); /// <summary> /// Gets the collection folders. diff --git a/MediaBrowser.Controller/LiveTv/LiveTvChannel.cs b/MediaBrowser.Controller/LiveTv/LiveTvChannel.cs index 074e023e8..e63874f21 100644 --- a/MediaBrowser.Controller/LiveTv/LiveTvChannel.cs +++ b/MediaBrowser.Controller/LiveTv/LiveTvChannel.cs @@ -5,9 +5,9 @@ using System; using System.Collections.Generic; using System.Globalization; -using System.Linq; using System.Text.Json.Serialization; using Jellyfin.Data.Enums; +using Jellyfin.Extensions; using MediaBrowser.Controller.Entities; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; @@ -74,7 +74,7 @@ namespace MediaBrowser.Controller.LiveTv /// </summary> /// <value><c>true</c> if this instance is kids; otherwise, <c>false</c>.</value> [JsonIgnore] - public bool IsKids => Tags.Contains("Kids", StringComparer.OrdinalIgnoreCase); + public bool IsKids => Tags.Contains("Kids", StringComparison.OrdinalIgnoreCase); [JsonIgnore] public bool IsRepeat { get; set; } @@ -107,9 +107,7 @@ namespace MediaBrowser.Controller.LiveTv { if (!string.IsNullOrEmpty(Number)) { - double number = 0; - - if (double.TryParse(Number, NumberStyles.Any, CultureInfo.InvariantCulture, out number)) + if (double.TryParse(Number, NumberStyles.Any, CultureInfo.InvariantCulture, out double number)) { return string.Format(CultureInfo.InvariantCulture, "{0:00000.0}", number) + "-" + (Name ?? string.Empty); } diff --git a/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs b/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs index 111dc0d27..6c4a5ea17 100644 --- a/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs +++ b/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs @@ -8,6 +8,7 @@ using System.Globalization; using System.Linq; using System.Text.Json.Serialization; using Jellyfin.Data.Enums; +using Jellyfin.Extensions; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Providers; @@ -66,7 +67,7 @@ namespace MediaBrowser.Controller.LiveTv /// </summary> /// <value><c>true</c> if this instance is sports; otherwise, <c>false</c>.</value> [JsonIgnore] - public bool IsSports => Tags.Contains("Sports", StringComparer.OrdinalIgnoreCase); + public bool IsSports => Tags.Contains("Sports", StringComparison.OrdinalIgnoreCase); /// <summary> /// Gets or sets a value indicating whether this instance is series. @@ -80,28 +81,28 @@ namespace MediaBrowser.Controller.LiveTv /// </summary> /// <value><c>true</c> if this instance is live; otherwise, <c>false</c>.</value> [JsonIgnore] - public bool IsLive => Tags.Contains("Live", StringComparer.OrdinalIgnoreCase); + public bool IsLive => Tags.Contains("Live", StringComparison.OrdinalIgnoreCase); /// <summary> /// Gets a value indicating whether this instance is news. /// </summary> /// <value><c>true</c> if this instance is news; otherwise, <c>false</c>.</value> [JsonIgnore] - public bool IsNews => Tags.Contains("News", StringComparer.OrdinalIgnoreCase); + public bool IsNews => Tags.Contains("News", StringComparison.OrdinalIgnoreCase); /// <summary> /// Gets a value indicating whether this instance is kids. /// </summary> /// <value><c>true</c> if this instance is kids; otherwise, <c>false</c>.</value> [JsonIgnore] - public bool IsKids => Tags.Contains("Kids", StringComparer.OrdinalIgnoreCase); + public bool IsKids => Tags.Contains("Kids", StringComparison.OrdinalIgnoreCase); /// <summary> /// Gets a value indicating whether this instance is premiere. /// </summary> /// <value><c>true</c> if this instance is premiere; otherwise, <c>false</c>.</value> [JsonIgnore] - public bool IsPremiere => Tags.Contains("Premiere", StringComparer.OrdinalIgnoreCase); + public bool IsPremiere => Tags.Contains("Premiere", StringComparison.OrdinalIgnoreCase); /// <summary> /// Gets the folder containing the item. diff --git a/MediaBrowser.Controller/LiveTv/TimerInfo.cs b/MediaBrowser.Controller/LiveTv/TimerInfo.cs index 1a2e8acb3..62541ea8b 100644 --- a/MediaBrowser.Controller/LiveTv/TimerInfo.cs +++ b/MediaBrowser.Controller/LiveTv/TimerInfo.cs @@ -4,8 +4,8 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Text.Json.Serialization; +using Jellyfin.Extensions; using MediaBrowser.Model.LiveTv; namespace MediaBrowser.Controller.LiveTv @@ -123,11 +123,11 @@ namespace MediaBrowser.Controller.LiveTv public bool IsMovie { get; set; } - public bool IsKids => Tags.Contains("Kids", StringComparer.OrdinalIgnoreCase); + public bool IsKids => Tags.Contains("Kids", StringComparison.OrdinalIgnoreCase); - public bool IsSports => Tags.Contains("Sports", StringComparer.OrdinalIgnoreCase); + public bool IsSports => Tags.Contains("Sports", StringComparison.OrdinalIgnoreCase); - public bool IsNews => Tags.Contains("News", StringComparer.OrdinalIgnoreCase); + public bool IsNews => Tags.Contains("News", StringComparison.OrdinalIgnoreCase); public bool IsSeries { get; set; } @@ -136,10 +136,10 @@ namespace MediaBrowser.Controller.LiveTv /// </summary> /// <value><c>true</c> if this instance is live; otherwise, <c>false</c>.</value> [JsonIgnore] - public bool IsLive => Tags.Contains("Live", StringComparer.OrdinalIgnoreCase); + public bool IsLive => Tags.Contains("Live", StringComparison.OrdinalIgnoreCase); [JsonIgnore] - public bool IsPremiere => Tags.Contains("Premiere", StringComparer.OrdinalIgnoreCase); + public bool IsPremiere => Tags.Contains("Premiere", StringComparison.OrdinalIgnoreCase); public int? ProductionYear { get; set; } diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 1da578462..c139d9d40 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -883,6 +883,11 @@ namespace MediaBrowser.Controller.MediaEncoding arg.Append(" -i file:\"").Append(subtitlePath).Append('\"'); } + if (state.AudioStream != null && state.AudioStream.IsExternal) + { + arg.Append(" -i \"").Append(state.AudioStream.Path).Append('"'); + } + return arg.ToString(); } @@ -2209,10 +2214,24 @@ namespace MediaBrowser.Controller.MediaEncoding if (state.AudioStream != null) { - args += string.Format( - CultureInfo.InvariantCulture, - " -map 0:{0}", - state.AudioStream.Index); + if (state.AudioStream.IsExternal) + { + int externalAudioMapIndex = state.SubtitleStream != null && state.SubtitleStream.IsExternal ? 2 : 1; + int externalAudioStream = state.MediaSource.MediaStreams.Where(i => i.Path == state.AudioStream.Path).ToList().IndexOf(state.AudioStream); + + args += string.Format( + CultureInfo.InvariantCulture, + " -map {0}:{1}", + externalAudioMapIndex, + externalAudioStream); + } + else + { + args += string.Format( + CultureInfo.InvariantCulture, + " -map 0:{0}", + state.AudioStream.Index); + } } else { diff --git a/MediaBrowser.Controller/Playlists/Playlist.cs b/MediaBrowser.Controller/Playlists/Playlist.cs index 5e671a725..89f3bdf46 100644 --- a/MediaBrowser.Controller/Playlists/Playlist.cs +++ b/MediaBrowser.Controller/Playlists/Playlist.cs @@ -189,7 +189,7 @@ namespace MediaBrowser.Controller.Playlists return LibraryManager.GetItemList(new InternalItemsQuery(user) { Recursive = true, - IncludeItemTypes = new[] { nameof(Audio) }, + IncludeItemTypes = new[] { BaseItemKind.Audio }, GenreIds = new[] { musicGenre.Id }, OrderBy = new[] { (ItemSortBy.AlbumArtist, SortOrder.Ascending), (ItemSortBy.Album, SortOrder.Ascending), (ItemSortBy.SortName, SortOrder.Ascending) }, DtoOptions = options @@ -201,7 +201,7 @@ namespace MediaBrowser.Controller.Playlists return LibraryManager.GetItemList(new InternalItemsQuery(user) { Recursive = true, - IncludeItemTypes = new[] { nameof(Audio) }, + IncludeItemTypes = new[] { BaseItemKind.Audio }, ArtistIds = new[] { musicArtist.Id }, OrderBy = new[] { (ItemSortBy.AlbumArtist, SortOrder.Ascending), (ItemSortBy.Album, SortOrder.Ascending), (ItemSortBy.SortName, SortOrder.Ascending) }, DtoOptions = options diff --git a/MediaBrowser.Controller/Providers/DirectoryService.cs b/MediaBrowser.Controller/Providers/DirectoryService.cs index b31270270..e6d975ffe 100644 --- a/MediaBrowser.Controller/Providers/DirectoryService.cs +++ b/MediaBrowser.Controller/Providers/DirectoryService.cs @@ -25,7 +25,7 @@ namespace MediaBrowser.Controller.Providers public FileSystemMetadata[] GetFileSystemEntries(string path) { - return _cache.GetOrAdd(path, (p, fileSystem) => fileSystem.GetFileSystemEntries(p).ToArray(), _fileSystem); + return _cache.GetOrAdd(path, static (p, fileSystem) => fileSystem.GetFileSystemEntries(p).ToArray(), _fileSystem); } public List<FileSystemMetadata> GetFiles(string path) @@ -69,7 +69,7 @@ namespace MediaBrowser.Controller.Providers _filePathCache.TryRemove(path, out _); } - var filePaths = _filePathCache.GetOrAdd(path, (p, fileSystem) => fileSystem.GetFilePaths(p).ToList(), _fileSystem); + var filePaths = _filePathCache.GetOrAdd(path, static (p, fileSystem) => fileSystem.GetFilePaths(p).ToList(), _fileSystem); if (sort) { diff --git a/MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs b/MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs index a42c7f8b5..90fd6e269 100644 --- a/MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs +++ b/MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs @@ -4,6 +4,7 @@ using System; using System.Linq; +using Jellyfin.Extensions; using MediaBrowser.Controller.Entities; using MediaBrowser.Model.Providers; @@ -58,7 +59,7 @@ namespace MediaBrowser.Controller.Providers { if (RefreshPaths != null && RefreshPaths.Length > 0) { - return RefreshPaths.Contains(item.Path ?? string.Empty, StringComparer.OrdinalIgnoreCase); + return RefreshPaths.Contains(item.Path ?? string.Empty, StringComparison.OrdinalIgnoreCase); } return true; diff --git a/MediaBrowser.LocalMetadata/Images/InternalMetadataFolderImageProvider.cs b/MediaBrowser.LocalMetadata/Images/InternalMetadataFolderImageProvider.cs index 6d076ba27..d3fa41bcd 100644 --- a/MediaBrowser.LocalMetadata/Images/InternalMetadataFolderImageProvider.cs +++ b/MediaBrowser.LocalMetadata/Images/InternalMetadataFolderImageProvider.cs @@ -1,7 +1,6 @@ using System.Collections.Generic; using System.IO; using System.Linq; -using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Providers; diff --git a/MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs b/MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs index f79147803..7dc6149f4 100644 --- a/MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs +++ b/MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; +using Jellyfin.Extensions; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Entities.Movies; @@ -117,16 +118,10 @@ namespace MediaBrowser.LocalMetadata.Images return Enumerable.Empty<FileSystemMetadata>(); } - if (includeDirectories) - { - return directoryService.GetFileSystemEntries(path) - .Where(i => BaseItem.SupportedImageExtensions.Contains(i.Extension, StringComparer.OrdinalIgnoreCase) || i.IsDirectory) - - .OrderBy(i => Array.IndexOf(BaseItem.SupportedImageExtensions, i.Extension ?? string.Empty)); - } - - return directoryService.GetFiles(path) - .Where(i => BaseItem.SupportedImageExtensions.Contains(i.Extension, StringComparer.OrdinalIgnoreCase)) + return directoryService.GetFileSystemEntries(path) + .Where(i => + (includeDirectories && i.IsDirectory) + || BaseItem.SupportedImageExtensions.Contains(i.Extension, StringComparison.OrdinalIgnoreCase)) .OrderBy(i => Array.IndexOf(BaseItem.SupportedImageExtensions, i.Extension ?? string.Empty)); } @@ -201,7 +196,7 @@ namespace MediaBrowser.LocalMetadata.Images added = AddImage(files, images, "logo", imagePrefix, isInMixedFolder, ImageType.Logo); if (!added) { - added = AddImage(files, images, "clearlogo", imagePrefix, isInMixedFolder, ImageType.Logo); + AddImage(files, images, "clearlogo", imagePrefix, isInMixedFolder, ImageType.Logo); } } @@ -218,7 +213,7 @@ namespace MediaBrowser.LocalMetadata.Images if (!added) { - added = AddImage(files, images, "disc", imagePrefix, isInMixedFolder, ImageType.Disc); + AddImage(files, images, "disc", imagePrefix, isInMixedFolder, ImageType.Disc); } } else if (item is Video || item is BoxSet) @@ -232,7 +227,7 @@ namespace MediaBrowser.LocalMetadata.Images if (!added) { - added = AddImage(files, images, "discart", imagePrefix, isInMixedFolder, ImageType.Disc); + AddImage(files, images, "discart", imagePrefix, isInMixedFolder, ImageType.Disc); } } @@ -248,7 +243,7 @@ namespace MediaBrowser.LocalMetadata.Images added = AddImage(files, images, "landscape", imagePrefix, isInMixedFolder, ImageType.Thumb); if (!added) { - added = AddImage(files, images, "thumb", imagePrefix, isInMixedFolder, ImageType.Thumb); + AddImage(files, images, "thumb", imagePrefix, isInMixedFolder, ImageType.Thumb); } } diff --git a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj index 6bb8bcdab..9f6d8e7fe 100644 --- a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj +++ b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj @@ -22,8 +22,8 @@ </ItemGroup> <ItemGroup> - <PackageReference Include="BDInfo" Version="0.7.6.1" /> - <PackageReference Include="libse" Version="3.6.2" /> + <PackageReference Include="BDInfo" Version="0.7.6.2" /> + <PackageReference Include="libse" Version="3.6.4" /> <PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" /> <PackageReference Include="System.Text.Encoding.CodePages" Version="6.0.0" /> <PackageReference Include="UTF.Unknown" Version="2.5.0" /> diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs index 5c37bd506..246c48bf5 100644 --- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs +++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs @@ -45,6 +45,7 @@ namespace MediaBrowser.MediaEncoding.Probing { "AC/DC", "Au/Ra", + "Bremer/McCoy", "이달의 소녀 1/3", "LOONA 1/3", "LOONA / yyxy", @@ -648,11 +649,6 @@ namespace MediaBrowser.MediaEncoding.Probing stream.IsAVC = false; } - if (!string.IsNullOrWhiteSpace(streamInfo.FieldOrder) && !string.Equals(streamInfo.FieldOrder, "progressive", StringComparison.OrdinalIgnoreCase)) - { - stream.IsInterlaced = true; - } - // Filter out junk if (!string.IsNullOrWhiteSpace(streamInfo.CodecTagString) && !streamInfo.CodecTagString.Contains("[0]", StringComparison.OrdinalIgnoreCase)) { @@ -724,6 +720,23 @@ namespace MediaBrowser.MediaEncoding.Probing stream.AverageFrameRate = GetFrameRate(streamInfo.AverageFrameRate); stream.RealFrameRate = GetFrameRate(streamInfo.RFrameRate); + // Some interlaced H.264 files in mp4 containers using MBAFF coding aren't flagged as being interlaced by FFprobe, + // so for H.264 files we also calculate the frame rate from the codec time base and check if it is double the reported + // frame rate (both rounded to the nearest integer) to determine if the file is interlaced + int roundedTimeBaseFPS = Convert.ToInt32(1 / GetFrameRate(stream.CodecTimeBase) ?? 0); + int roundedDoubleFrameRate = Convert.ToInt32(stream.AverageFrameRate * 2 ?? 0); + + bool videoInterlaced = !string.IsNullOrWhiteSpace(streamInfo.FieldOrder) + && !string.Equals(streamInfo.FieldOrder, "progressive", StringComparison.OrdinalIgnoreCase); + bool h264MbaffCoded = string.Equals(stream.Codec, "h264", StringComparison.OrdinalIgnoreCase) + && string.IsNullOrWhiteSpace(streamInfo.FieldOrder) + && roundedTimeBaseFPS == roundedDoubleFrameRate; + + if (videoInterlaced || h264MbaffCoded) + { + stream.IsInterlaced = true; + } + if (isAudio || string.Equals(stream.Codec, "mjpeg", StringComparison.OrdinalIgnoreCase) || string.Equals(stream.Codec, "gif", StringComparison.OrdinalIgnoreCase) @@ -1019,27 +1032,32 @@ namespace MediaBrowser.MediaEncoding.Probing /// </summary> /// <param name="value">The value.</param> /// <returns>System.Nullable{System.Single}.</returns> - private float? GetFrameRate(string value) + internal static float? GetFrameRate(ReadOnlySpan<char> value) { - if (string.IsNullOrEmpty(value)) + if (value.IsEmpty) { return null; } - var parts = value.Split('/'); - - float result; - - if (parts.Length == 2) + int index = value.IndexOf('/'); + if (index == -1) { - result = float.Parse(parts[0], CultureInfo.InvariantCulture) / float.Parse(parts[1], CultureInfo.InvariantCulture); - } - else - { - result = float.Parse(parts[0], CultureInfo.InvariantCulture); + // REVIEW: is this branch actually required? (i.e. does ffprobe ever output something other than a fraction?) + if (float.TryParse(value, NumberStyles.AllowThousands | NumberStyles.Float, CultureInfo.InvariantCulture, out var result)) + { + return result; + } + + return null; } - return float.IsNaN(result) ? null : result; + if (!float.TryParse(value[..index], NumberStyles.Integer, CultureInfo.InvariantCulture, out var dividend) + || !float.TryParse(value[(index + 1)..], NumberStyles.Integer, CultureInfo.InvariantCulture, out var divisor)) + { + return null; + } + + return divisor == 0f ? null : dividend / divisor; } private void SetAudioRuntimeTicks(InternalMediaInfoResult result, MediaInfo data) @@ -1349,8 +1367,8 @@ namespace MediaBrowser.MediaEncoding.Probing } // Don't add artist/album artist name to studios, even if it's listed there - if (info.Artists.Contains(studio, StringComparer.OrdinalIgnoreCase) - || info.AlbumArtists.Contains(studio, StringComparer.OrdinalIgnoreCase)) + if (info.Artists.Contains(studio, StringComparison.OrdinalIgnoreCase) + || info.AlbumArtists.Contains(studio, StringComparison.OrdinalIgnoreCase)) { continue; } diff --git a/MediaBrowser.Model/Configuration/LibraryOptions.cs b/MediaBrowser.Model/Configuration/LibraryOptions.cs index 90cf8f43b..d3ce6aa7f 100644 --- a/MediaBrowser.Model/Configuration/LibraryOptions.cs +++ b/MediaBrowser.Model/Configuration/LibraryOptions.cs @@ -21,7 +21,6 @@ namespace MediaBrowser.Model.Configuration SaveSubtitlesWithMedia = true; EnableRealtimeMonitor = true; PathInfos = Array.Empty<MediaPathInfo>(); - EnableInternetProviders = true; EnableAutomaticSeriesGrouping = true; SeasonZeroDisplayName = "Specials"; } @@ -38,6 +37,7 @@ namespace MediaBrowser.Model.Configuration public bool SaveLocalMetadata { get; set; } + [Obsolete("Disable remote providers in TypeOptions instead")] public bool EnableInternetProviders { get; set; } public bool EnableAutomaticSeriesGrouping { get; set; } @@ -81,6 +81,7 @@ namespace MediaBrowser.Model.Configuration public bool RequirePerfectSubtitleMatch { get; set; } public bool SaveSubtitlesWithMedia { get; set; } + public bool AutomaticallyAddToCollection { get; set; } public TypeOptions[] TypeOptions { get; set; } diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs index 0ab721b77..46e61ee1a 100644 --- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs +++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs @@ -13,18 +13,6 @@ namespace MediaBrowser.Model.Configuration /// </summary> public class ServerConfiguration : BaseApplicationConfiguration { - /// <summary> - /// The default value for <see cref="HttpServerPortNumber"/>. - /// </summary> - public const int DefaultHttpPort = 8096; - - /// <summary> - /// The default value for <see cref="PublicHttpsPort"/> and <see cref="HttpsPortNumber"/>. - /// </summary> - public const int DefaultHttpsPort = 8920; - - private string _baseUrl = string.Empty; - /// <summary> /// Initializes a new instance of the <see cref="ServerConfiguration" /> class. /// </summary> @@ -75,149 +63,13 @@ namespace MediaBrowser.Model.Configuration }; } - /// <summary> - /// Gets or sets a value indicating whether to enable automatic port forwarding. - /// </summary> - public bool EnableUPnP { get; set; } = false; - /// <summary> /// Gets or sets a value indicating whether to enable prometheus metrics exporting. /// </summary> public bool EnableMetrics { get; set; } = false; - /// <summary> - /// Gets or sets the public mapped port. - /// </summary> - /// <value>The public mapped port.</value> - public int PublicPort { get; set; } = DefaultHttpPort; - - /// <summary> - /// Gets or sets a value indicating whether the http port should be mapped as part of UPnP automatic port forwarding. - /// </summary> - public bool UPnPCreateHttpPortMap { get; set; } = false; - - /// <summary> - /// Gets or sets client udp port range. - /// </summary> - public string UDPPortRange { get; set; } = string.Empty; - - /// <summary> - /// Gets or sets a value indicating whether IPV6 capability is enabled. - /// </summary> - public bool EnableIPV6 { get; set; } = false; - - /// <summary> - /// Gets or sets a value indicating whether IPV4 capability is enabled. - /// </summary> - public bool EnableIPV4 { get; set; } = true; - - /// <summary> - /// Gets or sets a value indicating whether detailed ssdp logs are sent to the console/log. - /// "Emby.Dlna": "Debug" must be set in logging.default.json for this property to work. - /// </summary> - public bool EnableSSDPTracing { get; set; } = false; - - /// <summary> - /// Gets or sets a value indicating whether an IP address is to be used to filter the detailed ssdp logs that are being sent to the console/log. - /// If the setting "Emby.Dlna": "Debug" msut be set in logging.default.json for this property to work. - /// </summary> - public string SSDPTracingFilter { get; set; } = string.Empty; - - /// <summary> - /// Gets or sets the number of times SSDP UDP messages are sent. - /// </summary> - public int UDPSendCount { get; set; } = 2; - - /// <summary> - /// Gets or sets the delay between each groups of SSDP messages (in ms). - /// </summary> - public int UDPSendDelay { get; set; } = 100; - - /// <summary> - /// Gets or sets a value indicating whether address names that match <see cref="VirtualInterfaceNames"/> should be Ignore for the purposes of binding. - /// </summary> - public bool IgnoreVirtualInterfaces { get; set; } = true; - - /// <summary> - /// Gets or sets a value indicating the interfaces that should be ignored. The list can be comma separated. <seealso cref="IgnoreVirtualInterfaces"/>. - /// </summary> - public string VirtualInterfaceNames { get; set; } = "vEthernet*"; - - /// <summary> - /// Gets or sets the time (in seconds) between the pings of SSDP gateway monitor. - /// </summary> - public int GatewayMonitorPeriod { get; set; } = 60; - - /// <summary> - /// Gets a value indicating whether multi-socket binding is available. - /// </summary> - public bool EnableMultiSocketBinding { get; } = true; - - /// <summary> - /// Gets or sets a value indicating whether all IPv6 interfaces should be treated as on the internal network. - /// Depending on the address range implemented ULA ranges might not be used. - /// </summary> - public bool TrustAllIP6Interfaces { get; set; } = false; - - /// <summary> - /// Gets or sets the ports that HDHomerun uses. - /// </summary> - public string HDHomerunPortRange { get; set; } = string.Empty; - - /// <summary> - /// Gets or sets PublishedServerUri to advertise for specific subnets. - /// </summary> - public string[] PublishedServerUriBySubnet { get; set; } = Array.Empty<string>(); - - /// <summary> - /// Gets or sets a value indicating whether Autodiscovery tracing is enabled. - /// </summary> - public bool AutoDiscoveryTracing { get; set; } = false; - - /// <summary> - /// Gets or sets a value indicating whether Autodiscovery is enabled. - /// </summary> - public bool AutoDiscovery { get; set; } = true; - - /// <summary> - /// Gets or sets the public HTTPS port. - /// </summary> - /// <value>The public HTTPS port.</value> - public int PublicHttpsPort { get; set; } = DefaultHttpsPort; - - /// <summary> - /// Gets or sets the HTTP server port number. - /// </summary> - /// <value>The HTTP server port number.</value> - public int HttpServerPortNumber { get; set; } = DefaultHttpPort; - - /// <summary> - /// Gets or sets the HTTPS server port number. - /// </summary> - /// <value>The HTTPS server port number.</value> - public int HttpsPortNumber { get; set; } = DefaultHttpsPort; - - /// <summary> - /// Gets or sets a value indicating whether to use HTTPS. - /// </summary> - /// <remarks> - /// In order for HTTPS to be used, in addition to setting this to true, valid values must also be - /// provided for <see cref="CertificatePath"/> and <see cref="CertificatePassword"/>. - /// </remarks> - public bool EnableHttps { get; set; } = false; - public bool EnableNormalizedItemByNameIds { get; set; } = true; - /// <summary> - /// Gets or sets the filesystem path of an X.509 certificate to use for SSL. - /// </summary> - public string CertificatePath { get; set; } = string.Empty; - - /// <summary> - /// Gets or sets the password required to access the X.509 certificate data in the file specified by <see cref="CertificatePath"/>. - /// </summary> - public string CertificatePassword { get; set; } = string.Empty; - /// <summary> /// Gets or sets a value indicating whether this instance is port authorized. /// </summary> @@ -229,11 +81,6 @@ namespace MediaBrowser.Model.Configuration /// </summary> public bool QuickConnectAvailable { get; set; } = false; - /// <summary> - /// Gets or sets a value indicating whether access outside of the LAN is permitted. - /// </summary> - public bool EnableRemoteAccess { get; set; } = true; - /// <summary> /// Gets or sets a value indicating whether [enable case sensitive item ids]. /// </summary> @@ -318,13 +165,6 @@ namespace MediaBrowser.Model.Configuration /// <value>The file watcher delay.</value> public int LibraryMonitorDelay { get; set; } = 60; - /// <summary> - /// Gets or sets a value indicating whether [enable dashboard response caching]. - /// Allows potential contributors without visual studio to modify production dashboard code and test changes. - /// </summary> - /// <value><c>true</c> if [enable dashboard response caching]; otherwise, <c>false</c>.</value> - public bool EnableDashboardResponseCaching { get; set; } = true; - /// <summary> /// Gets or sets the image saving convention. /// </summary> @@ -337,36 +177,6 @@ namespace MediaBrowser.Model.Configuration public string ServerName { get; set; } = string.Empty; - public string BaseUrl - { - get => _baseUrl; - set - { - // Normalize the start of the string - if (string.IsNullOrWhiteSpace(value)) - { - // If baseUrl is empty, set an empty prefix string - _baseUrl = string.Empty; - return; - } - - if (value[0] != '/') - { - // If baseUrl was not configured with a leading slash, append one for consistency - value = "/" + value; - } - - // Normalize the end of the string - if (value[value.Length - 1] == '/') - { - // If baseUrl was configured with a trailing slash, remove it for consistency - value = value.Remove(value.Length - 1); - } - - _baseUrl = value; - } - } - public string UICulture { get; set; } = "en-US"; public bool SaveMetadataHidden { get; set; } = false; @@ -381,43 +191,16 @@ namespace MediaBrowser.Model.Configuration public bool DisplaySpecialsWithinSeasons { get; set; } = true; - /// <summary> - /// Gets or sets the subnets that are deemed to make up the LAN. - /// </summary> - public string[] LocalNetworkSubnets { get; set; } = Array.Empty<string>(); - - /// <summary> - /// Gets or sets the interface addresses which Jellyfin will bind to. If empty, all interfaces will be used. - /// </summary> - public string[] LocalNetworkAddresses { get; set; } = Array.Empty<string>(); - public string[] CodecsUsed { get; set; } = Array.Empty<string>(); public List<RepositoryInfo> PluginRepositories { get; set; } = new List<RepositoryInfo>(); public bool EnableExternalContentInSuggestions { get; set; } = true; - /// <summary> - /// Gets or sets a value indicating whether the server should force connections over HTTPS. - /// </summary> - public bool RequireHttps { get; set; } = false; - - /// <summary> - /// Gets or sets the filter for remote IP connectivity. Used in conjuntion with <seealso cref="IsRemoteIPFilterBlacklist"/>. - /// </summary> - public string[] RemoteIPFilter { get; set; } = Array.Empty<string>(); - - /// <summary> - /// Gets or sets a value indicating whether <seealso cref="RemoteIPFilter"/> contains a blacklist or a whitelist. Default is a whitelist. - /// </summary> - public bool IsRemoteIPFilterBlacklist { get; set; } = false; - public int ImageExtractionTimeoutMs { get; set; } = 0; public PathSubstitution[] PathSubstitutions { get; set; } = Array.Empty<PathSubstitution>(); - public string[] UninstalledPlugins { get; set; } = Array.Empty<string>(); - /// <summary> /// Gets or sets a value indicating whether slow server responses should be logged as a warning. /// </summary> @@ -433,11 +216,6 @@ namespace MediaBrowser.Model.Configuration /// </summary> public string[] CorsHosts { get; set; } = new[] { "*" }; - /// <summary> - /// Gets or sets the known proxies. - /// </summary> - public string[] KnownProxies { get; set; } = Array.Empty<string>(); - /// <summary> /// Gets or sets the number of days we should retain activity logs. /// </summary> diff --git a/MediaBrowser.Model/Dlna/CodecProfile.cs b/MediaBrowser.Model/Dlna/CodecProfile.cs index 8343cf028..f857bf3a8 100644 --- a/MediaBrowser.Model/Dlna/CodecProfile.cs +++ b/MediaBrowser.Model/Dlna/CodecProfile.cs @@ -2,8 +2,8 @@ #pragma warning disable CS1591 using System; -using System.Linq; using System.Xml.Serialization; +using Jellyfin.Extensions; namespace MediaBrowser.Model.Dlna { @@ -58,7 +58,7 @@ namespace MediaBrowser.Model.Dlna foreach (var val in codec) { - if (codecs.Contains(val, StringComparer.OrdinalIgnoreCase)) + if (codecs.Contains(val, StringComparison.OrdinalIgnoreCase)) { return true; } diff --git a/MediaBrowser.Model/Dlna/ConditionProcessor.cs b/MediaBrowser.Model/Dlna/ConditionProcessor.cs index 55c4dd074..8d03b4c0b 100644 --- a/MediaBrowser.Model/Dlna/ConditionProcessor.cs +++ b/MediaBrowser.Model/Dlna/ConditionProcessor.cs @@ -2,7 +2,7 @@ using System; using System.Globalization; -using System.Linq; +using Jellyfin.Extensions; using MediaBrowser.Model.MediaInfo; namespace MediaBrowser.Model.Dlna @@ -167,7 +167,7 @@ namespace MediaBrowser.Model.Dlna switch (condition.Condition) { case ProfileConditionType.EqualsAny: - return expected.Split('|').Contains(currentValue, StringComparer.OrdinalIgnoreCase); + return expected.Split('|').Contains(currentValue, StringComparison.OrdinalIgnoreCase); case ProfileConditionType.Equals: return string.Equals(currentValue, expected, StringComparison.OrdinalIgnoreCase); case ProfileConditionType.NotEquals: diff --git a/MediaBrowser.Model/Dlna/ContainerProfile.cs b/MediaBrowser.Model/Dlna/ContainerProfile.cs index 740966088..c6befdd85 100644 --- a/MediaBrowser.Model/Dlna/ContainerProfile.cs +++ b/MediaBrowser.Model/Dlna/ContainerProfile.cs @@ -1,8 +1,8 @@ #pragma warning disable CS1591 using System; -using System.Linq; using System.Xml.Serialization; +using Jellyfin.Extensions; namespace MediaBrowser.Model.Dlna { @@ -62,7 +62,7 @@ namespace MediaBrowser.Model.Dlna foreach (var container in allInputContainers) { - if (profileContainers.Contains(container, StringComparer.OrdinalIgnoreCase)) + if (profileContainers.Contains(container, StringComparison.OrdinalIgnoreCase)) { return !isNegativeList; } diff --git a/MediaBrowser.Model/Dlna/DeviceProfile.cs b/MediaBrowser.Model/Dlna/DeviceProfile.cs index feb3d880e..6170ff5bd 100644 --- a/MediaBrowser.Model/Dlna/DeviceProfile.cs +++ b/MediaBrowser.Model/Dlna/DeviceProfile.cs @@ -1,8 +1,8 @@ #pragma warning disable CA1819 // Properties should not return arrays using System; using System.ComponentModel; -using System.Linq; using System.Xml.Serialization; +using Jellyfin.Extensions; using MediaBrowser.Model.MediaInfo; namespace MediaBrowser.Model.Dlna @@ -253,7 +253,7 @@ namespace MediaBrowser.Model.Dlna continue; } - if (!i.GetAudioCodecs().Contains(audioCodec ?? string.Empty, StringComparer.OrdinalIgnoreCase)) + if (!i.GetAudioCodecs().Contains(audioCodec ?? string.Empty, StringComparison.OrdinalIgnoreCase)) { continue; } @@ -287,7 +287,7 @@ namespace MediaBrowser.Model.Dlna continue; } - if (!i.GetAudioCodecs().Contains(audioCodec ?? string.Empty, StringComparer.OrdinalIgnoreCase)) + if (!i.GetAudioCodecs().Contains(audioCodec ?? string.Empty, StringComparison.OrdinalIgnoreCase)) { continue; } @@ -328,7 +328,7 @@ namespace MediaBrowser.Model.Dlna } var audioCodecs = i.GetAudioCodecs(); - if (audioCodecs.Length > 0 && !audioCodecs.Contains(audioCodec ?? string.Empty, StringComparer.OrdinalIgnoreCase)) + if (audioCodecs.Length > 0 && !audioCodecs.Contains(audioCodec ?? string.Empty, StringComparison.OrdinalIgnoreCase)) { continue; } @@ -469,13 +469,13 @@ namespace MediaBrowser.Model.Dlna } var audioCodecs = i.GetAudioCodecs(); - if (audioCodecs.Length > 0 && !audioCodecs.Contains(audioCodec ?? string.Empty, StringComparer.OrdinalIgnoreCase)) + if (audioCodecs.Length > 0 && !audioCodecs.Contains(audioCodec ?? string.Empty, StringComparison.OrdinalIgnoreCase)) { continue; } var videoCodecs = i.GetVideoCodecs(); - if (videoCodecs.Length > 0 && !videoCodecs.Contains(videoCodec ?? string.Empty, StringComparer.OrdinalIgnoreCase)) + if (videoCodecs.Length > 0 && !videoCodecs.Contains(videoCodec ?? string.Empty, StringComparison.OrdinalIgnoreCase)) { continue; } diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs index 322cc367b..c6ce45788 100644 --- a/MediaBrowser.Model/Dlna/StreamBuilder.cs +++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs @@ -677,8 +677,8 @@ namespace MediaBrowser.Model.Dlna var videoStream = item.VideoStream; // TODO: This doesn't account for situations where the device is able to handle the media's bitrate, but the connection isn't fast enough - var directPlayEligibilityResult = IsEligibleForDirectPlay(item, GetBitrateForDirectPlayCheck(item, options, true) ?? 0, subtitleStream, options, PlayMethod.DirectPlay); - var directStreamEligibilityResult = IsEligibleForDirectPlay(item, options.GetMaxBitrate(false) ?? 0, subtitleStream, options, PlayMethod.DirectStream); + var directPlayEligibilityResult = IsEligibleForDirectPlay(item, GetBitrateForDirectPlayCheck(item, options, true) ?? 0, subtitleStream, audioStream, options, PlayMethod.DirectPlay); + var directStreamEligibilityResult = IsEligibleForDirectPlay(item, options.GetMaxBitrate(false) ?? 0, subtitleStream, audioStream, options, PlayMethod.DirectStream); bool isEligibleForDirectPlay = options.EnableDirectPlay && (options.ForceDirectPlay || directPlayEligibilityResult.Item1); bool isEligibleForDirectStream = options.EnableDirectStream && (options.ForceDirectStream || directStreamEligibilityResult.Item1); @@ -1213,6 +1213,7 @@ namespace MediaBrowser.Model.Dlna MediaSourceInfo item, long maxBitrate, MediaStream subtitleStream, + MediaStream audioStream, VideoOptions options, PlayMethod playMethod) { @@ -1220,7 +1221,9 @@ namespace MediaBrowser.Model.Dlna { var subtitleProfile = GetSubtitleProfile(item, subtitleStream, options.Profile.SubtitleProfiles, playMethod, _transcoderSupport, item.Container, null); - if (subtitleProfile.Method != SubtitleDeliveryMethod.External && subtitleProfile.Method != SubtitleDeliveryMethod.Embed) + if (subtitleProfile.Method != SubtitleDeliveryMethod.Drop + && subtitleProfile.Method != SubtitleDeliveryMethod.External + && subtitleProfile.Method != SubtitleDeliveryMethod.Embed) { _logger.LogDebug("Not eligible for {0} due to unsupported subtitles", playMethod); return (false, TranscodeReason.SubtitleCodecNotSupported); @@ -1228,8 +1231,17 @@ namespace MediaBrowser.Model.Dlna } bool result = IsAudioEligibleForDirectPlay(item, maxBitrate, playMethod); + if (!result) + { + return (false, TranscodeReason.ContainerBitrateExceedsLimit); + } - return (result, result ? null : TranscodeReason.ContainerBitrateExceedsLimit); + if (audioStream.IsExternal) + { + return (false, TranscodeReason.AudioIsExternal); + } + + return (true, null); } public static SubtitleProfile GetSubtitleProfile( diff --git a/MediaBrowser.Model/Dlna/SubtitleDeliveryMethod.cs b/MediaBrowser.Model/Dlna/SubtitleDeliveryMethod.cs index 9b39f9e11..69bda2d91 100644 --- a/MediaBrowser.Model/Dlna/SubtitleDeliveryMethod.cs +++ b/MediaBrowser.Model/Dlna/SubtitleDeliveryMethod.cs @@ -25,6 +25,11 @@ namespace MediaBrowser.Model.Dlna /// <summary> /// Serve the subtitles as a separate HLS stream. /// </summary> - Hls = 3 + Hls = 3, + + /// <summary> + /// Drop the subtitle. + /// </summary> + Drop = 4 } } diff --git a/MediaBrowser.Model/Dlna/SubtitleProfile.cs b/MediaBrowser.Model/Dlna/SubtitleProfile.cs index 01e3c696b..9ebde25ff 100644 --- a/MediaBrowser.Model/Dlna/SubtitleProfile.cs +++ b/MediaBrowser.Model/Dlna/SubtitleProfile.cs @@ -2,8 +2,8 @@ #pragma warning disable CS1591 using System; -using System.Linq; using System.Xml.Serialization; +using Jellyfin.Extensions; namespace MediaBrowser.Model.Dlna { @@ -42,7 +42,7 @@ namespace MediaBrowser.Model.Dlna } var languages = GetLanguages(); - return languages.Length == 0 || languages.Contains(subLanguage, StringComparer.OrdinalIgnoreCase); + return languages.Length == 0 || languages.Contains(subLanguage, StringComparison.OrdinalIgnoreCase); } } } diff --git a/MediaBrowser.Model/Entities/ImageType.cs b/MediaBrowser.Model/Entities/ImageType.cs index 684b7390a..1f7e03718 100644 --- a/MediaBrowser.Model/Entities/ImageType.cs +++ b/MediaBrowser.Model/Entities/ImageType.cs @@ -1,5 +1,3 @@ -using System; - namespace MediaBrowser.Model.Entities { /// <summary> diff --git a/MediaBrowser.Model/IO/IZipClient.cs b/MediaBrowser.Model/IO/IZipClient.cs index fca52ebae..2448575d1 100644 --- a/MediaBrowser.Model/IO/IZipClient.cs +++ b/MediaBrowser.Model/IO/IZipClient.cs @@ -9,64 +9,8 @@ namespace MediaBrowser.Model.IO /// </summary> public interface IZipClient { - /// <summary> - /// Extracts all. - /// </summary> - /// <param name="sourceFile">The source file.</param> - /// <param name="targetPath">The target path.</param> - /// <param name="overwriteExistingFiles">if set to <c>true</c> [overwrite existing files].</param> - void ExtractAll(string sourceFile, string targetPath, bool overwriteExistingFiles); - - /// <summary> - /// Extracts all. - /// </summary> - /// <param name="source">The source.</param> - /// <param name="targetPath">The target path.</param> - /// <param name="overwriteExistingFiles">if set to <c>true</c> [overwrite existing files].</param> - void ExtractAll(Stream source, string targetPath, bool overwriteExistingFiles); - void ExtractAllFromGz(Stream source, string targetPath, bool overwriteExistingFiles); void ExtractFirstFileFromGz(Stream source, string targetPath, string defaultFileName); - - /// <summary> - /// Extracts all from zip. - /// </summary> - /// <param name="source">The source.</param> - /// <param name="targetPath">The target path.</param> - /// <param name="overwriteExistingFiles">if set to <c>true</c> [overwrite existing files].</param> - void ExtractAllFromZip(Stream source, string targetPath, bool overwriteExistingFiles); - - /// <summary> - /// Extracts all from7z. - /// </summary> - /// <param name="sourceFile">The source file.</param> - /// <param name="targetPath">The target path.</param> - /// <param name="overwriteExistingFiles">if set to <c>true</c> [overwrite existing files].</param> - void ExtractAllFrom7z(string sourceFile, string targetPath, bool overwriteExistingFiles); - - /// <summary> - /// Extracts all from7z. - /// </summary> - /// <param name="source">The source.</param> - /// <param name="targetPath">The target path.</param> - /// <param name="overwriteExistingFiles">if set to <c>true</c> [overwrite existing files].</param> - void ExtractAllFrom7z(Stream source, string targetPath, bool overwriteExistingFiles); - - /// <summary> - /// Extracts all from tar. - /// </summary> - /// <param name="sourceFile">The source file.</param> - /// <param name="targetPath">The target path.</param> - /// <param name="overwriteExistingFiles">if set to <c>true</c> [overwrite existing files].</param> - void ExtractAllFromTar(string sourceFile, string targetPath, bool overwriteExistingFiles); - - /// <summary> - /// Extracts all from tar. - /// </summary> - /// <param name="source">The source.</param> - /// <param name="targetPath">The target path.</param> - /// <param name="overwriteExistingFiles">if set to <c>true</c> [overwrite existing files].</param> - void ExtractAllFromTar(Stream source, string targetPath, bool overwriteExistingFiles); } } diff --git a/MediaBrowser.Model/MediaBrowser.Model.csproj b/MediaBrowser.Model/MediaBrowser.Model.csproj index 70fef5d66..a161b99fd 100644 --- a/MediaBrowser.Model/MediaBrowser.Model.csproj +++ b/MediaBrowser.Model/MediaBrowser.Model.csproj @@ -31,8 +31,12 @@ <ItemGroup> <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" /> <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="6.0.0" /> + <PackageReference Include="MimeTypes" Version="2.2.1"> + <PrivateAssets>all</PrivateAssets> + <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> + </PackageReference> <PackageReference Include="System.Globalization" Version="4.3.0" /> - <PackageReference Include="System.Text.Json" Version="6.0.0" /> + <PackageReference Include="System.Text.Json" Version="6.0.1" /> </ItemGroup> <ItemGroup> diff --git a/MediaBrowser.Model/Net/MimeTypes.cs b/MediaBrowser.Model/Net/MimeTypes.cs index 043cee2a2..3b03466e9 100644 --- a/MediaBrowser.Model/Net/MimeTypes.cs +++ b/MediaBrowser.Model/Net/MimeTypes.cs @@ -12,6 +12,15 @@ namespace MediaBrowser.Model.Net /// <summary> /// Class MimeTypes. /// </summary> + /// + /// <remarks> + /// For more information on MIME types: + /// <list type="bullet"> + /// <item>http://en.wikipedia.org/wiki/Internet_media_type</item> + /// <item>https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types</item> + /// <item>http://www.iana.org/assignments/media-types/media-types.xhtml</item> + /// </list> + /// </remarks> public static class MimeTypes { /// <summary> @@ -50,81 +59,26 @@ namespace MediaBrowser.Model.Net ".wtv", }; - // http://en.wikipedia.org/wiki/Internet_media_type - // https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types - // http://www.iana.org/assignments/media-types/media-types.xhtml - // Add more as needed + /// <summary> + /// Used for extensions not in <see cref="Model.MimeTypes"/> or to override them. + /// </summary> private static readonly Dictionary<string, string> _mimeTypeLookup = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) { // Type application - { ".7z", "application/x-7z-compressed" }, - { ".azw", "application/vnd.amazon.ebook" }, { ".azw3", "application/vnd.amazon.ebook" }, - { ".cbz", "application/x-cbz" }, - { ".cbr", "application/epub+zip" }, - { ".eot", "application/vnd.ms-fontobject" }, - { ".epub", "application/epub+zip" }, - { ".js", "application/x-javascript" }, - { ".json", "application/json" }, - { ".m3u8", "application/x-mpegURL" }, - { ".map", "application/x-javascript" }, - { ".mobi", "application/x-mobipocket-ebook" }, - { ".opf", "application/oebps-package+xml" }, - { ".pdf", "application/pdf" }, - { ".rar", "application/vnd.rar" }, - { ".srt", "application/x-subrip" }, - { ".ttml", "application/ttml+xml" }, - { ".wasm", "application/wasm" }, - { ".xml", "application/xml" }, - { ".zip", "application/zip" }, // Type image - { ".bmp", "image/bmp" }, - { ".gif", "image/gif" }, - { ".ico", "image/vnd.microsoft.icon" }, - { ".jpg", "image/jpeg" }, - { ".jpeg", "image/jpeg" }, - { ".png", "image/png" }, - { ".svg", "image/svg+xml" }, - { ".svgz", "image/svg+xml" }, { ".tbn", "image/jpeg" }, - { ".tif", "image/tiff" }, - { ".tiff", "image/tiff" }, - { ".webp", "image/webp" }, - - // Type font - { ".ttf", "font/ttf" }, - { ".woff", "font/woff" }, - { ".woff2", "font/woff2" }, // Type text { ".ass", "text/x-ssa" }, { ".ssa", "text/x-ssa" }, - { ".css", "text/css" }, - { ".csv", "text/csv" }, { ".edl", "text/plain" }, - { ".rtf", "text/rtf" }, - { ".txt", "text/plain" }, - { ".vtt", "text/vtt" }, + { ".html", "text/html; charset=UTF-8" }, + { ".htm", "text/html; charset=UTF-8" }, // Type video - { ".3gp", "video/3gpp" }, - { ".3g2", "video/3gpp2" }, - { ".asf", "video/x-ms-asf" }, - { ".avi", "video/x-msvideo" }, - { ".flv", "video/x-flv" }, - { ".mp4", "video/mp4" }, - { ".m4s", "video/mp4" }, - { ".m4v", "video/x-m4v" }, { ".mpegts", "video/mp2t" }, - { ".mpg", "video/mpeg" }, - { ".mkv", "video/x-matroska" }, - { ".mov", "video/quicktime" }, - { ".mpd", "video/vnd.mpeg.dash.mpd" }, - { ".ogv", "video/ogg" }, - { ".ts", "video/mp2t" }, - { ".webm", "video/webm" }, - { ".wmv", "video/x-ms-wmv" }, // Type audio { ".aac", "audio/aac" }, @@ -133,37 +87,47 @@ namespace MediaBrowser.Model.Net { ".dsf", "audio/dsf" }, { ".dsp", "audio/dsp" }, { ".flac", "audio/flac" }, - { ".m4a", "audio/mp4" }, { ".m4b", "audio/m4b" }, - { ".mid", "audio/midi" }, - { ".midi", "audio/midi" }, { ".mp3", "audio/mpeg" }, - { ".oga", "audio/ogg" }, - { ".ogg", "audio/ogg" }, - { ".opus", "audio/ogg" }, { ".vorbis", "audio/vorbis" }, - { ".wav", "audio/wav" }, { ".webma", "audio/webm" }, - { ".wma", "audio/x-ms-wma" }, { ".wv", "audio/x-wavpack" }, { ".xsp", "audio/xsp" }, }; - private static readonly Dictionary<string, string> _extensionLookup = CreateExtensionLookup(); - - private static Dictionary<string, string> CreateExtensionLookup() + private static readonly Dictionary<string, string> _extensionLookup = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) { - var dict = _mimeTypeLookup - .GroupBy(i => i.Value) - .ToDictionary(x => x.Key, x => x.First().Key, StringComparer.OrdinalIgnoreCase); + // Type application + { "application/x-cbz", ".cbz" }, + { "application/x-javascript", ".js" }, + { "application/xml", ".xml" }, + { "application/x-mpegURL", ".m3u8" }, - dict["image/jpg"] = ".jpg"; - dict["image/x-png"] = ".png"; + // Type audio + { "audio/aac", ".aac" }, + { "audio/ac3", ".ac3" }, + { "audio/dsf", ".dsf" }, + { "audio/dsp", ".dsp" }, + { "audio/flac", ".flac" }, + { "audio/m4b", ".m4b" }, + { "audio/vorbis", ".vorbis" }, + { "audio/x-ape", ".ape" }, + { "audio/xsp", ".xsp" }, + { "audio/x-wavpack", ".wv" }, - dict["audio/x-aac"] = ".aac"; + // Type image + { "image/jpeg", ".jpg" }, + { "image/x-png", ".png" }, - return dict; - } + // Type text + { "text/plain", ".txt" }, + { "text/rtf", ".rtf" }, + { "text/x-ssa", ".ssa" }, + + // Type video + { "video/vnd.mpeg.dash.mpd", ".mpd" }, + { "video/x-matroska", ".mkv" }, + }; public static string GetMimeType(string path) => GetMimeType(path, "application/octet-stream"); @@ -173,7 +137,7 @@ namespace MediaBrowser.Model.Net /// <param name="filename">The filename to find the MIME type of.</param> /// <param name="defaultValue">The default value to return if no fitting MIME type is found.</param> /// <returns>The correct MIME type for the given filename, or <paramref name="defaultValue"/> if it wasn't found.</returns> - [return: NotNullIfNotNullAttribute("defaultValue")] + [return: NotNullIfNotNull("defaultValue")] public static string? GetMimeType(string filename, string? defaultValue = null) { if (filename.Length == 0) @@ -188,31 +152,17 @@ namespace MediaBrowser.Model.Net return result; } + if (Model.MimeTypes.TryGetMimeType(filename, out var mimeType)) + { + return mimeType; + } + // Catch-all for all video types that don't require specific mime types if (_videoFileExtensions.Contains(ext)) { return string.Concat("video/", ext.AsSpan(1)); } - // Type text - if (string.Equals(ext, ".html", StringComparison.OrdinalIgnoreCase) - || string.Equals(ext, ".htm", StringComparison.OrdinalIgnoreCase)) - { - return "text/html; charset=UTF-8"; - } - - if (string.Equals(ext, ".log", StringComparison.OrdinalIgnoreCase) - || string.Equals(ext, ".srt", StringComparison.OrdinalIgnoreCase)) - { - return "text/plain"; - } - - // Misc - if (string.Equals(ext, ".dll", StringComparison.OrdinalIgnoreCase)) - { - return "application/octet-stream"; - } - return defaultValue; } @@ -231,7 +181,8 @@ namespace MediaBrowser.Model.Net return result; } - return null; + var extension = Model.MimeTypes.GetMimeTypeExtensions(mimeType).FirstOrDefault(); + return string.IsNullOrEmpty(extension) ? null : "." + extension; } } } diff --git a/MediaBrowser.Model/Notifications/NotificationOptions.cs b/MediaBrowser.Model/Notifications/NotificationOptions.cs index 09beb2ef7..d1b5491bd 100644 --- a/MediaBrowser.Model/Notifications/NotificationOptions.cs +++ b/MediaBrowser.Model/Notifications/NotificationOptions.cs @@ -2,9 +2,9 @@ #pragma warning disable CS1591 using System; -using System.Linq; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Extensions; namespace MediaBrowser.Model.Notifications { @@ -94,7 +94,7 @@ namespace MediaBrowser.Model.Notifications NotificationOption opt = GetOptions(notificationType); return opt == null - || !opt.DisabledServices.Contains(service, StringComparer.OrdinalIgnoreCase); + || !opt.DisabledServices.Contains(service, StringComparison.OrdinalIgnoreCase); } public bool IsEnabledToMonitorUser(string type, Guid userId) @@ -103,7 +103,7 @@ namespace MediaBrowser.Model.Notifications return opt != null && opt.Enabled - && !opt.DisabledMonitorUsers.Contains(userId.ToString("N"), StringComparer.OrdinalIgnoreCase); + && !opt.DisabledMonitorUsers.Contains(userId.ToString("N"), StringComparison.OrdinalIgnoreCase); } public bool IsEnabledToSendToUser(string type, string userId, User user) @@ -122,7 +122,7 @@ namespace MediaBrowser.Model.Notifications return true; } - return opt.SendToUsers.Contains(userId, StringComparer.OrdinalIgnoreCase); + return opt.SendToUsers.Contains(userId, StringComparison.OrdinalIgnoreCase); } return false; diff --git a/MediaBrowser.Model/Querying/LatestItemsQuery.cs b/MediaBrowser.Model/Querying/LatestItemsQuery.cs index f555ffb36..d2d9f1f9a 100644 --- a/MediaBrowser.Model/Querying/LatestItemsQuery.cs +++ b/MediaBrowser.Model/Querying/LatestItemsQuery.cs @@ -2,6 +2,7 @@ #pragma warning disable CS1591 using System; +using Jellyfin.Data.Enums; using MediaBrowser.Model.Entities; namespace MediaBrowser.Model.Querying @@ -48,7 +49,7 @@ namespace MediaBrowser.Model.Querying /// Gets or sets the include item types. /// </summary> /// <value>The include item types.</value> - public string[] IncludeItemTypes { get; set; } + public BaseItemKind[] IncludeItemTypes { get; set; } /// <summary> /// Gets or sets a value indicating whether this instance is played. diff --git a/MediaBrowser.Model/Search/SearchQuery.cs b/MediaBrowser.Model/Search/SearchQuery.cs index aedfa4d36..1caed827f 100644 --- a/MediaBrowser.Model/Search/SearchQuery.cs +++ b/MediaBrowser.Model/Search/SearchQuery.cs @@ -2,6 +2,7 @@ #pragma warning disable CS1591 using System; +using Jellyfin.Data.Enums; namespace MediaBrowser.Model.Search { @@ -16,8 +17,8 @@ namespace MediaBrowser.Model.Search IncludeStudios = true; MediaTypes = Array.Empty<string>(); - IncludeItemTypes = Array.Empty<string>(); - ExcludeItemTypes = Array.Empty<string>(); + IncludeItemTypes = Array.Empty<BaseItemKind>(); + ExcludeItemTypes = Array.Empty<BaseItemKind>(); } /// <summary> @@ -56,9 +57,9 @@ namespace MediaBrowser.Model.Search public string[] MediaTypes { get; set; } - public string[] IncludeItemTypes { get; set; } + public BaseItemKind[] IncludeItemTypes { get; set; } - public string[] ExcludeItemTypes { get; set; } + public BaseItemKind[] ExcludeItemTypes { get; set; } public Guid? ParentId { get; set; } diff --git a/MediaBrowser.Model/Session/BrowseRequest.cs b/MediaBrowser.Model/Session/BrowseRequest.cs index 65afe5cf3..5ad7d783a 100644 --- a/MediaBrowser.Model/Session/BrowseRequest.cs +++ b/MediaBrowser.Model/Session/BrowseRequest.cs @@ -1,3 +1,5 @@ +using Jellyfin.Data.Enums; + #nullable disable namespace MediaBrowser.Model.Session { @@ -8,10 +10,9 @@ namespace MediaBrowser.Model.Session { /// <summary> /// Gets or sets the item type. - /// Artist, Genre, Studio, Person, or any kind of BaseItem. /// </summary> /// <value>The type of the item.</value> - public string ItemType { get; set; } + public BaseItemKind ItemType { get; set; } /// <summary> /// Gets or sets the item id. diff --git a/MediaBrowser.Model/Session/TranscodeReason.cs b/MediaBrowser.Model/Session/TranscodeReason.cs index e93b5d288..3c95df66d 100644 --- a/MediaBrowser.Model/Session/TranscodeReason.cs +++ b/MediaBrowser.Model/Session/TranscodeReason.cs @@ -26,6 +26,7 @@ namespace MediaBrowser.Model.Session VideoProfileNotSupported = 19, AudioBitDepthNotSupported = 20, SubtitleCodecNotSupported = 21, - DirectPlayError = 22 + DirectPlayError = 22, + AudioIsExternal = 23 } } diff --git a/MediaBrowser.Providers/Manager/ImageSaver.cs b/MediaBrowser.Providers/Manager/ImageSaver.cs index d2a3344be..4632e1d51 100644 --- a/MediaBrowser.Providers/Manager/ImageSaver.cs +++ b/MediaBrowser.Providers/Manager/ImageSaver.cs @@ -9,6 +9,7 @@ using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Extensions; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; @@ -173,7 +174,7 @@ namespace MediaBrowser.Providers.Manager // Delete the current path if (currentImageIsLocalFile - && !savedPaths.Contains(currentImagePath, StringComparer.OrdinalIgnoreCase) + && !savedPaths.Contains(currentImagePath, StringComparison.OrdinalIgnoreCase) && (saveLocally || currentImagePath.Contains(_config.ApplicationPaths.InternalMetadataPath, StringComparison.OrdinalIgnoreCase))) { var currentPath = currentImagePath; @@ -494,7 +495,7 @@ namespace MediaBrowser.Providers.Manager var filenames = images.Select(i => Path.GetFileNameWithoutExtension(i.Path)).ToList(); var current = 1; - while (filenames.Contains(numberedIndexPrefix + current.ToString(CultureInfo.InvariantCulture), StringComparer.OrdinalIgnoreCase)) + while (filenames.Contains(numberedIndexPrefix + current.ToString(CultureInfo.InvariantCulture), StringComparison.OrdinalIgnoreCase)) { current++; } diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs index 90d14a973..0af76f75a 100644 --- a/MediaBrowser.Providers/Manager/MetadataService.cs +++ b/MediaBrowser.Providers/Manager/MetadataService.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Net.Http; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Controller.Configuration; @@ -679,8 +680,15 @@ namespace MediaBrowser.Providers.Manager { foreach (var remoteImage in localItem.RemoteImages) { - await ProviderManager.SaveImage(item, remoteImage.url, remoteImage.type, null, cancellationToken).ConfigureAwait(false); - refreshResult.UpdateType |= ItemUpdateType.ImageUpdate; + try + { + await ProviderManager.SaveImage(item, remoteImage.url, remoteImage.type, null, cancellationToken).ConfigureAwait(false); + refreshResult.UpdateType |= ItemUpdateType.ImageUpdate; + } + catch (HttpRequestException ex) + { + Logger.LogError(ex, "Could not save {ImageType} image: {Url}", Enum.GetName(remoteImage.type), remoteImage.url); + } } if (imageService.MergeImages(item, localItem.Images)) diff --git a/MediaBrowser.Providers/Manager/ProviderManager.cs b/MediaBrowser.Providers/Manager/ProviderManager.cs index ca557f6d6..0385ce6a7 100644 --- a/MediaBrowser.Providers/Manager/ProviderManager.cs +++ b/MediaBrowser.Providers/Manager/ProviderManager.cs @@ -11,7 +11,9 @@ using System.Net.Http; using System.Net.Mime; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Data.Enums; using Jellyfin.Data.Events; +using Jellyfin.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Common.Progress; using MediaBrowser.Controller; @@ -659,7 +661,7 @@ namespace MediaBrowser.Providers.Manager /// <inheritdoc/> public void SaveMetadata(BaseItem item, ItemUpdateType updateType, IEnumerable<string> savers) { - SaveMetadata(item, updateType, _savers.Where(i => savers.Contains(i.Name, StringComparer.OrdinalIgnoreCase))); + SaveMetadata(item, updateType, _savers.Where(i => savers.Contains(i.Name, StringComparison.OrdinalIgnoreCase))); } /// <summary> @@ -736,7 +738,7 @@ namespace MediaBrowser.Providers.Manager { if (libraryOptions.MetadataSavers == null) { - if (options.DisabledMetadataSavers.Contains(saver.Name, StringComparer.OrdinalIgnoreCase)) + if (options.DisabledMetadataSavers.Contains(saver.Name, StringComparison.OrdinalIgnoreCase)) { return false; } @@ -762,7 +764,7 @@ namespace MediaBrowser.Providers.Manager } else { - if (!libraryOptions.MetadataSavers.Contains(saver.Name, StringComparer.OrdinalIgnoreCase)) + if (!libraryOptions.MetadataSavers.Contains(saver.Name, StringComparison.OrdinalIgnoreCase)) { return false; } @@ -1133,7 +1135,7 @@ namespace MediaBrowser.Providers.Manager var albums = _libraryManager .GetItemList(new InternalItemsQuery { - IncludeItemTypes = new[] { nameof(MusicAlbum) }, + IncludeItemTypes = new[] { BaseItemKind.MusicAlbum }, ArtistIds = new[] { item.Id }, DtoOptions = new DtoOptions(false) { diff --git a/MediaBrowser.Providers/MediaBrowser.Providers.csproj b/MediaBrowser.Providers/MediaBrowser.Providers.csproj index b42112111..dac5aaf56 100644 --- a/MediaBrowser.Providers/MediaBrowser.Providers.csproj +++ b/MediaBrowser.Providers/MediaBrowser.Providers.csproj @@ -46,6 +46,8 @@ <EmbeddedResource Include="Plugins\Omdb\Configuration\config.html" /> <None Remove="Plugins\MusicBrainz\Configuration\config.html" /> <EmbeddedResource Include="Plugins\MusicBrainz\Configuration\config.html" /> + <None Remove="Plugins\StudioImages\Configuration\config.html" /> + <EmbeddedResource Include="Plugins\StudioImages\Configuration\config.html" /> <None Remove="Plugins\Tmdb\Configuration\config.html" /> <EmbeddedResource Include="Plugins\Tmdb\Configuration\config.html" /> </ItemGroup> diff --git a/MediaBrowser.Providers/MediaInfo/AudioResolver.cs b/MediaBrowser.Providers/MediaInfo/AudioResolver.cs new file mode 100644 index 000000000..425913501 --- /dev/null +++ b/MediaBrowser.Providers/MediaInfo/AudioResolver.cs @@ -0,0 +1,176 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using Emby.Naming.Audio; +using Emby.Naming.Common; +using Jellyfin.Extensions; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.MediaEncoding; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Dlna; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Globalization; +using MediaBrowser.Model.MediaInfo; + +namespace MediaBrowser.Providers.MediaInfo +{ + /// <summary> + /// Resolves external audios for videos. + /// </summary> + public class AudioResolver + { + private readonly ILocalizationManager _localizationManager; + private readonly IMediaEncoder _mediaEncoder; + private readonly NamingOptions _namingOptions; + + /// <summary> + /// Initializes a new instance of the <see cref="AudioResolver"/> class. + /// </summary> + /// <param name="localizationManager">The localization manager.</param> + /// <param name="mediaEncoder">The media encoder.</param> + /// <param name="namingOptions">The naming options.</param> + public AudioResolver( + ILocalizationManager localizationManager, + IMediaEncoder mediaEncoder, + NamingOptions namingOptions) + { + _localizationManager = localizationManager; + _mediaEncoder = mediaEncoder; + _namingOptions = namingOptions; + } + + /// <summary> + /// Returns the audio streams found in the external audio files for the given video. + /// </summary> + /// <param name="video">The video to get the external audio streams from.</param> + /// <param name="startIndex">The stream index to start adding audio streams at.</param> + /// <param name="directoryService">The directory service to search for files.</param> + /// <param name="clearCache">True if the directory service cache should be cleared before searching.</param> + /// <param name="cancellationToken">The cancellation token to cancel operation.</param> + /// <returns>A list of external audio streams.</returns> + public async IAsyncEnumerable<MediaStream> GetExternalAudioStreams( + Video video, + int startIndex, + IDirectoryService directoryService, + bool clearCache, + [EnumeratorCancellation] CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + if (!video.IsFileProtocol) + { + yield break; + } + + IEnumerable<string> paths = GetExternalAudioFiles(video, directoryService, clearCache); + foreach (string path in paths) + { + string fileNameWithoutExtension = Path.GetFileNameWithoutExtension(path); + Model.MediaInfo.MediaInfo mediaInfo = await GetMediaInfo(path, cancellationToken).ConfigureAwait(false); + + foreach (MediaStream mediaStream in mediaInfo.MediaStreams) + { + mediaStream.Index = startIndex++; + mediaStream.Type = MediaStreamType.Audio; + mediaStream.IsExternal = true; + mediaStream.Path = path; + mediaStream.IsDefault = false; + mediaStream.Title = null; + + if (string.IsNullOrEmpty(mediaStream.Language)) + { + // Try to translate to three character code + // Be flexible and check against both the full and three character versions + var language = StringExtensions.RightPart(fileNameWithoutExtension, '.').ToString(); + + if (language != fileNameWithoutExtension) + { + var culture = _localizationManager.FindLanguageInfo(language); + + language = culture == null ? language : culture.ThreeLetterISOLanguageName; + mediaStream.Language = language; + } + } + + yield return mediaStream; + } + } + } + + /// <summary> + /// Returns the external audio file paths for the given video. + /// </summary> + /// <param name="video">The video to get the external audio file paths from.</param> + /// <param name="directoryService">The directory service to search for files.</param> + /// <param name="clearCache">True if the directory service cache should be cleared before searching.</param> + /// <returns>A list of external audio file paths.</returns> + public IEnumerable<string> GetExternalAudioFiles( + Video video, + IDirectoryService directoryService, + bool clearCache) + { + if (!video.IsFileProtocol) + { + yield break; + } + + // Check if video folder exists + string folder = video.ContainingFolderPath; + if (!Directory.Exists(folder)) + { + yield break; + } + + string videoFileNameWithoutExtension = Path.GetFileNameWithoutExtension(video.Path); + + var files = directoryService.GetFilePaths(folder, clearCache, true); + for (int i = 0; i < files.Count; i++) + { + string file = files[i]; + if (string.Equals(video.Path, file, StringComparison.OrdinalIgnoreCase) + || !AudioFileParser.IsAudioFile(file, _namingOptions) + || Path.GetExtension(file.AsSpan()).Equals(".strm", StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + string fileNameWithoutExtension = Path.GetFileNameWithoutExtension(file); + // The audio filename must either be equal to the video filename or start with the video filename followed by a dot + if (videoFileNameWithoutExtension.Equals(fileNameWithoutExtension, StringComparison.OrdinalIgnoreCase) + || (fileNameWithoutExtension.Length > videoFileNameWithoutExtension.Length + && fileNameWithoutExtension[videoFileNameWithoutExtension.Length] == '.' + && fileNameWithoutExtension.StartsWith(videoFileNameWithoutExtension, StringComparison.OrdinalIgnoreCase))) + { + yield return file; + } + } + } + + /// <summary> + /// Returns the media info of the given audio file. + /// </summary> + /// <param name="path">The path to the audio file.</param> + /// <param name="cancellationToken">The cancellation token to cancel operation.</param> + /// <returns>The media info for the given audio file.</returns> + private Task<Model.MediaInfo.MediaInfo> GetMediaInfo(string path, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + return _mediaEncoder.GetMediaInfo( + new MediaInfoRequest + { + MediaType = DlnaProfileType.Audio, + MediaSource = new MediaSourceInfo + { + Path = path, + Protocol = MediaProtocol.File + } + }, + cancellationToken); + } + } +} diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeProvider.cs b/MediaBrowser.Providers/MediaInfo/FFProbeProvider.cs index d4b5d8655..19a435196 100644 --- a/MediaBrowser.Providers/MediaInfo/FFProbeProvider.cs +++ b/MediaBrowser.Providers/MediaInfo/FFProbeProvider.cs @@ -7,6 +7,7 @@ using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Emby.Naming.Common; using MediaBrowser.Controller.Chapters; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; @@ -38,17 +39,10 @@ namespace MediaBrowser.Providers.MediaInfo IHasItemChangeMonitor { private readonly ILogger<FFProbeProvider> _logger; - private readonly IMediaEncoder _mediaEncoder; - private readonly IItemRepository _itemRepo; - private readonly IBlurayExaminer _blurayExaminer; - private readonly ILocalizationManager _localization; - private readonly IEncodingManager _encodingManager; - private readonly IServerConfigurationManager _config; - private readonly ISubtitleManager _subtitleManager; - private readonly IChapterManager _chapterManager; - private readonly ILibraryManager _libraryManager; - private readonly IMediaSourceManager _mediaSourceManager; private readonly SubtitleResolver _subtitleResolver; + private readonly AudioResolver _audioResolver; + private readonly FFProbeVideoInfo _videoProber; + private readonly FFProbeAudioInfo _audioProber; private readonly Task<ItemUpdateType> _cachedTask = Task.FromResult(ItemUpdateType.None); @@ -63,21 +57,26 @@ namespace MediaBrowser.Providers.MediaInfo IServerConfigurationManager config, ISubtitleManager subtitleManager, IChapterManager chapterManager, - ILibraryManager libraryManager) + ILibraryManager libraryManager, + NamingOptions namingOptions) { _logger = logger; - _mediaEncoder = mediaEncoder; - _itemRepo = itemRepo; - _blurayExaminer = blurayExaminer; - _localization = localization; - _encodingManager = encodingManager; - _config = config; - _subtitleManager = subtitleManager; - _chapterManager = chapterManager; - _libraryManager = libraryManager; - _mediaSourceManager = mediaSourceManager; - + _audioResolver = new AudioResolver(localization, mediaEncoder, namingOptions); _subtitleResolver = new SubtitleResolver(BaseItem.LocalizationManager); + _videoProber = new FFProbeVideoInfo( + _logger, + mediaSourceManager, + mediaEncoder, + itemRepo, + blurayExaminer, + localization, + encodingManager, + config, + subtitleManager, + chapterManager, + libraryManager, + _audioResolver); + _audioProber = new FFProbeAudioInfo(mediaSourceManager, mediaEncoder, itemRepo, libraryManager); } public string Name => "ffprobe"; @@ -97,7 +96,7 @@ namespace MediaBrowser.Providers.MediaInfo var file = directoryService.GetFile(path); if (file != null && file.LastWriteTimeUtc != item.DateModified) { - _logger.LogDebug("Refreshing {0} due to date modified timestamp change.", path); + _logger.LogDebug("Refreshing {ItemPath} due to date modified timestamp change.", path); return true; } } @@ -107,7 +106,15 @@ namespace MediaBrowser.Providers.MediaInfo && !video.SubtitleFiles.SequenceEqual( _subtitleResolver.GetExternalSubtitleFiles(video, directoryService, false), StringComparer.Ordinal)) { - _logger.LogDebug("Refreshing {0} due to external subtitles change.", item.Path); + _logger.LogDebug("Refreshing {ItemPath} due to external subtitles change.", item.Path); + return true; + } + + if (item.SupportsLocalMetadata && video != null && !video.IsPlaceHolder + && !video.AudioFiles.SequenceEqual( + _audioResolver.GetExternalAudioFiles(video, directoryService, false), StringComparer.Ordinal)) + { + _logger.LogDebug("Refreshing {ItemPath} due to external audio change.", item.Path); return true; } @@ -177,20 +184,7 @@ namespace MediaBrowser.Providers.MediaInfo FetchShortcutInfo(item); } - var prober = new FFProbeVideoInfo( - _logger, - _mediaSourceManager, - _mediaEncoder, - _itemRepo, - _blurayExaminer, - _localization, - _encodingManager, - _config, - _subtitleManager, - _chapterManager, - _libraryManager); - - return prober.ProbeVideo(item, options, cancellationToken); + return _videoProber.ProbeVideo(item, options, cancellationToken); } private string NormalizeStrmLine(string line) @@ -226,9 +220,7 @@ namespace MediaBrowser.Providers.MediaInfo FetchShortcutInfo(item); } - var prober = new FFProbeAudioInfo(_mediaSourceManager, _mediaEncoder, _itemRepo, _libraryManager); - - return prober.Probe(item, options, cancellationToken); + return _audioProber.Probe(item, options, cancellationToken); } } } diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs index 4ab15f60e..77372e063 100644 --- a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs +++ b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs @@ -44,6 +44,7 @@ namespace MediaBrowser.Providers.MediaInfo private readonly ISubtitleManager _subtitleManager; private readonly IChapterManager _chapterManager; private readonly ILibraryManager _libraryManager; + private readonly AudioResolver _audioResolver; private readonly IMediaSourceManager _mediaSourceManager; private readonly long _dummyChapterDuration = TimeSpan.FromMinutes(5).Ticks; @@ -59,7 +60,8 @@ namespace MediaBrowser.Providers.MediaInfo IServerConfigurationManager config, ISubtitleManager subtitleManager, IChapterManager chapterManager, - ILibraryManager libraryManager) + ILibraryManager libraryManager, + AudioResolver audioResolver) { _logger = logger; _mediaEncoder = mediaEncoder; @@ -71,6 +73,7 @@ namespace MediaBrowser.Providers.MediaInfo _subtitleManager = subtitleManager; _chapterManager = chapterManager; _libraryManager = libraryManager; + _audioResolver = audioResolver; _mediaSourceManager = mediaSourceManager; } @@ -214,6 +217,8 @@ namespace MediaBrowser.Providers.MediaInfo await AddExternalSubtitles(video, mediaStreams, options, cancellationToken).ConfigureAwait(false); + await AddExternalAudioAsync(video, mediaStreams, options, cancellationToken).ConfigureAwait(false); + var libraryOptions = _libraryManager.GetLibraryOptions(video); if (mediaInfo != null) @@ -574,6 +579,31 @@ namespace MediaBrowser.Providers.MediaInfo currentStreams.AddRange(externalSubtitleStreams); } + /// <summary> + /// Adds the external audio. + /// </summary> + /// <param name="video">The video.</param> + /// <param name="currentStreams">The current streams.</param> + /// <param name="options">The refreshOptions.</param> + /// <param name="cancellationToken">The cancellation token.</param> + private async Task AddExternalAudioAsync( + Video video, + List<MediaStream> currentStreams, + MetadataRefreshOptions options, + CancellationToken cancellationToken) + { + var startIndex = currentStreams.Count == 0 ? 0 : currentStreams.Max(i => i.Index) + 1; + var externalAudioStreams = _audioResolver.GetExternalAudioStreams(video, startIndex, options.DirectoryService, false, cancellationToken); + + await foreach (MediaStream externalAudioStream in externalAudioStreams) + { + currentStreams.Add(externalAudioStream); + } + + // Select all external audio file paths + video.AudioFiles = currentStreams.Where(i => i.Type == MediaStreamType.Audio && i.IsExternal).Select(i => i.Path).Distinct().ToArray(); + } + /// <summary> /// Creates dummy chapters. /// </summary> diff --git a/MediaBrowser.Providers/MediaInfo/SubtitleResolver.cs b/MediaBrowser.Providers/MediaInfo/SubtitleResolver.cs index dd4a5f061..ba284187e 100644 --- a/MediaBrowser.Providers/MediaInfo/SubtitleResolver.cs +++ b/MediaBrowser.Providers/MediaInfo/SubtitleResolver.cs @@ -1,7 +1,3 @@ -#nullable disable - -#pragma warning disable CA1002, CS1591 - using System; using System.Collections.Generic; using System.IO; @@ -12,15 +8,30 @@ using MediaBrowser.Model.Globalization; namespace MediaBrowser.Providers.MediaInfo { + /// <summary> + /// Resolves external subtitles for videos. + /// </summary> public class SubtitleResolver { private readonly ILocalizationManager _localization; + /// <summary> + /// Initializes a new instance of the <see cref="SubtitleResolver"/> class. + /// </summary> + /// <param name="localization">The localization manager.</param> public SubtitleResolver(ILocalizationManager localization) { _localization = localization; } + /// <summary> + /// Retrieves the external subtitle streams for the provided video. + /// </summary> + /// <param name="video">The video to search from.</param> + /// <param name="startIndex">The stream index to start adding subtitle streams at.</param> + /// <param name="directoryService">The directory service to search for files.</param> + /// <param name="clearCache">True if the directory service cache should be cleared before searching.</param> + /// <returns>The external subtitle streams located.</returns> public List<MediaStream> GetExternalSubtitleStreams( Video video, int startIndex, @@ -56,6 +67,13 @@ namespace MediaBrowser.Providers.MediaInfo return streams; } + /// <summary> + /// Locates the external subtitle files for the provided video. + /// </summary> + /// <param name="video">The video to search from.</param> + /// <param name="directoryService">The directory service to search for files.</param> + /// <param name="clearCache">True if the directory service cache should be cleared before searching.</param> + /// <returns>The external subtitle file paths located.</returns> public IEnumerable<string> GetExternalSubtitleFiles( Video video, IDirectoryService directoryService, @@ -74,6 +92,13 @@ namespace MediaBrowser.Providers.MediaInfo } } + /// <summary> + /// Extracts the subtitle files from the provided list and adds them to the list of streams. + /// </summary> + /// <param name="streams">The list of streams to add external subtitles to.</param> + /// <param name="videoPath">The path to the video file.</param> + /// <param name="startIndex">The stream index to start adding subtitle streams at.</param> + /// <param name="files">The files to add if they are subtitles.</param> public void AddExternalSubtitleStreams( List<MediaStream> streams, string videoPath, @@ -120,6 +145,12 @@ namespace MediaBrowser.Providers.MediaInfo while (languageSpan.Length > 0) { var lastDot = languageSpan.LastIndexOf('.'); + if (lastDot < videoFileNameWithoutExtension.Length) + { + languageSpan = ReadOnlySpan<char>.Empty; + break; + } + var currentSlice = languageSpan[lastDot..]; if (currentSlice.Equals(".default", StringComparison.OrdinalIgnoreCase) || currentSlice.Equals(".forced", StringComparison.OrdinalIgnoreCase) @@ -133,12 +164,19 @@ namespace MediaBrowser.Providers.MediaInfo break; } - // Try to translate to three character code - // Be flexible and check against both the full and three character versions var language = languageSpan.ToString(); - var culture = _localization.FindLanguageInfo(language); + if (string.IsNullOrWhiteSpace(language)) + { + language = null; + } + else + { + // Try to translate to three character code + // Be flexible and check against both the full and three character versions + var culture = _localization.FindLanguageInfo(language); - language = culture == null ? language : culture.ThreeLetterISOLanguageName; + language = culture == null ? language : culture.ThreeLetterISOLanguageName; + } mediaStream = new MediaStream { diff --git a/MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs b/MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs index 1eacbf1e1..58651d42a 100644 --- a/MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs +++ b/MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs @@ -7,6 +7,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Data.Enums; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Dto; @@ -66,7 +67,7 @@ namespace MediaBrowser.Providers.MediaInfo { var options = GetOptions(); - var types = new[] { "Episode", "Movie" }; + var types = new[] { BaseItemKind.Episode, BaseItemKind.Movie }; var dict = new Dictionary<Guid, BaseItem>(); @@ -77,21 +78,18 @@ namespace MediaBrowser.Providers.MediaInfo string[] subtitleDownloadLanguages; bool skipIfEmbeddedSubtitlesPresent; bool skipIfAudioTrackMatches; - bool requirePerfectMatch; if (libraryOptions.SubtitleDownloadLanguages == null) { subtitleDownloadLanguages = options.DownloadLanguages; skipIfEmbeddedSubtitlesPresent = options.SkipIfEmbeddedSubtitlesPresent; skipIfAudioTrackMatches = options.SkipIfAudioTrackMatches; - requirePerfectMatch = options.RequirePerfectMatch; } else { subtitleDownloadLanguages = libraryOptions.SubtitleDownloadLanguages; skipIfEmbeddedSubtitlesPresent = libraryOptions.SkipSubtitlesIfEmbeddedSubtitlesPresent; skipIfAudioTrackMatches = libraryOptions.SkipSubtitlesIfAudioTrackMatches; - requirePerfectMatch = libraryOptions.RequirePerfectSubtitleMatch; } foreach (var lang in subtitleDownloadLanguages) diff --git a/MediaBrowser.Providers/Playlists/PlaylistItemsProvider.cs b/MediaBrowser.Providers/Playlists/PlaylistItemsProvider.cs index 8baca30c6..fe9986d42 100644 --- a/MediaBrowser.Providers/Playlists/PlaylistItemsProvider.cs +++ b/MediaBrowser.Providers/Playlists/PlaylistItemsProvider.cs @@ -8,6 +8,7 @@ using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Extensions; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Playlists; @@ -44,7 +45,7 @@ namespace MediaBrowser.Providers.Playlists } var extension = Path.GetExtension(path); - if (!Playlist.SupportedExtensions.Contains(extension ?? string.Empty, StringComparer.OrdinalIgnoreCase)) + if (!Playlist.SupportedExtensions.Contains(extension ?? string.Empty, StringComparison.OrdinalIgnoreCase)) { return Task.FromResult(ItemUpdateType.None); } diff --git a/MediaBrowser.Providers/Plugins/Omdb/OmdbImageProvider.cs b/MediaBrowser.Providers/Plugins/Omdb/OmdbImageProvider.cs index 4c3fc23b2..60b373483 100644 --- a/MediaBrowser.Providers/Plugins/Omdb/OmdbImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/Omdb/OmdbImageProvider.cs @@ -2,7 +2,6 @@ #pragma warning disable CS1591 -using System; using System.Collections.Generic; using System.Linq; using System.Net.Http; diff --git a/MediaBrowser.Providers/Plugins/StudioImages/Configuration/PluginConfiguration.cs b/MediaBrowser.Providers/Plugins/StudioImages/Configuration/PluginConfiguration.cs new file mode 100644 index 000000000..fad989ab4 --- /dev/null +++ b/MediaBrowser.Providers/Plugins/StudioImages/Configuration/PluginConfiguration.cs @@ -0,0 +1,24 @@ +#pragma warning disable CS1591 + +using MediaBrowser.Model.Plugins; + +namespace MediaBrowser.Providers.Plugins.StudioImages +{ + public class PluginConfiguration : BasePluginConfiguration + { + private string _repository = Plugin.DefaultServer; + + public string RepositoryUrl + { + get + { + return _repository; + } + + set + { + _repository = value.TrimEnd('/'); + } + } + } +} diff --git a/MediaBrowser.Providers/Plugins/StudioImages/Configuration/config.html b/MediaBrowser.Providers/Plugins/StudioImages/Configuration/config.html new file mode 100644 index 000000000..f9fe3dc2e --- /dev/null +++ b/MediaBrowser.Providers/Plugins/StudioImages/Configuration/config.html @@ -0,0 +1,58 @@ +<!DOCTYPE html> +<html> +<head> + <title>Studio Images + + +
+
+
+
+
+ +
This can be any Jellyfin-compatible artwork repository.
+
+
+
+ +
+
+
+
+ +
+ + diff --git a/MediaBrowser.Providers/Plugins/StudioImages/Plugin.cs b/MediaBrowser.Providers/Plugins/StudioImages/Plugin.cs new file mode 100644 index 000000000..e0ab31b56 --- /dev/null +++ b/MediaBrowser.Providers/Plugins/StudioImages/Plugin.cs @@ -0,0 +1,44 @@ +#nullable disable +#pragma warning disable CS1591 + +using System; +using System.Collections.Generic; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Plugins; +using MediaBrowser.Model.Plugins; +using MediaBrowser.Model.Serialization; + +namespace MediaBrowser.Providers.Plugins.StudioImages +{ + public class Plugin : BasePlugin, IHasWebPages + { + // TODO change this for a Jellyfin-hosted repository. + public const string DefaultServer = "https://raw.github.com/MediaBrowser/MediaBrowser.Resources/master/images/imagesbyname"; + + public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer) + : base(applicationPaths, xmlSerializer) + { + Instance = this; + } + + public static Plugin Instance { get; private set; } + + public override Guid Id => new Guid("872a7849-1171-458d-a6fb-3de3d442ad30"); + + public override string Name => "Studio Images"; + + public override string Description => "Get artwork for studios from any Jellyfin-compatible repository."; + + // TODO remove when plugin removed from server. + public override string ConfigurationFileName => "Jellyfin.Plugin.StudioImages.xml"; + + public IEnumerable GetPages() + { + yield return new PluginPageInfo + { + Name = Name, + EmbeddedResourcePath = GetType().Namespace + ".Configuration.config.html" + }; + } + } +} diff --git a/MediaBrowser.Providers/Studios/StudiosImageProvider.cs b/MediaBrowser.Providers/Plugins/StudioImages/StudiosImageProvider.cs similarity index 93% rename from MediaBrowser.Providers/Studios/StudiosImageProvider.cs rename to MediaBrowser.Providers/Plugins/StudioImages/StudiosImageProvider.cs index 7f1665532..6fa34b985 100644 --- a/MediaBrowser.Providers/Studios/StudiosImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/StudioImages/StudiosImageProvider.cs @@ -18,6 +18,7 @@ using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; using MediaBrowser.Model.Providers; +using MediaBrowser.Providers.Plugins.StudioImages; namespace MediaBrowser.Providers.Studios { @@ -26,15 +27,17 @@ namespace MediaBrowser.Providers.Studios private readonly IServerConfigurationManager _config; private readonly IHttpClientFactory _httpClientFactory; private readonly IFileSystem _fileSystem; + private readonly String repositoryUrl; public StudiosImageProvider(IServerConfigurationManager config, IHttpClientFactory httpClientFactory, IFileSystem fileSystem) { _config = config; _httpClientFactory = httpClientFactory; _fileSystem = fileSystem; + repositoryUrl = Plugin.Instance.Configuration.RepositoryUrl; } - public string Name => "Emby Designs"; + public string Name => "Artwork Repository"; public int Order => 0; @@ -107,19 +110,19 @@ namespace MediaBrowser.Providers.Studios private string GetUrl(string image, string filename) { - return string.Format(CultureInfo.InvariantCulture, "https://raw.github.com/MediaBrowser/MediaBrowser.Resources/master/images/imagesbyname/studios/{0}/{1}.jpg", image, filename); + return string.Format(CultureInfo.InvariantCulture, "{0}/{1}/{2}.jpg", repositoryUrl, image, filename); } private Task EnsureThumbsList(string file, CancellationToken cancellationToken) { - const string url = "https://raw.github.com/MediaBrowser/MediaBrowser.Resources/master/images/imagesbyname/studiothumbs.txt"; + string url = string.Format(CultureInfo.InvariantCulture, "{0}/studiothumbs.txt", repositoryUrl); return EnsureList(url, file, _fileSystem, cancellationToken); } private Task EnsurePosterList(string file, CancellationToken cancellationToken) { - const string url = "https://raw.github.com/MediaBrowser/MediaBrowser.Resources/master/images/imagesbyname/studioposters.txt"; + string url = string.Format(CultureInfo.InvariantCulture, "{0}/studioposters.txt", repositoryUrl); return EnsureList(url, file, _fileSystem, cancellationToken); } diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Api/TmdbController.cs b/MediaBrowser.Providers/Plugins/Tmdb/Api/TmdbController.cs new file mode 100644 index 000000000..0bab7c3ca --- /dev/null +++ b/MediaBrowser.Providers/Plugins/Tmdb/Api/TmdbController.cs @@ -0,0 +1,41 @@ +using System.Net.Mime; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using TMDbLib.Objects.General; + +namespace MediaBrowser.Providers.Plugins.Tmdb.Api +{ + /// + /// The TMDb api controller. + /// + [ApiController] + [Authorize(Policy = "DefaultAuthorization")] + [Route("[controller]")] + [Produces(MediaTypeNames.Application.Json)] + public class TmdbController : ControllerBase + { + private readonly TmdbClientManager _tmdbClientManager; + + /// + /// Initializes a new instance of the class. + /// + /// The TMDb client manager. + public TmdbController(TmdbClientManager tmdbClientManager) + { + _tmdbClientManager = tmdbClientManager; + } + + /// + /// Gets the TMDb image configuration options. + /// + /// The image portion of the TMDb client configuration. + [HttpGet("ClientConfiguration")] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task TmdbClientConfiguration() + { + return (await _tmdbClientManager.GetClientConfiguration().ConfigureAwait(false)).Images; + } + } +} diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Configuration/PluginConfiguration.cs b/MediaBrowser.Providers/Plugins/Tmdb/Configuration/PluginConfiguration.cs index 9a78a7536..dec796148 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/Configuration/PluginConfiguration.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/Configuration/PluginConfiguration.cs @@ -26,5 +26,25 @@ namespace MediaBrowser.Providers.Plugins.Tmdb /// Gets or sets a value indicating the maximum number of cast members to fetch for an item. ///
public int MaxCastMembers { get; set; } = 15; + + /// + /// Gets or sets a value indicating the poster image size to fetch. + /// + public string? PosterSize { get; set; } + + /// + /// Gets or sets a value indicating the backdrop image size to fetch. + /// + public string? BackdropSize { get; set; } + + /// + /// Gets or sets a value indicating the profile image size to fetch. + /// + public string? ProfileSize { get; set; } + + /// + /// Gets or sets a value indicating the still image size to fetch. + /// + public string? StillSize { get; set; } } } diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Configuration/config.html b/MediaBrowser.Providers/Plugins/Tmdb/Configuration/config.html index 12b4c7ca4..52693795b 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/Configuration/config.html +++ b/MediaBrowser.Providers/Plugins/Tmdb/Configuration/config.html @@ -24,7 +24,21 @@
The maximum number of cast members to fetch for an item.
-
+
+

Image Scaling

+
+ +
+
+ +
+
+ +
+
+ +
+
@@ -39,6 +53,47 @@ document.querySelector('.configPage') .addEventListener('pageshow', function () { Dashboard.showLoadingMsg(); + + var clientConfig, pluginConfig; + var configureImageScaling = function() { + if (clientConfig === null || pluginConfig === null) { + return; + } + + var sizeOptionsGenerator = function (size) { + return ''; + } + + var selPosterSize = document.querySelector('#selectPosterSize'); + selPosterSize.innerHTML = clientConfig.PosterSizes.map(sizeOptionsGenerator); + selPosterSize.value = pluginConfig.PosterSize; + + var selBackdropSize = document.querySelector('#selectBackdropSize'); + selBackdropSize.innerHTML = clientConfig.BackdropSizes.map(sizeOptionsGenerator); + selBackdropSize.value = pluginConfig.BackdropSize; + + var selProfileSize = document.querySelector('#selectProfileSize'); + selProfileSize.innerHTML = clientConfig.ProfileSizes.map(sizeOptionsGenerator); + selProfileSize.value = pluginConfig.ProfileSize; + + var selStillSize = document.querySelector('#selectStillSize'); + selStillSize.innerHTML = clientConfig.StillSizes.map(sizeOptionsGenerator); + selStillSize.value = pluginConfig.StillSize; + + Dashboard.hideLoadingMsg(); + } + + const request = { + url: ApiClient.getUrl('tmdb/ClientConfiguration'), + dataType: 'json', + type: 'GET', + headers: { accept: 'application/json' } + } + ApiClient.fetch(request).then(function (config) { + clientConfig = config; + configureImageScaling(); + }); + ApiClient.getPluginConfiguration(PluginConfig.pluginId).then(function (config) { document.querySelector('#includeAdult').checked = config.IncludeAdult; document.querySelector('#excludeTagsSeries').checked = config.ExcludeTagsSeries; @@ -51,7 +106,8 @@ cancelable: false })); - Dashboard.hideLoadingMsg(); + pluginConfig = config; + configureImageScaling(); }); }); @@ -65,6 +121,10 @@ config.ExcludeTagsSeries = document.querySelector('#excludeTagsSeries').checked; config.ExcludeTagsMovies = document.querySelector('#excludeTagsMovies').checked; config.MaxCastMembers = document.querySelector('#maxCastMembers').value; + config.PosterSize = document.querySelector('#selectPosterSize').value; + config.BackdropSize = document.querySelector('#selectBackdropSize').value; + config.ProfileSize = document.querySelector('#selectProfileSize').value; + config.StillSize = document.querySelector('#selectStillSize').value; ApiClient.updatePluginConfiguration(PluginConfig.pluginId, config).then(Dashboard.processPluginConfigurationUpdateResult); }); diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs index fcaacc90d..e4a56fde9 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs @@ -9,6 +9,7 @@ using System.Linq; using System.Net.Http; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Movies; @@ -279,8 +280,8 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies // Normalize this var type = TmdbUtils.MapCrewToPersonType(person); - if (!keepTypes.Contains(type, StringComparer.OrdinalIgnoreCase) && - !keepTypes.Contains(person.Job ?? string.Empty, StringComparer.OrdinalIgnoreCase)) + if (!keepTypes.Contains(type, StringComparison.OrdinalIgnoreCase) && + !keepTypes.Contains(person.Job ?? string.Empty, StringComparison.OrdinalIgnoreCase)) { continue; } diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs index 8ac9d0cab..f50f15877 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs @@ -9,6 +9,7 @@ using System.Linq; using System.Net.Http; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.TV; @@ -188,8 +189,8 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV // Normalize this var type = TmdbUtils.MapCrewToPersonType(person); - if (!TmdbUtils.WantedCrewTypes.Contains(type, StringComparer.OrdinalIgnoreCase) - && !TmdbUtils.WantedCrewTypes.Contains(person.Job ?? string.Empty, StringComparer.OrdinalIgnoreCase)) + if (!TmdbUtils.WantedCrewTypes.Contains(type, StringComparison.OrdinalIgnoreCase) + && !TmdbUtils.WantedCrewTypes.Contains(person.Job ?? string.Empty, StringComparison.OrdinalIgnoreCase)) { continue; } diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs index 7afaddc24..27c52a5a2 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs @@ -7,6 +7,7 @@ using System.Linq; using System.Net.Http; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.TV; @@ -87,8 +88,8 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV // Normalize this var type = TmdbUtils.MapCrewToPersonType(person); - if (!TmdbUtils.WantedCrewTypes.Contains(type, StringComparer.OrdinalIgnoreCase) - && !TmdbUtils.WantedCrewTypes.Contains(person.Job ?? string.Empty, StringComparer.OrdinalIgnoreCase)) + if (!TmdbUtils.WantedCrewTypes.Contains(type, StringComparison.OrdinalIgnoreCase) + && !TmdbUtils.WantedCrewTypes.Contains(person.Job ?? string.Empty, StringComparison.OrdinalIgnoreCase)) { continue; } diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs index 77e22ffbf..f565b6569 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs @@ -9,6 +9,7 @@ using System.Linq; using System.Net.Http; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.TV; @@ -365,8 +366,8 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV // Normalize this var type = TmdbUtils.MapCrewToPersonType(person); - if (!keepTypes.Contains(type, StringComparer.OrdinalIgnoreCase) - && !keepTypes.Contains(person.Job ?? string.Empty, StringComparer.OrdinalIgnoreCase)) + if (!keepTypes.Contains(type, StringComparison.OrdinalIgnoreCase) + && !keepTypes.Contains(person.Job ?? string.Empty, StringComparison.OrdinalIgnoreCase)) { continue; } diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs b/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs index cb644c8ca..28d6f4d0c 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs @@ -498,7 +498,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb return null; } - return _tmDbClient.GetImageUrl(size, path).ToString(); + return _tmDbClient.GetImageUrl(size, path, true).ToString(); } /// @@ -508,7 +508,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb /// The absolute URL. public string GetPosterUrl(string posterPath) { - return GetUrl(_tmDbClient.Config.Images.PosterSizes[^1], posterPath); + return GetUrl(Plugin.Instance.Configuration.PosterSize, posterPath); } /// @@ -518,7 +518,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb /// The absolute URL. public string GetProfileUrl(string actorProfilePath) { - return GetUrl(_tmDbClient.Config.Images.ProfileSizes[^1], actorProfilePath); + return GetUrl(Plugin.Instance.Configuration.ProfileSize, actorProfilePath); } /// @@ -529,7 +529,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb /// The collection to add the remote images into. public void ConvertPostersToRemoteImageInfo(List images, string requestLanguage, List results) { - ConvertToRemoteImageInfo(images, _tmDbClient.Config.Images.PosterSizes[^1], ImageType.Primary, requestLanguage, results); + ConvertToRemoteImageInfo(images, Plugin.Instance.Configuration.PosterSize, ImageType.Primary, requestLanguage, results); } /// @@ -540,7 +540,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb /// The collection to add the remote images into. public void ConvertBackdropsToRemoteImageInfo(List images, string requestLanguage, List results) { - ConvertToRemoteImageInfo(images, _tmDbClient.Config.Images.BackdropSizes[^1], ImageType.Backdrop, requestLanguage, results); + ConvertToRemoteImageInfo(images, Plugin.Instance.Configuration.BackdropSize, ImageType.Backdrop, requestLanguage, results); } /// @@ -551,7 +551,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb /// The collection to add the remote images into. public void ConvertProfilesToRemoteImageInfo(List images, string requestLanguage, List results) { - ConvertToRemoteImageInfo(images, _tmDbClient.Config.Images.ProfileSizes[^1], ImageType.Primary, requestLanguage, results); + ConvertToRemoteImageInfo(images, Plugin.Instance.Configuration.ProfileSize, ImageType.Primary, requestLanguage, results); } /// @@ -562,7 +562,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb /// The collection to add the remote images into. public void ConvertStillsToRemoteImageInfo(List images, string requestLanguage, List results) { - ConvertToRemoteImageInfo(images, _tmDbClient.Config.Images.StillSizes[^1], ImageType.Primary, requestLanguage, results); + ConvertToRemoteImageInfo(images, Plugin.Instance.Configuration.StillSize, ImageType.Primary, requestLanguage, results); } /// @@ -575,16 +575,20 @@ namespace MediaBrowser.Providers.Plugins.Tmdb /// The collection to add the remote images into. private void ConvertToRemoteImageInfo(List images, string size, ImageType type, string requestLanguage, List results) { + // sizes provided are for original resolution, don't store them when downloading scaled images + var scaleImage = !string.Equals(size, "original", StringComparison.OrdinalIgnoreCase); + for (var i = 0; i < images.Count; i++) { var image = images[i]; + results.Add(new RemoteImageInfo { Url = GetUrl(size, image.FilePath), CommunityRating = image.VoteAverage, VoteCount = image.VoteCount, - Width = image.Width, - Height = image.Height, + Width = scaleImage ? null : image.Width, + Height = scaleImage ? null : image.Height, Language = TmdbUtils.AdjustImageLanguage(image.Iso_639_1, requestLanguage), ProviderName = TmdbUtils.ProviderName, Type = type, @@ -593,9 +597,51 @@ namespace MediaBrowser.Providers.Plugins.Tmdb } } - private Task EnsureClientConfigAsync() + private async Task EnsureClientConfigAsync() { - return !_tmDbClient.HasConfig ? _tmDbClient.GetConfigAsync() : Task.CompletedTask; + if (!_tmDbClient.HasConfig) + { + var config = await _tmDbClient.GetConfigAsync().ConfigureAwait(false); + ValidatePreferences(config); + } + } + + private static void ValidatePreferences(TMDbConfig config) + { + var imageConfig = config.Images; + + var pluginConfig = Plugin.Instance.Configuration; + + if (!imageConfig.PosterSizes.Contains(pluginConfig.PosterSize)) + { + pluginConfig.PosterSize = imageConfig.PosterSizes[^1]; + } + + if (!imageConfig.BackdropSizes.Contains(pluginConfig.BackdropSize)) + { + pluginConfig.BackdropSize = imageConfig.BackdropSizes[^1]; + } + + if (!imageConfig.ProfileSizes.Contains(pluginConfig.ProfileSize)) + { + pluginConfig.ProfileSize = imageConfig.ProfileSizes[^1]; + } + + if (!imageConfig.StillSizes.Contains(pluginConfig.StillSize)) + { + pluginConfig.StillSize = imageConfig.StillSizes[^1]; + } + } + + /// + /// Gets the configuration. + /// + /// The configuration. + public async Task GetClientConfiguration() + { + await EnsureClientConfigAsync().ConfigureAwait(false); + + return _tmDbClient.Config; } /// diff --git a/MediaBrowser.Providers/Subtitles/SubtitleManager.cs b/MediaBrowser.Providers/Subtitles/SubtitleManager.cs index c2b420c33..34019e582 100644 --- a/MediaBrowser.Providers/Subtitles/SubtitleManager.cs +++ b/MediaBrowser.Providers/Subtitles/SubtitleManager.cs @@ -9,6 +9,7 @@ using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Extensions; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Movies; @@ -76,8 +77,7 @@ namespace MediaBrowser.Providers.Subtitles var contentType = request.ContentType; var providers = _subtitleProviders - .Where(i => i.SupportedMediaTypes.Contains(contentType)) - .Where(i => !request.DisabledSubtitleFetchers.Contains(i.Name, StringComparer.OrdinalIgnoreCase)) + .Where(i => i.SupportedMediaTypes.Contains(contentType) && !request.DisabledSubtitleFetchers.Contains(i.Name, StringComparison.OrdinalIgnoreCase)) .OrderBy(i => { var index = request.SubtitleFetcherOrder.ToList().IndexOf(i.Name); diff --git a/MediaBrowser.Providers/TV/SeriesMetadataService.cs b/MediaBrowser.Providers/TV/SeriesMetadataService.cs index 9d223b4b6..770dc3e00 100644 --- a/MediaBrowser.Providers/TV/SeriesMetadataService.cs +++ b/MediaBrowser.Providers/TV/SeriesMetadataService.cs @@ -130,11 +130,12 @@ namespace MediaBrowser.Providers.TV /// The async task. private async Task FillInMissingSeasonsAsync(Series series, CancellationToken cancellationToken) { - var episodesInSeriesFolder = series.GetRecursiveChildren(i => i is Episode) - .Cast() + var seriesChildren = series.GetRecursiveChildren(i => i is Episode || i is Season); + var episodesInSeriesFolder = seriesChildren + .OfType() .Where(i => !i.IsInSeasonFolder); - List seasons = series.Children.OfType().ToList(); + List seasons = seriesChildren.OfType().ToList(); // Loop through the unique season numbers foreach (var episode in episodesInSeriesFolder) diff --git a/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs index 594402258..d09981304 100644 --- a/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs +++ b/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs @@ -9,6 +9,7 @@ using System.Text; using System.Text.RegularExpressions; using System.Threading; using System.Xml; +using Jellyfin.Extensions; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; @@ -1003,7 +1004,7 @@ namespace MediaBrowser.XbmcMetadata.Savers var name = reader.Name; if (!_commonTags.Contains(name) - && !xmlTagsUsed.Contains(name, StringComparer.OrdinalIgnoreCase)) + && !xmlTagsUsed.Contains(name, StringComparison.OrdinalIgnoreCase)) { writer.WriteNode(reader, false); } diff --git a/RSSDP/HttpRequestParser.cs b/RSSDP/HttpRequestParser.cs index 4114195a6..a3e100796 100644 --- a/RSSDP/HttpRequestParser.cs +++ b/RSSDP/HttpRequestParser.cs @@ -1,6 +1,6 @@ using System; -using System.Linq; using System.Net.Http; +using Jellyfin.Extensions; namespace Rssdp.Infrastructure { @@ -86,7 +86,7 @@ namespace Rssdp.Infrastructure /// A string containing the name of the header to return the type of. protected override bool IsContentHeader(string headerName) { - return ContentHeaderNames.Contains(headerName, StringComparer.OrdinalIgnoreCase); + return ContentHeaderNames.Contains(headerName, StringComparison.OrdinalIgnoreCase); } } } diff --git a/RSSDP/HttpResponseParser.cs b/RSSDP/HttpResponseParser.cs index 0dd4bb45a..3e361465d 100644 --- a/RSSDP/HttpResponseParser.cs +++ b/RSSDP/HttpResponseParser.cs @@ -1,7 +1,7 @@ using System; -using System.Linq; using System.Net; using System.Net.Http; +using Jellyfin.Extensions; namespace Rssdp.Infrastructure { @@ -49,7 +49,7 @@ namespace Rssdp.Infrastructure /// A boolean, true if th specified header relates to HTTP content, otherwise false. protected override bool IsContentHeader(string headerName) { - return ContentHeaderNames.Contains(headerName, StringComparer.OrdinalIgnoreCase); + return ContentHeaderNames.Contains(headerName, StringComparison.OrdinalIgnoreCase); } /// diff --git a/bump_version b/bump_version index f615606e2..41d27f5c8 100755 --- a/bump_version +++ b/bump_version @@ -52,7 +52,8 @@ echo $old_version # Set the build.yaml version to the specified new_version old_version_sed="$( sed 's/\./\\./g' <<<"${old_version}" )" # Escape the '.' chars -sed -i "s/${old_version_sed}/${new_version}/g" ${build_file} +new_version_sed="$( cut -f1 -d'-' <<<"${new_version}" )" +sed -i "s/${old_version_sed}/${new_version_sed}/g" ${build_file} # update nuget package version for subproject in ${jellyfin_subprojects[@]}; do @@ -64,26 +65,29 @@ for subproject in ${jellyfin_subprojects[@]}; do | sed -E 's/([0-9\.]+[-a-z0-9]*)<\/VersionPrefix>/\1/' )" echo old nuget version: $old_version + new_version_sed="$( cut -f1 -d'-' <<<"${new_version}" )" # Set the nuget version to the specified new_version - sed -i "s|${old_version}|${new_version}|g" ${subproject} + sed -i "s|${old_version}|${new_version_sed}|g" ${subproject} done if [[ ${new_version} == *"-"* ]]; then - new_version_deb="$( sed 's/-/~/g' <<<"${new_version}" )" + new_version_pkg="$( sed 's/-/~/g' <<<"${new_version}" )" + new_version_deb_sup="" else - new_version_deb="${new_version}-1" + new_version_pkg="${new_version}" + new_version_deb_sup="-1" fi # Update the metapackage equivs file debian_equivs_file="debian/metapackage/jellyfin" -sed -i "s/${old_version_sed}/${new_version}/g" ${debian_equivs_file} +sed -i "s/${old_version_sed}/${new_version_pkg}/g" ${debian_equivs_file} # Write out a temporary Debian changelog with our new stuff appended and some templated formatting debian_changelog_file="debian/changelog" debian_changelog_temp="$( mktemp )" # Create new temp file with our changelog -echo -e "jellyfin-server (${new_version_deb}) unstable; urgency=medium +echo -e "jellyfin-server (${new_version_pkg}${new_version_deb_sup}) unstable; urgency=medium * New upstream version ${new_version}; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v${new_version} @@ -104,7 +108,7 @@ pushd ${fedora_spec_temp_dir} # Split out the stuff before and after changelog csplit jellyfin.spec "/^%changelog/" # produces xx00 xx01 # Update the version in xx00 -sed -i "s/${old_version_sed}/${new_version_sed}/g" xx00 +sed -i "s/${old_version_sed}/${new_version_pkg}/g" xx00 # Remove the header from xx01 sed -i '/^%changelog/d' xx01 # Create new temp file with our changelog @@ -121,5 +125,5 @@ mv ${fedora_spec_temp} ${fedora_spec_file} rm -rf ${fedora_spec_temp_dir} # Stage the changed files for commit -git add ${shared_version_file} ${build_file} ${debian_equivs_file} ${debian_changelog_file} ${fedora_spec_file} -git status +git add . +git status -v diff --git a/debian/jellyfin.service b/debian/jellyfin.service index e215a8536..b86f40473 100644 --- a/debian/jellyfin.service +++ b/debian/jellyfin.service @@ -13,7 +13,19 @@ TimeoutSec = 15 NoNewPrivileges=true SystemCallArchitectures=native RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 AF_NETLINK -ProtectKernelModules=True +RestrictNamespaces=true +RestrictRealtime=true +RestrictSUIDSGID=true +ProtectControlGroups=true +ProtectHostname=true +ProtectKernelLogs=true +ProtectKernelModules=true +ProtectKernelTunables=true +LockPersonality=true +PrivateTmp=true +PrivateDevices=false +PrivateUsers=true +RemoveIPC=true SystemCallFilter=~@clock SystemCallFilter=~@aio SystemCallFilter=~@chown diff --git a/deployment/Dockerfile.centos.amd64 b/deployment/Dockerfile.centos.amd64 index 3967a165d..708c706b5 100644 --- a/deployment/Dockerfile.centos.amd64 +++ b/deployment/Dockerfile.centos.amd64 @@ -13,7 +13,7 @@ RUN yum update -yq \ && yum install -yq @buildsys-build rpmdevtools yum-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel git wget # Install DotNET SDK -RUN wget -q https://download.visualstudio.microsoft.com/download/pr/17b6759f-1af0-41bc-ab12-209ba0377779/e8d02195dbf1434b940e0f05ae086453/dotnet-sdk-6.0.100-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget -q https://download.visualstudio.microsoft.com/download/pr/ede8a287-3d61-4988-a356-32ff9129079e/bdb47b6b510ed0c4f0b132f7f4ad9d5a/dotnet-sdk-6.0.101-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ && mkdir -p dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet diff --git a/deployment/Dockerfile.fedora.amd64 b/deployment/Dockerfile.fedora.amd64 index bc40a8059..30615cd42 100644 --- a/deployment/Dockerfile.fedora.amd64 +++ b/deployment/Dockerfile.fedora.amd64 @@ -12,7 +12,7 @@ RUN dnf update -yq \ && dnf install -yq @buildsys-build rpmdevtools git dnf-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel systemd wget # Install DotNET SDK -RUN wget -q https://download.visualstudio.microsoft.com/download/pr/17b6759f-1af0-41bc-ab12-209ba0377779/e8d02195dbf1434b940e0f05ae086453/dotnet-sdk-6.0.100-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget -q https://download.visualstudio.microsoft.com/download/pr/ede8a287-3d61-4988-a356-32ff9129079e/bdb47b6b510ed0c4f0b132f7f4ad9d5a/dotnet-sdk-6.0.101-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ && mkdir -p dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet diff --git a/deployment/Dockerfile.ubuntu.amd64 b/deployment/Dockerfile.ubuntu.amd64 index c1b541c59..ccfaaa5f0 100644 --- a/deployment/Dockerfile.ubuntu.amd64 +++ b/deployment/Dockerfile.ubuntu.amd64 @@ -17,7 +17,7 @@ RUN apt-get update -yqq \ libfreetype6-dev libssl-dev libssl1.1 liblttng-ust0 # Install dotnet repository -RUN wget -q https://download.visualstudio.microsoft.com/download/pr/17b6759f-1af0-41bc-ab12-209ba0377779/e8d02195dbf1434b940e0f05ae086453/dotnet-sdk-6.0.100-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget -q https://download.visualstudio.microsoft.com/download/pr/ede8a287-3d61-4988-a356-32ff9129079e/bdb47b6b510ed0c4f0b132f7f4ad9d5a/dotnet-sdk-6.0.101-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ && mkdir -p dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet diff --git a/deployment/Dockerfile.ubuntu.arm64 b/deployment/Dockerfile.ubuntu.arm64 index 6aa98a289..988c8f16d 100644 --- a/deployment/Dockerfile.ubuntu.arm64 +++ b/deployment/Dockerfile.ubuntu.arm64 @@ -16,7 +16,7 @@ RUN apt-get update -yqq \ mmv build-essential lsb-release # Install dotnet repository -RUN wget -q https://download.visualstudio.microsoft.com/download/pr/17b6759f-1af0-41bc-ab12-209ba0377779/e8d02195dbf1434b940e0f05ae086453/dotnet-sdk-6.0.100-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget -q https://download.visualstudio.microsoft.com/download/pr/ede8a287-3d61-4988-a356-32ff9129079e/bdb47b6b510ed0c4f0b132f7f4ad9d5a/dotnet-sdk-6.0.101-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ && mkdir -p dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet diff --git a/deployment/Dockerfile.ubuntu.armhf b/deployment/Dockerfile.ubuntu.armhf index cc9d8dc79..61a008d6a 100644 --- a/deployment/Dockerfile.ubuntu.armhf +++ b/deployment/Dockerfile.ubuntu.armhf @@ -16,7 +16,7 @@ RUN apt-get update -yqq \ mmv build-essential lsb-release # Install dotnet repository -RUN wget -q https://download.visualstudio.microsoft.com/download/pr/17b6759f-1af0-41bc-ab12-209ba0377779/e8d02195dbf1434b940e0f05ae086453/dotnet-sdk-6.0.100-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget -q https://download.visualstudio.microsoft.com/download/pr/ede8a287-3d61-4988-a356-32ff9129079e/bdb47b6b510ed0c4f0b132f7f4ad9d5a/dotnet-sdk-6.0.101-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ && mkdir -p dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet diff --git a/fedora/Makefile b/fedora/Makefile index 97904ddd3..22cc30448 100644 --- a/fedora/Makefile +++ b/fedora/Makefile @@ -1,26 +1,55 @@ -VERSION := $(shell sed -ne '/^Version:/s/.* *//p' fedora/jellyfin.spec) +DIR := $(dir $(lastword $(MAKEFILE_LIST))) +INSTGIT := $(shell if [ "$$(id -u)" = "0" ]; then dnf -y install git; fi) +NAME := jellyfin-server +VERSION := $(shell sed -ne '/^Version:/s/.* *//p' $(DIR)/jellyfin.spec) +RELEASE := $(shell sed -ne '/^Release:/s/.* *\(.*\)%{.*}.*/\1/p' $(DIR)/jellyfin.spec) +GIT_VER := $(shell git describe --tags | sed -e 's/^v//' -e 's/-[0-9]*-g.*$$//') +SRPM := jellyfin-$(subst -,~,$(GIT_VER))-$(RELEASE)$(shell rpm --eval %dist).src.rpm +TARBALL :=$(NAME)-$(subst -,~,$(GIT_VER)).tar.gz -srpm: - cd fedora/; \ - SOURCE_DIR=.. \ - WORKDIR="$${PWD}"; \ - tar \ - --transform "s,^\.,jellyfin-server-$(VERSION)," \ - --exclude='.git*' \ - --exclude='**/.git' \ - --exclude='**/.hg' \ - --exclude='**/.vs' \ - --exclude='**/.vscode' \ - --exclude='deployment' \ - --exclude='**/bin' \ - --exclude='**/obj' \ - --exclude='**/.nuget' \ - --exclude='*.deb' \ - --exclude='*.rpm' \ - --exclude='jellyfin-server-$(VERSION).tar.gz' \ - -czf "jellyfin-server-$(VERSION).tar.gz" \ +epel-7-x86_64_repos := https://packages.microsoft.com/rhel/7/prod/ +epel-8-x86_64_repos := https://download.copr.fedorainfracloud.org/results/@dotnet-sig/dotnet-preview/$(TARGET)/ +fedora_repos := https://download.copr.fedorainfracloud.org/results/@dotnet-sig/dotnet-preview/$(TARGET)/ +fedora-34-x86_64_repos := $(fedora_repos) +fedora-35-x86_64_repos := $(fedora_repos) +fedora-34-x86_64_repos := $(fedora_repos) + +outdir ?= $(PWD)/$(DIR)/ +TARGET ?= fedora-35-x86_64 + +srpm: $(DIR)/$(SRPM) +tarball: $(DIR)/$(TARBALL) + +$(DIR)/$(TARBALL): + cd $(DIR)/; \ + SOURCE_DIR=.. \ + WORKDIR="$${PWD}"; \ + version=$(GIT_VER); \ + tar \ + --transform "s,^\.,$(NAME)-$(subst -,~,$(GIT_VER))," \ + --exclude='.git*' \ + --exclude='**/.git' \ + --exclude='**/.hg' \ + --exclude='**/.vs' \ + --exclude='**/.vscode' \ + --exclude=deployment \ + --exclude='**/bin' \ + --exclude='**/obj' \ + --exclude='**/.nuget' \ + --exclude='*.deb' \ + --exclude='*.rpm' \ + --exclude=$(notdir $@) \ + -czf $(notdir $@) \ -C $${SOURCE_DIR} ./ - cd fedora/; \ - rpmbuild -bs jellyfin.spec \ - --define "_sourcedir $$PWD/" \ + +$(DIR)/$(SRPM): $(DIR)/$(TARBALL) $(DIR)/jellyfin.spec + ./bump_version $(GIT_VER) + cd $(DIR)/; \ + rpmbuild -bs jellyfin.spec \ + --define "_sourcedir $$PWD/" \ --define "_srcrpmdir $(outdir)" + +rpms: $(DIR)/$(SRPM) + mock $(addprefix --addrepo=, $($(TARGET)_repos)) \ + --enable-network \ + -r $(TARGET) $< diff --git a/fedora/jellyfin-server-lowports.conf b/fedora/jellyfin-server-lowports.conf new file mode 100644 index 000000000..eeb48a4e4 --- /dev/null +++ b/fedora/jellyfin-server-lowports.conf @@ -0,0 +1,4 @@ +# This allows Jellyfin to bind to low ports such as 80 and/or 443 + +[Service] +AmbientCapabilities=CAP_NET_BIND_SERVICE \ No newline at end of file diff --git a/fedora/jellyfin.spec b/fedora/jellyfin.spec index 47dee7c13..e93944a20 100644 --- a/fedora/jellyfin.spec +++ b/fedora/jellyfin.spec @@ -12,7 +12,7 @@ Release: 1%{?dist} Summary: The Free Software Media System License: GPLv3 URL: https://jellyfin.org -# Jellyfin Server tarball created by `make -f .copr/Makefile srpm`, real URL ends with `v%{version}.tar.gz` +# Jellyfin Server tarball created by `make -f .copr/Makefile srpm`, real URL ends with `v%%{version}.tar.gz` Source0: jellyfin-server-%{version}.tar.gz Source11: jellyfin.service Source12: jellyfin.env @@ -20,6 +20,7 @@ Source13: jellyfin.sudoers Source14: restart.sh Source15: jellyfin.override.conf Source16: jellyfin-firewalld.xml +Source17: jellyfin-server-lowports.conf %{?systemd_requires} BuildRequires: systemd @@ -45,6 +46,16 @@ Requires: libcurl, fontconfig, freetype, openssl, glibc, libicu, at, sudo %description server The Jellyfin media server backend. +%package server-lowports +# RPMfusion free +Summary: The Free Software Media System Server backend. Low-port binding. +Requires: jellyfin-server + +%description server-lowports +The Jellyfin media server backend low port binding package. This package +enables binding to ports < 1024. You would install this if you want +the Jellyfin server to bind to ports 80 and/or 443 for example. + %prep %autosetup -n jellyfin-server-%{version} -b 0 @@ -53,10 +64,12 @@ The Jellyfin media server backend. %install export DOTNET_CLI_TELEMETRY_OPTOUT=1 export DOTNET_SKIP_FIRST_TIME_EXPERIENCE=1 +export PATH=$PATH:/usr/local/bin dotnet publish --configuration Release --output='%{buildroot}%{_libdir}/jellyfin' --self-contained --runtime %{dotnet_runtime} \ "-p:DebugSymbols=false;DebugType=none" Jellyfin.Server %{__install} -D -m 0644 LICENSE %{buildroot}%{_datadir}/licenses/jellyfin/LICENSE %{__install} -D -m 0644 %{SOURCE15} %{buildroot}%{_sysconfdir}/systemd/system/jellyfin.service.d/override.conf +%{__install} -D -m 0644 %{SOURCE17} %{buildroot}%{_unitdir}/jellyfin.service.d/jellyfin-server-lowports.conf %{__install} -D -m 0644 Jellyfin.Server/Resources/Configuration/logging.json %{buildroot}%{_sysconfdir}/jellyfin/logging.json %{__mkdir} -p %{buildroot}%{_bindir} tee %{buildroot}%{_bindir}/jellyfin << EOF @@ -95,6 +108,9 @@ EOF %attr(750,jellyfin,jellyfin) %dir %{_var}/cache/jellyfin %{_datadir}/licenses/jellyfin/LICENSE +%files server-lowports +%{_unitdir}/jellyfin.service.d/jellyfin-server-lowports.conf + %pre server getent group jellyfin >/dev/null || groupadd -r jellyfin getent passwd jellyfin >/dev/null || \ @@ -137,6 +153,9 @@ fi %systemd_postun_with_restart jellyfin.service %changelog +* Mon Nov 29 2021 Brian J. Murrell +- Add jellyfin-server-lowports.service drop-in in a server-lowports + subpackage to allow binding to low ports * Fri Dec 04 2020 Jellyfin Packaging Team - Forthcoming stable release * Mon Jul 27 2020 Jellyfin Packaging Team diff --git a/src/Jellyfin.Extensions/Json/Converters/JsonDelimitedArrayConverter.cs b/src/Jellyfin.Extensions/Json/Converters/JsonDelimitedArrayConverter.cs index 51b955145..321cfa502 100644 --- a/src/Jellyfin.Extensions/Json/Converters/JsonDelimitedArrayConverter.cs +++ b/src/Jellyfin.Extensions/Json/Converters/JsonDelimitedArrayConverter.cs @@ -9,7 +9,7 @@ namespace Jellyfin.Extensions.Json.Converters /// Convert delimited string to array of type. /// /// Type to convert to. - public abstract class JsonDelimitedArrayConverter : JsonConverter + public abstract class JsonDelimitedArrayConverter : JsonConverter { private readonly TypeConverter _typeConverter; @@ -31,9 +31,9 @@ namespace Jellyfin.Extensions.Json.Converters { if (reader.TokenType == JsonTokenType.String) { - // GetString can't return null here because we already handled it above - var stringEntries = reader.GetString()?.Split(Delimiter, StringSplitOptions.RemoveEmptyEntries); - if (stringEntries == null || stringEntries.Length == 0) + // null got handled higher up the call stack + var stringEntries = reader.GetString()!.Split(Delimiter, StringSplitOptions.RemoveEmptyEntries); + if (stringEntries.Length == 0) { return Array.Empty(); } diff --git a/src/Jellyfin.Extensions/Json/Converters/JsonGuidConverter.cs b/src/Jellyfin.Extensions/Json/Converters/JsonGuidConverter.cs index be94dd519..ea6d141cb 100644 --- a/src/Jellyfin.Extensions/Json/Converters/JsonGuidConverter.cs +++ b/src/Jellyfin.Extensions/Json/Converters/JsonGuidConverter.cs @@ -12,15 +12,19 @@ namespace Jellyfin.Extensions.Json.Converters { /// public override Guid Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - var guidStr = reader.GetString(); - return guidStr == null ? Guid.Empty : new Guid(guidStr); - } + => reader.TokenType == JsonTokenType.Null + ? Guid.Empty + : ReadInternal(ref reader); + + // TODO: optimize by parsing the UTF8 bytes instead of converting to string first + internal static Guid ReadInternal(ref Utf8JsonReader reader) + => Guid.Parse(reader.GetString()!); // null got handled higher up the call stack /// public override void Write(Utf8JsonWriter writer, Guid value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.ToString("N", CultureInfo.InvariantCulture)); - } + => WriteInternal(writer, value); + + internal static void WriteInternal(Utf8JsonWriter writer, Guid value) + => writer.WriteStringValue(value.ToString("N", CultureInfo.InvariantCulture)); } } diff --git a/src/Jellyfin.Extensions/Json/Converters/JsonNullableGuidConverter.cs b/src/Jellyfin.Extensions/Json/Converters/JsonNullableGuidConverter.cs index 6192d1598..b477bcb66 100644 --- a/src/Jellyfin.Extensions/Json/Converters/JsonNullableGuidConverter.cs +++ b/src/Jellyfin.Extensions/Json/Converters/JsonNullableGuidConverter.cs @@ -1,5 +1,4 @@ using System; -using System.Globalization; using System.Text.Json; using System.Text.Json.Serialization; @@ -12,21 +11,19 @@ namespace Jellyfin.Extensions.Json.Converters { /// public override Guid? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - var guidStr = reader.GetString(); - return guidStr == null ? null : new Guid(guidStr); - } + => JsonGuidConverter.ReadInternal(ref reader); /// public override void Write(Utf8JsonWriter writer, Guid? value, JsonSerializerOptions options) { - if (value == null || value == Guid.Empty) + if (value == Guid.Empty) { writer.WriteNullValue(); } else { - writer.WriteStringValue(value.Value.ToString("N", CultureInfo.InvariantCulture)); + // null got handled higher up the call stack + JsonGuidConverter.WriteInternal(writer, value!.Value); } } } diff --git a/src/Jellyfin.Extensions/Json/Converters/JsonNullableStructConverter.cs b/src/Jellyfin.Extensions/Json/Converters/JsonNullableStructConverter.cs index 6de238b39..28437023f 100644 --- a/src/Jellyfin.Extensions/Json/Converters/JsonNullableStructConverter.cs +++ b/src/Jellyfin.Extensions/Json/Converters/JsonNullableStructConverter.cs @@ -15,13 +15,10 @@ namespace Jellyfin.Extensions.Json.Converters /// public override TStruct? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - if (reader.TokenType == JsonTokenType.Null) - { - return null; - } - // Token is empty string. - if (reader.TokenType == JsonTokenType.String && ((reader.HasValueSequence && reader.ValueSequence.IsEmpty) || reader.ValueSpan.IsEmpty)) + if (reader.TokenType == JsonTokenType.String + && ((reader.HasValueSequence && reader.ValueSequence.IsEmpty) + || (!reader.HasValueSequence && reader.ValueSpan.IsEmpty))) { return null; } @@ -31,15 +28,6 @@ namespace Jellyfin.Extensions.Json.Converters /// public override void Write(Utf8JsonWriter writer, TStruct? value, JsonSerializerOptions options) - { - if (value.HasValue) - { - JsonSerializer.Serialize(writer, value.Value, options); - } - else - { - writer.WriteNullValue(); - } - } + => JsonSerializer.Serialize(writer, value!.Value, options); // null got handled higher up the call stack } } diff --git a/src/Jellyfin.Extensions/Json/Converters/JsonStringConverter.cs b/src/Jellyfin.Extensions/Json/Converters/JsonStringConverter.cs index 1a7a8c4f5..36b36c9b4 100644 --- a/src/Jellyfin.Extensions/Json/Converters/JsonStringConverter.cs +++ b/src/Jellyfin.Extensions/Json/Converters/JsonStringConverter.cs @@ -13,20 +13,11 @@ namespace Jellyfin.Extensions.Json.Converters { /// public override string? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - return reader.TokenType switch - { - JsonTokenType.Null => null, - JsonTokenType.String => reader.GetString(), - _ => GetRawValue(reader) - }; - } + => reader.TokenType == JsonTokenType.String ? reader.GetString() : GetRawValue(reader); /// public override void Write(Utf8JsonWriter writer, string? value, JsonSerializerOptions options) - { - writer.WriteStringValue(value); - } + => writer.WriteStringValue(value); private static string GetRawValue(Utf8JsonReader reader) { diff --git a/tests/Jellyfin.Api.Tests/Helpers/RequestHelpersTests.cs b/tests/Jellyfin.Api.Tests/Helpers/RequestHelpersTests.cs index 4ba7e1d2f..c4640bd22 100644 --- a/tests/Jellyfin.Api.Tests/Helpers/RequestHelpersTests.cs +++ b/tests/Jellyfin.Api.Tests/Helpers/RequestHelpersTests.cs @@ -55,35 +55,5 @@ namespace Jellyfin.Api.Tests.Helpers return data; } - - [Fact] - public static void GetItemTypeStrings_Empty_Empty() - { - Assert.Empty(RequestHelpers.GetItemTypeStrings(Array.Empty())); - } - - [Fact] - public static void GetItemTypeStrings_Valid_Success() - { - BaseItemKind[] input = - { - BaseItemKind.AggregateFolder, - BaseItemKind.Audio, - BaseItemKind.BasePluginFolder, - BaseItemKind.CollectionFolder - }; - - string[] expected = - { - "AggregateFolder", - "Audio", - "BasePluginFolder", - "CollectionFolder" - }; - - var res = RequestHelpers.GetItemTypeStrings(input); - - Assert.Equal(expected, res); - } } } diff --git a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj index 2aced0669..bcbe9c1be 100644 --- a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj +++ b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj @@ -15,7 +15,7 @@ - + diff --git a/tests/Jellyfin.Controller.Tests/BaseItemManagerTests.cs b/tests/Jellyfin.Controller.Tests/BaseItemManagerTests.cs index edceef4a7..463e17ad3 100644 --- a/tests/Jellyfin.Controller.Tests/BaseItemManagerTests.cs +++ b/tests/Jellyfin.Controller.Tests/BaseItemManagerTests.cs @@ -3,7 +3,6 @@ using MediaBrowser.Controller.BaseItemManager; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; -using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Model.Configuration; using Moq; using Xunit; diff --git a/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs b/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs index 4504924cb..0fc8724b6 100644 --- a/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs +++ b/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs @@ -18,6 +18,19 @@ namespace Jellyfin.MediaEncoding.Tests.Probing private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options; private readonly ProbeResultNormalizer _probeResultNormalizer = new ProbeResultNormalizer(new NullLogger(), null); + [Theory] + [InlineData("2997/125", 23.976f)] + [InlineData("1/50", 0.02f)] + [InlineData("25/1", 25f)] + [InlineData("120/1", 120f)] + [InlineData("1704753000/71073479", 23.98578237601117f)] + [InlineData("0/0", null)] + [InlineData("1/1000", 0.001f)] + [InlineData("1/90000", 1.1111111E-05f)] + [InlineData("1/48000", 2.0833333E-05f)] + public void GetFrameRate_Success(string value, float? expected) + => Assert.Equal(expected, ProbeResultNormalizer.GetFrameRate(value)); + [Fact] public void GetMediaInfo_MetaData_Success() { diff --git a/tests/Jellyfin.Model.Tests/Net/MimeTypesTests.cs b/tests/Jellyfin.Model.Tests/Net/MimeTypesTests.cs new file mode 100644 index 000000000..cbab455f0 --- /dev/null +++ b/tests/Jellyfin.Model.Tests/Net/MimeTypesTests.cs @@ -0,0 +1,159 @@ +using MediaBrowser.Model.Net; +using Xunit; + +namespace Jellyfin.Model.Tests.Net +{ + public class MimeTypesTests + { + [Theory] + [InlineData(".dll", "application/octet-stream")] + [InlineData(".log", "text/plain")] + [InlineData(".srt", "application/x-subrip")] + [InlineData(".html", "text/html; charset=UTF-8")] + [InlineData(".htm", "text/html; charset=UTF-8")] + [InlineData(".7z", "application/x-7z-compressed")] + [InlineData(".azw", "application/vnd.amazon.ebook")] + [InlineData(".azw3", "application/vnd.amazon.ebook")] + [InlineData(".eot", "application/vnd.ms-fontobject")] + [InlineData(".epub", "application/epub+zip")] + [InlineData(".json", "application/json")] + [InlineData(".mobi", "application/x-mobipocket-ebook")] + [InlineData(".opf", "application/oebps-package+xml")] + [InlineData(".pdf", "application/pdf")] + [InlineData(".rar", "application/vnd.rar")] + [InlineData(".ttml", "application/ttml+xml")] + [InlineData(".wasm", "application/wasm")] + [InlineData(".xml", "application/xml")] + [InlineData(".zip", "application/zip")] + [InlineData(".bmp", "image/bmp")] + [InlineData(".gif", "image/gif")] + [InlineData(".ico", "image/vnd.microsoft.icon")] + [InlineData(".jpg", "image/jpeg")] + [InlineData(".jpeg", "image/jpeg")] + [InlineData(".png", "image/png")] + [InlineData(".svg", "image/svg+xml")] + [InlineData(".svgz", "image/svg+xml")] + [InlineData(".tbn", "image/jpeg")] + [InlineData(".tif", "image/tiff")] + [InlineData(".tiff", "image/tiff")] + [InlineData(".webp", "image/webp")] + [InlineData(".ttf", "font/ttf")] + [InlineData(".woff", "font/woff")] + [InlineData(".woff2", "font/woff2")] + [InlineData(".ass", "text/x-ssa")] + [InlineData(".ssa", "text/x-ssa")] + [InlineData(".css", "text/css")] + [InlineData(".csv", "text/csv")] + [InlineData(".edl", "text/plain")] + [InlineData(".txt", "text/plain")] + [InlineData(".vtt", "text/vtt")] + [InlineData(".3gp", "video/3gpp")] + [InlineData(".3g2", "video/3gpp2")] + [InlineData(".asf", "video/x-ms-asf")] + [InlineData(".avi", "video/x-msvideo")] + [InlineData(".flv", "video/x-flv")] + [InlineData(".mp4", "video/mp4")] + [InlineData(".m4v", "video/x-m4v")] + [InlineData(".mpegts", "video/mp2t")] + [InlineData(".mpg", "video/mpeg")] + [InlineData(".mkv", "video/x-matroska")] + [InlineData(".mov", "video/quicktime")] + [InlineData(".ogv", "video/ogg")] + [InlineData(".ts", "video/mp2t")] + [InlineData(".webm", "video/webm")] + [InlineData(".wmv", "video/x-ms-wmv")] + [InlineData(".aac", "audio/aac")] + [InlineData(".ac3", "audio/ac3")] + [InlineData(".ape", "audio/x-ape")] + [InlineData(".dsf", "audio/dsf")] + [InlineData(".dsp", "audio/dsp")] + [InlineData(".flac", "audio/flac")] + [InlineData(".m4a", "audio/mp4")] + [InlineData(".m4b", "audio/m4b")] + [InlineData(".mid", "audio/midi")] + [InlineData(".midi", "audio/midi")] + [InlineData(".mp3", "audio/mpeg")] + [InlineData(".oga", "audio/ogg")] + [InlineData(".ogg", "audio/ogg")] + [InlineData(".opus", "audio/ogg")] + [InlineData(".vorbis", "audio/vorbis")] + [InlineData(".wav", "audio/wav")] + [InlineData(".webma", "audio/webm")] + [InlineData(".wma", "audio/x-ms-wma")] + [InlineData(".wv", "audio/x-wavpack")] + [InlineData(".xsp", "audio/xsp")] + public void GetMimeType_Valid_ReturnsCorrectResult(string input, string expectedResult) + { + Assert.Equal(expectedResult, MimeTypes.GetMimeType(input, null)); + } + + [Theory] + [InlineData("application/epub+zip", ".epub")] + [InlineData("application/json", ".json")] + [InlineData("application/oebps-package+xml", ".opf")] + [InlineData("application/pdf", ".pdf")] + [InlineData("application/ttml+xml", ".ttml")] + [InlineData("application/vnd.amazon.ebook", ".azw")] + [InlineData("application/vnd.ms-fontobject", ".eot")] + [InlineData("application/vnd.rar", ".rar")] + [InlineData("application/wasm", ".wasm")] + [InlineData("application/x-7z-compressed", ".7z")] + [InlineData("application/x-cbz", ".cbz")] + [InlineData("application/x-javascript", ".js")] + [InlineData("application/x-mobipocket-ebook", ".mobi")] + [InlineData("application/x-mpegURL", ".m3u8")] + [InlineData("application/x-subrip", ".srt")] + [InlineData("application/xml", ".xml")] + [InlineData("application/zip", ".zip")] + [InlineData("audio/aac", ".aac")] + [InlineData("audio/ac3", ".ac3")] + [InlineData("audio/dsf", ".dsf")] + [InlineData("audio/dsp", ".dsp")] + [InlineData("audio/flac", ".flac")] + [InlineData("audio/m4b", ".m4b")] + [InlineData("audio/mp4", ".m4a")] + [InlineData("audio/vorbis", ".vorbis")] + [InlineData("audio/wav", ".wav")] + [InlineData("audio/x-aac", ".aac")] + [InlineData("audio/x-ape", ".ape")] + [InlineData("audio/x-ms-wma", ".wma")] + [InlineData("audio/x-wavpack", ".wv")] + [InlineData("audio/xsp", ".xsp")] + [InlineData("font/ttf", ".ttf")] + [InlineData("font/woff", ".woff")] + [InlineData("font/woff2", ".woff2")] + [InlineData("image/bmp", ".bmp")] + [InlineData("image/gif", ".gif")] + [InlineData("image/jpeg", ".jpg")] + [InlineData("image/png", ".png")] + [InlineData("image/svg+xml", ".svg")] + [InlineData("image/tiff", ".tif")] + [InlineData("image/vnd.microsoft.icon", ".ico")] + [InlineData("image/webp", ".webp")] + [InlineData("image/x-png", ".png")] + [InlineData("text/css", ".css")] + [InlineData("text/csv", ".csv")] + [InlineData("text/plain", ".txt")] + [InlineData("text/rtf", ".rtf")] + [InlineData("text/vtt", ".vtt")] + [InlineData("text/x-ssa", ".ssa")] + [InlineData("video/3gpp", ".3gp")] + [InlineData("video/3gpp2", ".3g2")] + [InlineData("video/mp2t", ".ts")] + [InlineData("video/mp4", ".mp4")] + [InlineData("video/ogg", ".ogv")] + [InlineData("video/quicktime", ".mov")] + [InlineData("video/vnd.mpeg.dash.mpd", ".mpd")] + [InlineData("video/webm", ".webm")] + [InlineData("video/x-flv", ".flv")] + [InlineData("video/x-m4v", ".m4v")] + [InlineData("video/x-matroska", ".mkv")] + [InlineData("video/x-ms-asf", ".asf")] + [InlineData("video/x-ms-wmv", ".wmv")] + [InlineData("video/x-msvideo", ".avi")] + public void ToExtension_Valid_ReturnsCorrectResult(string input, string expectedResult) + { + Assert.Equal(expectedResult, MimeTypes.ToExtension(input)); + } + } +} diff --git a/tests/Jellyfin.Naming.Tests/Common/NamingOptionsTest.cs b/tests/Jellyfin.Naming.Tests/Common/NamingOptionsTest.cs index 3892d00f6..58aaed023 100644 --- a/tests/Jellyfin.Naming.Tests/Common/NamingOptionsTest.cs +++ b/tests/Jellyfin.Naming.Tests/Common/NamingOptionsTest.cs @@ -10,7 +10,6 @@ namespace Jellyfin.Naming.Tests.Common { var options = new NamingOptions(); - Assert.NotEmpty(options.VideoFileStackingRegexes); Assert.NotEmpty(options.CleanDateTimeRegexes); Assert.NotEmpty(options.CleanStringRegexes); Assert.NotEmpty(options.EpisodeWithoutSeasonRegexes); diff --git a/tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs b/tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs index d13e89cee..8dd637559 100644 --- a/tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs +++ b/tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs @@ -81,9 +81,7 @@ namespace Jellyfin.Naming.Tests.Video private void Test(string input, ExtraType? expectedType) { - var parser = GetExtraTypeParser(_videoOptions); - - var extraType = parser.GetExtraInfo(input).ExtraType; + var extraType = ExtraResolver.GetExtraInfo(input, _videoOptions).ExtraType; Assert.Equal(expectedType, extraType); } @@ -93,14 +91,9 @@ namespace Jellyfin.Naming.Tests.Video { var rule = new ExtraRule(ExtraType.Unknown, ExtraRuleType.Regex, @"([eE]x(tra)?\.\w+)", MediaType.Video); var options = new NamingOptions { VideoExtraRules = new[] { rule } }; - var res = GetExtraTypeParser(options).GetExtraInfo("extra.mp4"); + var res = ExtraResolver.GetExtraInfo("extra.mp4", options); Assert.Equal(rule, res.Rule); } - - private ExtraResolver GetExtraTypeParser(NamingOptions videoOptions) - { - return new ExtraResolver(videoOptions); - } } } diff --git a/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs b/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs index d02f8ae92..9a9a57be4 100644 --- a/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs +++ b/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs @@ -23,15 +23,11 @@ namespace Jellyfin.Naming.Tests.Video }; var result = VideoListResolver.Resolve( - files.Select(i => new FileSystemMetadata - { - IsDirectory = false, - FullName = i - }).ToList(), + files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType().ToList(), _namingOptions).ToList(); - Assert.Single(result); - Assert.Single(result[0].Extras); + Assert.Single(result.Where(v => v.ExtraType == null)); + Assert.Single(result.Where(v => v.ExtraType != null)); } [Fact] @@ -46,15 +42,11 @@ namespace Jellyfin.Naming.Tests.Video }; var result = VideoListResolver.Resolve( - files.Select(i => new FileSystemMetadata - { - IsDirectory = false, - FullName = i - }).ToList(), + files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType().ToList(), _namingOptions).ToList(); - Assert.Single(result); - Assert.Single(result[0].Extras); + Assert.Single(result.Where(v => v.ExtraType == null)); + Assert.Single(result.Where(v => v.ExtraType != null)); Assert.Equal(2, result[0].AlternateVersions.Count); } @@ -68,11 +60,7 @@ namespace Jellyfin.Naming.Tests.Video }; var result = VideoListResolver.Resolve( - files.Select(i => new FileSystemMetadata - { - IsDirectory = false, - FullName = i - }).ToList(), + files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType().ToList(), _namingOptions).ToList(); Assert.Single(result); @@ -94,15 +82,10 @@ namespace Jellyfin.Naming.Tests.Video }; var result = VideoListResolver.Resolve( - files.Select(i => new FileSystemMetadata - { - IsDirectory = false, - FullName = i - }).ToList(), + files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType().ToList(), _namingOptions).ToList(); Assert.Equal(7, result.Count); - Assert.Empty(result[0].Extras); Assert.Empty(result[0].AlternateVersions); } @@ -122,15 +105,10 @@ namespace Jellyfin.Naming.Tests.Video }; var result = VideoListResolver.Resolve( - files.Select(i => new FileSystemMetadata - { - IsDirectory = false, - FullName = i - }).ToList(), + files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType().ToList(), _namingOptions).ToList(); Assert.Single(result); - Assert.Empty(result[0].Extras); Assert.Equal(7, result[0].AlternateVersions.Count); } @@ -151,15 +129,10 @@ namespace Jellyfin.Naming.Tests.Video }; var result = VideoListResolver.Resolve( - files.Select(i => new FileSystemMetadata - { - IsDirectory = false, - FullName = i - }).ToList(), + files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType().ToList(), _namingOptions).ToList(); Assert.Equal(9, result.Count); - Assert.Empty(result[0].Extras); Assert.Empty(result[0].AlternateVersions); } @@ -176,15 +149,10 @@ namespace Jellyfin.Naming.Tests.Video }; var result = VideoListResolver.Resolve( - files.Select(i => new FileSystemMetadata - { - IsDirectory = false, - FullName = i - }).ToList(), + files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType().ToList(), _namingOptions).ToList(); Assert.Equal(5, result.Count); - Assert.Empty(result[0].Extras); Assert.Empty(result[0].AlternateVersions); } @@ -203,15 +171,10 @@ namespace Jellyfin.Naming.Tests.Video }; var result = VideoListResolver.Resolve( - files.Select(i => new FileSystemMetadata - { - IsDirectory = false, - FullName = i - }).ToList(), + files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType().ToList(), _namingOptions).ToList(); Assert.Equal(5, result.Count); - Assert.Empty(result[0].Extras); Assert.Empty(result[0].AlternateVersions); } @@ -231,15 +194,10 @@ namespace Jellyfin.Naming.Tests.Video }; var result = VideoListResolver.Resolve( - files.Select(i => new FileSystemMetadata - { - IsDirectory = false, - FullName = i - }).ToList(), + files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType().ToList(), _namingOptions).ToList(); Assert.Single(result); - Assert.Empty(result[0].Extras); Assert.Equal(7, result[0].AlternateVersions.Count); Assert.False(result[0].AlternateVersions[2].Is3D); Assert.True(result[0].AlternateVersions[3].Is3D); @@ -262,15 +220,10 @@ namespace Jellyfin.Naming.Tests.Video }; var result = VideoListResolver.Resolve( - files.Select(i => new FileSystemMetadata - { - IsDirectory = false, - FullName = i - }).ToList(), + files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType().ToList(), _namingOptions).ToList(); Assert.Single(result); - Assert.Empty(result[0].Extras); Assert.Equal(7, result[0].AlternateVersions.Count); Assert.False(result[0].AlternateVersions[3].Is3D); Assert.True(result[0].AlternateVersions[4].Is3D); @@ -287,11 +240,7 @@ namespace Jellyfin.Naming.Tests.Video }; var result = VideoListResolver.Resolve( - files.Select(i => new FileSystemMetadata - { - IsDirectory = false, - FullName = i - }).ToList(), + files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType().ToList(), _namingOptions).ToList(); Assert.Equal(2, result.Count); @@ -312,15 +261,10 @@ namespace Jellyfin.Naming.Tests.Video }; var result = VideoListResolver.Resolve( - files.Select(i => new FileSystemMetadata - { - IsDirectory = false, - FullName = i - }).ToList(), + files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType().ToList(), _namingOptions).ToList(); Assert.Equal(7, result.Count); - Assert.Empty(result[0].Extras); Assert.Empty(result[0].AlternateVersions); } @@ -339,15 +283,10 @@ namespace Jellyfin.Naming.Tests.Video }; var result = VideoListResolver.Resolve( - files.Select(i => new FileSystemMetadata - { - IsDirectory = false, - FullName = i - }).ToList(), + files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType().ToList(), _namingOptions).ToList(); Assert.Equal(5, result.Count); - Assert.Empty(result[0].Extras); Assert.Empty(result[0].AlternateVersions); } @@ -361,15 +300,10 @@ namespace Jellyfin.Naming.Tests.Video }; var result = VideoListResolver.Resolve( - files.Select(i => new FileSystemMetadata - { - IsDirectory = false, - FullName = i - }).ToList(), + files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType().ToList(), _namingOptions).ToList(); Assert.Single(result); - Assert.Empty(result[0].Extras); Assert.Single(result[0].AlternateVersions); } @@ -383,15 +317,10 @@ namespace Jellyfin.Naming.Tests.Video }; var result = VideoListResolver.Resolve( - files.Select(i => new FileSystemMetadata - { - IsDirectory = false, - FullName = i - }).ToList(), + files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType().ToList(), _namingOptions).ToList(); Assert.Single(result); - Assert.Empty(result[0].Extras); Assert.Single(result[0].AlternateVersions); } @@ -405,15 +334,10 @@ namespace Jellyfin.Naming.Tests.Video }; var result = VideoListResolver.Resolve( - files.Select(i => new FileSystemMetadata - { - IsDirectory = false, - FullName = i - }).ToList(), + files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType().ToList(), _namingOptions).ToList(); Assert.Single(result); - Assert.Empty(result[0].Extras); Assert.Single(result[0].AlternateVersions); } @@ -427,11 +351,7 @@ namespace Jellyfin.Naming.Tests.Video }; var result = VideoListResolver.Resolve( - files.Select(i => new FileSystemMetadata - { - IsDirectory = false, - FullName = i - }).ToList(), + files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType().ToList(), _namingOptions).ToList(); Assert.Equal(2, result.Count); @@ -440,7 +360,7 @@ namespace Jellyfin.Naming.Tests.Video [Fact] public void TestEmptyList() { - var result = VideoListResolver.Resolve(new List(), _namingOptions).ToList(); + var result = VideoListResolver.Resolve(new List(), _namingOptions).ToList(); Assert.Empty(result); } diff --git a/tests/Jellyfin.Naming.Tests/Video/StackTests.cs b/tests/Jellyfin.Naming.Tests/Video/StackTests.cs index 8794d3ebe..368c3592e 100644 --- a/tests/Jellyfin.Naming.Tests/Video/StackTests.cs +++ b/tests/Jellyfin.Naming.Tests/Video/StackTests.cs @@ -22,9 +22,7 @@ namespace Jellyfin.Naming.Tests.Video "Bad Boys (2006)-trailer.mkv" }; - var resolver = GetResolver(); - - var result = resolver.ResolveFiles(files).ToList(); + var result = StackResolver.ResolveFiles(files, _namingOptions).ToList(); Assert.Single(result); TestStackInfo(result[0], "Bad Boys (2006)", 4); @@ -39,9 +37,7 @@ namespace Jellyfin.Naming.Tests.Video "Bad Boys (2007).mkv" }; - var resolver = GetResolver(); - - var result = resolver.ResolveFiles(files).ToList(); + var result = StackResolver.ResolveFiles(files, _namingOptions).ToList(); Assert.Empty(result); } @@ -55,9 +51,7 @@ namespace Jellyfin.Naming.Tests.Video "Bad Boys 2007.mkv" }; - var resolver = GetResolver(); - - var result = resolver.ResolveFiles(files).ToList(); + var result = StackResolver.ResolveFiles(files, _namingOptions).ToList(); Assert.Empty(result); } @@ -71,9 +65,7 @@ namespace Jellyfin.Naming.Tests.Video "300 (2007).mkv" }; - var resolver = GetResolver(); - - var result = resolver.ResolveFiles(files).ToList(); + var result = StackResolver.ResolveFiles(files, _namingOptions).ToList(); Assert.Empty(result); } @@ -87,9 +79,7 @@ namespace Jellyfin.Naming.Tests.Video "300 2007.mkv" }; - var resolver = GetResolver(); - - var result = resolver.ResolveFiles(files).ToList(); + var result = StackResolver.ResolveFiles(files, _namingOptions).ToList(); Assert.Empty(result); } @@ -103,9 +93,7 @@ namespace Jellyfin.Naming.Tests.Video "Star Trek 2- The wrath of khan.mkv" }; - var resolver = GetResolver(); - - var result = resolver.ResolveFiles(files).ToList(); + var result = StackResolver.ResolveFiles(files, _namingOptions).ToList(); Assert.Empty(result); } @@ -119,9 +107,7 @@ namespace Jellyfin.Naming.Tests.Video "Red Riding in the Year of Our Lord 1974 (2009).mkv" }; - var resolver = GetResolver(); - - var result = resolver.ResolveFiles(files).ToList(); + var result = StackResolver.ResolveFiles(files, _namingOptions).ToList(); Assert.Empty(result); } @@ -135,16 +121,14 @@ namespace Jellyfin.Naming.Tests.Video "d:/movies/300 2006 part2.mkv" }; - var resolver = GetResolver(); - - var result = resolver.ResolveFiles(files).ToList(); + var result = StackResolver.ResolveFiles(files, _namingOptions).ToList(); Assert.Single(result); TestStackInfo(result[0], "300 2006", 2); } [Fact] - public void TestDirtyNames() + public void ResolveFiles_GivenPartInMiddleOfName_ReturnsNoStack() { var files = new[] { @@ -155,16 +139,13 @@ namespace Jellyfin.Naming.Tests.Video "Bad Boys (2006)-trailer.mkv" }; - var resolver = GetResolver(); + var result = StackResolver.ResolveFiles(files, _namingOptions).ToList(); - var result = resolver.ResolveFiles(files).ToList(); - - Assert.Single(result); - TestStackInfo(result[0], "Bad Boys (2006).stv.unrated.multi.1080p.bluray.x264-rough", 4); + Assert.Empty(result); } [Fact] - public void TestNumberedFiles() + public void ResolveFiles_FileNamesWithMissingPartType_ReturnsNoStack() { var files = new[] { @@ -175,9 +156,7 @@ namespace Jellyfin.Naming.Tests.Video "Bad Boys (2006)-trailer.mkv" }; - var resolver = GetResolver(); - - var result = resolver.ResolveFiles(files).ToList(); + var result = StackResolver.ResolveFiles(files, _namingOptions).ToList(); Assert.Empty(result); } @@ -194,9 +173,7 @@ namespace Jellyfin.Naming.Tests.Video "300 (2006)-trailer.mkv" }; - var resolver = GetResolver(); - - var result = resolver.ResolveFiles(files).ToList(); + var result = StackResolver.ResolveFiles(files, _namingOptions).ToList(); Assert.Single(result); TestStackInfo(result[0], "300 (2006)", 4); @@ -214,9 +191,7 @@ namespace Jellyfin.Naming.Tests.Video "Bad Boys (2006)-trailer.mkv" }; - var resolver = GetResolver(); - - var result = resolver.ResolveFiles(files).ToList(); + var result = StackResolver.ResolveFiles(files, _namingOptions).ToList(); Assert.Single(result); TestStackInfo(result[0], "Bad Boys (2006)", 3); @@ -238,9 +213,7 @@ namespace Jellyfin.Naming.Tests.Video "300 (2006)-trailer.mkv" }; - var resolver = GetResolver(); - - var result = resolver.ResolveFiles(files).ToList(); + var result = StackResolver.ResolveFiles(files, _namingOptions).ToList(); Assert.Equal(2, result.Count); TestStackInfo(result[1], "Bad Boys (2006)", 4); @@ -256,9 +229,7 @@ namespace Jellyfin.Naming.Tests.Video "blah blah - cd 2" }; - var resolver = GetResolver(); - - var result = resolver.ResolveDirectories(files).ToList(); + var result = StackResolver.ResolveDirectories(files, _namingOptions).ToList(); Assert.Single(result); TestStackInfo(result[0], "blah blah", 2); @@ -275,9 +246,7 @@ namespace Jellyfin.Naming.Tests.Video "300-trailer.mkv" }; - var resolver = GetResolver(); - - var result = resolver.ResolveFiles(files).ToList(); + var result = StackResolver.ResolveFiles(files, _namingOptions).ToList(); Assert.Single(result); @@ -297,9 +266,7 @@ namespace Jellyfin.Naming.Tests.Video "Avengers part3.mkv" }; - var resolver = GetResolver(); - - var result = resolver.ResolveFiles(files).ToList(); + var result = StackResolver.ResolveFiles(files, _namingOptions).ToList(); Assert.Equal(2, result.Count); @@ -328,9 +295,7 @@ namespace Jellyfin.Naming.Tests.Video "300-trailer.mkv" }; - var resolver = GetResolver(); - - var result = resolver.ResolveFiles(files).ToList(); + var result = StackResolver.ResolveFiles(files, _namingOptions).ToList(); Assert.Equal(3, result.Count); @@ -354,9 +319,7 @@ namespace Jellyfin.Naming.Tests.Video "300 (2006)-trailer.mkv" }; - var resolver = GetResolver(); - - var result = resolver.ResolveFiles(files).ToList(); + var result = StackResolver.ResolveFiles(files, _namingOptions).ToList(); Assert.Single(result); @@ -375,9 +338,7 @@ namespace Jellyfin.Naming.Tests.Video new FileSystemMetadata { FullName = "300 (2006) part1", IsDirectory = true } }; - var resolver = GetResolver(); - - var result = resolver.Resolve(files).ToList(); + var result = StackResolver.Resolve(files, _namingOptions).ToList(); Assert.Equal(2, result.Count); TestStackInfo(result[0], "300 (2006)", 3); @@ -397,9 +358,7 @@ namespace Jellyfin.Naming.Tests.Video "Harry Potter and the Deathly Hallows 4.mkv" }; - var resolver = GetResolver(); - - var result = resolver.ResolveFiles(files).ToList(); + var result = StackResolver.ResolveFiles(files, _namingOptions).ToList(); Assert.Empty(result); } @@ -414,9 +373,7 @@ namespace Jellyfin.Naming.Tests.Video "Neverland (2011)[720p][PG][Voted 6.5][Family-Fantasy]part2.mkv" }; - var resolver = GetResolver(); - - var result = resolver.ResolveFiles(files).ToList(); + var result = StackResolver.ResolveFiles(files, _namingOptions).ToList(); Assert.Single(result); Assert.Equal(2, result[0].Files.Count); @@ -432,9 +389,7 @@ namespace Jellyfin.Naming.Tests.Video @"M:/Movies (DVD)/Movies (Musical)/The Sound of Music/The Sound of Music (1965) (Disc 02)" }; - var resolver = GetResolver(); - - var result = resolver.ResolveDirectories(files).ToList(); + var result = StackResolver.ResolveDirectories(files, _namingOptions).ToList(); Assert.Single(result); Assert.Equal(2, result[0].Files.Count); @@ -445,10 +400,5 @@ namespace Jellyfin.Naming.Tests.Video Assert.Equal(fileCount, stack.Files.Count); Assert.Equal(name, stack.Name); } - - private StackResolver GetResolver() - { - return new StackResolver(_namingOptions); - } } } diff --git a/tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs b/tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs index 9e0776c3c..b76187842 100644 --- a/tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs +++ b/tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs @@ -2,6 +2,7 @@ using System; using System.Linq; using Emby.Naming.Common; using Emby.Naming.Video; +using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; using Xunit; @@ -41,23 +42,28 @@ namespace Jellyfin.Naming.Tests.Video }; var result = VideoListResolver.Resolve( - files.Select(i => new FileSystemMetadata - { - IsDirectory = false, - FullName = i - }).ToList(), + files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType().ToList(), _namingOptions).ToList(); - Assert.Equal(5, result.Count); + Assert.Equal(11, result.Count); var batman = result.FirstOrDefault(x => string.Equals(x.Name, "Batman", StringComparison.Ordinal)); Assert.NotNull(batman); Assert.Equal(3, batman!.Files.Count); - Assert.Equal(3, batman!.Extras.Count); var harry = result.FirstOrDefault(x => string.Equals(x.Name, "Harry Potter and the Deathly Hallows", StringComparison.Ordinal)); Assert.NotNull(harry); Assert.Equal(4, harry!.Files.Count); - Assert.Equal(2, harry!.Extras.Count); + + Assert.False(result[2].ExtraType.HasValue); + + Assert.Equal(ExtraType.Trailer, result[3].ExtraType); + Assert.Equal(ExtraType.Trailer, result[4].ExtraType); + Assert.Equal(ExtraType.DeletedScene, result[5].ExtraType); + Assert.Equal(ExtraType.Sample, result[6].ExtraType); + Assert.Equal(ExtraType.Trailer, result[7].ExtraType); + Assert.Equal(ExtraType.Trailer, result[8].ExtraType); + Assert.Equal(ExtraType.Trailer, result[9].ExtraType); + Assert.Equal(ExtraType.Trailer, result[10].ExtraType); } [Fact] @@ -70,11 +76,7 @@ namespace Jellyfin.Naming.Tests.Video }; var result = VideoListResolver.Resolve( - files.Select(i => new FileSystemMetadata - { - IsDirectory = false, - FullName = i - }).ToList(), + files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType().ToList(), _namingOptions).ToList(); Assert.Single(result); @@ -90,14 +92,12 @@ namespace Jellyfin.Naming.Tests.Video }; var result = VideoListResolver.Resolve( - files.Select(i => new FileSystemMetadata - { - IsDirectory = false, - FullName = i - }).ToList(), + files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType().ToList(), _namingOptions).ToList(); - Assert.Single(result); + Assert.Equal(2, result.Count); + Assert.False(result[0].ExtraType.HasValue); + Assert.Equal(ExtraType.Trailer, result[1].ExtraType); } [Fact] @@ -110,14 +110,12 @@ namespace Jellyfin.Naming.Tests.Video }; var result = VideoListResolver.Resolve( - files.Select(i => new FileSystemMetadata - { - IsDirectory = false, - FullName = i - }).ToList(), + files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType().ToList(), _namingOptions).ToList(); - Assert.Single(result); + Assert.Equal(2, result.Count); + Assert.False(result[0].ExtraType.HasValue); + Assert.Equal(ExtraType.Trailer, result[1].ExtraType); } [Fact] @@ -131,34 +129,51 @@ namespace Jellyfin.Naming.Tests.Video }; var result = VideoListResolver.Resolve( - files.Select(i => new FileSystemMetadata - { - IsDirectory = false, - FullName = i - }).ToList(), + files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType().ToList(), _namingOptions).ToList(); - Assert.Single(result); + Assert.Equal(3, result.Count); + Assert.False(result[0].ExtraType.HasValue); + Assert.Equal(ExtraType.Trailer, result[1].ExtraType); + Assert.Equal(ExtraType.Trailer, result[2].ExtraType); } [Fact] - public void TestDifferentNames() + public void Resolve_SameNameAndYear_ReturnsSingleItem() { var files = new[] { "Looper (2012)-trailer.mkv", + "Looper 2012-trailer.mkv", "Looper.2012.bluray.720p.x264.mkv" }; var result = VideoListResolver.Resolve( - files.Select(i => new FileSystemMetadata - { - IsDirectory = false, - FullName = i - }).ToList(), + files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType().ToList(), _namingOptions).ToList(); - Assert.Single(result); + Assert.Equal(3, result.Count); + Assert.False(result[0].ExtraType.HasValue); + Assert.Equal(ExtraType.Trailer, result[1].ExtraType); + Assert.Equal(ExtraType.Trailer, result[2].ExtraType); + } + + [Fact] + public void Resolve_TrailerMatchesFolderName_ReturnsSingleItem() + { + var files = new[] + { + "/movies/Looper (2012)/Looper (2012)-trailer.mkv", + "/movies/Looper (2012)/Looper.bluray.720p.x264.mkv" + }; + + var result = VideoListResolver.Resolve( + files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType().ToList(), + _namingOptions).ToList(); + + Assert.Equal(2, result.Count); + Assert.False(result[0].ExtraType.HasValue); + Assert.Equal(ExtraType.Trailer, result[1].ExtraType); } [Fact] @@ -175,11 +190,7 @@ namespace Jellyfin.Naming.Tests.Video }; var result = VideoListResolver.Resolve( - files.Select(i => new FileSystemMetadata - { - IsDirectory = false, - FullName = i - }).ToList(), + files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType().ToList(), _namingOptions).ToList(); Assert.Equal(5, result.Count); @@ -195,11 +206,7 @@ namespace Jellyfin.Naming.Tests.Video }; var result = VideoListResolver.Resolve( - files.Select(i => new FileSystemMetadata - { - IsDirectory = true, - FullName = i - }).ToList(), + files.Select(i => VideoResolver.Resolve(i, true, _namingOptions)).OfType().ToList(), _namingOptions).ToList(); Assert.Single(result); @@ -216,11 +223,7 @@ namespace Jellyfin.Naming.Tests.Video }; var result = VideoListResolver.Resolve( - files.Select(i => new FileSystemMetadata - { - IsDirectory = true, - FullName = i - }).ToList(), + files.Select(i => VideoResolver.Resolve(i, true, _namingOptions)).OfType().ToList(), _namingOptions).ToList(); Assert.Equal(2, result.Count); @@ -233,39 +236,18 @@ namespace Jellyfin.Naming.Tests.Video { @"No (2012) part1.mp4", @"No (2012) part2.mp4", - @"No (2012) part1-trailer.mp4" - }; - - var result = VideoListResolver.Resolve( - files.Select(i => new FileSystemMetadata - { - IsDirectory = false, - FullName = i - }).ToList(), - _namingOptions).ToList(); - - Assert.Single(result); - } - - [Fact] - public void TestStackedWithTrailer2() - { - var files = new[] - { - @"No (2012) part1.mp4", - @"No (2012) part2.mp4", + @"No (2012) part1-trailer.mp4", @"No (2012)-trailer.mp4" }; var result = VideoListResolver.Resolve( - files.Select(i => new FileSystemMetadata - { - IsDirectory = false, - FullName = i - }).ToList(), + files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType().ToList(), _namingOptions).ToList(); - Assert.Single(result); + Assert.Equal(3, result.Count); + Assert.False(result[0].ExtraType.HasValue); + Assert.Equal(ExtraType.Trailer, result[1].ExtraType); + Assert.Equal(ExtraType.Trailer, result[2].ExtraType); } [Fact] @@ -276,18 +258,18 @@ namespace Jellyfin.Naming.Tests.Video @"/Movies/Top Gun (1984)/movie.mp4", @"/Movies/Top Gun (1984)/Top Gun (1984)-trailer.mp4", @"/Movies/Top Gun (1984)/Top Gun (1984)-trailer2.mp4", - @"trailer.mp4" + @"/Movies/trailer.mp4" }; var result = VideoListResolver.Resolve( - files.Select(i => new FileSystemMetadata - { - IsDirectory = false, - FullName = i - }).ToList(), + files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType().ToList(), _namingOptions).ToList(); - Assert.Single(result); + Assert.Equal(4, result.Count); + Assert.False(result[0].ExtraType.HasValue); + Assert.Equal(ExtraType.Trailer, result[1].ExtraType); + Assert.Equal(ExtraType.Trailer, result[2].ExtraType); + Assert.Equal(ExtraType.Trailer, result[3].ExtraType); } [Fact] @@ -302,11 +284,7 @@ namespace Jellyfin.Naming.Tests.Video }; var result = VideoListResolver.Resolve( - files.Select(i => new FileSystemMetadata - { - IsDirectory = false, - FullName = i - }).ToList(), + files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType().ToList(), _namingOptions).ToList(); Assert.Equal(2, result.Count); @@ -321,11 +299,7 @@ namespace Jellyfin.Naming.Tests.Video }; var result = VideoListResolver.Resolve( - files.Select(i => new FileSystemMetadata - { - IsDirectory = false, - FullName = i - }).ToList(), + files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType().ToList(), _namingOptions).ToList(); Assert.Single(result); @@ -340,11 +314,7 @@ namespace Jellyfin.Naming.Tests.Video }; var result = VideoListResolver.Resolve( - files.Select(i => new FileSystemMetadata - { - IsDirectory = false, - FullName = i - }).ToList(), + files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType().ToList(), _namingOptions).ToList(); Assert.Single(result); @@ -360,11 +330,7 @@ namespace Jellyfin.Naming.Tests.Video }; var result = VideoListResolver.Resolve( - files.Select(i => new FileSystemMetadata - { - IsDirectory = false, - FullName = i - }).ToList(), + files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType().ToList(), _namingOptions).ToList(); Assert.Single(result); @@ -380,11 +346,7 @@ namespace Jellyfin.Naming.Tests.Video }; var result = VideoListResolver.Resolve( - files.Select(i => new FileSystemMetadata - { - IsDirectory = false, - FullName = i - }).ToList(), + files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType().ToList(), _namingOptions).ToList(); Assert.Equal(2, result.Count); @@ -396,40 +358,34 @@ namespace Jellyfin.Naming.Tests.Video var files = new[] { @"/Server/Despicable Me/Despicable Me (2010).mkv", - @"/Server/Despicable Me/movie-trailer.mkv" + @"/Server/Despicable Me/trailer.mkv" }; var result = VideoListResolver.Resolve( - files.Select(i => new FileSystemMetadata - { - IsDirectory = false, - FullName = i - }).ToList(), + files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType().ToList(), _namingOptions).ToList(); - Assert.Single(result); + Assert.Equal(2, result.Count); + Assert.False(result[0].ExtraType.HasValue); + Assert.Equal(ExtraType.Trailer, result[1].ExtraType); } [Fact] - public void TestTrailerFalsePositives() + public void Resolve_TrailerInTrailersFolder_ReturnsCorrectExtraType() { var files = new[] { - @"/Server/Despicable Me/Skyscraper (2018) - Big Game Spot.mkv", - @"/Server/Despicable Me/Skyscraper (2018) - Trailer.mkv", - @"/Server/Despicable Me/Baywatch (2017) - Big Game Spot.mkv", - @"/Server/Despicable Me/Baywatch (2017) - Trailer.mkv" + @"/Server/Despicable Me/Despicable Me (2010).mkv", + @"/Server/Despicable Me/trailers/some title.mkv" }; var result = VideoListResolver.Resolve( - files.Select(i => new FileSystemMetadata - { - IsDirectory = false, - FullName = i - }).ToList(), + files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType().ToList(), _namingOptions).ToList(); - Assert.Equal(4, result.Count); + Assert.Equal(2, result.Count); + Assert.False(result[0].ExtraType.HasValue); + Assert.Equal(ExtraType.Trailer, result[1].ExtraType); } [Fact] @@ -442,20 +398,18 @@ namespace Jellyfin.Naming.Tests.Video }; var result = VideoListResolver.Resolve( - files.Select(i => new FileSystemMetadata - { - IsDirectory = false, - FullName = i - }).ToList(), + files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType().ToList(), _namingOptions).ToList(); - Assert.Single(result); + Assert.Equal(2, result.Count); + Assert.False(result[0].ExtraType.HasValue); + Assert.Equal(ExtraType.Trailer, result[1].ExtraType); } [Fact] public void TestDirectoryStack() { - var stack = new FileStack(); + var stack = new FileStack(string.Empty, false, Array.Empty()); Assert.False(stack.ContainsFile("XX", true)); } } diff --git a/tests/Jellyfin.Providers.Tests/MediaInfo/SubtitleResolverTests.cs b/tests/Jellyfin.Providers.Tests/MediaInfo/SubtitleResolverTests.cs index c289a7112..33da277e3 100644 --- a/tests/Jellyfin.Providers.Tests/MediaInfo/SubtitleResolverTests.cs +++ b/tests/Jellyfin.Providers.Tests/MediaInfo/SubtitleResolverTests.cs @@ -80,6 +80,37 @@ namespace Jellyfin.Providers.Tests.MediaInfo } } + [Theory] + [InlineData("/video/My Video.mkv", "/video/My Video.srt", "srt", null, false, false)] + [InlineData("/video/My.Video.mkv", "/video/My.Video.srt", "srt", null, false, false)] + [InlineData("/video/My.Video.mkv", "/video/My.Video.foreign.srt", "srt", null, true, false)] + [InlineData("/video/My Video.mkv", "/video/My Video.forced.srt", "srt", null, true, false)] + [InlineData("/video/My.Video.mkv", "/video/My.Video.default.srt", "srt", null, false, true)] + [InlineData("/video/My.Video.mkv", "/video/My.Video.forced.default.srt", "srt", null, true, true)] + [InlineData("/video/My.Video.mkv", "/video/My.Video.en.srt", "srt", "en", false, false)] + [InlineData("/video/My.Video.mkv", "/video/My.Video.default.en.srt", "srt", "en", false, true)] + [InlineData("/video/My.Video.mkv", "/video/My.Video.default.forced.en.srt", "srt", "en", true, true)] + [InlineData("/video/My.Video.mkv", "/video/My.Video.en.default.forced.srt", "srt", "en", true, true)] + public void AddExternalSubtitleStreams_GivenSingleFile_ReturnsExpectedSubtitle(string videoPath, string file, string codec, string? language, bool isForced, bool isDefault) + { + var streams = new List(); + var expected = CreateMediaStream(file, codec, language, 0, isForced, isDefault); + + new SubtitleResolver(Mock.Of()).AddExternalSubtitleStreams(streams, videoPath, 0, new[] { file }); + + Assert.Single(streams); + + var actual = streams[0]; + + Assert.Equal(expected.Index, actual.Index); + Assert.Equal(expected.Type, actual.Type); + Assert.Equal(expected.IsExternal, actual.IsExternal); + Assert.Equal(expected.Path, actual.Path); + Assert.Equal(expected.IsDefault, actual.IsDefault); + Assert.Equal(expected.IsForced, actual.IsForced); + Assert.Equal(expected.Language, actual.Language); + } + private static MediaStream CreateMediaStream(string path, string codec, string? language, int index, bool isForced = false, bool isDefault = false) { return new () diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/EpisodeResolverTest.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/EpisodeResolverTest.cs index a0fe4a5cf..362c3216f 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Library/EpisodeResolverTest.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/Library/EpisodeResolverTest.cs @@ -1,4 +1,5 @@ -using Emby.Server.Implementations.Library.Resolvers.TV; +using Emby.Naming.Common; +using Emby.Server.Implementations.Library.Resolvers.TV; using MediaBrowser.Controller; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.TV; @@ -13,12 +14,14 @@ namespace Jellyfin.Server.Implementations.Tests.Library { public class EpisodeResolverTest { + private static readonly NamingOptions _namingOptions = new (); + [Fact] public void Resolve_GivenVideoInExtrasFolder_DoesNotResolveToEpisode() { var parent = new Folder { Name = "extras" }; - var episodeResolver = new EpisodeResolver(null); + var episodeResolver = new EpisodeResolver(_namingOptions); var itemResolveArgs = new ItemResolveArgs( Mock.Of(), Mock.Of()) @@ -41,14 +44,14 @@ namespace Jellyfin.Server.Implementations.Tests.Library // Have to create a mock because of moq proxies not being castable to a concrete implementation // https://github.com/jellyfin/jellyfin/blob/ab0cff8556403e123642dc9717ba778329554634/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs#L48 - var episodeResolver = new EpisodeResolverMock(); + var episodeResolver = new EpisodeResolverMock(_namingOptions); var itemResolveArgs = new ItemResolveArgs( Mock.Of(), Mock.Of()) { Parent = series, CollectionType = CollectionType.TvShows, - FileInfo = new FileSystemMetadata() + FileInfo = new FileSystemMetadata { FullName = "Extras/Extras S01E01.mkv" } @@ -58,7 +61,7 @@ namespace Jellyfin.Server.Implementations.Tests.Library private class EpisodeResolverMock : EpisodeResolver { - public EpisodeResolverMock() : base(null) + public EpisodeResolverMock(NamingOptions namingOptions) : base(namingOptions) { } diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/LibraryManager/FindExtrasTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/LibraryManager/FindExtrasTests.cs new file mode 100644 index 000000000..b29426d85 --- /dev/null +++ b/tests/Jellyfin.Server.Implementations.Tests/Library/LibraryManager/FindExtrasTests.cs @@ -0,0 +1,232 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using AutoFixture; +using AutoFixture.AutoMoq; +using Emby.Naming.Common; +using Emby.Server.Implementations.Library.Resolvers; +using Emby.Server.Implementations.Library.Resolvers.Audio; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Movies; +using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Controller.Resolvers; +using MediaBrowser.Controller.Sorting; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.IO; +using Moq; +using Xunit; + +namespace Jellyfin.Server.Implementations.Tests.Library.LibraryManager; + +public class FindExtrasTests +{ + private readonly Emby.Server.Implementations.Library.LibraryManager _libraryManager; + private readonly Mock _fileSystemMock; + + public FindExtrasTests() + { + var fixture = new Fixture().Customize(new AutoMoqCustomization()); + fixture.Register(() => new NamingOptions()); + var configMock = fixture.Freeze>(); + configMock.Setup(c => c.ApplicationPaths.ProgramDataPath).Returns("/data"); + _fileSystemMock = fixture.Freeze>(); + _fileSystemMock.Setup(f => f.GetFileInfo(It.IsAny())).Returns(path => new FileSystemMetadata { FullName = path }); + _libraryManager = fixture.Build().Do(s => s.AddParts( + fixture.Create>(), + new List { new GenericVideoResolver