aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/ci-codeql-analysis.yml6
-rw-r--r--.github/workflows/pull-request-conflict.yml2
-rw-r--r--Directory.Packages.props4
-rw-r--r--Emby.Server.Implementations/HttpServer/WebSocketConnection.cs10
-rw-r--r--Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs2
-rw-r--r--Emby.Server.Implementations/Library/LibraryManager.cs202
-rw-r--r--Emby.Server.Implementations/Library/PathExtensions.cs5
-rw-r--r--Emby.Server.Implementations/Library/Validators/PeopleValidator.cs7
-rw-r--r--Emby.Server.Implementations/Localization/Core/fa.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/my.json12
-rw-r--r--Emby.Server.Implementations/Localization/LocalizationManager.cs7
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionAndPlaylistPathsTask.cs2
-rw-r--r--Emby.Server.Implementations/Serialization/MyXmlSerializer.cs5
-rw-r--r--Emby.Server.Implementations/Session/SessionWebSocketListener.cs114
-rw-r--r--Jellyfin.Api/Controllers/DisplayPreferencesController.cs2
-rw-r--r--Jellyfin.Api/Controllers/FilterController.cs2
-rw-r--r--Jellyfin.Api/Controllers/ImageController.cs20
-rw-r--r--Jellyfin.Api/Controllers/InstantMixController.cs60
-rw-r--r--Jellyfin.Api/Controllers/ItemLookupController.cs13
-rw-r--r--Jellyfin.Api/Controllers/ItemRefreshController.cs5
-rw-r--r--Jellyfin.Api/Controllers/ItemUpdateController.cs12
-rw-r--r--Jellyfin.Api/Controllers/ItemsController.cs12
-rw-r--r--Jellyfin.Api/Controllers/LibraryController.cs65
-rw-r--r--Jellyfin.Api/Controllers/LibraryStructureController.cs28
-rw-r--r--Jellyfin.Api/Controllers/LiveTvController.cs27
-rw-r--r--Jellyfin.Api/Controllers/LyricsController.cs58
-rw-r--r--Jellyfin.Api/Controllers/MediaInfoController.cs42
-rw-r--r--Jellyfin.Api/Controllers/PlaylistsController.cs7
-rw-r--r--Jellyfin.Api/Controllers/PlaystateController.cs21
-rw-r--r--Jellyfin.Api/Controllers/RemoteImageController.cs9
-rw-r--r--Jellyfin.Api/Controllers/SearchController.cs2
-rw-r--r--Jellyfin.Api/Controllers/SubtitleController.cs50
-rw-r--r--Jellyfin.Api/Controllers/TrickplayController.cs4
-rw-r--r--Jellyfin.Api/Controllers/TvShowsController.cs15
-rw-r--r--Jellyfin.Api/Controllers/UniversalAudioController.cs27
-rw-r--r--Jellyfin.Api/Controllers/UserLibraryController.cs116
-rw-r--r--Jellyfin.Api/Controllers/VideoAttachmentsController.cs5
-rw-r--r--Jellyfin.Api/Controllers/VideosController.cs31
-rw-r--r--Jellyfin.Api/Helpers/MediaInfoHelper.cs16
-rw-r--r--Jellyfin.Api/Helpers/StreamingHelpers.cs7
-rw-r--r--Jellyfin.Api/Models/LibraryStructureDto/MediaPathDto.cs2
-rw-r--r--Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs4
-rw-r--r--Jellyfin.Server/Migrations/Routines/RemoveDownloadImagesInAdvance.cs2
-rw-r--r--MediaBrowser.Controller/Library/ILibraryManager.cs53
-rw-r--r--MediaBrowser.Controller/Resolvers/IResolverIgnoreRule.cs2
-rw-r--r--MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs2
-rw-r--r--MediaBrowser.MediaEncoding/Transcoding/TranscodeManager.cs7
-rw-r--r--MediaBrowser.Providers/MediaInfo/AudioFileProber.cs6
-rw-r--r--MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs2
-rw-r--r--src/Jellyfin.Drawing.Skia/SkiaEncoder.cs5
-rw-r--r--src/Jellyfin.Networking/AutoDiscoveryHost.cs40
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Library/PathExtensionsTests.cs6
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs16
53 files changed, 663 insertions, 522 deletions
diff --git a/.github/workflows/ci-codeql-analysis.yml b/.github/workflows/ci-codeql-analysis.yml
index 39fe6f1d2..d179ff4f6 100644
--- a/.github/workflows/ci-codeql-analysis.yml
+++ b/.github/workflows/ci-codeql-analysis.yml
@@ -27,11 +27,11 @@ jobs:
dotnet-version: '8.0.x'
- name: Initialize CodeQL
- uses: github/codeql-action/init@4355270be187e1b672a7a1c7c7bae5afdc1ab94a # v3.24.10
+ uses: github/codeql-action/init@c7f9125735019aa87cfc361530512d50ea439c71 # v3.25.1
with:
languages: ${{ matrix.language }}
queries: +security-extended
- name: Autobuild
- uses: github/codeql-action/autobuild@4355270be187e1b672a7a1c7c7bae5afdc1ab94a # v3.24.10
+ uses: github/codeql-action/autobuild@c7f9125735019aa87cfc361530512d50ea439c71 # v3.25.1
- name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@4355270be187e1b672a7a1c7c7bae5afdc1ab94a # v3.24.10
+ uses: github/codeql-action/analyze@c7f9125735019aa87cfc361530512d50ea439c71 # v3.25.1
diff --git a/.github/workflows/pull-request-conflict.yml b/.github/workflows/pull-request-conflict.yml
index 05517bb03..1c3fac3c6 100644
--- a/.github/workflows/pull-request-conflict.yml
+++ b/.github/workflows/pull-request-conflict.yml
@@ -15,7 +15,7 @@ jobs:
if: ${{ github.repository == 'jellyfin/jellyfin' }}
steps:
- name: Apply label
- uses: eps1lon/actions-label-merge-conflict@fd1f295ee7443d13745804bc49fe158e240f6c6e # tag=v2.1.0
+ uses: eps1lon/actions-label-merge-conflict@e62d7a53ff8be8b97684bffb6cfbbf3fc1115e2e # v3.0.0
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request_target'}}
with:
dirtyLabel: 'merge conflict'
diff --git a/Directory.Packages.props b/Directory.Packages.props
index d67dba225..e125b536a 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -16,13 +16,13 @@
<PackageVersion Include="Diacritics" Version="3.3.27" />
<PackageVersion Include="DiscUtils.Udf" Version="0.16.13" />
<PackageVersion Include="DotNet.Glob" Version="3.1.3" />
- <PackageVersion Include="EFCoreSecondLevelCacheInterceptor" Version="4.3.1" />
+ <PackageVersion Include="EFCoreSecondLevelCacheInterceptor" Version="4.4.1" />
<PackageVersion Include="FsCheck.Xunit" Version="2.16.6" />
<PackageVersion Include="HarfBuzzSharp.NativeAssets.Linux" Version="7.3.0.2" />
<PackageVersion Include="ICU4N.Transliterator" Version="60.1.0-alpha.356" />
<PackageVersion Include="IDisposableAnalyzers" Version="4.0.7" />
<PackageVersion Include="Jellyfin.XmlTv" Version="10.8.0" />
- <PackageVersion Include="libse" Version="3.6.13" />
+ <PackageVersion Include="libse" Version="4.0.5" />
<PackageVersion Include="LrcParser" Version="2023.524.0" />
<PackageVersion Include="MetaBrainz.MusicBrainz" Version="6.1.0" />
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="8.0.4" />
diff --git a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
index f83da566b..34dc027f1 100644
--- a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
+++ b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
@@ -101,14 +101,14 @@ namespace Emby.Server.Implementations.HttpServer
var pipe = new Pipe();
var writer = pipe.Writer;
- ValueWebSocketReceiveResult receiveresult;
+ ValueWebSocketReceiveResult receiveResult;
do
{
// Allocate at least 512 bytes from the PipeWriter
Memory<byte> memory = writer.GetMemory(512);
try
{
- receiveresult = await _socket.ReceiveAsync(memory, cancellationToken).ConfigureAwait(false);
+ receiveResult = await _socket.ReceiveAsync(memory, cancellationToken).ConfigureAwait(false);
}
catch (WebSocketException ex)
{
@@ -116,7 +116,7 @@ namespace Emby.Server.Implementations.HttpServer
break;
}
- int bytesRead = receiveresult.Count;
+ int bytesRead = receiveResult.Count;
if (bytesRead == 0)
{
break;
@@ -135,13 +135,13 @@ namespace Emby.Server.Implementations.HttpServer
LastActivityDate = DateTime.UtcNow;
- if (receiveresult.EndOfMessage)
+ if (receiveResult.EndOfMessage)
{
await ProcessInternal(pipe.Reader).ConfigureAwait(false);
}
}
while ((_socket.State == WebSocketState.Open || _socket.State == WebSocketState.Connecting)
- && receiveresult.MessageType != WebSocketMessageType.Close);
+ && receiveResult.MessageType != WebSocketMessageType.Close);
Closed?.Invoke(this, EventArgs.Empty);
diff --git a/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs b/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs
index 665d70a41..b01fd93a7 100644
--- a/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs
+++ b/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs
@@ -29,7 +29,7 @@ namespace Emby.Server.Implementations.Library
}
/// <inheritdoc />
- public bool ShouldIgnore(FileSystemMetadata fileInfo, BaseItem parent)
+ public bool ShouldIgnore(FileSystemMetadata fileInfo, BaseItem? parent)
{
// Don't ignore application folders
if (fileInfo.FullName.Contains(_serverApplicationPaths.RootFolderPath, StringComparison.InvariantCulture))
diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs
index ea8b83125..7b3871a9b 100644
--- a/Emby.Server.Implementations/Library/LibraryManager.cs
+++ b/Emby.Server.Implementations/Library/LibraryManager.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
#pragma warning disable CS1591
using System;
@@ -46,6 +44,7 @@ using MediaBrowser.Model.Library;
using MediaBrowser.Model.Querying;
using MediaBrowser.Model.Tasks;
using Microsoft.Extensions.Logging;
+using TMDbLib.Objects.Authentication;
using Episode = MediaBrowser.Controller.Entities.TV.Episode;
using EpisodeInfo = Emby.Naming.TV.EpisodeInfo;
using Genre = MediaBrowser.Controller.Entities.Genre;
@@ -89,8 +88,8 @@ namespace Emby.Server.Implementations.Library
/// <summary>
/// The _root folder.
/// </summary>
- private volatile AggregateFolder _rootFolder;
- private volatile UserRootFolder _userRootFolder;
+ private volatile AggregateFolder? _rootFolder;
+ private volatile UserRootFolder? _userRootFolder;
private bool _wizardCompleted;
@@ -155,17 +154,17 @@ namespace Emby.Server.Implementations.Library
/// <summary>
/// Occurs when [item added].
/// </summary>
- public event EventHandler<ItemChangeEventArgs> ItemAdded;
+ public event EventHandler<ItemChangeEventArgs>? ItemAdded;
/// <summary>
/// Occurs when [item updated].
/// </summary>
- public event EventHandler<ItemChangeEventArgs> ItemUpdated;
+ public event EventHandler<ItemChangeEventArgs>? ItemUpdated;
/// <summary>
/// Occurs when [item removed].
/// </summary>
- public event EventHandler<ItemChangeEventArgs> ItemRemoved;
+ public event EventHandler<ItemChangeEventArgs>? ItemRemoved;
/// <summary>
/// Gets the root folder.
@@ -264,7 +263,7 @@ namespace Emby.Server.Implementations.Library
/// </summary>
/// <param name="sender">The sender.</param>
/// <param name="e">The <see cref="EventArgs" /> instance containing the event data.</param>
- private void ConfigurationUpdated(object sender, EventArgs e)
+ private void ConfigurationUpdated(object? sender, EventArgs e)
{
var config = _configurationManager.Configuration;
@@ -479,7 +478,7 @@ namespace Emby.Server.Implementations.Library
/// <param name="args">The args.</param>
/// <param name="resolvers">The resolvers.</param>
/// <returns>BaseItem.</returns>
- private BaseItem ResolveItem(ItemResolveArgs args, IItemResolver[] resolvers)
+ private BaseItem? ResolveItem(ItemResolveArgs args, IItemResolver[]? resolvers)
{
var item = (resolvers ?? EntityResolvers).Select(r => Resolve(args, r))
.FirstOrDefault(i => i is not null);
@@ -492,7 +491,7 @@ namespace Emby.Server.Implementations.Library
return item;
}
- private BaseItem Resolve(ItemResolveArgs args, IItemResolver resolver)
+ private BaseItem? Resolve(ItemResolveArgs args, IItemResolver resolver)
{
try
{
@@ -534,16 +533,16 @@ namespace Emby.Server.Implementations.Library
return key.GetMD5();
}
- public BaseItem ResolvePath(FileSystemMetadata fileInfo, Folder parent = null, IDirectoryService directoryService = null)
+ public BaseItem? ResolvePath(FileSystemMetadata fileInfo, Folder? parent = null, IDirectoryService? directoryService = null)
=> ResolvePath(fileInfo, directoryService ?? new DirectoryService(_fileSystem), null, parent);
- private BaseItem ResolvePath(
+ private BaseItem? ResolvePath(
FileSystemMetadata fileInfo,
IDirectoryService directoryService,
- IItemResolver[] resolvers,
- Folder parent = null,
+ IItemResolver[]? resolvers,
+ Folder? parent = null,
CollectionType? collectionType = null,
- LibraryOptions libraryOptions = null)
+ LibraryOptions? libraryOptions = null)
{
ArgumentNullException.ThrowIfNull(fileInfo);
@@ -616,7 +615,7 @@ namespace Emby.Server.Implementations.Library
return ResolveItem(args, resolvers);
}
- public bool IgnoreFile(FileSystemMetadata file, BaseItem parent)
+ public bool IgnoreFile(FileSystemMetadata file, BaseItem? parent)
=> EntityResolutionIgnoreRules.Any(r => r.ShouldIgnore(file, parent));
public List<FileSystemMetadata> NormalizeRootPathList(IEnumerable<FileSystemMetadata> paths)
@@ -691,16 +690,16 @@ namespace Emby.Server.Implementations.Library
private IEnumerable<BaseItem> ResolveFileList(
IReadOnlyList<FileSystemMetadata> fileList,
IDirectoryService directoryService,
- Folder parent,
+ Folder? parent,
CollectionType? collectionType,
- IItemResolver[] resolvers,
+ IItemResolver[]? resolvers,
LibraryOptions libraryOptions)
{
// Given that fileList is a list we can save enumerator allocations by indexing
for (var i = 0; i < fileList.Count; i++)
{
var file = fileList[i];
- BaseItem result = null;
+ BaseItem? result = null;
try
{
result = ResolvePath(file, directoryService, resolvers, parent, collectionType, libraryOptions);
@@ -729,7 +728,7 @@ namespace Emby.Server.Implementations.Library
Directory.CreateDirectory(rootFolderPath);
var rootFolder = GetItemById(GetNewItemId(rootFolderPath, typeof(AggregateFolder))) as AggregateFolder ??
- ((Folder)ResolvePath(_fileSystem.GetDirectoryInfo(rootFolderPath)))
+ (ResolvePath(_fileSystem.GetDirectoryInfo(rootFolderPath)) as Folder ?? throw new InvalidOperationException("Something went very wong"))
.DeepCopy<Folder, AggregateFolder>();
// In case program data folder was moved
@@ -795,7 +794,7 @@ namespace Emby.Server.Implementations.Library
Directory.CreateDirectory(userRootPath);
var newItemId = GetNewItemId(userRootPath, typeof(UserRootFolder));
- UserRootFolder tmpItem = null;
+ UserRootFolder? tmpItem = null;
try
{
tmpItem = GetItemById(newItemId) as UserRootFolder;
@@ -808,7 +807,8 @@ namespace Emby.Server.Implementations.Library
if (tmpItem is null)
{
_logger.LogDebug("Creating new userRootFolder with DeepCopy");
- tmpItem = ((Folder)ResolvePath(_fileSystem.GetDirectoryInfo(userRootPath))).DeepCopy<Folder, UserRootFolder>();
+ tmpItem = (ResolvePath(_fileSystem.GetDirectoryInfo(userRootPath)) as Folder ?? throw new InvalidOperationException("Failed to get user root path"))
+ .DeepCopy<Folder, UserRootFolder>();
}
// In case program data folder was moved
@@ -827,7 +827,8 @@ namespace Emby.Server.Implementations.Library
return _userRootFolder;
}
- public BaseItem FindByPath(string path, bool? isFolder)
+ /// <inheritdoc />
+ public BaseItem? FindByPath(string path, bool? isFolder)
{
// If this returns multiple items it could be tricky figuring out which one is correct.
// In most cases, the newest one will be and the others obsolete but not yet cleaned up
@@ -846,12 +847,8 @@ namespace Emby.Server.Implementations.Library
.FirstOrDefault();
}
- /// <summary>
- /// Gets the person.
- /// </summary>
- /// <param name="name">The name.</param>
- /// <returns>Task{Person}.</returns>
- public Person GetPerson(string name)
+ /// <inheritdoc />
+ public Person? GetPerson(string name)
{
var path = Person.GetPath(name);
var id = GetItemByNameId<Person>(path);
@@ -1159,7 +1156,7 @@ namespace Emby.Server.Implementations.Library
.ToList();
}
- private VirtualFolderInfo GetVirtualFolderInfo(string dir, List<BaseItem> allCollectionFolders, HashSet<Guid> refreshQueue)
+ private VirtualFolderInfo GetVirtualFolderInfo(string dir, List<BaseItem> allCollectionFolders, HashSet<Guid>? refreshQueue)
{
var info = new VirtualFolderInfo
{
@@ -1223,20 +1220,15 @@ namespace Emby.Server.Implementations.Library
return null;
}
- /// <summary>
- /// Gets the item by id.
- /// </summary>
- /// <param name="id">The id.</param>
- /// <returns>BaseItem.</returns>
- /// <exception cref="ArgumentNullException"><paramref name="id"/> is <c>null</c>.</exception>
- public BaseItem GetItemById(Guid id)
+ /// <inheritdoc />
+ public BaseItem? GetItemById(Guid id)
{
if (id.IsEmpty())
{
throw new ArgumentException("Guid can't be empty", nameof(id));
}
- if (_cache.TryGetValue(id, out BaseItem item))
+ if (_cache.TryGetValue(id, out BaseItem? item))
{
return item;
}
@@ -1252,7 +1244,7 @@ namespace Emby.Server.Implementations.Library
}
/// <inheritdoc />
- public T GetItemById<T>(Guid id)
+ public T? GetItemById<T>(Guid id)
where T : BaseItem
{
var item = GetItemById(id);
@@ -1264,6 +1256,22 @@ namespace Emby.Server.Implementations.Library
return null;
}
+ /// <inheritdoc />
+ public T? GetItemById<T>(Guid id, Guid userId)
+ where T : BaseItem
+ {
+ var user = userId.IsEmpty() ? null : _userManager.GetUserById(userId);
+ return GetItemById<T>(id, user);
+ }
+
+ /// <inheritdoc />
+ public T? GetItemById<T>(Guid id, User? user)
+ where T : BaseItem
+ {
+ var item = GetItemById<T>(id);
+ return ItemIsVisible(item, user) ? item : null;
+ }
+
public List<BaseItem> GetItemList(InternalItemsQuery query, bool allowExternalContent)
{
if (query.Recursive && !query.ParentId.IsEmpty())
@@ -1424,7 +1432,7 @@ namespace Emby.Server.Implementations.Library
var parents = new BaseItem[len];
for (int i = 0; i < len; i++)
{
- parents[i] = GetItemById(ancestorIds[i]);
+ parents[i] = GetItemById(ancestorIds[i]) ?? throw new ArgumentException($"Failed to find parent with id: {ancestorIds[i]}");
if (parents[i] is not (ICollectionFolder or UserView))
{
return;
@@ -1438,7 +1446,7 @@ namespace Emby.Server.Implementations.Library
// Prevent searching in all libraries due to empty filter
if (query.TopParentIds.Length == 0)
{
- query.TopParentIds = new[] { Guid.NewGuid() };
+ query.TopParentIds = [Guid.NewGuid()];
}
}
@@ -1535,7 +1543,7 @@ namespace Emby.Server.Implementations.Library
}
}
- private IEnumerable<Guid> GetTopParentIdsForQuery(BaseItem item, User user)
+ private IEnumerable<Guid> GetTopParentIdsForQuery(BaseItem item, User? user)
{
if (item is UserView view)
{
@@ -1613,7 +1621,7 @@ namespace Emby.Server.Implementations.Library
return items
.SelectMany(i => i.ToArray())
.Select(ResolveIntro)
- .Where(i => i is not null);
+ .Where(i => i is not null)!; // null values got filtered out
}
/// <summary>
@@ -1642,9 +1650,9 @@ namespace Emby.Server.Implementations.Library
/// </summary>
/// <param name="info">The info.</param>
/// <returns>Video.</returns>
- private Video ResolveIntro(IntroInfo info)
+ private Video? ResolveIntro(IntroInfo info)
{
- Video video = null;
+ Video? video = null;
if (info.ItemId.HasValue)
{
@@ -1695,29 +1703,26 @@ namespace Emby.Server.Implementations.Library
return video;
}
- /// <summary>
- /// Sorts the specified sort by.
- /// </summary>
- /// <param name="items">The items.</param>
- /// <param name="user">The user.</param>
- /// <param name="sortBy">The sort by.</param>
- /// <param name="sortOrder">The sort order.</param>
- /// <returns>IEnumerable{BaseItem}.</returns>
- public IEnumerable<BaseItem> Sort(IEnumerable<BaseItem> items, User user, IEnumerable<ItemSortBy> sortBy, SortOrder sortOrder)
+ /// <inheritdoc />
+ public IEnumerable<BaseItem> Sort(IEnumerable<BaseItem> items, User? user, IEnumerable<ItemSortBy> sortBy, SortOrder sortOrder)
{
var isFirst = true;
- IOrderedEnumerable<BaseItem> orderedItems = null;
+ IOrderedEnumerable<BaseItem>? orderedItems = null;
foreach (var orderBy in sortBy.Select(o => GetComparer(o, user)).Where(c => c is not null))
{
if (isFirst)
{
- orderedItems = sortOrder == SortOrder.Descending ? items.OrderByDescending(i => i, orderBy) : items.OrderBy(i => i, orderBy);
+ orderedItems = sortOrder == SortOrder.Descending
+ ? items.OrderByDescending(i => i, orderBy)
+ : items.OrderBy(i => i, orderBy);
}
else
{
- orderedItems = sortOrder == SortOrder.Descending ? orderedItems.ThenByDescending(i => i, orderBy) : orderedItems.ThenBy(i => i, orderBy);
+ orderedItems = sortOrder == SortOrder.Descending
+ ? orderedItems!.ThenByDescending(i => i, orderBy)
+ : orderedItems!.ThenBy(i => i, orderBy); // orderedItems is set during the first iteration
}
isFirst = false;
@@ -1726,11 +1731,12 @@ namespace Emby.Server.Implementations.Library
return orderedItems ?? items;
}
- public IEnumerable<BaseItem> Sort(IEnumerable<BaseItem> items, User user, IEnumerable<(ItemSortBy OrderBy, SortOrder SortOrder)> orderBy)
+ /// <inheritdoc />
+ public IEnumerable<BaseItem> Sort(IEnumerable<BaseItem> items, User? user, IEnumerable<(ItemSortBy OrderBy, SortOrder SortOrder)> orderBy)
{
var isFirst = true;
- IOrderedEnumerable<BaseItem> orderedItems = null;
+ IOrderedEnumerable<BaseItem>? orderedItems = null;
foreach (var (name, sortOrder) in orderBy)
{
@@ -1742,11 +1748,15 @@ namespace Emby.Server.Implementations.Library
if (isFirst)
{
- orderedItems = sortOrder == SortOrder.Descending ? items.OrderByDescending(i => i, comparer) : items.OrderBy(i => i, comparer);
+ orderedItems = sortOrder == SortOrder.Descending
+ ? items.OrderByDescending(i => i, comparer)
+ : items.OrderBy(i => i, comparer);
}
else
{
- orderedItems = sortOrder == SortOrder.Descending ? orderedItems.ThenByDescending(i => i, comparer) : orderedItems.ThenBy(i => i, comparer);
+ orderedItems = sortOrder == SortOrder.Descending
+ ? orderedItems!.ThenByDescending(i => i, comparer)
+ : orderedItems!.ThenBy(i => i, comparer); // orderedItems is set during the first iteration
}
isFirst = false;
@@ -1761,14 +1771,14 @@ namespace Emby.Server.Implementations.Library
/// <param name="name">The name.</param>
/// <param name="user">The user.</param>
/// <returns>IBaseItemComparer.</returns>
- private IBaseItemComparer GetComparer(ItemSortBy name, User user)
+ private IBaseItemComparer? GetComparer(ItemSortBy name, User? user)
{
var comparer = Comparers.FirstOrDefault(c => name == c.Type);
// If it requires a user, create a new one, and assign the user
if (comparer is IUserBaseItemComparer)
{
- var userComparer = (IUserBaseItemComparer)Activator.CreateInstance(comparer.GetType());
+ var userComparer = (IUserBaseItemComparer)Activator.CreateInstance(comparer.GetType())!; // only null for Nullable<T> instances
userComparer.User = user;
userComparer.UserManager = _userManager;
@@ -1780,23 +1790,14 @@ namespace Emby.Server.Implementations.Library
return comparer;
}
- /// <summary>
- /// Creates the item.
- /// </summary>
- /// <param name="item">The item.</param>
- /// <param name="parent">The parent item.</param>
- public void CreateItem(BaseItem item, BaseItem parent)
+ /// <inheritdoc />
+ public void CreateItem(BaseItem item, BaseItem? parent)
{
CreateItems(new[] { item }, parent, CancellationToken.None);
}
- /// <summary>
- /// Creates the items.
- /// </summary>
- /// <param name="items">The items.</param>
- /// <param name="parent">The parent item.</param>
- /// <param name="cancellationToken">The cancellation token.</param>
- public void CreateItems(IReadOnlyList<BaseItem> items, BaseItem parent, CancellationToken cancellationToken)
+ /// <inheritdoc />
+ public void CreateItems(IReadOnlyList<BaseItem> items, BaseItem? parent, CancellationToken cancellationToken)
{
_itemRepository.SaveItems(items, cancellationToken);
@@ -2078,16 +2079,16 @@ namespace Emby.Server.Implementations.Library
public LibraryOptions GetLibraryOptions(BaseItem item)
{
- if (item is not CollectionFolder collectionFolder)
+ if (item is CollectionFolder collectionFolder)
{
- // List.Find is more performant than FirstOrDefault due to enumerator allocation
- collectionFolder = GetCollectionFolders(item)
- .Find(folder => folder is CollectionFolder) as CollectionFolder;
+ return collectionFolder.GetLibraryOptions();
}
- return collectionFolder is null
- ? new LibraryOptions()
- : collectionFolder.GetLibraryOptions();
+ // List.Find is more performant than FirstOrDefault due to enumerator allocation
+ return GetCollectionFolders(item)
+ .Find(folder => folder is CollectionFolder) is CollectionFolder collectionFolder2
+ ? collectionFolder2.GetLibraryOptions()
+ : new LibraryOptions();
}
public CollectionType? GetContentType(BaseItem item)
@@ -2441,7 +2442,7 @@ namespace Emby.Server.Implementations.Library
{
if (parentId.HasValue)
{
- return GetItemById(parentId.Value);
+ return GetItemById(parentId.Value) ?? throw new ArgumentException($"Invalid parent id: {parentId.Value}");
}
if (!userId.IsNullOrEmpty())
@@ -2478,7 +2479,7 @@ namespace Emby.Server.Implementations.Library
var isFolder = episode.VideoType == VideoType.BluRay || episode.VideoType == VideoType.Dvd;
// TODO nullable - what are we trying to do there with empty episodeInfo?
- EpisodeInfo episodeInfo = null;
+ EpisodeInfo? episodeInfo = null;
if (episode.IsFileProtocol)
{
episodeInfo = resolver.Resolve(episode.Path, isFolder, null, null, isAbsoluteNaming);
@@ -2681,7 +2682,7 @@ namespace Emby.Server.Implementations.Library
}
}
- BaseItem GetExtra(FileSystemMetadata file, ExtraType extraType)
+ BaseItem? GetExtra(FileSystemMetadata file, ExtraType extraType)
{
var extra = ResolvePath(_fileSystem.GetFileInfo(file.FullName), directoryService, _extraResolver.GetResolversForExtraType(extraType));
if (extra is not Video && extra is not Audio)
@@ -2708,9 +2709,9 @@ namespace Emby.Server.Implementations.Library
}
}
- public string GetPathAfterNetworkSubstitution(string path, BaseItem ownerItem)
+ public string GetPathAfterNetworkSubstitution(string path, BaseItem? ownerItem)
{
- string newPath;
+ string? newPath;
if (ownerItem is not null)
{
var libraryOptions = GetLibraryOptions(ownerItem);
@@ -2784,8 +2785,8 @@ namespace Emby.Server.Implementations.Library
}
})
.Where(i => i is not null)
- .Where(i => query.User is null || i.IsVisible(query.User))
- .ToList();
+ .Where(i => query.User is null || i!.IsVisible(query.User))
+ .ToList()!; // null values are filtered out
}
public List<string> GetPeopleNames(InternalPeopleQuery query)
@@ -2887,7 +2888,7 @@ namespace Emby.Server.Implementations.Library
if (collectionType is not null)
{
- var path = Path.Combine(virtualFolderPath, collectionType.ToString().ToLowerInvariant() + ".collection");
+ var path = Path.Combine(virtualFolderPath, collectionType.ToString()!.ToLowerInvariant() + ".collection"); // Can't be null with legal values?
await File.WriteAllBytesAsync(path, Array.Empty<byte>()).ConfigureAwait(false);
}
@@ -2921,7 +2922,7 @@ namespace Emby.Server.Implementations.Library
private async Task SavePeopleMetadataAsync(IEnumerable<PersonInfo> people, CancellationToken cancellationToken)
{
- List<BaseItem> personsToSave = null;
+ List<BaseItem>? personsToSave = null;
foreach (var person in people)
{
@@ -3139,7 +3140,7 @@ namespace Emby.Server.Implementations.Library
throw new ArgumentNullException(nameof(path));
}
- List<NameValuePair> removeList = null;
+ List<NameValuePair>? removeList = null;
foreach (var contentType in _configurationManager.Configuration.ContentTypes)
{
@@ -3192,5 +3193,20 @@ namespace Emby.Server.Implementations.Library
CollectionFolder.SaveLibraryOptions(virtualFolderPath, libraryOptions);
}
+
+ private static bool ItemIsVisible(BaseItem? item, User? user)
+ {
+ if (item is null)
+ {
+ return false;
+ }
+
+ if (user is null)
+ {
+ return true;
+ }
+
+ return item is UserRootFolder || item.IsVisibleStandalone(user);
+ }
}
}
diff --git a/Emby.Server.Implementations/Library/PathExtensions.cs b/Emby.Server.Implementations/Library/PathExtensions.cs
index c4b6b3756..21e7079d8 100644
--- a/Emby.Server.Implementations/Library/PathExtensions.cs
+++ b/Emby.Server.Implementations/Library/PathExtensions.cs
@@ -31,8 +31,9 @@ namespace Emby.Server.Implementations.Library
var attributeIndex = str.IndexOf(attribute, StringComparison.OrdinalIgnoreCase);
- // Must be at least 3 characters after the attribute =, ], any character.
- var maxIndex = str.Length - attribute.Length - 3;
+ // Must be at least 3 characters after the attribute =, ], any character,
+ // then we offset it by 1, because we want the index and not length.
+ var maxIndex = str.Length - attribute.Length - 2;
while (attributeIndex > -1 && attributeIndex < maxIndex)
{
var attributeEnd = attributeIndex + attribute.Length;
diff --git a/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs b/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs
index 601aab5b9..725b8f76c 100644
--- a/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs
+++ b/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs
@@ -64,6 +64,11 @@ namespace Emby.Server.Implementations.Library.Validators
try
{
var item = _libraryManager.GetPerson(person);
+ if (item is null)
+ {
+ _logger.LogWarning("Failed to get person: {Name}", person);
+ continue;
+ }
var options = new MetadataRefreshOptions(new DirectoryService(_fileSystem))
{
@@ -92,7 +97,7 @@ namespace Emby.Server.Implementations.Library.Validators
var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery
{
- IncludeItemTypes = new[] { BaseItemKind.Person },
+ IncludeItemTypes = [BaseItemKind.Person],
IsDeadPerson = true,
IsLocked = false
});
diff --git a/Emby.Server.Implementations/Localization/Core/fa.json b/Emby.Server.Implementations/Localization/Core/fa.json
index 8364ce236..ce5177d1f 100644
--- a/Emby.Server.Implementations/Localization/Core/fa.json
+++ b/Emby.Server.Implementations/Localization/Core/fa.json
@@ -126,5 +126,7 @@
"External": "خارجی",
"HearingImpaired": "مشکل شنوایی",
"TaskRefreshTrickplayImages": "تولید تصاویر Trickplay",
- "TaskRefreshTrickplayImagesDescription": "تولید پیش‌نمایش های trickplay برای ویدیو های فعال شده در کتابخانه."
+ "TaskRefreshTrickplayImagesDescription": "تولید پیش‌نمایش های trickplay برای ویدیو های فعال شده در کتابخانه.",
+ "TaskCleanCollectionsAndPlaylists": "پاکسازی مجموعه ها و لیست پخش",
+ "TaskCleanCollectionsAndPlaylistsDescription": "موارد را از مجموعه ها و لیست پخش هایی که دیگر وجود ندارند حذف میکند."
}
diff --git a/Emby.Server.Implementations/Localization/Core/my.json b/Emby.Server.Implementations/Localization/Core/my.json
index 198f7540c..4cb4cdc75 100644
--- a/Emby.Server.Implementations/Localization/Core/my.json
+++ b/Emby.Server.Implementations/Localization/Core/my.json
@@ -48,7 +48,7 @@
"Undefined": "သတ်မှတ်မထားသော",
"TvShows": "တီဗီ ဇာတ်လမ်းတွဲများ",
"System": "စနစ်",
- "Sync": "ထပ်တူကျသည်။",
+ "Sync": "ချိန်ကိုက်မည်",
"SubtitleDownloadFailureFromForItem": "{1} အတွက် {0} မှ စာတန်းထိုးများ ဒေါင်းလုဒ်လုပ်ခြင်း မအောင်မြင်ပါ",
"StartupEmbyServerIsLoading": "Jellyfin ဆာဗာကို အသင့်ပြင်နေပါသည်။ ခဏနေ ထပ်စမ်းကြည့်ပါ။",
"Songs": "သီချင်းများ",
@@ -104,7 +104,7 @@
"HeaderFavoriteSongs": "အကြိုက်ဆုံးသီချင်းများ",
"HeaderFavoriteShows": "အကြိုက်ဆုံး ဇာတ်လမ်းတွဲများ",
"HeaderFavoriteEpisodes": "အကြိုက်ဆုံး ဇာတ်လမ်းအပိုင်းများ",
- "HeaderFavoriteArtists": "အကြိုက်ဆုံးအနုပညာရှင်များ",
+ "HeaderFavoriteArtists": "အကြိုက်ဆုံး အနုပညာရှင်များ",
"HeaderFavoriteAlbums": "အကြိုက်ဆုံး အယ်လ်ဘမ်များ",
"HeaderContinueWatching": "ဆက်လက်ကြည့်ရှုပါ",
"HeaderAlbumArtists": "အယ်လ်ဘမ်အနုပညာရှင်များ",
@@ -120,5 +120,11 @@
"AuthenticationSucceededWithUserName": "{0} အောင်မြင်စွာ စစ်မှန်ကြောင်း အတည်ပြုပြီးပါပြီ",
"Application": "အပလီကေးရှင်း",
"AppDeviceValues": "အက်ပ်- {0}၊ စက်- {1}",
- "External": "ပြင်ပ"
+ "External": "ပြင်ပ",
+ "TaskKeyframeExtractorDescription": "ပိုမိုတိကျသည့် အိတ်ချ်အယ်လ်အက်စ် အစဉ်လိုက်ပြသမှုများ ဖန်တီးနိုင်ရန်အတွက် ဗီဒီယိုဖိုင်များမှ ကီးဖရိန်များကို ထုတ်နှုတ်ယူမည် ဖြစ်သည်။ ဤလုပ်ဆောင်မှုသည် အချိန်ကြာရှည်နိုင်သည်။",
+ "TaskCleanCollectionsAndPlaylistsDescription": "စုစည်းမှုများနှင့် အစဉ်လိုက်ပြသမှုများမှ မရှိတော့သည်များကို ဖယ်ရှားမည်။",
+ "TaskRefreshTrickplayImages": "ထရစ်ခ်ပလေး ပုံများကို ထုတ်မည်",
+ "TaskKeyframeExtractor": "ကီးဖရိန်များကို ထုတ်နုတ်ခြင်း",
+ "TaskCleanCollectionsAndPlaylists": "စုစည်းမှုများနှင့် အစဉ်လိုက်ပြသမှုများကို ရှင်းလင်းမည်",
+ "HearingImpaired": "အကြားအာရုံ ချို့တဲ့သူ"
}
diff --git a/Emby.Server.Implementations/Localization/LocalizationManager.cs b/Emby.Server.Implementations/Localization/LocalizationManager.cs
index 16776b6bd..bae201c70 100644
--- a/Emby.Server.Implementations/Localization/LocalizationManager.cs
+++ b/Emby.Server.Implementations/Localization/LocalizationManager.cs
@@ -278,6 +278,13 @@ namespace Emby.Server.Implementations.Localization
return null;
}
+ // Convert integers directly
+ // This may override some of the locale specific age ratings (but those always map to the same age)
+ if (int.TryParse(rating, out var ratingAge))
+ {
+ return ratingAge;
+ }
+
// Fairly common for some users to have "Rated R" in their rating field
rating = rating.Replace("Rated :", string.Empty, StringComparison.OrdinalIgnoreCase);
rating = rating.Replace("Rated ", string.Empty, StringComparison.OrdinalIgnoreCase);
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionAndPlaylistPathsTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionAndPlaylistPathsTask.cs
index 812df8192..f1c4d9b47 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionAndPlaylistPathsTask.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionAndPlaylistPathsTask.cs
@@ -116,7 +116,7 @@ public class CleanupCollectionAndPlaylistPathsTask : IScheduledTask
foreach (var linkedChild in folder.LinkedChildren)
{
var path = linkedChild.Path;
- if (!File.Exists(path))
+ if (Path.HasExtension(path) ? !File.Exists(path) : !Directory.Exists(path))
{
_logger.LogInformation("Item in {FolderName} cannot be found at {ItemPath}", folder.Name, path);
(itemsToRemove ??= new List<LinkedChild>()).Add(linkedChild);
diff --git a/Emby.Server.Implementations/Serialization/MyXmlSerializer.cs b/Emby.Server.Implementations/Serialization/MyXmlSerializer.cs
index 1bac2600c..aa5fbbdf7 100644
--- a/Emby.Server.Implementations/Serialization/MyXmlSerializer.cs
+++ b/Emby.Server.Implementations/Serialization/MyXmlSerializer.cs
@@ -15,10 +15,9 @@ namespace Emby.Server.Implementations.Serialization
{
// Need to cache these
// http://dotnetcodebox.blogspot.com/2013/01/xmlserializer-class-may-result-in.html
- private static readonly ConcurrentDictionary<string, XmlSerializer> _serializers =
- new ConcurrentDictionary<string, XmlSerializer>();
+ private readonly ConcurrentDictionary<string, XmlSerializer> _serializers = new();
- private static XmlSerializer GetSerializer(Type type)
+ private XmlSerializer GetSerializer(Type type)
=> _serializers.GetOrAdd(
type.FullName ?? throw new ArgumentException($"Invalid type {type}."),
static (_, t) => new XmlSerializer(t),
diff --git a/Emby.Server.Implementations/Session/SessionWebSocketListener.cs b/Emby.Server.Implementations/Session/SessionWebSocketListener.cs
index b3c93a904..aba51de8f 100644
--- a/Emby.Server.Implementations/Session/SessionWebSocketListener.cs
+++ b/Emby.Server.Implementations/Session/SessionWebSocketListener.cs
@@ -34,11 +34,6 @@ namespace Emby.Server.Implementations.Session
private const float ForceKeepAliveFactor = 0.75f;
/// <summary>
- /// Lock used for accessing the KeepAlive cancellation token.
- /// </summary>
- private readonly object _keepAliveLock = new object();
-
- /// <summary>
/// The WebSocket watchlist.
/// </summary>
private readonly HashSet<IWebSocketConnection> _webSockets = new HashSet<IWebSocketConnection>();
@@ -55,7 +50,7 @@ namespace Emby.Server.Implementations.Session
/// <summary>
/// The KeepAlive cancellation token.
/// </summary>
- private CancellationTokenSource? _keepAliveCancellationToken;
+ private System.Timers.Timer _keepAlive;
/// <summary>
/// Initializes a new instance of the <see cref="SessionWebSocketListener" /> class.
@@ -71,12 +66,34 @@ namespace Emby.Server.Implementations.Session
_logger = logger;
_sessionManager = sessionManager;
_loggerFactory = loggerFactory;
+ _keepAlive = new System.Timers.Timer(TimeSpan.FromSeconds(WebSocketLostTimeout * IntervalFactor))
+ {
+ AutoReset = true,
+ Enabled = false
+ };
+ _keepAlive.Elapsed += KeepAliveSockets;
}
/// <inheritdoc />
public void Dispose()
{
- StopKeepAlive();
+ if (_keepAlive is not null)
+ {
+ _keepAlive.Stop();
+ _keepAlive.Elapsed -= KeepAliveSockets;
+ _keepAlive.Dispose();
+ _keepAlive = null!;
+ }
+
+ lock (_webSocketsLock)
+ {
+ foreach (var webSocket in _webSockets)
+ {
+ webSocket.Closed -= OnWebSocketClosed;
+ }
+
+ _webSockets.Clear();
+ }
}
/// <summary>
@@ -164,7 +181,7 @@ namespace Emby.Server.Implementations.Session
webSocket.Closed += OnWebSocketClosed;
webSocket.LastKeepAliveDate = DateTime.UtcNow;
- StartKeepAlive();
+ _keepAlive.Start();
}
// Notify WebSocket about timeout
@@ -186,66 +203,26 @@ namespace Emby.Server.Implementations.Session
{
lock (_webSocketsLock)
{
- if (!_webSockets.Remove(webSocket))
- {
- _logger.LogWarning("WebSocket {0} not on watchlist.", webSocket);
- }
- else
+ if (_webSockets.Remove(webSocket))
{
webSocket.Closed -= OnWebSocketClosed;
}
- }
- }
-
- /// <summary>
- /// Starts the KeepAlive watcher.
- /// </summary>
- private void StartKeepAlive()
- {
- lock (_keepAliveLock)
- {
- if (_keepAliveCancellationToken is null)
- {
- _keepAliveCancellationToken = new CancellationTokenSource();
- // Start KeepAlive watcher
- _ = RepeatAsyncCallbackEvery(
- KeepAliveSockets,
- TimeSpan.FromSeconds(WebSocketLostTimeout * IntervalFactor),
- _keepAliveCancellationToken.Token);
- }
- }
- }
-
- /// <summary>
- /// Stops the KeepAlive watcher.
- /// </summary>
- private void StopKeepAlive()
- {
- lock (_keepAliveLock)
- {
- if (_keepAliveCancellationToken is not null)
+ else
{
- _keepAliveCancellationToken.Cancel();
- _keepAliveCancellationToken.Dispose();
- _keepAliveCancellationToken = null;
+ _logger.LogWarning("WebSocket {0} not on watchlist.", webSocket);
}
- }
- lock (_webSocketsLock)
- {
- foreach (var webSocket in _webSockets)
+ if (_webSockets.Count == 0)
{
- webSocket.Closed -= OnWebSocketClosed;
+ _keepAlive.Stop();
}
-
- _webSockets.Clear();
}
}
/// <summary>
/// Checks status of KeepAlive of WebSockets.
/// </summary>
- private async Task KeepAliveSockets()
+ private async void KeepAliveSockets(object? o, EventArgs? e)
{
List<IWebSocketConnection> inactive;
List<IWebSocketConnection> lost;
@@ -291,11 +268,6 @@ namespace Emby.Server.Implementations.Session
RemoveWebSocket(webSocket);
}
}
-
- if (_webSockets.Count == 0)
- {
- StopKeepAlive();
- }
}
}
@@ -310,29 +282,5 @@ namespace Emby.Server.Implementations.Session
new ForceKeepAliveMessage(WebSocketLostTimeout),
CancellationToken.None);
}
-
- /// <summary>
- /// Runs a given async callback once every specified interval time, until cancelled.
- /// </summary>
- /// <param name="callback">The async callback.</param>
- /// <param name="interval">The interval time.</param>
- /// <param name="cancellationToken">The cancellation token.</param>
- /// <returns>Task.</returns>
- private async Task RepeatAsyncCallbackEvery(Func<Task> callback, TimeSpan interval, CancellationToken cancellationToken)
- {
- while (!cancellationToken.IsCancellationRequested)
- {
- await callback().ConfigureAwait(false);
-
- try
- {
- await Task.Delay(interval, cancellationToken).ConfigureAwait(false);
- }
- catch (TaskCanceledException)
- {
- return;
- }
- }
- }
}
}
diff --git a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
index 1cad66326..6d94d96f3 100644
--- a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
+++ b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
@@ -194,7 +194,7 @@ public class DisplayPreferencesController : BaseJellyfinApiController
foreach (var key in displayPreferences.CustomPrefs.Keys.Where(key => key.StartsWith("landing-", StringComparison.OrdinalIgnoreCase)))
{
- if (!Enum.TryParse<ViewType>(displayPreferences.CustomPrefs[key], true, out var type))
+ if (!Enum.TryParse<ViewType>(displayPreferences.CustomPrefs[key], true, out _))
{
_logger.LogError("Invalid ViewType: {LandingScreenOption}", displayPreferences.CustomPrefs[key]);
displayPreferences.CustomPrefs.Remove(key);
diff --git a/Jellyfin.Api/Controllers/FilterController.cs b/Jellyfin.Api/Controllers/FilterController.cs
index d6e043e6a..4abca3271 100644
--- a/Jellyfin.Api/Controllers/FilterController.cs
+++ b/Jellyfin.Api/Controllers/FilterController.cs
@@ -162,7 +162,7 @@ public class FilterController : BaseJellyfinApiController
}
else if (parentId.HasValue)
{
- parentItem = _libraryManager.GetItemById(parentId.Value);
+ parentItem = _libraryManager.GetItemById<BaseItem>(parentId.Value);
}
var filters = new QueryFilters();
diff --git a/Jellyfin.Api/Controllers/ImageController.cs b/Jellyfin.Api/Controllers/ImageController.cs
index 6b38fa7d3..8e8accab3 100644
--- a/Jellyfin.Api/Controllers/ImageController.cs
+++ b/Jellyfin.Api/Controllers/ImageController.cs
@@ -90,6 +90,7 @@ public class ImageController : BaseJellyfinApiController
/// <param name="userId">User Id.</param>
/// <response code="204">Image updated.</response>
/// <response code="403">User does not have permission to delete the image.</response>
+ /// <response code="404">Item not found.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpPost("UserImage")]
[Authorize]
@@ -97,6 +98,7 @@ public class ImageController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult> PostUserImage(
[FromQuery] Guid? userId)
{
@@ -289,7 +291,7 @@ public class ImageController : BaseJellyfinApiController
[FromRoute, Required] ImageType imageType,
[FromQuery] int? imageIndex)
{
- var item = _libraryManager.GetItemById(itemId);
+ var item = _libraryManager.GetItemById<BaseItem>(itemId, User.GetUserId());
if (item is null)
{
return NotFound();
@@ -317,7 +319,7 @@ public class ImageController : BaseJellyfinApiController
[FromRoute, Required] ImageType imageType,
[FromRoute] int imageIndex)
{
- var item = _libraryManager.GetItemById(itemId);
+ var item = _libraryManager.GetItemById<BaseItem>(itemId, User.GetUserId());
if (item is null)
{
return NotFound();
@@ -346,7 +348,7 @@ public class ImageController : BaseJellyfinApiController
[FromRoute, Required] Guid itemId,
[FromRoute, Required] ImageType imageType)
{
- var item = _libraryManager.GetItemById(itemId);
+ var item = _libraryManager.GetItemById<BaseItem>(itemId, User.GetUserId());
if (item is null)
{
return NotFound();
@@ -390,7 +392,7 @@ public class ImageController : BaseJellyfinApiController
[FromRoute, Required] ImageType imageType,
[FromRoute] int imageIndex)
{
- var item = _libraryManager.GetItemById(itemId);
+ var item = _libraryManager.GetItemById<BaseItem>(itemId, User.GetUserId());
if (item is null)
{
return NotFound();
@@ -433,7 +435,7 @@ public class ImageController : BaseJellyfinApiController
[FromRoute, Required] int imageIndex,
[FromQuery, Required] int newIndex)
{
- var item = _libraryManager.GetItemById(itemId);
+ var item = _libraryManager.GetItemById<BaseItem>(itemId, User.GetUserId());
if (item is null)
{
return NotFound();
@@ -456,7 +458,7 @@ public class ImageController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<IEnumerable<ImageInfo>>> GetItemImageInfos([FromRoute, Required] Guid itemId)
{
- var item = _libraryManager.GetItemById(itemId);
+ var item = _libraryManager.GetItemById<BaseItem>(itemId, User.GetUserId());
if (item is null)
{
return NotFound();
@@ -559,7 +561,7 @@ public class ImageController : BaseJellyfinApiController
[FromQuery] string? foregroundLayer,
[FromQuery] int? imageIndex)
{
- var item = _libraryManager.GetItemById(itemId);
+ var item = _libraryManager.GetItemById<BaseItem>(itemId, User.GetUserId());
if (item is null)
{
return NotFound();
@@ -637,7 +639,7 @@ public class ImageController : BaseJellyfinApiController
[FromQuery] string? backgroundColor,
[FromQuery] string? foregroundLayer)
{
- var item = _libraryManager.GetItemById(itemId);
+ var item = _libraryManager.GetItemById<BaseItem>(itemId, User.GetUserId());
if (item is null)
{
return NotFound();
@@ -715,7 +717,7 @@ public class ImageController : BaseJellyfinApiController
[FromQuery] string? foregroundLayer,
[FromRoute, Required] int imageIndex)
{
- var item = _libraryManager.GetItemById(itemId);
+ var item = _libraryManager.GetItemById<BaseItem>(itemId, User.GetUserId());
if (item is null)
{
return NotFound();
diff --git a/Jellyfin.Api/Controllers/InstantMixController.cs b/Jellyfin.Api/Controllers/InstantMixController.cs
index 3cf485299..dcbacf1d7 100644
--- a/Jellyfin.Api/Controllers/InstantMixController.cs
+++ b/Jellyfin.Api/Controllers/InstantMixController.cs
@@ -62,9 +62,11 @@ public class InstantMixController : BaseJellyfinApiController
/// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
/// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
/// <response code="200">Instant playlist returned.</response>
+ /// <response code="404">Item not found.</response>
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
[HttpGet("Songs/{itemId}/InstantMix")]
[ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromSong(
[FromRoute, Required] Guid itemId,
[FromQuery] Guid? userId,
@@ -75,11 +77,16 @@ public class InstantMixController : BaseJellyfinApiController
[FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
{
- var item = _libraryManager.GetItemById(itemId);
userId = RequestHelpers.GetUserId(User, userId);
var user = userId.IsNullOrEmpty()
? null
: _userManager.GetUserById(userId.Value);
+ var item = _libraryManager.GetItemById<BaseItem>(itemId, user);
+ if (item is null)
+ {
+ return NotFound();
+ }
+
var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(User)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
@@ -99,9 +106,11 @@ public class InstantMixController : BaseJellyfinApiController
/// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
/// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
/// <response code="200">Instant playlist returned.</response>
+ /// <response code="404">Item not found.</response>
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
[HttpGet("Albums/{itemId}/InstantMix")]
[ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromAlbum(
[FromRoute, Required] Guid itemId,
[FromQuery] Guid? userId,
@@ -112,15 +121,20 @@ public class InstantMixController : BaseJellyfinApiController
[FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
{
- var album = _libraryManager.GetItemById(itemId);
userId = RequestHelpers.GetUserId(User, userId);
var user = userId.IsNullOrEmpty()
? null
: _userManager.GetUserById(userId.Value);
+ var item = _libraryManager.GetItemById<BaseItem>(itemId, user);
+ if (item is null)
+ {
+ return NotFound();
+ }
+
var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(User)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
- var items = _musicManager.GetInstantMixFromItem(album, user, dtoOptions);
+ var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions);
return GetResult(items, user, limit, dtoOptions);
}
@@ -136,9 +150,11 @@ public class InstantMixController : BaseJellyfinApiController
/// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
/// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
/// <response code="200">Instant playlist returned.</response>
+ /// <response code="404">Item not found.</response>
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
[HttpGet("Playlists/{itemId}/InstantMix")]
[ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromPlaylist(
[FromRoute, Required] Guid itemId,
[FromQuery] Guid? userId,
@@ -149,15 +165,20 @@ public class InstantMixController : BaseJellyfinApiController
[FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
{
- var playlist = (Playlist)_libraryManager.GetItemById(itemId);
userId = RequestHelpers.GetUserId(User, userId);
var user = userId.IsNullOrEmpty()
? null
: _userManager.GetUserById(userId.Value);
+ var item = _libraryManager.GetItemById<Playlist>(itemId, user);
+ if (item is null)
+ {
+ return NotFound();
+ }
+
var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(User)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
- var items = _musicManager.GetInstantMixFromItem(playlist, user, dtoOptions);
+ var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions);
return GetResult(items, user, limit, dtoOptions);
}
@@ -209,9 +230,11 @@ public class InstantMixController : BaseJellyfinApiController
/// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
/// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
/// <response code="200">Instant playlist returned.</response>
+ /// <response code="404">Item not found.</response>
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
[HttpGet("Artists/{itemId}/InstantMix")]
[ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromArtists(
[FromRoute, Required] Guid itemId,
[FromQuery] Guid? userId,
@@ -222,11 +245,16 @@ public class InstantMixController : BaseJellyfinApiController
[FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
{
- var item = _libraryManager.GetItemById(itemId);
userId = RequestHelpers.GetUserId(User, userId);
var user = userId.IsNullOrEmpty()
? null
: _userManager.GetUserById(userId.Value);
+ var item = _libraryManager.GetItemById<BaseItem>(itemId, user);
+ if (item is null)
+ {
+ return NotFound();
+ }
+
var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(User)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
@@ -246,9 +274,11 @@ public class InstantMixController : BaseJellyfinApiController
/// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
/// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
/// <response code="200">Instant playlist returned.</response>
+ /// <response code="404">Item not found.</response>
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
[HttpGet("Items/{itemId}/InstantMix")]
[ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromItem(
[FromRoute, Required] Guid itemId,
[FromQuery] Guid? userId,
@@ -259,11 +289,16 @@ public class InstantMixController : BaseJellyfinApiController
[FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
{
- var item = _libraryManager.GetItemById(itemId);
userId = RequestHelpers.GetUserId(User, userId);
var user = userId.IsNullOrEmpty()
? null
: _userManager.GetUserById(userId.Value);
+ var item = _libraryManager.GetItemById<BaseItem>(itemId, user);
+ if (item is null)
+ {
+ return NotFound();
+ }
+
var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(User)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
@@ -283,9 +318,11 @@ public class InstantMixController : BaseJellyfinApiController
/// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
/// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
/// <response code="200">Instant playlist returned.</response>
+ /// <response code="404">Item not found.</response>
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
[HttpGet("Artists/InstantMix")]
[ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
[Obsolete("Use GetInstantMixFromArtists")]
public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromArtists2(
[FromQuery, Required] Guid id,
@@ -320,9 +357,11 @@ public class InstantMixController : BaseJellyfinApiController
/// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
/// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
/// <response code="200">Instant playlist returned.</response>
+ /// <response code="404">Item not found.</response>
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
[HttpGet("MusicGenres/InstantMix")]
[ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromMusicGenreById(
[FromQuery, Required] Guid id,
[FromQuery] Guid? userId,
@@ -333,11 +372,16 @@ public class InstantMixController : BaseJellyfinApiController
[FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
{
- var item = _libraryManager.GetItemById(id);
userId = RequestHelpers.GetUserId(User, userId);
var user = userId.IsNullOrEmpty()
? null
: _userManager.GetUserById(userId.Value);
+ var item = _libraryManager.GetItemById<BaseItem>(id, user);
+ if (item is null)
+ {
+ return NotFound();
+ }
+
var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(User)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
diff --git a/Jellyfin.Api/Controllers/ItemLookupController.cs b/Jellyfin.Api/Controllers/ItemLookupController.cs
index e3aee1bf7..d009f80a9 100644
--- a/Jellyfin.Api/Controllers/ItemLookupController.cs
+++ b/Jellyfin.Api/Controllers/ItemLookupController.cs
@@ -4,6 +4,8 @@ using System.ComponentModel.DataAnnotations;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Constants;
+using Jellyfin.Api.Extensions;
+using Jellyfin.Api.Helpers;
using MediaBrowser.Common.Api;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
@@ -64,7 +66,7 @@ public class ItemLookupController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<IEnumerable<ExternalIdInfo>> GetExternalIdInfos([FromRoute, Required] Guid itemId)
{
- var item = _libraryManager.GetItemById(itemId);
+ var item = _libraryManager.GetItemById<BaseItem>(itemId, User.GetUserId());
if (item is null)
{
return NotFound();
@@ -234,6 +236,7 @@ public class ItemLookupController : BaseJellyfinApiController
/// <param name="searchResult">The remote search result.</param>
/// <param name="replaceAllImages">Optional. Whether or not to replace all images. Default: True.</param>
/// <response code="204">Item metadata refreshed.</response>
+ /// <response code="404">Item not found.</response>
/// <returns>
/// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results.
/// The task result contains an <see cref="NoContentResult"/>.
@@ -241,12 +244,18 @@ public class ItemLookupController : BaseJellyfinApiController
[HttpPost("Items/RemoteSearch/Apply/{itemId}")]
[Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult> ApplySearchCriteria(
[FromRoute, Required] Guid itemId,
[FromBody, Required] RemoteSearchResult searchResult,
[FromQuery] bool replaceAllImages = true)
{
- var item = _libraryManager.GetItemById(itemId);
+ var item = _libraryManager.GetItemById<BaseItem>(itemId, User.GetUserId());
+ if (item is null)
+ {
+ return NotFound();
+ }
+
_logger.LogInformation(
"Setting provider id's to item {ItemId}-{ItemName}: {@ProviderIds}",
item.Id,
diff --git a/Jellyfin.Api/Controllers/ItemRefreshController.cs b/Jellyfin.Api/Controllers/ItemRefreshController.cs
index 0a8522e1c..c1343b130 100644
--- a/Jellyfin.Api/Controllers/ItemRefreshController.cs
+++ b/Jellyfin.Api/Controllers/ItemRefreshController.cs
@@ -2,7 +2,10 @@ using System;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using Jellyfin.Api.Constants;
+using Jellyfin.Api.Extensions;
+using Jellyfin.Api.Helpers;
using MediaBrowser.Common.Api;
+using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.IO;
@@ -61,7 +64,7 @@ public class ItemRefreshController : BaseJellyfinApiController
[FromQuery] bool replaceAllMetadata = false,
[FromQuery] bool replaceAllImages = false)
{
- var item = _libraryManager.GetItemById(itemId);
+ var item = _libraryManager.GetItemById<BaseItem>(itemId, User.GetUserId());
if (item is null)
{
return NotFound();
diff --git a/Jellyfin.Api/Controllers/ItemUpdateController.cs b/Jellyfin.Api/Controllers/ItemUpdateController.cs
index 9800248c6..83f308bb1 100644
--- a/Jellyfin.Api/Controllers/ItemUpdateController.cs
+++ b/Jellyfin.Api/Controllers/ItemUpdateController.cs
@@ -5,6 +5,8 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Constants;
+using Jellyfin.Api.Extensions;
+using Jellyfin.Api.Helpers;
using Jellyfin.Data.Enums;
using MediaBrowser.Common.Api;
using MediaBrowser.Controller.Configuration;
@@ -72,7 +74,7 @@ public class ItemUpdateController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult> UpdateItem([FromRoute, Required] Guid itemId, [FromBody, Required] BaseItemDto request)
{
- var item = _libraryManager.GetItemById(itemId);
+ var item = _libraryManager.GetItemById<BaseItem>(itemId, User.GetUserId());
if (item is null)
{
return NotFound();
@@ -145,7 +147,11 @@ public class ItemUpdateController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<MetadataEditorInfo> GetMetadataEditorInfo([FromRoute, Required] Guid itemId)
{
- var item = _libraryManager.GetItemById(itemId);
+ var item = _libraryManager.GetItemById<BaseItem>(itemId, User.GetUserId());
+ if (item is null)
+ {
+ return NotFound();
+ }
var info = new MetadataEditorInfo
{
@@ -197,7 +203,7 @@ public class ItemUpdateController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult UpdateItemContentType([FromRoute, Required] Guid itemId, [FromQuery] string? contentType)
{
- var item = _libraryManager.GetItemById(itemId);
+ var item = _libraryManager.GetItemById<BaseItem>(itemId, User.GetUserId());
if (item is null)
{
return NotFound();
diff --git a/Jellyfin.Api/Controllers/ItemsController.cs b/Jellyfin.Api/Controllers/ItemsController.cs
index 26ae1a820..6ffe6e7da 100644
--- a/Jellyfin.Api/Controllers/ItemsController.cs
+++ b/Jellyfin.Api/Controllers/ItemsController.cs
@@ -967,9 +967,13 @@ public class ItemsController : BaseJellyfinApiController
}
var user = _userManager.GetUserById(requestUserId) ?? throw new ResourceNotFoundException();
- var item = _libraryManager.GetItemById(itemId);
+ var item = _libraryManager.GetItemById<BaseItem>(itemId, user);
+ if (item is null)
+ {
+ return NotFound();
+ }
- return (item == null) ? NotFound() : _userDataRepository.GetUserDataDto(item, user);
+ return _userDataRepository.GetUserDataDto(item, user);
}
/// <summary>
@@ -1014,8 +1018,8 @@ public class ItemsController : BaseJellyfinApiController
}
var user = _userManager.GetUserById(requestUserId) ?? throw new ResourceNotFoundException();
- var item = _libraryManager.GetItemById(itemId);
- if (item == null)
+ var item = _libraryManager.GetItemById<BaseItem>(itemId, user);
+ if (item is null)
{
return NotFound();
}
diff --git a/Jellyfin.Api/Controllers/LibraryController.cs b/Jellyfin.Api/Controllers/LibraryController.cs
index 360389d29..3b4e80ff3 100644
--- a/Jellyfin.Api/Controllers/LibraryController.cs
+++ b/Jellyfin.Api/Controllers/LibraryController.cs
@@ -102,7 +102,7 @@ public class LibraryController : BaseJellyfinApiController
[ProducesFile("video/*", "audio/*")]
public ActionResult GetFile([FromRoute, Required] Guid itemId)
{
- var item = _libraryManager.GetItemById(itemId);
+ var item = _libraryManager.GetItemById<BaseItem>(itemId, User.GetUserId());
if (item is null)
{
return NotFound();
@@ -152,11 +152,10 @@ public class LibraryController : BaseJellyfinApiController
? (userId.IsNullOrEmpty()
? _libraryManager.RootFolder
: _libraryManager.GetUserRootFolder())
- : _libraryManager.GetItemById(itemId);
-
+ : _libraryManager.GetItemById<BaseItem>(itemId, user);
if (item is null)
{
- return NotFound("Item not found.");
+ return NotFound();
}
IEnumerable<BaseItem> themeItems;
@@ -214,16 +213,14 @@ public class LibraryController : BaseJellyfinApiController
var user = userId.IsNullOrEmpty()
? null
: _userManager.GetUserById(userId.Value);
-
var item = itemId.IsEmpty()
? (userId.IsNullOrEmpty()
? _libraryManager.RootFolder
: _libraryManager.GetUserRootFolder())
- : _libraryManager.GetItemById(itemId);
-
+ : _libraryManager.GetItemById<BaseItem>(itemId, user);
if (item is null)
{
- return NotFound("Item not found.");
+ return NotFound();
}
IEnumerable<BaseItem> themeItems;
@@ -286,7 +283,8 @@ public class LibraryController : BaseJellyfinApiController
userId,
inheritFromParent);
- if (themeSongs.Result is NotFoundObjectResult || themeVideos.Result is NotFoundObjectResult)
+ if (themeSongs.Result is StatusCodeResult { StatusCode: StatusCodes.Status404NotFound }
+ || themeVideos.Result is StatusCodeResult { StatusCode: StatusCodes.Status404NotFound })
{
return NotFound();
}
@@ -327,6 +325,7 @@ public class LibraryController : BaseJellyfinApiController
/// <param name="itemId">The item id.</param>
/// <response code="204">Item deleted.</response>
/// <response code="401">Unauthorized access.</response>
+ /// <response code="404">Item not found.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpDelete("Items/{itemId}")]
[Authorize]
@@ -335,17 +334,18 @@ public class LibraryController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult DeleteItem(Guid itemId)
{
- var isApiKey = User.GetIsApiKey();
var userId = User.GetUserId();
- var user = !isApiKey && !userId.IsEmpty()
- ? _userManager.GetUserById(userId) ?? throw new ResourceNotFoundException()
- : null;
- if (!isApiKey && user is null)
+ var isApiKey = User.GetIsApiKey();
+ var user = userId.IsEmpty() && isApiKey
+ ? null
+ : _userManager.GetUserById(userId);
+
+ if (user is null && !isApiKey)
{
- return Unauthorized("Unauthorized access");
+ return NotFound();
}
- var item = _libraryManager.GetItemById(itemId);
+ var item = _libraryManager.GetItemById<BaseItem>(itemId, user);
if (item is null)
{
return NotFound();
@@ -391,7 +391,7 @@ public class LibraryController : BaseJellyfinApiController
foreach (var i in ids)
{
- var item = _libraryManager.GetItemById(i);
+ var item = _libraryManager.GetItemById<BaseItem>(i, user);
if (item is null)
{
return NotFound();
@@ -459,20 +459,18 @@ public class LibraryController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<IEnumerable<BaseItemDto>> GetAncestors([FromRoute, Required] Guid itemId, [FromQuery] Guid? userId)
{
- var item = _libraryManager.GetItemById(itemId);
userId = RequestHelpers.GetUserId(User, userId);
-
+ var user = userId.IsNullOrEmpty()
+ ? null
+ : _userManager.GetUserById(userId.Value);
+ var item = _libraryManager.GetItemById<BaseItem>(itemId, user);
if (item is null)
{
- return NotFound("Item not found");
+ return NotFound();
}
var baseItemDtos = new List<BaseItemDto>();
- var user = userId.IsNullOrEmpty()
- ? null
- : _userManager.GetUserById(userId.Value);
-
var dtoOptions = new DtoOptions().AddClientFields(User);
BaseItem? parent = item.GetParent();
@@ -644,14 +642,16 @@ public class LibraryController : BaseJellyfinApiController
[ProducesFile("video/*", "audio/*")]
public async Task<ActionResult> GetDownload([FromRoute, Required] Guid itemId)
{
- var item = _libraryManager.GetItemById(itemId);
+ var userId = User.GetUserId();
+ var user = userId.IsEmpty()
+ ? null
+ : _userManager.GetUserById(userId);
+ var item = _libraryManager.GetItemById<BaseItem>(itemId, user);
if (item is null)
{
return NotFound();
}
- var user = _userManager.GetUserById(User.GetUserId());
-
if (user is not null)
{
if (!item.CanDownload(user))
@@ -704,12 +704,14 @@ public class LibraryController : BaseJellyfinApiController
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields)
{
userId = RequestHelpers.GetUserId(User, userId);
+ var user = userId.IsNullOrEmpty()
+ ? null
+ : _userManager.GetUserById(userId.Value);
var item = itemId.IsEmpty()
- ? (userId.IsNullOrEmpty()
+ ? (user is null
? _libraryManager.RootFolder
: _libraryManager.GetUserRootFolder())
- : _libraryManager.GetItemById(itemId);
-
+ : _libraryManager.GetItemById<BaseItem>(itemId, user);
if (item is null)
{
return NotFound();
@@ -720,9 +722,6 @@ public class LibraryController : BaseJellyfinApiController
return new QueryResult<BaseItemDto>();
}
- var user = userId.IsNullOrEmpty()
- ? null
- : _userManager.GetUserById(userId.Value);
var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(User);
diff --git a/Jellyfin.Api/Controllers/LibraryStructureController.cs b/Jellyfin.Api/Controllers/LibraryStructureController.cs
index 23c430f85..f685eeaa0 100644
--- a/Jellyfin.Api/Controllers/LibraryStructureController.cs
+++ b/Jellyfin.Api/Controllers/LibraryStructureController.cs
@@ -6,6 +6,8 @@ using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Api.Extensions;
+using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Api.Models.LibraryStructureDto;
using MediaBrowser.Common.Api;
@@ -73,7 +75,7 @@ public class LibraryStructureController : BaseJellyfinApiController
[HttpPost]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<ActionResult> AddVirtualFolder(
- [FromQuery] string? name,
+ [FromQuery] string name,
[FromQuery] CollectionTypeOptions? collectionType,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] paths,
[FromBody] AddVirtualFolderDto? libraryOptionsDto,
@@ -101,7 +103,7 @@ public class LibraryStructureController : BaseJellyfinApiController
[HttpDelete]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<ActionResult> RemoveVirtualFolder(
- [FromQuery] string? name,
+ [FromQuery] string name,
[FromQuery] bool refreshLibrary = false)
{
await _libraryManager.RemoveVirtualFolder(name, refreshLibrary).ConfigureAwait(false);
@@ -265,18 +267,16 @@ public class LibraryStructureController : BaseJellyfinApiController
/// <param name="refreshLibrary">Whether to refresh the library.</param>
/// <returns>A <see cref="NoContentResult"/>.</returns>
/// <response code="204">Media path removed.</response>
- /// <exception cref="ArgumentNullException">The name of the library may not be empty.</exception>
+ /// <exception cref="ArgumentException">The name of the library and path may not be empty.</exception>
[HttpDelete("Paths")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult RemoveMediaPath(
- [FromQuery] string? name,
- [FromQuery] string? path,
+ [FromQuery] string name,
+ [FromQuery] string path,
[FromQuery] bool refreshLibrary = false)
{
- if (string.IsNullOrWhiteSpace(name))
- {
- throw new ArgumentNullException(nameof(name));
- }
+ ArgumentException.ThrowIfNullOrWhiteSpace(name);
+ ArgumentException.ThrowIfNullOrWhiteSpace(path);
_libraryMonitor.Stop();
@@ -311,15 +311,21 @@ public class LibraryStructureController : BaseJellyfinApiController
/// </summary>
/// <param name="request">The library name and options.</param>
/// <response code="204">Library updated.</response>
+ /// <response code="404">Item not found.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpPost("LibraryOptions")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult UpdateLibraryOptions(
[FromBody] UpdateLibraryOptionsDto request)
{
- var collectionFolder = (CollectionFolder)_libraryManager.GetItemById(request.Id);
+ var item = _libraryManager.GetItemById<CollectionFolder>(request.Id, User.GetUserId());
+ if (item is null)
+ {
+ return NotFound();
+ }
- collectionFolder.UpdateLibraryOptions(request.LibraryOptions);
+ item.UpdateLibraryOptions(request.LibraryOptions);
return NoContent();
}
}
diff --git a/Jellyfin.Api/Controllers/LiveTvController.cs b/Jellyfin.Api/Controllers/LiveTvController.cs
index 7768b3c45..2b26c01f8 100644
--- a/Jellyfin.Api/Controllers/LiveTvController.cs
+++ b/Jellyfin.Api/Controllers/LiveTvController.cs
@@ -220,9 +220,11 @@ public class LiveTvController : BaseJellyfinApiController
/// <param name="channelId">Channel id.</param>
/// <param name="userId">Optional. Attach user data.</param>
/// <response code="200">Live tv channel returned.</response>
+ /// <response code="404">Item not found.</response>
/// <returns>An <see cref="OkResult"/> containing the live tv channel.</returns>
[HttpGet("Channels/{channelId}")]
[ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
[Authorize(Policy = Policies.LiveTvAccess)]
public ActionResult<BaseItemDto> GetChannel([FromRoute, Required] Guid channelId, [FromQuery] Guid? userId)
{
@@ -232,7 +234,12 @@ public class LiveTvController : BaseJellyfinApiController
: _userManager.GetUserById(userId.Value);
var item = channelId.IsEmpty()
? _libraryManager.GetUserRootFolder()
- : _libraryManager.GetItemById(channelId);
+ : _libraryManager.GetItemById<BaseItem>(channelId, user);
+
+ if (item is null)
+ {
+ return NotFound();
+ }
var dtoOptions = new DtoOptions()
.AddClientFields(User);
@@ -416,9 +423,11 @@ public class LiveTvController : BaseJellyfinApiController
/// <param name="recordingId">Recording id.</param>
/// <param name="userId">Optional. Attach user data.</param>
/// <response code="200">Recording returned.</response>
+ /// <response code="404">Item not found.</response>
/// <returns>An <see cref="OkResult"/> containing the live tv recording.</returns>
[HttpGet("Recordings/{recordingId}")]
[ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
[Authorize(Policy = Policies.LiveTvAccess)]
public ActionResult<BaseItemDto> GetRecording([FromRoute, Required] Guid recordingId, [FromQuery] Guid? userId)
{
@@ -426,7 +435,13 @@ public class LiveTvController : BaseJellyfinApiController
var user = userId.IsNullOrEmpty()
? null
: _userManager.GetUserById(userId.Value);
- var item = recordingId.IsEmpty() ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(recordingId);
+ var item = recordingId.IsEmpty()
+ ? _libraryManager.GetUserRootFolder()
+ : _libraryManager.GetItemById<BaseItem>(recordingId, user);
+ if (item is null)
+ {
+ return NotFound();
+ }
var dtoOptions = new DtoOptions()
.AddClientFields(User);
@@ -611,7 +626,8 @@ public class LiveTvController : BaseJellyfinApiController
{
query.IsSeries = true;
- if (_libraryManager.GetItemById(librarySeriesId.Value) is Series series)
+ var series = _libraryManager.GetItemById<Series>(librarySeriesId.Value);
+ if (series is not null)
{
query.Name = series.Name;
}
@@ -665,7 +681,8 @@ public class LiveTvController : BaseJellyfinApiController
{
query.IsSeries = true;
- if (_libraryManager.GetItemById(body.LibrarySeriesId) is Series series)
+ var series = _libraryManager.GetItemById<Series>(body.LibrarySeriesId);
+ if (series is not null)
{
query.Name = series.Name;
}
@@ -779,7 +796,7 @@ public class LiveTvController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult DeleteRecording([FromRoute, Required] Guid recordingId)
{
- var item = _libraryManager.GetItemById(recordingId);
+ var item = _libraryManager.GetItemById<BaseItem>(recordingId, User.GetUserId());
if (item is null)
{
return NotFound();
diff --git a/Jellyfin.Api/Controllers/LyricsController.cs b/Jellyfin.Api/Controllers/LyricsController.cs
index f2b312b47..8eb4cadf8 100644
--- a/Jellyfin.Api/Controllers/LyricsController.cs
+++ b/Jellyfin.Api/Controllers/LyricsController.cs
@@ -7,6 +7,7 @@ using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Attributes;
using Jellyfin.Api.Extensions;
+using Jellyfin.Api.Helpers;
using Jellyfin.Extensions;
using MediaBrowser.Common.Api;
using MediaBrowser.Controller.Entities.Audio;
@@ -66,37 +67,16 @@ public class LyricsController : BaseJellyfinApiController
[HttpGet("Audio/{itemId}/Lyrics")]
[Authorize]
[ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<LyricDto>> GetLyrics([FromRoute, Required] Guid itemId)
{
- var isApiKey = User.GetIsApiKey();
- var userId = User.GetUserId();
- if (!isApiKey && userId.IsEmpty())
- {
- return BadRequest();
- }
-
- var audio = _libraryManager.GetItemById<Audio>(itemId);
- if (audio is null)
+ var item = _libraryManager.GetItemById<Audio>(itemId, User.GetUserId());
+ if (item is null)
{
return NotFound();
}
- if (!isApiKey)
- {
- var user = _userManager.GetUserById(userId);
- if (user is null)
- {
- return NotFound();
- }
-
- // Check the item is visible for the user
- if (!audio.IsVisible(user))
- {
- return Unauthorized($"{user.Username} is not permitted to access item {audio.Name}.");
- }
- }
-
- var result = await _lyricManager.GetLyricsAsync(audio, CancellationToken.None).ConfigureAwait(false);
+ var result = await _lyricManager.GetLyricsAsync(item, CancellationToken.None).ConfigureAwait(false);
if (result is not null)
{
return Ok(result);
@@ -124,8 +104,8 @@ public class LyricsController : BaseJellyfinApiController
[FromRoute, Required] Guid itemId,
[FromQuery, Required] string fileName)
{
- var audio = _libraryManager.GetItemById<Audio>(itemId);
- if (audio is null)
+ var item = _libraryManager.GetItemById<Audio>(itemId, User.GetUserId());
+ if (item is null)
{
return NotFound();
}
@@ -147,7 +127,7 @@ public class LyricsController : BaseJellyfinApiController
{
await Request.Body.CopyToAsync(stream).ConfigureAwait(false);
var uploadedLyric = await _lyricManager.SaveLyricAsync(
- audio,
+ item,
format,
stream)
.ConfigureAwait(false);
@@ -157,7 +137,7 @@ public class LyricsController : BaseJellyfinApiController
return BadRequest();
}
- _providerManager.QueueRefresh(audio.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.High);
+ _providerManager.QueueRefresh(item.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.High);
return Ok(uploadedLyric);
}
}
@@ -176,13 +156,13 @@ public class LyricsController : BaseJellyfinApiController
public async Task<ActionResult> DeleteLyrics(
[FromRoute, Required] Guid itemId)
{
- var audio = _libraryManager.GetItemById<Audio>(itemId);
- if (audio is null)
+ var item = _libraryManager.GetItemById<Audio>(itemId, User.GetUserId());
+ if (item is null)
{
return NotFound();
}
- await _lyricManager.DeleteLyricsAsync(audio).ConfigureAwait(false);
+ await _lyricManager.DeleteLyricsAsync(item).ConfigureAwait(false);
return NoContent();
}
@@ -199,13 +179,13 @@ public class LyricsController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<IReadOnlyList<RemoteLyricInfoDto>>> SearchRemoteLyrics([FromRoute, Required] Guid itemId)
{
- var audio = _libraryManager.GetItemById<Audio>(itemId);
- if (audio is null)
+ var item = _libraryManager.GetItemById<Audio>(itemId, User.GetUserId());
+ if (item is null)
{
return NotFound();
}
- var results = await _lyricManager.SearchLyricsAsync(audio, false, CancellationToken.None).ConfigureAwait(false);
+ var results = await _lyricManager.SearchLyricsAsync(item, false, CancellationToken.None).ConfigureAwait(false);
return Ok(results);
}
@@ -225,19 +205,19 @@ public class LyricsController : BaseJellyfinApiController
[FromRoute, Required] Guid itemId,
[FromRoute, Required] string lyricId)
{
- var audio = _libraryManager.GetItemById<Audio>(itemId);
- if (audio is null)
+ var item = _libraryManager.GetItemById<Audio>(itemId, User.GetUserId());
+ if (item is null)
{
return NotFound();
}
- var downloadedLyrics = await _lyricManager.DownloadLyricsAsync(audio, lyricId, CancellationToken.None).ConfigureAwait(false);
+ var downloadedLyrics = await _lyricManager.DownloadLyricsAsync(item, lyricId, CancellationToken.None).ConfigureAwait(false);
if (downloadedLyrics is null)
{
return NotFound();
}
- _providerManager.QueueRefresh(audio.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.High);
+ _providerManager.QueueRefresh(item.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.High);
return Ok(downloadedLyrics);
}
diff --git a/Jellyfin.Api/Controllers/MediaInfoController.cs b/Jellyfin.Api/Controllers/MediaInfoController.cs
index 742012b71..bc52be184 100644
--- a/Jellyfin.Api/Controllers/MediaInfoController.cs
+++ b/Jellyfin.Api/Controllers/MediaInfoController.cs
@@ -8,8 +8,10 @@ using Jellyfin.Api.Attributes;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.Models.MediaInfoDtos;
+using Jellyfin.Extensions;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Devices;
+using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.MediaInfo;
using Microsoft.AspNetCore.Authorization;
@@ -32,6 +34,7 @@ public class MediaInfoController : BaseJellyfinApiController
private readonly ILibraryManager _libraryManager;
private readonly ILogger<MediaInfoController> _logger;
private readonly MediaInfoHelper _mediaInfoHelper;
+ private readonly IUserManager _userManager;
/// <summary>
/// Initializes a new instance of the <see cref="MediaInfoController"/> class.
@@ -41,18 +44,21 @@ public class MediaInfoController : BaseJellyfinApiController
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
/// <param name="logger">Instance of the <see cref="ILogger{MediaInfoController}"/> interface.</param>
/// <param name="mediaInfoHelper">Instance of the <see cref="MediaInfoHelper"/>.</param>
+ /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface..</param>
public MediaInfoController(
IMediaSourceManager mediaSourceManager,
IDeviceManager deviceManager,
ILibraryManager libraryManager,
ILogger<MediaInfoController> logger,
- MediaInfoHelper mediaInfoHelper)
+ MediaInfoHelper mediaInfoHelper,
+ IUserManager userManager)
{
_mediaSourceManager = mediaSourceManager;
_deviceManager = deviceManager;
_libraryManager = libraryManager;
_logger = logger;
_mediaInfoHelper = mediaInfoHelper;
+ _userManager = userManager;
}
/// <summary>
@@ -61,16 +67,24 @@ public class MediaInfoController : BaseJellyfinApiController
/// <param name="itemId">The item id.</param>
/// <param name="userId">The user id.</param>
/// <response code="200">Playback info returned.</response>
+ /// <response code="404">Item not found.</response>
/// <returns>A <see cref="Task"/> containing a <see cref="PlaybackInfoResponse"/> with the playback information.</returns>
[HttpGet("Items/{itemId}/PlaybackInfo")]
[ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<PlaybackInfoResponse>> GetPlaybackInfo([FromRoute, Required] Guid itemId, [FromQuery] Guid? userId)
{
userId = RequestHelpers.GetUserId(User, userId);
- return await _mediaInfoHelper.GetPlaybackInfo(
- itemId,
- userId)
- .ConfigureAwait(false);
+ var user = userId.IsNullOrEmpty()
+ ? null
+ : _userManager.GetUserById(userId.Value);
+ var item = _libraryManager.GetItemById<BaseItem>(itemId, user);
+ if (item is null)
+ {
+ return NotFound();
+ }
+
+ return await _mediaInfoHelper.GetPlaybackInfo(item, user).ConfigureAwait(false);
}
/// <summary>
@@ -97,9 +111,11 @@ public class MediaInfoController : BaseJellyfinApiController
/// <param name="allowAudioStreamCopy">Whether to allow to copy the audio stream. Default: true.</param>
/// <param name="playbackInfoDto">The playback info.</param>
/// <response code="200">Playback info returned.</response>
+ /// <response code="404">Item not found.</response>
/// <returns>A <see cref="Task"/> containing a <see cref="PlaybackInfoResponse"/> with the playback info.</returns>
[HttpPost("Items/{itemId}/PlaybackInfo")]
[ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<PlaybackInfoResponse>> GetPostedPlaybackInfo(
[FromRoute, Required] Guid itemId,
[FromQuery, ParameterObsolete] Guid? userId,
@@ -148,9 +164,19 @@ public class MediaInfoController : BaseJellyfinApiController
allowVideoStreamCopy ??= playbackInfoDto?.AllowVideoStreamCopy ?? true;
allowAudioStreamCopy ??= playbackInfoDto?.AllowAudioStreamCopy ?? true;
+ userId = RequestHelpers.GetUserId(User, userId);
+ var user = userId.IsNullOrEmpty()
+ ? null
+ : _userManager.GetUserById(userId.Value);
+ var item = _libraryManager.GetItemById<BaseItem>(itemId, user);
+ if (item is null)
+ {
+ return NotFound();
+ }
+
var info = await _mediaInfoHelper.GetPlaybackInfo(
- itemId,
- userId,
+ item,
+ user,
mediaSourceId,
liveStreamId)
.ConfigureAwait(false);
@@ -163,8 +189,6 @@ public class MediaInfoController : BaseJellyfinApiController
if (profile is not null)
{
// set device specific data
- var item = _libraryManager.GetItemById(itemId);
-
foreach (var mediaSource in info.MediaSources)
{
_mediaInfoHelper.SetDeviceSpecificData(
diff --git a/Jellyfin.Api/Controllers/PlaylistsController.cs b/Jellyfin.Api/Controllers/PlaylistsController.cs
index 1100f85cf..abf94a32f 100644
--- a/Jellyfin.Api/Controllers/PlaylistsController.cs
+++ b/Jellyfin.Api/Controllers/PlaylistsController.cs
@@ -482,8 +482,13 @@ public class PlaylistsController : BaseJellyfinApiController
var user = userId.IsNullOrEmpty()
? null
: _userManager.GetUserById(userId.Value);
+ var item = _libraryManager.GetItemById<Playlist>(playlistId, user);
+ if (item is null)
+ {
+ return NotFound();
+ }
- var items = playlist.GetManageableItems().ToArray();
+ var items = item.GetManageableItems().ToArray();
var count = items.Length;
if (startIndex.HasValue)
{
diff --git a/Jellyfin.Api/Controllers/PlaystateController.cs b/Jellyfin.Api/Controllers/PlaystateController.cs
index 949d101dc..9d6d75681 100644
--- a/Jellyfin.Api/Controllers/PlaystateController.cs
+++ b/Jellyfin.Api/Controllers/PlaystateController.cs
@@ -6,6 +6,7 @@ using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Data.Entities;
+using Jellyfin.Extensions;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
@@ -76,21 +77,21 @@ public class PlaystateController : BaseJellyfinApiController
[FromRoute, Required] Guid itemId,
[FromQuery, ModelBinder(typeof(LegacyDateTimeModelBinder))] DateTime? datePlayed)
{
- var requestUserId = RequestHelpers.GetUserId(User, userId);
- var user = _userManager.GetUserById(requestUserId);
+ userId = RequestHelpers.GetUserId(User, userId);
+ var user = _userManager.GetUserById(userId.Value);
if (user is null)
{
return NotFound();
}
- var session = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
-
- var item = _libraryManager.GetItemById(itemId);
+ var item = _libraryManager.GetItemById<BaseItem>(itemId, user);
if (item is null)
{
return NotFound();
}
+ var session = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
+
var dto = UpdatePlayedStatus(user, item, true, datePlayed);
foreach (var additionalUserInfo in session.AdditionalUsers)
{
@@ -141,21 +142,21 @@ public class PlaystateController : BaseJellyfinApiController
[FromQuery] Guid? userId,
[FromRoute, Required] Guid itemId)
{
- var requestUserId = RequestHelpers.GetUserId(User, userId);
- var user = _userManager.GetUserById(requestUserId);
+ userId = RequestHelpers.GetUserId(User, userId);
+ var user = _userManager.GetUserById(userId.Value);
if (user is null)
{
return NotFound();
}
- var session = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
- var item = _libraryManager.GetItemById(itemId);
-
+ var item = _libraryManager.GetItemById<BaseItem>(itemId, user);
if (item is null)
{
return NotFound();
}
+ var session = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
+
var dto = UpdatePlayedStatus(user, item, false, null);
foreach (var additionalUserInfo in session.AdditionalUsers)
{
diff --git a/Jellyfin.Api/Controllers/RemoteImageController.cs b/Jellyfin.Api/Controllers/RemoteImageController.cs
index 595cab2df..a476005cb 100644
--- a/Jellyfin.Api/Controllers/RemoteImageController.cs
+++ b/Jellyfin.Api/Controllers/RemoteImageController.cs
@@ -6,8 +6,11 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Constants;
+using Jellyfin.Api.Extensions;
+using Jellyfin.Api.Helpers;
using MediaBrowser.Common.Api;
using MediaBrowser.Controller;
+using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
@@ -68,7 +71,7 @@ public class RemoteImageController : BaseJellyfinApiController
[FromQuery] string? providerName,
[FromQuery] bool includeAllLanguages = false)
{
- var item = _libraryManager.GetItemById(itemId);
+ var item = _libraryManager.GetItemById<BaseItem>(itemId, User.GetUserId());
if (item is null)
{
return NotFound();
@@ -127,7 +130,7 @@ public class RemoteImageController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<IEnumerable<ImageProviderInfo>> GetRemoteImageProviders([FromRoute, Required] Guid itemId)
{
- var item = _libraryManager.GetItemById(itemId);
+ var item = _libraryManager.GetItemById<BaseItem>(itemId, User.GetUserId());
if (item is null)
{
return NotFound();
@@ -154,7 +157,7 @@ public class RemoteImageController : BaseJellyfinApiController
[FromQuery, Required] ImageType type,
[FromQuery] string? imageUrl)
{
- var item = _libraryManager.GetItemById(itemId);
+ var item = _libraryManager.GetItemById<BaseItem>(itemId, User.GetUserId());
if (item is null)
{
return NotFound();
diff --git a/Jellyfin.Api/Controllers/SearchController.cs b/Jellyfin.Api/Controllers/SearchController.cs
index 413b7b834..8bae6fb9b 100644
--- a/Jellyfin.Api/Controllers/SearchController.cs
+++ b/Jellyfin.Api/Controllers/SearchController.cs
@@ -211,7 +211,7 @@ public class SearchController : BaseJellyfinApiController
if (!item.ChannelId.IsEmpty())
{
- var channel = _libraryManager.GetItemById(item.ChannelId);
+ var channel = _libraryManager.GetItemById<BaseItem>(item.ChannelId);
result.ChannelName = channel?.Name;
}
diff --git a/Jellyfin.Api/Controllers/SubtitleController.cs b/Jellyfin.Api/Controllers/SubtitleController.cs
index e2c5486d9..9da1dce93 100644
--- a/Jellyfin.Api/Controllers/SubtitleController.cs
+++ b/Jellyfin.Api/Controllers/SubtitleController.cs
@@ -12,6 +12,7 @@ using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Attributes;
using Jellyfin.Api.Extensions;
+using Jellyfin.Api.Helpers;
using Jellyfin.Api.Models.SubtitleDtos;
using MediaBrowser.Common.Api;
using MediaBrowser.Common.Configuration;
@@ -95,8 +96,7 @@ public class SubtitleController : BaseJellyfinApiController
[FromRoute, Required] Guid itemId,
[FromRoute, Required] int index)
{
- var item = _libraryManager.GetItemById(itemId);
-
+ var item = _libraryManager.GetItemById<BaseItem>(itemId, User.GetUserId());
if (item is null)
{
return NotFound();
@@ -113,18 +113,24 @@ public class SubtitleController : BaseJellyfinApiController
/// <param name="language">The language of the subtitles.</param>
/// <param name="isPerfectMatch">Optional. Only show subtitles which are a perfect match.</param>
/// <response code="200">Subtitles retrieved.</response>
+ /// <response code="404">Item not found.</response>
/// <returns>An array of <see cref="RemoteSubtitleInfo"/>.</returns>
[HttpGet("Items/{itemId}/RemoteSearch/Subtitles/{language}")]
[Authorize(Policy = Policies.SubtitleManagement)]
[ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<IEnumerable<RemoteSubtitleInfo>>> SearchRemoteSubtitles(
[FromRoute, Required] Guid itemId,
[FromRoute, Required] string language,
[FromQuery] bool? isPerfectMatch)
{
- var video = (Video)_libraryManager.GetItemById(itemId);
+ var item = _libraryManager.GetItemById<Video>(itemId, User.GetUserId());
+ if (item is null)
+ {
+ return NotFound();
+ }
- return await _subtitleManager.SearchSubtitles(video, language, isPerfectMatch, false, CancellationToken.None).ConfigureAwait(false);
+ return await _subtitleManager.SearchSubtitles(item, language, isPerfectMatch, false, CancellationToken.None).ConfigureAwait(false);
}
/// <summary>
@@ -133,22 +139,28 @@ public class SubtitleController : BaseJellyfinApiController
/// <param name="itemId">The item id.</param>
/// <param name="subtitleId">The subtitle id.</param>
/// <response code="204">Subtitle downloaded.</response>
+ /// <response code="404">Item not found.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpPost("Items/{itemId}/RemoteSearch/Subtitles/{subtitleId}")]
[Authorize(Policy = Policies.SubtitleManagement)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult> DownloadRemoteSubtitles(
[FromRoute, Required] Guid itemId,
[FromRoute, Required] string subtitleId)
{
- var video = (Video)_libraryManager.GetItemById(itemId);
+ var item = _libraryManager.GetItemById<Video>(itemId, User.GetUserId());
+ if (item is null)
+ {
+ return NotFound();
+ }
try
{
- await _subtitleManager.DownloadSubtitles(video, subtitleId, CancellationToken.None)
+ await _subtitleManager.DownloadSubtitles(item, subtitleId, CancellationToken.None)
.ConfigureAwait(false);
- _providerManager.QueueRefresh(video.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.High);
+ _providerManager.QueueRefresh(item.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.High);
}
catch (Exception ex)
{
@@ -223,7 +235,7 @@ public class SubtitleController : BaseJellyfinApiController
if (string.IsNullOrEmpty(format))
{
- var item = (Video)_libraryManager.GetItemById(itemId.Value);
+ var item = _libraryManager.GetItemById<Video>(itemId.Value);
var idString = itemId.Value.ToString("N", CultureInfo.InvariantCulture);
var mediaSource = _mediaSourceManager.GetStaticMediaSources(item, false)
@@ -321,10 +333,12 @@ public class SubtitleController : BaseJellyfinApiController
/// <param name="mediaSourceId">The media source id.</param>
/// <param name="segmentLength">The subtitle segment length.</param>
/// <response code="200">Subtitle playlist retrieved.</response>
+ /// <response code="404">Item not found.</response>
/// <returns>A <see cref="FileContentResult"/> with the HLS subtitle playlist.</returns>
[HttpGet("Videos/{itemId}/{mediaSourceId}/Subtitles/{index}/subtitles.m3u8")]
[Authorize]
[ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesPlaylistFile]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]
public async Task<ActionResult> GetSubtitlePlaylist(
@@ -333,7 +347,11 @@ public class SubtitleController : BaseJellyfinApiController
[FromRoute, Required] string mediaSourceId,
[FromQuery, Required] int segmentLength)
{
- var item = (Video)_libraryManager.GetItemById(itemId);
+ var item = _libraryManager.GetItemById<Video>(itemId, User.GetUserId());
+ if (item is null)
+ {
+ return NotFound();
+ }
var mediaSource = await _mediaSourceManager.GetMediaSource(item, mediaSourceId, null, false, CancellationToken.None).ConfigureAwait(false);
@@ -397,15 +415,21 @@ public class SubtitleController : BaseJellyfinApiController
/// <param name="itemId">The item the subtitle belongs to.</param>
/// <param name="body">The request body.</param>
/// <response code="204">Subtitle uploaded.</response>
+ /// <response code="404">Item not found.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpPost("Videos/{itemId}/Subtitles")]
[Authorize(Policy = Policies.SubtitleManagement)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult> UploadSubtitle(
[FromRoute, Required] Guid itemId,
[FromBody, Required] UploadSubtitleDto body)
{
- var video = (Video)_libraryManager.GetItemById(itemId);
+ var item = _libraryManager.GetItemById<Video>(itemId, User.GetUserId());
+ if (item is null)
+ {
+ return NotFound();
+ }
var bytes = Encoding.UTF8.GetBytes(body.Data);
var memoryStream = new MemoryStream(bytes, 0, bytes.Length, false, true);
@@ -416,7 +440,7 @@ public class SubtitleController : BaseJellyfinApiController
await using (stream.ConfigureAwait(false))
{
await _subtitleManager.UploadSubtitle(
- video,
+ item,
new SubtitleResponse
{
Format = body.Format,
@@ -425,7 +449,7 @@ public class SubtitleController : BaseJellyfinApiController
IsHearingImpaired = body.IsHearingImpaired,
Stream = stream
}).ConfigureAwait(false);
- _providerManager.QueueRefresh(video.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.High);
+ _providerManager.QueueRefresh(item.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.High);
return NoContent();
}
@@ -452,7 +476,7 @@ public class SubtitleController : BaseJellyfinApiController
long? endPositionTicks,
bool copyTimestamps)
{
- var item = _libraryManager.GetItemById(id);
+ var item = _libraryManager.GetItemById<BaseItem>(id);
return _subtitleEncoder.GetSubtitles(
item,
diff --git a/Jellyfin.Api/Controllers/TrickplayController.cs b/Jellyfin.Api/Controllers/TrickplayController.cs
index 2dc960229..0afe053da 100644
--- a/Jellyfin.Api/Controllers/TrickplayController.cs
+++ b/Jellyfin.Api/Controllers/TrickplayController.cs
@@ -5,6 +5,8 @@ using System.Text;
using System.Threading.Tasks;
using Jellyfin.Api.Attributes;
using Jellyfin.Api.Extensions;
+using Jellyfin.Api.Helpers;
+using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Trickplay;
using MediaBrowser.Model;
@@ -84,7 +86,7 @@ public class TrickplayController : BaseJellyfinApiController
[FromRoute, Required] int index,
[FromQuery] Guid? mediaSourceId)
{
- var item = _libraryManager.GetItemById(mediaSourceId ?? itemId);
+ var item = _libraryManager.GetItemById<BaseItem>(itemId, User.GetUserId());
if (item is null)
{
return NotFound();
diff --git a/Jellyfin.Api/Controllers/TvShowsController.cs b/Jellyfin.Api/Controllers/TvShowsController.cs
index 3d84b61bf..68b4b6b8b 100644
--- a/Jellyfin.Api/Controllers/TvShowsController.cs
+++ b/Jellyfin.Api/Controllers/TvShowsController.cs
@@ -234,7 +234,7 @@ public class TvShowsController : BaseJellyfinApiController
if (seasonId.HasValue) // Season id was supplied. Get episodes by season id.
{
- var item = _libraryManager.GetItemById(seasonId.Value);
+ var item = _libraryManager.GetItemById<BaseItem>(seasonId.Value);
if (item is not Season seasonItem)
{
return NotFound("No season exists with Id " + seasonId);
@@ -244,7 +244,8 @@ public class TvShowsController : BaseJellyfinApiController
}
else if (season.HasValue) // Season number was supplied. Get episodes by season number
{
- if (_libraryManager.GetItemById(seriesId) is not Series series)
+ var series = _libraryManager.GetItemById<Series>(seriesId);
+ if (series is null)
{
return NotFound("Series not found");
}
@@ -259,7 +260,7 @@ public class TvShowsController : BaseJellyfinApiController
}
else // No season number or season id was supplied. Returning all episodes.
{
- if (_libraryManager.GetItemById(seriesId) is not Series series)
+ if (_libraryManager.GetItemById<BaseItem>(seriesId) is not Series series)
{
return NotFound("Series not found");
}
@@ -342,13 +343,13 @@ public class TvShowsController : BaseJellyfinApiController
var user = userId.IsNullOrEmpty()
? null
: _userManager.GetUserById(userId.Value);
-
- if (_libraryManager.GetItemById(seriesId) is not Series series)
+ var item = _libraryManager.GetItemById<Series>(seriesId, user);
+ if (item is null)
{
- return NotFound("Series not found");
+ return NotFound();
}
- var seasons = series.GetItemList(new InternalItemsQuery(user)
+ var seasons = item.GetItemList(new InternalItemsQuery(user)
{
IsMissing = isMissing,
IsSpecialSeason = isSpecialSeason,
diff --git a/Jellyfin.Api/Controllers/UniversalAudioController.cs b/Jellyfin.Api/Controllers/UniversalAudioController.cs
index db78e9946..1d4adae06 100644
--- a/Jellyfin.Api/Controllers/UniversalAudioController.cs
+++ b/Jellyfin.Api/Controllers/UniversalAudioController.cs
@@ -9,7 +9,9 @@ using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Api.Models.StreamingDtos;
using Jellyfin.Data.Enums;
+using Jellyfin.Extensions;
using MediaBrowser.Common.Extensions;
+using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Streaming;
@@ -33,6 +35,7 @@ public class UniversalAudioController : BaseJellyfinApiController
private readonly MediaInfoHelper _mediaInfoHelper;
private readonly AudioHelper _audioHelper;
private readonly DynamicHlsHelper _dynamicHlsHelper;
+ private readonly IUserManager _userManager;
/// <summary>
/// Initializes a new instance of the <see cref="UniversalAudioController"/> class.
@@ -42,18 +45,21 @@ public class UniversalAudioController : BaseJellyfinApiController
/// <param name="mediaInfoHelper">Instance of <see cref="MediaInfoHelper"/>.</param>
/// <param name="audioHelper">Instance of <see cref="AudioHelper"/>.</param>
/// <param name="dynamicHlsHelper">Instance of <see cref="DynamicHlsHelper"/>.</param>
+ /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
public UniversalAudioController(
ILibraryManager libraryManager,
ILogger<UniversalAudioController> logger,
MediaInfoHelper mediaInfoHelper,
AudioHelper audioHelper,
- DynamicHlsHelper dynamicHlsHelper)
+ DynamicHlsHelper dynamicHlsHelper,
+ IUserManager userManager)
{
_libraryManager = libraryManager;
_logger = logger;
_mediaInfoHelper = mediaInfoHelper;
_audioHelper = audioHelper;
_dynamicHlsHelper = dynamicHlsHelper;
+ _userManager = userManager;
}
/// <summary>
@@ -79,12 +85,14 @@ public class UniversalAudioController : BaseJellyfinApiController
/// <param name="enableRedirection">Whether to enable redirection. Defaults to true.</param>
/// <response code="200">Audio stream returned.</response>
/// <response code="302">Redirected to remote audio stream.</response>
+ /// <response code="404">Item not found.</response>
/// <returns>A <see cref="Task"/> containing the audio file.</returns>
[HttpGet("Audio/{itemId}/universal")]
[HttpHead("Audio/{itemId}/universal", Name = "HeadUniversalAudioStream")]
[Authorize]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status302Found)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesAudioFile]
public async Task<ActionResult> GetUniversalAudioStream(
[FromRoute, Required] Guid itemId,
@@ -106,20 +114,27 @@ public class UniversalAudioController : BaseJellyfinApiController
[FromQuery] bool breakOnNonKeyFrames = false,
[FromQuery] bool enableRedirection = true)
{
- var deviceProfile = GetDeviceProfile(container, transcodingContainer, audioCodec, transcodingProtocol, breakOnNonKeyFrames, transcodingAudioChannels, maxAudioSampleRate, maxAudioBitDepth, maxAudioChannels);
userId = RequestHelpers.GetUserId(User, userId);
+ var user = userId.IsNullOrEmpty()
+ ? null
+ : _userManager.GetUserById(userId.Value);
+ var item = _libraryManager.GetItemById<BaseItem>(itemId, user);
+ if (item is null)
+ {
+ return NotFound();
+ }
+
+ var deviceProfile = GetDeviceProfile(container, transcodingContainer, audioCodec, transcodingProtocol, breakOnNonKeyFrames, transcodingAudioChannels, maxAudioSampleRate, maxAudioBitDepth, maxAudioChannels);
_logger.LogInformation("GetPostedPlaybackInfo profile: {@Profile}", deviceProfile);
var info = await _mediaInfoHelper.GetPlaybackInfo(
- itemId,
- userId,
+ item,
+ user,
mediaSourceId)
.ConfigureAwait(false);
// set device specific data
- var item = _libraryManager.GetItemById(itemId);
-
foreach (var sourceInfo in info.MediaSources)
{
_mediaInfoHelper.SetDeviceSpecificData(
diff --git a/Jellyfin.Api/Controllers/UserLibraryController.cs b/Jellyfin.Api/Controllers/UserLibraryController.cs
index c19ad33c8..421f1bfb5 100644
--- a/Jellyfin.Api/Controllers/UserLibraryController.cs
+++ b/Jellyfin.Api/Controllers/UserLibraryController.cs
@@ -77,8 +77,8 @@ public class UserLibraryController : BaseJellyfinApiController
[FromQuery] Guid? userId,
[FromRoute, Required] Guid itemId)
{
- var requestUserId = RequestHelpers.GetUserId(User, userId);
- var user = _userManager.GetUserById(requestUserId);
+ userId = RequestHelpers.GetUserId(User, userId);
+ var user = _userManager.GetUserById(userId.Value);
if (user is null)
{
return NotFound();
@@ -86,20 +86,12 @@ public class UserLibraryController : BaseJellyfinApiController
var item = itemId.IsEmpty()
? _libraryManager.GetUserRootFolder()
- : _libraryManager.GetItemById(itemId);
-
+ : _libraryManager.GetItemById<BaseItem>(itemId, user);
if (item is null)
{
return NotFound();
}
- if (item is not UserRootFolder
- // Check the item is visible for the user
- && !item.IsVisible(user))
- {
- return Unauthorized($"{user.Username} is not permitted to access item {item.Name}.");
- }
-
await RefreshItemOnDemandIfNeeded(item).ConfigureAwait(false);
var dtoOptions = new DtoOptions().AddClientFields(User);
@@ -133,8 +125,8 @@ public class UserLibraryController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<BaseItemDto> GetRootFolder([FromQuery] Guid? userId)
{
- var requestUserId = RequestHelpers.GetUserId(User, userId);
- var user = _userManager.GetUserById(requestUserId);
+ userId = RequestHelpers.GetUserId(User, userId);
+ var user = _userManager.GetUserById(userId.Value);
if (user is null)
{
return NotFound();
@@ -172,8 +164,8 @@ public class UserLibraryController : BaseJellyfinApiController
[FromQuery] Guid? userId,
[FromRoute, Required] Guid itemId)
{
- var requestUserId = RequestHelpers.GetUserId(User, userId);
- var user = _userManager.GetUserById(requestUserId);
+ userId = RequestHelpers.GetUserId(User, userId);
+ var user = _userManager.GetUserById(userId.Value);
if (user is null)
{
return NotFound();
@@ -181,20 +173,12 @@ public class UserLibraryController : BaseJellyfinApiController
var item = itemId.IsEmpty()
? _libraryManager.GetUserRootFolder()
- : _libraryManager.GetItemById(itemId);
-
+ : _libraryManager.GetItemById<BaseItem>(itemId, user);
if (item is null)
{
return NotFound();
}
- if (item is not UserRootFolder
- // Check the item is visible for the user
- && !item.IsVisible(user))
- {
- return Unauthorized($"{user.Username} is not permitted to access item {item.Name}.");
- }
-
var items = await _libraryManager.GetIntros(item, user).ConfigureAwait(false);
var dtoOptions = new DtoOptions().AddClientFields(User);
var dtos = items.Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user)).ToArray();
@@ -231,8 +215,8 @@ public class UserLibraryController : BaseJellyfinApiController
[FromQuery] Guid? userId,
[FromRoute, Required] Guid itemId)
{
- var requestUserId = RequestHelpers.GetUserId(User, userId);
- var user = _userManager.GetUserById(requestUserId);
+ userId = RequestHelpers.GetUserId(User, userId);
+ var user = _userManager.GetUserById(userId.Value);
if (user is null)
{
return NotFound();
@@ -240,20 +224,12 @@ public class UserLibraryController : BaseJellyfinApiController
var item = itemId.IsEmpty()
? _libraryManager.GetUserRootFolder()
- : _libraryManager.GetItemById(itemId);
-
+ : _libraryManager.GetItemById<BaseItem>(itemId, user);
if (item is null)
{
return NotFound();
}
- if (item is not UserRootFolder
- // Check the item is visible for the user
- && !item.IsVisible(user))
- {
- return Unauthorized($"{user.Username} is not permitted to access item {item.Name}.");
- }
-
return MarkFavorite(user, item, true);
}
@@ -286,8 +262,8 @@ public class UserLibraryController : BaseJellyfinApiController
[FromQuery] Guid? userId,
[FromRoute, Required] Guid itemId)
{
- var requestUserId = RequestHelpers.GetUserId(User, userId);
- var user = _userManager.GetUserById(requestUserId);
+ userId = RequestHelpers.GetUserId(User, userId);
+ var user = _userManager.GetUserById(userId.Value);
if (user is null)
{
return NotFound();
@@ -295,20 +271,12 @@ public class UserLibraryController : BaseJellyfinApiController
var item = itemId.IsEmpty()
? _libraryManager.GetUserRootFolder()
- : _libraryManager.GetItemById(itemId);
-
+ : _libraryManager.GetItemById<BaseItem>(itemId, user);
if (item is null)
{
return NotFound();
}
- if (item is not UserRootFolder
- // Check the item is visible for the user
- && !item.IsVisible(user))
- {
- return Unauthorized($"{user.Username} is not permitted to access item {item.Name}.");
- }
-
return MarkFavorite(user, item, false);
}
@@ -341,8 +309,8 @@ public class UserLibraryController : BaseJellyfinApiController
[FromQuery] Guid? userId,
[FromRoute, Required] Guid itemId)
{
- var requestUserId = RequestHelpers.GetUserId(User, userId);
- var user = _userManager.GetUserById(requestUserId);
+ userId = RequestHelpers.GetUserId(User, userId);
+ var user = _userManager.GetUserById(userId.Value);
if (user is null)
{
return NotFound();
@@ -350,20 +318,12 @@ public class UserLibraryController : BaseJellyfinApiController
var item = itemId.IsEmpty()
? _libraryManager.GetUserRootFolder()
- : _libraryManager.GetItemById(itemId);
-
+ : _libraryManager.GetItemById<BaseItem>(itemId, user);
if (item is null)
{
return NotFound();
}
- if (item is not UserRootFolder
- // Check the item is visible for the user
- && !item.IsVisible(user))
- {
- return Unauthorized($"{user.Username} is not permitted to access item {item.Name}.");
- }
-
return UpdateUserItemRatingInternal(user, item, null);
}
@@ -398,8 +358,8 @@ public class UserLibraryController : BaseJellyfinApiController
[FromRoute, Required] Guid itemId,
[FromQuery] bool? likes)
{
- var requestUserId = RequestHelpers.GetUserId(User, userId);
- var user = _userManager.GetUserById(requestUserId);
+ userId = RequestHelpers.GetUserId(User, userId);
+ var user = _userManager.GetUserById(userId.Value);
if (user is null)
{
return NotFound();
@@ -407,20 +367,12 @@ public class UserLibraryController : BaseJellyfinApiController
var item = itemId.IsEmpty()
? _libraryManager.GetUserRootFolder()
- : _libraryManager.GetItemById(itemId);
-
+ : _libraryManager.GetItemById<BaseItem>(itemId, user);
if (item is null)
{
return NotFound();
}
- if (item is not UserRootFolder
- // Check the item is visible for the user
- && !item.IsVisible(user))
- {
- return Unauthorized($"{user.Username} is not permitted to access item {item.Name}.");
- }
-
return UpdateUserItemRatingInternal(user, item, likes);
}
@@ -455,8 +407,8 @@ public class UserLibraryController : BaseJellyfinApiController
[FromQuery] Guid? userId,
[FromRoute, Required] Guid itemId)
{
- var requestUserId = RequestHelpers.GetUserId(User, userId);
- var user = _userManager.GetUserById(requestUserId);
+ userId = RequestHelpers.GetUserId(User, userId);
+ var user = _userManager.GetUserById(userId.Value);
if (user is null)
{
return NotFound();
@@ -464,20 +416,12 @@ public class UserLibraryController : BaseJellyfinApiController
var item = itemId.IsEmpty()
? _libraryManager.GetUserRootFolder()
- : _libraryManager.GetItemById(itemId);
-
+ : _libraryManager.GetItemById<BaseItem>(itemId, user);
if (item is null)
{
return NotFound();
}
- if (item is not UserRootFolder
- // Check the item is visible for the user
- && !item.IsVisible(user))
- {
- return Unauthorized($"{user.Username} is not permitted to access item {item.Name}.");
- }
-
var dtoOptions = new DtoOptions().AddClientFields(User);
if (item is IHasTrailers hasTrailers)
{
@@ -519,8 +463,8 @@ public class UserLibraryController : BaseJellyfinApiController
[FromQuery] Guid? userId,
[FromRoute, Required] Guid itemId)
{
- var requestUserId = RequestHelpers.GetUserId(User, userId);
- var user = _userManager.GetUserById(requestUserId);
+ userId = RequestHelpers.GetUserId(User, userId);
+ var user = _userManager.GetUserById(userId.Value);
if (user is null)
{
return NotFound();
@@ -528,20 +472,12 @@ public class UserLibraryController : BaseJellyfinApiController
var item = itemId.IsEmpty()
? _libraryManager.GetUserRootFolder()
- : _libraryManager.GetItemById(itemId);
-
+ : _libraryManager.GetItemById<BaseItem>(itemId, user);
if (item is null)
{
return NotFound();
}
- if (item is not UserRootFolder
- // Check the item is visible for the user
- && !item.IsVisible(user))
- {
- return Unauthorized($"{user.Username} is not permitted to access item {item.Name}.");
- }
-
var dtoOptions = new DtoOptions().AddClientFields(User);
return Ok(item
diff --git a/Jellyfin.Api/Controllers/VideoAttachmentsController.cs b/Jellyfin.Api/Controllers/VideoAttachmentsController.cs
index 23b9ba46f..b67c6fdb7 100644
--- a/Jellyfin.Api/Controllers/VideoAttachmentsController.cs
+++ b/Jellyfin.Api/Controllers/VideoAttachmentsController.cs
@@ -4,7 +4,10 @@ using System.Net.Mime;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Attributes;
+using Jellyfin.Api.Extensions;
+using Jellyfin.Api.Helpers;
using MediaBrowser.Common.Extensions;
+using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
using Microsoft.AspNetCore.Http;
@@ -54,7 +57,7 @@ public class VideoAttachmentsController : BaseJellyfinApiController
{
try
{
- var item = _libraryManager.GetItemById(videoId);
+ var item = _libraryManager.GetItemById<BaseItem>(videoId, User.GetUserId());
if (item is null)
{
return NotFound();
diff --git a/Jellyfin.Api/Controllers/VideosController.cs b/Jellyfin.Api/Controllers/VideosController.cs
index 380120032..a9e1d4484 100644
--- a/Jellyfin.Api/Controllers/VideosController.cs
+++ b/Jellyfin.Api/Controllers/VideosController.cs
@@ -7,7 +7,6 @@ using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Attributes;
-using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
@@ -105,7 +104,11 @@ public class VideosController : BaseJellyfinApiController
? (userId.IsNullOrEmpty()
? _libraryManager.RootFolder
: _libraryManager.GetUserRootFolder())
- : _libraryManager.GetItemById(itemId);
+ : _libraryManager.GetItemById<BaseItem>(itemId, user);
+ if (item is null)
+ {
+ return NotFound();
+ }
var dtoOptions = new DtoOptions();
dtoOptions = dtoOptions.AddClientFields(User);
@@ -139,24 +142,23 @@ public class VideosController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult> DeleteAlternateSources([FromRoute, Required] Guid itemId)
{
- var video = (Video)_libraryManager.GetItemById(itemId);
-
- if (video is null)
+ var item = _libraryManager.GetItemById<Video>(itemId, User.GetUserId());
+ if (item is null)
{
- return NotFound("The video either does not exist or the id does not belong to a video.");
+ return NotFound();
}
- if (video.LinkedAlternateVersions.Length == 0)
+ if (item.LinkedAlternateVersions.Length == 0)
{
- video = (Video?)_libraryManager.GetItemById(video.PrimaryVersionId);
+ item = _libraryManager.GetItemById<Video>(Guid.Parse(item.PrimaryVersionId));
}
- if (video is null)
+ if (item is null)
{
return NotFound();
}
- foreach (var link in video.GetLinkedAlternateVersions())
+ foreach (var link in item.GetLinkedAlternateVersions())
{
link.SetPrimaryVersionId(null);
link.LinkedAlternateVersions = Array.Empty<LinkedChild>();
@@ -164,9 +166,9 @@ public class VideosController : BaseJellyfinApiController
await link.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
}
- video.LinkedAlternateVersions = Array.Empty<LinkedChild>();
- video.SetPrimaryVersionId(null);
- await video.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
+ item.LinkedAlternateVersions = Array.Empty<LinkedChild>();
+ item.SetPrimaryVersionId(null);
+ await item.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
return NoContent();
}
@@ -184,8 +186,9 @@ public class VideosController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<ActionResult> MergeVersions([FromQuery, Required, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids)
{
+ var userId = User.GetUserId();
var items = ids
- .Select(i => _libraryManager.GetItemById(i))
+ .Select(i => _libraryManager.GetItemById<BaseItem>(i, userId))
.OfType<Video>()
.OrderBy(i => i.Id)
.ToList();
diff --git a/Jellyfin.Api/Helpers/MediaInfoHelper.cs b/Jellyfin.Api/Helpers/MediaInfoHelper.cs
index 6a24ad32a..212d678a8 100644
--- a/Jellyfin.Api/Helpers/MediaInfoHelper.cs
+++ b/Jellyfin.Api/Helpers/MediaInfoHelper.cs
@@ -24,6 +24,7 @@ using MediaBrowser.Model.Entities;
using MediaBrowser.Model.MediaInfo;
using MediaBrowser.Model.Session;
using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.Extensions.Logging;
namespace Jellyfin.Api.Helpers;
@@ -76,21 +77,17 @@ public class MediaInfoHelper
/// <summary>
/// Get playback info.
/// </summary>
- /// <param name="id">Item id.</param>
- /// <param name="userId">User Id.</param>
+ /// <param name="item">The item.</param>
+ /// <param name="user">The user.</param>
/// <param name="mediaSourceId">Media source id.</param>
/// <param name="liveStreamId">Live stream id.</param>
/// <returns>A <see cref="Task"/> containing the <see cref="PlaybackInfoResponse"/>.</returns>
public async Task<PlaybackInfoResponse> GetPlaybackInfo(
- Guid id,
- Guid? userId,
+ BaseItem item,
+ User? user,
string? mediaSourceId = null,
string? liveStreamId = null)
{
- var user = userId.IsNullOrEmpty()
- ? null
- : _userManager.GetUserById(userId.Value);
- var item = _libraryManager.GetItemById(id);
var result = new PlaybackInfoResponse();
MediaSourceInfo[] mediaSources;
@@ -402,7 +399,8 @@ public class MediaInfoHelper
if (profile is not null)
{
- var item = _libraryManager.GetItemById(request.ItemId);
+ var item = _libraryManager.GetItemById<BaseItem>(request.ItemId)
+ ?? throw new ResourceNotFoundException();
SetDeviceSpecificData(
item,
diff --git a/Jellyfin.Api/Helpers/StreamingHelpers.cs b/Jellyfin.Api/Helpers/StreamingHelpers.cs
index bfe71fd87..d6d7a56eb 100644
--- a/Jellyfin.Api/Helpers/StreamingHelpers.cs
+++ b/Jellyfin.Api/Helpers/StreamingHelpers.cs
@@ -11,6 +11,7 @@ using Jellyfin.Extensions;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Streaming;
@@ -18,6 +19,7 @@ using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.Net.Http.Headers;
namespace Jellyfin.Api.Helpers;
@@ -107,7 +109,8 @@ public static class StreamingHelpers
?? state.SupportedSubtitleCodecs.FirstOrDefault();
}
- var item = libraryManager.GetItemById(streamingRequest.Id);
+ var item = libraryManager.GetItemById<BaseItem>(streamingRequest.Id)
+ ?? throw new ResourceNotFoundException();
state.IsInputVideo = item.MediaType == MediaType.Video;
@@ -125,7 +128,7 @@ public static class StreamingHelpers
if (mediaSource is null)
{
- var mediaSources = await mediaSourceManager.GetPlaybackMediaSources(libraryManager.GetItemById(streamingRequest.Id), null, false, false, cancellationToken).ConfigureAwait(false);
+ var mediaSources = await mediaSourceManager.GetPlaybackMediaSources(libraryManager.GetItemById<BaseItem>(streamingRequest.Id), null, false, false, cancellationToken).ConfigureAwait(false);
mediaSource = string.IsNullOrEmpty(streamingRequest.MediaSourceId)
? mediaSources[0]
diff --git a/Jellyfin.Api/Models/LibraryStructureDto/MediaPathDto.cs b/Jellyfin.Api/Models/LibraryStructureDto/MediaPathDto.cs
index 94ffc5238..7a549aada 100644
--- a/Jellyfin.Api/Models/LibraryStructureDto/MediaPathDto.cs
+++ b/Jellyfin.Api/Models/LibraryStructureDto/MediaPathDto.cs
@@ -12,7 +12,7 @@ public class MediaPathDto
/// Gets or sets the name of the library.
/// </summary>
[Required]
- public string? Name { get; set; }
+ public required string Name { get; set; }
/// <summary>
/// Gets or sets the path to add.
diff --git a/Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs b/Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs
index bb8d4dd14..3d747f2ea 100644
--- a/Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs
+++ b/Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs
@@ -22,11 +22,9 @@ public static class ServiceCollectionExtensions
serviceCollection.AddEFSecondLevelCache(options =>
options.UseMemoryCacheProvider()
.CacheAllQueries(CacheExpirationMode.Sliding, TimeSpan.FromMinutes(10))
- .DisableLogging(true)
.UseCacheKeyPrefix("EF_")
// Don't cache null values. Remove this optional setting if it's not necessary.
- .SkipCachingResults(result =>
- result.Value is null || (result.Value is EFTableRows rows && rows.RowsCount == 0)));
+ .SkipCachingResults(result => result.Value is null or EFTableRows { RowsCount: 0 }));
serviceCollection.AddPooledDbContextFactory<JellyfinDbContext>((serviceProvider, opt) =>
{
diff --git a/Jellyfin.Server/Migrations/Routines/RemoveDownloadImagesInAdvance.cs b/Jellyfin.Server/Migrations/Routines/RemoveDownloadImagesInAdvance.cs
index 9137ea234..52fb93d59 100644
--- a/Jellyfin.Server/Migrations/Routines/RemoveDownloadImagesInAdvance.cs
+++ b/Jellyfin.Server/Migrations/Routines/RemoveDownloadImagesInAdvance.cs
@@ -42,7 +42,7 @@ namespace Jellyfin.Server.Migrations.Routines
}
var libraryOptions = virtualFolder.LibraryOptions;
- var collectionFolder = (CollectionFolder)_libraryManager.GetItemById(folderId);
+ var collectionFolder = _libraryManager.GetItemById<CollectionFolder>(folderId) ?? throw new InvalidOperationException("Failed to find CollectionFolder");
// The property no longer exists in LibraryOptions, so we just re-save the options to get old data removed.
collectionFolder.UpdateLibraryOptions(libraryOptions);
_logger.LogInformation("Removed from '{VirtualFolder}'", virtualFolder.Name);
diff --git a/MediaBrowser.Controller/Library/ILibraryManager.cs b/MediaBrowser.Controller/Library/ILibraryManager.cs
index 6532f7a34..37703ceee 100644
--- a/MediaBrowser.Controller/Library/ILibraryManager.cs
+++ b/MediaBrowser.Controller/Library/ILibraryManager.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
#pragma warning disable CA1002, CS1591
using System;
@@ -33,17 +31,17 @@ namespace MediaBrowser.Controller.Library
/// <summary>
/// Occurs when [item added].
/// </summary>
- event EventHandler<ItemChangeEventArgs> ItemAdded;
+ event EventHandler<ItemChangeEventArgs>? ItemAdded;
/// <summary>
/// Occurs when [item updated].
/// </summary>
- event EventHandler<ItemChangeEventArgs> ItemUpdated;
+ event EventHandler<ItemChangeEventArgs>? ItemUpdated;
/// <summary>
/// Occurs when [item removed].
/// </summary>
- event EventHandler<ItemChangeEventArgs> ItemRemoved;
+ event EventHandler<ItemChangeEventArgs>? ItemRemoved;
/// <summary>
/// Gets the root folder.
@@ -60,10 +58,10 @@ namespace MediaBrowser.Controller.Library
/// <param name="parent">The parent.</param>
/// <param name="directoryService">An instance of <see cref="IDirectoryService"/>.</param>
/// <returns>BaseItem.</returns>
- BaseItem ResolvePath(
+ BaseItem? ResolvePath(
FileSystemMetadata fileInfo,
- Folder parent = null,
- IDirectoryService directoryService = null);
+ Folder? parent = null,
+ IDirectoryService? directoryService = null);
/// <summary>
/// Resolves a set of files into a list of BaseItem.
@@ -86,7 +84,7 @@ namespace MediaBrowser.Controller.Library
/// </summary>
/// <param name="name">The name of the person.</param>
/// <returns>Task{Person}.</returns>
- Person GetPerson(string name);
+ Person? GetPerson(string name);
/// <summary>
/// Finds the by path.
@@ -94,7 +92,7 @@ namespace MediaBrowser.Controller.Library
/// <param name="path">The path.</param>
/// <param name="isFolder"><c>true</c> is the path is a directory; otherwise <c>false</c>.</param>
/// <returns>BaseItem.</returns>
- BaseItem FindByPath(string path, bool? isFolder);
+ BaseItem? FindByPath(string path, bool? isFolder);
/// <summary>
/// Gets the artist.
@@ -166,7 +164,8 @@ namespace MediaBrowser.Controller.Library
/// </summary>
/// <param name="id">The id.</param>
/// <returns>BaseItem.</returns>
- BaseItem GetItemById(Guid id);
+ /// <exception cref="ArgumentNullException"><paramref name="id"/> is <c>null</c>.</exception>
+ BaseItem? GetItemById(Guid id);
/// <summary>
/// Gets the item by id, as T.
@@ -174,7 +173,27 @@ namespace MediaBrowser.Controller.Library
/// <param name="id">The item id.</param>
/// <typeparam name="T">The type of item.</typeparam>
/// <returns>The item.</returns>
- T GetItemById<T>(Guid id)
+ T? GetItemById<T>(Guid id)
+ where T : BaseItem;
+
+ /// <summary>
+ /// Gets the item by id, as T, and validates user access.
+ /// </summary>
+ /// <param name="id">The item id.</param>
+ /// <param name="userId">The user id to validate against.</param>
+ /// <typeparam name="T">The type of item.</typeparam>
+ /// <returns>The item if found.</returns>
+ public T? GetItemById<T>(Guid id, Guid userId)
+ where T : BaseItem;
+
+ /// <summary>
+ /// Gets the item by id, as T, and validates user access.
+ /// </summary>
+ /// <param name="id">The item id.</param>
+ /// <param name="user">The user to validate against.</param>
+ /// <typeparam name="T">The type of item.</typeparam>
+ /// <returns>The item if found.</returns>
+ public T? GetItemById<T>(Guid id, User? user)
where T : BaseItem;
/// <summary>
@@ -208,9 +227,9 @@ namespace MediaBrowser.Controller.Library
/// <param name="sortBy">The sort by.</param>
/// <param name="sortOrder">The sort order.</param>
/// <returns>IEnumerable{BaseItem}.</returns>
- IEnumerable<BaseItem> Sort(IEnumerable<BaseItem> items, User user, IEnumerable<ItemSortBy> sortBy, SortOrder sortOrder);
+ IEnumerable<BaseItem> Sort(IEnumerable<BaseItem> items, User? user, IEnumerable<ItemSortBy> sortBy, SortOrder sortOrder);
- IEnumerable<BaseItem> Sort(IEnumerable<BaseItem> items, User user, IEnumerable<(ItemSortBy OrderBy, SortOrder SortOrder)> orderBy);
+ IEnumerable<BaseItem> Sort(IEnumerable<BaseItem> items, User? user, IEnumerable<(ItemSortBy OrderBy, SortOrder SortOrder)> orderBy);
/// <summary>
/// Gets the user root folder.
@@ -223,7 +242,7 @@ namespace MediaBrowser.Controller.Library
/// </summary>
/// <param name="item">Item to create.</param>
/// <param name="parent">Parent of new item.</param>
- void CreateItem(BaseItem item, BaseItem parent);
+ void CreateItem(BaseItem item, BaseItem? parent);
/// <summary>
/// Creates the items.
@@ -231,7 +250,7 @@ namespace MediaBrowser.Controller.Library
/// <param name="items">Items to create.</param>
/// <param name="parent">Parent of new items.</param>
/// <param name="cancellationToken">CancellationToken to use for operation.</param>
- void CreateItems(IReadOnlyList<BaseItem> items, BaseItem parent, CancellationToken cancellationToken);
+ void CreateItems(IReadOnlyList<BaseItem> items, BaseItem? parent, CancellationToken cancellationToken);
/// <summary>
/// Updates the item.
@@ -509,7 +528,7 @@ namespace MediaBrowser.Controller.Library
/// <returns>QueryResult&lt;BaseItem&gt;.</returns>
QueryResult<BaseItem> QueryItems(InternalItemsQuery query);
- string GetPathAfterNetworkSubstitution(string path, BaseItem ownerItem = null);
+ string GetPathAfterNetworkSubstitution(string path, BaseItem? ownerItem = null);
/// <summary>
/// Converts the image to local.
diff --git a/MediaBrowser.Controller/Resolvers/IResolverIgnoreRule.cs b/MediaBrowser.Controller/Resolvers/IResolverIgnoreRule.cs
index a07b3e898..733d40ba1 100644
--- a/MediaBrowser.Controller/Resolvers/IResolverIgnoreRule.cs
+++ b/MediaBrowser.Controller/Resolvers/IResolverIgnoreRule.cs
@@ -14,6 +14,6 @@ namespace MediaBrowser.Controller.Resolvers
/// <param name="fileInfo">The file information.</param>
/// <param name="parent">The parent BaseItem.</param>
/// <returns>True if the file should be ignored.</returns>
- bool ShouldIgnore(FileSystemMetadata fileInfo, BaseItem parent);
+ bool ShouldIgnore(FileSystemMetadata fileInfo, BaseItem? parent);
}
}
diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
index 4b1b1bbc6..21f4cb1cd 100644
--- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
+++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
@@ -463,7 +463,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
try
{
var subtitleStreams = mediaSource.MediaStreams
- .Where(stream => stream.IsTextSubtitleStream && stream.SupportsExternalStream);
+ .Where(stream => stream is { IsTextSubtitleStream: true, SupportsExternalStream: true, IsExternal: false });
foreach (var subtitleStream in subtitleStreams)
{
diff --git a/MediaBrowser.MediaEncoding/Transcoding/TranscodeManager.cs b/MediaBrowser.MediaEncoding/Transcoding/TranscodeManager.cs
index a07a0f41b..ea5dbf7f7 100644
--- a/MediaBrowser.MediaEncoding/Transcoding/TranscodeManager.cs
+++ b/MediaBrowser.MediaEncoding/Transcoding/TranscodeManager.cs
@@ -492,12 +492,11 @@ public sealed class TranscodeManager : ITranscodeManager, IDisposable
IODefaults.FileStreamBufferSize,
FileOptions.Asynchronous);
- var commandLineLogMessage = process.StartInfo.FileName + " " + process.StartInfo.Arguments;
+ await JsonSerializer.SerializeAsync(logStream, state.MediaSource, cancellationToken: cancellationTokenSource.Token).ConfigureAwait(false);
var commandLineLogMessageBytes = Encoding.UTF8.GetBytes(
- JsonSerializer.Serialize(state.MediaSource)
+ Environment.NewLine
+ Environment.NewLine
- + Environment.NewLine
- + commandLineLogMessage
+ + process.StartInfo.FileName + " " + process.StartInfo.Arguments
+ Environment.NewLine
+ Environment.NewLine);
diff --git a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs
index 67b84681d..bbc9af227 100644
--- a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs
+++ b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs
@@ -338,6 +338,12 @@ namespace MediaBrowser.Providers.MediaInfo
audio.Artists = performers;
}
+ if (albumArtists.Length == 0)
+ {
+ // Album artists not provided, fall back to performers (artists).
+ albumArtists = performers;
+ }
+
if (options.ReplaceAllMetadata && albumArtists.Length != 0)
{
audio.AlbumArtists = albumArtists;
diff --git a/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs
index 1399ac307..b25cfc83f 100644
--- a/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs
+++ b/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs
@@ -947,7 +947,7 @@ namespace MediaBrowser.XbmcMetadata.Savers
if (saveImagePath)
{
var personEntity = libraryManager.GetPerson(person.Name);
- var image = personEntity.GetImageInfo(ImageType.Primary, 0);
+ var image = personEntity?.GetImageInfo(ImageType.Primary, 0);
if (image is not null)
{
diff --git a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs
index 4f6ed4469..75963226a 100644
--- a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs
+++ b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs
@@ -263,6 +263,11 @@ public class SkiaEncoder : IImageEncoder
return null;
}
+ if (codec.FrameCount != 0)
+ {
+ throw new ArgumentException("Cannot decode images with multiple frames");
+ }
+
// create the bitmap
var bitmap = new SKBitmap(codec.Info.Width, codec.Info.Height, !requiresTransparencyHack);
diff --git a/src/Jellyfin.Networking/AutoDiscoveryHost.cs b/src/Jellyfin.Networking/AutoDiscoveryHost.cs
index 2be57d7a1..f1b79b4b4 100644
--- a/src/Jellyfin.Networking/AutoDiscoveryHost.cs
+++ b/src/Jellyfin.Networking/AutoDiscoveryHost.cs
@@ -11,6 +11,7 @@ using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller;
using MediaBrowser.Model.ApiClient;
+using MediaBrowser.Model.Net;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
@@ -50,6 +51,25 @@ public sealed class AutoDiscoveryHost : BackgroundService
_networkManager = networkManager;
}
+ private static IPAddress GetBindAddress(IPData intf)
+ {
+ if (intf.AddressFamily == AddressFamily.InterNetwork)
+ {
+ if (OperatingSystem.IsLinux())
+ {
+ return NetworkUtils.GetBroadcastAddress(intf.Subnet);
+ }
+
+ if (OperatingSystem.IsMacOS())
+ {
+ // macOS kernel does not allow bind to 127:255:255:255
+ return IPAddress.IsLoopback(intf.Address) ? intf.Address : NetworkUtils.GetBroadcastAddress(intf.Subnet);
+ }
+ }
+
+ return intf.Address;
+ }
+
/// <inheritdoc />
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
@@ -63,24 +83,23 @@ public sealed class AutoDiscoveryHost : BackgroundService
// Linux needs to bind to the broadcast addresses to receive broadcast traffic
if (OperatingSystem.IsLinux() && networkConfig.EnableIPv4)
{
- udpServers.Add(ListenForAutoDiscoveryMessage(IPAddress.Broadcast, stoppingToken));
+ udpServers.Add(ListenForAutoDiscoveryMessage(IPAddress.Broadcast, IPAddress.Broadcast, stoppingToken));
}
udpServers.AddRange(_networkManager.GetInternalBindAddresses()
.Select(intf => ListenForAutoDiscoveryMessage(
- OperatingSystem.IsLinux() && intf.AddressFamily == AddressFamily.InterNetwork
- ? NetworkUtils.GetBroadcastAddress(intf.Subnet)
- : intf.Address,
+ GetBindAddress(intf),
+ intf.Address,
stoppingToken)));
await Task.WhenAll(udpServers).ConfigureAwait(false);
}
- private async Task ListenForAutoDiscoveryMessage(IPAddress address, CancellationToken cancellationToken)
+ private async Task ListenForAutoDiscoveryMessage(IPAddress listenAddress, IPAddress respondAddress, CancellationToken cancellationToken)
{
try
{
- using var udpClient = new UdpClient(new IPEndPoint(address, PortNumber));
+ using var udpClient = new UdpClient(new IPEndPoint(listenAddress, PortNumber));
udpClient.MulticastLoopback = false;
while (!cancellationToken.IsCancellationRequested)
@@ -91,7 +110,7 @@ public sealed class AutoDiscoveryHost : BackgroundService
var text = Encoding.UTF8.GetString(result.Buffer);
if (text.Contains("who is JellyfinServer?", StringComparison.OrdinalIgnoreCase))
{
- await RespondToV2Message(udpClient, result.RemoteEndPoint, cancellationToken).ConfigureAwait(false);
+ await RespondToV2Message(respondAddress, result.RemoteEndPoint, cancellationToken).ConfigureAwait(false);
}
}
catch (SocketException ex)
@@ -107,11 +126,11 @@ public sealed class AutoDiscoveryHost : BackgroundService
catch (Exception ex)
{
// Exception in this function will prevent the background service from restarting in-process.
- _logger.LogError(ex, "Unable to bind to {Address}:{Port}", address, PortNumber);
+ _logger.LogError(ex, "Unable to bind to {Address}:{Port}", listenAddress, PortNumber);
}
}
- private async Task RespondToV2Message(UdpClient udpClient, IPEndPoint endpoint, CancellationToken cancellationToken)
+ private async Task RespondToV2Message(IPAddress responderIp, IPEndPoint endpoint, CancellationToken cancellationToken)
{
var localUrl = _appHost.GetSmartApiUrl(endpoint.Address);
if (string.IsNullOrEmpty(localUrl))
@@ -122,10 +141,11 @@ public sealed class AutoDiscoveryHost : BackgroundService
var response = new ServerDiscoveryInfo(localUrl, _appHost.SystemId, _appHost.FriendlyName);
+ using var responder = new UdpClient(new IPEndPoint(responderIp, PortNumber));
try
{
_logger.LogDebug("Sending AutoDiscovery response");
- await udpClient
+ await responder
.SendAsync(JsonSerializer.SerializeToUtf8Bytes(response).AsMemory(), endpoint, cancellationToken)
.ConfigureAwait(false);
}
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/PathExtensionsTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/PathExtensionsTests.cs
index d1be07aa2..940e3c2b1 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Library/PathExtensionsTests.cs
+++ b/tests/Jellyfin.Server.Implementations.Tests/Library/PathExtensionsTests.cs
@@ -18,6 +18,12 @@ namespace Jellyfin.Server.Implementations.Tests.Library
[InlineData("Superman: Red Son [tmdbid=618355][imdbid=tt10985510]", "imdbid", "tt10985510")]
[InlineData("Superman: Red Son [tmdbid-618355][imdbid-tt10985510]", "imdbid", "tt10985510")]
[InlineData("Superman: Red Son [tmdbid-618355][imdbid-tt10985510]", "tmdbid", "618355")]
+ [InlineData("Superman: Red Son [providera-id=1]", "providera-id", "1")]
+ [InlineData("Superman: Red Son [providerb-id=2]", "providerb-id", "2")]
+ [InlineData("Superman: Red Son [providera id=4]", "providera id", "4")]
+ [InlineData("Superman: Red Son [providerb id=5]", "providerb id", "5")]
+ [InlineData("Superman: Red Son [tmdbid=3]", "tmdbid", "3")]
+ [InlineData("Superman: Red Son [tvdbid-6]", "tvdbid", "6")]
[InlineData("[tmdbid=618355]", "tmdbid", "618355")]
[InlineData("[tmdbid-618355]", "tmdbid", "618355")]
[InlineData("tmdbid=111111][tmdbid=618355]", "tmdbid", "618355")]
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs
index 09e4709da..0f7f5c194 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs
+++ b/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs
@@ -127,6 +127,22 @@ namespace Jellyfin.Server.Implementations.Tests.Localization
Assert.Equal(expectedLevel, level!);
}
+ [Theory]
+ [InlineData("0", 0)]
+ [InlineData("1", 1)]
+ [InlineData("6", 6)]
+ [InlineData("12", 12)]
+ [InlineData("42", 42)]
+ [InlineData("9999", 9999)]
+ public async Task GetRatingLevel_GivenValidAge_Success(string value, int expectedLevel)
+ {
+ var localizationManager = Setup(new ServerConfiguration { MetadataCountryCode = "nl" });
+ await localizationManager.LoadAll();
+ var level = localizationManager.GetRatingLevel(value);
+ Assert.NotNull(level);
+ Assert.Equal(expectedLevel, level);
+ }
+
[Fact]
public async Task GetRatingLevel_GivenUnratedString_Success()
{