Merge remote-tracking branch 'origin/master' into feature/EFUserData

This commit is contained in:
JPVenson 2024-11-19 20:53:38 +00:00
commit 0dd6dacc4f
25 changed files with 280 additions and 101 deletions

View File

@ -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 },

View File

@ -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))
{ {

View File

@ -3,7 +3,7 @@
"AppDeviceValues": "앱: {0}, 장치: {1}", "AppDeviceValues": "앱: {0}, 장치: {1}",
"Application": "애플리케이션", "Application": "애플리케이션",
"Artists": "아티스트", "Artists": "아티스트",
"AuthenticationSucceededWithUserName": "{0}이(가) 성공적으로 인증됨", "AuthenticationSucceededWithUserName": "{0} 사용자가 성공적으로 인증됨",
"Books": "도서", "Books": "도서",
"CameraImageUploadedFrom": "{0}에서 새로운 카메라 이미지가 업로드됨", "CameraImageUploadedFrom": "{0}에서 새로운 카메라 이미지가 업로드됨",
"Channels": "채널", "Channels": "채널",
@ -70,7 +70,7 @@
"ScheduledTaskFailedWithName": "{0} 실패", "ScheduledTaskFailedWithName": "{0} 실패",
"ScheduledTaskStartedWithName": "{0} 시작", "ScheduledTaskStartedWithName": "{0} 시작",
"ServerNameNeedsToBeRestarted": "{0}를 재시작해야합니다", "ServerNameNeedsToBeRestarted": "{0}를 재시작해야합니다",
"Shows": "", "Shows": "시리즈",
"Songs": "노래", "Songs": "노래",
"StartupEmbyServerIsLoading": "Jellyfin 서버를 불러오고 있습니다. 잠시 후에 다시 시도하십시오.", "StartupEmbyServerIsLoading": "Jellyfin 서버를 불러오고 있습니다. 잠시 후에 다시 시도하십시오.",
"SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}", "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
@ -81,14 +81,14 @@
"User": "사용자", "User": "사용자",
"UserCreatedWithName": "사용자 {0} 생성됨", "UserCreatedWithName": "사용자 {0} 생성됨",
"UserDeletedWithName": "사용자 {0} 삭제됨", "UserDeletedWithName": "사용자 {0} 삭제됨",
"UserDownloadingItemWithValues": "{0}이(가) {1}을 다운로드 중입니다", "UserDownloadingItemWithValues": "{0} 사용자가 {1} 다운로드 중",
"UserLockedOutWithName": "유저 {0} 은(는) 잠금처리 되었습니다", "UserLockedOutWithName": "{0} 사용자 잠김",
"UserOfflineFromDevice": "{1}에서 {0}의 연결이 끊킴", "UserOfflineFromDevice": "{0} 사용자의 {1}에서 연결이 끊김",
"UserOnlineFromDevice": "{0}이 {1}으로 접속", "UserOnlineFromDevice": "{0} 사용자가 {1}에서 접속함",
"UserPasswordChangedWithName": "사용자 {0}의 비밀번호가 변경되었습니다", "UserPasswordChangedWithName": "{0} 사용자 비밀번호 변경됨",
"UserPolicyUpdatedWithName": "{0}의 사용자 정책이 업데이트되었습니다", "UserPolicyUpdatedWithName": "{0} 사용자 정책 업데이트됨",
"UserStartedPlayingItemWithValues": "{2}에서 {0}이 {1} 재생 중", "UserStartedPlayingItemWithValues": "{0} 사용자의 {2}에서 {1} 재생 중",
"UserStoppedPlayingItemWithValues": "{2}에서 {0}이 {1} 재생을 마침", "UserStoppedPlayingItemWithValues": "{0} 사용자의 {2}에서 {1} 재생을 마침",
"ValueHasBeenAddedToLibrary": "{0}가 미디어 라이브러리에 추가되었습니다", "ValueHasBeenAddedToLibrary": "{0}가 미디어 라이브러리에 추가되었습니다",
"ValueSpecialEpisodeName": "스페셜 - {0}", "ValueSpecialEpisodeName": "스페셜 - {0}",
"VersionNumber": "버전 {0}", "VersionNumber": "버전 {0}",

View File

@ -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];

View File

@ -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);

