diff --git a/.gitignore b/.gitignore index 5255da1..a2ef243 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ dist/ bin/ .pseudotv/ .dizquetv/ -web/public/bundle.js \ No newline at end of file +web/public/bundle.js +*.orig \ No newline at end of file diff --git a/README.md b/README.md index 5d6c6f0..3bab198 100644 --- a/README.md +++ b/README.md @@ -92,6 +92,11 @@ docker run --name dizquetv -p 8000:8000 -v C:\.dizquetv:/home/node/app/.dizquetv If you were a pseudotv user, make sure to stop the pseudotv container and use the same folder you used for configuration in pseudotv as configuration for dizquetv. +#### Unraid + +Template Repository: [https://github.com/vexorian/dizquetv/tree/main](https://github.com/vexorian/dizquetv/tree/main) + + #### Building Docker image from source Build docker image from source and run the container. (replace `C:\.dizquetv` with your desired config directory location) diff --git a/dizquetv-nvidia.xml b/dizquetv-nvidia.xml index 26a87e6..f55aeac 100644 --- a/dizquetv-nvidia.xml +++ b/dizquetv-nvidia.xml @@ -15,7 +15,7 @@ dizqueTV will show up as a HDHomeRun device within Plex. When configuring your P http://[IP]:[PORT:8000] - https://raw.githubusercontent.com/vexorian/dizquetv/master/resources/dizquetv.png + https://raw.githubusercontent.com/vexorian/dizquetv/main/resources/dizquetv.png --runtime=nvidia diff --git a/dizquetv.xml b/dizquetv.xml index da9178b..e939a5d 100644 --- a/dizquetv.xml +++ b/dizquetv.xml @@ -13,7 +13,7 @@ http://[IP]:[PORT:8000] - https://raw.githubusercontent.com/vexorian/dizquetv/master/resources/dizquetv.png + https://raw.githubusercontent.com/vexorian/dizquetv/main/resources/dizquetv.png diff --git a/index.js b/index.js index b0150da..1336335 100644 --- a/index.js +++ b/index.js @@ -19,9 +19,9 @@ console.log( ` \\ dizqueTV ${constants.VERSION_NAME} .------------. -|###:::||| o | -|###:::||| | -'###:::||| o | +|:::///### o | +|:::///### | +':::///### o | '------------' `); @@ -102,6 +102,24 @@ xmltvInterval.startInterval() let hdhr = HDHR(db) let app = express() app.use(bodyParser.json({limit: '50mb'})) +app.get('/version.js', (req, res) => { + res.writeHead(200, { + 'Content-Type': 'application/javascript' + }); + + res.write( ` + function setUIVersionNow() { + setTimeout( setUIVersionNow, 1000); + var element = document.getElementById("uiversion"); + if (element != null) { + element.innerHTML = "${constants.VERSION_NAME}"; + } + } + setTimeout( setUIVersionNow, 1000); + ` ); + res.end(); +}); +app.use('/images', express.static(path.join(process.env.DATABASE, 'images'))) app.use(express.static(path.join(__dirname, 'web/public'))) app.use('/images', express.static(path.join(process.env.DATABASE, 'images'))) app.use(api.router(db, xmltvInterval)) diff --git a/src/api.js b/src/api.js index 5f0fe39..96c4f1d 100644 --- a/src/api.js +++ b/src/api.js @@ -3,15 +3,21 @@ const express = require('express') const fs = require('fs') const databaseMigration = require('./database-migration'); const channelCache = require('./channel-cache') -const constants = require('./constants') +const constants = require('./constants'); +const FFMPEGInfo = require('./ffmpeg-info'); module.exports = { router: api } function api(db, xmltvInterval) { let router = express.Router() - router.get('/api/version', (req, res) => { - res.send( { "dizquetv" : constants.VERSION_NAME } ) - }) + router.get('/api/version', async (req, res) => { + let ffmpegSettings = db['ffmpeg-settings'].find()[0]; + let v = await (new FFMPEGInfo(ffmpegSettings)).getVersion(); + res.send( { + "dizquetv" : constants.VERSION_NAME, + "ffmpeg" : v, + } ); + }); // Plex Servers router.get('/api/plex-servers', (req, res) => { @@ -177,13 +183,14 @@ function api(db, xmltvInterval) { router.get('/api/channels.m3u', (req, res) => { res.type('text') let channels = db['channels'].find() + channels.sort((a, b) => { return a.number < b.number ? -1 : 1 }) var data = "#EXTM3U\n" for (var i = 0; i < channels.length; i++) { data += `#EXTINF:0 tvg-id="${channels[i].number}" tvg-name="${channels[i].name}" tvg-logo="${channels[i].icon}" group-title="dizqueTV",${channels[i].name}\n` data += `${req.protocol}://${req.get('host')}/video?channel=${channels[i].number}\n` } if (channels.length === 0) { - data += `#EXTINF:0 tvg-id="1" tvg-name="dizqueTV" tvg-logo="https://raw.githubusercontent.com/vexorian/dizquetv/master/resources/dizquetv.png" group-title="dizqueTV",dizqueTV\n` + data += `#EXTINF:0 tvg-id="1" tvg-name="dizqueTV" tvg-logo="https://raw.githubusercontent.com/vexorian/dizquetv/main/resources/dizquetv.png" group-title="dizqueTV",dizqueTV\n` data += `${req.protocol}://${req.get('host')}/setup\n` } res.send(data) diff --git a/src/channel-cache.js b/src/channel-cache.js index d6b03f0..4af4272 100644 --- a/src/channel-cache.js +++ b/src/channel-cache.js @@ -27,7 +27,7 @@ function getCurrentLineupItem(channelId, t1) { let recorded = cache[channelId]; let lineupItem = JSON.parse( JSON.stringify(recorded.lineupItem) ); let diff = t1 - recorded.t0; - if (diff <= SLACK) { + if ( (diff <= SLACK) && (lineupItem.actualDuration >= 2*SLACK) ) { //closed the stream and opened it again let's not lose seconds for //no reason return lineupItem; diff --git a/src/constants.js b/src/constants.js index 4a3a1c9..5cffd7c 100644 --- a/src/constants.js +++ b/src/constants.js @@ -2,5 +2,5 @@ module.exports = { SLACK: 9999, TVGUIDE_MAXIMUM_PADDING_LENGTH_MS: 30*60*1000, - VERSION_NAME: "0.0.61" + VERSION_NAME: "0.0.62-prerelease" } diff --git a/src/ffmpeg-info.js b/src/ffmpeg-info.js new file mode 100644 index 0000000..ef264fd --- /dev/null +++ b/src/ffmpeg-info.js @@ -0,0 +1,26 @@ +const exec = require('child_process').exec; + +class FFMPEGInfo { + constructor(opts) { + this.ffmpegPath = opts.ffmpegPath + } + async getVersion() { + try { + let s = await new Promise( (resolve, reject) => { + exec( `"${this.ffmpegPath}" -version`, function(error, stdout, stderr){ + if (error !== null) { + reject(error); + } else { + resolve(stdout); + } + }); + }); + return s.match( /version ([^\s]+) Copyright/ )[1]; + } catch (err) { + console.error("Error getting ffmpeg version", err); + return "Error"; + } + } +} + +module.exports = FFMPEGInfo \ No newline at end of file diff --git a/src/ffmpeg.js b/src/ffmpeg.js index 3dfa9f5..0d0d4bf 100644 --- a/src/ffmpeg.js +++ b/src/ffmpeg.js @@ -1,8 +1,6 @@ const spawn = require('child_process').spawn const events = require('events') -//they can customize this by modifying the picture in .dizquetv folder - const MAXIMUM_ERROR_DURATION_MS = 60000; class FFMPEG extends events.EventEmitter { diff --git a/src/program-player.js b/src/program-player.js index f3354ab..b3b727b 100644 --- a/src/program-player.js +++ b/src/program-player.js @@ -29,6 +29,11 @@ class ProgramPlayer { constructor( context ) { this.context = context; let program = context.lineupItem; + if (context.m3u8) { + context.ffmpegSettings.normalizeAudio = false; + // people might want the codec normalization to stay because of player support + context.ffmpegSettings.normalizeResolution = false; + } if (program.err instanceof Error) { console.log("About to play error stream"); this.delegate = new OfflinePlayer(true, context); diff --git a/src/video.js b/src/video.js index 2c4a886..7899d3f 100644 --- a/src/video.js +++ b/src/video.js @@ -135,7 +135,7 @@ function video(db) { res.status(400).send("No Channel Specified") return } - + let m3u8 = (req.query.m3u8 === '1'); let number = parseInt(req.query.channel); let channel = channelCache.getChannelConfig(db, number); @@ -228,6 +228,7 @@ function video(db) { ffmpegSettings : ffmpegSettings, channel: channel, db: db, + m3u8: m3u8, } let player = new ProgramPlayer(playerContext); @@ -320,11 +321,11 @@ function video(db) { let ffmpegSettings = db['ffmpeg-settings'].find()[0] if ( ffmpegSettings.enableFFMPEGTranscoding === true) { - data += `${req.protocol}://${req.get('host')}/stream?channel=${channelNum}&first=0\n`; + data += `${req.protocol}://${req.get('host')}/stream?channel=${channelNum}&first=0&m3u8=1\n`; } - data += `${req.protocol}://${req.get('host')}/stream?channel=${channelNum}&first=1\n` + data += `${req.protocol}://${req.get('host')}/stream?channel=${channelNum}&first=1&m3u8=1\n` for (var i = 0; i < maxStreamsToPlayInARow - 1; i++) { - data += `${req.protocol}://${req.get('host')}/stream?channel=${channelNum}\n` + data += `${req.protocol}://${req.get('host')}/stream?channel=${channelNum}&m3u8=1\n` } res.send(data) @@ -353,7 +354,13 @@ function video(db) { let ffmpegSettings = db['ffmpeg-settings'].find()[0] - if ( ffmpegSettings.enableFFMPEGTranscoding === true) { + if ( + (ffmpegSettings.enableFFMPEGTranscoding === true) + && (ffmpegSettings.normalizeVideoCodec === true) + && (ffmpegSettings.normalizeAudioCodec === true) + && (ffmpegSettings.normalizeResolution === true) + && (ffmpegSettings.normalizeAudio === true) + ) { data += `file 'http://localhost:${process.env.PORT}/stream?channel=${channelNum}&first=0'\n`; } data += `file 'http://localhost:${process.env.PORT}/stream?channel=${channelNum}&first=1'\n` diff --git a/src/xmltv.js b/src/xmltv.js index df8cffb..5901527 100644 --- a/src/xmltv.js +++ b/src/xmltv.js @@ -15,7 +15,7 @@ function WriteXMLTV(channels, xmlSettings) { _writeDocStart(xw) async function middle() { if (channels.length === 0) { // Write Dummy dizqueTV Channel if no channel exists - _writeChannels(xw, [{ number: 1, name: "dizqueTV", icon: "https://raw.githubusercontent.com/vexorain/dizquetv/master/resources/dizquetv.png" }]) + _writeChannels(xw, [{ number: 1, name: "dizqueTV", icon: "https://raw.githubusercontent.com/vexorain/dizquetv/main/resources/dizquetv.png" }]) let program = { program: { type: 'movie', diff --git a/web/controllers/version.js b/web/controllers/version.js index cd11aac..8f0f35d 100644 --- a/web/controllers/version.js +++ b/web/controllers/version.js @@ -1,7 +1,9 @@ module.exports = function ($scope, dizquetv) { $scope.version = "Getting dizqueTV version..." + $scope.ffmpegVersion = "Getting ffmpeg version..." dizquetv.getVersion().then((version) => { - $scope.version = version.dizquetv + $scope.version = version.dizquetv; + $scope.ffmpegVersion = version.ffmpeg; }) diff --git a/web/directives/channel-config.js b/web/directives/channel-config.js index 0d542ad..ca75418 100644 --- a/web/directives/channel-config.js +++ b/web/directives/channel-config.js @@ -137,7 +137,7 @@ module.exports = function ($timeout, $location) { isOffline: true } scope.updateChannelFromOfflineResult(result); - scope.channel.programs.push( program ); + scope.channel.programs.splice(scope.minProgramIndex, 0, program); scope._selectedOffline = null scope._addingOffline = null; updateChannelDuration() @@ -268,6 +268,20 @@ module.exports = function ($timeout, $location) { updateChannelDuration() } + scope.wipeSpecials = () => { + let tmpProgs = [] + let progs = scope.channel.programs + for (let i = 0, l = progs.length; i < l; i++) { + if (progs[i].season !== 0) { + tmpProgs.push(progs[i]); + } + } + scope.channel.programs = tmpProgs + updateChannelDuration() + } + + + scope.describeFallback = () => { if (scope.channel.offlineMode === 'pic') { if ( diff --git a/web/directives/plex-library.js b/web/directives/plex-library.js index 2e32c3f..272d783 100644 --- a/web/directives/plex-library.js +++ b/web/directives/plex-library.js @@ -10,6 +10,7 @@ module.exports = function (plex, dizquetv, $timeout) { limit: "@limit", }, link: function (scope, element, attrs) { + scope.errors=[]; if ( typeof(scope.limit) == 'undefined') { scope.limit = 1000000000; } @@ -45,8 +46,12 @@ module.exports = function (plex, dizquetv, $timeout) { await scope.wait(0); scope.pending += 1; try { - item.streams = await plex.getStreams(scope.plexServer, item.key) + item.streams = await plex.getStreams(scope.plexServer, item.key, scope.errors) scope.selection.push(JSON.parse(angular.toJson(item))) + } catch (err) { + let msg = "Unable to add item: " + item.key + " " + item.title; + scope.errors.push(msg); + console.error(msg, err); } finally { scope.pending -= 1; } @@ -99,7 +104,7 @@ module.exports = function (plex, dizquetv, $timeout) { } scope.fillNestedIfNecessary = async (x, isLibrary) => { if ( (typeof(x.nested) === 'undefined') && (x.type !== 'collection') ) { - x.nested = await plex.getNested(scope.plexServer, x.key, isLibrary); + x.nested = await plex.getNested(scope.plexServer, x.key, isLibrary, scope.errors); } } scope.getNested = (list, isLibrary) => { diff --git a/web/public/index.html b/web/public/index.html index 6cea21f..ed623f3 100644 --- a/web/public/index.html +++ b/web/public/index.html @@ -6,6 +6,7 @@ + diff --git a/web/public/templates/channel-config.html b/web/public/templates/channel-config.html index c2624e1..1652b04 100644 --- a/web/public/templates/channel-config.html +++ b/web/public/templates/channel-config.html @@ -139,7 +139,7 @@

