2
0
Эх сурвалжийг харах

Merge branch 'master' into simplify_authz

# Conflicts:
#	Jellyfin.Api/Auth/SyncPlayAccessPolicy/SyncPlayAccessHandler.cs
cvium 2 жил өмнө
parent
commit
52e2776d8e
60 өөрчлөгдсөн 694 нэмэгдсэн , 351 устгасан
  1. 3 3
      .github/workflows/codeql-analysis.yml
  2. 1 0
      CONTRIBUTORS.md
  3. 21 0
      Emby.Server.Implementations/Data/SqliteItemRepository.cs
  4. 12 11
      Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs
  5. 38 15
      Emby.Server.Implementations/Plugins/PluginManager.cs
  6. 12 2
      Jellyfin.Api/Controllers/ImageController.cs
  7. 7 1
      Jellyfin.Api/Controllers/ItemsController.cs
  8. 14 0
      Jellyfin.Api/Controllers/LibraryController.cs
  9. 1 1
      Jellyfin.Api/Controllers/LiveTvController.cs
  10. 5 0
      Jellyfin.Api/Controllers/MusicGenresController.cs
  11. 20 0
      Jellyfin.Api/Controllers/PlaystateController.cs
  12. 4 0
      Jellyfin.Api/Controllers/SessionController.cs
  13. 19 6
      Jellyfin.Api/Controllers/UserController.cs
  14. 41 0
      Jellyfin.Api/Controllers/UserLibraryController.cs
  15. 6 1
      Jellyfin.Api/Controllers/VideosController.cs
  16. 1 1
      Jellyfin.Api/Helpers/MediaInfoHelper.cs
  17. 6 1
      Jellyfin.Api/Helpers/RequestHelpers.cs
  18. 5 2
      Jellyfin.Api/Models/UserDtos/CreateUserByName.cs
  19. 1 1
      Jellyfin.Api/Models/UserDtos/ForgotPasswordDto.cs
  20. 1 1
      Jellyfin.Api/Models/UserDtos/ForgotPasswordPinDto.cs
  21. 6 1
      Jellyfin.Data/Enums/PreferenceKind.cs
  22. 5 0
      Jellyfin.Server.Implementations/Devices/DeviceManager.cs
  23. 2 0
      Jellyfin.Server.Implementations/Users/UserManager.cs
  24. 3 4
      MediaBrowser.Controller/Dto/IDtoService.cs
  25. 6 0
      MediaBrowser.Controller/Entities/BaseItem.cs
  26. 4 0
      MediaBrowser.Controller/Entities/InternalItemsQuery.cs
  27. 4 6
      MediaBrowser.Controller/Library/IUserManager.cs
  28. 1 3
      MediaBrowser.Controller/Library/LibraryManagerExtensions.cs
  29. 3 0
      MediaBrowser.Model/Users/UserPolicy.cs
  30. 1 1
      src/Jellyfin.Extensions/Jellyfin.Extensions.csproj
  31. 1 1
      src/Jellyfin.MediaEncoding.Hls/Jellyfin.MediaEncoding.Hls.csproj
  32. 1 1
      src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj
  33. 23 0
      tests/Directory.Build.props
  34. 0 17
      tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj
  35. 0 17
      tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj
  36. 0 17
      tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj
  37. 0 17
      tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj
  38. 0 17
      tests/Jellyfin.Extensions.Tests/Jellyfin.Extensions.Tests.csproj
  39. 0 16
      tests/Jellyfin.MediaEncoding.Hls.Tests/Jellyfin.MediaEncoding.Hls.Tests.csproj
  40. 0 18
      tests/Jellyfin.MediaEncoding.Keyframes.Tests/Jellyfin.MediaEncoding.Keyframes.Tests.csproj
  41. 0 17
      tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj
  42. 0 17
      tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj
  43. 0 17
      tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj
  44. 0 17
      tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj
  45. 0 17
      tests/Jellyfin.Providers.Tests/Jellyfin.Providers.Tests.csproj
  46. 0 18
      tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj
  47. 19 0
      tests/Jellyfin.Server.Implementations.Tests/LiveTv/Listings/XmlTvListingsProviderTests.cs
  48. 6 0
      tests/Jellyfin.Server.Implementations.Tests/Test Data/LiveTv/Listings/XmlTv/emptycategory.xml
  49. 28 0
      tests/Jellyfin.Server.Integration.Tests/AuthHelper.cs
  50. 64 0
      tests/Jellyfin.Server.Integration.Tests/Controllers/ItemsControllerTests.cs
  51. 40 0
      tests/Jellyfin.Server.Integration.Tests/Controllers/LibraryControllerTests.cs
  52. 26 0
      tests/Jellyfin.Server.Integration.Tests/Controllers/MusicGenreControllerTests.cs
  53. 26 15
      tests/Jellyfin.Server.Integration.Tests/Controllers/PlaystateControllerTests.cs
  54. 27 0
      tests/Jellyfin.Server.Integration.Tests/Controllers/SessionControllerTests.cs
  55. 24 1
      tests/Jellyfin.Server.Integration.Tests/Controllers/UserControllerTests.cs
  56. 129 0
      tests/Jellyfin.Server.Integration.Tests/Controllers/UserLibraryControllerTests.cs
  57. 27 0
      tests/Jellyfin.Server.Integration.Tests/Controllers/VideosControllerTests.cs
  58. 0 16
      tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj
  59. 0 17
      tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj
  60. 0 17
      tests/Jellyfin.XbmcMetadata.Tests/Jellyfin.XbmcMetadata.Tests.csproj

+ 3 - 3
.github/workflows/codeql-analysis.yml

@@ -27,11 +27,11 @@ jobs:
         dotnet-version: '7.0.x'
 
     - name: Initialize CodeQL
-      uses: github/codeql-action/init@8775e868027fa230df8586bdf502bbd9b618a477 # v2
+      uses: github/codeql-action/init@17573ee1cc1b9d061760f3a006fc4aac4f944fd5 # v2
       with:
         languages: ${{ matrix.language }}
         queries: +security-extended
     - name: Autobuild
-      uses: github/codeql-action/autobuild@8775e868027fa230df8586bdf502bbd9b618a477 # v2
+      uses: github/codeql-action/autobuild@17573ee1cc1b9d061760f3a006fc4aac4f944fd5 # v2
     - name: Perform CodeQL Analysis
-      uses: github/codeql-action/analyze@8775e868027fa230df8586bdf502bbd9b618a477 # v2
+      uses: github/codeql-action/analyze@17573ee1cc1b9d061760f3a006fc4aac4f944fd5 # v2

+ 1 - 0
CONTRIBUTORS.md

@@ -232,3 +232,4 @@
  - [Matthew Jones](https://github.com/matthew-jones-uk)
  - [Jakob Kukla](https://github.com/jakobkukla)
  - [Utku Özdemir](https://github.com/utkuozdemir)
+ - [JPUC1143](https://github.com/Jpuc1143/)

+ 21 - 0
Emby.Server.Implementations/Data/SqliteItemRepository.cs

@@ -4477,6 +4477,24 @@ namespace Emby.Server.Implementations.Data
                 }
             }
 
+            if (query.IncludeInheritedTags.Length > 0)
+            {
+                var paramName = "@IncludeInheritedTags";
+                if (statement is null)
+                {
+                    int index = 0;
+                    string includedTags = string.Join(',', query.IncludeInheritedTags.Select(_ => paramName + index++));
+                    whereClauses.Add("((select CleanValue from ItemValues where ItemId=Guid and Type=6 and cleanvalue in (" + includedTags + ")) is not null)");
+                }
+                else
+                {
+                    for (int index = 0; index < query.IncludeInheritedTags.Length; index++)
+                    {
+                        statement.TryBind(paramName + index, GetCleanValue(query.IncludeInheritedTags[index]));
+                    }
+                }
+            }
+
             if (query.SeriesStatuses.Length > 0)
             {
                 var statuses = new List<string>();
@@ -5440,6 +5458,9 @@ AND Type = @InternalPersonType)");
 
             list.AddRange(inheritedTags.Select(i => (6, i)));
 
+            // Remove all invalid values.
+            list.RemoveAll(i => string.IsNullOrEmpty(i.Item2));
+
             return list;
         }
 

+ 12 - 11
Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs

@@ -137,32 +137,33 @@ namespace Emby.Server.Implementations.LiveTv.Listings
 
         private static ProgramInfo GetProgramInfo(XmlTvProgram program, ListingsProviderInfo info)
         {
-            string episodeTitle = program.Episode?.Title;
+            string episodeTitle = program.Episode.Title;
+            var programCategories = program.Categories.Where(c => !string.IsNullOrWhiteSpace(c)).ToList();
 
             var programInfo = new ProgramInfo
             {
                 ChannelId = program.ChannelId,
                 EndDate = program.EndDate.UtcDateTime,
-                EpisodeNumber = program.Episode?.Episode,
+                EpisodeNumber = program.Episode.Episode,
                 EpisodeTitle = episodeTitle,
-                Genres = program.Categories,
+                Genres = programCategories,
                 StartDate = program.StartDate.UtcDateTime,
                 Name = program.Title,
                 Overview = program.Description,
                 ProductionYear = program.CopyrightDate?.Year,
-                SeasonNumber = program.Episode?.Series,
-                IsSeries = program.Episode is not null,
+                SeasonNumber = program.Episode.Series,
+                IsSeries = program.Episode.Series is not null,
                 IsRepeat = program.IsPreviouslyShown && !program.IsNew,
                 IsPremiere = program.Premiere is not null,
-                IsKids = program.Categories.Any(c => info.KidsCategories.Contains(c, StringComparison.OrdinalIgnoreCase)),
-                IsMovie = program.Categories.Any(c => info.MovieCategories.Contains(c, StringComparison.OrdinalIgnoreCase)),
-                IsNews = program.Categories.Any(c => info.NewsCategories.Contains(c, StringComparison.OrdinalIgnoreCase)),
-                IsSports = program.Categories.Any(c => info.SportsCategories.Contains(c, StringComparison.OrdinalIgnoreCase)),
+                IsKids = programCategories.Any(c => info.KidsCategories.Contains(c, StringComparison.OrdinalIgnoreCase)),
+                IsMovie = programCategories.Any(c => info.MovieCategories.Contains(c, StringComparison.OrdinalIgnoreCase)),
+                IsNews = programCategories.Any(c => info.NewsCategories.Contains(c, StringComparison.OrdinalIgnoreCase)),
+                IsSports = programCategories.Any(c => info.SportsCategories.Contains(c, StringComparison.OrdinalIgnoreCase)),
                 ImageUrl = string.IsNullOrEmpty(program.Icon?.Source) ? null : program.Icon.Source,
                 HasImage = !string.IsNullOrEmpty(program.Icon?.Source),
                 OfficialRating = string.IsNullOrEmpty(program.Rating?.Value) ? null : program.Rating.Value,
                 CommunityRating = program.StarRating,
-                SeriesId = program.Episode is null ? null : program.Title?.GetMD5().ToString("N", CultureInfo.InvariantCulture)
+                SeriesId = program.Episode.Episode is null ? null : program.Title?.GetMD5().ToString("N", CultureInfo.InvariantCulture)
             };
 
             if (string.IsNullOrWhiteSpace(program.ProgramId))
@@ -243,7 +244,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
             {
                 Id = c.Id,
                 Name = c.DisplayName,
-                ImageUrl = string.IsNullOrEmpty(c.Icon.Source) ? null : c.Icon.Source,
+                ImageUrl = string.IsNullOrEmpty(c.Icon?.Source) ? null : c.Icon.Source,
                 Number = string.IsNullOrWhiteSpace(c.Number) ? c.Id : c.Number
             }).ToList();
         }

