diff --git a/index.js b/index.js index ac9cb50..442f8ec 100644 --- a/index.js +++ b/index.js @@ -10,14 +10,15 @@ const api = require('./src/api') const dbMigration = require('./src/database-migration'); const video = require('./src/video') const HDHR = require('./src/hdhr') -const CacheImageService = require('./src/cache-image-service'); -const SettingsService = require('./src/services/settings-service'); +const FileCacheService = require('./src/services/file-cache-service'); +const CacheImageService = require('./src/services/cache-image-service'); const xmltv = require('./src/xmltv') const Plex = require('./src/plex'); const channelCache = require('./src/channel-cache'); const constants = require('./src/constants') const ChannelDB = require("./src/dao/channel-db"); +const M3uService = require("./src/services/m3u-service"); const FillerDB = require("./src/dao/filler-db"); const TVGuideService = require("./src/tv-guide-service"); const onShutdown = require("node-graceful-shutdown").onShutdown; @@ -72,10 +73,14 @@ fillerDB = new FillerDB( path.join(process.env.DATABASE, 'filler') , channelDB, db.connect(process.env.DATABASE, ['channels', 'plex-servers', 'ffmpeg-settings', 'plex-settings', 'xmltv-settings', 'hdhr-settings', 'db-version', 'client-id', 'cache-images', 'settings']) +fileCache = new FileCacheService( path.join(process.env.DATABASE, 'cache') ); +cacheImageService = new CacheImageService(db, fileCache); +m3uService = new M3uService(channelDB, fileCache, channelCache) + initDB(db, channelDB) -const guideService = new TVGuideService(xmltv, db); +const guideService = new TVGuideService(xmltv, db, cacheImageService); @@ -185,16 +190,15 @@ app.get('/version.js', (req, res) => { app.use('/images', express.static(path.join(process.env.DATABASE, 'images'))) app.use(express.static(path.join(__dirname, 'web/public'))) app.use('/images', express.static(path.join(process.env.DATABASE, 'images'))) -app.use('/cache/images', CacheImageService.routerInterceptor()) +app.use('/cache/images', cacheImageService.routerInterceptor()) app.use('/cache/images', express.static(path.join(process.env.DATABASE, 'cache/images'))) app.use('/favicon.svg', express.static( path.join(__dirname, 'resources/favicon.svg') ) ); // API Routers -app.use(api.router(db, channelDB, fillerDB, xmltvInterval, guideService )) -app.use('/api/cache/images', CacheImageService.apiRouters()) -app.use('/api/settings/cache', SettingsService.apiRouters()) +app.use(api.router(db, channelDB, fillerDB, xmltvInterval, guideService, m3uService )) +app.use('/api/cache/images', cacheImageService.apiRouters()) app.use(video.router( channelDB, fillerDB, db)) app.use(hdhr.router) diff --git a/src/api.js b/src/api.js index 37673dd..4f46328 100644 --- a/src/api.js +++ b/src/api.js @@ -10,11 +10,10 @@ const PlexServerDB = require('./dao/plex-server-db'); const Plex = require("./plex.js"); const FillerDB = require('./dao/filler-db'); const timeSlotsService = require('./services/time-slots-service'); -const M3uService = require('./services/m3u-service'); module.exports = { router: api } -function api(db, channelDB, fillerDB, xmltvInterval, guideService ) { - const m3uService = new M3uService(channelDB); +function api(db, channelDB, fillerDB, xmltvInterval, guideService, _m3uService ) { + const m3uService = _m3uService; const router = express.Router() const plexServerDB = new PlexServerDB(channelDB, channelCache, db); @@ -452,6 +451,7 @@ function api(db, channelDB, fillerDB, xmltvInterval, guideService ) { _id: req.body._id, cache: req.body.cache, refresh: req.body.refresh, + enableImageCache: (req.body.enableImageCache === true), file: xmltv.file, } ); @@ -612,24 +612,6 @@ function api(db, channelDB, fillerDB, xmltvInterval, guideService ) { }) - // hls.m3u Download is not really working correctly right now - router.get('/api/hls.m3u', async (req, res) => { - try { - res.type('text'); - - const host = `${req.protocol}://${req.get('host')}`; - const data = await m3uService.getChannelList(host, 'hls'); - - res.send(data); - - } catch(err) { - console.error(err); - res.status(500).send("error"); - } - }) - - - function updateXmltv() { xmltvInterval.updateXML() diff --git a/src/channel-cache.js b/src/channel-cache.js index 2a9fbdd..3588c96 100644 --- a/src/channel-cache.js +++ b/src/channel-cache.js @@ -4,6 +4,7 @@ let cache = {}; let programPlayTimeCache = {}; let fillerPlayTimeCache = {}; let configCache = {}; +let numbers = null; async function getChannelConfig(channelDB, channelId) { //with lazy-loading @@ -21,6 +22,22 @@ async function getChannelConfig(channelDB, channelId) { return configCache[channelId]; } +async function getAllNumbers(channelDB) { + if (numbers === null) { + let n = channelDB.getAllChannelNumbers(); + numbers = n; + } + return numbers; +} + +async function getAllChannels(channelDB) { + let channelNumbers = await getAllNumbers(channelDB); + return await Promise.all( channelNumbers.map( async (x) => { + return (await getChannelConfig(channelDB, x))[0]; + }) ); +} + + function saveChannelConfig(number, channel ) { configCache[number] = [channel]; } @@ -127,6 +144,7 @@ function clear() { //it's not necessary to clear the playback cache and it may be undesirable configCache = {}; cache = {}; + numbers = null; } module.exports = { @@ -134,6 +152,7 @@ module.exports = { recordPlayback: recordPlayback, clear: clear, getProgramLastPlayTime: getProgramLastPlayTime, + getAllChannels: getAllChannels, getChannelConfig: getChannelConfig, saveChannelConfig: saveChannelConfig, getFillerLastPlayTime: getFillerLastPlayTime, diff --git a/src/config.js b/src/config.js deleted file mode 100644 index 396ea30..0000000 --- a/src/config.js +++ /dev/null @@ -1,4 +0,0 @@ -module.exports = { - DATABASE: process.env.DATABASE || './.dizquetv', - PORT: process.env.PORT || 8000, -}; \ No newline at end of file diff --git a/src/database-migration.js b/src/database-migration.js index 4a3f9dc..2634fd5 100644 --- a/src/database-migration.js +++ b/src/database-migration.js @@ -20,7 +20,7 @@ const path = require('path'); var fs = require('fs'); -const TARGET_VERSION = 800; +const TARGET_VERSION = 801; const STEPS = [ // [v, v2, x] : if the current version is v, call x(db), and version becomes v2 @@ -40,6 +40,7 @@ const STEPS = [ // the addDeinterlaceFilter step. This 702 step no longer exists as a target // but we have to migrate it to 800 using the reAddIcon. [ 702, 800, (db,channels,dir) => reAddIcon(dir) ], + [ 800, 801, (db) => addImageCache(db) ], ] const { v4: uuidv4 } = require('uuid'); @@ -803,6 +804,14 @@ function addDeinterlaceFilter(db) { fs.writeFileSync( f, JSON.stringify( [ffmpegSettings] ) ); } +function addImageCache(db) { + let xmltvSettings = db['xmltv-settings'].find()[0]; + let f = path.join(process.env.DATABASE, 'xmltv-settings.json'); + xmltvSettings.enableImageCache = false; + fs.writeFileSync( f, JSON.stringify( [xmltvSettings] ) ); +} + + module.exports = { initDB: initDB, defaultFFMPEG: ffmpeg, diff --git a/src/cache-image-service.js b/src/services/cache-image-service.js similarity index 90% rename from src/cache-image-service.js rename to src/services/cache-image-service.js index c604796..8c1f7b6 100644 --- a/src/cache-image-service.js +++ b/src/services/cache-image-service.js @@ -1,11 +1,6 @@ const fs = require('fs'); const express = require('express'); const request = require('request'); -const diskDb = require('diskdb'); - -const config = require('./config'); -const CacheService = require('./cache-service'); -const SettingsService = require('./services/settings-service'); /** * Manager a cache in disk for external images. @@ -13,13 +8,10 @@ const SettingsService = require('./services/settings-service'); * @class CacheImageService */ class CacheImageService { - constructor() { - this.cacheService = CacheService; + constructor( db, fileCacheService ) { + this.cacheService = fileCacheService; this.imageCacheFolder = 'images'; - const connection = diskDb.connect(config.DATABASE, ['cache-images']); - this.db = connection['cache-images']; - - SettingsService.saveSetting('enabled-cache-image', 'Enable Cache Image', false); + this.db = db['cache-images']; } /** @@ -161,4 +153,4 @@ class CacheImageService { } } -module.exports = new CacheImageService(); \ No newline at end of file +module.exports = CacheImageService; \ No newline at end of file diff --git a/src/cache-service.js b/src/services/file-cache-service.js similarity index 85% rename from src/cache-service.js rename to src/services/file-cache-service.js index a81e236..1e0c6c7 100644 --- a/src/cache-service.js +++ b/src/services/file-cache-service.js @@ -1,15 +1,14 @@ const path = require('path'); const fs = require('fs'); -const config = require('./config'); /** - * A File Cache controller for store and retrieve files from disk + * Store files in cache * - * @class CacheService + * @class FileCacheService */ -class CacheService { - constructor() { - this.cachePath = path.join(config.DATABASE, 'cache'); +class FileCacheService { + constructor(cachePath) { + this.cachePath = cachePath; this.cache = {}; } @@ -76,7 +75,11 @@ class CacheService { deleteCache(fullFilePath) { return new Promise((resolve, reject) => { try { - fs.unlinkSync(path.join(this.cachePath, fullFilePath), (err) => { + let thePath = path.join(this.cachePath, fullFilePath); + if (! fs.existsSync(thePath)) { + return resolve(true); + } + fs.unlinkSync(thePath, (err) => { if(err) { throw Error("Can't save file: ", err); } else { @@ -91,4 +94,4 @@ class CacheService { } } -module.exports = new CacheService(); \ No newline at end of file +module.exports = FileCacheService; \ No newline at end of file diff --git a/src/services/m3u-service.js b/src/services/m3u-service.js index ab78064..674e200 100644 --- a/src/services/m3u-service.js +++ b/src/services/m3u-service.js @@ -1,16 +1,14 @@ -const CacheService = require('../cache-service'); -const SettingsService = require('./settings-service'); - /** * Manager and Generate M3U content * * @class M3uService */ class M3uService { - constructor(dataBase) { + constructor(dataBase, fileCacheService, channelCache) { this.dataBase = dataBase; - this.cacheService = CacheService; - SettingsService.saveSetting('enabled-cache-m3u', 'Enable Cache M3U', false); + this.cacheService = fileCacheService; + this.channelCache = channelCache; + this.cacheReady = false; } /** @@ -20,12 +18,8 @@ class M3uService { * @returns {promise} Return a Promise with HLS or M3U file content * @memberof M3uService */ - getChannelList(host, type = 'm3u') { - if(type === 'hls') { - return this.buildHLSList(host); - } else { - return this.buildM3uList(host); - } + getChannelList(host) { + return this.buildM3uList(host); } /** @@ -36,105 +30,46 @@ class M3uService { * @memberof M3uService */ - buildM3uList(host) { - return new Promise(async (resolve, reject) => { - - try { - - const cache = SettingsService.getSetting('enabled-cache-m3u').value; - - if(cache) { - const cacheChannels = await this.cacheService.getCache('channels.m3u'); - if(cacheChannels) { - resolve(this.replaceHostOnM3u(host, cacheChannels)); - } - } - - let channels = await this.dataBase.getAllChannels(); - - channels.sort((a, b) => { - return a.number < b.number ? -1 : 1 - }); - - const tvg = `{{host}}/api/xmltv.xml`; - - let data = `#EXTM3U url-tvg="${tvg}" x-tvg-url="${tvg}"\n`; - - for (var i = 0; i < channels.length; i++) { - if (channels[i].stealth!==true) { - data += `#EXTINF:0 tvg-id="${channels[i].number}" CUID="${channels[i].number}" tvg-chno="${channels[i].number}" tvg-name="${channels[i].name}" tvg-logo="${channels[i].icon}" group-title="dizqueTV",${channels[i].name}\n` - data += `{{host}}/video?channel=${channels[i].number}\n` - } - } - if (channels.length === 0) { - data += `#EXTINF:0 tvg-id="1" tvg-chno="1" tvg-name="dizqueTV" tvg-logo="{{host}}/resources/dizquetv.png" group-title="dizqueTV",dizqueTV\n` - data += `{{host}}/setup\n` - } - - if(cache) { - await this.cacheService.setCache('channels.m3u', data); - } - - resolve(this.replaceHostOnM3u(host, data)); - - } catch (error) { - reject(error); + async buildM3uList(host) { + if (this.cacheReady) { + const cachedM3U = await this.cacheService.getCache('channels.m3u'); + if (cachedM3U) { + return this.replaceHostOnM3u(host, cachedM3U); } + } + let channels = await this.channelCache.getAllChannels(this.dataBase); + + channels.sort((a, b) => { + return a.number < b.number ? -1 : 1 }); + + const tvg = `{{host}}/api/xmltv.xml`; + + let data = `#EXTM3U url-tvg="${tvg}" x-tvg-url="${tvg}"\n`; + + for (var i = 0; i < channels.length; i++) { + if (channels[i].stealth !== true) { + data += `#EXTINF:0 tvg-id="${channels[i].number}" CUID="${channels[i].number}" tvg-chno="${channels[i].number}" tvg-name="${channels[i].name}" tvg-logo="${channels[i].icon}" group-title="dizqueTV",${channels[i].name}\n` + data += `{{host}}/video?channel=${channels[i].number}\n` + } + } + if (channels.length === 0) { + data += `#EXTINF:0 tvg-id="1" tvg-chno="1" tvg-name="dizqueTV" tvg-logo="{{host}}/resources/dizquetv.png" group-title="dizqueTV",dizqueTV\n` + data += `{{host}}/setup\n` + } + let saveCacheThread = async() => { + try { + await this.cacheService.setCache('channels.m3u', data); + this.cacheReady = true; + } catch(err) { + console.error(err); + } + }; + saveCacheThread(); + return this.replaceHostOnM3u(host, data); } - /** - * - * "hls.m3u Download is not really working correctly right now" - * - * @param {boolean} [cache=true] - * @param {*} host - * @returns {promise} M3U file content - * @memberof M3uService - */ - buildHLSList(host, cache = true) { - return new Promise(async (resolve, reject) => { - - try { - - const cacheChannels = await this.cacheService.getCache('channels-hls.m3u'); - - if(cache && cacheChannels) { - resolve(this.replaceHostOnM3u(host, cacheChannels)); - } - - let channels = await this.dataBase.getAllChannels(); - - channels.sort((a, b) => { - return a.number < b.number ? -1 : 1 - }); - - const tvg = `{{host}}/api/xmltv.xml`; - - let data = "#EXTM3U\n" - for (var i = 0; i < channels.length; i++) { - data += `#EXTINF:0 tvg-id="${channels[i].number}" tvg-chno="${channels[i].number}" tvg-name="${channels[i].name}" tvg-logo="${channels[i].icon}" group-title="dizqueTV",${channels[i].name}\n` - data += `{{host}}/m3u8?channel=${channels[i].number}\n` - } - if (channels.length === 0) { - data += `#EXTINF:0 tvg-id="1" tvg-chno="1" tvg-name="dizqueTV" tvg-logo="{{host}}/resources/dizquetv.png" group-title="dizqueTV",dizqueTV\n` - data += `{{host}}/setup\n` - } - - if(cache) { - await this.cacheService.setCache('channels-hls.m3u', data); - } - - resolve(this.replaceHostOnM3u(host, data)); - - } catch (error) { - reject(error); - } - - }); - } - /** * Replace {{host}} string with a URL on file contents. * @@ -153,8 +88,7 @@ class M3uService { * @memberof M3uService */ async clearCache() { - await this.cacheService.deleteCache('channels.m3u'); - await this.cacheService.deleteCache('channels-hls.m3u'); + this.cacheReady = false; } } diff --git a/src/services/settings-service.js b/src/services/settings-service.js deleted file mode 100644 index 1df45e8..0000000 --- a/src/services/settings-service.js +++ /dev/null @@ -1,114 +0,0 @@ -const diskDb = require('diskdb'); -const express = require('express'); -const config = require('../config'); - -class SettingsService { - constructor() { - const connection = diskDb.connect(config.DATABASE, ['settings']); - this.db = connection['settings']; - } - - apiRouters() { - const router = express.Router(); - - router.get('/', async (req, res) => { - try { - const settings = await this.getAllSettings(); - res.send(settings); - } catch (error) { - console.error(error); - res.status(500).send("error"); - } - }); - - router.post('/', async (req, res) => { - try { - const {key, title, value} = req.body; - if(!key || !title || value === undefined) { - throw Error("Key, title and value are Required"); - } - - const settings = await this.saveSetting(key, title, value); - res.send(settings); - } catch (error) { - console.error(error); - res.status(500).send("error"); - } - }); - - router.put('/:key', async (req, res) => { - try { - const key = req.params.key; - const {value} = req.body; - console.log(key, value); - if(!key || value === undefined) { - throw Error("Key and value are Required"); - } - const settings = await this.updateSetting(key, value); - console.log(settings); - res.send(settings); - } catch (error) { - console.error(error); - res.status(500).send("error"); - } - }); - - return router; - } - - getSetting(key) { - return new Promise((resolve, reject) =>{ - try { - const setting = this.db.find({key})[0]; - resolve(setting); - } catch (error) { - reject(error); - } - }); - } - - getAllSettings() { - return new Promise((resolve, reject) =>{ - try { - const settings = this.db.find(); - resolve(settings); - } catch (error) { - reject(error); - } - }); - } - saveSetting(key, title, value) { - return new Promise((resolve, reject) =>{ - try { - const setting = this.db.find({key})[0]; - if(!setting) { - this.db.save({key, title, value}); - } - resolve(true); - } catch (error) { - reject(error); - } - }); - } - updateSetting(key, value) { - return new Promise((resolve, reject) => { - try { - const setting = this.db.find({key})[0]; - if(setting) { - const query = this.db.update({_id: setting._id}, {key, value}); - if(query.updated > 0) { - const settings = this.db.find(); - resolve(settings); - } - reject(); - } else { - reject({error: true, msg: "Setting not found!"}); - } - } catch (error) { - - } - }); - } -} - -module.exports = new SettingsService(); \ No newline at end of file diff --git a/src/tv-guide-service.js b/src/tv-guide-service.js index 3c40df9..5936dcd 100644 --- a/src/tv-guide-service.js +++ b/src/tv-guide-service.js @@ -7,7 +7,7 @@ class TVGuideService /**** * **/ - constructor(xmltv, db) { + constructor(xmltv, db, cacheImageService) { this.cached = null; this.lastUpdate = 0; this.updateTime = 0; @@ -18,6 +18,7 @@ class TVGuideService this.doThrottle = false; this.xmltv = xmltv; this.db = db; + this.cacheImageService = cacheImageService; } async get() { @@ -351,7 +352,7 @@ class TVGuideService async refreshXML() { let xmltvSettings = this.db['xmltv-settings'].find()[0]; - await this.xmltv.WriteXMLTV(this.cached, xmltvSettings, async() => await this._throttle(), this.db); + await this.xmltv.WriteXMLTV(this.cached, xmltvSettings, async() => await this._throttle(), this.cacheImageService); } async getStatus() { diff --git a/src/xmltv.js b/src/xmltv.js index ac38e98..a4c7798 100644 --- a/src/xmltv.js +++ b/src/xmltv.js @@ -1,14 +1,12 @@ const XMLWriter = require('xml-writer') const fs = require('fs') -const CacheImageService = require('./cache-image-service'); -const SettingsService = require('./services/settings-service'); module.exports = { WriteXMLTV: WriteXMLTV, shutdown: shutdown } let isShutdown = false; let isWorking = false; -async function WriteXMLTV(json, xmlSettings, throttle) { +async function WriteXMLTV(json, xmlSettings, throttle, cacheImageService) { if (isShutdown) { return; } @@ -18,14 +16,14 @@ async function WriteXMLTV(json, xmlSettings, throttle) { } isWorking = true; try { - await writePromise(json, xmlSettings, throttle); + await writePromise(json, xmlSettings, throttle, cacheImageService); } catch (err) { console.error("Error writing xmltv", err); } isWorking = false; } -function writePromise(json, xmlSettings, throttle) { +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)) @@ -39,7 +37,7 @@ function writePromise(json, xmlSettings, throttle) { _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); + await _writePrograms(xw, json[number].channel, json[number].programs, throttle, xmlSettings, cacheImageService); } } middle().then( () => { @@ -77,16 +75,16 @@ function _writeChannels(xw, channels) { } } -async function _writePrograms(xw, channel, programs, throttle) { +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); + await _writeProgramme(channel, programs[i], xw, xmlSettings, cacheImageService); } } -async function _writeProgramme(channel, program, xw) { +async function _writeProgramme(channel, program, xw, xmlSettings, cacheImageService) { // Programme xw.startElement('programme') xw.writeAttribute('start', _createXMLTVDate(program.start)) @@ -113,16 +111,14 @@ async function _writeProgramme(channel, program, xw) { } // Icon if (typeof program.icon !== 'undefined') { - xw.startElement('icon') - - if(await SettingsService.getSetting('enabled-cache-image')){ - const imgUrl = CacheImageService.registerImageOnDatabase(program.icon); - xw.writeAttribute('src', `{{host}}/cache/images/${imgUrl}`) - } else { - xw.writeAttribute('src', program.icon) + xw.startElement('icon'); + let icon = program.icon; + if (xmlSettings.enableImageCache === true) { + const imgUrl = cacheImageService.registerImageOnDatabase(icon); + icon = `{{host}}/cache/images/${imgUrl}`; } - - xw.endElement() + xw.writeAttribute('src', icon); + xw.endElement(); } // Desc xw.startElement('desc') diff --git a/web/public/templates/xmltv-settings.html b/web/public/templates/xmltv-settings.html index 1707974..20c65a0 100644 --- a/web/public/templates/xmltv-settings.html +++ b/web/public/templates/xmltv-settings.html @@ -14,7 +14,7 @@
- + How many hours of programming to include in the xmltv file.
@@ -24,5 +24,13 @@ How often should the xmltv file be updated.
+
+
+ + + +
If enabled the pictures used for Movie and TV Show posters will be cached in dizqueTV's .dizqueTV folder and will be delivered by dizqueTV's server instead of requiring calls to Plex. Note that using fixed xmltv location in Plex (as opposed to url) will not work correctly in this case.
+
+ \ No newline at end of file