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)]
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) =>
new CategoryResponse()
new()
{
Id = category.CategoryId,
Name = category.CategoryName,
};
private static ItemResponse CreateItemResponse(StreamInfo stream) =>
new ItemResponse()
new()
{
Id = stream.StreamId,
Name = stream.Name,
@@ -63,7 +52,7 @@ public class XtreamController : ControllerBase
};
private static ItemResponse CreateItemResponse(Series series) =>
new ItemResponse()
new()
{
Id = series.SeriesId,
Name = series.Name,
@@ -72,7 +61,7 @@ public class XtreamController : ControllerBase
};
private static ChannelResponse CreateChannelResponse(StreamInfo stream) =>
new ChannelResponse()
new()
{
Id = stream.StreamId,
LogoUrl = stream.StreamIcon,
@@ -90,11 +79,9 @@ public class XtreamController : ControllerBase
public async Task<ActionResult<IEnumerable<CategoryResponse>>> GetLiveCategories(CancellationToken cancellationToken)
{
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);
return Ok(categories.Select((Category c) => CreateCategoryResponse(c)));
}
return Ok(categories.Select(CreateCategoryResponse));
}
/// <summary>
@@ -108,14 +95,12 @@ public class XtreamController : ControllerBase
public async Task<ActionResult<IEnumerable<StreamInfo>>> GetLiveStreams(int categoryId, CancellationToken cancellationToken)
{
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);
return Ok(streams.Select((StreamInfo s) => CreateItemResponse(s)));
}
return Ok(streams.Select(CreateItemResponse));
}
/// <summary>
@@ -128,11 +113,9 @@ public class XtreamController : ControllerBase
public async Task<ActionResult<IEnumerable<CategoryResponse>>> GetVodCategories(CancellationToken cancellationToken)
{
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);
return Ok(categories.Select((Category c) => CreateCategoryResponse(c)));
}
return Ok(categories.Select(CreateCategoryResponse));
}
/// <summary>
@@ -146,14 +129,12 @@ public class XtreamController : ControllerBase
public async Task<ActionResult<IEnumerable<StreamInfo>>> GetVodStreams(int categoryId, CancellationToken cancellationToken)
{
Plugin plugin = Plugin.Instance;
using (XtreamClient client = new XtreamClient())
{
using XtreamClient client = new XtreamClient();
List<StreamInfo> streams = await client.GetVodStreamsByCategoryAsync(
plugin.Creds,
categoryId,
cancellationToken).ConfigureAwait(false);
return Ok(streams.Select((StreamInfo s) => CreateItemResponse(s)));
}
return Ok(streams.Select(CreateItemResponse));
}
/// <summary>
@@ -166,11 +147,9 @@ public class XtreamController : ControllerBase
public async Task<ActionResult<IEnumerable<CategoryResponse>>> GetSeriesCategories(CancellationToken cancellationToken)
{
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);
return Ok(categories.Select((Category c) => CreateCategoryResponse(c)));
}
return Ok(categories.Select(CreateCategoryResponse));
}
/// <summary>
@@ -184,14 +163,12 @@ public class XtreamController : ControllerBase
public async Task<ActionResult<IEnumerable<StreamInfo>>> GetSeriesStreams(int categoryId, CancellationToken cancellationToken)
{
Plugin plugin = Plugin.Instance;
using (XtreamClient client = new XtreamClient())
{
using XtreamClient client = new XtreamClient();
List<Series> series = await client.GetSeriesByCategoryAsync(
plugin.Creds,
categoryId,
cancellationToken).ConfigureAwait(false);
return Ok(series.Select((Series s) => CreateItemResponse(s)));
}
return Ok(series.Select(CreateItemResponse));
}
/// <summary>

View File

