diff --git a/Dockerfile b/Dockerfile index 651f823..23435cc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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" ] diff --git a/Dockerfile-nvidia b/Dockerfile-nvidia index 608d7f5..f21f8b1 100644 --- a/Dockerfile-nvidia +++ b/Dockerfile-nvidia @@ -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" ] diff --git a/README.md b/README.md index c561e27..ff86293 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/index.js b/index.js index 6eaf637..484e691 100644 --- a/index.js +++ b/index.js @@ -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) + } } diff --git a/resources/default-custom.css b/resources/default-custom.css new file mode 100644 index 0000000..2293389 --- /dev/null +++ b/resources/default-custom.css @@ -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; +} + diff --git a/src/api.js b/src/api.js index ea2d723..ee59ad9 100644 --- a/src/api.js +++ b/src/api.js @@ -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 { diff --git a/src/constants.js b/src/constants.js index b223d2b..15e9c2e 100644 --- a/src/constants.js +++ b/src/constants.js @@ -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" } diff --git a/src/dao/custom-show-db.js b/src/dao/custom-show-db.js new file mode 100644 index 0000000..f174f3b --- /dev/null +++ b/src/dao/custom-show-db.js @@ -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; \ No newline at end of file diff --git a/src/services/get-show-data.js b/src/services/get-show-data.js new file mode 100644 index 0000000..99d44b1 --- /dev/null +++ b/src/services/get-show-data.js @@ -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, + } + } + } + +} \ No newline at end of file diff --git a/src/services/random-slots-service.js b/src/services/random-slots-service.js index 931eeac..dc3ef70 100644 --- a/src/services/random-slots-service.js +++ b/src/services/random-slots-service.js @@ -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++; diff --git a/src/services/time-slots-service.js b/src/services/time-slots-service.js index 849470e..5fd3159 100644 --- a/src/services/time-slots-service.js +++ b/src/services/time-slots-service.js @@ -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] ]; diff --git a/web/app.js b/web/app.js index 1b15e15..b1d46c2 100644 --- a/web/app.js +++ b/web/app.js @@ -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" }) }) \ No newline at end of file diff --git a/web/controllers/custom-shows.js b/web/controllers/custom-shows.js new file mode 100644 index 0000000..517e00a --- /dev/null +++ b/web/controllers/custom-shows.js @@ -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); + } + } +} \ No newline at end of file diff --git a/web/controllers/guide.js b/web/controllers/guide.js index aa65776..7098b73 100644 --- a/web/controllers/guide.js +++ b/web/controllers/guide.js @@ -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, } ); diff --git a/web/controllers/library.js b/web/controllers/library.js new file mode 100644 index 0000000..264c31a --- /dev/null +++ b/web/controllers/library.js @@ -0,0 +1,2 @@ +module.exports = function () { +} \ No newline at end of file diff --git a/web/directives/channel-config.js b/web/directives/channel-config.js index 2dc63a4..685bce2 100644 --- a/web/directives/channel-config.js +++ b/web/directives/channel-config.js @@ -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 ); } diff --git a/web/directives/filler-config.js b/web/directives/filler-config.js index f5c89f5..aea88bc 100644 --- a/web/directives/filler-config.js +++ b/web/directives/filler-config.js @@ -33,6 +33,7 @@ module.exports = function ($timeout) { z--; } scope.content.splice(z, 0, program ); + refreshContentIndexes(); $timeout(); return false; } diff --git a/web/directives/plex-library.js b/web/directives/plex-library.js index b2f6576..4e21a52 100644 --- a/web/directives/plex-library.js +++ b/web/directives/plex-library.js @@ -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(); + } } }; } \ No newline at end of file diff --git a/web/directives/random-slots-schedule-editor.js b/web/directives/random-slots-schedule-editor.js index 7a6f334..0f88017 100644 --- a/web/directives/random-slots-schedule-editor.js +++ b/web/directives/random-slots-schedule-editor.js @@ -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; } } -} - +} \ No newline at end of file diff --git a/web/directives/remove-shows.js b/web/directives/remove-shows.js index 33efcd5..788a409 100644 --- a/web/directives/remove-shows.js +++ b/web/directives/remove-shows.js @@ -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); } diff --git a/web/directives/show-config.js b/web/directives/show-config.js new file mode 100644 index 0000000..1dbafa1 --- /dev/null +++ b/web/directives/show-config.js @@ -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); + } + + } + }; +} diff --git a/web/directives/time-slots-schedule-editor.js b/web/directives/time-slots-schedule-editor.js index bf673cc..45b2522 100644 --- a/web/directives/time-slots-schedule-editor.js +++ b/web/directives/time-slots-schedule-editor.js @@ -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.", - } - } -} - - diff --git a/web/public/index.html b/web/public/index.html index b39f56d..150d073 100644 --- a/web/public/index.html +++ b/web/public/index.html @@ -7,6 +7,7 @@ + @@ -32,7 +33,7 @@ - Guide - Channels - Filler - Settings - Version + Guide - Channels - Library - Settings - Version XMLTV diff --git a/web/public/style.css b/web/public/style.css index 92643a7..0ad24b1 100644 --- a/web/public/style.css +++ b/web/public/style.css @@ -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; } diff --git a/web/public/templates/channel-config.html b/web/public/templates/channel-config.html index dd7f5d0..3c8b57a 100644 --- a/web/public/templates/channel-config.html +++ b/web/public/templates/channel-config.html @@ -181,7 +181,7 @@
- {{ x.type === 'episode' ? x.showTitle + ' - S' + x.season.toString().padStart(2, '0') + 'E' + x.episode.toString().padStart(2, '0') : x.title}} + {{ getProgramDisplayTitle(x) }}
Flex @@ -856,7 +856,7 @@ - + diff --git a/web/public/templates/plex-library.html b/web/public/templates/plex-library.html index ee23347..01ed94f 100644 --- a/web/public/templates/plex-library.html +++ b/web/public/templates/plex-library.html @@ -3,7 +3,7 @@