improved guide generation
This commit is contained in:
parent
48561387b1
commit
0a37b6fc53
326
src/xmltv.js
326
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 <previously-shown/>');
|
||||
|
||||
if (prog.sub) {
|
||||
xw.startElement('sub-title');
|
||||
xw.writeAttribute('lang', 'en');
|
||||
xw.text(prog.sub.title);
|
||||
xw.endElement();
|
||||
xw.writeRaw('\n <previously-shown/>')
|
||||
|
||||
//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.');
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user