From 77e8d18b36b70ae687292a632f2d3cd594bf540f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 1 Jul 2022 06:05:54 +0000 Subject: [PATCH 1/6] ci(deps): bump actions/download-artifact from 2.1.0 to 3.0.0 Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 2.1.0 to 3.0.0. - [Release notes](https://github.com/actions/download-artifact/releases) - [Commits](https://github.com/actions/download-artifact/compare/v2.1.0...v3.0.0) --- updated-dependencies: - dependency-name: actions/download-artifact dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/publish.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index a3944ee..333b748 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -18,7 +18,7 @@ jobs: - build steps: - name: Download Artifact - uses: actions/download-artifact@v2.1.0 + uses: actions/download-artifact@v3.0.0 with: name: build-artifact - name: Prepare GitHub Release assets -- 2.51.0 From d810ed3581e27da9b9af33fc48051dc8e22d895f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 1 Jul 2022 06:05:56 +0000 Subject: [PATCH 2/6] ci(deps): bump shogo82148/actions-upload-release-asset Bumps [shogo82148/actions-upload-release-asset](https://github.com/shogo82148/actions-upload-release-asset) from 1.5.0 to 1.6.2. - [Release notes](https://github.com/shogo82148/actions-upload-release-asset/releases) - [Commits](https://github.com/shogo82148/actions-upload-release-asset/compare/v1.5.0...v1.6.2) --- updated-dependencies: - dependency-name: shogo82148/actions-upload-release-asset dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/publish.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index a3944ee..9ce0713 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -29,7 +29,7 @@ jobs: done ls -l - name: Upload GitHub Release assets - uses: shogo82148/actions-upload-release-asset@v1.5.0 + uses: shogo82148/actions-upload-release-asset@v1.6.2 with: upload_url: ${{ github.event.release.upload_url }} asset_path: ./* -- 2.51.0 From 9f5b2b57d5add129d71a9abbb4c35d43db54a074 Mon Sep 17 00:00:00 2001 From: Kevin Jilissen Date: Tue, 5 Jul 2022 19:52:35 +0200 Subject: [PATCH 3/6] Add plugin configuration for overriding TV channel properties. --- Jellyfin.Xtream/CatchupChannel.cs | 2 +- .../Configuration/ChannelOverrides.cs | 45 +++++++++++++++++++ .../Configuration/PluginConfiguration.cs | 6 +++ Jellyfin.Xtream/LiveTvService.cs | 5 ++- Jellyfin.Xtream/Service/StreamService.cs | 21 +++++++++ 5 files changed, 76 insertions(+), 3 deletions(-) create mode 100644 Jellyfin.Xtream/Configuration/ChannelOverrides.cs diff --git a/Jellyfin.Xtream/CatchupChannel.cs b/Jellyfin.Xtream/CatchupChannel.cs index 4806942..ffbcf45 100644 --- a/Jellyfin.Xtream/CatchupChannel.cs +++ b/Jellyfin.Xtream/CatchupChannel.cs @@ -116,7 +116,7 @@ namespace Jellyfin.Xtream { Plugin plugin = Plugin.Instance; List items = new List(); - await foreach (StreamInfo channel in plugin.StreamService.GetLiveStreams(cancellationToken)) + await foreach (StreamInfo channel in plugin.StreamService.GetLiveStreamsWithOverrides(cancellationToken)) { if (!channel.TvArchive) { diff --git a/Jellyfin.Xtream/Configuration/ChannelOverrides.cs b/Jellyfin.Xtream/Configuration/ChannelOverrides.cs new file mode 100644 index 0000000..a5cd013 --- /dev/null +++ b/Jellyfin.Xtream/Configuration/ChannelOverrides.cs @@ -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 . + +namespace Jellyfin.Xtream.Configuration +{ + /// + /// Override configuration for a Live TV channel. + /// + public class ChannelOverrides + { + /// + /// Initializes a new instance of the class. + /// + public ChannelOverrides() + { + } + + /// + /// Gets or sets the TV channel number. + /// + public int? Number { get; set; } + + /// + /// Gets or sets the TV channel name. + /// + public string? Name { get; set; } + + /// + /// Gets or sets the url of the channel logo. + /// + public string? LogoUrl { get; set; } + } +} diff --git a/Jellyfin.Xtream/Configuration/PluginConfiguration.cs b/Jellyfin.Xtream/Configuration/PluginConfiguration.cs index 4be5edc..d745740 100644 --- a/Jellyfin.Xtream/Configuration/PluginConfiguration.cs +++ b/Jellyfin.Xtream/Configuration/PluginConfiguration.cs @@ -39,6 +39,7 @@ namespace Jellyfin.Xtream.Configuration LiveTv = new SerializableDictionary>(); Vod = new SerializableDictionary>(); Series = new SerializableDictionary>(); + LiveTvOverrides = new SerializableDictionary(); } /// @@ -85,6 +86,11 @@ namespace Jellyfin.Xtream.Configuration /// Gets or sets the streams displayed in Series. /// public SerializableDictionary> Series { get; set; } + + /// + /// Gets or sets the channel override configuration for Live TV. + /// + public SerializableDictionary LiveTvOverrides { get; set; } } } #pragma warning restore CA2227 diff --git a/Jellyfin.Xtream/LiveTvService.cs b/Jellyfin.Xtream/LiveTvService.cs index 04cbc91..e05f892 100644 --- a/Jellyfin.Xtream/LiveTvService.cs +++ b/Jellyfin.Xtream/LiveTvService.cs @@ -68,12 +68,13 @@ namespace Jellyfin.Xtream { Plugin plugin = Plugin.Instance; List items = new List(); - 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, diff --git a/Jellyfin.Xtream/Service/StreamService.cs b/Jellyfin.Xtream/Service/StreamService.cs index 595fcb7..1cc048c 100644 --- a/Jellyfin.Xtream/Service/StreamService.cs +++ b/Jellyfin.Xtream/Service/StreamService.cs @@ -168,6 +168,27 @@ namespace Jellyfin.Xtream.Service } } + /// + /// Gets an async iterator for the configured channels after applying the configured overrides. + /// + /// The cancellation token. + /// IAsyncEnumerable{StreamInfo}. + public async IAsyncEnumerable 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; + } + } + /// /// Gets an channel item info for the category. /// -- 2.51.0 From 0c61fcea881436fcafb4fd466cd0af3b2511eab2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Aug 2022 06:08:50 +0000 Subject: [PATCH 4/6] ci(deps): bump Kevinjil/jellyfin-plugin-repo-action from 0.2.2 to 0.4.1 Bumps [Kevinjil/jellyfin-plugin-repo-action](https://github.com/Kevinjil/jellyfin-plugin-repo-action) from 0.2.2 to 0.4.1. - [Release notes](https://github.com/Kevinjil/jellyfin-plugin-repo-action/releases) - [Commits](https://github.com/Kevinjil/jellyfin-plugin-repo-action/compare/v0.2.2...v0.4.1) --- updated-dependencies: - dependency-name: Kevinjil/jellyfin-plugin-repo-action dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/publish.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index 0137a16..c18fecf 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -39,7 +39,7 @@ jobs: - upload steps: - name: Jellyfin plugin repo - uses: Kevinjil/jellyfin-plugin-repo-action@v0.2.2 + uses: Kevinjil/jellyfin-plugin-repo-action@v0.4.1 with: githubToken: ${{ secrets.GITHUB_TOKEN }} repository: ${{ github.repository }} -- 2.51.0 From 4ebb9742c3a53eee54f261264a40f0e53f915b31 Mon Sep 17 00:00:00 2001 From: Kevin Jilissen Date: Sun, 9 Oct 2022 12:50:16 +0200 Subject: [PATCH 5/6] Initial implementation of Live TV customization. --- Jellyfin.Xtream/Api/Models/ChannelResponse.cs | 43 ++++++++++ Jellyfin.Xtream/Api/XtreamController.cs | 31 +++++++ Jellyfin.Xtream/Configuration/Web/Xtream.css | 8 ++ Jellyfin.Xtream/Configuration/Web/Xtream.js | 11 ++- .../Web/XtreamLiveOverrides.html | 31 +++++++ .../Configuration/Web/XtreamLiveOverrides.js | 86 +++++++++++++++++++ .../Configuration/Web/XtreamSeries.js | 2 +- .../Configuration/Web/XtreamVod.js | 2 +- Jellyfin.Xtream/Plugin.cs | 18 ++-- 9 files changed, 221 insertions(+), 11 deletions(-) create mode 100644 Jellyfin.Xtream/Api/Models/ChannelResponse.cs create mode 100644 Jellyfin.Xtream/Configuration/Web/XtreamLiveOverrides.html create mode 100644 Jellyfin.Xtream/Configuration/Web/XtreamLiveOverrides.js diff --git a/Jellyfin.Xtream/Api/Models/ChannelResponse.cs b/Jellyfin.Xtream/Api/Models/ChannelResponse.cs new file mode 100644 index 0000000..5956572 --- /dev/null +++ b/Jellyfin.Xtream/Api/Models/ChannelResponse.cs @@ -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 . + +namespace Jellyfin.Xtream.Api.Models +{ + /// + /// Override configuration for a Live TV channel. + /// + public class ChannelResponse + { + /// + /// Gets or sets the Xtream API id of the TV channel. + /// + public int Id { get; set; } + + /// + /// Gets or sets the TV channel number. + /// + public int Number { get; set; } + + /// + /// Gets or sets the TV channel name. + /// + public string Name { get; set; } = string.Empty; + + /// + /// Gets or sets the url of the channel logo. + /// + public string LogoUrl { get; set; } = string.Empty; + } +} diff --git a/Jellyfin.Xtream/Api/XtreamController.cs b/Jellyfin.Xtream/Api/XtreamController.cs index fafd34d..b0f4a80 100644 --- a/Jellyfin.Xtream/Api/XtreamController.cs +++ b/Jellyfin.Xtream/Api/XtreamController.cs @@ -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, + }; + /// /// Get all Live TV categories. /// @@ -184,5 +193,27 @@ namespace Jellyfin.Xtream.Api return Ok(series.Select((Series s) => CreateItemResponse(s))); } } + + /// + /// Get all configured TV channels. + /// + /// The cancellation token for cancelling requests. + /// An enumerable containing the streams. + [Authorize(Policy = "RequiresElevation")] + [HttpGet("LiveTv")] + public async Task>> GetLiveTvChannels(CancellationToken cancellationToken) + { + Plugin plugin = Plugin.Instance; + using (XtreamClient client = new XtreamClient()) + { + List channels = new List(); + await foreach (StreamInfo stream in Plugin.Instance.StreamService.GetLiveStreams(cancellationToken)) + { + channels.Add(CreateChannelResponse(stream)); + } + + return Ok(channels); + } + } } } diff --git a/Jellyfin.Xtream/Configuration/Web/Xtream.css b/Jellyfin.Xtream/Configuration/Web/Xtream.css index e8c8ec6..56e13cf 100644 --- a/Jellyfin.Xtream/Configuration/Web/Xtream.css +++ b/Jellyfin.Xtream/Configuration/Web/Xtream.css @@ -26,3 +26,11 @@ font-size: 1em; vertical-align: middle; } + +.overrides-table { + width: 100%; +} + +.overrides-table thead th:first-child { + width: 0; +} diff --git a/Jellyfin.Xtream/Configuration/Web/Xtream.js b/Jellyfin.Xtream/Configuration/Web/Xtream.js index 95759c7..b0c3bd2 100644 --- a/Jellyfin.Xtream/Configuration/Web/Xtream.js +++ b/Jellyfin.Xtream/Configuration/Web/Xtream.js @@ -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, diff --git a/Jellyfin.Xtream/Configuration/Web/XtreamLiveOverrides.html b/Jellyfin.Xtream/Configuration/Web/XtreamLiveOverrides.html new file mode 100644 index 0000000..c01de8a --- /dev/null +++ b/Jellyfin.Xtream/Configuration/Web/XtreamLiveOverrides.html @@ -0,0 +1,31 @@ +
+
+
+
+
+

TV channel overrides

+
+
+ + + + + + + + + + + +
NumberNameLogo
+
+
+ +
+
+
+
+
\ No newline at end of file diff --git a/Jellyfin.Xtream/Configuration/Web/XtreamLiveOverrides.js b/Jellyfin.Xtream/Configuration/Web/XtreamLiveOverrides.js new file mode 100644 index 0000000..dd9eea6 --- /dev/null +++ b/Jellyfin.Xtream/Configuration/Web/XtreamLiveOverrides.js @@ -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; + }); + }); + })); +} \ No newline at end of file diff --git a/Jellyfin.Xtream/Configuration/Web/XtreamSeries.js b/Jellyfin.Xtream/Configuration/Web/XtreamSeries.js index 2952dab..a3eb72f 100644 --- a/Jellyfin.Xtream/Configuration/Web/XtreamSeries.js +++ b/Jellyfin.Xtream/Configuration/Web/XtreamSeries.js @@ -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"); diff --git a/Jellyfin.Xtream/Configuration/Web/XtreamVod.js b/Jellyfin.Xtream/Configuration/Web/XtreamVod.js index 0ded18e..59fb097 100644 --- a/Jellyfin.Xtream/Configuration/Web/XtreamVod.js +++ b/Jellyfin.Xtream/Configuration/Web/XtreamVod.js @@ -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"); diff --git a/Jellyfin.Xtream/Plugin.cs b/Jellyfin.Xtream/Plugin.cs index 9a12040..aeb3362 100644 --- a/Jellyfin.Xtream/Plugin.cs +++ b/Jellyfin.Xtream/Plugin.cs @@ -94,14 +94,14 @@ namespace Jellyfin.Xtream public TaskService TaskService { get; init; } private static PluginPageInfo CreateStatic(string name) => new PluginPageInfo - { - Name = name, - EmbeddedResourcePath = string.Format( - CultureInfo.InvariantCulture, - "{0}.Configuration.Web.{1}", - typeof(Plugin).Namespace, - name), - }; + { + Name = name, + EmbeddedResourcePath = string.Format( + CultureInfo.InvariantCulture, + "{0}.Configuration.Web.{1}", + typeof(Plugin).Namespace, + name), + }; /// public IEnumerable GetPages() @@ -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"), -- 2.51.0 From d136f34147c0c2b839925866837a97eec2de6535 Mon Sep 17 00:00:00 2001 From: Kevin Jilissen Date: Sun, 9 Oct 2022 13:09:39 +0200 Subject: [PATCH 6/6] Clean up the GetLiveTvChannels method. --- Jellyfin.Xtream/Api/XtreamController.cs | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/Jellyfin.Xtream/Api/XtreamController.cs b/Jellyfin.Xtream/Api/XtreamController.cs index b0f4a80..fdf33c3 100644 --- a/Jellyfin.Xtream/Api/XtreamController.cs +++ b/Jellyfin.Xtream/Api/XtreamController.cs @@ -203,17 +203,13 @@ namespace Jellyfin.Xtream.Api [HttpGet("LiveTv")] public async Task>> GetLiveTvChannels(CancellationToken cancellationToken) { - Plugin plugin = Plugin.Instance; - using (XtreamClient client = new XtreamClient()) + List channels = new List(); + await foreach (StreamInfo stream in Plugin.Instance.StreamService.GetLiveStreams(cancellationToken)) { - List channels = new List(); - await foreach (StreamInfo stream in Plugin.Instance.StreamService.GetLiveStreams(cancellationToken)) - { - channels.Add(CreateChannelResponse(stream)); - } - - return Ok(channels); + channels.Add(CreateChannelResponse(stream)); } + + return Ok(channels); } } } -- 2.51.0