TV channel customization #44
43
Jellyfin.Xtream/Api/Models/ChannelResponse.cs
Normal file
43
Jellyfin.Xtream/Api/Models/ChannelResponse.cs
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
// Copyright (C) 2022 Kevin Jilissen
|
||||||
|
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
namespace Jellyfin.Xtream.Api.Models
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Override configuration for a Live TV channel.
|
||||||
|
/// </summary>
|
||||||
|
public class ChannelResponse
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the Xtream API id of the TV channel.
|
||||||
|
/// </summary>
|
||||||
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the TV channel number.
|
||||||
|
/// </summary>
|
||||||
|
public int Number { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the TV channel name.
|
||||||
|
/// </summary>
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the url of the channel logo.
|
||||||
|
/// </summary>
|
||||||
|
public string LogoUrl { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
}
|
@@ -71,6 +71,15 @@ namespace Jellyfin.Xtream.Api
|
|||||||
CatchupDuration = 0,
|
CatchupDuration = 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private static ChannelResponse CreateChannelResponse(StreamInfo stream) =>
|
||||||
|
new ChannelResponse()
|
||||||
|
{
|
||||||
|
Id = stream.StreamId,
|
||||||
|
LogoUrl = stream.StreamIcon,
|
||||||
|
Name = stream.Name,
|
||||||
|
Number = stream.Num,
|
||||||
|
};
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get all Live TV categories.
|
/// Get all Live TV categories.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -184,5 +193,23 @@ namespace Jellyfin.Xtream.Api
|
|||||||
return Ok(series.Select((Series s) => CreateItemResponse(s)));
|
return Ok(series.Select((Series s) => CreateItemResponse(s)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get all configured TV channels.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="cancellationToken">The cancellation token for cancelling requests.</param>
|
||||||
|
/// <returns>An enumerable containing the streams.</returns>
|
||||||
|
[Authorize(Policy = "RequiresElevation")]
|
||||||
|
[HttpGet("LiveTv")]
|
||||||
|
public async Task<ActionResult<IEnumerable<StreamInfo>>> GetLiveTvChannels(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
List<ChannelResponse> channels = new List<ChannelResponse>();
|
||||||
|
await foreach (StreamInfo stream in Plugin.Instance.StreamService.GetLiveStreams(cancellationToken))
|
||||||
|
{
|
||||||
|
channels.Add(CreateChannelResponse(stream));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(channels);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -116,7 +116,7 @@ namespace Jellyfin.Xtream
|
|||||||
{
|
{
|
||||||
Plugin plugin = Plugin.Instance;
|
Plugin plugin = Plugin.Instance;
|
||||||
List<ChannelItemInfo> items = new List<ChannelItemInfo>();
|
List<ChannelItemInfo> items = new List<ChannelItemInfo>();
|
||||||
await foreach (StreamInfo channel in plugin.StreamService.GetLiveStreams(cancellationToken))
|
await foreach (StreamInfo channel in plugin.StreamService.GetLiveStreamsWithOverrides(cancellationToken))
|
||||||
{
|
{
|
||||||
if (!channel.TvArchive)
|
if (!channel.TvArchive)
|
||||||
{
|
{
|
||||||
|
45
Jellyfin.Xtream/Configuration/ChannelOverrides.cs
Normal file
45
Jellyfin.Xtream/Configuration/ChannelOverrides.cs
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
// Copyright (C) 2022 Kevin Jilissen
|
||||||
|
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
namespace Jellyfin.Xtream.Configuration
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Override configuration for a Live TV channel.
|
||||||
|
/// </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>
|
||||||
|
public int? Number { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the TV channel name.
|
||||||
|
/// </summary>
|
||||||
|
public string? Name { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the url of the channel logo.
|
||||||
|
/// </summary>
|
||||||
|
public string? LogoUrl { get; set; }
|
||||||
|
}
|
||||||
|
}
|
@@ -39,6 +39,7 @@ namespace Jellyfin.Xtream.Configuration
|
|||||||
LiveTv = new SerializableDictionary<int, HashSet<int>>();
|
LiveTv = new SerializableDictionary<int, HashSet<int>>();
|
||||||
Vod = new SerializableDictionary<int, HashSet<int>>();
|
Vod = new SerializableDictionary<int, HashSet<int>>();
|
||||||
Series = new SerializableDictionary<int, HashSet<int>>();
|
Series = new SerializableDictionary<int, HashSet<int>>();
|
||||||
|
LiveTvOverrides = new SerializableDictionary<int, ChannelOverrides>();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -85,6 +86,11 @@ namespace Jellyfin.Xtream.Configuration
|
|||||||
/// 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>
|
||||||
|
/// Gets or sets the channel override configuration for Live TV.
|
||||||
|
/// </summary>
|
||||||
|
public SerializableDictionary<int, ChannelOverrides> LiveTvOverrides { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#pragma warning restore CA2227
|
#pragma warning restore CA2227
|
||||||
|
@@ -26,3 +26,11 @@
|
|||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.overrides-table {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overrides-table thead th:first-child {
|
||||||
|
width: 0;
|
||||||
|
}
|
||||||
|
@@ -180,15 +180,23 @@ const fetchJson = (url) => ApiClient.fetch({
|
|||||||
url: ApiClient.getUrl(url),
|
url: ApiClient.getUrl(url),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const filter = (obj, predicate) => Object.keys(obj)
|
||||||
|
.filter(key => predicate(obj[key]))
|
||||||
|
.reduce((res, key) => (res[key] = obj[key], res), {});
|
||||||
|
|
||||||
const tabs = [
|
const tabs = [
|
||||||
{
|
{
|
||||||
href: url('XtreamCredentials.html'),
|
href: url('XtreamCredentials.html'),
|
||||||
name: 'Xtream Credentials'
|
name: 'Credentials'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: url('XtreamLive.html'),
|
href: url('XtreamLive.html'),
|
||||||
name: 'Live TV'
|
name: 'Live TV'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
href: url('XtreamLiveOverrides.html'),
|
||||||
|
name: 'TV overrides'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
href: url('XtreamVod.html'),
|
href: url('XtreamVod.html'),
|
||||||
name: 'Video On-Demand',
|
name: 'Video On-Demand',
|
||||||
@@ -210,6 +218,7 @@ const pluginConfig = {
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
fetchJson,
|
fetchJson,
|
||||||
|
filter,
|
||||||
pluginConfig,
|
pluginConfig,
|
||||||
populateCategoriesTable,
|
populateCategoriesTable,
|
||||||
setTabs,
|
setTabs,
|
||||||
|
31
Jellyfin.Xtream/Configuration/Web/XtreamLiveOverrides.html
Normal file
31
Jellyfin.Xtream/Configuration/Web/XtreamLiveOverrides.html
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
<div id="XtreamLivePage" data-role="page" class="page type-interior pluginConfigurationPage withTabs"
|
||||||
|
data-require="emby-input,emby-button" data-controller="__plugin/XtreamLiveOverrides.js">
|
||||||
|
<div data-role="content">
|
||||||
|
<div class="content-primary">
|
||||||
|
<form id="XtreamLiveOverridesForm">
|
||||||
|
<div class="sectionTitleContainer flex align-items-center">
|
||||||
|
<h2 class="sectionTitle">TV channel overrides</h2>
|
||||||
|
</div>
|
||||||
|
<div class="inputContainer">
|
||||||
|
<table class="overrides-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Number</th>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Logo</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="LiveChannels">
|
||||||
|
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button is="emby-button" type="submit" class="raised button-submit block emby-button">
|
||||||
|
<span>Save</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
86
Jellyfin.Xtream/Configuration/Web/XtreamLiveOverrides.js
Normal file
86
Jellyfin.Xtream/Configuration/Web/XtreamLiveOverrides.js
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
export default function (view) {
|
||||||
|
const createChannelRow = (channel, overrides) => {
|
||||||
|
const tr = document.createElement('tr');
|
||||||
|
tr.dataset['channelId'] = channel.Id;
|
||||||
|
|
||||||
|
let td = document.createElement('td');
|
||||||
|
const number = document.createElement('input');
|
||||||
|
number.type = 'number';
|
||||||
|
number.setAttribute('is', 'emby-input');
|
||||||
|
number.placeholder = channel.Number;
|
||||||
|
number.value = overrides.Number ?? '';
|
||||||
|
number.onchange = () => number.value ?
|
||||||
|
overrides.Number = parseInt(number.value) :
|
||||||
|
delete overrides.Number;
|
||||||
|
td.appendChild(number);
|
||||||
|
tr.appendChild(td);
|
||||||
|
|
||||||
|
td = document.createElement('td');
|
||||||
|
const name = document.createElement('input');
|
||||||
|
name.type = 'text';
|
||||||
|
name.setAttribute('is', 'emby-input');
|
||||||
|
name.placeholder = channel.Name;
|
||||||
|
name.value = overrides.Name ?? '';
|
||||||
|
name.onchange = () => name.value ?
|
||||||
|
overrides.Name = name.value :
|
||||||
|
delete overrides.Name;
|
||||||
|
td.appendChild(name);
|
||||||
|
tr.appendChild(td);
|
||||||
|
|
||||||
|
td = document.createElement('td');
|
||||||
|
const image = document.createElement('input');
|
||||||
|
image.type = 'text';
|
||||||
|
image.setAttribute('is', 'emby-input');
|
||||||
|
image.placeholder = channel.LogoUrl;
|
||||||
|
image.value = overrides.LogoUrl ?? '';
|
||||||
|
image.onchange = () => image.value ?
|
||||||
|
overrides.LogoUrl = image.value :
|
||||||
|
delete overrides.LogoUrl;
|
||||||
|
td.appendChild(image);
|
||||||
|
tr.appendChild(td);
|
||||||
|
|
||||||
|
return tr;
|
||||||
|
};
|
||||||
|
|
||||||
|
view.addEventListener("viewshow", () => import(
|
||||||
|
ApiClient.getUrl("web/ConfigurationPage", {
|
||||||
|
name: "Xtream.js",
|
||||||
|
})
|
||||||
|
).then((Xtream) => Xtream.default
|
||||||
|
).then((Xtream) => {
|
||||||
|
const pluginId = Xtream.pluginConfig.UniqueId;
|
||||||
|
Xtream.setTabs(2);
|
||||||
|
|
||||||
|
const getConfig = ApiClient.getPluginConfiguration(pluginId);
|
||||||
|
const table = view.querySelector('#LiveChannels');
|
||||||
|
Dashboard.showLoadingMsg();
|
||||||
|
Promise.all([
|
||||||
|
getConfig.then((config) => config.LiveTvOverrides),
|
||||||
|
Xtream.fetchJson('Xtream/LiveTv'),
|
||||||
|
]).then(([data, channels]) => {
|
||||||
|
for (const channel of channels) {
|
||||||
|
data[channel.Id] ??= {};
|
||||||
|
const row = createChannelRow(channel, data[channel.Id]);
|
||||||
|
table.appendChild(row);
|
||||||
|
}
|
||||||
|
Dashboard.hideLoadingMsg();
|
||||||
|
|
||||||
|
view.querySelector('#XtreamLiveOverridesForm').addEventListener('submit', (e) => {
|
||||||
|
Dashboard.showLoadingMsg();
|
||||||
|
|
||||||
|
ApiClient.getPluginConfiguration(pluginId).then((config) => {
|
||||||
|
config.LiveTvOverrides = Xtream.filter(
|
||||||
|
data,
|
||||||
|
overrides => Object.keys(overrides).length > 0
|
||||||
|
);
|
||||||
|
ApiClient.updatePluginConfiguration(pluginId, config).then((result) => {
|
||||||
|
Dashboard.processPluginConfigurationUpdateResult(result);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
}
|
@@ -6,7 +6,7 @@ export default function (view) {
|
|||||||
).then((Xtream) => Xtream.default
|
).then((Xtream) => Xtream.default
|
||||||
).then((Xtream) => {
|
).then((Xtream) => {
|
||||||
const pluginId = Xtream.pluginConfig.UniqueId;
|
const pluginId = Xtream.pluginConfig.UniqueId;
|
||||||
Xtream.setTabs(3);
|
Xtream.setTabs(4);
|
||||||
|
|
||||||
const getConfig = ApiClient.getPluginConfiguration(pluginId);
|
const getConfig = ApiClient.getPluginConfiguration(pluginId);
|
||||||
const visible = view.querySelector("#Visible");
|
const visible = view.querySelector("#Visible");
|
||||||
|
@@ -6,7 +6,7 @@ export default function (view) {
|
|||||||
).then((Xtream) => Xtream.default
|
).then((Xtream) => Xtream.default
|
||||||
).then((Xtream) => {
|
).then((Xtream) => {
|
||||||
const pluginId = Xtream.pluginConfig.UniqueId;
|
const pluginId = Xtream.pluginConfig.UniqueId;
|
||||||
Xtream.setTabs(2);
|
Xtream.setTabs(3);
|
||||||
|
|
||||||
const getConfig = ApiClient.getPluginConfiguration(pluginId);
|
const getConfig = ApiClient.getPluginConfiguration(pluginId);
|
||||||
const visible = view.querySelector("#Visible");
|
const visible = view.querySelector("#Visible");
|
||||||
|
@@ -68,12 +68,13 @@ namespace Jellyfin.Xtream
|
|||||||
{
|
{
|
||||||
Plugin plugin = Plugin.Instance;
|
Plugin plugin = Plugin.Instance;
|
||||||
List<ChannelInfo> items = new List<ChannelInfo>();
|
List<ChannelInfo> items = new List<ChannelInfo>();
|
||||||
await foreach (StreamInfo channel in plugin.StreamService.GetLiveStreams(cancellationToken))
|
await foreach (StreamInfo channel in plugin.StreamService.GetLiveStreamsWithOverrides(cancellationToken))
|
||||||
{
|
{
|
||||||
ParsedName parsed = plugin.StreamService.ParseName(channel.Name);
|
ParsedName parsed = plugin.StreamService.ParseName(channel.Name);
|
||||||
items.Add(new ChannelInfo()
|
items.Add(new ChannelInfo()
|
||||||
{
|
{
|
||||||
Id = channel.StreamId.ToString(System.Globalization.CultureInfo.InvariantCulture),
|
Id = channel.StreamId.ToString(CultureInfo.InvariantCulture),
|
||||||
|
Number = channel.Num.ToString(CultureInfo.InvariantCulture),
|
||||||
ImageUrl = channel.StreamIcon,
|
ImageUrl = channel.StreamIcon,
|
||||||
Name = parsed.Title,
|
Name = parsed.Title,
|
||||||
Tags = parsed.Tags,
|
Tags = parsed.Tags,
|
||||||
|
@@ -94,14 +94,14 @@ namespace Jellyfin.Xtream
|
|||||||
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 PluginPageInfo
|
||||||
{
|
{
|
||||||
Name = name,
|
Name = name,
|
||||||
EmbeddedResourcePath = string.Format(
|
EmbeddedResourcePath = string.Format(
|
||||||
CultureInfo.InvariantCulture,
|
CultureInfo.InvariantCulture,
|
||||||
"{0}.Configuration.Web.{1}",
|
"{0}.Configuration.Web.{1}",
|
||||||
typeof(Plugin).Namespace,
|
typeof(Plugin).Namespace,
|
||||||
name),
|
name),
|
||||||
};
|
};
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public IEnumerable<PluginPageInfo> GetPages()
|
public IEnumerable<PluginPageInfo> GetPages()
|
||||||
@@ -114,6 +114,8 @@ namespace Jellyfin.Xtream
|
|||||||
CreateStatic("Xtream.js"),
|
CreateStatic("Xtream.js"),
|
||||||
CreateStatic("XtreamLive.html"),
|
CreateStatic("XtreamLive.html"),
|
||||||
CreateStatic("XtreamLive.js"),
|
CreateStatic("XtreamLive.js"),
|
||||||
|
CreateStatic("XtreamLiveOverrides.html"),
|
||||||
|
CreateStatic("XtreamLiveOverrides.js"),
|
||||||
CreateStatic("XtreamSeries.html"),
|
CreateStatic("XtreamSeries.html"),
|
||||||
CreateStatic("XtreamSeries.js"),
|
CreateStatic("XtreamSeries.js"),
|
||||||
CreateStatic("XtreamVod.html"),
|
CreateStatic("XtreamVod.html"),
|
||||||
|
@@ -168,6 +168,27 @@ namespace Jellyfin.Xtream.Service
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets an async iterator for the configured channels after applying the configured overrides.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="cancellationToken">The cancellation token.</param>
|
||||||
|
/// <returns>IAsyncEnumerable{StreamInfo}.</returns>
|
||||||
|
public async IAsyncEnumerable<StreamInfo> GetLiveStreamsWithOverrides([EnumeratorCancellation] CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
PluginConfiguration config = Plugin.Instance.Configuration;
|
||||||
|
await foreach (StreamInfo stream in GetLiveStreams(cancellationToken))
|
||||||
|
{
|
||||||
|
if (config.LiveTvOverrides.TryGetValue(stream.StreamId, out ChannelOverrides? overrides))
|
||||||
|
{
|
||||||
|
stream.Num = overrides.Number ?? stream.Num;
|
||||||
|
stream.Name = overrides.Name ?? stream.Name;
|
||||||
|
stream.StreamIcon = overrides.LogoUrl ?? stream.StreamIcon;
|
||||||
|
}
|
||||||
|
|
||||||
|
yield return stream;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets an channel item info for the category.
|
/// Gets an channel item info for the category.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
Reference in New Issue
Block a user