Refactor XMLTV writing logic: enhance debug logging, streamline program merging, and improve error handling
This commit is contained in:
parent
f3954dde88
commit
947e948257
213
src/xmltv.js
213
src/xmltv.js
@ -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 ≤2‑second 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user