diff options
60 files changed, 741 insertions, 200 deletions
diff --git a/Emby.Dlna/Profiles/SonyPs3Profile.cs b/Emby.Dlna/Profiles/SonyPs3Profile.cs index d56b1df50..e4a7a3a59 100644 --- a/Emby.Dlna/Profiles/SonyPs3Profile.cs +++ b/Emby.Dlna/Profiles/SonyPs3Profile.cs @@ -52,7 +52,7 @@ namespace Emby.Dlna.Profiles Container = "ts,mpegts", Type = DlnaProfileType.Video, VideoCodec = "mpeg1video,mpeg2video,h264", - AudioCodec = "ac3,mp2,mp3,aac" + AudioCodec = "aac,ac3,mp2" }, new DirectPlayProfile { @@ -92,7 +92,7 @@ namespace Emby.Dlna.Profiles { Container = "ts", VideoCodec = "h264", - AudioCodec = "ac3,aac,mp3", + AudioCodec = "aac,ac3,mp2", Type = DlnaProfileType.Video }, new TranscodingProfile diff --git a/Emby.Dlna/Profiles/SonyPs4Profile.cs b/Emby.Dlna/Profiles/SonyPs4Profile.cs index db56094e2..985df0c9a 100644 --- a/Emby.Dlna/Profiles/SonyPs4Profile.cs +++ b/Emby.Dlna/Profiles/SonyPs4Profile.cs @@ -52,7 +52,7 @@ namespace Emby.Dlna.Profiles Container = "ts,mpegts", Type = DlnaProfileType.Video, VideoCodec = "mpeg1video,mpeg2video,h264", - AudioCodec = "ac3,mp2,mp3,aac" + AudioCodec = "aac,ac3,mp2" }, new DirectPlayProfile { @@ -94,7 +94,7 @@ namespace Emby.Dlna.Profiles { Container = "ts", VideoCodec = "h264", - AudioCodec = "mp3", + AudioCodec = "aac,ac3,mp2", Type = DlnaProfileType.Video }, new TranscodingProfile diff --git a/Emby.Dlna/Profiles/Xml/Sony PlayStation 3.xml b/Emby.Dlna/Profiles/Xml/Sony PlayStation 3.xml index bafa44b82..129b188e2 100644 --- a/Emby.Dlna/Profiles/Xml/Sony PlayStation 3.xml +++ b/Emby.Dlna/Profiles/Xml/Sony PlayStation 3.xml @@ -38,7 +38,7 @@ <XmlRootAttributes /> <DirectPlayProfiles> <DirectPlayProfile container="avi" audioCodec="mp2,mp3" videoCodec="mpeg4" type="Video" /> - <DirectPlayProfile container="ts,mpegts" audioCodec="ac3,mp2,mp3,aac" videoCodec="mpeg1video,mpeg2video,h264" type="Video" /> + <DirectPlayProfile container="ts,mpegts" audioCodec="aac,ac3,mp2" videoCodec="mpeg1video,mpeg2video,h264" type="Video" /> <DirectPlayProfile container="mpeg" audioCodec="mp2" videoCodec="mpeg1video,mpeg2video" type="Video" /> <DirectPlayProfile container="mp4" audioCodec="aac,ac3" videoCodec="h264,mpeg4" type="Video" /> <DirectPlayProfile container="aac,mp3,wav" type="Audio" /> @@ -46,7 +46,7 @@ </DirectPlayProfiles> <TranscodingProfiles> <TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" /> - <TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="ac3,aac,mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" /> + <TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="aac,ac3,mp2" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" /> <TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" /> </TranscodingProfiles> <ContainerProfiles> diff --git a/Emby.Dlna/Profiles/Xml/Sony PlayStation 4.xml b/Emby.Dlna/Profiles/Xml/Sony PlayStation 4.xml index eb8e645b3..592119305 100644 --- a/Emby.Dlna/Profiles/Xml/Sony PlayStation 4.xml +++ b/Emby.Dlna/Profiles/Xml/Sony PlayStation 4.xml @@ -38,7 +38,7 @@ <XmlRootAttributes /> <DirectPlayProfiles> <DirectPlayProfile container="avi" audioCodec="mp2,mp3" videoCodec="mpeg4" type="Video" /> - <DirectPlayProfile container="ts,mpegts" audioCodec="ac3,mp2,mp3,aac" videoCodec="mpeg1video,mpeg2video,h264" type="Video" /> + <DirectPlayProfile container="ts,mpegts" audioCodec="aac,ac3,mp2" videoCodec="mpeg1video,mpeg2video,h264" type="Video" /> <DirectPlayProfile container="mpeg" audioCodec="mp2" videoCodec="mpeg1video,mpeg2video" type="Video" /> <DirectPlayProfile container="mp4,mkv,m4v" audioCodec="aac,ac3" videoCodec="h264,mpeg4" type="Video" /> <DirectPlayProfile container="aac,mp3,wav" type="Audio" /> @@ -46,7 +46,7 @@ </DirectPlayProfiles> <TranscodingProfiles> <TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Bytes" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" /> - <TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" /> + <TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="aac,ac3,mp2" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" /> <TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" /> </TranscodingProfiles> <ContainerProfiles> diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index d74ea0352..50ef71a46 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Globalization; using System.IO; using System.Linq; using System.Net; @@ -284,13 +285,6 @@ namespace Emby.Server.Implementations fileSystem.AddShortcutHandler(new MbLinkShortcutHandler(fileSystem)); - CertificateInfo = new CertificateInfo - { - Path = ServerConfigurationManager.Configuration.CertificatePath, - Password = ServerConfigurationManager.Configuration.CertificatePassword - }; - Certificate = GetCertificate(CertificateInfo); - ApplicationVersion = typeof(ApplicationHost).Assembly.GetName().Version; ApplicationVersionString = ApplicationVersion.ToString(3); ApplicationUserAgent = Name.Replace(' ', '-') + "/" + ApplicationVersionString; @@ -456,6 +450,7 @@ namespace Emby.Server.Implementations Resolve<ITaskManager>().AddTasks(GetExports<IScheduledTask>(false)); ConfigurationManager.ConfigurationUpdated += OnConfigurationUpdated; + ConfigurationManager.NamedConfigurationUpdated += OnConfigurationUpdated; _mediaEncoder.SetFFmpegPath(); @@ -505,6 +500,13 @@ namespace Emby.Server.Implementations HttpsPort = NetworkConfiguration.DefaultHttpsPort; } + CertificateInfo = new CertificateInfo + { + Path = networkConfiguration.CertificatePath, + Password = networkConfiguration.CertificatePassword + }; + Certificate = GetCertificate(CertificateInfo); + DiscoverTypes(); RegisterServices(); @@ -714,7 +716,7 @@ namespace Emby.Server.Implementations // Don't use an empty string password var password = string.IsNullOrWhiteSpace(info.Password) ? null : info.Password; - var localCert = new X509Certificate2(certificateLocation, password); + var localCert = new X509Certificate2(certificateLocation, password, X509KeyStorageFlags.UserKeySet); // localCert.PrivateKey = PrivateKey.CreateFromFile(pvk_file).RSA; if (!localCert.HasPrivateKey) { @@ -912,11 +914,11 @@ namespace Emby.Server.Implementations protected void OnConfigurationUpdated(object sender, EventArgs e) { var requiresRestart = false; + var networkConfiguration = ServerConfigurationManager.GetNetworkConfiguration(); // Don't do anything if these haven't been set yet if (HttpPort != 0 && HttpsPort != 0) { - var networkConfiguration = ServerConfigurationManager.GetNetworkConfiguration(); // Need to restart if ports have changed if (networkConfiguration.HttpServerPortNumber != HttpPort || networkConfiguration.HttpsPortNumber != HttpsPort) @@ -936,10 +938,7 @@ namespace Emby.Server.Implementations requiresRestart = true; } - var currentCertPath = CertificateInfo?.Path; - var newCertPath = ServerConfigurationManager.Configuration.CertificatePath; - - if (!string.Equals(currentCertPath, newCertPath, StringComparison.OrdinalIgnoreCase)) + if (ValidateSslCertificate(networkConfiguration)) { requiresRestart = true; } @@ -953,6 +952,33 @@ namespace Emby.Server.Implementations } /// <summary> + /// Validates the SSL certificate. + /// </summary> + /// <param name="networkConfig">The new configuration.</param> + /// <exception cref="FileNotFoundException">The certificate path doesn't exist.</exception> + private bool ValidateSslCertificate(NetworkConfiguration networkConfig) + { + var newPath = networkConfig.CertificatePath; + + if (!string.IsNullOrWhiteSpace(newPath) + && !string.Equals(CertificateInfo?.Path, newPath, StringComparison.Ordinal)) + { + if (File.Exists(newPath)) + { + return true; + } + + throw new FileNotFoundException( + string.Format( + CultureInfo.InvariantCulture, + "Certificate file '{0}' does not exist.", + newPath)); + } + + return false; + } + + /// <summary> /// Notifies that the kernel that a change has been made that requires a restart. /// </summary> public void NotifyPendingRestart() diff --git a/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs b/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs index f05a30a89..7a8ed8c29 100644 --- a/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs +++ b/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs @@ -88,7 +88,6 @@ namespace Emby.Server.Implementations.Configuration var newConfig = (ServerConfiguration)newConfiguration; ValidateMetadataPath(newConfig); - ValidateSslCertificate(newConfig); ConfigurationUpdating?.Invoke(this, new GenericEventArgs<ServerConfiguration>(newConfig)); @@ -96,31 +95,6 @@ namespace Emby.Server.Implementations.Configuration } /// <summary> - /// Validates the SSL certificate. - /// </summary> - /// <param name="newConfig">The new configuration.</param> - /// <exception cref="FileNotFoundException">The certificate path doesn't exist.</exception> - private void ValidateSslCertificate(BaseApplicationConfiguration newConfig) - { - var serverConfig = (ServerConfiguration)newConfig; - - var newPath = serverConfig.CertificatePath; - - if (!string.IsNullOrWhiteSpace(newPath) - && !string.Equals(Configuration.CertificatePath, newPath, StringComparison.Ordinal)) - { - if (!File.Exists(newPath)) - { - throw new FileNotFoundException( - string.Format( - CultureInfo.InvariantCulture, - "Certificate file '{0}' does not exist.", - newPath)); - } - } - } - - /// <summary> /// Validates the metadata path. /// </summary> /// <param name="newConfig">The new configuration.</param> 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/DisplayPreferencesController.cs b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs index 8b8f63015..f7bb968f0 100644 --- a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs +++ b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs @@ -12,6 +12,7 @@ using MediaBrowser.Model.Entities; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; namespace Jellyfin.Api.Controllers { @@ -22,14 +23,17 @@ namespace Jellyfin.Api.Controllers public class DisplayPreferencesController : BaseJellyfinApiController { private readonly IDisplayPreferencesManager _displayPreferencesManager; + private readonly ILogger<DisplayPreferencesController> _logger; /// <summary> /// Initializes a new instance of the <see cref="DisplayPreferencesController"/> class. /// </summary> /// <param name="displayPreferencesManager">Instance of <see cref="IDisplayPreferencesManager"/> interface.</param> - public DisplayPreferencesController(IDisplayPreferencesManager displayPreferencesManager) + /// <param name="logger">Instance of <see cref="ILogger{DisplayPreferencesController}"/> interface.</param> + public DisplayPreferencesController(IDisplayPreferencesManager displayPreferencesManager, ILogger<DisplayPreferencesController> logger) { _displayPreferencesManager = displayPreferencesManager; + _logger = logger; } /// <summary> @@ -61,7 +65,6 @@ namespace Jellyfin.Api.Controllers { Client = displayPreferences.Client, Id = displayPreferences.ItemId.ToString(), - ViewType = itemPreferences.ViewType.ToString(), SortBy = itemPreferences.SortBy, SortOrder = itemPreferences.SortOrder, IndexBy = displayPreferences.IndexBy?.ToString(), @@ -77,11 +80,6 @@ namespace Jellyfin.Api.Controllers dto.CustomPrefs["homesection" + homeSection.Order] = homeSection.Type.ToString().ToLowerInvariant(); } - foreach (var itemDisplayPreferences in _displayPreferencesManager.ListItemDisplayPreferences(displayPreferences.UserId, displayPreferences.Client)) - { - dto.CustomPrefs["landing-" + itemDisplayPreferences.ItemId] = itemDisplayPreferences.ViewType.ToString().ToLowerInvariant(); - } - dto.CustomPrefs["chromecastVersion"] = displayPreferences.ChromecastVersion.ToString().ToLowerInvariant(); dto.CustomPrefs["skipForwardLength"] = displayPreferences.SkipForwardLength.ToString(CultureInfo.InvariantCulture); dto.CustomPrefs["skipBackLength"] = displayPreferences.SkipBackwardLength.ToString(CultureInfo.InvariantCulture); @@ -189,10 +187,9 @@ namespace Jellyfin.Api.Controllers foreach (var key in displayPreferences.CustomPrefs.Keys.Where(key => key.StartsWith("landing-", StringComparison.OrdinalIgnoreCase))) { - if (Guid.TryParse(key.AsSpan().Slice("landing-".Length), out var preferenceId)) + if (!Enum.TryParse<ViewType>(displayPreferences.CustomPrefs[key], true, out var type)) { - var itemPreferences = _displayPreferencesManager.GetItemDisplayPreferences(existingDisplayPreferences.UserId, preferenceId, existingDisplayPreferences.Client); - itemPreferences.ViewType = Enum.Parse<ViewType>(displayPreferences.ViewType); + _logger.LogError("Invalid ViewType: {LandingScreenOption}", displayPreferences.CustomPrefs[key]); displayPreferences.CustomPrefs.Remove(key); } } @@ -204,11 +201,6 @@ namespace Jellyfin.Api.Controllers itemPrefs.RememberSorting = displayPreferences.RememberSorting; itemPrefs.ItemId = itemId; - if (Enum.TryParse<ViewType>(displayPreferences.ViewType, true, out var viewType)) - { - itemPrefs.ViewType = viewType; - } - // Set all remaining custom preferences. _displayPreferencesManager.SetCustomItemDisplayPreferences(userId, itemId, existingDisplayPreferences.Client, displayPreferences.CustomPrefs); _displayPreferencesManager.SaveChanges(); diff --git a/Jellyfin.Api/Controllers/ImageController.cs b/Jellyfin.Api/Controllers/ImageController.cs index e828a0801..c606d327c 100644 --- a/Jellyfin.Api/Controllers/ImageController.cs +++ b/Jellyfin.Api/Controllers/ImageController.cs @@ -325,9 +325,11 @@ namespace Jellyfin.Api.Controllers return NotFound(); } + await using var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false); + // Handle image/png; charset=utf-8 var mimeType = Request.ContentType.Split(';').FirstOrDefault(); - await _providerManager.SaveImage(item, Request.Body, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false); + await _providerManager.SaveImage(item, memoryStream, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false); await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false); return NoContent(); @@ -358,9 +360,11 @@ namespace Jellyfin.Api.Controllers return NotFound(); } + await using var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false); + // Handle image/png; charset=utf-8 var mimeType = Request.ContentType.Split(';').FirstOrDefault(); - await _providerManager.SaveImage(item, Request.Body, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false); + await _providerManager.SaveImage(item, memoryStream, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false); await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false); return NoContent(); diff --git a/Jellyfin.Api/Controllers/LiveTvController.cs b/Jellyfin.Api/Controllers/LiveTvController.cs index 56d4b3933..6f2d43227 100644 --- a/Jellyfin.Api/Controllers/LiveTvController.cs +++ b/Jellyfin.Api/Controllers/LiveTvController.cs @@ -1119,20 +1119,15 @@ namespace Jellyfin.Api.Controllers /// <summary> /// Set channel mappings. /// </summary> - /// <param name="providerId">Provider id.</param> - /// <param name="tunerChannelId">Tuner channel id.</param> - /// <param name="providerChannelId">Provider channel id.</param> + /// <param name="setChannelMappingDto">The set channel mapping dto.</param> /// <response code="200">Created channel mapping returned.</response> /// <returns>An <see cref="OkResult"/> containing the created channel mapping.</returns> [HttpPost("ChannelMappings")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status200OK)] - public async Task<ActionResult<TunerChannelMapping>> SetChannelMapping( - [FromQuery] string? providerId, - [FromQuery] string? tunerChannelId, - [FromQuery] string? providerChannelId) + public async Task<ActionResult<TunerChannelMapping>> SetChannelMapping([FromBody, Required] SetChannelMappingDto setChannelMappingDto) { - return await _liveTvManager.SetChannelMapping(providerId, tunerChannelId, providerChannelId).ConfigureAwait(false); + return await _liveTvManager.SetChannelMapping(setChannelMappingDto.ProviderId, setChannelMappingDto.TunerChannelId, setChannelMappingDto.ProviderChannelId).ConfigureAwait(false); } /// <summary> diff --git a/Jellyfin.Api/Controllers/MediaInfoController.cs b/Jellyfin.Api/Controllers/MediaInfoController.cs index a76dc057a..2a1da31c9 100644 --- a/Jellyfin.Api/Controllers/MediaInfoController.cs +++ b/Jellyfin.Api/Controllers/MediaInfoController.cs @@ -17,6 +17,7 @@ using MediaBrowser.Model.MediaInfo; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.Extensions.Logging; namespace Jellyfin.Api.Controllers @@ -119,7 +120,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] bool? enableTranscoding, [FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowAudioStreamCopy, - [FromBody] PlaybackInfoDto? playbackInfoDto) + [FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Allow)] PlaybackInfoDto? playbackInfoDto) { var authInfo = _authContext.GetAuthorizationInfo(Request); diff --git a/Jellyfin.Api/Controllers/PlaylistsController.cs b/Jellyfin.Api/Controllers/PlaylistsController.cs index 3e55434c0..fcdad4bc7 100644 --- a/Jellyfin.Api/Controllers/PlaylistsController.cs +++ b/Jellyfin.Api/Controllers/PlaylistsController.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; using System.Threading.Tasks; @@ -17,6 +18,7 @@ using MediaBrowser.Model.Querying; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; namespace Jellyfin.Api.Controllers { @@ -53,6 +55,13 @@ namespace Jellyfin.Api.Controllers /// <summary> /// Creates a new playlist. /// </summary> + /// <remarks> + /// For backwards compatibility parameters can be sent via Query or Body, with Query having higher precedence. + /// </remarks> + /// <param name="name">The playlist name.</param> + /// <param name="ids">The item ids.</param> + /// <param name="userId">The user id.</param> + /// <param name="mediaType">The media type.</param> /// <param name="createPlaylistRequest">The create playlist payload.</param> /// <returns> /// A <see cref="Task" /> that represents the asynchronous operation to create a playlist. @@ -61,14 +70,23 @@ namespace Jellyfin.Api.Controllers [HttpPost] [ProducesResponseType(StatusCodes.Status200OK)] public async Task<ActionResult<PlaylistCreationResult>> CreatePlaylist( - [FromBody, Required] CreatePlaylistDto createPlaylistRequest) + [FromQuery] string? name, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] IReadOnlyList<Guid> ids, + [FromQuery] Guid? userId, + [FromQuery] string? mediaType, + [FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Allow)] CreatePlaylistDto? createPlaylistRequest) { + if (ids.Count == 0) + { + ids = createPlaylistRequest?.Ids ?? Array.Empty<Guid>(); + } + var result = await _playlistManager.CreatePlaylist(new PlaylistCreationRequest { - Name = createPlaylistRequest.Name, - ItemIdList = createPlaylistRequest.Ids, - UserId = createPlaylistRequest.UserId, - MediaType = createPlaylistRequest.MediaType + Name = name ?? createPlaylistRequest?.Name, + ItemIdList = ids, + UserId = userId ?? createPlaylistRequest?.UserId ?? default, + MediaType = mediaType ?? createPlaylistRequest?.MediaType }).ConfigureAwait(false); return result; 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.Api/Models/LiveTvDtos/SetChannelMappingDto.cs b/Jellyfin.Api/Models/LiveTvDtos/SetChannelMappingDto.cs new file mode 100644 index 000000000..2ddaa89e8 --- /dev/null +++ b/Jellyfin.Api/Models/LiveTvDtos/SetChannelMappingDto.cs @@ -0,0 +1,28 @@ +using System.ComponentModel.DataAnnotations; + +namespace Jellyfin.Api.Models.LiveTvDtos +{ + /// <summary> + /// Set channel mapping dto. + /// </summary> + public class SetChannelMappingDto + { + /// <summary> + /// Gets or sets the provider id. + /// </summary> + [Required] + public string ProviderId { get; set; } = string.Empty; + + /// <summary> + /// Gets or sets the tuner channel id. + /// </summary> + [Required] + public string TunerChannelId { get; set; } = string.Empty; + + /// <summary> + /// Gets or sets the provider channel id. + /// </summary> + [Required] + public string ProviderChannelId { get; set; } = string.Empty; + } +}
\ No newline at end of file diff --git a/Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs b/Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs index d0d6889fc..65d4b644e 100644 --- a/Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs +++ b/Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs @@ -24,7 +24,7 @@ namespace Jellyfin.Api.Models.PlaylistDtos /// <summary> /// Gets or sets the user id. /// </summary> - public Guid UserId { get; set; } + public Guid? UserId { get; set; } /// <summary> /// Gets or sets the media type. 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/ItemDisplayPreferences.cs b/Jellyfin.Data/Entities/ItemDisplayPreferences.cs index d81e4a31c..2b25bb25f 100644 --- a/Jellyfin.Data/Entities/ItemDisplayPreferences.cs +++ b/Jellyfin.Data/Entities/ItemDisplayPreferences.cs @@ -23,7 +23,6 @@ namespace Jellyfin.Data.Entities Client = client; SortBy = "SortName"; - ViewType = ViewType.Poster; SortOrder = SortOrder.Ascending; RememberSorting = false; RememberIndexing = false; diff --git a/Jellyfin.Data/Entities/User.cs b/Jellyfin.Data/Entities/User.cs index a0232e156..362f3b4eb 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.Data/Enums/ViewType.cs b/Jellyfin.Data/Enums/ViewType.cs index 595429ab1..c0fd7d448 100644 --- a/Jellyfin.Data/Enums/ViewType.cs +++ b/Jellyfin.Data/Enums/ViewType.cs @@ -1,4 +1,4 @@ -namespace Jellyfin.Data.Enums +namespace Jellyfin.Data.Enums { /// <summary> /// An enum representing the type of view for a library or collection. @@ -6,33 +6,108 @@ public enum ViewType { /// <summary> - /// Shows banners. + /// Shows albums. /// </summary> - Banner = 0, + Albums = 0, /// <summary> - /// Shows a list of content. + /// Shows album artists. /// </summary> - List = 1, + AlbumArtists = 1, /// <summary> - /// Shows poster artwork. + /// Shows artists. /// </summary> - Poster = 2, + Artists = 2, /// <summary> - /// Shows poster artwork with a card containing the name and year. + /// Shows channels. /// </summary> - PosterCard = 3, + Channels = 3, /// <summary> - /// Shows a thumbnail. + /// Shows collections. /// </summary> - Thumb = 4, + Collections = 4, /// <summary> - /// Shows a thumbnail with a card containing the name and year. + /// Shows episodes. /// </summary> - ThumbCard = 5 + Episodes = 5, + + /// <summary> + /// Shows favorites. + /// </summary> + Favorites = 6, + + /// <summary> + /// Shows genres. + /// </summary> + Genres = 7, + + /// <summary> + /// Shows guide. + /// </summary> + Guide = 8, + + /// <summary> + /// Shows movies. + /// </summary> + Movies = 9, + + /// <summary> + /// Shows networks. + /// </summary> + Networks = 10, + + /// <summary> + /// Shows playlists. + /// </summary> + Playlists = 11, + + /// <summary> + /// Shows programs. + /// </summary> + Programs = 12, + + /// <summary> + /// Shows recordings. + /// </summary> + Recordings = 13, + + /// <summary> + /// Shows schedule. + /// </summary> + Schedule = 14, + + /// <summary> + /// Shows series. + /// </summary> + Series = 15, + + /// <summary> + /// Shows shows. + /// </summary> + Shows = 16, + + /// <summary> + /// Shows songs. + /// </summary> + Songs = 17, + + /// <summary> + /// Shows songs. + /// </summary> + Suggestions = 18, + + /// <summary> + /// Shows trailers. + /// </summary> + Trailers = 19, + + /// <summary> + /// Shows upcoming. + /// </summary> + Upcoming = 20 } } 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.Networking/Configuration/NetworkConfiguration.cs b/Jellyfin.Networking/Configuration/NetworkConfiguration.cs index df420f48a..792e57f6a 100644 --- a/Jellyfin.Networking/Configuration/NetworkConfiguration.cs +++ b/Jellyfin.Networking/Configuration/NetworkConfiguration.cs @@ -28,6 +28,16 @@ namespace Jellyfin.Networking.Configuration public bool RequireHttps { get; set; } /// <summary> + /// Gets or sets the filesystem path of an X.509 certificate to use for SSL. + /// </summary> + public string CertificatePath { get; set; } = string.Empty; + + /// <summary> + /// Gets or sets the password required to access the X.509 certificate data in the file specified by <see cref="CertificatePath"/>. + /// </summary> + public string CertificatePassword { get; set; } = string.Empty; + + /// <summary> /// Gets or sets a value used to specify the URL prefix that your Jellyfin instance can be accessed at. /// </summary> public string BaseUrl @@ -83,7 +93,7 @@ namespace Jellyfin.Networking.Configuration /// </summary> /// <remarks> /// In order for HTTPS to be used, in addition to setting this to true, valid values must also be - /// provided for <see cref="ServerConfiguration.CertificatePath"/> and <see cref="ServerConfiguration.CertificatePassword"/>. + /// provided for <see cref="CertificatePath"/> and <see cref="CertificatePassword"/>. /// </remarks> public bool EnableHttps { get; set; } diff --git a/Jellyfin.Server.Implementations/Activity/ActivityManager.cs b/Jellyfin.Server.Implementations/Activity/ActivityManager.cs index 7bde4f35b..27360afb0 100644 --- a/Jellyfin.Server.Implementations/Activity/ActivityManager.cs +++ b/Jellyfin.Server.Implementations/Activity/ActivityManager.cs @@ -27,7 +27,7 @@ namespace Jellyfin.Server.Implementations.Activity } /// <inheritdoc/> - public event EventHandler<GenericEventArgs<ActivityLogEntry>> EntryCreated; + public event EventHandler<GenericEventArgs<ActivityLogEntry>>? EntryCreated; /// <inheritdoc/> public async Task CreateAsync(ActivityLog entry) diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStartLogger.cs b/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStartLogger.cs index ec4a76e7f..0340248bb 100644 --- a/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStartLogger.cs +++ b/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStartLogger.cs @@ -86,7 +86,7 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Session return name; } - private static string GetPlaybackNotificationType(string mediaType) + private static string? GetPlaybackNotificationType(string mediaType) { if (string.Equals(mediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase)) { diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStopLogger.cs b/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStopLogger.cs index a0bad29e9..1648b1b47 100644 --- a/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStopLogger.cs +++ b/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStopLogger.cs @@ -94,7 +94,7 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Session return name; } - private static string GetPlaybackStoppedNotificationType(string mediaType) + private static string? GetPlaybackStoppedNotificationType(string mediaType) { if (string.Equals(mediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase)) { diff --git a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj index 5f508ea0a..9e4a2065f 100644 --- a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj +++ b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj @@ -5,6 +5,7 @@ <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateDocumentationFile>true</GenerateDocumentationFile> <TreatWarningsAsErrors>true</TreatWarningsAsErrors> + <Nullable>enable</Nullable> </PropertyGroup> <PropertyGroup Condition=" '$(Configuration)' == 'Debug' "> diff --git a/Jellyfin.Server.Implementations/JellyfinDb.cs b/Jellyfin.Server.Implementations/JellyfinDb.cs index 7f3f83749..39f842354 100644 --- a/Jellyfin.Server.Implementations/JellyfinDb.cs +++ b/Jellyfin.Server.Implementations/JellyfinDb.cs @@ -1,3 +1,4 @@ +#nullable disable #pragma warning disable CS1591 using System; diff --git a/Jellyfin.Server.Implementations/Users/DefaultAuthenticationProvider.cs b/Jellyfin.Server.Implementations/Users/DefaultAuthenticationProvider.cs index 662b4bf65..6a78e7ee6 100644 --- a/Jellyfin.Server.Implementations/Users/DefaultAuthenticationProvider.cs +++ b/Jellyfin.Server.Implementations/Users/DefaultAuthenticationProvider.cs @@ -1,5 +1,3 @@ -#nullable enable - using System; using System.Linq; using System.Text; diff --git a/Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs b/Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs index 334f27f85..9cc1c3e5e 100644 --- a/Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs +++ b/Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs @@ -1,5 +1,3 @@ -#nullable enable - using System; using System.Collections.Generic; using System.IO; diff --git a/Jellyfin.Server.Implementations/Users/DeviceAccessEntryPoint.cs b/Jellyfin.Server.Implementations/Users/DeviceAccessEntryPoint.cs index 1fb89c4a6..dbba80c21 100644 --- a/Jellyfin.Server.Implementations/Users/DeviceAccessEntryPoint.cs +++ b/Jellyfin.Server.Implementations/Users/DeviceAccessEntryPoint.cs @@ -1,5 +1,4 @@ -#nullable enable -#pragma warning disable CS1591 +#pragma warning disable CS1591 using System.Threading.Tasks; using Jellyfin.Data.Entities; diff --git a/Jellyfin.Server.Implementations/Users/InvalidAuthProvider.cs b/Jellyfin.Server.Implementations/Users/InvalidAuthProvider.cs index 5f32479e1..c4e4c460a 100644 --- a/Jellyfin.Server.Implementations/Users/InvalidAuthProvider.cs +++ b/Jellyfin.Server.Implementations/Users/InvalidAuthProvider.cs @@ -1,5 +1,3 @@ -#nullable enable - using System.Threading.Tasks; using Jellyfin.Data.Entities; using MediaBrowser.Controller.Authentication; diff --git a/Jellyfin.Server.Implementations/Users/UserManager.cs b/Jellyfin.Server.Implementations/Users/UserManager.cs index 400f02ef2..d1de5408c 100644 --- a/Jellyfin.Server.Implementations/Users/UserManager.cs +++ b/Jellyfin.Server.Implementations/Users/UserManager.cs @@ -1,5 +1,4 @@ -#nullable enable -#pragma warning disable CA1307 +#pragma warning disable CA1307 using System; using System.Collections.Concurrent; diff --git a/Jellyfin.Server.Implementations/ValueConverters/DateTimeKindValueConverter.cs b/Jellyfin.Server.Implementations/ValueConverters/DateTimeKindValueConverter.cs index 8a510898b..a9a18c823 100644 --- a/Jellyfin.Server.Implementations/ValueConverters/DateTimeKindValueConverter.cs +++ b/Jellyfin.Server.Implementations/ValueConverters/DateTimeKindValueConverter.cs @@ -13,9 +13,9 @@ namespace Jellyfin.Server.Implementations.ValueConverters /// </summary> /// <param name="kind">The kind to specify.</param> /// <param name="mappingHints">The mapping hints.</param> - public DateTimeKindValueConverter(DateTimeKind kind, ConverterMappingHints mappingHints = null) + public DateTimeKindValueConverter(DateTimeKind kind, ConverterMappingHints? mappingHints = null) : base(v => v.ToUniversalTime(), v => DateTime.SpecifyKind(v, kind), mappingHints) { } } -}
\ No newline at end of file +} 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 b256c869c..cd594b5c5 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -24,6 +24,7 @@ using Jellyfin.Server.Configuration; using Jellyfin.Server.Filters; using Jellyfin.Server.Formatters; using MediaBrowser.Common.Json; +using MediaBrowser.Common.Net; using MediaBrowser.Model.Entities; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authorization; @@ -127,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)); }); }); } @@ -178,9 +193,9 @@ namespace Jellyfin.Server.Extensions { for (var i = 0; i < knownProxies.Count; i++) { - if (IPAddress.TryParse(knownProxies[i], out var address)) + if (IPHost.TryParse(knownProxies[i], out var host)) { - options.KnownProxies.Add(address); + options.KnownProxies.Add(host.Address); } } } 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/Migrations/Routines/MigrateDisplayPreferencesDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs index af4be5a26..dd005b7f4 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs @@ -81,6 +81,7 @@ namespace Jellyfin.Server.Migrations.Routines { "unstable", ChromecastVersion.Unstable } }; + var customDisplayPrefs = new HashSet<string>(); var dbFilePath = Path.Combine(_paths.DataPath, DbFilename); using (var connection = SQLite3.Open(dbFilePath, ConnectionFlags.ReadOnly, null)) { @@ -185,7 +186,13 @@ namespace Jellyfin.Server.Migrations.Routines foreach (var (key, value) in dto.CustomPrefs) { - dbContext.Add(new CustomItemDisplayPreferences(displayPreferences.UserId, itemId, displayPreferences.Client, key, value)); + // Custom display preferences can have a key collision. + var indexKey = $"{displayPreferences.UserId}|{itemId}|{displayPreferences.Client}|{key}"; + if (!customDisplayPrefs.Contains(indexKey)) + { + dbContext.Add(new CustomItemDisplayPreferences(displayPreferences.UserId, itemId, displayPreferences.Client, key, value)); + customDisplayPrefs.Add(indexKey); + } } dbContext.Add(displayPreferences); 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/BaseItemManager/BaseItemManager.cs b/MediaBrowser.Controller/BaseItemManager/BaseItemManager.cs index 299c555d0..31dd95402 100644 --- a/MediaBrowser.Controller/BaseItemManager/BaseItemManager.cs +++ b/MediaBrowser.Controller/BaseItemManager/BaseItemManager.cs @@ -51,7 +51,7 @@ namespace MediaBrowser.Controller.BaseItemManager var typeOptions = libraryOptions.GetTypeOptions(baseItem.GetType().Name); if (typeOptions != null) { - return typeOptions.ImageFetchers.Contains(name, StringComparer.OrdinalIgnoreCase); + return typeOptions.MetadataFetchers.Contains(name, StringComparer.OrdinalIgnoreCase); } if (!libraryOptions.EnableInternetProviders) @@ -61,7 +61,7 @@ namespace MediaBrowser.Controller.BaseItemManager var itemConfig = _serverConfigurationManager.Configuration.MetadataOptions.FirstOrDefault(i => string.Equals(i.ItemType, GetType().Name, StringComparison.OrdinalIgnoreCase)); - return itemConfig == null || !itemConfig.DisabledImageFetchers.Contains(name, StringComparer.OrdinalIgnoreCase); + return itemConfig == null || !itemConfig.DisabledMetadataFetchers.Contains(name, StringComparer.OrdinalIgnoreCase); } /// <inheritdoc /> 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/Configuration/EncodingOptions.cs b/MediaBrowser.Model/Configuration/EncodingOptions.cs index 100756c24..38b333510 100644 --- a/MediaBrowser.Model/Configuration/EncodingOptions.cs +++ b/MediaBrowser.Model/Configuration/EncodingOptions.cs @@ -88,11 +88,11 @@ namespace MediaBrowser.Model.Configuration // The left side of the dot is the platform number, and the right side is the device number on the platform. OpenclDevice = "0.0"; EnableTonemapping = false; - TonemappingAlgorithm = "reinhard"; + TonemappingAlgorithm = "hable"; TonemappingRange = "auto"; TonemappingDesat = 0; TonemappingThreshold = 0.8; - TonemappingPeak = 0; + TonemappingPeak = 100; TonemappingParam = 0; H264Crf = 23; H265Crf = 28; 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/MediaBrowser.Providers/TV/SeriesMetadataService.cs b/MediaBrowser.Providers/TV/SeriesMetadataService.cs index c8fc568a2..967908197 100644 --- a/MediaBrowser.Providers/TV/SeriesMetadataService.cs +++ b/MediaBrowser.Providers/TV/SeriesMetadataService.cs @@ -1,10 +1,16 @@ #pragma warning disable CS1591 +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Globalization; using MediaBrowser.Model.IO; using MediaBrowser.Providers.Manager; using Microsoft.Extensions.Logging; @@ -13,14 +19,27 @@ namespace MediaBrowser.Providers.TV { public class SeriesMetadataService : MetadataService<Series, SeriesInfo> { + private readonly ILocalizationManager _localizationManager; + public SeriesMetadataService( IServerConfigurationManager serverConfigurationManager, ILogger<SeriesMetadataService> logger, IProviderManager providerManager, IFileSystem fileSystem, - ILibraryManager libraryManager) + ILibraryManager libraryManager, + ILocalizationManager localizationManager) : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager) { + _localizationManager = localizationManager; + } + + /// <inheritdoc /> + protected override async Task AfterMetadataRefresh(Series item, MetadataRefreshOptions refreshOptions, CancellationToken cancellationToken) + { + await base.AfterMetadataRefresh(item, refreshOptions, cancellationToken).ConfigureAwait(false); + + RemoveObsoleteSeasons(item); + await FillInMissingSeasonsAsync(item, cancellationToken).ConfigureAwait(false); } /// <inheritdoc /> @@ -62,5 +81,117 @@ namespace MediaBrowser.Providers.TV targetItem.AirDays = sourceItem.AirDays; } } + + private void RemoveObsoleteSeasons(Series series) + { + // TODO Legacy. It's not really "physical" seasons as any virtual seasons are always converted to non-virtual in FillInMissingSeasonsAsync. + var physicalSeasonNumbers = new HashSet<int>(); + var virtualSeasons = new List<Season>(); + foreach (var existingSeason in series.Children.OfType<Season>()) + { + if (existingSeason.LocationType != LocationType.Virtual && existingSeason.IndexNumber.HasValue) + { + physicalSeasonNumbers.Add(existingSeason.IndexNumber.Value); + } + else if (existingSeason.LocationType == LocationType.Virtual) + { + virtualSeasons.Add(existingSeason); + } + } + + foreach (var virtualSeason in virtualSeasons) + { + var seasonNumber = virtualSeason.IndexNumber; + // If there's a physical season with the same number or no episodes in the season, delete it + if ((seasonNumber.HasValue && physicalSeasonNumbers.Contains(seasonNumber.Value)) + || !virtualSeason.GetEpisodes().Any()) + { + Logger.LogInformation("Removing virtual season {SeasonNumber} in series {SeriesName}", virtualSeason.IndexNumber, series.Name); + + LibraryManager.DeleteItem( + virtualSeason, + new DeleteOptions + { + DeleteFileLocation = true + }, + false); + } + } + } + + /// <summary> + /// Creates seasons for all episodes that aren't in a season folder. + /// If no season number can be determined, a dummy season will be created. + /// </summary> + /// <param name="series">The series.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>The async task.</returns> + private async Task FillInMissingSeasonsAsync(Series series, CancellationToken cancellationToken) + { + var episodesInSeriesFolder = series.GetRecursiveChildren(i => i is Episode) + .Cast<Episode>() + .Where(i => !i.IsInSeasonFolder); + + List<Season> seasons = series.Children.OfType<Season>().ToList(); + + // Loop through the unique season numbers + foreach (var episode in episodesInSeriesFolder) + { + // Null season numbers will have a 'dummy' season created because seasons are always required. + var seasonNumber = episode.ParentIndexNumber >= 0 ? episode.ParentIndexNumber : null; + var existingSeason = seasons.FirstOrDefault(i => i.IndexNumber == seasonNumber); + + if (existingSeason == null) + { + var season = await CreateSeasonAsync(series, seasonNumber, cancellationToken).ConfigureAwait(false); + seasons.Add(season); + } + else if (existingSeason.IsVirtualItem) + { + existingSeason.IsVirtualItem = false; + await existingSeason.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken).ConfigureAwait(false); + } + } + } + + /// <summary> + /// Creates a new season, adds it to the database by linking it to the [series] and refreshes the metadata. + /// </summary> + /// <param name="series">The series.</param> + /// <param name="seasonNumber">The season number.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>The newly created season.</returns> + private async Task<Season> CreateSeasonAsync( + Series series, + int? seasonNumber, + CancellationToken cancellationToken) + { + string seasonName = seasonNumber switch + { + null => _localizationManager.GetLocalizedString("NameSeasonUnknown"), + 0 => LibraryManager.GetLibraryOptions(series).SeasonZeroDisplayName, + _ => string.Format(CultureInfo.InvariantCulture, _localizationManager.GetLocalizedString("NameSeasonNumber"), seasonNumber.Value) + }; + + Logger.LogInformation("Creating Season {SeasonName} entry for {SeriesName}", seasonName, series.Name); + + var season = new Season + { + Name = seasonName, + IndexNumber = seasonNumber, + Id = LibraryManager.GetNewItemId( + series.Id + (seasonNumber ?? -1).ToString(CultureInfo.InvariantCulture) + seasonName, + typeof(Season)), + IsVirtualItem = false, + SeriesId = series.Id, + SeriesName = series.Name + }; + + series.AddChild(season, cancellationToken); + + await season.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(FileSystem)), cancellationToken).ConfigureAwait(false); + + return season; + } } } @@ -105,12 +105,6 @@ There are three options to get the files for the web client. 2. Build them from source following the instructions on the [jellyfin-web repository](https://github.com/jellyfin/jellyfin-web) 3. Get the pre-built files from an existing installation of the server. For example, with a Windows server installation the client files are located at `C:\Program Files\Jellyfin\Server\jellyfin-web` -Once you have a copy of the built web client files, you need to copy them into a specific directory. - -> `<repository root>/Mediabrowser.WebDashboard/jellyfin-web` - -As part of the build process, this folder will be copied to the build output directory, where it can be accessed by the server. - ### Running The Server The following instructions will help you get the project up and running via the command line, or your preferred IDE. @@ -133,7 +127,7 @@ To run the server from the command line you can use the `dotnet run` command. Th ```bash cd jellyfin # Move into the repository directory -dotnet run --project Jellyfin.Server # Run the server startup project +dotnet run --project Jellyfin.Server --webdir /absolute/path/to/jellyfin-web/dist # Run the server startup project ``` A second option is to build the project and then run the resulting executable file directly. When running the executable directly you can easily add command line options. Add the `--help` flag to list details on all the supported command line options. 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" /> |
