Files
NodecastV2/server/services/m3uXtreamAdapter.js
Vitor Simoes 700f63da90 Add Enigma2/OpenWebif M3U support: program mapping and zap-before-streaming
- Parse #EXTVLCOPT:program=N from M3U playlists (both full and streaming
  parsers, per channel, strict mode) and store it on playlist items
- Transcode multi-program MPEG-TS streams by mapping only the selected
  program's first video and audio stream (DVB teletext/subtitles in the
  program otherwise make FFmpeg fail to produce HLS)
- Optional per-source "zap before streaming" with configurable delay:
  POST /api/openwebif/zap takes sourceId + streamId, requires an enabled,
  zap-enabled M3U source containing that channel, derives the receiver
  from the configured source URL (never from the client), validates the
  Enigma2 service reference, uses a timeout without redirects and does
  not echo the receiver response
- Live TV: Stop button, and stop playback when leaving the page
- Tests for ordinary and program-tagged playlists and the zap endpoint

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 23:01:38 +01:00

136 lines
4.0 KiB
JavaScript

/**
* M3U Xtream Adapter
*
* Makes M3U sources respond to Xtream-style API methods.
* Queries data from SQLite (already synced during source refresh).
*/
const { getDb } = require('../db/sqlite');
class M3uXtreamAdapter {
constructor(sourceId) {
this.sourceId = sourceId;
}
/**
* Get live categories (groups) for this M3U source.
* Returns Xtream-compatible format: [{ category_id, category_name, parent_id }]
*/
getLiveCategories(includeHidden = false) {
const db = getDb();
// M3U stores group name in category_id field of playlist_items
// We need to aggregate unique groups with counts
let query = `
SELECT
category_id,
category_id as category_name,
NULL as parent_id,
COUNT(*) as channel_count
FROM playlist_items
WHERE source_id = ? AND type = 'live'
${!includeHidden ? 'AND is_hidden = 0' : ''}
GROUP BY category_id
ORDER BY category_id ASC
`;
const rows = db.prepare(query).all(this.sourceId);
return rows.map(row => ({
category_id: row.category_id || 'Uncategorized',
category_name: row.category_id || 'Uncategorized',
parent_id: null,
// Bonus: include count for lazy-loading UI
channel_count: row.channel_count
}));
}
/**
* Get live streams (channels), optionally filtered by category.
* Returns Xtream-compatible format: [{ stream_id, name, stream_icon, category_id, ... }]
*/
getLiveStreams(categoryId = null, includeHidden = false) {
const db = getDb();
let query = `
SELECT
item_id as stream_id,
name,
stream_icon,
stream_url,
program,
category_id,
added_at,
data
FROM playlist_items
WHERE source_id = ? AND type = 'live'
${!includeHidden ? 'AND is_hidden = 0' : ''}
`;
const params = [this.sourceId];
if (categoryId) {
query += ` AND category_id = ?`;
params.push(categoryId);
}
query += ` ORDER BY name ASC`;
const rows = db.prepare(query).all(...params);
return rows.map(row => {
// Parse any extra data stored as JSON
let extra = {};
if (row.data) {
try { extra = JSON.parse(row.data); } catch (e) { }
}
return {
stream_id: row.stream_id,
name: row.name,
stream_icon: row.stream_icon,
category_id: row.category_id,
added: row.added_at,
// M3U-specific: direct stream URL (Xtream builds URLs from credentials)
stream_url: row.stream_url,
program: row.program || extra.program || null,
// Include extra fields from parser (tvgId, etc.)
epg_channel_id: extra.tvgId || null,
...extra
};
});
}
/**
* Build stream URL for playback.
* For M3U, we return the stored URL directly (no credential building).
*/
buildStreamUrl(streamId, type = 'live', container = 'ts') {
const db = getDb();
const row = db.prepare(`
SELECT stream_url FROM playlist_items
WHERE source_id = ? AND item_id = ?
`).get(this.sourceId, streamId);
return row?.stream_url || null;
}
/**
* Get XMLTV EPG URL - M3U sources don't have built-in EPG URLs.
* Returns null; EPG must be configured as a separate source.
*/
getXmltvUrl() {
return null;
}
}
/**
* Factory function to create adapter from source ID
*/
function createFromSourceId(sourceId) {
return new M3uXtreamAdapter(sourceId);
}
module.exports = { M3uXtreamAdapter, createFromSourceId };