@@ -34,18 +34,10 @@ namespace Jellyfin.Xtream;
/// <summary>
/// The Xtream Codes API channel.
/// </summary>
public class CatchupChannel : IChannel
{
private readonly ILogger<CatchupChannel> 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)
public class CatchupChannel(ILogger<CatchupChannel> logger) : IChannel
{
this.logger = logger;
}
private readonly ILogger<CatchupChannel> _logger = logger;
/// <inheritdoc />
public string? Name => "Xtream Catch-up";
@@ -67,15 +59,12 @@ public class CatchupChannel : IChannel
{
return new InternalChannelFeatures
{
ContentTypes = new List<ChannelMediaContentType>
{
ContentTypes = [
ChannelMediaContentType.TvExtra,
},
MediaTypes = new List<ChannelMediaType>
{
],
MediaTypes = [
ChannelMediaType.Video
},
],
};
}
@@ -90,13 +79,10 @@ public class CatchupChannel : IChannel
}
/// <inheritdoc />
public IEnumerable<ImageType> GetSupportedChannelImages()
{
return new List<ImageType>
public IEnumerable<ImageType> GetSupportedChannelImages() => new List<ImageType>
{
// ImageType.Primary
};
}
/// <inheritdoc />
public async Task<ChannelItemResult> GetChannelItems(InternalChannelItemQuery query, CancellationToken cancellationToken)
@@ -120,7 +106,7 @@ public class CatchupChannel : IChannel
}
catch (Exception ex)
{
logger.LogError(ex, "Failed to get channel items");
_logger.LogError(ex, "Failed to get channel items");
throw;
}
}
@@ -128,7 +114,7 @@ public class CatchupChannel : IChannel
private async Task<ChannelItemResult> GetChannels(CancellationToken cancellationToken)
{
Plugin plugin = Plugin.Instance;
List<ChannelItemInfo> items = new List<ChannelItemInfo>();
List<ChannelItemInfo> items = [];
foreach (StreamInfo channel in await plugin.StreamService.GetLiveStreamsWithOverrides(cancellationToken).ConfigureAwait(false))
{
if (!channel.TvArchive)
@@ -159,23 +145,19 @@ public class CatchupChannel : IChannel
private async Task<ChannelItemResult> GetDays(int categoryId, int channelId, CancellationToken cancellationToken)
{
Plugin plugin = Plugin.Instance;
using (XtreamClient client = new XtreamClient())
{
StreamInfo? channel = (
await client.GetLiveStreamsByCategoryAsync(plugin.Creds, categoryId, cancellationToken).ConfigureAwait(false)
).FirstOrDefault(s => s.StreamId == channelId);
if (channel == null)
{
throw new ArgumentException($"Channel with id {channelId} not found in category {categoryId}");
}
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 = new List<ChannelItemInfo>();
List<ChannelItemInfo> items = [];
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()
items.Add(new()
{
Id = StreamService.ToGuid(StreamService.CatchupPrefix, channel.CategoryId, channel.StreamId, day).ToString(),
ImageUrl = channel.StreamIcon,
@@ -185,50 +167,43 @@ public class CatchupChannel : IChannel
});
}
ChannelItemResult result = new ChannelItemResult()
ChannelItemResult result = new()
{
Items = items,
TotalRecordCount = items.Count
};
return result;
}
}
private async Task<ChannelItemResult> GetStreams(int categoryId, int channelId, int day, CancellationToken cancellationToken)
{
DateTime start = DateTime.UnixEpoch.AddDays(day);
DateTime end = start.AddDays(1);
Plugin plugin = Plugin.Instance;
using (XtreamClient client = new XtreamClient())
{
StreamInfo? channel = (
await client.GetLiveStreamsByCategoryAsync(plugin.Creds, categoryId, cancellationToken).ConfigureAwait(false)
).FirstOrDefault(s => s.StreamId == channelId);
if (channel == null)
{
throw new ArgumentException($"Channel with id {channelId} not found in category {categoryId}");
}
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 = new List<ChannelItemInfo>();
List<ChannelItemInfo> items = [];
// Create fallback single-stream catch-up if no EPG is available.
if (epgs.Listings.Count == 0)
{
int duration = 24 * 60;
return new ChannelItemResult()
return new()
{
Items = new List<ChannelItemInfo>()
{
new ChannelItemInfo()
new()
{
ContentType = ChannelMediaContentType.TvExtra,
Id = StreamService.ToGuid(StreamService.CatchupStreamPrefix, channelId, 0, day).ToString(),
IsLiveStream = false,
MediaSources = new List<MediaSourceInfo>()
{
MediaSources = [
plugin.StreamService.GetMediaSourceInfo(StreamType.CatchUp, channelId, start: start, durationMinutes: duration)
},
],
MediaType = ChannelMediaType.Video,
Name = $"No EPG available",
Type = ChannelItemType.Media,
@@ -243,12 +218,11 @@ public class CatchupChannel : IChannel
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>()
{
List<MediaSourceInfo> sources = [
plugin.StreamService.GetMediaSourceInfo(StreamType.CatchUp, channelId, start: epg.StartLocalTime, durationMinutes: durationMinutes)
};
];
items.Add(new ChannelItemInfo()
items.Add(new()
{
ContentType = ChannelMediaContentType.TvExtra,
DateCreated = epg.Start,
@@ -264,14 +238,13 @@ public class CatchupChannel : IChannel
});
}
ChannelItemResult result = new ChannelItemResult()
ChannelItemResult result = new()
{
Items = items,
TotalRecordCount = items.Count
};
return result;
}
}
/// <inheritdoc />
public bool IsEnabledFor(string userId)

View File

@@ -18,35 +18,25 @@ namespace Jellyfin.Xtream.Client;
/// <summary>
/// A wrapper class for Xtream API client connection information.
/// </summary>
public class ConnectionInfo
{
/// <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)
public class ConnectionInfo(string baseUrl, string username, string password)
{
BaseUrl = baseUrl;
UserName = username;
Password = password;
}
/// <summary>
/// Gets or sets the base url including protocol and port number, without trailing slash.
/// </summary>
public string BaseUrl { get; set; }
public string BaseUrl { get; set; } = baseUrl;
/// <summary>
/// Gets or sets the username for authentication.
/// </summary>
public string UserName { get; set; }
public string UserName { get; set; } = username;
/// <summary>
/// Gets or sets the password for authentication.
/// </summary>
public string Password { get; set; }
public string Password { get; set; } = password;
/// <inheritdoc />
public override string ToString() => $"{BaseUrl} {UserName}:{Password}";

View File

@@ -27,10 +27,12 @@ namespace Jellyfin.Xtream.Client;
/// <summary>
/// The Xtream API client implementation.
/// </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>
/// Initializes a new instance of the <see cref="XtreamClient"/> class.
/// </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)
{
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)!;
}
@@ -114,7 +107,7 @@ public class XtreamClient : IDisposable
/// <param name="b">Unused.</param>
protected virtual void Dispose(bool b)
{
_client?.Dispose();
client?.Dispose();
}
/// <inheritdoc />

View File

@@ -20,13 +20,6 @@ namespace Jellyfin.Xtream.Configuration;
/// </summary>
public class ChannelOverrides
{
/// <summary>
/// Initializes a new instance of the <see cref="ChannelOverrides"/> class.
/// </summary>
public ChannelOverrides()
{
}
/// <summary>
/// Gets or sets the TV channel number.
/// </summary>

View File

@@ -24,38 +24,20 @@ namespace Jellyfin.Xtream.Configuration;
/// </summary>
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>
/// Gets or sets the base url including protocol and trailing slash.
/// </summary>
public string BaseUrl { get; set; }
public string BaseUrl { get; set; } = "https://example.com";
/// <summary>
/// Gets or sets the username.
/// </summary>
public string Username { get; set; }
public string Username { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the password.
/// </summary>
public string Password { get; set; }
public string Password { get; set; } = string.Empty;
/// <summary>
/// Gets or sets a value indicating whether the Catch-up channel is visible.
@@ -75,21 +57,21 @@ public class PluginConfiguration : BasePluginConfiguration
/// <summary>
/// Gets or sets the channels displayed in Live TV.
/// </summary>
public SerializableDictionary<int, HashSet<int>> LiveTv { get; set; }
public SerializableDictionary<int, HashSet<int>> LiveTv { get; set; } = [];
/// <summary>
/// Gets or sets the streams displayed in VOD.
/// </summary>
public SerializableDictionary<int, HashSet<int>> Vod { get; set; }
public SerializableDictionary<int, HashSet<int>> Vod { get; set; } = [];
/// <summary>
/// Gets or sets the streams displayed in Series.
/// </summary>
public SerializableDictionary<int, HashSet<int>> Series { get; set; }
public SerializableDictionary<int, HashSet<int>> Series { get; set; } = [];
/// <summary>
/// Gets or sets the channel override configuration for Live TV.
/// </summary>
public SerializableDictionary<int, ChannelOverrides> LiveTvOverrides { get; set; }
public SerializableDictionary<int, ChannelOverrides> LiveTvOverrides { get; set; } = [];
}
#pragma warning restore CA2227

View File

@@ -32,15 +32,15 @@ namespace Jellyfin.Xtream.Configuration;
public sealed class SerializableDictionary<TKey, TValue> : Dictionary<TKey, TValue>, IXmlSerializable
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
/// <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 />
public XmlSchema? GetSchema()
{
@@ -91,7 +85,7 @@ where TKey : notnull
{
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>
private void ReadItem(XmlReader reader)
{
reader.ReadStartElement(ItemTagName);
reader.ReadStartElement(ItemTag);
try
{
Add(ReadKey(reader), ReadValue(reader));
Add(SerializableDictionary<TKey, TValue>.ReadKey(reader), SerializableDictionary<TKey, TValue>.ReadValue(reader));
}
finally
{
@@ -117,17 +111,12 @@ where TKey : notnull
/// </summary>
/// <param name="reader">The XML representation of the object.</param>
/// <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
{
TKey? deserialized = (TKey?)KeySerializer.Deserialize(reader);
if (deserialized == null)
{
throw new SerializationException("Key cannot be null");
}
TKey deserialized = (TKey?)_keySerializer.Deserialize(reader) ?? throw new SerializationException("Key cannot be null");
return deserialized;
}
finally
@@ -141,17 +130,12 @@ where TKey : notnull
/// </summary>
/// <param name="reader">The XML representation of the object.</param>
/// <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
{
TValue? deserialized = (TValue?)ValueSerializer.Deserialize(reader);
if (deserialized == null)
{
throw new SerializationException("Value cannot be null");
}
TValue deserialized = (TValue?)_valueSerializer.Deserialize(reader) ?? throw new SerializationException("Value cannot be null");
return deserialized;
}
finally
@@ -165,13 +149,13 @@ where TKey : notnull
/// </summary>
/// <param name="writer">The XML writer to serialize to.</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
{
WriteKey(writer, keyValuePair.Key);
WriteValue(writer, keyValuePair.Value);
SerializableDictionary<TKey, TValue>.WriteKey(writer, keyValuePair.Key);
SerializableDictionary<TKey, TValue>.WriteValue(writer, keyValuePair.Value);
}
finally
{
@@ -184,12 +168,12 @@ where TKey : notnull
/// </summary>
/// <param name="writer">The XML writer to serialize to.</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
{
KeySerializer.Serialize(writer, key);
_keySerializer.Serialize(writer, key);
}
finally
{
@@ -202,12 +186,12 @@ where TKey : notnull
/// </summary>
/// <param name="writer">The XML writer to serialize to.</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
{
ValueSerializer.Serialize(writer, value);
_valueSerializer.Serialize(writer, value);
}
finally
{

View File

@@ -35,28 +35,15 @@ namespace Jellyfin.Xtream;
/// <summary>
/// Class LiveTvService.
/// </summary>
public class LiveTvService : ILiveTvService, ISupportsDirectStreamProvider
{
private readonly IServerApplicationHost appHost;
private readonly IHttpClientFactory httpClientFactory;
private readonly ILogger<LiveTvService> logger;
private readonly IMemoryCache memoryCache;
/// <summary>
/// <remarks>
/// Initializes a new instance of the <see cref="LiveTvService"/> class.
/// </summary>
/// </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 LiveTvService(IServerApplicationHost appHost, IHttpClientFactory httpClientFactory, ILogger<LiveTvService> logger, IMemoryCache memoryCache)
public class LiveTvService(IServerApplicationHost appHost, IHttpClientFactory httpClientFactory, ILogger<LiveTvService> logger, IMemoryCache memoryCache) : ILiveTvService, ISupportsDirectStreamProvider
{
this.appHost = appHost;
this.httpClientFactory = httpClientFactory;
this.logger = logger;
this.memoryCache = memoryCache;
}
/// <inheritdoc />
public string Name => "Xtream Live";
@@ -67,7 +54,7 @@ public class LiveTvService : ILiveTvService, ISupportsDirectStreamProvider
public async Task<IEnumerable<ChannelInfo>> GetChannelsAsync(CancellationToken cancellationToken)
{
Plugin plugin = Plugin.Instance;
List<ChannelInfo> items = new List<ChannelInfo>();
List<ChannelInfo> items = [];
foreach (StreamInfo channel in await plugin.StreamService.GetLiveStreamsWithOverrides(cancellationToken).ConfigureAwait(false))
{
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)
{
MediaSourceInfo source = await GetChannelStream(channelId, string.Empty, cancellationToken).ConfigureAwait(false);
return new List<MediaSourceInfo>() { source };
return [source];
}
/// <inheritdoc />
@@ -190,7 +177,7 @@ public class LiveTvService : ILiveTvService, ISupportsDirectStreamProvider
EpgListings epgs = await client.GetEpgInfoAsync(plugin.Creds, streamId, cancellationToken).ConfigureAwait(false);
foreach (EpgInfo epg in epgs.Listings)
{
items.Add(new ProgramInfo()
items.Add(new()
{
Id = StreamService.ToGuid(StreamService.EpgPrefix, streamId, epg.Id, 0).ToString(),
ChannelId = channelId,

View File

@@ -34,24 +34,20 @@ namespace Jellyfin.Xtream;
/// </summary>
public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages
{
private static Plugin? instance;
private readonly ILogger<Plugin> _logger;
private static Plugin? _instance;
/// <summary>
/// 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>
/// <param name="applicationPaths">Instance of the <see cref="IApplicationPaths"/> 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>
public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer, ILogger<Plugin> logger, ITaskManager taskManager)
public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer, ITaskManager taskManager)
: base(applicationPaths, xmlSerializer)
{
_logger = logger;
instance = this;
StreamService = new StreamService(logger, this);
TaskService = new TaskService(logger, this, taskManager);
_instance = this;
StreamService = new();
TaskService = new(taskManager);
}
/// <inheritdoc />
@@ -63,10 +59,7 @@ public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages
/// <summary>
/// Gets the Xtream connection info with credentials.
/// </summary>
public ConnectionInfo Creds
{
get => new ConnectionInfo(Configuration.BaseUrl, Configuration.Username, Configuration.Password);
}
public ConnectionInfo Creds => new(Configuration.BaseUrl, Configuration.Username, Configuration.Password);
/// <summary>
/// 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>
/// Gets the current plugin instance.
/// </summary>
public static Plugin Instance
{
get
{
if (instance == null)
{
throw new InvalidOperationException("Plugin instance not available");
}
return instance;
}
}
public static Plugin Instance => _instance ?? throw new InvalidOperationException("Plugin instance not available");
/// <summary>
/// Gets the stream service instance.
@@ -99,7 +81,7 @@ public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages
/// </summary>
public TaskService TaskService { get; init; }
private static PluginPageInfo CreateStatic(string name) => new PluginPageInfo
private static PluginPageInfo CreateStatic(string name) => new()
{
Name = name,
EmbeddedResourcePath = string.Format(

View File

@@ -34,19 +34,9 @@ namespace Jellyfin.Xtream;
/// <summary>
/// The Xtream Codes API channel.
/// </summary>
public class SeriesChannel : 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)
public class SeriesChannel(ILogger<SeriesChannel> logger) : IChannel
{
this.logger = logger;
}
/// <inheritdoc />
public string? Name => "Xtream Series";
@@ -67,15 +57,13 @@ public class SeriesChannel : IChannel
{
return new InternalChannelFeatures
{
ContentTypes = new List<ChannelMediaContentType>
{
ContentTypes = [
ChannelMediaContentType.Episode,
},
],
MediaTypes = new List<ChannelMediaType>
{
MediaTypes = [
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()
{
@@ -170,12 +158,12 @@ public class SeriesChannel : IChannel
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 cover = series.Info.Cover;
string? overview = null;
DateTime? created = null;
List<string> tags = new List<string>();
List<string> tags = [];
Season? season = series.Seasons.FirstOrDefault(s => s.SeasonId == seasonId);
if (season != null)
@@ -191,7 +179,7 @@ public class SeriesChannel : IChannel
}
}
return new ChannelItemInfo()
return new()
{
DateCreated = created,
// FolderType = ChannelFolderType.Season,
@@ -208,12 +196,11 @@ public class SeriesChannel : IChannel
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);
List<MediaSourceInfo> sources = new List<MediaSourceInfo>()
{
List<MediaSourceInfo> sources = [
Plugin.Instance.StreamService.GetMediaSourceInfo(StreamType.Series, episode.EpisodeId, episode.ContainerExtension)
};
];
string? cover = episode.Info?.MovieImage;
if (string.IsNullOrEmpty(cover) && season != null)
@@ -226,7 +213,7 @@ public class SeriesChannel : IChannel
cover = serie.Cover;
}
return new ChannelItemInfo()
return new()
{
ContentType = ChannelMediaContentType.Episode,
DateCreated = DateTimeOffset.FromUnixTimeSeconds(episode.Added).DateTime,
@@ -239,17 +226,17 @@ public class SeriesChannel : IChannel
Name = parsedName.Title,
Overview = episode.Info?.Plot,
People = GetPeople(serie.Cast),
Tags = new List<string>(parsedName.Tags),
Tags = new(parsedName.Tags),
Type = ChannelItemType.Media,
};
}
private async Task<ChannelItemResult> GetCategories(CancellationToken cancellationToken)
{
List<ChannelItemInfo> items = new List<ChannelItemInfo>(
(await Plugin.Instance.StreamService.GetSeriesCategories(cancellationToken).ConfigureAwait(false))
.Select((Category category) => StreamService.CreateChannelItemInfo(StreamService.SeriesCategoryPrefix, category)));
return new ChannelItemResult()
IEnumerable<Category> categories = await Plugin.Instance.StreamService.GetSeriesCategories(cancellationToken).ConfigureAwait(false);
List<ChannelItemInfo> items = new(
categories.Select((Category category) => StreamService.CreateChannelItemInfo(StreamService.SeriesCategoryPrefix, category)));
return new()
{
Items = items,
TotalRecordCount = items.Count
@@ -258,10 +245,9 @@ public class SeriesChannel : IChannel
private async Task<ChannelItemResult> GetSeries(int categoryId, CancellationToken cancellationToken)
{
List<ChannelItemInfo> items = new List<ChannelItemInfo>(
(await Plugin.Instance.StreamService.GetSeries(categoryId, cancellationToken).ConfigureAwait(false))
.Select((Series series) => CreateChannelItemInfo(series)));
return new ChannelItemResult()
IEnumerable<Series> series = await Plugin.Instance.StreamService.GetSeries(categoryId, cancellationToken).ConfigureAwait(false);
List<ChannelItemInfo> items = new(series.Select(CreateChannelItemInfo));
return new()
{
Items = items,
TotalRecordCount = items.Count
@@ -270,10 +256,10 @@ public class SeriesChannel : IChannel
private async Task<ChannelItemResult> GetSeasons(int seriesId, CancellationToken cancellationToken)
{
List<ChannelItemInfo> items = new List<ChannelItemInfo>(
(await Plugin.Instance.StreamService.GetSeasons(seriesId, cancellationToken).ConfigureAwait(false))
.Select((Tuple<SeriesStreamInfo, int> tuple) => CreateChannelItemInfo(seriesId, tuple.Item1, tuple.Item2)));
return new ChannelItemResult()
IEnumerable<Tuple<SeriesStreamInfo, int>> seasons = await Plugin.Instance.StreamService.GetSeasons(seriesId, cancellationToken).ConfigureAwait(false);
List<ChannelItemInfo> items = new(
seasons.Select((Tuple<SeriesStreamInfo, int> tuple) => CreateChannelItemInfo(seriesId, tuple.Item1, tuple.Item2)));
return new()
{
Items = items,
TotalRecordCount = items.Count
@@ -282,10 +268,10 @@ public class SeriesChannel : IChannel
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>(
(await Plugin.Instance.StreamService.GetEpisodes(seriesId, seasonId, cancellationToken).ConfigureAwait(false))
.Select((Tuple<SeriesStreamInfo, Season?, Episode> tuple) => CreateChannelItemInfo(tuple.Item1, tuple.Item2, tuple.Item3)));
return new ChannelItemResult()
episodes.Select((Tuple<SeriesStreamInfo, Season?, Episode> tuple) => CreateChannelItemInfo(tuple.Item1, tuple.Item2, tuple.Item3)));
return new()
{
Items = items,
TotalRecordCount = items.Count

View File

@@ -20,26 +20,17 @@ namespace Jellyfin.Xtream.Service;
/// <summary>
/// A struct which holds information of parsed stream names.
/// </summary>
public struct ParsedName
{
/// <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)
public readonly struct ParsedName(string title, string[] tags)
{
Title = title;
Tags = tags;
}
/// <summary>
/// Gets the parsed title.
/// </summary>
public string Title { get; init; }
public string Title { get; init; } = title;
/// <summary>
/// Gets the parsed tags.
/// </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>
public const string TunerHost = "Xtream-Restream";
private static readonly HttpStatusCode[] _redirects =
[
private static readonly HttpStatusCode[] _redirects = [
HttpStatusCode.Moved,
HttpStatusCode.MovedPermanently,
HttpStatusCode.PermanentRedirect,

View File

@@ -17,7 +17,6 @@ using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
@@ -35,7 +34,7 @@ namespace Jellyfin.Xtream.Service;
/// <summary>
/// A service for dealing with stream information.
/// </summary>
public class StreamService
public partial class StreamService
{
/// <summary>
/// The id prefix for VOD category channel items.
@@ -92,21 +91,7 @@ public class StreamService
/// </summary>
public const int EpgPrefix = 0x5d774c3f;
private static readonly Regex TagRegex = new Regex(@"\[([^\]]+)\]|\|([^\|]+)\|");
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;
}
private static readonly Regex _tagRegex = TagRegex();
/// <summary>
/// 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>
public static ParsedName ParseName(string name)
{
List<string> tags = new List<string>();
string title = TagRegex.Replace(
List<string> tags = [];
string title = _tagRegex.Replace(
name,
(match) =>
{
@@ -154,14 +139,13 @@ public class StreamService
return new ParsedName
{
Title = title[stripLength..].Trim(),
Tags = tags.ToArray(),
Tags = [.. tags],
};
}
private bool IsConfigured(SerializableDictionary<int, HashSet<int>> config, int category, int id)
{
HashSet<int>? values;
return config.TryGetValue(category, out values) && (values.Count == 0 || values.Contains(id));
return config.TryGetValue(category, out var values) && (values.Count == 0 || values.Contains(id));
}
/// <summary>
@@ -171,13 +155,13 @@ public class StreamService
/// <returns>IAsyncEnumerable{StreamInfo}.</returns>
public async Task<IEnumerable<StreamInfo>> GetLiveStreams(CancellationToken cancellationToken)
{
PluginConfiguration config = plugin.Configuration;
PluginConfiguration config = Plugin.Instance.Configuration;
using XtreamClient client = new XtreamClient();
IEnumerable<Task<IEnumerable<StreamInfo>>> tasks = config.LiveTv.Select(async (entry) =>
{
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 (await Task.WhenAll(tasks).ConfigureAwait(false)).SelectMany(i => i);
@@ -230,11 +214,9 @@ public class StreamService
/// <returns>IAsyncEnumerable{StreamInfo}.</returns>
public async Task<IEnumerable<Category>> GetVodCategories(CancellationToken cancellationToken)
{
using (XtreamClient client = new XtreamClient())
{
List<Category> categories = await client.GetVodCategoryAsync(plugin.Creds, cancellationToken).ConfigureAwait(false);
return categories.Where((Category category) => plugin.Configuration.Vod.ContainsKey(category.CategoryId));
}
using XtreamClient client = new XtreamClient();
List<Category> categories = await client.GetVodCategoryAsync(Plugin.Instance.Creds, cancellationToken).ConfigureAwait(false);
return categories.Where((Category category) => Plugin.Instance.Configuration.Vod.ContainsKey(category.CategoryId));
}
/// <summary>
@@ -245,16 +227,14 @@ public class StreamService
/// <returns>IAsyncEnumerable{StreamInfo}.</returns>
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>();
}
using (XtreamClient client = new XtreamClient())
{
List<StreamInfo> streams = await client.GetVodStreamsByCategoryAsync(plugin.Creds, categoryId, cancellationToken).ConfigureAwait(false);
return streams.Where((StreamInfo stream) => IsConfigured(plugin.Configuration.Vod, categoryId, stream.StreamId));
}
using XtreamClient client = new XtreamClient();
List<StreamInfo> streams = await client.GetVodStreamsByCategoryAsync(Plugin.Instance.Creds, categoryId, cancellationToken).ConfigureAwait(false);
return streams.Where((StreamInfo stream) => IsConfigured(Plugin.Instance.Configuration.Vod, categoryId, stream.StreamId));
}
/// <summary>
@@ -264,12 +244,9 @@ public class StreamService
/// <returns>IAsyncEnumerable{StreamInfo}.</returns>
public async Task<IEnumerable<Category>> GetSeriesCategories(CancellationToken cancellationToken)
{
using (XtreamClient client = new XtreamClient())
{
List<Category> categories = await client.GetSeriesCategoryAsync(plugin.Creds, cancellationToken).ConfigureAwait(false);
return categories
.Where((Category category) => plugin.Configuration.Series.ContainsKey(category.CategoryId));
}
using XtreamClient client = new XtreamClient();
List<Category> categories = await client.GetSeriesCategoryAsync(Plugin.Instance.Creds, cancellationToken).ConfigureAwait(false);
return categories.Where((Category category) => Plugin.Instance.Configuration.Series.ContainsKey(category.CategoryId));
}
/// <summary>
@@ -280,16 +257,14 @@ public class StreamService
/// <returns>IAsyncEnumerable{StreamInfo}.</returns>
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>();
}
using (XtreamClient client = new XtreamClient())
{
List<Series> series = await client.GetSeriesByCategoryAsync(plugin.Creds, categoryId, cancellationToken).ConfigureAwait(false);
return series.Where((Series series) => IsConfigured(plugin.Configuration.Series, series.CategoryId, series.SeriesId));
}
using XtreamClient client = new XtreamClient();
List<Series> series = await client.GetSeriesByCategoryAsync(Plugin.Instance.Creds, categoryId, cancellationToken).ConfigureAwait(false);
return series.Where((Series series) => IsConfigured(Plugin.Instance.Configuration.Series, series.CategoryId, series.SeriesId));
}
/// <summary>
@@ -300,18 +275,16 @@ public class StreamService
/// <returns>IAsyncEnumerable{StreamInfo}.</returns>
public async Task<IEnumerable<Tuple<SeriesStreamInfo, int>>> GetSeasons(int seriesId, CancellationToken cancellationToken)
{
using (XtreamClient client = new XtreamClient())
{
SeriesStreamInfo series = await client.GetSeriesStreamsBySeriesAsync(plugin.Creds, seriesId, cancellationToken).ConfigureAwait(false);
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.Configuration.Series, categoryId, seriesId))
if (!IsConfigured(Plugin.Instance.Configuration.Series, categoryId, seriesId))
{
return new List<Tuple<SeriesStreamInfo, int>>();
}
return series.Episodes.Keys.Select((int seasonId) => new Tuple<SeriesStreamInfo, int>(series, seasonId));
}
}
/// <summary>
/// Gets an iterator for the configured seasons in the Series.
@@ -322,13 +295,11 @@ public class StreamService
/// <returns>IAsyncEnumerable{StreamInfo}.</returns>
public async Task<IEnumerable<Tuple<SeriesStreamInfo, Season?, Episode>>> GetEpisodes(int seriesId, int seasonId, CancellationToken cancellationToken)
{
using (XtreamClient client = new XtreamClient())
{
SeriesStreamInfo series = await client.GetSeriesStreamsBySeriesAsync(plugin.Creds, seriesId, cancellationToken).ConfigureAwait(false);
using XtreamClient client = new XtreamClient();
SeriesStreamInfo series = await client.GetSeriesStreamsBySeriesAsync(Plugin.Instance.Creds, seriesId, cancellationToken).ConfigureAwait(false);
Season? season = series.Seasons.FirstOrDefault(s => s.SeasonId == seasonId);
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)
{
@@ -362,14 +333,14 @@ public class StreamService
/// <summary>
/// Gets the four 32-bit integers represented in the GUID.
/// </summary>
/// <param name="guid">The input GUID.</param>
/// <param name="id">The input GUID.</param>
/// <param name="i0">Bytes 0-3.</param>
/// <param name="i1">Bytes 4-7.</param>
/// <param name="i2">Bytes 8-11.</param>
/// <param name="i3">Bytes 12-15.</param>
public static void FromGuid(Guid guid, out int i0, out int i1, out int i2, out int i3)
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)
{
Array.Reverse(tmp);
@@ -416,7 +387,7 @@ public class StreamService
break;
}
PluginConfiguration config = plugin.Configuration;
PluginConfiguration config = Plugin.Instance.Configuration;
string uri = $"{config.BaseUrl}{prefix}/{config.Username}/{config.Password}/{id}";
if (!string.IsNullOrEmpty(extension))
{
@@ -437,20 +408,19 @@ public class StreamService
IsInfiniteStream = isLive,
IsRemote = true,
// Define media sources with unknown index and interlaced to improve compatibility.
MediaStreams = new MediaStream[]
{
new MediaStream
MediaStreams = [
new()
{
Type = MediaStreamType.Video,
Index = -1,
IsInterlaced = true
},
new MediaStream
new()
{
Type = MediaStreamType.Audio,
Index = -1
}
},
],
Name = "default",
Path = uri,
Protocol = MediaProtocol.Http,
@@ -461,4 +431,7 @@ public class StreamService
SupportsProbing = true,
};
}
[GeneratedRegex(@"\[([^\]]+)\]|\|([^\|]+)\|")]
private static partial Regex TagRegex();
}

View File

@@ -16,32 +16,15 @@
using System;
using System.Linq;
using MediaBrowser.Model.Tasks;
using Microsoft.Extensions.Logging;
namespace Jellyfin.Xtream.Service;
/// <summary>
/// A service for dealing with stream information.
/// </summary>
public class TaskService
{
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)
public class TaskService(ITaskManager taskManager)
{
this.logger = logger;
this.plugin = plugin;
this.taskManager = taskManager;
}
private static Type? FindType(string assembly, string fullName)
{
return AppDomain.CurrentDomain.GetAssemblies()
@@ -60,16 +43,12 @@ public class TaskService
/// <exception cref="ArgumentException">If the task type is not found.</exception>
public void CancelIfRunningAndQueue(string assembly, string fullName)
{
Type? refreshType = FindType(assembly, fullName);
if (refreshType == null)
{
throw new ArgumentException("Refresh task not found");
}
Type refreshType = FindType(assembly, fullName) ?? throw new ArgumentException("Refresh task not found");
// As the type is not publicly visible, use reflection.
typeof(ITaskManager)
.GetMethod(nameof(ITaskManager.CancelIfRunningAndQueue), 1, Array.Empty<Type>())?
.GetMethod(nameof(ITaskManager.CancelIfRunningAndQueue), 1, [])?
.MakeGenericMethod(refreshType)?
.Invoke(taskManager, Array.Empty<object>());
.Invoke(taskManager, []);
}
}

View File

@@ -24,10 +24,9 @@ namespace Jellyfin.Xtream.Service;
/// </summary>
public class WrappedBufferReadStream : Stream
{
private readonly WrappedBufferStream sourceBuffer;
private readonly WrappedBufferStream _sourceBuffer;
private readonly long initialReadHead;
private long readHead;
private readonly long _initialReadHead;
/// <summary>
/// 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>
public WrappedBufferReadStream(WrappedBufferStream sourceBuffer)
{
this.sourceBuffer = sourceBuffer;
this.readHead = sourceBuffer.TotalBytesWritten;
this.initialReadHead = readHead;
_sourceBuffer = sourceBuffer;
_initialReadHead = sourceBuffer.TotalBytesWritten;
ReadHead = _initialReadHead;
}
/// <summary>
/// Gets the virtual position in the source buffer.
/// </summary>
public long ReadHead { get => readHead; }
public long ReadHead { get; private set; }
/// <summary>
/// Gets the number of bytes that have been written to this stream.
/// </summary>
public long TotalBytesRead { get => readHead - initialReadHead; }
public long TotalBytesRead { get => ReadHead - _initialReadHead; }
/// <inheritdoc />
public override long Position
{
get => readHead % sourceBuffer.BufferSize; set { }
get => ReadHead % _sourceBuffer.BufferSize; set { }
}
/// <inheritdoc />
@@ -73,16 +72,16 @@ public class WrappedBufferReadStream : Stream
/// <inheritdoc />
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
while (gap == 0)
{
Thread.Sleep(1);
gap = sourceBuffer.TotalBytesWritten - readHead;
gap = _sourceBuffer.TotalBytesWritten - ReadHead;
}
if (gap > sourceBuffer.BufferSize)
if (gap > _sourceBuffer.BufferSize)
{
// TODO: design good handling method.
// Options:
@@ -99,12 +98,12 @@ public class WrappedBufferReadStream : Stream
while (read < canCopy)
{
// 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.
Array.Copy(sourceBuffer.Buffer, Position, buffer, offset + read, readable);
Array.Copy(_sourceBuffer.Buffer, Position, buffer, offset + read, readable);
read += readable;
readHead += readable;
ReadHead += readable;
}
return (int)read;

View File

@@ -21,43 +21,30 @@ namespace Jellyfin.Xtream.Service;
/// <summary>
/// Stream which writes to a self-overwriting internal buffer.
/// </summary>
public class WrappedBufferStream : 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)
public class WrappedBufferStream(int bufferSize) : Stream
{
this.sourceBuffer = new byte[bufferSize];
this.totalBytesWritten = 0;
}
/// <summary>
/// Gets the maximal size in bytes of read/write chunks.
/// </summary>
public int BufferSize { get => sourceBuffer.Length; }
public int BufferSize { get => Buffer.Length; }
#pragma warning disable CA1819
/// <summary>
/// Gets the internal buffer.
/// </summary>
public byte[] Buffer { get => sourceBuffer; }
public byte[] Buffer { get; } = new byte[bufferSize];
#pragma warning restore CA1819
/// <summary>
/// Gets the number of bytes that have been written to this stream.
/// </summary>
public long TotalBytesWritten { get => totalBytesWritten; }
public long TotalBytesWritten { get; private set; }
/// <inheritdoc />
public override long Position
{
get => totalBytesWritten % BufferSize; set { }
get => TotalBytesWritten % BufferSize; set { }
}
/// <inheritdoc />
@@ -69,16 +56,11 @@ public class WrappedBufferStream : Stream
/// <inheritdoc />
public override bool CanSeek => false;
#pragma warning disable CA1065
/// <inheritdoc />
public override long Length { get => throw new NotImplementedException(); }
#pragma warning restore CA1065
public override long Length => throw new NotSupportedException();
/// <inheritdoc />
public override int Read(byte[] buffer, int offset, int count)
{
throw new NotImplementedException();
}
public override int Read(byte[] buffer, int offset, int count) => throw new NotSupportedException();
/// <inheritdoc />
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);
// Copy the data.
Array.Copy(buffer, offset + written, sourceBuffer, Position, writable);
Array.Copy(buffer, offset + written, Buffer, Position, writable);
written += writable;
totalBytesWritten += writable;
TotalBytesWritten += writable;
}
}
/// <inheritdoc />
public override long Seek(long offset, SeekOrigin origin)
{
throw new NotImplementedException();
}
public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
/// <inheritdoc />
public override void SetLength(long value)
{
throw new NotImplementedException();
}
public override void SetLength(long value) => throw new NotSupportedException();
/// <inheritdoc />
public override void Flush()

View File

@@ -33,19 +33,9 @@ namespace Jellyfin.Xtream;
/// <summary>
/// The Xtream Codes API channel.
/// </summary>
public class VodChannel : 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)
public class VodChannel(ILogger<VodChannel> logger) : IChannel
{
this.logger = logger;
}
/// <inheritdoc />
public string? Name => "Xtream Video On-Demand";
@@ -64,17 +54,14 @@ public class VodChannel : IChannel
/// <inheritdoc />
public InternalChannelFeatures GetChannelFeatures()
{
return new InternalChannelFeatures
{
ContentTypes = new List<ChannelMediaContentType>
return new()
{
ContentTypes = [
ChannelMediaContentType.Movie,
},
MediaTypes = new List<ChannelMediaType>
{
],
MediaTypes = [
ChannelMediaType.Video
},
],
};
}
@@ -130,12 +117,11 @@ public class VodChannel : IChannel
{
long added = long.Parse(stream.Added, CultureInfo.InvariantCulture);
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)
};
];
return new ChannelItemInfo()
return new()
{
ContentType = ChannelMediaContentType.Movie,
DateCreated = DateTimeOffset.FromUnixTimeSeconds(added).DateTime,
@@ -153,10 +139,10 @@ public class VodChannel : IChannel
private async Task<ChannelItemResult> GetCategories(CancellationToken cancellationToken)
{
IEnumerable<Category> categories = await Plugin.Instance.StreamService.GetVodCategories(cancellationToken).ConfigureAwait(false);
List<ChannelItemInfo> items = new List<ChannelItemInfo>(
(await Plugin.Instance.StreamService.GetVodCategories(cancellationToken).ConfigureAwait(false))
.Select((Category category) => StreamService.CreateChannelItemInfo(StreamService.VodCategoryPrefix, category)));
return new ChannelItemResult()
categories.Select((Category category) => StreamService.CreateChannelItemInfo(StreamService.VodCategoryPrefix, category)));
return new()
{
Items = items,
TotalRecordCount = items.Count
@@ -165,10 +151,9 @@ public class VodChannel : IChannel
private async Task<ChannelItemResult> GetStreams(int categoryId, CancellationToken cancellationToken)
{
List<ChannelItemInfo> items = new List<ChannelItemInfo>(
(await Plugin.Instance.StreamService.GetVodStreams(categoryId, cancellationToken).ConfigureAwait(false))
.Select((StreamInfo stream) => CreateChannelItemInfo(stream)));
ChannelItemResult result = new ChannelItemResult()
IEnumerable<StreamInfo> streams = await Plugin.Instance.StreamService.GetVodStreams(categoryId, cancellationToken).ConfigureAwait(false);
List<ChannelItemInfo> items = new List<ChannelItemInfo>(streams.Select(CreateChannelItemInfo));
ChannelItemResult result = new()
{
Items = items,
TotalRecordCount = items.Count