diff --git a/src/xmltv.js b/src/xmltv.js
index 784e09d..4bb4110 100644
--- a/src/xmltv.js
+++ b/src/xmltv.js
@@ -1,173 +1,215 @@
-const XMLWriter = require('xml-writer')
-const fs = require('fs')
+const XMLWriter = require('xml-writer');
+const fs = require('fs');
-module.exports = { WriteXMLTV: WriteXMLTV, shutdown: shutdown }
+module.exports = { WriteXMLTV, shutdown };
let isShutdown = false;
-let isWorking = false;
+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
+
+// ────────────────────────────────────────────────────────────────────────────────
+// PUBLIC API
+// ────────────────────────────────────────────────────────────────────────────────
async function WriteXMLTV(json, xmlSettings, throttle, cacheImageService) {
- if (isShutdown) {
- return;
- }
- if (isWorking) {
- console.log("Concurrent xmltv write attempt detected, skipping");
- return;
- }
- isWorking = true;
- try {
- await writePromise(json, xmlSettings, throttle, cacheImageService);
- } catch (err) {
- console.error("Error writing xmltv", err);
- }
+ if (isShutdown) return;
+ if (isWorking) {
+ console.log('Concurrent xmltv write attempt detected, skipping');
+ return;
+ }
+ isWorking = true;
+ try {
+ await writePromise(json, xmlSettings, throttle, cacheImageService);
+ } catch (err) {
+ console.error('Error writing xmltv', err);
+ } finally {
isWorking = false;
+ }
}
+// ────────────────────────────────────────────────────────────────────────────────
function writePromise(json, xmlSettings, throttle, cacheImageService) {
- return new Promise((resolve, reject) => {
- let ws = fs.createWriteStream(xmlSettings.file)
- let xw = new XMLWriter(true, (str, enc) => ws.write(str, enc))
- ws.on('close', () => { resolve() })
- ws.on('error', (err) => { reject(err) })
- _writeDocStart(xw)
- async function middle() {
- let channelNumbers = [];
- Object.keys(json).forEach( (key, index) => channelNumbers.push(key) );
- let channels = channelNumbers.map( (number) => json[number].channel );
- _writeChannels( xw, channels );
- for (let i = 0; i < channelNumbers.length; i++) {
- let number = channelNumbers[i];
- await _writePrograms(xw, json[number].channel, json[number].programs, throttle, xmlSettings, cacheImageService);
- }
- }
- middle().then( () => {
- _writeDocEnd(xw, ws)
- }).catch( (err) => {
- console.error("Error", err);
- }).then( () => ws.end() );
- })
+ return new Promise((resolve, reject) => {
+ const ws = fs.createWriteStream(xmlSettings.file);
+ const xw = new XMLWriter(true, (str, enc) => ws.write(str, enc));
+
+ ws.on('finish', resolve);
+ ws.on('error', reject);
+
+ _writeDocStart(xw);
+
+ (async () => {
+ const channelNumbers = Object.keys(json);
+ const channels = channelNumbers.map(n => json[n].channel);
+ _writeChannels(xw, channels);
+
+ for (const number of channelNumbers) {
+ const merged = _smartMerge(json[number].programs);
+ await _writePrograms(xw, json[number].channel, merged, throttle, xmlSettings, cacheImageService);
+ }
+ })()
+ .then(() => _writeDocEnd(xw, ws))
+ .catch(err => console.error('Error', err))
+ .finally(() => ws.end());
+ });
}
-function _writeDocStart(xw) {
- xw.startDocument()
- xw.startElement('tv')
- xw.writeAttribute('generator-info-name', 'dizquetv')
+// ────────────────────────────────────────────────────────────────────────────────
+// MERGE LOGIC – episode fragments → single block, ignore short fillers
+// ────────────────────────────────────────────────────────────────────────────────
+function _smartMerge(programs) {
+ if (!Array.isArray(programs) || programs.length === 0) return [];
+
+ // 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);
+ }
+ }
+
+ return out;
}
+
+// ────────────────────────────────────────────────────────────────────────────────
+// XML WRITING HELPERS (unchanged below)
+// ────────────────────────────────────────────────────────────────────────────────
+function _writeDocStart(xw) {
+ xw.startDocument();
+ xw.startElement('tv');
+ xw.writeAttribute('generator-info-name', 'dizquetv');
+}
+
function _writeDocEnd(xw, ws) {
- xw.endElement()
- xw.endDocument()
+ xw.endElement();
+ xw.endDocument();
}
function _writeChannels(xw, channels) {
- for (let i = 0; i < channels.length; i++) {
- xw.startElement('channel')
- xw.writeAttribute('id', channels[i].number)
- xw.startElement('display-name')
- xw.writeAttribute('lang', 'en')
- xw.text(channels[i].name)
- xw.endElement()
- if (channels[i].icon) {
- xw.startElement('icon')
- xw.writeAttribute('src', channels[i].icon)
- xw.endElement()
- }
- xw.endElement()
+ for (const ch of channels) {
+ xw.startElement('channel');
+ xw.writeAttribute('id', ch.number);
+ xw.startElement('display-name');
+ xw.writeAttribute('lang', 'en');
+ xw.text(ch.name);
+ xw.endElement();
+ if (ch.icon) {
+ xw.startElement('icon');
+ xw.writeAttribute('src', ch.icon);
+ xw.endElement();
}
+ xw.endElement();
+ }
}
async function _writePrograms(xw, channel, programs, throttle, xmlSettings, cacheImageService) {
- for (let i = 0; i < programs.length; i++) {
- if (! isShutdown) {
- await throttle();
- }
- await _writeProgramme(channel, programs[i], xw, xmlSettings, cacheImageService);
- }
+ for (const prog of programs) {
+ if (!isShutdown) await throttle();
+ await _writeProgramme(channel, prog, xw, xmlSettings, cacheImageService);
+ }
}
-async function _writeProgramme(channel, program, xw, xmlSettings, cacheImageService) {
- // Programme
- xw.startElement('programme')
- xw.writeAttribute('start', _createXMLTVDate(program.start))
- xw.writeAttribute('stop', _createXMLTVDate(program.stop ))
- xw.writeAttribute('channel', channel.number)
- // Title
- xw.startElement('title')
- xw.writeAttribute('lang', 'en')
- xw.text(program.title);
+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.writeRaw('\n ')
- //sub-title
- // TODO: Add support for track data (artist, album) here
- if ( typeof(program.sub) !== 'undefined') {
- xw.startElement('sub-title')
- xw.writeAttribute('lang', 'en')
- xw.text(program.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', 'onscreen')
- xw.text( "S" + (program.sub.season) + ' E' + (program.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();
+ }
- xw.startElement('episode-num')
- xw.writeAttribute('system', 'xmltv_ns')
- xw.text((program.sub.season - 1) + '.' + (program.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();
+ }
- }
- // Icon
- if (typeof program.icon !== 'undefined') {
- xw.startElement('icon');
- let icon = program.icon;
- if (xmlSettings.enableImageCache === true) {
- const imgUrl = cacheImageService.registerImageOnDatabase(icon);
- icon = `{{host}}/cache/images/${imgUrl}`;
- }
- xw.writeAttribute('src', icon);
- xw.endElement();
- }
- // Desc
- xw.startElement('desc')
- xw.writeAttribute('lang', 'en')
- if ( (typeof(program.summary) !== 'undefined') && (program.summary.length > 0) ) {
- xw.text(program.summary)
- } else {
- xw.text(channel.name)
- }
- xw.endElement()
- // Rating
- if ( (program.rating != null) && (typeof program.rating !== 'undefined') ) {
- xw.startElement('rating')
- xw.writeAttribute('system', 'MPAA')
- xw.writeElement('value', program.rating)
- xw.endElement()
- }
- // End of Programme
- xw.endElement()
+ xw.startElement('desc');
+ xw.writeAttribute('lang', 'en');
+ xw.text(prog.summary && prog.summary.length > 0 ? prog.summary : channel.name);
+ xw.endElement();
+
+ if (prog.rating) {
+ xw.startElement('rating');
+ xw.writeAttribute('system', 'MPAA');
+ xw.writeElement('value', prog.rating);
+ xw.endElement();
+ }
+
+ xw.endElement();
}
-function _createXMLTVDate(d) {
- return d.substring(0,19).replace(/[-T:]/g,"") + " +0000";
-}
-function wait(x) {
- return new Promise((resolve) => {
- setTimeout(resolve, x);
- });
+
+function _xmltvDate(iso) {
+ return iso.substring(0, 19).replace(/[-T:]/g, '') + ' +0000';
}
+function wait(ms) { return new Promise(res => setTimeout(res, ms)); }
+
async function shutdown() {
- isShutdown = true;
- console.log("Shutting down xmltv writer.");
- if (isWorking) {
- let s = "Wait for xmltv writer...";
- while (isWorking) {
- console.log(s);
- await wait(100);
- s = "Still waiting for xmltv writer...";
- }
- console.log("Write finished.");
- } else {
- console.log("xmltv writer had no pending jobs.");
- }
-}
-
+ isShutdown = true;
+ console.log('Shutting down xmltv writer.');
+ while (isWorking) {
+ console.log('Waiting for xmltv writer to finish…');
+ await wait(100);
+ }
+ console.log('xmltv writer idle.');
+}
\ No newline at end of file