Support Jellyfin 10.9 #96
4
.github/workflows/build.yaml
vendored
4
.github/workflows/build.yaml
vendored
@@ -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"
|
||||||
|
4
.github/workflows/publish.yaml
vendored
4
.github/workflows/publish.yaml
vendored
@@ -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:
|
||||||
|
2
.github/workflows/scan-codeql.yaml
vendored
2
.github/workflows/scan-codeql.yaml
vendored
@@ -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
|
||||||
|
@@ -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>()
|
||||||
|
@@ -49,25 +49,6 @@ namespace Jellyfin.Xtream.Configuration
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the
|
|
||||||
/// <see cref="SerializableDictionary<TKey, TValue>"/> 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;
|
||||||
|
@@ -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>
|
||||||
|
|
||||||
|
@@ -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)
|
||||||
{
|
{
|
||||||
|
@@ -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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
35
Jellyfin.Xtream/PluginServiceRegistrator.cs
Normal file
35
Jellyfin.Xtream/PluginServiceRegistrator.cs
Normal 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>();
|
||||||
|
}
|
||||||
|
}
|
@@ -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,
|
||||||
|
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -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",
|
||||||
|
@@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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,
|
||||||
|
@@ -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.
|
||||||
|
Reference in New Issue
Block a user