View File

@ -865,6 +865,16 @@ public class LibraryController : BaseJellyfinApiController
.DistinctBy(i => i.Name, StringComparer.OrdinalIgnoreCase) .DistinctBy(i => i.Name, StringComparer.OrdinalIgnoreCase)
.ToArray(); .ToArray();
result.MediaSegmentProviders = plugins
.SelectMany(i => i.Plugins.Where(p => p.Type == MetadataPluginType.MediaSegmentProvider))
.Select(i => new LibraryOptionInfoDto
{
Name = i.Name,
DefaultEnabled = true
})
.DistinctBy(i => i.Name, StringComparer.OrdinalIgnoreCase)
.ToArray();
var typeOptions = new List<LibraryTypeOptionsDto>(); var typeOptions = new List<LibraryTypeOptionsDto>();
foreach (var type in types) foreach (var type in types)

View File

@ -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()));
} }
} }

View File

@ -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>(

View File

@ -28,6 +28,11 @@ public class LibraryOptionsResultDto
/// </summary> /// </summary>
public IReadOnlyList<LibraryOptionInfoDto> LyricFetchers { get; set; } = Array.Empty<LibraryOptionInfoDto>(); public IReadOnlyList<LibraryOptionInfoDto> LyricFetchers { get; set; } = Array.Empty<LibraryOptionInfoDto>();
/// <summary>
/// Gets or sets the list of MediaSegment Providers.
/// </summary>
public IReadOnlyList<LibraryOptionInfoDto> MediaSegmentProviders { get; set; } = Array.Empty<LibraryOptionInfoDto>();
/// <summary> /// <summary>
/// Gets or sets the type options. /// Gets or sets the type options.
/// </summary> /// </summary>

View File

@ -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()
.ToArray() .AsEnumerable()
.Select(Map); .Select(Map)
.ToArray();
} }
private static MediaSegmentDto Map(MediaSegment segment) private static MediaSegmentDto Map(MediaSegment segment)

View File

@ -238,7 +238,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);
} }

View File

@ -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)
{
}
} }
} }

View File

@ -48,6 +48,7 @@ namespace Jellyfin.Server.Migrations
typeof(Routines.UpdateDefaultPluginRepository), typeof(Routines.UpdateDefaultPluginRepository),
typeof(Routines.FixAudioData), typeof(Routines.FixAudioData),
typeof(Routines.MoveTrickplayFiles), typeof(Routines.MoveTrickplayFiles),
typeof(Routines.RemoveDuplicatePlaylistChildren),
typeof(Routines.MigrateLibraryDb), typeof(Routines.MigrateLibraryDb),
}; };

View File

@ -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)
{ {

View File

@ -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);
}
}
}
}
}

View File

@ -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,

View File

@ -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>

View File

@ -2196,7 +2196,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 +3321,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 +3527,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 +4141,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";

View File

@ -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.

View File

@ -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.

View File

@ -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));
} }

View File

@ -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();
} }
} }

View File

@ -38,5 +38,5 @@ public class PlaylistCreationRequest
/// <summary> /// <summary>
/// Gets or sets a value indicating whether the playlist is public. /// Gets or sets a value indicating whether the playlist is public.
/// </summary> /// </summary>
public bool? Public { get; set; } = true; public bool? Public { get; set; } = false;
} }

View File

@ -351,7 +351,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);
} }
} }
@ -361,7 +362,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);
} }
} }
@ -371,7 +373,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);
} }
} }
@ -381,7 +384,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);
} }
} }
@ -391,7 +395,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);
} }
} }
@ -445,5 +450,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;
}
} }
} }

View File

@ -70,24 +70,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--;
} }
} }
} }