File cache system (#242)
* Create a File Cache Service and Channels M3U Cache * Create a Cache Image Service for external images * Singleton, db configurations and repairs
This commit is contained in:
parent
56d6ae3bde
commit
c97ff8f24e
19
index.js
19
index.js
@ -10,6 +10,8 @@ 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 xmltv = require('./src/xmltv')
|
||||
const Plex = require('./src/plex');
|
||||
@ -51,22 +53,28 @@ if (!fs.existsSync(process.env.DATABASE)) {
|
||||
if(!fs.existsSync(path.join(process.env.DATABASE, 'images'))) {
|
||||
fs.mkdirSync(path.join(process.env.DATABASE, 'images'))
|
||||
}
|
||||
|
||||
if(!fs.existsSync(path.join(process.env.DATABASE, 'channels'))) {
|
||||
fs.mkdirSync(path.join(process.env.DATABASE, 'channels'))
|
||||
}
|
||||
if(!fs.existsSync(path.join(process.env.DATABASE, 'filler'))) {
|
||||
fs.mkdirSync(path.join(process.env.DATABASE, 'filler'))
|
||||
}
|
||||
if(!fs.existsSync(path.join(process.env.DATABASE, 'cache'))) {
|
||||
fs.mkdirSync(path.join(process.env.DATABASE, 'cache'))
|
||||
}
|
||||
if(!fs.existsSync(path.join(process.env.DATABASE, 'cache/images'))) {
|
||||
fs.mkdirSync(path.join(process.env.DATABASE, 'cache/images'))
|
||||
}
|
||||
|
||||
|
||||
channelDB = new ChannelDB( path.join(process.env.DATABASE, 'channels') );
|
||||
fillerDB = new FillerDB( path.join(process.env.DATABASE, 'filler') , channelDB, channelCache );
|
||||
|
||||
db.connect(process.env.DATABASE, ['channels', 'plex-servers', 'ffmpeg-settings', 'plex-settings', 'xmltv-settings', 'hdhr-settings', 'db-version', 'client-id'])
|
||||
db.connect(process.env.DATABASE, ['channels', 'plex-servers', 'ffmpeg-settings', 'plex-settings', 'xmltv-settings', 'hdhr-settings', 'db-version', 'client-id', 'cache-images', 'settings'])
|
||||
|
||||
initDB(db, channelDB)
|
||||
|
||||
|
||||
const guideService = new TVGuideService(xmltv, db);
|
||||
|
||||
|
||||
@ -156,6 +164,7 @@ app.use(fileUpload({
|
||||
createParentPath: true
|
||||
}));
|
||||
app.use(bodyParser.json({limit: '50mb'}))
|
||||
|
||||
app.get('/version.js', (req, res) => {
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'application/javascript'
|
||||
@ -176,11 +185,17 @@ 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', 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(video.router( channelDB, fillerDB, db))
|
||||
app.use(hdhr.router)
|
||||
app.listen(process.env.PORT, () => {
|
||||
|
||||
74
src/api.js
74
src/api.js
@ -1,6 +1,7 @@
|
||||
|
||||
const express = require('express')
|
||||
const path = require('path')
|
||||
const fs = require('fs')
|
||||
const databaseMigration = require('./database-migration');
|
||||
const channelCache = require('./channel-cache')
|
||||
const constants = require('./constants');
|
||||
@ -9,11 +10,13 @@ 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 ) {
|
||||
let router = express.Router()
|
||||
let plexServerDB = new PlexServerDB(channelDB, channelCache, db);
|
||||
const m3uService = new M3uService(channelDB);
|
||||
const router = express.Router()
|
||||
const plexServerDB = new PlexServerDB(channelDB, channelCache, db);
|
||||
|
||||
router.get('/api/version', async (req, res) => {
|
||||
try {
|
||||
@ -150,7 +153,7 @@ function api(db, channelDB, fillerDB, xmltvInterval, guideService ) {
|
||||
let channel = await channelCache.getChannelConfig(channelDB, number);
|
||||
if (channel.length == 1) {
|
||||
channel = channel[0];
|
||||
res.send( {
|
||||
res.send({
|
||||
number: channel.number,
|
||||
icon: channel.icon,
|
||||
name: channel.name,
|
||||
@ -176,6 +179,7 @@ function api(db, channelDB, fillerDB, xmltvInterval, guideService ) {
|
||||
})
|
||||
router.post('/api/channel', async (req, res) => {
|
||||
try {
|
||||
await m3uService.clearCache();
|
||||
cleanUpChannel(req.body);
|
||||
await channelDB.saveChannel( req.body.number, req.body );
|
||||
channelCache.clear();
|
||||
@ -188,6 +192,7 @@ function api(db, channelDB, fillerDB, xmltvInterval, guideService ) {
|
||||
})
|
||||
router.put('/api/channel', async (req, res) => {
|
||||
try {
|
||||
await m3uService.clearCache();
|
||||
cleanUpChannel(req.body);
|
||||
await channelDB.saveChannel( req.body.number, req.body );
|
||||
channelCache.clear();
|
||||
@ -200,6 +205,7 @@ function api(db, channelDB, fillerDB, xmltvInterval, guideService ) {
|
||||
})
|
||||
router.delete('/api/channel', async (req, res) => {
|
||||
try {
|
||||
await m3uService.clearCache();
|
||||
await channelDB.deleteChannel( req.body.number );
|
||||
channelCache.clear();
|
||||
res.send( { number: req.body.number} )
|
||||
@ -554,13 +560,19 @@ function api(db, channelDB, fillerDB, xmltvInterval, guideService ) {
|
||||
|
||||
|
||||
// XMLTV.XML Download
|
||||
router.get('/api/xmltv.xml', (req, res) => {
|
||||
router.get('/api/xmltv.xml', async (req, res) => {
|
||||
try {
|
||||
const host = `${req.protocol}://${req.get('host')}`;
|
||||
|
||||
res.set('Cache-Control', 'no-store')
|
||||
res.type('text')
|
||||
let xmltvSettings = db['xmltv-settings'].find()[0]
|
||||
let f = path.resolve(xmltvSettings.file);
|
||||
res.sendFile(f)
|
||||
res.type('application/xml');
|
||||
res.attachment('xmltv.xml');
|
||||
|
||||
|
||||
let xmltvSettings = db['xmltv-settings'].find()[0];
|
||||
const fileContent = await fs.readFileSync(xmltvSettings.file, 'utf8');
|
||||
const fileFinal = fileContent.replace(/\{\{host\}\}/g, host);
|
||||
res.send(fileFinal);
|
||||
} catch(err) {
|
||||
console.error(err);
|
||||
res.status(500).send("error");
|
||||
@ -571,6 +583,7 @@ function api(db, channelDB, fillerDB, xmltvInterval, guideService ) {
|
||||
//tool services
|
||||
router.post('/api/channel-tools/time-slots', async (req, res) => {
|
||||
try {
|
||||
await m3uService.clearCache();
|
||||
let toolRes = await timeSlotsService(req.body.programs, req.body.schedule);
|
||||
if ( typeof(toolRes.userError) !=='undefined') {
|
||||
return res.status(400).send(toolRes.userError);
|
||||
@ -585,22 +598,13 @@ function api(db, channelDB, fillerDB, xmltvInterval, guideService ) {
|
||||
// CHANNELS.M3U Download
|
||||
router.get('/api/channels.m3u', async (req, res) => {
|
||||
try {
|
||||
res.type('text')
|
||||
let channels = await channelDB.getAllChannels();
|
||||
channels.sort((a, b) => { return a.number < b.number ? -1 : 1 })
|
||||
let tvg = `${req.protocol}://${req.get('host')}/api/xmltv.xml`
|
||||
var 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 += `${req.protocol}://${req.get('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="https://raw.githubusercontent.com/vexorian/dizquetv/main/resources/dizquetv.png" group-title="dizqueTV",dizqueTV\n`
|
||||
data += `${req.protocol}://${req.get('host')}/setup\n`
|
||||
}
|
||||
res.send(data)
|
||||
res.type('text');
|
||||
|
||||
const host = `${req.protocol}://${req.get('host')}`;
|
||||
const data = await m3uService.getChannelList(host);
|
||||
|
||||
res.send(data);
|
||||
|
||||
} catch(err) {
|
||||
console.error(err);
|
||||
res.status(500).send("error");
|
||||
@ -611,28 +615,22 @@ 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')
|
||||
let channels = await channelDB.getAllChannels();
|
||||
channels.sort((a, b) => { return a.number < b.number ? -1 : 1 })
|
||||
var 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 += `${req.protocol}://${req.get('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="https://raw.githubusercontent.com/vexorian/dizquetv/main/resources/dizquetv.png" group-title="dizqueTV",dizqueTV\n`
|
||||
data += `${req.protocol}://${req.get('host')}/setup\n`
|
||||
}
|
||||
res.send(data)
|
||||
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()
|
||||
xmltvInterval.restartInterval()
|
||||
|
||||
164
src/cache-image-service.js
Normal file
164
src/cache-image-service.js
Normal file
@ -0,0 +1,164 @@
|
||||
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.
|
||||
*
|
||||
* @class CacheImageService
|
||||
*/
|
||||
class CacheImageService {
|
||||
constructor() {
|
||||
this.cacheService = CacheService;
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Router interceptor to download image and update cache before pass to express.static return this cached image.
|
||||
*
|
||||
* GET /:hash - Hash is a full external URL encoded in base64.
|
||||
* eg.: http://{host}/cache/images/aHR0cHM6Ly8xO...cXVUbmFVNDZQWS1LWQ==
|
||||
*
|
||||
* @returns
|
||||
* @memberof CacheImageService
|
||||
*/
|
||||
routerInterceptor() {
|
||||
const router = express.Router();
|
||||
|
||||
router.get('/:hash', async (req, res, next) => {
|
||||
try {
|
||||
const hash = req.params.hash;
|
||||
const imgItem = this.db.find({url: hash})[0];
|
||||
if(imgItem) {
|
||||
const file = await this.getImageFromCache(imgItem.url);
|
||||
if(!file.length) {
|
||||
const fileMimeType = await this.requestImageAndStore(Buffer.from(imgItem.url, 'base64').toString('ascii'), imgItem);
|
||||
res.set('content-type', fileMimeType);
|
||||
next();
|
||||
} else {
|
||||
res.set('content-type', imgItem.mimeType);
|
||||
next();
|
||||
}
|
||||
}
|
||||
} catch(err) {
|
||||
console.error(err);
|
||||
res.status(500).send("error");
|
||||
}
|
||||
});
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Routers exported to use on express.use() function.
|
||||
* Use on api routers, like `{host}/api/cache/images`
|
||||
*
|
||||
* `DELETE /` - Clear all files on .dizquetv/cache/images
|
||||
*
|
||||
* @returns {Router}
|
||||
* @memberof CacheImageService
|
||||
*/
|
||||
apiRouters() {
|
||||
const router = express.Router();
|
||||
|
||||
router.delete('/', async (req, res, next) => {
|
||||
try {
|
||||
await this.clearCache();
|
||||
res.status(200).send({msg: 'Cache Image are Cleared'});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
res.status(500).send("error");
|
||||
}
|
||||
});
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @param {*} url External URL to get file/image
|
||||
* @param {*} dbFile register of file from db
|
||||
* @returns {promise} `Resolve` when can download imagem and store on cache folder, `Reject` when file are inaccessible over network or can't write on directory
|
||||
* @memberof CacheImageService
|
||||
*/
|
||||
async requestImageAndStore(url, dbFile) {
|
||||
return new Promise( async(resolve, reject) => {
|
||||
const requestConfiguration = {
|
||||
method: 'get',
|
||||
url
|
||||
};
|
||||
|
||||
request(requestConfiguration, (err, res) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
const mimeType = res.headers['content-type'];
|
||||
this.db.update({_id: dbFile._id}, {url: dbFile.url, mimeType});
|
||||
request(requestConfiguration)
|
||||
.pipe(fs.createWriteStream(`${this.cacheService.cachePath}/${this.imageCacheFolder}/${dbFile.url}`))
|
||||
.on('close', () =>{
|
||||
resolve(mimeType);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get image from cache using an filename
|
||||
*
|
||||
* @param {*} fileName
|
||||
* @returns {promise} `Resolve` with file content
|
||||
* @memberof CacheImageService
|
||||
*/
|
||||
getImageFromCache(fileName) {
|
||||
return new Promise(async(resolve, reject) => {
|
||||
try {
|
||||
const file = await this.cacheService.getCache(`${this.imageCacheFolder}/${fileName}`);
|
||||
resolve(file);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all files on .dizquetv/cache/images
|
||||
*
|
||||
* @returns {promise}
|
||||
* @memberof CacheImageService
|
||||
*/
|
||||
async clearCache() {
|
||||
return new Promise( async(resolve, reject) => {
|
||||
const cachePath = `${this.cacheService.cachePath}/${this.imageCacheFolder}`;
|
||||
fs.rmdir(cachePath, { recursive: true }, (err) => {
|
||||
if(err) {
|
||||
reject();
|
||||
}
|
||||
fs.mkdirSync(cachePath);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
registerImageOnDatabase(imageUrl) {
|
||||
const url = Buffer.from(imageUrl).toString('base64');
|
||||
const dbQuery = {url};
|
||||
if(!this.db.find(dbQuery)[0]) {
|
||||
this.db.save(dbQuery);
|
||||
}
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new CacheImageService();
|
||||
94
src/cache-service.js
Normal file
94
src/cache-service.js
Normal file
@ -0,0 +1,94 @@
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const config = require('./config');
|
||||
|
||||
/**
|
||||
* A File Cache controller for store and retrieve files from disk
|
||||
*
|
||||
* @class CacheService
|
||||
*/
|
||||
class CacheService {
|
||||
constructor() {
|
||||
this.cachePath = path.join(config.DATABASE, 'cache');
|
||||
this.cache = {};
|
||||
}
|
||||
|
||||
/**
|
||||
* `save` a file on cache folder
|
||||
*
|
||||
* @param {string} fullFilePath
|
||||
* @param {*} data
|
||||
* @returns {promise}
|
||||
* @memberof CacheService
|
||||
*/
|
||||
setCache(fullFilePath, data) {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
const file = fs.createWriteStream(path.join(this.cachePath, fullFilePath));
|
||||
file.write(data, (err) => {
|
||||
if(err) {
|
||||
throw Error("Can't save file: ", err);
|
||||
} else {
|
||||
this.cache[fullFilePath] = data;
|
||||
resolve(true);
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* `get` a File from cache folder
|
||||
*
|
||||
* @param {string} fullFilePath
|
||||
* @returns {promise} `Resolve` with file content, `Reject` with false
|
||||
* @memberof CacheService
|
||||
*/
|
||||
getCache(fullFilePath) {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
if(fullFilePath in this.cache) {
|
||||
resolve(this.cache[fullFilePath]);
|
||||
} else {
|
||||
fs.readFile(path.join(this.cachePath, fullFilePath), 'utf8', function (err,data) {
|
||||
if (err) {
|
||||
resolve(false);
|
||||
}
|
||||
resolve(data);
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
resolve(false);
|
||||
throw Error("Can't get file", error)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* `delete` a File from cache folder
|
||||
*
|
||||
* @param {string} fullFilePath
|
||||
* @returns {promise}
|
||||
* @memberof CacheService
|
||||
*/
|
||||
deleteCache(fullFilePath) {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
fs.unlinkSync(path.join(this.cachePath, fullFilePath), (err) => {
|
||||
if(err) {
|
||||
throw Error("Can't save file: ", err);
|
||||
} else {
|
||||
delete this.cache[fullFilePath];
|
||||
resolve(true);
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new CacheService();
|
||||
4
src/config.js
Normal file
4
src/config.js
Normal file
@ -0,0 +1,4 @@
|
||||
module.exports = {
|
||||
DATABASE: process.env.DATABASE || './.dizquetv',
|
||||
PORT: process.env.PORT || 8000,
|
||||
};
|
||||
161
src/services/m3u-service.js
Normal file
161
src/services/m3u-service.js
Normal file
@ -0,0 +1,161 @@
|
||||
const CacheService = require('../cache-service');
|
||||
const SettingsService = require('./settings-service');
|
||||
|
||||
/**
|
||||
* Manager and Generate M3U content
|
||||
*
|
||||
* @class M3uService
|
||||
*/
|
||||
class M3uService {
|
||||
constructor(dataBase) {
|
||||
this.dataBase = dataBase;
|
||||
this.cacheService = CacheService;
|
||||
SettingsService.saveSetting('enabled-cache-m3u', 'Enable Cache M3U', false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the channel list in HLS or M3U
|
||||
*
|
||||
* @param {string} [type='m3u'] List type
|
||||
* @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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build M3U with cache
|
||||
*
|
||||
* @param {string} host
|
||||
* @returns {promise} M3U file content
|
||||
* @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);
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* "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.
|
||||
*
|
||||
* @param {*} host
|
||||
* @param {*} data
|
||||
* @returns
|
||||
* @memberof M3uService
|
||||
*/
|
||||
replaceHostOnM3u(host, data) {
|
||||
return data.replace(/\{\{host\}\}/g, host);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear channels.m3u file from cache folder.
|
||||
*
|
||||
* @memberof M3uService
|
||||
*/
|
||||
async clearCache() {
|
||||
await this.cacheService.deleteCache('channels.m3u');
|
||||
await this.cacheService.deleteCache('channels-hls.m3u');
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = M3uService;
|
||||
114
src/services/settings-service.js
Normal file
114
src/services/settings-service.js
Normal file
@ -0,0 +1,114 @@
|
||||
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();
|
||||
@ -351,7 +351,7 @@ class TVGuideService
|
||||
|
||||
async refreshXML() {
|
||||
let xmltvSettings = this.db['xmltv-settings'].find()[0];
|
||||
await this.xmltv.WriteXMLTV(this.cached, xmltvSettings, async() => await this._throttle() );
|
||||
await this.xmltv.WriteXMLTV(this.cached, xmltvSettings, async() => await this._throttle(), this.db);
|
||||
}
|
||||
|
||||
async getStatus() {
|
||||
|
||||
13
src/xmltv.js
13
src/xmltv.js
@ -1,12 +1,14 @@
|
||||
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) {
|
||||
if (isShutdown) {
|
||||
return;
|
||||
}
|
||||
@ -112,7 +114,14 @@ async function _writeProgramme(channel, program, xw) {
|
||||
// Icon
|
||||
if (typeof program.icon !== 'undefined') {
|
||||
xw.startElement('icon')
|
||||
xw.writeAttribute('src', program.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.endElement()
|
||||
}
|
||||
// Desc
|
||||
|
||||
@ -15,6 +15,7 @@ app.directive('plexSettings', require('./directives/plex-settings'))
|
||||
app.directive('ffmpegSettings', require('./directives/ffmpeg-settings'))
|
||||
app.directive('xmltvSettings', require('./directives/xmltv-settings'))
|
||||
app.directive('hdhrSettings', require('./directives/hdhr-settings'))
|
||||
app.directive('cacheSettings', require('./directives/cache-settings'))
|
||||
app.directive('plexLibrary', require('./directives/plex-library'))
|
||||
app.directive('programConfig', require('./directives/program-config'))
|
||||
app.directive('flexConfig', require('./directives/flex-config'))
|
||||
|
||||
23
web/directives/cache-settings.js
Normal file
23
web/directives/cache-settings.js
Normal file
@ -0,0 +1,23 @@
|
||||
module.exports = function (dizquetv) {
|
||||
return {
|
||||
restrict: 'E',
|
||||
templateUrl: 'templates/cache-settings.html',
|
||||
replace: true,
|
||||
scope: {
|
||||
},
|
||||
link: function (scope, element, attrs) {
|
||||
dizquetv.getAllSettings().then((settings) => {
|
||||
console.warn(settings);
|
||||
scope.settings = settings;
|
||||
scope.$apply();
|
||||
});
|
||||
scope.updateSetting = (setting) => {
|
||||
const {key, value} = setting;
|
||||
dizquetv.putSetting(key, !value).then((response) => {
|
||||
scope.settings = response;
|
||||
scope.$apply();
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
15
web/public/templates/cache-settings.html
Normal file
15
web/public/templates/cache-settings.html
Normal file
@ -0,0 +1,15 @@
|
||||
<div>
|
||||
<h5>Cache</h5>
|
||||
|
||||
<div class="options col-sm-4">
|
||||
<div class="option row" ng-repeat="setting in settings track by setting.key">
|
||||
<div class="option__title col-sm-6">{{setting.title}}</div>
|
||||
<div class="option__action col-sm-6">
|
||||
<button class="pull-right btn btn-sm" ng-class="[{'btn-danger': setting.value, 'btn-success': !setting.value}]" ng-click="updateSetting(setting)">
|
||||
{{(setting.value) ? 'Disable' : 'Enable'}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@ -20,10 +20,16 @@
|
||||
HDHR
|
||||
</span>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<span class="nav-link btn btn-link {{ selected === 'cache' ? 'active' : ''}}" ng-click="selected = 'cache'">
|
||||
Cache
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
<br />
|
||||
<plex-settings ng-if="selected == 'plex'"></plex-settings>
|
||||
<ffmpeg-settings ng-if="selected == 'ffmpeg'"></ffmpeg-settings>
|
||||
<xmltv-settings ng-if="selected == 'xmltv'"></xmltv-settings>
|
||||
<cache-settings ng-if="selected == 'cache'"></cache-settings>
|
||||
<hdhr-settings ng-if="selected == 'hdhr'"></hdhr-settings>
|
||||
</div>
|
||||
@ -1,4 +1,4 @@
|
||||
module.exports = function ($http) {
|
||||
module.exports = function ($http, $q) {
|
||||
return {
|
||||
getVersion: () => {
|
||||
return $http.get('/api/version').then((d) => { return d.data })
|
||||
@ -264,8 +264,47 @@ module.exports = function ($http) {
|
||||
headers: { 'Content-Type': 'application/json; charset=utf-8' },
|
||||
} );
|
||||
return d.data;
|
||||
}
|
||||
},
|
||||
|
||||
/*======================================================================
|
||||
* Settings
|
||||
*/
|
||||
getAllSettings: async () => {
|
||||
var deferred = $q.defer();
|
||||
$http({
|
||||
method: "GET",
|
||||
url : "/api/settings/cache",
|
||||
headers: { 'Content-Type': 'application/json; charset=utf-8' },
|
||||
}).then((response) => {
|
||||
if(response.status === 200) {
|
||||
deferred.resolve(response.data);
|
||||
} else {
|
||||
deferred.reject();
|
||||
}
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
},
|
||||
putSetting: async (key, value) => {
|
||||
console.warn(key, value);
|
||||
var deferred = $q.defer();
|
||||
$http({
|
||||
method: "PUT",
|
||||
url : `/api/settings/cache/${key}`,
|
||||
data: {
|
||||
value
|
||||
},
|
||||
headers: { 'Content-Type': 'application/json; charset=utf-8' },
|
||||
}).then((response) => {
|
||||
if(response.status === 200) {
|
||||
deferred.resolve(response.data);
|
||||
} else {
|
||||
deferred.reject();
|
||||
}
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user