Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions Jellyfin.Xtream/Api/CustomEpgController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
#pragma warning disable
// Custom XMLTV configuration API for Jellyfin.Xtream.
using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Mime;
using Jellyfin.Xtream.Service;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

namespace Jellyfin.Xtream.Api;

/// <summary>Provides the XMLTV channel list used by the TV Overrides mapping UI.</summary>
[ApiController]
[Route("Xtream/CustomEpg")]
[Produces(MediaTypeNames.Application.Json)]
public sealed class CustomEpgController : ControllerBase
{
/// <summary>Gets channels from the XMLTV file saved in plugin configuration.</summary>
[Authorize(Policy = "RequiresElevation")]
[HttpGet("Channels")]
public ActionResult<IReadOnlyList<XmlTvChannelInfo>> GetChannels()
{
string path = Plugin.Instance.Configuration.CustomEpgPath;
if (string.IsNullOrWhiteSpace(path))
{
return Ok(Array.Empty<XmlTvChannelInfo>());
}

try
{
return Ok(XmlTvGuideService.GetChannels(path));
}
catch (FileNotFoundException exception)
{
return NotFound(new { Message = exception.Message });
}
catch (Exception exception)
{
return StatusCode(500, new { Message = exception.Message });
}
}
}
#pragma warning restore
7 changes: 5 additions & 2 deletions Jellyfin.Xtream/Configuration/ChannelOverrides.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
// 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
Expand All @@ -14,7 +13,6 @@
// 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>
Expand All @@ -34,4 +32,9 @@ public class ChannelOverrides
/// Gets or sets the url of the channel logo.
/// </summary>
public string? LogoUrl { get; set; }

/// <summary>
/// Gets or sets the mapped XMLTV channel id used for custom guide data.
/// </summary>
public string? EpgId { get; set; }
}
18 changes: 11 additions & 7 deletions Jellyfin.Xtream/Configuration/PluginConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
// 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
Expand All @@ -15,7 +14,6 @@

using System.Collections.Generic;
using MediaBrowser.Model.Plugins;

#pragma warning disable CA2227
namespace Jellyfin.Xtream.Configuration;

Expand All @@ -33,7 +31,6 @@ public class PluginConfiguration : BasePluginConfiguration
/// Gets or sets the username.
/// </summary>
public string Username { get; set; } = string.Empty;

/// <summary>
/// Gets or sets the password.
/// </summary>
Expand All @@ -44,11 +41,21 @@ public class PluginConfiguration : BasePluginConfiguration
/// </summary>
public string UserAgent { get; set; } = string.Empty;

/// <summary>
/// Gets or sets the guide-data mode. Supported values are Xtream, CustomXmlTv,
/// and CustomXmlTvWithXtreamFallback.
/// </summary>
public string EpgMode { get; set; } = "Xtream";

/// <summary>
/// Gets or sets the local XMLTV file used by custom guide modes.
/// </summary>
public string CustomEpgPath { get; set; } = string.Empty;

/// <summary>
/// Gets or sets a value indicating whether the Catch-up channel is visible.
/// </summary>
public bool IsCatchupVisible { get; set; }

/// <summary>
/// Gets or sets a value indicating whether the Series channel is visible.
/// </summary>
Expand All @@ -58,7 +65,6 @@ public class PluginConfiguration : BasePluginConfiguration
/// Gets or sets a value indicating whether the Video On-demand channel is visible.
/// </summary>
public bool IsVodVisible { get; set; }

/// <summary>
/// Gets or sets a value indicating whether the Video On-demand channel is visible.
/// </summary>
Expand All @@ -68,7 +74,6 @@ public class PluginConfiguration : BasePluginConfiguration
/// Gets or sets the channels displayed in Live TV.
/// </summary>
public SerializableDictionary<int, HashSet<int>> LiveTv { get; set; } = [];

/// <summary>
/// Gets or sets the streams displayed in VOD.
/// </summary>
Expand All @@ -78,7 +83,6 @@ public class PluginConfiguration : BasePluginConfiguration
/// 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>
Expand Down
33 changes: 28 additions & 5 deletions Jellyfin.Xtream/Configuration/Web/XtreamLiveOverrides.html
Original file line number Diff line number Diff line change
@@ -1,23 +1,46 @@
<div id="XtreamLivePage" data-role="page" class="page type-interior pluginConfigurationPage withTabs"
data-require="emby-input,emby-button" data-controller="__plugin/XtreamLiveOverrides.js">
data-require="emby-input,emby-button,emby-select" 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">Custom EPG</h2>
</div>

<div class="selectContainer">
<label class="selectLabel" for="EpgMode">Guide data source</label>
<select is="emby-select" id="EpgMode">
<option value="Xtream">Xtream provider EPG</option>
<option value="CustomXmlTv">Custom XMLTV only</option>
<option value="CustomXmlTvWithXtreamFallback">Custom XMLTV with Xtream fallback</option>
</select>
<div class="fieldDescription">This changes guide data only. Live TV streams continue to use Xtream exactly as before.</div>
</div>

