Compare commits
236 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ec81dc9be2 | |||
| 45f3fb1cfc | |||
| f83a24ec43 | |||
| 84c03a2d93 | |||
| bc8e249080 | |||
| 5ea9a74289 | |||
| 987d31ea16 | |||
| f850779781 | |||
| 3bdc2bff5f | |||
| 5c6a84549a | |||
| 48da35f91f | |||
| 39b29eb9f1 | |||
| a6740bf51e | |||
| c7797d3ead | |||
| c86d5838be | |||
| 43223b9036 | |||
| c71385d2db | |||
| ad5becc524 | |||
| 7c75dcfb9c | |||
| 7937e31a9b | |||
| 1f1f26306b | |||
| 577399ca05 | |||
| e7ea7c0383 | |||
| 9fe7751d05 | |||
| bf129ab9b8 | |||
| 6d23de64c0 | |||
| e4f48bb486 | |||
| 866b4460b1 | |||
| 9db0b275ff | |||
| 14008fd7d0 | |||
| 8532d88a71 | |||
| 737c739d33 | |||
| 679e83082f | |||
| d8e53f35a5 | |||
| 7a0e7b3cf8 | |||
| 373c63bcc7 | |||
| be5d343efb | |||
| c9c91cc34e | |||
| 774b4a0d3f | |||
| a26cded0f5 | |||
| 4ec82ec662 | |||
| 879787212e | |||
| 23100c9b86 | |||
| e6124bc154 | |||
| 8753b7200f | |||
| 88d5230bab | |||
| 2920c52d61 | |||
| de196a7687 | |||
| ba026716c1 | |||
| 125ee88311 | |||
| c53f6a2890 | |||
| 649b4c49e0 | |||
| 848ea703bc | |||
| 0adadff3e7 | |||
| ffdc3a6734 | |||
| a51cd4f8db | |||
| af87706379 | |||
| 8422ab687b | |||
| 64753cfc7f | |||
| 632fb05f46 | |||
| 527ed0607d | |||
| b59daab273 | |||
| 8f28d52929 | |||
| 749b263c48 | |||
| 80c68b8948 | |||
| a5687793c9 | |||
| b344771f8a | |||
| 3ff78b687d | |||
| d260f30810 | |||
| 7ffdde9a0b | |||
| e14194bfe2 | |||
| 3bf1a7e445 | |||
| 1faee43b11 | |||
| 31f9938e3a | |||
| ae9fd4ab35 | |||
| 71ed7f7676 | |||
| 3b6e003029 | |||
| 9357d610b1 | |||
| 1d4755894e | |||
| 2320f06666 | |||
| 8296f07a39 | |||
| 30f6263806 | |||
| a9249393e1 | |||
| f49a051a5f | |||
| 5bcab0f0f8 | |||
| c5a2ff8ac4 | |||
| 494ed7e4d2 | |||
| dd97e6bc45 | |||
| 7323ccfc23 | |||
| d258a87fda | |||
| 77a007a24d | |||
| a380153f92 | |||
| 56c81696d3 | |||
| 7297431f23 | |||
| f2c7bccb89 | |||
| b0b4068ddf | |||
| 3bd2cc9860 | |||
| feb035b9e0 | |||
| 82f362abd9 | |||
| 04b73cace6 | |||
| 3b69f38a1f | |||
| 126da94020 | |||
| f9dffa767f | |||
| 444b0ea310 | |||
| 484427b4aa | |||
| c3f0649fde | |||
| e877486056 | |||
| 9854751137 | |||
| 057e8ef240 | |||
| 205783f46f | |||
| b2fb96ffed | |||
| ee22feb89a | |||
| ca5979cd77 | |||
| d36f49589a | |||
| 70f37f0527 | |||
| dfe0aef530 | |||
| 9e31d5a73f | |||
| f088ca5555 | |||
| 2b46917dcf | |||
| 7bae6eff95 | |||
| d0fd23bb4b | |||
| d694a6c09a | |||
| 58f61ed118 | |||
| b9da0e7f83 | |||
| 7eaa0600e0 | |||
| 47c2c536e4 | |||
| 7ef9e95d75 | |||
| f8ea4577ab | |||
| 72da42cb0a | |||
| dbfa0f3027 | |||
| 78f437401b | |||
| 1db748399c | |||
| a41c67d16b | |||
| 84a1674f39 | |||
| 81e535fc62 | |||
| f9d26ea1bc | |||
| 5f3dbd8294 | |||
| 9cebdfdec0 | |||
| 891ccd7bb2 | |||
| 54778d875d | |||
| 39d185c7b1 | |||
| a7d45b5d3a | |||
| 7efa4e38c1 | |||
| 506ed6940b | |||
| 5dbe16d3e6 | |||
| 4c178e9188 | |||
| 50bc41d84d | |||
| e931f5a32b | |||
| d2caed25fb | |||
| 3f37ef70e1 | |||
| d342b79218 | |||
| c35fc382d4 | |||
| cb6e6879e2 | |||
| a71b190142 | |||
| 910df89cce | |||
| 5f15339919 | |||
| 56e7b323de | |||
| a3a751a4f5 | |||
| c85255a615 | |||
| 8ea8dcf128 | |||
| 7884e7e829 | |||
| 3478554249 | |||
| 9898c10880 | |||
| ec2ad4ec8c | |||
| 1ffc77b43d | |||
| ae79bbc34c | |||
| 52704e8dd0 | |||
| 294ab0757e | |||
| bdd52df230 | |||
| 73117b079c | |||
| b60905f991 | |||
| 6d5c697183 | |||
| 24c56328f2 | |||
| ef037ad371 | |||
| a64e21f57a | |||
| 56e135f5e6 | |||
| ae22d0b7a5 | |||
| b36543275f | |||
| 2c0c3eb3ee | |||
| f1d56aa5ce | |||
| 0b6fbebf72 | |||
| db714f967e | |||
| f020bd6f3b | |||
| 3275f83c3b | |||
| f7813803c2 | |||
| 477b922e4a | |||
| be72001ff9 | |||
| 6749313249 | |||
| 4ebe70cf6a | |||
| c4051ac16d | |||
| 3491f0968b | |||
| fd4ffc6ba3 | |||
| 5912a49d1d | |||
| f336647d57 | |||
| 9b805c9e83 | |||
| 19ccf414ac | |||
| 0504ed9fe6 | |||
| c243f588a0 | |||
| 46491d0813 | |||
| c7c0cdad95 | |||
| 255f5a6707 | |||
| 3f497459cb | |||
| 5d66c84f2d | |||
| 052a59ac3e | |||
| 1b8a251991 | |||
| 1a787e273a | |||
| 16fba6035c | |||
| d73e9f3af5 | |||
| 2888080098 | |||
| c8282e8441 | |||
| b295b0478c | |||
| 42aaea3556 | |||
| 07b39655eb | |||
| 0f75f17736 | |||
| 83d8dbf93e | |||
| e5aa708cb9 | |||
| ac760b9c86 | |||
| c07c7b753c | |||
| 8b69b0f521 | |||
| 079fac4a54 | |||
| 21afec3225 | |||
| 11c5a0b182 | |||
| f318417c4f | |||
| 874fcaba69 | |||
| 5204863705 | |||
| 0f7ba42987 | |||
| 3e8fe1ce11 | |||
| 044ff0542b | |||
| abfbd04782 | |||
| 8d0024ec49 | |||
| 8f761a64f5 | |||
| a64eebe79f | |||
| 8bb4cd017c | |||
| c5f09ab650 | |||
| 99df9c8fcd | |||
| 4b563f4d7e |
@@ -36,6 +36,7 @@
|
|||||||
- [dmitrylyzo](https://github.com/dmitrylyzo)
|
- [dmitrylyzo](https://github.com/dmitrylyzo)
|
||||||
- [DMouse10462](https://github.com/DMouse10462)
|
- [DMouse10462](https://github.com/DMouse10462)
|
||||||
- [DrPandemic](https://github.com/DrPandemic)
|
- [DrPandemic](https://github.com/DrPandemic)
|
||||||
|
- [eglia](https://github.com/eglia)
|
||||||
- [EraYaN](https://github.com/EraYaN)
|
- [EraYaN](https://github.com/EraYaN)
|
||||||
- [escabe](https://github.com/escabe)
|
- [escabe](https://github.com/escabe)
|
||||||
- [excelite](https://github.com/excelite)
|
- [excelite](https://github.com/excelite)
|
||||||
@@ -157,6 +158,7 @@
|
|||||||
- [jonas-resch](https://github.com/jonas-resch)
|
- [jonas-resch](https://github.com/jonas-resch)
|
||||||
- [vgambier](https://github.com/vgambier)
|
- [vgambier](https://github.com/vgambier)
|
||||||
- [MinecraftPlaye](https://github.com/MinecraftPlaye)
|
- [MinecraftPlaye](https://github.com/MinecraftPlaye)
|
||||||
|
- [RealGreenDragon](https://github.com/RealGreenDragon)
|
||||||
|
|
||||||
# Emby Contributors
|
# Emby Contributors
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -72,7 +72,7 @@ COPY . .
|
|||||||
ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
|
ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
|
||||||
# because of changes in docker and systemd we need to not build in parallel at the moment
|
# because of changes in docker and systemd we need to not build in parallel at the moment
|
||||||
# see https://success.docker.com/article/how-to-reserve-resource-temporarily-unavailable-errors-due-to-tasksmax-setting
|
# see https://success.docker.com/article/how-to-reserve-resource-temporarily-unavailable-errors-due-to-tasksmax-setting
|
||||||
RUN dotnet publish Jellyfin.Server --disable-parallel --configuration Release --output="/jellyfin" --self-contained --runtime linux-x64 "-p:DebugSymbols=false;DebugType=none"
|
RUN dotnet publish Jellyfin.Server --disable-parallel --configuration Release --output="/jellyfin" --self-contained --runtime linux-x64 -p:DebugSymbols=false -p:DebugType=none
|
||||||
|
|
||||||
FROM app
|
FROM app
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -64,7 +64,7 @@ ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
|
|||||||
# Discard objs - may cause failures if exists
|
# Discard objs - may cause failures if exists
|
||||||
RUN find . -type d -name obj | xargs -r rm -r
|
RUN find . -type d -name obj | xargs -r rm -r
|
||||||
# Build
|
# Build
|
||||||
RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin" --self-contained --runtime linux-arm "-p:DebugSymbols=false;DebugType=none"
|
RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin" --self-contained --runtime linux-arm -p:DebugSymbols=false -p:DebugType=none
|
||||||
|
|
||||||
FROM app
|
FROM app
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -55,7 +55,7 @@ ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
|
|||||||
# Discard objs - may cause failures if exists
|
# Discard objs - may cause failures if exists
|
||||||
RUN find . -type d -name obj | xargs -r rm -r
|
RUN find . -type d -name obj | xargs -r rm -r
|
||||||
# Build
|
# Build
|
||||||
RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin" --self-contained --runtime linux-arm64 "-p:DebugSymbols=false;DebugType=none"
|
RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin" --self-contained --runtime linux-arm64 -p:DebugSymbols=false -p:DebugType=none
|
||||||
|
|
||||||
FROM app
|
FROM app
|
||||||
|
|
||||||
|
|||||||
@@ -221,6 +221,7 @@ namespace Emby.Dlna.Didl
|
|||||||
streamInfo.IsDirectStream,
|
streamInfo.IsDirectStream,
|
||||||
streamInfo.RunTimeTicks ?? 0,
|
streamInfo.RunTimeTicks ?? 0,
|
||||||
streamInfo.TargetVideoProfile,
|
streamInfo.TargetVideoProfile,
|
||||||
|
streamInfo.TargetVideoRangeType,
|
||||||
streamInfo.TargetVideoLevel,
|
streamInfo.TargetVideoLevel,
|
||||||
streamInfo.TargetFramerate ?? 0,
|
streamInfo.TargetFramerate ?? 0,
|
||||||
streamInfo.TargetPacketLength,
|
streamInfo.TargetPacketLength,
|
||||||
@@ -376,6 +377,7 @@ namespace Emby.Dlna.Didl
|
|||||||
targetHeight,
|
targetHeight,
|
||||||
streamInfo.TargetVideoBitDepth,
|
streamInfo.TargetVideoBitDepth,
|
||||||
streamInfo.TargetVideoProfile,
|
streamInfo.TargetVideoProfile,
|
||||||
|
streamInfo.TargetVideoRangeType,
|
||||||
streamInfo.TargetVideoLevel,
|
streamInfo.TargetVideoLevel,
|
||||||
streamInfo.TargetFramerate ?? 0,
|
streamInfo.TargetFramerate ?? 0,
|
||||||
streamInfo.TargetPacketLength,
|
streamInfo.TargetPacketLength,
|
||||||
|
|||||||
@@ -313,7 +313,7 @@ namespace Emby.Dlna.Main
|
|||||||
|
|
||||||
_logger.LogInformation("Registering publisher for {ResourceName} on {DeviceAddress}", fullService, address);
|
_logger.LogInformation("Registering publisher for {ResourceName} on {DeviceAddress}", fullService, address);
|
||||||
|
|
||||||
var uri = new UriBuilder(_appHost.GetApiUrlForLocalAccess(false) + descriptorUri);
|
var uri = new UriBuilder(_appHost.GetApiUrlForLocalAccess(address, false) + descriptorUri);
|
||||||
|
|
||||||
var device = new SsdpRootDevice
|
var device = new SsdpRootDevice
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -561,6 +561,7 @@ namespace Emby.Dlna.PlayTo
|
|||||||
streamInfo.IsDirectStream,
|
streamInfo.IsDirectStream,
|
||||||
streamInfo.RunTimeTicks ?? 0,
|
streamInfo.RunTimeTicks ?? 0,
|
||||||
streamInfo.TargetVideoProfile,
|
streamInfo.TargetVideoProfile,
|
||||||
|
streamInfo.TargetVideoRangeType,
|
||||||
streamInfo.TargetVideoLevel,
|
streamInfo.TargetVideoLevel,
|
||||||
streamInfo.TargetFramerate ?? 0,
|
streamInfo.TargetFramerate ?? 0,
|
||||||
streamInfo.TargetPacketLength,
|
streamInfo.TargetPacketLength,
|
||||||
|
|||||||
@@ -395,7 +395,13 @@ namespace Emby.Drawing
|
|||||||
public string GetImageBlurHash(string path)
|
public string GetImageBlurHash(string path)
|
||||||
{
|
{
|
||||||
var size = GetImageDimensions(path);
|
var size = GetImageDimensions(path);
|
||||||
if (size.Width <= 0 || size.Height <= 0)
|
return GetImageBlurHash(path, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public string GetImageBlurHash(string path, ImageDimensions imageDimensions)
|
||||||
|
{
|
||||||
|
if (imageDimensions.Width <= 0 || imageDimensions.Height <= 0)
|
||||||
{
|
{
|
||||||
return string.Empty;
|
return string.Empty;
|
||||||
}
|
}
|
||||||
@@ -403,8 +409,8 @@ namespace Emby.Drawing
|
|||||||
// We want tiles to be as close to square as possible, and to *mostly* keep under 16 tiles for performance.
|
// We want tiles to be as close to square as possible, and to *mostly* keep under 16 tiles for performance.
|
||||||
// One tile is (width / xComp) x (height / yComp) pixels, which means that ideally yComp = xComp * height / width.
|
// One tile is (width / xComp) x (height / yComp) pixels, which means that ideally yComp = xComp * height / width.
|
||||||
// See more at https://github.com/woltapp/blurhash/#how-do-i-pick-the-number-of-x-and-y-components
|
// See more at https://github.com/woltapp/blurhash/#how-do-i-pick-the-number-of-x-and-y-components
|
||||||
float xCompF = MathF.Sqrt(16.0f * size.Width / size.Height);
|
float xCompF = MathF.Sqrt(16.0f * imageDimensions.Width / imageDimensions.Height);
|
||||||
float yCompF = xCompF * size.Height / size.Width;
|
float yCompF = xCompF * imageDimensions.Height / imageDimensions.Width;
|
||||||
|
|
||||||
int xComp = Math.Min((int)xCompF + 1, 9);
|
int xComp = Math.Min((int)xCompF + 1, 9);
|
||||||
int yComp = Math.Min((int)yCompF + 1, 9);
|
int yComp = Math.Min((int)yCompF + 1, 9);
|
||||||
@@ -439,47 +445,46 @@ namespace Emby.Drawing
|
|||||||
.ToString("N", CultureInfo.InvariantCulture);
|
.ToString("N", CultureInfo.InvariantCulture);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<(string Path, DateTime DateModified)> GetSupportedImage(string originalImagePath, DateTime dateModified)
|
private Task<(string Path, DateTime DateModified)> GetSupportedImage(string originalImagePath, DateTime dateModified)
|
||||||
{
|
{
|
||||||
var inputFormat = Path.GetExtension(originalImagePath)
|
var inputFormat = Path.GetExtension(originalImagePath.AsSpan()).TrimStart('.').ToString();
|
||||||
.TrimStart('.')
|
|
||||||
.Replace("jpeg", "jpg", StringComparison.OrdinalIgnoreCase);
|
|
||||||
|
|
||||||
// These are just jpg files renamed as tbn
|
// These are just jpg files renamed as tbn
|
||||||
if (string.Equals(inputFormat, "tbn", StringComparison.OrdinalIgnoreCase))
|
if (string.Equals(inputFormat, "tbn", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
return (originalImagePath, dateModified);
|
return Task.FromResult((originalImagePath, dateModified));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!_imageEncoder.SupportedInputFormats.Contains(inputFormat))
|
// TODO _mediaEncoder.ConvertImage is not implemented
|
||||||
{
|
// if (!_imageEncoder.SupportedInputFormats.Contains(inputFormat))
|
||||||
try
|
// {
|
||||||
{
|
// try
|
||||||
string filename = (originalImagePath + dateModified.Ticks.ToString(CultureInfo.InvariantCulture)).GetMD5().ToString("N", CultureInfo.InvariantCulture);
|
// {
|
||||||
|
// string filename = (originalImagePath + dateModified.Ticks.ToString(CultureInfo.InvariantCulture)).GetMD5().ToString("N", CultureInfo.InvariantCulture);
|
||||||
|
//
|
||||||
|
// string cacheExtension = _mediaEncoder.SupportsEncoder("libwebp") ? ".webp" : ".png";
|
||||||
|
// var outputPath = Path.Combine(_appPaths.ImageCachePath, "converted-images", filename + cacheExtension);
|
||||||
|
//
|
||||||
|
// var file = _fileSystem.GetFileInfo(outputPath);
|
||||||
|
// if (!file.Exists)
|
||||||
|
// {
|
||||||
|
// await _mediaEncoder.ConvertImage(originalImagePath, outputPath).ConfigureAwait(false);
|
||||||
|
// dateModified = _fileSystem.GetLastWriteTimeUtc(outputPath);
|
||||||
|
// }
|
||||||
|
// else
|
||||||
|
// {
|
||||||
|
// dateModified = file.LastWriteTimeUtc;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// originalImagePath = outputPath;
|
||||||
|
// }
|
||||||
|
// catch (Exception ex)
|
||||||
|
// {
|
||||||
|
// _logger.LogError(ex, "Image conversion failed for {Path}", originalImagePath);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
string cacheExtension = _mediaEncoder.SupportsEncoder("libwebp") ? ".webp" : ".png";
|
return Task.FromResult((originalImagePath, dateModified));
|
||||||
var outputPath = Path.Combine(_appPaths.ImageCachePath, "converted-images", filename + cacheExtension);
|
|
||||||
|
|
||||||
var file = _fileSystem.GetFileInfo(outputPath);
|
|
||||||
if (!file.Exists)
|
|
||||||
{
|
|
||||||
await _mediaEncoder.ConvertImage(originalImagePath, outputPath).ConfigureAwait(false);
|
|
||||||
dateModified = _fileSystem.GetLastWriteTimeUtc(outputPath);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
dateModified = file.LastWriteTimeUtc;
|
|
||||||
}
|
|
||||||
|
|
||||||
originalImagePath = outputPath;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Image conversion failed for {Path}", originalImagePath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (originalImagePath, dateModified);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -36,7 +36,7 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Authors>Jellyfin Contributors</Authors>
|
<Authors>Jellyfin Contributors</Authors>
|
||||||
<PackageId>Jellyfin.Naming</PackageId>
|
<PackageId>Jellyfin.Naming</PackageId>
|
||||||
<VersionPrefix>10.8.0</VersionPrefix>
|
<VersionPrefix>10.8.7</VersionPrefix>
|
||||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="TagLibSharp" Version="2.2.0" />
|
<PackageReference Include="TagLibSharp" Version="2.3.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ namespace Emby.Server.Implementations
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Class CompositionRoot.
|
/// Class CompositionRoot.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract class ApplicationHost : IServerApplicationHost, IDisposable
|
public abstract class ApplicationHost : IServerApplicationHost, IAsyncDisposable, IDisposable
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The environment variable prefixes to log at server startup.
|
/// The environment variable prefixes to log at server startup.
|
||||||
@@ -1088,15 +1088,7 @@ namespace Emby.Server.Implementations
|
|||||||
return GetLocalApiUrl(request.Host.Host, request.Scheme, requestPort);
|
return GetLocalApiUrl(request.Host.Host, request.Scheme, requestPort);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Published server ends with a /
|
return GetSmartApiUrl(request.HttpContext.Connection.RemoteIpAddress ?? IPAddress.Loopback);
|
||||||
if (!string.IsNullOrEmpty(PublishedServerUrl))
|
|
||||||
{
|
|
||||||
// Published server ends with a '/', so we need to remove it.
|
|
||||||
return PublishedServerUrl.Trim('/');
|
|
||||||
}
|
|
||||||
|
|
||||||
string smart = NetManager.GetBindInterface(request, out var port);
|
|
||||||
return GetLocalApiUrl(smart.Trim('/'), request.Scheme, port);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
@@ -1114,13 +1106,13 @@ namespace Emby.Server.Implementations
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public string GetApiUrlForLocalAccess(bool allowHttps = true)
|
public string GetApiUrlForLocalAccess(IPObject hostname = null, bool allowHttps = true)
|
||||||
{
|
{
|
||||||
// With an empty source, the port will be null
|
// With an empty source, the port will be null
|
||||||
string smart = NetManager.GetBindInterface(string.Empty, out _);
|
var smart = NetManager.GetBindInterface(hostname ?? IPHost.None, out _);
|
||||||
var scheme = !allowHttps ? Uri.UriSchemeHttp : null;
|
var scheme = !allowHttps ? Uri.UriSchemeHttp : null;
|
||||||
int? port = !allowHttps ? HttpPort : null;
|
int? port = !allowHttps ? HttpPort : null;
|
||||||
return GetLocalApiUrl(smart.Trim('/'), scheme, port);
|
return GetLocalApiUrl(smart, scheme, port);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
@@ -1134,11 +1126,13 @@ namespace Emby.Server.Implementations
|
|||||||
|
|
||||||
// NOTE: If no BaseUrl is set then UriBuilder appends a trailing slash, but if there is no BaseUrl it does
|
// NOTE: If no BaseUrl is set then UriBuilder appends a trailing slash, but if there is no BaseUrl it does
|
||||||
// not. For consistency, always trim the trailing slash.
|
// not. For consistency, always trim the trailing slash.
|
||||||
|
scheme ??= ListenWithHttps ? Uri.UriSchemeHttps : Uri.UriSchemeHttp;
|
||||||
|
var isHttps = string.Equals(scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase);
|
||||||
return new UriBuilder
|
return new UriBuilder
|
||||||
{
|
{
|
||||||
Scheme = scheme ?? (ListenWithHttps ? Uri.UriSchemeHttps : Uri.UriSchemeHttp),
|
Scheme = scheme,
|
||||||
Host = hostname,
|
Host = hostname,
|
||||||
Port = port ?? (ListenWithHttps ? HttpsPort : HttpPort),
|
Port = port ?? (isHttps ? HttpsPort : HttpPort),
|
||||||
Path = ConfigurationManager.GetNetworkConfiguration().BaseUrl
|
Path = ConfigurationManager.GetNetworkConfiguration().BaseUrl
|
||||||
}.ToString().TrimEnd('/');
|
}.ToString().TrimEnd('/');
|
||||||
}
|
}
|
||||||
@@ -1230,5 +1224,49 @@ namespace Emby.Server.Implementations
|
|||||||
|
|
||||||
_disposed = true;
|
_disposed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async ValueTask DisposeAsync()
|
||||||
|
{
|
||||||
|
await DisposeAsyncCore().ConfigureAwait(false);
|
||||||
|
Dispose(false);
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Used to perform asynchronous cleanup of managed resources or for cascading calls to <see cref="DisposeAsync"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>A ValueTask.</returns>
|
||||||
|
protected virtual async ValueTask DisposeAsyncCore()
|
||||||
|
{
|
||||||
|
var type = GetType();
|
||||||
|
|
||||||
|
Logger.LogInformation("Disposing {Type}", type.Name);
|
||||||
|
|
||||||
|
foreach (var (part, _) in _disposableParts)
|
||||||
|
{
|
||||||
|
var partType = part.GetType();
|
||||||
|
if (partType == type)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.LogInformation("Disposing {Type}", partType.Name);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
part.Dispose();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogError(ex, "Error disposing {Type}", partType.Name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// used for closing websockets
|
||||||
|
foreach (var session in _sessionManager.Sessions)
|
||||||
|
{
|
||||||
|
await session.DisposeAsync().ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -170,7 +170,15 @@ namespace Emby.Server.Implementations.Data
|
|||||||
"CodecTimeBase",
|
"CodecTimeBase",
|
||||||
"ColorPrimaries",
|
"ColorPrimaries",
|
||||||
"ColorSpace",
|
"ColorSpace",
|
||||||
"ColorTransfer"
|
"ColorTransfer",
|
||||||
|
"DvVersionMajor",
|
||||||
|
"DvVersionMinor",
|
||||||
|
"DvProfile",
|
||||||
|
"DvLevel",
|
||||||
|
"RpuPresentFlag",
|
||||||
|
"ElPresentFlag",
|
||||||
|
"BlPresentFlag",
|
||||||
|
"DvBlSignalCompatibilityId"
|
||||||
};
|
};
|
||||||
|
|
||||||
private static readonly string _mediaStreamSaveColumnsInsertQuery =
|
private static readonly string _mediaStreamSaveColumnsInsertQuery =
|
||||||
@@ -341,7 +349,7 @@ namespace Emby.Server.Implementations.Data
|
|||||||
public void Initialize(SqliteUserDataRepository userDataRepo, IUserManager userManager)
|
public void Initialize(SqliteUserDataRepository userDataRepo, IUserManager userManager)
|
||||||
{
|
{
|
||||||
const string CreateMediaStreamsTableCommand
|
const string CreateMediaStreamsTableCommand
|
||||||
= "create table if not exists mediastreams (ItemId GUID, StreamIndex INT, StreamType TEXT, Codec TEXT, Language TEXT, ChannelLayout TEXT, Profile TEXT, AspectRatio TEXT, Path TEXT, IsInterlaced BIT, BitRate INT NULL, Channels INT NULL, SampleRate INT NULL, IsDefault BIT, IsForced BIT, IsExternal BIT, Height INT NULL, Width INT NULL, AverageFrameRate FLOAT NULL, RealFrameRate FLOAT NULL, Level FLOAT NULL, PixelFormat TEXT, BitDepth INT NULL, IsAnamorphic BIT NULL, RefFrames INT NULL, CodecTag TEXT NULL, Comment TEXT NULL, NalLengthSize TEXT NULL, IsAvc BIT NULL, Title TEXT NULL, TimeBase TEXT NULL, CodecTimeBase TEXT NULL, ColorPrimaries TEXT NULL, ColorSpace TEXT NULL, ColorTransfer TEXT NULL, PRIMARY KEY (ItemId, StreamIndex))";
|
= "create table if not exists mediastreams (ItemId GUID, StreamIndex INT, StreamType TEXT, Codec TEXT, Language TEXT, ChannelLayout TEXT, Profile TEXT, AspectRatio TEXT, Path TEXT, IsInterlaced BIT, BitRate INT NULL, Channels INT NULL, SampleRate INT NULL, IsDefault BIT, IsForced BIT, IsExternal BIT, Height INT NULL, Width INT NULL, AverageFrameRate FLOAT NULL, RealFrameRate FLOAT NULL, Level FLOAT NULL, PixelFormat TEXT, BitDepth INT NULL, IsAnamorphic BIT NULL, RefFrames INT NULL, CodecTag TEXT NULL, Comment TEXT NULL, NalLengthSize TEXT NULL, IsAvc BIT NULL, Title TEXT NULL, TimeBase TEXT NULL, CodecTimeBase TEXT NULL, ColorPrimaries TEXT NULL, ColorSpace TEXT NULL, ColorTransfer TEXT NULL, DvVersionMajor INT NULL, DvVersionMinor INT NULL, DvProfile INT NULL, DvLevel INT NULL, RpuPresentFlag INT NULL, ElPresentFlag INT NULL, BlPresentFlag INT NULL, DvBlSignalCompatibilityId INT NULL, PRIMARY KEY (ItemId, StreamIndex))";
|
||||||
const string CreateMediaAttachmentsTableCommand
|
const string CreateMediaAttachmentsTableCommand
|
||||||
= "create table if not exists mediaattachments (ItemId GUID, AttachmentIndex INT, Codec TEXT, CodecTag TEXT NULL, Comment TEXT NULL, Filename TEXT NULL, MIMEType TEXT NULL, PRIMARY KEY (ItemId, AttachmentIndex))";
|
= "create table if not exists mediaattachments (ItemId GUID, AttachmentIndex INT, Codec TEXT, CodecTag TEXT NULL, Comment TEXT NULL, Filename TEXT NULL, MIMEType TEXT NULL, PRIMARY KEY (ItemId, AttachmentIndex))";
|
||||||
|
|
||||||
@@ -555,6 +563,15 @@ namespace Emby.Server.Implementations.Data
|
|||||||
AddColumn(db, "MediaStreams", "ColorPrimaries", "TEXT", existingColumnNames);
|
AddColumn(db, "MediaStreams", "ColorPrimaries", "TEXT", existingColumnNames);
|
||||||
AddColumn(db, "MediaStreams", "ColorSpace", "TEXT", existingColumnNames);
|
AddColumn(db, "MediaStreams", "ColorSpace", "TEXT", existingColumnNames);
|
||||||
AddColumn(db, "MediaStreams", "ColorTransfer", "TEXT", existingColumnNames);
|
AddColumn(db, "MediaStreams", "ColorTransfer", "TEXT", existingColumnNames);
|
||||||
|
|
||||||
|
AddColumn(db, "MediaStreams", "DvVersionMajor", "INT", existingColumnNames);
|
||||||
|
AddColumn(db, "MediaStreams", "DvVersionMinor", "INT", existingColumnNames);
|
||||||
|
AddColumn(db, "MediaStreams", "DvProfile", "INT", existingColumnNames);
|
||||||
|
AddColumn(db, "MediaStreams", "DvLevel", "INT", existingColumnNames);
|
||||||
|
AddColumn(db, "MediaStreams", "RpuPresentFlag", "INT", existingColumnNames);
|
||||||
|
AddColumn(db, "MediaStreams", "ElPresentFlag", "INT", existingColumnNames);
|
||||||
|
AddColumn(db, "MediaStreams", "BlPresentFlag", "INT", existingColumnNames);
|
||||||
|
AddColumn(db, "MediaStreams", "DvBlSignalCompatibilityId", "INT", existingColumnNames);
|
||||||
},
|
},
|
||||||
TransactionMode);
|
TransactionMode);
|
||||||
|
|
||||||
@@ -2403,7 +2420,7 @@ namespace Emby.Server.Implementations.Data
|
|||||||
}
|
}
|
||||||
|
|
||||||
// genres, tags, studios, person, year?
|
// genres, tags, studios, person, year?
|
||||||
builder.Append("+ (Select count(1) * 10 from ItemValues where ItemId=Guid and CleanValue in (select CleanValue from itemvalues where ItemId=@SimilarItemId))");
|
builder.Append("+ (Select count(1) * 10 from ItemValues where ItemId=Guid and CleanValue in (select CleanValue from ItemValues where ItemId=@SimilarItemId))");
|
||||||
|
|
||||||
if (item is MusicArtist)
|
if (item is MusicArtist)
|
||||||
{
|
{
|
||||||
@@ -3058,12 +3075,12 @@ namespace Emby.Server.Implementations.Data
|
|||||||
|
|
||||||
if (string.Equals(name, ItemSortBy.Artist, StringComparison.OrdinalIgnoreCase))
|
if (string.Equals(name, ItemSortBy.Artist, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
return "(select CleanValue from itemvalues where ItemId=Guid and Type=0 LIMIT 1)";
|
return "(select CleanValue from ItemValues where ItemId=Guid and Type=0 LIMIT 1)";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.Equals(name, ItemSortBy.AlbumArtist, StringComparison.OrdinalIgnoreCase))
|
if (string.Equals(name, ItemSortBy.AlbumArtist, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
return "(select CleanValue from itemvalues where ItemId=Guid and Type=1 LIMIT 1)";
|
return "(select CleanValue from ItemValues where ItemId=Guid and Type=1 LIMIT 1)";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.Equals(name, ItemSortBy.OfficialRating, StringComparison.OrdinalIgnoreCase))
|
if (string.Equals(name, ItemSortBy.OfficialRating, StringComparison.OrdinalIgnoreCase))
|
||||||
@@ -3073,7 +3090,7 @@ namespace Emby.Server.Implementations.Data
|
|||||||
|
|
||||||
if (string.Equals(name, ItemSortBy.Studio, StringComparison.OrdinalIgnoreCase))
|
if (string.Equals(name, ItemSortBy.Studio, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
return "(select CleanValue from itemvalues where ItemId=Guid and Type=3 LIMIT 1)";
|
return "(select CleanValue from ItemValues where ItemId=Guid and Type=3 LIMIT 1)";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.Equals(name, ItemSortBy.SeriesDatePlayed, StringComparison.OrdinalIgnoreCase))
|
if (string.Equals(name, ItemSortBy.SeriesDatePlayed, StringComparison.OrdinalIgnoreCase))
|
||||||
@@ -3146,6 +3163,11 @@ namespace Emby.Server.Implementations.Data
|
|||||||
return ItemSortBy.IndexNumber;
|
return ItemSortBy.IndexNumber;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (string.Equals(name, ItemSortBy.SimilarityScore, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return ItemSortBy.SimilarityScore;
|
||||||
|
}
|
||||||
|
|
||||||
// Unknown SortBy, just sort by the SortName.
|
// Unknown SortBy, just sort by the SortName.
|
||||||
return ItemSortBy.SortName;
|
return ItemSortBy.SortName;
|
||||||
}
|
}
|
||||||
@@ -3519,6 +3541,13 @@ namespace Emby.Server.Implementations.Data
|
|||||||
statement?.TryBind("@MinIndexNumber", query.MinIndexNumber.Value);
|
statement?.TryBind("@MinIndexNumber", query.MinIndexNumber.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (query.MinParentAndIndexNumber.HasValue)
|
||||||
|
{
|
||||||
|
whereClauses.Add("((ParentIndexNumber=@MinParentAndIndexNumberParent and IndexNumber>=@MinParentAndIndexNumberIndex) or ParentIndexNumber>@MinParentAndIndexNumberParent)");
|
||||||
|
statement?.TryBind("@MinParentAndIndexNumberParent", query.MinParentAndIndexNumber.Value.ParentIndexNumber);
|
||||||
|
statement?.TryBind("@MinParentAndIndexNumberIndex", query.MinParentAndIndexNumber.Value.IndexNumber);
|
||||||
|
}
|
||||||
|
|
||||||
if (query.MinDateCreated.HasValue)
|
if (query.MinDateCreated.HasValue)
|
||||||
{
|
{
|
||||||
whereClauses.Add("DateCreated>=@MinDateCreated");
|
whereClauses.Add("DateCreated>=@MinDateCreated");
|
||||||
@@ -3846,7 +3875,7 @@ namespace Emby.Server.Implementations.Data
|
|||||||
{
|
{
|
||||||
var paramName = "@ArtistIds" + index;
|
var paramName = "@ArtistIds" + index;
|
||||||
|
|
||||||
clauses.Add("(guid in (select itemid from itemvalues where CleanValue = (select CleanName from TypedBaseItems where guid=" + paramName + ") and Type<=1))");
|
clauses.Add("(guid in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=" + paramName + ") and Type<=1))");
|
||||||
if (statement != null)
|
if (statement != null)
|
||||||
{
|
{
|
||||||
statement.TryBind(paramName, artistId);
|
statement.TryBind(paramName, artistId);
|
||||||
@@ -3867,7 +3896,7 @@ namespace Emby.Server.Implementations.Data
|
|||||||
{
|
{
|
||||||
var paramName = "@ArtistIds" + index;
|
var paramName = "@ArtistIds" + index;
|
||||||
|
|
||||||
clauses.Add("(guid in (select itemid from itemvalues where CleanValue = (select CleanName from TypedBaseItems where guid=" + paramName + ") and Type=1))");
|
clauses.Add("(guid in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=" + paramName + ") and Type=1))");
|
||||||
if (statement != null)
|
if (statement != null)
|
||||||
{
|
{
|
||||||
statement.TryBind(paramName, artistId);
|
statement.TryBind(paramName, artistId);
|
||||||
@@ -3888,7 +3917,7 @@ namespace Emby.Server.Implementations.Data
|
|||||||
{
|
{
|
||||||
var paramName = "@ArtistIds" + index;
|
var paramName = "@ArtistIds" + index;
|
||||||
|
|
||||||
clauses.Add("((select CleanName from TypedBaseItems where guid=" + paramName + ") in (select CleanValue from itemvalues where ItemId=Guid and Type=0) AND (select CleanName from TypedBaseItems where guid=" + paramName + ") not in (select CleanValue from itemvalues where ItemId=Guid and Type=1))");
|
clauses.Add("((select CleanName from TypedBaseItems where guid=" + paramName + ") in (select CleanValue from ItemValues where ItemId=Guid and Type=0) AND (select CleanName from TypedBaseItems where guid=" + paramName + ") not in (select CleanValue from ItemValues where ItemId=Guid and Type=1))");
|
||||||
if (statement != null)
|
if (statement != null)
|
||||||
{
|
{
|
||||||
statement.TryBind(paramName, artistId);
|
statement.TryBind(paramName, artistId);
|
||||||
@@ -3930,7 +3959,7 @@ namespace Emby.Server.Implementations.Data
|
|||||||
{
|
{
|
||||||
var paramName = "@ExcludeArtistId" + index;
|
var paramName = "@ExcludeArtistId" + index;
|
||||||
|
|
||||||
clauses.Add("(guid not in (select itemid from itemvalues where CleanValue = (select CleanName from TypedBaseItems where guid=" + paramName + ") and Type<=1))");
|
clauses.Add("(guid not in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=" + paramName + ") and Type<=1))");
|
||||||
if (statement != null)
|
if (statement != null)
|
||||||
{
|
{
|
||||||
statement.TryBind(paramName, artistId);
|
statement.TryBind(paramName, artistId);
|
||||||
@@ -3951,7 +3980,7 @@ namespace Emby.Server.Implementations.Data
|
|||||||
{
|
{
|
||||||
var paramName = "@GenreId" + index;
|
var paramName = "@GenreId" + index;
|
||||||
|
|
||||||
clauses.Add("(guid in (select itemid from itemvalues where CleanValue = (select CleanName from TypedBaseItems where guid=" + paramName + ") and Type=2))");
|
clauses.Add("(guid in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=" + paramName + ") and Type=2))");
|
||||||
if (statement != null)
|
if (statement != null)
|
||||||
{
|
{
|
||||||
statement.TryBind(paramName, genreId);
|
statement.TryBind(paramName, genreId);
|
||||||
@@ -3970,7 +3999,7 @@ namespace Emby.Server.Implementations.Data
|
|||||||
var index = 0;
|
var index = 0;
|
||||||
foreach (var item in query.Genres)
|
foreach (var item in query.Genres)
|
||||||
{
|
{
|
||||||
clauses.Add("@Genre" + index + " in (select CleanValue from itemvalues where ItemId=Guid and Type=2)");
|
clauses.Add("@Genre" + index + " in (select CleanValue from ItemValues where ItemId=Guid and Type=2)");
|
||||||
if (statement != null)
|
if (statement != null)
|
||||||
{
|
{
|
||||||
statement.TryBind("@Genre" + index, GetCleanValue(item));
|
statement.TryBind("@Genre" + index, GetCleanValue(item));
|
||||||
@@ -3989,7 +4018,7 @@ namespace Emby.Server.Implementations.Data
|
|||||||
var index = 0;
|
var index = 0;
|
||||||
foreach (var item in tags)
|
foreach (var item in tags)
|
||||||
{
|
{
|
||||||
clauses.Add("@Tag" + index + " in (select CleanValue from itemvalues where ItemId=Guid and Type=4)");
|
clauses.Add("@Tag" + index + " in (select CleanValue from ItemValues where ItemId=Guid and Type=4)");
|
||||||
if (statement != null)
|
if (statement != null)
|
||||||
{
|
{
|
||||||
statement.TryBind("@Tag" + index, GetCleanValue(item));
|
statement.TryBind("@Tag" + index, GetCleanValue(item));
|
||||||
@@ -4008,7 +4037,7 @@ namespace Emby.Server.Implementations.Data
|
|||||||
var index = 0;
|
var index = 0;
|
||||||
foreach (var item in excludeTags)
|
foreach (var item in excludeTags)
|
||||||
{
|
{
|
||||||
clauses.Add("@ExcludeTag" + index + " not in (select CleanValue from itemvalues where ItemId=Guid and Type=4)");
|
clauses.Add("@ExcludeTag" + index + " not in (select CleanValue from ItemValues where ItemId=Guid and Type=4)");
|
||||||
if (statement != null)
|
if (statement != null)
|
||||||
{
|
{
|
||||||
statement.TryBind("@ExcludeTag" + index, GetCleanValue(item));
|
statement.TryBind("@ExcludeTag" + index, GetCleanValue(item));
|
||||||
@@ -4029,7 +4058,7 @@ namespace Emby.Server.Implementations.Data
|
|||||||
{
|
{
|
||||||
var paramName = "@StudioId" + index;
|
var paramName = "@StudioId" + index;
|
||||||
|
|
||||||
clauses.Add("(guid in (select itemid from itemvalues where CleanValue = (select CleanName from TypedBaseItems where guid=" + paramName + ") and Type=3))");
|
clauses.Add("(guid in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=" + paramName + ") and Type=3))");
|
||||||
|
|
||||||
if (statement != null)
|
if (statement != null)
|
||||||
{
|
{
|
||||||
@@ -4508,7 +4537,7 @@ namespace Emby.Server.Implementations.Data
|
|||||||
{
|
{
|
||||||
int index = 0;
|
int index = 0;
|
||||||
string excludedTags = string.Join(',', query.ExcludeInheritedTags.Select(_ => paramName + index++));
|
string excludedTags = string.Join(',', query.ExcludeInheritedTags.Select(_ => paramName + index++));
|
||||||
whereClauses.Add("((select CleanValue from itemvalues where ItemId=Guid and Type=6 and cleanvalue in (" + excludedTags + ")) is null)");
|
whereClauses.Add("((select CleanValue from ItemValues where ItemId=Guid and Type=6 and cleanvalue in (" + excludedTags + ")) is null)");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -4743,11 +4772,11 @@ namespace Emby.Server.Implementations.Data
|
|||||||
';',
|
';',
|
||||||
new string[]
|
new string[]
|
||||||
{
|
{
|
||||||
"delete from itemvalues where type = 6",
|
"delete from ItemValues where type = 6",
|
||||||
|
|
||||||
"insert into itemvalues (ItemId, Type, Value, CleanValue) select ItemId, 6, Value, CleanValue from ItemValues where Type=4",
|
"insert into ItemValues (ItemId, Type, Value, CleanValue) select ItemId, 6, Value, CleanValue from ItemValues where Type=4",
|
||||||
|
|
||||||
@"insert into itemvalues (ItemId, Type, Value, CleanValue) select AncestorIds.itemid, 6, ItemValues.Value, ItemValues.CleanValue
|
@"insert into ItemValues (ItemId, Type, Value, CleanValue) select AncestorIds.itemid, 6, ItemValues.Value, ItemValues.CleanValue
|
||||||
FROM AncestorIds
|
FROM AncestorIds
|
||||||
LEFT JOIN ItemValues ON (AncestorIds.AncestorId = ItemValues.ItemId)
|
LEFT JOIN ItemValues ON (AncestorIds.AncestorId = ItemValues.ItemId)
|
||||||
where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type = 4 "
|
where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type = 4 "
|
||||||
@@ -4912,6 +4941,7 @@ SELECT key FROM UserDatas WHERE isFavorite=@IsFavorite AND userId=@UserId)
|
|||||||
AND Type = @InternalPersonType)");
|
AND Type = @InternalPersonType)");
|
||||||
statement?.TryBind("@IsFavorite", query.IsFavorite.Value);
|
statement?.TryBind("@IsFavorite", query.IsFavorite.Value);
|
||||||
statement?.TryBind("@InternalPersonType", typeof(Person).FullName);
|
statement?.TryBind("@InternalPersonType", typeof(Person).FullName);
|
||||||
|
statement?.TryBind("@UserId", query.User.InternalId);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!query.ItemId.Equals(default))
|
if (!query.ItemId.Equals(default))
|
||||||
@@ -4966,11 +4996,6 @@ AND Type = @InternalPersonType)");
|
|||||||
statement?.TryBind("@NameContains", "%" + query.NameContains + "%");
|
statement?.TryBind("@NameContains", "%" + query.NameContains + "%");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (query.User != null)
|
|
||||||
{
|
|
||||||
statement?.TryBind("@UserId", query.User.InternalId);
|
|
||||||
}
|
|
||||||
|
|
||||||
return whereClauses;
|
return whereClauses;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5854,6 +5879,15 @@ AND Type = @InternalPersonType)");
|
|||||||
statement.TryBind("@ColorPrimaries" + index, stream.ColorPrimaries);
|
statement.TryBind("@ColorPrimaries" + index, stream.ColorPrimaries);
|
||||||
statement.TryBind("@ColorSpace" + index, stream.ColorSpace);
|
statement.TryBind("@ColorSpace" + index, stream.ColorSpace);
|
||||||
statement.TryBind("@ColorTransfer" + index, stream.ColorTransfer);
|
statement.TryBind("@ColorTransfer" + index, stream.ColorTransfer);
|
||||||
|
|
||||||
|
statement.TryBind("@DvVersionMajor" + index, stream.DvVersionMajor);
|
||||||
|
statement.TryBind("@DvVersionMinor" + index, stream.DvVersionMinor);
|
||||||
|
statement.TryBind("@DvProfile" + index, stream.DvProfile);
|
||||||
|
statement.TryBind("@DvLevel" + index, stream.DvLevel);
|
||||||
|
statement.TryBind("@RpuPresentFlag" + index, stream.RpuPresentFlag);
|
||||||
|
statement.TryBind("@ElPresentFlag" + index, stream.ElPresentFlag);
|
||||||
|
statement.TryBind("@BlPresentFlag" + index, stream.BlPresentFlag);
|
||||||
|
statement.TryBind("@DvBlSignalCompatibilityId" + index, stream.DvBlSignalCompatibilityId);
|
||||||
}
|
}
|
||||||
|
|
||||||
statement.Reset();
|
statement.Reset();
|
||||||
@@ -6025,6 +6059,46 @@ AND Type = @InternalPersonType)");
|
|||||||
item.ColorTransfer = colorTransfer;
|
item.ColorTransfer = colorTransfer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (reader.TryGetInt32(35, out var dvVersionMajor))
|
||||||
|
{
|
||||||
|
item.DvVersionMajor = dvVersionMajor;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reader.TryGetInt32(36, out var dvVersionMinor))
|
||||||
|
{
|
||||||
|
item.DvVersionMinor = dvVersionMinor;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reader.TryGetInt32(37, out var dvProfile))
|
||||||
|
{
|
||||||
|
item.DvProfile = dvProfile;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reader.TryGetInt32(38, out var dvLevel))
|
||||||
|
{
|
||||||
|
item.DvLevel = dvLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reader.TryGetInt32(39, out var rpuPresentFlag))
|
||||||
|
{
|
||||||
|
item.RpuPresentFlag = rpuPresentFlag;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reader.TryGetInt32(40, out var elPresentFlag))
|
||||||
|
{
|
||||||
|
item.ElPresentFlag = elPresentFlag;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reader.TryGetInt32(41, out var blPresentFlag))
|
||||||
|
{
|
||||||
|
item.BlPresentFlag = blPresentFlag;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reader.TryGetInt32(42, out var dvBlSignalCompatibilityId))
|
||||||
|
{
|
||||||
|
item.DvBlSignalCompatibilityId = dvBlSignalCompatibilityId;
|
||||||
|
}
|
||||||
|
|
||||||
if (item.Type == MediaStreamType.Subtitle)
|
if (item.Type == MediaStreamType.Subtitle)
|
||||||
{
|
{
|
||||||
item.LocalizedUndefined = _localization.GetLocalizedString("Undefined");
|
item.LocalizedUndefined = _localization.GetLocalizedString("Undefined");
|
||||||
|
|||||||
@@ -182,7 +182,7 @@ namespace Emby.Server.Implementations.Dto
|
|||||||
|
|
||||||
if (options.ContainsField(ItemFields.People))
|
if (options.ContainsField(ItemFields.People))
|
||||||
{
|
{
|
||||||
AttachPeople(dto, item);
|
AttachPeople(dto, item, user);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.ContainsField(ItemFields.PrimaryImageAspectRatio))
|
if (options.ContainsField(ItemFields.PrimaryImageAspectRatio))
|
||||||
@@ -503,7 +503,8 @@ namespace Emby.Server.Implementations.Dto
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="dto">The dto.</param>
|
/// <param name="dto">The dto.</param>
|
||||||
/// <param name="item">The item.</param>
|
/// <param name="item">The item.</param>
|
||||||
private void AttachPeople(BaseItemDto dto, BaseItem item)
|
/// <param name="user">The requesting user.</param>
|
||||||
|
private void AttachPeople(BaseItemDto dto, BaseItem item, User user = null)
|
||||||
{
|
{
|
||||||
// Ordering by person type to ensure actors and artists are at the front.
|
// Ordering by person type to ensure actors and artists are at the front.
|
||||||
// This is taking advantage of the fact that they both begin with A
|
// This is taking advantage of the fact that they both begin with A
|
||||||
@@ -560,6 +561,9 @@ namespace Emby.Server.Implementations.Dto
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}).Where(i => i != null)
|
}).Where(i => i != null)
|
||||||
|
.Where(i => user == null ?
|
||||||
|
true :
|
||||||
|
i.IsVisible(user))
|
||||||
.GroupBy(i => i.Name, StringComparer.OrdinalIgnoreCase)
|
.GroupBy(i => i.Name, StringComparer.OrdinalIgnoreCase)
|
||||||
.Select(x => x.First())
|
.Select(x => x.First())
|
||||||
.ToDictionary(i => i.Name, StringComparer.OrdinalIgnoreCase);
|
.ToDictionary(i => i.Name, StringComparer.OrdinalIgnoreCase);
|
||||||
|
|||||||
@@ -29,10 +29,10 @@
|
|||||||
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="6.0.1" />
|
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="6.0.1" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="6.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="6.0.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="6.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="6.0.0" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.5" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.9" />
|
||||||
<PackageReference Include="Mono.Nat" Version="3.0.3" />
|
<PackageReference Include="Mono.Nat" Version="3.0.3" />
|
||||||
<PackageReference Include="prometheus-net.DotNetRuntime" Version="4.2.4" />
|
<PackageReference Include="prometheus-net.DotNetRuntime" Version="4.2.4" />
|
||||||
<PackageReference Include="sharpcompress" Version="0.31.0" />
|
<PackageReference Include="sharpcompress" Version="0.32.2" />
|
||||||
<PackageReference Include="SQLitePCL.pretty.netstandard" Version="3.1.0" />
|
<PackageReference Include="SQLitePCL.pretty.netstandard" Version="3.1.0" />
|
||||||
<PackageReference Include="DotNet.Glob" Version="3.1.3" />
|
<PackageReference Include="DotNet.Glob" Version="3.1.3" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ namespace Emby.Server.Implementations.HttpServer
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Class WebSocketConnection.
|
/// Class WebSocketConnection.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class WebSocketConnection : IWebSocketConnection, IDisposable
|
public class WebSocketConnection : IWebSocketConnection
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The logger.
|
/// The logger.
|
||||||
@@ -36,6 +36,8 @@ namespace Emby.Server.Implementations.HttpServer
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private readonly WebSocket _socket;
|
private readonly WebSocket _socket;
|
||||||
|
|
||||||
|
private bool _disposed = false;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="WebSocketConnection" /> class.
|
/// Initializes a new instance of the <see cref="WebSocketConnection" /> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -244,10 +246,39 @@ namespace Emby.Server.Implementations.HttpServer
|
|||||||
/// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
|
/// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
|
||||||
protected virtual void Dispose(bool dispose)
|
protected virtual void Dispose(bool dispose)
|
||||||
{
|
{
|
||||||
|
if (_disposed)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (dispose)
|
if (dispose)
|
||||||
{
|
{
|
||||||
_socket.Dispose();
|
_socket.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_disposed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async ValueTask DisposeAsync()
|
||||||
|
{
|
||||||
|
await DisposeAsyncCore().ConfigureAwait(false);
|
||||||
|
Dispose(false);
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Used to perform asynchronous cleanup of managed resources or for cascading calls to <see cref="DisposeAsync"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>A ValueTask.</returns>
|
||||||
|
protected virtual async ValueTask DisposeAsyncCore()
|
||||||
|
{
|
||||||
|
if (_socket.State == WebSocketState.Open)
|
||||||
|
{
|
||||||
|
await _socket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "System Shutdown", CancellationToken.None).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
_socket.Dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1860,7 +1860,9 @@ namespace Emby.Server.Implementations.Library
|
|||||||
throw new ArgumentNullException(nameof(item));
|
throw new ArgumentNullException(nameof(item));
|
||||||
}
|
}
|
||||||
|
|
||||||
var outdated = forceUpdate ? item.ImageInfos.Where(i => i.Path != null).ToArray() : item.ImageInfos.Where(ImageNeedsRefresh).ToArray();
|
var outdated = forceUpdate
|
||||||
|
? item.ImageInfos.Where(i => i.Path != null).ToArray()
|
||||||
|
: item.ImageInfos.Where(ImageNeedsRefresh).ToArray();
|
||||||
// Skip image processing if current or live tv source
|
// Skip image processing if current or live tv source
|
||||||
if (outdated.Length == 0 || item.SourceType != SourceType.Library)
|
if (outdated.Length == 0 || item.SourceType != SourceType.Library)
|
||||||
{
|
{
|
||||||
@@ -1883,7 +1885,7 @@ namespace Emby.Server.Implementations.Library
|
|||||||
_logger.LogWarning("Cannot get image index for {ImagePath}", img.Path);
|
_logger.LogWarning("Cannot get image index for {ImagePath}", img.Path);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
catch (Exception ex) when (ex is InvalidOperationException || ex is IOException)
|
catch (Exception ex) when (ex is InvalidOperationException or IOException)
|
||||||
{
|
{
|
||||||
_logger.LogWarning(ex, "Cannot fetch image from {ImagePath}", img.Path);
|
_logger.LogWarning(ex, "Cannot fetch image from {ImagePath}", img.Path);
|
||||||
continue;
|
continue;
|
||||||
@@ -1895,23 +1897,24 @@ namespace Emby.Server.Implementations.Library
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ImageDimensions size;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
ImageDimensions size = _imageProcessor.GetImageDimensions(item, image);
|
size = _imageProcessor.GetImageDimensions(item, image);
|
||||||
image.Width = size.Width;
|
image.Width = size.Width;
|
||||||
image.Height = size.Height;
|
image.Height = size.Height;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "Cannot get image dimensions for {ImagePath}", image.Path);
|
_logger.LogError(ex, "Cannot get image dimensions for {ImagePath}", image.Path);
|
||||||
|
size = new ImageDimensions(0, 0);
|
||||||
image.Width = 0;
|
image.Width = 0;
|
||||||
image.Height = 0;
|
image.Height = 0;
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
image.BlurHash = _imageProcessor.GetImageBlurHash(image.Path);
|
image.BlurHash = _imageProcessor.GetImageBlurHash(image.Path, size);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -2450,6 +2453,12 @@ namespace Emby.Server.Implementations.Library
|
|||||||
return RootFolder;
|
return RootFolder;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void QueueLibraryScan()
|
||||||
|
{
|
||||||
|
_taskManager.QueueScheduledTask<RefreshMediaLibraryTask>();
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public int? GetSeasonNumberFromPath(string path)
|
public int? GetSeasonNumberFromPath(string path)
|
||||||
=> SeasonPathParser.Parse(path, true, true).SeasonNumber;
|
=> SeasonPathParser.Parse(path, true, true).SeasonNumber;
|
||||||
@@ -2757,7 +2766,8 @@ namespace Emby.Server.Implementations.Library
|
|||||||
|
|
||||||
public List<Person> GetPeopleItems(InternalPeopleQuery query)
|
public List<Person> GetPeopleItems(InternalPeopleQuery query)
|
||||||
{
|
{
|
||||||
return _itemRepository.GetPeopleNames(query).Select(i =>
|
return _itemRepository.GetPeopleNames(query)
|
||||||
|
.Select(i =>
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -2768,7 +2778,12 @@ namespace Emby.Server.Implementations.Library
|
|||||||
_logger.LogError(ex, "Error getting person");
|
_logger.LogError(ex, "Error getting person");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}).Where(i => i != null).ToList();
|
})
|
||||||
|
.Where(i => i != null)
|
||||||
|
.Where(i => query.User == null ?
|
||||||
|
true :
|
||||||
|
i.IsVisible(query.User))
|
||||||
|
.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<string> GetPeopleNames(InternalPeopleQuery query)
|
public List<string> GetPeopleNames(InternalPeopleQuery query)
|
||||||
|
|||||||
@@ -13,11 +13,11 @@ namespace Emby.Server.Implementations.Library
|
|||||||
{
|
{
|
||||||
public static int? GetDefaultAudioStreamIndex(IReadOnlyList<MediaStream> streams, IReadOnlyList<string> preferredLanguages, bool preferDefaultTrack)
|
public static int? GetDefaultAudioStreamIndex(IReadOnlyList<MediaStream> streams, IReadOnlyList<string> preferredLanguages, bool preferDefaultTrack)
|
||||||
{
|
{
|
||||||
var sortedStreams = GetSortedStreams(streams, MediaStreamType.Audio, preferredLanguages);
|
var sortedStreams = GetSortedStreams(streams, MediaStreamType.Audio, preferredLanguages).ToList();
|
||||||
|
|
||||||
if (preferDefaultTrack)
|
if (preferDefaultTrack)
|
||||||
{
|
{
|
||||||
var defaultStream = streams.FirstOrDefault(i => i.IsDefault);
|
var defaultStream = sortedStreams.FirstOrDefault(i => i.IsDefault);
|
||||||
|
|
||||||
if (defaultStream != null)
|
if (defaultStream != null)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -225,7 +225,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
|
|||||||
|
|
||||||
if (string.Equals(collectionType, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase))
|
if (string.Equals(collectionType, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
return ResolveVideos<Episode>(parent, files, true, collectionType, true);
|
return ResolveVideos<Episode>(parent, files, false, collectionType, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -165,12 +165,12 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
|||||||
|
|
||||||
const double DesiredAspect = 2.0 / 3;
|
const double DesiredAspect = 2.0 / 3;
|
||||||
|
|
||||||
programEntry.PrimaryImage = GetProgramImage(ApiUrl, imagesWithText, DesiredAspect) ??
|
programEntry.PrimaryImage = GetProgramImage(ApiUrl, imagesWithText, DesiredAspect, token) ??
|
||||||
GetProgramImage(ApiUrl, allImages, DesiredAspect);
|
GetProgramImage(ApiUrl, allImages, DesiredAspect, token);
|
||||||
|
|
||||||
const double WideAspect = 16.0 / 9;
|
const double WideAspect = 16.0 / 9;
|
||||||
|
|
||||||
programEntry.ThumbImage = GetProgramImage(ApiUrl, imagesWithText, WideAspect);
|
programEntry.ThumbImage = GetProgramImage(ApiUrl, imagesWithText, WideAspect, token);
|
||||||
|
|
||||||
// Don't supply the same image twice
|
// Don't supply the same image twice
|
||||||
if (string.Equals(programEntry.PrimaryImage, programEntry.ThumbImage, StringComparison.Ordinal))
|
if (string.Equals(programEntry.PrimaryImage, programEntry.ThumbImage, StringComparison.Ordinal))
|
||||||
@@ -178,7 +178,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
|||||||
programEntry.ThumbImage = null;
|
programEntry.ThumbImage = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
programEntry.BackdropImage = GetProgramImage(ApiUrl, imagesWithoutText, WideAspect);
|
programEntry.BackdropImage = GetProgramImage(ApiUrl, imagesWithoutText, WideAspect, token);
|
||||||
|
|
||||||
// programEntry.bannerImage = GetProgramImage(ApiUrl, data, "Banner", false) ??
|
// programEntry.bannerImage = GetProgramImage(ApiUrl, data, "Banner", false) ??
|
||||||
// GetProgramImage(ApiUrl, data, "Banner-L1", false) ??
|
// GetProgramImage(ApiUrl, data, "Banner-L1", false) ??
|
||||||
@@ -399,7 +399,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
|||||||
return info;
|
return info;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string GetProgramImage(string apiUrl, IEnumerable<ImageDataDto> images, double desiredAspect)
|
private static string GetProgramImage(string apiUrl, IEnumerable<ImageDataDto> images, double desiredAspect, string token)
|
||||||
{
|
{
|
||||||
var match = images
|
var match = images
|
||||||
.OrderBy(i => Math.Abs(desiredAspect - GetAspectRatio(i)))
|
.OrderBy(i => Math.Abs(desiredAspect - GetAspectRatio(i)))
|
||||||
@@ -423,7 +423,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
return apiUrl + "/image/" + uri;
|
return apiUrl + "/image/" + uri + "?token=" + token;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -457,6 +457,8 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
|||||||
IReadOnlyList<string> programIds,
|
IReadOnlyList<string> programIds,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
|
var token = await GetToken(info, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
if (programIds.Count == 0)
|
if (programIds.Count == 0)
|
||||||
{
|
{
|
||||||
return Array.Empty<ShowImagesDto>();
|
return Array.Empty<ShowImagesDto>();
|
||||||
@@ -478,6 +480,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
|||||||
{
|
{
|
||||||
Content = new StringContent(str.ToString(), Encoding.UTF8, MediaTypeNames.Application.Json)
|
Content = new StringContent(str.ToString(), Encoding.UTF8, MediaTypeNames.Application.Json)
|
||||||
};
|
};
|
||||||
|
message.Headers.TryAddWithoutValidation("token", token);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -386,6 +386,7 @@ namespace Emby.Server.Implementations.Localization
|
|||||||
yield return new LocalizationOption("Español (Dominicana)", "es_DO");
|
yield return new LocalizationOption("Español (Dominicana)", "es_DO");
|
||||||
yield return new LocalizationOption("Español (México)", "es-MX");
|
yield return new LocalizationOption("Español (México)", "es-MX");
|
||||||
yield return new LocalizationOption("Eesti", "et");
|
yield return new LocalizationOption("Eesti", "et");
|
||||||
|
yield return new LocalizationOption("Basque", "eu");
|
||||||
yield return new LocalizationOption("فارسی", "fa");
|
yield return new LocalizationOption("فارسی", "fa");
|
||||||
yield return new LocalizationOption("Suomi", "fi");
|
yield return new LocalizationOption("Suomi", "fi");
|
||||||
yield return new LocalizationOption("Filipino", "fil");
|
yield return new LocalizationOption("Filipino", "fil");
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ using Microsoft.Extensions.Logging;
|
|||||||
|
|
||||||
namespace Emby.Server.Implementations.Session
|
namespace Emby.Server.Implementations.Session
|
||||||
{
|
{
|
||||||
public sealed class WebSocketController : ISessionController, IDisposable
|
public sealed class WebSocketController : ISessionController, IAsyncDisposable, IDisposable
|
||||||
{
|
{
|
||||||
private readonly ILogger<WebSocketController> _logger;
|
private readonly ILogger<WebSocketController> _logger;
|
||||||
private readonly ISessionManager _sessionManager;
|
private readonly ISessionManager _sessionManager;
|
||||||
@@ -99,6 +99,23 @@ namespace Emby.Server.Implementations.Session
|
|||||||
foreach (var socket in _sockets)
|
foreach (var socket in _sockets)
|
||||||
{
|
{
|
||||||
socket.Closed -= OnConnectionClosed;
|
socket.Closed -= OnConnectionClosed;
|
||||||
|
socket.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
_disposed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask DisposeAsync()
|
||||||
|
{
|
||||||
|
if (_disposed)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var socket in _sockets)
|
||||||
|
{
|
||||||
|
socket.Closed -= OnConnectionClosed;
|
||||||
|
await socket.DisposeAsync().ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
_disposed = true;
|
_disposed = true;
|
||||||
|
|||||||
@@ -135,20 +135,15 @@ namespace Emby.Server.Implementations.TV
|
|||||||
return GetResult(episodes, request);
|
return GetResult(episodes, request);
|
||||||
}
|
}
|
||||||
|
|
||||||
public IEnumerable<Episode> GetNextUpEpisodes(NextUpQuery request, User user, IReadOnlyList<string> seriesKeys, DtoOptions dtoOptions)
|
private IEnumerable<Episode> GetNextUpEpisodes(NextUpQuery request, User user, IReadOnlyList<string> seriesKeys, DtoOptions dtoOptions)
|
||||||
{
|
{
|
||||||
// Avoid implicitly captured closure
|
var allNextUp = seriesKeys.Select(i => GetNextUp(i, user, dtoOptions, false));
|
||||||
var currentUser = user;
|
|
||||||
|
|
||||||
var allNextUp = seriesKeys
|
|
||||||
.Select(i => GetNextUp(i, currentUser, dtoOptions, false));
|
|
||||||
|
|
||||||
if (request.EnableRewatching)
|
if (request.EnableRewatching)
|
||||||
{
|
{
|
||||||
allNextUp = allNextUp.Concat(
|
allNextUp = allNextUp.Concat(
|
||||||
seriesKeys.Select(i => GetNextUp(i, currentUser, dtoOptions, true))
|
seriesKeys.Select(i => GetNextUp(i, user, dtoOptions, true)))
|
||||||
)
|
.OrderByDescending(i => i.LastWatchedDate);
|
||||||
.OrderByDescending(i => i.Item1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If viewing all next up for all series, remove first episodes
|
// If viewing all next up for all series, remove first episodes
|
||||||
@@ -161,23 +156,18 @@ namespace Emby.Server.Implementations.TV
|
|||||||
{
|
{
|
||||||
if (request.DisableFirstEpisode)
|
if (request.DisableFirstEpisode)
|
||||||
{
|
{
|
||||||
return i.Item1 != DateTime.MinValue;
|
return i.LastWatchedDate != DateTime.MinValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (alwaysEnableFirstEpisode || (i.Item1 != DateTime.MinValue && i.Item1.Date >= request.NextUpDateCutoff))
|
if (alwaysEnableFirstEpisode || (i.LastWatchedDate != DateTime.MinValue && i.LastWatchedDate.Date >= request.NextUpDateCutoff))
|
||||||
{
|
{
|
||||||
anyFound = true;
|
anyFound = true;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!anyFound && i.Item1 == DateTime.MinValue)
|
return !anyFound && i.LastWatchedDate == DateTime.MinValue;
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
})
|
})
|
||||||
.Select(i => i.Item2())
|
.Select(i => i.GetEpisodeFunction())
|
||||||
.Where(i => i != null);
|
.Where(i => i != null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,14 +185,13 @@ namespace Emby.Server.Implementations.TV
|
|||||||
/// Gets the next up.
|
/// Gets the next up.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>Task{Episode}.</returns>
|
/// <returns>Task{Episode}.</returns>
|
||||||
private Tuple<DateTime, Func<Episode>> GetNextUp(string seriesKey, User user, DtoOptions dtoOptions, bool rewatching)
|
private (DateTime LastWatchedDate, Func<Episode> GetEpisodeFunction) GetNextUp(string seriesKey, User user, DtoOptions dtoOptions, bool rewatching)
|
||||||
{
|
{
|
||||||
var lastQuery = new InternalItemsQuery(user)
|
var lastQuery = new InternalItemsQuery(user)
|
||||||
{
|
{
|
||||||
AncestorWithPresentationUniqueKey = null,
|
AncestorWithPresentationUniqueKey = null,
|
||||||
SeriesPresentationUniqueKey = seriesKey,
|
SeriesPresentationUniqueKey = seriesKey,
|
||||||
IncludeItemTypes = new[] { BaseItemKind.Episode },
|
IncludeItemTypes = new[] { BaseItemKind.Episode },
|
||||||
OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Descending) },
|
|
||||||
IsPlayed = true,
|
IsPlayed = true,
|
||||||
Limit = 1,
|
Limit = 1,
|
||||||
ParentIndexNumberNotEquals = 0,
|
ParentIndexNumberNotEquals = 0,
|
||||||
@@ -213,42 +202,38 @@ namespace Emby.Server.Implementations.TV
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (rewatching)
|
// If rewatching is enabled, sort first by date played and then by season and episode numbers
|
||||||
{
|
lastQuery.OrderBy = rewatching
|
||||||
// find last watched by date played, not by newest episode watched
|
? new[] { (ItemSortBy.DatePlayed, SortOrder.Descending), (ItemSortBy.ParentIndexNumber, SortOrder.Descending), (ItemSortBy.IndexNumber, SortOrder.Descending) }
|
||||||
lastQuery.OrderBy = new[] { (ItemSortBy.DatePlayed, SortOrder.Descending) };
|
: new[] { (ItemSortBy.ParentIndexNumber, SortOrder.Descending), (ItemSortBy.IndexNumber, SortOrder.Descending) };
|
||||||
}
|
|
||||||
|
|
||||||
var lastWatchedEpisode = _libraryManager.GetItemList(lastQuery).Cast<Episode>().FirstOrDefault();
|
var lastWatchedEpisode = _libraryManager.GetItemList(lastQuery).Cast<Episode>().FirstOrDefault();
|
||||||
|
|
||||||
Func<Episode> getEpisode = () =>
|
Episode GetEpisode()
|
||||||
{
|
{
|
||||||
var nextQuery = new InternalItemsQuery(user)
|
var nextQuery = new InternalItemsQuery(user)
|
||||||
{
|
{
|
||||||
AncestorWithPresentationUniqueKey = null,
|
AncestorWithPresentationUniqueKey = null,
|
||||||
SeriesPresentationUniqueKey = seriesKey,
|
SeriesPresentationUniqueKey = seriesKey,
|
||||||
IncludeItemTypes = new[] { BaseItemKind.Episode },
|
IncludeItemTypes = new[] { BaseItemKind.Episode },
|
||||||
OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) },
|
OrderBy = new[] { (ItemSortBy.ParentIndexNumber, SortOrder.Ascending), (ItemSortBy.IndexNumber, SortOrder.Ascending) },
|
||||||
Limit = 1,
|
Limit = 1,
|
||||||
IsPlayed = rewatching,
|
IsPlayed = rewatching,
|
||||||
IsVirtualItem = false,
|
IsVirtualItem = false,
|
||||||
ParentIndexNumberNotEquals = 0,
|
ParentIndexNumberNotEquals = 0,
|
||||||
MinSortName = lastWatchedEpisode?.SortName,
|
|
||||||
DtoOptions = dtoOptions
|
DtoOptions = dtoOptions
|
||||||
};
|
};
|
||||||
|
|
||||||
Episode nextEpisode;
|
// Locate the next up episode based on the last watched episode's season and episode number
|
||||||
if (rewatching)
|
var lastWatchedParentIndexNumber = lastWatchedEpisode?.ParentIndexNumber;
|
||||||
|
var lastWatchedIndexNumber = lastWatchedEpisode?.IndexNumberEnd ?? lastWatchedEpisode?.IndexNumber;
|
||||||
|
if (lastWatchedParentIndexNumber.HasValue && lastWatchedIndexNumber.HasValue)
|
||||||
{
|
{
|
||||||
nextQuery.Limit = 2;
|
nextQuery.MinParentAndIndexNumber = (lastWatchedParentIndexNumber.Value, lastWatchedIndexNumber.Value + 1);
|
||||||
// get watched episode after most recently watched
|
|
||||||
nextEpisode = _libraryManager.GetItemList(nextQuery).Cast<Episode>().ElementAtOrDefault(1);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
nextEpisode = _libraryManager.GetItemList(nextQuery).Cast<Episode>().FirstOrDefault();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var nextEpisode = _libraryManager.GetItemList(nextQuery).Cast<Episode>().FirstOrDefault();
|
||||||
|
|
||||||
if (_configurationManager.Configuration.DisplaySpecialsWithinSeasons)
|
if (_configurationManager.Configuration.DisplaySpecialsWithinSeasons)
|
||||||
{
|
{
|
||||||
var consideredEpisodes = _libraryManager.GetItemList(new InternalItemsQuery(user)
|
var consideredEpisodes = _libraryManager.GetItemList(new InternalItemsQuery(user)
|
||||||
@@ -297,7 +282,7 @@ namespace Emby.Server.Implementations.TV
|
|||||||
}
|
}
|
||||||
|
|
||||||
return nextEpisode;
|
return nextEpisode;
|
||||||
};
|
}
|
||||||
|
|
||||||
if (lastWatchedEpisode != null)
|
if (lastWatchedEpisode != null)
|
||||||
{
|
{
|
||||||
@@ -305,11 +290,11 @@ namespace Emby.Server.Implementations.TV
|
|||||||
|
|
||||||
var lastWatchedDate = userData.LastPlayedDate ?? DateTime.MinValue.AddDays(1);
|
var lastWatchedDate = userData.LastPlayedDate ?? DateTime.MinValue.AddDays(1);
|
||||||
|
|
||||||
return new Tuple<DateTime, Func<Episode>>(lastWatchedDate, getEpisode);
|
return (lastWatchedDate, GetEpisode);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return the first episode
|
// Return the first episode
|
||||||
return new Tuple<DateTime, Func<Episode>>(DateTime.MinValue, getEpisode);
|
return (DateTime.MinValue, GetEpisode);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static QueryResult<BaseItem> GetResult(IEnumerable<BaseItem> items, NextUpQuery query)
|
private static QueryResult<BaseItem> GetResult(IEnumerable<BaseItem> items, NextUpQuery query)
|
||||||
|
|||||||
@@ -285,7 +285,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
// Due to CTS.Token calling ThrowIfDisposed (https://github.com/dotnet/runtime/issues/29970) we have to "cache" the token
|
// Due to CTS.Token calling ThrowIfDisposed (https://github.com/dotnet/runtime/issues/29970) we have to "cache" the token
|
||||||
// since it gets disposed when ffmpeg exits
|
// since it gets disposed when ffmpeg exits
|
||||||
var cancellationToken = cancellationTokenSource.Token;
|
var cancellationToken = cancellationTokenSource.Token;
|
||||||
using var state = await StreamingHelpers.GetStreamingState(
|
var state = await StreamingHelpers.GetStreamingState(
|
||||||
streamingRequest,
|
streamingRequest,
|
||||||
Request,
|
Request,
|
||||||
_authContext,
|
_authContext,
|
||||||
@@ -1414,7 +1414,8 @@ namespace Jellyfin.Api.Controllers
|
|||||||
state.RunTimeTicks ?? 0,
|
state.RunTimeTicks ?? 0,
|
||||||
state.Request.SegmentContainer ?? string.Empty,
|
state.Request.SegmentContainer ?? string.Empty,
|
||||||
"hls1/main/",
|
"hls1/main/",
|
||||||
Request.QueryString.ToString());
|
Request.QueryString.ToString(),
|
||||||
|
EncodingHelper.IsCopyCodec(state.OutputVideoCodec));
|
||||||
var playlist = _dynamicHlsPlaylistGenerator.CreateMainPlaylist(request);
|
var playlist = _dynamicHlsPlaylistGenerator.CreateMainPlaylist(request);
|
||||||
|
|
||||||
return new FileContentResult(Encoding.UTF8.GetBytes(playlist), MimeTypes.GetMimeType("playlist.m3u8"));
|
return new FileContentResult(Encoding.UTF8.GetBytes(playlist), MimeTypes.GetMimeType("playlist.m3u8"));
|
||||||
@@ -1431,7 +1432,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
var cancellationTokenSource = new CancellationTokenSource();
|
var cancellationTokenSource = new CancellationTokenSource();
|
||||||
var cancellationToken = cancellationTokenSource.Token;
|
var cancellationToken = cancellationTokenSource.Token;
|
||||||
|
|
||||||
using var state = await StreamingHelpers.GetStreamingState(
|
var state = await StreamingHelpers.GetStreamingState(
|
||||||
streamingRequest,
|
streamingRequest,
|
||||||
Request,
|
Request,
|
||||||
_authContext,
|
_authContext,
|
||||||
@@ -1789,7 +1790,8 @@ namespace Jellyfin.Api.Controllers
|
|||||||
|| string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase))
|
|| string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
if (EncodingHelper.IsCopyCodec(codec)
|
if (EncodingHelper.IsCopyCodec(codec)
|
||||||
&& (string.Equals(state.VideoStream.CodecTag, "dovi", StringComparison.OrdinalIgnoreCase)
|
&& (string.Equals(state.VideoStream.VideoRangeType, "DOVI", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| string.Equals(state.VideoStream.CodecTag, "dovi", StringComparison.OrdinalIgnoreCase)
|
||||||
|| string.Equals(state.VideoStream.CodecTag, "dvh1", StringComparison.OrdinalIgnoreCase)
|
|| string.Equals(state.VideoStream.CodecTag, "dvh1", StringComparison.OrdinalIgnoreCase)
|
||||||
|| string.Equals(state.VideoStream.CodecTag, "dvhe", StringComparison.OrdinalIgnoreCase)))
|
|| string.Equals(state.VideoStream.CodecTag, "dvhe", StringComparison.OrdinalIgnoreCase)))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1724,6 +1724,11 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[FromQuery, Range(0, 100)] int quality = 90)
|
[FromQuery, Range(0, 100)] int quality = 90)
|
||||||
{
|
{
|
||||||
var brandingOptions = _serverConfigurationManager.GetConfiguration<BrandingOptions>("branding");
|
var brandingOptions = _serverConfigurationManager.GetConfiguration<BrandingOptions>("branding");
|
||||||
|
if (!brandingOptions.SplashscreenEnabled)
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
string splashscreenPath;
|
string splashscreenPath;
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(brandingOptions.SplashscreenLocation)
|
if (!string.IsNullOrWhiteSpace(brandingOptions.SplashscreenLocation)
|
||||||
@@ -1776,6 +1781,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Uploads a custom splashscreen.
|
/// Uploads a custom splashscreen.
|
||||||
|
/// The body is expected to the image contents base64 encoded.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
||||||
/// <response code="204">Successfully uploaded new splashscreen.</response>
|
/// <response code="204">Successfully uploaded new splashscreen.</response>
|
||||||
@@ -1799,7 +1805,13 @@ namespace Jellyfin.Api.Controllers
|
|||||||
return BadRequest("Error reading mimetype from uploaded image");
|
return BadRequest("Error reading mimetype from uploaded image");
|
||||||
}
|
}
|
||||||
|
|
||||||
var filePath = Path.Combine(_appPaths.DataPath, "splashscreen-upload" + MimeTypes.ToExtension(mimeType.Value));
|
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");
|
var brandingOptions = _serverConfigurationManager.GetConfiguration<BrandingOptions>("branding");
|
||||||
brandingOptions.SplashscreenLocation = filePath;
|
brandingOptions.SplashscreenLocation = filePath;
|
||||||
_serverConfigurationManager.SaveConfiguration("branding", brandingOptions);
|
_serverConfigurationManager.SaveConfiguration("branding", brandingOptions);
|
||||||
@@ -1812,6 +1824,29 @@ namespace Jellyfin.Api.Controllers
|
|||||||
return NoContent();
|
return NoContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Delete a custom splashscreen.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
||||||
|
/// <response code="204">Successfully deleted the custom splashscreen.</response>
|
||||||
|
/// <response code="403">User does not have permission to delete splashscreen..</response>
|
||||||
|
[HttpDelete("Branding/Splashscreen")]
|
||||||
|
[Authorize(Policy = Policies.RequiresElevation)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
|
public ActionResult DeleteCustomSplashscreen()
|
||||||
|
{
|
||||||
|
var brandingOptions = _serverConfigurationManager.GetConfiguration<BrandingOptions>("branding");
|
||||||
|
if (!string.IsNullOrEmpty(brandingOptions.SplashscreenLocation)
|
||||||
|
&& System.IO.File.Exists(brandingOptions.SplashscreenLocation))
|
||||||
|
{
|
||||||
|
System.IO.File.Delete(brandingOptions.SplashscreenLocation);
|
||||||
|
brandingOptions.SplashscreenLocation = null;
|
||||||
|
_serverConfigurationManager.SaveConfiguration("branding", brandingOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
return NoContent();
|
||||||
|
}
|
||||||
|
|
||||||
private static async Task<MemoryStream> GetMemoryStream(Stream inputStream)
|
private static async Task<MemoryStream> GetMemoryStream(Stream inputStream)
|
||||||
{
|
{
|
||||||
using var reader = new StreamReader(inputStream);
|
using var reader = new StreamReader(inputStream);
|
||||||
|
|||||||
@@ -98,7 +98,10 @@ namespace Jellyfin.Api.Controllers
|
|||||||
Limit = limit ?? 0
|
Limit = limit ?? 0
|
||||||
});
|
});
|
||||||
|
|
||||||
return new QueryResult<BaseItemDto>(peopleItems.Select(person => _dtoService.GetItemByNameDto(person, dtoOptions, null, user)).ToArray());
|
return new QueryResult<BaseItemDto>(
|
||||||
|
peopleItems
|
||||||
|
.Select(person => _dtoService.GetItemByNameDto(person, dtoOptions, null, user))
|
||||||
|
.ToArray());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ using System.Linq;
|
|||||||
using Jellyfin.Api.Constants;
|
using Jellyfin.Api.Constants;
|
||||||
using Jellyfin.Api.ModelBinders;
|
using Jellyfin.Api.ModelBinders;
|
||||||
using Jellyfin.Data.Enums;
|
using Jellyfin.Data.Enums;
|
||||||
|
using Jellyfin.Extensions;
|
||||||
using MediaBrowser.Controller.Drawing;
|
using MediaBrowser.Controller.Drawing;
|
||||||
using MediaBrowser.Controller.Dto;
|
using MediaBrowser.Controller.Dto;
|
||||||
using MediaBrowser.Controller.Entities;
|
using MediaBrowser.Controller.Entities;
|
||||||
@@ -187,7 +188,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
result.AlbumArtist = album.AlbumArtist;
|
result.AlbumArtist = album.AlbumArtist;
|
||||||
break;
|
break;
|
||||||
case Audio song:
|
case Audio song:
|
||||||
result.AlbumArtist = song.AlbumArtists?[0];
|
result.AlbumArtist = song.AlbumArtists?.FirstOrDefault();
|
||||||
result.Artists = song.Artists;
|
result.Artists = song.Artists;
|
||||||
|
|
||||||
MusicAlbum musicAlbum = song.AlbumEntity;
|
MusicAlbum musicAlbum = song.AlbumEntity;
|
||||||
|
|||||||
@@ -117,7 +117,13 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[FromQuery] bool enableRedirection = true)
|
[FromQuery] bool enableRedirection = true)
|
||||||
{
|
{
|
||||||
var deviceProfile = GetDeviceProfile(container, transcodingContainer, audioCodec, transcodingProtocol, breakOnNonKeyFrames, transcodingAudioChannels, maxAudioSampleRate, maxAudioBitDepth, maxAudioChannels);
|
var deviceProfile = GetDeviceProfile(container, transcodingContainer, audioCodec, transcodingProtocol, breakOnNonKeyFrames, transcodingAudioChannels, maxAudioSampleRate, maxAudioBitDepth, maxAudioChannels);
|
||||||
(await _authorizationContext.GetAuthorizationInfo(Request).ConfigureAwait(false)).DeviceId = deviceId;
|
var authorizationInfo = await _authorizationContext.GetAuthorizationInfo(Request).ConfigureAwait(false);
|
||||||
|
authorizationInfo.DeviceId = deviceId;
|
||||||
|
|
||||||
|
if (!userId.HasValue || userId.Value.Equals(Guid.Empty))
|
||||||
|
{
|
||||||
|
userId = authorizationInfo.UserId;
|
||||||
|
}
|
||||||
|
|
||||||
var authInfo = await _authorizationContext.GetAuthorizationInfo(Request).ConfigureAwait(false);
|
var authInfo = await _authorizationContext.GetAuthorizationInfo(Request).ConfigureAwait(false);
|
||||||
|
|
||||||
|
|||||||
@@ -282,16 +282,19 @@ namespace Jellyfin.Api.Controllers
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var success = await _userManager.AuthenticateUser(
|
if (!HttpContext.User.IsInRole(UserRoles.Administrator))
|
||||||
user.Username,
|
|
||||||
request.CurrentPw,
|
|
||||||
request.CurrentPw,
|
|
||||||
HttpContext.GetNormalizedRemoteIp().ToString(),
|
|
||||||
false).ConfigureAwait(false);
|
|
||||||
|
|
||||||
if (success == null)
|
|
||||||
{
|
{
|
||||||
return StatusCode(StatusCodes.Status403Forbidden, "Invalid user or password entered.");
|
var success = await _userManager.AuthenticateUser(
|
||||||
|
user.Username,
|
||||||
|
request.CurrentPw,
|
||||||
|
request.CurrentPw,
|
||||||
|
HttpContext.GetNormalizedRemoteIp().ToString(),
|
||||||
|
false).ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (success == null)
|
||||||
|
{
|
||||||
|
return StatusCode(StatusCodes.Status403Forbidden, "Invalid user or password entered.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await _userManager.ChangePassword(user, request.NewPw).ConfigureAwait(false);
|
await _userManager.ChangePassword(user, request.NewPw).ConfigureAwait(false);
|
||||||
|
|||||||
@@ -427,7 +427,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
StreamOptions = streamOptions
|
StreamOptions = streamOptions
|
||||||
};
|
};
|
||||||
|
|
||||||
using var state = await StreamingHelpers.GetStreamingState(
|
var state = await StreamingHelpers.GetStreamingState(
|
||||||
streamingRequest,
|
streamingRequest,
|
||||||
Request,
|
Request,
|
||||||
_authContext,
|
_authContext,
|
||||||
|
|||||||
@@ -216,7 +216,7 @@ namespace Jellyfin.Api.Helpers
|
|||||||
var sdrVideoUrl = ReplaceProfile(playlistUrl, "hevc", string.Join(',', requestedVideoProfiles), "main");
|
var sdrVideoUrl = ReplaceProfile(playlistUrl, "hevc", string.Join(',', requestedVideoProfiles), "main");
|
||||||
sdrVideoUrl += "&AllowVideoStreamCopy=false";
|
sdrVideoUrl += "&AllowVideoStreamCopy=false";
|
||||||
|
|
||||||
var sdrOutputVideoBitrate = _encodingHelper.GetVideoBitrateParamValue(state.VideoRequest, state.VideoStream, state.OutputVideoCodec) ?? 0;
|
var sdrOutputVideoBitrate = _encodingHelper.GetVideoBitrateParamValue(state.VideoRequest, state.VideoStream, state.OutputVideoCodec);
|
||||||
var sdrOutputAudioBitrate = _encodingHelper.GetAudioBitrateParam(state.VideoRequest, state.AudioStream) ?? 0;
|
var sdrOutputAudioBitrate = _encodingHelper.GetAudioBitrateParam(state.VideoRequest, state.AudioStream) ?? 0;
|
||||||
var sdrTotalBitrate = sdrOutputAudioBitrate + sdrOutputVideoBitrate;
|
var sdrTotalBitrate = sdrOutputAudioBitrate + sdrOutputVideoBitrate;
|
||||||
|
|
||||||
|
|||||||
@@ -179,7 +179,7 @@ namespace Jellyfin.Api.Helpers
|
|||||||
{
|
{
|
||||||
containerInternal = streamingRequest.Static ?
|
containerInternal = streamingRequest.Static ?
|
||||||
StreamBuilder.NormalizeMediaSourceFormatIntoSingleContainer(state.InputContainer, null, DlnaProfileType.Audio)
|
StreamBuilder.NormalizeMediaSourceFormatIntoSingleContainer(state.InputContainer, null, DlnaProfileType.Audio)
|
||||||
: GetOutputFileExtension(state);
|
: GetOutputFileExtension(state, mediaSource);
|
||||||
}
|
}
|
||||||
|
|
||||||
state.OutputContainer = (containerInternal ?? string.Empty).TrimStart('.');
|
state.OutputContainer = (containerInternal ?? string.Empty).TrimStart('.');
|
||||||
@@ -235,7 +235,7 @@ namespace Jellyfin.Api.Helpers
|
|||||||
ApplyDeviceProfileSettings(state, dlnaManager, deviceManager, httpRequest, streamingRequest.DeviceProfileId, streamingRequest.Static);
|
ApplyDeviceProfileSettings(state, dlnaManager, deviceManager, httpRequest, streamingRequest.DeviceProfileId, streamingRequest.Static);
|
||||||
|
|
||||||
var ext = string.IsNullOrWhiteSpace(state.OutputContainer)
|
var ext = string.IsNullOrWhiteSpace(state.OutputContainer)
|
||||||
? GetOutputFileExtension(state)
|
? GetOutputFileExtension(state, mediaSource)
|
||||||
: ("." + state.OutputContainer);
|
: ("." + state.OutputContainer);
|
||||||
|
|
||||||
state.OutputFilePath = GetOutputFilePath(state, ext!, serverConfigurationManager, streamingRequest.DeviceId, streamingRequest.PlaySessionId);
|
state.OutputFilePath = GetOutputFilePath(state, ext!, serverConfigurationManager, streamingRequest.DeviceId, streamingRequest.PlaySessionId);
|
||||||
@@ -312,7 +312,7 @@ namespace Jellyfin.Api.Helpers
|
|||||||
|
|
||||||
responseHeaders.Add(
|
responseHeaders.Add(
|
||||||
"contentFeatures.dlna.org",
|
"contentFeatures.dlna.org",
|
||||||
ContentFeatureBuilder.BuildVideoHeader(profile, state.OutputContainer, videoCodec, audioCodec, state.OutputWidth, state.OutputHeight, state.TargetVideoBitDepth, state.OutputVideoBitrate, state.TargetTimestamp, isStaticallyStreamed, state.RunTimeTicks, state.TargetVideoProfile, state.TargetVideoLevel, state.TargetFramerate, state.TargetPacketLength, state.TranscodeSeekInfo, state.IsTargetAnamorphic, state.IsTargetInterlaced, state.TargetRefFrames, state.TargetVideoStreamCount, state.TargetAudioStreamCount, state.TargetVideoCodecTag, state.IsTargetAVC).FirstOrDefault() ?? string.Empty);
|
ContentFeatureBuilder.BuildVideoHeader(profile, state.OutputContainer, videoCodec, audioCodec, state.OutputWidth, state.OutputHeight, state.TargetVideoBitDepth, state.OutputVideoBitrate, state.TargetTimestamp, isStaticallyStreamed, state.RunTimeTicks, state.TargetVideoProfile, state.TargetVideoRangeType, state.TargetVideoLevel, state.TargetFramerate, state.TargetPacketLength, state.TranscodeSeekInfo, state.IsTargetAnamorphic, state.IsTargetInterlaced, state.TargetRefFrames, state.TargetVideoStreamCount, state.TargetAudioStreamCount, state.TargetVideoCodecTag, state.IsTargetAVC).FirstOrDefault() ?? string.Empty);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -409,8 +409,9 @@ namespace Jellyfin.Api.Helpers
|
|||||||
/// Gets the output file extension.
|
/// Gets the output file extension.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="state">The state.</param>
|
/// <param name="state">The state.</param>
|
||||||
|
/// <param name="mediaSource">The mediaSource.</param>
|
||||||
/// <returns>System.String.</returns>
|
/// <returns>System.String.</returns>
|
||||||
private static string? GetOutputFileExtension(StreamState state)
|
private static string? GetOutputFileExtension(StreamState state, MediaSourceInfo? mediaSource)
|
||||||
{
|
{
|
||||||
var ext = Path.GetExtension(state.RequestedUrl);
|
var ext = Path.GetExtension(state.RequestedUrl);
|
||||||
|
|
||||||
@@ -425,7 +426,7 @@ namespace Jellyfin.Api.Helpers
|
|||||||
var videoCodec = state.Request.VideoCodec;
|
var videoCodec = state.Request.VideoCodec;
|
||||||
|
|
||||||
if (string.Equals(videoCodec, "h264", StringComparison.OrdinalIgnoreCase) ||
|
if (string.Equals(videoCodec, "h264", StringComparison.OrdinalIgnoreCase) ||
|
||||||
string.Equals(videoCodec, "h265", StringComparison.OrdinalIgnoreCase))
|
string.Equals(videoCodec, "hevc", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
return ".ts";
|
return ".ts";
|
||||||
}
|
}
|
||||||
@@ -474,6 +475,13 @@ namespace Jellyfin.Api.Helpers
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fallback to the container of mediaSource
|
||||||
|
if (!string.IsNullOrEmpty(mediaSource?.Container))
|
||||||
|
{
|
||||||
|
var idx = mediaSource.Container.IndexOf(',', StringComparison.OrdinalIgnoreCase);
|
||||||
|
return '.' + (idx == -1 ? mediaSource.Container : mediaSource.Container[..idx]).Trim();
|
||||||
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -533,6 +541,7 @@ namespace Jellyfin.Api.Helpers
|
|||||||
state.TargetVideoBitDepth,
|
state.TargetVideoBitDepth,
|
||||||
state.OutputVideoBitrate,
|
state.OutputVideoBitrate,
|
||||||
state.TargetVideoProfile,
|
state.TargetVideoProfile,
|
||||||
|
state.TargetVideoRangeType,
|
||||||
state.TargetVideoLevel,
|
state.TargetVideoLevel,
|
||||||
state.TargetFramerate,
|
state.TargetFramerate,
|
||||||
state.TargetPacketLength,
|
state.TargetPacketLength,
|
||||||
|
|||||||
@@ -654,8 +654,8 @@ namespace Jellyfin.Api.Helpers
|
|||||||
{
|
{
|
||||||
if (EnableThrottling(state))
|
if (EnableThrottling(state))
|
||||||
{
|
{
|
||||||
transcodingJob.TranscodingThrottler = state.TranscodingThrottler = new TranscodingThrottler(transcodingJob, new Logger<TranscodingThrottler>(new LoggerFactory()), _serverConfigurationManager, _fileSystem);
|
transcodingJob.TranscodingThrottler = new TranscodingThrottler(transcodingJob, new Logger<TranscodingThrottler>(new LoggerFactory()), _serverConfigurationManager, _fileSystem, _mediaEncoder);
|
||||||
state.TranscodingThrottler.Start();
|
transcodingJob.TranscodingThrottler.Start();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -663,18 +663,11 @@ namespace Jellyfin.Api.Helpers
|
|||||||
{
|
{
|
||||||
var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
|
var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
|
||||||
|
|
||||||
// enable throttling when NOT using hardware acceleration
|
return state.InputProtocol == MediaProtocol.File &&
|
||||||
if (string.IsNullOrEmpty(encodingOptions.HardwareAccelerationType))
|
state.RunTimeTicks.HasValue &&
|
||||||
{
|
state.RunTimeTicks.Value >= TimeSpan.FromMinutes(5).Ticks &&
|
||||||
return state.InputProtocol == MediaProtocol.File &&
|
state.IsInputVideo &&
|
||||||
state.RunTimeTicks.HasValue &&
|
state.VideoType == VideoType.VideoFile;
|
||||||
state.RunTimeTicks.Value >= TimeSpan.FromMinutes(5).Ticks &&
|
|
||||||
state.IsInputVideo &&
|
|
||||||
state.VideoType == VideoType.VideoFile &&
|
|
||||||
!EncodingHelper.IsCopyCodec(state.OutputVideoCodec);
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authorization" Version="6.0.5" />
|
<PackageReference Include="Microsoft.AspNetCore.Authorization" Version="6.0.9" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" />
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.2.3" />
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.2.3" />
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore.ReDoc" Version="6.3.1" />
|
<PackageReference Include="Swashbuckle.AspNetCore.ReDoc" Version="6.3.1" />
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using MediaBrowser.Common.Configuration;
|
using MediaBrowser.Common.Configuration;
|
||||||
|
using MediaBrowser.Controller.MediaEncoding;
|
||||||
using MediaBrowser.Model.Configuration;
|
using MediaBrowser.Model.Configuration;
|
||||||
using MediaBrowser.Model.IO;
|
using MediaBrowser.Model.IO;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
@@ -17,6 +18,7 @@ namespace Jellyfin.Api.Models.PlaybackDtos
|
|||||||
private readonly ILogger<TranscodingThrottler> _logger;
|
private readonly ILogger<TranscodingThrottler> _logger;
|
||||||
private readonly IConfigurationManager _config;
|
private readonly IConfigurationManager _config;
|
||||||
private readonly IFileSystem _fileSystem;
|
private readonly IFileSystem _fileSystem;
|
||||||
|
private readonly IMediaEncoder _mediaEncoder;
|
||||||
private Timer? _timer;
|
private Timer? _timer;
|
||||||
private bool _isPaused;
|
private bool _isPaused;
|
||||||
|
|
||||||
@@ -27,12 +29,14 @@ namespace Jellyfin.Api.Models.PlaybackDtos
|
|||||||
/// <param name="logger">Instance of the <see cref="ILogger{TranscodingThrottler}"/> interface.</param>
|
/// <param name="logger">Instance of the <see cref="ILogger{TranscodingThrottler}"/> interface.</param>
|
||||||
/// <param name="config">Instance of the <see cref="IConfigurationManager"/> interface.</param>
|
/// <param name="config">Instance of the <see cref="IConfigurationManager"/> interface.</param>
|
||||||
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
|
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
|
||||||
public TranscodingThrottler(TranscodingJobDto job, ILogger<TranscodingThrottler> logger, IConfigurationManager config, IFileSystem fileSystem)
|
/// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
|
||||||
|
public TranscodingThrottler(TranscodingJobDto job, ILogger<TranscodingThrottler> logger, IConfigurationManager config, IFileSystem fileSystem, IMediaEncoder mediaEncoder)
|
||||||
{
|
{
|
||||||
_job = job;
|
_job = job;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_config = config;
|
_config = config;
|
||||||
_fileSystem = fileSystem;
|
_fileSystem = fileSystem;
|
||||||
|
_mediaEncoder = mediaEncoder;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -55,7 +59,8 @@ namespace Jellyfin.Api.Models.PlaybackDtos
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await _job.Process!.StandardInput.WriteLineAsync().ConfigureAwait(false);
|
var resumeKey = _mediaEncoder.IsPkeyPauseSupported ? "u" : Environment.NewLine;
|
||||||
|
await _job.Process!.StandardInput.WriteAsync(resumeKey).ConfigureAwait(false);
|
||||||
_isPaused = false;
|
_isPaused = false;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -125,11 +130,13 @@ namespace Jellyfin.Api.Models.PlaybackDtos
|
|||||||
{
|
{
|
||||||
if (!_isPaused)
|
if (!_isPaused)
|
||||||
{
|
{
|
||||||
_logger.LogDebug("Sending pause command to ffmpeg");
|
var pauseKey = _mediaEncoder.IsPkeyPauseSupported ? "p" : "c";
|
||||||
|
|
||||||
|
_logger.LogDebug("Sending pause command [{Key}] to ffmpeg", pauseKey);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await _job.Process!.StandardInput.WriteAsync("c").ConfigureAwait(false);
|
await _job.Process!.StandardInput.WriteAsync(pauseKey).ConfigureAwait(false);
|
||||||
_isPaused = true;
|
_isPaused = true;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
|||||||
@@ -47,11 +47,6 @@ namespace Jellyfin.Api.Models.StreamingDtos
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the transcoding throttler.
|
|
||||||
/// </summary>
|
|
||||||
public TranscodingThrottler? TranscodingThrottler { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the video request.
|
/// Gets the video request.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -191,11 +186,8 @@ namespace Jellyfin.Api.Models.StreamingDtos
|
|||||||
{
|
{
|
||||||
_mediaSourceManager.CloseLiveStream(MediaSource.LiveStreamId).GetAwaiter().GetResult();
|
_mediaSourceManager.CloseLiveStream(MediaSource.LiveStreamId).GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
TranscodingThrottler?.Dispose();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
TranscodingThrottler = null;
|
|
||||||
TranscodingJob = null;
|
TranscodingJob = null;
|
||||||
|
|
||||||
_disposed = true;
|
_disposed = true;
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Authors>Jellyfin Contributors</Authors>
|
<Authors>Jellyfin Contributors</Authors>
|
||||||
<PackageId>Jellyfin.Data</PackageId>
|
<PackageId>Jellyfin.Data</PackageId>
|
||||||
<VersionPrefix>10.8.0</VersionPrefix>
|
<VersionPrefix>10.8.7</VersionPrefix>
|
||||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|||||||
@@ -18,8 +18,9 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="BlurHashSharp" Version="1.2.0" />
|
<PackageReference Include="BlurHashSharp" Version="1.2.0" />
|
||||||
<PackageReference Include="BlurHashSharp.SkiaSharp" Version="1.2.0" />
|
<PackageReference Include="BlurHashSharp.SkiaSharp" Version="1.2.0" />
|
||||||
<PackageReference Include="SkiaSharp" Version="2.88.1-preview.1" />
|
<PackageReference Include="SkiaSharp" Version="2.88.2" />
|
||||||
<PackageReference Include="SkiaSharp.NativeAssets.Linux" Version="2.88.1-preview.1" />
|
<PackageReference Include="SkiaSharp.NativeAssets.Linux" Version="2.88.2" />
|
||||||
|
<PackageReference Include="SkiaSharp.Svg" Version="1.60.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ using MediaBrowser.Controller.Drawing;
|
|||||||
using MediaBrowser.Model.Drawing;
|
using MediaBrowser.Model.Drawing;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using SkiaSharp;
|
using SkiaSharp;
|
||||||
using static Jellyfin.Drawing.Skia.SkiaHelper;
|
using SKSvg = SkiaSharp.Extended.Svg.SKSvg;
|
||||||
|
|
||||||
namespace Jellyfin.Drawing.Skia
|
namespace Jellyfin.Drawing.Skia
|
||||||
{
|
{
|
||||||
@@ -19,8 +19,7 @@ namespace Jellyfin.Drawing.Skia
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class SkiaEncoder : IImageEncoder
|
public class SkiaEncoder : IImageEncoder
|
||||||
{
|
{
|
||||||
private static readonly HashSet<string> _transparentImageTypes
|
private static readonly HashSet<string> _transparentImageTypes = new(StringComparer.OrdinalIgnoreCase) { ".png", ".gif", ".webp" };
|
||||||
= new HashSet<string>(StringComparer.OrdinalIgnoreCase) { ".png", ".gif", ".webp" };
|
|
||||||
|
|
||||||
private readonly ILogger<SkiaEncoder> _logger;
|
private readonly ILogger<SkiaEncoder> _logger;
|
||||||
private readonly IApplicationPaths _appPaths;
|
private readonly IApplicationPaths _appPaths;
|
||||||
@@ -71,7 +70,7 @@ namespace Jellyfin.Drawing.Skia
|
|||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public IReadOnlyCollection<ImageFormat> SupportedOutputFormats
|
public IReadOnlyCollection<ImageFormat> SupportedOutputFormats
|
||||||
=> new HashSet<ImageFormat>() { ImageFormat.Webp, ImageFormat.Jpg, ImageFormat.Png };
|
=> new HashSet<ImageFormat> { ImageFormat.Webp, ImageFormat.Jpg, ImageFormat.Png };
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Check if the native lib is available.
|
/// Check if the native lib is available.
|
||||||
@@ -109,9 +108,7 @@ namespace Jellyfin.Drawing.Skia
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
/// <exception cref="ArgumentNullException">The path is null.</exception>
|
|
||||||
/// <exception cref="FileNotFoundException">The path is not valid.</exception>
|
/// <exception cref="FileNotFoundException">The path is not valid.</exception>
|
||||||
/// <exception cref="SkiaCodecException">The file at the specified path could not be used to generate a codec.</exception>
|
|
||||||
public ImageDimensions GetImageSize(string path)
|
public ImageDimensions GetImageSize(string path)
|
||||||
{
|
{
|
||||||
if (!File.Exists(path))
|
if (!File.Exists(path))
|
||||||
@@ -119,12 +116,27 @@ namespace Jellyfin.Drawing.Skia
|
|||||||
throw new FileNotFoundException("File not found", path);
|
throw new FileNotFoundException("File not found", path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var extension = Path.GetExtension(path.AsSpan());
|
||||||
|
if (extension.Equals(".svg", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
var svg = new SKSvg();
|
||||||
|
svg.Load(path);
|
||||||
|
return new ImageDimensions(Convert.ToInt32(svg.Picture.CullRect.Width), Convert.ToInt32(svg.Picture.CullRect.Height));
|
||||||
|
}
|
||||||
|
|
||||||
using var codec = SKCodec.Create(path, out SKCodecResult result);
|
using var codec = SKCodec.Create(path, out SKCodecResult result);
|
||||||
EnsureSuccess(result);
|
switch (result)
|
||||||
|
{
|
||||||
var info = codec.Info;
|
case SKCodecResult.Success:
|
||||||
|
var info = codec.Info;
|
||||||
return new ImageDimensions(info.Width, info.Height);
|
return new ImageDimensions(info.Width, info.Height);
|
||||||
|
case SKCodecResult.Unimplemented:
|
||||||
|
_logger.LogDebug("Image format not supported: {FilePath}", path);
|
||||||
|
return new ImageDimensions(0, 0);
|
||||||
|
default:
|
||||||
|
_logger.LogError("Unable to determine image dimensions for {FilePath}: {SkCodecResult}", path, result);
|
||||||
|
return new ImageDimensions(0, 0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@@ -138,6 +150,13 @@ namespace Jellyfin.Drawing.Skia
|
|||||||
throw new ArgumentNullException(nameof(path));
|
throw new ArgumentNullException(nameof(path));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var extension = Path.GetExtension(path.AsSpan()).TrimStart('.');
|
||||||
|
if (!SupportedInputFormats.Contains(extension, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Unable to compute blur hash due to unsupported format: {ImagePath}", path);
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
// Any larger than 128x128 is too slow and there's no visually discernible difference
|
// Any larger than 128x128 is too slow and there's no visually discernible difference
|
||||||
return BlurHashEncoder.Encode(xComp, yComp, path, 128, 128);
|
return BlurHashEncoder.Encode(xComp, yComp, path, 128, 128);
|
||||||
}
|
}
|
||||||
@@ -378,6 +397,13 @@ namespace Jellyfin.Drawing.Skia
|
|||||||
throw new ArgumentException("String can't be empty.", nameof(outputPath));
|
throw new ArgumentException("String can't be empty.", nameof(outputPath));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var inputFormat = Path.GetExtension(inputPath.AsSpan()).TrimStart('.');
|
||||||
|
if (!SupportedInputFormats.Contains(inputFormat, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Unable to encode image due to unsupported format: {ImagePath}", inputPath);
|
||||||
|
return inputPath;
|
||||||
|
}
|
||||||
|
|
||||||
var skiaOutputFormat = GetImageFormat(outputFormat);
|
var skiaOutputFormat = GetImageFormat(outputFormat);
|
||||||
|
|
||||||
var hasBackgroundColor = !string.IsNullOrWhiteSpace(options.BackgroundColor);
|
var hasBackgroundColor = !string.IsNullOrWhiteSpace(options.BackgroundColor);
|
||||||
|
|||||||
@@ -8,19 +8,6 @@ namespace Jellyfin.Drawing.Skia
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static class SkiaHelper
|
public static class SkiaHelper
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Ensures the result is a success
|
|
||||||
/// by throwing an exception when that's not the case.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="result">The result returned by Skia.</param>
|
|
||||||
public static void EnsureSuccess(SKCodecResult result)
|
|
||||||
{
|
|
||||||
if (result != SKCodecResult.Success)
|
|
||||||
{
|
|
||||||
throw new SkiaCodecException(result);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the next valid image as a bitmap.
|
/// Gets the next valid image as a bitmap.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -27,13 +27,13 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="System.Linq.Async" Version="6.0.1" />
|
<PackageReference Include="System.Linq.Async" Version="6.0.1" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.5" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.9" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.5" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.9" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.5">
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.9">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.5">
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.9">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
|
|||||||
+657
@@ -0,0 +1,657 @@
|
|||||||
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using Jellyfin.Server.Implementations;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Jellyfin.Server.Implementations.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(JellyfinDb))]
|
||||||
|
[Migration("20221022080052_AddIndexActivityLogsDateCreated")]
|
||||||
|
partial class AddIndexActivityLogsDateCreated
|
||||||
|
{
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasDefaultSchema("jellyfin")
|
||||||
|
.HasAnnotation("ProductVersion", "6.0.9");
|
||||||
|
|
||||||
|
modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("DayOfWeek")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<double>("EndHour")
|
||||||
|
.HasColumnType("REAL");
|
||||||
|
|
||||||
|
b.Property<double>("StartHour")
|
||||||
|
.HasColumnType("REAL");
|
||||||
|
|
||||||
|
b.Property<Guid>("UserId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("AccessSchedules", "jellyfin");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTime>("DateCreated")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("ItemId")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("LogSeverity")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(512)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Overview")
|
||||||
|
.HasMaxLength(512)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<uint>("RowVersion")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("ShortOverview")
|
||||||
|
.HasMaxLength(512)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Type")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<Guid>("UserId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("DateCreated");
|
||||||
|
|
||||||
|
b.ToTable("ActivityLogs", "jellyfin");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Jellyfin.Data.Entities.CustomItemDisplayPreferences", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Client")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(32)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<Guid>("ItemId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Key")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<Guid>("UserId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Value")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("UserId", "ItemId", "Client", "Key")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("CustomItemDisplayPreferences", "jellyfin");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("ChromecastVersion")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Client")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(32)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("DashboardTheme")
|
||||||
|
.HasMaxLength(32)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("EnableNextVideoInfoOverlay")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int?>("IndexBy")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<Guid>("ItemId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("ScrollDirection")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<bool>("ShowBackdrop")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<bool>("ShowSidebar")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("SkipBackwardLength")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("SkipForwardLength")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("TvHome")
|
||||||
|
.HasMaxLength(32)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<Guid>("UserId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("UserId", "ItemId", "Client")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("DisplayPreferences", "jellyfin");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("DisplayPreferencesId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("Order")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("Type")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("DisplayPreferencesId");
|
||||||
|
|
||||||
|
b.ToTable("HomeSection", "jellyfin");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTime>("LastModified")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Path")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(512)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<Guid?>("UserId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("UserId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("ImageInfos", "jellyfin");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Client")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(32)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int?>("IndexBy")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<Guid>("ItemId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("RememberIndexing")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<bool>("RememberSorting")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("SortBy")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("SortOrder")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<Guid>("UserId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("ViewType")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("ItemDisplayPreferences", "jellyfin");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("Kind")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<Guid?>("Permission_Permissions_Guid")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<uint>("RowVersion")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<Guid?>("UserId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("Value")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("UserId", "Kind")
|
||||||
|
.IsUnique()
|
||||||
|
.HasFilter("[UserId] IS NOT NULL");
|
||||||
|
|
||||||
|
b.ToTable("Permissions", "jellyfin");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("Kind")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<Guid?>("Preference_Preferences_Guid")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<uint>("RowVersion")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<Guid?>("UserId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Value")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(65535)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("UserId", "Kind")
|
||||||
|
.IsUnique()
|
||||||
|
.HasFilter("[UserId] IS NOT NULL");
|
||||||
|
|
||||||
|
b.ToTable("Preferences", "jellyfin");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Jellyfin.Data.Entities.Security.ApiKey", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("AccessToken")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("DateCreated")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("DateLastActivity")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("AccessToken")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("ApiKeys", "jellyfin");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("AccessToken")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("AppName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("AppVersion")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(32)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("DateCreated")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("DateLastActivity")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("DateModified")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("DeviceId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("DeviceName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("IsActive")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<Guid>("UserId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("DeviceId");
|
||||||
|
|
||||||
|
b.HasIndex("AccessToken", "DateLastActivity");
|
||||||
|
|
||||||
|
b.HasIndex("DeviceId", "DateLastActivity");
|
||||||
|
|
||||||
|
b.HasIndex("UserId", "DeviceId");
|
||||||
|
|
||||||
|
b.ToTable("Devices", "jellyfin");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Jellyfin.Data.Entities.Security.DeviceOptions", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("CustomName")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("DeviceId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("DeviceId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("DeviceOptions", "jellyfin");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("AudioLanguagePreference")
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("AuthenticationProviderId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("DisplayCollectionsView")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<bool>("DisplayMissingEpisodes")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("EasyPassword")
|
||||||
|
.HasMaxLength(65535)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("EnableAutoLogin")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<bool>("EnableLocalPassword")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<bool>("EnableNextEpisodeAutoPlay")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<bool>("EnableUserPreferenceAccess")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<bool>("HidePlayedInLatest")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<long>("InternalId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("InvalidLoginAttemptCount")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("LastActivityDate")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("LastLoginDate")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int?>("LoginAttemptsBeforeLockout")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("MaxActiveSessions")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int?>("MaxParentalAgeRating")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<bool>("MustUpdatePassword")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Password")
|
||||||
|
.HasMaxLength(65535)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("PasswordResetProviderId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("PlayDefaultAudioTrack")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<bool>("RememberAudioSelections")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<bool>("RememberSubtitleSelections")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int?>("RemoteClientBitrateLimit")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<uint>("RowVersion")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("SubtitleLanguagePreference")
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("SubtitleMode")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("SyncPlayAccess")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Username")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.UseCollation("NOCASE");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Username")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("Users", "jellyfin");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Jellyfin.Data.Entities.User", null)
|
||||||
|
.WithMany("AccessSchedules")
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Jellyfin.Data.Entities.User", null)
|
||||||
|
.WithMany("DisplayPreferences")
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Jellyfin.Data.Entities.DisplayPreferences", null)
|
||||||
|
.WithMany("HomeSections")
|
||||||
|
.HasForeignKey("DisplayPreferencesId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Jellyfin.Data.Entities.User", null)
|
||||||
|
.WithOne("ProfileImage")
|
||||||
|
.HasForeignKey("Jellyfin.Data.Entities.ImageInfo", "UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Jellyfin.Data.Entities.User", null)
|
||||||
|
.WithMany("ItemDisplayPreferences")
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Jellyfin.Data.Entities.User", null)
|
||||||
|
.WithMany("Permissions")
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Jellyfin.Data.Entities.User", null)
|
||||||
|
.WithMany("Preferences")
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Jellyfin.Data.Entities.User", "User")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("HomeSections");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("AccessSchedules");
|
||||||
|
|
||||||
|
b.Navigation("DisplayPreferences");
|
||||||
|
|
||||||
|
b.Navigation("ItemDisplayPreferences");
|
||||||
|
|
||||||
|
b.Navigation("Permissions");
|
||||||
|
|
||||||
|
b.Navigation("Preferences");
|
||||||
|
|
||||||
|
b.Navigation("ProfileImage");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+28
@@ -0,0 +1,28 @@
|
|||||||
|
#pragma warning disable CS1591, SA1601
|
||||||
|
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Jellyfin.Server.Implementations.Migrations
|
||||||
|
{
|
||||||
|
public partial class AddIndexActivityLogsDateCreated : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_ActivityLogs_DateCreated",
|
||||||
|
schema: "jellyfin",
|
||||||
|
table: "ActivityLogs",
|
||||||
|
column: "DateCreated");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "IX_ActivityLogs_DateCreated",
|
||||||
|
schema: "jellyfin",
|
||||||
|
table: "ActivityLogs");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,8 @@ using Microsoft.EntityFrameworkCore;
|
|||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
namespace Jellyfin.Server.Implementations.Migrations
|
namespace Jellyfin.Server.Implementations.Migrations
|
||||||
{
|
{
|
||||||
[DbContext(typeof(JellyfinDb))]
|
[DbContext(typeof(JellyfinDb))]
|
||||||
@@ -15,7 +17,7 @@ namespace Jellyfin.Server.Implementations.Migrations
|
|||||||
#pragma warning disable 612, 618
|
#pragma warning disable 612, 618
|
||||||
modelBuilder
|
modelBuilder
|
||||||
.HasDefaultSchema("jellyfin")
|
.HasDefaultSchema("jellyfin")
|
||||||
.HasAnnotation("ProductVersion", "5.0.7");
|
.HasAnnotation("ProductVersion", "6.0.9");
|
||||||
|
|
||||||
modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
|
modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
|
||||||
{
|
{
|
||||||
@@ -39,7 +41,7 @@ namespace Jellyfin.Server.Implementations.Migrations
|
|||||||
|
|
||||||
b.HasIndex("UserId");
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
b.ToTable("AccessSchedules");
|
b.ToTable("AccessSchedules", "jellyfin");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b =>
|
modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b =>
|
||||||
@@ -85,7 +87,9 @@ namespace Jellyfin.Server.Implementations.Migrations
|
|||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
b.ToTable("ActivityLogs");
|
b.HasIndex("DateCreated");
|
||||||
|
|
||||||
|
b.ToTable("ActivityLogs", "jellyfin");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Jellyfin.Data.Entities.CustomItemDisplayPreferences", b =>
|
modelBuilder.Entity("Jellyfin.Data.Entities.CustomItemDisplayPreferences", b =>
|
||||||
@@ -117,7 +121,7 @@ namespace Jellyfin.Server.Implementations.Migrations
|
|||||||
b.HasIndex("UserId", "ItemId", "Client", "Key")
|
b.HasIndex("UserId", "ItemId", "Client", "Key")
|
||||||
.IsUnique();
|
.IsUnique();
|
||||||
|
|
||||||
b.ToTable("CustomItemDisplayPreferences");
|
b.ToTable("CustomItemDisplayPreferences", "jellyfin");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
|
modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
|
||||||
@@ -174,7 +178,7 @@ namespace Jellyfin.Server.Implementations.Migrations
|
|||||||
b.HasIndex("UserId", "ItemId", "Client")
|
b.HasIndex("UserId", "ItemId", "Client")
|
||||||
.IsUnique();
|
.IsUnique();
|
||||||
|
|
||||||
b.ToTable("DisplayPreferences");
|
b.ToTable("DisplayPreferences", "jellyfin");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
|
modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
|
||||||
@@ -196,7 +200,7 @@ namespace Jellyfin.Server.Implementations.Migrations
|
|||||||
|
|
||||||
b.HasIndex("DisplayPreferencesId");
|
b.HasIndex("DisplayPreferencesId");
|
||||||
|
|
||||||
b.ToTable("HomeSection");
|
b.ToTable("HomeSection", "jellyfin");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
|
modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
|
||||||
@@ -221,7 +225,7 @@ namespace Jellyfin.Server.Implementations.Migrations
|
|||||||
b.HasIndex("UserId")
|
b.HasIndex("UserId")
|
||||||
.IsUnique();
|
.IsUnique();
|
||||||
|
|
||||||
b.ToTable("ImageInfos");
|
b.ToTable("ImageInfos", "jellyfin");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
|
modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
|
||||||
@@ -265,7 +269,7 @@ namespace Jellyfin.Server.Implementations.Migrations
|
|||||||
|
|
||||||
b.HasIndex("UserId");
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
b.ToTable("ItemDisplayPreferences");
|
b.ToTable("ItemDisplayPreferences", "jellyfin");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
|
modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
|
||||||
@@ -296,7 +300,7 @@ namespace Jellyfin.Server.Implementations.Migrations
|
|||||||
.IsUnique()
|
.IsUnique()
|
||||||
.HasFilter("[UserId] IS NOT NULL");
|
.HasFilter("[UserId] IS NOT NULL");
|
||||||
|
|
||||||
b.ToTable("Permissions");
|
b.ToTable("Permissions", "jellyfin");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
|
modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
|
||||||
@@ -329,7 +333,7 @@ namespace Jellyfin.Server.Implementations.Migrations
|
|||||||
.IsUnique()
|
.IsUnique()
|
||||||
.HasFilter("[UserId] IS NOT NULL");
|
.HasFilter("[UserId] IS NOT NULL");
|
||||||
|
|
||||||
b.ToTable("Preferences");
|
b.ToTable("Preferences", "jellyfin");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Jellyfin.Data.Entities.Security.ApiKey", b =>
|
modelBuilder.Entity("Jellyfin.Data.Entities.Security.ApiKey", b =>
|
||||||
@@ -358,7 +362,7 @@ namespace Jellyfin.Server.Implementations.Migrations
|
|||||||
b.HasIndex("AccessToken")
|
b.HasIndex("AccessToken")
|
||||||
.IsUnique();
|
.IsUnique();
|
||||||
|
|
||||||
b.ToTable("ApiKeys");
|
b.ToTable("ApiKeys", "jellyfin");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b =>
|
modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b =>
|
||||||
@@ -416,7 +420,7 @@ namespace Jellyfin.Server.Implementations.Migrations
|
|||||||
|
|
||||||
b.HasIndex("UserId", "DeviceId");
|
b.HasIndex("UserId", "DeviceId");
|
||||||
|
|
||||||
b.ToTable("Devices");
|
b.ToTable("Devices", "jellyfin");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Jellyfin.Data.Entities.Security.DeviceOptions", b =>
|
modelBuilder.Entity("Jellyfin.Data.Entities.Security.DeviceOptions", b =>
|
||||||
@@ -437,7 +441,7 @@ namespace Jellyfin.Server.Implementations.Migrations
|
|||||||
b.HasIndex("DeviceId")
|
b.HasIndex("DeviceId")
|
||||||
.IsUnique();
|
.IsUnique();
|
||||||
|
|
||||||
b.ToTable("DeviceOptions");
|
b.ToTable("DeviceOptions", "jellyfin");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
|
modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
|
||||||
@@ -550,7 +554,7 @@ namespace Jellyfin.Server.Implementations.Migrations
|
|||||||
b.HasIndex("Username")
|
b.HasIndex("Username")
|
||||||
.IsUnique();
|
.IsUnique();
|
||||||
|
|
||||||
b.ToTable("Users");
|
b.ToTable("Users", "jellyfin");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
|
modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
using Jellyfin.Data.Entities;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||||
|
|
||||||
|
namespace Jellyfin.Server.Implementations.ModelConfiguration;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// FluentAPI configuration for the ActivityLog entity.
|
||||||
|
/// </summary>
|
||||||
|
public class ActivityLogConfiguration : IEntityTypeConfiguration<ActivityLog>
|
||||||
|
{
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public void Configure(EntityTypeBuilder<ActivityLog> builder)
|
||||||
|
{
|
||||||
|
builder.HasIndex(entity => entity.DateCreated);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -434,11 +434,15 @@ namespace Jellyfin.Server.Extensions
|
|||||||
options.MapType<TranscodeReason>(() =>
|
options.MapType<TranscodeReason>(() =>
|
||||||
new OpenApiSchema
|
new OpenApiSchema
|
||||||
{
|
{
|
||||||
Type = "string",
|
Type = "array",
|
||||||
Enum = Enum.GetNames<TranscodeReason>()
|
Items = new OpenApiSchema
|
||||||
.Select(e => new OpenApiString(e))
|
{
|
||||||
.Cast<IOpenApiAny>()
|
Reference = new OpenApiReference
|
||||||
.ToArray()
|
{
|
||||||
|
Id = nameof(TranscodeReason),
|
||||||
|
Type = ReferenceType.Schema,
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Swashbuckle doesn't use JsonOptions to describe responses, so we need to manually describe it.
|
// Swashbuckle doesn't use JsonOptions to describe responses, so we need to manually describe it.
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Linq;
|
||||||
using Jellyfin.Extensions;
|
using Jellyfin.Extensions;
|
||||||
using Jellyfin.Server.Migrations;
|
using Jellyfin.Server.Migrations;
|
||||||
using MediaBrowser.Common.Plugins;
|
using MediaBrowser.Common.Plugins;
|
||||||
@@ -8,6 +9,7 @@ using MediaBrowser.Model.ApiClient;
|
|||||||
using MediaBrowser.Model.Entities;
|
using MediaBrowser.Model.Entities;
|
||||||
using MediaBrowser.Model.Session;
|
using MediaBrowser.Model.Session;
|
||||||
using MediaBrowser.Model.SyncPlay;
|
using MediaBrowser.Model.SyncPlay;
|
||||||
|
using Microsoft.OpenApi.Any;
|
||||||
using Microsoft.OpenApi.Models;
|
using Microsoft.OpenApi.Models;
|
||||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||||
|
|
||||||
@@ -56,6 +58,15 @@ namespace Jellyfin.Server.Filters
|
|||||||
|
|
||||||
context.SchemaGenerator.GenerateSchema(configuration.ConfigurationType, context.SchemaRepository);
|
context.SchemaGenerator.GenerateSchema(configuration.ConfigurationType, context.SchemaRepository);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
context.SchemaRepository.AddDefinition(nameof(TranscodeReason), new OpenApiSchema
|
||||||
|
{
|
||||||
|
Type = "string",
|
||||||
|
Enum = Enum.GetNames<TranscodeReason>()
|
||||||
|
.Select(e => new OpenApiString(e))
|
||||||
|
.Cast<IOpenApiAny>()
|
||||||
|
.ToArray()
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,8 +37,8 @@
|
|||||||
<PackageReference Include="CommandLineParser" Version="2.9.1" />
|
<PackageReference Include="CommandLineParser" Version="2.9.1" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="6.0.1" />
|
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="6.0.1" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="6.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="6.0.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="6.0.5" />
|
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="6.0.9" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="6.0.5" />
|
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="6.0.9" />
|
||||||
<PackageReference Include="prometheus-net" Version="6.0.0" />
|
<PackageReference Include="prometheus-net" Version="6.0.0" />
|
||||||
<PackageReference Include="prometheus-net.AspNetCore" Version="6.0.0" />
|
<PackageReference Include="prometheus-net.AspNetCore" Version="6.0.0" />
|
||||||
<PackageReference Include="Serilog.AspNetCore" Version="4.1.0" />
|
<PackageReference Include="Serilog.AspNetCore" Version="4.1.0" />
|
||||||
|
|||||||
@@ -19,41 +19,44 @@ namespace Jellyfin.Server.Middleware
|
|||||||
private readonly RequestDelegate _next;
|
private readonly RequestDelegate _next;
|
||||||
private readonly ILogger<ResponseTimeMiddleware> _logger;
|
private readonly ILogger<ResponseTimeMiddleware> _logger;
|
||||||
|
|
||||||
private readonly bool _enableWarning;
|
|
||||||
private readonly long _warningThreshold;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="ResponseTimeMiddleware"/> class.
|
/// Initializes a new instance of the <see cref="ResponseTimeMiddleware"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="next">Next request delegate.</param>
|
/// <param name="next">Next request delegate.</param>
|
||||||
/// <param name="logger">Instance of the <see cref="ILogger{ExceptionMiddleware}"/> interface.</param>
|
/// <param name="logger">Instance of the <see cref="ILogger{ExceptionMiddleware}"/> interface.</param>
|
||||||
/// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
|
|
||||||
public ResponseTimeMiddleware(
|
public ResponseTimeMiddleware(
|
||||||
RequestDelegate next,
|
RequestDelegate next,
|
||||||
ILogger<ResponseTimeMiddleware> logger,
|
ILogger<ResponseTimeMiddleware> logger)
|
||||||
IServerConfigurationManager serverConfigurationManager)
|
|
||||||
{
|
{
|
||||||
_next = next;
|
_next = next;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
|
||||||
_enableWarning = serverConfigurationManager.Configuration.EnableSlowResponseWarning;
|
|
||||||
_warningThreshold = serverConfigurationManager.Configuration.SlowResponseThresholdMs;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Invoke request.
|
/// Invoke request.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="context">Request context.</param>
|
/// <param name="context">Request context.</param>
|
||||||
|
/// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
|
||||||
/// <returns>Task.</returns>
|
/// <returns>Task.</returns>
|
||||||
public async Task Invoke(HttpContext context)
|
public async Task Invoke(HttpContext context, IServerConfigurationManager serverConfigurationManager)
|
||||||
{
|
{
|
||||||
var watch = new Stopwatch();
|
var watch = new Stopwatch();
|
||||||
watch.Start();
|
watch.Start();
|
||||||
|
var enableWarning = serverConfigurationManager.Configuration.EnableSlowResponseWarning;
|
||||||
|
var warningThreshold = serverConfigurationManager.Configuration.SlowResponseThresholdMs;
|
||||||
context.Response.OnStarting(() =>
|
context.Response.OnStarting(() =>
|
||||||
{
|
{
|
||||||
watch.Stop();
|
watch.Stop();
|
||||||
LogWarning(context, watch);
|
if (enableWarning && watch.ElapsedMilliseconds > warningThreshold)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(
|
||||||
|
"Slow HTTP Response from {Url} to {RemoteIp} in {Elapsed:g} with Status Code {StatusCode}",
|
||||||
|
context.Request.GetDisplayUrl(),
|
||||||
|
context.GetNormalizedRemoteIp(),
|
||||||
|
watch.Elapsed,
|
||||||
|
context.Response.StatusCode);
|
||||||
|
}
|
||||||
|
|
||||||
var responseTimeForCompleteRequest = watch.ElapsedMilliseconds;
|
var responseTimeForCompleteRequest = watch.ElapsedMilliseconds;
|
||||||
context.Response.Headers[ResponseHeaderResponseTime] = responseTimeForCompleteRequest.ToString(CultureInfo.InvariantCulture);
|
context.Response.Headers[ResponseHeaderResponseTime] = responseTimeForCompleteRequest.ToString(CultureInfo.InvariantCulture);
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
@@ -62,18 +65,5 @@ namespace Jellyfin.Server.Middleware
|
|||||||
// Call the next delegate/middleware in the pipeline
|
// Call the next delegate/middleware in the pipeline
|
||||||
await this._next(context).ConfigureAwait(false);
|
await this._next(context).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void LogWarning(HttpContext context, Stopwatch watch)
|
|
||||||
{
|
|
||||||
if (_enableWarning && watch.ElapsedMilliseconds > _warningThreshold)
|
|
||||||
{
|
|
||||||
_logger.LogWarning(
|
|
||||||
"Slow HTTP Response from {Url} to {RemoteIp} in {Elapsed:g} with Status Code {StatusCode}",
|
|
||||||
context.Request.GetDisplayUrl(),
|
|
||||||
context.GetNormalizedRemoteIp(),
|
|
||||||
watch.Elapsed,
|
|
||||||
context.Response.StatusCode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -243,7 +243,7 @@ namespace Jellyfin.Server
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
appHost.Dispose();
|
await appHost.DisposeAsync().ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_restartOnShutdown)
|
if (_restartOnShutdown)
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Authors>Jellyfin Contributors</Authors>
|
<Authors>Jellyfin Contributors</Authors>
|
||||||
<PackageId>Jellyfin.Common</PackageId>
|
<PackageId>Jellyfin.Common</PackageId>
|
||||||
<VersionPrefix>10.8.0</VersionPrefix>
|
<VersionPrefix>10.8.7</VersionPrefix>
|
||||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|||||||
@@ -50,6 +50,14 @@ namespace MediaBrowser.Controller.Drawing
|
|||||||
/// <returns>BlurHash.</returns>
|
/// <returns>BlurHash.</returns>
|
||||||
string GetImageBlurHash(string path);
|
string GetImageBlurHash(string path);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the blurhash of the image.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="path">Path to the image file.</param>
|
||||||
|
/// <param name="imageDimensions">The image dimensions.</param>
|
||||||
|
/// <returns>BlurHash.</returns>
|
||||||
|
string GetImageBlurHash(string path, ImageDimensions imageDimensions);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the image cache tag.
|
/// Gets the image cache tag.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -169,8 +169,8 @@ namespace MediaBrowser.Controller.Entities.Audio
|
|||||||
|
|
||||||
var childUpdateType = ItemUpdateType.None;
|
var childUpdateType = ItemUpdateType.None;
|
||||||
|
|
||||||
// Refresh songs
|
// Refresh songs only and not m3u files in album folder
|
||||||
foreach (var item in items)
|
foreach (var item in items.OfType<Audio>())
|
||||||
{
|
{
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
|||||||
@@ -1863,7 +1863,7 @@ namespace MediaBrowser.Controller.Entities
|
|||||||
data.PlaybackPositionTicks = 0;
|
data.PlaybackPositionTicks = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
data.LastPlayedDate = datePlayed ?? data.LastPlayedDate ?? DateTime.UtcNow;
|
data.LastPlayedDate = datePlayed ?? DateTime.UtcNow;
|
||||||
data.Played = true;
|
data.Played = true;
|
||||||
|
|
||||||
UserDataManager.SaveUserData(user.Id, this, data, UserDataSaveReason.TogglePlayed, CancellationToken.None);
|
UserDataManager.SaveUserData(user.Id, this, data, UserDataSaveReason.TogglePlayed, CancellationToken.None);
|
||||||
|
|||||||
@@ -892,29 +892,7 @@ namespace MediaBrowser.Controller.Entities
|
|||||||
|
|
||||||
private static BaseItem[] SortItemsByRequest(InternalItemsQuery query, IReadOnlyList<BaseItem> items)
|
private static BaseItem[] SortItemsByRequest(InternalItemsQuery query, IReadOnlyList<BaseItem> items)
|
||||||
{
|
{
|
||||||
var ids = query.ItemIds;
|
return items.OrderBy(i => Array.IndexOf(query.ItemIds, i.Id)).ToArray();
|
||||||
int size = items.Count;
|
|
||||||
|
|
||||||
// ids can potentially contain non-unique guids, but query result cannot,
|
|
||||||
// so we include only first occurrence of each guid
|
|
||||||
var positions = new Dictionary<Guid, int>(size);
|
|
||||||
int index = 0;
|
|
||||||
for (int i = 0; i < ids.Length; i++)
|
|
||||||
{
|
|
||||||
if (positions.TryAdd(ids[i], index))
|
|
||||||
{
|
|
||||||
index++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var newItems = new BaseItem[size];
|
|
||||||
for (int i = 0; i < size; i++)
|
|
||||||
{
|
|
||||||
var item = items[i];
|
|
||||||
newItems[positions[item.Id]] = item;
|
|
||||||
}
|
|
||||||
|
|
||||||
return newItems;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public QueryResult<BaseItem> GetItems(InternalItemsQuery query)
|
public QueryResult<BaseItem> GetItems(InternalItemsQuery query)
|
||||||
|
|||||||
@@ -205,6 +205,16 @@ namespace MediaBrowser.Controller.Entities
|
|||||||
|
|
||||||
public int? MinIndexNumber { get; set; }
|
public int? MinIndexNumber { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the minimum ParentIndexNumber and IndexNumber.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// It produces this where clause:
|
||||||
|
/// <para>(ParentIndexNumber = X and IndexNumber >= Y) or ParentIndexNumber > X.
|
||||||
|
/// </para>
|
||||||
|
/// </remarks>
|
||||||
|
public (int ParentIndexNumber, int IndexNumber)? MinParentAndIndexNumber { get; set; }
|
||||||
|
|
||||||
public int? AiredDuringSeason { get; set; }
|
public int? AiredDuringSeason { get; set; }
|
||||||
|
|
||||||
public double? MinCriticRating { get; set; }
|
public double? MinCriticRating { get; set; }
|
||||||
|
|||||||
@@ -261,7 +261,7 @@ namespace MediaBrowser.Controller.Entities.TV
|
|||||||
DtoOptions = options
|
DtoOptions = options
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!user.DisplayMissingEpisodes)
|
if (user == null || !user.DisplayMissingEpisodes)
|
||||||
{
|
{
|
||||||
query.IsMissing = false;
|
query.IsMissing = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using MediaBrowser.Common;
|
using MediaBrowser.Common;
|
||||||
|
using MediaBrowser.Common.Net;
|
||||||
using MediaBrowser.Model.System;
|
using MediaBrowser.Model.System;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
|
|
||||||
@@ -74,9 +75,10 @@ namespace MediaBrowser.Controller
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets an URL that can be used to access the API over LAN.
|
/// Gets an URL that can be used to access the API over LAN.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="hostname">An optional hostname to use.</param>
|
||||||
/// <param name="allowHttps">A value indicating whether to allow HTTPS.</param>
|
/// <param name="allowHttps">A value indicating whether to allow HTTPS.</param>
|
||||||
/// <returns>The API URL.</returns>
|
/// <returns>The API URL.</returns>
|
||||||
string GetApiUrlForLocalAccess(bool allowHttps = true);
|
string GetApiUrlForLocalAccess(IPObject hostname = null, bool allowHttps = true);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets a local (LAN) URL that can be used to access the API.
|
/// Gets a local (LAN) URL that can be used to access the API.
|
||||||
|
|||||||
@@ -570,5 +570,13 @@ namespace MediaBrowser.Controller.Library
|
|||||||
Task RunMetadataSavers(BaseItem item, ItemUpdateType updateReason);
|
Task RunMetadataSavers(BaseItem item, ItemUpdateType updateReason);
|
||||||
|
|
||||||
BaseItem GetParentItem(Guid? parentId, Guid? userId);
|
BaseItem GetParentItem(Guid? parentId, Guid? userId);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Queue a library scan.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This exists so plugins can trigger a library scan.
|
||||||
|
/// </remarks>
|
||||||
|
void QueueLibraryScan();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Authors>Jellyfin Contributors</Authors>
|
<Authors>Jellyfin Contributors</Authors>
|
||||||
<PackageId>Jellyfin.Controller</PackageId>
|
<PackageId>Jellyfin.Controller</PackageId>
|
||||||
<VersionPrefix>10.8.0</VersionPrefix>
|
<VersionPrefix>10.8.7</VersionPrefix>
|
||||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|||||||
@@ -75,6 +75,12 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
/// <value>The profile.</value>
|
/// <value>The profile.</value>
|
||||||
public string Profile { get; set; }
|
public string Profile { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the video range type.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>The video range type.</value>
|
||||||
|
public string VideoRangeType { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the level.
|
/// Gets or sets the level.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
private readonly IMediaEncoder _mediaEncoder;
|
private readonly IMediaEncoder _mediaEncoder;
|
||||||
private readonly ISubtitleEncoder _subtitleEncoder;
|
private readonly ISubtitleEncoder _subtitleEncoder;
|
||||||
private readonly IConfiguration _config;
|
private readonly IConfiguration _config;
|
||||||
|
private readonly Version _minKernelVersioni915Hang = new Version(5, 18);
|
||||||
|
|
||||||
private static readonly string[] _videoProfilesH264 = new[]
|
private static readonly string[] _videoProfilesH264 = new[]
|
||||||
{
|
{
|
||||||
@@ -125,6 +126,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
&& _mediaEncoder.SupportsFilter("scale_vaapi")
|
&& _mediaEncoder.SupportsFilter("scale_vaapi")
|
||||||
&& _mediaEncoder.SupportsFilter("deinterlace_vaapi")
|
&& _mediaEncoder.SupportsFilter("deinterlace_vaapi")
|
||||||
&& _mediaEncoder.SupportsFilter("tonemap_vaapi")
|
&& _mediaEncoder.SupportsFilter("tonemap_vaapi")
|
||||||
|
&& _mediaEncoder.SupportsFilter("procamp_vaapi")
|
||||||
&& _mediaEncoder.SupportsFilterWithOption(FilterOptionType.OverlayVaapiFrameSync)
|
&& _mediaEncoder.SupportsFilterWithOption(FilterOptionType.OverlayVaapiFrameSync)
|
||||||
&& _mediaEncoder.SupportsFilter("hwupload_vaapi");
|
&& _mediaEncoder.SupportsFilter("hwupload_vaapi");
|
||||||
}
|
}
|
||||||
@@ -156,9 +158,9 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.Equals(state.VideoStream.CodecTag, "dovi", StringComparison.OrdinalIgnoreCase)
|
if (string.Equals(state.VideoStream.Codec, "hevc", StringComparison.OrdinalIgnoreCase)
|
||||||
|| string.Equals(state.VideoStream.CodecTag, "dvh1", StringComparison.OrdinalIgnoreCase)
|
&& string.Equals(state.VideoStream.VideoRange, "HDR", StringComparison.OrdinalIgnoreCase)
|
||||||
|| string.Equals(state.VideoStream.CodecTag, "dvhe", StringComparison.OrdinalIgnoreCase))
|
&& string.Equals(state.VideoStream.VideoRangeType, "DOVI", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
// Only native SW decoder and HW accelerator can parse dovi rpu.
|
// Only native SW decoder and HW accelerator can parse dovi rpu.
|
||||||
var vidDecoder = GetHardwareVideoDecoder(state, options) ?? string.Empty;
|
var vidDecoder = GetHardwareVideoDecoder(state, options) ?? string.Empty;
|
||||||
@@ -169,22 +171,24 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
return isSwDecoder || isNvdecDecoder || isVaapiDecoder || isD3d11vaDecoder;
|
return isSwDecoder || isNvdecDecoder || isVaapiDecoder || isD3d11vaDecoder;
|
||||||
}
|
}
|
||||||
|
|
||||||
return string.Equals(state.VideoStream.ColorTransfer, "smpte2084", StringComparison.OrdinalIgnoreCase)
|
return string.Equals(state.VideoStream.VideoRange, "HDR", StringComparison.OrdinalIgnoreCase)
|
||||||
|| string.Equals(state.VideoStream.ColorTransfer, "arib-std-b67", StringComparison.OrdinalIgnoreCase);
|
&& (string.Equals(state.VideoStream.VideoRangeType, "HDR10", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| string.Equals(state.VideoStream.VideoRangeType, "HLG", StringComparison.OrdinalIgnoreCase));
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool IsVaapiVppTonemapAvailable(EncodingJobInfo state, EncodingOptions options)
|
private bool IsVaapiVppTonemapAvailable(EncodingJobInfo state, EncodingOptions options)
|
||||||
{
|
{
|
||||||
if (state.VideoStream == null)
|
if (state.VideoStream == null
|
||||||
|
|| !options.EnableVppTonemapping
|
||||||
|
|| GetVideoColorBitDepth(state) != 10)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Native VPP tonemapping may come to QSV in the future.
|
// Native VPP tonemapping may come to QSV in the future.
|
||||||
|
|
||||||
return options.EnableVppTonemapping
|
return string.Equals(state.VideoStream.VideoRange, "HDR", StringComparison.OrdinalIgnoreCase)
|
||||||
&& string.Equals(state.VideoStream.ColorTransfer, "smpte2084", StringComparison.OrdinalIgnoreCase)
|
&& string.Equals(state.VideoStream.VideoRangeType, "HDR10", StringComparison.OrdinalIgnoreCase);
|
||||||
&& GetVideoColorBitDepth(state) == 10;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -713,6 +717,9 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
}
|
}
|
||||||
else if (_mediaEncoder.IsVaapiDeviceInteli965)
|
else if (_mediaEncoder.IsVaapiDeviceInteli965)
|
||||||
{
|
{
|
||||||
|
// Only override i965 since it has lower priority than iHD in libva lookup.
|
||||||
|
Environment.SetEnvironmentVariable("LIBVA_DRIVER_NAME", "i965");
|
||||||
|
Environment.SetEnvironmentVariable("LIBVA_DRIVER_NAME_JELLYFIN", "i965");
|
||||||
args.Append(GetVaapiDeviceArgs(null, "i965", null, VaapiAlias));
|
args.Append(GetVaapiDeviceArgs(null, "i965", null, VaapiAlias));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -924,6 +931,13 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
arg.Append(" -i \"").Append(state.AudioStream.Path).Append('"');
|
arg.Append(" -i \"").Append(state.AudioStream.Path).Append('"');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Disable auto inserted SW scaler for HW decoders in case of changed resolution.
|
||||||
|
var isSwDecoder = string.IsNullOrEmpty(GetHardwareVideoDecoder(state, options));
|
||||||
|
if (!isSwDecoder)
|
||||||
|
{
|
||||||
|
arg.Append(" -autoscale 0");
|
||||||
|
}
|
||||||
|
|
||||||
return arg.ToString();
|
return arg.ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1138,16 +1152,15 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
|
|
||||||
if (state.SubtitleStream.IsExternal)
|
if (state.SubtitleStream.IsExternal)
|
||||||
{
|
{
|
||||||
var subtitlePath = state.SubtitleStream.Path;
|
|
||||||
var charsetParam = string.Empty;
|
var charsetParam = string.Empty;
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(state.SubtitleStream.Language))
|
if (!string.IsNullOrEmpty(state.SubtitleStream.Language))
|
||||||
{
|
{
|
||||||
var charenc = _subtitleEncoder.GetSubtitleFileCharacterSet(
|
var charenc = _subtitleEncoder.GetSubtitleFileCharacterSet(
|
||||||
subtitlePath,
|
state.SubtitleStream,
|
||||||
state.SubtitleStream.Language,
|
state.SubtitleStream.Language,
|
||||||
state.MediaSource.Protocol,
|
state.MediaSource,
|
||||||
CancellationToken.None).GetAwaiter().GetResult();
|
CancellationToken.None).GetAwaiter().GetResult();
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(charenc))
|
if (!string.IsNullOrEmpty(charenc))
|
||||||
{
|
{
|
||||||
@@ -1159,7 +1172,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
return string.Format(
|
return string.Format(
|
||||||
CultureInfo.InvariantCulture,
|
CultureInfo.InvariantCulture,
|
||||||
"subtitles=f='{0}'{1}{2}{3}{4}{5}",
|
"subtitles=f='{0}'{1}{2}{3}{4}{5}",
|
||||||
_mediaEncoder.EscapeSubtitleFilterPath(subtitlePath),
|
_mediaEncoder.EscapeSubtitleFilterPath(state.SubtitleStream.Path),
|
||||||
charsetParam,
|
charsetParam,
|
||||||
alphaParam,
|
alphaParam,
|
||||||
sub2videoParam,
|
sub2videoParam,
|
||||||
@@ -1296,6 +1309,10 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
// which will reduce overhead in performance intensive tasks such as 4k transcoding and tonemapping.
|
// which will reduce overhead in performance intensive tasks such as 4k transcoding and tonemapping.
|
||||||
var intelLowPowerHwEncoding = false;
|
var intelLowPowerHwEncoding = false;
|
||||||
|
|
||||||
|
// Workaround for linux 5.18+ i915 hang at cost of performance.
|
||||||
|
// https://github.com/intel/media-driver/issues/1456
|
||||||
|
var enableWaFori915Hang = false;
|
||||||
|
|
||||||
if (string.Equals(encodingOptions.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase))
|
if (string.Equals(encodingOptions.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
var isIntelVaapiDriver = _mediaEncoder.IsVaapiDeviceInteliHD || _mediaEncoder.IsVaapiDeviceInteli965;
|
var isIntelVaapiDriver = _mediaEncoder.IsVaapiDeviceInteliHD || _mediaEncoder.IsVaapiDeviceInteli965;
|
||||||
@@ -1311,6 +1328,20 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
}
|
}
|
||||||
else if (string.Equals(encodingOptions.HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase))
|
else if (string.Equals(encodingOptions.HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
|
if (OperatingSystem.IsLinux() && Environment.OSVersion.Version >= _minKernelVersioni915Hang)
|
||||||
|
{
|
||||||
|
var vidDecoder = GetHardwareVideoDecoder(state, encodingOptions) ?? string.Empty;
|
||||||
|
var isIntelDecoder = vidDecoder.Contains("qsv", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| vidDecoder.Contains("vaapi", StringComparison.OrdinalIgnoreCase);
|
||||||
|
var doOclTonemap = _mediaEncoder.SupportsHwaccel("qsv")
|
||||||
|
&& IsVaapiSupported(state)
|
||||||
|
&& IsOpenclFullSupported()
|
||||||
|
&& !IsVaapiVppTonemapAvailable(state, encodingOptions)
|
||||||
|
&& IsHwTonemapAvailable(state, encodingOptions);
|
||||||
|
|
||||||
|
enableWaFori915Hang = isIntelDecoder && doOclTonemap;
|
||||||
|
}
|
||||||
|
|
||||||
if (string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase))
|
if (string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
intelLowPowerHwEncoding = encodingOptions.EnableIntelLowPowerH264HwEncoder;
|
intelLowPowerHwEncoding = encodingOptions.EnableIntelLowPowerH264HwEncoder;
|
||||||
@@ -1319,6 +1350,10 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
{
|
{
|
||||||
intelLowPowerHwEncoding = encodingOptions.EnableIntelLowPowerHevcHwEncoder;
|
intelLowPowerHwEncoding = encodingOptions.EnableIntelLowPowerHevcHwEncoder;
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
enableWaFori915Hang = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (intelLowPowerHwEncoding)
|
if (intelLowPowerHwEncoding)
|
||||||
@@ -1326,6 +1361,11 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
param += " -low_power 1";
|
param += " -low_power 1";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (enableWaFori915Hang)
|
||||||
|
{
|
||||||
|
param += " -async_depth 1";
|
||||||
|
}
|
||||||
|
|
||||||
var isVc1 = string.Equals(state.VideoStream?.Codec, "vc1", StringComparison.OrdinalIgnoreCase);
|
var isVc1 = string.Equals(state.VideoStream?.Codec, "vc1", StringComparison.OrdinalIgnoreCase);
|
||||||
var isLibX265 = string.Equals(videoEncoder, "libx265", StringComparison.OrdinalIgnoreCase);
|
var isLibX265 = string.Equals(videoEncoder, "libx265", StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
@@ -1375,7 +1415,11 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
param += " -preset 7";
|
param += " -preset 7";
|
||||||
}
|
}
|
||||||
|
|
||||||
param += " -look_ahead 0";
|
// Only h264_qsv has look_ahead option
|
||||||
|
if (string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
param += " -look_ahead 0";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else if (string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase) // h264 (h264_nvenc)
|
else if (string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase) // h264 (h264_nvenc)
|
||||||
|| string.Equals(videoEncoder, "hevc_nvenc", StringComparison.OrdinalIgnoreCase)) // hevc (hevc_nvenc)
|
|| string.Equals(videoEncoder, "hevc_nvenc", StringComparison.OrdinalIgnoreCase)) // hevc (hevc_nvenc)
|
||||||
@@ -1413,7 +1457,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
param += " -preset p4";
|
param += " -preset p1";
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1707,6 +1751,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
|
|
||||||
// Can't stream copy if we're burning in subtitles
|
// Can't stream copy if we're burning in subtitles
|
||||||
if (request.SubtitleStreamIndex.HasValue
|
if (request.SubtitleStreamIndex.HasValue
|
||||||
|
&& request.SubtitleStreamIndex.Value >= 0
|
||||||
&& state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode)
|
&& state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
@@ -1753,6 +1798,20 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var requestedRangeTypes = state.GetRequestedRangeTypes(videoStream.Codec);
|
||||||
|
if (requestedRangeTypes.Length > 0)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(videoStream.VideoRangeType))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!requestedRangeTypes.Contains(videoStream.VideoRangeType, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Video width must fall within requested value
|
// Video width must fall within requested value
|
||||||
if (request.MaxWidth.HasValue
|
if (request.MaxWidth.HasValue
|
||||||
&& (!videoStream.Width.HasValue || videoStream.Width.Value > request.MaxWidth.Value))
|
&& (!videoStream.Width.HasValue || videoStream.Width.Value > request.MaxWidth.Value))
|
||||||
@@ -1893,7 +1952,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
return request.EnableAutoStreamCopy;
|
return request.EnableAutoStreamCopy;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int? GetVideoBitrateParamValue(BaseEncodingJobOptions request, MediaStream videoStream, string outputVideoCodec)
|
public int GetVideoBitrateParamValue(BaseEncodingJobOptions request, MediaStream videoStream, string outputVideoCodec)
|
||||||
{
|
{
|
||||||
var bitrate = request.VideoBitRate;
|
var bitrate = request.VideoBitRate;
|
||||||
|
|
||||||
@@ -1925,7 +1984,8 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return bitrate;
|
// Cap the max target bitrate to intMax/2 to satisify the bufsize=bitrate*2.
|
||||||
|
return Math.Min(bitrate ?? 0, int.MaxValue / 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
private int GetMinBitrate(int sourceBitrate, int requestedBitrate)
|
private int GetMinBitrate(int sourceBitrate, int requestedBitrate)
|
||||||
@@ -2005,6 +2065,8 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
{
|
{
|
||||||
if (string.Equals(audioCodec, "aac", StringComparison.OrdinalIgnoreCase)
|
if (string.Equals(audioCodec, "aac", StringComparison.OrdinalIgnoreCase)
|
||||||
|| string.Equals(audioCodec, "mp3", StringComparison.OrdinalIgnoreCase)
|
|| string.Equals(audioCodec, "mp3", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| string.Equals(audioCodec, "opus", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| string.Equals(audioCodec, "vorbis", StringComparison.OrdinalIgnoreCase)
|
||||||
|| string.Equals(audioCodec, "ac3", StringComparison.OrdinalIgnoreCase)
|
|| string.Equals(audioCodec, "ac3", StringComparison.OrdinalIgnoreCase)
|
||||||
|| string.Equals(audioCodec, "eac3", StringComparison.OrdinalIgnoreCase))
|
|| string.Equals(audioCodec, "eac3", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
@@ -2272,7 +2334,10 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
int audioStreamIndex = FindIndex(state.MediaSource.MediaStreams, state.AudioStream);
|
int audioStreamIndex = FindIndex(state.MediaSource.MediaStreams, state.AudioStream);
|
||||||
if (state.AudioStream.IsExternal)
|
if (state.AudioStream.IsExternal)
|
||||||
{
|
{
|
||||||
bool hasExternalGraphicsSubs = state.SubtitleStream != null && state.SubtitleStream.IsExternal && !state.SubtitleStream.IsTextSubtitleStream;
|
bool hasExternalGraphicsSubs = state.SubtitleStream != null
|
||||||
|
&& state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode
|
||||||
|
&& state.SubtitleStream.IsExternal
|
||||||
|
&& !state.SubtitleStream.IsTextSubtitleStream;
|
||||||
int externalAudioMapIndex = hasExternalGraphicsSubs ? 2 : 1;
|
int externalAudioMapIndex = hasExternalGraphicsSubs ? 2 : 1;
|
||||||
|
|
||||||
args += string.Format(
|
args += string.Format(
|
||||||
@@ -2700,7 +2765,18 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
|
|
||||||
var args = "tonemap_{0}=format={1}:p=bt709:t=bt709:m=bt709";
|
var args = "tonemap_{0}=format={1}:p=bt709:t=bt709:m=bt709";
|
||||||
|
|
||||||
if (!hwTonemapSuffix.Contains("vaapi", StringComparison.OrdinalIgnoreCase))
|
if (hwTonemapSuffix.Contains("vaapi", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
args += ",procamp_vaapi=b={2}:c={3}:extra_hw_frames=16";
|
||||||
|
return string.Format(
|
||||||
|
CultureInfo.InvariantCulture,
|
||||||
|
args,
|
||||||
|
hwTonemapSuffix,
|
||||||
|
videoFormat ?? "nv12",
|
||||||
|
options.VppTonemappingBrightness,
|
||||||
|
options.VppTonemappingContrast);
|
||||||
|
}
|
||||||
|
else
|
||||||
{
|
{
|
||||||
args += ":tonemap={2}:peak={3}:desat={4}";
|
args += ":tonemap={2}:peak={3}:desat={4}";
|
||||||
|
|
||||||
@@ -2902,7 +2978,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
// sw => hw
|
// sw => hw
|
||||||
if (doCuTonemap)
|
if (doCuTonemap)
|
||||||
{
|
{
|
||||||
mainFilters.Add("hwupload");
|
mainFilters.Add("hwupload=derive_device=cuda");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2982,7 +3058,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
subFilters.Add(subTextSubtitlesFilter);
|
subFilters.Add(subTextSubtitlesFilter);
|
||||||
}
|
}
|
||||||
|
|
||||||
subFilters.Add("hwupload");
|
subFilters.Add("hwupload=derive_device=cuda");
|
||||||
overlayFilters.Add("overlay_cuda=eof_action=endall:shortest=1:repeatlast=0");
|
overlayFilters.Add("overlay_cuda=eof_action=endall:shortest=1:repeatlast=0");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3094,7 +3170,9 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
// sw => hw
|
// sw => hw
|
||||||
if (doOclTonemap)
|
if (doOclTonemap)
|
||||||
{
|
{
|
||||||
mainFilters.Add("hwupload");
|
mainFilters.Add("hwupload=derive_device=d3d11va:extra_hw_frames=16");
|
||||||
|
mainFilters.Add("format=d3d11");
|
||||||
|
mainFilters.Add("hwmap=derive_device=opencl");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3121,7 +3199,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
|
|
||||||
var memoryOutput = false;
|
var memoryOutput = false;
|
||||||
var isUploadForOclTonemap = isSwDecoder && doOclTonemap;
|
var isUploadForOclTonemap = isSwDecoder && doOclTonemap;
|
||||||
if ((isD3d11vaDecoder && isSwEncoder) || isUploadForOclTonemap)
|
if (isD3d11vaDecoder && isSwEncoder)
|
||||||
{
|
{
|
||||||
memoryOutput = true;
|
memoryOutput = true;
|
||||||
|
|
||||||
@@ -3133,7 +3211,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
}
|
}
|
||||||
|
|
||||||
// OUTPUT yuv420p surface
|
// OUTPUT yuv420p surface
|
||||||
if (isSwDecoder && isAmfEncoder)
|
if (isSwDecoder && isAmfEncoder && !isUploadForOclTonemap)
|
||||||
{
|
{
|
||||||
memoryOutput = true;
|
memoryOutput = true;
|
||||||
}
|
}
|
||||||
@@ -3148,7 +3226,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isDxInDxOut && !hasSubs)
|
if ((isDxInDxOut || isUploadForOclTonemap) && !hasSubs)
|
||||||
{
|
{
|
||||||
// OUTPUT d3d11(nv12) surface(vram)
|
// OUTPUT d3d11(nv12) surface(vram)
|
||||||
// reverse-mapping via d3d11-opencl interop.
|
// reverse-mapping via d3d11-opencl interop.
|
||||||
@@ -3159,7 +3237,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
/* Make sub and overlay filters for subtitle stream */
|
/* Make sub and overlay filters for subtitle stream */
|
||||||
var subFilters = new List<string>();
|
var subFilters = new List<string>();
|
||||||
var overlayFilters = new List<string>();
|
var overlayFilters = new List<string>();
|
||||||
if (isDxInDxOut)
|
if (isDxInDxOut || isUploadForOclTonemap)
|
||||||
{
|
{
|
||||||
if (hasSubs)
|
if (hasSubs)
|
||||||
{
|
{
|
||||||
@@ -3180,7 +3258,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
subFilters.Add(subTextSubtitlesFilter);
|
subFilters.Add(subTextSubtitlesFilter);
|
||||||
}
|
}
|
||||||
|
|
||||||
subFilters.Add("hwupload");
|
subFilters.Add("hwupload=derive_device=opencl");
|
||||||
overlayFilters.Add("overlay_opencl=eof_action=endall:shortest=1:repeatlast=0");
|
overlayFilters.Add("overlay_opencl=eof_action=endall:shortest=1:repeatlast=0");
|
||||||
overlayFilters.Add("hwmap=derive_device=d3d11va:reverse=1");
|
overlayFilters.Add("hwmap=derive_device=d3d11va:reverse=1");
|
||||||
overlayFilters.Add("format=d3d11");
|
overlayFilters.Add("format=d3d11");
|
||||||
@@ -3314,7 +3392,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
// sw => hw
|
// sw => hw
|
||||||
if (doOclTonemap)
|
if (doOclTonemap)
|
||||||
{
|
{
|
||||||
mainFilters.Add("hwupload");
|
mainFilters.Add("hwupload=derive_device=opencl");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (isD3d11vaDecoder || isQsvDecoder)
|
else if (isD3d11vaDecoder || isQsvDecoder)
|
||||||
@@ -3330,6 +3408,12 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
// map from d3d11va to qsv.
|
// map from d3d11va to qsv.
|
||||||
mainFilters.Add("hwmap=derive_device=qsv");
|
mainFilters.Add("hwmap=derive_device=qsv");
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Insert a qsv scaler to sync the decoder surface,
|
||||||
|
// msdk will passthrough this internally.
|
||||||
|
mainFilters.Add("hwmap=derive_device=qsv,scale_qsv");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// hw deint
|
// hw deint
|
||||||
@@ -3421,7 +3505,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
|
|
||||||
// qsv requires a fixed pool size.
|
// qsv requires a fixed pool size.
|
||||||
// default to 64 otherwise it will fail on certain iGPU.
|
// default to 64 otherwise it will fail on certain iGPU.
|
||||||
subFilters.Add("hwupload=extra_hw_frames=64");
|
subFilters.Add("hwupload=derive_device=qsv:extra_hw_frames=64");
|
||||||
|
|
||||||
var (overlayW, overlayH) = GetFixedOutputSize(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
|
var (overlayW, overlayH) = GetFixedOutputSize(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
|
||||||
var overlaySize = (overlayW.HasValue && overlayH.HasValue)
|
var overlaySize = (overlayW.HasValue && overlayH.HasValue)
|
||||||
@@ -3511,7 +3595,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
// sw => hw
|
// sw => hw
|
||||||
if (doOclTonemap)
|
if (doOclTonemap)
|
||||||
{
|
{
|
||||||
mainFilters.Add("hwupload");
|
mainFilters.Add("hwupload=derive_device=opencl");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (isVaapiDecoder || isQsvDecoder)
|
else if (isVaapiDecoder || isQsvDecoder)
|
||||||
@@ -3632,7 +3716,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
|
|
||||||
// qsv requires a fixed pool size.
|
// qsv requires a fixed pool size.
|
||||||
// default to 64 otherwise it will fail on certain iGPU.
|
// default to 64 otherwise it will fail on certain iGPU.
|
||||||
subFilters.Add("hwupload=extra_hw_frames=64");
|
subFilters.Add("hwupload=derive_device=qsv:extra_hw_frames=64");
|
||||||
|
|
||||||
var (overlayW, overlayH) = GetFixedOutputSize(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
|
var (overlayW, overlayH) = GetFixedOutputSize(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
|
||||||
var overlaySize = (overlayW.HasValue && overlayH.HasValue)
|
var overlaySize = (overlayW.HasValue && overlayH.HasValue)
|
||||||
@@ -3695,7 +3779,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
var newfilters = new List<string>();
|
var newfilters = new List<string>();
|
||||||
var noOverlay = swFilterChain.OverlayFilters.Count == 0;
|
var noOverlay = swFilterChain.OverlayFilters.Count == 0;
|
||||||
newfilters.AddRange(noOverlay ? swFilterChain.MainFilters : swFilterChain.OverlayFilters);
|
newfilters.AddRange(noOverlay ? swFilterChain.MainFilters : swFilterChain.OverlayFilters);
|
||||||
newfilters.Add("hwupload");
|
newfilters.Add("hwupload=derive_device=vaapi");
|
||||||
|
|
||||||
var mainFilters = noOverlay ? newfilters : swFilterChain.MainFilters;
|
var mainFilters = noOverlay ? newfilters : swFilterChain.MainFilters;
|
||||||
var overlayFilters = noOverlay ? swFilterChain.OverlayFilters : newfilters;
|
var overlayFilters = noOverlay ? swFilterChain.OverlayFilters : newfilters;
|
||||||
@@ -3776,7 +3860,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
// sw => hw
|
// sw => hw
|
||||||
if (doOclTonemap)
|
if (doOclTonemap)
|
||||||
{
|
{
|
||||||
mainFilters.Add("hwupload");
|
mainFilters.Add("hwupload=derive_device=opencl");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (isVaapiDecoder)
|
else if (isVaapiDecoder)
|
||||||
@@ -3881,7 +3965,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
subFilters.Add(subTextSubtitlesFilter);
|
subFilters.Add(subTextSubtitlesFilter);
|
||||||
}
|
}
|
||||||
|
|
||||||
subFilters.Add("hwupload");
|
subFilters.Add("hwupload=derive_device=vaapi");
|
||||||
|
|
||||||
var (overlayW, overlayH) = GetFixedOutputSize(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
|
var (overlayW, overlayH) = GetFixedOutputSize(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
|
||||||
var overlaySize = (overlayW.HasValue && overlayH.HasValue)
|
var overlaySize = (overlayW.HasValue && overlayH.HasValue)
|
||||||
@@ -3972,7 +4056,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
// sw => hw
|
// sw => hw
|
||||||
if (doOclTonemap)
|
if (doOclTonemap)
|
||||||
{
|
{
|
||||||
mainFilters.Add("hwupload");
|
mainFilters.Add("hwupload=derive_device=opencl");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (isVaapiDecoder)
|
else if (isVaapiDecoder)
|
||||||
@@ -4002,7 +4086,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
{
|
{
|
||||||
mainFilters.Add("hwdownload");
|
mainFilters.Add("hwdownload");
|
||||||
mainFilters.Add("format=p010le");
|
mainFilters.Add("format=p010le");
|
||||||
mainFilters.Add("hwupload");
|
mainFilters.Add("hwupload=derive_device=opencl");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4253,6 +4337,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
return videoStream.BitDepth.Value;
|
return videoStream.BitDepth.Value;
|
||||||
}
|
}
|
||||||
else if (string.Equals(videoStream.PixelFormat, "yuv420p", StringComparison.OrdinalIgnoreCase)
|
else if (string.Equals(videoStream.PixelFormat, "yuv420p", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| string.Equals(videoStream.PixelFormat, "yuvj420p", StringComparison.OrdinalIgnoreCase)
|
||||||
|| string.Equals(videoStream.PixelFormat, "yuv444p", StringComparison.OrdinalIgnoreCase))
|
|| string.Equals(videoStream.PixelFormat, "yuv444p", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
return 8;
|
return 8;
|
||||||
@@ -4285,14 +4370,18 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
protected string GetHardwareVideoDecoder(EncodingJobInfo state, EncodingOptions options)
|
protected string GetHardwareVideoDecoder(EncodingJobInfo state, EncodingOptions options)
|
||||||
{
|
{
|
||||||
var videoStream = state.VideoStream;
|
var videoStream = state.VideoStream;
|
||||||
if (videoStream == null)
|
var mediaSource = state.MediaSource;
|
||||||
|
if (videoStream == null || mediaSource == null)
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only use alternative encoders for video files.
|
// HWA decoders can handle both video files and video folders.
|
||||||
var videoType = state.MediaSource.VideoType ?? VideoType.VideoFile;
|
var videoType = mediaSource.VideoType;
|
||||||
if (videoType != VideoType.VideoFile)
|
if (videoType != VideoType.VideoFile
|
||||||
|
&& videoType != VideoType.Iso
|
||||||
|
&& videoType != VideoType.Dvd
|
||||||
|
&& videoType != VideoType.BluRay)
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -4461,7 +4550,9 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
|
|
||||||
if (isD3d11Supported && isCodecAvailable)
|
if (isD3d11Supported && isCodecAvailable)
|
||||||
{
|
{
|
||||||
return " -hwaccel d3d11va" + (outputHwSurface ? " -hwaccel_output_format d3d11" : string.Empty) + (isAv1 ? " -c:v av1" : string.Empty);
|
// set -threads 3 to intel d3d11va decoder explicitly. Lower threads may result in dead lock.
|
||||||
|
// on newer devices such as Xe, the larger the init_pool_size, the longer the initialization time for opencl to derive from d3d11.
|
||||||
|
return " -hwaccel d3d11va" + (outputHwSurface ? " -hwaccel_output_format d3d11" : string.Empty) + " -threads 3" + (isAv1 ? " -c:v av1" : string.Empty);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -4539,7 +4630,8 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
var hwSurface = (isIntelDx11OclSupported || isIntelVaapiOclSupported)
|
var hwSurface = (isIntelDx11OclSupported || isIntelVaapiOclSupported)
|
||||||
&& _mediaEncoder.SupportsFilter("alphasrc");
|
&& _mediaEncoder.SupportsFilter("alphasrc");
|
||||||
|
|
||||||
var is8bitSwFormatsQsv = string.Equals("yuv420p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase);
|
var is8bitSwFormatsQsv = string.Equals("yuv420p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| string.Equals("yuvj420p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase);
|
||||||
var is8_10bitSwFormatsQsv = is8bitSwFormatsQsv || string.Equals("yuv420p10le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase);
|
var is8_10bitSwFormatsQsv = is8bitSwFormatsQsv || string.Equals("yuv420p10le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase);
|
||||||
// TODO: add more 8/10bit and 4:4:4 formats for Qsv after finishing the ffcheck tool
|
// TODO: add more 8/10bit and 4:4:4 formats for Qsv after finishing the ffcheck tool
|
||||||
|
|
||||||
@@ -4598,7 +4690,8 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
}
|
}
|
||||||
|
|
||||||
var hwSurface = IsCudaFullSupported() && _mediaEncoder.SupportsFilter("alphasrc");
|
var hwSurface = IsCudaFullSupported() && _mediaEncoder.SupportsFilter("alphasrc");
|
||||||
var is8bitSwFormatsNvdec = string.Equals("yuv420p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase);
|
var is8bitSwFormatsNvdec = string.Equals("yuv420p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| string.Equals("yuvj420p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase);
|
||||||
var is8_10bitSwFormatsNvdec = is8bitSwFormatsNvdec || string.Equals("yuv420p10le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase);
|
var is8_10bitSwFormatsNvdec = is8bitSwFormatsNvdec || string.Equals("yuv420p10le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase);
|
||||||
// TODO: add more 8/10/12bit and 4:4:4 formats for Nvdec after finishing the ffcheck tool
|
// TODO: add more 8/10/12bit and 4:4:4 formats for Nvdec after finishing the ffcheck tool
|
||||||
|
|
||||||
@@ -4664,7 +4757,8 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
var hwSurface = _mediaEncoder.SupportsHwaccel("d3d11va")
|
var hwSurface = _mediaEncoder.SupportsHwaccel("d3d11va")
|
||||||
&& IsOpenclFullSupported()
|
&& IsOpenclFullSupported()
|
||||||
&& _mediaEncoder.SupportsFilter("alphasrc");
|
&& _mediaEncoder.SupportsFilter("alphasrc");
|
||||||
var is8bitSwFormatsAmf = string.Equals("yuv420p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase);
|
var is8bitSwFormatsAmf = string.Equals("yuv420p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| string.Equals("yuvj420p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase);
|
||||||
var is8_10bitSwFormatsAmf = is8bitSwFormatsAmf || string.Equals("yuv420p10le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase);
|
var is8_10bitSwFormatsAmf = is8bitSwFormatsAmf || string.Equals("yuv420p10le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
if (is8bitSwFormatsAmf)
|
if (is8bitSwFormatsAmf)
|
||||||
@@ -4684,11 +4778,6 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
{
|
{
|
||||||
return GetHwaccelType(state, options, "vc1", bitDepth, hwSurface);
|
return GetHwaccelType(state, options, "vc1", bitDepth, hwSurface);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.Equals("mpeg4", videoStream.Codec, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return GetHwaccelType(state, options, "mpeg4", bitDepth, hwSurface);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (is8_10bitSwFormatsAmf)
|
if (is8_10bitSwFormatsAmf)
|
||||||
@@ -4725,7 +4814,8 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
&& IsVaapiFullSupported()
|
&& IsVaapiFullSupported()
|
||||||
&& IsOpenclFullSupported()
|
&& IsOpenclFullSupported()
|
||||||
&& _mediaEncoder.SupportsFilter("alphasrc");
|
&& _mediaEncoder.SupportsFilter("alphasrc");
|
||||||
var is8bitSwFormatsVaapi = string.Equals("yuv420p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase);
|
var is8bitSwFormatsVaapi = string.Equals("yuv420p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| string.Equals("yuvj420p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase);
|
||||||
var is8_10bitSwFormatsVaapi = is8bitSwFormatsVaapi || string.Equals("yuv420p10le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase);
|
var is8_10bitSwFormatsVaapi = is8bitSwFormatsVaapi || string.Equals("yuv420p10le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
if (is8bitSwFormatsVaapi)
|
if (is8bitSwFormatsVaapi)
|
||||||
@@ -4782,7 +4872,8 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var is8bitSwFormatsVt = string.Equals("yuv420p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase);
|
var is8bitSwFormatsVt = string.Equals("yuv420p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| string.Equals("yuvj420p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase);
|
||||||
var is8_10bitSwFormatsVt = is8bitSwFormatsVt || string.Equals("yuv420p10le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase);
|
var is8_10bitSwFormatsVt = is8bitSwFormatsVt || string.Equals("yuv420p10le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
if (is8bitSwFormatsVt)
|
if (is8bitSwFormatsVt)
|
||||||
@@ -4895,14 +4986,14 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
// The default value of -probesize is more than enough, so leave it as is.
|
// The default value of -probesize is more than enough, so leave it as is.
|
||||||
var ffmpegAnalyzeDuration = _config.GetFFmpegAnalyzeDuration() ?? string.Empty;
|
var ffmpegAnalyzeDuration = _config.GetFFmpegAnalyzeDuration() ?? string.Empty;
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(ffmpegAnalyzeDuration))
|
if (state.MediaSource.AnalyzeDurationMs > 0)
|
||||||
{
|
|
||||||
analyzeDurationArgument = "-analyzeduration " + ffmpegAnalyzeDuration;
|
|
||||||
}
|
|
||||||
else if (state.MediaSource.AnalyzeDurationMs.HasValue)
|
|
||||||
{
|
{
|
||||||
analyzeDurationArgument = "-analyzeduration " + (state.MediaSource.AnalyzeDurationMs.Value * 1000).ToString(CultureInfo.InvariantCulture);
|
analyzeDurationArgument = "-analyzeduration " + (state.MediaSource.AnalyzeDurationMs.Value * 1000).ToString(CultureInfo.InvariantCulture);
|
||||||
}
|
}
|
||||||
|
else if (!string.IsNullOrEmpty(ffmpegAnalyzeDuration))
|
||||||
|
{
|
||||||
|
analyzeDurationArgument = "-analyzeduration " + ffmpegAnalyzeDuration;
|
||||||
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(analyzeDurationArgument))
|
if (!string.IsNullOrEmpty(analyzeDurationArgument))
|
||||||
{
|
{
|
||||||
@@ -4925,7 +5016,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
|
|
||||||
if (state.InputProtocol == MediaProtocol.Rtsp)
|
if (state.InputProtocol == MediaProtocol.Rtsp)
|
||||||
{
|
{
|
||||||
inputModifier += " -rtsp_transport tcp -rtsp_transport udp -rtsp_flags prefer_tcp";
|
inputModifier += " -rtsp_transport tcp+udp -rtsp_flags prefer_tcp";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(state.InputAudioSync))
|
if (!string.IsNullOrEmpty(state.InputAudioSync))
|
||||||
@@ -5454,7 +5545,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
return index;
|
return index;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.Equals(currentMediaStream.Path, streamToFind.Path, StringComparison.Ordinal))
|
if (string.Equals(currentMediaStream.Path, streamToFind.Path, StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
index++;
|
index++;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -366,6 +366,28 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the target video range type.
|
||||||
|
/// </summary>
|
||||||
|
public string TargetVideoRangeType
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (BaseRequest.Static || EncodingHelper.IsCopyCodec(OutputVideoCodec))
|
||||||
|
{
|
||||||
|
return VideoStream?.VideoRangeType;
|
||||||
|
}
|
||||||
|
|
||||||
|
var requestedRangeType = GetRequestedRangeTypes(ActualOutputVideoCodec).FirstOrDefault();
|
||||||
|
if (!string.IsNullOrEmpty(requestedRangeType))
|
||||||
|
{
|
||||||
|
return requestedRangeType;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public string TargetVideoCodecTag
|
public string TargetVideoCodecTag
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
@@ -579,6 +601,26 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
return Array.Empty<string>();
|
return Array.Empty<string>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public string[] GetRequestedRangeTypes(string codec)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(BaseRequest.VideoRangeType))
|
||||||
|
{
|
||||||
|
return BaseRequest.VideoRangeType.Split(new[] { '|', ',' }, StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(codec))
|
||||||
|
{
|
||||||
|
var rangetype = BaseRequest.GetOption(codec, "rangetype");
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(rangetype))
|
||||||
|
{
|
||||||
|
return rangetype.Split(new[] { '|', ',' }, StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.Empty<string>();
|
||||||
|
}
|
||||||
|
|
||||||
public string GetRequestedLevel(string codec)
|
public string GetRequestedLevel(string codec)
|
||||||
{
|
{
|
||||||
if (!string.IsNullOrEmpty(BaseRequest.Level))
|
if (!string.IsNullOrEmpty(BaseRequest.Level))
|
||||||
|
|||||||
@@ -37,6 +37,12 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
/// <returns>The version of encoder.</returns>
|
/// <returns>The version of encoder.</returns>
|
||||||
Version EncoderVersion { get; }
|
Version EncoderVersion { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether p key pausing is supported.
|
||||||
|
/// </summary>
|
||||||
|
/// <value><c>true</c> if p key pausing is supported, <c>false</c> otherwise.</value>
|
||||||
|
bool IsPkeyPauseSupported { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets a value indicating whether the configured Vaapi device is from AMD(radeonsi/r600 Mesa driver).
|
/// Gets a value indicating whether the configured Vaapi device is from AMD(radeonsi/r600 Mesa driver).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ using System.IO;
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using MediaBrowser.Controller.Entities;
|
using MediaBrowser.Controller.Entities;
|
||||||
using MediaBrowser.Model.MediaInfo;
|
using MediaBrowser.Model.Dto;
|
||||||
|
using MediaBrowser.Model.Entities;
|
||||||
|
|
||||||
namespace MediaBrowser.Controller.MediaEncoding
|
namespace MediaBrowser.Controller.MediaEncoding
|
||||||
{
|
{
|
||||||
@@ -37,11 +38,11 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the subtitle language encoding parameter.
|
/// Gets the subtitle language encoding parameter.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="path">The path.</param>
|
/// <param name="subtitleStream">The subtitle stream.</param>
|
||||||
/// <param name="language">The language.</param>
|
/// <param name="language">The language.</param>
|
||||||
/// <param name="protocol">The protocol.</param>
|
/// <param name="mediaSource">The media source.</param>
|
||||||
/// <param name="cancellationToken">The cancellation token.</param>
|
/// <param name="cancellationToken">The cancellation token.</param>
|
||||||
/// <returns>System.String.</returns>
|
/// <returns>System.String.</returns>
|
||||||
Task<string> GetSubtitleFileCharacterSet(string path, string language, MediaProtocol protocol, CancellationToken cancellationToken);
|
Task<string> GetSubtitleFileCharacterSet(MediaStream subtitleStream, string language, MediaSourceInfo mediaSource, CancellationToken cancellationToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
|
|
||||||
percent = 100.0 * currentMs / totalMs;
|
percent = 100.0 * currentMs / totalMs;
|
||||||
|
|
||||||
transcodingPosition = val;
|
transcodingPosition = TimeSpan.FromMilliseconds(currentMs);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (part.StartsWith("size=", StringComparison.OrdinalIgnoreCase))
|
else if (part.StartsWith("size=", StringComparison.OrdinalIgnoreCase))
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ using Microsoft.AspNetCore.Http;
|
|||||||
|
|
||||||
namespace MediaBrowser.Controller.Net
|
namespace MediaBrowser.Controller.Net
|
||||||
{
|
{
|
||||||
public interface IWebSocketConnection
|
public interface IWebSocketConnection : IAsyncDisposable, IDisposable
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Occurs when [closed].
|
/// Occurs when [closed].
|
||||||
|
|||||||
@@ -34,8 +34,8 @@ namespace MediaBrowser.Controller.Providers
|
|||||||
|
|
||||||
public bool IsReplacingImage(ImageType type)
|
public bool IsReplacingImage(ImageType type)
|
||||||
{
|
{
|
||||||
return ImageRefreshMode == MetadataRefreshMode.FullRefresh &&
|
return ImageRefreshMode == MetadataRefreshMode.FullRefresh
|
||||||
(ReplaceAllImages || ReplaceImages.Contains(type));
|
&& (ReplaceAllImages || ReplaceImages.Contains(type));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ using System.Collections.Generic;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using MediaBrowser.Controller.Entities;
|
using MediaBrowser.Controller.Entities;
|
||||||
using MediaBrowser.Model.Dto;
|
using MediaBrowser.Model.Dto;
|
||||||
using MediaBrowser.Model.Session;
|
using MediaBrowser.Model.Session;
|
||||||
@@ -17,7 +18,7 @@ namespace MediaBrowser.Controller.Session
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Class SessionInfo.
|
/// Class SessionInfo.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class SessionInfo : IDisposable
|
public sealed class SessionInfo : IAsyncDisposable, IDisposable
|
||||||
{
|
{
|
||||||
// 1 second
|
// 1 second
|
||||||
private const long ProgressIncrement = 10000000;
|
private const long ProgressIncrement = 10000000;
|
||||||
@@ -380,10 +381,28 @@ namespace MediaBrowser.Controller.Session
|
|||||||
{
|
{
|
||||||
if (controller is IDisposable disposable)
|
if (controller is IDisposable disposable)
|
||||||
{
|
{
|
||||||
_logger.LogDebug("Disposing session controller {0}", disposable.GetType().Name);
|
_logger.LogDebug("Disposing session controller synchronously {TypeName}", disposable.GetType().Name);
|
||||||
disposable.Dispose();
|
disposable.Dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async ValueTask DisposeAsync()
|
||||||
|
{
|
||||||
|
_disposed = true;
|
||||||
|
|
||||||
|
StopAutomaticProgress();
|
||||||
|
|
||||||
|
var controllers = SessionControllers.ToList();
|
||||||
|
|
||||||
|
foreach (var controller in controllers)
|
||||||
|
{
|
||||||
|
if (controller is IAsyncDisposable disposableAsync)
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Disposing session controller asynchronously {TypeName}", disposableAsync.GetType().Name);
|
||||||
|
await disposableAsync.DisposeAsync().ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||||||
"scale_vaapi",
|
"scale_vaapi",
|
||||||
"deinterlace_vaapi",
|
"deinterlace_vaapi",
|
||||||
"tonemap_vaapi",
|
"tonemap_vaapi",
|
||||||
|
"procamp_vaapi",
|
||||||
"overlay_vaapi",
|
"overlay_vaapi",
|
||||||
"hwupload_vaapi"
|
"hwupload_vaapi"
|
||||||
};
|
};
|
||||||
@@ -152,7 +153,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||||||
string output;
|
string output;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
output = GetProcessOutput(_encoderPath, "-version", false);
|
output = GetProcessOutput(_encoderPath, "-version", false, null);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -233,7 +234,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||||||
string output;
|
string output;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
output = GetProcessOutput(_encoderPath, "-version", false);
|
output = GetProcessOutput(_encoderPath, "-version", false, null);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -340,7 +341,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var output = GetProcessOutput(_encoderPath, "-v verbose -hide_banner -init_hw_device vaapi=va:" + renderNodePath, true);
|
var output = GetProcessOutput(_encoderPath, "-v verbose -hide_banner -init_hw_device vaapi=va:" + renderNodePath, true, null);
|
||||||
return output.Contains(driverName, StringComparison.Ordinal);
|
return output.Contains(driverName, StringComparison.Ordinal);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -355,7 +356,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||||||
string? output = null;
|
string? output = null;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
output = GetProcessOutput(_encoderPath, "-hwaccels", false);
|
output = GetProcessOutput(_encoderPath, "-hwaccels", false, null);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -383,7 +384,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||||||
string output;
|
string output;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
output = GetProcessOutput(_encoderPath, "-h filter=" + filter, false);
|
output = GetProcessOutput(_encoderPath, "-h filter=" + filter, false, null);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -401,13 +402,34 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool CheckSupportedRuntimeKey(string keyDesc)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(keyDesc))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
string output;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
output = GetProcessOutput(_encoderPath, "-hide_banner -f lavfi -i nullsrc=s=1x1:d=500 -f null -", true, "?");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error checking supported runtime key");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return output.Contains(keyDesc, StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
private IEnumerable<string> GetCodecs(Codec codec)
|
private IEnumerable<string> GetCodecs(Codec codec)
|
||||||
{
|
{
|
||||||
string codecstr = codec == Codec.Encoder ? "encoders" : "decoders";
|
string codecstr = codec == Codec.Encoder ? "encoders" : "decoders";
|
||||||
string output;
|
string output;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
output = GetProcessOutput(_encoderPath, "-" + codecstr, false);
|
output = GetProcessOutput(_encoderPath, "-" + codecstr, false, null);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -438,7 +460,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||||||
string output;
|
string output;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
output = GetProcessOutput(_encoderPath, "-filters", false);
|
output = GetProcessOutput(_encoderPath, "-filters", false, null);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -476,7 +498,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||||||
return dict;
|
return dict;
|
||||||
}
|
}
|
||||||
|
|
||||||
private string GetProcessOutput(string path, string arguments, bool readStdErr)
|
private string GetProcessOutput(string path, string arguments, bool readStdErr, string? testKey)
|
||||||
{
|
{
|
||||||
using (var process = new Process()
|
using (var process = new Process()
|
||||||
{
|
{
|
||||||
@@ -486,6 +508,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||||||
UseShellExecute = false,
|
UseShellExecute = false,
|
||||||
WindowStyle = ProcessWindowStyle.Hidden,
|
WindowStyle = ProcessWindowStyle.Hidden,
|
||||||
ErrorDialog = false,
|
ErrorDialog = false,
|
||||||
|
RedirectStandardInput = !string.IsNullOrEmpty(testKey),
|
||||||
RedirectStandardOutput = true,
|
RedirectStandardOutput = true,
|
||||||
RedirectStandardError = true
|
RedirectStandardError = true
|
||||||
}
|
}
|
||||||
@@ -495,6 +518,11 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||||||
|
|
||||||
process.Start();
|
process.Start();
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(testKey))
|
||||||
|
{
|
||||||
|
process.StandardInput.Write(testKey);
|
||||||
|
}
|
||||||
|
|
||||||
return readStdErr ? process.StandardError.ReadToEnd() : process.StandardOutput.ReadToEnd();
|
return readStdErr ? process.StandardError.ReadToEnd() : process.StandardOutput.ReadToEnd();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,6 +67,8 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||||||
private List<string> _filters = new List<string>();
|
private List<string> _filters = new List<string>();
|
||||||
private IDictionary<int, bool> _filtersWithOption = new Dictionary<int, bool>();
|
private IDictionary<int, bool> _filtersWithOption = new Dictionary<int, bool>();
|
||||||
|
|
||||||
|
private bool _isPkeyPauseSupported = false;
|
||||||
|
|
||||||
private bool _isVaapiDeviceAmd = false;
|
private bool _isVaapiDeviceAmd = false;
|
||||||
private bool _isVaapiDeviceInteliHD = false;
|
private bool _isVaapiDeviceInteliHD = false;
|
||||||
private bool _isVaapiDeviceInteli965 = false;
|
private bool _isVaapiDeviceInteli965 = false;
|
||||||
@@ -100,6 +102,8 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||||||
|
|
||||||
public Version EncoderVersion => _ffmpegVersion;
|
public Version EncoderVersion => _ffmpegVersion;
|
||||||
|
|
||||||
|
public bool IsPkeyPauseSupported => _isPkeyPauseSupported;
|
||||||
|
|
||||||
public bool IsVaapiDeviceAmd => _isVaapiDeviceAmd;
|
public bool IsVaapiDeviceAmd => _isVaapiDeviceAmd;
|
||||||
|
|
||||||
public bool IsVaapiDeviceInteliHD => _isVaapiDeviceInteliHD;
|
public bool IsVaapiDeviceInteliHD => _isVaapiDeviceInteliHD;
|
||||||
@@ -154,6 +158,8 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||||||
|
|
||||||
_threads = EncodingHelper.GetNumberOfThreads(null, options, null);
|
_threads = EncodingHelper.GetNumberOfThreads(null, options, null);
|
||||||
|
|
||||||
|
_isPkeyPauseSupported = validator.CheckSupportedRuntimeKey("p pause transcoding");
|
||||||
|
|
||||||
// Check the Vaapi device vendor
|
// Check the Vaapi device vendor
|
||||||
if (OperatingSystem.IsLinux()
|
if (OperatingSystem.IsLinux()
|
||||||
&& SupportsHwaccel("vaapi")
|
&& SupportsHwaccel("vaapi")
|
||||||
@@ -376,15 +382,15 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||||||
string analyzeDuration = string.Empty;
|
string analyzeDuration = string.Empty;
|
||||||
string ffmpegAnalyzeDuration = _config.GetFFmpegAnalyzeDuration() ?? string.Empty;
|
string ffmpegAnalyzeDuration = _config.GetFFmpegAnalyzeDuration() ?? string.Empty;
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(ffmpegAnalyzeDuration))
|
if (request.MediaSource.AnalyzeDurationMs > 0)
|
||||||
{
|
|
||||||
analyzeDuration = "-analyzeduration " + ffmpegAnalyzeDuration;
|
|
||||||
}
|
|
||||||
else if (request.MediaSource.AnalyzeDurationMs > 0)
|
|
||||||
{
|
{
|
||||||
analyzeDuration = "-analyzeduration " +
|
analyzeDuration = "-analyzeduration " +
|
||||||
(request.MediaSource.AnalyzeDurationMs * 1000).ToString();
|
(request.MediaSource.AnalyzeDurationMs * 1000).ToString();
|
||||||
}
|
}
|
||||||
|
else if (!string.IsNullOrEmpty(ffmpegAnalyzeDuration))
|
||||||
|
{
|
||||||
|
analyzeDuration = "-analyzeduration " + ffmpegAnalyzeDuration;
|
||||||
|
}
|
||||||
|
|
||||||
var forceEnableLogging = request.MediaSource.Protocol != MediaProtocol.File;
|
var forceEnableLogging = request.MediaSource.Protocol != MediaProtocol.File;
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,7 @@
|
|||||||
<PackageReference Include="libse" Version="3.6.5" />
|
<PackageReference Include="libse" Version="3.6.5" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" />
|
||||||
<PackageReference Include="System.Text.Encoding.CodePages" Version="6.0.0" />
|
<PackageReference Include="System.Text.Encoding.CodePages" Version="6.0.0" />
|
||||||
<PackageReference Include="UTF.Unknown" Version="2.5.0" />
|
<PackageReference Include="UTF.Unknown" Version="2.5.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<!-- Code Analyzers-->
|
<!-- Code Analyzers-->
|
||||||
|
|||||||
@@ -310,5 +310,12 @@ namespace MediaBrowser.MediaEncoding.Probing
|
|||||||
/// <value>The color primaries.</value>
|
/// <value>The color primaries.</value>
|
||||||
[JsonPropertyName("color_primaries")]
|
[JsonPropertyName("color_primaries")]
|
||||||
public string ColorPrimaries { get; set; }
|
public string ColorPrimaries { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the side_data_list.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>The side_data_list.</value>
|
||||||
|
[JsonPropertyName("side_data_list")]
|
||||||
|
public IReadOnlyList<MediaStreamInfoSideData> SideDataList { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace MediaBrowser.MediaEncoding.Probing
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Class MediaStreamInfoSideData.
|
||||||
|
/// </summary>
|
||||||
|
public class MediaStreamInfoSideData
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the SideDataType.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>The SideDataType.</value>
|
||||||
|
[JsonPropertyName("side_data_type")]
|
||||||
|
public string? SideDataType { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the DvVersionMajor.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>The DvVersionMajor.</value>
|
||||||
|
[JsonPropertyName("dv_version_major")]
|
||||||
|
public int? DvVersionMajor { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the DvVersionMinor.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>The DvVersionMinor.</value>
|
||||||
|
[JsonPropertyName("dv_version_minor")]
|
||||||
|
public int? DvVersionMinor { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the DvProfile.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>The DvProfile.</value>
|
||||||
|
[JsonPropertyName("dv_profile")]
|
||||||
|
public int? DvProfile { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the DvLevel.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>The DvLevel.</value>
|
||||||
|
[JsonPropertyName("dv_level")]
|
||||||
|
public int? DvLevel { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the RpuPresentFlag.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>The RpuPresentFlag.</value>
|
||||||
|
[JsonPropertyName("rpu_present_flag")]
|
||||||
|
public int? RpuPresentFlag { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the ElPresentFlag.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>The ElPresentFlag.</value>
|
||||||
|
[JsonPropertyName("el_present_flag")]
|
||||||
|
public int? ElPresentFlag { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the BlPresentFlag.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>The BlPresentFlag.</value>
|
||||||
|
[JsonPropertyName("bl_present_flag")]
|
||||||
|
public int? BlPresentFlag { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the DvBlSignalCompatibilityId.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>The DvBlSignalCompatibilityId.</value>
|
||||||
|
[JsonPropertyName("dv_bl_signal_compatibility_id")]
|
||||||
|
public int? DvBlSignalCompatibilityId { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -841,9 +841,35 @@ namespace MediaBrowser.MediaEncoding.Probing
|
|||||||
{
|
{
|
||||||
stream.ColorPrimaries = streamInfo.ColorPrimaries;
|
stream.ColorPrimaries = streamInfo.ColorPrimaries;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (streamInfo.SideDataList != null)
|
||||||
|
{
|
||||||
|
foreach (var data in streamInfo.SideDataList)
|
||||||
|
{
|
||||||
|
// Parse Dolby Vision metadata from side_data
|
||||||
|
if (string.Equals(data.SideDataType, "DOVI configuration record", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
stream.DvVersionMajor = data.DvVersionMajor;
|
||||||
|
stream.DvVersionMinor = data.DvVersionMinor;
|
||||||
|
stream.DvProfile = data.DvProfile;
|
||||||
|
stream.DvLevel = data.DvLevel;
|
||||||
|
stream.RpuPresentFlag = data.RpuPresentFlag;
|
||||||
|
stream.ElPresentFlag = data.ElPresentFlag;
|
||||||
|
stream.BlPresentFlag = data.BlPresentFlag;
|
||||||
|
stream.DvBlSignalCompatibilityId = data.DvBlSignalCompatibilityId;
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (string.Equals(streamInfo.CodecType, "data", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
stream.Type = MediaStreamType.Data;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
_logger.LogError("Codec Type {CodecType} unknown. The stream (index: {Index}) will be ignored. Warning: Subsequential streams will have a wrong stream specifier!", streamInfo.CodecType, streamInfo.Index);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
using System;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using System.Threading;
|
||||||
|
using MediaBrowser.Model.MediaInfo;
|
||||||
|
|
||||||
|
namespace MediaBrowser.MediaEncoding.Subtitles
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// ASS subtitle writer.
|
||||||
|
/// </summary>
|
||||||
|
public class AssWriter : ISubtitleWriter
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void Write(SubtitleTrackInfo info, Stream stream, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
using (var writer = new StreamWriter(stream, Encoding.UTF8, 1024, true))
|
||||||
|
{
|
||||||
|
var trackEvents = info.TrackEvents;
|
||||||
|
var timeFormat = @"hh\:mm\:ss\.ff";
|
||||||
|
|
||||||
|
// Write ASS header
|
||||||
|
writer.WriteLine("[Script Info]");
|
||||||
|
writer.WriteLine("Title: Jellyfin transcoded ASS subtitle");
|
||||||
|
writer.WriteLine("ScriptType: v4.00+");
|
||||||
|
writer.WriteLine();
|
||||||
|
writer.WriteLine("[V4+ Styles]");
|
||||||
|
writer.WriteLine("Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding");
|
||||||
|
writer.WriteLine("Style: Default,Arial,20,&H00FFFFFF,&H00FFFFFF,&H19333333,&H910E0807,0,0,0,0,100,100,0,0,0,1,0,2,10,10,10,1");
|
||||||
|
writer.WriteLine();
|
||||||
|
writer.WriteLine("[Events]");
|
||||||
|
writer.WriteLine("Format: Layer, Start, End, Style, Text");
|
||||||
|
|
||||||
|
for (int i = 0; i < trackEvents.Count; i++)
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
var trackEvent = trackEvents[i];
|
||||||
|
var startTime = TimeSpan.FromTicks(trackEvent.StartPositionTicks).ToString(timeFormat, CultureInfo.InvariantCulture);
|
||||||
|
var endTime = TimeSpan.FromTicks(trackEvent.EndPositionTicks).ToString(timeFormat, CultureInfo.InvariantCulture);
|
||||||
|
var text = Regex.Replace(trackEvent.Text, @"\n", "\\n", RegexOptions.IgnoreCase);
|
||||||
|
|
||||||
|
writer.WriteLine(
|
||||||
|
"Dialogue: 0,{0},{1},Default,{2}",
|
||||||
|
startTime,
|
||||||
|
endTime,
|
||||||
|
text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
using System;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using System.Threading;
|
||||||
|
using MediaBrowser.Model.MediaInfo;
|
||||||
|
|
||||||
|
namespace MediaBrowser.MediaEncoding.Subtitles
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// SSA subtitle writer.
|
||||||
|
/// </summary>
|
||||||
|
public class SsaWriter : ISubtitleWriter
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void Write(SubtitleTrackInfo info, Stream stream, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
using (var writer = new StreamWriter(stream, Encoding.UTF8, 1024, true))
|
||||||
|
{
|
||||||
|
var trackEvents = info.TrackEvents;
|
||||||
|
var timeFormat = @"hh\:mm\:ss\.ff";
|
||||||
|
|
||||||
|
// Write SSA header
|
||||||
|
writer.WriteLine("[Script Info]");
|
||||||
|
writer.WriteLine("Title: Jellyfin transcoded SSA subtitle");
|
||||||
|
writer.WriteLine("ScriptType: v4.00");
|
||||||
|
writer.WriteLine();
|
||||||
|
writer.WriteLine("[V4 Styles]");
|
||||||
|
writer.WriteLine("Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, TertiaryColour, BackColour, Bold, Italic, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, AlphaLevel, Encoding");
|
||||||
|
writer.WriteLine("Style: Default,Arial,20,&H00FFFFFF,&H00FFFFFF,&H19333333,&H19333333,0,0,0,1,0,2,10,10,10,0,1");
|
||||||
|
writer.WriteLine();
|
||||||
|
writer.WriteLine("[Events]");
|
||||||
|
writer.WriteLine("Format: Layer, Start, End, Style, Text");
|
||||||
|
|
||||||
|
for (int i = 0; i < trackEvents.Count; i++)
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
var trackEvent = trackEvents[i];
|
||||||
|
var startTime = TimeSpan.FromTicks(trackEvent.StartPositionTicks).ToString(timeFormat, CultureInfo.InvariantCulture);
|
||||||
|
var endTime = TimeSpan.FromTicks(trackEvent.EndPositionTicks).ToString(timeFormat, CultureInfo.InvariantCulture);
|
||||||
|
var text = Regex.Replace(trackEvent.Text, @"\n", "\\n", RegexOptions.IgnoreCase);
|
||||||
|
|
||||||
|
writer.WriteLine(
|
||||||
|
"Dialogue: 0,{0},{1},Default,{2}",
|
||||||
|
startTime,
|
||||||
|
endTime,
|
||||||
|
text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -238,7 +238,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
|||||||
// Convert
|
// Convert
|
||||||
var outputPath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, ".srt");
|
var outputPath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, ".srt");
|
||||||
|
|
||||||
await ConvertTextSubtitleToSrt(subtitleStream.Path, subtitleStream.Language, mediaSource, outputPath, cancellationToken).ConfigureAwait(false);
|
await ConvertTextSubtitleToSrt(subtitleStream, mediaSource, outputPath, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
return new SubtitleInfo(outputPath, MediaProtocol.File, "srt", true);
|
return new SubtitleInfo(outputPath, MediaProtocol.File, "srt", true);
|
||||||
}
|
}
|
||||||
@@ -283,6 +283,12 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
|||||||
|
|
||||||
private bool TryGetWriter(string format, [NotNullWhen(true)] out ISubtitleWriter? value)
|
private bool TryGetWriter(string format, [NotNullWhen(true)] out ISubtitleWriter? value)
|
||||||
{
|
{
|
||||||
|
if (string.Equals(format, SubtitleFormat.ASS, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
value = new AssWriter();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(format))
|
if (string.IsNullOrEmpty(format))
|
||||||
{
|
{
|
||||||
throw new ArgumentNullException(nameof(format));
|
throw new ArgumentNullException(nameof(format));
|
||||||
@@ -294,12 +300,18 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.Equals(format, SubtitleFormat.SRT, StringComparison.OrdinalIgnoreCase))
|
if (string.Equals(format, SubtitleFormat.SRT, StringComparison.OrdinalIgnoreCase) || string.Equals(format, SubtitleFormat.SUBRIP, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
value = new SrtWriter();
|
value = new SrtWriter();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (string.Equals(format, SubtitleFormat.SSA, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
value = new SsaWriter();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
if (string.Equals(format, SubtitleFormat.VTT, StringComparison.OrdinalIgnoreCase))
|
if (string.Equals(format, SubtitleFormat.VTT, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
value = new VttWriter();
|
value = new VttWriter();
|
||||||
@@ -339,13 +351,12 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Converts the text subtitle to SRT.
|
/// Converts the text subtitle to SRT.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="inputPath">The input path.</param>
|
/// <param name="subtitleStream">The subtitle stream.</param>
|
||||||
/// <param name="language">The language.</param>
|
|
||||||
/// <param name="mediaSource">The input mediaSource.</param>
|
/// <param name="mediaSource">The input mediaSource.</param>
|
||||||
/// <param name="outputPath">The output path.</param>
|
/// <param name="outputPath">The output path.</param>
|
||||||
/// <param name="cancellationToken">The cancellation token.</param>
|
/// <param name="cancellationToken">The cancellation token.</param>
|
||||||
/// <returns>Task.</returns>
|
/// <returns>Task.</returns>
|
||||||
private async Task ConvertTextSubtitleToSrt(string inputPath, string language, MediaSourceInfo mediaSource, string outputPath, CancellationToken cancellationToken)
|
private async Task ConvertTextSubtitleToSrt(MediaStream subtitleStream, MediaSourceInfo mediaSource, string outputPath, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var semaphore = GetLock(outputPath);
|
var semaphore = GetLock(outputPath);
|
||||||
|
|
||||||
@@ -355,7 +366,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
|||||||
{
|
{
|
||||||
if (!File.Exists(outputPath))
|
if (!File.Exists(outputPath))
|
||||||
{
|
{
|
||||||
await ConvertTextSubtitleToSrtInternal(inputPath, language, mediaSource, outputPath, cancellationToken).ConfigureAwait(false);
|
await ConvertTextSubtitleToSrtInternal(subtitleStream, mediaSource, outputPath, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
@@ -367,8 +378,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Converts the text subtitle to SRT internal.
|
/// Converts the text subtitle to SRT internal.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="inputPath">The input path.</param>
|
/// <param name="subtitleStream">The subtitle stream.</param>
|
||||||
/// <param name="language">The language.</param>
|
|
||||||
/// <param name="mediaSource">The input mediaSource.</param>
|
/// <param name="mediaSource">The input mediaSource.</param>
|
||||||
/// <param name="outputPath">The output path.</param>
|
/// <param name="outputPath">The output path.</param>
|
||||||
/// <param name="cancellationToken">The cancellation token.</param>
|
/// <param name="cancellationToken">The cancellation token.</param>
|
||||||
@@ -376,8 +386,9 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
|||||||
/// <exception cref="ArgumentNullException">
|
/// <exception cref="ArgumentNullException">
|
||||||
/// The <c>inputPath</c> or <c>outputPath</c> is <c>null</c>.
|
/// The <c>inputPath</c> or <c>outputPath</c> is <c>null</c>.
|
||||||
/// </exception>
|
/// </exception>
|
||||||
private async Task ConvertTextSubtitleToSrtInternal(string inputPath, string language, MediaSourceInfo mediaSource, string outputPath, CancellationToken cancellationToken)
|
private async Task ConvertTextSubtitleToSrtInternal(MediaStream subtitleStream, MediaSourceInfo mediaSource, string outputPath, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
|
var inputPath = subtitleStream.Path;
|
||||||
if (string.IsNullOrEmpty(inputPath))
|
if (string.IsNullOrEmpty(inputPath))
|
||||||
{
|
{
|
||||||
throw new ArgumentNullException(nameof(inputPath));
|
throw new ArgumentNullException(nameof(inputPath));
|
||||||
@@ -390,7 +401,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
|||||||
|
|
||||||
Directory.CreateDirectory(Path.GetDirectoryName(outputPath) ?? throw new ArgumentException($"Provided path ({outputPath}) is not valid.", nameof(outputPath)));
|
Directory.CreateDirectory(Path.GetDirectoryName(outputPath) ?? throw new ArgumentException($"Provided path ({outputPath}) is not valid.", nameof(outputPath)));
|
||||||
|
|
||||||
var encodingParam = await GetSubtitleFileCharacterSet(inputPath, language, mediaSource.Protocol, cancellationToken).ConfigureAwait(false);
|
var encodingParam = await GetSubtitleFileCharacterSet(subtitleStream, subtitleStream.Language, mediaSource, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
// FFmpeg automatically convert character encoding when it is UTF-16
|
// FFmpeg automatically convert character encoding when it is UTF-16
|
||||||
// If we specify character encoding, it rejects with "do not specify a character encoding" and "Unable to recode subtitle event"
|
// If we specify character encoding, it rejects with "do not specify a character encoding" and "Unable to recode subtitle event"
|
||||||
@@ -408,18 +419,18 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
|||||||
int exitCode;
|
int exitCode;
|
||||||
|
|
||||||
using (var process = new Process
|
using (var process = new Process
|
||||||
|
{
|
||||||
|
StartInfo = new ProcessStartInfo
|
||||||
{
|
{
|
||||||
StartInfo = new ProcessStartInfo
|
CreateNoWindow = true,
|
||||||
{
|
UseShellExecute = false,
|
||||||
CreateNoWindow = true,
|
FileName = _mediaEncoder.EncoderPath,
|
||||||
UseShellExecute = false,
|
Arguments = string.Format(CultureInfo.InvariantCulture, "{0} -i \"{1}\" -c:s srt \"{2}\"", encodingParam, inputPath, outputPath),
|
||||||
FileName = _mediaEncoder.EncoderPath,
|
WindowStyle = ProcessWindowStyle.Hidden,
|
||||||
Arguments = string.Format(CultureInfo.InvariantCulture, "{0} -i \"{1}\" -c:s srt \"{2}\"", encodingParam, inputPath, outputPath),
|
ErrorDialog = false
|
||||||
WindowStyle = ProcessWindowStyle.Hidden,
|
},
|
||||||
ErrorDialog = false
|
EnableRaisingEvents = true
|
||||||
},
|
})
|
||||||
EnableRaisingEvents = true
|
|
||||||
})
|
|
||||||
{
|
{
|
||||||
_logger.LogInformation("{0} {1}", process.StartInfo.FileName, process.StartInfo.Arguments);
|
_logger.LogInformation("{0} {1}", process.StartInfo.FileName, process.StartInfo.Arguments);
|
||||||
|
|
||||||
@@ -559,7 +570,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
|||||||
|
|
||||||
var processArgs = string.Format(
|
var processArgs = string.Format(
|
||||||
CultureInfo.InvariantCulture,
|
CultureInfo.InvariantCulture,
|
||||||
"-i {0} -map 0:{1} -an -vn -c:s {2} \"{3}\"",
|
"-i {0} -copyts -map 0:{1} -an -vn -c:s {2} \"{3}\"",
|
||||||
inputPath,
|
inputPath,
|
||||||
subtitleStreamIndex,
|
subtitleStreamIndex,
|
||||||
outputCodec,
|
outputCodec,
|
||||||
@@ -568,18 +579,18 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
|||||||
int exitCode;
|
int exitCode;
|
||||||
|
|
||||||
using (var process = new Process
|
using (var process = new Process
|
||||||
|
{
|
||||||
|
StartInfo = new ProcessStartInfo
|
||||||
{
|
{
|
||||||
StartInfo = new ProcessStartInfo
|
CreateNoWindow = true,
|
||||||
{
|
UseShellExecute = false,
|
||||||
CreateNoWindow = true,
|
FileName = _mediaEncoder.EncoderPath,
|
||||||
UseShellExecute = false,
|
Arguments = processArgs,
|
||||||
FileName = _mediaEncoder.EncoderPath,
|
WindowStyle = ProcessWindowStyle.Hidden,
|
||||||
Arguments = processArgs,
|
ErrorDialog = false
|
||||||
WindowStyle = ProcessWindowStyle.Hidden,
|
},
|
||||||
ErrorDialog = false
|
EnableRaisingEvents = true
|
||||||
},
|
})
|
||||||
EnableRaisingEvents = true
|
|
||||||
})
|
|
||||||
{
|
{
|
||||||
_logger.LogInformation("{File} {Arguments}", process.StartInfo.FileName, process.StartInfo.Arguments);
|
_logger.LogInformation("{File} {Arguments}", process.StartInfo.FileName, process.StartInfo.Arguments);
|
||||||
|
|
||||||
@@ -594,7 +605,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
|||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
|
|
||||||
var ranToCompletion = await process.WaitForExitAsync(TimeSpan.FromMinutes(5)).ConfigureAwait(false);
|
var ranToCompletion = await process.WaitForExitAsync(TimeSpan.FromMinutes(30)).ConfigureAwait(false);
|
||||||
|
|
||||||
if (!ranToCompletion)
|
if (!ranToCompletion)
|
||||||
{
|
{
|
||||||
@@ -717,9 +728,19 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<string> GetSubtitleFileCharacterSet(string path, string language, MediaProtocol protocol, CancellationToken cancellationToken)
|
public async Task<string> GetSubtitleFileCharacterSet(MediaStream subtitleStream, string language, MediaSourceInfo mediaSource, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
using (var stream = await GetStream(path, protocol, cancellationToken).ConfigureAwait(false))
|
var subtitleCodec = subtitleStream.Codec;
|
||||||
|
var path = subtitleStream.Path;
|
||||||
|
|
||||||
|
if (path.EndsWith(".mks", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
path = GetSubtitleCachePath(mediaSource, subtitleStream.Index, "." + subtitleCodec);
|
||||||
|
await ExtractTextSubtitle(mediaSource, subtitleStream, subtitleCodec, path, cancellationToken)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
using (var stream = await GetStream(path, mediaSource.Protocol, cancellationToken).ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
var charset = CharsetDetector.DetectFromStream(stream).Detected?.EncodingName ?? string.Empty;
|
var charset = CharsetDetector.DetectFromStream(stream).Detected?.EncodingName ?? string.Empty;
|
||||||
|
|
||||||
@@ -742,12 +763,12 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
|||||||
switch (protocol)
|
switch (protocol)
|
||||||
{
|
{
|
||||||
case MediaProtocol.Http:
|
case MediaProtocol.Http:
|
||||||
{
|
{
|
||||||
using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
|
using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
|
||||||
.GetAsync(new Uri(path), cancellationToken)
|
.GetAsync(new Uri(path), cancellationToken)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
return await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
return await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
case MediaProtocol.File:
|
case MediaProtocol.File:
|
||||||
return AsyncFile.OpenRead(path);
|
return AsyncFile.OpenRead(path);
|
||||||
|
|||||||
@@ -20,6 +20,11 @@ public class BrandingOptions
|
|||||||
/// <value>The custom CSS.</value>
|
/// <value>The custom CSS.</value>
|
||||||
public string? CustomCss { get; set; }
|
public string? CustomCss { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a value indicating whether to enable the splashscreen.
|
||||||
|
/// </summary>
|
||||||
|
public bool SplashscreenEnabled { get; set; } = true;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the splashscreen location on disk.
|
/// Gets or sets the splashscreen location on disk.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ namespace MediaBrowser.Model.Configuration
|
|||||||
TonemappingThreshold = 0.8;
|
TonemappingThreshold = 0.8;
|
||||||
TonemappingPeak = 100;
|
TonemappingPeak = 100;
|
||||||
TonemappingParam = 0;
|
TonemappingParam = 0;
|
||||||
|
VppTonemappingBrightness = 0;
|
||||||
|
VppTonemappingContrast = 1.2;
|
||||||
H264Crf = 23;
|
H264Crf = 23;
|
||||||
H265Crf = 28;
|
H265Crf = 28;
|
||||||
DeinterlaceDoubleRate = false;
|
DeinterlaceDoubleRate = false;
|
||||||
@@ -89,6 +91,10 @@ namespace MediaBrowser.Model.Configuration
|
|||||||
|
|
||||||
public double TonemappingParam { get; set; }
|
public double TonemappingParam { get; set; }
|
||||||
|
|
||||||
|
public double VppTonemappingBrightness { get; set; }
|
||||||
|
|
||||||
|
public double VppTonemappingContrast { get; set; }
|
||||||
|
|
||||||
public int H264Crf { get; set; }
|
public int H264Crf { get; set; }
|
||||||
|
|
||||||
public int H265Crf { get; set; }
|
public int H265Crf { get; set; }
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ namespace MediaBrowser.Model.Dlna
|
|||||||
int? videoBitDepth,
|
int? videoBitDepth,
|
||||||
int? videoBitrate,
|
int? videoBitrate,
|
||||||
string? videoProfile,
|
string? videoProfile,
|
||||||
|
string? videoRangeType,
|
||||||
double? videoLevel,
|
double? videoLevel,
|
||||||
float? videoFramerate,
|
float? videoFramerate,
|
||||||
int? packetLength,
|
int? packetLength,
|
||||||
@@ -42,6 +43,8 @@ namespace MediaBrowser.Model.Dlna
|
|||||||
return IsConditionSatisfied(condition, videoLevel);
|
return IsConditionSatisfied(condition, videoLevel);
|
||||||
case ProfileConditionValue.VideoProfile:
|
case ProfileConditionValue.VideoProfile:
|
||||||
return IsConditionSatisfied(condition, videoProfile);
|
return IsConditionSatisfied(condition, videoProfile);
|
||||||
|
case ProfileConditionValue.VideoRangeType:
|
||||||
|
return IsConditionSatisfied(condition, videoRangeType);
|
||||||
case ProfileConditionValue.VideoCodecTag:
|
case ProfileConditionValue.VideoCodecTag:
|
||||||
return IsConditionSatisfied(condition, videoCodecTag);
|
return IsConditionSatisfied(condition, videoCodecTag);
|
||||||
case ProfileConditionValue.PacketLength:
|
case ProfileConditionValue.PacketLength:
|
||||||
|
|||||||
@@ -128,6 +128,7 @@ namespace MediaBrowser.Model.Dlna
|
|||||||
bool isDirectStream,
|
bool isDirectStream,
|
||||||
long? runtimeTicks,
|
long? runtimeTicks,
|
||||||
string videoProfile,
|
string videoProfile,
|
||||||
|
string videoRangeType,
|
||||||
double? videoLevel,
|
double? videoLevel,
|
||||||
float? videoFramerate,
|
float? videoFramerate,
|
||||||
int? packetLength,
|
int? packetLength,
|
||||||
@@ -176,6 +177,7 @@ namespace MediaBrowser.Model.Dlna
|
|||||||
bitDepth,
|
bitDepth,
|
||||||
videoBitrate,
|
videoBitrate,
|
||||||
videoProfile,
|
videoProfile,
|
||||||
|
videoRangeType,
|
||||||
videoLevel,
|
videoLevel,
|
||||||
videoFramerate,
|
videoFramerate,
|
||||||
packetLength,
|
packetLength,
|
||||||
|
|||||||
@@ -423,6 +423,7 @@ namespace MediaBrowser.Model.Dlna
|
|||||||
/// <param name="bitDepth">The bit depth.</param>
|
/// <param name="bitDepth">The bit depth.</param>
|
||||||
/// <param name="videoBitrate">The video bitrate.</param>
|
/// <param name="videoBitrate">The video bitrate.</param>
|
||||||
/// <param name="videoProfile">The video profile.</param>
|
/// <param name="videoProfile">The video profile.</param>
|
||||||
|
/// <param name="videoRangeType">The video range type.</param>
|
||||||
/// <param name="videoLevel">The video level.</param>
|
/// <param name="videoLevel">The video level.</param>
|
||||||
/// <param name="videoFramerate">The video framerate.</param>
|
/// <param name="videoFramerate">The video framerate.</param>
|
||||||
/// <param name="packetLength">The packet length.</param>
|
/// <param name="packetLength">The packet length.</param>
|
||||||
@@ -444,6 +445,7 @@ namespace MediaBrowser.Model.Dlna
|
|||||||
int? bitDepth,
|
int? bitDepth,
|
||||||
int? videoBitrate,
|
int? videoBitrate,
|
||||||
string videoProfile,
|
string videoProfile,
|
||||||
|
string videoRangeType,
|
||||||
double? videoLevel,
|
double? videoLevel,
|
||||||
float? videoFramerate,
|
float? videoFramerate,
|
||||||
int? packetLength,
|
int? packetLength,
|
||||||
@@ -483,7 +485,7 @@ namespace MediaBrowser.Model.Dlna
|
|||||||
var anyOff = false;
|
var anyOff = false;
|
||||||
foreach (ProfileCondition c in i.Conditions)
|
foreach (ProfileCondition c in i.Conditions)
|
||||||
{
|
{
|
||||||
if (!ConditionProcessor.IsVideoConditionSatisfied(GetModelProfileCondition(c), width, height, bitDepth, videoBitrate, videoProfile, videoLevel, videoFramerate, packetLength, timestamp, isAnamorphic, isInterlaced, refFrames, numVideoStreams, numAudioStreams, videoCodecTag, isAvc))
|
if (!ConditionProcessor.IsVideoConditionSatisfied(GetModelProfileCondition(c), width, height, bitDepth, videoBitrate, videoProfile, videoRangeType, videoLevel, videoFramerate, packetLength, timestamp, isAnamorphic, isInterlaced, refFrames, numVideoStreams, numAudioStreams, videoCodecTag, isAvc))
|
||||||
{
|
{
|
||||||
anyOff = true;
|
anyOff = true;
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ namespace MediaBrowser.Model.Dlna
|
|||||||
IsAvc = 20,
|
IsAvc = 20,
|
||||||
IsInterlaced = 21,
|
IsInterlaced = 21,
|
||||||
AudioSampleRate = 22,
|
AudioSampleRate = 22,
|
||||||
AudioBitDepth = 23
|
AudioBitDepth = 23,
|
||||||
|
VideoRangeType = 24
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -221,6 +221,9 @@ namespace MediaBrowser.Model.Dlna
|
|||||||
case ProfileConditionValue.VideoProfile:
|
case ProfileConditionValue.VideoProfile:
|
||||||
return TranscodeReason.VideoProfileNotSupported;
|
return TranscodeReason.VideoProfileNotSupported;
|
||||||
|
|
||||||
|
case ProfileConditionValue.VideoRangeType:
|
||||||
|
return TranscodeReason.VideoRangeTypeNotSupported;
|
||||||
|
|
||||||
case ProfileConditionValue.VideoTimestamp:
|
case ProfileConditionValue.VideoTimestamp:
|
||||||
// TODO
|
// TODO
|
||||||
return 0;
|
return 0;
|
||||||
@@ -433,9 +436,9 @@ namespace MediaBrowser.Model.Dlna
|
|||||||
{
|
{
|
||||||
containerSupported = true;
|
containerSupported = true;
|
||||||
|
|
||||||
videoSupported = videoStream != null && profile.SupportsVideoCodec(videoStream.Codec);
|
videoSupported = videoStream == null || profile.SupportsVideoCodec(videoStream.Codec);
|
||||||
|
|
||||||
audioSupported = audioStream != null && profile.SupportsAudioCodec(audioStream.Codec);
|
audioSupported = audioStream == null || profile.SupportsAudioCodec(audioStream.Codec);
|
||||||
|
|
||||||
if (videoSupported && audioSupported)
|
if (videoSupported && audioSupported)
|
||||||
{
|
{
|
||||||
@@ -444,18 +447,17 @@ namespace MediaBrowser.Model.Dlna
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var list = new List<TranscodeReason>();
|
|
||||||
if (!containerSupported)
|
if (!containerSupported)
|
||||||
{
|
{
|
||||||
reasons |= TranscodeReason.ContainerNotSupported;
|
reasons |= TranscodeReason.ContainerNotSupported;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (videoStream != null && !videoSupported)
|
if (!videoSupported)
|
||||||
{
|
{
|
||||||
reasons |= TranscodeReason.VideoCodecNotSupported;
|
reasons |= TranscodeReason.VideoCodecNotSupported;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (audioStream != null && !audioSupported)
|
if (!audioSupported)
|
||||||
{
|
{
|
||||||
reasons |= TranscodeReason.AudioCodecNotSupported;
|
reasons |= TranscodeReason.AudioCodecNotSupported;
|
||||||
}
|
}
|
||||||
@@ -587,21 +589,19 @@ namespace MediaBrowser.Model.Dlna
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Collect candidate audio streams
|
// Collect candidate audio streams
|
||||||
IEnumerable<MediaStream> candidateAudioStreams = audioStream == null ? Array.Empty<MediaStream>() : new[] { audioStream };
|
ICollection<MediaStream> candidateAudioStreams = audioStream == null ? Array.Empty<MediaStream>() : new[] { audioStream };
|
||||||
if (!options.AudioStreamIndex.HasValue || options.AudioStreamIndex < 0)
|
if (!options.AudioStreamIndex.HasValue || options.AudioStreamIndex < 0)
|
||||||
{
|
{
|
||||||
if (audioStream?.IsDefault == true)
|
if (audioStream?.IsDefault == true)
|
||||||
{
|
{
|
||||||
candidateAudioStreams = item.MediaStreams.Where(stream => stream.Type == MediaStreamType.Audio && stream.IsDefault);
|
candidateAudioStreams = item.MediaStreams.Where(stream => stream.Type == MediaStreamType.Audio && stream.IsDefault).ToArray();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
candidateAudioStreams = item.MediaStreams.Where(stream => stream.Type == MediaStreamType.Audio && stream.Language == audioStream?.Language);
|
candidateAudioStreams = item.MediaStreams.Where(stream => stream.Type == MediaStreamType.Audio && stream.Language == audioStream?.Language).ToArray();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
candidateAudioStreams = candidateAudioStreams.ToArray();
|
|
||||||
|
|
||||||
var videoStream = item.VideoStream;
|
var videoStream = item.VideoStream;
|
||||||
|
|
||||||
var directPlayBitrateEligibility = IsBitrateEligibleForDirectPlayback(item, options.GetMaxBitrate(false) ?? 0, options, PlayMethod.DirectPlay);
|
var directPlayBitrateEligibility = IsBitrateEligibleForDirectPlayback(item, options.GetMaxBitrate(false) ?? 0, options, PlayMethod.DirectPlay);
|
||||||
@@ -748,9 +748,9 @@ namespace MediaBrowser.Model.Dlna
|
|||||||
var appliedVideoConditions = options.Profile.CodecProfiles
|
var appliedVideoConditions = options.Profile.CodecProfiles
|
||||||
.Where(i => i.Type == CodecType.Video &&
|
.Where(i => i.Type == CodecType.Video &&
|
||||||
i.ContainsAnyCodec(videoCodec, container) &&
|
i.ContainsAnyCodec(videoCodec, container) &&
|
||||||
i.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoConditionSatisfied(applyCondition, videoStream?.Width, videoStream?.Height, videoStream?.BitDepth, videoStream?.BitRate, videoStream?.Profile, videoStream?.Level, videoFramerate, videoStream?.PacketLength, timestamp, videoStream?.IsAnamorphic, videoStream?.IsInterlaced, videoStream?.RefFrames, numVideoStreams, numAudioStreams, videoStream?.CodecTag, videoStream?.IsAVC)))
|
i.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoConditionSatisfied(applyCondition, videoStream?.Width, videoStream?.Height, videoStream?.BitDepth, videoStream?.BitRate, videoStream?.Profile, videoStream?.VideoRangeType, videoStream?.Level, videoFramerate, videoStream?.PacketLength, timestamp, videoStream?.IsAnamorphic, videoStream?.IsInterlaced, videoStream?.RefFrames, numVideoStreams, numAudioStreams, videoStream?.CodecTag, videoStream?.IsAVC)))
|
||||||
.Select(i =>
|
.Select(i =>
|
||||||
i.Conditions.All(condition => ConditionProcessor.IsVideoConditionSatisfied(condition, videoStream?.Width, videoStream?.Height, videoStream?.BitDepth, videoStream?.BitRate, videoStream?.Profile, videoStream?.Level, videoFramerate, videoStream?.PacketLength, timestamp, videoStream?.IsAnamorphic, videoStream?.IsInterlaced, videoStream?.RefFrames, numVideoStreams, numAudioStreams, videoStream?.CodecTag, videoStream?.IsAVC)));
|
i.Conditions.All(condition => ConditionProcessor.IsVideoConditionSatisfied(condition, videoStream?.Width, videoStream?.Height, videoStream?.BitDepth, videoStream?.BitRate, videoStream?.Profile, videoStream?.VideoRangeType, videoStream?.Level, videoFramerate, videoStream?.PacketLength, timestamp, videoStream?.IsAnamorphic, videoStream?.IsInterlaced, videoStream?.RefFrames, numVideoStreams, numAudioStreams, videoStream?.CodecTag, videoStream?.IsAVC)));
|
||||||
|
|
||||||
// An empty appliedVideoConditions means that the codec has no conditions for the current video stream
|
// An empty appliedVideoConditions means that the codec has no conditions for the current video stream
|
||||||
var conditionsSatisfied = appliedVideoConditions.All(satisfied => satisfied);
|
var conditionsSatisfied = appliedVideoConditions.All(satisfied => satisfied);
|
||||||
@@ -834,6 +834,7 @@ namespace MediaBrowser.Model.Dlna
|
|||||||
int? videoBitrate = videoStream?.BitRate;
|
int? videoBitrate = videoStream?.BitRate;
|
||||||
double? videoLevel = videoStream?.Level;
|
double? videoLevel = videoStream?.Level;
|
||||||
string videoProfile = videoStream?.Profile;
|
string videoProfile = videoStream?.Profile;
|
||||||
|
string videoRangeType = videoStream?.VideoRangeType;
|
||||||
float videoFramerate = videoStream == null ? 0 : videoStream.AverageFrameRate ?? videoStream.AverageFrameRate ?? 0;
|
float videoFramerate = videoStream == null ? 0 : videoStream.AverageFrameRate ?? videoStream.AverageFrameRate ?? 0;
|
||||||
bool? isAnamorphic = videoStream?.IsAnamorphic;
|
bool? isAnamorphic = videoStream?.IsAnamorphic;
|
||||||
bool? isInterlaced = videoStream?.IsInterlaced;
|
bool? isInterlaced = videoStream?.IsInterlaced;
|
||||||
@@ -850,7 +851,7 @@ namespace MediaBrowser.Model.Dlna
|
|||||||
var appliedVideoConditions = options.Profile.CodecProfiles
|
var appliedVideoConditions = options.Profile.CodecProfiles
|
||||||
.Where(i => i.Type == CodecType.Video &&
|
.Where(i => i.Type == CodecType.Video &&
|
||||||
i.ContainsAnyCodec(videoCodec, container) &&
|
i.ContainsAnyCodec(videoCodec, container) &&
|
||||||
i.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoConditionSatisfied(applyCondition, width, height, bitDepth, videoBitrate, videoProfile, videoLevel, videoFramerate, packetLength, timestamp, isAnamorphic, isInterlaced, refFrames, numVideoStreams, numAudioStreams, videoCodecTag, isAvc)));
|
i.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoConditionSatisfied(applyCondition, width, height, bitDepth, videoBitrate, videoProfile, videoRangeType, videoLevel, videoFramerate, packetLength, timestamp, isAnamorphic, isInterlaced, refFrames, numVideoStreams, numAudioStreams, videoCodecTag, isAvc)));
|
||||||
var isFirstAppliedCodecProfile = true;
|
var isFirstAppliedCodecProfile = true;
|
||||||
foreach (var i in appliedVideoConditions)
|
foreach (var i in appliedVideoConditions)
|
||||||
{
|
{
|
||||||
@@ -1056,7 +1057,7 @@ namespace MediaBrowser.Model.Dlna
|
|||||||
MediaSourceInfo mediaSource,
|
MediaSourceInfo mediaSource,
|
||||||
MediaStream videoStream,
|
MediaStream videoStream,
|
||||||
MediaStream audioStream,
|
MediaStream audioStream,
|
||||||
IEnumerable<MediaStream> candidateAudioStreams,
|
ICollection<MediaStream> candidateAudioStreams,
|
||||||
MediaStream subtitleStream,
|
MediaStream subtitleStream,
|
||||||
bool isEligibleForDirectPlay,
|
bool isEligibleForDirectPlay,
|
||||||
bool isEligibleForDirectStream)
|
bool isEligibleForDirectStream)
|
||||||
@@ -1081,6 +1082,7 @@ namespace MediaBrowser.Model.Dlna
|
|||||||
int? videoBitrate = videoStream?.BitRate;
|
int? videoBitrate = videoStream?.BitRate;
|
||||||
double? videoLevel = videoStream?.Level;
|
double? videoLevel = videoStream?.Level;
|
||||||
string videoProfile = videoStream?.Profile;
|
string videoProfile = videoStream?.Profile;
|
||||||
|
string videoRangeType = videoStream?.VideoRangeType;
|
||||||
float videoFramerate = videoStream == null ? 0 : videoStream.AverageFrameRate ?? videoStream.AverageFrameRate ?? 0;
|
float videoFramerate = videoStream == null ? 0 : videoStream.AverageFrameRate ?? videoStream.AverageFrameRate ?? 0;
|
||||||
bool? isAnamorphic = videoStream?.IsAnamorphic;
|
bool? isAnamorphic = videoStream?.IsAnamorphic;
|
||||||
bool? isInterlaced = videoStream?.IsInterlaced;
|
bool? isInterlaced = videoStream?.IsInterlaced;
|
||||||
@@ -1098,7 +1100,7 @@ namespace MediaBrowser.Model.Dlna
|
|||||||
int? numVideoStreams = mediaSource.GetStreamCount(MediaStreamType.Video);
|
int? numVideoStreams = mediaSource.GetStreamCount(MediaStreamType.Video);
|
||||||
|
|
||||||
var checkVideoConditions = (ProfileCondition[] conditions) =>
|
var checkVideoConditions = (ProfileCondition[] conditions) =>
|
||||||
conditions.Where(applyCondition => !ConditionProcessor.IsVideoConditionSatisfied(applyCondition, width, height, bitDepth, videoBitrate, videoProfile, videoLevel, videoFramerate, packetLength, timestamp, isAnamorphic, isInterlaced, refFrames, numVideoStreams, numAudioStreams, videoCodecTag, isAvc));
|
conditions.Where(applyCondition => !ConditionProcessor.IsVideoConditionSatisfied(applyCondition, width, height, bitDepth, videoBitrate, videoProfile, videoRangeType, videoLevel, videoFramerate, packetLength, timestamp, isAnamorphic, isInterlaced, refFrames, numVideoStreams, numAudioStreams, videoCodecTag, isAvc));
|
||||||
|
|
||||||
// Check container conditions
|
// Check container conditions
|
||||||
var containerProfileReasons = AggregateFailureConditions(
|
var containerProfileReasons = AggregateFailureConditions(
|
||||||
@@ -1177,14 +1179,18 @@ namespace MediaBrowser.Model.Dlna
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check audio codec
|
// Check audio codec
|
||||||
var selectedAudioStream = candidateAudioStreams.FirstOrDefault(audioStream => directPlayProfile.SupportsAudioCodec(audioStream.Codec));
|
MediaStream selectedAudioStream = null;
|
||||||
if (selectedAudioStream == null)
|
if (candidateAudioStreams.Any())
|
||||||
{
|
{
|
||||||
directPlayProfileReasons |= TranscodeReason.AudioCodecNotSupported;
|
selectedAudioStream = candidateAudioStreams.FirstOrDefault(audioStream => directPlayProfile.SupportsAudioCodec(audioStream.Codec));
|
||||||
}
|
if (selectedAudioStream == null)
|
||||||
else
|
{
|
||||||
{
|
directPlayProfileReasons |= TranscodeReason.AudioCodecNotSupported;
|
||||||
audioCodecProfileReasons = audioStreamMatches.GetValueOrDefault(selectedAudioStream);
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
audioCodecProfileReasons = audioStreamMatches.GetValueOrDefault(selectedAudioStream);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var failureReasons = directPlayProfileReasons | containerProfileReasons | subtitleProfileReasons;
|
var failureReasons = directPlayProfileReasons | containerProfileReasons | subtitleProfileReasons;
|
||||||
@@ -1852,6 +1858,38 @@ namespace MediaBrowser.Model.Dlna
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case ProfileConditionValue.VideoRangeType:
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(qualifier))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// change from split by | to comma
|
||||||
|
// strip spaces to avoid having to encode
|
||||||
|
var values = value
|
||||||
|
.Split('|', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||||
|
|
||||||
|
if (condition.Condition == ProfileConditionType.Equals)
|
||||||
|
{
|
||||||
|
item.SetOption(qualifier, "rangetype", string.Join(',', values));
|
||||||
|
}
|
||||||
|
else if (condition.Condition == ProfileConditionType.EqualsAny)
|
||||||
|
{
|
||||||
|
var currentValue = item.GetOption(qualifier, "rangetype");
|
||||||
|
if (!string.IsNullOrEmpty(currentValue) && values.Any(v => string.Equals(v, currentValue, StringComparison.OrdinalIgnoreCase)))
|
||||||
|
{
|
||||||
|
item.SetOption(qualifier, "rangetype", currentValue);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
item.SetOption(qualifier, "rangetype", string.Join(',', values));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case ProfileConditionValue.Height:
|
case ProfileConditionValue.Height:
|
||||||
{
|
{
|
||||||
if (!enableNonQualifiedConditions)
|
if (!enableNonQualifiedConditions)
|
||||||
|
|||||||
@@ -280,6 +280,29 @@ namespace MediaBrowser.Model.Dlna
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the target video range type that will be in the output stream.
|
||||||
|
/// </summary>
|
||||||
|
public string TargetVideoRangeType
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (IsDirectStream)
|
||||||
|
{
|
||||||
|
return TargetVideoStream?.VideoRangeType;
|
||||||
|
}
|
||||||
|
|
||||||
|
var targetVideoCodecs = TargetVideoCodec;
|
||||||
|
var videoCodec = targetVideoCodecs.Length == 0 ? null : targetVideoCodecs[0];
|
||||||
|
if (!string.IsNullOrEmpty(videoCodec))
|
||||||
|
{
|
||||||
|
return GetOption(videoCodec, "rangetype");
|
||||||
|
}
|
||||||
|
|
||||||
|
return TargetVideoStream?.VideoRangeType;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the target video codec tag.
|
/// Gets the target video codec tag.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -72,6 +72,54 @@ namespace MediaBrowser.Model.Entities
|
|||||||
/// <value>The color primaries.</value>
|
/// <value>The color primaries.</value>
|
||||||
public string ColorPrimaries { get; set; }
|
public string ColorPrimaries { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the Dolby Vision version major.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>The Dolby Vision version major.</value>
|
||||||
|
public int? DvVersionMajor { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the Dolby Vision version minor.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>The Dolby Vision version minor.</value>
|
||||||
|
public int? DvVersionMinor { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the Dolby Vision profile.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>The Dolby Vision profile.</value>
|
||||||
|
public int? DvProfile { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the Dolby Vision level.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>The Dolby Vision level.</value>
|
||||||
|
public int? DvLevel { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the Dolby Vision rpu present flag.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>The Dolby Vision rpu present flag.</value>
|
||||||
|
public int? RpuPresentFlag { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the Dolby Vision el present flag.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>The Dolby Vision el present flag.</value>
|
||||||
|
public int? ElPresentFlag { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the Dolby Vision bl present flag.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>The Dolby Vision bl present flag.</value>
|
||||||
|
public int? BlPresentFlag { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the Dolby Vision bl signal compatibility id.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>The Dolby Vision bl signal compatibility id.</value>
|
||||||
|
public int? DvBlSignalCompatibilityId { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the comment.
|
/// Gets or sets the comment.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -104,32 +152,64 @@ namespace MediaBrowser.Model.Entities
|
|||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
if (Type != MediaStreamType.Video)
|
var (videoRange, _) = GetVideoColorRange();
|
||||||
|
|
||||||
|
return videoRange;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the video range type.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>The video range type.</value>
|
||||||
|
public string VideoRangeType
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var (_, videoRangeType) = GetVideoColorRange();
|
||||||
|
|
||||||
|
return videoRangeType;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the video dovi title.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>The video dovi title.</value>
|
||||||
|
public string VideoDoViTitle
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var dvProfile = DvProfile;
|
||||||
|
var rpuPresentFlag = RpuPresentFlag == 1;
|
||||||
|
var blPresentFlag = BlPresentFlag == 1;
|
||||||
|
var dvBlCompatId = DvBlSignalCompatibilityId;
|
||||||
|
|
||||||
|
if (rpuPresentFlag
|
||||||
|
&& blPresentFlag
|
||||||
|
&& (dvProfile == 4
|
||||||
|
|| dvProfile == 5
|
||||||
|
|| dvProfile == 7
|
||||||
|
|| dvProfile == 8
|
||||||
|
|| dvProfile == 9))
|
||||||
{
|
{
|
||||||
return null;
|
var title = "DV Profile " + dvProfile;
|
||||||
|
|
||||||
|
if (dvBlCompatId > 0)
|
||||||
|
{
|
||||||
|
title += "." + dvBlCompatId;
|
||||||
|
}
|
||||||
|
|
||||||
|
return dvBlCompatId switch
|
||||||
|
{
|
||||||
|
1 => title + " (HDR10)",
|
||||||
|
2 => title + " (SDR)",
|
||||||
|
4 => title + " (HLG)",
|
||||||
|
_ => title
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
var colorTransfer = ColorTransfer;
|
return null;
|
||||||
|
|
||||||
if (string.Equals(colorTransfer, "smpte2084", StringComparison.OrdinalIgnoreCase)
|
|
||||||
|| string.Equals(colorTransfer, "arib-std-b67", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return "HDR";
|
|
||||||
}
|
|
||||||
|
|
||||||
// For some Dolby Vision files, no color transfer is provided, so check the codec
|
|
||||||
|
|
||||||
var codecTag = CodecTag;
|
|
||||||
|
|
||||||
if (string.Equals(codecTag, "dovi", StringComparison.OrdinalIgnoreCase)
|
|
||||||
|| string.Equals(codecTag, "dvh1", StringComparison.OrdinalIgnoreCase)
|
|
||||||
|| string.Equals(codecTag, "dvhe", StringComparison.OrdinalIgnoreCase)
|
|
||||||
|| string.Equals(codecTag, "dav1", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return "HDR";
|
|
||||||
}
|
|
||||||
|
|
||||||
return "SDR";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -508,15 +588,26 @@ namespace MediaBrowser.Model.Entities
|
|||||||
|
|
||||||
return Width switch
|
return Width switch
|
||||||
{
|
{
|
||||||
<= 720 when Height <= 480 => IsInterlaced ? "480i" : "480p",
|
// 256x144 (16:9 square pixel format)
|
||||||
// 720x576 (PAL) (768 when rescaled for square pixels)
|
<= 256 when Height <= 144 => IsInterlaced ? "144i" : "144p",
|
||||||
<= 768 when Height <= 576 => IsInterlaced ? "576i" : "576p",
|
// 426x240 (16:9 square pixel format)
|
||||||
// 960x540 (sometimes 544 which is multiple of 16)
|
<= 426 when Height <= 240 => IsInterlaced ? "240i" : "240p",
|
||||||
|
// 640x360 (16:9 square pixel format)
|
||||||
|
<= 640 when Height <= 360 => IsInterlaced ? "360i" : "360p",
|
||||||
|
// 682x384 (16:9 square pixel format)
|
||||||
|
<= 682 when Height <= 384 => IsInterlaced ? "384i" : "384p",
|
||||||
|
// 720x404 (16:9 square pixel format)
|
||||||
|
<= 720 when Height <= 404 => IsInterlaced ? "404i" : "404p",
|
||||||
|
// 854x480 (16:9 square pixel format)
|
||||||
|
<= 854 when Height <= 480 => IsInterlaced ? "480i" : "480p",
|
||||||
|
// 960x544 (16:9 square pixel format)
|
||||||
<= 960 when Height <= 544 => IsInterlaced ? "540i" : "540p",
|
<= 960 when Height <= 544 => IsInterlaced ? "540i" : "540p",
|
||||||
|
// 1024x576 (16:9 square pixel format)
|
||||||
|
<= 1024 when Height <= 576 => IsInterlaced ? "576i" : "576p",
|
||||||
// 1280x720
|
// 1280x720
|
||||||
<= 1280 when Height <= 962 => IsInterlaced ? "720i" : "720p",
|
<= 1280 when Height <= 962 => IsInterlaced ? "720i" : "720p",
|
||||||
// 1920x1080
|
// 2560x1080 (FHD ultra wide 21:9) using 1440px width to accomodate WQHD
|
||||||
<= 1920 when Height <= 1440 => IsInterlaced ? "1080i" : "1080p",
|
<= 2560 when Height <= 1440 => IsInterlaced ? "1080i" : "1080p",
|
||||||
// 4K
|
// 4K
|
||||||
<= 4096 when Height <= 3072 => "4K",
|
<= 4096 when Height <= 3072 => "4K",
|
||||||
// 8K
|
// 8K
|
||||||
@@ -571,5 +662,45 @@ namespace MediaBrowser.Model.Entities
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public (string VideoRange, string VideoRangeType) GetVideoColorRange()
|
||||||
|
{
|
||||||
|
if (Type != MediaStreamType.Video)
|
||||||
|
{
|
||||||
|
return (null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
var colorTransfer = ColorTransfer;
|
||||||
|
|
||||||
|
if (string.Equals(colorTransfer, "smpte2084", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return ("HDR", "HDR10");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.Equals(colorTransfer, "arib-std-b67", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return ("HDR", "HLG");
|
||||||
|
}
|
||||||
|
|
||||||
|
var codecTag = CodecTag;
|
||||||
|
var dvProfile = DvProfile;
|
||||||
|
var rpuPresentFlag = RpuPresentFlag == 1;
|
||||||
|
var blPresentFlag = BlPresentFlag == 1;
|
||||||
|
var dvBlCompatId = DvBlSignalCompatibilityId;
|
||||||
|
|
||||||
|
var isDoViHDRProfile = dvProfile == 5 || dvProfile == 7 || dvProfile == 8;
|
||||||
|
var isDoViHDRFlag = rpuPresentFlag && blPresentFlag && (dvBlCompatId == 0 || dvBlCompatId == 1 || dvBlCompatId == 4);
|
||||||
|
|
||||||
|
if ((isDoViHDRProfile && isDoViHDRFlag)
|
||||||
|
|| string.Equals(codecTag, "dovi", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| string.Equals(codecTag, "dvh1", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| string.Equals(codecTag, "dvhe", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| string.Equals(codecTag, "dav1", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return ("HDR", "DOVI");
|
||||||
|
}
|
||||||
|
|
||||||
|
return ("SDR", "SDR");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,11 @@ namespace MediaBrowser.Model.Entities
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// The embedded image.
|
/// The embedded image.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
EmbeddedImage
|
EmbeddedImage,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The data.
|
||||||
|
/// </summary>
|
||||||
|
Data
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Authors>Jellyfin Contributors</Authors>
|
<Authors>Jellyfin Contributors</Authors>
|
||||||
<PackageId>Jellyfin.Model</PackageId>
|
<PackageId>Jellyfin.Model</PackageId>
|
||||||
<VersionPrefix>10.8.0</VersionPrefix>
|
<VersionPrefix>10.8.7</VersionPrefix>
|
||||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
@@ -34,13 +34,13 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
|
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="6.0.1" />
|
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="6.0.2" />
|
||||||
<PackageReference Include="MimeTypes" Version="2.4.0">
|
<PackageReference Include="MimeTypes" Version="2.4.0">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="System.Globalization" Version="4.3.0" />
|
<PackageReference Include="System.Globalization" Version="4.3.0" />
|
||||||
<PackageReference Include="System.Text.Json" Version="6.0.4" />
|
<PackageReference Include="System.Text.Json" Version="6.0.6" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ namespace MediaBrowser.Model.MediaInfo
|
|||||||
public static class SubtitleFormat
|
public static class SubtitleFormat
|
||||||
{
|
{
|
||||||
public const string SRT = "srt";
|
public const string SRT = "srt";
|
||||||
|
public const string SUBRIP = "subrip";
|
||||||
public const string SSA = "ssa";
|
public const string SSA = "ssa";
|
||||||
public const string ASS = "ass";
|
public const string ASS = "ass";
|
||||||
public const string VTT = "vtt";
|
public const string VTT = "vtt";
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#pragma warning disable CS1591
|
|
||||||
|
|
||||||
namespace MediaBrowser.Model.Querying
|
namespace MediaBrowser.Model.Querying
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -7,6 +5,9 @@ namespace MediaBrowser.Model.Querying
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static class ItemSortBy
|
public static class ItemSortBy
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The aired episode order.
|
||||||
|
/// </summary>
|
||||||
public const string AiredEpisodeOrder = "AiredEpisodeOrder";
|
public const string AiredEpisodeOrder = "AiredEpisodeOrder";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -44,6 +45,9 @@ namespace MediaBrowser.Model.Querying
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public const string PremiereDate = "PremiereDate";
|
public const string PremiereDate = "PremiereDate";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The start date.
|
||||||
|
/// </summary>
|
||||||
public const string StartDate = "StartDate";
|
public const string StartDate = "StartDate";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -51,6 +55,9 @@ namespace MediaBrowser.Model.Querying
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public const string SortName = "SortName";
|
public const string SortName = "SortName";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The name.
|
||||||
|
/// </summary>
|
||||||
public const string Name = "Name";
|
public const string Name = "Name";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -83,28 +90,69 @@ namespace MediaBrowser.Model.Querying
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public const string CriticRating = "CriticRating";
|
public const string CriticRating = "CriticRating";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The IsFolder boolean.
|
||||||
|
/// </summary>
|
||||||
public const string IsFolder = "IsFolder";
|
public const string IsFolder = "IsFolder";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The IsUnplayed boolean.
|
||||||
|
/// </summary>
|
||||||
public const string IsUnplayed = "IsUnplayed";
|
public const string IsUnplayed = "IsUnplayed";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The IsPlayed boolean.
|
||||||
|
/// </summary>
|
||||||
public const string IsPlayed = "IsPlayed";
|
public const string IsPlayed = "IsPlayed";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The series sort.
|
||||||
|
/// </summary>
|
||||||
public const string SeriesSortName = "SeriesSortName";
|
public const string SeriesSortName = "SeriesSortName";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The video bitrate.
|
||||||
|
/// </summary>
|
||||||
public const string VideoBitRate = "VideoBitRate";
|
public const string VideoBitRate = "VideoBitRate";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The air time.
|
||||||
|
/// </summary>
|
||||||
public const string AirTime = "AirTime";
|
public const string AirTime = "AirTime";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The studio.
|
||||||
|
/// </summary>
|
||||||
public const string Studio = "Studio";
|
public const string Studio = "Studio";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The IsFavouriteOrLiked boolean.
|
||||||
|
/// </summary>
|
||||||
public const string IsFavoriteOrLiked = "IsFavoriteOrLiked";
|
public const string IsFavoriteOrLiked = "IsFavoriteOrLiked";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The last content added date.
|
||||||
|
/// </summary>
|
||||||
public const string DateLastContentAdded = "DateLastContentAdded";
|
public const string DateLastContentAdded = "DateLastContentAdded";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The series last played date.
|
||||||
|
/// </summary>
|
||||||
public const string SeriesDatePlayed = "SeriesDatePlayed";
|
public const string SeriesDatePlayed = "SeriesDatePlayed";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The parent index number.
|
||||||
|
/// </summary>
|
||||||
public const string ParentIndexNumber = "ParentIndexNumber";
|
public const string ParentIndexNumber = "ParentIndexNumber";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The index number.
|
||||||
|
/// </summary>
|
||||||
public const string IndexNumber = "IndexNumber";
|
public const string IndexNumber = "IndexNumber";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The similarity score.
|
||||||
|
/// </summary>
|
||||||
|
public const string SimilarityScore = "SimilarityScore";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,9 +14,9 @@ public class GeneralCommand
|
|||||||
}
|
}
|
||||||
|
|
||||||
[JsonConstructor]
|
[JsonConstructor]
|
||||||
public GeneralCommand(Dictionary<string, string> arguments)
|
public GeneralCommand(Dictionary<string, string>? arguments)
|
||||||
{
|
{
|
||||||
Arguments = arguments;
|
Arguments = arguments ?? new Dictionary<string, string>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public GeneralCommandType Name { get; set; }
|
public GeneralCommandType Name { get; set; }
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ namespace MediaBrowser.Model.Session
|
|||||||
|
|
||||||
// Video Constraints
|
// Video Constraints
|
||||||
VideoProfileNotSupported = 1 << 6,
|
VideoProfileNotSupported = 1 << 6,
|
||||||
|
VideoRangeTypeNotSupported = 1 << 24,
|
||||||
VideoLevelNotSupported = 1 << 7,
|
VideoLevelNotSupported = 1 << 7,
|
||||||
VideoResolutionNotSupported = 1 << 8,
|
VideoResolutionNotSupported = 1 << 8,
|
||||||
VideoBitDepthNotSupported = 1 << 9,
|
VideoBitDepthNotSupported = 1 << 9,
|
||||||
|
|||||||
@@ -655,8 +655,6 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
};
|
};
|
||||||
temp.Item.Path = item.Path;
|
temp.Item.Path = item.Path;
|
||||||
|
|
||||||
var userDataList = new List<UserItemData>();
|
|
||||||
|
|
||||||
// If replacing all metadata, run internet providers first
|
// If replacing all metadata, run internet providers first
|
||||||
if (options.ReplaceAllMetadata)
|
if (options.ReplaceAllMetadata)
|
||||||
{
|
{
|
||||||
@@ -670,7 +668,7 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
|
|
||||||
var hasLocalMetadata = false;
|
var hasLocalMetadata = false;
|
||||||
|
|
||||||
foreach (var provider in providers.OfType<ILocalMetadataProvider<TItemType>>().ToList())
|
foreach (var provider in providers.OfType<ILocalMetadataProvider<TItemType>>())
|
||||||
{
|
{
|
||||||
var providerName = provider.GetType().Name;
|
var providerName = provider.GetType().Name;
|
||||||
Logger.LogDebug("Running {Provider} for {Item}", providerName, logName);
|
Logger.LogDebug("Running {Provider} for {Item}", providerName, logName);
|
||||||
@@ -687,6 +685,11 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
if (!options.IsReplacingImage(remoteImage.Type))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
await ProviderManager.SaveImage(item, remoteImage.Url, remoteImage.Type, null, cancellationToken).ConfigureAwait(false);
|
await ProviderManager.SaveImage(item, remoteImage.Url, remoteImage.Type, null, cancellationToken).ConfigureAwait(false);
|
||||||
refreshResult.UpdateType |= ItemUpdateType.ImageUpdate;
|
refreshResult.UpdateType |= ItemUpdateType.ImageUpdate;
|
||||||
}
|
}
|
||||||
@@ -701,11 +704,6 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
refreshResult.UpdateType |= ItemUpdateType.ImageUpdate;
|
refreshResult.UpdateType |= ItemUpdateType.ImageUpdate;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (localItem.UserDataList != null)
|
|
||||||
{
|
|
||||||
userDataList.AddRange(localItem.UserDataList);
|
|
||||||
}
|
|
||||||
|
|
||||||
MergeData(localItem, temp, Array.Empty<MetadataField>(), !options.ReplaceAllMetadata, true);
|
MergeData(localItem, temp, Array.Empty<MetadataField>(), !options.ReplaceAllMetadata, true);
|
||||||
refreshResult.UpdateType |= ItemUpdateType.MetadataImport;
|
refreshResult.UpdateType |= ItemUpdateType.MetadataImport;
|
||||||
|
|
||||||
@@ -764,15 +762,11 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// var isUnidentified = failedProviderCount > 0 && successfulProviderCount == 0;
|
|
||||||
|
|
||||||
foreach (var provider in customProviders.Where(i => i is not IPreRefreshProvider))
|
foreach (var provider in customProviders.Where(i => i is not IPreRefreshProvider))
|
||||||
{
|
{
|
||||||
await RunCustomProvider(provider, item, logName, options, refreshResult, cancellationToken).ConfigureAwait(false);
|
await RunCustomProvider(provider, item, logName, options, refreshResult, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ImportUserData(item, userDataList, cancellationToken);
|
|
||||||
|
|
||||||
return refreshResult;
|
return refreshResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ using MediaBrowser.Controller.MediaEncoding;
|
|||||||
using MediaBrowser.Model.Dlna;
|
using MediaBrowser.Model.Dlna;
|
||||||
using MediaBrowser.Model.Globalization;
|
using MediaBrowser.Model.Globalization;
|
||||||
using MediaBrowser.Model.IO;
|
using MediaBrowser.Model.IO;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace MediaBrowser.Providers.MediaInfo
|
namespace MediaBrowser.Providers.MediaInfo
|
||||||
{
|
{
|
||||||
@@ -15,16 +16,19 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="AudioResolver"/> class for external audio file processing.
|
/// Initializes a new instance of the <see cref="AudioResolver"/> class for external audio file processing.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="logger">The logger.</param>
|
||||||
/// <param name="localizationManager">The localization manager.</param>
|
/// <param name="localizationManager">The localization manager.</param>
|
||||||
/// <param name="mediaEncoder">The media encoder.</param>
|
/// <param name="mediaEncoder">The media encoder.</param>
|
||||||
/// <param name="fileSystem">The file system.</param>
|
/// <param name="fileSystem">The file system.</param>
|
||||||
/// <param name="namingOptions">The <see cref="NamingOptions"/> object containing FileExtensions, MediaDefaultFlags, MediaForcedFlags and MediaFlagDelimiters.</param>
|
/// <param name="namingOptions">The <see cref="NamingOptions"/> object containing FileExtensions, MediaDefaultFlags, MediaForcedFlags and MediaFlagDelimiters.</param>
|
||||||
public AudioResolver(
|
public AudioResolver(
|
||||||
|
ILogger<AudioResolver> logger,
|
||||||
ILocalizationManager localizationManager,
|
ILocalizationManager localizationManager,
|
||||||
IMediaEncoder mediaEncoder,
|
IMediaEncoder mediaEncoder,
|
||||||
IFileSystem fileSystem,
|
IFileSystem fileSystem,
|
||||||
NamingOptions namingOptions)
|
NamingOptions namingOptions)
|
||||||
: base(
|
: base(
|
||||||
|
logger,
|
||||||
localizationManager,
|
localizationManager,
|
||||||
mediaEncoder,
|
mediaEncoder,
|
||||||
fileSystem,
|
fileSystem,
|
||||||
|
|||||||
@@ -47,7 +47,6 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||||||
private readonly Task<ItemUpdateType> _cachedTask = Task.FromResult(ItemUpdateType.None);
|
private readonly Task<ItemUpdateType> _cachedTask = Task.FromResult(ItemUpdateType.None);
|
||||||
|
|
||||||
public FFProbeProvider(
|
public FFProbeProvider(
|
||||||
ILogger<FFProbeProvider> logger,
|
|
||||||
IMediaSourceManager mediaSourceManager,
|
IMediaSourceManager mediaSourceManager,
|
||||||
IMediaEncoder mediaEncoder,
|
IMediaEncoder mediaEncoder,
|
||||||
IItemRepository itemRepo,
|
IItemRepository itemRepo,
|
||||||
@@ -59,11 +58,12 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||||||
IChapterManager chapterManager,
|
IChapterManager chapterManager,
|
||||||
ILibraryManager libraryManager,
|
ILibraryManager libraryManager,
|
||||||
IFileSystem fileSystem,
|
IFileSystem fileSystem,
|
||||||
|
ILoggerFactory loggerFactory,
|
||||||
NamingOptions namingOptions)
|
NamingOptions namingOptions)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = loggerFactory.CreateLogger<FFProbeProvider>();
|
||||||
_audioResolver = new AudioResolver(localization, mediaEncoder, fileSystem, namingOptions);
|
_audioResolver = new AudioResolver(loggerFactory.CreateLogger<AudioResolver>(), localization, mediaEncoder, fileSystem, namingOptions);
|
||||||
_subtitleResolver = new SubtitleResolver(localization, mediaEncoder, fileSystem, namingOptions);
|
_subtitleResolver = new SubtitleResolver(loggerFactory.CreateLogger<SubtitleResolver>(), localization, mediaEncoder, fileSystem, namingOptions);
|
||||||
_videoProber = new FFProbeVideoInfo(
|
_videoProber = new FFProbeVideoInfo(
|
||||||
_logger,
|
_logger,
|
||||||
mediaSourceManager,
|
mediaSourceManager,
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ using MediaBrowser.Model.Entities;
|
|||||||
using MediaBrowser.Model.Globalization;
|
using MediaBrowser.Model.Globalization;
|
||||||
using MediaBrowser.Model.IO;
|
using MediaBrowser.Model.IO;
|
||||||
using MediaBrowser.Model.MediaInfo;
|
using MediaBrowser.Model.MediaInfo;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace MediaBrowser.Providers.MediaInfo
|
namespace MediaBrowser.Providers.MediaInfo
|
||||||
{
|
{
|
||||||
@@ -33,6 +34,7 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private readonly IMediaEncoder _mediaEncoder;
|
private readonly IMediaEncoder _mediaEncoder;
|
||||||
|
|
||||||
|
private readonly ILogger _logger;
|
||||||
private readonly IFileSystem _fileSystem;
|
private readonly IFileSystem _fileSystem;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -48,18 +50,21 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="MediaInfoResolver"/> class.
|
/// Initializes a new instance of the <see cref="MediaInfoResolver"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="logger">The logger.</param>
|
||||||
/// <param name="localizationManager">The localization manager.</param>
|
/// <param name="localizationManager">The localization manager.</param>
|
||||||
/// <param name="mediaEncoder">The media encoder.</param>
|
/// <param name="mediaEncoder">The media encoder.</param>
|
||||||
/// <param name="fileSystem">The file system.</param>
|
/// <param name="fileSystem">The file system.</param>
|
||||||
/// <param name="namingOptions">The <see cref="NamingOptions"/> object containing FileExtensions, MediaDefaultFlags, MediaForcedFlags and MediaFlagDelimiters.</param>
|
/// <param name="namingOptions">The <see cref="NamingOptions"/> object containing FileExtensions, MediaDefaultFlags, MediaForcedFlags and MediaFlagDelimiters.</param>
|
||||||
/// <param name="type">The <see cref="DlnaProfileType"/> of the parsed file.</param>
|
/// <param name="type">The <see cref="DlnaProfileType"/> of the parsed file.</param>
|
||||||
protected MediaInfoResolver(
|
protected MediaInfoResolver(
|
||||||
|
ILogger logger,
|
||||||
ILocalizationManager localizationManager,
|
ILocalizationManager localizationManager,
|
||||||
IMediaEncoder mediaEncoder,
|
IMediaEncoder mediaEncoder,
|
||||||
IFileSystem fileSystem,
|
IFileSystem fileSystem,
|
||||||
NamingOptions namingOptions,
|
NamingOptions namingOptions,
|
||||||
DlnaProfileType type)
|
DlnaProfileType type)
|
||||||
{
|
{
|
||||||
|
_logger = logger;
|
||||||
_mediaEncoder = mediaEncoder;
|
_mediaEncoder = mediaEncoder;
|
||||||
_fileSystem = fileSystem;
|
_fileSystem = fileSystem;
|
||||||
_namingOptions = namingOptions;
|
_namingOptions = namingOptions;
|
||||||
@@ -101,34 +106,43 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||||||
{
|
{
|
||||||
if (!pathInfo.Path.AsSpan().EndsWith(".strm", StringComparison.OrdinalIgnoreCase))
|
if (!pathInfo.Path.AsSpan().EndsWith(".strm", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
var mediaInfo = await GetMediaInfo(pathInfo.Path, _type, cancellationToken).ConfigureAwait(false);
|
try
|
||||||
|
|
||||||
if (mediaInfo.MediaStreams.Count == 1)
|
|
||||||
{
|
{
|
||||||
MediaStream mediaStream = mediaInfo.MediaStreams[0];
|
var mediaInfo = await GetMediaInfo(pathInfo.Path, _type, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
if ((mediaStream.Type == MediaStreamType.Audio && _type == DlnaProfileType.Audio)
|
if (mediaInfo.MediaStreams.Count == 1)
|
||||||
|| (mediaStream.Type == MediaStreamType.Subtitle && _type == DlnaProfileType.Subtitle))
|
|
||||||
{
|
{
|
||||||
mediaStream.Index = startIndex++;
|
MediaStream mediaStream = mediaInfo.MediaStreams[0];
|
||||||
mediaStream.IsDefault = pathInfo.IsDefault || mediaStream.IsDefault;
|
|
||||||
mediaStream.IsForced = pathInfo.IsForced || mediaStream.IsForced;
|
|
||||||
|
|
||||||
mediaStreams.Add(MergeMetadata(mediaStream, pathInfo));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
foreach (MediaStream mediaStream in mediaInfo.MediaStreams)
|
|
||||||
{
|
|
||||||
if ((mediaStream.Type == MediaStreamType.Audio && _type == DlnaProfileType.Audio)
|
if ((mediaStream.Type == MediaStreamType.Audio && _type == DlnaProfileType.Audio)
|
||||||
|| (mediaStream.Type == MediaStreamType.Subtitle && _type == DlnaProfileType.Subtitle))
|
|| (mediaStream.Type == MediaStreamType.Subtitle && _type == DlnaProfileType.Subtitle))
|
||||||
{
|
{
|
||||||
mediaStream.Index = startIndex++;
|
mediaStream.Index = startIndex++;
|
||||||
|
mediaStream.IsDefault = pathInfo.IsDefault || mediaStream.IsDefault;
|
||||||
|
mediaStream.IsForced = pathInfo.IsForced || mediaStream.IsForced;
|
||||||
|
|
||||||
mediaStreams.Add(MergeMetadata(mediaStream, pathInfo));
|
mediaStreams.Add(MergeMetadata(mediaStream, pathInfo));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
foreach (MediaStream mediaStream in mediaInfo.MediaStreams)
|
||||||
|
{
|
||||||
|
if ((mediaStream.Type == MediaStreamType.Audio && _type == DlnaProfileType.Audio)
|
||||||
|
|| (mediaStream.Type == MediaStreamType.Subtitle && _type == DlnaProfileType.Subtitle))
|
||||||
|
{
|
||||||
|
mediaStream.Index = startIndex++;
|
||||||
|
|
||||||
|
mediaStreams.Add(MergeMetadata(mediaStream, pathInfo));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error getting external streams from {Path}", pathInfo.Path);
|
||||||
|
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ using MediaBrowser.Controller.MediaEncoding;
|
|||||||
using MediaBrowser.Model.Dlna;
|
using MediaBrowser.Model.Dlna;
|
||||||
using MediaBrowser.Model.Globalization;
|
using MediaBrowser.Model.Globalization;
|
||||||
using MediaBrowser.Model.IO;
|
using MediaBrowser.Model.IO;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace MediaBrowser.Providers.MediaInfo
|
namespace MediaBrowser.Providers.MediaInfo
|
||||||
{
|
{
|
||||||
@@ -15,16 +16,19 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="SubtitleResolver"/> class for external subtitle file processing.
|
/// Initializes a new instance of the <see cref="SubtitleResolver"/> class for external subtitle file processing.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="logger">The logger.</param>
|
||||||
/// <param name="localizationManager">The localization manager.</param>
|
/// <param name="localizationManager">The localization manager.</param>
|
||||||
/// <param name="mediaEncoder">The media encoder.</param>
|
/// <param name="mediaEncoder">The media encoder.</param>
|
||||||
/// <param name="fileSystem">The file system.</param>
|
/// <param name="fileSystem">The file system.</param>
|
||||||
/// <param name="namingOptions">The <see cref="NamingOptions"/> object containing FileExtensions, MediaDefaultFlags, MediaForcedFlags and MediaFlagDelimiters.</param>
|
/// <param name="namingOptions">The <see cref="NamingOptions"/> object containing FileExtensions, MediaDefaultFlags, MediaForcedFlags and MediaFlagDelimiters.</param>
|
||||||
public SubtitleResolver(
|
public SubtitleResolver(
|
||||||
|
ILogger<SubtitleResolver> logger,
|
||||||
ILocalizationManager localizationManager,
|
ILocalizationManager localizationManager,
|
||||||
IMediaEncoder mediaEncoder,
|
IMediaEncoder mediaEncoder,
|
||||||
IFileSystem fileSystem,
|
IFileSystem fileSystem,
|
||||||
NamingOptions namingOptions)
|
NamingOptions namingOptions)
|
||||||
: base(
|
: base(
|
||||||
|
logger,
|
||||||
localizationManager,
|
localizationManager,
|
||||||
mediaEncoder,
|
mediaEncoder,
|
||||||
fileSystem,
|
fileSystem,
|
||||||
|
|||||||
@@ -465,7 +465,8 @@ namespace MediaBrowser.Providers.Music
|
|||||||
ValidationType = ValidationType.None,
|
ValidationType = ValidationType.None,
|
||||||
CheckCharacters = false,
|
CheckCharacters = false,
|
||||||
IgnoreProcessingInstructions = true,
|
IgnoreProcessingInstructions = true,
|
||||||
IgnoreComments = true
|
IgnoreComments = true,
|
||||||
|
Async = true
|
||||||
};
|
};
|
||||||
|
|
||||||
using var reader = XmlReader.Create(oReader, settings);
|
using var reader = XmlReader.Create(oReader, settings);
|
||||||
|
|||||||
@@ -408,10 +408,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isEnglishRequested)
|
item.Overview = result.Plot;
|
||||||
{
|
|
||||||
item.Overview = result.Plot;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Plugin.Instance.Configuration.CastAndCrew)
|
if (!Plugin.Instance.Configuration.CastAndCrew)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ using System.Linq;
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using MediaBrowser.Controller.Configuration;
|
using MediaBrowser.Controller.Configuration;
|
||||||
|
using MediaBrowser.Controller.Dto;
|
||||||
using MediaBrowser.Controller.Entities.TV;
|
using MediaBrowser.Controller.Entities.TV;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
using MediaBrowser.Controller.Providers;
|
using MediaBrowser.Controller.Providers;
|
||||||
@@ -40,6 +41,7 @@ namespace MediaBrowser.Providers.TV
|
|||||||
{
|
{
|
||||||
await base.AfterMetadataRefresh(item, refreshOptions, cancellationToken).ConfigureAwait(false);
|
await base.AfterMetadataRefresh(item, refreshOptions, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
RemoveObsoleteEpisodes(item);
|
||||||
RemoveObsoleteSeasons(item);
|
RemoveObsoleteSeasons(item);
|
||||||
await FillInMissingSeasonsAsync(item, cancellationToken).ConfigureAwait(false);
|
await FillInMissingSeasonsAsync(item, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
@@ -121,6 +123,61 @@ namespace MediaBrowser.Providers.TV
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void RemoveObsoleteEpisodes(Series series)
|
||||||
|
{
|
||||||
|
var episodes = series.GetEpisodes(null, new DtoOptions()).OfType<Episode>().ToList();
|
||||||
|
var numberOfEpisodes = episodes.Count;
|
||||||
|
// TODO: O(n^2), but can it be done faster without overcomplicating it?
|
||||||
|
for (var i = 0; i < numberOfEpisodes; i++)
|
||||||
|
{
|
||||||
|
var currentEpisode = episodes[i];
|
||||||
|
// The outer loop only examines virtual episodes
|
||||||
|
if (!currentEpisode.IsVirtualItem)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Virtual episodes without an episode number are practically orphaned and should be deleted
|
||||||
|
if (!currentEpisode.IndexNumber.HasValue)
|
||||||
|
{
|
||||||
|
DeleteEpisode(currentEpisode);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var j = i + 1; j < numberOfEpisodes; j++)
|
||||||
|
{
|
||||||
|
var comparisonEpisode = episodes[j];
|
||||||
|
// The inner loop is only for "physical" episodes
|
||||||
|
if (comparisonEpisode.IsVirtualItem
|
||||||
|
|| currentEpisode.ParentIndexNumber != comparisonEpisode.ParentIndexNumber
|
||||||
|
|| !comparisonEpisode.ContainsEpisodeNumber(currentEpisode.IndexNumber.Value))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
DeleteEpisode(currentEpisode);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DeleteEpisode(Episode episode)
|
||||||
|
{
|
||||||
|
Logger.LogInformation(
|
||||||
|
"Removing virtual episode S{SeasonNumber}E{EpisodeNumber} in series {SeriesName}",
|
||||||
|
episode.ParentIndexNumber,
|
||||||
|
episode.IndexNumber,
|
||||||
|
episode.SeriesName);
|
||||||
|
|
||||||
|
LibraryManager.DeleteItem(
|
||||||
|
episode,
|
||||||
|
new DeleteOptions
|
||||||
|
{
|
||||||
|
DeleteFileLocation = true
|
||||||
|
},
|
||||||
|
false);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates seasons for all episodes that aren't in a season folder.
|
/// Creates seasons for all episodes that aren't in a season folder.
|
||||||
/// If no season number can be determined, a dummy season will be created.
|
/// If no season number can be determined, a dummy season will be created.
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user