Merge branch 'jellyfin:master' into master

This commit is contained in:
Logan Douglas
2025-11-05 13:06:57 -07:00
committed by GitHub
16 changed files with 110 additions and 47 deletions

View File

@@ -27,11 +27,11 @@ jobs:
dotnet-version: '9.0.x'
- name: Initialize CodeQL
uses: github/codeql-action/init@4e94bd11f71e507f7f87df81788dff88d1dacbfb # v4.31.0
uses: github/codeql-action/init@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2
with:
languages: ${{ matrix.language }}
queries: +security-extended
- name: Autobuild
uses: github/codeql-action/autobuild@4e94bd11f71e507f7f87df81788dff88d1dacbfb # v4.31.0
uses: github/codeql-action/autobuild@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@4e94bd11f71e507f7f87df81788dff88d1dacbfb # v4.31.0
uses: github/codeql-action/analyze@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2

View File

@@ -68,7 +68,7 @@
<PackageVersion Include="Serilog.Expressions" Version="5.0.0" />
<PackageVersion Include="Serilog.Settings.Configuration" Version="9.0.0" />
<PackageVersion Include="Serilog.Sinks.Async" Version="2.1.0" />
<PackageVersion Include="Serilog.Sinks.Console" Version="6.0.0" />
<PackageVersion Include="Serilog.Sinks.Console" Version="6.1.1" />
<PackageVersion Include="Serilog.Sinks.File" Version="7.0.0" />
<PackageVersion Include="Serilog.Sinks.Graylog" Version="3.1.1" />
<PackageVersion Include="SerilogAnalyzer" Version="0.15.0" />

View File

@@ -113,8 +113,10 @@ namespace Emby.Naming.TV
var numberString = match.Groups["seasonnumber"];
if (numberString.Success)
{
var seasonNumber = int.Parse(numberString.Value, CultureInfo.InvariantCulture);
return (seasonNumber, true);
if (int.TryParse(numberString.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var seasonNumber))
{
return (seasonNumber, true);
}
}
return (null, false);

View File

@@ -127,8 +127,8 @@
"HearingImpaired": "聽力障礙",
"TaskRefreshTrickplayImages": "建立 Trickplay 圖像",
"TaskRefreshTrickplayImagesDescription": "為已啟用 Trickplay 的媒體庫內的影片建立 Trickplay 預覽圖。",
"TaskExtractMediaSegments": "掃描媒體段落",
"TaskExtractMediaSegmentsDescription": "從MediaSegment中被允許的插件獲取媒體段。",
"TaskExtractMediaSegments": "掃描媒體分段資訊",
"TaskExtractMediaSegmentsDescription": "從允許MediaSegment 功能的插件獲取媒體段。",
"TaskDownloadMissingLyrics": "下載欠缺歌詞",
"TaskDownloadMissingLyricsDescription": "下載歌詞",
"TaskCleanCollectionsAndPlaylists": "整理媒體與播放清單",

View File

@@ -1625,8 +1625,11 @@ public class DynamicHlsController : BaseJellyfinApiController
var useLegacySegmentOption = _mediaEncoder.EncoderVersion < _minFFmpegHlsSegmentOptions;
// fMP4 needs this flag to write the audio packet DTS/PTS including the initial delay into MOOF::TRAF::TFDT
hlsArguments += $" {(useLegacySegmentOption ? "-hls_ts_options" : "-hls_segment_options")} movflags=+frag_discont";
if (state.VideoStream is not null && state.IsOutputVideo)
{
// fMP4 needs this flag to write the audio packet DTS/PTS including the initial delay into MOOF::TRAF::TFDT
hlsArguments += $" {(useLegacySegmentOption ? "-hls_ts_options" : "-hls_segment_options")} movflags=+frag_discont";
}
segmentFormat = "fmp4" + outputFmp4HeaderArg;
}

View File