<div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="CustomEpgPath">XMLTV file path</label>
<input is="emby-input" type="text" id="CustomEpgPath" placeholder="D:\Jellyfin Scripts\EPG\epg123.xmltv" />
<div class="fieldDescription">Enter a local path that the Jellyfin server process can read. Save once after changing this path, then reopen TV Overrides so the channel-id suggestions can be loaded.</div>
<div class="fieldDescription" id="CustomEpgStatus"></div>
</div>

<datalist id="XmlTvChannelOptions"></datalist>

<div class="sectionTitleContainer flex align-items-center">
<h2 class="sectionTitle">TV channel overrides</h2>
</div>
<div class="fieldDescription">For Custom XMLTV, map each Xtream channel to the XMLTV <code>&lt;channel id="..."&gt;</code>. The mapping affects guide data only.</div>
<div class="inputContainer">
<table class="overrides-table">
<thead>
<tr>
<th>Number</th>
<th>Name</th>
<th>Logo</th>
<th>Custom EPG ID</th>
</tr>
</thead>
<tbody id="LiveChannels">

</tbody>
<tbody id="LiveChannels"></tbody>
</table>
</div>
<div>
Expand All @@ -28,4 +51,4 @@ <h2 class="sectionTitle">TV channel overrides</h2>
</form>
</div>
</div>
</div>
</div>
87 changes: 67 additions & 20 deletions Jellyfin.Xtream/Configuration/Web/XtreamLiveOverrides.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,48 +39,95 @@ export default function (view) {
td.appendChild(image);
tr.appendChild(td);

td = document.createElement('td');
const epg = document.createElement('input');
epg.type = 'text';
epg.setAttribute('is', 'emby-input');
epg.setAttribute('list', 'XmlTvChannelOptions');
epg.placeholder = 'XMLTV channel id';
epg.value = overrides.EpgId ?? '';
epg.onchange = () => epg.value.trim() ?
overrides.EpgId = epg.value.trim() :
delete overrides.EpgId;
td.appendChild(epg);
tr.appendChild(td);

return tr;
};

view.addEventListener("viewshow", () => import(
ApiClient.getUrl("web/ConfigurationPage", {
name: "Xtream.js",
})
const populateEpgOptions = (channels) => {
const list = view.querySelector('#XmlTvChannelOptions');
list.innerHTML = '';
for (const channel of channels) {
const option = document.createElement('option');
option.value = channel.Id;
option.label = channel.Name;
list.appendChild(option);
}
};

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');
table.innerHTML = '';
const status = view.querySelector('#CustomEpgStatus');
status.textContent = '';

Dashboard.showLoadingMsg();
Promise.all([
getConfig.then((config) => config.LiveTvOverrides),
ApiClient.getPluginConfiguration(pluginId),
Xtream.fetchJson('Xtream/LiveTv'),
]).then(([data, channels]) => {
]).then(([config, channels]) => {
const data = config.LiveTvOverrides ?? {};
view.querySelector('#EpgMode').value = config.EpgMode ?? 'Xtream';
view.querySelector('#CustomEpgPath').value = config.CustomEpgPath ?? '';

for (const channel of channels) {
data[channel.Id] ??= {};
const row = createChannelRow(channel, data[channel.Id]);
table.appendChild(row);
table.appendChild(createChannelRow(channel, data[channel.Id]));
}
Dashboard.hideLoadingMsg();

view.querySelector('#XtreamLiveOverridesForm').addEventListener('submit', (e) => {
Dashboard.showLoadingMsg();
const epgPromise = config.CustomEpgPath ?
Xtream.fetchJson('Xtream/CustomEpg/Channels') :
Promise.resolve([]);

return epgPromise.then((epgChannels) => {
populateEpgOptions(epgChannels);
status.textContent = config.CustomEpgPath ?
`Loaded ${epgChannels.length} XMLTV channels.` :
'Set an XMLTV path and save to load channel-id suggestions.';
return { config, data };
}).catch((error) => {
console.error('Unable to load XMLTV channels', error);
status.textContent = 'The XMLTV channel list could not be loaded. Check the saved path and Jellyfin server permissions.';
return { config, data };
});
}).then(({ data }) => {
Dashboard.hideLoadingMsg();
view.querySelector('#XtreamLiveOverridesForm').onsubmit = (e) => {
e.preventDefault();
Dashboard.showLoadingMsg();
ApiClient.getPluginConfiguration(pluginId).then((config) => {
config.EpgMode = view.querySelector('#EpgMode').value;
config.CustomEpgPath = view.querySelector('#CustomEpgPath').value.trim();
config.LiveTvOverrides = Xtream.filter(
data,
overrides => Object.keys(overrides).length > 0
);
ApiClient.updatePluginConfiguration(pluginId, config).then((result) => {
Dashboard.processPluginConfigurationUpdateResult(result);
});
return ApiClient.updatePluginConfiguration(pluginId, config);
}).then((result) => {
Dashboard.processPluginConfigurationUpdateResult(result);
});

e.preventDefault();
return false;
});
};
}).catch((error) => {
console.error(error);
Dashboard.hideLoadingMsg();
status.textContent = 'Unable to load the TV Overrides page.';
});
}));
}
}
Loading