diff options
28 files changed, 368 insertions, 76 deletions
diff --git a/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs index 59af7ce8a..86242d137 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs @@ -11,7 +11,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books { public class BookResolver : MediaBrowser.Controller.Resolvers.ItemResolver<Book> { - private readonly string[] _validExtensions = { ".azw", ".azw3", ".cb7", ".cbr", ".cbt", ".cbz", ".epub", ".mobi", ".opf", ".pdf" }; + private readonly string[] _validExtensions = { ".azw", ".azw3", ".cb7", ".cbr", ".cbt", ".cbz", ".epub", ".mobi", ".pdf" }; protected override Book Resolve(ItemResolveArgs args) { diff --git a/Emby.Server.Implementations/Localization/Core/fr-CA.json b/Emby.Server.Implementations/Localization/Core/fr-CA.json index 0c19a0152..3c51d64e0 100644 --- a/Emby.Server.Implementations/Localization/Core/fr-CA.json +++ b/Emby.Server.Implementations/Localization/Core/fr-CA.json @@ -1,9 +1,9 @@ { "Albums": "Albums", - "AppDeviceValues": "Application : {0}, Appareil : {1}", + "AppDeviceValues": "App : {0}, Appareil : {1}", "Application": "Application", "Artists": "Artistes", - "AuthenticationSucceededWithUserName": "{0} s'est authentifié avec succès", + "AuthenticationSucceededWithUserName": "{0} authentifié avec succès", "Books": "Livres", "CameraImageUploadedFrom": "Une nouvelle image de caméra a été téléchargée depuis {0}", "Channels": "Chaînes", @@ -11,11 +11,11 @@ "Collections": "Collections", "DeviceOfflineWithName": "{0} s'est déconnecté", "DeviceOnlineWithName": "{0} est connecté", - "FailedLoginAttemptWithUserName": "Échec d'une tentative de connexion de {0}", + "FailedLoginAttemptWithUserName": "Tentative de connexion échoué par {0}", "Favorites": "Favoris", "Folders": "Dossiers", "Genres": "Genres", - "HeaderAlbumArtists": "Artistes", + "HeaderAlbumArtists": "Artistes de l'album", "HeaderContinueWatching": "Reprendre le visionnement", "HeaderFavoriteAlbums": "Albums favoris", "HeaderFavoriteArtists": "Artistes favoris", @@ -26,12 +26,12 @@ "HeaderNextUp": "À Suivre", "HeaderRecordingGroups": "Groupes d'enregistrements", "HomeVideos": "Vidéos personnelles", - "Inherit": "Hériter", + "Inherit": "Hérite", "ItemAddedWithName": "{0} a été ajouté à la médiathèque", "ItemRemovedWithName": "{0} a été supprimé de la médiathèque", "LabelIpAddressValue": "Adresse IP : {0}", "LabelRunningTimeValue": "Durée : {0}", - "Latest": "Derniers", + "Latest": "Plus récent", "MessageApplicationUpdated": "Le serveur Jellyfin a été mis à jour", "MessageApplicationUpdatedTo": "Le serveur Jellyfin a été mis à jour vers la version {0}", "MessageNamedServerConfigurationUpdatedWithValue": "La configuration de la section {0} du serveur a été mise à jour", @@ -40,15 +40,15 @@ "Movies": "Films", "Music": "Musique", "MusicVideos": "Vidéos musicales", - "NameInstallFailed": "{0} échec d'installation", + "NameInstallFailed": "échec d'installation de {0}", "NameSeasonNumber": "Saison {0}", "NameSeasonUnknown": "Saison Inconnue", - "NewVersionIsAvailable": "Une nouvelle version du serveur Jellyfin est disponible au téléchargement.", + "NewVersionIsAvailable": "Une nouvelle version du serveur Jellyfin est disponible.", "NotificationOptionApplicationUpdateAvailable": "Mise à jour de l'application disponible", "NotificationOptionApplicationUpdateInstalled": "Mise à jour de l'application installée", "NotificationOptionAudioPlayback": "Lecture audio démarrée", "NotificationOptionAudioPlaybackStopped": "Lecture audio arrêtée", - "NotificationOptionCameraImageUploaded": "L'image de l'appareil photo a été transférée", + "NotificationOptionCameraImageUploaded": "Image d'appareil photo transférée", "NotificationOptionInstallationFailed": "Échec d'installation", "NotificationOptionNewLibraryContent": "Nouveau contenu ajouté", "NotificationOptionPluginError": "Erreur d'extension", @@ -70,9 +70,9 @@ "ScheduledTaskFailedWithName": "{0} a échoué", "ScheduledTaskStartedWithName": "{0} a commencé", "ServerNameNeedsToBeRestarted": "{0} doit être redémarré", - "Shows": "Émissions", + "Shows": "Séries", "Songs": "Chansons", - "StartupEmbyServerIsLoading": "Le serveur Jellyfin est en cours de chargement. Veuillez réessayer dans quelques instants.", + "StartupEmbyServerIsLoading": "Serveur Jellyfin en cours de chargement. Réessayez dans quelques instants.", "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}", "SubtitleDownloadFailureFromForItem": "Échec du téléchargement des sous-titres depuis {0} pour {1}", "Sync": "Synchroniser", @@ -80,39 +80,43 @@ "TvShows": "Séries Télé", "User": "Utilisateur", "UserCreatedWithName": "L'utilisateur {0} a été créé", - "UserDeletedWithName": "L'utilisateur {0} a été supprimé", - "UserDownloadingItemWithValues": "{0} est en train de télécharger {1}", + "UserDeletedWithName": "L'utilisateur {0} supprimé", + "UserDownloadingItemWithValues": "{0} télécharge {1}", "UserLockedOutWithName": "L'utilisateur {0} a été verrouillé", - "UserOfflineFromDevice": "{0} s'est déconnecté depuis {1}", - "UserOnlineFromDevice": "{0} s'est connecté depuis {1}", - "UserPasswordChangedWithName": "Le mot de passe pour l'utilisateur {0} a été modifié", + "UserOfflineFromDevice": "{0} s'est déconnecté de {1}", + "UserOnlineFromDevice": "{0} s'est connecté de {1}", + "UserPasswordChangedWithName": "Le mot de passe de utilisateur {0} a été modifié", "UserPolicyUpdatedWithName": "La politique de l'utilisateur a été mise à jour pour {0}", - "UserStartedPlayingItemWithValues": "{0} est en train de lire {1} sur {2}", - "UserStoppedPlayingItemWithValues": "{0} vient d'arrêter la lecture de {1} sur {2}", + "UserStartedPlayingItemWithValues": "{0} joue {1} sur {2}", + "UserStoppedPlayingItemWithValues": "{0} a terminé la lecture de {1} sur {2}", "ValueHasBeenAddedToLibrary": "{0} a été ajouté à votre médiathèque", "ValueSpecialEpisodeName": "Spécial - {0}", "VersionNumber": "Version {0}", - "TasksLibraryCategory": "Bibliothèque", + "TasksLibraryCategory": "Médiathèque", "TasksMaintenanceCategory": "Entretien", - "TaskDownloadMissingSubtitlesDescription": "Recherche l'internet pour des sous-titres manquants à base de métadonnées configurées.", + "TaskDownloadMissingSubtitlesDescription": "Recherche les sous-titres manquant sur l'internet selon la configuration des métadonnées.", "TaskDownloadMissingSubtitles": "Télécharger les sous-titres manquants", - "TaskRefreshChannelsDescription": "Rafraîchit des informations des chaines internet.", - "TaskRefreshChannels": "Rafraîchir des chaines", - "TaskCleanTranscodeDescription": "Supprime les fichiers de transcodage de plus d'un jour.", + "TaskRefreshChannelsDescription": "Rafraîchit les informations des chaines internet.", + "TaskRefreshChannels": "Rafraîchir les chaines", + "TaskCleanTranscodeDescription": "Supprime les fichiers de transcodage datant de plus d'un jour.", "TaskCleanTranscode": "Nettoyer le répertoire de transcodage", - "TaskUpdatePluginsDescription": "Télécharger et installer les mises à jours des extensions qui sont configurés pour les m.à.j. automisés.", + "TaskUpdatePluginsDescription": "Télécharge et installe les mises à jours des extensions configurés pour les m.à.j. automatiques.", "TaskUpdatePlugins": "Mise à jour des extensions", - "TaskRefreshPeopleDescription": "Met à jour les métadonnées pour les acteurs et réalisateurs dans votre bibliothèque de médias.", - "TaskRefreshPeople": "Rafraîchir les acteurs", - "TaskCleanLogsDescription": "Supprime les journaux qui ont plus que {0} jours.", + "TaskRefreshPeopleDescription": "Met à jour les métadonnées pour les acteurs et réalisateurs dans votre médiathèque.", + "TaskRefreshPeople": "Rafraîchir les personnes", + "TaskCleanLogsDescription": "Supprime les journaux plus vieux que {0} jours.", "TaskCleanLogs": "Nettoyer le répertoire des journaux", - "TaskRefreshLibraryDescription": "Analyse votre bibliothèque média pour trouver de nouveaux fichiers et rafraîchit les métadonnées.", + "TaskRefreshLibraryDescription": "Analyse votre médiathèque pour trouver de nouveaux fichiers et rafraîchit les métadonnées.", "TaskRefreshChapterImages": "Extraire les images de chapitre", "TaskRefreshChapterImagesDescription": "Créer des vignettes pour les vidéos qui ont des chapitres.", - "TaskRefreshLibrary": "Analyser la bibliothèque de médias", + "TaskRefreshLibrary": "Analyser la médiathèque", "TaskCleanCache": "Nettoyer le répertoire des fichiers temporaires", "TasksApplicationCategory": "Application", "TaskCleanCacheDescription": "Supprime les fichiers temporaires qui ne sont plus nécessaire pour le système.", - "TasksChannelsCategory": "Canaux Internet", - "Default": "Par défaut" + "TasksChannelsCategory": "Chaines Internet", + "Default": "Par défaut", + "TaskCleanActivityLogDescription": "Éfface les entrées du journal plus anciennes que l'âge configuré.", + "TaskCleanActivityLog": "Nettoyer le journal d'activité", + "Undefined": "Indéfini", + "Forced": "Forcé" } diff --git a/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs b/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs index 1d87036a2..aee959c53 100644 --- a/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs +++ b/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs @@ -42,6 +42,12 @@ namespace Emby.Server.Implementations.SyncPlay private readonly ILibraryManager _libraryManager; /// <summary> + /// The map between users and counter of active sessions. + /// </summary> + private readonly ConcurrentDictionary<Guid, int> _activeUsers = + new ConcurrentDictionary<Guid, int>(); + + /// <summary> /// The map between sessions and groups. /// </summary> private readonly ConcurrentDictionary<string, Group> _sessionToGroupMap = @@ -122,6 +128,7 @@ namespace Emby.Server.Implementations.SyncPlay throw new InvalidOperationException("Could not add session to group!"); } + UpdateSessionsCounter(session.UserId, 1); group.CreateGroup(session, request, cancellationToken); } } @@ -172,6 +179,7 @@ namespace Emby.Server.Implementations.SyncPlay if (existingGroup.GroupId.Equals(request.GroupId)) { // Restore session. + UpdateSessionsCounter(session.UserId, 1); group.SessionJoin(session, request, cancellationToken); return; } @@ -185,6 +193,7 @@ namespace Emby.Server.Implementations.SyncPlay throw new InvalidOperationException("Could not add session to group!"); } + UpdateSessionsCounter(session.UserId, 1); group.SessionJoin(session, request, cancellationToken); } } @@ -223,6 +232,7 @@ namespace Emby.Server.Implementations.SyncPlay throw new InvalidOperationException("Could not remove session from group!"); } + UpdateSessionsCounter(session.UserId, -1); group.SessionLeave(session, request, cancellationToken); if (group.IsGroupEmpty()) @@ -318,6 +328,19 @@ namespace Emby.Server.Implementations.SyncPlay } } + /// <inheritdoc /> + public bool IsUserActive(Guid userId) + { + if (_activeUsers.TryGetValue(userId, out var sessionsCounter)) + { + return sessionsCounter > 0; + } + else + { + return false; + } + } + /// <summary> /// Releases unmanaged and optionally managed resources. /// </summary> @@ -343,5 +366,26 @@ namespace Emby.Server.Implementations.SyncPlay JoinGroup(session, request, CancellationToken.None); } } + + private void UpdateSessionsCounter(Guid userId, int toAdd) + { + // Update sessions counter. + var newSessionsCounter = _activeUsers.AddOrUpdate( + userId, + 1, + (key, sessionsCounter) => sessionsCounter + toAdd); + + // Should never happen. + if (newSessionsCounter < 0) + { + throw new InvalidOperationException("Sessions counter is negative!"); + } + + // Clean record if user has no more active sessions. + if (newSessionsCounter == 0) + { + _activeUsers.TryRemove(new KeyValuePair<Guid, int>(userId, newSessionsCounter)); + } + } } } diff --git a/Jellyfin.Api/Auth/SyncPlayAccessPolicy/SyncPlayAccessHandler.cs b/Jellyfin.Api/Auth/SyncPlayAccessPolicy/SyncPlayAccessHandler.cs index b5932ea6b..b898ac76c 100644 --- a/Jellyfin.Api/Auth/SyncPlayAccessPolicy/SyncPlayAccessHandler.cs +++ b/Jellyfin.Api/Auth/SyncPlayAccessPolicy/SyncPlayAccessHandler.cs @@ -3,6 +3,7 @@ using Jellyfin.Api.Helpers; using Jellyfin.Data.Enums; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.SyncPlay; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; @@ -13,20 +14,24 @@ namespace Jellyfin.Api.Auth.SyncPlayAccessPolicy /// </summary> public class SyncPlayAccessHandler : BaseAuthorizationHandler<SyncPlayAccessRequirement> { + private readonly ISyncPlayManager _syncPlayManager; private readonly IUserManager _userManager; /// <summary> /// Initializes a new instance of the <see cref="SyncPlayAccessHandler"/> class. /// </summary> + /// <param name="syncPlayManager">Instance of the <see cref="ISyncPlayManager"/> interface.</param> /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param> /// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param> /// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param> public SyncPlayAccessHandler( + ISyncPlayManager syncPlayManager, IUserManager userManager, INetworkManager networkManager, IHttpContextAccessor httpContextAccessor) : base(userManager, networkManager, httpContextAccessor) { + _syncPlayManager = syncPlayManager; _userManager = userManager; } @@ -42,10 +47,52 @@ namespace Jellyfin.Api.Auth.SyncPlayAccessPolicy var userId = ClaimHelpers.GetUserId(context.User); var user = _userManager.GetUserById(userId!.Value); - if ((requirement.RequiredAccess.HasValue && user.SyncPlayAccess == requirement.RequiredAccess) - || user.SyncPlayAccess == SyncPlayAccess.CreateAndJoinGroups) + if (requirement.RequiredAccess == SyncPlayAccessRequirementType.HasAccess) { - context.Succeed(requirement); + if (user.SyncPlayAccess == SyncPlayUserAccessType.CreateAndJoinGroups + || user.SyncPlayAccess == SyncPlayUserAccessType.JoinGroups + || _syncPlayManager.IsUserActive(userId!.Value)) + { + context.Succeed(requirement); + } + else + { + context.Fail(); + } + } + else if (requirement.RequiredAccess == SyncPlayAccessRequirementType.CreateGroup) + { + if (user.SyncPlayAccess == SyncPlayUserAccessType.CreateAndJoinGroups) + { + context.Succeed(requirement); + } + else + { + context.Fail(); + } + } + else if (requirement.RequiredAccess == SyncPlayAccessRequirementType.JoinGroup) + { + if (user.SyncPlayAccess == SyncPlayUserAccessType.CreateAndJoinGroups + || user.SyncPlayAccess == SyncPlayUserAccessType.JoinGroups) + { + context.Succeed(requirement); + } + else + { + context.Fail(); + } + } + else if (requirement.RequiredAccess == SyncPlayAccessRequirementType.IsInGroup) + { + if (_syncPlayManager.IsUserActive(userId!.Value)) + { + context.Succeed(requirement); + } + else + { + context.Fail(); + } } else { diff --git a/Jellyfin.Api/Auth/SyncPlayAccessPolicy/SyncPlayAccessRequirement.cs b/Jellyfin.Api/Auth/SyncPlayAccessPolicy/SyncPlayAccessRequirement.cs index 7fcaf69f6..6fab4c0ad 100644 --- a/Jellyfin.Api/Auth/SyncPlayAccessPolicy/SyncPlayAccessRequirement.cs +++ b/Jellyfin.Api/Auth/SyncPlayAccessPolicy/SyncPlayAccessRequirement.cs @@ -11,23 +11,15 @@ namespace Jellyfin.Api.Auth.SyncPlayAccessPolicy /// <summary> /// Initializes a new instance of the <see cref="SyncPlayAccessRequirement"/> class. /// </summary> - /// <param name="requiredAccess">A value of <see cref="SyncPlayAccess"/>.</param> - public SyncPlayAccessRequirement(SyncPlayAccess requiredAccess) + /// <param name="requiredAccess">A value of <see cref="SyncPlayAccessRequirementType"/>.</param> + public SyncPlayAccessRequirement(SyncPlayAccessRequirementType requiredAccess) { RequiredAccess = requiredAccess; } /// <summary> - /// Initializes a new instance of the <see cref="SyncPlayAccessRequirement"/> class. - /// </summary> - public SyncPlayAccessRequirement() - { - RequiredAccess = null; - } - - /// <summary> /// Gets the required SyncPlay access. /// </summary> - public SyncPlayAccess? RequiredAccess { get; } + public SyncPlayAccessRequirementType RequiredAccess { get; } } } diff --git a/Jellyfin.Api/Constants/Policies.cs b/Jellyfin.Api/Constants/Policies.cs index b35ceea1a..632dedb3c 100644 --- a/Jellyfin.Api/Constants/Policies.cs +++ b/Jellyfin.Api/Constants/Policies.cs @@ -51,13 +51,23 @@ namespace Jellyfin.Api.Constants public const string FirstTimeSetupOrIgnoreParentalControl = "FirstTimeSetupOrIgnoreParentalControl"; /// <summary> - /// Policy name for requiring access to SyncPlay. + /// Policy name for accessing SyncPlay. /// </summary> - public const string SyncPlayAccess = "SyncPlayAccess"; + public const string SyncPlayHasAccess = "SyncPlayHasAccess"; /// <summary> - /// Policy name for requiring group creation access to SyncPlay. + /// Policy name for creating a SyncPlay group. /// </summary> - public const string SyncPlayCreateGroupAccess = "SyncPlayCreateGroupAccess"; + public const string SyncPlayCreateGroup = "SyncPlayCreateGroup"; + + /// <summary> + /// Policy name for joining a SyncPlay group. + /// </summary> + public const string SyncPlayJoinGroup = "SyncPlayJoinGroup"; + + /// <summary> + /// Policy name for accessing a SyncPlay group. + /// </summary> + public const string SyncPlayIsInGroup = "SyncPlayIsInGroup"; } } diff --git a/Jellyfin.Api/Controllers/SyncPlayController.cs b/Jellyfin.Api/Controllers/SyncPlayController.cs index 471c9180d..82cbe58df 100644 --- a/Jellyfin.Api/Controllers/SyncPlayController.cs +++ b/Jellyfin.Api/Controllers/SyncPlayController.cs @@ -20,7 +20,7 @@ namespace Jellyfin.Api.Controllers /// <summary> /// The sync play controller. /// </summary> - [Authorize(Policy = Policies.SyncPlayAccess)] + [Authorize(Policy = Policies.SyncPlayHasAccess)] public class SyncPlayController : BaseJellyfinApiController { private readonly ISessionManager _sessionManager; @@ -51,7 +51,7 @@ namespace Jellyfin.Api.Controllers /// <returns>A <see cref="NoContentResult"/> indicating success.</returns> [HttpPost("New")] [ProducesResponseType(StatusCodes.Status204NoContent)] - [Authorize(Policy = Policies.SyncPlayCreateGroupAccess)] + [Authorize(Policy = Policies.SyncPlayCreateGroup)] public ActionResult SyncPlayCreateGroup( [FromBody, Required] NewGroupRequestDto requestData) { @@ -69,7 +69,7 @@ namespace Jellyfin.Api.Controllers /// <returns>A <see cref="NoContentResult"/> indicating success.</returns> [HttpPost("Join")] [ProducesResponseType(StatusCodes.Status204NoContent)] - [Authorize(Policy = Policies.SyncPlayAccess)] + [Authorize(Policy = Policies.SyncPlayJoinGroup)] public ActionResult SyncPlayJoinGroup( [FromBody, Required] JoinGroupRequestDto requestData) { @@ -86,6 +86,7 @@ namespace Jellyfin.Api.Controllers /// <returns>A <see cref="NoContentResult"/> indicating success.</returns> [HttpPost("Leave")] [ProducesResponseType(StatusCodes.Status204NoContent)] + [Authorize(Policy = Policies.SyncPlayIsInGroup)] public ActionResult SyncPlayLeaveGroup() { var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request); @@ -101,7 +102,7 @@ namespace Jellyfin.Api.Controllers /// <returns>An <see cref="IEnumerable{GroupInfoView}"/> containing the available SyncPlay groups.</returns> [HttpGet("List")] [ProducesResponseType(StatusCodes.Status200OK)] - [Authorize(Policy = Policies.SyncPlayAccess)] + [Authorize(Policy = Policies.SyncPlayJoinGroup)] public ActionResult<IEnumerable<GroupInfoDto>> SyncPlayGetGroups() { var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request); @@ -117,6 +118,7 @@ namespace Jellyfin.Api.Controllers /// <returns>A <see cref="NoContentResult"/> indicating success.</returns> [HttpPost("SetNewQueue")] [ProducesResponseType(StatusCodes.Status204NoContent)] + [Authorize(Policy = Policies.SyncPlayIsInGroup)] public ActionResult SyncPlaySetNewQueue( [FromBody, Required] PlayRequestDto requestData) { @@ -137,6 +139,7 @@ namespace Jellyfin.Api.Controllers /// <returns>A <see cref="NoContentResult"/> indicating success.</returns> [HttpPost("SetPlaylistItem")] [ProducesResponseType(StatusCodes.Status204NoContent)] + [Authorize(Policy = Policies.SyncPlayIsInGroup)] public ActionResult SyncPlaySetPlaylistItem( [FromBody, Required] SetPlaylistItemRequestDto requestData) { @@ -154,6 +157,7 @@ namespace Jellyfin.Api.Controllers /// <returns>A <see cref="NoContentResult"/> indicating success.</returns> [HttpPost("RemoveFromPlaylist")] [ProducesResponseType(StatusCodes.Status204NoContent)] + [Authorize(Policy = Policies.SyncPlayIsInGroup)] public ActionResult SyncPlayRemoveFromPlaylist( [FromBody, Required] RemoveFromPlaylistRequestDto requestData) { @@ -171,6 +175,7 @@ namespace Jellyfin.Api.Controllers /// <returns>A <see cref="NoContentResult"/> indicating success.</returns> [HttpPost("MovePlaylistItem")] [ProducesResponseType(StatusCodes.Status204NoContent)] + [Authorize(Policy = Policies.SyncPlayIsInGroup)] public ActionResult SyncPlayMovePlaylistItem( [FromBody, Required] MovePlaylistItemRequestDto requestData) { @@ -188,6 +193,7 @@ namespace Jellyfin.Api.Controllers /// <returns>A <see cref="NoContentResult"/> indicating success.</returns> [HttpPost("Queue")] [ProducesResponseType(StatusCodes.Status204NoContent)] + [Authorize(Policy = Policies.SyncPlayIsInGroup)] public ActionResult SyncPlayQueue( [FromBody, Required] QueueRequestDto requestData) { @@ -204,6 +210,7 @@ namespace Jellyfin.Api.Controllers /// <returns>A <see cref="NoContentResult"/> indicating success.</returns> [HttpPost("Unpause")] [ProducesResponseType(StatusCodes.Status204NoContent)] + [Authorize(Policy = Policies.SyncPlayIsInGroup)] public ActionResult SyncPlayUnpause() { var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request); @@ -219,6 +226,7 @@ namespace Jellyfin.Api.Controllers /// <returns>A <see cref="NoContentResult"/> indicating success.</returns> [HttpPost("Pause")] [ProducesResponseType(StatusCodes.Status204NoContent)] + [Authorize(Policy = Policies.SyncPlayIsInGroup)] public ActionResult SyncPlayPause() { var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request); @@ -234,6 +242,7 @@ namespace Jellyfin.Api.Controllers /// <returns>A <see cref="NoContentResult"/> indicating success.</returns> [HttpPost("Stop")] [ProducesResponseType(StatusCodes.Status204NoContent)] + [Authorize(Policy = Policies.SyncPlayIsInGroup)] public ActionResult SyncPlayStop() { var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request); @@ -250,6 +259,7 @@ namespace Jellyfin.Api.Controllers /// <returns>A <see cref="NoContentResult"/> indicating success.</returns> [HttpPost("Seek")] [ProducesResponseType(StatusCodes.Status204NoContent)] + [Authorize(Policy = Policies.SyncPlayIsInGroup)] public ActionResult SyncPlaySeek( [FromBody, Required] SeekRequestDto requestData) { @@ -267,6 +277,7 @@ namespace Jellyfin.Api.Controllers /// <returns>A <see cref="NoContentResult"/> indicating success.</returns> [HttpPost("Buffering")] [ProducesResponseType(StatusCodes.Status204NoContent)] + [Authorize(Policy = Policies.SyncPlayIsInGroup)] public ActionResult SyncPlayBuffering( [FromBody, Required] BufferRequestDto requestData) { @@ -288,6 +299,7 @@ namespace Jellyfin.Api.Controllers /// <returns>A <see cref="NoContentResult"/> indicating success.</returns> [HttpPost("Ready")] [ProducesResponseType(StatusCodes.Status204NoContent)] + [Authorize(Policy = Policies.SyncPlayIsInGroup)] public ActionResult SyncPlayReady( [FromBody, Required] ReadyRequestDto requestData) { @@ -309,6 +321,7 @@ namespace Jellyfin.Api.Controllers /// <returns>A <see cref="NoContentResult"/> indicating success.</returns> [HttpPost("SetIgnoreWait")] [ProducesResponseType(StatusCodes.Status204NoContent)] + [Authorize(Policy = Policies.SyncPlayIsInGroup)] public ActionResult SyncPlaySetIgnoreWait( [FromBody, Required] IgnoreWaitRequestDto requestData) { @@ -326,6 +339,7 @@ namespace Jellyfin.Api.Controllers /// <returns>A <see cref="NoContentResult"/> indicating success.</returns> [HttpPost("NextItem")] [ProducesResponseType(StatusCodes.Status204NoContent)] + [Authorize(Policy = Policies.SyncPlayIsInGroup)] public ActionResult SyncPlayNextItem( [FromBody, Required] NextItemRequestDto requestData) { @@ -343,6 +357,7 @@ namespace Jellyfin.Api.Controllers /// <returns>A <see cref="NoContentResult"/> indicating success.</returns> [HttpPost("PreviousItem")] [ProducesResponseType(StatusCodes.Status204NoContent)] + [Authorize(Policy = Policies.SyncPlayIsInGroup)] public ActionResult SyncPlayPreviousItem( [FromBody, Required] PreviousItemRequestDto requestData) { @@ -360,6 +375,7 @@ namespace Jellyfin.Api.Controllers /// <returns>A <see cref="NoContentResult"/> indicating success.</returns> [HttpPost("SetRepeatMode")] [ProducesResponseType(StatusCodes.Status204NoContent)] + [Authorize(Policy = Policies.SyncPlayIsInGroup)] public ActionResult SyncPlaySetRepeatMode( [FromBody, Required] SetRepeatModeRequestDto requestData) { @@ -377,6 +393,7 @@ namespace Jellyfin.Api.Controllers /// <returns>A <see cref="NoContentResult"/> indicating success.</returns> [HttpPost("SetShuffleMode")] [ProducesResponseType(StatusCodes.Status204NoContent)] + [Authorize(Policy = Policies.SyncPlayIsInGroup)] public ActionResult SyncPlaySetShuffleMode( [FromBody, Required] SetShuffleModeRequestDto requestData) { diff --git a/Jellyfin.Data/Entities/ActivityLog.cs b/Jellyfin.Data/Entities/ActivityLog.cs index 620e82830..e2d5c7187 100644 --- a/Jellyfin.Data/Entities/ActivityLog.cs +++ b/Jellyfin.Data/Entities/ActivityLog.cs @@ -18,7 +18,8 @@ namespace Jellyfin.Data.Entities /// <param name="name">The name.</param> /// <param name="type">The type.</param> /// <param name="userId">The user id.</param> - public ActivityLog(string name, string type, Guid userId) + /// <param name="logLevel">The log level.</param> + public ActivityLog(string name, string type, Guid userId, LogLevel logLevel = LogLevel.Information) { if (string.IsNullOrEmpty(name)) { @@ -34,7 +35,7 @@ namespace Jellyfin.Data.Entities Type = type; UserId = userId; DateCreated = DateTime.UtcNow; - LogSeverity = LogLevel.Trace; + LogSeverity = logLevel; } /// <summary> diff --git a/Jellyfin.Data/Entities/User.cs b/Jellyfin.Data/Entities/User.cs index 6d4681914..0fd8cb224 100644 --- a/Jellyfin.Data/Entities/User.cs +++ b/Jellyfin.Data/Entities/User.cs @@ -71,7 +71,7 @@ namespace Jellyfin.Data.Entities EnableAutoLogin = false; PlayDefaultAudioTrack = true; SubtitleMode = SubtitlePlaybackMode.Default; - SyncPlayAccess = SyncPlayAccess.CreateAndJoinGroups; + SyncPlayAccess = SyncPlayUserAccessType.CreateAndJoinGroups; AddDefaultPermissions(); AddDefaultPreferences(); @@ -326,7 +326,7 @@ namespace Jellyfin.Data.Entities /// <summary> /// Gets or sets the level of sync play permissions this user has. /// </summary> - public SyncPlayAccess SyncPlayAccess { get; set; } + public SyncPlayUserAccessType SyncPlayAccess { get; set; } /// <summary> /// Gets or sets the row version. diff --git a/Jellyfin.Data/Enums/SyncPlayAccessRequirementType.cs b/Jellyfin.Data/Enums/SyncPlayAccessRequirementType.cs new file mode 100644 index 000000000..8c3e6cb17 --- /dev/null +++ b/Jellyfin.Data/Enums/SyncPlayAccessRequirementType.cs @@ -0,0 +1,28 @@ +namespace Jellyfin.Data.Enums +{ + /// <summary> + /// Enum SyncPlayAccessRequirementType. + /// </summary> + public enum SyncPlayAccessRequirementType + { + /// <summary> + /// User must have access to SyncPlay, in some form. + /// </summary> + HasAccess = 0, + + /// <summary> + /// User must be able to create groups. + /// </summary> + CreateGroup = 1, + + /// <summary> + /// User must be able to join groups. + /// </summary> + JoinGroup = 2, + + /// <summary> + /// User must be in a group. + /// </summary> + IsInGroup = 3 + } +} diff --git a/Jellyfin.Data/Enums/SyncPlayAccess.cs b/Jellyfin.Data/Enums/SyncPlayUserAccessType.cs index 8c13b37a1..030d16fb9 100644 --- a/Jellyfin.Data/Enums/SyncPlayAccess.cs +++ b/Jellyfin.Data/Enums/SyncPlayUserAccessType.cs @@ -1,9 +1,9 @@ namespace Jellyfin.Data.Enums { /// <summary> - /// Enum SyncPlayAccess. + /// Enum SyncPlayUserAccessType. /// </summary> - public enum SyncPlayAccess + public enum SyncPlayUserAccessType { /// <summary> /// User can create groups and join them. diff --git a/Jellyfin.Drawing.Skia/SkiaEncoder.cs b/Jellyfin.Drawing.Skia/SkiaEncoder.cs index ee60748c7..eab5777d5 100644 --- a/Jellyfin.Drawing.Skia/SkiaEncoder.cs +++ b/Jellyfin.Drawing.Skia/SkiaEncoder.cs @@ -435,7 +435,7 @@ namespace Jellyfin.Drawing.Skia 0f, kernelOffset, SKShaderTileMode.Clamp, - false); + true); canvas.DrawBitmap( source, diff --git a/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs b/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs index 6bf6f383f..88e2b4152 100644 --- a/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs @@ -107,5 +107,28 @@ namespace Jellyfin.Server.Extensions { return appBuilder.UseMiddleware<WebSocketHandlerMiddleware>(); } + + /// <summary> + /// Adds robots.txt redirection to the application pipeline. + /// </summary> + /// <param name="appBuilder">The application builder.</param> + /// <returns>The updated application builder.</returns> + public static IApplicationBuilder UseRobotsRedirection(this IApplicationBuilder appBuilder) + { + return appBuilder.UseMiddleware<RobotsRedirectionMiddleware>(); + } + + /// <summary> + /// Adds /emby and /mediabrowser route trimming to the application pipeline. + /// </summary> + /// <remarks> + /// This must be injected before any path related middleware. + /// </remarks> + /// <param name="appBuilder">The application builder.</param> + /// <returns>The updated application builder.</returns> + public static IApplicationBuilder UsePathTrim(this IApplicationBuilder appBuilder) + { + return appBuilder.UseMiddleware<LegacyEmbyRouteRewriteMiddleware>(); + } } } diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index f0e37ff57..cd594b5c5 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -128,18 +128,32 @@ namespace Jellyfin.Server.Extensions policy.AddRequirements(new RequiresElevationRequirement()); }); options.AddPolicy( - Policies.SyncPlayAccess, + Policies.SyncPlayHasAccess, policy => { policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication); - policy.AddRequirements(new SyncPlayAccessRequirement(SyncPlayAccess.JoinGroups)); + policy.AddRequirements(new SyncPlayAccessRequirement(SyncPlayAccessRequirementType.HasAccess)); }); options.AddPolicy( - Policies.SyncPlayCreateGroupAccess, + Policies.SyncPlayCreateGroup, policy => { policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication); - policy.AddRequirements(new SyncPlayAccessRequirement(SyncPlayAccess.CreateAndJoinGroups)); + policy.AddRequirements(new SyncPlayAccessRequirement(SyncPlayAccessRequirementType.CreateGroup)); + }); + options.AddPolicy( + Policies.SyncPlayJoinGroup, + policy => + { + policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication); + policy.AddRequirements(new SyncPlayAccessRequirement(SyncPlayAccessRequirementType.JoinGroup)); + }); + options.AddPolicy( + Policies.SyncPlayIsInGroup, + policy => + { + policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication); + policy.AddRequirements(new SyncPlayAccessRequirement(SyncPlayAccessRequirementType.IsInGroup)); }); }); } diff --git a/Jellyfin.Server/Middleware/LegacyEmbyRouteRewriteMiddleware.cs b/Jellyfin.Server/Middleware/LegacyEmbyRouteRewriteMiddleware.cs new file mode 100644 index 000000000..fdd8974d2 --- /dev/null +++ b/Jellyfin.Server/Middleware/LegacyEmbyRouteRewriteMiddleware.cs @@ -0,0 +1,54 @@ +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; + +namespace Jellyfin.Server.Middleware +{ + /// <summary> + /// Removes /emby and /mediabrowser from requested route. + /// </summary> + public class LegacyEmbyRouteRewriteMiddleware + { + private const string EmbyPath = "/emby"; + private const string MediabrowserPath = "/mediabrowser"; + + private readonly RequestDelegate _next; + private readonly ILogger<LegacyEmbyRouteRewriteMiddleware> _logger; + + /// <summary> + /// Initializes a new instance of the <see cref="LegacyEmbyRouteRewriteMiddleware"/> class. + /// </summary> + /// <param name="next">The next delegate in the pipeline.</param> + /// <param name="logger">The logger.</param> + public LegacyEmbyRouteRewriteMiddleware( + RequestDelegate next, + ILogger<LegacyEmbyRouteRewriteMiddleware> logger) + { + _next = next; + _logger = logger; + } + + /// <summary> + /// Executes the middleware action. + /// </summary> + /// <param name="httpContext">The current HTTP context.</param> + /// <returns>The async task.</returns> + public async Task Invoke(HttpContext httpContext) + { + var localPath = httpContext.Request.Path.ToString(); + if (localPath.StartsWith(EmbyPath, StringComparison.OrdinalIgnoreCase)) + { + httpContext.Request.Path = localPath[EmbyPath.Length..]; + _logger.LogDebug("Removing {EmbyPath} from route.", EmbyPath); + } + else if (localPath.StartsWith(MediabrowserPath, StringComparison.OrdinalIgnoreCase)) + { + httpContext.Request.Path = localPath[MediabrowserPath.Length..]; + _logger.LogDebug("Removing {MediabrowserPath} from route.", MediabrowserPath); + } + + await _next(httpContext).ConfigureAwait(false); + } + } +}
\ No newline at end of file diff --git a/Jellyfin.Server/Middleware/RobotsRedirectionMiddleware.cs b/Jellyfin.Server/Middleware/RobotsRedirectionMiddleware.cs new file mode 100644 index 000000000..9d40d74fe --- /dev/null +++ b/Jellyfin.Server/Middleware/RobotsRedirectionMiddleware.cs @@ -0,0 +1,47 @@ +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; + +namespace Jellyfin.Server.Middleware +{ + /// <summary> + /// Redirect requests to robots.txt to web/robots.txt. + /// </summary> + public class RobotsRedirectionMiddleware + { + private readonly RequestDelegate _next; + private readonly ILogger<RobotsRedirectionMiddleware> _logger; + + /// <summary> + /// Initializes a new instance of the <see cref="RobotsRedirectionMiddleware"/> class. + /// </summary> + /// <param name="next">The next delegate in the pipeline.</param> + /// <param name="logger">The logger.</param> + public RobotsRedirectionMiddleware( + RequestDelegate next, + ILogger<RobotsRedirectionMiddleware> logger) + { + _next = next; + _logger = logger; + } + + /// <summary> + /// Executes the middleware action. + /// </summary> + /// <param name="httpContext">The current HTTP context.</param> + /// <returns>The async task.</returns> + public async Task Invoke(HttpContext httpContext) + { + var localPath = httpContext.Request.Path.ToString(); + if (string.Equals(localPath, "/robots.txt", StringComparison.OrdinalIgnoreCase)) + { + _logger.LogDebug("Redirecting robots.txt request to web/robots.txt"); + httpContext.Response.Redirect("web/robots.txt"); + return; + } + + await _next(httpContext).ConfigureAwait(false); + } + } +}
\ No newline at end of file diff --git a/Jellyfin.Server/Startup.cs b/Jellyfin.Server/Startup.cs index aa3ef5350..3395d2413 100644 --- a/Jellyfin.Server/Startup.cs +++ b/Jellyfin.Server/Startup.cs @@ -128,6 +128,8 @@ namespace Jellyfin.Server mainApp.UseHttpsRedirection(); } + // This must be injected before any path related middleware. + mainApp.UsePathTrim(); mainApp.UseStaticFiles(); if (appConfig.HostWebClient()) { @@ -142,6 +144,8 @@ namespace Jellyfin.Server RequestPath = "/web", ContentTypeProvider = extensionProvider }); + + mainApp.UseRobotsRedirection(); } mainApp.UseAuthentication(); diff --git a/MediaBrowser.Controller/SyncPlay/ISyncPlayManager.cs b/MediaBrowser.Controller/SyncPlay/ISyncPlayManager.cs index d0244563a..1c954828c 100644 --- a/MediaBrowser.Controller/SyncPlay/ISyncPlayManager.cs +++ b/MediaBrowser.Controller/SyncPlay/ISyncPlayManager.cs @@ -51,5 +51,12 @@ namespace MediaBrowser.Controller.SyncPlay /// <param name="request">The request.</param> /// <param name="cancellationToken">The cancellation token.</param> void HandleRequest(SessionInfo session, IGroupPlaybackRequest request, CancellationToken cancellationToken); + + /// <summary> + /// Checks whether a user has an active session using SyncPlay. + /// </summary> + /// <param name="userId">The user identifier to check.</param> + /// <returns><c>true</c> if the user is using SyncPlay; <c>false</c> otherwise.</returns> + bool IsUserActive(Guid userId); } } diff --git a/MediaBrowser.Model/Entities/ProviderIdsExtensions.cs b/MediaBrowser.Model/Entities/ProviderIdsExtensions.cs index 1782b42e2..98097477c 100644 --- a/MediaBrowser.Model/Entities/ProviderIdsExtensions.cs +++ b/MediaBrowser.Model/Entities/ProviderIdsExtensions.cs @@ -49,7 +49,7 @@ namespace MediaBrowser.Model.Entities } instance.ProviderIds.TryGetValue(name, out string? id); - return id; + return string.IsNullOrEmpty(id) ? null : id; } /// <summary> diff --git a/MediaBrowser.Model/Users/UserPolicy.cs b/MediaBrowser.Model/Users/UserPolicy.cs index 363b2633f..37da04adf 100644 --- a/MediaBrowser.Model/Users/UserPolicy.cs +++ b/MediaBrowser.Model/Users/UserPolicy.cs @@ -111,7 +111,7 @@ namespace MediaBrowser.Model.Users /// Gets or sets a value indicating what SyncPlay features the user can access. /// </summary> /// <value>Access level to SyncPlay features.</value> - public SyncPlayAccess SyncPlayAccess { get; set; } + public SyncPlayUserAccessType SyncPlayAccess { get; set; } public UserPolicy() { @@ -160,7 +160,7 @@ namespace MediaBrowser.Model.Users EnableContentDownloading = true; EnablePublicSharing = true; EnableRemoteAccess = true; - SyncPlayAccess = SyncPlayAccess.CreateAndJoinGroups; + SyncPlayAccess = SyncPlayUserAccessType.CreateAndJoinGroups; } } } diff --git a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj index 90222d5c8..b5e8e521c 100644 --- a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj +++ b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj @@ -18,7 +18,7 @@ <PackageReference Include="AutoFixture.Xunit2" Version="4.14.0" /> <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="5.0.1" /> <PackageReference Include="Microsoft.Extensions.Options" Version="5.0.0" /> - <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.0" /> + <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.3" /> <PackageReference Include="xunit" Version="2.4.1" /> <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" /> <PackageReference Include="coverlet.collector" Version="1.3.0" /> diff --git a/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj b/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj index e8eca6760..af4684f56 100644 --- a/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj +++ b/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj @@ -13,7 +13,7 @@ </PropertyGroup> <ItemGroup> - <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.0" /> + <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.3" /> <PackageReference Include="xunit" Version="2.4.1" /> <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" /> <PackageReference Include="coverlet.collector" Version="1.3.0" /> diff --git a/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj b/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj index 6e3fac43d..1ec88dada 100644 --- a/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj +++ b/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj @@ -13,7 +13,7 @@ </PropertyGroup> <ItemGroup> - <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.0" /> + <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.3" /> <PackageReference Include="xunit" Version="2.4.1" /> <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" /> <PackageReference Include="coverlet.collector" Version="1.3.0" /> diff --git a/tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj b/tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj index f91db6744..8c9dc4820 100644 --- a/tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj +++ b/tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj @@ -8,7 +8,7 @@ </PropertyGroup> <ItemGroup> - <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.0" /> + <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.3" /> <PackageReference Include="xunit" Version="2.4.1" /> <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" /> <PackageReference Include="coverlet.collector" Version="1.3.0" /> diff --git a/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj b/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj index e88de3811..c934ea1c2 100644 --- a/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj +++ b/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj @@ -19,7 +19,7 @@ </ItemGroup> <ItemGroup> - <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.0" /> + <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.3" /> <PackageReference Include="xunit" Version="2.4.1" /> <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" /> <PackageReference Include="coverlet.collector" Version="1.3.0" /> diff --git a/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj b/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj index 567cf34ef..6118581e1 100644 --- a/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj +++ b/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj @@ -13,7 +13,7 @@ </PropertyGroup> <ItemGroup> - <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.0" /> + <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.3" /> <PackageReference Include="xunit" Version="2.4.1" /> <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" /> <PackageReference Include="coverlet.collector" Version="1.3.0" /> diff --git a/tests/Jellyfin.Networking.Tests/NetworkTesting/Jellyfin.Networking.Tests.csproj b/tests/Jellyfin.Networking.Tests/NetworkTesting/Jellyfin.Networking.Tests.csproj index 48b0b4c7d..90782f6bb 100644 --- a/tests/Jellyfin.Networking.Tests/NetworkTesting/Jellyfin.Networking.Tests.csproj +++ b/tests/Jellyfin.Networking.Tests/NetworkTesting/Jellyfin.Networking.Tests.csproj @@ -13,7 +13,7 @@ </PropertyGroup> <ItemGroup> - <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.0" /> + <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.3" /> <PackageReference Include="xunit" Version="2.4.1" /> <PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" /> <PackageReference Include="coverlet.collector" Version="1.3.0" /> diff --git a/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj b/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj index 310219e74..bcd12deaf 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj +++ b/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj @@ -16,7 +16,7 @@ <ItemGroup> <PackageReference Include="AutoFixture" Version="4.14.0" /> <PackageReference Include="AutoFixture.AutoMoq" Version="4.14.0" /> - <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.0" /> + <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.3" /> <PackageReference Include="Moq" Version="4.15.2" /> <PackageReference Include="xunit" Version="2.4.1" /> <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" /> |
