Fixing up frontend with breaking changes made to API/DB schemas. Frontend should not depend directly on backend files...sign

This commit is contained in:
Christian Benincasa
2023-10-30 09:30:55 -04:00
parent 093d8c289c
commit c594b86ee2
17 changed files with 628 additions and 576 deletions

3
.gitignore vendored
View File

@@ -9,4 +9,5 @@ package-lock.json
config/
.vscode/
build/
build/
log.txt

View File

@@ -5,8 +5,8 @@
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node index.js",
"build": "browserify ./web/app.js -p tsify -p esmify -o ./web/public/bundle.js",
"dev-client": "watchify ./web/app.js -p tsify -p esmify -o ./web/public/bundle.js",
"build": "browserify ./web/app.js -o ./web/public/bundle.js",
"dev-client": "watchify ./web/app.js -o ./web/public/bundle.js",
"dev-server": "nodemon index.js --ignore ./web/ --ignore ./db/ --ignore ./xmltv.xml",
"compile": "babel index.js -d dist && babel src -d dist/src",
"package": "sh ./make_dist.sh",

View File

@@ -2,7 +2,7 @@ import JSONStream from 'JSONStream';
import express from 'express';
import fileUpload from 'express-fileupload';
import fs from 'fs';
import { isUndefined } from 'lodash-es';
import { find, isUndefined } from 'lodash-es';
import path from 'path';
import constants from './constants.js';
import { Channel, getDB } from './dao/db.js';
@@ -1010,6 +1010,20 @@ export function makeApi(
}
});
router.get('/api/plex', async (req, res) => {
const db = await getDB();
const servers = db.plexServers();
const server = find(servers, { name: req.query['name'] as string });
if (isUndefined(server)) {
return res
.status(404)
.json({ error: 'No server found with name: ' + req.query.name });
}
const plex = new Plex(server);
return res.json(await plex.Get(req.query['path']));
});
function updateXmltv() {
xmltvInterval.updateXML();
xmltvInterval.restartInterval();

View File

@@ -66,7 +66,7 @@ export type Channel = {
number: number;
watermark?: Watermark;
fillerCollections?: FillerCollection[];
programs?: Program[];
programs: Program[];
icon: ChannelIcon;
guideMinimumDurationSeconds: number;
groupTitle: string;

View File

@@ -138,9 +138,7 @@ function convertProgram(program: JSONObject): Program {
plexFile: program['plexFile'] as string,
ratingKey: program['ratingKey'] as string,
serverKey: program['serverKey'] as string,
showTitle: !isMovie
? (program['showTitle'] as string | undefined)
: undefined,
showTitle: program['showTitle'] as string | undefined,
summary: program['summary'] as string,
title: program['title'] as string,
type: program['type'] as string,

View File

@@ -1,11 +1,12 @@
import request from 'request';
import { isUndefined } from 'lodash-es';
import { PlexServerSettings } from './dao/db.js';
export class Plex {
private _accessToken: string;
private _server: any;
private _headers: object;
constructor(opts) {
constructor(opts: Partial<PlexServerSettings>) {
this._accessToken =
typeof opts.accessToken !== 'undefined' ? opts.accessToken : '';
let uri = 'http://127.0.0.1:32400';
@@ -17,9 +18,6 @@ export class Plex {
}
this._server = {
uri: uri,
host: typeof opts.host !== 'undefined' ? opts.host : '127.0.0.1',
port: typeof opts.port !== 'undefined' ? opts.port : '32400',
protocol: typeof opts.protocol !== 'undefined' ? opts.protocol : 'http',
};
this._headers = {
Accept: 'application/json',
@@ -83,6 +81,7 @@ export class Plex {
}
async Get(path, optionalHeaders = {}) {
console.log(path);
let req = {
method: 'get',
url: `${this.URL}${path}`,

View File

@@ -12,7 +12,6 @@ import { EventService } from './services/event-service.js';
import { FileCacheService } from './services/file-cache-service.js';
import { M3uService } from './services/m3u-service.js';
import { TVGuideService } from './services/tv-guide-service.js';
import * as xmltv from './xmltv.js';
// Temp
import { dirname } from 'node:path';
@@ -20,6 +19,7 @@ import { fileURLToPath } from 'node:url';
import { getDB } from './dao/db.js';
import { serverOptions } from './globals.js';
import { ChannelCache } from './channel-cache.js';
import { XmlTvWriter } from './xmltv.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
@@ -93,12 +93,12 @@ export const serverContext = once(async () => {
const cacheImageService = new CacheImageService(db, fileCache);
const m3uService = new M3uService(channelDB, fileCache, channelCache);
const eventService = new EventService();
const xmltv = new XmlTvWriter();
await initDB(db, channelDB);
const guideService = new TVGuideService(
xmltv,
db,
cacheImageService,
eventService,
);
@@ -115,5 +115,6 @@ export const serverContext = once(async () => {
hdhrService: hdhr(db, channelDB),
customShowDB: new CustomShowDB(path.join(opts.database, 'custom-shows')),
channelCache,
xmltv,
};
});

View File

@@ -8,7 +8,6 @@ import constants from './constants.js';
import createLogger from './logger.js';
import { serverContext } from './server-context.js';
import { video } from './video.js';
import * as xmltv from './xmltv.js';
import { xmltvInterval } from './xmltv-generator.js';
import { onShutdown } from 'node-graceful-shutdown';
@@ -192,5 +191,6 @@ onShutdown('log', [], async () => {
await _wait(2000);
});
onShutdown('xmltv-writer', [], async () => {
await xmltv.shutdown();
const ctx = await serverContext();
await ctx.xmltv.shutdown();
});

View File

@@ -1,5 +1,5 @@
//Adds a slight pause so that long operations
export default function () {
export default function (): Promise<void> {
return new Promise((resolve) => {
setImmediate(() => resolve(void 0));
});

View File

@@ -4,6 +4,8 @@ import createLogger from '../logger.js';
import throttle from './throttle.js';
import { CacheImageService } from './cache-image-service.js';
import { EventService } from './event-service.js';
import { Channel, getDB } from '../dao/db.js';
import { XmlTvWriter } from '../xmltv.js';
const logger = createLogger(import.meta);
@@ -11,32 +13,30 @@ const FALLBACK_ICON =
'https://raw.githubusercontent.com/vexorain/dizquetv/main/resources/dizquetv.png';
export class TVGuideService {
cached: any;
cached: Record<number, any>;
lastUpdate: number;
lastBackoff: number;
updateTime: number;
currentUpdate: number;
currentLimit: number;
currentChannels: any;
xmltv: any;
db: any;
xmltv: XmlTvWriter;
cacheImageService: any;
eventService: any;
_throttle: any;
eventService: EventService;
_throttle: () => Promise<void>;
updateLimit: any;
updateChannels: any[];
accumulateTable: any;
channelsByNumber: any;
channelsByNumber: Record<number, Channel>;
/****
*
**/
constructor(
xmltv,
db,
xmltv: XmlTvWriter,
cacheImageService: CacheImageService,
eventService: EventService,
) {
this.cached = null;
this.cached = {};
this.lastUpdate = 0;
this.lastBackoff = 100;
this.updateTime = 0;
@@ -44,7 +44,6 @@ export class TVGuideService {
this.currentLimit = -1;
this.currentChannels = null;
this.xmltv = xmltv;
this.db = db;
this.cacheImageService = cacheImageService;
this.eventService = eventService;
this._throttle = throttle;
@@ -100,7 +99,7 @@ export class TVGuideService {
return await this.get();
}
async makeAccumulated(channel) {
async makeAccumulated(channel: Channel) {
if (isUndefined(channel.programs)) {
throw Error(JSON.stringify(channel).slice(0, 200));
}
@@ -114,8 +113,8 @@ export class TVGuideService {
return arr;
}
async getCurrentPlayingIndex(channel, t): Promise<any> {
let s = new Date(channel.startTime).getTime();
async getCurrentPlayingIndex(channel: Channel, t): Promise<any> {
let s = new Date(channel.startTimeEpoch).getTime();
if (t < s) {
//it's flex time
return {
@@ -157,14 +156,14 @@ export class TVGuideService {
}
async getChannelPlaying(
channel,
channel: Channel,
previousKnown,
t,
t: number,
depth: any[] = [],
): Promise<any> {
let playing: Record<string, any> = {};
if (
typeof previousKnown !== 'undefined' &&
!isUndefined(previousKnown) &&
previousKnown.index !== -1 &&
previousKnown.program.duration ==
channel.programs[previousKnown.index].duration &&
@@ -233,7 +232,7 @@ export class TVGuideService {
return playing;
}
async getChannelPrograms(t0, t1, channel) {
async getChannelPrograms(t0: number, t1: number, channel: Channel) {
if (isUndefined(channel)) {
throw Error("Couldn't find channel?");
}
@@ -414,7 +413,7 @@ export class TVGuideService {
}
async refreshXML() {
let xmltvSettings = this.db['xmltv-settings'].find()[0];
let xmltvSettings = (await getDB()).xmlTvSettings();
await this.xmltv.WriteXMLTV(
this.cached,
xmltvSettings,

View File

@@ -1,6 +1,7 @@
import { Plex } from './plex.js';
import { serverContext } from './server-context.js';
import createLogger from './logger.js';
import { getDB } from './dao/db.js';
const logger = createLogger(import.meta);
@@ -20,10 +21,10 @@ const updateXML = async () => {
try {
channels = await getChannelsCached();
let xmltvSettings = ctx.db['xmltv-settings'].find()[0];
let xmltvSettings = (await getDB()).xmlTvSettings();
let t = ctx.guideService.prepareRefresh(
channels,
xmltvSettings.cache * 60 * 60 * 1000,
xmltvSettings.refreshHours * 60 * 60 * 1000,
);
channels = [];

View File

@@ -1,207 +1,214 @@
import XMLWriter from 'xml-writer';
import fs from 'fs';
import { Channel } from './dao/db.js';
import { Channel, XmlTvSettings } from './dao/db.js';
import { CacheImageService } from './services/cache-image-service.js';
let isShutdown = false;
let isWorking = false;
export async function WriteXMLTV(
json,
xmlSettings,
throttle,
cacheImageService,
) {
if (isShutdown) {
return;
}
if (isWorking) {
console.log('Concurrent xmltv write attempt detected, skipping');
return;
}
isWorking = true;
try {
await writePromise(json, xmlSettings, throttle, cacheImageService);
} catch (err) {
console.error('Error writing xmltv', err);
}
isWorking = false;
}
function writePromise(json, xmlSettings, throttle, cacheImageService) {
return new Promise((resolve, reject) => {
let ws = fs.createWriteStream(xmlSettings.file);
let xw = new XMLWriter(true, (str, enc) => ws.write(str, enc));
ws.on('close', () => {
resolve(void 0);
});
ws.on('error', (err) => {
reject(err);
});
_writeDocStart(xw);
async function middle() {
let channelNumbers: any[] = [];
Object.keys(json).forEach((key) => channelNumbers.push(key));
let channels = channelNumbers.map((number) => json[number].channel);
_writeChannels(xw, channels);
for (let i = 0; i < channelNumbers.length; i++) {
let number = channelNumbers[i];
await _writePrograms(
xw,
json[number].channel,
json[number].programs,
throttle,
xmlSettings,
cacheImageService,
);
}
export class XmlTvWriter {
async WriteXMLTV(
json,
xmlSettings: XmlTvSettings,
throttle: () => Promise<void>,
cacheImageService,
) {
if (isShutdown) {
return;
}
middle()
.then(() => {
_writeDocEnd(xw, ws);
})
.catch((err) => {
console.error('Error', err);
})
.then(() => ws.end());
});
}
if (isWorking) {
console.log('Concurrent xmltv write attempt detected, skipping');
return;
}
isWorking = true;
try {
await this.writePromise(json, xmlSettings, throttle, cacheImageService);
} catch (err) {
console.error('Error writing xmltv', err);
}
isWorking = false;
}
function _writeDocStart(xw) {
xw.startDocument();
xw.startElement('tv');
xw.writeAttribute('generator-info-name', 'psuedotv-plex');
}
function _writeDocEnd(xw, _) {
xw.endElement();
xw.endDocument();
}
private writePromise(
json,
xmlSettings: XmlTvSettings,
throttle: () => Promise<void>,
cacheImageService: CacheImageService,
) {
return new Promise((resolve, reject) => {
let ws = fs.createWriteStream(xmlSettings.outputPath);
let xw = new XMLWriter(true, (str, enc) => ws.write(str, enc));
ws.on('close', () => {
resolve(void 0);
});
ws.on('error', (err) => {
reject(err);
});
this._writeDocStart(xw);
const middle = async () => {
let channelNumbers: any[] = [];
Object.keys(json).forEach((key) => channelNumbers.push(key));
let channels = channelNumbers.map((number) => json[number].channel);
this._writeChannels(xw, channels);
for (let i = 0; i < channelNumbers.length; i++) {
let number = channelNumbers[i];
await this._writePrograms(
xw,
json[number].channel,
json[number].programs,
throttle,
xmlSettings,
cacheImageService,
);
}
};
middle()
.then(() => {
this._writeDocEnd(xw, ws);
})
.catch((err) => {
console.error('Error', err);
})
.then(() => ws.end());
});
}
function _writeChannels(xw, channels: Channel[]) {
for (let i = 0; i < channels.length; i++) {
xw.startElement('channel');
xw.writeAttribute('id', channels[i].number);
xw.startElement('display-name');
xw.writeAttribute('lang', 'en');
xw.text(channels[i].name);
private _writeDocStart(xw) {
xw.startDocument();
xw.startElement('tv');
xw.writeAttribute('generator-info-name', 'psuedotv-plex');
}
private _writeDocEnd(xw, _) {
xw.endElement();
if (channels[i].icon) {
xw.startElement('icon');
xw.writeAttribute('src', channels[i].icon.path);
xw.endDocument();
}
private _writeChannels(xw, channels: Channel[]) {
for (let i = 0; i < channels.length; i++) {
xw.startElement('channel');
xw.writeAttribute('id', channels[i].number);
xw.startElement('display-name');
xw.writeAttribute('lang', 'en');
xw.text(channels[i].name);
xw.endElement();
if (channels[i].icon) {
xw.startElement('icon');
xw.writeAttribute('src', channels[i].icon.path);
xw.endElement();
}
xw.endElement();
}
xw.endElement();
}
}
async function _writePrograms(
xw,
channel,
programs,
throttle,
xmlSettings,
cacheImageService,
) {
for (let i = 0; i < programs.length; i++) {
if (!isShutdown) {
await throttle();
async _writePrograms(
xw,
channel,
programs,
throttle,
xmlSettings,
cacheImageService,
) {
for (let i = 0; i < programs.length; i++) {
if (!isShutdown) {
await throttle();
}
await this._writeProgramme(
channel,
programs[i],
xw,
xmlSettings,
cacheImageService,
);
}
await _writeProgramme(
channel,
programs[i],
xw,
xmlSettings,
cacheImageService,
);
}
}
async function _writeProgramme(
channel,
program,
xw,
xmlSettings,
cacheImageService,
) {
// Programme
xw.startElement('programme');
xw.writeAttribute('start', _createXMLTVDate(program.start));
xw.writeAttribute('stop', _createXMLTVDate(program.stop));
xw.writeAttribute('channel', channel.number);
// Title
xw.startElement('title');
xw.writeAttribute('lang', 'en');
xw.text(program.title);
xw.endElement();
xw.writeRaw('\n <previously-shown/>');
//sub-title
if (typeof program.sub !== 'undefined') {
xw.startElement('sub-title');
async _writeProgramme(channel, program, xw, xmlSettings, cacheImageService) {
// Programme
xw.startElement('programme');
xw.writeAttribute('start', this._createXMLTVDate(program.start));
xw.writeAttribute('stop', this._createXMLTVDate(program.stop));
xw.writeAttribute('channel', channel.number);
// Title
xw.startElement('title');
xw.writeAttribute('lang', 'en');
xw.text(program.sub.title);
xw.text(program.title);
xw.endElement();
xw.writeRaw('\n <previously-shown/>');
xw.startElement('episode-num');
xw.writeAttribute('system', 'onscreen');
xw.text('S' + program.sub.season + ' E' + program.sub.episode);
xw.endElement();
//sub-title
if (typeof program.sub !== 'undefined') {
xw.startElement('sub-title');
xw.writeAttribute('lang', 'en');
xw.text(program.sub.title);
xw.endElement();
xw.startElement('episode-num');
xw.writeAttribute('system', 'xmltv_ns');
xw.text(program.sub.season - 1 + '.' + (program.sub.episode - 1) + '.0/1');
xw.endElement();
}
// Icon
if (typeof program.icon !== 'undefined') {
xw.startElement('icon');
let icon = program.icon;
if (xmlSettings.enableImageCache === true) {
const imgUrl = cacheImageService.registerImageOnDatabase(icon);
icon = `{{host}}/cache/images/${imgUrl}`;
xw.startElement('episode-num');
xw.writeAttribute('system', 'onscreen');
xw.text('S' + program.sub.season + ' E' + program.sub.episode);
xw.endElement();
xw.startElement('episode-num');
xw.writeAttribute('system', 'xmltv_ns');
xw.text(
program.sub.season - 1 + '.' + (program.sub.episode - 1) + '.0/1',
);
xw.endElement();
}
xw.writeAttribute('src', icon);
// Icon
if (typeof program.icon !== 'undefined') {
xw.startElement('icon');
let icon = program.icon;
if (xmlSettings.enableImageCache === true) {
const imgUrl = cacheImageService.registerImageOnDatabase(icon);
icon = `{{host}}/cache/images/${imgUrl}`;
}
xw.writeAttribute('src', icon);
xw.endElement();
}
// Desc
xw.startElement('desc');
xw.writeAttribute('lang', 'en');
if (typeof program.summary !== 'undefined' && program.summary.length > 0) {
xw.text(program.summary);
} else {
xw.text(channel.name);
}
xw.endElement();
// Rating
if (program.rating != null && typeof program.rating !== 'undefined') {
xw.startElement('rating');
xw.writeAttribute('system', 'MPAA');
xw.writeElement('value', program.rating);
xw.endElement();
}
// End of Programme
xw.endElement();
}
// Desc
xw.startElement('desc');
xw.writeAttribute('lang', 'en');
if (typeof program.summary !== 'undefined' && program.summary.length > 0) {
xw.text(program.summary);
} else {
xw.text(channel.name);
private _createXMLTVDate(d) {
return d.substring(0, 19).replace(/[-T:]/g, '') + ' +0000';
}
xw.endElement();
// Rating
if (program.rating != null && typeof program.rating !== 'undefined') {
xw.startElement('rating');
xw.writeAttribute('system', 'MPAA');
xw.writeElement('value', program.rating);
xw.endElement();
async shutdown() {
isShutdown = true;
console.log('Shutting down xmltv writer.');
if (isWorking) {
let s = 'Wait for xmltv writer...';
while (isWorking) {
console.log(s);
await wait(100);
s = 'Still waiting for xmltv writer...';
}
console.log('Write finished.');
} else {
console.log('xmltv writer had no pending jobs.');
}
}
// End of Programme
xw.endElement();
}
function _createXMLTVDate(d) {
return d.substring(0, 19).replace(/[-T:]/g, '') + ' +0000';
}
function wait(x) {
return new Promise((resolve) => {
setTimeout(resolve, x);
});
}
export async function shutdown() {
isShutdown = true;
console.log('Shutting down xmltv writer.');
if (isWorking) {
let s = 'Wait for xmltv writer...';
while (isWorking) {
console.log(s);
await wait(100);
s = 'Still waiting for xmltv writer...';
}
console.log('Write finished.');
} else {
console.log('xmltv writer had no pending jobs.');
}
}

View File

@@ -1,364 +1,370 @@
const MINUTE = 60 * 1000;
module.exports = function ($scope, $timeout, dizquetv) {
$scope.offset = 0;
$scope.M = 60 * MINUTE;
$scope.zoomLevel = 3;
$scope.T = 190 * MINUTE;
$scope.before = 15 * MINUTE;
$scope.enableNext = false;
$scope.enableBack = false;
$scope.showNow = false;
$scope.nowPosition = 0;
$scope.refreshHandle = null;
$scope.offset = 0;
$scope.M = 60 * MINUTE;
$scope.zoomLevel = 3
$scope.T = 190 * MINUTE;
$scope.before = 15 * MINUTE;
$scope.enableNext = false;
$scope.enableBack = false;
$scope.showNow = false;
$scope.nowPosition = 0;
$scope.refreshHandle = null;
const intl = new Intl.DateTimeFormat('default', {
hour12: true,
hour: 'numeric',
minute: 'numeric',
});
const intl = new Intl.DateTimeFormat('default',
{
hour12: true,
hour: 'numeric',
minute: 'numeric'
});
let hourMinute = (d) => {
return intl.format(d);
};
$scope.updateBasics = () => {
$scope.channelNumberWidth = 5;
$scope.channelIconWidth = 8;
$scope.channelWidth = $scope.channelNumberWidth + $scope.channelIconWidth;
//we want 1 minute = 1 colspan
$scope.colspanPercent = (100 - $scope.channelWidth) / ($scope.T / MINUTE);
$scope.channelColspan = Math.floor($scope.channelWidth / $scope.colspanPercent);
$scope.channelNumberColspan = Math.floor($scope.channelNumberWidth / $scope.colspanPercent);
$scope.channelIconColspan = $scope.channelColspan - $scope.channelNumberColspan;
$scope.totalSpan = Math.floor($scope.T / MINUTE);
$scope.colspanPercent = (100 - $scope.channelWidth) / ($scope.T / MINUTE);
$scope.channelColspan = Math.floor($scope.channelWidth / $scope.colspanPercent);
$scope.channelNumberColspan = Math.floor($scope.channelNumberWidth / $scope.colspanPercent);
$scope.channelIconColspan = $scope.channelColspan - $scope.channelNumberColspan;
}
$scope.updateBasics();
let hourMinute = (d) => {
return intl.format(d);
};
$scope.updateBasics = () => {
$scope.channelNumberWidth = 5;
$scope.channelIconWidth = 8;
$scope.channelWidth = $scope.channelNumberWidth + $scope.channelIconWidth;
//we want 1 minute = 1 colspan
$scope.colspanPercent = (100 - $scope.channelWidth) / ($scope.T / MINUTE);
$scope.channelColspan = Math.floor(
$scope.channelWidth / $scope.colspanPercent,
);
$scope.channelNumberColspan = Math.floor(
$scope.channelNumberWidth / $scope.colspanPercent,
);
$scope.channelIconColspan =
$scope.channelColspan - $scope.channelNumberColspan;
$scope.totalSpan = Math.floor($scope.T / MINUTE);
$scope.colspanPercent = (100 - $scope.channelWidth) / ($scope.T / MINUTE);
$scope.channelColspan = Math.floor(
$scope.channelWidth / $scope.colspanPercent,
);
$scope.channelNumberColspan = Math.floor(
$scope.channelNumberWidth / $scope.colspanPercent,
);
$scope.channelIconColspan =
$scope.channelColspan - $scope.channelNumberColspan;
};
$scope.updateBasics();
$scope.channelNumberWidth = 5;
$scope.channelIconWidth = 8;
$scope.channelWidth = $scope.channelNumberWidth + $scope.channelIconWidth;
//we want 1 minute = 1 colspan
$scope.applyLater = () => {
$timeout( () => $scope.$apply(), 0 );
$scope.applyLater = () => {
$timeout(() => $scope.$apply(), 0);
};
$scope.channelNumbers = [];
$scope.channels = {};
$scope.lastUpdate = -1;
$scope.updateJustNow = () => {
$scope.t1 = new Date().getTime();
if ($scope.t0 <= $scope.t1 && $scope.t1 < $scope.t0 + $scope.T) {
let n = ($scope.t1 - $scope.t0) / MINUTE;
$scope.nowPosition = ($scope.channelColspan + n) * $scope.colspanPercent;
if ($scope.nowPosition >= 50 && $scope.offset >= 0) {
$scope.offset = 0;
$scope.adjustZoom();
}
$scope.showNow = true;
} else {
$scope.showNow = false;
}
};
$scope.nowTimer = () => {
$scope.updateJustNow();
$timeout(() => $scope.nowTimer(), 10000);
};
$timeout(() => $scope.nowTimer(), 10000);
$scope.refreshManaged = async (skipStatus) => {
$scope.t1 = new Date().getTime();
$scope.t1 = $scope.t1 - ($scope.t1 % MINUTE);
$scope.t0 = $scope.t1 - $scope.before + $scope.offset;
$scope.title = 'TV Guide';
$scope.times = [];
$scope.updateJustNow();
let pending = 0;
let addDuration = (d) => {
let m = (pending + d) % MINUTE;
let r = pending + d - m;
pending = m;
return Math.floor(r / MINUTE);
};
let deleteIfZero = () => {
if (
$scope.times.length > 0 &&
$scope.times[$scope.times.length - 1].duration < 1
) {
$scope.times = $scope.times.slice(0, $scope.times.length - 1);
}
};
$scope.channelNumbers = [];
$scope.channels = {};
$scope.lastUpdate = -1;
$scope.updateJustNow = () => {
$scope.t1 = (new Date()).getTime();
if ($scope.t0 <= $scope.t1 && $scope.t1 < $scope.t0 + $scope.T) {
let n = ($scope.t1 - $scope.t0) / MINUTE;
$scope.nowPosition = ($scope.channelColspan + n) * $scope.colspanPercent
if ($scope.nowPosition >= 50 && $scope.offset >= 0) {
$scope.offset = 0;
$scope.adjustZoom();
}
$scope.showNow = true;
} else {
$scope.showNow = false;
}
let rem = $scope.T;
let t = $scope.t0;
if (t % $scope.M != 0) {
let dif = $scope.M - (t % $scope.M);
$scope.times.push({
duration: addDuration(dif),
});
deleteIfZero();
t += dif;
rem -= dif;
}
while (rem > 0) {
let d = Math.min(rem, $scope.M);
$scope.times.push({
duration: addDuration(d),
label: hourMinute(new Date(t)),
});
t += d;
rem -= d;
}
$scope.nowTimer = () => {
$scope.updateJustNow();
$timeout( () => $scope.nowTimer() , 10000);
if (skipStatus !== true) {
$scope.channelNumbers = [0];
$scope.channels = {};
$scope.channels[0] = {
loading: true,
};
$scope.applyLater();
console.log('getting status...');
let status = await dizquetv.getGuideStatus();
$scope.lastUpdate = new Date(status.lastUpdate).getTime();
console.log('got status: ' + JSON.stringify(status));
$scope.channelNumbers = status.channelNumbers;
$scope.channels = {};
}
$timeout( () => $scope.nowTimer() , 10000);
for (let i = 0; i < $scope.channelNumbers.length; i++) {
if (typeof $scope.channels[$scope.channelNumbers[i]] === 'undefined') {
$scope.channels[$scope.channelNumbers[i]] = {};
}
$scope.channels[$scope.channelNumbers[i]].loading = true;
}
$scope.applyLater();
$scope.enableBack = false;
$scope.enableNext = false;
await Promise.all($scope.channelNumbers.map($scope.loadChannel));
setupTimer();
};
$scope.refreshManaged = async (skipStatus) => {
$scope.t1 = (new Date()).getTime();
$scope.t1 = ($scope.t1 - $scope.t1 % MINUTE );
$scope.t0 = $scope.t1 - $scope.before + $scope.offset;
$scope.title = "TV Guide";
$scope.times = [];
let cancelTimerIfExists = () => {
if ($scope.refreshHandle != null) {
$timeout.cancel($scope.refreshHandle);
}
};
$scope.updateJustNow();
let pending = 0;
let addDuration = (d) => {
let m = (pending + d) % MINUTE;
let r = (pending + d) - m;
pending = m;
return Math.floor( r / MINUTE );
}
let deleteIfZero = () => {
if ( $scope.times.length > 0 && $scope.times[$scope.times.length - 1].duration < 1) {
$scope.times = $scope.times.slice(0, $scope.times.length - 1);
}
}
$scope.$on('$locationChangeStart', () => {
console.log('$locationChangeStart');
cancelTimerIfExists();
});
let setupTimer = () => {
cancelTimerIfExists();
$scope.refreshHandle = $timeout(() => $scope.checkUpdates(), 60000);
};
let rem = $scope.T;
let t = $scope.t0;
if (t % $scope.M != 0) {
let dif = $scope.M - t % $scope.M;
$scope.times.push( {
duration : addDuration(dif),
} );
deleteIfZero();
t += dif;
rem -= dif;
}
while (rem > 0) {
let d = Math.min(rem, $scope.M );
$scope.times.push( {
duration : addDuration(d),
label: hourMinute( new Date(t) ),
} );
t += d;
rem -= d;
}
if (skipStatus !== true) {
$scope.channelNumbers = [0];
$scope.channels = {} ;
$scope.channels[0] = {
loading: true,
}
$scope.applyLater();
console.log("getting status...");
let status = await dizquetv.getGuideStatus();
$scope.lastUpdate = new Date(status.lastUpdate).getTime();
console.log("got status: " + JSON.stringify(status) );
$scope.channelNumbers = status.channelNumbers;
$scope.channels = {} ;
}
$scope.adjustZoom = async () => {
switch ($scope.zoomLevel) {
case 1:
$scope.T = 50 * MINUTE;
$scope.M = 10 * MINUTE;
$scope.before = 5 * MINUTE;
break;
case 2:
$scope.T = 100 * MINUTE;
$scope.M = 15 * MINUTE;
$scope.before = 10 * MINUTE;
break;
case 3:
$scope.T = 190 * MINUTE;
$scope.M = 30 * MINUTE;
$scope.before = 15 * MINUTE;
break;
case 4:
$scope.T = 270 * MINUTE;
$scope.M = 60 * MINUTE;
$scope.before = 15 * MINUTE;
break;
case 5:
$scope.T = 380 * MINUTE;
$scope.M = 90 * MINUTE;
$scope.before = 15 * MINUTE;
break;
}
for (let i = 0; i < $scope.channelNumbers.length; i++) {
if ( typeof($scope.channels[$scope.channelNumbers[i]]) === 'undefined') {
$scope.channels[$scope.channelNumbers[i]] = {};
}
$scope.channels[$scope.channelNumbers[i]].loading = true;
$scope.updateBasics();
await $scope.refresh(true);
};
$scope.zoomOut = async () => {
$scope.zoomLevel = Math.min(5, $scope.zoomLevel + 1);
await $scope.adjustZoom();
};
$scope.zoomIn = async () => {
$scope.zoomLevel = Math.max(1, $scope.zoomLevel - 1);
await $scope.adjustZoom();
};
$scope.zoomOutEnabled = () => {
return $scope.zoomLevel < 5;
};
$scope.zoomInEnabled = () => {
return $scope.zoomLevel > 1;
};
$scope.next = async () => {
$scope.offset += ($scope.M * 7) / 8;
await $scope.adjustZoom();
};
$scope.back = async () => {
$scope.offset -= ($scope.M * 7) / 8;
await $scope.adjustZoom();
};
$scope.backEnabled = () => {
return $scope.enableBack;
};
$scope.nextEnabled = () => {
return $scope.enableNext;
};
$scope.loadChannel = async (number) => {
console.log(`number=${number}`);
let d0 = new Date($scope.t0);
let d1 = new Date($scope.t0 + $scope.T);
let lineup = await dizquetv.getChannelLineup(number, d0, d1);
let ch = {
icon: lineup.icon,
number: lineup.number,
name: lineup.name,
altTitle: `${lineup.number} - ${lineup.name}`,
programs: [],
};
let pending = 0;
let totalAdded = 0;
let addDuration = (d) => {
totalAdded += d;
let m = (pending + d) % MINUTE;
let r = pending + d - m;
pending = m;
return Math.floor(r / MINUTE);
};
let deleteIfZero = () => {
if (
ch.programs.length > 0 &&
ch.programs[ch.programs.length - 1].duration < 1
) {
ch.programs = ch.programs.slice(0, ch.programs.length - 1);
}
};
for (let i = 0; i < lineup.programs.length; i++) {
let program = lineup.programs[i];
let ad = new Date(program.start);
let bd = new Date(program.stop);
let a = ad.getTime();
let b = bd.getTime();
let hasStart = true;
let hasStop = true;
if (a < $scope.t0) {
//cut-off
a = $scope.t0;
hasStart = false;
$scope.enableBack = true;
} else if (a > $scope.t0 && i == 0) {
ch.programs.push({
duration: addDuration(a - $scope.t0),
showTitle: '',
start: false,
end: true,
});
deleteIfZero();
}
if (b > $scope.t0 + $scope.T) {
b = $scope.t0 + $scope.T;
hasStop = false;
$scope.enableNext = true;
}
let subTitle = undefined;
let episodeTitle = undefined;
let altTitle = hourMinute(ad) + '-' + hourMinute(bd);
if (typeof program.title !== 'undefined') {
altTitle = altTitle + ' · ' + program.title;
}
if (typeof program.sub !== 'undefined') {
ps = '' + program.sub.season;
if (ps.length < 2) {
ps = '0' + ps;
}
$scope.applyLater();
$scope.enableBack = false;
$scope.enableNext = false;
await Promise.all($scope.channelNumbers.map( $scope.loadChannel) );
pe = '' + program.sub.episode;
if (pe.length < 2) {
pe = '0' + pe;
}
subTitle = `S${ps} · E${pe}`;
altTitle = altTitle + ' ' + subTitle;
episodeTitle = program.sub.title;
} else if (typeof program.date === 'undefined') {
subTitle = '.';
} else {
subTitle = program.date.slice(0, 4);
}
ch.programs.push({
duration: addDuration(b - a),
altTitle: altTitle,
showTitle: program.title,
subTitle: subTitle,
episodeTitle: episodeTitle,
start: hasStart,
end: hasStop,
});
deleteIfZero();
}
if (totalAdded < $scope.T) {
ch.programs.push({
duration: addDuration($scope.T - totalAdded),
showTitle: '',
start: false,
end: true,
});
deleteIfZero();
}
$scope.channels[number] = ch;
$scope.applyLater();
};
$scope.refresh = async (skipStatus) => {
try {
await $scope.refreshManaged(skipStatus);
} catch (err) {
console.error('Refresh failed?', err);
}
};
$scope.adjustZoom();
$scope.refresh();
$scope.checkUpdates = async () => {
try {
console.log('get status ' + new Date());
let status = await dizquetv.getGuideStatus();
let t = new Date(status.lastUpdate).getTime();
if (t > $scope.lastUpdate) {
$scope.refreshManaged();
} else {
setupTimer();
};
let cancelTimerIfExists = () => {
if ($scope.refreshHandle != null) {
$timeout.cancel($scope.refreshHandle);
}
}
} catch (err) {
console.error(err);
}
$scope.$on('$locationChangeStart', () => {
console.log("$locationChangeStart" );
cancelTimerIfExists();
} );
let setupTimer = () => {
cancelTimerIfExists();
$scope.refreshHandle = $timeout( () => $scope.checkUpdates(), 60000 );
}
$scope.adjustZoom = async() => {
switch ($scope.zoomLevel) {
case 1:
$scope.T = 50 * MINUTE;
$scope.M = 10 * MINUTE;
$scope.before = 5 * MINUTE;
break;
case 2:
$scope.T = 100 * MINUTE;
$scope.M = 15 * MINUTE;
$scope.before = 10 * MINUTE;
break;
case 3:
$scope.T = 190 * MINUTE;
$scope.M = 30 * MINUTE;
$scope.before = 15 * MINUTE;
break;
case 4:
$scope.T = 270 * MINUTE;
$scope.M = 60 * MINUTE;
$scope.before = 15 * MINUTE;
break;
case 5:
$scope.T = 380 * MINUTE;
$scope.M = 90 * MINUTE;
$scope.before = 15 * MINUTE;
break;
}
$scope.updateBasics();
await $scope.refresh(true);
}
$scope.zoomOut = async() => {
$scope.zoomLevel = Math.min( 5, $scope.zoomLevel + 1 );
await $scope.adjustZoom();
}
$scope.zoomIn = async() => {
$scope.zoomLevel = Math.max( 1, $scope.zoomLevel - 1 );
await $scope.adjustZoom();
}
$scope.zoomOutEnabled = () => {
return $scope.zoomLevel < 5;
}
$scope.zoomInEnabled = () => {
return $scope.zoomLevel > 1;
}
$scope.next = async() => {
$scope.offset += $scope.M * 7 / 8
await $scope.adjustZoom();
}
$scope.back = async() => {
$scope.offset -= $scope.M * 7 / 8
await $scope.adjustZoom();
}
$scope.backEnabled = () => {
return $scope.enableBack;
}
$scope.nextEnabled = () => {
return $scope.enableNext;
}
$scope.loadChannel = async (number) => {
console.log(`number=${number}` );
let d0 = new Date($scope.t0);
let d1 = new Date($scope.t0 + $scope.T);
let lineup = await dizquetv.getChannelLineup(number, d0, d1);
let ch = {
icon : lineup.icon,
number : lineup.number,
name: lineup.name,
altTitle: `${lineup.number} - ${lineup.name}`,
programs: [],
};
let pending = 0;
let totalAdded = 0;
let addDuration = (d) => {
totalAdded += d;
let m = (pending + d) % MINUTE;
let r = (pending + d) - m;
pending = m;
return Math.floor( r / MINUTE );
}
let deleteIfZero = () => {
if ( ch.programs.length > 0 && ch.programs[ ch.programs.length - 1].duration < 1) {
ch.programs = ch.programs.slice(0, ch.programs.length - 1);
}
}
for (let i = 0; i < lineup.programs.length; i++) {
let program = lineup.programs[i];
let ad = new Date(program.start);
let bd = new Date(program.stop);
let a = ad.getTime();
let b = bd.getTime();
let hasStart = true;
let hasStop = true;
if (a < $scope.t0) {
//cut-off
a = $scope.t0;
hasStart = false;
$scope.enableBack = true;
} else if ( (a > $scope.t0) && (i == 0) ) {
ch.programs.push( {
duration: addDuration( (a - $scope.t0) ),
showTitle: "",
start: false,
end: true,
} );
deleteIfZero();
}
if (b > $scope.t0 + $scope.T) {
b = $scope.t0 + $scope.T;
hasStop = false;
$scope.enableNext = true;
}
let subTitle = undefined;
let episodeTitle = undefined;
let altTitle = hourMinute(ad) + "-" + hourMinute(bd);
if (typeof(program.title) !== 'undefined') {
altTitle = altTitle + " · " + program.title;
}
if (typeof(program.sub) !== 'undefined') {
ps = "" + program.sub.season;
if (ps.length < 2) {
ps = "0" + ps;
}
pe = "" + program.sub.episode;
if (pe.length < 2) {
pe = "0" + pe;
}
subTitle = `S${ps} · E${pe}`;
altTitle = altTitle + " " + subTitle;
episodeTitle = program.sub.title;
} else if ( typeof(program.date) === 'undefined' ) {
subTitle = '.';
} else {
subTitle = program.date.slice(0,4);
}
ch.programs.push( {
duration: addDuration(b - a),
altTitle: altTitle,
showTitle: program.title,
subTitle: subTitle,
episodeTitle : episodeTitle,
start: hasStart,
end: hasStop,
} );
deleteIfZero();
}
if (totalAdded < $scope.T) {
ch.programs.push( {
duration: addDuration( $scope.T - totalAdded ),
showTitle: "",
start: false,
end: true,
} );
deleteIfZero();
}
$scope.channels[number] = ch;
$scope.applyLater();
}
$scope.refresh = async (skipStatus) => {
try {
await $scope.refreshManaged(skipStatus);
} catch (err) {
console.error("Refresh failed?", err);
}
}
$scope.adjustZoom();
$scope.refresh();
$scope.checkUpdates = async () => {
try {
console.log("get status " + new Date() );
let status = await dizquetv.getGuideStatus();
let t = new Date(status.lastUpdate).getTime();
if ( t > $scope.lastUpdate) {
$scope.refreshManaged();
} else {
setupTimer();
}
} catch(err) {
console.error(err);
}
};
}
};
};

View File

@@ -25,7 +25,7 @@
<span ng-show="!x.pending">{{x.number}}</span>
</td>
<td style="padding: 0" class="text-center">
<img ng-if="x.icon !== ''" ng-src="{{x.icon}}" alt="{{x.name}}" style="max-height: 40px;"></img>
<img ng-if="x.icon !== ''" ng-src="{{x.icon.path}}" alt="{{x.name}}" style="max-height: 40px;"></img>
<div ng-if="x.icon === ''" style="padding-top: 14px;"><small>{{x.name}}</small></div>
</td>
<td>{{x.name}} <span ng-if='x.stealth===true' class='text-muted'>(Stealth)</span></td>

View File

@@ -38,7 +38,7 @@
</div>
</td>
<td title='{{channels[channelNumber].altTitle}}' class='even channel-icon' colspan="{{channelIconColspan}}" >
<img src='{{channels[channelNumber].icon}}' alt='{{channels[channelNumber].name}}' ng-click='channels[channelNumber].mouse=true' ></img>
<img src='{{channels[channelNumber].icon.path}}' alt='{{channels[channelNumber].name}}' ng-click='channels[channelNumber].mouse=true' ></img>
</td>
<td class='odd program' colspan="{{totalSpan}}" ng-if="channels[channelNumber].loading">

View File

@@ -159,7 +159,6 @@ module.exports = function ($http, $q) {
},
getChannels: () => {
return $http.get('/api/channels').then((d) => {
console.log(d);
return d.data;
});
},

View File

@@ -1,6 +1,4 @@
import { Plex } from '../../build/src/plex.js';
export default function ($http, $window, $interval) {
module.exports = function ($http, $window, $interval, dizquetv) {
let exported = {
login: async () => {
const headers = {
@@ -103,9 +101,15 @@ export default function ($http, $window, $interval) {
},
check: async (server) => {
let client = new Plex(server);
try {
const res = await client.Get('/');
await $http({
method: 'GET',
url: '/api/plex',
params: {
path: '/',
name: server.name,
},
});
return 1;
} catch (err) {
console.error(err);
@@ -114,8 +118,14 @@ export default function ($http, $window, $interval) {
},
getLibrary: async (server) => {
var client = new Plex(server);
const res = await client.Get('/library/sections');
const res = await $http({
method: 'GET',
url: '/api/plex',
params: {
path: '/library/sections',
name: server.name,
},
});
var sections = [];
for (
let i = 0,
@@ -163,8 +173,14 @@ export default function ($http, $window, $interval) {
return sections;
},
getPlaylists: async (server) => {
var client = new Plex(server);
const res = await client.Get('/playlists');
const res = await $http({
method: 'GET',
url: '/api/plex',
params: {
path: '/playlists',
name: server.name,
},
});
var playlists = [];
for (
let i = 0,
@@ -186,23 +202,34 @@ export default function ($http, $window, $interval) {
return playlists;
},
getStreams: async (server, key) => {
var client = new Plex(server);
return client.Get(key).then((res) => {
let streams = res.Metadata[0].Media[0].Part[0].Stream;
for (let i = 0, l = streams.length; i < l; i++) {
if (typeof streams[i].key !== 'undefined') {
streams[
i
].key = `${server.uri}${streams[i].key}?X-Plex-Token=${server.accessToken}`;
}
}
return streams;
const res = await $http({
method: 'GET',
url: '/api/plex',
params: {
path: key,
name: server.name,
},
});
let streams = res.Metadata[0].Media[0].Part[0].Stream;
for (let i = 0, l = streams.length; i < l; i++) {
if (typeof streams[i].key !== 'undefined') {
streams[
i
].key = `${server.uri}${streams[i].key}?X-Plex-Token=${server.accessToken}`;
}
}
return streams;
},
getNested: async (server, lib, includeCollections, errors) => {
var client = new Plex(server);
const key = lib.key;
const res = await client.Get(key);
const res = await $http({
method: 'GET',
url: '/api/plex',
params: {
path: key,
name: server.name,
},
});
const size =
typeof res.Metadata !== 'undefined' ? res.Metadata.length : 0;
@@ -365,7 +392,7 @@ export default function ($http, $window, $interval) {
},
};
return exported;
}
};
function msToTime(duration) {
var milliseconds = parseInt((duration % 1000) / 100),