Refactor XMLTV writing logic: enhance debug logging, streamline program merging, and improve error handling

This commit is contained in:
tim000x3 2025-04-17 14:02:18 -04:00
parent f3954dde88
commit 947e948257

View File

@ -7,12 +7,6 @@ module.exports = { WriteXMLTV, shutdown };
let isShutdown = false;
let isWorking = false;
// ────────────────────────────────────────────────────────────────────────────────
// CONFIG
// ────────────────────────────────────────────────────────────────────────────────
// Anything shorter than this is considered a bump / filler for merge purposes.
const CONTIG_MS = 2 * 1000; // consider ≤2second gaps continuous
// ────────────────────────────────────────────────────────────────────────────────
// PUBLIC API
// ────────────────────────────────────────────────────────────────────────────────
@ -44,47 +38,41 @@ 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);
console.log('XMLTV Debug: Channel numbers:', channelNumbers);
// First write all channel elements
const channels = channelNumbers.map(n => json[n].channel);
_writeChannels(xw, channels);
// Then write all programs for each channel
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
// Skip if programs array is missing or empty
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'}`);
});
console.log(`XMLTV Debug: Programs before merge: ${json[number].programs.length}`);
// Merge programs to eliminate placeholders and combine related content
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'}`);
});
// Write the merged programs
await _writePrograms(xw, json[number].channel, merged, throttle, xmlSettings, cacheImageService);
} else {
console.error('XMLTV Debug: ERROR - No programs after merge');
}
await _writePrograms(xw, json[number].channel, merged, throttle, xmlSettings, cacheImageService);
}
})()
.then(() => _writeDocEnd(xw, ws))
.catch(err => console.error('Error', err))
.catch(err => console.error('Error in XMLTV generation:', err))
.finally(() => ws.end());
});
}
@ -100,7 +88,7 @@ function _smartMerge(programs) {
// Debug log for input
console.log(`_smartMerge received ${programs.length} programs`);
// Helper functions
// Helper functions for date handling
function ms(t) {
try {
return Date.parse(t);
@ -119,9 +107,11 @@ function _smartMerge(programs) {
}
}
// Flex/placeholder program detection
function getChannelStealthDuration(channel) {
if (channel && typeof(channel.guideMinimumDurationSeconds) !== 'undefined'
&& !isNaN(channel.guideMinimumDurationSeconds)) {
if (channel &&
typeof(channel.guideMinimumDurationSeconds) !== 'undefined' &&
!isNaN(channel.guideMinimumDurationSeconds)) {
return channel.guideMinimumDurationSeconds * 1000;
}
return constants.DEFAULT_GUIDE_STEALTH_DURATION;
@ -133,39 +123,34 @@ function _smartMerge(programs) {
(program.duration && program.duration <= stealthDuration);
}
// Program similarity detection
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 rating keys first (most reliable)
if (a.ratingKey && b.ratingKey && a.ratingKey === b.ratingKey) {
return true;
}
// Check if it's the same show
// Check show title and type
if (a.showTitle && b.showTitle && a.showTitle === b.showTitle) {
// Same show, check if sequential episodes
if (a.type === 'episode' && b.type === 'episode') {
// For episodes, check if they're sequential
if (a.season === b.season) {
// Same season, check if episodes are sequential
if (Math.abs((a.episode || 0) - (b.episode || 0)) <= 1) {
return true;
}
return Math.abs((a.episode || 0) - (b.episode || 0)) <= 1;
}
return false;
}
// Same movie or generic content
// For non-episodes with same title (movies, etc)
return a.type === b.type && a.type !== 'episode';
}
return false;
}
// New helper function to detect exact duplicates
function isExactDuplicate(a, b) {
if (!a || !b) return false;
// Check for identical content
return a.title === b.title &&
((a.sub && b.sub && a.sub.season === b.sub.season && a.sub.episode === b.sub.episode) ||
(!a.sub && !b.sub)) &&
@ -174,56 +159,38 @@ function _smartMerge(programs) {
(a.ratingKey === b.ratingKey || (!a.ratingKey && !b.ratingKey));
}
// Get channel from first program if available
// Get channel info and define thresholds
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
// Step 0: Filter and prepare programs
let validPrograms = programs.filter(p => {
if (!p.start || !p.stop) {
return false;
}
if (!p.start || !p.stop) return false;
// Ensure title exists
if (!p.title) {
p.title = p.showTitle || 'Unknown';
}
if (!p.summary) {
p.summary = '';
}
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;
});
return ms(p.start) < ms(p.stop);
}).sort((a, b) => ms(a.start) - ms(b.start));
// 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
// Step 1: Merge related content blocks and remove flex
const firstPass = [];
for (let i = 0; i < validPrograms.length; i++) {
const prog = validPrograms[i];
// Skip flex/placeholder programs entirely
if (isProgramFlex(prog, channel)) {
continue;
}
// Skip flex/placeholder programs
if (isProgramFlex(prog, channel)) continue;
if (firstPass.length === 0) {
// First program in the list
firstPass.push(prog);
continue;
}
@ -231,59 +198,52 @@ function _smartMerge(programs) {
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
// Adjacent programs with same title - merge
if (isSameShow(lastProg, prog) || (lastProg.title === prog.title && lastProg.type === prog.type)) {
lastProg.stop = prog.stop;
} else {
// Different regular content - add as separate
// Different content - add separately
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
// Small gap between same show segments - merge
lastProg.stop = prog.stop;
} else {
// Significant gap or different content - add as separate
// Large gap or different content - add separately
firstPass.push(prog);
}
}
// Step 2: Connect all programs to ensure no gaps
// Step 2: Close gaps between programs
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
// Close any gaps by extending previous program
if (gap(lastProg, prog) > 0) {
lastProg.stop = prog.start;
}
// Add the current program
finalResult.push(prog);
}
// Step 3: Handle very long segments by splitting if needed
// Step 3: Split overly long segments
const splitResult = [];
for (let i = 0; i < finalResult.length; i++) {
const prog = finalResult[i];
for (const prog of finalResult) {
const duration = ms(prog.stop) - ms(prog.start);
if (duration > constants.TVGUIDE_MAXIMUM_FLEX_DURATION) {
// Split long content into segments
// Split into manageable segments
let currentStart = new Date(prog.start);
const endTime = new Date(prog.stop);
@ -302,81 +262,69 @@ function _smartMerge(programs) {
currentStart = segmentEnd;
}
} else {
// Not too long - add as is
splitResult.push(prog);
}
}
// Final verification to ensure no placeholder/flex content remains
// Step 4: Final verification and cleanup
// Remove any remaining flex content
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) {
// Fix by extending adjacent programs
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
i--;
}
}
}
// Final verification to check for any gaps
// Fix any remaining 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
if (currStart - prevStop > 1000) {
console.error(`ERROR: Gap detected after processing: ${new Date(prevStop).toISOString()} - ${new Date(currStart).toISOString()}`);
splitResult[i-1].stop = splitResult[i].start;
}
}
// Step 4: Merge consecutive identical programs (final pass)
// Step 5: Merge consecutive identical programs
console.log('XMLTV Debug: Starting final pass to merge identical consecutive programs');
const deduplicatedResult = [];
let currentGroup = null;
let lastProgram = null;
for (let i = 0; i < splitResult.length; i++) {
const prog = splitResult[i];
for (const prog of splitResult) {
if (lastProgram === null) {
// First program
lastProgram = prog;
deduplicatedResult.push(prog);
lastProgram = prog;
continue;
}
// Check if this is an exact duplicate of the last program
// Merge exact duplicates
if (isExactDuplicate(lastProgram, prog) &&
Math.abs(ms(lastProgram.stop) - ms(prog.start)) <= ADJACENT_THRESHOLD) {
// Merge by extending the last program
console.log(`XMLTV Debug: Merging duplicate program: ${prog.title}`);
lastProgram.stop = prog.stop;
} else {
// Different program, add as new
deduplicatedResult.push(prog);
lastProgram = prog;
}
}
console.log(`XMLTV Debug: Final pass reduced from ${splitResult.length} to ${deduplicatedResult.length} programs`);
console.log(`_smartMerge returning ${deduplicatedResult.length} programs (from original ${programs.length})`);
return deduplicatedResult;
}
@ -412,61 +360,72 @@ function _writeChannels(xw, channels) {
}
async function _writePrograms(xw, channel, programs, throttle, xmlSettings, cacheImageService) {
// Log the number of programs to be written
console.log(`Writing ${programs.length} programs for channel ${channel.number}`);
// Process each program with throttling
for (const prog of programs) {
if (!isShutdown) await throttle();
if (isShutdown) break; // Early exit if shutdown requested
await throttle();
await _writeProgramme(channel, prog, xw, xmlSettings, cacheImageService);
}
}
async function _writeProgramme(channel, prog, xw, xmlSettings, cacheImageService) {
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
// Validate program data
if (!prog.start || !prog.stop) {
console.error('ERROR: Program missing start or stop time:', prog);
return; // Skip this program
console.error('ERROR: Program missing start or stop time:', prog.title || 'Unknown');
return; // Skip invalid program
}
// Validate that the dates can be formatted
// Format dates and validate
let startDate, stopDate;
try {
const startDate = _xmltvDate(prog.start);
const stopDate = _xmltvDate(prog.stop);
console.log(`Formatted dates: start=${startDate}, stop=${stopDate}`);
startDate = _xmltvDate(prog.start);
stopDate = _xmltvDate(prog.stop);
} catch (e) {
console.error('ERROR: Failed to format dates:', e);
return; // Skip this program
console.error('ERROR: Invalid date format in program:', prog.title, e.message);
return; // Skip program with invalid dates
}
// Write program element and attributes
xw.startElement('programme');
xw.writeAttribute('start', _xmltvDate(prog.start));
xw.writeAttribute('stop', _xmltvDate(prog.stop));
xw.writeAttribute('start', startDate);
xw.writeAttribute('stop', stopDate);
xw.writeAttribute('channel', channel.number);
// Write title
xw.startElement('title');
xw.writeAttribute('lang', 'en');
xw.text(prog.title);
xw.endElement();
// Add previously-shown tag
xw.writeRaw('\n <previously-shown/>');
// Add episode information if available
if (prog.sub) {
// Subtitle (episode title)
xw.startElement('sub-title');
xw.writeAttribute('lang', 'en');
xw.text(prog.sub.title);
xw.endElement();
// Episode numbering in human-readable format
xw.startElement('episode-num');
xw.writeAttribute('system', 'onscreen');
xw.text(`S${prog.sub.season} E${prog.sub.episode}`);
xw.endElement();
// Episode numbering in XMLTV standard format (zero-based)
xw.startElement('episode-num');
xw.writeAttribute('system', 'xmltv_ns');
xw.text(`${prog.sub.season - 1}.${prog.sub.episode - 1}.0/1`);
xw.endElement();
}
// Add program icon/thumbnail if available
if (prog.icon) {
xw.startElement('icon');
let icon = prog.icon;
@ -477,11 +436,13 @@ async function _writeProgramme(channel, prog, xw, xmlSettings, cacheImageService
xw.endElement();
}
// Add program description
xw.startElement('desc');
xw.writeAttribute('lang', 'en');
xw.text(prog.summary && prog.summary.length > 0 ? prog.summary : channel.name);
xw.endElement();
// Add content rating if available
if (prog.rating) {
xw.startElement('rating');
xw.writeAttribute('system', 'MPAA');
@ -489,9 +450,9 @@ async function _writeProgramme(channel, prog, xw, xmlSettings, cacheImageService
xw.endElement();
}
xw.endElement();
xw.endElement(); // Close programme element
} catch (error) {
console.error('Error writing program:', error, prog);
console.error('Error writing program:', prog.title || 'Unknown', error.message);
}
}