mirror of
				https://github.com/jellyfin/jellyfin.git
				synced 2025-10-30 18:22:48 -04:00 
			
		
		
		
	Merge remote-tracking branch 'upstream/api-migration' into api-channel
This commit is contained in:
		
						commit
						cbcf3bfaff
					
				| @ -7,6 +7,7 @@ | ||||
|  - [anthonylavado](https://github.com/anthonylavado) | ||||
|  - [Artiume](https://github.com/Artiume) | ||||
|  - [AThomsen](https://github.com/AThomsen) | ||||
|  - [barronpm](https://github.com/barronpm) | ||||
|  - [bilde2910](https://github.com/bilde2910) | ||||
|  - [bfayers](https://github.com/bfayers) | ||||
|  - [BnMcG](https://github.com/BnMcG) | ||||
| @ -130,6 +131,7 @@ | ||||
|  - [XVicarious](https://github.com/XVicarious) | ||||
|  - [YouKnowBlom](https://github.com/YouKnowBlom) | ||||
|  - [KristupasSavickas](https://github.com/KristupasSavickas) | ||||
|  - [Pusta](https://github.com/pusta) | ||||
| 
 | ||||
| # Emby Contributors | ||||
| 
 | ||||
|  | ||||
| @ -4,11 +4,12 @@ using System; | ||||
| using System.Linq; | ||||
| using System.Threading.Tasks; | ||||
| using Emby.Dlna.Service; | ||||
| using Jellyfin.Data.Entities; | ||||
| using Jellyfin.Data.Enums; | ||||
| using MediaBrowser.Common.Net; | ||||
| using MediaBrowser.Controller.Configuration; | ||||
| using MediaBrowser.Controller.Dlna; | ||||
| using MediaBrowser.Controller.Drawing; | ||||
| using MediaBrowser.Controller.Entities; | ||||
| using MediaBrowser.Controller.Library; | ||||
| using MediaBrowser.Controller.MediaEncoding; | ||||
| using MediaBrowser.Controller.TV; | ||||
| @ -32,7 +33,8 @@ namespace Emby.Dlna.ContentDirectory | ||||
|         private readonly IMediaEncoder _mediaEncoder; | ||||
|         private readonly ITVSeriesManager _tvSeriesManager; | ||||
| 
 | ||||
|         public ContentDirectory(IDlnaManager dlna, | ||||
|         public ContentDirectory( | ||||
|             IDlnaManager dlna, | ||||
|             IUserDataManager userDataManager, | ||||
|             IImageProcessor imageProcessor, | ||||
|             ILibraryManager libraryManager, | ||||
| @ -131,7 +133,7 @@ namespace Emby.Dlna.ContentDirectory | ||||
| 
 | ||||
|             foreach (var user in _userManager.Users) | ||||
|             { | ||||
|                 if (user.Policy.IsAdministrator) | ||||
|                 if (user.HasPermission(PermissionKind.IsAdministrator)) | ||||
|                 { | ||||
|                     return user; | ||||
|                 } | ||||
|  | ||||
| @ -10,6 +10,7 @@ using System.Threading; | ||||
| using System.Xml; | ||||
| using Emby.Dlna.Didl; | ||||
| using Emby.Dlna.Service; | ||||
| using Jellyfin.Data.Entities; | ||||
| using MediaBrowser.Common.Extensions; | ||||
| using MediaBrowser.Controller.Configuration; | ||||
| using MediaBrowser.Controller.Drawing; | ||||
| @ -17,7 +18,6 @@ using MediaBrowser.Controller.Dto; | ||||
| using MediaBrowser.Controller.Entities; | ||||
| using MediaBrowser.Controller.Entities.Audio; | ||||
| using MediaBrowser.Controller.Entities.Movies; | ||||
| using MediaBrowser.Controller.Entities.TV; | ||||
| using MediaBrowser.Controller.Library; | ||||
| using MediaBrowser.Controller.LiveTv; | ||||
| using MediaBrowser.Controller.MediaEncoding; | ||||
| @ -28,6 +28,12 @@ using MediaBrowser.Model.Entities; | ||||
| using MediaBrowser.Model.Globalization; | ||||
| using MediaBrowser.Model.Querying; | ||||
| using Microsoft.Extensions.Logging; | ||||
| using Book = MediaBrowser.Controller.Entities.Book; | ||||
| using Episode = MediaBrowser.Controller.Entities.TV.Episode; | ||||
| using Genre = MediaBrowser.Controller.Entities.Genre; | ||||
| using Movie = MediaBrowser.Controller.Entities.Movies.Movie; | ||||
| using MusicAlbum = MediaBrowser.Controller.Entities.Audio.MusicAlbum; | ||||
| using Series = MediaBrowser.Controller.Entities.TV.Series; | ||||
| 
 | ||||
| namespace Emby.Dlna.ContentDirectory | ||||
| { | ||||
| @ -731,7 +737,7 @@ namespace Emby.Dlna.ContentDirectory | ||||
|                 return GetGenres(item, user, query); | ||||
|             } | ||||
| 
 | ||||
|             var array = new ServerItem[] | ||||
|             var array = new[] | ||||
|             { | ||||
|                 new ServerItem(item) | ||||
|                 { | ||||
| @ -1115,7 +1121,7 @@ namespace Emby.Dlna.ContentDirectory | ||||
|         private QueryResult<ServerItem> GetMusicPlaylists(User user, InternalItemsQuery query) | ||||
|         { | ||||
|             query.Parent = null; | ||||
|             query.IncludeItemTypes = new[] { typeof(Playlist).Name }; | ||||
|             query.IncludeItemTypes = new[] { nameof(Playlist) }; | ||||
|             query.SetUser(user); | ||||
|             query.Recursive = true; | ||||
| 
 | ||||
| @ -1132,10 +1138,9 @@ namespace Emby.Dlna.ContentDirectory | ||||
|             { | ||||
|                 UserId = user.Id, | ||||
|                 Limit = 50, | ||||
|                 IncludeItemTypes = new[] { typeof(Audio).Name }, | ||||
|                 ParentId = parent == null ? Guid.Empty : parent.Id, | ||||
|                 IncludeItemTypes = new[] { nameof(Audio) }, | ||||
|                 ParentId = parent?.Id ?? Guid.Empty, | ||||
|                 GroupItems = true | ||||
| 
 | ||||
|             }, query.DtoOptions).Select(i => i.Item1 ?? i.Item2.FirstOrDefault()).Where(i => i != null).ToArray(); | ||||
| 
 | ||||
|             return ToResult(items); | ||||
| @ -1150,7 +1155,6 @@ namespace Emby.Dlna.ContentDirectory | ||||
|                 Limit = query.Limit, | ||||
|                 StartIndex = query.StartIndex, | ||||
|                 UserId = query.User.Id | ||||
| 
 | ||||
|             }, new[] { parent }, query.DtoOptions); | ||||
| 
 | ||||
|             return ToResult(result); | ||||
| @ -1167,7 +1171,6 @@ namespace Emby.Dlna.ContentDirectory | ||||
|                 IncludeItemTypes = new[] { typeof(Episode).Name }, | ||||
|                 ParentId = parent == null ? Guid.Empty : parent.Id, | ||||
|                 GroupItems = false | ||||
| 
 | ||||
|             }, query.DtoOptions).Select(i => i.Item1 ?? i.Item2.FirstOrDefault()).Where(i => i != null).ToArray(); | ||||
| 
 | ||||
|             return ToResult(items); | ||||
| @ -1177,14 +1180,14 @@ namespace Emby.Dlna.ContentDirectory | ||||
|         { | ||||
|             query.OrderBy = Array.Empty<(string, SortOrder)>(); | ||||
| 
 | ||||
|             var items = _userViewManager.GetLatestItems(new LatestItemsQuery | ||||
|             var items = _userViewManager.GetLatestItems( | ||||
|                 new LatestItemsQuery | ||||
|             { | ||||
|                 UserId = user.Id, | ||||
|                 Limit = 50, | ||||
|                 IncludeItemTypes = new[] { typeof(Movie).Name }, | ||||
|                 ParentId = parent == null ? Guid.Empty : parent.Id, | ||||
|                 IncludeItemTypes = new[] { nameof(Movie) }, | ||||
|                 ParentId = parent?.Id ?? Guid.Empty, | ||||
|                 GroupItems = true | ||||
| 
 | ||||
|             }, query.DtoOptions).Select(i => i.Item1 ?? i.Item2.FirstOrDefault()).Where(i => i != null).ToArray(); | ||||
| 
 | ||||
|             return ToResult(items); | ||||
| @ -1217,7 +1220,11 @@ namespace Emby.Dlna.ContentDirectory | ||||
|                 Recursive = true, | ||||
|                 ParentId = parentId, | ||||
|                 GenreIds = new[] { item.Id }, | ||||
|                 IncludeItemTypes = new[] { typeof(Movie).Name, typeof(Series).Name }, | ||||
|                 IncludeItemTypes = new[] | ||||
|                 { | ||||
|                     nameof(Movie), | ||||
|                     nameof(Series) | ||||
|                 }, | ||||
|                 Limit = limit, | ||||
|                 StartIndex = startIndex, | ||||
|                 DtoOptions = GetDtoOptions() | ||||
|  | ||||
| @ -6,14 +6,13 @@ using System.IO; | ||||
| using System.Linq; | ||||
| using System.Text; | ||||
| using System.Xml; | ||||
| using Emby.Dlna.Configuration; | ||||
| using Emby.Dlna.ContentDirectory; | ||||
| using Jellyfin.Data.Entities; | ||||
| using MediaBrowser.Controller.Channels; | ||||
| using MediaBrowser.Controller.Drawing; | ||||
| using MediaBrowser.Controller.Entities; | ||||
| using MediaBrowser.Controller.Entities.Audio; | ||||
| using MediaBrowser.Controller.Entities.Movies; | ||||
| using MediaBrowser.Controller.Entities.TV; | ||||
| using MediaBrowser.Controller.Library; | ||||
| using MediaBrowser.Controller.MediaEncoding; | ||||
| using MediaBrowser.Controller.Playlists; | ||||
| @ -23,6 +22,13 @@ using MediaBrowser.Model.Entities; | ||||
| using MediaBrowser.Model.Globalization; | ||||
| using MediaBrowser.Model.Net; | ||||
| using Microsoft.Extensions.Logging; | ||||
| using Episode = MediaBrowser.Controller.Entities.TV.Episode; | ||||
| using Genre = MediaBrowser.Controller.Entities.Genre; | ||||
| using Movie = MediaBrowser.Controller.Entities.Movies.Movie; | ||||
| using MusicAlbum = MediaBrowser.Controller.Entities.Audio.MusicAlbum; | ||||
| using Season = MediaBrowser.Controller.Entities.TV.Season; | ||||
| using Series = MediaBrowser.Controller.Entities.TV.Series; | ||||
| using XmlAttribute = MediaBrowser.Model.Dlna.XmlAttribute; | ||||
| 
 | ||||
| namespace Emby.Dlna.Didl | ||||
| { | ||||
| @ -421,7 +427,6 @@ namespace Emby.Dlna.Didl | ||||
|                     case StubType.FavoriteSeries: return _localization.GetLocalizedString("HeaderFavoriteShows"); | ||||
|                     case StubType.FavoriteEpisodes: return _localization.GetLocalizedString("HeaderFavoriteEpisodes"); | ||||
|                     case StubType.Series: return _localization.GetLocalizedString("Shows"); | ||||
|                     default: break; | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
| @ -670,7 +675,7 @@ namespace Emby.Dlna.Didl | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             MediaBrowser.Model.Dlna.XmlAttribute secAttribute = null; | ||||
|             XmlAttribute secAttribute = null; | ||||
|             foreach (var attribute in _profile.XmlRootAttributes) | ||||
|             { | ||||
|                 if (string.Equals(attribute.Name, "xmlns:sec", StringComparison.OrdinalIgnoreCase)) | ||||
| @ -995,7 +1000,6 @@ namespace Emby.Dlna.Didl | ||||
|             } | ||||
| 
 | ||||
|             AddImageResElement(item, writer, 160, 160, "jpg", "JPEG_TN"); | ||||
| 
 | ||||
|         } | ||||
| 
 | ||||
|         private void AddImageResElement( | ||||
|  | ||||
| @ -7,6 +7,7 @@ using System.Linq; | ||||
| using System.Threading; | ||||
| using System.Threading.Tasks; | ||||
| using Emby.Dlna.Didl; | ||||
| using Jellyfin.Data.Entities; | ||||
| using MediaBrowser.Common.Configuration; | ||||
| using MediaBrowser.Controller.Dlna; | ||||
| using MediaBrowser.Controller.Drawing; | ||||
| @ -22,6 +23,7 @@ using MediaBrowser.Model.Globalization; | ||||
| using MediaBrowser.Model.Session; | ||||
| using Microsoft.AspNetCore.WebUtilities; | ||||
| using Microsoft.Extensions.Logging; | ||||
| using Photo = MediaBrowser.Controller.Entities.Photo; | ||||
| 
 | ||||
| namespace Emby.Dlna.PlayTo | ||||
| { | ||||
| @ -446,7 +448,13 @@ namespace Emby.Dlna.PlayTo | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         private PlaylistItem CreatePlaylistItem(BaseItem item, User user, long startPostionTicks, string mediaSourceId, int? audioStreamIndex, int? subtitleStreamIndex) | ||||
|         private PlaylistItem CreatePlaylistItem( | ||||
|             BaseItem item, | ||||
|             User user, | ||||
|             long startPostionTicks, | ||||
|             string mediaSourceId, | ||||
|             int? audioStreamIndex, | ||||
|             int? subtitleStreamIndex) | ||||
|         { | ||||
|             var deviceInfo = _device.Properties; | ||||
| 
 | ||||
|  | ||||
| @ -4,6 +4,7 @@ using System.Globalization; | ||||
| using System.IO; | ||||
| using System.Linq; | ||||
| using System.Threading.Tasks; | ||||
| using Jellyfin.Data.Entities; | ||||
| using MediaBrowser.Common.Extensions; | ||||
| using MediaBrowser.Controller; | ||||
| using MediaBrowser.Controller.Drawing; | ||||
| @ -14,6 +15,7 @@ using MediaBrowser.Model.Entities; | ||||
| using MediaBrowser.Model.IO; | ||||
| using MediaBrowser.Model.Net; | ||||
| using Microsoft.Extensions.Logging; | ||||
| using Photo = MediaBrowser.Controller.Entities.Photo; | ||||
| 
 | ||||
| namespace Emby.Drawing | ||||
| { | ||||
| @ -349,6 +351,13 @@ namespace Emby.Drawing | ||||
|             }); | ||||
|         } | ||||
| 
 | ||||
|         /// <inheritdoc /> | ||||
|         public string GetImageCacheTag(User user) | ||||
|         { | ||||
|             return (user.ProfileImage.Path + user.ProfileImage.LastModified.Ticks).GetMD5() | ||||
|                 .ToString("N", CultureInfo.InvariantCulture); | ||||
|         } | ||||
| 
 | ||||
|         private async Task<(string path, DateTime dateModified)> GetSupportedImage(string originalImagePath, DateTime dateModified) | ||||
|         { | ||||
|             var inputFormat = Path.GetExtension(originalImagePath) | ||||
|  | ||||
| @ -4,6 +4,8 @@ using System.Globalization; | ||||
| using System.Linq; | ||||
| using System.Threading; | ||||
| using System.Threading.Tasks; | ||||
| using Jellyfin.Data.Entities; | ||||
| using Jellyfin.Data.Enums; | ||||
| using MediaBrowser.Common.Configuration; | ||||
| using MediaBrowser.Common.Extensions; | ||||
| using MediaBrowser.Controller.Configuration; | ||||
| @ -101,7 +103,7 @@ namespace Emby.Notifications | ||||
|                 switch (request.SendToUserMode.Value) | ||||
|                 { | ||||
|                     case SendToUserType.Admins: | ||||
|                         return _userManager.Users.Where(i => i.Policy.IsAdministrator) | ||||
|                         return _userManager.Users.Where(i => i.HasPermission(PermissionKind.IsAdministrator)) | ||||
|                                 .Select(i => i.Id); | ||||
|                     case SendToUserType.All: | ||||
|                         return _userManager.UsersIds; | ||||
| @ -117,7 +119,7 @@ namespace Emby.Notifications | ||||
|                 var config = GetConfiguration(); | ||||
| 
 | ||||
|                 return _userManager.Users | ||||
|                     .Where(i => config.IsEnabledToSendToUser(request.NotificationType, i.Id.ToString("N", CultureInfo.InvariantCulture), i.Policy)) | ||||
|                     .Where(i => config.IsEnabledToSendToUser(request.NotificationType, i.Id.ToString("N", CultureInfo.InvariantCulture), i)) | ||||
|                     .Select(i => i.Id); | ||||
|             } | ||||
| 
 | ||||
| @ -142,7 +144,7 @@ namespace Emby.Notifications | ||||
|                 User = user | ||||
|             }; | ||||
| 
 | ||||
|             _logger.LogDebug("Sending notification via {0} to user {1}", service.Name, user.Name); | ||||
|             _logger.LogDebug("Sending notification via {0} to user {1}", service.Name, user.Username); | ||||
| 
 | ||||
|             try | ||||
|             { | ||||
|  | ||||
| @ -88,25 +88,26 @@ namespace Emby.Server.Implementations.Activity | ||||
| 
 | ||||
|             _subManager.SubtitleDownloadFailure += OnSubtitleDownloadFailure; | ||||
| 
 | ||||
|             _userManager.UserCreated += OnUserCreated; | ||||
|             _userManager.UserPasswordChanged += OnUserPasswordChanged; | ||||
|             _userManager.UserDeleted += OnUserDeleted; | ||||
|             _userManager.UserPolicyUpdated += OnUserPolicyUpdated; | ||||
|             _userManager.UserLockedOut += OnUserLockedOut; | ||||
|             _userManager.OnUserCreated += OnUserCreated; | ||||
|             _userManager.OnUserPasswordChanged += OnUserPasswordChanged; | ||||
|             _userManager.OnUserDeleted += OnUserDeleted; | ||||
|             _userManager.OnUserLockedOut += OnUserLockedOut; | ||||
| 
 | ||||
|             return Task.CompletedTask; | ||||
|         } | ||||
| 
 | ||||
|         private async void OnUserLockedOut(object sender, GenericEventArgs<MediaBrowser.Controller.Entities.User> e) | ||||
|         private async void OnUserLockedOut(object sender, GenericEventArgs<User> e) | ||||
|         { | ||||
|             await CreateLogEntry(new ActivityLog( | ||||
|                     string.Format( | ||||
|                         CultureInfo.InvariantCulture, | ||||
|                         _localization.GetLocalizedString("UserLockedOutWithName"), | ||||
|                         e.Argument.Name), | ||||
|                         e.Argument.Username), | ||||
|                     NotificationType.UserLockedOut.ToString(), | ||||
|                     e.Argument.Id)) | ||||
|                 .ConfigureAwait(false); | ||||
|                     e.Argument.Id) | ||||
|             { | ||||
|                 LogSeverity = LogLevel.Error | ||||
|             }).ConfigureAwait(false); | ||||
|         } | ||||
| 
 | ||||
|         private async void OnSubtitleDownloadFailure(object sender, SubtitleDownloadFailureEventArgs e) | ||||
| @ -152,7 +153,7 @@ namespace Emby.Server.Implementations.Activity | ||||
|                 string.Format( | ||||
|                     CultureInfo.InvariantCulture, | ||||
|                     _localization.GetLocalizedString("UserStoppedPlayingItemWithValues"), | ||||
|                     user.Name, | ||||
|                     user.Username, | ||||
|                     GetItemName(item), | ||||
|                     e.DeviceName), | ||||
|                 GetPlaybackStoppedNotificationType(item.MediaType), | ||||
| @ -187,7 +188,7 @@ namespace Emby.Server.Implementations.Activity | ||||
|                 string.Format( | ||||
|                     CultureInfo.InvariantCulture, | ||||
|                     _localization.GetLocalizedString("UserStartedPlayingItemWithValues"), | ||||
|                     user.Name, | ||||
|                     user.Username, | ||||
|                     GetItemName(item), | ||||
|                     e.DeviceName), | ||||
|                 GetPlaybackNotificationType(item.MediaType), | ||||
| @ -304,49 +305,37 @@ namespace Emby.Server.Implementations.Activity | ||||
|             }).ConfigureAwait(false); | ||||
|         } | ||||
| 
 | ||||
|         private async void OnUserPolicyUpdated(object sender, GenericEventArgs<MediaBrowser.Controller.Entities.User> e) | ||||
|         { | ||||
|             await CreateLogEntry(new ActivityLog( | ||||
|                 string.Format( | ||||
|                     CultureInfo.InvariantCulture, | ||||
|                     _localization.GetLocalizedString("UserPolicyUpdatedWithName"), | ||||
|                     e.Argument.Name), | ||||
|                 "UserPolicyUpdated", | ||||
|                 e.Argument.Id)) | ||||
|                 .ConfigureAwait(false); | ||||
|         } | ||||
| 
 | ||||
|         private async void OnUserDeleted(object sender, GenericEventArgs<MediaBrowser.Controller.Entities.User> e) | ||||
|         private async void OnUserDeleted(object sender, GenericEventArgs<User> e) | ||||
|         { | ||||
|             await CreateLogEntry(new ActivityLog( | ||||
|                 string.Format( | ||||
|                     CultureInfo.InvariantCulture, | ||||
|                     _localization.GetLocalizedString("UserDeletedWithName"), | ||||
|                     e.Argument.Name), | ||||
|                     e.Argument.Username), | ||||
|                 "UserDeleted", | ||||
|                 Guid.Empty)) | ||||
|                 .ConfigureAwait(false); | ||||
|         } | ||||
| 
 | ||||
|         private async void OnUserPasswordChanged(object sender, GenericEventArgs<MediaBrowser.Controller.Entities.User> e) | ||||
|         private async void OnUserPasswordChanged(object sender, GenericEventArgs<User> e) | ||||
|         { | ||||
|             await CreateLogEntry(new ActivityLog( | ||||
|                 string.Format( | ||||
|                     CultureInfo.InvariantCulture, | ||||
|                     _localization.GetLocalizedString("UserPasswordChangedWithName"), | ||||
|                     e.Argument.Name), | ||||
|                     e.Argument.Username), | ||||
|                 "UserPasswordChanged", | ||||
|                 e.Argument.Id)) | ||||
|                 .ConfigureAwait(false); | ||||
|         } | ||||
| 
 | ||||
|         private async void OnUserCreated(object sender, GenericEventArgs<MediaBrowser.Controller.Entities.User> e) | ||||
|         private async void OnUserCreated(object sender, GenericEventArgs<User> e) | ||||
|         { | ||||
|             await CreateLogEntry(new ActivityLog( | ||||
|                 string.Format( | ||||
|                     CultureInfo.InvariantCulture, | ||||
|                     _localization.GetLocalizedString("UserCreatedWithName"), | ||||
|                     e.Argument.Name), | ||||
|                     e.Argument.Username), | ||||
|                 "UserCreated", | ||||
|                 e.Argument.Id)) | ||||
|                 .ConfigureAwait(false); | ||||
| @ -510,11 +499,10 @@ namespace Emby.Server.Implementations.Activity | ||||
| 
 | ||||
|             _subManager.SubtitleDownloadFailure -= OnSubtitleDownloadFailure; | ||||
| 
 | ||||
|             _userManager.UserCreated -= OnUserCreated; | ||||
|             _userManager.UserPasswordChanged -= OnUserPasswordChanged; | ||||
|             _userManager.UserDeleted -= OnUserDeleted; | ||||
|             _userManager.UserPolicyUpdated -= OnUserPolicyUpdated; | ||||
|             _userManager.UserLockedOut -= OnUserLockedOut; | ||||
|             _userManager.OnUserCreated -= OnUserCreated; | ||||
|             _userManager.OnUserPasswordChanged -= OnUserPasswordChanged; | ||||
|             _userManager.OnUserDeleted -= OnUserDeleted; | ||||
|             _userManager.OnUserLockedOut -= OnUserLockedOut; | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|  | ||||
| @ -97,7 +97,6 @@ using MediaBrowser.Providers.Chapters; | ||||
| using MediaBrowser.Providers.Manager; | ||||
| using MediaBrowser.Providers.Plugins.TheTvdb; | ||||
| using MediaBrowser.Providers.Subtitles; | ||||
| using MediaBrowser.WebDashboard.Api; | ||||
| using MediaBrowser.XbmcMetadata.Providers; | ||||
| using Microsoft.AspNetCore.Http; | ||||
| using Microsoft.Extensions.DependencyInjection; | ||||
| @ -562,11 +561,8 @@ namespace Emby.Server.Implementations | ||||
| 
 | ||||
|             serviceCollection.AddSingleton<IAuthenticationRepository, AuthenticationRepository>(); | ||||
| 
 | ||||
|             serviceCollection.AddSingleton<IUserRepository, SqliteUserRepository>(); | ||||
| 
 | ||||
|             // TODO: Refactor to eliminate the circular dependency here so that Lazy<T> isn't required | ||||
|             serviceCollection.AddTransient(provider => new Lazy<IDtoService>(provider.GetRequiredService<IDtoService>)); | ||||
|             serviceCollection.AddSingleton<IUserManager, UserManager>(); | ||||
| 
 | ||||
|             // TODO: Refactor to eliminate the circular dependency here so that Lazy<T> isn't required | ||||
|             // TODO: Add StartupOptions.FFmpegPath to IConfiguration and remove this custom activation | ||||
| @ -659,15 +655,11 @@ namespace Emby.Server.Implementations | ||||
| 
 | ||||
|             ((SqliteDisplayPreferencesRepository)Resolve<IDisplayPreferencesRepository>()).Initialize(); | ||||
|             ((AuthenticationRepository)Resolve<IAuthenticationRepository>()).Initialize(); | ||||
|             ((SqliteUserRepository)Resolve<IUserRepository>()).Initialize(); | ||||
| 
 | ||||
|             SetStaticProperties(); | ||||
| 
 | ||||
|             var userManager = (UserManager)Resolve<IUserManager>(); | ||||
|             userManager.Initialize(); | ||||
| 
 | ||||
|             var userDataRepo = (SqliteUserDataRepository)Resolve<IUserDataRepository>(); | ||||
|             ((SqliteItemRepository)Resolve<IItemRepository>()).Initialize(userDataRepo, userManager); | ||||
|             ((SqliteItemRepository)Resolve<IItemRepository>()).Initialize(userDataRepo, Resolve<IUserManager>()); | ||||
| 
 | ||||
|             FindParts(); | ||||
|         } | ||||
| @ -750,7 +742,6 @@ namespace Emby.Server.Implementations | ||||
|             BaseItem.ProviderManager = Resolve<IProviderManager>(); | ||||
|             BaseItem.LocalizationManager = Resolve<ILocalizationManager>(); | ||||
|             BaseItem.ItemRepository = Resolve<IItemRepository>(); | ||||
|             User.UserManager = Resolve<IUserManager>(); | ||||
|             BaseItem.FileSystem = _fileSystemManager; | ||||
|             BaseItem.UserDataManager = Resolve<IUserDataManager>(); | ||||
|             BaseItem.ChannelManager = Resolve<IChannelManager>(); | ||||
| @ -1045,9 +1036,6 @@ namespace Emby.Server.Implementations | ||||
|             // Include composable parts in the Api assembly | ||||
|             yield return typeof(ApiEntryPoint).Assembly; | ||||
| 
 | ||||
|             // Include composable parts in the Dashboard assembly | ||||
|             yield return typeof(DashboardService).Assembly; | ||||
| 
 | ||||
|             // Include composable parts in the Model assembly | ||||
|             yield return typeof(SystemInfo).Assembly; | ||||
| 
 | ||||
|  | ||||
| @ -6,6 +6,7 @@ using System.IO; | ||||
| using System.Linq; | ||||
| using System.Threading; | ||||
| using System.Threading.Tasks; | ||||
| using Jellyfin.Data.Entities; | ||||
| using MediaBrowser.Common.Extensions; | ||||
| using MediaBrowser.Common.Progress; | ||||
| using MediaBrowser.Controller.Channels; | ||||
| @ -13,8 +14,6 @@ using MediaBrowser.Controller.Configuration; | ||||
| using MediaBrowser.Controller.Dto; | ||||
| using MediaBrowser.Controller.Entities; | ||||
| using MediaBrowser.Controller.Entities.Audio; | ||||
| using MediaBrowser.Controller.Entities.Movies; | ||||
| using MediaBrowser.Controller.Entities.TV; | ||||
| using MediaBrowser.Controller.Library; | ||||
| using MediaBrowser.Controller.Providers; | ||||
| using MediaBrowser.Model.Channels; | ||||
| @ -24,6 +23,11 @@ using MediaBrowser.Model.IO; | ||||
| using MediaBrowser.Model.Querying; | ||||
| using MediaBrowser.Model.Serialization; | ||||
| using Microsoft.Extensions.Logging; | ||||
| using Episode = MediaBrowser.Controller.Entities.TV.Episode; | ||||
| using Movie = MediaBrowser.Controller.Entities.Movies.Movie; | ||||
| using MusicAlbum = MediaBrowser.Controller.Entities.Audio.MusicAlbum; | ||||
| using Season = MediaBrowser.Controller.Entities.TV.Season; | ||||
| using Series = MediaBrowser.Controller.Entities.TV.Series; | ||||
| 
 | ||||
| namespace Emby.Server.Implementations.Channels | ||||
| { | ||||
| @ -791,7 +795,8 @@ namespace Emby.Server.Implementations.Channels | ||||
|             return result; | ||||
|         } | ||||
| 
 | ||||
|         private async Task<ChannelItemResult> GetChannelItems(IChannel channel, | ||||
|         private async Task<ChannelItemResult> GetChannelItems( | ||||
|             IChannel channel, | ||||
|             User user, | ||||
|             string externalFolderId, | ||||
|             ChannelItemSortField? sortField, | ||||
|  | ||||
| @ -5,6 +5,7 @@ using System.IO; | ||||
| using System.Linq; | ||||
| using System.Threading; | ||||
| using System.Threading.Tasks; | ||||
| using Jellyfin.Data.Entities; | ||||
| using MediaBrowser.Common.Configuration; | ||||
| using MediaBrowser.Controller.Collections; | ||||
| using MediaBrowser.Controller.Configuration; | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| #pragma warning disable CS1591 | ||||
| #pragma warning disable CS1591 | ||||
| 
 | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
|  | ||||
| @ -4,6 +4,7 @@ using System; | ||||
| using System.Collections.Generic; | ||||
| using System.IO; | ||||
| using System.Threading; | ||||
| using Jellyfin.Data.Entities; | ||||
| using MediaBrowser.Common.Configuration; | ||||
| using MediaBrowser.Controller.Entities; | ||||
| using MediaBrowser.Controller.Library; | ||||
|  | ||||
| @ -1,240 +0,0 @@ | ||||
| #pragma warning disable CS1591 | ||||
| 
 | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.IO; | ||||
| using System.Text.Json; | ||||
| using MediaBrowser.Common.Json; | ||||
| using MediaBrowser.Controller; | ||||
| using MediaBrowser.Controller.Entities; | ||||
| using MediaBrowser.Controller.Persistence; | ||||
| using Microsoft.Extensions.Logging; | ||||
| using SQLitePCL.pretty; | ||||
| 
 | ||||
| namespace Emby.Server.Implementations.Data | ||||
| { | ||||
|     /// <summary> | ||||
|     /// Class SQLiteUserRepository | ||||
|     /// </summary> | ||||
|     public class SqliteUserRepository : BaseSqliteRepository, IUserRepository | ||||
|     { | ||||
|         private readonly JsonSerializerOptions _jsonOptions; | ||||
| 
 | ||||
|         public SqliteUserRepository( | ||||
|             ILogger<SqliteUserRepository> logger, | ||||
|             IServerApplicationPaths appPaths) | ||||
|             : base(logger) | ||||
|         { | ||||
|             _jsonOptions = JsonDefaults.GetOptions(); | ||||
| 
 | ||||
|             DbFilePath = Path.Combine(appPaths.DataPath, "users.db"); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets the name of the repository | ||||
|         /// </summary> | ||||
|         /// <value>The name.</value> | ||||
|         public string Name => "SQLite"; | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Opens the connection to the database. | ||||
|         /// </summary> | ||||
|         public void Initialize() | ||||
|         { | ||||
|             using (var connection = GetConnection()) | ||||
|             { | ||||
|                 var localUsersTableExists = TableExists(connection, "LocalUsersv2"); | ||||
| 
 | ||||
|                 connection.RunQueries(new[] { | ||||
|                     "create table if not exists LocalUsersv2 (Id INTEGER PRIMARY KEY, guid GUID NOT NULL, data BLOB NOT NULL)", | ||||
|                     "drop index if exists idx_users" | ||||
|                 }); | ||||
| 
 | ||||
|                 if (!localUsersTableExists && TableExists(connection, "Users")) | ||||
|                 { | ||||
|                     TryMigrateToLocalUsersTable(connection); | ||||
|                 } | ||||
| 
 | ||||
|                 RemoveEmptyPasswordHashes(connection); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         private void TryMigrateToLocalUsersTable(ManagedConnection connection) | ||||
|         { | ||||
|             try | ||||
|             { | ||||
|                 connection.RunQueries(new[] | ||||
|                 { | ||||
|                     "INSERT INTO LocalUsersv2 (guid, data) SELECT guid,data from users" | ||||
|                 }); | ||||
|             } | ||||
|             catch (Exception ex) | ||||
|             { | ||||
|                 Logger.LogError(ex, "Error migrating users database"); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         private void RemoveEmptyPasswordHashes(ManagedConnection connection) | ||||
|         { | ||||
|             foreach (var user in RetrieveAllUsers(connection)) | ||||
|             { | ||||
|                 // If the user password is the sha1 hash of the empty string, remove it | ||||
|                 if (!string.Equals(user.Password, "DA39A3EE5E6B4B0D3255BFEF95601890AFD80709", StringComparison.Ordinal) | ||||
|                     && !string.Equals(user.Password, "$SHA1$DA39A3EE5E6B4B0D3255BFEF95601890AFD80709", StringComparison.Ordinal)) | ||||
|                 { | ||||
|                     continue; | ||||
|                 } | ||||
| 
 | ||||
|                 user.Password = null; | ||||
|                 var serialized = JsonSerializer.SerializeToUtf8Bytes(user, _jsonOptions); | ||||
| 
 | ||||
|                 connection.RunInTransaction(db => | ||||
|                 { | ||||
|                     using (var statement = db.PrepareStatement("update LocalUsersv2 set data=@data where Id=@InternalId")) | ||||
|                     { | ||||
|                         statement.TryBind("@InternalId", user.InternalId); | ||||
|                         statement.TryBind("@data", serialized); | ||||
|                         statement.MoveNext(); | ||||
|                     } | ||||
|                 }, TransactionMode); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Save a user in the repo | ||||
|         /// </summary> | ||||
|         public void CreateUser(User user) | ||||
|         { | ||||
|             if (user == null) | ||||
|             { | ||||
|                 throw new ArgumentNullException(nameof(user)); | ||||
|             } | ||||
| 
 | ||||
|             var serialized = JsonSerializer.SerializeToUtf8Bytes(user, _jsonOptions); | ||||
| 
 | ||||
|             using (var connection = GetConnection()) | ||||
|             { | ||||
|                 connection.RunInTransaction(db => | ||||
|                 { | ||||
|                     using (var statement = db.PrepareStatement("insert into LocalUsersv2 (guid, data) values (@guid, @data)")) | ||||
|                     { | ||||
|                         statement.TryBind("@guid", user.Id.ToByteArray()); | ||||
|                         statement.TryBind("@data", serialized); | ||||
| 
 | ||||
|                         statement.MoveNext(); | ||||
|                     } | ||||
| 
 | ||||
|                     var createdUser = GetUser(user.Id, connection); | ||||
| 
 | ||||
|                     if (createdUser == null) | ||||
|                     { | ||||
|                         throw new ApplicationException("created user should never be null"); | ||||
|                     } | ||||
| 
 | ||||
|                     user.InternalId = createdUser.InternalId; | ||||
| 
 | ||||
|                 }, TransactionMode); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         public void UpdateUser(User user) | ||||
|         { | ||||
|             if (user == null) | ||||
|             { | ||||
|                 throw new ArgumentNullException(nameof(user)); | ||||
|             } | ||||
| 
 | ||||
|             var serialized = JsonSerializer.SerializeToUtf8Bytes(user, _jsonOptions); | ||||
| 
 | ||||
|             using (var connection = GetConnection()) | ||||
|             { | ||||
|                 connection.RunInTransaction(db => | ||||
|                 { | ||||
|                     using (var statement = db.PrepareStatement("update LocalUsersv2 set data=@data where Id=@InternalId")) | ||||
|                     { | ||||
|                         statement.TryBind("@InternalId", user.InternalId); | ||||
|                         statement.TryBind("@data", serialized); | ||||
|                         statement.MoveNext(); | ||||
|                     } | ||||
| 
 | ||||
|                 }, TransactionMode); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         private User GetUser(Guid guid, ManagedConnection connection) | ||||
|         { | ||||
|             using (var statement = connection.PrepareStatement("select id,guid,data from LocalUsersv2 where guid=@guid")) | ||||
|             { | ||||
|                 statement.TryBind("@guid", guid); | ||||
| 
 | ||||
|                 foreach (var row in statement.ExecuteQuery()) | ||||
|                 { | ||||
|                     return GetUser(row); | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             return null; | ||||
|         } | ||||
| 
 | ||||
|         private User GetUser(IReadOnlyList<IResultSetValue> row) | ||||
|         { | ||||
|             var id = row[0].ToInt64(); | ||||
|             var guid = row[1].ReadGuidFromBlob(); | ||||
| 
 | ||||
|             var user = JsonSerializer.Deserialize<User>(row[2].ToBlob(), _jsonOptions); | ||||
|             user.InternalId = id; | ||||
|             user.Id = guid; | ||||
|             return user; | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Retrieve all users from the database | ||||
|         /// </summary> | ||||
|         /// <returns>IEnumerable{User}.</returns> | ||||
|         public List<User> RetrieveAllUsers() | ||||
|         { | ||||
|             using (var connection = GetConnection(true)) | ||||
|             { | ||||
|                 return new List<User>(RetrieveAllUsers(connection)); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Retrieve all users from the database | ||||
|         /// </summary> | ||||
|         /// <returns>IEnumerable{User}.</returns> | ||||
|         private IEnumerable<User> RetrieveAllUsers(ManagedConnection connection) | ||||
|         { | ||||
|             foreach (var row in connection.Query("select id,guid,data from LocalUsersv2")) | ||||
|             { | ||||
|                 yield return GetUser(row); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Deletes the user. | ||||
|         /// </summary> | ||||
|         /// <param name="user">The user.</param> | ||||
|         /// <returns>Task.</returns> | ||||
|         /// <exception cref="ArgumentNullException">user</exception> | ||||
|         public void DeleteUser(User user) | ||||
|         { | ||||
|             if (user == null) | ||||
|             { | ||||
|                 throw new ArgumentNullException(nameof(user)); | ||||
|             } | ||||
| 
 | ||||
|             using (var connection = GetConnection()) | ||||
|             { | ||||
|                 connection.RunInTransaction(db => | ||||
|                 { | ||||
|                     using (var statement = db.PrepareStatement("delete from LocalUsersv2 where Id=@id")) | ||||
|                     { | ||||
|                         statement.TryBind("@id", user.InternalId); | ||||
|                         statement.MoveNext(); | ||||
|                     } | ||||
|                 }, TransactionMode); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -5,10 +5,11 @@ using System.Collections.Generic; | ||||
| using System.Globalization; | ||||
| using System.IO; | ||||
| using System.Linq; | ||||
| using Jellyfin.Data.Enums; | ||||
| using Jellyfin.Data.Entities; | ||||
| using MediaBrowser.Common.Extensions; | ||||
| using MediaBrowser.Controller.Configuration; | ||||
| using MediaBrowser.Controller.Devices; | ||||
| using MediaBrowser.Controller.Entities; | ||||
| using MediaBrowser.Controller.Library; | ||||
| using MediaBrowser.Controller.Security; | ||||
| using MediaBrowser.Model.Devices; | ||||
| @ -16,7 +17,6 @@ using MediaBrowser.Model.Events; | ||||
| using MediaBrowser.Model.Querying; | ||||
| using MediaBrowser.Model.Serialization; | ||||
| using MediaBrowser.Model.Session; | ||||
| using MediaBrowser.Model.Users; | ||||
| 
 | ||||
| namespace Emby.Server.Implementations.Devices | ||||
| { | ||||
| @ -27,11 +27,10 @@ namespace Emby.Server.Implementations.Devices | ||||
|         private readonly IServerConfigurationManager _config; | ||||
|         private readonly IAuthenticationRepository _authRepo; | ||||
|         private readonly Dictionary<string, ClientCapabilities> _capabilitiesCache; | ||||
|         private readonly object _capabilitiesSyncLock = new object(); | ||||
| 
 | ||||
|         public event EventHandler<GenericEventArgs<Tuple<string, DeviceOptions>>> DeviceOptionsUpdated; | ||||
| 
 | ||||
|         private readonly object _capabilitiesSyncLock = new object(); | ||||
| 
 | ||||
|         public DeviceManager( | ||||
|             IAuthenticationRepository authRepo, | ||||
|             IJsonSerializer json, | ||||
| @ -175,7 +174,12 @@ namespace Emby.Server.Implementations.Devices | ||||
|                 throw new ArgumentNullException(nameof(deviceId)); | ||||
|             } | ||||
| 
 | ||||
|             if (!CanAccessDevice(user.Policy, deviceId)) | ||||
|             if (user.HasPermission(PermissionKind.EnableAllDevices) || user.HasPermission(PermissionKind.IsAdministrator)) | ||||
|             { | ||||
|                 return true; | ||||
|             } | ||||
| 
 | ||||
|             if (!user.GetPreference(PreferenceKind.EnabledDevices).Contains(deviceId, StringComparer.OrdinalIgnoreCase)) | ||||
|             { | ||||
|                 var capabilities = GetCapabilities(deviceId); | ||||
| 
 | ||||
| @ -187,20 +191,5 @@ namespace Emby.Server.Implementations.Devices | ||||
| 
 | ||||
|             return true; | ||||
|         } | ||||
| 
 | ||||
|         private static bool CanAccessDevice(UserPolicy policy, string id) | ||||
|         { | ||||
|             if (policy.EnableAllDevices) | ||||
|             { | ||||
|                 return true; | ||||
|             } | ||||
| 
 | ||||
|             if (policy.IsAdministrator) | ||||
|             { | ||||
|                 return true; | ||||
|             } | ||||
| 
 | ||||
|             return policy.EnabledDevices.Contains(id, StringComparer.OrdinalIgnoreCase); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -6,14 +6,14 @@ using System.Globalization; | ||||
| using System.IO; | ||||
| using System.Linq; | ||||
| using System.Threading.Tasks; | ||||
| using Jellyfin.Data.Entities; | ||||
| using Jellyfin.Data.Enums; | ||||
| using MediaBrowser.Common; | ||||
| using MediaBrowser.Controller.Channels; | ||||
| using MediaBrowser.Controller.Drawing; | ||||
| using MediaBrowser.Controller.Dto; | ||||
| using MediaBrowser.Controller.Entities; | ||||
| using MediaBrowser.Controller.Entities.Audio; | ||||
| using MediaBrowser.Controller.Entities.Movies; | ||||
| using MediaBrowser.Controller.Entities.TV; | ||||
| using MediaBrowser.Controller.Library; | ||||
| using MediaBrowser.Controller.LiveTv; | ||||
| using MediaBrowser.Controller.Persistence; | ||||
| @ -24,6 +24,14 @@ using MediaBrowser.Model.Dto; | ||||
| using MediaBrowser.Model.Entities; | ||||
| using MediaBrowser.Model.Querying; | ||||
| using Microsoft.Extensions.Logging; | ||||
| using Book = MediaBrowser.Controller.Entities.Book; | ||||
| using Episode = MediaBrowser.Controller.Entities.TV.Episode; | ||||
| using Movie = MediaBrowser.Controller.Entities.Movies.Movie; | ||||
| using MusicAlbum = MediaBrowser.Controller.Entities.Audio.MusicAlbum; | ||||
| using Person = MediaBrowser.Controller.Entities.Person; | ||||
| using Photo = MediaBrowser.Controller.Entities.Photo; | ||||
| using Season = MediaBrowser.Controller.Entities.TV.Season; | ||||
| using Series = MediaBrowser.Controller.Entities.TV.Series; | ||||
| 
 | ||||
| namespace Emby.Server.Implementations.Dto | ||||
| { | ||||
| @ -384,7 +392,7 @@ namespace Emby.Server.Implementations.Dto | ||||
| 
 | ||||
|                     if (options.ContainsField(ItemFields.ChildCount)) | ||||
|                     { | ||||
|                         dto.ChildCount = dto.ChildCount ?? GetChildCount(folder, user); | ||||
|                         dto.ChildCount ??= GetChildCount(folder, user); | ||||
|                     } | ||||
|                 } | ||||
| 
 | ||||
| @ -414,7 +422,7 @@ namespace Emby.Server.Implementations.Dto | ||||
| 
 | ||||
|             if (options.ContainsField(ItemFields.BasicSyncInfo)) | ||||
|             { | ||||
|                 var userCanSync = user != null && user.Policy.EnableContentDownloading; | ||||
|                 var userCanSync = user != null && user.HasPermission(PermissionKind.EnableContentDownloading); | ||||
|                 if (userCanSync && item.SupportsExternalTransfer) | ||||
|                 { | ||||
|                     dto.SupportsSync = true; | ||||
|  | ||||
| @ -13,7 +13,6 @@ | ||||
|     <ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj" /> | ||||
|     <ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" /> | ||||
|     <ProjectReference Include="..\MediaBrowser.Providers\MediaBrowser.Providers.csproj" /> | ||||
|     <ProjectReference Include="..\MediaBrowser.WebDashboard\MediaBrowser.WebDashboard.csproj" /> | ||||
|     <ProjectReference Include="..\MediaBrowser.XbmcMetadata\MediaBrowser.XbmcMetadata.csproj" /> | ||||
|     <ProjectReference Include="..\Emby.Dlna\Emby.Dlna.csproj" /> | ||||
|     <ProjectReference Include="..\MediaBrowser.Api\MediaBrowser.Api.csproj" /> | ||||
|  | ||||
| @ -6,6 +6,7 @@ using System.Globalization; | ||||
| using System.Linq; | ||||
| using System.Threading; | ||||
| using System.Threading.Tasks; | ||||
| using Jellyfin.Data.Entities; | ||||
| using MediaBrowser.Controller.Channels; | ||||
| using MediaBrowser.Controller.Entities; | ||||
| using MediaBrowser.Controller.Entities.Audio; | ||||
|  | ||||
| @ -4,6 +4,7 @@ using System; | ||||
| using System.Linq; | ||||
| using System.Threading; | ||||
| using System.Threading.Tasks; | ||||
| using Jellyfin.Data.Enums; | ||||
| using MediaBrowser.Controller.Library; | ||||
| using MediaBrowser.Controller.LiveTv; | ||||
| using MediaBrowser.Controller.Plugins; | ||||
| @ -64,7 +65,7 @@ namespace Emby.Server.Implementations.EntryPoints | ||||
| 
 | ||||
|         private async Task SendMessage(string name, TimerEventInfo info) | ||||
|         { | ||||
|             var users = _userManager.Users.Where(i => i.Policy.EnableLiveTvAccess).Select(i => i.Id).ToList(); | ||||
|             var users = _userManager.Users.Where(i => i.HasPermission(PermissionKind.EnableLiveTvAccess)).Select(i => i.Id).ToList(); | ||||
| 
 | ||||
|             try | ||||
|             { | ||||
|  | ||||
| @ -1,77 +0,0 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.Threading; | ||||
| using System.Threading.Tasks; | ||||
| using MediaBrowser.Controller.Library; | ||||
| using MediaBrowser.Controller.Providers; | ||||
| using MediaBrowser.Model.IO; | ||||
| using MediaBrowser.Model.Tasks; | ||||
| 
 | ||||
| namespace Emby.Server.Implementations.EntryPoints | ||||
| { | ||||
|     /// <summary> | ||||
|     /// Class RefreshUsersMetadata. | ||||
|     /// </summary> | ||||
|     public class RefreshUsersMetadata : IScheduledTask, IConfigurableScheduledTask | ||||
|     { | ||||
|         /// <summary> | ||||
|         /// The user manager. | ||||
|         /// </summary> | ||||
|         private readonly IUserManager _userManager; | ||||
|         private readonly IFileSystem _fileSystem; | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="RefreshUsersMetadata" /> class. | ||||
|         /// </summary> | ||||
|         public RefreshUsersMetadata(IUserManager userManager, IFileSystem fileSystem) | ||||
|         { | ||||
|             _userManager = userManager; | ||||
|             _fileSystem = fileSystem; | ||||
|         } | ||||
| 
 | ||||
|         /// <inheritdoc /> | ||||
|         public string Name => "Refresh Users"; | ||||
| 
 | ||||
|         /// <inheritdoc /> | ||||
|         public string Key => "RefreshUsers"; | ||||
| 
 | ||||
|         /// <inheritdoc /> | ||||
|         public string Description => "Refresh user infos"; | ||||
| 
 | ||||
|         /// <inheritdoc /> | ||||
|         public string Category => "Library"; | ||||
| 
 | ||||
|         /// <inheritdoc /> | ||||
|         public bool IsHidden => true; | ||||
| 
 | ||||
|         /// <inheritdoc /> | ||||
|         public bool IsEnabled => true; | ||||
| 
 | ||||
|         /// <inheritdoc /> | ||||
|         public bool IsLogged => true; | ||||
| 
 | ||||
|         /// <inheritdoc /> | ||||
|         public async Task Execute(CancellationToken cancellationToken, IProgress<double> progress) | ||||
|         { | ||||
|             foreach (var user in _userManager.Users) | ||||
|             { | ||||
|                 cancellationToken.ThrowIfCancellationRequested(); | ||||
| 
 | ||||
|                 await user.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(_fileSystem)), cancellationToken).ConfigureAwait(false); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         /// <inheritdoc /> | ||||
|         public IEnumerable<TaskTriggerInfo> GetDefaultTriggers() | ||||
|         { | ||||
|             return new[] | ||||
|             { | ||||
|                 new TaskTriggerInfo | ||||
|                 { | ||||
|                     IntervalTicks = TimeSpan.FromDays(1).Ticks, | ||||
|                     Type = TaskTriggerInfo.TriggerInterval | ||||
|                 } | ||||
|             }; | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -3,10 +3,10 @@ using System.Collections.Generic; | ||||
| using System.Globalization; | ||||
| using System.Threading; | ||||
| using System.Threading.Tasks; | ||||
| using Jellyfin.Data.Entities; | ||||
| using MediaBrowser.Common.Plugins; | ||||
| using MediaBrowser.Common.Updates; | ||||
| using MediaBrowser.Controller; | ||||
| using MediaBrowser.Controller.Entities; | ||||
| using MediaBrowser.Controller.Library; | ||||
| using MediaBrowser.Controller.Plugins; | ||||
| using MediaBrowser.Controller.Session; | ||||
| @ -68,10 +68,8 @@ namespace Emby.Server.Implementations.EntryPoints | ||||
|         /// <inheritdoc /> | ||||
|         public Task RunAsync() | ||||
|         { | ||||
|             _userManager.UserDeleted += OnUserDeleted; | ||||
|             _userManager.UserUpdated += OnUserUpdated; | ||||
|             _userManager.UserPolicyUpdated += OnUserPolicyUpdated; | ||||
|             _userManager.UserConfigurationUpdated += OnUserConfigurationUpdated; | ||||
|             _userManager.OnUserDeleted += OnUserDeleted; | ||||
|             _userManager.OnUserUpdated += OnUserUpdated; | ||||
| 
 | ||||
|             _appHost.HasPendingRestartChanged += OnHasPendingRestartChanged; | ||||
| 
 | ||||
| @ -153,20 +151,6 @@ namespace Emby.Server.Implementations.EntryPoints | ||||
|             await SendMessageToUserSession(e.Argument, "UserDeleted", e.Argument.Id.ToString("N", CultureInfo.InvariantCulture)).ConfigureAwait(false); | ||||
|         } | ||||
| 
 | ||||
|         private async void OnUserPolicyUpdated(object sender, GenericEventArgs<User> e) | ||||
|         { | ||||
|             var dto = _userManager.GetUserDto(e.Argument); | ||||
| 
 | ||||
|             await SendMessageToUserSession(e.Argument, "UserPolicyUpdated", dto).ConfigureAwait(false); | ||||
|         } | ||||
| 
 | ||||
|         private async void OnUserConfigurationUpdated(object sender, GenericEventArgs<User> e) | ||||
|         { | ||||
|             var dto = _userManager.GetUserDto(e.Argument); | ||||
| 
 | ||||
|             await SendMessageToUserSession(e.Argument, "UserConfigurationUpdated", dto).ConfigureAwait(false); | ||||
|         } | ||||
| 
 | ||||
|         private async Task SendMessageToAdminSessions<T>(string name, T data) | ||||
|         { | ||||
|             try | ||||
| @ -210,10 +194,8 @@ namespace Emby.Server.Implementations.EntryPoints | ||||
|         { | ||||
|             if (dispose) | ||||
|             { | ||||
|                 _userManager.UserDeleted -= OnUserDeleted; | ||||
|                 _userManager.UserUpdated -= OnUserUpdated; | ||||
|                 _userManager.UserPolicyUpdated -= OnUserPolicyUpdated; | ||||
|                 _userManager.UserConfigurationUpdated -= OnUserConfigurationUpdated; | ||||
|                 _userManager.OnUserDeleted -= OnUserDeleted; | ||||
|                 _userManager.OnUserUpdated -= OnUserUpdated; | ||||
| 
 | ||||
|                 _installationManager.PluginUninstalled -= OnPluginUninstalled; | ||||
|                 _installationManager.PackageInstalling -= OnPackageInstalling; | ||||
|  | ||||
| @ -1,5 +1,4 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.Globalization; | ||||
| using System.Text; | ||||
| using MediaBrowser.Controller.Net; | ||||
|  | ||||
| @ -3,10 +3,11 @@ | ||||
| using System; | ||||
| using System.Linq; | ||||
| using Emby.Server.Implementations.SocketSharp; | ||||
| using Jellyfin.Data.Entities; | ||||
| using Jellyfin.Data.Enums; | ||||
| using MediaBrowser.Common.Net; | ||||
| using MediaBrowser.Controller.Authentication; | ||||
| using MediaBrowser.Controller.Configuration; | ||||
| using MediaBrowser.Controller.Entities; | ||||
| using MediaBrowser.Controller.Net; | ||||
| using MediaBrowser.Controller.Security; | ||||
| using MediaBrowser.Controller.Session; | ||||
| @ -38,9 +39,9 @@ namespace Emby.Server.Implementations.HttpServer.Security | ||||
|             _networkManager = networkManager; | ||||
|         } | ||||
| 
 | ||||
|         public void Authenticate(IRequest request, IAuthenticationAttributes authAttribtues) | ||||
|         public void Authenticate(IRequest request, IAuthenticationAttributes authAttributes) | ||||
|         { | ||||
|             ValidateUser(request, authAttribtues); | ||||
|             ValidateUser(request, authAttributes); | ||||
|         } | ||||
| 
 | ||||
|         public User Authenticate(HttpRequest request, IAuthenticationAttributes authAttributes) | ||||
| @ -50,17 +51,33 @@ namespace Emby.Server.Implementations.HttpServer.Security | ||||
|             return user; | ||||
|         } | ||||
| 
 | ||||
|         private User ValidateUser(IRequest request, IAuthenticationAttributes authAttribtues) | ||||
|         public AuthorizationInfo Authenticate(HttpRequest request) | ||||
|         { | ||||
|             var auth = _authorizationContext.GetAuthorizationInfo(request); | ||||
|             if (auth?.User == null) | ||||
|             { | ||||
|                 return null; | ||||
|             } | ||||
| 
 | ||||
|             if (auth.User.HasPermission(PermissionKind.IsDisabled)) | ||||
|             { | ||||
|                 throw new SecurityException("User account has been disabled."); | ||||
|             } | ||||
| 
 | ||||
|             return auth; | ||||
|         } | ||||
| 
 | ||||
|         private User ValidateUser(IRequest request, IAuthenticationAttributes authAttributes) | ||||
|         { | ||||
|             // This code is executed before the service | ||||
|             var auth = _authorizationContext.GetAuthorizationInfo(request); | ||||
| 
 | ||||
|             if (!IsExemptFromAuthenticationToken(authAttribtues, request)) | ||||
|             if (!IsExemptFromAuthenticationToken(authAttributes, request)) | ||||
|             { | ||||
|                 ValidateSecurityToken(request, auth.Token); | ||||
|             } | ||||
| 
 | ||||
|             if (authAttribtues.AllowLocalOnly && !request.IsLocal) | ||||
|             if (authAttributes.AllowLocalOnly && !request.IsLocal) | ||||
|             { | ||||
|                 throw new SecurityException("Operation not found."); | ||||
|             } | ||||
| @ -74,14 +91,14 @@ namespace Emby.Server.Implementations.HttpServer.Security | ||||
| 
 | ||||
|             if (user != null) | ||||
|             { | ||||
|                 ValidateUserAccess(user, request, authAttribtues, auth); | ||||
|                 ValidateUserAccess(user, request, authAttributes); | ||||
|             } | ||||
| 
 | ||||
|             var info = GetTokenInfo(request); | ||||
| 
 | ||||
|             if (!IsExemptFromRoles(auth, authAttribtues, request, info)) | ||||
|             if (!IsExemptFromRoles(auth, authAttributes, request, info)) | ||||
|             { | ||||
|                 var roles = authAttribtues.GetRoles(); | ||||
|                 var roles = authAttributes.GetRoles(); | ||||
| 
 | ||||
|                 ValidateRoles(roles, user); | ||||
|             } | ||||
| @ -90,7 +107,8 @@ namespace Emby.Server.Implementations.HttpServer.Security | ||||
|                 !string.IsNullOrEmpty(auth.Client) && | ||||
|                 !string.IsNullOrEmpty(auth.Device)) | ||||
|             { | ||||
|                 _sessionManager.LogSessionActivity(auth.Client, | ||||
|                 _sessionManager.LogSessionActivity( | ||||
|                     auth.Client, | ||||
|                     auth.Version, | ||||
|                     auth.DeviceId, | ||||
|                     auth.Device, | ||||
| @ -104,21 +122,20 @@ namespace Emby.Server.Implementations.HttpServer.Security | ||||
|         private void ValidateUserAccess( | ||||
|             User user, | ||||
|             IRequest request, | ||||
|             IAuthenticationAttributes authAttribtues, | ||||
|             AuthorizationInfo auth) | ||||
|             IAuthenticationAttributes authAttributes) | ||||
|         { | ||||
|             if (user.Policy.IsDisabled) | ||||
|             if (user.HasPermission(PermissionKind.IsDisabled)) | ||||
|             { | ||||
|                 throw new SecurityException("User account has been disabled."); | ||||
|             } | ||||
| 
 | ||||
|             if (!user.Policy.EnableRemoteAccess && !_networkManager.IsInLocalNetwork(request.RemoteIp)) | ||||
|             if (!user.HasPermission(PermissionKind.EnableRemoteAccess) && !_networkManager.IsInLocalNetwork(request.RemoteIp)) | ||||
|             { | ||||
|                 throw new SecurityException("User account has been disabled."); | ||||
|             } | ||||
| 
 | ||||
|             if (!user.Policy.IsAdministrator | ||||
|                 && !authAttribtues.EscapeParentalControl | ||||
|             if (!user.HasPermission(PermissionKind.IsAdministrator) | ||||
|                 && !authAttributes.EscapeParentalControl | ||||
|                 && !user.IsParentalScheduleAllowed()) | ||||
|             { | ||||
|                 request.Response.Headers.Add("X-Application-Error-Code", "ParentalControl"); | ||||
| @ -186,7 +203,7 @@ namespace Emby.Server.Implementations.HttpServer.Security | ||||
|         { | ||||
|             if (roles.Contains("admin", StringComparer.OrdinalIgnoreCase)) | ||||
|             { | ||||
|                 if (user == null || !user.Policy.IsAdministrator) | ||||
|                 if (user == null || !user.HasPermission(PermissionKind.IsAdministrator)) | ||||
|                 { | ||||
|                     throw new SecurityException("User does not have admin access."); | ||||
|                 } | ||||
| @ -194,7 +211,7 @@ namespace Emby.Server.Implementations.HttpServer.Security | ||||
| 
 | ||||
|             if (roles.Contains("delete", StringComparer.OrdinalIgnoreCase)) | ||||
|             { | ||||
|                 if (user == null || !user.Policy.EnableContentDeletion) | ||||
|                 if (user == null || !user.HasPermission(PermissionKind.EnableContentDeletion)) | ||||
|                 { | ||||
|                     throw new SecurityException("User does not have delete access."); | ||||
|                 } | ||||
| @ -202,7 +219,7 @@ namespace Emby.Server.Implementations.HttpServer.Security | ||||
| 
 | ||||
|             if (roles.Contains("download", StringComparer.OrdinalIgnoreCase)) | ||||
|             { | ||||
|                 if (user == null || !user.Policy.EnableContentDownloading) | ||||
|                 if (user == null || !user.HasPermission(PermissionKind.EnableContentDownloading)) | ||||
|                 { | ||||
|                     throw new SecurityException("User does not have download access."); | ||||
|                 } | ||||
| @ -228,16 +245,6 @@ namespace Emby.Server.Implementations.HttpServer.Security | ||||
|             { | ||||
|                 throw new AuthenticationException("Access token is invalid or expired."); | ||||
|             } | ||||
| 
 | ||||
|             //if (!string.IsNullOrEmpty(info.UserId)) | ||||
|             //{ | ||||
|             //    var user = _userManager.GetUserById(info.UserId); | ||||
| 
 | ||||
|             //    if (user == null || user.Configuration.IsDisabled) | ||||
|             //    { | ||||
|             //        throw new SecurityException("User account has been disabled."); | ||||
|             //    } | ||||
|             //} | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -8,6 +8,7 @@ using MediaBrowser.Controller.Library; | ||||
| using MediaBrowser.Controller.Net; | ||||
| using MediaBrowser.Controller.Security; | ||||
| using MediaBrowser.Model.Services; | ||||
| using Microsoft.AspNetCore.Http; | ||||
| using Microsoft.Net.Http.Headers; | ||||
| 
 | ||||
| namespace Emby.Server.Implementations.HttpServer.Security | ||||
| @ -38,6 +39,14 @@ namespace Emby.Server.Implementations.HttpServer.Security | ||||
|             return GetAuthorization(requestContext); | ||||
|         } | ||||
| 
 | ||||
|         public AuthorizationInfo GetAuthorizationInfo(HttpRequest requestContext) | ||||
|         { | ||||
|             var auth = GetAuthorizationDictionary(requestContext); | ||||
|             var (authInfo, _) = | ||||
|                 GetAuthorizationInfoFromDictionary(auth, requestContext.Headers, requestContext.Query); | ||||
|             return authInfo; | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets the authorization. | ||||
|         /// </summary> | ||||
| @ -46,7 +55,23 @@ namespace Emby.Server.Implementations.HttpServer.Security | ||||
|         private AuthorizationInfo GetAuthorization(IRequest httpReq) | ||||
|         { | ||||
|             var auth = GetAuthorizationDictionary(httpReq); | ||||
|             var (authInfo, originalAuthInfo) = | ||||
|                 GetAuthorizationInfoFromDictionary(auth, httpReq.Headers, httpReq.QueryString); | ||||
| 
 | ||||
|             if (originalAuthInfo != null) | ||||
|             { | ||||
|                 httpReq.Items["OriginalAuthenticationInfo"] = originalAuthInfo; | ||||
|             } | ||||
| 
 | ||||
|             httpReq.Items["AuthorizationInfo"] = authInfo; | ||||
|             return authInfo; | ||||
|         } | ||||
| 
 | ||||
|         private (AuthorizationInfo authInfo, AuthenticationInfo originalAuthenticationInfo) GetAuthorizationInfoFromDictionary( | ||||
|             in Dictionary<string, string> auth, | ||||
|             in IHeaderDictionary headers, | ||||
|             in IQueryCollection queryString) | ||||
|         { | ||||
|             string deviceId = null; | ||||
|             string device = null; | ||||
|             string client = null; | ||||
| @ -64,19 +89,26 @@ namespace Emby.Server.Implementations.HttpServer.Security | ||||
| 
 | ||||
|             if (string.IsNullOrEmpty(token)) | ||||
|             { | ||||
|                 token = httpReq.Headers["X-Emby-Token"]; | ||||
|                 token = headers["X-Emby-Token"]; | ||||
|             } | ||||
| 
 | ||||
|             if (string.IsNullOrEmpty(token)) | ||||
|             { | ||||
|                 token = httpReq.Headers["X-MediaBrowser-Token"]; | ||||
|             } | ||||
|             if (string.IsNullOrEmpty(token)) | ||||
|             { | ||||
|                 token = httpReq.QueryString["api_key"]; | ||||
|                 token = headers["X-MediaBrowser-Token"]; | ||||
|             } | ||||
| 
 | ||||
|             var info = new AuthorizationInfo | ||||
|             if (string.IsNullOrEmpty(token)) | ||||
|             { | ||||
|                 token = queryString["ApiKey"]; | ||||
|             } | ||||
| 
 | ||||
|             // TODO deprecate this query parameter. | ||||
|             if (string.IsNullOrEmpty(token)) | ||||
|             { | ||||
|                 token = queryString["api_key"]; | ||||
|             } | ||||
| 
 | ||||
|             var authInfo = new AuthorizationInfo | ||||
|             { | ||||
|                 Client = client, | ||||
|                 Device = device, | ||||
| @ -85,6 +117,7 @@ namespace Emby.Server.Implementations.HttpServer.Security | ||||
|                 Token = token | ||||
|             }; | ||||
| 
 | ||||
|             AuthenticationInfo originalAuthenticationInfo = null; | ||||
|             if (!string.IsNullOrWhiteSpace(token)) | ||||
|             { | ||||
|                 var result = _authRepo.Get(new AuthenticationInfoQuery | ||||
| @ -92,81 +125,77 @@ namespace Emby.Server.Implementations.HttpServer.Security | ||||
|                     AccessToken = token | ||||
|                 }); | ||||
| 
 | ||||
|                 var tokenInfo = result.Items.Count > 0 ? result.Items[0] : null; | ||||
|                 originalAuthenticationInfo = result.Items.Count > 0 ? result.Items[0] : null; | ||||
| 
 | ||||
|                 if (tokenInfo != null) | ||||
|                 if (originalAuthenticationInfo != null) | ||||
|                 { | ||||
|                     var updateToken = false; | ||||
| 
 | ||||
|                     // TODO: Remove these checks for IsNullOrWhiteSpace | ||||
|                     if (string.IsNullOrWhiteSpace(info.Client)) | ||||
|                     if (string.IsNullOrWhiteSpace(authInfo.Client)) | ||||
|                     { | ||||
|                         info.Client = tokenInfo.AppName; | ||||
|                         authInfo.Client = originalAuthenticationInfo.AppName; | ||||
|                     } | ||||
| 
 | ||||
|                     if (string.IsNullOrWhiteSpace(info.DeviceId)) | ||||
|                     if (string.IsNullOrWhiteSpace(authInfo.DeviceId)) | ||||
|                     { | ||||
|                         info.DeviceId = tokenInfo.DeviceId; | ||||
|                         authInfo.DeviceId = originalAuthenticationInfo.DeviceId; | ||||
|                     } | ||||
| 
 | ||||
|                     // Temporary. TODO - allow clients to specify that the token has been shared with a casting device | ||||
|                     var allowTokenInfoUpdate = info.Client == null || info.Client.IndexOf("chromecast", StringComparison.OrdinalIgnoreCase) == -1; | ||||
|                     var allowTokenInfoUpdate = authInfo.Client == null || authInfo.Client.IndexOf("chromecast", StringComparison.OrdinalIgnoreCase) == -1; | ||||
| 
 | ||||
|                     if (string.IsNullOrWhiteSpace(info.Device)) | ||||
|                     if (string.IsNullOrWhiteSpace(authInfo.Device)) | ||||
|                     { | ||||
|                         info.Device = tokenInfo.DeviceName; | ||||
|                         authInfo.Device = originalAuthenticationInfo.DeviceName; | ||||
|                     } | ||||
| 
 | ||||
|                     else if (!string.Equals(info.Device, tokenInfo.DeviceName, StringComparison.OrdinalIgnoreCase)) | ||||
|                     else if (!string.Equals(authInfo.Device, originalAuthenticationInfo.DeviceName, StringComparison.OrdinalIgnoreCase)) | ||||
|                     { | ||||
|                         if (allowTokenInfoUpdate) | ||||
|                         { | ||||
|                             updateToken = true; | ||||
|                             tokenInfo.DeviceName = info.Device; | ||||
|                             originalAuthenticationInfo.DeviceName = authInfo.Device; | ||||
|                         } | ||||
|                     } | ||||
| 
 | ||||
|                     if (string.IsNullOrWhiteSpace(info.Version)) | ||||
|                     if (string.IsNullOrWhiteSpace(authInfo.Version)) | ||||
|                     { | ||||
|                         info.Version = tokenInfo.AppVersion; | ||||
|                         authInfo.Version = originalAuthenticationInfo.AppVersion; | ||||
|                     } | ||||
|                     else if (!string.Equals(info.Version, tokenInfo.AppVersion, StringComparison.OrdinalIgnoreCase)) | ||||
|                     else if (!string.Equals(authInfo.Version, originalAuthenticationInfo.AppVersion, StringComparison.OrdinalIgnoreCase)) | ||||
|                     { | ||||
|                         if (allowTokenInfoUpdate) | ||||
|                         { | ||||
|                             updateToken = true; | ||||
|                             tokenInfo.AppVersion = info.Version; | ||||
|                             originalAuthenticationInfo.AppVersion = authInfo.Version; | ||||
|                         } | ||||
|                     } | ||||
| 
 | ||||
|                     if ((DateTime.UtcNow - tokenInfo.DateLastActivity).TotalMinutes > 3) | ||||
|                     if ((DateTime.UtcNow - originalAuthenticationInfo.DateLastActivity).TotalMinutes > 3) | ||||
|                     { | ||||
|                         tokenInfo.DateLastActivity = DateTime.UtcNow; | ||||
|                         originalAuthenticationInfo.DateLastActivity = DateTime.UtcNow; | ||||
|                         updateToken = true; | ||||
|                     } | ||||
| 
 | ||||
|                     if (!tokenInfo.UserId.Equals(Guid.Empty)) | ||||
|                     if (!originalAuthenticationInfo.UserId.Equals(Guid.Empty)) | ||||
|                     { | ||||
|                         info.User = _userManager.GetUserById(tokenInfo.UserId); | ||||
|                         authInfo.User = _userManager.GetUserById(originalAuthenticationInfo.UserId); | ||||
| 
 | ||||
|                         if (info.User != null && !string.Equals(info.User.Name, tokenInfo.UserName, StringComparison.OrdinalIgnoreCase)) | ||||
|                         if (authInfo.User != null && !string.Equals(authInfo.User.Username, originalAuthenticationInfo.UserName, StringComparison.OrdinalIgnoreCase)) | ||||
|                         { | ||||
|                             tokenInfo.UserName = info.User.Name; | ||||
|                             originalAuthenticationInfo.UserName = authInfo.User.Username; | ||||
|                             updateToken = true; | ||||
|                         } | ||||
|                     } | ||||
| 
 | ||||
|                     if (updateToken) | ||||
|                     { | ||||
|                         _authRepo.Update(tokenInfo); | ||||
|                         _authRepo.Update(originalAuthenticationInfo); | ||||
|                     } | ||||
|                 } | ||||
|                 httpReq.Items["OriginalAuthenticationInfo"] = tokenInfo; | ||||
|             } | ||||
| 
 | ||||
|             httpReq.Items["AuthorizationInfo"] = info; | ||||
| 
 | ||||
|             return info; | ||||
|             return (authInfo, originalAuthenticationInfo); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
| @ -186,6 +215,23 @@ namespace Emby.Server.Implementations.HttpServer.Security | ||||
|             return GetAuthorization(auth); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets the auth. | ||||
|         /// </summary> | ||||
|         /// <param name="httpReq">The HTTP req.</param> | ||||
|         /// <returns>Dictionary{System.StringSystem.String}.</returns> | ||||
|         private Dictionary<string, string> GetAuthorizationDictionary(HttpRequest httpReq) | ||||
|         { | ||||
|             var auth = httpReq.Headers["X-Emby-Authorization"]; | ||||
| 
 | ||||
|             if (string.IsNullOrEmpty(auth)) | ||||
|             { | ||||
|                 auth = httpReq.Headers[HeaderNames.Authorization]; | ||||
|             } | ||||
| 
 | ||||
|             return GetAuthorization(auth); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets the authorization. | ||||
|         /// </summary> | ||||
| @ -236,12 +282,7 @@ namespace Emby.Server.Implementations.HttpServer.Security | ||||
| 
 | ||||
|         private static string NormalizeValue(string value) | ||||
|         { | ||||
|             if (string.IsNullOrEmpty(value)) | ||||
|             { | ||||
|                 return value; | ||||
|             } | ||||
| 
 | ||||
|             return WebUtility.HtmlEncode(value); | ||||
|             return string.IsNullOrEmpty(value) ? value : WebUtility.HtmlEncode(value); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -1,7 +1,7 @@ | ||||
| #pragma warning disable CS1591 | ||||
| 
 | ||||
| using System; | ||||
| using MediaBrowser.Controller.Entities; | ||||
| using Jellyfin.Data.Entities; | ||||
| using MediaBrowser.Controller.Library; | ||||
| using MediaBrowser.Controller.Net; | ||||
| using MediaBrowser.Controller.Security; | ||||
|  | ||||
| @ -234,10 +234,12 @@ namespace Emby.Server.Implementations.HttpServer | ||||
|         private Task SendKeepAliveResponse() | ||||
|         { | ||||
|             LastKeepAliveDate = DateTime.UtcNow; | ||||
|             return SendAsync(new WebSocketMessage<string> | ||||
|             { | ||||
|                 MessageType = "KeepAlive" | ||||
|             }, CancellationToken.None); | ||||
|             return SendAsync( | ||||
|                 new WebSocketMessage<string> | ||||
|                 { | ||||
|                     MessageId = Guid.NewGuid(), | ||||
|                     MessageType = "KeepAlive" | ||||
|                 }, CancellationToken.None); | ||||
|         } | ||||
| 
 | ||||
|         /// <inheritdoc /> | ||||
|  | ||||
| @ -17,6 +17,8 @@ using Emby.Server.Implementations.Library.Resolvers; | ||||
| using Emby.Server.Implementations.Library.Validators; | ||||
| using Emby.Server.Implementations.Playlists; | ||||
| using Emby.Server.Implementations.ScheduledTasks; | ||||
| using Jellyfin.Data.Entities; | ||||
| using Jellyfin.Data.Enums; | ||||
| using MediaBrowser.Common.Extensions; | ||||
| using MediaBrowser.Common.Progress; | ||||
| using MediaBrowser.Controller; | ||||
| @ -25,7 +27,6 @@ using MediaBrowser.Controller.Drawing; | ||||
| using MediaBrowser.Controller.Dto; | ||||
| using MediaBrowser.Controller.Entities; | ||||
| using MediaBrowser.Controller.Entities.Audio; | ||||
| using MediaBrowser.Controller.Entities.TV; | ||||
| using MediaBrowser.Controller.IO; | ||||
| using MediaBrowser.Controller.Library; | ||||
| using MediaBrowser.Controller.LiveTv; | ||||
| @ -46,6 +47,9 @@ using MediaBrowser.Model.Querying; | ||||
| using MediaBrowser.Model.Tasks; | ||||
| using MediaBrowser.Providers.MediaInfo; | ||||
| using Microsoft.Extensions.Logging; | ||||
| using Episode = MediaBrowser.Controller.Entities.TV.Episode; | ||||
| using Genre = MediaBrowser.Controller.Entities.Genre; | ||||
| using Person = MediaBrowser.Controller.Entities.Person; | ||||
| using SortOrder = MediaBrowser.Model.Entities.SortOrder; | ||||
| using VideoResolver = Emby.Naming.Video.VideoResolver; | ||||
| 
 | ||||
| @ -1539,7 +1543,8 @@ namespace Emby.Server.Implementations.Library | ||||
|                 } | ||||
| 
 | ||||
|                 // Handle grouping | ||||
|                 if (user != null && !string.IsNullOrEmpty(view.ViewType) && UserView.IsEligibleForGrouping(view.ViewType) && user.Configuration.GroupedFolders.Length > 0) | ||||
|                 if (user != null && !string.IsNullOrEmpty(view.ViewType) && UserView.IsEligibleForGrouping(view.ViewType) | ||||
|                     && user.GetPreference(PreferenceKind.GroupedFolders).Length > 0) | ||||
|                 { | ||||
|                     return GetUserRootFolder() | ||||
|                         .GetChildren(user, true) | ||||
|  | ||||
| @ -7,6 +7,8 @@ using System.IO; | ||||
| using System.Linq; | ||||
| using System.Threading; | ||||
| using System.Threading.Tasks; | ||||
| using Jellyfin.Data.Entities; | ||||
| using Jellyfin.Data.Enums; | ||||
| using MediaBrowser.Common.Configuration; | ||||
| using MediaBrowser.Common.Extensions; | ||||
| using MediaBrowser.Controller.Entities; | ||||
| @ -14,7 +16,6 @@ using MediaBrowser.Controller.Library; | ||||
| using MediaBrowser.Controller.MediaEncoding; | ||||
| using MediaBrowser.Controller.Persistence; | ||||
| using MediaBrowser.Controller.Providers; | ||||
| using MediaBrowser.Model.Configuration; | ||||
| using MediaBrowser.Model.Dlna; | ||||
| using MediaBrowser.Model.Dto; | ||||
| using MediaBrowser.Model.Entities; | ||||
| @ -190,10 +191,7 @@ namespace Emby.Server.Implementations.Library | ||||
|                 { | ||||
|                     if (string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase)) | ||||
|                     { | ||||
|                         if (!user.Policy.EnableAudioPlaybackTranscoding) | ||||
|                         { | ||||
|                             source.SupportsTranscoding = false; | ||||
|                         } | ||||
|                         source.SupportsTranscoding = user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding); | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
| @ -352,7 +350,9 @@ namespace Emby.Server.Implementations.Library | ||||
| 
 | ||||
|         private void SetDefaultSubtitleStreamIndex(MediaSourceInfo source, UserItemData userData, User user, bool allowRememberingSelection) | ||||
|         { | ||||
|             if (userData.SubtitleStreamIndex.HasValue && user.Configuration.RememberSubtitleSelections && user.Configuration.SubtitleMode != SubtitlePlaybackMode.None && allowRememberingSelection) | ||||
|             if (userData.SubtitleStreamIndex.HasValue | ||||
|                 && user.RememberSubtitleSelections | ||||
|                 && user.SubtitleMode != SubtitlePlaybackMode.None && allowRememberingSelection) | ||||
|             { | ||||
|                 var index = userData.SubtitleStreamIndex.Value; | ||||
|                 // Make sure the saved index is still valid | ||||
| @ -363,26 +363,27 @@ namespace Emby.Server.Implementations.Library | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             var preferredSubs = string.IsNullOrEmpty(user.Configuration.SubtitleLanguagePreference) | ||||
|                 ? Array.Empty<string>() : NormalizeLanguage(user.Configuration.SubtitleLanguagePreference); | ||||
| 
 | ||||
|             var preferredSubs = string.IsNullOrEmpty(user.SubtitleLanguagePreference) | ||||
|                 ? Array.Empty<string>() : NormalizeLanguage(user.SubtitleLanguagePreference); | ||||
| 
 | ||||
|             var defaultAudioIndex = source.DefaultAudioStreamIndex; | ||||
|             var audioLangage = defaultAudioIndex == null | ||||
|                 ? null | ||||
|                 : source.MediaStreams.Where(i => i.Type == MediaStreamType.Audio && i.Index == defaultAudioIndex).Select(i => i.Language).FirstOrDefault(); | ||||
| 
 | ||||
|             source.DefaultSubtitleStreamIndex = MediaStreamSelector.GetDefaultSubtitleStreamIndex(source.MediaStreams, | ||||
|             source.DefaultSubtitleStreamIndex = MediaStreamSelector.GetDefaultSubtitleStreamIndex( | ||||
|                 source.MediaStreams, | ||||
|                 preferredSubs, | ||||
|                 user.Configuration.SubtitleMode, | ||||
|                 user.SubtitleMode, | ||||
|                 audioLangage); | ||||
| 
 | ||||
|             MediaStreamSelector.SetSubtitleStreamScores(source.MediaStreams, preferredSubs, | ||||
|                 user.Configuration.SubtitleMode, audioLangage); | ||||
|             MediaStreamSelector.SetSubtitleStreamScores(source.MediaStreams, preferredSubs, user.SubtitleMode, audioLangage); | ||||
|         } | ||||
| 
 | ||||
|         private void SetDefaultAudioStreamIndex(MediaSourceInfo source, UserItemData userData, User user, bool allowRememberingSelection) | ||||
|         { | ||||
|             if (userData.AudioStreamIndex.HasValue && user.Configuration.RememberAudioSelections && allowRememberingSelection) | ||||
|             if (userData.AudioStreamIndex.HasValue && user.RememberAudioSelections && allowRememberingSelection) | ||||
|             { | ||||
|                 var index = userData.AudioStreamIndex.Value; | ||||
|                 // Make sure the saved index is still valid | ||||
| @ -393,11 +394,11 @@ namespace Emby.Server.Implementations.Library | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             var preferredAudio = string.IsNullOrEmpty(user.Configuration.AudioLanguagePreference) | ||||
|             var preferredAudio = string.IsNullOrEmpty(user.AudioLanguagePreference) | ||||
|                 ? Array.Empty<string>() | ||||
|                 : NormalizeLanguage(user.Configuration.AudioLanguagePreference); | ||||
|                 : NormalizeLanguage(user.AudioLanguagePreference); | ||||
| 
 | ||||
|             source.DefaultAudioStreamIndex = MediaStreamSelector.GetDefaultAudioStreamIndex(source.MediaStreams, preferredAudio, user.Configuration.PlayDefaultAudioTrack); | ||||
|             source.DefaultAudioStreamIndex = MediaStreamSelector.GetDefaultAudioStreamIndex(source.MediaStreams, preferredAudio, user.PlayDefaultAudioTrack); | ||||
|         } | ||||
| 
 | ||||
|         public void SetDefaultAudioAndSubtitleStreamIndexes(BaseItem item, MediaSourceInfo source, User user) | ||||
| @ -534,7 +535,7 @@ namespace Emby.Server.Implementations.Library | ||||
|                 mediaSource.RunTimeTicks = null; | ||||
|             } | ||||
| 
 | ||||
|             var audioStream = mediaSource.MediaStreams.FirstOrDefault(i => i.Type == MediaBrowser.Model.Entities.MediaStreamType.Audio); | ||||
|             var audioStream = mediaSource.MediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Audio); | ||||
| 
 | ||||
|             if (audioStream == null || audioStream.Index == -1) | ||||
|             { | ||||
| @ -545,7 +546,7 @@ namespace Emby.Server.Implementations.Library | ||||
|                 mediaSource.DefaultAudioStreamIndex = audioStream.Index; | ||||
|             } | ||||
| 
 | ||||
|             var videoStream = mediaSource.MediaStreams.FirstOrDefault(i => i.Type == MediaBrowser.Model.Entities.MediaStreamType.Video); | ||||
|             var videoStream = mediaSource.MediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Video); | ||||
|             if (videoStream != null) | ||||
|             { | ||||
|                 if (!videoStream.BitRate.HasValue) | ||||
| @ -556,17 +557,14 @@ namespace Emby.Server.Implementations.Library | ||||
|                     { | ||||
|                         videoStream.BitRate = 30000000; | ||||
|                     } | ||||
| 
 | ||||
|                     else if (width >= 1900) | ||||
|                     { | ||||
|                         videoStream.BitRate = 20000000; | ||||
|                     } | ||||
| 
 | ||||
|                     else if (width >= 1200) | ||||
|                     { | ||||
|                         videoStream.BitRate = 8000000; | ||||
|                     } | ||||
| 
 | ||||
|                     else if (width >= 700) | ||||
|                     { | ||||
|                         videoStream.BitRate = 2000000; | ||||
| @ -670,13 +668,14 @@ namespace Emby.Server.Implementations.Library | ||||
|                     mediaSource.AnalyzeDurationMs = 3000; | ||||
|                 } | ||||
| 
 | ||||
|                 mediaInfo = await _mediaEncoder.GetMediaInfo(new MediaInfoRequest | ||||
|                 mediaInfo = await _mediaEncoder.GetMediaInfo( | ||||
|                     new MediaInfoRequest | ||||
|                 { | ||||
|                     MediaSource = mediaSource, | ||||
|                     MediaType = isAudio ? DlnaProfileType.Audio : DlnaProfileType.Video, | ||||
|                     ExtractChapters = false | ||||
| 
 | ||||
|                 }, cancellationToken).ConfigureAwait(false); | ||||
|                 }, | ||||
|                     cancellationToken).ConfigureAwait(false); | ||||
| 
 | ||||
|                 if (cacheFilePath != null) | ||||
|                 { | ||||
|  | ||||
| @ -3,7 +3,7 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.Linq; | ||||
| using MediaBrowser.Model.Configuration; | ||||
| using Jellyfin.Data.Enums; | ||||
| using MediaBrowser.Model.Entities; | ||||
| 
 | ||||
| namespace Emby.Server.Implementations.Library | ||||
|  | ||||
| @ -3,6 +3,7 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.Linq; | ||||
| using Jellyfin.Data.Entities; | ||||
| using MediaBrowser.Controller.Dto; | ||||
| using MediaBrowser.Controller.Entities; | ||||
| using MediaBrowser.Controller.Entities.Audio; | ||||
| @ -10,6 +11,7 @@ using MediaBrowser.Controller.Library; | ||||
| using MediaBrowser.Controller.Playlists; | ||||
| using MediaBrowser.Model.Entities; | ||||
| using MediaBrowser.Model.Querying; | ||||
| using MusicAlbum = MediaBrowser.Controller.Entities.Audio.MusicAlbum; | ||||
| 
 | ||||
| namespace Emby.Server.Implementations.Library | ||||
| { | ||||
| @ -75,7 +77,6 @@ namespace Emby.Server.Implementations.Library | ||||
|                 { | ||||
|                     return Guid.Empty; | ||||
|                 } | ||||
| 
 | ||||
|             }).Where(i => !i.Equals(Guid.Empty)).ToArray(); | ||||
| 
 | ||||
|             return GetInstantMixFromGenreIds(genreIds, user, dtoOptions); | ||||
| @ -105,32 +106,27 @@ namespace Emby.Server.Implementations.Library | ||||
|                 return GetInstantMixFromGenreIds(new[] { item.Id }, user, dtoOptions); | ||||
|             } | ||||
| 
 | ||||
|             var playlist = item as Playlist; | ||||
|             if (playlist != null) | ||||
|             if (item is Playlist playlist) | ||||
|             { | ||||
|                 return GetInstantMixFromPlaylist(playlist, user, dtoOptions); | ||||
|             } | ||||
| 
 | ||||
|             var album = item as MusicAlbum; | ||||
|             if (album != null) | ||||
|             if (item is MusicAlbum album) | ||||
|             { | ||||
|                 return GetInstantMixFromAlbum(album, user, dtoOptions); | ||||
|             } | ||||
| 
 | ||||
|             var artist = item as MusicArtist; | ||||
|             if (artist != null) | ||||
|             if (item is MusicArtist artist) | ||||
|             { | ||||
|                 return GetInstantMixFromArtist(artist, user, dtoOptions); | ||||
|             } | ||||
| 
 | ||||
|             var song = item as Audio; | ||||
|             if (song != null) | ||||
|             if (item is Audio song) | ||||
|             { | ||||
|                 return GetInstantMixFromSong(song, user, dtoOptions); | ||||
|             } | ||||
| 
 | ||||
|             var folder = item as Folder; | ||||
|             if (folder != null) | ||||
|             if (item is Folder folder) | ||||
|             { | ||||
|                 return GetInstantMixFromFolder(folder, user, dtoOptions); | ||||
|             } | ||||
|  | ||||
| @ -3,6 +3,7 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.Linq; | ||||
| using Jellyfin.Data.Entities; | ||||
| using MediaBrowser.Controller.Dto; | ||||
| using MediaBrowser.Controller.Entities; | ||||
| using MediaBrowser.Controller.Entities.Audio; | ||||
| @ -12,6 +13,8 @@ using MediaBrowser.Model.Entities; | ||||
| using MediaBrowser.Model.Querying; | ||||
| using MediaBrowser.Model.Search; | ||||
| using Microsoft.Extensions.Logging; | ||||
| using Genre = MediaBrowser.Controller.Entities.Genre; | ||||
| using Person = MediaBrowser.Controller.Entities.Person; | ||||
| 
 | ||||
| namespace Emby.Server.Implementations.Library | ||||
| { | ||||
|  | ||||
| @ -5,6 +5,7 @@ using System.Collections.Concurrent; | ||||
| using System.Collections.Generic; | ||||
| using System.Globalization; | ||||
| using System.Threading; | ||||
| using Jellyfin.Data.Entities; | ||||
| using MediaBrowser.Controller.Configuration; | ||||
| using MediaBrowser.Controller.Dto; | ||||
| using MediaBrowser.Controller.Entities; | ||||
| @ -13,6 +14,7 @@ using MediaBrowser.Controller.Persistence; | ||||
| using MediaBrowser.Model.Dto; | ||||
| using MediaBrowser.Model.Entities; | ||||
| using Microsoft.Extensions.Logging; | ||||
| using Book = MediaBrowser.Controller.Entities.Book; | ||||
| 
 | ||||
| namespace Emby.Server.Implementations.Library | ||||
| { | ||||
|  | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @ -5,6 +5,8 @@ using System.Collections.Generic; | ||||
| using System.Globalization; | ||||
| using System.Linq; | ||||
| using System.Threading; | ||||
| using Jellyfin.Data.Entities; | ||||
| using Jellyfin.Data.Enums; | ||||
| using MediaBrowser.Controller.Channels; | ||||
| using MediaBrowser.Controller.Configuration; | ||||
| using MediaBrowser.Controller.Dto; | ||||
| @ -17,6 +19,8 @@ using MediaBrowser.Model.Entities; | ||||
| using MediaBrowser.Model.Globalization; | ||||
| using MediaBrowser.Model.Library; | ||||
| using MediaBrowser.Model.Querying; | ||||
| using Genre = MediaBrowser.Controller.Entities.Genre; | ||||
| using Person = MediaBrowser.Controller.Entities.Person; | ||||
| 
 | ||||
| namespace Emby.Server.Implementations.Library | ||||
| { | ||||
| @ -125,12 +129,12 @@ namespace Emby.Server.Implementations.Library | ||||
| 
 | ||||
|             if (!query.IncludeHidden) | ||||
|             { | ||||
|                 list = list.Where(i => !user.Configuration.MyMediaExcludes.Contains(i.Id.ToString("N", CultureInfo.InvariantCulture))).ToList(); | ||||
|                 list = list.Where(i => !user.GetPreference(PreferenceKind.MyMediaExcludes).Contains(i.Id.ToString("N", CultureInfo.InvariantCulture))).ToList(); | ||||
|             } | ||||
| 
 | ||||
|             var sorted = _libraryManager.Sort(list, user, new[] { ItemSortBy.SortName }, SortOrder.Ascending).ToList(); | ||||
| 
 | ||||
|             var orders = user.Configuration.OrderedViews.ToList(); | ||||
|             var orders = user.GetPreference(PreferenceKind.OrderedViews).ToList(); | ||||
| 
 | ||||
|             return list | ||||
|                 .OrderBy(i => | ||||
| @ -165,7 +169,13 @@ namespace Emby.Server.Implementations.Library | ||||
|             return GetUserSubViewWithName(name, parentId, type, sortName); | ||||
|         } | ||||
| 
 | ||||
|         private Folder GetUserView(List<ICollectionFolder> parents, string viewType, string localizationKey, string sortName, User user, string[] presetViews) | ||||
|         private Folder GetUserView( | ||||
|             List<ICollectionFolder> parents, | ||||
|             string viewType, | ||||
|             string localizationKey, | ||||
|             string sortName, | ||||
|             Jellyfin.Data.Entities.User user, | ||||
|             string[] presetViews) | ||||
|         { | ||||
|             if (parents.Count == 1 && parents.All(i => string.Equals(i.CollectionType, viewType, StringComparison.OrdinalIgnoreCase))) | ||||
|             { | ||||
| @ -270,7 +280,8 @@ namespace Emby.Server.Implementations.Library | ||||
|             { | ||||
|                 parents = _libraryManager.GetUserRootFolder().GetChildren(user, true) | ||||
|                     .Where(i => i is Folder) | ||||
|                     .Where(i => !user.Configuration.LatestItemsExcludes.Contains(i.Id.ToString("N", CultureInfo.InvariantCulture))) | ||||
|                     .Where(i => !user.GetPreference(PreferenceKind.LatestItemExcludes) | ||||
|                         .Contains(i.Id.ToString("N", CultureInfo.InvariantCulture))) | ||||
|                     .ToList(); | ||||
|             } | ||||
| 
 | ||||
| @ -331,12 +342,11 @@ namespace Emby.Server.Implementations.Library | ||||
| 
 | ||||
|             var excludeItemTypes = includeItemTypes.Length == 0 && mediaTypes.Count == 0 ? new[] | ||||
|             { | ||||
|                 typeof(Person).Name, | ||||
|                 typeof(Studio).Name, | ||||
|                 typeof(Year).Name, | ||||
|                 typeof(MusicGenre).Name, | ||||
|                 typeof(Genre).Name | ||||
| 
 | ||||
|                 nameof(Person), | ||||
|                 nameof(Studio), | ||||
|                 nameof(Year), | ||||
|                 nameof(MusicGenre), | ||||
|                 nameof(Genre) | ||||
|             } : Array.Empty<string>(); | ||||
| 
 | ||||
|             var query = new InternalItemsQuery(user) | ||||
|  | ||||
| @ -7,6 +7,8 @@ using System.Linq; | ||||
| using System.Threading; | ||||
| using System.Threading.Tasks; | ||||
| using Emby.Server.Implementations.Library; | ||||
| using Jellyfin.Data.Entities; | ||||
| using Jellyfin.Data.Enums; | ||||
| using MediaBrowser.Common.Configuration; | ||||
| using MediaBrowser.Common.Extensions; | ||||
| using MediaBrowser.Common.Progress; | ||||
| @ -14,8 +16,6 @@ using MediaBrowser.Controller.Channels; | ||||
| using MediaBrowser.Controller.Configuration; | ||||
| using MediaBrowser.Controller.Dto; | ||||
| using MediaBrowser.Controller.Entities; | ||||
| using MediaBrowser.Controller.Entities.Movies; | ||||
| using MediaBrowser.Controller.Entities.TV; | ||||
| using MediaBrowser.Controller.Library; | ||||
| using MediaBrowser.Controller.LiveTv; | ||||
| using MediaBrowser.Controller.Persistence; | ||||
| @ -31,6 +31,8 @@ using MediaBrowser.Model.Querying; | ||||
| using MediaBrowser.Model.Serialization; | ||||
| using MediaBrowser.Model.Tasks; | ||||
| using Microsoft.Extensions.Logging; | ||||
| using Episode = MediaBrowser.Controller.Entities.TV.Episode; | ||||
| using Movie = MediaBrowser.Controller.Entities.Movies.Movie; | ||||
| 
 | ||||
| namespace Emby.Server.Implementations.LiveTv | ||||
| { | ||||
| @ -696,7 +698,6 @@ namespace Emby.Server.Implementations.LiveTv | ||||
|                     { | ||||
|                         Path = info.ThumbImageUrl, | ||||
|                         Type = ImageType.Thumb | ||||
| 
 | ||||
|                     }, 0); | ||||
|                 } | ||||
|             } | ||||
| @ -709,7 +710,6 @@ namespace Emby.Server.Implementations.LiveTv | ||||
|                     { | ||||
|                         Path = info.LogoImageUrl, | ||||
|                         Type = ImageType.Logo | ||||
| 
 | ||||
|                     }, 0); | ||||
|                 } | ||||
|             } | ||||
| @ -722,7 +722,6 @@ namespace Emby.Server.Implementations.LiveTv | ||||
|                     { | ||||
|                         Path = info.BackdropImageUrl, | ||||
|                         Type = ImageType.Backdrop | ||||
| 
 | ||||
|                     }, 0); | ||||
|                 } | ||||
|             } | ||||
| @ -760,7 +759,8 @@ namespace Emby.Server.Implementations.LiveTv | ||||
| 
 | ||||
|             var dto = _dtoService.GetBaseItemDto(program, new DtoOptions(), user); | ||||
| 
 | ||||
|             var list = new List<Tuple<BaseItemDto, string, string>>() { | ||||
|             var list = new List<Tuple<BaseItemDto, string, string>> | ||||
|             { | ||||
|                 new Tuple<BaseItemDto, string, string>(dto, program.ExternalId, program.ExternalSeriesId) | ||||
|             }; | ||||
| 
 | ||||
| @ -2167,20 +2167,19 @@ namespace Emby.Server.Implementations.LiveTv | ||||
|             var info = new LiveTvInfo | ||||
|             { | ||||
|                 Services = services, | ||||
|                 IsEnabled = services.Length > 0 | ||||
|                 IsEnabled = services.Length > 0, | ||||
|                 EnabledUsers = _userManager.Users | ||||
|                     .Where(IsLiveTvEnabled) | ||||
|                     .Select(i => i.Id.ToString("N", CultureInfo.InvariantCulture)) | ||||
|                     .ToArray() | ||||
|             }; | ||||
| 
 | ||||
|             info.EnabledUsers = _userManager.Users | ||||
|                 .Where(IsLiveTvEnabled) | ||||
|                 .Select(i => i.Id.ToString("N", CultureInfo.InvariantCulture)) | ||||
|                 .ToArray(); | ||||
| 
 | ||||
|             return info; | ||||
|         } | ||||
| 
 | ||||
|         private bool IsLiveTvEnabled(User user) | ||||
|         { | ||||
|             return user.Policy.EnableLiveTvAccess && (Services.Count > 1 || GetConfiguration().TunerHosts.Length > 0); | ||||
|             return user.HasPermission(PermissionKind.EnableLiveTvAccess) && (Services.Count > 1 || GetConfiguration().TunerHosts.Length > 0); | ||||
|         } | ||||
| 
 | ||||
|         public IEnumerable<User> GetEnabledUsers() | ||||
|  | ||||
| @ -3,6 +3,7 @@ | ||||
| using System.Collections.Generic; | ||||
| using System.Linq; | ||||
| using System.Text.Json.Serialization; | ||||
| using Jellyfin.Data.Entities; | ||||
| using MediaBrowser.Controller.Entities; | ||||
| using MediaBrowser.Controller.Playlists; | ||||
| using MediaBrowser.Model.Querying; | ||||
| @ -44,7 +45,7 @@ namespace Emby.Server.Implementations.Playlists | ||||
|             } | ||||
| 
 | ||||
|             query.Recursive = true; | ||||
|             query.IncludeItemTypes = new string[] { "Playlist" }; | ||||
|             query.IncludeItemTypes = new[] { "Playlist" }; | ||||
|             query.Parent = null; | ||||
|             return LibraryManager.GetItemsResult(query); | ||||
|         } | ||||
|  | ||||
| @ -7,6 +7,7 @@ using System.IO; | ||||
| using System.Linq; | ||||
| using System.Threading; | ||||
| using System.Threading.Tasks; | ||||
| using Jellyfin.Data.Entities; | ||||
| using MediaBrowser.Controller.Dto; | ||||
| using MediaBrowser.Controller.Entities; | ||||
| using MediaBrowser.Controller.Entities.Audio; | ||||
| @ -21,6 +22,8 @@ using Microsoft.Extensions.Configuration; | ||||
| using Microsoft.Extensions.Logging; | ||||
| using PlaylistsNET.Content; | ||||
| using PlaylistsNET.Models; | ||||
| using Genre = MediaBrowser.Controller.Entities.Genre; | ||||
| using MusicAlbum = MediaBrowser.Controller.Entities.Audio.MusicAlbum; | ||||
| 
 | ||||
| namespace Emby.Server.Implementations.Playlists | ||||
| { | ||||
|  | ||||
| @ -7,6 +7,8 @@ using System.Globalization; | ||||
| using System.Linq; | ||||
| using System.Threading; | ||||
| using System.Threading.Tasks; | ||||
| using Jellyfin.Data.Entities; | ||||
| using Jellyfin.Data.Enums; | ||||
| using MediaBrowser.Common.Events; | ||||
| using MediaBrowser.Common.Extensions; | ||||
| using MediaBrowser.Controller; | ||||
| @ -15,7 +17,6 @@ using MediaBrowser.Controller.Devices; | ||||
| using MediaBrowser.Controller.Drawing; | ||||
| using MediaBrowser.Controller.Dto; | ||||
| using MediaBrowser.Controller.Entities; | ||||
| using MediaBrowser.Controller.Entities.TV; | ||||
| using MediaBrowser.Controller.Library; | ||||
| using MediaBrowser.Controller.Net; | ||||
| using MediaBrowser.Controller.Security; | ||||
| @ -28,7 +29,9 @@ using MediaBrowser.Model.Library; | ||||
| using MediaBrowser.Model.Querying; | ||||
| using MediaBrowser.Model.Session; | ||||
| using MediaBrowser.Model.SyncPlay; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Microsoft.Extensions.Logging; | ||||
| using Episode = MediaBrowser.Controller.Entities.TV.Episode; | ||||
| 
 | ||||
| namespace Emby.Server.Implementations.Session | ||||
| { | ||||
| @ -283,11 +286,18 @@ namespace Emby.Server.Implementations.Session | ||||
|             if (user != null) | ||||
|             { | ||||
|                 var userLastActivityDate = user.LastActivityDate ?? DateTime.MinValue; | ||||
|                 user.LastActivityDate = activityDate; | ||||
| 
 | ||||
|                 if ((activityDate - userLastActivityDate).TotalSeconds > 60) | ||||
|                 { | ||||
|                     _userManager.UpdateUser(user); | ||||
|                     try | ||||
|                     { | ||||
|                         user.LastActivityDate = activityDate; | ||||
|                         _userManager.UpdateUser(user); | ||||
|                     } | ||||
|                     catch (DbUpdateConcurrencyException e) | ||||
|                     { | ||||
|                         _logger.LogWarning(e, "Error updating user's last activity date."); | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
| @ -434,7 +444,13 @@ namespace Emby.Server.Implementations.Session | ||||
|         /// <param name="remoteEndPoint">The remote end point.</param> | ||||
|         /// <param name="user">The user.</param> | ||||
|         /// <returns>SessionInfo.</returns> | ||||
|         private SessionInfo GetSessionInfo(string appName, string appVersion, string deviceId, string deviceName, string remoteEndPoint, User user) | ||||
|         private SessionInfo GetSessionInfo( | ||||
|             string appName, | ||||
|             string appVersion, | ||||
|             string deviceId, | ||||
|             string deviceName, | ||||
|             string remoteEndPoint, | ||||
|             User user) | ||||
|         { | ||||
|             CheckDisposed(); | ||||
| 
 | ||||
| @ -447,14 +463,13 @@ namespace Emby.Server.Implementations.Session | ||||
| 
 | ||||
|             CheckDisposed(); | ||||
| 
 | ||||
|             var sessionInfo = _activeConnections.GetOrAdd(key, k => | ||||
|             { | ||||
|                 return CreateSession(k, appName, appVersion, deviceId, deviceName, remoteEndPoint, user); | ||||
|             }); | ||||
|             var sessionInfo = _activeConnections.GetOrAdd( | ||||
|                 key, | ||||
|                 k => CreateSession(k, appName, appVersion, deviceId, deviceName, remoteEndPoint, user)); | ||||
| 
 | ||||
|             sessionInfo.UserId = user == null ? Guid.Empty : user.Id; | ||||
|             sessionInfo.UserName = user?.Name; | ||||
|             sessionInfo.UserPrimaryImageTag = user == null ? null : GetImageCacheTag(user, ImageType.Primary); | ||||
|             sessionInfo.UserId = user?.Id ?? Guid.Empty; | ||||
|             sessionInfo.UserName = user?.Username; | ||||
|             sessionInfo.UserPrimaryImageTag = user?.ProfileImage == null ? null : GetImageCacheTag(user); | ||||
|             sessionInfo.RemoteEndPoint = remoteEndPoint; | ||||
|             sessionInfo.Client = appName; | ||||
| 
 | ||||
| @ -473,7 +488,14 @@ namespace Emby.Server.Implementations.Session | ||||
|             return sessionInfo; | ||||
|         } | ||||
| 
 | ||||
|         private SessionInfo CreateSession(string key, string appName, string appVersion, string deviceId, string deviceName, string remoteEndPoint, User user) | ||||
|         private SessionInfo CreateSession( | ||||
|             string key, | ||||
|             string appName, | ||||
|             string appVersion, | ||||
|             string deviceId, | ||||
|             string deviceName, | ||||
|             string remoteEndPoint, | ||||
|             User user) | ||||
|         { | ||||
|             var sessionInfo = new SessionInfo(this, _logger) | ||||
|             { | ||||
| @ -483,11 +505,11 @@ namespace Emby.Server.Implementations.Session | ||||
|                 Id = key.GetMD5().ToString("N", CultureInfo.InvariantCulture) | ||||
|             }; | ||||
| 
 | ||||
|             var username = user?.Name; | ||||
|             var username = user?.Username; | ||||
| 
 | ||||
|             sessionInfo.UserId = user?.Id ?? Guid.Empty; | ||||
|             sessionInfo.UserName = username; | ||||
|             sessionInfo.UserPrimaryImageTag = user == null ? null : GetImageCacheTag(user, ImageType.Primary); | ||||
|             sessionInfo.UserPrimaryImageTag = user?.ProfileImage == null ? null : GetImageCacheTag(user); | ||||
|             sessionInfo.RemoteEndPoint = remoteEndPoint; | ||||
| 
 | ||||
|             if (string.IsNullOrEmpty(deviceName)) | ||||
| @ -535,10 +557,7 @@ namespace Emby.Server.Implementations.Session | ||||
| 
 | ||||
|         private void StartIdleCheckTimer() | ||||
|         { | ||||
|             if (_idleTimer == null) | ||||
|             { | ||||
|                 _idleTimer = new Timer(CheckForIdlePlayback, null, TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(5)); | ||||
|             } | ||||
|             _idleTimer ??= new Timer(CheckForIdlePlayback, null, TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(5)); | ||||
|         } | ||||
| 
 | ||||
|         private void StopIdleCheckTimer() | ||||
| @ -786,7 +805,7 @@ namespace Emby.Server.Implementations.Session | ||||
|         { | ||||
|             var changed = false; | ||||
| 
 | ||||
|             if (user.Configuration.RememberAudioSelections) | ||||
|             if (user.RememberAudioSelections) | ||||
|             { | ||||
|                 if (data.AudioStreamIndex != info.AudioStreamIndex) | ||||
|                 { | ||||
| @ -803,7 +822,7 @@ namespace Emby.Server.Implementations.Session | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             if (user.Configuration.RememberSubtitleSelections) | ||||
|             if (user.RememberSubtitleSelections) | ||||
|             { | ||||
|                 if (data.SubtitleStreamIndex != info.SubtitleStreamIndex) | ||||
|                 { | ||||
| @ -1114,13 +1133,13 @@ namespace Emby.Server.Implementations.Session | ||||
|                 if (items.Any(i => i.GetPlayAccess(user) != PlayAccess.Full)) | ||||
|                 { | ||||
|                     throw new ArgumentException( | ||||
|                         string.Format(CultureInfo.InvariantCulture, "{0} is not allowed to play media.", user.Name)); | ||||
|                         string.Format(CultureInfo.InvariantCulture, "{0} is not allowed to play media.", user.Username)); | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             if (user != null | ||||
|                 && command.ItemIds.Length == 1 | ||||
|                 && user.Configuration.EnableNextEpisodeAutoPlay | ||||
|                 && user.EnableNextEpisodeAutoPlay | ||||
|                 && _libraryManager.GetItemById(command.ItemIds[0]) is Episode episode) | ||||
|             { | ||||
|                 var series = episode.Series; | ||||
| @ -1191,7 +1210,7 @@ namespace Emby.Server.Implementations.Session | ||||
|                     DtoOptions = new DtoOptions(false) | ||||
|                     { | ||||
|                         EnableImages = false, | ||||
|                         Fields = new ItemFields[] | ||||
|                         Fields = new[] | ||||
|                         { | ||||
|                             ItemFields.SortName | ||||
|                         } | ||||
| @ -1353,7 +1372,7 @@ namespace Emby.Server.Implementations.Session | ||||
|                 list.Add(new SessionUserInfo | ||||
|                 { | ||||
|                     UserId = userId, | ||||
|                     UserName = user.Name | ||||
|                     UserName = user.Username | ||||
|                 }); | ||||
| 
 | ||||
|                 session.AdditionalUsers = list.ToArray(); | ||||
| @ -1513,7 +1532,7 @@ namespace Emby.Server.Implementations.Session | ||||
|                 DeviceName = deviceName, | ||||
|                 UserId = user.Id, | ||||
|                 AccessToken = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture), | ||||
|                 UserName = user.Name | ||||
|                 UserName = user.Username | ||||
|             }; | ||||
| 
 | ||||
|             _logger.LogInformation("Creating new access token for user {0}", user.Id); | ||||
| @ -1710,15 +1729,15 @@ namespace Emby.Server.Implementations.Session | ||||
|             return info; | ||||
|         } | ||||
| 
 | ||||
|         private string GetImageCacheTag(BaseItem item, ImageType type) | ||||
|         private string GetImageCacheTag(User user) | ||||
|         { | ||||
|             try | ||||
|             { | ||||
|                 return _imageProcessor.GetImageCacheTag(item, type); | ||||
|                 return _imageProcessor.GetImageCacheTag(user); | ||||
|             } | ||||
|             catch (Exception ex) | ||||
|             catch (Exception e) | ||||
|             { | ||||
|                 _logger.LogError(ex, "Error getting image information for {Type}", type); | ||||
|                 _logger.LogError(e, "Error getting image information for profile image"); | ||||
|                 return null; | ||||
|             } | ||||
|         } | ||||
| @ -1827,7 +1846,10 @@ namespace Emby.Server.Implementations.Session | ||||
|         { | ||||
|             CheckDisposed(); | ||||
| 
 | ||||
|             var adminUserIds = _userManager.Users.Where(i => i.Policy.IsAdministrator).Select(i => i.Id).ToList(); | ||||
|             var adminUserIds = _userManager.Users | ||||
|                 .Where(i => i.HasPermission(PermissionKind.IsAdministrator)) | ||||
|                 .Select(i => i.Id) | ||||
|                 .ToList(); | ||||
| 
 | ||||
|             return SendMessageToUserSessions(adminUserIds, name, data, cancellationToken); | ||||
|         } | ||||
|  | ||||
| @ -1,6 +1,7 @@ | ||||
| #pragma warning disable CS1591 | ||||
| 
 | ||||
| using System; | ||||
| using Jellyfin.Data.Entities; | ||||
| using MediaBrowser.Controller.Entities; | ||||
| using MediaBrowser.Controller.Library; | ||||
| using MediaBrowser.Controller.Sorting; | ||||
|  | ||||
| @ -1,4 +1,5 @@ | ||||
| using System; | ||||
| using Jellyfin.Data.Entities; | ||||
| using MediaBrowser.Controller.Entities; | ||||
| using MediaBrowser.Controller.Library; | ||||
| using MediaBrowser.Controller.Sorting; | ||||
|  | ||||
| @ -1,5 +1,6 @@ | ||||
| #pragma warning disable CS1591 | ||||
| 
 | ||||
| using Jellyfin.Data.Entities; | ||||
| using MediaBrowser.Controller.Entities; | ||||
| using MediaBrowser.Controller.Library; | ||||
| using MediaBrowser.Controller.Sorting; | ||||
|  | ||||
| @ -1,5 +1,6 @@ | ||||
| #pragma warning disable CS1591 | ||||
| 
 | ||||
| using Jellyfin.Data.Entities; | ||||
| using MediaBrowser.Controller.Entities; | ||||
| using MediaBrowser.Controller.Library; | ||||
| using MediaBrowser.Controller.Sorting; | ||||
|  | ||||
| @ -1,5 +1,6 @@ | ||||
| #pragma warning disable CS1591 | ||||
| 
 | ||||
| using Jellyfin.Data.Entities; | ||||
| using MediaBrowser.Controller.Entities; | ||||
| using MediaBrowser.Controller.Library; | ||||
| using MediaBrowser.Controller.Sorting; | ||||
|  | ||||
| @ -1,3 +1,4 @@ | ||||
| using Jellyfin.Data.Entities; | ||||
| using MediaBrowser.Controller.Entities; | ||||
| using MediaBrowser.Controller.Library; | ||||
| using MediaBrowser.Controller.Sorting; | ||||
|  | ||||
| @ -3,13 +3,13 @@ using System.Collections.Generic; | ||||
| using System.Globalization; | ||||
| using System.Linq; | ||||
| using System.Threading; | ||||
| using Microsoft.Extensions.Logging; | ||||
| using MediaBrowser.Controller.Entities; | ||||
| using Jellyfin.Data.Entities; | ||||
| using Jellyfin.Data.Enums; | ||||
| using MediaBrowser.Controller.Library; | ||||
| using MediaBrowser.Controller.Session; | ||||
| using MediaBrowser.Controller.SyncPlay; | ||||
| using MediaBrowser.Model.Configuration; | ||||
| using MediaBrowser.Model.SyncPlay; | ||||
| using Microsoft.Extensions.Logging; | ||||
| 
 | ||||
| namespace Emby.Server.Implementations.SyncPlay | ||||
| { | ||||
| @ -109,14 +109,6 @@ namespace Emby.Server.Implementations.SyncPlay | ||||
|             _disposed = true; | ||||
|         } | ||||
| 
 | ||||
|         private void CheckDisposed() | ||||
|         { | ||||
|             if (_disposed) | ||||
|             { | ||||
|                 throw new ObjectDisposedException(GetType().Name); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         private void OnSessionManagerSessionEnded(object sender, SessionEventArgs e) | ||||
|         { | ||||
|             var session = e.SessionInfo; | ||||
| @ -149,38 +141,24 @@ namespace Emby.Server.Implementations.SyncPlay | ||||
|             var item = _libraryManager.GetItemById(itemId); | ||||
| 
 | ||||
|             // Check ParentalRating access | ||||
|             var hasParentalRatingAccess = true; | ||||
|             if (user.Policy.MaxParentalRating.HasValue) | ||||
|             { | ||||
|                 hasParentalRatingAccess = item.InheritedParentalRatingValue <= user.Policy.MaxParentalRating; | ||||
|             } | ||||
|             var hasParentalRatingAccess = !user.MaxParentalAgeRating.HasValue | ||||
|                 || item.InheritedParentalRatingValue <= user.MaxParentalAgeRating; | ||||
| 
 | ||||
|             if (!user.Policy.EnableAllFolders && hasParentalRatingAccess) | ||||
|             if (!user.HasPermission(PermissionKind.EnableAllFolders) && hasParentalRatingAccess) | ||||
|             { | ||||
|                 var collections = _libraryManager.GetCollectionFolders(item).Select( | ||||
|                     folder => folder.Id.ToString("N", CultureInfo.InvariantCulture) | ||||
|                 ); | ||||
|                 var intersect = collections.Intersect(user.Policy.EnabledFolders); | ||||
|                 return intersect.Any(); | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 return hasParentalRatingAccess; | ||||
|                     folder => folder.Id.ToString("N", CultureInfo.InvariantCulture)); | ||||
| 
 | ||||
|                 return collections.Intersect(user.GetPreference(PreferenceKind.EnabledFolders)).Any(); | ||||
|             } | ||||
| 
 | ||||
|             return hasParentalRatingAccess; | ||||
|         } | ||||
| 
 | ||||
|         private Guid? GetSessionGroup(SessionInfo session) | ||||
|         { | ||||
|             ISyncPlayController group; | ||||
|             _sessionToGroupMap.TryGetValue(session.Id, out group); | ||||
|             if (group != null) | ||||
|             { | ||||
|                 return group.GetGroupId(); | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 return null; | ||||
|             } | ||||
|             _sessionToGroupMap.TryGetValue(session.Id, out var group); | ||||
|             return group?.GetGroupId(); | ||||
|         } | ||||
| 
 | ||||
|         /// <inheritdoc /> | ||||
| @ -188,7 +166,7 @@ namespace Emby.Server.Implementations.SyncPlay | ||||
|         { | ||||
|             var user = _userManager.GetUserById(session.UserId); | ||||
| 
 | ||||
|             if (user.Policy.SyncPlayAccess != SyncPlayAccess.CreateAndJoinGroups) | ||||
|             if (user.SyncPlayAccess != SyncPlayAccess.CreateAndJoinGroups) | ||||
|             { | ||||
|                 _logger.LogWarning("NewGroup: {0} does not have permission to create groups.", session.Id); | ||||
| 
 | ||||
| @ -196,7 +174,7 @@ namespace Emby.Server.Implementations.SyncPlay | ||||
|                 { | ||||
|                     Type = GroupUpdateType.CreateGroupDenied | ||||
|                 }; | ||||
|                 _sessionManager.SendSyncPlayGroupUpdate(session.Id.ToString(), error, CancellationToken.None); | ||||
|                 _sessionManager.SendSyncPlayGroupUpdate(session.Id, error, CancellationToken.None); | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
| @ -219,7 +197,7 @@ namespace Emby.Server.Implementations.SyncPlay | ||||
|         { | ||||
|             var user = _userManager.GetUserById(session.UserId); | ||||
| 
 | ||||
|             if (user.Policy.SyncPlayAccess == SyncPlayAccess.None) | ||||
|             if (user.SyncPlayAccess == SyncPlayAccess.None) | ||||
|             { | ||||
|                 _logger.LogWarning("JoinGroup: {0} does not have access to SyncPlay.", session.Id); | ||||
| 
 | ||||
| @ -227,7 +205,7 @@ namespace Emby.Server.Implementations.SyncPlay | ||||
|                 { | ||||
|                     Type = GroupUpdateType.JoinGroupDenied | ||||
|                 }; | ||||
|                 _sessionManager.SendSyncPlayGroupUpdate(session.Id.ToString(), error, CancellationToken.None); | ||||
|                 _sessionManager.SendSyncPlayGroupUpdate(session.Id, error, CancellationToken.None); | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
| @ -244,7 +222,7 @@ namespace Emby.Server.Implementations.SyncPlay | ||||
|                     { | ||||
|                         Type = GroupUpdateType.GroupDoesNotExist | ||||
|                     }; | ||||
|                     _sessionManager.SendSyncPlayGroupUpdate(session.Id.ToString(), error, CancellationToken.None); | ||||
|                     _sessionManager.SendSyncPlayGroupUpdate(session.Id, error, CancellationToken.None); | ||||
|                     return; | ||||
|                 } | ||||
| 
 | ||||
| @ -257,7 +235,7 @@ namespace Emby.Server.Implementations.SyncPlay | ||||
|                         GroupId = group.GetGroupId().ToString(), | ||||
|                         Type = GroupUpdateType.LibraryAccessDenied | ||||
|                     }; | ||||
|                     _sessionManager.SendSyncPlayGroupUpdate(session.Id.ToString(), error, CancellationToken.None); | ||||
|                     _sessionManager.SendSyncPlayGroupUpdate(session.Id, error, CancellationToken.None); | ||||
|                     return; | ||||
|                 } | ||||
| 
 | ||||
| @ -281,8 +259,7 @@ namespace Emby.Server.Implementations.SyncPlay | ||||
|             // TODO: determine what happens to users that are in a group and get their permissions revoked | ||||
|             lock (_groupsLock) | ||||
|             { | ||||
|                 ISyncPlayController group; | ||||
|                 _sessionToGroupMap.TryGetValue(session.Id, out group); | ||||
|                 _sessionToGroupMap.TryGetValue(session.Id, out var group); | ||||
| 
 | ||||
|                 if (group == null) | ||||
|                 { | ||||
| @ -292,7 +269,7 @@ namespace Emby.Server.Implementations.SyncPlay | ||||
|                     { | ||||
|                         Type = GroupUpdateType.NotInGroup | ||||
|                     }; | ||||
|                     _sessionManager.SendSyncPlayGroupUpdate(session.Id.ToString(), error, CancellationToken.None); | ||||
|                     _sessionManager.SendSyncPlayGroupUpdate(session.Id, error, CancellationToken.None); | ||||
|                     return; | ||||
|                 } | ||||
| 
 | ||||
| @ -311,7 +288,7 @@ namespace Emby.Server.Implementations.SyncPlay | ||||
|         { | ||||
|             var user = _userManager.GetUserById(session.UserId); | ||||
| 
 | ||||
|             if (user.Policy.SyncPlayAccess == SyncPlayAccess.None) | ||||
|             if (user.SyncPlayAccess == SyncPlayAccess.None) | ||||
|             { | ||||
|                 return new List<GroupInfoView>(); | ||||
|             } | ||||
| @ -341,7 +318,7 @@ namespace Emby.Server.Implementations.SyncPlay | ||||
|         { | ||||
|             var user = _userManager.GetUserById(session.UserId); | ||||
| 
 | ||||
|             if (user.Policy.SyncPlayAccess == SyncPlayAccess.None) | ||||
|             if (user.SyncPlayAccess == SyncPlayAccess.None) | ||||
|             { | ||||
|                 _logger.LogWarning("HandleRequest: {0} does not have access to SyncPlay.", session.Id); | ||||
| 
 | ||||
| @ -349,14 +326,13 @@ namespace Emby.Server.Implementations.SyncPlay | ||||
|                 { | ||||
|                     Type = GroupUpdateType.JoinGroupDenied | ||||
|                 }; | ||||
|                 _sessionManager.SendSyncPlayGroupUpdate(session.Id.ToString(), error, CancellationToken.None); | ||||
|                 _sessionManager.SendSyncPlayGroupUpdate(session.Id, error, CancellationToken.None); | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             lock (_groupsLock) | ||||
|             { | ||||
|                 ISyncPlayController group; | ||||
|                 _sessionToGroupMap.TryGetValue(session.Id, out group); | ||||
|                 _sessionToGroupMap.TryGetValue(session.Id, out var group); | ||||
| 
 | ||||
|                 if (group == null) | ||||
|                 { | ||||
| @ -366,7 +342,7 @@ namespace Emby.Server.Implementations.SyncPlay | ||||
|                     { | ||||
|                         Type = GroupUpdateType.NotInGroup | ||||
|                     }; | ||||
|                     _sessionManager.SendSyncPlayGroupUpdate(session.Id.ToString(), error, CancellationToken.None); | ||||
|                     _sessionManager.SendSyncPlayGroupUpdate(session.Id, error, CancellationToken.None); | ||||
|                     return; | ||||
|                 } | ||||
| 
 | ||||
| @ -393,8 +369,7 @@ namespace Emby.Server.Implementations.SyncPlay | ||||
|                 throw new InvalidOperationException("Session not in any group!"); | ||||
|             } | ||||
| 
 | ||||
|             ISyncPlayController tempGroup; | ||||
|             _sessionToGroupMap.Remove(session.Id, out tempGroup); | ||||
|             _sessionToGroupMap.Remove(session.Id, out var tempGroup); | ||||
| 
 | ||||
|             if (!tempGroup.GetGroupId().Equals(group.GetGroupId())) | ||||
|             { | ||||
|  | ||||
| @ -4,13 +4,17 @@ using System; | ||||
| using System.Collections.Generic; | ||||
| using System.Globalization; | ||||
| using System.Linq; | ||||
| using Jellyfin.Data.Entities; | ||||
| using Jellyfin.Data.Enums; | ||||
| using MediaBrowser.Controller.Configuration; | ||||
| using MediaBrowser.Controller.Dto; | ||||
| using MediaBrowser.Controller.Entities; | ||||
| using MediaBrowser.Controller.Entities.TV; | ||||
| using MediaBrowser.Controller.Library; | ||||
| using MediaBrowser.Controller.TV; | ||||
| using MediaBrowser.Model.Entities; | ||||
| using MediaBrowser.Model.Querying; | ||||
| using Episode = MediaBrowser.Controller.Entities.TV.Episode; | ||||
| using Series = MediaBrowser.Controller.Entities.TV.Series; | ||||
| 
 | ||||
| namespace Emby.Server.Implementations.TV | ||||
| { | ||||
| @ -73,7 +77,8 @@ namespace Emby.Server.Implementations.TV | ||||
|             { | ||||
|                 parents = _libraryManager.GetUserRootFolder().GetChildren(user, true) | ||||
|                    .Where(i => i is Folder) | ||||
|                    .Where(i => !user.Configuration.LatestItemsExcludes.Contains(i.Id.ToString("N", CultureInfo.InvariantCulture))) | ||||
|                    .Where(i => !user.GetPreference(PreferenceKind.LatestItemExcludes) | ||||
|                        .Contains(i.Id.ToString("N", CultureInfo.InvariantCulture))) | ||||
|                    .ToArray(); | ||||
|             } | ||||
| 
 | ||||
| @ -191,7 +196,7 @@ namespace Emby.Server.Implementations.TV | ||||
|             { | ||||
|                 AncestorWithPresentationUniqueKey = null, | ||||
|                 SeriesPresentationUniqueKey = seriesKey, | ||||
|                 IncludeItemTypes = new[] { typeof(Episode).Name }, | ||||
|                 IncludeItemTypes = new[] { nameof(Episode) }, | ||||
|                 OrderBy = new[] { new ValueTuple<string, SortOrder>(ItemSortBy.SortName, SortOrder.Descending) }, | ||||
|                 IsPlayed = true, | ||||
|                 Limit = 1, | ||||
| @ -204,7 +209,6 @@ namespace Emby.Server.Implementations.TV | ||||
|                     }, | ||||
|                     EnableImages = false | ||||
|                 } | ||||
| 
 | ||||
|             }).FirstOrDefault(); | ||||
| 
 | ||||
|             Func<Episode> getEpisode = () => | ||||
| @ -219,7 +223,7 @@ namespace Emby.Server.Implementations.TV | ||||
|                     IsPlayed = false, | ||||
|                     IsVirtualItem = false, | ||||
|                     ParentIndexNumberNotEquals = 0, | ||||
|                     MinSortName = lastWatchedEpisode == null ? null : lastWatchedEpisode.SortName, | ||||
|                     MinSortName = lastWatchedEpisode?.SortName, | ||||
|                     DtoOptions = dtoOptions | ||||
| 
 | ||||
|                 }).Cast<Episode>().FirstOrDefault(); | ||||
|  | ||||
							
								
								
									
										100
									
								
								Jellyfin.Api/Auth/BaseAuthorizationHandler.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										100
									
								
								Jellyfin.Api/Auth/BaseAuthorizationHandler.cs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,100 @@ | ||||
| using System.Net; | ||||
| using System.Security.Claims; | ||||
| using Jellyfin.Api.Helpers; | ||||
| using Jellyfin.Data.Enums; | ||||
| using MediaBrowser.Common.Net; | ||||
| using MediaBrowser.Controller.Library; | ||||
| using Microsoft.AspNetCore.Authorization; | ||||
| using Microsoft.AspNetCore.Http; | ||||
| 
 | ||||
| namespace Jellyfin.Api.Auth | ||||
| { | ||||
|     /// <summary> | ||||
|     /// Base authorization handler. | ||||
|     /// </summary> | ||||
|     /// <typeparam name="T">Type of Authorization Requirement.</typeparam> | ||||
|     public abstract class BaseAuthorizationHandler<T> : AuthorizationHandler<T> | ||||
|         where T : IAuthorizationRequirement | ||||
|     { | ||||
|         private readonly IUserManager _userManager; | ||||
|         private readonly INetworkManager _networkManager; | ||||
|         private readonly IHttpContextAccessor _httpContextAccessor; | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="BaseAuthorizationHandler{T}"/> class. | ||||
|         /// </summary> | ||||
|         /// <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> | ||||
|         protected BaseAuthorizationHandler( | ||||
|             IUserManager userManager, | ||||
|             INetworkManager networkManager, | ||||
|             IHttpContextAccessor httpContextAccessor) | ||||
|         { | ||||
|             _userManager = userManager; | ||||
|             _networkManager = networkManager; | ||||
|             _httpContextAccessor = httpContextAccessor; | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Validate authenticated claims. | ||||
|         /// </summary> | ||||
|         /// <param name="claimsPrincipal">Request claims.</param> | ||||
|         /// <param name="ignoreSchedule">Whether to ignore parental control.</param> | ||||
|         /// <param name="localAccessOnly">Whether access is to be allowed locally only.</param> | ||||
|         /// <returns>Validated claim status.</returns> | ||||
|         protected bool ValidateClaims( | ||||
|             ClaimsPrincipal claimsPrincipal, | ||||
|             bool ignoreSchedule = false, | ||||
|             bool localAccessOnly = false) | ||||
|         { | ||||
|             // Ensure claim has userId. | ||||
|             var userId = ClaimHelpers.GetUserId(claimsPrincipal); | ||||
|             if (userId == null) | ||||
|             { | ||||
|                 return false; | ||||
|             } | ||||
| 
 | ||||
|             // Ensure userId links to a valid user. | ||||
|             var user = _userManager.GetUserById(userId.Value); | ||||
|             if (user == null) | ||||
|             { | ||||
|                 return false; | ||||
|             } | ||||
| 
 | ||||
|             // Ensure user is not disabled. | ||||
|             if (user.HasPermission(PermissionKind.IsDisabled)) | ||||
|             { | ||||
|                 return false; | ||||
|             } | ||||
| 
 | ||||
|             var ip = NormalizeIp(_httpContextAccessor.HttpContext.Connection.RemoteIpAddress).ToString(); | ||||
|             var isInLocalNetwork = _networkManager.IsInLocalNetwork(ip); | ||||
|             // User cannot access remotely and user is remote | ||||
|             if (!user.HasPermission(PermissionKind.EnableRemoteAccess) && !isInLocalNetwork) | ||||
|             { | ||||
|                 return false; | ||||
|             } | ||||
| 
 | ||||
|             if (localAccessOnly && !isInLocalNetwork) | ||||
|             { | ||||
|                 return false; | ||||
|             } | ||||
| 
 | ||||
|             // User attempting to access out of parental control hours. | ||||
|             if (!ignoreSchedule | ||||
|                 && !user.HasPermission(PermissionKind.IsAdministrator) | ||||
|                 && !user.IsParentalScheduleAllowed()) | ||||
|             { | ||||
|                 return false; | ||||
|             } | ||||
| 
 | ||||
|             return true; | ||||
|         } | ||||
| 
 | ||||
|         private static IPAddress NormalizeIp(IPAddress ip) | ||||
|         { | ||||
|             return ip.IsIPv4MappedToIPv6 ? ip.MapToIPv4() : ip; | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -1,8 +1,10 @@ | ||||
| using System.Globalization; | ||||
| using System.Security.Authentication; | ||||
| using System.Security.Claims; | ||||
| using System.Text.Encodings.Web; | ||||
| using System.Threading.Tasks; | ||||
| using Jellyfin.Api.Constants; | ||||
| using Jellyfin.Data.Enums; | ||||
| using MediaBrowser.Controller.Net; | ||||
| using Microsoft.AspNetCore.Authentication; | ||||
| using Microsoft.Extensions.Logging; | ||||
| @ -38,15 +40,10 @@ namespace Jellyfin.Api.Auth | ||||
|         /// <inheritdoc /> | ||||
|         protected override Task<AuthenticateResult> HandleAuthenticateAsync() | ||||
|         { | ||||
|             var authenticatedAttribute = new AuthenticatedAttribute | ||||
|             { | ||||
|                 IgnoreLegacyAuth = true | ||||
|             }; | ||||
| 
 | ||||
|             try | ||||
|             { | ||||
|                 var user = _authService.Authenticate(Request, authenticatedAttribute); | ||||
|                 if (user == null) | ||||
|                 var authorizationInfo = _authService.Authenticate(Request); | ||||
|                 if (authorizationInfo == null) | ||||
|                 { | ||||
|                     return Task.FromResult(AuthenticateResult.NoResult()); | ||||
|                     // TODO return when legacy API is removed. | ||||
| @ -56,11 +53,16 @@ namespace Jellyfin.Api.Auth | ||||
| 
 | ||||
|                 var claims = new[] | ||||
|                 { | ||||
|                     new Claim(ClaimTypes.Name, user.Name), | ||||
|                     new Claim( | ||||
|                         ClaimTypes.Role, | ||||
|                         value: user.Policy.IsAdministrator ? UserRoles.Administrator : UserRoles.User) | ||||
|                     new Claim(ClaimTypes.Name, authorizationInfo.User.Username), | ||||
|                     new Claim(ClaimTypes.Role, value: authorizationInfo.User.HasPermission(PermissionKind.IsAdministrator) ? UserRoles.Administrator : UserRoles.User), | ||||
|                     new Claim(InternalClaimTypes.UserId, authorizationInfo.UserId.ToString("N", CultureInfo.InvariantCulture)), | ||||
|                     new Claim(InternalClaimTypes.DeviceId, authorizationInfo.DeviceId), | ||||
|                     new Claim(InternalClaimTypes.Device, authorizationInfo.Device), | ||||
|                     new Claim(InternalClaimTypes.Client, authorizationInfo.Client), | ||||
|                     new Claim(InternalClaimTypes.Version, authorizationInfo.Version), | ||||
|                     new Claim(InternalClaimTypes.Token, authorizationInfo.Token), | ||||
|                 }; | ||||
| 
 | ||||
|                 var identity = new ClaimsIdentity(claims, Scheme.Name); | ||||
|                 var principal = new ClaimsPrincipal(identity); | ||||
|                 var ticket = new AuthenticationTicket(principal, Scheme.Name); | ||||
|  | ||||
| @ -0,0 +1,42 @@ | ||||
| using System.Threading.Tasks; | ||||
| using MediaBrowser.Common.Net; | ||||
| using MediaBrowser.Controller.Library; | ||||
| using Microsoft.AspNetCore.Authorization; | ||||
| using Microsoft.AspNetCore.Http; | ||||
| 
 | ||||
| namespace Jellyfin.Api.Auth.DefaultAuthorizationPolicy | ||||
| { | ||||
|     /// <summary> | ||||
|     /// Default authorization handler. | ||||
|     /// </summary> | ||||
|     public class DefaultAuthorizationHandler : BaseAuthorizationHandler<DefaultAuthorizationRequirement> | ||||
|     { | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="DefaultAuthorizationHandler"/> class. | ||||
|         /// </summary> | ||||
|         /// <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 DefaultAuthorizationHandler( | ||||
|             IUserManager userManager, | ||||
|             INetworkManager networkManager, | ||||
|             IHttpContextAccessor httpContextAccessor) | ||||
|             : base(userManager, networkManager, httpContextAccessor) | ||||
|         { | ||||
|         } | ||||
| 
 | ||||
|         /// <inheritdoc /> | ||||
|         protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, DefaultAuthorizationRequirement requirement) | ||||
|         { | ||||
|             var validated = ValidateClaims(context.User); | ||||
|             if (!validated) | ||||
|             { | ||||
|                 context.Fail(); | ||||
|                 return Task.CompletedTask; | ||||
|             } | ||||
| 
 | ||||
|             context.Succeed(requirement); | ||||
|             return Task.CompletedTask; | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,11 @@ | ||||
| using Microsoft.AspNetCore.Authorization; | ||||
| 
 | ||||
| namespace Jellyfin.Api.Auth.DefaultAuthorizationPolicy | ||||
| { | ||||
|     /// <summary> | ||||
|     /// The default authorization requirement. | ||||
|     /// </summary> | ||||
|     public class DefaultAuthorizationRequirement : IAuthorizationRequirement | ||||
|     { | ||||
|     } | ||||
| } | ||||
| @ -1,22 +1,33 @@ | ||||
| using System.Threading.Tasks; | ||||
| using Jellyfin.Api.Constants; | ||||
| using MediaBrowser.Common.Configuration; | ||||
| using MediaBrowser.Common.Net; | ||||
| using MediaBrowser.Controller.Library; | ||||
| using Microsoft.AspNetCore.Authorization; | ||||
| using Microsoft.AspNetCore.Http; | ||||
| 
 | ||||
| namespace Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy | ||||
| { | ||||
|     /// <summary> | ||||
|     /// Authorization handler for requiring first time setup or elevated privileges. | ||||
|     /// </summary> | ||||
|     public class FirstTimeSetupOrElevatedHandler : AuthorizationHandler<FirstTimeSetupOrElevatedRequirement> | ||||
|     public class FirstTimeSetupOrElevatedHandler : BaseAuthorizationHandler<FirstTimeSetupOrElevatedRequirement> | ||||
|     { | ||||
|         private readonly IConfigurationManager _configurationManager; | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="FirstTimeSetupOrElevatedHandler" /> class. | ||||
|         /// </summary> | ||||
|         /// <param name="configurationManager">The jellyfin configuration manager.</param> | ||||
|         public FirstTimeSetupOrElevatedHandler(IConfigurationManager configurationManager) | ||||
|         /// <param name="configurationManager">Instance of the <see cref="IConfigurationManager"/> 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 FirstTimeSetupOrElevatedHandler( | ||||
|             IConfigurationManager configurationManager, | ||||
|             IUserManager userManager, | ||||
|             INetworkManager networkManager, | ||||
|             IHttpContextAccessor httpContextAccessor) | ||||
|             : base(userManager, networkManager, httpContextAccessor) | ||||
|         { | ||||
|             _configurationManager = configurationManager; | ||||
|         } | ||||
| @ -27,8 +38,11 @@ namespace Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy | ||||
|             if (!_configurationManager.CommonConfiguration.IsStartupWizardCompleted) | ||||
|             { | ||||
|                 context.Succeed(firstTimeSetupOrElevatedRequirement); | ||||
|                 return Task.CompletedTask; | ||||
|             } | ||||
|             else if (context.User.IsInRole(UserRoles.Administrator)) | ||||
| 
 | ||||
|             var validated = ValidateClaims(context.User); | ||||
|             if (validated && context.User.IsInRole(UserRoles.Administrator)) | ||||
|             { | ||||
|                 context.Succeed(firstTimeSetupOrElevatedRequirement); | ||||
|             } | ||||
|  | ||||
| @ -0,0 +1,42 @@ | ||||
| using System.Threading.Tasks; | ||||
| using MediaBrowser.Common.Net; | ||||
| using MediaBrowser.Controller.Library; | ||||
| using Microsoft.AspNetCore.Authorization; | ||||
| using Microsoft.AspNetCore.Http; | ||||
| 
 | ||||
| namespace Jellyfin.Api.Auth.IgnoreSchedulePolicy | ||||
| { | ||||
|     /// <summary> | ||||
|     /// Escape schedule controls handler. | ||||
|     /// </summary> | ||||
|     public class IgnoreScheduleHandler : BaseAuthorizationHandler<IgnoreScheduleRequirement> | ||||
|     { | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="IgnoreScheduleHandler"/> class. | ||||
|         /// </summary> | ||||
|         /// <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 IgnoreScheduleHandler( | ||||
|             IUserManager userManager, | ||||
|             INetworkManager networkManager, | ||||
|             IHttpContextAccessor httpContextAccessor) | ||||
|             : base(userManager, networkManager, httpContextAccessor) | ||||
|         { | ||||
|         } | ||||
| 
 | ||||
|         /// <inheritdoc /> | ||||
|         protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, IgnoreScheduleRequirement requirement) | ||||
|         { | ||||
|             var validated = ValidateClaims(context.User, ignoreSchedule: true); | ||||
|             if (!validated) | ||||
|             { | ||||
|                 context.Fail(); | ||||
|                 return Task.CompletedTask; | ||||
|             } | ||||
| 
 | ||||
|             context.Succeed(requirement); | ||||
|             return Task.CompletedTask; | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,11 @@ | ||||
| using Microsoft.AspNetCore.Authorization; | ||||
| 
 | ||||
| namespace Jellyfin.Api.Auth.IgnoreSchedulePolicy | ||||
| { | ||||
|     /// <summary> | ||||
|     /// Escape schedule controls requirement. | ||||
|     /// </summary> | ||||
|     public class IgnoreScheduleRequirement : IAuthorizationRequirement | ||||
|     { | ||||
|     } | ||||
| } | ||||
							
								
								
									
										44
									
								
								Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessHandler.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessHandler.cs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,44 @@ | ||||
| using System.Threading.Tasks; | ||||
| using MediaBrowser.Common.Net; | ||||
| using MediaBrowser.Controller.Library; | ||||
| using Microsoft.AspNetCore.Authorization; | ||||
| using Microsoft.AspNetCore.Http; | ||||
| 
 | ||||
| namespace Jellyfin.Api.Auth.LocalAccessPolicy | ||||
| { | ||||
|     /// <summary> | ||||
|     /// Local access handler. | ||||
|     /// </summary> | ||||
|     public class LocalAccessHandler : BaseAuthorizationHandler<LocalAccessRequirement> | ||||
|     { | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="LocalAccessHandler"/> class. | ||||
|         /// </summary> | ||||
|         /// <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 LocalAccessHandler( | ||||
|             IUserManager userManager, | ||||
|             INetworkManager networkManager, | ||||
|             IHttpContextAccessor httpContextAccessor) | ||||
|             : base(userManager, networkManager, httpContextAccessor) | ||||
|         { | ||||
|         } | ||||
| 
 | ||||
|         /// <inheritdoc /> | ||||
|         protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, LocalAccessRequirement requirement) | ||||
|         { | ||||
|             var validated = ValidateClaims(context.User, localAccessOnly: true); | ||||
|             if (!validated) | ||||
|             { | ||||
|                 context.Fail(); | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 context.Succeed(requirement); | ||||
|             } | ||||
| 
 | ||||
|             return Task.CompletedTask; | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,11 @@ | ||||
| using Microsoft.AspNetCore.Authorization; | ||||
| 
 | ||||
| namespace Jellyfin.Api.Auth.LocalAccessPolicy | ||||
| { | ||||
|     /// <summary> | ||||
|     /// The local access authorization requirement. | ||||
|     /// </summary> | ||||
|     public class LocalAccessRequirement : IAuthorizationRequirement | ||||
|     { | ||||
|     } | ||||
| } | ||||
| @ -1,21 +1,43 @@ | ||||
| using System.Threading.Tasks; | ||||
| using Jellyfin.Api.Constants; | ||||
| using MediaBrowser.Common.Net; | ||||
| using MediaBrowser.Controller.Library; | ||||
| using Microsoft.AspNetCore.Authorization; | ||||
| using Microsoft.AspNetCore.Http; | ||||
| 
 | ||||
| namespace Jellyfin.Api.Auth.RequiresElevationPolicy | ||||
| { | ||||
|     /// <summary> | ||||
|     /// Authorization handler for requiring elevated privileges. | ||||
|     /// </summary> | ||||
|     public class RequiresElevationHandler : AuthorizationHandler<RequiresElevationRequirement> | ||||
|     public class RequiresElevationHandler : BaseAuthorizationHandler<RequiresElevationRequirement> | ||||
|     { | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="RequiresElevationHandler"/> class. | ||||
|         /// </summary> | ||||
|         /// <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 RequiresElevationHandler( | ||||
|             IUserManager userManager, | ||||
|             INetworkManager networkManager, | ||||
|             IHttpContextAccessor httpContextAccessor) | ||||
|             : base(userManager, networkManager, httpContextAccessor) | ||||
|         { | ||||
|         } | ||||
| 
 | ||||
|         /// <inheritdoc /> | ||||
|         protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, RequiresElevationRequirement requirement) | ||||
|         { | ||||
|             if (context.User.IsInRole(UserRoles.Administrator)) | ||||
|             var validated = ValidateClaims(context.User); | ||||
|             if (validated && context.User.IsInRole(UserRoles.Administrator)) | ||||
|             { | ||||
|                 context.Succeed(requirement); | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 context.Fail(); | ||||
|             } | ||||
| 
 | ||||
|             return Task.CompletedTask; | ||||
|         } | ||||
|  | ||||
							
								
								
									
										38
									
								
								Jellyfin.Api/Constants/InternalClaimTypes.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								Jellyfin.Api/Constants/InternalClaimTypes.cs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,38 @@ | ||||
| namespace Jellyfin.Api.Constants | ||||
| { | ||||
|     /// <summary> | ||||
|     /// Internal claim types for authorization. | ||||
|     /// </summary> | ||||
|     public static class InternalClaimTypes | ||||
|     { | ||||
|         /// <summary> | ||||
|         /// User Id. | ||||
|         /// </summary> | ||||
|         public const string UserId = "Jellyfin-UserId"; | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Device Id. | ||||
|         /// </summary> | ||||
|         public const string DeviceId = "Jellyfin-DeviceId"; | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Device. | ||||
|         /// </summary> | ||||
|         public const string Device = "Jellyfin-Device"; | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Client. | ||||
|         /// </summary> | ||||
|         public const string Client = "Jellyfin-Client"; | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Version. | ||||
|         /// </summary> | ||||
|         public const string Version = "Jellyfin-Version"; | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Token. | ||||
|         /// </summary> | ||||
|         public const string Token = "Jellyfin-Token"; | ||||
|     } | ||||
| } | ||||
| @ -5,6 +5,11 @@ namespace Jellyfin.Api.Constants | ||||
|     /// </summary> | ||||
|     public static class Policies | ||||
|     { | ||||
|         /// <summary> | ||||
|         /// Policy name for default authorization. | ||||
|         /// </summary> | ||||
|         public const string DefaultAuthorization = "DefaultAuthorization"; | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Policy name for requiring first time setup or elevated privileges. | ||||
|         /// </summary> | ||||
| @ -14,5 +19,15 @@ namespace Jellyfin.Api.Constants | ||||
|         /// Policy name for requiring elevated privileges. | ||||
|         /// </summary> | ||||
|         public const string RequiresElevation = "RequiresElevation"; | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Policy name for allowing local access only. | ||||
|         /// </summary> | ||||
|         public const string LocalAccessOnly = "LocalAccessOnly"; | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Policy name for escaping schedule controls. | ||||
|         /// </summary> | ||||
|         public const string IgnoreSchedule = "IgnoreSchedule"; | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -1,7 +1,5 @@ | ||||
| #nullable enable | ||||
| #pragma warning disable CA1801 | ||||
| 
 | ||||
| using System; | ||||
| using System.Diagnostics.CodeAnalysis; | ||||
| using System.Linq; | ||||
| using Jellyfin.Api.Constants; | ||||
| using Jellyfin.Data.Entities; | ||||
| @ -42,6 +40,7 @@ namespace Jellyfin.Api.Controllers | ||||
|         /// <returns>A <see cref="QueryResult{ActivityLogEntry}"/> containing the log entries.</returns> | ||||
|         [HttpGet("Entries")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "hasUserId", Justification = "Imported from ServiceStack")] | ||||
|         public ActionResult<QueryResult<ActivityLogEntry>> GetLogEntries( | ||||
|             [FromQuery] int? startIndex, | ||||
|             [FromQuery] int? limit, | ||||
|  | ||||
							
								
								
									
										57
									
								
								Jellyfin.Api/Controllers/BrandingController.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								Jellyfin.Api/Controllers/BrandingController.cs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,57 @@ | ||||
| using MediaBrowser.Common.Configuration; | ||||
| using MediaBrowser.Controller.Configuration; | ||||
| using MediaBrowser.Model.Branding; | ||||
| using Microsoft.AspNetCore.Http; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| 
 | ||||
| namespace Jellyfin.Api.Controllers | ||||
| { | ||||
|     /// <summary> | ||||
|     /// Branding controller. | ||||
|     /// </summary> | ||||
|     public class BrandingController : BaseJellyfinApiController | ||||
|     { | ||||
|         private readonly IServerConfigurationManager _serverConfigurationManager; | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="BrandingController"/> class. | ||||
|         /// </summary> | ||||
|         /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param> | ||||
|         public BrandingController(IServerConfigurationManager serverConfigurationManager) | ||||
|         { | ||||
|             _serverConfigurationManager = serverConfigurationManager; | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets branding configuration. | ||||
|         /// </summary> | ||||
|         /// <response code="200">Branding configuration returned.</response> | ||||
|         /// <returns>An <see cref="OkResult"/> containing the branding configuration.</returns> | ||||
|         [HttpGet("Configuration")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         public ActionResult<BrandingOptions> GetBrandingOptions() | ||||
|         { | ||||
|             return _serverConfigurationManager.GetConfiguration<BrandingOptions>("branding"); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets branding css. | ||||
|         /// </summary> | ||||
|         /// <response code="200">Branding css returned.</response> | ||||
|         /// <response code="204">No branding css configured.</response> | ||||
|         /// <returns> | ||||
|         /// An <see cref="OkResult"/> containing the branding css if exist, | ||||
|         /// or a <see cref="NoContentResult"/> if the css is not configured. | ||||
|         /// </returns> | ||||
|         [HttpGet("Css")] | ||||
|         [HttpGet("Css.css")] | ||||
|         [Produces("text/css")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         public ActionResult<string> GetBrandingCss() | ||||
|         { | ||||
|             var options = _serverConfigurationManager.GetConfiguration<BrandingOptions>("branding"); | ||||
|             return options.CustomCss ?? string.Empty; | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -1,5 +1,3 @@ | ||||
| #nullable enable | ||||
| 
 | ||||
| using System.Text.Json; | ||||
| using System.Threading.Tasks; | ||||
| using Jellyfin.Api.Constants; | ||||
| @ -18,7 +16,7 @@ namespace Jellyfin.Api.Controllers | ||||
|     /// Configuration Controller. | ||||
|     /// </summary> | ||||
|     [Route("System")] | ||||
|     [Authorize] | ||||
|     [Authorize(Policy = Policies.DefaultAuthorization)] | ||||
|     public class ConfigurationController : BaseJellyfinApiController | ||||
|     { | ||||
|         private readonly IServerConfigurationManager _configurationManager; | ||||
| @ -53,15 +51,15 @@ namespace Jellyfin.Api.Controllers | ||||
|         /// Updates application configuration. | ||||
|         /// </summary> | ||||
|         /// <param name="configuration">Configuration.</param> | ||||
|         /// <response code="200">Configuration updated.</response> | ||||
|         /// <response code="204">Configuration updated.</response> | ||||
|         /// <returns>Update status.</returns> | ||||
|         [HttpPost("Configuration")] | ||||
|         [Authorize(Policy = Policies.RequiresElevation)] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         public ActionResult UpdateConfiguration([FromBody, BindRequired] ServerConfiguration configuration) | ||||
|         { | ||||
|             _configurationManager.ReplaceConfiguration(configuration); | ||||
|             return Ok(); | ||||
|             return NoContent(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
| @ -70,7 +68,7 @@ namespace Jellyfin.Api.Controllers | ||||
|         /// <param name="key">Configuration key.</param> | ||||
|         /// <response code="200">Configuration returned.</response> | ||||
|         /// <returns>Configuration.</returns> | ||||
|         [HttpGet("Configuration/{Key}")] | ||||
|         [HttpGet("Configuration/{key}")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         public ActionResult<object> GetNamedConfiguration([FromRoute] string key) | ||||
|         { | ||||
| @ -81,17 +79,17 @@ namespace Jellyfin.Api.Controllers | ||||
|         /// Updates named configuration. | ||||
|         /// </summary> | ||||
|         /// <param name="key">Configuration key.</param> | ||||
|         /// <response code="200">Named configuration updated.</response> | ||||
|         /// <response code="204">Named configuration updated.</response> | ||||
|         /// <returns>Update status.</returns> | ||||
|         [HttpPost("Configuration/{Key}")] | ||||
|         [HttpPost("Configuration/{key}")] | ||||
|         [Authorize(Policy = Policies.RequiresElevation)] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         public async Task<ActionResult> UpdateNamedConfiguration([FromRoute] string key) | ||||
|         { | ||||
|             var configurationType = _configurationManager.GetConfigurationType(key); | ||||
|             var configuration = await JsonSerializer.DeserializeAsync(Request.Body, configurationType).ConfigureAwait(false); | ||||
|             _configurationManager.SaveConfiguration(key, configuration); | ||||
|             return Ok(); | ||||
|             return NoContent(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
| @ -111,15 +109,15 @@ namespace Jellyfin.Api.Controllers | ||||
|         /// Updates the path to the media encoder. | ||||
|         /// </summary> | ||||
|         /// <param name="mediaEncoderPath">Media encoder path form body.</param> | ||||
|         /// <response code="200">Media encoder path updated.</response> | ||||
|         /// <response code="204">Media encoder path updated.</response> | ||||
|         /// <returns>Status.</returns> | ||||
|         [HttpPost("MediaEncoder/Path")] | ||||
|         [Authorize(Policy = Policies.FirstTimeSetupOrElevated)] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         public ActionResult UpdateMediaEncoderPath([FromForm, BindRequired] MediaEncoderPathDto mediaEncoderPath) | ||||
|         { | ||||
|             _mediaEncoder.UpdateEncoderPath(mediaEncoderPath.Path, mediaEncoderPath.PathType); | ||||
|             return Ok(); | ||||
|             return NoContent(); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
							
								
								
									
										275
									
								
								Jellyfin.Api/Controllers/DashboardController.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										275
									
								
								Jellyfin.Api/Controllers/DashboardController.cs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,275 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.Diagnostics.CodeAnalysis; | ||||
| using System.IO; | ||||
| using System.Linq; | ||||
| using Jellyfin.Api.Models; | ||||
| using MediaBrowser.Common.Plugins; | ||||
| using MediaBrowser.Controller; | ||||
| using MediaBrowser.Controller.Configuration; | ||||
| using MediaBrowser.Controller.Extensions; | ||||
| using MediaBrowser.Controller.Plugins; | ||||
| using MediaBrowser.Model.Net; | ||||
| using MediaBrowser.Model.Plugins; | ||||
| using Microsoft.AspNetCore.Http; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| using Microsoft.Extensions.Configuration; | ||||
| using Microsoft.Extensions.Logging; | ||||
| 
 | ||||
| namespace Jellyfin.Api.Controllers | ||||
| { | ||||
|     /// <summary> | ||||
|     /// The dashboard controller. | ||||
|     /// </summary> | ||||
|     public class DashboardController : BaseJellyfinApiController | ||||
|     { | ||||
|         private readonly ILogger<DashboardController> _logger; | ||||
|         private readonly IServerApplicationHost _appHost; | ||||
|         private readonly IConfiguration _appConfig; | ||||
|         private readonly IServerConfigurationManager _serverConfigurationManager; | ||||
|         private readonly IResourceFileManager _resourceFileManager; | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="DashboardController"/> class. | ||||
|         /// </summary> | ||||
|         /// <param name="logger">Instance of <see cref="ILogger{DashboardController}"/> interface.</param> | ||||
|         /// <param name="appHost">Instance of <see cref="IServerApplicationHost"/> interface.</param> | ||||
|         /// <param name="appConfig">Instance of <see cref="IConfiguration"/> interface.</param> | ||||
|         /// <param name="resourceFileManager">Instance of <see cref="IResourceFileManager"/> interface.</param> | ||||
|         /// <param name="serverConfigurationManager">Instance of <see cref="IServerConfigurationManager"/> interface.</param> | ||||
|         public DashboardController( | ||||
|             ILogger<DashboardController> logger, | ||||
|             IServerApplicationHost appHost, | ||||
|             IConfiguration appConfig, | ||||
|             IResourceFileManager resourceFileManager, | ||||
|             IServerConfigurationManager serverConfigurationManager) | ||||
|         { | ||||
|             _logger = logger; | ||||
|             _appHost = appHost; | ||||
|             _appConfig = appConfig; | ||||
|             _resourceFileManager = resourceFileManager; | ||||
|             _serverConfigurationManager = serverConfigurationManager; | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets the path of the directory containing the static web interface content, or null if the server is not | ||||
|         /// hosting the web client. | ||||
|         /// </summary> | ||||
|         private string? WebClientUiPath => GetWebClientUiPath(_appConfig, _serverConfigurationManager); | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets the configuration pages. | ||||
|         /// </summary> | ||||
|         /// <param name="enableInMainMenu">Whether to enable in the main menu.</param> | ||||
|         /// <param name="pageType">The <see cref="ConfigurationPageInfo"/>.</param> | ||||
|         /// <response code="200">ConfigurationPages returned.</response> | ||||
|         /// <response code="404">Server still loading.</response> | ||||
|         /// <returns>An <see cref="IEnumerable{ConfigurationPageInfo}"/> with infos about the plugins.</returns> | ||||
|         [HttpGet("/web/ConfigurationPages")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         [ProducesResponseType(StatusCodes.Status404NotFound)] | ||||
|         public ActionResult<IEnumerable<ConfigurationPageInfo?>> GetConfigurationPages( | ||||
|             [FromQuery] bool? enableInMainMenu, | ||||
|             [FromQuery] ConfigurationPageType? pageType) | ||||
|         { | ||||
|             const string unavailableMessage = "The server is still loading. Please try again momentarily."; | ||||
| 
 | ||||
|             var pages = _appHost.GetExports<IPluginConfigurationPage>().ToList(); | ||||
| 
 | ||||
|             if (pages == null) | ||||
|             { | ||||
|                 return NotFound(unavailableMessage); | ||||
|             } | ||||
| 
 | ||||
|             // Don't allow a failing plugin to fail them all | ||||
|             var configPages = pages.Select(p => | ||||
|                 { | ||||
|                     try | ||||
|                     { | ||||
|                         return new ConfigurationPageInfo(p); | ||||
|                     } | ||||
|                     catch (Exception ex) | ||||
|                     { | ||||
|                         _logger.LogError(ex, "Error getting plugin information from {Plugin}", p.GetType().Name); | ||||
|                         return null; | ||||
|                     } | ||||
|                 }) | ||||
|                 .Where(i => i != null) | ||||
|                 .ToList(); | ||||
| 
 | ||||
|             configPages.AddRange(_appHost.Plugins.SelectMany(GetConfigPages)); | ||||
| 
 | ||||
|             if (pageType.HasValue) | ||||
|             { | ||||
|                 configPages = configPages.Where(p => p!.ConfigurationPageType == pageType).ToList(); | ||||
|             } | ||||
| 
 | ||||
|             if (enableInMainMenu.HasValue) | ||||
|             { | ||||
|                 configPages = configPages.Where(p => p!.EnableInMainMenu == enableInMainMenu.Value).ToList(); | ||||
|             } | ||||
| 
 | ||||
|             return configPages; | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets a dashboard configuration page. | ||||
|         /// </summary> | ||||
|         /// <param name="name">The name of the page.</param> | ||||
|         /// <response code="200">ConfigurationPage returned.</response> | ||||
|         /// <response code="404">Plugin configuration page not found.</response> | ||||
|         /// <returns>The configuration page.</returns> | ||||
|         [HttpGet("/web/ConfigurationPage")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         [ProducesResponseType(StatusCodes.Status404NotFound)] | ||||
|         public ActionResult GetDashboardConfigurationPage([FromQuery] string name) | ||||
|         { | ||||
|             IPlugin? plugin = null; | ||||
|             Stream? stream = null; | ||||
| 
 | ||||
|             var isJs = false; | ||||
|             var isTemplate = false; | ||||
| 
 | ||||
|             var page = _appHost.GetExports<IPluginConfigurationPage>().FirstOrDefault(p => string.Equals(p.Name, name, StringComparison.OrdinalIgnoreCase)); | ||||
|             if (page != null) | ||||
|             { | ||||
|                 plugin = page.Plugin; | ||||
|                 stream = page.GetHtmlStream(); | ||||
|             } | ||||
| 
 | ||||
|             if (plugin == null) | ||||
|             { | ||||
|                 var altPage = GetPluginPages().FirstOrDefault(p => string.Equals(p.Item1.Name, name, StringComparison.OrdinalIgnoreCase)); | ||||
|                 if (altPage != null) | ||||
|                 { | ||||
|                     plugin = altPage.Item2; | ||||
|                     stream = plugin.GetType().Assembly.GetManifestResourceStream(altPage.Item1.EmbeddedResourcePath); | ||||
| 
 | ||||
|                     isJs = string.Equals(Path.GetExtension(altPage.Item1.EmbeddedResourcePath), ".js", StringComparison.OrdinalIgnoreCase); | ||||
|                     isTemplate = altPage.Item1.EmbeddedResourcePath.EndsWith(".template.html", StringComparison.Ordinal); | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             if (plugin != null && stream != null) | ||||
|             { | ||||
|                 if (isJs) | ||||
|                 { | ||||
|                     return File(stream, MimeTypes.GetMimeType("page.js")); | ||||
|                 } | ||||
| 
 | ||||
|                 if (isTemplate) | ||||
|                 { | ||||
|                     return File(stream, MimeTypes.GetMimeType("page.html")); | ||||
|                 } | ||||
| 
 | ||||
|                 return File(stream, MimeTypes.GetMimeType("page.html")); | ||||
|             } | ||||
| 
 | ||||
|             return NotFound(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets the robots.txt. | ||||
|         /// </summary> | ||||
|         /// <response code="200">Robots.txt returned.</response> | ||||
|         /// <returns>The robots.txt.</returns> | ||||
|         [HttpGet("/robots.txt")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         [ApiExplorerSettings(IgnoreApi = true)] | ||||
|         public ActionResult GetRobotsTxt() | ||||
|         { | ||||
|             return GetWebClientResource("robots.txt", string.Empty); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets a resource from the web client. | ||||
|         /// </summary> | ||||
|         /// <param name="resourceName">The resource name.</param> | ||||
|         /// <param name="v">The v.</param> | ||||
|         /// <response code="200">Web client returned.</response> | ||||
|         /// <response code="404">Server does not host a web client.</response> | ||||
|         /// <returns>The resource.</returns> | ||||
|         [HttpGet("/web/{*resourceName}")] | ||||
|         [ApiExplorerSettings(IgnoreApi = true)] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         [ProducesResponseType(StatusCodes.Status404NotFound)] | ||||
|         [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "v", Justification = "Imported from ServiceStack")] | ||||
|         public ActionResult GetWebClientResource( | ||||
|             [FromRoute] string resourceName, | ||||
|             [FromQuery] string? v) | ||||
|         { | ||||
|             if (!_appConfig.HostWebClient() || WebClientUiPath == null) | ||||
|             { | ||||
|                 return NotFound("Server does not host a web client."); | ||||
|             } | ||||
| 
 | ||||
|             var path = resourceName; | ||||
|             var basePath = WebClientUiPath; | ||||
| 
 | ||||
|             // Bounce them to the startup wizard if it hasn't been completed yet | ||||
|             if (!_serverConfigurationManager.Configuration.IsStartupWizardCompleted | ||||
|                 && !Request.Path.Value.Contains("wizard", StringComparison.OrdinalIgnoreCase) | ||||
|                 && Request.Path.Value.Contains("index", StringComparison.OrdinalIgnoreCase)) | ||||
|             { | ||||
|                 return Redirect("index.html?start=wizard#!/wizardstart.html"); | ||||
|             } | ||||
| 
 | ||||
|             var stream = new FileStream(_resourceFileManager.GetResourcePath(basePath, path), FileMode.Open, FileAccess.Read); | ||||
|             return File(stream, MimeTypes.GetMimeType(path)); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets the favicon. | ||||
|         /// </summary> | ||||
|         /// <response code="200">Favicon.ico returned.</response> | ||||
|         /// <returns>The favicon.</returns> | ||||
|         [HttpGet("/favicon.ico")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         [ApiExplorerSettings(IgnoreApi = true)] | ||||
|         public ActionResult GetFavIcon() | ||||
|         { | ||||
|             return GetWebClientResource("favicon.ico", string.Empty); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets the path of the directory containing the static web interface content. | ||||
|         /// </summary> | ||||
|         /// <param name="appConfig">The app configuration.</param> | ||||
|         /// <param name="serverConfigManager">The server configuration manager.</param> | ||||
|         /// <returns>The directory path, or null if the server is not hosting the web client.</returns> | ||||
|         public static string? GetWebClientUiPath(IConfiguration appConfig, IServerConfigurationManager serverConfigManager) | ||||
|         { | ||||
|             if (!appConfig.HostWebClient()) | ||||
|             { | ||||
|                 return null; | ||||
|             } | ||||
| 
 | ||||
|             if (!string.IsNullOrEmpty(serverConfigManager.Configuration.DashboardSourcePath)) | ||||
|             { | ||||
|                 return serverConfigManager.Configuration.DashboardSourcePath; | ||||
|             } | ||||
| 
 | ||||
|             return serverConfigManager.ApplicationPaths.WebPath; | ||||
|         } | ||||
| 
 | ||||
|         private IEnumerable<ConfigurationPageInfo> GetConfigPages(IPlugin plugin) | ||||
|         { | ||||
|             return GetPluginPages(plugin).Select(i => new ConfigurationPageInfo(plugin, i.Item1)); | ||||
|         } | ||||
| 
 | ||||
|         private IEnumerable<Tuple<PluginPageInfo, IPlugin>> GetPluginPages(IPlugin plugin) | ||||
|         { | ||||
|             if (!(plugin is IHasWebPages hasWebPages)) | ||||
|             { | ||||
|                 return new List<Tuple<PluginPageInfo, IPlugin>>(); | ||||
|             } | ||||
| 
 | ||||
|             return hasWebPages.GetPages().Select(i => new Tuple<PluginPageInfo, IPlugin>(i, plugin)); | ||||
|         } | ||||
| 
 | ||||
|         private IEnumerable<Tuple<PluginPageInfo, IPlugin>> GetPluginPages() | ||||
|         { | ||||
|             return _appHost.Plugins.SelectMany(GetPluginPages); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -1,5 +1,3 @@ | ||||
| #nullable enable | ||||
| 
 | ||||
| using System; | ||||
| using Jellyfin.Api.Constants; | ||||
| using MediaBrowser.Controller.Devices; | ||||
| @ -17,7 +15,7 @@ namespace Jellyfin.Api.Controllers | ||||
|     /// <summary> | ||||
|     /// Devices Controller. | ||||
|     /// </summary> | ||||
|     [Authorize] | ||||
|     [Authorize(Policy = Policies.DefaultAuthorization)] | ||||
|     public class DevicesController : BaseJellyfinApiController | ||||
|     { | ||||
|         private readonly IDeviceManager _deviceManager; | ||||
| @ -105,12 +103,12 @@ namespace Jellyfin.Api.Controllers | ||||
|         /// </summary> | ||||
|         /// <param name="id">Device Id.</param> | ||||
|         /// <param name="deviceOptions">Device Options.</param> | ||||
|         /// <response code="200">Device options updated.</response> | ||||
|         /// <response code="204">Device options updated.</response> | ||||
|         /// <response code="404">Device not found.</response> | ||||
|         /// <returns>An <see cref="OkResult"/> on success, or a <see cref="NotFoundResult"/> if the device could not be found.</returns> | ||||
|         /// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the device could not be found.</returns> | ||||
|         [HttpPost("Options")] | ||||
|         [Authorize(Policy = Policies.RequiresElevation)] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         [ProducesResponseType(StatusCodes.Status404NotFound)] | ||||
|         public ActionResult UpdateDeviceOptions( | ||||
|             [FromQuery, BindRequired] string id, | ||||
| @ -123,18 +121,19 @@ namespace Jellyfin.Api.Controllers | ||||
|             } | ||||
| 
 | ||||
|             _deviceManager.UpdateDeviceOptions(id, deviceOptions); | ||||
|             return Ok(); | ||||
|             return NoContent(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Deletes a device. | ||||
|         /// </summary> | ||||
|         /// <param name="id">Device Id.</param> | ||||
|         /// <response code="200">Device deleted.</response> | ||||
|         /// <response code="204">Device deleted.</response> | ||||
|         /// <response code="404">Device not found.</response> | ||||
|         /// <returns>An <see cref="OkResult"/> on success, or a <see cref="NotFoundResult"/> if the device could not be found.</returns> | ||||
|         /// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the device could not be found.</returns> | ||||
|         [HttpDelete] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         [ProducesResponseType(StatusCodes.Status404NotFound)] | ||||
|         public ActionResult DeleteDevice([FromQuery, BindRequired] string id) | ||||
|         { | ||||
|             var existingDevice = _deviceManager.GetDevice(id); | ||||
| @ -150,7 +149,7 @@ namespace Jellyfin.Api.Controllers | ||||
|                 _sessionManager.Logout(session); | ||||
|             } | ||||
| 
 | ||||
|             return Ok(); | ||||
|             return NoContent(); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
							
								
								
									
										76
									
								
								Jellyfin.Api/Controllers/DisplayPreferencesController.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								Jellyfin.Api/Controllers/DisplayPreferencesController.cs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,76 @@ | ||||
| using System.ComponentModel.DataAnnotations; | ||||
| using System.Diagnostics.CodeAnalysis; | ||||
| using System.Threading; | ||||
| using Jellyfin.Api.Constants; | ||||
| using MediaBrowser.Controller.Persistence; | ||||
| using MediaBrowser.Model.Entities; | ||||
| using Microsoft.AspNetCore.Authorization; | ||||
| using Microsoft.AspNetCore.Http; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| using Microsoft.AspNetCore.Mvc.ModelBinding; | ||||
| 
 | ||||
| namespace Jellyfin.Api.Controllers | ||||
| { | ||||
|     /// <summary> | ||||
|     /// Display Preferences Controller. | ||||
|     /// </summary> | ||||
|     [Authorize(Policy = Policies.DefaultAuthorization)] | ||||
|     public class DisplayPreferencesController : BaseJellyfinApiController | ||||
|     { | ||||
|         private readonly IDisplayPreferencesRepository _displayPreferencesRepository; | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="DisplayPreferencesController"/> class. | ||||
|         /// </summary> | ||||
|         /// <param name="displayPreferencesRepository">Instance of <see cref="IDisplayPreferencesRepository"/> interface.</param> | ||||
|         public DisplayPreferencesController(IDisplayPreferencesRepository displayPreferencesRepository) | ||||
|         { | ||||
|             _displayPreferencesRepository = displayPreferencesRepository; | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Get Display Preferences. | ||||
|         /// </summary> | ||||
|         /// <param name="displayPreferencesId">Display preferences id.</param> | ||||
|         /// <param name="userId">User id.</param> | ||||
|         /// <param name="client">Client.</param> | ||||
|         /// <response code="200">Display preferences retrieved.</response> | ||||
|         /// <returns>An <see cref="OkResult"/> containing the display preferences on success, or a <see cref="NotFoundResult"/> if the display preferences could not be found.</returns> | ||||
|         [HttpGet("{displayPreferencesId}")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         public ActionResult<DisplayPreferences> GetDisplayPreferences( | ||||
|             [FromRoute] string displayPreferencesId, | ||||
|             [FromQuery] [Required] string userId, | ||||
|             [FromQuery] [Required] string client) | ||||
|         { | ||||
|             return _displayPreferencesRepository.GetDisplayPreferences(displayPreferencesId, userId, client); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Update Display Preferences. | ||||
|         /// </summary> | ||||
|         /// <param name="displayPreferencesId">Display preferences id.</param> | ||||
|         /// <param name="userId">User Id.</param> | ||||
|         /// <param name="client">Client.</param> | ||||
|         /// <param name="displayPreferences">New Display Preferences object.</param> | ||||
|         /// <response code="204">Display preferences updated.</response> | ||||
|         /// <returns>An <see cref="NoContentResult"/> on success.</returns> | ||||
|         [HttpPost("{displayPreferencesId}")] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "displayPreferencesId", Justification = "Imported from ServiceStack")] | ||||
|         public ActionResult UpdateDisplayPreferences( | ||||
|             [FromRoute] string displayPreferencesId, | ||||
|             [FromQuery, BindRequired] string userId, | ||||
|             [FromQuery, BindRequired] string client, | ||||
|             [FromBody, BindRequired] DisplayPreferences displayPreferences) | ||||
|         { | ||||
|             _displayPreferencesRepository.SaveDisplayPreferences( | ||||
|                 displayPreferences, | ||||
|                 userId, | ||||
|                 client, | ||||
|                 CancellationToken.None); | ||||
| 
 | ||||
|             return NoContent(); | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										221
									
								
								Jellyfin.Api/Controllers/FilterController.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										221
									
								
								Jellyfin.Api/Controllers/FilterController.cs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,221 @@ | ||||
| using System; | ||||
| using System.Diagnostics.CodeAnalysis; | ||||
| using System.Linq; | ||||
| using Jellyfin.Api.Constants; | ||||
| using MediaBrowser.Controller.Dto; | ||||
| using MediaBrowser.Controller.Entities; | ||||
| using MediaBrowser.Controller.Entities.Audio; | ||||
| using MediaBrowser.Controller.Entities.Movies; | ||||
| using MediaBrowser.Controller.Library; | ||||
| using MediaBrowser.Controller.Playlists; | ||||
| using MediaBrowser.Model.Dto; | ||||
| using MediaBrowser.Model.Querying; | ||||
| using Microsoft.AspNetCore.Authorization; | ||||
| using Microsoft.AspNetCore.Http; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| 
 | ||||
| namespace Jellyfin.Api.Controllers | ||||
| { | ||||
|     /// <summary> | ||||
|     /// Filters controller. | ||||
|     /// </summary> | ||||
|     [Authorize(Policy = Policies.DefaultAuthorization)] | ||||
|     public class FilterController : BaseJellyfinApiController | ||||
|     { | ||||
|         private readonly ILibraryManager _libraryManager; | ||||
|         private readonly IUserManager _userManager; | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="FilterController"/> class. | ||||
|         /// </summary> | ||||
|         /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param> | ||||
|         /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param> | ||||
|         public FilterController(ILibraryManager libraryManager, IUserManager userManager) | ||||
|         { | ||||
|             _libraryManager = libraryManager; | ||||
|             _userManager = userManager; | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets legacy query filters. | ||||
|         /// </summary> | ||||
|         /// <param name="userId">Optional. User id.</param> | ||||
|         /// <param name="parentId">Optional. Parent id.</param> | ||||
|         /// <param name="includeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param> | ||||
|         /// <param name="mediaTypes">Optional. Filter by MediaType. Allows multiple, comma delimited.</param> | ||||
|         /// <response code="200">Legacy filters retrieved.</response> | ||||
|         /// <returns>Legacy query filters.</returns> | ||||
|         [HttpGet("/Items/Filters")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         public ActionResult<QueryFiltersLegacy> GetQueryFiltersLegacy( | ||||
|             [FromQuery] Guid? userId, | ||||
|             [FromQuery] string? parentId, | ||||
|             [FromQuery] string? includeItemTypes, | ||||
|             [FromQuery] string? mediaTypes) | ||||
|         { | ||||
|             var parentItem = string.IsNullOrEmpty(parentId) | ||||
|                 ? null | ||||
|                 : _libraryManager.GetItemById(parentId); | ||||
| 
 | ||||
|             var user = userId == null || userId == Guid.Empty | ||||
|                 ? null | ||||
|                 : _userManager.GetUserById(userId.Value); | ||||
| 
 | ||||
|             if (string.Equals(includeItemTypes, nameof(BoxSet), StringComparison.OrdinalIgnoreCase) | ||||
|                 || string.Equals(includeItemTypes, nameof(Playlist), StringComparison.OrdinalIgnoreCase) | ||||
|                 || string.Equals(includeItemTypes, nameof(Trailer), StringComparison.OrdinalIgnoreCase) | ||||
|                 || string.Equals(includeItemTypes, "Program", StringComparison.OrdinalIgnoreCase)) | ||||
|             { | ||||
|                 parentItem = null; | ||||
|             } | ||||
| 
 | ||||
|             var item = string.IsNullOrEmpty(parentId) | ||||
|                 ? user == null | ||||
|                     ? _libraryManager.RootFolder | ||||
|                     : _libraryManager.GetUserRootFolder() | ||||
|                 : parentItem; | ||||
| 
 | ||||
|             var query = new InternalItemsQuery | ||||
|             { | ||||
|                 User = user, | ||||
|                 MediaTypes = (mediaTypes ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries), | ||||
|                 IncludeItemTypes = (includeItemTypes ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries), | ||||
|                 Recursive = true, | ||||
|                 EnableTotalRecordCount = false, | ||||
|                 DtoOptions = new DtoOptions | ||||
|                 { | ||||
|                     Fields = new[] { ItemFields.Genres, ItemFields.Tags }, | ||||
|                     EnableImages = false, | ||||
|                     EnableUserData = false | ||||
|                 } | ||||
|             }; | ||||
| 
 | ||||
|             var itemList = ((Folder)item!).GetItemList(query); | ||||
|             return new QueryFiltersLegacy | ||||
|             { | ||||
|                 Years = itemList.Select(i => i.ProductionYear ?? -1) | ||||
|                     .Where(i => i > 0) | ||||
|                     .Distinct() | ||||
|                     .OrderBy(i => i) | ||||
|                     .ToArray(), | ||||
| 
 | ||||
|                 Genres = itemList.SelectMany(i => i.Genres) | ||||
|                     .DistinctNames() | ||||
|                     .OrderBy(i => i) | ||||
|                     .ToArray(), | ||||
| 
 | ||||
|                 Tags = itemList | ||||
|                     .SelectMany(i => i.Tags) | ||||
|                     .Distinct(StringComparer.OrdinalIgnoreCase) | ||||
|                     .OrderBy(i => i) | ||||
|                     .ToArray(), | ||||
| 
 | ||||
|                 OfficialRatings = itemList | ||||
|                     .Select(i => i.OfficialRating) | ||||
|                     .Where(i => !string.IsNullOrWhiteSpace(i)) | ||||
|                     .Distinct(StringComparer.OrdinalIgnoreCase) | ||||
|                     .OrderBy(i => i) | ||||
|                     .ToArray() | ||||
|             }; | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets query filters. | ||||
|         /// </summary> | ||||
|         /// <param name="userId">Optional. User id.</param> | ||||
|         /// <param name="parentId">Optional. Specify this to localize the search to a specific item or folder. Omit to use the root.</param> | ||||
|         /// <param name="includeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param> | ||||
|         /// <param name="mediaTypes">[Unused] Optional. Filter by MediaType. Allows multiple, comma delimited.</param> | ||||
|         /// <param name="isAiring">Optional. Is item airing.</param> | ||||
|         /// <param name="isMovie">Optional. Is item movie.</param> | ||||
|         /// <param name="isSports">Optional. Is item sports.</param> | ||||
|         /// <param name="isKids">Optional. Is item kids.</param> | ||||
|         /// <param name="isNews">Optional. Is item news.</param> | ||||
|         /// <param name="isSeries">Optional. Is item series.</param> | ||||
|         /// <param name="recursive">Optional. Search recursive.</param> | ||||
|         /// <response code="200">Filters retrieved.</response> | ||||
|         /// <returns>Query filters.</returns> | ||||
|         [HttpGet("/Items/Filters2")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "mediaTypes", Justification = "Imported from ServiceStack")] | ||||
|         public ActionResult<QueryFilters> GetQueryFilters( | ||||
|             [FromQuery] Guid? userId, | ||||
|             [FromQuery] string? parentId, | ||||
|             [FromQuery] string? includeItemTypes, | ||||
|             [FromQuery] string? mediaTypes, | ||||
|             [FromQuery] bool? isAiring, | ||||
|             [FromQuery] bool? isMovie, | ||||
|             [FromQuery] bool? isSports, | ||||
|             [FromQuery] bool? isKids, | ||||
|             [FromQuery] bool? isNews, | ||||
|             [FromQuery] bool? isSeries, | ||||
|             [FromQuery] bool? recursive) | ||||
|         { | ||||
|             var parentItem = string.IsNullOrEmpty(parentId) | ||||
|                 ? null | ||||
|                 : _libraryManager.GetItemById(parentId); | ||||
| 
 | ||||
|             var user = userId == null || userId == Guid.Empty | ||||
|                 ? null | ||||
|                 : _userManager.GetUserById(userId.Value); | ||||
| 
 | ||||
|             if (string.Equals(includeItemTypes, nameof(BoxSet), StringComparison.OrdinalIgnoreCase) | ||||
|                 || string.Equals(includeItemTypes, nameof(Playlist), StringComparison.OrdinalIgnoreCase) | ||||
|                 || string.Equals(includeItemTypes, nameof(Trailer), StringComparison.OrdinalIgnoreCase) | ||||
|                 || string.Equals(includeItemTypes, "Program", StringComparison.OrdinalIgnoreCase)) | ||||
|             { | ||||
|                 parentItem = null; | ||||
|             } | ||||
| 
 | ||||
|             var filters = new QueryFilters(); | ||||
|             var genreQuery = new InternalItemsQuery(user) | ||||
|             { | ||||
|                 IncludeItemTypes = | ||||
|                     (includeItemTypes ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries), | ||||
|                 DtoOptions = new DtoOptions | ||||
|                 { | ||||
|                     Fields = Array.Empty<ItemFields>(), | ||||
|                     EnableImages = false, | ||||
|                     EnableUserData = false | ||||
|                 }, | ||||
|                 IsAiring = isAiring, | ||||
|                 IsMovie = isMovie, | ||||
|                 IsSports = isSports, | ||||
|                 IsKids = isKids, | ||||
|                 IsNews = isNews, | ||||
|                 IsSeries = isSeries | ||||
|             }; | ||||
| 
 | ||||
|             if ((recursive ?? true) || parentItem is UserView || parentItem is ICollectionFolder) | ||||
|             { | ||||
|                 genreQuery.AncestorIds = parentItem == null ? Array.Empty<Guid>() : new[] { parentItem.Id }; | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 genreQuery.Parent = parentItem; | ||||
|             } | ||||
| 
 | ||||
|             if (string.Equals(includeItemTypes, nameof(MusicAlbum), StringComparison.OrdinalIgnoreCase) | ||||
|                 || string.Equals(includeItemTypes, nameof(MusicVideo), StringComparison.OrdinalIgnoreCase) | ||||
|                 || string.Equals(includeItemTypes, nameof(MusicArtist), StringComparison.OrdinalIgnoreCase) | ||||
|                 || string.Equals(includeItemTypes, nameof(Audio), StringComparison.OrdinalIgnoreCase)) | ||||
|             { | ||||
|                 filters.Genres = _libraryManager.GetMusicGenres(genreQuery).Items.Select(i => new NameGuidPair | ||||
|                 { | ||||
|                     Name = i.Item1.Name, | ||||
|                     Id = i.Item1.Id | ||||
|                 }).ToArray(); | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 filters.Genres = _libraryManager.GetGenres(genreQuery).Items.Select(i => new NameGuidPair | ||||
|                 { | ||||
|                     Name = i.Item1.Name, | ||||
|                     Id = i.Item1.Id | ||||
|                 }).ToArray(); | ||||
|             } | ||||
| 
 | ||||
|             return filters; | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										230
									
								
								Jellyfin.Api/Controllers/ImageByNameController.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										230
									
								
								Jellyfin.Api/Controllers/ImageByNameController.cs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,230 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.IO; | ||||
| using System.Linq; | ||||
| using System.Net.Mime; | ||||
| using Jellyfin.Api.Constants; | ||||
| using MediaBrowser.Controller; | ||||
| using MediaBrowser.Controller.Configuration; | ||||
| using MediaBrowser.Controller.Entities; | ||||
| using MediaBrowser.Model.Dto; | ||||
| using MediaBrowser.Model.IO; | ||||
| using MediaBrowser.Model.Net; | ||||
| using Microsoft.AspNetCore.Authorization; | ||||
| using Microsoft.AspNetCore.Http; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| 
 | ||||
| namespace Jellyfin.Api.Controllers | ||||
| { | ||||
|     /// <summary> | ||||
|     ///     Images By Name Controller. | ||||
|     /// </summary> | ||||
|     [Route("Images")] | ||||
|     public class ImageByNameController : BaseJellyfinApiController | ||||
|     { | ||||
|         private readonly IServerApplicationPaths _applicationPaths; | ||||
|         private readonly IFileSystem _fileSystem; | ||||
| 
 | ||||
|         /// <summary> | ||||
|         ///     Initializes a new instance of the <see cref="ImageByNameController" /> class. | ||||
|         /// </summary> | ||||
|         /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager" /> interface.</param> | ||||
|         /// <param name="fileSystem">Instance of the <see cref="IFileSystem" /> interface.</param> | ||||
|         public ImageByNameController( | ||||
|             IServerConfigurationManager serverConfigurationManager, | ||||
|             IFileSystem fileSystem) | ||||
|         { | ||||
|             _applicationPaths = serverConfigurationManager.ApplicationPaths; | ||||
|             _fileSystem = fileSystem; | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         ///     Get all general images. | ||||
|         /// </summary> | ||||
|         /// <response code="200">Retrieved list of images.</response> | ||||
|         /// <returns>An <see cref="OkResult"/> containing the list of images.</returns> | ||||
|         [HttpGet("General")] | ||||
|         [Authorize(Policy = Policies.DefaultAuthorization)] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         public ActionResult<IEnumerable<ImageByNameInfo>> GetGeneralImages() | ||||
|         { | ||||
|             return GetImageList(_applicationPaths.GeneralPath, false); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         ///     Get General Image. | ||||
|         /// </summary> | ||||
|         /// <param name="name">The name of the image.</param> | ||||
|         /// <param name="type">Image Type (primary, backdrop, logo, etc).</param> | ||||
|         /// <response code="200">Image stream retrieved.</response> | ||||
|         /// <response code="404">Image not found.</response> | ||||
|         /// <returns>A <see cref="FileStreamResult"/> containing the image contents on success, or a <see cref="NotFoundResult"/> if the image could not be found.</returns> | ||||
|         [HttpGet("General/{name}/{type}")] | ||||
|         [AllowAnonymous] | ||||
|         [Produces(MediaTypeNames.Application.Octet)] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         [ProducesResponseType(StatusCodes.Status404NotFound)] | ||||
|         public ActionResult<FileStreamResult> GetGeneralImage([FromRoute] string name, [FromRoute] string type) | ||||
|         { | ||||
|             var filename = string.Equals(type, "primary", StringComparison.OrdinalIgnoreCase) | ||||
|                 ? "folder" | ||||
|                 : type; | ||||
| 
 | ||||
|             var path = BaseItem.SupportedImageExtensions | ||||
|                 .Select(i => Path.Combine(_applicationPaths.GeneralPath, name, filename + i)) | ||||
|                 .FirstOrDefault(System.IO.File.Exists); | ||||
| 
 | ||||
|             if (path == null) | ||||
|             { | ||||
|                 return NotFound(); | ||||
|             } | ||||
| 
 | ||||
|             var contentType = MimeTypes.GetMimeType(path); | ||||
|             return File(System.IO.File.OpenRead(path), contentType); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         ///     Get all general images. | ||||
|         /// </summary> | ||||
|         /// <response code="200">Retrieved list of images.</response> | ||||
|         /// <returns>An <see cref="OkResult"/> containing the list of images.</returns> | ||||
|         [HttpGet("Ratings")] | ||||
|         [Authorize(Policy = Policies.DefaultAuthorization)] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         public ActionResult<IEnumerable<ImageByNameInfo>> GetRatingImages() | ||||
|         { | ||||
|             return GetImageList(_applicationPaths.RatingsPath, false); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         ///     Get rating image. | ||||
|         /// </summary> | ||||
|         /// <param name="theme">The theme to get the image from.</param> | ||||
|         /// <param name="name">The name of the image.</param> | ||||
|         /// <response code="200">Image stream retrieved.</response> | ||||
|         /// <response code="404">Image not found.</response> | ||||
|         /// <returns>A <see cref="FileStreamResult"/> containing the image contents on success, or a <see cref="NotFoundResult"/> if the image could not be found.</returns> | ||||
|         [HttpGet("Ratings/{theme}/{name}")] | ||||
|         [AllowAnonymous] | ||||
|         [Produces(MediaTypeNames.Application.Octet)] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         [ProducesResponseType(StatusCodes.Status404NotFound)] | ||||
|         public ActionResult<FileStreamResult> GetRatingImage( | ||||
|             [FromRoute] string theme, | ||||
|             [FromRoute] string name) | ||||
|         { | ||||
|             return GetImageFile(_applicationPaths.RatingsPath, theme, name); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         ///     Get all media info images. | ||||
|         /// </summary> | ||||
|         /// <response code="200">Image list retrieved.</response> | ||||
|         /// <returns>An <see cref="OkResult"/> containing the list of images.</returns> | ||||
|         [HttpGet("MediaInfo")] | ||||
|         [Authorize(Policy = Policies.DefaultAuthorization)] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         public ActionResult<IEnumerable<ImageByNameInfo>> GetMediaInfoImages() | ||||
|         { | ||||
|             return GetImageList(_applicationPaths.MediaInfoImagesPath, false); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         ///     Get media info image. | ||||
|         /// </summary> | ||||
|         /// <param name="theme">The theme to get the image from.</param> | ||||
|         /// <param name="name">The name of the image.</param> | ||||
|         /// <response code="200">Image stream retrieved.</response> | ||||
|         /// <response code="404">Image not found.</response> | ||||
|         /// <returns>A <see cref="FileStreamResult"/> containing the image contents on success, or a <see cref="NotFoundResult"/> if the image could not be found.</returns> | ||||
|         [HttpGet("MediaInfo/{theme}/{name}")] | ||||
|         [AllowAnonymous] | ||||
|         [Produces(MediaTypeNames.Application.Octet)] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         [ProducesResponseType(StatusCodes.Status404NotFound)] | ||||
|         public ActionResult<FileStreamResult> GetMediaInfoImage( | ||||
|             [FromRoute] string theme, | ||||
|             [FromRoute] string name) | ||||
|         { | ||||
|             return GetImageFile(_applicationPaths.MediaInfoImagesPath, theme, name); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         ///     Internal FileHelper. | ||||
|         /// </summary> | ||||
|         /// <param name="basePath">Path to begin search.</param> | ||||
|         /// <param name="theme">Theme to search.</param> | ||||
|         /// <param name="name">File name to search for.</param> | ||||
|         /// <returns>A <see cref="FileStreamResult"/> containing the image contents on success, or a <see cref="NotFoundResult"/> if the image could not be found.</returns> | ||||
|         private ActionResult<FileStreamResult> GetImageFile(string basePath, string theme, string name) | ||||
|         { | ||||
|             var themeFolder = Path.Combine(basePath, theme); | ||||
|             if (Directory.Exists(themeFolder)) | ||||
|             { | ||||
|                 var path = BaseItem.SupportedImageExtensions.Select(i => Path.Combine(themeFolder, name + i)) | ||||
|                     .FirstOrDefault(System.IO.File.Exists); | ||||
| 
 | ||||
|                 if (!string.IsNullOrEmpty(path) && System.IO.File.Exists(path)) | ||||
|                 { | ||||
|                     var contentType = MimeTypes.GetMimeType(path); | ||||
|                     return File(System.IO.File.OpenRead(path), contentType); | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             var allFolder = Path.Combine(basePath, "all"); | ||||
|             if (Directory.Exists(allFolder)) | ||||
|             { | ||||
|                 var path = BaseItem.SupportedImageExtensions.Select(i => Path.Combine(allFolder, name + i)) | ||||
|                     .FirstOrDefault(System.IO.File.Exists); | ||||
| 
 | ||||
|                 if (!string.IsNullOrEmpty(path) && System.IO.File.Exists(path)) | ||||
|                 { | ||||
|                     var contentType = MimeTypes.GetMimeType(path); | ||||
|                     return File(System.IO.File.OpenRead(path), contentType); | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             return NotFound(); | ||||
|         } | ||||
| 
 | ||||
|         private List<ImageByNameInfo> GetImageList(string path, bool supportsThemes) | ||||
|         { | ||||
|             try | ||||
|             { | ||||
|                 return _fileSystem.GetFiles(path, BaseItem.SupportedImageExtensions, false, true) | ||||
|                     .Select(i => new ImageByNameInfo | ||||
|                     { | ||||
|                         Name = _fileSystem.GetFileNameWithoutExtension(i), | ||||
|                         FileLength = i.Length, | ||||
| 
 | ||||
|                         // For themeable images, use the Theme property | ||||
|                         // For general images, the same object structure is fine, | ||||
|                         // but it's not owned by a theme, so call it Context | ||||
|                         Theme = supportsThemes ? GetThemeName(i.FullName, path) : null, | ||||
|                         Context = supportsThemes ? null : GetThemeName(i.FullName, path), | ||||
|                         Format = i.Extension.ToLowerInvariant().TrimStart('.') | ||||
|                     }) | ||||
|                     .OrderBy(i => i.Name) | ||||
|                     .ToList(); | ||||
|             } | ||||
|             catch (IOException) | ||||
|             { | ||||
|                 return new List<ImageByNameInfo>(); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         private string? GetThemeName(string path, string rootImagePath) | ||||
|         { | ||||
|             var parentName = Path.GetDirectoryName(path); | ||||
| 
 | ||||
|             if (string.Equals(parentName, rootImagePath, StringComparison.OrdinalIgnoreCase)) | ||||
|             { | ||||
|                 return null; | ||||
|             } | ||||
| 
 | ||||
|             parentName = Path.GetFileName(parentName); | ||||
| 
 | ||||
|             return string.Equals(parentName, "all", StringComparison.OrdinalIgnoreCase) ? null : parentName; | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										364
									
								
								Jellyfin.Api/Controllers/ItemLookupController.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										364
									
								
								Jellyfin.Api/Controllers/ItemLookupController.cs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,364 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.ComponentModel.DataAnnotations; | ||||
| using System.IO; | ||||
| using System.Linq; | ||||
| using System.Net.Mime; | ||||
| using System.Text.Json; | ||||
| using System.Threading; | ||||
| using System.Threading.Tasks; | ||||
| using Jellyfin.Api.Constants; | ||||
| using MediaBrowser.Common.Extensions; | ||||
| using MediaBrowser.Controller; | ||||
| using MediaBrowser.Controller.Configuration; | ||||
| using MediaBrowser.Controller.Entities; | ||||
| using MediaBrowser.Controller.Entities.Audio; | ||||
| using MediaBrowser.Controller.Entities.Movies; | ||||
| using MediaBrowser.Controller.Entities.TV; | ||||
| using MediaBrowser.Controller.Library; | ||||
| using MediaBrowser.Controller.Providers; | ||||
| using MediaBrowser.Model.IO; | ||||
| using MediaBrowser.Model.Providers; | ||||
| 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 | ||||
| { | ||||
|     /// <summary> | ||||
|     /// Item lookup controller. | ||||
|     /// </summary> | ||||
|     [Authorize(Policy = Policies.DefaultAuthorization)] | ||||
|     public class ItemLookupController : BaseJellyfinApiController | ||||
|     { | ||||
|         private readonly IProviderManager _providerManager; | ||||
|         private readonly IServerApplicationPaths _appPaths; | ||||
|         private readonly IFileSystem _fileSystem; | ||||
|         private readonly ILibraryManager _libraryManager; | ||||
|         private readonly ILogger<ItemLookupController> _logger; | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="ItemLookupController"/> class. | ||||
|         /// </summary> | ||||
|         /// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param> | ||||
|         /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param> | ||||
|         /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param> | ||||
|         /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param> | ||||
|         /// <param name="logger">Instance of the <see cref="ILogger{ItemLookupController}"/> interface.</param> | ||||
|         public ItemLookupController( | ||||
|             IProviderManager providerManager, | ||||
|             IServerConfigurationManager serverConfigurationManager, | ||||
|             IFileSystem fileSystem, | ||||
|             ILibraryManager libraryManager, | ||||
|             ILogger<ItemLookupController> logger) | ||||
|         { | ||||
|             _providerManager = providerManager; | ||||
|             _appPaths = serverConfigurationManager.ApplicationPaths; | ||||
|             _fileSystem = fileSystem; | ||||
|             _libraryManager = libraryManager; | ||||
|             _logger = logger; | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Get the item's external id info. | ||||
|         /// </summary> | ||||
|         /// <param name="itemId">Item id.</param> | ||||
|         /// <response code="200">External id info retrieved.</response> | ||||
|         /// <response code="404">Item not found.</response> | ||||
|         /// <returns>List of external id info.</returns> | ||||
|         [HttpGet("/Items/{itemId}/ExternalIdInfos")] | ||||
|         [Authorize(Policy = Policies.RequiresElevation)] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         [ProducesResponseType(StatusCodes.Status404NotFound)] | ||||
|         public ActionResult<IEnumerable<ExternalIdInfo>> GetExternalIdInfos([FromRoute] Guid itemId) | ||||
|         { | ||||
|             var item = _libraryManager.GetItemById(itemId); | ||||
|             if (item == null) | ||||
|             { | ||||
|                 return NotFound(); | ||||
|             } | ||||
| 
 | ||||
|             return Ok(_providerManager.GetExternalIdInfos(item)); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Get movie remote search. | ||||
|         /// </summary> | ||||
|         /// <param name="query">Remote search query.</param> | ||||
|         /// <response code="200">Movie remote search executed.</response> | ||||
|         /// <returns> | ||||
|         /// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results. | ||||
|         /// The task result contains an <see cref="OkResult"/> containing the list of remote search results. | ||||
|         /// </returns> | ||||
|         [HttpPost("/Items/RemoteSearch/Movie")] | ||||
|         public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetMovieRemoteSearchResults([FromBody, BindRequired] RemoteSearchQuery<MovieInfo> query) | ||||
|         { | ||||
|             var results = await _providerManager.GetRemoteSearchResults<Movie, MovieInfo>(query, CancellationToken.None) | ||||
|                 .ConfigureAwait(false); | ||||
|             return Ok(results); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Get trailer remote search. | ||||
|         /// </summary> | ||||
|         /// <param name="query">Remote search query.</param> | ||||
|         /// <response code="200">Trailer remote search executed.</response> | ||||
|         /// <returns> | ||||
|         /// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results. | ||||
|         /// The task result contains an <see cref="OkResult"/> containing the list of remote search results. | ||||
|         /// </returns> | ||||
|         [HttpPost("/Items/RemoteSearch/Trailer")] | ||||
|         public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetTrailerRemoteSearchResults([FromBody, BindRequired] RemoteSearchQuery<TrailerInfo> query) | ||||
|         { | ||||
|             var results = await _providerManager.GetRemoteSearchResults<Trailer, TrailerInfo>(query, CancellationToken.None) | ||||
|                 .ConfigureAwait(false); | ||||
|             return Ok(results); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Get music video remote search. | ||||
|         /// </summary> | ||||
|         /// <param name="query">Remote search query.</param> | ||||
|         /// <response code="200">Music video remote search executed.</response> | ||||
|         /// <returns> | ||||
|         /// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results. | ||||
|         /// The task result contains an <see cref="OkResult"/> containing the list of remote search results. | ||||
|         /// </returns> | ||||
|         [HttpPost("/Items/RemoteSearch/MusicVideo")] | ||||
|         public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetMusicVideoRemoteSearchResults([FromBody, BindRequired] RemoteSearchQuery<MusicVideoInfo> query) | ||||
|         { | ||||
|             var results = await _providerManager.GetRemoteSearchResults<MusicVideo, MusicVideoInfo>(query, CancellationToken.None) | ||||
|                 .ConfigureAwait(false); | ||||
|             return Ok(results); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Get series remote search. | ||||
|         /// </summary> | ||||
|         /// <param name="query">Remote search query.</param> | ||||
|         /// <response code="200">Series remote search executed.</response> | ||||
|         /// <returns> | ||||
|         /// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results. | ||||
|         /// The task result contains an <see cref="OkResult"/> containing the list of remote search results. | ||||
|         /// </returns> | ||||
|         [HttpPost("/Items/RemoteSearch/Series")] | ||||
|         public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetSeriesRemoteSearchResults([FromBody, BindRequired] RemoteSearchQuery<SeriesInfo> query) | ||||
|         { | ||||
|             var results = await _providerManager.GetRemoteSearchResults<Series, SeriesInfo>(query, CancellationToken.None) | ||||
|                 .ConfigureAwait(false); | ||||
|             return Ok(results); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Get box set remote search. | ||||
|         /// </summary> | ||||
|         /// <param name="query">Remote search query.</param> | ||||
|         /// <response code="200">Box set remote search executed.</response> | ||||
|         /// <returns> | ||||
|         /// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results. | ||||
|         /// The task result contains an <see cref="OkResult"/> containing the list of remote search results. | ||||
|         /// </returns> | ||||
|         [HttpPost("/Items/RemoteSearch/BoxSet")] | ||||
|         public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetBoxSetRemoteSearchResults([FromBody, BindRequired] RemoteSearchQuery<BoxSetInfo> query) | ||||
|         { | ||||
|             var results = await _providerManager.GetRemoteSearchResults<BoxSet, BoxSetInfo>(query, CancellationToken.None) | ||||
|                 .ConfigureAwait(false); | ||||
|             return Ok(results); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Get music artist remote search. | ||||
|         /// </summary> | ||||
|         /// <param name="query">Remote search query.</param> | ||||
|         /// <response code="200">Music artist remote search executed.</response> | ||||
|         /// <returns> | ||||
|         /// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results. | ||||
|         /// The task result contains an <see cref="OkResult"/> containing the list of remote search results. | ||||
|         /// </returns> | ||||
|         [HttpPost("/Items/RemoteSearch/MusicArtist")] | ||||
|         public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetMusicArtistRemoteSearchResults([FromBody, BindRequired] RemoteSearchQuery<ArtistInfo> query) | ||||
|         { | ||||
|             var results = await _providerManager.GetRemoteSearchResults<MusicArtist, ArtistInfo>(query, CancellationToken.None) | ||||
|                 .ConfigureAwait(false); | ||||
|             return Ok(results); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Get music album remote search. | ||||
|         /// </summary> | ||||
|         /// <param name="query">Remote search query.</param> | ||||
|         /// <response code="200">Music album remote search executed.</response> | ||||
|         /// <returns> | ||||
|         /// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results. | ||||
|         /// The task result contains an <see cref="OkResult"/> containing the list of remote search results. | ||||
|         /// </returns> | ||||
|         [HttpPost("/Items/RemoteSearch/MusicAlbum")] | ||||
|         public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetMusicAlbumRemoteSearchResults([FromBody, BindRequired] RemoteSearchQuery<AlbumInfo> query) | ||||
|         { | ||||
|             var results = await _providerManager.GetRemoteSearchResults<MusicAlbum, AlbumInfo>(query, CancellationToken.None) | ||||
|                 .ConfigureAwait(false); | ||||
|             return Ok(results); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Get person remote search. | ||||
|         /// </summary> | ||||
|         /// <param name="query">Remote search query.</param> | ||||
|         /// <response code="200">Person remote search executed.</response> | ||||
|         /// <returns> | ||||
|         /// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results. | ||||
|         /// The task result contains an <see cref="OkResult"/> containing the list of remote search results. | ||||
|         /// </returns> | ||||
|         [HttpPost("/Items/RemoteSearch/Person")] | ||||
|         [Authorize(Policy = Policies.RequiresElevation)] | ||||
|         public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetPersonRemoteSearchResults([FromBody, BindRequired] RemoteSearchQuery<PersonLookupInfo> query) | ||||
|         { | ||||
|             var results = await _providerManager.GetRemoteSearchResults<Person, PersonLookupInfo>(query, CancellationToken.None) | ||||
|                 .ConfigureAwait(false); | ||||
|             return Ok(results); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Get book remote search. | ||||
|         /// </summary> | ||||
|         /// <param name="query">Remote search query.</param> | ||||
|         /// <response code="200">Book remote search executed.</response> | ||||
|         /// <returns> | ||||
|         /// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results. | ||||
|         /// The task result contains an <see cref="OkResult"/> containing the list of remote search results. | ||||
|         /// </returns> | ||||
|         [HttpPost("/Items/RemoteSearch/Book")] | ||||
|         public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetBookRemoteSearchResults([FromBody, BindRequired] RemoteSearchQuery<BookInfo> query) | ||||
|         { | ||||
|             var results = await _providerManager.GetRemoteSearchResults<Book, BookInfo>(query, CancellationToken.None) | ||||
|                 .ConfigureAwait(false); | ||||
|             return Ok(results); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets a remote image. | ||||
|         /// </summary> | ||||
|         /// <param name="imageUrl">The image url.</param> | ||||
|         /// <param name="providerName">The provider name.</param> | ||||
|         /// <response code="200">Remote image retrieved.</response> | ||||
|         /// <returns> | ||||
|         /// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results. | ||||
|         /// The task result contains an <see cref="FileStreamResult"/> containing the images file stream. | ||||
|         /// </returns> | ||||
|         [HttpGet("/Items/RemoteSearch/Image")] | ||||
|         public async Task<ActionResult> GetRemoteSearchImage( | ||||
|             [FromQuery, Required] string imageUrl, | ||||
|             [FromQuery, Required] string providerName) | ||||
|         { | ||||
|             var urlHash = imageUrl.GetMD5(); | ||||
|             var pointerCachePath = GetFullCachePath(urlHash.ToString()); | ||||
| 
 | ||||
|             try | ||||
|             { | ||||
|                 var contentPath = await System.IO.File.ReadAllTextAsync(pointerCachePath).ConfigureAwait(false); | ||||
|                 if (System.IO.File.Exists(contentPath)) | ||||
|                 { | ||||
|                     await using var fileStreamExisting = System.IO.File.OpenRead(pointerCachePath); | ||||
|                     return new FileStreamResult(fileStreamExisting, MediaTypeNames.Application.Octet); | ||||
|                 } | ||||
|             } | ||||
|             catch (FileNotFoundException) | ||||
|             { | ||||
|                 // Means the file isn't cached yet | ||||
|             } | ||||
|             catch (IOException) | ||||
|             { | ||||
|                 // Means the file isn't cached yet | ||||
|             } | ||||
| 
 | ||||
|             await DownloadImage(providerName, imageUrl, urlHash, pointerCachePath).ConfigureAwait(false); | ||||
| 
 | ||||
|             // Read the pointer file again | ||||
|             await using var fileStream = System.IO.File.OpenRead(pointerCachePath); | ||||
|             return new FileStreamResult(fileStream, MediaTypeNames.Application.Octet); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Applies search criteria to an item and refreshes metadata. | ||||
|         /// </summary> | ||||
|         /// <param name="itemId">Item id.</param> | ||||
|         /// <param name="searchResult">The remote search result.</param> | ||||
|         /// <param name="replaceAllImages">Optional. Whether or not to replace all images. Default: True.</param> | ||||
|         /// <response code="204">Item metadata refreshed.</response> | ||||
|         /// <returns> | ||||
|         /// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results. | ||||
|         /// The task result contains an <see cref="NoContentResult"/>. | ||||
|         /// </returns> | ||||
|         [HttpPost("/Items/RemoteSearch/Apply/{id}")] | ||||
|         [Authorize(Policy = Policies.RequiresElevation)] | ||||
|         public async Task<ActionResult> ApplySearchCriteria( | ||||
|             [FromRoute] Guid itemId, | ||||
|             [FromBody, BindRequired] RemoteSearchResult searchResult, | ||||
|             [FromQuery] bool replaceAllImages = true) | ||||
|         { | ||||
|             var item = _libraryManager.GetItemById(itemId); | ||||
|             _logger.LogInformation( | ||||
|                 "Setting provider id's to item {0}-{1}: {2}", | ||||
|                 item.Id, | ||||
|                 item.Name, | ||||
|                 JsonSerializer.Serialize(searchResult.ProviderIds)); | ||||
| 
 | ||||
|             // Since the refresh process won't erase provider Ids, we need to set this explicitly now. | ||||
|             item.ProviderIds = searchResult.ProviderIds; | ||||
|             await _providerManager.RefreshFullItem( | ||||
|                 item, | ||||
|                 new MetadataRefreshOptions(new DirectoryService(_fileSystem)) | ||||
|                 { | ||||
|                     MetadataRefreshMode = MetadataRefreshMode.FullRefresh, | ||||
|                     ImageRefreshMode = MetadataRefreshMode.FullRefresh, | ||||
|                     ReplaceAllMetadata = true, | ||||
|                     ReplaceAllImages = replaceAllImages, | ||||
|                     SearchResult = searchResult | ||||
|                 }, CancellationToken.None).ConfigureAwait(false); | ||||
| 
 | ||||
|             return NoContent(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Downloads the image. | ||||
|         /// </summary> | ||||
|         /// <param name="providerName">Name of the provider.</param> | ||||
|         /// <param name="url">The URL.</param> | ||||
|         /// <param name="urlHash">The URL hash.</param> | ||||
|         /// <param name="pointerCachePath">The pointer cache path.</param> | ||||
|         /// <returns>Task.</returns> | ||||
|         private async Task DownloadImage(string providerName, string url, Guid urlHash, string pointerCachePath) | ||||
|         { | ||||
|             var result = await _providerManager.GetSearchImage(providerName, url, CancellationToken.None).ConfigureAwait(false); | ||||
|             var ext = result.ContentType.Split('/').Last(); | ||||
|             var fullCachePath = GetFullCachePath(urlHash + "." + ext); | ||||
| 
 | ||||
|             Directory.CreateDirectory(Path.GetDirectoryName(fullCachePath)); | ||||
|             await using (var stream = result.Content) | ||||
|             { | ||||
|                 await using var fileStream = new FileStream( | ||||
|                     fullCachePath, | ||||
|                     FileMode.Create, | ||||
|                     FileAccess.Write, | ||||
|                     FileShare.Read, | ||||
|                     IODefaults.FileStreamBufferSize, | ||||
|                     true); | ||||
| 
 | ||||
|                 await stream.CopyToAsync(fileStream).ConfigureAwait(false); | ||||
|             } | ||||
| 
 | ||||
|             Directory.CreateDirectory(Path.GetDirectoryName(pointerCachePath)); | ||||
|             await System.IO.File.WriteAllTextAsync(pointerCachePath, fullCachePath).ConfigureAwait(false); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets the full cache path. | ||||
|         /// </summary> | ||||
|         /// <param name="filename">The filename.</param> | ||||
|         /// <returns>System.String.</returns> | ||||
|         private string GetFullCachePath(string filename) | ||||
|             => Path.Combine(_appPaths.CachePath, "remote-images", filename.Substring(0, 1), filename); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										90
									
								
								Jellyfin.Api/Controllers/ItemRefreshController.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										90
									
								
								Jellyfin.Api/Controllers/ItemRefreshController.cs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,90 @@ | ||||
| using System; | ||||
| using System.ComponentModel; | ||||
| using System.Diagnostics.CodeAnalysis; | ||||
| using Jellyfin.Api.Constants; | ||||
| using MediaBrowser.Controller.Library; | ||||
| using MediaBrowser.Controller.Providers; | ||||
| using MediaBrowser.Model.IO; | ||||
| using Microsoft.AspNetCore.Authorization; | ||||
| using Microsoft.AspNetCore.Http; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| 
 | ||||
| namespace Jellyfin.Api.Controllers | ||||
| { | ||||
|     /// <summary> | ||||
|     /// Item Refresh Controller. | ||||
|     /// </summary> | ||||
|     /// [Authenticated] | ||||
|     [Route("/Items")] | ||||
|     [Authorize(Policy = Policies.DefaultAuthorization)] | ||||
|     public class ItemRefreshController : BaseJellyfinApiController | ||||
|     { | ||||
|         private readonly ILibraryManager _libraryManager; | ||||
|         private readonly IProviderManager _providerManager; | ||||
|         private readonly IFileSystem _fileSystem; | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="ItemRefreshController"/> class. | ||||
|         /// </summary> | ||||
|         /// <param name="libraryManager">Instance of <see cref="ILibraryManager"/> interface.</param> | ||||
|         /// <param name="providerManager">Instance of <see cref="IProviderManager"/> interface.</param> | ||||
|         /// <param name="fileSystem">Instance of <see cref="IFileSystem"/> interface.</param> | ||||
|         public ItemRefreshController( | ||||
|             ILibraryManager libraryManager, | ||||
|             IProviderManager providerManager, | ||||
|             IFileSystem fileSystem) | ||||
|         { | ||||
|             _libraryManager = libraryManager; | ||||
|             _providerManager = providerManager; | ||||
|             _fileSystem = fileSystem; | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Refreshes metadata for an item. | ||||
|         /// </summary> | ||||
|         /// <param name="itemId">Item id.</param> | ||||
|         /// <param name="metadataRefreshMode">(Optional) Specifies the metadata refresh mode.</param> | ||||
|         /// <param name="imageRefreshMode">(Optional) Specifies the image refresh mode.</param> | ||||
|         /// <param name="replaceAllMetadata">(Optional) Determines if metadata should be replaced. Only applicable if mode is FullRefresh.</param> | ||||
|         /// <param name="replaceAllImages">(Optional) Determines if images should be replaced. Only applicable if mode is FullRefresh.</param> | ||||
|         /// <param name="recursive">(Unused) Indicates if the refresh should occur recursively.</param> | ||||
|         /// <response code="204">Item metadata refresh queued.</response> | ||||
|         /// <response code="404">Item to refresh not found.</response> | ||||
|         /// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the item could not be found.</returns> | ||||
|         [HttpPost("{itemId}/Refresh")] | ||||
|         [Description("Refreshes metadata for an item.")] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         [ProducesResponseType(StatusCodes.Status404NotFound)] | ||||
|         [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "recursive", Justification = "Imported from ServiceStack")] | ||||
|         public ActionResult Post( | ||||
|             [FromRoute] Guid itemId, | ||||
|             [FromQuery] MetadataRefreshMode metadataRefreshMode = MetadataRefreshMode.None, | ||||
|             [FromQuery] MetadataRefreshMode imageRefreshMode = MetadataRefreshMode.None, | ||||
|             [FromQuery] bool replaceAllMetadata = false, | ||||
|             [FromQuery] bool replaceAllImages = false, | ||||
|             [FromQuery] bool recursive = false) | ||||
|         { | ||||
|             var item = _libraryManager.GetItemById(itemId); | ||||
|             if (item == null) | ||||
|             { | ||||
|                 return NotFound(); | ||||
|             } | ||||
| 
 | ||||
|             var refreshOptions = new MetadataRefreshOptions(new DirectoryService(_fileSystem)) | ||||
|             { | ||||
|                 MetadataRefreshMode = metadataRefreshMode, | ||||
|                 ImageRefreshMode = imageRefreshMode, | ||||
|                 ReplaceAllImages = replaceAllImages, | ||||
|                 ReplaceAllMetadata = replaceAllMetadata, | ||||
|                 ForceSave = metadataRefreshMode == MetadataRefreshMode.FullRefresh | ||||
|                     || imageRefreshMode == MetadataRefreshMode.FullRefresh | ||||
|                     || replaceAllImages | ||||
|                     || replaceAllMetadata, | ||||
|                 IsAutomated = false | ||||
|             }; | ||||
| 
 | ||||
|             _providerManager.QueueRefresh(item.Id, refreshOptions, RefreshPriority.High); | ||||
|             return NoContent(); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -1,215 +1,99 @@ | ||||
| using System; | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.Linq; | ||||
| using System.Threading; | ||||
| using Jellyfin.Api.Constants; | ||||
| using MediaBrowser.Controller.Configuration; | ||||
| using MediaBrowser.Controller.Entities; | ||||
| using MediaBrowser.Controller.Entities.Audio; | ||||
| using MediaBrowser.Controller.Entities.TV; | ||||
| using MediaBrowser.Controller.Library; | ||||
| using MediaBrowser.Controller.LiveTv; | ||||
| using MediaBrowser.Controller.Net; | ||||
| using MediaBrowser.Controller.Providers; | ||||
| using MediaBrowser.Model.Dto; | ||||
| using MediaBrowser.Model.Entities; | ||||
| using MediaBrowser.Model.Globalization; | ||||
| using MediaBrowser.Model.IO; | ||||
| using MediaBrowser.Model.Services; | ||||
| using Microsoft.Extensions.Logging; | ||||
| using Microsoft.AspNetCore.Authorization; | ||||
| using Microsoft.AspNetCore.Http; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| using Microsoft.AspNetCore.Mvc.ModelBinding; | ||||
| 
 | ||||
| namespace MediaBrowser.Api | ||||
| namespace Jellyfin.Api.Controllers | ||||
| { | ||||
|     [Route("/Items/{ItemId}", "POST", Summary = "Updates an item")] | ||||
|     public class UpdateItem : BaseItemDto, IReturnVoid | ||||
|     { | ||||
|         [ApiMember(Name = "ItemId", Description = "The id of the item", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] | ||||
|         public string ItemId { get; set; } | ||||
|     } | ||||
| 
 | ||||
|     [Route("/Items/{ItemId}/MetadataEditor", "GET", Summary = "Gets metadata editor info for an item")] | ||||
|     public class GetMetadataEditorInfo : IReturn<MetadataEditorInfo> | ||||
|     { | ||||
|         [ApiMember(Name = "ItemId", Description = "The id of the item", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] | ||||
|         public string ItemId { get; set; } | ||||
|     } | ||||
| 
 | ||||
|     [Route("/Items/{ItemId}/ContentType", "POST", Summary = "Updates an item's content type")] | ||||
|     public class UpdateItemContentType : IReturnVoid | ||||
|     { | ||||
|         [ApiMember(Name = "ItemId", Description = "The id of the item", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] | ||||
|         public Guid ItemId { get; set; } | ||||
| 
 | ||||
|         [ApiMember(Name = "ContentType", Description = "The content type of the item", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")] | ||||
|         public string ContentType { get; set; } | ||||
|     } | ||||
| 
 | ||||
|     [Authenticated(Roles = "admin")] | ||||
|     public class ItemUpdateService : BaseApiService | ||||
|     /// <summary> | ||||
|     /// Item update controller. | ||||
|     /// </summary> | ||||
|     [Authorize(Policy = Policies.RequiresElevation)] | ||||
|     public class ItemUpdateController : BaseJellyfinApiController | ||||
|     { | ||||
|         private readonly ILibraryManager _libraryManager; | ||||
|         private readonly IProviderManager _providerManager; | ||||
|         private readonly ILocalizationManager _localizationManager; | ||||
|         private readonly IFileSystem _fileSystem; | ||||
|         private readonly IServerConfigurationManager _serverConfigurationManager; | ||||
| 
 | ||||
|         public ItemUpdateService( | ||||
|             ILogger<ItemUpdateService> logger, | ||||
|             IServerConfigurationManager serverConfigurationManager, | ||||
|             IHttpResultFactory httpResultFactory, | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="ItemUpdateController"/> class. | ||||
|         /// </summary> | ||||
|         /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param> | ||||
|         /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param> | ||||
|         /// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param> | ||||
|         /// <param name="localizationManager">Instance of the <see cref="ILocalizationManager"/> interface.</param> | ||||
|         /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param> | ||||
|         public ItemUpdateController( | ||||
|             IFileSystem fileSystem, | ||||
|             ILibraryManager libraryManager, | ||||
|             IProviderManager providerManager, | ||||
|             ILocalizationManager localizationManager) | ||||
|             : base(logger, serverConfigurationManager, httpResultFactory) | ||||
|             ILocalizationManager localizationManager, | ||||
|             IServerConfigurationManager serverConfigurationManager) | ||||
|         { | ||||
|             _libraryManager = libraryManager; | ||||
|             _providerManager = providerManager; | ||||
|             _localizationManager = localizationManager; | ||||
|             _fileSystem = fileSystem; | ||||
|             _serverConfigurationManager = serverConfigurationManager; | ||||
|         } | ||||
| 
 | ||||
|         public object Get(GetMetadataEditorInfo request) | ||||
|         /// <summary> | ||||
|         /// Updates an item. | ||||
|         /// </summary> | ||||
|         /// <param name="itemId">The item id.</param> | ||||
|         /// <param name="request">The new item properties.</param> | ||||
|         /// <response code="204">Item updated.</response> | ||||
|         /// <response code="404">Item not found.</response> | ||||
|         /// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the item could not be found.</returns> | ||||
|         [HttpPost("/Items/{itemId}")] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         [ProducesResponseType(StatusCodes.Status404NotFound)] | ||||
|         public ActionResult UpdateItem([FromRoute] Guid itemId, [FromBody, BindRequired] BaseItemDto request) | ||||
|         { | ||||
|             var item = _libraryManager.GetItemById(request.ItemId); | ||||
| 
 | ||||
|             var info = new MetadataEditorInfo | ||||
|             var item = _libraryManager.GetItemById(itemId); | ||||
|             if (item == null) | ||||
|             { | ||||
|                 ParentalRatingOptions = _localizationManager.GetParentalRatings().ToArray(), | ||||
|                 ExternalIdInfos = _providerManager.GetExternalIdInfos(item).ToArray(), | ||||
|                 Countries = _localizationManager.GetCountries().ToArray(), | ||||
|                 Cultures = _localizationManager.GetCultures().ToArray() | ||||
|             }; | ||||
| 
 | ||||
|             if (!item.IsVirtualItem && !(item is ICollectionFolder) && !(item is UserView) && !(item is AggregateFolder) && !(item is LiveTvChannel) && !(item is IItemByName) && | ||||
|                 item.SourceType == SourceType.Library) | ||||
|             { | ||||
|                 var inheritedContentType = _libraryManager.GetInheritedContentType(item); | ||||
|                 var configuredContentType = _libraryManager.GetConfiguredContentType(item); | ||||
| 
 | ||||
|                 if (string.IsNullOrWhiteSpace(inheritedContentType) || !string.IsNullOrWhiteSpace(configuredContentType)) | ||||
|                 { | ||||
|                     info.ContentTypeOptions = GetContentTypeOptions(true).ToArray(); | ||||
|                     info.ContentType = configuredContentType; | ||||
| 
 | ||||
|                     if (string.IsNullOrWhiteSpace(inheritedContentType) || string.Equals(inheritedContentType, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase)) | ||||
|                     { | ||||
|                         info.ContentTypeOptions = info.ContentTypeOptions | ||||
|                             .Where(i => string.IsNullOrWhiteSpace(i.Value) || string.Equals(i.Value, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase)) | ||||
|                             .ToArray(); | ||||
|                     } | ||||
|                 } | ||||
|                 return NotFound(); | ||||
|             } | ||||
| 
 | ||||
|             return ToOptimizedResult(info); | ||||
|         } | ||||
| 
 | ||||
|         public void Post(UpdateItemContentType request) | ||||
|         { | ||||
|             var item = _libraryManager.GetItemById(request.ItemId); | ||||
|             var path = item.ContainingFolderPath; | ||||
| 
 | ||||
|             var types = ServerConfigurationManager.Configuration.ContentTypes | ||||
|                 .Where(i => !string.IsNullOrWhiteSpace(i.Name)) | ||||
|                 .Where(i => !string.Equals(i.Name, path, StringComparison.OrdinalIgnoreCase)) | ||||
|                 .ToList(); | ||||
| 
 | ||||
|             if (!string.IsNullOrWhiteSpace(request.ContentType)) | ||||
|             { | ||||
|                 types.Add(new NameValuePair | ||||
|                 { | ||||
|                     Name = path, | ||||
|                     Value = request.ContentType | ||||
|                 }); | ||||
|             } | ||||
| 
 | ||||
|             ServerConfigurationManager.Configuration.ContentTypes = types.ToArray(); | ||||
|             ServerConfigurationManager.SaveConfiguration(); | ||||
|         } | ||||
| 
 | ||||
|         private List<NameValuePair> GetContentTypeOptions(bool isForItem) | ||||
|         { | ||||
|             var list = new List<NameValuePair>(); | ||||
| 
 | ||||
|             if (isForItem) | ||||
|             { | ||||
|                 list.Add(new NameValuePair | ||||
|                 { | ||||
|                     Name = "Inherit", | ||||
|                     Value = "" | ||||
|                 }); | ||||
|             } | ||||
| 
 | ||||
|             list.Add(new NameValuePair | ||||
|             { | ||||
|                 Name = "Movies", | ||||
|                 Value = "movies" | ||||
|             }); | ||||
|             list.Add(new NameValuePair | ||||
|             { | ||||
|                 Name = "Music", | ||||
|                 Value = "music" | ||||
|             }); | ||||
|             list.Add(new NameValuePair | ||||
|             { | ||||
|                 Name = "Shows", | ||||
|                 Value = "tvshows" | ||||
|             }); | ||||
| 
 | ||||
|             if (!isForItem) | ||||
|             { | ||||
|                 list.Add(new NameValuePair | ||||
|                 { | ||||
|                     Name = "Books", | ||||
|                     Value = "books" | ||||
|                 }); | ||||
|             } | ||||
| 
 | ||||
|             list.Add(new NameValuePair | ||||
|             { | ||||
|                 Name = "HomeVideos", | ||||
|                 Value = "homevideos" | ||||
|             }); | ||||
|             list.Add(new NameValuePair | ||||
|             { | ||||
|                 Name = "MusicVideos", | ||||
|                 Value = "musicvideos" | ||||
|             }); | ||||
|             list.Add(new NameValuePair | ||||
|             { | ||||
|                 Name = "Photos", | ||||
|                 Value = "photos" | ||||
|             }); | ||||
| 
 | ||||
|             if (!isForItem) | ||||
|             { | ||||
|                 list.Add(new NameValuePair | ||||
|                 { | ||||
|                     Name = "MixedContent", | ||||
|                     Value = "" | ||||
|                 }); | ||||
|             } | ||||
| 
 | ||||
|             foreach (var val in list) | ||||
|             { | ||||
|                 val.Name = _localizationManager.GetLocalizedString(val.Name); | ||||
|             } | ||||
| 
 | ||||
|             return list; | ||||
|         } | ||||
| 
 | ||||
|         public void Post(UpdateItem request) | ||||
|         { | ||||
|             var item = _libraryManager.GetItemById(request.ItemId); | ||||
| 
 | ||||
|             var newLockData = request.LockData ?? false; | ||||
|             var isLockedChanged = item.IsLocked != newLockData; | ||||
| 
 | ||||
|             var series = item as Series; | ||||
|             var displayOrderChanged = series != null && !string.Equals(series.DisplayOrder ?? string.Empty, request.DisplayOrder ?? string.Empty, StringComparison.OrdinalIgnoreCase); | ||||
|             var displayOrderChanged = series != null && !string.Equals( | ||||
|                 series.DisplayOrder ?? string.Empty, | ||||
|                 request.DisplayOrder ?? string.Empty, | ||||
|                 StringComparison.OrdinalIgnoreCase); | ||||
| 
 | ||||
|             // Do this first so that metadata savers can pull the updates from the database. | ||||
|             if (request.People != null) | ||||
|             { | ||||
|                 _libraryManager.UpdatePeople(item, request.People.Select(x => new PersonInfo { Name = x.Name, Role = x.Role, Type = x.Type }).ToList()); | ||||
|                 _libraryManager.UpdatePeople( | ||||
|                     item, | ||||
|                     request.People.Select(x => new PersonInfo | ||||
|                     { | ||||
|                         Name = x.Name, | ||||
|                         Role = x.Role, | ||||
|                         Type = x.Type | ||||
|                     }).ToList()); | ||||
|             } | ||||
| 
 | ||||
|             UpdateItem(request, item); | ||||
| @ -232,7 +116,7 @@ namespace MediaBrowser.Api | ||||
|             if (displayOrderChanged) | ||||
|             { | ||||
|                 _providerManager.QueueRefresh( | ||||
|                     series.Id, | ||||
|                     series!.Id, | ||||
|                     new MetadataRefreshOptions(new DirectoryService(_fileSystem)) | ||||
|                     { | ||||
|                         MetadataRefreshMode = MetadataRefreshMode.FullRefresh, | ||||
| @ -241,11 +125,101 @@ namespace MediaBrowser.Api | ||||
|                     }, | ||||
|                     RefreshPriority.High); | ||||
|             } | ||||
| 
 | ||||
|             return NoContent(); | ||||
|         } | ||||
| 
 | ||||
|         private DateTime NormalizeDateTime(DateTime val) | ||||
|         /// <summary> | ||||
|         /// Gets metadata editor info for an item. | ||||
|         /// </summary> | ||||
|         /// <param name="itemId">The item id.</param> | ||||
|         /// <response code="200">Item metadata editor returned.</response> | ||||
|         /// <response code="404">Item not found.</response> | ||||
|         /// <returns>An <see cref="OkResult"/> on success containing the metadata editor, or a <see cref="NotFoundResult"/> if the item could not be found.</returns> | ||||
|         [HttpGet("/Items/{itemId}/MetadataEditor")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         [ProducesResponseType(StatusCodes.Status404NotFound)] | ||||
|         public ActionResult<MetadataEditorInfo> GetMetadataEditorInfo([FromRoute] Guid itemId) | ||||
|         { | ||||
|             return DateTime.SpecifyKind(val, DateTimeKind.Utc); | ||||
|             var item = _libraryManager.GetItemById(itemId); | ||||
| 
 | ||||
|             var info = new MetadataEditorInfo | ||||
|             { | ||||
|                 ParentalRatingOptions = _localizationManager.GetParentalRatings().ToArray(), | ||||
|                 ExternalIdInfos = _providerManager.GetExternalIdInfos(item).ToArray(), | ||||
|                 Countries = _localizationManager.GetCountries().ToArray(), | ||||
|                 Cultures = _localizationManager.GetCultures().ToArray() | ||||
|             }; | ||||
| 
 | ||||
|             if (!item.IsVirtualItem | ||||
|                 && !(item is ICollectionFolder) | ||||
|                 && !(item is UserView) | ||||
|                 && !(item is AggregateFolder) | ||||
|                 && !(item is LiveTvChannel) | ||||
|                 && !(item is IItemByName) | ||||
|                 && item.SourceType == SourceType.Library) | ||||
|             { | ||||
|                 var inheritedContentType = _libraryManager.GetInheritedContentType(item); | ||||
|                 var configuredContentType = _libraryManager.GetConfiguredContentType(item); | ||||
| 
 | ||||
|                 if (string.IsNullOrWhiteSpace(inheritedContentType) || | ||||
|                     !string.IsNullOrWhiteSpace(configuredContentType)) | ||||
|                 { | ||||
|                     info.ContentTypeOptions = GetContentTypeOptions(true).ToArray(); | ||||
|                     info.ContentType = configuredContentType; | ||||
| 
 | ||||
|                     if (string.IsNullOrWhiteSpace(inheritedContentType) | ||||
|                         || string.Equals(inheritedContentType, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase)) | ||||
|                     { | ||||
|                         info.ContentTypeOptions = info.ContentTypeOptions | ||||
|                             .Where(i => string.IsNullOrWhiteSpace(i.Value) | ||||
|                                         || string.Equals(i.Value, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase)) | ||||
|                             .ToArray(); | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             return info; | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Updates an item's content type. | ||||
|         /// </summary> | ||||
|         /// <param name="itemId">The item id.</param> | ||||
|         /// <param name="contentType">The content type of the item.</param> | ||||
|         /// <response code="204">Item content type updated.</response> | ||||
|         /// <response code="404">Item not found.</response> | ||||
|         /// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the item could not be found.</returns> | ||||
|         [HttpPost("/Items/{itemId}/ContentType")] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         [ProducesResponseType(StatusCodes.Status404NotFound)] | ||||
|         public ActionResult UpdateItemContentType([FromRoute] Guid itemId, [FromQuery, BindRequired] string contentType) | ||||
|         { | ||||
|             var item = _libraryManager.GetItemById(itemId); | ||||
|             if (item == null) | ||||
|             { | ||||
|                 return NotFound(); | ||||
|             } | ||||
| 
 | ||||
|             var path = item.ContainingFolderPath; | ||||
| 
 | ||||
|             var types = _serverConfigurationManager.Configuration.ContentTypes | ||||
|                 .Where(i => !string.IsNullOrWhiteSpace(i.Name)) | ||||
|                 .Where(i => !string.Equals(i.Name, path, StringComparison.OrdinalIgnoreCase)) | ||||
|                 .ToList(); | ||||
| 
 | ||||
|             if (!string.IsNullOrWhiteSpace(contentType)) | ||||
|             { | ||||
|                 types.Add(new NameValuePair | ||||
|                 { | ||||
|                     Name = path, | ||||
|                     Value = contentType | ||||
|                 }); | ||||
|             } | ||||
| 
 | ||||
|             _serverConfigurationManager.Configuration.ContentTypes = types.ToArray(); | ||||
|             _serverConfigurationManager.SaveConfiguration(); | ||||
|             return NoContent(); | ||||
|         } | ||||
| 
 | ||||
|         private void UpdateItem(BaseItemDto request, BaseItem item) | ||||
| @ -361,24 +335,25 @@ namespace MediaBrowser.Api | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             if (item is Audio song) | ||||
|             switch (item) | ||||
|             { | ||||
|                 song.Album = request.Album; | ||||
|             } | ||||
| 
 | ||||
|             if (item is MusicVideo musicVideo) | ||||
|             { | ||||
|                 musicVideo.Album = request.Album; | ||||
|             } | ||||
| 
 | ||||
|             if (item is Series series) | ||||
|             { | ||||
|                 series.Status = GetSeriesStatus(request); | ||||
| 
 | ||||
|                 if (request.AirDays != null) | ||||
|                 case Audio song: | ||||
|                     song.Album = request.Album; | ||||
|                     break; | ||||
|                 case MusicVideo musicVideo: | ||||
|                     musicVideo.Album = request.Album; | ||||
|                     break; | ||||
|                 case Series series: | ||||
|                 { | ||||
|                     series.AirDays = request.AirDays; | ||||
|                     series.AirTime = request.AirTime; | ||||
|                     series.Status = GetSeriesStatus(request); | ||||
| 
 | ||||
|                     if (request.AirDays != null) | ||||
|                     { | ||||
|                         series.AirDays = request.AirDays; | ||||
|                         series.AirTime = request.AirTime; | ||||
|                     } | ||||
| 
 | ||||
|                     break; | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
| @ -392,5 +367,81 @@ namespace MediaBrowser.Api | ||||
| 
 | ||||
|             return (SeriesStatus)Enum.Parse(typeof(SeriesStatus), item.Status, true); | ||||
|         } | ||||
| 
 | ||||
|         private DateTime NormalizeDateTime(DateTime val) | ||||
|         { | ||||
|             return DateTime.SpecifyKind(val, DateTimeKind.Utc); | ||||
|         } | ||||
| 
 | ||||
|         private List<NameValuePair> GetContentTypeOptions(bool isForItem) | ||||
|         { | ||||
|             var list = new List<NameValuePair>(); | ||||
| 
 | ||||
|             if (isForItem) | ||||
|             { | ||||
|                 list.Add(new NameValuePair | ||||
|                 { | ||||
|                     Name = "Inherit", | ||||
|                     Value = string.Empty | ||||
|                 }); | ||||
|             } | ||||
| 
 | ||||
|             list.Add(new NameValuePair | ||||
|             { | ||||
|                 Name = "Movies", | ||||
|                 Value = "movies" | ||||
|             }); | ||||
|             list.Add(new NameValuePair | ||||
|             { | ||||
|                 Name = "Music", | ||||
|                 Value = "music" | ||||
|             }); | ||||
|             list.Add(new NameValuePair | ||||
|             { | ||||
|                 Name = "Shows", | ||||
|                 Value = "tvshows" | ||||
|             }); | ||||
| 
 | ||||
|             if (!isForItem) | ||||
|             { | ||||
|                 list.Add(new NameValuePair | ||||
|                 { | ||||
|                     Name = "Books", | ||||
|                     Value = "books" | ||||
|                 }); | ||||
|             } | ||||
| 
 | ||||
|             list.Add(new NameValuePair | ||||
|             { | ||||
|                 Name = "HomeVideos", | ||||
|                 Value = "homevideos" | ||||
|             }); | ||||
|             list.Add(new NameValuePair | ||||
|             { | ||||
|                 Name = "MusicVideos", | ||||
|                 Value = "musicvideos" | ||||
|             }); | ||||
|             list.Add(new NameValuePair | ||||
|             { | ||||
|                 Name = "Photos", | ||||
|                 Value = "photos" | ||||
|             }); | ||||
| 
 | ||||
|             if (!isForItem) | ||||
|             { | ||||
|                 list.Add(new NameValuePair | ||||
|                 { | ||||
|                     Name = "MixedContent", | ||||
|                     Value = string.Empty | ||||
|                 }); | ||||
|             } | ||||
| 
 | ||||
|             foreach (var val in list) | ||||
|             { | ||||
|                 val.Name = _localizationManager.GetLocalizedString(val.Name); | ||||
|             } | ||||
| 
 | ||||
|             return list; | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										341
									
								
								Jellyfin.Api/Controllers/LibraryStructureController.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										341
									
								
								Jellyfin.Api/Controllers/LibraryStructureController.cs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,341 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.Diagnostics.CodeAnalysis; | ||||
| using System.Globalization; | ||||
| using System.IO; | ||||
| using System.Linq; | ||||
| using System.Threading; | ||||
| using System.Threading.Tasks; | ||||
| using Jellyfin.Api.Constants; | ||||
| using MediaBrowser.Common.Progress; | ||||
| using MediaBrowser.Controller; | ||||
| using MediaBrowser.Controller.Configuration; | ||||
| using MediaBrowser.Controller.Entities; | ||||
| using MediaBrowser.Controller.Library; | ||||
| using MediaBrowser.Model.Configuration; | ||||
| using MediaBrowser.Model.Entities; | ||||
| using Microsoft.AspNetCore.Authorization; | ||||
| using Microsoft.AspNetCore.Http; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| 
 | ||||
| namespace Jellyfin.Api.Controllers | ||||
| { | ||||
|     /// <summary> | ||||
|     /// The library structure controller. | ||||
|     /// </summary> | ||||
|     [Route("/Library/VirtualFolders")] | ||||
|     [Authorize(Policy = Policies.FirstTimeSetupOrElevated)] | ||||
|     public class LibraryStructureController : BaseJellyfinApiController | ||||
|     { | ||||
|         private readonly IServerApplicationPaths _appPaths; | ||||
|         private readonly ILibraryManager _libraryManager; | ||||
|         private readonly ILibraryMonitor _libraryMonitor; | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="LibraryStructureController"/> class. | ||||
|         /// </summary> | ||||
|         /// <param name="serverConfigurationManager">Instance of <see cref="IServerConfigurationManager"/> interface.</param> | ||||
|         /// <param name="libraryManager">Instance of <see cref="ILibraryManager"/> interface.</param> | ||||
|         /// <param name="libraryMonitor">Instance of <see cref="ILibraryMonitor"/> interface.</param> | ||||
|         public LibraryStructureController( | ||||
|             IServerConfigurationManager serverConfigurationManager, | ||||
|             ILibraryManager libraryManager, | ||||
|             ILibraryMonitor libraryMonitor) | ||||
|         { | ||||
|             _appPaths = serverConfigurationManager.ApplicationPaths; | ||||
|             _libraryManager = libraryManager; | ||||
|             _libraryMonitor = libraryMonitor; | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets all virtual folders. | ||||
|         /// </summary> | ||||
|         /// <param name="userId">The user id.</param> | ||||
|         /// <response code="200">Virtual folders retrieved.</response> | ||||
|         /// <returns>An <see cref="IEnumerable{VirtualFolderInfo}"/> with the virtual folders.</returns> | ||||
|         [HttpGet] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Imported from ServiceStack")] | ||||
|         public ActionResult<IEnumerable<VirtualFolderInfo>> GetVirtualFolders([FromQuery] string userId) | ||||
|         { | ||||
|             return _libraryManager.GetVirtualFolders(true); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Adds a virtual folder. | ||||
|         /// </summary> | ||||
|         /// <param name="name">The name of the virtual folder.</param> | ||||
|         /// <param name="collectionType">The type of the collection.</param> | ||||
|         /// <param name="refreshLibrary">Whether to refresh the library.</param> | ||||
|         /// <param name="paths">The paths of the virtual folder.</param> | ||||
|         /// <param name="libraryOptions">The library options.</param> | ||||
|         /// <response code="204">Folder added.</response> | ||||
|         /// <returns>A <see cref="NoContentResult"/>.</returns> | ||||
|         [HttpPost] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         public async Task<ActionResult> AddVirtualFolder( | ||||
|             [FromQuery] string name, | ||||
|             [FromQuery] string collectionType, | ||||
|             [FromQuery] bool refreshLibrary, | ||||
|             [FromQuery] string[] paths, | ||||
|             [FromQuery] LibraryOptions libraryOptions) | ||||
|         { | ||||
|             libraryOptions ??= new LibraryOptions(); | ||||
| 
 | ||||
|             if (paths != null && paths.Length > 0) | ||||
|             { | ||||
|                 libraryOptions.PathInfos = paths.Select(i => new MediaPathInfo { Path = i }).ToArray(); | ||||
|             } | ||||
| 
 | ||||
|             await _libraryManager.AddVirtualFolder(name, collectionType, libraryOptions, refreshLibrary).ConfigureAwait(false); | ||||
| 
 | ||||
|             return NoContent(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Removes a virtual folder. | ||||
|         /// </summary> | ||||
|         /// <param name="name">The name of the folder.</param> | ||||
|         /// <param name="refreshLibrary">Whether to refresh the library.</param> | ||||
|         /// <response code="204">Folder removed.</response> | ||||
|         /// <returns>A <see cref="NoContentResult"/>.</returns> | ||||
|         [HttpDelete] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         public async Task<ActionResult> RemoveVirtualFolder( | ||||
|             [FromQuery] string name, | ||||
|             [FromQuery] bool refreshLibrary) | ||||
|         { | ||||
|             await _libraryManager.RemoveVirtualFolder(name, refreshLibrary).ConfigureAwait(false); | ||||
|             return NoContent(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Renames a virtual folder. | ||||
|         /// </summary> | ||||
|         /// <param name="name">The name of the virtual folder.</param> | ||||
|         /// <param name="newName">The new name.</param> | ||||
|         /// <param name="refreshLibrary">Whether to refresh the library.</param> | ||||
|         /// <response code="204">Folder renamed.</response> | ||||
|         /// <response code="404">Library doesn't exist.</response> | ||||
|         /// <response code="409">Library already exists.</response> | ||||
|         /// <returns>A <see cref="NoContentResult"/> on success, a <see cref="NotFoundResult"/> if the library doesn't exist, a <see cref="ConflictResult"/> if the new name is already taken.</returns> | ||||
|         /// <exception cref="ArgumentNullException">The new name may not be null.</exception> | ||||
|         [HttpPost("Name")] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         [ProducesResponseType(StatusCodes.Status404NotFound)] | ||||
|         [ProducesResponseType(StatusCodes.Status409Conflict)] | ||||
|         public ActionResult RenameVirtualFolder( | ||||
|             [FromQuery] string name, | ||||
|             [FromQuery] string newName, | ||||
|             [FromQuery] bool refreshLibrary) | ||||
|         { | ||||
|             if (string.IsNullOrWhiteSpace(name)) | ||||
|             { | ||||
|                 throw new ArgumentNullException(nameof(name)); | ||||
|             } | ||||
| 
 | ||||
|             if (string.IsNullOrWhiteSpace(newName)) | ||||
|             { | ||||
|                 throw new ArgumentNullException(nameof(newName)); | ||||
|             } | ||||
| 
 | ||||
|             var rootFolderPath = _appPaths.DefaultUserViewsPath; | ||||
| 
 | ||||
|             var currentPath = Path.Combine(rootFolderPath, name); | ||||
|             var newPath = Path.Combine(rootFolderPath, newName); | ||||
| 
 | ||||
|             if (!Directory.Exists(currentPath)) | ||||
|             { | ||||
|                 return NotFound("The media collection does not exist."); | ||||
|             } | ||||
| 
 | ||||
|             if (!string.Equals(currentPath, newPath, StringComparison.OrdinalIgnoreCase) && Directory.Exists(newPath)) | ||||
|             { | ||||
|                 return Conflict($"The media library already exists at {newPath}."); | ||||
|             } | ||||
| 
 | ||||
|             _libraryMonitor.Stop(); | ||||
| 
 | ||||
|             try | ||||
|             { | ||||
|                 // Changing capitalization. Handle windows case insensitivity | ||||
|                 if (string.Equals(currentPath, newPath, StringComparison.OrdinalIgnoreCase)) | ||||
|                 { | ||||
|                     var tempPath = Path.Combine( | ||||
|                         rootFolderPath, | ||||
|                         Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture)); | ||||
|                     Directory.Move(currentPath, tempPath); | ||||
|                     currentPath = tempPath; | ||||
|                 } | ||||
| 
 | ||||
|                 Directory.Move(currentPath, newPath); | ||||
|             } | ||||
|             finally | ||||
|             { | ||||
|                 CollectionFolder.OnCollectionFolderChange(); | ||||
| 
 | ||||
|                 Task.Run(async () => | ||||
|                 { | ||||
|                     // No need to start if scanning the library because it will handle it | ||||
|                     if (refreshLibrary) | ||||
|                     { | ||||
|                         await _libraryManager.ValidateMediaLibrary(new SimpleProgress<double>(), CancellationToken.None).ConfigureAwait(false); | ||||
|                     } | ||||
|                     else | ||||
|                     { | ||||
|                         // Need to add a delay here or directory watchers may still pick up the changes | ||||
|                         // Have to block here to allow exceptions to bubble | ||||
|                         await Task.Delay(1000).ConfigureAwait(false); | ||||
|                         _libraryMonitor.Start(); | ||||
|                     } | ||||
|                 }); | ||||
|             } | ||||
| 
 | ||||
|             return NoContent(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Add a media path to a library. | ||||
|         /// </summary> | ||||
|         /// <param name="name">The name of the library.</param> | ||||
|         /// <param name="path">The path to add.</param> | ||||
|         /// <param name="pathInfo">The path info.</param> | ||||
|         /// <param name="refreshLibrary">Whether to refresh the library.</param> | ||||
|         /// <returns>A <see cref="NoContentResult"/>.</returns> | ||||
|         /// <response code="204">Media path added.</response> | ||||
|         /// <exception cref="ArgumentNullException">The name of the library may not be empty.</exception> | ||||
|         [HttpPost("Paths")] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         public ActionResult AddMediaPath( | ||||
|             [FromQuery] string name, | ||||
|             [FromQuery] string path, | ||||
|             [FromQuery] MediaPathInfo pathInfo, | ||||
|             [FromQuery] bool refreshLibrary) | ||||
|         { | ||||
|             if (string.IsNullOrWhiteSpace(name)) | ||||
|             { | ||||
|                 throw new ArgumentNullException(nameof(name)); | ||||
|             } | ||||
| 
 | ||||
|             _libraryMonitor.Stop(); | ||||
| 
 | ||||
|             try | ||||
|             { | ||||
|                 var mediaPath = pathInfo ?? new MediaPathInfo { Path = path }; | ||||
| 
 | ||||
|                 _libraryManager.AddMediaPath(name, mediaPath); | ||||
|             } | ||||
|             finally | ||||
|             { | ||||
|                 Task.Run(async () => | ||||
|                 { | ||||
|                     // No need to start if scanning the library because it will handle it | ||||
|                     if (refreshLibrary) | ||||
|                     { | ||||
|                         await _libraryManager.ValidateMediaLibrary(new SimpleProgress<double>(), CancellationToken.None).ConfigureAwait(false); | ||||
|                     } | ||||
|                     else | ||||
|                     { | ||||
|                         // Need to add a delay here or directory watchers may still pick up the changes | ||||
|                         // Have to block here to allow exceptions to bubble | ||||
|                         await Task.Delay(1000).ConfigureAwait(false); | ||||
|                         _libraryMonitor.Start(); | ||||
|                     } | ||||
|                 }); | ||||
|             } | ||||
| 
 | ||||
|             return NoContent(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Updates a media path. | ||||
|         /// </summary> | ||||
|         /// <param name="name">The name of the library.</param> | ||||
|         /// <param name="pathInfo">The path info.</param> | ||||
|         /// <returns>A <see cref="NoContentResult"/>.</returns> | ||||
|         /// <response code="204">Media path updated.</response> | ||||
|         /// <exception cref="ArgumentNullException">The name of the library may not be empty.</exception> | ||||
|         [HttpPost("Paths/Update")] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         public ActionResult UpdateMediaPath( | ||||
|             [FromQuery] string name, | ||||
|             [FromQuery] MediaPathInfo pathInfo) | ||||
|         { | ||||
|             if (string.IsNullOrWhiteSpace(name)) | ||||
|             { | ||||
|                 throw new ArgumentNullException(nameof(name)); | ||||
|             } | ||||
| 
 | ||||
|             _libraryManager.UpdateMediaPath(name, pathInfo); | ||||
|             return NoContent(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Remove a media path. | ||||
|         /// </summary> | ||||
|         /// <param name="name">The name of the library.</param> | ||||
|         /// <param name="path">The path to remove.</param> | ||||
|         /// <param name="refreshLibrary">Whether to refresh the library.</param> | ||||
|         /// <returns>A <see cref="NoContentResult"/>.</returns> | ||||
|         /// <response code="204">Media path removed.</response> | ||||
|         /// <exception cref="ArgumentNullException">The name of the library may not be empty.</exception> | ||||
|         [HttpDelete("Paths")] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         public ActionResult RemoveMediaPath( | ||||
|             [FromQuery] string name, | ||||
|             [FromQuery] string path, | ||||
|             [FromQuery] bool refreshLibrary) | ||||
|         { | ||||
|             if (string.IsNullOrWhiteSpace(name)) | ||||
|             { | ||||
|                 throw new ArgumentNullException(nameof(name)); | ||||
|             } | ||||
| 
 | ||||
|             _libraryMonitor.Stop(); | ||||
| 
 | ||||
|             try | ||||
|             { | ||||
|                 _libraryManager.RemoveMediaPath(name, path); | ||||
|             } | ||||
|             finally | ||||
|             { | ||||
|                 Task.Run(async () => | ||||
|                 { | ||||
|                     // No need to start if scanning the library because it will handle it | ||||
|                     if (refreshLibrary) | ||||
|                     { | ||||
|                         await _libraryManager.ValidateMediaLibrary(new SimpleProgress<double>(), CancellationToken.None).ConfigureAwait(false); | ||||
|                     } | ||||
|                     else | ||||
|                     { | ||||
|                         // Need to add a delay here or directory watchers may still pick up the changes | ||||
|                         // Have to block here to allow exceptions to bubble | ||||
|                         await Task.Delay(1000).ConfigureAwait(false); | ||||
|                         _libraryMonitor.Start(); | ||||
|                     } | ||||
|                 }); | ||||
|             } | ||||
| 
 | ||||
|             return NoContent(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Update library options. | ||||
|         /// </summary> | ||||
|         /// <param name="id">The library name.</param> | ||||
|         /// <param name="libraryOptions">The library options.</param> | ||||
|         /// <response code="204">Library updated.</response> | ||||
|         /// <returns>A <see cref="NoContentResult"/>.</returns> | ||||
|         [HttpPost("LibraryOptions")] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         public ActionResult UpdateLibraryOptions( | ||||
|             [FromQuery] string id, | ||||
|             [FromQuery] LibraryOptions libraryOptions) | ||||
|         { | ||||
|             var collectionFolder = (CollectionFolder)_libraryManager.GetItemById(id); | ||||
| 
 | ||||
|             collectionFolder.UpdateLibraryOptions(libraryOptions); | ||||
|             return NoContent(); | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										76
									
								
								Jellyfin.Api/Controllers/LocalizationController.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								Jellyfin.Api/Controllers/LocalizationController.cs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,76 @@ | ||||
| using System.Collections.Generic; | ||||
| using Jellyfin.Api.Constants; | ||||
| using MediaBrowser.Model.Entities; | ||||
| using MediaBrowser.Model.Globalization; | ||||
| using Microsoft.AspNetCore.Authorization; | ||||
| using Microsoft.AspNetCore.Http; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| 
 | ||||
| namespace Jellyfin.Api.Controllers | ||||
| { | ||||
|     /// <summary> | ||||
|     /// Localization controller. | ||||
|     /// </summary> | ||||
|     [Authorize(Policy = Policies.FirstTimeSetupOrElevated)] | ||||
|     public class LocalizationController : BaseJellyfinApiController | ||||
|     { | ||||
|         private readonly ILocalizationManager _localization; | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="LocalizationController"/> class. | ||||
|         /// </summary> | ||||
|         /// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param> | ||||
|         public LocalizationController(ILocalizationManager localization) | ||||
|         { | ||||
|             _localization = localization; | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets known cultures. | ||||
|         /// </summary> | ||||
|         /// <response code="200">Known cultures returned.</response> | ||||
|         /// <returns>An <see cref="OkResult"/> containing the list of cultures.</returns> | ||||
|         [HttpGet("Cultures")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         public ActionResult<IEnumerable<CultureDto>> GetCultures() | ||||
|         { | ||||
|             return Ok(_localization.GetCultures()); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets known countries. | ||||
|         /// </summary> | ||||
|         /// <response code="200">Known countries returned.</response> | ||||
|         /// <returns>An <see cref="OkResult"/> containing the list of countries.</returns> | ||||
|         [HttpGet("Countries")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         public ActionResult<IEnumerable<CountryInfo>> GetCountries() | ||||
|         { | ||||
|             return Ok(_localization.GetCountries()); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets known parental ratings. | ||||
|         /// </summary> | ||||
|         /// <response code="200">Known parental ratings returned.</response> | ||||
|         /// <returns>An <see cref="OkResult"/> containing the list of parental ratings.</returns> | ||||
|         [HttpGet("ParentalRatings")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         public ActionResult<IEnumerable<ParentalRating>> GetParentalRatings() | ||||
|         { | ||||
|             return Ok(_localization.GetParentalRatings()); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets localization options. | ||||
|         /// </summary> | ||||
|         /// <response code="200">Localization options returned.</response> | ||||
|         /// <returns>An <see cref="OkResult"/> containing the list of localization options.</returns> | ||||
|         [HttpGet("Options")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         public ActionResult<IEnumerable<LocalizationOption>> GetLocalizationOptions() | ||||
|         { | ||||
|             return Ok(_localization.GetLocalizationOptions()); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -1,11 +1,10 @@ | ||||
| #nullable enable | ||||
| #pragma warning disable CA1801 | ||||
| 
 | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.Diagnostics.CodeAnalysis; | ||||
| using System.Linq; | ||||
| using System.Threading; | ||||
| using Jellyfin.Api.Models.NotificationDtos; | ||||
| using Jellyfin.Data.Enums; | ||||
| using MediaBrowser.Controller.Library; | ||||
| using MediaBrowser.Controller.Notifications; | ||||
| using MediaBrowser.Model.Dto; | ||||
| @ -43,8 +42,12 @@ namespace Jellyfin.Api.Controllers | ||||
|         /// <param name="limit">An optional limit on the number of notifications returned.</param> | ||||
|         /// <response code="200">Notifications returned.</response> | ||||
|         /// <returns>An <see cref="OkResult"/> containing a list of notifications.</returns> | ||||
|         [HttpGet("{UserID}")] | ||||
|         [HttpGet("{userId}")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Imported from ServiceStack")] | ||||
|         [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "isRead", Justification = "Imported from ServiceStack")] | ||||
|         [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "startIndex", Justification = "Imported from ServiceStack")] | ||||
|         [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "limit", Justification = "Imported from ServiceStack")] | ||||
|         public ActionResult<NotificationResultDto> GetNotifications( | ||||
|             [FromRoute] string userId, | ||||
|             [FromQuery] bool? isRead, | ||||
| @ -60,8 +63,9 @@ namespace Jellyfin.Api.Controllers | ||||
|         /// <param name="userId">The user's ID.</param> | ||||
|         /// <response code="200">Summary of user's notifications returned.</response> | ||||
|         /// <returns>An <cref see="OkResult"/> containing a summary of the users notifications.</returns> | ||||
|         [HttpGet("{UserID}/Summary")] | ||||
|         [HttpGet("{userId}/Summary")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Imported from ServiceStack")] | ||||
|         public ActionResult<NotificationsSummaryDto> GetNotificationsSummary( | ||||
|             [FromRoute] string userId) | ||||
|         { | ||||
| @ -99,10 +103,10 @@ namespace Jellyfin.Api.Controllers | ||||
|         /// <param name="description">The description of the notification.</param> | ||||
|         /// <param name="url">The URL of the notification.</param> | ||||
|         /// <param name="level">The level of the notification.</param> | ||||
|         /// <response code="200">Notification sent.</response> | ||||
|         /// <returns>An <cref see="OkResult"/>.</returns> | ||||
|         /// <response code="204">Notification sent.</response> | ||||
|         /// <returns>A <cref see="NoContentResult"/>.</returns> | ||||
|         [HttpPost("Admin")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         public ActionResult CreateAdminNotification( | ||||
|             [FromQuery] string name, | ||||
|             [FromQuery] string description, | ||||
| @ -115,13 +119,16 @@ namespace Jellyfin.Api.Controllers | ||||
|                 Description = description, | ||||
|                 Url = url, | ||||
|                 Level = level ?? NotificationLevel.Normal, | ||||
|                 UserIds = _userManager.Users.Where(i => i.Policy.IsAdministrator).Select(i => i.Id).ToArray(), | ||||
|                 UserIds = _userManager.Users | ||||
|                     .Where(user => user.HasPermission(PermissionKind.IsAdministrator)) | ||||
|                     .Select(user => user.Id) | ||||
|                     .ToArray(), | ||||
|                 Date = DateTime.UtcNow, | ||||
|             }; | ||||
| 
 | ||||
|             _notificationManager.SendNotification(notification, CancellationToken.None); | ||||
| 
 | ||||
|             return Ok(); | ||||
|             return NoContent(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
| @ -129,15 +136,17 @@ namespace Jellyfin.Api.Controllers | ||||
|         /// </summary> | ||||
|         /// <param name="userId">The userID.</param> | ||||
|         /// <param name="ids">A comma-separated list of the IDs of notifications which should be set as read.</param> | ||||
|         /// <response code="200">Notifications set as read.</response> | ||||
|         /// <returns>An <cref see="OkResult"/>.</returns> | ||||
|         [HttpPost("{UserID}/Read")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         /// <response code="204">Notifications set as read.</response> | ||||
|         /// <returns>A <cref see="NoContentResult"/>.</returns> | ||||
|         [HttpPost("{userId}/Read")] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Imported from ServiceStack")] | ||||
|         [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "ids", Justification = "Imported from ServiceStack")] | ||||
|         public ActionResult SetRead( | ||||
|             [FromRoute] string userId, | ||||
|             [FromQuery] string ids) | ||||
|         { | ||||
|             return Ok(); | ||||
|             return NoContent(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
| @ -145,15 +154,17 @@ namespace Jellyfin.Api.Controllers | ||||
|         /// </summary> | ||||
|         /// <param name="userId">The userID.</param> | ||||
|         /// <param name="ids">A comma-separated list of the IDs of notifications which should be set as unread.</param> | ||||
|         /// <response code="200">Notifications set as unread.</response> | ||||
|         /// <returns>An <cref see="OkResult"/>.</returns> | ||||
|         [HttpPost("{UserID}/Unread")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         /// <response code="204">Notifications set as unread.</response> | ||||
|         /// <returns>A <cref see="NoContentResult"/>.</returns> | ||||
|         [HttpPost("{userId}/Unread")] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Imported from ServiceStack")] | ||||
|         [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "ids", Justification = "Imported from ServiceStack")] | ||||
|         public ActionResult SetUnread( | ||||
|             [FromRoute] string userId, | ||||
|             [FromQuery] string ids) | ||||
|         { | ||||
|             return Ok(); | ||||
|             return NoContent(); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -1,5 +1,3 @@ | ||||
| #nullable enable | ||||
| 
 | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.ComponentModel.DataAnnotations; | ||||
| @ -18,7 +16,7 @@ namespace Jellyfin.Api.Controllers | ||||
|     /// Package Controller. | ||||
|     /// </summary> | ||||
|     [Route("Packages")] | ||||
|     [Authorize] | ||||
|     [Authorize(Policy = Policies.DefaultAuthorization)] | ||||
|     public class PackageController : BaseJellyfinApiController | ||||
|     { | ||||
|         private readonly IInstallationManager _installationManager; | ||||
| @ -37,9 +35,10 @@ namespace Jellyfin.Api.Controllers | ||||
|         /// </summary> | ||||
|         /// <param name="name">The name of the package.</param> | ||||
|         /// <param name="assemblyGuid">The GUID of the associated assembly.</param> | ||||
|         /// <response code="200">Package retrieved.</response> | ||||
|         /// <returns>A <see cref="PackageInfo"/> containing package information.</returns> | ||||
|         [HttpGet("/{Name}")] | ||||
|         [ProducesResponseType(typeof(PackageInfo), StatusCodes.Status200OK)] | ||||
|         [HttpGet("/{name}")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         public async Task<ActionResult<PackageInfo>> GetPackageInfo( | ||||
|             [FromRoute] [Required] string name, | ||||
|             [FromQuery] string? assemblyGuid) | ||||
| @ -56,9 +55,10 @@ namespace Jellyfin.Api.Controllers | ||||
|         /// <summary> | ||||
|         /// Gets available packages. | ||||
|         /// </summary> | ||||
|         /// <response code="200">Available packages returned.</response> | ||||
|         /// <returns>An <see cref="PackageInfo"/> containing available packages information.</returns> | ||||
|         [HttpGet] | ||||
|         [ProducesResponseType(typeof(PackageInfo[]), StatusCodes.Status200OK)] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         public async Task<IEnumerable<PackageInfo>> GetPackages() | ||||
|         { | ||||
|             IEnumerable<PackageInfo> packages = await _installationManager.GetAvailablePackages().ConfigureAwait(false); | ||||
| @ -72,11 +72,11 @@ namespace Jellyfin.Api.Controllers | ||||
|         /// <param name="name">Package name.</param> | ||||
|         /// <param name="assemblyGuid">GUID of the associated assembly.</param> | ||||
|         /// <param name="version">Optional version. Defaults to latest version.</param> | ||||
|         /// <response code="200">Package found.</response> | ||||
|         /// <response code="204">Package found.</response> | ||||
|         /// <response code="404">Package not found.</response> | ||||
|         /// <returns>An <see cref="OkResult"/> on success, or a <see cref="NotFoundResult"/> if the package could not be found.</returns> | ||||
|         [HttpPost("/Installed/{Name}")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         /// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the package could not be found.</returns> | ||||
|         [HttpPost("/Installed/{name}")] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         [ProducesResponseType(StatusCodes.Status404NotFound)] | ||||
|         [Authorize(Policy = Policies.RequiresElevation)] | ||||
|         public async Task<ActionResult> InstallPackage( | ||||
| @ -98,23 +98,23 @@ namespace Jellyfin.Api.Controllers | ||||
| 
 | ||||
|             await _installationManager.InstallPackage(package).ConfigureAwait(false); | ||||
| 
 | ||||
|             return Ok(); | ||||
|             return NoContent(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Cancels a package installation. | ||||
|         /// </summary> | ||||
|         /// <param name="id">Installation Id.</param> | ||||
|         /// <response code="200">Installation cancelled.</response> | ||||
|         /// <returns>An <see cref="OkResult"/> on successfully cancelling a package installation.</returns> | ||||
|         [HttpDelete("/Installing/{id}")] | ||||
|         /// <param name="packageId">Installation Id.</param> | ||||
|         /// <response code="204">Installation cancelled.</response> | ||||
|         /// <returns>A <see cref="NoContentResult"/> on successfully cancelling a package installation.</returns> | ||||
|         [HttpDelete("/Installing/{packageId}")] | ||||
|         [Authorize(Policy = Policies.RequiresElevation)] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         public IActionResult CancelPackageInstallation( | ||||
|             [FromRoute] [Required] string id) | ||||
|             [FromRoute] [Required] Guid packageId) | ||||
|         { | ||||
|             _installationManager.CancelInstallation(new Guid(id)); | ||||
| 
 | ||||
|             return Ok(); | ||||
|             _installationManager.CancelInstallation(packageId); | ||||
|             return NoContent(); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
							
								
								
									
										199
									
								
								Jellyfin.Api/Controllers/PlaylistsController.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										199
									
								
								Jellyfin.Api/Controllers/PlaylistsController.cs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,199 @@ | ||||
| using System; | ||||
| using System.Linq; | ||||
| using System.Threading.Tasks; | ||||
| using Jellyfin.Api.Constants; | ||||
| using Jellyfin.Api.Extensions; | ||||
| using Jellyfin.Api.Helpers; | ||||
| using Jellyfin.Api.Models.PlaylistDtos; | ||||
| using MediaBrowser.Controller.Dto; | ||||
| using MediaBrowser.Controller.Library; | ||||
| using MediaBrowser.Controller.Playlists; | ||||
| using MediaBrowser.Model.Dto; | ||||
| using MediaBrowser.Model.Playlists; | ||||
| 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 | ||||
| { | ||||
|     /// <summary> | ||||
|     /// Playlists controller. | ||||
|     /// </summary> | ||||
|     [Authorize(Policy = Policies.DefaultAuthorization)] | ||||
|     public class PlaylistsController : BaseJellyfinApiController | ||||
|     { | ||||
|         private readonly IPlaylistManager _playlistManager; | ||||
|         private readonly IDtoService _dtoService; | ||||
|         private readonly IUserManager _userManager; | ||||
|         private readonly ILibraryManager _libraryManager; | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="PlaylistsController"/> class. | ||||
|         /// </summary> | ||||
|         /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param> | ||||
|         /// <param name="playlistManager">Instance of the <see cref="IPlaylistManager"/> interface.</param> | ||||
|         /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param> | ||||
|         /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param> | ||||
|         public PlaylistsController( | ||||
|             IDtoService dtoService, | ||||
|             IPlaylistManager playlistManager, | ||||
|             IUserManager userManager, | ||||
|             ILibraryManager libraryManager) | ||||
|         { | ||||
|             _dtoService = dtoService; | ||||
|             _playlistManager = playlistManager; | ||||
|             _userManager = userManager; | ||||
|             _libraryManager = libraryManager; | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Creates a new playlist. | ||||
|         /// </summary> | ||||
|         /// <param name="createPlaylistRequest">The create playlist payload.</param> | ||||
|         /// <returns> | ||||
|         /// A <see cref="Task" /> that represents the asynchronous operation to create a playlist. | ||||
|         /// The task result contains an <see cref="OkResult"/> indicating success. | ||||
|         /// </returns> | ||||
|         [HttpPost] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         public async Task<ActionResult<PlaylistCreationResult>> CreatePlaylist( | ||||
|             [FromBody, BindRequired] CreatePlaylistDto createPlaylistRequest) | ||||
|         { | ||||
|             Guid[] idGuidArray = RequestHelpers.GetGuids(createPlaylistRequest.Ids); | ||||
|             var result = await _playlistManager.CreatePlaylist(new PlaylistCreationRequest | ||||
|             { | ||||
|                 Name = createPlaylistRequest.Name, | ||||
|                 ItemIdList = idGuidArray, | ||||
|                 UserId = createPlaylistRequest.UserId, | ||||
|                 MediaType = createPlaylistRequest.MediaType | ||||
|             }).ConfigureAwait(false); | ||||
| 
 | ||||
|             return result; | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Adds items to a playlist. | ||||
|         /// </summary> | ||||
|         /// <param name="playlistId">The playlist id.</param> | ||||
|         /// <param name="ids">Item id, comma delimited.</param> | ||||
|         /// <param name="userId">The userId.</param> | ||||
|         /// <response code="204">Items added to playlist.</response> | ||||
|         /// <returns>An <see cref="NoContentResult"/> on success.</returns> | ||||
|         [HttpPost("{playlistId}/Items")] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         public ActionResult AddToPlaylist( | ||||
|             [FromRoute] string playlistId, | ||||
|             [FromQuery] string ids, | ||||
|             [FromQuery] Guid userId) | ||||
|         { | ||||
|             _playlistManager.AddToPlaylist(playlistId, RequestHelpers.GetGuids(ids), userId); | ||||
|             return NoContent(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Moves a playlist item. | ||||
|         /// </summary> | ||||
|         /// <param name="playlistId">The playlist id.</param> | ||||
|         /// <param name="itemId">The item id.</param> | ||||
|         /// <param name="newIndex">The new index.</param> | ||||
|         /// <response code="204">Item moved to new index.</response> | ||||
|         /// <returns>An <see cref="NoContentResult"/> on success.</returns> | ||||
|         [HttpPost("{playlistId}/Items/{itemId}/Move/{newIndex}")] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         public ActionResult MoveItem( | ||||
|             [FromRoute] string playlistId, | ||||
|             [FromRoute] string itemId, | ||||
|             [FromRoute] int newIndex) | ||||
|         { | ||||
|             _playlistManager.MoveItem(playlistId, itemId, newIndex); | ||||
|             return NoContent(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Removes items from a playlist. | ||||
|         /// </summary> | ||||
|         /// <param name="playlistId">The playlist id.</param> | ||||
|         /// <param name="entryIds">The item ids, comma delimited.</param> | ||||
|         /// <response code="204">Items removed.</response> | ||||
|         /// <returns>An <see cref="NoContentResult"/> on success.</returns> | ||||
|         [HttpDelete("{playlistId}/Items")] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         public ActionResult RemoveFromPlaylist([FromRoute] string playlistId, [FromQuery] string entryIds) | ||||
|         { | ||||
|             _playlistManager.RemoveFromPlaylist(playlistId, RequestHelpers.Split(entryIds, ',', true)); | ||||
|             return NoContent(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets the original items of a playlist. | ||||
|         /// </summary> | ||||
|         /// <param name="playlistId">The playlist id.</param> | ||||
|         /// <param name="userId">User id.</param> | ||||
|         /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param> | ||||
|         /// <param name="limit">Optional. The maximum number of records to return.</param> | ||||
|         /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param> | ||||
|         /// <param name="enableImages">Optional. Include image information in output.</param> | ||||
|         /// <param name="enableUserData">Optional. Include user data.</param> | ||||
|         /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param> | ||||
|         /// <param name="enableImageTypes">Optional. The image types to include in the output.</param> | ||||
|         /// <response code="200">Original playlist returned.</response> | ||||
|         /// <response code="404">Playlist not found.</response> | ||||
|         /// <returns>The original playlist items.</returns> | ||||
|         [HttpGet("{playlistId}/Items")] | ||||
|         public ActionResult<QueryResult<BaseItemDto>> GetPlaylistItems( | ||||
|             [FromRoute] Guid playlistId, | ||||
|             [FromRoute] Guid userId, | ||||
|             [FromRoute] int? startIndex, | ||||
|             [FromRoute] int? limit, | ||||
|             [FromRoute] string fields, | ||||
|             [FromRoute] bool? enableImages, | ||||
|             [FromRoute] bool? enableUserData, | ||||
|             [FromRoute] int? imageTypeLimit, | ||||
|             [FromRoute] string enableImageTypes) | ||||
|         { | ||||
|             var playlist = (Playlist)_libraryManager.GetItemById(playlistId); | ||||
|             if (playlist == null) | ||||
|             { | ||||
|                 return NotFound(); | ||||
|             } | ||||
| 
 | ||||
|             var user = !userId.Equals(Guid.Empty) ? _userManager.GetUserById(userId) : null; | ||||
| 
 | ||||
|             var items = playlist.GetManageableItems().ToArray(); | ||||
| 
 | ||||
|             var count = items.Length; | ||||
| 
 | ||||
|             if (startIndex.HasValue) | ||||
|             { | ||||
|                 items = items.Skip(startIndex.Value).ToArray(); | ||||
|             } | ||||
| 
 | ||||
|             if (limit.HasValue) | ||||
|             { | ||||
|                 items = items.Take(limit.Value).ToArray(); | ||||
|             } | ||||
| 
 | ||||
|             var dtoOptions = new DtoOptions() | ||||
|                 .AddItemFields(fields) | ||||
|                 .AddClientFields(Request) | ||||
|                 .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); | ||||
| 
 | ||||
|             var dtos = _dtoService.GetBaseItemDtos(items.Select(i => i.Item2).ToList(), dtoOptions, user); | ||||
| 
 | ||||
|             for (int index = 0; index < dtos.Count; index++) | ||||
|             { | ||||
|                 dtos[index].PlaylistItemId = items[index].Item1.Id; | ||||
|             } | ||||
| 
 | ||||
|             var result = new QueryResult<BaseItemDto> | ||||
|             { | ||||
|                 Items = dtos, | ||||
|                 TotalRecordCount = count | ||||
|             }; | ||||
| 
 | ||||
|             return result; | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										200
									
								
								Jellyfin.Api/Controllers/PluginsController.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										200
									
								
								Jellyfin.Api/Controllers/PluginsController.cs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,200 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.Diagnostics.CodeAnalysis; | ||||
| using System.Linq; | ||||
| using System.Text.Json; | ||||
| using System.Threading.Tasks; | ||||
| using Jellyfin.Api.Constants; | ||||
| using Jellyfin.Api.Models.PluginDtos; | ||||
| using MediaBrowser.Common; | ||||
| using MediaBrowser.Common.Plugins; | ||||
| using MediaBrowser.Common.Updates; | ||||
| using MediaBrowser.Model.Plugins; | ||||
| using Microsoft.AspNetCore.Authorization; | ||||
| using Microsoft.AspNetCore.Http; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| using Microsoft.AspNetCore.Mvc.ModelBinding; | ||||
| 
 | ||||
| namespace Jellyfin.Api.Controllers | ||||
| { | ||||
|     /// <summary> | ||||
|     /// Plugins controller. | ||||
|     /// </summary> | ||||
|     [Authorize(Policy = Policies.DefaultAuthorization)] | ||||
|     public class PluginsController : BaseJellyfinApiController | ||||
|     { | ||||
|         private readonly IApplicationHost _appHost; | ||||
|         private readonly IInstallationManager _installationManager; | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="PluginsController"/> class. | ||||
|         /// </summary> | ||||
|         /// <param name="appHost">Instance of the <see cref="IApplicationHost"/> interface.</param> | ||||
|         /// <param name="installationManager">Instance of the <see cref="IInstallationManager"/> interface.</param> | ||||
|         public PluginsController( | ||||
|             IApplicationHost appHost, | ||||
|             IInstallationManager installationManager) | ||||
|         { | ||||
|             _appHost = appHost; | ||||
|             _installationManager = installationManager; | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets a list of currently installed plugins. | ||||
|         /// </summary> | ||||
|         /// <param name="isAppStoreEnabled">Optional. Unused.</param> | ||||
|         /// <response code="200">Installed plugins returned.</response> | ||||
|         /// <returns>List of currently installed plugins.</returns> | ||||
|         [HttpGet] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "isAppStoreEnabled", Justification = "Imported from ServiceStack")] | ||||
|         public ActionResult<IEnumerable<PluginInfo>> GetPlugins([FromRoute] bool? isAppStoreEnabled) | ||||
|         { | ||||
|             return Ok(_appHost.Plugins.OrderBy(p => p.Name).Select(p => p.GetPluginInfo())); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Uninstalls a plugin. | ||||
|         /// </summary> | ||||
|         /// <param name="pluginId">Plugin id.</param> | ||||
|         /// <response code="204">Plugin uninstalled.</response> | ||||
|         /// <response code="404">Plugin not found.</response> | ||||
|         /// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the file could not be found.</returns> | ||||
|         [HttpDelete("{pluginId}")] | ||||
|         [Authorize(Policy = Policies.RequiresElevation)] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         [ProducesResponseType(StatusCodes.Status404NotFound)] | ||||
|         public ActionResult UninstallPlugin([FromRoute] Guid pluginId) | ||||
|         { | ||||
|             var plugin = _appHost.Plugins.FirstOrDefault(p => p.Id == pluginId); | ||||
|             if (plugin == null) | ||||
|             { | ||||
|                 return NotFound(); | ||||
|             } | ||||
| 
 | ||||
|             _installationManager.UninstallPlugin(plugin); | ||||
|             return NoContent(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets plugin configuration. | ||||
|         /// </summary> | ||||
|         /// <param name="pluginId">Plugin id.</param> | ||||
|         /// <response code="200">Plugin configuration returned.</response> | ||||
|         /// <response code="404">Plugin not found or plugin configuration not found.</response> | ||||
|         /// <returns>Plugin configuration.</returns> | ||||
|         [HttpGet("{pluginId}/Configuration")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         [ProducesResponseType(StatusCodes.Status404NotFound)] | ||||
|         public ActionResult<BasePluginConfiguration> GetPluginConfiguration([FromRoute] Guid pluginId) | ||||
|         { | ||||
|             if (!(_appHost.Plugins.FirstOrDefault(p => p.Id == pluginId) is IHasPluginConfiguration plugin)) | ||||
|             { | ||||
|                 return NotFound(); | ||||
|             } | ||||
| 
 | ||||
|             return plugin.Configuration; | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Updates plugin configuration. | ||||
|         /// </summary> | ||||
|         /// <remarks> | ||||
|         /// Accepts plugin configuration as JSON body. | ||||
|         /// </remarks> | ||||
|         /// <param name="pluginId">Plugin id.</param> | ||||
|         /// <response code="204">Plugin configuration updated.</response> | ||||
|         /// <response code="404">Plugin not found or plugin does not have configuration.</response> | ||||
|         /// <returns> | ||||
|         /// A <see cref="Task" /> that represents the asynchronous operation to update plugin configuration. | ||||
|         ///    The task result contains an <see cref="NoContentResult"/> indicating success, or <see cref="NotFoundResult"/> | ||||
|         ///    when plugin not found or plugin doesn't have configuration. | ||||
|         /// </returns> | ||||
|         [HttpPost("{pluginId}/Configuration")] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         [ProducesResponseType(StatusCodes.Status404NotFound)] | ||||
|         public async Task<ActionResult> UpdatePluginConfiguration([FromRoute] Guid pluginId) | ||||
|         { | ||||
|             if (!(_appHost.Plugins.FirstOrDefault(p => p.Id == pluginId) is IHasPluginConfiguration plugin)) | ||||
|             { | ||||
|                 return NotFound(); | ||||
|             } | ||||
| 
 | ||||
|             var configuration = (BasePluginConfiguration)await JsonSerializer.DeserializeAsync(Request.Body, plugin.ConfigurationType) | ||||
|                 .ConfigureAwait(false); | ||||
| 
 | ||||
|             plugin.UpdateConfiguration(configuration); | ||||
|             return NoContent(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Get plugin security info. | ||||
|         /// </summary> | ||||
|         /// <response code="200">Plugin security info returned.</response> | ||||
|         /// <returns>Plugin security info.</returns> | ||||
|         [Obsolete("This endpoint should not be used.")] | ||||
|         [HttpGet("SecurityInfo")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         public ActionResult<PluginSecurityInfo> GetPluginSecurityInfo() | ||||
|         { | ||||
|             return new PluginSecurityInfo | ||||
|             { | ||||
|                 IsMbSupporter = true, | ||||
|                 SupporterKey = "IAmTotallyLegit" | ||||
|             }; | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Updates plugin security info. | ||||
|         /// </summary> | ||||
|         /// <param name="pluginSecurityInfo">Plugin security info.</param> | ||||
|         /// <response code="204">Plugin security info updated.</response> | ||||
|         /// <returns>An <see cref="NoContentResult"/>.</returns> | ||||
|         [Obsolete("This endpoint should not be used.")] | ||||
|         [HttpPost("SecurityInfo")] | ||||
|         [Authorize(Policy = Policies.RequiresElevation)] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         public ActionResult UpdatePluginSecurityInfo([FromBody, BindRequired] PluginSecurityInfo pluginSecurityInfo) | ||||
|         { | ||||
|             return NoContent(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets registration status for a feature. | ||||
|         /// </summary> | ||||
|         /// <param name="name">Feature name.</param> | ||||
|         /// <response code="200">Registration status returned.</response> | ||||
|         /// <returns>Mb registration record.</returns> | ||||
|         [Obsolete("This endpoint should not be used.")] | ||||
|         [HttpPost("RegistrationRecords/{name}")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         public ActionResult<MBRegistrationRecord> GetRegistrationStatus([FromRoute] string name) | ||||
|         { | ||||
|             return new MBRegistrationRecord | ||||
|             { | ||||
|                 IsRegistered = true, | ||||
|                 RegChecked = true, | ||||
|                 TrialVersion = false, | ||||
|                 IsValid = true, | ||||
|                 RegError = false | ||||
|             }; | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets registration status for a feature. | ||||
|         /// </summary> | ||||
|         /// <param name="name">Feature name.</param> | ||||
|         /// <response code="501">Not implemented.</response> | ||||
|         /// <returns>Not Implemented.</returns> | ||||
|         /// <exception cref="NotImplementedException">This endpoint is not implemented.</exception> | ||||
|         [Obsolete("Paid plugins are not supported")] | ||||
|         [HttpGet("/Registrations/{name}")] | ||||
|         [ProducesResponseType(StatusCodes.Status501NotImplemented)] | ||||
|         public ActionResult GetRegistration([FromRoute] string name) | ||||
|         { | ||||
|             // TODO Once we have proper apps and plugins and decide to break compatibility with paid plugins, | ||||
|             // delete all these registration endpoints. They are only kept for compatibility. | ||||
|             throw new NotImplementedException(); | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										266
									
								
								Jellyfin.Api/Controllers/RemoteImageController.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										266
									
								
								Jellyfin.Api/Controllers/RemoteImageController.cs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,266 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.IO; | ||||
| using System.Linq; | ||||
| using System.Net.Mime; | ||||
| using System.Threading; | ||||
| using System.Threading.Tasks; | ||||
| using Jellyfin.Api.Constants; | ||||
| using MediaBrowser.Common.Extensions; | ||||
| using MediaBrowser.Common.Net; | ||||
| using MediaBrowser.Controller; | ||||
| using MediaBrowser.Controller.Library; | ||||
| using MediaBrowser.Controller.Providers; | ||||
| using MediaBrowser.Model.Entities; | ||||
| using MediaBrowser.Model.IO; | ||||
| using MediaBrowser.Model.Net; | ||||
| using MediaBrowser.Model.Providers; | ||||
| using Microsoft.AspNetCore.Authorization; | ||||
| using Microsoft.AspNetCore.Http; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| using Microsoft.AspNetCore.Mvc.ModelBinding; | ||||
| 
 | ||||
| namespace Jellyfin.Api.Controllers | ||||
| { | ||||
|     /// <summary> | ||||
|     /// Remote Images Controller. | ||||
|     /// </summary> | ||||
|     [Route("Images")] | ||||
|     [Authorize(Policy = Policies.DefaultAuthorization)] | ||||
|     public class RemoteImageController : BaseJellyfinApiController | ||||
|     { | ||||
|         private readonly IProviderManager _providerManager; | ||||
|         private readonly IServerApplicationPaths _applicationPaths; | ||||
|         private readonly IHttpClient _httpClient; | ||||
|         private readonly ILibraryManager _libraryManager; | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="RemoteImageController"/> class. | ||||
|         /// </summary> | ||||
|         /// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param> | ||||
|         /// <param name="applicationPaths">Instance of the <see cref="IServerApplicationPaths"/> interface.</param> | ||||
|         /// <param name="httpClient">Instance of the <see cref="IHttpClient"/> interface.</param> | ||||
|         /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param> | ||||
|         public RemoteImageController( | ||||
|             IProviderManager providerManager, | ||||
|             IServerApplicationPaths applicationPaths, | ||||
|             IHttpClient httpClient, | ||||
|             ILibraryManager libraryManager) | ||||
|         { | ||||
|             _providerManager = providerManager; | ||||
|             _applicationPaths = applicationPaths; | ||||
|             _httpClient = httpClient; | ||||
|             _libraryManager = libraryManager; | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets available remote images for an item. | ||||
|         /// </summary> | ||||
|         /// <param name="itemId">Item Id.</param> | ||||
|         /// <param name="type">The image type.</param> | ||||
|         /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param> | ||||
|         /// <param name="limit">Optional. The maximum number of records to return.</param> | ||||
|         /// <param name="providerName">Optional. The image provider to use.</param> | ||||
|         /// <param name="includeAllLanguages">Optional. Include all languages.</param> | ||||
|         /// <response code="200">Remote Images returned.</response> | ||||
|         /// <response code="404">Item not found.</response> | ||||
|         /// <returns>Remote Image Result.</returns> | ||||
|         [HttpGet("{itemId}/RemoteImages")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         [ProducesResponseType(StatusCodes.Status404NotFound)] | ||||
|         public async Task<ActionResult<RemoteImageResult>> GetRemoteImages( | ||||
|             [FromRoute] Guid itemId, | ||||
|             [FromQuery] ImageType? type, | ||||
|             [FromQuery] int? startIndex, | ||||
|             [FromQuery] int? limit, | ||||
|             [FromQuery] string providerName, | ||||
|             [FromQuery] bool includeAllLanguages) | ||||
|         { | ||||
|             var item = _libraryManager.GetItemById(itemId); | ||||
|             if (item == null) | ||||
|             { | ||||
|                 return NotFound(); | ||||
|             } | ||||
| 
 | ||||
|             var images = await _providerManager.GetAvailableRemoteImages( | ||||
|                     item, | ||||
|                     new RemoteImageQuery(providerName) | ||||
|                     { | ||||
|                         IncludeAllLanguages = includeAllLanguages, | ||||
|                         IncludeDisabledProviders = true, | ||||
|                         ImageType = type | ||||
|                     }, CancellationToken.None) | ||||
|                 .ConfigureAwait(false); | ||||
| 
 | ||||
|             var imageArray = images.ToArray(); | ||||
|             var allProviders = _providerManager.GetRemoteImageProviderInfo(item); | ||||
|             if (type.HasValue) | ||||
|             { | ||||
|                 allProviders = allProviders.Where(o => o.SupportedImages.Contains(type.Value)); | ||||
|             } | ||||
| 
 | ||||
|             var result = new RemoteImageResult | ||||
|             { | ||||
|                 TotalRecordCount = imageArray.Length, | ||||
|                 Providers = allProviders.Select(o => o.Name) | ||||
|                     .Distinct(StringComparer.OrdinalIgnoreCase) | ||||
|                     .ToArray() | ||||
|             }; | ||||
| 
 | ||||
|             if (startIndex.HasValue) | ||||
|             { | ||||
|                 imageArray = imageArray.Skip(startIndex.Value).ToArray(); | ||||
|             } | ||||
| 
 | ||||
|             if (limit.HasValue) | ||||
|             { | ||||
|                 imageArray = imageArray.Take(limit.Value).ToArray(); | ||||
|             } | ||||
| 
 | ||||
|             result.Images = imageArray; | ||||
|             return result; | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets available remote image providers for an item. | ||||
|         /// </summary> | ||||
|         /// <param name="itemId">Item Id.</param> | ||||
|         /// <response code="200">Returned remote image providers.</response> | ||||
|         /// <response code="404">Item not found.</response> | ||||
|         /// <returns>List of remote image providers.</returns> | ||||
|         [HttpGet("{itemId}/RemoteImages/Providers")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         [ProducesResponseType(StatusCodes.Status404NotFound)] | ||||
|         public ActionResult<IEnumerable<ImageProviderInfo>> GetRemoteImageProviders([FromRoute] Guid itemId) | ||||
|         { | ||||
|             var item = _libraryManager.GetItemById(itemId); | ||||
|             if (item == null) | ||||
|             { | ||||
|                 return NotFound(); | ||||
|             } | ||||
| 
 | ||||
|             return Ok(_providerManager.GetRemoteImageProviderInfo(item)); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets a remote image. | ||||
|         /// </summary> | ||||
|         /// <param name="imageUrl">The image url.</param> | ||||
|         /// <response code="200">Remote image returned.</response> | ||||
|         /// <response code="404">Remote image not found.</response> | ||||
|         /// <returns>Image Stream.</returns> | ||||
|         [HttpGet("Remote")] | ||||
|         [Produces(MediaTypeNames.Application.Octet)] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         [ProducesResponseType(StatusCodes.Status404NotFound)] | ||||
|         public async Task<ActionResult<FileStreamResult>> GetRemoteImage([FromQuery, BindRequired] string imageUrl) | ||||
|         { | ||||
|             var urlHash = imageUrl.GetMD5(); | ||||
|             var pointerCachePath = GetFullCachePath(urlHash.ToString()); | ||||
| 
 | ||||
|             string? contentPath = null; | ||||
|             var hasFile = false; | ||||
| 
 | ||||
|             try | ||||
|             { | ||||
|                 contentPath = await System.IO.File.ReadAllTextAsync(pointerCachePath).ConfigureAwait(false); | ||||
|                 if (System.IO.File.Exists(contentPath)) | ||||
|                 { | ||||
|                     hasFile = true; | ||||
|                 } | ||||
|             } | ||||
|             catch (FileNotFoundException) | ||||
|             { | ||||
|                 // The file isn't cached yet | ||||
|             } | ||||
|             catch (IOException) | ||||
|             { | ||||
|                 // The file isn't cached yet | ||||
|             } | ||||
| 
 | ||||
|             if (!hasFile) | ||||
|             { | ||||
|                 await DownloadImage(imageUrl, urlHash, pointerCachePath).ConfigureAwait(false); | ||||
|                 contentPath = await System.IO.File.ReadAllTextAsync(pointerCachePath).ConfigureAwait(false); | ||||
|             } | ||||
| 
 | ||||
|             if (string.IsNullOrEmpty(contentPath)) | ||||
|             { | ||||
|                 return NotFound(); | ||||
|             } | ||||
| 
 | ||||
|             var contentType = MimeTypes.GetMimeType(contentPath); | ||||
|             return File(System.IO.File.OpenRead(contentPath), contentType); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Downloads a remote image for an item. | ||||
|         /// </summary> | ||||
|         /// <param name="itemId">Item Id.</param> | ||||
|         /// <param name="type">The image type.</param> | ||||
|         /// <param name="imageUrl">The image url.</param> | ||||
|         /// <response code="204">Remote image downloaded.</response> | ||||
|         /// <response code="404">Remote image not found.</response> | ||||
|         /// <returns>Download status.</returns> | ||||
|         [HttpPost("{itemId}/RemoteImages/Download")] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         [ProducesResponseType(StatusCodes.Status404NotFound)] | ||||
|         public async Task<ActionResult> DownloadRemoteImage( | ||||
|             [FromRoute] Guid itemId, | ||||
|             [FromQuery, BindRequired] ImageType type, | ||||
|             [FromQuery] string imageUrl) | ||||
|         { | ||||
|             var item = _libraryManager.GetItemById(itemId); | ||||
|             if (item == null) | ||||
|             { | ||||
|                 return NotFound(); | ||||
|             } | ||||
| 
 | ||||
|             await _providerManager.SaveImage(item, imageUrl, type, null, CancellationToken.None) | ||||
|                 .ConfigureAwait(false); | ||||
| 
 | ||||
|             item.UpdateToRepository(ItemUpdateType.ImageUpdate, CancellationToken.None); | ||||
|             return NoContent(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets the full cache path. | ||||
|         /// </summary> | ||||
|         /// <param name="filename">The filename.</param> | ||||
|         /// <returns>System.String.</returns> | ||||
|         private string GetFullCachePath(string filename) | ||||
|         { | ||||
|             return Path.Combine(_applicationPaths.CachePath, "remote-images", filename.Substring(0, 1), filename); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Downloads the image. | ||||
|         /// </summary> | ||||
|         /// <param name="url">The URL.</param> | ||||
|         /// <param name="urlHash">The URL hash.</param> | ||||
|         /// <param name="pointerCachePath">The pointer cache path.</param> | ||||
|         /// <returns>Task.</returns> | ||||
|         private async Task DownloadImage(string url, Guid urlHash, string pointerCachePath) | ||||
|         { | ||||
|             using var result = await _httpClient.GetResponse(new HttpRequestOptions | ||||
|             { | ||||
|                 Url = url, | ||||
|                 BufferContent = false | ||||
|             }).ConfigureAwait(false); | ||||
|             var ext = result.ContentType.Split('/').Last(); | ||||
| 
 | ||||
|             var fullCachePath = GetFullCachePath(urlHash + "." + ext); | ||||
| 
 | ||||
|             Directory.CreateDirectory(Path.GetDirectoryName(fullCachePath)); | ||||
|             await using (var stream = result.Content) | ||||
|             { | ||||
|                 await using var fileStream = new FileStream(fullCachePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true); | ||||
|                 await stream.CopyToAsync(fileStream).ConfigureAwait(false); | ||||
|             } | ||||
| 
 | ||||
|             Directory.CreateDirectory(Path.GetDirectoryName(pointerCachePath)); | ||||
|             await System.IO.File.WriteAllTextAsync(pointerCachePath, fullCachePath, CancellationToken.None) | ||||
|                 .ConfigureAwait(false); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -3,6 +3,7 @@ using System.ComponentModel; | ||||
| using System.ComponentModel.DataAnnotations; | ||||
| using System.Globalization; | ||||
| using System.Linq; | ||||
| using Jellyfin.Api.Constants; | ||||
| using Jellyfin.Api.Helpers; | ||||
| using MediaBrowser.Controller.Drawing; | ||||
| using MediaBrowser.Controller.Dto; | ||||
| @ -23,7 +24,7 @@ namespace Jellyfin.Api.Controllers | ||||
|     /// Search controller. | ||||
|     /// </summary> | ||||
|     [Route("/Search/Hints")] | ||||
|     [Authorize] | ||||
|     [Authorize(Policy = Policies.DefaultAuthorization)] | ||||
|     public class SearchController : BaseJellyfinApiController | ||||
|     { | ||||
|         private readonly ISearchEngine _searchEngine; | ||||
|  | ||||
							
								
								
									
										475
									
								
								Jellyfin.Api/Controllers/SessionController.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										475
									
								
								Jellyfin.Api/Controllers/SessionController.cs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,475 @@ | ||||
| #pragma warning disable CA1801 | ||||
| 
 | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.ComponentModel.DataAnnotations; | ||||
| using System.Linq; | ||||
| using System.Threading; | ||||
| using Jellyfin.Api.Constants; | ||||
| using Jellyfin.Api.Helpers; | ||||
| using Jellyfin.Data.Enums; | ||||
| using MediaBrowser.Controller.Devices; | ||||
| using MediaBrowser.Controller.Library; | ||||
| using MediaBrowser.Controller.Net; | ||||
| using MediaBrowser.Controller.Session; | ||||
| using MediaBrowser.Model.Dto; | ||||
| using MediaBrowser.Model.Session; | ||||
| using Microsoft.AspNetCore.Authorization; | ||||
| using Microsoft.AspNetCore.Http; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| 
 | ||||
| namespace Jellyfin.Api.Controllers | ||||
| { | ||||
|     /// <summary> | ||||
|     /// The session controller. | ||||
|     /// </summary> | ||||
|     public class SessionController : BaseJellyfinApiController | ||||
|     { | ||||
|         private readonly ISessionManager _sessionManager; | ||||
|         private readonly IUserManager _userManager; | ||||
|         private readonly IAuthorizationContext _authContext; | ||||
|         private readonly IDeviceManager _deviceManager; | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="SessionController"/> class. | ||||
|         /// </summary> | ||||
|         /// <param name="sessionManager">Instance of <see cref="ISessionManager"/> interface.</param> | ||||
|         /// <param name="userManager">Instance of <see cref="IUserManager"/> interface.</param> | ||||
|         /// <param name="authContext">Instance of <see cref="IAuthorizationContext"/> interface.</param> | ||||
|         /// <param name="deviceManager">Instance of <see cref="IDeviceManager"/> interface.</param> | ||||
|         public SessionController( | ||||
|             ISessionManager sessionManager, | ||||
|             IUserManager userManager, | ||||
|             IAuthorizationContext authContext, | ||||
|             IDeviceManager deviceManager) | ||||
|         { | ||||
|             _sessionManager = sessionManager; | ||||
|             _userManager = userManager; | ||||
|             _authContext = authContext; | ||||
|             _deviceManager = deviceManager; | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets a list of sessions. | ||||
|         /// </summary> | ||||
|         /// <param name="controllableByUserId">Filter by sessions that a given user is allowed to remote control.</param> | ||||
|         /// <param name="deviceId">Filter by device Id.</param> | ||||
|         /// <param name="activeWithinSeconds">Optional. Filter by sessions that were active in the last n seconds.</param> | ||||
|         /// <response code="200">List of sessions returned.</response> | ||||
|         /// <returns>An <see cref="IEnumerable{SessionInfo}"/> with the available sessions.</returns> | ||||
|         [HttpGet("/Sessions")] | ||||
|         [Authorize(Policy = Policies.DefaultAuthorization)] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         public ActionResult<IEnumerable<SessionInfo>> GetSessions( | ||||
|             [FromQuery] Guid controllableByUserId, | ||||
|             [FromQuery] string deviceId, | ||||
|             [FromQuery] int? activeWithinSeconds) | ||||
|         { | ||||
|             var result = _sessionManager.Sessions; | ||||
| 
 | ||||
|             if (!string.IsNullOrEmpty(deviceId)) | ||||
|             { | ||||
|                 result = result.Where(i => string.Equals(i.DeviceId, deviceId, StringComparison.OrdinalIgnoreCase)); | ||||
|             } | ||||
| 
 | ||||
|             if (!controllableByUserId.Equals(Guid.Empty)) | ||||
|             { | ||||
|                 result = result.Where(i => i.SupportsRemoteControl); | ||||
| 
 | ||||
|                 var user = _userManager.GetUserById(controllableByUserId); | ||||
| 
 | ||||
|                 if (!user.HasPermission(PermissionKind.EnableRemoteControlOfOtherUsers)) | ||||
|                 { | ||||
|                     result = result.Where(i => i.UserId.Equals(Guid.Empty) || i.ContainsUser(controllableByUserId)); | ||||
|                 } | ||||
| 
 | ||||
|                 if (!user.HasPermission(PermissionKind.EnableSharedDeviceControl)) | ||||
|                 { | ||||
|                     result = result.Where(i => !i.UserId.Equals(Guid.Empty)); | ||||
|                 } | ||||
| 
 | ||||
|                 if (activeWithinSeconds.HasValue && activeWithinSeconds.Value > 0) | ||||
|                 { | ||||
|                     var minActiveDate = DateTime.UtcNow.AddSeconds(0 - activeWithinSeconds.Value); | ||||
|                     result = result.Where(i => i.LastActivityDate >= minActiveDate); | ||||
|                 } | ||||
| 
 | ||||
|                 result = result.Where(i => | ||||
|                 { | ||||
|                     if (!string.IsNullOrWhiteSpace(i.DeviceId)) | ||||
|                     { | ||||
|                         if (!_deviceManager.CanAccessDevice(user, i.DeviceId)) | ||||
|                         { | ||||
|                             return false; | ||||
|                         } | ||||
|                     } | ||||
| 
 | ||||
|                     return true; | ||||
|                 }); | ||||
|             } | ||||
| 
 | ||||
|             return Ok(result); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Instructs a session to browse to an item or view. | ||||
|         /// </summary> | ||||
|         /// <param name="sessionId">The session Id.</param> | ||||
|         /// <param name="itemType">The type of item to browse to.</param> | ||||
|         /// <param name="itemId">The Id of the item.</param> | ||||
|         /// <param name="itemName">The name of the item.</param> | ||||
|         /// <response code="204">Instruction sent to session.</response> | ||||
|         /// <returns>A <see cref="NoContentResult"/>.</returns> | ||||
|         [HttpPost("/Sessions/{sessionId}/Viewing")] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         public ActionResult DisplayContent( | ||||
|             [FromRoute] string sessionId, | ||||
|             [FromQuery] string itemType, | ||||
|             [FromQuery] string itemId, | ||||
|             [FromQuery] string itemName) | ||||
|         { | ||||
|             var command = new BrowseRequest | ||||
|             { | ||||
|                 ItemId = itemId, | ||||
|                 ItemName = itemName, | ||||
|                 ItemType = itemType | ||||
|             }; | ||||
| 
 | ||||
|             _sessionManager.SendBrowseCommand( | ||||
|                 RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id, | ||||
|                 sessionId, | ||||
|                 command, | ||||
|                 CancellationToken.None); | ||||
| 
 | ||||
|             return NoContent(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Instructs a session to play an item. | ||||
|         /// </summary> | ||||
|         /// <param name="sessionId">The session id.</param> | ||||
|         /// <param name="itemIds">The ids of the items to play, comma delimited.</param> | ||||
|         /// <param name="startPositionTicks">The starting position of the first item.</param> | ||||
|         /// <param name="playCommand">The type of play command to issue (PlayNow, PlayNext, PlayLast). Clients who have not yet implemented play next and play last may play now.</param> | ||||
|         /// <param name="playRequest">The <see cref="PlayRequest"/>.</param> | ||||
|         /// <response code="204">Instruction sent to session.</response> | ||||
|         /// <returns>A <see cref="NoContentResult"/>.</returns> | ||||
|         [HttpPost("/Sessions/{sessionId}/Playing")] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         public ActionResult Play( | ||||
|             [FromRoute] string sessionId, | ||||
|             [FromQuery] Guid[] itemIds, | ||||
|             [FromQuery] long? startPositionTicks, | ||||
|             [FromQuery] PlayCommand playCommand, | ||||
|             [FromBody, Required] PlayRequest playRequest) | ||||
|         { | ||||
|             if (playRequest == null) | ||||
|             { | ||||
|                 throw new ArgumentException("Request Body may not be null"); | ||||
|             } | ||||
| 
 | ||||
|             playRequest.ItemIds = itemIds; | ||||
|             playRequest.StartPositionTicks = startPositionTicks; | ||||
|             playRequest.PlayCommand = playCommand; | ||||
| 
 | ||||
|             _sessionManager.SendPlayCommand( | ||||
|                 RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id, | ||||
|                 sessionId, | ||||
|                 playRequest, | ||||
|                 CancellationToken.None); | ||||
| 
 | ||||
|             return NoContent(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Issues a playstate command to a client. | ||||
|         /// </summary> | ||||
|         /// <param name="sessionId">The session id.</param> | ||||
|         /// <param name="playstateRequest">The <see cref="PlaystateRequest"/>.</param> | ||||
|         /// <response code="204">Playstate command sent to session.</response> | ||||
|         /// <returns>A <see cref="NoContentResult"/>.</returns> | ||||
|         [HttpPost("/Sessions/{sessionId}/Playing/{command}")] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         public ActionResult SendPlaystateCommand( | ||||
|             [FromRoute] string sessionId, | ||||
|             [FromBody] PlaystateRequest playstateRequest) | ||||
|         { | ||||
|             _sessionManager.SendPlaystateCommand( | ||||
|                 RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id, | ||||
|                 sessionId, | ||||
|                 playstateRequest, | ||||
|                 CancellationToken.None); | ||||
| 
 | ||||
|             return NoContent(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Issues a system command to a client. | ||||
|         /// </summary> | ||||
|         /// <param name="sessionId">The session id.</param> | ||||
|         /// <param name="command">The command to send.</param> | ||||
|         /// <response code="204">System command sent to session.</response> | ||||
|         /// <returns>A <see cref="NoContentResult"/>.</returns> | ||||
|         [HttpPost("/Sessions/{sessionId}/System/{command}")] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         public ActionResult SendSystemCommand( | ||||
|             [FromRoute] string sessionId, | ||||
|             [FromRoute] string command) | ||||
|         { | ||||
|             var name = command; | ||||
|             if (Enum.TryParse(name, true, out GeneralCommandType commandType)) | ||||
|             { | ||||
|                 name = commandType.ToString(); | ||||
|             } | ||||
| 
 | ||||
|             var currentSession = RequestHelpers.GetSession(_sessionManager, _authContext, Request); | ||||
|             var generalCommand = new GeneralCommand | ||||
|             { | ||||
|                 Name = name, | ||||
|                 ControllingUserId = currentSession.UserId | ||||
|             }; | ||||
| 
 | ||||
|             _sessionManager.SendGeneralCommand(currentSession.Id, sessionId, generalCommand, CancellationToken.None); | ||||
| 
 | ||||
|             return NoContent(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Issues a general command to a client. | ||||
|         /// </summary> | ||||
|         /// <param name="sessionId">The session id.</param> | ||||
|         /// <param name="command">The command to send.</param> | ||||
|         /// <response code="204">General command sent to session.</response> | ||||
|         /// <returns>A <see cref="NoContentResult"/>.</returns> | ||||
|         [HttpPost("/Sessions/{sessionId}/Command/{Command}")] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         public ActionResult SendGeneralCommand( | ||||
|             [FromRoute] string sessionId, | ||||
|             [FromRoute] string command) | ||||
|         { | ||||
|             var currentSession = RequestHelpers.GetSession(_sessionManager, _authContext, Request); | ||||
| 
 | ||||
|             var generalCommand = new GeneralCommand | ||||
|             { | ||||
|                 Name = command, | ||||
|                 ControllingUserId = currentSession.UserId | ||||
|             }; | ||||
| 
 | ||||
|             _sessionManager.SendGeneralCommand(currentSession.Id, sessionId, generalCommand, CancellationToken.None); | ||||
| 
 | ||||
|             return NoContent(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Issues a full general command to a client. | ||||
|         /// </summary> | ||||
|         /// <param name="sessionId">The session id.</param> | ||||
|         /// <param name="command">The <see cref="GeneralCommand"/>.</param> | ||||
|         /// <response code="204">Full general command sent to session.</response> | ||||
|         /// <returns>A <see cref="NoContentResult"/>.</returns> | ||||
|         [HttpPost("/Sessions/{sessionId}/Command")] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         public ActionResult SendFullGeneralCommand( | ||||
|             [FromRoute] string sessionId, | ||||
|             [FromBody, Required] GeneralCommand command) | ||||
|         { | ||||
|             var currentSession = RequestHelpers.GetSession(_sessionManager, _authContext, Request); | ||||
| 
 | ||||
|             if (command == null) | ||||
|             { | ||||
|                 throw new ArgumentException("Request body may not be null"); | ||||
|             } | ||||
| 
 | ||||
|             command.ControllingUserId = currentSession.UserId; | ||||
| 
 | ||||
|             _sessionManager.SendGeneralCommand( | ||||
|                 currentSession.Id, | ||||
|                 sessionId, | ||||
|                 command, | ||||
|                 CancellationToken.None); | ||||
| 
 | ||||
|             return NoContent(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Issues a command to a client to display a message to the user. | ||||
|         /// </summary> | ||||
|         /// <param name="sessionId">The session id.</param> | ||||
|         /// <param name="text">The message test.</param> | ||||
|         /// <param name="header">The message header.</param> | ||||
|         /// <param name="timeoutMs">The message timeout. If omitted the user will have to confirm viewing the message.</param> | ||||
|         /// <response code="204">Message sent.</response> | ||||
|         /// <returns>A <see cref="NoContentResult"/>.</returns> | ||||
|         [HttpPost("/Sessions/{sessionId}/Message")] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         public ActionResult SendMessageCommand( | ||||
|             [FromRoute] string sessionId, | ||||
|             [FromQuery] string text, | ||||
|             [FromQuery] string header, | ||||
|             [FromQuery] long? timeoutMs) | ||||
|         { | ||||
|             var command = new MessageCommand | ||||
|             { | ||||
|                 Header = string.IsNullOrEmpty(header) ? "Message from Server" : header, | ||||
|                 TimeoutMs = timeoutMs, | ||||
|                 Text = text | ||||
|             }; | ||||
| 
 | ||||
|             _sessionManager.SendMessageCommand(RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id, sessionId, command, CancellationToken.None); | ||||
| 
 | ||||
|             return NoContent(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Adds an additional user to a session. | ||||
|         /// </summary> | ||||
|         /// <param name="sessionId">The session id.</param> | ||||
|         /// <param name="userId">The user id.</param> | ||||
|         /// <response code="204">User added to session.</response> | ||||
|         /// <returns>A <see cref="NoContentResult"/>.</returns> | ||||
|         [HttpPost("/Sessions/{sessionId}/User/{userId}")] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         public ActionResult AddUserToSession( | ||||
|             [FromRoute] string sessionId, | ||||
|             [FromRoute] Guid userId) | ||||
|         { | ||||
|             _sessionManager.AddAdditionalUser(sessionId, userId); | ||||
|             return NoContent(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Removes an additional user from a session. | ||||
|         /// </summary> | ||||
|         /// <param name="sessionId">The session id.</param> | ||||
|         /// <param name="userId">The user id.</param> | ||||
|         /// <response code="204">User removed from session.</response> | ||||
|         /// <returns>A <see cref="NoContentResult"/>.</returns> | ||||
|         [HttpDelete("/Sessions/{sessionId}/User/{userId}")] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         public ActionResult RemoveUserFromSession( | ||||
|             [FromRoute] string sessionId, | ||||
|             [FromRoute] Guid userId) | ||||
|         { | ||||
|             _sessionManager.RemoveAdditionalUser(sessionId, userId); | ||||
|             return NoContent(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Updates capabilities for a device. | ||||
|         /// </summary> | ||||
|         /// <param name="id">The session id.</param> | ||||
|         /// <param name="playableMediaTypes">A list of playable media types, comma delimited. Audio, Video, Book, Photo.</param> | ||||
|         /// <param name="supportedCommands">A list of supported remote control commands, comma delimited.</param> | ||||
|         /// <param name="supportsMediaControl">Determines whether media can be played remotely..</param> | ||||
|         /// <param name="supportsSync">Determines whether sync is supported.</param> | ||||
|         /// <param name="supportsPersistentIdentifier">Determines whether the device supports a unique identifier.</param> | ||||
|         /// <response code="204">Capabilities posted.</response> | ||||
|         /// <returns>A <see cref="NoContentResult"/>.</returns> | ||||
|         [HttpPost("/Sessions/Capabilities")] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         public ActionResult PostCapabilities( | ||||
|             [FromQuery] string id, | ||||
|             [FromQuery] string playableMediaTypes, | ||||
|             [FromQuery] string supportedCommands, | ||||
|             [FromQuery] bool supportsMediaControl, | ||||
|             [FromQuery] bool supportsSync, | ||||
|             [FromQuery] bool supportsPersistentIdentifier = true) | ||||
|         { | ||||
|             if (string.IsNullOrWhiteSpace(id)) | ||||
|             { | ||||
|                 id = RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id; | ||||
|             } | ||||
| 
 | ||||
|             _sessionManager.ReportCapabilities(id, new ClientCapabilities | ||||
|             { | ||||
|                 PlayableMediaTypes = RequestHelpers.Split(playableMediaTypes, ',', true), | ||||
|                 SupportedCommands = RequestHelpers.Split(supportedCommands, ',', true), | ||||
|                 SupportsMediaControl = supportsMediaControl, | ||||
|                 SupportsSync = supportsSync, | ||||
|                 SupportsPersistentIdentifier = supportsPersistentIdentifier | ||||
|             }); | ||||
|             return NoContent(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Updates capabilities for a device. | ||||
|         /// </summary> | ||||
|         /// <param name="id">The session id.</param> | ||||
|         /// <param name="capabilities">The <see cref="ClientCapabilities"/>.</param> | ||||
|         /// <response code="204">Capabilities updated.</response> | ||||
|         /// <returns>A <see cref="NoContentResult"/>.</returns> | ||||
|         [HttpPost("/Sessions/Capabilities/Full")] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         public ActionResult PostFullCapabilities( | ||||
|             [FromQuery] string id, | ||||
|             [FromBody, Required] ClientCapabilities capabilities) | ||||
|         { | ||||
|             if (string.IsNullOrWhiteSpace(id)) | ||||
|             { | ||||
|                 id = RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id; | ||||
|             } | ||||
| 
 | ||||
|             _sessionManager.ReportCapabilities(id, capabilities); | ||||
| 
 | ||||
|             return NoContent(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Reports that a session is viewing an item. | ||||
|         /// </summary> | ||||
|         /// <param name="sessionId">The session id.</param> | ||||
|         /// <param name="itemId">The item id.</param> | ||||
|         /// <response code="204">Session reported to server.</response> | ||||
|         /// <returns>A <see cref="NoContentResult"/>.</returns> | ||||
|         [HttpPost("/Sessions/Viewing")] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         public ActionResult ReportViewing( | ||||
|             [FromQuery] string sessionId, | ||||
|             [FromQuery] string itemId) | ||||
|         { | ||||
|             string session = RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id; | ||||
| 
 | ||||
|             _sessionManager.ReportNowViewingItem(session, itemId); | ||||
|             return NoContent(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Reports that a session has ended. | ||||
|         /// </summary> | ||||
|         /// <response code="204">Session end reported to server.</response> | ||||
|         /// <returns>A <see cref="NoContentResult"/>.</returns> | ||||
|         [HttpPost("/Sessions/Logout")] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         public ActionResult ReportSessionEnded() | ||||
|         { | ||||
|             AuthorizationInfo auth = _authContext.GetAuthorizationInfo(Request); | ||||
| 
 | ||||
|             _sessionManager.Logout(auth.Token); | ||||
|             return NoContent(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Get all auth providers. | ||||
|         /// </summary> | ||||
|         /// <response code="200">Auth providers retrieved.</response> | ||||
|         /// <returns>An <see cref="IEnumerable{NameIdPair}"/> with the auth providers.</returns> | ||||
|         [HttpGet("/Auth/Providers")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         public ActionResult<IEnumerable<NameIdPair>> GetAuthProviders() | ||||
|         { | ||||
|             return _userManager.GetAuthenticationProviders(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Get all password reset providers. | ||||
|         /// </summary> | ||||
|         /// <response code="200">Password reset providers retrieved.</response> | ||||
|         /// <returns>An <see cref="IEnumerable{NameIdPair}"/> with the password reset providers.</returns> | ||||
|         [HttpGet("/Auto/PasswordResetProviders")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         public ActionResult<IEnumerable<NameIdPair>> GetPasswordResetProviders() | ||||
|         { | ||||
|             return _userManager.GetPasswordResetProviders(); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -33,16 +33,16 @@ namespace Jellyfin.Api.Controllers | ||||
|         /// <summary> | ||||
|         /// Completes the startup wizard. | ||||
|         /// </summary> | ||||
|         /// <response code="200">Startup wizard completed.</response> | ||||
|         /// <returns>An <see cref="OkResult"/> indicating success.</returns> | ||||
|         /// <response code="204">Startup wizard completed.</response> | ||||
|         /// <returns>A <see cref="NoContentResult"/> indicating success.</returns> | ||||
|         [HttpPost("Complete")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         public ActionResult CompleteWizard() | ||||
|         { | ||||
|             _config.Configuration.IsStartupWizardCompleted = true; | ||||
|             _config.SetOptimalValues(); | ||||
|             _config.SaveConfiguration(); | ||||
|             return Ok(); | ||||
|             return NoContent(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
| @ -70,10 +70,10 @@ namespace Jellyfin.Api.Controllers | ||||
|         /// <param name="uiCulture">The UI language culture.</param> | ||||
|         /// <param name="metadataCountryCode">The metadata country code.</param> | ||||
|         /// <param name="preferredMetadataLanguage">The preferred language for metadata.</param> | ||||
|         /// <response code="200">Configuration saved.</response> | ||||
|         /// <returns>An <see cref="OkResult"/> indicating success.</returns> | ||||
|         /// <response code="204">Configuration saved.</response> | ||||
|         /// <returns>A <see cref="NoContentResult"/> indicating success.</returns> | ||||
|         [HttpPost("Configuration")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         public ActionResult UpdateInitialConfiguration( | ||||
|             [FromForm] string uiCulture, | ||||
|             [FromForm] string metadataCountryCode, | ||||
| @ -83,7 +83,7 @@ namespace Jellyfin.Api.Controllers | ||||
|             _config.Configuration.MetadataCountryCode = metadataCountryCode; | ||||
|             _config.Configuration.PreferredMetadataLanguage = preferredMetadataLanguage; | ||||
|             _config.SaveConfiguration(); | ||||
|             return Ok(); | ||||
|             return NoContent(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
| @ -91,16 +91,16 @@ namespace Jellyfin.Api.Controllers | ||||
|         /// </summary> | ||||
|         /// <param name="enableRemoteAccess">Enable remote access.</param> | ||||
|         /// <param name="enableAutomaticPortMapping">Enable UPnP.</param> | ||||
|         /// <response code="200">Configuration saved.</response> | ||||
|         /// <returns>An <see cref="OkResult"/> indicating success.</returns> | ||||
|         /// <response code="204">Configuration saved.</response> | ||||
|         /// <returns>A <see cref="NoContentResult"/> indicating success.</returns> | ||||
|         [HttpPost("RemoteAccess")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         public ActionResult SetRemoteAccess([FromForm] bool enableRemoteAccess, [FromForm] bool enableAutomaticPortMapping) | ||||
|         { | ||||
|             _config.Configuration.EnableRemoteAccess = enableRemoteAccess; | ||||
|             _config.Configuration.EnableUPnP = enableAutomaticPortMapping; | ||||
|             _config.SaveConfiguration(); | ||||
|             return Ok(); | ||||
|             return NoContent(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
| @ -113,35 +113,41 @@ namespace Jellyfin.Api.Controllers | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         public ActionResult<StartupUserDto> GetFirstUser() | ||||
|         { | ||||
|             // TODO: Remove this method when startup wizard no longer requires an existing user. | ||||
|             _userManager.Initialize(); | ||||
|             var user = _userManager.Users.First(); | ||||
|             return new StartupUserDto { Name = user.Name, Password = user.Password }; | ||||
|             return new StartupUserDto | ||||
|             { | ||||
|                 Name = user.Username, | ||||
|                 Password = user.Password | ||||
|             }; | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Sets the user name and password. | ||||
|         /// </summary> | ||||
|         /// <param name="startupUserDto">The DTO containing username and password.</param> | ||||
|         /// <response code="200">Updated user name and password.</response> | ||||
|         /// <response code="204">Updated user name and password.</response> | ||||
|         /// <returns> | ||||
|         /// A <see cref="Task" /> that represents the asynchronous update operation. | ||||
|         /// The task result contains an <see cref="OkResult"/> indicating success. | ||||
|         /// The task result contains a <see cref="NoContentResult"/> indicating success. | ||||
|         /// </returns> | ||||
|         [HttpPost("User")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         public async Task<ActionResult> UpdateUser([FromForm] StartupUserDto startupUserDto) | ||||
|         { | ||||
|             var user = _userManager.Users.First(); | ||||
| 
 | ||||
|             user.Name = startupUserDto.Name; | ||||
|             user.Username = startupUserDto.Name; | ||||
| 
 | ||||
|             _userManager.UpdateUser(user); | ||||
|             await _userManager.UpdateUserAsync(user).ConfigureAwait(false); | ||||
| 
 | ||||
|             if (!string.IsNullOrEmpty(startupUserDto.Password)) | ||||
|             { | ||||
|                 await _userManager.ChangePassword(user, startupUserDto.Password).ConfigureAwait(false); | ||||
|             } | ||||
| 
 | ||||
|             return Ok(); | ||||
|             return NoContent(); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
							
								
								
									
										347
									
								
								Jellyfin.Api/Controllers/SubtitleController.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										347
									
								
								Jellyfin.Api/Controllers/SubtitleController.cs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,347 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.ComponentModel.DataAnnotations; | ||||
| using System.Diagnostics.CodeAnalysis; | ||||
| using System.Globalization; | ||||
| using System.IO; | ||||
| using System.Linq; | ||||
| using System.Net.Mime; | ||||
| using System.Text; | ||||
| using System.Threading; | ||||
| using System.Threading.Tasks; | ||||
| using Jellyfin.Api.Constants; | ||||
| using MediaBrowser.Controller.Entities; | ||||
| using MediaBrowser.Controller.Library; | ||||
| using MediaBrowser.Controller.MediaEncoding; | ||||
| using MediaBrowser.Controller.Net; | ||||
| using MediaBrowser.Controller.Providers; | ||||
| using MediaBrowser.Controller.Subtitles; | ||||
| using MediaBrowser.Model.Entities; | ||||
| using MediaBrowser.Model.IO; | ||||
| using MediaBrowser.Model.Net; | ||||
| using MediaBrowser.Model.Providers; | ||||
| using Microsoft.AspNetCore.Authorization; | ||||
| using Microsoft.AspNetCore.Http; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| using Microsoft.Extensions.Logging; | ||||
| 
 | ||||
| namespace Jellyfin.Api.Controllers | ||||
| { | ||||
|     /// <summary> | ||||
|     /// Subtitle controller. | ||||
|     /// </summary> | ||||
|     public class SubtitleController : BaseJellyfinApiController | ||||
|     { | ||||
|         private readonly ILibraryManager _libraryManager; | ||||
|         private readonly ISubtitleManager _subtitleManager; | ||||
|         private readonly ISubtitleEncoder _subtitleEncoder; | ||||
|         private readonly IMediaSourceManager _mediaSourceManager; | ||||
|         private readonly IProviderManager _providerManager; | ||||
|         private readonly IFileSystem _fileSystem; | ||||
|         private readonly IAuthorizationContext _authContext; | ||||
|         private readonly ILogger<SubtitleController> _logger; | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="SubtitleController"/> class. | ||||
|         /// </summary> | ||||
|         /// <param name="libraryManager">Instance of <see cref="ILibraryManager"/> interface.</param> | ||||
|         /// <param name="subtitleManager">Instance of <see cref="ISubtitleManager"/> interface.</param> | ||||
|         /// <param name="subtitleEncoder">Instance of <see cref="ISubtitleEncoder"/> interface.</param> | ||||
|         /// <param name="mediaSourceManager">Instance of <see cref="IMediaSourceManager"/> interface.</param> | ||||
|         /// <param name="providerManager">Instance of <see cref="IProviderManager"/> interface.</param> | ||||
|         /// <param name="fileSystem">Instance of <see cref="IFileSystem"/> interface.</param> | ||||
|         /// <param name="authContext">Instance of <see cref="IAuthorizationContext"/> interface.</param> | ||||
|         /// <param name="logger">Instance of <see cref="ILogger{SubtitleController}"/> interface.</param> | ||||
|         public SubtitleController( | ||||
|             ILibraryManager libraryManager, | ||||
|             ISubtitleManager subtitleManager, | ||||
|             ISubtitleEncoder subtitleEncoder, | ||||
|             IMediaSourceManager mediaSourceManager, | ||||
|             IProviderManager providerManager, | ||||
|             IFileSystem fileSystem, | ||||
|             IAuthorizationContext authContext, | ||||
|             ILogger<SubtitleController> logger) | ||||
|         { | ||||
|             _libraryManager = libraryManager; | ||||
|             _subtitleManager = subtitleManager; | ||||
|             _subtitleEncoder = subtitleEncoder; | ||||
|             _mediaSourceManager = mediaSourceManager; | ||||
|             _providerManager = providerManager; | ||||
|             _fileSystem = fileSystem; | ||||
|             _authContext = authContext; | ||||
|             _logger = logger; | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Deletes an external subtitle file. | ||||
|         /// </summary> | ||||
|         /// <param name="itemId">The item id.</param> | ||||
|         /// <param name="index">The index of the subtitle file.</param> | ||||
|         /// <response code="204">Subtitle deleted.</response> | ||||
|         /// <response code="404">Item not found.</response> | ||||
|         /// <returns>A <see cref="NoContentResult"/>.</returns> | ||||
|         [HttpDelete("/Videos/{itemId}/Subtitles/{index}")] | ||||
|         [Authorize(Policy = Policies.RequiresElevation)] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         [ProducesResponseType(StatusCodes.Status404NotFound)] | ||||
|         public ActionResult<Task> DeleteSubtitle( | ||||
|             [FromRoute] Guid itemId, | ||||
|             [FromRoute] int index) | ||||
|         { | ||||
|             var item = _libraryManager.GetItemById(itemId); | ||||
| 
 | ||||
|             if (item == null) | ||||
|             { | ||||
|                 return NotFound(); | ||||
|             } | ||||
| 
 | ||||
|             _subtitleManager.DeleteSubtitles(item, index); | ||||
|             return NoContent(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Search remote subtitles. | ||||
|         /// </summary> | ||||
|         /// <param name="itemId">The item id.</param> | ||||
|         /// <param name="language">The language of the subtitles.</param> | ||||
|         /// <param name="isPerfectMatch">Optional. Only show subtitles which are a perfect match.</param> | ||||
|         /// <response code="200">Subtitles retrieved.</response> | ||||
|         /// <returns>An array of <see cref="RemoteSubtitleInfo"/>.</returns> | ||||
|         [HttpGet("/Items/{itemId}/RemoteSearch/Subtitles/{language}")] | ||||
|         [Authorize(Policy = Policies.DefaultAuthorization)] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         public async Task<ActionResult<IEnumerable<RemoteSubtitleInfo>>> SearchRemoteSubtitles( | ||||
|             [FromRoute] Guid itemId, | ||||
|             [FromRoute] string language, | ||||
|             [FromQuery] bool? isPerfectMatch) | ||||
|         { | ||||
|             var video = (Video)_libraryManager.GetItemById(itemId); | ||||
| 
 | ||||
|             return await _subtitleManager.SearchSubtitles(video, language, isPerfectMatch, CancellationToken.None).ConfigureAwait(false); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Downloads a remote subtitle. | ||||
|         /// </summary> | ||||
|         /// <param name="itemId">The item id.</param> | ||||
|         /// <param name="subtitleId">The subtitle id.</param> | ||||
|         /// <response code="204">Subtitle downloaded.</response> | ||||
|         /// <returns>A <see cref="NoContentResult"/>.</returns> | ||||
|         [HttpPost("/Items/{itemId}/RemoteSearch/Subtitles/{subtitleId}")] | ||||
|         [Authorize(Policy = Policies.DefaultAuthorization)] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         public async Task<ActionResult> DownloadRemoteSubtitles( | ||||
|             [FromRoute] Guid itemId, | ||||
|             [FromRoute] string subtitleId) | ||||
|         { | ||||
|             var video = (Video)_libraryManager.GetItemById(itemId); | ||||
| 
 | ||||
|             try | ||||
|             { | ||||
|                 await _subtitleManager.DownloadSubtitles(video, subtitleId, CancellationToken.None) | ||||
|                     .ConfigureAwait(false); | ||||
| 
 | ||||
|                 _providerManager.QueueRefresh(video.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.High); | ||||
|             } | ||||
|             catch (Exception ex) | ||||
|             { | ||||
|                 _logger.LogError(ex, "Error downloading subtitles"); | ||||
|             } | ||||
| 
 | ||||
|             return NoContent(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets the remote subtitles. | ||||
|         /// </summary> | ||||
|         /// <param name="id">The item id.</param> | ||||
|         /// <response code="200">File returned.</response> | ||||
|         /// <returns>A <see cref="FileStreamResult"/> with the subtitle file.</returns> | ||||
|         [HttpGet("/Providers/Subtitles/Subtitles/{id}")] | ||||
|         [Authorize(Policy = Policies.DefaultAuthorization)] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         [Produces(MediaTypeNames.Application.Octet)] | ||||
|         public async Task<ActionResult> GetRemoteSubtitles([FromRoute] string id) | ||||
|         { | ||||
|             var result = await _subtitleManager.GetRemoteSubtitles(id, CancellationToken.None).ConfigureAwait(false); | ||||
| 
 | ||||
|             return File(result.Stream, MimeTypes.GetMimeType("file." + result.Format)); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets subtitles in a specified format. | ||||
|         /// </summary> | ||||
|         /// <param name="itemId">The item id.</param> | ||||
|         /// <param name="mediaSourceId">The media source id.</param> | ||||
|         /// <param name="index">The subtitle stream index.</param> | ||||
|         /// <param name="format">The format of the returned subtitle.</param> | ||||
|         /// <param name="endPositionTicks">Optional. The end position of the subtitle in ticks.</param> | ||||
|         /// <param name="copyTimestamps">Optional. Whether to copy the timestamps.</param> | ||||
|         /// <param name="addVttTimeMap">Optional. Whether to add a VTT time map.</param> | ||||
|         /// <param name="startPositionTicks">Optional. The start position of the subtitle in ticks.</param> | ||||
|         /// <response code="200">File returned.</response> | ||||
|         /// <returns>A <see cref="FileContentResult"/> with the subtitle file.</returns> | ||||
|         [HttpGet("/Videos/{itemId}/{mediaSourceId}/Subtitles/{index}/Stream.{format}")] | ||||
|         [HttpGet("/Videos/{itemId}/{mediaSourceId}/Subtitles/{index}/{startPositionTicks?}/Stream.{format}")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         public async Task<ActionResult> GetSubtitle( | ||||
|             [FromRoute, Required] Guid itemId, | ||||
|             [FromRoute, Required] string mediaSourceId, | ||||
|             [FromRoute, Required] int index, | ||||
|             [FromRoute, Required] string format, | ||||
|             [FromQuery] long? endPositionTicks, | ||||
|             [FromQuery] bool copyTimestamps, | ||||
|             [FromQuery] bool addVttTimeMap, | ||||
|             [FromRoute] long startPositionTicks = 0) | ||||
|         { | ||||
|             if (string.Equals(format, "js", StringComparison.OrdinalIgnoreCase)) | ||||
|             { | ||||
|                 format = "json"; | ||||
|             } | ||||
| 
 | ||||
|             if (string.IsNullOrEmpty(format)) | ||||
|             { | ||||
|                 var item = (Video)_libraryManager.GetItemById(itemId); | ||||
| 
 | ||||
|                 var idString = itemId.ToString("N", CultureInfo.InvariantCulture); | ||||
|                 var mediaSource = _mediaSourceManager.GetStaticMediaSources(item, false) | ||||
|                     .First(i => string.Equals(i.Id, mediaSourceId ?? idString, StringComparison.Ordinal)); | ||||
| 
 | ||||
|                 var subtitleStream = mediaSource.MediaStreams | ||||
|                     .First(i => i.Type == MediaStreamType.Subtitle && i.Index == index); | ||||
| 
 | ||||
|                 FileStream stream = new FileStream(subtitleStream.Path, FileMode.Open, FileAccess.Read); | ||||
|                 return File(stream, MimeTypes.GetMimeType(subtitleStream.Path)); | ||||
|             } | ||||
| 
 | ||||
|             if (string.Equals(format, "vtt", StringComparison.OrdinalIgnoreCase) && addVttTimeMap) | ||||
|             { | ||||
|                 await using Stream stream = await EncodeSubtitles(itemId, mediaSourceId, index, format, startPositionTicks, endPositionTicks, copyTimestamps).ConfigureAwait(false); | ||||
|                 using var reader = new StreamReader(stream); | ||||
| 
 | ||||
|                 var text = await reader.ReadToEndAsync().ConfigureAwait(false); | ||||
| 
 | ||||
|                 text = text.Replace("WEBVTT", "WEBVTT\nX-TIMESTAMP-MAP=MPEGTS:900000,LOCAL:00:00:00.000", StringComparison.Ordinal); | ||||
| 
 | ||||
|                 return File(Encoding.UTF8.GetBytes(text), MimeTypes.GetMimeType("file." + format)); | ||||
|             } | ||||
| 
 | ||||
|             return File( | ||||
|                 await EncodeSubtitles( | ||||
|                     itemId, | ||||
|                     mediaSourceId, | ||||
|                     index, | ||||
|                     format, | ||||
|                     startPositionTicks, | ||||
|                     endPositionTicks, | ||||
|                     copyTimestamps).ConfigureAwait(false), | ||||
|                 MimeTypes.GetMimeType("file." + format)); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets an HLS subtitle playlist. | ||||
|         /// </summary> | ||||
|         /// <param name="itemId">The item id.</param> | ||||
|         /// <param name="index">The subtitle stream index.</param> | ||||
|         /// <param name="mediaSourceId">The media source id.</param> | ||||
|         /// <param name="segmentLength">The subtitle segment length.</param> | ||||
|         /// <response code="200">Subtitle playlist retrieved.</response> | ||||
|         /// <returns>A <see cref="FileContentResult"/> with the HLS subtitle playlist.</returns> | ||||
|         [HttpGet("/Videos/{itemId}/{mediaSourceId}/Subtitles/{index}/subtitles.m3u8")] | ||||
|         [Authorize(Policy = Policies.DefaultAuthorization)] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")] | ||||
|         public async Task<ActionResult> GetSubtitlePlaylist( | ||||
|             [FromRoute] Guid itemId, | ||||
|             [FromRoute] int index, | ||||
|             [FromRoute] string mediaSourceId, | ||||
|             [FromQuery, Required] int segmentLength) | ||||
|         { | ||||
|             var item = (Video)_libraryManager.GetItemById(itemId); | ||||
| 
 | ||||
|             var mediaSource = await _mediaSourceManager.GetMediaSource(item, mediaSourceId, null, false, CancellationToken.None).ConfigureAwait(false); | ||||
| 
 | ||||
|             var builder = new StringBuilder(); | ||||
| 
 | ||||
|             var runtime = mediaSource.RunTimeTicks ?? -1; | ||||
| 
 | ||||
|             if (runtime <= 0) | ||||
|             { | ||||
|                 throw new ArgumentException("HLS Subtitles are not supported for this media."); | ||||
|             } | ||||
| 
 | ||||
|             var segmentLengthTicks = TimeSpan.FromSeconds(segmentLength).Ticks; | ||||
|             if (segmentLengthTicks <= 0) | ||||
|             { | ||||
|                 throw new ArgumentException("segmentLength was not given, or it was given incorrectly. (It should be bigger than 0)"); | ||||
|             } | ||||
| 
 | ||||
|             builder.AppendLine("#EXTM3U"); | ||||
|             builder.AppendLine("#EXT-X-TARGETDURATION:" + segmentLength.ToString(CultureInfo.InvariantCulture)); | ||||
|             builder.AppendLine("#EXT-X-VERSION:3"); | ||||
|             builder.AppendLine("#EXT-X-MEDIA-SEQUENCE:0"); | ||||
|             builder.AppendLine("#EXT-X-PLAYLIST-TYPE:VOD"); | ||||
| 
 | ||||
|             long positionTicks = 0; | ||||
| 
 | ||||
|             var accessToken = _authContext.GetAuthorizationInfo(Request).Token; | ||||
| 
 | ||||
|             while (positionTicks < runtime) | ||||
|             { | ||||
|                 var remaining = runtime - positionTicks; | ||||
|                 var lengthTicks = Math.Min(remaining, segmentLengthTicks); | ||||
| 
 | ||||
|                 builder.AppendLine("#EXTINF:" + TimeSpan.FromTicks(lengthTicks).TotalSeconds.ToString(CultureInfo.InvariantCulture) + ","); | ||||
| 
 | ||||
|                 var endPositionTicks = Math.Min(runtime, positionTicks + segmentLengthTicks); | ||||
| 
 | ||||
|                 var url = string.Format( | ||||
|                     CultureInfo.CurrentCulture, | ||||
|                     "stream.vtt?CopyTimestamps=true&AddVttTimeMap=true&StartPositionTicks={0}&EndPositionTicks={1}&api_key={2}", | ||||
|                     positionTicks.ToString(CultureInfo.InvariantCulture), | ||||
|                     endPositionTicks.ToString(CultureInfo.InvariantCulture), | ||||
|                     accessToken); | ||||
| 
 | ||||
|                 builder.AppendLine(url); | ||||
| 
 | ||||
|                 positionTicks += segmentLengthTicks; | ||||
|             } | ||||
| 
 | ||||
|             builder.AppendLine("#EXT-X-ENDLIST"); | ||||
|             return File(Encoding.UTF8.GetBytes(builder.ToString()), MimeTypes.GetMimeType("playlist.m3u8")); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Encodes a subtitle in the specified format. | ||||
|         /// </summary> | ||||
|         /// <param name="id">The media id.</param> | ||||
|         /// <param name="mediaSourceId">The source media id.</param> | ||||
|         /// <param name="index">The subtitle index.</param> | ||||
|         /// <param name="format">The format to convert to.</param> | ||||
|         /// <param name="startPositionTicks">The start position in ticks.</param> | ||||
|         /// <param name="endPositionTicks">The end position in ticks.</param> | ||||
|         /// <param name="copyTimestamps">Whether to copy the timestamps.</param> | ||||
|         /// <returns>A <see cref="Task{Stream}"/> with the new subtitle file.</returns> | ||||
|         private Task<Stream> EncodeSubtitles( | ||||
|             Guid id, | ||||
|             string mediaSourceId, | ||||
|             int index, | ||||
|             string format, | ||||
|             long startPositionTicks, | ||||
|             long? endPositionTicks, | ||||
|             bool copyTimestamps) | ||||
|         { | ||||
|             var item = _libraryManager.GetItemById(id); | ||||
| 
 | ||||
|             return _subtitleEncoder.GetSubtitles( | ||||
|                 item, | ||||
|                 mediaSourceId, | ||||
|                 index, | ||||
|                 format, | ||||
|                 startPositionTicks, | ||||
|                 endPositionTicks ?? 0, | ||||
|                 copyTimestamps, | ||||
|                 CancellationToken.None); | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										87
									
								
								Jellyfin.Api/Controllers/SuggestionsController.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								Jellyfin.Api/Controllers/SuggestionsController.cs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,87 @@ | ||||
| using System; | ||||
| using System.Linq; | ||||
| using Jellyfin.Api.Extensions; | ||||
| using Jellyfin.Api.Helpers; | ||||
| using MediaBrowser.Controller.Dto; | ||||
| using MediaBrowser.Controller.Entities; | ||||
| using MediaBrowser.Controller.Library; | ||||
| using MediaBrowser.Model.Dto; | ||||
| using MediaBrowser.Model.Entities; | ||||
| using MediaBrowser.Model.Querying; | ||||
| using Microsoft.AspNetCore.Http; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| 
 | ||||
| namespace Jellyfin.Api.Controllers | ||||
| { | ||||
|     /// <summary> | ||||
|     /// The suggestions controller. | ||||
|     /// </summary> | ||||
|     public class SuggestionsController : BaseJellyfinApiController | ||||
|     { | ||||
|         private readonly IDtoService _dtoService; | ||||
|         private readonly IUserManager _userManager; | ||||
|         private readonly ILibraryManager _libraryManager; | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="SuggestionsController"/> class. | ||||
|         /// </summary> | ||||
|         /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param> | ||||
|         /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param> | ||||
|         /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param> | ||||
|         public SuggestionsController( | ||||
|             IDtoService dtoService, | ||||
|             IUserManager userManager, | ||||
|             ILibraryManager libraryManager) | ||||
|         { | ||||
|             _dtoService = dtoService; | ||||
|             _userManager = userManager; | ||||
|             _libraryManager = libraryManager; | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets suggestions. | ||||
|         /// </summary> | ||||
|         /// <param name="userId">The user id.</param> | ||||
|         /// <param name="mediaType">The media types.</param> | ||||
|         /// <param name="type">The type.</param> | ||||
|         /// <param name="enableTotalRecordCount">Whether to enable the total record count.</param> | ||||
|         /// <param name="startIndex">Optional. The start index.</param> | ||||
|         /// <param name="limit">Optional. The limit.</param> | ||||
|         /// <response code="200">Suggestions returned.</response> | ||||
|         /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the suggestions.</returns> | ||||
|         [HttpGet("/Users/{userId}/Suggestions")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         public ActionResult<QueryResult<BaseItemDto>> GetSuggestions( | ||||
|             [FromRoute] Guid userId, | ||||
|             [FromQuery] string? mediaType, | ||||
|             [FromQuery] string? type, | ||||
|             [FromQuery] bool enableTotalRecordCount, | ||||
|             [FromQuery] int? startIndex, | ||||
|             [FromQuery] int? limit) | ||||
|         { | ||||
|             var user = !userId.Equals(Guid.Empty) ? _userManager.GetUserById(userId) : null; | ||||
| 
 | ||||
|             var dtoOptions = new DtoOptions().AddClientFields(Request); | ||||
|             var result = _libraryManager.GetItemsResult(new InternalItemsQuery(user) | ||||
|             { | ||||
|                 OrderBy = new[] { ItemSortBy.Random }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Descending)).ToArray(), | ||||
|                 MediaTypes = RequestHelpers.Split(mediaType!, ',', true), | ||||
|                 IncludeItemTypes = RequestHelpers.Split(type!, ',', true), | ||||
|                 IsVirtualItem = false, | ||||
|                 StartIndex = startIndex, | ||||
|                 Limit = limit, | ||||
|                 DtoOptions = dtoOptions, | ||||
|                 EnableTotalRecordCount = enableTotalRecordCount, | ||||
|                 Recursive = true | ||||
|             }); | ||||
| 
 | ||||
|             var dtoList = _dtoService.GetBaseItemDtos(result.Items, dtoOptions, user); | ||||
| 
 | ||||
|             return new QueryResult<BaseItemDto> | ||||
|             { | ||||
|                 TotalRecordCount = result.TotalRecordCount, | ||||
|                 Items = dtoList | ||||
|             }; | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										222
									
								
								Jellyfin.Api/Controllers/SystemController.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										222
									
								
								Jellyfin.Api/Controllers/SystemController.cs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,222 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.ComponentModel.DataAnnotations; | ||||
| using System.IO; | ||||
| using System.Linq; | ||||
| using System.Threading; | ||||
| using System.Threading.Tasks; | ||||
| using Jellyfin.Api.Constants; | ||||
| using MediaBrowser.Common.Configuration; | ||||
| using MediaBrowser.Common.Net; | ||||
| using MediaBrowser.Controller; | ||||
| using MediaBrowser.Controller.Configuration; | ||||
| using MediaBrowser.Model.IO; | ||||
| using MediaBrowser.Model.Net; | ||||
| using MediaBrowser.Model.System; | ||||
| using Microsoft.AspNetCore.Authorization; | ||||
| using Microsoft.AspNetCore.Http; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| using Microsoft.Extensions.Logging; | ||||
| 
 | ||||
| namespace Jellyfin.Api.Controllers | ||||
| { | ||||
|     /// <summary> | ||||
|     /// The system controller. | ||||
|     /// </summary> | ||||
|     [Route("/System")] | ||||
|     public class SystemController : BaseJellyfinApiController | ||||
|     { | ||||
|         private readonly IServerApplicationHost _appHost; | ||||
|         private readonly IApplicationPaths _appPaths; | ||||
|         private readonly IFileSystem _fileSystem; | ||||
|         private readonly INetworkManager _network; | ||||
|         private readonly ILogger<SystemController> _logger; | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="SystemController"/> class. | ||||
|         /// </summary> | ||||
|         /// <param name="serverConfigurationManager">Instance of <see cref="IServerConfigurationManager"/> interface.</param> | ||||
|         /// <param name="appHost">Instance of <see cref="IServerApplicationHost"/> interface.</param> | ||||
|         /// <param name="fileSystem">Instance of <see cref="IFileSystem"/> interface.</param> | ||||
|         /// <param name="network">Instance of <see cref="INetworkManager"/> interface.</param> | ||||
|         /// <param name="logger">Instance of <see cref="ILogger{SystemController}"/> interface.</param> | ||||
|         public SystemController( | ||||
|             IServerConfigurationManager serverConfigurationManager, | ||||
|             IServerApplicationHost appHost, | ||||
|             IFileSystem fileSystem, | ||||
|             INetworkManager network, | ||||
|             ILogger<SystemController> logger) | ||||
|         { | ||||
|             _appPaths = serverConfigurationManager.ApplicationPaths; | ||||
|             _appHost = appHost; | ||||
|             _fileSystem = fileSystem; | ||||
|             _network = network; | ||||
|             _logger = logger; | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets information about the server. | ||||
|         /// </summary> | ||||
|         /// <response code="200">Information retrieved.</response> | ||||
|         /// <returns>A <see cref="SystemInfo"/> with info about the system.</returns> | ||||
|         [HttpGet("Info")] | ||||
|         [Authorize(Policy = Policies.IgnoreSchedule)] | ||||
|         [Authorize(Policy = Policies.FirstTimeSetupOrElevated)] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         public async Task<ActionResult<SystemInfo>> GetSystemInfo() | ||||
|         { | ||||
|             return await _appHost.GetSystemInfo(CancellationToken.None).ConfigureAwait(false); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets public information about the server. | ||||
|         /// </summary> | ||||
|         /// <response code="200">Information retrieved.</response> | ||||
|         /// <returns>A <see cref="PublicSystemInfo"/> with public info about the system.</returns> | ||||
|         [HttpGet("Info/Public")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         public async Task<ActionResult<PublicSystemInfo>> GetPublicSystemInfo() | ||||
|         { | ||||
|             return await _appHost.GetPublicSystemInfo(CancellationToken.None).ConfigureAwait(false); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Pings the system. | ||||
|         /// </summary> | ||||
|         /// <response code="200">Information retrieved.</response> | ||||
|         /// <returns>The server name.</returns> | ||||
|         [HttpGet("Ping")] | ||||
|         [HttpPost("Ping")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         public ActionResult<string> PingSystem() | ||||
|         { | ||||
|             return _appHost.Name; | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Restarts the application. | ||||
|         /// </summary> | ||||
|         /// <response code="204">Server restarted.</response> | ||||
|         /// <returns>No content. Server restarted.</returns> | ||||
|         [HttpPost("Restart")] | ||||
|         [Authorize(Policy = Policies.LocalAccessOnly)] | ||||
|         [Authorize(Policy = Policies.RequiresElevation)] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         public ActionResult RestartApplication() | ||||
|         { | ||||
|             Task.Run(async () => | ||||
|             { | ||||
|                 await Task.Delay(100).ConfigureAwait(false); | ||||
|                 _appHost.Restart(); | ||||
|             }); | ||||
|             return NoContent(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Shuts down the application. | ||||
|         /// </summary> | ||||
|         /// <response code="204">Server shut down.</response> | ||||
|         /// <returns>No content. Server shut down.</returns> | ||||
|         [HttpPost("Shutdown")] | ||||
|         [Authorize(Policy = Policies.RequiresElevation)] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         public ActionResult ShutdownApplication() | ||||
|         { | ||||
|             Task.Run(async () => | ||||
|             { | ||||
|                 await Task.Delay(100).ConfigureAwait(false); | ||||
|                 await _appHost.Shutdown().ConfigureAwait(false); | ||||
|             }); | ||||
|             return NoContent(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets a list of available server log files. | ||||
|         /// </summary> | ||||
|         /// <response code="200">Information retrieved.</response> | ||||
|         /// <returns>An array of <see cref="LogFile"/> with the available log files.</returns> | ||||
|         [HttpGet("Logs")] | ||||
|         [Authorize(Policy = Policies.RequiresElevation)] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         public ActionResult<LogFile[]> GetServerLogs() | ||||
|         { | ||||
|             IEnumerable<FileSystemMetadata> files; | ||||
| 
 | ||||
|             try | ||||
|             { | ||||
|                 files = _fileSystem.GetFiles(_appPaths.LogDirectoryPath, new[] { ".txt", ".log" }, true, false); | ||||
|             } | ||||
|             catch (IOException ex) | ||||
|             { | ||||
|                 _logger.LogError(ex, "Error getting logs"); | ||||
|                 files = Enumerable.Empty<FileSystemMetadata>(); | ||||
|             } | ||||
| 
 | ||||
|             var result = files.Select(i => new LogFile | ||||
|                 { | ||||
|                     DateCreated = _fileSystem.GetCreationTimeUtc(i), | ||||
|                     DateModified = _fileSystem.GetLastWriteTimeUtc(i), | ||||
|                     Name = i.Name, | ||||
|                     Size = i.Length | ||||
|                 }) | ||||
|                 .OrderByDescending(i => i.DateModified) | ||||
|                 .ThenByDescending(i => i.DateCreated) | ||||
|                 .ThenBy(i => i.Name) | ||||
|                 .ToArray(); | ||||
| 
 | ||||
|             return result; | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets information about the request endpoint. | ||||
|         /// </summary> | ||||
|         /// <response code="200">Information retrieved.</response> | ||||
|         /// <returns><see cref="EndPointInfo"/> with information about the endpoint.</returns> | ||||
|         [HttpGet("Endpoint")] | ||||
|         [Authorize(Policy = Policies.DefaultAuthorization)] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         public ActionResult<EndPointInfo> GetEndpointInfo() | ||||
|         { | ||||
|             return new EndPointInfo | ||||
|             { | ||||
|                 IsLocal = Request.HttpContext.Connection.LocalIpAddress.Equals(Request.HttpContext.Connection.RemoteIpAddress), | ||||
|                 IsInNetwork = _network.IsInLocalNetwork(Request.HttpContext.Connection.RemoteIpAddress.ToString()) | ||||
|             }; | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets a log file. | ||||
|         /// </summary> | ||||
|         /// <param name="name">The name of the log file to get.</param> | ||||
|         /// <response code="200">Log file retrieved.</response> | ||||
|         /// <returns>The log file.</returns> | ||||
|         [HttpGet("Logs/Log")] | ||||
|         [Authorize(Policy = Policies.RequiresElevation)] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         public ActionResult GetLogFile([FromQuery, Required] string name) | ||||
|         { | ||||
|             var file = _fileSystem.GetFiles(_appPaths.LogDirectoryPath) | ||||
|                 .First(i => string.Equals(i.Name, name, StringComparison.OrdinalIgnoreCase)); | ||||
| 
 | ||||
|             // For older files, assume fully static | ||||
|             var fileShare = file.LastWriteTimeUtc < DateTime.UtcNow.AddHours(-1) ? FileShare.Read : FileShare.ReadWrite; | ||||
| 
 | ||||
|             FileStream stream = new FileStream(file.FullName, FileMode.Open, FileAccess.Read, fileShare); | ||||
|             return File(stream, "text/plain"); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets wake on lan information. | ||||
|         /// </summary> | ||||
|         /// <response code="200">Information retrieved.</response> | ||||
|         /// <returns>An <see cref="IEnumerable{WakeOnLanInfo}"/> with the WakeOnLan infos.</returns> | ||||
|         [HttpGet("WakeOnLanInfo")] | ||||
|         [Authorize(Policy = Policies.DefaultAuthorization)] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         public ActionResult<IEnumerable<WakeOnLanInfo>> GetWakeOnLanInfo() | ||||
|         { | ||||
|             var result = _appHost.GetWakeOnLanInfo(); | ||||
|             return Ok(result); | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										380
									
								
								Jellyfin.Api/Controllers/TvShowsController.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										380
									
								
								Jellyfin.Api/Controllers/TvShowsController.cs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,380 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.Diagnostics.CodeAnalysis; | ||||
| using System.Globalization; | ||||
| using System.Linq; | ||||
| using Jellyfin.Api.Constants; | ||||
| using Jellyfin.Api.Extensions; | ||||
| using MediaBrowser.Common.Extensions; | ||||
| using MediaBrowser.Controller.Dto; | ||||
| using MediaBrowser.Controller.Entities; | ||||
| using MediaBrowser.Controller.Entities.TV; | ||||
| using MediaBrowser.Controller.Library; | ||||
| using MediaBrowser.Controller.TV; | ||||
| using MediaBrowser.Model.Dto; | ||||
| using MediaBrowser.Model.Entities; | ||||
| using MediaBrowser.Model.Querying; | ||||
| using Microsoft.AspNetCore.Authorization; | ||||
| using Microsoft.AspNetCore.Http; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| 
 | ||||
| namespace Jellyfin.Api.Controllers | ||||
| { | ||||
|     /// <summary> | ||||
|     /// The tv shows controller. | ||||
|     /// </summary> | ||||
|     [Route("/Shows")] | ||||
|     [Authorize(Policy = Policies.DefaultAuthorization)] | ||||
|     public class TvShowsController : BaseJellyfinApiController | ||||
|     { | ||||
|         private readonly IUserManager _userManager; | ||||
|         private readonly ILibraryManager _libraryManager; | ||||
|         private readonly IDtoService _dtoService; | ||||
|         private readonly ITVSeriesManager _tvSeriesManager; | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="TvShowsController"/> class. | ||||
|         /// </summary> | ||||
|         /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param> | ||||
|         /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param> | ||||
|         /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param> | ||||
|         /// <param name="tvSeriesManager">Instance of the <see cref="ITVSeriesManager"/> interface.</param> | ||||
|         public TvShowsController( | ||||
|             IUserManager userManager, | ||||
|             ILibraryManager libraryManager, | ||||
|             IDtoService dtoService, | ||||
|             ITVSeriesManager tvSeriesManager) | ||||
|         { | ||||
|             _userManager = userManager; | ||||
|             _libraryManager = libraryManager; | ||||
|             _dtoService = dtoService; | ||||
|             _tvSeriesManager = tvSeriesManager; | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets a list of next up episodes. | ||||
|         /// </summary> | ||||
|         /// <param name="userId">The user id of the user to get the next up episodes for.</param> | ||||
|         /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param> | ||||
|         /// <param name="limit">Optional. The maximum number of records to return.</param> | ||||
|         /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls.</param> | ||||
|         /// <param name="seriesId">Optional. Filter by series id.</param> | ||||
|         /// <param name="parentId">Optional. Specify this to localize the search to a specific item or folder. Omit to use the root.</param> | ||||
|         /// <param name="enableImges">Optional. Include image information in output.</param> | ||||
|         /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param> | ||||
|         /// <param name="enableImageTypes">Optional. The image types to include in the output.</param> | ||||
|         /// <param name="enableUserData">Optional. Include user data.</param> | ||||
|         /// <param name="enableTotalRecordCount">Whether to enable the total records count. Defaults to true.</param> | ||||
|         /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the next up episodes.</returns> | ||||
|         [HttpGet("NextUp")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         public ActionResult<QueryResult<BaseItemDto>> GetNextUp( | ||||
|             [FromQuery] Guid userId, | ||||
|             [FromQuery] int? startIndex, | ||||
|             [FromQuery] int? limit, | ||||
|             [FromQuery] string? fields, | ||||
|             [FromQuery] string? seriesId, | ||||
|             [FromQuery] string? parentId, | ||||
|             [FromQuery] bool? enableImges, | ||||
|             [FromQuery] int? imageTypeLimit, | ||||
|             [FromQuery] string? enableImageTypes, | ||||
|             [FromQuery] bool? enableUserData, | ||||
|             [FromQuery] bool enableTotalRecordCount = true) | ||||
|         { | ||||
|             var options = new DtoOptions() | ||||
|                 .AddItemFields(fields!) | ||||
|                 .AddClientFields(Request) | ||||
|                 .AddAdditionalDtoOptions(enableImges, enableUserData, imageTypeLimit, enableImageTypes!); | ||||
| 
 | ||||
|             var result = _tvSeriesManager.GetNextUp( | ||||
|                 new NextUpQuery | ||||
|                 { | ||||
|                     Limit = limit, | ||||
|                     ParentId = parentId, | ||||
|                     SeriesId = seriesId, | ||||
|                     StartIndex = startIndex, | ||||
|                     UserId = userId, | ||||
|                     EnableTotalRecordCount = enableTotalRecordCount | ||||
|                 }, | ||||
|                 options); | ||||
| 
 | ||||
|             var user = _userManager.GetUserById(userId); | ||||
| 
 | ||||
|             var returnItems = _dtoService.GetBaseItemDtos(result.Items, options, user); | ||||
| 
 | ||||
|             return new QueryResult<BaseItemDto> | ||||
|             { | ||||
|                 TotalRecordCount = result.TotalRecordCount, | ||||
|                 Items = returnItems | ||||
|             }; | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets a list of upcoming episodes. | ||||
|         /// </summary> | ||||
|         /// <param name="userId">The user id of the user to get the upcoming episodes for.</param> | ||||
|         /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param> | ||||
|         /// <param name="limit">Optional. The maximum number of records to return.</param> | ||||
|         /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls.</param> | ||||
|         /// <param name="parentId">Optional. Specify this to localize the search to a specific item or folder. Omit to use the root.</param> | ||||
|         /// <param name="enableImges">Optional. Include image information in output.</param> | ||||
|         /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param> | ||||
|         /// <param name="enableImageTypes">Optional. The image types to include in the output.</param> | ||||
|         /// <param name="enableUserData">Optional. Include user data.</param> | ||||
|         /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the next up episodes.</returns> | ||||
|         [HttpGet("Upcoming")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         public ActionResult<QueryResult<BaseItemDto>> GetUpcomingEpisodes( | ||||
|             [FromQuery] Guid userId, | ||||
|             [FromQuery] int? startIndex, | ||||
|             [FromQuery] int? limit, | ||||
|             [FromQuery] string? fields, | ||||
|             [FromQuery] string? parentId, | ||||
|             [FromQuery] bool? enableImges, | ||||
|             [FromQuery] int? imageTypeLimit, | ||||
|             [FromQuery] string? enableImageTypes, | ||||
|             [FromQuery] bool? enableUserData) | ||||
|         { | ||||
|             var user = _userManager.GetUserById(userId); | ||||
| 
 | ||||
|             var minPremiereDate = DateTime.Now.Date.ToUniversalTime().AddDays(-1); | ||||
| 
 | ||||
|             var parentIdGuid = string.IsNullOrWhiteSpace(parentId) ? Guid.Empty : new Guid(parentId); | ||||
| 
 | ||||
|             var options = new DtoOptions() | ||||
|                 .AddItemFields(fields!) | ||||
|                 .AddClientFields(Request) | ||||
|                 .AddAdditionalDtoOptions(enableImges, enableUserData, imageTypeLimit, enableImageTypes!); | ||||
| 
 | ||||
|             var itemsResult = _libraryManager.GetItemList(new InternalItemsQuery(user) | ||||
|             { | ||||
|                 IncludeItemTypes = new[] { nameof(Episode) }, | ||||
|                 OrderBy = new[] { ItemSortBy.PremiereDate, ItemSortBy.SortName }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Ascending)).ToArray(), | ||||
|                 MinPremiereDate = minPremiereDate, | ||||
|                 StartIndex = startIndex, | ||||
|                 Limit = limit, | ||||
|                 ParentId = parentIdGuid, | ||||
|                 Recursive = true, | ||||
|                 DtoOptions = options | ||||
|             }); | ||||
| 
 | ||||
|             var returnItems = _dtoService.GetBaseItemDtos(itemsResult, options, user); | ||||
| 
 | ||||
|             return new QueryResult<BaseItemDto> | ||||
|             { | ||||
|                 TotalRecordCount = itemsResult.Count, | ||||
|                 Items = returnItems | ||||
|             }; | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets episodes for a tv season. | ||||
|         /// </summary> | ||||
|         /// <param name="seriesId">The series id.</param> | ||||
|         /// <param name="userId">The user id.</param> | ||||
|         /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls.</param> | ||||
|         /// <param name="season">Optional filter by season number.</param> | ||||
|         /// <param name="seasonId">Optional. Filter by season id.</param> | ||||
|         /// <param name="isMissing">Optional. Filter by items that are missing episodes or not.</param> | ||||
|         /// <param name="adjacentTo">Optional. Return items that are siblings of a supplied item.</param> | ||||
|         /// <param name="startItemId">Optional. Skip through the list until a given item is found.</param> | ||||
|         /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param> | ||||
|         /// <param name="limit">Optional. The maximum number of records to return.</param> | ||||
|         /// <param name="enableImages">Optional, include image information in output.</param> | ||||
|         /// <param name="imageTypeLimit">Optional, the max number of images to return, per image type.</param> | ||||
|         /// <param name="enableImageTypes">Optional. The image types to include in the output.</param> | ||||
|         /// <param name="enableUserData">Optional. Include user data.</param> | ||||
|         /// <param name="sortBy">Optional. Specify one or more sort orders, comma delimeted. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime.</param> | ||||
|         /// <param name="sortOrder">Optional. Sort order: Ascending,Descending.</param> | ||||
|         /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the episodes on success or a <see cref="NotFoundResult"/> if the series was not found.</returns> | ||||
|         [HttpGet("{seriesId}/Episodes")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         [ProducesResponseType(StatusCodes.Status404NotFound)] | ||||
|         [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "sortOrder", Justification = "Imported from ServiceStack")] | ||||
|         public ActionResult<QueryResult<BaseItemDto>> GetEpisodes( | ||||
|             [FromRoute] string seriesId, | ||||
|             [FromQuery] Guid userId, | ||||
|             [FromQuery] string? fields, | ||||
|             [FromQuery] int? season, | ||||
|             [FromQuery] string? seasonId, | ||||
|             [FromQuery] bool? isMissing, | ||||
|             [FromQuery] string? adjacentTo, | ||||
|             [FromQuery] string? startItemId, | ||||
|             [FromQuery] int? startIndex, | ||||
|             [FromQuery] int? limit, | ||||
|             [FromQuery] bool? enableImages, | ||||
|             [FromQuery] int? imageTypeLimit, | ||||
|             [FromQuery] string? enableImageTypes, | ||||
|             [FromQuery] bool? enableUserData, | ||||
|             [FromQuery] string? sortBy, | ||||
|             [FromQuery] SortOrder? sortOrder) | ||||
|         { | ||||
|             var user = _userManager.GetUserById(userId); | ||||
| 
 | ||||
|             List<BaseItem> episodes; | ||||
| 
 | ||||
|             var dtoOptions = new DtoOptions() | ||||
|                 .AddItemFields(fields!) | ||||
|                 .AddClientFields(Request) | ||||
|                 .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!); | ||||
| 
 | ||||
|             if (!string.IsNullOrWhiteSpace(seasonId)) // Season id was supplied. Get episodes by season id. | ||||
|             { | ||||
|                 var item = _libraryManager.GetItemById(new Guid(seasonId)); | ||||
|                 if (!(item is Season seasonItem)) | ||||
|                 { | ||||
|                     return NotFound("No season exists with Id " + seasonId); | ||||
|                 } | ||||
| 
 | ||||
|                 episodes = seasonItem.GetEpisodes(user, dtoOptions); | ||||
|             } | ||||
|             else if (season.HasValue) // Season number was supplied. Get episodes by season number | ||||
|             { | ||||
|                 if (!(_libraryManager.GetItemById(seriesId) is Series series)) | ||||
|                 { | ||||
|                     return NotFound("Series not found"); | ||||
|                 } | ||||
| 
 | ||||
|                 var seasonItem = series | ||||
|                     .GetSeasons(user, dtoOptions) | ||||
|                     .FirstOrDefault(i => i.IndexNumber == season.Value); | ||||
| 
 | ||||
|                 episodes = seasonItem == null ? | ||||
|                     new List<BaseItem>() | ||||
|                     : ((Season)seasonItem).GetEpisodes(user, dtoOptions); | ||||
|             } | ||||
|             else // No season number or season id was supplied. Returning all episodes. | ||||
|             { | ||||
|                 if (!(_libraryManager.GetItemById(seriesId) is Series series)) | ||||
|                 { | ||||
|                     return NotFound("Series not found"); | ||||
|                 } | ||||
| 
 | ||||
|                 episodes = series.GetEpisodes(user, dtoOptions).ToList(); | ||||
|             } | ||||
| 
 | ||||
|             // Filter after the fact in case the ui doesn't want them | ||||
|             if (isMissing.HasValue) | ||||
|             { | ||||
|                 var val = isMissing.Value; | ||||
|                 episodes = episodes | ||||
|                     .Where(i => ((Episode)i).IsMissingEpisode == val) | ||||
|                     .ToList(); | ||||
|             } | ||||
| 
 | ||||
|             if (!string.IsNullOrWhiteSpace(startItemId)) | ||||
|             { | ||||
|                 episodes = episodes | ||||
|                     .SkipWhile(i => !string.Equals(i.Id.ToString("N", CultureInfo.InvariantCulture), startItemId, StringComparison.OrdinalIgnoreCase)) | ||||
|                     .ToList(); | ||||
|             } | ||||
| 
 | ||||
|             // This must be the last filter | ||||
|             if (!string.IsNullOrEmpty(adjacentTo)) | ||||
|             { | ||||
|                 episodes = UserViewBuilder.FilterForAdjacency(episodes, adjacentTo).ToList(); | ||||
|             } | ||||
| 
 | ||||
|             if (string.Equals(sortBy, ItemSortBy.Random, StringComparison.OrdinalIgnoreCase)) | ||||
|             { | ||||
|                 episodes.Shuffle(); | ||||
|             } | ||||
| 
 | ||||
|             var returnItems = episodes; | ||||
| 
 | ||||
|             if (startIndex.HasValue || limit.HasValue) | ||||
|             { | ||||
|                 returnItems = ApplyPaging(episodes, startIndex, limit).ToList(); | ||||
|             } | ||||
| 
 | ||||
|             var dtos = _dtoService.GetBaseItemDtos(returnItems, dtoOptions, user); | ||||
| 
 | ||||
|             return new QueryResult<BaseItemDto> | ||||
|             { | ||||
|                 TotalRecordCount = episodes.Count, | ||||
|                 Items = dtos | ||||
|             }; | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets seasons for a tv series. | ||||
|         /// </summary> | ||||
|         /// <param name="seriesId">The series id.</param> | ||||
|         /// <param name="userId">The user id.</param> | ||||
|         /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls.</param> | ||||
|         /// <param name="isSpecialSeason">Optional. Filter by special season.</param> | ||||
|         /// <param name="isMissing">Optional. Filter by items that are missing episodes or not.</param> | ||||
|         /// <param name="adjacentTo">Optional. Return items that are siblings of a supplied item.</param> | ||||
|         /// <param name="enableImages">Optional. Include image information in output.</param> | ||||
|         /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param> | ||||
|         /// <param name="enableImageTypes">Optional. The image types to include in the output.</param> | ||||
|         /// <param name="enableUserData">Optional. Include user data.</param> | ||||
|         /// <returns>A <see cref="QueryResult{BaseItemDto}"/> on success or a <see cref="NotFoundResult"/> if the series was not found.</returns> | ||||
|         [HttpGet("{seriesId}/Seasons")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         [ProducesResponseType(StatusCodes.Status404NotFound)] | ||||
|         public ActionResult<QueryResult<BaseItemDto>> GetSeasons( | ||||
|             [FromRoute] string seriesId, | ||||
|             [FromQuery] Guid userId, | ||||
|             [FromQuery] string fields, | ||||
|             [FromQuery] bool? isSpecialSeason, | ||||
|             [FromQuery] bool? isMissing, | ||||
|             [FromQuery] string adjacentTo, | ||||
|             [FromQuery] bool? enableImages, | ||||
|             [FromQuery] int? imageTypeLimit, | ||||
|             [FromQuery] string? enableImageTypes, | ||||
|             [FromQuery] bool? enableUserData) | ||||
|         { | ||||
|             var user = _userManager.GetUserById(userId); | ||||
| 
 | ||||
|             if (!(_libraryManager.GetItemById(seriesId) is Series series)) | ||||
|             { | ||||
|                 return NotFound("Series not found"); | ||||
|             } | ||||
| 
 | ||||
|             var seasons = series.GetItemList(new InternalItemsQuery(user) | ||||
|             { | ||||
|                 IsMissing = isMissing, | ||||
|                 IsSpecialSeason = isSpecialSeason, | ||||
|                 AdjacentTo = adjacentTo | ||||
|             }); | ||||
| 
 | ||||
|             var dtoOptions = new DtoOptions() | ||||
|                 .AddItemFields(fields) | ||||
|                 .AddClientFields(Request) | ||||
|                 .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!); | ||||
| 
 | ||||
|             var returnItems = _dtoService.GetBaseItemDtos(seasons, dtoOptions, user); | ||||
| 
 | ||||
|             return new QueryResult<BaseItemDto> | ||||
|             { | ||||
|                 TotalRecordCount = returnItems.Count, | ||||
|                 Items = returnItems | ||||
|             }; | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Applies the paging. | ||||
|         /// </summary> | ||||
|         /// <param name="items">The items.</param> | ||||
|         /// <param name="startIndex">The start index.</param> | ||||
|         /// <param name="limit">The limit.</param> | ||||
|         /// <returns>IEnumerable{BaseItem}.</returns> | ||||
|         private IEnumerable<BaseItem> ApplyPaging(IEnumerable<BaseItem> items, int? startIndex, int? limit) | ||||
|         { | ||||
|             // Start at | ||||
|             if (startIndex.HasValue) | ||||
|             { | ||||
|                 items = items.Skip(startIndex.Value); | ||||
|             } | ||||
| 
 | ||||
|             // Return limit | ||||
|             if (limit.HasValue) | ||||
|             { | ||||
|                 items = items.Take(limit.Value); | ||||
|             } | ||||
| 
 | ||||
|             return items; | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										552
									
								
								Jellyfin.Api/Controllers/UserController.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										552
									
								
								Jellyfin.Api/Controllers/UserController.cs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,552 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.ComponentModel.DataAnnotations; | ||||
| using System.Diagnostics.CodeAnalysis; | ||||
| using System.Linq; | ||||
| using System.Threading.Tasks; | ||||
| using Jellyfin.Api.Constants; | ||||
| using Jellyfin.Api.Helpers; | ||||
| using Jellyfin.Api.Models.UserDtos; | ||||
| using Jellyfin.Data.Enums; | ||||
| using MediaBrowser.Common.Net; | ||||
| using MediaBrowser.Controller.Authentication; | ||||
| using MediaBrowser.Controller.Configuration; | ||||
| using MediaBrowser.Controller.Devices; | ||||
| using MediaBrowser.Controller.Library; | ||||
| using MediaBrowser.Controller.Net; | ||||
| using MediaBrowser.Controller.Session; | ||||
| using MediaBrowser.Model.Configuration; | ||||
| using MediaBrowser.Model.Dto; | ||||
| using MediaBrowser.Model.Users; | ||||
| using Microsoft.AspNetCore.Authorization; | ||||
| using Microsoft.AspNetCore.Http; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| using Microsoft.AspNetCore.Mvc.ModelBinding; | ||||
| 
 | ||||
| namespace Jellyfin.Api.Controllers | ||||
| { | ||||
|     /// <summary> | ||||
|     /// User controller. | ||||
|     /// </summary> | ||||
|     [Route("/Users")] | ||||
|     public class UserController : BaseJellyfinApiController | ||||
|     { | ||||
|         private readonly IUserManager _userManager; | ||||
|         private readonly ISessionManager _sessionManager; | ||||
|         private readonly INetworkManager _networkManager; | ||||
|         private readonly IDeviceManager _deviceManager; | ||||
|         private readonly IAuthorizationContext _authContext; | ||||
|         private readonly IServerConfigurationManager _config; | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="UserController"/> class. | ||||
|         /// </summary> | ||||
|         /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param> | ||||
|         /// <param name="sessionManager">Instance of the <see cref="ISessionManager"/> interface.</param> | ||||
|         /// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param> | ||||
|         /// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param> | ||||
|         /// <param name="authContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param> | ||||
|         /// <param name="config">Instance of the <see cref="IServerConfigurationManager"/> interface.</param> | ||||
|         public UserController( | ||||
|             IUserManager userManager, | ||||
|             ISessionManager sessionManager, | ||||
|             INetworkManager networkManager, | ||||
|             IDeviceManager deviceManager, | ||||
|             IAuthorizationContext authContext, | ||||
|             IServerConfigurationManager config) | ||||
|         { | ||||
|             _userManager = userManager; | ||||
|             _sessionManager = sessionManager; | ||||
|             _networkManager = networkManager; | ||||
|             _deviceManager = deviceManager; | ||||
|             _authContext = authContext; | ||||
|             _config = config; | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets a list of users. | ||||
|         /// </summary> | ||||
|         /// <param name="isHidden">Optional filter by IsHidden=true or false.</param> | ||||
|         /// <param name="isDisabled">Optional filter by IsDisabled=true or false.</param> | ||||
|         /// <param name="isGuest">Optional filter by IsGuest=true or false.</param> | ||||
|         /// <response code="200">Users returned.</response> | ||||
|         /// <returns>An <see cref="IEnumerable{UserDto}"/> containing the users.</returns> | ||||
|         [HttpGet] | ||||
|         [Authorize(Policy = Policies.DefaultAuthorization)] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "isGuest", Justification = "Imported from ServiceStack")] | ||||
|         public ActionResult<IEnumerable<UserDto>> GetUsers( | ||||
|             [FromQuery] bool? isHidden, | ||||
|             [FromQuery] bool? isDisabled, | ||||
|             [FromQuery] bool? isGuest) | ||||
|         { | ||||
|             var users = Get(isHidden, isDisabled, false, false); | ||||
|             return Ok(users); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets a list of publicly visible users for display on a login screen. | ||||
|         /// </summary> | ||||
|         /// <response code="200">Public users returned.</response> | ||||
|         /// <returns>An <see cref="IEnumerable{UserDto}"/> containing the public users.</returns> | ||||
|         [HttpGet("Public")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         public ActionResult<IEnumerable<UserDto>> GetPublicUsers() | ||||
|         { | ||||
|             // If the startup wizard hasn't been completed then just return all users | ||||
|             if (!_config.Configuration.IsStartupWizardCompleted) | ||||
|             { | ||||
|                 return Ok(Get(false, false, false, false)); | ||||
|             } | ||||
| 
 | ||||
|             return Ok(Get(false, false, true, true)); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets a user by Id. | ||||
|         /// </summary> | ||||
|         /// <param name="userId">The user id.</param> | ||||
|         /// <response code="200">User returned.</response> | ||||
|         /// <response code="404">User not found.</response> | ||||
|         /// <returns>An <see cref="UserDto"/> with information about the user or a <see cref="NotFoundResult"/> if the user was not found.</returns> | ||||
|         [HttpGet("{userId}")] | ||||
|         [Authorize(Policy = Policies.IgnoreSchedule)] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         [ProducesResponseType(StatusCodes.Status404NotFound)] | ||||
|         public ActionResult<UserDto> GetUserById([FromRoute] Guid userId) | ||||
|         { | ||||
|             var user = _userManager.GetUserById(userId); | ||||
| 
 | ||||
|             if (user == null) | ||||
|             { | ||||
|                 return NotFound("User not found"); | ||||
|             } | ||||
| 
 | ||||
|             var result = _userManager.GetUserDto(user, HttpContext.Connection.RemoteIpAddress.ToString()); | ||||
|             return result; | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Deletes a user. | ||||
|         /// </summary> | ||||
|         /// <param name="userId">The user id.</param> | ||||
|         /// <response code="200">User deleted.</response> | ||||
|         /// <response code="404">User not found.</response> | ||||
|         /// <returns>A <see cref="NoContentResult"/> indicating success or a <see cref="NotFoundResult"/> if the user was not found.</returns> | ||||
|         [HttpDelete("{userId}")] | ||||
|         [Authorize(Policy = Policies.RequiresElevation)] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         [ProducesResponseType(StatusCodes.Status404NotFound)] | ||||
|         public ActionResult DeleteUser([FromRoute] Guid userId) | ||||
|         { | ||||
|             var user = _userManager.GetUserById(userId); | ||||
| 
 | ||||
|             if (user == null) | ||||
|             { | ||||
|                 return NotFound("User not found"); | ||||
|             } | ||||
| 
 | ||||
|             _sessionManager.RevokeUserTokens(user.Id, null); | ||||
|             _userManager.DeleteUser(user); | ||||
|             return NoContent(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Authenticates a user. | ||||
|         /// </summary> | ||||
|         /// <param name="userId">The user id.</param> | ||||
|         /// <param name="pw">The password as plain text.</param> | ||||
|         /// <param name="password">The password sha1-hash.</param> | ||||
|         /// <response code="200">User authenticated.</response> | ||||
|         /// <response code="403">Sha1-hashed password only is not allowed.</response> | ||||
|         /// <response code="404">User not found.</response> | ||||
|         /// <returns>A <see cref="Task"/> containing an <see cref="AuthenticationResult"/>.</returns> | ||||
|         [HttpPost("{userId}/Authenticate")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         [ProducesResponseType(StatusCodes.Status403Forbidden)] | ||||
|         [ProducesResponseType(StatusCodes.Status404NotFound)] | ||||
|         public async Task<ActionResult<AuthenticationResult>> AuthenticateUser( | ||||
|             [FromRoute, Required] Guid userId, | ||||
|             [FromQuery, BindRequired] string pw, | ||||
|             [FromQuery, BindRequired] string password) | ||||
|         { | ||||
|             var user = _userManager.GetUserById(userId); | ||||
| 
 | ||||
|             if (user == null) | ||||
|             { | ||||
|                 return NotFound("User not found"); | ||||
|             } | ||||
| 
 | ||||
|             if (!string.IsNullOrEmpty(password) && string.IsNullOrEmpty(pw)) | ||||
|             { | ||||
|                 return Forbid("Only sha1 password is not allowed."); | ||||
|             } | ||||
| 
 | ||||
|             // Password should always be null | ||||
|             AuthenticateUserByName request = new AuthenticateUserByName | ||||
|             { | ||||
|                 Username = user.Username, | ||||
|                 Password = null, | ||||
|                 Pw = pw | ||||
|             }; | ||||
|             return await AuthenticateUserByName(request).ConfigureAwait(false); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Authenticates a user by name. | ||||
|         /// </summary> | ||||
|         /// <param name="request">The <see cref="AuthenticateUserByName"/> request.</param> | ||||
|         /// <response code="200">User authenticated.</response> | ||||
|         /// <returns>A <see cref="Task"/> containing an <see cref="AuthenticationRequest"/> with information about the new session.</returns> | ||||
|         [HttpPost("AuthenticateByName")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         public async Task<ActionResult<AuthenticationResult>> AuthenticateUserByName([FromBody, BindRequired] AuthenticateUserByName request) | ||||
|         { | ||||
|             var auth = _authContext.GetAuthorizationInfo(Request); | ||||
| 
 | ||||
|             try | ||||
|             { | ||||
|                 var result = await _sessionManager.AuthenticateNewSession(new AuthenticationRequest | ||||
|                 { | ||||
|                     App = auth.Client, | ||||
|                     AppVersion = auth.Version, | ||||
|                     DeviceId = auth.DeviceId, | ||||
|                     DeviceName = auth.Device, | ||||
|                     Password = request.Pw, | ||||
|                     PasswordSha1 = request.Password, | ||||
|                     RemoteEndPoint = HttpContext.Connection.RemoteIpAddress.ToString(), | ||||
|                     Username = request.Username | ||||
|                 }).ConfigureAwait(false); | ||||
| 
 | ||||
|                 return result; | ||||
|             } | ||||
|             catch (SecurityException e) | ||||
|             { | ||||
|                 // rethrow adding IP address to message | ||||
|                 throw new SecurityException($"[{HttpContext.Connection.RemoteIpAddress}] {e.Message}", e); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Updates a user's password. | ||||
|         /// </summary> | ||||
|         /// <param name="userId">The user id.</param> | ||||
|         /// <param name="request">The <see cref="UpdateUserPassword"/> request.</param> | ||||
|         /// <response code="200">Password successfully reset.</response> | ||||
|         /// <response code="403">User is not allowed to update the password.</response> | ||||
|         /// <response code="404">User not found.</response> | ||||
|         /// <returns>A <see cref="NoContentResult"/> indicating success or a <see cref="ForbidResult"/> or a <see cref="NotFoundResult"/> on failure.</returns> | ||||
|         [HttpPost("{userId}/Password")] | ||||
|         [Authorize(Policy = Policies.DefaultAuthorization)] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         [ProducesResponseType(StatusCodes.Status403Forbidden)] | ||||
|         [ProducesResponseType(StatusCodes.Status404NotFound)] | ||||
|         public async Task<ActionResult> UpdateUserPassword( | ||||
|             [FromRoute] Guid userId, | ||||
|             [FromBody] UpdateUserPassword request) | ||||
|         { | ||||
|             if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true)) | ||||
|             { | ||||
|                 return Forbid("User is not allowed to update the password."); | ||||
|             } | ||||
| 
 | ||||
|             var user = _userManager.GetUserById(userId); | ||||
| 
 | ||||
|             if (user == null) | ||||
|             { | ||||
|                 return NotFound("User not found"); | ||||
|             } | ||||
| 
 | ||||
|             if (request.ResetPassword) | ||||
|             { | ||||
|                 await _userManager.ResetPassword(user).ConfigureAwait(false); | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 var success = await _userManager.AuthenticateUser( | ||||
|                     user.Username, | ||||
|                     request.CurrentPw, | ||||
|                     request.CurrentPw, | ||||
|                     HttpContext.Connection.RemoteIpAddress.ToString(), | ||||
|                     false).ConfigureAwait(false); | ||||
| 
 | ||||
|                 if (success == null) | ||||
|                 { | ||||
|                     return Forbid("Invalid user or password entered."); | ||||
|                 } | ||||
| 
 | ||||
|                 await _userManager.ChangePassword(user, request.NewPw).ConfigureAwait(false); | ||||
| 
 | ||||
|                 var currentToken = _authContext.GetAuthorizationInfo(Request).Token; | ||||
| 
 | ||||
|                 _sessionManager.RevokeUserTokens(user.Id, currentToken); | ||||
|             } | ||||
| 
 | ||||
|             return NoContent(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Updates a user's easy password. | ||||
|         /// </summary> | ||||
|         /// <param name="userId">The user id.</param> | ||||
|         /// <param name="request">The <see cref="UpdateUserEasyPassword"/> request.</param> | ||||
|         /// <response code="200">Password successfully reset.</response> | ||||
|         /// <response code="403">User is not allowed to update the password.</response> | ||||
|         /// <response code="404">User not found.</response> | ||||
|         /// <returns>A <see cref="NoContentResult"/> indicating success or a <see cref="ForbidResult"/> or a <see cref="NotFoundResult"/> on failure.</returns> | ||||
|         [HttpPost("{userId}/EasyPassword")] | ||||
|         [Authorize(Policy = Policies.DefaultAuthorization)] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         [ProducesResponseType(StatusCodes.Status403Forbidden)] | ||||
|         [ProducesResponseType(StatusCodes.Status404NotFound)] | ||||
|         public ActionResult UpdateUserEasyPassword( | ||||
|             [FromRoute] Guid userId, | ||||
|             [FromBody] UpdateUserEasyPassword request) | ||||
|         { | ||||
|             if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true)) | ||||
|             { | ||||
|                 return Forbid("User is not allowed to update the easy password."); | ||||
|             } | ||||
| 
 | ||||
|             var user = _userManager.GetUserById(userId); | ||||
| 
 | ||||
|             if (user == null) | ||||
|             { | ||||
|                 return NotFound("User not found"); | ||||
|             } | ||||
| 
 | ||||
|             if (request.ResetPassword) | ||||
|             { | ||||
|                 _userManager.ResetEasyPassword(user); | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 _userManager.ChangeEasyPassword(user, request.NewPw, request.NewPassword); | ||||
|             } | ||||
| 
 | ||||
|             return NoContent(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Updates a user. | ||||
|         /// </summary> | ||||
|         /// <param name="userId">The user id.</param> | ||||
|         /// <param name="updateUser">The updated user model.</param> | ||||
|         /// <response code="204">User updated.</response> | ||||
|         /// <response code="400">User information was not supplied.</response> | ||||
|         /// <response code="403">User update forbidden.</response> | ||||
|         /// <returns>A <see cref="NoContentResult"/> indicating success or a <see cref="BadRequestResult"/> or a <see cref="ForbidResult"/> on failure.</returns> | ||||
|         [HttpPost("{userId}")] | ||||
|         [Authorize(Policy = Policies.DefaultAuthorization)] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         [ProducesResponseType(StatusCodes.Status400BadRequest)] | ||||
|         [ProducesResponseType(StatusCodes.Status403Forbidden)] | ||||
|         public async Task<ActionResult> UpdateUser( | ||||
|             [FromRoute] Guid userId, | ||||
|             [FromBody] UserDto updateUser) | ||||
|         { | ||||
|             if (updateUser == null) | ||||
|             { | ||||
|                 return BadRequest(); | ||||
|             } | ||||
| 
 | ||||
|             if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, false)) | ||||
|             { | ||||
|                 return Forbid("User update not allowed."); | ||||
|             } | ||||
| 
 | ||||
|             var user = _userManager.GetUserById(userId); | ||||
| 
 | ||||
|             if (string.Equals(user.Username, updateUser.Name, StringComparison.Ordinal)) | ||||
|             { | ||||
|                 await _userManager.UpdateUserAsync(user).ConfigureAwait(false); | ||||
|                 _userManager.UpdateConfiguration(user.Id, updateUser.Configuration); | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 await _userManager.RenameUser(user, updateUser.Name).ConfigureAwait(false); | ||||
|                 _userManager.UpdateConfiguration(updateUser.Id, updateUser.Configuration); | ||||
|             } | ||||
| 
 | ||||
|             return NoContent(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Updates a user policy. | ||||
|         /// </summary> | ||||
|         /// <param name="userId">The user id.</param> | ||||
|         /// <param name="newPolicy">The new user policy.</param> | ||||
|         /// <response code="204">User policy updated.</response> | ||||
|         /// <response code="400">User policy was not supplied.</response> | ||||
|         /// <response code="403">User policy update forbidden.</response> | ||||
|         /// <returns>A <see cref="NoContentResult"/> indicating success or a <see cref="BadRequestResult"/> or a <see cref="ForbidResult"/> on failure..</returns> | ||||
|         [HttpPost("{userId}/Policy")] | ||||
|         [Authorize(Policy = Policies.DefaultAuthorization)] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         [ProducesResponseType(StatusCodes.Status400BadRequest)] | ||||
|         [ProducesResponseType(StatusCodes.Status403Forbidden)] | ||||
|         public ActionResult UpdateUserPolicy( | ||||
|             [FromRoute] Guid userId, | ||||
|             [FromBody] UserPolicy newPolicy) | ||||
|         { | ||||
|             if (newPolicy == null) | ||||
|             { | ||||
|                 return BadRequest(); | ||||
|             } | ||||
| 
 | ||||
|             var user = _userManager.GetUserById(userId); | ||||
| 
 | ||||
|             // If removing admin access | ||||
|             if (!(newPolicy.IsAdministrator && user.HasPermission(PermissionKind.IsAdministrator))) | ||||
|             { | ||||
|                 if (_userManager.Users.Count(i => i.HasPermission(PermissionKind.IsAdministrator)) == 1) | ||||
|                 { | ||||
|                     return Forbid("There must be at least one user in the system with administrative access."); | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             // If disabling | ||||
|             if (newPolicy.IsDisabled && user.HasPermission(PermissionKind.IsAdministrator)) | ||||
|             { | ||||
|                 return Forbid("Administrators cannot be disabled."); | ||||
|             } | ||||
| 
 | ||||
|             // If disabling | ||||
|             if (newPolicy.IsDisabled && !user.HasPermission(PermissionKind.IsDisabled)) | ||||
|             { | ||||
|                 if (_userManager.Users.Count(i => !i.HasPermission(PermissionKind.IsDisabled)) == 1) | ||||
|                 { | ||||
|                     return Forbid("There must be at least one enabled user in the system."); | ||||
|                 } | ||||
| 
 | ||||
|                 var currentToken = _authContext.GetAuthorizationInfo(Request).Token; | ||||
|                 _sessionManager.RevokeUserTokens(user.Id, currentToken); | ||||
|             } | ||||
| 
 | ||||
|             _userManager.UpdatePolicy(userId, newPolicy); | ||||
| 
 | ||||
|             return NoContent(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Updates a user configuration. | ||||
|         /// </summary> | ||||
|         /// <param name="userId">The user id.</param> | ||||
|         /// <param name="userConfig">The new user configuration.</param> | ||||
|         /// <response code="204">User configuration updated.</response> | ||||
|         /// <response code="403">User configuration update forbidden.</response> | ||||
|         /// <returns>A <see cref="NoContentResult"/> indicating success.</returns> | ||||
|         [HttpPost("{userId}/Configuration")] | ||||
|         [Authorize(Policy = Policies.DefaultAuthorization)] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         [ProducesResponseType(StatusCodes.Status403Forbidden)] | ||||
|         public ActionResult UpdateUserConfiguration( | ||||
|             [FromRoute] Guid userId, | ||||
|             [FromBody] UserConfiguration userConfig) | ||||
|         { | ||||
|             if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, false)) | ||||
|             { | ||||
|                 return Forbid("User configuration update not allowed"); | ||||
|             } | ||||
| 
 | ||||
|             _userManager.UpdateConfiguration(userId, userConfig); | ||||
| 
 | ||||
|             return NoContent(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Creates a user. | ||||
|         /// </summary> | ||||
|         /// <param name="request">The create user by name request body.</param> | ||||
|         /// <response code="200">User created.</response> | ||||
|         /// <returns>An <see cref="UserDto"/> of the new user.</returns> | ||||
|         [HttpPost("/Users/New")] | ||||
|         [Authorize(Policy = Policies.RequiresElevation)] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         public async Task<ActionResult<UserDto>> CreateUserByName([FromBody] CreateUserByName request) | ||||
|         { | ||||
|             var newUser = _userManager.CreateUser(request.Name); | ||||
| 
 | ||||
|             // no need to authenticate password for new user | ||||
|             if (request.Password != null) | ||||
|             { | ||||
|                 await _userManager.ChangePassword(newUser, request.Password).ConfigureAwait(false); | ||||
|             } | ||||
| 
 | ||||
|             var result = _userManager.GetUserDto(newUser, HttpContext.Connection.RemoteIpAddress.ToString()); | ||||
| 
 | ||||
|             return result; | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Initiates the forgot password process for a local user. | ||||
|         /// </summary> | ||||
|         /// <param name="enteredUsername">The entered username.</param> | ||||
|         /// <response code="200">Password reset process started.</response> | ||||
|         /// <returns>A <see cref="Task"/> containing a <see cref="ForgotPasswordResult"/>.</returns> | ||||
|         [HttpPost("ForgotPassword")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         public async Task<ActionResult<ForgotPasswordResult>> ForgotPassword([FromBody] string enteredUsername) | ||||
|         { | ||||
|             var isLocal = HttpContext.Connection.RemoteIpAddress.Equals(HttpContext.Connection.LocalIpAddress) | ||||
|                           || _networkManager.IsInLocalNetwork(HttpContext.Connection.RemoteIpAddress.ToString()); | ||||
| 
 | ||||
|             var result = await _userManager.StartForgotPasswordProcess(enteredUsername, isLocal).ConfigureAwait(false); | ||||
| 
 | ||||
|             return result; | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Redeems a forgot password pin. | ||||
|         /// </summary> | ||||
|         /// <param name="pin">The pin.</param> | ||||
|         /// <response code="200">Pin reset process started.</response> | ||||
|         /// <returns>A <see cref="Task"/> containing a <see cref="PinRedeemResult"/>.</returns> | ||||
|         [HttpPost("ForgotPassword/Pin")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         public async Task<ActionResult<PinRedeemResult>> ForgotPasswordPin([FromBody] string pin) | ||||
|         { | ||||
|             var result = await _userManager.RedeemPasswordResetPin(pin).ConfigureAwait(false); | ||||
|             return result; | ||||
|         } | ||||
| 
 | ||||
|         private IEnumerable<UserDto> Get(bool? isHidden, bool? isDisabled, bool filterByDevice, bool filterByNetwork) | ||||
|         { | ||||
|             var users = _userManager.Users; | ||||
| 
 | ||||
|             if (isDisabled.HasValue) | ||||
|             { | ||||
|                 users = users.Where(i => i.HasPermission(PermissionKind.IsDisabled) == isDisabled.Value); | ||||
|             } | ||||
| 
 | ||||
|             if (isHidden.HasValue) | ||||
|             { | ||||
|                 users = users.Where(i => i.HasPermission(PermissionKind.IsHidden) == isHidden.Value); | ||||
|             } | ||||
| 
 | ||||
|             if (filterByDevice) | ||||
|             { | ||||
|                 var deviceId = _authContext.GetAuthorizationInfo(Request).DeviceId; | ||||
| 
 | ||||
|                 if (!string.IsNullOrWhiteSpace(deviceId)) | ||||
|                 { | ||||
|                     users = users.Where(i => _deviceManager.CanAccessDevice(i, deviceId)); | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             if (filterByNetwork) | ||||
|             { | ||||
|                 if (!_networkManager.IsInLocalNetwork(HttpContext.Connection.RemoteIpAddress.ToString())) | ||||
|                 { | ||||
|                     users = users.Where(i => i.HasPermission(PermissionKind.EnableRemoteAccess)); | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             var result = users | ||||
|                 .OrderBy(u => u.Username) | ||||
|                 .Select(i => _userManager.GetUserDto(i, HttpContext.Connection.RemoteIpAddress.ToString())); | ||||
| 
 | ||||
|             return result; | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -1,9 +1,8 @@ | ||||
| #nullable enable | ||||
| 
 | ||||
| using System; | ||||
| using System.Net.Mime; | ||||
| using System.Threading; | ||||
| using System.Threading.Tasks; | ||||
| using Jellyfin.Api.Constants; | ||||
| using MediaBrowser.Common.Extensions; | ||||
| using MediaBrowser.Controller.Library; | ||||
| using MediaBrowser.Controller.MediaEncoding; | ||||
| @ -17,7 +16,7 @@ namespace Jellyfin.Api.Controllers | ||||
|     /// Attachments controller. | ||||
|     /// </summary> | ||||
|     [Route("Videos")] | ||||
|     [Authorize] | ||||
|     [Authorize(Policy = Policies.DefaultAuthorization)] | ||||
|     public class VideoAttachmentsController : BaseJellyfinApiController | ||||
|     { | ||||
|         private readonly ILibraryManager _libraryManager; | ||||
| @ -45,7 +44,7 @@ namespace Jellyfin.Api.Controllers | ||||
|         /// <response code="200">Attachment retrieved.</response> | ||||
|         /// <response code="404">Video or attachment not found.</response> | ||||
|         /// <returns>An <see cref="FileStreamResult"/> containing the attachment stream on success, or a <see cref="NotFoundResult"/> if the attachment could not be found.</returns> | ||||
|         [HttpGet("{VideoID}/{MediaSourceID}/Attachments/{Index}")] | ||||
|         [HttpGet("{videoId}/{mediaSourceId}/Attachments/{index}")] | ||||
|         [Produces(MediaTypeNames.Application.Octet)] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         [ProducesResponseType(StatusCodes.Status404NotFound)] | ||||
|  | ||||
							
								
								
									
										202
									
								
								Jellyfin.Api/Controllers/VideosController.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										202
									
								
								Jellyfin.Api/Controllers/VideosController.cs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,202 @@ | ||||
| using System; | ||||
| using System.Globalization; | ||||
| using System.Linq; | ||||
| using System.Threading; | ||||
| using Jellyfin.Api.Constants; | ||||
| using Jellyfin.Api.Extensions; | ||||
| using Jellyfin.Api.Helpers; | ||||
| using MediaBrowser.Controller.Dto; | ||||
| using MediaBrowser.Controller.Entities; | ||||
| using MediaBrowser.Controller.Library; | ||||
| using MediaBrowser.Model.Dto; | ||||
| using MediaBrowser.Model.Entities; | ||||
| using MediaBrowser.Model.Querying; | ||||
| using Microsoft.AspNetCore.Authorization; | ||||
| using Microsoft.AspNetCore.Http; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| 
 | ||||
| namespace Jellyfin.Api.Controllers | ||||
| { | ||||
|     /// <summary> | ||||
|     /// The videos controller. | ||||
|     /// </summary> | ||||
|     [Route("Videos")] | ||||
|     public class VideosController : BaseJellyfinApiController | ||||
|     { | ||||
|         private readonly ILibraryManager _libraryManager; | ||||
|         private readonly IUserManager _userManager; | ||||
|         private readonly IDtoService _dtoService; | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="VideosController"/> class. | ||||
|         /// </summary> | ||||
|         /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param> | ||||
|         /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param> | ||||
|         /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param> | ||||
|         public VideosController( | ||||
|             ILibraryManager libraryManager, | ||||
|             IUserManager userManager, | ||||
|             IDtoService dtoService) | ||||
|         { | ||||
|             _libraryManager = libraryManager; | ||||
|             _userManager = userManager; | ||||
|             _dtoService = dtoService; | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets additional parts for a video. | ||||
|         /// </summary> | ||||
|         /// <param name="itemId">The item id.</param> | ||||
|         /// <param name="userId">Optional. Filter by user id, and attach user data.</param> | ||||
|         /// <response code="200">Additional parts returned.</response> | ||||
|         /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the parts.</returns> | ||||
|         [HttpGet("{itemId}/AdditionalParts")] | ||||
|         [Authorize(Policy = Policies.DefaultAuthorization)] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         public ActionResult<QueryResult<BaseItemDto>> GetAdditionalPart([FromRoute] Guid itemId, [FromQuery] Guid userId) | ||||
|         { | ||||
|             var user = !userId.Equals(Guid.Empty) ? _userManager.GetUserById(userId) : null; | ||||
| 
 | ||||
|             var item = itemId.Equals(Guid.Empty) | ||||
|                 ? (!userId.Equals(Guid.Empty) | ||||
|                     ? _libraryManager.GetUserRootFolder() | ||||
|                     : _libraryManager.RootFolder) | ||||
|                 : _libraryManager.GetItemById(itemId); | ||||
| 
 | ||||
|             var dtoOptions = new DtoOptions(); | ||||
|             dtoOptions = dtoOptions.AddClientFields(Request); | ||||
| 
 | ||||
|             BaseItemDto[] items; | ||||
|             if (item is Video video) | ||||
|             { | ||||
|                 items = video.GetAdditionalParts() | ||||
|                     .Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, video)) | ||||
|                     .ToArray(); | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 items = Array.Empty<BaseItemDto>(); | ||||
|             } | ||||
| 
 | ||||
|             var result = new QueryResult<BaseItemDto> | ||||
|             { | ||||
|                 Items = items, | ||||
|                 TotalRecordCount = items.Length | ||||
|             }; | ||||
| 
 | ||||
|             return result; | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Removes alternate video sources. | ||||
|         /// </summary> | ||||
|         /// <param name="itemId">The item id.</param> | ||||
|         /// <response code="204">Alternate sources deleted.</response> | ||||
|         /// <response code="404">Video not found.</response> | ||||
|         /// <returns>A <see cref="NoContentResult"/> indicating success, or a <see cref="NotFoundResult"/> if the video doesn't exist.</returns> | ||||
|         [HttpDelete("{itemId}/AlternateSources")] | ||||
|         [Authorize(Policy = Policies.RequiresElevation)] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         [ProducesResponseType(StatusCodes.Status404NotFound)] | ||||
|         public ActionResult DeleteAlternateSources([FromRoute] Guid itemId) | ||||
|         { | ||||
|             var video = (Video)_libraryManager.GetItemById(itemId); | ||||
| 
 | ||||
|             if (video == null) | ||||
|             { | ||||
|                 return NotFound("The video either does not exist or the id does not belong to a video."); | ||||
|             } | ||||
| 
 | ||||
|             foreach (var link in video.GetLinkedAlternateVersions()) | ||||
|             { | ||||
|                 link.SetPrimaryVersionId(null); | ||||
|                 link.LinkedAlternateVersions = Array.Empty<LinkedChild>(); | ||||
| 
 | ||||
|                 link.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None); | ||||
|             } | ||||
| 
 | ||||
|             video.LinkedAlternateVersions = Array.Empty<LinkedChild>(); | ||||
|             video.SetPrimaryVersionId(null); | ||||
|             video.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None); | ||||
| 
 | ||||
|             return NoContent(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Merges videos into a single record. | ||||
|         /// </summary> | ||||
|         /// <param name="itemIds">Item id list. This allows multiple, comma delimited.</param> | ||||
|         /// <response code="204">Videos merged.</response> | ||||
|         /// <response code="400">Supply at least 2 video ids.</response> | ||||
|         /// <returns>A <see cref="NoContentResult"/> indicating success, or a <see cref="BadRequestResult"/> if less than two ids were supplied.</returns> | ||||
|         [HttpPost("MergeVersions")] | ||||
|         [Authorize(Policy = Policies.RequiresElevation)] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         [ProducesResponseType(StatusCodes.Status400BadRequest)] | ||||
|         public ActionResult MergeVersions([FromQuery] string itemIds) | ||||
|         { | ||||
|             var items = RequestHelpers.Split(itemIds, ',', true) | ||||
|                 .Select(i => _libraryManager.GetItemById(i)) | ||||
|                 .OfType<Video>() | ||||
|                 .OrderBy(i => i.Id) | ||||
|                 .ToList(); | ||||
| 
 | ||||
|             if (items.Count < 2) | ||||
|             { | ||||
|                 return BadRequest("Please supply at least two videos to merge."); | ||||
|             } | ||||
| 
 | ||||
|             var videosWithVersions = items.Where(i => i.MediaSourceCount > 1).ToList(); | ||||
| 
 | ||||
|             var primaryVersion = videosWithVersions.FirstOrDefault(); | ||||
|             if (primaryVersion == null) | ||||
|             { | ||||
|                 primaryVersion = items | ||||
|                     .OrderBy(i => | ||||
|                     { | ||||
|                         if (i.Video3DFormat.HasValue || i.VideoType != VideoType.VideoFile) | ||||
|                         { | ||||
|                             return 1; | ||||
|                         } | ||||
| 
 | ||||
|                         return 0; | ||||
|                     }) | ||||
|                     .ThenByDescending(i => i.GetDefaultVideoStream()?.Width ?? 0) | ||||
|                     .First(); | ||||
|             } | ||||
| 
 | ||||
|             var list = primaryVersion.LinkedAlternateVersions.ToList(); | ||||
| 
 | ||||
|             foreach (var item in items.Where(i => i.Id != primaryVersion.Id)) | ||||
|             { | ||||
|                 item.SetPrimaryVersionId(primaryVersion.Id.ToString("N", CultureInfo.InvariantCulture)); | ||||
| 
 | ||||
|                 item.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None); | ||||
| 
 | ||||
|                 list.Add(new LinkedChild | ||||
|                 { | ||||
|                     Path = item.Path, | ||||
|                     ItemId = item.Id | ||||
|                 }); | ||||
| 
 | ||||
|                 foreach (var linkedItem in item.LinkedAlternateVersions) | ||||
|                 { | ||||
|                     if (!list.Any(i => string.Equals(i.Path, linkedItem.Path, StringComparison.OrdinalIgnoreCase))) | ||||
|                     { | ||||
|                         list.Add(linkedItem); | ||||
|                     } | ||||
|                 } | ||||
| 
 | ||||
|                 if (item.LinkedAlternateVersions.Length > 0) | ||||
|                 { | ||||
|                     item.LinkedAlternateVersions = Array.Empty<LinkedChild>(); | ||||
|                     item.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None); | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             primaryVersion.LinkedAlternateVersions = list.ToArray(); | ||||
|             primaryVersion.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None); | ||||
|             return NoContent(); | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										162
									
								
								Jellyfin.Api/Extensions/DtoExtensions.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										162
									
								
								Jellyfin.Api/Extensions/DtoExtensions.cs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,162 @@ | ||||
| using System; | ||||
| using System.Linq; | ||||
| using Jellyfin.Api.Helpers; | ||||
| using MediaBrowser.Controller.Dto; | ||||
| using MediaBrowser.Model.Entities; | ||||
| using MediaBrowser.Model.Querying; | ||||
| using Microsoft.AspNetCore.Http; | ||||
| 
 | ||||
| namespace Jellyfin.Api.Extensions | ||||
| { | ||||
|     /// <summary> | ||||
|     /// Dto Extensions. | ||||
|     /// </summary> | ||||
|     public static class DtoExtensions | ||||
|     { | ||||
|         /// <summary> | ||||
|         /// Add Dto Item fields. | ||||
|         /// </summary> | ||||
|         /// <remarks> | ||||
|         /// Converted from IHasItemFields. | ||||
|         /// Legacy order: 1. | ||||
|         /// </remarks> | ||||
|         /// <param name="dtoOptions">DtoOptions object.</param> | ||||
|         /// <param name="fields">Comma delimited string of fields.</param> | ||||
|         /// <returns>Modified DtoOptions object.</returns> | ||||
|         internal static DtoOptions AddItemFields(this DtoOptions dtoOptions, string fields) | ||||
|         { | ||||
|             if (string.IsNullOrEmpty(fields)) | ||||
|             { | ||||
|                 dtoOptions.Fields = Array.Empty<ItemFields>(); | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 dtoOptions.Fields = fields.Split(',') | ||||
|                     .Select(v => | ||||
|                     { | ||||
|                         if (Enum.TryParse(v, true, out ItemFields value)) | ||||
|                         { | ||||
|                             return (ItemFields?)value; | ||||
|                         } | ||||
| 
 | ||||
|                         return null; | ||||
|                     }) | ||||
|                     .Where(i => i.HasValue) | ||||
|                     .Select(i => i!.Value) | ||||
|                     .ToArray(); | ||||
|             } | ||||
| 
 | ||||
|             return dtoOptions; | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Add additional fields depending on client. | ||||
|         /// </summary> | ||||
|         /// <remarks> | ||||
|         /// Use in place of GetDtoOptions. | ||||
|         /// Legacy order: 2. | ||||
|         /// </remarks> | ||||
|         /// <param name="dtoOptions">DtoOptions object.</param> | ||||
|         /// <param name="request">Current request.</param> | ||||
|         /// <returns>Modified DtoOptions object.</returns> | ||||
|         internal static DtoOptions AddClientFields( | ||||
|             this DtoOptions dtoOptions, HttpRequest request) | ||||
|         { | ||||
|             dtoOptions.Fields ??= Array.Empty<ItemFields>(); | ||||
| 
 | ||||
|             string? client = ClaimHelpers.GetClient(request.HttpContext.User); | ||||
| 
 | ||||
|             // No client in claim | ||||
|             if (string.IsNullOrEmpty(client)) | ||||
|             { | ||||
|                 return dtoOptions; | ||||
|             } | ||||
| 
 | ||||
|             if (!dtoOptions.ContainsField(ItemFields.RecursiveItemCount)) | ||||
|             { | ||||
|                 if (client.IndexOf("kodi", StringComparison.OrdinalIgnoreCase) != -1 || | ||||
|                     client.IndexOf("wmc", StringComparison.OrdinalIgnoreCase) != -1 || | ||||
|                     client.IndexOf("media center", StringComparison.OrdinalIgnoreCase) != -1 || | ||||
|                     client.IndexOf("classic", StringComparison.OrdinalIgnoreCase) != -1) | ||||
|                 { | ||||
|                     int oldLen = dtoOptions.Fields.Length; | ||||
|                     var arr = new ItemFields[oldLen + 1]; | ||||
|                     dtoOptions.Fields.CopyTo(arr, 0); | ||||
|                     arr[oldLen] = ItemFields.RecursiveItemCount; | ||||
|                     dtoOptions.Fields = arr; | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             if (!dtoOptions.ContainsField(ItemFields.ChildCount)) | ||||
|             { | ||||
|                 if (client.IndexOf("kodi", StringComparison.OrdinalIgnoreCase) != -1 || | ||||
|                     client.IndexOf("wmc", StringComparison.OrdinalIgnoreCase) != -1 || | ||||
|                     client.IndexOf("media center", StringComparison.OrdinalIgnoreCase) != -1 || | ||||
|                     client.IndexOf("classic", StringComparison.OrdinalIgnoreCase) != -1 || | ||||
|                     client.IndexOf("roku", StringComparison.OrdinalIgnoreCase) != -1 || | ||||
|                     client.IndexOf("samsung", StringComparison.OrdinalIgnoreCase) != -1 || | ||||
|                     client.IndexOf("androidtv", StringComparison.OrdinalIgnoreCase) != -1) | ||||
|                 { | ||||
|                     int oldLen = dtoOptions.Fields.Length; | ||||
|                     var arr = new ItemFields[oldLen + 1]; | ||||
|                     dtoOptions.Fields.CopyTo(arr, 0); | ||||
|                     arr[oldLen] = ItemFields.ChildCount; | ||||
|                     dtoOptions.Fields = arr; | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             return dtoOptions; | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Add additional DtoOptions. | ||||
|         /// </summary> | ||||
|         /// <remarks> | ||||
|         /// Converted from IHasDtoOptions. | ||||
|         /// Legacy order: 3. | ||||
|         /// </remarks> | ||||
|         /// <param name="dtoOptions">DtoOptions object.</param> | ||||
|         /// <param name="enableImages">Enable images.</param> | ||||
|         /// <param name="enableUserData">Enable user data.</param> | ||||
|         /// <param name="imageTypeLimit">Image type limit.</param> | ||||
|         /// <param name="enableImageTypes">Enable image types.</param> | ||||
|         /// <returns>Modified DtoOptions object.</returns> | ||||
|         internal static DtoOptions AddAdditionalDtoOptions( | ||||
|             this DtoOptions dtoOptions, | ||||
|             bool? enableImages, | ||||
|             bool? enableUserData, | ||||
|             int? imageTypeLimit, | ||||
|             string enableImageTypes) | ||||
|         { | ||||
|             dtoOptions.EnableImages = enableImages ?? true; | ||||
| 
 | ||||
|             if (imageTypeLimit.HasValue) | ||||
|             { | ||||
|                 dtoOptions.ImageTypeLimit = imageTypeLimit.Value; | ||||
|             } | ||||
| 
 | ||||
|             if (enableUserData.HasValue) | ||||
|             { | ||||
|                 dtoOptions.EnableUserData = enableUserData.Value; | ||||
|             } | ||||
| 
 | ||||
|             if (!string.IsNullOrWhiteSpace(enableImageTypes)) | ||||
|             { | ||||
|                 dtoOptions.ImageTypes = enableImageTypes.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) | ||||
|                     .Select(v => (ImageType)Enum.Parse(typeof(ImageType), v, true)) | ||||
|                     .ToArray(); | ||||
|             } | ||||
| 
 | ||||
|             return dtoOptions; | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Check if DtoOptions contains field. | ||||
|         /// </summary> | ||||
|         /// <param name="dtoOptions">DtoOptions object.</param> | ||||
|         /// <param name="field">Field to check.</param> | ||||
|         /// <returns>Field existence.</returns> | ||||
|         internal static bool ContainsField(this DtoOptions dtoOptions, ItemFields field) | ||||
|             => dtoOptions.Fields != null && dtoOptions.Fields.Contains(field); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										75
									
								
								Jellyfin.Api/Helpers/ClaimHelpers.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								Jellyfin.Api/Helpers/ClaimHelpers.cs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,75 @@ | ||||
| using System; | ||||
| using System.Linq; | ||||
| using System.Security.Claims; | ||||
| using Jellyfin.Api.Constants; | ||||
| 
 | ||||
| namespace Jellyfin.Api.Helpers | ||||
| { | ||||
|     /// <summary> | ||||
|     /// Claim Helpers. | ||||
|     /// </summary> | ||||
|     public static class ClaimHelpers | ||||
|     { | ||||
|         /// <summary> | ||||
|         /// Get user id from claims. | ||||
|         /// </summary> | ||||
|         /// <param name="user">Current claims principal.</param> | ||||
|         /// <returns>User id.</returns> | ||||
|         public static Guid? GetUserId(in ClaimsPrincipal user) | ||||
|         { | ||||
|             var value = GetClaimValue(user, InternalClaimTypes.UserId); | ||||
|             return string.IsNullOrEmpty(value) | ||||
|                 ? null | ||||
|                 : (Guid?)Guid.Parse(value); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Get device id from claims. | ||||
|         /// </summary> | ||||
|         /// <param name="user">Current claims principal.</param> | ||||
|         /// <returns>Device id.</returns> | ||||
|         public static string? GetDeviceId(in ClaimsPrincipal user) | ||||
|             => GetClaimValue(user, InternalClaimTypes.DeviceId); | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Get device from claims. | ||||
|         /// </summary> | ||||
|         /// <param name="user">Current claims principal.</param> | ||||
|         /// <returns>Device.</returns> | ||||
|         public static string? GetDevice(in ClaimsPrincipal user) | ||||
|             => GetClaimValue(user, InternalClaimTypes.Device); | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Get client from claims. | ||||
|         /// </summary> | ||||
|         /// <param name="user">Current claims principal.</param> | ||||
|         /// <returns>Client.</returns> | ||||
|         public static string? GetClient(in ClaimsPrincipal user) | ||||
|             => GetClaimValue(user, InternalClaimTypes.Client); | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Get version from claims. | ||||
|         /// </summary> | ||||
|         /// <param name="user">Current claims principal.</param> | ||||
|         /// <returns>Version.</returns> | ||||
|         public static string? GetVersion(in ClaimsPrincipal user) | ||||
|             => GetClaimValue(user, InternalClaimTypes.Version); | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Get token from claims. | ||||
|         /// </summary> | ||||
|         /// <param name="user">Current claims principal.</param> | ||||
|         /// <returns>Token.</returns> | ||||
|         public static string? GetToken(in ClaimsPrincipal user) | ||||
|             => GetClaimValue(user, InternalClaimTypes.Token); | ||||
| 
 | ||||
|         private static string? GetClaimValue(in ClaimsPrincipal user, string name) | ||||
|         { | ||||
|             return user?.Identities | ||||
|                 .SelectMany(c => c.Claims) | ||||
|                 .Where(claim => claim.Type.Equals(name, StringComparison.OrdinalIgnoreCase)) | ||||
|                 .Select(claim => claim.Value) | ||||
|                 .FirstOrDefault(); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -1,8 +1,12 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.Linq; | ||||
| using Jellyfin.Data.Enums; | ||||
| using MediaBrowser.Model.Entities; | ||||
| using MediaBrowser.Model.Querying; | ||||
| using MediaBrowser.Controller.Net; | ||||
| using MediaBrowser.Controller.Session; | ||||
| using Microsoft.AspNetCore.Http; | ||||
| 
 | ||||
| namespace Jellyfin.Api.Helpers | ||||
| { | ||||
| @ -104,5 +108,66 @@ namespace Jellyfin.Api.Helpers | ||||
|                 ? value.Split(new[] { separator }, StringSplitOptions.RemoveEmptyEntries) | ||||
|                 : value.Split(separator); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Checks if the user can update an entry. | ||||
|         /// </summary> | ||||
|         /// <param name="authContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param> | ||||
|         /// <param name="requestContext">The <see cref="HttpRequest"/>.</param> | ||||
|         /// <param name="userId">The user id.</param> | ||||
|         /// <param name="restrictUserPreferences">Whether to restrict the user preferences.</param> | ||||
|         /// <returns>A <see cref="bool"/> whether the user can update the entry.</returns> | ||||
|         internal static bool AssertCanUpdateUser(IAuthorizationContext authContext, HttpRequest requestContext, Guid userId, bool restrictUserPreferences) | ||||
|         { | ||||
|             var auth = authContext.GetAuthorizationInfo(requestContext); | ||||
| 
 | ||||
|             var authenticatedUser = auth.User; | ||||
| 
 | ||||
|             // If they're going to update the record of another user, they must be an administrator | ||||
|             if ((!userId.Equals(auth.UserId) && !authenticatedUser.HasPermission(PermissionKind.IsAdministrator)) | ||||
|                 || (restrictUserPreferences && !authenticatedUser.EnableUserPreferenceAccess)) | ||||
|             { | ||||
|                 return false; | ||||
|             } | ||||
| 
 | ||||
|             return true; | ||||
|         } | ||||
| 
 | ||||
|         internal static SessionInfo GetSession(ISessionManager sessionManager, IAuthorizationContext authContext, HttpRequest request) | ||||
|         { | ||||
|             var authorization = authContext.GetAuthorizationInfo(request); | ||||
|             var user = authorization.User; | ||||
|             var session = sessionManager.LogSessionActivity( | ||||
|                 authorization.Client, | ||||
|                 authorization.Version, | ||||
|                 authorization.DeviceId, | ||||
|                 authorization.Device, | ||||
|                 request.HttpContext.Connection.RemoteIpAddress.ToString(), | ||||
|                 user); | ||||
| 
 | ||||
|             if (session == null) | ||||
|             { | ||||
|                 throw new ArgumentException("Session not found."); | ||||
|             } | ||||
| 
 | ||||
|             return session; | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Get Guid array from string. | ||||
|         /// </summary> | ||||
|         /// <param name="value">String value.</param> | ||||
|         /// <returns>Guid array.</returns> | ||||
|         internal static Guid[] GetGuids(string? value) | ||||
|         { | ||||
|             if (value == null) | ||||
|             { | ||||
|                 return Array.Empty<Guid>(); | ||||
|             } | ||||
| 
 | ||||
|             return value.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) | ||||
|                 .Select(i => new Guid(i)) | ||||
|                 .ToArray(); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -1,5 +1,3 @@ | ||||
| #nullable enable | ||||
| 
 | ||||
| namespace Jellyfin.Api.Models.ConfigurationDtos | ||||
| { | ||||
|     /// <summary> | ||||
|  | ||||
| @ -1,13 +1,18 @@ | ||||
| #pragma warning disable CS1591 | ||||
| 
 | ||||
| using MediaBrowser.Common.Plugins; | ||||
| using MediaBrowser.Common.Plugins; | ||||
| using MediaBrowser.Controller.Plugins; | ||||
| using MediaBrowser.Model.Plugins; | ||||
| 
 | ||||
| namespace MediaBrowser.WebDashboard.Api | ||||
| namespace Jellyfin.Api.Models | ||||
| { | ||||
|     /// <summary> | ||||
|     /// The configuration page info. | ||||
|     /// </summary> | ||||
|     public class ConfigurationPageInfo | ||||
|     { | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="ConfigurationPageInfo"/> class. | ||||
|         /// </summary> | ||||
|         /// <param name="page">Instance of <see cref="IPluginConfigurationPage"/> interface.</param> | ||||
|         public ConfigurationPageInfo(IPluginConfigurationPage page) | ||||
|         { | ||||
|             Name = page.Name; | ||||
| @ -22,6 +27,11 @@ namespace MediaBrowser.WebDashboard.Api | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="ConfigurationPageInfo"/> class. | ||||
|         /// </summary> | ||||
|         /// <param name="plugin">Instance of <see cref="IPlugin"/> interface.</param> | ||||
|         /// <param name="page">Instance of <see cref="PluginPageInfo"/> interface.</param> | ||||
|         public ConfigurationPageInfo(IPlugin plugin, PluginPageInfo page) | ||||
|         { | ||||
|             Name = page.Name; | ||||
| @ -40,13 +50,25 @@ namespace MediaBrowser.WebDashboard.Api | ||||
|         /// <value>The name.</value> | ||||
|         public string Name { get; set; } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets or sets a value indicating whether the configurations page is enabled in the main menu. | ||||
|         /// </summary> | ||||
|         public bool EnableInMainMenu { get; set; } | ||||
| 
 | ||||
|         public string MenuSection { get; set; } | ||||
|         /// <summary> | ||||
|         /// Gets or sets the menu section. | ||||
|         /// </summary> | ||||
|         public string? MenuSection { get; set; } | ||||
| 
 | ||||
|         public string MenuIcon { get; set; } | ||||
|         /// <summary> | ||||
|         /// Gets or sets the menu icon. | ||||
|         /// </summary> | ||||
|         public string? MenuIcon { get; set; } | ||||
| 
 | ||||
|         public string DisplayName { get; set; } | ||||
|         /// <summary> | ||||
|         /// Gets or sets the display name. | ||||
|         /// </summary> | ||||
|         public string? DisplayName { get; set; } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets or sets the type of the configuration page. | ||||
| @ -58,6 +80,6 @@ namespace MediaBrowser.WebDashboard.Api | ||||
|         /// Gets or sets the plugin id. | ||||
|         /// </summary> | ||||
|         /// <value>The plugin id.</value> | ||||
|         public string PluginId { get; set; } | ||||
|         public string? PluginId { get; set; } | ||||
|     } | ||||
| } | ||||
| @ -1,5 +1,3 @@ | ||||
| #nullable enable | ||||
| 
 | ||||
| using System; | ||||
| using MediaBrowser.Model.Notifications; | ||||
| 
 | ||||
|  | ||||
| @ -1,5 +1,3 @@ | ||||
| #nullable enable | ||||
| 
 | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| 
 | ||||
|  | ||||
| @ -1,5 +1,3 @@ | ||||
| #nullable enable | ||||
| 
 | ||||
| using MediaBrowser.Model.Notifications; | ||||
| 
 | ||||
| namespace Jellyfin.Api.Models.NotificationDtos | ||||
|  | ||||
							
								
								
									
										30
									
								
								Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,30 @@ | ||||
| using System; | ||||
| 
 | ||||
| namespace Jellyfin.Api.Models.PlaylistDtos | ||||
| { | ||||
|     /// <summary> | ||||
|     /// Create new playlist dto. | ||||
|     /// </summary> | ||||
|     public class CreatePlaylistDto | ||||
|     { | ||||
|         /// <summary> | ||||
|         /// Gets or sets the name of the new playlist. | ||||
|         /// </summary> | ||||
|         public string? Name { get; set; } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets or sets item ids to add to the playlist. | ||||
|         /// </summary> | ||||
|         public string? Ids { get; set; } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets or sets the user id. | ||||
|         /// </summary> | ||||
|         public Guid UserId { get; set; } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets or sets the media type. | ||||
|         /// </summary> | ||||
|         public string? MediaType { get; set; } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										40
									
								
								Jellyfin.Api/Models/PluginDtos/MBRegistrationRecord.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								Jellyfin.Api/Models/PluginDtos/MBRegistrationRecord.cs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,40 @@ | ||||
| using System; | ||||
| 
 | ||||
| namespace Jellyfin.Api.Models.PluginDtos | ||||
| { | ||||
|     /// <summary> | ||||
|     /// MB Registration Record. | ||||
|     /// </summary> | ||||
|     public class MBRegistrationRecord | ||||
|     { | ||||
|         /// <summary> | ||||
|         /// Gets or sets expiration date. | ||||
|         /// </summary> | ||||
|         public DateTime ExpirationDate { get; set; } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets or sets a value indicating whether is registered. | ||||
|         /// </summary> | ||||
|         public bool IsRegistered { get; set; } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets or sets a value indicating whether reg checked. | ||||
|         /// </summary> | ||||
|         public bool RegChecked { get; set; } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets or sets a value indicating whether reg error. | ||||
|         /// </summary> | ||||
|         public bool RegError { get; set; } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets or sets a value indicating whether trial version. | ||||
|         /// </summary> | ||||
|         public bool TrialVersion { get; set; } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets or sets a value indicating whether is valid. | ||||
|         /// </summary> | ||||
|         public bool IsValid { get; set; } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										18
									
								
								Jellyfin.Api/Models/PluginDtos/PluginSecurityInfo.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								Jellyfin.Api/Models/PluginDtos/PluginSecurityInfo.cs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,18 @@ | ||||
| namespace Jellyfin.Api.Models.PluginDtos | ||||
| { | ||||
|     /// <summary> | ||||
|     /// Plugin security info. | ||||
|     /// </summary> | ||||
|     public class PluginSecurityInfo | ||||
|     { | ||||
|         /// <summary> | ||||
|         /// Gets or sets the supporter key. | ||||
|         /// </summary> | ||||
|         public string? SupporterKey { get; set; } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets or sets a value indicating whether is mb supporter. | ||||
|         /// </summary> | ||||
|         public bool IsMbSupporter { get; set; } | ||||
|     } | ||||
| } | ||||
| @ -1,5 +1,3 @@ | ||||
| #nullable disable | ||||
| 
 | ||||
| namespace Jellyfin.Api.Models.StartupDtos | ||||
| { | ||||
|     /// <summary> | ||||
| @ -10,16 +8,16 @@ namespace Jellyfin.Api.Models.StartupDtos | ||||
|         /// <summary> | ||||
|         /// Gets or sets UI language culture. | ||||
|         /// </summary> | ||||
|         public string UICulture { get; set; } | ||||
|         public string? UICulture { get; set; } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets or sets the metadata country code. | ||||
|         /// </summary> | ||||
|         public string MetadataCountryCode { get; set; } | ||||
|         public string? MetadataCountryCode { get; set; } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets or sets the preferred language for the metadata. | ||||
|         /// </summary> | ||||
|         public string PreferredMetadataLanguage { get; set; } | ||||
|         public string? PreferredMetadataLanguage { get; set; } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -1,5 +1,3 @@ | ||||
| #nullable disable | ||||
| 
 | ||||
| namespace Jellyfin.Api.Models.StartupDtos | ||||
| { | ||||
|     /// <summary> | ||||
| @ -10,11 +8,11 @@ namespace Jellyfin.Api.Models.StartupDtos | ||||
|         /// <summary> | ||||
|         /// Gets or sets the username. | ||||
|         /// </summary> | ||||
|         public string Name { get; set; } | ||||
|         public string? Name { get; set; } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets or sets the user's password. | ||||
|         /// </summary> | ||||
|         public string Password { get; set; } | ||||
|         public string? Password { get; set; } | ||||
|     } | ||||
| } | ||||
|  | ||||
							
								
								
									
										23
									
								
								Jellyfin.Api/Models/UserDtos/AuthenticateUserByName.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								Jellyfin.Api/Models/UserDtos/AuthenticateUserByName.cs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,23 @@ | ||||
| namespace Jellyfin.Api.Models.UserDtos | ||||
| { | ||||
|     /// <summary> | ||||
|     /// The authenticate user by name request body. | ||||
|     /// </summary> | ||||
|     public class AuthenticateUserByName | ||||
|     { | ||||
|         /// <summary> | ||||
|         /// Gets or sets the username. | ||||
|         /// </summary> | ||||
|         public string? Username { get; set; } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets or sets the plain text password. | ||||
|         /// </summary> | ||||
|         public string? Pw { get; set; } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets or sets the sha1-hashed password. | ||||
|         /// </summary> | ||||
|         public string? Password { get; set; } | ||||
|     } | ||||
| } | ||||
Some files were not shown because too many files have changed in this diff Show More
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user