From cfa94a89a6100656c41b9dca817d4dfd63d9b95b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roman=20Van=C3=AD=C4=8Dek?= Date: Tue, 26 Nov 2024 22:21:10 +0100 Subject: [PATCH] Movie folder view, sort and search works --- .../StreamCinemaFilterFolder.cs | 453 +++++++++- StreamCinemaJellyfin/StreamCinemaHost.cs | 102 +-- .../StreamCinemaMoviesFolder.cs | 54 +- StreamCinemaJellyfin/StreamCinemaPlugin.cs | 7 - .../StreamCinemaPopularFolder.cs | 11 + .../StreamCinemaRootFolder.cs | 841 +++++------------- .../StreamCinemaTrendingFolder.cs | 11 + StreamCinemaLib/API/Metadata.cs | 26 +- 8 files changed, 754 insertions(+), 751 deletions(-) diff --git a/StreamCinemaJellyfin/StreamCinemaFilterFolder.cs b/StreamCinemaJellyfin/StreamCinemaFilterFolder.cs index 4c48005..2a06095 100644 --- a/StreamCinemaJellyfin/StreamCinemaFilterFolder.cs +++ b/StreamCinemaJellyfin/StreamCinemaFilterFolder.cs @@ -1,13 +1,456 @@ +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Audio; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Querying; +using StreamCinemaLib.API; namespace Jellyfin.Plugin.StreamCinema; +/// +/// Stream Cinema folder that also implicitly represents a filter (minimally filter by ). +/// public abstract class StreamCinemaFilterFolder : Folder { - public sealed override bool CanDelete() - { - return false; - } + private const string PreferredCulture = "cs"; + private const string FallbackCulture = "en"; - internal abstract string ImageName { get; } + /// + /// Filtering folders can never be deleted. + /// + /// + public sealed override bool CanDelete() + { + return false; + } + + /// + /// Type of the collection as reported to the web client so it knows what feaures (mostly filtering ones) to render. + /// + public abstract BaseItemKind ClientType { get; } + + /// + /// Forwards . + /// + public sealed override string GetClientTypeName() => ClientType.ToString(); + + /// + /// Stream Cinema item type in this folder. Shall follow as faithfully + /// as possible. Used for filtering. + /// + public abstract ItemType ItemType { get; } + + /// + /// Information for to render icon for us. + /// + internal abstract string ImageName { get; } + + /// + /// Gets the static filter folders. + /// + protected abstract IEnumerable GetFilterItems(); + + /// + /// Gets static as well as dynamic items from Stream Cinema database. + /// + protected sealed 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 }; + + // Static items + if (string.IsNullOrEmpty(query.SearchTerm)) + { + foreach (BaseItem i in GetFilterItems()) + items.Add(i); + int staticCount = items.Count; + if (offset <= items.Count) + { + items.RemoveRange(0, offset); + limit -= items.Count; + offset = 0; + } + else + { + items.Clear(); + offset -= staticCount; + } + } + + // Filtered content items + FilterSortBy sortBy; + ItemOrder sortDir; + if (query.OrderBy.Count == 0) { + sortBy = FilterSortBy.Title; + sortDir = ItemOrder.Ascending; + } else { + (ItemSortBy sortByJ, SortOrder sortDirJ) = query.OrderBy.First(); + sortBy = ConvertSort(sortByJ); + sortDir = sortDirJ == SortOrder.Ascending ? ItemOrder.Ascending : ItemOrder.Descending; + } + + FilterResponse? filterRes = Metadata.SearchAsync(query.SearchTerm ?? "", order: sortDir, sort: sortBy, type: ItemType, 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 (TryCreateMediaItem(i._id, i._source, this, out BaseItem? a)) + items.Add(a); + } + } + + return result; + } + + private static FilterSortBy ConvertSort(ItemSortBy sortByJ) + { + switch (sortByJ) { + default: + case ItemSortBy.Default: return FilterSortBy.Title; + case ItemSortBy.AiredEpisodeOrder: return FilterSortBy.Premiered; + //case ItemSortBy.Album: return + //case ItemSortBy.AlbumArtist: return + //case ItemSortBy.Artist: return + case ItemSortBy.DateCreated: return FilterSortBy.DateAdded; + case ItemSortBy.OfficialRating: return FilterSortBy.Score; + case ItemSortBy.DatePlayed: return FilterSortBy.LastSeen; + case ItemSortBy.PremiereDate: return FilterSortBy.Premiered; + case ItemSortBy.StartDate: return FilterSortBy.Premiered; + case ItemSortBy.SortName: return FilterSortBy.Title; + case ItemSortBy.Name: return FilterSortBy.Title; + case ItemSortBy.Random: return FilterSortBy.Trending; + //case ItemSortBy.Runtime: return + case ItemSortBy.CommunityRating: return FilterSortBy.Popularity; + case ItemSortBy.ProductionYear: return FilterSortBy.Year; + case ItemSortBy.PlayCount: return FilterSortBy.PlayCount; + case ItemSortBy.CriticRating: return FilterSortBy.Score; + //case ItemSortBy.IsFolder: + //case ItemSortBy.IsUnplayed: + //case ItemSortBy.IsPlayed: + case ItemSortBy.SeriesSortName: return FilterSortBy.Title; + //case ItemSortBy.VideoBitRate: + //case ItemSortBy.AirTime: + //case ItemSortBy.Studio: + //case ItemSortBy.IsFavoriteOrLiked: + case ItemSortBy.DateLastContentAdded: return FilterSortBy.LastChildrenDateAdded; + case ItemSortBy.SeriesDatePlayed: return FilterSortBy.LastChildPremiered; + //case ItemSortBy.ParentIndexNumber: + //case ItemSortBy.IndexNumber: + case ItemSortBy.SimilarityScore: return FilterSortBy.Score; + case ItemSortBy.SearchScore: return FilterSortBy.Score; + } + } + + /// + /// Gets an already persisted instance or creates a new instance of the given child filter folder. + /// + /// Type of the folder. + /// Culture localized name of the folder. + protected T CreateChildFolder(string localizedName) where T : StreamCinemaFilterFolder, new() + { + return CreateFilterFolderInternal(this, localizedName); + } + /// + /// Gets an already persisted instance or creates a new instance of the given filter folder. + /// + /// Type of the folder. + /// Parent folder. + /// Culture localized name of the folder. + internal static T CreateRootFilterFolder(string localizedName) where T : StreamCinemaRootFolder, new() + { + return CreateFilterFolderInternal(null, localizedName); + } + + private static T CreateFilterFolderInternal(StreamCinemaFilterFolder? parent, string localizedName) where T : StreamCinemaFilterFolder, new() + { + Guid folderId = StreamCinemaHost.LibraryManager.GetNewItemId("folder", typeof(T)); + string folderPath = GetInternalMetadataPath(StreamCinemaHost.InternalMetadataPath!, folderId); + Directory.CreateDirectory(folderPath); + + T? folder = StreamCinemaHost.LibraryManager.GetItemById(folderId) as T; + bool isNew; + bool forceUpdate = false; + if (isNew = folder == null) + { + folder = new T + { + Id = folderId, + Name = localizedName, + DateCreated = StreamCinemaHost.FileSystem.GetCreationTimeUtc(folderPath), + DateModified = StreamCinemaHost.FileSystem.GetLastWriteTimeUtc(folderPath) + }; + } + + folder.Id = folderId; + folder.Path = folderPath; + if (parent == null) + { + folder.ParentId = Guid.Empty; + folder.IsRoot = true; + StreamCinemaHost.LibraryManager.RootFolder.AddVirtualChild(folder); + } + else + { + folder.ParentId = parent.Id; + } + + if (isNew) + { + folder.OnMetadataChanged(); + StreamCinemaHost.LibraryManager.CreateItem(folder, parent); + } + + folder.RefreshMetadata( + new MetadataRefreshOptions(new DirectoryService(StreamCinemaHost.FileSystem)) { ForceSave = !isNew && forceUpdate }, + default + ).GetAwaiter().GetResult(); + + return folder; + } + + /// + /// Tries to create a Jellyfin media item from the provided Cinema Stream media item. + /// + /// Cinema Stream media identifier. + /// Cinema Stream metadata. + /// Jellyfin parent folder. + /// On success the created item. + /// True on success, false otherwise. + private bool TryCreateMediaItem(string csId, MediaSource? media, BaseItem parentFolder, [NotNullWhen(true)] out BaseItem? item) + { + if (media == null) + { + item = null; + return false; + } + + var parentFolderId = parentFolder.Id; + + bool isNew; + bool forceUpdate = false; + + bool isAudio = media.is_concert ?? false; + if (media.children_count != 0) + { + // Series, season or a music album + FilterResponse? res = Metadata.ChildrenAsync(csId, sort: FilterSortBy.Episode).GetAwaiter().GetResult(); + if (res == null || res.hits == null || res.hits.hits == null || res.hits.hits.Count == 0) + { + item = null; + return false; + } + + bool singleLevel = res.hits.hits[0]._source?.children_count == 0; + if (isAudio) + item = GetMediaItemById(csId, out isNew); + else if (singleLevel) + item = GetMediaItemById(csId, out isNew); + else + item = GetMediaItemById(csId, out isNew); + + } + else if (isAudio) + { + // TODO determine if this is an AudioBook od Audio + item = GetMediaItemById(csId, out isNew); + + } + else if (media.info_labels?.mediatype == "tvshow") + { + item = GetMediaItemById(csId, out isNew); + + } + else + { + item = GetMediaItemById(csId, out isNew); + } + + if (isNew && media.info_labels != null) + { + item.RunTimeTicks = (long?)(media.info_labels.duration * TimeSpan.TicksPerSecond); + } + + if (isNew) + { + InfoLabelI18n? loc = GetLocalized(media); + item.Name = loc?.title ?? media.info_labels?.originaltitle; + item.Genres = media.info_labels?.genre?.ToArray(); + item.Studios = media.info_labels?.studio?.ToArray(); + item.CommunityRating = (float?)media.ratings?.overall?.rating; + item.Overview = loc?.plot; + // TODO + // item.IndexNumber = info.IndexNumber; + //item.ParentIndexNumber = info.ParentIndexNumber; + item.PremiereDate = media.info_labels?.premiered; + item.ProductionYear = media.info_labels?.year; + item.ProviderIds = media.services != null ? ConvertProviderIds(media.services) : null; + //item.OfficialRating = info.OfficialRating; + item.DateCreated = media.info_labels?.dateadded ?? DateTime.UtcNow; + //item.Tags = info.Tags.ToArray(); + item.OriginalTitle = media.info_labels?.originaltitle; + + string? artUriS = loc?.art?.poster ?? loc?.art?.fanart; + Uri? artUri; + if (artUriS != null) + { + artUri = new Uri(artUriS); + if (Metadata.TryGetThumbnail(artUri, 128, 128, out Uri? artThumbUri)) + { + item.SetImagePath(ImageType.Thumb, artThumbUri.ToString()); + } + item.SetImagePath(ImageType.Primary, artUri.ToString()); + } + } + + if (item is IHasArtist hasArtists) + { + hasArtists.Artists = media.info_labels?.writer; + } + + if (item is IHasAlbumArtist hasAlbumArtists) + { + hasAlbumArtists.AlbumArtists = media.info_labels?.director; + } + + item.ParentId = parentFolderId; + + /* + if (item is IHasSeries hasSeries) + { + if (!string.Equals(hasSeries.SeriesName, info.SeriesName, StringComparison.OrdinalIgnoreCase)) + { + forceUpdate = true; + _logger.LogDebug("Forcing update due to SeriesName {0}", item.Name); + } + + hasSeries.SeriesName = info.SeriesName; + }*/ + + item.ExternalId = csId; + + /* + if (item is Audio channelAudioItem) + { + channelAudioItem.ExtraType = info.ExtraType; + + var mediaSource = info.MediaSources.FirstOrDefault(); + item.Path = mediaSource?.Path; + } + + if (item is Video channelVideoItem) + { + channelVideoItem.ExtraType = info.ExtraType; + + var mediaSource = info.MediaSources.FirstOrDefault(); + item.Path = mediaSource?.Path; + }*/ + + item.OnMetadataChanged(); + + if (isNew) + { + // HACK: We use RegisterItem that is volatile intead of CreateItem + //_libraryManager.CreateItem(item, parentFolder); + StreamCinemaHost.LibraryManager.RegisterItem(item); + + if (media.cast != null && media.cast.Count > 0) + { + //await _libraryManager.UpdatePeopleAsync(item, info.People, cancellationToken).ConfigureAwait(false); + } + } + else if (forceUpdate) + { + // HACK our items are volatile + //item.UpdateToRepositoryAsync(ItemUpdateType.None, default).GetAwaiter().GetResult(); + } + + /* + if ((isNew || forceUpdate) && info.Type == ChannelItemType.Media) + { + if (enableMediaProbe && !info.IsLiveStream && item.HasPathProtocol) + { + await SaveMediaSources(item, new List()).ConfigureAwait(false); + } + else + { + await SaveMediaSources(item, info.MediaSources).ConfigureAwait(false); + } + }*/ + + /* + if (isNew || forceUpdate || item.DateLastRefreshed == default) + { + _providerManager.QueueRefresh(item.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.Normal); + }*/ + + return true; + } + + private InfoLabelI18n? GetLocalized(MediaSource media) + { + if (media.i18n_info_labels == null) + return null; + InfoLabelI18n? first = null; + InfoLabelI18n? preferred = null; + InfoLabelI18n? fallback = null; + foreach (InfoLabelI18n i in media.i18n_info_labels) + if (i.lang == PreferredCulture) + preferred = i; + else if (i.lang == FallbackCulture) + fallback = i; + else if (first == null) + first = i; + + return preferred ?? fallback ?? first; + } + + private static Dictionary ConvertProviderIds(ServicesIds services) + { + Dictionary result = new Dictionary(); + if (services.imdb != null) + result.Add("Imdb", services.imdb); + if (services.tvdb != null) + result.Add("Tvdb", services.tvdb); + if (services.tmdb != null) + result.Add("Tmdb", services.tmdb); + return result; + + //public string? csfd { get; set; } + //public string? trakt { get; set; } + //public string? trakt_with_type { get; set; } + //public string? slug { get; set; } + } + + internal static string GetInternalMetadataPath(string basePath, Guid id) + { + return System.IO.Path.Combine(basePath, "streamcinema", id.ToString("N", CultureInfo.InvariantCulture), "metadata"); + } + + private T GetMediaItemById(string idString, out bool isNew) + where T : BaseItem, new() + { + Guid id = StreamCinemaHost.LibraryManager.GetNewItemId(idString, typeof(StreamCinemaPlugin)); + T? item = StreamCinemaHost.LibraryManager.GetItemById(id) as T; + + if (item == null) + { + item = new T(); + isNew = true; + } + else + { + isNew = false; + } + + item.Id = id; + return item; + } } \ No newline at end of file diff --git a/StreamCinemaJellyfin/StreamCinemaHost.cs b/StreamCinemaJellyfin/StreamCinemaHost.cs index 57c6110..0715ea6 100644 --- a/StreamCinemaJellyfin/StreamCinemaHost.cs +++ b/StreamCinemaJellyfin/StreamCinemaHost.cs @@ -12,82 +12,48 @@ namespace Jellyfin.Plugin.StreamCinema; /// public sealed class StreamCinemaHost : IHostedService { - private readonly ILibraryManager _libraryManager; - private readonly IServerConfigurationManager _config; - private readonly IFileSystem _fileSystem; - private readonly ILogger _logger; +#pragma warning disable CS8618 + // This instance is specially registered and gets created before all classes + // except StreamCinemaSeccviceRegistrator and StreamCinemaPlugin. + private static ILibraryManager _libraryManager; + private static IServerConfigurationManager _config; + private static IFileSystem _fileSystem; +#pragma warning restore CS8618 + private readonly ILogger _logger; - /// - /// Initializes a the Stream Cinema plugin. - /// - public StreamCinemaHost(ILibraryManager libraryManager, IServerConfigurationManager config, IFileSystem fileSystem, ILogger logger) - { - this._libraryManager = libraryManager; - this._config = config; - this._fileSystem = fileSystem; - this._logger = logger; - } + /// + /// Initializes a the Stream Cinema plugin. + /// + public StreamCinemaHost(ILibraryManager libraryManager, IServerConfigurationManager config, IFileSystem fileSystem, ILogger logger) + { + _libraryManager = libraryManager; + _config = config; + _fileSystem = fileSystem; + this._logger = logger; + } - /// - public Task StartAsync(CancellationToken cancellationToken) - { - // Make sure the Stream Cinema root folders are created - CreateRoot("movies", "Movies", _libraryManager, _config, _fileSystem); + public static ILibraryManager LibraryManager => _libraryManager; - /* + public static string InternalMetadataPath  => _config.ApplicationPaths.InternalMetadataPath; + + public static IFileSystem FileSystem => _fileSystem; + + /// + public Task StartAsync(CancellationToken cancellationToken) + { + // Make sure the Stream Cinema root folders are created + StreamCinemaFilterFolder.CreateRootFilterFolder("Movies"); + + /* pluginItems.Add(CreateMenuItem("movies", Resources.Movies, GetResourceUrl("movies.png"))); pluginItems.Add(CreateMenuItem("shows", Resources.Shows, GetResourceUrl("tvshows.png"))); pluginItems.Add(CreateMenuItem("tv", Resources.TvProgram, GetResourceUrl("tv-program.png"))); pluginItems.Add(CreateMenuItem("anime", Resources.Anime, GetResourceUrl("anime.png"))); pluginItems.Add(CreateMenuItem("concerts", Resources.Concerts, GetResourceUrl("music.png")));*/ - return Task.CompletedTask; - } + return Task.CompletedTask; + } - /// - public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; - - private static void CreateRoot(string idPrefix, string localizedName, ILibraryManager libraryManager, IServerConfigurationManager config, IFileSystem fileSystem) where T : StreamCinemaRootFolder, new() - { - string internalMetadataPath = config.ApplicationPaths.InternalMetadataPath; - - Guid rootFolderId = libraryManager.GetNewItemId(StreamCinemaRootFolder.GetIdToHash("", idPrefix), typeof(StreamCinemaRootFolder)); - string rootFolderPath = StreamCinemaRootFolder.GetInternalMetadataPath(internalMetadataPath, rootFolderId); - Directory.CreateDirectory(rootFolderPath); - StreamCinemaRootFolder? rootFolder = libraryManager.GetItemById(rootFolderId) as StreamCinemaRootFolder; - bool isNew; - bool forceUpdate = false; - if (isNew = rootFolder == null) - { - rootFolder = new T - { - Name = localizedName, - Id = rootFolderId, - DateCreated = fileSystem.GetCreationTimeUtc(rootFolderPath), - DateModified = fileSystem.GetLastWriteTimeUtc(rootFolderPath) - }; - } - - rootFolder.Initialize(libraryManager, idPrefix, internalMetadataPath); - rootFolder.Id = rootFolderId; // seems to get lost on existing item - rootFolder.Path = rootFolderPath; - rootFolder.ParentId = Guid.Empty; - rootFolder.IsRoot = true; - - if (isNew) - { - rootFolder.OnMetadataChanged(); - libraryManager.CreateItem(rootFolder, null); - } - - // We do not bother waiting for the task to finish - rootFolder.RefreshMetadata( - new MetadataRefreshOptions(new DirectoryService(fileSystem)) - { - ForceSave = !isNew && forceUpdate - }, - default).ConfigureAwait(false); - - libraryManager.RootFolder.AddVirtualChild(rootFolder); - } + /// + public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; } diff --git a/StreamCinemaJellyfin/StreamCinemaMoviesFolder.cs b/StreamCinemaJellyfin/StreamCinemaMoviesFolder.cs index b6614f5..fab8aba 100644 --- a/StreamCinemaJellyfin/StreamCinemaMoviesFolder.cs +++ b/StreamCinemaJellyfin/StreamCinemaMoviesFolder.cs @@ -1,45 +1,39 @@ using Jellyfin.Data.Enums; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; +using StreamCinemaLib.API; namespace Jellyfin.Plugin.StreamCinema; sealed class StreamCinemaMoviesFolder : StreamCinemaRootFolder { - private BaseItem? _trending; - private BaseItem? _popular; - private BaseItem? _mostWatched; - private BaseItem? _newReleases; + private readonly BaseItem _trending; + private readonly BaseItem _popular; + private readonly BaseItem _mostWatched; + private readonly BaseItem _newReleases; - internal override void Initialize(ILibraryManager libraryManager, string idPrefix, string internalMetadataPath) - { - base.Initialize(libraryManager, idPrefix, internalMetadataPath); + public StreamCinemaMoviesFolder() + { + this._trending = CreateChildFolder("Trending"); + this._popular = CreateChildFolder("Popular"); + //this._mostWatched = CreateFilterFolder("mostWatched", this, "Most Watched", "watched.png"); + //this._newReleases = CreateFilterFolder("newReleases", this, "New Releases", "new.png"); + } - this._trending = CreateFilterFolder("trending", this, "Trending"); - this._popular = CreateFilterFolder("popular", this, "Popular"); - //this._mostWatched = CreateFilterFolder("mostWatched", this, "Most Watched", "watched.png"); - //this._newReleases = CreateFilterFolder("newReleases", this, "New Releases", "new.png"); - } + public override CollectionType? CollectionType => Data.Enums.CollectionType.movies; - public override CollectionType? CollectionType => Data.Enums.CollectionType.movies; + public override BaseItemKind ClientType => BaseItemKind.Movie; - public override BaseItemKind ClientType => BaseItemKind.Movie; + public override ItemType ItemType => ItemType.Movie; - internal override string ImageName => "movies.png"; + internal override string ImageName => "movies.png"; - protected override IEnumerable GetFilterItems(string path) - { - if (_trending == null) - // Not yet initialized - yield break; - - if (path.Length == 0) - { - // Root items - yield return _trending; - yield return _popular!; - //yield return _mostWatched!; - //yield return _newReleases!; - } - } + protected override IEnumerable GetFilterItems() + { + // Root items + yield return _trending; + yield return _popular!; + //yield return _mostWatched!; + //yield return _newReleases!; + } } \ No newline at end of file diff --git a/StreamCinemaJellyfin/StreamCinemaPlugin.cs b/StreamCinemaJellyfin/StreamCinemaPlugin.cs index 243c4a4..6ac2671 100644 --- a/StreamCinemaJellyfin/StreamCinemaPlugin.cs +++ b/StreamCinemaJellyfin/StreamCinemaPlugin.cs @@ -18,7 +18,6 @@ public class StreamCinemaPlugin : BasePlugin, I public StreamCinemaPlugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer) : base(applicationPaths, xmlSerializer) { - Instance = this; } /// @@ -30,12 +29,6 @@ public class StreamCinemaPlugin : BasePlugin, I /// public override string Description => "Videodoplněk obsahující rozsáhlou databázi filmů a seriálů."; - /// - /// Gets the instance. - /// - /// The instance. - public static StreamCinemaPlugin Instance { get; private set; } - /// public IEnumerable GetPages() { diff --git a/StreamCinemaJellyfin/StreamCinemaPopularFolder.cs b/StreamCinemaJellyfin/StreamCinemaPopularFolder.cs index 0902133..b3ba949 100644 --- a/StreamCinemaJellyfin/StreamCinemaPopularFolder.cs +++ b/StreamCinemaJellyfin/StreamCinemaPopularFolder.cs @@ -1,3 +1,5 @@ +using Jellyfin.Data.Enums; +using MediaBrowser.Controller.Entities; using StreamCinemaLib.API; namespace Jellyfin.Plugin.StreamCinema; @@ -6,5 +8,14 @@ public sealed class StreamCinemaPopularFolder : StreamCinemaSortFolder { internal override string ImageName => "popular.png"; + public override BaseItemKind ClientType => BaseItemKind.Movie; + + public override ItemType ItemType => ItemType.Movie; + internal override FilterSortBy SortBy { get { return FilterSortBy.Popularity; } } + + protected override IEnumerable GetFilterItems() + { + yield break; + } } \ No newline at end of file diff --git a/StreamCinemaJellyfin/StreamCinemaRootFolder.cs b/StreamCinemaJellyfin/StreamCinemaRootFolder.cs index afcd1e6..a303077 100644 --- a/StreamCinemaJellyfin/StreamCinemaRootFolder.cs +++ b/StreamCinemaJellyfin/StreamCinemaRootFolder.cs @@ -13,644 +13,209 @@ using StreamCinemaLib.API; namespace Jellyfin.Plugin.StreamCinema; +/// +/// Stream Cinema root folder that is displayed identically to user-defined libraries. +/// public abstract class StreamCinemaRootFolder : StreamCinemaFilterFolder, ICollectionFolder { - private const int PageLimit = 30; - private const string PreferredCulture = "cs"; - private const string FallbackCulture = "en"; - - private ILibraryManager? _libraryManager; - private string? _idPrefix; - private string? _internalMetadataPath; - - internal virtual void Initialize(ILibraryManager libraryManager, string idPrefix, string internalMetadataPath) - { - this._libraryManager = libraryManager; - this._idPrefix = idPrefix; - this._internalMetadataPath = internalMetadataPath; - } - - public abstract CollectionType? CollectionType { get; } - - public abstract BaseItemKind ClientType { get; } - - /// - /// Gets the static filter folders either directly from the root or nested filter folder. - /// - /// Path to folder whose filter items to get (empty string for root). - protected abstract IEnumerable GetFilterItems(string path); - - public sealed override string GetClientTypeName() - { - return ClientType.ToString(); - } - - protected sealed override QueryResult GetItemsInternal(InternalItemsQuery query) - { - if (_libraryManager == null) - // We were not properly initialized yet so refuse to work - return new QueryResult() { Items = new List() }; - - BaseItem? parentItem = query.ParentId.IsEmpty() ? this : _libraryManager.GetItemById(query.ParentId); - if (parentItem == null) - throw new InvalidOperationException("StreamCinema parent folder is missing."); - - string folderId = parentItem?.ExternalId ?? ""; - int offset = query.StartIndex ?? 0; - int limit = query.Limit ?? PageLimit; - - List items = new List(); - QueryResult result = new QueryResult() { Items = items, StartIndex = offset }; - - // Static items - if (string.IsNullOrEmpty(query.SearchTerm)) - { - foreach (BaseItem i in GetFilterItems(folderId)) - items.Add(i); - int staticCount = items.Count; - if (offset <= items.Count) - { - items.RemoveRange(0, offset); - limit -= items.Count; - offset = 0; - } - else - { - items.Clear(); - offset -= staticCount; - } - } - - // Filtered content items - FilterResponse? filterRes = Metadata.SearchAsync(query.SearchTerm ?? "", 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 (TryCreateMediaItem(i._id, i._source, parentItem, out BaseItem? a)) - items.Add(a); - } - } - - return result; - } - - protected BaseItem CreateFilterFolder(string id, StreamCinemaFilterFolder parent, string localizedName) where T : StreamCinemaFilterFolder, new() - { - if (_libraryManager == null) - throw new InvalidOperationException(); - - Guid folderId = _libraryManager.GetNewItemId(GetIdToHash(id, _idPrefix!), typeof(StreamCinemaRootFolder)); - string folderPath = GetInternalMetadataPath(_internalMetadataPath!, folderId); - T? folder = _libraryManager.GetItemById(folderId) as T; - bool isNew; - bool forceUpdate = false; - if (isNew = folder == null) - { - folder = new T - { - Id = folderId, - Name = localizedName, - ExternalId = id, - //DateCreated = fileSystem.GetCreationTimeUtc(rootFolderPath), - //DateModified = fileSystem.GetLastWriteTimeUtc(rootFolderPath) - }; - } - - folder.Path = folderPath; - folder.ParentId = parent.Id; - - if (isNew) - { - folder.OnMetadataChanged(); - _libraryManager.CreateItem(folder, parent); - } - - return folder; - } - - private bool TryCreateMediaItem(string csId, MediaSource? media, BaseItem parentFolder, [NotNullWhen(true)] out BaseItem? item) - { - if (media == null) - { - item = null; - return false; - } - - var parentFolderId = parentFolder.Id; - - bool isNew; - bool forceUpdate = false; - - bool isAudio = media.is_concert ?? false; - if (media.children_count != 0) - { - // Series, season or a music album - FilterResponse? res = Metadata.ChildrenAsync(csId, sort: FilterSortBy.Episode).GetAwaiter().GetResult(); - if (res == null || res.hits == null || res.hits.hits == null || res.hits.hits.Count == 0) - { - item = null; - return false; - } - - bool singleLevel = res.hits.hits[0]._source?.children_count == 0; - if (isAudio) - item = GetItemById(csId, out isNew); - else if (singleLevel) - item = GetItemById(csId, out isNew); - else - item = GetItemById(csId, out isNew); - - } - else if (isAudio) - { - // TODO determine if this is an AudioBook od Audio - item = GetItemById(csId, out isNew); - - } - else if (media.info_labels?.mediatype == "tvshow") - { - item = GetItemById(csId, out isNew); - - } - else - { - item = GetItemById(csId, out isNew); - } - - if (isNew && media.info_labels != null) - { - item.RunTimeTicks = (long?)(media.info_labels.duration * TimeSpan.TicksPerSecond); - } - - if (isNew) - { - InfoLabelI18n? loc = GetLocalized(media); - item.Name = loc?.title ?? media.info_labels?.originaltitle; - item.Genres = media.info_labels?.genre?.ToArray(); - item.Studios = media.info_labels?.studio?.ToArray(); - item.CommunityRating = (float?)media.ratings?.overall?.rating; - item.Overview = loc?.plot; - // TODO - // item.IndexNumber = info.IndexNumber; - //item.ParentIndexNumber = info.ParentIndexNumber; - item.PremiereDate = media.info_labels?.premiered; - item.ProductionYear = media.info_labels?.year; - item.ProviderIds = media.services != null ? ConvertProviderIds(media.services) : null; - //item.OfficialRating = info.OfficialRating; - item.DateCreated = media.info_labels?.dateadded ?? DateTime.UtcNow; - //item.Tags = info.Tags.ToArray(); - item.OriginalTitle = media.info_labels?.originaltitle; - - string? artUriS = loc?.art?.poster ?? loc?.art?.fanart; - Uri? artUri; - if (artUriS != null) - { - artUri = new Uri(artUriS); - if (Metadata.TryGetThumbnail(artUri, 128, 128, out Uri? artThumbUri)) - { - item.SetImagePath(ImageType.Thumb, artThumbUri.ToString()); - } - item.SetImagePath(ImageType.Primary, artUri.ToString()); - } - } - - if (item is IHasArtist hasArtists) - { - hasArtists.Artists = media.info_labels?.writer; - } - - if (item is IHasAlbumArtist hasAlbumArtists) - { - hasAlbumArtists.AlbumArtists = media.info_labels?.director; - } - - item.ParentId = parentFolderId; - - /* - if (item is IHasSeries hasSeries) - { - if (!string.Equals(hasSeries.SeriesName, info.SeriesName, StringComparison.OrdinalIgnoreCase)) - { - forceUpdate = true; - _logger.LogDebug("Forcing update due to SeriesName {0}", item.Name); - } - - hasSeries.SeriesName = info.SeriesName; - }*/ - - item.ExternalId = csId; - - /* - if (item is Audio channelAudioItem) - { - channelAudioItem.ExtraType = info.ExtraType; - - var mediaSource = info.MediaSources.FirstOrDefault(); - item.Path = mediaSource?.Path; - } - - if (item is Video channelVideoItem) - { - channelVideoItem.ExtraType = info.ExtraType; - - var mediaSource = info.MediaSources.FirstOrDefault(); - item.Path = mediaSource?.Path; - }*/ - - item.OnMetadataChanged(); - - if (isNew) - { - // HACK: We use RegisterItem that is volatile intead of CreateItem - //_libraryManager.CreateItem(item, parentFolder); - _libraryManager.RegisterItem(item); - - if (media.cast != null && media.cast.Count > 0) - { - //await _libraryManager.UpdatePeopleAsync(item, info.People, cancellationToken).ConfigureAwait(false); - } - } - else if (forceUpdate) - { - // HACK our items are volatile - //item.UpdateToRepositoryAsync(ItemUpdateType.None, default).GetAwaiter().GetResult(); - } - - /* - if ((isNew || forceUpdate) && info.Type == ChannelItemType.Media) - { - if (enableMediaProbe && !info.IsLiveStream && item.HasPathProtocol) - { - await SaveMediaSources(item, new List()).ConfigureAwait(false); - } - else - { - await SaveMediaSources(item, info.MediaSources).ConfigureAwait(false); - } - }*/ - - /* - if (isNew || forceUpdate || item.DateLastRefreshed == default) - { - _providerManager.QueueRefresh(item.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.Normal); - }*/ - - return true; - } - - private InfoLabelI18n? GetLocalized(MediaSource media) - { - if (media.i18n_info_labels == null) - return null; - InfoLabelI18n? first = null; - InfoLabelI18n? preferred = null; - InfoLabelI18n? fallback = null; - foreach (InfoLabelI18n i in media.i18n_info_labels) - if (i.lang == PreferredCulture) - preferred = i; - else if (i.lang == FallbackCulture) - fallback = i; - else if (first == null) - first = i; - - return preferred ?? fallback ?? first; - } - - private static Dictionary ConvertProviderIds(ServicesIds services) - { - Dictionary result = new Dictionary(); - if (services.imdb != null) - result.Add("Imdb", services.imdb); - if (services.tvdb != null) - result.Add("Tvdb", services.tvdb); - if (services.tmdb != null) - result.Add("Tmdb", services.tmdb); - return result; - - //public string? csfd { get; set; } - //public string? trakt { get; set; } - //public string? trakt_with_type { get; set; } - //public string? slug { get; set; } - } - - internal static string GetInternalMetadataPath(string basePath, Guid id) - { - return System.IO.Path.Combine(basePath, "streamcinema", id.ToString("N", CultureInfo.InvariantCulture), "metadata"); - } - - private T GetItemById(string idString, out bool isNew) - where T : BaseItem, new() - { - if (_libraryManager == null) - throw new InvalidOperationException(); - - var id = _libraryManager.GetNewItemId(GetIdToHash(idString, _idPrefix!), typeof(T)); - - T? item = null; - - //try - //{ - item = _libraryManager.GetItemById(id) as T; - //} - //catch (Exception ex) - //{ - // _logger.LogError(ex, "Error retrieving channel item from database"); - //} - - if (item == null) - { - item = new T(); - isNew = true; - } - else - { - isNew = false; - } - - item.Id = id; - return item; - } - - internal static string GetIdToHash(string externalId, string idPrefix) - { - // Increment this as needed to force new downloads - return "StreamCinema-" + idPrefix + "-" + externalId; - } - - - - /* - public async Task GetChannelItems(InternalChannelItemQuery query, CancellationToken cancellationToken) - { - if (string.IsNullOrWhiteSpace(query.FolderId)) - { - // Main Menu - List pluginItems = new List(); - return new ChannelItemResult() { Items = pluginItems }; - } - - /* - if (query.FolderId.StartsWith("series_", StringComparison.OrdinalIgnoreCase)) - { - var hash = query.FolderId.Split('_')[1]; - return await GetChannelItems(query, i => i.IsSeries && string.Equals(i.Name.GetMD5().ToString("N"), hash, StringComparison.Ordinal), cancellationToken); - } - - if (string.Equals(query.FolderId, "kids", StringComparison.OrdinalIgnoreCase)) - { - return await GetChannelItems(query, i => i.IsKids, cancellationToken); - } - - if (string.Equals(query.FolderId, "movies", StringComparison.OrdinalIgnoreCase)) - { - return await GetChannelItems(query, i => i.IsMovie, cancellationToken); - } - - if (string.Equals(query.FolderId, "news", StringComparison.OrdinalIgnoreCase)) - { - return await GetChannelItems(query, i => i.IsNews, cancellationToken); - } - - if (string.Equals(query.FolderId, "sports", StringComparison.OrdinalIgnoreCase)) - { - return await GetChannelItems(query, i => i.IsSports, cancellationToken); - } - - if (string.Equals(query.FolderId, "others", StringComparison.OrdinalIgnoreCase)) - { - return await GetChannelItems(query, i => !i.IsSports && !i.IsNews && !i.IsMovie && !i.IsKids && !i.IsSeries, cancellationToken); - }*/ - /* - var result = new ChannelItemResult() { Items = new List() }; - - return result; - } - - public Task GetChannelImage(ImageType type, CancellationToken cancellationToken) - { - if (type == ImageType.Primary) - { - return Task.FromResult(new DynamicImageResponse { Path = "https://streamcinema.cz/assets/logo-iqdz28nk.png", Protocol = MediaProtocol.Http, HasImage = true }); - } - - return Task.FromResult(new DynamicImageResponse { HasImage = false }); - } - - public IEnumerable GetSupportedChannelImages() - { - return new List { ImageType.Primary }; - } - - - #region IHasCacheKey Members - - public string GetCacheKey(string userId) - { - DateTimeOffset dto = LiveTvService.Instance.RecordingModificationTime; - return $"{dto.ToUnixTimeSeconds()}-{_cacheKeyBase}"; - } - - #endregion - - #region ISupportsLatestMedia Members - - public async Task> GetLatestMedia(ChannelLatestMediaSearch request, CancellationToken cancellationToken) - { - var result = await GetChannelItems(new InternalChannelItemQuery(), _ => true, cancellationToken).ConfigureAwait(false); - - return result.Items.OrderByDescending(i => i.DateCreated ?? DateTime.MinValue); - } - - #endregion - - #region IHasFolderAttributes Members - - #pragma warning disable CA1819 - public string[] Attributes => ["Recordings"]; - #pragma warning restore CA1819 - - #endregion - - private static ChannelItemInfo CreateMenuItem(string id, string localizedName, string? imageUrl = null) - { - return new ChannelItemInfo - { - Id = id, - Name = localizedName, - FolderType = ChannelFolderType.Container, - Type = ChannelItemType.Folder, - ImageUrl = imageUrl - }; - } - - private static string GetResourceUrl(string fileName) - { - return "__plugin/" + fileName; - } - - private void CleanCache(bool cleanAll = false) - { - if (!string.IsNullOrEmpty(_recordingCacheDirectory) && Directory.Exists(_recordingCacheDirectory)) - { - string[] cachedJson = Directory.GetFiles(_recordingCacheDirectory, "*.json"); - _logger.LogInformation("Cleaning JSON cache {CacheDirectory} {FileCount}", _recordingCacheDirectory, cachedJson.Length); - foreach (string fileName in cachedJson) - { - if (cleanAll || _fileSystem.GetLastWriteTimeUtc(fileName).Add(TimeSpan.FromHours(3)) <= DateTimeOffset.UtcNow) - { - _fileSystem.DeleteFile(fileName); - } - } - } - } - - private LiveTvService GetService() - { - LiveTvService service = LiveTvService.Instance; - if (service is not null && (!service.IsActive || _cachedRecordingModificationTime != Plugin.Instance.Configuration.RecordingModificationTime || service.FlagRecordingChange)) - { - try - { - CancellationToken cancellationToken = CancellationToken.None; - service.EnsureConnectionAsync(cancellationToken).Wait(); - if (service.IsActive) - { - _useCachedRecordings = false; - if (_cachedRecordingModificationTime != Plugin.Instance.Configuration.RecordingModificationTime) - { - _cachedRecordingModificationTime = Plugin.Instance.Configuration.RecordingModificationTime; - } - } - } - catch (Exception) - { - } - } - - return service; - } - - public async Task GetChannelItems(InternalChannelItemQuery query, Func filter, CancellationToken cancellationToken) - { - await GetRecordingsAsync("GetChannelItems", cancellationToken); - List pluginItems = new List(); - pluginItems.AddRange(_allRecordings.Where(filter).Select(ConvertToChannelItem)); - var result = new ChannelItemResult() { Items = pluginItems }; - - return result; - } - - private ChannelItemInfo ConvertToChannelItem(MyRecordingInfo item) - { - var path = string.IsNullOrEmpty(item.Path) ? item.Url : item.Path; - - var channelItem = new ChannelItemInfo - { - Name = string.IsNullOrEmpty(item.EpisodeTitle) ? item.Name : item.EpisodeTitle, - SeriesName = !string.IsNullOrEmpty(item.EpisodeTitle) || item.IsSeries ? item.Name : null, - StartDate = item.StartDate, - EndDate = item.EndDate, - OfficialRating = item.OfficialRating, - CommunityRating = item.CommunityRating, - ContentType = item.IsMovie ? ChannelMediaContentType.Movie : ChannelMediaContentType.Episode, - Genres = item.Genres, - ImageUrl = item.ImageUrl, - Id = item.Id, - ParentIndexNumber = item.SeasonNumber, - IndexNumber = item.EpisodeNumber, - MediaType = item.ChannelType == ChannelType.TV ? ChannelMediaType.Video : ChannelMediaType.Audio, - MediaSources = new List - { - new MediaSourceInfo - { - Path = path, - Container = item.Status == RecordingStatus.InProgress ? "ts" : null, - Protocol = path.StartsWith("http", StringComparison.OrdinalIgnoreCase) ? MediaProtocol.Http : MediaProtocol.File, - BufferMs = 1000, - AnalyzeDurationMs = 0, - IsInfiniteStream = item.Status == RecordingStatus.InProgress, - TranscodingContainer = "ts", - RunTimeTicks = item.Status == RecordingStatus.InProgress ? null : (item.EndDate - item.StartDate).Ticks, - } - }, - PremiereDate = item.OriginalAirDate, - ProductionYear = item.ProductionYear, - Type = ChannelItemType.Media, - DateModified = item.Status == RecordingStatus.InProgress ? DateTime.Now : Plugin.Instance.Configuration.RecordingModificationTime, - Overview = item.Overview, - IsLiveStream = item.Status != RecordingStatus.InProgress ? false : Plugin.Instance.Configuration.EnableInProgress, - Etag = item.Status.ToString() - }; - - return channelItem; - } - - private async Task GetRecordingsAsync(string name, CancellationToken cancellationToken) - { - var service = GetService(); - if (service is null || !service.IsActive) - { - return false; - } - - if (_useCachedRecordings == false || service.FlagRecordingChange) - { - if (_pollInterval == -1) - { - var interval = TimeSpan.FromSeconds(Plugin.Instance.Configuration.PollInterval); - _updateTimer = new Timer(OnUpdateTimerCallbackAsync, null, TimeSpan.FromMinutes(2), interval); - if (_updateTimer != null) - { - _pollInterval = Plugin.Instance.Configuration.PollInterval; - } - } - - if (await _semaphore.WaitAsync(30000, cancellationToken)) - { - try - { - _logger.LogDebug("{0} Reload cache", name); - _allRecordings = await service.GetAllRecordingsAsync(cancellationToken).ConfigureAwait(false); - int maxId = _allRecordings.Max(r => int.Parse(r.Id, CultureInfo.InvariantCulture)); - int inProcessCount = _allRecordings.Count(r => r.Status == RecordingStatus.InProgress); - string keyBase = $"{maxId}-{inProcessCount}-{_allRecordings.Count()}"; - if (keyBase != _cacheKeyBase && !service.FlagRecordingChange) - { - _logger.LogDebug("External recording list change {0}", keyBase); - CleanCache(true); - } - - _cacheKeyBase = keyBase; - _lastUpdate = DateTimeOffset.UtcNow; - service.FlagRecordingChange = false; - _useCachedRecordings = true; - } - catch (Exception) - { - } - - _semaphore.Release(); - } - } - - return _useCachedRecordings; - } - - private async void OnUpdateTimerCallbackAsync(object state) - { - LiveTvService service = LiveTvService.Instance; - if (service is not null && service.IsActive) - { - var backendUpdate = await service.GetLastUpdate(_cancellationToken.Token).ConfigureAwait(false); - if (backendUpdate > _lastUpdate) - { - _logger.LogDebug("Recordings reset {0}", backendUpdate); - _useCachedRecordings = false; - await GetRecordingsAsync("OnUpdateTimerCallbackAsync", _cancellationToken.Token); - } - } - }*/ + + /// + /// Gets the item type for . Shall be kept in + /// sync with value for + /// + public abstract CollectionType? CollectionType { get; } + + + + + /* + + public Task GetChannelImage(ImageType type, CancellationToken cancellationToken) + { + if (type == ImageType.Primary) + { + return Task.FromResult(new DynamicImageResponse { Path = "https://streamcinema.cz/assets/logo-iqdz28nk.png", Protocol = MediaProtocol.Http, HasImage = true }); + } + + return Task.FromResult(new DynamicImageResponse { HasImage = false }); + } + + public IEnumerable GetSupportedChannelImages() + { + return new List { ImageType.Primary }; + } + + + #region IHasCacheKey Members + + public string GetCacheKey(string userId) + { + DateTimeOffset dto = LiveTvService.Instance.RecordingModificationTime; + return $"{dto.ToUnixTimeSeconds()}-{_cacheKeyBase}"; + } + + #endregion + + #region ISupportsLatestMedia Members + + public async Task> GetLatestMedia(ChannelLatestMediaSearch request, CancellationToken cancellationToken) + { + var result = await GetChannelItems(new InternalChannelItemQuery(), _ => true, cancellationToken).ConfigureAwait(false); + + return result.Items.OrderByDescending(i => i.DateCreated ?? DateTime.MinValue); + } + + #endregion + + #region IHasFolderAttributes Members + + #pragma warning disable CA1819 + public string[] Attributes => ["Recordings"]; + #pragma warning restore CA1819 + + #endregion + + + private static string GetResourceUrl(string fileName) + { + return "__plugin/" + fileName; + } + + private void CleanCache(bool cleanAll = false) + { + if (!string.IsNullOrEmpty(_recordingCacheDirectory) && Directory.Exists(_recordingCacheDirectory)) + { + string[] cachedJson = Directory.GetFiles(_recordingCacheDirectory, "*.json"); + _logger.LogInformation("Cleaning JSON cache {CacheDirectory} {FileCount}", _recordingCacheDirectory, cachedJson.Length); + foreach (string fileName in cachedJson) + { + if (cleanAll || _fileSystem.GetLastWriteTimeUtc(fileName).Add(TimeSpan.FromHours(3)) <= DateTimeOffset.UtcNow) + { + _fileSystem.DeleteFile(fileName); + } + } + } + } + + + public async Task GetChannelItems(InternalChannelItemQuery query, Func filter, CancellationToken cancellationToken) + { + await GetRecordingsAsync("GetChannelItems", cancellationToken); + List pluginItems = new List(); + pluginItems.AddRange(_allRecordings.Where(filter).Select(ConvertToChannelItem)); + var result = new ChannelItemResult() { Items = pluginItems }; + + return result; + } + + private ChannelItemInfo ConvertToChannelItem(MyRecordingInfo item) + { + var path = string.IsNullOrEmpty(item.Path) ? item.Url : item.Path; + + var channelItem = new ChannelItemInfo + { + Name = string.IsNullOrEmpty(item.EpisodeTitle) ? item.Name : item.EpisodeTitle, + SeriesName = !string.IsNullOrEmpty(item.EpisodeTitle) || item.IsSeries ? item.Name : null, + StartDate = item.StartDate, + EndDate = item.EndDate, + OfficialRating = item.OfficialRating, + CommunityRating = item.CommunityRating, + ContentType = item.IsMovie ? ChannelMediaContentType.Movie : ChannelMediaContentType.Episode, + Genres = item.Genres, + ImageUrl = item.ImageUrl, + Id = item.Id, + ParentIndexNumber = item.SeasonNumber, + IndexNumber = item.EpisodeNumber, + MediaType = item.ChannelType == ChannelType.TV ? ChannelMediaType.Video : ChannelMediaType.Audio, + MediaSources = new List + { + new MediaSourceInfo + { + Path = path, + Container = item.Status == RecordingStatus.InProgress ? "ts" : null, + Protocol = path.StartsWith("http", StringComparison.OrdinalIgnoreCase) ? MediaProtocol.Http : MediaProtocol.File, + BufferMs = 1000, + AnalyzeDurationMs = 0, + IsInfiniteStream = item.Status == RecordingStatus.InProgress, + TranscodingContainer = "ts", + RunTimeTicks = item.Status == RecordingStatus.InProgress ? null : (item.EndDate - item.StartDate).Ticks, + } + }, + PremiereDate = item.OriginalAirDate, + ProductionYear = item.ProductionYear, + Type = ChannelItemType.Media, + DateModified = item.Status == RecordingStatus.InProgress ? DateTime.Now : Plugin.Instance.Configuration.RecordingModificationTime, + Overview = item.Overview, + IsLiveStream = item.Status != RecordingStatus.InProgress ? false : Plugin.Instance.Configuration.EnableInProgress, + Etag = item.Status.ToString() + }; + + return channelItem; + } + + private async Task GetRecordingsAsync(string name, CancellationToken cancellationToken) + { + var service = GetService(); + if (service is null || !service.IsActive) + { + return false; + } + + if (_useCachedRecordings == false || service.FlagRecordingChange) + { + if (_pollInterval == -1) + { + var interval = TimeSpan.FromSeconds(Plugin.Instance.Configuration.PollInterval); + _updateTimer = new Timer(OnUpdateTimerCallbackAsync, null, TimeSpan.FromMinutes(2), interval); + if (_updateTimer != null) + { + _pollInterval = Plugin.Instance.Configuration.PollInterval; + } + } + + if (await _semaphore.WaitAsync(30000, cancellationToken)) + { + try + { + _logger.LogDebug("{0} Reload cache", name); + _allRecordings = await service.GetAllRecordingsAsync(cancellationToken).ConfigureAwait(false); + int maxId = _allRecordings.Max(r => int.Parse(r.Id, CultureInfo.InvariantCulture)); + int inProcessCount = _allRecordings.Count(r => r.Status == RecordingStatus.InProgress); + string keyBase = $"{maxId}-{inProcessCount}-{_allRecordings.Count()}"; + if (keyBase != _cacheKeyBase && !service.FlagRecordingChange) + { + _logger.LogDebug("External recording list change {0}", keyBase); + CleanCache(true); + } + + _cacheKeyBase = keyBase; + _lastUpdate = DateTimeOffset.UtcNow; + service.FlagRecordingChange = false; + _useCachedRecordings = true; + } + catch (Exception) + { + } + + _semaphore.Release(); + } + } + + return _useCachedRecordings; + } + + private async void OnUpdateTimerCallbackAsync(object state) + { + LiveTvService service = LiveTvService.Instance; + if (service is not null && service.IsActive) + { + var backendUpdate = await service.GetLastUpdate(_cancellationToken.Token).ConfigureAwait(false); + if (backendUpdate > _lastUpdate) + { + _logger.LogDebug("Recordings reset {0}", backendUpdate); + _useCachedRecordings = false; + await GetRecordingsAsync("OnUpdateTimerCallbackAsync", _cancellationToken.Token); + } + } + }*/ } diff --git a/StreamCinemaJellyfin/StreamCinemaTrendingFolder.cs b/StreamCinemaJellyfin/StreamCinemaTrendingFolder.cs index 604f9dc..439992a 100644 --- a/StreamCinemaJellyfin/StreamCinemaTrendingFolder.cs +++ b/StreamCinemaJellyfin/StreamCinemaTrendingFolder.cs @@ -1,3 +1,5 @@ +using Jellyfin.Data.Enums; +using MediaBrowser.Controller.Entities; using StreamCinemaLib.API; namespace Jellyfin.Plugin.StreamCinema; @@ -6,5 +8,14 @@ public sealed class StreamCinemaTrendingFolder : StreamCinemaSortFolder { internal override string ImageName => "trending.png"; + public override BaseItemKind ClientType => BaseItemKind.Movie; + + public override ItemType ItemType => ItemType.Movie; + internal override FilterSortBy SortBy { get { return FilterSortBy.Trending; } } + + protected override IEnumerable GetFilterItems() + { + yield break; + } } \ No newline at end of file diff --git a/StreamCinemaLib/API/Metadata.cs b/StreamCinemaLib/API/Metadata.cs index 8f66d87..8a99e48 100644 --- a/StreamCinemaLib/API/Metadata.cs +++ b/StreamCinemaLib/API/Metadata.cs @@ -12,7 +12,7 @@ public class Metadata { private const string UserAgent = "User-Agent: XBMC/19 (Linux 6.1; http://www.xbmc.org)"; private const string AccessToken = "9ajdu4xyn1ig8nxsodr3"; - private const int MaxPageLimit = 1000; + private const int MaxPageLimit = 100; // thousand is too much for some search types private static readonly Uri ApiRoot = new Uri("https://plugin.sc2.zone/api/"); private static readonly Uri ApiFilter = new Uri(ApiRoot, "media/filter/v2/"); @@ -20,7 +20,7 @@ public class Metadata /// /// Searches for media using an expression. /// - /// Expression like part of title. + /// Expression like part of title. Can be empty string to list all media. /// Result ordering direction. /// Result ordering column. /// Allowed result type. @@ -37,8 +37,28 @@ public class Metadata if (limit == 0 || limit > MaxPageLimit) limit = MaxPageLimit; - UriBuilder uri = new UriBuilder(new Uri(ApiFilter, "search")); + /* + bool noSearchTerms = expression.Trim().Length == 0; + UriBuilder uri = new UriBuilder(new Uri(ApiFilter, noSearchTerms ? "all" : "search")); uri.Query = $"?access_token={AccessToken}&value={Uri.EscapeDataString(expression)}&order={ToString(order)}&sort={ToString(sort)}&type={ToString(type)}&from={offset.ToString()}&size={limit.ToString()}"; + */ + + 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"; + 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}" : ""); FilterResponse? result = await Program._http.GetFromJsonAsync(uri.Uri, CreateFilterJsonOptions(), cancel); if (result != null)