mirror of
https://github.com/jellyfin/jellyfin.git
synced 2025-04-20 20:18:33 -04:00
Compare commits
100 commits
Author | SHA1 | Date | |
---|---|---|---|
|
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 | ||
|
be23f4eb0d | ||
|
38c08c4fad | ||
|
1b4ab5e777 | ||
|
293e0f5faf | ||
|
13ae2266de | ||
|
6870e3496c | ||
|
ea88bdf2f3 | ||
|
a6f04ffb7c | ||
|
db266d75d6 | ||
|
f47d2c1f1a | ||
|
8bee67f1f8 | ||
|
cf11a2dc1e | ||
|
e2434d38c5 | ||
|
9e61a6fd72 | ||
|
d292fde9e2 | ||
|
25321d7f80 | ||
|
9c6454ec46 | ||
|
09c377fb6c | ||
|
97dc02b163 | ||
|
aa08d3f2bf | ||
|
2354cd45d4 | ||
|
c8ca0c72e1 | ||
|
3089e9e40a | ||
|
954950dc14 | ||
|
f6f4cdf9e7 | ||
|
3a9b48a2aa | ||
|
5769d5ca91 | ||
|
03271c43a7 | ||
|
bb30d26ffb | ||
|
e9ee0ef1f5 | ||
|
3aefbf8cf6 | ||
|
469bf9d514 | ||
|
a165883999 | ||
|
74d2c2addf | ||
|
096e1b2970 | ||
|
b0f44f1d5a | ||
|
584be05e93 | ||
|
3592c629e7 | ||
|
f99e0407fd | ||
|
fe9c6fb8ae | ||
|
54a6a33c01 | ||
|
0130580151 | ||
|
aa4dd04b99 | ||
|
c08d1d5b7f | ||
|
312ff4f3d8 | ||
|
c6629aebf8 | ||
|
016a7e5542 |
66 changed files with 740 additions and 353 deletions
|
@ -3,7 +3,7 @@
|
|||
"isRoot": true,
|
||||
"tools": {
|
||||
"dotnet-ef": {
|
||||
"version": "8.0.8",
|
||||
"version": "8.0.11",
|
||||
"commands": [
|
||||
"dotnet-ef"
|
||||
]
|
||||
|
|
|
@ -192,6 +192,8 @@
|
|||
- [jaina heartles](https://github.com/heartles)
|
||||
- [oxixes](https://github.com/oxixes)
|
||||
- [elfalem](https://github.com/elfalem)
|
||||
- [benedikt257](https://github.com/benedikt257)
|
||||
- [revam](https://github.com/revam)
|
||||
|
||||
# Emby Contributors
|
||||
|
||||
|
|
|
@ -9,29 +9,29 @@
|
|||
<PackageVersion Include="AutoFixture.Xunit2" Version="4.18.1" />
|
||||
<PackageVersion Include="AutoFixture" Version="4.18.1" />
|
||||
<PackageVersion Include="BDInfo" Version="0.8.0" />
|
||||
<PackageVersion Include="BlurHashSharp.SkiaSharp" Version="1.3.3" />
|
||||
<PackageVersion Include="BlurHashSharp" Version="1.3.3" />
|
||||
<PackageVersion Include="BlurHashSharp.SkiaSharp" Version="1.3.4" />
|
||||
<PackageVersion Include="BlurHashSharp" Version="1.3.4" />
|
||||
<PackageVersion Include="CommandLineParser" Version="2.9.1" />
|
||||
<PackageVersion Include="coverlet.collector" Version="6.0.2" />
|
||||
<PackageVersion Include="Diacritics" Version="3.3.29" />
|
||||
<PackageVersion Include="DiscUtils.Udf" Version="0.16.13" />
|
||||
<PackageVersion Include="DotNet.Glob" Version="3.1.3" />
|
||||
<PackageVersion Include="FsCheck.Xunit" Version="2.16.6" />
|
||||
<PackageVersion Include="HarfBuzzSharp.NativeAssets.Linux" Version="7.3.0.2" />
|
||||
<PackageVersion Include="HarfBuzzSharp.NativeAssets.Linux" Version="7.3.0.3" />
|
||||
<PackageVersion Include="ICU4N.Transliterator" Version="60.1.0-alpha.356" />
|
||||
<PackageVersion Include="IDisposableAnalyzers" Version="4.0.8" />
|
||||
<PackageVersion Include="Jellyfin.XmlTv" Version="10.8.0" />
|
||||
<PackageVersion Include="libse" Version="4.0.8" />
|
||||
<PackageVersion Include="LrcParser" Version="2024.0728.2" />
|
||||
<PackageVersion Include="MetaBrainz.MusicBrainz" Version="6.1.0" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="8.0.10" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.10" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="8.0.11" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.11" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4" />
|
||||
<PackageVersion Include="Microsoft.Data.Sqlite" Version="8.0.10" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.10" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.10" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.10" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.10" />
|
||||
<PackageVersion Include="Microsoft.Data.Sqlite" Version="8.0.11" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.11" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.11" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.11" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.11" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="8.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" />
|
||||
|
@ -40,8 +40,8 @@
|
|||
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="8.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.2" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="8.0.10" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="8.0.10" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="8.0.11" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="8.0.11" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Http" Version="8.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.2" />
|
||||
|
@ -51,7 +51,7 @@
|
|||
<PackageVersion Include="MimeTypes" Version="2.4.0" />
|
||||
<PackageVersion Include="Mono.Nat" Version="3.0.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="PlaylistsNET" Version="1.4.1" />
|
||||
<PackageVersion Include="prometheus-net.AspNetCore" Version="8.2.1" />
|
||||
|
@ -66,9 +66,9 @@
|
|||
<PackageVersion Include="Serilog.Sinks.Graylog" Version="3.1.1" />
|
||||
<PackageVersion Include="SerilogAnalyzer" Version="0.15.0" />
|
||||
<PackageVersion Include="SharpFuzz" Version="2.1.1" />
|
||||
<PackageVersion Include="SkiaSharp" Version="2.88.8" />
|
||||
<PackageVersion Include="SkiaSharp.HarfBuzz" Version="2.88.8" />
|
||||
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="2.88.8" />
|
||||
<PackageVersion Include="SkiaSharp" Version="2.88.9" />
|
||||
<PackageVersion Include="SkiaSharp.HarfBuzz" Version="2.88.9" />
|
||||
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="2.88.9" />
|
||||
<PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" />
|
||||
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
|
||||
<PackageVersion Include="Svg.Skia" Version="2.0.0.1" />
|
||||
|
@ -80,7 +80,7 @@
|
|||
<PackageVersion Include="System.Text.Json" Version="8.0.5" />
|
||||
<PackageVersion Include="System.Threading.Tasks.Dataflow" Version="8.0.1" />
|
||||
<PackageVersion Include="TagLibSharp" Version="2.3.0" />
|
||||
<PackageVersion Include="z440.atl.core" Version="6.6.0" />
|
||||
<PackageVersion Include="z440.atl.core" Version="6.11.0" />
|
||||
<PackageVersion Include="TMDbLib" Version="2.2.0" />
|
||||
<PackageVersion Include="UTF.Unknown" Version="2.5.1" />
|
||||
<PackageVersion Include="Xunit.Priority" Version="1.1.6" />
|
||||
|
@ -88,4 +88,4 @@
|
|||
<PackageVersion Include="Xunit.SkippableFact" Version="1.4.13" />
|
||||
<PackageVersion Include="xunit" Version="2.9.2" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
|
|
|
@ -36,7 +36,7 @@
|
|||
<PropertyGroup>
|
||||
<Authors>Jellyfin Contributors</Authors>
|
||||
<PackageId>Jellyfin.Naming</PackageId>
|
||||
<VersionPrefix>10.10.0</VersionPrefix>
|
||||
<VersionPrefix>10.10.5</VersionPrefix>
|
||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
|
|
|
@ -17,7 +17,6 @@ namespace Emby.Server.Implementations
|
|||
{ DefaultRedirectKey, "web/" },
|
||||
{ FfmpegProbeSizeKey, "1G" },
|
||||
{ FfmpegAnalyzeDurationKey, "200M" },
|
||||
{ PlaylistsAllowDuplicatesKey, bool.FalseString },
|
||||
{ BindToUnixSocketKey, bool.FalseString },
|
||||
{ SqliteCacheSizeKey, "20000" },
|
||||
{ FfmpegSkipValidationKey, bool.FalseString },
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
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 />
|
||||
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 enumerationOptions = GetEnumerationOptions(recursive);
|
||||
|
||||
|
|
|
@ -122,7 +122,6 @@ namespace Emby.Server.Implementations.Images
|
|||
}
|
||||
|
||||
await ProviderManager.SaveImage(item, outputPath, mimeType, imageType, null, false, cancellationToken).ConfigureAwait(false);
|
||||
File.Delete(outputPath);
|
||||
|
||||
return ItemUpdateType.ImageUpdate;
|
||||
}
|
||||
|
|
|
@ -28,7 +28,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
|
|||
{
|
||||
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));
|
||||
if (string.IsNullOrEmpty(filename))
|
||||
{
|
||||
|
|
|
@ -1,8 +1,14 @@
|
|||
Livre,0
|
||||
L,0
|
||||
ER,9
|
||||
AL,0
|
||||
ER,10
|
||||
10,10
|
||||
A10,10
|
||||
12,12
|
||||
A12,12
|
||||
14,14
|
||||
A14,14
|
||||
16,16
|
||||
A16,16
|
||||
18,18
|
||||
A18,18
|
||||
|
|
|
|
@ -6,8 +6,6 @@ TV-Y7,7
|
|||
TV-Y7-FV,7
|
||||
PG,9
|
||||
TV-PG,9
|
||||
PG-13,13
|
||||
13+,13
|
||||
TV-14,14
|
||||
14A,14
|
||||
16+,16
|
||||
|
|
|
|
@ -1,7 +1,7 @@
|
|||
A,0
|
||||
A/fig,0
|
||||
A/i,0
|
||||
A/fig/i,0
|
||||
A/i/fig,0
|
||||
APTA,0
|
||||
ERI,0
|
||||
TP,0
|
||||
|
|
|
|
@ -6,10 +6,11 @@ U,0
|
|||
6+,6
|
||||
7+,7
|
||||
PG,8
|
||||
9+,9
|
||||
9,9
|
||||
12,12
|
||||
12+,12
|
||||
12A,12
|
||||
12PG,12
|
||||
Teen,13
|
||||
13+,13
|
||||
14+,14
|
||||
|
|
|
|
@ -4,6 +4,7 @@ PG,12
|
|||
12A,12
|
||||
12PG,12
|
||||
15,15
|
||||
15PG,15
|
||||
15A,15
|
||||
16,16
|
||||
18,18
|
||||
|
|
|
|
@ -6,4 +6,5 @@ A,0
|
|||
12,12
|
||||
15,15
|
||||
18,18
|
||||
C,18
|
||||
Not approved,1001
|
||||
|
|
|
|
@ -10,6 +10,7 @@ R16,16
|
|||
RP16,16
|
||||
GA,18
|
||||
R18,18
|
||||
RP18,18
|
||||
MA,1000
|
||||
R,1001
|
||||
Objectionable,1001
|
||||
|
|
|
|
@ -5,23 +5,23 @@ TV-Y,0
|
|||
TV-Y7,7
|
||||
TV-Y7-FV,7
|
||||
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,1
|
||||
TV-PG-DLS,10
|
||||
TV-PG-DLV,10
|
||||
TV-PG-DSV,10
|
||||
TV-PG-LSV,10
|
||||
TV-PG-DLSV,10
|
||||
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-D,14
|
||||
TV-14-L,14
|
||||
|
@ -48,3 +48,5 @@ TV-MA-LS,17
|
|||
TV-MA-LV,17
|
||||
TV-MA-SV,17
|
||||
TV-MA-LSV,17
|
||||
TV-X,18
|
||||
TV-AO,18
|
||||
|
|
|
|
@ -216,14 +216,11 @@ namespace Emby.Server.Implementations.Playlists
|
|||
var newItems = GetPlaylistItems(newItemIds, user, options)
|
||||
.Where(i => i.SupportsAddingToPlaylist);
|
||||
|
||||
// Filter out duplicate items, if necessary
|
||||
if (!_appConfig.DoPlaylistsAllowDuplicates())
|
||||
{
|
||||
var existingIds = playlist.LinkedChildren.Select(c => c.ItemId).ToHashSet();
|
||||
newItems = newItems
|
||||
.Where(i => !existingIds.Contains(i.Id))
|
||||
.Distinct();
|
||||
}
|
||||
// Filter out duplicate items
|
||||
var existingIds = playlist.LinkedChildren.Select(c => c.ItemId).ToHashSet();
|
||||
newItems = newItems
|
||||
.Where(i => !existingIds.Contains(i.Id))
|
||||
.Distinct();
|
||||
|
||||
// Create a list of the new linked children to add to the playlist
|
||||
var childrenToAdd = newItems
|
||||
|
@ -269,7 +266,7 @@ namespace Emby.Server.Implementations.Playlists
|
|||
|
||||
var idList = entryIds.ToList();
|
||||
|
||||
var removals = children.Where(i => idList.Contains(i.Item1.Id));
|
||||
var removals = children.Where(i => idList.Contains(i.Item1.ItemId?.ToString("N", CultureInfo.InvariantCulture)));
|
||||
|
||||
playlist.LinkedChildren = children.Except(removals)
|
||||
.Select(i => i.Item1)
|
||||
|
@ -286,26 +283,39 @@ namespace Emby.Server.Implementations.Playlists
|
|||
RefreshPriority.High);
|
||||
}
|
||||
|
||||
public async Task MoveItemAsync(string playlistId, string entryId, int newIndex)
|
||||
public async Task MoveItemAsync(string playlistId, string entryId, int newIndex, Guid callingUserId)
|
||||
{
|
||||
if (_libraryManager.GetItemById(playlistId) is not Playlist playlist)
|
||||
{
|
||||
throw new ArgumentException("No Playlist exists with the supplied Id");
|
||||
}
|
||||
|
||||
var user = _userManager.GetUserById(callingUserId);
|
||||
var children = playlist.GetManageableItems().ToList();
|
||||
var accessibleChildren = children.Where(c => c.Item2.IsVisible(user)).ToArray();
|
||||
|
||||
var oldIndex = children.FindIndex(i => string.Equals(entryId, i.Item1.Id, StringComparison.OrdinalIgnoreCase));
|
||||
var oldIndexAll = children.FindIndex(i => string.Equals(entryId, i.Item1.ItemId?.ToString("N", CultureInfo.InvariantCulture), StringComparison.OrdinalIgnoreCase));
|
||||
var oldIndexAccessible = accessibleChildren.FindIndex(i => string.Equals(entryId, i.Item1.ItemId?.ToString("N", CultureInfo.InvariantCulture), StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (oldIndex == newIndex)
|
||||
if (oldIndexAccessible == newIndex)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var item = playlist.LinkedChildren[oldIndex];
|
||||
var newPriorItemIndex = newIndex > oldIndexAccessible ? newIndex : newIndex - 1 < 0 ? 0 : newIndex - 1;
|
||||
var newPriorItemId = accessibleChildren[newPriorItemIndex].Item1.ItemId;
|
||||
var newPriorItemIndexOnAllChildren = children.FindIndex(c => c.Item1.ItemId.Equals(newPriorItemId));
|
||||
var adjustedNewIndex = newPriorItemIndexOnAllChildren + 1;
|
||||
|
||||
var item = playlist.LinkedChildren.FirstOrDefault(i => string.Equals(entryId, i.ItemId?.ToString("N", CultureInfo.InvariantCulture), StringComparison.OrdinalIgnoreCase));
|
||||
if (item is null)
|
||||
{
|
||||
_logger.LogWarning("Modified item not found in playlist. ItemId: {ItemId}, PlaylistId: {PlaylistId}", item.ItemId, playlistId);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var newList = playlist.LinkedChildren.ToList();
|
||||
|
||||
newList.Remove(item);
|
||||
|
||||
if (newIndex >= newList.Count)
|
||||
|
@ -314,7 +324,7 @@ namespace Emby.Server.Implementations.Playlists
|
|||
}
|
||||
else
|
||||
{
|
||||
newList.Insert(newIndex, item);
|
||||
newList.Insert(adjustedNewIndex, item);
|
||||
}
|
||||
|
||||
playlist.LinkedChildren = [.. newList];
|
||||
|
|
|
@ -1938,7 +1938,11 @@ namespace Emby.Server.Implementations.Session
|
|||
// Don't report acceleration type for non-admin users.
|
||||
result = result.Select(r =>
|
||||
{
|
||||
r.TranscodingInfo.HardwareAccelerationType = HardwareAccelerationType.none;
|
||||
if (r.TranscodingInfo is not null)
|
||||
{
|
||||
r.TranscodingInfo.HardwareAccelerationType = HardwareAccelerationType.none;
|
||||
}
|
||||
|
||||
return r;
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1819,16 +1819,13 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||
if (isActualOutputVideoCodecHevc || isActualOutputVideoCodecAv1)
|
||||
{
|
||||
var requestedRange = state.GetRequestedRangeTypes(state.ActualOutputVideoCodec);
|
||||
var requestHasDOVI = requestedRange.Contains(VideoRangeType.DOVI.ToString(), StringComparison.OrdinalIgnoreCase);
|
||||
var requestHasDOVIWithHDR10 = requestedRange.Contains(VideoRangeType.DOVIWithHDR10.ToString(), StringComparison.OrdinalIgnoreCase);
|
||||
var requestHasDOVIWithHLG = requestedRange.Contains(VideoRangeType.DOVIWithHLG.ToString(), StringComparison.OrdinalIgnoreCase);
|
||||
var requestHasDOVIWithSDR = requestedRange.Contains(VideoRangeType.DOVIWithSDR.ToString(), StringComparison.OrdinalIgnoreCase);
|
||||
// Clients reporting Dolby Vision capabilities with fallbacks may only support the fallback layer.
|
||||
// Only enable Dolby Vision remuxing if the client explicitly declares support for profiles without fallbacks.
|
||||
var clientSupportsDoVi = requestedRange.Contains(VideoRangeType.DOVI.ToString(), StringComparison.OrdinalIgnoreCase);
|
||||
var videoIsDoVi = state.VideoStream.VideoRangeType is VideoRangeType.DOVI or VideoRangeType.DOVIWithHDR10 or VideoRangeType.DOVIWithHLG or VideoRangeType.DOVIWithSDR;
|
||||
|
||||
if (EncodingHelper.IsCopyCodec(codec)
|
||||
&& ((state.VideoStream.VideoRangeType == VideoRangeType.DOVI && requestHasDOVI)
|
||||
|| (state.VideoStream.VideoRangeType == VideoRangeType.DOVIWithHDR10 && requestHasDOVIWithHDR10)
|
||||
|| (state.VideoStream.VideoRangeType == VideoRangeType.DOVIWithHLG && requestHasDOVIWithHLG)
|
||||
|| (state.VideoStream.VideoRangeType == VideoRangeType.DOVIWithSDR && requestHasDOVIWithSDR)))
|
||||
&& (videoIsDoVi && clientSupportsDoVi))
|
||||
{
|
||||
if (isActualOutputVideoCodecHevc)
|
||||
{
|
||||
|
|
|
@ -50,6 +50,7 @@ public class ItemRefreshController : BaseJellyfinApiController
|
|||
/// <param name="imageRefreshMode">(Optional) Specifies the image refresh mode.</param>
|
||||
/// <param name="replaceAllMetadata">(Optional) Determines if metadata should be replaced. Only applicable if mode is FullRefresh.</param>
|
||||
/// <param name="replaceAllImages">(Optional) Determines if images should be replaced. Only applicable if mode is FullRefresh.</param>
|
||||
/// <param name="regenerateTrickplay">(Optional) Determines if trickplay images should be replaced. Only applicable if mode is FullRefresh.</param>
|
||||
/// <response code="204">Item metadata refresh queued.</response>
|
||||
/// <response code="404">Item to refresh not found.</response>
|
||||
/// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the item could not be found.</returns>
|
||||
|
@ -62,7 +63,8 @@ public class ItemRefreshController : BaseJellyfinApiController
|
|||
[FromQuery] MetadataRefreshMode metadataRefreshMode = MetadataRefreshMode.None,
|
||||
[FromQuery] MetadataRefreshMode imageRefreshMode = MetadataRefreshMode.None,
|
||||
[FromQuery] bool replaceAllMetadata = false,
|
||||
[FromQuery] bool replaceAllImages = false)
|
||||
[FromQuery] bool replaceAllImages = false,
|
||||
[FromQuery] bool regenerateTrickplay = false)
|
||||
{
|
||||
var item = _libraryManager.GetItemById<BaseItem>(itemId, User.GetUserId());
|
||||
if (item is null)
|
||||
|
@ -81,7 +83,8 @@ public class ItemRefreshController : BaseJellyfinApiController
|
|||
|| replaceAllImages
|
||||
|| replaceAllMetadata,
|
||||
IsAutomated = false,
|
||||
RemoveOldMetadata = replaceAllMetadata
|
||||
RemoveOldMetadata = replaceAllMetadata,
|
||||
RegenerateTrickplay = regenerateTrickplay
|
||||
};
|
||||
|
||||
_providerManager.QueueRefresh(item.Id, refreshOptions, RefreshPriority.High);
|
||||
|
|
|
@ -55,7 +55,7 @@ public class MediaSegmentsController : BaseJellyfinApiController
|
|||
return NotFound();
|
||||
}
|
||||
|
||||
var items = await _mediaSegmentManager.GetSegmentsAsync(item.Id, includeSegmentTypes).ConfigureAwait(false);
|
||||
var items = await _mediaSegmentManager.GetSegmentsAsync(item, includeSegmentTypes).ConfigureAwait(false);
|
||||
return Ok(new QueryResult<MediaSegmentDto>(items.ToArray()));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Api.Attributes;
|
||||
|
@ -426,7 +427,7 @@ public class PlaylistsController : BaseJellyfinApiController
|
|||
return Forbid();
|
||||
}
|
||||
|
||||
await _playlistManager.MoveItemAsync(playlistId, itemId, newIndex).ConfigureAwait(false);
|
||||
await _playlistManager.MoveItemAsync(playlistId, itemId, newIndex, callingUserId).ConfigureAwait(false);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
|
@ -514,7 +515,8 @@ public class PlaylistsController : BaseJellyfinApiController
|
|||
return Forbid();
|
||||
}
|
||||
|
||||
var items = playlist.GetManageableItems().ToArray();
|
||||
var user = _userManager.GetUserById(callingUserId);
|
||||
var items = playlist.GetManageableItems().Where(i => i.Item2.IsVisible(user)).ToArray();
|
||||
var count = items.Length;
|
||||
if (startIndex.HasValue)
|
||||
{
|
||||
|
@ -529,11 +531,11 @@ public class PlaylistsController : BaseJellyfinApiController
|
|||
var dtoOptions = new DtoOptions { Fields = fields }
|
||||
.AddClientFields(User)
|
||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
||||
var user = _userManager.GetUserById(callingUserId);
|
||||
|
||||
var dtos = _dtoService.GetBaseItemDtos(items.Select(i => i.Item2).ToList(), dtoOptions, user);
|
||||
for (int index = 0; index < dtos.Count; index++)
|
||||
{
|
||||
dtos[index].PlaylistItemId = items[index].Item1.Id;
|
||||
dtos[index].PlaylistItemId = items[index].Item1.ItemId?.ToString("N", CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
var result = new QueryResult<BaseItemDto>(
|
||||
|
|
|
@ -235,6 +235,11 @@ public static class StreamingHelpers
|
|||
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)
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
<PropertyGroup>
|
||||
<Authors>Jellyfin Contributors</Authors>
|
||||
<PackageId>Jellyfin.Data</PackageId>
|
||||
<VersionPrefix>10.10.0</VersionPrefix>
|
||||
<VersionPrefix>10.10.5</VersionPrefix>
|
||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
|
|
|
@ -139,23 +139,53 @@ public class MediaSegmentManager : IMediaSegmentManager
|
|||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IEnumerable<MediaSegmentDto>> GetSegmentsAsync(Guid itemId, IEnumerable<MediaSegmentType>? typeFilter)
|
||||
public async Task<IEnumerable<MediaSegmentDto>> GetSegmentsAsync(Guid itemId, IEnumerable<MediaSegmentType>? typeFilter, bool filterByProvider = true)
|
||||
{
|
||||
var baseItem = _libraryManager.GetItemById(itemId);
|
||||
|
||||
if (baseItem is null)
|
||||
{
|
||||
_logger.LogError("Tried to request segments for an invalid item");
|
||||
return [];
|
||||
}
|
||||
|
||||
return await GetSegmentsAsync(baseItem, typeFilter, filterByProvider).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IEnumerable<MediaSegmentDto>> GetSegmentsAsync(BaseItem item, IEnumerable<MediaSegmentType>? typeFilter, bool filterByProvider = true)
|
||||
{
|
||||
using var db = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
|
||||
|
||||
var query = db.MediaSegments
|
||||
.Where(e => e.ItemId.Equals(itemId));
|
||||
.Where(e => e.ItemId.Equals(item.Id));
|
||||
|
||||
if (typeFilter is not null)
|
||||
{
|
||||
query = query.Where(e => typeFilter.Contains(e.Type));
|
||||
}
|
||||
|
||||
if (filterByProvider)
|
||||
{
|
||||
var libraryOptions = _libraryManager.GetLibraryOptions(item);
|
||||
var providerIds = _segmentProviders
|
||||
.Where(e => !libraryOptions.DisabledMediaSegmentProviders.Contains(GetProviderId(e.Name)))
|
||||
.Select(f => GetProviderId(f.Name))
|
||||
.ToArray();
|
||||
if (providerIds.Length == 0)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
query = query.Where(e => providerIds.Contains(e.SegmentProviderId));
|
||||
}
|
||||
|
||||
return query
|
||||
.OrderBy(e => e.StartTicks)
|
||||
.AsNoTracking()
|
||||
.ToImmutableList()
|
||||
.Select(Map);
|
||||
.AsEnumerable()
|
||||
.Select(Map)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
private static MediaSegmentDto Map(MediaSegment segment)
|
||||
|
|
|
@ -194,6 +194,14 @@ public class TrickplayManager : ITrickplayManager
|
|||
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
|
||||
var actualWidth = 2 * (width / 2);
|
||||
|
||||
|
@ -238,7 +246,7 @@ public class TrickplayManager : ITrickplayManager
|
|||
foreach (var tile in existingFiles)
|
||||
{
|
||||
var image = _imageEncoder.GetImageSize(tile);
|
||||
localTrickplayInfo.Height = Math.Max(localTrickplayInfo.Height, image.Height);
|
||||
localTrickplayInfo.Height = Math.Max(localTrickplayInfo.Height, (int)Math.Ceiling((double)image.Height / localTrickplayInfo.TileHeight));
|
||||
var bitrate = (int)Math.Ceiling((decimal)new FileInfo(tile).Length * 8 / localTrickplayInfo.TileWidth / localTrickplayInfo.TileHeight / (localTrickplayInfo.Interval / 1000));
|
||||
localTrickplayInfo.Bandwidth = Math.Max(localTrickplayInfo.Bandwidth, bitrate);
|
||||
}
|
||||
|
|
|
@ -101,7 +101,7 @@ namespace Jellyfin.Server.Infrastructure
|
|||
count: null);
|
||||
}
|
||||
|
||||
private async Task SendFileAsync(string filePath, HttpResponse response, long offset, long? count)
|
||||
private async Task SendFileAsync(string filePath, HttpResponse response, long offset, long? count, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var fileInfo = GetFileInfo(filePath);
|
||||
if (offset < 0 || offset > fileInfo.Length)
|
||||
|
@ -118,6 +118,9 @@ namespace Jellyfin.Server.Infrastructure
|
|||
// Copied from SendFileFallback.SendFileAsync
|
||||
const int BufferSize = 1024 * 16;
|
||||
|
||||
var useRequestAborted = !cancellationToken.CanBeCanceled;
|
||||
var localCancel = useRequestAborted ? response.HttpContext.RequestAborted : cancellationToken;
|
||||
|
||||
var fileStream = new FileStream(
|
||||
filePath,
|
||||
FileMode.Open,
|
||||
|
@ -127,10 +130,17 @@ namespace Jellyfin.Server.Infrastructure
|
|||
options: FileOptions.Asynchronous | FileOptions.SequentialScan);
|
||||
await using (fileStream.ConfigureAwait(false))
|
||||
{
|
||||
fileStream.Seek(offset, SeekOrigin.Begin);
|
||||
await StreamCopyOperation
|
||||
.CopyToAsync(fileStream, response.Body, count, BufferSize, CancellationToken.None)
|
||||
.ConfigureAwait(true);
|
||||
try
|
||||
{
|
||||
localCancel.ThrowIfCancellationRequested();
|
||||
fileStream.Seek(offset, SeekOrigin.Begin);
|
||||
await StreamCopyOperation
|
||||
.CopyToAsync(fileStream, response.Body, count, BufferSize, localCancel)
|
||||
.ConfigureAwait(true);
|
||||
}
|
||||
catch (OperationCanceledException) when (useRequestAborted)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -47,9 +47,12 @@ namespace Jellyfin.Server.Migrations
|
|||
typeof(Routines.AddDefaultCastReceivers),
|
||||
typeof(Routines.UpdateDefaultPluginRepository),
|
||||
typeof(Routines.FixAudioData),
|
||||
typeof(Routines.MoveTrickplayFiles)
|
||||
typeof(Routines.MoveTrickplayFiles),
|
||||
typeof(Routines.RemoveDuplicatePlaylistChildren)
|
||||
};
|
||||
|
||||
private static readonly Guid _downgradeCheckMigration = Guid.Parse("36445464-849f-429f-9ad0-bb130efa0664");
|
||||
|
||||
/// <summary>
|
||||
/// Run all needed migrations.
|
||||
/// </summary>
|
||||
|
@ -87,6 +90,12 @@ namespace Jellyfin.Server.Migrations
|
|||
? (MigrationOptions)xmlSerializer.DeserializeFromFile(typeof(MigrationOptions), migrationConfigPath)!
|
||||
: 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
|
||||
var serverConfig = File.Exists(appPaths.SystemConfigurationFilePath)
|
||||
? (ServerConfiguration)xmlSerializer.DeserializeFromFile(typeof(ServerConfiguration), appPaths.SystemConfigurationFilePath)!
|
||||
|
|
|
@ -15,12 +15,12 @@ namespace Jellyfin.Server.Migrations.Routines;
|
|||
/// </summary>
|
||||
internal class FixPlaylistOwner : IMigrationRoutine
|
||||
{
|
||||
private readonly ILogger<RemoveDuplicateExtras> _logger;
|
||||
private readonly ILogger<FixPlaylistOwner> _logger;
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly IPlaylistManager _playlistManager;
|
||||
|
||||
public FixPlaylistOwner(
|
||||
ILogger<RemoveDuplicateExtras> logger,
|
||||
ILogger<FixPlaylistOwner> logger,
|
||||
ILibraryManager libraryManager,
|
||||
IPlaylistManager playlistManager)
|
||||
{
|
||||
|
|
|
@ -30,7 +30,7 @@ namespace Jellyfin.Server.Migrations.Routines
|
|||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Guid Id => Guid.Parse("{67445D54-B895-4B24-9F4C-35CE0690EA07}");
|
||||
public Guid Id => Guid.Parse("{D34BFC33-5D2E-4790-8085-069EF6EECB4E}");
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string Name => "MigrateRatingLevels";
|
||||
|
|
|
@ -0,0 +1,69 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Playlists;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Jellyfin.Server.Migrations.Routines;
|
||||
|
||||
/// <summary>
|
||||
/// Remove duplicate playlist entries.
|
||||
/// </summary>
|
||||
internal class RemoveDuplicatePlaylistChildren : IMigrationRoutine
|
||||
{
|
||||
private readonly ILogger<RemoveDuplicatePlaylistChildren> _logger;
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly IPlaylistManager _playlistManager;
|
||||
|
||||
public RemoveDuplicatePlaylistChildren(
|
||||
ILogger<RemoveDuplicatePlaylistChildren> logger,
|
||||
ILibraryManager libraryManager,
|
||||
IPlaylistManager playlistManager)
|
||||
{
|
||||
_logger = logger;
|
||||
_libraryManager = libraryManager;
|
||||
_playlistManager = playlistManager;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Guid Id => Guid.Parse("{96C156A2-7A13-4B3B-A8B8-FB80C94D20C0}");
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string Name => "RemoveDuplicatePlaylistChildren";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool PerformOnNewInstall => false;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Perform()
|
||||
{
|
||||
var playlists = _libraryManager.GetItemList(new InternalItemsQuery
|
||||
{
|
||||
IncludeItemTypes = [BaseItemKind.Playlist]
|
||||
})
|
||||
.Cast<Playlist>()
|
||||
.Where(p => !p.OpenAccess || !p.OwnerUserId.Equals(Guid.Empty))
|
||||
.ToArray();
|
||||
|
||||
if (playlists.Length > 0)
|
||||
{
|
||||
foreach (var playlist in playlists)
|
||||
{
|
||||
var linkedChildren = playlist.LinkedChildren;
|
||||
if (linkedChildren.Length > 0)
|
||||
{
|
||||
var nullItemChildren = linkedChildren.Where(c => c.ItemId is null);
|
||||
var deduplicatedChildren = linkedChildren.DistinctBy(c => c.ItemId);
|
||||
var newLinkedChildren = nullItemChildren.Concat(deduplicatedChildren);
|
||||
playlist.LinkedChildren = linkedChildren;
|
||||
playlist.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).GetAwaiter().GetResult();
|
||||
_playlistManager.SavePlaylistFile(playlist);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -8,7 +8,7 @@
|
|||
<PropertyGroup>
|
||||
<Authors>Jellyfin Contributors</Authors>
|
||||
<PackageId>Jellyfin.Common</PackageId>
|
||||
<VersionPrefix>10.10.0</VersionPrefix>
|
||||
<VersionPrefix>10.10.5</VersionPrefix>
|
||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace MediaBrowser.Controller.Entities
|
||||
{
|
||||
|
@ -12,7 +11,6 @@ namespace MediaBrowser.Controller.Entities
|
|||
{
|
||||
public LinkedChild()
|
||||
{
|
||||
Id = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
public string Path { get; set; }
|
||||
|
@ -21,9 +19,6 @@ namespace MediaBrowser.Controller.Entities
|
|||
|
||||
public string LibraryItemId { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public string Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the linked item id.
|
||||
/// </summary>
|
||||
|
@ -31,6 +26,8 @@ namespace MediaBrowser.Controller.Entities
|
|||
|
||||
public static LinkedChild Create(BaseItem item)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(item);
|
||||
|
||||
var child = new LinkedChild
|
||||
{
|
||||
Path = item.Path,
|
||||
|
|
|
@ -49,11 +49,6 @@ namespace MediaBrowser.Controller.Extensions
|
|||
/// </summary>
|
||||
public const string FfmpegPathKey = "ffmpeg";
|
||||
|
||||
/// <summary>
|
||||
/// The key for a setting that indicates whether playlists should allow duplicate entries.
|
||||
/// </summary>
|
||||
public const string PlaylistsAllowDuplicatesKey = "playlists:allowDuplicates";
|
||||
|
||||
/// <summary>
|
||||
/// The key for a setting that indicates whether kestrel should bind to a unix socket.
|
||||
/// </summary>
|
||||
|
@ -120,14 +115,6 @@ namespace MediaBrowser.Controller.Extensions
|
|||
public static bool GetFFmpegImgExtractPerfTradeoff(this IConfiguration configuration)
|
||||
=> configuration.GetValue<bool>(FfmpegImgExtractPerfTradeoffKey);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether playlists should allow duplicate entries from the <see cref="IConfiguration"/>.
|
||||
/// </summary>
|
||||
/// <param name="configuration">The configuration to read the setting from.</param>
|
||||
/// <returns>True if playlists should allow duplicates, otherwise false.</returns>
|
||||
public static bool DoPlaylistsAllowDuplicates(this IConfiguration configuration)
|
||||
=> configuration.GetValue<bool>(PlaylistsAllowDuplicatesKey);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether kestrel should bind to a unix socket from the <see cref="IConfiguration" />.
|
||||
/// </summary>
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
<PropertyGroup>
|
||||
<Authors>Jellyfin Contributors</Authors>
|
||||
<PackageId>Jellyfin.Controller</PackageId>
|
||||
<VersionPrefix>10.10.0</VersionPrefix>
|
||||
<VersionPrefix>10.10.5</VersionPrefix>
|
||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
|
|
|
@ -309,7 +309,6 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||
private bool IsSwTonemapAvailable(EncodingJobInfo state, EncodingOptions options)
|
||||
{
|
||||
if (state.VideoStream is null
|
||||
|| !options.EnableTonemapping
|
||||
|| GetVideoColorBitDepth(state) < 10
|
||||
|| !_mediaEncoder.SupportsFilter("tonemapx"))
|
||||
{
|
||||
|
@ -2061,7 +2060,13 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||
// libx265 only accept level option in -x265-params.
|
||||
// level option may cause libx265 to fail.
|
||||
// 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)
|
||||
|
@ -2196,7 +2201,10 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||
{
|
||||
var videoFrameRate = videoStream.ReferenceFrameRate;
|
||||
|
||||
if (!videoFrameRate.HasValue || videoFrameRate.Value > requestedFramerate.Value)
|
||||
// Add a little tolerance to the framerate check because some videos might record a framerate
|
||||
// that is slightly higher than the intended framerate, but the device can still play it correctly.
|
||||
// 0.05 fps tolerance should be safe enough.
|
||||
if (!videoFrameRate.HasValue || videoFrameRate.Value > requestedFramerate.Value + 0.05f)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
@ -3318,24 +3326,25 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||
&& options.VppTonemappingBrightness >= -100
|
||||
&& options.VppTonemappingBrightness <= 100)
|
||||
{
|
||||
procampParams += $"=b={options.VppTonemappingBrightness}";
|
||||
procampParams += "procamp_vaapi=b={0}";
|
||||
doVaVppProcamp = true;
|
||||
}
|
||||
|
||||
if (options.VppTonemappingContrast > 1
|
||||
&& options.VppTonemappingContrast <= 10)
|
||||
{
|
||||
procampParams += doVaVppProcamp ? ":" : "=";
|
||||
procampParams += $"c={options.VppTonemappingContrast}";
|
||||
procampParams += doVaVppProcamp ? ":c={1}" : "procamp_vaapi=c={1}";
|
||||
doVaVppProcamp = true;
|
||||
}
|
||||
|
||||
args = "{0}tonemap_vaapi=format={1}:p=bt709:t=bt709:m=bt709:extra_hw_frames=32";
|
||||
args = procampParams + "{2}tonemap_vaapi=format={3}:p=bt709:t=bt709:m=bt709:extra_hw_frames=32";
|
||||
|
||||
return string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
args,
|
||||
doVaVppProcamp ? $"procamp_vaapi{procampParams}," : string.Empty,
|
||||
options.VppTonemappingBrightness,
|
||||
options.VppTonemappingContrast,
|
||||
doVaVppProcamp ? "," : string.Empty,
|
||||
videoFormat ?? "nv12");
|
||||
}
|
||||
else
|
||||
|
@ -3523,20 +3532,29 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||
{
|
||||
// tonemapx requires yuv420p10 input for dovi reshaping, let ffmpeg convert the frame when necessary
|
||||
var tonemapFormat = requireDoviReshaping ? "yuv420p" : outFormat;
|
||||
|
||||
var tonemapArgs = $"tonemapx=tonemap={options.TonemappingAlgorithm}:desat={options.TonemappingDesat}:peak={options.TonemappingPeak}:t=bt709:m=bt709:p=bt709:format={tonemapFormat}";
|
||||
var tonemapArgString = "tonemapx=tonemap={0}:desat={1}:peak={2}:t=bt709:m=bt709:p=bt709:format={3}";
|
||||
|
||||
if (options.TonemappingParam != 0)
|
||||
{
|
||||
tonemapArgs += $":param={options.TonemappingParam}";
|
||||
tonemapArgString += ":param={4}";
|
||||
}
|
||||
|
||||
var range = options.TonemappingRange;
|
||||
if (range == TonemappingRange.tv || range == TonemappingRange.pc)
|
||||
{
|
||||
tonemapArgs += $":range={options.TonemappingRange}";
|
||||
tonemapArgString += ":range={5}";
|
||||
}
|
||||
|
||||
var tonemapArgs = string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
tonemapArgString,
|
||||
options.TonemappingAlgorithm,
|
||||
options.TonemappingDesat,
|
||||
options.TonemappingPeak,
|
||||
tonemapFormat,
|
||||
options.TonemappingParam,
|
||||
options.TonemappingRange);
|
||||
|
||||
mainFilters.Add(tonemapArgs);
|
||||
}
|
||||
else
|
||||
|
@ -4128,31 +4146,46 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||
else if (isD3d11vaDecoder || isQsvDecoder)
|
||||
{
|
||||
var isRext = IsVideoStreamHevcRext(state);
|
||||
var twoPassVppTonemap = isRext;
|
||||
var twoPassVppTonemap = false;
|
||||
var doVppFullRangeOut = isMjpegEncoder
|
||||
&& _mediaEncoder.EncoderVersion >= _minFFmpegQsvVppOutRangeOption;
|
||||
var doVppScaleModeHq = isMjpegEncoder
|
||||
&& _mediaEncoder.EncoderVersion >= _minFFmpegQsvVppScaleModeOption;
|
||||
var doVppProcamp = false;
|
||||
var procampParams = string.Empty;
|
||||
var procampParamsString = string.Empty;
|
||||
if (doVppTonemap)
|
||||
{
|
||||
if (isRext)
|
||||
{
|
||||
// VPP tonemap requires p010 input
|
||||
twoPassVppTonemap = true;
|
||||
}
|
||||
|
||||
if (options.VppTonemappingBrightness != 0
|
||||
&& options.VppTonemappingBrightness >= -100
|
||||
&& options.VppTonemappingBrightness <= 100)
|
||||
{
|
||||
procampParams += $":brightness={options.VppTonemappingBrightness}";
|
||||
procampParamsString += ":brightness={0}";
|
||||
twoPassVppTonemap = doVppProcamp = true;
|
||||
}
|
||||
|
||||
if (options.VppTonemappingContrast > 1
|
||||
&& options.VppTonemappingContrast <= 10)
|
||||
{
|
||||
procampParams += $":contrast={options.VppTonemappingContrast}";
|
||||
procampParamsString += ":contrast={1}";
|
||||
twoPassVppTonemap = doVppProcamp = true;
|
||||
}
|
||||
|
||||
procampParams += doVppProcamp ? ":procamp=1:async_depth=2" : string.Empty;
|
||||
if (doVppProcamp)
|
||||
{
|
||||
procampParamsString += ":procamp=1:async_depth=2";
|
||||
procampParams = string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
procampParamsString,
|
||||
options.VppTonemappingBrightness,
|
||||
options.VppTonemappingContrast);
|
||||
}
|
||||
}
|
||||
|
||||
var outFormat = doOclTonemap ? ((doVppTranspose || isRext) ? "p010" : string.Empty) : "nv12";
|
||||
|
@ -5662,7 +5695,11 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||
if (!string.IsNullOrEmpty(doScaling)
|
||||
&& !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);
|
||||
}
|
||||
|
||||
|
@ -7036,7 +7073,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||
{
|
||||
// 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
|
||||
shiftAudioCodecs.Add("dca");
|
||||
shiftAudioCodecs.Add("dts");
|
||||
shiftAudioCodecs.Add("truehd");
|
||||
}
|
||||
else
|
||||
|
|
|
@ -50,8 +50,18 @@ public interface IMediaSegmentManager
|
|||
/// </summary>
|
||||
/// <param name="itemId">The id of the <see cref="BaseItem"/>.</param>
|
||||
/// <param name="typeFilter">filteres all media segments of the given type to be included. If null all types are included.</param>
|
||||
/// <param name="filterByProvider">When set filteres the segments to only return those that which providers are currently enabled on their library.</param>
|
||||
/// <returns>An enumerator of <see cref="MediaSegmentDto"/>'s.</returns>
|
||||
Task<IEnumerable<MediaSegmentDto>> GetSegmentsAsync(Guid itemId, IEnumerable<MediaSegmentType>? typeFilter);
|
||||
Task<IEnumerable<MediaSegmentDto>> GetSegmentsAsync(Guid itemId, IEnumerable<MediaSegmentType>? typeFilter, bool filterByProvider = true);
|
||||
|
||||
/// <summary>
|
||||
/// Obtains all segments accociated with the itemId.
|
||||
/// </summary>
|
||||
/// <param name="item">The <see cref="BaseItem"/>.</param>
|
||||
/// <param name="typeFilter">filteres all media segments of the given type to be included. If null all types are included.</param>
|
||||
/// <param name="filterByProvider">When set filteres the segments to only return those that which providers are currently enabled on their library.</param>
|
||||
/// <returns>An enumerator of <see cref="MediaSegmentDto"/>'s.</returns>
|
||||
Task<IEnumerable<MediaSegmentDto>> GetSegmentsAsync(BaseItem item, IEnumerable<MediaSegmentType>? typeFilter, bool filterByProvider = true);
|
||||
|
||||
/// <summary>
|
||||
/// Gets information about any media segments stored for the given itemId.
|
||||
|
|
|
@ -92,8 +92,9 @@ namespace MediaBrowser.Controller.Playlists
|
|||
/// <param name="playlistId">The playlist identifier.</param>
|
||||
/// <param name="entryId">The entry identifier.</param>
|
||||
/// <param name="newIndex">The new index.</param>
|
||||
/// <param name="callingUserId">The calling user.</param>
|
||||
/// <returns>Task.</returns>
|
||||
Task MoveItemAsync(string playlistId, string entryId, int newIndex);
|
||||
Task MoveItemAsync(string playlistId, string entryId, int newIndex, Guid callingUserId);
|
||||
|
||||
/// <summary>
|
||||
/// Removed all playlists of a user.
|
||||
|
|
|
@ -77,7 +77,8 @@ namespace MediaBrowser.Controller.Providers
|
|||
Task SaveImage(BaseItem item, Stream source, string mimeType, ImageType type, int? imageIndex, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Saves the image.
|
||||
/// Saves the image by giving the image path on filesystem.
|
||||
/// This method will remove the image on the source path after saving it to the destination.
|
||||
/// </summary>
|
||||
/// <param name="item">Image to save.</param>
|
||||
/// <param name="source">Source of image.</param>
|
||||
|
|
|
@ -1035,6 +1035,16 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||
if (exitCode == -1)
|
||||
{
|
||||
_logger.LogError("ffmpeg image extraction failed for {ProcessDescription}", processDescription);
|
||||
// Cleanup temp folder here, because the targetDirectory is not returned and the cleanup for failed ffmpeg process is not possible for caller.
|
||||
// Ideally the ffmpeg should not write any files if it fails, but it seems like it is not guaranteed.
|
||||
try
|
||||
{
|
||||
Directory.Delete(targetDirectory, true);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, "Failed to delete ffmpeg temp directory {TargetDirectory}", targetDirectory);
|
||||
}
|
||||
|
||||
throw new FfmpegException(string.Format(CultureInfo.InvariantCulture, "ffmpeg image extraction failed for {0}", processDescription));
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
|||
public class SubtitleEditParser : ISubtitleParser
|
||||
{
|
||||
private readonly ILogger<SubtitleEditParser> _logger;
|
||||
private readonly Dictionary<string, SubtitleFormat[]> _subtitleFormats;
|
||||
private readonly Dictionary<string, List<Type>> _subtitleFormatTypes;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SubtitleEditParser"/> class.
|
||||
|
@ -26,10 +26,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
|||
public SubtitleEditParser(ILogger<SubtitleEditParser> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
_subtitleFormats = GetSubtitleFormats()
|
||||
.Where(subtitleFormat => !string.IsNullOrEmpty(subtitleFormat.Extension))
|
||||
.GroupBy(subtitleFormat => subtitleFormat.Extension.TrimStart('.'), StringComparer.OrdinalIgnoreCase)
|
||||
.ToDictionary(g => g.Key, g => g.ToArray(), StringComparer.OrdinalIgnoreCase);
|
||||
_subtitleFormatTypes = GetSubtitleFormatTypes();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
@ -38,13 +35,14 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
|||
var subtitle = new Subtitle();
|
||||
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));
|
||||
}
|
||||
|
||||
foreach (var subtitleFormat in subtitleFormats)
|
||||
foreach (var subtitleFormatType in subtitleFormatTypesForExtension)
|
||||
{
|
||||
var subtitleFormat = (SubtitleFormat)Activator.CreateInstance(subtitleFormatType, true)!;
|
||||
_logger.LogDebug(
|
||||
"Trying to parse '{FileExtension}' subtitle using the {SubtitleFormatParser} format parser",
|
||||
fileExtension,
|
||||
|
@ -97,11 +95,11 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
|||
|
||||
/// <inheritdoc />
|
||||
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;
|
||||
|
||||
foreach (var type in assembly.GetTypes())
|
||||
|
@ -113,9 +111,20 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
|||
|
||||
try
|
||||
{
|
||||
// It shouldn't be null, but the exception is caught if it is
|
||||
var subtitleFormat = (SubtitleFormat)Activator.CreateInstance(type, true)!;
|
||||
subtitleFormats.Add(subtitleFormat);
|
||||
var tempInstance = (SubtitleFormat)Activator.CreateInstance(type, true)!;
|
||||
var extension = tempInstance.Extension.TrimStart('.');
|
||||
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)
|
||||
{
|
||||
|
@ -123,7 +132,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
|||
}
|
||||
}
|
||||
|
||||
return subtitleFormats;
|
||||
return subtitleFormatTypes;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -246,7 +246,7 @@ public class ServerConfiguration : BaseApplicationConfiguration
|
|||
/// <summary>
|
||||
/// Gets or sets a value indicating whether older plugins should automatically be deleted from the plugin folder.
|
||||
/// </summary>
|
||||
public bool RemoveOldPlugins { get; set; }
|
||||
public bool RemoveOldPlugins { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 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 static readonly string[] _supportedHlsVideoCodecs = ["h264", "hevc", "vp9", "av1"];
|
||||
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>
|
||||
/// Initializes a new instance of the <see cref="StreamBuilder"/> class.
|
||||
|
@ -208,6 +208,14 @@ namespace MediaBrowser.Model.Dlna
|
|||
|
||||
var longBitrate = Math.Min(transcodingBitrate, playlistItem.AudioBitrate ?? transcodingBitrate);
|
||||
playlistItem.AudioBitrate = longBitrate > int.MaxValue ? int.MaxValue : Convert.ToInt32(longBitrate);
|
||||
|
||||
// Pure audio transcoding does not support comma separated list of transcoding codec at the moment.
|
||||
// So just use the AudioCodec as is would be safe enough as the _transcoderSupport.CanEncodeToAudioCodec
|
||||
// would fail so this profile will not even be picked up.
|
||||
if (playlistItem.AudioCodecs.Count == 0 && !string.IsNullOrWhiteSpace(transcodingProfile.AudioCodec))
|
||||
{
|
||||
playlistItem.AudioCodecs = [transcodingProfile.AudioCodec];
|
||||
}
|
||||
}
|
||||
|
||||
playlistItem.TranscodeReasons = transcodeReasons;
|
||||
|
@ -854,18 +862,37 @@ namespace MediaBrowser.Model.Dlna
|
|||
|
||||
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
|
||||
.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)))
|
||||
.Select(i =>
|
||||
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
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -955,9 +982,28 @@ namespace MediaBrowser.Model.Dlna
|
|||
|
||||
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)
|
||||
{
|
||||
|
@ -2205,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
|
||||
if (!profile.SupportsContainer(item.Container))
|
||||
|
@ -2213,6 +2259,20 @@ namespace MediaBrowser.Model.Dlna
|
|||
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
|
||||
string? audioCodec = audioStream?.Codec;
|
||||
if (!profile.SupportsAudioCodec(audioCodec))
|
||||
|
@ -2227,19 +2287,16 @@ namespace MediaBrowser.Model.Dlna
|
|||
{
|
||||
// Check container type, this should NOT be supported
|
||||
// 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
|
||||
// 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;
|
||||
}
|
||||
|
||||
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)
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Xml.Serialization;
|
||||
using Jellyfin.Data.Enums;
|
||||
|
@ -6,6 +7,7 @@ namespace MediaBrowser.Model.Dlna;
|
|||
|
||||
/// <summary>
|
||||
/// 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>
|
||||
public class TranscodingProfile
|
||||
{
|
||||
|
@ -17,6 +19,33 @@ public class TranscodingProfile
|
|||
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>
|
||||
/// Gets or sets the container.
|
||||
/// </summary>
|
||||
|
|
|
@ -18,7 +18,7 @@ public static class LibraryOptionsExtension
|
|||
{
|
||||
ArgumentNullException.ThrowIfNull(options);
|
||||
|
||||
return options.CustomTagDelimiters.Select<string, char?>(x =>
|
||||
var delimiterList = options.CustomTagDelimiters.Select<string, char?>(x =>
|
||||
{
|
||||
var isChar = char.TryParse(x, out var c);
|
||||
if (isChar)
|
||||
|
@ -27,6 +27,8 @@ public static class LibraryOptionsExtension
|
|||
}
|
||||
|
||||
return null;
|
||||
}).Where(x => x is not null).Select(x => x!.Value).ToArray();
|
||||
}).Where(x => x is not null).Select(x => x!.Value).ToList();
|
||||
delimiterList.Add('\0');
|
||||
return delimiterList.ToArray();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
<PropertyGroup>
|
||||
<Authors>Jellyfin Contributors</Authors>
|
||||
<PackageId>Jellyfin.Model</PackageId>
|
||||
<VersionPrefix>10.10.0</VersionPrefix>
|
||||
<VersionPrefix>10.10.5</VersionPrefix>
|
||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
|
|
|
@ -291,6 +291,7 @@ namespace MediaBrowser.Providers.Manager
|
|||
|
||||
var fileStreamOptions = AsyncFile.WriteOptions;
|
||||
fileStreamOptions.Mode = FileMode.Create;
|
||||
fileStreamOptions.Options = FileOptions.WriteThrough;
|
||||
if (source.CanSeek)
|
||||
{
|
||||
fileStreamOptions.PreallocationSize = source.Length;
|
||||
|
|
|
@ -229,9 +229,7 @@ namespace MediaBrowser.Providers.Manager
|
|||
{
|
||||
var mimeType = MimeTypes.GetMimeType(response.Path);
|
||||
|
||||
var stream = AsyncFile.OpenRead(response.Path);
|
||||
|
||||
await _providerManager.SaveImage(item, stream, mimeType, imageType, null, cancellationToken).ConfigureAwait(false);
|
||||
await _providerManager.SaveImage(item, response.Path, mimeType, imageType, null, null, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -387,8 +385,8 @@ namespace MediaBrowser.Providers.Manager
|
|||
|
||||
item.RemoveImages(images);
|
||||
|
||||
// Cleanup old metadata directory for episodes if empty
|
||||
if (item is Episode)
|
||||
// Cleanup old metadata directory for episodes if empty, as long as it's not a virtual item
|
||||
if (item is Episode && !item.IsVirtualItem)
|
||||
{
|
||||
var oldLocalMetadataDirectory = Path.Combine(item.ContainingFolderPath, "metadata");
|
||||
if (_fileSystem.DirectoryExists(oldLocalMetadataDirectory) && !_fileSystem.GetFiles(oldLocalMetadataDirectory).Any())
|
||||
|
|
|
@ -7,6 +7,7 @@ using System.Linq;
|
|||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Mime;
|
||||
using System.Runtime.ExceptionServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using AsyncKeyedLock;
|
||||
|
@ -251,15 +252,29 @@ namespace MediaBrowser.Providers.Manager
|
|||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task SaveImage(BaseItem item, string source, string mimeType, ImageType type, int? imageIndex, bool? saveLocallyWithMedia, CancellationToken cancellationToken)
|
||||
public async Task SaveImage(BaseItem item, string source, string mimeType, ImageType type, int? imageIndex, bool? saveLocallyWithMedia, CancellationToken cancellationToken)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(source))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(source));
|
||||
}
|
||||
|
||||
var fileStream = AsyncFile.OpenRead(source);
|
||||
return new ImageSaver(_configurationManager, _libraryMonitor, _fileSystem, _logger).SaveImage(item, fileStream, mimeType, type, imageIndex, saveLocallyWithMedia, cancellationToken);
|
||||
try
|
||||
{
|
||||
var fileStream = AsyncFile.OpenRead(source);
|
||||
await new ImageSaver(_configurationManager, _libraryMonitor, _fileSystem, _logger).SaveImage(item, fileStream, mimeType, type, imageIndex, saveLocallyWithMedia, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
try
|
||||
{
|
||||
File.Delete(source);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Source file {Source} not found or in use, skip removing", source);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
|
|
@ -179,7 +179,7 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||
if (audio.SupportsPeople && !audio.LockedFields.Contains(MetadataField.Cast))
|
||||
{
|
||||
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)
|
||||
{
|
||||
|
@ -210,7 +210,7 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||
|
||||
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)
|
||||
|
@ -314,7 +314,7 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||
|
||||
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)
|
||||
{
|
||||
|
@ -347,7 +347,8 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||
|| track.AdditionalFields.TryGetValue("MusicBrainz Artist Id", out musicBrainzArtistTag))
|
||||
&& !string.IsNullOrEmpty(musicBrainzArtistTag))
|
||||
{
|
||||
audio.TrySetProviderId(MetadataProvider.MusicBrainzArtist, musicBrainzArtistTag);
|
||||
var id = GetFirstMusicBrainzId(musicBrainzArtistTag, libraryOptions.UseCustomTagDelimiters, libraryOptions.GetCustomTagDelimiters(), libraryOptions.DelimiterWhitelist);
|
||||
audio.TrySetProviderId(MetadataProvider.MusicBrainzArtist, id);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -357,7 +358,8 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||
|| track.AdditionalFields.TryGetValue("MusicBrainz Album Artist Id", out musicBrainzReleaseArtistIdTag))
|
||||
&& !string.IsNullOrEmpty(musicBrainzReleaseArtistIdTag))
|
||||
{
|
||||
audio.TrySetProviderId(MetadataProvider.MusicBrainzAlbumArtist, musicBrainzReleaseArtistIdTag);
|
||||
var id = GetFirstMusicBrainzId(musicBrainzReleaseArtistIdTag, libraryOptions.UseCustomTagDelimiters, libraryOptions.GetCustomTagDelimiters(), libraryOptions.DelimiterWhitelist);
|
||||
audio.TrySetProviderId(MetadataProvider.MusicBrainzAlbumArtist, id);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -367,7 +369,8 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||
|| track.AdditionalFields.TryGetValue("MusicBrainz Album Id", out musicBrainzReleaseIdTag))
|
||||
&& !string.IsNullOrEmpty(musicBrainzReleaseIdTag))
|
||||
{
|
||||
audio.TrySetProviderId(MetadataProvider.MusicBrainzAlbum, musicBrainzReleaseIdTag);
|
||||
var id = GetFirstMusicBrainzId(musicBrainzReleaseIdTag, libraryOptions.UseCustomTagDelimiters, libraryOptions.GetCustomTagDelimiters(), libraryOptions.DelimiterWhitelist);
|
||||
audio.TrySetProviderId(MetadataProvider.MusicBrainzAlbum, id);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -377,7 +380,8 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||
|| track.AdditionalFields.TryGetValue("MusicBrainz Release Group Id", out musicBrainzReleaseGroupIdTag))
|
||||
&& !string.IsNullOrEmpty(musicBrainzReleaseGroupIdTag))
|
||||
{
|
||||
audio.TrySetProviderId(MetadataProvider.MusicBrainzReleaseGroup, musicBrainzReleaseGroupIdTag);
|
||||
var id = GetFirstMusicBrainzId(musicBrainzReleaseGroupIdTag, libraryOptions.UseCustomTagDelimiters, libraryOptions.GetCustomTagDelimiters(), libraryOptions.DelimiterWhitelist);
|
||||
audio.TrySetProviderId(MetadataProvider.MusicBrainzReleaseGroup, id);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -387,7 +391,8 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||
|| track.AdditionalFields.TryGetValue("MusicBrainz Release Track Id", out trackMbId))
|
||||
&& !string.IsNullOrEmpty(trackMbId))
|
||||
{
|
||||
audio.TrySetProviderId(MetadataProvider.MusicBrainzTrack, trackMbId);
|
||||
var id = GetFirstMusicBrainzId(trackMbId, libraryOptions.UseCustomTagDelimiters, libraryOptions.GetCustomTagDelimiters(), libraryOptions.DelimiterWhitelist);
|
||||
audio.TrySetProviderId(MetadataProvider.MusicBrainzTrack, id);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -441,5 +446,18 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||
|
||||
return items;
|
||||
}
|
||||
|
||||
// MusicBrainz IDs are multi-value tags, so we need to split them
|
||||
// However, our current provider can only have one single ID, which means we need to pick the first one
|
||||
private string? GetFirstMusicBrainzId(string tag, bool useCustomTagDelimiters, char[] tagDelimiters, string[] whitelist)
|
||||
{
|
||||
var val = tag.Split(InternalValueSeparator).FirstOrDefault();
|
||||
if (val is not null && useCustomTagDelimiters)
|
||||
{
|
||||
val = SplitWithCustomDelimiter(val, tagDelimiters, whitelist).FirstOrDefault();
|
||||
}
|
||||
|
||||
return val;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -198,7 +198,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
|
|||
};
|
||||
|
||||
movie.SetProviderId(MetadataProvider.Tmdb, tmdbId);
|
||||
movie.SetProviderId(MetadataProvider.Imdb, movieResult.ImdbId);
|
||||
movie.TrySetProviderId(MetadataProvider.Imdb, movieResult.ImdbId);
|
||||
if (movieResult.BelongsToCollection is not null)
|
||||
{
|
||||
movie.SetProviderId(MetadataProvider.TmdbCollection, movieResult.BelongsToCollection.Id.ToString(CultureInfo.InvariantCulture));
|
||||
|
|
|
@ -140,38 +140,39 @@ namespace MediaBrowser.Providers.TV
|
|||
|
||||
private void RemoveObsoleteEpisodes(Series series)
|
||||
{
|
||||
var episodes = series.GetEpisodes(null, new DtoOptions(), true).OfType<Episode>().ToList();
|
||||
var numberOfEpisodes = episodes.Count;
|
||||
// TODO: O(n^2), but can it be done faster without overcomplicating it?
|
||||
for (var i = 0; i < numberOfEpisodes; i++)
|
||||
var episodesBySeason = series.GetEpisodes(null, new DtoOptions(), true)
|
||||
.OfType<Episode>()
|
||||
.GroupBy(e => e.ParentIndexNumber)
|
||||
.ToList();
|
||||
|
||||
foreach (var seasonEpisodes in episodesBySeason)
|
||||
{
|
||||
var currentEpisode = episodes[i];
|
||||
// The outer loop only examines virtual episodes
|
||||
if (!currentEpisode.IsVirtualItem)
|
||||
List<Episode> nonPhysicalEpisodes = [];
|
||||
List<Episode> physicalEpisodes = [];
|
||||
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
|
||||
if (!currentEpisode.IndexNumber.HasValue)
|
||||
// Only consider non-physical episodes
|
||||
foreach (var episode in nonPhysicalEpisodes)
|
||||
{
|
||||
DeleteEpisode(currentEpisode);
|
||||
continue;
|
||||
}
|
||||
// Episodes without an episode number are practically orphaned and should be deleted
|
||||
// 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++)
|
||||
{
|
||||
var comparisonEpisode = episodes[j];
|
||||
// The inner loop is only for "physical" episodes
|
||||
if (comparisonEpisode.IsVirtualItem
|
||||
|| currentEpisode.ParentIndexNumber != comparisonEpisode.ParentIndexNumber
|
||||
|| !comparisonEpisode.ContainsEpisodeNumber(currentEpisode.IndexNumber.Value))
|
||||
if (shouldKeep)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
DeleteEpisode(currentEpisode);
|
||||
break;
|
||||
DeleteEpisode(episode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -50,23 +50,20 @@ namespace MediaBrowser.XbmcMetadata.Parsers
|
|||
{
|
||||
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? 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();
|
||||
if (contentId.Contains("tt", StringComparison.Ordinal) && string.IsNullOrEmpty(imdbId))
|
||||
if (string.IsNullOrEmpty(imdbId) && contentId.StartsWith("tt", StringComparison.Ordinal))
|
||||
{
|
||||
imdbId = contentId;
|
||||
}
|
||||
else if (string.IsNullOrEmpty(tmdbId))
|
||||
{
|
||||
tmdbId = contentId;
|
||||
}
|
||||
|
||||
item.TrySetProviderId(MetadataProvider.Imdb, imdbId);
|
||||
item.TrySetProviderId(MetadataProvider.Tmdb, tmdbId);
|
||||
|
||||
break;
|
||||
}
|
||||
|
@ -82,21 +79,13 @@ namespace MediaBrowser.XbmcMetadata.Parsers
|
|||
|
||||
if (!string.IsNullOrWhiteSpace(val) && movie is not null)
|
||||
{
|
||||
// TODO Handle this better later
|
||||
if (!val.Contains('<', StringComparison.Ordinal))
|
||||
try
|
||||
{
|
||||
movie.CollectionName = val;
|
||||
ParseSetXml(val, movie);
|
||||
}
|
||||
else
|
||||
catch (Exception ex)
|
||||
{
|
||||
try
|
||||
{
|
||||
ParseSetXml(val, movie);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Error parsing set node");
|
||||
}
|
||||
Logger.LogError(ex, "Error parsing set node");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -139,7 +128,12 @@ namespace MediaBrowser.XbmcMetadata.Parsers
|
|||
// Loop through each element
|
||||
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)
|
||||
{
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
using System;
|
||||
using System.Globalization;
|
||||
using System.Xml;
|
||||
using Emby.Naming.TV;
|
||||
|
@ -48,16 +49,20 @@ namespace MediaBrowser.XbmcMetadata.Parsers
|
|||
{
|
||||
case "id":
|
||||
{
|
||||
item.TrySetProviderId(MetadataProvider.Imdb, reader.GetAttribute("IMDB"));
|
||||
// Get ids from attributes
|
||||
item.TrySetProviderId(MetadataProvider.Tmdb, reader.GetAttribute("TMDB"));
|
||||
item.TrySetProviderId(MetadataProvider.Tvdb, reader.GetAttribute("TVDB"));
|
||||
string? imdbId = reader.GetAttribute("IMDB");
|
||||
|
||||
string? tvdbId = reader.GetAttribute("TVDB");
|
||||
if (string.IsNullOrWhiteSpace(tvdbId))
|
||||
// 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();
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -115,7 +115,9 @@ namespace MediaBrowser.XbmcMetadata.Savers
|
|||
{
|
||||
if (!string.IsNullOrEmpty(movie.CollectionName))
|
||||
{
|
||||
writer.WriteElementString("set", movie.CollectionName);
|
||||
writer.WriteStartElement("set");
|
||||
writer.WriteElementString("name", movie.CollectionName);
|
||||
writer.WriteEndElement();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
using System.Reflection;
|
||||
|
||||
[assembly: AssemblyVersion("10.10.0")]
|
||||
[assembly: AssemblyFileVersion("10.10.0")]
|
||||
[assembly: AssemblyVersion("10.10.5")]
|
||||
[assembly: AssemblyFileVersion("10.10.5")]
|
||||
|
|
|
@ -195,8 +195,10 @@ public class SkiaEncoder : IImageEncoder
|
|||
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
|
||||
return BlurHashEncoder.Encode(xComp, yComp, path, 128, 128);
|
||||
return BlurHashEncoder.Encode(xComp, yComp, fileStream, 128, 128);
|
||||
}
|
||||
|
||||
private bool RequiresSpecialCharacterHack(string path)
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
<PropertyGroup>
|
||||
<Authors>Jellyfin Contributors</Authors>
|
||||
<PackageId>Jellyfin.Extensions</PackageId>
|
||||
<VersionPrefix>10.10.0</VersionPrefix>
|
||||
<VersionPrefix>10.10.5</VersionPrefix>
|
||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
|
|
|
@ -71,24 +71,11 @@ namespace Jellyfin.Extensions.Json.Converters
|
|||
writer.WriteStartArray();
|
||||
if (value.Length > 0)
|
||||
{
|
||||
var toWrite = value.Length - 1;
|
||||
foreach (var it in value)
|
||||
{
|
||||
var wrote = false;
|
||||
if (it is not null)
|
||||
{
|
||||
writer.WriteStringValue(it.ToString());
|
||||
wrote = true;
|
||||
}
|
||||
|
||||
if (toWrite > 0)
|
||||
{
|
||||
if (wrote)
|
||||
{
|
||||
writer.WriteStringValue(Delimiter.ToString());
|
||||
}
|
||||
|
||||
toWrite--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ using System.Collections.Generic;
|
|||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Data.Entities.Libraries;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Extensions;
|
||||
using Jellyfin.LiveTv.Configuration;
|
||||
|
@ -39,6 +40,11 @@ public class GuideManager : IGuideManager
|
|||
private readonly IRecordingsManager _recordingsManager;
|
||||
private readonly LiveTvDtoService _tvDtoService;
|
||||
|
||||
/// <summary>
|
||||
/// Amount of days images are pre-cached from external sources.
|
||||
/// </summary>
|
||||
public const int MaxCacheDays = 2;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="GuideManager"/> class.
|
||||
/// </summary>
|
||||
|
@ -204,14 +210,14 @@ public class GuideManager : IGuideManager
|
|||
progress.Report(15);
|
||||
|
||||
numComplete = 0;
|
||||
var programs = new List<Guid>();
|
||||
var programs = new List<LiveTvProgram>();
|
||||
var channels = new List<Guid>();
|
||||
|
||||
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)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
@ -237,22 +243,23 @@ public class GuideManager : IGuideManager
|
|||
DtoOptions = new DtoOptions(true)
|
||||
}).Cast<LiveTvProgram>().ToDictionary(i => i.Id);
|
||||
|
||||
var newPrograms = new List<LiveTvProgram>();
|
||||
var updatedPrograms = new List<BaseItem>();
|
||||
var newPrograms = new List<Guid>();
|
||||
var updatedPrograms = new List<Guid>();
|
||||
|
||||
foreach (var program in channelPrograms)
|
||||
{
|
||||
var (programItem, isNew, isUpdated) = GetProgram(program, existingPrograms, currentChannel);
|
||||
var id = programItem.Id;
|
||||
if (isNew)
|
||||
{
|
||||
newPrograms.Add(programItem);
|
||||
newPrograms.Add(id);
|
||||
}
|
||||
else if (isUpdated)
|
||||
{
|
||||
updatedPrograms.Add(programItem);
|
||||
updatedPrograms.Add(id);
|
||||
}
|
||||
|
||||
programs.Add(programItem.Id);
|
||||
programs.Add(programItem);
|
||||
|
||||
isMovie |= program.IsMovie;
|
||||
isSeries |= program.IsSeries;
|
||||
|
@ -261,24 +268,30 @@ public class GuideManager : IGuideManager
|
|||
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)
|
||||
{
|
||||
_libraryManager.CreateItems(newPrograms, null, cancellationToken);
|
||||
await PrecacheImages(newPrograms, maxCacheDate).ConfigureAwait(false);
|
||||
var newProgramDtos = programs.Where(b => newPrograms.Contains(b.Id)).ToList();
|
||||
_libraryManager.CreateItems(newProgramDtos, null, cancellationToken);
|
||||
}
|
||||
|
||||
if (updatedPrograms.Count > 0)
|
||||
{
|
||||
var updatedProgramDtos = programs.Where(b => updatedPrograms.Contains(b.Id)).ToList();
|
||||
await _libraryManager.UpdateItemsAsync(
|
||||
updatedPrograms,
|
||||
updatedProgramDtos,
|
||||
currentChannel,
|
||||
ItemUpdateType.MetadataImport,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
await PrecacheImages(updatedPrograms, maxCacheDate).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
await PreCacheImages(programs, maxCacheDate).ConfigureAwait(false);
|
||||
|
||||
currentChannel.IsMovie = isMovie;
|
||||
currentChannel.IsNews = isNews;
|
||||
currentChannel.IsSports = isSports;
|
||||
|
@ -313,7 +326,8 @@ public class GuideManager : IGuideManager
|
|||
}
|
||||
|
||||
progress.Report(100);
|
||||
return new Tuple<List<Guid>, List<Guid>>(channels, programs);
|
||||
var programIds = programs.Select(p => p.Id).ToList();
|
||||
return new Tuple<List<Guid>, List<Guid>>(channels, programIds);
|
||||
}
|
||||
|
||||
private void CleanDatabase(Guid[] currentIdList, BaseItemKind[] validTypes, IProgress<double> progress, CancellationToken cancellationToken)
|
||||
|
@ -618,77 +632,17 @@ public class GuideManager : IGuideManager
|
|||
item.IndexNumber = info.EpisodeNumber;
|
||||
item.ParentIndexNumber = info.SeasonNumber;
|
||||
|
||||
if (!item.HasImage(ImageType.Primary))
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
forceUpdate = forceUpdate || UpdateImages(item, info);
|
||||
|
||||
if (!item.HasImage(ImageType.Thumb))
|
||||
if (isNew)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(info.ThumbImageUrl))
|
||||
{
|
||||
item.SetImage(
|
||||
new ItemImageInfo
|
||||
{
|
||||
Path = info.ThumbImageUrl,
|
||||
Type = ImageType.Thumb
|
||||
},
|
||||
0);
|
||||
}
|
||||
}
|
||||
item.OnMetadataChanged();
|
||||
|
||||
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);
|
||||
}
|
||||
return (item, isNew, false);
|
||||
}
|
||||
|
||||
var isUpdated = false;
|
||||
if (isNew)
|
||||
{
|
||||
}
|
||||
else if (forceUpdate || string.IsNullOrWhiteSpace(info.Etag))
|
||||
if (forceUpdate || string.IsNullOrWhiteSpace(info.Etag))
|
||||
{
|
||||
isUpdated = true;
|
||||
}
|
||||
|
@ -703,7 +657,7 @@ public class GuideManager : IGuideManager
|
|||
}
|
||||
}
|
||||
|
||||
if (isNew || isUpdated)
|
||||
if (isUpdated)
|
||||
{
|
||||
item.OnMetadataChanged();
|
||||
}
|
||||
|
@ -711,7 +665,80 @@ public class GuideManager : IGuideManager
|
|||
return (item, isNew, isUpdated);
|
||||
}
|
||||
|
||||
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
|
||||
return updated || UpdateImage(ImageType.Backdrop, item, info);
|
||||
}
|
||||
|
||||
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,
|
||||
_ => string.Empty
|
||||
};
|
||||
var newImageUrl = imageType switch
|
||||
{
|
||||
ImageType.Backdrop => info.BackdropImageUrl,
|
||||
ImageType.Logo => info.LogoImageUrl,
|
||||
ImageType.Primary => info.ImageUrl,
|
||||
ImageType.Thumb => info.ThumbImageUrl,
|
||||
_ => string.Empty
|
||||
};
|
||||
|
||||
var differentImage = newImageUrl?.Equals(currentImagePath, StringComparison.OrdinalIgnoreCase) == false
|
||||
|| newImagePath?.Equals(currentImagePath, StringComparison.OrdinalIgnoreCase) == false;
|
||||
if (!differentImage)
|
||||
{
|
||||
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(
|
||||
programs
|
||||
|
@ -741,7 +768,7 @@ public class GuideManager : IGuideManager
|
|||
}
|
||||
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 Jellyfin.Extensions;
|
||||
using Jellyfin.Extensions.Json;
|
||||
using Jellyfin.LiveTv.Guide;
|
||||
using Jellyfin.LiveTv.Listings.SchedulesDirectDtos;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller.Authentication;
|
||||
|
@ -38,7 +39,7 @@ namespace Jellyfin.LiveTv.Listings
|
|||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
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 DateTime _lastErrorResponse;
|
||||
private bool _disposed = false;
|
||||
|
@ -86,7 +87,7 @@ namespace Jellyfin.LiveTv.Listings
|
|||
{
|
||||
_logger.LogWarning("SchedulesDirect token is empty, returning empty program list");
|
||||
|
||||
return Enumerable.Empty<ProgramInfo>();
|
||||
return [];
|
||||
}
|
||||
|
||||
var dates = GetScheduleRequestDates(startDateUtc, endDateUtc);
|
||||
|
@ -94,7 +95,7 @@ namespace Jellyfin.LiveTv.Listings
|
|||
_logger.LogInformation("Channel Station ID is: {ChannelID}", channelId);
|
||||
var requestList = new List<RequestScheduleForChannelDto>()
|
||||
{
|
||||
new RequestScheduleForChannelDto()
|
||||
new()
|
||||
{
|
||||
StationId = channelId,
|
||||
Date = dates
|
||||
|
@ -109,7 +110,7 @@ namespace Jellyfin.LiveTv.Listings
|
|||
var dailySchedules = await Request<IReadOnlyList<DayDto>>(options, true, info, cancellationToken).ConfigureAwait(false);
|
||||
if (dailySchedules is null)
|
||||
{
|
||||
return Array.Empty<ProgramInfo>();
|
||||
return [];
|
||||
}
|
||||
|
||||
_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();
|
||||
programRequestOptions.Content = JsonContent.Create(programIds, options: _jsonOptions);
|
||||
|
||||
var programDetails = await Request<IReadOnlyList<ProgramDetailsDto>>(programRequestOptions, true, info, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
var programDetails = await Request<IReadOnlyList<ProgramDetailsDto>>(programRequestOptions, true, info, cancellationToken).ConfigureAwait(false);
|
||||
if (programDetails is null)
|
||||
{
|
||||
return Array.Empty<ProgramInfo>();
|
||||
return [];
|
||||
}
|
||||
|
||||
var programDict = programDetails.ToDictionary(p => p.ProgramId, y => y);
|
||||
|
||||
var programIdsWithImages = programDetails
|
||||
.Where(p => p.HasImageArtwork).Select(p => p.ProgramId)
|
||||
.Where(p => p.HasImageArtwork)
|
||||
.Select(p => p.ProgramId)
|
||||
.ToList();
|
||||
|
||||
var images = await GetImageForPrograms(info, programIdsWithImages, cancellationToken).ConfigureAwait(false);
|
||||
|
@ -138,17 +139,15 @@ namespace Jellyfin.LiveTv.Listings
|
|||
var programsInfo = new List<ProgramInfo>();
|
||||
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))
|
||||
{
|
||||
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]);
|
||||
if (imageIndex > -1)
|
||||
|
@ -456,7 +455,7 @@ namespace Jellyfin.LiveTv.Listings
|
|||
|
||||
if (programIds.Count == 0)
|
||||
{
|
||||
return Array.Empty<ShowImagesDto>();
|
||||
return [];
|
||||
}
|
||||
|
||||
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");
|
||||
|
||||
return Array.Empty<ShowImagesDto>();
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -702,7 +702,7 @@ public class NetworkManager : INetworkManager, IDisposable
|
|||
return false;
|
||||
}
|
||||
}
|
||||
else if (!_lanSubnets.Any(x => x.Contains(remoteIP)))
|
||||
else if (!IsInLocalNetwork(remoteIP))
|
||||
{
|
||||
// Remote not enabled. So everyone should be LAN.
|
||||
return false;
|
||||
|
@ -997,7 +997,9 @@ public class NetworkManager : INetworkManager, IDisposable
|
|||
// Get interface matching override subnet
|
||||
var intf = _interfaces.OrderBy(x => x.Index).FirstOrDefault(x => data.Data.Subnet.Contains(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
|
||||
bindPreference = data.OverrideUri;
|
||||
|
@ -1025,6 +1027,7 @@ public class NetworkManager : INetworkManager, IDisposable
|
|||
}
|
||||
|
||||
_logger.LogDebug("{Source}: Matching bind address override found: {Address}", source, bindPreference);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -1062,7 +1065,7 @@ public class NetworkManager : INetworkManager, IDisposable
|
|||
// 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.
|
||||
bindAddress = externalInterfaces
|
||||
.OrderByDescending(x => x.Subnet.Contains(source))
|
||||
.OrderBy(x => x.Subnet.Contains(source))
|
||||
.ThenBy(x => x.Index)
|
||||
.Select(x => x.Address)
|
||||
.First();
|
||||
|
@ -1079,7 +1082,7 @@ public class NetworkManager : INetworkManager, IDisposable
|
|||
// 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.
|
||||
bindAddress = _interfaces.Where(x => IsInLocalNetwork(x.Address))
|
||||
.OrderByDescending(x => x.Subnet.Contains(source))
|
||||
.OrderBy(x => x.Subnet.Contains(source))
|
||||
.ThenBy(x => x.Index)
|
||||
.Select(x => x.Address)
|
||||
.FirstOrDefault();
|
||||
|
|
|
@ -292,6 +292,9 @@ namespace Jellyfin.Providers.Tests.Manager
|
|||
providerManager.Setup(pm => pm.SaveImage(item, It.IsAny<Stream>(), It.IsAny<string>(), imageType, null, It.IsAny<CancellationToken>()))
|
||||
.Callback<BaseItem, Stream, string, ImageType, int?, CancellationToken>((callbackItem, _, _, callbackType, _, _) => callbackItem.SetImagePath(callbackType, 0, new FileSystemMetadata()))
|
||||
.Returns(Task.CompletedTask);
|
||||
providerManager.Setup(pm => pm.SaveImage(item, It.IsAny<string>(), It.IsAny<string>(), imageType, null, null, It.IsAny<CancellationToken>()))
|
||||
.Callback<BaseItem, string, string, ImageType, int?, bool?, CancellationToken>((callbackItem, _, _, callbackType, _, _, _) => callbackItem.SetImagePath(callbackType, 0, new FileSystemMetadata()))
|
||||
.Returns(Task.CompletedTask);
|
||||
var itemImageProvider = GetItemImageProvider(providerManager.Object, null);
|
||||
var result = await itemImageProvider.RefreshImages(item, libraryOptions, new List<IImageProvider> { dynamicProvider.Object }, refreshOptions, CancellationToken.None);
|
||||
|
||||
|
|
|
@ -84,7 +84,7 @@ namespace Jellyfin.Server.Implementations.Tests.Localization
|
|||
await localizationManager.LoadAll();
|
||||
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));
|
||||
Assert.NotNull(tvma);
|
||||
|
|
|
@ -257,5 +257,23 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers
|
|||
|
||||
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>
|
Loading…
Add table
Add a link
Reference in a new issue