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,
|
||||
};
|
||||
|
||||
private static ChannelResponse CreateChannelResponse(StreamInfo stream) =>
|
||||
new ChannelResponse()
|
||||
{
|
||||
Id = stream.StreamId,
|
||||
LogoUrl = stream.StreamIcon,
|
||||
Name = stream.Name,
|
||||
Number = stream.Num,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Get all Live TV categories.
|
||||
/// </summary>
|
||||
@@ -184,5 +193,23 @@ namespace Jellyfin.Xtream.Api
|
||||
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;
|
||||
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)
|
||||
{
|
||||
|
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>>();
|
||||
Vod = new SerializableDictionary<int, HashSet<int>>();
|
||||
Series = new SerializableDictionary<int, HashSet<int>>();
|
||||
LiveTvOverrides = new SerializableDictionary<int, ChannelOverrides>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -85,6 +86,11 @@ namespace Jellyfin.Xtream.Configuration
|
||||
/// Gets or sets the streams displayed in Series.
|
||||
/// </summary>
|
||||
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
|
||||
|
@@ -26,3 +26,11 @@
|
||||
font-size: 1em;
|
||||
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),
|
||||
});
|
||||
|
||||
const filter = (obj, predicate) => Object.keys(obj)
|
||||
.filter(key => predicate(obj[key]))
|
||||
.reduce((res, key) => (res[key] = obj[key], res), {});
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
href: url('XtreamCredentials.html'),
|
||||
name: 'Xtream Credentials'
|
||||
name: 'Credentials'
|
||||
},
|
||||
{
|
||||
href: url('XtreamLive.html'),
|
||||
name: 'Live TV'
|
||||
},
|
||||
{
|
||||
href: url('XtreamLiveOverrides.html'),
|
||||
name: 'TV overrides'
|
||||
},
|
||||
{
|
||||
href: url('XtreamVod.html'),
|
||||
name: 'Video On-Demand',
|
||||
@@ -210,6 +218,7 @@ const pluginConfig = {
|
||||
|
||||
export default {
|
||||
fetchJson,
|
||||
filter,
|
||||
pluginConfig,
|
||||
populateCategoriesTable,
|
||||
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) => {
|
||||
const pluginId = Xtream.pluginConfig.UniqueId;
|
||||
Xtream.setTabs(3);
|
||||
Xtream.setTabs(4);
|
||||
|
||||
const getConfig = ApiClient.getPluginConfiguration(pluginId);
|
||||
const visible = view.querySelector("#Visible");
|
||||
|
@@ -6,7 +6,7 @@ export default function (view) {
|
||||
).then((Xtream) => Xtream.default
|
||||
).then((Xtream) => {
|
||||
const pluginId = Xtream.pluginConfig.UniqueId;
|
||||
Xtream.setTabs(2);
|
||||
Xtream.setTabs(3);
|
||||
|
||||
const getConfig = ApiClient.getPluginConfiguration(pluginId);
|
||||
const visible = view.querySelector("#Visible");
|
||||
|
@@ -68,12 +68,13 @@ namespace Jellyfin.Xtream
|
||||
{
|
||||
Plugin plugin = Plugin.Instance;
|
||||
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);
|
||||
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,
|
||||
Name = parsed.Title,
|
||||
Tags = parsed.Tags,
|
||||
|
@@ -114,6 +114,8 @@ namespace Jellyfin.Xtream
|
||||
CreateStatic("Xtream.js"),
|
||||
CreateStatic("XtreamLive.html"),
|
||||
CreateStatic("XtreamLive.js"),
|
||||
CreateStatic("XtreamLiveOverrides.html"),
|
||||
CreateStatic("XtreamLiveOverrides.js"),
|
||||
CreateStatic("XtreamSeries.html"),
|
||||
CreateStatic("XtreamSeries.js"),
|
||||
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>
|
||||
/// Gets an channel item info for the category.
|
||||
/// </summary>
|
||||
|
Reference in New Issue
Block a user