Support Jellyfin 10.9 #96

Merged
Kevinjil merged 4 commits from feature/jellyfin-10.9 into master 2024-05-15 17:52:08 +00:00
15 changed files with 172 additions and 113 deletions

View File

@@ -17,5 +17,5 @@ jobs:
call: call:
uses: jellyfin/jellyfin-meta-plugins/.github/workflows/build.yaml@master uses: jellyfin/jellyfin-meta-plugins/.github/workflows/build.yaml@master
with: with:
dotnet-version: "6.0.*" dotnet-version: "8.0.*"
dotnet-target: "net6.0" dotnet-target: "net8.0"

View File

@@ -10,8 +10,8 @@ jobs:
build: build:
uses: jellyfin/jellyfin-meta-plugins/.github/workflows/build.yaml@master uses: jellyfin/jellyfin-meta-plugins/.github/workflows/build.yaml@master
with: with:
dotnet-version: "6.0.*" dotnet-version: "8.0.*"
dotnet-target: "net6.0" dotnet-target: "net8.0"
upload: upload:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: needs:

View File

@@ -9,5 +9,5 @@ jobs:
call: call:
uses: jellyfin/jellyfin-meta-plugins/.github/workflows/scan-codeql.yaml@master uses: jellyfin/jellyfin-meta-plugins/.github/workflows/scan-codeql.yaml@master
with: with:
dotnet-version: "6.0.*" dotnet-version: "8.0.*"
repository-name: Kevinjil/Jellyfin.Xtream repository-name: Kevinjil/Jellyfin.Xtream

View File

