Solve most code style warnings #149

Merged
Kevinjil merged 1 commits from fix/code-style into master 2025-01-09 17:59:21 +00:00
17 changed files with 309 additions and 560 deletions

View File

@@ -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>

View File

@@ -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 />

View File

@@ -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}";

View File

@@ -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 />

View File

@@ -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>

View File

@@ -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

View File

@@ -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&lt;TKey, TValue&gt;"/> class. /// <see cref="SerializableDictionary&lt;TKey, TValue&gt;"/> 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
{ {

View File

@@ -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,

View File

@@ -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.
github-advanced-security[bot] commented 2025-01-09 17:41:16 +00:00 (Migrated from github.com)
Review

Missed 'readonly' opportunity

Field '_instance' can be 'readonly'.

Show more details

## Missed 'readonly' opportunity Field '_instance' can be 'readonly'. [Show more details](https://github.com/Kevinjil/Jellyfin.Xtream/security/code-scanning/128)
/// </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(

View File

@@ -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

View File

@@ -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;
} }

View File

@@ -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,

View File

@@ -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();
} }

View File

@@ -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, []);
} }
} }

View File

@@ -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;

View File

@@ -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()

View File

@@ -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