From 5f4ef4386dc007ff498a5d68d496da8bb893b92d Mon Sep 17 00:00:00 2001 From: Rafael Vieira Date: Sun, 10 Jan 2021 18:44:18 -0300 Subject: [PATCH 01/57] Removed unnecessary spaces on package.json --- package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index a5498d0..ce92890 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "angular": "^1.7.9", "angular-router-browserify": "0.0.2", "angular-vs-repeat": "2.0.13", - "random-js" : "2.1.0", + "random-js": "2.1.0", "axios": "^0.19.2", "body-parser": "^1.19.0", "diskdb": "^0.1.17", @@ -27,7 +27,7 @@ "node-ssdp": "^4.0.0", "request": "^2.88.2", "uuid": "^8.0.0", - "node-graceful-shutdown" : "1.1.0", + "node-graceful-shutdown": "1.1.0", "xml-writer": "^1.7.0" }, "bin": "dist/index.js", @@ -41,7 +41,7 @@ "del-cli": "^3.0.0", "nodemon": "^2.0.3", "watchify": "^3.11.1", - "nexe" : "^3.3.7" + "nexe": "^3.3.7" }, "babel": { "plugins": [ From 73cc9fb772cd47e943d30967ce3c826f4dd65640 Mon Sep 17 00:00:00 2001 From: Rafael Vieira Date: Sun, 10 Jan 2021 18:48:57 -0300 Subject: [PATCH 02/57] Fixed browser errors on Plex configuration --- web/directives/plex-settings.js | 56 ++++++++++++++++++--------------- 1 file changed, 31 insertions(+), 25 deletions(-) diff --git a/web/directives/plex-settings.js b/web/directives/plex-settings.js index 353962c..f9714b1 100644 --- a/web/directives/plex-settings.js +++ b/web/directives/plex-settings.js @@ -18,14 +18,16 @@ module.exports = function (plex, dizquetv, $timeout) { let servers = await dizquetv.getPlexServers(); scope.serversPending = false; scope.servers = servers; - for (let i = 0; i < scope.servers.length; i++) { - scope.servers[i].uiStatus = 0; - scope.servers[i].backendStatus = 0; - let t = (new Date()).getTime(); - scope.servers[i].uiPending = t; - scope.servers[i].backendPending = t; - scope.refreshUIStatus(t, i); - scope.refreshBackendStatus(t, i); + if(servers) { + for (let i = 0; i < scope.servers.length; i++) { + scope.servers[i].uiStatus = 0; + scope.servers[i].backendStatus = 0; + let t = (new Date()).getTime(); + scope.servers[i].uiPending = t; + scope.servers[i].backendPending = t; + scope.refreshUIStatus(t, i); + scope.refreshBackendStatus(t, i); + } } setTimeout( () => { scope.$apply() }, 31000 ); scope.$apply(); @@ -51,13 +53,15 @@ module.exports = function (plex, dizquetv, $timeout) { scope.isAnyUIBad = () => { let t = (new Date()).getTime(); - for (let i = 0; i < scope.servers.length; i++) { - let s = scope.servers[i]; - if ( - (s.uiStatus == -1) - || ( (s.uiStatus == 0) && (s.uiPending + 30000 < t) ) - ) { - return true; + if(scope.servers) { + for (let i = 0; i < scope.servers.length; i++) { + let s = scope.servers[i]; + if ( + (s.uiStatus == -1) + || ( (s.uiStatus == 0) && (s.uiPending + 30000 < t) ) + ) { + return true; + } } } return false; @@ -65,13 +69,15 @@ module.exports = function (plex, dizquetv, $timeout) { scope.isAnyBackendBad = () => { let t = (new Date()).getTime(); - for (let i = 0; i < scope.servers.length; i++) { - let s = scope.servers[i]; - if ( - (s.backendStatus == -1) - || ( (s.backendStatus == 0) && (s.backendPending + 30000 < t) ) - ) { - return true; + if(scope.servers) { + for (let i = 0; i < scope.servers.length; i++) { + let s = scope.servers[i]; + if ( + (s.backendStatus == -1) + || ( (s.backendStatus == 0) && (s.backendPending + 30000 < t) ) + ) { + return true; + } } } return false; @@ -146,7 +152,7 @@ module.exports = function (plex, dizquetv, $timeout) { } scope.shouldDisableSubtitles = () => { - return scope.settings.forceDirectPlay || (scope.settings.streamPath === "direct" ); + return scope.settings && (scope.settings.forceDirectPlay || (scope.settings.streamPath === "direct" )); } scope.addPlexServer = async () => { @@ -216,10 +222,10 @@ module.exports = function (plex, dizquetv, $timeout) { {id:"direct",description:"Direct"} ]; scope.hideIfNotPlexPath = () => { - return scope.settings.streamPath != 'plex' + return scope.settings && scope.settings.streamPath != 'plex' }; scope.hideIfNotDirectPath = () => { - return scope.settings.streamPath != 'direct' + return scope.settings && scope.settings.streamPath != 'direct' }; scope.maxAudioChannelsOptions=[ {id:"1",description:"1.0"}, From dfac30b4ce684a42cd1610ed1a7e138d3948c0ab Mon Sep 17 00:00:00 2001 From: Rafael Vieira Date: Sun, 10 Jan 2021 18:50:47 -0300 Subject: [PATCH 03/57] Improvement of UX with a Plex Authorization Modal --- web/services/plex.js | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/web/services/plex.js b/web/services/plex.js index b80fa0e..102da3f 100644 --- a/web/services/plex.js +++ b/web/services/plex.js @@ -17,7 +17,21 @@ module.exports = function ($http, $window, $interval) { url: 'https://plex.tv/api/v2/pins?strong=true', headers: headers }).then((res) => { - $window.open('https://app.plex.tv/auth/#!?clientID=rg14zekk3pa5zp4safjwaa8z&context[device][version]=Plex OAuth&context[device][model]=Plex OAuth&code=' + res.data.code + '&context[device][product]=Plex Web') + const plexWindowSizes = { + width: 800, + height: 700 + } + + const plexWindowPosition = { + width: window.innerWidth / 2 + plexWindowSizes.width, + height: window.innerHeight / 2 - plexWindowSizes.height + } + + const authModal = $window.open( + `https://app.plex.tv/auth/#!?clientID=rg14zekk3pa5zp4safjwaa8z&context[device][version]=Plex OAuth&context[device][model]=Plex OAuth&code=${res.data.code}&context[device][product]=Plex Web`, + "_blank", + `height=${plexWindowSizes.height}, width=${plexWindowSizes.width}, top=${plexWindowPosition.height}, left=${plexWindowPosition.width}` + ); let limit = 120000 // 2 minute time out limit let poll = 2000 // check every 2 seconds for token let interval = $interval(() => { @@ -29,11 +43,17 @@ module.exports = function ($http, $window, $interval) { limit -= poll if (limit <= 0) { $interval.cancel(interval) + if(authModal) { + authModal.close(); + } reject('Timed Out. Failed to sign in a timely manner (2 mins)') } if (r2.data.authToken !== null) { $interval.cancel(interval) - + if(authModal) { + authModal.close(); + } + headers['X-Plex-Token'] = r2.data.authToken $http({ @@ -63,6 +83,9 @@ module.exports = function ($http, $window, $interval) { } }, (err) => { $interval.cancel(interval) + if(authModal) { + authModal.close(); + } reject(err) }) From 9755d11689d46510378b65abdbde67004768b9f8 Mon Sep 17 00:00:00 2001 From: vexorian Date: Sun, 10 Jan 2021 18:39:00 -0400 Subject: [PATCH 04/57] Prepare 1.3.0 development --- README.md | 2 +- src/constants.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1f6777a..bef536a 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# dizqueTV 1.2.3-prerelease +# dizqueTV 1.3.0-prerelease ![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. diff --git a/src/constants.js b/src/constants.js index 174be43..bf45467 100644 --- a/src/constants.js +++ b/src/constants.js @@ -5,5 +5,5 @@ module.exports = { TVGUIDE_MAXIMUM_FLEX_DURATION : 6 * 60 * 60 * 1000, TOO_FREQUENT: 100, - VERSION_NAME: "1.2.3-prerelease" + VERSION_NAME: "1.3.0-prerelease" } From 6696d626fc0fc1732ab190b27bddbeb1165c3d1b Mon Sep 17 00:00:00 2001 From: Jason Dove Date: Thu, 14 Jan 2021 13:18:11 -0600 Subject: [PATCH 05/57] add ffmpeg deinterlace options --- src/database-migration.js | 10 +++++++++- src/ffmpeg.js | 6 ++++++ src/plexTranscoder.js | 1 + web/directives/ffmpeg-settings.js | 8 ++++++++ web/public/templates/ffmpeg-settings.html | 5 +++++ 5 files changed, 29 insertions(+), 1 deletion(-) diff --git a/src/database-migration.js b/src/database-migration.js index 1c6f0ef..0df83c7 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 = 701; +const TARGET_VERSION = 702; const STEPS = [ // [v, v2, x] : if the current version is v, call x(db), and version becomes v2 @@ -34,6 +34,7 @@ const STEPS = [ [ 600, 601, (db) => addFPS(db) ], [ 601, 700, (db) => migrateWatermark(db) ], [ 700, 701, (db) => addScalingAlgorithm(db) ], + [ 701, 702, (db) => addDeinterlaceFilter(db) ] ] const { v4: uuidv4 } = require('uuid'); @@ -397,6 +398,7 @@ function ffmpeg() { normalizeAudio: true, maxFPS: 60, scalingAlgorithm: "bicubic", + deinterlaceFilter: "none" } } @@ -755,6 +757,12 @@ function addScalingAlgorithm(db) { fs.writeFileSync( f, JSON.stringify( [ffmpegSettings] ) ); } +function addDeinterlaceFilter(db) { + let ffmpegSettings = db['ffmpeg-settings'].find()[0]; + let f = path.join(process.env.DATABASE, 'ffmpeg-settings.json'); + ffmpegSettings.deinterlaceFilter = "none"; + fs.writeFileSync( f, JSON.stringify( [ffmpegSettings] ) ); +} module.exports = { initDB: initDB, diff --git a/src/ffmpeg.js b/src/ffmpeg.js index b884c1c..f2403df 100644 --- a/src/ffmpeg.js +++ b/src/ffmpeg.js @@ -165,6 +165,12 @@ class FFMPEG extends events.EventEmitter { currentVideo ="[fpchange]"; } + // deinterlace if desired + if (streamStats.videoScanType == 'interlaced' && this.opts.deinterlaceFilter != 'none') { + videoComplex += `;${currentVideo}${this.opts.deinterlaceFilter}[deinterlaced]`; + currentVideo = "[deinterlaced]"; + } + // prepare input streams if ( typeof(streamUrl.errorTitle) !== 'undefined') { doOverlay = false; //never show icon in the error screen diff --git a/src/plexTranscoder.js b/src/plexTranscoder.js index 11f3def..ca40040 100644 --- a/src/plexTranscoder.js +++ b/src/plexTranscoder.js @@ -236,6 +236,7 @@ lang=en` // Rounding framerate avoids scenarios where // 29.9999999 & 30 don't match. ret.videoDecision = (typeof stream.decision === 'undefined') ? 'copy' : stream.decision; + ret.videoScanType = stream.scanType; } // Audio. Only look at stream being used if (stream["streamType"] == "2" && stream["selected"] == "1") { diff --git a/web/directives/ffmpeg-settings.js b/web/directives/ffmpeg-settings.js index f4f66c7..52c507d 100644 --- a/web/directives/ffmpeg-settings.js +++ b/web/directives/ffmpeg-settings.js @@ -66,6 +66,14 @@ module.exports = function (dizquetv, resolutionOptions) { {id: "lanczos", description: "lanczos"}, {id: "spline", description: "spline"}, ]; + scope.deinterlaceOptions = [ + {value: "none", description: "do not deinterlace"}, + {value: "bwdif=0", description: "bwdif send frame"}, + {value: "bwdif=1", description: "bwdif send field"}, + {value: "w3fdif", description: "w3fdif"}, + {value: "yadif=0", description: "yadif send frame"}, + {value: "yadif=1", description: "yadif send field"} + ]; } } diff --git a/web/public/templates/ffmpeg-settings.html b/web/public/templates/ffmpeg-settings.html index 71a66bb..09fb68f 100644 --- a/web/public/templates/ffmpeg-settings.html +++ b/web/public/templates/ffmpeg-settings.html @@ -103,6 +103,11 @@ ng-options="o.id as o.description for o in scalingOptions" > Scaling algorithm to use when the transcoder needs to change the video size. +
+ + + Deinterlace filter to use when video is interlaced. This is only needed when Plex transcoding is not used.
From 1b264b48b2d9882b8fa84053a3c2fe7707921d5d Mon Sep 17 00:00:00 2001 From: vexorian Date: Sun, 17 Jan 2021 12:21:43 -0400 Subject: [PATCH 06/57] Fix db-migration after merge --- src/database-migration.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/database-migration.js b/src/database-migration.js index b493f20..4a3f9dc 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 = 702; +const TARGET_VERSION = 800; const STEPS = [ // [v, v2, x] : if the current version is v, call x(db), and version becomes v2 @@ -35,7 +35,11 @@ const STEPS = [ [ 601, 700, (db) => migrateWatermark(db) ], [ 700, 701, (db) => addScalingAlgorithm(db) ], [ 701, 703, (db,channels,dir) => reAddIcon(dir) ], - [ 701, 702, (db) => addDeinterlaceFilter(db) ] + [ 703, 800, (db) => addDeinterlaceFilter(db) ], + // there was a bit of thing in which for a while 1.3.x migrated 701 to 702 using + // 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) ], ] const { v4: uuidv4 } = require('uuid'); @@ -399,7 +403,7 @@ function ffmpeg() { normalizeAudio: true, maxFPS: 60, scalingAlgorithm: "bicubic", - deinterlaceFilter: "none" + deinterlaceFilter: "none", } } From d40831a019856f21e68010651ea4ab3b057aeeca Mon Sep 17 00:00:00 2001 From: vexorian Date: Sun, 17 Jan 2021 16:19:45 -0400 Subject: [PATCH 07/57] Audio-only streams with /radio?channel=x endpoint --- src/ffmpeg.js | 76 +++++++++++++++++++++++++++++++------------ src/offline-player.js | 2 ++ src/plex-player.js | 2 ++ src/video.js | 26 +++++++++++---- 4 files changed, 79 insertions(+), 27 deletions(-) diff --git a/src/ffmpeg.js b/src/ffmpeg.js index 2ffd698..8fcd3cd 100644 --- a/src/ffmpeg.js +++ b/src/ffmpeg.js @@ -62,6 +62,10 @@ class FFMPEG extends events.EventEmitter { this.ensureResolution = this.opts.normalizeResolution; this.volumePercent = this.opts.audioVolumePercent; this.hasBeenKilled = false; + this.audioOnly = false; + } + setAudioOnly(audioOnly) { + this.audioOnly = audioOnly; } async spawnConcat(streamUrl) { return await this.spawn(streamUrl, undefined, undefined, undefined, true, false, undefined, true) @@ -108,10 +112,19 @@ class FFMPEG extends events.EventEmitter { `-threads`, isConcatPlaylist? 1 : this.opts.threads, `-fflags`, `+genpts+discardcorrupt+igndts`]; - if (limitRead === true) - ffmpegArgs.push(`-re`) - + if ( + (limitRead === true) + && + ( + (this.audioOnly !== true) + || + ( typeof(streamUrl.errorTitle) === 'undefined') + ) + ) { + ffmpegArgs.push(`-re`); + } + if (typeof startTime !== 'undefined') ffmpegArgs.push(`-ss`, startTime) @@ -186,6 +199,7 @@ class FFMPEG extends events.EventEmitter { iH = this.wantedH; } + if ( this.audioOnly !== true) { ffmpegArgs.push("-r" , "24"); if ( streamUrl.errorTitle == 'offline' ) { ffmpegArgs.push( @@ -233,6 +247,7 @@ class FFMPEG extends events.EventEmitter { inputFiles++; videoComplex = `;[${videoFile+1}:0]scale=${iW}:${iH}[videoy];[videoy]realtime[videox]`; } + } let durstr = `duration=${streamStats.duration}ms`; //silent audioComplex = `;aevalsrc=0:${durstr}[audioy]`; @@ -246,14 +261,24 @@ class FFMPEG extends events.EventEmitter { // 'size' in order to make the soundtrack actually loop audioComplex = `;[${inputFiles++}:a]aloop=loop=-1:size=2147483647[audioy]`; } - } else if (this.opts.errorAudio == 'whitenoise') { + } else if ( + (this.opts.errorAudio == 'whitenoise') + || + ( + !(this.opts.errorAudio == 'sine') + && + (this.audioOnly === true) //when it's in audio-only mode, silent stream is confusing for errors. + ) + ) { audioComplex = `;aevalsrc=random(0):${durstr}[audioy]`; this.volumePercent = Math.min(70, this.volumePercent); } else if (this.opts.errorAudio == 'sine') { audioComplex = `;sine=f=440:${durstr}[audioy]`; this.volumePercent = Math.min(70, this.volumePercent); } - ffmpegArgs.push('-pix_fmt' , 'yuv420p' ); + if ( this.audioOnly !== true ) { + ffmpegArgs.push('-pix_fmt' , 'yuv420p' ); + } audioComplex += ';[audioy]arealtime[audiox]'; currentVideo = "[videox]"; currentAudio = "[audiox]"; @@ -326,7 +351,7 @@ class FFMPEG extends events.EventEmitter { } // Channel watermark: - if (doOverlay) { + if (doOverlay && (this.audioOnly !== true) ) { var pW =watermark.width; var w = Math.round( pW * iW / 100.0 ); var mpHorz = watermark.horizontalMargin; @@ -368,7 +393,8 @@ class FFMPEG extends events.EventEmitter { currentAudio = '[boosted]'; } // Align audio is just the apad filter applied to audio stream - if (this.apad) { + if (this.apad && (this.audioOnly !== true) ) { + //it doesn't make much sense to pad audio when there is no video audioComplex += `;${currentAudio}apad=whole_dur=${streamStats.duration}ms[padded]`; currentAudio = '[padded]'; } else if (this.audioChannelsSampleRate) { @@ -389,11 +415,13 @@ class FFMPEG extends events.EventEmitter { } else { console.log(resizeMsg) } - if (currentVideo != '[video]') { - transcodeVideo = true; //this is useful so that it adds some lines below - filterComplex += videoComplex; - } else { - currentVideo = `${videoFile}:${videoIndex}`; + if (this.audioOnly !== true) { + if (currentVideo != '[video]') { + transcodeVideo = true; //this is useful so that it adds some lines below + filterComplex += videoComplex; + } else { + currentVideo = `${videoFile}:${videoIndex}`; + } } // same with audio: if (currentAudio != '[audio]') { @@ -410,15 +438,18 @@ class FFMPEG extends events.EventEmitter { ffmpegArgs.push('-shortest'); } } - + if (this.audioOnly !== true) { + ffmpegArgs.push( + '-map', currentVideo, + `-c:v`, (transcodeVideo ? this.opts.videoEncoder : 'copy'), + `-sc_threshold`, `1000000000`, + ); + } ffmpegArgs.push( - '-map', currentVideo, - '-map', currentAudio, - `-c:v`, (transcodeVideo ? this.opts.videoEncoder : 'copy'), - `-flags`, `cgop+ilme`, - `-sc_threshold`, `1000000000` + '-map', currentAudio, + `-flags`, `cgop+ilme`, ); - if ( transcodeVideo ) { + if ( transcodeVideo && (this.audioOnly !== true) ) { // add the video encoder flags ffmpegArgs.push( `-b:v`, `${this.opts.videoBitrate}k`, @@ -460,8 +491,11 @@ class FFMPEG extends events.EventEmitter { //Concat stream is simpler and should always copy the codec ffmpegArgs.push( `-probesize`, 32 /*`100000000`*/, - `-i`, streamUrl, - `-map`, `0:v`, + `-i`, streamUrl ); + if (this.audioOnly !== true) { + ffmpegArgs.push( `-map`, `0:v` ); + } + ffmpegArgs.push( `-map`, `0:${audioIndex}`, `-c`, `copy`, `-muxdelay`, this.opts.concatMuxDelay, diff --git a/src/offline-player.js b/src/offline-player.js index 802dd1f..38d846e 100644 --- a/src/offline-player.js +++ b/src/offline-player.js @@ -19,6 +19,7 @@ class OfflinePlayer { context.channel.offlineSoundtrack = undefined; } this.ffmpeg = new FFMPEG(context.ffmpegSettings, context.channel); + this.ffmpeg.setAudioOnly(this.context.audioOnly); } cleanUp() { @@ -55,6 +56,7 @@ class OfflinePlayer { ffmpeg.removeAllListeners('error'); ffmpeg.removeAllListeners('close'); ffmpeg = new FFMPEG(this.context.ffmpegSettings, this.context.channel); // Set the transcoder options + ffmpeg.setAudioOnly(this.context.audioOnly); ffmpeg.on('close', () => { emitter.emit('close'); }); diff --git a/src/plex-player.js b/src/plex-player.js index a6bce79..4874bbd 100644 --- a/src/plex-player.js +++ b/src/plex-player.js @@ -62,6 +62,7 @@ class PlexPlayer { this.plexTranscoder = plexTranscoder; let watermark = this.context.watermark; let ffmpeg = new FFMPEG(ffmpegSettings, channel); // Set the transcoder options + ffmpeg.setAudioOnly( this.context.audioOnly ); this.ffmpeg = ffmpeg; let streamDuration; if (typeof(lineupItem.streamDuration)!=='undefined') { @@ -104,6 +105,7 @@ class PlexPlayer { ffmpeg.removeAllListeners('error'); ffmpeg.removeAllListeners('close'); ffmpeg = new FFMPEG(ffmpegSettings, channel); // Set the transcoder options + ffmpeg.setAudioOnly(this.context.audioOnly); ffmpeg.on('close', () => { emitter.emit('close'); }); diff --git a/src/video.js b/src/video.js index d82073d..e658199 100644 --- a/src/video.js +++ b/src/video.js @@ -45,7 +45,7 @@ function video( channelDB , fillerDB, db) { }) }) // Continuously stream video to client. Leverage ffmpeg concat for piecing together videos - router.get('/video', async (req, res) => { + let concat = async (req, res, audioOnly) => { // Check if channel queried is valid if (typeof req.query.channel === 'undefined') { res.status(500).send("No Channel Specified") @@ -75,6 +75,7 @@ function video( channelDB , fillerDB, db) { console.log(`\r\nStream starting. Channel: ${channel.number} (${channel.name})`) let ffmpeg = new FFMPEG(ffmpegSettings, channel); // Set the transcoder options + ffmpeg.setAudioOnly(audioOnly); let stopped = false; function stop() { @@ -109,9 +110,16 @@ function video( channelDB , fillerDB, db) { }) let channelNum = parseInt(req.query.channel, 10) - let ff = await ffmpeg.spawnConcat(`http://localhost:${process.env.PORT}/playlist?channel=${channelNum}`); + let ff = await ffmpeg.spawnConcat(`http://localhost:${process.env.PORT}/playlist?channel=${channelNum}&audioOnly=${audioOnly}`); ff.pipe(res ); - }) + }; + router.get('/video', async(req, res) => { + return await concat(req, res, false); + } ); + router.get('/radio', async(req, res) => { + return await concat(req, res, true); + } ); + // Stream individual video to ffmpeg concat above. This is used by the server, NOT the client router.get('/stream', async (req, res) => { // Check if channel queried is valid @@ -119,6 +127,8 @@ function video( channelDB , fillerDB, db) { res.status(400).send("No Channel Specified") return } + let audioOnly = ("true" == req.query.audioOnly); + console.log(`/stream audioOnly=${audioOnly}`); let session = parseInt(req.query.session); let m3u8 = (req.query.m3u8 === '1'); let number = parseInt(req.query.channel); @@ -296,6 +306,7 @@ function video( channelDB , fillerDB, db) { channel: combinedChannel, db: db, m3u8: m3u8, + audioOnly : audioOnly, } let player = new ProgramPlayer(playerContext); @@ -416,6 +427,7 @@ function video( channelDB , fillerDB, db) { let ffmpegSettings = db['ffmpeg-settings'].find()[0] let sessionId = StreamCount++; + let audioOnly = ("true" == req.query.audioOnly); if ( (ffmpegSettings.enableFFMPEGTranscoding === true) @@ -423,12 +435,14 @@ function video( channelDB , fillerDB, db) { && (ffmpegSettings.normalizeAudioCodec === true) && (ffmpegSettings.normalizeResolution === true) && (ffmpegSettings.normalizeAudio === true) + && (audioOnly !== true) /* loading screen is pointless in audio mode (also for some reason it makes it fail when codec is aac, and I can't figure out why) */ ) { - data += `file 'http://localhost:${process.env.PORT}/stream?channel=${channelNum}&first=0&session=${sessionId}'\n`; + //loading screen + data += `file 'http://localhost:${process.env.PORT}/stream?channel=${channelNum}&first=0&session=${sessionId}&audioOnly=${audioOnly}'\n`; } - data += `file 'http://localhost:${process.env.PORT}/stream?channel=${channelNum}&first=1&session=${sessionId}'\n` + data += `file 'http://localhost:${process.env.PORT}/stream?channel=${channelNum}&first=1&session=${sessionId}&audioOnly=${audioOnly}'\n` for (var i = 0; i < maxStreamsToPlayInARow - 1; i++) { - data += `file 'http://localhost:${process.env.PORT}/stream?channel=${channelNum}&session=${sessionId}'\n` + data += `file 'http://localhost:${process.env.PORT}/stream?channel=${channelNum}&session=${sessionId}&audioOnly=${audioOnly}'\n` } res.send(data) From 3bf63be7684cf8af2d656b4b603e231933fc2e96 Mon Sep 17 00:00:00 2001 From: Rafael Vieira Date: Wed, 20 Jan 2021 00:30:46 -0300 Subject: [PATCH 08/57] Create a upload for channel logos --- index.js | 4 + package-lock.json | 1462 +++++++++++++++++----- package.json | 5 +- src/api.js | 26 + web/directives/channel-config.js | 8 + web/public/templates/channel-config.html | 6 + web/services/dizquetv.js | 8 + 7 files changed, 1171 insertions(+), 348 deletions(-) diff --git a/index.js b/index.js index 13af0b0..30fc1e4 100644 --- a/index.js +++ b/index.js @@ -4,6 +4,7 @@ const fs = require('fs') const path = require('path') const express = require('express') const bodyParser = require('body-parser') +const fileUpload = require('express-fileupload'); const api = require('./src/api') const dbMigration = require('./src/database-migration'); @@ -151,6 +152,9 @@ xmltvInterval.startInterval() let hdhr = HDHR(db, channelDB) let app = express() +app.use(fileUpload({ + createParentPath: true +})); app.use(bodyParser.json({limit: '50mb'})) app.get('/version.js', (req, res) => { res.writeHead(200, { diff --git a/package-lock.json b/package-lock.json index 1c71da7..895b66e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1005,6 +1005,28 @@ "to-fast-properties": "^2.0.0" } }, + "@calebboyd/async": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@calebboyd/async/-/async-1.4.0.tgz", + "integrity": "sha512-Q5tSWP28OF1nGd9KD6qspJselIfrHqk5+1gLGNNuQiUy80LE4g4ZAb+r+P8MYe1/FS5ICbm+OXiIalC7xhde3A==", + "dev": true + }, + "@calebboyd/semaphore": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@calebboyd/semaphore/-/semaphore-1.3.1.tgz", + "integrity": "sha512-17z9me12RgAEcMhIgR7f+BiXKbzwF9p1VraI69OxrUUSWGuSMOyOTEHQNVtMKuVrkEDVD0/Av5uiGZPBMYZljw==", + "dev": true + }, + "@mrmlnc/readdir-enhanced": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz", + "integrity": "sha512-bPHp6Ji8b41szTOcaP63VlnbbO5Ny6dwAATtY6JTjh5N2OLrb5Qk/Th5cRkRQhkWCt+EJsYrNB0MiL+Gpn6e3g==", + "dev": true, + "requires": { + "call-me-maybe": "^1.0.1", + "glob-to-regexp": "^0.3.0" + } + }, "@nodelib/fs.scandir": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.3.tgz", @@ -1160,6 +1182,11 @@ "resolved": "https://registry.npmjs.org/angular-router-browserify/-/angular-router-browserify-0.0.2.tgz", "integrity": "sha1-euL98uLowGxYz8aXrz56XohkJBg=" }, + "angular-vs-repeat": { + "version": "2.0.13", + "resolved": "https://registry.npmjs.org/angular-vs-repeat/-/angular-vs-repeat-2.0.13.tgz", + "integrity": "sha512-Jb0DOt4jU5/xZx7wjKvgZJtgAaMA4ZFLq5aeQHU8U2IZz8ixWP0ML6gFWQ1yefMapvg5LV+ZfA81ZeeN/NtJ7A==" + }, "ansi-align": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.0.tgz", @@ -1223,6 +1250,29 @@ } } }, + "app-builder": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/app-builder/-/app-builder-5.2.0.tgz", + "integrity": "sha512-RRj/vu8WhmMM71G9BxMLRvcwpr1QUJZ9NXURGGo1v3fPiauzkQfNi31kM7irRNqR87NV+lJ/qI62iTzcAc+V0Q==", + "dev": true + }, + "archive-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/archive-type/-/archive-type-4.0.0.tgz", + "integrity": "sha1-+S5yIzBW38aWlHJ0nCZ72wRrHXA=", + "dev": true, + "requires": { + "file-type": "^4.2.0" + }, + "dependencies": { + "file-type": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-4.4.0.tgz", + "integrity": "sha1-G2AOX8ofvcboDApwxxyNul95BsU=", + "dev": true + } + } + }, "arr-diff": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", @@ -1258,6 +1308,12 @@ "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", "dev": true }, + "array-uniq": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", + "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=", + "dev": true + }, "array-unique": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", @@ -1470,6 +1526,16 @@ "file-uri-to-path": "1.0.0" } }, + "bl": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.3.tgz", + "integrity": "sha512-pvcNpa0UU69UT341rO6AYy4FVAIkUHuZXRIWbq+zHnsVcRzDDjIAhGuuYoi0d//cwIwtt4pkpKycWEfjdV+vww==", + "dev": true, + "requires": { + "readable-stream": "^2.3.5", + "safe-buffer": "^5.1.1" + } + }, "bluebird": { "version": "3.7.2", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", @@ -1799,6 +1865,34 @@ "ieee754": "^1.1.4" } }, + "buffer-alloc": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz", + "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==", + "dev": true, + "requires": { + "buffer-alloc-unsafe": "^1.1.0", + "buffer-fill": "^1.0.0" + } + }, + "buffer-alloc-unsafe": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz", + "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==", + "dev": true + }, + "buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=", + "dev": true + }, + "buffer-fill": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz", + "integrity": "sha1-+PeLdniYiO858gXNY39o5wISKyw=", + "dev": true + }, "buffer-from": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", @@ -1817,11 +1911,13 @@ "integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=", "dev": true }, - "byline": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/byline/-/byline-5.0.0.tgz", - "integrity": "sha1-dBxSFkaOrcRXsDQQEYrXfejB3bE=", - "dev": true + "busboy": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-0.3.1.tgz", + "integrity": "sha512-y7tTxhGKXcyBxRKAni+awqx8uqaJKrSFSNFSeRG5CsWNdmy2BIK+6VGWEW7TZnIO/533mtMEA4rOevQV815YJw==", + "requires": { + "dicer": "0.3.0" + } }, "bytes": { "version": "3.1.0", @@ -1883,6 +1979,12 @@ "integrity": "sha512-5r2GqsoEb4qMTTN9J+WzXfjov+hjxT+j3u5K+kIVNIwAd99DLCJE9pBIMP1qVeybV6JiijL385Oz0DcYxfbOIg==", "dev": true }, + "call-me-maybe": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.1.tgz", + "integrity": "sha1-JtII6onje1y95gJQoV8DHBak1ms=", + "dev": true + }, "camelcase": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", @@ -1919,6 +2021,18 @@ "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" }, + "caw": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/caw/-/caw-2.0.1.tgz", + "integrity": "sha512-Cg8/ZSBEa8ZVY9HspcGUYaK63d/bN7rqS3CYCzEGUxuYv6UlmcjzDUz2fCFFHyTvUW5Pk0I+3hkA3iXlIj6guA==", + "dev": true, + "requires": { + "get-proxy": "^2.0.0", + "isurl": "^1.0.0-alpha5", + "tunnel-agent": "^0.6.0", + "url-to-options": "^1.0.1" + } + }, "chalk": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/chalk/-/chalk-0.4.0.tgz", @@ -1929,6 +2043,12 @@ "strip-ansi": "~0.1.0" } }, + "cherow": { + "version": "1.6.9", + "resolved": "https://registry.npmjs.org/cherow/-/cherow-1.6.9.tgz", + "integrity": "sha512-pmmkpIQRcnDA7EawKcg9+ncSZNTYfXqDx+K3oqqYvpZlqVBChjTomTfw+hePnkqYR3Y013818c0R1Q5P/7PGrQ==", + "dev": true + }, "chokidar": { "version": "2.1.8", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz", @@ -2000,6 +2120,27 @@ "integrity": "sha512-gpaBrMAizVEANOpfZp/EEUixTXDyGt7DFzdK5hU+UbWt/J0lB0w20ncZj59Z9a93xHb9u12zF5BS6i9RKbtg4w==", "dev": true }, + "cli-cursor": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", + "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", + "dev": true, + "requires": { + "restore-cursor": "^2.0.0" + } + }, + "cli-spinners": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.5.0.tgz", + "integrity": "sha512-PC+AmIuK04E6aeSs/pUccSujsTzBhu4HzC2dL+CfJB/Jcc2qTRbEwZQDfIUpt2Xl8BodYBEq8w4fc0kU2I9DjQ==", + "dev": true + }, + "clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=", + "dev": true + }, "clone-response": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz", @@ -2092,6 +2233,16 @@ "typedarray": "^0.0.6" } }, + "config-chain": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.12.tgz", + "integrity": "sha512-a1eOIcu8+7lUInge4Rpf/n4Krkf3Dd9lqhljRzII1/Zno/kRtUWnznPO3jOKBmTEktkt3fkxisUcivoj0ebzoA==", + "dev": true, + "requires": { + "ini": "^1.3.4", + "proto-list": "~1.2.1" + } + }, "configstore": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/configstore/-/configstore-5.0.1.tgz", @@ -2505,6 +2656,47 @@ "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", "dev": true }, + "decompress": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/decompress/-/decompress-4.2.1.tgz", + "integrity": "sha512-e48kc2IjU+2Zw8cTb6VZcJQ3lgVbS4uuB1TfCHbiZIP/haNXm+SVyhu+87jts5/3ROpd82GSVCoNs/z8l4ZOaQ==", + "dev": true, + "requires": { + "decompress-tar": "^4.0.0", + "decompress-tarbz2": "^4.0.0", + "decompress-targz": "^4.0.0", + "decompress-unzip": "^4.0.1", + "graceful-fs": "^4.1.10", + "make-dir": "^1.0.0", + "pify": "^2.3.0", + "strip-dirs": "^2.0.0" + }, + "dependencies": { + "make-dir": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", + "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", + "dev": true, + "requires": { + "pify": "^3.0.0" + }, + "dependencies": { + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + } + } + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + } + } + }, "decompress-response": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz", @@ -2514,17 +2706,115 @@ "mimic-response": "^1.0.0" } }, + "decompress-tar": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/decompress-tar/-/decompress-tar-4.1.1.tgz", + "integrity": "sha512-JdJMaCrGpB5fESVyxwpCx4Jdj2AagLmv3y58Qy4GE6HMVjWz1FeVQk1Ct4Kye7PftcdOo/7U7UKzYBJgqnGeUQ==", + "dev": true, + "requires": { + "file-type": "^5.2.0", + "is-stream": "^1.1.0", + "tar-stream": "^1.5.2" + }, + "dependencies": { + "file-type": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-5.2.0.tgz", + "integrity": "sha1-LdvqfHP/42No365J3DOMBYwritY=", + "dev": true + } + } + }, + "decompress-tarbz2": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/decompress-tarbz2/-/decompress-tarbz2-4.1.1.tgz", + "integrity": "sha512-s88xLzf1r81ICXLAVQVzaN6ZmX4A6U4z2nMbOwobxkLoIIfjVMBg7TeguTUXkKeXni795B6y5rnvDw7rxhAq9A==", + "dev": true, + "requires": { + "decompress-tar": "^4.1.0", + "file-type": "^6.1.0", + "is-stream": "^1.1.0", + "seek-bzip": "^1.0.5", + "unbzip2-stream": "^1.0.9" + }, + "dependencies": { + "file-type": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-6.2.0.tgz", + "integrity": "sha512-YPcTBDV+2Tm0VqjybVd32MHdlEGAtuxS3VAYsumFokDSMG+ROT5wawGlnHDoz7bfMcMDt9hxuXvXwoKUx2fkOg==", + "dev": true + } + } + }, + "decompress-targz": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/decompress-targz/-/decompress-targz-4.1.1.tgz", + "integrity": "sha512-4z81Znfr6chWnRDNfFNqLwPvm4db3WuZkqV+UgXQzSngG3CEKdBkw5jrv3axjjL96glyiiKjsxJG3X6WBZwX3w==", + "dev": true, + "requires": { + "decompress-tar": "^4.1.1", + "file-type": "^5.2.0", + "is-stream": "^1.1.0" + }, + "dependencies": { + "file-type": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-5.2.0.tgz", + "integrity": "sha1-LdvqfHP/42No365J3DOMBYwritY=", + "dev": true + } + } + }, + "decompress-unzip": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/decompress-unzip/-/decompress-unzip-4.0.1.tgz", + "integrity": "sha1-3qrM39FK6vhVePczroIQ+bSEj2k=", + "dev": true, + "requires": { + "file-type": "^3.8.0", + "get-stream": "^2.2.0", + "pify": "^2.3.0", + "yauzl": "^2.4.2" + }, + "dependencies": { + "file-type": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-3.9.0.tgz", + "integrity": "sha1-JXoHg4TR24CHvESdEH1SpSZyuek=", + "dev": true + }, + "get-stream": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-2.3.1.tgz", + "integrity": "sha1-Xzj5PzRgCWZu4BUKBUFn+Rvdld4=", + "dev": true, + "requires": { + "object-assign": "^4.0.1", + "pinkie-promise": "^2.0.0" + } + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + } + } + }, "deep-extend": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", "dev": true }, - "deep-is": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", - "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", - "dev": true + "defaults": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz", + "integrity": "sha1-xlYFHpgX2f8I7YgUd/P+QBnz730=", + "dev": true, + "requires": { + "clone": "^1.0.2" + } }, "defer-to-connect": { "version": "1.1.3", @@ -2670,6 +2960,14 @@ "minimist": "^1.1.1" } }, + "dicer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/dicer/-/dicer-0.3.0.tgz", + "integrity": "sha512-MdceRRWqltEG2dZqO769g27N/3PXfcKl04VhYnBlo2YhH7zPi88VebsjTKclaOyiuMaGU72hTfw3VkUitGcVCA==", + "requires": { + "streamsearch": "0.1.2" + } + }, "diffie-hellman": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", @@ -2723,6 +3021,144 @@ "is-obj": "^2.0.0" } }, + "download": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/download/-/download-7.1.0.tgz", + "integrity": "sha512-xqnBTVd/E+GxJVrX5/eUJiLYjCGPwMpdL+jGhGU57BvtcA7wwhtHVbXBeUk51kOpW3S7Jn3BQbN9Q1R1Km2qDQ==", + "dev": true, + "requires": { + "archive-type": "^4.0.0", + "caw": "^2.0.1", + "content-disposition": "^0.5.2", + "decompress": "^4.2.0", + "ext-name": "^5.0.0", + "file-type": "^8.1.0", + "filenamify": "^2.0.0", + "get-stream": "^3.0.0", + "got": "^8.3.1", + "make-dir": "^1.2.0", + "p-event": "^2.1.0", + "pify": "^3.0.0" + }, + "dependencies": { + "@sindresorhus/is": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.7.0.tgz", + "integrity": "sha512-ONhaKPIufzzrlNbqtWFFd+jlnemX6lJAgq9ZeiZtS7I1PIf/la7CW4m83rTXRnVnsMbW2k56pGYu7AUFJD9Pow==", + "dev": true + }, + "cacheable-request": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-2.1.4.tgz", + "integrity": "sha1-DYCIAbY0KtM8kd+dC0TcCbkeXD0=", + "dev": true, + "requires": { + "clone-response": "1.0.2", + "get-stream": "3.0.0", + "http-cache-semantics": "3.8.1", + "keyv": "3.0.0", + "lowercase-keys": "1.0.0", + "normalize-url": "2.0.1", + "responselike": "1.0.2" + }, + "dependencies": { + "lowercase-keys": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.0.tgz", + "integrity": "sha1-TjNms55/VFfjXxMkvfb4jQv8cwY=", + "dev": true + } + } + }, + "get-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=", + "dev": true + }, + "got": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/got/-/got-8.3.2.tgz", + "integrity": "sha512-qjUJ5U/hawxosMryILofZCkm3C84PLJS/0grRIpjAwu+Lkxxj5cxeCU25BG0/3mDSpXKTyZr8oh8wIgLaH0QCw==", + "dev": true, + "requires": { + "@sindresorhus/is": "^0.7.0", + "cacheable-request": "^2.1.1", + "decompress-response": "^3.3.0", + "duplexer3": "^0.1.4", + "get-stream": "^3.0.0", + "into-stream": "^3.1.0", + "is-retry-allowed": "^1.1.0", + "isurl": "^1.0.0-alpha5", + "lowercase-keys": "^1.0.0", + "mimic-response": "^1.0.0", + "p-cancelable": "^0.4.0", + "p-timeout": "^2.0.1", + "pify": "^3.0.0", + "safe-buffer": "^5.1.1", + "timed-out": "^4.0.1", + "url-parse-lax": "^3.0.0", + "url-to-options": "^1.0.1" + } + }, + "http-cache-semantics": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-3.8.1.tgz", + "integrity": "sha512-5ai2iksyV8ZXmnZhHH4rWPoxxistEexSi5936zIQ1bnNTW5VnA85B6P/VpXiRM017IgRvb2kKo1a//y+0wSp3w==", + "dev": true + }, + "keyv": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.0.0.tgz", + "integrity": "sha512-eguHnq22OE3uVoSYG0LVWNP+4ppamWr9+zWBe1bsNcovIMy6huUJFPgy4mGwCd/rnl3vOLGW1MTlu4c57CT1xA==", + "dev": true, + "requires": { + "json-buffer": "3.0.0" + } + }, + "make-dir": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", + "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", + "dev": true, + "requires": { + "pify": "^3.0.0" + } + }, + "normalize-url": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-2.0.1.tgz", + "integrity": "sha512-D6MUW4K/VzoJ4rJ01JFKxDrtY1v9wrgzCX5f2qj/lzH1m/lW6MhUZFKerVsnyjOhOsYzI9Kqqak+10l4LvLpMw==", + "dev": true, + "requires": { + "prepend-http": "^2.0.0", + "query-string": "^5.0.1", + "sort-keys": "^2.0.0" + } + }, + "p-cancelable": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-0.4.1.tgz", + "integrity": "sha512-HNa1A8LvB1kie7cERyy21VNeHb2CWJJYqyyC2o3klWFfMGlFmWv2Z7sFgZH8ZiaYL95ydToKTFVXgMV/Os0bBQ==", + "dev": true + }, + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + }, + "sort-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-2.0.0.tgz", + "integrity": "sha1-ZYU1WEhh7JfXMNbPQYIuH1ZoQSg=", + "dev": true, + "requires": { + "is-plain-obj": "^1.0.0" + } + } + } + }, "duplexer2": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", @@ -2793,6 +3229,26 @@ "once": "^1.4.0" } }, + "enhanced-resolve": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.5.0.tgz", + "integrity": "sha512-Nv9m36S/vxpsI+Hc4/ZGRs0n9mXqSWGGq49zxb/cJfPAQMbUtttJAlNPS4AQzaBdw/pKskw5bMbekT/Y7W/Wlg==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "memory-fs": "^0.5.0", + "tapable": "^1.0.0" + } + }, + "errno": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", + "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==", + "dev": true, + "requires": { + "prr": "~1.0.1" + } + }, "error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -2819,40 +3275,6 @@ "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", "dev": true }, - "escodegen": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.1.tgz", - "integrity": "sha512-Bmt7NcRySdIfNPfU2ZoXDrrXsG9ZjvDxcAlMfDUgRBjLOWTuIACXPBFJH7Z+cLb40JeQco5toikyc9t9P8E9SQ==", - "dev": true, - "requires": { - "esprima": "^4.0.1", - "estraverse": "^4.2.0", - "esutils": "^2.0.2", - "optionator": "^0.8.1", - "source-map": "~0.6.1" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "optional": true - } - } - }, - "esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true - }, - "estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true - }, "esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -2915,12 +3337,6 @@ } } }, - "expand-template": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", - "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", - "dev": true - }, "express": { "version": "4.17.1", "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", @@ -2958,6 +3374,33 @@ "vary": "~1.1.2" } }, + "express-fileupload": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/express-fileupload/-/express-fileupload-1.2.1.tgz", + "integrity": "sha512-fWPNAkBj+Azt9Itmcz/Reqdg3LeBfaXptDEev2JM8bCC0yDptglCnlizhf0YZauyU5X/g6v7v4Xxqhg8tmEfEA==", + "requires": { + "busboy": "^0.3.1" + } + }, + "ext-list": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/ext-list/-/ext-list-2.2.2.tgz", + "integrity": "sha512-u+SQgsubraE6zItfVA0tBuCBhfU9ogSRnsvygI7wht9TS510oLkBRXBsqopeUG/GBOIQyKZO9wjTqIu/sf5zFA==", + "dev": true, + "requires": { + "mime-db": "^1.28.0" + } + }, + "ext-name": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ext-name/-/ext-name-5.0.0.tgz", + "integrity": "sha512-yblEwXAbGv1VQDmow7s38W77hzAgJAO50ztBLMcUyUBfxv1HC+LGwtiEN+Co6LtlqT/5uwVOxsD4TNIilWhwdQ==", + "dev": true, + "requires": { + "ext-list": "^2.0.0", + "sort-keys-length": "^1.0.0" + } + }, "extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -3132,12 +3575,6 @@ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" }, - "fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", - "dev": true - }, "fast-safe-stringify": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz", @@ -3153,6 +3590,21 @@ "reusify": "^1.0.4" } }, + "fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4=", + "dev": true, + "requires": { + "pend": "~1.2.0" + } + }, + "file-type": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-8.1.0.tgz", + "integrity": "sha512-qyQ0pzAy78gVoJsmYeNgl8uH8yKhr1lVhW7JbzJmnlRi0I4R2eEDEJZVKG8agpDnLpacwNbDhLNG/LMdxHD2YQ==", + "dev": true + }, "file-uri-to-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", @@ -3160,6 +3612,23 @@ "dev": true, "optional": true }, + "filename-reserved-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-2.0.0.tgz", + "integrity": "sha1-q/c9+rc10EVECr/qLZHzieu/oik=", + "dev": true + }, + "filenamify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/filenamify/-/filenamify-2.1.0.tgz", + "integrity": "sha512-ICw7NTT6RsDp2rnYKVd8Fu4cr6ITzGy3+u4vUujPkabyaz+03F24NWEX7fs5fp+kBonlaqPH8fAO2NM+SXt/JA==", + "dev": true, + "requires": { + "filename-reserved-regex": "^2.0.0", + "strip-outer": "^1.0.0", + "trim-repeated": "^1.0.0" + } + }, "fill-range": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", @@ -3265,16 +3734,11 @@ "readable-stream": "^2.0.0" } }, - "fs-extra": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", - "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", - "dev": true, - "requires": { - "graceful-fs": "^4.2.0", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - } + "fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true }, "fs-readdir-recursive": { "version": "1.1.0", @@ -3857,6 +4321,15 @@ "integrity": "sha512-mBBwmeGTrxEMO4pMaaf/uUEFHnYtwr8FTe8Y/mer4rcV/bye0qGm6pw1bGZFGStxC5O76c5ZAVBGnqHmOaJpdQ==", "dev": true }, + "get-proxy": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/get-proxy/-/get-proxy-2.1.0.tgz", + "integrity": "sha512-zmZIaQTWnNQb4R4fJUEp/FC51eZsc6EkErspy3xtIYStaq8EB/hDIWipxsal+E8rz0qD7f2sL/NA9Xee4RInJw==", + "dev": true, + "requires": { + "npm-conf": "^1.1.0" + } + }, "get-stream": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", @@ -3915,6 +4388,12 @@ } } }, + "glob-to-regexp": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz", + "integrity": "sha1-jFoUlNIGbFcMw7/kSWF1rMTVAqs=", + "dev": true + }, "global-dirs": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-2.0.1.tgz", @@ -4013,12 +4492,27 @@ "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", "dev": true }, + "has-symbol-support-x": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/has-symbol-support-x/-/has-symbol-support-x-1.4.2.tgz", + "integrity": "sha512-3ToOva++HaW+eCpgqZrCfN51IPB+7bJNVT6CUATzueB5Heb8o6Nam0V3HG5dlDvZU1Gn5QLcbahiKw/XVk5JJw==", + "dev": true + }, "has-symbols": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==", "dev": true }, + "has-to-string-tag-x": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/has-to-string-tag-x/-/has-to-string-tag-x-1.4.1.tgz", + "integrity": "sha512-vdbKfmw+3LoOYVr+mtxHaX5a96+0f3DljYd8JOqvOLsf5mw2Otda2qCDT9qRqLAhrjyQ0h7ual5nOiASpsGNFw==", + "dev": true, + "requires": { + "has-symbol-support-x": "^1.4.1" + } + }, "has-value": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", @@ -4227,13 +4721,13 @@ } }, "into-stream": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/into-stream/-/into-stream-5.1.1.tgz", - "integrity": "sha512-krrAJ7McQxGGmvaYbB7Q1mcA+cRwg9Ij2RfWIeVesNBgVDZmzY/Fa4IpZUT3bmdRzMzdf/mzltCG2Dq99IZGBA==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/into-stream/-/into-stream-3.1.0.tgz", + "integrity": "sha1-lvsKk2wSur1v8XUqF9BWFqvQlMY=", "dev": true, "requires": { - "from2": "^2.3.0", - "p-is-promise": "^3.0.0" + "from2": "^2.1.1", + "p-is-promise": "^1.1.0" } }, "invariant": { @@ -4381,6 +4875,12 @@ "is-path-inside": "^3.0.1" } }, + "is-natural-number": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-natural-number/-/is-natural-number-4.0.1.tgz", + "integrity": "sha1-q5124dtM7VHjXeDHLr7PCfc0zeg=", + "dev": true + }, "is-npm": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-4.0.0.tgz", @@ -4413,6 +4913,12 @@ "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", "dev": true }, + "is-object": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-object/-/is-object-1.0.2.tgz", + "integrity": "sha512-2rRIahhZr2UWb45fIOuvZGpFtz0TyOZLf32KxBbSoUCeZR495zCKlWUKKUByk3geS2eAs7ZAABt0Y/Rx0GiQGA==", + "dev": true + }, "is-path-cwd": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz", @@ -4440,6 +4946,18 @@ "isobject": "^3.0.1" } }, + "is-retry-allowed": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.2.0.tgz", + "integrity": "sha512-RUbUeKwvm3XG2VYamhJL1xFktgjvPzL0Hq8C+6yrWIswDy3BIXGqCxhxkc30N9jqK311gVU137K8Ei55/zVJRg==", + "dev": true + }, + "is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", + "dev": true + }, "is-typedarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", @@ -4474,6 +4992,16 @@ "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" }, + "isurl": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isurl/-/isurl-1.0.0.tgz", + "integrity": "sha512-1P/yWsxPlDtn7QeRD+ULKQPaIaN6yF368GZ2vDfv0AL0NwpStafjWCDDdn0k8wgFMWpVAqG7oJhxHnlud42i9w==", + "dev": true, + "requires": { + "has-to-string-tag-x": "^1.2.0", + "is-object": "^1.0.1" + } + }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -4536,15 +5064,6 @@ "minimist": "^1.2.5" } }, - "jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.6" - } - }, "jsonify": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", @@ -4617,16 +5136,6 @@ "leven": "^3.1.0" } }, - "levn": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", - "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", - "dev": true, - "requires": { - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2" - } - }, "locate-path": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", @@ -4656,6 +5165,37 @@ "integrity": "sha1-LcvSwofLwKVcxCMovQxzYVDVPj8=", "dev": true }, + "log-symbols": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz", + "integrity": "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==", + "dev": true, + "requires": { + "chalk": "^2.0.1" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + } + } + }, "loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -4736,6 +5276,16 @@ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" }, + "memory-fs": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.5.0.tgz", + "integrity": "sha512-jA0rdU5KoQMC0e6ppoNRtpp6vjFq6+NY7r8hywnC7V+1Xj/MtHwGIbB1QaK/dunyjWteJzmkpd7ooeWg10T7GA==", + "dev": true, + "requires": { + "errno": "^0.1.3", + "readable-stream": "^2.0.1" + } + }, "meow": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/meow/-/meow-5.0.0.tgz", @@ -4859,6 +5409,12 @@ "integrity": "sha512-2j4DAdlBOkiSZIsaXk4mTE3sRS02yBHAtfy127xRV3bQUFqXkjHCHLW6Scv7DwNRbIWNHH8zpnz9zMaKXIdvYw==", "dev": true }, + "meriyah": { + "version": "1.9.15", + "resolved": "https://registry.npmjs.org/meriyah/-/meriyah-1.9.15.tgz", + "integrity": "sha512-D4rT6XIYGqZab0EI/xbihUpwh0WbNRVQ35l2J/5QC2atxaI8h3KvA55DEJLBB/FRdaji7JwkNehfCRjCyjCjqw==", + "dev": true + }, "methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", @@ -4913,6 +5469,12 @@ "mime-db": "1.43.0" } }, + "mimic-fn": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", + "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", + "dev": true + }, "mimic-response": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", @@ -5061,6 +5623,125 @@ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" }, + "nexe": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nexe/-/nexe-3.3.7.tgz", + "integrity": "sha512-guAZKlY6UZcovzvuYjo7Hy7mx52Uq5f284aPeDVN2TjR2/jkhEHgnKMxBc9ECmknEvHoABgk6rYDG92q+J30sQ==", + "dev": true, + "requires": { + "@calebboyd/semaphore": "^1.3.1", + "app-builder": "^5.2.0", + "caw": "^2.0.1", + "chalk": "^2.4.2", + "cherow": "1.6.9", + "download": "^7.1.0", + "globby": "^9.2.0", + "got": "^9.6.0", + "minimist": "^1.2.0", + "mkdirp": "^0.5.1", + "multistream": "^2.1.1", + "ora": "^3.4.0", + "pify": "^4.0.1", + "resolve-dependencies": "^4.2.5", + "rimraf": "^2.6.3" + }, + "dependencies": { + "@nodelib/fs.stat": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz", + "integrity": "sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==", + "dev": true + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "array-union": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", + "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=", + "dev": true, + "requires": { + "array-uniq": "^1.0.1" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "dir-glob": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-2.2.2.tgz", + "integrity": "sha512-f9LBi5QWzIW3I6e//uxZoLBlUt9kcp66qo0sSCxL6YZKc75R1c4MFCoe/LaZiBGmgujvQdxc5Bn3QhfyvK5Hsw==", + "dev": true, + "requires": { + "path-type": "^3.0.0" + } + }, + "fast-glob": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-2.2.7.tgz", + "integrity": "sha512-g1KuQwHOZAmOZMuBtHdxDtju+T2RT8jgCC9aANsbpdiDDTSnjgfuVsIBNKbUeJI3oKMRExcfNDtJl4OhbffMsw==", + "dev": true, + "requires": { + "@mrmlnc/readdir-enhanced": "^2.2.1", + "@nodelib/fs.stat": "^1.1.2", + "glob-parent": "^3.1.0", + "is-glob": "^4.0.0", + "merge2": "^1.2.3", + "micromatch": "^3.1.10" + } + }, + "globby": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-9.2.0.tgz", + "integrity": "sha512-ollPHROa5mcxDEkwg6bPt3QbEf4pDQSNtd6JPL1YvOvAo/7/0VAm9TccUeoTmarjPw4pfUthSCqcyfNB1I3ZSg==", + "dev": true, + "requires": { + "@types/glob": "^7.1.1", + "array-union": "^1.0.2", + "dir-glob": "^2.2.2", + "fast-glob": "^2.2.6", + "glob": "^7.1.3", + "ignore": "^4.0.3", + "pify": "^4.0.1", + "slash": "^2.0.0" + } + }, + "ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "dev": true + }, + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + } + } + }, + "node-graceful-shutdown": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/node-graceful-shutdown/-/node-graceful-shutdown-1.1.0.tgz", + "integrity": "sha512-g1tq/R8ie/At5xRHGfF+chTge1jVPxf1NEClLpZIPxOPi6PJ9II81T35ms1u+s4N/mqOCp60CFd+ps+DIWRigQ==" + }, "node-releases": { "version": "1.1.53", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.53.tgz", @@ -5302,6 +5983,24 @@ "integrity": "sha512-2s47yzUxdexf1OhyRi4Em83iQk0aPvwTddtFz4hnSSw9dCEsLEGf6SwIO8ss/19S9iBb5sJaOuTvTGDeZI00BQ==", "dev": true }, + "npm-conf": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/npm-conf/-/npm-conf-1.1.3.tgz", + "integrity": "sha512-Yic4bZHJOt9RCFbRP3GgpqhScOY4HH3V2P8yBj6CeYq118Qr+BLXqT2JvpJ00mryLESpgOxf5XlFv4ZjXxLScw==", + "dev": true, + "requires": { + "config-chain": "^1.1.11", + "pify": "^3.0.0" + }, + "dependencies": { + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + } + } + }, "oauth-sign": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", @@ -5397,18 +6096,58 @@ "wrappy": "1" } }, - "optionator": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", - "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "onetime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", + "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=", "dev": true, "requires": { - "deep-is": "~0.1.3", - "fast-levenshtein": "~2.0.6", - "levn": "~0.3.0", - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2", - "word-wrap": "~1.2.3" + "mimic-fn": "^1.0.0" + } + }, + "ora": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/ora/-/ora-3.4.0.tgz", + "integrity": "sha512-eNwHudNbO1folBP3JsZ19v9azXWtQZjICdr3Q0TDPIaeBQ3mXLrh54wM+er0+hSp+dWKf+Z8KM58CYzEyIYxYg==", + "dev": true, + "requires": { + "chalk": "^2.4.2", + "cli-cursor": "^2.1.0", + "cli-spinners": "^2.0.0", + "log-symbols": "^2.2.0", + "strip-ansi": "^5.2.0", + "wcwidth": "^1.0.1" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + } } }, "os-browserify": { @@ -5417,12 +6156,6 @@ "integrity": "sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=", "dev": true }, - "os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", - "dev": true - }, "outpipe": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/outpipe/-/outpipe-1.1.1.tgz", @@ -5438,10 +6171,25 @@ "integrity": "sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==", "dev": true }, + "p-event": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/p-event/-/p-event-2.3.1.tgz", + "integrity": "sha512-NQCqOFhbpVTMX4qMe8PF8lbGtzZ+LCiN7pcNrb/413Na7+TRoe1xkKUzuWa/YEJdGQ0FvKtj35EEbDoVPO2kbA==", + "dev": true, + "requires": { + "p-timeout": "^2.0.1" + } + }, + "p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", + "dev": true + }, "p-is-promise": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-3.0.0.tgz", - "integrity": "sha512-Wo8VsW4IRQSKVXsJCn7TomUaVtyfjVDn3nUP7kE967BQk0CwFpdbZs0X0uk5sW9mkBa9eNM7hCMaG93WUAwxYQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-1.1.0.tgz", + "integrity": "sha1-nJRWmJ6fZYgBewQ01WCXZ1w9oF4=", "dev": true }, "p-limit": { @@ -5471,6 +6219,15 @@ "aggregate-error": "^3.0.0" } }, + "p-timeout": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-2.0.1.tgz", + "integrity": "sha512-88em58dDVB/KzPEx1X0N3LwFfYZPyDc4B6eF38M1rk9VTZMbxXXgjugz8mmwpS9Ox4BDZ+t6t3QP5+/gazweIA==", + "dev": true, + "requires": { + "p-finally": "^1.0.0" + } + }, "p-try": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", @@ -5572,6 +6329,23 @@ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" }, + "path-type": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", + "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", + "dev": true, + "requires": { + "pify": "^3.0.0" + }, + "dependencies": { + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + } + } + }, "pbkdf2": { "version": "3.0.17", "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.17.tgz", @@ -5585,6 +6359,12 @@ "sha.js": "^2.4.8" } }, + "pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=", + "dev": true + }, "performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", @@ -5596,174 +6376,25 @@ "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==", "dev": true }, - "pkg": { - "version": "4.4.7", - "resolved": "https://registry.npmjs.org/pkg/-/pkg-4.4.7.tgz", - "integrity": "sha512-yDGEg2k09AOxV3KfJpKoEQkhckVN2woV/4Cm2iNnRUgJeSHcodxylertz49ePcJyknUyUFjTYDkogfK/188mag==", - "dev": true, - "requires": { - "@babel/parser": "^7.9.4", - "@babel/runtime": "^7.9.2", - "chalk": "^3.0.0", - "escodegen": "^1.14.1", - "fs-extra": "^8.1.0", - "globby": "^11.0.0", - "into-stream": "^5.1.1", - "minimist": "^1.2.5", - "multistream": "^2.1.1", - "pkg-fetch": "^2.6.6", - "progress": "^2.0.3", - "resolve": "^1.15.1", - "stream-meter": "^1.0.4" - }, - "dependencies": { - "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", - "dev": true, - "requires": { - "@types/color-name": "^1.1.1", - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "globby": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.0.0.tgz", - "integrity": "sha512-iuehFnR3xu5wBBtm4xi0dMe92Ob87ufyu/dHwpDYfbcpYpIbrO5OnS8M1vWvrBhSGEJ3/Ecj7gnX76P8YxpPEg==", - "dev": true, - "requires": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.1.1", - "ignore": "^5.1.4", - "merge2": "^1.3.0", - "slash": "^3.0.0" - } - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true - }, - "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } + "pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true }, - "pkg-fetch": { - "version": "2.6.6", - "resolved": "https://registry.npmjs.org/pkg-fetch/-/pkg-fetch-2.6.6.tgz", - "integrity": "sha512-PdL6lpoSryzP6rMZD1voZQX0LHx6q4pOaD1djaFphmBfYPoQzLalF2+St+wdYxbZ37xRNHACTeQIKNEKA0xdbA==", + "pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", + "dev": true + }, + "pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", "dev": true, "requires": { - "@babel/runtime": "^7.9.2", - "byline": "^5.0.0", - "chalk": "^3.0.0", - "expand-template": "^2.0.3", - "fs-extra": "^8.1.0", - "minimist": "^1.2.5", - "progress": "^2.0.3", - "request": "^2.88.0", - "request-progress": "^3.0.0", - "semver": "^6.3.0", - "unique-temp-dir": "^1.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", - "dev": true, - "requires": { - "@types/color-name": "^1.1.1", - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - }, - "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } + "pinkie": "^2.0.0" } }, "pkg-up": { @@ -5792,12 +6423,6 @@ "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=", "dev": true }, - "prelude-ls": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", - "dev": true - }, "prepend-http": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz", @@ -5822,10 +6447,10 @@ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", "dev": true }, - "progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "proto-list": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", + "integrity": "sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk=", "dev": true }, "proxy-addr": { @@ -5837,6 +6462,12 @@ "ipaddr.js": "1.9.1" } }, + "prr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", + "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=", + "dev": true + }, "psl": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", @@ -5891,6 +6522,17 @@ "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" }, + "query-string": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-5.1.1.tgz", + "integrity": "sha512-gjWOsm2SoGlgLEdAGt7a6slVOk9mGiXmPFMqrEhLQ68rhQuBnpfs3+EmlvqKyxnCo9/PPlF+9MtY02S1aFg+Jw==", + "dev": true, + "requires": { + "decode-uri-component": "^0.2.0", + "object-assign": "^4.1.0", + "strict-uri-encode": "^1.0.0" + } + }, "querystring": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", @@ -5909,6 +6551,11 @@ "integrity": "sha1-Q2CxfGETatOAeDl/8RQW4Ybc+7g=", "dev": true }, + "random-js": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/random-js/-/random-js-2.1.0.tgz", + "integrity": "sha512-CRUyWmnzmZBA7RZSVGq0xMqmgCyPPxbiKNLFA5ud7KenojVX2s7Gv+V7eB52beKTPGxWRnVZ7D/tCIgYJJ8vNQ==" + }, "randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -6162,15 +6809,6 @@ } } }, - "request-progress": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/request-progress/-/request-progress-3.0.0.tgz", - "integrity": "sha1-TKdUCBx/7GP1BeT6qCWqBs1mnb4=", - "dev": true, - "requires": { - "throttleit": "^1.0.0" - } - }, "require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -6186,6 +6824,40 @@ "path-parse": "^1.0.6" } }, + "resolve-dependencies": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/resolve-dependencies/-/resolve-dependencies-4.2.5.tgz", + "integrity": "sha512-swlXn30tAgdJZZwNuZfHmzGxQouHEhFWzkl8sJ8b65NjkjcIwHDEt5tuCze1zTtgMdg3jGQaa/RfSWV1b/9kqA==", + "dev": true, + "requires": { + "@calebboyd/async": "^1.4.0", + "enhanced-resolve": "^4.2.0", + "globby": "^11.0.1", + "meriyah": "^1.9.15" + }, + "dependencies": { + "globby": { + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.0.2.tgz", + "integrity": "sha512-2ZThXDvvV8fYFRVIxnrMQBipZQDr7MxKAmQK1vujaj9/7eF0efG7BPUKJ7jP7G5SLF37xKDXvO4S/KKLj/Z0og==", + "dev": true, + "requires": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.1.1", + "ignore": "^5.1.4", + "merge2": "^1.3.0", + "slash": "^3.0.0" + } + }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + } + } + }, "resolve-url": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", @@ -6201,6 +6873,16 @@ "lowercase-keys": "^1.0.0" } }, + "restore-cursor": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", + "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=", + "dev": true, + "requires": { + "onetime": "^2.0.0", + "signal-exit": "^3.0.2" + } + }, "ret": { "version": "0.1.15", "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", @@ -6257,6 +6939,23 @@ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, + "seek-bzip": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/seek-bzip/-/seek-bzip-1.0.6.tgz", + "integrity": "sha512-e1QtP3YL5tWww8uKaOCQ18UxIT2laNBXHjV/S2WYCiK4udiv8lkG89KRIoCjUagnAmCBurjF4zEVX2ByBbnCjQ==", + "dev": true, + "requires": { + "commander": "^2.8.1" + }, + "dependencies": { + "commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + } + } + }, "semver": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", @@ -6512,6 +7211,24 @@ } } }, + "sort-keys": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz", + "integrity": "sha1-RBttTTRnmPG05J6JIK37oOVD+a0=", + "dev": true, + "requires": { + "is-plain-obj": "^1.0.0" + } + }, + "sort-keys-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/sort-keys-length/-/sort-keys-length-1.0.1.tgz", + "integrity": "sha1-nLb09OnkgVWmqgZx7dM2/xR5oYg=", + "dev": true, + "requires": { + "sort-keys": "^1.0.0" + } + }, "source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", @@ -6665,15 +7382,6 @@ } } }, - "stream-meter": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/stream-meter/-/stream-meter-1.0.4.tgz", - "integrity": "sha1-Uq+Vql6nYKJJFxZwTb/5D3Ov3R0=", - "dev": true, - "requires": { - "readable-stream": "^2.1.4" - } - }, "stream-splicer": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/stream-splicer/-/stream-splicer-2.0.1.tgz", @@ -6684,6 +7392,17 @@ "readable-stream": "^2.0.2" } }, + "streamsearch": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz", + "integrity": "sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo=" + }, + "strict-uri-encode": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz", + "integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=", + "dev": true + }, "string-width": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", @@ -6738,6 +7457,15 @@ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-0.1.1.tgz", "integrity": "sha1-OeipjQRNFQZgq+SmgIrPcLt7yZE=" }, + "strip-dirs": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/strip-dirs/-/strip-dirs-2.1.0.tgz", + "integrity": "sha512-JOCxOeKLm2CAS73y/U4ZeZPTkE+gNVCzKt7Eox84Iej1LT/2pTWYpZKJuxwQpvX1LiZb1xokNR7RLfuBAa7T3g==", + "dev": true, + "requires": { + "is-natural-number": "^4.0.1" + } + }, "strip-indent": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-2.0.0.tgz", @@ -6750,6 +7478,15 @@ "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", "dev": true }, + "strip-outer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/strip-outer/-/strip-outer-1.0.1.tgz", + "integrity": "sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg==", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.2" + } + }, "subarg": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/subarg/-/subarg-1.0.0.tgz", @@ -6777,18 +7514,33 @@ "acorn-node": "^1.2.0" } }, + "tapable": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz", + "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==", + "dev": true + }, + "tar-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.6.2.tgz", + "integrity": "sha512-rzS0heiNf8Xn7/mpdSVVSMAWAoy9bfb1WOTYC78Z0UQKeKa/CWS8FOq0lKGNa8DWKAn9gxjCvMLYc5PGXYlK2A==", + "dev": true, + "requires": { + "bl": "^1.0.0", + "buffer-alloc": "^1.2.0", + "end-of-stream": "^1.0.0", + "fs-constants": "^1.0.0", + "readable-stream": "^2.3.0", + "to-buffer": "^1.1.1", + "xtend": "^4.0.0" + } + }, "term-size": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/term-size/-/term-size-2.2.0.tgz", "integrity": "sha512-a6sumDlzyHVJWb8+YofY4TW112G6p2FCPEAFk+59gIYHv3XHRhm9ltVQ9kli4hNWeQBwSpe8cRN25x0ROunMOw==", "dev": true }, - "throttleit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-1.0.0.tgz", - "integrity": "sha1-nnhYNtr0Z0MUWlmEtiaNgoUorGw=", - "dev": true - }, "through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", @@ -6805,6 +7557,12 @@ "xtend": "~4.0.1" } }, + "timed-out": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/timed-out/-/timed-out-4.0.1.tgz", + "integrity": "sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8=", + "dev": true + }, "timers-browserify": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-1.4.2.tgz", @@ -6814,6 +7572,12 @@ "process": "~0.11.0" } }, + "to-buffer": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.1.1.tgz", + "integrity": "sha512-lx9B5iv7msuFYE3dytT+KE5tap+rNYw+K4jVkb9R/asAb+pbBSM17jtunHplhBe6RRJdZx3Pn2Jph24O32mOVg==", + "dev": true + }, "to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", @@ -6897,6 +7661,15 @@ "integrity": "sha1-tAPQuRvlDDMd/EuC7s6yLD3hbSA=", "dev": true }, + "trim-repeated": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/trim-repeated/-/trim-repeated-1.0.0.tgz", + "integrity": "sha1-42RqLqTokTEr9+rObPsFOAvAHCE=", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.2" + } + }, "tty-browserify": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.1.tgz", @@ -6916,15 +7689,6 @@ "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" }, - "type-check": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", - "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", - "dev": true, - "requires": { - "prelude-ls": "~1.1.2" - } - }, "type-fest": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", @@ -6955,18 +7719,22 @@ "is-typedarray": "^1.0.0" } }, - "uid2": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.3.tgz", - "integrity": "sha1-SDEm4Rd03y9xuLY53NeZw3YWK4I=", - "dev": true - }, "umd": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/umd/-/umd-3.0.3.tgz", "integrity": "sha512-4IcGSufhFshvLNcMCV80UnQVlZ5pMOC8mvNPForqwA4+lzYQuetTESLDQkeLmihq8bRcnpbQa48Wb8Lh16/xow==", "dev": true }, + "unbzip2-stream": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz", + "integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==", + "dev": true, + "requires": { + "buffer": "^5.2.1", + "through": "^2.3.8" + } + }, "undeclared-identifiers": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/undeclared-identifiers/-/undeclared-identifiers-1.1.3.tgz", @@ -7038,23 +7806,6 @@ "crypto-random-string": "^2.0.0" } }, - "unique-temp-dir": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unique-temp-dir/-/unique-temp-dir-1.0.0.tgz", - "integrity": "sha1-bc6VsmgcoAPuv7MEpBX5y6vMU4U=", - "dev": true, - "requires": { - "mkdirp": "^0.5.1", - "os-tmpdir": "^1.0.1", - "uid2": "0.0.3" - } - }, - "universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", - "dev": true - }, "unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -7220,6 +7971,12 @@ "prepend-http": "^2.0.0" } }, + "url-to-options": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/url-to-options/-/url-to-options-1.0.1.tgz", + "integrity": "sha1-FQWgOiiaSMvXpDTvuu7FBV9WM6k=", + "dev": true + }, "use": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", @@ -7297,6 +8054,15 @@ "xtend": "^4.0.0" } }, + "wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g=", + "dev": true, + "requires": { + "defaults": "^1.0.3" + } + }, "widest-line": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz", @@ -7306,12 +8072,6 @@ "string-width": "^4.0.0" } }, - "word-wrap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", - "dev": true - }, "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -7346,6 +8106,16 @@ "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", "dev": true + }, + "yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=", + "dev": true, + "requires": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } } } } diff --git a/package.json b/package.json index ce92890..d6f96d5 100644 --- a/package.json +++ b/package.json @@ -19,15 +19,16 @@ "angular": "^1.7.9", "angular-router-browserify": "0.0.2", "angular-vs-repeat": "2.0.13", - "random-js": "2.1.0", "axios": "^0.19.2", "body-parser": "^1.19.0", "diskdb": "^0.1.17", "express": "^4.17.1", + "express-fileupload": "^1.2.1", + "node-graceful-shutdown": "1.1.0", "node-ssdp": "^4.0.0", + "random-js": "2.1.0", "request": "^2.88.2", "uuid": "^8.0.0", - "node-graceful-shutdown": "1.1.0", "xml-writer": "^1.7.0" }, "bin": "dist/index.js", diff --git a/src/api.js b/src/api.js index 9365120..4b1e338 100644 --- a/src/api.js +++ b/src/api.js @@ -209,6 +209,32 @@ function api(db, channelDB, fillerDB, xmltvInterval, guideService ) { res.status(500).send("error"); } }) + router.post('/api/channel/logo', async (req, res) => { + try { + if(!req.files) { + res.send({ + status: false, + message: 'No file uploaded' + }); + } else { + const logo = req.files.logo; + logo.mv(path.join(process.env.DATABASE, '/images/uploads/', logo.name)); + + res.send({ + status: true, + message: 'File is uploaded', + data: { + name: logo.name, + mimetype: logo.mimetype, + size: logo.size, + fileUrl: `${req.protocol}://${req.get('host')}/images/uploads/${logo.name}` + } + }); + } + } catch (err) { + res.status(500).send(err); + } + }) // Filler router.get('/api/fillers', async (req, res) => { diff --git a/web/directives/channel-config.js b/web/directives/channel-config.js index 0cb41b8..18b3626 100644 --- a/web/directives/channel-config.js +++ b/web/directives/channel-config.js @@ -1852,6 +1852,14 @@ module.exports = function ($timeout, $location, dizquetv, resolutionOptions) { scope.timeSlots.startDialog(scope.channel.programs, scope.maxSize, scope.channel.scheduleBackup ); } + scope.logoOnChange = (event) => { + const formData = new FormData(); + formData.append('logo', event.target.files[0]); + dizquetv.addChannelLogo(formData).then((response) => { + scope.channel.icon = response.data.fileUrl; + }) + } + }, pre: function(scope) { diff --git a/web/public/templates/channel-config.html b/web/public/templates/channel-config.html index f88c4b6..f9f1e39 100644 --- a/web/public/templates/channel-config.html +++ b/web/public/templates/channel-config.html @@ -41,6 +41,12 @@
Channel Icon Preview
{{channel.name}} {{channel.name}} +
+ +
diff --git a/web/services/dizquetv.js b/web/services/dizquetv.js index ac721fa..57076f2 100644 --- a/web/services/dizquetv.js +++ b/web/services/dizquetv.js @@ -150,6 +150,14 @@ module.exports = function ($http) { headers: { 'Content-Type': 'application/json; charset=utf-8' } }).then((d) => { return d.data }) }, + addChannelLogo: (file) => { + return $http({ + method: 'POST', + url: '/api/channel/logo?logo', + data: file, + headers: { 'Content-Type': undefined } + }).then((d) => { return d.data }) + }, updateChannel: (channel) => { return $http({ method: 'PUT', From f69b23e10e3ae6b3b50862ff2e9f0f81d2679247 Mon Sep 17 00:00:00 2001 From: Rafael Vieira Date: Wed, 20 Jan 2021 00:38:48 -0300 Subject: [PATCH 09/57] Fixed images on readme.md for docker hub --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index bef536a..94c2eb9 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ Create live TV channel streams from media on your Plex servers. **dizqueTV** ( *dis·keˈtiːˈvi* ) is a fork of the project previously-known as [pseudotv-plex](https://gitlab.com/DEFENDORe/pseudotv-plex) or [pseudotv](https://github.com/DEFENDORe/pseudotv). New repository because of lack of activity from the main repository and the name change is because projects with the old name already existed and were created long before this approach and it was causing confusion. You can migrate from pseudoTV 0.0.51 to dizqueTV by renaming the .pseudotv folder to .dizquetv and running the new executable (or doing a similar trick with the volumes used by the docker containers). - + Configure your channels, programs, commercials and settings using the dizqueTV web UI. @@ -43,13 +43,13 @@ EPG (Guide Information) data is stored to `.dizquetv/xmltv.xml` ## App Preview - +
- +
- +
- + ## Development Building/Packaging Binaries: (uses `browserify`, `babel` and `pkg`) From 54a6f14ff65b7a3af4b17c8bb69113a7f28cf5be Mon Sep 17 00:00:00 2001 From: vexorian Date: Thu, 21 Jan 2021 19:35:34 -0400 Subject: [PATCH 10/57] Tweaks to image upload . Now supports watermark as well. The api has changed. Dialog browses for Images. --- src/api.js | 5 +++-- web/directives/channel-config.js | 13 +++++++++-- web/public/templates/channel-config.html | 28 +++++++++++++++++++----- web/services/dizquetv.js | 12 ++++++++-- 4 files changed, 46 insertions(+), 12 deletions(-) diff --git a/src/api.js b/src/api.js index 4b1e338..67d937f 100644 --- a/src/api.js +++ b/src/api.js @@ -209,7 +209,8 @@ function api(db, channelDB, fillerDB, xmltvInterval, guideService ) { res.status(500).send("error"); } }) - router.post('/api/channel/logo', async (req, res) => { + + router.post('/api/upload/image', async (req, res) => { try { if(!req.files) { res.send({ @@ -217,7 +218,7 @@ function api(db, channelDB, fillerDB, xmltvInterval, guideService ) { message: 'No file uploaded' }); } else { - const logo = req.files.logo; + const logo = req.files.image; logo.mv(path.join(process.env.DATABASE, '/images/uploads/', logo.name)); res.send({ diff --git a/web/directives/channel-config.js b/web/directives/channel-config.js index 18b3626..00ce1ca 100644 --- a/web/directives/channel-config.js +++ b/web/directives/channel-config.js @@ -1854,12 +1854,21 @@ module.exports = function ($timeout, $location, dizquetv, resolutionOptions) { scope.logoOnChange = (event) => { const formData = new FormData(); - formData.append('logo', event.target.files[0]); - dizquetv.addChannelLogo(formData).then((response) => { + formData.append('image', event.target.files[0]); + dizquetv.uploadImage(formData).then((response) => { scope.channel.icon = response.data.fileUrl; }) } + scope.watermarkOnChange = (event) => { + const formData = new FormData(); + formData.append('image', event.target.files[0]); + dizquetv.uploadImage(formData).then((response) => { + scope.channel.watermark.url = response.data.fileUrl; + }) + } + + }, pre: function(scope) { diff --git a/web/public/templates/channel-config.html b/web/public/templates/channel-config.html index f9f1e39..506265a 100644 --- a/web/public/templates/channel-config.html +++ b/web/public/templates/channel-config.html @@ -34,19 +34,24 @@
{{error.icon}} +
+
+ + +
+
Channel Icon Preview
{{channel.name}} {{channel.name}} -
- -
@@ -696,8 +701,19 @@ + +
+ + +
+
diff --git a/web/services/dizquetv.js b/web/services/dizquetv.js index 57076f2..6acace0 100644 --- a/web/services/dizquetv.js +++ b/web/services/dizquetv.js @@ -150,10 +150,18 @@ module.exports = function ($http) { headers: { 'Content-Type': 'application/json; charset=utf-8' } }).then((d) => { return d.data }) }, - addChannelLogo: (file) => { + uploadImage: (file) => { return $http({ method: 'POST', - url: '/api/channel/logo?logo', + url: '/api/upload/image', + data: file, + headers: { 'Content-Type': undefined } + }).then((d) => { return d.data }) + }, + addChannelWatermark: (file) => { + return $http({ + method: 'POST', + url: '/api/channel/watermark', data: file, headers: { 'Content-Type': undefined } }).then((d) => { return d.data }) From 9f194e62c6aa5be0dabca3cfca34526d95d796cf Mon Sep 17 00:00:00 2001 From: vexorian Date: Sat, 23 Jan 2021 13:03:53 -0400 Subject: [PATCH 11/57] #18 Allowing to play audio files in channels. They actually play now, but requires editing the channel json manually because there is no UI to import them yet. --- index.js | 4 + resources/generic-music-screen.png | Bin 0 -> 22686 bytes src/ffmpeg.js | 85 ++++++++++++++------- src/plex-player.js | 6 +- src/plexTranscoder.js | 66 ++++++++++++++--- src/svg/generic-music-screen.svg | 115 +++++++++++++++++++++++++++++ 6 files changed, 236 insertions(+), 40 deletions(-) create mode 100644 resources/generic-music-screen.png create mode 100644 src/svg/generic-music-screen.svg diff --git a/index.js b/index.js index 30fc1e4..bbab982 100644 --- a/index.js +++ b/index.js @@ -212,6 +212,10 @@ function initDB(db, channelDB) { let data = fs.readFileSync(path.resolve(path.join(__dirname, 'resources/generic-offline-screen.png'))) fs.writeFileSync(process.env.DATABASE + '/images/generic-offline-screen.png', data) } + if (!fs.existsSync(process.env.DATABASE + '/images/generic-music-screen.png')) { + let data = fs.readFileSync(path.resolve(path.join(__dirname, 'resources/generic-music-screen.png'))) + fs.writeFileSync(process.env.DATABASE + '/images/generic-music-screen.png', data) + } if (!fs.existsSync(process.env.DATABASE + '/images/loading-screen.png')) { let data = fs.readFileSync(path.resolve(path.join(__dirname, 'resources/loading-screen.png'))) fs.writeFileSync(process.env.DATABASE + '/images/loading-screen.png', data) diff --git a/resources/generic-music-screen.png b/resources/generic-music-screen.png new file mode 100644 index 0000000000000000000000000000000000000000..4efa71f9115662577d182307e077e26049a251c4 GIT binary patch literal 22686 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 F{|95gT_FGf literal 0 HcmV?d00001 diff --git a/src/ffmpeg.js b/src/ffmpeg.js index 8fcd3cd..e264cf6 100644 --- a/src/ffmpeg.js +++ b/src/ffmpeg.js @@ -111,6 +111,7 @@ class FFMPEG extends events.EventEmitter { let ffmpegArgs = [ `-threads`, isConcatPlaylist? 1 : this.opts.threads, `-fflags`, `+genpts+discardcorrupt+igndts`]; + let stillImage = false; if ( (limitRead === true) @@ -185,28 +186,57 @@ class FFMPEG extends events.EventEmitter { } // prepare input streams - if ( typeof(streamUrl.errorTitle) !== 'undefined') { + if ( ( typeof(streamUrl.errorTitle) !== 'undefined') || (streamStats.audioOnly) ) { doOverlay = false; //never show icon in the error screen // for error stream, we have to generate the input as well this.apad = false; //all of these generate audio correctly-aligned to video so there is no need for apad this.audioChannelsSampleRate = true; //we'll need these - if (this.ensureResolution) { - //all of the error strings already choose the resolution to - //match iW x iH , so with this we save ourselves a second - // scale filter - iW = this.wantedW; - iH = this.wantedH; + //all of the error strings already choose the resolution to + //match iW x iH , so with this we save ourselves a second + // scale filter + iW = this.wantedW; + iH = this.wantedH; + + if (this.audioOnly !== true) { + ffmpegArgs.push("-r" , "24"); + let pic = null; + + //does an image to play exist? + if ( + (typeof(streamUrl.errorTitle) === 'undefined') + && + (streamStats.audioOnly) + ) { + pic = streamStats.placeholderImage; + } else if ( streamUrl.errorTitle == 'offline') { + pic = `${this.channel.offlinePicture}`; + } else if ( this.opts.errorScreen == 'pic' ) { + pic = `${this.errorPicturePath}`; } - if ( this.audioOnly !== true) { - ffmpegArgs.push("-r" , "24"); - if ( streamUrl.errorTitle == 'offline' ) { + if (pic != null) { ffmpegArgs.push( - '-loop', '1', - '-i', `${this.channel.offlinePicture}`, + '-i', pic, ); - videoComplex = `;[${inputFiles++}:0]loop=loop=-1:size=1:start=0[looped];[looped]scale=${iW}:${iH}[videoy];[videoy]realtime[videox]`; + if ( + (typeof duration === 'undefined') + && + (typeof(streamStats.duration) !== 'undefined' ) + ) { + //add 150 milliseconds just in case, exact duration seems to cut out the last bits of music some times. + duration = `${streamStats.duration + 150}ms`; + } + videoComplex = `;[${inputFiles++}:0]loop=loop=-1:size=1:start=0[looped]`; + videoComplex += `;[looped]format=yuv420p[formatted]`; + let stream = "scaled"; + videoComplex +=`;[formatted]scale=w=${iW}:h=${iH}:force_original_aspect_ratio=1[scaled]`; + if (this.ensureResolution) { + stream = "padded"; + videoComplex += `;[scaled]pad=${iW}:${iH}:(ow-iw)/2:(oh-ih)/2[padded]`; + } + videoComplex +=`;[${stream}]realtime[videox]`; + stillImage = true; } else if (this.opts.errorScreen == 'static') { ffmpegArgs.push( '-f', 'lavfi', @@ -232,23 +262,17 @@ class FFMPEG extends events.EventEmitter { inputFiles++; videoComplex = `;drawtext=fontfile=${process.env.DATABASE}/font.ttf:fontsize=${sz1}:fontcolor=white:x=(w-text_w)/2:y=(h-text_h)/2:text='${streamUrl.errorTitle}',drawtext=fontfile=${process.env.DATABASE}/font.ttf:fontsize=${sz2}:fontcolor=white:x=(w-text_w)/2:y=(h+text_h+${sz3})/2:text='${streamUrl.subtitle}'[videoy];[videoy]realtime[videox]`; - } else if (this.opts.errorScreen == 'blank') { + } else { //blank ffmpegArgs.push( '-f', 'lavfi', '-i', `color=c=black:s=${iW}x${iH}` ); inputFiles++; videoComplex = `;realtime[videox]`; - } else {//'pic' - ffmpegArgs.push( - '-loop', '1', - '-i', `${this.errorPicturePath}`, - ); - inputFiles++; - videoComplex = `;[${videoFile+1}:0]scale=${iW}:${iH}[videoy];[videoy]realtime[videox]`; } } let durstr = `duration=${streamStats.duration}ms`; + if (typeof(streamUrl.errorTitle) !== 'undefined') { //silent audioComplex = `;aevalsrc=0:${durstr}[audioy]`; if ( streamUrl.errorTitle == 'offline' ) { @@ -280,8 +304,9 @@ class FFMPEG extends events.EventEmitter { ffmpegArgs.push('-pix_fmt' , 'yuv420p' ); } audioComplex += ';[audioy]arealtime[audiox]'; - currentVideo = "[videox]"; currentAudio = "[audiox]"; + } + currentVideo = "[videox]"; } if (doOverlay) { if (watermark.animated === true) { @@ -297,9 +322,13 @@ class FFMPEG extends events.EventEmitter { let algo = this.opts.scalingAlgorithm; let resizeMsg = ""; if ( + (!streamStats.audioOnly) + && + ( (this.ensureResolution && ( streamStats.anamorphic || (iW != this.wantedW || iH != this.wantedH) ) ) || isLargerResolution(iW, iH, this.wantedW, this.wantedH) + ) ) { //scaler stuff, need to change the size of the video and also add bars // calculate wanted aspect ratio @@ -444,6 +473,9 @@ class FFMPEG extends events.EventEmitter { `-c:v`, (transcodeVideo ? this.opts.videoEncoder : 'copy'), `-sc_threshold`, `1000000000`, ); + if (stillImage) { + ffmpegArgs.push('-tune', 'stillimage'); + } } ffmpegArgs.push( '-map', currentAudio, @@ -506,14 +538,14 @@ class FFMPEG extends events.EventEmitter { `service_provider="dizqueTV"`, `-metadata`, `service_name="${this.channel.name}"`, - `-f`, `mpegts`); + ); - //t should be before output + //t should be before -f if (typeof duration !== 'undefined') { - ffmpegArgs.push(`-t`, duration) + ffmpegArgs.push(`-t`, `${duration}`); } - ffmpegArgs.push(`pipe:1`) + ffmpegArgs.push(`-f`, `mpegts`, `pipe:1`) let doLogs = this.opts.logFfmpeg && !isConcatPlaylist; if (this.hasBeenKilled) { @@ -521,6 +553,7 @@ class FFMPEG extends events.EventEmitter { } this.ffmpeg = spawn(this.ffmpegPath, ffmpegArgs, { stdio: ['ignore', 'pipe', (doLogs?process.stderr:"ignore") ] } ); if (this.hasBeenKilled) { + console.log("Send SIGKILL to ffmpeg"); this.ffmpeg.kill("SIGKILL"); return; } diff --git a/src/plex-player.js b/src/plex-player.js index 4874bbd..d21bcdc 100644 --- a/src/plex-player.js +++ b/src/plex-player.js @@ -124,11 +124,7 @@ class PlexPlayer { return emitter; } catch(err) { - if (err instanceof Error) { - throw err; - } else { - return Error("Error when playing plex program: " + JSON.stringify(err) ); - } + return Error("Error when playing plex program: " + JSON.stringify(err) ); } } } diff --git a/src/plexTranscoder.js b/src/plexTranscoder.js index 8106572..2c3fbc6 100644 --- a/src/plexTranscoder.js +++ b/src/plexTranscoder.js @@ -35,6 +35,10 @@ class PlexTranscoder { this.updateInterval = 30000 this.updatingPlex = undefined this.playState = "stopped" + this.albumArt = { + attempted : false, + path: null, + } } async getStream(deinterlace) { @@ -53,7 +57,7 @@ class PlexTranscoder { } else { try { this.log("Setting transcoding parameters") - this.setTranscodingArgs(stream.directPlay, true, deinterlace) + this.setTranscodingArgs(stream.directPlay, true, deinterlace, true) await this.getDecision(stream.directPlay); if (this.isDirectPlay()) { stream.directPlay = true; @@ -110,13 +114,14 @@ class PlexTranscoder { return stream } - setTranscodingArgs(directPlay, directStream, deinterlace) { + setTranscodingArgs(directPlay, directStream, deinterlace, firstTry) { let resolution = (directStream) ? this.settings.maxPlayableResolution : this.settings.maxTranscodeResolution let bitrate = (directStream) ? this.settings.directStreamBitrate : this.settings.transcodeBitrate let mediaBufferSize = (directStream) ? this.settings.mediaBufferSize : this.settings.transcodeMediaBufferSize let subtitles = (this.settings.enableSubtitles) ? "burn" : "none" // subtitle options: burn, none, embedded, sidecar let streamContainer = "mpegts" // Other option is mkv, mkv has the option of copying it's subs for later processing - let isDirectPlay = (directPlay) ? '1' : '0' + let isDirectPlay = (directPlay) ? '1' : (firstTry? '': '0'); + let hasMDE = '1'; let videoQuality=`100` // Not sure how this applies, maybe this works if maxVideoBitrate is not set let profileName=`Generic` // Blank profile, everything is specified through X-Plex-Client-Profile-Extra @@ -166,7 +171,7 @@ X-Plex-Token=${this.server.accessToken}&\ X-Plex-Client-Profile-Extra=${clientProfile_enc}&\ protocol=${this.settings.streamProtocol}&\ Connection=keep-alive&\ -hasMDE=1&\ +hasMDE=${hasMDE}&\ path=${this.key}&\ mediaIndex=0&\ partIndex=0&\ @@ -206,6 +211,9 @@ lang=en` isDirectPlay() { try { + if (this.getVideoStats().audioOnly) { + return this.getVideoStats().audioDecision === "copy"; + } return this.getVideoStats().videoDecision === "copy" && this.getVideoStats().audioDecision === "copy"; } catch (e) { console.log("Error at decision:" , e); @@ -217,7 +225,6 @@ lang=en` let ret = {} try { let streams = this.decisionJson.MediaContainer.Metadata[0].Media[0].Part[0].Stream - ret.duration = parseFloat( this.decisionJson.MediaContainer.Metadata[0].Media[0].Part[0].duration ); streams.forEach(function (_stream, $index) { // Video @@ -257,6 +264,14 @@ lang=en` } catch (e) { console.log("Error at decision:" , e); } + if (typeof(ret.videoCodec) === 'undefined') { + ret.audioOnly = true; + ret.placeholderImage = (this.albumArt.path != null) ? + ret.placeholderImage = this.albumArt.path + : + ret.placeholderImage = `http://localhost:${process.env.PORT}/images/generic-music-screen.png` + ; + } this.log("Current video stats:") this.log(ret) @@ -300,23 +315,56 @@ lang=en` } async getDecisionUnmanaged(directPlay) { - let res = await axios.get(`${this.server.uri}/video/:/transcode/universal/decision?${this.transcodingArgs}`, { + let url = `${this.server.uri}/video/:/transcode/universal/decision?${this.transcodingArgs}`; + let res = await axios.get(url, { headers: { Accept: 'application/json' } }) this.decisionJson = res.data; - this.log("Recieved transcode decision:") + this.log("Received transcode decision:"); this.log(res.data) // Print error message if transcode not possible // TODO: handle failure better - let transcodeDecisionCode = res.data.MediaContainer.transcodeDecisionCode - if (!(directPlay || transcodeDecisionCode == "1001")) { + let transcodeDecisionCode = res.data.MediaContainer.transcodeDecisionCode; + if ( + ( typeof(transcodeDecisionCode) === 'undefined' ) + ) { + this.decisionJson.MediaContainer.transcodeDecisionCode = 'novideo'; + console.log("Audio-only file detected"); + await this.tryToGetAlbumArt(); + } else if (!(directPlay || transcodeDecisionCode == "1001")) { console.log(`IMPORTANT: Recieved transcode decision code ${transcodeDecisionCode}! Expected code 1001.`) console.log(`Error message: '${res.data.MediaContainer.transcodeDecisionText}'`) } } + async tryToGetAlbumArt() { + try { + if(this.albumArt.attempted ) { + return; + } + this.albumArt.attempted = true; + + this.log("Try to get album art:"); + let url = `${this.server.uri}${this.key}?${this.transcodingArgs}`; + let res = await axios.get(url, { + headers: { Accept: 'application/json' } + }); + let mediaContainer = res.data.MediaContainer; + if (typeof(mediaContainer) !== 'undefined') { + for( let i = 0; i < mediaContainer.Metadata.length; i++) { + console.log("got art: " + mediaContainer.Metadata[i].thumb ); + this.albumArt.path = `${this.server.uri}${mediaContainer.Metadata[i].thumb}?${this.transcodingArgs}`; + } + } + } catch (err) { + console.error("Error when getting album art", err); + } + + + } + async getDecision(directPlay) { try { await this.getDecisionUnmanaged(directPlay); diff --git a/src/svg/generic-music-screen.svg b/src/svg/generic-music-screen.svg new file mode 100644 index 0000000..8ba273b --- /dev/null +++ b/src/svg/generic-music-screen.svg @@ -0,0 +1,115 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + From 9982f3c3dbc68c5e860c621376dd0f5b3a5bbe81 Mon Sep 17 00:00:00 2001 From: vexorian Date: Sat, 23 Jan 2021 20:39:30 -0400 Subject: [PATCH 12/57] Music Libraries UI --- web/directives/channel-config.js | 5 +++++ web/public/templates/plex-library.html | 6 +++--- web/public/templates/program-config.html | 21 ++++++++++++++++++++- web/services/plex.js | 24 +++++++++++++++++++----- 4 files changed, 47 insertions(+), 9 deletions(-) diff --git a/web/directives/channel-config.js b/web/directives/channel-config.js index 1a7055b..df76d16 100644 --- a/web/directives/channel-config.js +++ b/web/directives/channel-config.js @@ -525,6 +525,11 @@ module.exports = function ($timeout, $location, dizquetv, resolutionOptions) { if ( angle >= 350 || angle < 10 ) { angle += 53; } + } else if (program.type === 'track') { + r = 10, g = 10, b = 10; + r2 = 245, g2 = 245, b2 = 245; + angle = 315; + w = 2; } else { r = 10, g = 10, b = 10; r2 = 245, g2 = 245, b2 = 245; diff --git a/web/public/templates/plex-library.html b/web/public/templates/plex-library.html index 8e2c0d2..aa06c8d 100644 --- a/web/public/templates/plex-library.html +++ b/web/public/templates/plex-library.html @@ -37,7 +37,7 @@ {{a.title}} - +
@@ -57,7 +57,7 @@ - + @@ -81,7 +81,7 @@ class="flex-pull-right"> {{c.durationStr}} - + diff --git a/web/public/templates/program-config.html b/web/public/templates/program-config.html index ce48dab..015e6d1 100644 --- a/web/public/templates/program-config.html +++ b/web/public/templates/program-config.html @@ -4,14 +4,33 @@ +
+
+ + + +
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 From 42330a1215ca278209af2392fa63bff30208776d Mon Sep 17 00:00:00 2001 From: vexorian Date: Sun, 24 Jan 2021 20:56:00 -0400 Subject: [PATCH 17/57] Cleanup settings service stuff. --- web/app.js | 1 - web/directives/cache-settings.js | 23 ----------------------- web/public/templates/cache-settings.html | 15 --------------- web/public/views/settings.html | 6 ------ 4 files changed, 45 deletions(-) delete mode 100644 web/directives/cache-settings.js delete mode 100644 web/public/templates/cache-settings.html diff --git a/web/app.js b/web/app.js index 18d161c..af58820 100644 --- a/web/app.js +++ b/web/app.js @@ -15,7 +15,6 @@ 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')) diff --git a/web/directives/cache-settings.js b/web/directives/cache-settings.js deleted file mode 100644 index d37fd99..0000000 --- a/web/directives/cache-settings.js +++ /dev/null @@ -1,23 +0,0 @@ -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(); - }); - }; - } - } -} \ No newline at end of file diff --git a/web/public/templates/cache-settings.html b/web/public/templates/cache-settings.html deleted file mode 100644 index 376f8c9..0000000 --- a/web/public/templates/cache-settings.html +++ /dev/null @@ -1,15 +0,0 @@ -
-
Cache
- -
-
-
{{setting.title}}
-
- -
-
-
- -
\ No newline at end of file diff --git a/web/public/views/settings.html b/web/public/views/settings.html index 54d0bda..5ef4ff3 100644 --- a/web/public/views/settings.html +++ b/web/public/views/settings.html @@ -20,16 +20,10 @@ HDHR
-
- \ No newline at end of file From 1978a9e8376e30508c716f7e9d4d6f04339639c4 Mon Sep 17 00:00:00 2001 From: vexorian Date: Sun, 24 Jan 2021 21:33:30 -0400 Subject: [PATCH 18/57] #106 Allow group-title customization in channels. --- src/api.js | 7 +++++ src/database-migration.js | 29 ++++++++++++++++++-- src/services/m3u-service.js | 2 +- web/directives/channel-config.js | 9 ++++++ web/public/templates/channel-config.html | 35 ++++++++++++++++-------- 5 files changed, 68 insertions(+), 14 deletions(-) diff --git a/src/api.js b/src/api.js index 4f46328..5ab4a41 100644 --- a/src/api.js +++ b/src/api.js @@ -627,6 +627,13 @@ function api(db, channelDB, fillerDB, xmltvInterval, guideService, _m3uService } function cleanUpChannel(channel) { + if ( + (typeof(channel.groupTitle) === 'undefined') + || + (channel.groupTitle === '') + ) { + channel.groupTitle = "dizqueTV"; + } channel.programs.forEach( cleanUpProgram ); delete channel.fillerContent; delete channel.filler; diff --git a/src/database-migration.js b/src/database-migration.js index 2634fd5..4cf6321 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 = 801; +const TARGET_VERSION = 802; const STEPS = [ // [v, v2, x] : if the current version is v, call x(db), and version becomes v2 @@ -41,6 +41,7 @@ const STEPS = [ // but we have to migrate it to 800 using the reAddIcon. [ 702, 800, (db,channels,dir) => reAddIcon(dir) ], [ 800, 801, (db) => addImageCache(db) ], + [ 801, 802, () => addGroupTitle() ], ] const { v4: uuidv4 } = require('uuid'); @@ -741,7 +742,7 @@ function migrateWatermark(db, channelDB) { return channel; } - console.log("Extracting fillers from channels..."); + console.log("Migrating watermarks..."); let channels = path.join(process.env.DATABASE, 'channels'); let channelFiles = fs.readdirSync(channels); for (let i = 0; i < channelFiles.length; i++) { @@ -811,6 +812,30 @@ function addImageCache(db) { fs.writeFileSync( f, JSON.stringify( [xmltvSettings] ) ); } +function addGroupTitle() { + + function migrateChannel(channel) { + channel.groupTitle= "dizqueTV"; + return channel; + } + + console.log("Adding group title to channels..."); + let channels = path.join(process.env.DATABASE, 'channels'); + let channelFiles = fs.readdirSync(channels); + for (let i = 0; i < channelFiles.length; i++) { + if (path.extname( channelFiles[i] ) === '.json') { + console.log("Adding group title to channel : " + channelFiles[i] +"..." ); + let channelPath = path.join(channels, channelFiles[i]); + let channel = JSON.parse(fs.readFileSync(channelPath, 'utf-8')); + channel = migrateChannel(channel); + fs.writeFileSync( channelPath, JSON.stringify(channel), 'utf-8'); + } + } + console.log("Done migrating group titles in channels."); +} + + + module.exports = { initDB: initDB, diff --git a/src/services/m3u-service.js b/src/services/m3u-service.js index 674e200..f3563b5 100644 --- a/src/services/m3u-service.js +++ b/src/services/m3u-service.js @@ -50,7 +50,7 @@ class M3uService { 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 += `#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="${channels[i].groupTitle}",${channels[i].name}\n` data += `{{host}}/video?channel=${channels[i].number}\n` } } diff --git a/web/directives/channel-config.js b/web/directives/channel-config.js index df76d16..fd45176 100644 --- a/web/directives/channel-config.js +++ b/web/directives/channel-config.js @@ -60,6 +60,7 @@ module.exports = function ($timeout, $location, dizquetv, resolutionOptions) { scope.channel.guideMinimumDurationSeconds = 5 * 60; scope.isNewChannel = true scope.channel.icon = `${$location.protocol()}://${location.host}/images/dizquetv.png` + scope.channel.groupTitle = "dizqueTV"; scope.channel.disableFillerOverlay = true; scope.channel.iconWidth = 120 scope.channel.iconDuration = 60 @@ -95,6 +96,14 @@ module.exports = function ($timeout, $location, dizquetv, resolutionOptions) { scope.channel.watermark = defaultWatermark(); } + if ( + (typeof(scope.channel.groupTitle) === 'undefined') + || + (scope.channel.groupTitle === '') + ) { + scope.channel.groupTitle = "dizqueTV"; + } + if (typeof(scope.channel.fillerRepeatCooldown) === 'undefined') { scope.channel.fillerRepeatCooldown = 30 * 60 * 1000; } diff --git a/web/public/templates/channel-config.html b/web/public/templates/channel-config.html index 506265a..53606da 100644 --- a/web/public/templates/channel-config.html +++ b/web/public/templates/channel-config.html @@ -21,16 +21,28 @@ \ No newline at end of file From d6b2bd1d5e161c89f98cee87d5f516f4e06e6d70 Mon Sep 17 00:00:00 2001 From: vexorian Date: Fri, 19 Feb 2021 16:20:46 -0400 Subject: [PATCH 21/57] Random Slots --- src/api.js | 19 +- src/services/random-slots-service.js | 469 ++++++++++++++++++ web/app.js | 1 + web/directives/channel-config.js | 26 +- .../random-slots-schedule-editor.js | 338 +++++++++++++ web/directives/time-slots-schedule-editor.js | 2 +- web/public/templates/channel-config.html | 18 +- .../random-slots-schedule-editor.html | 185 +++++++ web/services/dizquetv.js | 13 + 9 files changed, 1063 insertions(+), 8 deletions(-) create mode 100644 src/services/random-slots-service.js create mode 100644 web/directives/random-slots-schedule-editor.js create mode 100644 web/public/templates/random-slots-schedule-editor.html diff --git a/src/api.js b/src/api.js index 5ab4a41..9c41ef6 100644 --- a/src/api.js +++ b/src/api.js @@ -8,8 +8,9 @@ const constants = require('./constants'); const FFMPEGInfo = require('./ffmpeg-info'); 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 randomSlotsService = require('./services/random-slots-service'); module.exports = { router: api } function api(db, channelDB, fillerDB, xmltvInterval, guideService, _m3uService ) { @@ -583,9 +584,23 @@ function api(db, channelDB, fillerDB, xmltvInterval, guideService, _m3uService //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') { + console.error("time slots error: " + toolRes.userError); + return res.status(400).send(toolRes.userError); + } + res.status(200).send(toolRes); + } catch(err) { + console.error(err); + res.status(500).send("Internal error"); + } + }); + + router.post('/api/channel-tools/random-slots', async (req, res) => { + try { + let toolRes = await randomSlotsService(req.body.programs, req.body.schedule); + if ( typeof(toolRes.userError) !=='undefined') { + console.error("random slots error: " + toolRes.userError); return res.status(400).send(toolRes.userError); } res.status(200).send(toolRes); diff --git a/src/services/random-slots-service.js b/src/services/random-slots-service.js new file mode 100644 index 0000000..931eeac --- /dev/null +++ b/src/services/random-slots-service.js @@ -0,0 +1,469 @@ +const constants = require("../constants"); + +const random = require('../helperFuncs').random; + +const MINUTE = 60*1000; +const DAY = 24*60*MINUTE; +const LIMIT = 40000; + + + +//This is a quadruplicate code, but maybe it doesn't have to be? +function getShow(program) { + //used for equalize and frequency tweak + if (program.isOffline) { + if (program.type == 'redirect') { + return { + description : `Redirect to channel ${program.channel}`, + id: "redirect." + program.channel, + channel: program.channel, + } + } else { + return null; + } + } else if ( (program.type == 'episode') && ( typeof(program.showTitle) !== 'undefined' ) ) { + return { + description: program.showTitle, + id: "tv." + program.showTitle, + } + } else { + return { + description: "Movies", + id: "movie.", + } + } +} + + +function shuffle(array, lo, hi ) { + if (typeof(lo) === 'undefined') { + lo = 0; + hi = array.length; + } + let currentIndex = hi, temporaryValue, randomIndex + while (lo !== currentIndex) { + randomIndex = random.integer(lo, currentIndex-1); + currentIndex -= 1 + temporaryValue = array[currentIndex] + array[currentIndex] = array[randomIndex] + array[randomIndex] = temporaryValue + } + return array +} + +function _wait(t) { + return new Promise((resolve) => { + setTimeout(resolve, t); + }); +} + +function getProgramId(program) { + let s = program.serverKey; + if (typeof(s) === 'undefined') { + s = 'unknown'; + } + let p = program.key; + if (typeof(p) === 'undefined') { + p = 'unknown'; + } + return s + "|" + p; +} + +function addProgramToShow(show, program) { + if ( (show.id == 'flex.') || show.id.startsWith("redirect.") ) { + //nothing to do + return; + } + let id = getProgramId(program) + if(show.programs[id] !== true) { + show.programs.push(program); + show.programs[id] = true + } +} + +function getShowOrderer(show) { + if (typeof(show.orderer) === 'undefined') { + + let sortedPrograms = JSON.parse( JSON.stringify(show.programs) ); + sortedPrograms.sort((a, b) => { + if (a.season === b.season) { + if (a.episode > b.episode) { + return 1 + } else { + return -1 + } + } else if (a.season > b.season) { + return 1; + } else if (b.season > a.season) { + return -1; + } else { + return 0 + } + }); + + let position = 0; + while ( + (position + 1 < sortedPrograms.length ) + && + ( + show.founder.season !== sortedPrograms[position].season + || + show.founder.episode !== sortedPrograms[position].episode + ) + ) { + position++; + } + + + show.orderer = { + + current : () => { + return sortedPrograms[position]; + }, + + next: () => { + position = (position + 1) % sortedPrograms.length; + }, + + } + } + return show.orderer; +} + + +function getShowShuffler(show) { + if (typeof(show.shuffler) === 'undefined') { + if (typeof(show.programs) === 'undefined') { + throw Error(show.id + " has no programs?") + } + + let randomPrograms = JSON.parse( JSON.stringify(show.programs) ); + let n = randomPrograms.length; + shuffle( randomPrograms, 0, n); + let position = 0; + + show.shuffler = { + + current : () => { + return randomPrograms[position]; + }, + + next: () => { + position++; + if (position == n) { + let a = Math.floor(n / 2); + shuffle(randomPrograms, 0, a ); + shuffle(randomPrograms, a, n ); + position = 0; + } + }, + + } + } + return show.shuffler; +} + +module.exports = async( programs, schedule ) => { + if (! Array.isArray(programs) ) { + return { userError: 'Expected a programs array' }; + } + if (typeof(schedule) === 'undefined') { + return { userError: 'Expected a schedule' }; + } + //verify that the schedule is in the correct format + if (! Array.isArray(schedule.slots) ) { + return { userError: 'Expected a "slots" array in schedule' }; + } + if (typeof(schedule).period === 'undefined') { + schedule.period = DAY; + } + for (let i = 0; i < schedule.slots.length; i++) { + if (typeof(schedule.slots[i].duration) === 'undefined') { + return { userError: "Each slot should have a duration" }; + } + if (typeof(schedule.slots[i].showId) === 'undefined') { + return { userError: "Each slot should have a showId" }; + } + if ( + (schedule.slots[i].duration <= 0) + || (Math.floor(schedule.slots[i].duration) != schedule.slots[i].duration) + ) { + return { userError: "Slot duration should be a integer number of milliseconds greater than 0" }; + } + if ( isNaN(schedule.slots[i].cooldown) ) { + schedule.slots[i].cooldown = 0; + } + if ( isNaN(schedule.slots[i].weight) ) { + schedule.slots[i].weight = 1; + } + } + if (typeof(schedule.pad) === 'undefined') { + return { userError: "Expected schedule.pad" }; + } + if (typeof(schedule.maxDays) == 'undefined') { + return { userError: "schedule.maxDays must be defined." }; + } + if (typeof(schedule.flexPreference) === 'undefined') { + schedule.flexPreference = "distribute"; + } + if (typeof(schedule.padStyle) === 'undefined') { + schedule.padStyle = "slot"; + } + if (schedule.padStyle !== "slot" && schedule.padStyle !== "episode") { + return { userError: `Invalid schedule.padStyle value: "${schedule.padStyle}"` }; + } + let flexBetween = ( schedule.flexPreference !== "end" ); + + // throttle so that the stream is not affected negatively + let steps = 0; + let throttle = async() => { + if (steps++ == 10) { + steps = 0; + await _wait(1); + } + } + + let showsById = {}; + let shows = []; + + function getNextForSlot(slot, remaining) { + //remaining doesn't restrict what next show is picked. It is only used + //for shows with flexible length (flex and redirects) + if (slot.showId === "flex.") { + return { + isOffline: true, + duration: remaining, + } + } + let show = shows[ showsById[slot.showId] ]; + if (slot.showId.startsWith("redirect.")) { + return { + isOffline: true, + type: "redirect", + duration: remaining, + channel: show.channel, + } + } else if (slot.order === 'shuffle') { + return getShowShuffler(show).current(); + } else if (slot.order === 'next') { + return getShowOrderer(show).current(); + } + } + + function advanceSlot(slot) { + if ( (slot.showId === "flex.") || (slot.showId.startsWith("redirect") ) ) { + return; + } + let show = shows[ showsById[slot.showId] ]; + if (slot.order === 'shuffle') { + return getShowShuffler(show).next(); + } else if (slot.order === 'next') { + return getShowOrderer(show).next(); + } + } + + function makePadded(item) { + let padOption = schedule.pad; + if (schedule.padStyle === "slot") { + padOption = 1; + } + let x = item.duration; + let m = x % padOption; + let f = 0; + if ( (m > constants.SLACK) && (padOption - m > constants.SLACK) ) { + f = padOption - m; + } + return { + item: item, + pad: f, + totalDuration: item.duration + f, + } + + } + + // load the programs + for (let i = 0; i < programs.length; i++) { + let p = programs[i]; + let show = getShow(p); + if (show != null) { + if (typeof(showsById[show.id] ) === 'undefined') { + showsById[show.id] = shows.length; + shows.push( show ); + show.founder = p; + show.programs = []; + } else { + show = shows[ showsById[show.id] ]; + } + addProgramToShow( show, p ); + } + } + + let s = schedule.slots; + let ts = (new Date() ).getTime(); + let curr = ts - ts % DAY; + let t0 = ts; + let p = []; + let t = t0; + let wantedFinish = 0; + let hardLimit = t0 + schedule.maxDays * DAY; + + let pushFlex = (d) => { + if (d > 0) { + t += d; + if ( (p.length > 0) && p[p.length-1].isOffline && (p[p.length-1].type != 'redirect') ) { + p[p.length-1].duration += d; + } else { + p.push( { + duration: d, + isOffline : true, + } ); + } + } + } + + let slotLastPlayed = {}; + + while ( (t < hardLimit) && (p.length < LIMIT) ) { + await throttle(); + //ensure t is padded + let m = t % schedule.pad; + if ( (t % schedule.pad > constants.SLACK) && (schedule.pad - m > constants.SLACK) ) { + pushFlex( schedule.pad - m ); + continue; + } + + let slot = null; + let slotIndex = null; + let remaining = null; + + let n = 0; + let minNextTime = t + 24*DAY; + for (let i = 0; i < s.length; i++) { + if ( typeof( slotLastPlayed[i] ) !== undefined ) { + let lastt = slotLastPlayed[i]; + minNextTime = Math.min( minNextTime, lastt + s[i].cooldown ); + if (t - lastt < s[i].cooldown - constants.SLACK ) { + continue; + } + } + n += s[i].weight; + if ( random.bool(s[i].weight,n) ) { + slot = s[i]; + slotIndex = i; + remaining = s[i].duration; + } + } + if (slot == null) { + //Nothing to play, likely due to cooldown + pushFlex( minNextTime - t); + continue; + } + let item = getNextForSlot(slot, remaining); + + if (item.isOffline) { + //flex or redirect. We can just use the whole duration + p.push(item); + t += remaining; + slotLastPlayed[ slotIndex ] = t; + continue; + } + if (item.duration > remaining) { + // Slide + p.push(item); + t += item.duration; + slotLastPlayed[ slotIndex ] = t; + advanceSlot(slot); + continue; + } + + let padded = makePadded(item); + let total = padded.totalDuration; + advanceSlot(slot); + let pads = [ padded ]; + + while(true) { + let item2 = getNextForSlot(slot); + if (total + item2.duration > remaining) { + break; + } + let padded2 = makePadded(item2); + pads.push(padded2); + advanceSlot(slot); + total += padded2.totalDuration; + } + let temt = t + total; + let rem = 0; + if ( + (temt % schedule.pad >= constants.SLACK) + && (temt % schedule.pad < schedule.pad - constants.SLACK) + ) { + rem = schedule.pad - temt % schedule.pad; + } + + + if (flexBetween && (schedule.padStyle === 'episode') ) { + let div = Math.floor(rem / schedule.pad ); + let mod = rem % schedule.pad; + // add mod to the latest item + pads[ pads.length - 1].pad += mod; + pads[ pads.length - 1].totalDuration += mod; + + let sortedPads = pads.map( (p, $index) => { + return { + pad: p.pad, + index : $index, + } + }); + sortedPads.sort( (a,b) => { return a.pad - b.pad; } ); + for (let i = 0; i < pads.length; i++) { + let q = Math.floor( div / pads.length ); + if (i < div % pads.length) { + q++; + } + let j = sortedPads[i].index; + pads[j].pad += q * schedule.pad; + } + } else if (flexBetween) { + //just distribute it equitatively + let div = rem / pads.length; + for (let i = 0; i < pads.length; i++) { + pads[i].pad += div; + } + } else { + //also add div to the latest item + pads[ pads.length - 1].pad += rem; + pads[ pads.length - 1].totalDuration += rem; + } + // now unroll them all + for (let i = 0; i < pads.length; i++) { + p.push( pads[i].item ); + t += pads[i].item.duration; + slotLastPlayed[ slotIndex ] = t; + pushFlex( pads[i].pad ); + } + } + while ( (t > hardLimit) || (p.length >= LIMIT) ) { + t -= p.pop().duration; + } + let m = t % schedule.period; + let rem = 0; + if (m > wantedFinish) { + rem = schedule.period + wantedFinish - m; + } else if (m < wantedFinish) { + rem = wantedFinish - m; + } + if (rem > constants.SLACK) { + pushFlex(rem); + } + + + return { + programs: p, + startTime: (new Date(t0)).toISOString(), + } + +} + + + + diff --git a/web/app.js b/web/app.js index 9cfa66d..b1d9887 100644 --- a/web/app.js +++ b/web/app.js @@ -27,6 +27,7 @@ app.directive('channelRedirect', require('./directives/channel-redirect')) app.directive('plexServerEdit', require('./directives/plex-server-edit')) app.directive('channelConfig', require('./directives/channel-config')) app.directive('timeSlotsScheduleEditor', require('./directives/time-slots-schedule-editor')) +app.directive('randomSlotsScheduleEditor', require('./directives/random-slots-schedule-editor')) app.controller('settingsCtrl', require('./controllers/settings')) app.controller('channelsCtrl', require('./controllers/channels')) diff --git a/web/directives/channel-config.js b/web/directives/channel-config.js index fbbff90..8f15518 100644 --- a/web/directives/channel-config.js +++ b/web/directives/channel-config.js @@ -1846,8 +1846,7 @@ module.exports = function ($timeout, $location, dizquetv, resolutionOptions) { return false; } - - scope.onTimeSlotsDone = (slotsResult) => { + let readSlotsResult = (slotsResult) => { scope.channel.programs = slotsResult.programs; let t = (new Date()).getTime(); @@ -1857,7 +1856,6 @@ module.exports = function ($timeout, $location, dizquetv, resolutionOptions) { total += slotsResult.programs[i].duration; } - scope.channel.scheduleBackup = slotsResult.schedule; while(t1 > t) { //TODO: Replace with division @@ -1866,10 +1864,27 @@ module.exports = function ($timeout, $location, dizquetv, resolutionOptions) { scope.channel.startTime = new Date(t1); adjustStartTimeToCurrentProgram(); updateChannelDuration(); + + }; + + + scope.onTimeSlotsDone = (slotsResult) => { + scope.channel.scheduleBackup = slotsResult.schedule; + readSlotsResult(slotsResult); } + + scope.onRandomSlotsDone = (slotsResult) => { + scope.channel.randomScheduleBackup = slotsResult.schedule; + readSlotsResult(slotsResult); + } + + scope.onTimeSlotsButtonClick = () => { scope.timeSlots.startDialog(scope.channel.programs, scope.maxSize, scope.channel.scheduleBackup ); } + scope.onRandomSlotsButtonClick = () => { + scope.randomSlots.startDialog(scope.channel.programs, scope.maxSize, scope.channel.randomScheduleBackup ); + } scope.logoOnChange = (event) => { const formData = new FormData(); @@ -1892,9 +1907,14 @@ module.exports = function ($timeout, $location, dizquetv, resolutionOptions) { pre: function(scope) { scope.timeSlots = null; + scope.randomSlots = null; scope.registerTimeSlots = (timeSlots) => { scope.timeSlots = timeSlots; } + scope.registerRandomSlots = (randomSlots) => { + scope.randomSlots = randomSlots; + } + }, } diff --git a/web/directives/random-slots-schedule-editor.js b/web/directives/random-slots-schedule-editor.js new file mode 100644 index 0000000..7a6f334 --- /dev/null +++ b/web/directives/random-slots-schedule-editor.js @@ -0,0 +1,338 @@ + +module.exports = function ($timeout, dizquetv) { + const MINUTE = 60*1000; + const HOUR = 60*MINUTE; + const DAY = 24*HOUR; + const WEEK = 7 * DAY; + + return { + restrict: 'E', + templateUrl: 'templates/random-slots-schedule-editor.html', + replace: true, + scope: { + linker: "=linker", + onDone: "=onDone" + }, + + link: function (scope, element, attrs) { + scope.limit = 50000; + scope.visible = false; + + scope.badTimes = false; + scope._editedTime = null; + let showsById; + let shows; + + + function reset() { + showsById = {}; + shows = []; + scope.schedule = { + maxDays: 365, + flexPreference : "distribute", + padStyle: "slot", + randomDistribution: "uniform", + slots : [], + pad: 1, + } + } + reset(); + + function loadBackup(backup) { + scope.schedule = JSON.parse( JSON.stringify(backup) ); + if (typeof(scope.schedule.pad) == 'undefined') { + scope.schedule.pad = 1; + } + let slots = scope.schedule.slots; + for (let i = 0; i < slots.length; i++) { + let found = false; + for (let j = 0; j < scope.showOptions.length; j++) { + if (slots[i].showId == scope.showOptions[j].id) { + found = true; + } + } + if (! found) { + slots[i].showId = "flex."; + slots[i].order = "shuffle"; + } + } + if (typeof(scope.schedule.flexPreference) === 'undefined') { + scope.schedule.flexPreference = "distribute"; + } + if (typeof(scope.schedule.padStyle) === 'undefined') { + scope.schedule.padStyle = "slot"; + } + if (typeof(scope.schedule.randomDistribution) === 'undefined') { + scope.schedule.randomDistribution = "uniform"; + } + + scope.refreshSlots(); + + } + + getTitle = (index) => { + let showId = scope.schedule.slots[index].showId; + for (let i = 0; i < scope.showOptions.length; i++) { + if (scope.showOptions[i].id == showId) { + return scope.showOptions[i].description; + } + } + return "Unknown"; + } + scope.isWeekly = () => { + return (scope.schedule.period === WEEK); + }; + scope.addSlot = () => { + scope.schedule.slots.push( + { + duration: 30 * MINUTE, + showId: "flex.", + order: "next", + cooldown : 0, + } + ); + } + scope.timeColumnClass = () => { + return { "col-md-1": true}; + } + scope.programColumnClass = () => { + return { "col-md-6": true}; + }; + scope.durationOptions = [ + { id: 5 * MINUTE , description: "5 Minutes" }, + { id: 10 * MINUTE , description: "10 Minutes" }, + { id: 15 * MINUTE , description: "15 Minutes" }, + { id: 20 * MINUTE , description: "20 Minutes" }, + { id: 25 * MINUTE , description: "25 Minutes" }, + { id: 30 * MINUTE , description: "30 Minutes" }, + { id: 45 * MINUTE , description: "45 Minutes" }, + { id: 1 * HOUR , description: "1 Hour" }, + { id: 90 * MINUTE , description: "90 Minutes" }, + { id: 100 * MINUTE , description: "100 Minutes" }, + { id: 2 * HOUR , description: "2 Hours" }, + { id: 3 * HOUR , description: "3 Hours" }, + { id: 4 * HOUR , description: "4 Hours" }, + { id: 5 * HOUR , description: "5 Hours" }, + { id: 6 * HOUR , description: "6 Hours" }, + { id: 8 * HOUR , description: "8 Hours" }, + { id: 10* HOUR , description: "10 Hours" }, + { id: 12* HOUR , description: "12 Hours" }, + { id: 1 * DAY , description: "1 Day" }, + ]; + scope.cooldownOptions = [ + { id: 0 , description: "No cooldown" }, + { id: 1 * MINUTE , description: "1 Minute" }, + { id: 5 * MINUTE , description: "5 Minutes" }, + { id: 10 * MINUTE , description: "10 Minutes" }, + { id: 15 * MINUTE , description: "15 Minutes" }, + { id: 20 * MINUTE , description: "20 Minutes" }, + { id: 25 * MINUTE , description: "25 Minutes" }, + { id: 30 * MINUTE , description: "30 Minutes" }, + { id: 45 * MINUTE , description: "45 Minutes" }, + { id: 1 * HOUR , description: "1 Hour" }, + { id: 90 * MINUTE , description: "90 Minutes" }, + { id: 100 * MINUTE , description: "100 Minutes" }, + { id: 2 * HOUR , description: "2 Hours" }, + { id: 3 * HOUR , description: "3 Hours" }, + { id: 4 * HOUR , description: "4 Hours" }, + { id: 5 * HOUR , description: "5 Hours" }, + { id: 6 * HOUR , description: "6 Hours" }, + { id: 8 * HOUR , description: "8 Hours" }, + { id: 10* HOUR , description: "10 Hours" }, + { id: 12* HOUR , description: "12 Hours" }, + { id: 1 * DAY , description: "1 Day" }, + { id: 1 * DAY , description: "2 Days" }, + { id: 3 * DAY + 12 * HOUR , description: "3.5 Days" }, + { id: 7 * DAY , description: "1 Week" }, + ]; + + scope.flexOptions = [ + { id: "distribute", description: "Between videos" }, + { id: "end", description: "End of the slot" }, + ] + + scope.distributionOptions = [ + { id: "uniform", description: "Uniform" }, + { id: "weighted", description: "Weighted" }, + ] + + + scope.padOptions = [ + {id: 1, description: "Do not pad" }, + {id: 1*MINUTE, description: "0:00, 0:01, 0:02, ..., 0:59" }, + {id: 5*MINUTE, description: "0:00, 0:05, 0:10, ..., 0:55" }, + {id: 10*60*1000, description: "0:00, 0:10, 0:20, ..., 0:50" }, + {id: 15*60*1000, description: "0:00, 0:15, 0:30, ..., 0:45" }, + {id: 30*60*1000, description: "0:00, 0:30" }, + {id: 1*60*60*1000, description: "0:00" }, + ]; + scope.padStyleOptions = [ + {id: "episode" , description: "Pad Episodes" }, + {id: "slot" , description: "Pad Slots" }, + ]; + + scope.showOptions = []; + scope.orderOptions = [ + { id: "next", description: "Play Next" }, + { id: "shuffle", description: "Shuffle" }, + ]; + + let doIt = async() => { + let res = await dizquetv.calculateRandomSlots(scope.programs, scope.schedule ); + for (let i = 0; i < scope.schedule.slots.length; i++) { + delete scope.schedule.slots[i].weightPercentage; + } + res.schedule = scope.schedule; + return res; + } + + + + + let startDialog = (programs, limit, backup) => { + scope.limit = limit; + scope.programs = programs; + + reset(); + + + + programs.forEach( (p) => { + let show = getShow(p); + if (show != null) { + if (typeof(showsById[show.id]) === 'undefined') { + showsById[show.id] = shows.length; + shows.push( show ); + } else { + show = shows[ showsById[show.id] ]; + } + } + } ); + scope.showOptions = shows.map( (show) => { return show } ); + scope.showOptions.push( { + id: "flex.", + description: "Flex", + } ); + if (typeof(backup) !== 'undefined') { + loadBackup(backup); + } + + scope.visible = true; + } + + + scope.linker( { + startDialog: startDialog, + } ); + + scope.finished = async (cancel) => { + scope.error = null; + if (!cancel) { + try { + scope.loading = true; + $timeout(); + scope.onDone( await doIt() ); + scope.visible = false; + } catch(err) { + console.error("Unable to generate channel lineup", err); + scope.error = "There was an error processing the schedule"; + return; + } finally { + scope.loading = false; + $timeout(); + } + } else { + scope.visible = false; + } + } + + scope.deleteSlot = (index) => { + scope.schedule.slots.splice(index, 1); + } + + scope.hasTimeError = (slot) => { + return typeof(slot.timeError) !== 'undefined'; + } + + scope.disableCreateLineup = () => { + if (scope.badTimes) { + return true; + } + if (typeof(scope.schedule.maxDays) === 'undefined') { + return true; + } + if (scope.schedule.slots.length == 0) { + return true; + } + return false; + } + + scope.canShowSlot = (slot) => { + return (slot.showId != 'flex.') && !(slot.showId.startsWith('redirect.')); + } + + scope.refreshSlots = () => { + let sum = 0; + for (let i = 0; i < scope.schedule.slots.length; i++) { + sum += scope.schedule.slots[i].weight; + } + for (let i = 0; i < scope.schedule.slots.length; i++) { + if (scope.schedule.slots[i].showId == 'movie.') { + scope.schedule.slots[i].order = "shuffle"; + } + if ( isNaN(scope.schedule.slots[i].cooldown) ) { + scope.schedule.slots[i].cooldown = 0; + } + scope.schedule.slots[i].weightPercentage + = (100 * scope.schedule.slots[i].weight / sum).toFixed(2) + "%"; + } + $timeout(); + } + + scope.randomDistributionChanged = () => { + if (scope.schedule.randomDistribution === 'uniform') { + for (let i = 0; i < scope.schedule.slots.length; i++) { + scope.schedule.slots[i].weight = 1; + } + } else { + for (let i = 0; i < scope.schedule.slots.length; i++) { + scope.schedule.slots[i].weight = 300; + } + } + scope.refreshSlots(); + } + + + + } + }; +} + + +//This is a duplicate code, but maybe it doesn't have to be? +function getShow(program) { + //used for equalize and frequency tweak + if (program.isOffline) { + if (program.type == 'redirect') { + return { + description : `Redirect to channel ${program.channel}`, + id: "redirect." + program.channel, + channel: program.channel, + } + } else { + return null; + } + } else if ( (program.type == 'episode') && ( typeof(program.showTitle) !== 'undefined' ) ) { + return { + description: program.showTitle, + id: "tv." + program.showTitle, + } + } else { + return { + description: "Movies", + id: "movie.", + } + } +} + + diff --git a/web/directives/time-slots-schedule-editor.js b/web/directives/time-slots-schedule-editor.js index b3d34f5..bf673cc 100644 --- a/web/directives/time-slots-schedule-editor.js +++ b/web/directives/time-slots-schedule-editor.js @@ -68,7 +68,7 @@ module.exports = function ($timeout, dizquetv) { } } - getTitle = (index) => { + let getTitle = (index) => { let showId = scope.schedule.slots[index].showId; for (let i = 0; i < scope.showOptions.length; i++) { if (scope.showOptions[i].id == showId) { diff --git a/web/public/templates/channel-config.html b/web/public/templates/channel-config.html index 53606da..d0a655d 100644 --- a/web/public/templates/channel-config.html +++ b/web/public/templates/channel-config.html @@ -449,7 +449,7 @@

Slides the whole schedule. The "Fast-Forward" button will advance the stream by the specified amount of time. The "Rewind" button does the opposite.

-
+
-

A more advanced dialog wip description.

+

This allows to schedul specific shows to run at specific time slots of the day or a week. It's recommended you first populate the channel with the episodes from the shows you want to play and/or other content like movies and redirects.

+ +
+ +
+
+ +
+

This is similar to Time Slots, but instead of time sections, you pick a probability to play each tv show and the length of the block.

+
@@ -849,4 +862,5 @@ +
diff --git a/web/public/templates/random-slots-schedule-editor.html b/web/public/templates/random-slots-schedule-editor.html new file mode 100644 index 0000000..3cb0eff --- /dev/null +++ b/web/public/templates/random-slots-schedule-editor.html @@ -0,0 +1,185 @@ +
+ +
\ No newline at end of file diff --git a/web/services/dizquetv.js b/web/services/dizquetv.js index de70f0d..0c20a91 100644 --- a/web/services/dizquetv.js +++ b/web/services/dizquetv.js @@ -266,6 +266,19 @@ module.exports = function ($http, $q) { return d.data; }, + calculateRandomSlots: async( programs, schedule) => { + let d = await $http( { + method: "POST", + url : "/api/channel-tools/random-slots", + data: { + programs: programs, + schedule: schedule, + }, + headers: { 'Content-Type': 'application/json; charset=utf-8' }, + } ); + return d.data; + }, + /*====================================================================== * Settings */ From 87b6bb6d85c0758843e95be2575da6270105fb42 Mon Sep 17 00:00:00 2001 From: vexorian Date: Sun, 21 Feb 2021 00:33:25 -0400 Subject: [PATCH 22/57] #144 Notification toast when updating settings (and other things) --- index.js | 47 ++- src/api.js | 297 +++++++++++++++++- src/services/event-service.js | 47 +++ src/tv-guide-service.js | 29 +- web/app.js | 1 + web/directives/toast-notifications.js | 116 +++++++ web/public/index.html | 1 + web/public/style.css | 32 ++ web/public/templates/ffmpeg-settings.html | 2 +- web/public/templates/hdhr-settings.html | 2 +- web/public/templates/plex-settings.html | 2 +- web/public/templates/toast-notifications.html | 11 + web/public/templates/xmltv-settings.html | 2 +- 13 files changed, 579 insertions(+), 10 deletions(-) create mode 100644 src/services/event-service.js create mode 100644 web/directives/toast-notifications.js create mode 100644 web/public/templates/toast-notifications.html diff --git a/index.js b/index.js index 442f8ec..6eaf637 100644 --- a/index.js +++ b/index.js @@ -21,6 +21,7 @@ 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 EventService = require("./src/services/event-service"); const onShutdown = require("node-graceful-shutdown").onShutdown; console.log( @@ -76,6 +77,7 @@ db.connect(process.env.DATABASE, ['channels', 'plex-servers', 'ffmpeg-settings', fileCache = new FileCacheService( path.join(process.env.DATABASE, 'cache') ); cacheImageService = new CacheImageService(db, fileCache); m3uService = new M3uService(channelDB, fileCache, channelCache) +eventService = new EventService(); initDB(db, channelDB) @@ -165,6 +167,8 @@ xmltvInterval.startInterval() let hdhr = HDHR(db, channelDB) let app = express() +eventService.setup(app); + app.use(fileUpload({ createParentPath: true })); @@ -197,7 +201,7 @@ app.use('/favicon.svg', express.static( ) ); // API Routers -app.use(api.router(db, channelDB, fillerDB, xmltvInterval, guideService, m3uService )) +app.use(api.router(db, channelDB, fillerDB, xmltvInterval, guideService, m3uService, eventService )) app.use('/api/cache/images', cacheImageService.apiRouters()) app.use(video.router( channelDB, fillerDB, db)) @@ -242,8 +246,49 @@ function initDB(db, channelDB) { } + +function _wait(t) { + return new Promise((resolve) => { + setTimeout(resolve, t); + }); +} + + +async function sendEventAfterTime() { + let t = (new Date()).getTime(); + await _wait(20000); + eventService.push( + "lifecycle", + { + "message": `Server Started`, + "detail" : { + "time": t, + }, + "level" : "success" + } + ); + +} +sendEventAfterTime(); + + + + onShutdown("log" , [], async() => { + let t = (new Date()).getTime(); + eventService.push( + "lifecycle", + { + "message": `Initiated Server Shutdown`, + "detail" : { + "time": t, + }, + "level" : "warning" + } + ); + console.log("Received exit signal, attempting graceful shutdonw..."); + await _wait(2000); }); onShutdown("xmltv-writer" , [], async() => { await xmltv.shutdown(); diff --git a/src/api.js b/src/api.js index 9c41ef6..1e417b8 100644 --- a/src/api.js +++ b/src/api.js @@ -12,9 +12,20 @@ const Plex = require("./plex.js"); const timeSlotsService = require('./services/time-slots-service'); const randomSlotsService = require('./services/random-slots-service'); +function safeString(object) { + let o = object; + for(let i = 1; i < arguments.length; i++) { + o = o[arguments[i]]; + if (typeof(o) === 'undefined') { + return "missing"; + } + } + return String(o); +} + module.exports = { router: api } -function api(db, channelDB, fillerDB, xmltvInterval, guideService, _m3uService ) { - const m3uService = _m3uService; +function api(db, channelDB, fillerDB, xmltvInterval, guideService, _m3uService, eventService ) { + let m3uService = _m3uService; const router = express.Router() const plexServerDB = new PlexServerDB(channelDB, channelCache, db); @@ -89,34 +100,113 @@ function api(db, channelDB, fillerDB, xmltvInterval, guideService, _m3uService } }) router.delete('/api/plex-servers', async (req, res) => { + let name = "unknown"; try { - let name = req.body.name; + name = req.body.name; if (typeof(name) === 'undefined') { return res.status(400).send("Missing name"); } let report = await plexServerDB.deleteServer(name); res.send(report) + eventService.push( + "settings-update", + { + "message": `Plex server ${name} removed.`, + "module" : "plex-server", + "detail" : { + "serverName" : name, + "action" : "delete" + }, + "level" : "warn" + } + ); + } catch(err) { console.error(err); res.status(500).send("error"); + eventService.push( + "settings-update", + { + "message": "Error deleting plex server.", + "module" : "plex-server", + "detail" : { + "action": "delete", + "serverName" : name, + "error" : safeString(err, "message"), + }, + "level" : "danger" + } + ); } }) router.post('/api/plex-servers', async (req, res) => { try { await plexServerDB.updateServer(req.body); res.status(204).send("Plex server updated.");; + eventService.push( + "settings-update", + { + "message": `Plex server ${req.body.name} updated.`, + "module" : "plex-server", + "detail" : { + "serverName" : req.body.name, + "action" : "update" + }, + "level" : "info" + } + ); + } catch (err) { - console.error("Could not add plex server.", err); + console.error("Could not update plex server.", err); res.status(400).send("Could not add plex server."); + eventService.push( + "settings-update", + { + "message": "Error updating plex server.", + "module" : "plex-server", + "detail" : { + "action": "update", + "serverName" : safeString(req, "body", "name"), + "error" : safeString(err, "message"), + }, + "level" : "danger" + } + ); } }) router.put('/api/plex-servers', async (req, res) => { try { await plexServerDB.addServer(req.body); res.status(201).send("Plex server added.");; + eventService.push( + "settings-update", + { + "message": `Plex server ${req.body.name} added.`, + "module" : "plex-server", + "detail" : { + "serverName" : req.body.name, + "action" : "add" + }, + "level" : "info" + } + ); + } catch (err) { console.error("Could not add plex server.", err); res.status(400).send("Could not add plex server."); + eventService.push( + "settings-update", + { + "message": "Error adding plex server.", + "module" : "plex-server", + "detail" : { + "action": "add", + "serverName" : safeString(req, "body", "name"), + "error" : safeString(err, "message"), + }, + "level" : "danger" + } + ); } }) @@ -341,10 +431,34 @@ function api(db, channelDB, fillerDB, xmltvInterval, guideService, _m3uService if (typeof(err) !== 'undefined') { return res.status(400).send(err); } + eventService.push( + "settings-update", + { + "message": "FFMPEG configuration updated.", + "module" : "ffmpeg", + "detail" : { + "action" : "update" + }, + "level" : "info" + } + ); res.send(ffmpeg) } catch(err) { console.error(err); res.status(500).send("error"); + eventService.push( + "settings-update", + { + "message": "Error updating FFMPEG configuration.", + "module" : "ffmpeg", + "detail" : { + "action": "update", + "error" : safeString(err, "message"), + }, + "level" : "danger" + } + ); + } }) router.post('/api/ffmpeg-settings', (req, res) => { // RESET @@ -353,10 +467,34 @@ function api(db, channelDB, fillerDB, xmltvInterval, guideService, _m3uService ffmpeg.ffmpegPath = req.body.ffmpegPath; db['ffmpeg-settings'].update({ _id: req.body._id }, ffmpeg) ffmpeg = db['ffmpeg-settings'].find()[0] + eventService.push( + "settings-update", + { + "message": "FFMPEG configuration reset.", + "module" : "ffmpeg", + "detail" : { + "action" : "reset" + }, + "level" : "warning" + } + ); res.send(ffmpeg) } catch(err) { console.error(err); res.status(500).send("error"); + eventService.push( + "settings-update", + { + "message": "Error reseting FFMPEG configuration.", + "module" : "ffmpeg", + "detail" : { + "action": "reset", + "error" : safeString(err, "message"), + }, + "level" : "danger" + } + ); + } }) @@ -384,9 +522,34 @@ function api(db, channelDB, fillerDB, xmltvInterval, guideService, _m3uService db['plex-settings'].update({ _id: req.body._id }, req.body) let plex = db['plex-settings'].find()[0] res.send(plex) + eventService.push( + "settings-update", + { + "message": "Plex configuration updated.", + "module" : "plex", + "detail" : { + "action" : "update" + }, + "level" : "info" + } + ); + } catch(err) { console.error(err); res.status(500).send("error"); + eventService.push( + "settings-update", + { + "message": "Error updating Plex configuration", + "module" : "plex", + "detail" : { + "action": "update", + "error" : safeString(err, "message"), + }, + "level" : "danger" + } + ); + } }) @@ -415,9 +578,35 @@ function api(db, channelDB, fillerDB, xmltvInterval, guideService, _m3uService }) let plex = db['plex-settings'].find()[0] res.send(plex) + eventService.push( + "settings-update", + { + "message": "Plex configuration reset.", + "module" : "plex", + "detail" : { + "action" : "reset" + }, + "level" : "warning" + } + ); } catch(err) { console.error(err); res.status(500).send("error"); + + eventService.push( + "settings-update", + { + "message": "Error reseting Plex configuration", + "module" : "plex", + "detail" : { + "action": "reset", + "error" : safeString(err, "message"), + }, + "level" : "danger" + } + ); + + } }) @@ -458,10 +647,35 @@ function api(db, channelDB, fillerDB, xmltvInterval, guideService, _m3uService ); xmltv = db['xmltv-settings'].find()[0] res.send(xmltv) + eventService.push( + "settings-update", + { + "message": "xmltv settings updated.", + "module" : "xmltv", + "detail" : { + "action" : "update" + }, + "level" : "info" + } + ); updateXmltv() } catch(err) { console.error(err); res.status(500).send("error"); + + eventService.push( + "settings-update", + { + "message": "Error updating xmltv configuration", + "module" : "xmltv", + "detail" : { + "action": "update", + "error" : safeString(err, "message"), + }, + "level" : "danger" + } + ); + } }) @@ -475,10 +689,35 @@ function api(db, channelDB, fillerDB, xmltvInterval, guideService, _m3uService }) var xmltv = db['xmltv-settings'].find()[0] res.send(xmltv) + eventService.push( + "settings-update", + { + "message": "xmltv settings reset.", + "module" : "xmltv", + "detail" : { + "action" : "reset" + }, + "level" : "warning" + } + ); + updateXmltv() } catch(err) { console.error(err); res.status(500).send("error"); + eventService.push( + "settings-update", + { + "message": "Error reseting xmltv configuration", + "module" : "xmltv", + "detail" : { + "action": "reset", + "error" : safeString(err, "message"), + }, + "level" : "danger" + } + ); + } }) @@ -537,9 +776,34 @@ function api(db, channelDB, fillerDB, xmltvInterval, guideService, _m3uService db['hdhr-settings'].update({ _id: req.body._id }, req.body) let hdhr = db['hdhr-settings'].find()[0] res.send(hdhr) + eventService.push( + "settings-update", + { + "message": "HDHR configuration updated.", + "module" : "hdhr", + "detail" : { + "action" : "update" + }, + "level" : "info" + } + ); + } catch(err) { console.error(err); res.status(500).send("error"); + eventService.push( + "settings-update", + { + "message": "Error updating HDHR configuration", + "module" : "hdhr", + "detail" : { + "action": "action", + "error" : safeString(err, "message"), + }, + "level" : "danger" + } + ); + } }) @@ -552,9 +816,34 @@ function api(db, channelDB, fillerDB, xmltvInterval, guideService, _m3uService }) var hdhr = db['hdhr-settings'].find()[0] res.send(hdhr) + eventService.push( + "settings-update", + { + "message": "HDHR configuration reset.", + "module" : "hdhr", + "detail" : { + "action" : "reset" + }, + "level" : "warning" + } + ); + } catch(err) { console.error(err); res.status(500).send("error"); + eventService.push( + "settings-update", + { + "message": "Error reseting HDHR configuration", + "module" : "hdhr", + "detail" : { + "action": "reset", + "error" : safeString(err, "message"), + }, + "level" : "danger" + } + ); + } }) diff --git a/src/services/event-service.js b/src/services/event-service.js new file mode 100644 index 0000000..1ac9893 --- /dev/null +++ b/src/services/event-service.js @@ -0,0 +1,47 @@ +const EventEmitter = require("events"); + +class EventsService { + constructor() { + this.stream = new EventEmitter(); + let that = this; + let fun = () => { + that.push( "heartbeat", "{}"); + setTimeout(fun, 5000) + }; + fun(); + + } + + setup(app) { + app.get("/api/events", (request, response) => { + console.log("Open event channel."); + response.writeHead(200, { + "Content-Type" : "text/event-stream", + "Cache-Control" : "no-cache", + "connection" : "keep-alive", + } ); + let listener = (event,data) => { + //console.log( String(event) + " " + JSON.stringify(data) ); + response.write("event: " + String(event) + "\ndata: " + + JSON.stringify(data) + "\nretry: 5000\n\n" ); + }; + + this.stream.on("push", listener ); + response.on( "close", () => { + console.log("Remove event channel."); + this.stream.removeListener("push", listener); + } ); + } ); + } + + push(event, data) { + if (typeof(data.message) !== 'undefined') { + console.log("Push event: " + data.message ); + } + this.stream.emit("push", event, data ); + } + + +} + +module.exports = EventsService; \ No newline at end of file diff --git a/src/tv-guide-service.js b/src/tv-guide-service.js index 5936dcd..d64da30 100644 --- a/src/tv-guide-service.js +++ b/src/tv-guide-service.js @@ -7,7 +7,7 @@ class TVGuideService /**** * **/ - constructor(xmltv, db, cacheImageService) { + constructor(xmltv, db, cacheImageService, eventService) { this.cached = null; this.lastUpdate = 0; this.updateTime = 0; @@ -19,6 +19,7 @@ class TVGuideService this.xmltv = xmltv; this.db = db; this.cacheImageService = cacheImageService; + this.eventService = eventService; } async get() { @@ -44,6 +45,19 @@ class TVGuideService this.currentUpdate = this.updateTime; this.currentLimit = this.updateLimit; this.currentChannels = this.updateChannels; + let t = "" + ( (new Date()) ); + eventService.push( + "xmltv", + { + "message": `Started building tv-guide at = ${t}`, + "module" : "xmltv", + "detail" : { + "time": new Date(), + }, + "level" : "info" + } + ); + await this.buildIt(); } await _wait(100); @@ -353,6 +367,19 @@ class TVGuideService async refreshXML() { let xmltvSettings = this.db['xmltv-settings'].find()[0]; await this.xmltv.WriteXMLTV(this.cached, xmltvSettings, async() => await this._throttle(), this.cacheImageService); + let t = "" + ( (new Date()) ); + eventService.push( + "xmltv", + { + "message": `XMLTV updated at server time = ${t}`, + "module" : "xmltv", + "detail" : { + "time": new Date(), + }, + "level" : "info" + } + ); + } async getStatus() { diff --git a/web/app.js b/web/app.js index b1d9887..1b15e15 100644 --- a/web/app.js +++ b/web/app.js @@ -19,6 +19,7 @@ app.directive('plexLibrary', require('./directives/plex-library')) app.directive('programConfig', require('./directives/program-config')) app.directive('flexConfig', require('./directives/flex-config')) app.directive('timeSlotsTimeEditor', require('./directives/time-slots-time-editor')) +app.directive('toastNotifications', require('./directives/toast-notifications')) app.directive('fillerConfig', require('./directives/filler-config')) app.directive('deleteFiller', require('./directives/delete-filler')) app.directive('frequencyTweak', require('./directives/frequency-tweak')) diff --git a/web/directives/toast-notifications.js b/web/directives/toast-notifications.js new file mode 100644 index 0000000..bb6f645 --- /dev/null +++ b/web/directives/toast-notifications.js @@ -0,0 +1,116 @@ +module.exports = function ($timeout) { + return { + restrict: 'E', + templateUrl: 'templates/toast-notifications.html', + replace: true, + scope: { + }, + link: function (scope, element, attrs) { + + const FADE_IN_START = 100; + const FADE_IN_END = 1000; + const FADE_OUT_START = 10000; + const TOTAL_DURATION = 11000; + + + scope.toasts = []; + + let eventSource = null; + + let timerHandle = null; + let refreshHandle = null; + + + let setResetTimer = () => { + if (timerHandle != null) { + clearTimeout( timerHandle ); + } + timerHandle = setTimeout( () => { + scope.setup(); + } , 10000); + }; + + let updateAfter = (wait) => { + if (refreshHandle != null) { + $timeout.cancel( refreshHandle ); + } + refreshHandle = $timeout( ()=> updater(), wait ); + }; + + let updater = () => { + let wait = 10000; + let updatedToasts = []; + try { + let t = (new Date()).getTime(); + for (let i = 0; i < scope.toasts.length; i++) { + let toast = scope.toasts[i]; + let diff = t - toast.time; + if (diff < TOTAL_DURATION) { + if (diff < FADE_IN_START) { + toast.clazz = { "about-to-fade-in" : true } + wait = Math.min( wait, FADE_IN_START - diff ); + } else if (diff < FADE_IN_END) { + toast.clazz = { "fade-in" : true } + wait = Math.min( wait, FADE_IN_END - diff ); + } else if (diff < FADE_OUT_START) { + toast.clazz = {} + wait = Math.min( wait, FADE_OUT_START - diff ); + } else { + toast.clazz = { "fade-out" : true } + wait = Math.min( wait, TOTAL_DURATION - diff ); + } + toast.clazz[toast.deco] = true; + updatedToasts.push(toast); + } + } + } catch (err) { + console.error("error", err); + } + scope.toasts = updatedToasts; + updateAfter(wait); + }; + + let addToast = (toast) => { + toast.time = (new Date()).getTime(); + toast.clazz= { "about-to-fade-in": true }; + toast.clazz[toast.deco] = true; + scope.toasts.push(toast); + $timeout( () => updateAfter(0) ); + }; + + let getDeco = (data) => { + return "bg-" + data.level; + } + + scope.setup = () => { + if (eventSource != null) { + eventSource.close(); + eventSource = null; + } + setResetTimer(); + + eventSource = new EventSource("api/events"); + + eventSource.addEventListener("heartbeat", () => { + setResetTimer(); + } ); + + let normalEvent = (title) => { + return (event) => { + let data = JSON.parse(event.data); + addToast ( { + title : title, + text : data.message, + deco: getDeco(data) + } ) + }; + }; + + eventSource.addEventListener('settings-update', normalEvent("Settings Update") ); + eventSource.addEventListener('xmltv', normalEvent("TV Guide") ); + eventSource.addEventListener('lifecycle', normalEvent("Server") ); + }; + scope.setup(); + } + }; +} diff --git a/web/public/index.html b/web/public/index.html index 3b20003..360b80c 100644 --- a/web/public/index.html +++ b/web/public/index.html @@ -43,6 +43,7 @@
+ diff --git a/web/public/style.css b/web/public/style.css index 0102e80..92643a7 100644 --- a/web/public/style.css +++ b/web/public/style.css @@ -357,6 +357,38 @@ div.programming-programs div.list-group-item { background : rgba(255,255,255, 0.1); } +.dizque-toast { + margin-top: 0.2rem; + padding: 0.5rem; + background: #FFFFFF; + border: 1px solid rgba(0,0,0,.1); + border-radius: .25rem; + color: #FFFFFF; +} + +.dizque-toast.bg-warning { + color: black +} + +.about-to-fade-in { + opacity: 0.00; + transition: opacity 1.00s ease-in-out; + -moz-transition: opacity 1.00s ease-in-out; + -webkit-transition: opacity 1.00s ease-in-out; +} +.fade-in { + opacity: 0.95; + transition: opacity 1.00s ease-in-out; + -moz-transition: opacity 1.00s ease-in-out; + -webkit-transition: opacity 1.00s ease-in-out; +} + +.fade-out { + transition: opacity 1.00s ease-in-out; + -moz-transition: opacity 1.00s ease-in-out; + -webkit-transition: opacity 1.00s ease-in-out; + opacity: 0.0; +} #dizquetv-logo { width: 1em; diff --git a/web/public/templates/ffmpeg-settings.html b/web/public/templates/ffmpeg-settings.html index 09fb68f..8ebd3ff 100644 --- a/web/public/templates/ffmpeg-settings.html +++ b/web/public/templates/ffmpeg-settings.html @@ -4,7 +4,7 @@ - diff --git a/web/public/templates/hdhr-settings.html b/web/public/templates/hdhr-settings.html index 0b6cd34..c00b6d3 100644 --- a/web/public/templates/hdhr-settings.html +++ b/web/public/templates/hdhr-settings.html @@ -3,7 +3,7 @@ - diff --git a/web/public/templates/plex-settings.html b/web/public/templates/plex-settings.html index 1324bf1..b5dfb39 100644 --- a/web/public/templates/plex-settings.html +++ b/web/public/templates/plex-settings.html @@ -70,7 +70,7 @@ - diff --git a/web/public/templates/toast-notifications.html b/web/public/templates/toast-notifications.html new file mode 100644 index 0000000..825a520 --- /dev/null +++ b/web/public/templates/toast-notifications.html @@ -0,0 +1,11 @@ +
+ +
+ {{ toast.title }} +
{{ toast.text }}
+
+
diff --git a/web/public/templates/xmltv-settings.html b/web/public/templates/xmltv-settings.html index 20c65a0..cfd3b97 100644 --- a/web/public/templates/xmltv-settings.html +++ b/web/public/templates/xmltv-settings.html @@ -4,7 +4,7 @@ - From c836528e50396641af148675a14f480ba51363cc Mon Sep 17 00:00:00 2001 From: vexorian Date: Sun, 21 Feb 2021 00:56:24 -0400 Subject: [PATCH 23/57] 1.3.0 --- README.md | 2 +- src/constants.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 94c2eb9..a37f50e 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# dizqueTV 1.3.0-prerelease +# dizqueTV 1.3.0 ![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. diff --git a/src/constants.js b/src/constants.js index bf45467..43f5023 100644 --- a/src/constants.js +++ b/src/constants.js @@ -5,5 +5,5 @@ module.exports = { TVGUIDE_MAXIMUM_FLEX_DURATION : 6 * 60 * 60 * 1000, TOO_FREQUENT: 100, - VERSION_NAME: "1.3.0-prerelease" + VERSION_NAME: "1.3.0" } From 415add6a06f2bf84bdd03bef7dd3b96a20eb175f Mon Sep 17 00:00:00 2001 From: vexorian Date: Sun, 28 Feb 2021 23:09:32 -0400 Subject: [PATCH 24/57] Prepare 1.3.1 development --- README.md | 2 +- src/constants.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 94c2eb9..c561e27 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# dizqueTV 1.3.0-prerelease +# dizqueTV 1.3.1-prerelease ![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. diff --git a/src/constants.js b/src/constants.js index bf45467..b223d2b 100644 --- a/src/constants.js +++ b/src/constants.js @@ -5,5 +5,5 @@ module.exports = { TVGUIDE_MAXIMUM_FLEX_DURATION : 6 * 60 * 60 * 1000, TOO_FREQUENT: 100, - VERSION_NAME: "1.3.0-prerelease" + VERSION_NAME: "1.3.1-prerelease" } From b9365115aa7c89b383586765e92c88c0b91b80ce Mon Sep 17 00:00:00 2001 From: vexorian Date: Sun, 28 Feb 2021 23:15:13 -0400 Subject: [PATCH 25/57] Remote attachment content-disposition from xmltv tv. Using URL rather than file is recommended for xmltv setup, and the attachment disposition encourages using the file. --- src/api.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/api.js b/src/api.js index 1e417b8..ea2d723 100644 --- a/src/api.js +++ b/src/api.js @@ -856,7 +856,6 @@ function api(db, channelDB, fillerDB, xmltvInterval, guideService, _m3uService, res.set('Cache-Control', 'no-store') res.type('application/xml'); - res.attachment('xmltv.xml'); let xmltvSettings = db['xmltv-settings'].find()[0]; From 80b1fb8ce77c631d9d28ae10807b7a68efc57ce2 Mon Sep 17 00:00:00 2001 From: timebomb0 Date: Sat, 6 Mar 2021 17:22:16 -0800 Subject: [PATCH 26/57] Improve logging around getting plex status --- src/plexTranscoder.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/plexTranscoder.js b/src/plexTranscoder.js index 21dd1ed..22b0017 100644 --- a/src/plexTranscoder.js +++ b/src/plexTranscoder.js @@ -416,8 +416,15 @@ X-Plex-Token=${this.server.accessToken}`; } updatePlex() { - this.log("Updating plex status") - axios.post(this.getStatusUrl()); + this.log("Updating plex status"); + const statusUrl = this.getStatusUrl(); + try { + axios.post(statusUrl); + } catch (error) { + this.log(`Problem updating Plex status using status URL ${statusUrl}:`); + this.log(error); + return false; + } this.currTimeMs += this.updateInterval; if (this.currTimeMs > this.duration) { this.currTimeMs = this.duration; From 8889d5a456fa67cf3d3b2db2d527848147b4b06f Mon Sep 17 00:00:00 2001 From: vexorian Date: Mon, 15 Mar 2021 00:03:55 -0400 Subject: [PATCH 27/57] #263 Remove duplicates in programming before sending to random slots endpoint --- web/directives/channel-config.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/web/directives/channel-config.js b/web/directives/channel-config.js index 9f3118f..2dc63a4 100644 --- a/web/directives/channel-config.js +++ b/web/directives/channel-config.js @@ -1886,7 +1886,8 @@ module.exports = function ($timeout, $location, dizquetv, resolutionOptions) { scope.timeSlots.startDialog( progs, scope.maxSize, scope.channel.scheduleBackup ); } scope.onRandomSlotsButtonClick = () => { - scope.randomSlots.startDialog(scope.channel.programs, scope.maxSize, scope.channel.randomScheduleBackup ); + let progs = removeDuplicatesSub( scope.channel.programs ); + scope.randomSlots.startDialog(progs, scope.maxSize, scope.channel.randomScheduleBackup ); } scope.logoOnChange = (event) => { From de3f859deaa542f1f79ea065e1a2ea55ef3a3ea9 Mon Sep 17 00:00:00 2001 From: vexorian Date: Mon, 15 Mar 2021 00:18:23 -0400 Subject: [PATCH 28/57] Add flex time so that the next program in time slots happens AFTER the current time. This is specially good for weekly slots, because programming won't start at Thursday for no reason anymore. --- src/services/time-slots-service.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/services/time-slots-service.js b/src/services/time-slots-service.js index dfb53a9..849470e 100644 --- a/src/services/time-slots-service.js +++ b/src/services/time-slots-service.js @@ -325,6 +325,9 @@ module.exports = async( programs, schedule ) => { } } + if (ts > t0) { + pushFlex( ts - t0 ); + } while ( (t < hardLimit) && (p.length < LIMIT) ) { await throttle(); //ensure t is padded From 06e6232ce8c12b09413abeeeb51209800aeadfa3 Mon Sep 17 00:00:00 2001 From: vexorian Date: Mon, 15 Mar 2021 00:41:18 -0400 Subject: [PATCH 29/57] 1.3.1 --- README.md | 2 +- src/constants.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a37f50e..f82f3ba 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# dizqueTV 1.3.0 +# dizqueTV 1.3.1 ![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. diff --git a/src/constants.js b/src/constants.js index 43f5023..6ccdd47 100644 --- a/src/constants.js +++ b/src/constants.js @@ -5,5 +5,5 @@ module.exports = { TVGUIDE_MAXIMUM_FLEX_DURATION : 6 * 60 * 60 * 1000, TOO_FREQUENT: 100, - VERSION_NAME: "1.3.0" + VERSION_NAME: "1.3.1" } From eca8d44af0930e34cc33aee68c12c5e136e24e83 Mon Sep 17 00:00:00 2001 From: vexorian Date: Sun, 21 Mar 2021 18:10:53 -0400 Subject: [PATCH 30/57] Prepare 1.3.2 development --- README.md | 2 +- src/constants.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c561e27..ff86293 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# dizqueTV 1.3.1-prerelease +# dizqueTV 1.3.2-prerelease ![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. diff --git a/src/constants.js b/src/constants.js index b223d2b..15e9c2e 100644 --- a/src/constants.js +++ b/src/constants.js @@ -5,5 +5,5 @@ module.exports = { TVGUIDE_MAXIMUM_FLEX_DURATION : 6 * 60 * 60 * 1000, TOO_FREQUENT: 100, - VERSION_NAME: "1.3.1-prerelease" + VERSION_NAME: "1.3.2-prerelease" } From 542fa93b5b8903ad62afa587a0c668dee4ee3dfc Mon Sep 17 00:00:00 2001 From: vexorian Date: Sun, 21 Mar 2021 18:11:18 -0400 Subject: [PATCH 31/57] Make ffmpeg 4.3 the default in docker --- Dockerfile | 2 +- Dockerfile-nvidia | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 651f823..23435cc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,7 +6,7 @@ COPY --from=vexorian/dizquetv:nexecache /var/nexe/linux-x64-12.16.2 /var/nexe/ COPY . . RUN npm run build && LINUXBUILD=dizquetv sh make_dist.sh linuxonly -FROM jrottenberg/ffmpeg:4.2-ubuntu1804 +FROM jrottenberg/ffmpeg:4.3-ubuntu1804 EXPOSE 8000 WORKDIR /home/node/app ENTRYPOINT [ "./dizquetv" ] diff --git a/Dockerfile-nvidia b/Dockerfile-nvidia index 608d7f5..f21f8b1 100644 --- a/Dockerfile-nvidia +++ b/Dockerfile-nvidia @@ -6,7 +6,7 @@ COPY --from=vexorian/dizquetv:nexecache /var/nexe/linux-x64-12.16.2 /var/nexe/ COPY . . RUN npm run build && LINUXBUILD=dizquetv sh make_dist.sh linuxonly -FROM jrottenberg/ffmpeg:4.2-nvidia +FROM jrottenberg/ffmpeg:4.3-nvidia EXPOSE 8000 WORKDIR /home/node/app ENTRYPOINT [ "./dizquetv" ] From fbbcf95bdd090a4d423087c938fde62563c858b5 Mon Sep 17 00:00:00 2001 From: vexorian Date: Sun, 21 Mar 2021 18:12:19 -0400 Subject: [PATCH 32/57] Customizable style.css --- index.js | 5 ++++ resources/default-custom.css | 14 ++++++++++ web/public/index.html | 1 + web/public/style.css | 52 +++++++++++++++--------------------- 4 files changed, 42 insertions(+), 30 deletions(-) create mode 100644 resources/default-custom.css diff --git a/index.js b/index.js index 6eaf637..c592560 100644 --- a/index.js +++ b/index.js @@ -199,6 +199,7 @@ app.use('/cache/images', express.static(path.join(process.env.DATABASE, 'cache/i app.use('/favicon.svg', express.static( path.join(__dirname, 'resources/favicon.svg') ) ); +app.use('/custom.css', express.static(path.join(process.env.DATABASE, 'custom.css'))) // API Routers app.use(api.router(db, channelDB, fillerDB, xmltvInterval, guideService, m3uService, eventService )) @@ -243,6 +244,10 @@ function initDB(db, channelDB) { let data = fs.readFileSync(path.resolve(path.join(__dirname, 'resources/loading-screen.png'))) fs.writeFileSync(process.env.DATABASE + '/images/loading-screen.png', data) } + if (!fs.existsSync( path.join(process.env.DATABASE, 'custom.css') )) { + let data = fs.readFileSync(path.resolve(path.join(__dirname, 'resources', 'default-custom.css'))) + fs.writeFileSync( path.join(process.env.DATABASE, 'custom.css'), data) + } } diff --git a/resources/default-custom.css b/resources/default-custom.css new file mode 100644 index 0000000..2293389 --- /dev/null +++ b/resources/default-custom.css @@ -0,0 +1,14 @@ +/** For example : */ + + + +:root { + --guide-text : #F0F0f0; + --guide-header-even: #423cd4ff; + --guide-header-odd: #262198ff; + --guide-color-a: #212121; + --guide-color-b: #515151; + --guide-color-c: #313131; + --guide-color-d: #414141; +} + diff --git a/web/public/index.html b/web/public/index.html index b39f56d..0e1d92e 100644 --- a/web/public/index.html +++ b/web/public/index.html @@ -7,6 +7,7 @@ + diff --git a/web/public/style.css b/web/public/style.css index 92643a7..8055fb6 100644 --- a/web/public/style.css +++ b/web/public/style.css @@ -1,3 +1,14 @@ +:root { + --guide-text : #F0F0f0; + --guide-header-even: #423cd4ff; + --guide-header-odd: #262198ff; + --guide-color-a: #212121; + --guide-color-b: #515151; + --guide-color-c: #313131; + --guide-color-d: #414141; +} + + .pull-right { float: right; } .modal-semi-body { @@ -5,14 +16,6 @@ flex: 1 1 auto; } -.commercials-panel { - background-color: rgb(70, 70, 70); - border-top: 1px solid #daa104; - border-left-color: #daa104; - border-right-color: #daa104; - color: white -} - .plex-panel { margin: 0; padding: 0; @@ -27,25 +30,15 @@ padding-right: 0.2em } -.list-group-item-video { - background-color: rgb(70, 70, 70); - border-top: 1px solid #daa104; - border-left-color: #daa104; - border-right-color: #daa104; - color: white -} -.list-group-item-video .fa-plus-circle { + +.fa-plus-circle { color: #daa104; } -.list-group-item-video:hover .fa-plus-circle { +.fa-plus-circle { color: #000; } -.list-group-item-video:hover { - background-color: #daa104; - color: #000 !important; -} .list-group.list-group-root .list-group-item { border-radius: 0; border-width: 1px 0 0 0; @@ -157,8 +150,7 @@ table.tvguide { position: sticky; top: 0; bottom: 0; - background: white; - border-bottom: 1px solid black; + /*border-bottom: 1px solid black;*/ } .tvguide th.guidenav { @@ -168,7 +160,7 @@ table.tvguide { .tvguide td, .tvguide th { - color: #F0F0f0; + color: var(--guide-text); border-top: 0; height: 3.5em; padding-top: 0; @@ -208,27 +200,27 @@ table.tvguide { .tvguide th.even { - background: #423cd4ff; + background: var(--guide-header-even); } .tvguide th.odd { - background: #262198ff; + background: var(--guide-header-odd); } .tvguide tr.odd td.even { - background: #212121; + background: var(--guide-color-a); } .tvguide tr.odd td.odd { - background: #515151;; + background: var(--guide-color-b); } .tvguide tr.even td.odd { - background: #313131 + background: var(--guide-color-c); } .tvguide tr.even td.even { - background: #414141; + background: var(--guide-color-d) ; } .tvguide td .play-channel { From 7a8031adc56027be820aab17535881516b02bb6c Mon Sep 17 00:00:00 2001 From: vexorian Date: Sun, 21 Mar 2021 18:43:40 -0400 Subject: [PATCH 33/57] Only guide is wide --- web/public/views/channels.html | 2 +- web/public/views/filler.html | 2 +- web/public/views/guide.html | 2 +- web/public/views/settings.html | 2 +- web/public/views/version.html | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/web/public/views/channels.html b/web/public/views/channels.html index dddb0ac..e566eb0 100644 --- a/web/public/views/channels.html +++ b/web/public/views/channels.html @@ -1,4 +1,4 @@ -
+
diff --git a/web/public/views/filler.html b/web/public/views/filler.html index 38947b2..f4546ef 100644 --- a/web/public/views/filler.html +++ b/web/public/views/filler.html @@ -1,4 +1,4 @@ -
+
diff --git a/web/public/views/guide.html b/web/public/views/guide.html index 9e8bf95..7621b7c 100644 --- a/web/public/views/guide.html +++ b/web/public/views/guide.html @@ -1,4 +1,4 @@ -
+
{{title}} diff --git a/web/public/views/settings.html b/web/public/views/settings.html index 5ef4ff3..e7bcd65 100644 --- a/web/public/views/settings.html +++ b/web/public/views/settings.html @@ -1,4 +1,4 @@ -
+
- Guide - Channels - Filler - Settings - Version + Guide - Channels - Library - Settings - Version XMLTV diff --git a/web/public/style.css b/web/public/style.css index 8055fb6..0ad24b1 100644 --- a/web/public/style.css +++ b/web/public/style.css @@ -246,17 +246,21 @@ table.tvguide { text-align: right; } -.filler-list .list-group-item, .program-row { +.filler-list .list-group-item, .program-row, .show-list .list-group-item, .program-row { min-height: 1.5em; } -.filler-list .list-group-item .title, .program-row .title { +.filler-list .list-group-item .title, .program-row .title, .show-list .list-group-item .title, .program-row .title { margin-right: 5px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +.show-row .program-start { + width: 2em; +} + div.channel-tools { max-height: 20em; overflow-y: scroll; @@ -307,7 +311,7 @@ div.programming-programs div.list-group-item { } -.program-row:nth-child(odd), .filler-row:nth-child(odd), .channel-row:nth-child(odd) { +.program-row:nth-child(odd), .show-row:nth-child(odd), .filler-row:nth-child(odd), .channel-row:nth-child(odd) { background-color: #eeeeee; } diff --git a/web/public/templates/channel-config.html b/web/public/templates/channel-config.html index dd7f5d0..3c8b57a 100644 --- a/web/public/templates/channel-config.html +++ b/web/public/templates/channel-config.html @@ -181,7 +181,7 @@
- {{ x.type === 'episode' ? x.showTitle + ' - S' + x.season.toString().padStart(2, '0') + 'E' + x.episode.toString().padStart(2, '0') : x.title}} + {{ getProgramDisplayTitle(x) }}
Flex @@ -856,7 +856,7 @@ - + diff --git a/web/public/templates/plex-library.html b/web/public/templates/plex-library.html index ee23347..01ed94f 100644 --- a/web/public/templates/plex-library.html +++ b/web/public/templates/plex-library.html @@ -3,7 +3,7 @@
- Guide - Channels - Library - Settings - Version + Guide - Channels - Library - Player - Settings - Version XMLTV diff --git a/web/public/views/player.html b/web/public/views/player.html new file mode 100644 index 0000000..d6bce09 --- /dev/null +++ b/web/public/views/player.html @@ -0,0 +1,85 @@ +
+ +
Player
+

Play your channels in a local media player. This is mostly meant for testing purposes and to show what endpoints are available.

+ + +
+ + +
+
+
+
+ + + +
+
+ +
+ + +
+ + + + The /video endpoint is the one used by IPTV player or Plex to play the channel's content. It creates a single mpegts stream for the channel out of all of the videos scheduled for it. For this reason, it needs the videos to be formatted to the same codec and resolution (normalized). Use this endpoint to debug issues with Plex/IPTV players or when the other endpoints don't work correctly in your player. + + + The /m3u8 endpoint (misnomer) sends the channel as a playlist of videos, which allows some players to play the channel in sequence without the need for a single stream. Since there is no need for a single stream, it requires less normalization work . + + + The /radio endpoint plays only the audio of the channel, effectively turning it into a radio station. If you only need the audio, this endpoint is much more efficient as it will not need to extract or transcode video at all. + + + +
+
+ +
+ + +
+ + +
+
+ + +
+ +
+ + +
\ No newline at end of file From 21371febd2aecc6e9a7e3e6916c7d1bba16873a5 Mon Sep 17 00:00:00 2001 From: vexorian Date: Tue, 23 Mar 2021 19:46:38 -0400 Subject: [PATCH 37/57] Custom select control in plex library, looks nicer --- web/public/templates/plex-library.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/public/templates/plex-library.html b/web/public/templates/plex-library.html index 01ed94f..7a08241 100644 --- a/web/public/templates/plex-library.html +++ b/web/public/templates/plex-library.html @@ -28,7 +28,7 @@
From a742da3ae0a6e3d6c5a92090803d2c000421eb24 Mon Sep 17 00:00:00 2001 From: vexorian Date: Thu, 25 Mar 2021 17:13:21 -0400 Subject: [PATCH 43/57] #286 : Update guide images when the Plex server configuration is changed. Also make sure that programs inside of filler lists and custom shows are fixed up when modifying the server or deleting it. --- src/api.js | 14 +- src/dao/custom-show-db.js | 4 +- src/dao/filler-db.js | 4 +- src/dao/plex-server-db.js | 141 ++++++++++++++++++--- src/plex-player.js | 2 +- web/directives/plex-server-edit.js | 4 + web/public/templates/plex-server-edit.html | 5 +- 7 files changed, 146 insertions(+), 28 deletions(-) diff --git a/src/api.js b/src/api.js index ee59ad9..ef2dd40 100644 --- a/src/api.js +++ b/src/api.js @@ -27,7 +27,7 @@ module.exports = { router: api } function api(db, channelDB, fillerDB, customShowDB, xmltvInterval, guideService, _m3uService, eventService ) { let m3uService = _m3uService; const router = express.Router() - const plexServerDB = new PlexServerDB(channelDB, channelCache, db); + const plexServerDB = new PlexServerDB(channelDB, channelCache, fillerDB, customShowDB, db); router.get('/api/version', async (req, res) => { try { @@ -141,18 +141,24 @@ function api(db, channelDB, fillerDB, customShowDB, xmltvInterval, guideService }) router.post('/api/plex-servers', async (req, res) => { try { - await plexServerDB.updateServer(req.body); + let report = await plexServerDB.updateServer(req.body); + let modifiedPrograms = 0; + let destroyedPrograms = 0; + report.forEach( (r) => { + modifiedPrograms += r.modifiedPrograms; + destroyedPrograms += r.destroyedPrograms; + } ); res.status(204).send("Plex server updated.");; eventService.push( "settings-update", { - "message": `Plex server ${req.body.name} updated.`, + "message": `Plex server ${req.body.name} updated. ${modifiedPrograms} programs modified, ${destroyedPrograms} programs deleted`, "module" : "plex-server", "detail" : { "serverName" : req.body.name, "action" : "update" }, - "level" : "info" + "level" : "warning" } ); diff --git a/src/dao/custom-show-db.js b/src/dao/custom-show-db.js index f174f3b..2f581d5 100644 --- a/src/dao/custom-show-db.js +++ b/src/dao/custom-show-db.js @@ -110,8 +110,8 @@ class CustomShowDB { async getAllShowsInfo() { //returns just name and id - let fillers = await this.getAllShows(); - return fillers.map( (f) => { + let shows = await this.getAllShows(); + return shows.map( (f) => { return { 'id' : f.id, 'name': f.name, diff --git a/src/dao/filler-db.js b/src/dao/filler-db.js index b9d7f9b..fcd2d3e 100644 --- a/src/dao/filler-db.js +++ b/src/dao/filler-db.js @@ -192,8 +192,8 @@ class FillerDB { } function fixup(json) { - if (typeof(json.fillerContent) === 'undefined') { - json.fillerContent = []; + if (typeof(json.content) === 'undefined') { + json.content = []; } if (typeof(json.name) === 'undefined') { json.name = "Unnamed Filler"; diff --git a/src/dao/plex-server-db.js b/src/dao/plex-server-db.js index ebbf09e..5b349c5 100644 --- a/src/dao/plex-server-db.js +++ b/src/dao/plex-server-db.js @@ -1,14 +1,20 @@ //hmnn this is more of a "PlexServerService"... +const ICON_REGEX = /https?:\/\/.*(\/library\/metadata\/\d+\/thumb\/\d+).X-Plex-Token=.*/; + +const ICON_FIELDS = ["icon", "showIcon", "seasonIcon", "episodeIcon"]; + class PlexServerDB { - constructor(channelDB, channelCache, db) { + constructor(channelDB, channelCache, fillerDB, showDB, db) { this.channelDB = channelDB; this.db = db; this.channelCache = channelCache; + this.fillerDB = fillerDB; + this.showDB = showDB; } - async deleteServer(name) { + async fixupAllChannels(name, newServer) { let channelNumbers = await this.channelDB.getAllChannelNumbers(); let report = await Promise.all( channelNumbers.map( async (i) => { let channel = await this.channelDB.getChannel(i); @@ -16,17 +22,10 @@ class PlexServerDB channelNumber : channel.number, channelName : channel.name, destroyedPrograms: 0, + modifiedPrograms: 0, }; - this.fixupProgramArray(channel.programs, name, channelReport); - this.fixupProgramArray(channel.fillerContent, name, channelReport); - this.fixupProgramArray(channel.fallback, name, channelReport); - if (typeof(channel.fillerContent) !== 'undefined') { - channel.fillerContent = channel.fillerContent.filter( - (p) => { - return (true !== p.isOffline); - } - ); - } + this.fixupProgramArray(channel.programs, name,newServer, channelReport); + //if fallback became offline, remove it if ( (typeof(channel.fallback) !=='undefined') && (channel.fallback.length > 0) @@ -38,15 +37,87 @@ class PlexServerDB channel.offlinePicture = `http://localhost:${process.env.PORT}/images/generic-offline-screen.png`; } } - this.fixupProgramArray(channel.fallback, name, channelReport); + this.fixupProgramArray(channel.fallback, name,newServer, channelReport); await this.channelDB.saveChannel(i, channel); - this.db['plex-servers'].remove( { name: name } ); return channelReport; }) ); this.channelCache.clear(); return report; } + async fixupAllFillers(name, newServer) { + let fillers = await this.fillerDB.getAllFillers(); + let report = await Promise.all( fillers.map( async (filler) => { + let fillerReport = { + channelNumber : "--", + channelName : filler.name + " (filler)", + destroyedPrograms: 0, + modifiedPrograms: 0, + }; + this.fixupProgramArray( filler.content, name,newServer, fillerReport ); + filler.content = this.removeOffline(filler.content); + + await this.fillerDB.saveFiller( filler.id, filler ); + + return fillerReport; + } ) ); + return report; + + } + + async fixupAllShows(name, newServer) { + let shows = await this.showDB.getAllShows(); + let report = await Promise.all( shows.map( async (show) => { + let showReport = { + channelNumber : "--", + channelName : show.name + " (custom show)", + destroyedPrograms: 0, + modifiedPrograms: 0, + }; + this.fixupProgramArray( show.content, name,newServer, showReport ); + show.content = this.removeOffline(show.content); + + await this.showDB.saveShow( show.id, show ); + + return showReport; + } ) ); + return report; + + } + + + removeOffline( progs ) { + if (typeof(progs) === 'undefined') { + return progs; + } + return progs.filter( + (p) => { + return (true !== p.isOffline); + } + ); + } + + async fixupEveryProgramHolders(serverName, newServer) { + let reports = await Promise.all( [ + this.fixupAllChannels( serverName, newServer ), + this.fixupAllFillers(serverName, newServer), + this.fixupAllShows(serverName, newServer), + ] ); + let report = []; + reports.forEach( + (r) => r.forEach( (r2) => { + report.push(r2) + } ) + ); + return report; + } + + async deleteServer(name) { + let report = await this.fixupEveryProgramHolders(name, null); + this.db['plex-servers'].remove( { name: name } ); + return report; + } + doesNameExist(name) { return this.db['plex-servers'].find( { name: name} ).length > 0; } @@ -77,11 +148,15 @@ class PlexServerDB arChannels: arChannels, index: s.index, } + this.normalizeServer(newServer); + + let report = await this.fixupEveryProgramHolders(name, newServer); this.db['plex-servers'].update( { _id: s._id }, newServer ); + return report; } @@ -117,26 +192,56 @@ class PlexServerDB arChannels: arChannels, index: index, }; + this.normalizeServer(newServer); this.db['plex-servers'].save(newServer); } - fixupProgramArray(arr, serverName, channelReport) { + fixupProgramArray(arr, serverName,newServer, channelReport) { if (typeof(arr) !== 'undefined') { for(let i = 0; i < arr.length; i++) { - arr[i] = this.fixupProgram( arr[i], serverName, channelReport ); + arr[i] = this.fixupProgram( arr[i], serverName,newServer, channelReport ); } } } - fixupProgram(program, serverName, channelReport) { - if (program.serverKey === serverName) { + fixupProgram(program, serverName,newServer, channelReport) { + if ( (program.serverKey === serverName) && (newServer == null) ) { channelReport.destroyedPrograms += 1; return { isOffline: true, duration: program.duration, } + } else if (program.serverKey === serverName) { + let modified = false; + ICON_FIELDS.forEach( (field) => { + if ( + (typeof(program[field] ) === 'string') + && + program[field].includes("/library/metadata") + && + program[field].includes("X-Plex-Token") + ) { + let m = program[field].match(ICON_REGEX); + if (m.length == 2) { + let lib = m[1]; + let newUri = `${newServer.uri}${lib}?X-Plex-Token=${newServer.accessToken}` + program[field] = newUri; + modified = true; + } + } + + } ); + if (modified) { + channelReport.modifiedPrograms += 1; + } } return program; } + + normalizeServer(server) { + while (server.uri.endsWith("/")) { + server.uri = server.uri.slice(0,-1); + } + } } module.exports = PlexServerDB \ No newline at end of file diff --git a/src/plex-player.js b/src/plex-player.js index d21bcdc..9a75f84 100644 --- a/src/plex-player.js +++ b/src/plex-player.js @@ -49,7 +49,7 @@ class PlexPlayer { let channel = this.context.channel; let server = db['plex-servers'].find( { 'name': lineupItem.serverKey } ); if (server.length == 0) { - throw Error(`Unable to find server "${lineupItem.serverKey}" specied by program.`); + throw Error(`Unable to find server "${lineupItem.serverKey}" specified by program.`); } server = server[0]; if (server.uri.endsWith("/")) { diff --git a/web/directives/plex-server-edit.js b/web/directives/plex-server-edit.js index c608d19..7772381 100644 --- a/web/directives/plex-server-edit.js +++ b/web/directives/plex-server-edit.js @@ -9,11 +9,13 @@ module.exports = function (dizquetv, $timeout) { }, link: function (scope, element, attrs) { scope.state.modified = false; + scope.loading = { show: false }; scope.setModified = () => { scope.state.modified = true; } scope.onSave = async () => { try { + scope.loading = { show: true }; await dizquetv.updatePlexServer(scope.state.server); scope.state.modified = false; scope.state.success = "The server was updated."; @@ -23,6 +25,8 @@ module.exports = function (dizquetv, $timeout) { scope.state.error = "There was an error updating the server"; scope.state.success = ""; console.error(scope.state.error, err); + } finally { + scope.loading = { show: false }; } $timeout( () => { scope.$apply() } , 0 ); } diff --git a/web/public/templates/plex-server-edit.html b/web/public/templates/plex-server-edit.html index 6a3f63f..444034d 100644 --- a/web/public/templates/plex-server-edit.html +++ b/web/public/templates/plex-server-edit.html @@ -84,12 +84,15 @@ - From 91a5f6337e339e83e03534bec0b912ed60014c29 Mon Sep 17 00:00:00 2001 From: vexorian Date: Thu, 25 Mar 2021 22:41:31 -0400 Subject: [PATCH 44/57] #297 include year in plex library. --- web/directives/plex-library.js | 25 +++++++++++++++++++++++++ web/public/templates/plex-library.html | 9 ++++----- 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/web/directives/plex-library.js b/web/directives/plex-library.js index 4e21a52..ce1018a 100644 --- a/web/directives/plex-library.js +++ b/web/directives/plex-library.js @@ -219,6 +219,31 @@ module.exports = function (plex, dizquetv, $timeout, commonProgramTools) { scope.customShows = await dizquetv.getAllShowsInfo(); scope.$apply(); } + + scope.displayTitle = (show) => { + let r = ""; + if (show.type === 'episode') { + r += show.showTitle + " - "; + if ( typeof(show.season) !== 'undefined' ) { + r += "S" + show.season.toString().padStart(2,'0'); + } + if ( typeof(show.episode) !== 'undefined' ) { + r += "E" + show.episode.toString().padStart(2,'0'); + } + } + if (r != "") { + r = r + " - "; + } + r += show.title; + if ( + (show.type !== 'episode') + && + (typeof(show.year) !== 'undefined') + ) { + r += " (" + JSON.stringify(show.year) + ")"; + } + return r; + } } }; } \ No newline at end of file diff --git a/web/public/templates/plex-library.html b/web/public/templates/plex-library.html index 7a08241..7c678e1 100644 --- a/web/public/templates/plex-library.html +++ b/web/public/templates/plex-library.html @@ -36,7 +36,7 @@
- {{a.title}} + {{ displayTitle(a) }} @@ -50,7 +50,7 @@ - {{b.title}} + {{ displayTitle(b) }} {{b.durationStr}} @@ -75,8 +75,7 @@ - {{ c.type === 'episode' ? c.showTitle + ' - S' + c.season.toString().padStart(2,'0') + 'E' + c.episode.toString().padStart(2,'0') + ' - ' : '' }} - {{c.title}} + {{ displayTitle(c) }} {{c.durationStr}} @@ -91,7 +90,7 @@
- E{{ d.episode.toString().padStart(2,'0')}} - {{d.title}} + {{ displayTitle(d) }} {{d.durationStr}}
From 8d844f0ae39d40f3d4a422a72ab6d3cbaa0a43e2 Mon Sep 17 00:00:00 2001 From: vexorian Date: Fri, 26 Mar 2021 09:59:00 -0400 Subject: [PATCH 45/57] Slots improvements. Fix rare bug in which some times the starting times would get completely messed up. Consecutive flex times are now guaranteed to be merged into a bigger one. --- src/services/random-slots-service.js | 36 +++++++++++++++------------- src/services/time-slots-service.js | 34 +++++++++++++------------- web/directives/channel-config.js | 10 +++++++- 3 files changed, 46 insertions(+), 34 deletions(-) diff --git a/src/services/random-slots-service.js b/src/services/random-slots-service.js index dc3ef70..2cc6064 100644 --- a/src/services/random-slots-service.js +++ b/src/services/random-slots-service.js @@ -276,11 +276,11 @@ module.exports = async( programs, schedule ) => { let s = schedule.slots; let ts = (new Date() ).getTime(); - let curr = ts - ts % DAY; + let t0 = ts; let p = []; let t = t0; - let wantedFinish = 0; + let hardLimit = t0 + schedule.maxDays * DAY; let pushFlex = (d) => { @@ -297,6 +297,15 @@ module.exports = async( programs, schedule ) => { } } + let pushProgram = (item) => { + if ( item.isOffline && (item.type !== 'redirect') ) { + pushFlex(item.duration); + } else { + p.push(item); + t += item.duration; + } + }; + let slotLastPlayed = {}; while ( (t < hardLimit) && (p.length < LIMIT) ) { @@ -338,15 +347,14 @@ module.exports = async( programs, schedule ) => { if (item.isOffline) { //flex or redirect. We can just use the whole duration - p.push(item); - t += remaining; + item.duration = remaining; + pushProgram(item); slotLastPlayed[ slotIndex ] = t; continue; } if (item.duration > remaining) { // Slide - p.push(item); - t += item.duration; + pushProgram(item); slotLastPlayed[ slotIndex ] = t; advanceSlot(slot); continue; @@ -412,8 +420,7 @@ module.exports = async( programs, schedule ) => { } // now unroll them all for (let i = 0; i < pads.length; i++) { - p.push( pads[i].item ); - t += pads[i].item.duration; + pushProgram( pads[i].item ); slotLastPlayed[ slotIndex ] = t; pushFlex( pads[i].pad ); } @@ -421,15 +428,10 @@ module.exports = async( programs, schedule ) => { while ( (t > hardLimit) || (p.length >= LIMIT) ) { t -= p.pop().duration; } - let m = t % schedule.period; - let rem = 0; - if (m > wantedFinish) { - rem = schedule.period + wantedFinish - m; - } else if (m < wantedFinish) { - rem = wantedFinish - m; - } - if (rem > constants.SLACK) { - pushFlex(rem); + let m = (t - t0) % schedule.period; + if (m != 0) { + //ensure the schedule is a multiple of period + pushFlex( schedule.period - m); } diff --git a/src/services/time-slots-service.js b/src/services/time-slots-service.js index 5fd3159..64b6115 100644 --- a/src/services/time-slots-service.js +++ b/src/services/time-slots-service.js @@ -302,6 +302,15 @@ module.exports = async( programs, schedule ) => { } } + let pushProgram = (item) => { + if ( item.isOffline && (item.type !== 'redirect') ) { + pushFlex(item.duration); + } else { + p.push(item); + t += item.duration; + } + }; + if (ts > t0) { pushFlex( ts - t0 ); } @@ -355,14 +364,13 @@ module.exports = async( programs, schedule ) => { if (item.isOffline) { //flex or redirect. We can just use the whole duration - p.push(item); - t += remaining; + item.duration = remaining; + pushProgram(item); continue; } if (item.duration > remaining) { // Slide - p.push(item); - t += item.duration; + pushProgram(item); advanceSlot(slot); continue; } @@ -373,7 +381,7 @@ module.exports = async( programs, schedule ) => { let pads = [ padded ]; while(true) { - let item2 = getNextForSlot(slot); + let item2 = getNextForSlot(slot, remaining); if (total + item2.duration > remaining) { break; } @@ -413,23 +421,17 @@ module.exports = async( programs, schedule ) => { } // now unroll them all for (let i = 0; i < pads.length; i++) { - p.push( pads[i].item ); - t += pads[i].item.duration; + pushProgram( pads[i].item ); pushFlex( pads[i].pad ); } } while ( (t > hardLimit) || (p.length >= LIMIT) ) { t -= p.pop().duration; } - let m = t % schedule.period; - let rem = 0; - if (m > wantedFinish) { - rem = schedule.period + wantedFinish - m; - } else if (m < wantedFinish) { - rem = wantedFinish - m; - } - if (rem > constants.SLACK) { - pushFlex(rem); + let m = (t - t0) % schedule.period; + if (m > 0) { + //ensure the schedule is a multiple of period + pushFlex( schedule.period - m); } diff --git a/web/directives/channel-config.js b/web/directives/channel-config.js index 685bce2..9885f29 100644 --- a/web/directives/channel-config.js +++ b/web/directives/channel-config.js @@ -163,7 +163,15 @@ module.exports = function ($timeout, $location, dizquetv, resolutionOptions, get let t = Date.now(); let originalStart = scope.channel.startTime.getTime(); let n = scope.channel.programs.length; - let totalDuration = scope.channel.duration; + //scope.channel.totalDuration might not have been initialized + let totalDuration = 0; + for (let i = 0; i < n; i++) { + totalDuration += scope.channel.programs[i].duration; + } + if (totalDuration == 0) { + return; + } + let m = (t - originalStart) % totalDuration; let x = 0; let runningProgram = -1; From c2731f0a3478a178e031f26935b57349e52ce1c6 Mon Sep 17 00:00:00 2001 From: vexorian Date: Fri, 26 Mar 2021 10:04:31 -0400 Subject: [PATCH 46/57] Maybe this helps? --- src/video.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/video.js b/src/video.js index 6c1d483..1010476 100644 --- a/src/video.js +++ b/src/video.js @@ -123,10 +123,14 @@ function video( channelDB , fillerDB, db) { // Stream individual video to ffmpeg concat above. This is used by the server, NOT the client router.get('/stream', async (req, res) => { // Check if channel queried is valid + res.on("error", (e) => { + console.err("There was an unexpected error in stream.", e); + } ); if (typeof req.query.channel === 'undefined') { res.status(400).send("No Channel Specified") return } + let audioOnly = ("true" == req.query.audioOnly); console.log(`/stream audioOnly=${audioOnly}`); let session = parseInt(req.query.session); @@ -323,6 +327,7 @@ function video( channelDB , fillerDB, db) { res.writeHead(200, { 'Content-Type': 'video/mp2t' }); + try { playerObj = await player.play(res); } catch (err) { From c75c9bc8e1cea73dbb7ede83b6d466eb66996ed3 Mon Sep 17 00:00:00 2001 From: vexorian Date: Fri, 26 Mar 2021 10:05:39 -0400 Subject: [PATCH 47/57] Rename 1.3.3 to 1.4.1 --- README.md | 2 +- src/constants.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 6c4769b..3581c59 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# dizqueTV 1.3.3-prerelease +# dizqueTV 1.4.1-prerelease ![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. diff --git a/src/constants.js b/src/constants.js index 760e229..aa15f72 100644 --- a/src/constants.js +++ b/src/constants.js @@ -5,5 +5,5 @@ module.exports = { TVGUIDE_MAXIMUM_FLEX_DURATION : 6 * 60 * 60 * 1000, TOO_FREQUENT: 100, - VERSION_NAME: "1.3.3-prerelease" + VERSION_NAME: "1.4.1-prerelease" } From 6b122aae5fe4cb84a9566f40b9fd574f8949b3bf Mon Sep 17 00:00:00 2001 From: vexorian Date: Fri, 26 Mar 2021 10:26:27 -0400 Subject: [PATCH 48/57] 1.4.1 --- README.md | 2 +- src/constants.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 881c8ef..3f0473f 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# dizqueTV 1.3.2 +# dizqueTV 1.4.1 ![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. diff --git a/src/constants.js b/src/constants.js index 5476edd..78b202d 100644 --- a/src/constants.js +++ b/src/constants.js @@ -5,5 +5,5 @@ module.exports = { TVGUIDE_MAXIMUM_FLEX_DURATION : 6 * 60 * 60 * 1000, TOO_FREQUENT: 100, - VERSION_NAME: "1.3.2" + VERSION_NAME: "1.4.1" } From 4ec285fecb322d54bdefa521c854f73da6892715 Mon Sep 17 00:00:00 2001 From: vexorian Date: Sun, 16 May 2021 06:47:55 -0400 Subject: [PATCH 49/57] #313 Fix console.err bug --- src/video.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/video.js b/src/video.js index 1010476..6cf31f6 100644 --- a/src/video.js +++ b/src/video.js @@ -124,7 +124,7 @@ function video( channelDB , fillerDB, db) { router.get('/stream', async (req, res) => { // Check if channel queried is valid res.on("error", (e) => { - console.err("There was an unexpected error in stream.", e); + console.error("There was an unexpected error in stream.", e); } ); if (typeof req.query.channel === 'undefined') { res.status(400).send("No Channel Specified") From 4236867992725b66ecac1c1fdec1751370c3cecc Mon Sep 17 00:00:00 2001 From: vexorian Date: Sun, 16 May 2021 07:12:38 -0400 Subject: [PATCH 50/57] Related to #308 , stop hiding collections with only 1 element. --- web/services/plex.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/services/plex.js b/web/services/plex.js index 9bda0f5..7256a7f 100644 --- a/web/services/plex.js +++ b/web/services/plex.js @@ -305,7 +305,7 @@ module.exports = function ($http, $window, $interval) { }); for (let k = 0; k < keys.length; k++) { let key = keys[k]; - if (collections[key].length <= 1) { + if ( !(collections[key].length >= 1) ) { //it's pointless to include it. continue; } From 35553f828549ff6c79f3e50b6b85b18d3a3a7eca Mon Sep 17 00:00:00 2001 From: vexorian Date: Sun, 16 May 2021 07:32:31 -0400 Subject: [PATCH 51/57] #305 Fix custom shows can't be deleted. --- src/dao/custom-show-db.js | 4 ---- web/controllers/custom-shows.js | 33 ++++++++------------------------- 2 files changed, 8 insertions(+), 29 deletions(-) diff --git a/src/dao/custom-show-db.js b/src/dao/custom-show-db.js index 2f581d5..d1ec450 100644 --- a/src/dao/custom-show-db.js +++ b/src/dao/custom-show-db.js @@ -68,7 +68,6 @@ class CustomShowDB { } async deleteShow(id) { - try { let f = path.join(this.folder, `${id}.json` ); await new Promise( (resolve, reject) => { fs.unlink(f, function (err) { @@ -78,9 +77,6 @@ class CustomShowDB { resolve(); }); }); - } finally { - delete this.cache[id]; - } } diff --git a/web/controllers/custom-shows.js b/web/controllers/custom-shows.js index 517e00a..d3b9378 100644 --- a/web/controllers/custom-shows.js +++ b/web/controllers/custom-shows.js @@ -72,36 +72,19 @@ module.exports = function ($scope, $timeout, dizquetv) { if ( $scope.shows[index].pending) { return; } - $scope.deleteShowIndex = index; - $scope.shows[index].pending = true; - let id = $scope.shows[index].id; - let channels = await dizquetv.getChannelsUsingShow(id); - feedToDeleteShow( { - id: id, - name: $scope.shows[index].name, - channels : channels, - } ); - $timeout(); - - } catch (err) { - console.error("Could not start delete show dialog.", err); - } - - } - - $scope.onShowDelete = async( id ) => { - try { - $scope.shows[ $scope.deleteShowIndex ].pending = false; - $timeout(); - if (typeof(id) !== 'undefined') { - $scope.shows[ $scope.deleteShowIndex ].pending = true; - await dizquetv.deleteShow(id); + let show = $scope.shows[index]; + if (confirm("Are you sure to delete show: " + show.name + "? This will NOT delete the show's programs from channels that are using.")) { + show.pending = true; + await dizquetv.deleteShow(show.id); $timeout(); await $scope.refreshShow(); $timeout(); } + } catch (err) { - console.error("Error attempting to delete show", err); + console.error("Could not delete show.", err); } + } + } \ No newline at end of file From ceb9a4574bc044f952bb8dc3a3cffd9138b9148e Mon Sep 17 00:00:00 2001 From: vexorian Date: Sun, 16 May 2021 07:33:23 -0400 Subject: [PATCH 52/57] Prepare 1.4.2 development --- README.md | 2 +- src/constants.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 3581c59..f33f9fe 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# dizqueTV 1.4.1-prerelease +# dizqueTV 1.4.2-prerelease ![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. diff --git a/src/constants.js b/src/constants.js index aa15f72..f64616c 100644 --- a/src/constants.js +++ b/src/constants.js @@ -5,5 +5,5 @@ module.exports = { TVGUIDE_MAXIMUM_FLEX_DURATION : 6 * 60 * 60 * 1000, TOO_FREQUENT: 100, - VERSION_NAME: "1.4.1-prerelease" + VERSION_NAME: "1.4.2-prerelease" } From 210a93043a36fd85a8dcecacc2c1387297b95543 Mon Sep 17 00:00:00 2001 From: vexorian Date: Sun, 16 May 2021 07:57:57 -0400 Subject: [PATCH 53/57] Fix #304 arChannels was forced to be equal to arGuide --- src/dao/plex-server-db.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dao/plex-server-db.js b/src/dao/plex-server-db.js index 5b349c5..02d19ed 100644 --- a/src/dao/plex-server-db.js +++ b/src/dao/plex-server-db.js @@ -136,7 +136,7 @@ class PlexServerDB if (typeof(arGuide) === 'undefined') { arGuide = true; } - let arChannels = server.arGuide; + let arChannels = server.arChannels; if (typeof(arChannels) === 'undefined') { arChannels = false; } From ad6dcb4a33a4f7790f247027da5c82e7aa28c385 Mon Sep 17 00:00:00 2001 From: vexorian Date: Sat, 29 May 2021 16:13:01 -0400 Subject: [PATCH 54/57] #299 Hopefully fix the playback issues introduced when adding music library-support --- src/plexTranscoder.js | 90 ++++++++++++++++++++++++++++--------------- 1 file changed, 60 insertions(+), 30 deletions(-) diff --git a/src/plexTranscoder.js b/src/plexTranscoder.js index 22b0017..b4f8524 100644 --- a/src/plexTranscoder.js +++ b/src/plexTranscoder.js @@ -35,6 +35,7 @@ class PlexTranscoder { this.updateInterval = 30000 this.updatingPlex = undefined this.playState = "stopped" + this.mediaHasNoVideo = false; this.albumArt = { attempted : false, path: null, @@ -48,23 +49,26 @@ class PlexTranscoder { this.log(` deinterlace: ${deinterlace}`) this.log(` streamPath: ${this.settings.streamPath}`) + this.setTranscodingArgs(stream.directPlay, true, false, false); + await this.tryToDetectAudioOnly(); + if (this.settings.streamPath === 'direct' || this.settings.forceDirectPlay) { if (this.settings.enableSubtitles) { - console.log("Direct play is forced, so subtitles are forcibly disabled."); + this.log("Direct play is forced, so subtitles are forcibly disabled."); this.settings.enableSubtitles = false; } stream = {directPlay: true} } else { try { this.log("Setting transcoding parameters") - this.setTranscodingArgs(stream.directPlay, true, deinterlace, true) + this.setTranscodingArgs(stream.directPlay, true, deinterlace, this.mediaHasNoVideo) await this.getDecision(stream.directPlay); if (this.isDirectPlay()) { stream.directPlay = true; stream.streamUrl = this.plexFile; } } catch (err) { - this.log("Error when getting decision. 1. Check Plex connection. 2. This might also be a sign that plex direct play and transcode settings are too strict and it can't find any allowed action for the selected video.") + console.error("Error when getting decision. 1. Check Plex connection. 2. This might also be a sign that plex direct play and transcode settings are too strict and it can't find any allowed action for the selected video.", err) stream.directPlay = true; } } @@ -74,7 +78,7 @@ class PlexTranscoder { } this.log("Direct play forced or native paths enabled") stream.directPlay = true - this.setTranscodingArgs(stream.directPlay, true, false) + this.setTranscodingArgs(stream.directPlay, true, false, this.mediaHasNoVideo ) // Update transcode decision for session await this.getDecision(stream.directPlay); stream.streamUrl = (this.settings.streamPath === 'direct') ? this.file : this.plexFile; @@ -92,7 +96,7 @@ class PlexTranscoder { } else if (this.isVideoDirectStream() === false) { this.log("Decision: Should transcode") // Change transcoding arguments to be the user chosen transcode parameters - this.setTranscodingArgs(stream.directPlay, false, deinterlace) + this.setTranscodingArgs(stream.directPlay, false, deinterlace, this.mediaHasNoVideo) // Update transcode decision for session await this.getDecision(stream.directPlay); stream.streamUrl = `${this.transcodeUrlBase}${this.transcodingArgs}` @@ -114,13 +118,13 @@ class PlexTranscoder { return stream } - setTranscodingArgs(directPlay, directStream, deinterlace, firstTry) { + setTranscodingArgs(directPlay, directStream, deinterlace, audioOnly) { let resolution = (directStream) ? this.settings.maxPlayableResolution : this.settings.maxTranscodeResolution let bitrate = (directStream) ? this.settings.directStreamBitrate : this.settings.transcodeBitrate let mediaBufferSize = (directStream) ? this.settings.mediaBufferSize : this.settings.transcodeMediaBufferSize let subtitles = (this.settings.enableSubtitles) ? "burn" : "none" // subtitle options: burn, none, embedded, sidecar let streamContainer = "mpegts" // Other option is mkv, mkv has the option of copying it's subs for later processing - let isDirectPlay = (directPlay) ? '1' : (firstTry? '': '0'); + let isDirectPlay = (directPlay) ? '1' : '0'; let hasMDE = '1'; let videoQuality=`100` // Not sure how this applies, maybe this works if maxVideoBitrate is not set @@ -137,12 +141,17 @@ class PlexTranscoder { vc = "av1"; } - let clientProfile=`add-transcode-target(type=videoProfile&protocol=${this.settings.streamProtocol}&container=${streamContainer}&videoCodec=${vc}&audioCodec=${this.settings.audioCodecs}&subtitleCodec=&context=streaming&replace=true)+\ + let clientProfile =""; + if (! audioOnly ) { + clientProfile=`add-transcode-target(type=videoProfile&protocol=${this.settings.streamProtocol}&container=${streamContainer}&videoCodec=${vc}&audioCodec=${this.settings.audioCodecs}&subtitleCodec=&context=streaming&replace=true)+\ add-transcode-target-settings(type=videoProfile&context=streaming&protocol=${this.settings.streamProtocol}&CopyMatroskaAttachments=true)+\ add-transcode-target-settings(type=videoProfile&context=streaming&protocol=${this.settings.streamProtocol}&BreakNonKeyframes=true)+\ add-limitation(scope=videoCodec&scopeName=*&type=upperBound&name=video.width&value=${resolutionArr[0]})+\ add-limitation(scope=videoCodec&scopeName=*&type=upperBound&name=video.height&value=${resolutionArr[1]})` - + } else { + clientProfile=`add-transcode-target(type=musicProfile&protocol=${this.settings.streamProtocol}&container=${streamContainer}&audioCodec=${this.settings.audioCodecs}&subtitleCodec=&context=streaming&replace=true)` + + } // Set transcode settings per audio codec this.settings.audioCodecs.split(",").forEach(function (codec) { clientProfile+=`+add-transcode-target-audio-codec(type=videoProfile&context=streaming&protocol=${this.settings.streamProtocol}&audioCodec=${codec})` @@ -196,7 +205,7 @@ lang=en` try { return this.getVideoStats().videoDecision === "copy"; } catch (e) { - console.log("Error at decision:", e); + console.error("Error at decision:", e); return false; } } @@ -216,7 +225,7 @@ lang=en` } return this.getVideoStats().videoDecision === "copy" && this.getVideoStats().audioDecision === "copy"; } catch (e) { - console.log("Error at decision:" , e); + console.error("Error at decision:" , e); return false; } } @@ -262,7 +271,7 @@ lang=en` } }.bind(this) ) } catch (e) { - console.log("Error at decision:" , e); + console.error("Error at decision:" , e); } if (typeof(ret.videoCodec) === 'undefined') { ret.audioOnly = true; @@ -297,11 +306,11 @@ lang=en` } }) } catch (e) { - console.log("Error at get media info:" + e); + console.error("Error at get media info:" + e); } }) .catch((err) => { - console.log(err); + console.error("Error getting audio index",err); }); this.log(`Found audio index: ${index}`) @@ -326,36 +335,42 @@ lang=en` // Print error message if transcode not possible // TODO: handle failure better + if (res.data.MediaContainer.mdeDecisionCode === 1000) { + this.log("mde decision code 1000, so it's all right?"); + return; + } + let transcodeDecisionCode = res.data.MediaContainer.transcodeDecisionCode; if ( ( typeof(transcodeDecisionCode) === 'undefined' ) ) { this.decisionJson.MediaContainer.transcodeDecisionCode = 'novideo'; - console.log("Audio-only file detected"); - await this.tryToGetAlbumArt(); + this.log("Strange case, attempt direct play"); } else if (!(directPlay || transcodeDecisionCode == "1001")) { - console.log(`IMPORTANT: Recieved transcode decision code ${transcodeDecisionCode}! Expected code 1001.`) - console.log(`Error message: '${res.data.MediaContainer.transcodeDecisionText}'`) + this.log(`IMPORTANT: Recieved transcode decision code ${transcodeDecisionCode}! Expected code 1001.`) + this.log(`Error message: '${res.data.MediaContainer.transcodeDecisionText}'`) } } - - async tryToGetAlbumArt() { + + async tryToDetectAudioOnly() { try { - if(this.albumArt.attempted ) { - return; - } - this.albumArt.attempted = true; - - this.log("Try to get album art:"); + this.log("Try to detect audio only:"); let url = `${this.server.uri}${this.key}?${this.transcodingArgs}`; let res = await axios.get(url, { headers: { Accept: 'application/json' } }); + let mediaContainer = res.data.MediaContainer; - if (typeof(mediaContainer) !== 'undefined') { - for( let i = 0; i < mediaContainer.Metadata.length; i++) { - console.log("got art: " + mediaContainer.Metadata[i].thumb ); - this.albumArt.path = `${this.server.uri}${mediaContainer.Metadata[i].thumb}?X-Plex-Token=${this.server.accessToken}`; + let metadata = getOneOrUndefined( mediaContainer, "Metadata"); + if (typeof(metadata) !== 'undefined') { + this.albumArt.path = `${this.server.uri}${metadata.thumb}?X-Plex-Token=${this.server.accessToken}`; + + let media = getOneOrUndefined( metadata, "Media"); + if (typeof(media) !== 'undefined') { + if (typeof(media.videoCodec)==='undefined') { + this.log("Audio-only file detected"); + this.mediaHasNoVideo = true; + } } } } catch (err) { @@ -447,4 +462,19 @@ function parsePixelAspectRatio(s) { q: parseInt(x[1], 10), } } + +function getOneOrUndefined(object, field) { + if (typeof(object) === 'undefined') { + return undefined; + } + if ( typeof(object[field]) === "undefined") { + return undefined; + } + let x = object[field]; + if (x.length < 1) { + return undefined; + } + return x[0]; +} + module.exports = PlexTranscoder From df382f26f728465af710672f012b7e361daf755e Mon Sep 17 00:00:00 2001 From: vexorian Date: Sat, 29 May 2021 16:53:25 -0400 Subject: [PATCH 55/57] TV Guide will be updater much quicker than before. Thanks to using setIPmmediate instead of 0-seconds timer. --- src/tv-guide-service.js | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/tv-guide-service.js b/src/tv-guide-service.js index d64da30..84027be 100644 --- a/src/tv-guide-service.js +++ b/src/tv-guide-service.js @@ -14,8 +14,6 @@ class TVGuideService this.currentUpdate = -1; this.currentLimit = -1; this.currentChannels = null; - this.throttleX = 0; - this.doThrottle = false; this.xmltv = xmltv; this.db = db; this.cacheImageService = cacheImageService; @@ -26,7 +24,7 @@ class TVGuideService while (this.cached == null) { await _wait(100); } - this.doThrottle = true; + return this.cached; } @@ -357,11 +355,10 @@ class TVGuideService } } - async _throttle() { - //this.doThrottle = true; - if ( this.doThrottle && (this.throttleX++)%10 == 0) { - await _wait(0); - } + _throttle() { + return new Promise((resolve) => { + setImmediate(() => resolve()); + }); } async refreshXML() { From f134a75e9822acf64b16c97aafd339b7a0b38aef Mon Sep 17 00:00:00 2001 From: vexorian Date: Sun, 30 May 2021 07:25:24 -0400 Subject: [PATCH 56/57] Fix #316 . Opening a large channel in the UI causes playback to freeze for a second. --- package.json | 1 + src/api.js | 68 +++++++++++++++++++++++++++++++++++-- web/controllers/channels.js | 7 +++- web/services/dizquetv.js | 7 ++++ 4 files changed, 80 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index d6f96d5..5990b96 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "author": "Dan Ferguson", "license": "ISC", "dependencies": { + "JSONStream": "1.0.5", "angular": "^1.7.9", "angular-router-browserify": "0.0.2", "angular-vs-repeat": "2.0.13", diff --git a/src/api.js b/src/api.js index ef2dd40..a00501a 100644 --- a/src/api.js +++ b/src/api.js @@ -5,6 +5,7 @@ const fs = require('fs') const databaseMigration = require('./database-migration'); const channelCache = require('./channel-cache') const constants = require('./constants'); +const JSONStream = require('JSONStream'); const FFMPEGInfo = require('./ffmpeg-info'); const PlexServerDB = require('./dao/plex-server-db'); const Plex = require("./plex.js"); @@ -232,9 +233,10 @@ function api(db, channelDB, fillerDB, customShowDB, xmltvInterval, guideService try { let number = parseInt(req.params.number, 10); let channel = await channelCache.getChannelConfig(channelDB, number); + if (channel.length == 1) { - channel = channel[0]; - res.send( channel ); + channel = channel[0]; + res.send(channel); } else { return res.status(404).send("Channel not found"); } @@ -243,6 +245,61 @@ function api(db, channelDB, fillerDB, customShowDB, xmltvInterval, guideService res.status(500).send("error"); } }) + router.get('/api/channel/programless/:number', async (req, res) => { + try { + let number = parseInt(req.params.number, 10); + let channel = await channelCache.getChannelConfig(channelDB, number); + + if (channel.length == 1) { + channel = channel[0]; + let copy = {}; + Object.keys(channel).forEach( (key) => { + if (key != 'programs') { + copy[key] = channel[key]; + } + } ); + res.send(copy); + } else { + return res.status(404).send("Channel not found"); + } + } catch(err) { + console.error(err); + res.status(500).send("error"); + } + }) + + router.get('/api/channel/programs/:number', async (req, res) => { + try { + let number = parseInt(req.params.number, 10); + let channel = await channelCache.getChannelConfig(channelDB, number); + + if (channel.length == 1) { + channel = channel[0]; + let programs = channel.programs; + if (typeof(programs) === 'undefined') { + return res.status(404).send("Channel doesn't have programs?"); + } + res.writeHead(200, { + 'Content-Type': 'application.json' + }); + + let transformStream = JSONStream.stringify(); //false makes it not add 'separators' + transformStream.pipe(res); + + for (let i = 0; i < programs.length; i++) { + transformStream.write( programs[i] ); + await throttle(); + } + transformStream.end(); + + } else { + return res.status(404).send("Channel not found"); + } + } catch(err) { + console.error(err); + res.status(500).send("error"); + } + }) router.get('/api/channel/description/:number', async (req, res) => { try { let number = parseInt(req.params.number, 10); @@ -1014,3 +1071,10 @@ function api(db, channelDB, fillerDB, customShowDB, xmltvInterval, guideService return router } + + +async function throttle() { + return new Promise((resolve) => { + setImmediate(() => resolve()); + }); +} diff --git a/web/controllers/channels.js b/web/controllers/channels.js index c9fa27f..1cc861d 100644 --- a/web/controllers/channels.js +++ b/web/controllers/channels.js @@ -80,7 +80,12 @@ module.exports = function ($scope, dizquetv) { $scope.showChannelConfig = true } else { $scope.channels[index].pending = true; - let ch = await dizquetv.getChannel($scope.channels[index].number); + let p = await Promise.all([ + dizquetv.getChannelProgramless($scope.channels[index].number), + dizquetv.getChannelPrograms($scope.channels[index].number), + ]); + let ch = p[0]; + ch.programs = p[1]; let newObj = ch; newObj.startTime = new Date(newObj.startTime) $scope.originalChannelNumber = newObj.number; diff --git a/web/services/dizquetv.js b/web/services/dizquetv.js index 2ad247e..4e8b8f6 100644 --- a/web/services/dizquetv.js +++ b/web/services/dizquetv.js @@ -137,6 +137,13 @@ module.exports = function ($http, $q) { return $http.get(`/api/channel/description/${number}`).then( (d) => { return d.data } ) }, + getChannelProgramless: (number) => { + return $http.get(`/api/channel/programless/${number}`).then( (d) => { return d.data }) + }, + getChannelPrograms: (number) => { + return $http.get(`/api/channel/programs/${number}`).then( (d) => { return d.data } ) + }, + getChannelNumbers: () => { return $http.get('/api/channelNumbers').then( (d) => { return d.data } ) From cbe7a53667bd74d2e8367e49a023cc2d6b9d2bef Mon Sep 17 00:00:00 2001 From: vexorian Date: Sun, 30 May 2021 08:04:44 -0400 Subject: [PATCH 57/57] 1.4.2 --- README.md | 2 +- src/constants.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 3f0473f..3fd9b6a 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# dizqueTV 1.4.1 +# dizqueTV 1.4.2 ![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. diff --git a/src/constants.js b/src/constants.js index 78b202d..f1b3bb0 100644 --- a/src/constants.js +++ b/src/constants.js @@ -5,5 +5,5 @@ module.exports = { TVGUIDE_MAXIMUM_FLEX_DURATION : 6 * 60 * 60 * 1000, TOO_FREQUENT: 100, - VERSION_NAME: "1.4.1" + VERSION_NAME: "1.4.2" }