mirror of
				https://github.com/jellyfin/jellyfin.git
				synced 2025-11-03 19:17:24 -05:00 
			
		
		
		
	Merge branch 'master' into network-rewrite
This commit is contained in:
		
						commit
						c5a363a007
					
				@ -54,9 +54,9 @@
 | 
			
		||||
    <PackageVersion Include="NEbml" Version="0.11.0" />
 | 
			
		||||
    <PackageVersion Include="Newtonsoft.Json" Version="13.0.2" />
 | 
			
		||||
    <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" Version="7.0.0" />
 | 
			
		||||
    <PackageVersion Include="prometheus-net" Version="8.0.0" />
 | 
			
		||||
    <PackageVersion Include="Serilog.AspNetCore" Version="6.1.0" />
 | 
			
		||||
    <PackageVersion Include="Serilog.Enrichers.Thread" Version="3.1.0" />
 | 
			
		||||
    <PackageVersion Include="Serilog.Settings.Configuration" Version="3.4.0" />
 | 
			
		||||
 | 
			
		||||
@ -3,6 +3,7 @@ using System.Globalization;
 | 
			
		||||
using System.IO;
 | 
			
		||||
using System.Text.RegularExpressions;
 | 
			
		||||
using Emby.Naming.Common;
 | 
			
		||||
using Jellyfin.Extensions;
 | 
			
		||||
 | 
			
		||||
