diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 5779ac3cf9..41d59e2435 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -27,11 +27,11 @@ jobs: dotnet-version: '7.0.x' - name: Initialize CodeQL - uses: github/codeql-action/init@3ebbd71c74ef574dbc558c82f70e52732c8b44fe # v2 + uses: github/codeql-action/init@17573ee1cc1b9d061760f3a006fc4aac4f944fd5 # v2 with: languages: ${{ matrix.language }} queries: +security-extended - name: Autobuild - uses: github/codeql-action/autobuild@3ebbd71c74ef574dbc558c82f70e52732c8b44fe # v2 + uses: github/codeql-action/autobuild@17573ee1cc1b9d061760f3a006fc4aac4f944fd5 # v2 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@3ebbd71c74ef574dbc558c82f70e52732c8b44fe # v2 + uses: github/codeql-action/analyze@17573ee1cc1b9d061760f3a006fc4aac4f944fd5 # v2 diff --git a/.github/workflows/commands.yml b/.github/workflows/commands.yml index 5d945c001b..75227c57b9 100644 --- a/.github/workflows/commands.yml +++ b/.github/workflows/commands.yml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Notify as seen - uses: peter-evans/create-or-update-comment@5adcb0bb0f9fb3f95ef05400558bdb3f329ee808 # tag=v2 + uses: peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d # v2 with: token: ${{ secrets.JF_BOT_TOKEN }} comment-id: ${{ github.event.comment.id }} @@ -43,7 +43,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Notify as seen - uses: peter-evans/create-or-update-comment@5adcb0bb0f9fb3f95ef05400558bdb3f329ee808 # tag=v2 + uses: peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d # v2 if: ${{ github.event.comment != null }} with: token: ${{ secrets.JF_BOT_TOKEN }} @@ -58,7 +58,7 @@ jobs: - name: Notify as running id: comment_running - uses: peter-evans/create-or-update-comment@5adcb0bb0f9fb3f95ef05400558bdb3f329ee808 # tag=v2 + uses: peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d # v2 if: ${{ github.event.comment != null }} with: token: ${{ secrets.JF_BOT_TOKEN }} @@ -93,7 +93,7 @@ jobs: exit ${retcode} - name: Notify with result success - uses: peter-evans/create-or-update-comment@5adcb0bb0f9fb3f95ef05400558bdb3f329ee808 # tag=v2 + uses: peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d # v2 if: ${{ github.event.comment != null && success() }} with: token: ${{ secrets.JF_BOT_TOKEN }} @@ -108,7 +108,7 @@ jobs: reactions: hooray - name: Notify with result failure - uses: peter-evans/create-or-update-comment@5adcb0bb0f9fb3f95ef05400558bdb3f329ee808 # tag=v2 + uses: peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d # v2 if: ${{ github.event.comment != null && failure() }} with: token: ${{ secrets.JF_BOT_TOKEN }} diff --git a/.github/workflows/openapi.yml b/.github/workflows/openapi.yml index 4577ff5251..5ba53af865 100644 --- a/.github/workflows/openapi.yml +++ b/.github/workflows/openapi.yml @@ -103,14 +103,14 @@ jobs: body="${body//$'\r'/'%0D'}" echo ::set-output name=body::$body - name: Find difference comment - uses: peter-evans/find-comment@81e2da3af01c92f83cb927cf3ace0e085617c556 # v2 + uses: peter-evans/find-comment@85a676a52594b4481e0532825a2d8906ef96dac2 # v2 id: find-comment with: issue-number: ${{ github.event.pull_request.number }} direction: last body-includes: openapi-diff-workflow-comment - name: Reply or edit difference comment (changed) - uses: peter-evans/create-or-update-comment@5adcb0bb0f9fb3f95ef05400558bdb3f329ee808 # tag=v2 + uses: peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d # v2 if: ${{ steps.read-diff.outputs.body != '' }} with: issue-number: ${{ github.event.pull_request.number }} @@ -125,7 +125,7 @@ jobs: - name: Edit difference comment (unchanged) - uses: peter-evans/create-or-update-comment@5adcb0bb0f9fb3f95ef05400558bdb3f329ee808 # tag=v2 + uses: peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d # v2 if: ${{ steps.read-diff.outputs.body == '' && steps.find-comment.outputs.comment-id != '' }} with: issue-number: ${{ github.event.pull_request.number }} diff --git a/.npmrc b/.npmrc deleted file mode 100644 index b7a317000b..0000000000 --- a/.npmrc +++ /dev/null @@ -1,3 +0,0 @@ -registry=https://registry.npmjs.org/ -@jellyfin:registry=https://pkgs.dev.azure.com/jellyfin-project/jellyfin/_packaging/unstable/npm/registry/ -always-auth=true \ No newline at end of file diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 5884c7376b..20ea7a4ab7 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -232,3 +232,4 @@ - [Matthew Jones](https://github.com/matthew-jones-uk) - [Jakob Kukla](https://github.com/jakobkukla) - [Utku Özdemir](https://github.com/utkuozdemir) + - [JPUC1143](https://github.com/Jpuc1143/) diff --git a/Directory.Packages.props b/Directory.Packages.props index 26f0692115..5060b3de56 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -8,7 +8,7 @@ - + @@ -23,29 +23,29 @@ - - + + - - - - + + + + - + - - + + - - + + @@ -53,9 +53,9 @@ - + - + @@ -77,7 +77,7 @@ - + diff --git a/Emby.Dlna/Server/DescriptionXmlBuilder.cs b/Emby.Dlna/Server/DescriptionXmlBuilder.cs index d00df781d6..69ef6f6456 100644 --- a/Emby.Dlna/Server/DescriptionXmlBuilder.cs +++ b/Emby.Dlna/Server/DescriptionXmlBuilder.cs @@ -147,11 +147,16 @@ namespace Emby.Dlna.Server } } - private string GetFriendlyName() + internal string GetFriendlyName() { if (string.IsNullOrEmpty(_profile.FriendlyName)) { - return "Jellyfin - " + _serverName; + return _serverName; + } + + if (!_profile.FriendlyName.Contains("${HostName}", StringComparison.OrdinalIgnoreCase)) + { + return _profile.FriendlyName; } var characterList = new List(); @@ -164,13 +169,18 @@ namespace Emby.Dlna.Server } } - var characters = characterList.ToArray(); + var serverName = string.Create( + characterList.Count, + characterList, + (dest, source) => + { + for (int i = 0; i < dest.Length; i++) + { + dest[i] = source[i]; + } + }); - var serverName = new string(characters); - - var name = _profile.FriendlyName?.Replace("${HostName}", serverName, StringComparison.OrdinalIgnoreCase); - - return name ?? string.Empty; + return _profile.FriendlyName.Replace("${HostName}", serverName, StringComparison.OrdinalIgnoreCase); } private void AppendIconList(StringBuilder builder) diff --git a/Emby.Naming/Audio/AlbumParser.cs b/Emby.Naming/Audio/AlbumParser.cs index bbfdccc902..86a5641531 100644 --- a/Emby.Naming/Audio/AlbumParser.cs +++ b/Emby.Naming/Audio/AlbumParser.cs @@ -3,6 +3,7 @@ using System.Globalization; using System.IO; using System.Text.RegularExpressions; using Emby.Naming.Common; +using Jellyfin.Extensions; namespace Emby.Naming.Audio { @@ -58,13 +59,7 @@ namespace Emby.Naming.Audio var tmp = trimmedFilename.Slice(prefix.Length).Trim(); - int index = tmp.IndexOf(' '); - if (index != -1) - { - tmp = tmp.Slice(0, index); - } - - if (int.TryParse(tmp, NumberStyles.Integer, CultureInfo.InvariantCulture, out _)) + if (int.TryParse(tmp.LeftPart(' '), CultureInfo.InvariantCulture, out _)) { return true; } diff --git a/Emby.Naming/AudioBook/AudioBookFilePathParser.cs b/Emby.Naming/AudioBook/AudioBookFilePathParser.cs index 7b4429ab15..75fdedfeab 100644 --- a/Emby.Naming/AudioBook/AudioBookFilePathParser.cs +++ b/Emby.Naming/AudioBook/AudioBookFilePathParser.cs @@ -32,7 +32,7 @@ namespace Emby.Naming.AudioBook var fileName = Path.GetFileNameWithoutExtension(path); foreach (var expression in _options.AudioBookPartsExpressions) { - var match = new Regex(expression, RegexOptions.IgnoreCase).Match(fileName); + var match = Regex.Match(fileName, expression, RegexOptions.IgnoreCase); if (match.Success) { if (!result.ChapterNumber.HasValue) @@ -40,7 +40,7 @@ namespace Emby.Naming.AudioBook var value = match.Groups["chapter"]; if (value.Success) { - if (int.TryParse(value.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intValue)) + if (int.TryParse(value.ValueSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intValue)) { result.ChapterNumber = intValue; } @@ -52,7 +52,7 @@ namespace Emby.Naming.AudioBook var value = match.Groups["part"]; if (value.Success) { - if (int.TryParse(value.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intValue)) + if (int.TryParse(value.ValueSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intValue)) { result.PartNumber = intValue; } diff --git a/Emby.Naming/AudioBook/AudioBookNameParser.cs b/Emby.Naming/AudioBook/AudioBookNameParser.cs index 97b34199e0..5ea649dbf7 100644 --- a/Emby.Naming/AudioBook/AudioBookNameParser.cs +++ b/Emby.Naming/AudioBook/AudioBookNameParser.cs @@ -30,7 +30,7 @@ namespace Emby.Naming.AudioBook AudioBookNameParserResult result = default; foreach (var expression in _options.AudioBookNamesExpressions) { - var match = new Regex(expression, RegexOptions.IgnoreCase).Match(name); + var match = Regex.Match(name, expression, RegexOptions.IgnoreCase); if (match.Success) { if (result.Name is null) @@ -47,7 +47,7 @@ namespace Emby.Naming.AudioBook var value = match.Groups["year"]; if (value.Success) { - if (int.TryParse(value.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intValue)) + if (int.TryParse(value.ValueSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intValue)) { result.Year = intValue; } diff --git a/Emby.Naming/Common/NamingOptions.cs b/Emby.Naming/Common/NamingOptions.cs index 54f62a1570..e9161a6b7b 100644 --- a/Emby.Naming/Common/NamingOptions.cs +++ b/Emby.Naming/Common/NamingOptions.cs @@ -338,7 +338,15 @@ namespace Emby.Naming.Common } }, - // This isn't a Kodi naming rule, but the expression below causes false positives, + // This isn't a Kodi naming rule, but the expression below causes false episode numbers for + // Title Season X Episode X naming schemes. + // "Series Season X Episode X - Title.avi", "Series S03 E09.avi", "s3 e9 - Title.avi" + new EpisodeExpression(@".*[\\\/]((?[^\\/]+?)\s)?[Ss](?:eason)?\s*(?[0-9]+)\s+[Ee](?:pisode)?\s*(?[0-9]+).*$") + { + IsNamed = true + }, + + // Not a Kodi rule as well, but the expression below also causes false positives, // so we make sure this one gets tested first. // "Foo Bar 889" new EpisodeExpression(@".*[\\\/](?![Ee]pisode)(?[\w\s]+?)\s(?[0-9]{1,4})(-(?[0-9]{2,4}))*[^\\\/x]*$") @@ -453,16 +461,6 @@ namespace Emby.Naming.Common }, }; - EpisodeWithoutSeasonExpressions = new[] - { - @"[/\._ \-]()([0-9]+)(-[0-9]+)?" - }; - - EpisodeMultiPartExpressions = new[] - { - @"^[-_ex]+([0-9]+(?:(?:[a-i]|\\.[1-9])(?![0-9]))?)" - }; - VideoExtraRules = new[] { new ExtraRule( @@ -797,16 +795,6 @@ namespace Emby.Naming.Common /// public EpisodeExpression[] EpisodeExpressions { get; set; } - /// - /// Gets or sets list of raw episode without season regular expressions strings. - /// - public string[] EpisodeWithoutSeasonExpressions { get; set; } - - /// - /// Gets or sets list of raw multi-part episodes regular expressions strings. - /// - public string[] EpisodeMultiPartExpressions { get; set; } - /// /// Gets or sets list of video file extensions. /// @@ -877,16 +865,6 @@ namespace Emby.Naming.Common /// public Regex[] CleanStringRegexes { get; private set; } = Array.Empty(); - /// - /// Gets list of episode without season regular expressions. - /// - public Regex[] EpisodeWithoutSeasonRegexes { get; private set; } = Array.Empty(); - - /// - /// Gets list of multi-part episode regular expressions. - /// - public Regex[] EpisodeMultiPartRegexes { get; private set; } = Array.Empty(); - /// /// Compiles raw regex strings into regexes. /// @@ -894,8 +872,6 @@ namespace Emby.Naming.Common { CleanDateTimeRegexes = CleanDateTimes.Select(Compile).ToArray(); CleanStringRegexes = CleanStrings.Select(Compile).ToArray(); - EpisodeWithoutSeasonRegexes = EpisodeWithoutSeasonExpressions.Select(Compile).ToArray(); - EpisodeMultiPartRegexes = EpisodeMultiPartExpressions.Select(Compile).ToArray(); } private Regex Compile(string exp) diff --git a/Emby.Naming/TV/EpisodePathParser.cs b/Emby.Naming/TV/EpisodePathParser.cs index d706be2802..8cd5a126e0 100644 --- a/Emby.Naming/TV/EpisodePathParser.cs +++ b/Emby.Naming/TV/EpisodePathParser.cs @@ -113,7 +113,7 @@ namespace Emby.Naming.TV if (expression.DateTimeFormats.Length > 0) { if (DateTime.TryParseExact( - match.Groups[0].Value, + match.Groups[0].ValueSpan, expression.DateTimeFormats, CultureInfo.InvariantCulture, DateTimeStyles.None, @@ -125,7 +125,7 @@ namespace Emby.Naming.TV result.Success = true; } } - else if (DateTime.TryParse(match.Groups[0].Value, out date)) + else if (DateTime.TryParse(match.Groups[0].ValueSpan, out date)) { result.Year = date.Year; result.Month = date.Month; @@ -138,12 +138,12 @@ namespace Emby.Naming.TV } else if (expression.IsNamed) { - if (int.TryParse(match.Groups["seasonnumber"].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var num)) + if (int.TryParse(match.Groups["seasonnumber"].ValueSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out var num)) { result.SeasonNumber = num; } - if (int.TryParse(match.Groups["epnumber"].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out num)) + if (int.TryParse(match.Groups["epnumber"].ValueSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out num)) { result.EpisodeNumber = num; } @@ -158,7 +158,7 @@ namespace Emby.Naming.TV if (nextIndex >= name.Length || !"0123456789iIpP".Contains(name[nextIndex], StringComparison.Ordinal)) { - if (int.TryParse(endingNumberGroup.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out num)) + if (int.TryParse(endingNumberGroup.ValueSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out num)) { result.EndingEpisodeNumber = num; } @@ -170,12 +170,12 @@ namespace Emby.Naming.TV } else { - if (int.TryParse(match.Groups[1].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var num)) + if (int.TryParse(match.Groups[1].ValueSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out var num)) { result.SeasonNumber = num; } - if (int.TryParse(match.Groups[2].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out num)) + if (int.TryParse(match.Groups[2].ValueSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out num)) { result.EpisodeNumber = num; } diff --git a/Emby.Naming/TV/SeriesResolver.cs b/Emby.Naming/TV/SeriesResolver.cs index 156a03c9ed..307a840964 100644 --- a/Emby.Naming/TV/SeriesResolver.cs +++ b/Emby.Naming/TV/SeriesResolver.cs @@ -14,7 +14,7 @@ namespace Emby.Naming.TV /// Used for removing separators between words, i.e turns "The_show" into "The show" while /// preserving namings like "S.H.O.W". /// - private static readonly Regex _seriesNameRegex = new Regex(@"((?[^\._]{2,})[\._]*)|([\._](?[^\._]{2,}))"); + private static readonly Regex _seriesNameRegex = new Regex(@"((?[^\._]{2,})[\._]*)|([\._](?[^\._]{2,}))", RegexOptions.Compiled); /// /// Resolve information about series from path. diff --git a/Emby.Naming/Video/CleanDateTimeParser.cs b/Emby.Naming/Video/CleanDateTimeParser.cs index 0ee633dcc6..9a6c6e978b 100644 --- a/Emby.Naming/Video/CleanDateTimeParser.cs +++ b/Emby.Naming/Video/CleanDateTimeParser.cs @@ -43,7 +43,7 @@ namespace Emby.Naming.Video && match.Groups.Count == 5 && match.Groups[1].Success && match.Groups[2].Success - && int.TryParse(match.Groups[2].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var year)) + && int.TryParse(match.Groups[2].ValueSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out var year)) { result = new CleanDateTimeResult(match.Groups[1].Value.TrimEnd(), year); return true; diff --git a/Emby.Naming/Video/ExtraRuleResolver.cs b/Emby.Naming/Video/ExtraRuleResolver.cs index 21d0da3642..3219472eff 100644 --- a/Emby.Naming/Video/ExtraRuleResolver.cs +++ b/Emby.Naming/Video/ExtraRuleResolver.cs @@ -56,7 +56,7 @@ namespace Emby.Naming.Video } else if (rule.RuleType == ExtraRuleType.Regex) { - var filename = Path.GetFileName(path); + var filename = Path.GetFileName(path.AsSpan()); var isMatch = Regex.IsMatch(filename, rule.Token, RegexOptions.IgnoreCase | RegexOptions.Compiled); diff --git a/Emby.Naming/Video/FileStackRule.cs b/Emby.Naming/Video/FileStackRule.cs index 76b487f428..be0f79d33a 100644 --- a/Emby.Naming/Video/FileStackRule.cs +++ b/Emby.Naming/Video/FileStackRule.cs @@ -17,7 +17,7 @@ public class FileStackRule /// Whether the file stack rule uses numerical or alphabetical numbering. public FileStackRule(string token, bool isNumerical) { - _tokenRegex = new Regex(token, RegexOptions.IgnoreCase); + _tokenRegex = new Regex(token, RegexOptions.IgnoreCase | RegexOptions.Compiled); IsNumerical = isNumerical; } diff --git a/Emby.Naming/Video/VideoListResolver.cs b/Emby.Naming/Video/VideoListResolver.cs index 8048320400..6209cd46f4 100644 --- a/Emby.Naming/Video/VideoListResolver.cs +++ b/Emby.Naming/Video/VideoListResolver.cs @@ -4,6 +4,7 @@ using System.IO; using System.Linq; using System.Text.RegularExpressions; using Emby.Naming.Common; +using Jellyfin.Extensions; using MediaBrowser.Model.IO; namespace Emby.Naming.Video @@ -13,6 +14,8 @@ namespace Emby.Naming.Video /// public static class VideoListResolver { + private static readonly Regex _resolutionRegex = new Regex("[0-9]{2}[0-9]+[ip]", RegexOptions.IgnoreCase | RegexOptions.Compiled); + /// /// Resolves alternative versions and extras from list of video files. /// @@ -106,6 +109,7 @@ namespace Emby.Naming.Video } // Cannot use Span inside local functions and delegates thus we cannot use LINQ here nor merge with the above [if] + VideoInfo? primary = null; for (var i = 0; i < videos.Count; i++) { var video = videos[i]; @@ -114,29 +118,43 @@ namespace Emby.Naming.Video continue; } - if (!IsEligibleForMultiVersion(folderName, video.Files[0].Path, namingOptions)) + if (!IsEligibleForMultiVersion(folderName, video.Files[0].FileNameWithoutExtension, namingOptions)) { return videos; } + + if (folderName.Equals(video.Files[0].FileNameWithoutExtension, StringComparison.Ordinal)) + { + primary = video; + } } - // The list is created and overwritten in the caller, so we are allowed to do in-place sorting - videos.Sort((x, y) => string.Compare(x.Name, y.Name, StringComparison.Ordinal)); + if (videos.Count > 1) + { + var groups = videos.GroupBy(x => _resolutionRegex.IsMatch(x.Files[0].FileNameWithoutExtension)).ToList(); + videos.Clear(); + foreach (var group in groups) + { + if (group.Key) + { + videos.InsertRange(0, group.OrderByDescending(x => x.Files[0].FileNameWithoutExtension.ToString(), new AlphanumericComparator())); + } + else + { + videos.AddRange(group.OrderBy(x => x.Files[0].FileNameWithoutExtension.ToString(), new AlphanumericComparator())); + } + } + } + + primary ??= videos[0]; + videos.Remove(primary); var list = new List { - videos[0] + primary }; - var alternateVersionsLen = videos.Count - 1; - var alternateVersions = new VideoFileInfo[alternateVersionsLen]; - for (int i = 0; i < alternateVersionsLen; i++) - { - var video = videos[i + 1]; - alternateVersions[i] = video.Files[0]; - } - - list[0].AlternateVersions = alternateVersions; + list[0].AlternateVersions = videos.Select(x => x.Files[0]).ToArray(); list[0].Name = folderName.ToString(); return list; @@ -161,9 +179,8 @@ namespace Emby.Naming.Video return true; } - private static bool IsEligibleForMultiVersion(ReadOnlySpan folderName, string testFilePath, NamingOptions namingOptions) + private static bool IsEligibleForMultiVersion(ReadOnlySpan folderName, ReadOnlySpan testFilename, NamingOptions namingOptions) { - var testFilename = Path.GetFileNameWithoutExtension(testFilePath.AsSpan()); if (!testFilename.StartsWith(folderName, StringComparison.OrdinalIgnoreCase)) { return false; @@ -176,16 +193,15 @@ namespace Emby.Naming.Video } // There are no span overloads for regex unfortunately - var tmpTestFilename = testFilename.ToString(); - if (CleanStringParser.TryClean(tmpTestFilename, namingOptions.CleanStringRegexes, out var cleanName)) + if (CleanStringParser.TryClean(testFilename.ToString(), namingOptions.CleanStringRegexes, out var cleanName)) { - tmpTestFilename = cleanName.Trim(); + testFilename = cleanName.AsSpan().Trim(); } // The CleanStringParser should have removed common keywords etc. - return string.IsNullOrEmpty(tmpTestFilename) + return testFilename.IsEmpty || testFilename[0] == '-' - || Regex.IsMatch(tmpTestFilename, @"^\[([^]]*)\]", RegexOptions.Compiled); + || Regex.IsMatch(testFilename, @"^\[([^]]*)\]", RegexOptions.Compiled); } } } diff --git a/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs b/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs index 985a127d50..a4deeddb78 100644 --- a/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs +++ b/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs @@ -1,5 +1,3 @@ -#nullable disable - using System; using System.Collections.Concurrent; using System.Collections.Generic; @@ -33,15 +31,10 @@ namespace Emby.Server.Implementations.AppBase private ConfigurationStore[] _configurationStores = Array.Empty(); private IConfigurationFactory[] _configurationFactories = Array.Empty(); - /// - /// The _configuration loaded. - /// - private bool _configurationLoaded; - /// /// The _configuration. /// - private BaseApplicationConfiguration _configuration; + private BaseApplicationConfiguration? _configuration; /// /// Initializes a new instance of the class. @@ -63,17 +56,17 @@ namespace Emby.Server.Implementations.AppBase /// /// Occurs when [configuration updated]. /// - public event EventHandler ConfigurationUpdated; + public event EventHandler? ConfigurationUpdated; /// /// Occurs when [configuration updating]. /// - public event EventHandler NamedConfigurationUpdating; + public event EventHandler? NamedConfigurationUpdating; /// /// Occurs when [named configuration updated]. /// - public event EventHandler NamedConfigurationUpdated; + public event EventHandler? NamedConfigurationUpdated; /// /// Gets the type of the configuration. @@ -107,31 +100,25 @@ namespace Emby.Server.Implementations.AppBase { get { - if (_configurationLoaded) + if (_configuration is not null) { return _configuration; } lock (_configurationSyncLock) { - if (_configurationLoaded) + if (_configuration is not null) { return _configuration; } - _configuration = (BaseApplicationConfiguration)ConfigurationHelper.GetXmlConfiguration(ConfigurationType, CommonApplicationPaths.SystemConfigurationFilePath, XmlSerializer); - - _configurationLoaded = true; - - return _configuration; + return _configuration = (BaseApplicationConfiguration)ConfigurationHelper.GetXmlConfiguration(ConfigurationType, CommonApplicationPaths.SystemConfigurationFilePath, XmlSerializer); } } protected set { _configuration = value; - - _configurationLoaded = value is not null; } } @@ -183,7 +170,7 @@ namespace Emby.Server.Implementations.AppBase Logger.LogInformation("Saving system configuration"); var path = CommonApplicationPaths.SystemConfigurationFilePath; - Directory.CreateDirectory(Path.GetDirectoryName(path)); + Directory.CreateDirectory(Path.GetDirectoryName(path) ?? throw new InvalidOperationException("Path can't be a root directory.")); lock (_configurationSyncLock) { @@ -323,25 +310,20 @@ namespace Emby.Server.Implementations.AppBase private object LoadConfiguration(string path, Type configurationType) { - if (!File.Exists(path)) - { - return Activator.CreateInstance(configurationType); - } - try { - return XmlSerializer.DeserializeFromFile(configurationType, path); + if (File.Exists(path)) + { + return XmlSerializer.DeserializeFromFile(configurationType, path); + } } - catch (IOException) - { - return Activator.CreateInstance(configurationType); - } - catch (Exception ex) + catch (Exception ex) when (ex is not IOException) { Logger.LogError(ex, "Error loading configuration file: {Path}", path); - - return Activator.CreateInstance(configurationType); } + + return Activator.CreateInstance(configurationType) + ?? throw new InvalidOperationException("Configuration type can't be Nullable."); } /// @@ -367,7 +349,7 @@ namespace Emby.Server.Implementations.AppBase _configurations.AddOrUpdate(key, configuration, (_, _) => configuration); var path = GetConfigurationFile(key); - Directory.CreateDirectory(Path.GetDirectoryName(path)); + Directory.CreateDirectory(Path.GetDirectoryName(path) ?? throw new InvalidOperationException("Path can't be a root directory.")); lock (_configurationSyncLock) { diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 76b96e474f..df100b44cc 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -1187,10 +1187,13 @@ namespace Emby.Server.Implementations } } - // used for closing websockets - foreach (var session in _sessionManager.Sessions) + if (_sessionManager != null) { - await session.DisposeAsync().ConfigureAwait(false); + // used for closing websockets + foreach (var session in _sessionManager.Sessions) + { + await session.DisposeAsync().ConfigureAwait(false); + } } } } diff --git a/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs b/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs index ff5602f243..6b8b1a620f 100644 --- a/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs +++ b/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs @@ -1,5 +1,3 @@ -#nullable disable - using System; using System.Globalization; using System.IO; @@ -36,7 +34,7 @@ namespace Emby.Server.Implementations.Configuration /// /// Configuration updating event. /// - public event EventHandler> ConfigurationUpdating; + public event EventHandler>? ConfigurationUpdating; /// /// Gets the type of the configuration. diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index bc703fe90d..90f03995e8 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -1195,7 +1195,7 @@ namespace Emby.Server.Implementations.Data Path = RestorePath(path.ToString()) }; - if (long.TryParse(dateModified, NumberStyles.Any, CultureInfo.InvariantCulture, out var ticks) + if (long.TryParse(dateModified, CultureInfo.InvariantCulture, out var ticks) && ticks >= DateTime.MinValue.Ticks && ticks <= DateTime.MaxValue.Ticks) { @@ -4477,6 +4477,24 @@ namespace Emby.Server.Implementations.Data } } + if (query.IncludeInheritedTags.Length > 0) + { + var paramName = "@IncludeInheritedTags"; + if (statement is null) + { + int index = 0; + string includedTags = string.Join(',', query.IncludeInheritedTags.Select(_ => paramName + index++)); + whereClauses.Add("((select CleanValue from ItemValues where ItemId=Guid and Type=6 and cleanvalue in (" + includedTags + ")) is not null)"); + } + else + { + for (int index = 0; index < query.IncludeInheritedTags.Length; index++) + { + statement.TryBind(paramName + index, GetCleanValue(query.IncludeInheritedTags[index])); + } + } + } + if (query.SeriesStatuses.Length > 0) { var statuses = new List(); @@ -5440,6 +5458,9 @@ AND Type = @InternalPersonType)"); list.AddRange(inheritedTags.Select(i => (6, i))); + // Remove all invalid values. + list.RemoveAll(i => string.IsNullOrEmpty(i.Item2)); + return list; } diff --git a/Emby.Server.Implementations/Images/BaseFolderImageProvider.cs b/Emby.Server.Implementations/Images/BaseFolderImageProvider.cs index 6fc7f1ac3a..84c21931c3 100644 --- a/Emby.Server.Implementations/Images/BaseFolderImageProvider.cs +++ b/Emby.Server.Implementations/Images/BaseFolderImageProvider.cs @@ -1,5 +1,3 @@ -#nullable disable - #pragma warning disable CS1591 using System.Collections.Generic; diff --git a/Emby.Server.Implementations/Images/FolderImageProvider.cs b/Emby.Server.Implementations/Images/FolderImageProvider.cs index 4376bd356c..90f7568a90 100644 --- a/Emby.Server.Implementations/Images/FolderImageProvider.cs +++ b/Emby.Server.Implementations/Images/FolderImageProvider.cs @@ -1,5 +1,3 @@ -#nullable disable - #pragma warning disable CS1591 using MediaBrowser.Common.Configuration; diff --git a/Emby.Server.Implementations/Images/GenreImageProvider.cs b/Emby.Server.Implementations/Images/GenreImageProvider.cs index 968bf5fa33..c9b41f8193 100644 --- a/Emby.Server.Implementations/Images/GenreImageProvider.cs +++ b/Emby.Server.Implementations/Images/GenreImageProvider.cs @@ -1,5 +1,3 @@ -#nullable disable - #pragma warning disable CS1591 using System.Collections.Generic; diff --git a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs index 1522cd3aef..ef4fa1fd2d 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs @@ -313,13 +313,8 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies return result; } - private static bool IsIgnored(string filename) - { - // Ignore samples - Match m = Regex.Match(filename, @"\bsample\b", RegexOptions.IgnoreCase | RegexOptions.Compiled); - - return m.Success; - } + private static bool IsIgnored(ReadOnlySpan filename) + => Regex.IsMatch(filename, @"\bsample\b", RegexOptions.IgnoreCase | RegexOptions.Compiled); private static bool ContainsFile(IReadOnlyList result, FileSystemMetadata file) { diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs index 3f7914d3bb..b5e742f98f 100644 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs +++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs @@ -570,15 +570,13 @@ namespace Emby.Server.Implementations.LiveTv.Listings _tokens.TryAdd(username, savedToken); } - if (!string.IsNullOrEmpty(savedToken.Name) && !string.IsNullOrEmpty(savedToken.Value)) + if (!string.IsNullOrEmpty(savedToken.Name) + && long.TryParse(savedToken.Value, CultureInfo.InvariantCulture, out long ticks)) { - if (long.TryParse(savedToken.Value, NumberStyles.Any, CultureInfo.InvariantCulture, out long ticks)) + // If it's under 24 hours old we can still use it + if (DateTime.UtcNow.Ticks - ticks < TimeSpan.FromHours(20).Ticks) { - // If it's under 24 hours old we can still use it - if (DateTime.UtcNow.Ticks - ticks < TimeSpan.FromHours(20).Ticks) - { - return savedToken.Name; - } + return savedToken.Name; } } diff --git a/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs b/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs index e874990da1..066afb956b 100644 --- a/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs +++ b/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs @@ -137,32 +137,33 @@ namespace Emby.Server.Implementations.LiveTv.Listings private static ProgramInfo GetProgramInfo(XmlTvProgram program, ListingsProviderInfo info) { - string episodeTitle = program.Episode?.Title; + string episodeTitle = program.Episode.Title; + var programCategories = program.Categories.Where(c => !string.IsNullOrWhiteSpace(c)).ToList(); var programInfo = new ProgramInfo { ChannelId = program.ChannelId, EndDate = program.EndDate.UtcDateTime, - EpisodeNumber = program.Episode?.Episode, + EpisodeNumber = program.Episode.Episode, EpisodeTitle = episodeTitle, - Genres = program.Categories, + Genres = programCategories, StartDate = program.StartDate.UtcDateTime, Name = program.Title, Overview = program.Description, ProductionYear = program.CopyrightDate?.Year, - SeasonNumber = program.Episode?.Series, - IsSeries = program.Episode is not null, + SeasonNumber = program.Episode.Series, + IsSeries = program.Episode.Series is not null, IsRepeat = program.IsPreviouslyShown && !program.IsNew, IsPremiere = program.Premiere is not null, - 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)), + IsKids = programCategories.Any(c => info.KidsCategories.Contains(c, StringComparison.OrdinalIgnoreCase)), + IsMovie = programCategories.Any(c => info.MovieCategories.Contains(c, StringComparison.OrdinalIgnoreCase)), + IsNews = programCategories.Any(c => info.NewsCategories.Contains(c, StringComparison.OrdinalIgnoreCase)), + IsSports = programCategories.Any(c => info.SportsCategories.Contains(c, StringComparison.OrdinalIgnoreCase)), ImageUrl = string.IsNullOrEmpty(program.Icon?.Source) ? null : program.Icon.Source, HasImage = !string.IsNullOrEmpty(program.Icon?.Source), OfficialRating = string.IsNullOrEmpty(program.Rating?.Value) ? null : program.Rating.Value, CommunityRating = program.StarRating, - SeriesId = program.Episode is null ? null : program.Title?.GetMD5().ToString("N", CultureInfo.InvariantCulture) + SeriesId = program.Episode.Episode is null ? null : program.Title?.GetMD5().ToString("N", CultureInfo.InvariantCulture) }; if (string.IsNullOrWhiteSpace(program.ProgramId)) @@ -243,7 +244,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings { Id = c.Id, Name = c.DisplayName, - ImageUrl = string.IsNullOrEmpty(c.Icon.Source) ? null : c.Icon.Source, + ImageUrl = string.IsNullOrEmpty(c.Icon?.Source) ? null : c.Icon.Source, Number = string.IsNullOrWhiteSpace(c.Number) ? c.Id : c.Number }).ToList(); } diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/LegacyHdHomerunChannelCommands.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/LegacyHdHomerunChannelCommands.cs index 80d9d07247..3450f971fc 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/LegacyHdHomerunChannelCommands.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/LegacyHdHomerunChannelCommands.cs @@ -13,8 +13,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun public LegacyHdHomerunChannelCommands(string url) { // parse url for channel and program - var regExp = new Regex(@"\/ch([0-9]+)-?([0-9]*)"); - var match = regExp.Match(url); + var match = Regex.Match(url, @"\/ch([0-9]+)-?([0-9]*)"); if (match.Success) { _channel = match.Groups[1].Value; diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs index a423ec8f48..046be7c5c7 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs @@ -168,28 +168,24 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts string numberString = null; string attributeValue; - if (attributes.TryGetValue("tvg-chno", out attributeValue)) + if (attributes.TryGetValue("tvg-chno", out attributeValue) + && double.TryParse(attributeValue, CultureInfo.InvariantCulture, out _)) { - if (double.TryParse(attributeValue, NumberStyles.Any, CultureInfo.InvariantCulture, out _)) - { - numberString = attributeValue; - } + numberString = attributeValue; } if (!IsValidChannelNumber(numberString)) { if (attributes.TryGetValue("tvg-id", out attributeValue)) { - if (double.TryParse(attributeValue, NumberStyles.Any, CultureInfo.InvariantCulture, out _)) + if (double.TryParse(attributeValue, CultureInfo.InvariantCulture, out _)) { numberString = attributeValue; } - else if (attributes.TryGetValue("channel-id", out attributeValue)) + else if (attributes.TryGetValue("channel-id", out attributeValue) + && double.TryParse(attributeValue, CultureInfo.InvariantCulture, out _)) { - if (double.TryParse(attributeValue, NumberStyles.Any, CultureInfo.InvariantCulture, out _)) - { - numberString = attributeValue; - } + numberString = attributeValue; } } @@ -207,7 +203,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts { var numberPart = nameInExtInf.Slice(0, numberIndex).Trim(new[] { ' ', '.' }); - if (double.TryParse(numberPart, NumberStyles.Any, CultureInfo.InvariantCulture, out _)) + if (double.TryParse(numberPart, CultureInfo.InvariantCulture, out _)) { numberString = numberPart.ToString(); } @@ -255,19 +251,14 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts private static bool IsValidChannelNumber(string numberString) { - if (string.IsNullOrWhiteSpace(numberString) || - string.Equals(numberString, "-1", StringComparison.OrdinalIgnoreCase) || - string.Equals(numberString, "0", StringComparison.OrdinalIgnoreCase)) + if (string.IsNullOrWhiteSpace(numberString) + || string.Equals(numberString, "-1", StringComparison.Ordinal) + || string.Equals(numberString, "0", StringComparison.Ordinal)) { return false; } - if (!double.TryParse(numberString, NumberStyles.Any, CultureInfo.InvariantCulture, out _)) - { - return false; - } - - return true; + return double.TryParse(numberString, CultureInfo.InvariantCulture, out _); } private static string GetChannelName(string extInf, Dictionary attributes) @@ -285,7 +276,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts { var numberPart = nameInExtInf.Substring(0, numberIndex).Trim(new[] { ' ', '.' }); - if (double.TryParse(numberPart, NumberStyles.Any, CultureInfo.InvariantCulture, out _)) + if (double.TryParse(numberPart, CultureInfo.InvariantCulture, out _)) { // channel.Number = number.ToString(); nameInExtInf = nameInExtInf.Substring(numberIndex + 1).Trim(new[] { ' ', '-' }); @@ -317,8 +308,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts { var dict = new Dictionary(StringComparer.OrdinalIgnoreCase); - var reg = new Regex(@"([a-z0-9\-_]+)=\""([^""]+)\""", RegexOptions.IgnoreCase); - var matches = reg.Matches(line); + var matches = Regex.Matches(line, @"([a-z0-9\-_]+)=\""([^""]+)\""", RegexOptions.IgnoreCase); remaining = line; diff --git a/Emby.Server.Implementations/Localization/Core/es-AR.json b/Emby.Server.Implementations/Localization/Core/es-AR.json index 8ad9e8c716..8bd3c5defe 100644 --- a/Emby.Server.Implementations/Localization/Core/es-AR.json +++ b/Emby.Server.Implementations/Localization/Core/es-AR.json @@ -118,11 +118,11 @@ "TaskCleanActivityLog": "Borrar log de actividades", "Undefined": "Indefinido", "Forced": "Forzado", - "Default": "Por Defecto", + "Default": "Predeterminado", "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", "External": "Externo", "TaskKeyframeExtractorDescription": "Extrae Fotogramas Clave de los archivos de vídeo para crear Listas de Reprodución HLS más precisas. Esta tarea puede durar mucho tiempo.", "TaskKeyframeExtractor": "Extractor de Fotogramas Clave", - "HearingImpaired": "Personas con discapacidad auditiva" + "HearingImpaired": "Discapacidad Auditiva" } diff --git a/Emby.Server.Implementations/Localization/Core/es.json b/Emby.Server.Implementations/Localization/Core/es.json index afffdf3bfa..5e41462db1 100644 --- a/Emby.Server.Implementations/Localization/Core/es.json +++ b/Emby.Server.Implementations/Localization/Core/es.json @@ -31,7 +31,7 @@ "ItemRemovedWithName": "{0} ha sido eliminado de la biblioteca", "LabelIpAddressValue": "Dirección IP: {0}", "LabelRunningTimeValue": "Tiempo de funcionamiento: {0}", - "Latest": "Últimos", + "Latest": "Último contenido en", "MessageApplicationUpdated": "Se ha actualizado el servidor Jellyfin", "MessageApplicationUpdatedTo": "Se ha actualizado el servidor Jellyfin a la versión {0}", "MessageNamedServerConfigurationUpdatedWithValue": "La sección {0} de configuración del servidor ha sido actualizada", diff --git a/Emby.Server.Implementations/Localization/Core/id.json b/Emby.Server.Implementations/Localization/Core/id.json index 695c0f4048..87ce07da31 100644 --- a/Emby.Server.Implementations/Localization/Core/id.json +++ b/Emby.Server.Implementations/Localization/Core/id.json @@ -82,7 +82,7 @@ "MessageServerConfigurationUpdated": "Konfigurasi server telah diperbarui", "MessageNamedServerConfigurationUpdatedWithValue": "Bagian konfigurasi server {0} telah diperbarui", "FailedLoginAttemptWithUserName": "Gagal melakukan login dari {0}", - "CameraImageUploadedFrom": "Gambar kamera baru telah diunggah dari {0}", + "CameraImageUploadedFrom": "Sebuah gambar kamera baru telah diunggah dari {0}", "DeviceOfflineWithName": "{0} telah terputus", "DeviceOnlineWithName": "{0} telah terhubung", "NotificationOptionVideoPlaybackStopped": "Pemutaran video berhenti", diff --git a/Emby.Server.Implementations/Localization/Core/nl.json b/Emby.Server.Implementations/Localization/Core/nl.json index e03747cbec..081ba0cc7e 100644 --- a/Emby.Server.Implementations/Localization/Core/nl.json +++ b/Emby.Server.Implementations/Localization/Core/nl.json @@ -58,8 +58,8 @@ "NotificationOptionServerRestartRequired": "Server herstart nodig", "NotificationOptionTaskFailed": "Geplande taak mislukt", "NotificationOptionUserLockedOut": "Gebruiker is vergrendeld", - "NotificationOptionVideoPlayback": "Video gestart", - "NotificationOptionVideoPlaybackStopped": "Video gestopt", + "NotificationOptionVideoPlayback": "Afspelen van video gestart", + "NotificationOptionVideoPlaybackStopped": "Afspelen van video gestopt", "Photos": "Foto's", "Playlists": "Afspeellijsten", "Plugin": "Plug-in", @@ -95,26 +95,26 @@ "TaskDownloadMissingSubtitlesDescription": "Zoekt op het internet naar ontbrekende ondertiteling gebaseerd op metadataconfiguratie.", "TaskDownloadMissingSubtitles": "Ontbrekende ondertiteling downloaden", "TaskRefreshChannelsDescription": "Vernieuwt informatie van internet kanalen.", - "TaskRefreshChannels": "Vernieuw Kanalen", + "TaskRefreshChannels": "Vernieuw kanalen", "TaskCleanTranscodeDescription": "Verwijdert transcode bestanden ouder dan 1 dag.", "TaskCleanLogs": "Logboekmap opschonen", "TaskCleanTranscode": "Transcoderingsmap opschonen", "TaskUpdatePluginsDescription": "Downloadt en installeert updates van plug-ins waarvoor automatisch bijwerken is ingeschakeld.", "TaskUpdatePlugins": "Plug-ins bijwerken", - "TaskRefreshPeopleDescription": "Update metadata for acteurs en regisseurs in de media bibliotheek.", + "TaskRefreshPeopleDescription": "Update metadata voor acteurs en regisseurs in de media bibliotheek.", "TaskRefreshPeople": "Personen vernieuwen", "TaskCleanLogsDescription": "Verwijdert log bestanden ouder dan {0} dagen.", "TaskRefreshLibraryDescription": "Scant de mediabibliotheek op nieuwe bestanden en vernieuwt de metadata.", "TaskRefreshLibrary": "Mediabibliotheek scannen", - "TaskRefreshChapterImagesDescription": "Maakt thumbnails aan voor videos met hoofdstukken.", - "TaskRefreshChapterImages": "Hoofdstukafbeeldingen uitpakken", + "TaskRefreshChapterImagesDescription": "Maakt voorbeeldafbeedingen aan voor video's met hoofdstukken.", + "TaskRefreshChapterImages": "Hoofdstukafbeeldingen extraheren", "TaskCleanCacheDescription": "Verwijdert gecachte bestanden die het systeem niet langer nodig heeft.", "TaskCleanCache": "Cache-map opschonen", - "TasksChannelsCategory": "Internet Kanalen", + "TasksChannelsCategory": "Internetkanalen", "TasksApplicationCategory": "Toepassing", "TasksLibraryCategory": "Bibliotheek", "TasksMaintenanceCategory": "Onderhoud", - "TaskCleanActivityLogDescription": "Verwijdert activiteiten logs ouder dan de ingestelde tijd.", + "TaskCleanActivityLogDescription": "Verwijdert activiteiten logs ouder dan de ingestelde leeftijd.", "TaskCleanActivityLog": "Activiteitenlogboek legen", "Undefined": "Niet gedefinieerd", "Forced": "Geforceerd", diff --git a/Emby.Server.Implementations/Localization/Core/ru.json b/Emby.Server.Implementations/Localization/Core/ru.json index 65cf29e807..839bbcb6d3 100644 --- a/Emby.Server.Implementations/Localization/Core/ru.json +++ b/Emby.Server.Implementations/Localization/Core/ru.json @@ -16,14 +16,14 @@ "Folders": "Папки", "Genres": "Жанры", "HeaderAlbumArtists": "Исполнители альбома", - "HeaderContinueWatching": "Продолжение просмотра", + "HeaderContinueWatching": "Продолжить просмотр", "HeaderFavoriteAlbums": "Избранные альбомы", "HeaderFavoriteArtists": "Избранные исполнители", "HeaderFavoriteEpisodes": "Избранные эпизоды", "HeaderFavoriteShows": "Избранные сериалы", "HeaderFavoriteSongs": "Избранные композиции", "HeaderLiveTV": "Эфир", - "HeaderNextUp": "Очередное", + "HeaderNextUp": "Следующий", "HeaderRecordingGroups": "Группы записей", "HomeVideos": "Домашние видео", "Inherit": "Наследуемое", @@ -70,7 +70,7 @@ "ScheduledTaskFailedWithName": "{0} - неудачна", "ScheduledTaskStartedWithName": "{0} - запущена", "ServerNameNeedsToBeRestarted": "Необходим перезапуск {0}", - "Shows": "Передачи", + "Shows": "Телешоу", "Songs": "Композиции", "StartupEmbyServerIsLoading": "Jellyfin Server загружается. Повторите попытку в ближайшее время.", "SubtitleDownloadFailureForItem": "Субтитры к {0} не удалось загрузить", diff --git a/Emby.Server.Implementations/Localization/Core/sl-SI.json b/Emby.Server.Implementations/Localization/Core/sl-SI.json index d845accac2..4c23f71efa 100644 --- a/Emby.Server.Implementations/Localization/Core/sl-SI.json +++ b/Emby.Server.Implementations/Localization/Core/sl-SI.json @@ -123,5 +123,6 @@ "TaskOptimizeDatabase": "Optimiziraj bazo podatkov", "TaskKeyframeExtractor": "Ekstraktor ključnih sličic", "External": "Zunanji", - "TaskKeyframeExtractorDescription": "Iz video datoteke Izvleče ključne sličice, da ustvari bolj natančne sezname predvajanja HLS. Proces lahko traja dolgo časa." + "TaskKeyframeExtractorDescription": "Iz video datoteke Izvleče ključne sličice, da ustvari bolj natančne sezname predvajanja HLS. Proces lahko traja dolgo časa.", + "HearingImpaired": "Oslabljen sluh" } diff --git a/Emby.Server.Implementations/Localization/Core/uk.json b/Emby.Server.Implementations/Localization/Core/uk.json index 92ce616f2e..ff77fb8c56 100644 --- a/Emby.Server.Implementations/Localization/Core/uk.json +++ b/Emby.Server.Implementations/Localization/Core/uk.json @@ -86,7 +86,7 @@ "Shows": "Шоу", "ServerNameNeedsToBeRestarted": "{0} потрібно перезапустити", "ScheduledTaskStartedWithName": "{0} розпочато", - "ScheduledTaskFailedWithName": "Помилка {0}", + "ScheduledTaskFailedWithName": "{0} незавершено, збій", "ProviderValue": "Постачальник: {0}", "PluginUpdatedWithName": "{0} оновлено", "PluginUninstalledWithName": "{0} видалено", diff --git a/Emby.Server.Implementations/Plugins/PluginManager.cs b/Emby.Server.Implementations/Plugins/PluginManager.cs index f2212f4dcb..7c23254a12 100644 --- a/Emby.Server.Implementations/Plugins/PluginManager.cs +++ b/Emby.Server.Implementations/Plugins/PluginManager.cs @@ -123,41 +123,64 @@ namespace Emby.Server.Implementations.Plugins continue; } + var assemblyLoadContext = new PluginLoadContext(plugin.Path); + _assemblyLoadContexts.Add(assemblyLoadContext); + + var assemblies = new List(plugin.DllFiles.Count); + var loadedAll = true; + foreach (var file in plugin.DllFiles) { - Assembly assembly; try { - var assemblyLoadContext = new PluginLoadContext(file); - _assemblyLoadContexts.Add(assemblyLoadContext); - - assembly = assemblyLoadContext.LoadFromAssemblyPath(file); - - // Load all required types to verify that the plugin will load - assembly.GetTypes(); + assemblies.Add(assemblyLoadContext.LoadFromAssemblyPath(file)); } catch (FileLoadException ex) { - _logger.LogError(ex, "Failed to load assembly {Path}. Disabling plugin.", file); + _logger.LogError(ex, "Failed to load assembly {Path}. Disabling plugin", file); ChangePluginState(plugin, PluginStatus.Malfunctioned); - continue; - } - catch (SystemException ex) when (ex is TypeLoadException or ReflectionTypeLoadException) // Undocumented exception - { - _logger.LogError(ex, "Failed to load assembly {Path}. This error occurs when a plugin references an incompatible version of one of the shared libraries. Disabling plugin.", file); - ChangePluginState(plugin, PluginStatus.NotSupported); - continue; + loadedAll = false; + break; } #pragma warning disable CA1031 // Do not catch general exception types catch (Exception ex) #pragma warning restore CA1031 // Do not catch general exception types { - _logger.LogError(ex, "Failed to load assembly {Path}. Unknown exception was thrown. Disabling plugin.", file); + _logger.LogError(ex, "Failed to load assembly {Path}. Unknown exception was thrown. Disabling plugin", file); ChangePluginState(plugin, PluginStatus.Malfunctioned); - continue; + loadedAll = false; + break; + } + } + + if (!loadedAll) + { + continue; + } + + foreach (var assembly in assemblies) + { + try + { + // Load all required types to verify that the plugin will load + assembly.GetTypes(); + } + catch (SystemException ex) when (ex is TypeLoadException or ReflectionTypeLoadException) // Undocumented exception + { + _logger.LogError(ex, "Failed to load assembly {Path}. This error occurs when a plugin references an incompatible version of one of the shared libraries. Disabling plugin", assembly.Location); + ChangePluginState(plugin, PluginStatus.NotSupported); + break; + } +#pragma warning disable CA1031 // Do not catch general exception types + catch (Exception ex) +#pragma warning restore CA1031 // Do not catch general exception types + { + _logger.LogError(ex, "Failed to load assembly {Path}. Unknown exception was thrown. Disabling plugin", assembly.Location); + ChangePluginState(plugin, PluginStatus.Malfunctioned); + break; } - _logger.LogInformation("Loaded assembly {Assembly} from {Path}", assembly.FullName, file); + _logger.LogInformation("Loaded assembly {Assembly} from {Path}", assembly.FullName, assembly.Location); yield return assembly; } } diff --git a/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs b/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs index 63f0beb105..6dc20e66ba 100644 --- a/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs +++ b/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs @@ -1,5 +1,3 @@ -#nullable disable - #pragma warning disable CS1591 using System; @@ -43,9 +41,9 @@ namespace Emby.Server.Implementations.ScheduledTasks ScheduledTasks = Array.Empty(); } - public event EventHandler> TaskExecuting; + public event EventHandler>? TaskExecuting; - public event EventHandler TaskCompleted; + public event EventHandler? TaskCompleted; /// /// Gets the list of Scheduled Tasks. diff --git a/Emby.Server.Implementations/Session/SessionWebSocketListener.cs b/Emby.Server.Implementations/Session/SessionWebSocketListener.cs index aebb559073..4e427b1a4b 100644 --- a/Emby.Server.Implementations/Session/SessionWebSocketListener.cs +++ b/Emby.Server.Implementations/Session/SessionWebSocketListener.cs @@ -1,5 +1,3 @@ -#nullable disable - using System; using System.Collections.Generic; using System.Linq; @@ -58,7 +56,7 @@ namespace Emby.Server.Implementations.Session /// /// The KeepAlive cancellation token. /// - private CancellationTokenSource _keepAliveCancellationToken; + private CancellationTokenSource? _keepAliveCancellationToken; /// /// Initializes a new instance of the class. @@ -105,7 +103,7 @@ namespace Emby.Server.Implementations.Session } } - private async Task GetSession(HttpContext httpContext, string remoteEndpoint) + private async Task GetSession(HttpContext httpContext, string? remoteEndpoint) { if (!httpContext.User.Identity?.IsAuthenticated ?? false) { @@ -138,8 +136,13 @@ namespace Emby.Server.Implementations.Session /// /// The WebSocket. /// The event arguments. - private void OnWebSocketClosed(object sender, EventArgs e) + private void OnWebSocketClosed(object? sender, EventArgs e) { + if (sender is null) + { + return; + } + var webSocket = (IWebSocketConnection)sender; _logger.LogDebug("WebSocket {0} is closed.", webSocket); RemoveWebSocket(webSocket); diff --git a/Emby.Server.Implementations/Sorting/RuntimeComparer.cs b/Emby.Server.Implementations/Sorting/RuntimeComparer.cs index 646bafbb54..753e58324c 100644 --- a/Emby.Server.Implementations/Sorting/RuntimeComparer.cs +++ b/Emby.Server.Implementations/Sorting/RuntimeComparer.cs @@ -1,5 +1,3 @@ -#nullable disable - using System; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Sorting; @@ -24,10 +22,9 @@ namespace Emby.Server.Implementations.Sorting /// The x. /// The y. /// System.Int32. - public int Compare(BaseItem x, BaseItem y) + public int Compare(BaseItem? x, BaseItem? y) { ArgumentNullException.ThrowIfNull(x); - ArgumentNullException.ThrowIfNull(y); return (x.RunTimeTicks ?? 0).CompareTo(y.RunTimeTicks ?? 0); diff --git a/Emby.Server.Implementations/Sorting/SeriesSortNameComparer.cs b/Emby.Server.Implementations/Sorting/SeriesSortNameComparer.cs index 0bd9600b98..5b6c64f63a 100644 --- a/Emby.Server.Implementations/Sorting/SeriesSortNameComparer.cs +++ b/Emby.Server.Implementations/Sorting/SeriesSortNameComparer.cs @@ -1,5 +1,3 @@ -#nullable disable - #pragma warning disable CS1591 using System; @@ -23,15 +21,14 @@ namespace Emby.Server.Implementations.Sorting /// The x. /// The y. /// System.Int32. - public int Compare(BaseItem x, BaseItem y) + public int Compare(BaseItem? x, BaseItem? y) { return string.Compare(GetValue(x), GetValue(y), StringComparison.OrdinalIgnoreCase); } - private static string GetValue(BaseItem item) + private static string? GetValue(BaseItem? item) { var hasSeries = item as IHasSeries; - return hasSeries?.FindSeriesSortName(); } } diff --git a/Emby.Server.Implementations/Sorting/SortNameComparer.cs b/Emby.Server.Implementations/Sorting/SortNameComparer.cs index 628b9b3dda..19abafe192 100644 --- a/Emby.Server.Implementations/Sorting/SortNameComparer.cs +++ b/Emby.Server.Implementations/Sorting/SortNameComparer.cs @@ -1,5 +1,3 @@ -#nullable disable - using System; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Sorting; @@ -24,10 +22,9 @@ namespace Emby.Server.Implementations.Sorting /// The x. /// The y. /// System.Int32. - public int Compare(BaseItem x, BaseItem y) + public int Compare(BaseItem? x, BaseItem? y) { ArgumentNullException.ThrowIfNull(x); - ArgumentNullException.ThrowIfNull(y); return string.Compare(x.SortName, y.SortName, StringComparison.OrdinalIgnoreCase); diff --git a/Emby.Server.Implementations/Sorting/StartDateComparer.cs b/Emby.Server.Implementations/Sorting/StartDateComparer.cs index c3df7c47e6..2759d20de8 100644 --- a/Emby.Server.Implementations/Sorting/StartDateComparer.cs +++ b/Emby.Server.Implementations/Sorting/StartDateComparer.cs @@ -1,5 +1,3 @@ -#nullable disable - #pragma warning disable CS1591 using System; @@ -24,7 +22,7 @@ namespace Emby.Server.Implementations.Sorting /// The x. /// The y. /// System.Int32. - public int Compare(BaseItem x, BaseItem y) + public int Compare(BaseItem? x, BaseItem? y) { return GetDate(x).CompareTo(GetDate(y)); } @@ -34,7 +32,7 @@ namespace Emby.Server.Implementations.Sorting /// /// The x. /// DateTime. - private static DateTime GetDate(BaseItem x) + private static DateTime GetDate(BaseItem? x) { if (x is LiveTvProgram hasStartDate) { diff --git a/Emby.Server.Implementations/Sorting/StudioComparer.cs b/Emby.Server.Implementations/Sorting/StudioComparer.cs index 457c062714..89d10f3d23 100644 --- a/Emby.Server.Implementations/Sorting/StudioComparer.cs +++ b/Emby.Server.Implementations/Sorting/StudioComparer.cs @@ -1,5 +1,3 @@ -#nullable disable - #pragma warning disable CS1591 using System; @@ -24,10 +22,9 @@ namespace Emby.Server.Implementations.Sorting /// The x. /// The y. /// System.Int32. - public int Compare(BaseItem x, BaseItem y) + public int Compare(BaseItem? x, BaseItem? y) { ArgumentNullException.ThrowIfNull(x); - ArgumentNullException.ThrowIfNull(y); return AlphanumericComparator.CompareValues(x.Studios.FirstOrDefault(), y.Studios.FirstOrDefault()); diff --git a/Emby.Server.Implementations/TV/TVSeriesManager.cs b/Emby.Server.Implementations/TV/TVSeriesManager.cs index 967f90b55f..f0e173f0b1 100644 --- a/Emby.Server.Implementations/TV/TVSeriesManager.cs +++ b/Emby.Server.Implementations/TV/TVSeriesManager.cs @@ -1,5 +1,3 @@ -#nullable disable - #pragma warning disable CS1591 using System; @@ -42,7 +40,7 @@ namespace Emby.Server.Implementations.TV throw new ArgumentException("User not found"); } - string presentationUniqueKey = null; + string? presentationUniqueKey = null; if (query.SeriesId.HasValue && !query.SeriesId.Value.Equals(default)) { if (_libraryManager.GetItemById(query.SeriesId.Value) is Series series) @@ -91,7 +89,7 @@ namespace Emby.Server.Implementations.TV throw new ArgumentException("User not found"); } - string presentationUniqueKey = null; + string? presentationUniqueKey = null; int? limit = null; if (request.SeriesId.HasValue && !request.SeriesId.Value.Equals(default)) { @@ -168,7 +166,7 @@ namespace Emby.Server.Implementations.TV return !anyFound && i.LastWatchedDate == DateTime.MinValue; }) .Select(i => i.GetEpisodeFunction()) - .Where(i => i is not null); + .Where(i => i is not null)!; } private static string GetUniqueSeriesKey(Episode episode) @@ -185,7 +183,7 @@ namespace Emby.Server.Implementations.TV /// Gets the next up. /// /// Task{Episode}. - private (DateTime LastWatchedDate, Func GetEpisodeFunction) GetNextUp(string seriesKey, User user, DtoOptions dtoOptions, bool rewatching) + private (DateTime LastWatchedDate, Func GetEpisodeFunction) GetNextUp(string seriesKey, User user, DtoOptions dtoOptions, bool rewatching) { var lastQuery = new InternalItemsQuery(user) { @@ -209,7 +207,7 @@ namespace Emby.Server.Implementations.TV var lastWatchedEpisode = _libraryManager.GetItemList(lastQuery).Cast().FirstOrDefault(); - Episode GetEpisode() + Episode? GetEpisode() { var nextQuery = new InternalItemsQuery(user) { diff --git a/Jellyfin.Api/Auth/AnonymousLanAccessPolicy/AnonymousLanAccessHandler.cs b/Jellyfin.Api/Auth/AnonymousLanAccessPolicy/AnonymousLanAccessHandler.cs index d4b1ffb060..741b88ea95 100644 --- a/Jellyfin.Api/Auth/AnonymousLanAccessPolicy/AnonymousLanAccessHandler.cs +++ b/Jellyfin.Api/Auth/AnonymousLanAccessPolicy/AnonymousLanAccessHandler.cs @@ -1,4 +1,5 @@ using System.Threading.Tasks; +using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; @@ -29,7 +30,7 @@ namespace Jellyfin.Api.Auth.AnonymousLanAccessPolicy /// protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, AnonymousLanAccessRequirement requirement) { - var ip = _httpContextAccessor.HttpContext?.Connection.RemoteIpAddress; + var ip = _httpContextAccessor.HttpContext?.GetNormalizedRemoteIp(); // Loopback will be on LAN, so we can accept null. if (ip is null || _networkManager.IsInLocalNetwork(ip)) diff --git a/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs b/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs deleted file mode 100644 index 8e5e66d64a..0000000000 --- a/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs +++ /dev/null @@ -1,113 +0,0 @@ -using System.Security.Claims; -using Jellyfin.Api.Extensions; -using Jellyfin.Api.Helpers; -using Jellyfin.Data.Enums; -using MediaBrowser.Common.Extensions; -using MediaBrowser.Common.Net; -using MediaBrowser.Controller.Library; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; - -namespace Jellyfin.Api.Auth -{ - /// - /// Base authorization handler. - /// - /// Type of Authorization Requirement. - public abstract class BaseAuthorizationHandler : AuthorizationHandler - where T : IAuthorizationRequirement - { - private readonly IUserManager _userManager; - private readonly INetworkManager _networkManager; - private readonly IHttpContextAccessor _httpContextAccessor; - - /// - /// Initializes a new instance of the class. - /// - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - protected BaseAuthorizationHandler( - IUserManager userManager, - INetworkManager networkManager, - IHttpContextAccessor httpContextAccessor) - { - _userManager = userManager; - _networkManager = networkManager; - _httpContextAccessor = httpContextAccessor; - } - - /// - /// Validate authenticated claims. - /// - /// Request claims. - /// Whether to ignore parental control. - /// Whether access is to be allowed locally only. - /// Whether validation requires download permission. - /// Validated claim status. - protected bool ValidateClaims( - ClaimsPrincipal claimsPrincipal, - bool ignoreSchedule = false, - bool localAccessOnly = false, - bool requiredDownloadPermission = false) - { - // ApiKey is currently global admin, always allow. - var isApiKey = claimsPrincipal.GetIsApiKey(); - if (isApiKey) - { - return true; - } - - // Ensure claim has userId. - var userId = claimsPrincipal.GetUserId(); - if (userId.Equals(default)) - { - return false; - } - - // Ensure userId links to a valid user. - var user = _userManager.GetUserById(userId); - if (user is null) - { - return false; - } - - // Ensure user is not disabled. - if (user.HasPermission(PermissionKind.IsDisabled)) - { - return false; - } - - var isInLocalNetwork = _httpContextAccessor.HttpContext is not null - && _networkManager.IsInLocalNetwork(_httpContextAccessor.HttpContext.GetNormalizedRemoteIp()); - - // User cannot access remotely and user is remote - if (!user.HasPermission(PermissionKind.EnableRemoteAccess) && !isInLocalNetwork) - { - return false; - } - - if (localAccessOnly && !isInLocalNetwork) - { - return false; - } - - // User attempting to access out of parental control hours. - if (!ignoreSchedule - && !user.HasPermission(PermissionKind.IsAdministrator) - && !user.IsParentalScheduleAllowed()) - { - return false; - } - - // User attempting to download without permission. - if (requiredDownloadPermission - && !user.HasPermission(PermissionKind.EnableContentDownloading)) - { - return false; - } - - return true; - } - } -} diff --git a/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs b/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs index be77b7a4e4..b1d97e4a1d 100644 --- a/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs +++ b/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs @@ -1,4 +1,8 @@ using System.Threading.Tasks; +using Jellyfin.Api.Constants; +using Jellyfin.Api.Extensions; +using Jellyfin.Data.Enums; +using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Library; using Microsoft.AspNetCore.Authorization; @@ -9,8 +13,12 @@ namespace Jellyfin.Api.Auth.DefaultAuthorizationPolicy /// /// Default authorization handler. /// - public class DefaultAuthorizationHandler : BaseAuthorizationHandler + public class DefaultAuthorizationHandler : AuthorizationHandler { + private readonly IUserManager _userManager; + private readonly INetworkManager _networkManager; + private readonly IHttpContextAccessor _httpContextAccessor; + /// /// Initializes a new instance of the class. /// @@ -21,21 +29,56 @@ namespace Jellyfin.Api.Auth.DefaultAuthorizationPolicy IUserManager userManager, INetworkManager networkManager, IHttpContextAccessor httpContextAccessor) - : base(userManager, networkManager, httpContextAccessor) { + _userManager = userManager; + _networkManager = networkManager; + _httpContextAccessor = httpContextAccessor; } /// protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, DefaultAuthorizationRequirement requirement) { - var validated = ValidateClaims(context.User); - if (validated) + var isApiKey = context.User.GetIsApiKey(); + var userId = context.User.GetUserId(); + // This likely only happens during the wizard, so skip the default checks and let any other handlers do it + if (!isApiKey && userId.Equals(default)) { - context.Succeed(requirement); + return Task.CompletedTask; } - else + + var isInLocalNetwork = _httpContextAccessor.HttpContext is not null + && _networkManager.IsInLocalNetwork(_httpContextAccessor.HttpContext.GetNormalizedRemoteIp()); + var user = _userManager.GetUserById(userId); + if (user is null) + { + throw new ResourceNotFoundException(); + } + + // User cannot access remotely and user is remote + if (!isInLocalNetwork && !user.HasPermission(PermissionKind.EnableRemoteAccess)) { context.Fail(); + return Task.CompletedTask; + } + + // Admins can do everything + if (isApiKey || context.User.IsInRole(UserRoles.Administrator)) + { + context.Succeed(requirement); + return Task.CompletedTask; + } + + // It's not great to have this check, but parental schedule must usually be honored except in a few rare cases + if (requirement.ValidateParentalSchedule && !user.IsParentalScheduleAllowed()) + { + context.Fail(); + return Task.CompletedTask; + } + + // Only succeed if the requirement isn't a subclass as any subclassed requirement will handle success in its own handler + if (requirement.GetType() == typeof(DefaultAuthorizationRequirement)) + { + context.Succeed(requirement); } return Task.CompletedTask; diff --git a/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationRequirement.cs b/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationRequirement.cs index 7cea00b694..5ba1bc330d 100644 --- a/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationRequirement.cs +++ b/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationRequirement.cs @@ -7,5 +7,18 @@ namespace Jellyfin.Api.Auth.DefaultAuthorizationPolicy /// public class DefaultAuthorizationRequirement : IAuthorizationRequirement { + /// + /// Initializes a new instance of the class. + /// + /// A value indicating whether to validate parental schedule. + public DefaultAuthorizationRequirement(bool validateParentalSchedule = true) + { + ValidateParentalSchedule = validateParentalSchedule; + } + + /// + /// Gets a value indicating whether to ignore parental schedule. + /// + public bool ValidateParentalSchedule { get; } } } diff --git a/Jellyfin.Api/Auth/DownloadPolicy/DownloadHandler.cs b/Jellyfin.Api/Auth/DownloadPolicy/DownloadHandler.cs deleted file mode 100644 index b61680ab1a..0000000000 --- a/Jellyfin.Api/Auth/DownloadPolicy/DownloadHandler.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System.Threading.Tasks; -using MediaBrowser.Common.Net; -using MediaBrowser.Controller.Library; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; - -namespace Jellyfin.Api.Auth.DownloadPolicy -{ - /// - /// Download authorization handler. - /// - public class DownloadHandler : BaseAuthorizationHandler - { - /// - /// Initializes a new instance of the class. - /// - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - public DownloadHandler( - IUserManager userManager, - INetworkManager networkManager, - IHttpContextAccessor httpContextAccessor) - : base(userManager, networkManager, httpContextAccessor) - { - } - - /// - protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, DownloadRequirement requirement) - { - var validated = ValidateClaims(context.User); - if (validated) - { - context.Succeed(requirement); - } - else - { - context.Fail(); - } - - return Task.CompletedTask; - } - } -} diff --git a/Jellyfin.Api/Auth/DownloadPolicy/DownloadRequirement.cs b/Jellyfin.Api/Auth/DownloadPolicy/DownloadRequirement.cs deleted file mode 100644 index b0a72a9dec..0000000000 --- a/Jellyfin.Api/Auth/DownloadPolicy/DownloadRequirement.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Microsoft.AspNetCore.Authorization; - -namespace Jellyfin.Api.Auth.DownloadPolicy -{ - /// - /// The download permission requirement. - /// - public class DownloadRequirement : IAuthorizationRequirement - { - } -} diff --git a/Jellyfin.Api/Auth/FirstTimeOrIgnoreParentalControlSetupPolicy/FirstTimeOrIgnoreParentalControlSetupHandler.cs b/Jellyfin.Api/Auth/FirstTimeOrIgnoreParentalControlSetupPolicy/FirstTimeOrIgnoreParentalControlSetupHandler.cs deleted file mode 100644 index 31482a930f..0000000000 --- a/Jellyfin.Api/Auth/FirstTimeOrIgnoreParentalControlSetupPolicy/FirstTimeOrIgnoreParentalControlSetupHandler.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System.Threading.Tasks; -using MediaBrowser.Common.Configuration; -using MediaBrowser.Common.Net; -using MediaBrowser.Controller.Library; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; - -namespace Jellyfin.Api.Auth.FirstTimeOrIgnoreParentalControlSetupPolicy -{ - /// - /// Ignore parental control schedule and allow before startup wizard has been completed. - /// - public class FirstTimeOrIgnoreParentalControlSetupHandler : BaseAuthorizationHandler - { - private readonly IConfigurationManager _configurationManager; - - /// - /// Initializes a new instance of the class. - /// - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - public FirstTimeOrIgnoreParentalControlSetupHandler( - IUserManager userManager, - INetworkManager networkManager, - IHttpContextAccessor httpContextAccessor, - IConfigurationManager configurationManager) - : base(userManager, networkManager, httpContextAccessor) - { - _configurationManager = configurationManager; - } - - /// - protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, FirstTimeOrIgnoreParentalControlSetupRequirement requirement) - { - if (!_configurationManager.CommonConfiguration.IsStartupWizardCompleted) - { - context.Succeed(requirement); - return Task.CompletedTask; - } - - var validated = ValidateClaims(context.User, ignoreSchedule: true); - if (validated) - { - context.Succeed(requirement); - } - else - { - context.Fail(); - } - - return Task.CompletedTask; - } - } -} diff --git a/Jellyfin.Api/Auth/FirstTimeOrIgnoreParentalControlSetupPolicy/FirstTimeOrIgnoreParentalControlSetupRequirement.cs b/Jellyfin.Api/Auth/FirstTimeOrIgnoreParentalControlSetupPolicy/FirstTimeOrIgnoreParentalControlSetupRequirement.cs deleted file mode 100644 index 00aaec334b..0000000000 --- a/Jellyfin.Api/Auth/FirstTimeOrIgnoreParentalControlSetupPolicy/FirstTimeOrIgnoreParentalControlSetupRequirement.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Microsoft.AspNetCore.Authorization; - -namespace Jellyfin.Api.Auth.FirstTimeOrIgnoreParentalControlSetupPolicy -{ - /// - /// First time setup or ignore parental controls requirement. - /// - public class FirstTimeOrIgnoreParentalControlSetupRequirement : IAuthorizationRequirement - { - } -} diff --git a/Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultHandler.cs b/Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultHandler.cs deleted file mode 100644 index dd0bd4ec2f..0000000000 --- a/Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultHandler.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System.Threading.Tasks; -using MediaBrowser.Common.Configuration; -using MediaBrowser.Common.Net; -using MediaBrowser.Controller.Library; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; - -namespace Jellyfin.Api.Auth.FirstTimeSetupOrDefaultPolicy -{ - /// - /// Authorization handler for requiring first time setup or default privileges. - /// - public class FirstTimeSetupOrDefaultHandler : BaseAuthorizationHandler - { - private readonly IConfigurationManager _configurationManager; - - /// - /// Initializes a new instance of the class. - /// - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - public FirstTimeSetupOrDefaultHandler( - IConfigurationManager configurationManager, - IUserManager userManager, - INetworkManager networkManager, - IHttpContextAccessor httpContextAccessor) - : base(userManager, networkManager, httpContextAccessor) - { - _configurationManager = configurationManager; - } - - /// - protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, FirstTimeSetupOrDefaultRequirement requirement) - { - if (!_configurationManager.CommonConfiguration.IsStartupWizardCompleted) - { - context.Succeed(requirement); - return Task.CompletedTask; - } - - var validated = ValidateClaims(context.User); - if (validated) - { - context.Succeed(requirement); - } - else - { - context.Fail(); - } - - return Task.CompletedTask; - } - } -} diff --git a/Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultRequirement.cs b/Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultRequirement.cs deleted file mode 100644 index f7366bd7a9..0000000000 --- a/Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultRequirement.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Microsoft.AspNetCore.Authorization; - -namespace Jellyfin.Api.Auth.FirstTimeSetupOrDefaultPolicy -{ - /// - /// The authorization requirement, requiring incomplete first time setup or default privileges, for the authorization handler. - /// - public class FirstTimeSetupOrDefaultRequirement : IAuthorizationRequirement - { - } -} diff --git a/Jellyfin.Api/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedRequirement.cs b/Jellyfin.Api/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedRequirement.cs deleted file mode 100644 index 51ba637b60..0000000000 --- a/Jellyfin.Api/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedRequirement.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Microsoft.AspNetCore.Authorization; - -namespace Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy -{ - /// - /// The authorization requirement, requiring incomplete first time setup or elevated privileges, for the authorization handler. - /// - public class FirstTimeSetupOrElevatedRequirement : IAuthorizationRequirement - { - } -} diff --git a/Jellyfin.Api/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandler.cs b/Jellyfin.Api/Auth/FirstTimeSetupPolicy/FirstTimeSetupHandler.cs similarity index 56% rename from Jellyfin.Api/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandler.cs rename to Jellyfin.Api/Auth/FirstTimeSetupPolicy/FirstTimeSetupHandler.cs index 90b76ee99a..28ba258503 100644 --- a/Jellyfin.Api/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandler.cs +++ b/Jellyfin.Api/Auth/FirstTimeSetupPolicy/FirstTimeSetupHandler.cs @@ -1,39 +1,36 @@ using System.Threading.Tasks; using Jellyfin.Api.Constants; +using Jellyfin.Api.Extensions; using MediaBrowser.Common.Configuration; -using MediaBrowser.Common.Net; +using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Library; using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; -namespace Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy +namespace Jellyfin.Api.Auth.FirstTimeSetupPolicy { /// - /// Authorization handler for requiring first time setup or elevated privileges. + /// Authorization handler for requiring first time setup or default privileges. /// - public class FirstTimeSetupOrElevatedHandler : BaseAuthorizationHandler + public class FirstTimeSetupHandler : AuthorizationHandler { private readonly IConfigurationManager _configurationManager; + private readonly IUserManager _userManager; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// Instance of the interface. /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - public FirstTimeSetupOrElevatedHandler( + public FirstTimeSetupHandler( IConfigurationManager configurationManager, - IUserManager userManager, - INetworkManager networkManager, - IHttpContextAccessor httpContextAccessor) - : base(userManager, networkManager, httpContextAccessor) + IUserManager userManager) { _configurationManager = configurationManager; + _userManager = userManager; } /// - protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, FirstTimeSetupOrElevatedRequirement requirement) + protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, FirstTimeSetupRequirement requirement) { if (!_configurationManager.CommonConfiguration.IsStartupWizardCompleted) { @@ -41,14 +38,27 @@ namespace Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy return Task.CompletedTask; } - var validated = ValidateClaims(context.User); - if (validated && context.User.IsInRole(UserRoles.Administrator)) - { - context.Succeed(requirement); - } - else + if (requirement.RequireAdmin && !context.User.IsInRole(UserRoles.Administrator)) { context.Fail(); + return Task.CompletedTask; + } + + if (!requirement.ValidateParentalSchedule) + { + context.Succeed(requirement); + return Task.CompletedTask; + } + + var user = _userManager.GetUserById(context.User.GetUserId()); + if (user is null) + { + throw new ResourceNotFoundException(); + } + + if (user.IsParentalScheduleAllowed()) + { + context.Succeed(requirement); } return Task.CompletedTask; diff --git a/Jellyfin.Api/Auth/FirstTimeSetupPolicy/FirstTimeSetupRequirement.cs b/Jellyfin.Api/Auth/FirstTimeSetupPolicy/FirstTimeSetupRequirement.cs new file mode 100644 index 0000000000..6252a2feb8 --- /dev/null +++ b/Jellyfin.Api/Auth/FirstTimeSetupPolicy/FirstTimeSetupRequirement.cs @@ -0,0 +1,25 @@ +using Jellyfin.Api.Auth.DefaultAuthorizationPolicy; + +namespace Jellyfin.Api.Auth.FirstTimeSetupPolicy +{ + /// + /// The authorization requirement, requiring incomplete first time setup or default privileges, for the authorization handler. + /// + public class FirstTimeSetupRequirement : DefaultAuthorizationRequirement + { + /// + /// Initializes a new instance of the class. + /// + /// A value indicating whether to ignore parental schedule. + /// A value indicating whether administrator role is required. + public FirstTimeSetupRequirement(bool validateParentalSchedule = false, bool requireAdmin = true) : base(validateParentalSchedule) + { + RequireAdmin = requireAdmin; + } + + /// + /// Gets a value indicating whether administrator role is required. + /// + public bool RequireAdmin { get; } + } +} diff --git a/Jellyfin.Api/Auth/IgnoreParentalControlPolicy/IgnoreParentalControlHandler.cs b/Jellyfin.Api/Auth/IgnoreParentalControlPolicy/IgnoreParentalControlHandler.cs deleted file mode 100644 index a7623556a9..0000000000 --- a/Jellyfin.Api/Auth/IgnoreParentalControlPolicy/IgnoreParentalControlHandler.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System.Threading.Tasks; -using MediaBrowser.Common.Net; -using MediaBrowser.Controller.Library; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; - -namespace Jellyfin.Api.Auth.IgnoreParentalControlPolicy -{ - /// - /// Escape schedule controls handler. - /// - public class IgnoreParentalControlHandler : BaseAuthorizationHandler - { - /// - /// Initializes a new instance of the class. - /// - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - public IgnoreParentalControlHandler( - IUserManager userManager, - INetworkManager networkManager, - IHttpContextAccessor httpContextAccessor) - : base(userManager, networkManager, httpContextAccessor) - { - } - - /// - protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, IgnoreParentalControlRequirement requirement) - { - var validated = ValidateClaims(context.User, ignoreSchedule: true); - if (validated) - { - context.Succeed(requirement); - } - else - { - context.Fail(); - } - - return Task.CompletedTask; - } - } -} diff --git a/Jellyfin.Api/Auth/IgnoreParentalControlPolicy/IgnoreParentalControlRequirement.cs b/Jellyfin.Api/Auth/IgnoreParentalControlPolicy/IgnoreParentalControlRequirement.cs deleted file mode 100644 index cdad74270e..0000000000 --- a/Jellyfin.Api/Auth/IgnoreParentalControlPolicy/IgnoreParentalControlRequirement.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Microsoft.AspNetCore.Authorization; - -namespace Jellyfin.Api.Auth.IgnoreParentalControlPolicy -{ - /// - /// Escape schedule controls requirement. - /// - public class IgnoreParentalControlRequirement : IAuthorizationRequirement - { - } -} diff --git a/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationHandler.cs b/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationHandler.cs index 14722aa57e..6ed6fc90be 100644 --- a/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationHandler.cs +++ b/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationHandler.cs @@ -1,7 +1,7 @@ -using System.Threading.Tasks; +using System.Threading.Tasks; using Jellyfin.Api.Constants; +using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; -using MediaBrowser.Controller.Library; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; @@ -10,27 +10,38 @@ namespace Jellyfin.Api.Auth.LocalAccessOrRequiresElevationPolicy /// /// Local access or require elevated privileges handler. /// - public class LocalAccessOrRequiresElevationHandler : BaseAuthorizationHandler + public class LocalAccessOrRequiresElevationHandler : AuthorizationHandler { + private readonly INetworkManager _networkManager; + private readonly IHttpContextAccessor _httpContextAccessor; + /// /// Initializes a new instance of the class. /// - /// Instance of the interface. /// Instance of the interface. /// Instance of the interface. public LocalAccessOrRequiresElevationHandler( - IUserManager userManager, INetworkManager networkManager, IHttpContextAccessor httpContextAccessor) - : base(userManager, networkManager, httpContextAccessor) { + _networkManager = networkManager; + _httpContextAccessor = httpContextAccessor; } /// protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, LocalAccessOrRequiresElevationRequirement requirement) { - var validated = ValidateClaims(context.User, localAccessOnly: true); - if (validated || context.User.IsInRole(UserRoles.Administrator)) + var ip = _httpContextAccessor.HttpContext?.GetNormalizedRemoteIp(); + + // Loopback will be on LAN, so we can accept null. + if (ip is null || _networkManager.IsInLocalNetwork(ip)) + { + context.Succeed(requirement); + + return Task.CompletedTask; + } + + if (context.User.IsInRole(UserRoles.Administrator)) { context.Succeed(requirement); } diff --git a/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationRequirement.cs b/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationRequirement.cs index d9c64d01c4..f633c69d8f 100644 --- a/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationRequirement.cs +++ b/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationRequirement.cs @@ -1,4 +1,4 @@ -using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Authorization; namespace Jellyfin.Api.Auth.LocalAccessOrRequiresElevationPolicy { diff --git a/Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessHandler.cs b/Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessHandler.cs deleted file mode 100644 index d772ec5542..0000000000 --- a/Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessHandler.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System.Threading.Tasks; -using MediaBrowser.Common.Net; -using MediaBrowser.Controller.Library; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; - -namespace Jellyfin.Api.Auth.LocalAccessPolicy -{ - /// - /// Local access handler. - /// - public class LocalAccessHandler : BaseAuthorizationHandler - { - /// - /// Initializes a new instance of the class. - /// - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - public LocalAccessHandler( - IUserManager userManager, - INetworkManager networkManager, - IHttpContextAccessor httpContextAccessor) - : base(userManager, networkManager, httpContextAccessor) - { - } - - /// - protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, LocalAccessRequirement requirement) - { - var validated = ValidateClaims(context.User, localAccessOnly: true); - if (validated) - { - context.Succeed(requirement); - } - else - { - context.Fail(); - } - - return Task.CompletedTask; - } - } -} diff --git a/Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessRequirement.cs b/Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessRequirement.cs deleted file mode 100644 index 761127fa40..0000000000 --- a/Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessRequirement.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Microsoft.AspNetCore.Authorization; - -namespace Jellyfin.Api.Auth.LocalAccessPolicy -{ - /// - /// The local access authorization requirement. - /// - public class LocalAccessRequirement : IAuthorizationRequirement - { - } -} diff --git a/Jellyfin.Api/Auth/RequiresElevationPolicy/RequiresElevationHandler.cs b/Jellyfin.Api/Auth/RequiresElevationPolicy/RequiresElevationHandler.cs deleted file mode 100644 index b235c4b63b..0000000000 --- a/Jellyfin.Api/Auth/RequiresElevationPolicy/RequiresElevationHandler.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System.Threading.Tasks; -using Jellyfin.Api.Constants; -using MediaBrowser.Common.Net; -using MediaBrowser.Controller.Library; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; - -namespace Jellyfin.Api.Auth.RequiresElevationPolicy -{ - /// - /// Authorization handler for requiring elevated privileges. - /// - public class RequiresElevationHandler : BaseAuthorizationHandler - { - /// - /// Initializes a new instance of the class. - /// - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - public RequiresElevationHandler( - IUserManager userManager, - INetworkManager networkManager, - IHttpContextAccessor httpContextAccessor) - : base(userManager, networkManager, httpContextAccessor) - { - } - - /// - protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, RequiresElevationRequirement requirement) - { - var validated = ValidateClaims(context.User); - if (validated && context.User.IsInRole(UserRoles.Administrator)) - { - context.Succeed(requirement); - } - else - { - context.Fail(); - } - - return Task.CompletedTask; - } - } -} diff --git a/Jellyfin.Api/Auth/RequiresElevationPolicy/RequiresElevationRequirement.cs b/Jellyfin.Api/Auth/RequiresElevationPolicy/RequiresElevationRequirement.cs deleted file mode 100644 index cfff1cc0c5..0000000000 --- a/Jellyfin.Api/Auth/RequiresElevationPolicy/RequiresElevationRequirement.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Microsoft.AspNetCore.Authorization; - -namespace Jellyfin.Api.Auth.RequiresElevationPolicy -{ - /// - /// The authorization requirement for requiring elevated privileges in the authorization handler. - /// - public class RequiresElevationRequirement : IAuthorizationRequirement - { - } -} diff --git a/Jellyfin.Api/Auth/SyncPlayAccessPolicy/SyncPlayAccessHandler.cs b/Jellyfin.Api/Auth/SyncPlayAccessPolicy/SyncPlayAccessHandler.cs index cdd7d8a52b..75ec9fcec6 100644 --- a/Jellyfin.Api/Auth/SyncPlayAccessPolicy/SyncPlayAccessHandler.cs +++ b/Jellyfin.Api/Auth/SyncPlayAccessPolicy/SyncPlayAccessHandler.cs @@ -1,19 +1,17 @@ using System.Threading.Tasks; using Jellyfin.Api.Extensions; -using Jellyfin.Api.Helpers; using Jellyfin.Data.Enums; -using MediaBrowser.Common.Net; +using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.SyncPlay; using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; namespace Jellyfin.Api.Auth.SyncPlayAccessPolicy { /// /// Default authorization handler. /// - public class SyncPlayAccessHandler : BaseAuthorizationHandler + public class SyncPlayAccessHandler : AuthorizationHandler { private readonly ISyncPlayManager _syncPlayManager; private readonly IUserManager _userManager; @@ -23,14 +21,9 @@ namespace Jellyfin.Api.Auth.SyncPlayAccessPolicy /// /// Instance of the interface. /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. public SyncPlayAccessHandler( ISyncPlayManager syncPlayManager, - IUserManager userManager, - INetworkManager networkManager, - IHttpContextAccessor httpContextAccessor) - : base(userManager, networkManager, httpContextAccessor) + IUserManager userManager) { _syncPlayManager = syncPlayManager; _userManager = userManager; @@ -39,27 +32,20 @@ namespace Jellyfin.Api.Auth.SyncPlayAccessPolicy /// protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, SyncPlayAccessRequirement requirement) { - if (!ValidateClaims(context.User)) - { - context.Fail(); - return Task.CompletedTask; - } - var userId = context.User.GetUserId(); var user = _userManager.GetUserById(userId); + if (user is null) + { + throw new ResourceNotFoundException(); + } if (requirement.RequiredAccess == SyncPlayAccessRequirementType.HasAccess) { - if (user.SyncPlayAccess == SyncPlayUserAccessType.CreateAndJoinGroups - || user.SyncPlayAccess == SyncPlayUserAccessType.JoinGroups + if (user.SyncPlayAccess is SyncPlayUserAccessType.CreateAndJoinGroups or SyncPlayUserAccessType.JoinGroups || _syncPlayManager.IsUserActive(userId)) { context.Succeed(requirement); } - else - { - context.Fail(); - } } else if (requirement.RequiredAccess == SyncPlayAccessRequirementType.CreateGroup) { @@ -67,10 +53,6 @@ namespace Jellyfin.Api.Auth.SyncPlayAccessPolicy { context.Succeed(requirement); } - else - { - context.Fail(); - } } else if (requirement.RequiredAccess == SyncPlayAccessRequirementType.JoinGroup) { @@ -79,10 +61,6 @@ namespace Jellyfin.Api.Auth.SyncPlayAccessPolicy { context.Succeed(requirement); } - else - { - context.Fail(); - } } else if (requirement.RequiredAccess == SyncPlayAccessRequirementType.IsInGroup) { @@ -90,14 +68,6 @@ namespace Jellyfin.Api.Auth.SyncPlayAccessPolicy { context.Succeed(requirement); } - else - { - context.Fail(); - } - } - else - { - context.Fail(); } return Task.CompletedTask; diff --git a/Jellyfin.Api/Auth/SyncPlayAccessPolicy/SyncPlayAccessRequirement.cs b/Jellyfin.Api/Auth/SyncPlayAccessPolicy/SyncPlayAccessRequirement.cs index 6fab4c0ad8..220b223b39 100644 --- a/Jellyfin.Api/Auth/SyncPlayAccessPolicy/SyncPlayAccessRequirement.cs +++ b/Jellyfin.Api/Auth/SyncPlayAccessPolicy/SyncPlayAccessRequirement.cs @@ -1,12 +1,12 @@ -using Jellyfin.Data.Enums; -using Microsoft.AspNetCore.Authorization; +using Jellyfin.Api.Auth.DefaultAuthorizationPolicy; +using Jellyfin.Data.Enums; namespace Jellyfin.Api.Auth.SyncPlayAccessPolicy { /// /// The default authorization requirement. /// - public class SyncPlayAccessRequirement : IAuthorizationRequirement + public class SyncPlayAccessRequirement : DefaultAuthorizationRequirement { /// /// Initializes a new instance of the class. diff --git a/Jellyfin.Api/Auth/UserPermissionPolicy/UserPermissionHandler.cs b/Jellyfin.Api/Auth/UserPermissionPolicy/UserPermissionHandler.cs new file mode 100644 index 0000000000..e72bec46fd --- /dev/null +++ b/Jellyfin.Api/Auth/UserPermissionPolicy/UserPermissionHandler.cs @@ -0,0 +1,42 @@ +using System.Threading.Tasks; +using Jellyfin.Api.Extensions; +using MediaBrowser.Common.Extensions; +using MediaBrowser.Controller.Library; +using Microsoft.AspNetCore.Authorization; + +namespace Jellyfin.Api.Auth.UserPermissionPolicy +{ + /// + /// User permission authorization handler. + /// + public class UserPermissionHandler : AuthorizationHandler + { + private readonly IUserManager _userManager; + + /// + /// Initializes a new instance of the class. + /// + /// Instance of the interface. + public UserPermissionHandler(IUserManager userManager) + { + _userManager = userManager; + } + + /// + protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, UserPermissionRequirement requirement) + { + var user = _userManager.GetUserById(context.User.GetUserId()); + if (user is null) + { + throw new ResourceNotFoundException(); + } + + if (user.HasPermission(requirement.RequiredPermission)) + { + context.Succeed(requirement); + } + + return Task.CompletedTask; + } + } +} diff --git a/Jellyfin.Api/Auth/UserPermissionPolicy/UserPermissionRequirement.cs b/Jellyfin.Api/Auth/UserPermissionPolicy/UserPermissionRequirement.cs new file mode 100644 index 0000000000..4694556eb7 --- /dev/null +++ b/Jellyfin.Api/Auth/UserPermissionPolicy/UserPermissionRequirement.cs @@ -0,0 +1,26 @@ +using Jellyfin.Api.Auth.DefaultAuthorizationPolicy; +using Jellyfin.Data.Enums; + +namespace Jellyfin.Api.Auth.UserPermissionPolicy +{ + /// + /// The user permission requirement. + /// + public class UserPermissionRequirement : DefaultAuthorizationRequirement + { + /// + /// Initializes a new instance of the class. + /// + /// The required . + /// Whether to validate the user's parental schedule. + public UserPermissionRequirement(PermissionKind requiredPermission, bool validateParentalSchedule = true) : base(validateParentalSchedule) + { + RequiredPermission = requiredPermission; + } + + /// + /// Gets the required user permission. + /// + public PermissionKind RequiredPermission { get; } + } +} diff --git a/Jellyfin.Api/Constants/Policies.cs b/Jellyfin.Api/Constants/Policies.cs index 5a5a2bf466..53841b0c44 100644 --- a/Jellyfin.Api/Constants/Policies.cs +++ b/Jellyfin.Api/Constants/Policies.cs @@ -5,11 +5,6 @@ namespace Jellyfin.Api.Constants; /// public static class Policies { - /// - /// Policy name for default authorization. - /// - public const string DefaultAuthorization = "DefaultAuthorization"; - /// /// Policy name for requiring first time setup or elevated privileges. /// @@ -74,4 +69,19 @@ public static class Policies /// Policy name for accessing a SyncPlay group. /// public const string SyncPlayIsInGroup = "SyncPlayIsInGroup"; + + /// + /// Policy name for accessing collection management. + /// + public const string CollectionManagement = "CollectionManagement"; + + /// + /// Policy name for accessing LiveTV. + /// + public const string LiveTvAccess = "LiveTvAccess"; + + /// + /// Policy name for managing LiveTV. + /// + public const string LiveTvManagement = "LiveTvManagement"; } diff --git a/Jellyfin.Api/Controllers/ArtistsController.cs b/Jellyfin.Api/Controllers/ArtistsController.cs index 069e7311b8..c9d2f67f92 100644 --- a/Jellyfin.Api/Controllers/ArtistsController.cs +++ b/Jellyfin.Api/Controllers/ArtistsController.cs @@ -1,7 +1,6 @@ using System; using System.ComponentModel.DataAnnotations; using System.Linq; -using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; @@ -23,7 +22,7 @@ namespace Jellyfin.Api.Controllers; /// The artists controller. /// [Route("Artists")] -[Authorize(Policy = Policies.DefaultAuthorization)] +[Authorize] public class ArtistsController : BaseJellyfinApiController { private readonly ILibraryManager _libraryManager; @@ -119,6 +118,7 @@ public class ArtistsController : BaseJellyfinApiController [FromQuery] bool? enableImages = true, [FromQuery] bool enableTotalRecordCount = true) { + userId = RequestHelpers.GetUserId(User, userId); var dtoOptions = new DtoOptions { Fields = fields } .AddClientFields(User) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); @@ -126,7 +126,7 @@ public class ArtistsController : BaseJellyfinApiController User? user = null; BaseItem parentItem = _libraryManager.GetParentItem(parentId, userId); - if (userId.HasValue && !userId.Equals(default)) + if (!userId.Value.Equals(default)) { user = _userManager.GetUserById(userId.Value); } @@ -322,6 +322,7 @@ public class ArtistsController : BaseJellyfinApiController [FromQuery] bool? enableImages = true, [FromQuery] bool enableTotalRecordCount = true) { + userId = RequestHelpers.GetUserId(User, userId); var dtoOptions = new DtoOptions { Fields = fields } .AddClientFields(User) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); @@ -329,7 +330,7 @@ public class ArtistsController : BaseJellyfinApiController User? user = null; BaseItem parentItem = _libraryManager.GetParentItem(parentId, userId); - if (userId.HasValue && !userId.Equals(default)) + if (!userId.Value.Equals(default)) { user = _userManager.GetUserById(userId.Value); } @@ -463,11 +464,12 @@ public class ArtistsController : BaseJellyfinApiController [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult GetArtistByName([FromRoute, Required] string name, [FromQuery] Guid? userId) { + userId = RequestHelpers.GetUserId(User, userId); var dtoOptions = new DtoOptions().AddClientFields(User); var item = _libraryManager.GetArtist(name, dtoOptions); - if (userId.HasValue && !userId.Value.Equals(default)) + if (!userId.Value.Equals(default)) { var user = _userManager.GetUserById(userId.Value); diff --git a/Jellyfin.Api/Controllers/ChannelsController.cs b/Jellyfin.Api/Controllers/ChannelsController.cs index 573b7069c5..b5c4d83462 100644 --- a/Jellyfin.Api/Controllers/ChannelsController.cs +++ b/Jellyfin.Api/Controllers/ChannelsController.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Threading; using System.Threading.Tasks; -using Jellyfin.Api.Constants; using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; using Jellyfin.Data.Enums; @@ -23,7 +22,7 @@ namespace Jellyfin.Api.Controllers; /// /// Channels Controller. /// -[Authorize(Policy = Policies.DefaultAuthorization)] +[Authorize] public class ChannelsController : BaseJellyfinApiController { private readonly IChannelManager _channelManager; @@ -61,11 +60,12 @@ public class ChannelsController : BaseJellyfinApiController [FromQuery] bool? supportsMediaDeletion, [FromQuery] bool? isFavorite) { + userId = RequestHelpers.GetUserId(User, userId); return _channelManager.GetChannels(new ChannelQuery { Limit = limit, StartIndex = startIndex, - UserId = userId ?? Guid.Empty, + UserId = userId.Value, SupportsLatestItems = supportsLatestItems, SupportsMediaDeletion = supportsMediaDeletion, IsFavorite = isFavorite @@ -125,7 +125,8 @@ public class ChannelsController : BaseJellyfinApiController [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields) { - var user = userId is null || userId.Value.Equals(default) + userId = RequestHelpers.GetUserId(User, userId); + var user = userId.Value.Equals(default) ? null : _userManager.GetUserById(userId.Value); @@ -199,7 +200,8 @@ public class ChannelsController : BaseJellyfinApiController [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] channelIds) { - var user = userId is null || userId.Value.Equals(default) + userId = RequestHelpers.GetUserId(User, userId); + var user = userId.Value.Equals(default) ? null : _userManager.GetUserById(userId.Value); diff --git a/Jellyfin.Api/Controllers/ClientLogController.cs b/Jellyfin.Api/Controllers/ClientLogController.cs index 21c31bc936..2c5dbacbbe 100644 --- a/Jellyfin.Api/Controllers/ClientLogController.cs +++ b/Jellyfin.Api/Controllers/ClientLogController.cs @@ -1,7 +1,6 @@ using System.Net.Mime; using System.Threading.Tasks; using Jellyfin.Api.Attributes; -using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; using Jellyfin.Api.Models.ClientLogDtos; using MediaBrowser.Controller.ClientEvent; @@ -15,7 +14,7 @@ namespace Jellyfin.Api.Controllers; /// /// Client log controller. /// -[Authorize(Policy = Policies.DefaultAuthorization)] +[Authorize] public class ClientLogController : BaseJellyfinApiController { private const int MaxDocumentSize = 1_000_000; diff --git a/Jellyfin.Api/Controllers/CollectionController.cs b/Jellyfin.Api/Controllers/CollectionController.cs index 5a4a9bf079..2db04afb80 100644 --- a/Jellyfin.Api/Controllers/CollectionController.cs +++ b/Jellyfin.Api/Controllers/CollectionController.cs @@ -17,7 +17,7 @@ namespace Jellyfin.Api.Controllers; /// The collection controller. /// [Route("Collections")] -[Authorize(Policy = Policies.DefaultAuthorization)] +[Authorize(Policy = Policies.CollectionManagement)] public class CollectionController : BaseJellyfinApiController { private readonly ICollectionManager _collectionManager; diff --git a/Jellyfin.Api/Controllers/ConfigurationController.cs b/Jellyfin.Api/Controllers/ConfigurationController.cs index d53d7cefd0..9007dfc410 100644 --- a/Jellyfin.Api/Controllers/ConfigurationController.cs +++ b/Jellyfin.Api/Controllers/ConfigurationController.cs @@ -19,7 +19,7 @@ namespace Jellyfin.Api.Controllers; /// Configuration Controller. /// [Route("System")] -[Authorize(Policy = Policies.DefaultAuthorization)] +[Authorize] public class ConfigurationController : BaseJellyfinApiController { private readonly IServerConfigurationManager _configurationManager; diff --git a/Jellyfin.Api/Controllers/DashboardController.cs b/Jellyfin.Api/Controllers/DashboardController.cs index f7e978bad0..076084c7a3 100644 --- a/Jellyfin.Api/Controllers/DashboardController.cs +++ b/Jellyfin.Api/Controllers/DashboardController.cs @@ -4,7 +4,6 @@ using System.IO; using System.Linq; using System.Net.Mime; using Jellyfin.Api.Attributes; -using Jellyfin.Api.Constants; using Jellyfin.Api.Models; using MediaBrowser.Common.Plugins; using MediaBrowser.Model.Net; @@ -48,7 +47,7 @@ public class DashboardController : BaseJellyfinApiController [HttpGet("web/ConfigurationPages")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] public ActionResult> GetConfigurationPages( [FromQuery] bool? enableInMainMenu) { diff --git a/Jellyfin.Api/Controllers/DevicesController.cs b/Jellyfin.Api/Controllers/DevicesController.cs index 4978622369..aa0dff2123 100644 --- a/Jellyfin.Api/Controllers/DevicesController.cs +++ b/Jellyfin.Api/Controllers/DevicesController.cs @@ -2,6 +2,7 @@ using System; using System.ComponentModel.DataAnnotations; using System.Threading.Tasks; using Jellyfin.Api.Constants; +using Jellyfin.Api.Helpers; using Jellyfin.Data.Dtos; using Jellyfin.Data.Entities.Security; using Jellyfin.Data.Queries; @@ -48,6 +49,7 @@ public class DevicesController : BaseJellyfinApiController [ProducesResponseType(StatusCodes.Status200OK)] public async Task>> GetDevices([FromQuery] bool? supportsSync, [FromQuery] Guid? userId) { + userId = RequestHelpers.GetUserId(User, userId); return await _deviceManager.GetDevicesForUser(userId, supportsSync).ConfigureAwait(false); } diff --git a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs index 49d87a3621..6f0006832b 100644 --- a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs +++ b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs @@ -3,7 +3,6 @@ using System.ComponentModel.DataAnnotations; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Linq; -using Jellyfin.Api.Constants; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; using MediaBrowser.Common.Extensions; @@ -19,7 +18,7 @@ namespace Jellyfin.Api.Controllers; /// /// Display Preferences Controller. /// -[Authorize(Policy = Policies.DefaultAuthorization)] +[Authorize] public class DisplayPreferencesController : BaseJellyfinApiController { private readonly IDisplayPreferencesManager _displayPreferencesManager; diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs index b68849171f..4d8b4de24f 100644 --- a/Jellyfin.Api/Controllers/DynamicHlsController.cs +++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs @@ -9,7 +9,6 @@ using System.Text; using System.Threading; using System.Threading.Tasks; using Jellyfin.Api.Attributes; -using Jellyfin.Api.Constants; using Jellyfin.Api.Helpers; using Jellyfin.Api.Models.PlaybackDtos; using Jellyfin.Api.Models.StreamingDtos; @@ -36,7 +35,7 @@ namespace Jellyfin.Api.Controllers; /// Dynamic hls controller. /// [Route("")] -[Authorize(Policy = Policies.DefaultAuthorization)] +[Authorize] public class DynamicHlsController : BaseJellyfinApiController { private const string DefaultVodEncoderPreset = "veryfast"; diff --git a/Jellyfin.Api/Controllers/FilterController.cs b/Jellyfin.Api/Controllers/FilterController.cs index 2378aada5f..dac07429ff 100644 --- a/Jellyfin.Api/Controllers/FilterController.cs +++ b/Jellyfin.Api/Controllers/FilterController.cs @@ -1,6 +1,7 @@ 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; @@ -18,7 +19,7 @@ namespace Jellyfin.Api.Controllers; /// Filters controller. /// [Route("")] -[Authorize(Policy = Policies.DefaultAuthorization)] +[Authorize] public class FilterController : BaseJellyfinApiController { private readonly ILibraryManager _libraryManager; @@ -52,7 +53,8 @@ public class FilterController : BaseJellyfinApiController [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes) { - var user = userId is null || userId.Value.Equals(default) + userId = RequestHelpers.GetUserId(User, userId); + var user = userId.Value.Equals(default) ? null : _userManager.GetUserById(userId.Value); @@ -144,7 +146,8 @@ public class FilterController : BaseJellyfinApiController [FromQuery] bool? isSeries, [FromQuery] bool? recursive) { - var user = userId is null || userId.Value.Equals(default) + userId = RequestHelpers.GetUserId(User, userId); + var user = userId.Value.Equals(default) ? null : _userManager.GetUserById(userId.Value); diff --git a/Jellyfin.Api/Controllers/GenresController.cs b/Jellyfin.Api/Controllers/GenresController.cs index 28ebe20475..eb03b514c7 100644 --- a/Jellyfin.Api/Controllers/GenresController.cs +++ b/Jellyfin.Api/Controllers/GenresController.cs @@ -1,7 +1,6 @@ using System; using System.ComponentModel.DataAnnotations; using System.Linq; -using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; @@ -23,7 +22,7 @@ namespace Jellyfin.Api.Controllers; /// /// The genres controller. /// -[Authorize(Policy = Policies.DefaultAuthorization)] +[Authorize] public class GenresController : BaseJellyfinApiController { private readonly IUserManager _userManager; @@ -91,11 +90,12 @@ public class GenresController : BaseJellyfinApiController [FromQuery] bool? enableImages = true, [FromQuery] bool enableTotalRecordCount = true) { + userId = RequestHelpers.GetUserId(User, userId); var dtoOptions = new DtoOptions { Fields = fields } .AddClientFields(User) .AddAdditionalDtoOptions(enableImages, false, imageTypeLimit, enableImageTypes); - User? user = userId is null || userId.Value.Equals(default) + User? user = userId.Value.Equals(default) ? null : _userManager.GetUserById(userId.Value); @@ -132,7 +132,7 @@ public class GenresController : BaseJellyfinApiController QueryResult<(BaseItem, ItemCounts)> result; if (parentItem is ICollectionFolder parentCollectionFolder && (string.Equals(parentCollectionFolder.CollectionType, CollectionType.Music, StringComparison.Ordinal) - || string.Equals(parentCollectionFolder.CollectionType, CollectionType.MusicVideos, StringComparison.Ordinal))) + || string.Equals(parentCollectionFolder.CollectionType, CollectionType.MusicVideos, StringComparison.Ordinal))) { result = _libraryManager.GetMusicGenres(query); } @@ -156,6 +156,7 @@ public class GenresController : BaseJellyfinApiController [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult GetGenre([FromRoute, Required] string genreName, [FromQuery] Guid? userId) { + userId = RequestHelpers.GetUserId(User, userId); var dtoOptions = new DtoOptions() .AddClientFields(User); @@ -171,7 +172,7 @@ public class GenresController : BaseJellyfinApiController item ??= new Genre(); - if (userId is null || userId.Value.Equals(default)) + if (userId.Value.Equals(default)) { return _dtoService.GetBaseItemDto(item, dtoOptions); } diff --git a/Jellyfin.Api/Controllers/HlsSegmentController.cs b/Jellyfin.Api/Controllers/HlsSegmentController.cs index 085115e1c8..d7cec865e1 100644 --- a/Jellyfin.Api/Controllers/HlsSegmentController.cs +++ b/Jellyfin.Api/Controllers/HlsSegmentController.cs @@ -4,7 +4,6 @@ using System.Diagnostics.CodeAnalysis; using System.IO; using System.Threading.Tasks; using Jellyfin.Api.Attributes; -using Jellyfin.Api.Constants; using Jellyfin.Api.Helpers; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Configuration; @@ -80,7 +79,7 @@ public class HlsSegmentController : BaseJellyfinApiController /// Hls video playlist returned. /// A containing the playlist. [HttpGet("Videos/{itemId}/hls/{playlistId}/stream.m3u8")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesPlaylistFile] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "itemId", Justification = "Required for ServiceStack")] @@ -106,7 +105,7 @@ public class HlsSegmentController : BaseJellyfinApiController /// Encoding stopped successfully. /// A indicating success. [HttpDelete("Videos/ActiveEncodings")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult StopEncodingProcess( [FromQuery, Required] string deviceId, diff --git a/Jellyfin.Api/Controllers/ImageController.cs b/Jellyfin.Api/Controllers/ImageController.cs index cc824c65ab..3c5f18af55 100644 --- a/Jellyfin.Api/Controllers/ImageController.cs +++ b/Jellyfin.Api/Controllers/ImageController.cs @@ -88,9 +88,10 @@ public class ImageController : BaseJellyfinApiController /// User does not have permission to delete the image. /// A . [HttpPost("Users/{userId}/Images/{imageType}")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [AcceptsImageFile] [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")] @@ -99,12 +100,22 @@ public class ImageController : BaseJellyfinApiController [FromRoute, Required] ImageType imageType, [FromQuery] int? index = null) { + var user = _userManager.GetUserById(userId); + if (user is null) + { + return NotFound(); + } + if (!RequestHelpers.AssertCanUpdateUser(_userManager, HttpContext.User, userId, true)) { return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to update the image."); } - var user = _userManager.GetUserById(userId); + if (!TryGetImageExtensionFromContentType(Request.ContentType, out string? extension)) + { + return BadRequest("Incorrect ContentType."); + } + var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false); await using (memoryStream.ConfigureAwait(false)) { @@ -116,7 +127,7 @@ public class ImageController : BaseJellyfinApiController await _userManager.ClearProfileImageAsync(user).ConfigureAwait(false); } - user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + MimeTypes.ToExtension(mimeType ?? string.Empty))); + user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + extension)); await _providerManager .SaveImage(memoryStream, mimeType, user.ProfileImage.Path) @@ -137,9 +148,10 @@ public class ImageController : BaseJellyfinApiController /// User does not have permission to delete the image. /// A . [HttpPost("Users/{userId}/Images/{imageType}/{index}")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [AcceptsImageFile] [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")] @@ -148,12 +160,22 @@ public class ImageController : BaseJellyfinApiController [FromRoute, Required] ImageType imageType, [FromRoute] int index) { + var user = _userManager.GetUserById(userId); + if (user is null) + { + return NotFound(); + } + if (!RequestHelpers.AssertCanUpdateUser(_userManager, HttpContext.User, userId, true)) { return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to update the image."); } - var user = _userManager.GetUserById(userId); + if (!TryGetImageExtensionFromContentType(Request.ContentType, out string? extension)) + { + return BadRequest("Incorrect ContentType."); + } + var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false); await using (memoryStream.ConfigureAwait(false)) { @@ -165,7 +187,7 @@ public class ImageController : BaseJellyfinApiController await _userManager.ClearProfileImageAsync(user).ConfigureAwait(false); } - user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + MimeTypes.ToExtension(mimeType ?? string.Empty))); + user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + extension)); await _providerManager .SaveImage(memoryStream, mimeType, user.ProfileImage.Path) @@ -186,7 +208,7 @@ public class ImageController : BaseJellyfinApiController /// User does not have permission to delete the image. /// A . [HttpDelete("Users/{userId}/Images/{imageType}")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")] [ProducesResponseType(StatusCodes.Status204NoContent)] @@ -230,7 +252,7 @@ public class ImageController : BaseJellyfinApiController /// User does not have permission to delete the image. /// A . [HttpDelete("Users/{userId}/Images/{imageType}/{index}")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")] [ProducesResponseType(StatusCodes.Status204NoContent)] @@ -332,6 +354,7 @@ public class ImageController : BaseJellyfinApiController [Authorize(Policy = Policies.RequiresElevation)] [AcceptsImageFile] [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status404NotFound)] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")] public async Task SetItemImage( @@ -344,6 +367,11 @@ public class ImageController : BaseJellyfinApiController return NotFound(); } + if (!TryGetImageExtensionFromContentType(Request.ContentType, out _)) + { + return BadRequest("Incorrect ContentType."); + } + var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false); await using (memoryStream.ConfigureAwait(false)) { @@ -369,6 +397,7 @@ public class ImageController : BaseJellyfinApiController [Authorize(Policy = Policies.RequiresElevation)] [AcceptsImageFile] [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status404NotFound)] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")] public async Task SetItemImageByIndex( @@ -382,6 +411,11 @@ public class ImageController : BaseJellyfinApiController return NotFound(); } + if (!TryGetImageExtensionFromContentType(Request.ContentType, out _)) + { + return BadRequest("Incorrect ContentType."); + } + var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false); await using (memoryStream.ConfigureAwait(false)) { @@ -432,7 +466,7 @@ public class ImageController : BaseJellyfinApiController /// Item not found. /// The list of image infos on success, or if item not found. [HttpGet("Items/{itemId}/Images")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task>> GetItemImageInfos([FromRoute, Required] Guid itemId) @@ -1753,22 +1787,14 @@ public class ImageController : BaseJellyfinApiController [AcceptsImageFile] public async Task UploadCustomSplashscreen() { + if (!TryGetImageExtensionFromContentType(Request.ContentType, out var extension)) + { + return BadRequest("Incorrect ContentType."); + } + var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false); await using (memoryStream.ConfigureAwait(false)) { - var mimeType = MediaTypeHeaderValue.Parse(Request.ContentType).MediaType; - - if (!mimeType.HasValue) - { - return BadRequest("Error reading mimetype from uploaded image"); - } - - var extension = MimeTypes.ToExtension(mimeType.Value); - if (string.IsNullOrEmpty(extension)) - { - return BadRequest("Error converting mimetype to an image extension"); - } - var filePath = Path.Combine(_appPaths.DataPath, "splashscreen-upload" + extension); var brandingOptions = _serverConfigurationManager.GetConfiguration("branding"); brandingOptions.SplashscreenLocation = filePath; @@ -1930,10 +1956,10 @@ public class ImageController : BaseJellyfinApiController } var responseHeaders = new Dictionary - { - { "transferMode.dlna.org", "Interactive" }, - { "realTimeInfo.dlna.org", "DLNA.ORG_TLAG=*" } - }; + { + { "transferMode.dlna.org", "Interactive" }, + { "realTimeInfo.dlna.org", "DLNA.ORG_TLAG=*" } + }; if (!imageInfo.IsLocalFile && item is not null) { @@ -2096,4 +2122,23 @@ public class ImageController : BaseJellyfinApiController return PhysicalFile(imagePath, imageContentType ?? MediaTypeNames.Text.Plain); } + + internal static bool TryGetImageExtensionFromContentType(string? contentType, [NotNullWhen(true)] out string? extension) + { + extension = null; + if (string.IsNullOrEmpty(contentType)) + { + return false; + } + + if (MediaTypeHeaderValue.TryParse(contentType, out var parsedValue) + && parsedValue.MediaType.HasValue + && MimeTypes.IsImage(parsedValue.MediaType.Value)) + { + extension = MimeTypes.ToExtension(parsedValue.MediaType.Value); + return extension is not null; + } + + return false; + } } diff --git a/Jellyfin.Api/Controllers/InstantMixController.cs b/Jellyfin.Api/Controllers/InstantMixController.cs index 89592bade4..4dc2a4253d 100644 --- a/Jellyfin.Api/Controllers/InstantMixController.cs +++ b/Jellyfin.Api/Controllers/InstantMixController.cs @@ -1,8 +1,8 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; +using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; using Jellyfin.Data.Entities; using MediaBrowser.Controller.Dto; @@ -22,7 +22,7 @@ namespace Jellyfin.Api.Controllers; /// The instant mix controller. /// [Route("")] -[Authorize(Policy = Policies.DefaultAuthorization)] +[Authorize] public class InstantMixController : BaseJellyfinApiController { private readonly IUserManager _userManager; @@ -75,7 +75,8 @@ public class InstantMixController : BaseJellyfinApiController [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes) { var item = _libraryManager.GetItemById(id); - var user = userId is null || userId.Value.Equals(default) + userId = RequestHelpers.GetUserId(User, userId); + var user = userId.Value.Equals(default) ? null : _userManager.GetUserById(userId.Value); var dtoOptions = new DtoOptions { Fields = fields } @@ -111,7 +112,8 @@ public class InstantMixController : BaseJellyfinApiController [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes) { var album = _libraryManager.GetItemById(id); - var user = userId is null || userId.Value.Equals(default) + userId = RequestHelpers.GetUserId(User, userId); + var user = userId.Value.Equals(default) ? null : _userManager.GetUserById(userId.Value); var dtoOptions = new DtoOptions { Fields = fields } @@ -147,7 +149,8 @@ public class InstantMixController : BaseJellyfinApiController [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes) { var playlist = (Playlist)_libraryManager.GetItemById(id); - var user = userId is null || userId.Value.Equals(default) + userId = RequestHelpers.GetUserId(User, userId); + var user = userId.Value.Equals(default) ? null : _userManager.GetUserById(userId.Value); var dtoOptions = new DtoOptions { Fields = fields } @@ -182,7 +185,8 @@ public class InstantMixController : BaseJellyfinApiController [FromQuery] int? imageTypeLimit, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes) { - var user = userId is null || userId.Value.Equals(default) + userId = RequestHelpers.GetUserId(User, userId); + var user = userId.Value.Equals(default) ? null : _userManager.GetUserById(userId.Value); var dtoOptions = new DtoOptions { Fields = fields } @@ -218,7 +222,8 @@ public class InstantMixController : BaseJellyfinApiController [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes) { var item = _libraryManager.GetItemById(id); - var user = userId is null || userId.Value.Equals(default) + userId = RequestHelpers.GetUserId(User, userId); + var user = userId.Value.Equals(default) ? null : _userManager.GetUserById(userId.Value); var dtoOptions = new DtoOptions { Fields = fields } @@ -254,7 +259,8 @@ public class InstantMixController : BaseJellyfinApiController [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes) { var item = _libraryManager.GetItemById(id); - var user = userId is null || userId.Value.Equals(default) + userId = RequestHelpers.GetUserId(User, userId); + var user = userId.Value.Equals(default) ? null : _userManager.GetUserById(userId.Value); var dtoOptions = new DtoOptions { Fields = fields } @@ -327,7 +333,8 @@ public class InstantMixController : BaseJellyfinApiController [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes) { var item = _libraryManager.GetItemById(id); - var user = userId is null || userId.Value.Equals(default) + userId = RequestHelpers.GetUserId(User, userId); + var user = userId.Value.Equals(default) ? null : _userManager.GetUserById(userId.Value); var dtoOptions = new DtoOptions { Fields = fields } diff --git a/Jellyfin.Api/Controllers/ItemLookupController.cs b/Jellyfin.Api/Controllers/ItemLookupController.cs index c2ce4e67e5..b030e74dda 100644 --- a/Jellyfin.Api/Controllers/ItemLookupController.cs +++ b/Jellyfin.Api/Controllers/ItemLookupController.cs @@ -23,7 +23,7 @@ namespace Jellyfin.Api.Controllers; /// Item lookup controller. /// [Route("")] -[Authorize(Policy = Policies.DefaultAuthorization)] +[Authorize] public class ItemLookupController : BaseJellyfinApiController { private readonly IProviderManager _providerManager; diff --git a/Jellyfin.Api/Controllers/ItemsController.cs b/Jellyfin.Api/Controllers/ItemsController.cs index 134974dbe0..728e628109 100644 --- a/Jellyfin.Api/Controllers/ItemsController.cs +++ b/Jellyfin.Api/Controllers/ItemsController.cs @@ -1,11 +1,11 @@ using System; using System.ComponentModel.DataAnnotations; using System.Linq; -using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; using Jellyfin.Data.Enums; +using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; @@ -25,7 +25,7 @@ namespace Jellyfin.Api.Controllers; /// The items controller. /// [Route("")] -[Authorize(Policy = Policies.DefaultAuthorization)] +[Authorize] public class ItemsController : BaseJellyfinApiController { private readonly IUserManager _userManager; @@ -240,8 +240,9 @@ public class ItemsController : BaseJellyfinApiController { var isApiKey = User.GetIsApiKey(); // if api key is used (auth.IsApiKey == true), then `user` will be null throughout this method - var user = !isApiKey && userId.HasValue && !userId.Value.Equals(default) - ? _userManager.GetUserById(userId.Value) + userId = RequestHelpers.GetUserId(User, userId); + var user = !isApiKey && !userId.Value.Equals(default) + ? _userManager.GetUserById(userId.Value) ?? throw new ResourceNotFoundException() : null; // beyond this point, we're either using an api key or we have a valid user @@ -815,6 +816,11 @@ public class ItemsController : BaseJellyfinApiController [FromQuery] bool excludeActiveSessions = false) { var user = _userManager.GetUserById(userId); + if (user is null) + { + return NotFound(); + } + var parentIdGuid = parentId ?? Guid.Empty; var dtoOptions = new DtoOptions { Fields = fields } .AddClientFields(User) diff --git a/Jellyfin.Api/Controllers/LibraryController.cs b/Jellyfin.Api/Controllers/LibraryController.cs index 830f84849e..bf59febed8 100644 --- a/Jellyfin.Api/Controllers/LibraryController.cs +++ b/Jellyfin.Api/Controllers/LibraryController.cs @@ -9,6 +9,7 @@ using System.Threading.Tasks; using Jellyfin.Api.Attributes; using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; +using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; using Jellyfin.Api.Models.LibraryDtos; using Jellyfin.Data.Entities; @@ -95,7 +96,7 @@ public class LibraryController : BaseJellyfinApiController /// Item not found. /// A with the original file. [HttpGet("Items/{itemId}/File")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesFile("video/*", "audio/*")] @@ -116,7 +117,7 @@ public class LibraryController : BaseJellyfinApiController /// Critic reviews returned. /// The list of critic reviews. [HttpGet("Items/{itemId}/CriticReviews")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [Obsolete("This endpoint is obsolete.")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetCriticReviews() @@ -134,7 +135,7 @@ public class LibraryController : BaseJellyfinApiController /// Item not found. /// The item theme songs. [HttpGet("Items/{itemId}/ThemeSongs")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult GetThemeSongs( @@ -142,12 +143,13 @@ public class LibraryController : BaseJellyfinApiController [FromQuery] Guid? userId, [FromQuery] bool inheritFromParent = false) { - var user = userId is null || userId.Value.Equals(default) + userId = RequestHelpers.GetUserId(User, userId); + var user = userId.Value.Equals(default) ? null : _userManager.GetUserById(userId.Value); var item = itemId.Equals(default) - ? (userId is null || userId.Value.Equals(default) + ? (userId.Value.Equals(default) ? _libraryManager.RootFolder : _libraryManager.GetUserRootFolder()) : _libraryManager.GetItemById(itemId); @@ -200,7 +202,7 @@ public class LibraryController : BaseJellyfinApiController /// Item not found. /// The item theme videos. [HttpGet("Items/{itemId}/ThemeVideos")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult GetThemeVideos( @@ -208,12 +210,13 @@ public class LibraryController : BaseJellyfinApiController [FromQuery] Guid? userId, [FromQuery] bool inheritFromParent = false) { - var user = userId is null || userId.Value.Equals(default) + userId = RequestHelpers.GetUserId(User, userId); + var user = userId.Value.Equals(default) ? null : _userManager.GetUserById(userId.Value); var item = itemId.Equals(default) - ? (userId is null || userId.Value.Equals(default) + ? (userId.Value.Equals(default) ? _libraryManager.RootFolder : _libraryManager.GetUserRootFolder()) : _libraryManager.GetItemById(itemId); @@ -266,7 +269,7 @@ public class LibraryController : BaseJellyfinApiController /// Item not found. /// The item theme videos. [HttpGet("Items/{itemId}/ThemeMedia")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult GetThemeMedia( [FromRoute, Required] Guid itemId, @@ -283,6 +286,11 @@ public class LibraryController : BaseJellyfinApiController userId, inheritFromParent); + if (themeSongs.Result is NotFoundObjectResult || themeVideos.Result is NotFoundObjectResult) + { + return NotFound(); + } + return new AllThemeMediaResult { ThemeSongsResult = themeSongs?.Value, @@ -321,7 +329,7 @@ public class LibraryController : BaseJellyfinApiController /// Unauthorized access. /// A . [HttpDelete("Items/{itemId}")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] public ActionResult DeleteItem(Guid itemId) @@ -350,7 +358,7 @@ public class LibraryController : BaseJellyfinApiController /// Unauthorized access. /// A . [HttpDelete("Items")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] public ActionResult DeleteItems([FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids) @@ -392,13 +400,14 @@ public class LibraryController : BaseJellyfinApiController /// Item counts returned. /// Item counts. [HttpGet("Items/Counts")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult GetItemCounts( [FromQuery] Guid? userId, [FromQuery] bool? isFavorite) { - var user = userId is null || userId.Value.Equals(default) + userId = RequestHelpers.GetUserId(User, userId); + var user = userId.Value.Equals(default) ? null : _userManager.GetUserById(userId.Value); @@ -426,12 +435,13 @@ public class LibraryController : BaseJellyfinApiController /// Item not found. /// Item parents. [HttpGet("Items/{itemId}/Ancestors")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult> GetAncestors([FromRoute, Required] Guid itemId, [FromQuery] Guid? userId) { var item = _libraryManager.GetItemById(itemId); + userId = RequestHelpers.GetUserId(User, userId); if (item is null) { @@ -440,7 +450,7 @@ public class LibraryController : BaseJellyfinApiController var baseItemDtos = new List(); - var user = userId is null || userId.Value.Equals(default) + var user = userId.Value.Equals(default) ? null : _userManager.GetUserById(userId.Value); @@ -452,6 +462,10 @@ public class LibraryController : BaseJellyfinApiController if (user is not null) { parent = TranslateParentItem(parent, user); + if (parent is null) + { + break; + } } baseItemDtos.Add(_dtoService.GetBaseItemDto(parent, dtoOptions, user)); @@ -509,7 +523,7 @@ public class LibraryController : BaseJellyfinApiController /// A . [HttpPost("Library/Series/Added", Name = "PostAddedSeries")] [HttpPost("Library/Series/Updated")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult PostUpdatedSeries([FromQuery] string? tvdbId) { @@ -539,7 +553,7 @@ public class LibraryController : BaseJellyfinApiController /// A . [HttpPost("Library/Movies/Added", Name = "PostAddedMovies")] [HttpPost("Library/Movies/Updated")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult PostUpdatedMovies([FromQuery] string? tmdbId, [FromQuery] string? imdbId) { @@ -580,7 +594,7 @@ public class LibraryController : BaseJellyfinApiController /// Report success. /// A . [HttpPost("Library/Media/Updated")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult PostUpdatedMedia([FromBody, Required] MediaUpdateInfoDto dto) { @@ -657,7 +671,7 @@ public class LibraryController : BaseJellyfinApiController [HttpGet("Shows/{itemId}/Similar", Name = "GetSimilarShows")] [HttpGet("Movies/{itemId}/Similar", Name = "GetSimilarMovies")] [HttpGet("Trailers/{itemId}/Similar", Name = "GetSimilarTrailers")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetSimilarItems( [FromRoute, Required] Guid itemId, @@ -666,18 +680,24 @@ public class LibraryController : BaseJellyfinApiController [FromQuery] int? limit, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields) { + userId = RequestHelpers.GetUserId(User, userId); var item = itemId.Equals(default) - ? (userId is null || userId.Value.Equals(default) + ? (userId.Value.Equals(default) ? _libraryManager.RootFolder : _libraryManager.GetUserRootFolder()) : _libraryManager.GetItemById(itemId); + if (item is null) + { + return NotFound(); + } + if (item is Episode || (item is IItemByName && item is not MusicArtist)) { return new QueryResult(); } - var user = userId is null || userId.Value.Equals(default) + var user = userId.Value.Equals(default) ? null : _userManager.GetUserById(userId.Value); var dtoOptions = new DtoOptions { Fields = fields } @@ -802,32 +822,32 @@ public class LibraryController : BaseJellyfinApiController Type = type, MetadataFetchers = plugins - .Where(i => string.Equals(i.ItemType, type, StringComparison.OrdinalIgnoreCase)) - .SelectMany(i => i.Plugins.Where(p => p.Type == MetadataPluginType.MetadataFetcher)) - .Select(i => new LibraryOptionInfoDto - { - Name = i.Name, - DefaultEnabled = IsMetadataFetcherEnabledByDefault(i.Name, type, isNewLibrary) - }) - .DistinctBy(i => i.Name, StringComparer.OrdinalIgnoreCase) - .ToArray(), + .Where(i => string.Equals(i.ItemType, type, StringComparison.OrdinalIgnoreCase)) + .SelectMany(i => i.Plugins.Where(p => p.Type == MetadataPluginType.MetadataFetcher)) + .Select(i => new LibraryOptionInfoDto + { + Name = i.Name, + DefaultEnabled = IsMetadataFetcherEnabledByDefault(i.Name, type, isNewLibrary) + }) + .DistinctBy(i => i.Name, StringComparer.OrdinalIgnoreCase) + .ToArray(), ImageFetchers = plugins - .Where(i => string.Equals(i.ItemType, type, StringComparison.OrdinalIgnoreCase)) - .SelectMany(i => i.Plugins.Where(p => p.Type == MetadataPluginType.ImageFetcher)) - .Select(i => new LibraryOptionInfoDto - { - Name = i.Name, - DefaultEnabled = IsImageFetcherEnabledByDefault(i.Name, type, isNewLibrary) - }) - .DistinctBy(i => i.Name, StringComparer.OrdinalIgnoreCase) - .ToArray(), + .Where(i => string.Equals(i.ItemType, type, StringComparison.OrdinalIgnoreCase)) + .SelectMany(i => i.Plugins.Where(p => p.Type == MetadataPluginType.ImageFetcher)) + .Select(i => new LibraryOptionInfoDto + { + Name = i.Name, + DefaultEnabled = IsImageFetcherEnabledByDefault(i.Name, type, isNewLibrary) + }) + .DistinctBy(i => i.Name, StringComparer.OrdinalIgnoreCase) + .ToArray(), SupportedImageTypes = plugins - .Where(i => string.Equals(i.ItemType, type, StringComparison.OrdinalIgnoreCase)) - .SelectMany(i => i.SupportedImageTypes ?? Array.Empty()) - .Distinct() - .ToArray(), + .Where(i => string.Equals(i.ItemType, type, StringComparison.OrdinalIgnoreCase)) + .SelectMany(i => i.SupportedImageTypes ?? Array.Empty()) + .Distinct() + .ToArray(), DefaultImageOptions = defaultImageOptions ?? Array.Empty() }); @@ -920,13 +940,13 @@ public class LibraryController : BaseJellyfinApiController if (string.Equals(name, "TheMovieDb", StringComparison.OrdinalIgnoreCase)) { return !(string.Equals(type, "Season", StringComparison.OrdinalIgnoreCase) - || string.Equals(type, "Episode", StringComparison.OrdinalIgnoreCase) - || string.Equals(type, "MusicVideo", StringComparison.OrdinalIgnoreCase)); + || string.Equals(type, "Episode", StringComparison.OrdinalIgnoreCase) + || string.Equals(type, "MusicVideo", StringComparison.OrdinalIgnoreCase)); } return string.Equals(name, "TheTVDB", StringComparison.OrdinalIgnoreCase) - || string.Equals(name, "TheAudioDB", StringComparison.OrdinalIgnoreCase) - || string.Equals(name, "MusicBrainz", StringComparison.OrdinalIgnoreCase); + || string.Equals(name, "TheAudioDB", StringComparison.OrdinalIgnoreCase) + || string.Equals(name, "MusicBrainz", StringComparison.OrdinalIgnoreCase); } var metadataOptions = _serverConfigurationManager.Configuration.MetadataOptions @@ -934,7 +954,7 @@ public class LibraryController : BaseJellyfinApiController .ToArray(); return metadataOptions.Length == 0 - || metadataOptions.Any(i => !i.DisabledMetadataFetchers.Contains(name, StringComparison.OrdinalIgnoreCase)); + || metadataOptions.Any(i => !i.DisabledMetadataFetchers.Contains(name, StringComparison.OrdinalIgnoreCase)); } private bool IsImageFetcherEnabledByDefault(string name, string type, bool isNewLibrary) diff --git a/Jellyfin.Api/Controllers/LiveTvController.cs b/Jellyfin.Api/Controllers/LiveTvController.cs index 21b4243464..96fc91f93c 100644 --- a/Jellyfin.Api/Controllers/LiveTvController.cs +++ b/Jellyfin.Api/Controllers/LiveTvController.cs @@ -17,14 +17,12 @@ using Jellyfin.Api.ModelBinders; using Jellyfin.Api.Models.LiveTvDtos; using Jellyfin.Data.Enums; using MediaBrowser.Common.Configuration; -using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.LiveTv; -using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Session; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; @@ -95,7 +93,7 @@ public class LiveTvController : BaseJellyfinApiController /// [HttpGet("Info")] [ProducesResponseType(StatusCodes.Status200OK)] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize(Policy = Policies.LiveTvAccess)] public ActionResult GetLiveTvInfo() { return _liveTvManager.GetLiveTvInfo(CancellationToken.None); @@ -131,7 +129,7 @@ public class LiveTvController : BaseJellyfinApiController /// [HttpGet("Channels")] [ProducesResponseType(StatusCodes.Status200OK)] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize(Policy = Policies.LiveTvAccess)] public ActionResult> GetLiveTvChannels( [FromQuery] ChannelType? type, [FromQuery] Guid? userId, @@ -155,6 +153,7 @@ public class LiveTvController : BaseJellyfinApiController [FromQuery] bool enableFavoriteSorting = false, [FromQuery] bool addCurrentProgram = true) { + userId = RequestHelpers.GetUserId(User, userId); var dtoOptions = new DtoOptions { Fields = fields } .AddClientFields(User) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); @@ -163,7 +162,7 @@ public class LiveTvController : BaseJellyfinApiController new LiveTvChannelQuery { ChannelType = type, - UserId = userId ?? Guid.Empty, + UserId = userId.Value, StartIndex = startIndex, Limit = limit, IsFavorite = isFavorite, @@ -182,7 +181,7 @@ public class LiveTvController : BaseJellyfinApiController dtoOptions, CancellationToken.None); - var user = userId is null || userId.Value.Equals(default) + var user = userId.Value.Equals(default) ? null : _userManager.GetUserById(userId.Value); @@ -210,10 +209,11 @@ public class LiveTvController : BaseJellyfinApiController /// An containing the live tv channel. [HttpGet("Channels/{channelId}")] [ProducesResponseType(StatusCodes.Status200OK)] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize(Policy = Policies.LiveTvAccess)] public ActionResult GetChannel([FromRoute, Required] Guid channelId, [FromQuery] Guid? userId) { - var user = userId is null || userId.Value.Equals(default) + userId = RequestHelpers.GetUserId(User, userId); + var user = userId.Value.Equals(default) ? null : _userManager.GetUserById(userId.Value); var item = channelId.Equals(default) @@ -251,7 +251,7 @@ public class LiveTvController : BaseJellyfinApiController /// An containing the live tv recordings. [HttpGet("Recordings")] [ProducesResponseType(StatusCodes.Status200OK)] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize(Policy = Policies.LiveTvAccess)] public ActionResult> GetRecordings( [FromQuery] string? channelId, [FromQuery] Guid? userId, @@ -273,6 +273,7 @@ public class LiveTvController : BaseJellyfinApiController [FromQuery] bool? isLibraryItem, [FromQuery] bool enableTotalRecordCount = true) { + userId = RequestHelpers.GetUserId(User, userId); var dtoOptions = new DtoOptions { Fields = fields } .AddClientFields(User) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); @@ -281,7 +282,7 @@ public class LiveTvController : BaseJellyfinApiController new RecordingQuery { ChannelId = channelId, - UserId = userId ?? Guid.Empty, + UserId = userId.Value, StartIndex = startIndex, Limit = limit, Status = status, @@ -322,7 +323,7 @@ public class LiveTvController : BaseJellyfinApiController /// An containing the live tv recordings. [HttpGet("Recordings/Series")] [ProducesResponseType(StatusCodes.Status200OK)] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize(Policy = Policies.LiveTvAccess)] [Obsolete("This endpoint is obsolete.")] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "channelId", Justification = "Imported from ServiceStack")] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Imported from ServiceStack")] @@ -365,7 +366,7 @@ public class LiveTvController : BaseJellyfinApiController /// An containing the recording groups. [HttpGet("Recordings/Groups")] [ProducesResponseType(StatusCodes.Status200OK)] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize(Policy = Policies.LiveTvAccess)] [Obsolete("This endpoint is obsolete.")] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Imported from ServiceStack")] public ActionResult> GetRecordingGroups([FromQuery] Guid? userId) @@ -381,10 +382,11 @@ public class LiveTvController : BaseJellyfinApiController /// An containing the recording folders. [HttpGet("Recordings/Folders")] [ProducesResponseType(StatusCodes.Status200OK)] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize(Policy = Policies.LiveTvAccess)] public ActionResult> GetRecordingFolders([FromQuery] Guid? userId) { - var user = userId is null || userId.Value.Equals(default) + userId = RequestHelpers.GetUserId(User, userId); + var user = userId.Value.Equals(default) ? null : _userManager.GetUserById(userId.Value); var folders = _liveTvManager.GetRecordingFolders(user); @@ -403,10 +405,11 @@ public class LiveTvController : BaseJellyfinApiController /// An containing the live tv recording. [HttpGet("Recordings/{recordingId}")] [ProducesResponseType(StatusCodes.Status200OK)] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize(Policy = Policies.LiveTvAccess)] public ActionResult GetRecording([FromRoute, Required] Guid recordingId, [FromQuery] Guid? userId) { - var user = userId is null || userId.Value.Equals(default) + userId = RequestHelpers.GetUserId(User, userId); + var user = userId.Value.Equals(default) ? null : _userManager.GetUserById(userId.Value); var item = recordingId.Equals(default) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(recordingId); @@ -425,10 +428,9 @@ public class LiveTvController : BaseJellyfinApiController /// A . [HttpPost("Tuners/{tunerId}/Reset")] [ProducesResponseType(StatusCodes.Status204NoContent)] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize(Policy = Policies.LiveTvManagement)] public async Task ResetTuner([FromRoute, Required] string tunerId) { - await AssertUserCanManageLiveTv().ConfigureAwait(false); await _liveTvManager.ResetTuner(tunerId, CancellationToken.None).ConfigureAwait(false); return NoContent(); } @@ -443,7 +445,7 @@ public class LiveTvController : BaseJellyfinApiController /// [HttpGet("Timers/{timerId}")] [ProducesResponseType(StatusCodes.Status200OK)] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize(Policy = Policies.LiveTvAccess)] public async Task> GetTimer([FromRoute, Required] string timerId) { return await _liveTvManager.GetTimer(timerId, CancellationToken.None).ConfigureAwait(false); @@ -459,7 +461,7 @@ public class LiveTvController : BaseJellyfinApiController /// [HttpGet("Timers/Defaults")] [ProducesResponseType(StatusCodes.Status200OK)] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize(Policy = Policies.LiveTvAccess)] public async Task> GetDefaultTimer([FromQuery] string? programId) { return string.IsNullOrEmpty(programId) @@ -479,7 +481,7 @@ public class LiveTvController : BaseJellyfinApiController /// [HttpGet("Timers")] [ProducesResponseType(StatusCodes.Status200OK)] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize(Policy = Policies.LiveTvAccess)] public async Task>> GetTimers( [FromQuery] string? channelId, [FromQuery] string? seriesTimerId, @@ -533,7 +535,7 @@ public class LiveTvController : BaseJellyfinApiController /// [HttpGet("Programs")] [ProducesResponseType(StatusCodes.Status200OK)] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize(Policy = Policies.LiveTvAccess)] public async Task>> GetLiveTvPrograms( [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] channelIds, [FromQuery] Guid? userId, @@ -563,7 +565,8 @@ public class LiveTvController : BaseJellyfinApiController [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, [FromQuery] bool enableTotalRecordCount = true) { - var user = userId is null || userId.Value.Equals(default) + userId = RequestHelpers.GetUserId(User, userId); + var user = userId.Value.Equals(default) ? null : _userManager.GetUserById(userId.Value); @@ -616,7 +619,7 @@ public class LiveTvController : BaseJellyfinApiController /// [HttpPost("Programs")] [ProducesResponseType(StatusCodes.Status200OK)] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize(Policy = Policies.LiveTvAccess)] public async Task>> GetPrograms([FromBody] GetProgramsDto body) { var user = body.UserId.Equals(default) ? null : _userManager.GetUserById(body.UserId); @@ -682,7 +685,7 @@ public class LiveTvController : BaseJellyfinApiController /// Recommended epgs returned. /// A containing the queryresult of recommended epgs. [HttpGet("Programs/Recommended")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize(Policy = Policies.LiveTvAccess)] [ProducesResponseType(StatusCodes.Status200OK)] public async Task>> GetRecommendedPrograms( [FromQuery] Guid? userId, @@ -702,7 +705,8 @@ public class LiveTvController : BaseJellyfinApiController [FromQuery] bool? enableUserData, [FromQuery] bool enableTotalRecordCount = true) { - var user = userId is null || userId.Value.Equals(default) + userId = RequestHelpers.GetUserId(User, userId); + var user = userId.Value.Equals(default) ? null : _userManager.GetUserById(userId.Value); @@ -734,13 +738,14 @@ public class LiveTvController : BaseJellyfinApiController /// Program returned. /// An containing the livetv program. [HttpGet("Programs/{programId}")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize(Policy = Policies.LiveTvAccess)] [ProducesResponseType(StatusCodes.Status200OK)] public async Task> GetProgram( [FromRoute, Required] string programId, [FromQuery] Guid? userId) { - var user = userId is null || userId.Value.Equals(default) + userId = RequestHelpers.GetUserId(User, userId); + var user = userId.Value.Equals(default) ? null : _userManager.GetUserById(userId.Value); @@ -755,13 +760,11 @@ public class LiveTvController : BaseJellyfinApiController /// Item not found. /// A on success, or a if item not found. [HttpDelete("Recordings/{recordingId}")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize(Policy = Policies.LiveTvManagement)] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task DeleteRecording([FromRoute, Required] Guid recordingId) + public ActionResult DeleteRecording([FromRoute, Required] Guid recordingId) { - await AssertUserCanManageLiveTv().ConfigureAwait(false); - var item = _libraryManager.GetItemById(recordingId); if (item is null) { @@ -783,11 +786,10 @@ public class LiveTvController : BaseJellyfinApiController /// Timer deleted. /// A . [HttpDelete("Timers/{timerId}")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize(Policy = Policies.LiveTvManagement)] [ProducesResponseType(StatusCodes.Status204NoContent)] public async Task CancelTimer([FromRoute, Required] string timerId) { - await AssertUserCanManageLiveTv().ConfigureAwait(false); await _liveTvManager.CancelTimer(timerId).ConfigureAwait(false); return NoContent(); } @@ -800,12 +802,11 @@ public class LiveTvController : BaseJellyfinApiController /// Timer updated. /// A . [HttpPost("Timers/{timerId}")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize(Policy = Policies.LiveTvManagement)] [ProducesResponseType(StatusCodes.Status204NoContent)] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "timerId", Justification = "Imported from ServiceStack")] public async Task UpdateTimer([FromRoute, Required] string timerId, [FromBody] TimerInfoDto timerInfo) { - await AssertUserCanManageLiveTv().ConfigureAwait(false); await _liveTvManager.UpdateTimer(timerInfo, CancellationToken.None).ConfigureAwait(false); return NoContent(); } @@ -817,11 +818,10 @@ public class LiveTvController : BaseJellyfinApiController /// Timer created. /// A . [HttpPost("Timers")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize(Policy = Policies.LiveTvManagement)] [ProducesResponseType(StatusCodes.Status204NoContent)] public async Task CreateTimer([FromBody] TimerInfoDto timerInfo) { - await AssertUserCanManageLiveTv().ConfigureAwait(false); await _liveTvManager.CreateTimer(timerInfo, CancellationToken.None).ConfigureAwait(false); return NoContent(); } @@ -834,7 +834,7 @@ public class LiveTvController : BaseJellyfinApiController /// Series timer not found. /// A on success, or a if timer not found. [HttpGet("SeriesTimers/{timerId}")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize(Policy = Policies.LiveTvAccess)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task> GetSeriesTimer([FromRoute, Required] string timerId) @@ -856,7 +856,7 @@ public class LiveTvController : BaseJellyfinApiController /// Timers returned. /// An of live tv series timers. [HttpGet("SeriesTimers")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize(Policy = Policies.LiveTvAccess)] [ProducesResponseType(StatusCodes.Status200OK)] public async Task>> GetSeriesTimers([FromQuery] string? sortBy, [FromQuery] SortOrder? sortOrder) { @@ -876,11 +876,10 @@ public class LiveTvController : BaseJellyfinApiController /// Timer cancelled. /// A . [HttpDelete("SeriesTimers/{timerId}")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize(Policy = Policies.LiveTvManagement)] [ProducesResponseType(StatusCodes.Status204NoContent)] public async Task CancelSeriesTimer([FromRoute, Required] string timerId) { - await AssertUserCanManageLiveTv().ConfigureAwait(false); await _liveTvManager.CancelSeriesTimer(timerId).ConfigureAwait(false); return NoContent(); } @@ -893,12 +892,11 @@ public class LiveTvController : BaseJellyfinApiController /// Series timer updated. /// A . [HttpPost("SeriesTimers/{timerId}")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize(Policy = Policies.LiveTvManagement)] [ProducesResponseType(StatusCodes.Status204NoContent)] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "timerId", Justification = "Imported from ServiceStack")] public async Task UpdateSeriesTimer([FromRoute, Required] string timerId, [FromBody] SeriesTimerInfoDto seriesTimerInfo) { - await AssertUserCanManageLiveTv().ConfigureAwait(false); await _liveTvManager.UpdateSeriesTimer(seriesTimerInfo, CancellationToken.None).ConfigureAwait(false); return NoContent(); } @@ -910,11 +908,10 @@ public class LiveTvController : BaseJellyfinApiController /// Series timer info created. /// A . [HttpPost("SeriesTimers")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize(Policy = Policies.LiveTvManagement)] [ProducesResponseType(StatusCodes.Status204NoContent)] public async Task CreateSeriesTimer([FromBody] SeriesTimerInfoDto seriesTimerInfo) { - await AssertUserCanManageLiveTv().ConfigureAwait(false); await _liveTvManager.CreateSeriesTimer(seriesTimerInfo, CancellationToken.None).ConfigureAwait(false); return NoContent(); } @@ -925,7 +922,7 @@ public class LiveTvController : BaseJellyfinApiController /// Group id. /// A . [HttpGet("Recordings/Groups/{groupId}")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize(Policy = Policies.LiveTvAccess)] [ProducesResponseType(StatusCodes.Status404NotFound)] [Obsolete("This endpoint is obsolete.")] public ActionResult GetRecordingGroup([FromRoute, Required] Guid groupId) @@ -939,7 +936,7 @@ public class LiveTvController : BaseJellyfinApiController /// Guid info returned. /// An containing the guide info. [HttpGet("GuideInfo")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize(Policy = Policies.LiveTvAccess)] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult GetGuideInfo() { @@ -953,7 +950,7 @@ public class LiveTvController : BaseJellyfinApiController /// Created tuner host returned. /// A containing the created tuner host. [HttpPost("TunerHosts")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize(Policy = Policies.LiveTvManagement)] [ProducesResponseType(StatusCodes.Status200OK)] public async Task> AddTunerHost([FromBody] TunerHostInfo tunerHostInfo) { @@ -967,7 +964,7 @@ public class LiveTvController : BaseJellyfinApiController /// Tuner host deleted. /// A . [HttpDelete("TunerHosts")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize(Policy = Policies.LiveTvManagement)] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult DeleteTunerHost([FromQuery] string? id) { @@ -983,7 +980,7 @@ public class LiveTvController : BaseJellyfinApiController /// Default listings provider info returned. /// An containing the default listings provider info. [HttpGet("ListingProviders/Default")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize(Policy = Policies.LiveTvAccess)] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult GetDefaultListingProvider() { @@ -1000,7 +997,7 @@ public class LiveTvController : BaseJellyfinApiController /// Created listings provider returned. /// A containing the created listings provider. [HttpPost("ListingProviders")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize(Policy = Policies.LiveTvManagement)] [ProducesResponseType(StatusCodes.Status200OK)] [SuppressMessage("Microsoft.Performance", "CA5350:RemoveSha1", MessageId = "AddListingProvider", Justification = "Imported from ServiceStack")] public async Task> AddListingProvider( @@ -1026,7 +1023,7 @@ public class LiveTvController : BaseJellyfinApiController /// Listing provider deleted. /// A . [HttpDelete("ListingProviders")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize(Policy = Policies.LiveTvManagement)] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult DeleteListingProvider([FromQuery] string? id) { @@ -1044,7 +1041,7 @@ public class LiveTvController : BaseJellyfinApiController /// Available lineups returned. /// A containing the available lineups. [HttpGet("ListingProviders/Lineups")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize(Policy = Policies.LiveTvAccess)] [ProducesResponseType(StatusCodes.Status200OK)] public async Task>> GetLineups( [FromQuery] string? id, @@ -1061,7 +1058,7 @@ public class LiveTvController : BaseJellyfinApiController /// Available countries returned. /// A containing the available countries. [HttpGet("ListingProviders/SchedulesDirect/Countries")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize(Policy = Policies.LiveTvAccess)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesFile(MediaTypeNames.Application.Json)] public async Task GetSchedulesDirectCountries() @@ -1082,7 +1079,7 @@ public class LiveTvController : BaseJellyfinApiController /// Channel mapping options returned. /// An containing the channel mapping options. [HttpGet("ChannelMappingOptions")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize(Policy = Policies.LiveTvAccess)] [ProducesResponseType(StatusCodes.Status200OK)] public async Task> GetChannelMappingOptions([FromQuery] string? providerId) { @@ -1120,7 +1117,7 @@ public class LiveTvController : BaseJellyfinApiController /// Created channel mapping returned. /// An containing the created channel mapping. [HttpPost("ChannelMappings")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize(Policy = Policies.LiveTvManagement)] [ProducesResponseType(StatusCodes.Status200OK)] public async Task> SetChannelMapping([FromBody, Required] SetChannelMappingDto setChannelMappingDto) { @@ -1133,7 +1130,7 @@ public class LiveTvController : BaseJellyfinApiController /// Tuner host types returned. /// An containing the tuner host types. [HttpGet("TunerHosts/Types")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize(Policy = Policies.LiveTvAccess)] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetTunerHostTypes() { @@ -1148,7 +1145,7 @@ public class LiveTvController : BaseJellyfinApiController /// An containing the tuners. [HttpGet("Tuners/Discvover", Name = "DiscvoverTuners")] [HttpGet("Tuners/Discover")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize(Policy = Policies.LiveTvManagement)] [ProducesResponseType(StatusCodes.Status200OK)] public async Task>> DiscoverTuners([FromQuery] bool newDevicesOnly = false) { @@ -1208,26 +1205,4 @@ public class LiveTvController : BaseJellyfinApiController var liveStream = new ProgressiveFileStream(liveStreamInfo.GetStream()); return new FileStreamResult(liveStream, MimeTypes.GetMimeType("file." + container)); } - - private async Task AssertUserCanManageLiveTv() - { - var user = _userManager.GetUserById(User.GetUserId()); - var session = await _sessionManager.LogSessionActivity( - User.GetClient(), - User.GetVersion(), - User.GetDeviceId(), - User.GetDevice(), - HttpContext.GetNormalizedRemoteIp().ToString(), - user).ConfigureAwait(false); - - if (session.UserId.Equals(default)) - { - throw new SecurityException("Anonymous live tv management is not allowed."); - } - - if (!user.HasPermission(PermissionKind.EnableLiveTvManagement)) - { - throw new SecurityException("The current user does not have permission to manage live tv."); - } - } } diff --git a/Jellyfin.Api/Controllers/MediaInfoController.cs b/Jellyfin.Api/Controllers/MediaInfoController.cs index eee7df3af8..da24616ff3 100644 --- a/Jellyfin.Api/Controllers/MediaInfoController.cs +++ b/Jellyfin.Api/Controllers/MediaInfoController.cs @@ -5,7 +5,6 @@ using System.Linq; using System.Net.Mime; using System.Threading.Tasks; using Jellyfin.Api.Attributes; -using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; using Jellyfin.Api.Models.MediaInfoDtos; @@ -25,7 +24,7 @@ namespace Jellyfin.Api.Controllers; /// The media info controller. /// [Route("")] -[Authorize(Policy = Policies.DefaultAuthorization)] +[Authorize] public class MediaInfoController : BaseJellyfinApiController { private readonly IMediaSourceManager _mediaSourceManager; @@ -133,6 +132,7 @@ public class MediaInfoController : BaseJellyfinApiController // Copy params from posted body // TODO clean up when breaking API compatibility. userId ??= playbackInfoDto?.UserId; + userId = RequestHelpers.GetUserId(User, userId); maxStreamingBitrate ??= playbackInfoDto?.MaxStreamingBitrate; startTimeTicks ??= playbackInfoDto?.StartTimeTicks; audioStreamIndex ??= playbackInfoDto?.AudioStreamIndex; @@ -254,10 +254,12 @@ public class MediaInfoController : BaseJellyfinApiController [FromQuery] bool? enableDirectPlay, [FromQuery] bool? enableDirectStream) { + userId ??= openLiveStreamDto?.UserId; + userId = RequestHelpers.GetUserId(User, userId); var request = new LiveStreamRequest { OpenToken = openToken ?? openLiveStreamDto?.OpenToken, - UserId = userId ?? openLiveStreamDto?.UserId ?? Guid.Empty, + UserId = userId.Value, PlaySessionId = playSessionId ?? openLiveStreamDto?.PlaySessionId, MaxStreamingBitrate = maxStreamingBitrate ?? openLiveStreamDto?.MaxStreamingBitrate, StartTimeTicks = startTimeTicks ?? openLiveStreamDto?.StartTimeTicks, diff --git a/Jellyfin.Api/Controllers/MoviesController.cs b/Jellyfin.Api/Controllers/MoviesController.cs index 4c30dd2b37..e1145481fa 100644 --- a/Jellyfin.Api/Controllers/MoviesController.cs +++ b/Jellyfin.Api/Controllers/MoviesController.cs @@ -2,8 +2,8 @@ using System; using System.Collections.Generic; using System.Globalization; using System.Linq; -using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; +using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; @@ -23,7 +23,7 @@ namespace Jellyfin.Api.Controllers; /// /// Movies controller. /// -[Authorize(Policy = Policies.DefaultAuthorization)] +[Authorize] public class MoviesController : BaseJellyfinApiController { private readonly IUserManager _userManager; @@ -68,7 +68,8 @@ public class MoviesController : BaseJellyfinApiController [FromQuery] int categoryLimit = 5, [FromQuery] int itemLimit = 8) { - var user = userId is null || userId.Value.Equals(default) + userId = RequestHelpers.GetUserId(User, userId); + var user = userId.Value.Equals(default) ? null : _userManager.GetUserById(userId.Value); var dtoOptions = new DtoOptions { Fields = fields } diff --git a/Jellyfin.Api/Controllers/MusicGenresController.cs b/Jellyfin.Api/Controllers/MusicGenresController.cs index 302f138ebc..435457af67 100644 --- a/Jellyfin.Api/Controllers/MusicGenresController.cs +++ b/Jellyfin.Api/Controllers/MusicGenresController.cs @@ -1,7 +1,6 @@ using System; using System.ComponentModel.DataAnnotations; using System.Linq; -using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; @@ -23,7 +22,7 @@ namespace Jellyfin.Api.Controllers; /// /// The music genres controller. /// -[Authorize(Policy = Policies.DefaultAuthorization)] +[Authorize] public class MusicGenresController : BaseJellyfinApiController { private readonly ILibraryManager _libraryManager; @@ -91,11 +90,12 @@ public class MusicGenresController : BaseJellyfinApiController [FromQuery] bool? enableImages = true, [FromQuery] bool enableTotalRecordCount = true) { + userId = RequestHelpers.GetUserId(User, userId); var dtoOptions = new DtoOptions { Fields = fields } .AddClientFields(User) .AddAdditionalDtoOptions(enableImages, false, imageTypeLimit, enableImageTypes); - User? user = userId is null || userId.Value.Equals(default) + User? user = userId.Value.Equals(default) ? null : _userManager.GetUserById(userId.Value); @@ -145,6 +145,7 @@ public class MusicGenresController : BaseJellyfinApiController [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult GetMusicGenre([FromRoute, Required] string genreName, [FromQuery] Guid? userId) { + userId = RequestHelpers.GetUserId(User, userId); var dtoOptions = new DtoOptions().AddClientFields(User); MusicGenre? item; @@ -158,7 +159,12 @@ public class MusicGenresController : BaseJellyfinApiController item = _libraryManager.GetMusicGenre(genreName); } - if (userId.HasValue && !userId.Value.Equals(default)) + if (item is null) + { + return NotFound(); + } + + if (!userId.Value.Equals(default)) { var user = _userManager.GetUserById(userId.Value); diff --git a/Jellyfin.Api/Controllers/PackageController.cs b/Jellyfin.Api/Controllers/PackageController.cs index 3cb3caadb3..0ba5e995fb 100644 --- a/Jellyfin.Api/Controllers/PackageController.cs +++ b/Jellyfin.Api/Controllers/PackageController.cs @@ -17,7 +17,7 @@ namespace Jellyfin.Api.Controllers; /// Package Controller. /// [Route("")] -[Authorize(Policy = Policies.DefaultAuthorization)] +[Authorize] public class PackageController : BaseJellyfinApiController { private readonly IInstallationManager _installationManager; diff --git a/Jellyfin.Api/Controllers/PersonsController.cs b/Jellyfin.Api/Controllers/PersonsController.cs index 9fb6da5276..b4c6f490a0 100644 --- a/Jellyfin.Api/Controllers/PersonsController.cs +++ b/Jellyfin.Api/Controllers/PersonsController.cs @@ -1,8 +1,8 @@ using System; using System.ComponentModel.DataAnnotations; using System.Linq; -using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; +using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; using Jellyfin.Data.Entities; using MediaBrowser.Controller.Dto; @@ -20,7 +20,7 @@ namespace Jellyfin.Api.Controllers; /// /// Persons controller. /// -[Authorize(Policy = Policies.DefaultAuthorization)] +[Authorize] public class PersonsController : BaseJellyfinApiController { private readonly ILibraryManager _libraryManager; @@ -78,11 +78,12 @@ public class PersonsController : BaseJellyfinApiController [FromQuery] Guid? userId, [FromQuery] bool? enableImages = true) { + userId = RequestHelpers.GetUserId(User, userId); var dtoOptions = new DtoOptions { Fields = fields } .AddClientFields(User) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); - User? user = userId is null || userId.Value.Equals(default) + User? user = userId.Value.Equals(default) ? null : _userManager.GetUserById(userId.Value); @@ -118,6 +119,7 @@ public class PersonsController : BaseJellyfinApiController [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult GetPerson([FromRoute, Required] string name, [FromQuery] Guid? userId) { + userId = RequestHelpers.GetUserId(User, userId); var dtoOptions = new DtoOptions() .AddClientFields(User); @@ -127,7 +129,7 @@ public class PersonsController : BaseJellyfinApiController return NotFound(); } - if (userId.HasValue && !userId.Value.Equals(default)) + if (!userId.Value.Equals(default)) { var user = _userManager.GetUserById(userId.Value); return _dtoService.GetBaseItemDto(item, dtoOptions, user); diff --git a/Jellyfin.Api/Controllers/PlaylistsController.cs b/Jellyfin.Api/Controllers/PlaylistsController.cs index 11e5893017..c6dbea5e22 100644 --- a/Jellyfin.Api/Controllers/PlaylistsController.cs +++ b/Jellyfin.Api/Controllers/PlaylistsController.cs @@ -4,8 +4,8 @@ using System.ComponentModel.DataAnnotations; using System.Linq; using System.Threading.Tasks; using Jellyfin.Api.Attributes; -using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; +using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; using Jellyfin.Api.Models.PlaylistDtos; using MediaBrowser.Controller.Dto; @@ -25,7 +25,7 @@ namespace Jellyfin.Api.Controllers; /// /// Playlists controller. /// -[Authorize(Policy = Policies.DefaultAuthorization)] +[Authorize] public class PlaylistsController : BaseJellyfinApiController { private readonly IPlaylistManager _playlistManager; @@ -82,11 +82,13 @@ public class PlaylistsController : BaseJellyfinApiController ids = createPlaylistRequest?.Ids ?? Array.Empty(); } + userId ??= createPlaylistRequest?.UserId ?? default; + userId = RequestHelpers.GetUserId(User, userId); var result = await _playlistManager.CreatePlaylist(new PlaylistCreationRequest { Name = name ?? createPlaylistRequest?.Name, ItemIdList = ids, - UserId = userId ?? createPlaylistRequest?.UserId ?? default, + UserId = userId.Value, MediaType = mediaType ?? createPlaylistRequest?.MediaType }).ConfigureAwait(false); @@ -108,7 +110,8 @@ public class PlaylistsController : BaseJellyfinApiController [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids, [FromQuery] Guid? userId) { - await _playlistManager.AddToPlaylistAsync(playlistId, ids, userId ?? Guid.Empty).ConfigureAwait(false); + userId = RequestHelpers.GetUserId(User, userId); + await _playlistManager.AddToPlaylistAsync(playlistId, ids, userId.Value).ConfigureAwait(false); return NoContent(); } diff --git a/Jellyfin.Api/Controllers/PlaystateController.cs b/Jellyfin.Api/Controllers/PlaystateController.cs index 18d6ebf1e0..8ad553bcb8 100644 --- a/Jellyfin.Api/Controllers/PlaystateController.cs +++ b/Jellyfin.Api/Controllers/PlaystateController.cs @@ -2,7 +2,6 @@ using System; using System.ComponentModel.DataAnnotations; using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; -using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; @@ -23,7 +22,7 @@ namespace Jellyfin.Api.Controllers; /// Playstate controller. /// [Route("")] -[Authorize(Policy = Policies.DefaultAuthorization)] +[Authorize] public class PlaystateController : BaseJellyfinApiController { private readonly IUserManager _userManager; @@ -77,6 +76,11 @@ public class PlaystateController : BaseJellyfinApiController [FromQuery, ModelBinder(typeof(LegacyDateTimeModelBinder))] DateTime? datePlayed) { var user = _userManager.GetUserById(userId); + if (user is null) + { + return NotFound(); + } + var session = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); var item = _libraryManager.GetItemById(itemId); @@ -89,6 +93,11 @@ public class PlaystateController : BaseJellyfinApiController foreach (var additionalUserInfo in session.AdditionalUsers) { var additionalUser = _userManager.GetUserById(additionalUserInfo.UserId); + if (additionalUser is null) + { + return NotFound(); + } + UpdatePlayedStatus(additionalUser, item, true, datePlayed); } @@ -109,6 +118,11 @@ public class PlaystateController : BaseJellyfinApiController public async Task> MarkUnplayedItem([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId) { var user = _userManager.GetUserById(userId); + if (user is null) + { + return NotFound(); + } + var session = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); var item = _libraryManager.GetItemById(itemId); @@ -121,6 +135,11 @@ public class PlaystateController : BaseJellyfinApiController foreach (var additionalUserInfo in session.AdditionalUsers) { var additionalUser = _userManager.GetUserById(additionalUserInfo.UserId); + if (additionalUser is null) + { + return NotFound(); + } + UpdatePlayedStatus(additionalUser, item, false, null); } diff --git a/Jellyfin.Api/Controllers/PluginsController.cs b/Jellyfin.Api/Controllers/PluginsController.cs index 5a037d7a6b..4726cf0663 100644 --- a/Jellyfin.Api/Controllers/PluginsController.cs +++ b/Jellyfin.Api/Controllers/PluginsController.cs @@ -21,7 +21,7 @@ namespace Jellyfin.Api.Controllers; /// /// Plugins controller. /// -[Authorize(Policy = Policies.DefaultAuthorization)] +[Authorize] public class PluginsController : BaseJellyfinApiController { private readonly IInstallationManager _installationManager; diff --git a/Jellyfin.Api/Controllers/QuickConnectController.cs b/Jellyfin.Api/Controllers/QuickConnectController.cs index a58e85b2b2..d7e54b5b6a 100644 --- a/Jellyfin.Api/Controllers/QuickConnectController.cs +++ b/Jellyfin.Api/Controllers/QuickConnectController.cs @@ -3,6 +3,7 @@ using System.ComponentModel.DataAnnotations; using System.Threading.Tasks; using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; +using Jellyfin.Api.Helpers; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Authentication; using MediaBrowser.Controller.Net; @@ -111,22 +112,16 @@ public class QuickConnectController : BaseJellyfinApiController /// Unknown user id. /// Boolean indicating if the authorization was successful. [HttpPost("Authorize")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] public async Task> AuthorizeQuickConnect([FromQuery, Required] string code, [FromQuery] Guid? userId = null) { - var currentUserId = User.GetUserId(); - var actualUserId = userId ?? currentUserId; - - if (actualUserId.Equals(default) || (!userId.Equals(currentUserId) && !User.IsInRole(UserRoles.Administrator))) - { - return Forbid("Unknown user id"); - } + userId = RequestHelpers.GetUserId(User, userId); try { - return await _quickConnect.AuthorizeRequest(actualUserId, code).ConfigureAwait(false); + return await _quickConnect.AuthorizeRequest(userId.Value, code).ConfigureAwait(false); } catch (AuthenticationException) { diff --git a/Jellyfin.Api/Controllers/RemoteImageController.cs b/Jellyfin.Api/Controllers/RemoteImageController.cs index 445c5594f8..5c77db2407 100644 --- a/Jellyfin.Api/Controllers/RemoteImageController.cs +++ b/Jellyfin.Api/Controllers/RemoteImageController.cs @@ -56,7 +56,7 @@ public class RemoteImageController : BaseJellyfinApiController /// Item not found. /// Remote Image Result. [HttpGet("Items/{itemId}/RemoteImages")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task> GetRemoteImages( @@ -121,7 +121,7 @@ public class RemoteImageController : BaseJellyfinApiController /// Item not found. /// List of remote image providers. [HttpGet("Items/{itemId}/RemoteImages/Providers")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult> GetRemoteImageProviders([FromRoute, Required] Guid itemId) diff --git a/Jellyfin.Api/Controllers/SearchController.cs b/Jellyfin.Api/Controllers/SearchController.cs index 46b4920cad..f638c31c39 100644 --- a/Jellyfin.Api/Controllers/SearchController.cs +++ b/Jellyfin.Api/Controllers/SearchController.cs @@ -4,6 +4,7 @@ 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 Jellyfin.Extensions; @@ -26,7 +27,7 @@ namespace Jellyfin.Api.Controllers; /// Search controller. /// [Route("Search/Hints")] -[Authorize(Policy = Policies.DefaultAuthorization)] +[Authorize] public class SearchController : BaseJellyfinApiController { private readonly ISearchEngine _searchEngine; @@ -99,6 +100,7 @@ public class SearchController : BaseJellyfinApiController [FromQuery] bool includeStudios = true, [FromQuery] bool includeArtists = true) { + userId = RequestHelpers.GetUserId(User, userId); var result = _searchEngine.GetSearchHints(new SearchQuery { Limit = limit, @@ -109,7 +111,7 @@ public class SearchController : BaseJellyfinApiController IncludePeople = includePeople, IncludeStudios = includeStudios, StartIndex = startIndex, - UserId = userId ?? Guid.Empty, + UserId = userId.Value, IncludeItemTypes = includeItemTypes, ExcludeItemTypes = excludeItemTypes, MediaTypes = mediaTypes, diff --git a/Jellyfin.Api/Controllers/SessionController.cs b/Jellyfin.Api/Controllers/SessionController.cs index ef33644785..e93456de66 100644 --- a/Jellyfin.Api/Controllers/SessionController.cs +++ b/Jellyfin.Api/Controllers/SessionController.cs @@ -56,7 +56,7 @@ public class SessionController : BaseJellyfinApiController /// List of sessions returned. /// An with the available sessions. [HttpGet("Sessions")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetSessions( [FromQuery] Guid? controllableByUserId, @@ -75,6 +75,10 @@ public class SessionController : BaseJellyfinApiController result = result.Where(i => i.SupportsRemoteControl); var user = _userManager.GetUserById(controllableByUserId.Value); + if (user is null) + { + return NotFound(); + } if (!user.HasPermission(PermissionKind.EnableRemoteControlOfOtherUsers)) { @@ -119,7 +123,7 @@ public class SessionController : BaseJellyfinApiController /// Instruction sent to session. /// A . [HttpPost("Sessions/{sessionId}/Viewing")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [ProducesResponseType(StatusCodes.Status204NoContent)] public async Task DisplayContent( [FromRoute, Required] string sessionId, @@ -158,7 +162,7 @@ public class SessionController : BaseJellyfinApiController /// Instruction sent to session. /// A . [HttpPost("Sessions/{sessionId}/Playing")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [ProducesResponseType(StatusCodes.Status204NoContent)] public async Task Play( [FromRoute, Required] string sessionId, @@ -201,7 +205,7 @@ public class SessionController : BaseJellyfinApiController /// Playstate command sent to session. /// A . [HttpPost("Sessions/{sessionId}/Playing/{command}")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [ProducesResponseType(StatusCodes.Status204NoContent)] public async Task SendPlaystateCommand( [FromRoute, Required] string sessionId, @@ -232,7 +236,7 @@ public class SessionController : BaseJellyfinApiController /// System command sent to session. /// A . [HttpPost("Sessions/{sessionId}/System/{command}")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [ProducesResponseType(StatusCodes.Status204NoContent)] public async Task SendSystemCommand( [FromRoute, Required] string sessionId, @@ -258,7 +262,7 @@ public class SessionController : BaseJellyfinApiController /// General command sent to session. /// A . [HttpPost("Sessions/{sessionId}/Command/{command}")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [ProducesResponseType(StatusCodes.Status204NoContent)] public async Task SendGeneralCommand( [FromRoute, Required] string sessionId, @@ -286,7 +290,7 @@ public class SessionController : BaseJellyfinApiController /// Full general command sent to session. /// A . [HttpPost("Sessions/{sessionId}/Command")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [ProducesResponseType(StatusCodes.Status204NoContent)] public async Task SendFullGeneralCommand( [FromRoute, Required] string sessionId, @@ -316,7 +320,7 @@ public class SessionController : BaseJellyfinApiController /// Message sent. /// A . [HttpPost("Sessions/{sessionId}/Message")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [ProducesResponseType(StatusCodes.Status204NoContent)] public async Task SendMessageCommand( [FromRoute, Required] string sessionId, @@ -345,7 +349,7 @@ public class SessionController : BaseJellyfinApiController /// User added to session. /// A . [HttpPost("Sessions/{sessionId}/User/{userId}")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult AddUserToSession( [FromRoute, Required] string sessionId, @@ -363,7 +367,7 @@ public class SessionController : BaseJellyfinApiController /// User removed from session. /// A . [HttpDelete("Sessions/{sessionId}/User/{userId}")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult RemoveUserFromSession( [FromRoute, Required] string sessionId, @@ -385,7 +389,7 @@ public class SessionController : BaseJellyfinApiController /// Capabilities posted. /// A . [HttpPost("Sessions/Capabilities")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [ProducesResponseType(StatusCodes.Status204NoContent)] public async Task PostCapabilities( [FromQuery] string? id, @@ -419,7 +423,7 @@ public class SessionController : BaseJellyfinApiController /// Capabilities updated. /// A . [HttpPost("Sessions/Capabilities/Full")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [ProducesResponseType(StatusCodes.Status204NoContent)] public async Task PostFullCapabilities( [FromQuery] string? id, @@ -443,7 +447,7 @@ public class SessionController : BaseJellyfinApiController /// Session reported to server. /// A . [HttpPost("Sessions/Viewing")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [ProducesResponseType(StatusCodes.Status204NoContent)] public async Task ReportViewing( [FromQuery] string? sessionId, @@ -461,7 +465,7 @@ public class SessionController : BaseJellyfinApiController /// Session end reported to server. /// A . [HttpPost("Sessions/Logout")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [ProducesResponseType(StatusCodes.Status204NoContent)] public async Task ReportSessionEnded() { diff --git a/Jellyfin.Api/Controllers/StudiosController.cs b/Jellyfin.Api/Controllers/StudiosController.cs index 799be2ae86..f434f60f51 100644 --- a/Jellyfin.Api/Controllers/StudiosController.cs +++ b/Jellyfin.Api/Controllers/StudiosController.cs @@ -1,6 +1,5 @@ using System; using System.ComponentModel.DataAnnotations; -using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; @@ -21,7 +20,7 @@ namespace Jellyfin.Api.Controllers; /// /// Studios controller. /// -[Authorize(Policy = Policies.DefaultAuthorization)] +[Authorize] public class StudiosController : BaseJellyfinApiController { private readonly ILibraryManager _libraryManager; @@ -87,11 +86,12 @@ public class StudiosController : BaseJellyfinApiController [FromQuery] bool? enableImages = true, [FromQuery] bool enableTotalRecordCount = true) { + userId = RequestHelpers.GetUserId(User, userId); var dtoOptions = new DtoOptions { Fields = fields } .AddClientFields(User) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); - User? user = userId is null || userId.Value.Equals(default) + User? user = userId.Value.Equals(default) ? null : _userManager.GetUserById(userId.Value); @@ -140,10 +140,11 @@ public class StudiosController : BaseJellyfinApiController [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult GetStudio([FromRoute, Required] string name, [FromQuery] Guid? userId) { + userId = RequestHelpers.GetUserId(User, userId); var dtoOptions = new DtoOptions().AddClientFields(User); var item = _libraryManager.GetStudio(name); - if (userId.HasValue && !userId.Equals(default)) + if (!userId.Equals(default)) { var user = _userManager.GetUserById(userId.Value); diff --git a/Jellyfin.Api/Controllers/SubtitleController.cs b/Jellyfin.Api/Controllers/SubtitleController.cs index fd0a71f9e3..e384213380 100644 --- a/Jellyfin.Api/Controllers/SubtitleController.cs +++ b/Jellyfin.Api/Controllers/SubtitleController.cs @@ -114,7 +114,7 @@ public class SubtitleController : BaseJellyfinApiController /// Subtitles retrieved. /// An array of . [HttpGet("Items/{itemId}/RemoteSearch/Subtitles/{language}")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] public async Task>> SearchRemoteSubtitles( [FromRoute, Required] Guid itemId, @@ -134,7 +134,7 @@ public class SubtitleController : BaseJellyfinApiController /// Subtitle downloaded. /// A . [HttpPost("Items/{itemId}/RemoteSearch/Subtitles/{subtitleId}")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [ProducesResponseType(StatusCodes.Status204NoContent)] public async Task DownloadRemoteSubtitles( [FromRoute, Required] Guid itemId, @@ -164,7 +164,7 @@ public class SubtitleController : BaseJellyfinApiController /// File returned. /// A with the subtitle file. [HttpGet("Providers/Subtitles/Subtitles/{id}")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] [Produces(MediaTypeNames.Application.Octet)] [ProducesFile("text/*")] @@ -322,7 +322,7 @@ public class SubtitleController : BaseJellyfinApiController /// Subtitle playlist retrieved. /// A with the HLS subtitle playlist. [HttpGet("Videos/{itemId}/{mediaSourceId}/Subtitles/{index}/subtitles.m3u8")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesPlaylistFile] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")] @@ -463,7 +463,7 @@ public class SubtitleController : BaseJellyfinApiController /// Information retrieved. /// An array of with the available font files. [HttpGet("FallbackFont/Fonts")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] public IEnumerable GetFallbackFontList() { @@ -514,7 +514,7 @@ public class SubtitleController : BaseJellyfinApiController /// Fallback font file retrieved. /// The fallback font file. [HttpGet("FallbackFont/Fonts/{name}")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesFile("font/*")] public ActionResult GetFallbackFont([FromRoute, Required] string name) diff --git a/Jellyfin.Api/Controllers/SuggestionsController.cs b/Jellyfin.Api/Controllers/SuggestionsController.cs index c5c429757a..5b808f257c 100644 --- a/Jellyfin.Api/Controllers/SuggestionsController.cs +++ b/Jellyfin.Api/Controllers/SuggestionsController.cs @@ -1,6 +1,5 @@ using System; using System.ComponentModel.DataAnnotations; -using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; using Jellyfin.Api.ModelBinders; using Jellyfin.Data.Enums; @@ -19,7 +18,7 @@ namespace Jellyfin.Api.Controllers; /// The suggestions controller. /// [Route("")] -[Authorize(Policy = Policies.DefaultAuthorization)] +[Authorize] public class SuggestionsController : BaseJellyfinApiController { private readonly IDtoService _dtoService; diff --git a/Jellyfin.Api/Controllers/SystemController.cs b/Jellyfin.Api/Controllers/SystemController.cs index b0b2e2d6d8..4ab705f40a 100644 --- a/Jellyfin.Api/Controllers/SystemController.cs +++ b/Jellyfin.Api/Controllers/SystemController.cs @@ -172,7 +172,7 @@ public class SystemController : BaseJellyfinApiController /// Information retrieved. /// with information about the endpoint. [HttpGet("Endpoint")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult GetEndpointInfo() { @@ -210,7 +210,7 @@ public class SystemController : BaseJellyfinApiController /// Information retrieved. /// An with the WakeOnLan infos. [HttpGet("WakeOnLanInfo")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [Obsolete("This endpoint is obsolete.")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetWakeOnLanInfo() diff --git a/Jellyfin.Api/Controllers/TrailersController.cs b/Jellyfin.Api/Controllers/TrailersController.cs index 115efcd8f3..b5b6406206 100644 --- a/Jellyfin.Api/Controllers/TrailersController.cs +++ b/Jellyfin.Api/Controllers/TrailersController.cs @@ -1,5 +1,4 @@ using System; -using Jellyfin.Api.Constants; using Jellyfin.Api.ModelBinders; using Jellyfin.Data.Enums; using MediaBrowser.Model.Dto; @@ -14,7 +13,7 @@ namespace Jellyfin.Api.Controllers; /// /// The trailers controller. /// -[Authorize(Policy = Policies.DefaultAuthorization)] +[Authorize] public class TrailersController : BaseJellyfinApiController { private readonly ItemsController _itemsController; diff --git a/Jellyfin.Api/Controllers/TvShowsController.cs b/Jellyfin.Api/Controllers/TvShowsController.cs index 2be32095e3..7d23281f2c 100644 --- a/Jellyfin.Api/Controllers/TvShowsController.cs +++ b/Jellyfin.Api/Controllers/TvShowsController.cs @@ -2,8 +2,8 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; -using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; +using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; using Jellyfin.Data.Enums; using Jellyfin.Extensions; @@ -25,7 +25,7 @@ namespace Jellyfin.Api.Controllers; /// The tv shows controller. /// [Route("Shows")] -[Authorize(Policy = Policies.DefaultAuthorization)] +[Authorize] public class TvShowsController : BaseJellyfinApiController { private readonly IUserManager _userManager; @@ -88,6 +88,7 @@ public class TvShowsController : BaseJellyfinApiController [FromQuery] bool disableFirstEpisode = false, [FromQuery] bool enableRewatching = false) { + userId = RequestHelpers.GetUserId(User, userId); var options = new DtoOptions { Fields = fields } .AddClientFields(User) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); @@ -99,7 +100,7 @@ public class TvShowsController : BaseJellyfinApiController ParentId = parentId, SeriesId = seriesId, StartIndex = startIndex, - UserId = userId ?? Guid.Empty, + UserId = userId.Value, EnableTotalRecordCount = enableTotalRecordCount, DisableFirstEpisode = disableFirstEpisode, NextUpDateCutoff = nextUpDateCutoff ?? DateTime.MinValue, @@ -107,7 +108,7 @@ public class TvShowsController : BaseJellyfinApiController }, options); - var user = userId is null || userId.Value.Equals(default) + var user = userId.Value.Equals(default) ? null : _userManager.GetUserById(userId.Value); @@ -145,7 +146,8 @@ public class TvShowsController : BaseJellyfinApiController [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, [FromQuery] bool? enableUserData) { - var user = userId is null || userId.Value.Equals(default) + userId = RequestHelpers.GetUserId(User, userId); + var user = userId.Value.Equals(default) ? null : _userManager.GetUserById(userId.Value); @@ -216,7 +218,8 @@ public class TvShowsController : BaseJellyfinApiController [FromQuery] bool? enableUserData, [FromQuery] string? sortBy) { - var user = userId is null || userId.Value.Equals(default) + userId = RequestHelpers.GetUserId(User, userId); + var user = userId.Value.Equals(default) ? null : _userManager.GetUserById(userId.Value); @@ -332,7 +335,8 @@ public class TvShowsController : BaseJellyfinApiController [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, [FromQuery] bool? enableUserData) { - var user = userId is null || userId.Value.Equals(default) + userId = RequestHelpers.GetUserId(User, userId); + var user = userId.Value.Equals(default) ? null : _userManager.GetUserById(userId.Value); diff --git a/Jellyfin.Api/Controllers/UniversalAudioController.cs b/Jellyfin.Api/Controllers/UniversalAudioController.cs index 6946caa2ba..12d033ae63 100644 --- a/Jellyfin.Api/Controllers/UniversalAudioController.cs +++ b/Jellyfin.Api/Controllers/UniversalAudioController.cs @@ -5,7 +5,6 @@ using System.Globalization; using System.Linq; using System.Threading.Tasks; using Jellyfin.Api.Attributes; -using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; @@ -82,7 +81,7 @@ public class UniversalAudioController : BaseJellyfinApiController /// A containing the audio file. [HttpGet("Audio/{itemId}/universal")] [HttpHead("Audio/{itemId}/universal", Name = "HeadUniversalAudioStream")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status302Found)] [ProducesAudioFile] @@ -107,11 +106,7 @@ public class UniversalAudioController : BaseJellyfinApiController [FromQuery] bool enableRedirection = true) { var deviceProfile = GetDeviceProfile(container, transcodingContainer, audioCodec, transcodingProtocol, breakOnNonKeyFrames, transcodingAudioChannels, maxAudioSampleRate, maxAudioBitDepth, maxAudioChannels); - - if (!userId.HasValue || userId.Value.Equals(default)) - { - userId = User.GetUserId(); - } + userId = RequestHelpers.GetUserId(User, userId); _logger.LogInformation("GetPostedPlaybackInfo profile: {@Profile}", deviceProfile); diff --git a/Jellyfin.Api/Controllers/UserController.cs b/Jellyfin.Api/Controllers/UserController.cs index 7f184f31e7..b0973b8a14 100644 --- a/Jellyfin.Api/Controllers/UserController.cs +++ b/Jellyfin.Api/Controllers/UserController.cs @@ -81,7 +81,7 @@ public class UserController : BaseJellyfinApiController /// Users returned. /// An containing the users. [HttpGet] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetUsers( [FromQuery] bool? isHidden, @@ -147,6 +147,11 @@ public class UserController : BaseJellyfinApiController public async Task DeleteUser([FromRoute, Required] Guid userId) { var user = _userManager.GetUserById(userId); + if (user is null) + { + return NotFound(); + } + await _sessionManager.RevokeUserTokens(user.Id, null).ConfigureAwait(false); await _userManager.DeleteUserAsync(userId).ConfigureAwait(false); return NoContent(); @@ -251,7 +256,7 @@ public class UserController : BaseJellyfinApiController /// User not found. /// A indicating success or a or a on failure. [HttpPost("{userId}/Password")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] @@ -281,8 +286,8 @@ public class UserController : BaseJellyfinApiController { var success = await _userManager.AuthenticateUser( user.Username, - request.CurrentPw, - request.CurrentPw, + request.CurrentPw ?? string.Empty, + request.CurrentPw ?? string.Empty, HttpContext.GetNormalizedRemoteIp().ToString(), false).ConfigureAwait(false); @@ -292,7 +297,7 @@ public class UserController : BaseJellyfinApiController } } - await _userManager.ChangePassword(user, request.NewPw).ConfigureAwait(false); + await _userManager.ChangePassword(user, request.NewPw ?? string.Empty).ConfigureAwait(false); var currentToken = User.GetToken(); @@ -312,7 +317,7 @@ public class UserController : BaseJellyfinApiController /// User not found. /// A indicating success or a or a on failure. [HttpPost("{userId}/EasyPassword")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] @@ -338,7 +343,7 @@ public class UserController : BaseJellyfinApiController } else { - await _userManager.ChangeEasyPassword(user, request.NewPw, request.NewPassword).ConfigureAwait(false); + await _userManager.ChangeEasyPassword(user, request.NewPw ?? string.Empty, request.NewPassword ?? string.Empty).ConfigureAwait(false); } return NoContent(); @@ -354,7 +359,7 @@ public class UserController : BaseJellyfinApiController /// User update forbidden. /// A indicating success or a or a on failure. [HttpPost("{userId}")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status403Forbidden)] @@ -362,13 +367,17 @@ public class UserController : BaseJellyfinApiController [FromRoute, Required] Guid userId, [FromBody, Required] UserDto updateUser) { + var user = _userManager.GetUserById(userId); + if (user is null) + { + return NotFound(); + } + if (!RequestHelpers.AssertCanUpdateUser(_userManager, User, userId, true)) { return StatusCode(StatusCodes.Status403Forbidden, "User update not allowed."); } - var user = _userManager.GetUserById(userId); - if (!string.Equals(user.Username, updateUser.Name, StringComparison.Ordinal)) { await _userManager.RenameUser(user, updateUser.Name).ConfigureAwait(false); @@ -398,6 +407,10 @@ public class UserController : BaseJellyfinApiController [FromBody, Required] UserPolicy newPolicy) { var user = _userManager.GetUserById(userId); + if (user is null) + { + return NotFound(); + } // If removing admin access if (!newPolicy.IsAdministrator && user.HasPermission(PermissionKind.IsAdministrator)) @@ -440,7 +453,7 @@ public class UserController : BaseJellyfinApiController /// User configuration update forbidden. /// A indicating success. [HttpPost("{userId}/Configuration")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status403Forbidden)] public async Task UpdateUserConfiguration( @@ -526,7 +539,7 @@ public class UserController : BaseJellyfinApiController /// Token is not owned by a user. /// A for the authenticated user. [HttpGet("Me")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] public ActionResult GetCurrentUser() diff --git a/Jellyfin.Api/Controllers/UserLibraryController.cs b/Jellyfin.Api/Controllers/UserLibraryController.cs index 556cf38945..1233995b4c 100644 --- a/Jellyfin.Api/Controllers/UserLibraryController.cs +++ b/Jellyfin.Api/Controllers/UserLibraryController.cs @@ -4,7 +4,6 @@ using System.ComponentModel.DataAnnotations; using System.Linq; using System.Threading; using System.Threading.Tasks; -using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; using Jellyfin.Api.ModelBinders; using Jellyfin.Data.Enums; @@ -28,7 +27,7 @@ namespace Jellyfin.Api.Controllers; /// User library controller. /// [Route("")] -[Authorize(Policy = Policies.DefaultAuthorization)] +[Authorize] public class UserLibraryController : BaseJellyfinApiController { private readonly IUserManager _userManager; @@ -79,10 +78,18 @@ public class UserLibraryController : BaseJellyfinApiController public async Task> GetItem([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId) { var user = _userManager.GetUserById(userId); + if (user is null) + { + return NotFound(); + } var item = itemId.Equals(default) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(itemId); + if (item is null) + { + return NotFound(); + } await RefreshItemOnDemandIfNeeded(item).ConfigureAwait(false); @@ -102,6 +109,11 @@ public class UserLibraryController : BaseJellyfinApiController public ActionResult GetRootFolder([FromRoute, Required] Guid userId) { var user = _userManager.GetUserById(userId); + if (user is null) + { + return NotFound(); + } + var item = _libraryManager.GetUserRootFolder(); var dtoOptions = new DtoOptions().AddClientFields(User); return _dtoService.GetBaseItemDto(item, dtoOptions, user); @@ -119,10 +131,18 @@ public class UserLibraryController : BaseJellyfinApiController public async Task>> GetIntros([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId) { var user = _userManager.GetUserById(userId); + if (user is null) + { + return NotFound(); + } var item = itemId.Equals(default) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(itemId); + if (item is null) + { + return NotFound(); + } var items = await _libraryManager.GetIntros(item, user).ConfigureAwait(false); var dtoOptions = new DtoOptions().AddClientFields(User); @@ -200,10 +220,18 @@ public class UserLibraryController : BaseJellyfinApiController public ActionResult> GetLocalTrailers([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId) { var user = _userManager.GetUserById(userId); + if (user is null) + { + return NotFound(); + } var item = itemId.Equals(default) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(itemId); + if (item is null) + { + return NotFound(); + } var dtoOptions = new DtoOptions().AddClientFields(User); @@ -230,10 +258,18 @@ public class UserLibraryController : BaseJellyfinApiController public ActionResult> GetSpecialFeatures([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId) { var user = _userManager.GetUserById(userId); + if (user is null) + { + return NotFound(); + } var item = itemId.Equals(default) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(itemId); + if (item is null) + { + return NotFound(); + } var dtoOptions = new DtoOptions().AddClientFields(User); @@ -275,6 +311,10 @@ public class UserLibraryController : BaseJellyfinApiController [FromQuery] bool groupItems = true) { var user = _userManager.GetUserById(userId); + if (user is null) + { + return NotFound(); + } if (!isPlayed.HasValue) { diff --git a/Jellyfin.Api/Controllers/UserViewsController.cs b/Jellyfin.Api/Controllers/UserViewsController.cs index aa7ba8891b..838b432340 100644 --- a/Jellyfin.Api/Controllers/UserViewsController.cs +++ b/Jellyfin.Api/Controllers/UserViewsController.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Globalization; using System.Linq; -using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; using Jellyfin.Api.ModelBinders; using Jellyfin.Api.Models.UserViewDtos; @@ -23,7 +22,7 @@ namespace Jellyfin.Api.Controllers; /// User views controller. /// [Route("")] -[Authorize(Policy = Policies.DefaultAuthorization)] +[Authorize] public class UserViewsController : BaseJellyfinApiController { private readonly IUserManager _userManager; diff --git a/Jellyfin.Api/Controllers/VideosController.cs b/Jellyfin.Api/Controllers/VideosController.cs index 01a319879a..c0ec646eda 100644 --- a/Jellyfin.Api/Controllers/VideosController.cs +++ b/Jellyfin.Api/Controllers/VideosController.cs @@ -100,16 +100,17 @@ public class VideosController : BaseJellyfinApiController /// Additional parts returned. /// A with the parts. [HttpGet("{itemId}/AdditionalParts")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetAdditionalPart([FromRoute, Required] Guid itemId, [FromQuery] Guid? userId) { - var user = userId is null || userId.Value.Equals(default) + userId = RequestHelpers.GetUserId(User, userId); + var user = userId.Value.Equals(default) ? null : _userManager.GetUserById(userId.Value); var item = itemId.Equals(default) - ? (userId is null || userId.Value.Equals(default) + ? (userId.Value.Equals(default) ? _libraryManager.RootFolder : _libraryManager.GetUserRootFolder()) : _libraryManager.GetItemById(itemId); @@ -155,7 +156,12 @@ public class VideosController : BaseJellyfinApiController if (video.LinkedAlternateVersions.Length == 0) { - video = (Video)_libraryManager.GetItemById(video.PrimaryVersionId); + video = (Video?)_libraryManager.GetItemById(video.PrimaryVersionId); + } + + if (video is null) + { + return NotFound(); } foreach (var link in video.GetLinkedAlternateVersions()) diff --git a/Jellyfin.Api/Controllers/YearsController.cs b/Jellyfin.Api/Controllers/YearsController.cs index 2e5fdc1464..74370db50b 100644 --- a/Jellyfin.Api/Controllers/YearsController.cs +++ b/Jellyfin.Api/Controllers/YearsController.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; -using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; @@ -24,7 +23,7 @@ namespace Jellyfin.Api.Controllers; /// /// Years controller. /// -[Authorize(Policy = Policies.DefaultAuthorization)] +[Authorize] public class YearsController : BaseJellyfinApiController { private readonly ILibraryManager _libraryManager; @@ -86,11 +85,12 @@ public class YearsController : BaseJellyfinApiController [FromQuery] bool recursive = true, [FromQuery] bool? enableImages = true) { + userId = RequestHelpers.GetUserId(User, userId); var dtoOptions = new DtoOptions { Fields = fields } .AddClientFields(User) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); - User? user = userId is null || userId.Value.Equals(default) + User? user = userId.Value.Equals(default) ? null : _userManager.GetUserById(userId.Value); BaseItem parentItem = _libraryManager.GetParentItem(parentId, userId); @@ -172,6 +172,7 @@ public class YearsController : BaseJellyfinApiController [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult GetYear([FromRoute, Required] int year, [FromQuery] Guid? userId) { + userId = RequestHelpers.GetUserId(User, userId); var item = _libraryManager.GetYear(year); if (item is null) { @@ -181,7 +182,7 @@ public class YearsController : BaseJellyfinApiController var dtoOptions = new DtoOptions() .AddClientFields(User); - if (userId.HasValue && !userId.Value.Equals(default)) + if (!userId.Value.Equals(default)) { var user = _userManager.GetUserById(userId.Value); return _dtoService.GetBaseItemDto(item, dtoOptions, user); diff --git a/Jellyfin.Api/Extensions/ClaimsPrincipalExtensions.cs b/Jellyfin.Api/Extensions/ClaimsPrincipalExtensions.cs index 6b3e78d4d1..d2e8eb378b 100644 --- a/Jellyfin.Api/Extensions/ClaimsPrincipalExtensions.cs +++ b/Jellyfin.Api/Extensions/ClaimsPrincipalExtensions.cs @@ -71,8 +71,7 @@ public static class ClaimsPrincipalExtensions public static bool GetIsApiKey(this ClaimsPrincipal user) { var claimValue = GetClaimValue(user, InternalClaimTypes.IsApiKey); - return !string.IsNullOrEmpty(claimValue) - && bool.TryParse(claimValue, out var parsedClaimValue) + return bool.TryParse(claimValue, out var parsedClaimValue) && parsedClaimValue; } diff --git a/Jellyfin.Api/Helpers/MediaInfoHelper.cs b/Jellyfin.Api/Helpers/MediaInfoHelper.cs index df37d96c60..5910d80737 100644 --- a/Jellyfin.Api/Helpers/MediaInfoHelper.cs +++ b/Jellyfin.Api/Helpers/MediaInfoHelper.cs @@ -200,7 +200,7 @@ public class MediaInfoHelper options.SubtitleStreamIndex = subtitleStreamIndex; } - var user = _userManager.GetUserById(userId); + var user = _userManager.GetUserById(userId) ?? throw new ResourceNotFoundException(); if (!enableDirectPlay) { diff --git a/Jellyfin.Api/Helpers/RequestHelpers.cs b/Jellyfin.Api/Helpers/RequestHelpers.cs index 3ce2b834d1..57098edbae 100644 --- a/Jellyfin.Api/Helpers/RequestHelpers.cs +++ b/Jellyfin.Api/Helpers/RequestHelpers.cs @@ -11,6 +11,7 @@ using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Session; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Querying; @@ -55,6 +56,32 @@ public static class RequestHelpers return result; } + /// + /// Checks if the user can access a user. + /// + /// The for the current request. + /// The user id. + /// A whether the user can access the user. + internal static Guid GetUserId(ClaimsPrincipal claimsPrincipal, Guid? userId) + { + var authenticatedUserId = claimsPrincipal.GetUserId(); + + // UserId not provided, fall back to authenticated user id. + if (userId is null || userId.Value.Equals(default)) + { + return authenticatedUserId; + } + + // User must be administrator to access another user. + var isAdministrator = claimsPrincipal.IsInRole(UserRoles.Administrator); + if (!userId.Value.Equals(authenticatedUserId) && !isAdministrator) + { + throw new SecurityException("Forbidden"); + } + + return userId.Value; + } + /// /// Checks if the user can update an entry. /// @@ -81,6 +108,11 @@ public static class RequestHelpers } var user = userManager.GetUserById(userId); + if (user is null) + { + throw new ResourceNotFoundException(); + } + return user.EnableUserPreferenceAccess; } @@ -98,7 +130,7 @@ public static class RequestHelpers if (session is null) { - throw new ArgumentException("Session not found."); + throw new ResourceNotFoundException("Session not found."); } return session; diff --git a/Jellyfin.Api/Helpers/StreamingHelpers.cs b/Jellyfin.Api/Helpers/StreamingHelpers.cs index d867df86ee..9b5a14c4de 100644 --- a/Jellyfin.Api/Helpers/StreamingHelpers.cs +++ b/Jellyfin.Api/Helpers/StreamingHelpers.cs @@ -337,10 +337,10 @@ public static class StreamingHelpers value = index == -1 ? value.Slice(npt.Length) : value.Slice(npt.Length, index - npt.Length); - if (value.IndexOf(':') == -1) + if (!value.Contains(':')) { // Parses npt times in the format of '417.33' - if (double.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var seconds)) + if (double.TryParse(value, CultureInfo.InvariantCulture, out var seconds)) { return TimeSpan.FromSeconds(seconds).Ticks; } diff --git a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs index 12960f87a2..cd8ac49820 100644 --- a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs +++ b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs @@ -457,8 +457,7 @@ public class TranscodingJobHelper : IDisposable var videoCodec = state.ActualOutputVideoCodec; var hardwareAccelerationTypeString = _serverConfigurationManager.GetEncodingOptions().HardwareAccelerationType; HardwareEncodingType? hardwareAccelerationType = null; - if (!string.IsNullOrEmpty(hardwareAccelerationTypeString) - && Enum.TryParse(hardwareAccelerationTypeString, out var parsedHardwareAccelerationType)) + if (Enum.TryParse(hardwareAccelerationTypeString, out var parsedHardwareAccelerationType)) { hardwareAccelerationType = parsedHardwareAccelerationType; } diff --git a/Jellyfin.Api/Middleware/LanFilteringMiddleware.cs b/Jellyfin.Api/Middleware/LanFilteringMiddleware.cs index 7b05351e32..9c2194fafd 100644 --- a/Jellyfin.Api/Middleware/LanFilteringMiddleware.cs +++ b/Jellyfin.Api/Middleware/LanFilteringMiddleware.cs @@ -1,6 +1,6 @@ -using System.Net; using System.Threading.Tasks; using Jellyfin.Networking.Configuration; +using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; using Microsoft.AspNetCore.Http; @@ -32,9 +32,14 @@ public class LanFilteringMiddleware /// The async task. public async Task Invoke(HttpContext httpContext, INetworkManager networkManager, IServerConfigurationManager serverConfigurationManager) { - var host = httpContext.Connection.RemoteIpAddress ?? IPAddress.Loopback; + if (serverConfigurationManager.GetNetworkConfiguration().EnableRemoteAccess) + { + await _next(httpContext).ConfigureAwait(false); + return; + } - if (!networkManager.IsInLocalNetwork(host) && !serverConfigurationManager.GetNetworkConfiguration().EnableRemoteAccess) + var host = httpContext.GetNormalizedRemoteIp(); + if (!networkManager.IsInLocalNetwork(host)) { return; } diff --git a/Jellyfin.Api/Models/UserDtos/CreateUserByName.cs b/Jellyfin.Api/Models/UserDtos/CreateUserByName.cs index 0503c5d57e..6b6d9682ba 100644 --- a/Jellyfin.Api/Models/UserDtos/CreateUserByName.cs +++ b/Jellyfin.Api/Models/UserDtos/CreateUserByName.cs @@ -1,4 +1,6 @@ -namespace Jellyfin.Api.Models.UserDtos; +using System.ComponentModel.DataAnnotations; + +namespace Jellyfin.Api.Models.UserDtos; /// /// The create user by name request body. @@ -8,7 +10,8 @@ public class CreateUserByName /// /// Gets or sets the username. /// - public string? Name { get; set; } + [Required] + required public string Name { get; set; } /// /// Gets or sets the password. diff --git a/Jellyfin.Api/Models/UserDtos/ForgotPasswordDto.cs b/Jellyfin.Api/Models/UserDtos/ForgotPasswordDto.cs index ebe9297ea6..a0631fd07b 100644 --- a/Jellyfin.Api/Models/UserDtos/ForgotPasswordDto.cs +++ b/Jellyfin.Api/Models/UserDtos/ForgotPasswordDto.cs @@ -11,5 +11,5 @@ public class ForgotPasswordDto /// Gets or sets the entered username to have its password reset. /// [Required] - public string? EnteredUsername { get; set; } + required public string EnteredUsername { get; set; } } diff --git a/Jellyfin.Api/Models/UserDtos/ForgotPasswordPinDto.cs b/Jellyfin.Api/Models/UserDtos/ForgotPasswordPinDto.cs index 2949efe29f..79b8a5d63f 100644 --- a/Jellyfin.Api/Models/UserDtos/ForgotPasswordPinDto.cs +++ b/Jellyfin.Api/Models/UserDtos/ForgotPasswordPinDto.cs @@ -11,5 +11,5 @@ public class ForgotPasswordPinDto /// Gets or sets the entered pin to have the password reset. /// [Required] - public string? Pin { get; set; } + required public string Pin { get; set; } } diff --git a/Jellyfin.Data/DayOfWeekHelper.cs b/Jellyfin.Data/DayOfWeekHelper.cs index b7ba30180e..82abfb8313 100644 --- a/Jellyfin.Data/DayOfWeekHelper.cs +++ b/Jellyfin.Data/DayOfWeekHelper.cs @@ -17,5 +17,16 @@ namespace Jellyfin.Data _ => new[] { (DayOfWeek)day } }; } + + public static bool Contains(this DynamicDayOfWeek dynamicDayOfWeek, DayOfWeek dayOfWeek) + { + return dynamicDayOfWeek switch + { + DynamicDayOfWeek.Everyday => true, + DynamicDayOfWeek.Weekday => dayOfWeek is >= DayOfWeek.Monday and <= DayOfWeek.Friday, + DynamicDayOfWeek.Weekend => dayOfWeek is DayOfWeek.Saturday or DayOfWeek.Sunday, + _ => (DayOfWeek)dynamicDayOfWeek == dayOfWeek + }; + } } } diff --git a/Jellyfin.Data/Entities/User.cs b/Jellyfin.Data/Entities/User.cs index eb59e70f3b..606e1b5427 100644 --- a/Jellyfin.Data/Entities/User.cs +++ b/Jellyfin.Data/Entities/User.cs @@ -508,6 +508,7 @@ namespace Jellyfin.Data.Entities Permissions.Add(new Permission(PermissionKind.EnableVideoPlaybackTranscoding, true)); Permissions.Add(new Permission(PermissionKind.ForceRemoteSourceTranscoding, false)); Permissions.Add(new Permission(PermissionKind.EnableRemoteControlOfOtherUsers, false)); + Permissions.Add(new Permission(PermissionKind.EnableCollectionManagement, false)); } /// @@ -525,8 +526,9 @@ namespace Jellyfin.Data.Entities { var localTime = date.ToLocalTime(); var hour = localTime.TimeOfDay.TotalHours; + var currentDayOfWeek = localTime.DayOfWeek; - return DayOfWeekHelper.GetDaysOfWeek(schedule.DayOfWeek).Contains(localTime.DayOfWeek) + return schedule.DayOfWeek.Contains(currentDayOfWeek) && hour >= schedule.StartHour && hour <= schedule.EndHour; } diff --git a/Jellyfin.Data/Enums/PermissionKind.cs b/Jellyfin.Data/Enums/PermissionKind.cs index 7d52008747..40280b95ef 100644 --- a/Jellyfin.Data/Enums/PermissionKind.cs +++ b/Jellyfin.Data/Enums/PermissionKind.cs @@ -108,6 +108,11 @@ namespace Jellyfin.Data.Enums /// /// Whether the server should force transcoding on remote connections for the user. /// - ForceRemoteSourceTranscoding = 20 + ForceRemoteSourceTranscoding = 20, + + /// + /// Whether the user can create, modify and delete collections. + /// + EnableCollectionManagement = 21 } } diff --git a/Jellyfin.Data/Enums/PreferenceKind.cs b/Jellyfin.Data/Enums/PreferenceKind.cs index a54d789afb..d2b412e459 100644 --- a/Jellyfin.Data/Enums/PreferenceKind.cs +++ b/Jellyfin.Data/Enums/PreferenceKind.cs @@ -63,6 +63,11 @@ namespace Jellyfin.Data.Enums /// /// A list of ordered views. /// - OrderedViews = 11 + OrderedViews = 11, + + /// + /// A list of allowed tags. + /// + AllowedTags = 12 } } diff --git a/Jellyfin.Networking/Manager/NetworkManager.cs b/Jellyfin.Networking/Manager/NetworkManager.cs index 86989bfde6..88332ce393 100644 --- a/Jellyfin.Networking/Manager/NetworkManager.cs +++ b/Jellyfin.Networking/Manager/NetworkManager.cs @@ -316,7 +316,7 @@ namespace Jellyfin.Networking.Manager /// public string GetBindInterface(string source, out int? port) { - if (!string.IsNullOrEmpty(source) && IPHost.TryParse(source, out IPHost host)) + if (IPHost.TryParse(source, out IPHost host)) { return GetBindInterface(host, out port); } diff --git a/Jellyfin.Server.Implementations/Devices/DeviceManager.cs b/Jellyfin.Server.Implementations/Devices/DeviceManager.cs index 8b15d6823d..a4b4c19599 100644 --- a/Jellyfin.Server.Implementations/Devices/DeviceManager.cs +++ b/Jellyfin.Server.Implementations/Devices/DeviceManager.cs @@ -9,6 +9,7 @@ using Jellyfin.Data.Enums; using Jellyfin.Data.Events; using Jellyfin.Data.Queries; using Jellyfin.Extensions; +using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Library; using MediaBrowser.Model.Devices; @@ -185,6 +186,10 @@ namespace Jellyfin.Server.Implementations.Devices if (userId.HasValue) { var user = _userManager.GetUserById(userId.Value); + if (user is null) + { + throw new ResourceNotFoundException(); + } sessions = sessions.Where(i => CanAccessDevice(user, i.DeviceId)); } diff --git a/Jellyfin.Server.Implementations/Users/UserManager.cs b/Jellyfin.Server.Implementations/Users/UserManager.cs index dc9d78857e..c4756433e0 100644 --- a/Jellyfin.Server.Implementations/Users/UserManager.cs +++ b/Jellyfin.Server.Implementations/Users/UserManager.cs @@ -369,8 +369,10 @@ namespace Jellyfin.Server.Implementations.Users EnablePlaybackRemuxing = user.HasPermission(PermissionKind.EnablePlaybackRemuxing), ForceRemoteSourceTranscoding = user.HasPermission(PermissionKind.ForceRemoteSourceTranscoding), EnablePublicSharing = user.HasPermission(PermissionKind.EnablePublicSharing), + EnableCollectionManagement = user.HasPermission(PermissionKind.EnableCollectionManagement), AccessSchedules = user.AccessSchedules.ToArray(), BlockedTags = user.GetPreference(PreferenceKind.BlockedTags), + AllowedTags = user.GetPreference(PreferenceKind.AllowedTags), EnabledChannels = user.GetPreferenceValues(PreferenceKind.EnabledChannels), EnabledDevices = user.GetPreference(PreferenceKind.EnabledDevices), EnabledFolders = user.GetPreferenceValues(PreferenceKind.EnabledFolders), @@ -684,6 +686,7 @@ namespace Jellyfin.Server.Implementations.Users user.SetPermission(PermissionKind.EnableAllFolders, policy.EnableAllFolders); user.SetPermission(PermissionKind.EnableRemoteControlOfOtherUsers, policy.EnableRemoteControlOfOtherUsers); user.SetPermission(PermissionKind.EnablePlaybackRemuxing, policy.EnablePlaybackRemuxing); + user.SetPermission(PermissionKind.EnableCollectionManagement, policy.EnableCollectionManagement); user.SetPermission(PermissionKind.ForceRemoteSourceTranscoding, policy.ForceRemoteSourceTranscoding); user.SetPermission(PermissionKind.EnablePublicSharing, policy.EnablePublicSharing); @@ -696,6 +699,7 @@ namespace Jellyfin.Server.Implementations.Users // TODO: fix this at some point user.SetPreference(PreferenceKind.BlockUnratedItems, policy.BlockUnratedItems ?? Array.Empty()); user.SetPreference(PreferenceKind.BlockedTags, policy.BlockedTags); + user.SetPreference(PreferenceKind.AllowedTags, policy.AllowedTags); user.SetPreference(PreferenceKind.EnabledChannels, policy.EnabledChannels); user.SetPreference(PreferenceKind.EnabledDevices, policy.EnabledDevices); user.SetPreference(PreferenceKind.EnabledFolders, policy.EnabledFolders); @@ -736,7 +740,7 @@ namespace Jellyfin.Server.Implementations.Users throw new ArgumentException("Usernames can contain unicode symbols, numbers (0-9), dashes (-), underscores (_), apostrophes ('), and periods (.)", nameof(name)); } - private static bool IsValidUsername(string name) + private static bool IsValidUsername(ReadOnlySpan name) { // This is some regex that matches only on unicode "word" characters, as well as -, _ and @ // In theory this will cut out most if not all 'control' characters which should help minimize any weirdness diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index e9af1cf83c..9867c9e47a 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -5,19 +5,15 @@ using System.Linq; using System.Net; using System.Net.Sockets; using System.Reflection; +using System.Security.Claims; using Emby.Server.Implementations; using Jellyfin.Api.Auth; using Jellyfin.Api.Auth.AnonymousLanAccessPolicy; using Jellyfin.Api.Auth.DefaultAuthorizationPolicy; -using Jellyfin.Api.Auth.DownloadPolicy; -using Jellyfin.Api.Auth.FirstTimeOrIgnoreParentalControlSetupPolicy; -using Jellyfin.Api.Auth.FirstTimeSetupOrDefaultPolicy; -using Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy; -using Jellyfin.Api.Auth.IgnoreParentalControlPolicy; +using Jellyfin.Api.Auth.FirstTimeSetupPolicy; using Jellyfin.Api.Auth.LocalAccessOrRequiresElevationPolicy; -using Jellyfin.Api.Auth.LocalAccessPolicy; -using Jellyfin.Api.Auth.RequiresElevationPolicy; using Jellyfin.Api.Auth.SyncPlayAccessPolicy; +using Jellyfin.Api.Auth.UserPermissionPolicy; using Jellyfin.Api.Constants; using Jellyfin.Api.Controllers; using Jellyfin.Api.Formatters; @@ -56,117 +52,38 @@ namespace Jellyfin.Server.Extensions /// The updated service collection. public static IServiceCollection AddJellyfinApiAuthorization(this IServiceCollection serviceCollection) { + // The default handler must be first so that it is evaluated first serviceCollection.AddSingleton(); - serviceCollection.AddSingleton(); - serviceCollection.AddSingleton(); - serviceCollection.AddSingleton(); - serviceCollection.AddSingleton(); - serviceCollection.AddSingleton(); - serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); - serviceCollection.AddSingleton(); - serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); + return serviceCollection.AddAuthorizationCore(options => { - options.AddPolicy( - Policies.DefaultAuthorization, - policy => - { - policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication); - policy.AddRequirements(new DefaultAuthorizationRequirement()); - }); - options.AddPolicy( - Policies.Download, - policy => - { - policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication); - policy.AddRequirements(new DownloadRequirement()); - }); - options.AddPolicy( - Policies.FirstTimeSetupOrDefault, - policy => - { - policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication); - policy.AddRequirements(new FirstTimeSetupOrDefaultRequirement()); - }); - options.AddPolicy( - Policies.FirstTimeSetupOrElevated, - policy => - { - policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication); - policy.AddRequirements(new FirstTimeSetupOrElevatedRequirement()); - }); - options.AddPolicy( - Policies.IgnoreParentalControl, - policy => - { - policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication); - policy.AddRequirements(new IgnoreParentalControlRequirement()); - }); - options.AddPolicy( - Policies.FirstTimeSetupOrIgnoreParentalControl, - policy => - { - policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication); - policy.AddRequirements(new FirstTimeOrIgnoreParentalControlSetupRequirement()); - }); - options.AddPolicy( - Policies.LocalAccessOnly, - policy => - { - policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication); - policy.AddRequirements(new LocalAccessRequirement()); - }); - options.AddPolicy( - Policies.LocalAccessOrRequiresElevation, - policy => - { - policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication); - policy.AddRequirements(new LocalAccessOrRequiresElevationRequirement()); - }); + options.DefaultPolicy = new AuthorizationPolicyBuilder() + .AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication) + .AddRequirements(new DefaultAuthorizationRequirement()) + .Build(); + + options.AddPolicy(Policies.AnonymousLanAccessPolicy, new AnonymousLanAccessRequirement()); + options.AddPolicy(Policies.CollectionManagement, new UserPermissionRequirement(PermissionKind.EnableCollectionManagement)); + options.AddPolicy(Policies.Download, new UserPermissionRequirement(PermissionKind.EnableContentDownloading)); + options.AddPolicy(Policies.FirstTimeSetupOrDefault, new FirstTimeSetupRequirement(requireAdmin: false)); + options.AddPolicy(Policies.FirstTimeSetupOrElevated, new FirstTimeSetupRequirement()); + options.AddPolicy(Policies.FirstTimeSetupOrIgnoreParentalControl, new FirstTimeSetupRequirement(false, false)); + options.AddPolicy(Policies.IgnoreParentalControl, new DefaultAuthorizationRequirement(validateParentalSchedule: false)); + options.AddPolicy(Policies.LiveTvAccess, new UserPermissionRequirement(PermissionKind.EnableLiveTvAccess)); + options.AddPolicy(Policies.LiveTvManagement, new UserPermissionRequirement(PermissionKind.EnableLiveTvManagement)); + options.AddPolicy(Policies.LocalAccessOrRequiresElevation, new LocalAccessOrRequiresElevationRequirement()); + options.AddPolicy(Policies.SyncPlayHasAccess, new SyncPlayAccessRequirement(SyncPlayAccessRequirementType.HasAccess)); + options.AddPolicy(Policies.SyncPlayCreateGroup, new SyncPlayAccessRequirement(SyncPlayAccessRequirementType.CreateGroup)); + options.AddPolicy(Policies.SyncPlayJoinGroup, new SyncPlayAccessRequirement(SyncPlayAccessRequirementType.JoinGroup)); + options.AddPolicy(Policies.SyncPlayIsInGroup, new SyncPlayAccessRequirement(SyncPlayAccessRequirementType.IsInGroup)); options.AddPolicy( Policies.RequiresElevation, - policy => - { - policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication); - policy.AddRequirements(new RequiresElevationRequirement()); - }); - options.AddPolicy( - Policies.SyncPlayHasAccess, - policy => - { - policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication); - policy.AddRequirements(new SyncPlayAccessRequirement(SyncPlayAccessRequirementType.HasAccess)); - }); - options.AddPolicy( - Policies.SyncPlayCreateGroup, - policy => - { - policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication); - policy.AddRequirements(new SyncPlayAccessRequirement(SyncPlayAccessRequirementType.CreateGroup)); - }); - options.AddPolicy( - Policies.SyncPlayJoinGroup, - policy => - { - policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication); - policy.AddRequirements(new SyncPlayAccessRequirement(SyncPlayAccessRequirementType.JoinGroup)); - }); - options.AddPolicy( - Policies.SyncPlayIsInGroup, - policy => - { - policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication); - policy.AddRequirements(new SyncPlayAccessRequirement(SyncPlayAccessRequirementType.IsInGroup)); - }); - options.AddPolicy( - Policies.AnonymousLanAccessPolicy, - policy => - { - policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication); - policy.AddRequirements(new AnonymousLanAccessRequirement()); - }); + policy => policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication) + .RequireClaim(ClaimTypes.Role, UserRoles.Administrator)); }); } @@ -334,6 +251,14 @@ namespace Jellyfin.Server.Extensions }); } + private static void AddPolicy(this AuthorizationOptions authorizationOptions, string policyName, IAuthorizationRequirement authorizationRequirement) + { + authorizationOptions.AddPolicy(policyName, policy => + { + policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication).AddRequirements(authorizationRequirement); + }); + } + /// /// Sets up the proxy configuration based on the addresses in . /// diff --git a/Jellyfin.Server/Filters/SecurityRequirementsOperationFilter.cs b/Jellyfin.Server/Filters/SecurityRequirementsOperationFilter.cs index 4af670e9a0..fb9f6d0a6e 100644 --- a/Jellyfin.Server/Filters/SecurityRequirementsOperationFilter.cs +++ b/Jellyfin.Server/Filters/SecurityRequirementsOperationFilter.cs @@ -18,11 +18,17 @@ namespace Jellyfin.Server.Filters { var requiredScopes = new List(); + var requiresAuth = false; // Add all method scopes. foreach (var attribute in context.MethodInfo.GetCustomAttributes(true)) { - if (attribute is AuthorizeAttribute authorizeAttribute - && authorizeAttribute.Policy is not null + if (attribute is not AuthorizeAttribute authorizeAttribute) + { + continue; + } + + requiresAuth = true; + if (authorizeAttribute.Policy is not null && !requiredScopes.Contains(authorizeAttribute.Policy, StringComparer.Ordinal)) { requiredScopes.Add(authorizeAttribute.Policy); @@ -35,8 +41,13 @@ namespace Jellyfin.Server.Filters { foreach (var attribute in controllerAttributes) { - if (attribute is AuthorizeAttribute authorizeAttribute - && authorizeAttribute.Policy is not null + if (attribute is not AuthorizeAttribute authorizeAttribute) + { + continue; + } + + requiresAuth = true; + if (authorizeAttribute.Policy is not null && !requiredScopes.Contains(authorizeAttribute.Policy, StringComparer.Ordinal)) { requiredScopes.Add(authorizeAttribute.Policy); @@ -44,35 +55,37 @@ namespace Jellyfin.Server.Filters } } - if (requiredScopes.Count != 0) + if (!requiresAuth) { - if (!operation.Responses.ContainsKey("401")) - { - operation.Responses.Add("401", new OpenApiResponse { Description = "Unauthorized" }); - } - - if (!operation.Responses.ContainsKey("403")) - { - operation.Responses.Add("403", new OpenApiResponse { Description = "Forbidden" }); - } - - var scheme = new OpenApiSecurityScheme - { - Reference = new OpenApiReference - { - Type = ReferenceType.SecurityScheme, - Id = AuthenticationSchemes.CustomAuthentication - } - }; - - operation.Security = new List - { - new OpenApiSecurityRequirement - { - [scheme] = requiredScopes - } - }; + return; } + + if (!operation.Responses.ContainsKey("401")) + { + operation.Responses.Add("401", new OpenApiResponse { Description = "Unauthorized" }); + } + + if (!operation.Responses.ContainsKey("403")) + { + operation.Responses.Add("403", new OpenApiResponse { Description = "Forbidden" }); + } + + var scheme = new OpenApiSecurityScheme + { + Reference = new OpenApiReference + { + Type = ReferenceType.SecurityScheme, + Id = AuthenticationSchemes.CustomAuthentication + } + }; + + operation.Security = new List + { + new OpenApiSecurityRequirement + { + [scheme] = requiredScopes + } + }; } } } diff --git a/Jellyfin.Server/Migrations/MigrationRunner.cs b/Jellyfin.Server/Migrations/MigrationRunner.cs index 23fb9e3708..8a6ab79323 100644 --- a/Jellyfin.Server/Migrations/MigrationRunner.cs +++ b/Jellyfin.Server/Migrations/MigrationRunner.cs @@ -21,7 +21,8 @@ namespace Jellyfin.Server.Migrations /// private static readonly Type[] _preStartupMigrationTypes = { - typeof(PreStartupRoutines.CreateNetworkConfiguration) + typeof(PreStartupRoutines.CreateNetworkConfiguration), + typeof(PreStartupRoutines.MigrateMusicBrainzTimeout) }; /// diff --git a/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateMusicBrainzTimeout.cs b/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateMusicBrainzTimeout.cs new file mode 100644 index 0000000000..14b51bd4c4 --- /dev/null +++ b/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateMusicBrainzTimeout.cs @@ -0,0 +1,89 @@ +using System; +using System.IO; +using System.Xml; +using System.Xml.Serialization; +using Emby.Server.Implementations; +using MediaBrowser.Providers.Plugins.MusicBrainz.Configuration; +using Microsoft.Extensions.Logging; + +namespace Jellyfin.Server.Migrations.PreStartupRoutines; + +/// +public class MigrateMusicBrainzTimeout : IMigrationRoutine +{ + private readonly ServerApplicationPaths _applicationPaths; + private readonly ILogger _logger; + + /// + /// Initializes a new instance of the class. + /// + /// An instance of . + /// An instance of the interface. + public MigrateMusicBrainzTimeout(ServerApplicationPaths applicationPaths, ILoggerFactory loggerFactory) + { + _applicationPaths = applicationPaths; + _logger = loggerFactory.CreateLogger(); + } + + /// + public Guid Id => Guid.Parse("A6DCACF4-C057-4Ef9-80D3-61CEF9DDB4F0"); + + /// + public string Name => nameof(MigrateMusicBrainzTimeout); + + /// + public bool PerformOnNewInstall => false; + + /// + public void Perform() + { + string path = Path.Combine(_applicationPaths.PluginConfigurationsPath, "Jellyfin.Plugin.MusicBrainz.xml"); + if (!File.Exists(path)) + { + _logger.LogDebug("No MusicBrainz plugin configuration file found, skipping"); + return; + } + + var serverConfigSerializer = new XmlSerializer(typeof(OldMusicBrainzConfiguration), new XmlRootAttribute("PluginConfiguration")); + using var xmlReader = XmlReader.Create(path); + var oldPluginConfiguration = serverConfigSerializer.Deserialize(xmlReader) as OldMusicBrainzConfiguration; + + if (oldPluginConfiguration is not null) + { + var newPluginConfiguration = new PluginConfiguration(); + newPluginConfiguration.Server = oldPluginConfiguration.Server; + newPluginConfiguration.ReplaceArtistName = oldPluginConfiguration.ReplaceArtistName; + var newRateLimit = oldPluginConfiguration.RateLimit / 1000.0; + newPluginConfiguration.RateLimit = newRateLimit < 1.0 ? 1.0 : newRateLimit; + + var pluginConfigurationSerializer = new XmlSerializer(typeof(PluginConfiguration), new XmlRootAttribute("PluginConfiguration")); + var xmlWriterSettings = new XmlWriterSettings { Indent = true }; + using var xmlWriter = XmlWriter.Create(path, xmlWriterSettings); + pluginConfigurationSerializer.Serialize(xmlWriter, newPluginConfiguration); + } + } + +#pragma warning disable + public sealed class OldMusicBrainzConfiguration + { + private string _server = string.Empty; + + private long _rateLimit = 0L; + + public string Server + { + get => _server; + set => _server = value.TrimEnd('/'); + } + + public long RateLimit + { + get => _rateLimit; + set => _rateLimit = value; + } + + public bool ReplaceArtistName { get; set; } + } +#pragma warning restore + +} diff --git a/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs index 4b692d14f0..7c4ffdbc00 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs @@ -130,7 +130,7 @@ namespace Jellyfin.Server.Migrations.Routines SkipForwardLength = dto.CustomPrefs.TryGetValue("skipForwardLength", out var length) && int.TryParse(length, out var skipForwardLength) ? skipForwardLength : 30000, - SkipBackwardLength = dto.CustomPrefs.TryGetValue("skipBackLength", out length) && !string.IsNullOrEmpty(length) && int.TryParse(length, out var skipBackwardLength) + SkipBackwardLength = dto.CustomPrefs.TryGetValue("skipBackLength", out length) && int.TryParse(length, out var skipBackwardLength) ? skipBackwardLength : 10000, EnableNextVideoInfoOverlay = dto.CustomPrefs.TryGetValue("enableNextVideoInfoOverlay", out var enabled) && !string.IsNullOrEmpty(enabled) diff --git a/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs index ea2f033027..9bf1e6b808 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs @@ -163,6 +163,7 @@ namespace Jellyfin.Server.Migrations.Routines user.SetPermission(PermissionKind.EnablePlaybackRemuxing, policy.EnablePlaybackRemuxing); user.SetPermission(PermissionKind.ForceRemoteSourceTranscoding, policy.ForceRemoteSourceTranscoding); user.SetPermission(PermissionKind.EnablePublicSharing, policy.EnablePublicSharing); + user.SetPermission(PermissionKind.EnableCollectionManagement, policy.EnableCollectionManagement); foreach (var policyAccessSchedule in policy.AccessSchedules) { diff --git a/MediaBrowser.Common/Net/IPHost.cs b/MediaBrowser.Common/Net/IPHost.cs index 7cf1b8aa0b..ec76a43b6f 100644 --- a/MediaBrowser.Common/Net/IPHost.cs +++ b/MediaBrowser.Common/Net/IPHost.cs @@ -190,7 +190,7 @@ namespace MediaBrowser.Common.Net /// Object representing the string, if it has successfully been parsed. public static IPHost Parse(string host) { - if (!string.IsNullOrEmpty(host) && IPHost.TryParse(host, out IPHost res)) + if (IPHost.TryParse(host, out IPHost res)) { return res; } @@ -206,7 +206,7 @@ namespace MediaBrowser.Common.Net /// Object representing the string, if it has successfully been parsed. public static IPHost Parse(string host, AddressFamily family) { - if (!string.IsNullOrEmpty(host) && IPHost.TryParse(host, out IPHost res)) + if (IPHost.TryParse(host, out IPHost res)) { if (family == AddressFamily.InterNetwork) { diff --git a/MediaBrowser.Common/Net/IPNetAddress.cs b/MediaBrowser.Common/Net/IPNetAddress.cs index ac3396a9f1..de72d978ec 100644 --- a/MediaBrowser.Common/Net/IPNetAddress.cs +++ b/MediaBrowser.Common/Net/IPNetAddress.cs @@ -167,6 +167,11 @@ namespace MediaBrowser.Common.Net address = address.MapToIPv4(); } + if (address.AddressFamily != AddressFamily) + { + return false; + } + var (altAddress, altPrefix) = NetworkAddressOf(address, PrefixLength); return NetworkAddress.Address.Equals(altAddress) && NetworkAddress.PrefixLength >= altPrefix; } diff --git a/MediaBrowser.Controller/Dto/IDtoService.cs b/MediaBrowser.Controller/Dto/IDtoService.cs index 89aafc84fb..22453f0f76 100644 --- a/MediaBrowser.Controller/Dto/IDtoService.cs +++ b/MediaBrowser.Controller/Dto/IDtoService.cs @@ -1,4 +1,3 @@ -#nullable disable #pragma warning disable CA1002 using System.Collections.Generic; @@ -28,7 +27,7 @@ namespace MediaBrowser.Controller.Dto /// The user. /// The owner. /// BaseItemDto. - BaseItemDto GetBaseItemDto(BaseItem item, DtoOptions options, User user = null, BaseItem owner = null); + BaseItemDto GetBaseItemDto(BaseItem item, DtoOptions options, User? user = null, BaseItem? owner = null); /// /// Gets the base item dtos. @@ -38,7 +37,7 @@ namespace MediaBrowser.Controller.Dto /// The user. /// The owner. /// The of . - IReadOnlyList GetBaseItemDtos(IReadOnlyList items, DtoOptions options, User user = null, BaseItem owner = null); + IReadOnlyList GetBaseItemDtos(IReadOnlyList items, DtoOptions options, User? user = null, BaseItem? owner = null); /// /// Gets the item by name dto. @@ -48,6 +47,6 @@ namespace MediaBrowser.Controller.Dto /// The list of tagged items. /// The user. /// The item dto. - BaseItemDto GetItemByNameDto(BaseItem item, DtoOptions options, List taggedItems, User user = null); + BaseItemDto GetItemByNameDto(BaseItem item, DtoOptions options, List? taggedItems, User? user = null); } } diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index f2c2007f7a..3d683052fe 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -1607,6 +1607,12 @@ namespace MediaBrowser.Controller.Entities return false; } + var allowedTagsPreference = user.GetPreference(PreferenceKind.AllowedTags); + if (allowedTagsPreference.Any() && !allowedTagsPreference.Any(i => Tags.Contains(i, StringComparison.OrdinalIgnoreCase))) + { + return false; + } + return true; } diff --git a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs index a1e5319048..a51299284b 100644 --- a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs +++ b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs @@ -26,6 +26,7 @@ namespace MediaBrowser.Controller.Entities EnableTotalRecordCount = true; ExcludeArtistIds = Array.Empty(); ExcludeInheritedTags = Array.Empty(); + IncludeInheritedTags = Array.Empty(); ExcludeItemIds = Array.Empty(); ExcludeItemTypes = Array.Empty(); ExcludeTags = Array.Empty(); @@ -95,6 +96,8 @@ namespace MediaBrowser.Controller.Entities public string[] ExcludeInheritedTags { get; set; } + public string[] IncludeInheritedTags { get; set; } + public IReadOnlyList Genres { get; set; } public bool? IsSpecialSeason { get; set; } @@ -368,6 +371,7 @@ namespace MediaBrowser.Controller.Entities } ExcludeInheritedTags = user.GetPreference(PreferenceKind.BlockedTags); + IncludeInheritedTags = user.GetPreference(PreferenceKind.AllowedTags); User = user; } diff --git a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs index 882abc9272..66210cb6c4 100644 --- a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs +++ b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs @@ -104,7 +104,7 @@ namespace MediaBrowser.Controller.Entities.Movies public override bool IsAuthorizedToDelete(User user, List allCollectionFolders) { - return true; + return user.HasPermission(PermissionKind.IsAdministrator) || user.HasPermission(PermissionKind.EnableCollectionManagement); } public override bool IsSaveLocalMetadataEnabled() diff --git a/MediaBrowser.Controller/Library/IUserManager.cs b/MediaBrowser.Controller/Library/IUserManager.cs index 993e3e18f9..37b4afcf32 100644 --- a/MediaBrowser.Controller/Library/IUserManager.cs +++ b/MediaBrowser.Controller/Library/IUserManager.cs @@ -1,5 +1,3 @@ -#nullable disable - #pragma warning disable CS1591 using System; @@ -47,14 +45,14 @@ namespace MediaBrowser.Controller.Library /// The id. /// The user with the specified Id, or null if the user doesn't exist. /// id is an empty Guid. - User GetUserById(Guid id); + User? GetUserById(Guid id); /// /// Gets the name of the user by. /// /// The name. /// User. - User GetUserByName(string name); + User? GetUserByName(string name); /// /// Renames the user. @@ -128,7 +126,7 @@ namespace MediaBrowser.Controller.Library /// The user. /// The remote end point. /// UserDto. - UserDto GetUserDto(User user, string remoteEndPoint = null); + UserDto GetUserDto(User user, string? remoteEndPoint = null); /// /// Authenticates the user. @@ -139,7 +137,7 @@ namespace MediaBrowser.Controller.Library /// Remove endpoint to use. /// Specifies if a user session. /// User wrapped in awaitable task. - Task AuthenticateUser(string username, string password, string passwordSha1, string remoteEndPoint, bool isUserSession); + Task AuthenticateUser(string username, string password, string passwordSha1, string remoteEndPoint, bool isUserSession); /// /// Starts the forgot password process. diff --git a/MediaBrowser.Controller/Library/LibraryManagerExtensions.cs b/MediaBrowser.Controller/Library/LibraryManagerExtensions.cs index 7bc8fa5abd..6d2c3c3d29 100644 --- a/MediaBrowser.Controller/Library/LibraryManagerExtensions.cs +++ b/MediaBrowser.Controller/Library/LibraryManagerExtensions.cs @@ -1,5 +1,3 @@ -#nullable disable - #pragma warning disable CS1591 using System; @@ -9,7 +7,7 @@ namespace MediaBrowser.Controller.Library { public static class LibraryManagerExtensions { - public static BaseItem GetItemById(this ILibraryManager manager, string id) + public static BaseItem? GetItemById(this ILibraryManager manager, string id) { return manager.GetItemById(new Guid(id)); } diff --git a/MediaBrowser.Controller/LiveTv/LiveTvChannel.cs b/MediaBrowser.Controller/LiveTv/LiveTvChannel.cs index 9788260420..f11e3c8f68 100644 --- a/MediaBrowser.Controller/LiveTv/LiveTvChannel.cs +++ b/MediaBrowser.Controller/LiveTv/LiveTvChannel.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Globalization; +using System.Linq; using System.Text.Json.Serialization; using Jellyfin.Data.Enums; using Jellyfin.Extensions; @@ -105,12 +106,9 @@ namespace MediaBrowser.Controller.LiveTv protected override string CreateSortName() { - if (!string.IsNullOrEmpty(Number)) + if (double.TryParse(Number, CultureInfo.InvariantCulture, out double number)) { - if (double.TryParse(Number, NumberStyles.Any, CultureInfo.InvariantCulture, out double number)) - { - return string.Format(CultureInfo.InvariantCulture, "{0:00000.0}", number) + "-" + (Name ?? string.Empty); - } + return string.Format(CultureInfo.InvariantCulture, "{0:00000.0}", number) + "-" + (Name ?? string.Empty); } return (Number ?? string.Empty) + "-" + (Name ?? string.Empty); @@ -122,9 +120,7 @@ namespace MediaBrowser.Controller.LiveTv } public IEnumerable GetTaggedItems() - { - return new List(); - } + => Enumerable.Empty(); public override List GetMediaSources(bool enablePathSubstitution) { diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 9b5edabc06..f4684a2218 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -559,6 +559,12 @@ namespace MediaBrowser.Controller.MediaEncoding if (string.Equals(codec, "aac", StringComparison.OrdinalIgnoreCase)) { + // Use Apple's aac encoder if available as it provides best audio quality + if (_mediaEncoder.SupportsEncoder("aac_at")) + { + return "aac_at"; + } + // Use libfdk_aac for better audio quality if using custom build of FFmpeg which has fdk_aac support if (_mediaEncoder.SupportsEncoder("libfdk_aac")) { @@ -1137,7 +1143,7 @@ namespace MediaBrowser.Controller.MediaEncoding public static string NormalizeTranscodingLevel(EncodingJobInfo state, string level) { - if (double.TryParse(level, NumberStyles.Any, CultureInfo.InvariantCulture, out double requestLevel)) + if (double.TryParse(level, CultureInfo.InvariantCulture, out double requestLevel)) { if (string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase) || string.Equals(state.ActualOutputVideoCodec, "h265", StringComparison.OrdinalIgnoreCase)) @@ -1731,7 +1737,7 @@ namespace MediaBrowser.Controller.MediaEncoding else if (string.Equals(videoEncoder, "hevc_qsv", StringComparison.OrdinalIgnoreCase)) { // hevc_qsv use -level 51 instead of -level 153. - if (double.TryParse(level, NumberStyles.Any, CultureInfo.InvariantCulture, out double hevcLevel)) + if (double.TryParse(level, CultureInfo.InvariantCulture, out double hevcLevel)) { param += " -level " + (hevcLevel / 3); } @@ -1910,8 +1916,7 @@ namespace MediaBrowser.Controller.MediaEncoding // If a specific level was requested, the source must match or be less than var level = state.GetRequestedLevel(videoStream.Codec); - if (!string.IsNullOrEmpty(level) - && double.TryParse(level, NumberStyles.Any, CultureInfo.InvariantCulture, out var requestLevel)) + if (double.TryParse(level, CultureInfo.InvariantCulture, out var requestLevel)) { if (!videoStream.Level.HasValue) { @@ -2211,87 +2216,59 @@ namespace MediaBrowser.Controller.MediaEncoding var request = state.BaseRequest; - var inputChannels = audioStream.Channels; - - if (inputChannels <= 0) - { - inputChannels = null; - } - var codec = outputAudioCodec ?? string.Empty; - int? transcoderChannelLimit; - if (codec.IndexOf("wma", StringComparison.OrdinalIgnoreCase) != -1) + int? resultChannels = state.GetRequestedAudioChannels(codec); + + var inputChannels = audioStream.Channels; + + if (inputChannels > 0) { - // wmav2 currently only supports two channel output - transcoderChannelLimit = 2; - } - else if (codec.IndexOf("mp3", StringComparison.OrdinalIgnoreCase) != -1) - { - // libmp3lame currently only supports two channel output - transcoderChannelLimit = 2; - } - else if (codec.IndexOf("aac", StringComparison.OrdinalIgnoreCase) != -1) - { - // aac is able to handle 8ch(7.1 layout) - transcoderChannelLimit = 8; - } - else - { - // If we don't have any media info then limit it to 6 to prevent encoding errors due to asking for too many channels - transcoderChannelLimit = 6; + resultChannels = inputChannels < resultChannels ? inputChannels : resultChannels ?? inputChannels; } var isTranscodingAudio = !IsCopyCodec(codec); - int? resultChannels = state.GetRequestedAudioChannels(codec); if (isTranscodingAudio) { - resultChannels = GetMinValue(request.TranscodingMaxAudioChannels, resultChannels); - } + // Set max transcoding channels for encoders that can't handle more than a set amount of channels + // AAC, FLAC, ALAC, libopus, libvorbis encoders all support at least 8 channels + int transcoderChannelLimit = GetAudioEncoder(state) switch + { + string audioEncoder when audioEncoder.Equals("wmav2", StringComparison.OrdinalIgnoreCase) + || audioEncoder.Equals("libmp3lame", StringComparison.OrdinalIgnoreCase) => 2, + string audioEncoder when audioEncoder.Equals("libfdk_aac", StringComparison.OrdinalIgnoreCase) + || audioEncoder.Equals("aac_at", StringComparison.OrdinalIgnoreCase) + || audioEncoder.Equals("ac3", StringComparison.OrdinalIgnoreCase) + || audioEncoder.Equals("eac3", StringComparison.OrdinalIgnoreCase) + || audioEncoder.Equals("dts", StringComparison.OrdinalIgnoreCase) + || audioEncoder.Equals("mlp", StringComparison.OrdinalIgnoreCase) + || audioEncoder.Equals("truehd", StringComparison.OrdinalIgnoreCase) => 6, + // Set default max transcoding channels to 8 to prevent encoding errors due to asking for too many channels + _ => 8, + }; - if (inputChannels.HasValue) - { - resultChannels = resultChannels.HasValue - ? Math.Min(resultChannels.Value, inputChannels.Value) - : inputChannels.Value; - } + // Set resultChannels to minimum between resultChannels, TranscodingMaxAudioChannels, transcoderChannelLimit - if (isTranscodingAudio && transcoderChannelLimit.HasValue) - { - resultChannels = resultChannels.HasValue - ? Math.Min(resultChannels.Value, transcoderChannelLimit.Value) - : transcoderChannelLimit.Value; - } + resultChannels = transcoderChannelLimit < resultChannels ? transcoderChannelLimit : resultChannels ?? transcoderChannelLimit; - // Avoid transcoding to audio channels other than 1ch, 2ch, 6ch (5.1 layout) and 8ch (7.1 layout). - // https://developer.apple.com/documentation/http_live_streaming/hls_authoring_specification_for_apple_devices - if (isTranscodingAudio - && state.TranscodingType != TranscodingJobType.Progressive - && resultChannels.HasValue - && ((resultChannels.Value > 2 && resultChannels.Value < 6) || resultChannels.Value == 7)) - { - resultChannels = 2; + if (request.TranscodingMaxAudioChannels < resultChannels) + { + resultChannels = request.TranscodingMaxAudioChannels; + } + + // Avoid transcoding to audio channels other than 1ch, 2ch, 6ch (5.1 layout) and 8ch (7.1 layout). + // https://developer.apple.com/documentation/http_live_streaming/hls_authoring_specification_for_apple_devices + if (state.TranscodingType != TranscodingJobType.Progressive + && ((resultChannels > 2 && resultChannels < 6) || resultChannels == 7)) + { + resultChannels = 2; + } } return resultChannels; } - private int? GetMinValue(int? val1, int? val2) - { - if (!val1.HasValue) - { - return val2; - } - - if (!val2.HasValue) - { - return val1; - } - - return Math.Min(val1.Value, val2.Value); - } - /// /// Enforces the resolution limit. /// @@ -2814,6 +2791,13 @@ namespace MediaBrowser.Controller.MediaEncoding { return "deinterlace_qsv=mode=2"; } + else if (hwDeintSuffix.Contains("videotoolbox", StringComparison.OrdinalIgnoreCase)) + { + return string.Format( + CultureInfo.InvariantCulture, + "yadif_videotoolbox={0}:-1:0", + doubleRateDeint ? "1" : "0"); + } return string.Empty; } @@ -4450,6 +4434,75 @@ namespace MediaBrowser.Controller.MediaEncoding return (mainFilters, subFilters, overlayFilters); } + /// + /// Gets the parameter of Apple VideoToolBox filter chain. + /// + /// Encoding state. + /// Encoding options. + /// Video encoder to use. + /// The tuple contains three lists: main, sub and overlay filters. + public (List MainFilters, List SubFilters, List OverlayFilters) GetAppleVidFilterChain( + EncodingJobInfo state, + EncodingOptions options, + string vidEncoder) + { + if (!string.Equals(options.HardwareAccelerationType, "videotoolbox", StringComparison.OrdinalIgnoreCase)) + { + return (null, null, null); + } + + var swFilterChain = GetSwVidFilterChain(state, options, vidEncoder); + + if (!options.EnableHardwareEncoding) + { + return swFilterChain; + } + + if (_mediaEncoder.EncoderVersion.CompareTo(new Version("5.0.0")) < 0) + { + // All features used here requires ffmpeg 5.0 or later, fallback to software filters if using an old ffmpeg + return swFilterChain; + } + + var doDeintH264 = state.DeInterlace("h264", true) || state.DeInterlace("avc", true); + var doDeintHevc = state.DeInterlace("h265", true) || state.DeInterlace("hevc", true); + var doDeintH2645 = doDeintH264 || doDeintHevc; + var inW = state.VideoStream?.Width; + var inH = state.VideoStream?.Height; + var reqW = state.BaseRequest.Width; + var reqH = state.BaseRequest.Height; + var reqMaxW = state.BaseRequest.MaxWidth; + var reqMaxH = state.BaseRequest.MaxHeight; + var threeDFormat = state.MediaSource.Video3DFormat; + var newfilters = new List(); + var noOverlay = swFilterChain.OverlayFilters.Count == 0; + var supportsHwDeint = _mediaEncoder.SupportsFilter("yadif_videotoolbox"); + // fallback to software filters if we are using filters not supported by hardware yet. + var useHardwareFilters = noOverlay && (!doDeintH2645 || supportsHwDeint); + + if (!useHardwareFilters) + { + return swFilterChain; + } + + // ffmpeg cannot use videotoolbox to scale + var swScaleFilter = GetSwScaleFilter(state, options, vidEncoder, inW, inH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH); + newfilters.Add(swScaleFilter); + + // hwupload on videotoolbox encoders can automatically convert AVFrame into its CVPixelBuffer equivalent + // videotoolbox will automatically convert the CVPixelBuffer to a pixel format the encoder supports, so we don't have to set a pixel format explicitly here + // This will reduce CPU usage significantly on UHD videos with 10 bit colors because we bypassed the ffmpeg pixel format conversion + newfilters.Add("hwupload"); + + if (doDeintH2645) + { + var deintFilter = GetHwDeinterlaceFilter(state, options, "videotoolbox"); + newfilters.Add(deintFilter); + } + + return (newfilters, swFilterChain.SubFilters, swFilterChain.OverlayFilters); + } + /// /// Gets the parameter of video processing filters. /// @@ -4492,6 +4545,10 @@ namespace MediaBrowser.Controller.MediaEncoding { (mainFilters, subFilters, overlayFilters) = GetAmdVidFilterChain(state, options, outputVideoCodec); } + else if (string.Equals(options.HardwareAccelerationType, "videotoolbox", StringComparison.OrdinalIgnoreCase)) + { + (mainFilters, subFilters, overlayFilters) = GetAppleVidFilterChain(state, options, outputVideoCodec); + } else { (mainFilters, subFilters, overlayFilters) = GetSwVidFilterChain(state, options, outputVideoCodec); diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs index 179cabc84a..a6b5416601 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs @@ -250,8 +250,7 @@ namespace MediaBrowser.Controller.MediaEncoding } var level = GetRequestedLevel(ActualOutputVideoCodec); - if (!string.IsNullOrEmpty(level) - && double.TryParse(level, NumberStyles.Any, CultureInfo.InvariantCulture, out var result)) + if (double.TryParse(level, CultureInfo.InvariantCulture, out var result)) { return result; } @@ -645,8 +644,7 @@ namespace MediaBrowser.Controller.MediaEncoding if (!string.IsNullOrEmpty(codec)) { var value = BaseRequest.GetOption(codec, "maxrefframes"); - if (!string.IsNullOrEmpty(value) - && int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var result)) + if (int.TryParse(value, CultureInfo.InvariantCulture, out var result)) { return result; } @@ -665,8 +663,7 @@ namespace MediaBrowser.Controller.MediaEncoding if (!string.IsNullOrEmpty(codec)) { var value = BaseRequest.GetOption(codec, "videobitdepth"); - if (!string.IsNullOrEmpty(value) - && int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var result)) + if (int.TryParse(value, CultureInfo.InvariantCulture, out var result)) { return result; } @@ -685,8 +682,7 @@ namespace MediaBrowser.Controller.MediaEncoding if (!string.IsNullOrEmpty(codec)) { var value = BaseRequest.GetOption(codec, "audiobitdepth"); - if (!string.IsNullOrEmpty(value) - && int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var result)) + if (int.TryParse(value, CultureInfo.InvariantCulture, out var result)) { return result; } @@ -700,8 +696,7 @@ namespace MediaBrowser.Controller.MediaEncoding if (!string.IsNullOrEmpty(codec)) { var value = BaseRequest.GetOption(codec, "audiochannels"); - if (!string.IsNullOrEmpty(value) - && int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var result)) + if (int.TryParse(value, CultureInfo.InvariantCulture, out var result)) { return result; } diff --git a/MediaBrowser.Controller/MediaEncoding/JobLogger.cs b/MediaBrowser.Controller/MediaEncoding/JobLogger.cs index d8475f12ae..3b34af4e96 100644 --- a/MediaBrowser.Controller/MediaEncoding/JobLogger.cs +++ b/MediaBrowser.Controller/MediaEncoding/JobLogger.cs @@ -86,7 +86,7 @@ namespace MediaBrowser.Controller.MediaEncoding { var rate = parts[i + 1]; - if (float.TryParse(rate, NumberStyles.Any, CultureInfo.InvariantCulture, out var val)) + if (float.TryParse(rate, CultureInfo.InvariantCulture, out var val)) { framerate = val; } @@ -95,7 +95,7 @@ namespace MediaBrowser.Controller.MediaEncoding { var rate = part.Split('=', 2)[^1]; - if (float.TryParse(rate, NumberStyles.Any, CultureInfo.InvariantCulture, out var val)) + if (float.TryParse(rate, CultureInfo.InvariantCulture, out var val)) { framerate = val; } @@ -127,7 +127,7 @@ namespace MediaBrowser.Controller.MediaEncoding if (scale.HasValue) { - if (long.TryParse(size, NumberStyles.Any, CultureInfo.InvariantCulture, out var val)) + if (long.TryParse(size, CultureInfo.InvariantCulture, out var val)) { bytesTranscoded = val * scale.Value; } @@ -146,7 +146,7 @@ namespace MediaBrowser.Controller.MediaEncoding if (scale.HasValue) { - if (float.TryParse(rate, NumberStyles.Any, CultureInfo.InvariantCulture, out var val)) + if (float.TryParse(rate, CultureInfo.InvariantCulture, out var val)) { bitRate = (int)Math.Ceiling(val * scale.Value); } diff --git a/MediaBrowser.Controller/Session/ISessionController.cs b/MediaBrowser.Controller/Session/ISessionController.cs index b38ee11462..c8b29aa1f3 100644 --- a/MediaBrowser.Controller/Session/ISessionController.cs +++ b/MediaBrowser.Controller/Session/ISessionController.cs @@ -1,5 +1,3 @@ -#nullable disable - #pragma warning disable CS1591 using System; diff --git a/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs b/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs index 1030cf0558..c8912807ea 100644 --- a/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs +++ b/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs @@ -169,12 +169,9 @@ namespace MediaBrowser.LocalMetadata.Parsers { var text = reader.ReadElementContentAsString(); - if (!string.IsNullOrEmpty(text)) + if (float.TryParse(text, CultureInfo.InvariantCulture, out var value)) { - if (float.TryParse(text, NumberStyles.Any, CultureInfo.InvariantCulture, out var value)) - { - item.CriticRating = value; - } + item.CriticRating = value; } break; diff --git a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs index 8479b7d50d..540d50bf15 100644 --- a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs +++ b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs @@ -56,6 +56,7 @@ namespace MediaBrowser.MediaEncoding.Encoder "libvpx", "libvpx-vp9", "aac", + "aac_at", "libfdk_aac", "ac3", "libmp3lame", @@ -106,7 +107,10 @@ namespace MediaBrowser.MediaEncoding.Encoder // vulkan "libplacebo", "scale_vulkan", - "overlay_vulkan" + "overlay_vulkan", + "hwupload_vaapi", + // videotoolbox + "yadif_videotoolbox" }; private static readonly IReadOnlyDictionary _filterOptionsDict = new Dictionary @@ -273,7 +277,7 @@ namespace MediaBrowser.MediaEncoding.Encoder if (match.Success) { - if (Version.TryParse(match.Groups[1].Value, out var result)) + if (Version.TryParse(match.Groups[1].ValueSpan, out var result)) { return result; } @@ -323,8 +327,8 @@ namespace MediaBrowser.MediaEncoding.Encoder RegexOptions.Multiline)) { var version = new Version( - int.Parse(match.Groups["major"].Value, CultureInfo.InvariantCulture), - int.Parse(match.Groups["minor"].Value, CultureInfo.InvariantCulture)); + int.Parse(match.Groups["major"].ValueSpan, CultureInfo.InvariantCulture), + int.Parse(match.Groups["minor"].ValueSpan, CultureInfo.InvariantCulture)); map.Add(match.Groups["name"].Value, version); } diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs index 99310a75dd..8b82795883 100644 --- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs +++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs @@ -97,12 +97,9 @@ namespace MediaBrowser.MediaEncoding.Probing { info.Container = NormalizeFormat(data.Format.FormatName); - if (!string.IsNullOrEmpty(data.Format.BitRate)) + if (int.TryParse(data.Format.BitRate, CultureInfo.InvariantCulture, out var value)) { - if (int.TryParse(data.Format.BitRate, NumberStyles.Any, CultureInfo.InvariantCulture, out var value)) - { - info.Bitrate = value; - } + info.Bitrate = value; } } @@ -561,8 +558,8 @@ namespace MediaBrowser.MediaEncoding.Probing } } - if (string.IsNullOrWhiteSpace(name) || - string.IsNullOrWhiteSpace(value)) + if (string.IsNullOrWhiteSpace(name) + || string.IsNullOrWhiteSpace(value)) { return null; } @@ -674,9 +671,9 @@ namespace MediaBrowser.MediaEncoding.Probing stream.Channels = streamInfo.Channels; - if (int.TryParse(streamInfo.SampleRate, NumberStyles.Any, CultureInfo.InvariantCulture, out var value)) + if (int.TryParse(streamInfo.SampleRate, CultureInfo.InvariantCulture, out var sampleRate)) { - stream.SampleRate = value; + stream.SampleRate = sampleRate; } stream.ChannelLayout = ParseChannelLayout(streamInfo.ChannelLayout); @@ -853,22 +850,18 @@ namespace MediaBrowser.MediaEncoding.Probing // Get stream bitrate var bitrate = 0; - if (!string.IsNullOrEmpty(streamInfo.BitRate)) + if (int.TryParse(streamInfo.BitRate, CultureInfo.InvariantCulture, out var value)) { - if (int.TryParse(streamInfo.BitRate, NumberStyles.Any, CultureInfo.InvariantCulture, out var value)) - { - bitrate = value; - } + bitrate = value; } // The bitrate info of FLAC musics and some videos is included in formatInfo. if (bitrate == 0 && formatInfo is not null - && !string.IsNullOrEmpty(formatInfo.BitRate) && (stream.Type == MediaStreamType.Video || (isAudio && stream.Type == MediaStreamType.Audio))) { // If the stream info doesn't have a bitrate get the value from the media format info - if (int.TryParse(formatInfo.BitRate, NumberStyles.Any, CultureInfo.InvariantCulture, out var value)) + if (int.TryParse(formatInfo.BitRate, CultureInfo.InvariantCulture, out value)) { bitrate = value; } @@ -972,8 +965,8 @@ namespace MediaBrowser.MediaEncoding.Probing var parts = (original ?? string.Empty).Split(':'); if (!(parts.Length == 2 - && int.TryParse(parts[0], NumberStyles.Any, CultureInfo.InvariantCulture, out var width) - && int.TryParse(parts[1], NumberStyles.Any, CultureInfo.InvariantCulture, out var height) + && int.TryParse(parts[0], CultureInfo.InvariantCulture, out var width) + && int.TryParse(parts[1], CultureInfo.InvariantCulture, out var height) && width > 0 && height > 0)) { @@ -1117,7 +1110,7 @@ namespace MediaBrowser.MediaEncoding.Probing } var duration = GetDictionaryValue(streamInfo.Tags, "DURATION-eng") ?? GetDictionaryValue(streamInfo.Tags, "DURATION"); - if (!string.IsNullOrEmpty(duration) && TimeSpan.TryParse(duration, out var parsedDuration)) + if (TimeSpan.TryParse(duration, out var parsedDuration)) { return parsedDuration.TotalSeconds; } @@ -1446,7 +1439,7 @@ namespace MediaBrowser.MediaEncoding.Probing // Limit accuracy to milliseconds to match xml saving var secondsString = chapter.StartTime; - if (double.TryParse(secondsString, NumberStyles.Any, CultureInfo.InvariantCulture, out var seconds)) + if (double.TryParse(secondsString, CultureInfo.InvariantCulture, out var seconds)) { var ms = Math.Round(TimeSpan.FromSeconds(seconds).TotalMilliseconds); info.StartPositionTicks = TimeSpan.FromMilliseconds(ms).Ticks; diff --git a/MediaBrowser.Model/Dlna/ConditionProcessor.cs b/MediaBrowser.Model/Dlna/ConditionProcessor.cs index 5734224167..00b406bbe6 100644 --- a/MediaBrowser.Model/Dlna/ConditionProcessor.cs +++ b/MediaBrowser.Model/Dlna/ConditionProcessor.cs @@ -136,7 +136,7 @@ namespace MediaBrowser.Model.Dlna return !condition.IsRequired; } - if (int.TryParse(condition.Value, NumberStyles.Any, CultureInfo.InvariantCulture, out var expected)) + if (int.TryParse(condition.Value, CultureInfo.InvariantCulture, out var expected)) { switch (condition.Condition) { @@ -212,7 +212,7 @@ namespace MediaBrowser.Model.Dlna return !condition.IsRequired; } - if (double.TryParse(condition.Value, NumberStyles.Any, CultureInfo.InvariantCulture, out var expected)) + if (double.TryParse(condition.Value, CultureInfo.InvariantCulture, out var expected)) { switch (condition.Condition) { diff --git a/MediaBrowser.Model/Dlna/SortCriteria.cs b/MediaBrowser.Model/Dlna/SortCriteria.cs index 7fef16e535..7df53c6d19 100644 --- a/MediaBrowser.Model/Dlna/SortCriteria.cs +++ b/MediaBrowser.Model/Dlna/SortCriteria.cs @@ -9,7 +9,7 @@ namespace MediaBrowser.Model.Dlna { public SortCriteria(string sortOrder) { - if (!string.IsNullOrEmpty(sortOrder) && Enum.TryParse(sortOrder, true, out var sortOrderValue)) + if (Enum.TryParse(sortOrder, true, out var sortOrderValue)) { SortOrder = sortOrderValue; } diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs index eb6d602959..ab81bfb34c 100644 --- a/MediaBrowser.Model/Dlna/StreamBuilder.cs +++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs @@ -551,8 +551,7 @@ namespace MediaBrowser.Model.Dlna } playlistItem.TranscodeSeekInfo = transcodingProfile.TranscodeSeekInfo; - if (!string.IsNullOrEmpty(transcodingProfile.MaxAudioChannels) - && int.TryParse(transcodingProfile.MaxAudioChannels, NumberStyles.Any, CultureInfo.InvariantCulture, out int transcodingMaxAudioChannels)) + if (int.TryParse(transcodingProfile.MaxAudioChannels, CultureInfo.InvariantCulture, out int transcodingMaxAudioChannels)) { playlistItem.TranscodingMaxAudioChannels = transcodingMaxAudioChannels; } @@ -1607,7 +1606,7 @@ namespace MediaBrowser.Model.Dlna continue; } - if (int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var num)) + if (int.TryParse(value, CultureInfo.InvariantCulture, out var num)) { if (condition.Condition == ProfileConditionType.Equals) { @@ -1633,7 +1632,7 @@ namespace MediaBrowser.Model.Dlna continue; } - if (int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var num)) + if (int.TryParse(value, CultureInfo.InvariantCulture, out var num)) { if (condition.Condition == ProfileConditionType.Equals) { @@ -1669,7 +1668,7 @@ namespace MediaBrowser.Model.Dlna } } - if (int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var num)) + if (int.TryParse(value, CultureInfo.InvariantCulture, out var num)) { if (condition.Condition == ProfileConditionType.Equals) { @@ -1793,7 +1792,7 @@ namespace MediaBrowser.Model.Dlna } } - if (int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var num)) + if (int.TryParse(value, CultureInfo.InvariantCulture, out var num)) { if (condition.Condition == ProfileConditionType.Equals) { @@ -1829,7 +1828,7 @@ namespace MediaBrowser.Model.Dlna } } - if (int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var num)) + if (int.TryParse(value, CultureInfo.InvariantCulture, out var num)) { if (condition.Condition == ProfileConditionType.Equals) { @@ -1919,7 +1918,7 @@ namespace MediaBrowser.Model.Dlna continue; } - if (int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var num)) + if (int.TryParse(value, CultureInfo.InvariantCulture, out var num)) { if (condition.Condition == ProfileConditionType.Equals) { @@ -1945,7 +1944,7 @@ namespace MediaBrowser.Model.Dlna continue; } - if (int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var num)) + if (int.TryParse(value, CultureInfo.InvariantCulture, out var num)) { if (condition.Condition == ProfileConditionType.Equals) { @@ -1971,7 +1970,7 @@ namespace MediaBrowser.Model.Dlna continue; } - if (float.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var num)) + if (float.TryParse(value, CultureInfo.InvariantCulture, out var num)) { if (condition.Condition == ProfileConditionType.Equals) { @@ -1997,7 +1996,7 @@ namespace MediaBrowser.Model.Dlna continue; } - if (int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var num)) + if (int.TryParse(value, CultureInfo.InvariantCulture, out var num)) { if (condition.Condition == ProfileConditionType.Equals) { @@ -2023,7 +2022,7 @@ namespace MediaBrowser.Model.Dlna continue; } - if (int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var num)) + if (int.TryParse(value, CultureInfo.InvariantCulture, out var num)) { if (condition.Condition == ProfileConditionType.Equals) { diff --git a/MediaBrowser.Model/Dlna/StreamInfo.cs b/MediaBrowser.Model/Dlna/StreamInfo.cs index 3b55099079..93ace43df4 100644 --- a/MediaBrowser.Model/Dlna/StreamInfo.cs +++ b/MediaBrowser.Model/Dlna/StreamInfo.cs @@ -922,12 +922,8 @@ namespace MediaBrowser.Model.Dlna public int? GetTargetVideoBitDepth(string codec) { var value = GetOption(codec, "videobitdepth"); - if (string.IsNullOrEmpty(value)) - { - return null; - } - if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result)) + if (int.TryParse(value, CultureInfo.InvariantCulture, out var result)) { return result; } @@ -938,12 +934,8 @@ namespace MediaBrowser.Model.Dlna public int? GetTargetAudioBitDepth(string codec) { var value = GetOption(codec, "audiobitdepth"); - if (string.IsNullOrEmpty(value)) - { - return null; - } - if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result)) + if (int.TryParse(value, CultureInfo.InvariantCulture, out var result)) { return result; } @@ -954,12 +946,8 @@ namespace MediaBrowser.Model.Dlna public double? GetTargetVideoLevel(string codec) { var value = GetOption(codec, "level"); - if (string.IsNullOrEmpty(value)) - { - return null; - } - if (double.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var result)) + if (double.TryParse(value, CultureInfo.InvariantCulture, out var result)) { return result; } @@ -970,12 +958,8 @@ namespace MediaBrowser.Model.Dlna public int? GetTargetRefFrames(string codec) { var value = GetOption(codec, "maxrefframes"); - if (string.IsNullOrEmpty(value)) - { - return null; - } - if (int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var result)) + if (int.TryParse(value, CultureInfo.InvariantCulture, out var result)) { return result; } diff --git a/MediaBrowser.Model/Net/MimeTypes.cs b/MediaBrowser.Model/Net/MimeTypes.cs index 8157dc0c24..5a1871070d 100644 --- a/MediaBrowser.Model/Net/MimeTypes.cs +++ b/MediaBrowser.Model/Net/MimeTypes.cs @@ -117,7 +117,9 @@ namespace MediaBrowser.Model.Net // Type image { "image/jpeg", ".jpg" }, + { "image/tiff", ".tiff" }, { "image/x-png", ".png" }, + { "image/x-icon", ".ico" }, // Type text { "text/plain", ".txt" }, @@ -178,5 +180,8 @@ namespace MediaBrowser.Model.Net var extension = Model.MimeTypes.GetMimeTypeExtensions(mimeType).FirstOrDefault(); return string.IsNullOrEmpty(extension) ? null : "." + extension; } + + public static bool IsImage(ReadOnlySpan mimeType) + => mimeType.StartsWith("image/", StringComparison.OrdinalIgnoreCase); } } diff --git a/MediaBrowser.Model/Tasks/ITaskManager.cs b/MediaBrowser.Model/Tasks/ITaskManager.cs index 13bebc479e..5b55667e82 100644 --- a/MediaBrowser.Model/Tasks/ITaskManager.cs +++ b/MediaBrowser.Model/Tasks/ITaskManager.cs @@ -9,9 +9,9 @@ namespace MediaBrowser.Model.Tasks { public interface ITaskManager : IDisposable { - event EventHandler> TaskExecuting; + event EventHandler>? TaskExecuting; - event EventHandler TaskCompleted; + event EventHandler? TaskCompleted; /// /// Gets the list of Scheduled Tasks. diff --git a/MediaBrowser.Model/Users/UserPolicy.cs b/MediaBrowser.Model/Users/UserPolicy.cs index 3634d07058..cc7d57eb0a 100644 --- a/MediaBrowser.Model/Users/UserPolicy.cs +++ b/MediaBrowser.Model/Users/UserPolicy.cs @@ -13,6 +13,7 @@ namespace MediaBrowser.Model.Users public UserPolicy() { IsHidden = true; + EnableCollectionManagement = false; EnableContentDeletion = false; EnableContentDeletionFromFolders = Array.Empty(); @@ -35,6 +36,7 @@ namespace MediaBrowser.Model.Users EnableSharedDeviceControl = true; BlockedTags = Array.Empty(); + AllowedTags = Array.Empty(); BlockUnratedItems = Array.Empty(); EnableUserPreferenceAccess = true; @@ -72,6 +74,12 @@ namespace MediaBrowser.Model.Users /// true if this instance is hidden; otherwise, false. public bool IsHidden { get; set; } + /// + /// Gets or sets a value indicating whether this instance can manage collections. + /// + /// true if this instance is hidden; otherwise, false. + public bool EnableCollectionManagement { get; set; } + /// /// Gets or sets a value indicating whether this instance is disabled. /// @@ -86,6 +94,8 @@ namespace MediaBrowser.Model.Users public string[] BlockedTags { get; set; } + public string[] AllowedTags { get; set; } + public bool EnableUserPreferenceAccess { get; set; } public AccessSchedule[] AccessSchedules { get; set; } diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs index ffae772008..0605b0bd78 100644 --- a/MediaBrowser.Providers/Manager/MetadataService.cs +++ b/MediaBrowser.Providers/Manager/MetadataService.cs @@ -151,7 +151,6 @@ namespace MediaBrowser.Providers.Manager ApplySearchResult(id, refreshOptions.SearchResult); } - // await FindIdentities(id, cancellationToken).ConfigureAwait(false); id.IsAutomated = refreshOptions.IsAutomated; var result = await RefreshWithProviders(metadataResult, id, refreshOptions, providers, ImageProvider, cancellationToken).ConfigureAwait(false); @@ -334,6 +333,12 @@ namespace MediaBrowser.Providers.Manager updateType |= UpdateCumulativeRunTimeTicks(item, children); updateType |= UpdateDateLastMediaAdded(item, children); + // don't update user-changeable metadata for locked items + if (item.IsLocked) + { + return updateType; + } + if (EnableUpdatingPremiereDateFromChildren) { updateType |= UpdatePremiereDate(item, children); @@ -375,7 +380,7 @@ namespace MediaBrowser.Providers.Manager if (!folder.RunTimeTicks.HasValue || folder.RunTimeTicks.Value != ticks) { folder.RunTimeTicks = ticks; - return ItemUpdateType.MetadataEdit; + return ItemUpdateType.MetadataImport; } } diff --git a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs index 74210b1f22..19b594c1c8 100644 --- a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs +++ b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs @@ -105,7 +105,10 @@ namespace MediaBrowser.Providers.MediaInfo audio.RunTimeTicks = mediaInfo.RunTimeTicks; audio.Size = mediaInfo.Size; - FetchDataFromTags(audio); + if (!audio.IsLocked) + { + FetchDataFromTags(audio); + } _itemRepo.SaveMediaStreams(audio.Id, mediaInfo.MediaStreams, cancellationToken); } diff --git a/MediaBrowser.Providers/Music/AlbumMetadataService.cs b/MediaBrowser.Providers/Music/AlbumMetadataService.cs index 58cd23aa34..3476e70009 100644 --- a/MediaBrowser.Providers/Music/AlbumMetadataService.cs +++ b/MediaBrowser.Providers/Music/AlbumMetadataService.cs @@ -54,6 +54,12 @@ namespace MediaBrowser.Providers.Music { var updateType = base.UpdateMetadataFromChildren(item, children, isFullRefresh, currentUpdateType); + // don't update user-changeable metadata for locked items + if (item.IsLocked) + { + return updateType; + } + if (isFullRefresh || currentUpdateType > ItemUpdateType.None) { if (!item.LockedFields.Contains(MetadataField.Name)) diff --git a/MediaBrowser.Providers/Plugins/AudioDb/Configuration/config.html b/MediaBrowser.Providers/Plugins/AudioDb/Configuration/config.html index eab252005f..2093effca6 100644 --- a/MediaBrowser.Providers/Plugins/AudioDb/Configuration/config.html +++ b/MediaBrowser.Providers/Plugins/AudioDb/Configuration/config.html @@ -1,12 +1,13 @@  - AudioDB + TheAudioDB -
+
+

TheAudioDB

public class PluginConfiguration : BasePluginConfiguration { - private const string DefaultServer = "musicbrainz.org"; + /// + /// The default server URL. + /// + public const string DefaultServer = "https://musicbrainz.org"; - private const double DefaultRateLimit = 1.0; + /// + /// The default rate limit. + /// + public const double DefaultRateLimit = 1.0; private string _server = DefaultServer; private double _rateLimit = DefaultRateLimit; /// - /// Gets or sets the server url. + /// Gets or sets the server URL. /// public string Server { diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/config.html b/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/config.html index 6f1296bb77..24f2ac0ca9 100644 --- a/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/config.html +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/config.html @@ -4,17 +4,18 @@ MusicBrainz -
+
- +

MusicBrainz

+
This can be a mirror of the official server or even a custom server.
- -
Span of time between requests in milliseconds. The official server is limited to one request every two seconds.
+ +
Span of time between requests in seconds. The official server is limited to one request every seconds.