浏览代码

Validate item access (#11171)

Cody Robibero 1 年之前
父节点
当前提交
6fb6b5f176
共有 28 个文件被更改,包括 414 次插入281 次删除
  1. 33 6
      Emby.Server.Implementations/Library/LibraryManager.cs
  2. 1 1
      Jellyfin.Api/Controllers/DisplayPreferencesController.cs
  3. 1 1
      Jellyfin.Api/Controllers/FilterController.cs
  4. 11 9
      Jellyfin.Api/Controllers/ImageController.cs
  5. 52 8
      Jellyfin.Api/Controllers/InstantMixController.cs
  6. 11 2
      Jellyfin.Api/Controllers/ItemLookupController.cs
  7. 4 1
      Jellyfin.Api/Controllers/ItemRefreshController.cs
  8. 9 3
      Jellyfin.Api/Controllers/ItemUpdateController.cs
  9. 8 4
      Jellyfin.Api/Controllers/ItemsController.cs
  10. 32 33
      Jellyfin.Api/Controllers/LibraryController.cs
  11. 10 2
      Jellyfin.Api/Controllers/LibraryStructureController.cs
  12. 22 5
      Jellyfin.Api/Controllers/LiveTvController.cs
  13. 19 39
      Jellyfin.Api/Controllers/LyricsController.cs
  14. 33 9
      Jellyfin.Api/Controllers/MediaInfoController.cs
  15. 6 1
      Jellyfin.Api/Controllers/PlaylistsController.cs
  16. 11 10
      Jellyfin.Api/Controllers/PlaystateController.cs
  17. 6 3
      Jellyfin.Api/Controllers/RemoteImageController.cs
  18. 1 1
      Jellyfin.Api/Controllers/SearchController.cs
  19. 37 13
      Jellyfin.Api/Controllers/SubtitleController.cs
  20. 3 1
      Jellyfin.Api/Controllers/TrickplayController.cs
  21. 8 7
      Jellyfin.Api/Controllers/TvShowsController.cs
  22. 21 6
      Jellyfin.Api/Controllers/UniversalAudioController.cs
  23. 26 90
      Jellyfin.Api/Controllers/UserLibraryController.cs
  24. 4 1
      Jellyfin.Api/Controllers/VideoAttachmentsController.cs
  25. 17 14
      Jellyfin.Api/Controllers/VideosController.cs
  26. 5 9
      Jellyfin.Api/Helpers/MediaInfoHelper.cs
  27. 3 2
      Jellyfin.Api/Helpers/StreamingHelpers.cs
  28. 20 0
      MediaBrowser.Controller/Library/ILibraryManager.cs

+ 33 - 6
Emby.Server.Implementations/Library/LibraryManager.cs

@@ -46,6 +46,7 @@ using MediaBrowser.Model.Library;
 using MediaBrowser.Model.Querying;
 using MediaBrowser.Model.Querying;
 using MediaBrowser.Model.Tasks;
 using MediaBrowser.Model.Tasks;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Logging;
+using TMDbLib.Objects.Authentication;
 using Episode = MediaBrowser.Controller.Entities.TV.Episode;
 using Episode = MediaBrowser.Controller.Entities.TV.Episode;
 using EpisodeInfo = Emby.Naming.TV.EpisodeInfo;
 using EpisodeInfo = Emby.Naming.TV.EpisodeInfo;
 using Genre = MediaBrowser.Controller.Entities.Genre;
 using Genre = MediaBrowser.Controller.Entities.Genre;
@@ -1222,12 +1223,7 @@ namespace Emby.Server.Implementations.Library
             return null;
             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>
+        /// <inheritdoc />
         public BaseItem GetItemById(Guid id)
         public BaseItem GetItemById(Guid id)
         {
         {
             if (id.IsEmpty())
             if (id.IsEmpty())
@@ -1263,6 +1259,22 @@ namespace Emby.Server.Implementations.Library
             return null;
             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)
         public List<BaseItem> GetItemList(InternalItemsQuery query, bool allowExternalContent)
         {
         {
             if (query.Recursive && !query.ParentId.IsEmpty())
             if (query.Recursive && !query.ParentId.IsEmpty())
@@ -3191,5 +3203,20 @@ namespace Emby.Server.Implementations.Library
 
 
             CollectionFolder.SaveLibraryOptions(virtualFolderPath, libraryOptions);
             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);
+        }
     }
     }
 }
 }

+ 1 - 1
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)))
         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]);
                 _logger.LogError("Invalid ViewType: {LandingScreenOption}", displayPreferences.CustomPrefs[key]);
                 displayPreferences.CustomPrefs.Remove(key);
                 displayPreferences.CustomPrefs.Remove(key);

+ 1 - 1
Jellyfin.Api/Controllers/FilterController.cs

@@ -162,7 +162,7 @@ public class FilterController : BaseJellyfinApiController
         }
         }
         else if (parentId.HasValue)
         else if (parentId.HasValue)
         {
         {
-            parentItem = _libraryManager.GetItemById(parentId.Value);
+            parentItem = _libraryManager.GetItemById<BaseItem>(parentId.Value);
         }
         }
 
 
         var filters = new QueryFilters();
         var filters = new QueryFilters();

+ 11 - 9
Jellyfin.Api/Controllers/ImageController.cs

@@ -90,6 +90,7 @@ public class ImageController : BaseJellyfinApiController
     /// <param name="userId">User Id.</param>
     /// <param name="userId">User Id.</param>
     /// <response code="204">Image updated.</response>
     /// <response code="204">Image updated.</response>
     /// <response code="403">User does not have permission to delete the image.</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>
     /// <returns>A <see cref="NoContentResult"/>.</returns>
     [HttpPost("UserImage")]
     [HttpPost("UserImage")]
     [Authorize]
     [Authorize]
@@ -97,6 +98,7 @@ public class ImageController : BaseJellyfinApiController
     [ProducesResponseType(StatusCodes.Status204NoContent)]
     [ProducesResponseType(StatusCodes.Status204NoContent)]
     [ProducesResponseType(StatusCodes.Status400BadRequest)]
     [ProducesResponseType(StatusCodes.Status400BadRequest)]
     [ProducesResponseType(StatusCodes.Status403Forbidden)]
     [ProducesResponseType(StatusCodes.Status403Forbidden)]
