Merge pull request #290 from vexorian/20210323_dev

20210323 dev
This commit is contained in:
vexorian 2021-03-23 17:02:30 -04:00 committed by GitHub
commit fe2e7770fa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
38 changed files with 1430 additions and 572 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

@ -1,4 +1,4 @@
# dizqueTV 1.3.1-prerelease
# dizqueTV 1.3.2-prerelease
![Discord](https://img.shields.io/discord/711313431457693727?logo=discord&logoColor=fff&style=flat-square) ![GitHub top language](https://img.shields.io/github/languages/top/vexorian/dizquetv?logo=github&style=flat-square) ![Docker Pulls](https://img.shields.io/docker/pulls/vexorian/dizquetv?logo=docker&logoColor=fff&style=flat-square)
Create live TV channel streams from media on your Plex servers.

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 {

View File

@ -5,5 +5,5 @@ module.exports = {
TVGUIDE_MAXIMUM_FLEX_DURATION : 6 * 60 * 60 * 1000,
TOO_FREQUENT: 100,
VERSION_NAME: "1.3.1-prerelease"
VERSION_NAME: "1.3.2-prerelease"
}

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

@ -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,10 @@ 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('fillerCtrl', require('./controllers/filler'))
app.controller('customShowsCtrl', require('./controllers/custom-shows'))
app.config(function ($routeProvider) {
$routeProvider
@ -50,6 +55,14 @@ 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'
@ -59,6 +72,6 @@ app.config(function ($routeProvider) {
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 () {
}

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="#!/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>
@ -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" 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

@ -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,
}
}
}
}