diff --git a/Jellyfin.Xtream/CatchupChannel.cs b/Jellyfin.Xtream/CatchupChannel.cs index 0814052..25ebd8b 100644 --- a/Jellyfin.Xtream/CatchupChannel.cs +++ b/Jellyfin.Xtream/CatchupChannel.cs @@ -35,7 +35,7 @@ namespace Jellyfin.Xtream; /// The Xtream Codes API channel. /// /// Instance of the interface. -public class CatchupChannel(ILogger logger) : IChannel +public class CatchupChannel(ILogger logger) : IChannel, IDisableMediaSourceDisplay { private readonly ILogger _logger = logger; diff --git a/Jellyfin.Xtream/Client/Models/AudioInfo.cs b/Jellyfin.Xtream/Client/Models/AudioInfo.cs new file mode 100644 index 0000000..8ec4799 --- /dev/null +++ b/Jellyfin.Xtream/Client/Models/AudioInfo.cs @@ -0,0 +1,46 @@ +// Copyright (C) 2022 Kevin Jilissen + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +using Newtonsoft.Json; + +#pragma warning disable CS1591 +namespace Jellyfin.Xtream.Client.Models; + +public class AudioInfo +{ + [JsonProperty("index")] + public int Index { get; set; } + + [JsonProperty("codec_name")] + public string CodecName { get; set; } = string.Empty; + + [JsonProperty("profile")] + public string Profile { get; set; } = string.Empty; + + [JsonProperty("sample_fmt")] + public string SampleFormat { get; set; } = string.Empty; + + [JsonProperty("sample_rate")] + public int SampleRate { get; set; } + + [JsonProperty("channels")] + public int Channels { get; set; } + + [JsonProperty("channel_layout")] + public string ChannelLayout { get; set; } = string.Empty; + + [JsonProperty("bit_rate")] + public int Bitrate { get; set; } +} diff --git a/Jellyfin.Xtream/Client/Models/EpisodeInfo.cs b/Jellyfin.Xtream/Client/Models/EpisodeInfo.cs index 4f19b10..0519340 100644 --- a/Jellyfin.Xtream/Client/Models/EpisodeInfo.cs +++ b/Jellyfin.Xtream/Client/Models/EpisodeInfo.cs @@ -22,20 +22,28 @@ namespace Jellyfin.Xtream.Client.Models; public class EpisodeInfo { [JsonProperty("movie_image")] - public string MovieImage { get; set; } = string.Empty; + public string? MovieImage { get; set; } [JsonProperty("plot")] - public string Plot { get; set; } = string.Empty; + public string? Plot { get; set; } [JsonProperty("releasedate")] - public DateTime ReleaseDate { get; set; } + public DateTime? ReleaseDate { get; set; } [JsonProperty("rating")] - public decimal Rating { get; set; } + public decimal? Rating { get; set; } [JsonProperty("duration_secs")] - public int DurationSecs { get; set; } + public int? DurationSecs { get; set; } [JsonProperty("bitrate")] - public int Bitrate { get; set; } + public int? Bitrate { get; set; } + + [JsonProperty("video")] + [JsonConverter(typeof(OnlyObjectConverter))] + public VideoInfo? Video { get; set; } + + [JsonProperty("audio")] + [JsonConverter(typeof(OnlyObjectConverter))] + public AudioInfo? Audio { get; set; } } diff --git a/Jellyfin.Xtream/Client/Models/VideoInfo.cs b/Jellyfin.Xtream/Client/Models/VideoInfo.cs new file mode 100644 index 0000000..12dacf0 --- /dev/null +++ b/Jellyfin.Xtream/Client/Models/VideoInfo.cs @@ -0,0 +1,64 @@ +// Copyright (C) 2022 Kevin Jilissen + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +using Newtonsoft.Json; + +#pragma warning disable CS1591 +namespace Jellyfin.Xtream.Client.Models; + +public class VideoInfo +{ + [JsonProperty("index")] + public int Index { get; set; } + + [JsonProperty("codec_name")] + public string CodecName { get; set; } = string.Empty; + + [JsonProperty("profile")] + public string Profile { get; set; } = string.Empty; + + [JsonProperty("width")] + public int Width { get; set; } + + [JsonProperty("height")] + public int Height { get; set; } + + [JsonProperty("display_aspect_ratio")] + public string AspectRatio { get; set; } = string.Empty; + + [JsonProperty("pix_fmt")] + public string PixelFormat { get; set; } = string.Empty; + + [JsonProperty("level")] + public int Level { get; set; } + + [JsonProperty("color_range")] + public string ColorRange { get; set; } = string.Empty; + + [JsonProperty("color_space")] + public string ColorSpace { get; set; } = string.Empty; + + [JsonProperty("color_transfer")] + public string ColorTransfer { get; set; } = string.Empty; + + [JsonProperty("color_primaries")] + public string ColorPrimaries { get; set; } = string.Empty; + + [JsonProperty("is_avc")] + public bool IsAVC { get; set; } + + [JsonProperty("bits_per_raw_sample")] + public int BitsPerRawSample { get; set; } +} diff --git a/Jellyfin.Xtream/Client/Models/VodInfo.cs b/Jellyfin.Xtream/Client/Models/VodInfo.cs new file mode 100644 index 0000000..0c5db3d --- /dev/null +++ b/Jellyfin.Xtream/Client/Models/VodInfo.cs @@ -0,0 +1,58 @@ +// Copyright (C) 2022 Kevin Jilissen + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +using System; +using Newtonsoft.Json; + +#pragma warning disable CS1591 +namespace Jellyfin.Xtream.Client.Models; + +public class VodInfo +{ + [JsonProperty("movie_image")] + public string? MovieImage { get; set; } + + [JsonProperty("genre")] + public string? Genre { get; set; } + + [JsonProperty("plot")] + public string? Plot { get; set; } + + [JsonProperty("director")] + public string? Director { get; set; } + + [JsonProperty("rating")] + public decimal? Rating { get; set; } + + [JsonProperty("releasedate")] + public DateTime? ReleaseDate { get; set; } + + [JsonProperty("duration_secs")] + public int? DurationSecs { get; set; } + + [JsonProperty("tmdb_id")] + public int? TmdbId { get; set; } + + [JsonProperty("bitrate")] + public int Bitrate { get; set; } + + [JsonProperty("video")] + [JsonConverter(typeof(OnlyObjectConverter))] + public VideoInfo? Video { get; set; } + + [JsonProperty("audio")] + [JsonConverter(typeof(OnlyObjectConverter))] + public AudioInfo? Audio { get; set; } +} diff --git a/Jellyfin.Xtream/Client/Models/VodStreamInfo.cs b/Jellyfin.Xtream/Client/Models/VodStreamInfo.cs new file mode 100644 index 0000000..06e4dce --- /dev/null +++ b/Jellyfin.Xtream/Client/Models/VodStreamInfo.cs @@ -0,0 +1,30 @@ +// Copyright (C) 2022 Kevin Jilissen + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +using Newtonsoft.Json; + +#pragma warning disable CS1591 +namespace Jellyfin.Xtream.Client.Models; + +public class VodStreamInfo +{ + [JsonProperty("info")] + [JsonConverter(typeof(OnlyObjectConverter))] + public VodInfo? Info { get; set; } + + [JsonProperty("movie_data")] + [JsonConverter(typeof(OnlyObjectConverter))] + public StreamInfo? MovieData { get; set; } +} diff --git a/Jellyfin.Xtream/Client/XtreamClient.cs b/Jellyfin.Xtream/Client/XtreamClient.cs index b24219c..53854f3 100644 --- a/Jellyfin.Xtream/Client/XtreamClient.cs +++ b/Jellyfin.Xtream/Client/XtreamClient.cs @@ -71,6 +71,12 @@ public class XtreamClient(HttpClient client) : IDisposable $"/player_api.php?username={connectionInfo.UserName}&password={connectionInfo.Password}&action=get_vod_streams&category_id={categoryId}", cancellationToken); + public Task GetVodInfoAsync(ConnectionInfo connectionInfo, int streamId, CancellationToken cancellationToken) => + QueryApi( + connectionInfo, + $"/player_api.php?username={connectionInfo.UserName}&password={connectionInfo.Password}&action=get_vod_info&vod_id={streamId}", + cancellationToken); + public Task> GetLiveStreamsByCategoryAsync(ConnectionInfo connectionInfo, int categoryId, CancellationToken cancellationToken) => QueryApi>( connectionInfo, diff --git a/Jellyfin.Xtream/Configuration/PluginConfiguration.cs b/Jellyfin.Xtream/Configuration/PluginConfiguration.cs index c70caf4..7cb48c9 100644 --- a/Jellyfin.Xtream/Configuration/PluginConfiguration.cs +++ b/Jellyfin.Xtream/Configuration/PluginConfiguration.cs @@ -54,6 +54,11 @@ public class PluginConfiguration : BasePluginConfiguration /// public bool IsVodVisible { get; set; } + /// + /// Gets or sets a value indicating whether the Video On-demand channel is visible. + /// + public bool IsTmdbVodOverride { get; set; } = true; + /// /// Gets or sets the channels displayed in Live TV. /// diff --git a/Jellyfin.Xtream/Configuration/Web/XtreamVod.html b/Jellyfin.Xtream/Configuration/Web/XtreamVod.html index 57d257c..40bb72d 100644 --- a/Jellyfin.Xtream/Configuration/Web/XtreamVod.html +++ b/Jellyfin.Xtream/Configuration/Web/XtreamVod.html @@ -9,6 +9,12 @@ Show this channel to users +
+ +

