Merge branch 'jellyfin:master' into master

This commit is contained in:
clay 2023-02-22 02:04:07 -05:00 committed by GitHub
commit 65f1a7e183
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
224 changed files with 2286 additions and 2055 deletions

View file

@ -27,11 +27,11 @@ jobs:
dotnet-version: '7.0.x' dotnet-version: '7.0.x'
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@3ebbd71c74ef574dbc558c82f70e52732c8b44fe # v2 uses: github/codeql-action/init@17573ee1cc1b9d061760f3a006fc4aac4f944fd5 # v2
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
queries: +security-extended queries: +security-extended
- name: Autobuild - name: Autobuild
uses: github/codeql-action/autobuild@3ebbd71c74ef574dbc558c82f70e52732c8b44fe # v2 uses: github/codeql-action/autobuild@17573ee1cc1b9d061760f3a006fc4aac4f944fd5 # v2
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@3ebbd71c74ef574dbc558c82f70e52732c8b44fe # v2 uses: github/codeql-action/analyze@17573ee1cc1b9d061760f3a006fc4aac4f944fd5 # v2

View file

@ -17,7 +17,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Notify as seen - name: Notify as seen
uses: peter-evans/create-or-update-comment@5adcb0bb0f9fb3f95ef05400558bdb3f329ee808 # tag=v2 uses: peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d # v2
with: with:
token: ${{ secrets.JF_BOT_TOKEN }} token: ${{ secrets.JF_BOT_TOKEN }}
comment-id: ${{ github.event.comment.id }} comment-id: ${{ github.event.comment.id }}
@ -43,7 +43,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Notify as seen - 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 }} if: ${{ github.event.comment != null }}
with: with:
token: ${{ secrets.JF_BOT_TOKEN }} token: ${{ secrets.JF_BOT_TOKEN }}
@ -58,7 +58,7 @@ jobs:
- name: Notify as running - name: Notify as running
id: comment_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 }} if: ${{ github.event.comment != null }}
with: with:
token: ${{ secrets.JF_BOT_TOKEN }} token: ${{ secrets.JF_BOT_TOKEN }}
@ -93,7 +93,7 @@ jobs:
exit ${retcode} exit ${retcode}
- name: Notify with result success - 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() }} if: ${{ github.event.comment != null && success() }}
with: with:
token: ${{ secrets.JF_BOT_TOKEN }} token: ${{ secrets.JF_BOT_TOKEN }}
@ -108,7 +108,7 @@ jobs:
reactions: hooray reactions: hooray
- name: Notify with result failure - 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() }} if: ${{ github.event.comment != null && failure() }}
with: with:
token: ${{ secrets.JF_BOT_TOKEN }} token: ${{ secrets.JF_BOT_TOKEN }}

View file

@ -103,14 +103,14 @@ jobs:
body="${body//$'\r'/'%0D'}" body="${body//$'\r'/'%0D'}"
echo ::set-output name=body::$body echo ::set-output name=body::$body
- name: Find difference comment - name: Find difference comment
uses: peter-evans/find-comment@81e2da3af01c92f83cb927cf3ace0e085617c556 # v2 uses: peter-evans/find-comment@85a676a52594b4481e0532825a2d8906ef96dac2 # v2
id: find-comment id: find-comment
with: with:
issue-number: ${{ github.event.pull_request.number }} issue-number: ${{ github.event.pull_request.number }}
direction: last direction: last
body-includes: openapi-diff-workflow-comment body-includes: openapi-diff-workflow-comment
- name: Reply or edit difference comment (changed) - 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 != '' }} if: ${{ steps.read-diff.outputs.body != '' }}
with: with:
issue-number: ${{ github.event.pull_request.number }} issue-number: ${{ github.event.pull_request.number }}
@ -125,7 +125,7 @@ jobs:
</details> </details>
- name: Edit difference comment (unchanged) - 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 != '' }} if: ${{ steps.read-diff.outputs.body == '' && steps.find-comment.outputs.comment-id != '' }}
with: with:
issue-number: ${{ github.event.pull_request.number }} issue-number: ${{ github.event.pull_request.number }}

3
.npmrc
View file

@ -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

View file

@ -232,3 +232,4 @@
- [Matthew Jones](https://github.com/matthew-jones-uk) - [Matthew Jones](https://github.com/matthew-jones-uk)
- [Jakob Kukla](https://github.com/jakobkukla) - [Jakob Kukla](https://github.com/jakobkukla)
- [Utku Özdemir](https://github.com/utkuozdemir) - [Utku Özdemir](https://github.com/utkuozdemir)
- [JPUC1143](https://github.com/Jpuc1143/)

View file

@ -8,7 +8,7 @@
<ItemGroup Label="Package Dependencies"> <ItemGroup Label="Package Dependencies">
<PackageVersion Include="AutoFixture.AutoMoq" Version="4.17.0" /> <PackageVersion Include="AutoFixture.AutoMoq" Version="4.17.0" />
<PackageVersion Include="AutoFixture.Xunit2" Version="4.17.0" /> <PackageVersion Include="AutoFixture.Xunit2" Version="4.17.0" />
<PackageVersion Include="AutoFixture" Version="4.17.0" /> <PackageVersion Include="AutoFixture" Version="4.18.0" />
<PackageVersion Include="BDInfo" Version="0.7.6.2" /> <PackageVersion Include="BDInfo" Version="0.7.6.2" />
<PackageVersion Include="BlurHashSharp.SkiaSharp" Version="1.2.0" /> <PackageVersion Include="BlurHashSharp.SkiaSharp" Version="1.2.0" />
<PackageVersion Include="BlurHashSharp" Version="1.2.0" /> <PackageVersion Include="BlurHashSharp" Version="1.2.0" />
@ -23,29 +23,29 @@
<PackageVersion Include="libse" Version="3.6.10" /> <PackageVersion Include="libse" Version="3.6.10" />
<PackageVersion Include="LrcParser" Version="2022.529.1" /> <PackageVersion Include="LrcParser" Version="2022.529.1" />
<PackageVersion Include="MetaBrainz.MusicBrainz" Version="5.0.0" /> <PackageVersion Include="MetaBrainz.MusicBrainz" Version="5.0.0" />
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="7.0.2" /> <PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="7.0.3" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="7.0.2" /> <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="7.0.3" />
<PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4" /> <PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.2" /> <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.3" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="7.0.2" /> <PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="7.0.3" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.2" /> <PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.3" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.2" /> <PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.3" />
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="7.0.0" /> <PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="7.0.0" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="7.0.0" /> <PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="7.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="7.0.0" /> <PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="7.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="7.0.2" /> <PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="7.0.3" />
<PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="7.0.0" /> <PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="7.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="7.0.0" /> <PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="7.0.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="7.0.0" /> <PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="7.0.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" /> <PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="7.0.2" /> <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="7.0.3" />
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="7.0.2" /> <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="7.0.3" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="7.0.0" /> <PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="7.0.0" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="7.0.0" /> <PackageVersion Include="Microsoft.Extensions.Http" Version="7.0.0" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="7.0.0" /> <PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="7.0.0" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="7.0.0" /> <PackageVersion Include="Microsoft.Extensions.Logging" Version="7.0.0" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="7.0.0" /> <PackageVersion Include="Microsoft.Extensions.Options" Version="7.0.1" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.4.1" /> <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.5.0" />
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="1.1.1" /> <PackageVersion Include="Microsoft.SourceLink.GitHub" Version="1.1.1" />
<PackageVersion Include="MimeTypes" Version="2.4.0" /> <PackageVersion Include="MimeTypes" Version="2.4.0" />
<PackageVersion Include="Mono.Nat" Version="3.0.4" /> <PackageVersion Include="Mono.Nat" Version="3.0.4" />
@ -53,9 +53,9 @@
<PackageVersion Include="NEbml" Version="0.11.0" /> <PackageVersion Include="NEbml" Version="0.11.0" />
<PackageVersion Include="Newtonsoft.Json" Version="13.0.2" /> <PackageVersion Include="Newtonsoft.Json" Version="13.0.2" />
<PackageVersion Include="PlaylistsNET" Version="1.3.1" /> <PackageVersion Include="PlaylistsNET" Version="1.3.1" />
<PackageVersion Include="prometheus-net.AspNetCore" Version="7.0.0" /> <PackageVersion Include="prometheus-net.AspNetCore" Version="8.0.0" />
<PackageVersion Include="prometheus-net.DotNetRuntime" Version="4.4.0" /> <PackageVersion Include="prometheus-net.DotNetRuntime" Version="4.4.0" />
<PackageVersion Include="prometheus-net" Version="7.0.0" /> <PackageVersion Include="prometheus-net" Version="8.0.0" />
<PackageVersion Include="Serilog.AspNetCore" Version="6.1.0" /> <PackageVersion Include="Serilog.AspNetCore" Version="6.1.0" />
<PackageVersion Include="Serilog.Enrichers.Thread" Version="3.1.0" /> <PackageVersion Include="Serilog.Enrichers.Thread" Version="3.1.0" />
<PackageVersion Include="Serilog.Settings.Configuration" Version="3.4.0" /> <PackageVersion Include="Serilog.Settings.Configuration" Version="3.4.0" />
@ -77,7 +77,7 @@
<PackageVersion Include="System.Globalization" Version="4.3.0" /> <PackageVersion Include="System.Globalization" Version="4.3.0" />
<PackageVersion Include="System.Linq.Async" Version="6.0.1" /> <PackageVersion Include="System.Linq.Async" Version="6.0.1" />
<PackageVersion Include="System.Text.Encoding.CodePages" Version="7.0.0" /> <PackageVersion Include="System.Text.Encoding.CodePages" Version="7.0.0" />
<PackageVersion Include="System.Text.Json" Version="7.0.1" /> <PackageVersion Include="System.Text.Json" Version="7.0.2" />
<PackageVersion Include="System.Threading.Tasks.Dataflow" Version="7.0.0" /> <PackageVersion Include="System.Threading.Tasks.Dataflow" Version="7.0.0" />
<PackageVersion Include="TagLibSharp" Version="2.3.0" /> <PackageVersion Include="TagLibSharp" Version="2.3.0" />
<PackageVersion Include="TMDbLib" Version="2.0.0" /> <PackageVersion Include="TMDbLib" Version="2.0.0" />

View file

@ -147,11 +147,16 @@ namespace Emby.Dlna.Server
} }
} }
private string GetFriendlyName() internal string GetFriendlyName()
{ {
if (string.IsNullOrEmpty(_profile.FriendlyName)) if (string.IsNullOrEmpty(_profile.FriendlyName))
{ {
return "Jellyfin - " + _serverName; return _serverName;
}
if (!_profile.FriendlyName.Contains("${HostName}", StringComparison.OrdinalIgnoreCase))
{
return _profile.FriendlyName;
} }
var characterList = new List<char>(); var characterList = new List<char>();
@ -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); return _profile.FriendlyName.Replace("${HostName}", serverName, StringComparison.OrdinalIgnoreCase);
var name = _profile.FriendlyName?.Replace("${HostName}", serverName, StringComparison.OrdinalIgnoreCase);
return name ?? string.Empty;
} }
private void AppendIconList(StringBuilder builder) private void AppendIconList(StringBuilder builder)

View file

@ -3,6 +3,7 @@ using System.Globalization;
using System.IO; using System.IO;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using Emby.Naming.Common; using Emby.Naming.Common;
using Jellyfin.Extensions;
namespace Emby.Naming.Audio namespace Emby.Naming.Audio
{ {
@ -58,13 +59,7 @@ namespace Emby.Naming.Audio
var tmp = trimmedFilename.Slice(prefix.Length).Trim(); var tmp = trimmedFilename.Slice(prefix.Length).Trim();
int index = tmp.IndexOf(' '); if (int.TryParse(tmp.LeftPart(' '), CultureInfo.InvariantCulture, out _))
if (index != -1)
{
tmp = tmp.Slice(0, index);
}
if (int.TryParse(tmp, NumberStyles.Integer, CultureInfo.InvariantCulture, out _))
{ {
return true; return true;
} }

View file

@ -32,7 +32,7 @@ namespace Emby.Naming.AudioBook
var fileName = Path.GetFileNameWithoutExtension(path); var fileName = Path.GetFileNameWithoutExtension(path);
foreach (var expression in _options.AudioBookPartsExpressions) 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 (match.Success)
{ {
if (!result.ChapterNumber.HasValue) if (!result.ChapterNumber.HasValue)
@ -40,7 +40,7 @@ namespace Emby.Naming.AudioBook
var value = match.Groups["chapter"]; var value = match.Groups["chapter"];
if (value.Success) 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; result.ChapterNumber = intValue;
} }
@ -52,7 +52,7 @@ namespace Emby.Naming.AudioBook
var value = match.Groups["part"]; var value = match.Groups["part"];
if (value.Success) 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; result.PartNumber = intValue;
} }

View file

@ -30,7 +30,7 @@ namespace Emby.Naming.AudioBook
AudioBookNameParserResult result = default; AudioBookNameParserResult result = default;
foreach (var expression in _options.AudioBookNamesExpressions) 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 (match.Success)
{ {
if (result.Name is null) if (result.Name is null)
@ -47,7 +47,7 @@ namespace Emby.Naming.AudioBook
var value = match.Groups["year"]; var value = match.Groups["year"];
if (value.Success) 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; result.Year = intValue;
} }

View file

@ -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(@".*[\\\/]((?<seriesname>[^\\/]+?)\s)?[Ss](?:eason)?\s*(?<seasonnumber>[0-9]+)\s+[Ee](?:pisode)?\s*(?<epnumber>[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. // so we make sure this one gets tested first.
// "Foo Bar 889" // "Foo Bar 889"
new EpisodeExpression(@".*[\\\/](?![Ee]pisode)(?<seriesname>[\w\s]+?)\s(?<epnumber>[0-9]{1,4})(-(?<endingepnumber>[0-9]{2,4}))*[^\\\/x]*$") new EpisodeExpression(@".*[\\\/](?![Ee]pisode)(?<seriesname>[\w\s]+?)\s(?<epnumber>[0-9]{1,4})(-(?<endingepnumber>[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[] VideoExtraRules = new[]
{ {
new ExtraRule( new ExtraRule(
@ -797,16 +795,6 @@ namespace Emby.Naming.Common
/// </summary> /// </summary>
public EpisodeExpression[] EpisodeExpressions { get; set; } public EpisodeExpression[] EpisodeExpressions { get; set; }
/// <summary>
/// Gets or sets list of raw episode without season regular expressions strings.
/// </summary>
public string[] EpisodeWithoutSeasonExpressions { get; set; }
/// <summary>
/// Gets or sets list of raw multi-part episodes regular expressions strings.
/// </summary>
public string[] EpisodeMultiPartExpressions { get; set; }
/// <summary> /// <summary>
/// Gets or sets list of video file extensions. /// Gets or sets list of video file extensions.
/// </summary> /// </summary>
@ -877,16 +865,6 @@ namespace Emby.Naming.Common
/// </summary> /// </summary>
public Regex[] CleanStringRegexes { get; private set; } = Array.Empty<Regex>(); public Regex[] CleanStringRegexes { get; private set; } = Array.Empty<Regex>();
/// <summary>
/// Gets list of episode without season regular expressions.
/// </summary>
public Regex[] EpisodeWithoutSeasonRegexes { get; private set; } = Array.Empty<Regex>();
/// <summary>
/// Gets list of multi-part episode regular expressions.
/// </summary>
public Regex[] EpisodeMultiPartRegexes { get; private set; } = Array.Empty<Regex>();
/// <summary> /// <summary>
/// Compiles raw regex strings into regexes. /// Compiles raw regex strings into regexes.
/// </summary> /// </summary>
@ -894,8 +872,6 @@ namespace Emby.Naming.Common
{ {
CleanDateTimeRegexes = CleanDateTimes.Select(Compile).ToArray(); CleanDateTimeRegexes = CleanDateTimes.Select(Compile).ToArray();
CleanStringRegexes = CleanStrings.Select(Compile).ToArray(); CleanStringRegexes = CleanStrings.Select(Compile).ToArray();
EpisodeWithoutSeasonRegexes = EpisodeWithoutSeasonExpressions.Select(Compile).ToArray();
EpisodeMultiPartRegexes = EpisodeMultiPartExpressions.Select(Compile).ToArray();
} }
private Regex Compile(string exp) private Regex Compile(string exp)

View file

@ -113,7 +113,7 @@ namespace Emby.Naming.TV
if (expression.DateTimeFormats.Length > 0) if (expression.DateTimeFormats.Length > 0)
{ {
if (DateTime.TryParseExact( if (DateTime.TryParseExact(
match.Groups[0].Value, match.Groups[0].ValueSpan,
expression.DateTimeFormats, expression.DateTimeFormats,
CultureInfo.InvariantCulture, CultureInfo.InvariantCulture,
DateTimeStyles.None, DateTimeStyles.None,
@ -125,7 +125,7 @@ namespace Emby.Naming.TV
result.Success = true; 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.Year = date.Year;
result.Month = date.Month; result.Month = date.Month;
@ -138,12 +138,12 @@ namespace Emby.Naming.TV
} }
else if (expression.IsNamed) 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; 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; result.EpisodeNumber = num;
} }
@ -158,7 +158,7 @@ namespace Emby.Naming.TV
if (nextIndex >= name.Length if (nextIndex >= name.Length
|| !"0123456789iIpP".Contains(name[nextIndex], StringComparison.Ordinal)) || !"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; result.EndingEpisodeNumber = num;
} }
@ -170,12 +170,12 @@ namespace Emby.Naming.TV
} }
else 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; 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; result.EpisodeNumber = num;
} }

View file

@ -14,7 +14,7 @@ namespace Emby.Naming.TV
/// Used for removing separators between words, i.e turns "The_show" into "The show" while /// Used for removing separators between words, i.e turns "The_show" into "The show" while
/// preserving namings like "S.H.O.W". /// preserving namings like "S.H.O.W".
/// </summary> /// </summary>
private static readonly Regex _seriesNameRegex = new Regex(@"((?<a>[^\._]{2,})[\._]*)|([\._](?<b>[^\._]{2,}))"); private static readonly Regex _seriesNameRegex = new Regex(@"((?<a>[^\._]{2,})[\._]*)|([\._](?<b>[^\._]{2,}))", RegexOptions.Compiled);
/// <summary> /// <summary>
/// Resolve information about series from path. /// Resolve information about series from path.

View file

@ -43,7 +43,7 @@ namespace Emby.Naming.Video
&& match.Groups.Count == 5 && match.Groups.Count == 5
&& match.Groups[1].Success && match.Groups[1].Success
&& match.Groups[2].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); result = new CleanDateTimeResult(match.Groups[1].Value.TrimEnd(), year);
return true; return true;

View file

@ -56,7 +56,7 @@ namespace Emby.Naming.Video
} }
else if (rule.RuleType == ExtraRuleType.Regex) 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); var isMatch = Regex.IsMatch(filename, rule.Token, RegexOptions.IgnoreCase | RegexOptions.Compiled);

View file

@ -17,7 +17,7 @@ public class FileStackRule
/// <param name="isNumerical">Whether the file stack rule uses numerical or alphabetical numbering.</param> /// <param name="isNumerical">Whether the file stack rule uses numerical or alphabetical numbering.</param>
public FileStackRule(string token, bool isNumerical) public FileStackRule(string token, bool isNumerical)
{ {
_tokenRegex = new Regex(token, RegexOptions.IgnoreCase); _tokenRegex = new Regex(token, RegexOptions.IgnoreCase | RegexOptions.Compiled);
IsNumerical = isNumerical; IsNumerical = isNumerical;
} }

View file

@ -4,6 +4,7 @@ using System.IO;
using System.Linq; using System.Linq;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using Emby.Naming.Common; using Emby.Naming.Common;
using Jellyfin.Extensions;
using MediaBrowser.Model.IO; using MediaBrowser.Model.IO;
namespace Emby.Naming.Video namespace Emby.Naming.Video
@ -13,6 +14,8 @@ namespace Emby.Naming.Video
/// </summary> /// </summary>
public static class VideoListResolver public static class VideoListResolver
{ {
private static readonly Regex _resolutionRegex = new Regex("[0-9]{2}[0-9]+[ip]", RegexOptions.IgnoreCase | RegexOptions.Compiled);
/// <summary> /// <summary>
/// Resolves alternative versions and extras from list of video files. /// Resolves alternative versions and extras from list of video files.
/// </summary> /// </summary>
@ -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] // 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++) for (var i = 0; i < videos.Count; i++)
{ {
var video = videos[i]; var video = videos[i];
@ -114,29 +118,43 @@ namespace Emby.Naming.Video
continue; continue;
} }
if (!IsEligibleForMultiVersion(folderName, video.Files[0].Path, namingOptions)) if (!IsEligibleForMultiVersion(folderName, video.Files[0].FileNameWithoutExtension, namingOptions))
{ {
return videos; 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 if (videos.Count > 1)
videos.Sort((x, y) => string.Compare(x.Name, y.Name, StringComparison.Ordinal)); {
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<VideoInfo> var list = new List<VideoInfo>
{ {
videos[0] primary
}; };
var alternateVersionsLen = videos.Count - 1; list[0].AlternateVersions = videos.Select(x => x.Files[0]).ToArray();
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].Name = folderName.ToString(); list[0].Name = folderName.ToString();
return list; return list;
@ -161,9 +179,8 @@ namespace Emby.Naming.Video
return true; return true;
} }
private static bool IsEligibleForMultiVersion(ReadOnlySpan<char> folderName, string testFilePath, NamingOptions namingOptions) private static bool IsEligibleForMultiVersion(ReadOnlySpan<char> folderName, ReadOnlySpan<char> testFilename, NamingOptions namingOptions)
{ {
var testFilename = Path.GetFileNameWithoutExtension(testFilePath.AsSpan());
if (!testFilename.StartsWith(folderName, StringComparison.OrdinalIgnoreCase)) if (!testFilename.StartsWith(folderName, StringComparison.OrdinalIgnoreCase))
{ {
return false; return false;
@ -176,16 +193,15 @@ namespace Emby.Naming.Video
} }
// There are no span overloads for regex unfortunately // There are no span overloads for regex unfortunately
var tmpTestFilename = testFilename.ToString(); if (CleanStringParser.TryClean(testFilename.ToString(), namingOptions.CleanStringRegexes, out var cleanName))
if (CleanStringParser.TryClean(tmpTestFilename, namingOptions.CleanStringRegexes, out var cleanName))
{ {
tmpTestFilename = cleanName.Trim(); testFilename = cleanName.AsSpan().Trim();
} }
// The CleanStringParser should have removed common keywords etc. // The CleanStringParser should have removed common keywords etc.
return string.IsNullOrEmpty(tmpTestFilename) return testFilename.IsEmpty
|| testFilename[0] == '-' || testFilename[0] == '-'
|| Regex.IsMatch(tmpTestFilename, @"^\[([^]]*)\]", RegexOptions.Compiled); || Regex.IsMatch(testFilename, @"^\[([^]]*)\]", RegexOptions.Compiled);
} }
} }
} }