@@ -106,9 +106,8 @@ namespace Jellyfin.Xtream
return await GetChannels(cancellationToken).ConfigureAwait(false); return await GetChannels(cancellationToken).ConfigureAwait(false);
} }
int separator = query.FolderId.IndexOf('-', StringComparison.InvariantCulture); Guid guid = Guid.Parse(query.FolderId);
int categoryId = int.Parse(query.FolderId.Substring(0, separator), CultureInfo.InvariantCulture); StreamService.FromGuid(guid, out int prefix, out int categoryId, out int channelId, out int _);
int channelId = int.Parse(query.FolderId.Substring(separator + 1), CultureInfo.InvariantCulture);
return await GetStreams(categoryId, channelId, cancellationToken).ConfigureAwait(false); return await GetStreams(categoryId, channelId, cancellationToken).ConfigureAwait(false);
} }
@@ -124,10 +123,10 @@ namespace Jellyfin.Xtream
continue; continue;
} }
ParsedName parsedName = plugin.StreamService.ParseName(channel.Name); ParsedName parsedName = StreamService.ParseName(channel.Name);
items.Add(new ChannelItemInfo() items.Add(new ChannelItemInfo()
{ {
Id = $"{channel.CategoryId}-{channel.StreamId}", Id = StreamService.ToGuid(StreamService.CatchupPrefix, channel.CategoryId, channel.StreamId, 0).ToString(),
ImageUrl = channel.StreamIcon, ImageUrl = channel.StreamIcon,
Name = parsedName.Title, Name = parsedName.Title,
Tags = new List<string>(parsedName.Tags), Tags = new List<string>(parsedName.Tags),
@@ -173,7 +172,7 @@ namespace Jellyfin.Xtream
{ {
ContentType = ChannelMediaContentType.TvExtra, ContentType = ChannelMediaContentType.TvExtra,
FolderType = ChannelFolderType.Container, FolderType = ChannelFolderType.Container,
Id = $"fallback-{channelId}", Id = StreamService.ToGuid(StreamService.FallbackPrefix, channelId, 0, 0).ToString(),
IsLiveStream = false, IsLiveStream = false,
MediaSources = new List<MediaSourceInfo>() MediaSources = new List<MediaSourceInfo>()
{ {
@@ -194,7 +193,7 @@ namespace Jellyfin.Xtream
foreach (EpgInfo epg in epgs.Listings.Where(epg => epg.Start < startBefore && epg.Start >= startAfter)) foreach (EpgInfo epg in epgs.Listings.Where(epg => epg.Start < startBefore && epg.Start >= startAfter))
{ {
string id = epg.Id.ToString(System.Globalization.CultureInfo.InvariantCulture); 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); int durationMinutes = (int)Math.Ceiling((epg.End - epg.Start).TotalMinutes);
string dateTitle = epg.Start.ToLocalTime().ToString("ddd HH:mm", CultureInfo.InvariantCulture); string dateTitle = epg.Start.ToLocalTime().ToString("ddd HH:mm", CultureInfo.InvariantCulture);
List<MediaSourceInfo> sources = new List<MediaSourceInfo>() List<MediaSourceInfo> sources = new List<MediaSourceInfo>()

View File

@@ -49,25 +49,6 @@ namespace Jellyfin.Xtream.Configuration
{ {
} }
/// <summary>
/// Initializes a new instance of the
/// <see cref="SerializableDictionary&lt;TKey, TValue&gt;"/> class.
/// </summary>
/// <param name="info">A
/// <see cref="System.Runtime.Serialization.SerializationInfo"/> object
/// containing the information required to serialize the
/// <see cref="System.Collections.Generic.Dictionary{TKey, TValue}"/>.
/// </param>
/// <param name="context">A
/// <see cref="System.Runtime.Serialization.StreamingContext"/> structure
/// containing the source and destination of the serialized stream
/// associated with the
/// <see cref="System.Collections.Generic.Dictionary{TKey, TValue}"/>.
/// </param>
private SerializableDictionary(SerializationInfo info, StreamingContext context) : base(info, context)
{
}
private string ItemTagName => DefaultItemTag; private string ItemTagName => DefaultItemTag;
private string KeyTagName => DefaultKeyTag; private string KeyTagName => DefaultKeyTag;

View File

@@ -1,10 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<RootNamespace>Jellyfin.Xtream</RootNamespace> <RootNamespace>Jellyfin.Xtream</RootNamespace>
<AssemblyVersion>0.6.1.0</AssemblyVersion> <AssemblyVersion>0.6.2.0</AssemblyVersion>
<FileVersion>0.6.1.0</FileVersion> <FileVersion>0.6.2.0</FileVersion>
<GenerateDocumentationFile>true</GenerateDocumentationFile> <GenerateDocumentationFile>true</GenerateDocumentationFile>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors> <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
@@ -13,8 +13,8 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Jellyfin.Controller" Version="10.8.4" /> <PackageReference Include="Jellyfin.Controller" Version="10.9.1" />
<PackageReference Include="Jellyfin.Model" Version="10.8.4" /> <PackageReference Include="Jellyfin.Model" Version="10.9.1" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup> </ItemGroup>
@@ -24,7 +24,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.507" PrivateAssets="All" /> <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556" PrivateAssets="All" />
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
</ItemGroup> </ItemGroup>

View File

@@ -70,7 +70,7 @@ namespace Jellyfin.Xtream
List<ChannelInfo> items = new List<ChannelInfo>(); List<ChannelInfo> items = new List<ChannelInfo>();
await foreach (StreamInfo channel in plugin.StreamService.GetLiveStreamsWithOverrides(cancellationToken)) 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() items.Add(new ChannelInfo()
{ {
Id = channel.StreamId.ToString(CultureInfo.InvariantCulture), Id = channel.StreamId.ToString(CultureInfo.InvariantCulture),
@@ -170,7 +170,7 @@ namespace Jellyfin.Xtream
{ {
string key = $"xtream-epg-{channelId}"; string key = $"xtream-epg-{channelId}";
ICollection<ProgramInfo>? items = null; ICollection<ProgramInfo>? items = null;
if (memoryCache.TryGetValue(key, out ICollection<ProgramInfo> o)) if (memoryCache.TryGetValue(key, out ICollection<ProgramInfo>? o))
{ {
items = o; items = o;
} }
@@ -204,12 +204,6 @@ namespace Jellyfin.Xtream
select epg; select epg;
} }
/// <inheritdoc />
public Task RecordLiveStream(string id, CancellationToken cancellationToken)
{
throw new NotImplementedException();
}
/// <inheritdoc /> /// <inheritdoc />
public Task ResetTuner(string id, CancellationToken cancellationToken) public Task ResetTuner(string id, CancellationToken cancellationToken)
{ {

View File

@@ -132,15 +132,15 @@ namespace Jellyfin.Xtream
// - This will update the TV channels. // - This will update the TV channels.
// - This will remove channels on credentials change. // - This will remove channels on credentials change.
TaskService.CancelIfRunningAndQueue( TaskService.CancelIfRunningAndQueue(
"Emby.Server.Implementations", "Jellyfin.LiveTv",
"Emby.Server.Implementations.LiveTv.RefreshGuideScheduledTask"); "Jellyfin.LiveTv.Guide.RefreshGuideScheduledTask");
// Force a refresh of Channels on configuration update. // Force a refresh of Channels on configuration update.
// - This will update the channel entries. // - This will update the channel entries.
// - This will remove channel entries on credentials change. // - This will remove channel entries on credentials change.
TaskService.CancelIfRunningAndQueue( TaskService.CancelIfRunningAndQueue(
"Emby.Server.Implementations", "Jellyfin.LiveTv",
"Emby.Server.Implementations.Channels.RefreshChannelsScheduledTask"); "Jellyfin.LiveTv.Channels.RefreshChannelsScheduledTask");
} }
} }
} }

View File

@@ -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 <https://www.gnu.org/licenses/>.
using MediaBrowser.Controller;
using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.Plugins;
using Microsoft.Extensions.DependencyInjection;
namespace Jellyfin.Xtream;
/// <inheritdoc />
public class PluginServiceRegistrator : IPluginServiceRegistrator
{
/// <inheritdoc />
public void RegisterServices(IServiceCollection serviceCollection, IServerApplicationHost applicationHost)
{
serviceCollection.AddSingleton<ILiveTvService, LiveTvService>();
serviceCollection.AddSingleton<IChannel, CatchupChannel>();
serviceCollection.AddSingleton<IChannel, SeriesChannel>();
serviceCollection.AddSingleton<IChannel, VodChannel>();
}
}

View File

@@ -101,31 +101,25 @@ namespace Jellyfin.Xtream
/// <inheritdoc /> /// <inheritdoc />
public async Task<ChannelItemResult> GetChannelItems(InternalChannelItemQuery query, CancellationToken cancellationToken) public async Task<ChannelItemResult> GetChannelItems(InternalChannelItemQuery query, CancellationToken cancellationToken)
{ {
Plugin plugin = Plugin.Instance;
if (string.IsNullOrEmpty(query.FolderId)) if (string.IsNullOrEmpty(query.FolderId))
{ {
return await GetCategories(cancellationToken).ConfigureAwait(false); 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); 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); 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); return await GetEpisodes(seriesId, seasonId, cancellationToken).ConfigureAwait(false);
} }
@@ -137,14 +131,14 @@ namespace Jellyfin.Xtream
private ChannelItemInfo CreateChannelItemInfo(Series series) private ChannelItemInfo CreateChannelItemInfo(Series series)
{ {
ParsedName parsedName = Plugin.Instance.StreamService.ParseName(series.Name); ParsedName parsedName = StreamService.ParseName(series.Name);
return new ChannelItemInfo() return new ChannelItemInfo()
{ {
CommunityRating = (float)series.Rating5Based, CommunityRating = (float)series.Rating5Based,
DateModified = series.LastModified, DateModified = series.LastModified,
// FolderType = ChannelFolderType.Series, // FolderType = ChannelFolderType.Series,
Genres = GetGenres(series.Genre), Genres = GetGenres(series.Genre),
Id = $"{StreamService.SeriesPrefix}{series.SeriesId}", Id = StreamService.ToGuid(StreamService.SeriesPrefix, series.CategoryId, series.SeriesId, 0).ToString(),
ImageUrl = series.Cover, ImageUrl = series.Cover,
Name = parsedName.Title, Name = parsedName.Title,
People = GetPeople(series.Cast), People = GetPeople(series.Cast),
@@ -178,7 +172,7 @@ namespace Jellyfin.Xtream
Season? season = series.Seasons.FirstOrDefault(s => s.SeasonId == seasonId); Season? season = series.Seasons.FirstOrDefault(s => s.SeasonId == seasonId);
if (season != null) if (season != null)
{ {
ParsedName parsedName = Plugin.Instance.StreamService.ParseName(season.Name); ParsedName parsedName = StreamService.ParseName(season.Name);
name = parsedName.Title; name = parsedName.Title;
tags.AddRange(parsedName.Tags); tags.AddRange(parsedName.Tags);
created = season.AirDate; created = season.AirDate;
@@ -194,7 +188,7 @@ namespace Jellyfin.Xtream
DateCreated = created, DateCreated = created,
// FolderType = ChannelFolderType.Season, // FolderType = ChannelFolderType.Season,
Genres = GetGenres(serie.Genre), Genres = GetGenres(serie.Genre),
Id = $"{StreamService.SeasonPrefix}{seriesId}-{seasonId}", Id = StreamService.ToGuid(StreamService.SeasonPrefix, serie.CategoryId, seriesId, seasonId).ToString(),
ImageUrl = cover, ImageUrl = cover,
Name = name, Name = name,
Overview = overview, Overview = overview,
@@ -207,7 +201,7 @@ namespace Jellyfin.Xtream
private ChannelItemInfo CreateChannelItemInfo(SeriesStreamInfo series, Season? season, Episode episode) private ChannelItemInfo CreateChannelItemInfo(SeriesStreamInfo series, Season? season, Episode episode)
{ {
Jellyfin.Xtream.Client.Models.SeriesInfo serie = series.Info; Jellyfin.Xtream.Client.Models.SeriesInfo serie = series.Info;
ParsedName parsedName = Plugin.Instance.StreamService.ParseName(episode.Title); ParsedName parsedName = StreamService.ParseName(episode.Title);
List<MediaSourceInfo> sources = new List<MediaSourceInfo>() List<MediaSourceInfo> sources = new List<MediaSourceInfo>()
{ {
Plugin.Instance.StreamService.GetMediaSourceInfo(StreamType.Series, episode.EpisodeId, episode.ContainerExtension) Plugin.Instance.StreamService.GetMediaSourceInfo(StreamType.Series, episode.EpisodeId, episode.ContainerExtension)
@@ -229,7 +223,7 @@ namespace Jellyfin.Xtream
ContentType = ChannelMediaContentType.Episode, ContentType = ChannelMediaContentType.Episode,
DateCreated = DateTimeOffset.FromUnixTimeSeconds(episode.Added).DateTime, DateCreated = DateTimeOffset.FromUnixTimeSeconds(episode.Added).DateTime,
Genres = GetGenres(serie.Genre), Genres = GetGenres(serie.Genre),
Id = $"{StreamService.EpisodePrefix}{episode.EpisodeId}", Id = StreamService.ToGuid(StreamService.EpisodePrefix, 0, 0, episode.EpisodeId).ToString(),
ImageUrl = cover, ImageUrl = cover,
IsLiveStream = false, IsLiveStream = false,
MediaSources = sources, MediaSources = sources,
@@ -246,7 +240,7 @@ namespace Jellyfin.Xtream
{ {
List<ChannelItemInfo> items = new List<ChannelItemInfo>( List<ChannelItemInfo> items = new List<ChannelItemInfo>(
(await Plugin.Instance.StreamService.GetSeriesCategories(cancellationToken).ConfigureAwait(false)) (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() return new ChannelItemResult()
{ {
Items = items, Items = items,

View File

@@ -158,7 +158,7 @@ namespace Jellyfin.Xtream.Service
throw new ArgumentNullException("copyTask"); throw new ArgumentNullException("copyTask");
} }
tokenSource.Cancel(); await tokenSource.CancelAsync().ConfigureAwait(false);
await copyTask.ConfigureAwait(false); await copyTask.ConfigureAwait(false);
} }

View File

@@ -37,29 +37,49 @@ namespace Jellyfin.Xtream.Service
public class StreamService public class StreamService
{ {
/// <summary> /// <summary>
/// The id prefix for category channel items. /// The id prefix for VOD category channel items.
/// </summary> /// </summary>
public const string CategoryPrefix = "category-"; public const int VodCategoryPrefix = 0x5d774c35;
/// <summary> /// <summary>
/// The id prefix for stream channel items. /// The id prefix for stream channel items.
/// </summary> /// </summary>
public const string StreamPrefix = "stream-"; public const int StreamPrefix = 0x5d774c36;
/// <summary> /// <summary>
/// The id prefix for series channel items. /// The id prefix for series category channel items.
/// </summary> /// </summary>
public const string SeriesPrefix = "series-"; public const int SeriesCategoryPrefix = 0x5d774c37;
/// <summary>
/// The id prefix for series category channel items.
/// </summary>
public const int SeriesPrefix = 0x5d774c38;
/// <summary> /// <summary>
/// The id prefix for season channel items. /// The id prefix for season channel items.
/// </summary> /// </summary>
public const string SeasonPrefix = "seasons-"; public const int SeasonPrefix = 0x5d774c39;
/// <summary> /// <summary>
/// The id prefix for season channel items. /// The id prefix for season channel items.
/// </summary> /// </summary>
public const string EpisodePrefix = "episode-"; public const int EpisodePrefix = 0x5d774c3a;
/// <summary>
/// The id prefix for catchup channel items.
/// </summary>
public const int CatchupPrefix = 0x5d774c3b;
/// <summary>
/// The id prefix for fallback EPG items.
/// </summary>
public const int FallbackPrefix = 0x5d774c3c;
/// <summary>
/// The id prefix for media source items.
/// </summary>
public const int MediaSourcePrefix = 0x5d774c3d;
private static readonly Regex TagRegex = new Regex(@"\[([^\]]+)\]|\|([^\|]+)\|"); private static readonly Regex TagRegex = new Regex(@"\[([^\]]+)\]|\|([^\|]+)\|");
@@ -89,7 +109,7 @@ namespace Jellyfin.Xtream.Service
/// </summary> /// </summary>
/// <param name="name">The name which should be parsed.</param> /// <param name="name">The name which should be parsed.</param>
/// <returns>A <see cref="ParsedName"/> struct containing the cleaned title and parsed tags.</returns> /// <returns>A <see cref="ParsedName"/> struct containing the cleaned title and parsed tags.</returns>
public ParsedName ParseName(string name) public static ParsedName ParseName(string name)
{ {
List<string> tags = new List<string>(); List<string> tags = new List<string>();
string title = TagRegex.Replace( string title = TagRegex.Replace(
@@ -115,28 +135,6 @@ namespace Jellyfin.Xtream.Service
}; };
} }
/// <summary>
/// Checks if the id string is an id with the given prefix.
/// </summary>
/// <param name="id">The id string.</param>
/// <param name="prefix">The prefix string.</param>
/// <returns>Whether or not the id string has the given prefix.</returns>
public bool IsId(string id, string prefix)
{
return id.StartsWith(prefix, StringComparison.InvariantCulture);
}
/// <summary>
/// Parses the given id by removing the prefix.
/// </summary>
/// <param name="id">The id string.</param>
/// <param name="prefix">The prefix string.</param>
/// <returns>The parsed it as integer.</returns>
public int ParseId(string id, string prefix)
{
return int.Parse(id.Substring(prefix.Length), CultureInfo.InvariantCulture);
}
private bool IsConfigured(SerializableDictionary<int, HashSet<int>> config, int category, int id) private bool IsConfigured(SerializableDictionary<int, HashSet<int>> config, int category, int id)
{ {
HashSet<int>? values; HashSet<int>? values;
@@ -192,14 +190,15 @@ namespace Jellyfin.Xtream.Service
/// <summary> /// <summary>
/// Gets an channel item info for the category. /// Gets an channel item info for the category.
/// </summary> /// </summary>
/// <param name="prefix">The channel category prefix.</param>
/// <param name="category">The Xtream category.</param> /// <param name="category">The Xtream category.</param>
/// <returns>A channel item representing the category.</returns> /// <returns>A channel item representing the category.</returns>
public ChannelItemInfo CreateChannelItemInfo(Category category) public static ChannelItemInfo CreateChannelItemInfo(int prefix, Category category)
{ {
ParsedName parsedName = ParseName(category.CategoryName); ParsedName parsedName = ParseName(category.CategoryName);
return new ChannelItemInfo() return new ChannelItemInfo()
{ {
Id = $"{CategoryPrefix}{category.CategoryId}", Id = ToGuid(prefix, category.CategoryId, 0, 0).ToString(),
Name = category.CategoryName, Name = category.CategoryName,
Tags = new List<string>(parsedName.Tags), Tags = new List<string>(parsedName.Tags),
Type = ChannelItemType.Folder, 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);
}
/// <summary>
/// Gets a GUID representing the four 32-bit integers.
/// </summary>
/// <param name="i0">Bytes 0-3.</param>
/// <param name="i1">Bytes 4-7.</param>
/// <param name="i2">Bytes 8-11.</param>
/// <param name="i3">Bytes 12-15.</param>
/// <returns>Guid.</returns>
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);
}
/// <summary>
/// Gets the four 32-bit integers represented in the GUID.
/// </summary>
/// <param name="guid">The input GUID.</param>
/// <param name="i0">Bytes 0-3.</param>
/// <param name="i1">Bytes 4-7.</param>
/// <param name="i2">Bytes 8-11.</param>
/// <param name="i3">Bytes 12-15.</param>
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);
}
}
/// <summary> /// <summary>
/// Gets the media source information for the given Xtream stream. /// Gets the media source information for the given Xtream stream.
/// </summary> /// </summary>
@@ -359,7 +415,7 @@ namespace Jellyfin.Xtream.Service
return new MediaSourceInfo() return new MediaSourceInfo()
{ {
EncoderProtocol = MediaProtocol.Http, EncoderProtocol = MediaProtocol.Http,
Id = id.ToString(CultureInfo.InvariantCulture), Id = ToGuid(MediaSourcePrefix, (int)type, id, 0).ToString(),
IsInfiniteStream = isLive, IsInfiniteStream = isLive,
IsRemote = true, IsRemote = true,
Name = "default", Name = "default",

View File

@@ -18,26 +18,26 @@ namespace Jellyfin.Xtream.Service
/// <summary> /// <summary>
/// An enum describing the Xtream stream types. /// An enum describing the Xtream stream types.
/// </summary> /// </summary>
public enum StreamType public enum StreamType : int
{ {
/// <summary> /// <summary>
/// Live IPTV. /// Live IPTV.
/// </summary> /// </summary>
Live, Live = 0,
/// <summary> /// <summary>
/// Catch up IPTV. /// Catch up IPTV.
/// </summary> /// </summary>
CatchUp, CatchUp = 1,
/// <summary> /// <summary>
/// On-demand series grouped in seasons and episodes. /// On-demand series grouped in seasons and episodes.
/// </summary> /// </summary>
Series, Series = 2,
/// <summary> /// <summary>
/// Video on-demand. /// Video on-demand.
/// </summary> /// </summary>
Vod, Vod = 3,
} }
} }

View File

@@ -100,15 +100,15 @@ namespace Jellyfin.Xtream
/// <inheritdoc /> /// <inheritdoc />
public async Task<ChannelItemResult> GetChannelItems(InternalChannelItemQuery query, CancellationToken cancellationToken) public async Task<ChannelItemResult> GetChannelItems(InternalChannelItemQuery query, CancellationToken cancellationToken)
{ {
Plugin plugin = Plugin.Instance;
if (string.IsNullOrEmpty(query.FolderId)) if (string.IsNullOrEmpty(query.FolderId))
{ {
return await GetCategories(cancellationToken).ConfigureAwait(false); 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); return await GetStreams(categoryId, cancellationToken).ConfigureAwait(false);
} }
@@ -121,7 +121,7 @@ namespace Jellyfin.Xtream
private ChannelItemInfo CreateChannelItemInfo(StreamInfo stream) private ChannelItemInfo CreateChannelItemInfo(StreamInfo stream)
{ {
long added = long.Parse(stream.Added, CultureInfo.InvariantCulture); long added = long.Parse(stream.Added, CultureInfo.InvariantCulture);
ParsedName parsedName = Plugin.Instance.StreamService.ParseName(stream.Name); ParsedName parsedName = StreamService.ParseName(stream.Name);
List<MediaSourceInfo> sources = new List<MediaSourceInfo>() List<MediaSourceInfo> sources = new List<MediaSourceInfo>()
{ {
Plugin.Instance.StreamService.GetMediaSourceInfo(StreamType.Vod, stream.StreamId, stream.ContainerExtension) Plugin.Instance.StreamService.GetMediaSourceInfo(StreamType.Vod, stream.StreamId, stream.ContainerExtension)
@@ -147,7 +147,7 @@ namespace Jellyfin.Xtream
{ {
List<ChannelItemInfo> items = new List<ChannelItemInfo>( List<ChannelItemInfo> items = new List<ChannelItemInfo>(
(await Plugin.Instance.StreamService.GetVodCategories(cancellationToken).ConfigureAwait(false)) (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() return new ChannelItemResult()
{ {
Items = items, Items = items,

View File

@@ -1,9 +1,9 @@
--- ---
name: "Jellyfin Xtream" name: "Jellyfin Xtream"
guid: "5d774c35-8567-46d3-a950-9bb8227a0c5d" guid: "5d774c35-8567-46d3-a950-9bb8227a0c5d"
version: "0.6.1.0" version: "0.6.2.0"
targetAbi: "10.8.4.0" targetAbi: "10.8.4.0"
framework: "net6.0" framework: "net8.0"
overview: "Stream content from an Xtream-compatible server." overview: "Stream content from an Xtream-compatible server."
description: > description: >
Stream Live IPTV, Video On-Demand, and Series from an Xtream-compatible server using this plugin. Stream Live IPTV, Video On-Demand, and Series from an Xtream-compatible server using this plugin.