namespace Emby.Naming.Audio
 | 
			
		||||
{
 | 
			
		||||
@ -58,13 +59,7 @@ namespace Emby.Naming.Audio
 | 
			
		||||
 | 
			
		||||
                var tmp = trimmedFilename.Slice(prefix.Length).Trim();
 | 
			
		||||
 | 
			
		||||
                int index = tmp.IndexOf(' ');
 | 
			
		||||
                if (index != -1)
 | 
			
		||||
                {
 | 
			
		||||
                    tmp = tmp.Slice(0, index);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (int.TryParse(tmp, NumberStyles.Integer, CultureInfo.InvariantCulture, out _))
 | 
			
		||||
                if (int.TryParse(tmp.LeftPart(' '), CultureInfo.InvariantCulture, out _))
 | 
			
		||||
                {
 | 
			
		||||
                    return true;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
@ -40,7 +40,7 @@ namespace Emby.Naming.AudioBook
 | 
			
		||||
                        var value = match.Groups["chapter"];
 | 
			
		||||
                        if (value.Success)
 | 
			
		||||
                        {
 | 
			
		||||
                            if (int.TryParse(value.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intValue))
 | 
			
		||||
                            if (int.TryParse(value.ValueSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intValue))
 | 
			
		||||
                            {
 | 
			
		||||
                                result.ChapterNumber = intValue;
 | 
			
		||||
                            }
 | 
			
		||||
@ -52,7 +52,7 @@ namespace Emby.Naming.AudioBook
 | 
			
		||||
                        var value = match.Groups["part"];
 | 
			
		||||
                        if (value.Success)
 | 
			
		||||
                        {
 | 
			
		||||
                            if (int.TryParse(value.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intValue))
 | 
			
		||||
                            if (int.TryParse(value.ValueSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intValue))
 | 
			
		||||
                            {
 | 
			
		||||
                                result.PartNumber = intValue;
 | 
			
		||||
                            }
 | 
			
		||||
 | 
			
		||||
@ -47,7 +47,7 @@ namespace Emby.Naming.AudioBook
 | 
			
		||||
                        var value = match.Groups["year"];
 | 
			
		||||
                        if (value.Success)
 | 
			
		||||
                        {
 | 
			
		||||
                            if (int.TryParse(value.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intValue))
 | 
			
		||||
                            if (int.TryParse(value.ValueSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intValue))
 | 
			
		||||
                            {
 | 
			
		||||
                                result.Year = intValue;
 | 
			
		||||
                            }
 | 
			
		||||
 | 
			
		||||
@ -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.
 | 
			
		||||
                // "Foo Bar 889"
 | 
			
		||||
                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[]
 | 
			
		||||
            {
 | 
			
		||||
                new ExtraRule(
 | 
			
		||||
@ -797,16 +795,6 @@ namespace Emby.Naming.Common
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        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>
 | 
			
		||||
        /// Gets or sets list of video file extensions.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
@ -877,16 +865,6 @@ namespace Emby.Naming.Common
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        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>
 | 
			
		||||
        /// Compiles raw regex strings into regexes.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
@ -894,8 +872,6 @@ namespace Emby.Naming.Common
 | 
			
		||||
        {
 | 
			
		||||
            CleanDateTimeRegexes = CleanDateTimes.Select(Compile).ToArray();
 | 
			
		||||
            CleanStringRegexes = CleanStrings.Select(Compile).ToArray();
 | 
			
		||||
            EpisodeWithoutSeasonRegexes = EpisodeWithoutSeasonExpressions.Select(Compile).ToArray();
 | 
			
		||||
            EpisodeMultiPartRegexes = EpisodeMultiPartExpressions.Select(Compile).ToArray();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private Regex Compile(string exp)
 | 
			
		||||
 | 
			
		||||
@ -113,7 +113,7 @@ namespace Emby.Naming.TV
 | 
			
		||||
                    if (expression.DateTimeFormats.Length > 0)
 | 
			
		||||
                    {
 | 
			
		||||
                        if (DateTime.TryParseExact(
 | 
			
		||||
                            match.Groups[0].Value,
 | 
			
		||||
                            match.Groups[0].ValueSpan,
 | 
			
		||||
                            expression.DateTimeFormats,
 | 
			
		||||
                            CultureInfo.InvariantCulture,
 | 
			
		||||
                            DateTimeStyles.None,
 | 
			
		||||
@ -125,7 +125,7 @@ namespace Emby.Naming.TV
 | 
			
		||||
                            result.Success = true;
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                    else if (DateTime.TryParse(match.Groups[0].Value, out date))
 | 
			
		||||
                    else if (DateTime.TryParse(match.Groups[0].ValueSpan, out date))
 | 
			
		||||
                    {
 | 
			
		||||
                        result.Year = date.Year;
 | 
			
		||||
                        result.Month = date.Month;
 | 
			
		||||
@ -138,12 +138,12 @@ namespace Emby.Naming.TV
 | 
			
		||||
                }
 | 
			
		||||
                else if (expression.IsNamed)
 | 
			
		||||
                {
 | 
			
		||||
                    if (int.TryParse(match.Groups["seasonnumber"].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var num))
 | 
			
		||||
                    if (int.TryParse(match.Groups["seasonnumber"].ValueSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out var num))
 | 
			
		||||
                    {
 | 
			
		||||
                        result.SeasonNumber = num;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    if (int.TryParse(match.Groups["epnumber"].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out num))
 | 
			
		||||
                    if (int.TryParse(match.Groups["epnumber"].ValueSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out num))
 | 
			
		||||
                    {
 | 
			
		||||
                        result.EpisodeNumber = num;
 | 
			
		||||
                    }
 | 
			
		||||
@ -158,7 +158,7 @@ namespace Emby.Naming.TV
 | 
			
		||||
                        if (nextIndex >= name.Length
 | 
			
		||||
                            || !"0123456789iIpP".Contains(name[nextIndex], StringComparison.Ordinal))
 | 
			
		||||
                        {
 | 
			
		||||
                            if (int.TryParse(endingNumberGroup.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out num))
 | 
			
		||||
                            if (int.TryParse(endingNumberGroup.ValueSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out num))
 | 
			
		||||
                            {
 | 
			
		||||
                                result.EndingEpisodeNumber = num;
 | 
			
		||||
                            }
 | 
			
		||||
@ -170,12 +170,12 @@ namespace Emby.Naming.TV
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    if (int.TryParse(match.Groups[1].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var num))
 | 
			
		||||
                    if (int.TryParse(match.Groups[1].ValueSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out var num))
 | 
			
		||||
                    {
 | 
			
		||||
                        result.SeasonNumber = num;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    if (int.TryParse(match.Groups[2].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out num))
 | 
			
		||||
                    if (int.TryParse(match.Groups[2].ValueSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out num))
 | 
			
		||||
                    {
 | 
			
		||||
                        result.EpisodeNumber = num;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
@ -43,7 +43,7 @@ namespace Emby.Naming.Video
 | 
			
		||||
                && match.Groups.Count == 5
 | 
			
		||||
                && match.Groups[1].Success
 | 
			
		||||
                && match.Groups[2].Success
 | 
			
		||||
                && int.TryParse(match.Groups[2].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var year))
 | 
			
		||||
                && int.TryParse(match.Groups[2].ValueSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out var year))
 | 
			
		||||
            {
 | 
			
		||||
                result = new CleanDateTimeResult(match.Groups[1].Value.TrimEnd(), year);
 | 
			
		||||
                return true;
 | 
			
		||||
 | 
			
		||||
@ -56,7 +56,7 @@ namespace Emby.Naming.Video
 | 
			
		||||
                }
 | 
			
		||||
                else if (rule.RuleType == ExtraRuleType.Regex)
 | 
			
		||||
                {
 | 
			
		||||
                    var filename = Path.GetFileName(path);
 | 
			
		||||
                    var filename = Path.GetFileName(path.AsSpan());
 | 
			
		||||
 | 
			
		||||
                    var isMatch = Regex.IsMatch(filename, rule.Token, RegexOptions.IgnoreCase | RegexOptions.Compiled);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -106,6 +106,7 @@ namespace Emby.Naming.Video
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Cannot use Span inside local functions and delegates thus we cannot use LINQ here nor merge with the above [if]
 | 
			
		||||
            VideoInfo? primary = null;
 | 
			
		||||
            for (var i = 0; i < videos.Count; i++)
 | 
			
		||||
            {
 | 
			
		||||
                var video = videos[i];
 | 
			
		||||
@ -118,25 +119,24 @@ namespace Emby.Naming.Video
 | 
			
		||||
                {
 | 
			
		||||
                    return videos;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (folderName.Equals(Path.GetFileNameWithoutExtension(video.Files[0].Path.AsSpan()), StringComparison.Ordinal))
 | 
			
		||||
                {
 | 
			
		||||
                    primary = video;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // The list is created and overwritten in the caller, so we are allowed to do in-place sorting
 | 
			
		||||
            videos.Sort((x, y) => string.Compare(x.Name, y.Name, StringComparison.Ordinal));
 | 
			
		||||
            primary ??= videos[0];
 | 
			
		||||
            videos.Remove(primary);
 | 
			
		||||
 | 
			
		||||
            var list = new List<VideoInfo>
 | 
			
		||||
            {
 | 
			
		||||
                videos[0]
 | 
			
		||||
                primary
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            var alternateVersionsLen = videos.Count - 1;
 | 
			
		||||
            var alternateVersions = new VideoFileInfo[alternateVersionsLen];
 | 
			
		||||
            for (int i = 0; i < alternateVersionsLen; i++)
 | 
			
		||||
            {
 | 
			
		||||
                var video = videos[i + 1];
 | 
			
		||||
                alternateVersions[i] = video.Files[0];
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            list[0].AlternateVersions = alternateVersions;
 | 
			
		||||
            list[0].AlternateVersions = videos.Select(x => x.Files[0]).ToArray();
 | 
			
		||||
            list[0].Name = folderName.ToString();
 | 
			
		||||
 | 
			
		||||
            return list;
 | 
			
		||||
@ -176,16 +176,15 @@ namespace Emby.Naming.Video
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // There are no span overloads for regex unfortunately
 | 
			
		||||
            var tmpTestFilename = testFilename.ToString();
 | 
			
		||||
            if (CleanStringParser.TryClean(tmpTestFilename, namingOptions.CleanStringRegexes, out var cleanName))
 | 
			
		||||
            if (CleanStringParser.TryClean(testFilename.ToString(), namingOptions.CleanStringRegexes, out var cleanName))
 | 
			
		||||
            {
 | 
			
		||||
                tmpTestFilename = cleanName.Trim();
 | 
			
		||||
                testFilename = cleanName.AsSpan().Trim();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // The CleanStringParser should have removed common keywords etc.
 | 
			
		||||
            return string.IsNullOrEmpty(tmpTestFilename)
 | 
			
		||||
            return testFilename.IsEmpty
 | 
			
		||||
                   || testFilename[0] == '-'
 | 
			
		||||
                   || Regex.IsMatch(tmpTestFilename, @"^\[([^]]*)\]", RegexOptions.Compiled);
 | 
			
		||||
                   || Regex.IsMatch(testFilename, @"^\[([^]]*)\]", RegexOptions.Compiled);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1195,7 +1195,7 @@ namespace Emby.Server.Implementations.Data
 | 
			
		||||
                Path = RestorePath(path.ToString())
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            if (long.TryParse(dateModified, NumberStyles.Any, CultureInfo.InvariantCulture, out var ticks)
 | 
			
		||||
            if (long.TryParse(dateModified, CultureInfo.InvariantCulture, out var ticks)
 | 
			
		||||
                && ticks >= DateTime.MinValue.Ticks
 | 
			
		||||
                && ticks <= DateTime.MaxValue.Ticks)
 | 
			
		||||
            {
 | 
			
		||||
 | 
			
		||||
@ -313,13 +313,8 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
 | 
			
		||||
            return result;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private static bool IsIgnored(string filename)
 | 
			
		||||
        {
 | 
			
		||||
            // Ignore samples
 | 
			
		||||
            Match m = Regex.Match(filename, @"\bsample\b", RegexOptions.IgnoreCase | RegexOptions.Compiled);
 | 
			
		||||
 | 
			
		||||
            return m.Success;
 | 
			
		||||
        }
 | 
			
		||||
        private static bool IsIgnored(ReadOnlySpan<char> filename)
 | 
			
		||||
            => Regex.IsMatch(filename, @"\bsample\b", RegexOptions.IgnoreCase | RegexOptions.Compiled);
 | 
			
		||||
 | 
			
		||||
        private static bool ContainsFile(IReadOnlyList<VideoInfo> result, FileSystemMetadata file)
 | 
			
		||||
        {
 | 
			
		||||
 | 
			
		||||
@ -570,9 +570,8 @@ namespace Emby.Server.Implementations.LiveTv.Listings
 | 
			
		||||
                _tokens.TryAdd(username, savedToken);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (!string.IsNullOrEmpty(savedToken.Name) && !string.IsNullOrEmpty(savedToken.Value))
 | 
			
		||||
            {
 | 
			
		||||
                if (long.TryParse(savedToken.Value, NumberStyles.Any, CultureInfo.InvariantCulture, out long ticks))
 | 
			
		||||
            if (!string.IsNullOrEmpty(savedToken.Name)
 | 
			
		||||
                && long.TryParse(savedToken.Value, 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)
 | 
			
		||||
@ -580,7 +579,6 @@ namespace Emby.Server.Implementations.LiveTv.Listings
 | 
			
		||||
                    return savedToken.Name;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            await _tokenSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
 | 
			
		||||
            try
 | 
			
		||||
 | 
			
		||||
@ -168,30 +168,26 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
 | 
			
		||||
            string numberString = null;
 | 
			
		||||
            string attributeValue;
 | 
			
		||||
 | 
			
		||||
            if (attributes.TryGetValue("tvg-chno", out attributeValue))
 | 
			
		||||
            {
 | 
			
		||||
                if (double.TryParse(attributeValue, NumberStyles.Any, CultureInfo.InvariantCulture, out _))
 | 
			
		||||
            if (attributes.TryGetValue("tvg-chno", out attributeValue)
 | 
			
		||||
                && double.TryParse(attributeValue, CultureInfo.InvariantCulture, out _))
 | 
			
		||||
            {
 | 
			
		||||
                numberString = attributeValue;
 | 
			
		||||
            }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (!IsValidChannelNumber(numberString))
 | 
			
		||||
            {
 | 
			
		||||
                if (attributes.TryGetValue("tvg-id", out attributeValue))
 | 
			
		||||
                {
 | 
			
		||||
                    if (double.TryParse(attributeValue, NumberStyles.Any, CultureInfo.InvariantCulture, out _))
 | 
			
		||||
                    if (double.TryParse(attributeValue, CultureInfo.InvariantCulture, out _))
 | 
			
		||||
                    {
 | 
			
		||||
                        numberString = attributeValue;
 | 
			
		||||
                    }
 | 
			
		||||
                    else if (attributes.TryGetValue("channel-id", out attributeValue))
 | 
			
		||||
                    {
 | 
			
		||||
                        if (double.TryParse(attributeValue, NumberStyles.Any, CultureInfo.InvariantCulture, out _))
 | 
			
		||||
                    else if (attributes.TryGetValue("channel-id", out attributeValue)
 | 
			
		||||
                        && double.TryParse(attributeValue, CultureInfo.InvariantCulture, out _))
 | 
			
		||||
                    {
 | 
			
		||||
                        numberString = attributeValue;
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (string.IsNullOrWhiteSpace(numberString))
 | 
			
		||||
                {
 | 
			
		||||
@ -207,7 +203,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
 | 
			
		||||
                        {
 | 
			
		||||
                            var numberPart = nameInExtInf.Slice(0, numberIndex).Trim(new[] { ' ', '.' });
 | 
			
		||||
 | 
			
		||||
                            if (double.TryParse(numberPart, NumberStyles.Any, CultureInfo.InvariantCulture, out _))
 | 
			
		||||
                            if (double.TryParse(numberPart, CultureInfo.InvariantCulture, out _))
 | 
			
		||||
                            {
 | 
			
		||||
                                numberString = numberPart.ToString();
 | 
			
		||||
                            }
 | 
			
		||||
@ -255,19 +251,14 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
 | 
			
		||||
 | 
			
		||||
        private static bool IsValidChannelNumber(string numberString)
 | 
			
		||||
        {
 | 
			
		||||
            if (string.IsNullOrWhiteSpace(numberString) ||
 | 
			
		||||
                string.Equals(numberString, "-1", StringComparison.OrdinalIgnoreCase) ||
 | 
			
		||||
                string.Equals(numberString, "0", StringComparison.OrdinalIgnoreCase))
 | 
			
		||||
            if (string.IsNullOrWhiteSpace(numberString)
 | 
			
		||||
                || string.Equals(numberString, "-1", StringComparison.Ordinal)
 | 
			
		||||
                || string.Equals(numberString, "0", StringComparison.Ordinal))
 | 
			
		||||
            {
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (!double.TryParse(numberString, NumberStyles.Any, CultureInfo.InvariantCulture, out _))
 | 
			
		||||
            {
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return true;
 | 
			
		||||
            return double.TryParse(numberString, CultureInfo.InvariantCulture, out _);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private static string GetChannelName(string extInf, Dictionary<string, string> attributes)
 | 
			
		||||
@ -285,7 +276,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
 | 
			
		||||
                {
 | 
			
		||||
                    var numberPart = nameInExtInf.Substring(0, numberIndex).Trim(new[] { ' ', '.' });
 | 
			
		||||
 | 
			
		||||
                    if (double.TryParse(numberPart, NumberStyles.Any, CultureInfo.InvariantCulture, out _))
 | 
			
		||||
                    if (double.TryParse(numberPart, CultureInfo.InvariantCulture, out _))
 | 
			
		||||
                    {
 | 
			
		||||
                        // channel.Number = number.ToString();
 | 
			
		||||
                        nameInExtInf = nameInExtInf.Substring(numberIndex + 1).Trim(new[] { ' ', '-' });
 | 
			
		||||
 | 
			
		||||
@ -118,11 +118,11 @@
 | 
			
		||||
    "TaskCleanActivityLog": "Borrar log de actividades",
 | 
			
		||||
    "Undefined": "Indefinido",
 | 
			
		||||
    "Forced": "Forzado",
 | 
			
		||||
    "Default": "Por Defecto",
 | 
			
		||||
    "Default": "Predeterminado",
 | 
			
		||||
    "TaskOptimizeDatabaseDescription": "Compacta la base de datos y restaura el espacio libre. Ejecutar esta tarea después de actualizar las librerías o realizar otros cambios que impliquen modificar las bases de datos puede mejorar la performance.",
 | 
			
		||||
    "TaskOptimizeDatabase": "Optimización de base de datos",
 | 
			
		||||
    "External": "Externo",
 | 
			
		||||
    "TaskKeyframeExtractorDescription": "Extrae Fotogramas Clave de los archivos de vídeo para crear Listas de Reprodución HLS más precisas. Esta tarea puede durar mucho tiempo.",
 | 
			
		||||
    "TaskKeyframeExtractor": "Extractor de Fotogramas Clave",
 | 
			
		||||
    "HearingImpaired": "Personas con discapacidad auditiva"
 | 
			
		||||
    "HearingImpaired": "Discapacidad Auditiva"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -82,7 +82,7 @@
 | 
			
		||||
    "MessageServerConfigurationUpdated": "Konfigurasi server telah diperbarui",
 | 
			
		||||
    "MessageNamedServerConfigurationUpdatedWithValue": "Bagian konfigurasi server {0} telah diperbarui",
 | 
			
		||||
    "FailedLoginAttemptWithUserName": "Gagal melakukan login dari {0}",
 | 
			
		||||
    "CameraImageUploadedFrom": "Gambar kamera baru telah diunggah dari {0}",
 | 
			
		||||
    "CameraImageUploadedFrom": "Sebuah gambar kamera baru telah diunggah dari {0}",
 | 
			
		||||
    "DeviceOfflineWithName": "{0} telah terputus",
 | 
			
		||||
    "DeviceOnlineWithName": "{0} telah terhubung",
 | 
			
		||||
    "NotificationOptionVideoPlaybackStopped": "Pemutaran video berhenti",
 | 
			
		||||
 | 
			
		||||
@ -58,8 +58,8 @@
 | 
			
		||||
    "NotificationOptionServerRestartRequired": "Server herstart nodig",
 | 
			
		||||
    "NotificationOptionTaskFailed": "Geplande taak mislukt",
 | 
			
		||||
    "NotificationOptionUserLockedOut": "Gebruiker is vergrendeld",
 | 
			
		||||
    "NotificationOptionVideoPlayback": "Video gestart",
 | 
			
		||||
    "NotificationOptionVideoPlaybackStopped": "Video gestopt",
 | 
			
		||||
    "NotificationOptionVideoPlayback": "Afspelen van video gestart",
 | 
			
		||||
    "NotificationOptionVideoPlaybackStopped": "Afspelen van video gestopt",
 | 
			
		||||
    "Photos": "Foto's",
 | 
			
		||||
    "Playlists": "Afspeellijsten",
 | 
			
		||||
    "Plugin": "Plug-in",
 | 
			
		||||
@ -95,26 +95,26 @@
 | 
			
		||||
    "TaskDownloadMissingSubtitlesDescription": "Zoekt op het internet naar ontbrekende ondertiteling gebaseerd op metadataconfiguratie.",
 | 
			
		||||
    "TaskDownloadMissingSubtitles": "Ontbrekende ondertiteling downloaden",
 | 
			
		||||
    "TaskRefreshChannelsDescription": "Vernieuwt informatie van internet kanalen.",
 | 
			
		||||
    "TaskRefreshChannels": "Vernieuw Kanalen",
 | 
			
		||||
    "TaskRefreshChannels": "Vernieuw kanalen",
 | 
			
		||||
    "TaskCleanTranscodeDescription": "Verwijdert transcode bestanden ouder dan 1 dag.",
 | 
			
		||||
    "TaskCleanLogs": "Logboekmap opschonen",
 | 
			
		||||
    "TaskCleanTranscode": "Transcoderingsmap opschonen",
 | 
			
		||||
    "TaskUpdatePluginsDescription": "Downloadt en installeert updates van plug-ins waarvoor automatisch bijwerken is ingeschakeld.",
 | 
			
		||||
    "TaskUpdatePlugins": "Plug-ins bijwerken",
 | 
			
		||||
    "TaskRefreshPeopleDescription": "Update metadata for acteurs en regisseurs in de media bibliotheek.",
 | 
			
		||||
    "TaskRefreshPeopleDescription": "Update metadata voor acteurs en regisseurs in de media bibliotheek.",
 | 
			
		||||
    "TaskRefreshPeople": "Personen vernieuwen",
 | 
			
		||||
    "TaskCleanLogsDescription": "Verwijdert log bestanden ouder dan {0} dagen.",
 | 
			
		||||
    "TaskRefreshLibraryDescription": "Scant de mediabibliotheek op nieuwe bestanden en vernieuwt de metadata.",
 | 
			
		||||
    "TaskRefreshLibrary": "Mediabibliotheek scannen",
 | 
			
		||||
    "TaskRefreshChapterImagesDescription": "Maakt thumbnails aan voor videos met hoofdstukken.",
 | 
			
		||||
    "TaskRefreshChapterImages": "Hoofdstukafbeeldingen uitpakken",
 | 
			
		||||
    "TaskRefreshChapterImagesDescription": "Maakt voorbeeldafbeedingen aan voor video's met hoofdstukken.",
 | 
			
		||||
    "TaskRefreshChapterImages": "Hoofdstukafbeeldingen extraheren",
 | 
			
		||||
    "TaskCleanCacheDescription": "Verwijdert gecachte bestanden die het systeem niet langer nodig heeft.",
 | 
			
		||||
    "TaskCleanCache": "Cache-map opschonen",
 | 
			
		||||
    "TasksChannelsCategory": "Internet Kanalen",
 | 
			
		||||
    "TasksChannelsCategory": "Internetkanalen",
 | 
			
		||||
    "TasksApplicationCategory": "Toepassing",
 | 
			
		||||
    "TasksLibraryCategory": "Bibliotheek",
 | 
			
		||||
    "TasksMaintenanceCategory": "Onderhoud",
 | 
			
		||||
    "TaskCleanActivityLogDescription": "Verwijdert activiteiten logs ouder dan de ingestelde tijd.",
 | 
			
		||||
    "TaskCleanActivityLogDescription": "Verwijdert activiteiten logs ouder dan de ingestelde leeftijd.",
 | 
			
		||||
    "TaskCleanActivityLog": "Activiteitenlogboek legen",
 | 
			
		||||
    "Undefined": "Niet gedefinieerd",
 | 
			
		||||
    "Forced": "Geforceerd",
 | 
			
		||||
 | 
			
		||||
@ -16,14 +16,14 @@
 | 
			
		||||
    "Folders": "Папки",
 | 
			
		||||
    "Genres": "Жанры",
 | 
			
		||||
    "HeaderAlbumArtists": "Исполнители альбома",
 | 
			
		||||
    "HeaderContinueWatching": "Продолжение просмотра",
 | 
			
		||||
    "HeaderContinueWatching": "Продолжить просмотр",
 | 
			
		||||
    "HeaderFavoriteAlbums": "Избранные альбомы",
 | 
			
		||||
    "HeaderFavoriteArtists": "Избранные исполнители",
 | 
			
		||||
    "HeaderFavoriteEpisodes": "Избранные эпизоды",
 | 
			
		||||
    "HeaderFavoriteShows": "Избранные сериалы",
 | 
			
		||||
    "HeaderFavoriteSongs": "Избранные композиции",
 | 
			
		||||
    "HeaderLiveTV": "Эфир",
 | 
			
		||||
    "HeaderNextUp": "Очередное",
 | 
			
		||||
    "HeaderNextUp": "Следующий",
 | 
			
		||||
    "HeaderRecordingGroups": "Группы записей",
 | 
			
		||||
    "HomeVideos": "Домашние видео",
 | 
			
		||||
    "Inherit": "Наследуемое",
 | 
			
		||||
@ -70,7 +70,7 @@
 | 
			
		||||
    "ScheduledTaskFailedWithName": "{0} - неудачна",
 | 
			
		||||
    "ScheduledTaskStartedWithName": "{0} - запущена",
 | 
			
		||||
    "ServerNameNeedsToBeRestarted": "Необходим перезапуск {0}",
 | 
			
		||||
    "Shows": "Передачи",
 | 
			
		||||
    "Shows": "Телешоу",
 | 
			
		||||
    "Songs": "Композиции",
 | 
			
		||||
    "StartupEmbyServerIsLoading": "Jellyfin Server загружается. Повторите попытку в ближайшее время.",
 | 
			
		||||
    "SubtitleDownloadFailureForItem": "Субтитры к {0} не удалось загрузить",
 | 
			
		||||
 | 
			
		||||
@ -123,5 +123,6 @@
 | 
			
		||||
    "TaskOptimizeDatabase": "Optimiziraj bazo podatkov",
 | 
			
		||||
    "TaskKeyframeExtractor": "Ekstraktor ključnih sličic",
 | 
			
		||||
    "External": "Zunanji",
 | 
			
		||||
    "TaskKeyframeExtractorDescription": "Iz video datoteke Izvleče ključne sličice, da ustvari bolj natančne sezname predvajanja HLS. Proces lahko traja dolgo časa."
 | 
			
		||||
    "TaskKeyframeExtractorDescription": "Iz video datoteke Izvleče ključne sličice, da ustvari bolj natančne sezname predvajanja HLS. Proces lahko traja dolgo časa.",
 | 
			
		||||
    "HearingImpaired": "Oslabljen sluh"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,3 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
 | 
			
		||||
#pragma warning disable CS1591
 | 
			
		||||
 | 
			
		||||
using System;
 | 
			
		||||
@ -43,9 +41,9 @@ namespace Emby.Server.Implementations.ScheduledTasks
 | 
			
		||||
            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>
 | 
			
		||||
        /// Gets the list of Scheduled Tasks.
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,3 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
@ -58,7 +56,7 @@ namespace Emby.Server.Implementations.Session
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// The KeepAlive cancellation token.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        private CancellationTokenSource _keepAliveCancellationToken;
 | 
			
		||||
        private CancellationTokenSource? _keepAliveCancellationToken;
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 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)
 | 
			
		||||
            {
 | 
			
		||||
@ -138,8 +136,13 @@ namespace Emby.Server.Implementations.Session
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="sender">The WebSocket.</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;
 | 
			
		||||
            _logger.LogDebug("WebSocket {0} is closed.", webSocket);
 | 
			
		||||
            RemoveWebSocket(webSocket);
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,3 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
 | 
			
		||||
using System;
 | 
			
		||||
using MediaBrowser.Controller.Entities;
 | 
			
		||||
using MediaBrowser.Controller.Sorting;
 | 
			
		||||
@ -24,10 +22,9 @@ namespace Emby.Server.Implementations.Sorting
 | 
			
		||||
        /// <param name="x">The x.</param>
 | 
			
		||||
        /// <param name="y">The y.</param>
 | 
			
		||||
        /// <returns>System.Int32.</returns>
 | 
			
		||||
        public int Compare(BaseItem x, BaseItem y)
 | 
			
		||||
        public int Compare(BaseItem? x, BaseItem? y)
 | 
			
		||||
        {
 | 
			
		||||
            ArgumentNullException.ThrowIfNull(x);
 | 
			
		||||
 | 
			
		||||
            ArgumentNullException.ThrowIfNull(y);
 | 
			
		||||
 | 
			
		||||
            return (x.RunTimeTicks ?? 0).CompareTo(y.RunTimeTicks ?? 0);
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,3 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
 | 
			
		||||
#pragma warning disable CS1591
 | 
			
		||||
 | 
			
		||||
using System;
 | 
			
		||||
@ -23,15 +21,14 @@ namespace Emby.Server.Implementations.Sorting
 | 
			
		||||
        /// <param name="x">The x.</param>
 | 
			
		||||
        /// <param name="y">The y.</param>
 | 
			
		||||
        /// <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);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private static string GetValue(BaseItem item)
 | 
			
		||||
        private static string? GetValue(BaseItem? item)
 | 
			
		||||
        {
 | 
			
		||||
            var hasSeries = item as IHasSeries;
 | 
			
		||||
 | 
			
		||||
            return hasSeries?.FindSeriesSortName();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,3 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
 | 
			
		||||
using System;
 | 
			
		||||
using MediaBrowser.Controller.Entities;
 | 
			
		||||
using MediaBrowser.Controller.Sorting;
 | 
			
		||||
@ -24,10 +22,9 @@ namespace Emby.Server.Implementations.Sorting
 | 
			
		||||
        /// <param name="x">The x.</param>
 | 
			
		||||
        /// <param name="y">The y.</param>
 | 
			
		||||
        /// <returns>System.Int32.</returns>
 | 
			
		||||
        public int Compare(BaseItem x, BaseItem y)
 | 
			
		||||
        public int Compare(BaseItem? x, BaseItem? y)
 | 
			
		||||
        {
 | 
			
		||||
            ArgumentNullException.ThrowIfNull(x);
 | 
			
		||||
 | 
			
		||||
            ArgumentNullException.ThrowIfNull(y);
 | 
			
		||||
 | 
			
		||||
            return string.Compare(x.SortName, y.SortName, StringComparison.OrdinalIgnoreCase);
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,3 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
 | 
			
		||||
#pragma warning disable CS1591
 | 
			
		||||
 | 
			
		||||
using System;
 | 
			
		||||
@ -24,7 +22,7 @@ namespace Emby.Server.Implementations.Sorting
 | 
			
		||||
        /// <param name="x">The x.</param>
 | 
			
		||||
        /// <param name="y">The y.</param>
 | 
			
		||||
        /// <returns>System.Int32.</returns>
 | 
			
		||||
        public int Compare(BaseItem x, BaseItem y)
 | 
			
		||||
        public int Compare(BaseItem? x, BaseItem? y)
 | 
			
		||||
        {
 | 
			
		||||
            return GetDate(x).CompareTo(GetDate(y));
 | 
			
		||||
        }
 | 
			
		||||
@ -34,7 +32,7 @@ namespace Emby.Server.Implementations.Sorting
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="x">The x.</param>
 | 
			
		||||
        /// <returns>DateTime.</returns>
 | 
			
		||||
        private static DateTime GetDate(BaseItem x)
 | 
			
		||||
        private static DateTime GetDate(BaseItem? x)
 | 
			
		||||
        {
 | 
			
		||||
            if (x is LiveTvProgram hasStartDate)
 | 
			
		||||
            {
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,3 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
 | 
			
		||||
#pragma warning disable CS1591
 | 
			
		||||
 | 
			
		||||
using System;
 | 
			
		||||
@ -24,10 +22,9 @@ namespace Emby.Server.Implementations.Sorting
 | 
			
		||||
        /// <param name="x">The x.</param>
 | 
			
		||||
        /// <param name="y">The y.</param>
 | 
			
		||||
        /// <returns>System.Int32.</returns>
 | 
			
		||||
        public int Compare(BaseItem x, BaseItem y)
 | 
			
		||||
        public int Compare(BaseItem? x, BaseItem? y)
 | 
			
		||||
        {
 | 
			
		||||
            ArgumentNullException.ThrowIfNull(x);
 | 
			
		||||
 | 
			
		||||
            ArgumentNullException.ThrowIfNull(y);
 | 
			
		||||
 | 
			
		||||
            return AlphanumericComparator.CompareValues(x.Studios.FirstOrDefault(), y.Studios.FirstOrDefault());
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,3 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
 | 
			
		||||
#pragma warning disable CS1591
 | 
			
		||||
 | 
			
		||||
using System;
 | 
			
		||||
@ -42,7 +40,7 @@ namespace Emby.Server.Implementations.TV
 | 
			
		||||
                throw new ArgumentException("User not found");
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            string presentationUniqueKey = null;
 | 
			
		||||
            string? presentationUniqueKey = null;
 | 
			
		||||
            if (query.SeriesId.HasValue && !query.SeriesId.Value.Equals(default))
 | 
			
		||||
            {
 | 
			
		||||
                if (_libraryManager.GetItemById(query.SeriesId.Value) is Series series)
 | 
			
		||||
@ -91,7 +89,7 @@ namespace Emby.Server.Implementations.TV
 | 
			
		||||
                throw new ArgumentException("User not found");
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            string presentationUniqueKey = null;
 | 
			
		||||
            string? presentationUniqueKey = null;
 | 
			
		||||
            int? limit = null;
 | 
			
		||||
            if (request.SeriesId.HasValue && !request.SeriesId.Value.Equals(default))
 | 
			
		||||
            {
 | 
			
		||||
@ -168,7 +166,7 @@ namespace Emby.Server.Implementations.TV
 | 
			
		||||
                    return !anyFound && i.LastWatchedDate == DateTime.MinValue;
 | 
			
		||||
                })
 | 
			
		||||
                .Select(i => i.GetEpisodeFunction())
 | 
			
		||||
                .Where(i => i is not null);
 | 
			
		||||
                .Where(i => i is not null)!;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private static string GetUniqueSeriesKey(Episode episode)
 | 
			
		||||
@ -185,7 +183,7 @@ namespace Emby.Server.Implementations.TV
 | 
			
		||||
        /// Gets the next up.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <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)
 | 
			
		||||
            {
 | 
			
		||||
@ -209,7 +207,7 @@ namespace Emby.Server.Implementations.TV
 | 
			
		||||
 | 
			
		||||
            var lastWatchedEpisode = _libraryManager.GetItemList(lastQuery).Cast<Episode>().FirstOrDefault();
 | 
			
		||||
 | 
			
		||||
            Episode GetEpisode()
 | 
			
		||||
            Episode? GetEpisode()
 | 
			
		||||
            {
 | 
			
		||||
                var nextQuery = new InternalItemsQuery(user)
 | 
			
		||||
                {
 | 
			
		||||
 | 
			
		||||
@ -118,6 +118,7 @@ public class ArtistsController : BaseJellyfinApiController
 | 
			
		||||
        [FromQuery] bool? enableImages = true,
 | 
			
		||||
        [FromQuery] bool enableTotalRecordCount = true)
 | 
			
		||||
    {
 | 
			
		||||
        userId = RequestHelpers.GetUserId(User, userId);
 | 
			
		||||
        var dtoOptions = new DtoOptions { Fields = fields }
 | 
			
		||||
            .AddClientFields(User)
 | 
			
		||||
            .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
 | 
			
		||||
@ -125,7 +126,7 @@ public class ArtistsController : BaseJellyfinApiController
 | 
			
		||||
        User? user = null;
 | 
			
		||||
        BaseItem parentItem = _libraryManager.GetParentItem(parentId, userId);
 | 
			
		||||
 | 
			
		||||
        if (userId.HasValue && !userId.Equals(default))
 | 
			
		||||
        if (!userId.Value.Equals(default))
 | 
			
		||||
        {
 | 
			
		||||
            user = _userManager.GetUserById(userId.Value);
 | 
			
		||||
        }
 | 
			
		||||
@ -321,6 +322,7 @@ public class ArtistsController : BaseJellyfinApiController
 | 
			
		||||
        [FromQuery] bool? enableImages = true,
 | 
			
		||||
        [FromQuery] bool enableTotalRecordCount = true)
 | 
			
		||||
    {
 | 
			
		||||
        userId = RequestHelpers.GetUserId(User, userId);
 | 
			
		||||
        var dtoOptions = new DtoOptions { Fields = fields }
 | 
			
		||||
            .AddClientFields(User)
 | 
			
		||||
            .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
 | 
			
		||||
@ -328,7 +330,7 @@ public class ArtistsController : BaseJellyfinApiController
 | 
			
		||||
        User? user = null;
 | 
			
		||||
        BaseItem parentItem = _libraryManager.GetParentItem(parentId, userId);
 | 
			
		||||
 | 
			
		||||
        if (userId.HasValue && !userId.Equals(default))
 | 
			
		||||
        if (!userId.Value.Equals(default))
 | 
			
		||||
        {
 | 
			
		||||
            user = _userManager.GetUserById(userId.Value);
 | 
			
		||||
        }
 | 
			
		||||
@ -462,11 +464,12 @@ public class ArtistsController : BaseJellyfinApiController
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status200OK)]
 | 
			
		||||
    public ActionResult<BaseItemDto> GetArtistByName([FromRoute, Required] string name, [FromQuery] Guid? userId)
 | 
			
		||||
    {
 | 
			
		||||
        userId = RequestHelpers.GetUserId(User, userId);
 | 
			
		||||
        var dtoOptions = new DtoOptions().AddClientFields(User);
 | 
			
		||||
 | 
			
		||||
        var item = _libraryManager.GetArtist(name, dtoOptions);
 | 
			
		||||
 | 
			
		||||
        if (userId.HasValue && !userId.Value.Equals(default))
 | 
			
		||||
        if (!userId.Value.Equals(default))
 | 
			
		||||
        {
 | 
			
		||||
            var user = _userManager.GetUserById(userId.Value);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -60,11 +60,12 @@ public class ChannelsController : BaseJellyfinApiController
 | 
			
		||||
        [FromQuery] bool? supportsMediaDeletion,
 | 
			
		||||
        [FromQuery] bool? isFavorite)
 | 
			
		||||
    {
 | 
			
		||||
        userId = RequestHelpers.GetUserId(User, userId);
 | 
			
		||||
        return _channelManager.GetChannels(new ChannelQuery
 | 
			
		||||
        {
 | 
			
		||||
            Limit = limit,
 | 
			
		||||
            StartIndex = startIndex,
 | 
			
		||||
            UserId = userId ?? Guid.Empty,
 | 
			
		||||
            UserId = userId.Value,
 | 
			
		||||
            SupportsLatestItems = supportsLatestItems,
 | 
			
		||||
            SupportsMediaDeletion = supportsMediaDeletion,
 | 
			
		||||
            IsFavorite = isFavorite
 | 
			
		||||
@ -124,7 +125,8 @@ public class ChannelsController : BaseJellyfinApiController
 | 
			
		||||
        [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
 | 
			
		||||
        [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields)
 | 
			
		||||
    {
 | 
			
		||||
        var user = userId is null || userId.Value.Equals(default)
 | 
			
		||||
        userId = RequestHelpers.GetUserId(User, userId);
 | 
			
		||||
        var user = userId.Value.Equals(default)
 | 
			
		||||
            ? null
 | 
			
		||||
            : _userManager.GetUserById(userId.Value);
 | 
			
		||||
 | 
			
		||||
@ -198,7 +200,8 @@ public class ChannelsController : BaseJellyfinApiController
 | 
			
		||||
        [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
 | 
			
		||||
        [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] channelIds)
 | 
			
		||||
    {
 | 
			
		||||
        var user = userId is null || userId.Value.Equals(default)
 | 
			
		||||
        userId = RequestHelpers.GetUserId(User, userId);
 | 
			
		||||
        var user = userId.Value.Equals(default)
 | 
			
		||||
            ? null
 | 
			
		||||
            : _userManager.GetUserById(userId.Value);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -2,6 +2,7 @@ using System;
 | 
			
		||||
using System.ComponentModel.DataAnnotations;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using Jellyfin.Api.Constants;
 | 
			
		||||
using Jellyfin.Api.Helpers;
 | 
			
		||||
using Jellyfin.Data.Dtos;
 | 
			
		||||
using Jellyfin.Data.Entities.Security;
 | 
			
		||||
using Jellyfin.Data.Queries;
 | 
			
		||||
@ -48,6 +49,7 @@ public class DevicesController : BaseJellyfinApiController
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status200OK)]
 | 
			
		||||
    public async Task<ActionResult<QueryResult<DeviceInfo>>> GetDevices([FromQuery] bool? supportsSync, [FromQuery] Guid? userId)
 | 
			
		||||
    {
 | 
			
		||||
        userId = RequestHelpers.GetUserId(User, userId);
 | 
			
		||||
        return await _deviceManager.GetDevicesForUser(userId, supportsSync).ConfigureAwait(false);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,7 @@
 | 
			
		||||
using System;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using Jellyfin.Api.Constants;
 | 
			
		||||
using Jellyfin.Api.Helpers;
 | 
			
		||||
using Jellyfin.Api.ModelBinders;
 | 
			
		||||
using Jellyfin.Data.Enums;
 | 
			
		||||
using MediaBrowser.Controller.Dto;
 | 
			
		||||
@ -51,7 +53,8 @@ public class FilterController : BaseJellyfinApiController
 | 
			
		||||
        [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
 | 
			
		||||
        [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes)
 | 
			
		||||
    {
 | 
			
		||||
        var user = userId is null || userId.Value.Equals(default)
 | 
			
		||||
        userId = RequestHelpers.GetUserId(User, userId);
 | 
			
		||||
        var user = userId.Value.Equals(default)
 | 
			
		||||
            ? null
 | 
			
		||||
            : _userManager.GetUserById(userId.Value);
 | 
			
		||||
 | 
			
		||||
@ -143,7 +146,8 @@ public class FilterController : BaseJellyfinApiController
 | 
			
		||||
        [FromQuery] bool? isSeries,
 | 
			
		||||
        [FromQuery] bool? recursive)
 | 
			
		||||
    {
 | 
			
		||||
        var user = userId is null || userId.Value.Equals(default)
 | 
			
		||||
        userId = RequestHelpers.GetUserId(User, userId);
 | 
			
		||||
        var user = userId.Value.Equals(default)
 | 
			
		||||
            ? null
 | 
			
		||||
            : _userManager.GetUserById(userId.Value);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -90,11 +90,12 @@ public class GenresController : BaseJellyfinApiController
 | 
			
		||||
        [FromQuery] bool? enableImages = true,
 | 
			
		||||
        [FromQuery] bool enableTotalRecordCount = true)
 | 
			
		||||
    {
 | 
			
		||||
        userId = RequestHelpers.GetUserId(User, userId);
 | 
			
		||||
        var dtoOptions = new DtoOptions { Fields = fields }
 | 
			
		||||
            .AddClientFields(User)
 | 
			
		||||
            .AddAdditionalDtoOptions(enableImages, false, imageTypeLimit, enableImageTypes);
 | 
			
		||||
 | 
			
		||||
        User? user = userId is null || userId.Value.Equals(default)
 | 
			
		||||
        User? user = userId.Value.Equals(default)
 | 
			
		||||
            ? null
 | 
			
		||||
            : _userManager.GetUserById(userId.Value);
 | 
			
		||||
 | 
			
		||||
@ -155,6 +156,7 @@ public class GenresController : BaseJellyfinApiController
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status200OK)]
 | 
			
		||||
    public ActionResult<BaseItemDto> GetGenre([FromRoute, Required] string genreName, [FromQuery] Guid? userId)
 | 
			
		||||
    {
 | 
			
		||||
        userId = RequestHelpers.GetUserId(User, userId);
 | 
			
		||||
        var dtoOptions = new DtoOptions()
 | 
			
		||||
            .AddClientFields(User);
 | 
			
		||||
 | 
			
		||||
@ -170,7 +172,7 @@ public class GenresController : BaseJellyfinApiController
 | 
			
		||||
 | 
			
		||||
        item ??= new Genre();
 | 
			
		||||
 | 
			
		||||
        if (userId is null || userId.Value.Equals(default))
 | 
			
		||||
        if (userId.Value.Equals(default))
 | 
			
		||||
        {
 | 
			
		||||
            return _dtoService.GetBaseItemDto(item, dtoOptions);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -91,6 +91,7 @@ public class ImageController : BaseJellyfinApiController
 | 
			
		||||
    [Authorize]
 | 
			
		||||
    [AcceptsImageFile]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status204NoContent)]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status400BadRequest)]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status403Forbidden)]
 | 
			
		||||
    [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")]
 | 
			
		||||
    [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]
 | 
			
		||||
@ -110,6 +111,11 @@ public class ImageController : BaseJellyfinApiController
 | 
			
		||||
            return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to update the image.");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!TryGetImageExtensionFromContentType(Request.ContentType, out string? extension))
 | 
			
		||||
        {
 | 
			
		||||
            return BadRequest("Incorrect ContentType.");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
 | 
			
		||||
        await using (memoryStream.ConfigureAwait(false))
 | 
			
		||||
        {
 | 
			
		||||
@ -121,7 +127,7 @@ public class ImageController : BaseJellyfinApiController
 | 
			
		||||
                await _userManager.ClearProfileImageAsync(user).ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + MimeTypes.ToExtension(mimeType ?? string.Empty)));
 | 
			
		||||
            user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + extension));
 | 
			
		||||
 | 
			
		||||
            await _providerManager
 | 
			
		||||
                .SaveImage(memoryStream, mimeType, user.ProfileImage.Path)
 | 
			
		||||
@ -145,6 +151,7 @@ public class ImageController : BaseJellyfinApiController
 | 
			
		||||
    [Authorize]
 | 
			
		||||
    [AcceptsImageFile]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status204NoContent)]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status400BadRequest)]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status403Forbidden)]
 | 
			
		||||
    [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")]
 | 
			
		||||
    [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]
 | 
			
		||||
@ -164,6 +171,11 @@ public class ImageController : BaseJellyfinApiController
 | 
			
		||||
            return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to update the image.");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!TryGetImageExtensionFromContentType(Request.ContentType, out string? extension))
 | 
			
		||||
        {
 | 
			
		||||
            return BadRequest("Incorrect ContentType.");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
 | 
			
		||||
        await using (memoryStream.ConfigureAwait(false))
 | 
			
		||||
        {
 | 
			
		||||
@ -175,7 +187,7 @@ public class ImageController : BaseJellyfinApiController
 | 
			
		||||
                await _userManager.ClearProfileImageAsync(user).ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + MimeTypes.ToExtension(mimeType ?? string.Empty)));
 | 
			
		||||
            user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + extension));
 | 
			
		||||
 | 
			
		||||
            await _providerManager
 | 
			
		||||
                .SaveImage(memoryStream, mimeType, user.ProfileImage.Path)
 | 
			
		||||
@ -342,6 +354,7 @@ public class ImageController : BaseJellyfinApiController
 | 
			
		||||
    [Authorize(Policy = Policies.RequiresElevation)]
 | 
			
		||||
    [AcceptsImageFile]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status204NoContent)]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status400BadRequest)]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status404NotFound)]
 | 
			
		||||
    [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]
 | 
			
		||||
    public async Task<ActionResult> SetItemImage(
 | 
			
		||||
@ -354,6 +367,11 @@ public class ImageController : BaseJellyfinApiController
 | 
			
		||||
            return NotFound();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!TryGetImageExtensionFromContentType(Request.ContentType, out _))
 | 
			
		||||
        {
 | 
			
		||||
            return BadRequest("Incorrect ContentType.");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
 | 
			
		||||
        await using (memoryStream.ConfigureAwait(false))
 | 
			
		||||
        {
 | 
			
		||||
@ -379,6 +397,7 @@ public class ImageController : BaseJellyfinApiController
 | 
			
		||||
    [Authorize(Policy = Policies.RequiresElevation)]
 | 
			
		||||
    [AcceptsImageFile]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status204NoContent)]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status400BadRequest)]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status404NotFound)]
 | 
			
		||||
    [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]
 | 
			
		||||
    public async Task<ActionResult> SetItemImageByIndex(
 | 
			
		||||
@ -392,6 +411,11 @@ public class ImageController : BaseJellyfinApiController
 | 
			
		||||
            return NotFound();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!TryGetImageExtensionFromContentType(Request.ContentType, out _))
 | 
			
		||||
        {
 | 
			
		||||
            return BadRequest("Incorrect ContentType.");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
 | 
			
		||||
        await using (memoryStream.ConfigureAwait(false))
 | 
			
		||||
        {
 | 
			
		||||
@ -1763,22 +1787,14 @@ public class ImageController : BaseJellyfinApiController
 | 
			
		||||
    [AcceptsImageFile]
 | 
			
		||||
    public async Task<ActionResult> UploadCustomSplashscreen()
 | 
			
		||||
    {
 | 
			
		||||
        if (!TryGetImageExtensionFromContentType(Request.ContentType, out var extension))
 | 
			
		||||
        {
 | 
			
		||||
            return BadRequest("Incorrect ContentType.");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
 | 
			
		||||
        await using (memoryStream.ConfigureAwait(false))
 | 
			
		||||
        {
 | 
			
		||||
            var mimeType = MediaTypeHeaderValue.Parse(Request.ContentType).MediaType;
 | 
			
		||||
 | 
			
		||||
            if (!mimeType.HasValue)
 | 
			
		||||
            {
 | 
			
		||||
                return BadRequest("Error reading mimetype from uploaded image");
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var extension = MimeTypes.ToExtension(mimeType.Value);
 | 
			
		||||
            if (string.IsNullOrEmpty(extension))
 | 
			
		||||
            {
 | 
			
		||||
                return BadRequest("Error converting mimetype to an image extension");
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var filePath = Path.Combine(_appPaths.DataPath, "splashscreen-upload" + extension);
 | 
			
		||||
            var brandingOptions = _serverConfigurationManager.GetConfiguration<BrandingOptions>("branding");
 | 
			
		||||
            brandingOptions.SplashscreenLocation = filePath;
 | 
			
		||||
@ -2106,4 +2122,23 @@ public class ImageController : BaseJellyfinApiController
 | 
			
		||||
 | 
			
		||||
        return PhysicalFile(imagePath, imageContentType ?? MediaTypeNames.Text.Plain);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    internal static bool TryGetImageExtensionFromContentType(string? contentType, [NotNullWhen(true)] out string? extension)
 | 
			
		||||
    {
 | 
			
		||||
        extension = null;
 | 
			
		||||
        if (string.IsNullOrEmpty(contentType))
 | 
			
		||||
        {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (MediaTypeHeaderValue.TryParse(contentType, out var parsedValue)
 | 
			
		||||
            && parsedValue.MediaType.HasValue
 | 
			
		||||
            && MimeTypes.IsImage(parsedValue.MediaType.Value))
 | 
			
		||||
        {
 | 
			
		||||
            extension = MimeTypes.ToExtension(parsedValue.MediaType.Value);
 | 
			
		||||
            return extension is not null;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -2,6 +2,7 @@ using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.ComponentModel.DataAnnotations;
 | 
			
		||||
using Jellyfin.Api.Extensions;
 | 
			
		||||
using Jellyfin.Api.Helpers;
 | 
			
		||||
using Jellyfin.Api.ModelBinders;
 | 
			
		||||
using Jellyfin.Data.Entities;
 | 
			
		||||
using MediaBrowser.Controller.Dto;
 | 
			
		||||
@ -74,7 +75,8 @@ public class InstantMixController : BaseJellyfinApiController
 | 
			
		||||
        [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
 | 
			
		||||
    {
 | 
			
		||||
        var item = _libraryManager.GetItemById(id);
 | 
			
		||||
        var user = userId is null || userId.Value.Equals(default)
 | 
			
		||||
        userId = RequestHelpers.GetUserId(User, userId);
 | 
			
		||||
        var user = userId.Value.Equals(default)
 | 
			
		||||
            ? null
 | 
			
		||||
            : _userManager.GetUserById(userId.Value);
 | 
			
		||||
        var dtoOptions = new DtoOptions { Fields = fields }
 | 
			
		||||
@ -110,7 +112,8 @@ public class InstantMixController : BaseJellyfinApiController
 | 
			
		||||
        [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
 | 
			
		||||
    {
 | 
			
		||||
        var album = _libraryManager.GetItemById(id);
 | 
			
		||||
        var user = userId is null || userId.Value.Equals(default)
 | 
			
		||||
        userId = RequestHelpers.GetUserId(User, userId);
 | 
			
		||||
        var user = userId.Value.Equals(default)
 | 
			
		||||
            ? null
 | 
			
		||||
            : _userManager.GetUserById(userId.Value);
 | 
			
		||||
        var dtoOptions = new DtoOptions { Fields = fields }
 | 
			
		||||
@ -146,7 +149,8 @@ public class InstantMixController : BaseJellyfinApiController
 | 
			
		||||
        [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
 | 
			
		||||
    {
 | 
			
		||||
        var playlist = (Playlist)_libraryManager.GetItemById(id);
 | 
			
		||||
        var user = userId is null || userId.Value.Equals(default)
 | 
			
		||||
        userId = RequestHelpers.GetUserId(User, userId);
 | 
			
		||||
        var user = userId.Value.Equals(default)
 | 
			
		||||
            ? null
 | 
			
		||||
            : _userManager.GetUserById(userId.Value);
 | 
			
		||||
        var dtoOptions = new DtoOptions { Fields = fields }
 | 
			
		||||
@ -181,7 +185,8 @@ public class InstantMixController : BaseJellyfinApiController
 | 
			
		||||
        [FromQuery] int? imageTypeLimit,
 | 
			
		||||
        [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
 | 
			
		||||
    {
 | 
			
		||||
        var user = userId is null || userId.Value.Equals(default)
 | 
			
		||||
        userId = RequestHelpers.GetUserId(User, userId);
 | 
			
		||||
        var user = userId.Value.Equals(default)
 | 
			
		||||
            ? null
 | 
			
		||||
            : _userManager.GetUserById(userId.Value);
 | 
			
		||||
        var dtoOptions = new DtoOptions { Fields = fields }
 | 
			
		||||
@ -217,7 +222,8 @@ public class InstantMixController : BaseJellyfinApiController
 | 
			
		||||
        [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
 | 
			
		||||
    {
 | 
			
		||||
        var item = _libraryManager.GetItemById(id);
 | 
			
		||||
        var user = userId is null || userId.Value.Equals(default)
 | 
			
		||||
        userId = RequestHelpers.GetUserId(User, userId);
 | 
			
		||||
        var user = userId.Value.Equals(default)
 | 
			
		||||
            ? null
 | 
			
		||||
            : _userManager.GetUserById(userId.Value);
 | 
			
		||||
        var dtoOptions = new DtoOptions { Fields = fields }
 | 
			
		||||
@ -253,7 +259,8 @@ public class InstantMixController : BaseJellyfinApiController
 | 
			
		||||
        [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
 | 
			
		||||
    {
 | 
			
		||||
        var item = _libraryManager.GetItemById(id);
 | 
			
		||||
        var user = userId is null || userId.Value.Equals(default)
 | 
			
		||||
        userId = RequestHelpers.GetUserId(User, userId);
 | 
			
		||||
        var user = userId.Value.Equals(default)
 | 
			
		||||
            ? null
 | 
			
		||||
            : _userManager.GetUserById(userId.Value);
 | 
			
		||||
        var dtoOptions = new DtoOptions { Fields = fields }
 | 
			
		||||
@ -326,7 +333,8 @@ public class InstantMixController : BaseJellyfinApiController
 | 
			
		||||
        [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
 | 
			
		||||
    {
 | 
			
		||||
        var item = _libraryManager.GetItemById(id);
 | 
			
		||||
        var user = userId is null || userId.Value.Equals(default)
 | 
			
		||||
        userId = RequestHelpers.GetUserId(User, userId);
 | 
			
		||||
        var user = userId.Value.Equals(default)
 | 
			
		||||
            ? null
 | 
			
		||||
            : _userManager.GetUserById(userId.Value);
 | 
			
		||||
        var dtoOptions = new DtoOptions { Fields = fields }
 | 
			
		||||
 | 
			
		||||
@ -240,7 +240,8 @@ public class ItemsController : BaseJellyfinApiController
 | 
			
		||||
    {
 | 
			
		||||
        var isApiKey = User.GetIsApiKey();
 | 
			
		||||
        // if api key is used (auth.IsApiKey == true), then `user` will be null throughout this method
 | 
			
		||||
        var user = !isApiKey && userId.HasValue && !userId.Value.Equals(default)
 | 
			
		||||
        userId = RequestHelpers.GetUserId(User, userId);
 | 
			
		||||
        var user = !isApiKey && !userId.Value.Equals(default)
 | 
			
		||||
            ? _userManager.GetUserById(userId.Value) ?? throw new ResourceNotFoundException()
 | 
			
		||||
            : null;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -9,6 +9,7 @@ using System.Threading.Tasks;
 | 
			
		||||
using Jellyfin.Api.Attributes;
 | 
			
		||||
using Jellyfin.Api.Constants;
 | 
			
		||||
using Jellyfin.Api.Extensions;
 | 
			
		||||
using Jellyfin.Api.Helpers;
 | 
			
		||||
using Jellyfin.Api.ModelBinders;
 | 
			
		||||
using Jellyfin.Api.Models.LibraryDtos;
 | 
			
		||||
using Jellyfin.Data.Entities;
 | 
			
		||||
@ -142,12 +143,13 @@ public class LibraryController : BaseJellyfinApiController
 | 
			
		||||
        [FromQuery] Guid? userId,
 | 
			
		||||
        [FromQuery] bool inheritFromParent = false)
 | 
			
		||||
    {
 | 
			
		||||
        var user = userId is null || userId.Value.Equals(default)
 | 
			
		||||
        userId = RequestHelpers.GetUserId(User, userId);
 | 
			
		||||
        var user = userId.Value.Equals(default)
 | 
			
		||||
            ? null
 | 
			
		||||
            : _userManager.GetUserById(userId.Value);
 | 
			
		||||
 | 
			
		||||
        var item = itemId.Equals(default)
 | 
			
		||||
            ? (userId is null || userId.Value.Equals(default)
 | 
			
		||||
            ? (userId.Value.Equals(default)
 | 
			
		||||
                ? _libraryManager.RootFolder
 | 
			
		||||
                : _libraryManager.GetUserRootFolder())
 | 
			
		||||
            : _libraryManager.GetItemById(itemId);
 | 
			
		||||
@ -208,12 +210,13 @@ public class LibraryController : BaseJellyfinApiController
 | 
			
		||||
        [FromQuery] Guid? userId,
 | 
			
		||||
        [FromQuery] bool inheritFromParent = false)
 | 
			
		||||
    {
 | 
			
		||||
        var user = userId is null || userId.Value.Equals(default)
 | 
			
		||||
        userId = RequestHelpers.GetUserId(User, userId);
 | 
			
		||||
        var user = userId.Value.Equals(default)
 | 
			
		||||
            ? null
 | 
			
		||||
            : _userManager.GetUserById(userId.Value);
 | 
			
		||||
 | 
			
		||||
        var item = itemId.Equals(default)
 | 
			
		||||
            ? (userId is null || userId.Value.Equals(default)
 | 
			
		||||
            ? (userId.Value.Equals(default)
 | 
			
		||||
                ? _libraryManager.RootFolder
 | 
			
		||||
                : _libraryManager.GetUserRootFolder())
 | 
			
		||||
            : _libraryManager.GetItemById(itemId);
 | 
			
		||||
@ -403,7 +406,8 @@ public class LibraryController : BaseJellyfinApiController
 | 
			
		||||
        [FromQuery] Guid? userId,
 | 
			
		||||
        [FromQuery] bool? isFavorite)
 | 
			
		||||
    {
 | 
			
		||||
        var user = userId is null || userId.Value.Equals(default)
 | 
			
		||||
        userId = RequestHelpers.GetUserId(User, userId);
 | 
			
		||||
        var user = userId.Value.Equals(default)
 | 
			
		||||
            ? null
 | 
			
		||||
            : _userManager.GetUserById(userId.Value);
 | 
			
		||||
 | 
			
		||||
@ -437,6 +441,7 @@ public class LibraryController : BaseJellyfinApiController
 | 
			
		||||
    public ActionResult<IEnumerable<BaseItemDto>> GetAncestors([FromRoute, Required] Guid itemId, [FromQuery] Guid? userId)
 | 
			
		||||
    {
 | 
			
		||||
        var item = _libraryManager.GetItemById(itemId);
 | 
			
		||||
        userId = RequestHelpers.GetUserId(User, userId);
 | 
			
		||||
 | 
			
		||||
        if (item is null)
 | 
			
		||||
        {
 | 
			
		||||
@ -445,7 +450,7 @@ public class LibraryController : BaseJellyfinApiController
 | 
			
		||||
 | 
			
		||||
        var baseItemDtos = new List<BaseItemDto>();
 | 
			
		||||
 | 
			
		||||
        var user = userId is null || userId.Value.Equals(default)
 | 
			
		||||
        var user = userId.Value.Equals(default)
 | 
			
		||||
            ? null
 | 
			
		||||
            : _userManager.GetUserById(userId.Value);
 | 
			
		||||
 | 
			
		||||
@ -675,8 +680,9 @@ public class LibraryController : BaseJellyfinApiController
 | 
			
		||||
        [FromQuery] int? limit,
 | 
			
		||||
        [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields)
 | 
			
		||||
    {
 | 
			
		||||
        userId = RequestHelpers.GetUserId(User, userId);
 | 
			
		||||
        var item = itemId.Equals(default)
 | 
			
		||||
            ? (userId is null || userId.Value.Equals(default)
 | 
			
		||||
            ? (userId.Value.Equals(default)
 | 
			
		||||
                ? _libraryManager.RootFolder
 | 
			
		||||
                : _libraryManager.GetUserRootFolder())
 | 
			
		||||
            : _libraryManager.GetItemById(itemId);
 | 
			
		||||
@ -691,7 +697,7 @@ public class LibraryController : BaseJellyfinApiController
 | 
			
		||||
            return new QueryResult<BaseItemDto>();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var user = userId is null || userId.Value.Equals(default)
 | 
			
		||||
        var user = userId.Value.Equals(default)
 | 
			
		||||
            ? null
 | 
			
		||||
            : _userManager.GetUserById(userId.Value);
 | 
			
		||||
        var dtoOptions = new DtoOptions { Fields = fields }
 | 
			
		||||
 | 
			
		||||
@ -153,6 +153,7 @@ public class LiveTvController : BaseJellyfinApiController
 | 
			
		||||
        [FromQuery] bool enableFavoriteSorting = false,
 | 
			
		||||
        [FromQuery] bool addCurrentProgram = true)
 | 
			
		||||
    {
 | 
			
		||||
        userId = RequestHelpers.GetUserId(User, userId);
 | 
			
		||||
        var dtoOptions = new DtoOptions { Fields = fields }
 | 
			
		||||
            .AddClientFields(User)
 | 
			
		||||
            .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
 | 
			
		||||
@ -161,7 +162,7 @@ public class LiveTvController : BaseJellyfinApiController
 | 
			
		||||
            new LiveTvChannelQuery
 | 
			
		||||
            {
 | 
			
		||||
                ChannelType = type,
 | 
			
		||||
                UserId = userId ?? Guid.Empty,
 | 
			
		||||
                UserId = userId.Value,
 | 
			
		||||
                StartIndex = startIndex,
 | 
			
		||||
                Limit = limit,
 | 
			
		||||
                IsFavorite = isFavorite,
 | 
			
		||||
@ -180,7 +181,7 @@ public class LiveTvController : BaseJellyfinApiController
 | 
			
		||||
            dtoOptions,
 | 
			
		||||
            CancellationToken.None);
 | 
			
		||||
 | 
			
		||||
        var user = userId is null || userId.Value.Equals(default)
 | 
			
		||||
        var user = userId.Value.Equals(default)
 | 
			
		||||
            ? null
 | 
			
		||||
            : _userManager.GetUserById(userId.Value);
 | 
			
		||||
 | 
			
		||||
@ -211,7 +212,8 @@ public class LiveTvController : BaseJellyfinApiController
 | 
			
		||||
    [Authorize(Policy = Policies.LiveTvAccess)]
 | 
			
		||||
    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
 | 
			
		||||
            : _userManager.GetUserById(userId.Value);
 | 
			
		||||
        var item = channelId.Equals(default)
 | 
			
		||||
@ -271,6 +273,7 @@ public class LiveTvController : BaseJellyfinApiController
 | 
			
		||||
        [FromQuery] bool? isLibraryItem,
 | 
			
		||||
        [FromQuery] bool enableTotalRecordCount = true)
 | 
			
		||||
    {
 | 
			
		||||
        userId = RequestHelpers.GetUserId(User, userId);
 | 
			
		||||
        var dtoOptions = new DtoOptions { Fields = fields }
 | 
			
		||||
            .AddClientFields(User)
 | 
			
		||||
            .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
 | 
			
		||||
@ -279,7 +282,7 @@ public class LiveTvController : BaseJellyfinApiController
 | 
			
		||||
            new RecordingQuery
 | 
			
		||||
            {
 | 
			
		||||
                ChannelId = channelId,
 | 
			
		||||
                UserId = userId ?? Guid.Empty,
 | 
			
		||||
                UserId = userId.Value,
 | 
			
		||||
                StartIndex = startIndex,
 | 
			
		||||
                Limit = limit,
 | 
			
		||||
                Status = status,
 | 
			
		||||
@ -382,7 +385,8 @@ public class LiveTvController : BaseJellyfinApiController
 | 
			
		||||
    [Authorize(Policy = Policies.LiveTvAccess)]
 | 
			
		||||
    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
 | 
			
		||||
            : _userManager.GetUserById(userId.Value);
 | 
			
		||||
        var folders = _liveTvManager.GetRecordingFolders(user);
 | 
			
		||||
@ -404,7 +408,8 @@ public class LiveTvController : BaseJellyfinApiController
 | 
			
		||||
    [Authorize(Policy = Policies.LiveTvAccess)]
 | 
			
		||||
    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
 | 
			
		||||
            : _userManager.GetUserById(userId.Value);
 | 
			
		||||
        var item = recordingId.Equals(default) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(recordingId);
 | 
			
		||||
@ -560,7 +565,8 @@ public class LiveTvController : BaseJellyfinApiController
 | 
			
		||||
        [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
 | 
			
		||||
        [FromQuery] bool enableTotalRecordCount = true)
 | 
			
		||||
    {
 | 
			
		||||
        var user = userId is null || userId.Value.Equals(default)
 | 
			
		||||
        userId = RequestHelpers.GetUserId(User, userId);
 | 
			
		||||
        var user = userId.Value.Equals(default)
 | 
			
		||||
            ? null
 | 
			
		||||
            : _userManager.GetUserById(userId.Value);
 | 
			
		||||
 | 
			
		||||
@ -699,7 +705,8 @@ public class LiveTvController : BaseJellyfinApiController
 | 
			
		||||
        [FromQuery] bool? enableUserData,
 | 
			
		||||
        [FromQuery] bool enableTotalRecordCount = true)
 | 
			
		||||
    {
 | 
			
		||||
        var user = userId is null || userId.Value.Equals(default)
 | 
			
		||||
        userId = RequestHelpers.GetUserId(User, userId);
 | 
			
		||||
        var user = userId.Value.Equals(default)
 | 
			
		||||
            ? null
 | 
			
		||||
            : _userManager.GetUserById(userId.Value);
 | 
			
		||||
 | 
			
		||||
@ -737,7 +744,8 @@ public class LiveTvController : BaseJellyfinApiController
 | 
			
		||||
        [FromRoute, Required] string programId,
 | 
			
		||||
        [FromQuery] Guid? userId)
 | 
			
		||||
    {
 | 
			
		||||
        var user = userId is null || userId.Value.Equals(default)
 | 
			
		||||
        userId = RequestHelpers.GetUserId(User, userId);
 | 
			
		||||
        var user = userId.Value.Equals(default)
 | 
			
		||||
            ? null
 | 
			
		||||
            : _userManager.GetUserById(userId.Value);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -132,6 +132,7 @@ public class MediaInfoController : BaseJellyfinApiController
 | 
			
		||||
        // Copy params from posted body
 | 
			
		||||
        // TODO clean up when breaking API compatibility.
 | 
			
		||||
        userId ??= playbackInfoDto?.UserId;
 | 
			
		||||
        userId = RequestHelpers.GetUserId(User, userId);
 | 
			
		||||
        maxStreamingBitrate ??= playbackInfoDto?.MaxStreamingBitrate;
 | 
			
		||||
        startTimeTicks ??= playbackInfoDto?.StartTimeTicks;
 | 
			
		||||
        audioStreamIndex ??= playbackInfoDto?.AudioStreamIndex;
 | 
			
		||||
@ -253,10 +254,12 @@ public class MediaInfoController : BaseJellyfinApiController
 | 
			
		||||
        [FromQuery] bool? enableDirectPlay,
 | 
			
		||||
        [FromQuery] bool? enableDirectStream)
 | 
			
		||||
    {
 | 
			
		||||
        userId ??= openLiveStreamDto?.UserId;
 | 
			
		||||
        userId = RequestHelpers.GetUserId(User, userId);
 | 
			
		||||
        var request = new LiveStreamRequest
 | 
			
		||||
        {
 | 
			
		||||
            OpenToken = openToken ?? openLiveStreamDto?.OpenToken,
 | 
			
		||||
            UserId = userId ?? openLiveStreamDto?.UserId ?? Guid.Empty,
 | 
			
		||||
            UserId = userId.Value,
 | 
			
		||||
            PlaySessionId = playSessionId ?? openLiveStreamDto?.PlaySessionId,
 | 
			
		||||
            MaxStreamingBitrate = maxStreamingBitrate ?? openLiveStreamDto?.MaxStreamingBitrate,
 | 
			
		||||
            StartTimeTicks = startTimeTicks ?? openLiveStreamDto?.StartTimeTicks,
 | 
			
		||||
 | 
			
		||||
@ -3,6 +3,7 @@ using System.Collections.Generic;
 | 
			
		||||
using System.Globalization;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using Jellyfin.Api.Extensions;
 | 
			
		||||
using Jellyfin.Api.Helpers;
 | 
			
		||||
using Jellyfin.Api.ModelBinders;
 | 
			
		||||
using Jellyfin.Data.Entities;
 | 
			
		||||
using Jellyfin.Data.Enums;
 | 
			
		||||
@ -67,7 +68,8 @@ public class MoviesController : BaseJellyfinApiController
 | 
			
		||||
        [FromQuery] int categoryLimit = 5,
 | 
			
		||||
        [FromQuery] int itemLimit = 8)
 | 
			
		||||
    {
 | 
			
		||||
        var user = userId is null || userId.Value.Equals(default)
 | 
			
		||||
        userId = RequestHelpers.GetUserId(User, userId);
 | 
			
		||||
        var user = userId.Value.Equals(default)
 | 
			
		||||
            ? null
 | 
			
		||||
            : _userManager.GetUserById(userId.Value);
 | 
			
		||||
        var dtoOptions = new DtoOptions { Fields = fields }
 | 
			
		||||
 | 
			
		||||
@ -90,11 +90,12 @@ public class MusicGenresController : BaseJellyfinApiController
 | 
			
		||||
        [FromQuery] bool? enableImages = true,
 | 
			
		||||
        [FromQuery] bool enableTotalRecordCount = true)
 | 
			
		||||
    {
 | 
			
		||||
        userId = RequestHelpers.GetUserId(User, userId);
 | 
			
		||||
        var dtoOptions = new DtoOptions { Fields = fields }
 | 
			
		||||
            .AddClientFields(User)
 | 
			
		||||
            .AddAdditionalDtoOptions(enableImages, false, imageTypeLimit, enableImageTypes);
 | 
			
		||||
 | 
			
		||||
        User? user = userId is null || userId.Value.Equals(default)
 | 
			
		||||
        User? user = userId.Value.Equals(default)
 | 
			
		||||
            ? null
 | 
			
		||||
            : _userManager.GetUserById(userId.Value);
 | 
			
		||||
 | 
			
		||||
@ -144,6 +145,7 @@ public class MusicGenresController : BaseJellyfinApiController
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status200OK)]
 | 
			
		||||
    public ActionResult<BaseItemDto> GetMusicGenre([FromRoute, Required] string genreName, [FromQuery] Guid? userId)
 | 
			
		||||
    {
 | 
			
		||||
        userId = RequestHelpers.GetUserId(User, userId);
 | 
			
		||||
        var dtoOptions = new DtoOptions().AddClientFields(User);
 | 
			
		||||
 | 
			
		||||
        MusicGenre? item;
 | 
			
		||||
@ -162,7 +164,7 @@ public class MusicGenresController : BaseJellyfinApiController
 | 
			
		||||
            return NotFound();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (userId.HasValue && !userId.Value.Equals(default))
 | 
			
		||||
        if (!userId.Value.Equals(default))
 | 
			
		||||
        {
 | 
			
		||||
            var user = _userManager.GetUserById(userId.Value);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -2,6 +2,7 @@ using System;
 | 
			
		||||
using System.ComponentModel.DataAnnotations;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using Jellyfin.Api.Extensions;
 | 
			
		||||
using Jellyfin.Api.Helpers;
 | 
			
		||||
using Jellyfin.Api.ModelBinders;
 | 
			
		||||
using Jellyfin.Data.Entities;
 | 
			
		||||
using MediaBrowser.Controller.Dto;
 | 
			
		||||
@ -77,11 +78,12 @@ public class PersonsController : BaseJellyfinApiController
 | 
			
		||||
        [FromQuery] Guid? userId,
 | 
			
		||||
        [FromQuery] bool? enableImages = true)
 | 
			
		||||
    {
 | 
			
		||||
        userId = RequestHelpers.GetUserId(User, userId);
 | 
			
		||||
        var dtoOptions = new DtoOptions { Fields = fields }
 | 
			
		||||
            .AddClientFields(User)
 | 
			
		||||
            .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
 | 
			
		||||
 | 
			
		||||
        User? user = userId is null || userId.Value.Equals(default)
 | 
			
		||||
        User? user = userId.Value.Equals(default)
 | 
			
		||||
            ? null
 | 
			
		||||
            : _userManager.GetUserById(userId.Value);
 | 
			
		||||
 | 
			
		||||
@ -117,6 +119,7 @@ public class PersonsController : BaseJellyfinApiController
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status404NotFound)]
 | 
			
		||||
    public ActionResult<BaseItemDto> GetPerson([FromRoute, Required] string name, [FromQuery] Guid? userId)
 | 
			
		||||
    {
 | 
			
		||||
        userId = RequestHelpers.GetUserId(User, userId);
 | 
			
		||||
        var dtoOptions = new DtoOptions()
 | 
			
		||||
            .AddClientFields(User);
 | 
			
		||||
 | 
			
		||||
@ -126,7 +129,7 @@ public class PersonsController : BaseJellyfinApiController
 | 
			
		||||
            return NotFound();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (userId.HasValue && !userId.Value.Equals(default))
 | 
			
		||||
        if (!userId.Value.Equals(default))
 | 
			
		||||
        {
 | 
			
		||||
            var user = _userManager.GetUserById(userId.Value);
 | 
			
		||||
            return _dtoService.GetBaseItemDto(item, dtoOptions, user);
 | 
			
		||||
 | 
			
		||||
@ -5,6 +5,7 @@ using System.Linq;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using Jellyfin.Api.Attributes;
 | 
			
		||||
using Jellyfin.Api.Extensions;
 | 
			
		||||
using Jellyfin.Api.Helpers;
 | 
			
		||||
using Jellyfin.Api.ModelBinders;
 | 
			
		||||
using Jellyfin.Api.Models.PlaylistDtos;
 | 
			
		||||
using MediaBrowser.Controller.Dto;
 | 
			
		||||
@ -81,11 +82,13 @@ public class PlaylistsController : BaseJellyfinApiController
 | 
			
		||||
            ids = createPlaylistRequest?.Ids ?? Array.Empty<Guid>();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        userId ??= createPlaylistRequest?.UserId ?? default;
 | 
			
		||||
        userId = RequestHelpers.GetUserId(User, userId);
 | 
			
		||||
        var result = await _playlistManager.CreatePlaylist(new PlaylistCreationRequest
 | 
			
		||||
        {
 | 
			
		||||
            Name = name ?? createPlaylistRequest?.Name,
 | 
			
		||||
            ItemIdList = ids,
 | 
			
		||||
            UserId = userId ?? createPlaylistRequest?.UserId ?? default,
 | 
			
		||||
            UserId = userId.Value,
 | 
			
		||||
            MediaType = mediaType ?? createPlaylistRequest?.MediaType
 | 
			
		||||
        }).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
@ -107,7 +110,8 @@ public class PlaylistsController : BaseJellyfinApiController
 | 
			
		||||
        [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids,
 | 
			
		||||
        [FromQuery] Guid? userId)
 | 
			
		||||
    {
 | 
			
		||||
        await _playlistManager.AddToPlaylistAsync(playlistId, ids, userId ?? Guid.Empty).ConfigureAwait(false);
 | 
			
		||||
        userId = RequestHelpers.GetUserId(User, userId);
 | 
			
		||||
        await _playlistManager.AddToPlaylistAsync(playlistId, ids, userId.Value).ConfigureAwait(false);
 | 
			
		||||
        return NoContent();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -3,6 +3,7 @@ using System.ComponentModel.DataAnnotations;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using Jellyfin.Api.Constants;
 | 
			
		||||
using Jellyfin.Api.Extensions;
 | 
			
		||||
using Jellyfin.Api.Helpers;
 | 
			
		||||
using MediaBrowser.Common.Extensions;
 | 
			
		||||
using MediaBrowser.Controller.Authentication;
 | 
			
		||||
using MediaBrowser.Controller.Net;
 | 
			
		||||
@ -116,17 +117,11 @@ public class QuickConnectController : BaseJellyfinApiController
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status403Forbidden)]
 | 
			
		||||
    public async Task<ActionResult<bool>> AuthorizeQuickConnect([FromQuery, Required] string code, [FromQuery] Guid? userId = null)
 | 
			
		||||
    {
 | 
			
		||||
        var currentUserId = User.GetUserId();
 | 
			
		||||
        var actualUserId = userId ?? currentUserId;
 | 
			
		||||
 | 
			
		||||
        if (actualUserId.Equals(default) || (!userId.Equals(currentUserId) && !User.IsInRole(UserRoles.Administrator)))
 | 
			
		||||
        {
 | 
			
		||||
            return Forbid("Unknown user id");
 | 
			
		||||
        }
 | 
			
		||||
        userId = RequestHelpers.GetUserId(User, userId);
 | 
			
		||||
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            return await _quickConnect.AuthorizeRequest(actualUserId, code).ConfigureAwait(false);
 | 
			
		||||
            return await _quickConnect.AuthorizeRequest(userId.Value, code).ConfigureAwait(false);
 | 
			
		||||
        }
 | 
			
		||||
        catch (AuthenticationException)
 | 
			
		||||
        {
 | 
			
		||||
 | 
			
		||||
@ -3,6 +3,8 @@ using System.ComponentModel;
 | 
			
		||||
using System.ComponentModel.DataAnnotations;
 | 
			
		||||
using System.Globalization;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using Jellyfin.Api.Constants;
 | 
			
		||||
using Jellyfin.Api.Helpers;
 | 
			
		||||
using Jellyfin.Api.ModelBinders;
 | 
			
		||||
using Jellyfin.Data.Enums;
 | 
			
		||||
using Jellyfin.Extensions;
 | 
			
		||||
@ -98,6 +100,7 @@ public class SearchController : BaseJellyfinApiController
 | 
			
		||||
        [FromQuery] bool includeStudios = true,
 | 
			
		||||
        [FromQuery] bool includeArtists = true)
 | 
			
		||||
    {
 | 
			
		||||
        userId = RequestHelpers.GetUserId(User, userId);
 | 
			
		||||
        var result = _searchEngine.GetSearchHints(new SearchQuery
 | 
			
		||||
        {
 | 
			
		||||
            Limit = limit,
 | 
			
		||||
@ -108,7 +111,7 @@ public class SearchController : BaseJellyfinApiController
 | 
			
		||||
            IncludePeople = includePeople,
 | 
			
		||||
            IncludeStudios = includeStudios,
 | 
			
		||||
            StartIndex = startIndex,
 | 
			
		||||
            UserId = userId ?? Guid.Empty,
 | 
			
		||||
            UserId = userId.Value,
 | 
			
		||||
            IncludeItemTypes = includeItemTypes,
 | 
			
		||||
            ExcludeItemTypes = excludeItemTypes,
 | 
			
		||||
            MediaTypes = mediaTypes,
 | 
			
		||||
 | 
			
		||||
@ -86,11 +86,12 @@ public class StudiosController : BaseJellyfinApiController
 | 
			
		||||
        [FromQuery] bool? enableImages = true,
 | 
			
		||||
        [FromQuery] bool enableTotalRecordCount = true)
 | 
			
		||||
    {
 | 
			
		||||
        userId = RequestHelpers.GetUserId(User, userId);
 | 
			
		||||
        var dtoOptions = new DtoOptions { Fields = fields }
 | 
			
		||||
            .AddClientFields(User)
 | 
			
		||||
            .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
 | 
			
		||||
 | 
			
		||||
        User? user = userId is null || userId.Value.Equals(default)
 | 
			
		||||
        User? user = userId.Value.Equals(default)
 | 
			
		||||
            ? null
 | 
			
		||||
            : _userManager.GetUserById(userId.Value);
 | 
			
		||||
 | 
			
		||||
@ -139,10 +140,11 @@ public class StudiosController : BaseJellyfinApiController
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status200OK)]
 | 
			
		||||
    public ActionResult<BaseItemDto> GetStudio([FromRoute, Required] string name, [FromQuery] Guid? userId)
 | 
			
		||||
    {
 | 
			
		||||
        userId = RequestHelpers.GetUserId(User, userId);
 | 
			
		||||
        var dtoOptions = new DtoOptions().AddClientFields(User);
 | 
			
		||||
 | 
			
		||||
        var item = _libraryManager.GetStudio(name);
 | 
			
		||||
        if (userId.HasValue && !userId.Equals(default))
 | 
			
		||||
        if (!userId.Equals(default))
 | 
			
		||||
        {
 | 
			
		||||
            var user = _userManager.GetUserById(userId.Value);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -3,6 +3,7 @@ using System.Collections.Generic;
 | 
			
		||||
using System.ComponentModel.DataAnnotations;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using Jellyfin.Api.Extensions;
 | 
			
		||||
using Jellyfin.Api.Helpers;
 | 
			
		||||
using Jellyfin.Api.ModelBinders;
 | 
			
		||||
using Jellyfin.Data.Enums;
 | 
			
		||||
using Jellyfin.Extensions;
 | 
			
		||||
@ -87,6 +88,7 @@ public class TvShowsController : BaseJellyfinApiController
 | 
			
		||||
        [FromQuery] bool disableFirstEpisode = false,
 | 
			
		||||
        [FromQuery] bool enableRewatching = false)
 | 
			
		||||
    {
 | 
			
		||||
        userId = RequestHelpers.GetUserId(User, userId);
 | 
			
		||||
        var options = new DtoOptions { Fields = fields }
 | 
			
		||||
            .AddClientFields(User)
 | 
			
		||||
            .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
 | 
			
		||||
@ -98,7 +100,7 @@ public class TvShowsController : BaseJellyfinApiController
 | 
			
		||||
                ParentId = parentId,
 | 
			
		||||
                SeriesId = seriesId,
 | 
			
		||||
                StartIndex = startIndex,
 | 
			
		||||
                UserId = userId ?? Guid.Empty,
 | 
			
		||||
                UserId = userId.Value,
 | 
			
		||||
                EnableTotalRecordCount = enableTotalRecordCount,
 | 
			
		||||
                DisableFirstEpisode = disableFirstEpisode,
 | 
			
		||||
                NextUpDateCutoff = nextUpDateCutoff ?? DateTime.MinValue,
 | 
			
		||||
@ -106,7 +108,7 @@ public class TvShowsController : BaseJellyfinApiController
 | 
			
		||||
            },
 | 
			
		||||
            options);
 | 
			
		||||
 | 
			
		||||
        var user = userId is null || userId.Value.Equals(default)
 | 
			
		||||
        var user = userId.Value.Equals(default)
 | 
			
		||||
            ? null
 | 
			
		||||
            : _userManager.GetUserById(userId.Value);
 | 
			
		||||
 | 
			
		||||
@ -144,7 +146,8 @@ public class TvShowsController : BaseJellyfinApiController
 | 
			
		||||
        [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
 | 
			
		||||
        [FromQuery] bool? enableUserData)
 | 
			
		||||
    {
 | 
			
		||||
        var user = userId is null || userId.Value.Equals(default)
 | 
			
		||||
        userId = RequestHelpers.GetUserId(User, userId);
 | 
			
		||||
        var user = userId.Value.Equals(default)
 | 
			
		||||
            ? null
 | 
			
		||||
            : _userManager.GetUserById(userId.Value);
 | 
			
		||||
 | 
			
		||||
@ -215,7 +218,8 @@ public class TvShowsController : BaseJellyfinApiController
 | 
			
		||||
        [FromQuery] bool? enableUserData,
 | 
			
		||||
        [FromQuery] string? sortBy)
 | 
			
		||||
    {
 | 
			
		||||
        var user = userId is null || userId.Value.Equals(default)
 | 
			
		||||
        userId = RequestHelpers.GetUserId(User, userId);
 | 
			
		||||
        var user = userId.Value.Equals(default)
 | 
			
		||||
            ? null
 | 
			
		||||
            : _userManager.GetUserById(userId.Value);
 | 
			
		||||
 | 
			
		||||
@ -331,7 +335,8 @@ public class TvShowsController : BaseJellyfinApiController
 | 
			
		||||
        [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
 | 
			
		||||
        [FromQuery] bool? enableUserData)
 | 
			
		||||
    {
 | 
			
		||||
        var user = userId is null || userId.Value.Equals(default)
 | 
			
		||||
        userId = RequestHelpers.GetUserId(User, userId);
 | 
			
		||||
        var user = userId.Value.Equals(default)
 | 
			
		||||
            ? null
 | 
			
		||||
            : _userManager.GetUserById(userId.Value);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -106,11 +106,7 @@ public class UniversalAudioController : BaseJellyfinApiController
 | 
			
		||||
        [FromQuery] bool enableRedirection = true)
 | 
			
		||||
    {
 | 
			
		||||
        var deviceProfile = GetDeviceProfile(container, transcodingContainer, audioCodec, transcodingProtocol, breakOnNonKeyFrames, transcodingAudioChannels, maxAudioSampleRate, maxAudioBitDepth, maxAudioChannels);
 | 
			
		||||
 | 
			
		||||
        if (!userId.HasValue || userId.Value.Equals(default))
 | 
			
		||||
        {
 | 
			
		||||
            userId = User.GetUserId();
 | 
			
		||||
        }
 | 
			
		||||
        userId = RequestHelpers.GetUserId(User, userId);
 | 
			
		||||
 | 
			
		||||
        _logger.LogInformation("GetPostedPlaybackInfo profile: {@Profile}", deviceProfile);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -104,12 +104,13 @@ public class VideosController : BaseJellyfinApiController
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status200OK)]
 | 
			
		||||
    public ActionResult<QueryResult<BaseItemDto>> GetAdditionalPart([FromRoute, Required] Guid itemId, [FromQuery] Guid? userId)
 | 
			
		||||
    {
 | 
			
		||||
        var user = userId is null || userId.Value.Equals(default)
 | 
			
		||||
        userId = RequestHelpers.GetUserId(User, userId);
 | 
			
		||||
        var user = userId.Value.Equals(default)
 | 
			
		||||
            ? null
 | 
			
		||||
            : _userManager.GetUserById(userId.Value);
 | 
			
		||||
 | 
			
		||||
        var item = itemId.Equals(default)
 | 
			
		||||
            ? (userId is null || userId.Value.Equals(default)
 | 
			
		||||
            ? (userId.Value.Equals(default)
 | 
			
		||||
                ? _libraryManager.RootFolder
 | 
			
		||||
                : _libraryManager.GetUserRootFolder())
 | 
			
		||||
            : _libraryManager.GetItemById(itemId);
 | 
			
		||||
 | 
			
		||||
@ -85,11 +85,12 @@ public class YearsController : BaseJellyfinApiController
 | 
			
		||||
        [FromQuery] bool recursive = true,
 | 
			
		||||
        [FromQuery] bool? enableImages = true)
 | 
			
		||||
    {
 | 
			
		||||
        userId = RequestHelpers.GetUserId(User, userId);
 | 
			
		||||
        var dtoOptions = new DtoOptions { Fields = fields }
 | 
			
		||||
            .AddClientFields(User)
 | 
			
		||||
            .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
 | 
			
		||||
 | 
			
		||||
        User? user = userId is null || userId.Value.Equals(default)
 | 
			
		||||
        User? user = userId.Value.Equals(default)
 | 
			
		||||
            ? null
 | 
			
		||||
            : _userManager.GetUserById(userId.Value);
 | 
			
		||||
        BaseItem parentItem = _libraryManager.GetParentItem(parentId, userId);
 | 
			
		||||
@ -171,6 +172,7 @@ public class YearsController : BaseJellyfinApiController
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status404NotFound)]
 | 
			
		||||
    public ActionResult<BaseItemDto> GetYear([FromRoute, Required] int year, [FromQuery] Guid? userId)
 | 
			
		||||
    {
 | 
			
		||||
        userId = RequestHelpers.GetUserId(User, userId);
 | 
			
		||||
        var item = _libraryManager.GetYear(year);
 | 
			
		||||
        if (item is null)
 | 
			
		||||
        {
 | 
			
		||||
@ -180,7 +182,7 @@ public class YearsController : BaseJellyfinApiController
 | 
			
		||||
        var dtoOptions = new DtoOptions()
 | 
			
		||||
            .AddClientFields(User);
 | 
			
		||||
 | 
			
		||||
        if (userId.HasValue && !userId.Value.Equals(default))
 | 
			
		||||
        if (!userId.Value.Equals(default))
 | 
			
		||||
        {
 | 
			
		||||
            var user = _userManager.GetUserById(userId.Value);
 | 
			
		||||
            return _dtoService.GetBaseItemDto(item, dtoOptions, user);
 | 
			
		||||
 | 
			
		||||
@ -71,8 +71,7 @@ public static class ClaimsPrincipalExtensions
 | 
			
		||||
    public static bool GetIsApiKey(this ClaimsPrincipal user)
 | 
			
		||||
    {
 | 
			
		||||
        var claimValue = GetClaimValue(user, InternalClaimTypes.IsApiKey);
 | 
			
		||||
        return !string.IsNullOrEmpty(claimValue)
 | 
			
		||||
               && bool.TryParse(claimValue, out var parsedClaimValue)
 | 
			
		||||
        return bool.TryParse(claimValue, out var parsedClaimValue)
 | 
			
		||||
               && parsedClaimValue;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -11,6 +11,7 @@ using MediaBrowser.Common.Extensions;
 | 
			
		||||
using MediaBrowser.Controller.Dto;
 | 
			
		||||
using MediaBrowser.Controller.Entities;
 | 
			
		||||
using MediaBrowser.Controller.Library;
 | 
			
		||||
using MediaBrowser.Controller.Net;
 | 
			
		||||
using MediaBrowser.Controller.Session;
 | 
			
		||||
using MediaBrowser.Model.Dto;
 | 
			
		||||
using MediaBrowser.Model.Querying;
 | 
			
		||||
@ -55,6 +56,32 @@ public static class RequestHelpers
 | 
			
		||||
        return result;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Checks if the user can access a user.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="claimsPrincipal">The <see cref="ClaimsPrincipal"/> for the current request.</param>
 | 
			
		||||
    /// <param name="userId">The user id.</param>
 | 
			
		||||
    /// <returns>A <see cref="bool"/> whether the user can access the user.</returns>
 | 
			
		||||
    internal static Guid GetUserId(ClaimsPrincipal claimsPrincipal, Guid? userId)
 | 
			
		||||
    {
 | 
			
		||||
        var authenticatedUserId = claimsPrincipal.GetUserId();
 | 
			
		||||
 | 
			
		||||
        // UserId not provided, fall back to authenticated user id.
 | 
			
		||||
        if (userId is null || userId.Value.Equals(default))
 | 
			
		||||
        {
 | 
			
		||||
            return authenticatedUserId;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // User must be administrator to access another user.
 | 
			
		||||
        var isAdministrator = claimsPrincipal.IsInRole(UserRoles.Administrator);
 | 
			
		||||
        if (!userId.Value.Equals(authenticatedUserId) && !isAdministrator)
 | 
			
		||||
        {
 | 
			
		||||
            throw new SecurityException("Forbidden");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return userId.Value;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Checks if the user can update an entry.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
 | 
			
		||||
@ -337,10 +337,10 @@ public static class StreamingHelpers
 | 
			
		||||
        value = index == -1
 | 
			
		||||
            ? value.Slice(npt.Length)
 | 
			
		||||
            : value.Slice(npt.Length, index - npt.Length);
 | 
			
		||||
        if (value.IndexOf(':') == -1)
 | 
			
		||||
        if (!value.Contains(':'))
 | 
			
		||||
        {
 | 
			
		||||
            // Parses npt times in the format of '417.33'
 | 
			
		||||
            if (double.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var seconds))
 | 
			
		||||
            if (double.TryParse(value, CultureInfo.InvariantCulture, out var seconds))
 | 
			
		||||
            {
 | 
			
		||||
                return TimeSpan.FromSeconds(seconds).Ticks;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
@ -457,8 +457,7 @@ public class TranscodingJobHelper : IDisposable
 | 
			
		||||
            var videoCodec = state.ActualOutputVideoCodec;
 | 
			
		||||
            var hardwareAccelerationTypeString = _serverConfigurationManager.GetEncodingOptions().HardwareAccelerationType;
 | 
			
		||||
            HardwareEncodingType? hardwareAccelerationType = null;
 | 
			
		||||
            if (!string.IsNullOrEmpty(hardwareAccelerationTypeString)
 | 
			
		||||
                && Enum.TryParse<HardwareEncodingType>(hardwareAccelerationTypeString, out var parsedHardwareAccelerationType))
 | 
			
		||||
            if (Enum.TryParse<HardwareEncodingType>(hardwareAccelerationTypeString, out var parsedHardwareAccelerationType))
 | 
			
		||||
            {
 | 
			
		||||
                hardwareAccelerationType = parsedHardwareAccelerationType;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
@ -740,7 +740,7 @@ namespace Jellyfin.Server.Implementations.Users
 | 
			
		||||
            throw new ArgumentException("Usernames can contain unicode symbols, numbers (0-9), dashes (-), underscores (_), apostrophes ('), and periods (.)", nameof(name));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private static bool IsValidUsername(string name)
 | 
			
		||||
        private static bool IsValidUsername(ReadOnlySpan<char> name)
 | 
			
		||||
        {
 | 
			
		||||
            // This is some regex that matches only on unicode "word" characters, as well as -, _ and @
 | 
			
		||||
            // In theory this will cut out most if not all 'control' characters which should help minimize any weirdness
 | 
			
		||||
 | 
			
		||||
@ -21,7 +21,8 @@ namespace Jellyfin.Server.Migrations
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        private static readonly Type[] _preStartupMigrationTypes =
 | 
			
		||||
        {
 | 
			
		||||
            typeof(PreStartupRoutines.CreateNetworkConfiguration)
 | 
			
		||||
            typeof(PreStartupRoutines.CreateNetworkConfiguration),
 | 
			
		||||
            typeof(PreStartupRoutines.MigrateMusicBrainzTimeout)
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,89 @@
 | 
			
		||||
using System;
 | 
			
		||||
using System.IO;
 | 
			
		||||
using System.Xml;
 | 
			
		||||
using System.Xml.Serialization;
 | 
			
		||||
using Emby.Server.Implementations;
 | 
			
		||||
using MediaBrowser.Providers.Plugins.MusicBrainz.Configuration;
 | 
			
		||||
using Microsoft.Extensions.Logging;
 | 
			
		||||
 | 
			
		||||
namespace Jellyfin.Server.Migrations.PreStartupRoutines;
 | 
			
		||||
 | 
			
		||||
/// <inheritdoc />
 | 
			
		||||
public class MigrateMusicBrainzTimeout : IMigrationRoutine
 | 
			
		||||
{
 | 
			
		||||
    private readonly ServerApplicationPaths _applicationPaths;
 | 
			
		||||
    private readonly ILogger<MigrateMusicBrainzTimeout> _logger;
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Initializes a new instance of the <see cref="MigrateMusicBrainzTimeout"/> class.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="applicationPaths">An instance of <see cref="ServerApplicationPaths"/>.</param>
 | 
			
		||||
    /// <param name="loggerFactory">An instance of the <see cref="ILoggerFactory"/> interface.</param>
 | 
			
		||||
    public MigrateMusicBrainzTimeout(ServerApplicationPaths applicationPaths, ILoggerFactory loggerFactory)
 | 
			
		||||
    {
 | 
			
		||||
        _applicationPaths = applicationPaths;
 | 
			
		||||
        _logger = loggerFactory.CreateLogger<MigrateMusicBrainzTimeout>();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc />
 | 
			
		||||
    public Guid Id => Guid.Parse("A6DCACF4-C057-4Ef9-80D3-61CEF9DDB4F0");
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc />
 | 
			
		||||
    public string Name => nameof(MigrateMusicBrainzTimeout);
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc />
 | 
			
		||||
    public bool PerformOnNewInstall => false;
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc />
 | 
			
		||||
    public void Perform()
 | 
			
		||||
    {
 | 
			
		||||
        string path = Path.Combine(_applicationPaths.PluginConfigurationsPath, "Jellyfin.Plugin.MusicBrainz.xml");
 | 
			
		||||
        if (!File.Exists(path))
 | 
			
		||||
        {
 | 
			
		||||
            _logger.LogDebug("No MusicBrainz plugin configuration file found, skipping");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var serverConfigSerializer = new XmlSerializer(typeof(OldMusicBrainzConfiguration), new XmlRootAttribute("PluginConfiguration"));
 | 
			
		||||
        using var xmlReader = XmlReader.Create(path);
 | 
			
		||||
        var oldPluginConfiguration = serverConfigSerializer.Deserialize(xmlReader) as OldMusicBrainzConfiguration;
 | 
			
		||||
 | 
			
		||||
        if (oldPluginConfiguration is not null)
 | 
			
		||||
        {
 | 
			
		||||
            var newPluginConfiguration = new PluginConfiguration();
 | 
			
		||||
            newPluginConfiguration.Server = oldPluginConfiguration.Server;
 | 
			
		||||
            newPluginConfiguration.ReplaceArtistName = oldPluginConfiguration.ReplaceArtistName;
 | 
			
		||||
            var newRateLimit = oldPluginConfiguration.RateLimit / 1000.0;
 | 
			
		||||
            newPluginConfiguration.RateLimit = newRateLimit < 1.0 ? 1.0 : newRateLimit;
 | 
			
		||||
 | 
			
		||||
            var pluginConfigurationSerializer = new XmlSerializer(typeof(PluginConfiguration), new XmlRootAttribute("PluginConfiguration"));
 | 
			
		||||
            var xmlWriterSettings = new XmlWriterSettings { Indent = true };
 | 
			
		||||
            using var xmlWriter = XmlWriter.Create(path, xmlWriterSettings);
 | 
			
		||||
            pluginConfigurationSerializer.Serialize(xmlWriter, newPluginConfiguration);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
#pragma warning disable
 | 
			
		||||
    public sealed class OldMusicBrainzConfiguration
 | 
			
		||||
    {
 | 
			
		||||
        private string _server = string.Empty;
 | 
			
		||||
 | 
			
		||||
        private long _rateLimit = 0L;
 | 
			
		||||
 | 
			
		||||
        public string Server
 | 
			
		||||
        {
 | 
			
		||||
            get => _server;
 | 
			
		||||
            set => _server = value.TrimEnd('/');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public long RateLimit
 | 
			
		||||
        {
 | 
			
		||||
            get => _rateLimit;
 | 
			
		||||
            set => _rateLimit = value;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public bool ReplaceArtistName { get; set; }
 | 
			
		||||
    }
 | 
			
		||||
#pragma warning restore
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -130,7 +130,7 @@ namespace Jellyfin.Server.Migrations.Routines
 | 
			
		||||
                        SkipForwardLength = dto.CustomPrefs.TryGetValue("skipForwardLength", out var length) && int.TryParse(length, out var skipForwardLength)
 | 
			
		||||
                            ? skipForwardLength
 | 
			
		||||
                            : 30000,
 | 
			
		||||
                        SkipBackwardLength = dto.CustomPrefs.TryGetValue("skipBackLength", out length) && !string.IsNullOrEmpty(length) && int.TryParse(length, out var skipBackwardLength)
 | 
			
		||||
                        SkipBackwardLength = dto.CustomPrefs.TryGetValue("skipBackLength", out length) && int.TryParse(length, out var skipBackwardLength)
 | 
			
		||||
                            ? skipBackwardLength
 | 
			
		||||
                            : 10000,
 | 
			
		||||
                        EnableNextVideoInfoOverlay = dto.CustomPrefs.TryGetValue("enableNextVideoInfoOverlay", out var enabled) && !string.IsNullOrEmpty(enabled)
 | 
			
		||||
 | 
			
		||||
@ -5,6 +5,7 @@
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.Globalization;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Text.Json.Serialization;
 | 
			
		||||
using Jellyfin.Data.Enums;
 | 
			
		||||
using Jellyfin.Extensions;
 | 
			
		||||
@ -105,13 +106,10 @@ namespace MediaBrowser.Controller.LiveTv
 | 
			
		||||
 | 
			
		||||
        protected override string CreateSortName()
 | 
			
		||||
        {
 | 
			
		||||
            if (!string.IsNullOrEmpty(Number))
 | 
			
		||||
            {
 | 
			
		||||
                if (double.TryParse(Number, NumberStyles.Any, CultureInfo.InvariantCulture, out double number))
 | 
			
		||||
            if (double.TryParse(Number, CultureInfo.InvariantCulture, out double number))
 | 
			
		||||
            {
 | 
			
		||||
                return string.Format(CultureInfo.InvariantCulture, "{0:00000.0}", number) + "-" + (Name ?? string.Empty);
 | 
			
		||||
            }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return (Number ?? string.Empty) + "-" + (Name ?? string.Empty);
 | 
			
		||||
        }
 | 
			
		||||
@ -122,9 +120,7 @@ namespace MediaBrowser.Controller.LiveTv
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public IEnumerable<BaseItem> GetTaggedItems()
 | 
			
		||||
        {
 | 
			
		||||
            return new List<BaseItem>();
 | 
			
		||||
        }
 | 
			
		||||
            => Enumerable.Empty<BaseItem>();
 | 
			
		||||
 | 
			
		||||
        public override List<MediaSourceInfo> GetMediaSources(bool enablePathSubstitution)
 | 
			
		||||
        {
 | 
			
		||||
 | 
			
		||||
@ -1143,7 +1143,7 @@ namespace MediaBrowser.Controller.MediaEncoding
 | 
			
		||||
 | 
			
		||||
        public static string NormalizeTranscodingLevel(EncodingJobInfo state, string level)
 | 
			
		||||
        {
 | 
			
		||||
            if (double.TryParse(level, NumberStyles.Any, CultureInfo.InvariantCulture, out double requestLevel))
 | 
			
		||||
            if (double.TryParse(level, CultureInfo.InvariantCulture, out double requestLevel))
 | 
			
		||||
            {
 | 
			
		||||
                if (string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase)
 | 
			
		||||
                    || string.Equals(state.ActualOutputVideoCodec, "h265", StringComparison.OrdinalIgnoreCase))
 | 
			
		||||
@ -1737,7 +1737,7 @@ namespace MediaBrowser.Controller.MediaEncoding
 | 
			
		||||
                else if (string.Equals(videoEncoder, "hevc_qsv", StringComparison.OrdinalIgnoreCase))
 | 
			
		||||
                {
 | 
			
		||||
                    // hevc_qsv use -level 51 instead of -level 153.
 | 
			
		||||
                    if (double.TryParse(level, NumberStyles.Any, CultureInfo.InvariantCulture, out double hevcLevel))
 | 
			
		||||
                    if (double.TryParse(level, CultureInfo.InvariantCulture, out double hevcLevel))
 | 
			
		||||
                    {
 | 
			
		||||
                        param += " -level " + (hevcLevel / 3);
 | 
			
		||||
                    }
 | 
			
		||||
@ -1916,8 +1916,7 @@ namespace MediaBrowser.Controller.MediaEncoding
 | 
			
		||||
 | 
			
		||||
            // If a specific level was requested, the source must match or be less than
 | 
			
		||||
            var level = state.GetRequestedLevel(videoStream.Codec);
 | 
			
		||||
            if (!string.IsNullOrEmpty(level)
 | 
			
		||||
                && double.TryParse(level, NumberStyles.Any, CultureInfo.InvariantCulture, out var requestLevel))
 | 
			
		||||
            if (double.TryParse(level, CultureInfo.InvariantCulture, out var requestLevel))
 | 
			
		||||
            {
 | 
			
		||||
                if (!videoStream.Level.HasValue)
 | 
			
		||||
                {
 | 
			
		||||
@ -2217,87 +2216,59 @@ namespace MediaBrowser.Controller.MediaEncoding
 | 
			
		||||
 | 
			
		||||
            var request = state.BaseRequest;
 | 
			
		||||
 | 
			
		||||
            var inputChannels = audioStream.Channels;
 | 
			
		||||
 | 
			
		||||
            if (inputChannels <= 0)
 | 
			
		||||
            {
 | 
			
		||||
                inputChannels = null;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var codec = outputAudioCodec ?? string.Empty;
 | 
			
		||||
 | 
			
		||||
            int? transcoderChannelLimit;
 | 
			
		||||
            if (codec.IndexOf("wma", StringComparison.OrdinalIgnoreCase) != -1)
 | 
			
		||||
            int? resultChannels = state.GetRequestedAudioChannels(codec);
 | 
			
		||||
 | 
			
		||||
            var inputChannels = audioStream.Channels;
 | 
			
		||||
 | 
			
		||||
            if (inputChannels > 0)
 | 
			
		||||
            {
 | 
			
		||||
                // wmav2 currently only supports two channel output
 | 
			
		||||
                transcoderChannelLimit = 2;
 | 
			
		||||
            }
 | 
			
		||||
            else if (codec.IndexOf("mp3", StringComparison.OrdinalIgnoreCase) != -1)
 | 
			
		||||
            {
 | 
			
		||||
                // libmp3lame currently only supports two channel output
 | 
			
		||||
                transcoderChannelLimit = 2;
 | 
			
		||||
            }
 | 
			
		||||
            else if (codec.IndexOf("aac", StringComparison.OrdinalIgnoreCase) != -1)
 | 
			
		||||
            {
 | 
			
		||||
                // aac is able to handle 8ch(7.1 layout)
 | 
			
		||||
                transcoderChannelLimit = 8;
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                // If we don't have any media info then limit it to 6 to prevent encoding errors due to asking for too many channels
 | 
			
		||||
                transcoderChannelLimit = 6;
 | 
			
		||||
                resultChannels = inputChannels < resultChannels ? inputChannels : resultChannels ?? inputChannels;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var isTranscodingAudio = !IsCopyCodec(codec);
 | 
			
		||||
 | 
			
		||||
            int? resultChannels = state.GetRequestedAudioChannels(codec);
 | 
			
		||||
            if (isTranscodingAudio)
 | 
			
		||||
            {
 | 
			
		||||
                resultChannels = GetMinValue(request.TranscodingMaxAudioChannels, resultChannels);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (inputChannels.HasValue)
 | 
			
		||||
                // Set max transcoding channels for encoders that can't handle more than a set amount of channels
 | 
			
		||||
                // AAC, FLAC, ALAC, libopus, libvorbis encoders all support at least 8 channels
 | 
			
		||||
                int transcoderChannelLimit = GetAudioEncoder(state) switch
 | 
			
		||||
                {
 | 
			
		||||
                resultChannels = resultChannels.HasValue
 | 
			
		||||
                    ? Math.Min(resultChannels.Value, inputChannels.Value)
 | 
			
		||||
                    : inputChannels.Value;
 | 
			
		||||
            }
 | 
			
		||||
                    string audioEncoder when audioEncoder.Equals("wmav2", StringComparison.OrdinalIgnoreCase)
 | 
			
		||||
                                          || audioEncoder.Equals("libmp3lame", StringComparison.OrdinalIgnoreCase) => 2,
 | 
			
		||||
                    string audioEncoder when audioEncoder.Equals("libfdk_aac", StringComparison.OrdinalIgnoreCase)
 | 
			
		||||
                                          || audioEncoder.Equals("aac_at", StringComparison.OrdinalIgnoreCase)
 | 
			
		||||
                                          || audioEncoder.Equals("ac3", StringComparison.OrdinalIgnoreCase)
 | 
			
		||||
                                          || audioEncoder.Equals("eac3", StringComparison.OrdinalIgnoreCase)
 | 
			
		||||
                                          || audioEncoder.Equals("dts", StringComparison.OrdinalIgnoreCase)
 | 
			
		||||
                                          || audioEncoder.Equals("mlp", StringComparison.OrdinalIgnoreCase)
 | 
			
		||||
                                          || audioEncoder.Equals("truehd", StringComparison.OrdinalIgnoreCase) => 6,
 | 
			
		||||
                    // Set default max transcoding channels to 8 to prevent encoding errors due to asking for too many channels
 | 
			
		||||
                    _ => 8,
 | 
			
		||||
                };
 | 
			
		||||
 | 
			
		||||
            if (isTranscodingAudio && transcoderChannelLimit.HasValue)
 | 
			
		||||
                // Set resultChannels to minimum between resultChannels, TranscodingMaxAudioChannels, transcoderChannelLimit
 | 
			
		||||
 | 
			
		||||
                resultChannels = transcoderChannelLimit < resultChannels ? transcoderChannelLimit : resultChannels ?? transcoderChannelLimit;
 | 
			
		||||
 | 
			
		||||
                if (request.TranscodingMaxAudioChannels < resultChannels)
 | 
			
		||||
                {
 | 
			
		||||
                resultChannels = resultChannels.HasValue
 | 
			
		||||
                    ? Math.Min(resultChannels.Value, transcoderChannelLimit.Value)
 | 
			
		||||
                    : transcoderChannelLimit.Value;
 | 
			
		||||
                    resultChannels = request.TranscodingMaxAudioChannels;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // Avoid transcoding to audio channels other than 1ch, 2ch, 6ch (5.1 layout) and 8ch (7.1 layout).
 | 
			
		||||
                // https://developer.apple.com/documentation/http_live_streaming/hls_authoring_specification_for_apple_devices
 | 
			
		||||
            if (isTranscodingAudio
 | 
			
		||||
                && state.TranscodingType != TranscodingJobType.Progressive
 | 
			
		||||
                && resultChannels.HasValue
 | 
			
		||||
                && ((resultChannels.Value > 2 && resultChannels.Value < 6) || resultChannels.Value == 7))
 | 
			
		||||
                if (state.TranscodingType != TranscodingJobType.Progressive
 | 
			
		||||
                    && ((resultChannels > 2 && resultChannels < 6) || resultChannels == 7))
 | 
			
		||||
                {
 | 
			
		||||
                    resultChannels = 2;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return resultChannels;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private int? GetMinValue(int? val1, int? val2)
 | 
			
		||||
        {
 | 
			
		||||
            if (!val1.HasValue)
 | 
			
		||||
            {
 | 
			
		||||
                return val2;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (!val2.HasValue)
 | 
			
		||||
            {
 | 
			
		||||
                return val1;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return Math.Min(val1.Value, val2.Value);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Enforces the resolution limit.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
 | 
			
		||||
@ -250,8 +250,7 @@ namespace MediaBrowser.Controller.MediaEncoding
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                var level = GetRequestedLevel(ActualOutputVideoCodec);
 | 
			
		||||
                if (!string.IsNullOrEmpty(level)
 | 
			
		||||
                    && double.TryParse(level, NumberStyles.Any, CultureInfo.InvariantCulture, out var result))
 | 
			
		||||
                if (double.TryParse(level, CultureInfo.InvariantCulture, out var result))
 | 
			
		||||
                {
 | 
			
		||||
                    return result;
 | 
			
		||||
                }
 | 
			
		||||
@ -645,8 +644,7 @@ namespace MediaBrowser.Controller.MediaEncoding
 | 
			
		||||
            if (!string.IsNullOrEmpty(codec))
 | 
			
		||||
            {
 | 
			
		||||
                var value = BaseRequest.GetOption(codec, "maxrefframes");
 | 
			
		||||
                if (!string.IsNullOrEmpty(value)
 | 
			
		||||
                    && int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var result))
 | 
			
		||||
                if (int.TryParse(value, CultureInfo.InvariantCulture, out var result))
 | 
			
		||||
                {
 | 
			
		||||
                    return result;
 | 
			
		||||
                }
 | 
			
		||||
@ -665,8 +663,7 @@ namespace MediaBrowser.Controller.MediaEncoding
 | 
			
		||||
            if (!string.IsNullOrEmpty(codec))
 | 
			
		||||
            {
 | 
			
		||||
                var value = BaseRequest.GetOption(codec, "videobitdepth");
 | 
			
		||||
                if (!string.IsNullOrEmpty(value)
 | 
			
		||||
                    && int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var result))
 | 
			
		||||
                if (int.TryParse(value, CultureInfo.InvariantCulture, out var result))
 | 
			
		||||
                {
 | 
			
		||||
                    return result;
 | 
			
		||||
                }
 | 
			
		||||
@ -685,8 +682,7 @@ namespace MediaBrowser.Controller.MediaEncoding
 | 
			
		||||
            if (!string.IsNullOrEmpty(codec))
 | 
			
		||||
            {
 | 
			
		||||
                var value = BaseRequest.GetOption(codec, "audiobitdepth");
 | 
			
		||||
                if (!string.IsNullOrEmpty(value)
 | 
			
		||||
                    && int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var result))
 | 
			
		||||
                if (int.TryParse(value, CultureInfo.InvariantCulture, out var result))
 | 
			
		||||
                {
 | 
			
		||||
                    return result;
 | 
			
		||||
                }
 | 
			
		||||
@ -700,8 +696,7 @@ namespace MediaBrowser.Controller.MediaEncoding
 | 
			
		||||
            if (!string.IsNullOrEmpty(codec))
 | 
			
		||||
            {
 | 
			
		||||
                var value = BaseRequest.GetOption(codec, "audiochannels");
 | 
			
		||||
                if (!string.IsNullOrEmpty(value)
 | 
			
		||||
                    && int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var result))
 | 
			
		||||
                if (int.TryParse(value, CultureInfo.InvariantCulture, out var result))
 | 
			
		||||
                {
 | 
			
		||||
                    return result;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
@ -86,7 +86,7 @@ namespace MediaBrowser.Controller.MediaEncoding
 | 
			
		||||
                {
 | 
			
		||||
                    var rate = parts[i + 1];
 | 
			
		||||
 | 
			
		||||
                    if (float.TryParse(rate, NumberStyles.Any, CultureInfo.InvariantCulture, out var val))
 | 
			
		||||
                    if (float.TryParse(rate, CultureInfo.InvariantCulture, out var val))
 | 
			
		||||
                    {
 | 
			
		||||
                        framerate = val;
 | 
			
		||||
                    }
 | 
			
		||||
@ -95,7 +95,7 @@ namespace MediaBrowser.Controller.MediaEncoding
 | 
			
		||||
                {
 | 
			
		||||
                    var rate = part.Split('=', 2)[^1];
 | 
			
		||||
 | 
			
		||||
                    if (float.TryParse(rate, NumberStyles.Any, CultureInfo.InvariantCulture, out var val))
 | 
			
		||||
                    if (float.TryParse(rate, CultureInfo.InvariantCulture, out var val))
 | 
			
		||||
                    {
 | 
			
		||||
                        framerate = val;
 | 
			
		||||
                    }
 | 
			
		||||
@ -127,7 +127,7 @@ namespace MediaBrowser.Controller.MediaEncoding
 | 
			
		||||
 | 
			
		||||
                    if (scale.HasValue)
 | 
			
		||||
                    {
 | 
			
		||||
                        if (long.TryParse(size, NumberStyles.Any, CultureInfo.InvariantCulture, out var val))
 | 
			
		||||
                        if (long.TryParse(size, CultureInfo.InvariantCulture, out var val))
 | 
			
		||||
                        {
 | 
			
		||||
                            bytesTranscoded = val * scale.Value;
 | 
			
		||||
                        }
 | 
			
		||||
@ -146,7 +146,7 @@ namespace MediaBrowser.Controller.MediaEncoding
 | 
			
		||||
 | 
			
		||||
                    if (scale.HasValue)
 | 
			
		||||
                    {
 | 
			
		||||
                        if (float.TryParse(rate, NumberStyles.Any, CultureInfo.InvariantCulture, out var val))
 | 
			
		||||
                        if (float.TryParse(rate, CultureInfo.InvariantCulture, out var val))
 | 
			
		||||
                        {
 | 
			
		||||
                            bitRate = (int)Math.Ceiling(val * scale.Value);
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,3 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
 | 
			
		||||
#pragma warning disable CS1591
 | 
			
		||||
 | 
			
		||||
using System;
 | 
			
		||||
 | 
			
		||||
@ -169,13 +169,10 @@ namespace MediaBrowser.LocalMetadata.Parsers
 | 
			
		||||
                {
 | 
			
		||||
                    var text = reader.ReadElementContentAsString();
 | 
			
		||||
 | 
			
		||||
                    if (!string.IsNullOrEmpty(text))
 | 
			
		||||
                    {
 | 
			
		||||
                        if (float.TryParse(text, NumberStyles.Any, CultureInfo.InvariantCulture, out var value))
 | 
			
		||||
                    if (float.TryParse(text, CultureInfo.InvariantCulture, out var value))
 | 
			
		||||
                    {
 | 
			
		||||
                        item.CriticRating = value;
 | 
			
		||||
                    }
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    break;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
@ -277,7 +277,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
 | 
			
		||||
 | 
			
		||||
            if (match.Success)
 | 
			
		||||
            {
 | 
			
		||||
                if (Version.TryParse(match.Groups[1].Value, out var result))
 | 
			
		||||
                if (Version.TryParse(match.Groups[1].ValueSpan, out var result))
 | 
			
		||||
                {
 | 
			
		||||
                    return result;
 | 
			
		||||
                }
 | 
			
		||||
@ -327,8 +327,8 @@ namespace MediaBrowser.MediaEncoding.Encoder
 | 
			
		||||
                RegexOptions.Multiline))
 | 
			
		||||
            {
 | 
			
		||||
                var version = new Version(
 | 
			
		||||
                    int.Parse(match.Groups["major"].Value, CultureInfo.InvariantCulture),
 | 
			
		||||
                    int.Parse(match.Groups["minor"].Value, CultureInfo.InvariantCulture));
 | 
			
		||||
                    int.Parse(match.Groups["major"].ValueSpan, CultureInfo.InvariantCulture),
 | 
			
		||||
                    int.Parse(match.Groups["minor"].ValueSpan, CultureInfo.InvariantCulture));
 | 
			
		||||
 | 
			
		||||
                map.Add(match.Groups["name"].Value, version);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
@ -97,14 +97,11 @@ namespace MediaBrowser.MediaEncoding.Probing
 | 
			
		||||
            {
 | 
			
		||||
                info.Container = NormalizeFormat(data.Format.FormatName);
 | 
			
		||||
 | 
			
		||||
                if (!string.IsNullOrEmpty(data.Format.BitRate))
 | 
			
		||||
                {
 | 
			
		||||
                    if (int.TryParse(data.Format.BitRate, NumberStyles.Any, CultureInfo.InvariantCulture, out var value))
 | 
			
		||||
                if (int.TryParse(data.Format.BitRate, CultureInfo.InvariantCulture, out var value))
 | 
			
		||||
                {
 | 
			
		||||
                    info.Bitrate = value;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var tags = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
 | 
			
		||||
            var tagStreamType = isAudio ? CodecType.Audio : CodecType.Video;
 | 
			
		||||
@ -561,8 +558,8 @@ namespace MediaBrowser.MediaEncoding.Probing
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (string.IsNullOrWhiteSpace(name) ||
 | 
			
		||||
                string.IsNullOrWhiteSpace(value))
 | 
			
		||||
            if (string.IsNullOrWhiteSpace(name)
 | 
			
		||||
                || string.IsNullOrWhiteSpace(value))
 | 
			
		||||
            {
 | 
			
		||||
                return null;
 | 
			
		||||
            }
 | 
			
		||||
@ -674,9 +671,9 @@ namespace MediaBrowser.MediaEncoding.Probing
 | 
			
		||||
 | 
			
		||||
                stream.Channels = streamInfo.Channels;
 | 
			
		||||
 | 
			
		||||
                if (int.TryParse(streamInfo.SampleRate, NumberStyles.Any, CultureInfo.InvariantCulture, out var value))
 | 
			
		||||
                if (int.TryParse(streamInfo.SampleRate, CultureInfo.InvariantCulture, out var sampleRate))
 | 
			
		||||
                {
 | 
			
		||||
                    stream.SampleRate = value;
 | 
			
		||||
                    stream.SampleRate = sampleRate;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                stream.ChannelLayout = ParseChannelLayout(streamInfo.ChannelLayout);
 | 
			
		||||
@ -853,22 +850,18 @@ namespace MediaBrowser.MediaEncoding.Probing
 | 
			
		||||
            // Get stream bitrate
 | 
			
		||||
            var bitrate = 0;
 | 
			
		||||
 | 
			
		||||
            if (!string.IsNullOrEmpty(streamInfo.BitRate))
 | 
			
		||||
            {
 | 
			
		||||
                if (int.TryParse(streamInfo.BitRate, NumberStyles.Any, CultureInfo.InvariantCulture, out var value))
 | 
			
		||||
            if (int.TryParse(streamInfo.BitRate, CultureInfo.InvariantCulture, out var value))
 | 
			
		||||
            {
 | 
			
		||||
                bitrate = value;
 | 
			
		||||
            }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // The bitrate info of FLAC musics and some videos is included in formatInfo.
 | 
			
		||||
            if (bitrate == 0
 | 
			
		||||
                && formatInfo is not null
 | 
			
		||||
                && !string.IsNullOrEmpty(formatInfo.BitRate)
 | 
			
		||||
                && (stream.Type == MediaStreamType.Video || (isAudio && stream.Type == MediaStreamType.Audio)))
 | 
			
		||||
            {
 | 
			
		||||
                // If the stream info doesn't have a bitrate get the value from the media format info
 | 
			
		||||
                if (int.TryParse(formatInfo.BitRate, NumberStyles.Any, CultureInfo.InvariantCulture, out var value))
 | 
			
		||||
                if (int.TryParse(formatInfo.BitRate, CultureInfo.InvariantCulture, out value))
 | 
			
		||||
                {
 | 
			
		||||
                    bitrate = value;
 | 
			
		||||
                }
 | 
			
		||||
@ -972,8 +965,8 @@ namespace MediaBrowser.MediaEncoding.Probing
 | 
			
		||||
 | 
			
		||||
            var parts = (original ?? string.Empty).Split(':');
 | 
			
		||||
            if (!(parts.Length == 2
 | 
			
		||||
                    && int.TryParse(parts[0], NumberStyles.Any, CultureInfo.InvariantCulture, out var width)
 | 
			
		||||
                    && int.TryParse(parts[1], NumberStyles.Any, CultureInfo.InvariantCulture, out var height)
 | 
			
		||||
                    && int.TryParse(parts[0], CultureInfo.InvariantCulture, out var width)
 | 
			
		||||
                    && int.TryParse(parts[1], CultureInfo.InvariantCulture, out var height)
 | 
			
		||||
                    && width > 0
 | 
			
		||||
                    && height > 0))
 | 
			
		||||
            {
 | 
			
		||||
@ -1117,7 +1110,7 @@ namespace MediaBrowser.MediaEncoding.Probing
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var duration = GetDictionaryValue(streamInfo.Tags, "DURATION-eng") ?? GetDictionaryValue(streamInfo.Tags, "DURATION");
 | 
			
		||||
            if (!string.IsNullOrEmpty(duration) && TimeSpan.TryParse(duration, out var parsedDuration))
 | 
			
		||||
            if (TimeSpan.TryParse(duration, out var parsedDuration))
 | 
			
		||||
            {
 | 
			
		||||
                return parsedDuration.TotalSeconds;
 | 
			
		||||
            }
 | 
			
		||||
@ -1446,7 +1439,7 @@ namespace MediaBrowser.MediaEncoding.Probing
 | 
			
		||||
            // Limit accuracy to milliseconds to match xml saving
 | 
			
		||||
            var secondsString = chapter.StartTime;
 | 
			
		||||
 | 
			
		||||
            if (double.TryParse(secondsString, NumberStyles.Any, CultureInfo.InvariantCulture, out var seconds))
 | 
			
		||||
            if (double.TryParse(secondsString, CultureInfo.InvariantCulture, out var seconds))
 | 
			
		||||
            {
 | 
			
		||||
                var ms = Math.Round(TimeSpan.FromSeconds(seconds).TotalMilliseconds);
 | 
			
		||||
                info.StartPositionTicks = TimeSpan.FromMilliseconds(ms).Ticks;
 | 
			
		||||
 | 
			
		||||
@ -136,7 +136,7 @@ namespace MediaBrowser.Model.Dlna
 | 
			
		||||
                return !condition.IsRequired;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (int.TryParse(condition.Value, NumberStyles.Any, CultureInfo.InvariantCulture, out var expected))
 | 
			
		||||
            if (int.TryParse(condition.Value, CultureInfo.InvariantCulture, out var expected))
 | 
			
		||||
            {
 | 
			
		||||
                switch (condition.Condition)
 | 
			
		||||
                {
 | 
			
		||||
@ -212,7 +212,7 @@ namespace MediaBrowser.Model.Dlna
 | 
			
		||||
                return !condition.IsRequired;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (double.TryParse(condition.Value, NumberStyles.Any, CultureInfo.InvariantCulture, out var expected))
 | 
			
		||||
            if (double.TryParse(condition.Value, CultureInfo.InvariantCulture, out var expected))
 | 
			
		||||
            {
 | 
			
		||||
                switch (condition.Condition)
 | 
			
		||||
                {
 | 
			
		||||
 | 
			
		||||
@ -9,7 +9,7 @@ namespace MediaBrowser.Model.Dlna
 | 
			
		||||
    {
 | 
			
		||||
        public SortCriteria(string sortOrder)
 | 
			
		||||
        {
 | 
			
		||||
            if (!string.IsNullOrEmpty(sortOrder) && Enum.TryParse<SortOrder>(sortOrder, true, out var sortOrderValue))
 | 
			
		||||
            if (Enum.TryParse<SortOrder>(sortOrder, true, out var sortOrderValue))
 | 
			
		||||
            {
 | 
			
		||||
                SortOrder = sortOrderValue;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
@ -551,8 +551,7 @@ namespace MediaBrowser.Model.Dlna
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            playlistItem.TranscodeSeekInfo = transcodingProfile.TranscodeSeekInfo;
 | 
			
		||||
            if (!string.IsNullOrEmpty(transcodingProfile.MaxAudioChannels)
 | 
			
		||||
                && int.TryParse(transcodingProfile.MaxAudioChannels, NumberStyles.Any, CultureInfo.InvariantCulture, out int transcodingMaxAudioChannels))
 | 
			
		||||
            if (int.TryParse(transcodingProfile.MaxAudioChannels, CultureInfo.InvariantCulture, out int transcodingMaxAudioChannels))
 | 
			
		||||
            {
 | 
			
		||||
                playlistItem.TranscodingMaxAudioChannels = transcodingMaxAudioChannels;
 | 
			
		||||
            }
 | 
			
		||||
@ -1607,7 +1606,7 @@ namespace MediaBrowser.Model.Dlna
 | 
			
		||||
                                continue;
 | 
			
		||||
                            }
 | 
			
		||||
 | 
			
		||||
                            if (int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var num))
 | 
			
		||||
                            if (int.TryParse(value, CultureInfo.InvariantCulture, out var num))
 | 
			
		||||
                            {
 | 
			
		||||
                                if (condition.Condition == ProfileConditionType.Equals)
 | 
			
		||||
                                {
 | 
			
		||||
@ -1633,7 +1632,7 @@ namespace MediaBrowser.Model.Dlna
 | 
			
		||||
                                continue;
 | 
			
		||||
                            }
 | 
			
		||||
 | 
			
		||||
                            if (int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var num))
 | 
			
		||||
                            if (int.TryParse(value, CultureInfo.InvariantCulture, out var num))
 | 
			
		||||
                            {
 | 
			
		||||
                                if (condition.Condition == ProfileConditionType.Equals)
 | 
			
		||||
                                {
 | 
			
		||||
@ -1669,7 +1668,7 @@ namespace MediaBrowser.Model.Dlna
 | 
			
		||||
                                }
 | 
			
		||||
                            }
 | 
			
		||||
 | 
			
		||||
                            if (int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var num))
 | 
			
		||||
                            if (int.TryParse(value, CultureInfo.InvariantCulture, out var num))
 | 
			
		||||
                            {
 | 
			
		||||
                                if (condition.Condition == ProfileConditionType.Equals)
 | 
			
		||||
                                {
 | 
			
		||||
@ -1793,7 +1792,7 @@ namespace MediaBrowser.Model.Dlna
 | 
			
		||||
                                }
 | 
			
		||||
                            }
 | 
			
		||||
 | 
			
		||||
                            if (int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var num))
 | 
			
		||||
                            if (int.TryParse(value, CultureInfo.InvariantCulture, out var num))
 | 
			
		||||
                            {
 | 
			
		||||
                                if (condition.Condition == ProfileConditionType.Equals)
 | 
			
		||||
                                {
 | 
			
		||||
@ -1829,7 +1828,7 @@ namespace MediaBrowser.Model.Dlna
 | 
			
		||||
                                }
 | 
			
		||||
                            }
 | 
			
		||||
 | 
			
		||||
                            if (int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var num))
 | 
			
		||||
                            if (int.TryParse(value, CultureInfo.InvariantCulture, out var num))
 | 
			
		||||
                            {
 | 
			
		||||
                                if (condition.Condition == ProfileConditionType.Equals)
 | 
			
		||||
                                {
 | 
			
		||||
@ -1919,7 +1918,7 @@ namespace MediaBrowser.Model.Dlna
 | 
			
		||||
                                continue;
 | 
			
		||||
                            }
 | 
			
		||||
 | 
			
		||||
                            if (int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var num))
 | 
			
		||||
                            if (int.TryParse(value, CultureInfo.InvariantCulture, out var num))
 | 
			
		||||
                            {
 | 
			
		||||
                                if (condition.Condition == ProfileConditionType.Equals)
 | 
			
		||||
                                {
 | 
			
		||||
@ -1945,7 +1944,7 @@ namespace MediaBrowser.Model.Dlna
 | 
			
		||||
                                continue;
 | 
			
		||||
                            }
 | 
			
		||||
 | 
			
		||||
                            if (int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var num))
 | 
			
		||||
                            if (int.TryParse(value, CultureInfo.InvariantCulture, out var num))
 | 
			
		||||
                            {
 | 
			
		||||
                                if (condition.Condition == ProfileConditionType.Equals)
 | 
			
		||||
                                {
 | 
			
		||||
@ -1971,7 +1970,7 @@ namespace MediaBrowser.Model.Dlna
 | 
			
		||||
                                continue;
 | 
			
		||||
                            }
 | 
			
		||||
 | 
			
		||||
                            if (float.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var num))
 | 
			
		||||
                            if (float.TryParse(value, CultureInfo.InvariantCulture, out var num))
 | 
			
		||||
                            {
 | 
			
		||||
                                if (condition.Condition == ProfileConditionType.Equals)
 | 
			
		||||
                                {
 | 
			
		||||
@ -1997,7 +1996,7 @@ namespace MediaBrowser.Model.Dlna
 | 
			
		||||
                                continue;
 | 
			
		||||
                            }
 | 
			
		||||
 | 
			
		||||
                            if (int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var num))
 | 
			
		||||
                            if (int.TryParse(value, CultureInfo.InvariantCulture, out var num))
 | 
			
		||||
                            {
 | 
			
		||||
                                if (condition.Condition == ProfileConditionType.Equals)
 | 
			
		||||
                                {
 | 
			
		||||
@ -2023,7 +2022,7 @@ namespace MediaBrowser.Model.Dlna
 | 
			
		||||
                                continue;
 | 
			
		||||
                            }
 | 
			
		||||
 | 
			
		||||
                            if (int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var num))
 | 
			
		||||
                            if (int.TryParse(value, CultureInfo.InvariantCulture, out var num))
 | 
			
		||||
                            {
 | 
			
		||||
                                if (condition.Condition == ProfileConditionType.Equals)
 | 
			
		||||
                                {
 | 
			
		||||
 | 
			
		||||
@ -922,12 +922,8 @@ namespace MediaBrowser.Model.Dlna
 | 
			
		||||
        public int? GetTargetVideoBitDepth(string codec)
 | 
			
		||||
        {
 | 
			
		||||
            var value = GetOption(codec, "videobitdepth");
 | 
			
		||||
            if (string.IsNullOrEmpty(value))
 | 
			
		||||
            {
 | 
			
		||||
                return null;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result))
 | 
			
		||||
            if (int.TryParse(value, CultureInfo.InvariantCulture, out var result))
 | 
			
		||||
            {
 | 
			
		||||
                return result;
 | 
			
		||||
            }
 | 
			
		||||
@ -938,12 +934,8 @@ namespace MediaBrowser.Model.Dlna
 | 
			
		||||
        public int? GetTargetAudioBitDepth(string codec)
 | 
			
		||||
        {
 | 
			
		||||
            var value = GetOption(codec, "audiobitdepth");
 | 
			
		||||
            if (string.IsNullOrEmpty(value))
 | 
			
		||||
            {
 | 
			
		||||
                return null;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result))
 | 
			
		||||
            if (int.TryParse(value, CultureInfo.InvariantCulture, out var result))
 | 
			
		||||
            {
 | 
			
		||||
                return result;
 | 
			
		||||
            }
 | 
			
		||||
@ -954,12 +946,8 @@ namespace MediaBrowser.Model.Dlna
 | 
			
		||||
        public double? GetTargetVideoLevel(string codec)
 | 
			
		||||
        {
 | 
			
		||||
            var value = GetOption(codec, "level");
 | 
			
		||||
            if (string.IsNullOrEmpty(value))
 | 
			
		||||
            {
 | 
			
		||||
                return null;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (double.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var result))
 | 
			
		||||
            if (double.TryParse(value, CultureInfo.InvariantCulture, out var result))
 | 
			
		||||
            {
 | 
			
		||||
                return result;
 | 
			
		||||
            }
 | 
			
		||||
@ -970,12 +958,8 @@ namespace MediaBrowser.Model.Dlna
 | 
			
		||||
        public int? GetTargetRefFrames(string codec)
 | 
			
		||||
        {
 | 
			
		||||
            var value = GetOption(codec, "maxrefframes");
 | 
			
		||||
            if (string.IsNullOrEmpty(value))
 | 
			
		||||
            {
 | 
			
		||||
                return null;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var result))
 | 
			
		||||
            if (int.TryParse(value, CultureInfo.InvariantCulture, out var result))
 | 
			
		||||
            {
 | 
			
		||||
                return result;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
@ -117,7 +117,9 @@ namespace MediaBrowser.Model.Net
 | 
			
		||||
 | 
			
		||||
            // Type image
 | 
			
		||||
            { "image/jpeg", ".jpg" },
 | 
			
		||||
            { "image/tiff", ".tiff" },
 | 
			
		||||
            { "image/x-png", ".png" },
 | 
			
		||||
            { "image/x-icon", ".ico" },
 | 
			
		||||
 | 
			
		||||
            // Type text
 | 
			
		||||
            { "text/plain", ".txt" },
 | 
			
		||||
@ -178,5 +180,8 @@ namespace MediaBrowser.Model.Net
 | 
			
		||||
            var extension = Model.MimeTypes.GetMimeTypeExtensions(mimeType).FirstOrDefault();
 | 
			
		||||
            return string.IsNullOrEmpty(extension) ? null : "." + extension;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public static bool IsImage(ReadOnlySpan<char> mimeType)
 | 
			
		||||
            => mimeType.StartsWith("image/", StringComparison.OrdinalIgnoreCase);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -9,9 +9,9 @@ namespace MediaBrowser.Model.Tasks
 | 
			
		||||
{
 | 
			
		||||
    public interface ITaskManager : IDisposable
 | 
			
		||||
    {
 | 
			
		||||
        event EventHandler<GenericEventArgs<IScheduledTaskWorker>> TaskExecuting;
 | 
			
		||||
        event EventHandler<GenericEventArgs<IScheduledTaskWorker>>? TaskExecuting;
 | 
			
		||||
 | 
			
		||||
        event EventHandler<TaskCompletionEventArgs> TaskCompleted;
 | 
			
		||||
        event EventHandler<TaskCompletionEventArgs>? TaskCompleted;
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets the list of Scheduled Tasks.
 | 
			
		||||
 | 
			
		||||
@ -151,7 +151,6 @@ namespace MediaBrowser.Providers.Manager
 | 
			
		||||
                        ApplySearchResult(id, refreshOptions.SearchResult);
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    // await FindIdentities(id, cancellationToken).ConfigureAwait(false);
 | 
			
		||||
                    id.IsAutomated = refreshOptions.IsAutomated;
 | 
			
		||||
 | 
			
		||||
                    var result = await RefreshWithProviders(metadataResult, id, refreshOptions, providers, ImageProvider, cancellationToken).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,4 @@
 | 
			
		||||
using MediaBrowser.Model.Plugins;
 | 
			
		||||
using MetaBrainz.MusicBrainz;
 | 
			
		||||
 | 
			
		||||
namespace MediaBrowser.Providers.Plugins.MusicBrainz.Configuration;
 | 
			
		||||
 | 
			
		||||
@ -8,7 +7,7 @@ namespace MediaBrowser.Providers.Plugins.MusicBrainz.Configuration;
 | 
			
		||||
/// </summary>
 | 
			
		||||
public class PluginConfiguration : BasePluginConfiguration
 | 
			
		||||
{
 | 
			
		||||
    private const string DefaultServer = "musicbrainz.org";
 | 
			
		||||
    private const string DefaultServer = "https://musicbrainz.org";
 | 
			
		||||
 | 
			
		||||
    private const double DefaultRateLimit = 1.0;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,20 +1,16 @@
 | 
			
		||||
<!DOCTYPE html>
 | 
			
		||||
<html>
 | 
			
		||||
<head>
 | 
			
		||||
    <title>MusicBrainz</title>
 | 
			
		||||
</head>
 | 
			
		||||
<body>
 | 
			
		||||
    <div data-role="page" class="page type-interior pluginConfigurationPage musicBrainzConfigPage" data-require="emby-input,emby-button,emby-checkbox">
 | 
			
		||||
<div id="musicBrainzConfigurationPage" data-role="page"
 | 
			
		||||
    class="page type-interior pluginConfigurationPage musicBrainzConfigurationPage" data-require="emby-input,emby-button,emby-checkbox">
 | 
			
		||||
    <div data-role="content">
 | 
			
		||||
        <div class="content-primary">
 | 
			
		||||
                <form class="musicBrainzConfigForm">
 | 
			
		||||
            <h1>MusicBrainz</h1>
 | 
			
		||||
                <form class="musicBrainzConfigurationForm">
 | 
			
		||||
                    <div class="inputContainer">
 | 
			
		||||
                        <input is="emby-input" type="text" id="server" required label="Server" />
 | 
			
		||||
                        <div class="fieldDescription">This can be a mirror of the official server or even a custom server.</div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div class="inputContainer">
 | 
			
		||||
                        <input is="emby-input" type="number" id="rateLimit" pattern="[0-9]*" required min="0" max="10000" label="Rate Limit" />
 | 
			
		||||
                        <div class="fieldDescription">Span of time between requests in milliseconds. The official server is limited to one request every two seconds.</div>
 | 
			
		||||
                        <input is="emby-input" type="number" id="rateLimit" required pattern="[0-9]*" min="0" max="10" step=".01" label="Rate Limit" />
 | 
			
		||||
                        <div class="fieldDescription">Span of time between requests in seconds. The official server is limited to one request every seconds.</div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <label class="checkboxContainer">
 | 
			
		||||
                        <input is="emby-checkbox" type="checkbox" id="replaceArtistName" />
 | 
			
		||||
@ -32,7 +28,7 @@
 | 
			
		||||
                uniquePluginId: "8c95c4d2-e50c-4fb0-a4f3-6c06ff0f9a1a"
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            document.querySelector('.musicBrainzConfigPage')
 | 
			
		||||
            document.querySelector('.musicBrainzConfigurationPage')
 | 
			
		||||
                .addEventListener('pageshow', function () {
 | 
			
		||||
                    Dashboard.showLoadingMsg();
 | 
			
		||||
                    ApiClient.getPluginConfiguration(MusicBrainzPluginConfig.uniquePluginId).then(function (config) {
 | 
			
		||||
@ -56,7 +52,7 @@
 | 
			
		||||
                    });
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            document.querySelector('.musicBrainzConfigForm')
 | 
			
		||||
            document.querySelector('.musicBrainzConfigurationForm')
 | 
			
		||||
                .addEventListener('submit', function (e) {
 | 
			
		||||
                    Dashboard.showLoadingMsg();
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -8,8 +8,10 @@ using Jellyfin.Extensions;
 | 
			
		||||
using MediaBrowser.Controller.Entities.Audio;
 | 
			
		||||
using MediaBrowser.Controller.Providers;
 | 
			
		||||
using MediaBrowser.Model.Entities;
 | 
			
		||||
using MediaBrowser.Model.Plugins;
 | 
			
		||||
using MediaBrowser.Model.Providers;
 | 
			
		||||
using MediaBrowser.Providers.Music;
 | 
			
		||||
using MediaBrowser.Providers.Plugins.MusicBrainz.Configuration;
 | 
			
		||||
using MetaBrainz.MusicBrainz;
 | 
			
		||||
using MetaBrainz.MusicBrainz.Interfaces.Entities;
 | 
			
		||||
using MetaBrainz.MusicBrainz.Interfaces.Searches;
 | 
			
		||||
@ -23,8 +25,7 @@ namespace MediaBrowser.Providers.Plugins.MusicBrainz;
 | 
			
		||||
public class MusicBrainzAlbumProvider : IRemoteMetadataProvider<MusicAlbum, AlbumInfo>, IHasOrder, IDisposable
 | 
			
		||||
{
 | 
			
		||||
    private readonly ILogger<MusicBrainzAlbumProvider> _logger;
 | 
			
		||||
    private readonly Query _musicBrainzQuery;
 | 
			
		||||
    private readonly string _musicBrainzDefaultUri = "https://musicbrainz.org";
 | 
			
		||||
    private Query _musicBrainzQuery;
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Initializes a new instance of the <see cref="MusicBrainzAlbumProvider"/> class.
 | 
			
		||||
@ -33,29 +34,9 @@ public class MusicBrainzAlbumProvider : IRemoteMetadataProvider<MusicAlbum, Albu
 | 
			
		||||
    public MusicBrainzAlbumProvider(ILogger<MusicBrainzAlbumProvider> logger)
 | 
			
		||||
    {
 | 
			
		||||
        _logger = logger;
 | 
			
		||||
 | 
			
		||||
        MusicBrainz.Plugin.Instance!.ConfigurationChanged += (_, _) =>
 | 
			
		||||
            {
 | 
			
		||||
                if (Uri.TryCreate(MusicBrainz.Plugin.Instance.Configuration.Server, UriKind.Absolute, out var server))
 | 
			
		||||
                {
 | 
			
		||||
                    Query.DefaultServer = server.Host;
 | 
			
		||||
                    Query.DefaultPort = server.Port;
 | 
			
		||||
                    Query.DefaultUrlScheme = server.Scheme;
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    // Fallback to official server
 | 
			
		||||
                    _logger.LogWarning("Invalid MusicBrainz server specified, falling back to official server");
 | 
			
		||||
                    var defaultServer = new Uri(_musicBrainzDefaultUri);
 | 
			
		||||
                    Query.DefaultServer = defaultServer.Host;
 | 
			
		||||
                    Query.DefaultPort = defaultServer.Port;
 | 
			
		||||
                    Query.DefaultUrlScheme = defaultServer.Scheme;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                Query.DelayBetweenRequests = MusicBrainz.Plugin.Instance.Configuration.RateLimit;
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
        _musicBrainzQuery = new Query();
 | 
			
		||||
        ReloadConfig(null, MusicBrainz.Plugin.Instance!.Configuration);
 | 
			
		||||
        MusicBrainz.Plugin.Instance!.ConfigurationChanged += ReloadConfig;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc />
 | 
			
		||||
@ -64,6 +45,29 @@ public class MusicBrainzAlbumProvider : IRemoteMetadataProvider<MusicAlbum, Albu
 | 
			
		||||
    /// <inheritdoc />
 | 
			
		||||
    public int Order => 0;
 | 
			
		||||
 | 
			
		||||
    private void ReloadConfig(object? sender, BasePluginConfiguration e)
 | 
			
		||||
    {
 | 
			
		||||
        var configuration = (PluginConfiguration)e;
 | 
			
		||||
        if (Uri.TryCreate(configuration.Server, UriKind.Absolute, out var server))
 | 
			
		||||
        {
 | 
			
		||||
            Query.DefaultServer = server.DnsSafeHost;
 | 
			
		||||
            Query.DefaultPort = server.Port;
 | 
			
		||||
            Query.DefaultUrlScheme = server.Scheme;
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
        {
 | 
			
		||||
            // Fallback to official server
 | 
			
		||||
            _logger.LogWarning("Invalid MusicBrainz server specified, falling back to official server");
 | 
			
		||||
            var defaultServer = new Uri(configuration.Server);
 | 
			
		||||
            Query.DefaultServer = defaultServer.Host;
 | 
			
		||||
            Query.DefaultPort = defaultServer.Port;
 | 
			
		||||
            Query.DefaultUrlScheme = defaultServer.Scheme;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        Query.DelayBetweenRequests = configuration.RateLimit;
 | 
			
		||||
        _musicBrainzQuery = new Query();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc />
 | 
			
		||||
    public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(AlbumInfo searchInfo, CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
@ -72,13 +76,13 @@ public class MusicBrainzAlbumProvider : IRemoteMetadataProvider<MusicAlbum, Albu
 | 
			
		||||
 | 
			
		||||
        if (!string.IsNullOrEmpty(releaseId))
 | 
			
		||||
        {
 | 
			
		||||
            var releaseResult = await _musicBrainzQuery.LookupReleaseAsync(new Guid(releaseId), Include.ReleaseGroups, cancellationToken).ConfigureAwait(false);
 | 
			
		||||
            var releaseResult = await _musicBrainzQuery.LookupReleaseAsync(new Guid(releaseId), Include.Artists | Include.ReleaseGroups, cancellationToken).ConfigureAwait(false);
 | 
			
		||||
            return GetReleaseResult(releaseResult).SingleItemAsEnumerable();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!string.IsNullOrEmpty(releaseGroupId))
 | 
			
		||||
        {
 | 
			
		||||
            var releaseGroupResult = await _musicBrainzQuery.LookupReleaseGroupAsync(new Guid(releaseGroupId), Include.None, null, cancellationToken).ConfigureAwait(false);
 | 
			
		||||
            var releaseGroupResult = await _musicBrainzQuery.LookupReleaseGroupAsync(new Guid(releaseGroupId), Include.Releases, null, cancellationToken).ConfigureAwait(false);
 | 
			
		||||
            return GetReleaseGroupResult(releaseGroupResult.Releases);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -133,7 +137,9 @@ public class MusicBrainzAlbumProvider : IRemoteMetadataProvider<MusicAlbum, Albu
 | 
			
		||||
 | 
			
		||||
        foreach (var result in releaseSearchResults)
 | 
			
		||||
        {
 | 
			
		||||
            yield return GetReleaseResult(result);
 | 
			
		||||
            // Fetch full release info, otherwise artists are missing
 | 
			
		||||
            var fullResult = _musicBrainzQuery.LookupRelease(result.Id, Include.Artists | Include.ReleaseGroups);
 | 
			
		||||
            yield return GetReleaseResult(fullResult);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -143,21 +149,33 @@ public class MusicBrainzAlbumProvider : IRemoteMetadataProvider<MusicAlbum, Albu
 | 
			
		||||
        {
 | 
			
		||||
            Name = releaseSearchResult.Title,
 | 
			
		||||
            ProductionYear = releaseSearchResult.Date?.Year,
 | 
			
		||||
            PremiereDate = releaseSearchResult.Date?.NearestDate
 | 
			
		||||
            PremiereDate = releaseSearchResult.Date?.NearestDate,
 | 
			
		||||
            SearchProviderName = Name
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        if (releaseSearchResult.ArtistCredit?.Count > 0)
 | 
			
		||||
        // Add artists and use first as album artist
 | 
			
		||||
        var artists = releaseSearchResult.ArtistCredit;
 | 
			
		||||
        if (artists is not null && artists.Count > 0)
 | 
			
		||||
        {
 | 
			
		||||
            searchResult.AlbumArtist = new RemoteSearchResult
 | 
			
		||||
            var artistResults = new List<RemoteSearchResult>();
 | 
			
		||||
 | 
			
		||||
            foreach (var artist in artists)
 | 
			
		||||
            {
 | 
			
		||||
                SearchProviderName = Name,
 | 
			
		||||
                Name = releaseSearchResult.ArtistCredit[0].Name
 | 
			
		||||
                var artistResult = new RemoteSearchResult
 | 
			
		||||
                {
 | 
			
		||||
                    Name = artist.Name
 | 
			
		||||
                };
 | 
			
		||||
 | 
			
		||||
            if (releaseSearchResult.ArtistCredit[0].Artist?.Id is not null)
 | 
			
		||||
                if (artist.Artist?.Id is not null)
 | 
			
		||||
                {
 | 
			
		||||
                searchResult.AlbumArtist.SetProviderId(MetadataProvider.MusicBrainzArtist, releaseSearchResult.ArtistCredit[0].Artist!.Id.ToString());
 | 
			
		||||
                    artistResult.SetProviderId(MetadataProvider.MusicBrainzArtist, artist.Artist!.Id.ToString());
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                artistResults.Add(artistResult);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            searchResult.AlbumArtist = artistResults[0];
 | 
			
		||||
            searchResult.Artists = artistResults.ToArray();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        searchResult.SetProviderId(MetadataProvider.MusicBrainzAlbum, releaseSearchResult.Id.ToString());
 | 
			
		||||
 | 
			
		||||
@ -8,8 +8,10 @@ using Jellyfin.Extensions;
 | 
			
		||||
using MediaBrowser.Controller.Entities.Audio;
 | 
			
		||||
using MediaBrowser.Controller.Providers;
 | 
			
		||||
using MediaBrowser.Model.Entities;
 | 
			
		||||
using MediaBrowser.Model.Plugins;
 | 
			
		||||
using MediaBrowser.Model.Providers;
 | 
			
		||||
using MediaBrowser.Providers.Music;
 | 
			
		||||
using MediaBrowser.Providers.Plugins.MusicBrainz.Configuration;
 | 
			
		||||
using MetaBrainz.MusicBrainz;
 | 
			
		||||
using MetaBrainz.MusicBrainz.Interfaces.Entities;
 | 
			
		||||
using MetaBrainz.MusicBrainz.Interfaces.Searches;
 | 
			
		||||
@ -23,8 +25,7 @@ namespace MediaBrowser.Providers.Plugins.MusicBrainz;
 | 
			
		||||
public class MusicBrainzArtistProvider : IRemoteMetadataProvider<MusicArtist, ArtistInfo>, IDisposable
 | 
			
		||||
{
 | 
			
		||||
    private readonly ILogger<MusicBrainzArtistProvider> _logger;
 | 
			
		||||
    private readonly Query _musicBrainzQuery;
 | 
			
		||||
    private readonly string _musicBrainzDefaultUri = "https://musicbrainz.org";
 | 
			
		||||
    private Query _musicBrainzQuery;
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Initializes a new instance of the <see cref="MusicBrainzArtistProvider"/> class.
 | 
			
		||||
@ -33,12 +34,20 @@ public class MusicBrainzArtistProvider : IRemoteMetadataProvider<MusicArtist, Ar
 | 
			
		||||
    public MusicBrainzArtistProvider(ILogger<MusicBrainzArtistProvider> logger)
 | 
			
		||||
    {
 | 
			
		||||
        _logger = logger;
 | 
			
		||||
        _musicBrainzQuery = new Query();
 | 
			
		||||
        ReloadConfig(null, MusicBrainz.Plugin.Instance!.Configuration);
 | 
			
		||||
        MusicBrainz.Plugin.Instance!.ConfigurationChanged += ReloadConfig;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
        MusicBrainz.Plugin.Instance!.ConfigurationChanged += (_, _) =>
 | 
			
		||||
    /// <inheritdoc />
 | 
			
		||||
    public string Name => "MusicBrainz";
 | 
			
		||||
 | 
			
		||||
    private void ReloadConfig(object? sender, BasePluginConfiguration e)
 | 
			
		||||
    {
 | 
			
		||||
                if (Uri.TryCreate(MusicBrainz.Plugin.Instance.Configuration.Server, UriKind.Absolute, out var server))
 | 
			
		||||
        var configuration = (PluginConfiguration)e;
 | 
			
		||||
        if (Uri.TryCreate(configuration.Server, UriKind.Absolute, out var server))
 | 
			
		||||
        {
 | 
			
		||||
                    Query.DefaultServer = server.Host;
 | 
			
		||||
            Query.DefaultServer = server.DnsSafeHost;
 | 
			
		||||
            Query.DefaultPort = server.Port;
 | 
			
		||||
            Query.DefaultUrlScheme = server.Scheme;
 | 
			
		||||
        }
 | 
			
		||||
@ -46,21 +55,16 @@ public class MusicBrainzArtistProvider : IRemoteMetadataProvider<MusicArtist, Ar
 | 
			
		||||
        {
 | 
			
		||||
            // Fallback to official server
 | 
			
		||||
            _logger.LogWarning("Invalid MusicBrainz server specified, falling back to official server");
 | 
			
		||||
                    var defaultServer = new Uri(_musicBrainzDefaultUri);
 | 
			
		||||
            var defaultServer = new Uri(configuration.Server);
 | 
			
		||||
            Query.DefaultServer = defaultServer.Host;
 | 
			
		||||
            Query.DefaultPort = defaultServer.Port;
 | 
			
		||||
            Query.DefaultUrlScheme = defaultServer.Scheme;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
                Query.DelayBetweenRequests = MusicBrainz.Plugin.Instance.Configuration.RateLimit;
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
        Query.DelayBetweenRequests = configuration.RateLimit;
 | 
			
		||||
        _musicBrainzQuery = new Query();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc />
 | 
			
		||||
    public string Name => "MusicBrainz";
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc />
 | 
			
		||||
    public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(ArtistInfo searchInfo, CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
@ -112,7 +116,8 @@ public class MusicBrainzArtistProvider : IRemoteMetadataProvider<MusicArtist, Ar
 | 
			
		||||
        {
 | 
			
		||||
            Name = artist.Name,
 | 
			
		||||
            ProductionYear = artist.LifeSpan?.Begin?.Year,
 | 
			
		||||
            PremiereDate = artist.LifeSpan?.Begin?.NearestDate
 | 
			
		||||
            PremiereDate = artist.LifeSpan?.Begin?.NearestDate,
 | 
			
		||||
            SearchProviderName = Name,
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        searchResult.SetProviderId(MetadataProvider.MusicBrainzArtist, artist.Id.ToString());
 | 
			
		||||
 | 
			
		||||
@ -98,8 +98,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
 | 
			
		||||
                // item.VoteCount = voteCount;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (!string.IsNullOrEmpty(result.imdbRating)
 | 
			
		||||
                && float.TryParse(result.imdbRating, NumberStyles.Any, CultureInfo.InvariantCulture, out var imdbRating)
 | 
			
		||||
            if (float.TryParse(result.imdbRating, CultureInfo.InvariantCulture, out var imdbRating)
 | 
			
		||||
                && imdbRating >= 0)
 | 
			
		||||
            {
 | 
			
		||||
                item.CommunityRating = imdbRating;
 | 
			
		||||
@ -209,8 +208,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
 | 
			
		||||
                // item.VoteCount = voteCount;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (!string.IsNullOrEmpty(result.imdbRating)
 | 
			
		||||
                && float.TryParse(result.imdbRating, NumberStyles.Any, CultureInfo.InvariantCulture, out var imdbRating)
 | 
			
		||||
            if (float.TryParse(result.imdbRating, CultureInfo.InvariantCulture, out var imdbRating)
 | 
			
		||||
                && imdbRating >= 0)
 | 
			
		||||
            {
 | 
			
		||||
                item.CommunityRating = imdbRating;
 | 
			
		||||
@ -552,7 +550,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
 | 
			
		||||
                    if (rating?.Value is not null)
 | 
			
		||||
                    {
 | 
			
		||||
                        var value = rating.Value.TrimEnd('%');
 | 
			
		||||
                        if (float.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var score))
 | 
			
		||||
                        if (float.TryParse(value, CultureInfo.InvariantCulture, out var score))
 | 
			
		||||
                        {
 | 
			
		||||
                            return score;
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
@ -193,7 +193,6 @@ namespace MediaBrowser.Providers.Subtitles
 | 
			
		||||
                    await stream.CopyToAsync(memoryStream).ConfigureAwait(false);
 | 
			
		||||
                    memoryStream.Position = 0;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
                var savePaths = new List<string>();
 | 
			
		||||
                var saveFileName = Path.GetFileNameWithoutExtension(video.Path) + "." + response.Language.ToLowerInvariant();
 | 
			
		||||
@ -232,6 +231,7 @@ namespace MediaBrowser.Providers.Subtitles
 | 
			
		||||
                    _logger.LogError("An uploaded subtitle could not be saved because the resulting paths were invalid.");
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private async Task TrySaveToFiles(Stream stream, List<string> savePaths)
 | 
			
		||||
        {
 | 
			
		||||
 | 
			
		||||
@ -315,13 +315,10 @@ namespace MediaBrowser.XbmcMetadata.Parsers
 | 
			
		||||
                    {
 | 
			
		||||
                        var text = reader.ReadElementContentAsString();
 | 
			
		||||
 | 
			
		||||
                        if (!string.IsNullOrEmpty(text))
 | 
			
		||||
                        {
 | 
			
		||||
                            if (float.TryParse(text, NumberStyles.Any, CultureInfo.InvariantCulture, out var value))
 | 
			
		||||
                        if (float.TryParse(text, CultureInfo.InvariantCulture, out var value))
 | 
			
		||||
                        {
 | 
			
		||||
                            item.CriticRating = value;
 | 
			
		||||
                        }
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        break;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										36
									
								
								tests/Jellyfin.Api.Tests/Controllers/ImageControllerTests.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								tests/Jellyfin.Api.Tests/Controllers/ImageControllerTests.cs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,36 @@
 | 
			
		||||
using System;
 | 
			
		||||
using Jellyfin.Api.Controllers;
 | 
			
		||||
using Xunit;
 | 
			
		||||
 | 
			
		||||
namespace Jellyfin.Api.Tests.Controllers;
 | 
			
		||||
 | 
			
		||||
public static class ImageControllerTests
 | 
			
		||||
{
 | 
			
		||||
    [Theory]
 | 
			
		||||
    [InlineData("image/apng", ".apng")]
 | 
			
		||||
    [InlineData("image/avif", ".avif")]
 | 
			
		||||
    [InlineData("image/bmp", ".bmp")]
 | 
			
		||||
    [InlineData("image/gif", ".gif")]
 | 
			
		||||
    [InlineData("image/x-icon", ".ico")]
 | 
			
		||||
    [InlineData("image/jpeg", ".jpg")]
 | 
			
		||||
    [InlineData("image/png", ".png")]
 | 
			
		||||
    [InlineData("image/png; charset=utf-8", ".png")]
 | 
			
		||||
    [InlineData("image/svg+xml", ".svg")]
 | 
			
		||||
    [InlineData("image/tiff", ".tiff")]
 | 
			
		||||
    [InlineData("image/webp", ".webp")]
 | 
			
		||||
    public static void TryGetImageExtensionFromContentType_Valid_True(string contentType, string extension)
 | 
			
		||||
    {
 | 
			
		||||
        Assert.True(ImageController.TryGetImageExtensionFromContentType(contentType, out var ex));
 | 
			
		||||
        Assert.Equal(extension, ex);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [Theory]
 | 
			
		||||
    [InlineData(null)]
 | 
			
		||||
    [InlineData("")]
 | 
			
		||||
    [InlineData("text/html")]
 | 
			
		||||
    public static void TryGetImageExtensionFromContentType_InValid_False(string contentType)
 | 
			
		||||
    {
 | 
			
		||||
        Assert.False(ImageController.TryGetImageExtensionFromContentType(contentType, out var ex));
 | 
			
		||||
        Assert.Null(ex);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -1,7 +1,11 @@
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.Globalization;
 | 
			
		||||
using System.Security.Claims;
 | 
			
		||||
using Jellyfin.Api.Constants;
 | 
			
		||||
using Jellyfin.Api.Helpers;
 | 
			
		||||
using Jellyfin.Data.Enums;
 | 
			
		||||
using MediaBrowser.Controller.Net;
 | 
			
		||||
using Xunit;
 | 
			
		||||
 | 
			
		||||
namespace Jellyfin.Api.Tests.Helpers
 | 
			
		||||
@ -15,6 +19,82 @@ namespace Jellyfin.Api.Tests.Helpers
 | 
			
		||||
            Assert.Equal(expected, RequestHelpers.GetOrderBy(sortBy, requestedSortOrder));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Fact]
 | 
			
		||||
        public static void GetUserId_IsAdmin()
 | 
			
		||||
        {
 | 
			
		||||
            Guid? requestUserId = Guid.NewGuid();
 | 
			
		||||
            Guid? authUserId = Guid.NewGuid();
 | 
			
		||||
 | 
			
		||||
            var claims = new[]
 | 
			
		||||
            {
 | 
			
		||||
                new Claim(InternalClaimTypes.UserId, authUserId.Value.ToString("N", CultureInfo.InvariantCulture)),
 | 
			
		||||
                new Claim(InternalClaimTypes.IsApiKey, bool.FalseString),
 | 
			
		||||
                new Claim(ClaimTypes.Role, UserRoles.Administrator)
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            var identity = new ClaimsIdentity(claims, string.Empty);
 | 
			
		||||
            var principal = new ClaimsPrincipal(identity);
 | 
			
		||||
 | 
			
		||||
            var userId = RequestHelpers.GetUserId(principal, requestUserId);
 | 
			
		||||
 | 
			
		||||
            Assert.Equal(requestUserId, userId);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Fact]
 | 
			
		||||
        public static void GetUserId_IsApiKey_EmptyGuid()
 | 
			
		||||
        {
 | 
			
		||||
            Guid? requestUserId = Guid.Empty;
 | 
			
		||||
 | 
			
		||||
            var claims = new[]
 | 
			
		||||
            {
 | 
			
		||||
                new Claim(InternalClaimTypes.IsApiKey, bool.TrueString)
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            var identity = new ClaimsIdentity(claims, string.Empty);
 | 
			
		||||
            var principal = new ClaimsPrincipal(identity);
 | 
			
		||||
 | 
			
		||||
            var userId = RequestHelpers.GetUserId(principal, requestUserId);
 | 
			
		||||
 | 
			
		||||
            Assert.Equal(Guid.Empty, userId);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Fact]
 | 
			
		||||
        public static void GetUserId_IsApiKey_Null()
 | 
			
		||||
        {
 | 
			
		||||
            Guid? requestUserId = null;
 | 
			
		||||
 | 
			
		||||
            var claims = new[]
 | 
			
		||||
            {
 | 
			
		||||
                new Claim(InternalClaimTypes.IsApiKey, bool.TrueString)
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            var identity = new ClaimsIdentity(claims, string.Empty);
 | 
			
		||||
            var principal = new ClaimsPrincipal(identity);
 | 
			
		||||
 | 
			
		||||
            var userId = RequestHelpers.GetUserId(principal, requestUserId);
 | 
			
		||||
 | 
			
		||||
            Assert.Equal(Guid.Empty, userId);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Fact]
 | 
			
		||||
        public static void GetUserId_IsUser()
 | 
			
		||||
        {
 | 
			
		||||
            Guid? requestUserId = Guid.NewGuid();
 | 
			
		||||
            Guid? authUserId = Guid.NewGuid();
 | 
			
		||||
 | 
			
		||||
            var claims = new[]
 | 
			
		||||
            {
 | 
			
		||||
                new Claim(InternalClaimTypes.UserId, authUserId.Value.ToString("N", CultureInfo.InvariantCulture)),
 | 
			
		||||
                new Claim(InternalClaimTypes.IsApiKey, bool.FalseString),
 | 
			
		||||
                new Claim(ClaimTypes.Role, UserRoles.User)
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            var identity = new ClaimsIdentity(claims, string.Empty);
 | 
			
		||||
            var principal = new ClaimsPrincipal(identity);
 | 
			
		||||
 | 
			
		||||
            Assert.Throws<SecurityException>(() => RequestHelpers.GetUserId(principal, requestUserId));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public static TheoryData<IReadOnlyList<string>, IReadOnlyList<SortOrder>, (string, SortOrder)[]> GetOrderBy_Success_TestData()
 | 
			
		||||
        {
 | 
			
		||||
            var data = new TheoryData<IReadOnlyList<string>, IReadOnlyList<SortOrder>, (string, SortOrder)[]>();
 | 
			
		||||
 | 
			
		||||
@ -127,9 +127,10 @@ namespace Jellyfin.Model.Tests.Net
 | 
			
		||||
        [InlineData("image/jpeg", ".jpg")]
 | 
			
		||||
        [InlineData("image/png", ".png")]
 | 
			
		||||
        [InlineData("image/svg+xml", ".svg")]
 | 
			
		||||
        [InlineData("image/tiff", ".tif")]
 | 
			
		||||
        [InlineData("image/tiff", ".tiff")]
 | 
			
		||||
        [InlineData("image/vnd.microsoft.icon", ".ico")]
 | 
			
		||||
        [InlineData("image/webp", ".webp")]
 | 
			
		||||
        [InlineData("image/x-icon", ".ico")]
 | 
			
		||||
        [InlineData("image/x-png", ".png")]
 | 
			
		||||
        [InlineData("text/css", ".css")]
 | 
			
		||||
        [InlineData("text/csv", ".csv")]
 | 
			
		||||
 | 
			
		||||
@ -12,8 +12,6 @@ namespace Jellyfin.Naming.Tests.Common
 | 
			
		||||
 | 
			
		||||
            Assert.NotEmpty(options.CleanDateTimeRegexes);
 | 
			
		||||
            Assert.NotEmpty(options.CleanStringRegexes);
 | 
			
		||||
            Assert.NotEmpty(options.EpisodeWithoutSeasonRegexes);
 | 
			
		||||
            Assert.NotEmpty(options.EpisodeMultiPartRegexes);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Fact]
 | 
			
		||||
 | 
			
		||||
@ -73,6 +73,11 @@ namespace Jellyfin.Naming.Tests.TV
 | 
			
		||||
        [InlineData("[BBT-RMX] Ranma ½ - 154 [50AC421A].mkv", 154)] // hyphens in the pre-name info, triple digit episode number
 | 
			
		||||
        [InlineData("Season 2/Episode 21 - 94 Meetings.mp4", 21)] // Title starts with a number
 | 
			
		||||
        [InlineData("/The.Legend.of.Condor.Heroes.2017.V2.web-dl.1080p.h264.aac-hdctv/The.Legend.of.Condor.Heroes.2017.E07.V2.web-dl.1080p.h264.aac-hdctv.mkv", 7)]
 | 
			
		||||
        [InlineData("Season 3/The Series Season 3 Episode 9 - The title.avi", 9)]
 | 
			
		||||
        [InlineData("Season 3/The Series S3 E9 - The title.avi", 9)]
 | 
			
		||||
        [InlineData("Season 3/S003 E009.avi", 9)]
 | 
			
		||||
        [InlineData("Season 3/Season 3 Episode 9.avi", 9)]
 | 
			
		||||
 | 
			
		||||
        // [InlineData("Case Closed (1996-2007)/Case Closed - 317.mkv", 317)] // triple digit episode number
 | 
			
		||||
        // TODO: [InlineData("Season 2/16 12 Some Title.avi", 16)]
 | 
			
		||||
        // TODO: [InlineData("Season 4/Uchuu.Senkan.Yamato.2199.E03.avi", 3)]
 | 
			
		||||
 | 
			
		||||
@ -30,6 +30,7 @@ namespace Jellyfin.Naming.Tests.TV
 | 
			
		||||
        [InlineData("/Season 02/Elementary - 02x03-E15 - Ep Name.mp4", false, "Elementary", 2, 3)]
 | 
			
		||||
        [InlineData("/Season 1/Elementary - S01E23-E24-E26 - The Woman.mp4", false, "Elementary", 1, 23)]
 | 
			
		||||
        [InlineData("/The Wonder Years/The.Wonder.Years.S04.PDTV.x264-JCH/The Wonder Years s04e07 Christmas Party NTSC PDTV.avi", false, "The Wonder Years", 4, 7)]
 | 
			
		||||
        [InlineData("/The.Sopranos/Season 3/The Sopranos Season 3 Episode 09 - The Telltale Moozadell.avi", false, "The Sopranos", 3, 9)]
 | 
			
		||||
        // TODO: [InlineData("/Castle Rock 2x01 Que el rio siga su curso [WEB-DL HULU 1080p h264 Dual DD5.1 Subs].mkv", "Castle Rock", 2, 1)]
 | 
			
		||||
        // TODO: [InlineData("/After Life 1x06 Episodio 6 [WEB-DL NF 1080p h264 Dual DD 5.1 Sub].mkv", "After Life", 1, 6)]
 | 
			
		||||
        // TODO: [InlineData("/Season 4/Uchuu.Senkan.Yamato.2199.E03.avi", "Uchuu Senkan Yamoto 2199", 4, 3)]
 | 
			
		||||
 | 
			
		||||
@ -323,6 +323,25 @@ namespace Jellyfin.Naming.Tests.Video
 | 
			
		||||
            Assert.Single(result[0].AlternateVersions);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Fact]
 | 
			
		||||
        public void TestMultiVersion12()
 | 
			
		||||
        {
 | 
			
		||||
            var files = new[]
 | 
			
		||||
            {
 | 
			
		||||
                @"/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - 1080p.mkv",
 | 
			
		||||
                @"/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016).mkv"
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            var result = VideoListResolver.Resolve(
 | 
			
		||||
                files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
 | 
			
		||||
                _namingOptions).ToList();
 | 
			
		||||
 | 
			
		||||
            Assert.Single(result);
 | 
			
		||||
            Assert.Equal("/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016).mkv", result[0].Files[0].Path);
 | 
			
		||||
            Assert.Single(result[0].AlternateVersions);
 | 
			
		||||
            Assert.Equal("/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - 1080p.mkv", result[0].AlternateVersions[0].Path);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Fact]
 | 
			
		||||
        public void Resolve_GivenFolderNameWithBracketsAndHyphens_GroupsBasedOnFolderName()
 | 
			
		||||
        {
 | 
			
		||||
 | 
			
		||||
@ -22,13 +22,13 @@ public sealed class ItemsControllerTests : IClassFixture<JellyfinApplicationFact
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [Fact]
 | 
			
		||||
    public async Task GetItems_NoApiKeyOrUserId_BadRequest()
 | 
			
		||||
    public async Task GetItems_NoApiKeyOrUserId_Success()
 | 
			
		||||
    {
 | 
			
		||||
        var client = _factory.CreateClient();
 | 
			
		||||
        client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
 | 
			
		||||
 | 
			
		||||
        var response = await client.GetAsync("Items").ConfigureAwait(false);
 | 
			
		||||
        Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
 | 
			
		||||
        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [Theory]
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user