Merge branch 'dev/1.3.x' into edge

This commit is contained in:
vexorian 2021-03-23 20:02:39 -04:00
commit f12940bcca
39 changed files with 1630 additions and 579 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.2-ubuntu1804
FROM jrottenberg/ffmpeg:4.3-ubuntu1804
EXPOSE 8000
WORKDIR /home/node/app
ENTRYPOINT [ "./dizquetv" ]

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

View File

@ -20,6 +20,7 @@ const constants = require('./src/constants')
const ChannelDB = require("./src/dao/channel-db");
const M3uService = require("./src/services/m3u-service");
const FillerDB = require("./src/dao/filler-db");
const CustomShowDB = require("./src/dao/custom-show-db");
const TVGuideService = require("./src/tv-guide-service");
const EventService = require("./src/services/event-service");
const onShutdown = require("node-graceful-shutdown").onShutdown;
@ -42,11 +43,11 @@ for (let i = 0, l = process.argv.length; i < l; i++) {
process.env.DATABASE = process.argv[i + 1]
}
process.env.DATABASE = process.env.DATABASE || './.dizquetv'
process.env.DATABASE = process.env.DATABASE || path.join(".", ".dizquetv")
process.env.PORT = process.env.PORT || 8000
if (!fs.existsSync(process.env.DATABASE)) {
if (fs.existsSync("./.pseudotv")) {
if (fs.existsSync( path.join(".", ".pseudotv") )) {
throw Error(process.env.DATABASE + " folder not found but ./.pseudotv has been found. Please rename this folder or create an empty " + process.env.DATABASE + " folder so that the program is not confused about.");
}
fs.mkdirSync(process.env.DATABASE)
@ -61,17 +62,22 @@ if(!fs.existsSync(path.join(process.env.DATABASE, 'channels'))) {
if(!fs.existsSync(path.join(process.env.DATABASE, 'filler'))) {
fs.mkdirSync(path.join(process.env.DATABASE, 'filler'))
}
if(!fs.existsSync(path.join(process.env.DATABASE, 'custom-shows'))) {
fs.mkdirSync(path.join(process.env.DATABASE, 'custom-shows'))
}
if(!fs.existsSync(path.join(process.env.DATABASE, 'cache'))) {
fs.mkdirSync(path.join(process.env.DATABASE, 'cache'))
}
if(!fs.existsSync(path.join(process.env.DATABASE, 'cache/images'))) {
fs.mkdirSync(path.join(process.env.DATABASE, 'cache/images'))
if(!fs.existsSync(path.join(process.env.DATABASE, 'cache','images'))) {
fs.mkdirSync(path.join(process.env.DATABASE, 'cache','images'))
}
channelDB = new ChannelDB( path.join(process.env.DATABASE, 'channels') );
fillerDB = new FillerDB( path.join(process.env.DATABASE, 'filler') , channelDB, channelCache );
customShowDB = new CustomShowDB( path.join(process.env.DATABASE, 'custom-shows') );
db.connect(process.env.DATABASE, ['channels', 'plex-servers', 'ffmpeg-settings', 'plex-settings', 'xmltv-settings', 'hdhr-settings', 'db-version', 'client-id', 'cache-images', 'settings'])
fileCache = new FileCacheService( path.join(process.env.DATABASE, 'cache') );
@ -192,16 +198,17 @@ app.get('/version.js', (req, res) => {
res.end();
});
app.use('/images', express.static(path.join(process.env.DATABASE, 'images')))
app.use(express.static(path.join(__dirname, 'web/public')))
app.use(express.static(path.join(__dirname, 'web','public')))
app.use('/images', express.static(path.join(process.env.DATABASE, 'images')))
app.use('/cache/images', cacheImageService.routerInterceptor())
app.use('/cache/images', express.static(path.join(process.env.DATABASE, 'cache/images')))
app.use('/cache/images', express.static(path.join(process.env.DATABASE, 'cache','images')))
app.use('/favicon.svg', express.static(
path.join(__dirname, 'resources/favicon.svg')
path.join(__dirname, 'resources','favicon.svg')
) );
app.use('/custom.css', express.static(path.join(process.env.DATABASE, 'custom.css')))
// API Routers
app.use(api.router(db, channelDB, fillerDB, xmltvInterval, guideService, m3uService, eventService ))
app.use(api.router(db, channelDB, fillerDB, customShowDB, xmltvInterval, guideService, m3uService, eventService ))
app.use('/api/cache/images', cacheImageService.apiRouters())
app.use(video.router( channelDB, fillerDB, db))
@ -243,6 +250,10 @@ function initDB(db, channelDB) {
let data = fs.readFileSync(path.resolve(path.join(__dirname, 'resources/loading-screen.png')))
fs.writeFileSync(process.env.DATABASE + '/images/loading-screen.png', data)
}
if (!fs.existsSync( path.join(process.env.DATABASE, 'custom.css') )) {
let data = fs.readFileSync(path.resolve(path.join(__dirname, 'resources', 'default-custom.css')))
fs.writeFileSync( path.join(process.env.DATABASE, 'custom.css'), data)
}
}

View File

@ -0,0 +1,14 @@
/** For example : */
:root {
--guide-text : #F0F0f0;
--guide-header-even: #423cd4ff;
--guide-header-odd: #262198ff;
--guide-color-a: #212121;
--guide-color-b: #515151;
--guide-color-c: #313131;
--guide-color-d: #414141;
}

View File

@ -24,7 +24,7 @@ function safeString(object) {
}
module.exports = { router: api }
function api(db, channelDB, fillerDB, xmltvInterval, guideService, _m3uService, eventService ) {
function api(db, channelDB, fillerDB, customShowDB, xmltvInterval, guideService, _m3uService, eventService ) {
let m3uService = _m3uService;
const router = express.Router()
const plexServerDB = new PlexServerDB(channelDB, channelCache, db);
@ -413,6 +413,68 @@ function api(db, channelDB, fillerDB, xmltvInterval, guideService, _m3uService,
} );
// Custom Shows
router.get('/api/shows', async (req, res) => {
try {
let fillers = await customShowDB.getAllShowsInfo();
res.send(fillers);
} catch(err) {
console.error(err);
res.status(500).send("error");
}
})
router.get('/api/show/:id', async (req, res) => {
try {
let id = req.params.id;
if (typeof(id) === 'undefined') {
return res.status(400).send("Missing id");
}
let filler = await customShowDB.getShow(id);
if (filler == null) {
return res.status(404).send("Custom show not found");
}
res.send(filler);
} catch(err) {
console.error(err);
res.status(500).send("error");
}
})
router.post('/api/show/:id', async (req, res) => {
try {
let id = req.params.id;
if (typeof(id) === 'undefined') {
return res.status(400).send("Missing id");
}
await customShowDB.saveShow(id, req.body );
return res.status(204).send({});
} catch(err) {
console.error(err);
res.status(500).send("error");
}
})
router.put('/api/show', async (req, res) => {
try {
let uuid = await customShowDB.createShow(req.body );
return res.status(201).send({id: uuid});
} catch(err) {
console.error(err);
res.status(500).send("error");
}
})
router.delete('/api/show/:id', async (req, res) => {
try {
let id = req.params.id;
if (typeof(id) === 'undefined') {
return res.status(400).send("Missing id");
}
await customShowDB.deleteShow(id);
return res.status(204).send({});
} catch(err) {
console.error(err);
res.status(500).send("error");
}
});
// FFMPEG SETTINGS
router.get('/api/ffmpeg-settings', (req, res) => {
try {

135
src/dao/custom-show-db.js Normal file
View File

@ -0,0 +1,135 @@
const path = require('path');
const { v4: uuidv4 } = require('uuid');
let fs = require('fs');
class CustomShowDB {
constructor(folder) {
this.folder = folder;
}
async $loadShow(id) {
let f = path.join(this.folder, `${id}.json` );
try {
return await new Promise( (resolve, reject) => {
fs.readFile(f, (err, data) => {
if (err) {
return reject(err);
}
try {
let j = JSON.parse(data);
j.id = id;
resolve(j);
} catch (err) {
reject(err);
}
})
});
} catch (err) {
console.error(err);
return null;
}
}
async getShow(id) {
return await this.$loadShow(id);
}
async saveShow(id, json) {
if (typeof(id) === 'undefined') {
throw Error("Mising custom show id");
}
let f = path.join(this.folder, `${id}.json` );
await new Promise( (resolve, reject) => {
let data = undefined;
try {
//id is determined by the file name, not the contents
fixup(json);
delete json.id;
data = JSON.stringify(json);
} catch (err) {
return reject(err);
}
fs.writeFile(f, data, (err) => {
if (err) {
return reject(err);
}
resolve();
});
});
}
async createShow(json) {
let id = uuidv4();
fixup(json);
await this.saveShow(id, json);
return id;
}
async deleteShow(id) {
try {
let f = path.join(this.folder, `${id}.json` );
await new Promise( (resolve, reject) => {
fs.unlink(f, function (err) {
if (err) {
return reject(err);
}
resolve();
});
});
} finally {
delete this.cache[id];
}
}
async getAllShowIds() {
return await new Promise( (resolve, reject) => {
fs.readdir(this.folder, function(err, items) {
if (err) {
return reject(err);
}
let fillerIds = [];
for (let i = 0; i < items.length; i++) {
let name = path.basename( items[i] );
if (path.extname(name) === '.json') {
let id = name.slice(0, -5);
fillerIds.push(id);
}
}
resolve (fillerIds);
});
});
}
async getAllShows() {
let ids = await this.getAllShowIds();
return await Promise.all( ids.map( async (c) => this.getShow(c) ) );
}
async getAllShowsInfo() {
//returns just name and id
let fillers = await this.getAllShows();
return fillers.map( (f) => {
return {
'id' : f.id,
'name': f.name,
'count': f.content.length,
}
} );
}
}
function fixup(json) {
if (typeof(json.content) === 'undefined') {
json.content = [];
}
if (typeof(json.name) === 'undefined') {
json.name = "Unnamed Show";
}
}
module.exports = CustomShowDB;

View File

@ -0,0 +1,65 @@
//This is an exact copy of the file with the same now in the web project
//one of these days, we'll figure out how to share the code.
module.exports = function () {
let movieTitleOrder = {};
let movieTitleOrderNumber = 0;
return (program) => {
if ( typeof(program.customShowId) !== 'undefined' ) {
return {
hasShow : true,
showId : "custom." + program.customShowId,
showDisplayName : program.customShowName,
order : program.customOrder,
}
} else if (program.isOffline && program.type === 'redirect') {
return {
hasShow : true,
showId : "redirect." + program.channel,
order : program.duration,
showDisplayName : `Redirect to channel ${program.channel}`,
channel: program.channel,
}
} else if (program.isOffline) {
return {
hasShow : false
}
} else if (program.type === 'movie') {
let key = program.serverKey + "|" + program.key;
if (typeof(movieTitleOrder[key]) === 'undefined') {
movieTitleOrder[key] = movieTitleOrderNumber++;
}
return {
hasShow : true,
showId : "movie.",
showDisplayName : "Movies",
order : movieTitleOrder[key],
}
} else if ( (program.type === 'episode') || (program.type === 'track') ) {
let s = 0;
let e = 0;
if ( typeof(program.season) !== 'undefined') {
s = program.season;
}
if ( typeof(program.episode) !== 'undefined') {
e = program.episode;
}
let prefix = "tv.";
if (program.type === 'track') {
prefix = "audio.";
}
return {
hasShow: true,
showId : prefix + program.showTitle,
showDisplayName : program.showTitle,
order : s * 1000000 + e,
}
} else {
return {
hasShow : false,
}
}
}
}

View File

@ -1,5 +1,5 @@
const constants = require("../constants");
const getShowData = require("./get-show-data")();
const random = require('../helperFuncs').random;
const MINUTE = 60*1000;
@ -8,29 +8,15 @@ const LIMIT = 40000;
//This is a quadruplicate code, but maybe it doesn't have to be?
function getShow(program) {
//used for equalize and frequency tweak
if (program.isOffline) {
if (program.type == 'redirect') {
return {
description : `Redirect to channel ${program.channel}`,
id: "redirect." + program.channel,
channel: program.channel,
}
} else {
return null;
}
} else if ( (program.type == 'episode') && ( typeof(program.showTitle) !== 'undefined' ) ) {
return {
description: program.showTitle,
id: "tv." + program.showTitle,
}
let d = getShowData(program);
if (! d.hasShow) {
return null;
} else {
return {
description: "Movies",
id: "movie.",
}
d.description = d.showDisplayName;
d.id = d.showId;
return d;
}
}
@ -86,19 +72,9 @@ function getShowOrderer(show) {
let sortedPrograms = JSON.parse( JSON.stringify(show.programs) );
sortedPrograms.sort((a, b) => {
if (a.season === b.season) {
if (a.episode > b.episode) {
return 1
} else {
return -1
}
} else if (a.season > b.season) {
return 1;
} else if (b.season > a.season) {
return -1;
} else {
return 0
}
let showA = getShowData(a);
let showB = getShowData(b);
return showA.order - showB.order;
});
let position = 0;
@ -106,9 +82,9 @@ function getShowOrderer(show) {
(position + 1 < sortedPrograms.length )
&&
(
show.founder.season !== sortedPrograms[position].season
||
show.founder.episode !== sortedPrograms[position].episode
getShowData(show.founder).order
!==
getShowData(sortedPrograms[position]).order
)
) {
position++;

View File

@ -1,5 +1,7 @@
const constants = require("../constants");
const getShowData = require("./get-show-data")();
const random = require('../helperFuncs').random;
const MINUTE = 60*1000;
@ -7,34 +9,18 @@ const DAY = 24*60*MINUTE;
const LIMIT = 40000;
//This is a triplicate code, but maybe it doesn't have to be?
function getShow(program) {
//used for equalize and frequency tweak
if (program.isOffline) {
if (program.type == 'redirect') {
return {
description : `Redirect to channel ${program.channel}`,
id: "redirect." + program.channel,
channel: program.channel,
}
} else {
return null;
}
} else if ( (program.type == 'episode') && ( typeof(program.showTitle) !== 'undefined' ) ) {
return {
description: program.showTitle,
id: "tv." + program.showTitle,
}
let d = getShowData(program);
if (! d.hasShow) {
return null;
} else {
return {
description: "Movies",
id: "movie.",
}
d.description = d.showDisplayName;
d.id = d.showId;
return d;
}
}
function shuffle(array, lo, hi ) {
if (typeof(lo) === 'undefined') {
lo = 0;
@ -86,19 +72,9 @@ function getShowOrderer(show) {
let sortedPrograms = JSON.parse( JSON.stringify(show.programs) );
sortedPrograms.sort((a, b) => {
if (a.season === b.season) {
if (a.episode > b.episode) {
return 1
} else {
return -1
}
} else if (a.season > b.season) {
return 1;
} else if (b.season > a.season) {
return -1;
} else {
return 0
}
let showA = getShowData(a);
let showB = getShowData(b);
return showA.order - showB.order;
});
let position = 0;
@ -106,9 +82,9 @@ function getShowOrderer(show) {
(position + 1 < sortedPrograms.length )
&&
(
show.founder.season !== sortedPrograms[position].season
||
show.founder.episode !== sortedPrograms[position].episode
getShowData(show.founder).order
!==
getShowData(sortedPrograms[position]).order
)
) {
position++;
@ -244,6 +220,7 @@ module.exports = async( programs, schedule ) => {
}
}
let show = shows[ showsById[slot.showId] ];
if (slot.showId.startsWith("redirect.")) {
return {
isOffline: true,
@ -259,7 +236,7 @@ module.exports = async( programs, schedule ) => {
}
function advanceSlot(slot) {
if ( (slot.showId === "flex.") || (slot.showId.startsWith("redirect") ) ) {
if ( (slot.showId === "flex.") || (slot.showId.startsWith("redirect.") ) ) {
return;
}
let show = shows[ showsById[slot.showId] ];

View File

@ -448,25 +448,55 @@ function video( channelDB , fillerDB, db) {
res.send(data)
})
let mediaPlayer = async(channelNum, path, req, res) => {
let channel = await channelCache.getChannelConfig(channelDB, channelNum );
if (channel.length === 0) {
res.status(404).send("Channel not found.");
return;
}
res.type('video/x-mpegurl');
res.status(200).send(`#EXTM3U\n${req.protocol}://${req.get('host')}/${path}?channel=${channelNum}\n\n`);
}
router.get('/media-player/:number.m3u', async (req, res) => {
try {
let channelNum = parseInt(req.params.number, 10);
let channel = await channelCache.getChannelConfig(channelDB, channelNum );
if (channel.length === 0) {
res.status(404).send("Channel not found.");
return;
}
res.type('video/x-mpegurl');
let path ="video";
if (req.query.fast==="1") {
path ="m3u8";
}
res.status(200).send(`#EXTM3U\n${req.protocol}://${req.get('host')}/${path}?channel=${channelNum}\n\n`);
return await mediaPlayer(channelNum, path, req, res);
} catch(err) {
console.error(err);
res.status(500).send("There was an error.");
}
});
router.get('/media-player/fast/:number.m3u', async (req, res) => {
try {
let channelNum = parseInt(req.params.number, 10);
let path ="m3u8";
return await mediaPlayer(channelNum, path, req, res);
} catch(err) {
console.error(err);
res.status(500).send("There was an error.");
}
});
router.get('/media-player/radio/:number.m3u', async (req, res) => {
try {
let channelNum = parseInt(req.params.number, 10);
let path ="radio";
return await mediaPlayer(channelNum, path, req, res);
} catch(err) {
console.error(err);
res.status(500).send("There was an error.");
}
});
return router
}

View File

@ -10,6 +10,8 @@ var app = angular.module('myApp', ['ngRoute', 'vs-repeat', 'angularLazyImg', 'dn
app.service('plex', require('./services/plex'))
app.service('dizquetv', require('./services/dizquetv'))
app.service('resolutionOptions', require('./services/resolution-options'))
app.service('getShowData', require('./services/get-show-data'))
app.service('commonProgramTools', require('./services/common-program-tools'))
app.directive('plexSettings', require('./directives/plex-settings'))
app.directive('ffmpegSettings', require('./directives/ffmpeg-settings'))
@ -21,6 +23,7 @@ app.directive('flexConfig', require('./directives/flex-config'))
app.directive('timeSlotsTimeEditor', require('./directives/time-slots-time-editor'))
app.directive('toastNotifications', require('./directives/toast-notifications'))
app.directive('fillerConfig', require('./directives/filler-config'))
app.directive('showConfig', require('./directives/show-config'))
app.directive('deleteFiller', require('./directives/delete-filler'))
app.directive('frequencyTweak', require('./directives/frequency-tweak'))
app.directive('removeShows', require('./directives/remove-shows'))
@ -33,8 +36,11 @@ app.directive('randomSlotsScheduleEditor', require('./directives/random-slots-s
app.controller('settingsCtrl', require('./controllers/settings'))
app.controller('channelsCtrl', require('./controllers/channels'))
app.controller('versionCtrl', require('./controllers/version'))
app.controller('libraryCtrl', require('./controllers/library'))
app.controller('guideCtrl', require('./controllers/guide'))
app.controller('playerCtrl', require('./controllers/player'))
app.controller('fillerCtrl', require('./controllers/filler'))
app.controller('customShowsCtrl', require('./controllers/custom-shows'))
app.config(function ($routeProvider) {
$routeProvider
@ -50,15 +56,27 @@ app.config(function ($routeProvider) {
templateUrl: "views/filler.html",
controller: 'fillerCtrl'
})
.when("/custom-shows", {
templateUrl: "views/custom-shows.html",
controller: 'customShowsCtrl'
})
.when("/library", {
templateUrl: "views/library.html",
controller: 'libraryCtrl'
})
.when("/guide", {
templateUrl: "views/guide.html",
controller: 'guideCtrl'
})
.when("/player", {
templateUrl: "views/player.html",
controller: 'playerCtrl'
})
.when("/version", {
templateUrl: "views/version.html",
controller: 'versionCtrl'
})
.otherwise({
redirectTo: "channels"
redirectTo: "guide"
})
})

View File

@ -0,0 +1,107 @@
module.exports = function ($scope, $timeout, dizquetv) {
$scope.showss = []
$scope.showShowConfig = false
$scope.selectedShow = null
$scope.selectedShowIndex = -1
$scope.refreshShow = async () => {
$scope.shows = [ { id: '?', pending: true} ]
$timeout();
let shows = await dizquetv.getAllShowsInfo();
$scope.shows = shows;
$timeout();
}
$scope.refreshShow();
let feedToShowConfig = () => {};
let feedToDeleteShow = feedToShowConfig;
$scope.registerShowConfig = (feed) => {
feedToShowConfig = feed;
}
$scope.registerDeleteShow = (feed) => {
feedToDeleteShow = feed;
}
$scope.queryChannel = async (index, channel) => {
let ch = await dizquetv.getChannelDescription(channel.number);
ch.pending = false;
$scope.shows[index] = ch;
$scope.$apply();
}
$scope.onShowConfigDone = async (show) => {
if ($scope.selectedChannelIndex != -1) {
$scope.shows[ $scope.selectedChannelIndex ].pending = false;
}
if (typeof show !== 'undefined') {
// not canceled
if ($scope.selectedChannelIndex == -1) { // add new channel
await dizquetv.createShow(show);
} else {
$scope.shows[ $scope.selectedChannelIndex ].pending = true;
await dizquetv.updateShow(show.id, show);
}
await $scope.refreshShow();
}
}
$scope.selectShow = async (index) => {
try {
if ( (index != -1) && $scope.shows[index].pending) {
return;
}
$scope.selectedChannelIndex = index;
if (index === -1) {
feedToShowConfig();
} else {
$scope.shows[index].pending = true;
let f = await dizquetv.getShow($scope.shows[index].id);
feedToShowConfig(f);
$timeout();
}
} catch( err ) {
console.error("Could not fetch show.", err);
}
}
$scope.deleteShow = async (index) => {
try {
if ( $scope.shows[index].pending) {
return;
}
$scope.deleteShowIndex = index;
$scope.shows[index].pending = true;
let id = $scope.shows[index].id;
let channels = await dizquetv.getChannelsUsingShow(id);
feedToDeleteShow( {
id: id,
name: $scope.shows[index].name,
channels : channels,
} );
$timeout();
} catch (err) {
console.error("Could not start delete show dialog.", err);
}
}
$scope.onShowDelete = async( id ) => {
try {
$scope.shows[ $scope.deleteShowIndex ].pending = false;
$timeout();
if (typeof(id) !== 'undefined') {
$scope.shows[ $scope.deleteShowIndex ].pending = true;
await dizquetv.deleteShow(id);
$timeout();
await $scope.refreshShow();
$timeout();
}
} catch (err) {
console.error("Error attempting to delete show", err);
}
}
}

View File

@ -287,6 +287,7 @@ module.exports = function ($scope, $timeout, dizquetv) {
$scope.enableNext = true;
}
let subTitle = undefined;
let episodeTitle = undefined;
let altTitle = hourMinute(ad) + "-" + hourMinute(bd);
if (typeof(program.title) !== 'undefined') {
altTitle = altTitle + " · " + program.title;
@ -303,6 +304,7 @@ module.exports = function ($scope, $timeout, dizquetv) {
}
subTitle = `S${ps} · E${pe}`;
altTitle = altTitle + " " + subTitle;
episodeTitle = program.sub.title;
} else if ( typeof(program.date) === 'undefined' ) {
subTitle = '.';
} else {
@ -313,6 +315,7 @@ module.exports = function ($scope, $timeout, dizquetv) {
altTitle: altTitle,
showTitle: program.title,
subTitle: subTitle,
episodeTitle : episodeTitle,
start: hasStart,
end: hasStop,
} );

View File

@ -0,0 +1,2 @@
module.exports = function () {
}

73
web/controllers/player.js Normal file
View File

@ -0,0 +1,73 @@
module.exports = function ($scope, dizquetv, $timeout) {
$scope.loading = true;
$scope.channelOptions = [
{ id: undefined, description: "Select a channel" },
];
$scope.icons = {};
$scope.endpointOptions = [
{ id: "video", description: "/video - Channel mpegts" },
{ id: "m3u8", description: "/m3u8 - Playlist of individual videos" },
{ id: "radio", description: "/radio - Audio-only channel mpegts" },
];
$scope.selectedEndpoint = "video";
$scope.channel = undefined;
$scope.endpointButtonHref = () => {
if ( $scope.selectedEndpoint == "video") {
return `./media-player/${$scope.channel}.m3u`
} else if ( $scope.selectedEndpoint == "m3u8") {
return `./media-player/fast/${$scope.channel}.m3u`
} else if ( $scope.selectedEndpoint == "radio") {
return `./media-player/radio/${$scope.channel}.m3u`
}
}
$scope.buttonDisabled = () => {
return typeof($scope.channel) === 'undefined';
}
$scope.endpoint = () => {
if ( typeof($scope.channel) === 'undefined' ) {
return "--"
}
let path = "";
if ( $scope.selectedEndpoint == "video") {
path = `/video?channel=${$scope.channel}`
} else if ( $scope.selectedEndpoint == "m3u8") {
path = `/m3u8?channel=${$scope.channel}`
} else if ( $scope.selectedEndpoint == "radio") {
path= `/radio?channel=${$scope.channel}`
}
return window.location.href.replace("/#!/player", path);
}
let loadChannels = async() => {
let channelNumbers = await dizquetv.getChannelNumbers();
try {
await Promise.all( channelNumbers.map( async(x) => {
let desc = await dizquetv.getChannelDescription(x);
let option = {
id: x,
description: `${x} - ${desc.name}`,
};
$scope.channelOptions.push( option );
$scope.icons[x] = desc.icon;
}) );
$scope.channelOptions.sort( (a,b) => {
let za = ( (typeof(a.id) === undefined)?-1:a.id);
let zb = ( (typeof(b.id) === undefined)?-1:b.id);
return za - zb;
} );
$scope.loading = false;
$scope.$apply();
} catch (err) {
console.error(err);
}
$timeout( () => $scope.$apply(), 0);
}
loadChannels();
}

View File

@ -1,4 +1,4 @@
module.exports = function ($timeout, $location, dizquetv, resolutionOptions) {
module.exports = function ($timeout, $location, dizquetv, resolutionOptions, getShowData, commonProgramTools) {
return {
restrict: 'E',
templateUrl: 'templates/channel-config.html',
@ -293,39 +293,7 @@ module.exports = function ($timeout, $location, dizquetv, resolutionOptions) {
})
scope.sortShows = () => {
scope.removeOffline();
let shows = {}
let movies = []
let newProgs = []
let progs = scope.channel.programs
for (let i = 0, l = progs.length; i < l; i++) {
if ( progs[i].isOffline || (progs[i].type === 'movie') ) {
movies.push(progs[i])
} else {
if (typeof shows[progs[i].showTitle] === 'undefined')
shows[progs[i].showTitle] = []
shows[progs[i].showTitle].push(progs[i])
}
}
let keys = Object.keys(shows)
for (let i = 0, l = keys.length; i < l; i++) {
shows[keys[i]].sort((a, b) => {
if (a.season === b.season) {
if (a.episode > b.episode) {
return 1
} else {
return -1
}
} else if (a.season > b.season) {
return 1;
} else if (b.season > a.season) {
return -1;
} else {
return 0
}
})
newProgs = newProgs.concat(shows[keys[i]])
}
scope.channel.programs = newProgs.concat(movies)
scope.channel.programs = commonProgramTools.sortShows(scope.channel.programs);
updateChannelDuration()
}
scope.dateForGuide = (date) => {
@ -345,43 +313,9 @@ module.exports = function ($timeout, $location, dizquetv, resolutionOptions) {
}
scope.sortByDate = () => {
scope.removeOffline();
scope.channel.programs.sort( (a,b) => {
let aHas = ( typeof(a.date) !== 'undefined' );
let bHas = ( typeof(b.date) !== 'undefined' );
if (!aHas && !bHas) {
return 0;
} else if (! aHas) {
return 1;
} else if (! bHas) {
return -1;
}
if (a.date < b.date ) {
return -1;
} else if (a.date > b.date) {
return 1;
} else {
let aHasSeason = ( typeof(a.season) !== 'undefined' );
let bHasSeason = ( typeof(b.season) !== 'undefined' );
if (! aHasSeason && ! bHasSeason) {
return 0;
} else if (! aHasSeason) {
return 1;
} else if (! bHasSeason) {
return -1;
}
if (a.season < b.season) {
return -1;
} else if (a.season > b.season) {
return 1;
} else if (a.episode < b.episode) {
return -1;
} else if (a.episode > b.episode) {
return 1;
} else {
return 0;
}
}
});
scope.channel.programs = commonProgramTools.sortByDate(
scope.channel.programs
);
updateChannelDuration()
}
scope.slideAllPrograms = (offset) => {
@ -397,26 +331,8 @@ module.exports = function ($timeout, $location, dizquetv, resolutionOptions) {
adjustStartTimeToCurrentProgram();
updateChannelDuration();
}
let removeDuplicatesSub = (progs) => {
let tmpProgs = {}
for (let i = 0, l = progs.length; i < l; i++) {
if ( progs[i].type ==='redirect' ) {
tmpProgs['_redirect ' + progs[i].channel + ' _ '+ progs[i].duration ] = progs[i];
} else if (progs[i].type === 'movie') {
tmpProgs[progs[i].title + progs[i].durationStr] = progs[i]
} else {
tmpProgs[progs[i].showTitle + '-' + progs[i].season + '-' + progs[i].episode] = progs[i]
}
}
let newProgs = []
let keys = Object.keys(tmpProgs)
for (let i = 0, l = keys.length; i < l; i++) {
newProgs.push(tmpProgs[keys[i]])
}
return newProgs;
}
scope.removeDuplicates = () => {
scope.channel.programs = removeDuplicatesSub(scope.channel.programs);
scope.channel.programs = commonProgramTools.removeDuplicates(scope.channel.programs);
updateChannelDuration(); //oops someone forgot to add this
}
scope.removeOffline = () => {
@ -432,42 +348,37 @@ module.exports = function ($timeout, $location, dizquetv, resolutionOptions) {
}
scope.wipeSpecials = () => {
let tmpProgs = []
let progs = scope.channel.programs
for (let i = 0, l = progs.length; i < l; i++) {
if (progs[i].season !== 0) {
tmpProgs.push(progs[i]);
}
}
scope.channel.programs = tmpProgs
scope.channel.programs =commonProgramTools.removeSpecials(scope.channel.programs);
updateChannelDuration()
}
scope.getShowTitle = (program) => {
if (program.isOffline && program.type == 'redirect') {
return `Redirect to channel ${program.channel}`;
} else {
return program.showTitle;
}
}
scope.startRemoveShows = () => {
scope._removablePrograms = scope.channel.programs
.map(scope.getShowTitle)
.reduce((dedupedArr, showTitle) => {
if (!dedupedArr.includes(showTitle)) {
dedupedArr.push(showTitle)
let seenIds = {};
let rem = [];
scope.channel.programs
.map( getShowData )
.filter( data => data.hasShow )
.forEach( x => {
if ( seenIds[x.showId] !== true) {
seenIds[x.showId] = true;
rem.push( {
id: x.showId,
displayName : x.showDisplayName
} );
}
return dedupedArr
}, [])
.filter(showTitle => !!showTitle);
} );
scope._removablePrograms = rem;
scope._deletedProgramNames = [];
}
scope.removeShows = (deletedShowNames) => {
scope.removeShows = (deletedShowIds) => {
const p = scope.channel.programs;
let set = {};
deletedShowNames.forEach( (a) => set[a] = true );
scope.channel.programs = p.filter( (a) => (set[scope.getShowTitle(a)]!==true) );
deletedShowIds.forEach( (a) => set[a] = true );
scope.channel.programs = p.filter( (a) => {
let data = getShowData(a);
return ( ! data.hasShow || ! set[ data.showId ] );
} );
updateChannelDuration();
}
scope.describeFallback = () => {
@ -485,107 +396,15 @@ module.exports = function ($timeout, $location, dizquetv, resolutionOptions) {
}
}
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') ) {
background = "rgb(255, 255, 255)";
} else {
let r = 0, g = 0, b = 0, r2=0, g2=0,b2=0;
let angle = 45;
let w = 3;
if (program.type === 'redirect') {
angle = 0;
w = 4 + (program.channel % 10);
let c = (program.channel * 100019);
//r = 255, g = 0, b = 0;
//r2 = 0, g2 = 0, b2 = 255;
r = ( (c & 3) * 77 );
g = ( ( (c >> 1) & 3) * 77 );
b = ( ( (c >> 2) & 3) * 77 );
r2 = ( ( (c >> 5) & 3) * 37 );
g2 = ( ( (c >> 3) & 3) * 37 );
b2 = ( ( (c >> 4) & 3) * 37 );
} else if (program.type === 'episode') {
let h = Math.abs(scope.getHashCode(program.showTitle, false));
let h2 = Math.abs(scope.getHashCode(program.showTitle, true));
r = h % 256;
g = (h / 256) % 256;
b = (h / (256*256) ) % 256;
r2 = (h2 / (256*256) ) % 256;
g2 = (h2 / (256*256) ) % 256;
b2 = (h2 / (256*256) ) % 256;
angle = (360 - 90 + h % 180) % 360;
if ( angle >= 350 || angle < 10 ) {
angle += 53;
}
} else if (program.type === 'track') {
r = 10, g = 10, b = 10;
r2 = 245, g2 = 245, b2 = 245;
angle = 315;
w = 2;
} else {
r = 10, g = 10, b = 10;
r2 = 245, g2 = 245, b2 = 245;
angle = 45;
w = 6;
}
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 f = interpolate;
let w = 15.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': `${a}%`,
'height': '1.3em',
'margin-right': `${b}%`,
'background': background,
'border': '1px solid black',
'margin-top': "0.01em",
'margin-bottom': '1px',
};
scope.getProgramDisplayTitle = (x) => {
return commonProgramTools.getProgramDisplayTitle(x);
}
scope.getHashCode = (s, rev) => {
var hash = 0;
if (s.length == 0) return hash;
let inc = 1, st = 0, e = s.length;
if (rev) {
inc = -1, st = e - 1, e = -1;
}
for (var i = st; i != e; i+= inc) {
hash = s.charCodeAt(i) + ((hash << 5) - hash);
hash = hash & hash; // Convert to 32bit integer
}
return hash;
scope.programSquareStyle = (x) => {
return commonProgramTools.programSquareStyle(x);
}
scope.doReruns = (rerunStart, rerunBlockSize, rerunRepeats) => {
let o =(new Date()).getTimezoneOffset() * 60 * 1000;
let start = (o + rerunStart * 60 * 60 * 1000) % (24*60*60*1000);
@ -726,13 +545,11 @@ module.exports = function ($timeout, $location, dizquetv, resolutionOptions) {
};
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;
let data = getShowData( array[i] );
if (data.hasShow) {
let key = data.showId;
if (typeof(scope.episodeMemory[key]) === 'undefined') {
scope.episodeMemory[key] = {
season: array[i].season,
episode: array[i].episode,
}
scope.episodeMemory[key] = data.order;
}
}
}
@ -747,11 +564,11 @@ module.exports = function ($timeout, $location, dizquetv, resolutionOptions) {
// 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 data = getShowData(vid);
if (data.hasShow) {
let countKey = {
title: vid.showTitle,
s: vid.season,
e: vid.episode,
id: data.showId,
order: data.order,
}
let key = JSON.stringify(countKey);
let c = ( (typeof(counts[key]) === 'undefined') ? 0 : counts[key] );
@ -760,10 +577,10 @@ module.exports = function ($timeout, $location, dizquetv, resolutionOptions) {
c: c,
it: vid
}
if ( typeof(shows[vid.showTitle]) === 'undefined') {
shows[vid.showTitle] = [];
if ( typeof(shows[data.showId]) === 'undefined') {
shows[data.showId] = [];
}
shows[vid.showTitle].push(showEntry);
shows[data.showId].push(showEntry);
}
}
//this is O(|N| log|M|) where |N| is the total number of TV
@ -773,15 +590,7 @@ module.exports = function ($timeout, $location, dizquetv, resolutionOptions) {
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;
}
return getShowData(a.it).order - getShowData(b.it).order;
} else {
return (a.c < b.c)? -1: 1;
}
@ -790,8 +599,7 @@ module.exports = function ($timeout, $location, dizquetv, resolutionOptions) {
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)
getShowData(shows[key][i].it).order == scope.episodeMemory[key]
) {
next[key] = i;
break;
@ -800,13 +608,14 @@ module.exports = function ($timeout, $location, dizquetv, resolutionOptions) {
}
});
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];
let data = getShowData( array[i] );
if (data.hasShow) {
let key = data.showId;
var sequence = shows[key];
let j = next[key];
array[i] = sequence[j].it;
next[title] = (j + 1) % sequence.length;
next[key] = (j + 1) % sequence.length;
}
}
scope.channel.programs = array;
@ -888,18 +697,23 @@ module.exports = function ($timeout, $location, dizquetv, resolutionOptions) {
let newProgs = []
let progs = scope.channel.programs
for (let i = 0, l = progs.length; i < l; i++) {
if (progs[i].type === 'movie') {
let data = getShowData(progs[i]);
if (! data.hasShow) {
continue;
} else if (data.showId === 'movie.') {
movies.push(progs[i])
} else {
if (typeof shows[progs[i].showTitle] === 'undefined')
shows[progs[i].showTitle] = []
shows[progs[i].showTitle].push(progs[i])
if (typeof shows[data.showId] === 'undefined') {
shows[data.showId] = [];
}
shows[data.showId].push(progs[i])
}
}
let keys = Object.keys(shows)
let index = 0
if (randomize)
index = getRandomInt(0, keys.length - 1)
if (randomize) {
index = getRandomInt(0, keys.length - 1);
}
while (keys.length > 0) {
if (shows[keys[index]].length === 0) {
keys.splice(index, 1)
@ -933,12 +747,17 @@ module.exports = function ($timeout, $location, dizquetv, resolutionOptions) {
updateChannelDuration()
}
scope.randomShuffle = () => {
shuffle(scope.channel.programs)
commonProgramTools.shuffle(scope.channel.programs);
updateChannelDuration()
}
scope.cyclicShuffle = () => {
cyclicShuffle(scope.channel.programs);
updateChannelDuration();
// cyclic shuffle can be reproduced by simulating the effects
// of save and recover positions.
let oldSaved = scope.episodeMemory;
commonProgramTools.shuffle(scope.channel.programs);
scope.savePositions();
scope.recoverPositions();
scope.episodeMemory = oldSaved;
}
scope.equalizeShows = () => {
scope.removeDuplicates();
@ -947,9 +766,12 @@ module.exports = function ($timeout, $location, dizquetv, resolutionOptions) {
}
scope.startFrequencyTweak = () => {
let programs = {};
let displayName = {};
for (let i = 0; i < scope.channel.programs.length; i++) {
if ( !scope.channel.programs[i].isOffline || (scope.channel.programs[i].type === 'redirect') ) {
let c = getShowCode(scope.channel.programs[i]);
let data = getShowData( scope.channel.programs[i] );
if ( data.hasShow ) {
let c = data.showId;
displayName[c] = data.showDisplayName;
if ( typeof(programs[c]) === 'undefined') {
programs[c] = 0;
}
@ -967,11 +789,10 @@ module.exports = function ($timeout, $location, dizquetv, resolutionOptions) {
name : key,
weight: w,
specialCategory: false,
displayName: key,
displayName: displayName[key],
}
if (key.startsWith("_internal.")) {
if (! key.startsWith("tv.")) {
obj.specialCategory = true;
obj.displayName = key.slice("_internal.".length);
}
arr.push(obj);
});
@ -1011,16 +832,7 @@ module.exports = function ($timeout, $location, dizquetv, resolutionOptions) {
}
function getShowCode(program) {
//used for equalize and frequency tweak
let showName = "_internal.Unknown";
if ( program.isOffline && (program.type == 'redirect') ) {
showName = `Redirect to channel ${program.channel}`;
} else if ( (program.type == 'episode') && ( typeof(program.showTitle) !== 'undefined' ) ) {
showName = program.showTitle;
} else {
showName = "_internal.Movies";
}
return showName;
return getShowData(program).showId;
}
function getRandomInt(min, max) {
@ -1028,21 +840,6 @@ module.exports = function ($timeout, $location, dizquetv, resolutionOptions) {
max = Math.floor(max)
return Math.floor(Math.random() * (max - min + 1)) + min
}
function shuffle(array, lo, hi ) {
if (typeof(lo) === 'undefined') {
lo = 0;
hi = array.length;
}
let currentIndex = hi, temporaryValue, randomIndex
while (lo !== currentIndex) {
randomIndex = lo + Math.floor(Math.random() * (currentIndex -lo) );
currentIndex -= 1
temporaryValue = array[currentIndex]
array[currentIndex] = array[randomIndex]
array[randomIndex] = temporaryValue
}
return array
}
function equalizeShows(array, freqObject) {
let shows = {};
let progs = [];
@ -1104,79 +901,17 @@ module.exports = function ($timeout, $location, dizquetv, resolutionOptions) {
updateChannelDuration();
}
scope.shuffleReplicate =(t) => {
shuffle( scope.channel.programs );
commonProgramTools.shuffle( scope.channel.programs );
let n = scope.channel.programs.length;
let a = Math.floor(n / 2);
scope.replicate(t);
for (let i = 0; i < t; i++) {
shuffle( scope.channel.programs, n*i, n*i + a);
shuffle( scope.channel.programs, n*i + a, n*i + n);
commonProgramTools.shuffle( scope.channel.programs, n*i, n*i + a);
commonProgramTools.shuffle( scope.channel.programs, n*i + a, n*i + n);
}
updateChannelDuration();
}
function cyclicShuffle(array) {
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: array[i],
}
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] = Math.floor( Math.random() * shows[key].length );
});
shuffle(array);
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;
}
}
return array
}
scope.updateChannelDuration = updateChannelDuration
function updateChannelDuration() {
scope.showRotatedNote = false;
@ -1882,11 +1617,11 @@ module.exports = function ($timeout, $location, dizquetv, resolutionOptions) {
scope.onTimeSlotsButtonClick = () => {
let progs = removeDuplicatesSub( scope.channel.programs );
let progs = commonProgramTools.removeDuplicates( scope.channel.programs );
scope.timeSlots.startDialog( progs, scope.maxSize, scope.channel.scheduleBackup );
}
scope.onRandomSlotsButtonClick = () => {
let progs = removeDuplicatesSub( scope.channel.programs );
let progs = commonProgramTools.removeDuplicates( scope.channel.programs );
scope.randomSlots.startDialog(progs, scope.maxSize, scope.channel.randomScheduleBackup );
}

View File

@ -33,6 +33,7 @@ module.exports = function ($timeout) {
z--;
}
scope.content.splice(z, 0, program );
refreshContentIndexes();
$timeout();
return false;
}

View File

@ -1,4 +1,4 @@
module.exports = function (plex, dizquetv, $timeout) {
module.exports = function (plex, dizquetv, $timeout, commonProgramTools) {
return {
restrict: 'E',
templateUrl: 'templates/plex-library.html',
@ -14,6 +14,9 @@ module.exports = function (plex, dizquetv, $timeout) {
if ( typeof(scope.limit) == 'undefined') {
scope.limit = 1000000000;
}
scope.customShows = [];
scope.origins = [];
scope.currentOrigin = undefined;
scope.pending = 0;
scope.allowedIndexes = [];
for (let i = -10; i <= -1; i++) {
@ -25,9 +28,14 @@ module.exports = function (plex, dizquetv, $timeout) {
$timeout(resolve,t);
});
}
scope.selectServer = function (server) {
scope.plexServer = server
updateLibrary(server)
scope.selectOrigin = function (origin) {
if ( origin.type === 'plex' ) {
scope.plexServer = origin.server;
updateLibrary(scope.plexServer);
} else {
scope.plexServer = undefined;
updateCustomShows();
}
}
scope._onFinish = (s) => {
if (s.length > scope.limit) {
@ -78,30 +86,41 @@ module.exports = function (plex, dizquetv, $timeout) {
scope.$apply()
}
}
dizquetv.getPlexServers().then((servers) => {
if (servers.length === 0) {
scope.noServers = true
return
}
scope.plexServers = servers
scope.plexServer = servers[0]
scope.origins = servers.map( (s) => {
return {
"type" : "plex",
"name" : `Plex - ${s.name}`,
"server": s,
}
} );
scope.currentOrigin = scope.origins[0];
scope.plexServer = scope.currentOrigin.server;
scope.origins.push( {
"type": "dizquetv",
"name" : "dizqueTV - Custom Shows",
} );
updateLibrary(scope.plexServer)
})
function updateLibrary(server) {
plex.getLibrary(server).then((lib) => {
plex.getPlaylists(server).then((play) => {
for (let i = 0, l = play.length; i < l; i++)
play[i].type = 'playlist'
let updateLibrary = async(server) => {
let lib = await plex.getLibrary(server);
let play = await plex.getPlaylists(server);
play.forEach( p => {
p.type = "playlist";
} );
scope.$apply(() => {
scope.libraries = lib
if (play.length > 0)
scope.libraries.push({ title: "Playlists", key: "", icon: "", nested: play })
})
})
}, (err) => {
console.log(err)
})
}
scope.fillNestedIfNecessary = async (x, isLibrary) => {
if ( (typeof(x.nested) === 'undefined') && (x.type !== 'collection') ) {
@ -174,6 +193,32 @@ module.exports = function (plex, dizquetv, $timeout) {
scope.createShowIdentifier = (season, ep) => {
return 'S' + (season.toString().padStart(2, '0')) + 'E' + (ep.toString().padStart(2, '0'))
}
scope.addCustomShow = async(show) => {
scope.pending++;
try {
show = await dizquetv.getShow(show.id);
for (let i = 0; i < show.content.length; i++) {
let item = JSON.parse(angular.toJson( show.content[i] ));
item.customShowId = show.id;
item.customShowName = show.name;
item.customOrder = i;
scope.selection.push(item);
}
scope.$apply();
} finally {
scope.pending--;
}
}
scope.getProgramDisplayTitle = (x) => {
return commonProgramTools.getProgramDisplayTitle(x);
}
let updateCustomShows = async() => {
scope.customShows = await dizquetv.getAllShowsInfo();
scope.$apply();
}
}
};
}

View File

@ -1,5 +1,5 @@
module.exports = function ($timeout, dizquetv) {
module.exports = function ($timeout, dizquetv, getShowData) {
const MINUTE = 60*1000;
const HOUR = 60*MINUTE;
const DAY = 24*HOUR;
@ -306,33 +306,17 @@ module.exports = function ($timeout, dizquetv) {
}
};
}
function getShow(program) {
//This is a duplicate code, but maybe it doesn't have to be?
function getShow(program) {
//used for equalize and frequency tweak
if (program.isOffline) {
if (program.type == 'redirect') {
return {
description : `Redirect to channel ${program.channel}`,
id: "redirect." + program.channel,
channel: program.channel,
}
} else {
let d = getShowData(program);
if (! d.hasShow) {
return null;
}
} else if ( (program.type == 'episode') && ( typeof(program.showTitle) !== 'undefined' ) ) {
return {
description: program.showTitle,
id: "tv." + program.showTitle,
}
} else {
return {
description: "Movies",
id: "movie.",
} else {
d.description = d.showDisplayName;
d.id = d.showId;
return d;
}
}
}
}

View File

@ -4,23 +4,23 @@ module.exports = function ($timeout) {
templateUrl: 'templates/remove-shows.html',
replace: true,
scope: {
programTitles: "=programTitles",
programInfos: "=programInfos",
visible: "=visible",
onDone: "=onDone",
deleted: "=deleted"
},
link: function (scope, element, attrs) {
scope.toggleShowDeletion = (programTitle) => {
const deletedIdx = scope.deleted.indexOf(programTitle);
scope.toggleShowDeletion = (programId) => {
const deletedIdx = scope.deleted.indexOf(programId);
if (deletedIdx === -1) {
scope.deleted.push(programTitle);
scope.deleted.push(programId);
} else {
scope.deleted.splice(deletedIdx, 1);
}
}
scope.finished = () => {
const d = scope.deleted;
scope.programTitles = null;
scope.programInfos = null;
scope.deleted = null;
scope.onDone(d);
}

View File

@ -0,0 +1,165 @@
module.exports = function ($timeout, commonProgramTools) {
return {
restrict: 'E',
templateUrl: 'templates/show-config.html',
replace: true,
scope: {
linker: "=linker",
onDone: "=onDone"
},
link: function (scope, element, attrs) {
scope.showTools = false;
scope.showPlexLibrary = false;
scope.content = [];
scope.visible = false;
scope.error = undefined;
function refreshContentIndexes() {
for (let i = 0; i < scope.content.length; i++) {
scope.content[i].$index = i;
}
}
scope.contentSplice = (a,b) => {
scope.content.splice(a,b)
refreshContentIndexes();
}
scope.dropFunction = (dropIndex, program) => {
let y = program.$index;
let z = dropIndex + scope.currentStartIndex - 1;
scope.content.splice(y, 1);
if (z >= y) {
z--;
}
scope.content.splice(z, 0, program );
refreshContentIndexes();
$timeout();
return false;
}
scope.setUpWatcher = function setupWatchers() {
this.$watch('vsRepeat.startIndex', function(val) {
scope.currentStartIndex = val;
});
};
scope.movedFunction = (index) => {
console.log("movedFunction(" + index + ")");
}
scope.linker( (show) => {
if ( typeof(show) === 'undefined') {
scope.name = "";
scope.content = [];
scope.id = undefined;
scope.title = "Create Custom Show";
} else {
scope.name = show.name;
scope.content = show.content;
scope.id = show.id;
scope.title = "Edit Custom Show";
}
refreshContentIndexes();
scope.visible = true;
} );
scope.finished = (cancelled) => {
if (cancelled) {
scope.visible = false;
return scope.onDone();
}
if ( (typeof(scope.name) === 'undefined') || (scope.name.length == 0) ) {
scope.error = "Please enter a name";
}
if ( scope.content.length == 0) {
scope.error = "Please add at least one clip.";
}
if (typeof(scope.error) !== 'undefined') {
$timeout( () => {
scope.error = undefined;
}, 30000);
return;
}
scope.visible = false;
scope.onDone( {
name: scope.name,
content: scope.content.map( (c) => {
delete c.$index
return c;
} ),
id: scope.id,
} );
}
scope.showList = () => {
return ! scope.showPlexLibrary;
}
scope.sortShows = () => {
scope.content = commonProgramTools.sortShows(scope.content);
refreshContentIndexes();
}
scope.sortByDate = () => {
scope.content = commonProgramTools.sortByDate(scope.content);
refreshContentIndexes();
}
scope.shuffleShows = () => {
scope.content = commonProgramTools.shuffle(scope.content);
refreshContentIndexes();
}
scope.showRemoveAllShow = () => {
scope.content = [];
refreshContentIndexes();
}
scope.showRemoveDuplicates = () => {
scope.content = commonProgramTools.removeDuplicates(scope.content);
refreshContentIndexes();
}
scope.getProgramDisplayTitle = (x) => {
return commonProgramTools.getProgramDisplayTitle(x);
}
scope.removeSpecials = () => {
scope.content = commonProgramTools.removeSpecials(scope.content);
refreshContentIndexes();
}
scope.importPrograms = (selectedPrograms) => {
for (let i = 0, l = selectedPrograms.length; i < l; i++) {
selectedPrograms[i].commercials = []
}
scope.content = scope.content.concat(selectedPrograms);
refreshContentIndexes();
scope.showPlexLibrary = false;
}
scope.durationString = (duration) => {
var date = new Date(0);
date.setSeconds( Math.floor(duration / 1000) ); // specify value for SECONDS here
return date.toISOString().substr(11, 8);
}
let interpolate = ( () => {
let h = 60*60*1000 / 6;
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 = (x) => {
return commonProgramTools.programSquareStyle(x);
}
}
};
}

View File

@ -1,5 +1,5 @@
module.exports = function ($timeout, dizquetv) {
module.exports = function ($timeout, dizquetv, getShowData ) {
const DAY = 24*60*60*1000;
const WEEK = 7 * DAY;
const WEEK_DAYS = [ "Thursday", "Friday", "Saturday", "Sunday", "Monday", "Tuesday", "Wednesday" ];
@ -329,6 +329,22 @@ module.exports = function ($timeout, dizquetv) {
}
};
function getShow(program) {
let d = getShowData(program);
if (! d.hasShow) {
return null;
} else {
d.description = d.showDisplayName;
d.id = d.showId;
return d;
}
}
}
function niceLookingTime(t) {
@ -338,30 +354,3 @@ function niceLookingTime(t) {
return d.toLocaleTimeString( [] , {timeZone: 'UTC' } );
}
//This is a duplicate code, but maybe it doesn't have to be?
function getShow(program) {
//used for equalize and frequency tweak
if (program.isOffline) {
if (program.type == 'redirect') {
return {
description : `Redirect to channel ${program.channel}`,
id: "redirect." + program.channel,
channel: program.channel,
}
} else {
return null;
}
} else if ( (program.type == 'episode') && ( typeof(program.showTitle) !== 'undefined' ) ) {
return {
description: program.showTitle,
id: "tv." + program.showTitle,
}
} else {
return {
description: "Movies",
id: "movie.",
}
}
}

View File

@ -7,6 +7,7 @@
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css">
<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">
<link href="custom.css" rel="stylesheet">
<script src="version.js"></script>
<script src="bundle.js"></script>
</head>
@ -32,7 +33,7 @@
</a>
</small>
</h1>
<a href="#!/guide">Guide</a> - <a href="#!/channels">Channels</a> - <a href="#!/filler">Filler</a> - <a href="#!/settings">Settings</a> - <a href="#!/version">Version</a>
<a href="#!/guide">Guide</a> - <a href="#!/channels">Channels</a> - <a href="#!/library">Library</a> - <a href="#!/player">Player</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="far fa-file-code"></span></a>

View File

@ -1,3 +1,14 @@
:root {
--guide-text : #F0F0f0;
--guide-header-even: #423cd4ff;
--guide-header-odd: #262198ff;
--guide-color-a: #212121;
--guide-color-b: #515151;
--guide-color-c: #313131;
--guide-color-d: #414141;
}
.pull-right { float: right; }
.modal-semi-body {
@ -5,14 +16,6 @@
flex: 1 1 auto;
}
.commercials-panel {
background-color: rgb(70, 70, 70);
border-top: 1px solid #daa104;
border-left-color: #daa104;
border-right-color: #daa104;
color: white
}
.plex-panel {
margin: 0;
padding: 0;
@ -27,25 +30,15 @@
padding-right: 0.2em
}
.list-group-item-video {
background-color: rgb(70, 70, 70);
border-top: 1px solid #daa104;
border-left-color: #daa104;
border-right-color: #daa104;
color: white
}
.list-group-item-video .fa-plus-circle {
.fa-plus-circle {
color: #daa104;
}
.list-group-item-video:hover .fa-plus-circle {
.fa-plus-circle {
color: #000;
}
.list-group-item-video:hover {
background-color: #daa104;
color: #000 !important;
}
.list-group.list-group-root .list-group-item {
border-radius: 0;
border-width: 1px 0 0 0;
@ -157,8 +150,7 @@ table.tvguide {
position: sticky;
top: 0;
bottom: 0;
background: white;
border-bottom: 1px solid black;
/*border-bottom: 1px solid black;*/
}
.tvguide th.guidenav {
@ -168,7 +160,7 @@ table.tvguide {
.tvguide td, .tvguide th {
color: #F0F0f0;
color: var(--guide-text);
border-top: 0;
height: 3.5em;
padding-top: 0;
@ -208,27 +200,27 @@ table.tvguide {
.tvguide th.even {
background: #423cd4ff;
background: var(--guide-header-even);
}
.tvguide th.odd {
background: #262198ff;
background: var(--guide-header-odd);
}
.tvguide tr.odd td.even {
background: #212121;
background: var(--guide-color-a);
}
.tvguide tr.odd td.odd {
background: #515151;;
background: var(--guide-color-b);
}
.tvguide tr.even td.odd {
background: #313131
background: var(--guide-color-c);
}
.tvguide tr.even td.even {
background: #414141;
background: var(--guide-color-d) ;
}
.tvguide td .play-channel {
@ -254,17 +246,21 @@ table.tvguide {
text-align: right;
}
.filler-list .list-group-item, .program-row {
.filler-list .list-group-item, .program-row, .show-list .list-group-item, .program-row {
min-height: 1.5em;
}
.filler-list .list-group-item .title, .program-row .title {
.filler-list .list-group-item .title, .program-row .title, .show-list .list-group-item .title, .program-row .title {
margin-right: 5px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.show-row .program-start {
width: 2em;
}
div.channel-tools {
max-height: 20em;
overflow-y: scroll;
@ -315,7 +311,7 @@ div.programming-programs div.list-group-item {
}
.program-row:nth-child(odd), .filler-row:nth-child(odd), .channel-row:nth-child(odd) {
.program-row:nth-child(odd), .show-row:nth-child(odd), .filler-row:nth-child(odd), .channel-row:nth-child(odd) {
background-color: #eeeeee;
}

View File

@ -181,7 +181,7 @@
<div ng-style="programSquareStyle(x)" />
<div ng-hidden="x.isOffline" class='title' >
{{ x.type === 'episode' ? x.showTitle + ' - S' + x.season.toString().padStart(2, '0') + 'E' + x.episode.toString().padStart(2, '0') : x.title}}
{{ getProgramDisplayTitle(x) }}
</div>
<div style="font-weight:ligther" ng-show="x.isOffline" class='title' >
<i ng-if="x.type !== 'redirect' " >Flex</i>
@ -601,7 +601,7 @@
<h5 style="margin-top: 10px;">Filler</h5>
<div>
<label>Minimum time before replaying a filler (Minutes): </label>
<input type="number" class="form-control form-control-sm" ng-model="channel.fillerRepeatCooldownMinutes" ng-pattern="/^([1-9][0-9]*)$/" min='0' max='1440' />
<input type="number" class="form-control form-control-sm" ng-model="channel.fillerRepeatCooldownMinutes" ng-pattern="/^([1-9][0-9]*)$/" min='0' max='10080' />
<span class="text-danger pull-right">{{error.blockRepeats}}</span>
</div>
@ -627,7 +627,7 @@
<div class='form-group col-md-2' ng-if="x.id !== &apos;none&apos; " >
<label ng-if="$index==0" for="cooldown{{$index}}">Cooldown (minutes)</label>
<input class='form-control' id="cooldown{{$index}}" type='number' ng-model='x.cooldownMinutes' ng-pattern="/^([0-9][0-9]*)$/"
min='0' max='1440'
min='0' max='10080'
data-toggle="tooltip" data-placement="bottom" title="The channel won&apos;t pick a video from this list if it played something from this list less than this amount of minutes ago."
> </input>
</div>
@ -856,7 +856,7 @@
<program-config program="_selectedProgram" on-done="finshedProgramEdit"></program-config>
<flex-config offline-title="Modify Flex Time" program="_selectedOffline" on-done="finishedOfflineEdit"></flex-config>
<frequency-tweak programs="_programFrequencies" message="_frequencyMessage" modified="_frequencyModified" on-done="tweakFrequencies"></frequency-tweak>
<remove-shows program-titles="_removablePrograms" on-done="removeShows" deleted="_deletedProgramNames"></remove-shows>
<remove-shows program-infos="_removablePrograms" on-done="removeShows" deleted="_deletedProgramNames"></remove-shows>
<flex-config offline-title="Add Flex Time" program="_addingOffline" on-done="finishedAddingOffline"></flex-config>
<plex-library limit="libraryLimit" height="300" visible="displayPlexLibrary" on-finish="importPrograms"></plex-library>
<plex-library height="300" limit=1 visible="showFallbackPlexLibrary" on-finish="importFallback"></plex-library>

View File

@ -3,7 +3,7 @@
<div class="modal-dialog modal-dialog-scrollable modal-xl" role="document">
<div class="modal-content" ng-if="noServers">
<div class="modal-header">
<h5 class="modal-title">Plex Library</h5>
<h5 class="modal-title">Library</h5>
</div>
<div class="model-body">
<br/>
@ -21,17 +21,17 @@
<div class="modal-content" ng-if="!noServers">
<div class="modal-header">
<h5 class="modal-title">Plex Library</h5>
<h5 class="modal-title">Library</h5>
<span class="pull-right">
<label class="small" for="displayImages">Thumbnails</label>&nbsp;
<input id="displayImages" type="checkbox" ng-model="displayImages" />&nbsp;
</span>
</div>
<div class="modal-body">
<select class="form-control form-control-sm" ng-model="plexServer"
ng-options="x.name for x in plexServers" ng-change="selectServer(plexServer)"></select>
<select class="form-control form-control-sm custom-select" ng-model="currentOrigin"
ng-options="x.name for x in origins" ng-change="selectOrigin(currentOrigin)"></select>
<hr />
<ul class="list-group list-group-root plex-panel" ng-init="setHeight = {'height': height + 'px'}" ng-style="setHeight" lazy-img-container>
<ul ng-show="currentOrigin.type=='plex' " class="list-group list-group-root plex-panel" ng-init="setHeight = {'height': height + 'px'}" ng-style="setHeight" lazy-img-container>
<li class="list-group-item" ng-repeat="a in libraries">
<div class="flex-container {{ displayImages ? 'w_images' : 'wo_images' }}" ng-click="getNested(a, true);">
<span class="fa {{ a.collapse ? 'fa-chevron-down' : 'fa-chevron-right' }} tab"></span>
@ -103,6 +103,16 @@
</ul>
</li>
</ul>
<ul ng-show="currentOrigin.type=='dizquetv' " class="list-group list-group-root plex-panel" ng-init="setHeight = {'height': height + 'px'}" ng-style="setHeight" lazy-img-container>
<li class="list-group-item" ng-repeat="x in customShows">
<div class="flex-container" ng-click="addCustomShow(x);">
<span class="fa fa-plus-circle tab"></span>
<span>{{x.name}} ({{x.count}})</span>
</div>
</li>
</ul>
<hr/>
<div class="loader" ng-if="pending &gt; 0" ></div> <h6 style='display:inline-block'>Selected Items</h6>
@ -112,7 +122,7 @@
<ul class="list-group list-group-root" style="height: 180px; overflow-y: scroll" dnd-list="selection" scroll-glue>
<div ng-if="selection.length === 0">Select media items from your plex library above.</div>
<li ng-if="selection.length + x &gt;= 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'))}}
{{ getProgramDisplayTitle(selection[selection.length + x]) }}
<button class="pull-right btn btn-sm btn-link" ng-click="selection.splice(selection.length + x,1)">
<span class="text-danger fa fa-trash-alt" ></span>
</button>

View File

@ -1,4 +1,4 @@
<div ng-show="programTitles.length > 0">
<div ng-show="programInfos.length > 0">
<div class="modal" tabindex="-1" role="dialog" style="display: block; background-color: rgba(0, 0, 0, .5);">
<div class="modal-dialog modal-dialog-scrollable modal-xl" role="document">
<div class="modal-content">
@ -10,16 +10,16 @@
<div class="modal-body container">
<div class="list-group list-group-root">
<div class="list-group-item flex-container program-row" ng-repeat="title in programTitles" ng-click="toggleShowDeletion(title)">
<div class="list-group-item flex-container program-row" ng-repeat="program in programInfos" ng-click="toggleShowDeletion(program.id)">
<div class='col-sm-7 col-md-9'>
<span ng-show='deleted.indexOf(title) === -1'>{{title}}</span>
<span class="text-muted" ng-show='deleted.indexOf(title) > -1'><strike>{{title}}</strike></span>
<span ng-show='deleted.indexOf(program.id) === -1'>{{program.displayName}}</span>
<span class="text-muted" ng-show='deleted.indexOf(program.id) > -1'><strike>{{program.displayName}}</strike></span>
</div>
<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-alt"></i>
<i ng-show="deleted.indexOf(title) > -1" class="text-success fa fa-undo"></i>
<i ng-show="deleted.indexOf(program.id) === -1" class="text-danger fa fa-trash-alt"></i>
<i ng-show="deleted.indexOf(program.id) > -1" class="text-success fa fa-undo"></i>
</button>
</div>
</div>
@ -28,9 +28,9 @@
<div class="modal-footer">
<button type="button" class="btn btn-sm btn-link" ng-click="programTitles = null" ng-show="deleted.length > 0">Cancel</button>
<button type="button" class="btn btn-sm btn-link" ng-click="programTitles = null" ng-show="deleted.length === 0">Close</button>
<button type="button" class="btn btn-sm btn-primary" ng-click="finished(programTitles);" ng-show="deleted.length > 0" >Apply</button>
<button type="button" class="btn btn-sm btn-link" ng-click="programInfos = null" ng-show="deleted.length > 0">Cancel</button>
<button type="button" class="btn btn-sm btn-link" ng-click="programInfos = null" ng-show="deleted.length === 0">Close</button>
<button type="button" class="btn btn-sm btn-primary" ng-click="finished(programInfos);" ng-show="deleted.length > 0" >Apply</button>
</div>
</div>
</div>

View File

@ -0,0 +1,121 @@
<div ng-show="visible">
<div class="modal" tabindex="-1" role="dialog" style="display: block; background-color: rgba(0, 0, 0, .5);">
<div class="modal-dialog modal-dialog-scrollable modal-xl" role="document">
<div class="modal-content">
<div>
<div class="modal-header">
<h5 class="modal-title">{{title}}</h5>
</div>
<div style='padding-left: 1rem; padding-right: 1rem' >
<div class="form-group">
<label for="name">Show Name:</label>
<input type="text" class="form-control" id="name" placeholder="Show Name" ng-model="name" ></input>
</div>
<h6 style="margin-top: 10px;">Clips</h6>
<div class="flex-container">
<div class="programming-counter small" ng-show="content.length > 0">
<span class="small"><b>Total:</b> {{content.length}}</span>
</div>
<div class='flex-pull-right' />
<div>
<button class="btn btn-sm btn-secondary btn-programming-tools"
ng-click="showTools = !showTools"
ng-show="content.length !== 0">
<span
class="fa {{ showTools ? 'fa-chevron-down' : 'fa-chevron-right'}}"></span>&nbsp;&nbsp;Tools
</button>
</div>
<div>
<button class="btn btn-sm btn-primary" ng-click="showPlexLibrary = true">
<span class="fa fa-plus"></span>
</button>
</div>
</div>
<div ng-show="showTools" class='tools-pane' >
<div class="row">
<!-- TODO: Probably sort shows and sort dates are needed here -->
<div class="input-group col-xl-3 col-lg-6" style="padding: 5px;">
<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-xl-3 col-lg-6" style="padding: 5px;">
<button class="btn btn-sm btn-warning form-control form-control-sm" type="button" ng-click="sortByDate()">
<i class='fa fa-sort-alpha-down'></i> Sort Release Dates
</button>
</div>
<div class="input-group col-xl-6 col-lg-12" style="padding: 5px;">
<button class="btn btn-sm btn-warning form-control form-control-sm" type="button" ng-click="shuffleShows()">
<i class='fa fa fa-random'></i> Random Shuffle
</button>
</div>
<div class="input-group col-xl-3 col-lg-6" style="padding: 5px;">
<button class="btn btn-sm btn-danger form-control form-control-sm" type="button" ng-click="showRemoveDuplicates()">
<i class='fa fa-trash-alt'></i> Remove Duplicates
</button>
</div>
<div class="input-group col-xl-3 col-lg-6" style="padding: 5px;">
<button class="btn btn-sm btn-danger form-control form-control-sm" type="button" ng-click="removeSpecials()">
<i class='fa fa-trash-alt'></i> Remove Specials
</button>
</div>
<div class="input-group col-xl-6 col-lg-12" style="padding: 5px;">
<button class="btn btn-sm btn-danger form-control form-control-sm" type="button" ng-click="showRemoveAllShow()">
<i class='fa fa-trash-alt'></i> Remove All
</button>
</div>
</div>
</div>
<div ng-show="content.length === 0">
<p class="text-center text-info">Click the <span class="fa fa-plus"></span> to import show content from your Plex server(s).</p>
</div>
</div>
</div>
<div vs-repeat class="modal-body container list-group list-group-root show-list"
dnd-list="content" ng-if="showList()"
vs-repeat-reinitialized="vsReinitialized(event, startIndex, endIndex)"
ng-init="setUpWatcher()"
dnd-drop="dropFunction(index , item)"
dnd-list=""
>
<div class="list-group-item flex-container show-row" style="cursor: default; height:1.1em; overflow:hidden" ng-repeat="x in content" track-by="x.$index" dnd-draggable="x"
dnd-effect-allowed="move"
dnd-moved="movedFunction(x.$index)"
>
<div class="program-start" >
X{{ (x.$index + 1).toString().padStart(2, '0') }}
</div>
<div ng-style="programSquareStyle(x, false)" />
<div class="title" >
{{ getProgramDisplayTitle(x) }}
</div>
<div class="flex-pull-right">
<button class="btn btn-sm btn-link" ng-click="contentSplice(x.$index,1)">
<i class="text-danger fa fa-trash-alt" ></i>
</button>
</div>
</div>
</div>
<div class="modal-footer">
<div class='text-danger small'>{{error}}</div>
<button type="button" class="btn btn-sm btn-link" ng-click="finished(true)">Cancel</button>
<button type="button" class="btn btn-sm btn-primary" ng-click="finished(false);">Done</button>
</div>
</div>
</div>
</div>
<plex-library limit=1000000000 height="300" visible="showPlexLibrary" on-finish="importPrograms"></plex-library>
</div>

View File

@ -1,4 +1,4 @@
<div>
<div class='container'>
<channel-config ng-if="showChannelConfig" channel="selectedChannel" channels="channels" on-done="onChannelConfigDone"></channel-config>
<h5>

View File

@ -0,0 +1,37 @@
<div class='container'>
<show-config linker="registerShowConfig" on-done="onShowConfigDone"></show-config>
<delete-show linker="registerDeleteShow" on-exit="onShowDelete"></delete-show>
<h5>
Custom Shows
<button class="pull-right btn btn-sm btn-primary" ng-click="selectShow(-1)">
<span class="fa fa-plus"></span>
</button>
</h5>
<table class="table">
<tr>
<th>Name</th>
<th width="40">Clips</th>
<th style='width:2em'></th>
</tr>
<tr ng-if="shows.length === 0">
<td colspan="3">
<p class="text-center text-danger">No Custom Shows set. Click the <span class="fa fa-plus"></span> to add custom shows.</p>
</td>
</tr>
<tr class='show-row' ng-repeat="x in shows" ng-click="selectShow($index)" style="cursor: pointer; height: 3em" >
<td style='height: 3em'>
<div class="loader" ng-if="x.pending"></div>
<span ng-show="!x.pending">{{x.name}}</span>
</td>
<td>{{x.count}}</td>
<td>
<button class='btn btn-link' title='Delete...' ng-click='deleteShow($index)' >
<i class='fas fa-trash-alt text-danger'></i>
</button>
</td>
</tr>
</table>
</div>

View File

@ -1,4 +1,4 @@
<div>
<div class='container'>
<filler-config linker="registerFillerConfig" on-done="onFillerConfigDone"></filler-config>
<delete-filler linker="registerDeleteFiller" on-exit="onFillerDelete"></delete-filler>

View File

@ -1,4 +1,4 @@
<div>
<div class='container-fluid'>
<h5>
{{title}}
@ -54,7 +54,7 @@
{{program.showTitle}}
</div>
<div class='sub-title'>
{{program.subTitle}}
{{program.subTitle}} <span class='episodeTitle'>{{program.episodeTitle}}</span>
</div>
</td>
</tr>

View File

@ -0,0 +1,40 @@
<div class='container'>
<div class='row gy-15'>
<div class='col'>
<h5>
Library
</h5>
<p>Components that will allow you to organize your media library to help with the creation of channels that do things the way you want.</p>
</div>
</div>
<div class='row'>
<div class='col-md-auto'>
<div class="card" style="width: 18rem;">
<div class="card-body">
<h5 class="card-title"><a href="#!/filler" class="card-link">Filler...</a></h5>
<p class="card-text">Filler lists are collections of videos that you may want to play during <i>&apos;flex&apos;</i> time segments. Flex is time within a channel that does not have a program scheduled (Usually used for padding).</p>
</div>
</div>
</div>
<div class='col-md-auto'>
<div class="card" style="width: 18rem;">
<div class="card-body">
<h5 class="card-title"><a href="#!/custom-shows" class="card-link">Custom Shows....</a></h5>
<p class="card-text">Custom Shows are sequences of videos that represent a episodes of a virtual TV show. When you add these shows to a channel, the schedule tools will treat the videos as if they belonged to a single TV show.</p>
</div>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,85 @@
<div class='container'>
<h5>Player</h5>
<p class='text-small text-info'>Play your channels in a local media player. This is mostly meant for testing purposes and to show what endpoints are available. </p>
<div class="form-group row">
<label for="channel" class="col-form-label col-sm-2">Channel:</label>
<div class="col">
<div ng-show='loading'>
<div class='loader'></div>
</div>
<input ng-show='! loading && channelOptions.length == 1' readonly class='form-control-plaintext'
id="endpoint"
value = "No channels found."
>
</input>
<select
id="channel"
ng-show='! loading && channelOptions.length != 1'
class="custom-select"
ng-model="channel"
ng-options="o.id as o.description for o in channelOptions"
>
</select>
</div>
</div>
<div class="form-group row">
<label for="endpoint" class="col-form-label col-sm-2">Endpoint:</label>
<div class="col">
<select
id="endpoint"
class="custom-select"
ng-model="selectedEndpoint"
ng-options="o.id as o.description for o in endpointOptions"
aria-describedby="endpointHelp"
>
</select>
<small id="endpointHelp" class="form-text text-muted">
<span ng-show="selectedEndpoint == 'video' ">
The /video endpoint is the one used by IPTV player or Plex to play the channel&apos;s content. It creates a single mpegts stream for the channel out of all of the videos scheduled for it. For this reason, it needs the videos to be formatted to the same codec and resolution (normalized). Use this endpoint to debug issues with Plex/IPTV players or when the other endpoints don&apos;t work correctly in your player.
</span>
<span ng-show="selectedEndpoint == 'm3u8' ">
The /m3u8 endpoint (misnomer) sends the channel as a playlist of videos, which allows <i>some</i> players to play the channel in sequence without the need for a single stream. Since there is no need for a single stream, it requires less normalization work .
</span>
<span ng-show="selectedEndpoint == 'radio' ">
The /radio endpoint plays only the audio of the channel, effectively turning it into a radio station. If you only need the audio, this endpoint is much more efficient as it will not need to extract or transcode video at all.
</span>
</small>
</div>
</div>
<div class="form-group row">
<label for="endpoint" class="col-form-label col-sm-2">URL to play:</label>
<div class="col">
<input readonly class='form-control-plaintext'
id="endpoint"
style="font-family: monospace"
value = "{{ endpoint() }}"
>
</input>
</div>
</div>
<div class='form-group row' ng-show="! buttonDisabled()" >
<div class='col-sm-2'>
<a
role="button"
ng-href='{{ endpointButtonHref() }}'
style="width:99%"
title="Attempt to play in local media player" class='btn btn-primary player-button'
>
<span class='fa fa-play'> Play</span>
</a>
</div>
</div>
</div>

View File

@ -1,4 +1,4 @@
<div>
<div class='container'>
<ul class="nav nav-tabs">
<li class="nav-item">
<span class="nav-link btn btn-link {{ selected === 'xmltv' ? 'active' : ''}}" ng-click="selected = 'xmltv'">

View File

@ -1,4 +1,4 @@
<div>
<div class='container'>
<h5>
Version Info

View File

@ -0,0 +1,263 @@
//This is an exact copy of the file with the same now in the nodejs
//one of these days, we'll figure out how to share the code.
module.exports = function (getShowData) {
/*** Input: list of programs
* output: sorted list of programs */
function sortShows(programs) {
let shows = {}
let movies = [] //not exactly accurate name
let newProgs = []
let progs = programs
for (let i = 0, l = progs.length; i < l; i++) {
let showData = getShowData( progs[i] );
if ( showData.showId === 'movie.' || ! showData.hasShow ) {
movies.push(progs[i]);
} else {
if (typeof shows[showData.showId] === 'undefined') {
shows[showData.showId] = [];
}
shows[showData.showId].push(progs[i]);
}
}
let keys = Object.keys(shows)
for (let i = 0, l = keys.length; i < l; i++) {
shows[keys[i]].sort((a, b) => {
let aData = getShowData(a);
let bData = getShowData(b);
return aData.order - bData.order;
})
newProgs = newProgs.concat(shows[keys[i]])
}
newProgs.concat(movies);
return newProgs;
}
function shuffle(array, lo, hi ) {
if (typeof(lo) === 'undefined') {
lo = 0;
hi = array.length;
}
let currentIndex = hi, temporaryValue, randomIndex
while (lo !== currentIndex) {
randomIndex = lo + Math.floor(Math.random() * (currentIndex -lo) );
currentIndex -= 1
temporaryValue = array[currentIndex]
array[currentIndex] = array[randomIndex]
array[randomIndex] = temporaryValue
}
return array
}
let removeDuplicates = (progs) => {
let tmpProgs = {}
for (let i = 0, l = progs.length; i < l; i++) {
if ( progs[i].type ==='redirect' ) {
tmpProgs['_redirect ' + progs[i].channel + ' _ '+ progs[i].duration ] = progs[i];
} else {
let data = getShowData(progs[i]);
if (data.hasShow) {
let key = data.showId + "|" + data.order;
tmpProgs[key] = progs[i];
}
}
}
let newProgs = [];
let keys = Object.keys(tmpProgs);
for (let i = 0, l = keys.length; i < l; i++) {
newProgs.push(tmpProgs[keys[i]])
}
return newProgs;
}
let removeSpecials = (progs) => {
let tmpProgs = []
for (let i = 0, l = progs.length; i < l; i++) {
if (
(typeof(progs[i].customShowId) !== 'undefined')
||
(progs[i].season !== 0)
) {
tmpProgs.push(progs[i]);
}
}
return tmpProgs;
}
let getProgramDisplayTitle = (x) => {
let s = x.type === 'episode' ? x.showTitle + ' - S' + x.season.toString().padStart(2, '0') + 'E' + x.episode.toString().padStart(2, '0') : x.title
if (typeof(x.customShowId) !== 'undefined') {
s = x.customShowName + " X" + (x.customOrder+1).toString().padStart(2,'0') + " (" + s + ")";
}
return s;
}
let sortByDate = (programs) => {
programs.sort( (a,b) => {
let aHas = ( typeof(a.date) !== 'undefined' );
let bHas = ( typeof(b.date) !== 'undefined' );
if (!aHas && !bHas) {
return 0;
} else if (! aHas) {
return 1;
} else if (! bHas) {
return -1;
}
if (a.date < b.date ) {
return -1;
} else if (a.date > b.date) {
return 1;
} else {
let aHasSeason = ( typeof(a.season) !== 'undefined' );
let bHasSeason = ( typeof(b.season) !== 'undefined' );
if (! aHasSeason && ! bHasSeason) {
return 0;
} else if (! aHasSeason) {
return 1;
} else if (! bHasSeason) {
return -1;
}
if (a.season < b.season) {
return -1;
} else if (a.season > b.season) {
return 1;
} else if (a.episode < b.episode) {
return -1;
} else if (a.episode > b.episode) {
return 1;
} else {
return 0;
}
}
});
return programs;
}
let programSquareStyle = (program) => {
let background ="";
if ( (program.isOffline) && (program.type !== 'redirect') ) {
background = "rgb(255, 255, 255)";
} else {
let r = 0, g = 0, b = 0, r2=0, g2=0,b2=0;
let angle = 45;
let w = 3;
if (program.type === 'redirect') {
angle = 0;
w = 4 + (program.channel % 10);
let c = (program.channel * 100019);
//r = 255, g = 0, b = 0;
//r2 = 0, g2 = 0, b2 = 255;
r = ( (c & 3) * 77 );
g = ( ( (c >> 1) & 3) * 77 );
b = ( ( (c >> 2) & 3) * 77 );
r2 = ( ( (c >> 5) & 3) * 37 );
g2 = ( ( (c >> 3) & 3) * 37 );
b2 = ( ( (c >> 4) & 3) * 37 );
} else if ( typeof(program.customShowId) !== 'undefined') {
let h = Math.abs( getHashCode(program.customShowId, false));
let h2 = Math.abs( getHashCode(program.customShowId, true));
r = h % 256;
g = (h / 256) % 256;
b = (h / (256*256) ) % 256;
r2 = (h2 / (256*256) ) % 256;
g2 = (h2 / (256*256) ) % 256;
b2 = (h2 / (256*256) ) % 256;
angle = (360 - 90 + h % 180) % 360;
if ( angle >= 350 || angle < 10 ) {
angle += 53;
}
} else if (program.type === 'episode') {
let h = Math.abs( getHashCode(program.showTitle, false));
let h2 = Math.abs( getHashCode(program.showTitle, true));
r = h % 256;
g = (h / 256) % 256;
b = (h / (256*256) ) % 256;
r2 = (h2 / (256*256) ) % 256;
g2 = (h2 / (256*256) ) % 256;
b2 = (h2 / (256*256) ) % 256;
angle = (360 - 90 + h % 180) % 360;
if ( angle >= 350 || angle < 10 ) {
angle += 53;
}
} else if (program.type === 'track') {
r = 10, g = 10, b = 10;
r2 = 245, g2 = 245, b2 = 245;
angle = 315;
w = 2;
} else {
r = 10, g = 10, b = 10;
r2 = 245, g2 = 245, b2 = 245;
angle = 45;
w = 6;
}
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 f = interpolate;
let w = 15.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': `${a}%`,
'height': '1.3em',
'margin-right': `${b}%`,
'background': background,
'border': '1px solid black',
'margin-top': "0.01em",
'margin-bottom': '1px',
};
}
let getHashCode = (s, rev) => {
var hash = 0;
if (s.length == 0) return hash;
let inc = 1, st = 0, e = s.length;
if (rev) {
inc = -1, st = e - 1, e = -1;
}
for (var i = st; i != e; i+= inc) {
hash = s.charCodeAt(i) + ((hash << 5) - hash);
hash = hash & hash; // Convert to 32bit integer
}
return hash;
}
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]) );
}
}
}
} )();
return {
sortShows: sortShows,
shuffle: shuffle,
removeDuplicates: removeDuplicates,
removeSpecials: removeSpecials,
sortByDate: sortByDate,
getProgramDisplayTitle: getProgramDisplayTitle,
programSquareStyle: programSquareStyle,
}
}

View File

@ -227,6 +227,47 @@ module.exports = function ($http, $q) {
return (await $http.get( `/api/filler/${fillerId}/channels` )).data;
},
/*======================================================================
* Custom Show stuff
*/
getAllShowsInfo: async () => {
let f = await $http.get('/api/shows');
return f.data;
},
getShow: async (id) => {
let f = await $http.get(`/api/show/${id}`);
return f.data;
},
updateShow: async(id, show) => {
return (await $http({
method: "POST",
url : `/api/show/${id}`,
data: angular.toJson(show),
headers: { 'Content-Type': 'application/json; charset=utf-8' }
}) ).data;
},
createShow: async(show) => {
return (await $http({
method: "PUT",
url : `/api/show`,
data: angular.toJson(show),
headers: { 'Content-Type': 'application/json; charset=utf-8' }
}) ).data;
},
deleteShow: async(id) => {
return ( await $http({
method: "DELETE",
url : `/api/show/${id}`,
data: {},
headers: { 'Content-Type': 'application/json; charset=utf-8' }
}) ).data;
},
/*======================================================================
* TV Guide endpoints
*/

View File

@ -0,0 +1,65 @@
//This is an exact copy of the file with the same now in the nodejs
//one of these days, we'll figure out how to share the code.
module.exports = function () {
let movieTitleOrder = {};
let movieTitleOrderNumber = 0;
return (program) => {
if ( typeof(program.customShowId) !== 'undefined' ) {
return {
hasShow : true,
showId : "custom." + program.customShowId,
showDisplayName : program.customShowName,
order : program.customOrder,
}
} else if (program.isOffline && program.type === 'redirect') {
return {
hasShow : true,
showId : "redirect." + program.channel,
order : program.duration,
showDisplayName : `Redirect to channel ${program.channel}`,
channel: program.channel,
}
} else if (program.isOffline) {
return {
hasShow : false
}
} else if (program.type === 'movie') {
let key = program.serverKey + "|" + program.key;
if (typeof(movieTitleOrder[key]) === 'undefined') {
movieTitleOrder[key] = movieTitleOrderNumber++;
}
return {
hasShow : true,
showId : "movie.",
showDisplayName : "Movies",
order : movieTitleOrder[key],
}
} else if ( (program.type === 'episode') || (program.type === 'track') ) {
let s = 0;
let e = 0;
if ( typeof(program.season) !== 'undefined') {
s = program.season;
}
if ( typeof(program.episode) !== 'undefined') {
e = program.episode;
}
let prefix = "tv.";
if (program.type === 'track') {
prefix = "audio.";
}
return {
hasShow: true,
showId : prefix + program.showTitle,
showDisplayName : program.showTitle,
order : s * 1000000 + e,
}
} else {
return {
hasShow : false,
}
}
}
}