+ 38 - 15
Emby.Server.Implementations/Plugins/PluginManager.cs

@@ -123,41 +123,64 @@ namespace Emby.Server.Implementations.Plugins
                     continue;
                 }
 
+                var assemblyLoadContext = new PluginLoadContext(plugin.Path);
+                _assemblyLoadContexts.Add(assemblyLoadContext);
+
+                var assemblies = new List<Assembly>(plugin.DllFiles.Count);
+                var loadedAll = true;
+
                 foreach (var file in plugin.DllFiles)
                 {
-                    Assembly assembly;
                     try
                     {
-                        var assemblyLoadContext = new PluginLoadContext(file);
-                        _assemblyLoadContexts.Add(assemblyLoadContext);
-
-                        assembly = assemblyLoadContext.LoadFromAssemblyPath(file);
-
-                        // Load all required types to verify that the plugin will load
-                        assembly.GetTypes();
+                        assemblies.Add(assemblyLoadContext.LoadFromAssemblyPath(file));
                     }
                     catch (FileLoadException ex)
                     {
-                        _logger.LogError(ex, "Failed to load assembly {Path}. Disabling plugin.", file);
+                        _logger.LogError(ex, "Failed to load assembly {Path}. Disabling plugin", file);
                         ChangePluginState(plugin, PluginStatus.Malfunctioned);
-                        continue;
+                        loadedAll = false;
+                        break;
+                    }
+#pragma warning disable CA1031 // Do not catch general exception types
+                    catch (Exception ex)
+#pragma warning restore CA1031 // Do not catch general exception types
+                    {
+                        _logger.LogError(ex, "Failed to load assembly {Path}. Unknown exception was thrown. Disabling plugin", file);
+                        ChangePluginState(plugin, PluginStatus.Malfunctioned);
+                        loadedAll = false;
+                        break;
+                    }
+                }
+
+                if (!loadedAll)
+                {
+                    continue;
+                }
+
+                foreach (var assembly in assemblies)
+                {
+                    try
+                    {
+                        // Load all required types to verify that the plugin will load
+                        assembly.GetTypes();
                     }
                     catch (SystemException ex) when (ex is TypeLoadException or ReflectionTypeLoadException) // Undocumented exception
                     {
-                        _logger.LogError(ex, "Failed to load assembly {Path}. This error occurs when a plugin references an incompatible version of one of the shared libraries. Disabling plugin.", file);
+                        _logger.LogError(ex, "Failed to load assembly {Path}. This error occurs when a plugin references an incompatible version of one of the shared libraries. Disabling plugin", assembly.Location);
                         ChangePluginState(plugin, PluginStatus.NotSupported);
-                        continue;
+                        break;
                     }
 #pragma warning disable CA1031 // Do not catch general exception types
                     catch (Exception ex)
 #pragma warning restore CA1031 // Do not catch general exception types
                     {
-                        _logger.LogError(ex, "Failed to load assembly {Path}. Unknown exception was thrown. Disabling plugin.", file);
+                        _logger.LogError(ex, "Failed to load assembly {Path}. Unknown exception was thrown. Disabling plugin", assembly.Location);
                         ChangePluginState(plugin, PluginStatus.Malfunctioned);
-                        continue;
+                        break;
                     }
 
-                    _logger.LogInformation("Loaded assembly {Assembly} from {Path}", assembly.FullName, file);
+                    _logger.LogInformation("Loaded assembly {Assembly} from {Path}", assembly.FullName, assembly.Location);
                     yield return assembly;
                 }
             }

+ 12 - 2
Jellyfin.Api/Controllers/ImageController.cs