Similar to Balance TV Shows, but this allows you to pick the weights for each of the shows, so you can decide that some shows should be less frequent than other shows. It has similar caveats as "Balance Shows".

Add Flex
-

Adds a "Flex" Time Slot. Can be configured to play a fallback screen and/or random "filler" content (e.g "commercials", trailers, prerolls, countdowns, music videos, channel bumpers, etc.). Short Flex periods are hidden from the TV guide and are displayed as extensions to the previous program. Long Flex periods appear as the channel name in the TV guide.

+

Adds a "Flex" Time Slot. Can be configured to play a fallback screen and/or random "filler" content (e.g "commercials", trailers, prerolls, countdowns, music videos, channel bumpers, etc.). Short Flex periods are hidden from the TV guide and are displayed as extensions to the previous program. Long Flex periods appear as the channel name in the TV guide. Normally this is not the best way to add Flex time, and you'd be better off using the Pad Times, Restrict Hours or Add Breaks features. This one is for adding specific, single instances of flex time.

Pad Times

Adds Flex breaks after each TV episode or movie to ensure that the program starts at one of the allowed minute marks. For example, you can use this to ensure that all your programs start at either XX:00 times or XX:30 times. Removes any existing Flex periods before adding the new ones.

@@ -156,6 +156,9 @@
Remove Flex

