feat: group variants of the same IPTV channel into a logical channel (#9)
Add a channelVariantGrouping service that normalizes channel names (strips region prefixes and quality tokens) to fingerprint duplicate stream entries and cluster them under a single logical channel with quality/source variants. Wire it into the Live TV channel list and player variant selection, reducing duplicates without destructively merging streams. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -23,6 +23,9 @@ class ChannelList {
|
||||
this.sources = [];
|
||||
this.isLoading = false;
|
||||
this.renderedChannels = [];
|
||||
this.logicalChannels = [];
|
||||
this.selectedVariants = new Map();
|
||||
this.expandedVariants = new Set();
|
||||
|
||||
this.loadCollapsedState();
|
||||
this.init();
|
||||
@ -216,14 +219,7 @@ class ChannelList {
|
||||
// Find all visible channel items and update their program info
|
||||
const channelItems = this.container.querySelectorAll('.channel-item');
|
||||
channelItems.forEach(item => {
|
||||
const channelId = item.dataset.channelId;
|
||||
const sourceId = item.dataset.sourceId;
|
||||
|
||||
// Find the channel data
|
||||
const channel = this.channels.find(c =>
|
||||
String(c.id) === String(channelId) &&
|
||||
String(c.sourceId) === String(sourceId)
|
||||
);
|
||||
const channel = this.logicalChannels.find(c => c.fingerprint === item.dataset.logicalId);
|
||||
|
||||
if (channel) {
|
||||
const programInfo = this.getProgramInfo(channel);
|
||||
@ -246,7 +242,8 @@ class ChannelList {
|
||||
|
||||
// Cache key: channel_id + current_minute (invalidate every minute)
|
||||
const currentMinute = Math.floor(Date.now() / 60000);
|
||||
const cacheKey = `${channel.tvgId || channel.name}:${currentMinute}`;
|
||||
const variant = channel.preferredVariant || channel;
|
||||
const cacheKey = `${variant.tvgId || variant.name}:${currentMinute}`;
|
||||
|
||||
if (this._programInfoCache && this._programInfoCache.has(cacheKey)) {
|
||||
return this._programInfoCache.get(cacheKey);
|
||||
@ -258,7 +255,13 @@ class ChannelList {
|
||||
this._lastCacheMinute = currentMinute;
|
||||
}
|
||||
|
||||
const program = window.app.epgGuide.getCurrentProgram(channel.tvgId, channel.name);
|
||||
let program = window.app.epgGuide.getCurrentProgram(variant.tvgId, variant.name);
|
||||
if (!program && channel.variants) {
|
||||
for (const fallback of channel.variants) {
|
||||
program = window.app.epgGuide.getCurrentProgram(fallback.tvgId, fallback.name);
|
||||
if (program) break;
|
||||
}
|
||||
}
|
||||
const result = program ? program.title : null;
|
||||
|
||||
this._programInfoCache.set(cacheKey, result);
|
||||
@ -296,6 +299,13 @@ class ChannelList {
|
||||
const searchTerm = this.searchInput.value.toLowerCase();
|
||||
const showHidden = this.showHiddenCheckbox ? this.showHiddenCheckbox.checked : false;
|
||||
|
||||
this.logicalChannels = ChannelVariantGrouping.groupChannels(this.channels, {
|
||||
showHidden,
|
||||
selectedVariants: this.selectedVariants,
|
||||
isFavorite: channel => this.isFavorite(channel.sourceId, channel.id),
|
||||
isHidden: channel => this.isHidden('channel', channel.sourceId, channel.streamId || channel.id)
|
||||
});
|
||||
|
||||
// Reset batching
|
||||
this.currentBatch = 0;
|
||||
this.batchSize = 100; // Number of groups to render per batch (increased to handle many hidden groups)
|
||||
@ -305,11 +315,12 @@ class ChannelList {
|
||||
const groupedChannels = {};
|
||||
|
||||
// 1. Filter
|
||||
this.filteredChannels = this.channels;
|
||||
this.filteredChannels = this.logicalChannels;
|
||||
if (searchTerm) {
|
||||
this.filteredChannels = this.channels.filter(ch =>
|
||||
String(ch.name ?? "").toLowerCase().includes(searchTerm) ||
|
||||
String(ch.groupTitle ?? "").toLowerCase().includes(searchTerm)
|
||||
this.filteredChannels = this.logicalChannels.filter(ch =>
|
||||
String(ch.displayName ?? '').toLowerCase().includes(searchTerm) ||
|
||||
ch.variants.some(variant => String(variant.name ?? '').toLowerCase().includes(searchTerm)) ||
|
||||
ch.variants.some(variant => String(variant.groupTitle ?? '').toLowerCase().includes(searchTerm))
|
||||
);
|
||||
}
|
||||
|
||||
@ -325,7 +336,7 @@ class ChannelList {
|
||||
});
|
||||
|
||||
// 3. Add Favorites
|
||||
const favoritedChannels = this.channels.filter(ch => this.isFavorite(ch.sourceId, ch.id));
|
||||
const favoritedChannels = this.logicalChannels.filter(ch => ch.isFavorite);
|
||||
if (favoritedChannels.length > 0) {
|
||||
favoritedChannels.sort((a, b) => a.name.localeCompare(b.name));
|
||||
groupedChannels['Favorites'] = favoritedChannels;
|
||||
@ -344,9 +355,7 @@ class ChannelList {
|
||||
const channels = groupedChannels[groupName];
|
||||
// Check if any channel in this group is visible
|
||||
return channels.some(channel => {
|
||||
const rawChannelId = channel.streamId || channel.id;
|
||||
const isHidden = this.isHidden('channel', channel.sourceId, rawChannelId);
|
||||
return !isHidden || showHidden;
|
||||
return !channel.isHidden || showHidden;
|
||||
});
|
||||
});
|
||||
|
||||
@ -373,9 +382,7 @@ class ChannelList {
|
||||
|
||||
const visibleChannels = channels.filter(channel => {
|
||||
if (isFavoritesGroup) return true;
|
||||
const rawChannelId = channel.streamId || channel.id;
|
||||
const channelHidden = this.isHidden('channel', channel.sourceId, rawChannelId);
|
||||
return !channelHidden || this.showHidden;
|
||||
return !channel.isHidden || this.showHidden;
|
||||
});
|
||||
|
||||
// Assign unique render IDs for linear navigation
|
||||
@ -454,9 +461,7 @@ class ChannelList {
|
||||
// Pre-filter visible channels for this group
|
||||
const visibleChannels = channels.filter(channel => {
|
||||
if (isFavoritesGroup) return true;
|
||||
const rawChannelId = channel.streamId || channel.id;
|
||||
const channelHidden = this.isHidden('channel', channel.sourceId, rawChannelId);
|
||||
return !channelHidden || this.showHidden;
|
||||
return !channel.isHidden || this.showHidden;
|
||||
});
|
||||
|
||||
// Skip group if no visible channels (derived visibility)
|
||||
@ -487,39 +492,14 @@ class ChannelList {
|
||||
|
||||
|
||||
for (const channel of visibleChannels) {
|
||||
// Check hidden again for styling (showHidden mode)
|
||||
const rawChannelId = channel.streamId || channel.id;
|
||||
const channelHidden = !isFavoritesGroup && this.isHidden('channel', channel.sourceId, rawChannelId);
|
||||
|
||||
const isActive = this.currentChannel?.id === channel.id;
|
||||
// Check if this specific instance is the "active" one for navigation purposes
|
||||
const isRenderActive = this.currentRenderId && this.renderedChannels[renderIndex]?._renderId === this.currentRenderId;
|
||||
|
||||
const isFavorite = this.isFavorite(channel.sourceId, channel.id);
|
||||
const renderId = this.renderedChannels[renderIndex]?._renderId || '';
|
||||
const renderGroup = this.renderedChannels[renderIndex]?._renderGroup || groupName;
|
||||
renderIndex++;
|
||||
|
||||
html += `
|
||||
<div class="channel-item ${isActive ? 'active' : ''} ${isRenderActive ? 'nav-active' : ''} ${channelHidden ? 'hidden' : ''}"
|
||||
data-channel-id="${channel.id}"
|
||||
data-source-id="${channel.sourceId}"
|
||||
data-source-type="${channel.sourceType}"
|
||||
data-stream-id="${channel.streamId || ''}"
|
||||
data-url="${channel.url || ''}"
|
||||
data-render-id="${renderId}"
|
||||
data-render-group="${renderGroup}">
|
||||
<img class="channel-logo" src="${this.getProxiedImageUrl(channel.tvgLogo)}"
|
||||
alt="" onerror="this.onerror=null;this.src='/img/placeholder.png'">
|
||||
<div class="channel-info">
|
||||
<div class="channel-name">${this.escapeHtml(channel.name)}</div>
|
||||
<div class="channel-program">${this.escapeHtml(this.getProgramInfo(channel) || '')}</div>
|
||||
</div>
|
||||
<button class="favorite-btn ${isFavorite ? 'active' : ''}" title="${isFavorite ? 'Remove from Favorites' : 'Add to Favorites'}">
|
||||
${isFavorite ? Icons.favorite : Icons.favoriteOutline}
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
html += this.renderLogicalChannel(channel, renderId, renderGroup, isActive, isRenderActive);
|
||||
}
|
||||
html += '</div></div>';
|
||||
}
|
||||
@ -565,9 +545,13 @@ class ChannelList {
|
||||
header.addEventListener('contextmenu', (e) => this.showContextMenu(e, 'group', header.dataset));
|
||||
}
|
||||
|
||||
this.attachChannelListeners(groupEl);
|
||||
}
|
||||
|
||||
attachChannelListeners(groupEl) {
|
||||
groupEl.querySelectorAll('.channel-item').forEach(item => {
|
||||
item.addEventListener('click', (e) => {
|
||||
if (e.target.closest('.favorite-btn')) return;
|
||||
if (e.target.closest('.favorite-btn') || e.target.closest('.variant-toggle')) return;
|
||||
this.selectChannel(item.dataset);
|
||||
});
|
||||
item.addEventListener('contextmenu', (e) => this.showContextMenu(e, 'channel', item.dataset));
|
||||
@ -576,10 +560,51 @@ class ChannelList {
|
||||
if (favBtn) {
|
||||
favBtn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
this.toggleFavorite(parseInt(item.dataset.sourceId), item.dataset.channelId);
|
||||
this.toggleFavorite(parseInt(item.dataset.sourceId), item.dataset.variantId);
|
||||
});
|
||||
}
|
||||
});
|
||||
groupEl.querySelectorAll('.variant-toggle').forEach(button => button.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
const fingerprint = button.closest('.channel-item').dataset.logicalId;
|
||||
this.expandedVariants.has(fingerprint) ? this.expandedVariants.delete(fingerprint) : this.expandedVariants.add(fingerprint);
|
||||
this.render();
|
||||
}));
|
||||
groupEl.querySelectorAll('.channel-variant').forEach(item => item.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
this.selectChannel(item.dataset);
|
||||
}));
|
||||
}
|
||||
|
||||
renderLogicalChannel(channel, renderId, renderGroup, isActive = false, isRenderActive = false) {
|
||||
const selected = channel.preferredVariant;
|
||||
const selectedIsFavorite = this.isFavorite(selected.sourceId, selected.id);
|
||||
const expanded = this.expandedVariants.has(channel.fingerprint);
|
||||
const variants = channel.variants.map(variant => {
|
||||
const hidden = this.isHidden('channel', variant.sourceId, variant.streamId || variant.id);
|
||||
if (hidden && !this.showHidden) return '';
|
||||
const source = this.sources.find(item => String(item.id) === String(variant.sourceId));
|
||||
return `<button class="channel-variant ${hidden ? 'is-hidden' : ''}" type="button"
|
||||
data-logical-id="${this.escapeHtml(channel.fingerprint)}" data-variant-id="${this.escapeHtml(String(variant.id))}"
|
||||
data-source-id="${variant.sourceId}" data-render-id="${renderId}" data-render-group="${this.escapeHtml(renderGroup)}">
|
||||
<span class="variant-quality">${ChannelVariantGrouping.getQuality(variant)}</span>
|
||||
<span class="variant-source">${this.escapeHtml(source?.name || variant.sourceType)}</span>
|
||||
<span class="variant-name">${this.escapeHtml(variant.name)}</span>
|
||||
</button>`;
|
||||
}).join('');
|
||||
return `<div class="logical-channel ${expanded ? 'expanded' : ''}">
|
||||
<div class="channel-item ${isActive ? 'active' : ''} ${isRenderActive ? 'nav-active' : ''} ${channel.isHidden ? 'hidden' : ''}"
|
||||
data-logical-id="${this.escapeHtml(channel.fingerprint)}" data-variant-id="${this.escapeHtml(String(selected.id))}"
|
||||
data-source-id="${selected.sourceId}" data-stream-id="${selected.streamId || ''}"
|
||||
data-render-id="${renderId}" data-render-group="${this.escapeHtml(renderGroup)}">
|
||||
<img class="channel-logo" src="${this.getProxiedImageUrl(channel.tvgLogo)}" alt="" onerror="this.onerror=null;this.src='/img/placeholder.png'">
|
||||
<div class="channel-info"><div class="channel-name">${this.escapeHtml(channel.displayName)}</div>
|
||||
<div class="channel-program">${this.escapeHtml(this.getProgramInfo(channel) || '')}</div></div>
|
||||
${channel.variants.length > 1 ? `<button class="variant-toggle" type="button" aria-expanded="${expanded}" title="Show streams"><span>${channel.variants.length} streams</span><span class="variant-chevron">⌄</span></button>` : ''}
|
||||
<button class="favorite-btn ${channel.isFavorite ? 'active' : ''}" title="${selectedIsFavorite ? 'Remove selected stream from Favorites' : 'Add selected stream to Favorites'}">${channel.isFavorite ? Icons.favorite : Icons.favoriteOutline}</button>
|
||||
</div>
|
||||
${channel.variants.length > 1 ? `<div class="channel-variants" ${expanded ? '' : 'hidden'}>${variants}</div>` : ''}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -594,17 +619,12 @@ class ChannelList {
|
||||
// Filter visible channels
|
||||
const visibleChannels = channels.filter(channel => {
|
||||
if (isFavoritesGroup) return true;
|
||||
const rawChannelId = channel.streamId || channel.id;
|
||||
const channelHidden = this.isHidden('channel', channel.sourceId, rawChannelId);
|
||||
return !channelHidden || this.showHidden;
|
||||
return !channel.isHidden || this.showHidden;
|
||||
});
|
||||
|
||||
let html = '';
|
||||
for (const channel of visibleChannels) {
|
||||
const rawChannelId = channel.streamId || channel.id;
|
||||
const channelHidden = !isFavoritesGroup && this.isHidden('channel', channel.sourceId, rawChannelId);
|
||||
const isActive = this.currentChannel?.id === channel.id;
|
||||
const isFavorite = this.isFavorite(channel.sourceId, channel.id);
|
||||
|
||||
// Find the matching rendered channel to get its unique IDs
|
||||
const renderedChannel = this.renderedChannels.find(rc =>
|
||||
@ -613,46 +633,12 @@ class ChannelList {
|
||||
const renderId = renderedChannel?._renderId || '';
|
||||
const renderGroup = renderedChannel?._renderGroup || groupName;
|
||||
|
||||
html += `
|
||||
<div class="channel-item ${isActive ? 'active' : ''} ${channelHidden ? 'hidden' : ''}"
|
||||
data-channel-id="${channel.id}"
|
||||
data-source-id="${channel.sourceId}"
|
||||
data-source-type="${channel.sourceType}"
|
||||
data-stream-id="${channel.streamId || ''}"
|
||||
data-url="${channel.url || ''}"
|
||||
data-render-id="${renderId}"
|
||||
data-render-group="${renderGroup}">
|
||||
<img class="channel-logo" src="${this.getProxiedImageUrl(channel.tvgLogo)}"
|
||||
alt="" onerror="this.onerror=null;this.src='/img/placeholder.png'">
|
||||
<div class="channel-info">
|
||||
<div class="channel-name">${this.escapeHtml(channel.name)}</div>
|
||||
<div class="channel-program">${this.escapeHtml(this.getProgramInfo(channel) || '')}</div>
|
||||
</div>
|
||||
<button class="favorite-btn ${isFavorite ? 'active' : ''}" title="${isFavorite ? 'Remove from Favorites' : 'Add to Favorites'}">
|
||||
${isFavorite ? Icons.favorite : Icons.favoriteOutline}
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
html += this.renderLogicalChannel(channel, renderId, renderGroup, isActive);
|
||||
}
|
||||
|
||||
container.innerHTML = html;
|
||||
|
||||
// Attach listeners to the new channel items
|
||||
container.querySelectorAll('.channel-item').forEach(item => {
|
||||
item.addEventListener('click', (e) => {
|
||||
if (e.target.closest('.favorite-btn')) return;
|
||||
this.selectChannel(item.dataset);
|
||||
});
|
||||
item.addEventListener('contextmenu', (e) => this.showContextMenu(e, 'channel', item.dataset));
|
||||
|
||||
const favBtn = item.querySelector('.favorite-btn');
|
||||
if (favBtn) {
|
||||
favBtn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
this.toggleFavorite(parseInt(item.dataset.sourceId), item.dataset.channelId);
|
||||
});
|
||||
}
|
||||
});
|
||||
this.attachChannelListeners(container);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -927,6 +913,7 @@ class ChannelList {
|
||||
if (channel) {
|
||||
this.updateFavoritesGroup(channel, !wasFavorite);
|
||||
}
|
||||
this.render();
|
||||
// Do NOT call this.render() - it causes lag
|
||||
|
||||
// Perform API call
|
||||
@ -962,6 +949,7 @@ class ChannelList {
|
||||
const channel = this.channels.find(c => c.sourceId == sourceId && c.id == channelId);
|
||||
if (channel) this.updateFavoritesGroup(channel, false);
|
||||
}
|
||||
this.render();
|
||||
}
|
||||
}
|
||||
|
||||
@ -1074,10 +1062,21 @@ class ChannelList {
|
||||
* Select and play a channel
|
||||
*/
|
||||
async selectChannel(dataset) {
|
||||
const channel = this.channels.find(c => c.id === dataset.channelId);
|
||||
if (!channel) return;
|
||||
const logicalChannel = this.logicalChannels.find(c => c.fingerprint === dataset.logicalId) ||
|
||||
this.renderedChannels.find(c => c.fingerprint === dataset.logicalId) ||
|
||||
this.logicalChannels.find(c => c.variants.some(variant =>
|
||||
String(variant.id) === String(dataset.channelId) &&
|
||||
(!dataset.sourceId || String(variant.sourceId) === String(dataset.sourceId))
|
||||
));
|
||||
if (!logicalChannel) return;
|
||||
const channel = logicalChannel.variants.find(variant =>
|
||||
String(variant.id) === String(dataset.variantId || dataset.channelId) &&
|
||||
(!dataset.sourceId || String(variant.sourceId) === String(dataset.sourceId))
|
||||
) || logicalChannel.preferredVariant;
|
||||
this.selectedVariants.set(logicalChannel.fingerprint, ChannelVariantGrouping.getVariantKey(channel));
|
||||
logicalChannel.preferredVariant = channel;
|
||||
|
||||
this.currentChannel = channel;
|
||||
this.currentChannel = logicalChannel;
|
||||
this.currentRenderId = dataset.renderId; // Track which visual instance is active
|
||||
this.currentRenderGroup = dataset.renderGroup; // Track which group the selection came from
|
||||
|
||||
@ -1106,7 +1105,8 @@ class ChannelList {
|
||||
|
||||
// Fallback checks if still not found
|
||||
if (!activeItem) {
|
||||
activeItem = this.container.querySelector(`[data-channel-id="${channel.id}"]`);
|
||||
activeItem = Array.from(this.container.querySelectorAll('.channel-item'))
|
||||
.find(item => item.dataset.logicalId === logicalChannel.fingerprint);
|
||||
// If we fell back to channel ID, update currentRenderId to match what we found
|
||||
if (activeItem && activeItem.dataset.renderId) {
|
||||
this.currentRenderId = activeItem.dataset.renderId;
|
||||
@ -1177,10 +1177,17 @@ class ChannelList {
|
||||
*/
|
||||
showContextMenu(e, type, data) {
|
||||
e.preventDefault();
|
||||
const logicalChannel = type === 'channel'
|
||||
? this.logicalChannels.find(channel => channel.fingerprint === data.logicalId)
|
||||
: null;
|
||||
const variant = logicalChannel?.variants.find(item =>
|
||||
String(item.id) === String(data.variantId) && String(item.sourceId) === String(data.sourceId)
|
||||
) || logicalChannel?.preferredVariant;
|
||||
this.contextMenu.dataset.type = type;
|
||||
this.contextMenu.dataset.sourceId = data.sourceId;
|
||||
this.contextMenu.dataset.itemId = type === 'group' ? data.group : data.channelId;
|
||||
this.contextMenu.dataset.streamId = data.streamId || '';
|
||||
this.contextMenu.dataset.sourceId = variant?.sourceId ?? data.sourceId;
|
||||
this.contextMenu.dataset.itemId = type === 'group' ? data.group : (variant?.id || data.variantId);
|
||||
this.contextMenu.dataset.streamId = variant?.streamId || data.streamId || '';
|
||||
this.contextMenu.dataset.logicalId = data.logicalId || '';
|
||||
|
||||
this.contextMenu.style.left = `${e.clientX}px`;
|
||||
this.contextMenu.style.top = `${e.clientY}px`;
|
||||
@ -1199,15 +1206,12 @@ class ChannelList {
|
||||
*/
|
||||
async handleContextAction(e) {
|
||||
const action = e.target.dataset.action;
|
||||
const { type, sourceId, itemId, streamId } = this.contextMenu.dataset;
|
||||
const { type, sourceId, itemId, streamId, logicalId } = this.contextMenu.dataset;
|
||||
|
||||
switch (action) {
|
||||
case 'play':
|
||||
if (type === 'channel') {
|
||||
const channel = this.channels.find(c => c.id === itemId);
|
||||
if (channel) {
|
||||
await this.selectChannel({ channelId: channel.id });
|
||||
}
|
||||
await this.selectChannel({ logicalId, variantId: itemId, sourceId });
|
||||
}
|
||||
break;
|
||||
case 'hide':
|
||||
@ -1345,6 +1349,7 @@ class ChannelList {
|
||||
if (channel) {
|
||||
this.updateFavoritesGroup(channel, isFavorite);
|
||||
}
|
||||
this.render();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1382,11 +1387,9 @@ class ChannelList {
|
||||
const nextChannel = this.renderedChannels[nextIndex];
|
||||
|
||||
this.selectChannel({
|
||||
channelId: nextChannel.id,
|
||||
sourceId: nextChannel.sourceId,
|
||||
sourceType: nextChannel.sourceType,
|
||||
streamId: nextChannel.streamId,
|
||||
url: nextChannel.url,
|
||||
logicalId: nextChannel.fingerprint,
|
||||
variantId: nextChannel.preferredVariant.id,
|
||||
sourceId: nextChannel.preferredVariant.sourceId,
|
||||
renderId: nextChannel._renderId // Pass the unique render ID
|
||||
});
|
||||
}
|
||||
@ -1425,11 +1428,9 @@ class ChannelList {
|
||||
const prevChannel = this.renderedChannels[prevIndex];
|
||||
|
||||
this.selectChannel({
|
||||
channelId: prevChannel.id,
|
||||
sourceId: prevChannel.sourceId,
|
||||
sourceType: prevChannel.sourceType,
|
||||
streamId: prevChannel.streamId,
|
||||
url: prevChannel.url,
|
||||
logicalId: prevChannel.fingerprint,
|
||||
variantId: prevChannel.preferredVariant.id,
|
||||
sourceId: prevChannel.preferredVariant.sourceId,
|
||||
renderId: prevChannel._renderId
|
||||
});
|
||||
}
|
||||
@ -1449,13 +1450,7 @@ class ChannelList {
|
||||
* Get list of visible (non-hidden) channels in display order
|
||||
*/
|
||||
getVisibleChannels() {
|
||||
const showHidden = this.showHiddenCheckbox?.checked ?? false;
|
||||
return this.channels.filter(ch => {
|
||||
if (showHidden) return true;
|
||||
const channelHidden = this.isHidden('channel', ch.sourceId, ch.id);
|
||||
const groupHidden = this.isHidden('group', ch.sourceId, ch.groupTitle);
|
||||
return !channelHidden && !groupHidden;
|
||||
});
|
||||
return this.logicalChannels;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1819,11 +1819,18 @@ class VideoPlayer {
|
||||
if (channels.length === 0) return;
|
||||
|
||||
const currentIdx = this.currentChannel
|
||||
? channels.findIndex(c => c.id === this.currentChannel.id)
|
||||
? channels.findIndex(c => c.variants.some(variant =>
|
||||
variant.id === this.currentChannel.id && variant.sourceId === this.currentChannel.sourceId
|
||||
))
|
||||
: -1;
|
||||
|
||||
const prevIdx = currentIdx <= 0 ? channels.length - 1 : currentIdx - 1;
|
||||
window.app.channelList.selectChannel({ channelId: channels[prevIdx].id });
|
||||
const previous = channels[prevIdx];
|
||||
window.app.channelList.selectChannel({
|
||||
logicalId: previous.fingerprint,
|
||||
variantId: previous.preferredVariant.id,
|
||||
sourceId: previous.preferredVariant.sourceId
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1835,11 +1842,18 @@ class VideoPlayer {
|
||||
if (channels.length === 0) return;
|
||||
|
||||
const currentIdx = this.currentChannel
|
||||
? channels.findIndex(c => c.id === this.currentChannel.id)
|
||||
? channels.findIndex(c => c.variants.some(variant =>
|
||||
variant.id === this.currentChannel.id && variant.sourceId === this.currentChannel.sourceId
|
||||
))
|
||||
: -1;
|
||||
|
||||
const nextIdx = currentIdx >= channels.length - 1 ? 0 : currentIdx + 1;
|
||||
window.app.channelList.selectChannel({ channelId: channels[nextIdx].id });
|
||||
const next = channels[nextIdx];
|
||||
window.app.channelList.selectChannel({
|
||||
logicalId: next.fingerprint,
|
||||
variantId: next.preferredVariant.id,
|
||||
sourceId: next.preferredVariant.sourceId
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user