Compare commits
110 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| aefb9b2cff | |||
| abc51e89a3 | |||
| c929229030 | |||
| f625665cb1 | |||
| 79f3ce5325 | |||
| 6938fac73e | |||
| cafa0a1e10 | |||
| 24242a510d | |||
| 2d95245405 | |||
| 784f6c5eff | |||
| 3ee7194706 | |||
| a4ab5e5a14 | |||
| 1fff472569 | |||
| 91ca81eca7 | |||
| 78d9fa72e0 | |||
| 9eb2044eae | |||
| 7ea09a8637 | |||
| 069b518ab0 | |||
| ae6a7acf14 | |||
| 64e0f9099a | |||
| 4f94d23011 | |||
| fcdef875a2 | |||
| 2da4a2d753 | |||
| 9ae68057a7 | |||
| 767a5e6193 | |||
| 346f36bc09 | |||
| 1daf761aec | |||
| a70a09fdb3 | |||
| 888838adbc | |||
| 72911501d3 | |||
| 1af7b6d348 | |||
| 317d7a9f4f | |||
| 9d9c6fe5eb | |||
| a5b771861f | |||
| 3f539472f3 | |||
| 7f43521b64 | |||
| 99006c370f | |||
| e3f9f0a7f3 | |||
| d1fbdcee34 | |||
| 21e398ba0c | |||
| 8544e7fc72 | |||
| 117d2082aa | |||
| 03082e90f9 | |||
| 88026518b1 | |||
| 5f1fb26382 | |||
| 070d04c1b2 | |||
| 8aa4e2e320 | |||
| 49bb5a6442 | |||
| 9e869b4541 | |||
| 710e877762 | |||
| f536e08e14 | |||
| 4eecfee29f | |||
| 731874429c | |||
| e6c6441abf | |||
| 4d89a095ed | |||
| 1136a36eed | |||
| e8514de33b | |||
| 722cdcce5e | |||
| bfe0fdbcdc | |||
| 0b2a59e963 | |||
| 6329de4fc3 | |||
| 644df3585b | |||
| 3766a88bea | |||
| f333ef74b3 | |||
| 0394965753 | |||
| 53a45c6033 | |||
| adfe52f55a | |||
| cf78aefbb7 | |||
| c693da94ce | |||
| 1a7c2299c6 | |||
| 9c7d735a96 | |||
| 344cc8b97b | |||
| cc9c000412 | |||
| 5c6317f68d | |||
| 80940c0c57 | |||
| 8aa41d5904 | |||
| cea0c95942 | |||
| 4e28f4fe03 | |||
| f0e9b2fb96 | |||
| b9881b8bdf | |||
| b31f1696f2 | |||
| 86160cd99c | |||
| 230eacf15e | |||
| 0ecaa98ee7 | |||
| 45c4bedbc6 | |||
| 2c4c1d054d | |||
| f97f38585b | |||
| a2a0cbf7ab | |||
| eb5f8d49dd | |||
| 6f7ce439d3 | |||
| 03ea566271 | |||
| 2a96b8b34b | |||
| ff4f3b0441 | |||
| d49bb1d86d | |||
| cf6aa12627 | |||
| cd4519c15f | |||
| 8e248c7c05 | |||
| 65f722f23c | |||
| 5df03b9010 | |||
| e7ac3e3929 | |||
| 9464f9e622 | |||
| 746280af0b | |||
| 9bc6e8a306 | |||
| b0105179eb | |||
| ef13a18450 | |||
| b3e563385c | |||
| 5e45403cb1 | |||
| 23de7e517e | |||
| ba28e3041d | |||
| 34f3ed0a4d |
@@ -9,8 +9,8 @@
|
|||||||
<PackageVersion Include="AutoFixture.Xunit2" Version="4.18.1" />
|
<PackageVersion Include="AutoFixture.Xunit2" Version="4.18.1" />
|
||||||
<PackageVersion Include="AutoFixture" Version="4.18.1" />
|
<PackageVersion Include="AutoFixture" Version="4.18.1" />
|
||||||
<PackageVersion Include="BDInfo" Version="0.8.0" />
|
<PackageVersion Include="BDInfo" Version="0.8.0" />
|
||||||
<PackageVersion Include="BlurHashSharp.SkiaSharp" Version="1.3.3" />
|
<PackageVersion Include="BlurHashSharp.SkiaSharp" Version="1.3.4" />
|
||||||
<PackageVersion Include="BlurHashSharp" Version="1.3.3" />
|
<PackageVersion Include="BlurHashSharp" Version="1.3.4" />
|
||||||
<PackageVersion Include="CommandLineParser" Version="2.9.1" />
|
<PackageVersion Include="CommandLineParser" Version="2.9.1" />
|
||||||
<PackageVersion Include="coverlet.collector" Version="6.0.2" />
|
<PackageVersion Include="coverlet.collector" Version="6.0.2" />
|
||||||
<PackageVersion Include="Diacritics" Version="3.3.29" />
|
<PackageVersion Include="Diacritics" Version="3.3.29" />
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
<PackageVersion Include="IDisposableAnalyzers" Version="4.0.8" />
|
<PackageVersion Include="IDisposableAnalyzers" Version="4.0.8" />
|
||||||
<PackageVersion Include="Jellyfin.XmlTv" Version="10.8.0" />
|
<PackageVersion Include="Jellyfin.XmlTv" Version="10.8.0" />
|
||||||
<PackageVersion Include="libse" Version="4.0.8" />
|
<PackageVersion Include="libse" Version="4.0.8" />
|
||||||
<PackageVersion Include="LrcParser" Version="2024.0728.2" />
|
<PackageVersion Include="LrcParser" Version="2025.228.1" />
|
||||||
<PackageVersion Include="MetaBrainz.MusicBrainz" Version="6.1.0" />
|
<PackageVersion Include="MetaBrainz.MusicBrainz" Version="6.1.0" />
|
||||||
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="8.0.11" />
|
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="8.0.11" />
|
||||||
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.11" />
|
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.11" />
|
||||||
@@ -51,7 +51,7 @@
|
|||||||
<PackageVersion Include="MimeTypes" Version="2.4.0" />
|
<PackageVersion Include="MimeTypes" Version="2.4.0" />
|
||||||
<PackageVersion Include="Mono.Nat" Version="3.0.4" />
|
<PackageVersion Include="Mono.Nat" Version="3.0.4" />
|
||||||
<PackageVersion Include="Moq" Version="4.18.4" />
|
<PackageVersion Include="Moq" Version="4.18.4" />
|
||||||
<PackageVersion Include="NEbml" Version="0.11.0" />
|
<PackageVersion Include="NEbml" Version="0.12.0" />
|
||||||
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
|
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
|
||||||
<PackageVersion Include="PlaylistsNET" Version="1.4.1" />
|
<PackageVersion Include="PlaylistsNET" Version="1.4.1" />
|
||||||
<PackageVersion Include="prometheus-net.AspNetCore" Version="8.2.1" />
|
<PackageVersion Include="prometheus-net.AspNetCore" Version="8.2.1" />
|
||||||
@@ -80,7 +80,7 @@
|
|||||||
<PackageVersion Include="System.Text.Json" Version="8.0.5" />
|
<PackageVersion Include="System.Text.Json" Version="8.0.5" />
|
||||||
<PackageVersion Include="System.Threading.Tasks.Dataflow" Version="8.0.1" />
|
<PackageVersion Include="System.Threading.Tasks.Dataflow" Version="8.0.1" />
|
||||||
<PackageVersion Include="TagLibSharp" Version="2.3.0" />
|
<PackageVersion Include="TagLibSharp" Version="2.3.0" />
|
||||||
<PackageVersion Include="z440.atl.core" Version="6.8.0" />
|
<PackageVersion Include="z440.atl.core" Version="6.20.0" />
|
||||||
<PackageVersion Include="TMDbLib" Version="2.2.0" />
|
<PackageVersion Include="TMDbLib" Version="2.2.0" />
|
||||||
<PackageVersion Include="UTF.Unknown" Version="2.5.1" />
|
<PackageVersion Include="UTF.Unknown" Version="2.5.1" />
|
||||||
<PackageVersion Include="Xunit.Priority" Version="1.1.6" />
|
<PackageVersion Include="Xunit.Priority" Version="1.1.6" />
|
||||||
|
|||||||
@@ -36,7 +36,7 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Authors>Jellyfin Contributors</Authors>
|
<Authors>Jellyfin Contributors</Authors>
|
||||||
<PackageId>Jellyfin.Naming</PackageId>
|
<PackageId>Jellyfin.Naming</PackageId>
|
||||||
<VersionPrefix>10.10.2</VersionPrefix>
|
<VersionPrefix>10.10.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>
|
||||||
|
|||||||
@@ -276,6 +276,13 @@ namespace Emby.Server.Implementations.IO
|
|||||||
{
|
{
|
||||||
_logger.LogError(ex, "Reading the file at {Path} failed due to a permissions exception.", fileInfo.FullName);
|
_logger.LogError(ex, "Reading the file at {Path} failed due to a permissions exception.", fileInfo.FullName);
|
||||||
}
|
}
|
||||||
|
catch (IOException ex)
|
||||||
|
{
|
||||||
|
// IOException generally means the file is not accessible due to filesystem issues
|
||||||
|
// Catch this exception and mark the file as not exist to ignore it
|
||||||
|
_logger.LogError(ex, "Reading the file at {Path} failed due to an IO Exception. Marking the file as not existing", fileInfo.FullName);
|
||||||
|
result.Exists = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -590,6 +597,9 @@ namespace Emby.Server.Implementations.IO
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public virtual IEnumerable<FileSystemMetadata> GetFileSystemEntries(string path, bool recursive = false)
|
public virtual IEnumerable<FileSystemMetadata> GetFileSystemEntries(string path, bool recursive = false)
|
||||||
{
|
{
|
||||||
|
// Note: any of unhandled exceptions thrown by this method may cause the caller to believe the whole path is not accessible.
|
||||||
|
// But what causing the exception may be a single file under that path. This could lead to unexpected behavior.
|
||||||
|
// For example, the scanner will remove everything in that path due to unhandled errors.
|
||||||
var directoryInfo = new DirectoryInfo(path);
|
var directoryInfo = new DirectoryInfo(path);
|
||||||
var enumerationOptions = GetEnumerationOptions(recursive);
|
var enumerationOptions = GetEnumerationOptions(recursive);
|
||||||
|
|
||||||
|
|||||||
@@ -454,6 +454,7 @@ namespace Emby.Server.Implementations.Library
|
|||||||
foreach (var child in children)
|
foreach (var child in children)
|
||||||
{
|
{
|
||||||
_itemRepository.DeleteItem(child.Id);
|
_itemRepository.DeleteItem(child.Id);
|
||||||
|
_cache.TryRemove(child.Id, out _);
|
||||||
}
|
}
|
||||||
|
|
||||||
_cache.TryRemove(item.Id, out _);
|
_cache.TryRemove(item.Id, out _);
|
||||||
@@ -2626,15 +2627,6 @@ namespace Emby.Server.Implementations.Library
|
|||||||
{
|
{
|
||||||
episode.ParentIndexNumber = season.IndexNumber;
|
episode.ParentIndexNumber = season.IndexNumber;
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
|
||||||
/*
|
|
||||||
Anime series don't generally have a season in their file name, however,
|
|
||||||
TVDb needs a season to correctly get the metadata.
|
|
||||||
Hence, a null season needs to be filled with something. */
|
|
||||||
// FIXME perhaps this would be better for TVDb parser to ask for season 1 if no season is specified
|
|
||||||
episode.ParentIndexNumber = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (episode.ParentIndexNumber.HasValue)
|
if (episode.ParentIndexNumber.HasValue)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
|
|||||||
{
|
{
|
||||||
if (args.IsDirectory)
|
if (args.IsDirectory)
|
||||||
{
|
{
|
||||||
// It's a boxset if the path is a directory with [playlist] in its name
|
// It's a playlist if the path is a directory with [playlist] in its name
|
||||||
var filename = Path.GetFileName(Path.TrimEndingDirectorySeparator(args.Path));
|
var filename = Path.GetFileName(Path.TrimEndingDirectorySeparator(args.Path));
|
||||||
if (string.IsNullOrEmpty(filename))
|
if (string.IsNullOrEmpty(filename))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -286,8 +286,10 @@ namespace Emby.Server.Implementations.Localization
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Fairly common for some users to have "Rated R" in their rating field
|
// Fairly common for some users to have "Rated R" in their rating field
|
||||||
rating = rating.Replace("Rated :", string.Empty, StringComparison.OrdinalIgnoreCase);
|
rating = rating.Replace("Rated :", string.Empty, StringComparison.OrdinalIgnoreCase)
|
||||||
rating = rating.Replace("Rated ", string.Empty, StringComparison.OrdinalIgnoreCase);
|
.Replace("Rated:", string.Empty, StringComparison.OrdinalIgnoreCase)
|
||||||
|
.Replace("Rated ", string.Empty, StringComparison.OrdinalIgnoreCase)
|
||||||
|
.Trim();
|
||||||
|
|
||||||
// Use rating system matching the language
|
// Use rating system matching the language
|
||||||
if (!string.IsNullOrEmpty(countryCode))
|
if (!string.IsNullOrEmpty(countryCode))
|
||||||
|
|||||||
@@ -1,8 +1,14 @@
|
|||||||
Livre,0
|
Livre,0
|
||||||
L,0
|
L,0
|
||||||
ER,9
|
AL,0
|
||||||
|
ER,10
|
||||||
10,10
|
10,10
|
||||||
|
A10,10
|
||||||
12,12
|
12,12
|
||||||
|
A12,12
|
||||||
14,14
|
14,14
|
||||||
|
A14,14
|
||||||
16,16
|
16,16
|
||||||
|
A16,16
|
||||||
18,18
|
18,18
|
||||||
|
A18,18
|
||||||
|
|||||||
|
@@ -6,8 +6,6 @@ TV-Y7,7
|
|||||||
TV-Y7-FV,7
|
TV-Y7-FV,7
|
||||||
PG,9
|
PG,9
|
||||||
TV-PG,9
|
TV-PG,9
|
||||||
PG-13,13
|
|
||||||
13+,13
|
|
||||||
TV-14,14
|
TV-14,14
|
||||||
14A,14
|
14A,14
|
||||||
16+,16
|
16+,16
|
||||||
|
|||||||
|
@@ -1,7 +1,7 @@
|
|||||||
A,0
|
A,0
|
||||||
A/fig,0
|
A/fig,0
|
||||||
A/i,0
|
A/i,0
|
||||||
A/fig/i,0
|
A/i/fig,0
|
||||||
APTA,0
|
APTA,0
|
||||||
ERI,0
|
ERI,0
|
||||||
TP,0
|
TP,0
|
||||||
|
|||||||
|
@@ -6,10 +6,11 @@ U,0
|
|||||||
6+,6
|
6+,6
|
||||||
7+,7
|
7+,7
|
||||||
PG,8
|
PG,8
|
||||||
9+,9
|
9,9
|
||||||
12,12
|
12,12
|
||||||
12+,12
|
12+,12
|
||||||
12A,12
|
12A,12
|
||||||
|
12PG,12
|
||||||
Teen,13
|
Teen,13
|
||||||
13+,13
|
13+,13
|
||||||
14+,14
|
14+,14
|
||||||
|
|||||||
|
@@ -4,6 +4,7 @@ PG,12
|
|||||||
12A,12
|
12A,12
|
||||||
12PG,12
|
12PG,12
|
||||||
15,15
|
15,15
|
||||||
|
15PG,15
|
||||||
15A,15
|
15A,15
|
||||||
16,16
|
16,16
|
||||||
18,18
|
18,18
|
||||||
|
|||||||
|
@@ -6,4 +6,5 @@ A,0
|
|||||||
12,12
|
12,12
|
||||||
15,15
|
15,15
|
||||||
18,18
|
18,18
|
||||||
|
C,18
|
||||||
Not approved,1001
|
Not approved,1001
|
||||||
|
|||||||
|
@@ -10,6 +10,7 @@ R16,16
|
|||||||
RP16,16
|
RP16,16
|
||||||
GA,18
|
GA,18
|
||||||
R18,18
|
R18,18
|
||||||
|
RP18,18
|
||||||
MA,1000
|
MA,1000
|
||||||
R,1001
|
R,1001
|
||||||
Objectionable,1001
|
Objectionable,1001
|
||||||
|
|||||||
|
@@ -5,23 +5,23 @@ TV-Y,0
|
|||||||
TV-Y7,7
|
TV-Y7,7
|
||||||
TV-Y7-FV,7
|
TV-Y7-FV,7
|
||||||
PG,10
|
PG,10
|
||||||
|
TV-PG,10
|
||||||
|
TV-PG-D,10
|
||||||
|
TV-PG-L,10
|
||||||
|
TV-PG-S,10
|
||||||
|
TV-PG-V,10
|
||||||
|
TV-PG-DL,10
|
||||||
|
TV-PG-DS,10
|
||||||
|
TV-PG-DV,10
|
||||||
|
TV-PG-LS,10
|
||||||
|
TV-PG-LV,10
|
||||||
|
TV-PG-SV,10
|
||||||
|
TV-PG-DLS,10
|
||||||
|
TV-PG-DLV,10
|
||||||
|
TV-PG-DSV,10
|
||||||
|
TV-PG-LSV,10
|
||||||
|
TV-PG-DLSV,10
|
||||||
PG-13,13
|
PG-13,13
|
||||||
TV-PG,13
|
|
||||||
TV-PG-D,13
|
|
||||||
TV-PG-L,13
|
|
||||||
TV-PG-S,13
|
|
||||||
TV-PG-V,13
|
|
||||||
TV-PG-DL,13
|
|
||||||
TV-PG-DS,13
|
|
||||||
TV-PG-DV,13
|
|
||||||
TV-PG-LS,13
|
|
||||||
TV-PG-LV,13
|
|
||||||
TV-PG-SV,13
|
|
||||||
TV-PG-DLS,13
|
|
||||||
TV-PG-DLV,13
|
|
||||||
TV-PG-DSV,13
|
|
||||||
TV-PG-LSV,13
|
|
||||||
TV-PG-DLSV,13
|
|
||||||
TV-14,14
|
TV-14,14
|
||||||
TV-14-D,14
|
TV-14-D,14
|
||||||
TV-14-L,14
|
TV-14-L,14
|
||||||
@@ -48,3 +48,5 @@ TV-MA-LS,17
|
|||||||
TV-MA-LV,17
|
TV-MA-LV,17
|
||||||
TV-MA-SV,17
|
TV-MA-SV,17
|
||||||
TV-MA-LSV,17
|
TV-MA-LSV,17
|
||||||
|
TV-X,18
|
||||||
|
TV-AO,18
|
||||||
|
|||||||
|
@@ -116,6 +116,7 @@ public partial class AudioNormalizationTask : IScheduledTask
|
|||||||
{
|
{
|
||||||
a.LUFS = await CalculateLUFSAsync(
|
a.LUFS = await CalculateLUFSAsync(
|
||||||
string.Format(CultureInfo.InvariantCulture, "-f concat -safe 0 -i \"{0}\"", tempFile),
|
string.Format(CultureInfo.InvariantCulture, "-f concat -safe 0 -i \"{0}\"", tempFile),
|
||||||
|
OperatingSystem.IsWindows(), // Wait for process to exit on Windows before we try deleting the concat file
|
||||||
cancellationToken).ConfigureAwait(false);
|
cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
@@ -142,7 +143,10 @@ public partial class AudioNormalizationTask : IScheduledTask
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
t.LUFS = await CalculateLUFSAsync(string.Format(CultureInfo.InvariantCulture, "-i \"{0}\"", t.Path.Replace("\"", "\\\"", StringComparison.Ordinal)), cancellationToken).ConfigureAwait(false);
|
t.LUFS = await CalculateLUFSAsync(
|
||||||
|
string.Format(CultureInfo.InvariantCulture, "-i \"{0}\"", t.Path.Replace("\"", "\\\"", StringComparison.Ordinal)),
|
||||||
|
false,
|
||||||
|
cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
_itemRepository.SaveItems(tracks, cancellationToken);
|
_itemRepository.SaveItems(tracks, cancellationToken);
|
||||||
@@ -162,7 +166,7 @@ public partial class AudioNormalizationTask : IScheduledTask
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<float?> CalculateLUFSAsync(string inputArgs, CancellationToken cancellationToken)
|
private async Task<float?> CalculateLUFSAsync(string inputArgs, bool waitForExit, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var args = $"-hide_banner {inputArgs} -af ebur128=framelog=verbose -f null -";
|
var args = $"-hide_banner {inputArgs} -af ebur128=framelog=verbose -f null -";
|
||||||
|
|
||||||
@@ -189,18 +193,28 @@ public partial class AudioNormalizationTask : IScheduledTask
|
|||||||
}
|
}
|
||||||
|
|
||||||
using var reader = process.StandardError;
|
using var reader = process.StandardError;
|
||||||
|
float? lufs = null;
|
||||||
await foreach (var line in reader.ReadAllLinesAsync(cancellationToken))
|
await foreach (var line in reader.ReadAllLinesAsync(cancellationToken))
|
||||||
{
|
{
|
||||||
Match match = LUFSRegex().Match(line);
|
Match match = LUFSRegex().Match(line);
|
||||||
|
|
||||||
if (match.Success)
|
if (match.Success)
|
||||||
{
|
{
|
||||||
return float.Parse(match.Groups[1].ValueSpan, CultureInfo.InvariantCulture.NumberFormat);
|
lufs = float.Parse(match.Groups[1].ValueSpan, CultureInfo.InvariantCulture.NumberFormat);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.LogError("Failed to find LUFS value in output");
|
if (lufs is null)
|
||||||
return null;
|
{
|
||||||
|
_logger.LogError("Failed to find LUFS value in output");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (waitForExit)
|
||||||
|
{
|
||||||
|
await process.WaitForExitAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return lufs;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ namespace Emby.Server.Implementations.Session
|
|||||||
private readonly SessionInfo _session;
|
private readonly SessionInfo _session;
|
||||||
|
|
||||||
private readonly List<IWebSocketConnection> _sockets;
|
private readonly List<IWebSocketConnection> _sockets;
|
||||||
|
private readonly ReaderWriterLockSlim _socketsLock;
|
||||||
private bool _disposed = false;
|
private bool _disposed = false;
|
||||||
|
|
||||||
public WebSocketController(
|
public WebSocketController(
|
||||||
@@ -31,10 +32,26 @@ namespace Emby.Server.Implementations.Session
|
|||||||
_logger = logger;
|
_logger = logger;
|
||||||
_session = session;
|
_session = session;
|
||||||
_sessionManager = sessionManager;
|
_sessionManager = sessionManager;
|
||||||
_sockets = new List<IWebSocketConnection>();
|
_sockets = new();
|
||||||
|
_socketsLock = new();
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool HasOpenSockets => GetActiveSockets().Any();
|
private bool HasOpenSockets
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_socketsLock.EnterReadLock();
|
||||||
|
return _sockets.Any(i => i.State == WebSocketState.Open);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_socketsLock.ExitReadLock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public bool SupportsMediaControl => HasOpenSockets;
|
public bool SupportsMediaControl => HasOpenSockets;
|
||||||
@@ -42,23 +59,38 @@ namespace Emby.Server.Implementations.Session
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public bool IsSessionActive => HasOpenSockets;
|
public bool IsSessionActive => HasOpenSockets;
|
||||||
|
|
||||||
private IEnumerable<IWebSocketConnection> GetActiveSockets()
|
|
||||||
=> _sockets.Where(i => i.State == WebSocketState.Open);
|
|
||||||
|
|
||||||
public void AddWebSocket(IWebSocketConnection connection)
|
public void AddWebSocket(IWebSocketConnection connection)
|
||||||
{
|
{
|
||||||
_logger.LogDebug("Adding websocket to session {Session}", _session.Id);
|
_logger.LogDebug("Adding websocket to session {Session}", _session.Id);
|
||||||
_sockets.Add(connection);
|
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||||
|
try
|
||||||
connection.Closed += OnConnectionClosed;
|
{
|
||||||
|
_socketsLock.EnterWriteLock();
|
||||||
|
_sockets.Add(connection);
|
||||||
|
connection.Closed += OnConnectionClosed;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_socketsLock.ExitWriteLock();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void OnConnectionClosed(object? sender, EventArgs e)
|
private async void OnConnectionClosed(object? sender, EventArgs e)
|
||||||
{
|
{
|
||||||
var connection = sender as IWebSocketConnection ?? throw new ArgumentException($"{nameof(sender)} is not of type {nameof(IWebSocketConnection)}", nameof(sender));
|
var connection = sender as IWebSocketConnection ?? throw new ArgumentException($"{nameof(sender)} is not of type {nameof(IWebSocketConnection)}", nameof(sender));
|
||||||
_logger.LogDebug("Removing websocket from session {Session}", _session.Id);
|
_logger.LogDebug("Removing websocket from session {Session}", _session.Id);
|
||||||
_sockets.Remove(connection);
|
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||||
connection.Closed -= OnConnectionClosed;
|
try
|
||||||
|
{
|
||||||
|
_socketsLock.EnterWriteLock();
|
||||||
|
_sockets.Remove(connection);
|
||||||
|
connection.Closed -= OnConnectionClosed;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_socketsLock.ExitWriteLock();
|
||||||
|
}
|
||||||
|
|
||||||
await _sessionManager.CloseIfNeededAsync(_session).ConfigureAwait(false);
|
await _sessionManager.CloseIfNeededAsync(_session).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,7 +101,17 @@ namespace Emby.Server.Implementations.Session
|
|||||||
T data,
|
T data,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var socket = GetActiveSockets().MaxBy(i => i.LastActivityDate);
|
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||||
|
IWebSocketConnection? socket;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_socketsLock.EnterReadLock();
|
||||||
|
socket = _sockets.Where(i => i.State == WebSocketState.Open).MaxBy(i => i.LastActivityDate);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_socketsLock.ExitReadLock();
|
||||||
|
}
|
||||||
|
|
||||||
if (socket is null)
|
if (socket is null)
|
||||||
{
|
{
|
||||||
@@ -94,12 +136,23 @@ namespace Emby.Server.Implementations.Session
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var socket in _sockets)
|
try
|
||||||
{
|
{
|
||||||
socket.Closed -= OnConnectionClosed;
|
_socketsLock.EnterWriteLock();
|
||||||
socket.Dispose();
|
foreach (var socket in _sockets)
|
||||||
|
{
|
||||||
|
socket.Closed -= OnConnectionClosed;
|
||||||
|
socket.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
_sockets.Clear();
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_socketsLock.ExitWriteLock();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_socketsLock.Dispose();
|
||||||
_disposed = true;
|
_disposed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,12 +163,23 @@ namespace Emby.Server.Implementations.Session
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var socket in _sockets)
|
try
|
||||||
{
|
{
|
||||||
socket.Closed -= OnConnectionClosed;
|
_socketsLock.EnterWriteLock();
|
||||||
await socket.DisposeAsync().ConfigureAwait(false);
|
foreach (var socket in _sockets)
|
||||||
|
{
|
||||||
|
socket.Closed -= OnConnectionClosed;
|
||||||
|
await socket.DisposeAsync().ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
_sockets.Clear();
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_socketsLock.ExitWriteLock();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_socketsLock.Dispose();
|
||||||
_disposed = true;
|
_disposed = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -92,18 +92,18 @@ public class AudioController : BaseJellyfinApiController
|
|||||||
[ProducesAudioFile]
|
[ProducesAudioFile]
|
||||||
public async Task<ActionResult> GetAudioStream(
|
public async Task<ActionResult> GetAudioStream(
|
||||||
[FromRoute, Required] Guid itemId,
|
[FromRoute, Required] Guid itemId,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? container,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? container,
|
||||||
[FromQuery] bool? @static,
|
[FromQuery] bool? @static,
|
||||||
[FromQuery] string? @params,
|
[FromQuery] string? @params,
|
||||||
[FromQuery] string? tag,
|
[FromQuery] string? tag,
|
||||||
[FromQuery, ParameterObsolete] string? deviceProfileId,
|
[FromQuery, ParameterObsolete] string? deviceProfileId,
|
||||||
[FromQuery] string? playSessionId,
|
[FromQuery] string? playSessionId,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer,
|
||||||
[FromQuery] int? segmentLength,
|
[FromQuery] int? segmentLength,
|
||||||
[FromQuery] int? minSegments,
|
[FromQuery] int? minSegments,
|
||||||
[FromQuery] string? mediaSourceId,
|
[FromQuery] string? mediaSourceId,
|
||||||
[FromQuery] string? deviceId,
|
[FromQuery] string? deviceId,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec,
|
||||||
[FromQuery] bool? enableAutoStreamCopy,
|
[FromQuery] bool? enableAutoStreamCopy,
|
||||||
[FromQuery] bool? allowVideoStreamCopy,
|
[FromQuery] bool? allowVideoStreamCopy,
|
||||||
[FromQuery] bool? allowAudioStreamCopy,
|
[FromQuery] bool? allowAudioStreamCopy,
|
||||||
@@ -114,7 +114,7 @@ public class AudioController : BaseJellyfinApiController
|
|||||||
[FromQuery] int? audioChannels,
|
[FromQuery] int? audioChannels,
|
||||||
[FromQuery] int? maxAudioChannels,
|
[FromQuery] int? maxAudioChannels,
|
||||||
[FromQuery] string? profile,
|
[FromQuery] string? profile,
|
||||||
[FromQuery] string? level,
|
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level,
|
||||||
[FromQuery] float? framerate,
|
[FromQuery] float? framerate,
|
||||||
[FromQuery] float? maxFramerate,
|
[FromQuery] float? maxFramerate,
|
||||||
[FromQuery] bool? copyTimestamps,
|
[FromQuery] bool? copyTimestamps,
|
||||||
@@ -133,8 +133,8 @@ public class AudioController : BaseJellyfinApiController
|
|||||||
[FromQuery] int? cpuCoreLimit,
|
[FromQuery] int? cpuCoreLimit,
|
||||||
[FromQuery] string? liveStreamId,
|
[FromQuery] string? liveStreamId,
|
||||||
[FromQuery] bool? enableMpegtsM2TsMode,
|
[FromQuery] bool? enableMpegtsM2TsMode,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec,
|
||||||
[FromQuery] string? transcodeReasons,
|
[FromQuery] string? transcodeReasons,
|
||||||
[FromQuery] int? audioStreamIndex,
|
[FromQuery] int? audioStreamIndex,
|
||||||
[FromQuery] int? videoStreamIndex,
|
[FromQuery] int? videoStreamIndex,
|
||||||
@@ -259,18 +259,18 @@ public class AudioController : BaseJellyfinApiController
|
|||||||
[ProducesAudioFile]
|
[ProducesAudioFile]
|
||||||
public async Task<ActionResult> GetAudioStreamByContainer(
|
public async Task<ActionResult> GetAudioStreamByContainer(
|
||||||
[FromRoute, Required] Guid itemId,
|
[FromRoute, Required] Guid itemId,
|
||||||
[FromRoute, Required] string container,
|
[FromRoute, Required] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string container,
|
||||||
[FromQuery] bool? @static,
|
[FromQuery] bool? @static,
|
||||||
[FromQuery] string? @params,
|
[FromQuery] string? @params,
|
||||||
[FromQuery] string? tag,
|
[FromQuery] string? tag,
|
||||||
[FromQuery, ParameterObsolete] string? deviceProfileId,
|
[FromQuery, ParameterObsolete] string? deviceProfileId,
|
||||||
[FromQuery] string? playSessionId,
|
[FromQuery] string? playSessionId,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer,
|
||||||
[FromQuery] int? segmentLength,
|
[FromQuery] int? segmentLength,
|
||||||
[FromQuery] int? minSegments,
|
[FromQuery] int? minSegments,
|
||||||
[FromQuery] string? mediaSourceId,
|
[FromQuery] string? mediaSourceId,
|
||||||
[FromQuery] string? deviceId,
|
[FromQuery] string? deviceId,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec,
|
||||||
[FromQuery] bool? enableAutoStreamCopy,
|
[FromQuery] bool? enableAutoStreamCopy,
|
||||||
[FromQuery] bool? allowVideoStreamCopy,
|
[FromQuery] bool? allowVideoStreamCopy,
|
||||||
[FromQuery] bool? allowAudioStreamCopy,
|
[FromQuery] bool? allowAudioStreamCopy,
|
||||||
@@ -281,7 +281,7 @@ public class AudioController : BaseJellyfinApiController
|
|||||||
[FromQuery] int? audioChannels,
|
[FromQuery] int? audioChannels,
|
||||||
[FromQuery] int? maxAudioChannels,
|
[FromQuery] int? maxAudioChannels,
|
||||||
[FromQuery] string? profile,
|
[FromQuery] string? profile,
|
||||||
[FromQuery] string? level,
|
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level,
|
||||||
[FromQuery] float? framerate,
|
[FromQuery] float? framerate,
|
||||||
[FromQuery] float? maxFramerate,
|
[FromQuery] float? maxFramerate,
|
||||||
[FromQuery] bool? copyTimestamps,
|
[FromQuery] bool? copyTimestamps,
|
||||||
@@ -300,8 +300,8 @@ public class AudioController : BaseJellyfinApiController
|
|||||||
[FromQuery] int? cpuCoreLimit,
|
[FromQuery] int? cpuCoreLimit,
|
||||||
[FromQuery] string? liveStreamId,
|
[FromQuery] string? liveStreamId,
|
||||||
[FromQuery] bool? enableMpegtsM2TsMode,
|
[FromQuery] bool? enableMpegtsM2TsMode,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec,
|
||||||
[FromQuery] string? transcodeReasons,
|
[FromQuery] string? transcodeReasons,
|
||||||
[FromQuery] int? audioStreamIndex,
|
[FromQuery] int? audioStreamIndex,
|
||||||
[FromQuery] int? videoStreamIndex,
|
[FromQuery] int? videoStreamIndex,
|
||||||
|
|||||||
@@ -166,18 +166,18 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||||||
[ProducesPlaylistFile]
|
[ProducesPlaylistFile]
|
||||||
public async Task<ActionResult> GetLiveHlsStream(
|
public async Task<ActionResult> GetLiveHlsStream(
|
||||||
[FromRoute, Required] Guid itemId,
|
[FromRoute, Required] Guid itemId,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? container,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? container,
|
||||||
[FromQuery] bool? @static,
|
[FromQuery] bool? @static,
|
||||||
[FromQuery] string? @params,
|
[FromQuery] string? @params,
|
||||||
[FromQuery] string? tag,
|
[FromQuery] string? tag,
|
||||||
[FromQuery, ParameterObsolete] string? deviceProfileId,
|
[FromQuery, ParameterObsolete] string? deviceProfileId,
|
||||||
[FromQuery] string? playSessionId,
|
[FromQuery] string? playSessionId,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer,
|
||||||
[FromQuery] int? segmentLength,
|
[FromQuery] int? segmentLength,
|
||||||
[FromQuery] int? minSegments,
|
[FromQuery] int? minSegments,
|
||||||
[FromQuery] string? mediaSourceId,
|
[FromQuery] string? mediaSourceId,
|
||||||
[FromQuery] string? deviceId,
|
[FromQuery] string? deviceId,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec,
|
||||||
[FromQuery] bool? enableAutoStreamCopy,
|
[FromQuery] bool? enableAutoStreamCopy,
|
||||||
[FromQuery] bool? allowVideoStreamCopy,
|
[FromQuery] bool? allowVideoStreamCopy,
|
||||||
[FromQuery] bool? allowAudioStreamCopy,
|
[FromQuery] bool? allowAudioStreamCopy,
|
||||||
@@ -188,7 +188,7 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||||||
[FromQuery] int? audioChannels,
|
[FromQuery] int? audioChannels,
|
||||||
[FromQuery] int? maxAudioChannels,
|
[FromQuery] int? maxAudioChannels,
|
||||||
[FromQuery] string? profile,
|
[FromQuery] string? profile,
|
||||||
[FromQuery] string? level,
|
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level,
|
||||||
[FromQuery] float? framerate,
|
[FromQuery] float? framerate,
|
||||||
[FromQuery] float? maxFramerate,
|
[FromQuery] float? maxFramerate,
|
||||||
[FromQuery] bool? copyTimestamps,
|
[FromQuery] bool? copyTimestamps,
|
||||||
@@ -207,8 +207,8 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||||||
[FromQuery] int? cpuCoreLimit,
|
[FromQuery] int? cpuCoreLimit,
|
||||||
[FromQuery] string? liveStreamId,
|
[FromQuery] string? liveStreamId,
|
||||||
[FromQuery] bool? enableMpegtsM2TsMode,
|
[FromQuery] bool? enableMpegtsM2TsMode,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec,
|
||||||
[FromQuery] string? transcodeReasons,
|
[FromQuery] string? transcodeReasons,
|
||||||
[FromQuery] int? audioStreamIndex,
|
[FromQuery] int? audioStreamIndex,
|
||||||
[FromQuery] int? videoStreamIndex,
|
[FromQuery] int? videoStreamIndex,
|
||||||
@@ -415,12 +415,12 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||||||
[FromQuery] string? tag,
|
[FromQuery] string? tag,
|
||||||
[FromQuery, ParameterObsolete] string? deviceProfileId,
|
[FromQuery, ParameterObsolete] string? deviceProfileId,
|
||||||
[FromQuery] string? playSessionId,
|
[FromQuery] string? playSessionId,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer,
|
||||||
[FromQuery] int? segmentLength,
|
[FromQuery] int? segmentLength,
|
||||||
[FromQuery] int? minSegments,
|
[FromQuery] int? minSegments,
|
||||||
[FromQuery, Required] string mediaSourceId,
|
[FromQuery, Required] string mediaSourceId,
|
||||||
[FromQuery] string? deviceId,
|
[FromQuery] string? deviceId,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec,
|
||||||
[FromQuery] bool? enableAutoStreamCopy,
|
[FromQuery] bool? enableAutoStreamCopy,
|
||||||
[FromQuery] bool? allowVideoStreamCopy,
|
[FromQuery] bool? allowVideoStreamCopy,
|
||||||
[FromQuery] bool? allowAudioStreamCopy,
|
[FromQuery] bool? allowAudioStreamCopy,
|
||||||
@@ -431,7 +431,7 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||||||
[FromQuery] int? audioChannels,
|
[FromQuery] int? audioChannels,
|
||||||
[FromQuery] int? maxAudioChannels,
|
[FromQuery] int? maxAudioChannels,
|
||||||
[FromQuery] string? profile,
|
[FromQuery] string? profile,
|
||||||
[FromQuery] string? level,
|
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level,
|
||||||
[FromQuery] float? framerate,
|
[FromQuery] float? framerate,
|
||||||
[FromQuery] float? maxFramerate,
|
[FromQuery] float? maxFramerate,
|
||||||
[FromQuery] bool? copyTimestamps,
|
[FromQuery] bool? copyTimestamps,
|
||||||
@@ -452,8 +452,8 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||||||
[FromQuery] int? cpuCoreLimit,
|
[FromQuery] int? cpuCoreLimit,
|
||||||
[FromQuery] string? liveStreamId,
|
[FromQuery] string? liveStreamId,
|
||||||
[FromQuery] bool? enableMpegtsM2TsMode,
|
[FromQuery] bool? enableMpegtsM2TsMode,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec,
|
||||||
[FromQuery] string? transcodeReasons,
|
[FromQuery] string? transcodeReasons,
|
||||||
[FromQuery] int? audioStreamIndex,
|
[FromQuery] int? audioStreamIndex,
|
||||||
[FromQuery] int? videoStreamIndex,
|
[FromQuery] int? videoStreamIndex,
|
||||||
@@ -591,12 +591,12 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||||||
[FromQuery] string? tag,
|
[FromQuery] string? tag,
|
||||||
[FromQuery, ParameterObsolete] string? deviceProfileId,
|
[FromQuery, ParameterObsolete] string? deviceProfileId,
|
||||||
[FromQuery] string? playSessionId,
|
[FromQuery] string? playSessionId,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer,
|
||||||
[FromQuery] int? segmentLength,
|
[FromQuery] int? segmentLength,
|
||||||
[FromQuery] int? minSegments,
|
[FromQuery] int? minSegments,
|
||||||
[FromQuery, Required] string mediaSourceId,
|
[FromQuery, Required] string mediaSourceId,
|
||||||
[FromQuery] string? deviceId,
|
[FromQuery] string? deviceId,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec,
|
||||||
[FromQuery] bool? enableAutoStreamCopy,
|
[FromQuery] bool? enableAutoStreamCopy,
|
||||||
[FromQuery] bool? allowVideoStreamCopy,
|
[FromQuery] bool? allowVideoStreamCopy,
|
||||||
[FromQuery] bool? allowAudioStreamCopy,
|
[FromQuery] bool? allowAudioStreamCopy,
|
||||||
@@ -608,7 +608,7 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||||||
[FromQuery] int? audioChannels,
|
[FromQuery] int? audioChannels,
|
||||||
[FromQuery] int? maxAudioChannels,
|
[FromQuery] int? maxAudioChannels,
|
||||||
[FromQuery] string? profile,
|
[FromQuery] string? profile,
|
||||||
[FromQuery] string? level,
|
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level,
|
||||||
[FromQuery] float? framerate,
|
[FromQuery] float? framerate,
|
||||||
[FromQuery] float? maxFramerate,
|
[FromQuery] float? maxFramerate,
|
||||||
[FromQuery] bool? copyTimestamps,
|
[FromQuery] bool? copyTimestamps,
|
||||||
@@ -627,8 +627,8 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||||||
[FromQuery] int? cpuCoreLimit,
|
[FromQuery] int? cpuCoreLimit,
|
||||||
[FromQuery] string? liveStreamId,
|
[FromQuery] string? liveStreamId,
|
||||||
[FromQuery] bool? enableMpegtsM2TsMode,
|
[FromQuery] bool? enableMpegtsM2TsMode,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec,
|
||||||
[FromQuery] string? transcodeReasons,
|
[FromQuery] string? transcodeReasons,
|
||||||
[FromQuery] int? audioStreamIndex,
|
[FromQuery] int? audioStreamIndex,
|
||||||
[FromQuery] int? videoStreamIndex,
|
[FromQuery] int? videoStreamIndex,
|
||||||
@@ -761,12 +761,12 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||||||
[FromQuery] string? tag,
|
[FromQuery] string? tag,
|
||||||
[FromQuery, ParameterObsolete] string? deviceProfileId,
|
[FromQuery, ParameterObsolete] string? deviceProfileId,
|
||||||
[FromQuery] string? playSessionId,
|
[FromQuery] string? playSessionId,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer,
|
||||||
[FromQuery] int? segmentLength,
|
[FromQuery] int? segmentLength,
|
||||||
[FromQuery] int? minSegments,
|
[FromQuery] int? minSegments,
|
||||||
[FromQuery] string? mediaSourceId,
|
[FromQuery] string? mediaSourceId,
|
||||||
[FromQuery] string? deviceId,
|
[FromQuery] string? deviceId,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec,
|
||||||
[FromQuery] bool? enableAutoStreamCopy,
|
[FromQuery] bool? enableAutoStreamCopy,
|
||||||
[FromQuery] bool? allowVideoStreamCopy,
|
[FromQuery] bool? allowVideoStreamCopy,
|
||||||
[FromQuery] bool? allowAudioStreamCopy,
|
[FromQuery] bool? allowAudioStreamCopy,
|
||||||
@@ -777,7 +777,7 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||||||
[FromQuery] int? audioChannels,
|
[FromQuery] int? audioChannels,
|
||||||
[FromQuery] int? maxAudioChannels,
|
[FromQuery] int? maxAudioChannels,
|
||||||
[FromQuery] string? profile,
|
[FromQuery] string? profile,
|
||||||
[FromQuery] string? level,
|
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level,
|
||||||
[FromQuery] float? framerate,
|
[FromQuery] float? framerate,
|
||||||
[FromQuery] float? maxFramerate,
|
[FromQuery] float? maxFramerate,
|
||||||
[FromQuery] bool? copyTimestamps,
|
[FromQuery] bool? copyTimestamps,
|
||||||
@@ -798,8 +798,8 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||||||
[FromQuery] int? cpuCoreLimit,
|
[FromQuery] int? cpuCoreLimit,
|
||||||
[FromQuery] string? liveStreamId,
|
[FromQuery] string? liveStreamId,
|
||||||
[FromQuery] bool? enableMpegtsM2TsMode,
|
[FromQuery] bool? enableMpegtsM2TsMode,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec,
|
||||||
[FromQuery] string? transcodeReasons,
|
[FromQuery] string? transcodeReasons,
|
||||||
[FromQuery] int? audioStreamIndex,
|
[FromQuery] int? audioStreamIndex,
|
||||||
[FromQuery] int? videoStreamIndex,
|
[FromQuery] int? videoStreamIndex,
|
||||||
@@ -933,12 +933,12 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||||||
[FromQuery] string? tag,
|
[FromQuery] string? tag,
|
||||||
[FromQuery, ParameterObsolete] string? deviceProfileId,
|
[FromQuery, ParameterObsolete] string? deviceProfileId,
|
||||||
[FromQuery] string? playSessionId,
|
[FromQuery] string? playSessionId,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer,
|
||||||
[FromQuery] int? segmentLength,
|
[FromQuery] int? segmentLength,
|
||||||
[FromQuery] int? minSegments,
|
[FromQuery] int? minSegments,
|
||||||
[FromQuery] string? mediaSourceId,
|
[FromQuery] string? mediaSourceId,
|
||||||
[FromQuery] string? deviceId,
|
[FromQuery] string? deviceId,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec,
|
||||||
[FromQuery] bool? enableAutoStreamCopy,
|
[FromQuery] bool? enableAutoStreamCopy,
|
||||||
[FromQuery] bool? allowVideoStreamCopy,
|
[FromQuery] bool? allowVideoStreamCopy,
|
||||||
[FromQuery] bool? allowAudioStreamCopy,
|
[FromQuery] bool? allowAudioStreamCopy,
|
||||||
@@ -950,7 +950,7 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||||||
[FromQuery] int? audioChannels,
|
[FromQuery] int? audioChannels,
|
||||||
[FromQuery] int? maxAudioChannels,
|
[FromQuery] int? maxAudioChannels,
|
||||||
[FromQuery] string? profile,
|
[FromQuery] string? profile,
|
||||||
[FromQuery] string? level,
|
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level,
|
||||||
[FromQuery] float? framerate,
|
[FromQuery] float? framerate,
|
||||||
[FromQuery] float? maxFramerate,
|
[FromQuery] float? maxFramerate,
|
||||||
[FromQuery] bool? copyTimestamps,
|
[FromQuery] bool? copyTimestamps,
|
||||||
@@ -969,8 +969,8 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||||||
[FromQuery] int? cpuCoreLimit,
|
[FromQuery] int? cpuCoreLimit,
|
||||||
[FromQuery] string? liveStreamId,
|
[FromQuery] string? liveStreamId,
|
||||||
[FromQuery] bool? enableMpegtsM2TsMode,
|
[FromQuery] bool? enableMpegtsM2TsMode,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec,
|
||||||
[FromQuery] string? transcodeReasons,
|
[FromQuery] string? transcodeReasons,
|
||||||
[FromQuery] int? audioStreamIndex,
|
[FromQuery] int? audioStreamIndex,
|
||||||
[FromQuery] int? videoStreamIndex,
|
[FromQuery] int? videoStreamIndex,
|
||||||
@@ -1106,7 +1106,7 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||||||
[FromRoute, Required] Guid itemId,
|
[FromRoute, Required] Guid itemId,
|
||||||
[FromRoute, Required] string playlistId,
|
[FromRoute, Required] string playlistId,
|
||||||
[FromRoute, Required] int segmentId,
|
[FromRoute, Required] int segmentId,
|
||||||
[FromRoute, Required] string container,
|
[FromRoute, Required] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string container,
|
||||||
[FromQuery, Required] long runtimeTicks,
|
[FromQuery, Required] long runtimeTicks,
|
||||||
[FromQuery, Required] long actualSegmentLengthTicks,
|
[FromQuery, Required] long actualSegmentLengthTicks,
|
||||||
[FromQuery] bool? @static,
|
[FromQuery] bool? @static,
|
||||||
@@ -1114,12 +1114,12 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||||||
[FromQuery] string? tag,
|
[FromQuery] string? tag,
|
||||||
[FromQuery, ParameterObsolete] string? deviceProfileId,
|
[FromQuery, ParameterObsolete] string? deviceProfileId,
|
||||||
[FromQuery] string? playSessionId,
|
[FromQuery] string? playSessionId,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer,
|
||||||
[FromQuery] int? segmentLength,
|
[FromQuery] int? segmentLength,
|
||||||
[FromQuery] int? minSegments,
|
[FromQuery] int? minSegments,
|
||||||
[FromQuery] string? mediaSourceId,
|
[FromQuery] string? mediaSourceId,
|
||||||
[FromQuery] string? deviceId,
|
[FromQuery] string? deviceId,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec,
|
||||||
[FromQuery] bool? enableAutoStreamCopy,
|
[FromQuery] bool? enableAutoStreamCopy,
|
||||||
[FromQuery] bool? allowVideoStreamCopy,
|
[FromQuery] bool? allowVideoStreamCopy,
|
||||||
[FromQuery] bool? allowAudioStreamCopy,
|
[FromQuery] bool? allowAudioStreamCopy,
|
||||||
@@ -1130,7 +1130,7 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||||||
[FromQuery] int? audioChannels,
|
[FromQuery] int? audioChannels,
|
||||||
[FromQuery] int? maxAudioChannels,
|
[FromQuery] int? maxAudioChannels,
|
||||||
[FromQuery] string? profile,
|
[FromQuery] string? profile,
|
||||||
[FromQuery] string? level,
|
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level,
|
||||||
[FromQuery] float? framerate,
|
[FromQuery] float? framerate,
|
||||||
[FromQuery] float? maxFramerate,
|
[FromQuery] float? maxFramerate,
|
||||||
[FromQuery] bool? copyTimestamps,
|
[FromQuery] bool? copyTimestamps,
|
||||||
@@ -1151,8 +1151,8 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||||||
[FromQuery] int? cpuCoreLimit,
|
[FromQuery] int? cpuCoreLimit,
|
||||||
[FromQuery] string? liveStreamId,
|
[FromQuery] string? liveStreamId,
|
||||||
[FromQuery] bool? enableMpegtsM2TsMode,
|
[FromQuery] bool? enableMpegtsM2TsMode,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec,
|
||||||
[FromQuery] string? transcodeReasons,
|
[FromQuery] string? transcodeReasons,
|
||||||
[FromQuery] int? audioStreamIndex,
|
[FromQuery] int? audioStreamIndex,
|
||||||
[FromQuery] int? videoStreamIndex,
|
[FromQuery] int? videoStreamIndex,
|
||||||
@@ -1291,7 +1291,7 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||||||
[FromRoute, Required] Guid itemId,
|
[FromRoute, Required] Guid itemId,
|
||||||
[FromRoute, Required] string playlistId,
|
[FromRoute, Required] string playlistId,
|
||||||
[FromRoute, Required] int segmentId,
|
[FromRoute, Required] int segmentId,
|
||||||
[FromRoute, Required] string container,
|
[FromRoute, Required] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string container,
|
||||||
[FromQuery, Required] long runtimeTicks,
|
[FromQuery, Required] long runtimeTicks,
|
||||||
[FromQuery, Required] long actualSegmentLengthTicks,
|
[FromQuery, Required] long actualSegmentLengthTicks,
|
||||||
[FromQuery] bool? @static,
|
[FromQuery] bool? @static,
|
||||||
@@ -1299,12 +1299,12 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||||||
[FromQuery] string? tag,
|
[FromQuery] string? tag,
|
||||||
[FromQuery, ParameterObsolete] string? deviceProfileId,
|
[FromQuery, ParameterObsolete] string? deviceProfileId,
|
||||||
[FromQuery] string? playSessionId,
|
[FromQuery] string? playSessionId,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer,
|
||||||
[FromQuery] int? segmentLength,
|
[FromQuery] int? segmentLength,
|
||||||
[FromQuery] int? minSegments,
|
[FromQuery] int? minSegments,
|
||||||
[FromQuery] string? mediaSourceId,
|
[FromQuery] string? mediaSourceId,
|
||||||
[FromQuery] string? deviceId,
|
[FromQuery] string? deviceId,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec,
|
||||||
[FromQuery] bool? enableAutoStreamCopy,
|
[FromQuery] bool? enableAutoStreamCopy,
|
||||||
[FromQuery] bool? allowVideoStreamCopy,
|
[FromQuery] bool? allowVideoStreamCopy,
|
||||||
[FromQuery] bool? allowAudioStreamCopy,
|
[FromQuery] bool? allowAudioStreamCopy,
|
||||||
@@ -1316,7 +1316,7 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||||||
[FromQuery] int? audioChannels,
|
[FromQuery] int? audioChannels,
|
||||||
[FromQuery] int? maxAudioChannels,
|
[FromQuery] int? maxAudioChannels,
|
||||||
[FromQuery] string? profile,
|
[FromQuery] string? profile,
|
||||||
[FromQuery] string? level,
|
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level,
|
||||||
[FromQuery] float? framerate,
|
[FromQuery] float? framerate,
|
||||||
[FromQuery] float? maxFramerate,
|
[FromQuery] float? maxFramerate,
|
||||||
[FromQuery] bool? copyTimestamps,
|
[FromQuery] bool? copyTimestamps,
|
||||||
@@ -1335,8 +1335,8 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||||||
[FromQuery] int? cpuCoreLimit,
|
[FromQuery] int? cpuCoreLimit,
|
||||||
[FromQuery] string? liveStreamId,
|
[FromQuery] string? liveStreamId,
|
||||||
[FromQuery] bool? enableMpegtsM2TsMode,
|
[FromQuery] bool? enableMpegtsM2TsMode,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec,
|
||||||
[FromQuery] string? transcodeReasons,
|
[FromQuery] string? transcodeReasons,
|
||||||
[FromQuery] int? audioStreamIndex,
|
[FromQuery] int? audioStreamIndex,
|
||||||
[FromQuery] int? videoStreamIndex,
|
[FromQuery] int? videoStreamIndex,
|
||||||
@@ -1819,16 +1819,13 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||||||
if (isActualOutputVideoCodecHevc || isActualOutputVideoCodecAv1)
|
if (isActualOutputVideoCodecHevc || isActualOutputVideoCodecAv1)
|
||||||
{
|
{
|
||||||
var requestedRange = state.GetRequestedRangeTypes(state.ActualOutputVideoCodec);
|
var requestedRange = state.GetRequestedRangeTypes(state.ActualOutputVideoCodec);
|
||||||
var requestHasDOVI = requestedRange.Contains(VideoRangeType.DOVI.ToString(), StringComparison.OrdinalIgnoreCase);
|
// Clients reporting Dolby Vision capabilities with fallbacks may only support the fallback layer.
|
||||||
var requestHasDOVIWithHDR10 = requestedRange.Contains(VideoRangeType.DOVIWithHDR10.ToString(), StringComparison.OrdinalIgnoreCase);
|
// Only enable Dolby Vision remuxing if the client explicitly declares support for profiles without fallbacks.
|
||||||
var requestHasDOVIWithHLG = requestedRange.Contains(VideoRangeType.DOVIWithHLG.ToString(), StringComparison.OrdinalIgnoreCase);
|
var clientSupportsDoVi = requestedRange.Contains(VideoRangeType.DOVI.ToString(), StringComparison.OrdinalIgnoreCase);
|
||||||
var requestHasDOVIWithSDR = requestedRange.Contains(VideoRangeType.DOVIWithSDR.ToString(), StringComparison.OrdinalIgnoreCase);
|
var videoIsDoVi = state.VideoStream.VideoRangeType is VideoRangeType.DOVI or VideoRangeType.DOVIWithHDR10 or VideoRangeType.DOVIWithHLG or VideoRangeType.DOVIWithSDR;
|
||||||
|
|
||||||
if (EncodingHelper.IsCopyCodec(codec)
|
if (EncodingHelper.IsCopyCodec(codec)
|
||||||
&& ((state.VideoStream.VideoRangeType == VideoRangeType.DOVI && requestHasDOVI)
|
&& (videoIsDoVi && clientSupportsDoVi))
|
||||||
|| (state.VideoStream.VideoRangeType == VideoRangeType.DOVIWithHDR10 && requestHasDOVIWithHDR10)
|
|
||||||
|| (state.VideoStream.VideoRangeType == VideoRangeType.DOVIWithHLG && requestHasDOVIWithHLG)
|
|
||||||
|| (state.VideoStream.VideoRangeType == VideoRangeType.DOVIWithSDR && requestHasDOVIWithSDR)))
|
|
||||||
{
|
{
|
||||||
if (isActualOutputVideoCodecHevc)
|
if (isActualOutputVideoCodecHevc)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1186,7 +1186,9 @@ public class LiveTvController : BaseJellyfinApiController
|
|||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
[ProducesVideoFile]
|
[ProducesVideoFile]
|
||||||
public ActionResult GetLiveStreamFile([FromRoute, Required] string streamId, [FromRoute, Required] string container)
|
public ActionResult GetLiveStreamFile(
|
||||||
|
[FromRoute, Required] string streamId,
|
||||||
|
[FromRoute, Required] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string container)
|
||||||
{
|
{
|
||||||
var liveStreamInfo = _mediaSourceManager.GetLiveStreamInfoByUniqueId(streamId);
|
var liveStreamInfo = _mediaSourceManager.GetLiveStreamInfoByUniqueId(streamId);
|
||||||
if (liveStreamInfo is null)
|
if (liveStreamInfo is null)
|
||||||
|
|||||||
@@ -102,13 +102,13 @@ public class UniversalAudioController : BaseJellyfinApiController
|
|||||||
[FromQuery] string? mediaSourceId,
|
[FromQuery] string? mediaSourceId,
|
||||||
[FromQuery] string? deviceId,
|
[FromQuery] string? deviceId,
|
||||||
[FromQuery] Guid? userId,
|
[FromQuery] Guid? userId,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec,
|
||||||
[FromQuery] int? maxAudioChannels,
|
[FromQuery] int? maxAudioChannels,
|
||||||
[FromQuery] int? transcodingAudioChannels,
|
[FromQuery] int? transcodingAudioChannels,
|
||||||
[FromQuery] int? maxStreamingBitrate,
|
[FromQuery] int? maxStreamingBitrate,
|
||||||
[FromQuery] int? audioBitRate,
|
[FromQuery] int? audioBitRate,
|
||||||
[FromQuery] long? startTimeTicks,
|
[FromQuery] long? startTimeTicks,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? transcodingContainer,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? transcodingContainer,
|
||||||
[FromQuery] MediaStreamProtocol? transcodingProtocol,
|
[FromQuery] MediaStreamProtocol? transcodingProtocol,
|
||||||
[FromQuery] int? maxAudioSampleRate,
|
[FromQuery] int? maxAudioSampleRate,
|
||||||
[FromQuery] int? maxAudioBitDepth,
|
[FromQuery] int? maxAudioBitDepth,
|
||||||
|
|||||||
@@ -315,18 +315,18 @@ public class VideosController : BaseJellyfinApiController
|
|||||||
[ProducesVideoFile]
|
[ProducesVideoFile]
|
||||||
public async Task<ActionResult> GetVideoStream(
|
public async Task<ActionResult> GetVideoStream(
|
||||||
[FromRoute, Required] Guid itemId,
|
[FromRoute, Required] Guid itemId,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? container,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? container,
|
||||||
[FromQuery] bool? @static,
|
[FromQuery] bool? @static,
|
||||||
[FromQuery] string? @params,
|
[FromQuery] string? @params,
|
||||||
[FromQuery] string? tag,
|
[FromQuery] string? tag,
|
||||||
[FromQuery, ParameterObsolete] string? deviceProfileId,
|
[FromQuery, ParameterObsolete] string? deviceProfileId,
|
||||||
[FromQuery] string? playSessionId,
|
[FromQuery] string? playSessionId,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer,
|
||||||
[FromQuery] int? segmentLength,
|
[FromQuery] int? segmentLength,
|
||||||
[FromQuery] int? minSegments,
|
[FromQuery] int? minSegments,
|
||||||
[FromQuery] string? mediaSourceId,
|
[FromQuery] string? mediaSourceId,
|
||||||
[FromQuery] string? deviceId,
|
[FromQuery] string? deviceId,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec,
|
||||||
[FromQuery] bool? enableAutoStreamCopy,
|
[FromQuery] bool? enableAutoStreamCopy,
|
||||||
[FromQuery] bool? allowVideoStreamCopy,
|
[FromQuery] bool? allowVideoStreamCopy,
|
||||||
[FromQuery] bool? allowAudioStreamCopy,
|
[FromQuery] bool? allowAudioStreamCopy,
|
||||||
@@ -337,7 +337,7 @@ public class VideosController : BaseJellyfinApiController
|
|||||||
[FromQuery] int? audioChannels,
|
[FromQuery] int? audioChannels,
|
||||||
[FromQuery] int? maxAudioChannels,
|
[FromQuery] int? maxAudioChannels,
|
||||||
[FromQuery] string? profile,
|
[FromQuery] string? profile,
|
||||||
[FromQuery] string? level,
|
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level,
|
||||||
[FromQuery] float? framerate,
|
[FromQuery] float? framerate,
|
||||||
[FromQuery] float? maxFramerate,
|
[FromQuery] float? maxFramerate,
|
||||||
[FromQuery] bool? copyTimestamps,
|
[FromQuery] bool? copyTimestamps,
|
||||||
@@ -358,8 +358,8 @@ public class VideosController : BaseJellyfinApiController
|
|||||||
[FromQuery] int? cpuCoreLimit,
|
[FromQuery] int? cpuCoreLimit,
|
||||||
[FromQuery] string? liveStreamId,
|
[FromQuery] string? liveStreamId,
|
||||||
[FromQuery] bool? enableMpegtsM2TsMode,
|
[FromQuery] bool? enableMpegtsM2TsMode,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec,
|
||||||
[FromQuery] string? transcodeReasons,
|
[FromQuery] string? transcodeReasons,
|
||||||
[FromQuery] int? audioStreamIndex,
|
[FromQuery] int? audioStreamIndex,
|
||||||
[FromQuery] int? videoStreamIndex,
|
[FromQuery] int? videoStreamIndex,
|
||||||
@@ -556,18 +556,18 @@ public class VideosController : BaseJellyfinApiController
|
|||||||
[ProducesVideoFile]
|
[ProducesVideoFile]
|
||||||
public Task<ActionResult> GetVideoStreamByContainer(
|
public Task<ActionResult> GetVideoStreamByContainer(
|
||||||
[FromRoute, Required] Guid itemId,
|
[FromRoute, Required] Guid itemId,
|
||||||
[FromRoute, Required] string container,
|
[FromRoute, Required] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string container,
|
||||||
[FromQuery] bool? @static,
|
[FromQuery] bool? @static,
|
||||||
[FromQuery] string? @params,
|
[FromQuery] string? @params,
|
||||||
[FromQuery] string? tag,
|
[FromQuery] string? tag,
|
||||||
[FromQuery] string? deviceProfileId,
|
[FromQuery] string? deviceProfileId,
|
||||||
[FromQuery] string? playSessionId,
|
[FromQuery] string? playSessionId,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer,
|
||||||
[FromQuery] int? segmentLength,
|
[FromQuery] int? segmentLength,
|
||||||
[FromQuery] int? minSegments,
|
[FromQuery] int? minSegments,
|
||||||
[FromQuery] string? mediaSourceId,
|
[FromQuery] string? mediaSourceId,
|
||||||
[FromQuery] string? deviceId,
|
[FromQuery] string? deviceId,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec,
|
||||||
[FromQuery] bool? enableAutoStreamCopy,
|
[FromQuery] bool? enableAutoStreamCopy,
|
||||||
[FromQuery] bool? allowVideoStreamCopy,
|
[FromQuery] bool? allowVideoStreamCopy,
|
||||||
[FromQuery] bool? allowAudioStreamCopy,
|
[FromQuery] bool? allowAudioStreamCopy,
|
||||||
@@ -578,7 +578,7 @@ public class VideosController : BaseJellyfinApiController
|
|||||||
[FromQuery] int? audioChannels,
|
[FromQuery] int? audioChannels,
|
||||||
[FromQuery] int? maxAudioChannels,
|
[FromQuery] int? maxAudioChannels,
|
||||||
[FromQuery] string? profile,
|
[FromQuery] string? profile,
|
||||||
[FromQuery] string? level,
|
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level,
|
||||||
[FromQuery] float? framerate,
|
[FromQuery] float? framerate,
|
||||||
[FromQuery] float? maxFramerate,
|
[FromQuery] float? maxFramerate,
|
||||||
[FromQuery] bool? copyTimestamps,
|
[FromQuery] bool? copyTimestamps,
|
||||||
@@ -599,8 +599,8 @@ public class VideosController : BaseJellyfinApiController
|
|||||||
[FromQuery] int? cpuCoreLimit,
|
[FromQuery] int? cpuCoreLimit,
|
||||||
[FromQuery] string? liveStreamId,
|
[FromQuery] string? liveStreamId,
|
||||||
[FromQuery] bool? enableMpegtsM2TsMode,
|
[FromQuery] bool? enableMpegtsM2TsMode,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec,
|
||||||
[FromQuery] string? transcodeReasons,
|
[FromQuery] string? transcodeReasons,
|
||||||
[FromQuery] int? audioStreamIndex,
|
[FromQuery] int? audioStreamIndex,
|
||||||
[FromQuery] int? videoStreamIndex,
|
[FromQuery] int? videoStreamIndex,
|
||||||
|
|||||||
@@ -235,6 +235,11 @@ public static class StreamingHelpers
|
|||||||
state.VideoRequest.MaxHeight = resolution.MaxHeight;
|
state.VideoRequest.MaxHeight = resolution.MaxHeight;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (state.AudioStream is not null && !EncodingHelper.IsCopyCodec(state.OutputAudioCodec) && string.Equals(state.AudioStream.Codec, state.OutputAudioCodec, StringComparison.OrdinalIgnoreCase) && state.OutputAudioBitrate.HasValue)
|
||||||
|
{
|
||||||
|
state.OutputAudioCodec = state.SupportedAudioCodecs.Where(c => !EncodingHelper.LosslessAudioCodecs.Contains(c)).FirstOrDefault(mediaEncoder.CanEncodeToAudioCodec);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var ext = string.IsNullOrWhiteSpace(state.OutputContainer)
|
var ext = string.IsNullOrWhiteSpace(state.OutputContainer)
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ public class ActivityLogWebSocketListener : BasePeriodicWebSocketListener<Activi
|
|||||||
/// <param name="message">The message.</param>
|
/// <param name="message">The message.</param>
|
||||||
protected override void Start(WebSocketMessageInfo message)
|
protected override void Start(WebSocketMessageInfo message)
|
||||||
{
|
{
|
||||||
if (!message.Connection.AuthorizationInfo.User.HasPermission(PermissionKind.IsAdministrator))
|
if (!message.Connection.AuthorizationInfo.User.HasPermission(PermissionKind.IsAdministrator) && !message.Connection.AuthorizationInfo.IsApiKey)
|
||||||
{
|
{
|
||||||
throw new AuthenticationException("Only admin users can retrieve the activity log.");
|
throw new AuthenticationException("Only admin users can retrieve the activity log.");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ public class SessionInfoWebSocketListener : BasePeriodicWebSocketListener<IEnume
|
|||||||
/// <param name="message">The message.</param>
|
/// <param name="message">The message.</param>
|
||||||
protected override void Start(WebSocketMessageInfo message)
|
protected override void Start(WebSocketMessageInfo message)
|
||||||
{
|
{
|
||||||
if (!message.Connection.AuthorizationInfo.User.HasPermission(PermissionKind.IsAdministrator))
|
if (!message.Connection.AuthorizationInfo.User.HasPermission(PermissionKind.IsAdministrator) && !message.Connection.AuthorizationInfo.IsApiKey)
|
||||||
{
|
{
|
||||||
throw new AuthenticationException("Only admin users can subscribe to session information.");
|
throw new AuthenticationException("Only admin users can subscribe to session information.");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Authors>Jellyfin Contributors</Authors>
|
<Authors>Jellyfin Contributors</Authors>
|
||||||
<PackageId>Jellyfin.Data</PackageId>
|
<PackageId>Jellyfin.Data</PackageId>
|
||||||
<VersionPrefix>10.10.2</VersionPrefix>
|
<VersionPrefix>10.10.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>
|
||||||
|
|||||||
@@ -194,6 +194,14 @@ public class TrickplayManager : ITrickplayManager
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We support video backdrops, but we should not generate trickplay images for them
|
||||||
|
var parentDirectory = Directory.GetParent(mediaPath);
|
||||||
|
if (parentDirectory is not null && string.Equals(parentDirectory.Name, "backdrops", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Ignoring backdrop media found at {Path} for item {ItemID}", mediaPath, video.Id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// The width has to be even, otherwise a lot of filters will not be able to sample it
|
// The width has to be even, otherwise a lot of filters will not be able to sample it
|
||||||
var actualWidth = 2 * (width / 2);
|
var actualWidth = 2 * (width / 2);
|
||||||
|
|
||||||
|
|||||||
@@ -118,15 +118,15 @@ namespace Jellyfin.Server.Extensions
|
|||||||
// https://github.com/dotnet/aspnetcore/blob/master/src/Middleware/HttpOverrides/src/ForwardedHeadersMiddleware.cs
|
// https://github.com/dotnet/aspnetcore/blob/master/src/Middleware/HttpOverrides/src/ForwardedHeadersMiddleware.cs
|
||||||
// Enable debug logging on Microsoft.AspNetCore.HttpOverrides.ForwardedHeadersMiddleware to help investigate issues.
|
// Enable debug logging on Microsoft.AspNetCore.HttpOverrides.ForwardedHeadersMiddleware to help investigate issues.
|
||||||
|
|
||||||
options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto | ForwardedHeaders.XForwardedHost;
|
|
||||||
|
|
||||||
if (config.KnownProxies.Length == 0)
|
if (config.KnownProxies.Length == 0)
|
||||||
{
|
{
|
||||||
|
options.ForwardedHeaders = ForwardedHeaders.None;
|
||||||
options.KnownNetworks.Clear();
|
options.KnownNetworks.Clear();
|
||||||
options.KnownProxies.Clear();
|
options.KnownProxies.Clear();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto | ForwardedHeaders.XForwardedHost;
|
||||||
AddProxyAddresses(config, config.KnownProxies, options);
|
AddProxyAddresses(config, config.KnownProxies, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -51,6 +51,8 @@ namespace Jellyfin.Server.Migrations
|
|||||||
typeof(Routines.RemoveDuplicatePlaylistChildren)
|
typeof(Routines.RemoveDuplicatePlaylistChildren)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private static readonly Guid _downgradeCheckMigration = Guid.Parse("36445464-849f-429f-9ad0-bb130efa0664");
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Run all needed migrations.
|
/// Run all needed migrations.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -88,6 +90,12 @@ namespace Jellyfin.Server.Migrations
|
|||||||
? (MigrationOptions)xmlSerializer.DeserializeFromFile(typeof(MigrationOptions), migrationConfigPath)!
|
? (MigrationOptions)xmlSerializer.DeserializeFromFile(typeof(MigrationOptions), migrationConfigPath)!
|
||||||
: new MigrationOptions();
|
: new MigrationOptions();
|
||||||
|
|
||||||
|
// 10.10 specific EFCore migration check.
|
||||||
|
if (migrationOptions.Applied.Any(f => f.Id.Equals(_downgradeCheckMigration)))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("You cannot downgrade your jellyfin install from the library.db migration.");
|
||||||
|
}
|
||||||
|
|
||||||
// We have to deserialize it manually since the configuration manager may overwrite it
|
// We have to deserialize it manually since the configuration manager may overwrite it
|
||||||
var serverConfig = File.Exists(appPaths.SystemConfigurationFilePath)
|
var serverConfig = File.Exists(appPaths.SystemConfigurationFilePath)
|
||||||
? (ServerConfiguration)xmlSerializer.DeserializeFromFile(typeof(ServerConfiguration), appPaths.SystemConfigurationFilePath)!
|
? (ServerConfiguration)xmlSerializer.DeserializeFromFile(typeof(ServerConfiguration), appPaths.SystemConfigurationFilePath)!
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ namespace Jellyfin.Server.Migrations.Routines
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public Guid Id => Guid.Parse("{67445D54-B895-4B24-9F4C-35CE0690EA07}");
|
public Guid Id => Guid.Parse("{D34BFC33-5D2E-4790-8085-069EF6EECB4E}");
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public string Name => "MigrateRatingLevels";
|
public string Name => "MigrateRatingLevels";
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ internal class RemoveDuplicatePlaylistChildren : IMigrationRoutine
|
|||||||
IncludeItemTypes = [BaseItemKind.Playlist]
|
IncludeItemTypes = [BaseItemKind.Playlist]
|
||||||
})
|
})
|
||||||
.Cast<Playlist>()
|
.Cast<Playlist>()
|
||||||
|
.Where(p => !p.OpenAccess || !p.OwnerUserId.Equals(Guid.Empty))
|
||||||
.ToArray();
|
.ToArray();
|
||||||
|
|
||||||
if (playlists.Length > 0)
|
if (playlists.Length > 0)
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Authors>Jellyfin Contributors</Authors>
|
<Authors>Jellyfin Contributors</Authors>
|
||||||
<PackageId>Jellyfin.Common</PackageId>
|
<PackageId>Jellyfin.Common</PackageId>
|
||||||
<VersionPrefix>10.10.2</VersionPrefix>
|
<VersionPrefix>10.10.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>
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ namespace MediaBrowser.Controller.Channels
|
|||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public override SourceType SourceType => SourceType.Channel;
|
public override SourceType SourceType => SourceType.Channel;
|
||||||
|
|
||||||
public override bool IsVisible(User user)
|
public override bool IsVisible(User user, bool skipAllowedTagsCheck = false)
|
||||||
{
|
{
|
||||||
var blockedChannelsPreference = user.GetPreferenceValues<Guid>(PreferenceKind.BlockedChannels);
|
var blockedChannelsPreference = user.GetPreferenceValues<Guid>(PreferenceKind.BlockedChannels);
|
||||||
if (blockedChannelsPreference.Length != 0)
|
if (blockedChannelsPreference.Length != 0)
|
||||||
@@ -41,7 +41,7 @@ namespace MediaBrowser.Controller.Channels
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return base.IsVisible(user);
|
return base.IsVisible(user, skipAllowedTagsCheck);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override QueryResult<BaseItem> GetItemsInternal(InternalItemsQuery query)
|
protected override QueryResult<BaseItem> GetItemsInternal(InternalItemsQuery query)
|
||||||
|
|||||||
@@ -1299,7 +1299,7 @@ namespace MediaBrowser.Controller.Entities
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (GetParents().Any(i => !i.IsVisible(user)))
|
if (GetParents().Any(i => !i.IsVisible(user, true)))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -1521,13 +1521,14 @@ namespace MediaBrowser.Controller.Entities
|
|||||||
/// Determines if a given user has access to this item.
|
/// Determines if a given user has access to this item.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="user">The user.</param>
|
/// <param name="user">The user.</param>
|
||||||
|
/// <param name="skipAllowedTagsCheck">Don't check for allowed tags.</param>
|
||||||
/// <returns><c>true</c> if [is parental allowed] [the specified user]; otherwise, <c>false</c>.</returns>
|
/// <returns><c>true</c> if [is parental allowed] [the specified user]; otherwise, <c>false</c>.</returns>
|
||||||
/// <exception cref="ArgumentNullException">If user is null.</exception>
|
/// <exception cref="ArgumentNullException">If user is null.</exception>
|
||||||
public bool IsParentalAllowed(User user)
|
public bool IsParentalAllowed(User user, bool skipAllowedTagsCheck)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(user);
|
ArgumentNullException.ThrowIfNull(user);
|
||||||
|
|
||||||
if (!IsVisibleViaTags(user))
|
if (!IsVisibleViaTags(user, skipAllowedTagsCheck))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -1599,7 +1600,7 @@ namespace MediaBrowser.Controller.Entities
|
|||||||
return list.Distinct(StringComparer.OrdinalIgnoreCase).ToList();
|
return list.Distinct(StringComparer.OrdinalIgnoreCase).ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool IsVisibleViaTags(User user)
|
private bool IsVisibleViaTags(User user, bool skipAllowedTagsCheck)
|
||||||
{
|
{
|
||||||
var allTags = GetInheritedTags();
|
var allTags = GetInheritedTags();
|
||||||
if (user.GetPreference(PreferenceKind.BlockedTags).Any(i => allTags.Contains(i, StringComparison.OrdinalIgnoreCase)))
|
if (user.GetPreference(PreferenceKind.BlockedTags).Any(i => allTags.Contains(i, StringComparison.OrdinalIgnoreCase)))
|
||||||
@@ -1614,7 +1615,7 @@ namespace MediaBrowser.Controller.Entities
|
|||||||
}
|
}
|
||||||
|
|
||||||
var allowedTagsPreference = user.GetPreference(PreferenceKind.AllowedTags);
|
var allowedTagsPreference = user.GetPreference(PreferenceKind.AllowedTags);
|
||||||
if (allowedTagsPreference.Length != 0 && !allowedTagsPreference.Any(i => allTags.Contains(i, StringComparison.OrdinalIgnoreCase)))
|
if (!skipAllowedTagsCheck && allowedTagsPreference.Length != 0 && !allowedTagsPreference.Any(i => allTags.Contains(i, StringComparison.OrdinalIgnoreCase)))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -1654,13 +1655,14 @@ namespace MediaBrowser.Controller.Entities
|
|||||||
/// Default is just parental allowed. Can be overridden for more functionality.
|
/// Default is just parental allowed. Can be overridden for more functionality.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="user">The user.</param>
|
/// <param name="user">The user.</param>
|
||||||
|
/// <param name="skipAllowedTagsCheck">Don't check for allowed tags.</param>
|
||||||
/// <returns><c>true</c> if the specified user is visible; otherwise, <c>false</c>.</returns>
|
/// <returns><c>true</c> if the specified user is visible; otherwise, <c>false</c>.</returns>
|
||||||
/// <exception cref="ArgumentNullException"><paramref name="user" /> is <c>null</c>.</exception>
|
/// <exception cref="ArgumentNullException"><paramref name="user" /> is <c>null</c>.</exception>
|
||||||
public virtual bool IsVisible(User user)
|
public virtual bool IsVisible(User user, bool skipAllowedTagsCheck = false)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(user);
|
ArgumentNullException.ThrowIfNull(user);
|
||||||
|
|
||||||
return IsParentalAllowed(user);
|
return IsParentalAllowed(user, skipAllowedTagsCheck);
|
||||||
}
|
}
|
||||||
|
|
||||||
public virtual bool IsVisibleStandalone(User user)
|
public virtual bool IsVisibleStandalone(User user)
|
||||||
|
|||||||
@@ -96,11 +96,11 @@ namespace MediaBrowser.Controller.Entities
|
|||||||
return GetLibraryOptions(Path);
|
return GetLibraryOptions(Path);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override bool IsVisible(User user)
|
public override bool IsVisible(User user, bool skipAllowedTagsCheck = false)
|
||||||
{
|
{
|
||||||
if (GetLibraryOptions().Enabled)
|
if (GetLibraryOptions().Enabled)
|
||||||
{
|
{
|
||||||
return base.IsVisible(user);
|
return base.IsVisible(user, skipAllowedTagsCheck);
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -217,7 +217,7 @@ namespace MediaBrowser.Controller.Entities
|
|||||||
LibraryManager.CreateItem(item, this);
|
LibraryManager.CreateItem(item, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override bool IsVisible(User user)
|
public override bool IsVisible(User user, bool skipAllowedTagsCheck = false)
|
||||||
{
|
{
|
||||||
if (this is ICollectionFolder && this is not BasePluginFolder)
|
if (this is ICollectionFolder && this is not BasePluginFolder)
|
||||||
{
|
{
|
||||||
@@ -239,7 +239,7 @@ namespace MediaBrowser.Controller.Entities
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return base.IsVisible(user);
|
return base.IsVisible(user, skipAllowedTagsCheck);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -1200,6 +1200,11 @@ namespace MediaBrowser.Controller.Entities
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (request.Is4K.HasValue)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (request.IsHD.HasValue)
|
if (request.IsHD.HasValue)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -144,14 +144,14 @@ namespace MediaBrowser.Controller.Entities.Movies
|
|||||||
return GetItemLookupInfo<BoxSetInfo>();
|
return GetItemLookupInfo<BoxSetInfo>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public override bool IsVisible(User user)
|
public override bool IsVisible(User user, bool skipAllowedTagsCheck = false)
|
||||||
{
|
{
|
||||||
if (IsLegacyBoxSet)
|
if (IsLegacyBoxSet)
|
||||||
{
|
{
|
||||||
return base.IsVisible(user);
|
return base.IsVisible(user, skipAllowedTagsCheck);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (base.IsVisible(user))
|
if (base.IsVisible(user, skipAllowedTagsCheck))
|
||||||
{
|
{
|
||||||
if (LinkedChildren.Length == 0)
|
if (LinkedChildren.Length == 0)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -152,16 +152,7 @@ namespace MediaBrowser.Controller.Entities
|
|||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
if (!string.IsNullOrEmpty(PrimaryVersionId))
|
return GetMediaSourceCount(new HashSet<Guid>());
|
||||||
{
|
|
||||||
var item = LibraryManager.GetItemById(PrimaryVersionId);
|
|
||||||
if (item is Video video)
|
|
||||||
{
|
|
||||||
return video.MediaSourceCount;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return LinkedAlternateVersions.Length + LocalAlternateVersions.Length + 1;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -550,5 +541,25 @@ namespace MediaBrowser.Controller.Entities
|
|||||||
|
|
||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private int GetMediaSourceCount(HashSet<Guid> callstack)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(PrimaryVersionId))
|
||||||
|
{
|
||||||
|
var item = LibraryManager.GetItemById(PrimaryVersionId);
|
||||||
|
if (item is Video video)
|
||||||
|
{
|
||||||
|
if (callstack.Contains(video.Id))
|
||||||
|
{
|
||||||
|
return video.LinkedAlternateVersions.Length + video.LocalAlternateVersions.Length + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
callstack.Add(video.Id);
|
||||||
|
return video.GetMediaSourceCount(callstack);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return LinkedAlternateVersions.Length + LocalAlternateVersions.Length + 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Authors>Jellyfin Contributors</Authors>
|
<Authors>Jellyfin Contributors</Authors>
|
||||||
<PackageId>Jellyfin.Controller</PackageId>
|
<PackageId>Jellyfin.Controller</PackageId>
|
||||||
<VersionPrefix>10.10.2</VersionPrefix>
|
<VersionPrefix>10.10.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>
|
||||||
|
|||||||
@@ -35,7 +35,13 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
/// periods, underscores, commas, and vertical bars, with a length between 0 and 40 characters.
|
/// periods, underscores, commas, and vertical bars, with a length between 0 and 40 characters.
|
||||||
/// This should matches all common valid codecs.
|
/// This should matches all common valid codecs.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string ValidationRegex = @"^[a-zA-Z0-9\-\._,|]{0,40}$";
|
public const string ContainerValidationRegex = @"^[a-zA-Z0-9\-\._,|]{0,40}$";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The level validation regex.
|
||||||
|
/// This regular expression matches strings representing a double.
|
||||||
|
/// </summary>
|
||||||
|
public const string LevelValidationRegex = @"-?[0-9]+(?:\.[0-9]+)?";
|
||||||
|
|
||||||
private const string _defaultMjpegEncoder = "mjpeg";
|
private const string _defaultMjpegEncoder = "mjpeg";
|
||||||
|
|
||||||
@@ -75,7 +81,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
private readonly Version _minFFmpegVaapiDeviceVendorId = new Version(7, 0, 1);
|
private readonly Version _minFFmpegVaapiDeviceVendorId = new Version(7, 0, 1);
|
||||||
private readonly Version _minFFmpegQsvVppScaleModeOption = new Version(6, 0);
|
private readonly Version _minFFmpegQsvVppScaleModeOption = new Version(6, 0);
|
||||||
|
|
||||||
private static readonly Regex _validationRegex = new(ValidationRegex, RegexOptions.Compiled);
|
private static readonly Regex _containerValidationRegex = new(ContainerValidationRegex, RegexOptions.Compiled);
|
||||||
|
|
||||||
private static readonly string[] _videoProfilesH264 =
|
private static readonly string[] _videoProfilesH264 =
|
||||||
[
|
[
|
||||||
@@ -309,7 +315,6 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
private bool IsSwTonemapAvailable(EncodingJobInfo state, EncodingOptions options)
|
private bool IsSwTonemapAvailable(EncodingJobInfo state, EncodingOptions options)
|
||||||
{
|
{
|
||||||
if (state.VideoStream is null
|
if (state.VideoStream is null
|
||||||
|| !options.EnableTonemapping
|
|
||||||
|| GetVideoColorBitDepth(state) < 10
|
|| GetVideoColorBitDepth(state) < 10
|
||||||
|| !_mediaEncoder.SupportsFilter("tonemapx"))
|
|| !_mediaEncoder.SupportsFilter("tonemapx"))
|
||||||
{
|
{
|
||||||
@@ -451,7 +456,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
return GetMjpegEncoder(state, encodingOptions);
|
return GetMjpegEncoder(state, encodingOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_validationRegex.IsMatch(codec))
|
if (_containerValidationRegex.IsMatch(codec))
|
||||||
{
|
{
|
||||||
return codec.ToLowerInvariant();
|
return codec.ToLowerInvariant();
|
||||||
}
|
}
|
||||||
@@ -492,7 +497,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
|
|
||||||
public static string GetInputFormat(string container)
|
public static string GetInputFormat(string container)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(container) || !_validationRegex.IsMatch(container))
|
if (string.IsNullOrEmpty(container) || !_containerValidationRegex.IsMatch(container))
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -710,7 +715,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
{
|
{
|
||||||
var codec = state.OutputAudioCodec;
|
var codec = state.OutputAudioCodec;
|
||||||
|
|
||||||
if (!_validationRegex.IsMatch(codec))
|
if (!_containerValidationRegex.IsMatch(codec))
|
||||||
{
|
{
|
||||||
codec = "aac";
|
codec = "aac";
|
||||||
}
|
}
|
||||||
@@ -2061,7 +2066,13 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
// libx265 only accept level option in -x265-params.
|
// libx265 only accept level option in -x265-params.
|
||||||
// level option may cause libx265 to fail.
|
// level option may cause libx265 to fail.
|
||||||
// libx265 cannot adjust the given level, just throw an error.
|
// libx265 cannot adjust the given level, just throw an error.
|
||||||
param += " -x265-params:0 subme=3:merange=25:rc-lookahead=10:me=star:ctu=32:max-tu-size=32:min-cu-size=16:rskip=2:rskip-edge-threshold=2:no-sao=1:no-strong-intra-smoothing=1:no-scenecut=1:no-open-gop=1:no-info=1";
|
param += " -x265-params:0 no-scenecut=1:no-open-gop=1:no-info=1";
|
||||||
|
|
||||||
|
if (encodingOptions.EncoderPreset < EncoderPreset.ultrafast)
|
||||||
|
{
|
||||||
|
// The following params are slower than the ultrafast preset, don't use when ultrafast is selected.
|
||||||
|
param += ":subme=3:merange=25:rc-lookahead=10:me=star:ctu=32:max-tu-size=32:min-cu-size=16:rskip=2:rskip-edge-threshold=2:no-sao=1:no-strong-intra-smoothing=1";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.Equals(videoEncoder, "libsvtav1", StringComparison.OrdinalIgnoreCase)
|
if (string.Equals(videoEncoder, "libsvtav1", StringComparison.OrdinalIgnoreCase)
|
||||||
@@ -5690,7 +5701,11 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
if (!string.IsNullOrEmpty(doScaling)
|
if (!string.IsNullOrEmpty(doScaling)
|
||||||
&& !IsScaleRatioSupported(inW, inH, reqW, reqH, reqMaxW, reqMaxH, 8.0f))
|
&& !IsScaleRatioSupported(inW, inH, reqW, reqH, reqMaxW, reqMaxH, 8.0f))
|
||||||
{
|
{
|
||||||
var hwScaleFilterFirstPass = $"scale_rkrga=w=iw/7.9:h=ih/7.9:format={outFormat}:afbc=1";
|
// Vendor provided BSP kernel has an RGA driver bug that causes the output to be corrupted for P010 format.
|
||||||
|
// Use NV15 instead of P010 to avoid the issue.
|
||||||
|
// SDR inputs are using BGRA formats already which is not affected.
|
||||||
|
var intermediateFormat = string.Equals(outFormat, "p010", StringComparison.OrdinalIgnoreCase) ? "nv15" : outFormat;
|
||||||
|
var hwScaleFilterFirstPass = $"scale_rkrga=w=iw/7.9:h=ih/7.9:format={intermediateFormat}:force_divisible_by=4:afbc=1";
|
||||||
mainFilters.Add(hwScaleFilterFirstPass);
|
mainFilters.Add(hwScaleFilterFirstPass);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -7064,7 +7079,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
{
|
{
|
||||||
// DTS and TrueHD are not supported by HLS
|
// DTS and TrueHD are not supported by HLS
|
||||||
// Keep them in the supported codecs list, but shift them to the end of the list so that if transcoding happens, another codec is used
|
// Keep them in the supported codecs list, but shift them to the end of the list so that if transcoding happens, another codec is used
|
||||||
shiftAudioCodecs.Add("dca");
|
shiftAudioCodecs.Add("dts");
|
||||||
shiftAudioCodecs.Add("truehd");
|
shiftAudioCodecs.Add("truehd");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -227,11 +227,11 @@ namespace MediaBrowser.Controller.Playlists
|
|||||||
return [item];
|
return [item];
|
||||||
}
|
}
|
||||||
|
|
||||||
public override bool IsVisible(User user)
|
public override bool IsVisible(User user, bool skipAllowedTagsCheck = false)
|
||||||
{
|
{
|
||||||
if (!IsSharedItem)
|
if (!IsSharedItem)
|
||||||
{
|
{
|
||||||
return base.IsVisible(user);
|
return base.IsVisible(user, skipAllowedTagsCheck);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (OpenAccess)
|
if (OpenAccess)
|
||||||
|
|||||||
@@ -122,7 +122,13 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||||||
_jsonSerializerOptions = new JsonSerializerOptions(JsonDefaults.Options);
|
_jsonSerializerOptions = new JsonSerializerOptions(JsonDefaults.Options);
|
||||||
_jsonSerializerOptions.Converters.Add(new JsonBoolStringConverter());
|
_jsonSerializerOptions.Converters.Add(new JsonBoolStringConverter());
|
||||||
|
|
||||||
var semaphoreCount = 2 * Environment.ProcessorCount;
|
// Although the type is not nullable, this might still be null during unit tests
|
||||||
|
var semaphoreCount = serverConfig.Configuration?.ParallelImageEncodingLimit ?? 0;
|
||||||
|
if (semaphoreCount < 1)
|
||||||
|
{
|
||||||
|
semaphoreCount = Environment.ProcessorCount;
|
||||||
|
}
|
||||||
|
|
||||||
_thumbnailResourcePool = new(semaphoreCount);
|
_thumbnailResourcePool = new(semaphoreCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
|||||||
public class SubtitleEditParser : ISubtitleParser
|
public class SubtitleEditParser : ISubtitleParser
|
||||||
{
|
{
|
||||||
private readonly ILogger<SubtitleEditParser> _logger;
|
private readonly ILogger<SubtitleEditParser> _logger;
|
||||||
private readonly Dictionary<string, SubtitleFormat[]> _subtitleFormats;
|
private readonly Dictionary<string, List<Type>> _subtitleFormatTypes;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="SubtitleEditParser"/> class.
|
/// Initializes a new instance of the <see cref="SubtitleEditParser"/> class.
|
||||||
@@ -26,10 +26,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
|||||||
public SubtitleEditParser(ILogger<SubtitleEditParser> logger)
|
public SubtitleEditParser(ILogger<SubtitleEditParser> logger)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_subtitleFormats = GetSubtitleFormats()
|
_subtitleFormatTypes = GetSubtitleFormatTypes();
|
||||||
.Where(subtitleFormat => !string.IsNullOrEmpty(subtitleFormat.Extension))
|
|
||||||
.GroupBy(subtitleFormat => subtitleFormat.Extension.TrimStart('.'), StringComparer.OrdinalIgnoreCase)
|
|
||||||
.ToDictionary(g => g.Key, g => g.ToArray(), StringComparer.OrdinalIgnoreCase);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@@ -38,13 +35,14 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
|||||||
var subtitle = new Subtitle();
|
var subtitle = new Subtitle();
|
||||||
var lines = stream.ReadAllLines().ToList();
|
var lines = stream.ReadAllLines().ToList();
|
||||||
|
|
||||||
if (!_subtitleFormats.TryGetValue(fileExtension, out var subtitleFormats))
|
if (!_subtitleFormatTypes.TryGetValue(fileExtension, out var subtitleFormatTypesForExtension))
|
||||||
{
|
{
|
||||||
throw new ArgumentException($"Unsupported file extension: {fileExtension}", nameof(fileExtension));
|
throw new ArgumentException($"Unsupported file extension: {fileExtension}", nameof(fileExtension));
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var subtitleFormat in subtitleFormats)
|
foreach (var subtitleFormatType in subtitleFormatTypesForExtension)
|
||||||
{
|
{
|
||||||
|
var subtitleFormat = (SubtitleFormat)Activator.CreateInstance(subtitleFormatType, true)!;
|
||||||
_logger.LogDebug(
|
_logger.LogDebug(
|
||||||
"Trying to parse '{FileExtension}' subtitle using the {SubtitleFormatParser} format parser",
|
"Trying to parse '{FileExtension}' subtitle using the {SubtitleFormatParser} format parser",
|
||||||
fileExtension,
|
fileExtension,
|
||||||
@@ -97,11 +95,11 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
|||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public bool SupportsFileExtension(string fileExtension)
|
public bool SupportsFileExtension(string fileExtension)
|
||||||
=> _subtitleFormats.ContainsKey(fileExtension);
|
=> _subtitleFormatTypes.ContainsKey(fileExtension);
|
||||||
|
|
||||||
private List<SubtitleFormat> GetSubtitleFormats()
|
private Dictionary<string, List<Type>> GetSubtitleFormatTypes()
|
||||||
{
|
{
|
||||||
var subtitleFormats = new List<SubtitleFormat>();
|
var subtitleFormatTypes = new Dictionary<string, List<Type>>(StringComparer.OrdinalIgnoreCase);
|
||||||
var assembly = typeof(SubtitleFormat).Assembly;
|
var assembly = typeof(SubtitleFormat).Assembly;
|
||||||
|
|
||||||
foreach (var type in assembly.GetTypes())
|
foreach (var type in assembly.GetTypes())
|
||||||
@@ -113,9 +111,20 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// It shouldn't be null, but the exception is caught if it is
|
var tempInstance = (SubtitleFormat)Activator.CreateInstance(type, true)!;
|
||||||
var subtitleFormat = (SubtitleFormat)Activator.CreateInstance(type, true)!;
|
var extension = tempInstance.Extension.TrimStart('.');
|
||||||
subtitleFormats.Add(subtitleFormat);
|
if (!string.IsNullOrEmpty(extension))
|
||||||
|
{
|
||||||
|
// Store only the type, we will instantiate from it later
|
||||||
|
if (!subtitleFormatTypes.TryGetValue(extension, out var subtitleFormatTypesForExtension))
|
||||||
|
{
|
||||||
|
subtitleFormatTypes[extension] = [type];
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
subtitleFormatTypesForExtension.Add(type);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -123,7 +132,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return subtitleFormats;
|
return subtitleFormatTypes;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -246,7 +246,7 @@ public class ServerConfiguration : BaseApplicationConfiguration
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets a value indicating whether older plugins should automatically be deleted from the plugin folder.
|
/// Gets or sets a value indicating whether older plugins should automatically be deleted from the plugin folder.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool RemoveOldPlugins { get; set; }
|
public bool RemoveOldPlugins { get; set; } = true;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets a value indicating whether clients should be allowed to upload logs.
|
/// Gets or sets a value indicating whether clients should be allowed to upload logs.
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ namespace MediaBrowser.Model.Dlna
|
|||||||
private readonly ITranscoderSupport _transcoderSupport;
|
private readonly ITranscoderSupport _transcoderSupport;
|
||||||
private static readonly string[] _supportedHlsVideoCodecs = ["h264", "hevc", "vp9", "av1"];
|
private static readonly string[] _supportedHlsVideoCodecs = ["h264", "hevc", "vp9", "av1"];
|
||||||
private static readonly string[] _supportedHlsAudioCodecsTs = ["aac", "ac3", "eac3", "mp3"];
|
private static readonly string[] _supportedHlsAudioCodecsTs = ["aac", "ac3", "eac3", "mp3"];
|
||||||
private static readonly string[] _supportedHlsAudioCodecsMp4 = ["aac", "ac3", "eac3", "mp3", "alac", "flac", "opus", "dca", "truehd"];
|
private static readonly string[] _supportedHlsAudioCodecsMp4 = ["aac", "ac3", "eac3", "mp3", "alac", "flac", "opus", "dts", "truehd"];
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="StreamBuilder"/> class.
|
/// Initializes a new instance of the <see cref="StreamBuilder"/> class.
|
||||||
@@ -862,18 +862,37 @@ namespace MediaBrowser.Model.Dlna
|
|||||||
|
|
||||||
if (options.AllowAudioStreamCopy)
|
if (options.AllowAudioStreamCopy)
|
||||||
{
|
{
|
||||||
if (ContainerHelper.ContainsContainer(transcodingProfile.AudioCodec, audioCodec))
|
// For Audio stream, we prefer the audio codec that can be directly copied, then the codec that can otherwise satisfies
|
||||||
|
// the transcoding conditions, then the one does not satisfy the transcoding conditions.
|
||||||
|
// For example: A client can support both aac and flac, but flac only supports 2 channels while aac supports 6.
|
||||||
|
// When the source audio is 6 channel flac, we should transcode to 6 channel aac, instead of down-mix to 2 channel flac.
|
||||||
|
var transcodingAudioCodecs = ContainerHelper.Split(transcodingProfile.AudioCodec);
|
||||||
|
|
||||||
|
foreach (var transcodingAudioCodec in transcodingAudioCodecs)
|
||||||
{
|
{
|
||||||
var appliedVideoConditions = options.Profile.CodecProfiles
|
var appliedVideoConditions = options.Profile.CodecProfiles
|
||||||
.Where(i => i.Type == CodecType.VideoAudio &&
|
.Where(i => i.Type == CodecType.VideoAudio &&
|
||||||
i.ContainsAnyCodec(audioCodec, container) &&
|
i.ContainsAnyCodec(transcodingAudioCodec, container) &&
|
||||||
i.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoAudioConditionSatisfied(applyCondition, audioChannels, audioBitrate, audioSampleRate, audioBitDepth, audioProfile, false)))
|
i.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoAudioConditionSatisfied(applyCondition, audioChannels, audioBitrate, audioSampleRate, audioBitDepth, audioProfile, false)))
|
||||||
.Select(i =>
|
.Select(i =>
|
||||||
i.Conditions.All(condition => ConditionProcessor.IsVideoAudioConditionSatisfied(condition, audioChannels, audioBitrate, audioSampleRate, audioBitDepth, audioProfile, false)));
|
i.Conditions.All(condition => ConditionProcessor.IsVideoAudioConditionSatisfied(condition, audioChannels, audioBitrate, audioSampleRate, audioBitDepth, audioProfile, false)));
|
||||||
|
|
||||||
// An empty appliedVideoConditions means that the codec has no conditions for the current audio stream
|
// An empty appliedVideoConditions means that the codec has no conditions for the current audio stream
|
||||||
var conditionsSatisfied = appliedVideoConditions.All(satisfied => satisfied);
|
var conditionsSatisfied = appliedVideoConditions.All(satisfied => satisfied);
|
||||||
rank.Audio = conditionsSatisfied ? 1 : 2;
|
|
||||||
|
var rankAudio = 3;
|
||||||
|
|
||||||
|
if (conditionsSatisfied)
|
||||||
|
{
|
||||||
|
rankAudio = string.Equals(transcodingAudioCodec, audioCodec, StringComparison.OrdinalIgnoreCase) ? 1 : 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
rank.Audio = Math.Min(rank.Audio, rankAudio);
|
||||||
|
|
||||||
|
if (rank.Audio == 1)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -963,9 +982,28 @@ namespace MediaBrowser.Model.Dlna
|
|||||||
|
|
||||||
var audioStreamWithSupportedCodec = candidateAudioStreams.Where(stream => ContainerHelper.ContainsContainer(audioCodecs, false, stream.Codec)).FirstOrDefault();
|
var audioStreamWithSupportedCodec = candidateAudioStreams.Where(stream => ContainerHelper.ContainsContainer(audioCodecs, false, stream.Codec)).FirstOrDefault();
|
||||||
|
|
||||||
var directAudioStream = audioStreamWithSupportedCodec?.Channels is not null && audioStreamWithSupportedCodec.Channels.Value <= (playlistItem.TranscodingMaxAudioChannels ?? int.MaxValue) ? audioStreamWithSupportedCodec : null;
|
var channelsExceedsLimit = audioStreamWithSupportedCodec is not null && audioStreamWithSupportedCodec.Channels > (playlistItem.TranscodingMaxAudioChannels ?? int.MaxValue);
|
||||||
|
|
||||||
var channelsExceedsLimit = audioStreamWithSupportedCodec is not null && directAudioStream is null;
|
var directAudioStreamSatisfied = audioStreamWithSupportedCodec is not null && !channelsExceedsLimit
|
||||||
|
&& options.Profile.CodecProfiles
|
||||||
|
.Where(i => i.Type == CodecType.VideoAudio
|
||||||
|
&& i.ContainsAnyCodec(audioStreamWithSupportedCodec.Codec, container)
|
||||||
|
&& i.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoAudioConditionSatisfied(applyCondition, audioStreamWithSupportedCodec.Channels, audioStreamWithSupportedCodec.BitRate, audioStreamWithSupportedCodec.SampleRate, audioStreamWithSupportedCodec.BitDepth, audioStreamWithSupportedCodec.Profile, false)))
|
||||||
|
.Select(i => i.Conditions.All(condition =>
|
||||||
|
{
|
||||||
|
var satisfied = ConditionProcessor.IsVideoAudioConditionSatisfied(condition, audioStreamWithSupportedCodec.Channels, audioStreamWithSupportedCodec.BitRate, audioStreamWithSupportedCodec.SampleRate, audioStreamWithSupportedCodec.BitDepth, audioStreamWithSupportedCodec.Profile, false);
|
||||||
|
if (!satisfied)
|
||||||
|
{
|
||||||
|
playlistItem.TranscodeReasons |= GetTranscodeReasonForFailedCondition(condition);
|
||||||
|
}
|
||||||
|
|
||||||
|
return satisfied;
|
||||||
|
}))
|
||||||
|
.All(satisfied => satisfied);
|
||||||
|
|
||||||
|
directAudioStreamSatisfied = directAudioStreamSatisfied && !playlistItem.TranscodeReasons.HasFlag(TranscodeReason.ContainerBitrateExceedsLimit);
|
||||||
|
|
||||||
|
var directAudioStream = directAudioStreamSatisfied ? audioStreamWithSupportedCodec : null;
|
||||||
|
|
||||||
if (channelsExceedsLimit && playlistItem.TargetAudioStream is not null)
|
if (channelsExceedsLimit && playlistItem.TargetAudioStream is not null)
|
||||||
{
|
{
|
||||||
@@ -2213,7 +2251,7 @@ namespace MediaBrowser.Model.Dlna
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool IsAudioDirectPlaySupported(DirectPlayProfile profile, MediaSourceInfo item, MediaStream audioStream)
|
private static bool IsAudioContainerSupported(DirectPlayProfile profile, MediaSourceInfo item)
|
||||||
{
|
{
|
||||||
// Check container type
|
// Check container type
|
||||||
if (!profile.SupportsContainer(item.Container))
|
if (!profile.SupportsContainer(item.Container))
|
||||||
@@ -2221,6 +2259,20 @@ namespace MediaBrowser.Model.Dlna
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Never direct play audio in matroska when the device only declare support for webm.
|
||||||
|
// The first check is not enough because mkv is assumed can be webm.
|
||||||
|
// See https://github.com/jellyfin/jellyfin/issues/13344
|
||||||
|
return !ContainerHelper.ContainsContainer("mkv", item.Container)
|
||||||
|
|| profile.SupportsContainer("mkv");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsAudioDirectPlaySupported(DirectPlayProfile profile, MediaSourceInfo item, MediaStream audioStream)
|
||||||
|
{
|
||||||
|
if (!IsAudioContainerSupported(profile, item))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// Check audio codec
|
// Check audio codec
|
||||||
string? audioCodec = audioStream?.Codec;
|
string? audioCodec = audioStream?.Codec;
|
||||||
if (!profile.SupportsAudioCodec(audioCodec))
|
if (!profile.SupportsAudioCodec(audioCodec))
|
||||||
@@ -2235,19 +2287,16 @@ namespace MediaBrowser.Model.Dlna
|
|||||||
{
|
{
|
||||||
// Check container type, this should NOT be supported
|
// Check container type, this should NOT be supported
|
||||||
// If the container is supported, the file should be directly played
|
// If the container is supported, the file should be directly played
|
||||||
if (!profile.SupportsContainer(item.Container))
|
if (IsAudioContainerSupported(profile, item))
|
||||||
{
|
{
|
||||||
// Check audio codec, we cannot use the SupportsAudioCodec here
|
return false;
|
||||||
// Because that one assumes empty container supports all codec, which is just useless
|
|
||||||
string? audioCodec = audioStream?.Codec;
|
|
||||||
if (string.Equals(profile.AudioCodec, audioCodec, StringComparison.OrdinalIgnoreCase) ||
|
|
||||||
string.Equals(profile.Container, audioCodec, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
// Check audio codec, we cannot use the SupportsAudioCodec here
|
||||||
|
// Because that one assumes empty container supports all codec, which is just useless
|
||||||
|
string? audioCodec = audioStream?.Codec;
|
||||||
|
return string.Equals(profile.AudioCodec, audioCodec, StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| string.Equals(profile.Container, audioCodec, StringComparison.OrdinalIgnoreCase);
|
||||||
}
|
}
|
||||||
|
|
||||||
private int GetRank(ref TranscodeReason a, TranscodeReason[] rankings)
|
private int GetRank(ref TranscodeReason a, TranscodeReason[] rankings)
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using System;
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using System.Xml.Serialization;
|
using System.Xml.Serialization;
|
||||||
using Jellyfin.Data.Enums;
|
using Jellyfin.Data.Enums;
|
||||||
@@ -6,6 +7,7 @@ namespace MediaBrowser.Model.Dlna;
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A class for transcoding profile information.
|
/// A class for transcoding profile information.
|
||||||
|
/// Note for client developers: Conditions defined in <see cref="CodecProfile"/> has higher priority and can override values defined here.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class TranscodingProfile
|
public class TranscodingProfile
|
||||||
{
|
{
|
||||||
@@ -17,6 +19,33 @@ public class TranscodingProfile
|
|||||||
Conditions = [];
|
Conditions = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="TranscodingProfile" /> class copying the values from another instance.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="other">Another instance of <see cref="TranscodingProfile" /> to be copied.</param>
|
||||||
|
public TranscodingProfile(TranscodingProfile other)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(other);
|
||||||
|
|
||||||
|
Container = other.Container;
|
||||||
|
Type = other.Type;
|
||||||
|
VideoCodec = other.VideoCodec;
|
||||||
|
AudioCodec = other.AudioCodec;
|
||||||
|
Protocol = other.Protocol;
|
||||||
|
EstimateContentLength = other.EstimateContentLength;
|
||||||
|
EnableMpegtsM2TsMode = other.EnableMpegtsM2TsMode;
|
||||||
|
TranscodeSeekInfo = other.TranscodeSeekInfo;
|
||||||
|
CopyTimestamps = other.CopyTimestamps;
|
||||||
|
Context = other.Context;
|
||||||
|
EnableSubtitlesInManifest = other.EnableSubtitlesInManifest;
|
||||||
|
MaxAudioChannels = other.MaxAudioChannels;
|
||||||
|
MinSegments = other.MinSegments;
|
||||||
|
SegmentLength = other.SegmentLength;
|
||||||
|
BreakOnNonKeyFrames = other.BreakOnNonKeyFrames;
|
||||||
|
Conditions = other.Conditions;
|
||||||
|
EnableAudioVbrEncoding = other.EnableAudioVbrEncoding;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the container.
|
/// Gets or sets the container.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel;
|
||||||
using Jellyfin.Data.Entities;
|
using Jellyfin.Data.Entities;
|
||||||
using Jellyfin.Data.Enums;
|
using Jellyfin.Data.Enums;
|
||||||
using MediaBrowser.Model.Drawing;
|
using MediaBrowser.Model.Drawing;
|
||||||
@@ -586,6 +587,7 @@ namespace MediaBrowser.Model.Dto
|
|||||||
/// Gets or sets the type of the media.
|
/// Gets or sets the type of the media.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>The type of the media.</value>
|
/// <value>The type of the media.</value>
|
||||||
|
[DefaultValue(MediaType.Unknown)]
|
||||||
public MediaType MediaType { get; set; }
|
public MediaType MediaType { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#nullable disable
|
#nullable disable
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using Jellyfin.Data.Enums;
|
using Jellyfin.Data.Enums;
|
||||||
using MediaBrowser.Model.Entities;
|
using MediaBrowser.Model.Entities;
|
||||||
@@ -34,6 +35,7 @@ namespace MediaBrowser.Model.Dto
|
|||||||
/// Gets or sets the type.
|
/// Gets or sets the type.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>The type.</value>
|
/// <value>The type.</value>
|
||||||
|
[DefaultValue(PersonKind.Unknown)]
|
||||||
public PersonKind Type { get; set; }
|
public PersonKind Type { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -157,6 +157,7 @@ namespace MediaBrowser.Model.Entities
|
|||||||
/// Gets the video range.
|
/// Gets the video range.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>The video range.</value>
|
/// <value>The video range.</value>
|
||||||
|
[DefaultValue(VideoRange.Unknown)]
|
||||||
public VideoRange VideoRange
|
public VideoRange VideoRange
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
@@ -171,6 +172,7 @@ namespace MediaBrowser.Model.Entities
|
|||||||
/// Gets the video range type.
|
/// Gets the video range type.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>The video range type.</value>
|
/// <value>The video range type.</value>
|
||||||
|
[DefaultValue(VideoRangeType.Unknown)]
|
||||||
public VideoRangeType VideoRangeType
|
public VideoRangeType VideoRangeType
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Authors>Jellyfin Contributors</Authors>
|
<Authors>Jellyfin Contributors</Authors>
|
||||||
<PackageId>Jellyfin.Model</PackageId>
|
<PackageId>Jellyfin.Model</PackageId>
|
||||||
<VersionPrefix>10.10.2</VersionPrefix>
|
<VersionPrefix>10.10.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>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.ComponentModel;
|
||||||
using Jellyfin.Data.Enums;
|
using Jellyfin.Data.Enums;
|
||||||
|
|
||||||
namespace MediaBrowser.Model.MediaSegments;
|
namespace MediaBrowser.Model.MediaSegments;
|
||||||
@@ -21,6 +22,7 @@ public class MediaSegmentDto
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the type of content this segment defines.
|
/// Gets or sets the type of content this segment defines.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[DefaultValue(MediaSegmentType.Unknown)]
|
||||||
public MediaSegmentType Type { get; set; }
|
public MediaSegmentType Type { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel;
|
||||||
using Jellyfin.Data.Enums;
|
using Jellyfin.Data.Enums;
|
||||||
|
|
||||||
namespace MediaBrowser.Model.Search
|
namespace MediaBrowser.Model.Search
|
||||||
@@ -115,6 +116,7 @@ namespace MediaBrowser.Model.Search
|
|||||||
/// Gets or sets the type of the media.
|
/// Gets or sets the type of the media.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>The type of the media.</value>
|
/// <value>The type of the media.</value>
|
||||||
|
[DefaultValue(MediaType.Unknown)]
|
||||||
public MediaType MediaType { get; set; }
|
public MediaType MediaType { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -291,6 +291,7 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
|
|
||||||
var fileStreamOptions = AsyncFile.WriteOptions;
|
var fileStreamOptions = AsyncFile.WriteOptions;
|
||||||
fileStreamOptions.Mode = FileMode.Create;
|
fileStreamOptions.Mode = FileMode.Create;
|
||||||
|
fileStreamOptions.Options = FileOptions.WriteThrough;
|
||||||
if (source.CanSeek)
|
if (source.CanSeek)
|
||||||
{
|
{
|
||||||
fileStreamOptions.PreallocationSize = source.Length;
|
fileStreamOptions.PreallocationSize = source.Length;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ using System.IO;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
|
using System.Net.Mime;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using MediaBrowser.Controller.Entities;
|
using MediaBrowser.Controller.Entities;
|
||||||
@@ -551,10 +552,16 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
||||||
await using (stream.ConfigureAwait(false))
|
await using (stream.ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
|
var mimetype = response.Content.Headers.ContentType?.MediaType;
|
||||||
|
if (mimetype is null || mimetype.Equals(MediaTypeNames.Application.Octet, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
mimetype = MimeTypes.GetMimeType(response.RequestMessage.RequestUri.GetLeftPart(UriPartial.Path));
|
||||||
|
}
|
||||||
|
|
||||||
await _providerManager.SaveImage(
|
await _providerManager.SaveImage(
|
||||||
item,
|
item,
|
||||||
stream,
|
stream,
|
||||||
response.Content.Headers.ContentType?.MediaType,
|
mimetype,
|
||||||
type,
|
type,
|
||||||
null,
|
null,
|
||||||
cancellationToken).ConfigureAwait(false);
|
cancellationToken).ConfigureAwait(false);
|
||||||
@@ -677,10 +684,16 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
||||||
await using (stream.ConfigureAwait(false))
|
await using (stream.ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
|
var mimetype = response.Content.Headers.ContentType?.MediaType;
|
||||||
|
if (mimetype is null || mimetype.Equals(MediaTypeNames.Application.Octet, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
mimetype = MimeTypes.GetMimeType(response.RequestMessage.RequestUri.GetLeftPart(UriPartial.Path));
|
||||||
|
}
|
||||||
|
|
||||||
await _providerManager.SaveImage(
|
await _providerManager.SaveImage(
|
||||||
item,
|
item,
|
||||||
stream,
|
stream,
|
||||||
response.Content.Headers.ContentType?.MediaType,
|
mimetype,
|
||||||
imageType,
|
imageType,
|
||||||
null,
|
null,
|
||||||
cancellationToken).ConfigureAwait(false);
|
cancellationToken).ConfigureAwait(false);
|
||||||
|
|||||||
@@ -1010,7 +1010,7 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (replaceData || !target.PremiereDate.HasValue)
|
if (replaceData || !target.PremiereDate.HasValue || (IsYearOnlyDate(target.PremiereDate.Value) && source.PremiereDate.HasValue))
|
||||||
{
|
{
|
||||||
target.PremiereDate = source.PremiereDate;
|
target.PremiereDate = source.PremiereDate;
|
||||||
}
|
}
|
||||||
@@ -1142,6 +1142,8 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool IsYearOnlyDate(DateTime date) => date.Month == 1 && date.Day == 1;
|
||||||
|
|
||||||
private static void MergePeople(List<PersonInfo> source, List<PersonInfo> target)
|
private static void MergePeople(List<PersonInfo> source, List<PersonInfo> target)
|
||||||
{
|
{
|
||||||
if (target is null)
|
if (target is null)
|
||||||
@@ -1149,13 +1151,24 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
target = new List<PersonInfo>();
|
target = new List<PersonInfo>();
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var person in target)
|
var sourceByName = source.ToLookup(p => p.Name.RemoveDiacritics(), StringComparer.OrdinalIgnoreCase);
|
||||||
{
|
var targetByName = target.ToLookup(p => p.Name.RemoveDiacritics(), StringComparer.OrdinalIgnoreCase);
|
||||||
var normalizedName = person.Name.RemoveDiacritics();
|
|
||||||
var personInSource = source.FirstOrDefault(i => string.Equals(i.Name.RemoveDiacritics(), normalizedName, StringComparison.OrdinalIgnoreCase));
|
|
||||||
|
|
||||||
if (personInSource is not null)
|
foreach (var name in targetByName.Select(g => g.Key))
|
||||||
|
{
|
||||||
|
var targetPeople = targetByName[name].ToArray();
|
||||||
|
var sourcePeople = sourceByName[name].ToArray();
|
||||||
|
|
||||||
|
if (sourcePeople.Length == 0)
|
||||||
{
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < targetPeople.Length; i++)
|
||||||
|
{
|
||||||
|
var person = targetPeople[i];
|
||||||
|
var personInSource = i < sourcePeople.Length ? sourcePeople[i] : sourcePeople[0];
|
||||||
|
|
||||||
foreach (var providerId in personInSource.ProviderIds)
|
foreach (var providerId in personInSource.ProviderIds)
|
||||||
{
|
{
|
||||||
person.ProviderIds.TryAdd(providerId.Key, providerId.Value);
|
person.ProviderIds.TryAdd(providerId.Key, providerId.Value);
|
||||||
@@ -1165,6 +1178,16 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
{
|
{
|
||||||
person.ImageUrl = personInSource.ImageUrl;
|
person.ImageUrl = personInSource.ImageUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(personInSource.Role) && string.IsNullOrWhiteSpace(person.Role))
|
||||||
|
{
|
||||||
|
person.Role = personInSource.Role;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (personInSource.SortOrder.HasValue && !person.SortOrder.HasValue)
|
||||||
|
{
|
||||||
|
person.SortOrder = personInSource.SortOrder;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -204,20 +204,10 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
{
|
{
|
||||||
contentType = MediaTypeNames.Image.Png;
|
contentType = MediaTypeNames.Image.Png;
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
|
||||||
throw new HttpRequestException("Invalid image received: contentType not set.", null, response.StatusCode);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TVDb will sometimes serve a rubbish 404 html page with a 200 OK code, because reasons...
|
// some providers don't correctly report media type, extract from url if no extension found
|
||||||
if (contentType.Equals(MediaTypeNames.Text.Html, StringComparison.OrdinalIgnoreCase))
|
if (contentType is null || contentType.Equals(MediaTypeNames.Application.Octet, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
|
||||||
throw new HttpRequestException("Invalid image received.", null, HttpStatusCode.NotFound);
|
|
||||||
}
|
|
||||||
|
|
||||||
// some iptv/epg providers don't correctly report media type, extract from url if no extension found
|
|
||||||
if (string.IsNullOrWhiteSpace(MimeTypes.ToExtension(contentType)))
|
|
||||||
{
|
{
|
||||||
// Strip query parameters from url to get actual path.
|
// Strip query parameters from url to get actual path.
|
||||||
contentType = MimeTypes.GetMimeType(new Uri(url).GetLeftPart(UriPartial.Path));
|
contentType = MimeTypes.GetMimeType(new Uri(url).GetLeftPart(UriPartial.Path));
|
||||||
@@ -225,7 +215,7 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
|
|
||||||
if (!contentType.StartsWith("image/", StringComparison.OrdinalIgnoreCase))
|
if (!contentType.StartsWith("image/", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
throw new HttpRequestException($"Request returned {contentType} instead of an image type", null, HttpStatusCode.NotFound);
|
throw new HttpRequestException($"Request returned '{contentType}' instead of an image type", null, HttpStatusCode.NotFound);
|
||||||
}
|
}
|
||||||
|
|
||||||
var responseBytes = await response.Content.ReadAsByteArrayAsync(cancellationToken).ConfigureAwait(false);
|
var responseBytes = await response.Content.ReadAsByteArrayAsync(cancellationToken).ConfigureAwait(false);
|
||||||
@@ -262,7 +252,7 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var fileStream = AsyncFile.OpenRead(source);
|
var fileStream = AsyncFile.OpenRead(source);
|
||||||
await new ImageSaver(_configurationManager, _libraryMonitor, _fileSystem, _logger).SaveImage(item, fileStream, mimeType, type, imageIndex, saveLocallyWithMedia, cancellationToken);
|
await new ImageSaver(_configurationManager, _libraryMonitor, _fileSystem, _logger).SaveImage(item, fileStream, mimeType, type, imageIndex, saveLocallyWithMedia, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -170,16 +170,20 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||||||
_logger.LogWarning("File {File} only has ID3v1 tags, some fields may be truncated", audio.Path);
|
_logger.LogWarning("File {File} only has ID3v1 tags, some fields may be truncated", audio.Path);
|
||||||
}
|
}
|
||||||
|
|
||||||
track.Title = string.IsNullOrEmpty(track.Title) ? mediaInfo.Name : track.Title;
|
// We should never use the property setter of the ATL.Track class.
|
||||||
track.Album = string.IsNullOrEmpty(track.Album) ? mediaInfo.Album : track.Album;
|
// That setter is meant for its own tag parser and external editor usage and will have unwanted side effects
|
||||||
track.Year ??= mediaInfo.ProductionYear;
|
// For example, setting the Year property will also set the Date property, which is not what we want here.
|
||||||
track.TrackNumber ??= mediaInfo.IndexNumber;
|
// To properly handle fallback values, we make a clone of those fields when valid.
|
||||||
track.DiscNumber ??= mediaInfo.ParentIndexNumber;
|
var trackTitle = string.IsNullOrEmpty(track.Title) ? mediaInfo.Name : track.Title;
|
||||||
|
var trackAlbum = string.IsNullOrEmpty(track.Album) ? mediaInfo.Album : track.Album;
|
||||||
|
var trackYear = track.Year is null or 0 ? mediaInfo.ProductionYear : track.Year;
|
||||||
|
var trackTrackNumber = track.TrackNumber is null or 0 ? mediaInfo.IndexNumber : track.TrackNumber;
|
||||||
|
var trackDiscNumber = track.DiscNumber is null or 0 ? mediaInfo.ParentIndexNumber : track.DiscNumber;
|
||||||
|
|
||||||
if (audio.SupportsPeople && !audio.LockedFields.Contains(MetadataField.Cast))
|
if (audio.SupportsPeople && !audio.LockedFields.Contains(MetadataField.Cast))
|
||||||
{
|
{
|
||||||
var people = new List<PersonInfo>();
|
var people = new List<PersonInfo>();
|
||||||
var albumArtists = string.IsNullOrEmpty(track.AlbumArtist) ? mediaInfo.AlbumArtists : track.AlbumArtist.Split(InternalValueSeparator);
|
var albumArtists = string.IsNullOrEmpty(track.AlbumArtist) ? [] : track.AlbumArtist.Split(InternalValueSeparator);
|
||||||
|
|
||||||
if (libraryOptions.UseCustomTagDelimiters)
|
if (libraryOptions.UseCustomTagDelimiters)
|
||||||
{
|
{
|
||||||
@@ -210,7 +214,7 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||||||
|
|
||||||
if (performers is null || performers.Length == 0)
|
if (performers is null || performers.Length == 0)
|
||||||
{
|
{
|
||||||
performers = string.IsNullOrEmpty(track.Artist) ? mediaInfo.Artists : track.Artist.Split(InternalValueSeparator);
|
performers = string.IsNullOrEmpty(track.Artist) ? [] : track.Artist.Split(InternalValueSeparator);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (libraryOptions.UseCustomTagDelimiters)
|
if (libraryOptions.UseCustomTagDelimiters)
|
||||||
@@ -271,22 +275,22 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!audio.LockedFields.Contains(MetadataField.Name) && !string.IsNullOrEmpty(track.Title))
|
if (!audio.LockedFields.Contains(MetadataField.Name) && !string.IsNullOrEmpty(trackTitle))
|
||||||
{
|
{
|
||||||
audio.Name = track.Title;
|
audio.Name = trackTitle;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.ReplaceAllMetadata)
|
if (options.ReplaceAllMetadata)
|
||||||
{
|
{
|
||||||
audio.Album = track.Album;
|
audio.Album = trackAlbum;
|
||||||
audio.IndexNumber = track.TrackNumber;
|
audio.IndexNumber = trackTrackNumber;
|
||||||
audio.ParentIndexNumber = track.DiscNumber;
|
audio.ParentIndexNumber = trackDiscNumber;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
audio.Album ??= track.Album;
|
audio.Album ??= trackAlbum;
|
||||||
audio.IndexNumber ??= track.TrackNumber;
|
audio.IndexNumber ??= trackTrackNumber;
|
||||||
audio.ParentIndexNumber ??= track.DiscNumber;
|
audio.ParentIndexNumber ??= trackDiscNumber;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (track.Date.HasValue)
|
if (track.Date.HasValue)
|
||||||
@@ -294,11 +298,12 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||||||
audio.PremiereDate = track.Date;
|
audio.PremiereDate = track.Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (track.Year.HasValue)
|
if (trackYear.HasValue)
|
||||||
{
|
{
|
||||||
var year = track.Year.Value;
|
var year = trackYear.Value;
|
||||||
audio.ProductionYear = year;
|
audio.ProductionYear = year;
|
||||||
|
|
||||||
|
// ATL library handles such fallback this with its own internal logic, but we also need to handle it here for the ffprobe fallbacks.
|
||||||
if (!audio.PremiereDate.HasValue)
|
if (!audio.PremiereDate.HasValue)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -307,14 +312,14 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||||||
}
|
}
|
||||||
catch (ArgumentOutOfRangeException ex)
|
catch (ArgumentOutOfRangeException ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "Error parsing YEAR tag in {File}. '{TagValue}' is an invalid year", audio.Path, track.Year);
|
_logger.LogError(ex, "Error parsing YEAR tag in {File}. '{TagValue}' is an invalid year", audio.Path, trackYear);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!audio.LockedFields.Contains(MetadataField.Genres))
|
if (!audio.LockedFields.Contains(MetadataField.Genres))
|
||||||
{
|
{
|
||||||
var genres = string.IsNullOrEmpty(track.Genre) ? mediaInfo.Genres : track.Genre.Split(InternalValueSeparator).Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
|
var genres = string.IsNullOrEmpty(track.Genre) ? [] : track.Genre.Split(InternalValueSeparator).Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
|
||||||
|
|
||||||
if (libraryOptions.UseCustomTagDelimiters)
|
if (libraryOptions.UseCustomTagDelimiters)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -55,13 +55,12 @@ namespace MediaBrowser.Providers.Plugins.Omdb
|
|||||||
|
|
||||||
if (info.SeriesProviderIds.TryGetValue(MetadataProvider.Imdb.ToString(), out string? seriesImdbId)
|
if (info.SeriesProviderIds.TryGetValue(MetadataProvider.Imdb.ToString(), out string? seriesImdbId)
|
||||||
&& !string.IsNullOrEmpty(seriesImdbId)
|
&& !string.IsNullOrEmpty(seriesImdbId)
|
||||||
&& info.IndexNumber.HasValue
|
&& info.IndexNumber.HasValue)
|
||||||
&& info.ParentIndexNumber.HasValue)
|
|
||||||
{
|
{
|
||||||
result.HasMetadata = await _omdbProvider.FetchEpisodeData(
|
result.HasMetadata = await _omdbProvider.FetchEpisodeData(
|
||||||
result,
|
result,
|
||||||
info.IndexNumber.Value,
|
info.IndexNumber.Value,
|
||||||
info.ParentIndexNumber.Value,
|
info.ParentIndexNumber ?? 1,
|
||||||
info.GetProviderId(MetadataProvider.Imdb),
|
info.GetProviderId(MetadataProvider.Imdb),
|
||||||
seriesImdbId,
|
seriesImdbId,
|
||||||
info.MetadataLanguage,
|
info.MetadataLanguage,
|
||||||
|
|||||||
@@ -63,10 +63,10 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
|
|||||||
return Enumerable.Empty<RemoteImageInfo>();
|
return Enumerable.Empty<RemoteImageInfo>();
|
||||||
}
|
}
|
||||||
|
|
||||||
var seasonNumber = episode.ParentIndexNumber;
|
var seasonNumber = episode.ParentIndexNumber ?? 1;
|
||||||
var episodeNumber = episode.IndexNumber;
|
var episodeNumber = episode.IndexNumber;
|
||||||
|
|
||||||
if (!seasonNumber.HasValue || !episodeNumber.HasValue)
|
if (!episodeNumber.HasValue)
|
||||||
{
|
{
|
||||||
return Enumerable.Empty<RemoteImageInfo>();
|
return Enumerable.Empty<RemoteImageInfo>();
|
||||||
}
|
}
|
||||||
@@ -75,7 +75,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
|
|||||||
|
|
||||||
// TODO use image languages if All Languages isn't toggled, but there's currently no way to get that value in here
|
// TODO use image languages if All Languages isn't toggled, but there's currently no way to get that value in here
|
||||||
var episodeResult = await _tmdbClientManager
|
var episodeResult = await _tmdbClientManager
|
||||||
.GetEpisodeAsync(seriesTmdbId, seasonNumber.Value, episodeNumber.Value, series.DisplayOrder, null, null, cancellationToken)
|
.GetEpisodeAsync(seriesTmdbId, seasonNumber, episodeNumber.Value, series.DisplayOrder, null, null, cancellationToken)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
var stills = episodeResult?.Images?.Stills;
|
var stills = episodeResult?.Images?.Stills;
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
|
|||||||
public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(EpisodeInfo searchInfo, CancellationToken cancellationToken)
|
public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(EpisodeInfo searchInfo, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
// The search query must either provide an episode number or date
|
// The search query must either provide an episode number or date
|
||||||
if (!searchInfo.IndexNumber.HasValue || !searchInfo.ParentIndexNumber.HasValue)
|
if (!searchInfo.IndexNumber.HasValue)
|
||||||
{
|
{
|
||||||
return Enumerable.Empty<RemoteSearchResult>();
|
return Enumerable.Empty<RemoteSearchResult>();
|
||||||
}
|
}
|
||||||
@@ -96,10 +96,10 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
|
|||||||
return metadataResult;
|
return metadataResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
var seasonNumber = info.ParentIndexNumber;
|
var seasonNumber = info.ParentIndexNumber ?? 1;
|
||||||
var episodeNumber = info.IndexNumber;
|
var episodeNumber = info.IndexNumber;
|
||||||
|
|
||||||
if (!seasonNumber.HasValue || !episodeNumber.HasValue)
|
if (!episodeNumber.HasValue)
|
||||||
{
|
{
|
||||||
return metadataResult;
|
return metadataResult;
|
||||||
}
|
}
|
||||||
@@ -112,7 +112,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
|
|||||||
List<TvEpisode>? result = null;
|
List<TvEpisode>? result = null;
|
||||||
for (int? episode = startindex; episode <= endindex; episode++)
|
for (int? episode = startindex; episode <= endindex; episode++)
|
||||||
{
|
{
|
||||||
var episodeInfo = await _tmdbClientManager.GetEpisodeAsync(seriesTmdbId, seasonNumber.Value, episode.Value, info.SeriesDisplayOrder, info.MetadataLanguage, TmdbUtils.GetImageLanguagesParam(info.MetadataLanguage), cancellationToken).ConfigureAwait(false);
|
var episodeInfo = await _tmdbClientManager.GetEpisodeAsync(seriesTmdbId, seasonNumber, episode.Value, info.SeriesDisplayOrder, info.MetadataLanguage, TmdbUtils.GetImageLanguagesParam(info.MetadataLanguage), cancellationToken).ConfigureAwait(false);
|
||||||
if (episodeInfo is not null)
|
if (episodeInfo is not null)
|
||||||
{
|
{
|
||||||
(result ??= new List<TvEpisode>()).Add(episodeInfo);
|
(result ??= new List<TvEpisode>()).Add(episodeInfo);
|
||||||
@@ -156,7 +156,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
episodeResult = await _tmdbClientManager
|
episodeResult = await _tmdbClientManager
|
||||||
.GetEpisodeAsync(seriesTmdbId, seasonNumber.Value, episodeNumber.Value, info.SeriesDisplayOrder, info.MetadataLanguage, TmdbUtils.GetImageLanguagesParam(info.MetadataLanguage), cancellationToken)
|
.GetEpisodeAsync(seriesTmdbId, seasonNumber, episodeNumber.Value, info.SeriesDisplayOrder, info.MetadataLanguage, TmdbUtils.GetImageLanguagesParam(info.MetadataLanguage), cancellationToken)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -140,38 +140,39 @@ namespace MediaBrowser.Providers.TV
|
|||||||
|
|
||||||
private void RemoveObsoleteEpisodes(Series series)
|
private void RemoveObsoleteEpisodes(Series series)
|
||||||
{
|
{
|
||||||
var episodes = series.GetEpisodes(null, new DtoOptions(), true).OfType<Episode>().ToList();
|
var episodesBySeason = series.GetEpisodes(null, new DtoOptions(), true)
|
||||||
var numberOfEpisodes = episodes.Count;
|
.OfType<Episode>()
|
||||||
// TODO: O(n^2), but can it be done faster without overcomplicating it?
|
.GroupBy(e => e.ParentIndexNumber)
|
||||||
for (var i = 0; i < numberOfEpisodes; i++)
|
.ToList();
|
||||||
|
|
||||||
|
foreach (var seasonEpisodes in episodesBySeason)
|
||||||
{
|
{
|
||||||
var currentEpisode = episodes[i];
|
List<Episode> nonPhysicalEpisodes = [];
|
||||||
// The outer loop only examines virtual episodes
|
List<Episode> physicalEpisodes = [];
|
||||||
if (!currentEpisode.IsVirtualItem)
|
foreach (var episode in seasonEpisodes)
|
||||||
{
|
{
|
||||||
continue;
|
if (episode.IsVirtualItem || episode.IsMissingEpisode)
|
||||||
|
{
|
||||||
|
nonPhysicalEpisodes.Add(episode);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
physicalEpisodes.Add(episode);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Virtual episodes without an episode number are practically orphaned and should be deleted
|
// Only consider non-physical episodes
|
||||||
if (!currentEpisode.IndexNumber.HasValue)
|
foreach (var episode in nonPhysicalEpisodes)
|
||||||
{
|
{
|
||||||
DeleteEpisode(currentEpisode);
|
// Episodes without an episode number are practically orphaned and should be deleted
|
||||||
continue;
|
// Episodes with a physical equivalent should be deleted (they are no longer missing)
|
||||||
}
|
var shouldKeep = episode.IndexNumber.HasValue && !physicalEpisodes.Any(e => e.ContainsEpisodeNumber(episode.IndexNumber.Value));
|
||||||
|
|
||||||
for (var j = i + 1; j < numberOfEpisodes; j++)
|
if (shouldKeep)
|
||||||
{
|
|
||||||
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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
DeleteEpisode(currentEpisode);
|
DeleteEpisode(episode);
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,23 +50,20 @@ namespace MediaBrowser.XbmcMetadata.Parsers
|
|||||||
{
|
{
|
||||||
case "id":
|
case "id":
|
||||||
{
|
{
|
||||||
// get ids from attributes
|
// Get ids from attributes
|
||||||
|
item.TrySetProviderId(MetadataProvider.Tmdb, reader.GetAttribute("TMDB"));
|
||||||
|
item.TrySetProviderId(MetadataProvider.Tvdb, reader.GetAttribute("TVDB"));
|
||||||
string? imdbId = reader.GetAttribute("IMDB");
|
string? imdbId = reader.GetAttribute("IMDB");
|
||||||
string? tmdbId = reader.GetAttribute("TMDB");
|
|
||||||
|
|
||||||
// read id from content
|
// Read id from content
|
||||||
|
// Content can be arbitrary according to Kodi wiki, so only parse if we are sure it matches a provider-specific schema
|
||||||
var contentId = reader.ReadElementContentAsString();
|
var contentId = reader.ReadElementContentAsString();
|
||||||
if (contentId.Contains("tt", StringComparison.Ordinal) && string.IsNullOrEmpty(imdbId))
|
if (string.IsNullOrEmpty(imdbId) && contentId.StartsWith("tt", StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
imdbId = contentId;
|
imdbId = contentId;
|
||||||
}
|
}
|
||||||
else if (string.IsNullOrEmpty(tmdbId))
|
|
||||||
{
|
|
||||||
tmdbId = contentId;
|
|
||||||
}
|
|
||||||
|
|
||||||
item.TrySetProviderId(MetadataProvider.Imdb, imdbId);
|
item.TrySetProviderId(MetadataProvider.Imdb, imdbId);
|
||||||
item.TrySetProviderId(MetadataProvider.Tmdb, tmdbId);
|
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -82,21 +79,13 @@ namespace MediaBrowser.XbmcMetadata.Parsers
|
|||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(val) && movie is not null)
|
if (!string.IsNullOrWhiteSpace(val) && movie is not null)
|
||||||
{
|
{
|
||||||
// TODO Handle this better later
|
try
|
||||||
if (!val.Contains('<', StringComparison.Ordinal))
|
|
||||||
{
|
{
|
||||||
movie.CollectionName = val;
|
ParseSetXml(val, movie);
|
||||||
}
|
}
|
||||||
else
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
try
|
Logger.LogError(ex, "Error parsing set node");
|
||||||
{
|
|
||||||
ParseSetXml(val, movie);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Logger.LogError(ex, "Error parsing set node");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,7 +128,12 @@ namespace MediaBrowser.XbmcMetadata.Parsers
|
|||||||
// Loop through each element
|
// Loop through each element
|
||||||
while (!reader.EOF && reader.ReadState == ReadState.Interactive)
|
while (!reader.EOF && reader.ReadState == ReadState.Interactive)
|
||||||
{
|
{
|
||||||
if (reader.NodeType == XmlNodeType.Element)
|
if (reader.NodeType == XmlNodeType.Text && reader.Depth == 1)
|
||||||
|
{
|
||||||
|
movie.CollectionName = reader.Value;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
else if (reader.NodeType == XmlNodeType.Element)
|
||||||
{
|
{
|
||||||
switch (reader.Name)
|
switch (reader.Name)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using System;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Xml;
|
using System.Xml;
|
||||||
using Emby.Naming.TV;
|
using Emby.Naming.TV;
|
||||||
@@ -48,16 +49,20 @@ namespace MediaBrowser.XbmcMetadata.Parsers
|
|||||||
{
|
{
|
||||||
case "id":
|
case "id":
|
||||||
{
|
{
|
||||||
item.TrySetProviderId(MetadataProvider.Imdb, reader.GetAttribute("IMDB"));
|
// Get ids from attributes
|
||||||
item.TrySetProviderId(MetadataProvider.Tmdb, reader.GetAttribute("TMDB"));
|
item.TrySetProviderId(MetadataProvider.Tmdb, reader.GetAttribute("TMDB"));
|
||||||
|
item.TrySetProviderId(MetadataProvider.Tvdb, reader.GetAttribute("TVDB"));
|
||||||
|
string? imdbId = reader.GetAttribute("IMDB");
|
||||||
|
|
||||||
string? tvdbId = reader.GetAttribute("TVDB");
|
// Read id from content
|
||||||
if (string.IsNullOrWhiteSpace(tvdbId))
|
// Content can be arbitrary according to Kodi wiki, so only parse if we are sure it matches a provider-specific schema
|
||||||
|
var contentId = reader.ReadElementContentAsString();
|
||||||
|
if (string.IsNullOrEmpty(imdbId) && contentId.StartsWith("tt", StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
tvdbId = reader.ReadElementContentAsString();
|
imdbId = contentId;
|
||||||
}
|
}
|
||||||
|
|
||||||
item.TrySetProviderId(MetadataProvider.Tvdb, tvdbId);
|
item.TrySetProviderId(MetadataProvider.Imdb, imdbId);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -115,7 +115,9 @@ namespace MediaBrowser.XbmcMetadata.Savers
|
|||||||
{
|
{
|
||||||
if (!string.IsNullOrEmpty(movie.CollectionName))
|
if (!string.IsNullOrEmpty(movie.CollectionName))
|
||||||
{
|
{
|
||||||
writer.WriteElementString("set", movie.CollectionName);
|
writer.WriteStartElement("set");
|
||||||
|
writer.WriteElementString("name", movie.CollectionName);
|
||||||
|
writer.WriteEndElement();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
|
||||||
[assembly: AssemblyVersion("10.10.2")]
|
[assembly: AssemblyVersion("10.10.7")]
|
||||||
[assembly: AssemblyFileVersion("10.10.2")]
|
[assembly: AssemblyFileVersion("10.10.7")]
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"sdk": {
|
"sdk": {
|
||||||
"version": "8.0.404",
|
"version": "8.0.0",
|
||||||
"rollForward": "latestMinor"
|
"rollForward": "latestMinor"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -195,8 +195,10 @@ public class SkiaEncoder : IImageEncoder
|
|||||||
return string.Empty;
|
return string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Use FileStream with FileShare.Read instead of having Skia open the file to allow concurrent read access
|
||||||
|
using var fileStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||||
// 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, fileStream, 128, 128);
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool RequiresSpecialCharacterHack(string path)
|
private bool RequiresSpecialCharacterHack(string path)
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable
|
|||||||
var semaphoreCount = config.Configuration.ParallelImageEncodingLimit;
|
var semaphoreCount = config.Configuration.ParallelImageEncodingLimit;
|
||||||
if (semaphoreCount < 1)
|
if (semaphoreCount < 1)
|
||||||
{
|
{
|
||||||
semaphoreCount = 2 * Environment.ProcessorCount;
|
semaphoreCount = Environment.ProcessorCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
_parallelEncodingLimit = new(semaphoreCount);
|
_parallelEncodingLimit = new(semaphoreCount);
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Authors>Jellyfin Contributors</Authors>
|
<Authors>Jellyfin Contributors</Authors>
|
||||||
<PackageId>Jellyfin.Extensions</PackageId>
|
<PackageId>Jellyfin.Extensions</PackageId>
|
||||||
<VersionPrefix>10.10.2</VersionPrefix>
|
<VersionPrefix>10.10.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>
|
||||||
|
|||||||
@@ -39,6 +39,11 @@ public class GuideManager : IGuideManager
|
|||||||
private readonly IRecordingsManager _recordingsManager;
|
private readonly IRecordingsManager _recordingsManager;
|
||||||
private readonly LiveTvDtoService _tvDtoService;
|
private readonly LiveTvDtoService _tvDtoService;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Amount of days images are pre-cached from external sources.
|
||||||
|
/// </summary>
|
||||||
|
public const int MaxCacheDays = 2;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="GuideManager"/> class.
|
/// Initializes a new instance of the <see cref="GuideManager"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -204,14 +209,14 @@ public class GuideManager : IGuideManager
|
|||||||
progress.Report(15);
|
progress.Report(15);
|
||||||
|
|
||||||
numComplete = 0;
|
numComplete = 0;
|
||||||
var programs = new List<Guid>();
|
var programIds = new List<Guid>();
|
||||||
var channels = new List<Guid>();
|
var channels = new List<Guid>();
|
||||||
|
|
||||||
var guideDays = GetGuideDays();
|
var guideDays = GetGuideDays();
|
||||||
|
|
||||||
_logger.LogInformation("Refreshing guide with {0} days of guide data", guideDays);
|
_logger.LogInformation("Refreshing guide with {Days} days of guide data", guideDays);
|
||||||
|
|
||||||
var maxCacheDate = DateTime.UtcNow.AddDays(2);
|
var maxCacheDate = DateTime.UtcNow.AddDays(MaxCacheDays);
|
||||||
foreach (var currentChannel in list)
|
foreach (var currentChannel in list)
|
||||||
{
|
{
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
@@ -238,11 +243,12 @@ public class GuideManager : IGuideManager
|
|||||||
}).Cast<LiveTvProgram>().ToDictionary(i => i.Id);
|
}).Cast<LiveTvProgram>().ToDictionary(i => i.Id);
|
||||||
|
|
||||||
var newPrograms = new List<LiveTvProgram>();
|
var newPrograms = new List<LiveTvProgram>();
|
||||||
var updatedPrograms = new List<BaseItem>();
|
var updatedPrograms = new List<LiveTvProgram>();
|
||||||
|
|
||||||
foreach (var program in channelPrograms)
|
foreach (var program in channelPrograms)
|
||||||
{
|
{
|
||||||
var (programItem, isNew, isUpdated) = GetProgram(program, existingPrograms, currentChannel);
|
var (programItem, isNew, isUpdated) = GetProgram(program, existingPrograms, currentChannel);
|
||||||
|
var id = programItem.Id;
|
||||||
if (isNew)
|
if (isNew)
|
||||||
{
|
{
|
||||||
newPrograms.Add(programItem);
|
newPrograms.Add(programItem);
|
||||||
@@ -252,7 +258,7 @@ public class GuideManager : IGuideManager
|
|||||||
updatedPrograms.Add(programItem);
|
updatedPrograms.Add(programItem);
|
||||||
}
|
}
|
||||||
|
|
||||||
programs.Add(programItem.Id);
|
programIds.Add(programItem.Id);
|
||||||
|
|
||||||
isMovie |= program.IsMovie;
|
isMovie |= program.IsMovie;
|
||||||
isSeries |= program.IsSeries;
|
isSeries |= program.IsSeries;
|
||||||
@@ -261,12 +267,17 @@ public class GuideManager : IGuideManager
|
|||||||
isKids |= program.IsKids;
|
isKids |= program.IsKids;
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.LogDebug("Channel {0} has {1} new programs and {2} updated programs", currentChannel.Name, newPrograms.Count, updatedPrograms.Count);
|
_logger.LogDebug(
|
||||||
|
"Channel {Name} has {NewCount} new programs and {UpdatedCount} updated programs",
|
||||||
|
currentChannel.Name,
|
||||||
|
newPrograms.Count,
|
||||||
|
updatedPrograms.Count);
|
||||||
|
|
||||||
if (newPrograms.Count > 0)
|
if (newPrograms.Count > 0)
|
||||||
{
|
{
|
||||||
_libraryManager.CreateItems(newPrograms, null, cancellationToken);
|
_libraryManager.CreateItems(newPrograms, currentChannel, cancellationToken);
|
||||||
await PrecacheImages(newPrograms, maxCacheDate).ConfigureAwait(false);
|
|
||||||
|
await PreCacheImages(newPrograms, maxCacheDate).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (updatedPrograms.Count > 0)
|
if (updatedPrograms.Count > 0)
|
||||||
@@ -276,7 +287,8 @@ public class GuideManager : IGuideManager
|
|||||||
currentChannel,
|
currentChannel,
|
||||||
ItemUpdateType.MetadataImport,
|
ItemUpdateType.MetadataImport,
|
||||||
cancellationToken).ConfigureAwait(false);
|
cancellationToken).ConfigureAwait(false);
|
||||||
await PrecacheImages(updatedPrograms, maxCacheDate).ConfigureAwait(false);
|
|
||||||
|
await PreCacheImages(updatedPrograms, maxCacheDate).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
currentChannel.IsMovie = isMovie;
|
currentChannel.IsMovie = isMovie;
|
||||||
@@ -313,7 +325,7 @@ public class GuideManager : IGuideManager
|
|||||||
}
|
}
|
||||||
|
|
||||||
progress.Report(100);
|
progress.Report(100);
|
||||||
return new Tuple<List<Guid>, List<Guid>>(channels, programs);
|
return new Tuple<List<Guid>, List<Guid>>(channels, programIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void CleanDatabase(Guid[] currentIdList, BaseItemKind[] validTypes, IProgress<double> progress, CancellationToken cancellationToken)
|
private void CleanDatabase(Guid[] currentIdList, BaseItemKind[] validTypes, IProgress<double> progress, CancellationToken cancellationToken)
|
||||||
@@ -488,35 +500,27 @@ public class GuideManager : IGuideManager
|
|||||||
forceUpdate = true;
|
forceUpdate = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
var seriesId = info.SeriesId;
|
var channelId = channel.Id;
|
||||||
|
if (!item.ParentId.Equals(channelId))
|
||||||
if (!item.ParentId.Equals(channel.Id))
|
|
||||||
{
|
{
|
||||||
|
item.ParentId = channel.Id;
|
||||||
forceUpdate = true;
|
forceUpdate = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
item.ParentId = channel.Id;
|
|
||||||
|
|
||||||
item.Audio = info.Audio;
|
item.Audio = info.Audio;
|
||||||
item.ChannelId = channel.Id;
|
item.ChannelId = channelId;
|
||||||
item.CommunityRating ??= info.CommunityRating;
|
item.CommunityRating = info.CommunityRating;
|
||||||
if ((item.CommunityRating ?? 0).Equals(0))
|
|
||||||
{
|
|
||||||
item.CommunityRating = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
item.EpisodeTitle = info.EpisodeTitle;
|
item.EpisodeTitle = info.EpisodeTitle;
|
||||||
item.ExternalId = info.Id;
|
item.ExternalId = info.Id;
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(seriesId) && !string.Equals(item.ExternalSeriesId, seriesId, StringComparison.Ordinal))
|
var seriesId = info.SeriesId;
|
||||||
|
if (!string.IsNullOrWhiteSpace(seriesId) && !string.Equals(item.ExternalSeriesId, seriesId, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
|
item.ExternalSeriesId = seriesId;
|
||||||
forceUpdate = true;
|
forceUpdate = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
item.ExternalSeriesId = seriesId;
|
|
||||||
|
|
||||||
var isSeries = info.IsSeries || !string.IsNullOrEmpty(info.EpisodeTitle);
|
var isSeries = info.IsSeries || !string.IsNullOrEmpty(info.EpisodeTitle);
|
||||||
|
|
||||||
if (isSeries || !string.IsNullOrEmpty(info.EpisodeTitle))
|
if (isSeries || !string.IsNullOrEmpty(info.EpisodeTitle))
|
||||||
{
|
{
|
||||||
item.SeriesName = info.Name;
|
item.SeriesName = info.Name;
|
||||||
@@ -564,7 +568,6 @@ public class GuideManager : IGuideManager
|
|||||||
}
|
}
|
||||||
|
|
||||||
item.Tags = tags.ToArray();
|
item.Tags = tags.ToArray();
|
||||||
|
|
||||||
item.Genres = info.Genres.ToArray();
|
item.Genres = info.Genres.ToArray();
|
||||||
|
|
||||||
if (info.IsHD ?? false)
|
if (info.IsHD ?? false)
|
||||||
@@ -575,41 +578,35 @@ public class GuideManager : IGuideManager
|
|||||||
|
|
||||||
item.IsMovie = info.IsMovie;
|
item.IsMovie = info.IsMovie;
|
||||||
item.IsRepeat = info.IsRepeat;
|
item.IsRepeat = info.IsRepeat;
|
||||||
|
|
||||||
if (item.IsSeries != isSeries)
|
if (item.IsSeries != isSeries)
|
||||||
{
|
{
|
||||||
|
item.IsSeries = isSeries;
|
||||||
forceUpdate = true;
|
forceUpdate = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
item.IsSeries = isSeries;
|
|
||||||
|
|
||||||
item.Name = info.Name;
|
item.Name = info.Name;
|
||||||
item.OfficialRating ??= info.OfficialRating;
|
item.OfficialRating = info.OfficialRating;
|
||||||
item.Overview ??= info.Overview;
|
item.Overview = info.Overview;
|
||||||
item.RunTimeTicks = (info.EndDate - info.StartDate).Ticks;
|
item.RunTimeTicks = (info.EndDate - info.StartDate).Ticks;
|
||||||
item.ProviderIds = info.ProviderIds;
|
|
||||||
|
|
||||||
foreach (var providerId in info.SeriesProviderIds)
|
foreach (var providerId in info.SeriesProviderIds)
|
||||||
{
|
{
|
||||||
info.ProviderIds["Series" + providerId.Key] = providerId.Value;
|
info.ProviderIds["Series" + providerId.Key] = providerId.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
item.ProviderIds = info.ProviderIds;
|
||||||
if (item.StartDate != info.StartDate)
|
if (item.StartDate != info.StartDate)
|
||||||
{
|
{
|
||||||
|
item.StartDate = info.StartDate;
|
||||||
forceUpdate = true;
|
forceUpdate = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
item.StartDate = info.StartDate;
|
|
||||||
|
|
||||||
if (item.EndDate != info.EndDate)
|
if (item.EndDate != info.EndDate)
|
||||||
{
|
{
|
||||||
|
item.EndDate = info.EndDate;
|
||||||
forceUpdate = true;
|
forceUpdate = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
item.EndDate = info.EndDate;
|
|
||||||
|
|
||||||
item.ProductionYear = info.ProductionYear;
|
item.ProductionYear = info.ProductionYear;
|
||||||
|
|
||||||
if (!isSeries || info.IsRepeat)
|
if (!isSeries || info.IsRepeat)
|
||||||
{
|
{
|
||||||
item.PremiereDate = info.OriginalAirDate;
|
item.PremiereDate = info.OriginalAirDate;
|
||||||
@@ -618,100 +615,113 @@ public class GuideManager : IGuideManager
|
|||||||
item.IndexNumber = info.EpisodeNumber;
|
item.IndexNumber = info.EpisodeNumber;
|
||||||
item.ParentIndexNumber = info.SeasonNumber;
|
item.ParentIndexNumber = info.SeasonNumber;
|
||||||
|
|
||||||
if (!item.HasImage(ImageType.Primary))
|
forceUpdate |= UpdateImages(item, info);
|
||||||
{
|
|
||||||
if (!string.IsNullOrWhiteSpace(info.ImagePath))
|
|
||||||
{
|
|
||||||
item.SetImage(
|
|
||||||
new ItemImageInfo
|
|
||||||
{
|
|
||||||
Path = info.ImagePath,
|
|
||||||
Type = ImageType.Primary
|
|
||||||
},
|
|
||||||
0);
|
|
||||||
}
|
|
||||||
else if (!string.IsNullOrWhiteSpace(info.ImageUrl))
|
|
||||||
{
|
|
||||||
item.SetImage(
|
|
||||||
new ItemImageInfo
|
|
||||||
{
|
|
||||||
Path = info.ImageUrl,
|
|
||||||
Type = ImageType.Primary
|
|
||||||
},
|
|
||||||
0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!item.HasImage(ImageType.Thumb))
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrWhiteSpace(info.ThumbImageUrl))
|
|
||||||
{
|
|
||||||
item.SetImage(
|
|
||||||
new ItemImageInfo
|
|
||||||
{
|
|
||||||
Path = info.ThumbImageUrl,
|
|
||||||
Type = ImageType.Thumb
|
|
||||||
},
|
|
||||||
0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!item.HasImage(ImageType.Logo))
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrWhiteSpace(info.LogoImageUrl))
|
|
||||||
{
|
|
||||||
item.SetImage(
|
|
||||||
new ItemImageInfo
|
|
||||||
{
|
|
||||||
Path = info.LogoImageUrl,
|
|
||||||
Type = ImageType.Logo
|
|
||||||
},
|
|
||||||
0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!item.HasImage(ImageType.Backdrop))
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrWhiteSpace(info.BackdropImageUrl))
|
|
||||||
{
|
|
||||||
item.SetImage(
|
|
||||||
new ItemImageInfo
|
|
||||||
{
|
|
||||||
Path = info.BackdropImageUrl,
|
|
||||||
Type = ImageType.Backdrop
|
|
||||||
},
|
|
||||||
0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var isUpdated = false;
|
|
||||||
if (isNew)
|
if (isNew)
|
||||||
{
|
{
|
||||||
|
item.OnMetadataChanged();
|
||||||
|
|
||||||
|
return (item, true, false);
|
||||||
}
|
}
|
||||||
else if (forceUpdate || string.IsNullOrWhiteSpace(info.Etag))
|
|
||||||
|
var isUpdated = forceUpdate;
|
||||||
|
var etag = info.Etag;
|
||||||
|
if (string.IsNullOrWhiteSpace(etag))
|
||||||
{
|
{
|
||||||
isUpdated = true;
|
isUpdated = true;
|
||||||
}
|
}
|
||||||
else
|
else if (!string.Equals(etag, item.GetProviderId(EtagKey), StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
var etag = info.Etag;
|
item.SetProviderId(EtagKey, etag);
|
||||||
|
isUpdated = true;
|
||||||
if (!string.Equals(etag, item.GetProviderId(EtagKey), StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
item.SetProviderId(EtagKey, etag);
|
|
||||||
isUpdated = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isNew || isUpdated)
|
if (isUpdated)
|
||||||
{
|
{
|
||||||
item.OnMetadataChanged();
|
item.OnMetadataChanged();
|
||||||
|
|
||||||
|
return (item, false, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (item, isNew, isUpdated);
|
return (item, false, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task PrecacheImages(IReadOnlyList<BaseItem> programs, DateTime maxCacheDate)
|
private static bool UpdateImages(BaseItem item, ProgramInfo info)
|
||||||
|
{
|
||||||
|
var updated = false;
|
||||||
|
|
||||||
|
// Primary
|
||||||
|
updated |= UpdateImage(ImageType.Primary, item, info);
|
||||||
|
|
||||||
|
// Thumbnail
|
||||||
|
updated |= UpdateImage(ImageType.Thumb, item, info);
|
||||||
|
|
||||||
|
// Logo
|
||||||
|
updated |= UpdateImage(ImageType.Logo, item, info);
|
||||||
|
|
||||||
|
// Backdrop
|
||||||
|
updated |= UpdateImage(ImageType.Backdrop, item, info);
|
||||||
|
|
||||||
|
return updated;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool UpdateImage(ImageType imageType, BaseItem item, ProgramInfo info)
|
||||||
|
{
|
||||||
|
var image = item.GetImages(imageType).FirstOrDefault();
|
||||||
|
var currentImagePath = image?.Path;
|
||||||
|
var newImagePath = imageType switch
|
||||||
|
{
|
||||||
|
ImageType.Primary => info.ImagePath,
|
||||||
|
_ => null
|
||||||
|
};
|
||||||
|
var newImageUrl = imageType switch
|
||||||
|
{
|
||||||
|
ImageType.Backdrop => info.BackdropImageUrl,
|
||||||
|
ImageType.Logo => info.LogoImageUrl,
|
||||||
|
ImageType.Primary => info.ImageUrl,
|
||||||
|
ImageType.Thumb => info.ThumbImageUrl,
|
||||||
|
_ => null
|
||||||
|
};
|
||||||
|
|
||||||
|
var sameImage = (currentImagePath?.Equals(newImageUrl, StringComparison.OrdinalIgnoreCase) ?? false)
|
||||||
|
|| (currentImagePath?.Equals(newImagePath, StringComparison.OrdinalIgnoreCase) ?? false);
|
||||||
|
if (sameImage)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(newImagePath))
|
||||||
|
{
|
||||||
|
item.SetImage(
|
||||||
|
new ItemImageInfo
|
||||||
|
{
|
||||||
|
Path = newImagePath,
|
||||||
|
Type = imageType
|
||||||
|
},
|
||||||
|
0);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(newImageUrl))
|
||||||
|
{
|
||||||
|
item.SetImage(
|
||||||
|
new ItemImageInfo
|
||||||
|
{
|
||||||
|
Path = newImageUrl,
|
||||||
|
Type = imageType
|
||||||
|
},
|
||||||
|
0);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
item.RemoveImage(image);
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task PreCacheImages(IReadOnlyList<BaseItem> programs, DateTime maxCacheDate)
|
||||||
{
|
{
|
||||||
await Parallel.ForEachAsync(
|
await Parallel.ForEachAsync(
|
||||||
programs
|
programs
|
||||||
@@ -730,6 +740,7 @@ public class GuideManager : IGuideManager
|
|||||||
var imageInfo = program.ImageInfos[i];
|
var imageInfo = program.ImageInfos[i];
|
||||||
if (!imageInfo.IsLocalFile)
|
if (!imageInfo.IsLocalFile)
|
||||||
{
|
{
|
||||||
|
_logger.LogDebug("Caching image locally: {Url}", imageInfo.Path);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
program.ImageInfos[i] = await _libraryManager.ConvertImageToLocal(
|
program.ImageInfos[i] = await _libraryManager.ConvertImageToLocal(
|
||||||
@@ -741,7 +752,7 @@ public class GuideManager : IGuideManager
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogWarning(ex, "Unable to precache {Url}", imageInfo.Path);
|
_logger.LogWarning(ex, "Unable to pre-cache {Url}", imageInfo.Path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ using System.Threading.Tasks;
|
|||||||
using AsyncKeyedLock;
|
using AsyncKeyedLock;
|
||||||
using Jellyfin.Extensions;
|
using Jellyfin.Extensions;
|
||||||
using Jellyfin.Extensions.Json;
|
using Jellyfin.Extensions.Json;
|
||||||
|
using Jellyfin.LiveTv.Guide;
|
||||||
using Jellyfin.LiveTv.Listings.SchedulesDirectDtos;
|
using Jellyfin.LiveTv.Listings.SchedulesDirectDtos;
|
||||||
using MediaBrowser.Common.Net;
|
using MediaBrowser.Common.Net;
|
||||||
using MediaBrowser.Controller.Authentication;
|
using MediaBrowser.Controller.Authentication;
|
||||||
@@ -38,7 +39,7 @@ namespace Jellyfin.LiveTv.Listings
|
|||||||
private readonly IHttpClientFactory _httpClientFactory;
|
private readonly IHttpClientFactory _httpClientFactory;
|
||||||
private readonly AsyncNonKeyedLocker _tokenLock = new(1);
|
private readonly AsyncNonKeyedLocker _tokenLock = new(1);
|
||||||
|
|
||||||
private readonly ConcurrentDictionary<string, NameValuePair> _tokens = new ConcurrentDictionary<string, NameValuePair>();
|
private readonly ConcurrentDictionary<string, NameValuePair> _tokens = new();
|
||||||
private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
|
private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
|
||||||
private DateTime _lastErrorResponse;
|
private DateTime _lastErrorResponse;
|
||||||
private bool _disposed = false;
|
private bool _disposed = false;
|
||||||
@@ -86,7 +87,7 @@ namespace Jellyfin.LiveTv.Listings
|
|||||||
{
|
{
|
||||||
_logger.LogWarning("SchedulesDirect token is empty, returning empty program list");
|
_logger.LogWarning("SchedulesDirect token is empty, returning empty program list");
|
||||||
|
|
||||||
return Enumerable.Empty<ProgramInfo>();
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
var dates = GetScheduleRequestDates(startDateUtc, endDateUtc);
|
var dates = GetScheduleRequestDates(startDateUtc, endDateUtc);
|
||||||
@@ -94,7 +95,7 @@ namespace Jellyfin.LiveTv.Listings
|
|||||||
_logger.LogInformation("Channel Station ID is: {ChannelID}", channelId);
|
_logger.LogInformation("Channel Station ID is: {ChannelID}", channelId);
|
||||||
var requestList = new List<RequestScheduleForChannelDto>()
|
var requestList = new List<RequestScheduleForChannelDto>()
|
||||||
{
|
{
|
||||||
new RequestScheduleForChannelDto()
|
new()
|
||||||
{
|
{
|
||||||
StationId = channelId,
|
StationId = channelId,
|
||||||
Date = dates
|
Date = dates
|
||||||
@@ -109,7 +110,7 @@ namespace Jellyfin.LiveTv.Listings
|
|||||||
var dailySchedules = await Request<IReadOnlyList<DayDto>>(options, true, info, cancellationToken).ConfigureAwait(false);
|
var dailySchedules = await Request<IReadOnlyList<DayDto>>(options, true, info, cancellationToken).ConfigureAwait(false);
|
||||||
if (dailySchedules is null)
|
if (dailySchedules is null)
|
||||||
{
|
{
|
||||||
return Array.Empty<ProgramInfo>();
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.LogDebug("Found {ScheduleCount} programs on {ChannelID} ScheduleDirect", dailySchedules.Count, channelId);
|
_logger.LogDebug("Found {ScheduleCount} programs on {ChannelID} ScheduleDirect", dailySchedules.Count, channelId);
|
||||||
@@ -120,17 +121,17 @@ namespace Jellyfin.LiveTv.Listings
|
|||||||
var programIds = dailySchedules.SelectMany(d => d.Programs.Select(s => s.ProgramId)).Distinct();
|
var programIds = dailySchedules.SelectMany(d => d.Programs.Select(s => s.ProgramId)).Distinct();
|
||||||
programRequestOptions.Content = JsonContent.Create(programIds, options: _jsonOptions);
|
programRequestOptions.Content = JsonContent.Create(programIds, options: _jsonOptions);
|
||||||
|
|
||||||
var programDetails = await Request<IReadOnlyList<ProgramDetailsDto>>(programRequestOptions, true, info, cancellationToken)
|
var programDetails = await Request<IReadOnlyList<ProgramDetailsDto>>(programRequestOptions, true, info, cancellationToken).ConfigureAwait(false);
|
||||||
.ConfigureAwait(false);
|
|
||||||
if (programDetails is null)
|
if (programDetails is null)
|
||||||
{
|
{
|
||||||
return Array.Empty<ProgramInfo>();
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
var programDict = programDetails.ToDictionary(p => p.ProgramId, y => y);
|
var programDict = programDetails.ToDictionary(p => p.ProgramId, y => y);
|
||||||
|
|
||||||
var programIdsWithImages = programDetails
|
var programIdsWithImages = programDetails
|
||||||
.Where(p => p.HasImageArtwork).Select(p => p.ProgramId)
|
.Where(p => p.HasImageArtwork)
|
||||||
|
.Select(p => p.ProgramId)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
var images = await GetImageForPrograms(info, programIdsWithImages, cancellationToken).ConfigureAwait(false);
|
var images = await GetImageForPrograms(info, programIdsWithImages, cancellationToken).ConfigureAwait(false);
|
||||||
@@ -138,17 +139,15 @@ namespace Jellyfin.LiveTv.Listings
|
|||||||
var programsInfo = new List<ProgramInfo>();
|
var programsInfo = new List<ProgramInfo>();
|
||||||
foreach (ProgramDto schedule in dailySchedules.SelectMany(d => d.Programs))
|
foreach (ProgramDto schedule in dailySchedules.SelectMany(d => d.Programs))
|
||||||
{
|
{
|
||||||
// _logger.LogDebug("Proccesing Schedule for statio ID " + stationID +
|
|
||||||
// " which corresponds to channel " + channelNumber + " and program id " +
|
|
||||||
// schedule.ProgramId + " which says it has images? " +
|
|
||||||
// programDict[schedule.ProgramId].hasImageArtwork);
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(schedule.ProgramId))
|
if (string.IsNullOrEmpty(schedule.ProgramId))
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (images is not null)
|
// Only add images which will be pre-cached until we can implement dynamic token fetching
|
||||||
|
var endDate = schedule.AirDateTime?.AddSeconds(schedule.Duration);
|
||||||
|
var willBeCached = endDate.HasValue && endDate.Value < DateTime.UtcNow.AddDays(GuideManager.MaxCacheDays);
|
||||||
|
if (willBeCached && images is not null)
|
||||||
{
|
{
|
||||||
var imageIndex = images.FindIndex(i => i.ProgramId == schedule.ProgramId[..10]);
|
var imageIndex = images.FindIndex(i => i.ProgramId == schedule.ProgramId[..10]);
|
||||||
if (imageIndex > -1)
|
if (imageIndex > -1)
|
||||||
@@ -456,7 +455,7 @@ namespace Jellyfin.LiveTv.Listings
|
|||||||
|
|
||||||
if (programIds.Count == 0)
|
if (programIds.Count == 0)
|
||||||
{
|
{
|
||||||
return Array.Empty<ShowImagesDto>();
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
StringBuilder str = new StringBuilder("[", 1 + (programIds.Count * 13));
|
StringBuilder str = new StringBuilder("[", 1 + (programIds.Count * 13));
|
||||||
@@ -483,7 +482,7 @@ namespace Jellyfin.LiveTv.Listings
|
|||||||
{
|
{
|
||||||
_logger.LogError(ex, "Error getting image info from schedules direct");
|
_logger.LogError(ex, "Error getting image info from schedules direct");
|
||||||
|
|
||||||
return Array.Empty<ShowImagesDto>();
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -689,10 +689,10 @@ public class NetworkManager : INetworkManager, IDisposable
|
|||||||
{
|
{
|
||||||
// Comma separated list of IP addresses or IP/netmask entries for networks that will be allowed to connect remotely.
|
// Comma separated list of IP addresses or IP/netmask entries for networks that will be allowed to connect remotely.
|
||||||
// If left blank, all remote addresses will be allowed.
|
// If left blank, all remote addresses will be allowed.
|
||||||
if (_remoteAddressFilter.Any() && !_lanSubnets.Any(x => x.Contains(remoteIP)))
|
if (_remoteAddressFilter.Any() && !IsInLocalNetwork(remoteIP))
|
||||||
{
|
{
|
||||||
// remoteAddressFilter is a whitelist or blacklist.
|
// remoteAddressFilter is a whitelist or blacklist.
|
||||||
var matches = _remoteAddressFilter.Count(remoteNetwork => remoteNetwork.Contains(remoteIP));
|
var matches = _remoteAddressFilter.Count(remoteNetwork => SubnetContainsAddress(remoteNetwork, remoteIP));
|
||||||
if ((!config.IsRemoteIPFilterBlacklist && matches > 0)
|
if ((!config.IsRemoteIPFilterBlacklist && matches > 0)
|
||||||
|| (config.IsRemoteIPFilterBlacklist && matches == 0))
|
|| (config.IsRemoteIPFilterBlacklist && matches == 0))
|
||||||
{
|
{
|
||||||
@@ -702,7 +702,7 @@ public class NetworkManager : INetworkManager, IDisposable
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (!_lanSubnets.Any(x => x.Contains(remoteIP)))
|
else if (!IsInLocalNetwork(remoteIP))
|
||||||
{
|
{
|
||||||
// Remote not enabled. So everyone should be LAN.
|
// Remote not enabled. So everyone should be LAN.
|
||||||
return false;
|
return false;
|
||||||
@@ -816,7 +816,7 @@ public class NetworkManager : INetworkManager, IDisposable
|
|||||||
_logger.LogWarning("IPv4 is disabled in Jellyfin, but enabled in the OS. This may affect how the interface is selected.");
|
_logger.LogWarning("IPv4 is disabled in Jellyfin, but enabled in the OS. This may affect how the interface is selected.");
|
||||||
}
|
}
|
||||||
|
|
||||||
bool isExternal = !_lanSubnets.Any(network => network.Contains(source));
|
bool isExternal = !IsInLocalNetwork(source);
|
||||||
_logger.LogDebug("Trying to get bind address for source {Source} - External: {IsExternal}", source, isExternal);
|
_logger.LogDebug("Trying to get bind address for source {Source} - External: {IsExternal}", source, isExternal);
|
||||||
|
|
||||||
if (!skipOverrides && MatchesPublishedServerUrl(source, isExternal, out result))
|
if (!skipOverrides && MatchesPublishedServerUrl(source, isExternal, out result))
|
||||||
@@ -863,7 +863,7 @@ public class NetworkManager : INetworkManager, IDisposable
|
|||||||
// (For systems with multiple internal network cards, and multiple subnets)
|
// (For systems with multiple internal network cards, and multiple subnets)
|
||||||
foreach (var intf in availableInterfaces)
|
foreach (var intf in availableInterfaces)
|
||||||
{
|
{
|
||||||
if (intf.Subnet.Contains(source))
|
if (SubnetContainsAddress(intf.Subnet, source))
|
||||||
{
|
{
|
||||||
result = NetworkUtils.FormatIPString(intf.Address);
|
result = NetworkUtils.FormatIPString(intf.Address);
|
||||||
_logger.LogDebug("{Source}: Found interface with matching subnet, using it as bind address: {Result}", source, result);
|
_logger.LogDebug("{Source}: Found interface with matching subnet, using it as bind address: {Result}", source, result);
|
||||||
@@ -891,21 +891,11 @@ public class NetworkManager : INetworkManager, IDisposable
|
|||||||
{
|
{
|
||||||
if (NetworkUtils.TryParseToSubnet(address, out var subnet))
|
if (NetworkUtils.TryParseToSubnet(address, out var subnet))
|
||||||
{
|
{
|
||||||
return IPAddress.IsLoopback(subnet.Prefix) || (_lanSubnets.Any(x => x.Contains(subnet.Prefix)) && !_excludedSubnets.Any(x => x.Contains(subnet.Prefix)));
|
return IsInLocalNetwork(subnet.Prefix);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (NetworkUtils.TryParseHost(address, out var addresses, IsIPv4Enabled, IsIPv6Enabled))
|
return NetworkUtils.TryParseHost(address, out var addresses, IsIPv4Enabled, IsIPv6Enabled)
|
||||||
{
|
&& addresses.Any(IsInLocalNetwork);
|
||||||
foreach (var ept in addresses)
|
|
||||||
{
|
|
||||||
if (IPAddress.IsLoopback(ept) || (_lanSubnets.Any(x => x.Contains(ept)) && !_excludedSubnets.Any(x => x.Contains(ept))))
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -919,6 +909,19 @@ public class NetworkManager : INetworkManager, IDisposable
|
|||||||
return NetworkConstants.IPv4RFC3927LinkLocal.Contains(address) || address.IsIPv6LinkLocal;
|
return NetworkConstants.IPv4RFC3927LinkLocal.Contains(address) || address.IsIPv6LinkLocal;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool SubnetContainsAddress(IPNetwork network, IPAddress address)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(address);
|
||||||
|
ArgumentNullException.ThrowIfNull(network);
|
||||||
|
|
||||||
|
if (address.IsIPv4MappedToIPv6)
|
||||||
|
{
|
||||||
|
address = address.MapToIPv4();
|
||||||
|
}
|
||||||
|
|
||||||
|
return network.Contains(address);
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public bool IsInLocalNetwork(IPAddress address)
|
public bool IsInLocalNetwork(IPAddress address)
|
||||||
{
|
{
|
||||||
@@ -940,6 +943,11 @@ public class NetworkManager : INetworkManager, IDisposable
|
|||||||
return CheckIfLanAndNotExcluded(address);
|
return CheckIfLanAndNotExcluded(address);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Check if the address is in the LAN and not excluded.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="address">The IP address to check. The caller should make sure this is not an IPv4MappedToIPv6 address.</param>
|
||||||
|
/// <returns>Boolean indicates whether the address is in LAN.</returns>
|
||||||
private bool CheckIfLanAndNotExcluded(IPAddress address)
|
private bool CheckIfLanAndNotExcluded(IPAddress address)
|
||||||
{
|
{
|
||||||
foreach (var lanSubnet in _lanSubnets)
|
foreach (var lanSubnet in _lanSubnets)
|
||||||
@@ -979,7 +987,7 @@ public class NetworkManager : INetworkManager, IDisposable
|
|||||||
{
|
{
|
||||||
// Only use matching internal subnets
|
// Only use matching internal subnets
|
||||||
// Prefer more specific (bigger subnet prefix) overrides
|
// Prefer more specific (bigger subnet prefix) overrides
|
||||||
validPublishedServerUrls = _publishedServerUrls.Where(x => x.IsInternalOverride && x.Data.Subnet.Contains(source))
|
validPublishedServerUrls = _publishedServerUrls.Where(x => x.IsInternalOverride && SubnetContainsAddress(x.Data.Subnet, source))
|
||||||
.OrderByDescending(x => x.Data.Subnet.PrefixLength)
|
.OrderByDescending(x => x.Data.Subnet.PrefixLength)
|
||||||
.ToList();
|
.ToList();
|
||||||
}
|
}
|
||||||
@@ -987,7 +995,7 @@ public class NetworkManager : INetworkManager, IDisposable
|
|||||||
{
|
{
|
||||||
// Only use matching external subnets
|
// Only use matching external subnets
|
||||||
// Prefer more specific (bigger subnet prefix) overrides
|
// Prefer more specific (bigger subnet prefix) overrides
|
||||||
validPublishedServerUrls = _publishedServerUrls.Where(x => x.IsExternalOverride && x.Data.Subnet.Contains(source))
|
validPublishedServerUrls = _publishedServerUrls.Where(x => x.IsExternalOverride && SubnetContainsAddress(x.Data.Subnet, source))
|
||||||
.OrderByDescending(x => x.Data.Subnet.PrefixLength)
|
.OrderByDescending(x => x.Data.Subnet.PrefixLength)
|
||||||
.ToList();
|
.ToList();
|
||||||
}
|
}
|
||||||
@@ -995,9 +1003,11 @@ public class NetworkManager : INetworkManager, IDisposable
|
|||||||
foreach (var data in validPublishedServerUrls)
|
foreach (var data in validPublishedServerUrls)
|
||||||
{
|
{
|
||||||
// Get interface matching override subnet
|
// Get interface matching override subnet
|
||||||
var intf = _interfaces.OrderBy(x => x.Index).FirstOrDefault(x => data.Data.Subnet.Contains(x.Address));
|
var intf = _interfaces.OrderBy(x => x.Index).FirstOrDefault(x => SubnetContainsAddress(data.Data.Subnet, x.Address));
|
||||||
|
|
||||||
if (intf?.Address is not null)
|
if (intf?.Address is not null
|
||||||
|
|| (data.Data.AddressFamily == AddressFamily.InterNetwork && data.Data.Address.Equals(IPAddress.Any))
|
||||||
|
|| (data.Data.AddressFamily == AddressFamily.InterNetworkV6 && data.Data.Address.Equals(IPAddress.IPv6Any)))
|
||||||
{
|
{
|
||||||
// If matching interface is found, use override
|
// If matching interface is found, use override
|
||||||
bindPreference = data.OverrideUri;
|
bindPreference = data.OverrideUri;
|
||||||
@@ -1025,6 +1035,7 @@ public class NetworkManager : INetworkManager, IDisposable
|
|||||||
}
|
}
|
||||||
|
|
||||||
_logger.LogDebug("{Source}: Matching bind address override found: {Address}", source, bindPreference);
|
_logger.LogDebug("{Source}: Matching bind address override found: {Address}", source, bindPreference);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1055,6 +1066,7 @@ public class NetworkManager : INetworkManager, IDisposable
|
|||||||
if (isInExternalSubnet)
|
if (isInExternalSubnet)
|
||||||
{
|
{
|
||||||
var externalInterfaces = _interfaces.Where(x => !IsInLocalNetwork(x.Address))
|
var externalInterfaces = _interfaces.Where(x => !IsInLocalNetwork(x.Address))
|
||||||
|
.Where(x => !IsLinkLocalAddress(x.Address))
|
||||||
.OrderBy(x => x.Index)
|
.OrderBy(x => x.Index)
|
||||||
.ToList();
|
.ToList();
|
||||||
if (externalInterfaces.Count > 0)
|
if (externalInterfaces.Count > 0)
|
||||||
@@ -1062,7 +1074,8 @@ public class NetworkManager : INetworkManager, IDisposable
|
|||||||
// Check to see if any of the external bind interfaces are in the same subnet as the source.
|
// Check to see if any of the external bind interfaces are in the same subnet as the source.
|
||||||
// If none exists, this will select the first external interface if there is one.
|
// If none exists, this will select the first external interface if there is one.
|
||||||
bindAddress = externalInterfaces
|
bindAddress = externalInterfaces
|
||||||
.OrderByDescending(x => x.Subnet.Contains(source))
|
.OrderByDescending(x => SubnetContainsAddress(x.Subnet, source))
|
||||||
|
.ThenByDescending(x => x.Subnet.PrefixLength)
|
||||||
.ThenBy(x => x.Index)
|
.ThenBy(x => x.Index)
|
||||||
.Select(x => x.Address)
|
.Select(x => x.Address)
|
||||||
.First();
|
.First();
|
||||||
@@ -1079,7 +1092,8 @@ public class NetworkManager : INetworkManager, IDisposable
|
|||||||
// Check to see if any of the internal bind interfaces are in the same subnet as the source.
|
// Check to see if any of the internal bind interfaces are in the same subnet as the source.
|
||||||
// If none exists, this will select the first internal interface if there is one.
|
// If none exists, this will select the first internal interface if there is one.
|
||||||
bindAddress = _interfaces.Where(x => IsInLocalNetwork(x.Address))
|
bindAddress = _interfaces.Where(x => IsInLocalNetwork(x.Address))
|
||||||
.OrderByDescending(x => x.Subnet.Contains(source))
|
.OrderByDescending(x => SubnetContainsAddress(x.Subnet, source))
|
||||||
|
.ThenByDescending(x => x.Subnet.PrefixLength)
|
||||||
.ThenBy(x => x.Index)
|
.ThenBy(x => x.Index)
|
||||||
.Select(x => x.Address)
|
.Select(x => x.Address)
|
||||||
.FirstOrDefault();
|
.FirstOrDefault();
|
||||||
@@ -1122,7 +1136,7 @@ public class NetworkManager : INetworkManager, IDisposable
|
|||||||
// (For systems with multiple network cards and/or multiple subnets)
|
// (For systems with multiple network cards and/or multiple subnets)
|
||||||
foreach (var intf in extResult)
|
foreach (var intf in extResult)
|
||||||
{
|
{
|
||||||
if (intf.Subnet.Contains(source))
|
if (SubnetContainsAddress(intf.Subnet, source))
|
||||||
{
|
{
|
||||||
result = NetworkUtils.FormatIPString(intf.Address);
|
result = NetworkUtils.FormatIPString(intf.Address);
|
||||||
_logger.LogDebug("{Source}: Found external interface with matching subnet, using it as bind address: {Result}", source, result);
|
_logger.LogDebug("{Source}: Found external interface with matching subnet, using it as bind address: {Result}", source, result);
|
||||||
|
|||||||
@@ -141,8 +141,10 @@ namespace Jellyfin.Providers.Tests.Manager
|
|||||||
{ "ProductionYear", 1, 2 },
|
{ "ProductionYear", 1, 2 },
|
||||||
{ "CommunityRating", 1.0f, 2.0f },
|
{ "CommunityRating", 1.0f, 2.0f },
|
||||||
{ "CriticRating", 1.0f, 2.0f },
|
{ "CriticRating", 1.0f, 2.0f },
|
||||||
{ "EndDate", DateTime.UnixEpoch, DateTime.Now },
|
{ "EndDate", DateTime.UnixEpoch, DateTime.UtcNow },
|
||||||
{ "PremiereDate", DateTime.UnixEpoch, DateTime.Now },
|
{ "PremiereDate", DateTime.UnixEpoch, DateTime.UtcNow },
|
||||||
|
{ "PremiereDate", new DateTime(1999, 1, 1, 0, 0, 0, DateTimeKind.Utc), DateTime.UtcNow },
|
||||||
|
{ "PremiereDate", new DateTime(2025, 2, 21, 0, 0, 0, DateTimeKind.Utc), DateTime.UtcNow },
|
||||||
{ "Video3DFormat", Video3DFormat.HalfSideBySide, Video3DFormat.FullSideBySide }
|
{ "Video3DFormat", Video3DFormat.HalfSideBySide, Video3DFormat.FullSideBySide }
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -151,7 +153,15 @@ namespace Jellyfin.Providers.Tests.Manager
|
|||||||
public void MergeBaseItemData_SimpleField_ReplacesAppropriately(string propName, object oldValue, object newValue)
|
public void MergeBaseItemData_SimpleField_ReplacesAppropriately(string propName, object oldValue, object newValue)
|
||||||
{
|
{
|
||||||
// Use type Movie to allow testing of Video3DFormat
|
// Use type Movie to allow testing of Video3DFormat
|
||||||
Assert.False(TestMergeBaseItemData<Movie, MovieInfo>(propName, oldValue, newValue, null, false, out _));
|
if (propName.Equals("PremiereDate", StringComparison.Ordinal) && oldValue is DateTime oldDateTime)
|
||||||
|
{
|
||||||
|
bool expectReplaced = oldDateTime.Month == 1 && oldDateTime.Day == 1;
|
||||||
|
Assert.Equal(TestMergeBaseItemData<Movie, MovieInfo>(propName, oldValue, newValue, null, false, out _), expectReplaced);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Assert.False(TestMergeBaseItemData<Movie, MovieInfo>(propName, oldValue, newValue, null, false, out _));
|
||||||
|
}
|
||||||
|
|
||||||
Assert.True(TestMergeBaseItemData<Movie, MovieInfo>(propName, oldValue, newValue, null, true, out _));
|
Assert.True(TestMergeBaseItemData<Movie, MovieInfo>(propName, oldValue, newValue, null, true, out _));
|
||||||
Assert.True(TestMergeBaseItemData<Movie, MovieInfo>(propName, null, newValue, null, false, out _));
|
Assert.True(TestMergeBaseItemData<Movie, MovieInfo>(propName, null, newValue, null, false, out _));
|
||||||
|
|||||||
+5
-2
@@ -1,6 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Emby.Server.Implementations.Localization;
|
using Emby.Server.Implementations.Localization;
|
||||||
using MediaBrowser.Controller.Configuration;
|
using MediaBrowser.Controller.Configuration;
|
||||||
@@ -84,7 +83,7 @@ namespace Jellyfin.Server.Implementations.Tests.Localization
|
|||||||
await localizationManager.LoadAll();
|
await localizationManager.LoadAll();
|
||||||
var ratings = localizationManager.GetParentalRatings().ToList();
|
var ratings = localizationManager.GetParentalRatings().ToList();
|
||||||
|
|
||||||
Assert.Equal(54, ratings.Count);
|
Assert.Equal(56, ratings.Count);
|
||||||
|
|
||||||
var tvma = ratings.FirstOrDefault(x => x.Name.Equals("TV-MA", StringComparison.Ordinal));
|
var tvma = ratings.FirstOrDefault(x => x.Name.Equals("TV-MA", StringComparison.Ordinal));
|
||||||
Assert.NotNull(tvma);
|
Assert.NotNull(tvma);
|
||||||
@@ -116,6 +115,10 @@ namespace Jellyfin.Server.Implementations.Tests.Localization
|
|||||||
[InlineData("TV-MA", "US", 17)]
|
[InlineData("TV-MA", "US", 17)]
|
||||||
[InlineData("XXX", "asdf", 1000)]
|
[InlineData("XXX", "asdf", 1000)]
|
||||||
[InlineData("Germany: FSK-18", "DE", 18)]
|
[InlineData("Germany: FSK-18", "DE", 18)]
|
||||||
|
[InlineData("Rated : R", "US", 17)]
|
||||||
|
[InlineData("Rated: R", "US", 17)]
|
||||||
|
[InlineData("Rated R", "US", 17)]
|
||||||
|
[InlineData(" PG-13 ", "US", 13)]
|
||||||
public async Task GetRatingLevel_GivenValidString_Success(string value, string countryCode, int expectedLevel)
|
public async Task GetRatingLevel_GivenValidString_Success(string value, string countryCode, int expectedLevel)
|
||||||
{
|
{
|
||||||
var localizationManager = Setup(new ServerConfiguration()
|
var localizationManager = Setup(new ServerConfiguration()
|
||||||
|
|||||||
@@ -257,5 +257,23 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers
|
|||||||
|
|
||||||
Assert.Throws<ArgumentException>(() => _parser.Fetch(result, string.Empty, CancellationToken.None));
|
Assert.Throws<ArgumentException>(() => _parser.Fetch(result, string.Empty, CancellationToken.None));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Parsing_Fields_With_Escaped_Xml_Special_Characters_Success()
|
||||||
|
{
|
||||||
|
var result = new MetadataResult<Video>()
|
||||||
|
{
|
||||||
|
Item = new Movie()
|
||||||
|
};
|
||||||
|
|
||||||
|
_parser.Fetch(result, "Test Data/Lilo & Stitch.nfo", CancellationToken.None);
|
||||||
|
var item = (Movie)result.Item;
|
||||||
|
|
||||||
|
Assert.Equal("Lilo & Stitch", item.Name);
|
||||||
|
Assert.Equal("Lilo & Stitch", item.OriginalTitle);
|
||||||
|
Assert.Equal("Lilo & Stitch Collection", item.CollectionName);
|
||||||
|
Assert.StartsWith(">>", item.Overview, StringComparison.InvariantCulture);
|
||||||
|
Assert.EndsWith("<<", item.Overview, StringComparison.InvariantCulture);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
|
||||||
|
<movie>
|
||||||
|
<title>Lilo & Stitch</title>
|
||||||
|
<originaltitle>Lilo & Stitch</originaltitle>
|
||||||
|
<set>Lilo & Stitch Collection</set>
|
||||||
|
<plot>>>As Stitch, a runaway genetic experiment from a faraway planet, wreaks havoc on the Hawaiian Islands, he becomes the mischievous adopted alien "puppy" of an independent little girl named Lilo and learns about loyalty, friendship, and ʻohana, the Hawaiian tradition of family.<<</plot>
|
||||||
|
</movie>
|
||||||
Reference in New Issue
Block a user