View file

@ -1,5 +1,3 @@
#nullable disable
using System; using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
@ -33,15 +31,10 @@ namespace Emby.Server.Implementations.AppBase
private ConfigurationStore[] _configurationStores = Array.Empty<ConfigurationStore>(); private ConfigurationStore[] _configurationStores = Array.Empty<ConfigurationStore>();
private IConfigurationFactory[] _configurationFactories = Array.Empty<IConfigurationFactory>(); private IConfigurationFactory[] _configurationFactories = Array.Empty<IConfigurationFactory>();
/// <summary>
/// The _configuration loaded.
/// </summary>
private bool _configurationLoaded;
/// <summary> /// <summary>
/// The _configuration. /// The _configuration.
/// </summary> /// </summary>
private BaseApplicationConfiguration _configuration; private BaseApplicationConfiguration? _configuration;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="BaseConfigurationManager" /> class. /// Initializes a new instance of the <see cref="BaseConfigurationManager" /> class.
@ -63,17 +56,17 @@ namespace Emby.Server.Implementations.AppBase
/// <summary> /// <summary>
/// Occurs when [configuration updated]. /// Occurs when [configuration updated].
/// </summary> /// </summary>
public event EventHandler<EventArgs> ConfigurationUpdated; public event EventHandler<EventArgs>? ConfigurationUpdated;
/// <summary> /// <summary>
/// Occurs when [configuration updating]. /// Occurs when [configuration updating].
/// </summary> /// </summary>
public event EventHandler<ConfigurationUpdateEventArgs> NamedConfigurationUpdating; public event EventHandler<ConfigurationUpdateEventArgs>? NamedConfigurationUpdating;
/// <summary> /// <summary>
/// Occurs when [named configuration updated]. /// Occurs when [named configuration updated].
/// </summary> /// </summary>
public event EventHandler<ConfigurationUpdateEventArgs> NamedConfigurationUpdated; public event EventHandler<ConfigurationUpdateEventArgs>? NamedConfigurationUpdated;
/// <summary> /// <summary>
/// Gets the type of the configuration. /// Gets the type of the configuration.
@ -107,31 +100,25 @@ namespace Emby.Server.Implementations.AppBase
{ {
get get
{ {
if (_configurationLoaded) if (_configuration is not null)
{ {
return _configuration; return _configuration;
} }
lock (_configurationSyncLock) lock (_configurationSyncLock)
{ {
if (_configurationLoaded) if (_configuration is not null)
{ {
return _configuration; return _configuration;
} }
_configuration = (BaseApplicationConfiguration)ConfigurationHelper.GetXmlConfiguration(ConfigurationType, CommonApplicationPaths.SystemConfigurationFilePath, XmlSerializer); return _configuration = (BaseApplicationConfiguration)ConfigurationHelper.GetXmlConfiguration(ConfigurationType, CommonApplicationPaths.SystemConfigurationFilePath, XmlSerializer);
_configurationLoaded = true;
return _configuration;
} }
} }
protected set protected set
{ {
_configuration = value; _configuration = value;
_configurationLoaded = value is not null;
} }
} }
@ -183,7 +170,7 @@ namespace Emby.Server.Implementations.AppBase
Logger.LogInformation("Saving system configuration"); Logger.LogInformation("Saving system configuration");
var path = CommonApplicationPaths.SystemConfigurationFilePath; 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) lock (_configurationSyncLock)
{ {
@ -323,25 +310,20 @@ namespace Emby.Server.Implementations.AppBase
private object LoadConfiguration(string path, Type configurationType) private object LoadConfiguration(string path, Type configurationType)
{ {
if (!File.Exists(path))
{
return Activator.CreateInstance(configurationType);
}
try try
{ {
return XmlSerializer.DeserializeFromFile(configurationType, path); if (File.Exists(path))
{
return XmlSerializer.DeserializeFromFile(configurationType, path);
}
} }
catch (IOException) catch (Exception ex) when (ex is not IOException)
{
return Activator.CreateInstance(configurationType);
}
catch (Exception ex)
{ {
Logger.LogError(ex, "Error loading configuration file: {Path}", path); 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<T>.");
} }
/// <inheritdoc /> /// <inheritdoc />
@ -367,7 +349,7 @@ namespace Emby.Server.Implementations.AppBase
_configurations.AddOrUpdate(key, configuration, (_, _) => configuration); _configurations.AddOrUpdate(key, configuration, (_, _) => configuration);
var path = GetConfigurationFile(key); 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) lock (_configurationSyncLock)
{ {

View file

@ -1187,10 +1187,13 @@ namespace Emby.Server.Implementations
} }
} }
// used for closing websockets if (_sessionManager != null)
foreach (var session in _sessionManager.Sessions)
{ {
await session.DisposeAsync().ConfigureAwait(false); // used for closing websockets
foreach (var session in _sessionManager.Sessions)
{
await session.DisposeAsync().ConfigureAwait(false);
}
} }
} }
} }

View file

@ -1,5 +1,3 @@
#nullable disable
using System; using System;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
@ -36,7 +34,7 @@ namespace Emby.Server.Implementations.Configuration
/// <summary> /// <summary>
/// Configuration updating event. /// Configuration updating event.
/// </summary> /// </summary>
public event EventHandler<GenericEventArgs<ServerConfiguration>> ConfigurationUpdating; public event EventHandler<GenericEventArgs<ServerConfiguration>>? ConfigurationUpdating;
/// <summary> /// <summary>
/// Gets the type of the configuration. /// Gets the type of the configuration.

View file

@ -1195,7 +1195,7 @@ namespace Emby.Server.Implementations.Data
Path = RestorePath(path.ToString()) 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.MinValue.Ticks
&& ticks <= DateTime.MaxValue.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) if (query.SeriesStatuses.Length > 0)
{ {
var statuses = new List<string>(); var statuses = new List<string>();
@ -5440,6 +5458,9 @@ AND Type = @InternalPersonType)");
list.AddRange(inheritedTags.Select(i => (6, i))); list.AddRange(inheritedTags.Select(i => (6, i)));
// Remove all invalid values.
list.RemoveAll(i => string.IsNullOrEmpty(i.Item2));
return list; return list;
} }

View file

@ -1,5 +1,3 @@
#nullable disable
#pragma warning disable CS1591 #pragma warning disable CS1591
using System.Collections.Generic; using System.Collections.Generic;

View file

@ -1,5 +1,3 @@
#nullable disable
#pragma warning disable CS1591 #pragma warning disable CS1591
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;

View file

@ -1,5 +1,3 @@
#nullable disable
#pragma warning disable CS1591 #pragma warning disable CS1591
using System.Collections.Generic; using System.Collections.Generic;

View file

@ -313,13 +313,8 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
return result; return result;
} }
private static bool IsIgnored(string filename) private static bool IsIgnored(ReadOnlySpan<char> filename)
{ => Regex.IsMatch(filename, @"\bsample\b", RegexOptions.IgnoreCase | RegexOptions.Compiled);
// Ignore samples
Match m = Regex.Match(filename, @"\bsample\b", RegexOptions.IgnoreCase | RegexOptions.Compiled);
return m.Success;
}
private static bool ContainsFile(IReadOnlyList<VideoInfo> result, FileSystemMetadata file) private static bool ContainsFile(IReadOnlyList<VideoInfo> result, FileSystemMetadata file)
{ {

View file

@ -570,15 +570,13 @@ namespace Emby.Server.Implementations.LiveTv.Listings
_tokens.TryAdd(username, savedToken); _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 return savedToken.Name;
if (DateTime.UtcNow.Ticks - ticks < TimeSpan.FromHours(20).Ticks)
{
return savedToken.Name;
}
} }
} }

View file

@ -137,32 +137,33 @@ namespace Emby.Server.Implementations.LiveTv.Listings
private static ProgramInfo GetProgramInfo(XmlTvProgram program, ListingsProviderInfo info) 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 var programInfo = new ProgramInfo
{ {
ChannelId = program.ChannelId, ChannelId = program.ChannelId,
EndDate = program.EndDate.UtcDateTime, EndDate = program.EndDate.UtcDateTime,
EpisodeNumber = program.Episode?.Episode, EpisodeNumber = program.Episode.Episode,
EpisodeTitle = episodeTitle, EpisodeTitle = episodeTitle,
Genres = program.Categories, Genres = programCategories,
StartDate = program.StartDate.UtcDateTime, StartDate = program.StartDate.UtcDateTime,
Name = program.Title, Name = program.Title,
Overview = program.Description, Overview = program.Description,
ProductionYear = program.CopyrightDate?.Year, ProductionYear = program.CopyrightDate?.Year,
SeasonNumber = program.Episode?.Series, SeasonNumber = program.Episode.Series,
IsSeries = program.Episode is not null, IsSeries = program.Episode.Series is not null,
IsRepeat = program.IsPreviouslyShown && !program.IsNew, IsRepeat = program.IsPreviouslyShown && !program.IsNew,
IsPremiere = program.Premiere is not null, IsPremiere = program.Premiere is not null,
IsKids = program.Categories.Any(c => info.KidsCategories.Contains(c, StringComparison.OrdinalIgnoreCase)), IsKids = programCategories.Any(c => info.KidsCategories.Contains(c, StringComparison.OrdinalIgnoreCase)),
IsMovie = program.Categories.Any(c => info.MovieCategories.Contains(c, StringComparison.OrdinalIgnoreCase)), IsMovie = programCategories.Any(c => info.MovieCategories.Contains(c, StringComparison.OrdinalIgnoreCase)),
IsNews = program.Categories.Any(c => info.NewsCategories.Contains(c, StringComparison.OrdinalIgnoreCase)), IsNews = programCategories.Any(c => info.NewsCategories.Contains(c, StringComparison.OrdinalIgnoreCase)),
IsSports = program.Categories.Any(c => info.SportsCategories.Contains(c, StringComparison.OrdinalIgnoreCase)), IsSports = programCategories.Any(c => info.SportsCategories.Contains(c, StringComparison.OrdinalIgnoreCase)),
ImageUrl = string.IsNullOrEmpty(program.Icon?.Source) ? null : program.Icon.Source, ImageUrl = string.IsNullOrEmpty(program.Icon?.Source) ? null : program.Icon.Source,
HasImage = !string.IsNullOrEmpty(program.Icon?.Source), HasImage = !string.IsNullOrEmpty(program.Icon?.Source),
OfficialRating = string.IsNullOrEmpty(program.Rating?.Value) ? null : program.Rating.Value, OfficialRating = string.IsNullOrEmpty(program.Rating?.Value) ? null : program.Rating.Value,
CommunityRating = program.StarRating, 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)) if (string.IsNullOrWhiteSpace(program.ProgramId))
@ -243,7 +244,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
{ {
Id = c.Id, Id = c.Id,
Name = c.DisplayName, 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 Number = string.IsNullOrWhiteSpace(c.Number) ? c.Id : c.Number
}).ToList(); }).ToList();
} }

View file

