commit
65507f8cb2
@ -16,6 +16,7 @@
|
||||
"author": "Dan Ferguson",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"JSONStream": "1.0.5",
|
||||
"angular": "^1.7.9",
|
||||
"angular-router-browserify": "0.0.2",
|
||||
"angular-vs-repeat": "2.0.13",
|
||||
|
||||
68
src/api.js
68
src/api.js
@ -5,6 +5,7 @@ const fs = require('fs')
|
||||
const databaseMigration = require('./database-migration');
|
||||
const channelCache = require('./channel-cache')
|
||||
const constants = require('./constants');
|
||||
const JSONStream = require('JSONStream');
|
||||
const FFMPEGInfo = require('./ffmpeg-info');
|
||||
const PlexServerDB = require('./dao/plex-server-db');
|
||||
const Plex = require("./plex.js");
|
||||
@ -232,9 +233,10 @@ function api(db, channelDB, fillerDB, customShowDB, xmltvInterval, guideService
|
||||
try {
|
||||
let number = parseInt(req.params.number, 10);
|
||||
let channel = await channelCache.getChannelConfig(channelDB, number);
|
||||
|
||||
if (channel.length == 1) {
|
||||
channel = channel[0];
|
||||
res.send( channel );
|
||||
channel = channel[0];
|
||||
res.send(channel);
|
||||
} else {
|
||||
return res.status(404).send("Channel not found");
|
||||
}
|
||||
@ -243,6 +245,61 @@ function api(db, channelDB, fillerDB, customShowDB, xmltvInterval, guideService
|
||||
res.status(500).send("error");
|
||||
}
|
||||
})
|
||||
router.get('/api/channel/programless/:number', async (req, res) => {
|
||||
try {
|
||||
let number = parseInt(req.params.number, 10);
|
||||
let channel = await channelCache.getChannelConfig(channelDB, number);
|
||||
|
||||
if (channel.length == 1) {
|
||||
channel = channel[0];
|
||||
let copy = {};
|
||||
Object.keys(channel).forEach( (key) => {
|
||||
if (key != 'programs') {
|
||||
copy[key] = channel[key];
|
||||
}
|
||||
} );
|
||||
res.send(copy);
|
||||
} else {
|
||||
return res.status(404).send("Channel not found");
|
||||
}
|
||||
} catch(err) {
|
||||
console.error(err);
|
||||
res.status(500).send("error");
|
||||
}
|
||||
})
|
||||
|
||||
router.get('/api/channel/programs/:number', async (req, res) => {
|
||||
try {
|
||||
let number = parseInt(req.params.number, 10);
|
||||
let channel = await channelCache.getChannelConfig(channelDB, number);
|
||||
|
||||
if (channel.length == 1) {
|
||||
channel = channel[0];
|
||||
let programs = channel.programs;
|
||||
if (typeof(programs) === 'undefined') {
|
||||
return res.status(404).send("Channel doesn't have programs?");
|
||||
}
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'application.json'
|
||||
});
|
||||
|
||||
let transformStream = JSONStream.stringify(); //false makes it not add 'separators'
|
||||
transformStream.pipe(res);
|
||||
|
||||
for (let i = 0; i < programs.length; i++) {
|
||||
transformStream.write( programs[i] );
|
||||
await throttle();
|
||||
}
|
||||
transformStream.end();
|
||||
|
||||
} else {
|
||||
return res.status(404).send("Channel not found");
|
||||
}
|
||||
} catch(err) {
|
||||
console.error(err);
|
||||
res.status(500).send("error");
|
||||
}
|
||||
})
|
||||
router.get('/api/channel/description/:number', async (req, res) => {
|
||||
try {
|
||||
let number = parseInt(req.params.number, 10);
|
||||
@ -1014,3 +1071,10 @@ function api(db, channelDB, fillerDB, customShowDB, xmltvInterval, guideService
|
||||
|
||||
return router
|
||||
}
|
||||
|
||||
|
||||
async function throttle() {
|
||||
return new Promise((resolve) => {
|
||||
setImmediate(() => resolve());
|
||||
});
|
||||
}
|
||||
|
||||
@ -35,6 +35,7 @@ class PlexTranscoder {
|
||||
this.updateInterval = 30000
|
||||
this.updatingPlex = undefined
|
||||
this.playState = "stopped"
|
||||
this.mediaHasNoVideo = false;
|
||||
this.albumArt = {
|
||||
attempted : false,
|
||||
path: null,
|
||||
@ -48,23 +49,26 @@ class PlexTranscoder {
|
||||
this.log(` deinterlace: ${deinterlace}`)
|
||||
this.log(` streamPath: ${this.settings.streamPath}`)
|
||||
|
||||
this.setTranscodingArgs(stream.directPlay, true, false, false);
|
||||
await this.tryToDetectAudioOnly();
|
||||
|
||||
if (this.settings.streamPath === 'direct' || this.settings.forceDirectPlay) {
|
||||
if (this.settings.enableSubtitles) {
|
||||
console.log("Direct play is forced, so subtitles are forcibly disabled.");
|
||||
this.log("Direct play is forced, so subtitles are forcibly disabled.");
|
||||
this.settings.enableSubtitles = false;
|
||||
}
|
||||
stream = {directPlay: true}
|
||||
} else {
|
||||
try {
|
||||
this.log("Setting transcoding parameters")
|
||||
this.setTranscodingArgs(stream.directPlay, true, deinterlace, true)
|
||||
this.setTranscodingArgs(stream.directPlay, true, deinterlace, this.mediaHasNoVideo)
|
||||
await this.getDecision(stream.directPlay);
|
||||
if (this.isDirectPlay()) {
|
||||
stream.directPlay = true;
|
||||
stream.streamUrl = this.plexFile;
|
||||
}
|
||||
} catch (err) {
|
||||
this.log("Error when getting decision. 1. Check Plex connection. 2. This might also be a sign that plex direct play and transcode settings are too strict and it can't find any allowed action for the selected video.")
|
||||
console.error("Error when getting decision. 1. Check Plex connection. 2. This might also be a sign that plex direct play and transcode settings are too strict and it can't find any allowed action for the selected video.", err)
|
||||
stream.directPlay = true;
|
||||
}
|
||||
}
|
||||
@ -74,7 +78,7 @@ class PlexTranscoder {
|
||||
}
|
||||
this.log("Direct play forced or native paths enabled")
|
||||
stream.directPlay = true
|
||||
this.setTranscodingArgs(stream.directPlay, true, false)
|
||||
this.setTranscodingArgs(stream.directPlay, true, false, this.mediaHasNoVideo )
|
||||
// Update transcode decision for session
|
||||
await this.getDecision(stream.directPlay);
|
||||
stream.streamUrl = (this.settings.streamPath === 'direct') ? this.file : this.plexFile;
|
||||
@ -92,7 +96,7 @@ class PlexTranscoder {
|
||||
} else if (this.isVideoDirectStream() === false) {
|
||||
this.log("Decision: Should transcode")
|
||||
// Change transcoding arguments to be the user chosen transcode parameters
|
||||
this.setTranscodingArgs(stream.directPlay, false, deinterlace)
|
||||
this.setTranscodingArgs(stream.directPlay, false, deinterlace, this.mediaHasNoVideo)
|
||||
// Update transcode decision for session
|
||||
await this.getDecision(stream.directPlay);
|
||||
stream.streamUrl = `${this.transcodeUrlBase}${this.transcodingArgs}`
|
||||
@ -114,13 +118,13 @@ class PlexTranscoder {
|
||||
return stream
|
||||
}
|
||||
|
||||
setTranscodingArgs(directPlay, directStream, deinterlace, firstTry) {
|
||||
setTranscodingArgs(directPlay, directStream, deinterlace, audioOnly) {
|
||||
let resolution = (directStream) ? this.settings.maxPlayableResolution : this.settings.maxTranscodeResolution
|
||||
let bitrate = (directStream) ? this.settings.directStreamBitrate : this.settings.transcodeBitrate
|
||||
let mediaBufferSize = (directStream) ? this.settings.mediaBufferSize : this.settings.transcodeMediaBufferSize
|
||||
let subtitles = (this.settings.enableSubtitles) ? "burn" : "none" // subtitle options: burn, none, embedded, sidecar
|
||||
let streamContainer = "mpegts" // Other option is mkv, mkv has the option of copying it's subs for later processing
|
||||
let isDirectPlay = (directPlay) ? '1' : (firstTry? '': '0');
|
||||
let isDirectPlay = (directPlay) ? '1' : '0';
|
||||
let hasMDE = '1';
|
||||
|
||||
let videoQuality=`100` // Not sure how this applies, maybe this works if maxVideoBitrate is not set
|
||||
@ -137,12 +141,17 @@ class PlexTranscoder {
|
||||
vc = "av1";
|
||||
}
|
||||
|
||||
let clientProfile=`add-transcode-target(type=videoProfile&protocol=${this.settings.streamProtocol}&container=${streamContainer}&videoCodec=${vc}&audioCodec=${this.settings.audioCodecs}&subtitleCodec=&context=streaming&replace=true)+\
|
||||
let clientProfile ="";
|
||||
if (! audioOnly ) {
|
||||
clientProfile=`add-transcode-target(type=videoProfile&protocol=${this.settings.streamProtocol}&container=${streamContainer}&videoCodec=${vc}&audioCodec=${this.settings.audioCodecs}&subtitleCodec=&context=streaming&replace=true)+\
|
||||
add-transcode-target-settings(type=videoProfile&context=streaming&protocol=${this.settings.streamProtocol}&CopyMatroskaAttachments=true)+\
|
||||
add-transcode-target-settings(type=videoProfile&context=streaming&protocol=${this.settings.streamProtocol}&BreakNonKeyframes=true)+\
|
||||
add-limitation(scope=videoCodec&scopeName=*&type=upperBound&name=video.width&value=${resolutionArr[0]})+\
|
||||
add-limitation(scope=videoCodec&scopeName=*&type=upperBound&name=video.height&value=${resolutionArr[1]})`
|
||||
|
||||
} else {
|
||||
clientProfile=`add-transcode-target(type=musicProfile&protocol=${this.settings.streamProtocol}&container=${streamContainer}&audioCodec=${this.settings.audioCodecs}&subtitleCodec=&context=streaming&replace=true)`
|
||||
|
||||
}
|
||||
// Set transcode settings per audio codec
|
||||
this.settings.audioCodecs.split(",").forEach(function (codec) {
|
||||
clientProfile+=`+add-transcode-target-audio-codec(type=videoProfile&context=streaming&protocol=${this.settings.streamProtocol}&audioCodec=${codec})`
|
||||
@ -196,7 +205,7 @@ lang=en`
|
||||
try {
|
||||
return this.getVideoStats().videoDecision === "copy";
|
||||
} catch (e) {
|
||||
console.log("Error at decision:", e);
|
||||
console.error("Error at decision:", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@ -216,7 +225,7 @@ lang=en`
|
||||
}
|
||||
return this.getVideoStats().videoDecision === "copy" && this.getVideoStats().audioDecision === "copy";
|
||||
} catch (e) {
|
||||
console.log("Error at decision:" , e);
|
||||
console.error("Error at decision:" , e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@ -262,7 +271,7 @@ lang=en`
|
||||
}
|
||||
}.bind(this) )
|
||||
} catch (e) {
|
||||
console.log("Error at decision:" , e);
|
||||
console.error("Error at decision:" , e);
|
||||
}
|
||||
if (typeof(ret.videoCodec) === 'undefined') {
|
||||
ret.audioOnly = true;
|
||||
@ -297,11 +306,11 @@ lang=en`
|
||||
}
|
||||
})
|
||||
} catch (e) {
|
||||
console.log("Error at get media info:" + e);
|
||||
console.error("Error at get media info:" + e);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
console.error("Error getting audio index",err);
|
||||
});
|
||||
|
||||
this.log(`Found audio index: ${index}`)
|
||||
@ -326,36 +335,42 @@ lang=en`
|
||||
|
||||
// Print error message if transcode not possible
|
||||
// TODO: handle failure better
|
||||
if (res.data.MediaContainer.mdeDecisionCode === 1000) {
|
||||
this.log("mde decision code 1000, so it's all right?");
|
||||
return;
|
||||
}
|
||||
|
||||
let transcodeDecisionCode = res.data.MediaContainer.transcodeDecisionCode;
|
||||
if (
|
||||
( typeof(transcodeDecisionCode) === 'undefined' )
|
||||
) {
|
||||
this.decisionJson.MediaContainer.transcodeDecisionCode = 'novideo';
|
||||
console.log("Audio-only file detected");
|
||||
await this.tryToGetAlbumArt();
|
||||
this.log("Strange case, attempt direct play");
|
||||
} else if (!(directPlay || transcodeDecisionCode == "1001")) {
|
||||
console.log(`IMPORTANT: Recieved transcode decision code ${transcodeDecisionCode}! Expected code 1001.`)
|
||||
console.log(`Error message: '${res.data.MediaContainer.transcodeDecisionText}'`)
|
||||
this.log(`IMPORTANT: Recieved transcode decision code ${transcodeDecisionCode}! Expected code 1001.`)
|
||||
this.log(`Error message: '${res.data.MediaContainer.transcodeDecisionText}'`)
|
||||
}
|
||||
}
|
||||
|
||||
async tryToGetAlbumArt() {
|
||||
|
||||
async tryToDetectAudioOnly() {
|
||||
try {
|
||||
if(this.albumArt.attempted ) {
|
||||
return;
|
||||
}
|
||||
this.albumArt.attempted = true;
|
||||
|
||||
this.log("Try to get album art:");
|
||||
this.log("Try to detect audio only:");
|
||||
let url = `${this.server.uri}${this.key}?${this.transcodingArgs}`;
|
||||
let res = await axios.get(url, {
|
||||
headers: { Accept: 'application/json' }
|
||||
});
|
||||
|
||||
let mediaContainer = res.data.MediaContainer;
|
||||
if (typeof(mediaContainer) !== 'undefined') {
|
||||
for( let i = 0; i < mediaContainer.Metadata.length; i++) {
|
||||
console.log("got art: " + mediaContainer.Metadata[i].thumb );
|
||||
this.albumArt.path = `${this.server.uri}${mediaContainer.Metadata[i].thumb}?X-Plex-Token=${this.server.accessToken}`;
|
||||
let metadata = getOneOrUndefined( mediaContainer, "Metadata");
|
||||
if (typeof(metadata) !== 'undefined') {
|
||||
this.albumArt.path = `${this.server.uri}${metadata.thumb}?X-Plex-Token=${this.server.accessToken}`;
|
||||
|
||||
let media = getOneOrUndefined( metadata, "Media");
|
||||
if (typeof(media) !== 'undefined') {
|
||||
if (typeof(media.videoCodec)==='undefined') {
|
||||
this.log("Audio-only file detected");
|
||||
this.mediaHasNoVideo = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
@ -447,4 +462,19 @@ function parsePixelAspectRatio(s) {
|
||||
q: parseInt(x[1], 10),
|
||||
}
|
||||
}
|
||||
|
||||
function getOneOrUndefined(object, field) {
|
||||
if (typeof(object) === 'undefined') {
|
||||
return undefined;
|
||||
}
|
||||
if ( typeof(object[field]) === "undefined") {
|
||||
return undefined;
|
||||
}
|
||||
let x = object[field];
|
||||
if (x.length < 1) {
|
||||
return undefined;
|
||||
}
|
||||
return x[0];
|
||||
}
|
||||
|
||||
module.exports = PlexTranscoder
|
||||
|
||||
@ -14,8 +14,6 @@ class TVGuideService
|
||||
this.currentUpdate = -1;
|
||||
this.currentLimit = -1;
|
||||
this.currentChannels = null;
|
||||
this.throttleX = 0;
|
||||
this.doThrottle = false;
|
||||
this.xmltv = xmltv;
|
||||
this.db = db;
|
||||
this.cacheImageService = cacheImageService;
|
||||
@ -26,7 +24,7 @@ class TVGuideService
|
||||
while (this.cached == null) {
|
||||
await _wait(100);
|
||||
}
|
||||
this.doThrottle = true;
|
||||
|
||||
return this.cached;
|
||||
}
|
||||
|
||||
@ -357,11 +355,10 @@ class TVGuideService
|
||||
}
|
||||
}
|
||||
|
||||
async _throttle() {
|
||||
//this.doThrottle = true;
|
||||
if ( this.doThrottle && (this.throttleX++)%10 == 0) {
|
||||
await _wait(0);
|
||||
}
|
||||
_throttle() {
|
||||
return new Promise((resolve) => {
|
||||
setImmediate(() => resolve());
|
||||
});
|
||||
}
|
||||
|
||||
async refreshXML() {
|
||||
|
||||
@ -80,7 +80,12 @@ module.exports = function ($scope, dizquetv) {
|
||||
$scope.showChannelConfig = true
|
||||
} else {
|
||||
$scope.channels[index].pending = true;
|
||||
let ch = await dizquetv.getChannel($scope.channels[index].number);
|
||||
let p = await Promise.all([
|
||||
dizquetv.getChannelProgramless($scope.channels[index].number),
|
||||
dizquetv.getChannelPrograms($scope.channels[index].number),
|
||||
]);
|
||||
let ch = p[0];
|
||||
ch.programs = p[1];
|
||||
let newObj = ch;
|
||||
newObj.startTime = new Date(newObj.startTime)
|
||||
$scope.originalChannelNumber = newObj.number;
|
||||
|
||||
@ -137,6 +137,13 @@ module.exports = function ($http, $q) {
|
||||
return $http.get(`/api/channel/description/${number}`).then( (d) => { return d.data } )
|
||||
},
|
||||
|
||||
getChannelProgramless: (number) => {
|
||||
return $http.get(`/api/channel/programless/${number}`).then( (d) => { return d.data })
|
||||
},
|
||||
getChannelPrograms: (number) => {
|
||||
return $http.get(`/api/channel/programs/${number}`).then( (d) => { return d.data } )
|
||||
},
|
||||
|
||||
|
||||
getChannelNumbers: () => {
|
||||
return $http.get('/api/channelNumbers').then( (d) => { return d.data } )
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user