Solve most code style warnings #149
@@ -35,26 +35,15 @@ namespace Jellyfin.Xtream.Api;
|
|||||||
[Produces(MediaTypeNames.Application.Json)]
|
[Produces(MediaTypeNames.Application.Json)]
|
||||||
public class XtreamController : ControllerBase
|
public class XtreamController : ControllerBase
|
||||||
{
|
{
|
||||||
private readonly ILogger<XtreamController> logger;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the <see cref="XtreamController"/> class.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param>
|
|
||||||
public XtreamController(ILogger<XtreamController> logger)
|
|
||||||
{
|
|
||||||
this.logger = logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static CategoryResponse CreateCategoryResponse(Category category) =>
|
private static CategoryResponse CreateCategoryResponse(Category category) =>
|
||||||
new CategoryResponse()
|
new()
|
||||||
{
|
{
|
||||||
Id = category.CategoryId,
|
Id = category.CategoryId,
|
||||||
Name = category.CategoryName,
|
Name = category.CategoryName,
|
||||||
};
|
};
|
||||||
|
|
||||||
private static ItemResponse CreateItemResponse(StreamInfo stream) =>
|
private static ItemResponse CreateItemResponse(StreamInfo stream) =>
|
||||||
new ItemResponse()
|
new()
|
||||||
{
|
{
|
||||||
Id = stream.StreamId,
|
Id = stream.StreamId,
|
||||||
Name = stream.Name,
|
Name = stream.Name,
|
||||||
@@ -63,7 +52,7 @@ public class XtreamController : ControllerBase
|
|||||||
};
|
};
|
||||||
|
|
||||||
private static ItemResponse CreateItemResponse(Series series) =>
|
private static ItemResponse CreateItemResponse(Series series) =>
|
||||||
new ItemResponse()
|
new()
|
||||||
{
|
{
|
||||||
Id = series.SeriesId,
|
Id = series.SeriesId,
|
||||||
Name = series.Name,
|
Name = series.Name,
|
||||||
@@ -72,7 +61,7 @@ public class XtreamController : ControllerBase
|
|||||||
};
|
};
|
||||||
|
|
||||||
private static ChannelResponse CreateChannelResponse(StreamInfo stream) =>
|
private static ChannelResponse CreateChannelResponse(StreamInfo stream) =>
|
||||||
new ChannelResponse()
|
new()
|
||||||
{
|
{
|
||||||
Id = stream.StreamId,
|
Id = stream.StreamId,
|
||||||
LogoUrl = stream.StreamIcon,
|
LogoUrl = stream.StreamIcon,
|
||||||
@@ -90,11 +79,9 @@ public class XtreamController : ControllerBase
|
|||||||
public async Task<ActionResult<IEnumerable<CategoryResponse>>> GetLiveCategories(CancellationToken cancellationToken)
|
public async Task<ActionResult<IEnumerable<CategoryResponse>>> GetLiveCategories(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
Plugin plugin = Plugin.Instance;
|
Plugin plugin = Plugin.Instance;
|
||||||
using (XtreamClient client = new XtreamClient())
|
using XtreamClient client = new XtreamClient();
|
||||||
{
|
List<Category> categories = await client.GetLiveCategoryAsync(plugin.Creds, cancellationToken).ConfigureAwait(false);
|
||||||
List<Category> categories = await client.GetLiveCategoryAsync(plugin.Creds, cancellationToken).ConfigureAwait(false);
|
return Ok(categories.Select(CreateCategoryResponse));
|
||||||
return Ok(categories.Select((Category c) => CreateCategoryResponse(c)));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -108,14 +95,12 @@ public class XtreamController : ControllerBase
|
|||||||
public async Task<ActionResult<IEnumerable<StreamInfo>>> GetLiveStreams(int categoryId, CancellationToken cancellationToken)
|
public async Task<ActionResult<IEnumerable<StreamInfo>>> GetLiveStreams(int categoryId, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
Plugin plugin = Plugin.Instance;
|
Plugin plugin = Plugin.Instance;
|
||||||
using (XtreamClient client = new XtreamClient())
|
using XtreamClient client = new XtreamClient();
|
||||||
{
|
List<StreamInfo> streams = await client.GetLiveStreamsByCategoryAsync(
|
||||||
List<StreamInfo> streams = await client.GetLiveStreamsByCategoryAsync(
|
plugin.Creds,
|
||||||
plugin.Creds,
|
categoryId,
|
||||||
categoryId,
|
cancellationToken).ConfigureAwait(false);
|
||||||
cancellationToken).ConfigureAwait(false);
|
return Ok(streams.Select(CreateItemResponse));
|
||||||
return Ok(streams.Select((StreamInfo s) => CreateItemResponse(s)));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -128,11 +113,9 @@ public class XtreamController : ControllerBase
|
|||||||
public async Task<ActionResult<IEnumerable<CategoryResponse>>> GetVodCategories(CancellationToken cancellationToken)
|
public async Task<ActionResult<IEnumerable<CategoryResponse>>> GetVodCategories(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
Plugin plugin = Plugin.Instance;
|
Plugin plugin = Plugin.Instance;
|
||||||
using (XtreamClient client = new XtreamClient())
|
using XtreamClient client = new XtreamClient();
|
||||||
{
|
List<Category> categories = await client.GetVodCategoryAsync(plugin.Creds, cancellationToken).ConfigureAwait(false);
|
||||||
List<Category> categories = await client.GetVodCategoryAsync(plugin.Creds, cancellationToken).ConfigureAwait(false);
|
return Ok(categories.Select(CreateCategoryResponse));
|
||||||
return Ok(categories.Select((Category c) => CreateCategoryResponse(c)));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -146,14 +129,12 @@ public class XtreamController : ControllerBase
|
|||||||
public async Task<ActionResult<IEnumerable<StreamInfo>>> GetVodStreams(int categoryId, CancellationToken cancellationToken)
|
public async Task<ActionResult<IEnumerable<StreamInfo>>> GetVodStreams(int categoryId, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
Plugin plugin = Plugin.Instance;
|
Plugin plugin = Plugin.Instance;
|
||||||
using (XtreamClient client = new XtreamClient())
|
using XtreamClient client = new XtreamClient();
|
||||||
{
|
List<StreamInfo> streams = await client.GetVodStreamsByCategoryAsync(
|
||||||
List<StreamInfo> streams = await client.GetVodStreamsByCategoryAsync(
|
plugin.Creds,
|
||||||
plugin.Creds,
|
categoryId,
|
||||||
categoryId,
|
cancellationToken).ConfigureAwait(false);
|
||||||
cancellationToken).ConfigureAwait(false);
|
return Ok(streams.Select(CreateItemResponse));
|
||||||
return Ok(streams.Select((StreamInfo s) => CreateItemResponse(s)));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -166,11 +147,9 @@ public class XtreamController : ControllerBase
|
|||||||
public async Task<ActionResult<IEnumerable<CategoryResponse>>> GetSeriesCategories(CancellationToken cancellationToken)
|
public async Task<ActionResult<IEnumerable<CategoryResponse>>> GetSeriesCategories(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
Plugin plugin = Plugin.Instance;
|
Plugin plugin = Plugin.Instance;
|
||||||
using (XtreamClient client = new XtreamClient())
|
using XtreamClient client = new XtreamClient();
|
||||||
{
|
List<Category> categories = await client.GetSeriesCategoryAsync(plugin.Creds, cancellationToken).ConfigureAwait(false);
|
||||||
List<Category> categories = await client.GetSeriesCategoryAsync(plugin.Creds, cancellationToken).ConfigureAwait(false);
|
return Ok(categories.Select(CreateCategoryResponse));
|
||||||
return Ok(categories.Select((Category c) => CreateCategoryResponse(c)));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -184,14 +163,12 @@ public class XtreamController : ControllerBase
|
|||||||
public async Task<ActionResult<IEnumerable<StreamInfo>>> GetSeriesStreams(int categoryId, CancellationToken cancellationToken)
|
public async Task<ActionResult<IEnumerable<StreamInfo>>> GetSeriesStreams(int categoryId, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
Plugin plugin = Plugin.Instance;
|
Plugin plugin = Plugin.Instance;
|
||||||
using (XtreamClient client = new XtreamClient())
|
using XtreamClient client = new XtreamClient();
|
||||||
{
|
List<Series> series = await client.GetSeriesByCategoryAsync(
|
||||||
List<Series> series = await client.GetSeriesByCategoryAsync(
|
plugin.Creds,
|
||||||
plugin.Creds,
|
categoryId,
|
||||||
categoryId,
|
cancellationToken).ConfigureAwait(false);
|
||||||
cancellationToken).ConfigureAwait(false);
|
return Ok(series.Select(CreateItemResponse));
|
||||||
return Ok(series.Select((Series s) => CreateItemResponse(s)));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@@ -34,18 +34,10 @@ namespace Jellyfin.Xtream;
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// The Xtream Codes API channel.
|
/// The Xtream Codes API channel.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class CatchupChannel : IChannel
|
/// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param>
|
||||||
|
public class CatchupChannel(ILogger<CatchupChannel> logger) : IChannel
|
||||||
{
|
{
|
||||||
private readonly ILogger<CatchupChannel> logger;
|
private readonly ILogger<CatchupChannel> _logger = logger;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the <see cref="CatchupChannel"/> class.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param>
|
|
||||||
public CatchupChannel(ILogger<CatchupChannel> logger)
|
|
||||||
{
|
|
||||||
this.logger = logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public string? Name => "Xtream Catch-up";
|
public string? Name => "Xtream Catch-up";
|
||||||
@@ -67,15 +59,12 @@ public class CatchupChannel : IChannel
|
|||||||
{
|
{
|
||||||
return new InternalChannelFeatures
|
return new InternalChannelFeatures
|
||||||
{
|
{
|
||||||
ContentTypes = new List<ChannelMediaContentType>
|
ContentTypes = [
|
||||||
{
|
|
||||||
ChannelMediaContentType.TvExtra,
|
ChannelMediaContentType.TvExtra,
|
||||||
},
|
],
|
||||||
|
MediaTypes = [
|
||||||
MediaTypes = new List<ChannelMediaType>
|
|
||||||
{
|
|
||||||
ChannelMediaType.Video
|
ChannelMediaType.Video
|
||||||
},
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,13 +79,10 @@ public class CatchupChannel : IChannel
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public IEnumerable<ImageType> GetSupportedChannelImages()
|
public IEnumerable<ImageType> GetSupportedChannelImages() => new List<ImageType>
|
||||||
{
|
{
|
||||||
return new List<ImageType>
|
// ImageType.Primary
|
||||||
{
|
};
|
||||||
// ImageType.Primary
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<ChannelItemResult> GetChannelItems(InternalChannelItemQuery query, CancellationToken cancellationToken)
|
public async Task<ChannelItemResult> GetChannelItems(InternalChannelItemQuery query, CancellationToken cancellationToken)
|
||||||
@@ -120,7 +106,7 @@ public class CatchupChannel : IChannel
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
logger.LogError(ex, "Failed to get channel items");
|
_logger.LogError(ex, "Failed to get channel items");
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -128,7 +114,7 @@ public class CatchupChannel : IChannel
|
|||||||
private async Task<ChannelItemResult> GetChannels(CancellationToken cancellationToken)
|
private async Task<ChannelItemResult> GetChannels(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
Plugin plugin = Plugin.Instance;
|
Plugin plugin = Plugin.Instance;
|
||||||
List<ChannelItemInfo> items = new List<ChannelItemInfo>();
|
List<ChannelItemInfo> items = [];
|
||||||
foreach (StreamInfo channel in await plugin.StreamService.GetLiveStreamsWithOverrides(cancellationToken).ConfigureAwait(false))
|
foreach (StreamInfo channel in await plugin.StreamService.GetLiveStreamsWithOverrides(cancellationToken).ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
if (!channel.TvArchive)
|
if (!channel.TvArchive)
|
||||||
@@ -159,39 +145,34 @@ public class CatchupChannel : IChannel
|
|||||||
private async Task<ChannelItemResult> GetDays(int categoryId, int channelId, CancellationToken cancellationToken)
|
private async Task<ChannelItemResult> GetDays(int categoryId, int channelId, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
Plugin plugin = Plugin.Instance;
|
Plugin plugin = Plugin.Instance;
|
||||||
using (XtreamClient client = new XtreamClient())
|
using XtreamClient client = new XtreamClient();
|
||||||
|
|
||||||
|
List<StreamInfo> streams = await client.GetLiveStreamsByCategoryAsync(plugin.Creds, categoryId, cancellationToken).ConfigureAwait(false);
|
||||||
|
StreamInfo channel = streams.FirstOrDefault(s => s.StreamId == channelId)
|
||||||
|
?? throw new ArgumentException($"Channel with id {channelId} not found in category {categoryId}");
|
||||||
|
ParsedName parsedName = StreamService.ParseName(channel.Name);
|
||||||
|
|
||||||
|
List<ChannelItemInfo> items = [];
|
||||||
|
for (int i = 0; i <= channel.TvArchiveDuration; i++)
|
||||||
{
|
{
|
||||||
StreamInfo? channel = (
|
DateTime channelDay = DateTime.Today.AddDays(-i);
|
||||||
await client.GetLiveStreamsByCategoryAsync(plugin.Creds, categoryId, cancellationToken).ConfigureAwait(false)
|
int day = (int)(channelDay - DateTime.UnixEpoch).TotalDays;
|
||||||
).FirstOrDefault(s => s.StreamId == channelId);
|
items.Add(new()
|
||||||
if (channel == null)
|
|
||||||
{
|
{
|
||||||
throw new ArgumentException($"Channel with id {channelId} not found in category {categoryId}");
|
Id = StreamService.ToGuid(StreamService.CatchupPrefix, channel.CategoryId, channel.StreamId, day).ToString(),
|
||||||
}
|
ImageUrl = channel.StreamIcon,
|
||||||
|
Name = channelDay.ToLocalTime().ToString("ddd dd'-'MM'-'yyyy", CultureInfo.InvariantCulture),
|
||||||
ParsedName parsedName = StreamService.ParseName(channel.Name);
|
Tags = new List<string>(parsedName.Tags),
|
||||||
List<ChannelItemInfo> items = new List<ChannelItemInfo>();
|
Type = ChannelItemType.Folder,
|
||||||
for (int i = 0; i <= channel.TvArchiveDuration; i++)
|
});
|
||||||
{
|
|
||||||
DateTime channelDay = DateTime.Today.AddDays(-i);
|
|
||||||
int day = (int)(channelDay - DateTime.UnixEpoch).TotalDays;
|
|
||||||
items.Add(new ChannelItemInfo()
|
|
||||||
{
|
|
||||||
Id = StreamService.ToGuid(StreamService.CatchupPrefix, channel.CategoryId, channel.StreamId, day).ToString(),
|
|
||||||
ImageUrl = channel.StreamIcon,
|
|
||||||
Name = channelDay.ToLocalTime().ToString("ddd dd'-'MM'-'yyyy", CultureInfo.InvariantCulture),
|
|
||||||
Tags = new List<string>(parsedName.Tags),
|
|
||||||
Type = ChannelItemType.Folder,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
ChannelItemResult result = new ChannelItemResult()
|
|
||||||
{
|
|
||||||
Items = items,
|
|
||||||
TotalRecordCount = items.Count
|
|
||||||
};
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ChannelItemResult result = new()
|
||||||
|
{
|
||||||
|
Items = items,
|
||||||
|
TotalRecordCount = items.Count
|
||||||
|
};
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<ChannelItemResult> GetStreams(int categoryId, int channelId, int day, CancellationToken cancellationToken)
|
private async Task<ChannelItemResult> GetStreams(int categoryId, int channelId, int day, CancellationToken cancellationToken)
|
||||||
@@ -199,78 +180,70 @@ public class CatchupChannel : IChannel
|
|||||||
DateTime start = DateTime.UnixEpoch.AddDays(day);
|
DateTime start = DateTime.UnixEpoch.AddDays(day);
|
||||||
DateTime end = start.AddDays(1);
|
DateTime end = start.AddDays(1);
|
||||||
Plugin plugin = Plugin.Instance;
|
Plugin plugin = Plugin.Instance;
|
||||||
using (XtreamClient client = new XtreamClient())
|
using XtreamClient client = new XtreamClient();
|
||||||
|
|
||||||
|
List<StreamInfo> streams = await client.GetLiveStreamsByCategoryAsync(plugin.Creds, categoryId, cancellationToken).ConfigureAwait(false);
|
||||||
|
StreamInfo channel = streams.FirstOrDefault(s => s.StreamId == channelId)
|
||||||
|
?? throw new ArgumentException($"Channel with id {channelId} not found in category {categoryId}");
|
||||||
|
EpgListings epgs = await client.GetEpgInfoAsync(plugin.Creds, channelId, cancellationToken).ConfigureAwait(false);
|
||||||
|
List<ChannelItemInfo> items = [];
|
||||||
|
|
||||||
|
// Create fallback single-stream catch-up if no EPG is available.
|
||||||
|
if (epgs.Listings.Count == 0)
|
||||||
{
|
{
|
||||||
StreamInfo? channel = (
|
int duration = 24 * 60;
|
||||||
await client.GetLiveStreamsByCategoryAsync(plugin.Creds, categoryId, cancellationToken).ConfigureAwait(false)
|
return new()
|
||||||
).FirstOrDefault(s => s.StreamId == channelId);
|
|
||||||
if (channel == null)
|
|
||||||
{
|
{
|
||||||
throw new ArgumentException($"Channel with id {channelId} not found in category {categoryId}");
|
Items = new List<ChannelItemInfo>()
|
||||||
}
|
|
||||||
|
|
||||||
EpgListings epgs = await client.GetEpgInfoAsync(plugin.Creds, channelId, cancellationToken).ConfigureAwait(false);
|
|
||||||
List<ChannelItemInfo> items = new List<ChannelItemInfo>();
|
|
||||||
|
|
||||||
// Create fallback single-stream catch-up if no EPG is available.
|
|
||||||
if (epgs.Listings.Count == 0)
|
|
||||||
{
|
|
||||||
int duration = 24 * 60;
|
|
||||||
return new ChannelItemResult()
|
|
||||||
{
|
|
||||||
Items = new List<ChannelItemInfo>()
|
|
||||||
{
|
{
|
||||||
new ChannelItemInfo()
|
new()
|
||||||
{
|
{
|
||||||
ContentType = ChannelMediaContentType.TvExtra,
|
ContentType = ChannelMediaContentType.TvExtra,
|
||||||
Id = StreamService.ToGuid(StreamService.CatchupStreamPrefix, channelId, 0, day).ToString(),
|
Id = StreamService.ToGuid(StreamService.CatchupStreamPrefix, channelId, 0, day).ToString(),
|
||||||
IsLiveStream = false,
|
IsLiveStream = false,
|
||||||
MediaSources = new List<MediaSourceInfo>()
|
MediaSources = [
|
||||||
{
|
|
||||||
plugin.StreamService.GetMediaSourceInfo(StreamType.CatchUp, channelId, start: start, durationMinutes: duration)
|
plugin.StreamService.GetMediaSourceInfo(StreamType.CatchUp, channelId, start: start, durationMinutes: duration)
|
||||||
},
|
],
|
||||||
MediaType = ChannelMediaType.Video,
|
MediaType = ChannelMediaType.Video,
|
||||||
Name = $"No EPG available",
|
Name = $"No EPG available",
|
||||||
Type = ChannelItemType.Media,
|
Type = ChannelItemType.Media,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
TotalRecordCount = 1
|
TotalRecordCount = 1
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (EpgInfo epg in epgs.Listings.Where(epg => epg.Start <= end && epg.End >= start))
|
|
||||||
{
|
|
||||||
ParsedName parsedName = StreamService.ParseName(epg.Title);
|
|
||||||
int durationMinutes = (int)Math.Ceiling((epg.End - epg.Start).TotalMinutes);
|
|
||||||
string dateTitle = epg.Start.ToLocalTime().ToString("HH:mm", CultureInfo.InvariantCulture);
|
|
||||||
List<MediaSourceInfo> sources = new List<MediaSourceInfo>()
|
|
||||||
{
|
|
||||||
plugin.StreamService.GetMediaSourceInfo(StreamType.CatchUp, channelId, start: epg.StartLocalTime, durationMinutes: durationMinutes)
|
|
||||||
};
|
|
||||||
|
|
||||||
items.Add(new ChannelItemInfo()
|
|
||||||
{
|
|
||||||
ContentType = ChannelMediaContentType.TvExtra,
|
|
||||||
DateCreated = epg.Start,
|
|
||||||
Id = StreamService.ToGuid(StreamService.CatchupStreamPrefix, channel.StreamId, epg.Id, day).ToString(),
|
|
||||||
IsLiveStream = false,
|
|
||||||
MediaSources = sources,
|
|
||||||
MediaType = ChannelMediaType.Video,
|
|
||||||
Name = $"{dateTitle} - {parsedName.Title}",
|
|
||||||
Overview = epg.Description,
|
|
||||||
PremiereDate = epg.Start,
|
|
||||||
Tags = new List<string>(parsedName.Tags),
|
|
||||||
Type = ChannelItemType.Media,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
ChannelItemResult result = new ChannelItemResult()
|
|
||||||
{
|
|
||||||
Items = items,
|
|
||||||
TotalRecordCount = items.Count
|
|
||||||
};
|
};
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
foreach (EpgInfo epg in epgs.Listings.Where(epg => epg.Start <= end && epg.End >= start))
|
||||||
|
{
|
||||||
|
ParsedName parsedName = StreamService.ParseName(epg.Title);
|
||||||
|
int durationMinutes = (int)Math.Ceiling((epg.End - epg.Start).TotalMinutes);
|
||||||
|
string dateTitle = epg.Start.ToLocalTime().ToString("HH:mm", CultureInfo.InvariantCulture);
|
||||||
|
List<MediaSourceInfo> sources = [
|
||||||
|
plugin.StreamService.GetMediaSourceInfo(StreamType.CatchUp, channelId, start: epg.StartLocalTime, durationMinutes: durationMinutes)
|
||||||
|
];
|
||||||
|
|
||||||
|
items.Add(new()
|
||||||
|
{
|
||||||
|
ContentType = ChannelMediaContentType.TvExtra,
|
||||||
|
DateCreated = epg.Start,
|
||||||
|
Id = StreamService.ToGuid(StreamService.CatchupStreamPrefix, channel.StreamId, epg.Id, day).ToString(),
|
||||||
|
IsLiveStream = false,
|
||||||
|
MediaSources = sources,
|
||||||
|
MediaType = ChannelMediaType.Video,
|
||||||
|
Name = $"{dateTitle} - {parsedName.Title}",
|
||||||
|
Overview = epg.Description,
|
||||||
|
PremiereDate = epg.Start,
|
||||||
|
Tags = new List<string>(parsedName.Tags),
|
||||||
|
Type = ChannelItemType.Media,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ChannelItemResult result = new()
|
||||||
|
{
|
||||||
|
Items = items,
|
||||||
|
TotalRecordCount = items.Count
|
||||||
|
};
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
@@ -18,35 +18,25 @@ namespace Jellyfin.Xtream.Client;
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// A wrapper class for Xtream API client connection information.
|
/// A wrapper class for Xtream API client connection information.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class ConnectionInfo
|
/// <param name="baseUrl">The base url including protocol and port number, without trailing slash.</param>
|
||||||
|
/// <param name="username">The username for authentication.</param>
|
||||||
|
/// <param name="password">The password for authentication.</param>
|
||||||
|
public class ConnectionInfo(string baseUrl, string username, string password)
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the <see cref="ConnectionInfo"/> class.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="baseUrl">The base url including protocol and port number, without trailing slash.</param>
|
|
||||||
/// <param name="username">The username for authentication.</param>
|
|
||||||
/// <param name="password">The password for authentication.</param>
|
|
||||||
public ConnectionInfo(string baseUrl, string username, string password)
|
|
||||||
{
|
|
||||||
BaseUrl = baseUrl;
|
|
||||||
UserName = username;
|
|
||||||
Password = password;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the base url including protocol and port number, without trailing slash.
|
/// Gets or sets the base url including protocol and port number, without trailing slash.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string BaseUrl { get; set; }
|
public string BaseUrl { get; set; } = baseUrl;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the username for authentication.
|
/// Gets or sets the username for authentication.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string UserName { get; set; }
|
public string UserName { get; set; } = username;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the password for authentication.
|
/// Gets or sets the password for authentication.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Password { get; set; }
|
public string Password { get; set; } = password;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override string ToString() => $"{BaseUrl} {UserName}:{Password}";
|
public override string ToString() => $"{BaseUrl} {UserName}:{Password}";
|
||||||
|
@@ -27,10 +27,12 @@ namespace Jellyfin.Xtream.Client;
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// The Xtream API client implementation.
|
/// The Xtream API client implementation.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class XtreamClient : IDisposable
|
/// <remarks>
|
||||||
|
/// Initializes a new instance of the <see cref="XtreamClient"/> class.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="client">The HTTP client used.</param>
|
||||||
|
public class XtreamClient(HttpClient client) : IDisposable
|
||||||
{
|
{
|
||||||
private readonly HttpClient _client;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="XtreamClient"/> class.
|
/// Initializes a new instance of the <see cref="XtreamClient"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -38,19 +40,10 @@ public class XtreamClient : IDisposable
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the <see cref="XtreamClient"/> class.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="client">The HTTP client used.</param>
|
|
||||||
public XtreamClient(HttpClient client)
|
|
||||||
{
|
|
||||||
_client = client;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<T> QueryApi<T>(ConnectionInfo connectionInfo, string urlPath, CancellationToken cancellationToken)
|
private async Task<T> QueryApi<T>(ConnectionInfo connectionInfo, string urlPath, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
Uri uri = new Uri(connectionInfo.BaseUrl + urlPath);
|
Uri uri = new Uri(connectionInfo.BaseUrl + urlPath);
|
||||||
string jsonContent = await _client.GetStringAsync(uri, cancellationToken).ConfigureAwait(false);
|
string jsonContent = await client.GetStringAsync(uri, cancellationToken).ConfigureAwait(false);
|
||||||
return JsonConvert.DeserializeObject<T>(jsonContent)!;
|
return JsonConvert.DeserializeObject<T>(jsonContent)!;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,7 +107,7 @@ public class XtreamClient : IDisposable
|
|||||||
/// <param name="b">Unused.</param>
|
/// <param name="b">Unused.</param>
|
||||||
protected virtual void Dispose(bool b)
|
protected virtual void Dispose(bool b)
|
||||||
{
|
{
|
||||||
_client?.Dispose();
|
client?.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
@@ -20,13 +20,6 @@ namespace Jellyfin.Xtream.Configuration;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class ChannelOverrides
|
public class ChannelOverrides
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the <see cref="ChannelOverrides"/> class.
|
|
||||||
/// </summary>
|
|
||||||
public ChannelOverrides()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the TV channel number.
|
/// Gets or sets the TV channel number.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@@ -24,38 +24,20 @@ namespace Jellyfin.Xtream.Configuration;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class PluginConfiguration : BasePluginConfiguration
|
public class PluginConfiguration : BasePluginConfiguration
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the <see cref="PluginConfiguration"/> class.
|
|
||||||
/// </summary>
|
|
||||||
public PluginConfiguration()
|
|
||||||
{
|
|
||||||
// set default options here
|
|
||||||
BaseUrl = "https://example.com";
|
|
||||||
Username = string.Empty;
|
|
||||||
Password = string.Empty;
|
|
||||||
IsCatchupVisible = false;
|
|
||||||
IsSeriesVisible = false;
|
|
||||||
IsVodVisible = false;
|
|
||||||
LiveTv = new SerializableDictionary<int, HashSet<int>>();
|
|
||||||
Vod = new SerializableDictionary<int, HashSet<int>>();
|
|
||||||
Series = new SerializableDictionary<int, HashSet<int>>();
|
|
||||||
LiveTvOverrides = new SerializableDictionary<int, ChannelOverrides>();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the base url including protocol and trailing slash.
|
/// Gets or sets the base url including protocol and trailing slash.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string BaseUrl { get; set; }
|
public string BaseUrl { get; set; } = "https://example.com";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the username.
|
/// Gets or sets the username.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Username { get; set; }
|
public string Username { get; set; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the password.
|
/// Gets or sets the password.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Password { get; set; }
|
public string Password { get; set; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets a value indicating whether the Catch-up channel is visible.
|
/// Gets or sets a value indicating whether the Catch-up channel is visible.
|
||||||
@@ -75,21 +57,21 @@ public class PluginConfiguration : BasePluginConfiguration
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the channels displayed in Live TV.
|
/// Gets or sets the channels displayed in Live TV.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public SerializableDictionary<int, HashSet<int>> LiveTv { get; set; }
|
public SerializableDictionary<int, HashSet<int>> LiveTv { get; set; } = [];
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the streams displayed in VOD.
|
/// Gets or sets the streams displayed in VOD.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public SerializableDictionary<int, HashSet<int>> Vod { get; set; }
|
public SerializableDictionary<int, HashSet<int>> Vod { get; set; } = [];
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the streams displayed in Series.
|
/// Gets or sets the streams displayed in Series.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public SerializableDictionary<int, HashSet<int>> Series { get; set; }
|
public SerializableDictionary<int, HashSet<int>> Series { get; set; } = [];
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the channel override configuration for Live TV.
|
/// Gets or sets the channel override configuration for Live TV.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public SerializableDictionary<int, ChannelOverrides> LiveTvOverrides { get; set; }
|
public SerializableDictionary<int, ChannelOverrides> LiveTvOverrides { get; set; } = [];
|
||||||
}
|
}
|
||||||
#pragma warning restore CA2227
|
#pragma warning restore CA2227
|
||||||
|
@@ -32,15 +32,15 @@ namespace Jellyfin.Xtream.Configuration;
|
|||||||
public sealed class SerializableDictionary<TKey, TValue> : Dictionary<TKey, TValue>, IXmlSerializable
|
public sealed class SerializableDictionary<TKey, TValue> : Dictionary<TKey, TValue>, IXmlSerializable
|
||||||
where TKey : notnull
|
where TKey : notnull
|
||||||
{
|
{
|
||||||
private const string DefaultItemTag = "Item";
|
private const string ItemTag = "Item";
|
||||||
|
|
||||||
private const string DefaultKeyTag = "Key";
|
private const string KeyTag = "Key";
|
||||||
|
|
||||||
private const string DefaultValueTag = "Value";
|
private const string ValueTag = "Value";
|
||||||
|
|
||||||
private static readonly XmlSerializer KeySerializer = new XmlSerializer(typeof(TKey));
|
private static readonly XmlSerializer _keySerializer = new(typeof(TKey));
|
||||||
|
|
||||||
private static readonly XmlSerializer ValueSerializer = new XmlSerializer(typeof(TValue));
|
private static readonly XmlSerializer _valueSerializer = new(typeof(TValue));
|
||||||
|
|
||||||
/// <summary>Initializes a new instance of the
|
/// <summary>Initializes a new instance of the
|
||||||
/// <see cref="SerializableDictionary<TKey, TValue>"/> class.
|
/// <see cref="SerializableDictionary<TKey, TValue>"/> class.
|
||||||
@@ -49,12 +49,6 @@ where TKey : notnull
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
private string ItemTagName => DefaultItemTag;
|
|
||||||
|
|
||||||
private string KeyTagName => DefaultKeyTag;
|
|
||||||
|
|
||||||
private string ValueTagName => DefaultValueTag;
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public XmlSchema? GetSchema()
|
public XmlSchema? GetSchema()
|
||||||
{
|
{
|
||||||
@@ -91,7 +85,7 @@ where TKey : notnull
|
|||||||
{
|
{
|
||||||
foreach (var keyValuePair in this)
|
foreach (var keyValuePair in this)
|
||||||
{
|
{
|
||||||
WriteItem(writer, keyValuePair);
|
SerializableDictionary<TKey, TValue>.WriteItem(writer, keyValuePair);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,10 +95,10 @@ where TKey : notnull
|
|||||||
/// <param name="reader">The XML representation of the object.</param>
|
/// <param name="reader">The XML representation of the object.</param>
|
||||||
private void ReadItem(XmlReader reader)
|
private void ReadItem(XmlReader reader)
|
||||||
{
|
{
|
||||||
reader.ReadStartElement(ItemTagName);
|
reader.ReadStartElement(ItemTag);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
Add(ReadKey(reader), ReadValue(reader));
|
Add(SerializableDictionary<TKey, TValue>.ReadKey(reader), SerializableDictionary<TKey, TValue>.ReadValue(reader));
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@@ -117,17 +111,12 @@ where TKey : notnull
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="reader">The XML representation of the object.</param>
|
/// <param name="reader">The XML representation of the object.</param>
|
||||||
/// <returns>The dictionary item's key.</returns>
|
/// <returns>The dictionary item's key.</returns>
|
||||||
private TKey ReadKey(XmlReader reader)
|
private static TKey ReadKey(XmlReader reader)
|
||||||
{
|
{
|
||||||
reader.ReadStartElement(KeyTagName);
|
reader.ReadStartElement(KeyTag);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
TKey? deserialized = (TKey?)KeySerializer.Deserialize(reader);
|
TKey deserialized = (TKey?)_keySerializer.Deserialize(reader) ?? throw new SerializationException("Key cannot be null");
|
||||||
if (deserialized == null)
|
|
||||||
{
|
|
||||||
throw new SerializationException("Key cannot be null");
|
|
||||||
}
|
|
||||||
|
|
||||||
return deserialized;
|
return deserialized;
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
@@ -141,17 +130,12 @@ where TKey : notnull
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="reader">The XML representation of the object.</param>
|
/// <param name="reader">The XML representation of the object.</param>
|
||||||
/// <returns>The dictionary item's value.</returns>
|
/// <returns>The dictionary item's value.</returns>
|
||||||
private TValue ReadValue(XmlReader reader)
|
private static TValue ReadValue(XmlReader reader)
|
||||||
{
|
{
|
||||||
reader.ReadStartElement(ValueTagName);
|
reader.ReadStartElement(ValueTag);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
TValue? deserialized = (TValue?)ValueSerializer.Deserialize(reader);
|
TValue deserialized = (TValue?)_valueSerializer.Deserialize(reader) ?? throw new SerializationException("Value cannot be null");
|
||||||
if (deserialized == null)
|
|
||||||
{
|
|
||||||
throw new SerializationException("Value cannot be null");
|
|
||||||
}
|
|
||||||
|
|
||||||
return deserialized;
|
return deserialized;
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
@@ -165,13 +149,13 @@ where TKey : notnull
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="writer">The XML writer to serialize to.</param>
|
/// <param name="writer">The XML writer to serialize to.</param>
|
||||||
/// <param name="keyValuePair">The key/value pair.</param>
|
/// <param name="keyValuePair">The key/value pair.</param>
|
||||||
private void WriteItem(XmlWriter writer, KeyValuePair<TKey, TValue> keyValuePair)
|
private static void WriteItem(XmlWriter writer, KeyValuePair<TKey, TValue> keyValuePair)
|
||||||
{
|
{
|
||||||
writer.WriteStartElement(ItemTagName);
|
writer.WriteStartElement(ItemTag);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
WriteKey(writer, keyValuePair.Key);
|
SerializableDictionary<TKey, TValue>.WriteKey(writer, keyValuePair.Key);
|
||||||
WriteValue(writer, keyValuePair.Value);
|
SerializableDictionary<TKey, TValue>.WriteValue(writer, keyValuePair.Value);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@@ -184,12 +168,12 @@ where TKey : notnull
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="writer">The XML writer to serialize to.</param>
|
/// <param name="writer">The XML writer to serialize to.</param>
|
||||||
/// <param name="key">The dictionary item's key.</param>
|
/// <param name="key">The dictionary item's key.</param>
|
||||||
private void WriteKey(XmlWriter writer, TKey key)
|
private static void WriteKey(XmlWriter writer, TKey key)
|
||||||
{
|
{
|
||||||
writer.WriteStartElement(KeyTagName);
|
writer.WriteStartElement(KeyTag);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
KeySerializer.Serialize(writer, key);
|
_keySerializer.Serialize(writer, key);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@@ -202,12 +186,12 @@ where TKey : notnull
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="writer">The XML writer to serialize to.</param>
|
/// <param name="writer">The XML writer to serialize to.</param>
|
||||||
/// <param name="value">The dictionary item's value.</param>
|
/// <param name="value">The dictionary item's value.</param>
|
||||||
private void WriteValue(XmlWriter writer, TValue value)
|
private static void WriteValue(XmlWriter writer, TValue value)
|
||||||
{
|
{
|
||||||
writer.WriteStartElement(ValueTagName);
|
writer.WriteStartElement(ValueTag);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
ValueSerializer.Serialize(writer, value);
|
_valueSerializer.Serialize(writer, value);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
@@ -35,28 +35,15 @@ namespace Jellyfin.Xtream;
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Class LiveTvService.
|
/// Class LiveTvService.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class LiveTvService : ILiveTvService, ISupportsDirectStreamProvider
|
/// <remarks>
|
||||||
|
/// Initializes a new instance of the <see cref="LiveTvService"/> class.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="appHost">Instance of the <see cref="IServerApplicationHost"/> interface.</param>
|
||||||
|
/// <param name="httpClientFactory">Instance of the <see cref="IHttpClientFactory"/> interface.</param>
|
||||||
|
/// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param>
|
||||||
|
/// <param name="memoryCache">Instance of the <see cref="IMemoryCache"/> interface.</param>
|
||||||
|
public class LiveTvService(IServerApplicationHost appHost, IHttpClientFactory httpClientFactory, ILogger<LiveTvService> logger, IMemoryCache memoryCache) : ILiveTvService, ISupportsDirectStreamProvider
|
||||||
{
|
{
|
||||||
private readonly IServerApplicationHost appHost;
|
|
||||||
private readonly IHttpClientFactory httpClientFactory;
|
|
||||||
private readonly ILogger<LiveTvService> logger;
|
|
||||||
private readonly IMemoryCache memoryCache;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the <see cref="LiveTvService"/> class.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="appHost">Instance of the <see cref="IServerApplicationHost"/> interface.</param>
|
|
||||||
/// <param name="httpClientFactory">Instance of the <see cref="IHttpClientFactory"/> interface.</param>
|
|
||||||
/// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param>
|
|
||||||
/// <param name="memoryCache">Instance of the <see cref="IMemoryCache"/> interface.</param>
|
|
||||||
public LiveTvService(IServerApplicationHost appHost, IHttpClientFactory httpClientFactory, ILogger<LiveTvService> logger, IMemoryCache memoryCache)
|
|
||||||
{
|
|
||||||
this.appHost = appHost;
|
|
||||||
this.httpClientFactory = httpClientFactory;
|
|
||||||
this.logger = logger;
|
|
||||||
this.memoryCache = memoryCache;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public string Name => "Xtream Live";
|
public string Name => "Xtream Live";
|
||||||
|
|
||||||
@@ -67,7 +54,7 @@ public class LiveTvService : ILiveTvService, ISupportsDirectStreamProvider
|
|||||||
public async Task<IEnumerable<ChannelInfo>> GetChannelsAsync(CancellationToken cancellationToken)
|
public async Task<IEnumerable<ChannelInfo>> GetChannelsAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
Plugin plugin = Plugin.Instance;
|
Plugin plugin = Plugin.Instance;
|
||||||
List<ChannelInfo> items = new List<ChannelInfo>();
|
List<ChannelInfo> items = [];
|
||||||
foreach (StreamInfo channel in await plugin.StreamService.GetLiveStreamsWithOverrides(cancellationToken).ConfigureAwait(false))
|
foreach (StreamInfo channel in await plugin.StreamService.GetLiveStreamsWithOverrides(cancellationToken).ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
ParsedName parsed = StreamService.ParseName(channel.Name);
|
ParsedName parsed = StreamService.ParseName(channel.Name);
|
||||||
@@ -136,7 +123,7 @@ public class LiveTvService : ILiveTvService, ISupportsDirectStreamProvider
|
|||||||
public async Task<List<MediaSourceInfo>> GetChannelStreamMediaSources(string channelId, CancellationToken cancellationToken)
|
public async Task<List<MediaSourceInfo>> GetChannelStreamMediaSources(string channelId, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
MediaSourceInfo source = await GetChannelStream(channelId, string.Empty, cancellationToken).ConfigureAwait(false);
|
MediaSourceInfo source = await GetChannelStream(channelId, string.Empty, cancellationToken).ConfigureAwait(false);
|
||||||
return new List<MediaSourceInfo>() { source };
|
return [source];
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@@ -190,7 +177,7 @@ public class LiveTvService : ILiveTvService, ISupportsDirectStreamProvider
|
|||||||
EpgListings epgs = await client.GetEpgInfoAsync(plugin.Creds, streamId, cancellationToken).ConfigureAwait(false);
|
EpgListings epgs = await client.GetEpgInfoAsync(plugin.Creds, streamId, cancellationToken).ConfigureAwait(false);
|
||||||
foreach (EpgInfo epg in epgs.Listings)
|
foreach (EpgInfo epg in epgs.Listings)
|
||||||
{
|
{
|
||||||
items.Add(new ProgramInfo()
|
items.Add(new()
|
||||||
{
|
{
|
||||||
Id = StreamService.ToGuid(StreamService.EpgPrefix, streamId, epg.Id, 0).ToString(),
|
Id = StreamService.ToGuid(StreamService.EpgPrefix, streamId, epg.Id, 0).ToString(),
|
||||||
ChannelId = channelId,
|
ChannelId = channelId,
|
||||||
|
@@ -34,24 +34,20 @@ namespace Jellyfin.Xtream;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages
|
public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages
|
||||||
{
|
{
|
||||||
private static Plugin? instance;
|
private static Plugin? _instance;
|
||||||
|
|
||||||
private readonly ILogger<Plugin> _logger;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="Plugin"/> class.
|
/// Initializes a new instance of the <see cref="Plugin"/> class.
|
||||||
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="applicationPaths">Instance of the <see cref="IApplicationPaths"/> interface.</param>
|
/// <param name="applicationPaths">Instance of the <see cref="IApplicationPaths"/> interface.</param>
|
||||||
/// <param name="xmlSerializer">Instance of the <see cref="IXmlSerializer"/> interface.</param>
|
/// <param name="xmlSerializer">Instance of the <see cref="IXmlSerializer"/> interface.</param>
|
||||||
/// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param>
|
|
||||||
/// <param name="taskManager">Instance of the <see cref="ITaskManager"/> interface.</param>
|
/// <param name="taskManager">Instance of the <see cref="ITaskManager"/> interface.</param>
|
||||||
public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer, ILogger<Plugin> logger, ITaskManager taskManager)
|
public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer, ITaskManager taskManager)
|
||||||
: base(applicationPaths, xmlSerializer)
|
: base(applicationPaths, xmlSerializer)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_instance = this;
|
||||||
instance = this;
|
StreamService = new();
|
||||||
StreamService = new StreamService(logger, this);
|
TaskService = new(taskManager);
|
||||||
TaskService = new TaskService(logger, this, taskManager);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@@ -63,10 +59,7 @@ public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the Xtream connection info with credentials.
|
/// Gets the Xtream connection info with credentials.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public ConnectionInfo Creds
|
public ConnectionInfo Creds => new(Configuration.BaseUrl, Configuration.Username, Configuration.Password);
|
||||||
{
|
|
||||||
get => new ConnectionInfo(Configuration.BaseUrl, Configuration.Username, Configuration.Password);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the data version used to trigger a cache invalidation on plugin update or config change.
|
/// Gets the data version used to trigger a cache invalidation on plugin update or config change.
|
||||||
@@ -76,18 +69,7 @@ public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the current plugin instance.
|
/// Gets the current plugin instance.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static Plugin Instance
|
public static Plugin Instance => _instance ?? throw new InvalidOperationException("Plugin instance not available");
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
if (instance == null)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException("Plugin instance not available");
|
|
||||||
}
|
|
||||||
|
|
||||||
return instance;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the stream service instance.
|
/// Gets the stream service instance.
|
||||||
@@ -99,7 +81,7 @@ public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public TaskService TaskService { get; init; }
|
public TaskService TaskService { get; init; }
|
||||||
|
|
||||||
private static PluginPageInfo CreateStatic(string name) => new PluginPageInfo
|
private static PluginPageInfo CreateStatic(string name) => new()
|
||||||
{
|
{
|
||||||
Name = name,
|
Name = name,
|
||||||
EmbeddedResourcePath = string.Format(
|
EmbeddedResourcePath = string.Format(
|
||||||
|
@@ -34,19 +34,9 @@ namespace Jellyfin.Xtream;
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// The Xtream Codes API channel.
|
/// The Xtream Codes API channel.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class SeriesChannel : IChannel
|
/// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param>
|
||||||
|
public class SeriesChannel(ILogger<SeriesChannel> logger) : IChannel
|
||||||
{
|
{
|
||||||
private readonly ILogger<SeriesChannel> logger;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the <see cref="SeriesChannel"/> class.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param>
|
|
||||||
public SeriesChannel(ILogger<SeriesChannel> logger)
|
|
||||||
{
|
|
||||||
this.logger = logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public string? Name => "Xtream Series";
|
public string? Name => "Xtream Series";
|
||||||
|
|
||||||
@@ -67,15 +57,13 @@ public class SeriesChannel : IChannel
|
|||||||
{
|
{
|
||||||
return new InternalChannelFeatures
|
return new InternalChannelFeatures
|
||||||
{
|
{
|
||||||
ContentTypes = new List<ChannelMediaContentType>
|
ContentTypes = [
|
||||||
{
|
|
||||||
ChannelMediaContentType.Episode,
|
ChannelMediaContentType.Episode,
|
||||||
},
|
],
|
||||||
|
|
||||||
MediaTypes = new List<ChannelMediaType>
|
MediaTypes = [
|
||||||
{
|
|
||||||
ChannelMediaType.Video
|
ChannelMediaType.Video
|
||||||
},
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,12 +143,12 @@ public class SeriesChannel : IChannel
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<string> GetGenres(string genreString)
|
private static List<string> GetGenres(string genreString)
|
||||||
{
|
{
|
||||||
return new List<string>(genreString.Split(',').Select(genre => genre.Trim()));
|
return new(genreString.Split(',').Select(genre => genre.Trim()));
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<PersonInfo> GetPeople(string cast)
|
private static List<PersonInfo> GetPeople(string cast)
|
||||||
{
|
{
|
||||||
return cast.Split(',').Select(name => new PersonInfo()
|
return cast.Split(',').Select(name => new PersonInfo()
|
||||||
{
|
{
|
||||||
@@ -170,12 +158,12 @@ public class SeriesChannel : IChannel
|
|||||||
|
|
||||||
private ChannelItemInfo CreateChannelItemInfo(int seriesId, SeriesStreamInfo series, int seasonId)
|
private ChannelItemInfo CreateChannelItemInfo(int seriesId, SeriesStreamInfo series, int seasonId)
|
||||||
{
|
{
|
||||||
Jellyfin.Xtream.Client.Models.SeriesInfo serie = series.Info;
|
Client.Models.SeriesInfo serie = series.Info;
|
||||||
string name = $"Season {seasonId}";
|
string name = $"Season {seasonId}";
|
||||||
string cover = series.Info.Cover;
|
string cover = series.Info.Cover;
|
||||||
string? overview = null;
|
string? overview = null;
|
||||||
DateTime? created = null;
|
DateTime? created = null;
|
||||||
List<string> tags = new List<string>();
|
List<string> tags = [];
|
||||||
|
|
||||||
Season? season = series.Seasons.FirstOrDefault(s => s.SeasonId == seasonId);
|
Season? season = series.Seasons.FirstOrDefault(s => s.SeasonId == seasonId);
|
||||||
if (season != null)
|
if (season != null)
|
||||||
@@ -191,7 +179,7 @@ public class SeriesChannel : IChannel
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return new ChannelItemInfo()
|
return new()
|
||||||
{
|
{
|
||||||
DateCreated = created,
|
DateCreated = created,
|
||||||
// FolderType = ChannelFolderType.Season,
|
// FolderType = ChannelFolderType.Season,
|
||||||
@@ -208,12 +196,11 @@ public class SeriesChannel : IChannel
|
|||||||
|
|
||||||
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;
|
Client.Models.SeriesInfo serie = series.Info;
|
||||||
ParsedName parsedName = StreamService.ParseName(episode.Title);
|
ParsedName parsedName = StreamService.ParseName(episode.Title);
|
||||||
List<MediaSourceInfo> sources = new List<MediaSourceInfo>()
|
List<MediaSourceInfo> sources = [
|
||||||
{
|
|
||||||
Plugin.Instance.StreamService.GetMediaSourceInfo(StreamType.Series, episode.EpisodeId, episode.ContainerExtension)
|
Plugin.Instance.StreamService.GetMediaSourceInfo(StreamType.Series, episode.EpisodeId, episode.ContainerExtension)
|
||||||
};
|
];
|
||||||
|
|
||||||
string? cover = episode.Info?.MovieImage;
|
string? cover = episode.Info?.MovieImage;
|
||||||
if (string.IsNullOrEmpty(cover) && season != null)
|
if (string.IsNullOrEmpty(cover) && season != null)
|
||||||
@@ -226,7 +213,7 @@ public class SeriesChannel : IChannel
|
|||||||
cover = serie.Cover;
|
cover = serie.Cover;
|
||||||
}
|
}
|
||||||
|
|
||||||
return new ChannelItemInfo()
|
return new()
|
||||||
{
|
{
|
||||||
ContentType = ChannelMediaContentType.Episode,
|
ContentType = ChannelMediaContentType.Episode,
|
||||||
DateCreated = DateTimeOffset.FromUnixTimeSeconds(episode.Added).DateTime,
|
DateCreated = DateTimeOffset.FromUnixTimeSeconds(episode.Added).DateTime,
|
||||||
@@ -239,17 +226,17 @@ public class SeriesChannel : IChannel
|
|||||||
Name = parsedName.Title,
|
Name = parsedName.Title,
|
||||||
Overview = episode.Info?.Plot,
|
Overview = episode.Info?.Plot,
|
||||||
People = GetPeople(serie.Cast),
|
People = GetPeople(serie.Cast),
|
||||||
Tags = new List<string>(parsedName.Tags),
|
Tags = new(parsedName.Tags),
|
||||||
Type = ChannelItemType.Media,
|
Type = ChannelItemType.Media,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<ChannelItemResult> GetCategories(CancellationToken cancellationToken)
|
private async Task<ChannelItemResult> GetCategories(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
List<ChannelItemInfo> items = new List<ChannelItemInfo>(
|
IEnumerable<Category> categories = await Plugin.Instance.StreamService.GetSeriesCategories(cancellationToken).ConfigureAwait(false);
|
||||||
(await Plugin.Instance.StreamService.GetSeriesCategories(cancellationToken).ConfigureAwait(false))
|
List<ChannelItemInfo> items = new(
|
||||||
.Select((Category category) => StreamService.CreateChannelItemInfo(StreamService.SeriesCategoryPrefix, category)));
|
categories.Select((Category category) => StreamService.CreateChannelItemInfo(StreamService.SeriesCategoryPrefix, category)));
|
||||||
return new ChannelItemResult()
|
return new()
|
||||||
{
|
{
|
||||||
Items = items,
|
Items = items,
|
||||||
TotalRecordCount = items.Count
|
TotalRecordCount = items.Count
|
||||||
@@ -258,10 +245,9 @@ public class SeriesChannel : IChannel
|
|||||||
|
|
||||||
private async Task<ChannelItemResult> GetSeries(int categoryId, CancellationToken cancellationToken)
|
private async Task<ChannelItemResult> GetSeries(int categoryId, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
List<ChannelItemInfo> items = new List<ChannelItemInfo>(
|
IEnumerable<Series> series = await Plugin.Instance.StreamService.GetSeries(categoryId, cancellationToken).ConfigureAwait(false);
|
||||||
(await Plugin.Instance.StreamService.GetSeries(categoryId, cancellationToken).ConfigureAwait(false))
|
List<ChannelItemInfo> items = new(series.Select(CreateChannelItemInfo));
|
||||||
.Select((Series series) => CreateChannelItemInfo(series)));
|
return new()
|
||||||
return new ChannelItemResult()
|
|
||||||
{
|
{
|
||||||
Items = items,
|
Items = items,
|
||||||
TotalRecordCount = items.Count
|
TotalRecordCount = items.Count
|
||||||
@@ -270,10 +256,10 @@ public class SeriesChannel : IChannel
|
|||||||
|
|
||||||
private async Task<ChannelItemResult> GetSeasons(int seriesId, CancellationToken cancellationToken)
|
private async Task<ChannelItemResult> GetSeasons(int seriesId, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
List<ChannelItemInfo> items = new List<ChannelItemInfo>(
|
IEnumerable<Tuple<SeriesStreamInfo, int>> seasons = await Plugin.Instance.StreamService.GetSeasons(seriesId, cancellationToken).ConfigureAwait(false);
|
||||||
(await Plugin.Instance.StreamService.GetSeasons(seriesId, cancellationToken).ConfigureAwait(false))
|
List<ChannelItemInfo> items = new(
|
||||||
.Select((Tuple<SeriesStreamInfo, int> tuple) => CreateChannelItemInfo(seriesId, tuple.Item1, tuple.Item2)));
|
seasons.Select((Tuple<SeriesStreamInfo, int> tuple) => CreateChannelItemInfo(seriesId, tuple.Item1, tuple.Item2)));
|
||||||
return new ChannelItemResult()
|
return new()
|
||||||
{
|
{
|
||||||
Items = items,
|
Items = items,
|
||||||
TotalRecordCount = items.Count
|
TotalRecordCount = items.Count
|
||||||
@@ -282,10 +268,10 @@ public class SeriesChannel : IChannel
|
|||||||
|
|
||||||
private async Task<ChannelItemResult> GetEpisodes(int seriesId, int seasonId, CancellationToken cancellationToken)
|
private async Task<ChannelItemResult> GetEpisodes(int seriesId, int seasonId, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
|
IEnumerable<Tuple<SeriesStreamInfo, Season?, Episode>> episodes = await Plugin.Instance.StreamService.GetEpisodes(seriesId, seasonId, cancellationToken).ConfigureAwait(false);
|
||||||
List<ChannelItemInfo> items = new List<ChannelItemInfo>(
|
List<ChannelItemInfo> items = new List<ChannelItemInfo>(
|
||||||
(await Plugin.Instance.StreamService.GetEpisodes(seriesId, seasonId, cancellationToken).ConfigureAwait(false))
|
episodes.Select((Tuple<SeriesStreamInfo, Season?, Episode> tuple) => CreateChannelItemInfo(tuple.Item1, tuple.Item2, tuple.Item3)));
|
||||||
.Select((Tuple<SeriesStreamInfo, Season?, Episode> tuple) => CreateChannelItemInfo(tuple.Item1, tuple.Item2, tuple.Item3)));
|
return new()
|
||||||
return new ChannelItemResult()
|
|
||||||
{
|
{
|
||||||
Items = items,
|
Items = items,
|
||||||
TotalRecordCount = items.Count
|
TotalRecordCount = items.Count
|
||||||
|
@@ -20,26 +20,17 @@ namespace Jellyfin.Xtream.Service;
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// A struct which holds information of parsed stream names.
|
/// A struct which holds information of parsed stream names.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public struct ParsedName
|
/// <param name="title">The parsed title.</param>
|
||||||
|
/// <param name="tags">The parsed tags.</param>
|
||||||
|
public readonly struct ParsedName(string title, string[] tags)
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the <see cref="ParsedName"/> struct.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="title">The parsed title.</param>
|
|
||||||
/// <param name="tags">The parsed tags.</param>
|
|
||||||
public ParsedName(string title, string[] tags)
|
|
||||||
{
|
|
||||||
Title = title;
|
|
||||||
Tags = tags;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the parsed title.
|
/// Gets the parsed title.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Title { get; init; }
|
public string Title { get; init; } = title;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the parsed tags.
|
/// Gets the parsed tags.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string[] Tags { get; init; }
|
public string[] Tags { get; init; } = tags;
|
||||||
}
|
}
|
||||||
|
@@ -39,8 +39,7 @@ public class Restream : ILiveStream, IDirectStreamProvider, IDisposable
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public const string TunerHost = "Xtream-Restream";
|
public const string TunerHost = "Xtream-Restream";
|
||||||
|
|
||||||
private static readonly HttpStatusCode[] _redirects =
|
private static readonly HttpStatusCode[] _redirects = [
|
||||||
[
|
|
||||||
HttpStatusCode.Moved,
|
HttpStatusCode.Moved,
|
||||||
HttpStatusCode.MovedPermanently,
|
HttpStatusCode.MovedPermanently,
|
||||||
HttpStatusCode.PermanentRedirect,
|
HttpStatusCode.PermanentRedirect,
|
||||||
|
@@ -17,7 +17,6 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Runtime.CompilerServices;
|
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
@@ -35,7 +34,7 @@ namespace Jellyfin.Xtream.Service;
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// A service for dealing with stream information.
|
/// A service for dealing with stream information.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class StreamService
|
public partial class StreamService
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The id prefix for VOD category channel items.
|
/// The id prefix for VOD category channel items.
|
||||||
@@ -92,21 +91,7 @@ public class StreamService
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public const int EpgPrefix = 0x5d774c3f;
|
public const int EpgPrefix = 0x5d774c3f;
|
||||||
|
|
||||||
private static readonly Regex TagRegex = new Regex(@"\[([^\]]+)\]|\|([^\|]+)\|");
|
private static readonly Regex _tagRegex = TagRegex();
|
||||||
|
|
||||||
private readonly ILogger logger;
|
|
||||||
private readonly Plugin plugin;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the <see cref="StreamService"/> class.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param>
|
|
||||||
/// <param name="plugin">Instance of the <see cref="Plugin"/> class.</param>
|
|
||||||
public StreamService(ILogger logger, Plugin plugin)
|
|
||||||
{
|
|
||||||
this.logger = logger;
|
|
||||||
this.plugin = plugin;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Parses tags in the name of a stream entry.
|
/// Parses tags in the name of a stream entry.
|
||||||
@@ -122,8 +107,8 @@ public class StreamService
|
|||||||
/// <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 static ParsedName ParseName(string name)
|
public static ParsedName ParseName(string name)
|
||||||
{
|
{
|
||||||
List<string> tags = new List<string>();
|
List<string> tags = [];
|
||||||
string title = TagRegex.Replace(
|
string title = _tagRegex.Replace(
|
||||||
name,
|
name,
|
||||||
(match) =>
|
(match) =>
|
||||||
{
|
{
|
||||||
@@ -154,14 +139,13 @@ public class StreamService
|
|||||||
return new ParsedName
|
return new ParsedName
|
||||||
{
|
{
|
||||||
Title = title[stripLength..].Trim(),
|
Title = title[stripLength..].Trim(),
|
||||||
Tags = tags.ToArray(),
|
Tags = [.. tags],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
return config.TryGetValue(category, out var values) && (values.Count == 0 || values.Contains(id));
|
||||||
return config.TryGetValue(category, out values) && (values.Count == 0 || values.Contains(id));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -171,13 +155,13 @@ public class StreamService
|
|||||||
/// <returns>IAsyncEnumerable{StreamInfo}.</returns>
|
/// <returns>IAsyncEnumerable{StreamInfo}.</returns>
|
||||||
public async Task<IEnumerable<StreamInfo>> GetLiveStreams(CancellationToken cancellationToken)
|
public async Task<IEnumerable<StreamInfo>> GetLiveStreams(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
PluginConfiguration config = plugin.Configuration;
|
PluginConfiguration config = Plugin.Instance.Configuration;
|
||||||
using XtreamClient client = new XtreamClient();
|
using XtreamClient client = new XtreamClient();
|
||||||
|
|
||||||
IEnumerable<Task<IEnumerable<StreamInfo>>> tasks = config.LiveTv.Select(async (entry) =>
|
IEnumerable<Task<IEnumerable<StreamInfo>>> tasks = config.LiveTv.Select(async (entry) =>
|
||||||
{
|
{
|
||||||
int categoryId = entry.Key;
|
int categoryId = entry.Key;
|
||||||
var streams = await client.GetLiveStreamsByCategoryAsync(plugin.Creds, categoryId, cancellationToken).ConfigureAwait(false);
|
var streams = await client.GetLiveStreamsByCategoryAsync(Plugin.Instance.Creds, categoryId, cancellationToken).ConfigureAwait(false);
|
||||||
return streams.Where((StreamInfo channel) => IsConfigured(config.LiveTv, categoryId, channel.StreamId));
|
return streams.Where((StreamInfo channel) => IsConfigured(config.LiveTv, categoryId, channel.StreamId));
|
||||||
});
|
});
|
||||||
return (await Task.WhenAll(tasks).ConfigureAwait(false)).SelectMany(i => i);
|
return (await Task.WhenAll(tasks).ConfigureAwait(false)).SelectMany(i => i);
|
||||||
@@ -230,11 +214,9 @@ public class StreamService
|
|||||||
/// <returns>IAsyncEnumerable{StreamInfo}.</returns>
|
/// <returns>IAsyncEnumerable{StreamInfo}.</returns>
|
||||||
public async Task<IEnumerable<Category>> GetVodCategories(CancellationToken cancellationToken)
|
public async Task<IEnumerable<Category>> GetVodCategories(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
using (XtreamClient client = new XtreamClient())
|
using XtreamClient client = new XtreamClient();
|
||||||
{
|
List<Category> categories = await client.GetVodCategoryAsync(Plugin.Instance.Creds, cancellationToken).ConfigureAwait(false);
|
||||||
List<Category> categories = await client.GetVodCategoryAsync(plugin.Creds, cancellationToken).ConfigureAwait(false);
|
return categories.Where((Category category) => Plugin.Instance.Configuration.Vod.ContainsKey(category.CategoryId));
|
||||||
return categories.Where((Category category) => plugin.Configuration.Vod.ContainsKey(category.CategoryId));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -245,16 +227,14 @@ public class StreamService
|
|||||||
/// <returns>IAsyncEnumerable{StreamInfo}.</returns>
|
/// <returns>IAsyncEnumerable{StreamInfo}.</returns>
|
||||||
public async Task<IEnumerable<StreamInfo>> GetVodStreams(int categoryId, CancellationToken cancellationToken)
|
public async Task<IEnumerable<StreamInfo>> GetVodStreams(int categoryId, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
if (!plugin.Configuration.Vod.ContainsKey(categoryId))
|
if (!Plugin.Instance.Configuration.Vod.ContainsKey(categoryId))
|
||||||
{
|
{
|
||||||
return new List<StreamInfo>();
|
return new List<StreamInfo>();
|
||||||
}
|
}
|
||||||
|
|
||||||
using (XtreamClient client = new XtreamClient())
|
using XtreamClient client = new XtreamClient();
|
||||||
{
|
List<StreamInfo> streams = await client.GetVodStreamsByCategoryAsync(Plugin.Instance.Creds, categoryId, cancellationToken).ConfigureAwait(false);
|
||||||
List<StreamInfo> streams = await client.GetVodStreamsByCategoryAsync(plugin.Creds, categoryId, cancellationToken).ConfigureAwait(false);
|
return streams.Where((StreamInfo stream) => IsConfigured(Plugin.Instance.Configuration.Vod, categoryId, stream.StreamId));
|
||||||
return streams.Where((StreamInfo stream) => IsConfigured(plugin.Configuration.Vod, categoryId, stream.StreamId));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -264,12 +244,9 @@ public class StreamService
|
|||||||
/// <returns>IAsyncEnumerable{StreamInfo}.</returns>
|
/// <returns>IAsyncEnumerable{StreamInfo}.</returns>
|
||||||
public async Task<IEnumerable<Category>> GetSeriesCategories(CancellationToken cancellationToken)
|
public async Task<IEnumerable<Category>> GetSeriesCategories(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
using (XtreamClient client = new XtreamClient())
|
using XtreamClient client = new XtreamClient();
|
||||||
{
|
List<Category> categories = await client.GetSeriesCategoryAsync(Plugin.Instance.Creds, cancellationToken).ConfigureAwait(false);
|
||||||
List<Category> categories = await client.GetSeriesCategoryAsync(plugin.Creds, cancellationToken).ConfigureAwait(false);
|
return categories.Where((Category category) => Plugin.Instance.Configuration.Series.ContainsKey(category.CategoryId));
|
||||||
return categories
|
|
||||||
.Where((Category category) => plugin.Configuration.Series.ContainsKey(category.CategoryId));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -280,16 +257,14 @@ public class StreamService
|
|||||||
/// <returns>IAsyncEnumerable{StreamInfo}.</returns>
|
/// <returns>IAsyncEnumerable{StreamInfo}.</returns>
|
||||||
public async Task<IEnumerable<Series>> GetSeries(int categoryId, CancellationToken cancellationToken)
|
public async Task<IEnumerable<Series>> GetSeries(int categoryId, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
if (!plugin.Configuration.Series.ContainsKey(categoryId))
|
if (!Plugin.Instance.Configuration.Series.ContainsKey(categoryId))
|
||||||
{
|
{
|
||||||
return new List<Series>();
|
return new List<Series>();
|
||||||
}
|
}
|
||||||
|
|
||||||
using (XtreamClient client = new XtreamClient())
|
using XtreamClient client = new XtreamClient();
|
||||||
{
|
List<Series> series = await client.GetSeriesByCategoryAsync(Plugin.Instance.Creds, categoryId, cancellationToken).ConfigureAwait(false);
|
||||||
List<Series> series = await client.GetSeriesByCategoryAsync(plugin.Creds, categoryId, cancellationToken).ConfigureAwait(false);
|
return series.Where((Series series) => IsConfigured(Plugin.Instance.Configuration.Series, series.CategoryId, series.SeriesId));
|
||||||
return series.Where((Series series) => IsConfigured(plugin.Configuration.Series, series.CategoryId, series.SeriesId));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -300,17 +275,15 @@ public class StreamService
|
|||||||
/// <returns>IAsyncEnumerable{StreamInfo}.</returns>
|
/// <returns>IAsyncEnumerable{StreamInfo}.</returns>
|
||||||
public async Task<IEnumerable<Tuple<SeriesStreamInfo, int>>> GetSeasons(int seriesId, CancellationToken cancellationToken)
|
public async Task<IEnumerable<Tuple<SeriesStreamInfo, int>>> GetSeasons(int seriesId, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
using (XtreamClient client = new XtreamClient())
|
using XtreamClient client = new XtreamClient();
|
||||||
|
SeriesStreamInfo series = await client.GetSeriesStreamsBySeriesAsync(Plugin.Instance.Creds, seriesId, cancellationToken).ConfigureAwait(false);
|
||||||
|
int categoryId = series.Info.CategoryId;
|
||||||
|
if (!IsConfigured(Plugin.Instance.Configuration.Series, categoryId, seriesId))
|
||||||
{
|
{
|
||||||
SeriesStreamInfo series = await client.GetSeriesStreamsBySeriesAsync(plugin.Creds, seriesId, cancellationToken).ConfigureAwait(false);
|
return new List<Tuple<SeriesStreamInfo, int>>();
|
||||||
int categoryId = series.Info.CategoryId;
|
|
||||||
if (!IsConfigured(plugin.Configuration.Series, categoryId, seriesId))
|
|
||||||
{
|
|
||||||
return new List<Tuple<SeriesStreamInfo, int>>();
|
|
||||||
}
|
|
||||||
|
|
||||||
return series.Episodes.Keys.Select((int seasonId) => new Tuple<SeriesStreamInfo, int>(series, seasonId));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return series.Episodes.Keys.Select((int seasonId) => new Tuple<SeriesStreamInfo, int>(series, seasonId));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -322,12 +295,10 @@ public class StreamService
|
|||||||
/// <returns>IAsyncEnumerable{StreamInfo}.</returns>
|
/// <returns>IAsyncEnumerable{StreamInfo}.</returns>
|
||||||
public async Task<IEnumerable<Tuple<SeriesStreamInfo, Season?, Episode>>> GetEpisodes(int seriesId, int seasonId, CancellationToken cancellationToken)
|
public async Task<IEnumerable<Tuple<SeriesStreamInfo, Season?, Episode>>> GetEpisodes(int seriesId, int seasonId, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
using (XtreamClient client = new XtreamClient())
|
using XtreamClient client = new XtreamClient();
|
||||||
{
|
SeriesStreamInfo series = await client.GetSeriesStreamsBySeriesAsync(Plugin.Instance.Creds, seriesId, cancellationToken).ConfigureAwait(false);
|
||||||
SeriesStreamInfo series = await client.GetSeriesStreamsBySeriesAsync(plugin.Creds, seriesId, cancellationToken).ConfigureAwait(false);
|
Season? season = series.Seasons.FirstOrDefault(s => s.SeasonId == seasonId);
|
||||||
Season? season = series.Seasons.FirstOrDefault(s => s.SeasonId == seasonId);
|
return series.Episodes[seasonId].Select((Episode episode) => new Tuple<SeriesStreamInfo, Season?, Episode>(series, season, episode));
|
||||||
return series.Episodes[seasonId].Select((Episode episode) => new Tuple<SeriesStreamInfo, Season?, Episode>(series, season, episode));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void StoreBytes(byte[] dst, int offset, int i)
|
private static void StoreBytes(byte[] dst, int offset, int i)
|
||||||
@@ -362,14 +333,14 @@ public class StreamService
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the four 32-bit integers represented in the GUID.
|
/// Gets the four 32-bit integers represented in the GUID.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="guid">The input GUID.</param>
|
/// <param name="id">The input GUID.</param>
|
||||||
/// <param name="i0">Bytes 0-3.</param>
|
/// <param name="i0">Bytes 0-3.</param>
|
||||||
/// <param name="i1">Bytes 4-7.</param>
|
/// <param name="i1">Bytes 4-7.</param>
|
||||||
/// <param name="i2">Bytes 8-11.</param>
|
/// <param name="i2">Bytes 8-11.</param>
|
||||||
/// <param name="i3">Bytes 12-15.</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)
|
public static void FromGuid(Guid id, out int i0, out int i1, out int i2, out int i3)
|
||||||
{
|
{
|
||||||
byte[] tmp = guid.ToByteArray();
|
byte[] tmp = id.ToByteArray();
|
||||||
if (BitConverter.IsLittleEndian)
|
if (BitConverter.IsLittleEndian)
|
||||||
{
|
{
|
||||||
Array.Reverse(tmp);
|
Array.Reverse(tmp);
|
||||||
@@ -416,7 +387,7 @@ public class StreamService
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
PluginConfiguration config = plugin.Configuration;
|
PluginConfiguration config = Plugin.Instance.Configuration;
|
||||||
string uri = $"{config.BaseUrl}{prefix}/{config.Username}/{config.Password}/{id}";
|
string uri = $"{config.BaseUrl}{prefix}/{config.Username}/{config.Password}/{id}";
|
||||||
if (!string.IsNullOrEmpty(extension))
|
if (!string.IsNullOrEmpty(extension))
|
||||||
{
|
{
|
||||||
@@ -437,20 +408,19 @@ public class StreamService
|
|||||||
IsInfiniteStream = isLive,
|
IsInfiniteStream = isLive,
|
||||||
IsRemote = true,
|
IsRemote = true,
|
||||||
// Define media sources with unknown index and interlaced to improve compatibility.
|
// Define media sources with unknown index and interlaced to improve compatibility.
|
||||||
MediaStreams = new MediaStream[]
|
MediaStreams = [
|
||||||
{
|
new()
|
||||||
new MediaStream
|
|
||||||
{
|
{
|
||||||
Type = MediaStreamType.Video,
|
Type = MediaStreamType.Video,
|
||||||
Index = -1,
|
Index = -1,
|
||||||
IsInterlaced = true
|
IsInterlaced = true
|
||||||
},
|
},
|
||||||
new MediaStream
|
new()
|
||||||
{
|
{
|
||||||
Type = MediaStreamType.Audio,
|
Type = MediaStreamType.Audio,
|
||||||
Index = -1
|
Index = -1
|
||||||
}
|
}
|
||||||
},
|
],
|
||||||
Name = "default",
|
Name = "default",
|
||||||
Path = uri,
|
Path = uri,
|
||||||
Protocol = MediaProtocol.Http,
|
Protocol = MediaProtocol.Http,
|
||||||
@@ -461,4 +431,7 @@ public class StreamService
|
|||||||
SupportsProbing = true,
|
SupportsProbing = true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[GeneratedRegex(@"\[([^\]]+)\]|\|([^\|]+)\|")]
|
||||||
|
private static partial Regex TagRegex();
|
||||||
}
|
}
|
||||||
|
@@ -16,32 +16,15 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using MediaBrowser.Model.Tasks;
|
using MediaBrowser.Model.Tasks;
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
namespace Jellyfin.Xtream.Service;
|
namespace Jellyfin.Xtream.Service;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A service for dealing with stream information.
|
/// A service for dealing with stream information.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class TaskService
|
/// <param name="taskManager">Instance of the <see cref="ITaskManager"/> interface.</param>
|
||||||
|
public class TaskService(ITaskManager taskManager)
|
||||||
{
|
{
|
||||||
private readonly ILogger logger;
|
|
||||||
private readonly Plugin plugin;
|
|
||||||
private readonly ITaskManager taskManager;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the <see cref="TaskService"/> class.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param>
|
|
||||||
/// <param name="plugin">Instance of the <see cref="Plugin"/> class.</param>
|
|
||||||
/// <param name="taskManager">Instance of the <see cref="ITaskManager"/> interface.</param>
|
|
||||||
public TaskService(ILogger logger, Plugin plugin, ITaskManager taskManager)
|
|
||||||
{
|
|
||||||
this.logger = logger;
|
|
||||||
this.plugin = plugin;
|
|
||||||
this.taskManager = taskManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Type? FindType(string assembly, string fullName)
|
private static Type? FindType(string assembly, string fullName)
|
||||||
{
|
{
|
||||||
return AppDomain.CurrentDomain.GetAssemblies()
|
return AppDomain.CurrentDomain.GetAssemblies()
|
||||||
@@ -60,16 +43,12 @@ public class TaskService
|
|||||||
/// <exception cref="ArgumentException">If the task type is not found.</exception>
|
/// <exception cref="ArgumentException">If the task type is not found.</exception>
|
||||||
public void CancelIfRunningAndQueue(string assembly, string fullName)
|
public void CancelIfRunningAndQueue(string assembly, string fullName)
|
||||||
{
|
{
|
||||||
Type? refreshType = FindType(assembly, fullName);
|
Type refreshType = FindType(assembly, fullName) ?? throw new ArgumentException("Refresh task not found");
|
||||||
if (refreshType == null)
|
|
||||||
{
|
|
||||||
throw new ArgumentException("Refresh task not found");
|
|
||||||
}
|
|
||||||
|
|
||||||
// As the type is not publicly visible, use reflection.
|
// As the type is not publicly visible, use reflection.
|
||||||
typeof(ITaskManager)
|
typeof(ITaskManager)
|
||||||
.GetMethod(nameof(ITaskManager.CancelIfRunningAndQueue), 1, Array.Empty<Type>())?
|
.GetMethod(nameof(ITaskManager.CancelIfRunningAndQueue), 1, [])?
|
||||||
.MakeGenericMethod(refreshType)?
|
.MakeGenericMethod(refreshType)?
|
||||||
.Invoke(taskManager, Array.Empty<object>());
|
.Invoke(taskManager, []);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -24,10 +24,9 @@ namespace Jellyfin.Xtream.Service;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class WrappedBufferReadStream : Stream
|
public class WrappedBufferReadStream : Stream
|
||||||
{
|
{
|
||||||
private readonly WrappedBufferStream sourceBuffer;
|
private readonly WrappedBufferStream _sourceBuffer;
|
||||||
|
|
||||||
private readonly long initialReadHead;
|
private readonly long _initialReadHead;
|
||||||
private long readHead;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="WrappedBufferReadStream"/> class.
|
/// Initializes a new instance of the <see cref="WrappedBufferReadStream"/> class.
|
||||||
@@ -35,25 +34,25 @@ public class WrappedBufferReadStream : Stream
|
|||||||
/// <param name="sourceBuffer">The source buffer to read from.</param>
|
/// <param name="sourceBuffer">The source buffer to read from.</param>
|
||||||
public WrappedBufferReadStream(WrappedBufferStream sourceBuffer)
|
public WrappedBufferReadStream(WrappedBufferStream sourceBuffer)
|
||||||
{
|
{
|
||||||
this.sourceBuffer = sourceBuffer;
|
_sourceBuffer = sourceBuffer;
|
||||||
this.readHead = sourceBuffer.TotalBytesWritten;
|
_initialReadHead = sourceBuffer.TotalBytesWritten;
|
||||||
this.initialReadHead = readHead;
|
ReadHead = _initialReadHead;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the virtual position in the source buffer.
|
/// Gets the virtual position in the source buffer.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public long ReadHead { get => readHead; }
|
public long ReadHead { get; private set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the number of bytes that have been written to this stream.
|
/// Gets the number of bytes that have been written to this stream.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public long TotalBytesRead { get => readHead - initialReadHead; }
|
public long TotalBytesRead { get => ReadHead - _initialReadHead; }
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override long Position
|
public override long Position
|
||||||
{
|
{
|
||||||
get => readHead % sourceBuffer.BufferSize; set { }
|
get => ReadHead % _sourceBuffer.BufferSize; set { }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@@ -73,16 +72,16 @@ public class WrappedBufferReadStream : Stream
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override int Read(byte[] buffer, int offset, int count)
|
public override int Read(byte[] buffer, int offset, int count)
|
||||||
{
|
{
|
||||||
long gap = sourceBuffer.TotalBytesWritten - readHead;
|
long gap = _sourceBuffer.TotalBytesWritten - ReadHead;
|
||||||
|
|
||||||
// We cannot return with 0 bytes read, as that indicates the end of the stream has been reached
|
// We cannot return with 0 bytes read, as that indicates the end of the stream has been reached
|
||||||
while (gap == 0)
|
while (gap == 0)
|
||||||
{
|
{
|
||||||
Thread.Sleep(1);
|
Thread.Sleep(1);
|
||||||
gap = sourceBuffer.TotalBytesWritten - readHead;
|
gap = _sourceBuffer.TotalBytesWritten - ReadHead;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (gap > sourceBuffer.BufferSize)
|
if (gap > _sourceBuffer.BufferSize)
|
||||||
{
|
{
|
||||||
// TODO: design good handling method.
|
// TODO: design good handling method.
|
||||||
// Options:
|
// Options:
|
||||||
@@ -99,12 +98,12 @@ public class WrappedBufferReadStream : Stream
|
|||||||
while (read < canCopy)
|
while (read < canCopy)
|
||||||
{
|
{
|
||||||
// The amount of bytes that we can directly write from the current position without wrapping.
|
// The amount of bytes that we can directly write from the current position without wrapping.
|
||||||
long readable = Math.Min(canCopy - read, sourceBuffer.BufferSize - Position);
|
long readable = Math.Min(canCopy - read, _sourceBuffer.BufferSize - Position);
|
||||||
|
|
||||||
// Copy the data.
|
// Copy the data.
|
||||||
Array.Copy(sourceBuffer.Buffer, Position, buffer, offset + read, readable);
|
Array.Copy(_sourceBuffer.Buffer, Position, buffer, offset + read, readable);
|
||||||
read += readable;
|
read += readable;
|
||||||
readHead += readable;
|
ReadHead += readable;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (int)read;
|
return (int)read;
|
||||||
|
@@ -21,43 +21,30 @@ namespace Jellyfin.Xtream.Service;
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Stream which writes to a self-overwriting internal buffer.
|
/// Stream which writes to a self-overwriting internal buffer.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class WrappedBufferStream : Stream
|
/// <param name="bufferSize">Size in bytes of the internal buffer.</param>
|
||||||
|
public class WrappedBufferStream(int bufferSize) : Stream
|
||||||
{
|
{
|
||||||
private readonly byte[] sourceBuffer;
|
|
||||||
|
|
||||||
private long totalBytesWritten;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the <see cref="WrappedBufferStream"/> class.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="bufferSize">Size in bytes of the internal buffer.</param>
|
|
||||||
public WrappedBufferStream(int bufferSize)
|
|
||||||
{
|
|
||||||
this.sourceBuffer = new byte[bufferSize];
|
|
||||||
this.totalBytesWritten = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the maximal size in bytes of read/write chunks.
|
/// Gets the maximal size in bytes of read/write chunks.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int BufferSize { get => sourceBuffer.Length; }
|
public int BufferSize { get => Buffer.Length; }
|
||||||
|
|
||||||
#pragma warning disable CA1819
|
#pragma warning disable CA1819
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the internal buffer.
|
/// Gets the internal buffer.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public byte[] Buffer { get => sourceBuffer; }
|
public byte[] Buffer { get; } = new byte[bufferSize];
|
||||||
#pragma warning restore CA1819
|
#pragma warning restore CA1819
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the number of bytes that have been written to this stream.
|
/// Gets the number of bytes that have been written to this stream.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public long TotalBytesWritten { get => totalBytesWritten; }
|
public long TotalBytesWritten { get; private set; }
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override long Position
|
public override long Position
|
||||||
{
|
{
|
||||||
get => totalBytesWritten % BufferSize; set { }
|
get => TotalBytesWritten % BufferSize; set { }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@@ -69,16 +56,11 @@ public class WrappedBufferStream : Stream
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override bool CanSeek => false;
|
public override bool CanSeek => false;
|
||||||
|
|
||||||
#pragma warning disable CA1065
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override long Length { get => throw new NotImplementedException(); }
|
public override long Length => throw new NotSupportedException();
|
||||||
#pragma warning restore CA1065
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override int Read(byte[] buffer, int offset, int count)
|
public override int Read(byte[] buffer, int offset, int count) => throw new NotSupportedException();
|
||||||
{
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override void Write(byte[] buffer, int offset, int count)
|
public override void Write(byte[] buffer, int offset, int count)
|
||||||
@@ -92,23 +74,17 @@ public class WrappedBufferStream : Stream
|
|||||||
long writable = Math.Min(count - written, BufferSize - Position);
|
long writable = Math.Min(count - written, BufferSize - Position);
|
||||||
|
|
||||||
// Copy the data.
|
// Copy the data.
|
||||||
Array.Copy(buffer, offset + written, sourceBuffer, Position, writable);
|
Array.Copy(buffer, offset + written, Buffer, Position, writable);
|
||||||
written += writable;
|
written += writable;
|
||||||
totalBytesWritten += writable;
|
TotalBytesWritten += writable;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override long Seek(long offset, SeekOrigin origin)
|
public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
|
||||||
{
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override void SetLength(long value)
|
public override void SetLength(long value) => throw new NotSupportedException();
|
||||||
{
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override void Flush()
|
public override void Flush()
|
||||||
|
@@ -33,19 +33,9 @@ namespace Jellyfin.Xtream;
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// The Xtream Codes API channel.
|
/// The Xtream Codes API channel.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class VodChannel : IChannel
|
/// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param>
|
||||||
|
public class VodChannel(ILogger<VodChannel> logger) : IChannel
|
||||||
{
|
{
|
||||||
private readonly ILogger<VodChannel> logger;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the <see cref="VodChannel"/> class.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param>
|
|
||||||
public VodChannel(ILogger<VodChannel> logger)
|
|
||||||
{
|
|
||||||
this.logger = logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public string? Name => "Xtream Video On-Demand";
|
public string? Name => "Xtream Video On-Demand";
|
||||||
|
|
||||||
@@ -64,17 +54,14 @@ public class VodChannel : IChannel
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public InternalChannelFeatures GetChannelFeatures()
|
public InternalChannelFeatures GetChannelFeatures()
|
||||||
{
|
{
|
||||||
return new InternalChannelFeatures
|
return new()
|
||||||
{
|
{
|
||||||
ContentTypes = new List<ChannelMediaContentType>
|
ContentTypes = [
|
||||||
{
|
|
||||||
ChannelMediaContentType.Movie,
|
ChannelMediaContentType.Movie,
|
||||||
},
|
],
|
||||||
|
MediaTypes = [
|
||||||
MediaTypes = new List<ChannelMediaType>
|
|
||||||
{
|
|
||||||
ChannelMediaType.Video
|
ChannelMediaType.Video
|
||||||
},
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,12 +117,11 @@ public class VodChannel : IChannel
|
|||||||
{
|
{
|
||||||
long added = long.Parse(stream.Added, CultureInfo.InvariantCulture);
|
long added = long.Parse(stream.Added, CultureInfo.InvariantCulture);
|
||||||
ParsedName parsedName = StreamService.ParseName(stream.Name);
|
ParsedName parsedName = StreamService.ParseName(stream.Name);
|
||||||
List<MediaSourceInfo> sources = new List<MediaSourceInfo>()
|
List<MediaSourceInfo> sources = [
|
||||||
{
|
|
||||||
Plugin.Instance.StreamService.GetMediaSourceInfo(StreamType.Vod, stream.StreamId, stream.ContainerExtension)
|
Plugin.Instance.StreamService.GetMediaSourceInfo(StreamType.Vod, stream.StreamId, stream.ContainerExtension)
|
||||||
};
|
];
|
||||||
|
|
||||||
return new ChannelItemInfo()
|
return new()
|
||||||
{
|
{
|
||||||
ContentType = ChannelMediaContentType.Movie,
|
ContentType = ChannelMediaContentType.Movie,
|
||||||
DateCreated = DateTimeOffset.FromUnixTimeSeconds(added).DateTime,
|
DateCreated = DateTimeOffset.FromUnixTimeSeconds(added).DateTime,
|
||||||
@@ -153,10 +139,10 @@ public class VodChannel : IChannel
|
|||||||
|
|
||||||
private async Task<ChannelItemResult> GetCategories(CancellationToken cancellationToken)
|
private async Task<ChannelItemResult> GetCategories(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
|
IEnumerable<Category> categories = await Plugin.Instance.StreamService.GetVodCategories(cancellationToken).ConfigureAwait(false);
|
||||||
List<ChannelItemInfo> items = new List<ChannelItemInfo>(
|
List<ChannelItemInfo> items = new List<ChannelItemInfo>(
|
||||||
(await Plugin.Instance.StreamService.GetVodCategories(cancellationToken).ConfigureAwait(false))
|
categories.Select((Category category) => StreamService.CreateChannelItemInfo(StreamService.VodCategoryPrefix, category)));
|
||||||
.Select((Category category) => StreamService.CreateChannelItemInfo(StreamService.VodCategoryPrefix, category)));
|
return new()
|
||||||
return new ChannelItemResult()
|
|
||||||
{
|
{
|
||||||
Items = items,
|
Items = items,
|
||||||
TotalRecordCount = items.Count
|
TotalRecordCount = items.Count
|
||||||
@@ -165,10 +151,9 @@ public class VodChannel : IChannel
|
|||||||
|
|
||||||
private async Task<ChannelItemResult> GetStreams(int categoryId, CancellationToken cancellationToken)
|
private async Task<ChannelItemResult> GetStreams(int categoryId, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
List<ChannelItemInfo> items = new List<ChannelItemInfo>(
|
IEnumerable<StreamInfo> streams = await Plugin.Instance.StreamService.GetVodStreams(categoryId, cancellationToken).ConfigureAwait(false);
|
||||||
(await Plugin.Instance.StreamService.GetVodStreams(categoryId, cancellationToken).ConfigureAwait(false))
|
List<ChannelItemInfo> items = new List<ChannelItemInfo>(streams.Select(CreateChannelItemInfo));
|
||||||
.Select((StreamInfo stream) => CreateChannelItemInfo(stream)));
|
ChannelItemResult result = new()
|
||||||
ChannelItemResult result = new ChannelItemResult()
|
|
||||||
{
|
{
|
||||||
Items = items,
|
Items = items,
|
||||||
TotalRecordCount = items.Count
|
TotalRecordCount = items.Count
|
||||||
|
Reference in New Issue
Block a user
Missed 'readonly' opportunity
Field '_instance' can be 'readonly'.
Show more details