diff --git a/README.md b/README.md index 804fb2d..79c06a4 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# dizqueTV 1.5.3 +# dizqueTV 1.5.4 ![Discord](https://img.shields.io/discord/711313431457693727?logo=discord&logoColor=fff&style=flat-square) ![GitHub top language](https://img.shields.io/github/languages/top/vexorian/dizquetv?logo=github&style=flat-square) ![Docker Pulls](https://img.shields.io/docker/pulls/vexorian/dizquetv?logo=docker&logoColor=fff&style=flat-square) Create live TV channel streams from media on your Plex servers. @@ -33,6 +33,7 @@ EPG (Guide Information) data is stored to `.dizquetv/xmltv.xml` - dizqueTV does not currently watch your Plex server for media updates/changes. You must manually remove and re-add your programs for any changes to take effect. Same goes for Plex server changes (changing IP, port, etc).. You'll have to update the server settings manually in that case. - Most players (including Plex) will break after switching episodes if video / audio format is too different. dizqueTV can be configured to use ffmpeg transcoding to prevent this, but that costs resources. - If you configure Plex DVR, it will always be recording and transcoding the channel's contents. +- In its current state, dizquetv is intended for private use only and you should be discouraged from running dizqueTV in any capacity where other users can have access to dizqueTV's ports. You can use Plex's iptv player feature to share dizqueTV streams or you'll have to come up with some work arounds to make sure that streams can be played without ever actually exposing the dizquetv port to the outside world. Please use it with care, consider exposing dizqueTV's ports as something only advanced users who know what they are doing should try. ## Releases diff --git a/index.js b/index.js index cb8f60b..e520a6d 100644 --- a/index.js +++ b/index.js @@ -31,6 +31,7 @@ const OnDemandService = require("./src/services/on-demand-service"); const ProgrammingService = require("./src/services/programming-service"); const ActiveChannelService = require('./src/services/active-channel-service') const ProgramPlayTimeDB = require('./src/dao/program-play-time-db') +const FfmpegSettingsService = require('./src/services/ffmpeg-settings-service') const onShutdown = require("node-graceful-shutdown").onShutdown; @@ -50,12 +51,15 @@ if (NODE < 12) { console.error(`WARNING: Your nodejs version ${process.version} is lower than supported. dizqueTV has been tested best on nodejs 12.16.`); } - +process.env.unlock = false; for (let i = 0, l = process.argv.length; i < l; i++) { if ((process.argv[i] === "-p" || process.argv[i] === "--port") && i + 1 !== l) process.env.PORT = process.argv[i + 1] if ((process.argv[i] === "-d" || process.argv[i] === "--database") && i + 1 !== l) process.env.DATABASE = process.argv[i + 1] + if (process.argv[i] === "--unlock") { + process.env.unlock = true; + } } process.env.DATABASE = process.env.DATABASE || path.join(".", ".dizquetv") @@ -101,6 +105,7 @@ channelService = new ChannelService(channelDB); fillerDB = new FillerDB( path.join(process.env.DATABASE, 'filler') , channelService ); customShowDB = new CustomShowDB( path.join(process.env.DATABASE, 'custom-shows') ); let programPlayTimeDB = new ProgramPlayTimeDB( path.join(process.env.DATABASE, 'play-cache') ); +let ffmpegSettingsService = new FfmpegSettingsService(db, process.env.unlock); async function initializeProgramPlayTimeDB() { try { @@ -284,7 +289,7 @@ app.use('/favicon.svg', express.static( app.use('/custom.css', express.static(path.join(process.env.DATABASE, 'custom.css'))) // API Routers -app.use(api.router(db, channelService, fillerDB, customShowDB, xmltvInterval, guideService, m3uService, eventService )) +app.use(api.router(db, channelService, fillerDB, customShowDB, xmltvInterval, guideService, m3uService, eventService, ffmpegSettingsService)) app.use('/api/cache/images', cacheImageService.apiRouters()) app.use('/' + fontAwesome, express.static(path.join(process.env.DATABASE, fontAwesome))) app.use('/' + bootstrap, express.static(path.join(process.env.DATABASE, bootstrap))) diff --git a/src/api.js b/src/api.js index dfe10ef..7d21f4d 100644 --- a/src/api.js +++ b/src/api.js @@ -2,7 +2,6 @@ const express = require('express') const path = require('path') const fs = require('fs') -const databaseMigration = require('./database-migration'); const constants = require('./constants'); const JSONStream = require('JSONStream'); const FFMPEGInfo = require('./ffmpeg-info'); @@ -25,7 +24,7 @@ function safeString(object) { } module.exports = { router: api } -function api(db, channelService, fillerDB, customShowDB, xmltvInterval, guideService, _m3uService, eventService ) { +function api(db, channelService, fillerDB, customShowDB, xmltvInterval, guideService, _m3uService, eventService, ffmpegSettingsService ) { let m3uService = _m3uService; const router = express.Router() const plexServerDB = new PlexServerDB(channelService, fillerDB, customShowDB, db); @@ -529,7 +528,7 @@ function api(db, channelService, fillerDB, customShowDB, xmltvInterval, guideSe // FFMPEG SETTINGS router.get('/api/ffmpeg-settings', (req, res) => { try { - let ffmpeg = db['ffmpeg-settings'].find()[0] + let ffmpeg = ffmpegSettingsService.get(); res.send(ffmpeg) } catch(err) { console.error(err); @@ -538,9 +537,9 @@ function api(db, channelService, fillerDB, customShowDB, xmltvInterval, guideSe }) router.put('/api/ffmpeg-settings', (req, res) => { try { - db['ffmpeg-settings'].update({ _id: req.body._id }, req.body) - let ffmpeg = db['ffmpeg-settings'].find()[0] - let err = fixupFFMPEGSettings(ffmpeg); + let result = ffmpegSettingsService.update(req.body); + let err = result.error + if (typeof(err) !== 'undefined') { return res.status(400).send(err); } @@ -555,7 +554,7 @@ function api(db, channelService, fillerDB, customShowDB, xmltvInterval, guideSe "level" : "info" } ); - res.send(ffmpeg) + res.send(result.ffmpeg) } catch(err) { console.error(err); res.status(500).send("error"); @@ -576,10 +575,8 @@ function api(db, channelService, fillerDB, customShowDB, xmltvInterval, guideSe }) router.post('/api/ffmpeg-settings', (req, res) => { // RESET try { - let ffmpeg = databaseMigration.defaultFFMPEG() ; - ffmpeg.ffmpegPath = req.body.ffmpegPath; - db['ffmpeg-settings'].update({ _id: req.body._id }, ffmpeg) - ffmpeg = db['ffmpeg-settings'].find()[0] + let ffmpeg = ffmpegSettingsService.reset(); + eventService.push( "settings-update", { @@ -612,14 +609,6 @@ function api(db, channelService, fillerDB, customShowDB, xmltvInterval, guideSe }) - function fixupFFMPEGSettings(ffmpeg) { - if (typeof(ffmpeg.maxFPS) === 'undefined') { - ffmpeg.maxFPS = 60; - } else if ( isNaN(ffmpeg.maxFPS) ) { - return "maxFPS should be a number"; - } - } - // PLEX SETTINGS router.get('/api/plex-settings', (req, res) => { try { diff --git a/src/constants.js b/src/constants.js index 0a2d664..b4ec30f 100644 --- a/src/constants.js +++ b/src/constants.js @@ -35,5 +35,5 @@ module.exports = { // staying active, it checks every 5 seconds PLAYED_MONITOR_CHECK_FREQUENCY: 5*1000, - VERSION_NAME: "1.5.3" + VERSION_NAME: "1.5.4" } diff --git a/src/database-migration.js b/src/database-migration.js index 3eada1f..f026639 100644 --- a/src/database-migration.js +++ b/src/database-migration.js @@ -20,7 +20,8 @@ const path = require('path'); var fs = require('fs'); -const TARGET_VERSION = 803; +const TARGET_VERSION = 804; +const DAY_MS = 1000 * 60 * 60 * 24; const STEPS = [ // [v, v2, x] : if the current version is v, call x(db), and version becomes v2 @@ -43,6 +44,7 @@ const STEPS = [ [ 800, 801, (db) => addImageCache(db) ], [ 801, 802, () => addGroupTitle() ], [ 802, 803, () => fixNonIntegerDurations() ], + [ 803, 804, (db) => addFFMpegLock(db) ], ] const { v4: uuidv4 } = require('uuid'); @@ -384,7 +386,7 @@ function ffmpeg() { //How default ffmpeg settings should look configVersion: 5, ffmpegPath: "/usr/bin/ffmpeg", - threads: 4, + pathLockDate: new Date().getTime() + DAY_MS, concatMuxDelay: "0", logFfmpeg: false, enableFFMPEGTranscoding: true, @@ -765,6 +767,19 @@ function addScalingAlgorithm(db) { fs.writeFileSync( f, JSON.stringify( [ffmpegSettings] ) ); } +function addFFMpegLock(db) { + let ffmpegSettings = db['ffmpeg-settings'].find()[0]; + let f = path.join(process.env.DATABASE, 'ffmpeg-settings.json'); + if ( typeof(ffmpegSettings.pathLockDate) === 'undefined' || ffmpegSettings.pathLockDate == null ) { + + console.log("Adding ffmpeg lock. For your security it will not be possible to modify the ffmpeg path using the UI anymore unless you launch dizquetv by following special instructions.."); + // We are migrating an existing db that had a ffmpeg path. Make sure + // it's already locked. + ffmpegSettings.pathLockDate = new Date().getTime() - 2 * DAY_MS; + } + fs.writeFileSync( f, JSON.stringify( [ffmpegSettings] ) ); +} + function moveBackup(path) { if (fs.existsSync(`${process.env.DATABASE}${path}`) ) { let i = 0; diff --git a/src/ffmpeg-info.js b/src/ffmpeg-info.js index 00cb1c9..e7dd7f0 100644 --- a/src/ffmpeg-info.js +++ b/src/ffmpeg-info.js @@ -18,7 +18,7 @@ class FFMPEGInfo { var m = s.match( /version\s+([^\s]+)\s+.*Copyright/ ) if (m == null) { console.error("ffmpeg -version command output not in the expected format: " + s); - return s; + return "Unknown"; } return m[1]; } catch (err) { diff --git a/src/services/ffmpeg-settings-service.js b/src/services/ffmpeg-settings-service.js new file mode 100644 index 0000000..7d1ddb9 --- /dev/null +++ b/src/services/ffmpeg-settings-service.js @@ -0,0 +1,123 @@ +const databaseMigration = require('../database-migration'); +const DAY_MS = 1000 * 60 * 60 * 24; +const path = require('path'); +const fs = require('fs'); + +class FfmpegSettingsService { + constructor(db, unlock) { + this.db = db; + if (unlock) { + this.unlock(); + } + } + + get() { + let ffmpeg = this.getCurrentState(); + if (isLocked(ffmpeg)) { + ffmpeg.lock = true; + } + // Hid this info from the API + delete ffmpeg.pathLockDate; + return ffmpeg; + } + + unlock() { + let ffmpeg = this.getCurrentState(); + console.log("ffmpeg path UI unlocked for another day..."); + ffmpeg.pathLockDate = new Date().getTime() + DAY_MS; + this.db['ffmpeg-settings'].update({ _id: ffmpeg._id }, ffmpeg) + } + + + update(attempt) { + let ffmpeg = this.getCurrentState(); + attempt.pathLockDate = ffmpeg.pathLockDate; + if (isLocked(ffmpeg)) { + console.log("Note: ffmpeg path is not being updated since it's been locked for your security."); + attempt.ffmpegPath = ffmpeg.ffmpegPath; + if (typeof(ffmpeg.pathLockDate) === 'undefined') { + // make sure to lock it even if it was undefined + attempt.pathLockDate = new Date().getTime() - DAY_MS; + } + } else if (attempt.addLock === true) { + // lock it right now + attempt.pathLockDate = new Date().getTime() - DAY_MS; + } else { + attempt.pathLockDate = new Date().getTime() + DAY_MS; + } + delete attempt.addLock; + delete attempt.lock; + + let err = fixupFFMPEGSettings(attempt); + if ( typeof(err) !== "undefined" ) { + return { + error: err + } + } + + this.db['ffmpeg-settings'].update({ _id: ffmpeg._id }, attempt) + return { + ffmpeg: this.get() + } + } + + reset() { + // Even if reseting, it's impossible to unlock the ffmpeg path + let ffmpeg = databaseMigration.defaultFFMPEG() ; + this.update(ffmpeg); + return this.get(); + } + + getCurrentState() { + return this.db['ffmpeg-settings'].find()[0] + } + + +} + +function fixupFFMPEGSettings(ffmpeg) { + if (typeof(ffmpeg.ffmpegPath) !== 'string') { + return "ffmpeg path is required." + } + if (! isValidFilePath(ffmpeg.ffmpegPath)) { + return "ffmpeg path must be a valid file path." + } + + if (typeof(ffmpeg.maxFPS) === 'undefined') { + ffmpeg.maxFPS = 60; + return null; + } else if ( isNaN(ffmpeg.maxFPS) ) { + return "maxFPS should be a number"; + } +} + +//These checks are good but might not be enough, as long as we are letting the +//user choose any path and we are making dizqueTV execute, it is too risky, +//hence why we are also adding the lock feature on top of these checks. +function isValidFilePath(filePath) { + const normalizedPath = path.normalize(filePath); + + if (!path.isAbsolute(normalizedPath)) { + return false; + } + + try { + const stats = fs.statSync(normalizedPath); + return stats.isFile(); + } catch (err) { + // Handle potential errors (e.g., file not found, permission issues) + if (err.code === 'ENOENT') { + return false; // File does not exist + } else { + throw err; // Re-throw other errors for debugging + } + } +} + +function isLocked(ffmpeg) { + return isNaN(ffmpeg.pathLockDate) || ffmpeg.pathLockDate < new Date().getTime(); +} + + + +module.exports =FfmpegSettingsService; \ No newline at end of file diff --git a/web/directives/ffmpeg-settings.js b/web/directives/ffmpeg-settings.js index 52c507d..e474097 100644 --- a/web/directives/ffmpeg-settings.js +++ b/web/directives/ffmpeg-settings.js @@ -11,8 +11,13 @@ module.exports = function (dizquetv, resolutionOptions) { scope.settings = settings }) scope.updateSettings = (settings) => { + delete scope.settingsError; dizquetv.updateFfmpegSettings(settings).then((_settings) => { scope.settings = _settings + }).catch( (err) => { + if ( typeof(err.data) === "string") { + scope.settingsError = err.data; + } }) } scope.resetSettings = (settings) => { diff --git a/web/public/templates/ffmpeg-settings.html b/web/public/templates/ffmpeg-settings.html index 6f4ae84..bcaa3f7 100644 --- a/web/public/templates/ffmpeg-settings.html +++ b/web/public/templates/ffmpeg-settings.html @@ -1,4 +1,8 @@