| | 1 | | #nullable disable |
| | 2 | |
|
| | 3 | | #pragma warning disable CS1591 |
| | 4 | |
|
| | 5 | | using System; |
| | 6 | | using System.Collections.Generic; |
| | 7 | | using System.Linq; |
| | 8 | | using System.Threading; |
| | 9 | | using Jellyfin.Data; |
| | 10 | | using Jellyfin.Data.Enums; |
| | 11 | | using Jellyfin.Database.Implementations.Entities; |
| | 12 | | using Jellyfin.Database.Implementations.Enums; |
| | 13 | | using Jellyfin.Extensions; |
| | 14 | | using MediaBrowser.Controller.Channels; |
| | 15 | | using MediaBrowser.Controller.Configuration; |
| | 16 | | using MediaBrowser.Controller.Dto; |
| | 17 | | using MediaBrowser.Controller.Entities; |
| | 18 | | using MediaBrowser.Controller.Library; |
| | 19 | | using MediaBrowser.Controller.LiveTv; |
| | 20 | | using MediaBrowser.Model.Channels; |
| | 21 | | using MediaBrowser.Model.Globalization; |
| | 22 | | using MediaBrowser.Model.Library; |
| | 23 | | using MediaBrowser.Model.Querying; |
| | 24 | |
|
| | 25 | | namespace Emby.Server.Implementations.Library |
| | 26 | | { |
| | 27 | | public class UserViewManager : IUserViewManager |
| | 28 | | { |
| | 29 | | private readonly ILibraryManager _libraryManager; |
| | 30 | | private readonly ILocalizationManager _localizationManager; |
| | 31 | |
|
| | 32 | | private readonly IChannelManager _channelManager; |
| | 33 | | private readonly ILiveTvManager _liveTvManager; |
| | 34 | | private readonly IServerConfigurationManager _config; |
| | 35 | |
|
| | 36 | | public UserViewManager(ILibraryManager libraryManager, ILocalizationManager localizationManager, IChannelManager |
| | 37 | | { |
| 21 | 38 | | _libraryManager = libraryManager; |
| 21 | 39 | | _localizationManager = localizationManager; |
| 21 | 40 | | _channelManager = channelManager; |
| 21 | 41 | | _liveTvManager = liveTvManager; |
| 21 | 42 | | _config = config; |
| 21 | 43 | | } |
| | 44 | |
|
| | 45 | | public Folder[] GetUserViews(UserViewQuery query) |
| | 46 | | { |
| 1 | 47 | | var user = query.User; |
| | 48 | |
|
| 1 | 49 | | var folders = _libraryManager.GetUserRootFolder() |
| 1 | 50 | | .GetChildren(user, true) |
| 1 | 51 | | .OfType<Folder>() |
| 1 | 52 | | .ToList(); |
| | 53 | |
|
| 1 | 54 | | var groupedFolders = new List<ICollectionFolder>(); |
| 1 | 55 | | var list = new List<Folder>(); |
| | 56 | |
|
| 4 | 57 | | foreach (var folder in folders) |
| | 58 | | { |
| 1 | 59 | | var collectionFolder = folder as ICollectionFolder; |
| 1 | 60 | | var folderViewType = collectionFolder?.CollectionType; |
| | 61 | |
|
| | 62 | | // Playlist library requires special handling because the folder only references user playlists |
| 1 | 63 | | if (folderViewType == CollectionType.playlists) |
| | 64 | | { |
| 1 | 65 | | var items = folder.GetItemList(new InternalItemsQuery(user) |
| 1 | 66 | | { |
| 1 | 67 | | ParentId = folder.ParentId |
| 1 | 68 | | }); |
| | 69 | |
|
| 1 | 70 | | if (!items.Any(item => item.IsVisible(user))) |
| | 71 | | { |
| | 72 | | continue; |
| | 73 | | } |
| | 74 | | } |
| | 75 | |
|
| 0 | 76 | | if (UserView.IsUserSpecific(folder)) |
| | 77 | | { |
| 0 | 78 | | list.Add(_libraryManager.GetNamedView(user, folder.Name, folder.Id, folderViewType, null)); |
| 0 | 79 | | continue; |
| | 80 | | } |
| | 81 | |
|
| 0 | 82 | | if (collectionFolder is not null && UserView.IsEligibleForGrouping(folder) && user.IsFolderGrouped(folde |
| | 83 | | { |
| 0 | 84 | | groupedFolders.Add(collectionFolder); |
| 0 | 85 | | continue; |
| | 86 | | } |
| | 87 | |
|
| 0 | 88 | | if (query.PresetViews.Contains(folderViewType)) |
| | 89 | | { |
| 0 | 90 | | list.Add(GetUserView(folder, folderViewType, string.Empty)); |
| | 91 | | } |
| | 92 | | else |
| | 93 | | { |
| 0 | 94 | | list.Add(folder); |
| | 95 | | } |
| | 96 | | } |
| | 97 | |
|
| 6 | 98 | | foreach (var viewType in new[] { CollectionType.movies, CollectionType.tvshows }) |
| | 99 | | { |
| 2 | 100 | | var parents = groupedFolders.Where(i => i.CollectionType == viewType || i.CollectionType is null) |
| 2 | 101 | | .ToList(); |
| | 102 | |
|
| 2 | 103 | | if (parents.Count > 0) |
| | 104 | | { |
| 0 | 105 | | var localizationKey = viewType == CollectionType.tvshows |
| 0 | 106 | | ? "TvShows" |
| 0 | 107 | | : "Movies"; |
| | 108 | |
|
| 0 | 109 | | list.Add(GetUserView(parents, viewType, localizationKey, string.Empty, user, query.PresetViews)); |
| | 110 | | } |
| | 111 | | } |
| | 112 | |
|
| 1 | 113 | | if (_config.Configuration.EnableFolderView) |
| | 114 | | { |
| 0 | 115 | | var name = _localizationManager.GetLocalizedString("Folders"); |
| 0 | 116 | | list.Add(_libraryManager.GetNamedView(name, CollectionType.folders, string.Empty)); |
| | 117 | | } |
| | 118 | |
|
| 1 | 119 | | if (query.IncludeExternalContent) |
| | 120 | | { |
| 1 | 121 | | var channelResult = _channelManager.GetChannelsInternalAsync(new ChannelQuery |
| 1 | 122 | | { |
| 1 | 123 | | UserId = user.Id |
| 1 | 124 | | }).GetAwaiter().GetResult(); |
| | 125 | |
|
| 1 | 126 | | var channels = channelResult.Items; |
| | 127 | |
|
| 1 | 128 | | list.AddRange(channels); |
| | 129 | |
|
| 1 | 130 | | if (_liveTvManager.GetEnabledUsers().Select(i => i.Id).Contains(user.Id)) |
| | 131 | | { |
| 0 | 132 | | list.Add(_liveTvManager.GetInternalLiveTvFolder(CancellationToken.None)); |
| | 133 | | } |
| | 134 | | } |
| | 135 | |
|
| 1 | 136 | | if (!query.IncludeHidden) |
| | 137 | | { |
| 0 | 138 | | list = list.Where(i => !user.GetPreferenceValues<Guid>(PreferenceKind.MyMediaExcludes).Contains(i.Id)).T |
| | 139 | | } |
| | 140 | |
|
| 1 | 141 | | var sorted = _libraryManager.Sort(list, user, new[] { ItemSortBy.SortName }, SortOrder.Ascending).ToList(); |
| 1 | 142 | | var orders = user.GetPreferenceValues<Guid>(PreferenceKind.OrderedViews); |
| | 143 | |
|
| 1 | 144 | | return list |
| 1 | 145 | | .OrderBy(i => |
| 1 | 146 | | { |
| 1 | 147 | | var index = Array.IndexOf(orders, i.Id); |
| 1 | 148 | | if (index == -1 |
| 1 | 149 | | && i is UserView view |
| 1 | 150 | | && !view.DisplayParentId.IsEmpty()) |
| 1 | 151 | | { |
| 1 | 152 | | index = Array.IndexOf(orders, view.DisplayParentId); |
| 1 | 153 | | } |
| 1 | 154 | |
|
| 1 | 155 | | return index == -1 ? int.MaxValue : index; |
| 1 | 156 | | }) |
| 1 | 157 | | .ThenBy(sorted.IndexOf) |
| 1 | 158 | | .ThenBy(i => i.SortName) |
| 1 | 159 | | .ToArray(); |
| | 160 | | } |
| | 161 | |
|
| | 162 | | public UserView GetUserSubViewWithName(string name, Guid parentId, CollectionType? type, string sortName) |
| | 163 | | { |
| 0 | 164 | | var uniqueId = parentId + "subview" + type; |
| | 165 | |
|
| 0 | 166 | | return _libraryManager.GetNamedView(name, parentId, type, sortName, uniqueId); |
| | 167 | | } |
| | 168 | |
|
| | 169 | | public UserView GetUserSubView(Guid parentId, CollectionType? type, string localizationKey, string sortName) |
| | 170 | | { |
| 0 | 171 | | var name = _localizationManager.GetLocalizedString(localizationKey); |
| | 172 | |
|
| 0 | 173 | | return GetUserSubViewWithName(name, parentId, type, sortName); |
| | 174 | | } |
| | 175 | |
|
| | 176 | | private Folder GetUserView( |
| | 177 | | List<ICollectionFolder> parents, |
| | 178 | | CollectionType? viewType, |
| | 179 | | string localizationKey, |
| | 180 | | string sortName, |
| | 181 | | User user, |
| | 182 | | CollectionType?[] presetViews) |
| | 183 | | { |
| 0 | 184 | | if (parents.Count == 1 && parents.All(i => i.CollectionType == viewType)) |
| | 185 | | { |
| 0 | 186 | | if (!presetViews.Contains(viewType)) |
| | 187 | | { |
| 0 | 188 | | return (Folder)parents[0]; |
| | 189 | | } |
| | 190 | |
|
| 0 | 191 | | return GetUserView((Folder)parents[0], viewType, string.Empty); |
| | 192 | | } |
| | 193 | |
|
| 0 | 194 | | var name = _localizationManager.GetLocalizedString(localizationKey); |
| 0 | 195 | | return _libraryManager.GetNamedView(user, name, viewType, sortName); |
| | 196 | | } |
| | 197 | |
|
| | 198 | | public UserView GetUserView(Folder parent, CollectionType? viewType, string sortName) |
| | 199 | | { |
| 0 | 200 | | return _libraryManager.GetShadowView(parent, viewType, sortName); |
| | 201 | | } |
| | 202 | |
|
| | 203 | | public List<Tuple<BaseItem, List<BaseItem>>> GetLatestItems(LatestItemsQuery request, DtoOptions options) |
| | 204 | | { |
| 0 | 205 | | var libraryItems = GetItemsForLatestItems(request.User, request, options); |
| | 206 | |
|
| 0 | 207 | | var list = new List<Tuple<BaseItem, List<BaseItem>>>(); |
| | 208 | |
|
| 0 | 209 | | foreach (var item in libraryItems) |
| | 210 | | { |
| | 211 | | // Only grab the index container for media |
| 0 | 212 | | var container = item.IsFolder || !request.GroupItems ? null : item.LatestItemsIndexContainer; |
| | 213 | |
|
| 0 | 214 | | if (container is null) |
| | 215 | | { |
| 0 | 216 | | list.Add(new Tuple<BaseItem, List<BaseItem>>(null, new List<BaseItem> { item })); |
| | 217 | | } |
| | 218 | | else |
| | 219 | | { |
| 0 | 220 | | var current = list.FirstOrDefault(i => i.Item1 is not null && i.Item1.Id.Equals(container.Id)); |
| | 221 | |
|
| 0 | 222 | | if (current is not null) |
| | 223 | | { |
| 0 | 224 | | current.Item2.Add(item); |
| | 225 | | } |
| | 226 | | else |
| | 227 | | { |
| 0 | 228 | | list.Add(new Tuple<BaseItem, List<BaseItem>>(container, new List<BaseItem> { item })); |
| | 229 | | } |
| | 230 | | } |
| | 231 | |
|
| 0 | 232 | | if (list.Count >= request.Limit) |
| | 233 | | { |
| 0 | 234 | | break; |
| | 235 | | } |
| | 236 | | } |
| | 237 | |
|
| 0 | 238 | | return list; |
| | 239 | | } |
| | 240 | |
|
| | 241 | | private IReadOnlyList<BaseItem> GetItemsForLatestItems(User user, LatestItemsQuery request, DtoOptions options) |
| | 242 | | { |
| 0 | 243 | | var parentId = request.ParentId; |
| | 244 | |
|
| 0 | 245 | | var includeItemTypes = request.IncludeItemTypes; |
| 0 | 246 | | var limit = request.Limit ?? 10; |
| | 247 | |
|
| 0 | 248 | | var parents = new List<BaseItem>(); |
| | 249 | |
|
| 0 | 250 | | if (!parentId.IsEmpty()) |
| | 251 | | { |
| 0 | 252 | | var parentItem = _libraryManager.GetItemById(parentId); |
| 0 | 253 | | if (parentItem is Channel) |
| | 254 | | { |
| 0 | 255 | | return _channelManager.GetLatestChannelItemsInternal( |
| 0 | 256 | | new InternalItemsQuery(user) |
| 0 | 257 | | { |
| 0 | 258 | | ChannelIds = new[] { parentId }, |
| 0 | 259 | | IsPlayed = request.IsPlayed, |
| 0 | 260 | | StartIndex = request.StartIndex, |
| 0 | 261 | | Limit = request.Limit, |
| 0 | 262 | | IncludeItemTypes = request.IncludeItemTypes, |
| 0 | 263 | | EnableTotalRecordCount = false |
| 0 | 264 | | }, |
| 0 | 265 | | CancellationToken.None).GetAwaiter().GetResult().Items; |
| | 266 | | } |
| | 267 | |
|
| 0 | 268 | | if (parentItem is Folder parent) |
| | 269 | | { |
| 0 | 270 | | parents.Add(parent); |
| | 271 | | } |
| | 272 | | } |
| | 273 | |
|
| 0 | 274 | | var isPlayed = request.IsPlayed; |
| | 275 | |
|
| 0 | 276 | | if (parents.OfType<ICollectionFolder>().Any(i => i.CollectionType == CollectionType.music)) |
| | 277 | | { |
| 0 | 278 | | isPlayed = null; |
| | 279 | | } |
| | 280 | |
|
| 0 | 281 | | if (parents.Count == 0) |
| | 282 | | { |
| 0 | 283 | | parents = _libraryManager.GetUserRootFolder().GetChildren(user, true) |
| 0 | 284 | | .Where(i => i is Folder) |
| 0 | 285 | | .Where(i => !user.GetPreferenceValues<Guid>(PreferenceKind.LatestItemExcludes) |
| 0 | 286 | | .Contains(i.Id)) |
| 0 | 287 | | .ToList(); |
| | 288 | | } |
| | 289 | |
|
| 0 | 290 | | if (parents.Count == 0) |
| | 291 | | { |
| 0 | 292 | | return Array.Empty<BaseItem>(); |
| | 293 | | } |
| | 294 | |
|
| 0 | 295 | | if (includeItemTypes.Length == 0) |
| | 296 | | { |
| | 297 | | // Handle situations with the grouping setting, e.g. movies showing up in tv, etc. |
| | 298 | | // Thanks to mixed content libraries included in the UserView |
| 0 | 299 | | var hasCollectionType = parents.OfType<UserView>().ToList(); |
| 0 | 300 | | if (hasCollectionType.Count > 0) |
| | 301 | | { |
| 0 | 302 | | if (hasCollectionType.All(i => i.CollectionType == CollectionType.movies)) |
| | 303 | | { |
| 0 | 304 | | includeItemTypes = new[] { BaseItemKind.Movie }; |
| | 305 | | } |
| 0 | 306 | | else if (hasCollectionType.All(i => i.CollectionType == CollectionType.tvshows)) |
| | 307 | | { |
| 0 | 308 | | includeItemTypes = new[] { BaseItemKind.Episode }; |
| | 309 | | } |
| | 310 | | } |
| | 311 | | } |
| | 312 | |
|
| 0 | 313 | | MediaType[] mediaTypes = []; |
| | 314 | |
|
| 0 | 315 | | if (includeItemTypes.Length == 0) |
| | 316 | | { |
| 0 | 317 | | HashSet<MediaType> tmpMediaTypes = []; |
| 0 | 318 | | foreach (var parent in parents.OfType<ICollectionFolder>()) |
| | 319 | | { |
| 0 | 320 | | switch (parent.CollectionType) |
| | 321 | | { |
| | 322 | | case CollectionType.books: |
| 0 | 323 | | tmpMediaTypes.Add(MediaType.Book); |
| 0 | 324 | | tmpMediaTypes.Add(MediaType.Audio); |
| 0 | 325 | | break; |
| | 326 | | case CollectionType.music: |
| 0 | 327 | | tmpMediaTypes.Add(MediaType.Audio); |
| 0 | 328 | | break; |
| | 329 | | case CollectionType.photos: |
| 0 | 330 | | tmpMediaTypes.Add(MediaType.Photo); |
| 0 | 331 | | tmpMediaTypes.Add(MediaType.Video); |
| 0 | 332 | | break; |
| | 333 | | case CollectionType.homevideos: |
| 0 | 334 | | tmpMediaTypes.Add(MediaType.Photo); |
| 0 | 335 | | tmpMediaTypes.Add(MediaType.Video); |
| 0 | 336 | | break; |
| | 337 | | default: |
| 0 | 338 | | tmpMediaTypes.Add(MediaType.Video); |
| | 339 | | break; |
| | 340 | | } |
| | 341 | | } |
| | 342 | |
|
| 0 | 343 | | mediaTypes = tmpMediaTypes.ToArray(); |
| | 344 | | } |
| | 345 | |
|
| 0 | 346 | | var excludeItemTypes = includeItemTypes.Length == 0 && mediaTypes.Length == 0 |
| 0 | 347 | | ? new[] |
| 0 | 348 | | { |
| 0 | 349 | | BaseItemKind.Person, |
| 0 | 350 | | BaseItemKind.Studio, |
| 0 | 351 | | BaseItemKind.Year, |
| 0 | 352 | | BaseItemKind.MusicGenre, |
| 0 | 353 | | BaseItemKind.Genre |
| 0 | 354 | | } |
| 0 | 355 | | : Array.Empty<BaseItemKind>(); |
| | 356 | |
|
| 0 | 357 | | var query = new InternalItemsQuery(user) |
| 0 | 358 | | { |
| 0 | 359 | | IncludeItemTypes = includeItemTypes, |
| 0 | 360 | | OrderBy = new[] |
| 0 | 361 | | { |
| 0 | 362 | | (ItemSortBy.DateCreated, SortOrder.Descending), |
| 0 | 363 | | (ItemSortBy.SortName, SortOrder.Descending), |
| 0 | 364 | | (ItemSortBy.ProductionYear, SortOrder.Descending) |
| 0 | 365 | | }, |
| 0 | 366 | | IsFolder = includeItemTypes.Length == 0 ? false : null, |
| 0 | 367 | | ExcludeItemTypes = excludeItemTypes, |
| 0 | 368 | | IsVirtualItem = false, |
| 0 | 369 | | Limit = limit * 5, |
| 0 | 370 | | IsPlayed = isPlayed, |
| 0 | 371 | | DtoOptions = options, |
| 0 | 372 | | MediaTypes = mediaTypes |
| 0 | 373 | | }; |
| | 374 | |
|
| 0 | 375 | | if (request.GroupItems) |
| | 376 | | { |
| 0 | 377 | | if (parents.OfType<ICollectionFolder>().All(i => i.CollectionType == CollectionType.tvshows)) |
| | 378 | | { |
| 0 | 379 | | query.Limit = limit; |
| 0 | 380 | | return _libraryManager.GetLatestItemList(query, parents, CollectionType.tvshows); |
| | 381 | | } |
| | 382 | |
|
| 0 | 383 | | if (parents.OfType<ICollectionFolder>().All(i => i.CollectionType == CollectionType.music)) |
| | 384 | | { |
| 0 | 385 | | query.Limit = limit; |
| 0 | 386 | | return _libraryManager.GetLatestItemList(query, parents, CollectionType.music); |
| | 387 | | } |
| | 388 | | } |
| | 389 | |
|
| 0 | 390 | | return _libraryManager.GetItemList(query, parents); |
| | 391 | | } |
| | 392 | | } |
| | 393 | | } |