@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Security.Cryptography;
using System.Text.Json;
@@ -92,33 +93,38 @@ namespace Jellyfin.Server.Implementations.Users
}
/// <inheritdoc />
public async Task<ForgotPasswordResult> StartForgotPasswordProcess(User user, bool isInNetwork)
public async Task<ForgotPasswordResult> StartForgotPasswordProcess(User? user, string enteredUsername, bool isInNetwork)
{
byte[] bytes = new byte[4];
RandomNumberGenerator.Fill(bytes);
string pin = BitConverter.ToString(bytes);
DateTime expireTime = DateTime.UtcNow.AddMinutes(30);
string filePath = _passwordResetFileBase + user.Id + ".json";
SerializablePasswordReset spr = new SerializablePasswordReset
{
ExpirationDate = expireTime,
Pin = pin,
PinFile = filePath,
UserName = user.Username
};
var usernameHash = enteredUsername.ToUpperInvariant().GetMD5().ToString("N", CultureInfo.InvariantCulture);
var pinFile = _passwordResetFileBase + usernameHash + ".json";
FileStream fileStream = AsyncFile.Create(filePath);
await using (fileStream.ConfigureAwait(false))
if (user is not null && isInNetwork)
{
await JsonSerializer.SerializeAsync(fileStream, spr).ConfigureAwait(false);
byte[] bytes = new byte[4];
RandomNumberGenerator.Fill(bytes);
string pin = BitConverter.ToString(bytes);
SerializablePasswordReset spr = new SerializablePasswordReset
{
ExpirationDate = expireTime,
Pin = pin,
PinFile = pinFile,
UserName = user.Username
};
FileStream fileStream = AsyncFile.Create(pinFile);
await using (fileStream.ConfigureAwait(false))
{
await JsonSerializer.SerializeAsync(fileStream, spr).ConfigureAwait(false);
}
}
return new ForgotPasswordResult
{
Action = ForgotPasswordAction.PinCode,
PinExpirationDate = expireTime,
PinFile = filePath
PinFile = pinFile
};
}

View File

@@ -508,23 +508,18 @@ namespace Jellyfin.Server.Implementations.Users
public async Task<ForgotPasswordResult> StartForgotPasswordProcess(string enteredUsername, bool isInNetwork)
{
var user = string.IsNullOrWhiteSpace(enteredUsername) ? null : GetUserByName(enteredUsername);
var passwordResetProvider = GetPasswordResetProvider(user);
var result = await passwordResetProvider
.StartForgotPasswordProcess(user, enteredUsername, isInNetwork)
.ConfigureAwait(false);
if (user is not null && isInNetwork)
{
var passwordResetProvider = GetPasswordResetProvider(user);
var result = await passwordResetProvider
.StartForgotPasswordProcess(user, isInNetwork)
.ConfigureAwait(false);
await UpdateUserAsync(user).ConfigureAwait(false);
return result;
}
return new ForgotPasswordResult
{
Action = ForgotPasswordAction.InNetworkRequired,
PinFile = string.Empty
};
return result;
}
/// <inheritdoc/>
@@ -760,8 +755,13 @@ namespace Jellyfin.Server.Implementations.Users
return GetAuthenticationProviders(user)[0];
}
private IPasswordResetProvider GetPasswordResetProvider(User user)
private IPasswordResetProvider GetPasswordResetProvider(User? user)
{
if (user is null)
{
return _defaultPasswordResetProvider;
}
return GetPasswordResetProviders(user)[0];
}

View File

@@ -71,7 +71,7 @@ namespace Jellyfin.Server.Migrations.Routines
if (row.GetInt32(0) == 0)
{
_logger.LogWarning("Table 'ActivityLog' doesn't exist in {ActivityLogPath}, nothing to migrate", activityLogPath);
break;
return;
}
}

View File

