aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/ci-codeql-analysis.yml6
-rw-r--r--.github/workflows/ci-openapi.yml4
-rw-r--r--Emby.Server.Implementations/ConfigurationOptions.cs1
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs2
-rw-r--r--Emby.Server.Implementations/Playlists/PlaylistManager.cs40
-rw-r--r--Jellyfin.Api/Controllers/ItemRefreshController.cs7
-rw-r--r--Jellyfin.Api/Controllers/MediaSegmentsController.cs2
-rw-r--r--Jellyfin.Api/Controllers/PlaylistsController.cs10
-rw-r--r--Jellyfin.Server.Implementations/MediaSegments/MediaSegmentManager.cs38
-rw-r--r--Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs2
-rw-r--r--Jellyfin.Server/Infrastructure/SymlinkFollowingPhysicalFileResultExecutor.cs20
-rw-r--r--Jellyfin.Server/Migrations/MigrationRunner.cs3
-rw-r--r--Jellyfin.Server/Migrations/Routines/FixPlaylistOwner.cs4
-rw-r--r--Jellyfin.Server/Migrations/Routines/RemoveDuplicatePlaylistChildren.cs69
-rw-r--r--MediaBrowser.Controller/Entities/LinkedChild.cs7
-rw-r--r--MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs13
-rw-r--r--MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs56
-rw-r--r--MediaBrowser.Controller/MediaSegements/IMediaSegmentManager.cs12
-rw-r--r--MediaBrowser.Controller/Playlists/IPlaylistManager.cs3
-rw-r--r--MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs10
-rw-r--r--MediaBrowser.Model/Extensions/LibraryOptionsExtension.cs6
-rw-r--r--MediaBrowser.Providers/MediaInfo/AudioFileProber.cs28
-rw-r--r--src/Jellyfin.Extensions/Json/Converters/JsonDelimitedArrayConverter.cs13
23 files changed, 260 insertions, 96 deletions
diff --git a/.github/workflows/ci-codeql-analysis.yml b/.github/workflows/ci-codeql-analysis.yml
index e6993d39d..81be3882a 100644
--- a/.github/workflows/ci-codeql-analysis.yml
+++ b/.github/workflows/ci-codeql-analysis.yml
@@ -27,11 +27,11 @@ jobs:
dotnet-version: '9.0.x'
- name: Initialize CodeQL
- uses: github/codeql-action/init@ea9e4e37992a54ee68a9622e985e60c8e8f12d9f # v3.27.4
+ uses: github/codeql-action/init@f09c1c0a94de965c15400f5634aa42fac8fb8f88 # v3.27.5
with:
languages: ${{ matrix.language }}
queries: +security-extended
- name: Autobuild
- uses: github/codeql-action/autobuild@ea9e4e37992a54ee68a9622e985e60c8e8f12d9f # v3.27.4
+ uses: github/codeql-action/autobuild@f09c1c0a94de965c15400f5634aa42fac8fb8f88 # v3.27.5
- name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@ea9e4e37992a54ee68a9622e985e60c8e8f12d9f # v3.27.4
+ uses: github/codeql-action/analyze@f09c1c0a94de965c15400f5634aa42fac8fb8f88 # v3.27.5
diff --git a/.github/workflows/ci-openapi.yml b/.github/workflows/ci-openapi.yml
index 353c47c54..25b4b9f81 100644
--- a/.github/workflows/ci-openapi.yml
+++ b/.github/workflows/ci-openapi.yml
@@ -172,7 +172,7 @@ jobs:
strip_components: 1
target: "/srv/incoming/openapi/unstable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}"
- name: Move openapi.json (unstable) into place
- uses: appleboy/ssh-action@25ce8cbbcb08177468c7ff7ec5cbfa236f9341e1 # v1.1.0
+ uses: appleboy/ssh-action@7eaf76671a0d7eec5d98ee897acda4f968735a17 # v1.2.0
with:
host: "${{ secrets.REPO_HOST }}"
username: "${{ secrets.REPO_USER }}"
@@ -234,7 +234,7 @@ jobs:
strip_components: 1
target: "/srv/incoming/openapi/stable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}"
- name: Move openapi.json (stable) into place
- uses: appleboy/ssh-action@25ce8cbbcb08177468c7ff7ec5cbfa236f9341e1 # v1.1.0
+ uses: appleboy/ssh-action@7eaf76671a0d7eec5d98ee897acda4f968735a17 # v1.2.0
with:
host: "${{ secrets.REPO_HOST }}"
username: "${{ secrets.REPO_USER }}"
diff --git a/Emby.Server.Implementations/ConfigurationOptions.cs b/Emby.Server.Implementations/ConfigurationOptions.cs
index 91791a1c8..a06f6e7fe 100644
--- a/Emby.Server.Implementations/ConfigurationOptions.cs
+++ b/Emby.Server.Implementations/ConfigurationOptions.cs
@@ -17,7 +17,6 @@ namespace Emby.Server.Implementations
{ DefaultRedirectKey, "web/" },
{ FfmpegProbeSizeKey, "1G" },
{ FfmpegAnalyzeDurationKey, "200M" },
- { PlaylistsAllowDuplicatesKey, bool.FalseString },
{ BindToUnixSocketKey, bool.FalseString },
{ SqliteCacheSizeKey, "20000" },
{ FfmpegSkipValidationKey, bool.FalseString },
diff --git a/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs b/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs
index a03c1214d..14798dda6 100644
--- a/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs
@@ -28,7 +28,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
{
if (args.IsDirectory)
{
- // It's a boxset if the path is a directory with [playlist] in its name
+ // It's a playlist if the path is a directory with [playlist] in its name
var filename = Path.GetFileName(Path.TrimEndingDirectorySeparator(args.Path));
if (string.IsNullOrEmpty(filename))
{
diff --git a/Emby.Server.Implementations/Playlists/PlaylistManager.cs b/Emby.Server.Implementations/Playlists/PlaylistManager.cs
index 47ff22c0b..daeb7fed8 100644
--- a/Emby.Server.Implementations/Playlists/PlaylistManager.cs
+++ b/Emby.Server.Implementations/Playlists/PlaylistManager.cs
@@ -216,14 +216,11 @@ namespace Emby.Server.Implementations.Playlists
var newItems = GetPlaylistItems(newItemIds, user, options)
.Where(i => i.SupportsAddingToPlaylist);
- // Filter out duplicate items, if necessary
- if (!_appConfig.DoPlaylistsAllowDuplicates())
- {
- var existingIds = playlist.LinkedChildren.Select(c => c.ItemId).ToHashSet();
- newItems = newItems
- .Where(i => !existingIds.Contains(i.Id))
- .Distinct();
- }
+ // Filter out duplicate items
+ var existingIds = playlist.LinkedChildren.Select(c => c.ItemId).ToHashSet();
+ newItems = newItems
+ .Where(i => !existingIds.Contains(i.Id))
+ .Distinct();
// Create a list of the new linked children to add to the playlist
var childrenToAdd = newItems
@@ -269,7 +266,7 @@ namespace Emby.Server.Implementations.Playlists
var idList = entryIds.ToList();
- var removals = children.Where(i => idList.Contains(i.Item1.Id));
+ var removals = children.Where(i => idList.Contains(i.Item1.ItemId?.ToString("N", CultureInfo.InvariantCulture)));
playlist.LinkedChildren = children.Except(removals)
.Select(i => i.Item1)
@@ -286,26 +283,39 @@ namespace Emby.Server.Implementations.Playlists
RefreshPriority.High);
}
- public async Task MoveItemAsync(string playlistId, string entryId, int newIndex)
+ public async Task MoveItemAsync(string playlistId, string entryId, int newIndex, Guid callingUserId)
{
if (_libraryManager.GetItemById(playlistId) is not Playlist playlist)
{
throw new ArgumentException("No Playlist exists with the supplied Id");
}
+ var user = _userManager.GetUserById(callingUserId);
var children = playlist.GetManageableItems().ToList();
+ var accessibleChildren = children.Where(c => c.Item2.IsVisible(user)).ToArray();
- var oldIndex = children.FindIndex(i => string.Equals(entryId, i.Item1.Id, StringComparison.OrdinalIgnoreCase));
+ var oldIndexAll = children.FindIndex(i => string.Equals(entryId, i.Item1.ItemId?.ToString("N", CultureInfo.InvariantCulture), StringComparison.OrdinalIgnoreCase));
+ var oldIndexAccessible = accessibleChildren.FindIndex(i => string.Equals(entryId, i.Item1.ItemId?.ToString("N", CultureInfo.InvariantCulture), StringComparison.OrdinalIgnoreCase));
- if (oldIndex == newIndex)
+ if (oldIndexAccessible == newIndex)
{
return;
}
- var item = playlist.LinkedChildren[oldIndex];
+ var newPriorItemIndex = newIndex > oldIndexAccessible ? newIndex : newIndex - 1 < 0 ? 0 : newIndex - 1;
+ var newPriorItemId = accessibleChildren[newPriorItemIndex].Item1.ItemId;
+ var newPriorItemIndexOnAllChildren = children.FindIndex(c => c.Item1.ItemId.Equals(newPriorItemId));
+ var adjustedNewIndex = newPriorItemIndexOnAllChildren + 1;
- var newList = playlist.LinkedChildren.ToList();
+ var item = playlist.LinkedChildren.FirstOrDefault(i => string.Equals(entryId, i.ItemId?.ToString("N", CultureInfo.InvariantCulture), StringComparison.OrdinalIgnoreCase));
+ if (item is null)
+ {
+ _logger.LogWarning("Modified item not found in playlist. ItemId: {ItemId}, PlaylistId: {PlaylistId}", item.ItemId, playlistId);
+ return;
+ }
+
+ var newList = playlist.LinkedChildren.ToList();
newList.Remove(item);
if (newIndex >= newList.Count)
@@ -314,7 +324,7 @@ namespace Emby.Server.Implementations.Playlists
}
else
{
- newList.Insert(newIndex, item);
+ newList.Insert(adjustedNewIndex, item);
}
playlist.LinkedChildren = [.. newList];
diff --git a/Jellyfin.Api/Controllers/ItemRefreshController.cs b/Jellyfin.Api/Controllers/ItemRefreshController.cs
index d7a8c37c4..7effe61e4 100644
--- a/Jellyfin.Api/Controllers/ItemRefreshController.cs
+++ b/Jellyfin.Api/Controllers/ItemRefreshController.cs
@@ -50,6 +50,7 @@ public class ItemRefreshController : BaseJellyfinApiController
/// <param name="imageRefreshMode">(Optional) Specifies the image refresh mode.</param>
/// <param name="replaceAllMetadata">(Optional) Determines if metadata should be replaced. Only applicable if mode is FullRefresh.</param>
/// <param name="replaceAllImages">(Optional) Determines if images should be replaced. Only applicable if mode is FullRefresh.</param>
+ /// <param name="regenerateTrickplay">(Optional) Determines if trickplay images should be replaced. Only applicable if mode is FullRefresh.</param>
/// <response code="204">Item metadata refresh queued.</response>
/// <response code="404">Item to refresh not found.</response>
/// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the item could not be found.</returns>
@@ -62,7 +63,8 @@ public class ItemRefreshController : BaseJellyfinApiController
[FromQuery] MetadataRefreshMode metadataRefreshMode = MetadataRefreshMode.None,
[FromQuery] MetadataRefreshMode imageRefreshMode = MetadataRefreshMode.None,
[FromQuery] bool replaceAllMetadata = false,
- [FromQuery] bool replaceAllImages = false)
+ [FromQuery] bool replaceAllImages = false,
+ [FromQuery] bool regenerateTrickplay = false)
{
var item = _libraryManager.GetItemById<BaseItem>(itemId, User.GetUserId());
if (item is null)
@@ -81,7 +83,8 @@ public class ItemRefreshController : BaseJellyfinApiController
|| replaceAllImages
|| replaceAllMetadata,
IsAutomated = false,
- RemoveOldMetadata = replaceAllMetadata
+ RemoveOldMetadata = replaceAllMetadata,
+ RegenerateTrickplay = regenerateTrickplay
};
_providerManager.QueueRefresh(item.Id, refreshOptions, RefreshPriority.High);
diff --git a/Jellyfin.Api/Controllers/MediaSegmentsController.cs b/Jellyfin.Api/Controllers/MediaSegmentsController.cs
index 3dc5167a2..2d1d4e2c8 100644
--- a/Jellyfin.Api/Controllers/MediaSegmentsController.cs
+++ b/Jellyfin.Api/Controllers/MediaSegmentsController.cs
@@ -55,7 +55,7 @@ public class MediaSegmentsController : BaseJellyfinApiController
return NotFound();
}
- var items = await _mediaSegmentManager.GetSegmentsAsync(item.Id, includeSegmentTypes).ConfigureAwait(false);
+ var items = await _mediaSegmentManager.GetSegmentsAsync(item, includeSegmentTypes).ConfigureAwait(false);
return Ok(new QueryResult<MediaSegmentDto>(items.ToArray()));
}
}
diff --git a/Jellyfin.Api/Controllers/PlaylistsController.cs b/Jellyfin.Api/Controllers/PlaylistsController.cs
index e6f23b136..1ab36ccc6 100644
--- a/Jellyfin.Api/Controllers/PlaylistsController.cs
+++ b/Jellyfin.Api/Controllers/PlaylistsController.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
+using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using Jellyfin.Api.Attributes;
@@ -426,7 +427,7 @@ public class PlaylistsController : BaseJellyfinApiController
return Forbid();
}
- await _playlistManager.MoveItemAsync(playlistId, itemId, newIndex).ConfigureAwait(false);
+ await _playlistManager.MoveItemAsync(playlistId, itemId, newIndex, callingUserId).ConfigureAwait(false);
return NoContent();
}
@@ -514,7 +515,8 @@ public class PlaylistsController : BaseJellyfinApiController
return Forbid();
}
- var items = playlist.GetManageableItems().ToArray();
+ var user = _userManager.GetUserById(callingUserId);
+ var items = playlist.GetManageableItems().Where(i => i.Item2.IsVisible(user)).ToArray();
var count = items.Length;
if (startIndex.HasValue)
{
@@ -529,11 +531,11 @@ public class PlaylistsController : BaseJellyfinApiController
var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(User)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
- var user = _userManager.GetUserById(callingUserId);
+
var dtos = _dtoService.GetBaseItemDtos(items.Select(i => i.Item2).ToList(), dtoOptions, user);
for (int index = 0; index < dtos.Count; index++)
{
- dtos[index].PlaylistItemId = items[index].Item1.Id;
+ dtos[index].PlaylistItemId = items[index].Item1.ItemId?.ToString("N", CultureInfo.InvariantCulture);
}
var result = new QueryResult<BaseItemDto>(
diff --git a/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentManager.cs b/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentManager.cs
index d641f521b..a044fec0d 100644
--- a/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentManager.cs
+++ b/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentManager.cs
@@ -139,23 +139,53 @@ public class MediaSegmentManager : IMediaSegmentManager
}
/// <inheritdoc />
- public async Task<IEnumerable<MediaSegmentDto>> GetSegmentsAsync(Guid itemId, IEnumerable<MediaSegmentType>? typeFilter)
+ public async Task<IEnumerable<MediaSegmentDto>> GetSegmentsAsync(Guid itemId, IEnumerable<MediaSegmentType>? typeFilter, bool filterByProvider = true)
+ {
+ var baseItem = _libraryManager.GetItemById(itemId);
+
+ if (baseItem is null)
+ {
+ _logger.LogError("Tried to request segments for an invalid item");
+ return [];
+ }
+
+ return await GetSegmentsAsync(baseItem, typeFilter, filterByProvider).ConfigureAwait(false);
+ }
+
+ /// <inheritdoc />
+ public async Task<IEnumerable<MediaSegmentDto>> GetSegmentsAsync(BaseItem item, IEnumerable<MediaSegmentType>? typeFilter, bool filterByProvider = true)
{
using var db = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
var query = db.MediaSegments
- .Where(e => e.ItemId.Equals(itemId));
+ .Where(e => e.ItemId.Equals(item.Id));
if (typeFilter is not null)
{
query = query.Where(e => typeFilter.Contains(e.Type));
}
+ if (filterByProvider)
+ {
+ var libraryOptions = _libraryManager.GetLibraryOptions(item);
+ var providerIds = _segmentProviders
+ .Where(e => !libraryOptions.DisabledMediaSegmentProviders.Contains(GetProviderId(e.Name)))
+ .Select(f => GetProviderId(f.Name))
+ .ToArray();
+ if (providerIds.Length == 0)
+ {
+ return [];
+ }
+
+ query = query.Where(e => providerIds.Contains(e.SegmentProviderId));
+ }
+
return query
.OrderBy(e => e.StartTicks)
.AsNoTracking()
- .ToImmutableList()
- .Select(Map);
+ .AsEnumerable()
+ .Select(Map)
+ .ToArray();
}
private static MediaSegmentDto Map(MediaSegment segment)
diff --git a/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs b/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs
index cfe385106..af57bc134 100644
--- a/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs
+++ b/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs
@@ -238,7 +238,7 @@ public class TrickplayManager : ITrickplayManager
foreach (var tile in existingFiles)
{
var image = _imageEncoder.GetImageSize(tile);
- localTrickplayInfo.Height = Math.Max(localTrickplayInfo.Height, image.Height);
+ localTrickplayInfo.Height = Math.Max(localTrickplayInfo.Height, (int)Math.Ceiling((double)image.Height / localTrickplayInfo.TileHeight));
var bitrate = (int)Math.Ceiling((decimal)new FileInfo(tile).Length * 8 / localTrickplayInfo.TileWidth / localTrickplayInfo.TileHeight / (localTrickplayInfo.Interval / 1000));
localTrickplayInfo.Bandwidth = Math.Max(localTrickplayInfo.Bandwidth, bitrate);
}
diff --git a/Jellyfin.Server/Infrastructure/SymlinkFollowingPhysicalFileResultExecutor.cs b/Jellyfin.Server/Infrastructure/SymlinkFollowingPhysicalFileResultExecutor.cs
index 801026c54..901ed55be 100644
--- a/Jellyfin.Server/Infrastructure/SymlinkFollowingPhysicalFileResultExecutor.cs
+++ b/Jellyfin.Server/Infrastructure/SymlinkFollowingPhysicalFileResultExecutor.cs
@@ -101,7 +101,7 @@ namespace Jellyfin.Server.Infrastructure
count: null);
}
- private async Task SendFileAsync(string filePath, HttpResponse response, long offset, long? count)
+ private async Task SendFileAsync(string filePath, HttpResponse response, long offset, long? count, CancellationToken cancellationToken = default)
{
var fileInfo = GetFileInfo(filePath);
if (offset < 0 || offset > fileInfo.Length)
@@ -118,6 +118,9 @@ namespace Jellyfin.Server.Infrastructure
// Copied from SendFileFallback.SendFileAsync
const int BufferSize = 1024 * 16;
+ var useRequestAborted = !cancellationToken.CanBeCanceled;
+ var localCancel = useRequestAborted ? response.HttpContext.RequestAborted : cancellationToken;
+
var fileStream = new FileStream(
filePath,
FileMode.Open,
@@ -127,10 +130,17 @@ namespace Jellyfin.Server.Infrastructure
options: FileOptions.Asynchronous | FileOptions.SequentialScan);
await using (fileStream.ConfigureAwait(false))
{
- fileStream.Seek(offset, SeekOrigin.Begin);
- await StreamCopyOperation
- .CopyToAsync(fileStream, response.Body, count, BufferSize, CancellationToken.None)
- .ConfigureAwait(true);
+ try
+ {
+ localCancel.ThrowIfCancellationRequested();
+ fileStream.Seek(offset, SeekOrigin.Begin);
+ await StreamCopyOperation
+ .CopyToAsync(fileStream, response.Body, count, BufferSize, localCancel)
+ .ConfigureAwait(true);
+ }
+ catch (OperationCanceledException) when (useRequestAborted)
+ {
+ }
}
}
diff --git a/Jellyfin.Server/Migrations/MigrationRunner.cs b/Jellyfin.Server/Migrations/MigrationRunner.cs
index 9d4441ac3..2ab130eef 100644
--- a/Jellyfin.Server/Migrations/MigrationRunner.cs
+++ b/Jellyfin.Server/Migrations/MigrationRunner.cs
@@ -47,7 +47,8 @@ namespace Jellyfin.Server.Migrations
typeof(Routines.AddDefaultCastReceivers),
typeof(Routines.UpdateDefaultPluginRepository),
typeof(Routines.FixAudioData),
- typeof(Routines.MoveTrickplayFiles)
+ typeof(Routines.MoveTrickplayFiles),
+ typeof(Routines.RemoveDuplicatePlaylistChildren)
};
/// <summary>
diff --git a/Jellyfin.Server/Migrations/Routines/FixPlaylistOwner.cs b/Jellyfin.Server/Migrations/Routines/FixPlaylistOwner.cs
index 3655a610d..192c170b2 100644
--- a/Jellyfin.Server/Migrations/Routines/FixPlaylistOwner.cs
+++ b/Jellyfin.Server/Migrations/Routines/FixPlaylistOwner.cs
@@ -15,12 +15,12 @@ namespace Jellyfin.Server.Migrations.Routines;
/// </summary>
internal class FixPlaylistOwner : IMigrationRoutine
{
- private readonly ILogger<RemoveDuplicateExtras> _logger;
+ private readonly ILogger<FixPlaylistOwner> _logger;
private readonly ILibraryManager _libraryManager;
private readonly IPlaylistManager _playlistManager;
public FixPlaylistOwner(
- ILogger<RemoveDuplicateExtras> logger,
+ ILogger<FixPlaylistOwner> logger,
ILibraryManager libraryManager,
IPlaylistManager playlistManager)
{
diff --git a/Jellyfin.Server/Migrations/Routines/RemoveDuplicatePlaylistChildren.cs b/Jellyfin.Server/Migrations/Routines/RemoveDuplicatePlaylistChildren.cs
new file mode 100644
index 000000000..f84bccc25
--- /dev/null
+++ b/Jellyfin.Server/Migrations/Routines/RemoveDuplicatePlaylistChildren.cs
@@ -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);
+ }
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Entities/LinkedChild.cs b/MediaBrowser.Controller/Entities/LinkedChild.cs
index fd5fef3dc..98e4f525f 100644
--- a/MediaBrowser.Controller/Entities/LinkedChild.cs
+++ b/MediaBrowser.Controller/Entities/LinkedChild.cs
@@ -4,7 +4,6 @@
using System;
using System.Globalization;
-using System.Text.Json.Serialization;
namespace MediaBrowser.Controller.Entities
{
@@ -12,7 +11,6 @@ namespace MediaBrowser.Controller.Entities
{
public LinkedChild()
{
- Id = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
}
public string Path { get; set; }
@@ -21,9 +19,6 @@ namespace MediaBrowser.Controller.Entities
public string LibraryItemId { get; set; }
- [JsonIgnore]
- public string Id { get; set; }
-
/// <summary>
/// Gets or sets the linked item id.
/// </summary>
@@ -31,6 +26,8 @@ namespace MediaBrowser.Controller.Entities
public static LinkedChild Create(BaseItem item)
{
+ ArgumentNullException.ThrowIfNull(item);
+
var child = new LinkedChild
{
Path = item.Path,
diff --git a/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs b/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs
index f8049cd48..e4806109a 100644
--- a/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs
+++ b/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs
@@ -50,11 +50,6 @@ namespace MediaBrowser.Controller.Extensions
public const string FfmpegPathKey = "ffmpeg";
/// <summary>
- /// The key for a setting that indicates whether playlists should allow duplicate entries.
- /// </summary>
- public const string PlaylistsAllowDuplicatesKey = "playlists:allowDuplicates";
-
- /// <summary>
/// The key for a setting that indicates whether kestrel should bind to a unix socket.
/// </summary>
public const string BindToUnixSocketKey = "kestrel:socket";
@@ -121,14 +116,6 @@ namespace MediaBrowser.Controller.Extensions
=> configuration.GetValue<bool>(FfmpegImgExtractPerfTradeoffKey);
/// <summary>
- /// Gets a value indicating whether playlists should allow duplicate entries from the <see cref="IConfiguration"/>.
- /// </summary>
- /// <param name="configuration">The configuration to read the setting from.</param>
- /// <returns>True if playlists should allow duplicates, otherwise false.</returns>
- public static bool DoPlaylistsAllowDuplicates(this IConfiguration configuration)
- => configuration.GetValue<bool>(PlaylistsAllowDuplicatesKey);
-
- /// <summary>
/// Gets a value indicating whether kestrel should bind to a unix socket from the <see cref="IConfiguration" />.
/// </summary>
/// <param name="configuration">The configuration to read the setting from.</param>
diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
index 28f0d1fff..9399679a4 100644
--- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
+++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
@@ -2196,7 +2196,10 @@ namespace MediaBrowser.Controller.MediaEncoding
{
var videoFrameRate = videoStream.ReferenceFrameRate;
- if (!videoFrameRate.HasValue || videoFrameRate.Value > requestedFramerate.Value)
+ // Add a little tolerance to the framerate check because some videos might record a framerate
+ // that is slightly higher than the intended framerate, but the device can still play it correctly.
+ // 0.05 fps tolerance should be safe enough.
+ if (!videoFrameRate.HasValue || videoFrameRate.Value > requestedFramerate.Value + 0.05f)
{
return false;
}
@@ -3318,24 +3321,25 @@ namespace MediaBrowser.Controller.MediaEncoding
&& options.VppTonemappingBrightness >= -100
&& options.VppTonemappingBrightness <= 100)
{
- procampParams += $"=b={options.VppTonemappingBrightness}";
+ procampParams += "procamp_vaapi=b={0}";
doVaVppProcamp = true;
}
if (options.VppTonemappingContrast > 1
&& options.VppTonemappingContrast <= 10)
{
- procampParams += doVaVppProcamp ? ":" : "=";
- procampParams += $"c={options.VppTonemappingContrast}";
+ procampParams += doVaVppProcamp ? ":c={1}" : "procamp_vaapi=c={1}";
doVaVppProcamp = true;
}
- args = "{0}tonemap_vaapi=format={1}:p=bt709:t=bt709:m=bt709:extra_hw_frames=32";
+ args = procampParams + "{2}tonemap_vaapi=format={3}:p=bt709:t=bt709:m=bt709:extra_hw_frames=32";
return string.Format(
CultureInfo.InvariantCulture,
args,
- doVaVppProcamp ? $"procamp_vaapi{procampParams}," : string.Empty,
+ options.VppTonemappingBrightness,
+ options.VppTonemappingContrast,
+ doVaVppProcamp ? "," : string.Empty,
videoFormat ?? "nv12");
}
else
@@ -3523,20 +3527,29 @@ namespace MediaBrowser.Controller.MediaEncoding
{
// tonemapx requires yuv420p10 input for dovi reshaping, let ffmpeg convert the frame when necessary
var tonemapFormat = requireDoviReshaping ? "yuv420p" : outFormat;
-
- var tonemapArgs = $"tonemapx=tonemap={options.TonemappingAlgorithm}:desat={options.TonemappingDesat}:peak={options.TonemappingPeak}:t=bt709:m=bt709:p=bt709:format={tonemapFormat}";
+ var tonemapArgString = "tonemapx=tonemap={0}:desat={1}:peak={2}:t=bt709:m=bt709:p=bt709:format={3}";
if (options.TonemappingParam != 0)
{
- tonemapArgs += $":param={options.TonemappingParam}";
+ tonemapArgString += ":param={4}";
}
var range = options.TonemappingRange;
if (range == TonemappingRange.tv || range == TonemappingRange.pc)
{
- tonemapArgs += $":range={options.TonemappingRange}";
+ tonemapArgString += ":range={5}";
}
+ var tonemapArgs = string.Format(
+ CultureInfo.InvariantCulture,
+ tonemapArgString,
+ options.TonemappingAlgorithm,
+ options.TonemappingDesat,
+ options.TonemappingPeak,
+ tonemapFormat,
+ options.TonemappingParam,
+ options.TonemappingRange);
+
mainFilters.Add(tonemapArgs);
}
else
@@ -4128,31 +4141,46 @@ namespace MediaBrowser.Controller.MediaEncoding
else if (isD3d11vaDecoder || isQsvDecoder)
{
var isRext = IsVideoStreamHevcRext(state);
- var twoPassVppTonemap = isRext;
+ var twoPassVppTonemap = false;
var doVppFullRangeOut = isMjpegEncoder
&& _mediaEncoder.EncoderVersion >= _minFFmpegQsvVppOutRangeOption;
var doVppScaleModeHq = isMjpegEncoder
&& _mediaEncoder.EncoderVersion >= _minFFmpegQsvVppScaleModeOption;
var doVppProcamp = false;
var procampParams = string.Empty;
+ var procampParamsString = string.Empty;
if (doVppTonemap)
{
+ if (isRext)
+ {
+ // VPP tonemap requires p010 input
+ twoPassVppTonemap = true;
+ }
+
if (options.VppTonemappingBrightness != 0
&& options.VppTonemappingBrightness >= -100
&& options.VppTonemappingBrightness <= 100)
{
- procampParams += $":brightness={options.VppTonemappingBrightness}";
+ procampParamsString += ":brightness={0}";
twoPassVppTonemap = doVppProcamp = true;
}
if (options.VppTonemappingContrast > 1
&& options.VppTonemappingContrast <= 10)
{
- procampParams += $":contrast={options.VppTonemappingContrast}";
+ procampParamsString += ":contrast={1}";
twoPassVppTonemap = doVppProcamp = true;
}
- procampParams += doVppProcamp ? ":procamp=1:async_depth=2" : string.Empty;
+ if (doVppProcamp)
+ {
+ procampParamsString += ":procamp=1:async_depth=2";
+ procampParams = string.Format(
+ CultureInfo.InvariantCulture,
+ procampParamsString,
+ options.VppTonemappingBrightness,
+ options.VppTonemappingContrast);
+ }
}
var outFormat = doOclTonemap ? ((doVppTranspose || isRext) ? "p010" : string.Empty) : "nv12";
diff --git a/MediaBrowser.Controller/MediaSegements/IMediaSegmentManager.cs b/MediaBrowser.Controller/MediaSegements/IMediaSegmentManager.cs
index 010d7edb4..672f27eca 100644
--- a/MediaBrowser.Controller/MediaSegements/IMediaSegmentManager.cs
+++ b/MediaBrowser.Controller/MediaSegements/IMediaSegmentManager.cs
@@ -50,8 +50,18 @@ public interface IMediaSegmentManager
/// </summary>
/// <param name="itemId">The id of the <see cref="BaseItem"/>.</param>
/// <param name="typeFilter">filteres all media segments of the given type to be included. If null all types are included.</param>
+ /// <param name="filterByProvider">When set filteres the segments to only return those that which providers are currently enabled on their library.</param>
/// <returns>An enumerator of <see cref="MediaSegmentDto"/>'s.</returns>
- Task<IEnumerable<MediaSegmentDto>> GetSegmentsAsync(Guid itemId, IEnumerable<MediaSegmentType>? typeFilter);
+ Task<IEnumerable<MediaSegmentDto>> GetSegmentsAsync(Guid itemId, IEnumerable<MediaSegmentType>? typeFilter, bool filterByProvider = true);
+
+ /// <summary>
+ /// Obtains all segments accociated with the itemId.
+ /// </summary>
+ /// <param name="item">The <see cref="BaseItem"/>.</param>
+ /// <param name="typeFilter">filteres all media segments of the given type to be included. If null all types are included.</param>
+ /// <param name="filterByProvider">When set filteres the segments to only return those that which providers are currently enabled on their library.</param>
+ /// <returns>An enumerator of <see cref="MediaSegmentDto"/>'s.</returns>
+ Task<IEnumerable<MediaSegmentDto>> GetSegmentsAsync(BaseItem item, IEnumerable<MediaSegmentType>? typeFilter, bool filterByProvider = true);
/// <summary>
/// Gets information about any media segments stored for the given itemId.
diff --git a/MediaBrowser.Controller/Playlists/IPlaylistManager.cs b/MediaBrowser.Controller/Playlists/IPlaylistManager.cs
index 038cbd2d6..497c4a511 100644
--- a/MediaBrowser.Controller/Playlists/IPlaylistManager.cs
+++ b/MediaBrowser.Controller/Playlists/IPlaylistManager.cs
@@ -92,8 +92,9 @@ namespace MediaBrowser.Controller.Playlists
/// <param name="playlistId">The playlist identifier.</param>
/// <param name="entryId">The entry identifier.</param>
/// <param name="newIndex">The new index.</param>
+ /// <param name="callingUserId">The calling user.</param>
/// <returns>Task.</returns>
- Task MoveItemAsync(string playlistId, string entryId, int newIndex);
+ Task MoveItemAsync(string playlistId, string entryId, int newIndex, Guid callingUserId);
/// <summary>
/// Removed all playlists of a user.
diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
index 826ffd0b7..a34238cd6 100644
--- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
+++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
@@ -1035,6 +1035,16 @@ namespace MediaBrowser.MediaEncoding.Encoder
if (exitCode == -1)
{
_logger.LogError("ffmpeg image extraction failed for {ProcessDescription}", processDescription);
+ // Cleanup temp folder here, because the targetDirectory is not returned and the cleanup for failed ffmpeg process is not possible for caller.
+ // Ideally the ffmpeg should not write any files if it fails, but it seems like it is not guaranteed.
+ try
+ {
+ Directory.Delete(targetDirectory, true);
+ }
+ catch (Exception e)
+ {
+ _logger.LogError(e, "Failed to delete ffmpeg temp directory {TargetDirectory}", targetDirectory);
+ }
throw new FfmpegException(string.Format(CultureInfo.InvariantCulture, "ffmpeg image extraction failed for {0}", processDescription));
}
diff --git a/MediaBrowser.Model/Extensions/LibraryOptionsExtension.cs b/MediaBrowser.Model/Extensions/LibraryOptionsExtension.cs
index 4a814f22a..b088cfb53 100644
--- a/MediaBrowser.Model/Extensions/LibraryOptionsExtension.cs
+++ b/MediaBrowser.Model/Extensions/LibraryOptionsExtension.cs
@@ -18,7 +18,7 @@ public static class LibraryOptionsExtension
{
ArgumentNullException.ThrowIfNull(options);
- return options.CustomTagDelimiters.Select<string, char?>(x =>
+ var delimiterList = options.CustomTagDelimiters.Select<string, char?>(x =>
{
var isChar = char.TryParse(x, out var c);
if (isChar)
@@ -27,6 +27,8 @@ public static class LibraryOptionsExtension
}
return null;
- }).Where(x => x is not null).Select(x => x!.Value).ToArray();
+ }).Where(x => x is not null).Select(x => x!.Value).ToList();
+ delimiterList.Add('\0');
+ return delimiterList.ToArray();
}
}
diff --git a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs
index 27f6d120f..7f1fdbcb8 100644
--- a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs
+++ b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs
@@ -347,7 +347,8 @@ namespace MediaBrowser.Providers.MediaInfo
|| track.AdditionalFields.TryGetValue("MusicBrainz Artist Id", out musicBrainzArtistTag))
&& !string.IsNullOrEmpty(musicBrainzArtistTag))
{
- audio.TrySetProviderId(MetadataProvider.MusicBrainzArtist, musicBrainzArtistTag);
+ var id = GetFirstMusicBrainzId(musicBrainzArtistTag, libraryOptions.UseCustomTagDelimiters, libraryOptions.GetCustomTagDelimiters(), libraryOptions.DelimiterWhitelist);
+ audio.TrySetProviderId(MetadataProvider.MusicBrainzArtist, id);
}
}
@@ -357,7 +358,8 @@ namespace MediaBrowser.Providers.MediaInfo
|| track.AdditionalFields.TryGetValue("MusicBrainz Album Artist Id", out musicBrainzReleaseArtistIdTag))
&& !string.IsNullOrEmpty(musicBrainzReleaseArtistIdTag))
{
- audio.TrySetProviderId(MetadataProvider.MusicBrainzAlbumArtist, musicBrainzReleaseArtistIdTag);
+ var id = GetFirstMusicBrainzId(musicBrainzReleaseArtistIdTag, libraryOptions.UseCustomTagDelimiters, libraryOptions.GetCustomTagDelimiters(), libraryOptions.DelimiterWhitelist);
+ audio.TrySetProviderId(MetadataProvider.MusicBrainzAlbumArtist, id);
}
}
@@ -367,7 +369,8 @@ namespace MediaBrowser.Providers.MediaInfo
|| track.AdditionalFields.TryGetValue("MusicBrainz Album Id", out musicBrainzReleaseIdTag))
&& !string.IsNullOrEmpty(musicBrainzReleaseIdTag))
{
- audio.TrySetProviderId(MetadataProvider.MusicBrainzAlbum, musicBrainzReleaseIdTag);
+ var id = GetFirstMusicBrainzId(musicBrainzReleaseIdTag, libraryOptions.UseCustomTagDelimiters, libraryOptions.GetCustomTagDelimiters(), libraryOptions.DelimiterWhitelist);
+ audio.TrySetProviderId(MetadataProvider.MusicBrainzAlbum, id);
}
}
@@ -377,7 +380,8 @@ namespace MediaBrowser.Providers.MediaInfo
|| track.AdditionalFields.TryGetValue("MusicBrainz Release Group Id", out musicBrainzReleaseGroupIdTag))
&& !string.IsNullOrEmpty(musicBrainzReleaseGroupIdTag))
{
- audio.TrySetProviderId(MetadataProvider.MusicBrainzReleaseGroup, musicBrainzReleaseGroupIdTag);
+ var id = GetFirstMusicBrainzId(musicBrainzReleaseGroupIdTag, libraryOptions.UseCustomTagDelimiters, libraryOptions.GetCustomTagDelimiters(), libraryOptions.DelimiterWhitelist);
+ audio.TrySetProviderId(MetadataProvider.MusicBrainzReleaseGroup, id);
}
}
@@ -387,7 +391,8 @@ namespace MediaBrowser.Providers.MediaInfo
|| track.AdditionalFields.TryGetValue("MusicBrainz Release Track Id", out trackMbId))
&& !string.IsNullOrEmpty(trackMbId))
{
- audio.TrySetProviderId(MetadataProvider.MusicBrainzTrack, trackMbId);
+ var id = GetFirstMusicBrainzId(trackMbId, libraryOptions.UseCustomTagDelimiters, libraryOptions.GetCustomTagDelimiters(), libraryOptions.DelimiterWhitelist);
+ audio.TrySetProviderId(MetadataProvider.MusicBrainzTrack, id);
}
}
@@ -441,5 +446,18 @@ namespace MediaBrowser.Providers.MediaInfo
return items;
}
+
+ // MusicBrainz IDs are multi-value tags, so we need to split them
+ // However, our current provider can only have one single ID, which means we need to pick the first one
+ private string? GetFirstMusicBrainzId(string tag, bool useCustomTagDelimiters, char[] tagDelimiters, string[] whitelist)
+ {
+ var val = tag.Split(InternalValueSeparator).FirstOrDefault();
+ if (val is not null && useCustomTagDelimiters)
+ {
+ val = SplitWithCustomDelimiter(val, tagDelimiters, whitelist).FirstOrDefault();
+ }
+
+ return val;
+ }
}
}
diff --git a/src/Jellyfin.Extensions/Json/Converters/JsonDelimitedArrayConverter.cs b/src/Jellyfin.Extensions/Json/Converters/JsonDelimitedArrayConverter.cs
index cdeaf29b0..c53ef275b 100644
--- a/src/Jellyfin.Extensions/Json/Converters/JsonDelimitedArrayConverter.cs
+++ b/src/Jellyfin.Extensions/Json/Converters/JsonDelimitedArrayConverter.cs
@@ -70,24 +70,11 @@ namespace Jellyfin.Extensions.Json.Converters
writer.WriteStartArray();
if (value.Length > 0)
{
- var toWrite = value.Length - 1;
foreach (var it in value)
{
- var wrote = false;
if (it is not null)
{
writer.WriteStringValue(it.ToString());
- wrote = true;
- }
-
- if (toWrite > 0)
- {
- if (wrote)
- {
- writer.WriteStringValue(Delimiter.ToString());
- }
-
- toWrite--;
}
}
}