diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/CinemaJellyfin/CinemaAudio.cs b/CinemaJellyfin/CinemaAudio.cs deleted file mode 100644 index e69de29..0000000 diff --git a/CinemaJellyfin/CinemaConcertFolder.cs b/CinemaJellyfin/CinemaConcertFolder.cs index 2f5c2eb..54eb531 100644 --- a/CinemaJellyfin/CinemaConcertFolder.cs +++ b/CinemaJellyfin/CinemaConcertFolder.cs @@ -11,13 +11,13 @@ public sealed class CinemaConcertFolder : CinemaRootFolder { } - public override CollectionType? CollectionType => Data.Enums.CollectionType.music; + public override CollectionType? CollectionType => Data.Enums.CollectionType.musicvideos; - public override BaseItemKind ClientType => BaseItemKind.MusicAlbum; + public override BaseItemKind ClientType => BaseItemKind.MusicVideo; public override ItemType ItemType => ItemType.Concert; - internal override string ImageName => "music.png"; + internal override string ImageName => "concert.png"; protected override IEnumerable GetFilterItems() { @@ -28,6 +28,6 @@ public sealed class CinemaConcertFolder : CinemaRootFolder public override bool TryCreateMediaItem(MediaSource? media, string csId, BaseItem parentFolder, [NotNullWhen(true)] out BaseItem? item) { - return media.TryCreateMediaItem(csId, parentFolder, true, out item); + return media.TryCreateMediaItem(csId, parentFolder, true, out item); } } \ No newline at end of file diff --git a/CinemaJellyfin/CinemaEpisode.cs b/CinemaJellyfin/CinemaEpisode.cs index 60c16af..f6e8ce0 100644 --- a/CinemaJellyfin/CinemaEpisode.cs +++ b/CinemaJellyfin/CinemaEpisode.cs @@ -12,6 +12,11 @@ public class CinemaEpisode : Episode { public sealed override string GetClientTypeName() => BaseItemKind.Episode.ToString(); + public override List GetUserDataKeys() + { + return new List() { ExternalId }; + } + protected override IEnumerable<(BaseItem Item, MediaSourceType MediaSourceType)> GetAllItemsForMediaSources() { var result = this.VideoGetAllItemsForMediaSources(); diff --git a/CinemaJellyfin/CinemaFilterFolder.cs b/CinemaJellyfin/CinemaFilterFolder.cs index 3096b6b..de7d6bc 100644 --- a/CinemaJellyfin/CinemaFilterFolder.cs +++ b/CinemaJellyfin/CinemaFilterFolder.cs @@ -172,9 +172,6 @@ public abstract class CinemaFilterFolder : Folder { Folder parentItem = LibraryManager.GetUserRootFolder(); folder.ParentId = parentItem.Id; - //folder.IsRoot = true; - //if (!CinemaHost.LibraryManager.RootFolder.VirtualChildren.Contains(folder)) - // CinemaHost.LibraryManager.RootFolder.AddVirtualChild(folder); if (!parentItem.Children.Contains(folder)) parentItem.AddChild(folder); } diff --git a/CinemaJellyfin/CinemaHost.cs b/CinemaJellyfin/CinemaHost.cs index 3fdf31b..ef34c0a 100644 --- a/CinemaJellyfin/CinemaHost.cs +++ b/CinemaJellyfin/CinemaHost.cs @@ -86,8 +86,8 @@ sealed class CinemaHost : IHostedService .Hide = config.HideSeriesFolder; CinemaFilterFolder.CreateRootFilterFolder(string.IsNullOrWhiteSpace(config.AnimeFolderName) ? "Anime" : config.AnimeFolderName) .Hide = config.HideAnimeFolder; - CinemaFilterFolder.CreateRootFilterFolder(string.IsNullOrWhiteSpace(config.MusicFolderName) ? "Music" : config.MusicFolderName) - .Hide = config.HideMusicFolder; + CinemaFilterFolder.CreateRootFilterFolder(string.IsNullOrWhiteSpace(config.ConcertFolderName) ? "Concerts" : config.ConcertFolderName) + .Hide = config.HideConcertFolder; } private void EnsureWebshareSession(CinemaPluginConfiguration config) diff --git a/CinemaJellyfin/CinemaLibraryManager.cs b/CinemaJellyfin/CinemaLibraryManager.cs index e1252d4..f0ab392 100644 --- a/CinemaJellyfin/CinemaLibraryManager.cs +++ b/CinemaJellyfin/CinemaLibraryManager.cs @@ -1,3 +1,4 @@ +using CinemaLib.API; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; using MediaBrowser.Controller.Dto; @@ -5,6 +6,7 @@ using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Resolvers; using MediaBrowser.Controller.Sorting; @@ -19,8 +21,9 @@ namespace Jellyfin.Plugin.Cinema; sealed class CinemaLibraryManager : ILibraryManager { private readonly ILibraryManager _inner; + private readonly IUserDataRepository _userData; - public CinemaLibraryManager(CinemaInnerLibraryManager innerLibraryManager, IServiceProvider svc) + public CinemaLibraryManager(CinemaInnerLibraryManager innerLibraryManager, IUserDataRepository userData, IServiceProvider svc) { if (innerLibraryManager == null || svc == null) throw new ArgumentNullException(); @@ -29,7 +32,7 @@ sealed class CinemaLibraryManager : ILibraryManager if (inner == null) throw new InvalidOperationException("Original LibraryManager service not found."); this._inner = inner; - + this._userData = userData; } #region ILibraryManager Members @@ -265,20 +268,46 @@ sealed class CinemaLibraryManager : ILibraryManager public QueryResult GetItemsResult(InternalItemsQuery query) { QueryResult result = _inner.GetItemsResult(query); - if (query.ParentId != Guid.Empty || query.OrderBy.FirstOrDefault().OrderBy == ItemSortBy.DatePlayed) + if (query.ParentId != Guid.Empty) + // Not a search at the root so do not involve our root folders return result; + List resultL = new List(result.Items); - List a = new List(result.Items); - foreach (BaseItem i in GetUserRootFolder().Children) - if (i is CinemaRootFolder root) + if (query.OrderBy.FirstOrDefault().OrderBy == ItemSortBy.DatePlayed && query.User != null) + { + // Get Resume play items + // PERF: This may quickly become very slow + var resumePlay = _userData.GetAllUserData(query.User.InternalId) + .OrderByDescending(x => x.Played) + .Skip(query.StartIndex ?? 0) + .Take(query.Limit ?? 20); + foreach (var i in resumePlay) { - var b = root.GetItemList(query); - if (b != null) - a.AddRange(b); - } + // Note: All Cinema items override GetUserDataKeys and return ExternalId + string? csId; + if (!CinemaQueryExtensions.TryGetCinemaIdFromExternalId(i.Key, out csId)) + continue; - return new QueryResult() { Items = a }; + MediaSource? ms = Metadata.DetailAsync(csId, default).GetAwaiter().GetResult(); + if (CinemaQueryExtensions.TryCreateMediaItem(ms, csId, null, false, out BaseItem? a)) + resultL.Add(a); + } + } + else + { + // Add our root folders to the search + foreach (BaseItem i in GetUserRootFolder().Children) + // Prevent duplicates as content in Anime is also elsewhere + if (i is CinemaRootFolder root && i is not CinemaAnimeFolder) + { + var b = root.GetItemList(query); + if (b != null) + resultL.AddRange(b); + } + } + + return new QueryResult() { Items = resultL }; } public LibraryOptions GetLibraryOptions(BaseItem item) diff --git a/CinemaJellyfin/CinemaMovie.cs b/CinemaJellyfin/CinemaMovie.cs index c559a6f..8c9fe46 100644 --- a/CinemaJellyfin/CinemaMovie.cs +++ b/CinemaJellyfin/CinemaMovie.cs @@ -12,6 +12,11 @@ public class CinemaMovie : Movie { public sealed override string GetClientTypeName() => BaseItemKind.Movie.ToString(); + public override List GetUserDataKeys() + { + return new List() { ExternalId }; + } + protected override IEnumerable<(BaseItem Item, MediaSourceType MediaSourceType)> GetAllItemsForMediaSources() { var result = this.VideoGetAllItemsForMediaSources(); diff --git a/CinemaJellyfin/CinemaMusicAlbum.cs b/CinemaJellyfin/CinemaMusicAlbum.cs deleted file mode 100644 index 00ed947..0000000 --- a/CinemaJellyfin/CinemaMusicAlbum.cs +++ /dev/null @@ -1,68 +0,0 @@ -using CinemaLib.API; -using Jellyfin.Data.Entities; -using Jellyfin.Data.Enums; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Entities.Audio; -using MediaBrowser.Model.Querying; - -namespace Jellyfin.Plugin.Cinema; - -/// -/// Music album folder item from Cinema. -/// -public sealed class CinemaMusicAlbum : MusicAlbum -{ - public sealed override string GetClientTypeName() => BaseItemKind.Series.ToString(); - - public override int GetChildCount(User user) - { - return base.GetChildCount(user); - } - - public override int GetRecursiveChildCount(User user) - { - return base.GetRecursiveChildCount(user); - } - - public override List GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery query) - { - return (List)GetItemsInternal(query).Items; - } - - protected override QueryResult GetItemsInternal(InternalItemsQuery query) - { - int offset = query.StartIndex ?? 0; - int limit = query.Limit ?? 0; - - List items = new List(); - QueryResult result = new QueryResult() { Items = items, StartIndex = offset }; - - FilterSortBy sortBy; - ItemOrder sortDir; - if (query.OrderBy.Count == 0) - { - sortBy = FilterSortBy.Episode; - sortDir = ItemOrder.Ascending; - } - else - { - (ItemSortBy sortByJ, SortOrder sortDirJ) = query.OrderBy.First(); - sortBy = sortByJ.ToCinema(); - sortDir = sortDirJ == SortOrder.Ascending ? ItemOrder.Ascending : ItemOrder.Descending; - } - - FilterResponse? filterRes = Metadata.ChildrenAsync(query.SearchTerm ?? "", order: sortDir, sort: sortBy, offset: offset, limit: limit).GetAwaiter().GetResult(); - if (filterRes != null && filterRes.hits != null && filterRes.hits.hits != null) - { - if (filterRes.hits.total != null) - result.TotalRecordCount = (int)filterRes.hits.total.value; - foreach (var i in filterRes.hits.hits) - { - if (i._source.TryCreateMediaItem(i._id, this, false, out BaseItem? a)) - items.Add(a); - } - } - - return result; - } -} \ No newline at end of file diff --git a/CinemaJellyfin/CinemaMusicVideo.cs b/CinemaJellyfin/CinemaMusicVideo.cs new file mode 100644 index 0000000..b53d68e --- /dev/null +++ b/CinemaJellyfin/CinemaMusicVideo.cs @@ -0,0 +1,36 @@ +using Jellyfin.Data.Enums; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Model.Dto; + +namespace Jellyfin.Plugin.Cinema; + +/// +/// Concerts folder item from Cinema. +/// +public sealed class CinemaMusicVideo : MusicVideo +{ + public sealed override string GetClientTypeName() => BaseItemKind.MusicVideo.ToString(); + + public override List GetUserDataKeys() + { + return new List() { ExternalId }; + } + + protected override IEnumerable<(BaseItem Item, MediaSourceType MediaSourceType)> GetAllItemsForMediaSources() + { + var result = this.VideoGetAllItemsForMediaSources(); + if (result == null) + return base.GetAllItemsForMediaSources(); + else + return result; + } + + public override List GetMediaSources(bool enablePathSubstitution) + { + var result = this.VideoGetMediaSources(enablePathSubstitution); + if (result == null) + return base.GetMediaSources(enablePathSubstitution); + else + return result; + } +} \ No newline at end of file diff --git a/CinemaJellyfin/CinemaQueryExtensions.cs b/CinemaJellyfin/CinemaQueryExtensions.cs index 1469b24..e76b5f0 100644 --- a/CinemaJellyfin/CinemaQueryExtensions.cs +++ b/CinemaJellyfin/CinemaQueryExtensions.cs @@ -61,7 +61,7 @@ static class CinemaQueryExtensions /// Jellyfin parent folder. /// On success the created item. /// True on success, false otherwise. - public static bool TryCreateMediaItem(this MediaSource? media, string csId, BaseItem parentFolder, bool allowContainer, [NotNullWhen(true)] out BaseItem? item) + public static bool TryCreateMediaItem(this MediaSource? media, string csId, BaseItem? parentFolder, bool allowContainer, [NotNullWhen(true)] out BaseItem? item) where TContainerType : Folder, new() { if (media == null) @@ -70,12 +70,10 @@ static class CinemaQueryExtensions return false; } - var parentFolderId = parentFolder.Id; - bool isNew; bool forceUpdate = false; - bool isAudio = media.is_concert ?? false; + bool isConcert = media.is_concert ?? false; if (media.children_count != 0) { // Container @@ -87,10 +85,9 @@ static class CinemaQueryExtensions item = GetMediaItemById(csId, null, out isNew); } - else if (isAudio) + else if (isConcert) { - // TODO determine if this is an AudioBook od Audio - item = GetMediaItemById(csId, null, out isNew); + item = GetMediaItemById(csId, null, out isNew); } else if (media.info_labels?.mediatype == "episode") @@ -164,7 +161,8 @@ static class CinemaQueryExtensions hasAlbumArtists.AlbumArtists = media.info_labels?.director; } - item.ParentId = parentFolderId; + if (parentFolder != null) + item.ParentId = parentFolder.Id; /* if (item is IHasSeries hasSeries) diff --git a/CinemaJellyfin/CinemaSeason.cs b/CinemaJellyfin/CinemaSeason.cs index 41f333f..4953b77 100644 --- a/CinemaJellyfin/CinemaSeason.cs +++ b/CinemaJellyfin/CinemaSeason.cs @@ -15,10 +15,16 @@ public sealed class CinemaSeason : Season, ICinemaChildrenCount { private CinemaSubfolderHelper _helper; - public CinemaSeason() { + public CinemaSeason() + { this._helper = new CinemaSubfolderHelper(this, FilterSortBy.Episode); } + public override List GetUserDataKeys() + { + return new List() { ExternalId }; + } + #region ICinemaChildrenCountMembers public int ChildrenCount @@ -27,9 +33,10 @@ public sealed class CinemaSeason : Season, ICinemaChildrenCount set => _helper.ChildrenCount = value; } - public int TotalChildrenCount { - get => _helper.TotalChildrenCount; - set => _helper.TotalChildrenCount = value; + public int TotalChildrenCount + { + get => _helper.TotalChildrenCount; + set => _helper.TotalChildrenCount = value; } #endregion diff --git a/CinemaJellyfin/CinemaTvSeries.cs b/CinemaJellyfin/CinemaTvSeries.cs index dc5e0ff..97773ec 100644 --- a/CinemaJellyfin/CinemaTvSeries.cs +++ b/CinemaJellyfin/CinemaTvSeries.cs @@ -15,10 +15,16 @@ public sealed class CinemaTvSeries : Series, ICinemaChildrenCount { private CinemaSubfolderHelper _helper; - public CinemaTvSeries() { + public CinemaTvSeries() + { this._helper = new CinemaSubfolderHelper(this, FilterSortBy.Episode); } + public override List GetUserDataKeys() + { + return new List() { ExternalId }; + } + #region ICinemaChildrenCountMembers public int ChildrenCount @@ -27,9 +33,10 @@ public sealed class CinemaTvSeries : Series, ICinemaChildrenCount set => _helper.ChildrenCount = value; } - public int TotalChildrenCount { - get => _helper.TotalChildrenCount; - set => _helper.TotalChildrenCount = value; + public int TotalChildrenCount + { + get => _helper.TotalChildrenCount; + set => _helper.TotalChildrenCount = value; } #endregion @@ -56,7 +63,7 @@ public sealed class CinemaTvSeries : Series, ICinemaChildrenCount /// Let the hack in find us. /// public override string CreatePresentationUniqueKey() => Id.ToString("N", CultureInfo.InvariantCulture); - + public override List GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery query) { return (List)GetItemsInternal(query).Items; @@ -68,7 +75,8 @@ public sealed class CinemaTvSeries : Series, ICinemaChildrenCount // Set series identifier on all seasons foreach (var i in result.Items) - if (i is CinemaSeason season) { + if (i is CinemaSeason season) + { season.SeriesId = Id; season.SeriesName = Name; } diff --git a/CinemaJellyfin/Configuration/CinemaPluginConfiguration.cs b/CinemaJellyfin/Configuration/CinemaPluginConfiguration.cs index e295994..995ceb9 100644 --- a/CinemaJellyfin/Configuration/CinemaPluginConfiguration.cs +++ b/CinemaJellyfin/Configuration/CinemaPluginConfiguration.cs @@ -20,8 +20,8 @@ public class CinemaPluginConfiguration : BasePluginConfiguration public string AnimeFolderName { get; set; } public bool HideAnimeFolder { get; set; } - public string MusicFolderName { get; set; } - public bool HideMusicFolder { get; set; } + public string ConcertFolderName { get; set; } + public bool HideConcertFolder { get; set; } public string? WebshareUser { get; set; } public string? WebsharePassword { get; set; } diff --git a/CinemaJellyfin/Configuration/config.html b/CinemaJellyfin/Configuration/config.html index 68fc9ce..e4b8106 100644 --- a/CinemaJellyfin/Configuration/config.html +++ b/CinemaJellyfin/Configuration/config.html @@ -40,12 +40,12 @@
- -
Custom name for the Cinema Music root folder.
+ +
Custom name for the Cinema Concerts root folder.

Webshare Account

@@ -101,14 +101,14 @@ })); document.querySelector('#hideAnimeFolder').checked = config.HideAnimeFolder; - var musicFolderName = document.querySelector('#musicFolderName'); - if (config.MusicFolderName) - musicFolderName.value = config.MusicFolderName; - musicFolderName.dispatchEvent(new Event('change', { + var concertFolderName = document.querySelector('#concertFolderName'); + if (config.ConcertFolderName) + concertFolderName.value = config.ConcertFolderName; + concertFolderName.dispatchEvent(new Event('change', { bubbles: true, cancelable: false })); - document.querySelector('#hideMusicFolder').checked = config.HideMusicFolder; + document.querySelector('#hideConcertFolder').checked = config.HideConcertFolder; var webshareUser = document.querySelector('#webshareUser'); if (config.WebshareUser) @@ -140,8 +140,8 @@ config.HideSeriesFolder = document.querySelector('#hideSeriesFolder').checked; config.AnimeFolderName = document.querySelector('#animeFolderName').value; config.HideAnimeFolder = document.querySelector('#hideAnimeFolder').checked; - config.MusicFolderName = document.querySelector('#musicFolderName').value; - config.HideMusicFolder = document.querySelector('#hideMusicFolder').checked; + config.ConcertFolderName = document.querySelector('#concertFolderName').value; + config.HideConcertFolder = document.querySelector('#hideConcertFolder').checked; config.WebshareUser = document.querySelector('#webshareUser').value; config.WebsharePassword = document.querySelector('#websharePassword').value; diff --git a/CinemaLib/API/Metadata.cs b/CinemaLib/API/Metadata.cs index c433d46..a49b862 100644 --- a/CinemaLib/API/Metadata.cs +++ b/CinemaLib/API/Metadata.cs @@ -37,22 +37,27 @@ public class Metadata if (limit == 0 || limit > MaxPageLimit) limit = MaxPageLimit; - bool noSearchTerms = expression.Trim().Length == 0; - string filterName; - string sortS = ToString(sort); - if (sort == FilterSortBy.Title) { - filterName = "startsWithSimple"; - sortS = ""; - } else if (noSearchTerms) { - filterName = "all"; - } else { - filterName = "search"; - } + bool noSearchTerms = expression.Trim().Length == 0; + string filterName; + string sortS = ToString(sort); + if (sort == FilterSortBy.Title) + { + filterName = "startsWithSimple"; + sortS = ""; + } + else if (noSearchTerms) + { + filterName = "all"; + } + else + { + filterName = "search"; + } UriBuilder uri = new UriBuilder(new Uri(ApiFilter, filterName)); - string orderS = order == ItemOrder.Ascending ? "asc" : "desc"; + string orderS = order == ItemOrder.Ascending ? "asc" : "desc"; uri.Query = $"?access_token={AccessToken}&value={Uri.EscapeDataString(expression)}&type={ToString(type)}&from={offset.ToString()}&size={limit.ToString()}" - + (sortS.Length != 0 ? $"&order={ToString(order)}&sort={sortS}" : ""); + + (sortS.Length != 0 ? $"&order={ToString(order)}&sort={sortS}" : ""); FilterResponse? result = await Program._http.GetFromJsonAsync(uri.Uri, CreateFilterJsonOptions(), cancel); if (result != null) @@ -81,6 +86,24 @@ public class Metadata return result; } + /// + /// Gets detail info about in item. + /// + /// Item identifier. + /// Asynchronous cancellation. + /// Media info. + public async static Task DetailAsync(string id, CancellationToken cancel) + { + if (id == null || id.Length == 0) + throw new ArgumentNullException(); + + Uri uri = new Uri(new Uri(ApiRoot, "media/"), id); + MediaSource? result = await Program._http.GetFromJsonAsync(uri, CreateFilterJsonOptions(), cancel); + if (result != null) + result = FixDetailResponse(result); + return result; + } + /// /// Gets available streams for the given media. /// @@ -160,17 +183,26 @@ public class Metadata if (i._source == null) continue; - if (i._source.cast != null) - foreach (Cast j in i._source.cast) - j.thumbnail = FixImageUrl(j.thumbnail); + FixDetailResponse(i._source); + } - if (i._source.i18n_info_labels != null) - foreach (InfoLabelI18n j in i._source.i18n_info_labels) { - if (j.art != null) { - j.art.poster = FixImageUrl(j.art.poster); - j.art.fanart = FixImageUrl(j.art.fanart); - } - } + return res; + } + + private static MediaSource FixDetailResponse(MediaSource res) + { + if (res.cast != null) + foreach (Cast j in res.cast) + j.thumbnail = FixImageUrl(j.thumbnail); + + if (res.i18n_info_labels != null) + foreach (InfoLabelI18n j in res.i18n_info_labels) + { + if (j.art != null) + { + j.art.poster = FixImageUrl(j.art.poster); + j.art.fanart = FixImageUrl(j.art.fanart); + } } return res; @@ -178,7 +210,8 @@ public class Metadata private static string FixImageUrl(string url) { - if (url != null && !url.StartsWith("http")) { + if (url != null && !url.StartsWith("http")) + { if (url.StartsWith("//")) url = "https:" + url; else