Merge pull request #379 from vexorian/20210921_dev

20210921 dev
This commit is contained in:
vexorian 2021-09-21 08:57:17 -04:00 committed by GitHub
commit 31f7011c86
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 602 additions and 385 deletions

View File

@ -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.3-nvidia
FROM jrottenberg/ffmpeg:4.3-nvidia1804
EXPOSE 8000
WORKDIR /home/node/app
ENTRYPOINT [ "./dizquetv" ]

View File

@ -68,7 +68,6 @@ npm run dev-server
## Contribute
* Pull requests welcome but please read the [Code of Conduct](CODE_OF_CONDUCT.md) and the [Pull Request Template](pull_request_template.md) first.
* We use [Conventional Commits](https://www.conventionalcommits.org/), a specification for adding human and machine readable meaning to commit messages. Add files with `git add` and call `git commit` to use your command line utility and create a commit.
* Tip Jar: https://buymeacoffee.com/vexorian
## License

View File

@ -42,6 +42,12 @@ console.log(
'------------'
`);
const NODE = parseInt( process.version.match(/^[^0-9]*(\d+)\..*$/)[1] );
if (NODE < 12) {
console.error(`WARNING: Your nodejs version ${process.version} is lower than supported. dizqueTV has been tested best on nodejs 12.16.`);
}
for (let i = 0, l = process.argv.length; i < l; i++) {
if ((process.argv[i] === "-p" || process.argv[i] === "--port") && i + 1 !== l)

View File

@ -37,6 +37,7 @@ function api(db, channelService, fillerDB, customShowDB, xmltvInterval, guideSe
res.send( {
"dizquetv" : constants.VERSION_NAME,
"ffmpeg" : v,
"nodejs" : process.version,
} );
} catch(err) {
console.error(err);
@ -1037,7 +1038,6 @@ function api(db, channelService, fillerDB, customShowDB, xmltvInterval, guideSe
delete toolRes.programs;
let s = JSON.stringify(toolRes);
s = s.slice(0, -1);
console.log( JSON.stringify(toolRes));
res.writeHead(200, {
'Content-Type': 'application/json'

View File

@ -167,6 +167,10 @@ function recordPlayback(channelId, t0, lineupItem) {
}
}
function clearPlayback(channelId) {
delete cache[channelId];
}
function clear() {
//it's not necessary to clear the playback cache and it may be undesirable
configCache = {};
@ -184,4 +188,5 @@ module.exports = {
getChannelConfig: getChannelConfig,
saveChannelConfig: saveChannelConfig,
getFillerLastPlayTime: getFillerLastPlayTime,
clearPlayback: clearPlayback,
}

View File

@ -3,7 +3,7 @@ module.exports = {
TVGUIDE_MAXIMUM_PADDING_LENGTH_MS: 30*60*1000,
DEFAULT_GUIDE_STEALTH_DURATION: 5 * 60* 1000,
TVGUIDE_MAXIMUM_FLEX_DURATION : 6 * 60 * 60 * 1000,
TOO_FREQUENT: 100,
TOO_FREQUENT: 1000,
//when a channel is forcibly stopped due to an update, let's mark it as active
// for a while during the transaction just in case.

View File

@ -29,10 +29,8 @@ class ChannelDB {
}
async saveChannel(number, json) {
if (typeof(number) === 'undefined') {
throw Error("Mising channel number");
}
let f = path.join(this.folder, `${number}.json` );
await this.validateChannelJson(number, json);
let f = path.join(this.folder, `${json.number}.json` );
return await new Promise( (resolve, reject) => {
let data = undefined;
try {
@ -50,12 +48,30 @@ class ChannelDB {
}
saveChannelSync(number, json) {
json.number = number;
this.validateChannelJson(number, json);
let data = JSON.stringify(json);
let f = path.join(this.folder, `${number}.json` );
let f = path.join(this.folder, `${json.number}.json` );
fs.writeFileSync( f, data );
}
validateChannelJson(number, json) {
json.number = number;
if (typeof(json.number) === 'undefined') {
throw Error("Expected a channel.number");
}
if (typeof(json.number) === 'string') {
try {
json.number = parseInt(json.number);
} catch (err) {
console.error("Error parsing channel number.", err);
}
}
if ( isNaN(json.number)) {
throw Error("channel.number must be a integer");
}
}
async deleteChannel(number) {
let f = path.join(this.folder, `${number}.json` );
await new Promise( (resolve, reject) => {

View File

@ -135,7 +135,7 @@ class PlexServerDB
s = s[0];
let arGuide = server.arGuide;
if (typeof(arGuide) === 'undefined') {
arGuide = true;
arGuide = false;
}
let arChannels = server.arChannels;
if (typeof(arChannels) === 'undefined') {
@ -177,7 +177,7 @@ class PlexServerDB
name = resultName;
let arGuide = server.arGuide;
if (typeof(arGuide) === 'undefined') {
arGuide = true;
arGuide = false;
}
let arChannels = server.arGuide;
if (typeof(arChannels) === 'undefined') {

View File

@ -485,7 +485,7 @@ function splitServersSingleChannels(db, channelDB ) {
let saveServer = (name, uri, accessToken, arGuide, arChannels) => {
if (typeof(arGuide) === 'undefined') {
arGuide = true;
arGuide = false;
}
if (typeof(arChannels) === 'undefined') {
arChannels = false;

View File

@ -29,7 +29,6 @@ function getCurrentProgramAndTimeElapsed(date, channel) {
if (channelStartTime > date) {
let t0 = date;
let t1 = channelStartTime;
console.log(t0, t1);
console.log("Channel start time is above the given date. Flex time is picked till that.");
return {
program: {
@ -185,10 +184,11 @@ function pickRandomWithMaxDuration(channel, fillers, maxDuration) {
list = list.concat(fillers[i].content);
}
let pick1 = null;
let pick2 = null;
let t0 = (new Date()).getTime();
let minimumWait = 1000000000;
const D = 7*24*60*60*1000;
const E = 5*60*60*1000;
if (typeof(channel.fillerRepeatCooldown) === 'undefined') {
channel.fillerRepeatCooldown = 30*60*1000;
}
@ -198,7 +198,7 @@ function pickRandomWithMaxDuration(channel, fillers, maxDuration) {
list = fillers[j].content;
let pickedList = false;
let n = 0;
let m = 0;
for (let i = 0; i < list.length; i++) {
let clip = list[i];
// a few extra milliseconds won't hurt anyone, would it? dun dun dun
@ -206,7 +206,6 @@ function pickRandomWithMaxDuration(channel, fillers, maxDuration) {
let t1 = channelCache.getProgramLastPlayTime( channel.number, clip );
let timeSince = ( (t1 == 0) ? D : (t0 - t1) );
if (timeSince < channel.fillerRepeatCooldown - SLACK) {
let w = channel.fillerRepeatCooldown - timeSince;
if (clip.duration + w <= maxDuration + SLACK) {
@ -223,6 +222,7 @@ function pickRandomWithMaxDuration(channel, fillers, maxDuration) {
if ( weighedPick(fillers[j].weight, listM) ) {
pickedList = true;
fillerId = fillers[j].id;
n = 0;
} else {
break;
}
@ -235,29 +235,20 @@ function pickRandomWithMaxDuration(channel, fillers, maxDuration) {
break;
}
}
if (timeSince >= D) {
let p = 200, q = Math.max( maxDuration - clip.duration, 1 );
let pq = Math.min( Math.ceil(p / q), 10 );
let w = pq;
n += w;
if ( weighedPick(w, n) ) {
pick1 = clip;
}
} else {
let adjust = Math.floor(timeSince / (60*1000));
if (adjust > 0) {
adjust = adjust * adjust;
//weighted
m += adjust;
if ( weighedPick(adjust, m) ) {
pick2 = clip;
}
}
if (timeSince <= 0) {
continue;
}
let s = norm_s( (timeSince >= E) ? E : timeSince );
let d = norm_d( clip.duration);
let w = s + d;
n += w;
if (weighedPick(w,n)) {
pick1 = clip;
}
}
}
}
let pick = (pick1 == null) ? pick2: pick1;
let pick = pick1;
let pickTitle = "null";
if (pick != null) {
pickTitle = pick.title;
@ -272,6 +263,22 @@ function pickRandomWithMaxDuration(channel, fillers, maxDuration) {
}
}
function norm_d(x) {
x /= 60 * 1000;
if (x >= 3.0) {
x = 3.0 + Math.log(x);
}
let y = 10000 * ( Math.ceil(x * 1000) + 1 );
return Math.ceil(y / 1000000) + 1;
}
function norm_s(x) {
let y = Math.ceil(x / 600) + 1;
y = y*y;
return Math.ceil(y / 1000000) + 1;
}
// any channel thing used here should be added to channel context
function getWatermark( ffmpegSettings, channel, type) {
if (! ffmpegSettings.enableFFMPEGTranscoding || ffmpegSettings.disableChannelOverlay ) {
@ -319,7 +326,10 @@ function generateChannelContext(channel) {
let channelContext = {};
for (let i = 0; i < CHANNEL_CONTEXT_KEYS.length; i++) {
let key = CHANNEL_CONTEXT_KEYS[i];
channelContext[key] = JSON.parse( JSON.stringify(channel[key] ) );
if (typeof(channel[key]) !== 'undefined') {
channelContext[key] = JSON.parse( JSON.stringify(channel[key] ) );
}
}
return channelContext;
}

View File

@ -12,6 +12,7 @@ module.exports = function () {
showId : "custom." + program.customShowId,
showDisplayName : program.customShowName,
order : program.customOrder,
shuffleOrder : program.shuffleOrder,
}
} else if (program.isOffline && program.type === 'redirect') {
return {
@ -35,6 +36,7 @@ module.exports = function () {
showId : "movie.",
showDisplayName : "Movies",
order : movieTitleOrder[key],
shuffleOrder : program.shuffleOrder,
}
} else if ( (program.type === 'episode') || (program.type === 'track') ) {
let s = 0;
@ -54,6 +56,7 @@ module.exports = function () {
showId : prefix + program.showTitle,
showDisplayName : program.showTitle,
order : s * 1000000 + e,
shuffleOrder : program.shuffleOrder,
}
} else {
return {

View File

@ -43,7 +43,7 @@ class M3uService {
channels.sort((a, b) => {
return a.number < b.number ? -1 : 1
return parseInt(a.number) < parseInt(b.number) ? -1 : 1
});
const tvg = `{{host}}/api/xmltv.xml`;

View File

@ -197,7 +197,9 @@ class OnDemandService
} else {
let o = (tm - pm);
startTime = startTime - o;
if (o >= SLACK) {
//It looks like it is convenient to make the on-demand a bit more lenient SLACK-wise tha
//other parts of the schedule process. So SLACK*2 instead of just SLACK
if (o >= SLACK*2) {
startTime += onDemand.modulo;
}
}

View File

@ -2,7 +2,7 @@ const constants = require("../constants");
const getShowData = require("./get-show-data")();
const random = require('../helperFuncs').random;
const throttle = require('./throttle');
const orderers = require("./show-orderers");
const MINUTE = 60*1000;
const DAY = 24*60*MINUTE;
@ -22,29 +22,6 @@ function getShow(program) {
}
}
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') {
@ -69,78 +46,6 @@ function addProgramToShow(show, program) {
}
}
function getShowOrderer(show) {
if (typeof(show.orderer) === 'undefined') {
let sortedPrograms = JSON.parse( JSON.stringify(show.programs) );
sortedPrograms.sort((a, b) => {
let showA = getShowData(a);
let showB = getShowData(b);
return showA.order - showB.order;
});
let position = 0;
while (
(position + 1 < sortedPrograms.length )
&&
(
getShowData(show.founder).order
!==
getShowData(sortedPrograms[position]).order
)
) {
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' };
@ -192,9 +97,6 @@ module.exports = async( programs, schedule ) => {
}
let flexBetween = ( schedule.flexPreference !== "end" );
// throttle so that the stream is not affected negatively
let steps = 0;
let showsById = {};
let shows = [];
@ -216,9 +118,9 @@ module.exports = async( programs, schedule ) => {
channel: show.channel,
}
} else if (slot.order === 'shuffle') {
return getShowShuffler(show).current();
return orderers.getShowShuffler(show).current();
} else if (slot.order === 'next') {
return getShowOrderer(show).current();
return orderers.getShowOrderer(show).current();
}
}
@ -228,9 +130,9 @@ module.exports = async( programs, schedule ) => {
}
let show = shows[ showsById[slot.showId] ];
if (slot.order === 'shuffle') {
return getShowShuffler(show).next();
return orderers.getShowShuffler(show).next();
} else if (slot.order === 'next') {
return getShowOrderer(show).next();
return orderers.getShowOrderer(show).next();
}
}

View File

@ -0,0 +1,156 @@
const random = require('../helperFuncs').random;
const getShowData = require("./get-show-data")();
const randomJS = require("random-js");
const Random = randomJS.Random;
/****
*
* Code shared by random slots and time slots for keeping track of the order
* of episodes
*
**/
function shuffle(array, lo, hi, randomOverride ) {
let r = randomOverride;
if (typeof(r) === 'undefined') {
r = random;
}
if (typeof(lo) === 'undefined') {
lo = 0;
hi = array.length;
}
let currentIndex = hi, temporaryValue, randomIndex
while (lo !== currentIndex) {
randomIndex = r.integer(lo, currentIndex-1);
currentIndex -= 1
temporaryValue = array[currentIndex]
array[currentIndex] = array[randomIndex]
array[randomIndex] = temporaryValue
}
return array
}
function getShowOrderer(show) {
if (typeof(show.orderer) === 'undefined') {
let sortedPrograms = JSON.parse( JSON.stringify(show.programs) );
sortedPrograms.sort((a, b) => {
let showA = getShowData(a);
let showB = getShowData(b);
return showA.order - showB.order;
});
let position = 0;
while (
(position + 1 < sortedPrograms.length )
&&
(
getShowData(show.founder).order
!==
getShowData(sortedPrograms[position]).order
)
) {
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 sortedPrograms = JSON.parse( JSON.stringify(show.programs) );
sortedPrograms.sort((a, b) => {
let showA = getShowData(a);
let showB = getShowData(b);
return showA.order - showB.order;
});
let n = sortedPrograms.length;
let splitPrograms = [];
let randomPrograms = [];
for (let i = 0; i < n; i++) {
splitPrograms.push( sortedPrograms[i] );
randomPrograms.push( {} );
}
let showId = getShowData(show.programs[0]).showId;
let position = show.founder.shuffleOrder;
if (typeof(position) === 'undefined') {
position = 0;
}
let localRandom = null;
let initGeneration = (generation) => {
let seed = [];
for (let i = 0 ; i < show.showId.length; i++) {
seed.push( showId.charCodeAt(i) );
}
seed.push(generation);
localRandom = new Random( randomJS.MersenneTwister19937.seedWithArray(seed) )
if (generation == 0) {
shuffle( splitPrograms, 0, n , localRandom );
}
for (let i = 0; i < n; i++) {
randomPrograms[i] = splitPrograms[i];
}
let a = Math.floor(n / 2);
shuffle( randomPrograms, 0, a, localRandom );
shuffle( randomPrograms, a, n, localRandom );
};
initGeneration(0);
let generation = Math.floor( position / n );
initGeneration( generation );
show.shuffler = {
current : () => {
let prog = JSON.parse(
JSON.stringify(randomPrograms[position % n] )
);
prog.shuffleOrder = position;
return prog;
},
next: () => {
position++;
if (position % n == 0) {
let generation = Math.floor( position / n );
initGeneration( generation );
}
},
}
}
return show.shuffler;
}
module.exports = {
getShowOrderer : getShowOrderer,
getShowShuffler: getShowShuffler,
}

View File

@ -4,6 +4,7 @@ const constants = require("../constants");
const getShowData = require("./get-show-data")();
const random = require('../helperFuncs').random;
const throttle = require('./throttle');
const orderers = require("./show-orderers");
const MINUTE = 60*1000;
const DAY = 24*60*MINUTE;
@ -22,28 +23,6 @@ function getShow(program) {
}
}
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') {
@ -68,78 +47,6 @@ function addProgramToShow(show, program) {
}
}
function getShowOrderer(show) {
if (typeof(show.orderer) === 'undefined') {
let sortedPrograms = JSON.parse( JSON.stringify(show.programs) );
sortedPrograms.sort((a, b) => {
let showA = getShowData(a);
let showB = getShowData(b);
return showA.order - showB.order;
});
let position = 0;
while (
(position + 1 < sortedPrograms.length )
&&
(
getShowData(show.founder).order
!==
getShowData(sortedPrograms[position]).order
)
) {
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' };
@ -224,9 +131,9 @@ module.exports = async( programs, schedule ) => {
channel: show.channel,
}
} else if (slot.order === 'shuffle') {
return getShowShuffler(show).current();
return orderers.getShowShuffler(show).current();
} else if (slot.order === 'next') {
return getShowOrderer(show).current();
return orderers.getShowOrderer(show).current();
}
}
@ -236,9 +143,9 @@ module.exports = async( programs, schedule ) => {
}
let show = shows[ showsById[slot.showId] ];
if (slot.order === 'shuffle') {
return getShowShuffler(show).next();
return orderers.getShowShuffler(show).next();
} else if (slot.order === 'next') {
return getShowOrderer(show).next();
return orderers.getShowOrderer(show).next();
}
}

View File

@ -36,7 +36,15 @@ class TVGuideService extends events.EventEmitter
let t = (new Date()).getTime();
this.updateTime = t;
this.updateLimit = t + limit;
let channels = inputChannels;
let channels = [];
for (let i = 0; i < inputChannels.length; i++) {
if (typeof(inputChannels[i]) !== 'undefined') {
channels.push(inputChannels[i]);
} else {
console.error(`There is an issue with one of the channels provided to TV-guide service, it will be ignored: ${i}` );
}
}
this.updateChannels = channels;
return t;
}

View File

@ -13,29 +13,38 @@ function equalItems(a, b) {
function wereThereTooManyAttempts(sessionId, lineupItem) {
let obj = cache[sessionId];
let t1 = (new Date()).getTime();
if (typeof(obj) === 'undefined') {
let previous = cache[sessionId];
if (typeof(previous) === 'undefined') {
previous = cache[sessionId] = {
t0: t1 - constants.TOO_FREQUENT * 5
t0: t1 - constants.TOO_FREQUENT * 5,
lineupItem: null,
};
} else {
clearTimeout(obj.timer);
}
previous.timer = setTimeout( () => {
cache[sessionId].timer = null;
delete cache[sessionId];
}, constants.TOO_FREQUENT*5 );
let result = false;
if (previous.t0 + constants.TOO_FREQUENT >= t1) {
if (t1 - previous.t0 < constants.TOO_FREQUENT) {
//certainly too frequent
result = equalItems( previous.lineupItem, lineupItem );
}
cache[sessionId].t0 = t1;
cache[sessionId].lineupItem = lineupItem;
cache[sessionId] = {
t0: t1,
lineupItem : lineupItem,
};
setTimeout( () => {
if (
(typeof(cache[sessionId]) !== 'undefined')
&&
(cache[sessionId].t0 === t1)
) {
delete cache[sessionId];
}
}, constants.TOO_FREQUENT * 5 );
return result;
}

View File

@ -2,11 +2,11 @@ const express = require('express')
const helperFuncs = require('./helperFuncs')
const FFMPEG = require('./ffmpeg')
const FFMPEG_TEXT = require('./ffmpegText')
const constants = require('./constants')
const fs = require('fs')
const ProgramPlayer = require('./program-player');
const channelCache = require('./channel-cache')
const wereThereTooManyAttempts = require('./throttler');
const constants = require('./constants');
module.exports = { router: video, shutdown: shutdown }
@ -131,7 +131,7 @@ function video( channelService, fillerDB, db, programmingService, activeChannelS
} );
// Stream individual video to ffmpeg concat above. This is used by the server, NOT the client
router.get('/stream', async (req, res) => {
let streamFunction = async (req, res, t0, allowSkip) => {
if (stopPlayback) {
res.status(503).send("Server is shutting down.")
return;
@ -180,7 +180,6 @@ function video( channelService, fillerDB, db, programmingService, activeChannelS
// Get video lineup (array of video urls with calculated start times and durations.)
let t0 = (new Date()).getTime();
let lineupItem = channelCache.getCurrentLineupItem( channel.number, t0);
let prog = null;
let brandChannel = channel;
@ -261,12 +260,15 @@ function video( channelService, fillerDB, db, programmingService, activeChannelS
duration: t,
isOffline : true,
};
} else if (prog.program.isOffline && prog.program.duration - prog.timeElapsed <= 10000) {
} else if ( allowSkip && (prog.program.isOffline && prog.program.duration - prog.timeElapsed <= constants.SLACK + 1) ) {
//it's pointless to show the offline screen for such a short time, might as well
//skip to the next program
prog.programIndex = (prog.programIndex + 1) % channel.programs.length;
prog.program = channel.programs[prog.programIndex ];
prog.timeElapsed = 0;
let dt = prog.program.duration - prog.timeElapsed;
for (let i = 0; i < redirectChannels.length; i++) {
channelCache.clearPlayback(redirectChannels[i].number );
}
console.log("Too litlle time before the filler ends, skip to next slot");
return await streamFunction(req, res, t0 + dt + 1, false);
}
if ( (prog == null) || (typeof(prog) === 'undefined') || (prog.program == null) || (typeof(prog.program) == "undefined") ) {
throw "No video to play, this means there's a serious unexpected bug or the channel db is corrupted."
@ -319,6 +321,7 @@ function video( channelService, fillerDB, db, programmingService, activeChannelS
channelCache.recordPlayback(channel.number, t0, lineupItem);
}
if (wereThereTooManyAttempts(session, lineupItem)) {
console.error("There are too many attempts to play the same item in a short period of time, playing the error stream instead.");
lineupItem = {
isOffline: true,
err: Error("Too many attempts, throttling.."),
@ -443,6 +446,11 @@ function video( channelService, fillerDB, db, programmingService, activeChannelS
console.log("Client Closed");
stop();
});
};
router.get('/stream', async (req, res) => {
let t0 = (new Date).getTime();
return await streamFunction(req, res, t0, true);
});

View File

@ -51,7 +51,7 @@ function writePromise(json, xmlSettings, throttle, cacheImageService) {
function _writeDocStart(xw) {
xw.startDocument()
xw.startElement('tv')
xw.writeAttribute('generator-info-name', 'psuedotv-plex')
xw.writeAttribute('generator-info-name', 'dizquetv')
}
function _writeDocEnd(xw, ws) {
xw.endElement()

View File

@ -4,6 +4,7 @@ module.exports = function ($scope, dizquetv) {
dizquetv.getVersion().then((version) => {
$scope.version = version.dizquetv;
$scope.ffmpegVersion = version.ffmpeg;
$scope.nodejs = version.nodejs;
})

View File

@ -989,7 +989,7 @@ module.exports = function ($timeout, $location, dizquetv, resolutionOptions, get
scope.error.any = true;
if (typeof channel.number === "undefined" || channel.number === null || channel.number === "") {
if (typeof channel.number === "undefined" || channel.number === null || channel.number === "" ) {
scope.error.number = "Select a channel number"
scope.error.tab = "basic";
} else if (channelNumbers.indexOf(parseInt(channel.number, 10)) !== -1 && scope.isNewChannel) { // we need the parseInt for indexOf to work properly
@ -998,6 +998,9 @@ module.exports = function ($timeout, $location, dizquetv, resolutionOptions, get
} else if (!scope.isNewChannel && channel.number !== scope.beforeEditChannelNumber && channelNumbers.indexOf(parseInt(channel.number, 10)) !== -1) {
scope.error.number = "Channel number already in use."
scope.error.tab = "basic";
} else if ( ! checkChannelNumber(channel.number) ) {
scope.error.number = "Invalid channel number.";
scope.error.tab = "basic";
} else if (channel.number < 0 || channel.number > 9999) {
scope.error.name = "Enter a valid number (0-9999)"
scope.error.tab = "basic";
@ -1462,6 +1465,10 @@ module.exports = function ($timeout, $location, dizquetv, resolutionOptions, get
scope.videoRateDefault = "(Use global setting)";
scope.videoBufSizeDefault = "(Use global setting)";
scope.randomizeBlockShuffle = false;
scope.advancedTools = (localStorage.getItem("channel-programming-advanced-tools" ) === "show");
let refreshScreenResolution = async () => {
@ -1650,13 +1657,21 @@ module.exports = function ($timeout, $location, dizquetv, resolutionOptions, get
scope.onTimeSlotsDone = (slotsResult) => {
scope.channel.scheduleBackup = slotsResult.schedule;
readSlotsResult(slotsResult);
if (slotsResult === null) {
delete scope.channel.scheduleBackup;
} else {
scope.channel.scheduleBackup = slotsResult.schedule;
readSlotsResult(slotsResult);
}
}
scope.onRandomSlotsDone = (slotsResult) => {
scope.channel.randomScheduleBackup = slotsResult.schedule;
readSlotsResult(slotsResult);
if (slotsResult === null) {
delete scope.channel.randomScheduleBackup;
} else {
scope.channel.randomScheduleBackup = slotsResult.schedule;
readSlotsResult(slotsResult);
}
}
@ -1669,6 +1684,73 @@ module.exports = function ($timeout, $location, dizquetv, resolutionOptions, get
scope.randomSlots.startDialog(progs, scope.maxSize, scope.channel.randomScheduleBackup );
}
scope.rerollRandomSlots = () => {
let progs = commonProgramTools.removeDuplicates( scope.channel.programs );
scope.randomSlots.startDialog(
progs, scope.maxSize, scope.channel.randomScheduleBackup,
true
);
}
scope.hasNoRandomSlots = () => {
return (
(typeof(scope.channel.randomScheduleBackup) === 'undefined' )
||
(scope.channel.randomScheduleBackup == null)
);
}
scope.rerollTimeSlots = () => {
let progs = commonProgramTools.removeDuplicates( scope.channel.programs );
scope.timeSlots.startDialog(
progs, scope.maxSize, scope.channel.scheduleBackup,
true
);
}
scope.hasNoTimeSlots = () => {
return (
(typeof(scope.channel.scheduleBackup) === 'undefined' )
||
(scope.channel.scheduleBackup == null)
);
}
scope.toggleAdvanced = () => {
scope.advancedTools = ! scope.advancedTools;
localStorage.setItem("channel-programming-advanced-tools" , scope.advancedTools ? "show" : "hide");
}
scope.hasAdvancedTools = () => {
return scope.advancedTools;
}
scope.toolWide = () => {
if ( scope.hasAdvancedTools()) {
return {
"col-xl-6": true,
"col-md-12" : true
}
} else {
return {
"col-xl-12": true,
"col-lg-12" : true
}
}
}
scope.toolThin = () => {
if ( scope.hasAdvancedTools()) {
return {
"col-xl-3": true,
"col-lg-6" : true
}
} else {
return {
"col-xl-6": true,
"col-lg-6" : true
}
}
}
scope.logoOnChange = (event) => {
const formData = new FormData();
formData.append('image', event.target.files[0]);
@ -1706,3 +1788,12 @@ module.exports = function ($timeout, $location, dizquetv, resolutionOptions, get
function validURL(url) {
return /^(ftp|http|https):\/\/[^ "]+$/.test(url);
}
function checkChannelNumber(number) {
if ( /^(([1-9][0-9]*)|(0))$/.test(number) ) {
let x = parseInt(number);
return (0 <= x && x < 10000);
} else {
return false;
}
}

View File

@ -123,8 +123,19 @@ module.exports = function (plex, dizquetv, $timeout, commonProgramTools) {
}
scope.fillNestedIfNecessary = async (x, isLibrary) => {
if ( (typeof(x.nested) === 'undefined') && (x.type !== 'collection') ) {
if (typeof(x.nested) === 'undefined') {
x.nested = await plex.getNested(scope.plexServer, x, isLibrary, scope.errors);
if (x.type === "collection" && x.collectionType === "show") {
let nested = x.nested;
x.nested = [];
for (let i = 0; i < nested.length; i++) {
let subNested = await plex.getNested(scope.plexServer, nested[i], false, scope.errors);
for (let j = 0; j < subNested.length; j++) {
subNested[j].title = nested[i].title + " - " + subNested[j].title;
x.nested.push( subNested[j] );
}
}
}
}
}
scope.getNested = (list, isLibrary) => {

View File

@ -192,7 +192,7 @@ module.exports = function (plex, dizquetv, $timeout) {
accessToken: server.accessToken,
}
}
connection.arGuide = true
connection.arGuide = false
connection.arChannels = false // should not be enabled unless dizqueTV tuner already added to plex
await dizquetv.addPlexServer(connection);
} catch (err) {

View File

@ -177,8 +177,22 @@ module.exports = function ($timeout, dizquetv, getShowData) {
{ id: "shuffle", description: "Shuffle" },
];
let doIt = async() => {
let doWait = (millis) => {
return new Promise( (resolve) => {
$timeout( resolve, millis );
} );
}
let doIt = async(fromInstant) => {
let t0 = new Date().getTime();
let res = await dizquetv.calculateRandomSlots(scope.programs, scope.schedule );
let t1 = new Date().getTime();
let w = Math.max(0, 250 - (t1 - t0) );
if (fromInstant && (w > 0) ) {
await doWait(w);
}
for (let i = 0; i < scope.schedule.slots.length; i++) {
delete scope.schedule.slots[i].weightPercentage;
}
@ -189,7 +203,7 @@ module.exports = function ($timeout, dizquetv, getShowData) {
let startDialog = (programs, limit, backup) => {
let startDialog = (programs, limit, backup, instant) => {
scope.limit = limit;
scope.programs = programs;
@ -213,11 +227,15 @@ module.exports = function ($timeout, dizquetv, getShowData) {
id: "flex.",
description: "Flex",
} );
if (typeof(backup) !== 'undefined') {
scope.hadBackup = (typeof(backup) !== 'undefined');
if (scope.hadBackup) {
loadBackup(backup);
}
scope.visible = true;
if (instant) {
scope.finished(false, true);
}
}
@ -225,13 +243,18 @@ module.exports = function ($timeout, dizquetv, getShowData) {
startDialog: startDialog,
} );
scope.finished = async (cancel) => {
scope.finished = async (cancel, fromInstant) => {
scope.error = null;
if (!cancel) {
if ( scope.schedule.slots.length === 0) {
scope.onDone(null);
scope.visible = false;
return;
}
try {
scope.loading = true;
$timeout();
scope.onDone( await doIt() );
scope.onDone( await doIt(fromInstant) );
scope.visible = false;
} catch(err) {
console.error("Unable to generate channel lineup", err);
@ -267,6 +290,20 @@ module.exports = function ($timeout, dizquetv, getShowData) {
return false;
}
scope.hideCreateLineup = () => {
return (
scope.disableCreateLineup()
&& (scope.schedule.slots.length == 0)
&& scope.hadBackup
);
}
scope.showResetSlots = () => {
return scope.hideCreateLineup();
}
scope.canShowSlot = (slot) => {
return (slot.showId != 'flex.') && !(slot.showId.startsWith('redirect.'));
}

View File

@ -203,9 +203,23 @@ module.exports = function ($timeout, dizquetv, getShowData ) {
{ id: "shuffle", description: "Shuffle" },
];
let doIt = async() => {
let doWait = (millis) => {
return new Promise( (resolve) => {
$timeout( resolve, millis );
} );
}
let doIt = async(fromInstant) => {
scope.schedule.timeZoneOffset = (new Date()).getTimezoneOffset();
let t0 = new Date().getTime();
let res = await dizquetv.calculateTimeSlots(scope.programs, scope.schedule );
let t1 = new Date().getTime();
let w = Math.max(0, 250 - (t1 - t0) );
if (fromInstant && (w > 0) ) {
await doWait(w);
}
res.schedule = scope.schedule;
delete res.schedule.fake;
return res;
@ -214,7 +228,7 @@ module.exports = function ($timeout, dizquetv, getShowData ) {
let startDialog = (programs, limit, backup) => {
let startDialog = (programs, limit, backup, instant) => {
scope.limit = limit;
scope.programs = programs;
@ -238,11 +252,15 @@ module.exports = function ($timeout, dizquetv, getShowData ) {
id: "flex.",
description: "Flex",
} );
if (typeof(backup) !== 'undefined') {
scope.hadBackup = (typeof(backup) !== 'undefined');
if (scope.hadBackup) {
loadBackup(backup);
}
scope.visible = true;
if (instant) {
scope.finished(false, true);
}
}
@ -250,13 +268,19 @@ module.exports = function ($timeout, dizquetv, getShowData ) {
startDialog: startDialog,
} );
scope.finished = async (cancel) => {
scope.finished = async (cancel, fromInstant) => {
scope.error = null;
if (!cancel) {
if ( scope.schedule.slots.length === 0) {
scope.onDone(null);
scope.visible = false;
return;
}
try {
scope.loading = true;
$timeout();
scope.onDone( await doIt() );
scope.onDone( await doIt(fromInstant) );
scope.visible = false;
} catch(err) {
console.error("Unable to generate channel lineup", err);
@ -292,6 +316,18 @@ module.exports = function ($timeout, dizquetv, getShowData ) {
return false;
}
scope.hideCreateLineup = () => {
return (
scope.disableCreateLineup()
&& (scope.schedule.slots.length == 0)
&& scope.hadBackup
);
}
scope.showResetSlots = () => {
return scope.hideCreateLineup();
}
scope.canShowSlot = (slot) => {
return (slot.showId != 'flex.') && !(slot.showId.startsWith('redirect.'));
}

View File

@ -197,23 +197,29 @@
<small class='text-info'>There are no programs in the channel, use the <i class='fas fa-plus'></i> button to add programs from your media library or use the Tools to add Flex time or a Channel Redirect</small>
</div>
<div class='col-md-4 col-sm-12 col-xl-6 col-lg-5 programming-pane tools-pane' ng-show="showShuffleOptions"
<div ng-class='{
"col-md-4" : true,
"col-sm-12" : true,
"col-xl-6" : hasAdvancedTools(),
"col-lg-5" : hasAdvancedTools(),
"col-xl-4" : !hasAdvancedTools(),
"col-lg-3" : !hasAdvancedTools()
}' class='programming-pane tools-pane' ng-show="showShuffleOptions"
ng-style="{'max-height':programmingHeight()}"
>
<div class="row">
<div class="col-xl-6 col-md-12" style="padding: 5px;" ng-show="hasPrograms()">
<div ng-class="toolWide()" style="padding: 5px;" ng-show="hasPrograms() && hasAdvancedTools()">
<div class="input-group">
<div class="input-group-prepend">
<input type="number" class="form-control form-control-sm" placeholder="Desired number of consecutive TV shows." min="1" max="10" ng-model="blockCount" style="width:5em">
</input>
</div>
<div class="input-group-prepend">
<div class="input-group-text" style="padding: 0;">
<label class="small" for="randomizeBlockShuffle" style="margin-bottom: 2px;">&nbsp;Randomize&nbsp;&nbsp;</label>
<input id="randomizeBlockShuffle" type="checkbox" ng-model="randomizeBlockShuffle"></input>
&nbsp;
</div>
<select class="custom-select" ng-model="randomizeBlockShuffle">
<option ng-value="false" label="Fixed" />
<option ng-value="true" label="Random" />
</select>
</div>
<button class="btn btn-sm btn-warning form-control form-control-sm" type="button" ng-click="blockShuffle(blockCount, randomizeBlockShuffle)">
<i class='fa fa-random' title='Block Shuffle' ></i> Block Shuffle
@ -223,7 +229,7 @@
</div>
<div class="col-xl-3 col-lg-6" style="padding: 5px;" ng-show="hasPrograms()">
<div ng-class="toolThin()" style="padding: 5px;" ng-show="hasPrograms()">
<div class="input-group">
<button class="btn btn-sm btn-warning form-control form-control-sm" type="button" ng-click="randomShuffle()" aria-describedby="randomShuffleHelp" title='Random Shuffle'>
<i class='fa fa-random'></i> Random Shuffle
@ -234,7 +240,7 @@
</p>
</div>
<div class='col-xl-3 col-lg-6' style="padding: 5px;" ng-show="hasPrograms()" >
<div ng-class="toolThin()" style="padding: 5px;" ng-show="hasPrograms()" >
<div class="input-group">
<button class="btn btn-sm btn-warning form-control form-control-sm" type="button" ng-click="cyclicShuffle()" title='Cyclic Shuffle' >
<i class='fa fa-random'></i> Cyclic Shuffle
@ -243,7 +249,7 @@
<p ng-show='showHelp.check'>Like Random Shuffle, but tries to preserve the sequence of episodes for each TV show. If a TV show has multiple instances of its episodes, they are also cycled appropriately.</p>
</div>
<div class="col-xl-6 col-lg-12" style="padding: 5px;" ng-show="hasPrograms()">
<div ng-class="toolWide()" style="padding: 5px;" ng-show="hasPrograms() && hasAdvancedTools()">
<div class="input-group">
<div class="input-group-prepend">
<input type="number" class="form-control form-control-sm" placeholder="Repeats" min="1" max="{{maxReplicas()}}" ng-model="replicaCount" style="width:5em">
@ -255,7 +261,7 @@
<p ng-show='showHelp.check'>Makes multiple copies of the schedule and plays them in sequence. Normally this isn&apos;t necessary, because dizqueTV will always play the schedule back from the beginning when it finishes. But creating replicas is a useful intermediary step sometimes before applying other transformations. Note that because very large channels can be problematic, the number of replicas will be limited to avoid creating really large channels.</p>
</div>
<div class="col-xl-6 col-lg-12" style="padding: 5px;" ng-show="hasPrograms()">
<div ng-class="toolWide()" style="padding: 5px;" ng-show="hasPrograms() && hasAdvancedTools()">
<div class="input-group">
<div class="input-group-prepend">
<input type="number" class="form-control form-control-sm" placeholder="Repeats" min="1" max="{{maxReplicas()}}" ng-model="randomReplicaCount" style="width:5em">
@ -267,7 +273,7 @@
<p ng-show='showHelp.check'>Like &quot;Replicate&quot;, it will make multiple copies of the programming. In addition it will shuffle the programs, but it will make sure not to have too small a distance between two identical programs.</p>
</div>
<div class="col-xl-3 col-lg-6" style="padding: 5px;" ng-show="hasPrograms()" >
<div ng-class="toolThin()" style="padding: 5px;" ng-show="hasPrograms()" >
<div class='input-group'>
<button class="btn btn-sm btn-warning form-control form-control-sm" type="button" ng-click="sortShows()" title='Sort TV Shows' >
<i class='fa fa-sort-alpha-down'></i> Sort TV Shows
@ -278,7 +284,7 @@
</p>
</div>
<div class="col-xl-3 col-lg-6" style="padding: 5px;" ng-show="hasPrograms()" >
<div ng-class="toolThin()" style="padding: 5px;" ng-show="hasPrograms()" >
<div class="input-group">
<button class="btn btn-sm btn-warning form-control form-control-sm" type="button" ng-click="sortByDate()" title='Sort Release Dates' >
<i class='fa fa-sort-numeric-down'></i> Sort Release Dates
@ -287,7 +293,7 @@
<p ng-show='showHelp.check'>Sorts everything by its release date. This will only work correctly if the release dates in Plex are correct. In case any item does not have a release date specified, it will be moved to the bottom.</p>
</div>
<div class="col-xl-3 col-lg-6" style="padding: 5px;" ng-show="hasPrograms()" >
<div class="col-xl-3 col-lg-6" style="padding: 5px;" ng-show="hasPrograms() && hasAdvancedTools()" >
<div class="input-group">
<button class="btn btn-sm btn-warning form-control form-control-sm" type="button" ng-click="equalizeShows()" title='Balance Shows' >
<i class='fa fa-balance-scale'></i> Balance Shows
@ -296,7 +302,7 @@
<p ng-show='showHelp.check'>Will replicate some TV shows or delete duplicates of other TV shows in an effort to make it so the total durations of all episodes of each episode are as similar as possible. It&apos;s usually impossible to make the shows perfectly balanced without creating a really high number of duplicates, but it will try to get close. Movies are treated as a single show.</p>
</div>
<div class="col-xl-3 col-lg-6" style="padding: 5px;" ng-show="hasPrograms()" >
<div class="col-xl-3 col-lg-6" style="padding: 5px;" ng-show="hasPrograms() && hasAdvancedTools()" >
<div class='input-group'>
<button class="btn btn-sm btn-warning form-control form-control-sm" type="button" ng-click="startFrequencyTweak()" title='Tweak Weights...'>
<i class='fa fa-balance-scale'></i> Tweak Weights...
@ -314,7 +320,7 @@
<p ng-show='showHelp.check'>Programs a Flex time slot. Normally you&apos;d use pad times, restrict times or add breaks to add a large quantity of Flex times at once, but this exists for more specific cases.</p>
</div>
<div class="col-xl-6 col-lg-12" style="padding: 5px;" ng-show="hasPrograms()">
<div class="col-xl-6 col-lg-12" style="padding: 5px;" ng-show="hasPrograms() && hasAdvancedTools()">
<div class="input-group">
<div class="input-group-prepend">
<select class="custom-select" ng-model="nightStart"
@ -330,7 +336,7 @@
<p ng-show='showHelp.check'>The channel's regular programming between the specified hours. Flex time will fill up the remaining hours.</p>
</div>
<div class="col-xl-6 col-lg-12" style="padding: 5px;" ng-show="hasPrograms()">
<div class="col-xl-6 col-lg-12" style="padding: 5px;" ng-show="hasPrograms() && hasAdvancedTools()">
<div class="input-group">
<div class="input-group-prepend">
<select class="custom-select" ng-model="paddingOption"
@ -344,7 +350,7 @@
<p ng-show='showHelp.check'>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. This button might be disabled if the channel is already too large.</p>
</div>
<div class="col-xl-6 col-lg-12" style="padding: 5px;" ng-show="hasPrograms()">
<div class="col-xl-6 col-lg-12" style="padding: 5px;" ng-show="hasPrograms() && hasAdvancedTools()">
<div class="input-group">
<div class="input-group-prepend">
<select class="custom-select" style="width:5em" ng-model="breakAfter"
@ -361,7 +367,7 @@
<p ng-show='showHelp.check'>Adds Flex breaks between programs, attempting to avoid groups of consecutive programs that exceed the specified number of minutes. This button might be disabled if the channel is already too large.</p>
</div>
<div class="col-xl-6 col-lg-12" style="padding: 5px;" ng-show="hasPrograms()">
<div class="col-xl-6 col-lg-12" style="padding: 5px;" ng-show="hasPrograms() && hasAdvancedTools()">
<div class="input-group">
<div class="input-group-prepend">
<select class="custom-select" ng-model="rerunStart"
@ -381,7 +387,7 @@
<p ng-show='showHelp.check'>Divides the programming in blocks of 6, 8 or 12 hours then repeats each of the blocks the specified number of times. For example, you can make a channel that plays exactly the same channels in the morning and in the afternoon. This button might be disabled if the channel is already too large.</p>
</div>
<div class="col-xl-6 col-lg-12" style="padding: 5px;" ng-show="hasPrograms()">
<div class="col-xl-6 col-lg-12" style="padding: 5px;" ng-show="hasPrograms() && hasAdvancedTools()">
<div class="input-group" >
<div class="input-group-prepend">
<button class="btn btn-sm btn-secondary form-control form-control-sm" type="button" ng-click="savePositions()">
@ -407,7 +413,7 @@
<p ng-show='showHelp.check'>Adds a channel redirect. During this period of time, the channel will redirect to another channel.</p>
</div>
<div class="col-xl-6 col-lg-12" style="padding: 5px;" ng-show="hasPrograms()">
<div class="col-xl-6 col-lg-12" style="padding: 5px;" ng-show="hasPrograms() && hasAdvancedTools()">
<div class="input-group">
<div class="input-group-prepend">
<div class='loader' ng-hide='channelsDownloaded'></div>
@ -425,7 +431,7 @@
<p ng-show='showHelp.check'>Will redirect to another channel while between the selected hours.</p>
</div>
<div class="col-xl-6 col-lg-12" style="padding: 5px;" ng-show="hasPrograms()">
<div ng-class="toolWide()" style="padding: 5px;" ng-show="hasPrograms()">
<div class="input-group">
<div class="input-group-prepend">
<button class='btn btn-sm btn-warning form-control' ng-click="slideAllPrograms(-slide.value)"
@ -449,27 +455,51 @@
<p ng-show='showHelp.check'>Slides the whole schedule. The &quot;Fast-Forward&quot; button will advance the stream by the specified amount of time. The &quot;Rewind&quot; button does the opposite.</p>
</div>
<div class="col-xl-3 col-lg-6" style="padding: 5px;" ng-show="hasPrograms()">
<div class="input-group">
<div ng-class="toolThin()" style="padding: 5px;" ng-show="hasPrograms()">
<div class="input-group" >
<div class="input-group-prepend">
<button class="btn btn-sm btn-warning form-control"
type="button"
ng-click="rerollTimeSlots()"
ng-disabled="hasNoTimeSlots()"
title = "Regenerate time slots..."
>
<i class='fas fa-redo'></i>
</button>
</div>
<button class='btn btn-sm btn-warning form-control' ng-click="onTimeSlotsButtonClick()"
title="Time Slots..."
>
<i class='fas fa-blender'></i> Time Slots...
</button>
</div>
<p ng-show='showHelp.check'>This allows to schedul specific shows to run at specific time slots of the day or a week. It&apos;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.</p>
<p ng-show='showHelp.check'>This allows to schedule specific shows to run at specific time slots of the day or a week. It&apos;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.</p>
</div>
<div class="col-xl-3 col-lg-6" style="padding: 5px;" ng-show="hasPrograms()">
<div class="input-group">
<div ng-class="toolThin()" style="padding: 5px;" ng-show="hasPrograms()">
<div class="input-group" >
<div class="input-group-prepend">
<button class="btn btn-sm btn-warning form-control"
type="button"
ng-click="rerollRandomSlots()"
ng-disabled="hasNoRandomSlots()"
title = "Regenerate random slots..."
>
<i class='fas fa-redo'></i>
</button>
</div>
<button class='btn btn-sm btn-warning form-control' ng-click="onRandomSlotsButtonClick()"
title="Random Slots..."
>
<i class='fas fa-flask'></i> Random Slots...
</button>
</div>
<p ng-show='showHelp.check'>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.</p>
<p ng-show='showHelp.check'>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. Once a channel has been configured with random slots, the reload button can re-evaluate them again, with the saved settings.</p>
</div>
@ -493,7 +523,7 @@
<p ng-show='showHelp.check'>Removes any Flex periods from the schedule.</p>
</div>
<div class="col-md-auto" style="padding: 5px;" ng-show="hasPrograms()">
<div class="col-md-auto" style="padding: 5px;" ng-show="hasPrograms() && hasAdvancedTools()">
<div class='input-group'>
<button class="btn btn-sm btn-danger form-control form-control-sm" type="button" ng-click="wipeSpecials()" title='Remove Specials' >
<i class='fa fa-trash-alt'></i> Specials
@ -519,6 +549,24 @@
</div>
<p ng-show='showHelp.check'>Wipes out the schedule so that you can start over.</p>
</div>
<br />
<div class="col-xl-6 col-lg-6" style="padding: 5px;" ng-show="hasPrograms()">
<div class="input-group" >
<button class='btn btn-sm btn-outline-secondary form-control' ng-click="toggleAdvanced()"
title="Toggle extra tools..."
>
<i class='fas fa-tools'></i> {{ hasAdvancedTools() ? "Less" : "More"}} Tools...
</button>
</div>
<p ng-show='showHelp.check'>Use this button to show or hide a bunch of additional tools that might be useful.</p>
</div>
</div>
</div>

View File

@ -177,7 +177,8 @@
<div class="modal-footer" ng-show='!loading'>
<div class='text-danger small'>{{error}}</div>
<button type="button" class="btn btn-sm btn-link" ng-click="finished(true)">Cancel</button>
<button ng-disabled='disableCreateLineup()' type="button" class="btn btn-sm btn-primary" ng-click="finished(false);">Create Lineup</button>
<button ng-show='! hideCreateLineup()' ng-disabled='disableCreateLineup()' type="button" class="btn btn-sm btn-primary" ng-click="finished(false);">Create Lineup</button>
<button ng-show='showResetSlots()' type="button" class="btn btn-sm btn-danger" ng-click="finished(false);">Reset Slots</button>
</div>
</div>
</div>

View File

@ -148,7 +148,8 @@
<div class="modal-footer" ng-show='!loading'>
<div class='text-danger small'>{{error}}</div>
<button type="button" class="btn btn-sm btn-link" ng-click="finished(true)">Cancel</button>
<button ng-disabled='disableCreateLineup()' type="button" class="btn btn-sm btn-primary" ng-click="finished(false);">Create Lineup</button>
<button ng-show='! hideCreateLineup()' ng-disabled='disableCreateLineup()' type="button" class="btn btn-sm btn-primary" ng-click="finished(false);">Create Lineup</button>
<button ng-show='showResetSlots()' type="button" class="btn btn-sm btn-danger" ng-click="finished(false);">Reset Slots</button>
</div>
</div>
</div>

View File

@ -20,7 +20,12 @@
<td>FFMPEG</td>
<td><div class='loader' ng-if="version.length &lt;= 0"></div>{{ffmpegVersion}}</td>
</tr>
<!-- coming soon, ffmpeg version, nodejs version, plex version, whatever can be used to help debug things-->
<tr>
<td>nodejs</td>
<td><div class='loader' ng-if="version.length &lt;= 0"></div>{{nodejs}}</td>
</tr>
<!-- coming soon: plex version, whatever can be used to help debug things-->
</table>
</div>

View File

@ -59,7 +59,9 @@ module.exports = function (getShowData) {
let data = getShowData(progs[i]);
if (data.hasShow) {
let key = data.showId + "|" + data.order;
tmpProgs[key] = progs[i];
if (typeof(tmpProgs[key]) === 'undefined') {
tmpProgs[key] = progs[i];
}
}
}
}

View File

@ -172,13 +172,13 @@ module.exports = function ($http, $window, $interval) {
var client = new Plex(server)
const key = lib.key
const res = await client.Get(key)
const size = res.Metadata !== 'undefined' ? res.Metadata.length : 0;
const size = (typeof(res.Metadata) !== 'undefined') ? res.Metadata.length : 0;
var nested = []
if (typeof (lib.genres) !== 'undefined') {
nested = Array.from(lib.genres)
}
var seenFiles = {};
var collections = {};
let albumKeys = {};
let albums = {};
@ -276,43 +276,6 @@ module.exports = function ($http, $window, $interval) {
program.episode = 1
program.season = 1
}
if (typeof (res.Metadata[i].Collection) !== 'undefined') {
let coll = res.Metadata[i].Collection;
if (coll.length == 2) {
// the /all endpoint returns incomplete data, so we
// might have to complete the list of collections
// when there are already 2 collections there.
//console.log(res.Metadata[i]);
let complete = {}
try {
complete = await client.Get(`/library/metadata/${res.Metadata[i].ratingKey}`);
} catch (err) {
console.error("Error attempting to load collections", err);
}
if (
(typeof(complete.Metadata) !== 'undefined')
&&
(complete.Metadata.length == 1)
&&
(typeof(complete.Metadata[0].Collection) !== 'undefined')
&&
( complete.Metadata[0].Collection.length > 2)
) {
coll = complete.Metadata[0].Collection;
}
}
for (let j = 0; j < coll.length; j++) {
let tag = coll[j].tag;
if ( (typeof(tag)!== "undefined") && (tag.length > 0) ) {
let collection = collections[tag];
if (typeof(collection) === 'undefined') {
collection = [];
collections[tag] = collection;
}
collection.push( program );
}
}
}
nested.push(program)
} catch(err) {
let msg = "Error when attempting to read nested data for " + key + " " + res.Metadata[i].title;
@ -320,40 +283,30 @@ module.exports = function ($http, $window, $interval) {
console.error(msg , err);
}
}
if (includeCollections === true) {
if ( (includeCollections === true) && (res.viewGroup !== "artist" ) ) {
let k = res.librarySectionID;
k = `/library/sections/${k}/collections`;
let collections = await client.Get(k);
if ( typeof(collections.Metadata) === 'undefined') {
collections.Metadata = [];
}
let directories = collections.Metadata;
let nestedCollections = [];
let keys = [];
Object.keys(collections).forEach(function(key,index) {
keys.push(key);
});
for (let k = 0; k < keys.length; k++) {
let key = keys[k];
if ( !(collections[key].length >= 1) ) {
//it's pointless to include it.
continue;
for (let i = 0; i < directories.length; i++) {
let title;
if (res.viewGroup === "show") {
title = directories[i].title + " Collection"
} else {
title = directories[i].title;
}
let collection = {
title: key,
key: "#collection",
icon : "",
type : "collection",
nested: collections[key],
}
if (res.viewGroup === 'show') {
collection.title = collection.title + " Collection";
//nest the seasons directly because that's way too many depth levels already
let shows = collection.nested;
let collectionContents = [];
for (let i = 0; i < shows.length; i++) {
let seasons = await exported.getNested(server, shows[i], false);
for (let j = 0; j < seasons.length; j++) {
seasons[j].title = shows[i].title + " - " + seasons[j].title;
collectionContents.push(seasons[j]);
}
}
collection.nested = collectionContents;
}
nestedCollections.push( collection );
nestedCollections.push( {
key : directories[i].key,
title : title,
type: "collection",
collectionType : res.viewGroup,
} );
}
nested = nestedCollections.concat(nested);
}