@@ -99,12 +99,17 @@ public class ImageController : BaseJellyfinApiController
         [FromRoute, Required] ImageType imageType,
         [FromQuery] int? index = null)
     {
+        var user = _userManager.GetUserById(userId);
+        if (user is null)
+        {
+            return NotFound();
+        }
+
         if (!RequestHelpers.AssertCanUpdateUser(_userManager, HttpContext.User, userId, true))
         {
             return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to update the image.");
         }
 
-        var user = _userManager.GetUserById(userId);
         var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
         await using (memoryStream.ConfigureAwait(false))
         {
@@ -148,12 +153,17 @@ public class ImageController : BaseJellyfinApiController
         [FromRoute, Required] ImageType imageType,
         [FromRoute] int index)
     {
+        var user = _userManager.GetUserById(userId);
+        if (user is null)
+        {
+            return NotFound();
+        }
+
         if (!RequestHelpers.AssertCanUpdateUser(_userManager, HttpContext.User, userId, true))
         {
             return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to update the image.");
         }
 
-        var user = _userManager.GetUserById(userId);
         var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
         await using (memoryStream.ConfigureAwait(false))
         {

+ 7 - 1
Jellyfin.Api/Controllers/ItemsController.cs

@@ -5,6 +5,7 @@ using Jellyfin.Api.Extensions;
 using Jellyfin.Api.Helpers;
 using Jellyfin.Api.ModelBinders;
 using Jellyfin.Data.Enums;
+using MediaBrowser.Common.Extensions;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
@@ -240,7 +241,7 @@ public class ItemsController : BaseJellyfinApiController
         var isApiKey = User.GetIsApiKey();
         // if api key is used (auth.IsApiKey == true), then `user` will be null throughout this method
         var user = !isApiKey && userId.HasValue && !userId.Value.Equals(default)
-            ? _userManager.GetUserById(userId.Value)
+            ? _userManager.GetUserById(userId.Value) ?? throw new ResourceNotFoundException()
             : null;
 
         // beyond this point, we're either using an api key or we have a valid user
@@ -814,6 +815,11 @@ public class ItemsController : BaseJellyfinApiController
         [FromQuery] bool excludeActiveSessions = false)
     {
         var user = _userManager.GetUserById(userId);
+        if (user is null)
+        {
+            return NotFound();
+        }
+
         var parentIdGuid = parentId ?? Guid.Empty;
         var dtoOptions = new DtoOptions { Fields = fields }
             .AddClientFields(User)

+ 14 - 0
Jellyfin.Api/Controllers/LibraryController.cs

@@ -283,6 +283,11 @@ public class LibraryController : BaseJellyfinApiController
             userId,
             inheritFromParent);
 
+        if (themeSongs.Result is NotFoundObjectResult || themeVideos.Result is NotFoundObjectResult)
+        {
+            return NotFound();
+        }
+
         return new AllThemeMediaResult
         {
             ThemeSongsResult = themeSongs?.Value,
@@ -452,6 +457,10 @@ public class LibraryController : BaseJellyfinApiController
             if (user is not null)
             {
                 parent = TranslateParentItem(parent, user);
+                if (parent is null)
+                {
+                    break;
+                }
             }
 
             baseItemDtos.Add(_dtoService.GetBaseItemDto(parent, dtoOptions, user));
@@ -672,6 +681,11 @@ public class LibraryController : BaseJellyfinApiController
                 : _libraryManager.GetUserRootFolder())
             : _libraryManager.GetItemById(itemId);
 
+        if (item is null)
+        {
+            return NotFound();
+        }
+
         if (item is Episode || (item is IItemByName && item is not MusicArtist))
         {
             return new QueryResult<BaseItemDto>();

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

@@ -1210,7 +1210,7 @@ public class LiveTvController : BaseJellyfinApiController
 
     private async Task AssertUserCanManageLiveTv()
     {
-        var user = _userManager.GetUserById(User.GetUserId());
+        var user = _userManager.GetUserById(User.GetUserId()) ?? throw new ResourceNotFoundException();
         var session = await _sessionManager.LogSessionActivity(
             User.GetClient(),
             User.GetVersion(),

+ 5 - 0
Jellyfin.Api/Controllers/MusicGenresController.cs

@@ -157,6 +157,11 @@ public class MusicGenresController : BaseJellyfinApiController
             item = _libraryManager.GetMusicGenre(genreName);
         }
 
+        if (item is null)
+        {
+            return NotFound();
+        }
+
         if (userId.HasValue && !userId.Value.Equals(default))
         {
             var user = _userManager.GetUserById(userId.Value);

+ 20 - 0
Jellyfin.Api/Controllers/PlaystateController.cs

@@ -76,6 +76,11 @@ public class PlaystateController : BaseJellyfinApiController
         [FromQuery, ModelBinder(typeof(LegacyDateTimeModelBinder))] DateTime? datePlayed)
     {
         var user = _userManager.GetUserById(userId);
+        if (user is null)
+        {
+            return NotFound();
+        }
+
         var session = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
 
         var item = _libraryManager.GetItemById(itemId);
@@ -88,6 +93,11 @@ public class PlaystateController : BaseJellyfinApiController
         foreach (var additionalUserInfo in session.AdditionalUsers)
         {
             var additionalUser = _userManager.GetUserById(additionalUserInfo.UserId);
+            if (additionalUser is null)
+            {
+                return NotFound();
+            }
+
             UpdatePlayedStatus(additionalUser, item, true, datePlayed);
         }
 
@@ -108,6 +118,11 @@ public class PlaystateController : BaseJellyfinApiController
     public async Task<ActionResult<UserItemDataDto>> MarkUnplayedItem([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId)
     {
         var user = _userManager.GetUserById(userId);
+        if (user is null)
+        {
+            return NotFound();
+        }
+
         var session = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
         var item = _libraryManager.GetItemById(itemId);
 
@@ -120,6 +135,11 @@ public class PlaystateController : BaseJellyfinApiController
         foreach (var additionalUserInfo in session.AdditionalUsers)
         {
             var additionalUser = _userManager.GetUserById(additionalUserInfo.UserId);
+            if (additionalUser is null)
+            {
+                return NotFound();
+            }
+
             UpdatePlayedStatus(additionalUser, item, false, null);
         }
 

+ 4 - 0
Jellyfin.Api/Controllers/SessionController.cs

@@ -75,6 +75,10 @@ public class SessionController : BaseJellyfinApiController
             result = result.Where(i => i.SupportsRemoteControl);
 
             var user = _userManager.GetUserById(controllableByUserId.Value);
+            if (user is null)
+            {
+                return NotFound();
+            }
 
             if (!user.HasPermission(PermissionKind.EnableRemoteControlOfOtherUsers))
             {

+ 19 - 6
Jellyfin.Api/Controllers/UserController.cs

@@ -147,6 +147,11 @@ public class UserController : BaseJellyfinApiController
     public async Task<ActionResult> DeleteUser([FromRoute, Required] Guid userId)
     {
         var user = _userManager.GetUserById(userId);
+        if (user is null)
+        {
+            return NotFound();
+        }
+
         await _sessionManager.RevokeUserTokens(user.Id, null).ConfigureAwait(false);
         await _userManager.DeleteUserAsync(userId).ConfigureAwait(false);
         return NoContent();
@@ -281,8 +286,8 @@ public class UserController : BaseJellyfinApiController
             {
                 var success = await _userManager.AuthenticateUser(
                     user.Username,
-                    request.CurrentPw,
-                    request.CurrentPw,
+                    request.CurrentPw ?? string.Empty,
+                    request.CurrentPw ?? string.Empty,
                     HttpContext.GetNormalizedRemoteIp().ToString(),
                     false).ConfigureAwait(false);
 
@@ -292,7 +297,7 @@ public class UserController : BaseJellyfinApiController
                 }
             }
 
-            await _userManager.ChangePassword(user, request.NewPw).ConfigureAwait(false);
+            await _userManager.ChangePassword(user, request.NewPw ?? string.Empty).ConfigureAwait(false);
 
             var currentToken = User.GetToken();
 
@@ -338,7 +343,7 @@ public class UserController : BaseJellyfinApiController
         }
         else
         {
-            await _userManager.ChangeEasyPassword(user, request.NewPw, request.NewPassword).ConfigureAwait(false);
+            await _userManager.ChangeEasyPassword(user, request.NewPw ?? string.Empty, request.NewPassword ?? string.Empty).ConfigureAwait(false);
         }
 
         return NoContent();
@@ -362,13 +367,17 @@ public class UserController : BaseJellyfinApiController
         [FromRoute, Required] Guid userId,
         [FromBody, Required] UserDto updateUser)
     {
+        var user = _userManager.GetUserById(userId);
+        if (user is null)
+        {
+            return NotFound();
+        }
+
         if (!RequestHelpers.AssertCanUpdateUser(_userManager, User, userId, true))
         {
             return StatusCode(StatusCodes.Status403Forbidden, "User update not allowed.");
         }
 
-        var user = _userManager.GetUserById(userId);
-
         if (!string.Equals(user.Username, updateUser.Name, StringComparison.Ordinal))
         {
             await _userManager.RenameUser(user, updateUser.Name).ConfigureAwait(false);
@@ -398,6 +407,10 @@ public class UserController : BaseJellyfinApiController
         [FromBody, Required] UserPolicy newPolicy)
     {
         var user = _userManager.GetUserById(userId);
+        if (user is null)
+        {
+            return NotFound();
+        }
 
         // If removing admin access
         if (!newPolicy.IsAdministrator && user.HasPermission(PermissionKind.IsAdministrator))

+ 41 - 0
Jellyfin.Api/Controllers/UserLibraryController.cs

@@ -78,10 +78,18 @@ public class UserLibraryController : BaseJellyfinApiController
     public async Task<ActionResult<BaseItemDto>> GetItem([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId)
     {
         var user = _userManager.GetUserById(userId);
+        if (user is null)
+        {
+            return NotFound();
+        }
 
         var item = itemId.Equals(default)
             ? _libraryManager.GetUserRootFolder()
             : _libraryManager.GetItemById(itemId);
+        if (item is null)
+        {
+            return NotFound();
+        }
 
         await RefreshItemOnDemandIfNeeded(item).ConfigureAwait(false);
 
@@ -101,6 +109,11 @@ public class UserLibraryController : BaseJellyfinApiController
     public ActionResult<BaseItemDto> GetRootFolder([FromRoute, Required] Guid userId)
     {
         var user = _userManager.GetUserById(userId);
+        if (user is null)
+        {
+            return NotFound();
+        }
+
         var item = _libraryManager.GetUserRootFolder();
         var dtoOptions = new DtoOptions().AddClientFields(User);
         return _dtoService.GetBaseItemDto(item, dtoOptions, user);
@@ -118,10 +131,18 @@ public class UserLibraryController : BaseJellyfinApiController
     public async Task<ActionResult<QueryResult<BaseItemDto>>> GetIntros([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId)
     {
         var user = _userManager.GetUserById(userId);
+        if (user is null)
+        {
+            return NotFound();
+        }
 
         var item = itemId.Equals(default)
             ? _libraryManager.GetUserRootFolder()
             : _libraryManager.GetItemById(itemId);
+        if (item is null)
+        {
+            return NotFound();
+        }
 
         var items = await _libraryManager.GetIntros(item, user).ConfigureAwait(false);
         var dtoOptions = new DtoOptions().AddClientFields(User);
@@ -199,10 +220,18 @@ public class UserLibraryController : BaseJellyfinApiController
     public ActionResult<IEnumerable<BaseItemDto>> GetLocalTrailers([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId)
     {
         var user = _userManager.GetUserById(userId);
+        if (user is null)
+        {
+            return NotFound();
+        }
 
         var item = itemId.Equals(default)
             ? _libraryManager.GetUserRootFolder()
             : _libraryManager.GetItemById(itemId);
+        if (item is null)
+        {
+            return NotFound();
+        }
 
         var dtoOptions = new DtoOptions().AddClientFields(User);
 
@@ -229,10 +258,18 @@ public class UserLibraryController : BaseJellyfinApiController
     public ActionResult<IEnumerable<BaseItemDto>> GetSpecialFeatures([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId)
     {
         var user = _userManager.GetUserById(userId);
+        if (user is null)
+        {
+            return NotFound();
+        }
 
         var item = itemId.Equals(default)
             ? _libraryManager.GetUserRootFolder()
             : _libraryManager.GetItemById(itemId);
+        if (item is null)
+        {
+            return NotFound();
+        }
 
         var dtoOptions = new DtoOptions().AddClientFields(User);
 
@@ -274,6 +311,10 @@ public class UserLibraryController : BaseJellyfinApiController
         [FromQuery] bool groupItems = true)
     {
         var user = _userManager.GetUserById(userId);
+        if (user is null)
+        {
+            return NotFound();
+        }
 
         if (!isPlayed.HasValue)
         {

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

@@ -155,7 +155,12 @@ public class VideosController : BaseJellyfinApiController
 
         if (video.LinkedAlternateVersions.Length == 0)
         {
-            video = (Video)_libraryManager.GetItemById(video.PrimaryVersionId);
+            video = (Video?)_libraryManager.GetItemById(video.PrimaryVersionId);
+        }
+
+        if (video is null)
+        {
+            return NotFound();
         }
 
         foreach (var link in video.GetLinkedAlternateVersions())

+ 1 - 1
Jellyfin.Api/Helpers/MediaInfoHelper.cs

@@ -200,7 +200,7 @@ public class MediaInfoHelper
             options.SubtitleStreamIndex = subtitleStreamIndex;
         }
 
-        var user = _userManager.GetUserById(userId);
+        var user = _userManager.GetUserById(userId) ?? throw new ResourceNotFoundException();
 
         if (!enableDirectPlay)
         {

+ 6 - 1
Jellyfin.Api/Helpers/RequestHelpers.cs

@@ -81,6 +81,11 @@ public static class RequestHelpers
         }
 
         var user = userManager.GetUserById(userId);
+        if (user is null)
+        {
+            throw new ResourceNotFoundException();
+        }
+
         return user.EnableUserPreferenceAccess;
     }
 
@@ -98,7 +103,7 @@ public static class RequestHelpers
 
         if (session is null)
         {
-            throw new ArgumentException("Session not found.");
+            throw new ResourceNotFoundException("Session not found.");
         }
 
         return session;

+ 5 - 2
Jellyfin.Api/Models/UserDtos/CreateUserByName.cs

@@ -1,4 +1,6 @@
-namespace Jellyfin.Api.Models.UserDtos;
+using System.ComponentModel.DataAnnotations;
+
+namespace Jellyfin.Api.Models.UserDtos;
 
 /// <summary>
 /// The create user by name request body.
@@ -8,7 +10,8 @@ public class CreateUserByName
     /// <summary>
     /// Gets or sets the username.
     /// </summary>
-    public string? Name { get; set; }
+    [Required]
+    required public string Name { get; set; }
 
     /// <summary>
     /// Gets or sets the password.

+ 1 - 1
Jellyfin.Api/Models/UserDtos/ForgotPasswordDto.cs

@@ -11,5 +11,5 @@ public class ForgotPasswordDto
     /// Gets or sets the entered username to have its password reset.
     /// </summary>
     [Required]
-    public string? EnteredUsername { get; set; }
+    required public string EnteredUsername { get; set; }
 }

+ 1 - 1
Jellyfin.Api/Models/UserDtos/ForgotPasswordPinDto.cs

@@ -11,5 +11,5 @@ public class ForgotPasswordPinDto
     /// Gets or sets the entered pin to have the password reset.
     /// </summary>
     [Required]
-    public string? Pin { get; set; }
+    required public string Pin { get; set; }
 }

+ 6 - 1
Jellyfin.Data/Enums/PreferenceKind.cs

@@ -63,6 +63,11 @@ namespace Jellyfin.Data.Enums
         /// <summary>
         /// A list of ordered views.
         /// </summary>
-        OrderedViews = 11
+        OrderedViews = 11,
+
+        /// <summary>
+        /// A list of allowed tags.
+        /// </summary>
+        AllowedTags = 12
     }
 }

+ 5 - 0
Jellyfin.Server.Implementations/Devices/DeviceManager.cs

@@ -9,6 +9,7 @@ using Jellyfin.Data.Enums;
 using Jellyfin.Data.Events;
 using Jellyfin.Data.Queries;
 using Jellyfin.Extensions;
+using MediaBrowser.Common.Extensions;
 using MediaBrowser.Controller.Devices;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Model.Devices;
@@ -185,6 +186,10 @@ namespace Jellyfin.Server.Implementations.Devices
                 if (userId.HasValue)
                 {
                     var user = _userManager.GetUserById(userId.Value);
+                    if (user is null)
+                    {
+                        throw new ResourceNotFoundException();
+                    }
 
                     sessions = sessions.Where(i => CanAccessDevice(user, i.DeviceId));
                 }

+ 2 - 0
Jellyfin.Server.Implementations/Users/UserManager.cs

@@ -371,6 +371,7 @@ namespace Jellyfin.Server.Implementations.Users
                     EnablePublicSharing = user.HasPermission(PermissionKind.EnablePublicSharing),
                     AccessSchedules = user.AccessSchedules.ToArray(),
                     BlockedTags = user.GetPreference(PreferenceKind.BlockedTags),
+                    AllowedTags = user.GetPreference(PreferenceKind.AllowedTags),
                     EnabledChannels = user.GetPreferenceValues<Guid>(PreferenceKind.EnabledChannels),
                     EnabledDevices = user.GetPreference(PreferenceKind.EnabledDevices),
                     EnabledFolders = user.GetPreferenceValues<Guid>(PreferenceKind.EnabledFolders),
@@ -696,6 +697,7 @@ namespace Jellyfin.Server.Implementations.Users
                 // TODO: fix this at some point
                 user.SetPreference(PreferenceKind.BlockUnratedItems, policy.BlockUnratedItems ?? Array.Empty<UnratedItem>());
                 user.SetPreference(PreferenceKind.BlockedTags, policy.BlockedTags);
+                user.SetPreference(PreferenceKind.AllowedTags, policy.AllowedTags);
                 user.SetPreference(PreferenceKind.EnabledChannels, policy.EnabledChannels);
                 user.SetPreference(PreferenceKind.EnabledDevices, policy.EnabledDevices);
                 user.SetPreference(PreferenceKind.EnabledFolders, policy.EnabledFolders);

+ 3 - 4
MediaBrowser.Controller/Dto/IDtoService.cs

@@ -1,4 +1,3 @@
-#nullable disable
 #pragma warning disable CA1002
 
 using System.Collections.Generic;
@@ -28,7 +27,7 @@ namespace MediaBrowser.Controller.Dto
         /// <param name="user">The user.</param>
         /// <param name="owner">The owner.</param>
         /// <returns>BaseItemDto.</returns>
-        BaseItemDto GetBaseItemDto(BaseItem item, DtoOptions options, User user = null, BaseItem owner = null);
+        BaseItemDto GetBaseItemDto(BaseItem item, DtoOptions options, User? user = null, BaseItem? owner = null);
 
         /// <summary>
         /// Gets the base item dtos.
@@ -38,7 +37,7 @@ namespace MediaBrowser.Controller.Dto
         /// <param name="user">The user.</param>
         /// <param name="owner">The owner.</param>
         /// <returns>The <see cref="IReadOnlyList{T}"/> of <see cref="BaseItemDto"/>.</returns>
-        IReadOnlyList<BaseItemDto> GetBaseItemDtos(IReadOnlyList<BaseItem> items, DtoOptions options, User user = null, BaseItem owner = null);
+        IReadOnlyList<BaseItemDto> GetBaseItemDtos(IReadOnlyList<BaseItem> items, DtoOptions options, User? user = null, BaseItem? owner = null);
 
         /// <summary>
         /// Gets the item by name dto.
@@ -48,6 +47,6 @@ namespace MediaBrowser.Controller.Dto
         /// <param name="taggedItems">The list of tagged items.</param>
         /// <param name="user">The user.</param>
         /// <returns>The item dto.</returns>
-        BaseItemDto GetItemByNameDto(BaseItem item, DtoOptions options, List<BaseItem> taggedItems, User user = null);
+        BaseItemDto GetItemByNameDto(BaseItem item, DtoOptions options, List<BaseItem>? taggedItems, User? user = null);
     }
 }

+ 6 - 0
MediaBrowser.Controller/Entities/BaseItem.cs

@@ -1607,6 +1607,12 @@ namespace MediaBrowser.Controller.Entities
                 return false;
             }
 
+            var allowedTagsPreference = user.GetPreference(PreferenceKind.AllowedTags);
+            if (allowedTagsPreference.Any() && !allowedTagsPreference.Any(i => Tags.Contains(i, StringComparison.OrdinalIgnoreCase)))
+            {
+                return false;
+            }
+
             return true;
         }
 

+ 4 - 0
MediaBrowser.Controller/Entities/InternalItemsQuery.cs

@@ -26,6 +26,7 @@ namespace MediaBrowser.Controller.Entities
             EnableTotalRecordCount = true;
             ExcludeArtistIds = Array.Empty<Guid>();
             ExcludeInheritedTags = Array.Empty<string>();
+            IncludeInheritedTags = Array.Empty<string>();
             ExcludeItemIds = Array.Empty<Guid>();
             ExcludeItemTypes = Array.Empty<BaseItemKind>();
             ExcludeTags = Array.Empty<string>();
@@ -95,6 +96,8 @@ namespace MediaBrowser.Controller.Entities
 
         public string[] ExcludeInheritedTags { get; set; }
 
+        public string[] IncludeInheritedTags { get; set; }
+
         public IReadOnlyList<string> Genres { get; set; }
 
         public bool? IsSpecialSeason { get; set; }
@@ -368,6 +371,7 @@ namespace MediaBrowser.Controller.Entities
             }
 
             ExcludeInheritedTags = user.GetPreference(PreferenceKind.BlockedTags);
+            IncludeInheritedTags = user.GetPreference(PreferenceKind.AllowedTags);
 
             User = user;
         }

+ 4 - 6
MediaBrowser.Controller/Library/IUserManager.cs

@@ -1,5 +1,3 @@
-#nullable disable
-
 #pragma warning disable CS1591
 
 using System;
@@ -47,14 +45,14 @@ namespace MediaBrowser.Controller.Library
         /// <param name="id">The id.</param>
         /// <returns>The user with the specified Id, or <c>null</c> if the user doesn't exist.</returns>
         /// <exception cref="ArgumentException"><c>id</c> is an empty Guid.</exception>
-        User GetUserById(Guid id);
+        User? GetUserById(Guid id);
 
         /// <summary>
         /// Gets the name of the user by.
         /// </summary>
         /// <param name="name">The name.</param>
         /// <returns>User.</returns>
-        User GetUserByName(string name);
+        User? GetUserByName(string name);
 
         /// <summary>
         /// Renames the user.
@@ -128,7 +126,7 @@ namespace MediaBrowser.Controller.Library
         /// <param name="user">The user.</param>
         /// <param name="remoteEndPoint">The remote end point.</param>
         /// <returns>UserDto.</returns>
-        UserDto GetUserDto(User user, string remoteEndPoint = null);
+        UserDto GetUserDto(User user, string? remoteEndPoint = null);
 
         /// <summary>
         /// Authenticates the user.
@@ -139,7 +137,7 @@ namespace MediaBrowser.Controller.Library
         /// <param name="remoteEndPoint">Remove endpoint to use.</param>
         /// <param name="isUserSession">Specifies if a user session.</param>
         /// <returns>User wrapped in awaitable task.</returns>
-        Task<User> AuthenticateUser(string username, string password, string passwordSha1, string remoteEndPoint, bool isUserSession);
+        Task<User?> AuthenticateUser(string username, string password, string passwordSha1, string remoteEndPoint, bool isUserSession);
 
         /// <summary>
         /// Starts the forgot password process.

+ 1 - 3
MediaBrowser.Controller/Library/LibraryManagerExtensions.cs

@@ -1,5 +1,3 @@
-#nullable disable
-
 #pragma warning disable CS1591
 
 using System;
@@ -9,7 +7,7 @@ namespace MediaBrowser.Controller.Library
 {
     public static class LibraryManagerExtensions
     {
-        public static BaseItem GetItemById(this ILibraryManager manager, string id)
+        public static BaseItem? GetItemById(this ILibraryManager manager, string id)
         {
             return manager.GetItemById(new Guid(id));
         }

+ 3 - 0
MediaBrowser.Model/Users/UserPolicy.cs

@@ -35,6 +35,7 @@ namespace MediaBrowser.Model.Users
             EnableSharedDeviceControl = true;
 
             BlockedTags = Array.Empty<string>();
+            AllowedTags = Array.Empty<string>();
             BlockUnratedItems = Array.Empty<UnratedItem>();
 
             EnableUserPreferenceAccess = true;
@@ -86,6 +87,8 @@ namespace MediaBrowser.Model.Users
 
         public string[] BlockedTags { get; set; }
 
+        public string[] AllowedTags { get; set; }
+
         public bool EnableUserPreferenceAccess { get; set; }
 
         public AccessSchedule[] AccessSchedules { get; set; }

+ 1 - 1
src/Jellyfin.Extensions/Jellyfin.Extensions.csproj

@@ -33,7 +33,7 @@
   </ItemGroup>
 
   <!-- Code Analyzers-->
-  <ItemGroup>
+  <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
     <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
       <PrivateAssets>all</PrivateAssets>
       <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>

+ 1 - 1
src/Jellyfin.MediaEncoding.Hls/Jellyfin.MediaEncoding.Hls.csproj

@@ -6,7 +6,7 @@
   </PropertyGroup>
 
   <!-- Code Analyzers-->
-  <ItemGroup>
+  <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
     <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
       <PrivateAssets>all</PrivateAssets>
       <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>

+ 1 - 1
src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj

@@ -10,7 +10,7 @@
   </ItemGroup>
 
   <!-- Code Analyzers-->
-  <ItemGroup>
+  <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
     <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
       <PrivateAssets>all</PrivateAssets>
       <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>

+ 23 - 0
tests/Directory.Build.props

@@ -0,0 +1,23 @@
+<Project>
+  <!-- Sets defaults for all test projects -->
+
+  <Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))" />
+
+  <PropertyGroup>
+    <TargetFramework>net7.0</TargetFramework>
+    <IsPackable>false</IsPackable>
+    <CodeAnalysisRuleSet>$(MSBuildThisFileDirectory)/jellyfin-tests.ruleset</CodeAnalysisRuleSet>
+  </PropertyGroup>
+
+  <!-- Code Analyzers -->
+  <ItemGroup>
+    <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
+      <PrivateAssets>all</PrivateAssets>
+      <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
+    </PackageReference>
+    <PackageReference Include="SerilogAnalyzer" PrivateAssets="All" />
+    <PackageReference Include="StyleCop.Analyzers" PrivateAssets="All" />
+    <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" />
+  </ItemGroup>
+
+</Project>

+ 0 - 17
tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj

@@ -5,12 +5,6 @@
     <ProjectGuid>{A2FD0A10-8F62-4F9D-B171-FFDF9F0AFA9D}</ProjectGuid>
   </PropertyGroup>
 
-  <PropertyGroup>
-    <TargetFramework>net7.0</TargetFramework>
-    <IsPackable>false</IsPackable>
-    <CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
-  </PropertyGroup>
-
   <ItemGroup>
     <PackageReference Include="AutoFixture" />
     <PackageReference Include="AutoFixture.AutoMoq" />
@@ -27,17 +21,6 @@
     <PackageReference Include="Moq" />
   </ItemGroup>
 
-  <!-- Code Analyzers -->
-  <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
-    <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
-      <PrivateAssets>all</PrivateAssets>
-      <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
-    </PackageReference>
-    <PackageReference Include="SerilogAnalyzer" PrivateAssets="All" />
-    <PackageReference Include="StyleCop.Analyzers" PrivateAssets="All" />
-    <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" />
-  </ItemGroup>
-
   <ItemGroup>
     <ProjectReference Include="../../Jellyfin.Api/Jellyfin.Api.csproj" />
     <ProjectReference Include="../../Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj" />

+ 0 - 17
tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj

@@ -5,12 +5,6 @@
     <ProjectGuid>{DF194677-DFD3-42AF-9F75-D44D5A416478}</ProjectGuid>
   </PropertyGroup>
 
-  <PropertyGroup>
-    <TargetFramework>net7.0</TargetFramework>
-    <IsPackable>false</IsPackable>
-    <CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
-  </PropertyGroup>
-
   <ItemGroup>
     <PackageReference Include="Microsoft.NET.Test.Sdk" />
     <PackageReference Include="xunit" />
@@ -22,17 +16,6 @@
     <PackageReference Include="FsCheck.Xunit" />
   </ItemGroup>
 
-  <!-- Code Analyzers -->
-  <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
-    <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
-      <PrivateAssets>all</PrivateAssets>
-      <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
-    </PackageReference>
-    <PackageReference Include="SerilogAnalyzer" PrivateAssets="All" />
-    <PackageReference Include="StyleCop.Analyzers" PrivateAssets="All" />
-    <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" />
-  </ItemGroup>
-
   <ItemGroup>
     <ProjectReference Include="../../MediaBrowser.Common/MediaBrowser.Common.csproj" />
     <ProjectReference Include="../../MediaBrowser.Providers/MediaBrowser.Providers.csproj" />

+ 0 - 17
tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj

@@ -5,12 +5,6 @@
     <ProjectGuid>{462584F7-5023-4019-9EAC-B98CA458C0A0}</ProjectGuid>
   </PropertyGroup>
 
-  <PropertyGroup>
-    <TargetFramework>net7.0</TargetFramework>
-    <IsPackable>false</IsPackable>
-    <CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
-  </PropertyGroup>
-
   <ItemGroup>
     <PackageReference Include="Microsoft.NET.Test.Sdk" />
     <PackageReference Include="Moq" />
@@ -22,17 +16,6 @@
     <PackageReference Include="coverlet.collector" />
   </ItemGroup>
 
-  <!-- Code Analyzers -->
-  <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
-    <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
-      <PrivateAssets>all</PrivateAssets>
-      <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
-    </PackageReference>
-    <PackageReference Include="SerilogAnalyzer" PrivateAssets="All" />
-    <PackageReference Include="StyleCop.Analyzers" PrivateAssets="All" />
-    <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" />
-  </ItemGroup>
-
   <ItemGroup>
     <ProjectReference Include="../../MediaBrowser.Controller/MediaBrowser.Controller.csproj" />
   </ItemGroup>

+ 0 - 17
tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj

@@ -1,11 +1,5 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
-  <PropertyGroup>
-    <TargetFramework>net7.0</TargetFramework>
-    <IsPackable>false</IsPackable>
-    <CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
-  </PropertyGroup>
-
   <ItemGroup>
     <PackageReference Include="Microsoft.NET.Test.Sdk" />
     <PackageReference Include="Moq" />
@@ -17,17 +11,6 @@
     <PackageReference Include="coverlet.collector" />
   </ItemGroup>
 
-  <!-- Code Analyzers -->
-  <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
-    <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
-      <PrivateAssets>all</PrivateAssets>
-      <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
-    </PackageReference>
-    <PackageReference Include="SerilogAnalyzer" PrivateAssets="All" />
-    <PackageReference Include="StyleCop.Analyzers" PrivateAssets="All" />
-    <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" />
-  </ItemGroup>
-
   <ItemGroup>
     <ProjectReference Include="../../Emby.Dlna/Emby.Dlna.csproj" />
   </ItemGroup>

+ 0 - 17
tests/Jellyfin.Extensions.Tests/Jellyfin.Extensions.Tests.csproj

@@ -1,11 +1,5 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
-  <PropertyGroup>
-    <TargetFramework>net7.0</TargetFramework>
-    <IsPackable>false</IsPackable>
-    <CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
-  </PropertyGroup>
-
   <ItemGroup>
     <PackageReference Include="Microsoft.NET.Test.Sdk" />
     <PackageReference Include="xunit" />
@@ -20,17 +14,6 @@
     <PackageReference Include="FsCheck.Xunit" />
   </ItemGroup>
 
-  <!-- Code Analyzers -->
-  <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
-    <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
-      <PrivateAssets>all</PrivateAssets>
-      <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
-    </PackageReference>
-    <PackageReference Include="SerilogAnalyzer" PrivateAssets="All" />
-    <PackageReference Include="StyleCop.Analyzers" PrivateAssets="All" />
-    <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" />
-  </ItemGroup>
-
   <ItemGroup>
     <ProjectReference Include="../../MediaBrowser.Model/MediaBrowser.Model.csproj" />
     <ProjectReference Include="../../src/Jellyfin.Extensions/Jellyfin.Extensions.csproj" />

+ 0 - 16
tests/Jellyfin.MediaEncoding.Hls.Tests/Jellyfin.MediaEncoding.Hls.Tests.csproj

@@ -1,11 +1,5 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
-  <PropertyGroup>
-    <TargetFramework>net7.0</TargetFramework>
-    <IsPackable>false</IsPackable>
-    <CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
-  </PropertyGroup>
-
   <ItemGroup>
     <PackageReference Include="Microsoft.NET.Test.Sdk" />
     <PackageReference Include="xunit" />
@@ -19,16 +13,6 @@
     </PackageReference>
   </ItemGroup>
 
-  <!-- Code Analyzers -->
-  <ItemGroup>
-    <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
-      <PrivateAssets>all</PrivateAssets>
-      <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
-    </PackageReference>
-    <PackageReference Include="SerilogAnalyzer" PrivateAssets="All" />
-    <PackageReference Include="StyleCop.Analyzers" PrivateAssets="All" />
-    <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" />
-  </ItemGroup>
   <ItemGroup>
     <ProjectReference Include="..\..\src\Jellyfin.MediaEncoding.Hls\Jellyfin.MediaEncoding.Hls.csproj" />
     <ProjectReference Include="..\..\src\Jellyfin.MediaEncoding.Keyframes\Jellyfin.MediaEncoding.Keyframes.csproj" />

+ 0 - 18
tests/Jellyfin.MediaEncoding.Keyframes.Tests/Jellyfin.MediaEncoding.Keyframes.Tests.csproj

@@ -1,12 +1,5 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
-  <PropertyGroup>
-    <TargetFramework>net7.0</TargetFramework>
-    <IsPackable>false</IsPackable>
-    <CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
-    <RootNamespace>Jellyfin.MediaEncoding.Keyframes</RootNamespace>
-  </PropertyGroup>
-
   <ItemGroup>
     <PackageReference Include="Microsoft.NET.Test.Sdk" />
     <PackageReference Include="xunit" />
@@ -20,17 +13,6 @@
     </PackageReference>
   </ItemGroup>
 
-  <!-- Code Analyzers -->
-  <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
-    <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
-      <PrivateAssets>all</PrivateAssets>
-      <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
-    </PackageReference>
-    <PackageReference Include="SerilogAnalyzer" PrivateAssets="All" />
-    <PackageReference Include="StyleCop.Analyzers" PrivateAssets="All" />
-    <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" />
-  </ItemGroup>
-
   <ItemGroup>
     <ProjectReference Include="../../src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj" />
   </ItemGroup>

+ 0 - 17
tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj

@@ -5,12 +5,6 @@
     <ProjectGuid>{28464062-0939-4AA7-9F7B-24DDDA61A7C0}</ProjectGuid>
   </PropertyGroup>
 
-  <PropertyGroup>
-    <TargetFramework>net7.0</TargetFramework>
-    <IsPackable>false</IsPackable>
-    <CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
-  </PropertyGroup>
-
   <ItemGroup>
     <None Include="Test Data\**\*.*">
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
@@ -31,17 +25,6 @@
     </PackageReference>
   </ItemGroup>
 
-  <!-- Code Analyzers -->
-  <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
-    <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
-      <PrivateAssets>all</PrivateAssets>
-      <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
-    </PackageReference>
-    <PackageReference Include="SerilogAnalyzer" PrivateAssets="All" />
-    <PackageReference Include="StyleCop.Analyzers" PrivateAssets="All" />
-    <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" />
-  </ItemGroup>
-
   <ItemGroup>
     <ProjectReference Include="../../MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj" />
   </ItemGroup>

+ 0 - 17
tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj

@@ -1,11 +1,5 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
-  <PropertyGroup>
-    <TargetFramework>net7.0</TargetFramework>
-    <IsPackable>false</IsPackable>
-    <CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
-  </PropertyGroup>
-
   <ItemGroup>
     <PackageReference Include="Microsoft.NET.Test.Sdk" />
     <PackageReference Include="Moq" />
@@ -24,17 +18,6 @@
     </None>
   </ItemGroup>
 
-  <!-- Code Analyzers -->
-  <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
-    <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
-      <PrivateAssets>all</PrivateAssets>
-      <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
-    </PackageReference>
-    <PackageReference Include="SerilogAnalyzer" PrivateAssets="All" />
-    <PackageReference Include="StyleCop.Analyzers" PrivateAssets="All" />
-    <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" />
-  </ItemGroup>
-
   <ItemGroup>
     <ProjectReference Include="../../MediaBrowser.Model/MediaBrowser.Model.csproj" />
   </ItemGroup>

+ 0 - 17
tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj

@@ -5,12 +5,6 @@
     <ProjectGuid>{3998657B-1CCC-49DD-A19F-275DC8495F57}</ProjectGuid>
   </PropertyGroup>
 
-  <PropertyGroup>
-    <TargetFramework>net7.0</TargetFramework>
-    <IsPackable>false</IsPackable>
-    <CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
-  </PropertyGroup>
-
   <ItemGroup>
     <PackageReference Include="Microsoft.NET.Test.Sdk" />
     <PackageReference Include="Moq" />
@@ -26,15 +20,4 @@
     <ProjectReference Include="..\..\Emby.Naming\Emby.Naming.csproj" />
   </ItemGroup>
 
-  <!-- Code Analyzers-->
-  <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
-    <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
-      <PrivateAssets>all</PrivateAssets>
-      <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
-    </PackageReference>
-    <PackageReference Include="SerilogAnalyzer" PrivateAssets="All" />
-    <PackageReference Include="StyleCop.Analyzers" PrivateAssets="All" />
-    <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" />
-  </ItemGroup>
-
 </Project>

+ 0 - 17
tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj

@@ -5,12 +5,6 @@
     <ProjectGuid>{42816EA8-4511-4CBF-A9C7-7791D5DDDAE6}</ProjectGuid>
   </PropertyGroup>
 
-  <PropertyGroup>
-    <TargetFramework>net7.0</TargetFramework>
-    <IsPackable>false</IsPackable>
-    <CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
-  </PropertyGroup>
-
   <ItemGroup>
     <PackageReference Include="Microsoft.NET.Test.Sdk" />
     <PackageReference Include="xunit" />
@@ -23,17 +17,6 @@
     <PackageReference Include="Moq" />
   </ItemGroup>
 
-  <!-- Code Analyzers-->
-  <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
-    <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
-      <PrivateAssets>all</PrivateAssets>
-      <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
-    </PackageReference>
-    <PackageReference Include="SerilogAnalyzer" PrivateAssets="All" />
-    <PackageReference Include="StyleCop.Analyzers" PrivateAssets="All" />
-    <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" />
-  </ItemGroup>
-
   <ItemGroup>
     <ProjectReference Include="../../Emby.Server.Implementations/Emby.Server.Implementations.csproj" />
     <ProjectReference Include="../../MediaBrowser.Common/MediaBrowser.Common.csproj" />

+ 0 - 17
tests/Jellyfin.Providers.Tests/Jellyfin.Providers.Tests.csproj

@@ -1,11 +1,5 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
-  <PropertyGroup>
-    <TargetFramework>net7.0</TargetFramework>
-    <IsPackable>false</IsPackable>
-    <CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
-  </PropertyGroup>
-
   <ItemGroup>
     <None Include="Test Data\**\*.*">
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
@@ -26,17 +20,6 @@
     </PackageReference>
   </ItemGroup>
 
-  <!-- Code Analyzers -->
-  <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
-    <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
-      <PrivateAssets>all</PrivateAssets>
-      <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
-    </PackageReference>
-    <PackageReference Include="SerilogAnalyzer" PrivateAssets="All" />
-    <PackageReference Include="StyleCop.Analyzers" PrivateAssets="All" />
-    <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" />
-  </ItemGroup>
-
   <ItemGroup>
     <ProjectReference Include="../../MediaBrowser.Providers/MediaBrowser.Providers.csproj" />
   </ItemGroup>

+ 0 - 18
tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj

@@ -5,13 +5,6 @@
     <ProjectGuid>{2E3A1B4B-4225-4AAA-8B29-0181A84E7AEE}</ProjectGuid>
   </PropertyGroup>
 
-  <PropertyGroup>
-    <TargetFramework>net7.0</TargetFramework>
-    <IsPackable>false</IsPackable>
-    <CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
-    <RootNamespace>Jellyfin.Server.Implementations.Tests</RootNamespace>
-  </PropertyGroup>
-
   <ItemGroup>
     <None Include="Test Data\**\*.*">
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
@@ -32,17 +25,6 @@
     <PackageReference Include="coverlet.collector" />
   </ItemGroup>
 
-  <!-- Code Analyzers -->
-  <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
-    <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
-      <PrivateAssets>all</PrivateAssets>
-      <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
-    </PackageReference>
-    <PackageReference Include="SerilogAnalyzer" PrivateAssets="All" />
-    <PackageReference Include="StyleCop.Analyzers" PrivateAssets="All" />
-    <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" />
-  </ItemGroup>
-
   <ItemGroup>
     <ProjectReference Include="..\..\Emby.Server.Implementations\Emby.Server.Implementations.csproj" />
     <ProjectReference Include="..\..\Jellyfin.Server.Implementations\Jellyfin.Server.Implementations.csproj" />

+ 19 - 0
tests/Jellyfin.Server.Implementations.Tests/LiveTv/Listings/XmlTvListingsProviderTests.cs

@@ -67,4 +67,23 @@ public class XmlTvListingsProviderTests
         Assert.Equal("https://domain.tld/image.png", program.ImageUrl);
         Assert.Equal("3297", program.ChannelId);
     }
+
+    [Theory]
+    [InlineData("Test Data/LiveTv/Listings/XmlTv/emptycategory.xml")]
+    [InlineData("https://example.com/emptycategory.xml")]
+    public async Task GetProgramsAsync_EmptyCategories_Success(string path)
+    {
+        var info = new ListingsProviderInfo()
+        {
+            Path = path
+        };
+
+        var startDate = new DateTime(2022, 11, 4);
+        var programs = await _xmlTvListingsProvider.GetProgramsAsync(info, "3297", startDate, startDate.AddDays(1), CancellationToken.None);
+        var programsList = programs.ToList();
+        Assert.Single(programsList);
+        var program = programsList[0];
+        Assert.DoesNotContain(program.Genres, g => string.IsNullOrEmpty(g));
+        Assert.Equal("3297", program.ChannelId);
+    }
 }

+ 6 - 0
tests/Jellyfin.Server.Implementations.Tests/Test Data/LiveTv/Listings/XmlTv/emptycategory.xml

@@ -0,0 +1,6 @@
+<tv date="20221104">
+  <programme channel="3297" start="20221104130000 -0400" stop="20221105235959 -0400">
+    <category lang="en" />
+    <category lang="en">sports</category>
+  </programme>
+</tv>

+ 28 - 0
tests/Jellyfin.Server.Integration.Tests/AuthHelper.cs

@@ -8,6 +8,7 @@ using System.Threading.Tasks;
 using Jellyfin.Api.Models.StartupDtos;
 using Jellyfin.Api.Models.UserDtos;
 using Jellyfin.Extensions.Json;
+using MediaBrowser.Model.Dto;
 using Xunit;
 
 namespace Jellyfin.Server.Integration.Tests
@@ -43,6 +44,33 @@ namespace Jellyfin.Server.Integration.Tests
             return auth!.AccessToken;
         }
 
+        public static async Task<UserDto> GetUserDtoAsync(HttpClient client)
+        {
+            using var response = await client.GetAsync("Users/Me").ConfigureAwait(false);
+            Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+            var userDto = await JsonSerializer.DeserializeAsync<UserDto>(
+                    await response.Content.ReadAsStreamAsync().ConfigureAwait(false), JsonDefaults.Options).ConfigureAwait(false);
+            Assert.NotNull(userDto);
+            return userDto;
+        }
+
+        public static async Task<BaseItemDto> GetRootFolderDtoAsync(HttpClient client, Guid userId = default)
+        {
+            if (userId.Equals(default))
+            {
+                var userDto = await GetUserDtoAsync(client).ConfigureAwait(false);
+                userId = userDto.Id;
+            }
+
+            var response = await client.GetAsync($"Users/{userId}/Items/Root").ConfigureAwait(false);
+            Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+            var rootDto = await JsonSerializer.DeserializeAsync<BaseItemDto>(
+                    await response.Content.ReadAsStreamAsync().ConfigureAwait(false),
+                    JsonDefaults.Options).ConfigureAwait(false);
+            Assert.NotNull(rootDto);
+            return rootDto;
+        }
+
         public static void AddAuthHeader(this HttpHeaders headers, string accessToken)
         {
             headers.Add(AuthHeaderName, DummyAuthHeader + $", Token={accessToken}");

+ 64 - 0
tests/Jellyfin.Server.Integration.Tests/Controllers/ItemsControllerTests.cs

@@ -0,0 +1,64 @@
+using System;
+using System.Globalization;
+using System.Net;
+using System.Text.Json;
+using System.Threading.Tasks;
+using Jellyfin.Extensions.Json;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Querying;
+using Xunit;
+
+namespace Jellyfin.Server.Integration.Tests.Controllers;
+
+public sealed class ItemsControllerTests : IClassFixture<JellyfinApplicationFactory>
+{
+    private readonly JellyfinApplicationFactory _factory;
+    private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
+    private static string? _accessToken;
+
+    public ItemsControllerTests(JellyfinApplicationFactory factory)
+    {
+        _factory = factory;
+    }
+
+    [Fact]
+    public async Task GetItems_NoApiKeyOrUserId_BadRequest()
+    {
+        var client = _factory.CreateClient();
+        client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+
+        var response = await client.GetAsync("Items").ConfigureAwait(false);
+        Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
+    }
+
+    [Theory]
+    [InlineData("Users/{0}/Items")]
+    [InlineData("Users/{0}/Items/Resume")]
+    public async Task GetUserItems_NonExistentUserId_NotFound(string format)
+    {
+        var client = _factory.CreateClient();
+        client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+
+        var response = await client.GetAsync(string.Format(CultureInfo.InvariantCulture, format, Guid.NewGuid())).ConfigureAwait(false);
+        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
+    }
+
+    [Theory]
+    [InlineData("Items?userId={0}")]
+    [InlineData("Users/{0}/Items")]
+    [InlineData("Users/{0}/Items/Resume")]
+    public async Task GetItems_UserId_Ok(string format)
+    {
+        var client = _factory.CreateClient();
+        client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+
+        var userDto = await AuthHelper.GetUserDtoAsync(client).ConfigureAwait(false);
+
+        var response = await client.GetAsync(string.Format(CultureInfo.InvariantCulture, format, userDto.Id)).ConfigureAwait(false);
+        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+        var items = await JsonSerializer.DeserializeAsync<QueryResult<BaseItemDto>>(
+                    await response.Content.ReadAsStreamAsync().ConfigureAwait(false),
+                    _jsonOptions).ConfigureAwait(false);
+        Assert.NotNull(items);
+    }
+}

+ 40 - 0
tests/Jellyfin.Server.Integration.Tests/Controllers/LibraryControllerTests.cs

@@ -0,0 +1,40 @@
+using System;
+using System.Globalization;
+using System.Net;
+using System.Threading.Tasks;
+using Xunit;
+
+namespace Jellyfin.Server.Integration.Tests.Controllers;
+
+public sealed class LibraryControllerTests : IClassFixture<JellyfinApplicationFactory>
+{
+    private readonly JellyfinApplicationFactory _factory;
+    private static string? _accessToken;
+
+    public LibraryControllerTests(JellyfinApplicationFactory factory)
+    {
+        _factory = factory;
+    }
+
+    [Theory]
+    [InlineData("Items/{0}/File")]
+    [InlineData("Items/{0}/ThemeSongs")]
+    [InlineData("Items/{0}/ThemeVideos")]
+    [InlineData("Items/{0}/ThemeMedia")]
+    [InlineData("Items/{0}/Ancestors")]
+    [InlineData("Items/{0}/Download")]
+    [InlineData("Artists/{0}/Similar")]
+    [InlineData("Items/{0}/Similar")]
+    [InlineData("Albums/{0}/Similar")]
+    [InlineData("Shows/{0}/Similar")]
+    [InlineData("Movies/{0}/Similar")]
+    [InlineData("Trailers/{0}/Similar")]
+    public async Task Get_NonExistentItemId_NotFound(string format)
+    {
+        var client = _factory.CreateClient();
+        client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+
+        var response = await client.GetAsync(string.Format(CultureInfo.InvariantCulture, format, Guid.NewGuid())).ConfigureAwait(false);
+        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
+    }
+}

+ 26 - 0
tests/Jellyfin.Server.Integration.Tests/Controllers/MusicGenreControllerTests.cs

@@ -0,0 +1,26 @@
+using System.Net;
+using System.Threading.Tasks;
+using Xunit;
+
+namespace Jellyfin.Server.Integration.Tests.Controllers;
+
+public sealed class MusicGenreControllerTests : IClassFixture<JellyfinApplicationFactory>
+{
+    private readonly JellyfinApplicationFactory _factory;
+    private static string? _accessToken;
+
+    public MusicGenreControllerTests(JellyfinApplicationFactory factory)
+    {
+        _factory = factory;
+    }
+
+    [Fact]
+    public async Task MusicGenres_FakeMusicGenre_NotFound()
+    {
+        var client = _factory.CreateClient();
+        client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+
+        var response = await client.GetAsync("MusicGenres/Fake-MusicGenre").ConfigureAwait(false);
+        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
+    }
+}

+ 26 - 15
tests/Jellyfin.Server.Integration.Tests/Controllers/PlaystateControllerTests.cs

@@ -1,18 +1,13 @@
 using System;
 using System.Net;
-using System.Net.Http;
 using System.Threading.Tasks;
 using Xunit;
-using Xunit.Priority;
 
 namespace Jellyfin.Server.Integration.Tests.Controllers;
 
-[TestCaseOrderer(PriorityOrderer.Name, PriorityOrderer.Assembly)]
 public class PlaystateControllerTests : IClassFixture<JellyfinApplicationFactory>
 {
     private readonly JellyfinApplicationFactory _factory;
-    private static readonly Guid _testUserId = Guid.NewGuid();
-    private static readonly Guid _testItemId = Guid.NewGuid();
     private static string? _accessToken;
 
     public PlaystateControllerTests(JellyfinApplicationFactory factory)
@@ -20,31 +15,47 @@ public class PlaystateControllerTests : IClassFixture<JellyfinApplicationFactory
         _factory = factory;
     }
 
-    private Task<HttpResponseMessage> DeleteUserPlayedItems(HttpClient httpClient, Guid userId, Guid itemId)
-        => httpClient.DeleteAsync($"Users/{userId}/PlayedItems/{itemId}");
+    [Fact]
+    public async Task DeleteMarkUnplayedItem_NonExistentUserId_NotFound()
+    {
+        var client = _factory.CreateClient();
+        client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+
+        using var response = await client.DeleteAsync($"Users/{Guid.NewGuid()}/PlayedItems/{Guid.NewGuid()}").ConfigureAwait(false);
+        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
+    }
+
+    [Fact]
+    public async Task PostMarkPlayedItem_NonExistentUserId_NotFound()
+    {
+        var client = _factory.CreateClient();
+        client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
 
-    private Task<HttpResponseMessage> PostUserPlayedItems(HttpClient httpClient, Guid userId, Guid itemId)
-        => httpClient.PostAsync($"Users/{userId}/PlayedItems/{itemId}", null);
+        using var response = await client.PostAsync($"Users/{Guid.NewGuid()}/PlayedItems/{Guid.NewGuid()}", null).ConfigureAwait(false);
+        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
+    }
 
     [Fact]
-    [Priority(0)]
-    public async Task DeleteMarkUnplayedItem_DoesNotExist_NotFound()
+    public async Task DeleteMarkUnplayedItem_NonExistentItemId_NotFound()
     {
         var client = _factory.CreateClient();
         client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
 
-        using var response = await DeleteUserPlayedItems(client, _testUserId, _testItemId).ConfigureAwait(false);
+        var userDto = await AuthHelper.GetUserDtoAsync(client).ConfigureAwait(false);
+
+        using var response = await client.DeleteAsync($"Users/{userDto.Id}/PlayedItems/{Guid.NewGuid()}").ConfigureAwait(false);
         Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
     }
 
     [Fact]
-    [Priority(0)]
-    public async Task PostMarkPlayedItem_DoesNotExist_NotFound()
+    public async Task PostMarkPlayedItem_NonExistentItemId_NotFound()
     {
         var client = _factory.CreateClient();
         client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
 
-        using var response = await PostUserPlayedItems(client, _testUserId, _testItemId).ConfigureAwait(false);
+        var userDto = await AuthHelper.GetUserDtoAsync(client).ConfigureAwait(false);
+
+        using var response = await client.PostAsync($"Users/{userDto.Id}/PlayedItems/{Guid.NewGuid()}", null).ConfigureAwait(false);
         Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
     }
 }

+ 27 - 0
tests/Jellyfin.Server.Integration.Tests/Controllers/SessionControllerTests.cs

@@ -0,0 +1,27 @@
+using System;
+using System.Net;
+using System.Threading.Tasks;
+using Xunit;
+
+namespace Jellyfin.Server.Integration.Tests.Controllers;
+
+public class SessionControllerTests : IClassFixture<JellyfinApplicationFactory>
+{
+    private readonly JellyfinApplicationFactory _factory;
+    private static string? _accessToken;
+
+    public SessionControllerTests(JellyfinApplicationFactory factory)
+    {
+        _factory = factory;
+    }
+
+    [Fact]
+    public async Task GetSessions_NonExistentUserId_NotFound()
+    {
+        var client = _factory.CreateClient();
+        client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+
+        using var response = await client.GetAsync($"Session/Sessions?userId={Guid.NewGuid()}").ConfigureAwait(false);
+        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
+    }
+}

+ 24 - 1
tests/Jellyfin.Server.Integration.Tests/Controllers/UserControllerTests.cs

@@ -66,6 +66,16 @@ namespace Jellyfin.Server.Integration.Tests.Controllers
             Assert.False(users![0].HasConfiguredPassword);
         }
 
+        [Fact]
+        [Priority(-1)]
+        public async Task Me_Valid_Success()
+        {
+            var client = _factory.CreateClient();
+            client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+
+            _ = await AuthHelper.GetUserDtoAsync(client).ConfigureAwait(false);
+        }
+
         [Fact]
         [Priority(0)]
         public async Task New_Valid_Success()
@@ -108,13 +118,26 @@ namespace Jellyfin.Server.Integration.Tests.Controllers
 
             var createRequest = new CreateUserByName()
             {
-                Name = username
+                Name = username!
             };
 
             using var response = await CreateUserByName(client, createRequest).ConfigureAwait(false);
             Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
         }
 
+        [Fact]
+        [Priority(0)]
+        public async Task Delete_DoesntExist_NotFound()
+        {
+            var client = _factory.CreateClient();
+
+            // access token can't be null here as the previous test populated it
+            client.DefaultRequestHeaders.AddAuthHeader(_accessToken!);
+
+            using var response = await client.DeleteAsync($"User/{Guid.NewGuid()}").ConfigureAwait(false);
+            Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
+        }
+
         [Fact]
         [Priority(1)]
         public async Task UpdateUserPassword_Valid_Success()

+ 129 - 0
tests/Jellyfin.Server.Integration.Tests/Controllers/UserLibraryControllerTests.cs

@@ -0,0 +1,129 @@
+using System;
+using System.Globalization;
+using System.Net;
+using System.Text.Json;
+using System.Threading.Tasks;
+using Jellyfin.Extensions.Json;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Querying;
+using Xunit;
+
+namespace Jellyfin.Server.Integration.Tests.Controllers;
+
+public sealed class UserLibraryControllerTests : IClassFixture<JellyfinApplicationFactory>
+{
+    private readonly JellyfinApplicationFactory _factory;
+    private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
+    private static string? _accessToken;
+
+    public UserLibraryControllerTests(JellyfinApplicationFactory factory)
+    {
+        _factory = factory;
+    }
+
+    [Fact]
+    public async Task GetRootFolder_NonExistenUserId_NotFound()
+    {
+        var client = _factory.CreateClient();
+        client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+
+        var response = await client.GetAsync($"Users/{Guid.NewGuid()}/Items/Root").ConfigureAwait(false);
+        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
+    }
+
+    [Fact]
+    public async Task GetRootFolder_UserId_Valid()
+    {
+        var client = _factory.CreateClient();
+        client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+
+        _ = await AuthHelper.GetRootFolderDtoAsync(client).ConfigureAwait(false);
+    }
+
+    [Theory]
+    [InlineData("Users/{0}/Items/{1}")]
+    [InlineData("Users/{0}/Items/{1}/Intros")]
+    [InlineData("Users/{0}/Items/{1}/LocalTrailers")]
+    [InlineData("Users/{0}/Items/{1}/SpecialFeatures")]
+    [InlineData("Users/{0}/Items/{1}/Lyrics")]
+    public async Task GetItem_NonExistenUserId_NotFound(string format)
+    {
+        var client = _factory.CreateClient();
+        client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+
+        var rootFolderDto = await AuthHelper.GetRootFolderDtoAsync(client).ConfigureAwait(false);
+
+        var response = await client.GetAsync(string.Format(CultureInfo.InvariantCulture, format, Guid.NewGuid(), rootFolderDto.Id)).ConfigureAwait(false);
+        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
+    }
+
+    [Theory]
+    [InlineData("Users/{0}/Items/{1}")]
+    [InlineData("Users/{0}/Items/{1}/Intros")]
+    [InlineData("Users/{0}/Items/{1}/LocalTrailers")]
+    [InlineData("Users/{0}/Items/{1}/SpecialFeatures")]
+    [InlineData("Users/{0}/Items/{1}/Lyrics")]
+    public async Task GetItem_NonExistentItemId_NotFound(string format)
+    {
+        var client = _factory.CreateClient();
+        client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+
+        var userDto = await AuthHelper.GetUserDtoAsync(client).ConfigureAwait(false);
+
+        var response = await client.GetAsync(string.Format(CultureInfo.InvariantCulture, format, userDto.Id, Guid.NewGuid())).ConfigureAwait(false);
+        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
+    }
+
+    [Fact]
+    public async Task GetItem_UserIdAndItemId_Valid()
+    {
+        var client = _factory.CreateClient();
+        client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+
+        var userDto = await AuthHelper.GetUserDtoAsync(client).ConfigureAwait(false);
+        var rootFolderDto = await AuthHelper.GetRootFolderDtoAsync(client, userDto.Id).ConfigureAwait(false);
+
+        var response = await client.GetAsync($"Users/{userDto.Id}/Items/{rootFolderDto.Id}").ConfigureAwait(false);
+        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+        var rootDto = await JsonSerializer.DeserializeAsync<BaseItemDto>(
+                    await response.Content.ReadAsStreamAsync().ConfigureAwait(false),
+                    _jsonOptions).ConfigureAwait(false);
+        Assert.NotNull(rootDto);
+    }
+
+    [Fact]
+    public async Task GetIntros_UserIdAndItemId_Valid()
+    {
+        var client = _factory.CreateClient();
+        client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+
+        var userDto = await AuthHelper.GetUserDtoAsync(client).ConfigureAwait(false);
+        var rootFolderDto = await AuthHelper.GetRootFolderDtoAsync(client, userDto.Id).ConfigureAwait(false);
+
+        var response = await client.GetAsync($"Users/{userDto.Id}/Items/{rootFolderDto.Id}/Intros").ConfigureAwait(false);
+        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+        var rootDto = await JsonSerializer.DeserializeAsync<QueryResult<BaseItemDto>>(
+                    await response.Content.ReadAsStreamAsync().ConfigureAwait(false),
+                    _jsonOptions).ConfigureAwait(false);
+        Assert.NotNull(rootDto);
+    }
+
+    [Theory]
+    [InlineData("Users/{0}/Items/{1}/LocalTrailers")]
+    [InlineData("Users/{0}/Items/{1}/SpecialFeatures")]
+    public async Task LocalTrailersAndSpecialFeatures_UserIdAndItemId_Valid(string format)
+    {
+        var client = _factory.CreateClient();
+        client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+
+        var userDto = await AuthHelper.GetUserDtoAsync(client).ConfigureAwait(false);
+        var rootFolderDto = await AuthHelper.GetRootFolderDtoAsync(client, userDto.Id).ConfigureAwait(false);
+
+        var response = await client.GetAsync(string.Format(CultureInfo.InvariantCulture, format, userDto.Id, rootFolderDto.Id)).ConfigureAwait(false);
+        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+        var rootDto = await JsonSerializer.DeserializeAsync<BaseItemDto[]>(
+                    await response.Content.ReadAsStreamAsync().ConfigureAwait(false),
+                    _jsonOptions).ConfigureAwait(false);
+        Assert.NotNull(rootDto);
+    }
+}

+ 27 - 0
tests/Jellyfin.Server.Integration.Tests/Controllers/VideosControllerTests.cs

@@ -0,0 +1,27 @@
+using System;
+using System.Net;
+using System.Threading.Tasks;
+using Xunit;
+
+namespace Jellyfin.Server.Integration.Tests.Controllers;
+
+public sealed class VideosControllerTests : IClassFixture<JellyfinApplicationFactory>
+{
+    private readonly JellyfinApplicationFactory _factory;
+    private static string? _accessToken;
+
+    public VideosControllerTests(JellyfinApplicationFactory factory)
+    {
+        _factory = factory;
+    }
+
+    [Fact]
+    public async Task DeleteAlternateSources_NonExistentItemId_NotFound()
+    {
+        var client = _factory.CreateClient();
+        client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+
+        var response = await client.DeleteAsync($"Videos/{Guid.NewGuid()}").ConfigureAwait(false);
+        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
+    }
+}

+ 0 - 16
tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj

@@ -1,9 +1,4 @@
 <Project Sdk="Microsoft.NET.Sdk">
-  <PropertyGroup>
-    <TargetFramework>net7.0</TargetFramework>
-    <IsPackable>false</IsPackable>
-    <CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
-  </PropertyGroup>
 
   <ItemGroup>
     <PackageReference Include="AutoFixture" />
@@ -29,17 +24,6 @@
     </None>
   </ItemGroup>
 
-  <!-- Code Analyzers -->
-  <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
-    <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
-      <PrivateAssets>all</PrivateAssets>
-      <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
-    </PackageReference>
-    <PackageReference Include="SerilogAnalyzer" PrivateAssets="All" />
-    <PackageReference Include="StyleCop.Analyzers" PrivateAssets="All" />
-    <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" />
-  </ItemGroup>
-
   <ItemGroup>
     <ProjectReference Include="../../Jellyfin.Server/Jellyfin.Server.csproj" />
   </ItemGroup>

+ 0 - 17
tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj

@@ -1,11 +1,5 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
-  <PropertyGroup>
-    <TargetFramework>net7.0</TargetFramework>
-    <IsPackable>false</IsPackable>
-    <CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
-  </PropertyGroup>
-
   <ItemGroup>
     <PackageReference Include="AutoFixture" />
     <PackageReference Include="AutoFixture.AutoMoq" />
@@ -22,17 +16,6 @@
     <PackageReference Include="Moq" />
   </ItemGroup>
 
-  <!-- Code Analyzers -->
-  <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
-    <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
-      <PrivateAssets>all</PrivateAssets>
-      <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
-    </PackageReference>
-    <PackageReference Include="SerilogAnalyzer" PrivateAssets="All" />
-    <PackageReference Include="StyleCop.Analyzers" PrivateAssets="All" />
-    <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" />
-  </ItemGroup>
-
   <ItemGroup>
     <ProjectReference Include="../../Jellyfin.Server/Jellyfin.Server.csproj" />
   </ItemGroup>

+ 0 - 17
tests/Jellyfin.XbmcMetadata.Tests/Jellyfin.XbmcMetadata.Tests.csproj

@@ -1,11 +1,5 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
-  <PropertyGroup>
-    <TargetFramework>net7.0</TargetFramework>
-    <IsPackable>false</IsPackable>
-    <CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
-  </PropertyGroup>
-
   <ItemGroup>
     <None Include="Test Data\**\*.*">
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
@@ -23,17 +17,6 @@
     <PackageReference Include="coverlet.collector" />
   </ItemGroup>
 
-  <!-- Code Analyzers -->
-  <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
-    <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
-      <PrivateAssets>all</PrivateAssets>
-      <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
-    </PackageReference>
-    <PackageReference Include="SerilogAnalyzer" PrivateAssets="All" />
-    <PackageReference Include="StyleCop.Analyzers" PrivateAssets="All" />
-    <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" />
-  </ItemGroup>
-
   <ItemGroup>
     <ProjectReference Include="../../MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj" />
     <ProjectReference Include="../../MediaBrowser.Providers/MediaBrowser.Providers.csproj" />