mirror of
https://github.com/jellyfin/jellyfin.git
synced 2026-01-18 17:18:08 +00:00
Compare commits
83 Commits
openapi-ca
...
v10.10.4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cf78aefbb7 | ||
|
|
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 | ||
|
|
e7ac3e3929 | ||
|
|
9464f9e622 | ||
|
|
746280af0b | ||
|
|
9bc6e8a306 | ||
|
|
b0105179eb | ||
|
|
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 |
@@ -3,7 +3,7 @@
|
|||||||
"isRoot": true,
|
"isRoot": true,
|
||||||
"tools": {
|
"tools": {
|
||||||
"dotnet-ef": {
|
"dotnet-ef": {
|
||||||
"version": "8.0.8",
|
"version": "8.0.11",
|
||||||
"commands": [
|
"commands": [
|
||||||
"dotnet-ef"
|
"dotnet-ef"
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -192,6 +192,8 @@
|
|||||||
- [jaina heartles](https://github.com/heartles)
|
- [jaina heartles](https://github.com/heartles)
|
||||||
- [oxixes](https://github.com/oxixes)
|
- [oxixes](https://github.com/oxixes)
|
||||||
- [elfalem](https://github.com/elfalem)
|
- [elfalem](https://github.com/elfalem)
|
||||||
|
- [benedikt257](https://github.com/benedikt257)
|
||||||
|
- [revam](https://github.com/revam)
|
||||||
|
|
||||||
# Emby Contributors
|
# Emby Contributors
|
||||||
|
|
||||||
|
|||||||
@@ -17,21 +17,21 @@
|
|||||||
<PackageVersion Include="DiscUtils.Udf" Version="0.16.13" />
|
<PackageVersion Include="DiscUtils.Udf" Version="0.16.13" />
|
||||||
<PackageVersion Include="DotNet.Glob" Version="3.1.3" />
|
<PackageVersion Include="DotNet.Glob" Version="3.1.3" />
|
||||||
<PackageVersion Include="FsCheck.Xunit" Version="2.16.6" />
|
<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="ICU4N.Transliterator" Version="60.1.0-alpha.356" />
|
||||||
<PackageVersion Include="IDisposableAnalyzers" Version="4.0.8" />
|
<PackageVersion Include="IDisposableAnalyzers" Version="4.0.8" />
|
||||||
<PackageVersion Include="Jellyfin.XmlTv" Version="10.8.0" />
|
<PackageVersion Include="Jellyfin.XmlTv" Version="10.8.0" />
|
||||||
<PackageVersion Include="libse" Version="4.0.8" />
|
<PackageVersion Include="libse" Version="4.0.8" />
|
||||||
<PackageVersion Include="LrcParser" Version="2024.0728.2" />
|
<PackageVersion Include="LrcParser" Version="2024.0728.2" />
|
||||||
<PackageVersion Include="MetaBrainz.MusicBrainz" Version="6.1.0" />
|
<PackageVersion Include="MetaBrainz.MusicBrainz" Version="6.1.0" />
|
||||||
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="8.0.10" />
|
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="8.0.11" />
|
||||||
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.10" />
|
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.11" />
|
||||||
<PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4" />
|
<PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4" />
|
||||||
<PackageVersion Include="Microsoft.Data.Sqlite" Version="8.0.10" />
|
<PackageVersion Include="Microsoft.Data.Sqlite" Version="8.0.11" />
|
||||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.10" />
|
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.11" />
|
||||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.10" />
|
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.11" />
|
||||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.10" />
|
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.11" />
|
||||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.10" />
|
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.11" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="8.0.0" />
|
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="8.0.0" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="8.0.1" />
|
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="8.0.1" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" />
|
<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.Configuration.Json" Version="8.0.1" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.2" />
|
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.2" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
|
<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.EntityFrameworkCore" Version="8.0.11" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="8.0.10" />
|
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="8.0.11" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.1" />
|
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.1" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.Http" Version="8.0.1" />
|
<PackageVersion Include="Microsoft.Extensions.Http" Version="8.0.1" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.2" />
|
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.2" />
|
||||||
@@ -51,7 +51,7 @@
|
|||||||
<PackageVersion Include="MimeTypes" Version="2.4.0" />
|
<PackageVersion Include="MimeTypes" Version="2.4.0" />
|
||||||
<PackageVersion Include="Mono.Nat" Version="3.0.4" />
|
<PackageVersion Include="Mono.Nat" Version="3.0.4" />
|
||||||
<PackageVersion Include="Moq" Version="4.18.4" />
|
<PackageVersion Include="Moq" Version="4.18.4" />
|
||||||
<PackageVersion Include="NEbml" Version="0.11.0" />
|
<PackageVersion Include="NEbml" Version="0.12.0" />
|
||||||
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
|
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
|
||||||
<PackageVersion Include="PlaylistsNET" Version="1.4.1" />
|
<PackageVersion Include="PlaylistsNET" Version="1.4.1" />
|
||||||
<PackageVersion Include="prometheus-net.AspNetCore" Version="8.2.1" />
|
<PackageVersion Include="prometheus-net.AspNetCore" Version="8.2.1" />
|
||||||
@@ -66,9 +66,9 @@
|
|||||||
<PackageVersion Include="Serilog.Sinks.Graylog" Version="3.1.1" />
|
<PackageVersion Include="Serilog.Sinks.Graylog" Version="3.1.1" />
|
||||||
<PackageVersion Include="SerilogAnalyzer" Version="0.15.0" />
|
<PackageVersion Include="SerilogAnalyzer" Version="0.15.0" />
|
||||||
<PackageVersion Include="SharpFuzz" Version="2.1.1" />
|
<PackageVersion Include="SharpFuzz" Version="2.1.1" />
|
||||||
<PackageVersion Include="SkiaSharp" Version="2.88.8" />
|
<PackageVersion Include="SkiaSharp" Version="2.88.9" />
|
||||||
<PackageVersion Include="SkiaSharp.HarfBuzz" Version="2.88.8" />
|
<PackageVersion Include="SkiaSharp.HarfBuzz" Version="2.88.9" />
|
||||||
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="2.88.8" />
|
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="2.88.9" />
|
||||||
<PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" />
|
<PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" />
|
||||||
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
|
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
|
||||||
<PackageVersion Include="Svg.Skia" Version="2.0.0.1" />
|
<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.Text.Json" Version="8.0.5" />
|
||||||
<PackageVersion Include="System.Threading.Tasks.Dataflow" Version="8.0.1" />
|
<PackageVersion Include="System.Threading.Tasks.Dataflow" Version="8.0.1" />
|
||||||
<PackageVersion Include="TagLibSharp" Version="2.3.0" />
|
<PackageVersion Include="TagLibSharp" Version="2.3.0" />
|
||||||
<PackageVersion Include="z440.atl.core" Version="6.6.0" />
|
<PackageVersion Include="z440.atl.core" Version="6.11.0" />
|
||||||
<PackageVersion Include="TMDbLib" Version="2.2.0" />
|
<PackageVersion Include="TMDbLib" Version="2.2.0" />
|
||||||
<PackageVersion Include="UTF.Unknown" Version="2.5.1" />
|
<PackageVersion Include="UTF.Unknown" Version="2.5.1" />
|
||||||
<PackageVersion Include="Xunit.Priority" Version="1.1.6" />
|
<PackageVersion Include="Xunit.Priority" Version="1.1.6" />
|
||||||
@@ -88,4 +88,4 @@
|
|||||||
<PackageVersion Include="Xunit.SkippableFact" Version="1.4.13" />
|
<PackageVersion Include="Xunit.SkippableFact" Version="1.4.13" />
|
||||||
<PackageVersion Include="xunit" Version="2.9.2" />
|
<PackageVersion Include="xunit" Version="2.9.2" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -36,7 +36,7 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Authors>Jellyfin Contributors</Authors>
|
<Authors>Jellyfin Contributors</Authors>
|
||||||
<PackageId>Jellyfin.Naming</PackageId>
|
<PackageId>Jellyfin.Naming</PackageId>
|
||||||
<VersionPrefix>10.10.0</VersionPrefix>
|
<VersionPrefix>10.10.4</VersionPrefix>
|
||||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ namespace Emby.Server.Implementations
|
|||||||
{ DefaultRedirectKey, "web/" },
|
{ DefaultRedirectKey, "web/" },
|
||||||
{ FfmpegProbeSizeKey, "1G" },
|
{ FfmpegProbeSizeKey, "1G" },
|
||||||
{ FfmpegAnalyzeDurationKey, "200M" },
|
{ FfmpegAnalyzeDurationKey, "200M" },
|
||||||
{ PlaylistsAllowDuplicatesKey, bool.FalseString },
|
|
||||||
{ BindToUnixSocketKey, bool.FalseString },
|
{ BindToUnixSocketKey, bool.FalseString },
|
||||||
{ SqliteCacheSizeKey, "20000" },
|
{ SqliteCacheSizeKey, "20000" },
|
||||||
{ FfmpegSkipValidationKey, bool.FalseString },
|
{ FfmpegSkipValidationKey, bool.FalseString },
|
||||||
|
|||||||
@@ -122,7 +122,6 @@ namespace Emby.Server.Implementations.Images
|
|||||||
}
|
}
|
||||||
|
|
||||||
await ProviderManager.SaveImage(item, outputPath, mimeType, imageType, null, false, cancellationToken).ConfigureAwait(false);
|
await ProviderManager.SaveImage(item, outputPath, mimeType, imageType, null, false, cancellationToken).ConfigureAwait(false);
|
||||||
File.Delete(outputPath);
|
|
||||||
|
|
||||||
return ItemUpdateType.ImageUpdate;
|
return ItemUpdateType.ImageUpdate;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
|
|||||||
{
|
{
|
||||||
if (args.IsDirectory)
|
if (args.IsDirectory)
|
||||||
{
|
{
|
||||||
// It's a boxset if the path is a directory with [playlist] in its name
|
// It's a playlist if the path is a directory with [playlist] in its name
|
||||||
var filename = Path.GetFileName(Path.TrimEndingDirectorySeparator(args.Path));
|
var filename = Path.GetFileName(Path.TrimEndingDirectorySeparator(args.Path));
|
||||||
if (string.IsNullOrEmpty(filename))
|
if (string.IsNullOrEmpty(filename))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -216,14 +216,11 @@ namespace Emby.Server.Implementations.Playlists
|
|||||||
var newItems = GetPlaylistItems(newItemIds, user, options)
|
var newItems = GetPlaylistItems(newItemIds, user, options)
|
||||||
.Where(i => i.SupportsAddingToPlaylist);
|
.Where(i => i.SupportsAddingToPlaylist);
|
||||||
|
|
||||||
// Filter out duplicate items, if necessary
|
// Filter out duplicate items
|
||||||
if (!_appConfig.DoPlaylistsAllowDuplicates())
|
var existingIds = playlist.LinkedChildren.Select(c => c.ItemId).ToHashSet();
|
||||||
{
|
newItems = newItems
|
||||||
var existingIds = playlist.LinkedChildren.Select(c => c.ItemId).ToHashSet();
|
.Where(i => !existingIds.Contains(i.Id))
|
||||||
newItems = newItems
|
.Distinct();
|
||||||
.Where(i => !existingIds.Contains(i.Id))
|
|
||||||
.Distinct();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a list of the new linked children to add to the playlist
|
// Create a list of the new linked children to add to the playlist
|
||||||
var childrenToAdd = newItems
|
var childrenToAdd = newItems
|
||||||
@@ -269,7 +266,7 @@ namespace Emby.Server.Implementations.Playlists
|
|||||||
|
|
||||||
var idList = entryIds.ToList();
|
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)
|
playlist.LinkedChildren = children.Except(removals)
|
||||||
.Select(i => i.Item1)
|
.Select(i => i.Item1)
|
||||||
@@ -286,26 +283,39 @@ namespace Emby.Server.Implementations.Playlists
|
|||||||
RefreshPriority.High);
|
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)
|
if (_libraryManager.GetItemById(playlistId) is not Playlist playlist)
|
||||||
{
|
{
|
||||||
throw new ArgumentException("No Playlist exists with the supplied Id");
|
throw new ArgumentException("No Playlist exists with the supplied Id");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var user = _userManager.GetUserById(callingUserId);
|
||||||
var children = playlist.GetManageableItems().ToList();
|
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;
|
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();
|
var newList = playlist.LinkedChildren.ToList();
|
||||||
|
|
||||||
newList.Remove(item);
|
newList.Remove(item);
|
||||||
|
|
||||||
if (newIndex >= newList.Count)
|
if (newIndex >= newList.Count)
|
||||||
@@ -314,7 +324,7 @@ namespace Emby.Server.Implementations.Playlists
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
newList.Insert(newIndex, item);
|
newList.Insert(adjustedNewIndex, item);
|
||||||
}
|
}
|
||||||
|
|
||||||
playlist.LinkedChildren = [.. newList];
|
playlist.LinkedChildren = [.. newList];
|
||||||
|
|||||||
@@ -1938,7 +1938,11 @@ namespace Emby.Server.Implementations.Session
|
|||||||
// Don't report acceleration type for non-admin users.
|
// Don't report acceleration type for non-admin users.
|
||||||
result = result.Select(r =>
|
result = result.Select(r =>
|
||||||
{
|
{
|
||||||
r.TranscodingInfo.HardwareAccelerationType = HardwareAccelerationType.none;
|
if (r.TranscodingInfo is not null)
|
||||||
|
{
|
||||||
|
r.TranscodingInfo.HardwareAccelerationType = HardwareAccelerationType.none;
|
||||||
|
}
|
||||||
|
|
||||||
return r;
|
return r;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1819,16 +1819,13 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||||||
if (isActualOutputVideoCodecHevc || isActualOutputVideoCodecAv1)
|
if (isActualOutputVideoCodecHevc || isActualOutputVideoCodecAv1)
|
||||||
{
|
{
|
||||||
var requestedRange = state.GetRequestedRangeTypes(state.ActualOutputVideoCodec);
|
var requestedRange = state.GetRequestedRangeTypes(state.ActualOutputVideoCodec);
|
||||||
var requestHasDOVI = requestedRange.Contains(VideoRangeType.DOVI.ToString(), StringComparison.OrdinalIgnoreCase);
|
// Clients reporting Dolby Vision capabilities with fallbacks may only support the fallback layer.
|
||||||
var requestHasDOVIWithHDR10 = requestedRange.Contains(VideoRangeType.DOVIWithHDR10.ToString(), StringComparison.OrdinalIgnoreCase);
|
// Only enable Dolby Vision remuxing if the client explicitly declares support for profiles without fallbacks.
|
||||||
var requestHasDOVIWithHLG = requestedRange.Contains(VideoRangeType.DOVIWithHLG.ToString(), StringComparison.OrdinalIgnoreCase);
|
var clientSupportsDoVi = requestedRange.Contains(VideoRangeType.DOVI.ToString(), StringComparison.OrdinalIgnoreCase);
|
||||||
var requestHasDOVIWithSDR = requestedRange.Contains(VideoRangeType.DOVIWithSDR.ToString(), StringComparison.OrdinalIgnoreCase);
|
var videoIsDoVi = state.VideoStream.VideoRangeType is VideoRangeType.DOVI or VideoRangeType.DOVIWithHDR10 or VideoRangeType.DOVIWithHLG or VideoRangeType.DOVIWithSDR;
|
||||||
|
|
||||||
if (EncodingHelper.IsCopyCodec(codec)
|
if (EncodingHelper.IsCopyCodec(codec)
|
||||||
&& ((state.VideoStream.VideoRangeType == VideoRangeType.DOVI && requestHasDOVI)
|
&& (videoIsDoVi && clientSupportsDoVi))
|
||||||
|| (state.VideoStream.VideoRangeType == VideoRangeType.DOVIWithHDR10 && requestHasDOVIWithHDR10)
|
|
||||||
|| (state.VideoStream.VideoRangeType == VideoRangeType.DOVIWithHLG && requestHasDOVIWithHLG)
|
|
||||||
|| (state.VideoStream.VideoRangeType == VideoRangeType.DOVIWithSDR && requestHasDOVIWithSDR)))
|
|
||||||
{
|
{
|
||||||
if (isActualOutputVideoCodecHevc)
|
if (isActualOutputVideoCodecHevc)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ public class ItemRefreshController : BaseJellyfinApiController
|
|||||||
/// <param name="imageRefreshMode">(Optional) Specifies the image refresh mode.</param>
|
/// <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="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="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="204">Item metadata refresh queued.</response>
|
||||||
/// <response code="404">Item to refresh not found.</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>
|
/// <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 metadataRefreshMode = MetadataRefreshMode.None,
|
||||||
[FromQuery] MetadataRefreshMode imageRefreshMode = MetadataRefreshMode.None,
|
[FromQuery] MetadataRefreshMode imageRefreshMode = MetadataRefreshMode.None,
|
||||||
[FromQuery] bool replaceAllMetadata = false,
|
[FromQuery] bool replaceAllMetadata = false,
|
||||||
[FromQuery] bool replaceAllImages = false)
|
[FromQuery] bool replaceAllImages = false,
|
||||||
|
[FromQuery] bool regenerateTrickplay = false)
|
||||||
{
|
{
|
||||||
var item = _libraryManager.GetItemById<BaseItem>(itemId, User.GetUserId());
|
var item = _libraryManager.GetItemById<BaseItem>(itemId, User.GetUserId());
|
||||||
if (item is null)
|
if (item is null)
|
||||||
@@ -81,7 +83,8 @@ public class ItemRefreshController : BaseJellyfinApiController
|
|||||||
|| replaceAllImages
|
|| replaceAllImages
|
||||||
|| replaceAllMetadata,
|
|| replaceAllMetadata,
|
||||||
IsAutomated = false,
|
IsAutomated = false,
|
||||||
RemoveOldMetadata = replaceAllMetadata
|
RemoveOldMetadata = replaceAllMetadata,
|
||||||
|
RegenerateTrickplay = regenerateTrickplay
|
||||||
};
|
};
|
||||||
|
|
||||||
_providerManager.QueueRefresh(item.Id, refreshOptions, RefreshPriority.High);
|
_providerManager.QueueRefresh(item.Id, refreshOptions, RefreshPriority.High);
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ public class MediaSegmentsController : BaseJellyfinApiController
|
|||||||
return NotFound();
|
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()));
|
return Ok(new QueryResult<MediaSegmentDto>(items.ToArray()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Jellyfin.Api.Attributes;
|
using Jellyfin.Api.Attributes;
|
||||||
@@ -426,7 +427,7 @@ public class PlaylistsController : BaseJellyfinApiController
|
|||||||
return Forbid();
|
return Forbid();
|
||||||
}
|
}
|
||||||
|
|
||||||
await _playlistManager.MoveItemAsync(playlistId, itemId, newIndex).ConfigureAwait(false);
|
await _playlistManager.MoveItemAsync(playlistId, itemId, newIndex, callingUserId).ConfigureAwait(false);
|
||||||
return NoContent();
|
return NoContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -514,7 +515,8 @@ public class PlaylistsController : BaseJellyfinApiController
|
|||||||
return Forbid();
|
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;
|
var count = items.Length;
|
||||||
if (startIndex.HasValue)
|
if (startIndex.HasValue)
|
||||||
{
|
{
|
||||||
@@ -529,11 +531,11 @@ public class PlaylistsController : BaseJellyfinApiController
|
|||||||
var dtoOptions = new DtoOptions { Fields = fields }
|
var dtoOptions = new DtoOptions { Fields = fields }
|
||||||
.AddClientFields(User)
|
.AddClientFields(User)
|
||||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
||||||
var user = _userManager.GetUserById(callingUserId);
|
|
||||||
var dtos = _dtoService.GetBaseItemDtos(items.Select(i => i.Item2).ToList(), dtoOptions, user);
|
var dtos = _dtoService.GetBaseItemDtos(items.Select(i => i.Item2).ToList(), dtoOptions, user);
|
||||||
for (int index = 0; index < dtos.Count; index++)
|
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>(
|
var result = new QueryResult<BaseItemDto>(
|
||||||
|
|||||||
@@ -235,6 +235,11 @@ public static class StreamingHelpers
|
|||||||
state.VideoRequest.MaxHeight = resolution.MaxHeight;
|
state.VideoRequest.MaxHeight = resolution.MaxHeight;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (state.AudioStream is not null && !EncodingHelper.IsCopyCodec(state.OutputAudioCodec) && string.Equals(state.AudioStream.Codec, state.OutputAudioCodec, StringComparison.OrdinalIgnoreCase) && state.OutputAudioBitrate.HasValue)
|
||||||
|
{
|
||||||
|
state.OutputAudioCodec = state.SupportedAudioCodecs.Where(c => !EncodingHelper.LosslessAudioCodecs.Contains(c)).FirstOrDefault(mediaEncoder.CanEncodeToAudioCodec);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var ext = string.IsNullOrWhiteSpace(state.OutputContainer)
|
var ext = string.IsNullOrWhiteSpace(state.OutputContainer)
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Authors>Jellyfin Contributors</Authors>
|
<Authors>Jellyfin Contributors</Authors>
|
||||||
<PackageId>Jellyfin.Data</PackageId>
|
<PackageId>Jellyfin.Data</PackageId>
|
||||||
<VersionPrefix>10.10.0</VersionPrefix>
|
<VersionPrefix>10.10.4</VersionPrefix>
|
||||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|||||||
@@ -139,23 +139,53 @@ public class MediaSegmentManager : IMediaSegmentManager
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <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);
|
using var db = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
var query = db.MediaSegments
|
var query = db.MediaSegments
|
||||||
.Where(e => e.ItemId.Equals(itemId));
|
.Where(e => e.ItemId.Equals(item.Id));
|
||||||
|
|
||||||
if (typeFilter is not null)
|
if (typeFilter is not null)
|
||||||
{
|
{
|
||||||
query = query.Where(e => typeFilter.Contains(e.Type));
|
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
|
return query
|
||||||
.OrderBy(e => e.StartTicks)
|
.OrderBy(e => e.StartTicks)
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.ToImmutableList()
|
.AsEnumerable()
|
||||||
.Select(Map);
|
.Select(Map)
|
||||||
|
.ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static MediaSegmentDto Map(MediaSegment segment)
|
private static MediaSegmentDto Map(MediaSegment segment)
|
||||||
|
|||||||
@@ -194,6 +194,14 @@ public class TrickplayManager : ITrickplayManager
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We support video backdrops, but we should not generate trickplay images for them
|
||||||
|
var parentDirectory = Directory.GetParent(mediaPath);
|
||||||
|
if (parentDirectory is not null && string.Equals(parentDirectory.Name, "backdrops", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Ignoring backdrop media found at {Path} for item {ItemID}", mediaPath, video.Id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// The width has to be even, otherwise a lot of filters will not be able to sample it
|
// The width has to be even, otherwise a lot of filters will not be able to sample it
|
||||||
var actualWidth = 2 * (width / 2);
|
var actualWidth = 2 * (width / 2);
|
||||||
|
|
||||||
@@ -238,7 +246,7 @@ public class TrickplayManager : ITrickplayManager
|
|||||||
foreach (var tile in existingFiles)
|
foreach (var tile in existingFiles)
|
||||||
{
|
{
|
||||||
var image = _imageEncoder.GetImageSize(tile);
|
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));
|
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);
|
localTrickplayInfo.Bandwidth = Math.Max(localTrickplayInfo.Bandwidth, bitrate);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ namespace Jellyfin.Server.Infrastructure
|
|||||||
count: null);
|
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);
|
var fileInfo = GetFileInfo(filePath);
|
||||||
if (offset < 0 || offset > fileInfo.Length)
|
if (offset < 0 || offset > fileInfo.Length)
|
||||||
@@ -118,6 +118,9 @@ namespace Jellyfin.Server.Infrastructure
|
|||||||
// Copied from SendFileFallback.SendFileAsync
|
// Copied from SendFileFallback.SendFileAsync
|
||||||
const int BufferSize = 1024 * 16;
|
const int BufferSize = 1024 * 16;
|
||||||
|
|
||||||
|
var useRequestAborted = !cancellationToken.CanBeCanceled;
|
||||||
|
var localCancel = useRequestAborted ? response.HttpContext.RequestAborted : cancellationToken;
|
||||||
|
|
||||||
var fileStream = new FileStream(
|
var fileStream = new FileStream(
|
||||||
filePath,
|
filePath,
|
||||||
FileMode.Open,
|
FileMode.Open,
|
||||||
@@ -127,10 +130,17 @@ namespace Jellyfin.Server.Infrastructure
|
|||||||
options: FileOptions.Asynchronous | FileOptions.SequentialScan);
|
options: FileOptions.Asynchronous | FileOptions.SequentialScan);
|
||||||
await using (fileStream.ConfigureAwait(false))
|
await using (fileStream.ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
fileStream.Seek(offset, SeekOrigin.Begin);
|
try
|
||||||
await StreamCopyOperation
|
{
|
||||||
.CopyToAsync(fileStream, response.Body, count, BufferSize, CancellationToken.None)
|
localCancel.ThrowIfCancellationRequested();
|
||||||
.ConfigureAwait(true);
|
fileStream.Seek(offset, SeekOrigin.Begin);
|
||||||
|
await StreamCopyOperation
|
||||||
|
.CopyToAsync(fileStream, response.Body, count, BufferSize, localCancel)
|
||||||
|
.ConfigureAwait(true);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException) when (useRequestAborted)
|
||||||
|
{
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -47,7 +47,8 @@ namespace Jellyfin.Server.Migrations
|
|||||||
typeof(Routines.AddDefaultCastReceivers),
|
typeof(Routines.AddDefaultCastReceivers),
|
||||||
typeof(Routines.UpdateDefaultPluginRepository),
|
typeof(Routines.UpdateDefaultPluginRepository),
|
||||||
typeof(Routines.FixAudioData),
|
typeof(Routines.FixAudioData),
|
||||||
typeof(Routines.MoveTrickplayFiles)
|
typeof(Routines.MoveTrickplayFiles),
|
||||||
|
typeof(Routines.RemoveDuplicatePlaylistChildren)
|
||||||
};
|
};
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -15,12 +15,12 @@ namespace Jellyfin.Server.Migrations.Routines;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
internal class FixPlaylistOwner : IMigrationRoutine
|
internal class FixPlaylistOwner : IMigrationRoutine
|
||||||
{
|
{
|
||||||
private readonly ILogger<RemoveDuplicateExtras> _logger;
|
private readonly ILogger<FixPlaylistOwner> _logger;
|
||||||
private readonly ILibraryManager _libraryManager;
|
private readonly ILibraryManager _libraryManager;
|
||||||
private readonly IPlaylistManager _playlistManager;
|
private readonly IPlaylistManager _playlistManager;
|
||||||
|
|
||||||
public FixPlaylistOwner(
|
public FixPlaylistOwner(
|
||||||
ILogger<RemoveDuplicateExtras> logger,
|
ILogger<FixPlaylistOwner> logger,
|
||||||
ILibraryManager libraryManager,
|
ILibraryManager libraryManager,
|
||||||
IPlaylistManager playlistManager)
|
IPlaylistManager playlistManager)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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>
|
<PropertyGroup>
|
||||||
<Authors>Jellyfin Contributors</Authors>
|
<Authors>Jellyfin Contributors</Authors>
|
||||||
<PackageId>Jellyfin.Common</PackageId>
|
<PackageId>Jellyfin.Common</PackageId>
|
||||||
<VersionPrefix>10.10.0</VersionPrefix>
|
<VersionPrefix>10.10.4</VersionPrefix>
|
||||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Text.Json.Serialization;
|
|
||||||
|
|
||||||
namespace MediaBrowser.Controller.Entities
|
namespace MediaBrowser.Controller.Entities
|
||||||
{
|
{
|
||||||
@@ -12,7 +11,6 @@ namespace MediaBrowser.Controller.Entities
|
|||||||
{
|
{
|
||||||
public LinkedChild()
|
public LinkedChild()
|
||||||
{
|
{
|
||||||
Id = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public string Path { get; set; }
|
public string Path { get; set; }
|
||||||
@@ -21,9 +19,6 @@ namespace MediaBrowser.Controller.Entities
|
|||||||
|
|
||||||
public string LibraryItemId { get; set; }
|
public string LibraryItemId { get; set; }
|
||||||
|
|
||||||
[JsonIgnore]
|
|
||||||
public string Id { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the linked item id.
|
/// Gets or sets the linked item id.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -31,6 +26,8 @@ namespace MediaBrowser.Controller.Entities
|
|||||||
|
|
||||||
public static LinkedChild Create(BaseItem item)
|
public static LinkedChild Create(BaseItem item)
|
||||||
{
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(item);
|
||||||
|
|
||||||
var child = new LinkedChild
|
var child = new LinkedChild
|
||||||
{
|
{
|
||||||
Path = item.Path,
|
Path = item.Path,
|
||||||
|
|||||||
@@ -49,11 +49,6 @@ namespace MediaBrowser.Controller.Extensions
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public const string FfmpegPathKey = "ffmpeg";
|
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>
|
/// <summary>
|
||||||
/// The key for a setting that indicates whether kestrel should bind to a unix socket.
|
/// The key for a setting that indicates whether kestrel should bind to a unix socket.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -120,14 +115,6 @@ namespace MediaBrowser.Controller.Extensions
|
|||||||
public static bool GetFFmpegImgExtractPerfTradeoff(this IConfiguration configuration)
|
public static bool GetFFmpegImgExtractPerfTradeoff(this IConfiguration configuration)
|
||||||
=> configuration.GetValue<bool>(FfmpegImgExtractPerfTradeoffKey);
|
=> 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>
|
/// <summary>
|
||||||
/// Gets a value indicating whether kestrel should bind to a unix socket from the <see cref="IConfiguration" />.
|
/// Gets a value indicating whether kestrel should bind to a unix socket from the <see cref="IConfiguration" />.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Authors>Jellyfin Contributors</Authors>
|
<Authors>Jellyfin Contributors</Authors>
|
||||||
<PackageId>Jellyfin.Controller</PackageId>
|
<PackageId>Jellyfin.Controller</PackageId>
|
||||||
<VersionPrefix>10.10.0</VersionPrefix>
|
<VersionPrefix>10.10.4</VersionPrefix>
|
||||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|||||||
@@ -309,7 +309,6 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
private bool IsSwTonemapAvailable(EncodingJobInfo state, EncodingOptions options)
|
private bool IsSwTonemapAvailable(EncodingJobInfo state, EncodingOptions options)
|
||||||
{
|
{
|
||||||
if (state.VideoStream is null
|
if (state.VideoStream is null
|
||||||
|| !options.EnableTonemapping
|
|
||||||
|| GetVideoColorBitDepth(state) < 10
|
|| GetVideoColorBitDepth(state) < 10
|
||||||
|| !_mediaEncoder.SupportsFilter("tonemapx"))
|
|| !_mediaEncoder.SupportsFilter("tonemapx"))
|
||||||
{
|
{
|
||||||
@@ -2061,7 +2060,13 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
// libx265 only accept level option in -x265-params.
|
// libx265 only accept level option in -x265-params.
|
||||||
// level option may cause libx265 to fail.
|
// level option may cause libx265 to fail.
|
||||||
// libx265 cannot adjust the given level, just throw an error.
|
// libx265 cannot adjust the given level, just throw an error.
|
||||||
param += " -x265-params:0 subme=3:merange=25:rc-lookahead=10:me=star:ctu=32:max-tu-size=32:min-cu-size=16:rskip=2:rskip-edge-threshold=2:no-sao=1:no-strong-intra-smoothing=1:no-scenecut=1:no-open-gop=1:no-info=1";
|
param += " -x265-params:0 no-scenecut=1:no-open-gop=1:no-info=1";
|
||||||
|
|
||||||
|
if (encodingOptions.EncoderPreset < EncoderPreset.ultrafast)
|
||||||
|
{
|
||||||
|
// The following params are slower than the ultrafast preset, don't use when ultrafast is selected.
|
||||||
|
param += ":subme=3:merange=25:rc-lookahead=10:me=star:ctu=32:max-tu-size=32:min-cu-size=16:rskip=2:rskip-edge-threshold=2:no-sao=1:no-strong-intra-smoothing=1";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.Equals(videoEncoder, "libsvtav1", StringComparison.OrdinalIgnoreCase)
|
if (string.Equals(videoEncoder, "libsvtav1", StringComparison.OrdinalIgnoreCase)
|
||||||
@@ -2196,7 +2201,10 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
{
|
{
|
||||||
var videoFrameRate = videoStream.ReferenceFrameRate;
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -3318,24 +3326,25 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
&& options.VppTonemappingBrightness >= -100
|
&& options.VppTonemappingBrightness >= -100
|
||||||
&& options.VppTonemappingBrightness <= 100)
|
&& options.VppTonemappingBrightness <= 100)
|
||||||
{
|
{
|
||||||
procampParams += $"=b={options.VppTonemappingBrightness}";
|
procampParams += "procamp_vaapi=b={0}";
|
||||||
doVaVppProcamp = true;
|
doVaVppProcamp = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.VppTonemappingContrast > 1
|
if (options.VppTonemappingContrast > 1
|
||||||
&& options.VppTonemappingContrast <= 10)
|
&& options.VppTonemappingContrast <= 10)
|
||||||
{
|
{
|
||||||
procampParams += doVaVppProcamp ? ":" : "=";
|
procampParams += doVaVppProcamp ? ":c={1}" : "procamp_vaapi=c={1}";
|
||||||
procampParams += $"c={options.VppTonemappingContrast}";
|
|
||||||
doVaVppProcamp = true;
|
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(
|
return string.Format(
|
||||||
CultureInfo.InvariantCulture,
|
CultureInfo.InvariantCulture,
|
||||||
args,
|
args,
|
||||||
doVaVppProcamp ? $"procamp_vaapi{procampParams}," : string.Empty,
|
options.VppTonemappingBrightness,
|
||||||
|
options.VppTonemappingContrast,
|
||||||
|
doVaVppProcamp ? "," : string.Empty,
|
||||||
videoFormat ?? "nv12");
|
videoFormat ?? "nv12");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -3523,20 +3532,29 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
{
|
{
|
||||||
// tonemapx requires yuv420p10 input for dovi reshaping, let ffmpeg convert the frame when necessary
|
// tonemapx requires yuv420p10 input for dovi reshaping, let ffmpeg convert the frame when necessary
|
||||||
var tonemapFormat = requireDoviReshaping ? "yuv420p" : outFormat;
|
var tonemapFormat = requireDoviReshaping ? "yuv420p" : outFormat;
|
||||||
|
var tonemapArgString = "tonemapx=tonemap={0}:desat={1}:peak={2}:t=bt709:m=bt709:p=bt709:format={3}";
|
||||||
var tonemapArgs = $"tonemapx=tonemap={options.TonemappingAlgorithm}:desat={options.TonemappingDesat}:peak={options.TonemappingPeak}:t=bt709:m=bt709:p=bt709:format={tonemapFormat}";
|
|
||||||
|
|
||||||
if (options.TonemappingParam != 0)
|
if (options.TonemappingParam != 0)
|
||||||
{
|
{
|
||||||
tonemapArgs += $":param={options.TonemappingParam}";
|
tonemapArgString += ":param={4}";
|
||||||
}
|
}
|
||||||
|
|
||||||
var range = options.TonemappingRange;
|
var range = options.TonemappingRange;
|
||||||
if (range == TonemappingRange.tv || range == TonemappingRange.pc)
|
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);
|
mainFilters.Add(tonemapArgs);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -4128,31 +4146,46 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
else if (isD3d11vaDecoder || isQsvDecoder)
|
else if (isD3d11vaDecoder || isQsvDecoder)
|
||||||
{
|
{
|
||||||
var isRext = IsVideoStreamHevcRext(state);
|
var isRext = IsVideoStreamHevcRext(state);
|
||||||
var twoPassVppTonemap = isRext;
|
var twoPassVppTonemap = false;
|
||||||
var doVppFullRangeOut = isMjpegEncoder
|
var doVppFullRangeOut = isMjpegEncoder
|
||||||
&& _mediaEncoder.EncoderVersion >= _minFFmpegQsvVppOutRangeOption;
|
&& _mediaEncoder.EncoderVersion >= _minFFmpegQsvVppOutRangeOption;
|
||||||
var doVppScaleModeHq = isMjpegEncoder
|
var doVppScaleModeHq = isMjpegEncoder
|
||||||
&& _mediaEncoder.EncoderVersion >= _minFFmpegQsvVppScaleModeOption;
|
&& _mediaEncoder.EncoderVersion >= _minFFmpegQsvVppScaleModeOption;
|
||||||
var doVppProcamp = false;
|
var doVppProcamp = false;
|
||||||
var procampParams = string.Empty;
|
var procampParams = string.Empty;
|
||||||
|
var procampParamsString = string.Empty;
|
||||||
if (doVppTonemap)
|
if (doVppTonemap)
|
||||||
{
|
{
|
||||||
|
if (isRext)
|
||||||
|
{
|
||||||
|
// VPP tonemap requires p010 input
|
||||||
|
twoPassVppTonemap = true;
|
||||||
|
}
|
||||||
|
|
||||||
if (options.VppTonemappingBrightness != 0
|
if (options.VppTonemappingBrightness != 0
|
||||||
&& options.VppTonemappingBrightness >= -100
|
&& options.VppTonemappingBrightness >= -100
|
||||||
&& options.VppTonemappingBrightness <= 100)
|
&& options.VppTonemappingBrightness <= 100)
|
||||||
{
|
{
|
||||||
procampParams += $":brightness={options.VppTonemappingBrightness}";
|
procampParamsString += ":brightness={0}";
|
||||||
twoPassVppTonemap = doVppProcamp = true;
|
twoPassVppTonemap = doVppProcamp = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.VppTonemappingContrast > 1
|
if (options.VppTonemappingContrast > 1
|
||||||
&& options.VppTonemappingContrast <= 10)
|
&& options.VppTonemappingContrast <= 10)
|
||||||
{
|
{
|
||||||
procampParams += $":contrast={options.VppTonemappingContrast}";
|
procampParamsString += ":contrast={1}";
|
||||||
twoPassVppTonemap = doVppProcamp = true;
|
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";
|
var outFormat = doOclTonemap ? ((doVppTranspose || isRext) ? "p010" : string.Empty) : "nv12";
|
||||||
@@ -5662,7 +5695,11 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
if (!string.IsNullOrEmpty(doScaling)
|
if (!string.IsNullOrEmpty(doScaling)
|
||||||
&& !IsScaleRatioSupported(inW, inH, reqW, reqH, reqMaxW, reqMaxH, 8.0f))
|
&& !IsScaleRatioSupported(inW, inH, reqW, reqH, reqMaxW, reqMaxH, 8.0f))
|
||||||
{
|
{
|
||||||
var hwScaleFilterFirstPass = $"scale_rkrga=w=iw/7.9:h=ih/7.9:format={outFormat}:afbc=1";
|
// Vendor provided BSP kernel has an RGA driver bug that causes the output to be corrupted for P010 format.
|
||||||
|
// Use NV15 instead of P010 to avoid the issue.
|
||||||
|
// SDR inputs are using BGRA formats already which is not affected.
|
||||||
|
var intermediateFormat = string.Equals(outFormat, "p010", StringComparison.OrdinalIgnoreCase) ? "nv15" : outFormat;
|
||||||
|
var hwScaleFilterFirstPass = $"scale_rkrga=w=iw/7.9:h=ih/7.9:format={intermediateFormat}:force_divisible_by=4:afbc=1";
|
||||||
mainFilters.Add(hwScaleFilterFirstPass);
|
mainFilters.Add(hwScaleFilterFirstPass);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -7036,7 +7073,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
{
|
{
|
||||||
// DTS and TrueHD are not supported by HLS
|
// DTS and TrueHD are not supported by HLS
|
||||||
// Keep them in the supported codecs list, but shift them to the end of the list so that if transcoding happens, another codec is used
|
// Keep them in the supported codecs list, but shift them to the end of the list so that if transcoding happens, another codec is used
|
||||||
shiftAudioCodecs.Add("dca");
|
shiftAudioCodecs.Add("dts");
|
||||||
shiftAudioCodecs.Add("truehd");
|
shiftAudioCodecs.Add("truehd");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -50,8 +50,18 @@ public interface IMediaSegmentManager
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="itemId">The id of the <see cref="BaseItem"/>.</param>
|
/// <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="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>
|
/// <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>
|
/// <summary>
|
||||||
/// Gets information about any media segments stored for the given itemId.
|
/// 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="playlistId">The playlist identifier.</param>
|
||||||
/// <param name="entryId">The entry identifier.</param>
|
/// <param name="entryId">The entry identifier.</param>
|
||||||
/// <param name="newIndex">The new index.</param>
|
/// <param name="newIndex">The new index.</param>
|
||||||
|
/// <param name="callingUserId">The calling user.</param>
|
||||||
/// <returns>Task.</returns>
|
/// <returns>Task.</returns>
|
||||||
Task MoveItemAsync(string playlistId, string entryId, int newIndex);
|
Task MoveItemAsync(string playlistId, string entryId, int newIndex, Guid callingUserId);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Removed all playlists of a user.
|
/// 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);
|
Task SaveImage(BaseItem item, Stream source, string mimeType, ImageType type, int? imageIndex, CancellationToken cancellationToken);
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
/// <param name="item">Image to save.</param>
|
/// <param name="item">Image to save.</param>
|
||||||
/// <param name="source">Source of image.</param>
|
/// <param name="source">Source of image.</param>
|
||||||
|
|||||||
@@ -1035,6 +1035,16 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||||||
if (exitCode == -1)
|
if (exitCode == -1)
|
||||||
{
|
{
|
||||||
_logger.LogError("ffmpeg image extraction failed for {ProcessDescription}", processDescription);
|
_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));
|
throw new FfmpegException(string.Format(CultureInfo.InvariantCulture, "ffmpeg image extraction failed for {0}", processDescription));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -246,7 +246,7 @@ public class ServerConfiguration : BaseApplicationConfiguration
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets a value indicating whether older plugins should automatically be deleted from the plugin folder.
|
/// Gets or sets a value indicating whether older plugins should automatically be deleted from the plugin folder.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool RemoveOldPlugins { get; set; }
|
public bool RemoveOldPlugins { get; set; } = true;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets a value indicating whether clients should be allowed to upload logs.
|
/// Gets or sets a value indicating whether clients should be allowed to upload logs.
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ namespace MediaBrowser.Model.Dlna
|
|||||||
private readonly ITranscoderSupport _transcoderSupport;
|
private readonly ITranscoderSupport _transcoderSupport;
|
||||||
private static readonly string[] _supportedHlsVideoCodecs = ["h264", "hevc", "vp9", "av1"];
|
private static readonly string[] _supportedHlsVideoCodecs = ["h264", "hevc", "vp9", "av1"];
|
||||||
private static readonly string[] _supportedHlsAudioCodecsTs = ["aac", "ac3", "eac3", "mp3"];
|
private static readonly string[] _supportedHlsAudioCodecsTs = ["aac", "ac3", "eac3", "mp3"];
|
||||||
private static readonly string[] _supportedHlsAudioCodecsMp4 = ["aac", "ac3", "eac3", "mp3", "alac", "flac", "opus", "dca", "truehd"];
|
private static readonly string[] _supportedHlsAudioCodecsMp4 = ["aac", "ac3", "eac3", "mp3", "alac", "flac", "opus", "dts", "truehd"];
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="StreamBuilder"/> class.
|
/// Initializes a new instance of the <see cref="StreamBuilder"/> class.
|
||||||
@@ -208,6 +208,14 @@ namespace MediaBrowser.Model.Dlna
|
|||||||
|
|
||||||
var longBitrate = Math.Min(transcodingBitrate, playlistItem.AudioBitrate ?? transcodingBitrate);
|
var longBitrate = Math.Min(transcodingBitrate, playlistItem.AudioBitrate ?? transcodingBitrate);
|
||||||
playlistItem.AudioBitrate = longBitrate > int.MaxValue ? int.MaxValue : Convert.ToInt32(longBitrate);
|
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;
|
playlistItem.TranscodeReasons = transcodeReasons;
|
||||||
@@ -854,18 +862,37 @@ namespace MediaBrowser.Model.Dlna
|
|||||||
|
|
||||||
if (options.AllowAudioStreamCopy)
|
if (options.AllowAudioStreamCopy)
|
||||||
{
|
{
|
||||||
if (ContainerHelper.ContainsContainer(transcodingProfile.AudioCodec, audioCodec))
|
// For Audio stream, we prefer the audio codec that can be directly copied, then the codec that can otherwise satisfies
|
||||||
|
// the transcoding conditions, then the one does not satisfy the transcoding conditions.
|
||||||
|
// For example: A client can support both aac and flac, but flac only supports 2 channels while aac supports 6.
|
||||||
|
// When the source audio is 6 channel flac, we should transcode to 6 channel aac, instead of down-mix to 2 channel flac.
|
||||||
|
var transcodingAudioCodecs = ContainerHelper.Split(transcodingProfile.AudioCodec);
|
||||||
|
|
||||||
|
foreach (var transcodingAudioCodec in transcodingAudioCodecs)
|
||||||
{
|
{
|
||||||
var appliedVideoConditions = options.Profile.CodecProfiles
|
var appliedVideoConditions = options.Profile.CodecProfiles
|
||||||
.Where(i => i.Type == CodecType.VideoAudio &&
|
.Where(i => i.Type == CodecType.VideoAudio &&
|
||||||
i.ContainsAnyCodec(audioCodec, container) &&
|
i.ContainsAnyCodec(transcodingAudioCodec, container) &&
|
||||||
i.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoAudioConditionSatisfied(applyCondition, audioChannels, audioBitrate, audioSampleRate, audioBitDepth, audioProfile, false)))
|
i.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoAudioConditionSatisfied(applyCondition, audioChannels, audioBitrate, audioSampleRate, audioBitDepth, audioProfile, false)))
|
||||||
.Select(i =>
|
.Select(i =>
|
||||||
i.Conditions.All(condition => ConditionProcessor.IsVideoAudioConditionSatisfied(condition, audioChannels, audioBitrate, audioSampleRate, audioBitDepth, audioProfile, false)));
|
i.Conditions.All(condition => ConditionProcessor.IsVideoAudioConditionSatisfied(condition, audioChannels, audioBitrate, audioSampleRate, audioBitDepth, audioProfile, false)));
|
||||||
|
|
||||||
// An empty appliedVideoConditions means that the codec has no conditions for the current audio stream
|
// An empty appliedVideoConditions means that the codec has no conditions for the current audio stream
|
||||||
var conditionsSatisfied = appliedVideoConditions.All(satisfied => satisfied);
|
var conditionsSatisfied = appliedVideoConditions.All(satisfied => satisfied);
|
||||||
rank.Audio = conditionsSatisfied ? 1 : 2;
|
|
||||||
|
var rankAudio = 3;
|
||||||
|
|
||||||
|
if (conditionsSatisfied)
|
||||||
|
{
|
||||||
|
rankAudio = string.Equals(transcodingAudioCodec, audioCodec, StringComparison.OrdinalIgnoreCase) ? 1 : 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
rank.Audio = Math.Min(rank.Audio, rankAudio);
|
||||||
|
|
||||||
|
if (rank.Audio == 1)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -955,9 +982,26 @@ namespace MediaBrowser.Model.Dlna
|
|||||||
|
|
||||||
var audioStreamWithSupportedCodec = candidateAudioStreams.Where(stream => ContainerHelper.ContainsContainer(audioCodecs, false, stream.Codec)).FirstOrDefault();
|
var audioStreamWithSupportedCodec = candidateAudioStreams.Where(stream => ContainerHelper.ContainsContainer(audioCodecs, false, stream.Codec)).FirstOrDefault();
|
||||||
|
|
||||||
var directAudioStream = audioStreamWithSupportedCodec?.Channels is not null && audioStreamWithSupportedCodec.Channels.Value <= (playlistItem.TranscodingMaxAudioChannels ?? int.MaxValue) ? audioStreamWithSupportedCodec : null;
|
var channelsExceedsLimit = audioStreamWithSupportedCodec is not null && audioStreamWithSupportedCodec.Channels > (playlistItem.TranscodingMaxAudioChannels ?? int.MaxValue);
|
||||||
|
|
||||||
var channelsExceedsLimit = audioStreamWithSupportedCodec is not null && directAudioStream is null;
|
var directAudioStreamSatisfied = audioStreamWithSupportedCodec is not null && !channelsExceedsLimit
|
||||||
|
&& options.Profile.CodecProfiles
|
||||||
|
.Where(i => i.Type == CodecType.VideoAudio
|
||||||
|
&& i.ContainsAnyCodec(audioStreamWithSupportedCodec.Codec, container)
|
||||||
|
&& i.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoAudioConditionSatisfied(applyCondition, audioStreamWithSupportedCodec.Channels, audioStreamWithSupportedCodec.BitRate, audioStreamWithSupportedCodec.SampleRate, audioStreamWithSupportedCodec.BitDepth, audioStreamWithSupportedCodec.Profile, false)))
|
||||||
|
.Select(i => i.Conditions.All(condition =>
|
||||||
|
{
|
||||||
|
var satisfied = ConditionProcessor.IsVideoAudioConditionSatisfied(condition, audioStreamWithSupportedCodec.Channels, audioStreamWithSupportedCodec.BitRate, audioStreamWithSupportedCodec.SampleRate, audioStreamWithSupportedCodec.BitDepth, audioStreamWithSupportedCodec.Profile, false);
|
||||||
|
if (!satisfied)
|
||||||
|
{
|
||||||
|
playlistItem.TranscodeReasons |= GetTranscodeReasonForFailedCondition(condition);
|
||||||
|
}
|
||||||
|
|
||||||
|
return satisfied;
|
||||||
|
}))
|
||||||
|
.All(satisfied => satisfied);
|
||||||
|
|
||||||
|
var directAudioStream = directAudioStreamSatisfied ? audioStreamWithSupportedCodec : null;
|
||||||
|
|
||||||
if (channelsExceedsLimit && playlistItem.TargetAudioStream is not null)
|
if (channelsExceedsLimit && playlistItem.TargetAudioStream is not null)
|
||||||
{
|
{
|
||||||
@@ -2205,7 +2249,7 @@ namespace MediaBrowser.Model.Dlna
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool IsAudioDirectPlaySupported(DirectPlayProfile profile, MediaSourceInfo item, MediaStream audioStream)
|
private static bool IsAudioContainerSupported(DirectPlayProfile profile, MediaSourceInfo item)
|
||||||
{
|
{
|
||||||
// Check container type
|
// Check container type
|
||||||
if (!profile.SupportsContainer(item.Container))
|
if (!profile.SupportsContainer(item.Container))
|
||||||
@@ -2213,6 +2257,20 @@ namespace MediaBrowser.Model.Dlna
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Never direct play audio in matroska when the device only declare support for webm.
|
||||||
|
// The first check is not enough because mkv is assumed can be webm.
|
||||||
|
// See https://github.com/jellyfin/jellyfin/issues/13344
|
||||||
|
return !ContainerHelper.ContainsContainer("mkv", item.Container)
|
||||||
|
|| profile.SupportsContainer("mkv");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsAudioDirectPlaySupported(DirectPlayProfile profile, MediaSourceInfo item, MediaStream audioStream)
|
||||||
|
{
|
||||||
|
if (!IsAudioContainerSupported(profile, item))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// Check audio codec
|
// Check audio codec
|
||||||
string? audioCodec = audioStream?.Codec;
|
string? audioCodec = audioStream?.Codec;
|
||||||
if (!profile.SupportsAudioCodec(audioCodec))
|
if (!profile.SupportsAudioCodec(audioCodec))
|
||||||
@@ -2227,19 +2285,16 @@ namespace MediaBrowser.Model.Dlna
|
|||||||
{
|
{
|
||||||
// Check container type, this should NOT be supported
|
// Check container type, this should NOT be supported
|
||||||
// If the container is supported, the file should be directly played
|
// If the container is supported, the file should be directly played
|
||||||
if (!profile.SupportsContainer(item.Container))
|
if (IsAudioContainerSupported(profile, item))
|
||||||
{
|
{
|
||||||
// Check audio codec, we cannot use the SupportsAudioCodec here
|
return false;
|
||||||
// Because that one assumes empty container supports all codec, which is just useless
|
|
||||||
string? audioCodec = audioStream?.Codec;
|
|
||||||
if (string.Equals(profile.AudioCodec, audioCodec, StringComparison.OrdinalIgnoreCase) ||
|
|
||||||
string.Equals(profile.Container, audioCodec, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
// Check audio codec, we cannot use the SupportsAudioCodec here
|
||||||
|
// Because that one assumes empty container supports all codec, which is just useless
|
||||||
|
string? audioCodec = audioStream?.Codec;
|
||||||
|
return string.Equals(profile.AudioCodec, audioCodec, StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| string.Equals(profile.Container, audioCodec, StringComparison.OrdinalIgnoreCase);
|
||||||
}
|
}
|
||||||
|
|
||||||
private int GetRank(ref TranscodeReason a, TranscodeReason[] rankings)
|
private int GetRank(ref TranscodeReason a, TranscodeReason[] rankings)
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using System;
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using System.Xml.Serialization;
|
using System.Xml.Serialization;
|
||||||
using Jellyfin.Data.Enums;
|
using Jellyfin.Data.Enums;
|
||||||
@@ -6,6 +7,7 @@ namespace MediaBrowser.Model.Dlna;
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A class for transcoding profile information.
|
/// A class for transcoding profile information.
|
||||||
|
/// Note for client developers: Conditions defined in <see cref="CodecProfile"/> has higher priority and can override values defined here.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class TranscodingProfile
|
public class TranscodingProfile
|
||||||
{
|
{
|
||||||
@@ -17,6 +19,33 @@ public class TranscodingProfile
|
|||||||
Conditions = [];
|
Conditions = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="TranscodingProfile" /> class copying the values from another instance.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="other">Another instance of <see cref="TranscodingProfile" /> to be copied.</param>
|
||||||
|
public TranscodingProfile(TranscodingProfile other)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(other);
|
||||||
|
|
||||||
|
Container = other.Container;
|
||||||
|
Type = other.Type;
|
||||||
|
VideoCodec = other.VideoCodec;
|
||||||
|
AudioCodec = other.AudioCodec;
|
||||||
|
Protocol = other.Protocol;
|
||||||
|
EstimateContentLength = other.EstimateContentLength;
|
||||||
|
EnableMpegtsM2TsMode = other.EnableMpegtsM2TsMode;
|
||||||
|
TranscodeSeekInfo = other.TranscodeSeekInfo;
|
||||||
|
CopyTimestamps = other.CopyTimestamps;
|
||||||
|
Context = other.Context;
|
||||||
|
EnableSubtitlesInManifest = other.EnableSubtitlesInManifest;
|
||||||
|
MaxAudioChannels = other.MaxAudioChannels;
|
||||||
|
MinSegments = other.MinSegments;
|
||||||
|
SegmentLength = other.SegmentLength;
|
||||||
|
BreakOnNonKeyFrames = other.BreakOnNonKeyFrames;
|
||||||
|
Conditions = other.Conditions;
|
||||||
|
EnableAudioVbrEncoding = other.EnableAudioVbrEncoding;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the container.
|
/// Gets or sets the container.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ public static class LibraryOptionsExtension
|
|||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(options);
|
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);
|
var isChar = char.TryParse(x, out var c);
|
||||||
if (isChar)
|
if (isChar)
|
||||||
@@ -27,6 +27,8 @@ public static class LibraryOptionsExtension
|
|||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
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>
|
<PropertyGroup>
|
||||||
<Authors>Jellyfin Contributors</Authors>
|
<Authors>Jellyfin Contributors</Authors>
|
||||||
<PackageId>Jellyfin.Model</PackageId>
|
<PackageId>Jellyfin.Model</PackageId>
|
||||||
<VersionPrefix>10.10.0</VersionPrefix>
|
<VersionPrefix>10.10.4</VersionPrefix>
|
||||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|||||||
@@ -229,9 +229,7 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
{
|
{
|
||||||
var mimeType = MimeTypes.GetMimeType(response.Path);
|
var mimeType = MimeTypes.GetMimeType(response.Path);
|
||||||
|
|
||||||
var stream = AsyncFile.OpenRead(response.Path);
|
await _providerManager.SaveImage(item, response.Path, mimeType, imageType, null, null, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
await _providerManager.SaveImage(item, stream, mimeType, imageType, null, cancellationToken).ConfigureAwait(false);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -387,8 +385,8 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
|
|
||||||
item.RemoveImages(images);
|
item.RemoveImages(images);
|
||||||
|
|
||||||
// Cleanup old metadata directory for episodes if empty
|
// Cleanup old metadata directory for episodes if empty, as long as it's not a virtual item
|
||||||
if (item is Episode)
|
if (item is Episode && !item.IsVirtualItem)
|
||||||
{
|
{
|
||||||
var oldLocalMetadataDirectory = Path.Combine(item.ContainingFolderPath, "metadata");
|
var oldLocalMetadataDirectory = Path.Combine(item.ContainingFolderPath, "metadata");
|
||||||
if (_fileSystem.DirectoryExists(oldLocalMetadataDirectory) && !_fileSystem.GetFiles(oldLocalMetadataDirectory).Any())
|
if (_fileSystem.DirectoryExists(oldLocalMetadataDirectory) && !_fileSystem.GetFiles(oldLocalMetadataDirectory).Any())
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ using System.Linq;
|
|||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Net.Mime;
|
using System.Net.Mime;
|
||||||
|
using System.Runtime.ExceptionServices;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using AsyncKeyedLock;
|
using AsyncKeyedLock;
|
||||||
@@ -251,15 +252,29 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <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))
|
if (string.IsNullOrWhiteSpace(source))
|
||||||
{
|
{
|
||||||
throw new ArgumentNullException(nameof(source));
|
throw new ArgumentNullException(nameof(source));
|
||||||
}
|
}
|
||||||
|
|
||||||
var fileStream = AsyncFile.OpenRead(source);
|
try
|
||||||
return new ImageSaver(_configurationManager, _libraryMonitor, _fileSystem, _logger).SaveImage(item, fileStream, mimeType, type, imageIndex, saveLocallyWithMedia, cancellationToken);
|
{
|
||||||
|
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/>
|
/// <inheritdoc/>
|
||||||
|
|||||||
@@ -179,7 +179,7 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||||||
if (audio.SupportsPeople && !audio.LockedFields.Contains(MetadataField.Cast))
|
if (audio.SupportsPeople && !audio.LockedFields.Contains(MetadataField.Cast))
|
||||||
{
|
{
|
||||||
var people = new List<PersonInfo>();
|
var people = new List<PersonInfo>();
|
||||||
var albumArtists = string.IsNullOrEmpty(track.AlbumArtist) ? mediaInfo.AlbumArtists : track.AlbumArtist.Split(InternalValueSeparator);
|
var albumArtists = string.IsNullOrEmpty(track.AlbumArtist) ? [] : track.AlbumArtist.Split(InternalValueSeparator);
|
||||||
|
|
||||||
if (libraryOptions.UseCustomTagDelimiters)
|
if (libraryOptions.UseCustomTagDelimiters)
|
||||||
{
|
{
|
||||||
@@ -210,7 +210,7 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||||||
|
|
||||||
if (performers is null || performers.Length == 0)
|
if (performers is null || performers.Length == 0)
|
||||||
{
|
{
|
||||||
performers = string.IsNullOrEmpty(track.Artist) ? mediaInfo.Artists : track.Artist.Split(InternalValueSeparator);
|
performers = string.IsNullOrEmpty(track.Artist) ? [] : track.Artist.Split(InternalValueSeparator);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (libraryOptions.UseCustomTagDelimiters)
|
if (libraryOptions.UseCustomTagDelimiters)
|
||||||
@@ -314,7 +314,7 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||||||
|
|
||||||
if (!audio.LockedFields.Contains(MetadataField.Genres))
|
if (!audio.LockedFields.Contains(MetadataField.Genres))
|
||||||
{
|
{
|
||||||
var genres = string.IsNullOrEmpty(track.Genre) ? mediaInfo.Genres : track.Genre.Split(InternalValueSeparator).Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
|
var genres = string.IsNullOrEmpty(track.Genre) ? [] : track.Genre.Split(InternalValueSeparator).Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
|
||||||
|
|
||||||
if (libraryOptions.UseCustomTagDelimiters)
|
if (libraryOptions.UseCustomTagDelimiters)
|
||||||
{
|
{
|
||||||
@@ -347,7 +347,8 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||||||
|| track.AdditionalFields.TryGetValue("MusicBrainz Artist Id", out musicBrainzArtistTag))
|
|| track.AdditionalFields.TryGetValue("MusicBrainz Artist Id", out musicBrainzArtistTag))
|
||||||
&& !string.IsNullOrEmpty(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))
|
|| track.AdditionalFields.TryGetValue("MusicBrainz Album Artist Id", out musicBrainzReleaseArtistIdTag))
|
||||||
&& !string.IsNullOrEmpty(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))
|
|| track.AdditionalFields.TryGetValue("MusicBrainz Album Id", out musicBrainzReleaseIdTag))
|
||||||
&& !string.IsNullOrEmpty(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))
|
|| track.AdditionalFields.TryGetValue("MusicBrainz Release Group Id", out musicBrainzReleaseGroupIdTag))
|
||||||
&& !string.IsNullOrEmpty(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))
|
|| track.AdditionalFields.TryGetValue("MusicBrainz Release Track Id", out trackMbId))
|
||||||
&& !string.IsNullOrEmpty(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;
|
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.Tmdb, tmdbId);
|
||||||
movie.SetProviderId(MetadataProvider.Imdb, movieResult.ImdbId);
|
movie.TrySetProviderId(MetadataProvider.Imdb, movieResult.ImdbId);
|
||||||
if (movieResult.BelongsToCollection is not null)
|
if (movieResult.BelongsToCollection is not null)
|
||||||
{
|
{
|
||||||
movie.SetProviderId(MetadataProvider.TmdbCollection, movieResult.BelongsToCollection.Id.ToString(CultureInfo.InvariantCulture));
|
movie.SetProviderId(MetadataProvider.TmdbCollection, movieResult.BelongsToCollection.Id.ToString(CultureInfo.InvariantCulture));
|
||||||
|
|||||||
@@ -140,38 +140,39 @@ namespace MediaBrowser.Providers.TV
|
|||||||
|
|
||||||
private void RemoveObsoleteEpisodes(Series series)
|
private void RemoveObsoleteEpisodes(Series series)
|
||||||
{
|
{
|
||||||
var episodes = series.GetEpisodes(null, new DtoOptions(), true).OfType<Episode>().ToList();
|
var episodesBySeason = series.GetEpisodes(null, new DtoOptions(), true)
|
||||||
var numberOfEpisodes = episodes.Count;
|
.OfType<Episode>()
|
||||||
// TODO: O(n^2), but can it be done faster without overcomplicating it?
|
.GroupBy(e => e.ParentIndexNumber)
|
||||||
for (var i = 0; i < numberOfEpisodes; i++)
|
.ToList();
|
||||||
|
|
||||||
|
foreach (var seasonEpisodes in episodesBySeason)
|
||||||
{
|
{
|
||||||
var currentEpisode = episodes[i];
|
List<Episode> nonPhysicalEpisodes = [];
|
||||||
// The outer loop only examines virtual episodes
|
List<Episode> physicalEpisodes = [];
|
||||||
if (!currentEpisode.IsVirtualItem)
|
foreach (var episode in seasonEpisodes)
|
||||||
{
|
{
|
||||||
continue;
|
if (episode.IsVirtualItem || episode.IsMissingEpisode)
|
||||||
|
{
|
||||||
|
nonPhysicalEpisodes.Add(episode);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
physicalEpisodes.Add(episode);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Virtual episodes without an episode number are practically orphaned and should be deleted
|
// Only consider non-physical episodes
|
||||||
if (!currentEpisode.IndexNumber.HasValue)
|
foreach (var episode in nonPhysicalEpisodes)
|
||||||
{
|
{
|
||||||
DeleteEpisode(currentEpisode);
|
// Episodes without an episode number are practically orphaned and should be deleted
|
||||||
continue;
|
// Episodes with a physical equivalent should be deleted (they are no longer missing)
|
||||||
}
|
var shouldKeep = episode.IndexNumber.HasValue && !physicalEpisodes.Any(e => e.ContainsEpisodeNumber(episode.IndexNumber.Value));
|
||||||
|
|
||||||
for (var j = i + 1; j < numberOfEpisodes; j++)
|
if (shouldKeep)
|
||||||
{
|
|
||||||
var comparisonEpisode = episodes[j];
|
|
||||||
// The inner loop is only for "physical" episodes
|
|
||||||
if (comparisonEpisode.IsVirtualItem
|
|
||||||
|| currentEpisode.ParentIndexNumber != comparisonEpisode.ParentIndexNumber
|
|
||||||
|| !comparisonEpisode.ContainsEpisodeNumber(currentEpisode.IndexNumber.Value))
|
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
DeleteEpisode(currentEpisode);
|
DeleteEpisode(episode);
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,23 +50,20 @@ namespace MediaBrowser.XbmcMetadata.Parsers
|
|||||||
{
|
{
|
||||||
case "id":
|
case "id":
|
||||||
{
|
{
|
||||||
// get ids from attributes
|
// Get ids from attributes
|
||||||
|
item.TrySetProviderId(MetadataProvider.Tmdb, reader.GetAttribute("TMDB"));
|
||||||
|
item.TrySetProviderId(MetadataProvider.Tvdb, reader.GetAttribute("TVDB"));
|
||||||
string? imdbId = reader.GetAttribute("IMDB");
|
string? imdbId = reader.GetAttribute("IMDB");
|
||||||
string? tmdbId = reader.GetAttribute("TMDB");
|
|
||||||
|
|
||||||
// read id from content
|
// Read id from content
|
||||||
|
// Content can be arbitrary according to Kodi wiki, so only parse if we are sure it matches a provider-specific schema
|
||||||
var contentId = reader.ReadElementContentAsString();
|
var contentId = reader.ReadElementContentAsString();
|
||||||
if (contentId.Contains("tt", StringComparison.Ordinal) && string.IsNullOrEmpty(imdbId))
|
if (string.IsNullOrEmpty(imdbId) && contentId.StartsWith("tt", StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
imdbId = contentId;
|
imdbId = contentId;
|
||||||
}
|
}
|
||||||
else if (string.IsNullOrEmpty(tmdbId))
|
|
||||||
{
|
|
||||||
tmdbId = contentId;
|
|
||||||
}
|
|
||||||
|
|
||||||
item.TrySetProviderId(MetadataProvider.Imdb, imdbId);
|
item.TrySetProviderId(MetadataProvider.Imdb, imdbId);
|
||||||
item.TrySetProviderId(MetadataProvider.Tmdb, tmdbId);
|
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using System;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Xml;
|
using System.Xml;
|
||||||
using Emby.Naming.TV;
|
using Emby.Naming.TV;
|
||||||
@@ -48,16 +49,20 @@ namespace MediaBrowser.XbmcMetadata.Parsers
|
|||||||
{
|
{
|
||||||
case "id":
|
case "id":
|
||||||
{
|
{
|
||||||
item.TrySetProviderId(MetadataProvider.Imdb, reader.GetAttribute("IMDB"));
|
// Get ids from attributes
|
||||||
item.TrySetProviderId(MetadataProvider.Tmdb, reader.GetAttribute("TMDB"));
|
item.TrySetProviderId(MetadataProvider.Tmdb, reader.GetAttribute("TMDB"));
|
||||||
|
item.TrySetProviderId(MetadataProvider.Tvdb, reader.GetAttribute("TVDB"));
|
||||||
|
string? imdbId = reader.GetAttribute("IMDB");
|
||||||
|
|
||||||
string? tvdbId = reader.GetAttribute("TVDB");
|
// Read id from content
|
||||||
if (string.IsNullOrWhiteSpace(tvdbId))
|
// Content can be arbitrary according to Kodi wiki, so only parse if we are sure it matches a provider-specific schema
|
||||||
|
var contentId = reader.ReadElementContentAsString();
|
||||||
|
if (string.IsNullOrEmpty(imdbId) && contentId.StartsWith("tt", StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
tvdbId = reader.ReadElementContentAsString();
|
imdbId = contentId;
|
||||||
}
|
}
|
||||||
|
|
||||||
item.TrySetProviderId(MetadataProvider.Tvdb, tvdbId);
|
item.TrySetProviderId(MetadataProvider.Imdb, imdbId);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
|
||||||
[assembly: AssemblyVersion("10.10.0")]
|
[assembly: AssemblyVersion("10.10.4")]
|
||||||
[assembly: AssemblyFileVersion("10.10.0")]
|
[assembly: AssemblyFileVersion("10.10.4")]
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Authors>Jellyfin Contributors</Authors>
|
<Authors>Jellyfin Contributors</Authors>
|
||||||
<PackageId>Jellyfin.Extensions</PackageId>
|
<PackageId>Jellyfin.Extensions</PackageId>
|
||||||
<VersionPrefix>10.10.0</VersionPrefix>
|
<VersionPrefix>10.10.4</VersionPrefix>
|
||||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|||||||
@@ -71,24 +71,11 @@ namespace Jellyfin.Extensions.Json.Converters
|
|||||||
writer.WriteStartArray();
|
writer.WriteStartArray();
|
||||||
if (value.Length > 0)
|
if (value.Length > 0)
|
||||||
{
|
{
|
||||||
var toWrite = value.Length - 1;
|
|
||||||
foreach (var it in value)
|
foreach (var it in value)
|
||||||
{
|
{
|
||||||
var wrote = false;
|
|
||||||
if (it is not null)
|
if (it is not null)
|
||||||
{
|
{
|
||||||
writer.WriteStringValue(it.ToString());
|
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.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Jellyfin.Data.Entities.Libraries;
|
||||||
using Jellyfin.Data.Enums;
|
using Jellyfin.Data.Enums;
|
||||||
using Jellyfin.Extensions;
|
using Jellyfin.Extensions;
|
||||||
using Jellyfin.LiveTv.Configuration;
|
using Jellyfin.LiveTv.Configuration;
|
||||||
@@ -39,6 +40,11 @@ public class GuideManager : IGuideManager
|
|||||||
private readonly IRecordingsManager _recordingsManager;
|
private readonly IRecordingsManager _recordingsManager;
|
||||||
private readonly LiveTvDtoService _tvDtoService;
|
private readonly LiveTvDtoService _tvDtoService;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Amount of days images are pre-cached from external sources.
|
||||||
|
/// </summary>
|
||||||
|
public const int MaxCacheDays = 2;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="GuideManager"/> class.
|
/// Initializes a new instance of the <see cref="GuideManager"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -204,14 +210,14 @@ public class GuideManager : IGuideManager
|
|||||||
progress.Report(15);
|
progress.Report(15);
|
||||||
|
|
||||||
numComplete = 0;
|
numComplete = 0;
|
||||||
var programs = new List<Guid>();
|
var programs = new List<LiveTvProgram>();
|
||||||
var channels = new List<Guid>();
|
var channels = new List<Guid>();
|
||||||
|
|
||||||
var guideDays = GetGuideDays();
|
var guideDays = GetGuideDays();
|
||||||
|
|
||||||
_logger.LogInformation("Refreshing guide with {0} days of guide data", guideDays);
|
_logger.LogInformation("Refreshing guide with {Days} days of guide data", guideDays);
|
||||||
|
|
||||||
var maxCacheDate = DateTime.UtcNow.AddDays(2);
|
var maxCacheDate = DateTime.UtcNow.AddDays(MaxCacheDays);
|
||||||
foreach (var currentChannel in list)
|
foreach (var currentChannel in list)
|
||||||
{
|
{
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
@@ -237,22 +243,23 @@ public class GuideManager : IGuideManager
|
|||||||
DtoOptions = new DtoOptions(true)
|
DtoOptions = new DtoOptions(true)
|
||||||
}).Cast<LiveTvProgram>().ToDictionary(i => i.Id);
|
}).Cast<LiveTvProgram>().ToDictionary(i => i.Id);
|
||||||
|
|
||||||
var newPrograms = new List<LiveTvProgram>();
|
var newPrograms = new List<Guid>();
|
||||||
var updatedPrograms = new List<BaseItem>();
|
var updatedPrograms = new List<Guid>();
|
||||||
|
|
||||||
foreach (var program in channelPrograms)
|
foreach (var program in channelPrograms)
|
||||||
{
|
{
|
||||||
var (programItem, isNew, isUpdated) = GetProgram(program, existingPrograms, currentChannel);
|
var (programItem, isNew, isUpdated) = GetProgram(program, existingPrograms, currentChannel);
|
||||||
|
var id = programItem.Id;
|
||||||
if (isNew)
|
if (isNew)
|
||||||
{
|
{
|
||||||
newPrograms.Add(programItem);
|
newPrograms.Add(id);
|
||||||
}
|
}
|
||||||
else if (isUpdated)
|
else if (isUpdated)
|
||||||
{
|
{
|
||||||
updatedPrograms.Add(programItem);
|
updatedPrograms.Add(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
programs.Add(programItem.Id);
|
programs.Add(programItem);
|
||||||
|
|
||||||
isMovie |= program.IsMovie;
|
isMovie |= program.IsMovie;
|
||||||
isSeries |= program.IsSeries;
|
isSeries |= program.IsSeries;
|
||||||
@@ -261,24 +268,30 @@ public class GuideManager : IGuideManager
|
|||||||
isKids |= program.IsKids;
|
isKids |= program.IsKids;
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.LogDebug("Channel {0} has {1} new programs and {2} updated programs", currentChannel.Name, newPrograms.Count, updatedPrograms.Count);
|
_logger.LogDebug(
|
||||||
|
"Channel {Name} has {NewCount} new programs and {UpdatedCount} updated programs",
|
||||||
|
currentChannel.Name,
|
||||||
|
newPrograms.Count,
|
||||||
|
updatedPrograms.Count);
|
||||||
|
|
||||||
if (newPrograms.Count > 0)
|
if (newPrograms.Count > 0)
|
||||||
{
|
{
|
||||||
_libraryManager.CreateItems(newPrograms, null, cancellationToken);
|
var newProgramDtos = programs.Where(b => newPrograms.Contains(b.Id)).ToList();
|
||||||
await PrecacheImages(newPrograms, maxCacheDate).ConfigureAwait(false);
|
_libraryManager.CreateItems(newProgramDtos, null, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (updatedPrograms.Count > 0)
|
if (updatedPrograms.Count > 0)
|
||||||
{
|
{
|
||||||
|
var updatedProgramDtos = programs.Where(b => updatedPrograms.Contains(b.Id)).ToList();
|
||||||
await _libraryManager.UpdateItemsAsync(
|
await _libraryManager.UpdateItemsAsync(
|
||||||
updatedPrograms,
|
updatedProgramDtos,
|
||||||
currentChannel,
|
currentChannel,
|
||||||
ItemUpdateType.MetadataImport,
|
ItemUpdateType.MetadataImport,
|
||||||
cancellationToken).ConfigureAwait(false);
|
cancellationToken).ConfigureAwait(false);
|
||||||
await PrecacheImages(updatedPrograms, maxCacheDate).ConfigureAwait(false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await PreCacheImages(programs, maxCacheDate).ConfigureAwait(false);
|
||||||
|
|
||||||
currentChannel.IsMovie = isMovie;
|
currentChannel.IsMovie = isMovie;
|
||||||
currentChannel.IsNews = isNews;
|
currentChannel.IsNews = isNews;
|
||||||
currentChannel.IsSports = isSports;
|
currentChannel.IsSports = isSports;
|
||||||
@@ -313,7 +326,8 @@ public class GuideManager : IGuideManager
|
|||||||
}
|
}
|
||||||
|
|
||||||
progress.Report(100);
|
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)
|
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.IndexNumber = info.EpisodeNumber;
|
||||||
item.ParentIndexNumber = info.SeasonNumber;
|
item.ParentIndexNumber = info.SeasonNumber;
|
||||||
|
|
||||||
if (!item.HasImage(ImageType.Primary))
|
forceUpdate = forceUpdate || UpdateImages(item, info);
|
||||||
{
|
|
||||||
if (!string.IsNullOrWhiteSpace(info.ImagePath))
|
|
||||||
{
|
|
||||||
item.SetImage(
|
|
||||||
new ItemImageInfo
|
|
||||||
{
|
|
||||||
Path = info.ImagePath,
|
|
||||||
Type = ImageType.Primary
|
|
||||||
},
|
|
||||||
0);
|
|
||||||
}
|
|
||||||
else if (!string.IsNullOrWhiteSpace(info.ImageUrl))
|
|
||||||
{
|
|
||||||
item.SetImage(
|
|
||||||
new ItemImageInfo
|
|
||||||
{
|
|
||||||
Path = info.ImageUrl,
|
|
||||||
Type = ImageType.Primary
|
|
||||||
},
|
|
||||||
0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!item.HasImage(ImageType.Thumb))
|
if (isNew)
|
||||||
{
|
{
|
||||||
if (!string.IsNullOrWhiteSpace(info.ThumbImageUrl))
|
item.OnMetadataChanged();
|
||||||
{
|
|
||||||
item.SetImage(
|
|
||||||
new ItemImageInfo
|
|
||||||
{
|
|
||||||
Path = info.ThumbImageUrl,
|
|
||||||
Type = ImageType.Thumb
|
|
||||||
},
|
|
||||||
0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!item.HasImage(ImageType.Logo))
|
return (item, isNew, false);
|
||||||
{
|
|
||||||
if (!string.IsNullOrWhiteSpace(info.LogoImageUrl))
|
|
||||||
{
|
|
||||||
item.SetImage(
|
|
||||||
new ItemImageInfo
|
|
||||||
{
|
|
||||||
Path = info.LogoImageUrl,
|
|
||||||
Type = ImageType.Logo
|
|
||||||
},
|
|
||||||
0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!item.HasImage(ImageType.Backdrop))
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrWhiteSpace(info.BackdropImageUrl))
|
|
||||||
{
|
|
||||||
item.SetImage(
|
|
||||||
new ItemImageInfo
|
|
||||||
{
|
|
||||||
Path = info.BackdropImageUrl,
|
|
||||||
Type = ImageType.Backdrop
|
|
||||||
},
|
|
||||||
0);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var isUpdated = false;
|
var isUpdated = false;
|
||||||
if (isNew)
|
if (forceUpdate || string.IsNullOrWhiteSpace(info.Etag))
|
||||||
{
|
|
||||||
}
|
|
||||||
else if (forceUpdate || string.IsNullOrWhiteSpace(info.Etag))
|
|
||||||
{
|
{
|
||||||
isUpdated = true;
|
isUpdated = true;
|
||||||
}
|
}
|
||||||
@@ -703,7 +657,7 @@ public class GuideManager : IGuideManager
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isNew || isUpdated)
|
if (isUpdated)
|
||||||
{
|
{
|
||||||
item.OnMetadataChanged();
|
item.OnMetadataChanged();
|
||||||
}
|
}
|
||||||
@@ -711,7 +665,80 @@ public class GuideManager : IGuideManager
|
|||||||
return (item, isNew, isUpdated);
|
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(
|
await Parallel.ForEachAsync(
|
||||||
programs
|
programs
|
||||||
@@ -741,7 +768,7 @@ public class GuideManager : IGuideManager
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogWarning(ex, "Unable to precache {Url}", imageInfo.Path);
|
_logger.LogWarning(ex, "Unable to pre-cache {Url}", imageInfo.Path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ using System.Threading.Tasks;
|
|||||||
using AsyncKeyedLock;
|
using AsyncKeyedLock;
|
||||||
using Jellyfin.Extensions;
|
using Jellyfin.Extensions;
|
||||||
using Jellyfin.Extensions.Json;
|
using Jellyfin.Extensions.Json;
|
||||||
|
using Jellyfin.LiveTv.Guide;
|
||||||
using Jellyfin.LiveTv.Listings.SchedulesDirectDtos;
|
using Jellyfin.LiveTv.Listings.SchedulesDirectDtos;
|
||||||
using MediaBrowser.Common.Net;
|
using MediaBrowser.Common.Net;
|
||||||
using MediaBrowser.Controller.Authentication;
|
using MediaBrowser.Controller.Authentication;
|
||||||
@@ -38,7 +39,7 @@ namespace Jellyfin.LiveTv.Listings
|
|||||||
private readonly IHttpClientFactory _httpClientFactory;
|
private readonly IHttpClientFactory _httpClientFactory;
|
||||||
private readonly AsyncNonKeyedLocker _tokenLock = new(1);
|
private readonly AsyncNonKeyedLocker _tokenLock = new(1);
|
||||||
|
|
||||||
private readonly ConcurrentDictionary<string, NameValuePair> _tokens = new ConcurrentDictionary<string, NameValuePair>();
|
private readonly ConcurrentDictionary<string, NameValuePair> _tokens = new();
|
||||||
private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
|
private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
|
||||||
private DateTime _lastErrorResponse;
|
private DateTime _lastErrorResponse;
|
||||||
private bool _disposed = false;
|
private bool _disposed = false;
|
||||||
@@ -86,7 +87,7 @@ namespace Jellyfin.LiveTv.Listings
|
|||||||
{
|
{
|
||||||
_logger.LogWarning("SchedulesDirect token is empty, returning empty program list");
|
_logger.LogWarning("SchedulesDirect token is empty, returning empty program list");
|
||||||
|
|
||||||
return Enumerable.Empty<ProgramInfo>();
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
var dates = GetScheduleRequestDates(startDateUtc, endDateUtc);
|
var dates = GetScheduleRequestDates(startDateUtc, endDateUtc);
|
||||||
@@ -94,7 +95,7 @@ namespace Jellyfin.LiveTv.Listings
|
|||||||
_logger.LogInformation("Channel Station ID is: {ChannelID}", channelId);
|
_logger.LogInformation("Channel Station ID is: {ChannelID}", channelId);
|
||||||
var requestList = new List<RequestScheduleForChannelDto>()
|
var requestList = new List<RequestScheduleForChannelDto>()
|
||||||
{
|
{
|
||||||
new RequestScheduleForChannelDto()
|
new()
|
||||||
{
|
{
|
||||||
StationId = channelId,
|
StationId = channelId,
|
||||||
Date = dates
|
Date = dates
|
||||||
@@ -109,7 +110,7 @@ namespace Jellyfin.LiveTv.Listings
|
|||||||
var dailySchedules = await Request<IReadOnlyList<DayDto>>(options, true, info, cancellationToken).ConfigureAwait(false);
|
var dailySchedules = await Request<IReadOnlyList<DayDto>>(options, true, info, cancellationToken).ConfigureAwait(false);
|
||||||
if (dailySchedules is null)
|
if (dailySchedules is null)
|
||||||
{
|
{
|
||||||
return Array.Empty<ProgramInfo>();
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.LogDebug("Found {ScheduleCount} programs on {ChannelID} ScheduleDirect", dailySchedules.Count, channelId);
|
_logger.LogDebug("Found {ScheduleCount} programs on {ChannelID} ScheduleDirect", dailySchedules.Count, channelId);
|
||||||
@@ -120,17 +121,17 @@ namespace Jellyfin.LiveTv.Listings
|
|||||||
var programIds = dailySchedules.SelectMany(d => d.Programs.Select(s => s.ProgramId)).Distinct();
|
var programIds = dailySchedules.SelectMany(d => d.Programs.Select(s => s.ProgramId)).Distinct();
|
||||||
programRequestOptions.Content = JsonContent.Create(programIds, options: _jsonOptions);
|
programRequestOptions.Content = JsonContent.Create(programIds, options: _jsonOptions);
|
||||||
|
|
||||||
var programDetails = await Request<IReadOnlyList<ProgramDetailsDto>>(programRequestOptions, true, info, cancellationToken)
|
var programDetails = await Request<IReadOnlyList<ProgramDetailsDto>>(programRequestOptions, true, info, cancellationToken).ConfigureAwait(false);
|
||||||
.ConfigureAwait(false);
|
|
||||||
if (programDetails is null)
|
if (programDetails is null)
|
||||||
{
|
{
|
||||||
return Array.Empty<ProgramInfo>();
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
var programDict = programDetails.ToDictionary(p => p.ProgramId, y => y);
|
var programDict = programDetails.ToDictionary(p => p.ProgramId, y => y);
|
||||||
|
|
||||||
var programIdsWithImages = programDetails
|
var programIdsWithImages = programDetails
|
||||||
.Where(p => p.HasImageArtwork).Select(p => p.ProgramId)
|
.Where(p => p.HasImageArtwork)
|
||||||
|
.Select(p => p.ProgramId)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
var images = await GetImageForPrograms(info, programIdsWithImages, cancellationToken).ConfigureAwait(false);
|
var images = await GetImageForPrograms(info, programIdsWithImages, cancellationToken).ConfigureAwait(false);
|
||||||
@@ -138,17 +139,15 @@ namespace Jellyfin.LiveTv.Listings
|
|||||||
var programsInfo = new List<ProgramInfo>();
|
var programsInfo = new List<ProgramInfo>();
|
||||||
foreach (ProgramDto schedule in dailySchedules.SelectMany(d => d.Programs))
|
foreach (ProgramDto schedule in dailySchedules.SelectMany(d => d.Programs))
|
||||||
{
|
{
|
||||||
// _logger.LogDebug("Proccesing Schedule for statio ID " + stationID +
|
|
||||||
// " which corresponds to channel " + channelNumber + " and program id " +
|
|
||||||
// schedule.ProgramId + " which says it has images? " +
|
|
||||||
// programDict[schedule.ProgramId].hasImageArtwork);
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(schedule.ProgramId))
|
if (string.IsNullOrEmpty(schedule.ProgramId))
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (images is not null)
|
// Only add images which will be pre-cached until we can implement dynamic token fetching
|
||||||
|
var endDate = schedule.AirDateTime?.AddSeconds(schedule.Duration);
|
||||||
|
var willBeCached = endDate.HasValue && endDate.Value < DateTime.UtcNow.AddDays(GuideManager.MaxCacheDays);
|
||||||
|
if (willBeCached && images is not null)
|
||||||
{
|
{
|
||||||
var imageIndex = images.FindIndex(i => i.ProgramId == schedule.ProgramId[..10]);
|
var imageIndex = images.FindIndex(i => i.ProgramId == schedule.ProgramId[..10]);
|
||||||
if (imageIndex > -1)
|
if (imageIndex > -1)
|
||||||
@@ -456,7 +455,7 @@ namespace Jellyfin.LiveTv.Listings
|
|||||||
|
|
||||||
if (programIds.Count == 0)
|
if (programIds.Count == 0)
|
||||||
{
|
{
|
||||||
return Array.Empty<ShowImagesDto>();
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
StringBuilder str = new StringBuilder("[", 1 + (programIds.Count * 13));
|
StringBuilder str = new StringBuilder("[", 1 + (programIds.Count * 13));
|
||||||
@@ -483,7 +482,7 @@ namespace Jellyfin.LiveTv.Listings
|
|||||||
{
|
{
|
||||||
_logger.LogError(ex, "Error getting image info from schedules direct");
|
_logger.LogError(ex, "Error getting image info from schedules direct");
|
||||||
|
|
||||||
return Array.Empty<ShowImagesDto>();
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -702,7 +702,7 @@ public class NetworkManager : INetworkManager, IDisposable
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (!_lanSubnets.Any(x => x.Contains(remoteIP)))
|
else if (!IsInLocalNetwork(remoteIP))
|
||||||
{
|
{
|
||||||
// Remote not enabled. So everyone should be LAN.
|
// Remote not enabled. So everyone should be LAN.
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -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>()))
|
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()))
|
.Callback<BaseItem, Stream, string, ImageType, int?, CancellationToken>((callbackItem, _, _, callbackType, _, _) => callbackItem.SetImagePath(callbackType, 0, new FileSystemMetadata()))
|
||||||
.Returns(Task.CompletedTask);
|
.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 itemImageProvider = GetItemImageProvider(providerManager.Object, null);
|
||||||
var result = await itemImageProvider.RefreshImages(item, libraryOptions, new List<IImageProvider> { dynamicProvider.Object }, refreshOptions, CancellationToken.None);
|
var result = await itemImageProvider.RefreshImages(item, libraryOptions, new List<IImageProvider> { dynamicProvider.Object }, refreshOptions, CancellationToken.None);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user