mirror of
https://github.com/jellyfin/jellyfin.git
synced 2025-12-09 18:43:05 +03:00
Compare commits
38 Commits
6c507b77ae
...
v10.11.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4187c6f620 | ||
|
|
e7dbb3afec | ||
|
|
f994dd6211 | ||
|
|
da254ee968 | ||
|
|
4ad3141875 | ||
|
|
b5f0199a25 | ||
|
|
6bf88c049e | ||
|
|
40a33da2a5 | ||
|
|
3596fc0693 | ||
|
|
93824dad97 | ||
|
|
e5656af1f2 | ||
|
|
c127c10458 | ||
|
|
7d1824ea27 | ||
|
|
2966d27c97 | ||
|
|
618ec4543e | ||
|
|
0e4031ae52 | ||
|
|
442af96ed9 | ||
|
|
a305204cfa | ||
|
|
75f472e6a7 | ||
|
|
cc32e8f7cb | ||
|
|
14b3085ff1 | ||
|
|
5691eee4f1 | ||
|
|
1520a697ad | ||
|
|
81b8b0ca4a | ||
|
|
ac3fa3c376 | ||
|
|
7a1c1cd342 | ||
|
|
70c32a26fa | ||
|
|
2b94bb54aa | ||
|
|
0a6e8146be | ||
|
|
305b0fdca3 | ||
|
|
d738386fe2 | ||
|
|
ca830d5be7 | ||
|
|
a5bc4524d8 | ||
|
|
175ee12bbc | ||
|
|
a725220c21 | ||
|
|
a245605152 | ||
|
|
f4a53209f4 | ||
|
|
877251bcae |
@@ -88,7 +88,7 @@
|
||||
<PackageVersion Include="System.Text.Json" Version="9.0.10" />
|
||||
<PackageVersion Include="System.Threading.Tasks.Dataflow" Version="9.0.10" />
|
||||
<PackageVersion Include="TagLibSharp" Version="2.3.0" />
|
||||
<PackageVersion Include="z440.atl.core" Version="7.5.0" />
|
||||
<PackageVersion Include="z440.atl.core" Version="7.6.0" />
|
||||
<PackageVersion Include="TMDbLib" Version="2.3.0" />
|
||||
<PackageVersion Include="UTF.Unknown" Version="2.6.0" />
|
||||
<PackageVersion Include="Xunit.Priority" Version="1.1.6" />
|
||||
@@ -96,4 +96,4 @@
|
||||
<PackageVersion Include="Xunit.SkippableFact" Version="1.5.23" />
|
||||
<PackageVersion Include="xunit" Version="2.9.3" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
<PropertyGroup>
|
||||
<Authors>Jellyfin Contributors</Authors>
|
||||
<PackageId>Jellyfin.Naming</PackageId>
|
||||
<VersionPrefix>10.11.0</VersionPrefix>
|
||||
<VersionPrefix>10.11.2</VersionPrefix>
|
||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -10,10 +10,10 @@ namespace Emby.Naming.TV
|
||||
/// </summary>
|
||||
public static partial class SeasonPathParser
|
||||
{
|
||||
[GeneratedRegex(@"^\s*((?<seasonnumber>(?>\d+))(?:st|nd|rd|th|\.)*(?!\s*[Ee]\d+))\s*(?:[[시즌]*|[シーズン]*|[sS](?:eason|æson|aison|taffel|eries|tagione|äsong|eizoen|easong|ezon|ezona|ezóna|ezonul)*|[tT](?:emporada)*|[kK](?:ausi)*|[Сс](?:езон)*)\s*(?<rightpart>.*)$")]
|
||||
[GeneratedRegex(@"^\s*((?<seasonnumber>(?>\d+))(?:st|nd|rd|th|\.)*(?!\s*[Ee]\d+))\s*(?:[[시즌]*|[シーズン]*|[sS](?:eason|æson|aison|taffel|eries|tagione|äsong|eizoen|easong|ezon|ezona|ezóna|ezonul)*|[tT](?:emporada)*|[kK](?:ausi)*|[Сс](?:езон)*)\s*(?<rightpart>.*)$", RegexOptions.IgnoreCase)]
|
||||
private static partial Regex ProcessPre();
|
||||
|
||||
[GeneratedRegex(@"^\s*(?:[[시즌]*|[シーズン]*|[sS](?:eason|æson|aison|taffel|eries|tagione|äsong|eizoen|easong|ezon|ezona|ezóna|ezonul)*|[tT](?:emporada)*|[kK](?:ausi)*|[Сс](?:езон)*)\s*(?<seasonnumber>(?>\d+)(?!\s*[Ee]\d+))(?<rightpart>.*)$")]
|
||||
[GeneratedRegex(@"^\s*(?:[[시즌]*|[シーズン]*|[sS](?:eason|æson|aison|taffel|eries|tagione|äsong|eizoen|easong|ezon|ezona|ezóna|ezonul)*|[tT](?:emporada)*|[kK](?:ausi)*|[Сс](?:езон)*)\s*(?<seasonnumber>(?>\d+)(?!\s*[Ee]\d+))(?<rightpart>.*)$", RegexOptions.IgnoreCase)]
|
||||
private static partial Regex ProcessPost();
|
||||
|
||||
/// <summary>
|
||||
@@ -86,7 +86,7 @@ namespace Emby.Naming.TV
|
||||
}
|
||||
}
|
||||
|
||||
if (filename.StartsWith('s'))
|
||||
if (filename.Length > 0 && (filename[0] == 'S' || filename[0] == 's'))
|
||||
{
|
||||
var testFilename = filename.AsSpan()[1..];
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -223,7 +223,7 @@ public class ChapterManager : IChapterManager
|
||||
|
||||
if (saveChapters && changesMade)
|
||||
{
|
||||
_chapterRepository.SaveChapters(video.Id, chapters);
|
||||
SaveChapters(video, chapters);
|
||||
}
|
||||
|
||||
DeleteDeadImages(currentImages, chapters);
|
||||
@@ -234,7 +234,9 @@ public class ChapterManager : IChapterManager
|
||||
/// <inheritdoc />
|
||||
public void SaveChapters(Video video, IReadOnlyList<ChapterInfo> chapters)
|
||||
{
|
||||
_chapterRepository.SaveChapters(video.Id, chapters);
|
||||
// Remove any chapters that are outside of the runtime of the video
|
||||
var validChapters = chapters.Where(c => c.StartPositionTicks < video.RunTimeTicks).ToList();
|
||||
_chapterRepository.SaveChapters(video.Id, validChapters);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@@ -152,6 +152,10 @@ namespace Emby.Server.Implementations.IO
|
||||
/// <inheritdoc />
|
||||
public void MoveDirectory(string source, string destination)
|
||||
{
|
||||
// Make sure parent directory of target exists
|
||||
var parent = Directory.GetParent(destination);
|
||||
parent?.Create();
|
||||
|
||||
try
|
||||
{
|
||||
Directory.Move(source, destination);
|
||||
@@ -248,47 +252,40 @@ namespace Emby.Server.Implementations.IO
|
||||
{
|
||||
result.IsDirectory = info is DirectoryInfo || (info.Attributes & FileAttributes.Directory) == FileAttributes.Directory;
|
||||
|
||||
// if (!result.IsDirectory)
|
||||
// {
|
||||
// result.IsHidden = (info.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden;
|
||||
// }
|
||||
|
||||
if (info is FileInfo fileInfo)
|
||||
{
|
||||
result.Length = fileInfo.Length;
|
||||
|
||||
// Issue #2354 get the size of files behind symbolic links. Also Enum.HasFlag is bad as it boxes!
|
||||
if ((fileInfo.Attributes & FileAttributes.ReparsePoint) == FileAttributes.ReparsePoint)
|
||||
result.CreationTimeUtc = GetCreationTimeUtc(info);
|
||||
result.LastWriteTimeUtc = GetLastWriteTimeUtc(info);
|
||||
if (fileInfo.LinkTarget is not null)
|
||||
{
|
||||
try
|
||||
{
|
||||
using (var fileHandle = File.OpenHandle(fileInfo.FullName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
|
||||
var targetFileInfo = (FileInfo?)fileInfo.ResolveLinkTarget(returnFinalTarget: true);
|
||||
if (targetFileInfo is not null)
|
||||
{
|
||||
result.Length = RandomAccess.GetLength(fileHandle);
|
||||
result.Exists = targetFileInfo.Exists;
|
||||
if (result.Exists)
|
||||
{
|
||||
result.Length = targetFileInfo.Length;
|
||||
result.CreationTimeUtc = GetCreationTimeUtc(targetFileInfo);
|
||||
result.LastWriteTimeUtc = GetLastWriteTimeUtc(targetFileInfo);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
result.Exists = false;
|
||||
}
|
||||
}
|
||||
catch (FileNotFoundException ex)
|
||||
{
|
||||
// Dangling symlinks cannot be detected before opening the file unfortunately...
|
||||
_logger.LogError(ex, "Reading the file size of the symlink at {Path} failed. Marking the file as not existing.", fileInfo.FullName);
|
||||
result.Exists = false;
|
||||
}
|
||||
catch (UnauthorizedAccessException ex)
|
||||
{
|
||||
_logger.LogError(ex, "Reading the file at {Path} failed due to a permissions exception.", fileInfo.FullName);
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
// IOException generally means the file is not accessible due to filesystem issues
|
||||
// Catch this exception and mark the file as not exist to ignore it
|
||||
_logger.LogError(ex, "Reading the file at {Path} failed due to an IO Exception. Marking the file as not existing", fileInfo.FullName);
|
||||
result.Exists = false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
result.Length = fileInfo.Length;
|
||||
}
|
||||
}
|
||||
|
||||
result.CreationTimeUtc = GetCreationTimeUtc(info);
|
||||
result.LastWriteTimeUtc = GetLastWriteTimeUtc(info);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -51,8 +51,7 @@ public class DotIgnoreIgnoreRule : IResolverIgnoreRule
|
||||
}
|
||||
|
||||
// Fast path in case the ignore files isn't a symlink and is empty
|
||||
if ((dirIgnoreFile.Attributes & FileAttributes.ReparsePoint) == 0
|
||||
&& dirIgnoreFile.Length == 0)
|
||||
if (dirIgnoreFile.LinkTarget is null && dirIgnoreFile.Length == 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
@@ -93,6 +92,12 @@ public class DotIgnoreIgnoreRule : IResolverIgnoreRule
|
||||
|
||||
private static string GetFileContent(FileInfo dirIgnoreFile)
|
||||
{
|
||||
dirIgnoreFile = (FileInfo?)dirIgnoreFile.ResolveLinkTarget(returnFinalTarget: true) ?? dirIgnoreFile;
|
||||
if (!dirIgnoreFile.Exists)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
using (var reader = dirIgnoreFile.OpenText())
|
||||
{
|
||||
return reader.ReadToEnd();
|
||||
|
||||
@@ -28,7 +28,9 @@ namespace Emby.Server.Implementations.Library
|
||||
|
||||
public IReadOnlyList<BaseItem> GetInstantMixFromSong(Audio item, User? user, DtoOptions dtoOptions)
|
||||
{
|
||||
return GetInstantMixFromGenres(item.Genres, user, dtoOptions);
|
||||
var instantMixItems = GetInstantMixFromGenres(item.Genres, user, dtoOptions);
|
||||
|
||||
return [item, .. instantMixItems.Where(i => !i.Id.Equals(item.Id))];
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@@ -223,15 +223,14 @@ namespace Emby.Server.Implementations.Updates
|
||||
Guid id = default,
|
||||
Version? specificVersion = null)
|
||||
{
|
||||
if (name is not null)
|
||||
{
|
||||
availablePackages = availablePackages.Where(x => x.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
if (!id.IsEmpty())
|
||||
{
|
||||
availablePackages = availablePackages.Where(x => x.Id.Equals(id));
|
||||
}
|
||||
else if (name is not null)
|
||||
{
|
||||
availablePackages = availablePackages.Where(x => x.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
if (specificVersion is not null)
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
using System;
|
||||
using System.Net.Mime;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc.Formatters;
|
||||
|
||||
namespace Jellyfin.Api.Formatters;
|
||||
@@ -6,7 +10,7 @@ namespace Jellyfin.Api.Formatters;
|
||||
/// <summary>
|
||||
/// Xml output formatter.
|
||||
/// </summary>
|
||||
public sealed class XmlOutputFormatter : StringOutputFormatter
|
||||
public sealed class XmlOutputFormatter : TextOutputFormatter
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="XmlOutputFormatter"/> class.
|
||||
@@ -15,5 +19,24 @@ public sealed class XmlOutputFormatter : StringOutputFormatter
|
||||
{
|
||||
SupportedMediaTypes.Clear();
|
||||
SupportedMediaTypes.Add(MediaTypeNames.Text.Xml);
|
||||
|
||||
SupportedEncodings.Add(Encoding.UTF8);
|
||||
SupportedEncodings.Add(Encoding.Unicode);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(context);
|
||||
ArgumentNullException.ThrowIfNull(selectedEncoding);
|
||||
|
||||
var valueAsString = context.Object?.ToString();
|
||||
if (string.IsNullOrEmpty(valueAsString))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var response = context.HttpContext.Response;
|
||||
await response.WriteAsync(valueAsString, selectedEncoding).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
<PropertyGroup>
|
||||
<Authors>Jellyfin Contributors</Authors>
|
||||
<PackageId>Jellyfin.Data</PackageId>
|
||||
<VersionPrefix>10.11.0</VersionPrefix>
|
||||
<VersionPrefix>10.11.2</VersionPrefix>
|
||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -128,7 +128,8 @@ public class BackupService : IBackupService
|
||||
var targetPath = Path.GetFullPath(Path.Combine(target, Path.GetRelativePath(source, item.FullName)));
|
||||
|
||||
if (!sourcePath.StartsWith(fullSourcePath, StringComparison.Ordinal)
|
||||
|| !targetPath.StartsWith(fullTargetRoot, StringComparison.Ordinal))
|
||||
|| !targetPath.StartsWith(fullTargetRoot, StringComparison.Ordinal)
|
||||
|| Path.EndsInDirectorySeparator(item.FullName))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -199,7 +200,7 @@ public class BackupService : IBackupService
|
||||
var zipEntry = zipArchive.GetEntry(NormalizePathSeparator(Path.Combine("Database", $"{entityType.Type.Name}.json")));
|
||||
if (zipEntry is null)
|
||||
{
|
||||
_logger.LogInformation("No backup of expected table {Table} is present in backup. Continue anyway.", entityType.Type.Name);
|
||||
_logger.LogInformation("No backup of expected table {Table} is present in backup, continuing anyway", entityType.Type.Name);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -223,7 +224,7 @@ public class BackupService : IBackupService
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Could not store entity {Entity} continue anyway.", item);
|
||||
_logger.LogError(ex, "Could not store entity {Entity}, continuing anyway", item);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -233,11 +234,11 @@ public class BackupService : IBackupService
|
||||
|
||||
_logger.LogInformation("Try restore Database");
|
||||
await dbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
_logger.LogInformation("Restored database.");
|
||||
_logger.LogInformation("Restored database");
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogInformation("Restored Jellyfin system from {Date}.", manifest.DateCreated);
|
||||
_logger.LogInformation("Restored Jellyfin system from {Date}", manifest.DateCreated);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -263,6 +264,8 @@ public class BackupService : IBackupService
|
||||
Options = Map(backupOptions)
|
||||
};
|
||||
|
||||
_logger.LogInformation("Running database optimization before backup");
|
||||
|
||||
await _jellyfinDatabaseProvider.RunScheduledOptimisation(CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
var backupFolder = Path.Combine(_applicationPaths.BackupPath);
|
||||
@@ -281,130 +284,154 @@ public class BackupService : IBackupService
|
||||
}
|
||||
|
||||
var backupPath = Path.Combine(backupFolder, $"jellyfin-backup-{manifest.DateCreated.ToLocalTime():yyyyMMddHHmmss}.zip");
|
||||
_logger.LogInformation("Attempt to create a new backup at {BackupPath}", backupPath);
|
||||
var fileStream = File.OpenWrite(backupPath);
|
||||
await using (fileStream.ConfigureAwait(false))
|
||||
using (var zipArchive = new ZipArchive(fileStream, ZipArchiveMode.Create, false))
|
||||
|
||||
try
|
||||
{
|
||||
_logger.LogInformation("Start backup process.");
|
||||
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
|
||||
await using (dbContext.ConfigureAwait(false))
|
||||
_logger.LogInformation("Attempting to create a new backup at {BackupPath}", backupPath);
|
||||
var fileStream = File.OpenWrite(backupPath);
|
||||
await using (fileStream.ConfigureAwait(false))
|
||||
using (var zipArchive = new ZipArchive(fileStream, ZipArchiveMode.Create, false))
|
||||
{
|
||||
dbContext.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
|
||||
static IAsyncEnumerable<object> GetValues(IQueryable dbSet)
|
||||
_logger.LogInformation("Starting backup process");
|
||||
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
|
||||
await using (dbContext.ConfigureAwait(false))
|
||||
{
|
||||
var method = dbSet.GetType().GetMethod(nameof(DbSet<object>.AsAsyncEnumerable))!;
|
||||
var enumerable = method.Invoke(dbSet, null)!;
|
||||
return (IAsyncEnumerable<object>)enumerable;
|
||||
}
|
||||
dbContext.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
|
||||
|
||||
// include the migration history as well
|
||||
var historyRepository = dbContext.GetService<IHistoryRepository>();
|
||||
var migrations = await historyRepository.GetAppliedMigrationsAsync().ConfigureAwait(false);
|
||||
|
||||
ICollection<(Type Type, string SourceName, Func<IAsyncEnumerable<object>> ValueFactory)> entityTypes = [
|
||||
.. typeof(JellyfinDbContext)
|
||||
.GetProperties(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance)
|
||||
.Where(e => e.PropertyType.IsAssignableTo(typeof(IQueryable)))
|
||||
.Select(e => (Type: e.PropertyType, dbContext.Model.FindEntityType(e.PropertyType.GetGenericArguments()[0])!.GetSchemaQualifiedTableName()!, ValueFactory: new Func<IAsyncEnumerable<object>>(() => GetValues((IQueryable)e.GetValue(dbContext)!)))),
|
||||
(Type: typeof(HistoryRow), SourceName: nameof(HistoryRow), ValueFactory: () => migrations.ToAsyncEnumerable())
|
||||
];
|
||||
manifest.DatabaseTables = entityTypes.Select(e => e.Type.Name).ToArray();
|
||||
var transaction = await dbContext.Database.BeginTransactionAsync().ConfigureAwait(false);
|
||||
|
||||
await using (transaction.ConfigureAwait(false))
|
||||
{
|
||||
_logger.LogInformation("Begin Database backup");
|
||||
|
||||
foreach (var entityType in entityTypes)
|
||||
static IAsyncEnumerable<object> GetValues(IQueryable dbSet)
|
||||
{
|
||||
_logger.LogInformation("Begin backup of entity {Table}", entityType.SourceName);
|
||||
var zipEntry = zipArchive.CreateEntry(NormalizePathSeparator(Path.Combine("Database", $"{entityType.SourceName}.json")));
|
||||
var entities = 0;
|
||||
var zipEntryStream = zipEntry.Open();
|
||||
await using (zipEntryStream.ConfigureAwait(false))
|
||||
var method = dbSet.GetType().GetMethod(nameof(DbSet<object>.AsAsyncEnumerable))!;
|
||||
var enumerable = method.Invoke(dbSet, null)!;
|
||||
return (IAsyncEnumerable<object>)enumerable;
|
||||
}
|
||||
|
||||
// include the migration history as well
|
||||
var historyRepository = dbContext.GetService<IHistoryRepository>();
|
||||
var migrations = await historyRepository.GetAppliedMigrationsAsync().ConfigureAwait(false);
|
||||
|
||||
ICollection<(Type Type, string SourceName, Func<IAsyncEnumerable<object>> ValueFactory)> entityTypes =
|
||||
[
|
||||
.. typeof(JellyfinDbContext)
|
||||
.GetProperties(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance)
|
||||
.Where(e => e.PropertyType.IsAssignableTo(typeof(IQueryable)))
|
||||
.Select(e => (Type: e.PropertyType, dbContext.Model.FindEntityType(e.PropertyType.GetGenericArguments()[0])!.GetSchemaQualifiedTableName()!, ValueFactory: new Func<IAsyncEnumerable<object>>(() => GetValues((IQueryable)e.GetValue(dbContext)!)))),
|
||||
(Type: typeof(HistoryRow), SourceName: nameof(HistoryRow), ValueFactory: () => migrations.ToAsyncEnumerable())
|
||||
];
|
||||
manifest.DatabaseTables = entityTypes.Select(e => e.Type.Name).ToArray();
|
||||
var transaction = await dbContext.Database.BeginTransactionAsync().ConfigureAwait(false);
|
||||
|
||||
await using (transaction.ConfigureAwait(false))
|
||||
{
|
||||
_logger.LogInformation("Begin Database backup");
|
||||
|
||||
foreach (var entityType in entityTypes)
|
||||
{
|
||||
var jsonSerializer = new Utf8JsonWriter(zipEntryStream);
|
||||
await using (jsonSerializer.ConfigureAwait(false))
|
||||
_logger.LogInformation("Begin backup of entity {Table}", entityType.SourceName);
|
||||
var zipEntry = zipArchive.CreateEntry(NormalizePathSeparator(Path.Combine("Database", $"{entityType.SourceName}.json")));
|
||||
var entities = 0;
|
||||
var zipEntryStream = zipEntry.Open();
|
||||
await using (zipEntryStream.ConfigureAwait(false))
|
||||
{
|
||||
jsonSerializer.WriteStartArray();
|
||||
|
||||
var set = entityType.ValueFactory().ConfigureAwait(false);
|
||||
await foreach (var item in set.ConfigureAwait(false))
|
||||
var jsonSerializer = new Utf8JsonWriter(zipEntryStream);
|
||||
await using (jsonSerializer.ConfigureAwait(false))
|
||||
{
|
||||
entities++;
|
||||
try
|
||||
jsonSerializer.WriteStartArray();
|
||||
|
||||
var set = entityType.ValueFactory().ConfigureAwait(false);
|
||||
await foreach (var item in set.ConfigureAwait(false))
|
||||
{
|
||||
JsonSerializer.SerializeToDocument(item, _serializerSettings).WriteTo(jsonSerializer);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Could not load entity {Entity}", item);
|
||||
throw;
|
||||
entities++;
|
||||
try
|
||||
{
|
||||
using var document = JsonSerializer.SerializeToDocument(item, _serializerSettings);
|
||||
document.WriteTo(jsonSerializer);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Could not load entity {Entity}", item);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
jsonSerializer.WriteEndArray();
|
||||
}
|
||||
|
||||
jsonSerializer.WriteEndArray();
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogInformation("backup of entity {Table} with {Number} created", entityType.Type.Name, entities);
|
||||
_logger.LogInformation("Backup of entity {Table} with {Number} created", entityType.SourceName, entities);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogInformation("Backup of folder {Table}", _applicationPaths.ConfigurationDirectoryPath);
|
||||
foreach (var item in Directory.EnumerateFiles(_applicationPaths.ConfigurationDirectoryPath, "*.xml", SearchOption.TopDirectoryOnly)
|
||||
.Union(Directory.EnumerateFiles(_applicationPaths.ConfigurationDirectoryPath, "*.json", SearchOption.TopDirectoryOnly)))
|
||||
{
|
||||
zipArchive.CreateEntryFromFile(item, NormalizePathSeparator(Path.Combine("Config", Path.GetFileName(item))));
|
||||
}
|
||||
|
||||
void CopyDirectory(string source, string target, string filter = "*")
|
||||
{
|
||||
if (!Directory.Exists(source))
|
||||
_logger.LogInformation("Backup of folder {Table}", _applicationPaths.ConfigurationDirectoryPath);
|
||||
foreach (var item in Directory.EnumerateFiles(_applicationPaths.ConfigurationDirectoryPath, "*.xml", SearchOption.TopDirectoryOnly)
|
||||
.Union(Directory.EnumerateFiles(_applicationPaths.ConfigurationDirectoryPath, "*.json", SearchOption.TopDirectoryOnly)))
|
||||
{
|
||||
return;
|
||||
zipArchive.CreateEntryFromFile(item, NormalizePathSeparator(Path.Combine("Config", Path.GetFileName(item))));
|
||||
}
|
||||
|
||||
_logger.LogInformation("Backup of folder {Table}", source);
|
||||
|
||||
foreach (var item in Directory.EnumerateFiles(source, filter, SearchOption.AllDirectories))
|
||||
void CopyDirectory(string source, string target, string filter = "*")
|
||||
{
|
||||
zipArchive.CreateEntryFromFile(item, NormalizePathSeparator(Path.Combine(target, Path.GetRelativePath(source, item))));
|
||||
if (!Directory.Exists(source))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.LogInformation("Backup of folder {Table}", source);
|
||||
|
||||
foreach (var item in Directory.EnumerateFiles(source, filter, SearchOption.AllDirectories))
|
||||
{
|
||||
zipArchive.CreateEntryFromFile(item, NormalizePathSeparator(Path.Combine(target, Path.GetRelativePath(source, item))));
|
||||
}
|
||||
}
|
||||
|
||||
CopyDirectory(Path.Combine(_applicationPaths.ConfigurationDirectoryPath, "users"), Path.Combine("Config", "users"));
|
||||
CopyDirectory(Path.Combine(_applicationPaths.ConfigurationDirectoryPath, "ScheduledTasks"), Path.Combine("Config", "ScheduledTasks"));
|
||||
CopyDirectory(Path.Combine(_applicationPaths.RootFolderPath), "Root");
|
||||
CopyDirectory(Path.Combine(_applicationPaths.DataPath, "collections"), Path.Combine("Data", "collections"));
|
||||
CopyDirectory(Path.Combine(_applicationPaths.DataPath, "playlists"), Path.Combine("Data", "playlists"));
|
||||
CopyDirectory(Path.Combine(_applicationPaths.DataPath, "ScheduledTasks"), Path.Combine("Data", "ScheduledTasks"));
|
||||
if (backupOptions.Subtitles)
|
||||
{
|
||||
CopyDirectory(Path.Combine(_applicationPaths.DataPath, "subtitles"), Path.Combine("Data", "subtitles"));
|
||||
}
|
||||
|
||||
if (backupOptions.Trickplay)
|
||||
{
|
||||
CopyDirectory(Path.Combine(_applicationPaths.DataPath, "trickplay"), Path.Combine("Data", "trickplay"));
|
||||
}
|
||||
|
||||
if (backupOptions.Metadata)
|
||||
{
|
||||
CopyDirectory(Path.Combine(_applicationPaths.InternalMetadataPath), Path.Combine("Data", "metadata"));
|
||||
}
|
||||
|
||||
var manifestStream = zipArchive.CreateEntry(ManifestEntryName).Open();
|
||||
await using (manifestStream.ConfigureAwait(false))
|
||||
{
|
||||
await JsonSerializer.SerializeAsync(manifestStream, manifest).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
CopyDirectory(Path.Combine(_applicationPaths.ConfigurationDirectoryPath, "users"), Path.Combine("Config", "users"));
|
||||
CopyDirectory(Path.Combine(_applicationPaths.ConfigurationDirectoryPath, "ScheduledTasks"), Path.Combine("Config", "ScheduledTasks"));
|
||||
CopyDirectory(Path.Combine(_applicationPaths.RootFolderPath), "Root");
|
||||
CopyDirectory(Path.Combine(_applicationPaths.DataPath, "collections"), Path.Combine("Data", "collections"));
|
||||
CopyDirectory(Path.Combine(_applicationPaths.DataPath, "playlists"), Path.Combine("Data", "playlists"));
|
||||
CopyDirectory(Path.Combine(_applicationPaths.DataPath, "ScheduledTasks"), Path.Combine("Data", "ScheduledTasks"));
|
||||
if (backupOptions.Subtitles)
|
||||
{
|
||||
CopyDirectory(Path.Combine(_applicationPaths.DataPath, "subtitles"), Path.Combine("Data", "subtitles"));
|
||||
}
|
||||
|
||||
if (backupOptions.Trickplay)
|
||||
{
|
||||
CopyDirectory(Path.Combine(_applicationPaths.DataPath, "trickplay"), Path.Combine("Data", "trickplay"));
|
||||
}
|
||||
|
||||
if (backupOptions.Metadata)
|
||||
{
|
||||
CopyDirectory(Path.Combine(_applicationPaths.InternalMetadataPath), Path.Combine("Data", "metadata"));
|
||||
}
|
||||
|
||||
var manifestStream = zipArchive.CreateEntry(ManifestEntryName).Open();
|
||||
await using (manifestStream.ConfigureAwait(false))
|
||||
{
|
||||
await JsonSerializer.SerializeAsync(manifestStream, manifest).ConfigureAwait(false);
|
||||
}
|
||||
_logger.LogInformation("Backup created");
|
||||
return Map(manifest, backupPath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to create backup, removing {BackupPath}", backupPath);
|
||||
try
|
||||
{
|
||||
if (File.Exists(backupPath))
|
||||
{
|
||||
File.Delete(backupPath);
|
||||
}
|
||||
}
|
||||
catch (Exception innerEx)
|
||||
{
|
||||
_logger.LogWarning(innerEx, "Unable to remove failed backup");
|
||||
}
|
||||
|
||||
_logger.LogInformation("Backup created");
|
||||
return Map(manifest, backupPath);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -422,7 +449,7 @@ public class BackupService : IBackupService
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Tried to load archive from {Path} but failed.", archivePath);
|
||||
_logger.LogWarning(ex, "Tried to load manifest from archive {Path} but failed", archivePath);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -459,7 +486,7 @@ public class BackupService : IBackupService
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Could not load {BackupArchive} path.", item);
|
||||
_logger.LogWarning(ex, "Tried to load manifest from archive {Path} but failed", item);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -614,6 +614,13 @@ public sealed class BaseItemRepository
|
||||
else
|
||||
{
|
||||
context.BaseItemProviders.Where(e => e.ItemId == entity.Id).ExecuteDelete();
|
||||
context.BaseItemImageInfos.Where(e => e.ItemId == entity.Id).ExecuteDelete();
|
||||
|
||||
if (entity.Images is { Count: > 0 })
|
||||
{
|
||||
context.BaseItemImageInfos.AddRange(entity.Images);
|
||||
}
|
||||
|
||||
context.BaseItems.Attach(entity).State = EntityState.Modified;
|
||||
}
|
||||
}
|
||||
@@ -1756,7 +1763,8 @@ public sealed class BaseItemRepository
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(filter.Path))
|
||||
{
|
||||
baseQuery = baseQuery.Where(e => e.Path == filter.Path);
|
||||
var pathToQuery = GetPathToSave(filter.Path);
|
||||
baseQuery = baseQuery.Where(e => e.Path == pathToQuery);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(filter.PresentationUniqueKey))
|
||||
@@ -2046,7 +2054,7 @@ public sealed class BaseItemRepository
|
||||
|
||||
if (filter.ExcludeArtistIds.Length > 0)
|
||||
{
|
||||
baseQuery = baseQuery.WhereReferencedItem(context, ItemValueType.Artist, filter.ExcludeArtistIds, true);
|
||||
baseQuery = baseQuery.WhereReferencedItemMultipleTypes(context, [ItemValueType.Artist, ItemValueType.AlbumArtist], filter.ExcludeArtistIds, true);
|
||||
}
|
||||
|
||||
if (filter.GenreIds.Count > 0)
|
||||
@@ -2353,17 +2361,23 @@ public sealed class BaseItemRepository
|
||||
|
||||
if (filter.HasImdbId.HasValue)
|
||||
{
|
||||
baseQuery = baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId == "imdb"));
|
||||
baseQuery = filter.HasImdbId.Value
|
||||
? baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId.ToLower() == MetadataProvider.Imdb.ToString().ToLower()))
|
||||
: baseQuery.Where(e => e.Provider!.All(f => f.ProviderId.ToLower() != MetadataProvider.Imdb.ToString().ToLower()));
|
||||
}
|
||||
|
||||
if (filter.HasTmdbId.HasValue)
|
||||
{
|
||||
baseQuery = baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId == "tmdb"));
|
||||
baseQuery = filter.HasTmdbId.Value
|
||||
? baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId.ToLower() == MetadataProvider.Tmdb.ToString().ToLower()))
|
||||
: baseQuery.Where(e => e.Provider!.All(f => f.ProviderId.ToLower() != MetadataProvider.Tmdb.ToString().ToLower()));
|
||||
}
|
||||
|
||||
if (filter.HasTvdbId.HasValue)
|
||||
{
|
||||
baseQuery = baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId == "tvdb"));
|
||||
baseQuery = filter.HasTvdbId.Value
|
||||
? baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId.ToLower() == MetadataProvider.Tvdb.ToString().ToLower()))
|
||||
: baseQuery.Where(e => e.Provider!.All(f => f.ProviderId.ToLower() != MetadataProvider.Tvdb.ToString().ToLower()));
|
||||
}
|
||||
|
||||
var queryTopParentIds = filter.TopParentIds;
|
||||
|
||||
@@ -14,7 +14,7 @@ public static class StorageHelper
|
||||
{
|
||||
private const long TwoGigabyte = 2_147_483_647L;
|
||||
private const long FiveHundredAndTwelveMegaByte = 536_870_911L;
|
||||
private static readonly string[] _byteHumanizedSuffixes = ["B", "KB", "MB", "GB", "TB", "PB", "EB"];
|
||||
private static readonly string[] _byteHumanizedSuffixes = ["B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB"];
|
||||
|
||||
/// <summary>
|
||||
/// Tests the available storage capacity on the jellyfin paths with estimated minimum values.
|
||||
@@ -27,7 +27,7 @@ public static class StorageHelper
|
||||
TestDataDirectorySize(applicationPaths.LogDirectoryPath, logger, FiveHundredAndTwelveMegaByte);
|
||||
TestDataDirectorySize(applicationPaths.CachePath, logger, TwoGigabyte);
|
||||
TestDataDirectorySize(applicationPaths.ProgramDataPath, logger, TwoGigabyte);
|
||||
TestDataDirectorySize(applicationPaths.TempDirectory, logger, TwoGigabyte);
|
||||
TestDataDirectorySize(applicationPaths.TempDirectory, logger, FiveHundredAndTwelveMegaByte);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -77,7 +77,7 @@ public static class StorageHelper
|
||||
var drive = new DriveInfo(path);
|
||||
if (threshold != -1 && drive.AvailableFreeSpace < threshold)
|
||||
{
|
||||
throw new InvalidOperationException($"The path `{path}` has insufficient free space. Required: at least {HumanizeStorageSize(threshold)}.");
|
||||
throw new InvalidOperationException($"The path `{path}` has insufficient free space. Available: {HumanizeStorageSize(drive.AvailableFreeSpace)}, Required: {HumanizeStorageSize(threshold)}.");
|
||||
}
|
||||
|
||||
logger.LogInformation(
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
|
||||
@@ -1,151 +0,0 @@
|
||||
// The MIT License (MIT)
|
||||
//
|
||||
// Copyright (c) .NET Foundation and Contributors
|
||||
//
|
||||
// All rights reserved.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http.Extensions;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
|
||||
namespace Jellyfin.Server.Infrastructure
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public class SymlinkFollowingPhysicalFileResultExecutor : PhysicalFileResultExecutor
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SymlinkFollowingPhysicalFileResultExecutor"/> class.
|
||||
/// </summary>
|
||||
/// <param name="loggerFactory">An instance of the <see cref="ILoggerFactory"/> interface.</param>
|
||||
public SymlinkFollowingPhysicalFileResultExecutor(ILoggerFactory loggerFactory) : base(loggerFactory)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override FileMetadata GetFileInfo(string path)
|
||||
{
|
||||
var fileInfo = new FileInfo(path);
|
||||
var length = fileInfo.Length;
|
||||
// This may or may not be fixed in .NET 6, but looks like it will not https://github.com/dotnet/aspnetcore/issues/34371
|
||||
if ((fileInfo.Attributes & FileAttributes.ReparsePoint) == FileAttributes.ReparsePoint)
|
||||
{
|
||||
using var fileHandle = File.OpenHandle(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
|
||||
length = RandomAccess.GetLength(fileHandle);
|
||||
}
|
||||
|
||||
return new FileMetadata
|
||||
{
|
||||
Exists = fileInfo.Exists,
|
||||
Length = length,
|
||||
LastModified = fileInfo.LastWriteTimeUtc
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task WriteFileAsync(ActionContext context, PhysicalFileResult result, RangeItemHeaderValue? range, long rangeLength)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(context);
|
||||
ArgumentNullException.ThrowIfNull(result);
|
||||
|
||||
if (range is not null && rangeLength == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// It's a bit of wasted IO to perform this check again, but non-symlinks shouldn't use this code
|
||||
if (!IsSymLink(result.FileName))
|
||||
{
|
||||
await base.WriteFileAsync(context, result, range, rangeLength).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
var response = context.HttpContext.Response;
|
||||
|
||||
if (range is not null)
|
||||
{
|
||||
await SendFileAsync(
|
||||
result.FileName,
|
||||
response,
|
||||
offset: range.From ?? 0L,
|
||||
count: rangeLength).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
await SendFileAsync(
|
||||
result.FileName,
|
||||
response,
|
||||
offset: 0,
|
||||
count: null).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task SendFileAsync(string filePath, HttpResponse response, long offset, long? count, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var fileInfo = GetFileInfo(filePath);
|
||||
if (offset < 0 || offset > fileInfo.Length)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(offset), offset, string.Empty);
|
||||
}
|
||||
|
||||
if (count.HasValue
|
||||
&& (count.Value < 0 || count.Value > fileInfo.Length - offset))
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(count), count, string.Empty);
|
||||
}
|
||||
|
||||
// Copied from SendFileFallback.SendFileAsync
|
||||
const int BufferSize = 1024 * 16;
|
||||
|
||||
var useRequestAborted = !cancellationToken.CanBeCanceled;
|
||||
var localCancel = useRequestAborted ? response.HttpContext.RequestAborted : cancellationToken;
|
||||
|
||||
var fileStream = new FileStream(
|
||||
filePath,
|
||||
FileMode.Open,
|
||||
FileAccess.Read,
|
||||
FileShare.ReadWrite,
|
||||
bufferSize: BufferSize,
|
||||
options: FileOptions.Asynchronous | FileOptions.SequentialScan);
|
||||
await using (fileStream.ConfigureAwait(false))
|
||||
{
|
||||
try
|
||||
{
|
||||
localCancel.ThrowIfCancellationRequested();
|
||||
fileStream.Seek(offset, SeekOrigin.Begin);
|
||||
await StreamCopyOperation
|
||||
.CopyToAsync(fileStream, response.Body, count, BufferSize, localCancel)
|
||||
.ConfigureAwait(true);
|
||||
}
|
||||
catch (OperationCanceledException) when (useRequestAborted)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsSymLink(string path) => (File.GetAttributes(path) & FileAttributes.ReparsePoint) == FileAttributes.ReparsePoint;
|
||||
}
|
||||
}
|
||||
@@ -55,9 +55,25 @@ namespace Jellyfin.Server.Migrations.Routines
|
||||
};
|
||||
|
||||
var dataPath = _paths.DataPath;
|
||||
using (var connection = new SqliteConnection($"Filename={Path.Combine(dataPath, DbFilename)}"))
|
||||
var activityLogPath = Path.Combine(dataPath, DbFilename);
|
||||
if (!File.Exists(activityLogPath))
|
||||
{
|
||||
_logger.LogWarning("{ActivityLogDb} doesn't exist, nothing to migrate", activityLogPath);
|
||||
return;
|
||||
}
|
||||
|
||||
using (var connection = new SqliteConnection($"Filename={activityLogPath}"))
|
||||
{
|
||||
connection.Open();
|
||||
var tableQuery = connection.Query("SELECT count(*) FROM sqlite_master WHERE type='table' AND name='ActivityLog';");
|
||||
foreach (var row in tableQuery)
|
||||
{
|
||||
if (row.GetInt32(0) == 0)
|
||||
{
|
||||
_logger.LogWarning("Table 'ActivityLog' doesn't exist in {ActivityLogPath}, nothing to migrate", activityLogPath);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
using var userDbConnection = new SqliteConnection($"Filename={Path.Combine(dataPath, "users.db")}");
|
||||
userDbConnection.Open();
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -122,6 +122,16 @@ public class MigrateKeyframeData : IDatabaseMigrationRoutine
|
||||
{
|
||||
lastWriteTimeUtc = File.GetLastWriteTimeUtc(filePath);
|
||||
}
|
||||
catch (ArgumentOutOfRangeException e)
|
||||
{
|
||||
_logger.LogDebug("Skipping {Path}: {Exception}", filePath, e.Message);
|
||||
return null;
|
||||
}
|
||||
catch (UnauthorizedAccessException e)
|
||||
{
|
||||
_logger.LogDebug("Skipping {Path}: {Exception}", filePath, e.Message);
|
||||
return null;
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
_logger.LogDebug("Skipping {Path}: {Exception}", filePath, e.Message);
|
||||
@@ -135,14 +145,21 @@ public class MigrateKeyframeData : IDatabaseMigrationRoutine
|
||||
return Path.Join(keyframeCachePath, prefix, filename);
|
||||
}
|
||||
|
||||
private static bool TryReadFromCache(string? cachePath, [NotNullWhen(true)] out MediaEncoding.Keyframes.KeyframeData? cachedResult)
|
||||
private bool TryReadFromCache(string? cachePath, [NotNullWhen(true)] out MediaEncoding.Keyframes.KeyframeData? cachedResult)
|
||||
{
|
||||
if (File.Exists(cachePath))
|
||||
{
|
||||
var bytes = File.ReadAllBytes(cachePath);
|
||||
cachedResult = JsonSerializer.Deserialize<MediaEncoding.Keyframes.KeyframeData>(bytes, _jsonOptions);
|
||||
try
|
||||
{
|
||||
var bytes = File.ReadAllBytes(cachePath);
|
||||
cachedResult = JsonSerializer.Deserialize<MediaEncoding.Keyframes.KeyframeData>(bytes, _jsonOptions);
|
||||
|
||||
return cachedResult is not null;
|
||||
return cachedResult is not null;
|
||||
}
|
||||
catch (JsonException jsonException)
|
||||
{
|
||||
_logger.LogWarning(jsonException, "Failed to read {Path}", cachePath);
|
||||
}
|
||||
}
|
||||
|
||||
cachedResult = null;
|
||||
|
||||
@@ -57,11 +57,28 @@ public class MigrateUserDb : IMigrationRoutine
|
||||
public void Perform()
|
||||
{
|
||||
var dataPath = _paths.DataPath;
|
||||
var userDbPath = Path.Combine(dataPath, DbFilename);
|
||||
if (!File.Exists(userDbPath))
|
||||
{
|
||||
_logger.LogWarning("{UserDbPath} doesn't exist, nothing to migrate", userDbPath);
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.LogInformation("Migrating the user database may take a while, do not stop Jellyfin.");
|
||||
|
||||
using (var connection = new SqliteConnection($"Filename={Path.Combine(dataPath, DbFilename)}"))
|
||||
using (var connection = new SqliteConnection($"Filename={userDbPath}"))
|
||||
{
|
||||
connection.Open();
|
||||
var tableQuery = connection.Query("SELECT count(*) FROM sqlite_master WHERE type='table' AND name='LocalUsersv2';");
|
||||
foreach (var row in tableQuery)
|
||||
{
|
||||
if (row.GetInt32(0) == 0)
|
||||
{
|
||||
_logger.LogWarning("Table 'LocalUsersv2' doesn't exist in {UserDbPath}, nothing to migrate", userDbPath);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
using var dbContext = _provider.CreateDbContext();
|
||||
|
||||
var queryResult = connection.Query("SELECT * FROM LocalUsersv2");
|
||||
|
||||
@@ -224,6 +224,18 @@ public class MoveExtractedFiles : IAsyncMigrationRoutine
|
||||
|
||||
return null;
|
||||
}
|
||||
catch (UnauthorizedAccessException e)
|
||||
{
|
||||
_logger.LogDebug("Skipping subtitle at index {Index} for {Path}: {Exception}", attachmentStreamIndex, mediaPath, e.Message);
|
||||
|
||||
return null;
|
||||
}
|
||||
catch (ArgumentOutOfRangeException e)
|
||||
{
|
||||
_logger.LogDebug("Skipping attachment at index {Index} for {Path}: {Exception}", attachmentStreamIndex, mediaPath, e.Message);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
filename = (mediaPath + attachmentStreamIndex.ToString(CultureInfo.InvariantCulture) + "_" + date.Value.Ticks.ToString(CultureInfo.InvariantCulture)).GetMD5().ToString("D", CultureInfo.InvariantCulture);
|
||||
}
|
||||
@@ -263,6 +275,18 @@ public class MoveExtractedFiles : IAsyncMigrationRoutine
|
||||
{
|
||||
date = File.GetLastWriteTimeUtc(path);
|
||||
}
|
||||
catch (ArgumentOutOfRangeException e)
|
||||
{
|
||||
_logger.LogDebug("Skipping subtitle at index {Index} for {Path}: {Exception}", streamIndex, path, e.Message);
|
||||
|
||||
return null;
|
||||
}
|
||||
catch (UnauthorizedAccessException e)
|
||||
{
|
||||
_logger.LogDebug("Skipping subtitle at index {Index} for {Path}: {Exception}", streamIndex, path, e.Message);
|
||||
|
||||
return null;
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
_logger.LogDebug("Skipping subtitle at index {Index} for {Path}: {Exception}", streamIndex, path, e.Message);
|
||||
|
||||
@@ -184,6 +184,12 @@ namespace Jellyfin.Server
|
||||
.AddSingleton<IServiceCollection>(e))
|
||||
.Build();
|
||||
|
||||
/*
|
||||
* Initialize the transcode path marker so we avoid starting Jellyfin in a broken state.
|
||||
* This should really be a part of IApplicationPaths but this path is configured differently.
|
||||
*/
|
||||
_ = appHost.ConfigurationManager.GetTranscodePath();
|
||||
|
||||
// Re-use the host service provider in the app host since ASP.NET doesn't allow a custom service collection.
|
||||
appHost.ServiceProvider = _jellyfinHost.Services;
|
||||
PrepareDatabaseProvider(appHost.ServiceProvider);
|
||||
|
||||
@@ -250,6 +250,7 @@ public sealed class SetupServer : IDisposable
|
||||
{ "isInReportingMode", _isUnhealthy },
|
||||
{ "retryValue", retryAfterValue },
|
||||
{ "logs", startupLogEntries },
|
||||
{ "networkManagerReady", networkManager is not null },
|
||||
{ "localNetworkRequest", networkManager is not null && context.Connection.RemoteIpAddress is not null && networkManager.IsInLocalNetwork(context.Connection.RemoteIpAddress) }
|
||||
},
|
||||
new ByteCounterStream(context.Response.BodyWriter.AsStream(), IODefaults.FileStreamBufferSize, true, _startupUiRenderer.ParserOptions))
|
||||
|
||||
@@ -213,7 +213,12 @@
|
||||
</ol>
|
||||
</div>
|
||||
{{#ELSE}}
|
||||
{{#IF networkManagerReady}}
|
||||
<p>Please visit this page from your local network to view detailed startup logs.</p>
|
||||
{{#ELSE}}
|
||||
<p>Initializing network settings. Please wait.</p>
|
||||
{{/ELSE}}
|
||||
{{/IF}}
|
||||
{{/ELSE}}
|
||||
{{/IF}}
|
||||
</div>
|
||||
|
||||
@@ -16,15 +16,12 @@ using Jellyfin.Networking.HappyEyeballs;
|
||||
using Jellyfin.Server.Extensions;
|
||||
using Jellyfin.Server.HealthChecks;
|
||||
using Jellyfin.Server.Implementations.Extensions;
|
||||
using Jellyfin.Server.Infrastructure;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Extensions;
|
||||
using MediaBrowser.XbmcMetadata;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||
using Microsoft.AspNetCore.StaticFiles;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
@@ -69,8 +66,6 @@ namespace Jellyfin.Server
|
||||
options.HttpsPort = _serverApplicationHost.HttpsPort;
|
||||
});
|
||||
|
||||
// TODO remove once this is fixed upstream https://github.com/dotnet/aspnetcore/issues/34371
|
||||
services.AddSingleton<IActionResultExecutor<PhysicalFileResult>, SymlinkFollowingPhysicalFileResultExecutor>();
|
||||
services.AddJellyfinApi(_serverApplicationHost.GetApiPluginAssemblies(), _serverConfigurationManager.GetNetworkConfiguration());
|
||||
services.AddJellyfinDbContext(_serverApplicationHost.ConfigurationManager, _configuration);
|
||||
services.AddJellyfinApiSwagger();
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<PropertyGroup>
|
||||
<Authors>Jellyfin Contributors</Authors>
|
||||
<PackageId>Jellyfin.Common</PackageId>
|
||||
<VersionPrefix>10.11.0</VersionPrefix>
|
||||
<VersionPrefix>10.11.2</VersionPrefix>
|
||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -715,9 +715,18 @@ namespace MediaBrowser.Controller.Entities
|
||||
}
|
||||
else
|
||||
{
|
||||
items = GetRecursiveChildren(user, query, out totalCount);
|
||||
// Save pagination params before clearing them to prevent pagination from happening
|
||||
// before sorting. PostFilterAndSort will apply pagination after sorting.
|
||||
var limit = query.Limit;
|
||||
var startIndex = query.StartIndex;
|
||||
query.Limit = null;
|
||||
query.StartIndex = null; // override these here as they have already been applied
|
||||
query.StartIndex = null;
|
||||
|
||||
items = GetRecursiveChildren(user, query, out totalCount);
|
||||
|
||||
// Restore pagination params so PostFilterAndSort can apply them after sorting
|
||||
query.Limit = limit;
|
||||
query.StartIndex = startIndex;
|
||||
}
|
||||
|
||||
var result = PostFilterAndSort(items, query);
|
||||
@@ -980,20 +989,16 @@ namespace MediaBrowser.Controller.Entities
|
||||
else
|
||||
{
|
||||
// need to pass this param to the children.
|
||||
// Note: Don't pass Limit/StartIndex here as pagination should happen after sorting in PostFilterAndSort
|
||||
var childQuery = new InternalItemsQuery
|
||||
{
|
||||
DisplayAlbumFolders = query.DisplayAlbumFolders,
|
||||
Limit = query.Limit,
|
||||
StartIndex = query.StartIndex,
|
||||
NameStartsWith = query.NameStartsWith,
|
||||
NameStartsWithOrGreater = query.NameStartsWithOrGreater,
|
||||
NameLessThan = query.NameLessThan
|
||||
};
|
||||
|
||||
items = GetChildren(user, true, out totalItemCount, childQuery).Where(filter);
|
||||
|
||||
query.Limit = null;
|
||||
query.StartIndex = null;
|
||||
}
|
||||
|
||||
var result = PostFilterAndSort(items, query);
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<PropertyGroup>
|
||||
<Authors>Jellyfin Contributors</Authors>
|
||||
<PackageId>Jellyfin.Controller</PackageId>
|
||||
<VersionPrefix>10.11.0</VersionPrefix>
|
||||
<VersionPrefix>10.11.2</VersionPrefix>
|
||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -2390,8 +2390,8 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
|| (requestHasSDR && videoStream.VideoRangeType == VideoRangeType.DOVIWithSDR)
|
||||
|| (requestHasHDR10 && videoStream.VideoRangeType == VideoRangeType.HDR10Plus)))
|
||||
{
|
||||
// If the video stream is in a static HDR format, don't allow copy if the client does not support HDR10 or HLG.
|
||||
if (videoStream.VideoRangeType is VideoRangeType.HDR10 or VideoRangeType.HLG)
|
||||
// If the video stream is in HDR10+ or a static HDR format, don't allow copy if the client does not support HDR10 or HLG.
|
||||
if (videoStream.VideoRangeType is VideoRangeType.HDR10Plus or VideoRangeType.HDR10 or VideoRangeType.HLG)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1122,7 +1122,15 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
private void StartProcess(ProcessWrapper process)
|
||||
{
|
||||
process.Process.Start();
|
||||
process.Process.PriorityClass = ProcessPriorityClass.BelowNormal;
|
||||
|
||||
try
|
||||
{
|
||||
process.Process.PriorityClass = ProcessPriorityClass.BelowNormal;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Unable to set process priority to BelowNormal for {ProcessFileName}", process.Process.StartInfo.FileName);
|
||||
}
|
||||
|
||||
lock (_runningProcessesLock)
|
||||
{
|
||||
|
||||
@@ -930,6 +930,15 @@ namespace MediaBrowser.MediaEncoding.Probing
|
||||
{
|
||||
stream.Rotation = data.Rotation;
|
||||
}
|
||||
|
||||
// Parse video frame cropping metadata from side_data
|
||||
// TODO: save them and make HW filters to apply them in HWA pipelines
|
||||
else if (string.Equals(data.SideDataType, "Frame Cropping", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// Streams containing artificially added frame cropping
|
||||
// metadata should not be marked as anamorphic.
|
||||
stream.IsAnamorphic = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<PropertyGroup>
|
||||
<Authors>Jellyfin Contributors</Authors>
|
||||
<PackageId>Jellyfin.Model</PackageId>
|
||||
<VersionPrefix>10.11.0</VersionPrefix>
|
||||
<VersionPrefix>10.11.2</VersionPrefix>
|
||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -229,6 +229,11 @@ namespace MediaBrowser.Providers.Manager
|
||||
if (file is not null)
|
||||
{
|
||||
item.DateModified = file.LastWriteTimeUtc;
|
||||
|
||||
if (!file.IsDirectory)
|
||||
{
|
||||
item.Size = file.Length;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -138,6 +138,8 @@ namespace MediaBrowser.Providers.Plugins.Omdb
|
||||
}
|
||||
|
||||
var item = itemResult.Item;
|
||||
item.IndexNumber = episodeNumber;
|
||||
item.ParentIndexNumber = seasonNumber;
|
||||
|
||||
var seasonResult = await GetSeasonRootObject(seriesImdbId, seasonNumber, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
|
||||
@@ -177,8 +177,8 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
|
||||
|
||||
var item = new Episode
|
||||
{
|
||||
IndexNumber = info.IndexNumber,
|
||||
ParentIndexNumber = info.ParentIndexNumber,
|
||||
IndexNumber = episodeNumber,
|
||||
ParentIndexNumber = seasonNumber,
|
||||
IndexNumberEnd = info.IndexNumberEnd,
|
||||
Name = episodeResult.Name,
|
||||
PremiereDate = episodeResult.AirDate,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Reflection;
|
||||
|
||||
[assembly: AssemblyVersion("10.11.0")]
|
||||
[assembly: AssemblyFileVersion("10.11.0")]
|
||||
[assembly: AssemblyVersion("10.11.2")]
|
||||
[assembly: AssemblyFileVersion("10.11.2")]
|
||||
|
||||
@@ -58,7 +58,7 @@ for subproject in ${jellyfin_subprojects[@]}; do
|
||||
done
|
||||
|
||||
# Set the version in the GitHub issue template file
|
||||
sed -i "s|${old_version}|${new_version_sed}|g" ${issue_template_file}
|
||||
sed -i "s|${old_version}|${new_version_sed}|g" "${issue_template_file}"
|
||||
|
||||
# Stage the changed files for commit
|
||||
git add .
|
||||
|
||||
@@ -70,13 +70,14 @@ public static class JellyfinQueryHelperExtensions
|
||||
bool invert = false)
|
||||
{
|
||||
var itemFilter = OneOrManyExpressionBuilder<BaseItemEntity, Guid>(referenceIds, f => f.Id);
|
||||
var typeFilter = OneOrManyExpressionBuilder<ItemValue, ItemValueType>(itemValueTypes, iv => iv.Type);
|
||||
|
||||
return baseQuery.Where(item =>
|
||||
context.ItemValues
|
||||
.Where(typeFilter)
|
||||
.Join(context.ItemValuesMap, e => e.ItemValueId, e => e.ItemValueId, (itemVal, map) => new { itemVal, map })
|
||||
.Any(val =>
|
||||
itemValueTypes.Contains(val.itemVal.Type)
|
||||
&& context.BaseItems.Where(itemFilter).Any(e => e.CleanName == val.itemVal.CleanValue)
|
||||
context.BaseItems.Where(itemFilter).Any(e => e.CleanName == val.itemVal.CleanValue)
|
||||
&& val.map.ItemId == item.Id) == EF.Constant(!invert));
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<PropertyGroup>
|
||||
<Authors>Jellyfin Contributors</Authors>
|
||||
<PackageId>Jellyfin.Extensions</PackageId>
|
||||
<VersionPrefix>10.11.0</VersionPrefix>
|
||||
<VersionPrefix>10.11.2</VersionPrefix>
|
||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -42,7 +42,15 @@ public static class FfProbeKeyframeExtractor
|
||||
try
|
||||
{
|
||||
process.Start();
|
||||
process.PriorityClass = ProcessPriorityClass.BelowNormal;
|
||||
try
|
||||
{
|
||||
process.PriorityClass = ProcessPriorityClass.BelowNormal;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// We do not care if process priority setting fails
|
||||
// Ideally log a warning but this does not have a logger available
|
||||
}
|
||||
|
||||
return ParseStream(process.StandardOutput);
|
||||
}
|
||||
|
||||
@@ -7,23 +7,38 @@ public class SeasonPathParserTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("/Drive/Season 1", "/Drive", 1, true)]
|
||||
[InlineData("/Drive/SEASON 1", "/Drive", 1, true)]
|
||||
[InlineData("/Drive/Staffel 1", "/Drive", 1, true)]
|
||||
[InlineData("/Drive/STAFFEL 1", "/Drive", 1, true)]
|
||||
[InlineData("/Drive/Stagione 1", "/Drive", 1, true)]
|
||||
[InlineData("/Drive/STAGIONE 1", "/Drive", 1, true)]
|
||||
[InlineData("/Drive/sæson 1", "/Drive", 1, true)]
|
||||
[InlineData("/Drive/SÆSON 1", "/Drive", 1, true)]
|
||||
[InlineData("/Drive/Temporada 1", "/Drive", 1, true)]
|
||||
[InlineData("/Drive/TEMPORADA 1", "/Drive", 1, true)]
|
||||
[InlineData("/Drive/series 1", "/Drive", 1, true)]
|
||||
[InlineData("/Drive/SERIES 1", "/Drive", 1, true)]
|
||||
[InlineData("/Drive/Kausi 1", "/Drive", 1, true)]
|
||||
[InlineData("/Drive/KAUSI 1", "/Drive", 1, true)]
|
||||
[InlineData("/Drive/Säsong 1", "/Drive", 1, true)]
|
||||
[InlineData("/Drive/SÄSONG 1", "/Drive", 1, true)]
|
||||
[InlineData("/Drive/Seizoen 1", "/Drive", 1, true)]
|
||||
[InlineData("/Drive/SEIZOEN 1", "/Drive", 1, true)]
|
||||
[InlineData("/Drive/Seasong 1", "/Drive", 1, true)]
|
||||
[InlineData("/Drive/SEASONG 1", "/Drive", 1, true)]
|
||||
[InlineData("/Drive/Sezon 1", "/Drive", 1, true)]
|
||||
[InlineData("/Drive/SEZON 1", "/Drive", 1, true)]
|
||||
[InlineData("/Drive/sezona 1", "/Drive", 1, true)]
|
||||
[InlineData("/Drive/SEZONA 1", "/Drive", 1, true)]
|
||||
[InlineData("/Drive/sezóna 1", "/Drive", 1, true)]
|
||||
[InlineData("/Drive/SEZÓNA 1", "/Drive", 1, true)]
|
||||
[InlineData("/Drive/Sezonul 1", "/Drive", 1, true)]
|
||||
[InlineData("/Drive/SEZONUL 1", "/Drive", 1, true)]
|
||||
[InlineData("/Drive/시즌 1", "/Drive", 1, true)]
|
||||
[InlineData("/Drive/シーズン 1", "/Drive", 1, true)]
|
||||
[InlineData("/Drive/сезон 1", "/Drive", 1, true)]
|
||||
[InlineData("/Drive/Сезон 1", "/Drive", 1, true)]
|
||||
[InlineData("/Drive/СЕЗОН 1", "/Drive", 1, true)]
|
||||
[InlineData("/Drive/Season 10", "/Drive", 10, true)]
|
||||
[InlineData("/Drive/Season 100", "/Drive", 100, true)]
|
||||
[InlineData("/Drive/s1", "/Drive", 1, true)]
|
||||
@@ -46,8 +61,14 @@ public class SeasonPathParserTests
|
||||
[InlineData("/Drive/s06e05", "/Drive", null, false)]
|
||||
[InlineData("/Drive/The.Legend.of.Condor.Heroes.2017.V2.web-dl.1080p.h264.aac-hdctv", "/Drive", null, false)]
|
||||
[InlineData("/Drive/extras", "/Drive", 0, true)]
|
||||
[InlineData("/Drive/EXTRAS", "/Drive", 0, true)]
|
||||
[InlineData("/Drive/specials", "/Drive", 0, true)]
|
||||
[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);
|
||||
|
||||
Reference in New Issue
Block a user