@@ -50,9 +50,28 @@ namespace Jellyfin.Server.Migrations.Routines
public void Perform()
{
var dataPath = _appPaths.DataPath;
using (var connection = new SqliteConnection($"Filename={Path.Combine(dataPath, DbFilename)}"))
var dbFilePath = Path.Combine(dataPath, DbFilename);
if (!File.Exists(dbFilePath))
{
_logger.LogWarning("{Path} doesn't exist, nothing to migrate", dbFilePath);
return;
}
using (var connection = new SqliteConnection($"Filename={dbFilePath}"))
{
connection.Open();
var tableQuery = connection.Query("SELECT count(*) FROM sqlite_master WHERE type='table' AND name='Tokens';");
foreach (var row in tableQuery)
{
if (row.GetInt32(0) == 0)
{
_logger.LogWarning("Table 'Tokens' doesn't exist in {Path}, nothing to migrate", dbFilePath);
return;
}
}
using var dbContext = _dbProvider.CreateDbContext();
var authenticatedDevices = connection.Query("SELECT * FROM Tokens");

View File

@@ -78,9 +78,27 @@ namespace Jellyfin.Server.Migrations.Routines
var displayPrefs = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var customDisplayPrefs = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var dbFilePath = Path.Combine(_paths.DataPath, DbFilename);
if (!File.Exists(dbFilePath))
{
_logger.LogWarning("{Path} doesn't exist, nothing to migrate", dbFilePath);
return;
}
using (var connection = new SqliteConnection($"Filename={dbFilePath}"))
{
connection.Open();
var tableQuery = connection.Query("SELECT count(*) FROM sqlite_master WHERE type='table' AND name='userdisplaypreferences';");
foreach (var row in tableQuery)
{
if (row.GetInt32(0) == 0)
{
_logger.LogWarning("Table 'userdisplaypreferences' doesn't exist in {Path}, nothing to migrate", dbFilePath);
return;
}
}
using var dbContext = _provider.CreateDbContext();
var results = connection.Query("SELECT * FROM userdisplaypreferences");

View File

@@ -75,7 +75,7 @@ public class MigrateUserDb : IMigrationRoutine
if (row.GetInt32(0) == 0)
{
_logger.LogWarning("Table 'LocalUsersv2' doesn't exist in {UserDbPath}, nothing to migrate", userDbPath);
break;
return;
}
}

View File

@@ -1,5 +1,3 @@
#nullable disable
#pragma warning disable CS1591
using System;
@@ -15,11 +13,12 @@ namespace MediaBrowser.Controller.Authentication
bool IsEnabled { get; }
Task<ForgotPasswordResult> StartForgotPasswordProcess(User user, bool isInNetwork);
Task<ForgotPasswordResult> StartForgotPasswordProcess(User? user, string enteredUsername, bool isInNetwork);
Task<PinRedeemResult> RedeemPasswordResetPin(string pin);
}
#nullable disable
public class PasswordPinCreationResult
{
public string PinFile { get; set; }

View File

@@ -1,11 +1,15 @@
#pragma warning disable CS1591
using System;
namespace MediaBrowser.Model.Users
{
public enum ForgotPasswordAction
{
[Obsolete("Returning different actions represents a security concern.")]
ContactAdmin = 0,
PinCode = 1,
[Obsolete("Returning different actions represents a security concern.")]
InNetworkRequired = 2
}
}

View File

@@ -229,6 +229,11 @@ namespace MediaBrowser.Providers.Manager
if (file is not null)
{
item.DateModified = file.LastWriteTimeUtc;
if (!file.IsDirectory)
{
item.Size = file.Length;
}
}
}

View File

@@ -52,10 +52,14 @@ public class OptimisticLockBehavior : IEntityFrameworkCoreLockingBehavior
_logger = logger;
_writePolicy = Policy
.HandleInner<Exception>(e => e.Message.Contains("database is locked", StringComparison.InvariantCultureIgnoreCase))
.HandleInner<Exception>(e =>
e.Message.Contains("database is locked", StringComparison.InvariantCultureIgnoreCase) ||
e.Message.Contains("database table is locked", StringComparison.InvariantCultureIgnoreCase))
.WaitAndRetry(sleepDurations.Length, backoffProvider, RetryHandle);
_writeAsyncPolicy = Policy
.HandleInner<Exception>(e => e.Message.Contains("database is locked", StringComparison.InvariantCultureIgnoreCase))
.HandleInner<Exception>(e =>
e.Message.Contains("database is locked", StringComparison.InvariantCultureIgnoreCase) ||
e.Message.Contains("database table is locked", StringComparison.InvariantCultureIgnoreCase))
.WaitAndRetryAsync(sleepDurations.Length, backoffProvider, RetryHandle);
void RetryHandle(Exception exception, TimeSpan timespan, int retryNo, Context context)

View File

@@ -66,6 +66,9 @@ public class SeasonPathParserTests
[InlineData("/Drive/SPECIALS", "/Drive", 0, true)]
[InlineData("/Drive/Episode 1 Season 2", "/Drive", null, false)]
[InlineData("/Drive/Episode 1 SEASON 2", "/Drive", null, false)]
[InlineData("/media/YouTube/Devyn Johnston/2024-01-24 4070 Ti SUPER in under 7 minutes", "/media/YouTube/Devyn Johnston", null, false)]
[InlineData("/media/YouTube/Devyn Johnston/2025-01-28 5090 vs 2 SFF Cases", "/media/YouTube/Devyn Johnston", null, false)]
[InlineData("/Drive/202401244070", "/Drive", null, false)]
public void GetSeasonNumberFromPathTest(string path, string? parentPath, int? seasonNumber, bool isSeasonDirectory)
{
var result = SeasonPathParser.Parse(path, parentPath, true, true);