+    [ProducesResponseType(StatusCodes.Status404NotFound)]
     public async Task<ActionResult> PostUserImage(
     public async Task<ActionResult> PostUserImage(
         [FromQuery] Guid? userId)
         [FromQuery] Guid? userId)
     {
     {
@@ -289,7 +291,7 @@ public class ImageController : BaseJellyfinApiController
         [FromRoute, Required] ImageType imageType,
         [FromRoute, Required] ImageType imageType,
         [FromQuery] int? imageIndex)
         [FromQuery] int? imageIndex)
     {
     {
-        var item = _libraryManager.GetItemById(itemId);
+        var item = _libraryManager.GetItemById<BaseItem>(itemId, User.GetUserId());
         if (item is null)
         if (item is null)
         {
         {
             return NotFound();
             return NotFound();
@@ -317,7 +319,7 @@ public class ImageController : BaseJellyfinApiController
         [FromRoute, Required] ImageType imageType,
         [FromRoute, Required] ImageType imageType,
         [FromRoute] int imageIndex)
         [FromRoute] int imageIndex)
     {
     {
-        var item = _libraryManager.GetItemById(itemId);
+        var item = _libraryManager.GetItemById<BaseItem>(itemId, User.GetUserId());
         if (item is null)
         if (item is null)
         {
         {
             return NotFound();
             return NotFound();
@@ -346,7 +348,7 @@ public class ImageController : BaseJellyfinApiController
         [FromRoute, Required] Guid itemId,
         [FromRoute, Required] Guid itemId,
         [FromRoute, Required] ImageType imageType)
         [FromRoute, Required] ImageType imageType)
     {
     {
-        var item = _libraryManager.GetItemById(itemId);
+        var item = _libraryManager.GetItemById<BaseItem>(itemId, User.GetUserId());
         if (item is null)
         if (item is null)
         {
         {
             return NotFound();
             return NotFound();
@@ -390,7 +392,7 @@ public class ImageController : BaseJellyfinApiController
         [FromRoute, Required] ImageType imageType,
         [FromRoute, Required] ImageType imageType,
         [FromRoute] int imageIndex)
         [FromRoute] int imageIndex)
     {
     {
-        var item = _libraryManager.GetItemById(itemId);
+        var item = _libraryManager.GetItemById<BaseItem>(itemId, User.GetUserId());
         if (item is null)
         if (item is null)
         {
         {
             return NotFound();
             return NotFound();
@@ -433,7 +435,7 @@ public class ImageController : BaseJellyfinApiController
         [FromRoute, Required] int imageIndex,
         [FromRoute, Required] int imageIndex,
         [FromQuery, Required] int newIndex)
         [FromQuery, Required] int newIndex)
     {
     {
-        var item = _libraryManager.GetItemById(itemId);
+        var item = _libraryManager.GetItemById<BaseItem>(itemId, User.GetUserId());
         if (item is null)
         if (item is null)
         {
         {
             return NotFound();
             return NotFound();
@@ -456,7 +458,7 @@ public class ImageController : BaseJellyfinApiController
     [ProducesResponseType(StatusCodes.Status404NotFound)]
     [ProducesResponseType(StatusCodes.Status404NotFound)]
     public async Task<ActionResult<IEnumerable<ImageInfo>>> GetItemImageInfos([FromRoute, Required] Guid itemId)
     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)
         if (item is null)
         {
         {
             return NotFound();
             return NotFound();
@@ -559,7 +561,7 @@ public class ImageController : BaseJellyfinApiController
         [FromQuery] string? foregroundLayer,
         [FromQuery] string? foregroundLayer,
         [FromQuery] int? imageIndex)
         [FromQuery] int? imageIndex)
     {
     {
-        var item = _libraryManager.GetItemById(itemId);
+        var item = _libraryManager.GetItemById<BaseItem>(itemId, User.GetUserId());
         if (item is null)
         if (item is null)
         {
         {
             return NotFound();
             return NotFound();
@@ -637,7 +639,7 @@ public class ImageController : BaseJellyfinApiController
         [FromQuery] string? backgroundColor,
         [FromQuery] string? backgroundColor,
         [FromQuery] string? foregroundLayer)
         [FromQuery] string? foregroundLayer)
     {
     {
-        var item = _libraryManager.GetItemById(itemId);
+        var item = _libraryManager.GetItemById<BaseItem>(itemId, User.GetUserId());
         if (item is null)
         if (item is null)
         {
         {
             return NotFound();
             return NotFound();
@@ -715,7 +717,7 @@ public class ImageController : BaseJellyfinApiController
         [FromQuery] string? foregroundLayer,
         [FromQuery] string? foregroundLayer,
         [FromRoute, Required] int imageIndex)
         [FromRoute, Required] int imageIndex)
     {
     {
-        var item = _libraryManager.GetItemById(itemId);
+        var item = _libraryManager.GetItemById<BaseItem>(itemId, User.GetUserId());
         if (item is null)
         if (item is null)
         {
         {
             return NotFound();
             return NotFound();

+ 52 - 8
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="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>
     /// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
     /// <response code="200">Instant playlist returned.</response>
     /// <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>
     /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
     [HttpGet("Songs/{itemId}/InstantMix")]
     [HttpGet("Songs/{itemId}/InstantMix")]
     [ProducesResponseType(StatusCodes.Status200OK)]
     [ProducesResponseType(StatusCodes.Status200OK)]
+    [ProducesResponseType(StatusCodes.Status404NotFound)]
     public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromSong(
     public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromSong(
         [FromRoute, Required] Guid itemId,
         [FromRoute, Required] Guid itemId,
         [FromQuery] Guid? userId,
         [FromQuery] Guid? userId,
@@ -75,11 +77,16 @@ public class InstantMixController : BaseJellyfinApiController
         [FromQuery] int? imageTypeLimit,
         [FromQuery] int? imageTypeLimit,
         [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
         [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
     {
     {
-        var item = _libraryManager.GetItemById(itemId);
         userId = RequestHelpers.GetUserId(User, userId);
         userId = RequestHelpers.GetUserId(User, userId);
         var user = userId.IsNullOrEmpty()
         var user = userId.IsNullOrEmpty()
             ? null
             ? null
             : _userManager.GetUserById(userId.Value);
             : _userManager.GetUserById(userId.Value);
+        var item = _libraryManager.GetItemById<BaseItem>(itemId, user);
+        if (item is null)
+        {
+            return NotFound();
+        }
+
         var dtoOptions = new DtoOptions { Fields = fields }
         var dtoOptions = new DtoOptions { Fields = fields }
             .AddClientFields(User)
             .AddClientFields(User)
             .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
             .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
@@ -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="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>
     /// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
     /// <response code="200">Instant playlist returned.</response>
     /// <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>
     /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
     [HttpGet("Albums/{itemId}/InstantMix")]
     [HttpGet("Albums/{itemId}/InstantMix")]
     [ProducesResponseType(StatusCodes.Status200OK)]
     [ProducesResponseType(StatusCodes.Status200OK)]
+    [ProducesResponseType(StatusCodes.Status404NotFound)]
     public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromAlbum(
     public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromAlbum(
         [FromRoute, Required] Guid itemId,
         [FromRoute, Required] Guid itemId,
         [FromQuery] Guid? userId,
         [FromQuery] Guid? userId,
@@ -112,15 +121,20 @@ public class InstantMixController : BaseJellyfinApiController
         [FromQuery] int? imageTypeLimit,
         [FromQuery] int? imageTypeLimit,
         [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
         [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
     {
     {
-        var album = _libraryManager.GetItemById(itemId);
         userId = RequestHelpers.GetUserId(User, userId);
         userId = RequestHelpers.GetUserId(User, userId);
         var user = userId.IsNullOrEmpty()
         var user = userId.IsNullOrEmpty()
             ? null
             ? null
             : _userManager.GetUserById(userId.Value);
             : _userManager.GetUserById(userId.Value);
+        var item = _libraryManager.GetItemById<BaseItem>(itemId, user);
+        if (item is null)
+        {
+            return NotFound();
+        }
+
         var dtoOptions = new DtoOptions { Fields = fields }
         var dtoOptions = new DtoOptions { Fields = fields }
             .AddClientFields(User)
             .AddClientFields(User)
             .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
             .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
-        var items = _musicManager.GetInstantMixFromItem(album, user, dtoOptions);
+        var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions);
         return GetResult(items, user, limit, 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="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>
     /// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
     /// <response code="200">Instant playlist returned.</response>
     /// <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>
     /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
     [HttpGet("Playlists/{itemId}/InstantMix")]
     [HttpGet("Playlists/{itemId}/InstantMix")]
     [ProducesResponseType(StatusCodes.Status200OK)]
     [ProducesResponseType(StatusCodes.Status200OK)]
+    [ProducesResponseType(StatusCodes.Status404NotFound)]
     public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromPlaylist(
     public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromPlaylist(
         [FromRoute, Required] Guid itemId,
         [FromRoute, Required] Guid itemId,
         [FromQuery] Guid? userId,
         [FromQuery] Guid? userId,
@@ -149,15 +165,20 @@ public class InstantMixController : BaseJellyfinApiController
         [FromQuery] int? imageTypeLimit,
         [FromQuery] int? imageTypeLimit,
         [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
         [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
     {
     {
-        var playlist = (Playlist)_libraryManager.GetItemById(itemId);
         userId = RequestHelpers.GetUserId(User, userId);
         userId = RequestHelpers.GetUserId(User, userId);
         var user = userId.IsNullOrEmpty()
         var user = userId.IsNullOrEmpty()
             ? null
             ? null
             : _userManager.GetUserById(userId.Value);
             : _userManager.GetUserById(userId.Value);
+        var item = _libraryManager.GetItemById<Playlist>(itemId, user);
+        if (item is null)
+        {
+            return NotFound();
+        }
+
         var dtoOptions = new DtoOptions { Fields = fields }
         var dtoOptions = new DtoOptions { Fields = fields }
             .AddClientFields(User)
             .AddClientFields(User)
             .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
             .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
-        var items = _musicManager.GetInstantMixFromItem(playlist, user, dtoOptions);
+        var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions);
         return GetResult(items, user, limit, 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="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>
     /// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
     /// <response code="200">Instant playlist returned.</response>
     /// <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>
     /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
     [HttpGet("Artists/{itemId}/InstantMix")]
     [HttpGet("Artists/{itemId}/InstantMix")]
     [ProducesResponseType(StatusCodes.Status200OK)]
     [ProducesResponseType(StatusCodes.Status200OK)]
+    [ProducesResponseType(StatusCodes.Status404NotFound)]
     public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromArtists(
     public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromArtists(
         [FromRoute, Required] Guid itemId,
         [FromRoute, Required] Guid itemId,
         [FromQuery] Guid? userId,
         [FromQuery] Guid? userId,
@@ -222,11 +245,16 @@ public class InstantMixController : BaseJellyfinApiController
         [FromQuery] int? imageTypeLimit,
         [FromQuery] int? imageTypeLimit,
         [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
         [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
     {
     {
-        var item = _libraryManager.GetItemById(itemId);
         userId = RequestHelpers.GetUserId(User, userId);
         userId = RequestHelpers.GetUserId(User, userId);
         var user = userId.IsNullOrEmpty()
         var user = userId.IsNullOrEmpty()
             ? null
             ? null
             : _userManager.GetUserById(userId.Value);
             : _userManager.GetUserById(userId.Value);
+        var item = _libraryManager.GetItemById<BaseItem>(itemId, user);
+        if (item is null)
+        {
+            return NotFound();
+        }
+
         var dtoOptions = new DtoOptions { Fields = fields }
         var dtoOptions = new DtoOptions { Fields = fields }
             .AddClientFields(User)
             .AddClientFields(User)
             .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
             .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
@@ -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="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>
     /// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
     /// <response code="200">Instant playlist returned.</response>
     /// <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>
     /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
     [HttpGet("Items/{itemId}/InstantMix")]
     [HttpGet("Items/{itemId}/InstantMix")]
     [ProducesResponseType(StatusCodes.Status200OK)]
     [ProducesResponseType(StatusCodes.Status200OK)]
+    [ProducesResponseType(StatusCodes.Status404NotFound)]
     public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromItem(
     public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromItem(
         [FromRoute, Required] Guid itemId,
         [FromRoute, Required] Guid itemId,
         [FromQuery] Guid? userId,
         [FromQuery] Guid? userId,
@@ -259,11 +289,16 @@ public class InstantMixController : BaseJellyfinApiController
         [FromQuery] int? imageTypeLimit,
         [FromQuery] int? imageTypeLimit,
         [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
         [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
     {
     {
-        var item = _libraryManager.GetItemById(itemId);
         userId = RequestHelpers.GetUserId(User, userId);
         userId = RequestHelpers.GetUserId(User, userId);
         var user = userId.IsNullOrEmpty()
         var user = userId.IsNullOrEmpty()
             ? null
             ? null
             : _userManager.GetUserById(userId.Value);
             : _userManager.GetUserById(userId.Value);
+        var item = _libraryManager.GetItemById<BaseItem>(itemId, user);
+        if (item is null)
+        {
+            return NotFound();
+        }
+
         var dtoOptions = new DtoOptions { Fields = fields }
         var dtoOptions = new DtoOptions { Fields = fields }
             .AddClientFields(User)
             .AddClientFields(User)
             .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
             .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
@@ -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="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>
     /// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
     /// <response code="200">Instant playlist returned.</response>
     /// <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>
     /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
     [HttpGet("Artists/InstantMix")]
     [HttpGet("Artists/InstantMix")]
     [ProducesResponseType(StatusCodes.Status200OK)]
     [ProducesResponseType(StatusCodes.Status200OK)]
+    [ProducesResponseType(StatusCodes.Status404NotFound)]
     [Obsolete("Use GetInstantMixFromArtists")]
     [Obsolete("Use GetInstantMixFromArtists")]
     public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromArtists2(
     public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromArtists2(
         [FromQuery, Required] Guid id,
         [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="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>
     /// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
     /// <response code="200">Instant playlist returned.</response>
     /// <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>
     /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
     [HttpGet("MusicGenres/InstantMix")]
     [HttpGet("MusicGenres/InstantMix")]
     [ProducesResponseType(StatusCodes.Status200OK)]
     [ProducesResponseType(StatusCodes.Status200OK)]
+    [ProducesResponseType(StatusCodes.Status404NotFound)]
     public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromMusicGenreById(
     public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromMusicGenreById(
         [FromQuery, Required] Guid id,
         [FromQuery, Required] Guid id,
         [FromQuery] Guid? userId,
         [FromQuery] Guid? userId,
@@ -333,11 +372,16 @@ public class InstantMixController : BaseJellyfinApiController
         [FromQuery] int? imageTypeLimit,
         [FromQuery] int? imageTypeLimit,
         [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
         [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
     {
     {
-        var item = _libraryManager.GetItemById(id);
         userId = RequestHelpers.GetUserId(User, userId);
         userId = RequestHelpers.GetUserId(User, userId);
         var user = userId.IsNullOrEmpty()
         var user = userId.IsNullOrEmpty()
             ? null
             ? null
             : _userManager.GetUserById(userId.Value);
             : _userManager.GetUserById(userId.Value);
+        var item = _libraryManager.GetItemById<BaseItem>(id, user);
+        if (item is null)
+        {
+            return NotFound();
+        }
+
         var dtoOptions = new DtoOptions { Fields = fields }
         var dtoOptions = new DtoOptions { Fields = fields }
             .AddClientFields(User)
             .AddClientFields(User)
             .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
             .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);

+ 11 - 2
Jellyfin.Api/Controllers/ItemLookupController.cs

@@ -4,6 +4,8 @@ using System.ComponentModel.DataAnnotations;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 using Jellyfin.Api.Constants;
 using Jellyfin.Api.Constants;
+using Jellyfin.Api.Extensions;
+using Jellyfin.Api.Helpers;
 using MediaBrowser.Common.Api;
 using MediaBrowser.Common.Api;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Entities.Audio;
@@ -64,7 +66,7 @@ public class ItemLookupController : BaseJellyfinApiController
     [ProducesResponseType(StatusCodes.Status404NotFound)]
     [ProducesResponseType(StatusCodes.Status404NotFound)]
     public ActionResult<IEnumerable<ExternalIdInfo>> GetExternalIdInfos([FromRoute, Required] Guid itemId)
     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)
         if (item is null)
         {
         {
             return NotFound();
             return NotFound();
@@ -234,6 +236,7 @@ public class ItemLookupController : BaseJellyfinApiController
     /// <param name="searchResult">The remote search result.</param>
     /// <param name="searchResult">The remote search result.</param>
     /// <param name="replaceAllImages">Optional. Whether or not to replace all images. Default: True.</param>
     /// <param name="replaceAllImages">Optional. Whether or not to replace all images. Default: True.</param>
     /// <response code="204">Item metadata refreshed.</response>
     /// <response code="204">Item metadata refreshed.</response>
+    /// <response code="404">Item not found.</response>
     /// <returns>
     /// <returns>
     /// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results.
     /// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results.
     /// The task result contains an <see cref="NoContentResult"/>.
     /// The task result contains an <see cref="NoContentResult"/>.
@@ -241,12 +244,18 @@ public class ItemLookupController : BaseJellyfinApiController
     [HttpPost("Items/RemoteSearch/Apply/{itemId}")]
     [HttpPost("Items/RemoteSearch/Apply/{itemId}")]
     [Authorize(Policy = Policies.RequiresElevation)]
     [Authorize(Policy = Policies.RequiresElevation)]
     [ProducesResponseType(StatusCodes.Status204NoContent)]
     [ProducesResponseType(StatusCodes.Status204NoContent)]
+    [ProducesResponseType(StatusCodes.Status404NotFound)]
     public async Task<ActionResult> ApplySearchCriteria(
     public async Task<ActionResult> ApplySearchCriteria(
         [FromRoute, Required] Guid itemId,
         [FromRoute, Required] Guid itemId,
         [FromBody, Required] RemoteSearchResult searchResult,
         [FromBody, Required] RemoteSearchResult searchResult,
         [FromQuery] bool replaceAllImages = true)
         [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(
         _logger.LogInformation(
             "Setting provider id's to item {ItemId}-{ItemName}: {@ProviderIds}",
             "Setting provider id's to item {ItemId}-{ItemName}: {@ProviderIds}",
             item.Id,
             item.Id,

+ 4 - 1
Jellyfin.Api/Controllers/ItemRefreshController.cs

@@ -2,7 +2,10 @@ using System;
 using System.ComponentModel;
 using System.ComponentModel;
 using System.ComponentModel.DataAnnotations;
 using System.ComponentModel.DataAnnotations;
 using Jellyfin.Api.Constants;
 using Jellyfin.Api.Constants;
+using Jellyfin.Api.Extensions;
+using Jellyfin.Api.Helpers;
 using MediaBrowser.Common.Api;
 using MediaBrowser.Common.Api;
+using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.IO;
@@ -61,7 +64,7 @@ public class ItemRefreshController : BaseJellyfinApiController
         [FromQuery] bool replaceAllMetadata = false,
         [FromQuery] bool replaceAllMetadata = false,
         [FromQuery] bool replaceAllImages = false)
         [FromQuery] bool replaceAllImages = false)
     {
     {
-        var item = _libraryManager.GetItemById(itemId);
+        var item = _libraryManager.GetItemById<BaseItem>(itemId, User.GetUserId());
         if (item is null)
         if (item is null)
         {
         {
             return NotFound();
             return NotFound();

+ 9 - 3
Jellyfin.Api/Controllers/ItemUpdateController.cs

@@ -5,6 +5,8 @@ using System.Linq;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 using Jellyfin.Api.Constants;
 using Jellyfin.Api.Constants;
+using Jellyfin.Api.Extensions;
+using Jellyfin.Api.Helpers;
 using Jellyfin.Data.Enums;
 using Jellyfin.Data.Enums;
 using MediaBrowser.Common.Api;
 using MediaBrowser.Common.Api;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Configuration;
@@ -72,7 +74,7 @@ public class ItemUpdateController : BaseJellyfinApiController
     [ProducesResponseType(StatusCodes.Status404NotFound)]
     [ProducesResponseType(StatusCodes.Status404NotFound)]
     public async Task<ActionResult> UpdateItem([FromRoute, Required] Guid itemId, [FromBody, Required] BaseItemDto request)
     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)
         if (item is null)
         {
         {
             return NotFound();
             return NotFound();
@@ -145,7 +147,11 @@ public class ItemUpdateController : BaseJellyfinApiController
     [ProducesResponseType(StatusCodes.Status404NotFound)]
     [ProducesResponseType(StatusCodes.Status404NotFound)]
     public ActionResult<MetadataEditorInfo> GetMetadataEditorInfo([FromRoute, Required] Guid itemId)
     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
         var info = new MetadataEditorInfo
         {
         {
@@ -197,7 +203,7 @@ public class ItemUpdateController : BaseJellyfinApiController
     [ProducesResponseType(StatusCodes.Status404NotFound)]
     [ProducesResponseType(StatusCodes.Status404NotFound)]
     public ActionResult UpdateItemContentType([FromRoute, Required] Guid itemId, [FromQuery] string? contentType)
     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)
         if (item is null)
         {
         {
             return NotFound();
             return NotFound();

+ 8 - 4
Jellyfin.Api/Controllers/ItemsController.cs

@@ -967,9 +967,13 @@ public class ItemsController : BaseJellyfinApiController
         }
         }
 
 
         var user = _userManager.GetUserById(requestUserId) ?? throw new ResourceNotFoundException();
         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>
     /// <summary>
@@ -1014,8 +1018,8 @@ public class ItemsController : BaseJellyfinApiController
         }
         }
 
 
         var user = _userManager.GetUserById(requestUserId) ?? throw new ResourceNotFoundException();
         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();
             return NotFound();
         }
         }

+ 32 - 33
Jellyfin.Api/Controllers/LibraryController.cs

@@ -102,7 +102,7 @@ public class LibraryController : BaseJellyfinApiController
     [ProducesFile("video/*", "audio/*")]
     [ProducesFile("video/*", "audio/*")]
     public ActionResult GetFile([FromRoute, Required] Guid itemId)
     public ActionResult GetFile([FromRoute, Required] Guid itemId)
     {
     {
-        var item = _libraryManager.GetItemById(itemId);
+        var item = _libraryManager.GetItemById<BaseItem>(itemId, User.GetUserId());
         if (item is null)
         if (item is null)
         {
         {
             return NotFound();
             return NotFound();
@@ -152,11 +152,10 @@ public class LibraryController : BaseJellyfinApiController
             ? (userId.IsNullOrEmpty()
             ? (userId.IsNullOrEmpty()
                 ? _libraryManager.RootFolder
                 ? _libraryManager.RootFolder
                 : _libraryManager.GetUserRootFolder())
                 : _libraryManager.GetUserRootFolder())
-            : _libraryManager.GetItemById(itemId);
-
+            : _libraryManager.GetItemById<BaseItem>(itemId, user);
         if (item is null)
         if (item is null)
         {
         {
-            return NotFound("Item not found.");
+            return NotFound();
         }
         }
 
 
         IEnumerable<BaseItem> themeItems;
         IEnumerable<BaseItem> themeItems;
@@ -214,16 +213,14 @@ public class LibraryController : BaseJellyfinApiController
         var user = userId.IsNullOrEmpty()
         var user = userId.IsNullOrEmpty()
             ? null
             ? null
             : _userManager.GetUserById(userId.Value);
             : _userManager.GetUserById(userId.Value);
-
         var item = itemId.IsEmpty()
         var item = itemId.IsEmpty()
             ? (userId.IsNullOrEmpty()
             ? (userId.IsNullOrEmpty()
                 ? _libraryManager.RootFolder
                 ? _libraryManager.RootFolder
                 : _libraryManager.GetUserRootFolder())
                 : _libraryManager.GetUserRootFolder())
-            : _libraryManager.GetItemById(itemId);
-
+            : _libraryManager.GetItemById<BaseItem>(itemId, user);
         if (item is null)
         if (item is null)
         {
         {
-            return NotFound("Item not found.");
+            return NotFound();
         }
         }
 
 
         IEnumerable<BaseItem> themeItems;
         IEnumerable<BaseItem> themeItems;
@@ -286,7 +283,8 @@ public class LibraryController : BaseJellyfinApiController
             userId,
             userId,
             inheritFromParent);
             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();
             return NotFound();
         }
         }
@@ -327,6 +325,7 @@ public class LibraryController : BaseJellyfinApiController
     /// <param name="itemId">The item id.</param>
     /// <param name="itemId">The item id.</param>
     /// <response code="204">Item deleted.</response>
     /// <response code="204">Item deleted.</response>
     /// <response code="401">Unauthorized access.</response>
     /// <response code="401">Unauthorized access.</response>
+    /// <response code="404">Item not found.</response>
     /// <returns>A <see cref="NoContentResult"/>.</returns>
     /// <returns>A <see cref="NoContentResult"/>.</returns>
     [HttpDelete("Items/{itemId}")]
     [HttpDelete("Items/{itemId}")]
     [Authorize]
     [Authorize]
@@ -335,17 +334,18 @@ public class LibraryController : BaseJellyfinApiController
     [ProducesResponseType(StatusCodes.Status404NotFound)]
     [ProducesResponseType(StatusCodes.Status404NotFound)]
     public ActionResult DeleteItem(Guid itemId)
     public ActionResult DeleteItem(Guid itemId)
     {
     {
-        var isApiKey = User.GetIsApiKey();
         var userId = User.GetUserId();
         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)
         if (item is null)
         {
         {
             return NotFound();
             return NotFound();
@@ -391,7 +391,7 @@ public class LibraryController : BaseJellyfinApiController
 
 
         foreach (var i in ids)
         foreach (var i in ids)
         {
         {
-            var item = _libraryManager.GetItemById(i);
+            var item = _libraryManager.GetItemById<BaseItem>(i, user);
             if (item is null)
             if (item is null)
             {
             {
                 return NotFound();
                 return NotFound();
@@ -459,20 +459,18 @@ public class LibraryController : BaseJellyfinApiController
     [ProducesResponseType(StatusCodes.Status404NotFound)]
     [ProducesResponseType(StatusCodes.Status404NotFound)]
     public ActionResult<IEnumerable<BaseItemDto>> GetAncestors([FromRoute, Required] Guid itemId, [FromQuery] Guid? userId)
     public ActionResult<IEnumerable<BaseItemDto>> GetAncestors([FromRoute, Required] Guid itemId, [FromQuery] Guid? userId)
     {
     {
-        var item = _libraryManager.GetItemById(itemId);
         userId = RequestHelpers.GetUserId(User, userId);
         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)
         if (item is null)
         {
         {
-            return NotFound("Item not found");
+            return NotFound();
         }
         }
 
 
         var baseItemDtos = new List<BaseItemDto>();
         var baseItemDtos = new List<BaseItemDto>();
 
 
-        var user = userId.IsNullOrEmpty()
-            ? null
-            : _userManager.GetUserById(userId.Value);
-
         var dtoOptions = new DtoOptions().AddClientFields(User);
         var dtoOptions = new DtoOptions().AddClientFields(User);
         BaseItem? parent = item.GetParent();
         BaseItem? parent = item.GetParent();
 
 
@@ -644,14 +642,16 @@ public class LibraryController : BaseJellyfinApiController
     [ProducesFile("video/*", "audio/*")]
     [ProducesFile("video/*", "audio/*")]
     public async Task<ActionResult> GetDownload([FromRoute, Required] Guid itemId)
     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)
         if (item is null)
         {
         {
             return NotFound();
             return NotFound();
         }
         }
 
 
-        var user = _userManager.GetUserById(User.GetUserId());
-
         if (user is not null)
         if (user is not null)
         {
         {
             if (!item.CanDownload(user))
             if (!item.CanDownload(user))
@@ -704,12 +704,14 @@ public class LibraryController : BaseJellyfinApiController
         [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields)
         [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields)
     {
     {
         userId = RequestHelpers.GetUserId(User, userId);
         userId = RequestHelpers.GetUserId(User, userId);
+        var user = userId.IsNullOrEmpty()
+            ? null
+            : _userManager.GetUserById(userId.Value);
         var item = itemId.IsEmpty()
         var item = itemId.IsEmpty()
-            ? (userId.IsNullOrEmpty()
+            ? (user is null
                 ? _libraryManager.RootFolder
                 ? _libraryManager.RootFolder
                 : _libraryManager.GetUserRootFolder())
                 : _libraryManager.GetUserRootFolder())
-            : _libraryManager.GetItemById(itemId);
-
+            : _libraryManager.GetItemById<BaseItem>(itemId, user);
         if (item is null)
         if (item is null)
         {
         {
             return NotFound();
             return NotFound();
@@ -720,9 +722,6 @@ public class LibraryController : BaseJellyfinApiController
             return new QueryResult<BaseItemDto>();
             return new QueryResult<BaseItemDto>();
         }
         }
 
 
-        var user = userId.IsNullOrEmpty()
-            ? null
-            : _userManager.GetUserById(userId.Value);
         var dtoOptions = new DtoOptions { Fields = fields }
         var dtoOptions = new DtoOptions { Fields = fields }
             .AddClientFields(User);
             .AddClientFields(User);
 
 

+ 10 - 2
Jellyfin.Api/Controllers/LibraryStructureController.cs

@@ -6,6 +6,8 @@ using System.IO;
 using System.Linq;
 using System.Linq;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
+using Jellyfin.Api.Extensions;
+using Jellyfin.Api.Helpers;
 using Jellyfin.Api.ModelBinders;
 using Jellyfin.Api.ModelBinders;
 using Jellyfin.Api.Models.LibraryStructureDto;
 using Jellyfin.Api.Models.LibraryStructureDto;
 using MediaBrowser.Common.Api;
 using MediaBrowser.Common.Api;
@@ -311,15 +313,21 @@ public class LibraryStructureController : BaseJellyfinApiController
     /// </summary>
     /// </summary>
     /// <param name="request">The library name and options.</param>
     /// <param name="request">The library name and options.</param>
     /// <response code="204">Library updated.</response>
     /// <response code="204">Library updated.</response>
+    /// <response code="404">Item not found.</response>
     /// <returns>A <see cref="NoContentResult"/>.</returns>
     /// <returns>A <see cref="NoContentResult"/>.</returns>
     [HttpPost("LibraryOptions")]
     [HttpPost("LibraryOptions")]
     [ProducesResponseType(StatusCodes.Status204NoContent)]
     [ProducesResponseType(StatusCodes.Status204NoContent)]
+    [ProducesResponseType(StatusCodes.Status404NotFound)]
     public ActionResult UpdateLibraryOptions(
     public ActionResult UpdateLibraryOptions(
         [FromBody] UpdateLibraryOptionsDto request)
         [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();
         return NoContent();
     }
     }
 }
 }

+ 22 - 5
Jellyfin.Api/Controllers/LiveTvController.cs

@@ -220,9 +220,11 @@ public class LiveTvController : BaseJellyfinApiController
     /// <param name="channelId">Channel id.</param>
     /// <param name="channelId">Channel id.</param>
     /// <param name="userId">Optional. Attach user data.</param>
     /// <param name="userId">Optional. Attach user data.</param>
     /// <response code="200">Live tv channel returned.</response>
     /// <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>
     /// <returns>An <see cref="OkResult"/> containing the live tv channel.</returns>
     [HttpGet("Channels/{channelId}")]
     [HttpGet("Channels/{channelId}")]
     [ProducesResponseType(StatusCodes.Status200OK)]
     [ProducesResponseType(StatusCodes.Status200OK)]
+    [ProducesResponseType(StatusCodes.Status404NotFound)]
     [Authorize(Policy = Policies.LiveTvAccess)]
     [Authorize(Policy = Policies.LiveTvAccess)]
     public ActionResult<BaseItemDto> GetChannel([FromRoute, Required] Guid channelId, [FromQuery] Guid? userId)
     public ActionResult<BaseItemDto> GetChannel([FromRoute, Required] Guid channelId, [FromQuery] Guid? userId)
     {
     {
@@ -232,7 +234,12 @@ public class LiveTvController : BaseJellyfinApiController
             : _userManager.GetUserById(userId.Value);
             : _userManager.GetUserById(userId.Value);
         var item = channelId.IsEmpty()
         var item = channelId.IsEmpty()
             ? _libraryManager.GetUserRootFolder()
             ? _libraryManager.GetUserRootFolder()
-            : _libraryManager.GetItemById(channelId);
+            : _libraryManager.GetItemById<BaseItem>(channelId, user);
+
+        if (item is null)
+        {
+            return NotFound();
+        }
 
 
         var dtoOptions = new DtoOptions()
         var dtoOptions = new DtoOptions()
             .AddClientFields(User);
             .AddClientFields(User);
@@ -416,9 +423,11 @@ public class LiveTvController : BaseJellyfinApiController
     /// <param name="recordingId">Recording id.</param>
     /// <param name="recordingId">Recording id.</param>
     /// <param name="userId">Optional. Attach user data.</param>
     /// <param name="userId">Optional. Attach user data.</param>
     /// <response code="200">Recording returned.</response>
     /// <response code="200">Recording returned.</response>
+    /// <response code="404">Item not found.</response>
     /// <returns>An <see cref="OkResult"/> containing the live tv recording.</returns>
     /// <returns>An <see cref="OkResult"/> containing the live tv recording.</returns>
     [HttpGet("Recordings/{recordingId}")]
     [HttpGet("Recordings/{recordingId}")]
     [ProducesResponseType(StatusCodes.Status200OK)]
     [ProducesResponseType(StatusCodes.Status200OK)]
+    [ProducesResponseType(StatusCodes.Status404NotFound)]
     [Authorize(Policy = Policies.LiveTvAccess)]
     [Authorize(Policy = Policies.LiveTvAccess)]
     public ActionResult<BaseItemDto> GetRecording([FromRoute, Required] Guid recordingId, [FromQuery] Guid? userId)
     public ActionResult<BaseItemDto> GetRecording([FromRoute, Required] Guid recordingId, [FromQuery] Guid? userId)
     {
     {
@@ -426,7 +435,13 @@ public class LiveTvController : BaseJellyfinApiController
         var user = userId.IsNullOrEmpty()
         var user = userId.IsNullOrEmpty()
             ? null
             ? null
             : _userManager.GetUserById(userId.Value);
             : _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()
         var dtoOptions = new DtoOptions()
             .AddClientFields(User);
             .AddClientFields(User);
@@ -611,7 +626,8 @@ public class LiveTvController : BaseJellyfinApiController
         {
         {
             query.IsSeries = true;
             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;
                 query.Name = series.Name;
             }
             }
@@ -665,7 +681,8 @@ public class LiveTvController : BaseJellyfinApiController
         {
         {
             query.IsSeries = true;
             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;
                 query.Name = series.Name;
             }
             }
@@ -779,7 +796,7 @@ public class LiveTvController : BaseJellyfinApiController
     [ProducesResponseType(StatusCodes.Status404NotFound)]
     [ProducesResponseType(StatusCodes.Status404NotFound)]
     public ActionResult DeleteRecording([FromRoute, Required] Guid recordingId)
     public ActionResult DeleteRecording([FromRoute, Required] Guid recordingId)
     {
     {
-        var item = _libraryManager.GetItemById(recordingId);
+        var item = _libraryManager.GetItemById<BaseItem>(recordingId, User.GetUserId());
         if (item is null)
         if (item is null)
         {
         {
             return NotFound();
             return NotFound();

+ 19 - 39
Jellyfin.Api/Controllers/LyricsController.cs

@@ -7,6 +7,7 @@ using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 using Jellyfin.Api.Attributes;
 using Jellyfin.Api.Attributes;
 using Jellyfin.Api.Extensions;
 using Jellyfin.Api.Extensions;
+using Jellyfin.Api.Helpers;
 using Jellyfin.Extensions;
 using Jellyfin.Extensions;
 using MediaBrowser.Common.Api;
 using MediaBrowser.Common.Api;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Entities.Audio;
@@ -66,37 +67,16 @@ public class LyricsController : BaseJellyfinApiController
     [HttpGet("Audio/{itemId}/Lyrics")]
     [HttpGet("Audio/{itemId}/Lyrics")]
     [Authorize]
     [Authorize]
     [ProducesResponseType(StatusCodes.Status200OK)]
     [ProducesResponseType(StatusCodes.Status200OK)]
+    [ProducesResponseType(StatusCodes.Status404NotFound)]
     public async Task<ActionResult<LyricDto>> GetLyrics([FromRoute, Required] Guid itemId)
     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();
             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)
         if (result is not null)
         {
         {
             return Ok(result);
             return Ok(result);
@@ -124,8 +104,8 @@ public class LyricsController : BaseJellyfinApiController
         [FromRoute, Required] Guid itemId,
         [FromRoute, Required] Guid itemId,
         [FromQuery, Required] string fileName)
         [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();
             return NotFound();
         }
         }
@@ -147,7 +127,7 @@ public class LyricsController : BaseJellyfinApiController
         {
         {
             await Request.Body.CopyToAsync(stream).ConfigureAwait(false);
             await Request.Body.CopyToAsync(stream).ConfigureAwait(false);
             var uploadedLyric = await _lyricManager.SaveLyricAsync(
             var uploadedLyric = await _lyricManager.SaveLyricAsync(
-                    audio,
+                    item,
                     format,
                     format,
                     stream)
                     stream)
                 .ConfigureAwait(false);
                 .ConfigureAwait(false);
@@ -157,7 +137,7 @@ public class LyricsController : BaseJellyfinApiController
                 return BadRequest();
                 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);
             return Ok(uploadedLyric);
         }
         }
     }
     }
@@ -176,13 +156,13 @@ public class LyricsController : BaseJellyfinApiController
     public async Task<ActionResult> DeleteLyrics(
     public async Task<ActionResult> DeleteLyrics(
         [FromRoute, Required] Guid itemId)
         [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();
             return NotFound();
         }
         }
 
 
-        await _lyricManager.DeleteLyricsAsync(audio).ConfigureAwait(false);
+        await _lyricManager.DeleteLyricsAsync(item).ConfigureAwait(false);
         return NoContent();
         return NoContent();
     }
     }
 
 
@@ -199,13 +179,13 @@ public class LyricsController : BaseJellyfinApiController
     [ProducesResponseType(StatusCodes.Status404NotFound)]
     [ProducesResponseType(StatusCodes.Status404NotFound)]
     public async Task<ActionResult<IReadOnlyList<RemoteLyricInfoDto>>> SearchRemoteLyrics([FromRoute, Required] Guid itemId)
     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();
             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);
         return Ok(results);
     }
     }
 
 
@@ -225,19 +205,19 @@ public class LyricsController : BaseJellyfinApiController
         [FromRoute, Required] Guid itemId,
         [FromRoute, Required] Guid itemId,
         [FromRoute, Required] string lyricId)
         [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();
             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)
         if (downloadedLyrics is null)
         {
         {
             return NotFound();
             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);
         return Ok(downloadedLyrics);
     }
     }
 
 

+ 33 - 9
Jellyfin.Api/Controllers/MediaInfoController.cs

@@ -8,8 +8,10 @@ using Jellyfin.Api.Attributes;
 using Jellyfin.Api.Extensions;
 using Jellyfin.Api.Extensions;
 using Jellyfin.Api.Helpers;
 using Jellyfin.Api.Helpers;
 using Jellyfin.Api.Models.MediaInfoDtos;
 using Jellyfin.Api.Models.MediaInfoDtos;
+using Jellyfin.Extensions;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Controller.Devices;
 using MediaBrowser.Controller.Devices;
+using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Model.MediaInfo;
 using MediaBrowser.Model.MediaInfo;
 using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Authorization;
@@ -32,6 +34,7 @@ public class MediaInfoController : BaseJellyfinApiController
     private readonly ILibraryManager _libraryManager;
     private readonly ILibraryManager _libraryManager;
     private readonly ILogger<MediaInfoController> _logger;
     private readonly ILogger<MediaInfoController> _logger;
     private readonly MediaInfoHelper _mediaInfoHelper;
     private readonly MediaInfoHelper _mediaInfoHelper;
+    private readonly IUserManager _userManager;
 
 
     /// <summary>
     /// <summary>
     /// Initializes a new instance of the <see cref="MediaInfoController"/> class.
     /// 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="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
     /// <param name="logger">Instance of the <see cref="ILogger{MediaInfoController}"/> 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="mediaInfoHelper">Instance of the <see cref="MediaInfoHelper"/>.</param>
+    /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface..</param>
     public MediaInfoController(
     public MediaInfoController(
         IMediaSourceManager mediaSourceManager,
         IMediaSourceManager mediaSourceManager,
         IDeviceManager deviceManager,
         IDeviceManager deviceManager,
         ILibraryManager libraryManager,
         ILibraryManager libraryManager,
         ILogger<MediaInfoController> logger,
         ILogger<MediaInfoController> logger,
-        MediaInfoHelper mediaInfoHelper)
+        MediaInfoHelper mediaInfoHelper,
+        IUserManager userManager)
     {
     {
         _mediaSourceManager = mediaSourceManager;
         _mediaSourceManager = mediaSourceManager;
         _deviceManager = deviceManager;
         _deviceManager = deviceManager;
         _libraryManager = libraryManager;
         _libraryManager = libraryManager;
         _logger = logger;
         _logger = logger;
         _mediaInfoHelper = mediaInfoHelper;
         _mediaInfoHelper = mediaInfoHelper;
+        _userManager = userManager;
     }
     }
 
 
     /// <summary>
     /// <summary>
@@ -61,16 +67,24 @@ public class MediaInfoController : BaseJellyfinApiController
     /// <param name="itemId">The item id.</param>
     /// <param name="itemId">The item id.</param>
     /// <param name="userId">The user id.</param>
     /// <param name="userId">The user id.</param>
     /// <response code="200">Playback info returned.</response>
     /// <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>
     /// <returns>A <see cref="Task"/> containing a <see cref="PlaybackInfoResponse"/> with the playback information.</returns>
     [HttpGet("Items/{itemId}/PlaybackInfo")]
     [HttpGet("Items/{itemId}/PlaybackInfo")]
     [ProducesResponseType(StatusCodes.Status200OK)]
     [ProducesResponseType(StatusCodes.Status200OK)]
+    [ProducesResponseType(StatusCodes.Status404NotFound)]
     public async Task<ActionResult<PlaybackInfoResponse>> GetPlaybackInfo([FromRoute, Required] Guid itemId, [FromQuery] Guid? userId)
     public async Task<ActionResult<PlaybackInfoResponse>> GetPlaybackInfo([FromRoute, Required] Guid itemId, [FromQuery] Guid? userId)
     {
     {
         userId = RequestHelpers.GetUserId(User, 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>
     /// <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="allowAudioStreamCopy">Whether to allow to copy the audio stream. Default: true.</param>
     /// <param name="playbackInfoDto">The playback info.</param>
     /// <param name="playbackInfoDto">The playback info.</param>
     /// <response code="200">Playback info returned.</response>
     /// <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>
     /// <returns>A <see cref="Task"/> containing a <see cref="PlaybackInfoResponse"/> with the playback info.</returns>
     [HttpPost("Items/{itemId}/PlaybackInfo")]
     [HttpPost("Items/{itemId}/PlaybackInfo")]
     [ProducesResponseType(StatusCodes.Status200OK)]
     [ProducesResponseType(StatusCodes.Status200OK)]
+    [ProducesResponseType(StatusCodes.Status404NotFound)]
     public async Task<ActionResult<PlaybackInfoResponse>> GetPostedPlaybackInfo(
     public async Task<ActionResult<PlaybackInfoResponse>> GetPostedPlaybackInfo(
         [FromRoute, Required] Guid itemId,
         [FromRoute, Required] Guid itemId,
         [FromQuery, ParameterObsolete] Guid? userId,
         [FromQuery, ParameterObsolete] Guid? userId,
@@ -148,9 +164,19 @@ public class MediaInfoController : BaseJellyfinApiController
         allowVideoStreamCopy ??= playbackInfoDto?.AllowVideoStreamCopy ?? true;
         allowVideoStreamCopy ??= playbackInfoDto?.AllowVideoStreamCopy ?? true;
         allowAudioStreamCopy ??= playbackInfoDto?.AllowAudioStreamCopy ?? 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(
         var info = await _mediaInfoHelper.GetPlaybackInfo(
-                itemId,
-                userId,
+                item,
+                user,
                 mediaSourceId,
                 mediaSourceId,
                 liveStreamId)
                 liveStreamId)
             .ConfigureAwait(false);
             .ConfigureAwait(false);
@@ -163,8 +189,6 @@ public class MediaInfoController : BaseJellyfinApiController
         if (profile is not null)
         if (profile is not null)
         {
         {
             // set device specific data
             // set device specific data
-            var item = _libraryManager.GetItemById(itemId);
-
             foreach (var mediaSource in info.MediaSources)
             foreach (var mediaSource in info.MediaSources)
             {
             {
                 _mediaInfoHelper.SetDeviceSpecificData(
                 _mediaInfoHelper.SetDeviceSpecificData(

+ 6 - 1
Jellyfin.Api/Controllers/PlaylistsController.cs

@@ -482,8 +482,13 @@ public class PlaylistsController : BaseJellyfinApiController
         var user = userId.IsNullOrEmpty()
         var user = userId.IsNullOrEmpty()
             ? null
             ? null
             : _userManager.GetUserById(userId.Value);
             : _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;
         var count = items.Length;
         if (startIndex.HasValue)
         if (startIndex.HasValue)
         {
         {

+ 11 - 10
Jellyfin.Api/Controllers/PlaystateController.cs

@@ -6,6 +6,7 @@ using Jellyfin.Api.Extensions;
 using Jellyfin.Api.Helpers;
 using Jellyfin.Api.Helpers;
 using Jellyfin.Api.ModelBinders;
 using Jellyfin.Api.ModelBinders;
 using Jellyfin.Data.Entities;
 using Jellyfin.Data.Entities;
+using Jellyfin.Extensions;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.MediaEncoding;
 using MediaBrowser.Controller.MediaEncoding;
@@ -76,21 +77,21 @@ public class PlaystateController : BaseJellyfinApiController
         [FromRoute, Required] Guid itemId,
         [FromRoute, Required] Guid itemId,
         [FromQuery, ModelBinder(typeof(LegacyDateTimeModelBinder))] DateTime? datePlayed)
         [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)
         if (user is null)
         {
         {
             return NotFound();
             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)
         if (item is null)
         {
         {
             return NotFound();
             return NotFound();
         }
         }
 
 
+        var session = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
+
         var dto = UpdatePlayedStatus(user, item, true, datePlayed);
         var dto = UpdatePlayedStatus(user, item, true, datePlayed);
         foreach (var additionalUserInfo in session.AdditionalUsers)
         foreach (var additionalUserInfo in session.AdditionalUsers)
         {
         {
@@ -141,21 +142,21 @@ public class PlaystateController : BaseJellyfinApiController
         [FromQuery] Guid? userId,
         [FromQuery] Guid? userId,
         [FromRoute, Required] Guid itemId)
         [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)
         if (user is null)
         {
         {
             return NotFound();
             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)
         if (item is null)
         {
         {
             return NotFound();
             return NotFound();
         }
         }
 
 
+        var session = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
+
         var dto = UpdatePlayedStatus(user, item, false, null);
         var dto = UpdatePlayedStatus(user, item, false, null);
         foreach (var additionalUserInfo in session.AdditionalUsers)
         foreach (var additionalUserInfo in session.AdditionalUsers)
         {
         {

+ 6 - 3
Jellyfin.Api/Controllers/RemoteImageController.cs

@@ -6,8 +6,11 @@ using System.Linq;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 using Jellyfin.Api.Constants;
 using Jellyfin.Api.Constants;
+using Jellyfin.Api.Extensions;
+using Jellyfin.Api.Helpers;
 using MediaBrowser.Common.Api;
 using MediaBrowser.Common.Api;
 using MediaBrowser.Controller;
 using MediaBrowser.Controller;
+using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Entities;
@@ -68,7 +71,7 @@ public class RemoteImageController : BaseJellyfinApiController
         [FromQuery] string? providerName,
         [FromQuery] string? providerName,
         [FromQuery] bool includeAllLanguages = false)
         [FromQuery] bool includeAllLanguages = false)
     {
     {
-        var item = _libraryManager.GetItemById(itemId);
+        var item = _libraryManager.GetItemById<BaseItem>(itemId, User.GetUserId());
         if (item is null)
         if (item is null)
         {
         {
             return NotFound();
             return NotFound();
@@ -127,7 +130,7 @@ public class RemoteImageController : BaseJellyfinApiController
     [ProducesResponseType(StatusCodes.Status404NotFound)]
     [ProducesResponseType(StatusCodes.Status404NotFound)]
     public ActionResult<IEnumerable<ImageProviderInfo>> GetRemoteImageProviders([FromRoute, Required] Guid itemId)
     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)
         if (item is null)
         {
         {
             return NotFound();
             return NotFound();
@@ -154,7 +157,7 @@ public class RemoteImageController : BaseJellyfinApiController
         [FromQuery, Required] ImageType type,
         [FromQuery, Required] ImageType type,
         [FromQuery] string? imageUrl)
         [FromQuery] string? imageUrl)
     {
     {
-        var item = _libraryManager.GetItemById(itemId);
+        var item = _libraryManager.GetItemById<BaseItem>(itemId, User.GetUserId());
         if (item is null)
         if (item is null)
         {
         {
             return NotFound();
             return NotFound();

+ 1 - 1
Jellyfin.Api/Controllers/SearchController.cs

@@ -211,7 +211,7 @@ public class SearchController : BaseJellyfinApiController
 
 
         if (!item.ChannelId.IsEmpty())
         if (!item.ChannelId.IsEmpty())
         {
         {
-            var channel = _libraryManager.GetItemById(item.ChannelId);
+            var channel = _libraryManager.GetItemById<BaseItem>(item.ChannelId);
             result.ChannelName = channel?.Name;
             result.ChannelName = channel?.Name;
         }
         }
 
 

+ 37 - 13
Jellyfin.Api/Controllers/SubtitleController.cs

@@ -12,6 +12,7 @@ using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 using Jellyfin.Api.Attributes;
 using Jellyfin.Api.Attributes;
 using Jellyfin.Api.Extensions;
 using Jellyfin.Api.Extensions;
+using Jellyfin.Api.Helpers;
 using Jellyfin.Api.Models.SubtitleDtos;
 using Jellyfin.Api.Models.SubtitleDtos;
 using MediaBrowser.Common.Api;
 using MediaBrowser.Common.Api;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Configuration;
@@ -95,8 +96,7 @@ public class SubtitleController : BaseJellyfinApiController
         [FromRoute, Required] Guid itemId,
         [FromRoute, Required] Guid itemId,
         [FromRoute, Required] int index)
         [FromRoute, Required] int index)
     {
     {
-        var item = _libraryManager.GetItemById(itemId);
-
+        var item = _libraryManager.GetItemById<BaseItem>(itemId, User.GetUserId());
         if (item is null)
         if (item is null)
         {
         {
             return NotFound();
             return NotFound();
@@ -113,18 +113,24 @@ public class SubtitleController : BaseJellyfinApiController
     /// <param name="language">The language of the subtitles.</param>
     /// <param name="language">The language of the subtitles.</param>
     /// <param name="isPerfectMatch">Optional. Only show subtitles which are a perfect match.</param>
     /// <param name="isPerfectMatch">Optional. Only show subtitles which are a perfect match.</param>
     /// <response code="200">Subtitles retrieved.</response>
     /// <response code="200">Subtitles retrieved.</response>
+    /// <response code="404">Item not found.</response>
     /// <returns>An array of <see cref="RemoteSubtitleInfo"/>.</returns>
     /// <returns>An array of <see cref="RemoteSubtitleInfo"/>.</returns>
     [HttpGet("Items/{itemId}/RemoteSearch/Subtitles/{language}")]
     [HttpGet("Items/{itemId}/RemoteSearch/Subtitles/{language}")]
     [Authorize(Policy = Policies.SubtitleManagement)]
     [Authorize(Policy = Policies.SubtitleManagement)]
     [ProducesResponseType(StatusCodes.Status200OK)]
     [ProducesResponseType(StatusCodes.Status200OK)]
+    [ProducesResponseType(StatusCodes.Status404NotFound)]
     public async Task<ActionResult<IEnumerable<RemoteSubtitleInfo>>> SearchRemoteSubtitles(
     public async Task<ActionResult<IEnumerable<RemoteSubtitleInfo>>> SearchRemoteSubtitles(
         [FromRoute, Required] Guid itemId,
         [FromRoute, Required] Guid itemId,
         [FromRoute, Required] string language,
         [FromRoute, Required] string language,
         [FromQuery] bool? isPerfectMatch)
         [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>
     /// <summary>
@@ -133,22 +139,28 @@ public class SubtitleController : BaseJellyfinApiController
     /// <param name="itemId">The item id.</param>
     /// <param name="itemId">The item id.</param>
     /// <param name="subtitleId">The subtitle id.</param>
     /// <param name="subtitleId">The subtitle id.</param>
     /// <response code="204">Subtitle downloaded.</response>
     /// <response code="204">Subtitle downloaded.</response>
+    /// <response code="404">Item not found.</response>
     /// <returns>A <see cref="NoContentResult"/>.</returns>
     /// <returns>A <see cref="NoContentResult"/>.</returns>
     [HttpPost("Items/{itemId}/RemoteSearch/Subtitles/{subtitleId}")]
     [HttpPost("Items/{itemId}/RemoteSearch/Subtitles/{subtitleId}")]
     [Authorize(Policy = Policies.SubtitleManagement)]
     [Authorize(Policy = Policies.SubtitleManagement)]
     [ProducesResponseType(StatusCodes.Status204NoContent)]
     [ProducesResponseType(StatusCodes.Status204NoContent)]
+    [ProducesResponseType(StatusCodes.Status404NotFound)]
     public async Task<ActionResult> DownloadRemoteSubtitles(
     public async Task<ActionResult> DownloadRemoteSubtitles(
         [FromRoute, Required] Guid itemId,
         [FromRoute, Required] Guid itemId,
         [FromRoute, Required] string subtitleId)
         [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
         try
         {
         {
-            await _subtitleManager.DownloadSubtitles(video, subtitleId, CancellationToken.None)
+            await _subtitleManager.DownloadSubtitles(item, subtitleId, CancellationToken.None)
                 .ConfigureAwait(false);
                 .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)
         catch (Exception ex)
         {
         {
@@ -223,7 +235,7 @@ public class SubtitleController : BaseJellyfinApiController
 
 
         if (string.IsNullOrEmpty(format))
         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 idString = itemId.Value.ToString("N", CultureInfo.InvariantCulture);
             var mediaSource = _mediaSourceManager.GetStaticMediaSources(item, false)
             var mediaSource = _mediaSourceManager.GetStaticMediaSources(item, false)
@@ -321,10 +333,12 @@ public class SubtitleController : BaseJellyfinApiController
     /// <param name="mediaSourceId">The media source id.</param>
     /// <param name="mediaSourceId">The media source id.</param>
     /// <param name="segmentLength">The subtitle segment length.</param>
     /// <param name="segmentLength">The subtitle segment length.</param>
     /// <response code="200">Subtitle playlist retrieved.</response>
     /// <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>
     /// <returns>A <see cref="FileContentResult"/> with the HLS subtitle playlist.</returns>
     [HttpGet("Videos/{itemId}/{mediaSourceId}/Subtitles/{index}/subtitles.m3u8")]
     [HttpGet("Videos/{itemId}/{mediaSourceId}/Subtitles/{index}/subtitles.m3u8")]
     [Authorize]
     [Authorize]
     [ProducesResponseType(StatusCodes.Status200OK)]
     [ProducesResponseType(StatusCodes.Status200OK)]
+    [ProducesResponseType(StatusCodes.Status404NotFound)]
     [ProducesPlaylistFile]
     [ProducesPlaylistFile]
     [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]
     [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]
     public async Task<ActionResult> GetSubtitlePlaylist(
     public async Task<ActionResult> GetSubtitlePlaylist(
@@ -333,7 +347,11 @@ public class SubtitleController : BaseJellyfinApiController
         [FromRoute, Required] string mediaSourceId,
         [FromRoute, Required] string mediaSourceId,
         [FromQuery, Required] int segmentLength)
         [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);
         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="itemId">The item the subtitle belongs to.</param>
     /// <param name="body">The request body.</param>
     /// <param name="body">The request body.</param>
     /// <response code="204">Subtitle uploaded.</response>
     /// <response code="204">Subtitle uploaded.</response>
+    /// <response code="404">Item not found.</response>
     /// <returns>A <see cref="NoContentResult"/>.</returns>
     /// <returns>A <see cref="NoContentResult"/>.</returns>
     [HttpPost("Videos/{itemId}/Subtitles")]
     [HttpPost("Videos/{itemId}/Subtitles")]
     [Authorize(Policy = Policies.SubtitleManagement)]
     [Authorize(Policy = Policies.SubtitleManagement)]
     [ProducesResponseType(StatusCodes.Status204NoContent)]
     [ProducesResponseType(StatusCodes.Status204NoContent)]
+    [ProducesResponseType(StatusCodes.Status404NotFound)]
     public async Task<ActionResult> UploadSubtitle(
     public async Task<ActionResult> UploadSubtitle(
         [FromRoute, Required] Guid itemId,
         [FromRoute, Required] Guid itemId,
         [FromBody, Required] UploadSubtitleDto body)
         [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 bytes = Encoding.UTF8.GetBytes(body.Data);
         var memoryStream = new MemoryStream(bytes, 0, bytes.Length, false, true);
         var memoryStream = new MemoryStream(bytes, 0, bytes.Length, false, true);
@@ -416,7 +440,7 @@ public class SubtitleController : BaseJellyfinApiController
             await using (stream.ConfigureAwait(false))
             await using (stream.ConfigureAwait(false))
             {
             {
                 await _subtitleManager.UploadSubtitle(
                 await _subtitleManager.UploadSubtitle(
-                    video,
+                    item,
                     new SubtitleResponse
                     new SubtitleResponse
                     {
                     {
                         Format = body.Format,
                         Format = body.Format,
@@ -425,7 +449,7 @@ public class SubtitleController : BaseJellyfinApiController
                         IsHearingImpaired = body.IsHearingImpaired,
                         IsHearingImpaired = body.IsHearingImpaired,
                         Stream = stream
                         Stream = stream
                     }).ConfigureAwait(false);
                     }).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();
                 return NoContent();
             }
             }
@@ -452,7 +476,7 @@ public class SubtitleController : BaseJellyfinApiController
         long? endPositionTicks,
         long? endPositionTicks,
         bool copyTimestamps)
         bool copyTimestamps)
     {
     {
-        var item = _libraryManager.GetItemById(id);
+        var item = _libraryManager.GetItemById<BaseItem>(id);
 
 
         return _subtitleEncoder.GetSubtitles(
         return _subtitleEncoder.GetSubtitles(
             item,
             item,

+ 3 - 1
Jellyfin.Api/Controllers/TrickplayController.cs

@@ -5,6 +5,8 @@ using System.Text;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 using Jellyfin.Api.Attributes;
 using Jellyfin.Api.Attributes;
 using Jellyfin.Api.Extensions;
 using Jellyfin.Api.Extensions;
+using Jellyfin.Api.Helpers;
+using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Trickplay;
 using MediaBrowser.Controller.Trickplay;
 using MediaBrowser.Model;
 using MediaBrowser.Model;
@@ -84,7 +86,7 @@ public class TrickplayController : BaseJellyfinApiController
         [FromRoute, Required] int index,
         [FromRoute, Required] int index,
         [FromQuery] Guid? mediaSourceId)
         [FromQuery] Guid? mediaSourceId)
     {
     {
-        var item = _libraryManager.GetItemById(mediaSourceId ?? itemId);
+        var item = _libraryManager.GetItemById<BaseItem>(itemId, User.GetUserId());
         if (item is null)
         if (item is null)
         {
         {
             return NotFound();
             return NotFound();

+ 8 - 7
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.
         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)
             if (item is not Season seasonItem)
             {
             {
                 return NotFound("No season exists with Id " + seasonId);
                 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
         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");
                 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.
         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");
                 return NotFound("Series not found");
             }
             }
@@ -342,13 +343,13 @@ public class TvShowsController : BaseJellyfinApiController
         var user = userId.IsNullOrEmpty()
         var user = userId.IsNullOrEmpty()
             ? null
             ? null
             : _userManager.GetUserById(userId.Value);
             : _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,
             IsMissing = isMissing,
             IsSpecialSeason = isSpecialSeason,
             IsSpecialSeason = isSpecialSeason,

+ 21 - 6
Jellyfin.Api/Controllers/UniversalAudioController.cs

@@ -9,7 +9,9 @@ using Jellyfin.Api.Helpers;
 using Jellyfin.Api.ModelBinders;
 using Jellyfin.Api.ModelBinders;
 using Jellyfin.Api.Models.StreamingDtos;
 using Jellyfin.Api.Models.StreamingDtos;
 using Jellyfin.Data.Enums;
 using Jellyfin.Data.Enums;
+using Jellyfin.Extensions;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Extensions;
+using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.MediaEncoding;
 using MediaBrowser.Controller.MediaEncoding;
 using MediaBrowser.Controller.Streaming;
 using MediaBrowser.Controller.Streaming;
@@ -33,6 +35,7 @@ public class UniversalAudioController : BaseJellyfinApiController
     private readonly MediaInfoHelper _mediaInfoHelper;
     private readonly MediaInfoHelper _mediaInfoHelper;
     private readonly AudioHelper _audioHelper;
     private readonly AudioHelper _audioHelper;
     private readonly DynamicHlsHelper _dynamicHlsHelper;
     private readonly DynamicHlsHelper _dynamicHlsHelper;
+    private readonly IUserManager _userManager;
 
 
     /// <summary>
     /// <summary>
     /// Initializes a new instance of the <see cref="UniversalAudioController"/> class.
     /// 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="mediaInfoHelper">Instance of <see cref="MediaInfoHelper"/>.</param>
     /// <param name="audioHelper">Instance of <see cref="AudioHelper"/>.</param>
     /// <param name="audioHelper">Instance of <see cref="AudioHelper"/>.</param>
     /// <param name="dynamicHlsHelper">Instance of <see cref="DynamicHlsHelper"/>.</param>
     /// <param name="dynamicHlsHelper">Instance of <see cref="DynamicHlsHelper"/>.</param>
+    /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
     public UniversalAudioController(
     public UniversalAudioController(
         ILibraryManager libraryManager,
         ILibraryManager libraryManager,
         ILogger<UniversalAudioController> logger,
         ILogger<UniversalAudioController> logger,
         MediaInfoHelper mediaInfoHelper,
         MediaInfoHelper mediaInfoHelper,
         AudioHelper audioHelper,
         AudioHelper audioHelper,
-        DynamicHlsHelper dynamicHlsHelper)
+        DynamicHlsHelper dynamicHlsHelper,
+        IUserManager userManager)
     {
     {
         _libraryManager = libraryManager;
         _libraryManager = libraryManager;
         _logger = logger;
         _logger = logger;
         _mediaInfoHelper = mediaInfoHelper;
         _mediaInfoHelper = mediaInfoHelper;
         _audioHelper = audioHelper;
         _audioHelper = audioHelper;
         _dynamicHlsHelper = dynamicHlsHelper;
         _dynamicHlsHelper = dynamicHlsHelper;
+        _userManager = userManager;
     }
     }
 
 
     /// <summary>
     /// <summary>
@@ -79,12 +85,14 @@ public class UniversalAudioController : BaseJellyfinApiController
     /// <param name="enableRedirection">Whether to enable redirection. Defaults to true.</param>
     /// <param name="enableRedirection">Whether to enable redirection. Defaults to true.</param>
     /// <response code="200">Audio stream returned.</response>
     /// <response code="200">Audio stream returned.</response>
     /// <response code="302">Redirected to remote audio stream.</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>
     /// <returns>A <see cref="Task"/> containing the audio file.</returns>
     [HttpGet("Audio/{itemId}/universal")]
     [HttpGet("Audio/{itemId}/universal")]
     [HttpHead("Audio/{itemId}/universal", Name = "HeadUniversalAudioStream")]
     [HttpHead("Audio/{itemId}/universal", Name = "HeadUniversalAudioStream")]
     [Authorize]
     [Authorize]
     [ProducesResponseType(StatusCodes.Status200OK)]
     [ProducesResponseType(StatusCodes.Status200OK)]
     [ProducesResponseType(StatusCodes.Status302Found)]
     [ProducesResponseType(StatusCodes.Status302Found)]
+    [ProducesResponseType(StatusCodes.Status404NotFound)]
     [ProducesAudioFile]
     [ProducesAudioFile]
     public async Task<ActionResult> GetUniversalAudioStream(
     public async Task<ActionResult> GetUniversalAudioStream(
         [FromRoute, Required] Guid itemId,
         [FromRoute, Required] Guid itemId,
@@ -106,20 +114,27 @@ public class UniversalAudioController : BaseJellyfinApiController
         [FromQuery] bool breakOnNonKeyFrames = false,
         [FromQuery] bool breakOnNonKeyFrames = false,
         [FromQuery] bool enableRedirection = true)
         [FromQuery] bool enableRedirection = true)
     {
     {
-        var deviceProfile = GetDeviceProfile(container, transcodingContainer, audioCodec, transcodingProtocol, breakOnNonKeyFrames, transcodingAudioChannels, maxAudioSampleRate, maxAudioBitDepth, maxAudioChannels);
         userId = RequestHelpers.GetUserId(User, userId);
         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);
         _logger.LogInformation("GetPostedPlaybackInfo profile: {@Profile}", deviceProfile);
 
 
         var info = await _mediaInfoHelper.GetPlaybackInfo(
         var info = await _mediaInfoHelper.GetPlaybackInfo(
-                itemId,
-                userId,
+                item,
+                user,
                 mediaSourceId)
                 mediaSourceId)
             .ConfigureAwait(false);
             .ConfigureAwait(false);
 
 
         // set device specific data
         // set device specific data
-        var item = _libraryManager.GetItemById(itemId);
-
         foreach (var sourceInfo in info.MediaSources)
         foreach (var sourceInfo in info.MediaSources)
         {
         {
             _mediaInfoHelper.SetDeviceSpecificData(
             _mediaInfoHelper.SetDeviceSpecificData(

+ 26 - 90
Jellyfin.Api/Controllers/UserLibraryController.cs

@@ -77,8 +77,8 @@ public class UserLibraryController : BaseJellyfinApiController
         [FromQuery] Guid? userId,
         [FromQuery] Guid? userId,
         [FromRoute, Required] Guid itemId)
         [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)
         if (user is null)
         {
         {
             return NotFound();
             return NotFound();
@@ -86,20 +86,12 @@ public class UserLibraryController : BaseJellyfinApiController
 
 
         var item = itemId.IsEmpty()
         var item = itemId.IsEmpty()
             ? _libraryManager.GetUserRootFolder()
             ? _libraryManager.GetUserRootFolder()
-            : _libraryManager.GetItemById(itemId);
-
+            : _libraryManager.GetItemById<BaseItem>(itemId, user);
         if (item is null)
         if (item is null)
         {
         {
             return NotFound();
             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);
         await RefreshItemOnDemandIfNeeded(item).ConfigureAwait(false);
 
 
         var dtoOptions = new DtoOptions().AddClientFields(User);
         var dtoOptions = new DtoOptions().AddClientFields(User);
@@ -133,8 +125,8 @@ public class UserLibraryController : BaseJellyfinApiController
     [ProducesResponseType(StatusCodes.Status200OK)]
     [ProducesResponseType(StatusCodes.Status200OK)]
     public ActionResult<BaseItemDto> GetRootFolder([FromQuery] Guid? userId)
     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)
         if (user is null)
         {
         {
             return NotFound();
             return NotFound();
@@ -172,8 +164,8 @@ public class UserLibraryController : BaseJellyfinApiController
         [FromQuery] Guid? userId,
         [FromQuery] Guid? userId,
         [FromRoute, Required] Guid itemId)
         [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)
         if (user is null)
         {
         {
             return NotFound();
             return NotFound();
@@ -181,20 +173,12 @@ public class UserLibraryController : BaseJellyfinApiController
 
 
         var item = itemId.IsEmpty()
         var item = itemId.IsEmpty()
             ? _libraryManager.GetUserRootFolder()
             ? _libraryManager.GetUserRootFolder()
-            : _libraryManager.GetItemById(itemId);
-
+            : _libraryManager.GetItemById<BaseItem>(itemId, user);
         if (item is null)
         if (item is null)
         {
         {
             return NotFound();
             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 items = await _libraryManager.GetIntros(item, user).ConfigureAwait(false);
         var dtoOptions = new DtoOptions().AddClientFields(User);
         var dtoOptions = new DtoOptions().AddClientFields(User);
         var dtos = items.Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user)).ToArray();
         var dtos = items.Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user)).ToArray();
@@ -231,8 +215,8 @@ public class UserLibraryController : BaseJellyfinApiController
         [FromQuery] Guid? userId,
         [FromQuery] Guid? userId,
         [FromRoute, Required] Guid itemId)
         [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)
         if (user is null)
         {
         {
             return NotFound();
             return NotFound();
@@ -240,20 +224,12 @@ public class UserLibraryController : BaseJellyfinApiController
 
 
         var item = itemId.IsEmpty()
         var item = itemId.IsEmpty()
             ? _libraryManager.GetUserRootFolder()
             ? _libraryManager.GetUserRootFolder()
-            : _libraryManager.GetItemById(itemId);
-
+            : _libraryManager.GetItemById<BaseItem>(itemId, user);
         if (item is null)
         if (item is null)
         {
         {
             return NotFound();
             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);
         return MarkFavorite(user, item, true);
     }
     }
 
 
@@ -286,8 +262,8 @@ public class UserLibraryController : BaseJellyfinApiController
         [FromQuery] Guid? userId,
         [FromQuery] Guid? userId,
         [FromRoute, Required] Guid itemId)
         [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)
         if (user is null)
         {
         {
             return NotFound();
             return NotFound();
@@ -295,20 +271,12 @@ public class UserLibraryController : BaseJellyfinApiController
 
 
         var item = itemId.IsEmpty()
         var item = itemId.IsEmpty()
             ? _libraryManager.GetUserRootFolder()
             ? _libraryManager.GetUserRootFolder()
-            : _libraryManager.GetItemById(itemId);
-
+            : _libraryManager.GetItemById<BaseItem>(itemId, user);
         if (item is null)
         if (item is null)
         {
         {
             return NotFound();
             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);
         return MarkFavorite(user, item, false);
     }
     }
 
 
@@ -341,8 +309,8 @@ public class UserLibraryController : BaseJellyfinApiController
         [FromQuery] Guid? userId,
         [FromQuery] Guid? userId,
         [FromRoute, Required] Guid itemId)
         [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)
         if (user is null)
         {
         {
             return NotFound();
             return NotFound();
@@ -350,20 +318,12 @@ public class UserLibraryController : BaseJellyfinApiController
 
 
         var item = itemId.IsEmpty()
         var item = itemId.IsEmpty()
             ? _libraryManager.GetUserRootFolder()
             ? _libraryManager.GetUserRootFolder()
-            : _libraryManager.GetItemById(itemId);
-
+            : _libraryManager.GetItemById<BaseItem>(itemId, user);
         if (item is null)
         if (item is null)
         {
         {
             return NotFound();
             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);
         return UpdateUserItemRatingInternal(user, item, null);
     }
     }
 
 
@@ -398,8 +358,8 @@ public class UserLibraryController : BaseJellyfinApiController
         [FromRoute, Required] Guid itemId,
         [FromRoute, Required] Guid itemId,
         [FromQuery] bool? likes)
         [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)
         if (user is null)
         {
         {
             return NotFound();
             return NotFound();
@@ -407,20 +367,12 @@ public class UserLibraryController : BaseJellyfinApiController
 
 
         var item = itemId.IsEmpty()
         var item = itemId.IsEmpty()
             ? _libraryManager.GetUserRootFolder()
             ? _libraryManager.GetUserRootFolder()
-            : _libraryManager.GetItemById(itemId);
-
+            : _libraryManager.GetItemById<BaseItem>(itemId, user);
         if (item is null)
         if (item is null)
         {
         {
             return NotFound();
             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);
         return UpdateUserItemRatingInternal(user, item, likes);
     }
     }
 
 
@@ -455,8 +407,8 @@ public class UserLibraryController : BaseJellyfinApiController
         [FromQuery] Guid? userId,
         [FromQuery] Guid? userId,
         [FromRoute, Required] Guid itemId)
         [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)
         if (user is null)
         {
         {
             return NotFound();
             return NotFound();
@@ -464,20 +416,12 @@ public class UserLibraryController : BaseJellyfinApiController
 
 
         var item = itemId.IsEmpty()
         var item = itemId.IsEmpty()
             ? _libraryManager.GetUserRootFolder()
             ? _libraryManager.GetUserRootFolder()
-            : _libraryManager.GetItemById(itemId);
-
+            : _libraryManager.GetItemById<BaseItem>(itemId, user);
         if (item is null)
         if (item is null)
         {
         {
             return NotFound();
             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);
         var dtoOptions = new DtoOptions().AddClientFields(User);
         if (item is IHasTrailers hasTrailers)
         if (item is IHasTrailers hasTrailers)
         {
         {
@@ -519,8 +463,8 @@ public class UserLibraryController : BaseJellyfinApiController
         [FromQuery] Guid? userId,
         [FromQuery] Guid? userId,
         [FromRoute, Required] Guid itemId)
         [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)
         if (user is null)
         {
         {
             return NotFound();
             return NotFound();
@@ -528,20 +472,12 @@ public class UserLibraryController : BaseJellyfinApiController
 
 
         var item = itemId.IsEmpty()
         var item = itemId.IsEmpty()
             ? _libraryManager.GetUserRootFolder()
             ? _libraryManager.GetUserRootFolder()
-            : _libraryManager.GetItemById(itemId);
-
+            : _libraryManager.GetItemById<BaseItem>(itemId, user);
         if (item is null)
         if (item is null)
         {
         {
             return NotFound();
             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);
         var dtoOptions = new DtoOptions().AddClientFields(User);
 
 
         return Ok(item
         return Ok(item

+ 4 - 1
Jellyfin.Api/Controllers/VideoAttachmentsController.cs

@@ -4,7 +4,10 @@ using System.Net.Mime;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 using Jellyfin.Api.Attributes;
 using Jellyfin.Api.Attributes;
+using Jellyfin.Api.Extensions;
+using Jellyfin.Api.Helpers;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Extensions;
+using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.MediaEncoding;
 using MediaBrowser.Controller.MediaEncoding;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Http;
@@ -54,7 +57,7 @@ public class VideoAttachmentsController : BaseJellyfinApiController
     {
     {
         try
         try
         {
         {
-            var item = _libraryManager.GetItemById(videoId);
+            var item = _libraryManager.GetItemById<BaseItem>(videoId, User.GetUserId());
             if (item is null)
             if (item is null)
             {
             {
                 return NotFound();
                 return NotFound();

+ 17 - 14
Jellyfin.Api/Controllers/VideosController.cs

@@ -7,7 +7,6 @@ using System.Net.Http;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 using Jellyfin.Api.Attributes;
 using Jellyfin.Api.Attributes;
-using Jellyfin.Api.Constants;
 using Jellyfin.Api.Extensions;
 using Jellyfin.Api.Extensions;
 using Jellyfin.Api.Helpers;
 using Jellyfin.Api.Helpers;
 using Jellyfin.Api.ModelBinders;
 using Jellyfin.Api.ModelBinders;
@@ -105,7 +104,11 @@ public class VideosController : BaseJellyfinApiController
             ? (userId.IsNullOrEmpty()
             ? (userId.IsNullOrEmpty()
                 ? _libraryManager.RootFolder
                 ? _libraryManager.RootFolder
                 : _libraryManager.GetUserRootFolder())
                 : _libraryManager.GetUserRootFolder())
-            : _libraryManager.GetItemById(itemId);
+            : _libraryManager.GetItemById<BaseItem>(itemId, user);
+        if (item is null)
+        {
+            return NotFound();
+        }
 
 
         var dtoOptions = new DtoOptions();
         var dtoOptions = new DtoOptions();
         dtoOptions = dtoOptions.AddClientFields(User);
         dtoOptions = dtoOptions.AddClientFields(User);
@@ -139,24 +142,23 @@ public class VideosController : BaseJellyfinApiController
     [ProducesResponseType(StatusCodes.Status404NotFound)]
     [ProducesResponseType(StatusCodes.Status404NotFound)]
     public async Task<ActionResult> DeleteAlternateSources([FromRoute, Required] Guid itemId)
     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();
             return NotFound();
         }
         }
 
 
-        foreach (var link in video.GetLinkedAlternateVersions())
+        foreach (var link in item.GetLinkedAlternateVersions())
         {
         {
             link.SetPrimaryVersionId(null);
             link.SetPrimaryVersionId(null);
             link.LinkedAlternateVersions = Array.Empty<LinkedChild>();
             link.LinkedAlternateVersions = Array.Empty<LinkedChild>();
@@ -164,9 +166,9 @@ public class VideosController : BaseJellyfinApiController
             await link.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
             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();
         return NoContent();
     }
     }
@@ -184,8 +186,9 @@ public class VideosController : BaseJellyfinApiController
     [ProducesResponseType(StatusCodes.Status400BadRequest)]
     [ProducesResponseType(StatusCodes.Status400BadRequest)]
     public async Task<ActionResult> MergeVersions([FromQuery, Required, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids)
     public async Task<ActionResult> MergeVersions([FromQuery, Required, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids)
     {
     {
+        var userId = User.GetUserId();
         var items = ids
         var items = ids
-            .Select(i => _libraryManager.GetItemById(i))
+            .Select(i => _libraryManager.GetItemById<BaseItem>(i, userId))
             .OfType<Video>()
             .OfType<Video>()
             .OrderBy(i => i.Id)
             .OrderBy(i => i.Id)
             .ToList();
             .ToList();

+ 5 - 9
Jellyfin.Api/Helpers/MediaInfoHelper.cs

@@ -76,21 +76,17 @@ public class MediaInfoHelper
     /// <summary>
     /// <summary>
     /// Get playback info.
     /// Get playback info.
     /// </summary>
     /// </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="mediaSourceId">Media source id.</param>
     /// <param name="liveStreamId">Live stream id.</param>
     /// <param name="liveStreamId">Live stream id.</param>
     /// <returns>A <see cref="Task"/> containing the <see cref="PlaybackInfoResponse"/>.</returns>
     /// <returns>A <see cref="Task"/> containing the <see cref="PlaybackInfoResponse"/>.</returns>
     public async Task<PlaybackInfoResponse> GetPlaybackInfo(
     public async Task<PlaybackInfoResponse> GetPlaybackInfo(
-        Guid id,
-        Guid? userId,
+        BaseItem item,
+        User? user,
         string? mediaSourceId = null,
         string? mediaSourceId = null,
         string? liveStreamId = null)
         string? liveStreamId = null)
     {
     {
-        var user = userId.IsNullOrEmpty()
-            ? null
-            : _userManager.GetUserById(userId.Value);
-        var item = _libraryManager.GetItemById(id);
         var result = new PlaybackInfoResponse();
         var result = new PlaybackInfoResponse();
 
 
         MediaSourceInfo[] mediaSources;
         MediaSourceInfo[] mediaSources;
@@ -402,7 +398,7 @@ public class MediaInfoHelper
 
 
         if (profile is not null)
         if (profile is not null)
         {
         {
-            var item = _libraryManager.GetItemById(request.ItemId);
+            var item = _libraryManager.GetItemById<BaseItem>(request.ItemId);
 
 
             SetDeviceSpecificData(
             SetDeviceSpecificData(
                 item,
                 item,

+ 3 - 2
Jellyfin.Api/Helpers/StreamingHelpers.cs

@@ -11,6 +11,7 @@ using Jellyfin.Extensions;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.MediaEncoding;
 using MediaBrowser.Controller.MediaEncoding;
 using MediaBrowser.Controller.Streaming;
 using MediaBrowser.Controller.Streaming;
@@ -107,7 +108,7 @@ public static class StreamingHelpers
                                           ?? state.SupportedSubtitleCodecs.FirstOrDefault();
                                           ?? state.SupportedSubtitleCodecs.FirstOrDefault();
         }
         }
 
 
-        var item = libraryManager.GetItemById(streamingRequest.Id);
+        var item = libraryManager.GetItemById<BaseItem>(streamingRequest.Id);
 
 
         state.IsInputVideo = item.MediaType == MediaType.Video;
         state.IsInputVideo = item.MediaType == MediaType.Video;
 
 
@@ -125,7 +126,7 @@ public static class StreamingHelpers
 
 
             if (mediaSource is null)
             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)
                 mediaSource = string.IsNullOrEmpty(streamingRequest.MediaSourceId)
                     ? mediaSources[0]
                     ? mediaSources[0]

+ 20 - 0
MediaBrowser.Controller/Library/ILibraryManager.cs

@@ -177,6 +177,26 @@ namespace MediaBrowser.Controller.Library
         T GetItemById<T>(Guid id)
         T GetItemById<T>(Guid id)
             where T : BaseItem;
             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>
         /// <summary>
         /// Gets the intros.
         /// Gets the intros.
         /// </summary>
         /// </summary>