diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index a392ab9..0eabf05 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -17,5 +17,5 @@ jobs: call: uses: jellyfin/jellyfin-meta-plugins/.github/workflows/build.yaml@master with: - dotnet-version: "6.0.*" - dotnet-target: "net6.0" + dotnet-version: "8.0.*" + dotnet-target: "net8.0" diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index 874c3f9..cb617d2 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -10,8 +10,8 @@ jobs: build: uses: jellyfin/jellyfin-meta-plugins/.github/workflows/build.yaml@master with: - dotnet-version: "6.0.*" - dotnet-target: "net6.0" + dotnet-version: "8.0.*" + dotnet-target: "net8.0" upload: runs-on: ubuntu-latest needs: diff --git a/.github/workflows/scan-codeql.yaml b/.github/workflows/scan-codeql.yaml index 7a66dd9..3595f1d 100644 --- a/.github/workflows/scan-codeql.yaml +++ b/.github/workflows/scan-codeql.yaml @@ -9,5 +9,5 @@ jobs: call: uses: jellyfin/jellyfin-meta-plugins/.github/workflows/scan-codeql.yaml@master with: - dotnet-version: "6.0.*" + dotnet-version: "8.0.*" repository-name: Kevinjil/Jellyfin.Xtream diff --git a/Jellyfin.Xtream/CatchupChannel.cs b/Jellyfin.Xtream/CatchupChannel.cs index ffbcf45..7fcbaf5 100644 --- a/Jellyfin.Xtream/CatchupChannel.cs +++ b/Jellyfin.Xtream/CatchupChannel.cs @@ -106,9 +106,8 @@ namespace Jellyfin.Xtream return await GetChannels(cancellationToken).ConfigureAwait(false); } - int separator = query.FolderId.IndexOf('-', StringComparison.InvariantCulture); - int categoryId = int.Parse(query.FolderId.Substring(0, separator), CultureInfo.InvariantCulture); - int channelId = int.Parse(query.FolderId.Substring(separator + 1), CultureInfo.InvariantCulture); + Guid guid = Guid.Parse(query.FolderId); + StreamService.FromGuid(guid, out int prefix, out int categoryId, out int channelId, out int _); return await GetStreams(categoryId, channelId, cancellationToken).ConfigureAwait(false); } @@ -124,10 +123,10 @@ namespace Jellyfin.Xtream continue; } - ParsedName parsedName = plugin.StreamService.ParseName(channel.Name); + ParsedName parsedName = StreamService.ParseName(channel.Name); items.Add(new ChannelItemInfo() { - Id = $"{channel.CategoryId}-{channel.StreamId}", + Id = StreamService.ToGuid(StreamService.CatchupPrefix, channel.CategoryId, channel.StreamId, 0).ToString(), ImageUrl = channel.StreamIcon, Name = parsedName.Title, Tags = new List(parsedName.Tags), @@ -173,7 +172,7 @@ namespace Jellyfin.Xtream { ContentType = ChannelMediaContentType.TvExtra, FolderType = ChannelFolderType.Container, - Id = $"fallback-{channelId}", + Id = StreamService.ToGuid(StreamService.FallbackPrefix, channelId, 0, 0).ToString(), IsLiveStream = false, MediaSources = new List() { @@ -194,7 +193,7 @@ namespace Jellyfin.Xtream foreach (EpgInfo epg in epgs.Listings.Where(epg => epg.Start < startBefore && epg.Start >= startAfter)) { string id = epg.Id.ToString(System.Globalization.CultureInfo.InvariantCulture); - ParsedName parsedName = plugin.StreamService.ParseName(epg.Title); + ParsedName parsedName = StreamService.ParseName(epg.Title); int durationMinutes = (int)Math.Ceiling((epg.End - epg.Start).TotalMinutes); string dateTitle = epg.Start.ToLocalTime().ToString("ddd HH:mm", CultureInfo.InvariantCulture); List sources = new List() diff --git a/Jellyfin.Xtream/Configuration/SerializableDictionary.cs b/Jellyfin.Xtream/Configuration/SerializableDictionary.cs index cb81fa7..2edaa3e 100644 --- a/Jellyfin.Xtream/Configuration/SerializableDictionary.cs +++ b/Jellyfin.Xtream/Configuration/SerializableDictionary.cs @@ -49,25 +49,6 @@ namespace Jellyfin.Xtream.Configuration { } - /// - /// Initializes a new instance of the - /// class. - /// - /// A - /// object - /// containing the information required to serialize the - /// . - /// - /// A - /// structure - /// containing the source and destination of the serialized stream - /// associated with the - /// . - /// - private SerializableDictionary(SerializationInfo info, StreamingContext context) : base(info, context) - { - } - private string ItemTagName => DefaultItemTag; private string KeyTagName => DefaultKeyTag; diff --git a/Jellyfin.Xtream/Jellyfin.Xtream.csproj b/Jellyfin.Xtream/Jellyfin.Xtream.csproj index dedc09b..c27d098 100644 --- a/Jellyfin.Xtream/Jellyfin.Xtream.csproj +++ b/Jellyfin.Xtream/Jellyfin.Xtream.csproj @@ -1,10 +1,10 @@ - net6.0 + net8.0 Jellyfin.Xtream - 0.6.1.0 - 0.6.1.0 + 0.6.2.0 + 0.6.2.0 true true enable @@ -13,8 +13,8 @@ - - + + @@ -24,7 +24,7 @@ - + diff --git a/Jellyfin.Xtream/LiveTvService.cs b/Jellyfin.Xtream/LiveTvService.cs index e05f892..1b23623 100644 --- a/Jellyfin.Xtream/LiveTvService.cs +++ b/Jellyfin.Xtream/LiveTvService.cs @@ -70,7 +70,7 @@ namespace Jellyfin.Xtream List items = new List(); await foreach (StreamInfo channel in plugin.StreamService.GetLiveStreamsWithOverrides(cancellationToken)) { - ParsedName parsed = plugin.StreamService.ParseName(channel.Name); + ParsedName parsed = StreamService.ParseName(channel.Name); items.Add(new ChannelInfo() { Id = channel.StreamId.ToString(CultureInfo.InvariantCulture), @@ -170,7 +170,7 @@ namespace Jellyfin.Xtream { string key = $"xtream-epg-{channelId}"; ICollection? items = null; - if (memoryCache.TryGetValue(key, out ICollection o)) + if (memoryCache.TryGetValue(key, out ICollection? o)) { items = o; } @@ -204,12 +204,6 @@ namespace Jellyfin.Xtream select epg; } - /// - public Task RecordLiveStream(string id, CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } - /// public Task ResetTuner(string id, CancellationToken cancellationToken) { diff --git a/Jellyfin.Xtream/Plugin.cs b/Jellyfin.Xtream/Plugin.cs index aeb3362..155e8b9 100644 --- a/Jellyfin.Xtream/Plugin.cs +++ b/Jellyfin.Xtream/Plugin.cs @@ -132,15 +132,15 @@ namespace Jellyfin.Xtream // - This will update the TV channels. // - This will remove channels on credentials change. TaskService.CancelIfRunningAndQueue( - "Emby.Server.Implementations", - "Emby.Server.Implementations.LiveTv.RefreshGuideScheduledTask"); + "Jellyfin.LiveTv", + "Jellyfin.LiveTv.Guide.RefreshGuideScheduledTask"); // Force a refresh of Channels on configuration update. // - This will update the channel entries. // - This will remove channel entries on credentials change. TaskService.CancelIfRunningAndQueue( - "Emby.Server.Implementations", - "Emby.Server.Implementations.Channels.RefreshChannelsScheduledTask"); + "Jellyfin.LiveTv", + "Jellyfin.LiveTv.Channels.RefreshChannelsScheduledTask"); } } } diff --git a/Jellyfin.Xtream/PluginServiceRegistrator.cs b/Jellyfin.Xtream/PluginServiceRegistrator.cs new file mode 100644 index 0000000..1303535 --- /dev/null +++ b/Jellyfin.Xtream/PluginServiceRegistrator.cs @@ -0,0 +1,35 @@ +// 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 MediaBrowser.Controller; +using MediaBrowser.Controller.Channels; +using MediaBrowser.Controller.LiveTv; +using MediaBrowser.Controller.Plugins; +using Microsoft.Extensions.DependencyInjection; + +namespace Jellyfin.Xtream; + +/// +public class PluginServiceRegistrator : IPluginServiceRegistrator +{ + /// + public void RegisterServices(IServiceCollection serviceCollection, IServerApplicationHost applicationHost) + { + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); + } +} diff --git a/Jellyfin.Xtream/SeriesChannel.cs b/Jellyfin.Xtream/SeriesChannel.cs index ad61be9..c5989e5 100644 --- a/Jellyfin.Xtream/SeriesChannel.cs +++ b/Jellyfin.Xtream/SeriesChannel.cs @@ -101,31 +101,25 @@ namespace Jellyfin.Xtream /// public async Task GetChannelItems(InternalChannelItemQuery query, CancellationToken cancellationToken) { - Plugin plugin = Plugin.Instance; if (string.IsNullOrEmpty(query.FolderId)) { return await GetCategories(cancellationToken).ConfigureAwait(false); } - if (plugin.StreamService.IsId(query.FolderId, StreamService.CategoryPrefix)) + Guid guid = Guid.Parse(query.FolderId); + StreamService.FromGuid(guid, out int prefix, out int categoryId, out int seriesId, out int seasonId); + if (prefix == StreamService.SeriesCategoryPrefix) { - int categoryId = plugin.StreamService.ParseId(query.FolderId, StreamService.CategoryPrefix); return await GetSeries(categoryId, cancellationToken).ConfigureAwait(false); } - if (plugin.StreamService.IsId(query.FolderId, StreamService.SeriesPrefix)) + if (prefix == StreamService.SeriesPrefix) { - int seriesId = plugin.StreamService.ParseId(query.FolderId, StreamService.SeriesPrefix); return await GetSeasons(seriesId, cancellationToken).ConfigureAwait(false); } - if (plugin.StreamService.IsId(query.FolderId, StreamService.SeasonPrefix)) + if (prefix == StreamService.SeasonPrefix) { - string folder = query.FolderId.Substring(StreamService.SeasonPrefix.Length); - string[] parts = folder.Split('-'); - int seriesId = int.Parse(parts[0], System.Globalization.CultureInfo.InvariantCulture); - int seasonId = int.Parse(parts[1], System.Globalization.CultureInfo.InvariantCulture); - return await GetEpisodes(seriesId, seasonId, cancellationToken).ConfigureAwait(false); } @@ -137,14 +131,14 @@ namespace Jellyfin.Xtream private ChannelItemInfo CreateChannelItemInfo(Series series) { - ParsedName parsedName = Plugin.Instance.StreamService.ParseName(series.Name); + ParsedName parsedName = StreamService.ParseName(series.Name); return new ChannelItemInfo() { CommunityRating = (float)series.Rating5Based, DateModified = series.LastModified, // FolderType = ChannelFolderType.Series, Genres = GetGenres(series.Genre), - Id = $"{StreamService.SeriesPrefix}{series.SeriesId}", + Id = StreamService.ToGuid(StreamService.SeriesPrefix, series.CategoryId, series.SeriesId, 0).ToString(), ImageUrl = series.Cover, Name = parsedName.Title, People = GetPeople(series.Cast), @@ -178,7 +172,7 @@ namespace Jellyfin.Xtream Season? season = series.Seasons.FirstOrDefault(s => s.SeasonId == seasonId); if (season != null) { - ParsedName parsedName = Plugin.Instance.StreamService.ParseName(season.Name); + ParsedName parsedName = StreamService.ParseName(season.Name); name = parsedName.Title; tags.AddRange(parsedName.Tags); created = season.AirDate; @@ -194,7 +188,7 @@ namespace Jellyfin.Xtream DateCreated = created, // FolderType = ChannelFolderType.Season, Genres = GetGenres(serie.Genre), - Id = $"{StreamService.SeasonPrefix}{seriesId}-{seasonId}", + Id = StreamService.ToGuid(StreamService.SeasonPrefix, serie.CategoryId, seriesId, seasonId).ToString(), ImageUrl = cover, Name = name, Overview = overview, @@ -207,7 +201,7 @@ namespace Jellyfin.Xtream private ChannelItemInfo CreateChannelItemInfo(SeriesStreamInfo series, Season? season, Episode episode) { Jellyfin.Xtream.Client.Models.SeriesInfo serie = series.Info; - ParsedName parsedName = Plugin.Instance.StreamService.ParseName(episode.Title); + ParsedName parsedName = StreamService.ParseName(episode.Title); List sources = new List() { Plugin.Instance.StreamService.GetMediaSourceInfo(StreamType.Series, episode.EpisodeId, episode.ContainerExtension) @@ -229,7 +223,7 @@ namespace Jellyfin.Xtream ContentType = ChannelMediaContentType.Episode, DateCreated = DateTimeOffset.FromUnixTimeSeconds(episode.Added).DateTime, Genres = GetGenres(serie.Genre), - Id = $"{StreamService.EpisodePrefix}{episode.EpisodeId}", + Id = StreamService.ToGuid(StreamService.EpisodePrefix, 0, 0, episode.EpisodeId).ToString(), ImageUrl = cover, IsLiveStream = false, MediaSources = sources, @@ -246,7 +240,7 @@ namespace Jellyfin.Xtream { List items = new List( (await Plugin.Instance.StreamService.GetSeriesCategories(cancellationToken).ConfigureAwait(false)) - .Select((Category category) => Plugin.Instance.StreamService.CreateChannelItemInfo(category))); + .Select((Category category) => StreamService.CreateChannelItemInfo(StreamService.SeriesCategoryPrefix, category))); return new ChannelItemResult() { Items = items, diff --git a/Jellyfin.Xtream/Service/Restream.cs b/Jellyfin.Xtream/Service/Restream.cs index 8d8903f..1931728 100644 --- a/Jellyfin.Xtream/Service/Restream.cs +++ b/Jellyfin.Xtream/Service/Restream.cs @@ -158,7 +158,7 @@ namespace Jellyfin.Xtream.Service throw new ArgumentNullException("copyTask"); } - tokenSource.Cancel(); + await tokenSource.CancelAsync().ConfigureAwait(false); await copyTask.ConfigureAwait(false); } diff --git a/Jellyfin.Xtream/Service/StreamService.cs b/Jellyfin.Xtream/Service/StreamService.cs index 3620752..608629f 100644 --- a/Jellyfin.Xtream/Service/StreamService.cs +++ b/Jellyfin.Xtream/Service/StreamService.cs @@ -37,29 +37,49 @@ namespace Jellyfin.Xtream.Service public class StreamService { /// - /// The id prefix for category channel items. + /// The id prefix for VOD category channel items. /// - public const string CategoryPrefix = "category-"; + public const int VodCategoryPrefix = 0x5d774c35; /// /// The id prefix for stream channel items. /// - public const string StreamPrefix = "stream-"; + public const int StreamPrefix = 0x5d774c36; /// - /// The id prefix for series channel items. + /// The id prefix for series category channel items. /// - public const string SeriesPrefix = "series-"; + public const int SeriesCategoryPrefix = 0x5d774c37; + + /// + /// The id prefix for series category channel items. + /// + public const int SeriesPrefix = 0x5d774c38; /// /// The id prefix for season channel items. /// - public const string SeasonPrefix = "seasons-"; + public const int SeasonPrefix = 0x5d774c39; /// /// The id prefix for season channel items. /// - public const string EpisodePrefix = "episode-"; + public const int EpisodePrefix = 0x5d774c3a; + + /// + /// The id prefix for catchup channel items. + /// + public const int CatchupPrefix = 0x5d774c3b; + + /// + /// The id prefix for fallback EPG items. + /// + public const int FallbackPrefix = 0x5d774c3c; + + /// + /// The id prefix for media source items. + /// + public const int MediaSourcePrefix = 0x5d774c3d; private static readonly Regex TagRegex = new Regex(@"\[([^\]]+)\]|\|([^\|]+)\|"); @@ -89,7 +109,7 @@ namespace Jellyfin.Xtream.Service /// /// The name which should be parsed. /// A struct containing the cleaned title and parsed tags. - public ParsedName ParseName(string name) + public static ParsedName ParseName(string name) { List tags = new List(); string title = TagRegex.Replace( @@ -115,28 +135,6 @@ namespace Jellyfin.Xtream.Service }; } - /// - /// Checks if the id string is an id with the given prefix. - /// - /// The id string. - /// The prefix string. - /// Whether or not the id string has the given prefix. - public bool IsId(string id, string prefix) - { - return id.StartsWith(prefix, StringComparison.InvariantCulture); - } - - /// - /// Parses the given id by removing the prefix. - /// - /// The id string. - /// The prefix string. - /// The parsed it as integer. - public int ParseId(string id, string prefix) - { - return int.Parse(id.Substring(prefix.Length), CultureInfo.InvariantCulture); - } - private bool IsConfigured(SerializableDictionary> config, int category, int id) { HashSet? values; @@ -192,14 +190,15 @@ namespace Jellyfin.Xtream.Service /// /// Gets an channel item info for the category. /// + /// The channel category prefix. /// The Xtream category. /// A channel item representing the category. - public ChannelItemInfo CreateChannelItemInfo(Category category) + public static ChannelItemInfo CreateChannelItemInfo(int prefix, Category category) { ParsedName parsedName = ParseName(category.CategoryName); return new ChannelItemInfo() { - Id = $"{CategoryPrefix}{category.CategoryId}", + Id = ToGuid(prefix, category.CategoryId, 0, 0).ToString(), Name = category.CategoryName, Tags = new List(parsedName.Tags), Type = ChannelItemType.Folder, @@ -313,6 +312,63 @@ namespace Jellyfin.Xtream.Service } } + private static void StoreBytes(byte[] dst, int offset, int i) + { + byte[] intBytes = BitConverter.GetBytes(i); + if (BitConverter.IsLittleEndian) + { + Array.Reverse(intBytes); + } + + Buffer.BlockCopy(intBytes, 0, dst, offset, 4); + } + + /// + /// Gets a GUID representing the four 32-bit integers. + /// + /// Bytes 0-3. + /// Bytes 4-7. + /// Bytes 8-11. + /// Bytes 12-15. + /// Guid. + public static Guid ToGuid(int i0, int i1, int i2, int i3) + { + byte[] guid = new byte[16]; + StoreBytes(guid, 0, i0); + StoreBytes(guid, 4, i1); + StoreBytes(guid, 8, i2); + StoreBytes(guid, 12, i3); + return new Guid(guid); + } + + /// + /// Gets the four 32-bit integers represented in the GUID. + /// + /// The input GUID. + /// Bytes 0-3. + /// Bytes 4-7. + /// Bytes 8-11. + /// Bytes 12-15. + public static void FromGuid(Guid guid, out int i0, out int i1, out int i2, out int i3) + { + byte[] tmp = guid.ToByteArray(); + if (BitConverter.IsLittleEndian) + { + Array.Reverse(tmp); + i0 = BitConverter.ToInt32(tmp, 12); + i1 = BitConverter.ToInt32(tmp, 8); + i2 = BitConverter.ToInt32(tmp, 4); + i3 = BitConverter.ToInt32(tmp, 0); + } + else + { + i0 = BitConverter.ToInt32(tmp, 0); + i1 = BitConverter.ToInt32(tmp, 4); + i2 = BitConverter.ToInt32(tmp, 8); + i3 = BitConverter.ToInt32(tmp, 12); + } + } + /// /// Gets the media source information for the given Xtream stream. /// @@ -359,7 +415,7 @@ namespace Jellyfin.Xtream.Service return new MediaSourceInfo() { EncoderProtocol = MediaProtocol.Http, - Id = id.ToString(CultureInfo.InvariantCulture), + Id = ToGuid(MediaSourcePrefix, (int)type, id, 0).ToString(), IsInfiniteStream = isLive, IsRemote = true, Name = "default", diff --git a/Jellyfin.Xtream/Service/StreamType.cs b/Jellyfin.Xtream/Service/StreamType.cs index 2b9e143..c185c3a 100644 --- a/Jellyfin.Xtream/Service/StreamType.cs +++ b/Jellyfin.Xtream/Service/StreamType.cs @@ -18,26 +18,26 @@ namespace Jellyfin.Xtream.Service /// /// An enum describing the Xtream stream types. /// - public enum StreamType + public enum StreamType : int { /// /// Live IPTV. /// - Live, + Live = 0, /// /// Catch up IPTV. /// - CatchUp, + CatchUp = 1, /// /// On-demand series grouped in seasons and episodes. /// - Series, + Series = 2, /// /// Video on-demand. /// - Vod, + Vod = 3, } } diff --git a/Jellyfin.Xtream/VodChannel.cs b/Jellyfin.Xtream/VodChannel.cs index 17c6d41..02b5785 100644 --- a/Jellyfin.Xtream/VodChannel.cs +++ b/Jellyfin.Xtream/VodChannel.cs @@ -100,15 +100,15 @@ namespace Jellyfin.Xtream /// public async Task GetChannelItems(InternalChannelItemQuery query, CancellationToken cancellationToken) { - Plugin plugin = Plugin.Instance; if (string.IsNullOrEmpty(query.FolderId)) { return await GetCategories(cancellationToken).ConfigureAwait(false); } - if (plugin.StreamService.IsId(query.FolderId, StreamService.CategoryPrefix)) + Guid guid = Guid.Parse(query.FolderId); + StreamService.FromGuid(guid, out int prefix, out int categoryId, out int _, out int _); + if (prefix == StreamService.VodCategoryPrefix) { - int categoryId = plugin.StreamService.ParseId(query.FolderId, StreamService.CategoryPrefix); return await GetStreams(categoryId, cancellationToken).ConfigureAwait(false); } @@ -121,7 +121,7 @@ namespace Jellyfin.Xtream private ChannelItemInfo CreateChannelItemInfo(StreamInfo stream) { long added = long.Parse(stream.Added, CultureInfo.InvariantCulture); - ParsedName parsedName = Plugin.Instance.StreamService.ParseName(stream.Name); + ParsedName parsedName = StreamService.ParseName(stream.Name); List sources = new List() { Plugin.Instance.StreamService.GetMediaSourceInfo(StreamType.Vod, stream.StreamId, stream.ContainerExtension) @@ -147,7 +147,7 @@ namespace Jellyfin.Xtream { List items = new List( (await Plugin.Instance.StreamService.GetVodCategories(cancellationToken).ConfigureAwait(false)) - .Select((Category category) => Plugin.Instance.StreamService.CreateChannelItemInfo(category))); + .Select((Category category) => StreamService.CreateChannelItemInfo(StreamService.VodCategoryPrefix, category))); return new ChannelItemResult() { Items = items, diff --git a/build.yaml b/build.yaml index 2130152..f666219 100644 --- a/build.yaml +++ b/build.yaml @@ -1,9 +1,9 @@ --- name: "Jellyfin Xtream" guid: "5d774c35-8567-46d3-a950-9bb8227a0c5d" -version: "0.6.1.0" +version: "0.6.2.0" targetAbi: "10.8.4.0" -framework: "net6.0" +framework: "net8.0" overview: "Stream content from an Xtream-compatible server." description: > Stream Live IPTV, Video On-Demand, and Series from an Xtream-compatible server using this plugin.