Removes any Flex periods from the schedule.

+
Remove Specials
+

Removes any specials from the schedule. Specials are episodes with season "00".

+
Remove All

Wipes out the schedule so that you can start over.

@@ -253,7 +256,10 @@
-
+
+ +
+
diff --git a/web/public/templates/ffmpeg-settings.html b/web/public/templates/ffmpeg-settings.html index e558b14..6e417d4 100644 --- a/web/public/templates/ffmpeg-settings.html +++ b/web/public/templates/ffmpeg-settings.html @@ -10,7 +10,7 @@
FFMPEG Executable Path (eg: C:\ffmpeg\bin\ffmpeg.exe || /usr/bin/ffmpeg)
- FFMPEG version 4.2+ required. Check by running '{{settings.ffmpegPath}} -version' from the command line + FFMPEG version 4.2+ required. Check by opening the version tab
Miscellaneous Options
diff --git a/web/public/templates/plex-library.html b/web/public/templates/plex-library.html index 4270bce..a95c9c1 100644 --- a/web/public/templates/plex-library.html +++ b/web/public/templates/plex-library.html @@ -107,6 +107,8 @@
Selected Items
{{ selection.length }} elements added in total. Only the last 10 elements are displayed:
+
{{ e }}
+
    Select media items from your plex library above.
  • diff --git a/web/public/views/version.html b/web/public/views/version.html index 9f29b91..c0d2f0e 100644 --- a/web/public/views/version.html +++ b/web/public/views/version.html @@ -9,9 +9,17 @@ Version - dizqueTV + dizqueTV-backend {{version}} + + dizqueTV-ui + Getting dizqueTV UI version... + + + FFMPEG + {{ffmpegVersion}} + diff --git a/web/services/plex.js b/web/services/plex.js index 1944415..381b3a5 100644 --- a/web/services/plex.js +++ b/web/services/plex.js @@ -119,13 +119,14 @@ module.exports = function ($http, $window, $interval) { return streams }) }, - getNested: async (server, key, includeCollections) => { + getNested: async (server, key, includeCollections, errors) => { var client = new Plex(server) const res = await client.Get(key) var nested = [] var seenFiles = {}; var collections = {}; for (let i = 0, l = typeof res.Metadata !== 'undefined' ? res.Metadata.length : 0; i < l; i++) { + try { // Skip any videos (movie or episode) without a duration set... if (typeof res.Metadata[i].duration === 'undefined' && (res.Metadata[i].type === "episode" || res.Metadata[i].type === "movie")) continue @@ -194,6 +195,11 @@ module.exports = function ($http, $window, $interval) { } } nested.push(program) + } catch(err) { + let msg = "Error when attempting to read nested data for " + key + " " + res.Metadata[i].title; + errors.push(msg); + console.error(msg , err); + } } if (includeCollections === true) { let nestedCollections = [];