@ -13,8 +13,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
public LegacyHdHomerunChannelCommands(string url) public LegacyHdHomerunChannelCommands(string url)
{ {
// parse url for channel and program // parse url for channel and program
var regExp = new Regex(@"\/ch([0-9]+)-?([0-9]*)"); var match = Regex.Match(url, @"\/ch([0-9]+)-?([0-9]*)");
var match = regExp.Match(url);
if (match.Success) if (match.Success)
{ {
_channel = match.Groups[1].Value; _channel = match.Groups[1].Value;

View file

@ -168,28 +168,24 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
string numberString = null; string numberString = null;
string attributeValue; 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 (!IsValidChannelNumber(numberString))
{ {
if (attributes.TryGetValue("tvg-id", out attributeValue)) 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; 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[] { ' ', '.' }); 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(); numberString = numberPart.ToString();
} }
@ -255,19 +251,14 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
private static bool IsValidChannelNumber(string numberString) private static bool IsValidChannelNumber(string numberString)
{ {
if (string.IsNullOrWhiteSpace(numberString) || if (string.IsNullOrWhiteSpace(numberString)
string.Equals(numberString, "-1", StringComparison.OrdinalIgnoreCase) || || string.Equals(numberString, "-1", StringComparison.Ordinal)
string.Equals(numberString, "0", StringComparison.OrdinalIgnoreCase)) || string.Equals(numberString, "0", StringComparison.Ordinal))
{ {
return false; return false;
} }
if (!double.TryParse(numberString, NumberStyles.Any, CultureInfo.InvariantCulture, out _)) return double.TryParse(numberString, CultureInfo.InvariantCulture, out _);
{
return false;
}
return true;
} }
private static string GetChannelName(string extInf, Dictionary<string, string> attributes) private static string GetChannelName(string extInf, Dictionary<string, string> attributes)
@ -285,7 +276,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
{ {
var numberPart = nameInExtInf.Substring(0, numberIndex).Trim(new[] { ' ', '.' }); 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(); // channel.Number = number.ToString();
nameInExtInf = nameInExtInf.Substring(numberIndex + 1).Trim(new[] { ' ', '-' }); nameInExtInf = nameInExtInf.Substring(numberIndex + 1).Trim(new[] { ' ', '-' });
@ -317,8 +308,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
{ {
var dict = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); var dict = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
var reg = new Regex(@"([a-z0-9\-_]+)=\""([^""]+)\""", RegexOptions.IgnoreCase); var matches = Regex.Matches(line, @"([a-z0-9\-_]+)=\""([^""]+)\""", RegexOptions.IgnoreCase);
var matches = reg.Matches(line);
remaining = line; remaining = line;

View file

@ -118,11 +118,11 @@
"TaskCleanActivityLog": "Borrar log de actividades", "TaskCleanActivityLog": "Borrar log de actividades",
"Undefined": "Indefinido", "Undefined": "Indefinido",
"Forced": "Forzado", "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.", "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", "TaskOptimizeDatabase": "Optimización de base de datos",
"External": "Externo", "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.", "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", "TaskKeyframeExtractor": "Extractor de Fotogramas Clave",
"HearingImpaired": "Personas con discapacidad auditiva" "HearingImpaired": "Discapacidad Auditiva"
} }

View file

@ -31,7 +31,7 @@
"ItemRemovedWithName": "{0} ha sido eliminado de la biblioteca", "ItemRemovedWithName": "{0} ha sido eliminado de la biblioteca",
"LabelIpAddressValue": "Dirección IP: {0}", "LabelIpAddressValue": "Dirección IP: {0}",
"LabelRunningTimeValue": "Tiempo de funcionamiento: {0}", "LabelRunningTimeValue": "Tiempo de funcionamiento: {0}",
"Latest": "Últimos", "Latest": "Último contenido en",
"MessageApplicationUpdated": "Se ha actualizado el servidor Jellyfin", "MessageApplicationUpdated": "Se ha actualizado el servidor Jellyfin",
"MessageApplicationUpdatedTo": "Se ha actualizado el servidor Jellyfin a la versión {0}", "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", "MessageNamedServerConfigurationUpdatedWithValue": "La sección {0} de configuración del servidor ha sido actualizada",

View file

@ -82,7 +82,7 @@
"MessageServerConfigurationUpdated": "Konfigurasi server telah diperbarui", "MessageServerConfigurationUpdated": "Konfigurasi server telah diperbarui",
"MessageNamedServerConfigurationUpdatedWithValue": "Bagian konfigurasi server {0} telah diperbarui", "MessageNamedServerConfigurationUpdatedWithValue": "Bagian konfigurasi server {0} telah diperbarui",
"FailedLoginAttemptWithUserName": "Gagal melakukan login dari {0}", "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", "DeviceOfflineWithName": "{0} telah terputus",
"DeviceOnlineWithName": "{0} telah terhubung", "DeviceOnlineWithName": "{0} telah terhubung",
"NotificationOptionVideoPlaybackStopped": "Pemutaran video berhenti", "NotificationOptionVideoPlaybackStopped": "Pemutaran video berhenti",

View file

@ -58,8 +58,8 @@
"NotificationOptionServerRestartRequired": "Server herstart nodig", "NotificationOptionServerRestartRequired": "Server herstart nodig",
"NotificationOptionTaskFailed": "Geplande taak mislukt", "NotificationOptionTaskFailed": "Geplande taak mislukt",
"NotificationOptionUserLockedOut": "Gebruiker is vergrendeld", "NotificationOptionUserLockedOut": "Gebruiker is vergrendeld",
"NotificationOptionVideoPlayback": "Video gestart", "NotificationOptionVideoPlayback": "Afspelen van video gestart",
"NotificationOptionVideoPlaybackStopped": "Video gestopt", "NotificationOptionVideoPlaybackStopped": "Afspelen van video gestopt",
"Photos": "Foto's", "Photos": "Foto's",
"Playlists": "Afspeellijsten", "Playlists": "Afspeellijsten",
"Plugin": "Plug-in", "Plugin": "Plug-in",
@ -95,26 +95,26 @@
"TaskDownloadMissingSubtitlesDescription": "Zoekt op het internet naar ontbrekende ondertiteling gebaseerd op metadataconfiguratie.", "TaskDownloadMissingSubtitlesDescription": "Zoekt op het internet naar ontbrekende ondertiteling gebaseerd op metadataconfiguratie.",
"TaskDownloadMissingSubtitles": "Ontbrekende ondertiteling downloaden", "TaskDownloadMissingSubtitles": "Ontbrekende ondertiteling downloaden",
"TaskRefreshChannelsDescription": "Vernieuwt informatie van internet kanalen.", "TaskRefreshChannelsDescription": "Vernieuwt informatie van internet kanalen.",
"TaskRefreshChannels": "Vernieuw Kanalen", "TaskRefreshChannels": "Vernieuw kanalen",
"TaskCleanTranscodeDescription": "Verwijdert transcode bestanden ouder dan 1 dag.", "TaskCleanTranscodeDescription": "Verwijdert transcode bestanden ouder dan 1 dag.",
"TaskCleanLogs": "Logboekmap opschonen", "TaskCleanLogs": "Logboekmap opschonen",
"TaskCleanTranscode": "Transcoderingsmap opschonen", "TaskCleanTranscode": "Transcoderingsmap opschonen",
"TaskUpdatePluginsDescription": "Downloadt en installeert updates van plug-ins waarvoor automatisch bijwerken is ingeschakeld.", "TaskUpdatePluginsDescription": "Downloadt en installeert updates van plug-ins waarvoor automatisch bijwerken is ingeschakeld.",
"TaskUpdatePlugins": "Plug-ins bijwerken", "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", "TaskRefreshPeople": "Personen vernieuwen",
"TaskCleanLogsDescription": "Verwijdert log bestanden ouder dan {0} dagen.", "TaskCleanLogsDescription": "Verwijdert log bestanden ouder dan {0} dagen.",
"TaskRefreshLibraryDescription": "Scant de mediabibliotheek op nieuwe bestanden en vernieuwt de metadata.", "TaskRefreshLibraryDescription": "Scant de mediabibliotheek op nieuwe bestanden en vernieuwt de metadata.",
"TaskRefreshLibrary": "Mediabibliotheek scannen", "TaskRefreshLibrary": "Mediabibliotheek scannen",
"TaskRefreshChapterImagesDescription": "Maakt thumbnails aan voor videos met hoofdstukken.", "TaskRefreshChapterImagesDescription": "Maakt voorbeeldafbeedingen aan voor video's met hoofdstukken.",
"TaskRefreshChapterImages": "Hoofdstukafbeeldingen uitpakken", "TaskRefreshChapterImages": "Hoofdstukafbeeldingen extraheren",
"TaskCleanCacheDescription": "Verwijdert gecachte bestanden die het systeem niet langer nodig heeft.", "TaskCleanCacheDescription": "Verwijdert gecachte bestanden die het systeem niet langer nodig heeft.",
"TaskCleanCache": "Cache-map opschonen", "TaskCleanCache": "Cache-map opschonen",
"TasksChannelsCategory": "Internet Kanalen", "TasksChannelsCategory": "Internetkanalen",
"TasksApplicationCategory": "Toepassing", "TasksApplicationCategory": "Toepassing",
"TasksLibraryCategory": "Bibliotheek", "TasksLibraryCategory": "Bibliotheek",
"TasksMaintenanceCategory": "Onderhoud", "TasksMaintenanceCategory": "Onderhoud",
"TaskCleanActivityLogDescription": "Verwijdert activiteiten logs ouder dan de ingestelde tijd.", "TaskCleanActivityLogDescription": "Verwijdert activiteiten logs ouder dan de ingestelde leeftijd.",
"TaskCleanActivityLog": "Activiteitenlogboek legen", "TaskCleanActivityLog": "Activiteitenlogboek legen",
"Undefined": "Niet gedefinieerd", "Undefined": "Niet gedefinieerd",
"Forced": "Geforceerd", "Forced": "Geforceerd",

View file

@ -16,14 +16,14 @@
"Folders": "Папки", "Folders": "Папки",
"Genres": "Жанры", "Genres": "Жанры",
"HeaderAlbumArtists": "Исполнители альбома", "HeaderAlbumArtists": "Исполнители альбома",
"HeaderContinueWatching": "Продолжение просмотра", "HeaderContinueWatching": "Продолжить просмотр",
"HeaderFavoriteAlbums": "Избранные альбомы", "HeaderFavoriteAlbums": "Избранные альбомы",
"HeaderFavoriteArtists": "Избранные исполнители", "HeaderFavoriteArtists": "Избранные исполнители",
"HeaderFavoriteEpisodes": "Избранные эпизоды", "HeaderFavoriteEpisodes": "Избранные эпизоды",
"HeaderFavoriteShows": "Избранные сериалы", "HeaderFavoriteShows": "Избранные сериалы",
"HeaderFavoriteSongs": "Избранные композиции", "HeaderFavoriteSongs": "Избранные композиции",
"HeaderLiveTV": "Эфир", "HeaderLiveTV": "Эфир",
"HeaderNextUp": "Очередное", "HeaderNextUp": "Следующий",
"HeaderRecordingGroups": "Группы записей", "HeaderRecordingGroups": "Группы записей",
"HomeVideos": "Домашние видео", "HomeVideos": "Домашние видео",
"Inherit": "Наследуемое", "Inherit": "Наследуемое",
@ -70,7 +70,7 @@
"ScheduledTaskFailedWithName": "{0} - неудачна", "ScheduledTaskFailedWithName": "{0} - неудачна",
"ScheduledTaskStartedWithName": "{0} - запущена", "ScheduledTaskStartedWithName": "{0} - запущена",
"ServerNameNeedsToBeRestarted": "Необходим перезапуск {0}", "ServerNameNeedsToBeRestarted": "Необходим перезапуск {0}",
"Shows": "Передачи", "Shows": "Телешоу",
"Songs": "Композиции", "Songs": "Композиции",
"StartupEmbyServerIsLoading": "Jellyfin Server загружается. Повторите попытку в ближайшее время.", "StartupEmbyServerIsLoading": "Jellyfin Server загружается. Повторите попытку в ближайшее время.",
"SubtitleDownloadFailureForItem": "Субтитры к {0} не удалось загрузить", "SubtitleDownloadFailureForItem": "Субтитры к {0} не удалось загрузить",

View file

@ -123,5 +123,6 @@
"TaskOptimizeDatabase": "Optimiziraj bazo podatkov", "TaskOptimizeDatabase": "Optimiziraj bazo podatkov",
"TaskKeyframeExtractor": "Ekstraktor ključnih sličic", "TaskKeyframeExtractor": "Ekstraktor ključnih sličic",
"External": "Zunanji", "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"
} }

View file

@ -86,7 +86,7 @@
"Shows": "Шоу", "Shows": "Шоу",
"ServerNameNeedsToBeRestarted": "{0} потрібно перезапустити", "ServerNameNeedsToBeRestarted": "{0} потрібно перезапустити",
"ScheduledTaskStartedWithName": "{0} розпочато", "ScheduledTaskStartedWithName": "{0} розпочато",
"ScheduledTaskFailedWithName": "Помилка {0}", "ScheduledTaskFailedWithName": "{0} незавершено, збій",
"ProviderValue": "Постачальник: {0}", "ProviderValue": "Постачальник: {0}",
"PluginUpdatedWithName": "{0} оновлено", "PluginUpdatedWithName": "{0} оновлено",
"PluginUninstalledWithName": "{0} видалено", "PluginUninstalledWithName": "{0} видалено",

View file

@ -123,41 +123,64 @@ namespace Emby.Server.Implementations.Plugins
continue; continue;
} }
var assemblyLoadContext = new PluginLoadContext(plugin.Path);
_assemblyLoadContexts.Add(assemblyLoadContext);
var assemblies = new List<Assembly>(plugin.DllFiles.Count);
var loadedAll = true;
foreach (var file in plugin.DllFiles) foreach (var file in plugin.DllFiles)
{ {
Assembly assembly;
try try
{ {
var assemblyLoadContext = new PluginLoadContext(file); assemblies.Add(assemblyLoadContext.LoadFromAssemblyPath(file));
_assemblyLoadContexts.Add(assemblyLoadContext);
assembly = assemblyLoadContext.LoadFromAssemblyPath(file);
// Load all required types to verify that the plugin will load
assembly.GetTypes();
} }
catch (FileLoadException ex) 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); ChangePluginState(plugin, PluginStatus.Malfunctioned);
continue; loadedAll = false;
} break;
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;
} }
#pragma warning disable CA1031 // Do not catch general exception types #pragma warning disable CA1031 // Do not catch general exception types
catch (Exception ex) catch (Exception ex)
#pragma warning restore CA1031 // Do not catch general exception types #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); 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; yield return assembly;
} }
} }

View file

@ -1,5 +1,3 @@
#nullable disable
#pragma warning disable CS1591 #pragma warning disable CS1591
using System; using System;
@ -43,9 +41,9 @@ namespace Emby.Server.Implementations.ScheduledTasks
ScheduledTasks = Array.Empty<IScheduledTaskWorker>(); ScheduledTasks = Array.Empty<IScheduledTaskWorker>();
} }
public event EventHandler<GenericEventArgs<IScheduledTaskWorker>> TaskExecuting; public event EventHandler<GenericEventArgs<IScheduledTaskWorker>>? TaskExecuting;
public event EventHandler<TaskCompletionEventArgs> TaskCompleted; public event EventHandler<TaskCompletionEventArgs>? TaskCompleted;
/// <summary> /// <summary>
/// Gets the list of Scheduled Tasks. /// Gets the list of Scheduled Tasks.

View file

@ -1,5 +1,3 @@
#nullable disable
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
@ -58,7 +56,7 @@ namespace Emby.Server.Implementations.Session
/// <summary> /// <summary>
/// The KeepAlive cancellation token. /// The KeepAlive cancellation token.
/// </summary> /// </summary>
private CancellationTokenSource _keepAliveCancellationToken; private CancellationTokenSource? _keepAliveCancellationToken;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="SessionWebSocketListener" /> class. /// Initializes a new instance of the <see cref="SessionWebSocketListener" /> class.
@ -105,7 +103,7 @@ namespace Emby.Server.Implementations.Session
} }
} }
private async Task<SessionInfo> GetSession(HttpContext httpContext, string remoteEndpoint) private async Task<SessionInfo?> GetSession(HttpContext httpContext, string? remoteEndpoint)
{ {
if (!httpContext.User.Identity?.IsAuthenticated ?? false) if (!httpContext.User.Identity?.IsAuthenticated ?? false)
{ {
@ -138,8 +136,13 @@ namespace Emby.Server.Implementations.Session
/// </summary> /// </summary>
/// <param name="sender">The WebSocket.</param> /// <param name="sender">The WebSocket.</param>
/// <param name="e">The event arguments.</param> /// <param name="e">The event arguments.</param>
private void OnWebSocketClosed(object sender, EventArgs e) private void OnWebSocketClosed(object? sender, EventArgs e)
{ {
if (sender is null)
{
return;
}
var webSocket = (IWebSocketConnection)sender; var webSocket = (IWebSocketConnection)sender;
_logger.LogDebug("WebSocket {0} is closed.", webSocket); _logger.LogDebug("WebSocket {0} is closed.", webSocket);
RemoveWebSocket(webSocket); RemoveWebSocket(webSocket);

View file

@ -1,5 +1,3 @@
#nullable disable
using System; using System;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Sorting; using MediaBrowser.Controller.Sorting;
@ -24,10 +22,9 @@ namespace Emby.Server.Implementations.Sorting
/// <param name="x">The x.</param> /// <param name="x">The x.</param>
/// <param name="y">The y.</param> /// <param name="y">The y.</param>
/// <returns>System.Int32.</returns> /// <returns>System.Int32.</returns>
public int Compare(BaseItem x, BaseItem y) public int Compare(BaseItem? x, BaseItem? y)
{ {
ArgumentNullException.ThrowIfNull(x); ArgumentNullException.ThrowIfNull(x);
ArgumentNullException.ThrowIfNull(y); ArgumentNullException.ThrowIfNull(y);
return (x.RunTimeTicks ?? 0).CompareTo(y.RunTimeTicks ?? 0); return (x.RunTimeTicks ?? 0).CompareTo(y.RunTimeTicks ?? 0);

View file

@ -1,5 +1,3 @@
#nullable disable
#pragma warning disable CS1591 #pragma warning disable CS1591
using System; using System;
@ -23,15 +21,14 @@ namespace Emby.Server.Implementations.Sorting
/// <param name="x">The x.</param> /// <param name="x">The x.</param>
/// <param name="y">The y.</param> /// <param name="y">The y.</param>
/// <returns>System.Int32.</returns> /// <returns>System.Int32.</returns>
public int Compare(BaseItem x, BaseItem y) public int Compare(BaseItem? x, BaseItem? y)
{ {
return string.Compare(GetValue(x), GetValue(y), StringComparison.OrdinalIgnoreCase); 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; var hasSeries = item as IHasSeries;
return hasSeries?.FindSeriesSortName(); return hasSeries?.FindSeriesSortName();
} }
} }

View file

@ -1,5 +1,3 @@
#nullable disable
using System; using System;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Sorting; using MediaBrowser.Controller.Sorting;
@ -24,10 +22,9 @@ namespace Emby.Server.Implementations.Sorting
/// <param name="x">The x.</param> /// <param name="x">The x.</param>
/// <param name="y">The y.</param> /// <param name="y">The y.</param>
/// <returns>System.Int32.</returns> /// <returns>System.Int32.</returns>
public int Compare(BaseItem x, BaseItem y) public int Compare(BaseItem? x, BaseItem? y)
{ {
ArgumentNullException.ThrowIfNull(x); ArgumentNullException.ThrowIfNull(x);
ArgumentNullException.ThrowIfNull(y); ArgumentNullException.ThrowIfNull(y);
return string.Compare(x.SortName, y.SortName, StringComparison.OrdinalIgnoreCase); return string.Compare(x.SortName, y.SortName, StringComparison.OrdinalIgnoreCase);

View file

@ -1,5 +1,3 @@
#nullable disable
#pragma warning disable CS1591 #pragma warning disable CS1591
using System; using System;
@ -24,7 +22,7 @@ namespace Emby.Server.Implementations.Sorting
/// <param name="x">The x.</param> /// <param name="x">The x.</param>
/// <param name="y">The y.</param> /// <param name="y">The y.</param>
/// <returns>System.Int32.</returns> /// <returns>System.Int32.</returns>
public int Compare(BaseItem x, BaseItem y) public int Compare(BaseItem? x, BaseItem? y)
{ {
return GetDate(x).CompareTo(GetDate(y)); return GetDate(x).CompareTo(GetDate(y));
} }
@ -34,7 +32,7 @@ namespace Emby.Server.Implementations.Sorting
/// </summary> /// </summary>
/// <param name="x">The x.</param> /// <param name="x">The x.</param>
/// <returns>DateTime.</returns> /// <returns>DateTime.</returns>
private static DateTime GetDate(BaseItem x) private static DateTime GetDate(BaseItem? x)
{ {
if (x is LiveTvProgram hasStartDate) if (x is LiveTvProgram hasStartDate)
{ {

View file

@ -1,5 +1,3 @@
#nullable disable
#pragma warning disable CS1591 #pragma warning disable CS1591
using System; using System;
@ -24,10 +22,9 @@ namespace Emby.Server.Implementations.Sorting
/// <param name="x">The x.</param> /// <param name="x">The x.</param>
/// <param name="y">The y.</param> /// <param name="y">The y.</param>
/// <returns>System.Int32.</returns> /// <returns>System.Int32.</returns>
public int Compare(BaseItem x, BaseItem y) public int Compare(BaseItem? x, BaseItem? y)
{ {
ArgumentNullException.ThrowIfNull(x); ArgumentNullException.ThrowIfNull(x);
ArgumentNullException.ThrowIfNull(y); ArgumentNullException.ThrowIfNull(y);
return AlphanumericComparator.CompareValues(x.Studios.FirstOrDefault(), y.Studios.FirstOrDefault()); return AlphanumericComparator.CompareValues(x.Studios.FirstOrDefault(), y.Studios.FirstOrDefault());

View file

@ -1,5 +1,3 @@
#nullable disable
#pragma warning disable CS1591 #pragma warning disable CS1591
using System; using System;
@ -42,7 +40,7 @@ namespace Emby.Server.Implementations.TV
throw new ArgumentException("User not found"); throw new ArgumentException("User not found");
} }
string presentationUniqueKey = null; string? presentationUniqueKey = null;
if (query.SeriesId.HasValue && !query.SeriesId.Value.Equals(default)) if (query.SeriesId.HasValue && !query.SeriesId.Value.Equals(default))
{ {
if (_libraryManager.GetItemById(query.SeriesId.Value) is Series series) if (_libraryManager.GetItemById(query.SeriesId.Value) is Series series)
@ -91,7 +89,7 @@ namespace Emby.Server.Implementations.TV
throw new ArgumentException("User not found"); throw new ArgumentException("User not found");
} }
string presentationUniqueKey = null; string? presentationUniqueKey = null;
int? limit = null; int? limit = null;
if (request.SeriesId.HasValue && !request.SeriesId.Value.Equals(default)) if (request.SeriesId.HasValue && !request.SeriesId.Value.Equals(default))
{ {
@ -168,7 +166,7 @@ namespace Emby.Server.Implementations.TV
return !anyFound && i.LastWatchedDate == DateTime.MinValue; return !anyFound && i.LastWatchedDate == DateTime.MinValue;
}) })
.Select(i => i.GetEpisodeFunction()) .Select(i => i.GetEpisodeFunction())
.Where(i => i is not null); .Where(i => i is not null)!;
} }
private static string GetUniqueSeriesKey(Episode episode) private static string GetUniqueSeriesKey(Episode episode)
@ -185,7 +183,7 @@ namespace Emby.Server.Implementations.TV
/// Gets the next up. /// Gets the next up.
/// </summary> /// </summary>
/// <returns>Task{Episode}.</returns> /// <returns>Task{Episode}.</returns>
private (DateTime LastWatchedDate, Func<Episode> GetEpisodeFunction) GetNextUp(string seriesKey, User user, DtoOptions dtoOptions, bool rewatching) private (DateTime LastWatchedDate, Func<Episode?> GetEpisodeFunction) GetNextUp(string seriesKey, User user, DtoOptions dtoOptions, bool rewatching)
{ {
var lastQuery = new InternalItemsQuery(user) var lastQuery = new InternalItemsQuery(user)
{ {
@ -209,7 +207,7 @@ namespace Emby.Server.Implementations.TV
var lastWatchedEpisode = _libraryManager.GetItemList(lastQuery).Cast<Episode>().FirstOrDefault(); var lastWatchedEpisode = _libraryManager.GetItemList(lastQuery).Cast<Episode>().FirstOrDefault();
Episode GetEpisode() Episode? GetEpisode()
{ {
var nextQuery = new InternalItemsQuery(user) var nextQuery = new InternalItemsQuery(user)
{ {

View file

@ -1,4 +1,5 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net; using MediaBrowser.Common.Net;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
@ -29,7 +30,7 @@ namespace Jellyfin.Api.Auth.AnonymousLanAccessPolicy
/// <inheritdoc /> /// <inheritdoc />
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, AnonymousLanAccessRequirement requirement) 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. // Loopback will be on LAN, so we can accept null.
if (ip is null || _networkManager.IsInLocalNetwork(ip)) if (ip is null || _networkManager.IsInLocalNetwork(ip))

View file

@ -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
{
/// <summary>
/// Base authorization handler.
/// </summary>
/// <typeparam name="T">Type of Authorization Requirement.</typeparam>
public abstract class BaseAuthorizationHandler<T> : AuthorizationHandler<T>
where T : IAuthorizationRequirement
{
private readonly IUserManager _userManager;
private readonly INetworkManager _networkManager;
private readonly IHttpContextAccessor _httpContextAccessor;
/// <summary>
/// Initializes a new instance of the <see cref="BaseAuthorizationHandler{T}"/> class.
/// </summary>
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
/// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
/// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
protected BaseAuthorizationHandler(
IUserManager userManager,
INetworkManager networkManager,
IHttpContextAccessor httpContextAccessor)
{
_userManager = userManager;
_networkManager = networkManager;
_httpContextAccessor = httpContextAccessor;
}
/// <summary>
/// Validate authenticated claims.
/// </summary>
/// <param name="claimsPrincipal">Request claims.</param>
/// <param name="ignoreSchedule">Whether to ignore parental control.</param>
/// <param name="localAccessOnly">Whether access is to be allowed locally only.</param>
/// <param name="requiredDownloadPermission">Whether validation requires download permission.</param>
/// <returns>Validated claim status.</returns>
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;
}
}
}

View file

@ -1,4 +1,8 @@
using System.Threading.Tasks; 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.Common.Net;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
@ -9,8 +13,12 @@ namespace Jellyfin.Api.Auth.DefaultAuthorizationPolicy
/// <summary> /// <summary>
/// Default authorization handler. /// Default authorization handler.
/// </summary> /// </summary>
public class DefaultAuthorizationHandler : BaseAuthorizationHandler<DefaultAuthorizationRequirement> public class DefaultAuthorizationHandler : AuthorizationHandler<DefaultAuthorizationRequirement>
{ {
private readonly IUserManager _userManager;
private readonly INetworkManager _networkManager;
private readonly IHttpContextAccessor _httpContextAccessor;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="DefaultAuthorizationHandler"/> class. /// Initializes a new instance of the <see cref="DefaultAuthorizationHandler"/> class.
/// </summary> /// </summary>
@ -21,21 +29,56 @@ namespace Jellyfin.Api.Auth.DefaultAuthorizationPolicy
IUserManager userManager, IUserManager userManager,
INetworkManager networkManager, INetworkManager networkManager,
IHttpContextAccessor httpContextAccessor) IHttpContextAccessor httpContextAccessor)
: base(userManager, networkManager, httpContextAccessor)
{ {
_userManager = userManager;
_networkManager = networkManager;
_httpContextAccessor = httpContextAccessor;
} }
/// <inheritdoc /> /// <inheritdoc />
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, DefaultAuthorizationRequirement requirement) protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, DefaultAuthorizationRequirement requirement)
{ {
var validated = ValidateClaims(context.User); var isApiKey = context.User.GetIsApiKey();
if (validated) 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(); 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; return Task.CompletedTask;

View file

@ -7,5 +7,18 @@ namespace Jellyfin.Api.Auth.DefaultAuthorizationPolicy
/// </summary> /// </summary>
public class DefaultAuthorizationRequirement : IAuthorizationRequirement public class DefaultAuthorizationRequirement : IAuthorizationRequirement
{ {
/// <summary>
/// Initializes a new instance of the <see cref="DefaultAuthorizationRequirement"/> class.
/// </summary>
/// <param name="validateParentalSchedule">A value indicating whether to validate parental schedule.</param>
public DefaultAuthorizationRequirement(bool validateParentalSchedule = true)
{
ValidateParentalSchedule = validateParentalSchedule;
}
/// <summary>
/// Gets a value indicating whether to ignore parental schedule.
/// </summary>
public bool ValidateParentalSchedule { get; }
} }
} }

View file

@ -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
{
/// <summary>
/// Download authorization handler.
/// </summary>
public class DownloadHandler : BaseAuthorizationHandler<DownloadRequirement>
{
/// <summary>
/// Initializes a new instance of the <see cref="DownloadHandler"/> class.
/// </summary>
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
/// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
/// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
public DownloadHandler(
IUserManager userManager,
INetworkManager networkManager,
IHttpContextAccessor httpContextAccessor)
: base(userManager, networkManager, httpContextAccessor)
{
}
/// <inheritdoc />
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, DownloadRequirement requirement)
{
var validated = ValidateClaims(context.User);
if (validated)
{
context.Succeed(requirement);
}
else
{
context.Fail();
}
return Task.CompletedTask;
}
}
}

View file

@ -1,11 +0,0 @@
using Microsoft.AspNetCore.Authorization;
namespace Jellyfin.Api.Auth.DownloadPolicy
{
/// <summary>
/// The download permission requirement.
/// </summary>
public class DownloadRequirement : IAuthorizationRequirement
{
}
}

View file

@ -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
{
/// <summary>
/// Ignore parental control schedule and allow before startup wizard has been completed.
/// </summary>
public class FirstTimeOrIgnoreParentalControlSetupHandler : BaseAuthorizationHandler<FirstTimeOrIgnoreParentalControlSetupRequirement>
{
private readonly IConfigurationManager _configurationManager;
/// <summary>
/// Initializes a new instance of the <see cref="FirstTimeOrIgnoreParentalControlSetupHandler"/> class.
/// </summary>
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
/// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
/// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
/// <param name="configurationManager">Instance of the <see cref="IConfigurationManager"/> interface.</param>
public FirstTimeOrIgnoreParentalControlSetupHandler(
IUserManager userManager,
INetworkManager networkManager,
IHttpContextAccessor httpContextAccessor,
IConfigurationManager configurationManager)
: base(userManager, networkManager, httpContextAccessor)
{
_configurationManager = configurationManager;
}
/// <inheritdoc />
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;
}
}
}

View file

@ -1,11 +0,0 @@
using Microsoft.AspNetCore.Authorization;
namespace Jellyfin.Api.Auth.FirstTimeOrIgnoreParentalControlSetupPolicy
{
/// <summary>
/// First time setup or ignore parental controls requirement.
/// </summary>
public class FirstTimeOrIgnoreParentalControlSetupRequirement : IAuthorizationRequirement
{
}
}

View file

@ -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
{
/// <summary>
/// Authorization handler for requiring first time setup or default privileges.
/// </summary>
public class FirstTimeSetupOrDefaultHandler : BaseAuthorizationHandler<FirstTimeSetupOrDefaultRequirement>
{
private readonly IConfigurationManager _configurationManager;
/// <summary>
/// Initializes a new instance of the <see cref="FirstTimeSetupOrDefaultHandler" /> class.
/// </summary>
/// <param name="configurationManager">Instance of the <see cref="IConfigurationManager"/> interface.</param>
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
/// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
/// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
public FirstTimeSetupOrDefaultHandler(
IConfigurationManager configurationManager,
IUserManager userManager,
INetworkManager networkManager,
IHttpContextAccessor httpContextAccessor)
: base(userManager, networkManager, httpContextAccessor)
{
_configurationManager = configurationManager;
}
/// <inheritdoc />
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;
}
}
}

View file

@ -1,11 +0,0 @@
using Microsoft.AspNetCore.Authorization;
namespace Jellyfin.Api.Auth.FirstTimeSetupOrDefaultPolicy
{
/// <summary>
/// The authorization requirement, requiring incomplete first time setup or default privileges, for the authorization handler.
/// </summary>
public class FirstTimeSetupOrDefaultRequirement : IAuthorizationRequirement
{
}
}

View file

@ -1,11 +0,0 @@
using Microsoft.AspNetCore.Authorization;
namespace Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy
{
/// <summary>
/// The authorization requirement, requiring incomplete first time setup or elevated privileges, for the authorization handler.
/// </summary>
public class FirstTimeSetupOrElevatedRequirement : IAuthorizationRequirement
{
}
}

View file

@ -1,39 +1,36 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using Jellyfin.Api.Constants; using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Net; using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
namespace Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy namespace Jellyfin.Api.Auth.FirstTimeSetupPolicy
{ {
/// <summary> /// <summary>
/// Authorization handler for requiring first time setup or elevated privileges. /// Authorization handler for requiring first time setup or default privileges.
/// </summary> /// </summary>
public class FirstTimeSetupOrElevatedHandler : BaseAuthorizationHandler<FirstTimeSetupOrElevatedRequirement> public class FirstTimeSetupHandler : AuthorizationHandler<FirstTimeSetupRequirement>
{ {
private readonly IConfigurationManager _configurationManager; private readonly IConfigurationManager _configurationManager;
private readonly IUserManager _userManager;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="FirstTimeSetupOrElevatedHandler" /> class. /// Initializes a new instance of the <see cref="FirstTimeSetupHandler" /> class.
/// </summary> /// </summary>
/// <param name="configurationManager">Instance of the <see cref="IConfigurationManager"/> interface.</param> /// <param name="configurationManager">Instance of the <see cref="IConfigurationManager"/> interface.</param>
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param> /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
/// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param> public FirstTimeSetupHandler(
/// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
public FirstTimeSetupOrElevatedHandler(
IConfigurationManager configurationManager, IConfigurationManager configurationManager,
IUserManager userManager, IUserManager userManager)
INetworkManager networkManager,
IHttpContextAccessor httpContextAccessor)
: base(userManager, networkManager, httpContextAccessor)
{ {
_configurationManager = configurationManager; _configurationManager = configurationManager;
_userManager = userManager;
} }
/// <inheritdoc /> /// <inheritdoc />
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, FirstTimeSetupOrElevatedRequirement requirement) protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, FirstTimeSetupRequirement requirement)
{ {
if (!_configurationManager.CommonConfiguration.IsStartupWizardCompleted) if (!_configurationManager.CommonConfiguration.IsStartupWizardCompleted)
{ {
@ -41,14 +38,27 @@ namespace Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy
return Task.CompletedTask; return Task.CompletedTask;
} }
var validated = ValidateClaims(context.User); if (requirement.RequireAdmin && !context.User.IsInRole(UserRoles.Administrator))
if (validated && context.User.IsInRole(UserRoles.Administrator))
{
context.Succeed(requirement);
}
else
{ {
context.Fail(); 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; return Task.CompletedTask;

View file

@ -0,0 +1,25 @@
using Jellyfin.Api.Auth.DefaultAuthorizationPolicy;
namespace Jellyfin.Api.Auth.FirstTimeSetupPolicy
{
/// <summary>
/// The authorization requirement, requiring incomplete first time setup or default privileges, for the authorization handler.
/// </summary>
public class FirstTimeSetupRequirement : DefaultAuthorizationRequirement
{
/// <summary>
/// Initializes a new instance of the <see cref="FirstTimeSetupRequirement"/> class.
/// </summary>
/// <param name="validateParentalSchedule">A value indicating whether to ignore parental schedule.</param>
/// <param name="requireAdmin">A value indicating whether administrator role is required.</param>
public FirstTimeSetupRequirement(bool validateParentalSchedule = false, bool requireAdmin = true) : base(validateParentalSchedule)
{
RequireAdmin = requireAdmin;
}
/// <summary>
/// Gets a value indicating whether administrator role is required.
/// </summary>
public bool RequireAdmin { get; }
}
}

View file

@ -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
{
/// <summary>
/// Escape schedule controls handler.
/// </summary>
public class IgnoreParentalControlHandler : BaseAuthorizationHandler<IgnoreParentalControlRequirement>
{
/// <summary>
/// Initializes a new instance of the <see cref="IgnoreParentalControlHandler"/> class.
/// </summary>
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
/// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
/// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
public IgnoreParentalControlHandler(
IUserManager userManager,
INetworkManager networkManager,
IHttpContextAccessor httpContextAccessor)
: base(userManager, networkManager, httpContextAccessor)
{
}
/// <inheritdoc />
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;
}
}
}

View file

@ -1,11 +0,0 @@
using Microsoft.AspNetCore.Authorization;
namespace Jellyfin.Api.Auth.IgnoreParentalControlPolicy
{
/// <summary>
/// Escape schedule controls requirement.
/// </summary>
public class IgnoreParentalControlRequirement : IAuthorizationRequirement
{
}
}

View file

@ -1,7 +1,7 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using Jellyfin.Api.Constants; using Jellyfin.Api.Constants;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net; using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Library;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
@ -10,27 +10,38 @@ namespace Jellyfin.Api.Auth.LocalAccessOrRequiresElevationPolicy
/// <summary> /// <summary>
/// Local access or require elevated privileges handler. /// Local access or require elevated privileges handler.
/// </summary> /// </summary>
public class LocalAccessOrRequiresElevationHandler : BaseAuthorizationHandler<LocalAccessOrRequiresElevationRequirement> public class LocalAccessOrRequiresElevationHandler : AuthorizationHandler<LocalAccessOrRequiresElevationRequirement>
{ {
private readonly INetworkManager _networkManager;
private readonly IHttpContextAccessor _httpContextAccessor;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="LocalAccessOrRequiresElevationHandler"/> class. /// Initializes a new instance of the <see cref="LocalAccessOrRequiresElevationHandler"/> class.
/// </summary> /// </summary>
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
/// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param> /// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
/// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param> /// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
public LocalAccessOrRequiresElevationHandler( public LocalAccessOrRequiresElevationHandler(
IUserManager userManager,
INetworkManager networkManager, INetworkManager networkManager,
IHttpContextAccessor httpContextAccessor) IHttpContextAccessor httpContextAccessor)
: base(userManager, networkManager, httpContextAccessor)
{ {
_networkManager = networkManager;
_httpContextAccessor = httpContextAccessor;
} }
/// <inheritdoc /> /// <inheritdoc />
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, LocalAccessOrRequiresElevationRequirement requirement) protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, LocalAccessOrRequiresElevationRequirement requirement)
{ {
var validated = ValidateClaims(context.User, localAccessOnly: true); var ip = _httpContextAccessor.HttpContext?.GetNormalizedRemoteIp();
if (validated || context.User.IsInRole(UserRoles.Administrator))
// 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); context.Succeed(requirement);
} }

View file

@ -1,4 +1,4 @@
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
namespace Jellyfin.Api.Auth.LocalAccessOrRequiresElevationPolicy namespace Jellyfin.Api.Auth.LocalAccessOrRequiresElevationPolicy
{ {

View file

@ -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
{
/// <summary>
/// Local access handler.
/// </summary>
public class LocalAccessHandler : BaseAuthorizationHandler<LocalAccessRequirement>
{
/// <summary>
/// Initializes a new instance of the <see cref="LocalAccessHandler"/> class.
/// </summary>
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
/// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
/// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
public LocalAccessHandler(
IUserManager userManager,
INetworkManager networkManager,
IHttpContextAccessor httpContextAccessor)
: base(userManager, networkManager, httpContextAccessor)
{
}
/// <inheritdoc />
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;
}
}
}

View file

@ -1,11 +0,0 @@
using Microsoft.AspNetCore.Authorization;
namespace Jellyfin.Api.Auth.LocalAccessPolicy
{
/// <summary>
/// The local access authorization requirement.
/// </summary>
public class LocalAccessRequirement : IAuthorizationRequirement
{
}
}

View file

@ -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
{
/// <summary>
/// Authorization handler for requiring elevated privileges.
/// </summary>
public class RequiresElevationHandler : BaseAuthorizationHandler<RequiresElevationRequirement>
{
/// <summary>
/// Initializes a new instance of the <see cref="RequiresElevationHandler"/> class.
/// </summary>
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
/// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
/// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
public RequiresElevationHandler(
IUserManager userManager,
INetworkManager networkManager,
IHttpContextAccessor httpContextAccessor)
: base(userManager, networkManager, httpContextAccessor)
{
}
/// <inheritdoc />
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;
}
}
}

View file

@ -1,11 +0,0 @@
using Microsoft.AspNetCore.Authorization;
namespace Jellyfin.Api.Auth.RequiresElevationPolicy
{
/// <summary>
/// The authorization requirement for requiring elevated privileges in the authorization handler.
/// </summary>
public class RequiresElevationRequirement : IAuthorizationRequirement
{
}
}

View file

@ -1,19 +1,17 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using Jellyfin.Api.Extensions; using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
using Jellyfin.Data.Enums; using Jellyfin.Data.Enums;
using MediaBrowser.Common.Net; using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.SyncPlay; using MediaBrowser.Controller.SyncPlay;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
namespace Jellyfin.Api.Auth.SyncPlayAccessPolicy namespace Jellyfin.Api.Auth.SyncPlayAccessPolicy
{ {
/// <summary> /// <summary>
/// Default authorization handler. /// Default authorization handler.
/// </summary> /// </summary>
public class SyncPlayAccessHandler : BaseAuthorizationHandler<SyncPlayAccessRequirement> public class SyncPlayAccessHandler : AuthorizationHandler<SyncPlayAccessRequirement>
{ {
private readonly ISyncPlayManager _syncPlayManager; private readonly ISyncPlayManager _syncPlayManager;
private readonly IUserManager _userManager; private readonly IUserManager _userManager;
@ -23,14 +21,9 @@ namespace Jellyfin.Api.Auth.SyncPlayAccessPolicy
/// </summary> /// </summary>
/// <param name="syncPlayManager">Instance of the <see cref="ISyncPlayManager"/> interface.</param> /// <param name="syncPlayManager">Instance of the <see cref="ISyncPlayManager"/> interface.</param>
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param> /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
/// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
/// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
public SyncPlayAccessHandler( public SyncPlayAccessHandler(
ISyncPlayManager syncPlayManager, ISyncPlayManager syncPlayManager,
IUserManager userManager, IUserManager userManager)
INetworkManager networkManager,
IHttpContextAccessor httpContextAccessor)
: base(userManager, networkManager, httpContextAccessor)
{ {
_syncPlayManager = syncPlayManager; _syncPlayManager = syncPlayManager;
_userManager = userManager; _userManager = userManager;
@ -39,27 +32,20 @@ namespace Jellyfin.Api.Auth.SyncPlayAccessPolicy
/// <inheritdoc /> /// <inheritdoc />
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, SyncPlayAccessRequirement requirement) protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, SyncPlayAccessRequirement requirement)
{ {
if (!ValidateClaims(context.User))
{
context.Fail();
return Task.CompletedTask;
}
var userId = context.User.GetUserId(); var userId = context.User.GetUserId();
var user = _userManager.GetUserById(userId); var user = _userManager.GetUserById(userId);
if (user is null)
{
throw new ResourceNotFoundException();
}
if (requirement.RequiredAccess == SyncPlayAccessRequirementType.HasAccess) if (requirement.RequiredAccess == SyncPlayAccessRequirementType.HasAccess)
{ {
if (user.SyncPlayAccess == SyncPlayUserAccessType.CreateAndJoinGroups if (user.SyncPlayAccess is SyncPlayUserAccessType.CreateAndJoinGroups or SyncPlayUserAccessType.JoinGroups
|| user.SyncPlayAccess == SyncPlayUserAccessType.JoinGroups
|| _syncPlayManager.IsUserActive(userId)) || _syncPlayManager.IsUserActive(userId))
{ {
context.Succeed(requirement); context.Succeed(requirement);
} }
else
{
context.Fail();
}
} }
else if (requirement.RequiredAccess == SyncPlayAccessRequirementType.CreateGroup) else if (requirement.RequiredAccess == SyncPlayAccessRequirementType.CreateGroup)
{ {
@ -67,10 +53,6 @@ namespace Jellyfin.Api.Auth.SyncPlayAccessPolicy
{ {
context.Succeed(requirement); context.Succeed(requirement);
} }
else
{
context.Fail();
}
} }
else if (requirement.RequiredAccess == SyncPlayAccessRequirementType.JoinGroup) else if (requirement.RequiredAccess == SyncPlayAccessRequirementType.JoinGroup)
{ {
@ -79,10 +61,6 @@ namespace Jellyfin.Api.Auth.SyncPlayAccessPolicy
{ {
context.Succeed(requirement); context.Succeed(requirement);
} }
else
{
context.Fail();
}
} }
else if (requirement.RequiredAccess == SyncPlayAccessRequirementType.IsInGroup) else if (requirement.RequiredAccess == SyncPlayAccessRequirementType.IsInGroup)
{ {
@ -90,14 +68,6 @@ namespace Jellyfin.Api.Auth.SyncPlayAccessPolicy
{ {
context.Succeed(requirement); context.Succeed(requirement);
} }
else
{
context.Fail();
}
}
else
{
context.Fail();
} }
return Task.CompletedTask; return Task.CompletedTask;

View file

@ -1,12 +1,12 @@
using Jellyfin.Data.Enums; using Jellyfin.Api.Auth.DefaultAuthorizationPolicy;
using Microsoft.AspNetCore.Authorization; using Jellyfin.Data.Enums;
namespace Jellyfin.Api.Auth.SyncPlayAccessPolicy namespace Jellyfin.Api.Auth.SyncPlayAccessPolicy
{ {
/// <summary> /// <summary>
/// The default authorization requirement. /// The default authorization requirement.
/// </summary> /// </summary>
public class SyncPlayAccessRequirement : IAuthorizationRequirement public class SyncPlayAccessRequirement : DefaultAuthorizationRequirement
{ {
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="SyncPlayAccessRequirement"/> class. /// Initializes a new instance of the <see cref="SyncPlayAccessRequirement"/> class.

View file

@ -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
{
/// <summary>
/// User permission authorization handler.
/// </summary>
public class UserPermissionHandler : AuthorizationHandler<UserPermissionRequirement>
{
private readonly IUserManager _userManager;
/// <summary>
/// Initializes a new instance of the <see cref="UserPermissionHandler"/> class.
/// </summary>
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
public UserPermissionHandler(IUserManager userManager)
{
_userManager = userManager;
}
/// <inheritdoc />
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;
}
}
}

View file

@ -0,0 +1,26 @@
using Jellyfin.Api.Auth.DefaultAuthorizationPolicy;
using Jellyfin.Data.Enums;
namespace Jellyfin.Api.Auth.UserPermissionPolicy
{
/// <summary>
/// The user permission requirement.
/// </summary>
public class UserPermissionRequirement : DefaultAuthorizationRequirement
{
/// <summary>
/// Initializes a new instance of the <see cref="UserPermissionRequirement"/> class.
/// </summary>
/// <param name="requiredPermission">The required <see cref="PermissionKind"/>.</param>
/// <param name="validateParentalSchedule">Whether to validate the user's parental schedule.</param>
public UserPermissionRequirement(PermissionKind requiredPermission, bool validateParentalSchedule = true) : base(validateParentalSchedule)
{
RequiredPermission = requiredPermission;
}
/// <summary>
/// Gets the required user permission.
/// </summary>
public PermissionKind RequiredPermission { get; }
}
}

View file

@ -5,11 +5,6 @@ namespace Jellyfin.Api.Constants;
/// </summary> /// </summary>
public static class Policies public static class Policies
{ {
/// <summary>
/// Policy name for default authorization.
/// </summary>
public const string DefaultAuthorization = "DefaultAuthorization";
/// <summary> /// <summary>
/// Policy name for requiring first time setup or elevated privileges. /// Policy name for requiring first time setup or elevated privileges.
/// </summary> /// </summary>
@ -74,4 +69,19 @@ public static class Policies
/// Policy name for accessing a SyncPlay group. /// Policy name for accessing a SyncPlay group.
/// </summary> /// </summary>
public const string SyncPlayIsInGroup = "SyncPlayIsInGroup"; public const string SyncPlayIsInGroup = "SyncPlayIsInGroup";
/// <summary>
/// Policy name for accessing collection management.
/// </summary>
public const string CollectionManagement = "CollectionManagement";
/// <summary>
/// Policy name for accessing LiveTV.
/// </summary>
public const string LiveTvAccess = "LiveTvAccess";
/// <summary>
/// Policy name for managing LiveTV.
/// </summary>
public const string LiveTvManagement = "LiveTvManagement";
} }

View file

@ -1,7 +1,6 @@
using System; using System;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Linq; using System.Linq;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions; using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers; using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders; using Jellyfin.Api.ModelBinders;
@ -23,7 +22,7 @@ namespace Jellyfin.Api.Controllers;
/// The artists controller. /// The artists controller.
/// </summary> /// </summary>
[Route("Artists")] [Route("Artists")]
[Authorize(Policy = Policies.DefaultAuthorization)] [Authorize]
public class ArtistsController : BaseJellyfinApiController public class ArtistsController : BaseJellyfinApiController
{ {
private readonly ILibraryManager _libraryManager; private readonly ILibraryManager _libraryManager;
@ -119,6 +118,7 @@ public class ArtistsController : BaseJellyfinApiController
[FromQuery] bool? enableImages = true, [FromQuery] bool? enableImages = true,
[FromQuery] bool enableTotalRecordCount = true) [FromQuery] bool enableTotalRecordCount = true)
{ {
userId = RequestHelpers.GetUserId(User, userId);
var dtoOptions = new DtoOptions { Fields = fields } var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(User) .AddClientFields(User)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
@ -126,7 +126,7 @@ public class ArtistsController : BaseJellyfinApiController
User? user = null; User? user = null;
BaseItem parentItem = _libraryManager.GetParentItem(parentId, userId); BaseItem parentItem = _libraryManager.GetParentItem(parentId, userId);
if (userId.HasValue && !userId.Equals(default)) if (!userId.Value.Equals(default))
{ {
user = _userManager.GetUserById(userId.Value); user = _userManager.GetUserById(userId.Value);
} }
@ -322,6 +322,7 @@ public class ArtistsController : BaseJellyfinApiController
[FromQuery] bool? enableImages = true, [FromQuery] bool? enableImages = true,
[FromQuery] bool enableTotalRecordCount = true) [FromQuery] bool enableTotalRecordCount = true)
{ {
userId = RequestHelpers.GetUserId(User, userId);
var dtoOptions = new DtoOptions { Fields = fields } var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(User) .AddClientFields(User)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
@ -329,7 +330,7 @@ public class ArtistsController : BaseJellyfinApiController
User? user = null; User? user = null;
BaseItem parentItem = _libraryManager.GetParentItem(parentId, userId); BaseItem parentItem = _libraryManager.GetParentItem(parentId, userId);
if (userId.HasValue && !userId.Equals(default)) if (!userId.Value.Equals(default))
{ {
user = _userManager.GetUserById(userId.Value); user = _userManager.GetUserById(userId.Value);
} }
@ -463,11 +464,12 @@ public class ArtistsController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<BaseItemDto> GetArtistByName([FromRoute, Required] string name, [FromQuery] Guid? userId) public ActionResult<BaseItemDto> GetArtistByName([FromRoute, Required] string name, [FromQuery] Guid? userId)
{ {
userId = RequestHelpers.GetUserId(User, userId);
var dtoOptions = new DtoOptions().AddClientFields(User); var dtoOptions = new DtoOptions().AddClientFields(User);
var item = _libraryManager.GetArtist(name, dtoOptions); var item = _libraryManager.GetArtist(name, dtoOptions);
if (userId.HasValue && !userId.Value.Equals(default)) if (!userId.Value.Equals(default))
{ {
var user = _userManager.GetUserById(userId.Value); var user = _userManager.GetUserById(userId.Value);

View file

@ -3,7 +3,6 @@ using System.Collections.Generic;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Helpers; using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders; using Jellyfin.Api.ModelBinders;
using Jellyfin.Data.Enums; using Jellyfin.Data.Enums;
@ -23,7 +22,7 @@ namespace Jellyfin.Api.Controllers;
/// <summary> /// <summary>
/// Channels Controller. /// Channels Controller.
/// </summary> /// </summary>
[Authorize(Policy = Policies.DefaultAuthorization)] [Authorize]
public class ChannelsController : BaseJellyfinApiController public class ChannelsController : BaseJellyfinApiController
{ {
private readonly IChannelManager _channelManager; private readonly IChannelManager _channelManager;
@ -61,11 +60,12 @@ public class ChannelsController : BaseJellyfinApiController
[FromQuery] bool? supportsMediaDeletion, [FromQuery] bool? supportsMediaDeletion,
[FromQuery] bool? isFavorite) [FromQuery] bool? isFavorite)
{ {
userId = RequestHelpers.GetUserId(User, userId);
return _channelManager.GetChannels(new ChannelQuery return _channelManager.GetChannels(new ChannelQuery
{ {
Limit = limit, Limit = limit,
StartIndex = startIndex, StartIndex = startIndex,
UserId = userId ?? Guid.Empty, UserId = userId.Value,
SupportsLatestItems = supportsLatestItems, SupportsLatestItems = supportsLatestItems,
SupportsMediaDeletion = supportsMediaDeletion, SupportsMediaDeletion = supportsMediaDeletion,
IsFavorite = isFavorite IsFavorite = isFavorite
@ -125,7 +125,8 @@ public class ChannelsController : BaseJellyfinApiController
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields) [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 ? null
: _userManager.GetUserById(userId.Value); : _userManager.GetUserById(userId.Value);
@ -199,7 +200,8 @@ public class ChannelsController : BaseJellyfinApiController
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] channelIds) [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 ? null
: _userManager.GetUserById(userId.Value); : _userManager.GetUserById(userId.Value);

View file

@ -1,7 +1,6 @@
using System.Net.Mime; using System.Net.Mime;
using System.Threading.Tasks; using System.Threading.Tasks;
using Jellyfin.Api.Attributes; using Jellyfin.Api.Attributes;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions; using Jellyfin.Api.Extensions;
using Jellyfin.Api.Models.ClientLogDtos; using Jellyfin.Api.Models.ClientLogDtos;
using MediaBrowser.Controller.ClientEvent; using MediaBrowser.Controller.ClientEvent;
@ -15,7 +14,7 @@ namespace Jellyfin.Api.Controllers;
/// <summary> /// <summary>
/// Client log controller. /// Client log controller.
/// </summary> /// </summary>
[Authorize(Policy = Policies.DefaultAuthorization)] [Authorize]
public class ClientLogController : BaseJellyfinApiController public class ClientLogController : BaseJellyfinApiController
{ {
private const int MaxDocumentSize = 1_000_000; private const int MaxDocumentSize = 1_000_000;

View file

@ -17,7 +17,7 @@ namespace Jellyfin.Api.Controllers;
/// The collection controller. /// The collection controller.
/// </summary> /// </summary>
[Route("Collections")] [Route("Collections")]
[Authorize(Policy = Policies.DefaultAuthorization)] [Authorize(Policy = Policies.CollectionManagement)]
public class CollectionController : BaseJellyfinApiController public class CollectionController : BaseJellyfinApiController
{ {
private readonly ICollectionManager _collectionManager; private readonly ICollectionManager _collectionManager;

View file

@ -19,7 +19,7 @@ namespace Jellyfin.Api.Controllers;
/// Configuration Controller. /// Configuration Controller.
/// </summary> /// </summary>
[Route("System")] [Route("System")]
[Authorize(Policy = Policies.DefaultAuthorization)] [Authorize]
public class ConfigurationController : BaseJellyfinApiController public class ConfigurationController : BaseJellyfinApiController
{ {
private readonly IServerConfigurationManager _configurationManager; private readonly IServerConfigurationManager _configurationManager;

View file

@ -4,7 +4,6 @@ using System.IO;
using System.Linq; using System.Linq;
using System.Net.Mime; using System.Net.Mime;
using Jellyfin.Api.Attributes; using Jellyfin.Api.Attributes;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Models; using Jellyfin.Api.Models;
using MediaBrowser.Common.Plugins; using MediaBrowser.Common.Plugins;
using MediaBrowser.Model.Net; using MediaBrowser.Model.Net;
@ -48,7 +47,7 @@ public class DashboardController : BaseJellyfinApiController
[HttpGet("web/ConfigurationPages")] [HttpGet("web/ConfigurationPages")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
[Authorize(Policy = Policies.DefaultAuthorization)] [Authorize]
public ActionResult<IEnumerable<ConfigurationPageInfo>> GetConfigurationPages( public ActionResult<IEnumerable<ConfigurationPageInfo>> GetConfigurationPages(
[FromQuery] bool? enableInMainMenu) [FromQuery] bool? enableInMainMenu)
{ {

View file

@ -2,6 +2,7 @@ using System;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Threading.Tasks; using System.Threading.Tasks;
using Jellyfin.Api.Constants; using Jellyfin.Api.Constants;
using Jellyfin.Api.Helpers;
using Jellyfin.Data.Dtos; using Jellyfin.Data.Dtos;
using Jellyfin.Data.Entities.Security; using Jellyfin.Data.Entities.Security;
using Jellyfin.Data.Queries; using Jellyfin.Data.Queries;
@ -48,6 +49,7 @@ public class DevicesController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<QueryResult<DeviceInfo>>> GetDevices([FromQuery] bool? supportsSync, [FromQuery] Guid? userId) public async Task<ActionResult<QueryResult<DeviceInfo>>> GetDevices([FromQuery] bool? supportsSync, [FromQuery] Guid? userId)
{ {
userId = RequestHelpers.GetUserId(User, userId);
return await _deviceManager.GetDevicesForUser(userId, supportsSync).ConfigureAwait(false); return await _deviceManager.GetDevicesForUser(userId, supportsSync).ConfigureAwait(false);
} }

View file

@ -3,7 +3,6 @@ using System.ComponentModel.DataAnnotations;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
using Jellyfin.Api.Constants;
using Jellyfin.Data.Entities; using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums; using Jellyfin.Data.Enums;
using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Extensions;
@ -19,7 +18,7 @@ namespace Jellyfin.Api.Controllers;
/// <summary> /// <summary>
/// Display Preferences Controller. /// Display Preferences Controller.
/// </summary> /// </summary>
[Authorize(Policy = Policies.DefaultAuthorization)] [Authorize]
public class DisplayPreferencesController : BaseJellyfinApiController public class DisplayPreferencesController : BaseJellyfinApiController
{ {
private readonly IDisplayPreferencesManager _displayPreferencesManager; private readonly IDisplayPreferencesManager _displayPreferencesManager;

View file

@ -9,7 +9,6 @@ using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Jellyfin.Api.Attributes; using Jellyfin.Api.Attributes;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Helpers; using Jellyfin.Api.Helpers;
using Jellyfin.Api.Models.PlaybackDtos; using Jellyfin.Api.Models.PlaybackDtos;
using Jellyfin.Api.Models.StreamingDtos; using Jellyfin.Api.Models.StreamingDtos;
@ -36,7 +35,7 @@ namespace Jellyfin.Api.Controllers;
/// Dynamic hls controller. /// Dynamic hls controller.
/// </summary> /// </summary>
[Route("")] [Route("")]
[Authorize(Policy = Policies.DefaultAuthorization)] [Authorize]
public class DynamicHlsController : BaseJellyfinApiController public class DynamicHlsController : BaseJellyfinApiController
{ {
private const string DefaultVodEncoderPreset = "veryfast"; private const string DefaultVodEncoderPreset = "veryfast";

View file

@ -1,6 +1,7 @@
using System; using System;
using System.Linq; using System.Linq;
using Jellyfin.Api.Constants; using Jellyfin.Api.Constants;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders; using Jellyfin.Api.ModelBinders;
using Jellyfin.Data.Enums; using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Dto;
@ -18,7 +19,7 @@ namespace Jellyfin.Api.Controllers;
/// Filters controller. /// Filters controller.
/// </summary> /// </summary>
[Route("")] [Route("")]
[Authorize(Policy = Policies.DefaultAuthorization)] [Authorize]
public class FilterController : BaseJellyfinApiController public class FilterController : BaseJellyfinApiController
{ {
private readonly ILibraryManager _libraryManager; private readonly ILibraryManager _libraryManager;
@ -52,7 +53,8 @@ public class FilterController : BaseJellyfinApiController
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes) [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 ? null
: _userManager.GetUserById(userId.Value); : _userManager.GetUserById(userId.Value);
@ -144,7 +146,8 @@ public class FilterController : BaseJellyfinApiController
[FromQuery] bool? isSeries, [FromQuery] bool? isSeries,
[FromQuery] bool? recursive) [FromQuery] bool? recursive)
{ {
var user = userId is null || userId.Value.Equals(default) userId = RequestHelpers.GetUserId(User, userId);
var user = userId.Value.Equals(default)
? null ? null
: _userManager.GetUserById(userId.Value); : _userManager.GetUserById(userId.Value);

View file

@ -1,7 +1,6 @@
using System; using System;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Linq; using System.Linq;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions; using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers; using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders; using Jellyfin.Api.ModelBinders;
@ -23,7 +22,7 @@ namespace Jellyfin.Api.Controllers;
/// <summary> /// <summary>
/// The genres controller. /// The genres controller.
/// </summary> /// </summary>
[Authorize(Policy = Policies.DefaultAuthorization)] [Authorize]
public class GenresController : BaseJellyfinApiController public class GenresController : BaseJellyfinApiController
{ {
private readonly IUserManager _userManager; private readonly IUserManager _userManager;
@ -91,11 +90,12 @@ public class GenresController : BaseJellyfinApiController
[FromQuery] bool? enableImages = true, [FromQuery] bool? enableImages = true,
[FromQuery] bool enableTotalRecordCount = true) [FromQuery] bool enableTotalRecordCount = true)
{ {
userId = RequestHelpers.GetUserId(User, userId);
var dtoOptions = new DtoOptions { Fields = fields } var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(User) .AddClientFields(User)
.AddAdditionalDtoOptions(enableImages, false, imageTypeLimit, enableImageTypes); .AddAdditionalDtoOptions(enableImages, false, imageTypeLimit, enableImageTypes);
User? user = userId is null || userId.Value.Equals(default) User? user = userId.Value.Equals(default)
? null ? null
: _userManager.GetUserById(userId.Value); : _userManager.GetUserById(userId.Value);
@ -132,7 +132,7 @@ public class GenresController : BaseJellyfinApiController
QueryResult<(BaseItem, ItemCounts)> result; QueryResult<(BaseItem, ItemCounts)> result;
if (parentItem is ICollectionFolder parentCollectionFolder if (parentItem is ICollectionFolder parentCollectionFolder
&& (string.Equals(parentCollectionFolder.CollectionType, CollectionType.Music, StringComparison.Ordinal) && (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); result = _libraryManager.GetMusicGenres(query);
} }
@ -156,6 +156,7 @@ public class GenresController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<BaseItemDto> GetGenre([FromRoute, Required] string genreName, [FromQuery] Guid? userId) public ActionResult<BaseItemDto> GetGenre([FromRoute, Required] string genreName, [FromQuery] Guid? userId)
{ {
userId = RequestHelpers.GetUserId(User, userId);
var dtoOptions = new DtoOptions() var dtoOptions = new DtoOptions()
.AddClientFields(User); .AddClientFields(User);
@ -171,7 +172,7 @@ public class GenresController : BaseJellyfinApiController
item ??= new Genre(); item ??= new Genre();
if (userId is null || userId.Value.Equals(default)) if (userId.Value.Equals(default))
{ {
return _dtoService.GetBaseItemDto(item, dtoOptions); return _dtoService.GetBaseItemDto(item, dtoOptions);
} }

View file

@ -4,7 +4,6 @@ using System.Diagnostics.CodeAnalysis;
using System.IO; using System.IO;
using System.Threading.Tasks; using System.Threading.Tasks;
using Jellyfin.Api.Attributes; using Jellyfin.Api.Attributes;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Helpers; using Jellyfin.Api.Helpers;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;
@ -80,7 +79,7 @@ public class HlsSegmentController : BaseJellyfinApiController
/// <response code="200">Hls video playlist returned.</response> /// <response code="200">Hls video playlist returned.</response>
/// <returns>A <see cref="FileStreamResult"/> containing the playlist.</returns> /// <returns>A <see cref="FileStreamResult"/> containing the playlist.</returns>
[HttpGet("Videos/{itemId}/hls/{playlistId}/stream.m3u8")] [HttpGet("Videos/{itemId}/hls/{playlistId}/stream.m3u8")]
[Authorize(Policy = Policies.DefaultAuthorization)] [Authorize]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesPlaylistFile] [ProducesPlaylistFile]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "itemId", Justification = "Required for ServiceStack")] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "itemId", Justification = "Required for ServiceStack")]
@ -106,7 +105,7 @@ public class HlsSegmentController : BaseJellyfinApiController
/// <response code="204">Encoding stopped successfully.</response> /// <response code="204">Encoding stopped successfully.</response>
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns> /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpDelete("Videos/ActiveEncodings")] [HttpDelete("Videos/ActiveEncodings")]
[Authorize(Policy = Policies.DefaultAuthorization)] [Authorize]
[ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult StopEncodingProcess( public ActionResult StopEncodingProcess(
[FromQuery, Required] string deviceId, [FromQuery, Required] string deviceId,

View file

@ -88,9 +88,10 @@ public class ImageController : BaseJellyfinApiController
/// <response code="403">User does not have permission to delete the image.</response> /// <response code="403">User does not have permission to delete the image.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns> /// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpPost("Users/{userId}/Images/{imageType}")] [HttpPost("Users/{userId}/Images/{imageType}")]
[Authorize(Policy = Policies.DefaultAuthorization)] [Authorize]
[AcceptsImageFile] [AcceptsImageFile]
[ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status403Forbidden)]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", 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, [FromRoute, Required] ImageType imageType,
[FromQuery] int? index = null) [FromQuery] int? index = null)
{ {
var user = _userManager.GetUserById(userId);
if (user is null)
{
return NotFound();
}
if (!RequestHelpers.AssertCanUpdateUser(_userManager, HttpContext.User, userId, true)) if (!RequestHelpers.AssertCanUpdateUser(_userManager, HttpContext.User, userId, true))
{ {
return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to update the image."); 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); var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
await using (memoryStream.ConfigureAwait(false)) await using (memoryStream.ConfigureAwait(false))
{ {
@ -116,7 +127,7 @@ public class ImageController : BaseJellyfinApiController
await _userManager.ClearProfileImageAsync(user).ConfigureAwait(false); 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 await _providerManager
.SaveImage(memoryStream, mimeType, user.ProfileImage.Path) .SaveImage(memoryStream, mimeType, user.ProfileImage.Path)
@ -137,9 +148,10 @@ public class ImageController : BaseJellyfinApiController
/// <response code="403">User does not have permission to delete the image.</response> /// <response code="403">User does not have permission to delete the image.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns> /// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpPost("Users/{userId}/Images/{imageType}/{index}")] [HttpPost("Users/{userId}/Images/{imageType}/{index}")]
[Authorize(Policy = Policies.DefaultAuthorization)] [Authorize]
[AcceptsImageFile] [AcceptsImageFile]
[ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status403Forbidden)]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", 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, Required] ImageType imageType,
[FromRoute] int index) [FromRoute] int index)
{ {
var user = _userManager.GetUserById(userId);
if (user is null)
{
return NotFound();
}
if (!RequestHelpers.AssertCanUpdateUser(_userManager, HttpContext.User, userId, true)) if (!RequestHelpers.AssertCanUpdateUser(_userManager, HttpContext.User, userId, true))
{ {
return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to update the image."); 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); var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
await using (memoryStream.ConfigureAwait(false)) await using (memoryStream.ConfigureAwait(false))
{ {
@ -165,7 +187,7 @@ public class ImageController : BaseJellyfinApiController
await _userManager.ClearProfileImageAsync(user).ConfigureAwait(false); 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 await _providerManager
.SaveImage(memoryStream, mimeType, user.ProfileImage.Path) .SaveImage(memoryStream, mimeType, user.ProfileImage.Path)
@ -186,7 +208,7 @@ public class ImageController : BaseJellyfinApiController
/// <response code="403">User does not have permission to delete the image.</response> /// <response code="403">User does not have permission to delete the image.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns> /// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpDelete("Users/{userId}/Images/{imageType}")] [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 = "imageType", Justification = "Imported from ServiceStack")]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]
[ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status204NoContent)]
@ -230,7 +252,7 @@ public class ImageController : BaseJellyfinApiController
/// <response code="403">User does not have permission to delete the image.</response> /// <response code="403">User does not have permission to delete the image.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns> /// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpDelete("Users/{userId}/Images/{imageType}/{index}")] [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 = "imageType", Justification = "Imported from ServiceStack")]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]
[ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status204NoContent)]
@ -332,6 +354,7 @@ public class ImageController : BaseJellyfinApiController
[Authorize(Policy = Policies.RequiresElevation)] [Authorize(Policy = Policies.RequiresElevation)]
[AcceptsImageFile] [AcceptsImageFile]
[ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]
public async Task<ActionResult> SetItemImage( public async Task<ActionResult> SetItemImage(
@ -344,6 +367,11 @@ public class ImageController : BaseJellyfinApiController
return NotFound(); return NotFound();
} }
if (!TryGetImageExtensionFromContentType(Request.ContentType, out _))
{
return BadRequest("Incorrect ContentType.");
}
var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false); var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
await using (memoryStream.ConfigureAwait(false)) await using (memoryStream.ConfigureAwait(false))
{ {
@ -369,6 +397,7 @@ public class ImageController : BaseJellyfinApiController
[Authorize(Policy = Policies.RequiresElevation)] [Authorize(Policy = Policies.RequiresElevation)]
[AcceptsImageFile] [AcceptsImageFile]
[ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]
public async Task<ActionResult> SetItemImageByIndex( public async Task<ActionResult> SetItemImageByIndex(
@ -382,6 +411,11 @@ public class ImageController : BaseJellyfinApiController
return NotFound(); return NotFound();
} }
if (!TryGetImageExtensionFromContentType(Request.ContentType, out _))
{
return BadRequest("Incorrect ContentType.");
}
var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false); var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
await using (memoryStream.ConfigureAwait(false)) await using (memoryStream.ConfigureAwait(false))
{ {
@ -432,7 +466,7 @@ public class ImageController : BaseJellyfinApiController
/// <response code="404">Item not found.</response> /// <response code="404">Item not found.</response>
/// <returns>The list of image infos on success, or <see cref="NotFoundResult"/> if item not found.</returns> /// <returns>The list of image infos on success, or <see cref="NotFoundResult"/> if item not found.</returns>
[HttpGet("Items/{itemId}/Images")] [HttpGet("Items/{itemId}/Images")]
[Authorize(Policy = Policies.DefaultAuthorization)] [Authorize]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<IEnumerable<ImageInfo>>> GetItemImageInfos([FromRoute, Required] Guid itemId) public async Task<ActionResult<IEnumerable<ImageInfo>>> GetItemImageInfos([FromRoute, Required] Guid itemId)
@ -1753,22 +1787,14 @@ public class ImageController : BaseJellyfinApiController
[AcceptsImageFile] [AcceptsImageFile]
public async Task<ActionResult> UploadCustomSplashscreen() public async Task<ActionResult> UploadCustomSplashscreen()
{ {
if (!TryGetImageExtensionFromContentType(Request.ContentType, out var extension))
{
return BadRequest("Incorrect ContentType.");
}
var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false); var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
await using (memoryStream.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 filePath = Path.Combine(_appPaths.DataPath, "splashscreen-upload" + extension);
var brandingOptions = _serverConfigurationManager.GetConfiguration<BrandingOptions>("branding"); var brandingOptions = _serverConfigurationManager.GetConfiguration<BrandingOptions>("branding");
brandingOptions.SplashscreenLocation = filePath; brandingOptions.SplashscreenLocation = filePath;
@ -1930,10 +1956,10 @@ public class ImageController : BaseJellyfinApiController
} }
var responseHeaders = new Dictionary<string, string> var responseHeaders = new Dictionary<string, string>
{ {
{ "transferMode.dlna.org", "Interactive" }, { "transferMode.dlna.org", "Interactive" },
{ "realTimeInfo.dlna.org", "DLNA.ORG_TLAG=*" } { "realTimeInfo.dlna.org", "DLNA.ORG_TLAG=*" }
}; };
if (!imageInfo.IsLocalFile && item is not null) if (!imageInfo.IsLocalFile && item is not null)
{ {
@ -2096,4 +2122,23 @@ public class ImageController : BaseJellyfinApiController
return PhysicalFile(imagePath, imageContentType ?? MediaTypeNames.Text.Plain); 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;
}
} }

View file

@ -1,8 +1,8 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions; using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders; using Jellyfin.Api.ModelBinders;
using Jellyfin.Data.Entities; using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Dto;
@ -22,7 +22,7 @@ namespace Jellyfin.Api.Controllers;
/// The instant mix controller. /// The instant mix controller.
/// </summary> /// </summary>
[Route("")] [Route("")]
[Authorize(Policy = Policies.DefaultAuthorization)] [Authorize]
public class InstantMixController : BaseJellyfinApiController public class InstantMixController : BaseJellyfinApiController
{ {
private readonly IUserManager _userManager; private readonly IUserManager _userManager;
@ -75,7 +75,8 @@ public class InstantMixController : BaseJellyfinApiController
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes) [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
{ {
var item = _libraryManager.GetItemById(id); 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 ? null
: _userManager.GetUserById(userId.Value); : _userManager.GetUserById(userId.Value);
var dtoOptions = new DtoOptions { Fields = fields } var dtoOptions = new DtoOptions { Fields = fields }
@ -111,7 +112,8 @@ public class InstantMixController : BaseJellyfinApiController
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes) [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
{ {
var album = _libraryManager.GetItemById(id); 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 ? null
: _userManager.GetUserById(userId.Value); : _userManager.GetUserById(userId.Value);
var dtoOptions = new DtoOptions { Fields = fields } var dtoOptions = new DtoOptions { Fields = fields }
@ -147,7 +149,8 @@ public class InstantMixController : BaseJellyfinApiController
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes) [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
{ {
var playlist = (Playlist)_libraryManager.GetItemById(id); 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 ? null
: _userManager.GetUserById(userId.Value); : _userManager.GetUserById(userId.Value);
var dtoOptions = new DtoOptions { Fields = fields } var dtoOptions = new DtoOptions { Fields = fields }
@ -182,7 +185,8 @@ public class InstantMixController : BaseJellyfinApiController
[FromQuery] int? imageTypeLimit, [FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes) [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 ? null
: _userManager.GetUserById(userId.Value); : _userManager.GetUserById(userId.Value);
var dtoOptions = new DtoOptions { Fields = fields } var dtoOptions = new DtoOptions { Fields = fields }
@ -218,7 +222,8 @@ public class InstantMixController : BaseJellyfinApiController
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes) [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
{ {
var item = _libraryManager.GetItemById(id); 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 ? null
: _userManager.GetUserById(userId.Value); : _userManager.GetUserById(userId.Value);
var dtoOptions = new DtoOptions { Fields = fields } var dtoOptions = new DtoOptions { Fields = fields }
@ -254,7 +259,8 @@ public class InstantMixController : BaseJellyfinApiController
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes) [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
{ {
var item = _libraryManager.GetItemById(id); 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 ? null
: _userManager.GetUserById(userId.Value); : _userManager.GetUserById(userId.Value);
var dtoOptions = new DtoOptions { Fields = fields } var dtoOptions = new DtoOptions { Fields = fields }
@ -327,7 +333,8 @@ public class InstantMixController : BaseJellyfinApiController
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes) [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
{ {
var item = _libraryManager.GetItemById(id); 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 ? null
: _userManager.GetUserById(userId.Value); : _userManager.GetUserById(userId.Value);
var dtoOptions = new DtoOptions { Fields = fields } var dtoOptions = new DtoOptions { Fields = fields }

View file

@ -23,7 +23,7 @@ namespace Jellyfin.Api.Controllers;
/// Item lookup controller. /// Item lookup controller.
/// </summary> /// </summary>
[Route("")] [Route("")]
[Authorize(Policy = Policies.DefaultAuthorization)] [Authorize]
public class ItemLookupController : BaseJellyfinApiController public class ItemLookupController : BaseJellyfinApiController
{ {
private readonly IProviderManager _providerManager; private readonly IProviderManager _providerManager;

View file

@ -1,11 +1,11 @@
using System; using System;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Linq; using System.Linq;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions; using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers; using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders; using Jellyfin.Api.ModelBinders;
using Jellyfin.Data.Enums; using Jellyfin.Data.Enums;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
@ -25,7 +25,7 @@ namespace Jellyfin.Api.Controllers;
/// The items controller. /// The items controller.
/// </summary> /// </summary>
[Route("")] [Route("")]
[Authorize(Policy = Policies.DefaultAuthorization)] [Authorize]
public class ItemsController : BaseJellyfinApiController public class ItemsController : BaseJellyfinApiController
{ {
private readonly IUserManager _userManager; private readonly IUserManager _userManager;
@ -240,8 +240,9 @@ public class ItemsController : BaseJellyfinApiController
{ {
var isApiKey = User.GetIsApiKey(); var isApiKey = User.GetIsApiKey();
// if api key is used (auth.IsApiKey == true), then `user` will be null throughout this method // 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) userId = RequestHelpers.GetUserId(User, userId);
? _userManager.GetUserById(userId.Value) var user = !isApiKey && !userId.Value.Equals(default)
? _userManager.GetUserById(userId.Value) ?? throw new ResourceNotFoundException()
: null; : null;
// beyond this point, we're either using an api key or we have a valid user // 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) [FromQuery] bool excludeActiveSessions = false)
{ {
var user = _userManager.GetUserById(userId); var user = _userManager.GetUserById(userId);
if (user is null)
{
return NotFound();
}
var parentIdGuid = parentId ?? Guid.Empty; var parentIdGuid = parentId ?? Guid.Empty;
var dtoOptions = new DtoOptions { Fields = fields } var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(User) .AddClientFields(User)

View file

@ -9,6 +9,7 @@ using System.Threading.Tasks;
using Jellyfin.Api.Attributes; using Jellyfin.Api.Attributes;
using Jellyfin.Api.Constants; using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions; using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders; using Jellyfin.Api.ModelBinders;
using Jellyfin.Api.Models.LibraryDtos; using Jellyfin.Api.Models.LibraryDtos;
using Jellyfin.Data.Entities; using Jellyfin.Data.Entities;
@ -95,7 +96,7 @@ public class LibraryController : BaseJellyfinApiController
/// <response code="404">Item not found.</response> /// <response code="404">Item not found.</response>
/// <returns>A <see cref="FileStreamResult"/> with the original file.</returns> /// <returns>A <see cref="FileStreamResult"/> with the original file.</returns>
[HttpGet("Items/{itemId}/File")] [HttpGet("Items/{itemId}/File")]
[Authorize(Policy = Policies.DefaultAuthorization)] [Authorize]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesFile("video/*", "audio/*")] [ProducesFile("video/*", "audio/*")]
@ -116,7 +117,7 @@ public class LibraryController : BaseJellyfinApiController
/// <response code="200">Critic reviews returned.</response> /// <response code="200">Critic reviews returned.</response>
/// <returns>The list of critic reviews.</returns> /// <returns>The list of critic reviews.</returns>
[HttpGet("Items/{itemId}/CriticReviews")] [HttpGet("Items/{itemId}/CriticReviews")]
[Authorize(Policy = Policies.DefaultAuthorization)] [Authorize]
[Obsolete("This endpoint is obsolete.")] [Obsolete("This endpoint is obsolete.")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetCriticReviews() public ActionResult<QueryResult<BaseItemDto>> GetCriticReviews()
@ -134,7 +135,7 @@ public class LibraryController : BaseJellyfinApiController
/// <response code="404">Item not found.</response> /// <response code="404">Item not found.</response>
/// <returns>The item theme songs.</returns> /// <returns>The item theme songs.</returns>
[HttpGet("Items/{itemId}/ThemeSongs")] [HttpGet("Items/{itemId}/ThemeSongs")]
[Authorize(Policy = Policies.DefaultAuthorization)] [Authorize]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<ThemeMediaResult> GetThemeSongs( public ActionResult<ThemeMediaResult> GetThemeSongs(
@ -142,12 +143,13 @@ public class LibraryController : BaseJellyfinApiController
[FromQuery] Guid? userId, [FromQuery] Guid? userId,
[FromQuery] bool inheritFromParent = false) [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 ? null
: _userManager.GetUserById(userId.Value); : _userManager.GetUserById(userId.Value);
var item = itemId.Equals(default) var item = itemId.Equals(default)
? (userId is null || userId.Value.Equals(default) ? (userId.Value.Equals(default)
? _libraryManager.RootFolder ? _libraryManager.RootFolder
: _libraryManager.GetUserRootFolder()) : _libraryManager.GetUserRootFolder())
: _libraryManager.GetItemById(itemId); : _libraryManager.GetItemById(itemId);
@ -200,7 +202,7 @@ public class LibraryController : BaseJellyfinApiController
/// <response code="404">Item not found.</response> /// <response code="404">Item not found.</response>
/// <returns>The item theme videos.</returns> /// <returns>The item theme videos.</returns>
[HttpGet("Items/{itemId}/ThemeVideos")] [HttpGet("Items/{itemId}/ThemeVideos")]
[Authorize(Policy = Policies.DefaultAuthorization)] [Authorize]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<ThemeMediaResult> GetThemeVideos( public ActionResult<ThemeMediaResult> GetThemeVideos(
@ -208,12 +210,13 @@ public class LibraryController : BaseJellyfinApiController
[FromQuery] Guid? userId, [FromQuery] Guid? userId,
[FromQuery] bool inheritFromParent = false) [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 ? null
: _userManager.GetUserById(userId.Value); : _userManager.GetUserById(userId.Value);
var item = itemId.Equals(default) var item = itemId.Equals(default)
? (userId is null || userId.Value.Equals(default) ? (userId.Value.Equals(default)
? _libraryManager.RootFolder ? _libraryManager.RootFolder
: _libraryManager.GetUserRootFolder()) : _libraryManager.GetUserRootFolder())
: _libraryManager.GetItemById(itemId); : _libraryManager.GetItemById(itemId);
@ -266,7 +269,7 @@ public class LibraryController : BaseJellyfinApiController
/// <response code="404">Item not found.</response> /// <response code="404">Item not found.</response>
/// <returns>The item theme videos.</returns> /// <returns>The item theme videos.</returns>
[HttpGet("Items/{itemId}/ThemeMedia")] [HttpGet("Items/{itemId}/ThemeMedia")]
[Authorize(Policy = Policies.DefaultAuthorization)] [Authorize]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<AllThemeMediaResult> GetThemeMedia( public ActionResult<AllThemeMediaResult> GetThemeMedia(
[FromRoute, Required] Guid itemId, [FromRoute, Required] Guid itemId,
@ -283,6 +286,11 @@ public class LibraryController : BaseJellyfinApiController
userId, userId,
inheritFromParent); inheritFromParent);
if (themeSongs.Result is NotFoundObjectResult || themeVideos.Result is NotFoundObjectResult)
{
return NotFound();
}
return new AllThemeMediaResult return new AllThemeMediaResult
{ {
ThemeSongsResult = themeSongs?.Value, ThemeSongsResult = themeSongs?.Value,
@ -321,7 +329,7 @@ public class LibraryController : BaseJellyfinApiController
/// <response code="401">Unauthorized access.</response> /// <response code="401">Unauthorized access.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns> /// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpDelete("Items/{itemId}")] [HttpDelete("Items/{itemId}")]
[Authorize(Policy = Policies.DefaultAuthorization)] [Authorize]
[ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status401Unauthorized)]
public ActionResult DeleteItem(Guid itemId) public ActionResult DeleteItem(Guid itemId)
@ -350,7 +358,7 @@ public class LibraryController : BaseJellyfinApiController
/// <response code="401">Unauthorized access.</response> /// <response code="401">Unauthorized access.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns> /// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpDelete("Items")] [HttpDelete("Items")]
[Authorize(Policy = Policies.DefaultAuthorization)] [Authorize]
[ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status401Unauthorized)]
public ActionResult DeleteItems([FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids) public ActionResult DeleteItems([FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids)
@ -392,13 +400,14 @@ public class LibraryController : BaseJellyfinApiController
/// <response code="200">Item counts returned.</response> /// <response code="200">Item counts returned.</response>
/// <returns>Item counts.</returns> /// <returns>Item counts.</returns>
[HttpGet("Items/Counts")] [HttpGet("Items/Counts")]
[Authorize(Policy = Policies.DefaultAuthorization)] [Authorize]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<ItemCounts> GetItemCounts( public ActionResult<ItemCounts> GetItemCounts(
[FromQuery] Guid? userId, [FromQuery] Guid? userId,
[FromQuery] bool? isFavorite) [FromQuery] bool? isFavorite)
{ {
var user = userId is null || userId.Value.Equals(default) userId = RequestHelpers.GetUserId(User, userId);
var user = userId.Value.Equals(default)
? null ? null
: _userManager.GetUserById(userId.Value); : _userManager.GetUserById(userId.Value);
@ -426,12 +435,13 @@ public class LibraryController : BaseJellyfinApiController
/// <response code="404">Item not found.</response> /// <response code="404">Item not found.</response>
/// <returns>Item parents.</returns> /// <returns>Item parents.</returns>
[HttpGet("Items/{itemId}/Ancestors")] [HttpGet("Items/{itemId}/Ancestors")]
[Authorize(Policy = Policies.DefaultAuthorization)] [Authorize]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<IEnumerable<BaseItemDto>> GetAncestors([FromRoute, Required] Guid itemId, [FromQuery] Guid? userId) public ActionResult<IEnumerable<BaseItemDto>> GetAncestors([FromRoute, Required] Guid itemId, [FromQuery] Guid? userId)
{ {
var item = _libraryManager.GetItemById(itemId); var item = _libraryManager.GetItemById(itemId);
userId = RequestHelpers.GetUserId(User, userId);
if (item is null) if (item is null)
{ {
@ -440,7 +450,7 @@ public class LibraryController : BaseJellyfinApiController
var baseItemDtos = new List<BaseItemDto>(); var baseItemDtos = new List<BaseItemDto>();
var user = userId is null || userId.Value.Equals(default) var user = userId.Value.Equals(default)
? null ? null
: _userManager.GetUserById(userId.Value); : _userManager.GetUserById(userId.Value);
@ -452,6 +462,10 @@ public class LibraryController : BaseJellyfinApiController
if (user is not null) if (user is not null)
{ {
parent = TranslateParentItem(parent, user); parent = TranslateParentItem(parent, user);
if (parent is null)
{
break;
}
} }
baseItemDtos.Add(_dtoService.GetBaseItemDto(parent, dtoOptions, user)); baseItemDtos.Add(_dtoService.GetBaseItemDto(parent, dtoOptions, user));
@ -509,7 +523,7 @@ public class LibraryController : BaseJellyfinApiController
/// <returns>A <see cref="NoContentResult"/>.</returns> /// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpPost("Library/Series/Added", Name = "PostAddedSeries")] [HttpPost("Library/Series/Added", Name = "PostAddedSeries")]
[HttpPost("Library/Series/Updated")] [HttpPost("Library/Series/Updated")]
[Authorize(Policy = Policies.DefaultAuthorization)] [Authorize]
[ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult PostUpdatedSeries([FromQuery] string? tvdbId) public ActionResult PostUpdatedSeries([FromQuery] string? tvdbId)
{ {
@ -539,7 +553,7 @@ public class LibraryController : BaseJellyfinApiController
/// <returns>A <see cref="NoContentResult"/>.</returns> /// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpPost("Library/Movies/Added", Name = "PostAddedMovies")] [HttpPost("Library/Movies/Added", Name = "PostAddedMovies")]
[HttpPost("Library/Movies/Updated")] [HttpPost("Library/Movies/Updated")]
[Authorize(Policy = Policies.DefaultAuthorization)] [Authorize]
[ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult PostUpdatedMovies([FromQuery] string? tmdbId, [FromQuery] string? imdbId) public ActionResult PostUpdatedMovies([FromQuery] string? tmdbId, [FromQuery] string? imdbId)
{ {
@ -580,7 +594,7 @@ public class LibraryController : BaseJellyfinApiController
/// <response code="204">Report success.</response> /// <response code="204">Report success.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns> /// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpPost("Library/Media/Updated")] [HttpPost("Library/Media/Updated")]
[Authorize(Policy = Policies.DefaultAuthorization)] [Authorize]
[ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult PostUpdatedMedia([FromBody, Required] MediaUpdateInfoDto dto) public ActionResult PostUpdatedMedia([FromBody, Required] MediaUpdateInfoDto dto)
{ {
@ -657,7 +671,7 @@ public class LibraryController : BaseJellyfinApiController
[HttpGet("Shows/{itemId}/Similar", Name = "GetSimilarShows")] [HttpGet("Shows/{itemId}/Similar", Name = "GetSimilarShows")]
[HttpGet("Movies/{itemId}/Similar", Name = "GetSimilarMovies")] [HttpGet("Movies/{itemId}/Similar", Name = "GetSimilarMovies")]
[HttpGet("Trailers/{itemId}/Similar", Name = "GetSimilarTrailers")] [HttpGet("Trailers/{itemId}/Similar", Name = "GetSimilarTrailers")]
[Authorize(Policy = Policies.DefaultAuthorization)] [Authorize]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetSimilarItems( public ActionResult<QueryResult<BaseItemDto>> GetSimilarItems(
[FromRoute, Required] Guid itemId, [FromRoute, Required] Guid itemId,
@ -666,18 +680,24 @@ public class LibraryController : BaseJellyfinApiController
[FromQuery] int? limit, [FromQuery] int? limit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields) [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields)
{ {
userId = RequestHelpers.GetUserId(User, userId);
var item = itemId.Equals(default) var item = itemId.Equals(default)
? (userId is null || userId.Value.Equals(default) ? (userId.Value.Equals(default)
? _libraryManager.RootFolder ? _libraryManager.RootFolder
: _libraryManager.GetUserRootFolder()) : _libraryManager.GetUserRootFolder())
: _libraryManager.GetItemById(itemId); : _libraryManager.GetItemById(itemId);
if (item is null)
{
return NotFound();
}
if (item is Episode || (item is IItemByName && item is not MusicArtist)) if (item is Episode || (item is IItemByName && item is not MusicArtist))
{ {
return new QueryResult<BaseItemDto>(); return new QueryResult<BaseItemDto>();
} }
var user = userId is null || userId.Value.Equals(default) var user = userId.Value.Equals(default)
? null ? null
: _userManager.GetUserById(userId.Value); : _userManager.GetUserById(userId.Value);
var dtoOptions = new DtoOptions { Fields = fields } var dtoOptions = new DtoOptions { Fields = fields }
@ -802,32 +822,32 @@ public class LibraryController : BaseJellyfinApiController
Type = type, Type = type,
MetadataFetchers = plugins MetadataFetchers = plugins
.Where(i => string.Equals(i.ItemType, type, StringComparison.OrdinalIgnoreCase)) .Where(i => string.Equals(i.ItemType, type, StringComparison.OrdinalIgnoreCase))
.SelectMany(i => i.Plugins.Where(p => p.Type == MetadataPluginType.MetadataFetcher)) .SelectMany(i => i.Plugins.Where(p => p.Type == MetadataPluginType.MetadataFetcher))
.Select(i => new LibraryOptionInfoDto .Select(i => new LibraryOptionInfoDto
{ {
Name = i.Name, Name = i.Name,
DefaultEnabled = IsMetadataFetcherEnabledByDefault(i.Name, type, isNewLibrary) DefaultEnabled = IsMetadataFetcherEnabledByDefault(i.Name, type, isNewLibrary)
}) })
.DistinctBy(i => i.Name, StringComparer.OrdinalIgnoreCase) .DistinctBy(i => i.Name, StringComparer.OrdinalIgnoreCase)
.ToArray(), .ToArray(),
ImageFetchers = plugins ImageFetchers = plugins
.Where(i => string.Equals(i.ItemType, type, StringComparison.OrdinalIgnoreCase)) .Where(i => string.Equals(i.ItemType, type, StringComparison.OrdinalIgnoreCase))
.SelectMany(i => i.Plugins.Where(p => p.Type == MetadataPluginType.ImageFetcher)) .SelectMany(i => i.Plugins.Where(p => p.Type == MetadataPluginType.ImageFetcher))
.Select(i => new LibraryOptionInfoDto .Select(i => new LibraryOptionInfoDto
{ {
Name = i.Name, Name = i.Name,
DefaultEnabled = IsImageFetcherEnabledByDefault(i.Name, type, isNewLibrary) DefaultEnabled = IsImageFetcherEnabledByDefault(i.Name, type, isNewLibrary)
}) })
.DistinctBy(i => i.Name, StringComparer.OrdinalIgnoreCase) .DistinctBy(i => i.Name, StringComparer.OrdinalIgnoreCase)
.ToArray(), .ToArray(),
SupportedImageTypes = plugins SupportedImageTypes = plugins
.Where(i => string.Equals(i.ItemType, type, StringComparison.OrdinalIgnoreCase)) .Where(i => string.Equals(i.ItemType, type, StringComparison.OrdinalIgnoreCase))
.SelectMany(i => i.SupportedImageTypes ?? Array.Empty<ImageType>()) .SelectMany(i => i.SupportedImageTypes ?? Array.Empty<ImageType>())
.Distinct() .Distinct()
.ToArray(), .ToArray(),
DefaultImageOptions = defaultImageOptions ?? Array.Empty<ImageOption>() DefaultImageOptions = defaultImageOptions ?? Array.Empty<ImageOption>()
}); });
@ -920,13 +940,13 @@ public class LibraryController : BaseJellyfinApiController
if (string.Equals(name, "TheMovieDb", StringComparison.OrdinalIgnoreCase)) if (string.Equals(name, "TheMovieDb", StringComparison.OrdinalIgnoreCase))
{ {
return !(string.Equals(type, "Season", StringComparison.OrdinalIgnoreCase) return !(string.Equals(type, "Season", StringComparison.OrdinalIgnoreCase)
|| string.Equals(type, "Episode", StringComparison.OrdinalIgnoreCase) || string.Equals(type, "Episode", StringComparison.OrdinalIgnoreCase)
|| string.Equals(type, "MusicVideo", StringComparison.OrdinalIgnoreCase)); || string.Equals(type, "MusicVideo", StringComparison.OrdinalIgnoreCase));
} }
return string.Equals(name, "TheTVDB", StringComparison.OrdinalIgnoreCase) return string.Equals(name, "TheTVDB", StringComparison.OrdinalIgnoreCase)
|| string.Equals(name, "TheAudioDB", StringComparison.OrdinalIgnoreCase) || string.Equals(name, "TheAudioDB", StringComparison.OrdinalIgnoreCase)
|| string.Equals(name, "MusicBrainz", StringComparison.OrdinalIgnoreCase); || string.Equals(name, "MusicBrainz", StringComparison.OrdinalIgnoreCase);
} }
var metadataOptions = _serverConfigurationManager.Configuration.MetadataOptions var metadataOptions = _serverConfigurationManager.Configuration.MetadataOptions
@ -934,7 +954,7 @@ public class LibraryController : BaseJellyfinApiController
.ToArray(); .ToArray();
return metadataOptions.Length == 0 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) private bool IsImageFetcherEnabledByDefault(string name, string type, bool isNewLibrary)

View file

@ -17,14 +17,12 @@ using Jellyfin.Api.ModelBinders;
using Jellyfin.Api.Models.LiveTvDtos; using Jellyfin.Api.Models.LiveTvDtos;
using Jellyfin.Data.Enums; using Jellyfin.Data.Enums;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net; using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.Session; using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Dto; using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities; using MediaBrowser.Model.Entities;
@ -95,7 +93,7 @@ public class LiveTvController : BaseJellyfinApiController
/// </returns> /// </returns>
[HttpGet("Info")] [HttpGet("Info")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[Authorize(Policy = Policies.DefaultAuthorization)] [Authorize(Policy = Policies.LiveTvAccess)]
public ActionResult<LiveTvInfo> GetLiveTvInfo() public ActionResult<LiveTvInfo> GetLiveTvInfo()
{ {
return _liveTvManager.GetLiveTvInfo(CancellationToken.None); return _liveTvManager.GetLiveTvInfo(CancellationToken.None);
@ -131,7 +129,7 @@ public class LiveTvController : BaseJellyfinApiController
/// </returns> /// </returns>
[HttpGet("Channels")] [HttpGet("Channels")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[Authorize(Policy = Policies.DefaultAuthorization)] [Authorize(Policy = Policies.LiveTvAccess)]
public ActionResult<QueryResult<BaseItemDto>> GetLiveTvChannels( public ActionResult<QueryResult<BaseItemDto>> GetLiveTvChannels(
[FromQuery] ChannelType? type, [FromQuery] ChannelType? type,
[FromQuery] Guid? userId, [FromQuery] Guid? userId,
@ -155,6 +153,7 @@ public class LiveTvController : BaseJellyfinApiController
[FromQuery] bool enableFavoriteSorting = false, [FromQuery] bool enableFavoriteSorting = false,
[FromQuery] bool addCurrentProgram = true) [FromQuery] bool addCurrentProgram = true)
{ {
userId = RequestHelpers.GetUserId(User, userId);
var dtoOptions = new DtoOptions { Fields = fields } var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(User) .AddClientFields(User)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
@ -163,7 +162,7 @@ public class LiveTvController : BaseJellyfinApiController
new LiveTvChannelQuery new LiveTvChannelQuery
{ {
ChannelType = type, ChannelType = type,
UserId = userId ?? Guid.Empty, UserId = userId.Value,
StartIndex = startIndex, StartIndex = startIndex,
Limit = limit, Limit = limit,
IsFavorite = isFavorite, IsFavorite = isFavorite,
@ -182,7 +181,7 @@ public class LiveTvController : BaseJellyfinApiController
dtoOptions, dtoOptions,
CancellationToken.None); CancellationToken.None);
var user = userId is null || userId.Value.Equals(default) var user = userId.Value.Equals(default)
? null ? null
: _userManager.GetUserById(userId.Value); : _userManager.GetUserById(userId.Value);
@ -210,10 +209,11 @@ public class LiveTvController : BaseJellyfinApiController
/// <returns>An <see cref="OkResult"/> containing the live tv channel.</returns> /// <returns>An <see cref="OkResult"/> containing the live tv channel.</returns>
[HttpGet("Channels/{channelId}")] [HttpGet("Channels/{channelId}")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[Authorize(Policy = Policies.DefaultAuthorization)] [Authorize(Policy = Policies.LiveTvAccess)]
public ActionResult<BaseItemDto> GetChannel([FromRoute, Required] Guid channelId, [FromQuery] Guid? userId) public ActionResult<BaseItemDto> 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 ? null
: _userManager.GetUserById(userId.Value); : _userManager.GetUserById(userId.Value);
var item = channelId.Equals(default) var item = channelId.Equals(default)
@ -251,7 +251,7 @@ public class LiveTvController : BaseJellyfinApiController
/// <returns>An <see cref="OkResult"/> containing the live tv recordings.</returns> /// <returns>An <see cref="OkResult"/> containing the live tv recordings.</returns>
[HttpGet("Recordings")] [HttpGet("Recordings")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[Authorize(Policy = Policies.DefaultAuthorization)] [Authorize(Policy = Policies.LiveTvAccess)]
public ActionResult<QueryResult<BaseItemDto>> GetRecordings( public ActionResult<QueryResult<BaseItemDto>> GetRecordings(
[FromQuery] string? channelId, [FromQuery] string? channelId,
[FromQuery] Guid? userId, [FromQuery] Guid? userId,
@ -273,6 +273,7 @@ public class LiveTvController : BaseJellyfinApiController
[FromQuery] bool? isLibraryItem, [FromQuery] bool? isLibraryItem,
[FromQuery] bool enableTotalRecordCount = true) [FromQuery] bool enableTotalRecordCount = true)
{ {
userId = RequestHelpers.GetUserId(User, userId);
var dtoOptions = new DtoOptions { Fields = fields } var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(User) .AddClientFields(User)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
@ -281,7 +282,7 @@ public class LiveTvController : BaseJellyfinApiController
new RecordingQuery new RecordingQuery
{ {
ChannelId = channelId, ChannelId = channelId,
UserId = userId ?? Guid.Empty, UserId = userId.Value,
StartIndex = startIndex, StartIndex = startIndex,
Limit = limit, Limit = limit,
Status = status, Status = status,
@ -322,7 +323,7 @@ public class LiveTvController : BaseJellyfinApiController
/// <returns>An <see cref="OkResult"/> containing the live tv recordings.</returns> /// <returns>An <see cref="OkResult"/> containing the live tv recordings.</returns>
[HttpGet("Recordings/Series")] [HttpGet("Recordings/Series")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[Authorize(Policy = Policies.DefaultAuthorization)] [Authorize(Policy = Policies.LiveTvAccess)]
[Obsolete("This endpoint is obsolete.")] [Obsolete("This endpoint is obsolete.")]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "channelId", Justification = "Imported from ServiceStack")] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "channelId", Justification = "Imported from ServiceStack")]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Imported from ServiceStack")] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Imported from ServiceStack")]
@ -365,7 +366,7 @@ public class LiveTvController : BaseJellyfinApiController
/// <returns>An <see cref="OkResult"/> containing the recording groups.</returns> /// <returns>An <see cref="OkResult"/> containing the recording groups.</returns>
[HttpGet("Recordings/Groups")] [HttpGet("Recordings/Groups")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[Authorize(Policy = Policies.DefaultAuthorization)] [Authorize(Policy = Policies.LiveTvAccess)]
[Obsolete("This endpoint is obsolete.")] [Obsolete("This endpoint is obsolete.")]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Imported from ServiceStack")] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Imported from ServiceStack")]
public ActionResult<QueryResult<BaseItemDto>> GetRecordingGroups([FromQuery] Guid? userId) public ActionResult<QueryResult<BaseItemDto>> GetRecordingGroups([FromQuery] Guid? userId)
@ -381,10 +382,11 @@ public class LiveTvController : BaseJellyfinApiController
/// <returns>An <see cref="OkResult"/> containing the recording folders.</returns> /// <returns>An <see cref="OkResult"/> containing the recording folders.</returns>
[HttpGet("Recordings/Folders")] [HttpGet("Recordings/Folders")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[Authorize(Policy = Policies.DefaultAuthorization)] [Authorize(Policy = Policies.LiveTvAccess)]
public ActionResult<QueryResult<BaseItemDto>> GetRecordingFolders([FromQuery] Guid? userId) public ActionResult<QueryResult<BaseItemDto>> 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 ? null
: _userManager.GetUserById(userId.Value); : _userManager.GetUserById(userId.Value);
var folders = _liveTvManager.GetRecordingFolders(user); var folders = _liveTvManager.GetRecordingFolders(user);
@ -403,10 +405,11 @@ public class LiveTvController : BaseJellyfinApiController
/// <returns>An <see cref="OkResult"/> containing the live tv recording.</returns> /// <returns>An <see cref="OkResult"/> containing the live tv recording.</returns>
[HttpGet("Recordings/{recordingId}")] [HttpGet("Recordings/{recordingId}")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[Authorize(Policy = Policies.DefaultAuthorization)] [Authorize(Policy = Policies.LiveTvAccess)]
public ActionResult<BaseItemDto> GetRecording([FromRoute, Required] Guid recordingId, [FromQuery] Guid? userId) public ActionResult<BaseItemDto> 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 ? null
: _userManager.GetUserById(userId.Value); : _userManager.GetUserById(userId.Value);
var item = recordingId.Equals(default) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(recordingId); var item = recordingId.Equals(default) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(recordingId);
@ -425,10 +428,9 @@ public class LiveTvController : BaseJellyfinApiController
/// <returns>A <see cref="NoContentResult"/>.</returns> /// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpPost("Tuners/{tunerId}/Reset")] [HttpPost("Tuners/{tunerId}/Reset")]
[ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status204NoContent)]
[Authorize(Policy = Policies.DefaultAuthorization)] [Authorize(Policy = Policies.LiveTvManagement)]
public async Task<ActionResult> ResetTuner([FromRoute, Required] string tunerId) public async Task<ActionResult> ResetTuner([FromRoute, Required] string tunerId)
{ {
await AssertUserCanManageLiveTv().ConfigureAwait(false);
await _liveTvManager.ResetTuner(tunerId, CancellationToken.None).ConfigureAwait(false); await _liveTvManager.ResetTuner(tunerId, CancellationToken.None).ConfigureAwait(false);
return NoContent(); return NoContent();
} }
@ -443,7 +445,7 @@ public class LiveTvController : BaseJellyfinApiController
/// </returns> /// </returns>
[HttpGet("Timers/{timerId}")] [HttpGet("Timers/{timerId}")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[Authorize(Policy = Policies.DefaultAuthorization)] [Authorize(Policy = Policies.LiveTvAccess)]
public async Task<ActionResult<TimerInfoDto>> GetTimer([FromRoute, Required] string timerId) public async Task<ActionResult<TimerInfoDto>> GetTimer([FromRoute, Required] string timerId)
{ {
return await _liveTvManager.GetTimer(timerId, CancellationToken.None).ConfigureAwait(false); return await _liveTvManager.GetTimer(timerId, CancellationToken.None).ConfigureAwait(false);
@ -459,7 +461,7 @@ public class LiveTvController : BaseJellyfinApiController
/// </returns> /// </returns>
[HttpGet("Timers/Defaults")] [HttpGet("Timers/Defaults")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[Authorize(Policy = Policies.DefaultAuthorization)] [Authorize(Policy = Policies.LiveTvAccess)]
public async Task<ActionResult<SeriesTimerInfoDto>> GetDefaultTimer([FromQuery] string? programId) public async Task<ActionResult<SeriesTimerInfoDto>> GetDefaultTimer([FromQuery] string? programId)
{ {
return string.IsNullOrEmpty(programId) return string.IsNullOrEmpty(programId)
@ -479,7 +481,7 @@ public class LiveTvController : BaseJellyfinApiController
/// </returns> /// </returns>
[HttpGet("Timers")] [HttpGet("Timers")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[Authorize(Policy = Policies.DefaultAuthorization)] [Authorize(Policy = Policies.LiveTvAccess)]
public async Task<ActionResult<QueryResult<TimerInfoDto>>> GetTimers( public async Task<ActionResult<QueryResult<TimerInfoDto>>> GetTimers(
[FromQuery] string? channelId, [FromQuery] string? channelId,
[FromQuery] string? seriesTimerId, [FromQuery] string? seriesTimerId,
@ -533,7 +535,7 @@ public class LiveTvController : BaseJellyfinApiController
/// </returns> /// </returns>
[HttpGet("Programs")] [HttpGet("Programs")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[Authorize(Policy = Policies.DefaultAuthorization)] [Authorize(Policy = Policies.LiveTvAccess)]
public async Task<ActionResult<QueryResult<BaseItemDto>>> GetLiveTvPrograms( public async Task<ActionResult<QueryResult<BaseItemDto>>> GetLiveTvPrograms(
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] channelIds, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] channelIds,
[FromQuery] Guid? userId, [FromQuery] Guid? userId,
@ -563,7 +565,8 @@ public class LiveTvController : BaseJellyfinApiController
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery] bool enableTotalRecordCount = true) [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 ? null
: _userManager.GetUserById(userId.Value); : _userManager.GetUserById(userId.Value);
@ -616,7 +619,7 @@ public class LiveTvController : BaseJellyfinApiController
/// </returns> /// </returns>
[HttpPost("Programs")] [HttpPost("Programs")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[Authorize(Policy = Policies.DefaultAuthorization)] [Authorize(Policy = Policies.LiveTvAccess)]
public async Task<ActionResult<QueryResult<BaseItemDto>>> GetPrograms([FromBody] GetProgramsDto body) public async Task<ActionResult<QueryResult<BaseItemDto>>> GetPrograms([FromBody] GetProgramsDto body)
{ {
var user = body.UserId.Equals(default) ? null : _userManager.GetUserById(body.UserId); var user = body.UserId.Equals(default) ? null : _userManager.GetUserById(body.UserId);
@ -682,7 +685,7 @@ public class LiveTvController : BaseJellyfinApiController
/// <response code="200">Recommended epgs returned.</response> /// <response code="200">Recommended epgs returned.</response>
/// <returns>A <see cref="OkResult"/> containing the queryresult of recommended epgs.</returns> /// <returns>A <see cref="OkResult"/> containing the queryresult of recommended epgs.</returns>
[HttpGet("Programs/Recommended")] [HttpGet("Programs/Recommended")]
[Authorize(Policy = Policies.DefaultAuthorization)] [Authorize(Policy = Policies.LiveTvAccess)]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<QueryResult<BaseItemDto>>> GetRecommendedPrograms( public async Task<ActionResult<QueryResult<BaseItemDto>>> GetRecommendedPrograms(
[FromQuery] Guid? userId, [FromQuery] Guid? userId,
@ -702,7 +705,8 @@ public class LiveTvController : BaseJellyfinApiController
[FromQuery] bool? enableUserData, [FromQuery] bool? enableUserData,
[FromQuery] bool enableTotalRecordCount = true) [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 ? null
: _userManager.GetUserById(userId.Value); : _userManager.GetUserById(userId.Value);
@ -734,13 +738,14 @@ public class LiveTvController : BaseJellyfinApiController
/// <response code="200">Program returned.</response> /// <response code="200">Program returned.</response>
/// <returns>An <see cref="OkResult"/> containing the livetv program.</returns> /// <returns>An <see cref="OkResult"/> containing the livetv program.</returns>
[HttpGet("Programs/{programId}")] [HttpGet("Programs/{programId}")]
[Authorize(Policy = Policies.DefaultAuthorization)] [Authorize(Policy = Policies.LiveTvAccess)]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<BaseItemDto>> GetProgram( public async Task<ActionResult<BaseItemDto>> GetProgram(
[FromRoute, Required] string programId, [FromRoute, Required] string programId,
[FromQuery] Guid? userId) [FromQuery] Guid? userId)
{ {
var user = userId is null || userId.Value.Equals(default) userId = RequestHelpers.GetUserId(User, userId);
var user = userId.Value.Equals(default)
? null ? null
: _userManager.GetUserById(userId.Value); : _userManager.GetUserById(userId.Value);
@ -755,13 +760,11 @@ public class LiveTvController : BaseJellyfinApiController
/// <response code="404">Item not found.</response> /// <response code="404">Item not found.</response>
/// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if item not found.</returns> /// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if item not found.</returns>
[HttpDelete("Recordings/{recordingId}")] [HttpDelete("Recordings/{recordingId}")]
[Authorize(Policy = Policies.DefaultAuthorization)] [Authorize(Policy = Policies.LiveTvManagement)]
[ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult> DeleteRecording([FromRoute, Required] Guid recordingId) public ActionResult DeleteRecording([FromRoute, Required] Guid recordingId)
{ {
await AssertUserCanManageLiveTv().ConfigureAwait(false);
var item = _libraryManager.GetItemById(recordingId); var item = _libraryManager.GetItemById(recordingId);
if (item is null) if (item is null)
{ {
@ -783,11 +786,10 @@ public class LiveTvController : BaseJellyfinApiController
/// <response code="204">Timer deleted.</response> /// <response code="204">Timer deleted.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns> /// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpDelete("Timers/{timerId}")] [HttpDelete("Timers/{timerId}")]
[Authorize(Policy = Policies.DefaultAuthorization)] [Authorize(Policy = Policies.LiveTvManagement)]
[ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<ActionResult> CancelTimer([FromRoute, Required] string timerId) public async Task<ActionResult> CancelTimer([FromRoute, Required] string timerId)
{ {
await AssertUserCanManageLiveTv().ConfigureAwait(false);
await _liveTvManager.CancelTimer(timerId).ConfigureAwait(false); await _liveTvManager.CancelTimer(timerId).ConfigureAwait(false);
return NoContent(); return NoContent();
} }
@ -800,12 +802,11 @@ public class LiveTvController : BaseJellyfinApiController
/// <response code="204">Timer updated.</response> /// <response code="204">Timer updated.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns> /// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpPost("Timers/{timerId}")] [HttpPost("Timers/{timerId}")]
[Authorize(Policy = Policies.DefaultAuthorization)] [Authorize(Policy = Policies.LiveTvManagement)]
[ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status204NoContent)]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "timerId", Justification = "Imported from ServiceStack")] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "timerId", Justification = "Imported from ServiceStack")]
public async Task<ActionResult> UpdateTimer([FromRoute, Required] string timerId, [FromBody] TimerInfoDto timerInfo) public async Task<ActionResult> UpdateTimer([FromRoute, Required] string timerId, [FromBody] TimerInfoDto timerInfo)
{ {
await AssertUserCanManageLiveTv().ConfigureAwait(false);
await _liveTvManager.UpdateTimer(timerInfo, CancellationToken.None).ConfigureAwait(false); await _liveTvManager.UpdateTimer(timerInfo, CancellationToken.None).ConfigureAwait(false);
return NoContent(); return NoContent();
} }
@ -817,11 +818,10 @@ public class LiveTvController : BaseJellyfinApiController
/// <response code="204">Timer created.</response> /// <response code="204">Timer created.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns> /// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpPost("Timers")] [HttpPost("Timers")]
[Authorize(Policy = Policies.DefaultAuthorization)] [Authorize(Policy = Policies.LiveTvManagement)]
[ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<ActionResult> CreateTimer([FromBody] TimerInfoDto timerInfo) public async Task<ActionResult> CreateTimer([FromBody] TimerInfoDto timerInfo)
{ {
await AssertUserCanManageLiveTv().ConfigureAwait(false);
await _liveTvManager.CreateTimer(timerInfo, CancellationToken.None).ConfigureAwait(false); await _liveTvManager.CreateTimer(timerInfo, CancellationToken.None).ConfigureAwait(false);
return NoContent(); return NoContent();
} }
@ -834,7 +834,7 @@ public class LiveTvController : BaseJellyfinApiController
/// <response code="404">Series timer not found.</response> /// <response code="404">Series timer not found.</response>
/// <returns>A <see cref="OkResult"/> on success, or a <see cref="NotFoundResult"/> if timer not found.</returns> /// <returns>A <see cref="OkResult"/> on success, or a <see cref="NotFoundResult"/> if timer not found.</returns>
[HttpGet("SeriesTimers/{timerId}")] [HttpGet("SeriesTimers/{timerId}")]
[Authorize(Policy = Policies.DefaultAuthorization)] [Authorize(Policy = Policies.LiveTvAccess)]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<SeriesTimerInfoDto>> GetSeriesTimer([FromRoute, Required] string timerId) public async Task<ActionResult<SeriesTimerInfoDto>> GetSeriesTimer([FromRoute, Required] string timerId)
@ -856,7 +856,7 @@ public class LiveTvController : BaseJellyfinApiController
/// <response code="200">Timers returned.</response> /// <response code="200">Timers returned.</response>
/// <returns>An <see cref="OkResult"/> of live tv series timers.</returns> /// <returns>An <see cref="OkResult"/> of live tv series timers.</returns>
[HttpGet("SeriesTimers")] [HttpGet("SeriesTimers")]
[Authorize(Policy = Policies.DefaultAuthorization)] [Authorize(Policy = Policies.LiveTvAccess)]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<QueryResult<SeriesTimerInfoDto>>> GetSeriesTimers([FromQuery] string? sortBy, [FromQuery] SortOrder? sortOrder) public async Task<ActionResult<QueryResult<SeriesTimerInfoDto>>> GetSeriesTimers([FromQuery] string? sortBy, [FromQuery] SortOrder? sortOrder)
{ {
@ -876,11 +876,10 @@ public class LiveTvController : BaseJellyfinApiController
/// <response code="204">Timer cancelled.</response> /// <response code="204">Timer cancelled.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns> /// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpDelete("SeriesTimers/{timerId}")] [HttpDelete("SeriesTimers/{timerId}")]
[Authorize(Policy = Policies.DefaultAuthorization)] [Authorize(Policy = Policies.LiveTvManagement)]
[ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<ActionResult> CancelSeriesTimer([FromRoute, Required] string timerId) public async Task<ActionResult> CancelSeriesTimer([FromRoute, Required] string timerId)
{ {
await AssertUserCanManageLiveTv().ConfigureAwait(false);
await _liveTvManager.CancelSeriesTimer(timerId).ConfigureAwait(false); await _liveTvManager.CancelSeriesTimer(timerId).ConfigureAwait(false);
return NoContent(); return NoContent();
} }
@ -893,12 +892,11 @@ public class LiveTvController : BaseJellyfinApiController
/// <response code="204">Series timer updated.</response> /// <response code="204">Series timer updated.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns> /// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpPost("SeriesTimers/{timerId}")] [HttpPost("SeriesTimers/{timerId}")]
[Authorize(Policy = Policies.DefaultAuthorization)] [Authorize(Policy = Policies.LiveTvManagement)]
[ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status204NoContent)]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "timerId", Justification = "Imported from ServiceStack")] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "timerId", Justification = "Imported from ServiceStack")]
public async Task<ActionResult> UpdateSeriesTimer([FromRoute, Required] string timerId, [FromBody] SeriesTimerInfoDto seriesTimerInfo) public async Task<ActionResult> UpdateSeriesTimer([FromRoute, Required] string timerId, [FromBody] SeriesTimerInfoDto seriesTimerInfo)
{ {
await AssertUserCanManageLiveTv().ConfigureAwait(false);
await _liveTvManager.UpdateSeriesTimer(seriesTimerInfo, CancellationToken.None).ConfigureAwait(false); await _liveTvManager.UpdateSeriesTimer(seriesTimerInfo, CancellationToken.None).ConfigureAwait(false);
return NoContent(); return NoContent();
} }
@ -910,11 +908,10 @@ public class LiveTvController : BaseJellyfinApiController
/// <response code="204">Series timer info created.</response> /// <response code="204">Series timer info created.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns> /// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpPost("SeriesTimers")] [HttpPost("SeriesTimers")]
[Authorize(Policy = Policies.DefaultAuthorization)] [Authorize(Policy = Policies.LiveTvManagement)]
[ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<ActionResult> CreateSeriesTimer([FromBody] SeriesTimerInfoDto seriesTimerInfo) public async Task<ActionResult> CreateSeriesTimer([FromBody] SeriesTimerInfoDto seriesTimerInfo)
{ {
await AssertUserCanManageLiveTv().ConfigureAwait(false);
await _liveTvManager.CreateSeriesTimer(seriesTimerInfo, CancellationToken.None).ConfigureAwait(false); await _liveTvManager.CreateSeriesTimer(seriesTimerInfo, CancellationToken.None).ConfigureAwait(false);
return NoContent(); return NoContent();
} }
@ -925,7 +922,7 @@ public class LiveTvController : BaseJellyfinApiController
/// <param name="groupId">Group id.</param> /// <param name="groupId">Group id.</param>
/// <returns>A <see cref="NotFoundResult"/>.</returns> /// <returns>A <see cref="NotFoundResult"/>.</returns>
[HttpGet("Recordings/Groups/{groupId}")] [HttpGet("Recordings/Groups/{groupId}")]
[Authorize(Policy = Policies.DefaultAuthorization)] [Authorize(Policy = Policies.LiveTvAccess)]
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
[Obsolete("This endpoint is obsolete.")] [Obsolete("This endpoint is obsolete.")]
public ActionResult<BaseItemDto> GetRecordingGroup([FromRoute, Required] Guid groupId) public ActionResult<BaseItemDto> GetRecordingGroup([FromRoute, Required] Guid groupId)
@ -939,7 +936,7 @@ public class LiveTvController : BaseJellyfinApiController
/// <response code="200">Guid info returned.</response> /// <response code="200">Guid info returned.</response>
/// <returns>An <see cref="OkResult"/> containing the guide info.</returns> /// <returns>An <see cref="OkResult"/> containing the guide info.</returns>
[HttpGet("GuideInfo")] [HttpGet("GuideInfo")]
[Authorize(Policy = Policies.DefaultAuthorization)] [Authorize(Policy = Policies.LiveTvAccess)]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<GuideInfo> GetGuideInfo() public ActionResult<GuideInfo> GetGuideInfo()
{ {
@ -953,7 +950,7 @@ public class LiveTvController : BaseJellyfinApiController
/// <response code="200">Created tuner host returned.</response> /// <response code="200">Created tuner host returned.</response>
/// <returns>A <see cref="OkResult"/> containing the created tuner host.</returns> /// <returns>A <see cref="OkResult"/> containing the created tuner host.</returns>
[HttpPost("TunerHosts")] [HttpPost("TunerHosts")]
[Authorize(Policy = Policies.DefaultAuthorization)] [Authorize(Policy = Policies.LiveTvManagement)]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<TunerHostInfo>> AddTunerHost([FromBody] TunerHostInfo tunerHostInfo) public async Task<ActionResult<TunerHostInfo>> AddTunerHost([FromBody] TunerHostInfo tunerHostInfo)
{ {
@ -967,7 +964,7 @@ public class LiveTvController : BaseJellyfinApiController
/// <response code="204">Tuner host deleted.</response> /// <response code="204">Tuner host deleted.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns> /// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpDelete("TunerHosts")] [HttpDelete("TunerHosts")]
[Authorize(Policy = Policies.DefaultAuthorization)] [Authorize(Policy = Policies.LiveTvManagement)]
[ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult DeleteTunerHost([FromQuery] string? id) public ActionResult DeleteTunerHost([FromQuery] string? id)
{ {
@ -983,7 +980,7 @@ public class LiveTvController : BaseJellyfinApiController
/// <response code="200">Default listings provider info returned.</response> /// <response code="200">Default listings provider info returned.</response>
/// <returns>An <see cref="OkResult"/> containing the default listings provider info.</returns> /// <returns>An <see cref="OkResult"/> containing the default listings provider info.</returns>
[HttpGet("ListingProviders/Default")] [HttpGet("ListingProviders/Default")]
[Authorize(Policy = Policies.DefaultAuthorization)] [Authorize(Policy = Policies.LiveTvAccess)]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<ListingsProviderInfo> GetDefaultListingProvider() public ActionResult<ListingsProviderInfo> GetDefaultListingProvider()
{ {
@ -1000,7 +997,7 @@ public class LiveTvController : BaseJellyfinApiController
/// <response code="200">Created listings provider returned.</response> /// <response code="200">Created listings provider returned.</response>
/// <returns>A <see cref="OkResult"/> containing the created listings provider.</returns> /// <returns>A <see cref="OkResult"/> containing the created listings provider.</returns>
[HttpPost("ListingProviders")] [HttpPost("ListingProviders")]
[Authorize(Policy = Policies.DefaultAuthorization)] [Authorize(Policy = Policies.LiveTvManagement)]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[SuppressMessage("Microsoft.Performance", "CA5350:RemoveSha1", MessageId = "AddListingProvider", Justification = "Imported from ServiceStack")] [SuppressMessage("Microsoft.Performance", "CA5350:RemoveSha1", MessageId = "AddListingProvider", Justification = "Imported from ServiceStack")]
public async Task<ActionResult<ListingsProviderInfo>> AddListingProvider( public async Task<ActionResult<ListingsProviderInfo>> AddListingProvider(
@ -1026,7 +1023,7 @@ public class LiveTvController : BaseJellyfinApiController
/// <response code="204">Listing provider deleted.</response> /// <response code="204">Listing provider deleted.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns> /// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpDelete("ListingProviders")] [HttpDelete("ListingProviders")]
[Authorize(Policy = Policies.DefaultAuthorization)] [Authorize(Policy = Policies.LiveTvManagement)]
[ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult DeleteListingProvider([FromQuery] string? id) public ActionResult DeleteListingProvider([FromQuery] string? id)
{ {
@ -1044,7 +1041,7 @@ public class LiveTvController : BaseJellyfinApiController
/// <response code="200">Available lineups returned.</response> /// <response code="200">Available lineups returned.</response>
/// <returns>A <see cref="OkResult"/> containing the available lineups.</returns> /// <returns>A <see cref="OkResult"/> containing the available lineups.</returns>
[HttpGet("ListingProviders/Lineups")] [HttpGet("ListingProviders/Lineups")]
[Authorize(Policy = Policies.DefaultAuthorization)] [Authorize(Policy = Policies.LiveTvAccess)]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<IEnumerable<NameIdPair>>> GetLineups( public async Task<ActionResult<IEnumerable<NameIdPair>>> GetLineups(
[FromQuery] string? id, [FromQuery] string? id,
@ -1061,7 +1058,7 @@ public class LiveTvController : BaseJellyfinApiController
/// <response code="200">Available countries returned.</response> /// <response code="200">Available countries returned.</response>
/// <returns>A <see cref="FileResult"/> containing the available countries.</returns> /// <returns>A <see cref="FileResult"/> containing the available countries.</returns>
[HttpGet("ListingProviders/SchedulesDirect/Countries")] [HttpGet("ListingProviders/SchedulesDirect/Countries")]
[Authorize(Policy = Policies.DefaultAuthorization)] [Authorize(Policy = Policies.LiveTvAccess)]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesFile(MediaTypeNames.Application.Json)] [ProducesFile(MediaTypeNames.Application.Json)]
public async Task<ActionResult> GetSchedulesDirectCountries() public async Task<ActionResult> GetSchedulesDirectCountries()
@ -1082,7 +1079,7 @@ public class LiveTvController : BaseJellyfinApiController
/// <response code="200">Channel mapping options returned.</response> /// <response code="200">Channel mapping options returned.</response>
/// <returns>An <see cref="OkResult"/> containing the channel mapping options.</returns> /// <returns>An <see cref="OkResult"/> containing the channel mapping options.</returns>
[HttpGet("ChannelMappingOptions")] [HttpGet("ChannelMappingOptions")]
[Authorize(Policy = Policies.DefaultAuthorization)] [Authorize(Policy = Policies.LiveTvAccess)]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<ChannelMappingOptionsDto>> GetChannelMappingOptions([FromQuery] string? providerId) public async Task<ActionResult<ChannelMappingOptionsDto>> GetChannelMappingOptions([FromQuery] string? providerId)
{ {
@ -1120,7 +1117,7 @@ public class LiveTvController : BaseJellyfinApiController
/// <response code="200">Created channel mapping returned.</response> /// <response code="200">Created channel mapping returned.</response>
/// <returns>An <see cref="OkResult"/> containing the created channel mapping.</returns> /// <returns>An <see cref="OkResult"/> containing the created channel mapping.</returns>
[HttpPost("ChannelMappings")] [HttpPost("ChannelMappings")]
[Authorize(Policy = Policies.DefaultAuthorization)] [Authorize(Policy = Policies.LiveTvManagement)]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<TunerChannelMapping>> SetChannelMapping([FromBody, Required] SetChannelMappingDto setChannelMappingDto) public async Task<ActionResult<TunerChannelMapping>> SetChannelMapping([FromBody, Required] SetChannelMappingDto setChannelMappingDto)
{ {
@ -1133,7 +1130,7 @@ public class LiveTvController : BaseJellyfinApiController
/// <response code="200">Tuner host types returned.</response> /// <response code="200">Tuner host types returned.</response>
/// <returns>An <see cref="OkResult"/> containing the tuner host types.</returns> /// <returns>An <see cref="OkResult"/> containing the tuner host types.</returns>
[HttpGet("TunerHosts/Types")] [HttpGet("TunerHosts/Types")]
[Authorize(Policy = Policies.DefaultAuthorization)] [Authorize(Policy = Policies.LiveTvAccess)]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<IEnumerable<NameIdPair>> GetTunerHostTypes() public ActionResult<IEnumerable<NameIdPair>> GetTunerHostTypes()
{ {
@ -1148,7 +1145,7 @@ public class LiveTvController : BaseJellyfinApiController
/// <returns>An <see cref="OkResult"/> containing the tuners.</returns> /// <returns>An <see cref="OkResult"/> containing the tuners.</returns>
[HttpGet("Tuners/Discvover", Name = "DiscvoverTuners")] [HttpGet("Tuners/Discvover", Name = "DiscvoverTuners")]
[HttpGet("Tuners/Discover")] [HttpGet("Tuners/Discover")]
[Authorize(Policy = Policies.DefaultAuthorization)] [Authorize(Policy = Policies.LiveTvManagement)]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<IEnumerable<TunerHostInfo>>> DiscoverTuners([FromQuery] bool newDevicesOnly = false) public async Task<ActionResult<IEnumerable<TunerHostInfo>>> DiscoverTuners([FromQuery] bool newDevicesOnly = false)
{ {
@ -1208,26 +1205,4 @@ public class LiveTvController : BaseJellyfinApiController
var liveStream = new ProgressiveFileStream(liveStreamInfo.GetStream()); var liveStream = new ProgressiveFileStream(liveStreamInfo.GetStream());
return new FileStreamResult(liveStream, MimeTypes.GetMimeType("file." + container)); 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.");
}
}
} }

View file

@ -5,7 +5,6 @@ using System.Linq;
using System.Net.Mime; using System.Net.Mime;
using System.Threading.Tasks; using System.Threading.Tasks;
using Jellyfin.Api.Attributes; using Jellyfin.Api.Attributes;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions; using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers; using Jellyfin.Api.Helpers;
using Jellyfin.Api.Models.MediaInfoDtos; using Jellyfin.Api.Models.MediaInfoDtos;
@ -25,7 +24,7 @@ namespace Jellyfin.Api.Controllers;
/// The media info controller. /// The media info controller.
/// </summary> /// </summary>
[Route("")] [Route("")]
[Authorize(Policy = Policies.DefaultAuthorization)] [Authorize]
public class MediaInfoController : BaseJellyfinApiController public class MediaInfoController : BaseJellyfinApiController
{ {
private readonly IMediaSourceManager _mediaSourceManager; private readonly IMediaSourceManager _mediaSourceManager;
@ -133,6 +132,7 @@ public class MediaInfoController : BaseJellyfinApiController
// Copy params from posted body // Copy params from posted body
// TODO clean up when breaking API compatibility. // TODO clean up when breaking API compatibility.
userId ??= playbackInfoDto?.UserId; userId ??= playbackInfoDto?.UserId;
userId = RequestHelpers.GetUserId(User, userId);
maxStreamingBitrate ??= playbackInfoDto?.MaxStreamingBitrate; maxStreamingBitrate ??= playbackInfoDto?.MaxStreamingBitrate;
startTimeTicks ??= playbackInfoDto?.StartTimeTicks; startTimeTicks ??= playbackInfoDto?.StartTimeTicks;
audioStreamIndex ??= playbackInfoDto?.AudioStreamIndex; audioStreamIndex ??= playbackInfoDto?.AudioStreamIndex;
@ -254,10 +254,12 @@ public class MediaInfoController : BaseJellyfinApiController
[FromQuery] bool? enableDirectPlay, [FromQuery] bool? enableDirectPlay,
[FromQuery] bool? enableDirectStream) [FromQuery] bool? enableDirectStream)
{ {
userId ??= openLiveStreamDto?.UserId;
userId = RequestHelpers.GetUserId(User, userId);
var request = new LiveStreamRequest var request = new LiveStreamRequest
{ {
OpenToken = openToken ?? openLiveStreamDto?.OpenToken, OpenToken = openToken ?? openLiveStreamDto?.OpenToken,
UserId = userId ?? openLiveStreamDto?.UserId ?? Guid.Empty, UserId = userId.Value,
PlaySessionId = playSessionId ?? openLiveStreamDto?.PlaySessionId, PlaySessionId = playSessionId ?? openLiveStreamDto?.PlaySessionId,
MaxStreamingBitrate = maxStreamingBitrate ?? openLiveStreamDto?.MaxStreamingBitrate, MaxStreamingBitrate = maxStreamingBitrate ?? openLiveStreamDto?.MaxStreamingBitrate,
StartTimeTicks = startTimeTicks ?? openLiveStreamDto?.StartTimeTicks, StartTimeTicks = startTimeTicks ?? openLiveStreamDto?.StartTimeTicks,

View file

@ -2,8 +2,8 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions; using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders; using Jellyfin.Api.ModelBinders;
using Jellyfin.Data.Entities; using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums; using Jellyfin.Data.Enums;
@ -23,7 +23,7 @@ namespace Jellyfin.Api.Controllers;
/// <summary> /// <summary>
/// Movies controller. /// Movies controller.
/// </summary> /// </summary>
[Authorize(Policy = Policies.DefaultAuthorization)] [Authorize]
public class MoviesController : BaseJellyfinApiController public class MoviesController : BaseJellyfinApiController
{ {
private readonly IUserManager _userManager; private readonly IUserManager _userManager;
@ -68,7 +68,8 @@ public class MoviesController : BaseJellyfinApiController
[FromQuery] int categoryLimit = 5, [FromQuery] int categoryLimit = 5,
[FromQuery] int itemLimit = 8) [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 ? null
: _userManager.GetUserById(userId.Value); : _userManager.GetUserById(userId.Value);
var dtoOptions = new DtoOptions { Fields = fields } var dtoOptions = new DtoOptions { Fields = fields }

View file

@ -1,7 +1,6 @@
using System; using System;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Linq; using System.Linq;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions; using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers; using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders; using Jellyfin.Api.ModelBinders;
@ -23,7 +22,7 @@ namespace Jellyfin.Api.Controllers;
/// <summary> /// <summary>
/// The music genres controller. /// The music genres controller.
/// </summary> /// </summary>
[Authorize(Policy = Policies.DefaultAuthorization)] [Authorize]
public class MusicGenresController : BaseJellyfinApiController public class MusicGenresController : BaseJellyfinApiController
{ {
private readonly ILibraryManager _libraryManager; private readonly ILibraryManager _libraryManager;
@ -91,11 +90,12 @@ public class MusicGenresController : BaseJellyfinApiController
[FromQuery] bool? enableImages = true, [FromQuery] bool? enableImages = true,
[FromQuery] bool enableTotalRecordCount = true) [FromQuery] bool enableTotalRecordCount = true)
{ {
userId = RequestHelpers.GetUserId(User, userId);
var dtoOptions = new DtoOptions { Fields = fields } var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(User) .AddClientFields(User)
.AddAdditionalDtoOptions(enableImages, false, imageTypeLimit, enableImageTypes); .AddAdditionalDtoOptions(enableImages, false, imageTypeLimit, enableImageTypes);
User? user = userId is null || userId.Value.Equals(default) User? user = userId.Value.Equals(default)
? null ? null
: _userManager.GetUserById(userId.Value); : _userManager.GetUserById(userId.Value);
@ -145,6 +145,7 @@ public class MusicGenresController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<BaseItemDto> GetMusicGenre([FromRoute, Required] string genreName, [FromQuery] Guid? userId) public ActionResult<BaseItemDto> GetMusicGenre([FromRoute, Required] string genreName, [FromQuery] Guid? userId)
{ {
userId = RequestHelpers.GetUserId(User, userId);
var dtoOptions = new DtoOptions().AddClientFields(User); var dtoOptions = new DtoOptions().AddClientFields(User);
MusicGenre? item; MusicGenre? item;
@ -158,7 +159,12 @@ public class MusicGenresController : BaseJellyfinApiController
item = _libraryManager.GetMusicGenre(genreName); 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); var user = _userManager.GetUserById(userId.Value);

View file

@ -17,7 +17,7 @@ namespace Jellyfin.Api.Controllers;
/// Package Controller. /// Package Controller.
/// </summary> /// </summary>
[Route("")] [Route("")]
[Authorize(Policy = Policies.DefaultAuthorization)] [Authorize]
public class PackageController : BaseJellyfinApiController public class PackageController : BaseJellyfinApiController
{ {
private readonly IInstallationManager _installationManager; private readonly IInstallationManager _installationManager;

View file

@ -1,8 +1,8 @@
using System; using System;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Linq; using System.Linq;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions; using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders; using Jellyfin.Api.ModelBinders;
using Jellyfin.Data.Entities; using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Dto;
@ -20,7 +20,7 @@ namespace Jellyfin.Api.Controllers;
/// <summary> /// <summary>
/// Persons controller. /// Persons controller.
/// </summary> /// </summary>
[Authorize(Policy = Policies.DefaultAuthorization)] [Authorize]
public class PersonsController : BaseJellyfinApiController public class PersonsController : BaseJellyfinApiController
{ {
private readonly ILibraryManager _libraryManager; private readonly ILibraryManager _libraryManager;
@ -78,11 +78,12 @@ public class PersonsController : BaseJellyfinApiController
[FromQuery] Guid? userId, [FromQuery] Guid? userId,
[FromQuery] bool? enableImages = true) [FromQuery] bool? enableImages = true)
{ {
userId = RequestHelpers.GetUserId(User, userId);
var dtoOptions = new DtoOptions { Fields = fields } var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(User) .AddClientFields(User)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
User? user = userId is null || userId.Value.Equals(default) User? user = userId.Value.Equals(default)
? null ? null
: _userManager.GetUserById(userId.Value); : _userManager.GetUserById(userId.Value);
@ -118,6 +119,7 @@ public class PersonsController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<BaseItemDto> GetPerson([FromRoute, Required] string name, [FromQuery] Guid? userId) public ActionResult<BaseItemDto> GetPerson([FromRoute, Required] string name, [FromQuery] Guid? userId)
{ {
userId = RequestHelpers.GetUserId(User, userId);
var dtoOptions = new DtoOptions() var dtoOptions = new DtoOptions()
.AddClientFields(User); .AddClientFields(User);
@ -127,7 +129,7 @@ public class PersonsController : BaseJellyfinApiController
return NotFound(); return NotFound();
} }
if (userId.HasValue && !userId.Value.Equals(default)) if (!userId.Value.Equals(default))
{ {
var user = _userManager.GetUserById(userId.Value); var user = _userManager.GetUserById(userId.Value);
return _dtoService.GetBaseItemDto(item, dtoOptions, user); return _dtoService.GetBaseItemDto(item, dtoOptions, user);

View file

@ -4,8 +4,8 @@ using System.ComponentModel.DataAnnotations;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Jellyfin.Api.Attributes; using Jellyfin.Api.Attributes;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions; using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders; using Jellyfin.Api.ModelBinders;
using Jellyfin.Api.Models.PlaylistDtos; using Jellyfin.Api.Models.PlaylistDtos;
using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Dto;
@ -25,7 +25,7 @@ namespace Jellyfin.Api.Controllers;
/// <summary> /// <summary>
/// Playlists controller. /// Playlists controller.
/// </summary> /// </summary>
[Authorize(Policy = Policies.DefaultAuthorization)] [Authorize]
public class PlaylistsController : BaseJellyfinApiController public class PlaylistsController : BaseJellyfinApiController
{ {
private readonly IPlaylistManager _playlistManager; private readonly IPlaylistManager _playlistManager;
@ -82,11 +82,13 @@ public class PlaylistsController : BaseJellyfinApiController
ids = createPlaylistRequest?.Ids ?? Array.Empty<Guid>(); ids = createPlaylistRequest?.Ids ?? Array.Empty<Guid>();
} }
userId ??= createPlaylistRequest?.UserId ?? default;
userId = RequestHelpers.GetUserId(User, userId);
var result = await _playlistManager.CreatePlaylist(new PlaylistCreationRequest var result = await _playlistManager.CreatePlaylist(new PlaylistCreationRequest
{ {
Name = name ?? createPlaylistRequest?.Name, Name = name ?? createPlaylistRequest?.Name,
ItemIdList = ids, ItemIdList = ids,
UserId = userId ?? createPlaylistRequest?.UserId ?? default, UserId = userId.Value,
MediaType = mediaType ?? createPlaylistRequest?.MediaType MediaType = mediaType ?? createPlaylistRequest?.MediaType
}).ConfigureAwait(false); }).ConfigureAwait(false);
@ -108,7 +110,8 @@ public class PlaylistsController : BaseJellyfinApiController
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids,
[FromQuery] Guid? userId) [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(); return NoContent();
} }

View file

@ -2,7 +2,6 @@ using System;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Threading.Tasks; using System.Threading.Tasks;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions; using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers; using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders; using Jellyfin.Api.ModelBinders;
@ -23,7 +22,7 @@ namespace Jellyfin.Api.Controllers;
/// Playstate controller. /// Playstate controller.
/// </summary> /// </summary>
[Route("")] [Route("")]
[Authorize(Policy = Policies.DefaultAuthorization)] [Authorize]
public class PlaystateController : BaseJellyfinApiController public class PlaystateController : BaseJellyfinApiController
{ {
private readonly IUserManager _userManager; private readonly IUserManager _userManager;
@ -77,6 +76,11 @@ public class PlaystateController : BaseJellyfinApiController
[FromQuery, ModelBinder(typeof(LegacyDateTimeModelBinder))] DateTime? datePlayed) [FromQuery, ModelBinder(typeof(LegacyDateTimeModelBinder))] DateTime? datePlayed)
{ {
var user = _userManager.GetUserById(userId); var user = _userManager.GetUserById(userId);
if (user is null)
{
return NotFound();
}
var session = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); var session = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
var item = _libraryManager.GetItemById(itemId); var item = _libraryManager.GetItemById(itemId);
@ -89,6 +93,11 @@ public class PlaystateController : BaseJellyfinApiController
foreach (var additionalUserInfo in session.AdditionalUsers) foreach (var additionalUserInfo in session.AdditionalUsers)
{ {
var additionalUser = _userManager.GetUserById(additionalUserInfo.UserId); var additionalUser = _userManager.GetUserById(additionalUserInfo.UserId);
if (additionalUser is null)
{
return NotFound();
}
UpdatePlayedStatus(additionalUser, item, true, datePlayed); UpdatePlayedStatus(additionalUser, item, true, datePlayed);
} }
@ -109,6 +118,11 @@ public class PlaystateController : BaseJellyfinApiController
public async Task<ActionResult<UserItemDataDto>> MarkUnplayedItem([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId) public async Task<ActionResult<UserItemDataDto>> MarkUnplayedItem([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId)
{ {
var user = _userManager.GetUserById(userId); var user = _userManager.GetUserById(userId);
if (user is null)
{
return NotFound();
}
var session = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); var session = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
var item = _libraryManager.GetItemById(itemId); var item = _libraryManager.GetItemById(itemId);
@ -121,6 +135,11 @@ public class PlaystateController : BaseJellyfinApiController
foreach (var additionalUserInfo in session.AdditionalUsers) foreach (var additionalUserInfo in session.AdditionalUsers)
{ {
var additionalUser = _userManager.GetUserById(additionalUserInfo.UserId); var additionalUser = _userManager.GetUserById(additionalUserInfo.UserId);
if (additionalUser is null)
{
return NotFound();
}
UpdatePlayedStatus(additionalUser, item, false, null); UpdatePlayedStatus(additionalUser, item, false, null);
} }

View file

@ -21,7 +21,7 @@ namespace Jellyfin.Api.Controllers;
/// <summary> /// <summary>
/// Plugins controller. /// Plugins controller.
/// </summary> /// </summary>
[Authorize(Policy = Policies.DefaultAuthorization)] [Authorize]
public class PluginsController : BaseJellyfinApiController public class PluginsController : BaseJellyfinApiController
{ {
private readonly IInstallationManager _installationManager; private readonly IInstallationManager _installationManager;

View file

@ -3,6 +3,7 @@ using System.ComponentModel.DataAnnotations;
using System.Threading.Tasks; using System.Threading.Tasks;
using Jellyfin.Api.Constants; using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions; using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Authentication; using MediaBrowser.Controller.Authentication;
using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Net;
@ -111,22 +112,16 @@ public class QuickConnectController : BaseJellyfinApiController
/// <response code="403">Unknown user id.</response> /// <response code="403">Unknown user id.</response>
/// <returns>Boolean indicating if the authorization was successful.</returns> /// <returns>Boolean indicating if the authorization was successful.</returns>
[HttpPost("Authorize")] [HttpPost("Authorize")]
[Authorize(Policy = Policies.DefaultAuthorization)] [Authorize]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status403Forbidden)]
public async Task<ActionResult<bool>> AuthorizeQuickConnect([FromQuery, Required] string code, [FromQuery] Guid? userId = null) public async Task<ActionResult<bool>> AuthorizeQuickConnect([FromQuery, Required] string code, [FromQuery] Guid? userId = null)
{ {
var currentUserId = User.GetUserId(); userId = RequestHelpers.GetUserId(User, userId);
var actualUserId = userId ?? currentUserId;
if (actualUserId.Equals(default) || (!userId.Equals(currentUserId) && !User.IsInRole(UserRoles.Administrator)))
{
return Forbid("Unknown user id");
}
try try
{ {
return await _quickConnect.AuthorizeRequest(actualUserId, code).ConfigureAwait(false); return await _quickConnect.AuthorizeRequest(userId.Value, code).ConfigureAwait(false);
} }
catch (AuthenticationException) catch (AuthenticationException)
{ {

View file

@ -56,7 +56,7 @@ public class RemoteImageController : BaseJellyfinApiController
/// <response code="404">Item not found.</response> /// <response code="404">Item not found.</response>
/// <returns>Remote Image Result.</returns> /// <returns>Remote Image Result.</returns>
[HttpGet("Items/{itemId}/RemoteImages")] [HttpGet("Items/{itemId}/RemoteImages")]
[Authorize(Policy = Policies.DefaultAuthorization)] [Authorize]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<RemoteImageResult>> GetRemoteImages( public async Task<ActionResult<RemoteImageResult>> GetRemoteImages(
@ -121,7 +121,7 @@ public class RemoteImageController : BaseJellyfinApiController
/// <response code="404">Item not found.</response> /// <response code="404">Item not found.</response>
/// <returns>List of remote image providers.</returns> /// <returns>List of remote image providers.</returns>
[HttpGet("Items/{itemId}/RemoteImages/Providers")] [HttpGet("Items/{itemId}/RemoteImages/Providers")]
[Authorize(Policy = Policies.DefaultAuthorization)] [Authorize]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<IEnumerable<ImageProviderInfo>> GetRemoteImageProviders([FromRoute, Required] Guid itemId) public ActionResult<IEnumerable<ImageProviderInfo>> GetRemoteImageProviders([FromRoute, Required] Guid itemId)

View file

@ -4,6 +4,7 @@ using System.ComponentModel.DataAnnotations;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
using Jellyfin.Api.Constants; using Jellyfin.Api.Constants;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders; using Jellyfin.Api.ModelBinders;
using Jellyfin.Data.Enums; using Jellyfin.Data.Enums;
using Jellyfin.Extensions; using Jellyfin.Extensions;
@ -26,7 +27,7 @@ namespace Jellyfin.Api.Controllers;
/// Search controller. /// Search controller.
/// </summary> /// </summary>
[Route("Search/Hints")] [Route("Search/Hints")]
[Authorize(Policy = Policies.DefaultAuthorization)] [Authorize]
public class SearchController : BaseJellyfinApiController public class SearchController : BaseJellyfinApiController
{ {
private readonly ISearchEngine _searchEngine; private readonly ISearchEngine _searchEngine;
@ -99,6 +100,7 @@ public class SearchController : BaseJellyfinApiController
[FromQuery] bool includeStudios = true, [FromQuery] bool includeStudios = true,
[FromQuery] bool includeArtists = true) [FromQuery] bool includeArtists = true)
{ {
userId = RequestHelpers.GetUserId(User, userId);
var result = _searchEngine.GetSearchHints(new SearchQuery var result = _searchEngine.GetSearchHints(new SearchQuery
{ {
Limit = limit, Limit = limit,
@ -109,7 +111,7 @@ public class SearchController : BaseJellyfinApiController
IncludePeople = includePeople, IncludePeople = includePeople,
IncludeStudios = includeStudios, IncludeStudios = includeStudios,
StartIndex = startIndex, StartIndex = startIndex,
UserId = userId ?? Guid.Empty, UserId = userId.Value,
IncludeItemTypes = includeItemTypes, IncludeItemTypes = includeItemTypes,
ExcludeItemTypes = excludeItemTypes, ExcludeItemTypes = excludeItemTypes,
MediaTypes = mediaTypes, MediaTypes = mediaTypes,

Some files were not shown because too many files have changed in this diff Show more