mirror of
				https://github.com/jellyfin/jellyfin.git
				synced 2025-10-31 10:37:22 -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) |  - [anthonylavado](https://github.com/anthonylavado) | ||||||
|  - [Artiume](https://github.com/Artiume) |  - [Artiume](https://github.com/Artiume) | ||||||
|  - [AThomsen](https://github.com/AThomsen) |  - [AThomsen](https://github.com/AThomsen) | ||||||
|  |  - [barronpm](https://github.com/barronpm) | ||||||
|  - [bilde2910](https://github.com/bilde2910) |  - [bilde2910](https://github.com/bilde2910) | ||||||
|  - [bfayers](https://github.com/bfayers) |  - [bfayers](https://github.com/bfayers) | ||||||
|  - [BnMcG](https://github.com/BnMcG) |  - [BnMcG](https://github.com/BnMcG) | ||||||
| @ -130,6 +131,7 @@ | |||||||
|  - [XVicarious](https://github.com/XVicarious) |  - [XVicarious](https://github.com/XVicarious) | ||||||
|  - [YouKnowBlom](https://github.com/YouKnowBlom) |  - [YouKnowBlom](https://github.com/YouKnowBlom) | ||||||
|  - [KristupasSavickas](https://github.com/KristupasSavickas) |  - [KristupasSavickas](https://github.com/KristupasSavickas) | ||||||
|  |  - [Pusta](https://github.com/pusta) | ||||||
| 
 | 
 | ||||||
| # Emby Contributors | # Emby Contributors | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -4,11 +4,12 @@ using System; | |||||||
| using System.Linq; | using System.Linq; | ||||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||||
| using Emby.Dlna.Service; | using Emby.Dlna.Service; | ||||||
|  | using Jellyfin.Data.Entities; | ||||||
|  | using Jellyfin.Data.Enums; | ||||||
| using MediaBrowser.Common.Net; | using MediaBrowser.Common.Net; | ||||||
| using MediaBrowser.Controller.Configuration; | using MediaBrowser.Controller.Configuration; | ||||||
| using MediaBrowser.Controller.Dlna; | using MediaBrowser.Controller.Dlna; | ||||||
| using MediaBrowser.Controller.Drawing; | using MediaBrowser.Controller.Drawing; | ||||||
| using MediaBrowser.Controller.Entities; |  | ||||||
| using MediaBrowser.Controller.Library; | using MediaBrowser.Controller.Library; | ||||||
| using MediaBrowser.Controller.MediaEncoding; | using MediaBrowser.Controller.MediaEncoding; | ||||||
| using MediaBrowser.Controller.TV; | using MediaBrowser.Controller.TV; | ||||||
| @ -32,7 +33,8 @@ namespace Emby.Dlna.ContentDirectory | |||||||
|         private readonly IMediaEncoder _mediaEncoder; |         private readonly IMediaEncoder _mediaEncoder; | ||||||
|         private readonly ITVSeriesManager _tvSeriesManager; |         private readonly ITVSeriesManager _tvSeriesManager; | ||||||
| 
 | 
 | ||||||
|         public ContentDirectory(IDlnaManager dlna, |         public ContentDirectory( | ||||||
|  |             IDlnaManager dlna, | ||||||
|             IUserDataManager userDataManager, |             IUserDataManager userDataManager, | ||||||
|             IImageProcessor imageProcessor, |             IImageProcessor imageProcessor, | ||||||
|             ILibraryManager libraryManager, |             ILibraryManager libraryManager, | ||||||
| @ -131,7 +133,7 @@ namespace Emby.Dlna.ContentDirectory | |||||||
| 
 | 
 | ||||||
|             foreach (var user in _userManager.Users) |             foreach (var user in _userManager.Users) | ||||||
|             { |             { | ||||||
|                 if (user.Policy.IsAdministrator) |                 if (user.HasPermission(PermissionKind.IsAdministrator)) | ||||||
|                 { |                 { | ||||||
|                     return user; |                     return user; | ||||||
|                 } |                 } | ||||||
|  | |||||||
| @ -10,6 +10,7 @@ using System.Threading; | |||||||
| using System.Xml; | using System.Xml; | ||||||
| using Emby.Dlna.Didl; | using Emby.Dlna.Didl; | ||||||
| using Emby.Dlna.Service; | using Emby.Dlna.Service; | ||||||
|  | using Jellyfin.Data.Entities; | ||||||
| using MediaBrowser.Common.Extensions; | using MediaBrowser.Common.Extensions; | ||||||
| using MediaBrowser.Controller.Configuration; | using MediaBrowser.Controller.Configuration; | ||||||
| using MediaBrowser.Controller.Drawing; | using MediaBrowser.Controller.Drawing; | ||||||
| @ -17,7 +18,6 @@ using MediaBrowser.Controller.Dto; | |||||||
| using MediaBrowser.Controller.Entities; | using MediaBrowser.Controller.Entities; | ||||||
| using MediaBrowser.Controller.Entities.Audio; | using MediaBrowser.Controller.Entities.Audio; | ||||||
| using MediaBrowser.Controller.Entities.Movies; | using MediaBrowser.Controller.Entities.Movies; | ||||||
| using MediaBrowser.Controller.Entities.TV; |  | ||||||
| using MediaBrowser.Controller.Library; | using MediaBrowser.Controller.Library; | ||||||
| using MediaBrowser.Controller.LiveTv; | using MediaBrowser.Controller.LiveTv; | ||||||
| using MediaBrowser.Controller.MediaEncoding; | using MediaBrowser.Controller.MediaEncoding; | ||||||
| @ -28,6 +28,12 @@ using MediaBrowser.Model.Entities; | |||||||
| using MediaBrowser.Model.Globalization; | using MediaBrowser.Model.Globalization; | ||||||
| using MediaBrowser.Model.Querying; | using MediaBrowser.Model.Querying; | ||||||
| using Microsoft.Extensions.Logging; | 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 | namespace Emby.Dlna.ContentDirectory | ||||||
| { | { | ||||||
| @ -731,7 +737,7 @@ namespace Emby.Dlna.ContentDirectory | |||||||
|                 return GetGenres(item, user, query); |                 return GetGenres(item, user, query); | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             var array = new ServerItem[] |             var array = new[] | ||||||
|             { |             { | ||||||
|                 new ServerItem(item) |                 new ServerItem(item) | ||||||
|                 { |                 { | ||||||
| @ -1115,7 +1121,7 @@ namespace Emby.Dlna.ContentDirectory | |||||||
|         private QueryResult<ServerItem> GetMusicPlaylists(User user, InternalItemsQuery query) |         private QueryResult<ServerItem> GetMusicPlaylists(User user, InternalItemsQuery query) | ||||||
|         { |         { | ||||||
|             query.Parent = null; |             query.Parent = null; | ||||||
|             query.IncludeItemTypes = new[] { typeof(Playlist).Name }; |             query.IncludeItemTypes = new[] { nameof(Playlist) }; | ||||||
|             query.SetUser(user); |             query.SetUser(user); | ||||||
|             query.Recursive = true; |             query.Recursive = true; | ||||||
| 
 | 
 | ||||||
| @ -1132,10 +1138,9 @@ namespace Emby.Dlna.ContentDirectory | |||||||
|             { |             { | ||||||
|                 UserId = user.Id, |                 UserId = user.Id, | ||||||
|                 Limit = 50, |                 Limit = 50, | ||||||
|                 IncludeItemTypes = new[] { typeof(Audio).Name }, |                 IncludeItemTypes = new[] { nameof(Audio) }, | ||||||
|                 ParentId = parent == null ? Guid.Empty : parent.Id, |                 ParentId = parent?.Id ?? Guid.Empty, | ||||||
|                 GroupItems = true |                 GroupItems = true | ||||||
| 
 |  | ||||||
|             }, query.DtoOptions).Select(i => i.Item1 ?? i.Item2.FirstOrDefault()).Where(i => i != null).ToArray(); |             }, query.DtoOptions).Select(i => i.Item1 ?? i.Item2.FirstOrDefault()).Where(i => i != null).ToArray(); | ||||||
| 
 | 
 | ||||||
|             return ToResult(items); |             return ToResult(items); | ||||||
| @ -1150,7 +1155,6 @@ namespace Emby.Dlna.ContentDirectory | |||||||
|                 Limit = query.Limit, |                 Limit = query.Limit, | ||||||
|                 StartIndex = query.StartIndex, |                 StartIndex = query.StartIndex, | ||||||
|                 UserId = query.User.Id |                 UserId = query.User.Id | ||||||
| 
 |  | ||||||
|             }, new[] { parent }, query.DtoOptions); |             }, new[] { parent }, query.DtoOptions); | ||||||
| 
 | 
 | ||||||
|             return ToResult(result); |             return ToResult(result); | ||||||
| @ -1167,7 +1171,6 @@ namespace Emby.Dlna.ContentDirectory | |||||||
|                 IncludeItemTypes = new[] { typeof(Episode).Name }, |                 IncludeItemTypes = new[] { typeof(Episode).Name }, | ||||||
|                 ParentId = parent == null ? Guid.Empty : parent.Id, |                 ParentId = parent == null ? Guid.Empty : parent.Id, | ||||||
|                 GroupItems = false |                 GroupItems = false | ||||||
| 
 |  | ||||||
|             }, query.DtoOptions).Select(i => i.Item1 ?? i.Item2.FirstOrDefault()).Where(i => i != null).ToArray(); |             }, query.DtoOptions).Select(i => i.Item1 ?? i.Item2.FirstOrDefault()).Where(i => i != null).ToArray(); | ||||||
| 
 | 
 | ||||||
|             return ToResult(items); |             return ToResult(items); | ||||||
| @ -1177,14 +1180,14 @@ namespace Emby.Dlna.ContentDirectory | |||||||
|         { |         { | ||||||
|             query.OrderBy = Array.Empty<(string, SortOrder)>(); |             query.OrderBy = Array.Empty<(string, SortOrder)>(); | ||||||
| 
 | 
 | ||||||
|             var items = _userViewManager.GetLatestItems(new LatestItemsQuery |             var items = _userViewManager.GetLatestItems( | ||||||
|  |                 new LatestItemsQuery | ||||||
|             { |             { | ||||||
|                 UserId = user.Id, |                 UserId = user.Id, | ||||||
|                 Limit = 50, |                 Limit = 50, | ||||||
|                 IncludeItemTypes = new[] { typeof(Movie).Name }, |                 IncludeItemTypes = new[] { nameof(Movie) }, | ||||||
|                 ParentId = parent == null ? Guid.Empty : parent.Id, |                 ParentId = parent?.Id ?? Guid.Empty, | ||||||
|                 GroupItems = true |                 GroupItems = true | ||||||
| 
 |  | ||||||
|             }, query.DtoOptions).Select(i => i.Item1 ?? i.Item2.FirstOrDefault()).Where(i => i != null).ToArray(); |             }, query.DtoOptions).Select(i => i.Item1 ?? i.Item2.FirstOrDefault()).Where(i => i != null).ToArray(); | ||||||
| 
 | 
 | ||||||
|             return ToResult(items); |             return ToResult(items); | ||||||
| @ -1217,7 +1220,11 @@ namespace Emby.Dlna.ContentDirectory | |||||||
|                 Recursive = true, |                 Recursive = true, | ||||||
|                 ParentId = parentId, |                 ParentId = parentId, | ||||||
|                 GenreIds = new[] { item.Id }, |                 GenreIds = new[] { item.Id }, | ||||||
|                 IncludeItemTypes = new[] { typeof(Movie).Name, typeof(Series).Name }, |                 IncludeItemTypes = new[] | ||||||
|  |                 { | ||||||
|  |                     nameof(Movie), | ||||||
|  |                     nameof(Series) | ||||||
|  |                 }, | ||||||
|                 Limit = limit, |                 Limit = limit, | ||||||
|                 StartIndex = startIndex, |                 StartIndex = startIndex, | ||||||
|                 DtoOptions = GetDtoOptions() |                 DtoOptions = GetDtoOptions() | ||||||
|  | |||||||
| @ -6,14 +6,13 @@ using System.IO; | |||||||
| using System.Linq; | using System.Linq; | ||||||
| using System.Text; | using System.Text; | ||||||
| using System.Xml; | using System.Xml; | ||||||
| using Emby.Dlna.Configuration; |  | ||||||
| using Emby.Dlna.ContentDirectory; | using Emby.Dlna.ContentDirectory; | ||||||
|  | using Jellyfin.Data.Entities; | ||||||
| using MediaBrowser.Controller.Channels; | using MediaBrowser.Controller.Channels; | ||||||
| using MediaBrowser.Controller.Drawing; | using MediaBrowser.Controller.Drawing; | ||||||
| using MediaBrowser.Controller.Entities; | using MediaBrowser.Controller.Entities; | ||||||
| using MediaBrowser.Controller.Entities.Audio; | using MediaBrowser.Controller.Entities.Audio; | ||||||
| using MediaBrowser.Controller.Entities.Movies; | using MediaBrowser.Controller.Entities.Movies; | ||||||
| using MediaBrowser.Controller.Entities.TV; |  | ||||||
| using MediaBrowser.Controller.Library; | using MediaBrowser.Controller.Library; | ||||||
| using MediaBrowser.Controller.MediaEncoding; | using MediaBrowser.Controller.MediaEncoding; | ||||||
| using MediaBrowser.Controller.Playlists; | using MediaBrowser.Controller.Playlists; | ||||||
| @ -23,6 +22,13 @@ using MediaBrowser.Model.Entities; | |||||||
| using MediaBrowser.Model.Globalization; | using MediaBrowser.Model.Globalization; | ||||||
| using MediaBrowser.Model.Net; | using MediaBrowser.Model.Net; | ||||||
| using Microsoft.Extensions.Logging; | 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 | namespace Emby.Dlna.Didl | ||||||
| { | { | ||||||
| @ -421,7 +427,6 @@ namespace Emby.Dlna.Didl | |||||||
|                     case StubType.FavoriteSeries: return _localization.GetLocalizedString("HeaderFavoriteShows"); |                     case StubType.FavoriteSeries: return _localization.GetLocalizedString("HeaderFavoriteShows"); | ||||||
|                     case StubType.FavoriteEpisodes: return _localization.GetLocalizedString("HeaderFavoriteEpisodes"); |                     case StubType.FavoriteEpisodes: return _localization.GetLocalizedString("HeaderFavoriteEpisodes"); | ||||||
|                     case StubType.Series: return _localization.GetLocalizedString("Shows"); |                     case StubType.Series: return _localization.GetLocalizedString("Shows"); | ||||||
|                     default: break; |  | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
| @ -670,7 +675,7 @@ namespace Emby.Dlna.Didl | |||||||
|                 return; |                 return; | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             MediaBrowser.Model.Dlna.XmlAttribute secAttribute = null; |             XmlAttribute secAttribute = null; | ||||||
|             foreach (var attribute in _profile.XmlRootAttributes) |             foreach (var attribute in _profile.XmlRootAttributes) | ||||||
|             { |             { | ||||||
|                 if (string.Equals(attribute.Name, "xmlns:sec", StringComparison.OrdinalIgnoreCase)) |                 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"); |             AddImageResElement(item, writer, 160, 160, "jpg", "JPEG_TN"); | ||||||
| 
 |  | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         private void AddImageResElement( |         private void AddImageResElement( | ||||||
|  | |||||||
| @ -7,6 +7,7 @@ using System.Linq; | |||||||
| using System.Threading; | using System.Threading; | ||||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||||
| using Emby.Dlna.Didl; | using Emby.Dlna.Didl; | ||||||
|  | using Jellyfin.Data.Entities; | ||||||
| using MediaBrowser.Common.Configuration; | using MediaBrowser.Common.Configuration; | ||||||
| using MediaBrowser.Controller.Dlna; | using MediaBrowser.Controller.Dlna; | ||||||
| using MediaBrowser.Controller.Drawing; | using MediaBrowser.Controller.Drawing; | ||||||
| @ -22,6 +23,7 @@ using MediaBrowser.Model.Globalization; | |||||||
| using MediaBrowser.Model.Session; | using MediaBrowser.Model.Session; | ||||||
| using Microsoft.AspNetCore.WebUtilities; | using Microsoft.AspNetCore.WebUtilities; | ||||||
| using Microsoft.Extensions.Logging; | using Microsoft.Extensions.Logging; | ||||||
|  | using Photo = MediaBrowser.Controller.Entities.Photo; | ||||||
| 
 | 
 | ||||||
| namespace Emby.Dlna.PlayTo | 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; |             var deviceInfo = _device.Properties; | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -4,6 +4,7 @@ using System.Globalization; | |||||||
| using System.IO; | using System.IO; | ||||||
| using System.Linq; | using System.Linq; | ||||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||||
|  | using Jellyfin.Data.Entities; | ||||||
| using MediaBrowser.Common.Extensions; | using MediaBrowser.Common.Extensions; | ||||||
| using MediaBrowser.Controller; | using MediaBrowser.Controller; | ||||||
| using MediaBrowser.Controller.Drawing; | using MediaBrowser.Controller.Drawing; | ||||||
| @ -14,6 +15,7 @@ using MediaBrowser.Model.Entities; | |||||||
| using MediaBrowser.Model.IO; | using MediaBrowser.Model.IO; | ||||||
| using MediaBrowser.Model.Net; | using MediaBrowser.Model.Net; | ||||||
| using Microsoft.Extensions.Logging; | using Microsoft.Extensions.Logging; | ||||||
|  | using Photo = MediaBrowser.Controller.Entities.Photo; | ||||||
| 
 | 
 | ||||||
| namespace Emby.Drawing | 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) |         private async Task<(string path, DateTime dateModified)> GetSupportedImage(string originalImagePath, DateTime dateModified) | ||||||
|         { |         { | ||||||
|             var inputFormat = Path.GetExtension(originalImagePath) |             var inputFormat = Path.GetExtension(originalImagePath) | ||||||
|  | |||||||
| @ -4,6 +4,8 @@ using System.Globalization; | |||||||
| using System.Linq; | using System.Linq; | ||||||
| using System.Threading; | using System.Threading; | ||||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||||
|  | using Jellyfin.Data.Entities; | ||||||
|  | using Jellyfin.Data.Enums; | ||||||
| using MediaBrowser.Common.Configuration; | using MediaBrowser.Common.Configuration; | ||||||
| using MediaBrowser.Common.Extensions; | using MediaBrowser.Common.Extensions; | ||||||
| using MediaBrowser.Controller.Configuration; | using MediaBrowser.Controller.Configuration; | ||||||
| @ -101,7 +103,7 @@ namespace Emby.Notifications | |||||||
|                 switch (request.SendToUserMode.Value) |                 switch (request.SendToUserMode.Value) | ||||||
|                 { |                 { | ||||||
|                     case SendToUserType.Admins: |                     case SendToUserType.Admins: | ||||||
|                         return _userManager.Users.Where(i => i.Policy.IsAdministrator) |                         return _userManager.Users.Where(i => i.HasPermission(PermissionKind.IsAdministrator)) | ||||||
|                                 .Select(i => i.Id); |                                 .Select(i => i.Id); | ||||||
|                     case SendToUserType.All: |                     case SendToUserType.All: | ||||||
|                         return _userManager.UsersIds; |                         return _userManager.UsersIds; | ||||||
| @ -117,7 +119,7 @@ namespace Emby.Notifications | |||||||
|                 var config = GetConfiguration(); |                 var config = GetConfiguration(); | ||||||
| 
 | 
 | ||||||
|                 return _userManager.Users |                 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); |                     .Select(i => i.Id); | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
| @ -142,7 +144,7 @@ namespace Emby.Notifications | |||||||
|                 User = user |                 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 |             try | ||||||
|             { |             { | ||||||
|  | |||||||
| @ -88,25 +88,26 @@ namespace Emby.Server.Implementations.Activity | |||||||
| 
 | 
 | ||||||
|             _subManager.SubtitleDownloadFailure += OnSubtitleDownloadFailure; |             _subManager.SubtitleDownloadFailure += OnSubtitleDownloadFailure; | ||||||
| 
 | 
 | ||||||
|             _userManager.UserCreated += OnUserCreated; |             _userManager.OnUserCreated += OnUserCreated; | ||||||
|             _userManager.UserPasswordChanged += OnUserPasswordChanged; |             _userManager.OnUserPasswordChanged += OnUserPasswordChanged; | ||||||
|             _userManager.UserDeleted += OnUserDeleted; |             _userManager.OnUserDeleted += OnUserDeleted; | ||||||
|             _userManager.UserPolicyUpdated += OnUserPolicyUpdated; |             _userManager.OnUserLockedOut += OnUserLockedOut; | ||||||
|             _userManager.UserLockedOut += OnUserLockedOut; |  | ||||||
| 
 | 
 | ||||||
|             return Task.CompletedTask; |             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( |             await CreateLogEntry(new ActivityLog( | ||||||
|                     string.Format( |                     string.Format( | ||||||
|                         CultureInfo.InvariantCulture, |                         CultureInfo.InvariantCulture, | ||||||
|                         _localization.GetLocalizedString("UserLockedOutWithName"), |                         _localization.GetLocalizedString("UserLockedOutWithName"), | ||||||
|                         e.Argument.Name), |                         e.Argument.Username), | ||||||
|                     NotificationType.UserLockedOut.ToString(), |                     NotificationType.UserLockedOut.ToString(), | ||||||
|                     e.Argument.Id)) |                     e.Argument.Id) | ||||||
|                 .ConfigureAwait(false); |             { | ||||||
|  |                 LogSeverity = LogLevel.Error | ||||||
|  |             }).ConfigureAwait(false); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         private async void OnSubtitleDownloadFailure(object sender, SubtitleDownloadFailureEventArgs e) |         private async void OnSubtitleDownloadFailure(object sender, SubtitleDownloadFailureEventArgs e) | ||||||
| @ -152,7 +153,7 @@ namespace Emby.Server.Implementations.Activity | |||||||
|                 string.Format( |                 string.Format( | ||||||
|                     CultureInfo.InvariantCulture, |                     CultureInfo.InvariantCulture, | ||||||
|                     _localization.GetLocalizedString("UserStoppedPlayingItemWithValues"), |                     _localization.GetLocalizedString("UserStoppedPlayingItemWithValues"), | ||||||
|                     user.Name, |                     user.Username, | ||||||
|                     GetItemName(item), |                     GetItemName(item), | ||||||
|                     e.DeviceName), |                     e.DeviceName), | ||||||
|                 GetPlaybackStoppedNotificationType(item.MediaType), |                 GetPlaybackStoppedNotificationType(item.MediaType), | ||||||
| @ -187,7 +188,7 @@ namespace Emby.Server.Implementations.Activity | |||||||
|                 string.Format( |                 string.Format( | ||||||
|                     CultureInfo.InvariantCulture, |                     CultureInfo.InvariantCulture, | ||||||
|                     _localization.GetLocalizedString("UserStartedPlayingItemWithValues"), |                     _localization.GetLocalizedString("UserStartedPlayingItemWithValues"), | ||||||
|                     user.Name, |                     user.Username, | ||||||
|                     GetItemName(item), |                     GetItemName(item), | ||||||
|                     e.DeviceName), |                     e.DeviceName), | ||||||
|                 GetPlaybackNotificationType(item.MediaType), |                 GetPlaybackNotificationType(item.MediaType), | ||||||
| @ -304,49 +305,37 @@ namespace Emby.Server.Implementations.Activity | |||||||
|             }).ConfigureAwait(false); |             }).ConfigureAwait(false); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         private async void OnUserPolicyUpdated(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("UserPolicyUpdatedWithName"), |  | ||||||
|                     e.Argument.Name), |  | ||||||
|                 "UserPolicyUpdated", |  | ||||||
|                 e.Argument.Id)) |  | ||||||
|                 .ConfigureAwait(false); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         private async void OnUserDeleted(object sender, GenericEventArgs<MediaBrowser.Controller.Entities.User> e) |  | ||||||
|         { |         { | ||||||
|             await CreateLogEntry(new ActivityLog( |             await CreateLogEntry(new ActivityLog( | ||||||
|                 string.Format( |                 string.Format( | ||||||
|                     CultureInfo.InvariantCulture, |                     CultureInfo.InvariantCulture, | ||||||
|                     _localization.GetLocalizedString("UserDeletedWithName"), |                     _localization.GetLocalizedString("UserDeletedWithName"), | ||||||
|                     e.Argument.Name), |                     e.Argument.Username), | ||||||
|                 "UserDeleted", |                 "UserDeleted", | ||||||
|                 Guid.Empty)) |                 Guid.Empty)) | ||||||
|                 .ConfigureAwait(false); |                 .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( |             await CreateLogEntry(new ActivityLog( | ||||||
|                 string.Format( |                 string.Format( | ||||||
|                     CultureInfo.InvariantCulture, |                     CultureInfo.InvariantCulture, | ||||||
|                     _localization.GetLocalizedString("UserPasswordChangedWithName"), |                     _localization.GetLocalizedString("UserPasswordChangedWithName"), | ||||||
|                     e.Argument.Name), |                     e.Argument.Username), | ||||||
|                 "UserPasswordChanged", |                 "UserPasswordChanged", | ||||||
|                 e.Argument.Id)) |                 e.Argument.Id)) | ||||||
|                 .ConfigureAwait(false); |                 .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( |             await CreateLogEntry(new ActivityLog( | ||||||
|                 string.Format( |                 string.Format( | ||||||
|                     CultureInfo.InvariantCulture, |                     CultureInfo.InvariantCulture, | ||||||
|                     _localization.GetLocalizedString("UserCreatedWithName"), |                     _localization.GetLocalizedString("UserCreatedWithName"), | ||||||
|                     e.Argument.Name), |                     e.Argument.Username), | ||||||
|                 "UserCreated", |                 "UserCreated", | ||||||
|                 e.Argument.Id)) |                 e.Argument.Id)) | ||||||
|                 .ConfigureAwait(false); |                 .ConfigureAwait(false); | ||||||
| @ -510,11 +499,10 @@ namespace Emby.Server.Implementations.Activity | |||||||
| 
 | 
 | ||||||
|             _subManager.SubtitleDownloadFailure -= OnSubtitleDownloadFailure; |             _subManager.SubtitleDownloadFailure -= OnSubtitleDownloadFailure; | ||||||
| 
 | 
 | ||||||
|             _userManager.UserCreated -= OnUserCreated; |             _userManager.OnUserCreated -= OnUserCreated; | ||||||
|             _userManager.UserPasswordChanged -= OnUserPasswordChanged; |             _userManager.OnUserPasswordChanged -= OnUserPasswordChanged; | ||||||
|             _userManager.UserDeleted -= OnUserDeleted; |             _userManager.OnUserDeleted -= OnUserDeleted; | ||||||
|             _userManager.UserPolicyUpdated -= OnUserPolicyUpdated; |             _userManager.OnUserLockedOut -= OnUserLockedOut; | ||||||
|             _userManager.UserLockedOut -= OnUserLockedOut; |  | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         /// <summary> |         /// <summary> | ||||||
|  | |||||||
| @ -97,7 +97,6 @@ using MediaBrowser.Providers.Chapters; | |||||||
| using MediaBrowser.Providers.Manager; | using MediaBrowser.Providers.Manager; | ||||||
| using MediaBrowser.Providers.Plugins.TheTvdb; | using MediaBrowser.Providers.Plugins.TheTvdb; | ||||||
| using MediaBrowser.Providers.Subtitles; | using MediaBrowser.Providers.Subtitles; | ||||||
| using MediaBrowser.WebDashboard.Api; |  | ||||||
| using MediaBrowser.XbmcMetadata.Providers; | using MediaBrowser.XbmcMetadata.Providers; | ||||||
| using Microsoft.AspNetCore.Http; | using Microsoft.AspNetCore.Http; | ||||||
| using Microsoft.Extensions.DependencyInjection; | using Microsoft.Extensions.DependencyInjection; | ||||||
| @ -562,11 +561,8 @@ namespace Emby.Server.Implementations | |||||||
| 
 | 
 | ||||||
|             serviceCollection.AddSingleton<IAuthenticationRepository, AuthenticationRepository>(); |             serviceCollection.AddSingleton<IAuthenticationRepository, AuthenticationRepository>(); | ||||||
| 
 | 
 | ||||||
|             serviceCollection.AddSingleton<IUserRepository, SqliteUserRepository>(); |  | ||||||
| 
 |  | ||||||
|             // TODO: Refactor to eliminate the circular dependency here so that Lazy<T> isn't required |             // 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.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: 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 |             // TODO: Add StartupOptions.FFmpegPath to IConfiguration and remove this custom activation | ||||||
| @ -659,15 +655,11 @@ namespace Emby.Server.Implementations | |||||||
| 
 | 
 | ||||||
|             ((SqliteDisplayPreferencesRepository)Resolve<IDisplayPreferencesRepository>()).Initialize(); |             ((SqliteDisplayPreferencesRepository)Resolve<IDisplayPreferencesRepository>()).Initialize(); | ||||||
|             ((AuthenticationRepository)Resolve<IAuthenticationRepository>()).Initialize(); |             ((AuthenticationRepository)Resolve<IAuthenticationRepository>()).Initialize(); | ||||||
|             ((SqliteUserRepository)Resolve<IUserRepository>()).Initialize(); |  | ||||||
| 
 | 
 | ||||||
|             SetStaticProperties(); |             SetStaticProperties(); | ||||||
| 
 | 
 | ||||||
|             var userManager = (UserManager)Resolve<IUserManager>(); |  | ||||||
|             userManager.Initialize(); |  | ||||||
| 
 |  | ||||||
|             var userDataRepo = (SqliteUserDataRepository)Resolve<IUserDataRepository>(); |             var userDataRepo = (SqliteUserDataRepository)Resolve<IUserDataRepository>(); | ||||||
|             ((SqliteItemRepository)Resolve<IItemRepository>()).Initialize(userDataRepo, userManager); |             ((SqliteItemRepository)Resolve<IItemRepository>()).Initialize(userDataRepo, Resolve<IUserManager>()); | ||||||
| 
 | 
 | ||||||
|             FindParts(); |             FindParts(); | ||||||
|         } |         } | ||||||
| @ -750,7 +742,6 @@ namespace Emby.Server.Implementations | |||||||
|             BaseItem.ProviderManager = Resolve<IProviderManager>(); |             BaseItem.ProviderManager = Resolve<IProviderManager>(); | ||||||
|             BaseItem.LocalizationManager = Resolve<ILocalizationManager>(); |             BaseItem.LocalizationManager = Resolve<ILocalizationManager>(); | ||||||
|             BaseItem.ItemRepository = Resolve<IItemRepository>(); |             BaseItem.ItemRepository = Resolve<IItemRepository>(); | ||||||
|             User.UserManager = Resolve<IUserManager>(); |  | ||||||
|             BaseItem.FileSystem = _fileSystemManager; |             BaseItem.FileSystem = _fileSystemManager; | ||||||
|             BaseItem.UserDataManager = Resolve<IUserDataManager>(); |             BaseItem.UserDataManager = Resolve<IUserDataManager>(); | ||||||
|             BaseItem.ChannelManager = Resolve<IChannelManager>(); |             BaseItem.ChannelManager = Resolve<IChannelManager>(); | ||||||
| @ -1045,9 +1036,6 @@ namespace Emby.Server.Implementations | |||||||
|             // Include composable parts in the Api assembly |             // Include composable parts in the Api assembly | ||||||
|             yield return typeof(ApiEntryPoint).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 |             // Include composable parts in the Model assembly | ||||||
|             yield return typeof(SystemInfo).Assembly; |             yield return typeof(SystemInfo).Assembly; | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -6,6 +6,7 @@ using System.IO; | |||||||
| using System.Linq; | using System.Linq; | ||||||
| using System.Threading; | using System.Threading; | ||||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||||
|  | using Jellyfin.Data.Entities; | ||||||
| using MediaBrowser.Common.Extensions; | using MediaBrowser.Common.Extensions; | ||||||
| using MediaBrowser.Common.Progress; | using MediaBrowser.Common.Progress; | ||||||
| using MediaBrowser.Controller.Channels; | using MediaBrowser.Controller.Channels; | ||||||
| @ -13,8 +14,6 @@ using MediaBrowser.Controller.Configuration; | |||||||
| using MediaBrowser.Controller.Dto; | using MediaBrowser.Controller.Dto; | ||||||
| using MediaBrowser.Controller.Entities; | using MediaBrowser.Controller.Entities; | ||||||
| using MediaBrowser.Controller.Entities.Audio; | using MediaBrowser.Controller.Entities.Audio; | ||||||
| using MediaBrowser.Controller.Entities.Movies; |  | ||||||
| using MediaBrowser.Controller.Entities.TV; |  | ||||||
| using MediaBrowser.Controller.Library; | using MediaBrowser.Controller.Library; | ||||||
| using MediaBrowser.Controller.Providers; | using MediaBrowser.Controller.Providers; | ||||||
| using MediaBrowser.Model.Channels; | using MediaBrowser.Model.Channels; | ||||||
| @ -24,6 +23,11 @@ using MediaBrowser.Model.IO; | |||||||
| using MediaBrowser.Model.Querying; | using MediaBrowser.Model.Querying; | ||||||
| using MediaBrowser.Model.Serialization; | using MediaBrowser.Model.Serialization; | ||||||
| using Microsoft.Extensions.Logging; | 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 | namespace Emby.Server.Implementations.Channels | ||||||
| { | { | ||||||
| @ -791,7 +795,8 @@ namespace Emby.Server.Implementations.Channels | |||||||
|             return result; |             return result; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         private async Task<ChannelItemResult> GetChannelItems(IChannel channel, |         private async Task<ChannelItemResult> GetChannelItems( | ||||||
|  |             IChannel channel, | ||||||
|             User user, |             User user, | ||||||
|             string externalFolderId, |             string externalFolderId, | ||||||
|             ChannelItemSortField? sortField, |             ChannelItemSortField? sortField, | ||||||
|  | |||||||
| @ -5,6 +5,7 @@ using System.IO; | |||||||
| using System.Linq; | using System.Linq; | ||||||
| using System.Threading; | using System.Threading; | ||||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||||
|  | using Jellyfin.Data.Entities; | ||||||
| using MediaBrowser.Common.Configuration; | using MediaBrowser.Common.Configuration; | ||||||
| using MediaBrowser.Controller.Collections; | using MediaBrowser.Controller.Collections; | ||||||
| using MediaBrowser.Controller.Configuration; | using MediaBrowser.Controller.Configuration; | ||||||
|  | |||||||
| @ -1,4 +1,4 @@ | |||||||
| #pragma warning disable CS1591 | #pragma warning disable CS1591 | ||||||
| 
 | 
 | ||||||
| using System; | using System; | ||||||
| using System.Collections.Generic; | using System.Collections.Generic; | ||||||
|  | |||||||
| @ -4,6 +4,7 @@ using System; | |||||||
| using System.Collections.Generic; | using System.Collections.Generic; | ||||||
| using System.IO; | using System.IO; | ||||||
| using System.Threading; | using System.Threading; | ||||||
|  | using Jellyfin.Data.Entities; | ||||||
| using MediaBrowser.Common.Configuration; | using MediaBrowser.Common.Configuration; | ||||||
| using MediaBrowser.Controller.Entities; | using MediaBrowser.Controller.Entities; | ||||||
| using MediaBrowser.Controller.Library; | 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.Globalization; | ||||||
| using System.IO; | using System.IO; | ||||||
| using System.Linq; | using System.Linq; | ||||||
|  | using Jellyfin.Data.Enums; | ||||||
|  | using Jellyfin.Data.Entities; | ||||||
| using MediaBrowser.Common.Extensions; | using MediaBrowser.Common.Extensions; | ||||||
| using MediaBrowser.Controller.Configuration; | using MediaBrowser.Controller.Configuration; | ||||||
| using MediaBrowser.Controller.Devices; | using MediaBrowser.Controller.Devices; | ||||||
| using MediaBrowser.Controller.Entities; |  | ||||||
| using MediaBrowser.Controller.Library; | using MediaBrowser.Controller.Library; | ||||||
| using MediaBrowser.Controller.Security; | using MediaBrowser.Controller.Security; | ||||||
| using MediaBrowser.Model.Devices; | using MediaBrowser.Model.Devices; | ||||||
| @ -16,7 +17,6 @@ using MediaBrowser.Model.Events; | |||||||
| using MediaBrowser.Model.Querying; | using MediaBrowser.Model.Querying; | ||||||
| using MediaBrowser.Model.Serialization; | using MediaBrowser.Model.Serialization; | ||||||
| using MediaBrowser.Model.Session; | using MediaBrowser.Model.Session; | ||||||
| using MediaBrowser.Model.Users; |  | ||||||
| 
 | 
 | ||||||
| namespace Emby.Server.Implementations.Devices | namespace Emby.Server.Implementations.Devices | ||||||
| { | { | ||||||
| @ -27,11 +27,10 @@ namespace Emby.Server.Implementations.Devices | |||||||
|         private readonly IServerConfigurationManager _config; |         private readonly IServerConfigurationManager _config; | ||||||
|         private readonly IAuthenticationRepository _authRepo; |         private readonly IAuthenticationRepository _authRepo; | ||||||
|         private readonly Dictionary<string, ClientCapabilities> _capabilitiesCache; |         private readonly Dictionary<string, ClientCapabilities> _capabilitiesCache; | ||||||
|  |         private readonly object _capabilitiesSyncLock = new object(); | ||||||
| 
 | 
 | ||||||
|         public event EventHandler<GenericEventArgs<Tuple<string, DeviceOptions>>> DeviceOptionsUpdated; |         public event EventHandler<GenericEventArgs<Tuple<string, DeviceOptions>>> DeviceOptionsUpdated; | ||||||
| 
 | 
 | ||||||
|         private readonly object _capabilitiesSyncLock = new object(); |  | ||||||
| 
 |  | ||||||
|         public DeviceManager( |         public DeviceManager( | ||||||
|             IAuthenticationRepository authRepo, |             IAuthenticationRepository authRepo, | ||||||
|             IJsonSerializer json, |             IJsonSerializer json, | ||||||
| @ -175,7 +174,12 @@ namespace Emby.Server.Implementations.Devices | |||||||
|                 throw new ArgumentNullException(nameof(deviceId)); |                 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); |                 var capabilities = GetCapabilities(deviceId); | ||||||
| 
 | 
 | ||||||
| @ -187,20 +191,5 @@ namespace Emby.Server.Implementations.Devices | |||||||
| 
 | 
 | ||||||
|             return true; |             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.IO; | ||||||
| using System.Linq; | using System.Linq; | ||||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||||
|  | using Jellyfin.Data.Entities; | ||||||
|  | using Jellyfin.Data.Enums; | ||||||
| using MediaBrowser.Common; | using MediaBrowser.Common; | ||||||
| using MediaBrowser.Controller.Channels; | using MediaBrowser.Controller.Channels; | ||||||
| using MediaBrowser.Controller.Drawing; | using MediaBrowser.Controller.Drawing; | ||||||
| using MediaBrowser.Controller.Dto; | using MediaBrowser.Controller.Dto; | ||||||
| using MediaBrowser.Controller.Entities; | using MediaBrowser.Controller.Entities; | ||||||
| using MediaBrowser.Controller.Entities.Audio; | using MediaBrowser.Controller.Entities.Audio; | ||||||
| using MediaBrowser.Controller.Entities.Movies; |  | ||||||
| using MediaBrowser.Controller.Entities.TV; |  | ||||||
| using MediaBrowser.Controller.Library; | using MediaBrowser.Controller.Library; | ||||||
| using MediaBrowser.Controller.LiveTv; | using MediaBrowser.Controller.LiveTv; | ||||||
| using MediaBrowser.Controller.Persistence; | using MediaBrowser.Controller.Persistence; | ||||||
| @ -24,6 +24,14 @@ using MediaBrowser.Model.Dto; | |||||||
| using MediaBrowser.Model.Entities; | using MediaBrowser.Model.Entities; | ||||||
| using MediaBrowser.Model.Querying; | using MediaBrowser.Model.Querying; | ||||||
| using Microsoft.Extensions.Logging; | 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 | namespace Emby.Server.Implementations.Dto | ||||||
| { | { | ||||||
| @ -384,7 +392,7 @@ namespace Emby.Server.Implementations.Dto | |||||||
| 
 | 
 | ||||||
|                     if (options.ContainsField(ItemFields.ChildCount)) |                     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)) |             if (options.ContainsField(ItemFields.BasicSyncInfo)) | ||||||
|             { |             { | ||||||
|                 var userCanSync = user != null && user.Policy.EnableContentDownloading; |                 var userCanSync = user != null && user.HasPermission(PermissionKind.EnableContentDownloading); | ||||||
|                 if (userCanSync && item.SupportsExternalTransfer) |                 if (userCanSync && item.SupportsExternalTransfer) | ||||||
|                 { |                 { | ||||||
|                     dto.SupportsSync = true; |                     dto.SupportsSync = true; | ||||||
|  | |||||||
| @ -13,7 +13,6 @@ | |||||||
|     <ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj" /> |     <ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj" /> | ||||||
|     <ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" /> |     <ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" /> | ||||||
|     <ProjectReference Include="..\MediaBrowser.Providers\MediaBrowser.Providers.csproj" /> |     <ProjectReference Include="..\MediaBrowser.Providers\MediaBrowser.Providers.csproj" /> | ||||||
|     <ProjectReference Include="..\MediaBrowser.WebDashboard\MediaBrowser.WebDashboard.csproj" /> |  | ||||||
|     <ProjectReference Include="..\MediaBrowser.XbmcMetadata\MediaBrowser.XbmcMetadata.csproj" /> |     <ProjectReference Include="..\MediaBrowser.XbmcMetadata\MediaBrowser.XbmcMetadata.csproj" /> | ||||||
|     <ProjectReference Include="..\Emby.Dlna\Emby.Dlna.csproj" /> |     <ProjectReference Include="..\Emby.Dlna\Emby.Dlna.csproj" /> | ||||||
|     <ProjectReference Include="..\MediaBrowser.Api\MediaBrowser.Api.csproj" /> |     <ProjectReference Include="..\MediaBrowser.Api\MediaBrowser.Api.csproj" /> | ||||||
|  | |||||||
| @ -6,6 +6,7 @@ using System.Globalization; | |||||||
| using System.Linq; | using System.Linq; | ||||||
| using System.Threading; | using System.Threading; | ||||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||||
|  | using Jellyfin.Data.Entities; | ||||||
| using MediaBrowser.Controller.Channels; | using MediaBrowser.Controller.Channels; | ||||||
| using MediaBrowser.Controller.Entities; | using MediaBrowser.Controller.Entities; | ||||||
| using MediaBrowser.Controller.Entities.Audio; | using MediaBrowser.Controller.Entities.Audio; | ||||||
|  | |||||||
| @ -4,6 +4,7 @@ using System; | |||||||
| using System.Linq; | using System.Linq; | ||||||
| using System.Threading; | using System.Threading; | ||||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||||
|  | using Jellyfin.Data.Enums; | ||||||
| using MediaBrowser.Controller.Library; | using MediaBrowser.Controller.Library; | ||||||
| using MediaBrowser.Controller.LiveTv; | using MediaBrowser.Controller.LiveTv; | ||||||
| using MediaBrowser.Controller.Plugins; | using MediaBrowser.Controller.Plugins; | ||||||
| @ -64,7 +65,7 @@ namespace Emby.Server.Implementations.EntryPoints | |||||||
| 
 | 
 | ||||||
|         private async Task SendMessage(string name, TimerEventInfo info) |         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 |             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.Globalization; | ||||||
| using System.Threading; | using System.Threading; | ||||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||||
|  | using Jellyfin.Data.Entities; | ||||||
| using MediaBrowser.Common.Plugins; | using MediaBrowser.Common.Plugins; | ||||||
| using MediaBrowser.Common.Updates; | using MediaBrowser.Common.Updates; | ||||||
| using MediaBrowser.Controller; | using MediaBrowser.Controller; | ||||||
| using MediaBrowser.Controller.Entities; |  | ||||||
| using MediaBrowser.Controller.Library; | using MediaBrowser.Controller.Library; | ||||||
| using MediaBrowser.Controller.Plugins; | using MediaBrowser.Controller.Plugins; | ||||||
| using MediaBrowser.Controller.Session; | using MediaBrowser.Controller.Session; | ||||||
| @ -68,10 +68,8 @@ namespace Emby.Server.Implementations.EntryPoints | |||||||
|         /// <inheritdoc /> |         /// <inheritdoc /> | ||||||
|         public Task RunAsync() |         public Task RunAsync() | ||||||
|         { |         { | ||||||
|             _userManager.UserDeleted += OnUserDeleted; |             _userManager.OnUserDeleted += OnUserDeleted; | ||||||
|             _userManager.UserUpdated += OnUserUpdated; |             _userManager.OnUserUpdated += OnUserUpdated; | ||||||
|             _userManager.UserPolicyUpdated += OnUserPolicyUpdated; |  | ||||||
|             _userManager.UserConfigurationUpdated += OnUserConfigurationUpdated; |  | ||||||
| 
 | 
 | ||||||
|             _appHost.HasPendingRestartChanged += OnHasPendingRestartChanged; |             _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); |             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) |         private async Task SendMessageToAdminSessions<T>(string name, T data) | ||||||
|         { |         { | ||||||
|             try |             try | ||||||
| @ -210,10 +194,8 @@ namespace Emby.Server.Implementations.EntryPoints | |||||||
|         { |         { | ||||||
|             if (dispose) |             if (dispose) | ||||||
|             { |             { | ||||||
|                 _userManager.UserDeleted -= OnUserDeleted; |                 _userManager.OnUserDeleted -= OnUserDeleted; | ||||||
|                 _userManager.UserUpdated -= OnUserUpdated; |                 _userManager.OnUserUpdated -= OnUserUpdated; | ||||||
|                 _userManager.UserPolicyUpdated -= OnUserPolicyUpdated; |  | ||||||
|                 _userManager.UserConfigurationUpdated -= OnUserConfigurationUpdated; |  | ||||||
| 
 | 
 | ||||||
|                 _installationManager.PluginUninstalled -= OnPluginUninstalled; |                 _installationManager.PluginUninstalled -= OnPluginUninstalled; | ||||||
|                 _installationManager.PackageInstalling -= OnPackageInstalling; |                 _installationManager.PackageInstalling -= OnPackageInstalling; | ||||||
|  | |||||||
| @ -1,5 +1,4 @@ | |||||||
| using System; | using System; | ||||||
| using System.Collections.Generic; |  | ||||||
| using System.Globalization; | using System.Globalization; | ||||||
| using System.Text; | using System.Text; | ||||||
| using MediaBrowser.Controller.Net; | using MediaBrowser.Controller.Net; | ||||||
|  | |||||||
| @ -3,10 +3,11 @@ | |||||||
| using System; | using System; | ||||||
| using System.Linq; | using System.Linq; | ||||||
| using Emby.Server.Implementations.SocketSharp; | using Emby.Server.Implementations.SocketSharp; | ||||||
|  | using Jellyfin.Data.Entities; | ||||||
|  | using Jellyfin.Data.Enums; | ||||||
| using MediaBrowser.Common.Net; | using MediaBrowser.Common.Net; | ||||||
| using MediaBrowser.Controller.Authentication; | using MediaBrowser.Controller.Authentication; | ||||||
| using MediaBrowser.Controller.Configuration; | using MediaBrowser.Controller.Configuration; | ||||||
| using MediaBrowser.Controller.Entities; |  | ||||||
| using MediaBrowser.Controller.Net; | using MediaBrowser.Controller.Net; | ||||||
| using MediaBrowser.Controller.Security; | using MediaBrowser.Controller.Security; | ||||||
| using MediaBrowser.Controller.Session; | using MediaBrowser.Controller.Session; | ||||||
| @ -38,9 +39,9 @@ namespace Emby.Server.Implementations.HttpServer.Security | |||||||
|             _networkManager = networkManager; |             _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) |         public User Authenticate(HttpRequest request, IAuthenticationAttributes authAttributes) | ||||||
| @ -50,17 +51,33 @@ namespace Emby.Server.Implementations.HttpServer.Security | |||||||
|             return user; |             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 |             // This code is executed before the service | ||||||
|             var auth = _authorizationContext.GetAuthorizationInfo(request); |             var auth = _authorizationContext.GetAuthorizationInfo(request); | ||||||
| 
 | 
 | ||||||
|             if (!IsExemptFromAuthenticationToken(authAttribtues, request)) |             if (!IsExemptFromAuthenticationToken(authAttributes, request)) | ||||||
|             { |             { | ||||||
|                 ValidateSecurityToken(request, auth.Token); |                 ValidateSecurityToken(request, auth.Token); | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             if (authAttribtues.AllowLocalOnly && !request.IsLocal) |             if (authAttributes.AllowLocalOnly && !request.IsLocal) | ||||||
|             { |             { | ||||||
|                 throw new SecurityException("Operation not found."); |                 throw new SecurityException("Operation not found."); | ||||||
|             } |             } | ||||||
| @ -74,14 +91,14 @@ namespace Emby.Server.Implementations.HttpServer.Security | |||||||
| 
 | 
 | ||||||
|             if (user != null) |             if (user != null) | ||||||
|             { |             { | ||||||
|                 ValidateUserAccess(user, request, authAttribtues, auth); |                 ValidateUserAccess(user, request, authAttributes); | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             var info = GetTokenInfo(request); |             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); |                 ValidateRoles(roles, user); | ||||||
|             } |             } | ||||||
| @ -90,7 +107,8 @@ namespace Emby.Server.Implementations.HttpServer.Security | |||||||
|                 !string.IsNullOrEmpty(auth.Client) && |                 !string.IsNullOrEmpty(auth.Client) && | ||||||
|                 !string.IsNullOrEmpty(auth.Device)) |                 !string.IsNullOrEmpty(auth.Device)) | ||||||
|             { |             { | ||||||
|                 _sessionManager.LogSessionActivity(auth.Client, |                 _sessionManager.LogSessionActivity( | ||||||
|  |                     auth.Client, | ||||||
|                     auth.Version, |                     auth.Version, | ||||||
|                     auth.DeviceId, |                     auth.DeviceId, | ||||||
|                     auth.Device, |                     auth.Device, | ||||||
| @ -104,21 +122,20 @@ namespace Emby.Server.Implementations.HttpServer.Security | |||||||
|         private void ValidateUserAccess( |         private void ValidateUserAccess( | ||||||
|             User user, |             User user, | ||||||
|             IRequest request, |             IRequest request, | ||||||
|             IAuthenticationAttributes authAttribtues, |             IAuthenticationAttributes authAttributes) | ||||||
|             AuthorizationInfo auth) |  | ||||||
|         { |         { | ||||||
|             if (user.Policy.IsDisabled) |             if (user.HasPermission(PermissionKind.IsDisabled)) | ||||||
|             { |             { | ||||||
|                 throw new SecurityException("User account has been disabled."); |                 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."); |                 throw new SecurityException("User account has been disabled."); | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             if (!user.Policy.IsAdministrator |             if (!user.HasPermission(PermissionKind.IsAdministrator) | ||||||
|                 && !authAttribtues.EscapeParentalControl |                 && !authAttributes.EscapeParentalControl | ||||||
|                 && !user.IsParentalScheduleAllowed()) |                 && !user.IsParentalScheduleAllowed()) | ||||||
|             { |             { | ||||||
|                 request.Response.Headers.Add("X-Application-Error-Code", "ParentalControl"); |                 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 (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."); |                     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 (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."); |                     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 (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."); |                     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."); |                 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.Net; | ||||||
| using MediaBrowser.Controller.Security; | using MediaBrowser.Controller.Security; | ||||||
| using MediaBrowser.Model.Services; | using MediaBrowser.Model.Services; | ||||||
|  | using Microsoft.AspNetCore.Http; | ||||||
| using Microsoft.Net.Http.Headers; | using Microsoft.Net.Http.Headers; | ||||||
| 
 | 
 | ||||||
| namespace Emby.Server.Implementations.HttpServer.Security | namespace Emby.Server.Implementations.HttpServer.Security | ||||||
| @ -38,6 +39,14 @@ namespace Emby.Server.Implementations.HttpServer.Security | |||||||
|             return GetAuthorization(requestContext); |             return GetAuthorization(requestContext); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  |         public AuthorizationInfo GetAuthorizationInfo(HttpRequest requestContext) | ||||||
|  |         { | ||||||
|  |             var auth = GetAuthorizationDictionary(requestContext); | ||||||
|  |             var (authInfo, _) = | ||||||
|  |                 GetAuthorizationInfoFromDictionary(auth, requestContext.Headers, requestContext.Query); | ||||||
|  |             return authInfo; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|         /// <summary> |         /// <summary> | ||||||
|         /// Gets the authorization. |         /// Gets the authorization. | ||||||
|         /// </summary> |         /// </summary> | ||||||
| @ -46,7 +55,23 @@ namespace Emby.Server.Implementations.HttpServer.Security | |||||||
|         private AuthorizationInfo GetAuthorization(IRequest httpReq) |         private AuthorizationInfo GetAuthorization(IRequest httpReq) | ||||||
|         { |         { | ||||||
|             var auth = GetAuthorizationDictionary(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 deviceId = null; | ||||||
|             string device = null; |             string device = null; | ||||||
|             string client = null; |             string client = null; | ||||||
| @ -64,19 +89,26 @@ namespace Emby.Server.Implementations.HttpServer.Security | |||||||
| 
 | 
 | ||||||
|             if (string.IsNullOrEmpty(token)) |             if (string.IsNullOrEmpty(token)) | ||||||
|             { |             { | ||||||
|                 token = httpReq.Headers["X-Emby-Token"]; |                 token = headers["X-Emby-Token"]; | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             if (string.IsNullOrEmpty(token)) |             if (string.IsNullOrEmpty(token)) | ||||||
|             { |             { | ||||||
|                 token = httpReq.Headers["X-MediaBrowser-Token"]; |                 token = headers["X-MediaBrowser-Token"]; | ||||||
|             } |  | ||||||
|             if (string.IsNullOrEmpty(token)) |  | ||||||
|             { |  | ||||||
|                 token = httpReq.QueryString["api_key"]; |  | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             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, |                 Client = client, | ||||||
|                 Device = device, |                 Device = device, | ||||||
| @ -85,6 +117,7 @@ namespace Emby.Server.Implementations.HttpServer.Security | |||||||
|                 Token = token |                 Token = token | ||||||
|             }; |             }; | ||||||
| 
 | 
 | ||||||
|  |             AuthenticationInfo originalAuthenticationInfo = null; | ||||||
|             if (!string.IsNullOrWhiteSpace(token)) |             if (!string.IsNullOrWhiteSpace(token)) | ||||||
|             { |             { | ||||||
|                 var result = _authRepo.Get(new AuthenticationInfoQuery |                 var result = _authRepo.Get(new AuthenticationInfoQuery | ||||||
| @ -92,81 +125,77 @@ namespace Emby.Server.Implementations.HttpServer.Security | |||||||
|                     AccessToken = token |                     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; |                     var updateToken = false; | ||||||
| 
 | 
 | ||||||
|                     // TODO: Remove these checks for IsNullOrWhiteSpace |                     // 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 |                     // 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(authInfo.Device, originalAuthenticationInfo.DeviceName, StringComparison.OrdinalIgnoreCase)) | ||||||
|                     else if (!string.Equals(info.Device, tokenInfo.DeviceName, StringComparison.OrdinalIgnoreCase)) |  | ||||||
|                     { |                     { | ||||||
|                         if (allowTokenInfoUpdate) |                         if (allowTokenInfoUpdate) | ||||||
|                         { |                         { | ||||||
|                             updateToken = true; |                             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) |                         if (allowTokenInfoUpdate) | ||||||
|                         { |                         { | ||||||
|                             updateToken = true; |                             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; |                         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; |                             updateToken = true; | ||||||
|                         } |                         } | ||||||
|                     } |                     } | ||||||
| 
 | 
 | ||||||
|                     if (updateToken) |                     if (updateToken) | ||||||
|                     { |                     { | ||||||
|                         _authRepo.Update(tokenInfo); |                         _authRepo.Update(originalAuthenticationInfo); | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|                 httpReq.Items["OriginalAuthenticationInfo"] = tokenInfo; |  | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             httpReq.Items["AuthorizationInfo"] = info; |             return (authInfo, originalAuthenticationInfo); | ||||||
| 
 |  | ||||||
|             return info; |  | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         /// <summary> |         /// <summary> | ||||||
| @ -186,6 +215,23 @@ namespace Emby.Server.Implementations.HttpServer.Security | |||||||
|             return GetAuthorization(auth); |             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> |         /// <summary> | ||||||
|         /// Gets the authorization. |         /// Gets the authorization. | ||||||
|         /// </summary> |         /// </summary> | ||||||
| @ -236,12 +282,7 @@ namespace Emby.Server.Implementations.HttpServer.Security | |||||||
| 
 | 
 | ||||||
|         private static string NormalizeValue(string value) |         private static string NormalizeValue(string value) | ||||||
|         { |         { | ||||||
|             if (string.IsNullOrEmpty(value)) |             return string.IsNullOrEmpty(value) ? value : WebUtility.HtmlEncode(value); | ||||||
|             { |  | ||||||
|                 return value; |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             return WebUtility.HtmlEncode(value); |  | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,7 +1,7 @@ | |||||||
| #pragma warning disable CS1591 | #pragma warning disable CS1591 | ||||||
| 
 | 
 | ||||||
| using System; | using System; | ||||||
| using MediaBrowser.Controller.Entities; | using Jellyfin.Data.Entities; | ||||||
| using MediaBrowser.Controller.Library; | using MediaBrowser.Controller.Library; | ||||||
| using MediaBrowser.Controller.Net; | using MediaBrowser.Controller.Net; | ||||||
| using MediaBrowser.Controller.Security; | using MediaBrowser.Controller.Security; | ||||||
|  | |||||||
| @ -234,10 +234,12 @@ namespace Emby.Server.Implementations.HttpServer | |||||||
|         private Task SendKeepAliveResponse() |         private Task SendKeepAliveResponse() | ||||||
|         { |         { | ||||||
|             LastKeepAliveDate = DateTime.UtcNow; |             LastKeepAliveDate = DateTime.UtcNow; | ||||||
|             return SendAsync(new WebSocketMessage<string> |             return SendAsync( | ||||||
|             { |                 new WebSocketMessage<string> | ||||||
|                 MessageType = "KeepAlive" |                 { | ||||||
|             }, CancellationToken.None); |                     MessageId = Guid.NewGuid(), | ||||||
|  |                     MessageType = "KeepAlive" | ||||||
|  |                 }, CancellationToken.None); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         /// <inheritdoc /> |         /// <inheritdoc /> | ||||||
|  | |||||||
| @ -17,6 +17,8 @@ using Emby.Server.Implementations.Library.Resolvers; | |||||||
| using Emby.Server.Implementations.Library.Validators; | using Emby.Server.Implementations.Library.Validators; | ||||||
| using Emby.Server.Implementations.Playlists; | using Emby.Server.Implementations.Playlists; | ||||||
| using Emby.Server.Implementations.ScheduledTasks; | using Emby.Server.Implementations.ScheduledTasks; | ||||||
|  | using Jellyfin.Data.Entities; | ||||||
|  | using Jellyfin.Data.Enums; | ||||||
| using MediaBrowser.Common.Extensions; | using MediaBrowser.Common.Extensions; | ||||||
| using MediaBrowser.Common.Progress; | using MediaBrowser.Common.Progress; | ||||||
| using MediaBrowser.Controller; | using MediaBrowser.Controller; | ||||||
| @ -25,7 +27,6 @@ using MediaBrowser.Controller.Drawing; | |||||||
| using MediaBrowser.Controller.Dto; | using MediaBrowser.Controller.Dto; | ||||||
| using MediaBrowser.Controller.Entities; | using MediaBrowser.Controller.Entities; | ||||||
| using MediaBrowser.Controller.Entities.Audio; | using MediaBrowser.Controller.Entities.Audio; | ||||||
| using MediaBrowser.Controller.Entities.TV; |  | ||||||
| using MediaBrowser.Controller.IO; | using MediaBrowser.Controller.IO; | ||||||
| using MediaBrowser.Controller.Library; | using MediaBrowser.Controller.Library; | ||||||
| using MediaBrowser.Controller.LiveTv; | using MediaBrowser.Controller.LiveTv; | ||||||
| @ -46,6 +47,9 @@ using MediaBrowser.Model.Querying; | |||||||
| using MediaBrowser.Model.Tasks; | using MediaBrowser.Model.Tasks; | ||||||
| using MediaBrowser.Providers.MediaInfo; | using MediaBrowser.Providers.MediaInfo; | ||||||
| using Microsoft.Extensions.Logging; | 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 SortOrder = MediaBrowser.Model.Entities.SortOrder; | ||||||
| using VideoResolver = Emby.Naming.Video.VideoResolver; | using VideoResolver = Emby.Naming.Video.VideoResolver; | ||||||
| 
 | 
 | ||||||
| @ -1539,7 +1543,8 @@ namespace Emby.Server.Implementations.Library | |||||||
|                 } |                 } | ||||||
| 
 | 
 | ||||||
|                 // Handle grouping |                 // 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() |                     return GetUserRootFolder() | ||||||
|                         .GetChildren(user, true) |                         .GetChildren(user, true) | ||||||
|  | |||||||
| @ -7,6 +7,8 @@ using System.IO; | |||||||
| using System.Linq; | using System.Linq; | ||||||
| using System.Threading; | using System.Threading; | ||||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||||
|  | using Jellyfin.Data.Entities; | ||||||
|  | using Jellyfin.Data.Enums; | ||||||
| using MediaBrowser.Common.Configuration; | using MediaBrowser.Common.Configuration; | ||||||
| using MediaBrowser.Common.Extensions; | using MediaBrowser.Common.Extensions; | ||||||
| using MediaBrowser.Controller.Entities; | using MediaBrowser.Controller.Entities; | ||||||
| @ -14,7 +16,6 @@ using MediaBrowser.Controller.Library; | |||||||
| using MediaBrowser.Controller.MediaEncoding; | using MediaBrowser.Controller.MediaEncoding; | ||||||
| using MediaBrowser.Controller.Persistence; | using MediaBrowser.Controller.Persistence; | ||||||
| using MediaBrowser.Controller.Providers; | using MediaBrowser.Controller.Providers; | ||||||
| using MediaBrowser.Model.Configuration; |  | ||||||
| using MediaBrowser.Model.Dlna; | using MediaBrowser.Model.Dlna; | ||||||
| using MediaBrowser.Model.Dto; | using MediaBrowser.Model.Dto; | ||||||
| using MediaBrowser.Model.Entities; | using MediaBrowser.Model.Entities; | ||||||
| @ -190,10 +191,7 @@ namespace Emby.Server.Implementations.Library | |||||||
|                 { |                 { | ||||||
|                     if (string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase)) |                     if (string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase)) | ||||||
|                     { |                     { | ||||||
|                         if (!user.Policy.EnableAudioPlaybackTranscoding) |                         source.SupportsTranscoding = user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding); | ||||||
|                         { |  | ||||||
|                             source.SupportsTranscoding = false; |  | ||||||
|                         } |  | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
| @ -352,7 +350,9 @@ namespace Emby.Server.Implementations.Library | |||||||
| 
 | 
 | ||||||
|         private void SetDefaultSubtitleStreamIndex(MediaSourceInfo source, UserItemData userData, User user, bool allowRememberingSelection) |         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; |                 var index = userData.SubtitleStreamIndex.Value; | ||||||
|                 // Make sure the saved index is still valid |                 // 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 defaultAudioIndex = source.DefaultAudioStreamIndex; | ||||||
|             var audioLangage = defaultAudioIndex == null |             var audioLangage = defaultAudioIndex == null | ||||||
|                 ? null |                 ? null | ||||||
|                 : source.MediaStreams.Where(i => i.Type == MediaStreamType.Audio && i.Index == defaultAudioIndex).Select(i => i.Language).FirstOrDefault(); |                 : 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, |                 preferredSubs, | ||||||
|                 user.Configuration.SubtitleMode, |                 user.SubtitleMode, | ||||||
|                 audioLangage); |                 audioLangage); | ||||||
| 
 | 
 | ||||||
|             MediaStreamSelector.SetSubtitleStreamScores(source.MediaStreams, preferredSubs, |             MediaStreamSelector.SetSubtitleStreamScores(source.MediaStreams, preferredSubs, user.SubtitleMode, audioLangage); | ||||||
|                 user.Configuration.SubtitleMode, audioLangage); |  | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         private void SetDefaultAudioStreamIndex(MediaSourceInfo source, UserItemData userData, User user, bool allowRememberingSelection) |         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; |                 var index = userData.AudioStreamIndex.Value; | ||||||
|                 // Make sure the saved index is still valid |                 // 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>() |                 ? 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) |         public void SetDefaultAudioAndSubtitleStreamIndexes(BaseItem item, MediaSourceInfo source, User user) | ||||||
| @ -534,7 +535,7 @@ namespace Emby.Server.Implementations.Library | |||||||
|                 mediaSource.RunTimeTicks = null; |                 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) |             if (audioStream == null || audioStream.Index == -1) | ||||||
|             { |             { | ||||||
| @ -545,7 +546,7 @@ namespace Emby.Server.Implementations.Library | |||||||
|                 mediaSource.DefaultAudioStreamIndex = audioStream.Index; |                 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 != null) | ||||||
|             { |             { | ||||||
|                 if (!videoStream.BitRate.HasValue) |                 if (!videoStream.BitRate.HasValue) | ||||||
| @ -556,17 +557,14 @@ namespace Emby.Server.Implementations.Library | |||||||
|                     { |                     { | ||||||
|                         videoStream.BitRate = 30000000; |                         videoStream.BitRate = 30000000; | ||||||
|                     } |                     } | ||||||
| 
 |  | ||||||
|                     else if (width >= 1900) |                     else if (width >= 1900) | ||||||
|                     { |                     { | ||||||
|                         videoStream.BitRate = 20000000; |                         videoStream.BitRate = 20000000; | ||||||
|                     } |                     } | ||||||
| 
 |  | ||||||
|                     else if (width >= 1200) |                     else if (width >= 1200) | ||||||
|                     { |                     { | ||||||
|                         videoStream.BitRate = 8000000; |                         videoStream.BitRate = 8000000; | ||||||
|                     } |                     } | ||||||
| 
 |  | ||||||
|                     else if (width >= 700) |                     else if (width >= 700) | ||||||
|                     { |                     { | ||||||
|                         videoStream.BitRate = 2000000; |                         videoStream.BitRate = 2000000; | ||||||
| @ -670,13 +668,14 @@ namespace Emby.Server.Implementations.Library | |||||||
|                     mediaSource.AnalyzeDurationMs = 3000; |                     mediaSource.AnalyzeDurationMs = 3000; | ||||||
|                 } |                 } | ||||||
| 
 | 
 | ||||||
|                 mediaInfo = await _mediaEncoder.GetMediaInfo(new MediaInfoRequest |                 mediaInfo = await _mediaEncoder.GetMediaInfo( | ||||||
|  |                     new MediaInfoRequest | ||||||
|                 { |                 { | ||||||
|                     MediaSource = mediaSource, |                     MediaSource = mediaSource, | ||||||
|                     MediaType = isAudio ? DlnaProfileType.Audio : DlnaProfileType.Video, |                     MediaType = isAudio ? DlnaProfileType.Audio : DlnaProfileType.Video, | ||||||
|                     ExtractChapters = false |                     ExtractChapters = false | ||||||
| 
 |                 }, | ||||||
|                 }, cancellationToken).ConfigureAwait(false); |                     cancellationToken).ConfigureAwait(false); | ||||||
| 
 | 
 | ||||||
|                 if (cacheFilePath != null) |                 if (cacheFilePath != null) | ||||||
|                 { |                 { | ||||||
|  | |||||||
| @ -3,7 +3,7 @@ | |||||||
| using System; | using System; | ||||||
| using System.Collections.Generic; | using System.Collections.Generic; | ||||||
| using System.Linq; | using System.Linq; | ||||||
| using MediaBrowser.Model.Configuration; | using Jellyfin.Data.Enums; | ||||||
| using MediaBrowser.Model.Entities; | using MediaBrowser.Model.Entities; | ||||||
| 
 | 
 | ||||||
| namespace Emby.Server.Implementations.Library | namespace Emby.Server.Implementations.Library | ||||||
|  | |||||||
| @ -3,6 +3,7 @@ | |||||||
| using System; | using System; | ||||||
| using System.Collections.Generic; | using System.Collections.Generic; | ||||||
| using System.Linq; | using System.Linq; | ||||||
|  | using Jellyfin.Data.Entities; | ||||||
| using MediaBrowser.Controller.Dto; | using MediaBrowser.Controller.Dto; | ||||||
| using MediaBrowser.Controller.Entities; | using MediaBrowser.Controller.Entities; | ||||||
| using MediaBrowser.Controller.Entities.Audio; | using MediaBrowser.Controller.Entities.Audio; | ||||||
| @ -10,6 +11,7 @@ using MediaBrowser.Controller.Library; | |||||||
| using MediaBrowser.Controller.Playlists; | using MediaBrowser.Controller.Playlists; | ||||||
| using MediaBrowser.Model.Entities; | using MediaBrowser.Model.Entities; | ||||||
| using MediaBrowser.Model.Querying; | using MediaBrowser.Model.Querying; | ||||||
|  | using MusicAlbum = MediaBrowser.Controller.Entities.Audio.MusicAlbum; | ||||||
| 
 | 
 | ||||||
| namespace Emby.Server.Implementations.Library | namespace Emby.Server.Implementations.Library | ||||||
| { | { | ||||||
| @ -75,7 +77,6 @@ namespace Emby.Server.Implementations.Library | |||||||
|                 { |                 { | ||||||
|                     return Guid.Empty; |                     return Guid.Empty; | ||||||
|                 } |                 } | ||||||
| 
 |  | ||||||
|             }).Where(i => !i.Equals(Guid.Empty)).ToArray(); |             }).Where(i => !i.Equals(Guid.Empty)).ToArray(); | ||||||
| 
 | 
 | ||||||
|             return GetInstantMixFromGenreIds(genreIds, user, dtoOptions); |             return GetInstantMixFromGenreIds(genreIds, user, dtoOptions); | ||||||
| @ -105,32 +106,27 @@ namespace Emby.Server.Implementations.Library | |||||||
|                 return GetInstantMixFromGenreIds(new[] { item.Id }, user, dtoOptions); |                 return GetInstantMixFromGenreIds(new[] { item.Id }, user, dtoOptions); | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             var playlist = item as Playlist; |             if (item is Playlist playlist) | ||||||
|             if (playlist != null) |  | ||||||
|             { |             { | ||||||
|                 return GetInstantMixFromPlaylist(playlist, user, dtoOptions); |                 return GetInstantMixFromPlaylist(playlist, user, dtoOptions); | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             var album = item as MusicAlbum; |             if (item is MusicAlbum album) | ||||||
|             if (album != null) |  | ||||||
|             { |             { | ||||||
|                 return GetInstantMixFromAlbum(album, user, dtoOptions); |                 return GetInstantMixFromAlbum(album, user, dtoOptions); | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             var artist = item as MusicArtist; |             if (item is MusicArtist artist) | ||||||
|             if (artist != null) |  | ||||||
|             { |             { | ||||||
|                 return GetInstantMixFromArtist(artist, user, dtoOptions); |                 return GetInstantMixFromArtist(artist, user, dtoOptions); | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             var song = item as Audio; |             if (item is Audio song) | ||||||
|             if (song != null) |  | ||||||
|             { |             { | ||||||
|                 return GetInstantMixFromSong(song, user, dtoOptions); |                 return GetInstantMixFromSong(song, user, dtoOptions); | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             var folder = item as Folder; |             if (item is Folder folder) | ||||||
|             if (folder != null) |  | ||||||
|             { |             { | ||||||
|                 return GetInstantMixFromFolder(folder, user, dtoOptions); |                 return GetInstantMixFromFolder(folder, user, dtoOptions); | ||||||
|             } |             } | ||||||
|  | |||||||
| @ -3,6 +3,7 @@ | |||||||
| using System; | using System; | ||||||
| using System.Collections.Generic; | using System.Collections.Generic; | ||||||
| using System.Linq; | using System.Linq; | ||||||
|  | using Jellyfin.Data.Entities; | ||||||
| using MediaBrowser.Controller.Dto; | using MediaBrowser.Controller.Dto; | ||||||
| using MediaBrowser.Controller.Entities; | using MediaBrowser.Controller.Entities; | ||||||
| using MediaBrowser.Controller.Entities.Audio; | using MediaBrowser.Controller.Entities.Audio; | ||||||
| @ -12,6 +13,8 @@ using MediaBrowser.Model.Entities; | |||||||
| using MediaBrowser.Model.Querying; | using MediaBrowser.Model.Querying; | ||||||
| using MediaBrowser.Model.Search; | using MediaBrowser.Model.Search; | ||||||
| using Microsoft.Extensions.Logging; | using Microsoft.Extensions.Logging; | ||||||
|  | using Genre = MediaBrowser.Controller.Entities.Genre; | ||||||
|  | using Person = MediaBrowser.Controller.Entities.Person; | ||||||
| 
 | 
 | ||||||
| namespace Emby.Server.Implementations.Library | namespace Emby.Server.Implementations.Library | ||||||
| { | { | ||||||
|  | |||||||
| @ -5,6 +5,7 @@ using System.Collections.Concurrent; | |||||||
| using System.Collections.Generic; | using System.Collections.Generic; | ||||||
| using System.Globalization; | using System.Globalization; | ||||||
| using System.Threading; | using System.Threading; | ||||||
|  | using Jellyfin.Data.Entities; | ||||||
| using MediaBrowser.Controller.Configuration; | using MediaBrowser.Controller.Configuration; | ||||||
| using MediaBrowser.Controller.Dto; | using MediaBrowser.Controller.Dto; | ||||||
| using MediaBrowser.Controller.Entities; | using MediaBrowser.Controller.Entities; | ||||||
| @ -13,6 +14,7 @@ using MediaBrowser.Controller.Persistence; | |||||||
| using MediaBrowser.Model.Dto; | using MediaBrowser.Model.Dto; | ||||||
| using MediaBrowser.Model.Entities; | using MediaBrowser.Model.Entities; | ||||||
| using Microsoft.Extensions.Logging; | using Microsoft.Extensions.Logging; | ||||||
|  | using Book = MediaBrowser.Controller.Entities.Book; | ||||||
| 
 | 
 | ||||||
| namespace Emby.Server.Implementations.Library | 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.Globalization; | ||||||
| using System.Linq; | using System.Linq; | ||||||
| using System.Threading; | using System.Threading; | ||||||
|  | using Jellyfin.Data.Entities; | ||||||
|  | using Jellyfin.Data.Enums; | ||||||
| using MediaBrowser.Controller.Channels; | using MediaBrowser.Controller.Channels; | ||||||
| using MediaBrowser.Controller.Configuration; | using MediaBrowser.Controller.Configuration; | ||||||
| using MediaBrowser.Controller.Dto; | using MediaBrowser.Controller.Dto; | ||||||
| @ -17,6 +19,8 @@ using MediaBrowser.Model.Entities; | |||||||
| using MediaBrowser.Model.Globalization; | using MediaBrowser.Model.Globalization; | ||||||
| using MediaBrowser.Model.Library; | using MediaBrowser.Model.Library; | ||||||
| using MediaBrowser.Model.Querying; | using MediaBrowser.Model.Querying; | ||||||
|  | using Genre = MediaBrowser.Controller.Entities.Genre; | ||||||
|  | using Person = MediaBrowser.Controller.Entities.Person; | ||||||
| 
 | 
 | ||||||
| namespace Emby.Server.Implementations.Library | namespace Emby.Server.Implementations.Library | ||||||
| { | { | ||||||
| @ -125,12 +129,12 @@ namespace Emby.Server.Implementations.Library | |||||||
| 
 | 
 | ||||||
|             if (!query.IncludeHidden) |             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 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 |             return list | ||||||
|                 .OrderBy(i => |                 .OrderBy(i => | ||||||
| @ -165,7 +169,13 @@ namespace Emby.Server.Implementations.Library | |||||||
|             return GetUserSubViewWithName(name, parentId, type, sortName); |             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))) |             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) |                 parents = _libraryManager.GetUserRootFolder().GetChildren(user, true) | ||||||
|                     .Where(i => i is Folder) |                     .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(); |                     .ToList(); | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
| @ -331,12 +342,11 @@ namespace Emby.Server.Implementations.Library | |||||||
| 
 | 
 | ||||||
|             var excludeItemTypes = includeItemTypes.Length == 0 && mediaTypes.Count == 0 ? new[] |             var excludeItemTypes = includeItemTypes.Length == 0 && mediaTypes.Count == 0 ? new[] | ||||||
|             { |             { | ||||||
|                 typeof(Person).Name, |                 nameof(Person), | ||||||
|                 typeof(Studio).Name, |                 nameof(Studio), | ||||||
|                 typeof(Year).Name, |                 nameof(Year), | ||||||
|                 typeof(MusicGenre).Name, |                 nameof(MusicGenre), | ||||||
|                 typeof(Genre).Name |                 nameof(Genre) | ||||||
| 
 |  | ||||||
|             } : Array.Empty<string>(); |             } : Array.Empty<string>(); | ||||||
| 
 | 
 | ||||||
|             var query = new InternalItemsQuery(user) |             var query = new InternalItemsQuery(user) | ||||||
|  | |||||||
| @ -7,6 +7,8 @@ using System.Linq; | |||||||
| using System.Threading; | using System.Threading; | ||||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||||
| using Emby.Server.Implementations.Library; | using Emby.Server.Implementations.Library; | ||||||
|  | using Jellyfin.Data.Entities; | ||||||
|  | using Jellyfin.Data.Enums; | ||||||
| using MediaBrowser.Common.Configuration; | using MediaBrowser.Common.Configuration; | ||||||
| using MediaBrowser.Common.Extensions; | using MediaBrowser.Common.Extensions; | ||||||
| using MediaBrowser.Common.Progress; | using MediaBrowser.Common.Progress; | ||||||
| @ -14,8 +16,6 @@ using MediaBrowser.Controller.Channels; | |||||||
| using MediaBrowser.Controller.Configuration; | using MediaBrowser.Controller.Configuration; | ||||||
| using MediaBrowser.Controller.Dto; | using MediaBrowser.Controller.Dto; | ||||||
| using MediaBrowser.Controller.Entities; | using MediaBrowser.Controller.Entities; | ||||||
| using MediaBrowser.Controller.Entities.Movies; |  | ||||||
| using MediaBrowser.Controller.Entities.TV; |  | ||||||
| using MediaBrowser.Controller.Library; | using MediaBrowser.Controller.Library; | ||||||
| using MediaBrowser.Controller.LiveTv; | using MediaBrowser.Controller.LiveTv; | ||||||
| using MediaBrowser.Controller.Persistence; | using MediaBrowser.Controller.Persistence; | ||||||
| @ -31,6 +31,8 @@ using MediaBrowser.Model.Querying; | |||||||
| using MediaBrowser.Model.Serialization; | using MediaBrowser.Model.Serialization; | ||||||
| using MediaBrowser.Model.Tasks; | using MediaBrowser.Model.Tasks; | ||||||
| using Microsoft.Extensions.Logging; | using Microsoft.Extensions.Logging; | ||||||
|  | using Episode = MediaBrowser.Controller.Entities.TV.Episode; | ||||||
|  | using Movie = MediaBrowser.Controller.Entities.Movies.Movie; | ||||||
| 
 | 
 | ||||||
| namespace Emby.Server.Implementations.LiveTv | namespace Emby.Server.Implementations.LiveTv | ||||||
| { | { | ||||||
| @ -696,7 +698,6 @@ namespace Emby.Server.Implementations.LiveTv | |||||||
|                     { |                     { | ||||||
|                         Path = info.ThumbImageUrl, |                         Path = info.ThumbImageUrl, | ||||||
|                         Type = ImageType.Thumb |                         Type = ImageType.Thumb | ||||||
| 
 |  | ||||||
|                     }, 0); |                     }, 0); | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
| @ -709,7 +710,6 @@ namespace Emby.Server.Implementations.LiveTv | |||||||
|                     { |                     { | ||||||
|                         Path = info.LogoImageUrl, |                         Path = info.LogoImageUrl, | ||||||
|                         Type = ImageType.Logo |                         Type = ImageType.Logo | ||||||
| 
 |  | ||||||
|                     }, 0); |                     }, 0); | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
| @ -722,7 +722,6 @@ namespace Emby.Server.Implementations.LiveTv | |||||||
|                     { |                     { | ||||||
|                         Path = info.BackdropImageUrl, |                         Path = info.BackdropImageUrl, | ||||||
|                         Type = ImageType.Backdrop |                         Type = ImageType.Backdrop | ||||||
| 
 |  | ||||||
|                     }, 0); |                     }, 0); | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
| @ -760,7 +759,8 @@ namespace Emby.Server.Implementations.LiveTv | |||||||
| 
 | 
 | ||||||
|             var dto = _dtoService.GetBaseItemDto(program, new DtoOptions(), user); |             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) |                 new Tuple<BaseItemDto, string, string>(dto, program.ExternalId, program.ExternalSeriesId) | ||||||
|             }; |             }; | ||||||
| 
 | 
 | ||||||
| @ -2167,20 +2167,19 @@ namespace Emby.Server.Implementations.LiveTv | |||||||
|             var info = new LiveTvInfo |             var info = new LiveTvInfo | ||||||
|             { |             { | ||||||
|                 Services = services, |                 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; |             return info; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         private bool IsLiveTvEnabled(User user) |         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() |         public IEnumerable<User> GetEnabledUsers() | ||||||
|  | |||||||
| @ -3,6 +3,7 @@ | |||||||
| using System.Collections.Generic; | using System.Collections.Generic; | ||||||
| using System.Linq; | using System.Linq; | ||||||
| using System.Text.Json.Serialization; | using System.Text.Json.Serialization; | ||||||
|  | using Jellyfin.Data.Entities; | ||||||
| using MediaBrowser.Controller.Entities; | using MediaBrowser.Controller.Entities; | ||||||
| using MediaBrowser.Controller.Playlists; | using MediaBrowser.Controller.Playlists; | ||||||
| using MediaBrowser.Model.Querying; | using MediaBrowser.Model.Querying; | ||||||
| @ -44,7 +45,7 @@ namespace Emby.Server.Implementations.Playlists | |||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             query.Recursive = true; |             query.Recursive = true; | ||||||
|             query.IncludeItemTypes = new string[] { "Playlist" }; |             query.IncludeItemTypes = new[] { "Playlist" }; | ||||||
|             query.Parent = null; |             query.Parent = null; | ||||||
|             return LibraryManager.GetItemsResult(query); |             return LibraryManager.GetItemsResult(query); | ||||||
|         } |         } | ||||||
|  | |||||||
| @ -7,6 +7,7 @@ using System.IO; | |||||||
| using System.Linq; | using System.Linq; | ||||||
| using System.Threading; | using System.Threading; | ||||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||||
|  | using Jellyfin.Data.Entities; | ||||||
| using MediaBrowser.Controller.Dto; | using MediaBrowser.Controller.Dto; | ||||||
| using MediaBrowser.Controller.Entities; | using MediaBrowser.Controller.Entities; | ||||||
| using MediaBrowser.Controller.Entities.Audio; | using MediaBrowser.Controller.Entities.Audio; | ||||||
| @ -21,6 +22,8 @@ using Microsoft.Extensions.Configuration; | |||||||
| using Microsoft.Extensions.Logging; | using Microsoft.Extensions.Logging; | ||||||
| using PlaylistsNET.Content; | using PlaylistsNET.Content; | ||||||
| using PlaylistsNET.Models; | using PlaylistsNET.Models; | ||||||
|  | using Genre = MediaBrowser.Controller.Entities.Genre; | ||||||
|  | using MusicAlbum = MediaBrowser.Controller.Entities.Audio.MusicAlbum; | ||||||
| 
 | 
 | ||||||
| namespace Emby.Server.Implementations.Playlists | namespace Emby.Server.Implementations.Playlists | ||||||
| { | { | ||||||
|  | |||||||
| @ -7,6 +7,8 @@ using System.Globalization; | |||||||
| using System.Linq; | using System.Linq; | ||||||
| using System.Threading; | using System.Threading; | ||||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||||
|  | using Jellyfin.Data.Entities; | ||||||
|  | using Jellyfin.Data.Enums; | ||||||
| using MediaBrowser.Common.Events; | using MediaBrowser.Common.Events; | ||||||
| using MediaBrowser.Common.Extensions; | using MediaBrowser.Common.Extensions; | ||||||
| using MediaBrowser.Controller; | using MediaBrowser.Controller; | ||||||
| @ -15,7 +17,6 @@ using MediaBrowser.Controller.Devices; | |||||||
| using MediaBrowser.Controller.Drawing; | using MediaBrowser.Controller.Drawing; | ||||||
| using MediaBrowser.Controller.Dto; | using MediaBrowser.Controller.Dto; | ||||||
| using MediaBrowser.Controller.Entities; | using MediaBrowser.Controller.Entities; | ||||||
| using MediaBrowser.Controller.Entities.TV; |  | ||||||
| using MediaBrowser.Controller.Library; | using MediaBrowser.Controller.Library; | ||||||
| using MediaBrowser.Controller.Net; | using MediaBrowser.Controller.Net; | ||||||
| using MediaBrowser.Controller.Security; | using MediaBrowser.Controller.Security; | ||||||
| @ -28,7 +29,9 @@ using MediaBrowser.Model.Library; | |||||||
| using MediaBrowser.Model.Querying; | using MediaBrowser.Model.Querying; | ||||||
| using MediaBrowser.Model.Session; | using MediaBrowser.Model.Session; | ||||||
| using MediaBrowser.Model.SyncPlay; | using MediaBrowser.Model.SyncPlay; | ||||||
|  | using Microsoft.EntityFrameworkCore; | ||||||
| using Microsoft.Extensions.Logging; | using Microsoft.Extensions.Logging; | ||||||
|  | using Episode = MediaBrowser.Controller.Entities.TV.Episode; | ||||||
| 
 | 
 | ||||||
| namespace Emby.Server.Implementations.Session | namespace Emby.Server.Implementations.Session | ||||||
| { | { | ||||||
| @ -283,11 +286,18 @@ namespace Emby.Server.Implementations.Session | |||||||
|             if (user != null) |             if (user != null) | ||||||
|             { |             { | ||||||
|                 var userLastActivityDate = user.LastActivityDate ?? DateTime.MinValue; |                 var userLastActivityDate = user.LastActivityDate ?? DateTime.MinValue; | ||||||
|                 user.LastActivityDate = activityDate; |  | ||||||
| 
 | 
 | ||||||
|                 if ((activityDate - userLastActivityDate).TotalSeconds > 60) |                 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="remoteEndPoint">The remote end point.</param> | ||||||
|         /// <param name="user">The user.</param> |         /// <param name="user">The user.</param> | ||||||
|         /// <returns>SessionInfo.</returns> |         /// <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(); |             CheckDisposed(); | ||||||
| 
 | 
 | ||||||
| @ -447,14 +463,13 @@ namespace Emby.Server.Implementations.Session | |||||||
| 
 | 
 | ||||||
|             CheckDisposed(); |             CheckDisposed(); | ||||||
| 
 | 
 | ||||||
|             var sessionInfo = _activeConnections.GetOrAdd(key, k => |             var sessionInfo = _activeConnections.GetOrAdd( | ||||||
|             { |                 key, | ||||||
|                 return CreateSession(k, appName, appVersion, deviceId, deviceName, remoteEndPoint, user); |                 k => CreateSession(k, appName, appVersion, deviceId, deviceName, remoteEndPoint, user)); | ||||||
|             }); |  | ||||||
| 
 | 
 | ||||||
|             sessionInfo.UserId = user == null ? Guid.Empty : user.Id; |             sessionInfo.UserId = user?.Id ?? Guid.Empty; | ||||||
|             sessionInfo.UserName = user?.Name; |             sessionInfo.UserName = user?.Username; | ||||||
|             sessionInfo.UserPrimaryImageTag = user == null ? null : GetImageCacheTag(user, ImageType.Primary); |             sessionInfo.UserPrimaryImageTag = user?.ProfileImage == null ? null : GetImageCacheTag(user); | ||||||
|             sessionInfo.RemoteEndPoint = remoteEndPoint; |             sessionInfo.RemoteEndPoint = remoteEndPoint; | ||||||
|             sessionInfo.Client = appName; |             sessionInfo.Client = appName; | ||||||
| 
 | 
 | ||||||
| @ -473,7 +488,14 @@ namespace Emby.Server.Implementations.Session | |||||||
|             return sessionInfo; |             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) |             var sessionInfo = new SessionInfo(this, _logger) | ||||||
|             { |             { | ||||||
| @ -483,11 +505,11 @@ namespace Emby.Server.Implementations.Session | |||||||
|                 Id = key.GetMD5().ToString("N", CultureInfo.InvariantCulture) |                 Id = key.GetMD5().ToString("N", CultureInfo.InvariantCulture) | ||||||
|             }; |             }; | ||||||
| 
 | 
 | ||||||
|             var username = user?.Name; |             var username = user?.Username; | ||||||
| 
 | 
 | ||||||
|             sessionInfo.UserId = user?.Id ?? Guid.Empty; |             sessionInfo.UserId = user?.Id ?? Guid.Empty; | ||||||
|             sessionInfo.UserName = username; |             sessionInfo.UserName = username; | ||||||
|             sessionInfo.UserPrimaryImageTag = user == null ? null : GetImageCacheTag(user, ImageType.Primary); |             sessionInfo.UserPrimaryImageTag = user?.ProfileImage == null ? null : GetImageCacheTag(user); | ||||||
|             sessionInfo.RemoteEndPoint = remoteEndPoint; |             sessionInfo.RemoteEndPoint = remoteEndPoint; | ||||||
| 
 | 
 | ||||||
|             if (string.IsNullOrEmpty(deviceName)) |             if (string.IsNullOrEmpty(deviceName)) | ||||||
| @ -535,10 +557,7 @@ namespace Emby.Server.Implementations.Session | |||||||
| 
 | 
 | ||||||
|         private void StartIdleCheckTimer() |         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() |         private void StopIdleCheckTimer() | ||||||
| @ -786,7 +805,7 @@ namespace Emby.Server.Implementations.Session | |||||||
|         { |         { | ||||||
|             var changed = false; |             var changed = false; | ||||||
| 
 | 
 | ||||||
|             if (user.Configuration.RememberAudioSelections) |             if (user.RememberAudioSelections) | ||||||
|             { |             { | ||||||
|                 if (data.AudioStreamIndex != info.AudioStreamIndex) |                 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) |                 if (data.SubtitleStreamIndex != info.SubtitleStreamIndex) | ||||||
|                 { |                 { | ||||||
| @ -1114,13 +1133,13 @@ namespace Emby.Server.Implementations.Session | |||||||
|                 if (items.Any(i => i.GetPlayAccess(user) != PlayAccess.Full)) |                 if (items.Any(i => i.GetPlayAccess(user) != PlayAccess.Full)) | ||||||
|                 { |                 { | ||||||
|                     throw new ArgumentException( |                     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 |             if (user != null | ||||||
|                 && command.ItemIds.Length == 1 |                 && command.ItemIds.Length == 1 | ||||||
|                 && user.Configuration.EnableNextEpisodeAutoPlay |                 && user.EnableNextEpisodeAutoPlay | ||||||
|                 && _libraryManager.GetItemById(command.ItemIds[0]) is Episode episode) |                 && _libraryManager.GetItemById(command.ItemIds[0]) is Episode episode) | ||||||
|             { |             { | ||||||
|                 var series = episode.Series; |                 var series = episode.Series; | ||||||
| @ -1191,7 +1210,7 @@ namespace Emby.Server.Implementations.Session | |||||||
|                     DtoOptions = new DtoOptions(false) |                     DtoOptions = new DtoOptions(false) | ||||||
|                     { |                     { | ||||||
|                         EnableImages = false, |                         EnableImages = false, | ||||||
|                         Fields = new ItemFields[] |                         Fields = new[] | ||||||
|                         { |                         { | ||||||
|                             ItemFields.SortName |                             ItemFields.SortName | ||||||
|                         } |                         } | ||||||
| @ -1353,7 +1372,7 @@ namespace Emby.Server.Implementations.Session | |||||||
|                 list.Add(new SessionUserInfo |                 list.Add(new SessionUserInfo | ||||||
|                 { |                 { | ||||||
|                     UserId = userId, |                     UserId = userId, | ||||||
|                     UserName = user.Name |                     UserName = user.Username | ||||||
|                 }); |                 }); | ||||||
| 
 | 
 | ||||||
|                 session.AdditionalUsers = list.ToArray(); |                 session.AdditionalUsers = list.ToArray(); | ||||||
| @ -1513,7 +1532,7 @@ namespace Emby.Server.Implementations.Session | |||||||
|                 DeviceName = deviceName, |                 DeviceName = deviceName, | ||||||
|                 UserId = user.Id, |                 UserId = user.Id, | ||||||
|                 AccessToken = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture), |                 AccessToken = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture), | ||||||
|                 UserName = user.Name |                 UserName = user.Username | ||||||
|             }; |             }; | ||||||
| 
 | 
 | ||||||
|             _logger.LogInformation("Creating new access token for user {0}", user.Id); |             _logger.LogInformation("Creating new access token for user {0}", user.Id); | ||||||
| @ -1710,15 +1729,15 @@ namespace Emby.Server.Implementations.Session | |||||||
|             return info; |             return info; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         private string GetImageCacheTag(BaseItem item, ImageType type) |         private string GetImageCacheTag(User user) | ||||||
|         { |         { | ||||||
|             try |             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; |                 return null; | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| @ -1827,7 +1846,10 @@ namespace Emby.Server.Implementations.Session | |||||||
|         { |         { | ||||||
|             CheckDisposed(); |             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); |             return SendMessageToUserSessions(adminUserIds, name, data, cancellationToken); | ||||||
|         } |         } | ||||||
|  | |||||||
| @ -1,6 +1,7 @@ | |||||||
| #pragma warning disable CS1591 | #pragma warning disable CS1591 | ||||||
| 
 | 
 | ||||||
| using System; | using System; | ||||||
|  | using Jellyfin.Data.Entities; | ||||||
| using MediaBrowser.Controller.Entities; | using MediaBrowser.Controller.Entities; | ||||||
| using MediaBrowser.Controller.Library; | using MediaBrowser.Controller.Library; | ||||||
| using MediaBrowser.Controller.Sorting; | using MediaBrowser.Controller.Sorting; | ||||||
|  | |||||||
| @ -1,4 +1,5 @@ | |||||||
| using System; | using System; | ||||||
|  | using Jellyfin.Data.Entities; | ||||||
| using MediaBrowser.Controller.Entities; | using MediaBrowser.Controller.Entities; | ||||||
| using MediaBrowser.Controller.Library; | using MediaBrowser.Controller.Library; | ||||||
| using MediaBrowser.Controller.Sorting; | using MediaBrowser.Controller.Sorting; | ||||||
|  | |||||||
| @ -1,5 +1,6 @@ | |||||||
| #pragma warning disable CS1591 | #pragma warning disable CS1591 | ||||||
| 
 | 
 | ||||||
|  | using Jellyfin.Data.Entities; | ||||||
| using MediaBrowser.Controller.Entities; | using MediaBrowser.Controller.Entities; | ||||||
| using MediaBrowser.Controller.Library; | using MediaBrowser.Controller.Library; | ||||||
| using MediaBrowser.Controller.Sorting; | using MediaBrowser.Controller.Sorting; | ||||||
|  | |||||||
| @ -1,5 +1,6 @@ | |||||||
| #pragma warning disable CS1591 | #pragma warning disable CS1591 | ||||||
| 
 | 
 | ||||||
|  | using Jellyfin.Data.Entities; | ||||||
| using MediaBrowser.Controller.Entities; | using MediaBrowser.Controller.Entities; | ||||||
| using MediaBrowser.Controller.Library; | using MediaBrowser.Controller.Library; | ||||||
| using MediaBrowser.Controller.Sorting; | using MediaBrowser.Controller.Sorting; | ||||||
|  | |||||||
| @ -1,5 +1,6 @@ | |||||||
| #pragma warning disable CS1591 | #pragma warning disable CS1591 | ||||||
| 
 | 
 | ||||||
|  | using Jellyfin.Data.Entities; | ||||||
| using MediaBrowser.Controller.Entities; | using MediaBrowser.Controller.Entities; | ||||||
| using MediaBrowser.Controller.Library; | using MediaBrowser.Controller.Library; | ||||||
| using MediaBrowser.Controller.Sorting; | using MediaBrowser.Controller.Sorting; | ||||||
|  | |||||||
| @ -1,3 +1,4 @@ | |||||||
|  | using Jellyfin.Data.Entities; | ||||||
| using MediaBrowser.Controller.Entities; | using MediaBrowser.Controller.Entities; | ||||||
| using MediaBrowser.Controller.Library; | using MediaBrowser.Controller.Library; | ||||||
| using MediaBrowser.Controller.Sorting; | using MediaBrowser.Controller.Sorting; | ||||||
|  | |||||||
| @ -3,13 +3,13 @@ using System.Collections.Generic; | |||||||
| using System.Globalization; | using System.Globalization; | ||||||
| using System.Linq; | using System.Linq; | ||||||
| using System.Threading; | using System.Threading; | ||||||
| using Microsoft.Extensions.Logging; | using Jellyfin.Data.Entities; | ||||||
| using MediaBrowser.Controller.Entities; | using Jellyfin.Data.Enums; | ||||||
| using MediaBrowser.Controller.Library; | using MediaBrowser.Controller.Library; | ||||||
| using MediaBrowser.Controller.Session; | using MediaBrowser.Controller.Session; | ||||||
| using MediaBrowser.Controller.SyncPlay; | using MediaBrowser.Controller.SyncPlay; | ||||||
| using MediaBrowser.Model.Configuration; |  | ||||||
| using MediaBrowser.Model.SyncPlay; | using MediaBrowser.Model.SyncPlay; | ||||||
|  | using Microsoft.Extensions.Logging; | ||||||
| 
 | 
 | ||||||
| namespace Emby.Server.Implementations.SyncPlay | namespace Emby.Server.Implementations.SyncPlay | ||||||
| { | { | ||||||
| @ -109,14 +109,6 @@ namespace Emby.Server.Implementations.SyncPlay | |||||||
|             _disposed = true; |             _disposed = true; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         private void CheckDisposed() |  | ||||||
|         { |  | ||||||
|             if (_disposed) |  | ||||||
|             { |  | ||||||
|                 throw new ObjectDisposedException(GetType().Name); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         private void OnSessionManagerSessionEnded(object sender, SessionEventArgs e) |         private void OnSessionManagerSessionEnded(object sender, SessionEventArgs e) | ||||||
|         { |         { | ||||||
|             var session = e.SessionInfo; |             var session = e.SessionInfo; | ||||||
| @ -149,38 +141,24 @@ namespace Emby.Server.Implementations.SyncPlay | |||||||
|             var item = _libraryManager.GetItemById(itemId); |             var item = _libraryManager.GetItemById(itemId); | ||||||
| 
 | 
 | ||||||
|             // Check ParentalRating access |             // Check ParentalRating access | ||||||
|             var hasParentalRatingAccess = true; |             var hasParentalRatingAccess = !user.MaxParentalAgeRating.HasValue | ||||||
|             if (user.Policy.MaxParentalRating.HasValue) |                 || item.InheritedParentalRatingValue <= user.MaxParentalAgeRating; | ||||||
|             { |  | ||||||
|                 hasParentalRatingAccess = item.InheritedParentalRatingValue <= user.Policy.MaxParentalRating; |  | ||||||
|             } |  | ||||||
| 
 | 
 | ||||||
|             if (!user.Policy.EnableAllFolders && hasParentalRatingAccess) |             if (!user.HasPermission(PermissionKind.EnableAllFolders) && hasParentalRatingAccess) | ||||||
|             { |             { | ||||||
|                 var collections = _libraryManager.GetCollectionFolders(item).Select( |                 var collections = _libraryManager.GetCollectionFolders(item).Select( | ||||||
|                     folder => folder.Id.ToString("N", CultureInfo.InvariantCulture) |                     folder => folder.Id.ToString("N", CultureInfo.InvariantCulture)); | ||||||
|                 ); | 
 | ||||||
|                 var intersect = collections.Intersect(user.Policy.EnabledFolders); |                 return collections.Intersect(user.GetPreference(PreferenceKind.EnabledFolders)).Any(); | ||||||
|                 return intersect.Any(); |  | ||||||
|             } |  | ||||||
|             else |  | ||||||
|             { |  | ||||||
|                 return hasParentalRatingAccess; |  | ||||||
|             } |             } | ||||||
|  | 
 | ||||||
|  |             return hasParentalRatingAccess; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         private Guid? GetSessionGroup(SessionInfo session) |         private Guid? GetSessionGroup(SessionInfo session) | ||||||
|         { |         { | ||||||
|             ISyncPlayController group; |             _sessionToGroupMap.TryGetValue(session.Id, out var group); | ||||||
|             _sessionToGroupMap.TryGetValue(session.Id, out group); |             return group?.GetGroupId(); | ||||||
|             if (group != null) |  | ||||||
|             { |  | ||||||
|                 return group.GetGroupId(); |  | ||||||
|             } |  | ||||||
|             else |  | ||||||
|             { |  | ||||||
|                 return null; |  | ||||||
|             } |  | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         /// <inheritdoc /> |         /// <inheritdoc /> | ||||||
| @ -188,7 +166,7 @@ namespace Emby.Server.Implementations.SyncPlay | |||||||
|         { |         { | ||||||
|             var user = _userManager.GetUserById(session.UserId); |             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); |                 _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 |                     Type = GroupUpdateType.CreateGroupDenied | ||||||
|                 }; |                 }; | ||||||
|                 _sessionManager.SendSyncPlayGroupUpdate(session.Id.ToString(), error, CancellationToken.None); |                 _sessionManager.SendSyncPlayGroupUpdate(session.Id, error, CancellationToken.None); | ||||||
|                 return; |                 return; | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
| @ -219,7 +197,7 @@ namespace Emby.Server.Implementations.SyncPlay | |||||||
|         { |         { | ||||||
|             var user = _userManager.GetUserById(session.UserId); |             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); |                 _logger.LogWarning("JoinGroup: {0} does not have access to SyncPlay.", session.Id); | ||||||
| 
 | 
 | ||||||
| @ -227,7 +205,7 @@ namespace Emby.Server.Implementations.SyncPlay | |||||||
|                 { |                 { | ||||||
|                     Type = GroupUpdateType.JoinGroupDenied |                     Type = GroupUpdateType.JoinGroupDenied | ||||||
|                 }; |                 }; | ||||||
|                 _sessionManager.SendSyncPlayGroupUpdate(session.Id.ToString(), error, CancellationToken.None); |                 _sessionManager.SendSyncPlayGroupUpdate(session.Id, error, CancellationToken.None); | ||||||
|                 return; |                 return; | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
| @ -244,7 +222,7 @@ namespace Emby.Server.Implementations.SyncPlay | |||||||
|                     { |                     { | ||||||
|                         Type = GroupUpdateType.GroupDoesNotExist |                         Type = GroupUpdateType.GroupDoesNotExist | ||||||
|                     }; |                     }; | ||||||
|                     _sessionManager.SendSyncPlayGroupUpdate(session.Id.ToString(), error, CancellationToken.None); |                     _sessionManager.SendSyncPlayGroupUpdate(session.Id, error, CancellationToken.None); | ||||||
|                     return; |                     return; | ||||||
|                 } |                 } | ||||||
| 
 | 
 | ||||||
| @ -257,7 +235,7 @@ namespace Emby.Server.Implementations.SyncPlay | |||||||
|                         GroupId = group.GetGroupId().ToString(), |                         GroupId = group.GetGroupId().ToString(), | ||||||
|                         Type = GroupUpdateType.LibraryAccessDenied |                         Type = GroupUpdateType.LibraryAccessDenied | ||||||
|                     }; |                     }; | ||||||
|                     _sessionManager.SendSyncPlayGroupUpdate(session.Id.ToString(), error, CancellationToken.None); |                     _sessionManager.SendSyncPlayGroupUpdate(session.Id, error, CancellationToken.None); | ||||||
|                     return; |                     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 |             // TODO: determine what happens to users that are in a group and get their permissions revoked | ||||||
|             lock (_groupsLock) |             lock (_groupsLock) | ||||||
|             { |             { | ||||||
|                 ISyncPlayController group; |                 _sessionToGroupMap.TryGetValue(session.Id, out var group); | ||||||
|                 _sessionToGroupMap.TryGetValue(session.Id, out group); |  | ||||||
| 
 | 
 | ||||||
|                 if (group == null) |                 if (group == null) | ||||||
|                 { |                 { | ||||||
| @ -292,7 +269,7 @@ namespace Emby.Server.Implementations.SyncPlay | |||||||
|                     { |                     { | ||||||
|                         Type = GroupUpdateType.NotInGroup |                         Type = GroupUpdateType.NotInGroup | ||||||
|                     }; |                     }; | ||||||
|                     _sessionManager.SendSyncPlayGroupUpdate(session.Id.ToString(), error, CancellationToken.None); |                     _sessionManager.SendSyncPlayGroupUpdate(session.Id, error, CancellationToken.None); | ||||||
|                     return; |                     return; | ||||||
|                 } |                 } | ||||||
| 
 | 
 | ||||||
| @ -311,7 +288,7 @@ namespace Emby.Server.Implementations.SyncPlay | |||||||
|         { |         { | ||||||
|             var user = _userManager.GetUserById(session.UserId); |             var user = _userManager.GetUserById(session.UserId); | ||||||
| 
 | 
 | ||||||
|             if (user.Policy.SyncPlayAccess == SyncPlayAccess.None) |             if (user.SyncPlayAccess == SyncPlayAccess.None) | ||||||
|             { |             { | ||||||
|                 return new List<GroupInfoView>(); |                 return new List<GroupInfoView>(); | ||||||
|             } |             } | ||||||
| @ -341,7 +318,7 @@ namespace Emby.Server.Implementations.SyncPlay | |||||||
|         { |         { | ||||||
|             var user = _userManager.GetUserById(session.UserId); |             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); |                 _logger.LogWarning("HandleRequest: {0} does not have access to SyncPlay.", session.Id); | ||||||
| 
 | 
 | ||||||
| @ -349,14 +326,13 @@ namespace Emby.Server.Implementations.SyncPlay | |||||||
|                 { |                 { | ||||||
|                     Type = GroupUpdateType.JoinGroupDenied |                     Type = GroupUpdateType.JoinGroupDenied | ||||||
|                 }; |                 }; | ||||||
|                 _sessionManager.SendSyncPlayGroupUpdate(session.Id.ToString(), error, CancellationToken.None); |                 _sessionManager.SendSyncPlayGroupUpdate(session.Id, error, CancellationToken.None); | ||||||
|                 return; |                 return; | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             lock (_groupsLock) |             lock (_groupsLock) | ||||||
|             { |             { | ||||||
|                 ISyncPlayController group; |                 _sessionToGroupMap.TryGetValue(session.Id, out var group); | ||||||
|                 _sessionToGroupMap.TryGetValue(session.Id, out group); |  | ||||||
| 
 | 
 | ||||||
|                 if (group == null) |                 if (group == null) | ||||||
|                 { |                 { | ||||||
| @ -366,7 +342,7 @@ namespace Emby.Server.Implementations.SyncPlay | |||||||
|                     { |                     { | ||||||
|                         Type = GroupUpdateType.NotInGroup |                         Type = GroupUpdateType.NotInGroup | ||||||
|                     }; |                     }; | ||||||
|                     _sessionManager.SendSyncPlayGroupUpdate(session.Id.ToString(), error, CancellationToken.None); |                     _sessionManager.SendSyncPlayGroupUpdate(session.Id, error, CancellationToken.None); | ||||||
|                     return; |                     return; | ||||||
|                 } |                 } | ||||||
| 
 | 
 | ||||||
| @ -393,8 +369,7 @@ namespace Emby.Server.Implementations.SyncPlay | |||||||
|                 throw new InvalidOperationException("Session not in any group!"); |                 throw new InvalidOperationException("Session not in any group!"); | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             ISyncPlayController tempGroup; |             _sessionToGroupMap.Remove(session.Id, out var tempGroup); | ||||||
|             _sessionToGroupMap.Remove(session.Id, out tempGroup); |  | ||||||
| 
 | 
 | ||||||
|             if (!tempGroup.GetGroupId().Equals(group.GetGroupId())) |             if (!tempGroup.GetGroupId().Equals(group.GetGroupId())) | ||||||
|             { |             { | ||||||
|  | |||||||
| @ -4,13 +4,17 @@ using System; | |||||||
| using System.Collections.Generic; | using System.Collections.Generic; | ||||||
| using System.Globalization; | using System.Globalization; | ||||||
| using System.Linq; | using System.Linq; | ||||||
|  | using Jellyfin.Data.Entities; | ||||||
|  | using Jellyfin.Data.Enums; | ||||||
|  | using MediaBrowser.Controller.Configuration; | ||||||
| using MediaBrowser.Controller.Dto; | using MediaBrowser.Controller.Dto; | ||||||
| using MediaBrowser.Controller.Entities; | using MediaBrowser.Controller.Entities; | ||||||
| using MediaBrowser.Controller.Entities.TV; |  | ||||||
| using MediaBrowser.Controller.Library; | using MediaBrowser.Controller.Library; | ||||||
| using MediaBrowser.Controller.TV; | using MediaBrowser.Controller.TV; | ||||||
| using MediaBrowser.Model.Entities; | using MediaBrowser.Model.Entities; | ||||||
| using MediaBrowser.Model.Querying; | using MediaBrowser.Model.Querying; | ||||||
|  | using Episode = MediaBrowser.Controller.Entities.TV.Episode; | ||||||
|  | using Series = MediaBrowser.Controller.Entities.TV.Series; | ||||||
| 
 | 
 | ||||||
| namespace Emby.Server.Implementations.TV | namespace Emby.Server.Implementations.TV | ||||||
| { | { | ||||||
| @ -73,7 +77,8 @@ namespace Emby.Server.Implementations.TV | |||||||
|             { |             { | ||||||
|                 parents = _libraryManager.GetUserRootFolder().GetChildren(user, true) |                 parents = _libraryManager.GetUserRootFolder().GetChildren(user, true) | ||||||
|                    .Where(i => i is Folder) |                    .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(); |                    .ToArray(); | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
| @ -191,7 +196,7 @@ namespace Emby.Server.Implementations.TV | |||||||
|             { |             { | ||||||
|                 AncestorWithPresentationUniqueKey = null, |                 AncestorWithPresentationUniqueKey = null, | ||||||
|                 SeriesPresentationUniqueKey = seriesKey, |                 SeriesPresentationUniqueKey = seriesKey, | ||||||
|                 IncludeItemTypes = new[] { typeof(Episode).Name }, |                 IncludeItemTypes = new[] { nameof(Episode) }, | ||||||
|                 OrderBy = new[] { new ValueTuple<string, SortOrder>(ItemSortBy.SortName, SortOrder.Descending) }, |                 OrderBy = new[] { new ValueTuple<string, SortOrder>(ItemSortBy.SortName, SortOrder.Descending) }, | ||||||
|                 IsPlayed = true, |                 IsPlayed = true, | ||||||
|                 Limit = 1, |                 Limit = 1, | ||||||
| @ -204,7 +209,6 @@ namespace Emby.Server.Implementations.TV | |||||||
|                     }, |                     }, | ||||||
|                     EnableImages = false |                     EnableImages = false | ||||||
|                 } |                 } | ||||||
| 
 |  | ||||||
|             }).FirstOrDefault(); |             }).FirstOrDefault(); | ||||||
| 
 | 
 | ||||||
|             Func<Episode> getEpisode = () => |             Func<Episode> getEpisode = () => | ||||||
| @ -219,7 +223,7 @@ namespace Emby.Server.Implementations.TV | |||||||
|                     IsPlayed = false, |                     IsPlayed = false, | ||||||
|                     IsVirtualItem = false, |                     IsVirtualItem = false, | ||||||
|                     ParentIndexNumberNotEquals = 0, |                     ParentIndexNumberNotEquals = 0, | ||||||
|                     MinSortName = lastWatchedEpisode == null ? null : lastWatchedEpisode.SortName, |                     MinSortName = lastWatchedEpisode?.SortName, | ||||||
|                     DtoOptions = dtoOptions |                     DtoOptions = dtoOptions | ||||||
| 
 | 
 | ||||||
|                 }).Cast<Episode>().FirstOrDefault(); |                 }).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.Authentication; | ||||||
| using System.Security.Claims; | using System.Security.Claims; | ||||||
| using System.Text.Encodings.Web; | using System.Text.Encodings.Web; | ||||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||||
| using Jellyfin.Api.Constants; | using Jellyfin.Api.Constants; | ||||||
|  | using Jellyfin.Data.Enums; | ||||||
| using MediaBrowser.Controller.Net; | using MediaBrowser.Controller.Net; | ||||||
| using Microsoft.AspNetCore.Authentication; | using Microsoft.AspNetCore.Authentication; | ||||||
| using Microsoft.Extensions.Logging; | using Microsoft.Extensions.Logging; | ||||||
| @ -38,15 +40,10 @@ namespace Jellyfin.Api.Auth | |||||||
|         /// <inheritdoc /> |         /// <inheritdoc /> | ||||||
|         protected override Task<AuthenticateResult> HandleAuthenticateAsync() |         protected override Task<AuthenticateResult> HandleAuthenticateAsync() | ||||||
|         { |         { | ||||||
|             var authenticatedAttribute = new AuthenticatedAttribute |  | ||||||
|             { |  | ||||||
|                 IgnoreLegacyAuth = true |  | ||||||
|             }; |  | ||||||
| 
 |  | ||||||
|             try |             try | ||||||
|             { |             { | ||||||
|                 var user = _authService.Authenticate(Request, authenticatedAttribute); |                 var authorizationInfo = _authService.Authenticate(Request); | ||||||
|                 if (user == null) |                 if (authorizationInfo == null) | ||||||
|                 { |                 { | ||||||
|                     return Task.FromResult(AuthenticateResult.NoResult()); |                     return Task.FromResult(AuthenticateResult.NoResult()); | ||||||
|                     // TODO return when legacy API is removed. |                     // TODO return when legacy API is removed. | ||||||
| @ -56,11 +53,16 @@ namespace Jellyfin.Api.Auth | |||||||
| 
 | 
 | ||||||
|                 var claims = new[] |                 var claims = new[] | ||||||
|                 { |                 { | ||||||
|                     new Claim(ClaimTypes.Name, user.Name), |                     new Claim(ClaimTypes.Name, authorizationInfo.User.Username), | ||||||
|                     new Claim( |                     new Claim(ClaimTypes.Role, value: authorizationInfo.User.HasPermission(PermissionKind.IsAdministrator) ? UserRoles.Administrator : UserRoles.User), | ||||||
|                         ClaimTypes.Role, |                     new Claim(InternalClaimTypes.UserId, authorizationInfo.UserId.ToString("N", CultureInfo.InvariantCulture)), | ||||||
|                         value: user.Policy.IsAdministrator ? UserRoles.Administrator : UserRoles.User) |                     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 identity = new ClaimsIdentity(claims, Scheme.Name); | ||||||
|                 var principal = new ClaimsPrincipal(identity); |                 var principal = new ClaimsPrincipal(identity); | ||||||
|                 var ticket = new AuthenticationTicket(principal, Scheme.Name); |                 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 System.Threading.Tasks; | ||||||
| using Jellyfin.Api.Constants; | using Jellyfin.Api.Constants; | ||||||
| using MediaBrowser.Common.Configuration; | using MediaBrowser.Common.Configuration; | ||||||
|  | using MediaBrowser.Common.Net; | ||||||
|  | using MediaBrowser.Controller.Library; | ||||||
| using Microsoft.AspNetCore.Authorization; | using Microsoft.AspNetCore.Authorization; | ||||||
|  | using Microsoft.AspNetCore.Http; | ||||||
| 
 | 
 | ||||||
| namespace Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy | namespace Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy | ||||||
| { | { | ||||||
|     /// <summary> |     /// <summary> | ||||||
|     /// Authorization handler for requiring first time setup or elevated privileges. |     /// Authorization handler for requiring first time setup or elevated privileges. | ||||||
|     /// </summary> |     /// </summary> | ||||||
|     public class FirstTimeSetupOrElevatedHandler : AuthorizationHandler<FirstTimeSetupOrElevatedRequirement> |     public class FirstTimeSetupOrElevatedHandler : BaseAuthorizationHandler<FirstTimeSetupOrElevatedRequirement> | ||||||
|     { |     { | ||||||
|         private readonly IConfigurationManager _configurationManager; |         private readonly IConfigurationManager _configurationManager; | ||||||
| 
 | 
 | ||||||
|         /// <summary> |         /// <summary> | ||||||
|         /// Initializes a new instance of the <see cref="FirstTimeSetupOrElevatedHandler" /> class. |         /// Initializes a new instance of the <see cref="FirstTimeSetupOrElevatedHandler" /> class. | ||||||
|         /// </summary> |         /// </summary> | ||||||
|         /// <param name="configurationManager">The jellyfin configuration manager.</param> |         /// <param name="configurationManager">Instance of the <see cref="IConfigurationManager"/> interface.</param> | ||||||
|         public FirstTimeSetupOrElevatedHandler(IConfigurationManager configurationManager) |         /// <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; |             _configurationManager = configurationManager; | ||||||
|         } |         } | ||||||
| @ -27,8 +38,11 @@ namespace Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy | |||||||
|             if (!_configurationManager.CommonConfiguration.IsStartupWizardCompleted) |             if (!_configurationManager.CommonConfiguration.IsStartupWizardCompleted) | ||||||
|             { |             { | ||||||
|                 context.Succeed(firstTimeSetupOrElevatedRequirement); |                 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); |                 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 System.Threading.Tasks; | ||||||
| using Jellyfin.Api.Constants; | using Jellyfin.Api.Constants; | ||||||
|  | using MediaBrowser.Common.Net; | ||||||
|  | using MediaBrowser.Controller.Library; | ||||||
| using Microsoft.AspNetCore.Authorization; | using Microsoft.AspNetCore.Authorization; | ||||||
|  | using Microsoft.AspNetCore.Http; | ||||||
| 
 | 
 | ||||||
| namespace Jellyfin.Api.Auth.RequiresElevationPolicy | namespace Jellyfin.Api.Auth.RequiresElevationPolicy | ||||||
| { | { | ||||||
|     /// <summary> |     /// <summary> | ||||||
|     /// Authorization handler for requiring elevated privileges. |     /// Authorization handler for requiring elevated privileges. | ||||||
|     /// </summary> |     /// </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 /> |         /// <inheritdoc /> | ||||||
|         protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, RequiresElevationRequirement requirement) |         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); |                 context.Succeed(requirement); | ||||||
|             } |             } | ||||||
|  |             else | ||||||
|  |             { | ||||||
|  |                 context.Fail(); | ||||||
|  |             } | ||||||
| 
 | 
 | ||||||
|             return Task.CompletedTask; |             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> |     /// </summary> | ||||||
|     public static class Policies |     public static class Policies | ||||||
|     { |     { | ||||||
|  |         /// <summary> | ||||||
|  |         /// Policy name for default authorization. | ||||||
|  |         /// </summary> | ||||||
|  |         public const string DefaultAuthorization = "DefaultAuthorization"; | ||||||
|  | 
 | ||||||
|         /// <summary> |         /// <summary> | ||||||
|         /// Policy name for requiring first time setup or elevated privileges. |         /// Policy name for requiring first time setup or elevated privileges. | ||||||
|         /// </summary> |         /// </summary> | ||||||
| @ -14,5 +19,15 @@ namespace Jellyfin.Api.Constants | |||||||
|         /// Policy name for requiring elevated privileges. |         /// Policy name for requiring elevated privileges. | ||||||
|         /// </summary> |         /// </summary> | ||||||
|         public const string RequiresElevation = "RequiresElevation"; |         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; | ||||||
|  | using System.Diagnostics.CodeAnalysis; | ||||||
| using System.Linq; | using System.Linq; | ||||||
| using Jellyfin.Api.Constants; | using Jellyfin.Api.Constants; | ||||||
| using Jellyfin.Data.Entities; | using Jellyfin.Data.Entities; | ||||||
| @ -42,6 +40,7 @@ namespace Jellyfin.Api.Controllers | |||||||
|         /// <returns>A <see cref="QueryResult{ActivityLogEntry}"/> containing the log entries.</returns> |         /// <returns>A <see cref="QueryResult{ActivityLogEntry}"/> containing the log entries.</returns> | ||||||
|         [HttpGet("Entries")] |         [HttpGet("Entries")] | ||||||
|         [ProducesResponseType(StatusCodes.Status200OK)] |         [ProducesResponseType(StatusCodes.Status200OK)] | ||||||
|  |         [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "hasUserId", Justification = "Imported from ServiceStack")] | ||||||
|         public ActionResult<QueryResult<ActivityLogEntry>> GetLogEntries( |         public ActionResult<QueryResult<ActivityLogEntry>> GetLogEntries( | ||||||
|             [FromQuery] int? startIndex, |             [FromQuery] int? startIndex, | ||||||
|             [FromQuery] int? limit, |             [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.Text.Json; | ||||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||||
| using Jellyfin.Api.Constants; | using Jellyfin.Api.Constants; | ||||||
| @ -18,7 +16,7 @@ namespace Jellyfin.Api.Controllers | |||||||
|     /// Configuration Controller. |     /// Configuration Controller. | ||||||
|     /// </summary> |     /// </summary> | ||||||
|     [Route("System")] |     [Route("System")] | ||||||
|     [Authorize] |     [Authorize(Policy = Policies.DefaultAuthorization)] | ||||||
|     public class ConfigurationController : BaseJellyfinApiController |     public class ConfigurationController : BaseJellyfinApiController | ||||||
|     { |     { | ||||||
|         private readonly IServerConfigurationManager _configurationManager; |         private readonly IServerConfigurationManager _configurationManager; | ||||||
| @ -53,15 +51,15 @@ namespace Jellyfin.Api.Controllers | |||||||
|         /// Updates application configuration. |         /// Updates application configuration. | ||||||
|         /// </summary> |         /// </summary> | ||||||
|         /// <param name="configuration">Configuration.</param> |         /// <param name="configuration">Configuration.</param> | ||||||
|         /// <response code="200">Configuration updated.</response> |         /// <response code="204">Configuration updated.</response> | ||||||
|         /// <returns>Update status.</returns> |         /// <returns>Update status.</returns> | ||||||
|         [HttpPost("Configuration")] |         [HttpPost("Configuration")] | ||||||
|         [Authorize(Policy = Policies.RequiresElevation)] |         [Authorize(Policy = Policies.RequiresElevation)] | ||||||
|         [ProducesResponseType(StatusCodes.Status200OK)] |         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||||
|         public ActionResult UpdateConfiguration([FromBody, BindRequired] ServerConfiguration configuration) |         public ActionResult UpdateConfiguration([FromBody, BindRequired] ServerConfiguration configuration) | ||||||
|         { |         { | ||||||
|             _configurationManager.ReplaceConfiguration(configuration); |             _configurationManager.ReplaceConfiguration(configuration); | ||||||
|             return Ok(); |             return NoContent(); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         /// <summary> |         /// <summary> | ||||||
| @ -70,7 +68,7 @@ namespace Jellyfin.Api.Controllers | |||||||
|         /// <param name="key">Configuration key.</param> |         /// <param name="key">Configuration key.</param> | ||||||
|         /// <response code="200">Configuration returned.</response> |         /// <response code="200">Configuration returned.</response> | ||||||
|         /// <returns>Configuration.</returns> |         /// <returns>Configuration.</returns> | ||||||
|         [HttpGet("Configuration/{Key}")] |         [HttpGet("Configuration/{key}")] | ||||||
|         [ProducesResponseType(StatusCodes.Status200OK)] |         [ProducesResponseType(StatusCodes.Status200OK)] | ||||||
|         public ActionResult<object> GetNamedConfiguration([FromRoute] string key) |         public ActionResult<object> GetNamedConfiguration([FromRoute] string key) | ||||||
|         { |         { | ||||||
| @ -81,17 +79,17 @@ namespace Jellyfin.Api.Controllers | |||||||
|         /// Updates named configuration. |         /// Updates named configuration. | ||||||
|         /// </summary> |         /// </summary> | ||||||
|         /// <param name="key">Configuration key.</param> |         /// <param name="key">Configuration key.</param> | ||||||
|         /// <response code="200">Named configuration updated.</response> |         /// <response code="204">Named configuration updated.</response> | ||||||
|         /// <returns>Update status.</returns> |         /// <returns>Update status.</returns> | ||||||
|         [HttpPost("Configuration/{Key}")] |         [HttpPost("Configuration/{key}")] | ||||||
|         [Authorize(Policy = Policies.RequiresElevation)] |         [Authorize(Policy = Policies.RequiresElevation)] | ||||||
|         [ProducesResponseType(StatusCodes.Status200OK)] |         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||||
|         public async Task<ActionResult> UpdateNamedConfiguration([FromRoute] string key) |         public async Task<ActionResult> UpdateNamedConfiguration([FromRoute] string key) | ||||||
|         { |         { | ||||||
|             var configurationType = _configurationManager.GetConfigurationType(key); |             var configurationType = _configurationManager.GetConfigurationType(key); | ||||||
|             var configuration = await JsonSerializer.DeserializeAsync(Request.Body, configurationType).ConfigureAwait(false); |             var configuration = await JsonSerializer.DeserializeAsync(Request.Body, configurationType).ConfigureAwait(false); | ||||||
|             _configurationManager.SaveConfiguration(key, configuration); |             _configurationManager.SaveConfiguration(key, configuration); | ||||||
|             return Ok(); |             return NoContent(); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         /// <summary> |         /// <summary> | ||||||
| @ -111,15 +109,15 @@ namespace Jellyfin.Api.Controllers | |||||||
|         /// Updates the path to the media encoder. |         /// Updates the path to the media encoder. | ||||||
|         /// </summary> |         /// </summary> | ||||||
|         /// <param name="mediaEncoderPath">Media encoder path form body.</param> |         /// <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> |         /// <returns>Status.</returns> | ||||||
|         [HttpPost("MediaEncoder/Path")] |         [HttpPost("MediaEncoder/Path")] | ||||||
|         [Authorize(Policy = Policies.FirstTimeSetupOrElevated)] |         [Authorize(Policy = Policies.FirstTimeSetupOrElevated)] | ||||||
|         [ProducesResponseType(StatusCodes.Status200OK)] |         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||||
|         public ActionResult UpdateMediaEncoderPath([FromForm, BindRequired] MediaEncoderPathDto mediaEncoderPath) |         public ActionResult UpdateMediaEncoderPath([FromForm, BindRequired] MediaEncoderPathDto mediaEncoderPath) | ||||||
|         { |         { | ||||||
|             _mediaEncoder.UpdateEncoderPath(mediaEncoderPath.Path, mediaEncoderPath.PathType); |             _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 System; | ||||||
| using Jellyfin.Api.Constants; | using Jellyfin.Api.Constants; | ||||||
| using MediaBrowser.Controller.Devices; | using MediaBrowser.Controller.Devices; | ||||||
| @ -17,7 +15,7 @@ namespace Jellyfin.Api.Controllers | |||||||
|     /// <summary> |     /// <summary> | ||||||
|     /// Devices Controller. |     /// Devices Controller. | ||||||
|     /// </summary> |     /// </summary> | ||||||
|     [Authorize] |     [Authorize(Policy = Policies.DefaultAuthorization)] | ||||||
|     public class DevicesController : BaseJellyfinApiController |     public class DevicesController : BaseJellyfinApiController | ||||||
|     { |     { | ||||||
|         private readonly IDeviceManager _deviceManager; |         private readonly IDeviceManager _deviceManager; | ||||||
| @ -105,12 +103,12 @@ namespace Jellyfin.Api.Controllers | |||||||
|         /// </summary> |         /// </summary> | ||||||
|         /// <param name="id">Device Id.</param> |         /// <param name="id">Device Id.</param> | ||||||
|         /// <param name="deviceOptions">Device Options.</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> |         /// <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")] |         [HttpPost("Options")] | ||||||
|         [Authorize(Policy = Policies.RequiresElevation)] |         [Authorize(Policy = Policies.RequiresElevation)] | ||||||
|         [ProducesResponseType(StatusCodes.Status200OK)] |         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||||
|         [ProducesResponseType(StatusCodes.Status404NotFound)] |         [ProducesResponseType(StatusCodes.Status404NotFound)] | ||||||
|         public ActionResult UpdateDeviceOptions( |         public ActionResult UpdateDeviceOptions( | ||||||
|             [FromQuery, BindRequired] string id, |             [FromQuery, BindRequired] string id, | ||||||
| @ -123,18 +121,19 @@ namespace Jellyfin.Api.Controllers | |||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             _deviceManager.UpdateDeviceOptions(id, deviceOptions); |             _deviceManager.UpdateDeviceOptions(id, deviceOptions); | ||||||
|             return Ok(); |             return NoContent(); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         /// <summary> |         /// <summary> | ||||||
|         /// Deletes a device. |         /// Deletes a device. | ||||||
|         /// </summary> |         /// </summary> | ||||||
|         /// <param name="id">Device Id.</param> |         /// <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> |         /// <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] |         [HttpDelete] | ||||||
|         [ProducesResponseType(StatusCodes.Status200OK)] |         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||||
|  |         [ProducesResponseType(StatusCodes.Status404NotFound)] | ||||||
|         public ActionResult DeleteDevice([FromQuery, BindRequired] string id) |         public ActionResult DeleteDevice([FromQuery, BindRequired] string id) | ||||||
|         { |         { | ||||||
|             var existingDevice = _deviceManager.GetDevice(id); |             var existingDevice = _deviceManager.GetDevice(id); | ||||||
| @ -150,7 +149,7 @@ namespace Jellyfin.Api.Controllers | |||||||
|                 _sessionManager.Logout(session); |                 _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.Collections.Generic; | ||||||
| using System.Linq; | using System.Linq; | ||||||
| using System.Threading; | using System.Threading; | ||||||
|  | using Jellyfin.Api.Constants; | ||||||
| using MediaBrowser.Controller.Configuration; | using MediaBrowser.Controller.Configuration; | ||||||
| using MediaBrowser.Controller.Entities; | using MediaBrowser.Controller.Entities; | ||||||
| using MediaBrowser.Controller.Entities.Audio; | using MediaBrowser.Controller.Entities.Audio; | ||||||
| using MediaBrowser.Controller.Entities.TV; | using MediaBrowser.Controller.Entities.TV; | ||||||
| using MediaBrowser.Controller.Library; | using MediaBrowser.Controller.Library; | ||||||
| using MediaBrowser.Controller.LiveTv; | using MediaBrowser.Controller.LiveTv; | ||||||
| using MediaBrowser.Controller.Net; |  | ||||||
| using MediaBrowser.Controller.Providers; | using MediaBrowser.Controller.Providers; | ||||||
| using MediaBrowser.Model.Dto; | using MediaBrowser.Model.Dto; | ||||||
| using MediaBrowser.Model.Entities; | using MediaBrowser.Model.Entities; | ||||||
| using MediaBrowser.Model.Globalization; | using MediaBrowser.Model.Globalization; | ||||||
| using MediaBrowser.Model.IO; | using MediaBrowser.Model.IO; | ||||||
| using MediaBrowser.Model.Services; | using Microsoft.AspNetCore.Authorization; | ||||||
| using Microsoft.Extensions.Logging; | 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")] |     /// <summary> | ||||||
|     public class UpdateItem : BaseItemDto, IReturnVoid |     /// Item update controller. | ||||||
|     { |     /// </summary> | ||||||
|         [ApiMember(Name = "ItemId", Description = "The id of the item", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] |     [Authorize(Policy = Policies.RequiresElevation)] | ||||||
|         public string ItemId { get; set; } |     public class ItemUpdateController : BaseJellyfinApiController | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     [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 |  | ||||||
|     { |     { | ||||||
|         private readonly ILibraryManager _libraryManager; |         private readonly ILibraryManager _libraryManager; | ||||||
|         private readonly IProviderManager _providerManager; |         private readonly IProviderManager _providerManager; | ||||||
|         private readonly ILocalizationManager _localizationManager; |         private readonly ILocalizationManager _localizationManager; | ||||||
|         private readonly IFileSystem _fileSystem; |         private readonly IFileSystem _fileSystem; | ||||||
|  |         private readonly IServerConfigurationManager _serverConfigurationManager; | ||||||
| 
 | 
 | ||||||
|         public ItemUpdateService( |         /// <summary> | ||||||
|             ILogger<ItemUpdateService> logger, |         /// Initializes a new instance of the <see cref="ItemUpdateController"/> class. | ||||||
|             IServerConfigurationManager serverConfigurationManager, |         /// </summary> | ||||||
|             IHttpResultFactory httpResultFactory, |         /// <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, |             IFileSystem fileSystem, | ||||||
|             ILibraryManager libraryManager, |             ILibraryManager libraryManager, | ||||||
|             IProviderManager providerManager, |             IProviderManager providerManager, | ||||||
|             ILocalizationManager localizationManager) |             ILocalizationManager localizationManager, | ||||||
|             : base(logger, serverConfigurationManager, httpResultFactory) |             IServerConfigurationManager serverConfigurationManager) | ||||||
|         { |         { | ||||||
|             _libraryManager = libraryManager; |             _libraryManager = libraryManager; | ||||||
|             _providerManager = providerManager; |             _providerManager = providerManager; | ||||||
|             _localizationManager = localizationManager; |             _localizationManager = localizationManager; | ||||||
|             _fileSystem = fileSystem; |             _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 item = _libraryManager.GetItemById(itemId); | ||||||
| 
 |             if (item == null) | ||||||
|             var info = new MetadataEditorInfo |  | ||||||
|             { |             { | ||||||
|                 ParentalRatingOptions = _localizationManager.GetParentalRatings().ToArray(), |                 return NotFound(); | ||||||
|                 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 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 newLockData = request.LockData ?? false; | ||||||
|             var isLockedChanged = item.IsLocked != newLockData; |             var isLockedChanged = item.IsLocked != newLockData; | ||||||
| 
 | 
 | ||||||
|             var series = item as Series; |             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. |             // Do this first so that metadata savers can pull the updates from the database. | ||||||
|             if (request.People != null) |             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); |             UpdateItem(request, item); | ||||||
| @ -232,7 +116,7 @@ namespace MediaBrowser.Api | |||||||
|             if (displayOrderChanged) |             if (displayOrderChanged) | ||||||
|             { |             { | ||||||
|                 _providerManager.QueueRefresh( |                 _providerManager.QueueRefresh( | ||||||
|                     series.Id, |                     series!.Id, | ||||||
|                     new MetadataRefreshOptions(new DirectoryService(_fileSystem)) |                     new MetadataRefreshOptions(new DirectoryService(_fileSystem)) | ||||||
|                     { |                     { | ||||||
|                         MetadataRefreshMode = MetadataRefreshMode.FullRefresh, |                         MetadataRefreshMode = MetadataRefreshMode.FullRefresh, | ||||||
| @ -241,11 +125,101 @@ namespace MediaBrowser.Api | |||||||
|                     }, |                     }, | ||||||
|                     RefreshPriority.High); |                     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) |         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; |                 case Audio song: | ||||||
|             } |                     song.Album = request.Album; | ||||||
| 
 |                     break; | ||||||
|             if (item is MusicVideo musicVideo) |                 case MusicVideo musicVideo: | ||||||
|             { |                     musicVideo.Album = request.Album; | ||||||
|                 musicVideo.Album = request.Album; |                     break; | ||||||
|             } |                 case Series series: | ||||||
| 
 |  | ||||||
|             if (item is Series series) |  | ||||||
|             { |  | ||||||
|                 series.Status = GetSeriesStatus(request); |  | ||||||
| 
 |  | ||||||
|                 if (request.AirDays != null) |  | ||||||
|                 { |                 { | ||||||
|                     series.AirDays = request.AirDays; |                     series.Status = GetSeriesStatus(request); | ||||||
|                     series.AirTime = request.AirTime; | 
 | ||||||
|  |                     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); |             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; | ||||||
| using System.Collections.Generic; | using System.Collections.Generic; | ||||||
|  | using System.Diagnostics.CodeAnalysis; | ||||||
| using System.Linq; | using System.Linq; | ||||||
| using System.Threading; | using System.Threading; | ||||||
| using Jellyfin.Api.Models.NotificationDtos; | using Jellyfin.Api.Models.NotificationDtos; | ||||||
|  | using Jellyfin.Data.Enums; | ||||||
| using MediaBrowser.Controller.Library; | using MediaBrowser.Controller.Library; | ||||||
| using MediaBrowser.Controller.Notifications; | using MediaBrowser.Controller.Notifications; | ||||||
| using MediaBrowser.Model.Dto; | 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> |         /// <param name="limit">An optional limit on the number of notifications returned.</param> | ||||||
|         /// <response code="200">Notifications returned.</response> |         /// <response code="200">Notifications returned.</response> | ||||||
|         /// <returns>An <see cref="OkResult"/> containing a list of notifications.</returns> |         /// <returns>An <see cref="OkResult"/> containing a list of notifications.</returns> | ||||||
|         [HttpGet("{UserID}")] |         [HttpGet("{userId}")] | ||||||
|         [ProducesResponseType(StatusCodes.Status200OK)] |         [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( |         public ActionResult<NotificationResultDto> GetNotifications( | ||||||
|             [FromRoute] string userId, |             [FromRoute] string userId, | ||||||
|             [FromQuery] bool? isRead, |             [FromQuery] bool? isRead, | ||||||
| @ -60,8 +63,9 @@ namespace Jellyfin.Api.Controllers | |||||||
|         /// <param name="userId">The user's ID.</param> |         /// <param name="userId">The user's ID.</param> | ||||||
|         /// <response code="200">Summary of user's notifications returned.</response> |         /// <response code="200">Summary of user's notifications returned.</response> | ||||||
|         /// <returns>An <cref see="OkResult"/> containing a summary of the users notifications.</returns> |         /// <returns>An <cref see="OkResult"/> containing a summary of the users notifications.</returns> | ||||||
|         [HttpGet("{UserID}/Summary")] |         [HttpGet("{userId}/Summary")] | ||||||
|         [ProducesResponseType(StatusCodes.Status200OK)] |         [ProducesResponseType(StatusCodes.Status200OK)] | ||||||
|  |         [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Imported from ServiceStack")] | ||||||
|         public ActionResult<NotificationsSummaryDto> GetNotificationsSummary( |         public ActionResult<NotificationsSummaryDto> GetNotificationsSummary( | ||||||
|             [FromRoute] string userId) |             [FromRoute] string userId) | ||||||
|         { |         { | ||||||
| @ -99,10 +103,10 @@ namespace Jellyfin.Api.Controllers | |||||||
|         /// <param name="description">The description of the notification.</param> |         /// <param name="description">The description of the notification.</param> | ||||||
|         /// <param name="url">The URL of the notification.</param> |         /// <param name="url">The URL of the notification.</param> | ||||||
|         /// <param name="level">The level of the notification.</param> |         /// <param name="level">The level of the notification.</param> | ||||||
|         /// <response code="200">Notification sent.</response> |         /// <response code="204">Notification sent.</response> | ||||||
|         /// <returns>An <cref see="OkResult"/>.</returns> |         /// <returns>A <cref see="NoContentResult"/>.</returns> | ||||||
|         [HttpPost("Admin")] |         [HttpPost("Admin")] | ||||||
|         [ProducesResponseType(StatusCodes.Status200OK)] |         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||||
|         public ActionResult CreateAdminNotification( |         public ActionResult CreateAdminNotification( | ||||||
|             [FromQuery] string name, |             [FromQuery] string name, | ||||||
|             [FromQuery] string description, |             [FromQuery] string description, | ||||||
| @ -115,13 +119,16 @@ namespace Jellyfin.Api.Controllers | |||||||
|                 Description = description, |                 Description = description, | ||||||
|                 Url = url, |                 Url = url, | ||||||
|                 Level = level ?? NotificationLevel.Normal, |                 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, |                 Date = DateTime.UtcNow, | ||||||
|             }; |             }; | ||||||
| 
 | 
 | ||||||
|             _notificationManager.SendNotification(notification, CancellationToken.None); |             _notificationManager.SendNotification(notification, CancellationToken.None); | ||||||
| 
 | 
 | ||||||
|             return Ok(); |             return NoContent(); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         /// <summary> |         /// <summary> | ||||||
| @ -129,15 +136,17 @@ namespace Jellyfin.Api.Controllers | |||||||
|         /// </summary> |         /// </summary> | ||||||
|         /// <param name="userId">The userID.</param> |         /// <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> |         /// <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> |         /// <response code="204">Notifications set as read.</response> | ||||||
|         /// <returns>An <cref see="OkResult"/>.</returns> |         /// <returns>A <cref see="NoContentResult"/>.</returns> | ||||||
|         [HttpPost("{UserID}/Read")] |         [HttpPost("{userId}/Read")] | ||||||
|         [ProducesResponseType(StatusCodes.Status200OK)] |         [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( |         public ActionResult SetRead( | ||||||
|             [FromRoute] string userId, |             [FromRoute] string userId, | ||||||
|             [FromQuery] string ids) |             [FromQuery] string ids) | ||||||
|         { |         { | ||||||
|             return Ok(); |             return NoContent(); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         /// <summary> |         /// <summary> | ||||||
| @ -145,15 +154,17 @@ namespace Jellyfin.Api.Controllers | |||||||
|         /// </summary> |         /// </summary> | ||||||
|         /// <param name="userId">The userID.</param> |         /// <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> |         /// <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> |         /// <response code="204">Notifications set as unread.</response> | ||||||
|         /// <returns>An <cref see="OkResult"/>.</returns> |         /// <returns>A <cref see="NoContentResult"/>.</returns> | ||||||
|         [HttpPost("{UserID}/Unread")] |         [HttpPost("{userId}/Unread")] | ||||||
|         [ProducesResponseType(StatusCodes.Status200OK)] |         [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( |         public ActionResult SetUnread( | ||||||
|             [FromRoute] string userId, |             [FromRoute] string userId, | ||||||
|             [FromQuery] string ids) |             [FromQuery] string ids) | ||||||
|         { |         { | ||||||
|             return Ok(); |             return NoContent(); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,5 +1,3 @@ | |||||||
| #nullable enable |  | ||||||
| 
 |  | ||||||
| using System; | using System; | ||||||
| using System.Collections.Generic; | using System.Collections.Generic; | ||||||
| using System.ComponentModel.DataAnnotations; | using System.ComponentModel.DataAnnotations; | ||||||
| @ -18,7 +16,7 @@ namespace Jellyfin.Api.Controllers | |||||||
|     /// Package Controller. |     /// Package Controller. | ||||||
|     /// </summary> |     /// </summary> | ||||||
|     [Route("Packages")] |     [Route("Packages")] | ||||||
|     [Authorize] |     [Authorize(Policy = Policies.DefaultAuthorization)] | ||||||
|     public class PackageController : BaseJellyfinApiController |     public class PackageController : BaseJellyfinApiController | ||||||
|     { |     { | ||||||
|         private readonly IInstallationManager _installationManager; |         private readonly IInstallationManager _installationManager; | ||||||
| @ -37,9 +35,10 @@ namespace Jellyfin.Api.Controllers | |||||||
|         /// </summary> |         /// </summary> | ||||||
|         /// <param name="name">The name of the package.</param> |         /// <param name="name">The name of the package.</param> | ||||||
|         /// <param name="assemblyGuid">The GUID of the associated assembly.</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> |         /// <returns>A <see cref="PackageInfo"/> containing package information.</returns> | ||||||
|         [HttpGet("/{Name}")] |         [HttpGet("/{name}")] | ||||||
|         [ProducesResponseType(typeof(PackageInfo), StatusCodes.Status200OK)] |         [ProducesResponseType(StatusCodes.Status200OK)] | ||||||
|         public async Task<ActionResult<PackageInfo>> GetPackageInfo( |         public async Task<ActionResult<PackageInfo>> GetPackageInfo( | ||||||
|             [FromRoute] [Required] string name, |             [FromRoute] [Required] string name, | ||||||
|             [FromQuery] string? assemblyGuid) |             [FromQuery] string? assemblyGuid) | ||||||
| @ -56,9 +55,10 @@ namespace Jellyfin.Api.Controllers | |||||||
|         /// <summary> |         /// <summary> | ||||||
|         /// Gets available packages. |         /// Gets available packages. | ||||||
|         /// </summary> |         /// </summary> | ||||||
|  |         /// <response code="200">Available packages returned.</response> | ||||||
|         /// <returns>An <see cref="PackageInfo"/> containing available packages information.</returns> |         /// <returns>An <see cref="PackageInfo"/> containing available packages information.</returns> | ||||||
|         [HttpGet] |         [HttpGet] | ||||||
|         [ProducesResponseType(typeof(PackageInfo[]), StatusCodes.Status200OK)] |         [ProducesResponseType(StatusCodes.Status200OK)] | ||||||
|         public async Task<IEnumerable<PackageInfo>> GetPackages() |         public async Task<IEnumerable<PackageInfo>> GetPackages() | ||||||
|         { |         { | ||||||
|             IEnumerable<PackageInfo> packages = await _installationManager.GetAvailablePackages().ConfigureAwait(false); |             IEnumerable<PackageInfo> packages = await _installationManager.GetAvailablePackages().ConfigureAwait(false); | ||||||
| @ -72,11 +72,11 @@ namespace Jellyfin.Api.Controllers | |||||||
|         /// <param name="name">Package name.</param> |         /// <param name="name">Package name.</param> | ||||||
|         /// <param name="assemblyGuid">GUID of the associated assembly.</param> |         /// <param name="assemblyGuid">GUID of the associated assembly.</param> | ||||||
|         /// <param name="version">Optional version. Defaults to latest version.</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> |         /// <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> |         /// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the package could not be found.</returns> | ||||||
|         [HttpPost("/Installed/{Name}")] |         [HttpPost("/Installed/{name}")] | ||||||
|         [ProducesResponseType(StatusCodes.Status200OK)] |         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||||
|         [ProducesResponseType(StatusCodes.Status404NotFound)] |         [ProducesResponseType(StatusCodes.Status404NotFound)] | ||||||
|         [Authorize(Policy = Policies.RequiresElevation)] |         [Authorize(Policy = Policies.RequiresElevation)] | ||||||
|         public async Task<ActionResult> InstallPackage( |         public async Task<ActionResult> InstallPackage( | ||||||
| @ -98,23 +98,23 @@ namespace Jellyfin.Api.Controllers | |||||||
| 
 | 
 | ||||||
|             await _installationManager.InstallPackage(package).ConfigureAwait(false); |             await _installationManager.InstallPackage(package).ConfigureAwait(false); | ||||||
| 
 | 
 | ||||||
|             return Ok(); |             return NoContent(); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         /// <summary> |         /// <summary> | ||||||
|         /// Cancels a package installation. |         /// Cancels a package installation. | ||||||
|         /// </summary> |         /// </summary> | ||||||
|         /// <param name="id">Installation Id.</param> |         /// <param name="packageId">Installation Id.</param> | ||||||
|         /// <response code="200">Installation cancelled.</response> |         /// <response code="204">Installation cancelled.</response> | ||||||
|         /// <returns>An <see cref="OkResult"/> on successfully cancelling a package installation.</returns> |         /// <returns>A <see cref="NoContentResult"/> on successfully cancelling a package installation.</returns> | ||||||
|         [HttpDelete("/Installing/{id}")] |         [HttpDelete("/Installing/{packageId}")] | ||||||
|         [Authorize(Policy = Policies.RequiresElevation)] |         [Authorize(Policy = Policies.RequiresElevation)] | ||||||
|  |         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||||
|         public IActionResult CancelPackageInstallation( |         public IActionResult CancelPackageInstallation( | ||||||
|             [FromRoute] [Required] string id) |             [FromRoute] [Required] Guid packageId) | ||||||
|         { |         { | ||||||
|             _installationManager.CancelInstallation(new Guid(id)); |             _installationManager.CancelInstallation(packageId); | ||||||
| 
 |             return NoContent(); | ||||||
|             return Ok(); |  | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | |||||||
							
								
								
									
										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.ComponentModel.DataAnnotations; | ||||||
| using System.Globalization; | using System.Globalization; | ||||||
| using System.Linq; | using System.Linq; | ||||||
|  | using Jellyfin.Api.Constants; | ||||||
| using Jellyfin.Api.Helpers; | using Jellyfin.Api.Helpers; | ||||||
| using MediaBrowser.Controller.Drawing; | using MediaBrowser.Controller.Drawing; | ||||||
| using MediaBrowser.Controller.Dto; | using MediaBrowser.Controller.Dto; | ||||||
| @ -23,7 +24,7 @@ namespace Jellyfin.Api.Controllers | |||||||
|     /// Search controller. |     /// Search controller. | ||||||
|     /// </summary> |     /// </summary> | ||||||
|     [Route("/Search/Hints")] |     [Route("/Search/Hints")] | ||||||
|     [Authorize] |     [Authorize(Policy = Policies.DefaultAuthorization)] | ||||||
|     public class SearchController : BaseJellyfinApiController |     public class SearchController : BaseJellyfinApiController | ||||||
|     { |     { | ||||||
|         private readonly ISearchEngine _searchEngine; |         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> |         /// <summary> | ||||||
|         /// Completes the startup wizard. |         /// Completes the startup wizard. | ||||||
|         /// </summary> |         /// </summary> | ||||||
|         /// <response code="200">Startup wizard completed.</response> |         /// <response code="204">Startup wizard completed.</response> | ||||||
|         /// <returns>An <see cref="OkResult"/> indicating success.</returns> |         /// <returns>A <see cref="NoContentResult"/> indicating success.</returns> | ||||||
|         [HttpPost("Complete")] |         [HttpPost("Complete")] | ||||||
|         [ProducesResponseType(StatusCodes.Status200OK)] |         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||||
|         public ActionResult CompleteWizard() |         public ActionResult CompleteWizard() | ||||||
|         { |         { | ||||||
|             _config.Configuration.IsStartupWizardCompleted = true; |             _config.Configuration.IsStartupWizardCompleted = true; | ||||||
|             _config.SetOptimalValues(); |             _config.SetOptimalValues(); | ||||||
|             _config.SaveConfiguration(); |             _config.SaveConfiguration(); | ||||||
|             return Ok(); |             return NoContent(); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         /// <summary> |         /// <summary> | ||||||
| @ -70,10 +70,10 @@ namespace Jellyfin.Api.Controllers | |||||||
|         /// <param name="uiCulture">The UI language culture.</param> |         /// <param name="uiCulture">The UI language culture.</param> | ||||||
|         /// <param name="metadataCountryCode">The metadata country code.</param> |         /// <param name="metadataCountryCode">The metadata country code.</param> | ||||||
|         /// <param name="preferredMetadataLanguage">The preferred language for metadata.</param> |         /// <param name="preferredMetadataLanguage">The preferred language for metadata.</param> | ||||||
|         /// <response code="200">Configuration saved.</response> |         /// <response code="204">Configuration saved.</response> | ||||||
|         /// <returns>An <see cref="OkResult"/> indicating success.</returns> |         /// <returns>A <see cref="NoContentResult"/> indicating success.</returns> | ||||||
|         [HttpPost("Configuration")] |         [HttpPost("Configuration")] | ||||||
|         [ProducesResponseType(StatusCodes.Status200OK)] |         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||||
|         public ActionResult UpdateInitialConfiguration( |         public ActionResult UpdateInitialConfiguration( | ||||||
|             [FromForm] string uiCulture, |             [FromForm] string uiCulture, | ||||||
|             [FromForm] string metadataCountryCode, |             [FromForm] string metadataCountryCode, | ||||||
| @ -83,7 +83,7 @@ namespace Jellyfin.Api.Controllers | |||||||
|             _config.Configuration.MetadataCountryCode = metadataCountryCode; |             _config.Configuration.MetadataCountryCode = metadataCountryCode; | ||||||
|             _config.Configuration.PreferredMetadataLanguage = preferredMetadataLanguage; |             _config.Configuration.PreferredMetadataLanguage = preferredMetadataLanguage; | ||||||
|             _config.SaveConfiguration(); |             _config.SaveConfiguration(); | ||||||
|             return Ok(); |             return NoContent(); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         /// <summary> |         /// <summary> | ||||||
| @ -91,16 +91,16 @@ namespace Jellyfin.Api.Controllers | |||||||
|         /// </summary> |         /// </summary> | ||||||
|         /// <param name="enableRemoteAccess">Enable remote access.</param> |         /// <param name="enableRemoteAccess">Enable remote access.</param> | ||||||
|         /// <param name="enableAutomaticPortMapping">Enable UPnP.</param> |         /// <param name="enableAutomaticPortMapping">Enable UPnP.</param> | ||||||
|         /// <response code="200">Configuration saved.</response> |         /// <response code="204">Configuration saved.</response> | ||||||
|         /// <returns>An <see cref="OkResult"/> indicating success.</returns> |         /// <returns>A <see cref="NoContentResult"/> indicating success.</returns> | ||||||
|         [HttpPost("RemoteAccess")] |         [HttpPost("RemoteAccess")] | ||||||
|         [ProducesResponseType(StatusCodes.Status200OK)] |         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||||
|         public ActionResult SetRemoteAccess([FromForm] bool enableRemoteAccess, [FromForm] bool enableAutomaticPortMapping) |         public ActionResult SetRemoteAccess([FromForm] bool enableRemoteAccess, [FromForm] bool enableAutomaticPortMapping) | ||||||
|         { |         { | ||||||
|             _config.Configuration.EnableRemoteAccess = enableRemoteAccess; |             _config.Configuration.EnableRemoteAccess = enableRemoteAccess; | ||||||
|             _config.Configuration.EnableUPnP = enableAutomaticPortMapping; |             _config.Configuration.EnableUPnP = enableAutomaticPortMapping; | ||||||
|             _config.SaveConfiguration(); |             _config.SaveConfiguration(); | ||||||
|             return Ok(); |             return NoContent(); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         /// <summary> |         /// <summary> | ||||||
| @ -113,35 +113,41 @@ namespace Jellyfin.Api.Controllers | |||||||
|         [ProducesResponseType(StatusCodes.Status200OK)] |         [ProducesResponseType(StatusCodes.Status200OK)] | ||||||
|         public ActionResult<StartupUserDto> GetFirstUser() |         public ActionResult<StartupUserDto> GetFirstUser() | ||||||
|         { |         { | ||||||
|  |             // TODO: Remove this method when startup wizard no longer requires an existing user. | ||||||
|  |             _userManager.Initialize(); | ||||||
|             var user = _userManager.Users.First(); |             var user = _userManager.Users.First(); | ||||||
|             return new StartupUserDto { Name = user.Name, Password = user.Password }; |             return new StartupUserDto | ||||||
|  |             { | ||||||
|  |                 Name = user.Username, | ||||||
|  |                 Password = user.Password | ||||||
|  |             }; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         /// <summary> |         /// <summary> | ||||||
|         /// Sets the user name and password. |         /// Sets the user name and password. | ||||||
|         /// </summary> |         /// </summary> | ||||||
|         /// <param name="startupUserDto">The DTO containing username and password.</param> |         /// <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> |         /// <returns> | ||||||
|         /// A <see cref="Task" /> that represents the asynchronous update operation. |         /// 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> |         /// </returns> | ||||||
|         [HttpPost("User")] |         [HttpPost("User")] | ||||||
|         [ProducesResponseType(StatusCodes.Status200OK)] |         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||||
|         public async Task<ActionResult> UpdateUser([FromForm] StartupUserDto startupUserDto) |         public async Task<ActionResult> UpdateUser([FromForm] StartupUserDto startupUserDto) | ||||||
|         { |         { | ||||||
|             var user = _userManager.Users.First(); |             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)) |             if (!string.IsNullOrEmpty(startupUserDto.Password)) | ||||||
|             { |             { | ||||||
|                 await _userManager.ChangePassword(user, startupUserDto.Password).ConfigureAwait(false); |                 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; | ||||||
| using System.Net.Mime; | using System.Net.Mime; | ||||||
| using System.Threading; | using System.Threading; | ||||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||||
|  | using Jellyfin.Api.Constants; | ||||||
| using MediaBrowser.Common.Extensions; | using MediaBrowser.Common.Extensions; | ||||||
| using MediaBrowser.Controller.Library; | using MediaBrowser.Controller.Library; | ||||||
| using MediaBrowser.Controller.MediaEncoding; | using MediaBrowser.Controller.MediaEncoding; | ||||||
| @ -17,7 +16,7 @@ namespace Jellyfin.Api.Controllers | |||||||
|     /// Attachments controller. |     /// Attachments controller. | ||||||
|     /// </summary> |     /// </summary> | ||||||
|     [Route("Videos")] |     [Route("Videos")] | ||||||
|     [Authorize] |     [Authorize(Policy = Policies.DefaultAuthorization)] | ||||||
|     public class VideoAttachmentsController : BaseJellyfinApiController |     public class VideoAttachmentsController : BaseJellyfinApiController | ||||||
|     { |     { | ||||||
|         private readonly ILibraryManager _libraryManager; |         private readonly ILibraryManager _libraryManager; | ||||||
| @ -45,7 +44,7 @@ namespace Jellyfin.Api.Controllers | |||||||
|         /// <response code="200">Attachment retrieved.</response> |         /// <response code="200">Attachment retrieved.</response> | ||||||
|         /// <response code="404">Video or attachment not found.</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> |         /// <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)] |         [Produces(MediaTypeNames.Application.Octet)] | ||||||
|         [ProducesResponseType(StatusCodes.Status200OK)] |         [ProducesResponseType(StatusCodes.Status200OK)] | ||||||
|         [ProducesResponseType(StatusCodes.Status404NotFound)] |         [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; | ||||||
| using System.Collections.Generic; | using System.Collections.Generic; | ||||||
| using System.Linq; | using System.Linq; | ||||||
|  | using Jellyfin.Data.Enums; | ||||||
| using MediaBrowser.Model.Entities; | using MediaBrowser.Model.Entities; | ||||||
| using MediaBrowser.Model.Querying; | using MediaBrowser.Model.Querying; | ||||||
|  | using MediaBrowser.Controller.Net; | ||||||
|  | using MediaBrowser.Controller.Session; | ||||||
|  | using Microsoft.AspNetCore.Http; | ||||||
| 
 | 
 | ||||||
| namespace Jellyfin.Api.Helpers | namespace Jellyfin.Api.Helpers | ||||||
| { | { | ||||||
| @ -104,5 +108,66 @@ namespace Jellyfin.Api.Helpers | |||||||
|                 ? value.Split(new[] { separator }, StringSplitOptions.RemoveEmptyEntries) |                 ? value.Split(new[] { separator }, StringSplitOptions.RemoveEmptyEntries) | ||||||
|                 : value.Split(separator); |                 : 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 | namespace Jellyfin.Api.Models.ConfigurationDtos | ||||||
| { | { | ||||||
|     /// <summary> |     /// <summary> | ||||||
|  | |||||||
| @ -1,13 +1,18 @@ | |||||||
| #pragma warning disable CS1591 | using MediaBrowser.Common.Plugins; | ||||||
| 
 |  | ||||||
| using MediaBrowser.Common.Plugins; |  | ||||||
| using MediaBrowser.Controller.Plugins; | using MediaBrowser.Controller.Plugins; | ||||||
| using MediaBrowser.Model.Plugins; | using MediaBrowser.Model.Plugins; | ||||||
| 
 | 
 | ||||||
| namespace MediaBrowser.WebDashboard.Api | namespace Jellyfin.Api.Models | ||||||
| { | { | ||||||
|  |     /// <summary> | ||||||
|  |     /// The configuration page info. | ||||||
|  |     /// </summary> | ||||||
|     public class ConfigurationPageInfo |     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) |         public ConfigurationPageInfo(IPluginConfigurationPage page) | ||||||
|         { |         { | ||||||
|             Name = page.Name; |             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) |         public ConfigurationPageInfo(IPlugin plugin, PluginPageInfo page) | ||||||
|         { |         { | ||||||
|             Name = page.Name; |             Name = page.Name; | ||||||
| @ -40,13 +50,25 @@ namespace MediaBrowser.WebDashboard.Api | |||||||
|         /// <value>The name.</value> |         /// <value>The name.</value> | ||||||
|         public string Name { get; set; } |         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 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> |         /// <summary> | ||||||
|         /// Gets or sets the type of the configuration page. |         /// Gets or sets the type of the configuration page. | ||||||
| @ -58,6 +80,6 @@ namespace MediaBrowser.WebDashboard.Api | |||||||
|         /// Gets or sets the plugin id. |         /// Gets or sets the plugin id. | ||||||
|         /// </summary> |         /// </summary> | ||||||
|         /// <value>The plugin id.</value> |         /// <value>The plugin id.</value> | ||||||
|         public string PluginId { get; set; } |         public string? PluginId { get; set; } | ||||||
|     } |     } | ||||||
| } | } | ||||||
| @ -1,5 +1,3 @@ | |||||||
| #nullable enable |  | ||||||
| 
 |  | ||||||
| using System; | using System; | ||||||
| using MediaBrowser.Model.Notifications; | using MediaBrowser.Model.Notifications; | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -1,5 +1,3 @@ | |||||||
| #nullable enable |  | ||||||
| 
 |  | ||||||
| using System; | using System; | ||||||
| using System.Collections.Generic; | using System.Collections.Generic; | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -1,5 +1,3 @@ | |||||||
| #nullable enable |  | ||||||
| 
 |  | ||||||
| using MediaBrowser.Model.Notifications; | using MediaBrowser.Model.Notifications; | ||||||
| 
 | 
 | ||||||
| namespace Jellyfin.Api.Models.NotificationDtos | 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 | namespace Jellyfin.Api.Models.StartupDtos | ||||||
| { | { | ||||||
|     /// <summary> |     /// <summary> | ||||||
| @ -10,16 +8,16 @@ namespace Jellyfin.Api.Models.StartupDtos | |||||||
|         /// <summary> |         /// <summary> | ||||||
|         /// Gets or sets UI language culture. |         /// Gets or sets UI language culture. | ||||||
|         /// </summary> |         /// </summary> | ||||||
|         public string UICulture { get; set; } |         public string? UICulture { get; set; } | ||||||
| 
 | 
 | ||||||
|         /// <summary> |         /// <summary> | ||||||
|         /// Gets or sets the metadata country code. |         /// Gets or sets the metadata country code. | ||||||
|         /// </summary> |         /// </summary> | ||||||
|         public string MetadataCountryCode { get; set; } |         public string? MetadataCountryCode { get; set; } | ||||||
| 
 | 
 | ||||||
|         /// <summary> |         /// <summary> | ||||||
|         /// Gets or sets the preferred language for the metadata. |         /// Gets or sets the preferred language for the metadata. | ||||||
|         /// </summary> |         /// </summary> | ||||||
|         public string PreferredMetadataLanguage { get; set; } |         public string? PreferredMetadataLanguage { get; set; } | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,5 +1,3 @@ | |||||||
| #nullable disable |  | ||||||
| 
 |  | ||||||
| namespace Jellyfin.Api.Models.StartupDtos | namespace Jellyfin.Api.Models.StartupDtos | ||||||
| { | { | ||||||
|     /// <summary> |     /// <summary> | ||||||
| @ -10,11 +8,11 @@ namespace Jellyfin.Api.Models.StartupDtos | |||||||
|         /// <summary> |         /// <summary> | ||||||
|         /// Gets or sets the username. |         /// Gets or sets the username. | ||||||
|         /// </summary> |         /// </summary> | ||||||
|         public string Name { get; set; } |         public string? Name { get; set; } | ||||||
| 
 | 
 | ||||||
|         /// <summary> |         /// <summary> | ||||||
|         /// Gets or sets the user's password. |         /// Gets or sets the user's password. | ||||||
|         /// </summary> |         /// </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