Merge branch 'dev/0.0.x' into main
This commit is contained in:
commit
29ea556b23
@ -1,4 +1,4 @@
|
||||
# dizqueTV 0.0.67
|
||||
# dizqueTV 0.0.68-prerelease
|
||||
  
|
||||
|
||||
Create live TV channel streams from media on your Plex servers.
|
||||
|
||||
20
index.js
20
index.js
@ -67,19 +67,29 @@ let xmltvInterval = {
|
||||
interval: null,
|
||||
lastRefresh: null,
|
||||
updateXML: async () => {
|
||||
let channels = [];
|
||||
try {
|
||||
let getChannelsCached = async() => {
|
||||
let channelNumbers = await channelDB.getAllChannelNumbers();
|
||||
channels = await Promise.all( channelNumbers.map( async (x) => {
|
||||
return await channelCache.getChannelConfig(channelDB, x);
|
||||
return await Promise.all( channelNumbers.map( async (x) => {
|
||||
return (await channelCache.getChannelConfig(channelDB, x))[0];
|
||||
}) );
|
||||
}
|
||||
|
||||
let channels = [];
|
||||
|
||||
try {
|
||||
channels = await getChannelsCached();
|
||||
let xmltvSettings = db['xmltv-settings'].find()[0];
|
||||
await guideService.refresh( await channelDB.getAllChannels(), xmltvSettings.cache*60*60*1000 );
|
||||
let t = guideService.prepareRefresh(channels, xmltvSettings.cache*60*60*1000);
|
||||
channels = null;
|
||||
|
||||
await guideService.refresh(t);
|
||||
xmltvInterval.lastRefresh = new Date()
|
||||
console.log('XMLTV Updated at ', xmltvInterval.lastRefresh.toLocaleString());
|
||||
} catch (err) {
|
||||
console.error("Unable to update TV guide?", err);
|
||||
return;
|
||||
}
|
||||
channels = await getChannelsCached();
|
||||
|
||||
let plexServers = db['plex-servers'].find()
|
||||
for (let i = 0, l = plexServers.length; i < l; i++) { // Foreach plex server
|
||||
|
||||
@ -18,6 +18,7 @@
|
||||
"dependencies": {
|
||||
"angular": "^1.7.9",
|
||||
"angular-router-browserify": "0.0.2",
|
||||
"angular-vs-repeat": "2.0.13",
|
||||
"axios": "^0.19.2",
|
||||
"body-parser": "^1.19.0",
|
||||
"diskdb": "^0.1.17",
|
||||
|
||||
@ -4,5 +4,5 @@ module.exports = {
|
||||
STEALTH_DURATION: 5 * 60* 1000,
|
||||
TVGUIDE_MAXIMUM_FLEX_DURATION : 6 * 60 * 60 * 1000,
|
||||
|
||||
VERSION_NAME: "0.0.67"
|
||||
VERSION_NAME: "0.0.68-prerelease"
|
||||
}
|
||||
|
||||
@ -30,6 +30,7 @@ class FFMPEG extends events.EventEmitter {
|
||||
this.audioChannelsSampleRate = this.opts.normalizeAudio;
|
||||
this.ensureResolution = this.opts.normalizeResolution;
|
||||
this.volumePercent = this.opts.audioVolumePercent;
|
||||
this.hasBeenKilled = false;
|
||||
}
|
||||
async spawnConcat(streamUrl) {
|
||||
return await this.spawn(streamUrl, undefined, undefined, undefined, true, false, undefined, true)
|
||||
@ -404,24 +405,37 @@ class FFMPEG extends events.EventEmitter {
|
||||
let doLogs = this.opts.logFfmpeg && !isConcatPlaylist;
|
||||
this.ffmpeg = spawn(this.ffmpegPath, ffmpegArgs, { stdio: ['ignore', 'pipe', (doLogs?process.stderr:"ignore") ] } );
|
||||
|
||||
this.ffmpeg.on('close', (code) => {
|
||||
let ffmpegName = (isConcatPlaylist ? "Concat FFMPEG": "Stream FFMPEG");
|
||||
|
||||
this.ffmpeg.on('exit', (code, signal) => {
|
||||
if (code === null) {
|
||||
console.log( `${ffmpegName} exited due to signal: ${signal}` );
|
||||
this.emit('close', code)
|
||||
} else if (code === 0) {
|
||||
console.log( `${ffmpegName} exited normally.` );
|
||||
this.emit('end')
|
||||
} else if (code === 255) {
|
||||
if (this.hasBeenKilled) {
|
||||
console.log( `${ffmpegName} finished with code 255.` );
|
||||
this.emit('close', code)
|
||||
return;
|
||||
}
|
||||
if (! this.sentData) {
|
||||
this.emit('error', { code: code, cmd: `${this.opts.ffmpegPath} ${ffmpegArgs.join(' ')}` })
|
||||
}
|
||||
console.log( `${ffmpegName} exited with code 255.` );
|
||||
this.emit('close', code)
|
||||
} else {
|
||||
console.log( `${ffmpegName} exited with code ${code}.` );
|
||||
this.emit('error', { code: code, cmd: `${this.opts.ffmpegPath} ${ffmpegArgs.join(' ')}` })
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
return this.ffmpeg.stdout;
|
||||
}
|
||||
kill() {
|
||||
if (typeof this.ffmpeg != "undefined") {
|
||||
this.hasBeenKilled = true;
|
||||
this.ffmpeg.kill()
|
||||
}
|
||||
}
|
||||
|
||||
@ -42,9 +42,11 @@ class PlexTranscoder {
|
||||
this.log(` deinterlace: ${deinterlace}`)
|
||||
this.log(` streamPath: ${this.settings.streamPath}`)
|
||||
|
||||
|
||||
|
||||
if (this.settings.streamPath === 'direct' || this.settings.forceDirectPlay) {
|
||||
if (this.settings.enableSubtitles) {
|
||||
console.log("Direct play is forced, so subtitles are forcibly disabled.");
|
||||
this.settings.enableSubtitles = false;
|
||||
}
|
||||
stream = {directPlay: true}
|
||||
} else {
|
||||
try {
|
||||
|
||||
@ -28,12 +28,16 @@ class TVGuideService
|
||||
return this.cached;
|
||||
}
|
||||
|
||||
async refresh(inputChannels, limit) {
|
||||
prepareRefresh(inputChannels, limit) {
|
||||
let t = (new Date()).getTime();
|
||||
this.updateTime = t;
|
||||
this.updateLimit = t + limit;
|
||||
let channels = inputChannels.filter( ch => (ch.stealth !== true) );
|
||||
let channels = inputChannels;
|
||||
this.updateChannels = channels;
|
||||
return t;
|
||||
}
|
||||
|
||||
async refresh(t) {
|
||||
while( this.lastUpdate < t) {
|
||||
if (this.currentUpdate == -1) {
|
||||
this.currentUpdate = this.updateTime;
|
||||
@ -47,6 +51,9 @@ class TVGuideService
|
||||
}
|
||||
|
||||
async makeAccumulated(channel) {
|
||||
if (typeof(channel.programs) === 'undefined') {
|
||||
throw Error( JSON.stringify(channel).slice(0,200) );
|
||||
}
|
||||
let n = channel.programs.length;
|
||||
let arr = new Array( channel.programs.length + 1);
|
||||
arr[0] = 0;
|
||||
@ -296,8 +303,10 @@ class TVGuideService
|
||||
}
|
||||
} else {
|
||||
for (let i = 0; i < channels.length; i++) {
|
||||
if(! channels[i].stealth) {
|
||||
let programs = await this.getChannelPrograms(t0, t1, channels[i] );
|
||||
result[ channels[i].number ] = programs;
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
|
||||
@ -3,8 +3,9 @@ require('angular-router-browserify')(angular)
|
||||
require('./ext/lazyload')(angular)
|
||||
require('./ext/dragdrop')
|
||||
require('./ext/angularjs-scroll-glue')
|
||||
require('angular-vs-repeat');
|
||||
|
||||
var app = angular.module('myApp', ['ngRoute', 'angularLazyImg', 'dndLists', 'luegg.directives'])
|
||||
var app = angular.module('myApp', ['ngRoute', 'vs-repeat', 'angularLazyImg', 'dndLists', 'luegg.directives'])
|
||||
|
||||
app.service('plex', require('./services/plex'))
|
||||
app.service('dizquetv', require('./services/dizquetv'))
|
||||
|
||||
@ -16,6 +16,9 @@ module.exports = function ($timeout, $location, dizquetv) {
|
||||
scope._frequencyMessage = "";
|
||||
scope.millisecondsOffset = 0;
|
||||
scope.minProgramIndex = 0;
|
||||
scope.episodeMemory = {
|
||||
saved : false,
|
||||
};
|
||||
if (typeof scope.channel === 'undefined' || scope.channel == null) {
|
||||
scope.channel = {}
|
||||
scope.channel.programs = []
|
||||
@ -113,6 +116,7 @@ module.exports = function ($timeout, $location, dizquetv) {
|
||||
setTimeout( () => {
|
||||
scope.channel.programs.splice(dropIndex + index, 0, program);
|
||||
updateChannelDuration()
|
||||
scope.$apply();
|
||||
}, 1);
|
||||
return true;
|
||||
}
|
||||
@ -328,6 +332,22 @@ module.exports = function ($timeout, $location, dizquetv) {
|
||||
}
|
||||
}
|
||||
|
||||
let interpolate = ( () => {
|
||||
let h = 60*60*1000;
|
||||
let ix = [0, 1*h, 2*h, 4*h, 8*h, 24*h];
|
||||
let iy = [0, 1.0, 1.25, 1.5, 1.75, 2.0];
|
||||
let n = ix.length;
|
||||
|
||||
return (x) => {
|
||||
for (let i = 0; i < n-1; i++) {
|
||||
if( (ix[i] <= x) && ( (x < ix[i+1]) || i==n-2 ) ) {
|
||||
return iy[i] + (iy[i+1] - iy[i]) * ( (x - ix[i]) / (ix[i+1] - ix[i]) );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} )();
|
||||
|
||||
scope.programSquareStyle = (program) => {
|
||||
let background ="";
|
||||
if ( (program.isOffline) && (program.type !== 'redirect') ) {
|
||||
@ -371,26 +391,26 @@ module.exports = function ($timeout, $location, dizquetv) {
|
||||
}
|
||||
let rgb1 = "rgb("+ r + "," + g + "," + b +")";
|
||||
let rgb2 = "rgb("+ r2 + "," + g2 + "," + b2 +")"
|
||||
angle += 90;
|
||||
background = "repeating-linear-gradient( " + angle + "deg, " + rgb1 + ", " + rgb1 + " " + w + "px, " + rgb2 + " " + w + "px, " + rgb2 + " " + (w*2) + "px)";
|
||||
|
||||
}
|
||||
let ems = Math.pow( Math.min(24*60*60*1000, program.duration), 0.7 );
|
||||
ems = ems / Math.pow(5*60*1000., 0.7);
|
||||
ems = Math.max( 0.25 , ems);
|
||||
let top = Math.max(0.0, (1.75 - ems) / 2.0) ;
|
||||
if (top == 0.0) {
|
||||
top = "1px";
|
||||
} else {
|
||||
top = top + "em";
|
||||
}
|
||||
let f = interpolate;
|
||||
let w = 5.0;
|
||||
let t = 4*60*60*1000;
|
||||
//let d = Math.log( Math.min(t, program.duration) ) / Math.log(2);
|
||||
//let a = (d * Math.log(2) ) / Math.log(t);
|
||||
let a = ( f(program.duration) *w) / f(t);
|
||||
a = Math.min( w, Math.max(0.3, a) );
|
||||
b = w - a + 0.01;
|
||||
|
||||
return {
|
||||
'width': '0.5em',
|
||||
'height': ems + 'em',
|
||||
'margin-right': '0.50em',
|
||||
'width': `${a}%`,
|
||||
'height': '1.3em',
|
||||
'margin-right': `${b}%`,
|
||||
'background': background,
|
||||
'border': '1px solid black',
|
||||
'margin-top': top,
|
||||
'margin-top': "0.01em",
|
||||
'margin-bottom': '1px',
|
||||
};
|
||||
}
|
||||
@ -491,6 +511,17 @@ module.exports = function ($timeout, $location, dizquetv) {
|
||||
for (let i = 0, l = scope.channel.programs.length; i < l; i++) {
|
||||
let p = pos(t);
|
||||
if ( (p != 0) && (p + scope.channel.programs[i].duration > b) ) {
|
||||
if (b - 30000 > p) {
|
||||
let d = b- p;
|
||||
t += d;
|
||||
p = pos(t);
|
||||
progs.push(
|
||||
{
|
||||
duration: d,
|
||||
isOffline: true,
|
||||
}
|
||||
)
|
||||
}
|
||||
//time to pad
|
||||
let d = m - p;
|
||||
progs.push(
|
||||
@ -508,19 +539,126 @@ module.exports = function ($timeout, $location, dizquetv) {
|
||||
t += scope.channel.programs[i].duration;
|
||||
}
|
||||
if (pos(t) != 0) {
|
||||
if (b > pos(t)) {
|
||||
let d = b - pos(t) % m;
|
||||
t += d;
|
||||
progs.push(
|
||||
{
|
||||
duration: d,
|
||||
isOffline: true,
|
||||
}
|
||||
)
|
||||
}
|
||||
let d = m - pos(t);
|
||||
progs.push(
|
||||
{
|
||||
duration: d,
|
||||
isOffline: true,
|
||||
type: (typeof(ch) === 'undefined') ? undefined: "redirect",
|
||||
channel: ch,
|
||||
type: (typeof(ch) === 'undefined') ? undefined: "redirect",
|
||||
}
|
||||
)
|
||||
}
|
||||
scope.channel.programs = progs;
|
||||
updateChannelDuration();
|
||||
}
|
||||
scope.savePositions = () => {
|
||||
scope.episodeMemory = {
|
||||
saved : false,
|
||||
};
|
||||
let array = scope.channel.programs;
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
if (array[i].type === 'episode' && array[i].season != 0) {
|
||||
let key = array[i].showTitle;
|
||||
if (typeof(scope.episodeMemory[key]) === 'undefined') {
|
||||
scope.episodeMemory[key] = {
|
||||
season: array[i].season,
|
||||
episode: array[i].episode,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
scope.episodeMemory.saved = true;
|
||||
}
|
||||
scope.recoverPositions = () => {
|
||||
//this is basically the code for cyclic shuffle
|
||||
let array = scope.channel.programs;
|
||||
let shows = {};
|
||||
let next = {};
|
||||
let counts = {};
|
||||
// some precalculation, useful to stop the shuffle from being quadratic...
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
let vid = array[i];
|
||||
if (vid.type === 'episode' && vid.season != 0) {
|
||||
let countKey = {
|
||||
title: vid.showTitle,
|
||||
s: vid.season,
|
||||
e: vid.episode,
|
||||
}
|
||||
let key = JSON.stringify(countKey);
|
||||
let c = ( (typeof(counts[key]) === 'undefined') ? 0 : counts[key] );
|
||||
counts[key] = c + 1;
|
||||
let showEntry = {
|
||||
c: c,
|
||||
it: vid
|
||||
}
|
||||
if ( typeof(shows[vid.showTitle]) === 'undefined') {
|
||||
shows[vid.showTitle] = [];
|
||||
}
|
||||
shows[vid.showTitle].push(showEntry);
|
||||
}
|
||||
}
|
||||
//this is O(|N| log|M|) where |N| is the total number of TV
|
||||
// episodes and |M| is the maximum number of episodes
|
||||
// in a single show. I am pretty sure this is a lower bound
|
||||
// on the time complexity that's possible here.
|
||||
Object.keys(shows).forEach(function(key,index) {
|
||||
shows[key].sort( (a,b) => {
|
||||
if (a.c == b.c) {
|
||||
if (a.it.season == b.it.season) {
|
||||
if (a.it.episode == b.it.episode) {
|
||||
return 0;
|
||||
} else {
|
||||
return (a.it.episode < b.it.episode)?-1: 1;
|
||||
}
|
||||
} else {
|
||||
return (a.it.season < b.it.season)?-1: 1;
|
||||
}
|
||||
} else {
|
||||
return (a.c < b.c)? -1: 1;
|
||||
}
|
||||
});
|
||||
next[key] = 0;
|
||||
if (typeof(scope.episodeMemory[key]) !== 'undefined') {
|
||||
for (let i = 0; i < shows[key].length; i++) {
|
||||
if (
|
||||
(shows[key][i].it.season === scope.episodeMemory[key].season)
|
||||
&&(shows[key][i].it.episode === scope.episodeMemory[key].episode)
|
||||
) {
|
||||
next[key] = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
if (array[i].type === 'episode' && array[i].season != 0) {
|
||||
let title = array[i].showTitle;
|
||||
var sequence = shows[title];
|
||||
let j = next[title];
|
||||
array[i] = sequence[j].it;
|
||||
|
||||
next[title] = (j + 1) % sequence.length;
|
||||
}
|
||||
}
|
||||
scope.channel.programs = array;
|
||||
updateChannelDuration();
|
||||
|
||||
}
|
||||
scope.cannotRecoverPositions = () => {
|
||||
return scope.episodeMemory.saved !== true;
|
||||
}
|
||||
|
||||
scope.addBreaks = (afterMinutes, minDurationSeconds, maxDurationSeconds) => {
|
||||
let after = afterMinutes * 60 * 1000 + 5000; //allow some seconds of excess
|
||||
let minDur = minDurationSeconds;
|
||||
@ -869,6 +1007,7 @@ module.exports = function ($timeout, $location, dizquetv) {
|
||||
scope.hasFlex = false;
|
||||
for (let i = 0, l = scope.channel.programs.length; i < l; i++) {
|
||||
scope.channel.programs[i].start = new Date(scope.channel.startTime.valueOf() + scope.channel.duration)
|
||||
scope.channel.programs[i].$index = i;
|
||||
scope.channel.duration += scope.channel.programs[i].duration
|
||||
scope.channel.programs[i].stop = new Date(scope.channel.startTime.valueOf() + scope.channel.duration)
|
||||
if (scope.channel.programs[i].isOffline) {
|
||||
@ -906,6 +1045,9 @@ module.exports = function ($timeout, $location, dizquetv) {
|
||||
scope.error.programs = "No programs have been selected. Select at least one program."
|
||||
else {
|
||||
channel.startTime.setMilliseconds( scope.millisecondsOffset);
|
||||
for (let i = 0; i < scope.channel.programs.length; i++) {
|
||||
delete scope.channel.programs[i].$index;
|
||||
}
|
||||
scope.onDone(JSON.parse(angular.toJson(channel)))
|
||||
}
|
||||
$timeout(() => { scope.error = {} }, 3500)
|
||||
|
||||
@ -145,6 +145,9 @@ module.exports = function (plex, dizquetv, $timeout) {
|
||||
return r;
|
||||
}
|
||||
|
||||
scope.shouldDisableSubtitles = () => {
|
||||
return scope.settings.forceDirectPlay || (scope.settings.streamPath === "direct" );
|
||||
}
|
||||
|
||||
scope.addPlexServer = async () => {
|
||||
scope.isProcessing = true;
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
<title>dizqueTV</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css">
|
||||
<link href="https://stackpath.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.14.0/css/all.min.css">
|
||||
<link href="style.css" rel="stylesheet">
|
||||
<script src="version.js"></script>
|
||||
<script src="bundle.js"></script>
|
||||
@ -14,18 +14,23 @@
|
||||
<div class="container">
|
||||
<h1>dizqueTV
|
||||
<small class="pull-right" style="padding: 5px;">
|
||||
<a href="https://github.com/vexorian/dizquetv">
|
||||
<span class="fa fa-github text-sm"></span>
|
||||
<a href="https://github.com/vexorian/dizquetv" title='Git Repository'>
|
||||
<span class="fab fa-github text-sm"></span>
|
||||
</a>
|
||||
</small>
|
||||
<small class="pull-right" style="padding: 5px;">
|
||||
<a href="https://discord.gg/U64P9MR" title='Discord' >
|
||||
<span class="fab fa-discord"></span>
|
||||
</a>
|
||||
</small>
|
||||
</h1>
|
||||
<a href="#!/channels">Channels</a> - <a href="#!/guide">Guide</a> - <a href="#!/settings">Settings</a> - <a href="#!/version">Version</a>
|
||||
<span class="pull-right">
|
||||
<span style="margin-right: 15px;">
|
||||
<a href="/api/xmltv.xml">XMLTV <span class="fa fa-file-code-o"></span></a>
|
||||
<a href="/api/xmltv.xml">XMLTV <span class="far fa-file-code"></span></a>
|
||||
</span>
|
||||
<span>
|
||||
<a href="/api/channels.m3u">M3U <span class="fa fa-file-movie-o"></span></a>
|
||||
<a href="/api/channels.m3u">M3U <span class="far fa-file-video"></span></a>
|
||||
</span>
|
||||
</span>
|
||||
<hr/>
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
.pull-right { float: right; }
|
||||
|
||||
.commercials-panel {
|
||||
background-color: rgb(70, 70, 70);
|
||||
border-top: 1px solid #daa104;
|
||||
|
||||
@ -139,6 +139,9 @@
|
||||
<h6>Sort Release Dates</h6>
|
||||
<p>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>
|
||||
|
||||
<h6>Sort Release Dates</h6>
|
||||
<p>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>
|
||||
|
||||
<h6>Balance Shows</h6>
|
||||
<p>Attempts to make the total amount of time each TV show appears in the programming as balanced as possible. This works by adding multiple copies of TV shows that have too little total time and by possibly removing duplicated episodes from TV shows that have too much total time. Note that in many situations it would be impossible to achieve perfect balance because channel duration is not infinite. Movies/Clips are treated as a single TV show. Note that this will most likely result in a larger channel and that having large channels makes some UI operations slower.</p>
|
||||
|
||||
@ -160,6 +163,10 @@
|
||||
<h6>Reruns</h6>
|
||||
<p>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. </p>
|
||||
|
||||
<h6>Save|Recover Episode Positions</h6>
|
||||
<p>The "Save" button saves the current episodes that are next to be played for each tv show. Then whenever you click the "Recover Episode Popsitions" button, episodes will be rearranged cyclically and they will start with the saved positions. So you can maintain episode sequences even after modifying the channel. If there are any new TV shows, they will start at their current positions. Movies and specials won't change positions.
|
||||
</p>
|
||||
|
||||
<h6>Add Redirect</h6>
|
||||
<p>Adds a channel redirect. During this period of time, the channel will redirect to another channel.</p>
|
||||
|
||||
@ -189,7 +196,7 @@
|
||||
<div class="col-md-6" style="padding: 5px;">
|
||||
<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">
|
||||
<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">
|
||||
</div>
|
||||
<div class="input-group-prepend">
|
||||
<div class="input-group-text" style="padding: 0;">
|
||||
@ -197,37 +204,54 @@
|
||||
<input id="randomizeBlockShuffle" type="checkbox" ng-model="randomizeBlockShuffle">
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-sm btn-warning form-control form-control-sm" type="button" ng-click="blockShuffle(blockCount, randomizeBlockShuffle)">Block Shuffle</button>
|
||||
<button class="btn btn-sm btn-warning form-control form-control-sm" type="button" ng-click="blockShuffle(blockCount, randomizeBlockShuffle)">
|
||||
<i class='fa fa-random'></i> Block Shuffle
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-group col-md-3" style="padding: 5px;">
|
||||
<button class="btn btn-sm btn-warning form-control form-control-sm" type="button" ng-click="randomShuffle()">Random Shuffle</button>
|
||||
<button class="btn btn-sm btn-warning form-control form-control-sm" type="button" ng-click="randomShuffle()">
|
||||
<i class='fa fa-random'></i> Random Shuffle
|
||||
</button>
|
||||
</div>
|
||||
<div class="input-group col-md-3" style="padding: 5px;">
|
||||
<button class="btn btn-sm btn-warning form-control form-control-sm" type="button" ng-click="cyclicShuffle()">Cyclic Shuffle</button>
|
||||
<button class="btn btn-sm btn-warning form-control form-control-sm" type="button" ng-click="cyclicShuffle()">
|
||||
<i class='fa fa-random'></i> Cyclic Shuffle
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="input-group col-md-6" style="padding: 5px;">
|
||||
<button class="btn btn-sm btn-warning form-control form-control-sm" type="button" ng-click="sortShows()">Sort TV Shows</button>
|
||||
<button class="btn btn-sm btn-warning form-control form-control-sm" type="button" ng-click="sortShows()">
|
||||
<i class='fa fa-sort-alpha-down'></i> Sort TV Shows
|
||||
</button>
|
||||
</div>
|
||||
<div class="input-group col-md-6" style="padding: 5px;">
|
||||
<button class="btn btn-sm btn-warning form-control form-control-sm" type="button" ng-click="sortByDate()">Sort Release Dates</button>
|
||||
<button class="btn btn-sm btn-warning form-control form-control-sm" type="button" ng-click="sortByDate()">
|
||||
<i class='fa fa-sort-numeric-down'></i> Sort Release Dates
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="input-group col-md-6" style="padding: 5px;">
|
||||
<button class="btn btn-sm btn-warning form-control form-control-sm" type="button" ng-click="equalizeShows()">Balance Shows</button>
|
||||
<button class="btn btn-sm btn-warning form-control form-control-sm" type="button" ng-click="equalizeShows()">
|
||||
<i class='fa fa-balance-scale'></i> Balance Shows
|
||||
</button>
|
||||
</div>
|
||||
<div class="input-group col-md-6" style="padding: 5px;">
|
||||
<button class="btn btn-sm btn-warning form-control form-control-sm" type="button" ng-click="startFrequencyTweak()">Tweak Weights...</button>
|
||||
<button class="btn btn-sm btn-warning form-control form-control-sm" type="button" ng-click="startFrequencyTweak()">
|
||||
<i class='fa fa-balance-scale'></i> Tweak Weights...
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="input-group col-md-6" style="padding: 5px;">
|
||||
<button class="btn btn-sm btn-warning form-control form-control-sm" type="button" ng-click="addOffline()">Add Flex...</button>
|
||||
<button class="btn btn-sm btn-warning form-control form-control-sm" type="button" ng-click="addOffline()">
|
||||
<i class='fa fa-plus'></i> Add Flex...
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6" style="padding: 5px;">
|
||||
@ -238,7 +262,10 @@
|
||||
<select ng-model="nightEnd"
|
||||
ng-options="o.id as o.description for o in nightEndHours" />
|
||||
</div>
|
||||
<button class="btn btn-sm btn-warning form-control form-control-sm" type="button" ng-click="nightChannel(nightStart, nightEnd)" ng-disabled="nightStart==-1 || nightEnd==-1">Restrict Hours</button>
|
||||
<button class="btn btn-sm btn-warning form-control form-control-sm" type="button" ng-click="nightChannel(nightStart, nightEnd)" ng-disabled="nightStart==-1 || nightEnd==-1">
|
||||
|
||||
<i class='far fa-moon'></i> Restrict Hours
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -251,21 +278,25 @@
|
||||
ng-options="o as o.description for o in paddingOptions" />
|
||||
|
||||
</div>
|
||||
<button ng-disabled="paddingOption.id==-1" class="btn btn-sm btn-warning form-control form-control-sm" type="button" ng-click="padTimes(paddingOption.id, paddingOption.allow5)">Pad Times</button>
|
||||
<button ng-disabled="paddingOption.id==-1" class="btn btn-sm btn-warning form-control form-control-sm" type="button" ng-click="padTimes(paddingOption.id, paddingOption.allow5)">
|
||||
<i class='far fa-clock'></i> Pad Times
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6" style="padding: 5px;">
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend">
|
||||
<select ng-model="breakAfter"
|
||||
<select style="width:5em" ng-model="breakAfter"
|
||||
ng-options="o.id as o.description for o in breakAfterOptions" />
|
||||
<select ng-model="minBreakSize"
|
||||
<select style="width:5em" ng-model="minBreakSize"
|
||||
ng-options="o.id as o.description for o in minBreakSizeOptions" />
|
||||
<select ng-model="maxBreakSize"
|
||||
<select style="width:5em" ng-model="maxBreakSize"
|
||||
ng-options="o.id as o.description for o in maxBreakSizeOptions" />
|
||||
</div>
|
||||
<button class="btn btn-sm btn-warning form-control form-control-sm" type="button" ng-click="addBreaks(breakAfter, minBreakSize, maxBreakSize)" ng-disabled="breakAfter==-1 || minBreakSize==-1 || maxBreakSize==-1 || (minBreakSize > maxBreakSize)">Add Breaks</button>
|
||||
<button class="btn btn-sm btn-warning form-control form-control-sm" type="button" ng-click="addBreaks(breakAfter, minBreakSize, maxBreakSize)" ng-disabled="breakAfter==-1 || minBreakSize==-1 || maxBreakSize==-1 || (minBreakSize > maxBreakSize)">
|
||||
<i class='fa fa-coffee'></i> Add Breaks
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -283,14 +314,34 @@
|
||||
ng-options="o.id as o.description for o in rerunRepeatOptions">
|
||||
</select>
|
||||
</div>
|
||||
<button class="btn btn-sm btn-warning form-control form-control-sm" type="button" ng-click="doReruns(rerunStart, rerunBlockSize, rerunRepeats)" ng-disabled="rerunStart == -1 || rerunBlockSize == -1 || rerunRepeats == -1" >Reruns</button>
|
||||
<button class="btn btn-sm btn-warning form-control form-control-sm" type="button" ng-click="doReruns(rerunStart, rerunBlockSize, rerunRepeats)" ng-disabled="rerunStart == -1 || rerunBlockSize == -1 || rerunRepeats == -1" >
|
||||
<i class='far fa-clone'></i> Reruns
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="input-group col-md-6" style="padding: 5px;">
|
||||
<div class="input-group-prepend">
|
||||
<button class="btn btn-sm btn-secondary form-control form-control-sm" type="button" ng-click="savePositions()">
|
||||
<i class='fa fa-file-import'></i> Save
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
<button class="btn btn-sm btn-warning form-control form-control-sm" type="button" ng-click="recoverPositions()" ng-disabled='cannotRecoverPositions()' >
|
||||
<i class='fa fa-file-export'></i> Recover Episode Positions
|
||||
</button>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<div class="row">
|
||||
<div class="input-group col-md-6" style="padding: 5px;">
|
||||
<button class="btn btn-sm btn-warning form-control form-control-sm" type="button" ng-click="addRedirect()">Add Redirect...</button>
|
||||
<button class="btn btn-sm btn-warning form-control form-control-sm" type="button" ng-click="addRedirect()">
|
||||
<i class='fas fa-external-link-alt'></i> Add Redirect...
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6" style="padding: 5px;">
|
||||
@ -304,7 +355,9 @@
|
||||
<select ng-model="atNightEnd"
|
||||
ng-options="o.id as o.description for o in nightEndHours" ></select>
|
||||
</div>
|
||||
<button class="btn btn-sm btn-warning form-control form-control-sm" type="button" ng-click="nightChannel(atNightEnd, atNightStart, atNightChannelNumber)" ng-disabled="atNightChannelNumber==-1 || atNightStart==-1 || atNightEnd==-1">"Channel at Night"</button>
|
||||
<button class="btn btn-sm btn-warning form-control form-control-sm" type="button" ng-click="nightChannel(atNightEnd, atNightStart, atNightChannelNumber)" ng-disabled="atNightChannelNumber==-1 || atNightStart==-1 || atNightEnd==-1">
|
||||
<i class='far fa-moon'></i> "Channel at Night"
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -314,19 +367,29 @@
|
||||
|
||||
<div class="row">
|
||||
<div class="input-group col" style="padding: 5px;">
|
||||
<button class="btn btn-sm btn-danger form-control form-control-sm" type="button" ng-click="removeDuplicates()">Remove Duplicates</button>
|
||||
<button class="btn btn-sm btn-danger form-control form-control-sm" type="button" ng-click="removeDuplicates()">
|
||||
<i class='fa fa-trash-alt'></i> Remove Duplicates
|
||||
</button>
|
||||
</div>
|
||||
<div class="input-group col" style="padding: 5px;">
|
||||
<button class="btn btn-sm btn-danger form-control form-control-sm" type="button" ng-click="removeOffline()">Remove Flex</button>
|
||||
<button class="btn btn-sm btn-danger form-control form-control-sm" type="button" ng-click="removeOffline()">
|
||||
<i class='fa fa-trash-alt'></i> Remove Flex
|
||||
</button>
|
||||
</div>
|
||||
<div class="input-group col" style="padding: 5px;">
|
||||
<button class="btn btn-sm btn-danger form-control form-control-sm" type="button" ng-click="wipeSpecials()">Remove Specials</button>
|
||||
<button class="btn btn-sm btn-danger form-control form-control-sm" type="button" ng-click="wipeSpecials()">
|
||||
<i class='fa fa-trash-alt'></i> Remove Specials
|
||||
</button>
|
||||
</div>
|
||||
<div class="input-group col" style="padding: 5px;">
|
||||
<button class="btn btn-sm btn-danger form-control form-control-sm" type="button" ng-click="startRemoveShows()">Remove Show(s)...</button>
|
||||
<button class="btn btn-sm btn-danger form-control form-control-sm" type="button" ng-click="startRemoveShows()">
|
||||
<i class='fa fa-trash-alt'></i> Remove Show(s)...
|
||||
</button>
|
||||
</div>
|
||||
<div class="input-group col" style="padding: 5px;">
|
||||
<button class="btn btn-sm btn-danger form-control form-control-sm" type="button" ng-click="wipeSchedule()">Remove All</button>
|
||||
<button class="btn btn-sm btn-danger form-control form-control-sm" type="button" ng-click="wipeSchedule()">
|
||||
<i class='fa fa-trash-alt'></i> Remove All
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -334,28 +397,17 @@
|
||||
<div class="small">Add programs to this channel by selecting media from your Plex library</div>
|
||||
<br/>
|
||||
<h5 class="text-center text-danger">No programs are currently scheduled
|
||||
<button class="btn btn-sm btn-secondary" style="margin-left: 10px" type="button" ng-click="addOffline()">Schedule Flex Time
|
||||
<button class="btn btn-sm btn-secondary" style="margin-left: 10px" type="button" ng-click="addOffline()">
|
||||
<i class='fa fa-plus'></i> Schedule Flex Time
|
||||
</button>
|
||||
</h6>
|
||||
|
||||
</div>
|
||||
<div class="list-group list-group-root">
|
||||
<div ng-show="channel.programs.length > 100">
|
||||
<div>Showing programs {{minProgramIndex+1}} to {{minProgramIndex+100}}</div>
|
||||
</div>
|
||||
<input ng-show="channel.programs.length > 100" type="range" ng-model="minProgramIndex" min="0" max="{{ channel.programs.length - 100 }}" />
|
||||
<div ng-if="minProgramIndex > 0" class="list-group-item flex-container" >
|
||||
<div class="program-start">
|
||||
{{ dateForGuide(channel.startTime) }}
|
||||
</div>
|
||||
|
||||
<div style="margin-right: 5px; font-weight:ligther; text-align:center">
|
||||
⋮
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="minProgramIndex <= $index && $index < minProgramIndex+100" ng-repeat="x in channel.programs track by $index" ng-click="selectProgram($index)" dnd-list="" dnd-drop="dropFunction(index , $index, item)"
|
||||
>
|
||||
<div class="list-group-item flex-container program-row" dnd-draggable="x" dnd-moved="channel.programs.splice($index, 1);" dnd-effect-allowed="move"
|
||||
<div vs-repeat="options" style='max-height: 30em; overflow-y: auto;'>
|
||||
<div ng-repeat="x in channel.programs track by x.$index" ng-click="selectProgram(x.$index)" dnd-list="" dnd-drop="dropFunction(index , x.$index, item)" style="height: 1.5em; overflow:hidden">
|
||||
|
||||
<div class="list-group-item flex-container program-row" dnd-draggable="x" dnd-moved="channel.programs.splice(x.$index, 1);" dnd-effect-allowed="move"
|
||||
>
|
||||
|
||||
<div class="program-start">
|
||||
@ -371,20 +423,12 @@
|
||||
<span ng-if="x.type === 'redirect' " ><i>Redirect to channel:</i> <b>{{x.channel}}</b></span>
|
||||
</div>
|
||||
<div class="flex-pull-right"></div>
|
||||
<button class="btn btn-sm btn-link" ng-click="removeItem($index); $event.stopPropagation()">
|
||||
<i class="text-danger fa fa-trash"></i>
|
||||
<button class="btn btn-sm btn-link" ng-click="removeItem(x.$index); $event.stopPropagation()">
|
||||
<i class="text-danger fa fa fa-trash-alt"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="minProgramIndex < channel.programs.length - 100" class="list-group-item flex-container" >
|
||||
<div class="program-start">
|
||||
{{ dateForGuide(channel.programs[minProgramIndex + 100 - 1].stop)}}
|
||||
</div>
|
||||
|
||||
<div style="margin-right: 5px; font-weight:ligther; text-align:center">
|
||||
⋮
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="list-group-item flex-container" ng-if="channel.programs.length > 0" >
|
||||
<div class="program-start">
|
||||
{{ dateForGuide(channel.programs[channel.programs.length-1].stop)}}
|
||||
|
||||
@ -117,13 +117,19 @@
|
||||
<div ng-show="showTools">
|
||||
<div class="row">
|
||||
<div class="input-group col-md-3" style="padding: 5px;">
|
||||
<button class="btn btn-sm btn-warning form-control form-control-sm" type="button" ng-click="sortFillers()">Sort Lengths</button>
|
||||
<button class="btn btn-sm btn-warning form-control form-control-sm" type="button" ng-click="sortFillers()">
|
||||
<i class='fa fa-sort-amount-down-alt'></i> Sort Lengths
|
||||
</button>
|
||||
</div>
|
||||
<div class="input-group col-md-3" style="padding: 5px;">
|
||||
<button class="btn btn-sm btn-danger form-control form-control-sm" type="button" ng-click="fillerRemoveDuplicates()">Remove Duplicates</button>
|
||||
<button class="btn btn-sm btn-danger form-control form-control-sm" type="button" ng-click="fillerRemoveDuplicates()">
|
||||
<i class='fa fa-trash-alt'></i> Remove Duplicates
|
||||
</button>
|
||||
</div>
|
||||
<div class="input-group col-md-6" style="padding: 5px;">
|
||||
<button class="btn btn-sm btn-danger form-control form-control-sm" type="button" ng-click="fillerRemoveAllFiller()">Remove All Filler</button>
|
||||
<button class="btn btn-sm btn-danger form-control form-control-sm" type="button" ng-click="fillerRemoveAllFiller()">
|
||||
<i class='fa fa-trash-alt'></i> Remove All Filler
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -143,7 +149,7 @@
|
||||
</div>
|
||||
<div class="flex-pull-right">
|
||||
<button class="btn btn-sm btn-link" ng-click="program.filler.splice($index,1)">
|
||||
<i class="text-danger fa fa-trash" ></i>
|
||||
<i class="text-danger fa fa-trash-alt" ></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -114,7 +114,7 @@
|
||||
<li ng-if="selection.length + x >= 0" class="list-group-item" ng-repeat="x in allowedIndexes" style="cursor:default;" dnd-draggable="x" dnd-moved="selection.splice(selection.length + x, 1)" dnd-effect-allowed="move">
|
||||
{{ (selection[selection.length + x].type !== 'episode') ? selection[selection.length + x].title : (selection[selection.length + x].showTitle + ' - S' + selection[selection.length + x].season.toString().padStart(2,'0') + 'E' + selection[selection.length + x].episode.toString().padStart(2,'0'))}}
|
||||
<button class="pull-right btn btn-sm btn-link" ng-click="selection.splice(selection.length + x,1)">
|
||||
<span class="text-danger fa fa-trash" ></span>
|
||||
<span class="text-danger fa fa-trash-alt" ></span>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@ -53,7 +53,7 @@
|
||||
|
||||
<div ng-if="state.channelReport == null" ng-hide="state.showDelete" class="form-group">
|
||||
<button class="btn btn-link" ng-click="onShowDelete()">
|
||||
<span class="text-danger"><i class="fa fa-trash"></i> Delete server...</span>
|
||||
<span class="text-danger"><i class="fa fa-trash-alt"></i> Delete server...</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@ -65,7 +65,7 @@
|
||||
<p class="card-text">If you delete a plex server, all the existing programs that reference to it will be
|
||||
replaced with Flex time. Fillers that reference to the server will be removed. This operation cannot be undone.</p>
|
||||
</div>
|
||||
<button ng-if="state.channelReport == null" type="button" class="btn btn-sm btn-danger" ng-click="onDelete();" ><i class='fa fa-trash'></i> Delete</button>
|
||||
<button ng-if="state.channelReport == null" type="button" class="btn btn-sm btn-danger" ng-click="onDelete();" ><i class='fa fa-trash-alt'></i> Delete</button>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
@ -33,13 +33,13 @@
|
||||
<td><span class='text-secondary text-small'>{{ x.uri }}</span></td>
|
||||
<td>
|
||||
<div class='loader' ng-if="x.uiStatus == 0"></div>
|
||||
<div class='fa fa-check text-success' ng-if="x.uiStatus == 1">ok</div>
|
||||
<div class='fa fa-warning text-danger' ng-if="x.uiStatus == -1">error</div>
|
||||
<div class='text-success' ng-if="x.uiStatus == 1"><i class='fa fa-check'></i>ok</div>
|
||||
<div class='text-danger' ng-if="x.uiStatus == -1"><i class='fa fa-exclamation-triangle'></i>error</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class='loader' ng-if="x.backendStatus == 0"></div>
|
||||
<div class='fa fa-check text-success' ng-if="x.backendStatus == 1">ok</div>
|
||||
<div class='fa fa-warning text-danger' ng-if="x.backendStatus == -1">error</div>
|
||||
<div class='text-success' ng-if="x.backendStatus == 1"><i class='fa fa-check'></i>ok</div>
|
||||
<div class='text-danger' ng-if="x.backendStatus == -1"><i class='fa fa-exclamation-triangle'></i>error</div>
|
||||
</td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-outline-secondary" ng-click="editPlexServer(x)">
|
||||
@ -178,8 +178,8 @@
|
||||
<input type="text" class="form-control form-control-sm" ng-model="settings.subtitleSize" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input id="enableSubtitles" type="checkbox" ng-model="settings.enableSubtitles"/>
|
||||
<label for="enableSubtitles">Enable Subtitles (Requires Transcoding)</label>
|
||||
<input class="form-check-input" id="enableSubtitles" type="checkbox" ng-model="settings.enableSubtitles" ng-disabled="shouldDisableSubtitles()" />
|
||||
<label class="form-check-label" for="enableSubtitles">Enable Subtitles (Requires Transcoding)</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -18,7 +18,7 @@
|
||||
<div class="flex-pull-right"></div>
|
||||
<div class='col-sm-1 col-md-1'>
|
||||
<button class="btn btn-sm btn-link">
|
||||
<i ng-show="deleted.indexOf(title) === -1" class="text-danger fa fa-trash"></i>
|
||||
<i ng-show="deleted.indexOf(title) === -1" class="text-danger fa fa-trash-alt"></i>
|
||||
<i ng-show="deleted.indexOf(title) > -1" class="text-success fa fa-undo"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@ -31,7 +31,7 @@
|
||||
<td>{{x.name}} <span ng-if='x.stealth===true' class='text-muted'>(Stealth)</span></td>
|
||||
<td class="text-right">
|
||||
<button ng-show="!x.pending" class="btn btn-sm btn-link" ng-click="removeChannel($index, x); $event.stopPropagation()">
|
||||
<span class="text-danger fa fa-trash"></span>
|
||||
<span class="text-danger fa fa-trash-alt"></span>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user