mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
feat(stalker): full Live TV channel list for complete search, count badges, and all-channels grid (#1209)
Load the complete ITV channel list once per portal (Ministra get_all_channels with a paged crawl fallback) and use it for: full local search across every channel, per-genre category count badges, an all-channels paginated grid on Live TV entry, and eager bulk EPG so row previews appear without playing a channel. Censored (adult) genres absent from get_all_channels fall back to the legacy paged flow and show no badge. Includes mock-server support, unit + E2E coverage, and architecture docs.
This commit is contained in:
1 parent
e53adcbeaf
commit
b3e130aa65
56 files changed
+3044
-63
No files matched your search
@@ -38,6 +38,7 @@ Then in IPTVnator, add a new Stalker portal:
|
||||
| `00:1A:79:00:00:03` | **minimal** | 2 categories, 5 items — edge case testing (empty states, single items) |
|
||||
| `00:1A:79:00:00:04` | **is-series** | 60% of VOD items have `is_series=1` — tests the Ministra lazy-season flow |
|
||||
| `00:1A:79:00:00:05` | **embedded-series** | 50% of VOD items have embedded `series[]` arrays — tests the embedded series flow |
|
||||
| `00:1A:79:00:00:06` | **legacy-pagination** | No `get_all_channels` support — tests the paginated `get_ordered_list` crawl fallback for the full ITV channel list |
|
||||
| `<any other MAC>` | **auto** | MAC bytes used as seed → deterministic unique dataset |
|
||||
|
||||
## Configuration
|
||||
@@ -65,6 +66,7 @@ All endpoints are served at `GET /portal.php?action=<action>&...` matching the r
|
||||
| `get_categories` | Category list filtered by `type` (itv/vod/series) |
|
||||
| `get_genres` | Genre list (mirrors categories) |
|
||||
| `get_ordered_list` | Paginated content list; if `movie_id` is present → returns seasons |
|
||||
| `get_all_channels` | Complete ITV channel list in one response (`type=itv` only); excludes censored (adult) genres; disabled in the `legacy-pagination` scenario |
|
||||
| `create_link` | Returns a real public HLS stream URL for playback |
|
||||
| `favorites` | Add / remove / get favorites (in-memory, resets on restart) |
|
||||
| `get_short_epg` | Current-and-upcoming EPG window for a channel (`ch_id`, `size`) |
|
||||
|
||||
@@ -9,6 +9,8 @@ export interface RawCategory {
|
||||
id: string;
|
||||
title: string;
|
||||
alias: string;
|
||||
/** Ministra adult-genre flag ('1' = censored). */
|
||||
censored?: string;
|
||||
}
|
||||
|
||||
export interface RawChannel {
|
||||
@@ -171,6 +173,16 @@ export function generatePortalData(config: ScenarioConfig): GeneratedPortalData
|
||||
|
||||
// ------ ITV categories + channels ------
|
||||
data.itvCategories = generateCategories('itv', config.categoryCount.itv);
|
||||
// Real Ministra portals mark adult genres as censored and EXCLUDE their
|
||||
// channels from get_all_channels / the "*" listing; the channels are only
|
||||
// served by paging the specific genre. Mirror that with one extra
|
||||
// censored category so clients can exercise the fallback path.
|
||||
data.itvCategories.push({
|
||||
id: '1099',
|
||||
title: 'For adults',
|
||||
alias: 'for_adults',
|
||||
censored: '1',
|
||||
});
|
||||
let channelIndex = 0;
|
||||
for (const cat of data.itvCategories) {
|
||||
const channels = generateChannels(cat.id, config.itemsPerCategory, channelIndex);
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import { Request, Response } from 'express';
|
||||
import { getPortalData } from '../data-store.js';
|
||||
import { getScenario } from '../scenarios.js';
|
||||
import { extractMac } from './get-categories.handler.js';
|
||||
|
||||
/**
|
||||
* Stalker/Ministra get_all_channels — returns the COMPLETE ITV channel list
|
||||
* in a single response (no pagination). This is what STB clients use to build
|
||||
* their local channel list.
|
||||
*
|
||||
* Only type=itv is supported, matching the reference Ministra middleware.
|
||||
* Scenarios with supportsGetAllChannels=false mimic legacy portals and answer
|
||||
* with an error payload so clients fall back to paginated get_ordered_list.
|
||||
*/
|
||||
export function handleGetAllChannels(req: Request, res: Response): void {
|
||||
const mac = extractMac(req);
|
||||
const type = (req.query['type'] as string) ?? 'itv';
|
||||
const scenario = getScenario(mac);
|
||||
|
||||
if (scenario.supportsGetAllChannels === false || type !== 'itv') {
|
||||
res.json({ js: { error: 'Unknown action: get_all_channels' } });
|
||||
return;
|
||||
}
|
||||
|
||||
const data = getPortalData(mac);
|
||||
// Like real Ministra portals: censored (adult) genres are excluded from
|
||||
// the bulk channel list — clients must page those genres explicitly.
|
||||
const censoredIds = new Set(
|
||||
data.itvCategories
|
||||
.filter((cat) => cat.censored === '1')
|
||||
.map((cat) => cat.id)
|
||||
);
|
||||
const allChannels = [...data.channels.entries()]
|
||||
.filter(([categoryId]) => !censoredIds.has(categoryId))
|
||||
.flatMap(([, channels]) => channels);
|
||||
|
||||
res.json({
|
||||
js: {
|
||||
data: allChannels,
|
||||
total_items: allChannels.length,
|
||||
max_page_items: allChannels.length,
|
||||
cur_page: 0,
|
||||
selected_item: 0,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -26,7 +26,7 @@ export function handleGetGenres(req: Request, res: Response): void {
|
||||
id: cat.id,
|
||||
title: cat.title,
|
||||
alias: cat.alias,
|
||||
censored: '0',
|
||||
censored: cat.censored ?? '0',
|
||||
}));
|
||||
|
||||
res.json({ js: genres });
|
||||
|
||||
@@ -34,8 +34,17 @@ export function handleGetOrderedList(req: Request, res: Response): void {
|
||||
|
||||
if (type === 'itv') {
|
||||
if (categoryId === '*') {
|
||||
for (const items of data.channels.values()) {
|
||||
allItems.push(...items);
|
||||
// The "*" listing excludes censored (adult) genres, matching
|
||||
// real portals; their channels are only served by genre id.
|
||||
const censoredIds = new Set(
|
||||
data.itvCategories
|
||||
.filter((cat) => cat.censored === '1')
|
||||
.map((cat) => cat.id)
|
||||
);
|
||||
for (const [catId, items] of data.channels.entries()) {
|
||||
if (!censoredIds.has(catId)) {
|
||||
allItems.push(...items);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
allItems = data.channels.get(categoryId) ?? [];
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Request, Response } from 'express';
|
||||
import { handleHandshake } from '../handlers/handshake.handler.js';
|
||||
import { handleDoAuth } from '../handlers/do-auth.handler.js';
|
||||
import { handleGetAllChannels } from '../handlers/get-all-channels.handler.js';
|
||||
import { handleGetCategories } from '../handlers/get-categories.handler.js';
|
||||
import { handleGetOrderedList } from '../handlers/get-ordered-list.handler.js';
|
||||
import { handleGetSeasons } from '../handlers/get-seasons.handler.js';
|
||||
@@ -39,6 +40,9 @@ export default function dispatchPortalAction(req: Request, res: Response): void
|
||||
handleGetOrderedList(req, res);
|
||||
}
|
||||
break;
|
||||
case 'get_all_channels':
|
||||
handleGetAllChannels(req, res);
|
||||
break;
|
||||
case 'create_link':
|
||||
handleCreateLink(req, res);
|
||||
break;
|
||||
|
||||
@@ -15,6 +15,12 @@ export interface ScenarioConfig {
|
||||
isSeriesFraction: number;
|
||||
/** Fraction of VOD items that have embedded series[] array */
|
||||
embeddedSeriesFraction: number;
|
||||
/**
|
||||
* Whether the portal implements the ITV `get_all_channels` action.
|
||||
* Defaults to true; set false to force clients onto paginated
|
||||
* `get_ordered_list` crawling (legacy portals).
|
||||
*/
|
||||
supportsGetAllChannels?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -88,6 +94,19 @@ export const SCENARIOS: Record<string, ScenarioConfig> = {
|
||||
isSeriesFraction: 0,
|
||||
embeddedSeriesFraction: 0.5, // 50% of VOD items have embedded series[]
|
||||
},
|
||||
'00:1a:79:00:00:06': {
|
||||
name: 'legacy-pagination',
|
||||
description:
|
||||
'Portal without get_all_channels — clients must crawl get_ordered_list pages',
|
||||
seed: 6006,
|
||||
categoryCount: { itv: 6, radio: 4, vod: 4, series: 4 },
|
||||
itemsPerCategory: 40,
|
||||
seasonsPerSeries: 2,
|
||||
episodesPerSeason: 5,
|
||||
isSeriesFraction: 0,
|
||||
embeddedSeriesFraction: 0,
|
||||
supportsGetAllChannels: false,
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
Reference in new issue
Block a user