Video On-demand selection

diff --git a/Jellyfin.Xtream/Configuration/Web/XtreamVod.js b/Jellyfin.Xtream/Configuration/Web/XtreamVod.js index 59fb097..9b3bde0 100644 --- a/Jellyfin.Xtream/Configuration/Web/XtreamVod.js +++ b/Jellyfin.Xtream/Configuration/Web/XtreamVod.js @@ -11,6 +11,8 @@ export default function (view) { const getConfig = ApiClient.getPluginConfiguration(pluginId); const visible = view.querySelector("#Visible"); getConfig.then((config) => visible.checked = config.IsVodVisible); + const tmdbOverride = view.querySelector("#TmdbOverride"); + getConfig.then((config) => TmdbOverride.checked = config.IsTmdbVodOverride); const table = view.querySelector('#VodContent'); Xtream.populateCategoriesTable( table, @@ -23,6 +25,7 @@ export default function (view) { ApiClient.getPluginConfiguration(pluginId).then((config) => { config.IsVodVisible = visible.checked; + config.IsTmdbVodOverride = tmdbOverride.checked; config.Vod = data; ApiClient.updatePluginConfiguration(pluginId, config).then((result) => { Dashboard.processPluginConfigurationUpdateResult(result); diff --git a/Jellyfin.Xtream/PluginServiceRegistrator.cs b/Jellyfin.Xtream/PluginServiceRegistrator.cs index 1303535..85cdb5d 100644 --- a/Jellyfin.Xtream/PluginServiceRegistrator.cs +++ b/Jellyfin.Xtream/PluginServiceRegistrator.cs @@ -13,10 +13,12 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . +using Jellyfin.Xtream.Providers; using MediaBrowser.Controller; using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.Plugins; +using MediaBrowser.Controller.Providers; using Microsoft.Extensions.DependencyInjection; namespace Jellyfin.Xtream; @@ -31,5 +33,6 @@ public class PluginServiceRegistrator : IPluginServiceRegistrator serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); } } diff --git a/Jellyfin.Xtream/Providers/XtreamVodProvider.cs b/Jellyfin.Xtream/Providers/XtreamVodProvider.cs new file mode 100644 index 0000000..37aa51a --- /dev/null +++ b/Jellyfin.Xtream/Providers/XtreamVodProvider.cs @@ -0,0 +1,116 @@ +// Copyright (C) 2022 Kevin Jilissen + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Jellyfin.Xtream.Client; +using Jellyfin.Xtream.Client.Models; +using Jellyfin.Xtream.Service; +using MediaBrowser.Controller.Entities.Movies; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Providers; +using Microsoft.Extensions.Logging; + +namespace Jellyfin.Xtream.Providers; + +/// +/// The Xtream Codes VOD metadata provider. +/// +/// Instance of the interface. +/// Instance of the interface. +public class XtreamVodProvider(ILogger logger, IProviderManager providerManager) : ICustomMetadataProvider, IPreRefreshProvider +{ + /// + /// The name of the provider. + /// + public const string ProviderName = "XtreamVodProvider"; + + /// + public string Name => ProviderName; + + /// + public async Task FetchAsync(Movie item, MetadataRefreshOptions options, CancellationToken cancellationToken) + { + string? idStr = item.GetProviderId(ProviderName); + if (idStr is not null) + { + logger.LogDebug("Getting metadata for movie {Id}", idStr); + int id = int.Parse(idStr, CultureInfo.InvariantCulture); + using XtreamClient client = new(); + VodStreamInfo vod = await client.GetVodInfoAsync(Plugin.Instance.Creds, id, cancellationToken).ConfigureAwait(false); + VodInfo? i = vod.Info; + + if (i is null) + { + return ItemUpdateType.None; + } + + item.Overview ??= i.Plot; + item.PremiereDate ??= i.ReleaseDate; + item.RunTimeTicks ??= i.DurationSecs is not null ? TimeSpan.TicksPerSecond * i.DurationSecs : null; + item.TotalBitrate ??= i.Bitrate; + + if (i.Genre is string genres) + { + item.Genres ??= genres.Split(',').Select(genre => genre.Trim()).ToArray(); + } + + if (!item.HasProviderId(MetadataProvider.Tmdb)) + { + if (i.TmdbId is int tmdbId) + { + options.ReplaceAllMetadata = true; + item.SetProviderId(MetadataProvider.Tmdb, tmdbId.ToString(CultureInfo.InvariantCulture)); + } + else if (Plugin.Instance.Configuration.IsTmdbVodOverride) + { + MovieInfo queryInfo = new() + { + Name = StreamService.ParseName(vod.MovieData?.Name ?? string.Empty).Title, + Year = item.PremiereDate?.Year, + }; + // Try to fetch the TMDB id to get proper metadata. + RemoteSearchQuery query = new() + { + SearchInfo = queryInfo, + SearchProviderName = "TheMovieDb", + }; + IEnumerable results = await providerManager.GetRemoteSearchResults(query, cancellationToken).ConfigureAwait(false); + if (results.Any()) + { + RemoteSearchResult tmdbMovie = results.First(); + if (tmdbMovie.HasProviderId(MetadataProvider.Tmdb)) + { + string? queryId = tmdbMovie.GetProviderId(MetadataProvider.Tmdb); + if (queryId is not null) + { + options.ReplaceAllMetadata = true; + item.SetProviderId(MetadataProvider.Tmdb, queryId); + } + } + } + } + } + } + + return ItemUpdateType.MetadataImport; + } +} diff --git a/Jellyfin.Xtream/SeriesChannel.cs b/Jellyfin.Xtream/SeriesChannel.cs index a0f1e8e..5b72b29 100644 --- a/Jellyfin.Xtream/SeriesChannel.cs +++ b/Jellyfin.Xtream/SeriesChannel.cs @@ -35,7 +35,7 @@ namespace Jellyfin.Xtream; /// The Xtream Codes API channel. /// /// Instance of the interface. -public class SeriesChannel(ILogger logger) : IChannel +public class SeriesChannel(ILogger logger) : IChannel, IDisableMediaSourceDisplay { /// public string? Name => "Xtream Series"; @@ -198,20 +198,19 @@ public class SeriesChannel(ILogger logger) : IChannel { Client.Models.SeriesInfo serie = series.Info; ParsedName parsedName = StreamService.ParseName(episode.Title); - List sources = [ - Plugin.Instance.StreamService.GetMediaSourceInfo(StreamType.Series, episode.EpisodeId, episode.ContainerExtension) + List sources = + [ + Plugin.Instance.StreamService.GetMediaSourceInfo( + StreamType.Series, + episode.EpisodeId, + episode.ContainerExtension, + videoInfo: episode.Info?.Video, + audioInfo: episode.Info?.Audio) ]; string? cover = episode.Info?.MovieImage; - if (string.IsNullOrEmpty(cover) && season != null) - { - cover = season.Cover; - } - - if (string.IsNullOrEmpty(cover)) - { - cover = serie.Cover; - } + cover ??= season?.Cover; + cover ??= serie.Cover; return new() { diff --git a/Jellyfin.Xtream/Service/StreamService.cs b/Jellyfin.Xtream/Service/StreamService.cs index c5f6abd..ce3b80f 100644 --- a/Jellyfin.Xtream/Service/StreamService.cs +++ b/Jellyfin.Xtream/Service/StreamService.cs @@ -24,6 +24,7 @@ using Jellyfin.Xtream.Client; using Jellyfin.Xtream.Client.Models; using Jellyfin.Xtream.Configuration; using MediaBrowser.Controller.Channels; +using MediaBrowser.Controller.Entities; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.MediaInfo; @@ -367,6 +368,8 @@ public partial class StreamService /// Boolean indicating whether or not restreaming is used. /// The datetime representing the start time of catcup TV. /// The duration in minutes of the catcup TV stream. + /// The Xtream video info if known. + /// The Xtream audio info if known. /// The media source info as class. public MediaSourceInfo GetMediaSourceInfo( StreamType type, @@ -374,7 +377,9 @@ public partial class StreamService string? extension = null, bool restream = false, DateTime? start = null, - int durationMinutes = 0) + int durationMinutes = 0, + VideoInfo? videoInfo = null, + AudioInfo? audioInfo = null) { string prefix = string.Empty; switch (type) @@ -403,22 +408,41 @@ public partial class StreamService bool isLive = type == StreamType.Live; return new MediaSourceInfo() { + Container = extension, EncoderProtocol = MediaProtocol.Http, Id = ToGuid(MediaSourcePrefix, (int)type, id, 0).ToString(), IsInfiniteStream = isLive, IsRemote = true, - // Define media sources with unknown index and interlaced to improve compatibility. - MediaStreams = [ + MediaStreams = + [ new() { + AspectRatio = videoInfo?.AspectRatio, + BitDepth = videoInfo?.BitsPerRawSample, + Codec = videoInfo?.CodecName, + ColorPrimaries = videoInfo?.ColorPrimaries, + ColorRange = videoInfo?.ColorRange, + ColorSpace = videoInfo?.ColorSpace, + ColorTransfer = videoInfo?.ColorTransfer, + Height = videoInfo?.Height, + Index = videoInfo?.Index ?? -1, + IsAVC = videoInfo?.IsAVC, + IsInterlaced = true, + Level = videoInfo?.Level, + PixelFormat = videoInfo?.PixelFormat, + Profile = videoInfo?.Profile, Type = MediaStreamType.Video, - Index = -1, - IsInterlaced = true }, new() { + BitRate = audioInfo?.Bitrate, + ChannelLayout = audioInfo?.ChannelLayout, + Channels = audioInfo?.Channels, + Codec = audioInfo?.CodecName, + Index = audioInfo?.Index ?? -1, + Profile = audioInfo?.Profile, + SampleRate = audioInfo?.SampleRate, Type = MediaStreamType.Audio, - Index = -1 } ], Name = "default", diff --git a/Jellyfin.Xtream/VodChannel.cs b/Jellyfin.Xtream/VodChannel.cs index b2211f9..b08397f 100644 --- a/Jellyfin.Xtream/VodChannel.cs +++ b/Jellyfin.Xtream/VodChannel.cs @@ -20,6 +20,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using Jellyfin.Xtream.Client.Models; +using Jellyfin.Xtream.Providers; using Jellyfin.Xtream.Service; using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.Providers; @@ -34,7 +35,7 @@ namespace Jellyfin.Xtream; /// The Xtream Codes API channel. /// /// Instance of the interface. -public class VodChannel(ILogger logger) : IChannel +public class VodChannel(ILogger logger) : IChannel, IDisableMediaSourceDisplay { /// public string? Name => "Xtream Video On-Demand"; @@ -113,19 +114,23 @@ public class VodChannel(ILogger logger) : IChannel } } - private ChannelItemInfo CreateChannelItemInfo(StreamInfo stream) + private Task CreateChannelItemInfo(StreamInfo stream) { long added = long.Parse(stream.Added, CultureInfo.InvariantCulture); ParsedName parsedName = StreamService.ParseName(stream.Name); - List sources = [ - Plugin.Instance.StreamService.GetMediaSourceInfo(StreamType.Vod, stream.StreamId, stream.ContainerExtension) + + List sources = + [ + Plugin.Instance.StreamService.GetMediaSourceInfo( + StreamType.Vod, + stream.StreamId, + stream.ContainerExtension) ]; - return new() + ChannelItemInfo result = new ChannelItemInfo() { ContentType = ChannelMediaContentType.Movie, DateCreated = DateTimeOffset.FromUnixTimeSeconds(added).DateTime, - FolderType = ChannelFolderType.Container, Id = $"{StreamService.StreamPrefix}{stream.StreamId}", ImageUrl = stream.StreamIcon, IsLiveStream = false, @@ -134,7 +139,10 @@ public class VodChannel(ILogger logger) : IChannel Name = parsedName.Title, Tags = new List(parsedName.Tags), Type = ChannelItemType.Media, + ProviderIds = { { XtreamVodProvider.ProviderName, stream.StreamId.ToString(CultureInfo.InvariantCulture) } }, }; + + return Task.FromResult(result); } private async Task GetCategories(CancellationToken cancellationToken) @@ -152,8 +160,8 @@ public class VodChannel(ILogger logger) : IChannel private async Task GetStreams(int categoryId, CancellationToken cancellationToken) { IEnumerable streams = await Plugin.Instance.StreamService.GetVodStreams(categoryId, cancellationToken).ConfigureAwait(false); - List items = new List(streams.Select(CreateChannelItemInfo)); - ChannelItemResult result = new() + List items = [.. await Task.WhenAll(streams.Select(CreateChannelItemInfo)).ConfigureAwait(false)]; + ChannelItemResult result = new ChannelItemResult() { Items = items, TotalRecordCount = items.Count diff --git a/jellyfin.ruleset b/jellyfin.ruleset index 8af791c..d6994a1 100644 --- a/jellyfin.ruleset +++ b/jellyfin.ruleset @@ -63,7 +63,7 @@ - +