diff --git a/src/xmltv.js b/src/xmltv.js
index 4bb4110..77116d7 100644
--- a/src/xmltv.js
+++ b/src/xmltv.js
@@ -1,5 +1,6 @@
const XMLWriter = require('xml-writer');
const fs = require('fs');
+const constants = require('./constants');
module.exports = { WriteXMLTV, shutdown };
@@ -10,8 +11,6 @@ let isWorking = false;
// CONFIG
// ────────────────────────────────────────────────────────────────────────────────
// Anything shorter than this is considered a bump / filler for merge purposes.
-const FILLER_MAX_MS = 1 * 60 * 1000; // 1 minute – tweak in UI later if desired
-const FILLER_MS = 90 * 1000; // ignore clips < 1½ min
const CONTIG_MS = 2 * 1000; // consider ≤2‑second gaps continuous
// ────────────────────────────────────────────────────────────────────────────────
@@ -45,12 +44,42 @@ function writePromise(json, xmlSettings, throttle, cacheImageService) {
_writeDocStart(xw);
(async () => {
+ // Debug logging to see what's happening
+ console.log('XMLTV Debug: Writing XMLTV file');
+ console.log('XMLTV Debug: Channel numbers:', Object.keys(json));
+
const channelNumbers = Object.keys(json);
- const channels = channelNumbers.map(n => json[n].channel);
+ const channels = channelNumbers.map(n => json[n].channel);
_writeChannels(xw, channels);
for (const number of channelNumbers) {
+ console.log(`XMLTV Debug: Processing channel ${number}`);
+ console.log(`XMLTV Debug: Programs before merge: ${json[number].programs.length}`);
+
+ // Check if the programs array exists and has items
+ if (!Array.isArray(json[number].programs) || json[number].programs.length === 0) {
+ console.error(`XMLTV Debug: ERROR - No programs array or empty array for channel ${number}`);
+ continue;
+ }
+
+ // Check the first few programs
+ console.log('XMLTV Debug: Sample programs before merge:');
+ json[number].programs.slice(0, 3).forEach((prog, idx) => {
+ console.log(` ${idx+1}. ${prog.title || 'No title'}, start: ${prog.start || 'No start'}, stop: ${prog.stop || 'No stop'}`);
+ });
+
const merged = _smartMerge(json[number].programs);
+ console.log(`XMLTV Debug: Programs after merge: ${merged.length}`);
+
+ if (merged.length > 0) {
+ console.log('XMLTV Debug: Sample programs after merge:');
+ merged.slice(0, 3).forEach((prog, idx) => {
+ console.log(` ${idx+1}. ${prog.title || 'No title'}, start: ${prog.start || 'No start'}, stop: ${prog.stop || 'No stop'}`);
+ });
+ } else {
+ console.error('XMLTV Debug: ERROR - No programs after merge');
+ }
+
await _writePrograms(xw, json[number].channel, merged, throttle, xmlSettings, cacheImageService);
}
})()
@@ -61,49 +90,250 @@ function writePromise(json, xmlSettings, throttle, cacheImageService) {
}
// ────────────────────────────────────────────────────────────────────────────────
-// MERGE LOGIC – episode fragments → single block, ignore short fillers
+// MERGE LOGIC – completely eliminate placeholders
// ────────────────────────────────────────────────────────────────────────────────
function _smartMerge(programs) {
- if (!Array.isArray(programs) || programs.length === 0) return [];
+ if (!Array.isArray(programs) || programs.length === 0) {
+ return [];
+ }
+
+ // Debug log for input
+ console.log(`_smartMerge received ${programs.length} programs`);
// Helper functions
- function isFiller(p) { return Number(p.duration) < FILLER_MS; }
- function ms(t) { return Date.parse(t); }
- function gap(a, b) { return ms(b.start) - ms(a.stop); }
-
- // Chronological order & only items with valid ISO
- const sorted = programs
- .filter(p => typeof p.start === 'string' && !isNaN(Date.parse(p.start)))
- .sort((a, b) => ms(a.start) - ms(b.start));
-
- const out = [];
- let cur = null;
-
- for (const p of sorted) {
- if (isFiller(p)) { // bump: just extend current block's stop
- if (cur) cur.stop = p.stop;
- continue;
- }
- if ( cur &&
- cur.ratingKey === p.ratingKey &&
- gap(cur, p) <= CONTIG_MS ) { // same ep, continuous ⇒ extend
- cur.stop = p.stop;
- } else { // new block
- cur = {
- start: p.start,
- stop: p.stop,
- ratingKey: p.ratingKey,
- title: p.title,
- sub: p.sub,
- icon: p.icon,
- summary: p.summary,
- rating: p.rating
- };
- out.push(cur);
+ function ms(t) {
+ try {
+ return Date.parse(t);
+ } catch (e) {
+ console.error('Error parsing date:', t, e);
+ return 0;
}
}
- return out;
+ function gap(a, b) {
+ try {
+ return ms(b.start) - ms(a.stop);
+ } catch (e) {
+ console.error('Error calculating gap:', e);
+ return 999999; // Large gap to prevent merging on error
+ }
+ }
+
+ function getChannelStealthDuration(channel) {
+ if (channel && typeof(channel.guideMinimumDurationSeconds) !== 'undefined'
+ && !isNaN(channel.guideMinimumDurationSeconds)) {
+ return channel.guideMinimumDurationSeconds * 1000;
+ }
+ return constants.DEFAULT_GUIDE_STEALTH_DURATION;
+ }
+
+ function isProgramFlex(program, channel) {
+ const stealthDuration = getChannelStealthDuration(channel);
+ return program.isOffline ||
+ (program.duration && program.duration <= stealthDuration);
+ }
+
+ function isSameShow(a, b) {
+ // Check if two programs are the same show (can be different episodes)
+ if (!a || !b) return false;
+
+ // If we have ratingKeys, use those as a definitive check
+ if (a.ratingKey && b.ratingKey) {
+ // For exact same episode
+ if (a.ratingKey === b.ratingKey) return true;
+ }
+
+ // Check if it's the same show
+ if (a.showTitle && b.showTitle && a.showTitle === b.showTitle) {
+ // Same show, check if sequential episodes
+ if (a.type === 'episode' && b.type === 'episode') {
+ if (a.season === b.season) {
+ // Same season, check if episodes are sequential
+ if (Math.abs((a.episode || 0) - (b.episode || 0)) <= 1) {
+ return true;
+ }
+ }
+ }
+ // Same movie or generic content
+ return a.type === b.type && a.type !== 'episode';
+ }
+
+ return false;
+ }
+
+ // Get channel from first program if available
+ const channel = programs.length > 0 && programs[0].channel ? programs[0].channel : {
+ guideMinimumDurationSeconds: constants.DEFAULT_GUIDE_STEALTH_DURATION / 1000,
+ name: "dizqueTV"
+ };
+
+ const flexTitle = channel.guideFlexPlaceholder || channel.name;
+
+ // Threshold for considering programs adjacent
+ const ADJACENT_THRESHOLD = 30 * 1000; // 30 seconds
+
+ // Maximum gap for merging shows of the same title
+ const SAME_SHOW_MAX_GAP = 10 * 60 * 1000; // 10 minutes
+
+ // Ensure all programs have the required fields and valid start/stop times
+ let validPrograms = programs.filter(p => {
+ if (!p.start || !p.stop) {
+ return false;
+ }
+
+ // Ensure title exists
+ if (!p.title) {
+ p.title = p.showTitle || 'Unknown';
+ }
+ if (!p.summary) {
+ p.summary = '';
+ }
+
+ // Validate that start is before stop
+ const startTime = ms(p.start);
+ const stopTime = ms(p.stop);
+ return startTime < stopTime;
+ });
+
+ // Sort by start time
+ validPrograms.sort((a, b) => ms(a.start) - ms(b.start));
+
+ // Step 1: Identify and merge blocks of content that belong together
+ const firstPass = [];
+
+ for (let i = 0; i < validPrograms.length; i++) {
+ const prog = validPrograms[i];
+
+ // Skip flex/placeholder programs entirely
+ if (isProgramFlex(prog, channel)) {
+ continue;
+ }
+
+ if (firstPass.length === 0) {
+ // First program in the list
+ firstPass.push(prog);
+ continue;
+ }
+
+ const lastProg = firstPass[firstPass.length - 1];
+ const gapDuration = gap(lastProg, prog);
+
+ // Handle overlapping or adjacent programs
+ if (gapDuration <= ADJACENT_THRESHOLD) {
+ // Very small gap or overlap
+ if (isSameShow(lastProg, prog) ||
+ (lastProg.title === prog.title && lastProg.type === prog.type)) {
+ // Merge same content
+ lastProg.stop = prog.stop;
+ } else {
+ // Different regular content - add as separate
+ firstPass.push(prog);
+ }
+ } else if (gapDuration <= SAME_SHOW_MAX_GAP && isSameShow(lastProg, prog)) {
+ // Small gap between segments of the same show - merge and include the gap
+ lastProg.stop = prog.stop;
+ } else {
+ // Significant gap or different content - add as separate
+ firstPass.push(prog);
+ }
+ }
+
+ // Step 2: Connect all programs to ensure no gaps
+ const finalResult = [];
+
+ for (let i = 0; i < firstPass.length; i++) {
+ const prog = firstPass[i];
+
+ if (i === 0) {
+ // First program - add as is
+ finalResult.push(prog);
+ continue;
+ }
+
+ const lastProg = finalResult[finalResult.length - 1];
+ const gapDuration = gap(lastProg, prog);
+
+ if (gapDuration > 0) {
+ // There's a gap - extend the previous program to close it
+ lastProg.stop = prog.start;
+ }
+
+ // Add the current program
+ finalResult.push(prog);
+ }
+
+ // Step 3: Handle very long segments by splitting if needed
+ const splitResult = [];
+
+ for (let i = 0; i < finalResult.length; i++) {
+ const prog = finalResult[i];
+ const duration = ms(prog.stop) - ms(prog.start);
+
+ if (duration > constants.TVGUIDE_MAXIMUM_FLEX_DURATION) {
+ // Split long content into segments
+ let currentStart = new Date(prog.start);
+ const endTime = new Date(prog.stop);
+
+ while (currentStart < endTime) {
+ const segmentEnd = new Date(Math.min(
+ currentStart.getTime() + constants.TVGUIDE_MAXIMUM_FLEX_DURATION,
+ endTime.getTime()
+ ));
+
+ splitResult.push({
+ ...prog,
+ start: currentStart.toISOString(),
+ stop: segmentEnd.toISOString()
+ });
+
+ currentStart = segmentEnd;
+ }
+ } else {
+ // Not too long - add as is
+ splitResult.push(prog);
+ }
+ }
+
+ // Final verification to ensure no placeholder/flex content remains
+ for (let i = 0; i < splitResult.length; i++) {
+ const prog = splitResult[i];
+
+ // If somehow a flex/placeholder program made it through, remove it
+ if (prog.title === flexTitle || isProgramFlex(prog, channel)) {
+ console.error(`ERROR: Flex content found after processing! This should not happen.`);
+
+ // If it's not the only program, extend adjacent program(s) to cover this gap
+ if (splitResult.length > 1) {
+ if (i > 0) {
+ // Extend previous program to cover this gap
+ splitResult[i - 1].stop = prog.stop;
+ } else if (i < splitResult.length - 1) {
+ // Extend next program to cover this gap
+ splitResult[i + 1].start = prog.start;
+ }
+
+ // Remove this flex program
+ splitResult.splice(i, 1);
+ i--; // Adjust index after removal
+ }
+ }
+ }
+
+ // Final verification to check for any gaps
+ for (let i = 1; i < splitResult.length; i++) {
+ const prevStop = ms(splitResult[i-1].stop);
+ const currStart = ms(splitResult[i].start);
+
+ if (currStart - prevStop > 1000) { // 1 second threshold
+ console.error(`ERROR: Gap detected after final processing: ${new Date(prevStop).toISOString()} - ${new Date(currStart).toISOString()}`);
+
+ // Fix the gap
+ splitResult[i-1].stop = splitResult[i].start;
+ }
+ }
+
+ console.log(`_smartMerge returning ${splitResult.length} programs (from original ${programs.length})`);
+ return splitResult;
}
// ────────────────────────────────────────────────────────────────────────────────
@@ -145,57 +375,80 @@ async function _writePrograms(xw, channel, programs, throttle, xmlSettings, cach
}
async function _writeProgramme(channel, prog, xw, xmlSettings, cacheImageService) {
- xw.startElement('programme');
- xw.writeAttribute('start', _xmltvDate(prog.start));
- xw.writeAttribute('stop', _xmltvDate(prog.stop));
- xw.writeAttribute('channel', channel.number);
-
- xw.startElement('title');
- xw.writeAttribute('lang', 'en');
- xw.text(prog.title);
- xw.endElement();
- xw.writeRaw('\n ');
-
- if (prog.sub) {
- xw.startElement('sub-title');
- xw.writeAttribute('lang', 'en');
- xw.text(prog.sub.title);
- xw.endElement();
-
- xw.startElement('episode-num');
- xw.writeAttribute('system', 'onscreen');
- xw.text(`S${prog.sub.season} E${prog.sub.episode}`);
- xw.endElement();
-
- xw.startElement('episode-num');
- xw.writeAttribute('system', 'xmltv_ns');
- xw.text(`${prog.sub.season - 1}.${prog.sub.episode - 1}.0/1`);
- xw.endElement();
- }
-
- if (prog.icon) {
- xw.startElement('icon');
- let icon = prog.icon;
- if (xmlSettings.enableImageCache) {
- icon = `{{host}}/cache/images/${cacheImageService.registerImageOnDatabase(icon)}`;
+ try {
+ // Debug log to identify issues
+ console.log(`Writing program: ${prog.title}, start: ${prog.start}, stop: ${prog.stop}`);
+
+ // Validate that we have valid ISO date strings
+ if (!prog.start || !prog.stop) {
+ console.error('ERROR: Program missing start or stop time:', prog);
+ return; // Skip this program
}
- xw.writeAttribute('src', icon);
+
+ // Validate that the dates can be formatted
+ try {
+ const startDate = _xmltvDate(prog.start);
+ const stopDate = _xmltvDate(prog.stop);
+ console.log(`Formatted dates: start=${startDate}, stop=${stopDate}`);
+ } catch (e) {
+ console.error('ERROR: Failed to format dates:', e);
+ return; // Skip this program
+ }
+
+ xw.startElement('programme');
+ xw.writeAttribute('start', _xmltvDate(prog.start));
+ xw.writeAttribute('stop', _xmltvDate(prog.stop));
+ xw.writeAttribute('channel', channel.number);
+
+ xw.startElement('title');
+ xw.writeAttribute('lang', 'en');
+ xw.text(prog.title);
xw.endElement();
- }
+ xw.writeRaw('\n ');
- xw.startElement('desc');
- xw.writeAttribute('lang', 'en');
- xw.text(prog.summary && prog.summary.length > 0 ? prog.summary : channel.name);
- xw.endElement();
+ if (prog.sub) {
+ xw.startElement('sub-title');
+ xw.writeAttribute('lang', 'en');
+ xw.text(prog.sub.title);
+ xw.endElement();
- if (prog.rating) {
- xw.startElement('rating');
- xw.writeAttribute('system', 'MPAA');
- xw.writeElement('value', prog.rating);
+ xw.startElement('episode-num');
+ xw.writeAttribute('system', 'onscreen');
+ xw.text(`S${prog.sub.season} E${prog.sub.episode}`);
+ xw.endElement();
+
+ xw.startElement('episode-num');
+ xw.writeAttribute('system', 'xmltv_ns');
+ xw.text(`${prog.sub.season - 1}.${prog.sub.episode - 1}.0/1`);
+ xw.endElement();
+ }
+
+ if (prog.icon) {
+ xw.startElement('icon');
+ let icon = prog.icon;
+ if (xmlSettings.enableImageCache) {
+ icon = `{{host}}/cache/images/${cacheImageService.registerImageOnDatabase(icon)}`;
+ }
+ xw.writeAttribute('src', icon);
+ xw.endElement();
+ }
+
+ xw.startElement('desc');
+ xw.writeAttribute('lang', 'en');
+ xw.text(prog.summary && prog.summary.length > 0 ? prog.summary : channel.name);
xw.endElement();
- }
- xw.endElement();
+ if (prog.rating) {
+ xw.startElement('rating');
+ xw.writeAttribute('system', 'MPAA');
+ xw.writeElement('value', prog.rating);
+ xw.endElement();
+ }
+
+ xw.endElement();
+ } catch (error) {
+ console.error('Error writing program:', error, prog);
+ }
}
function _xmltvDate(iso) {