422 lines
16 KiB
JavaScript
422 lines
16 KiB
JavaScript
/**
|
|
* Setting up channels is a lot of work. Same goes for configuration.
|
|
* Also, it's always healthy to be releasing versions frequently and have people
|
|
* test them frequently. But if losing data after upgrades is a common ocurrence
|
|
* then users will just not want to give new versions a try. That's why
|
|
* starting with version 0.0.54 and forward we don't want users to be losing
|
|
* data just because they upgraded their channels. In order to accomplish that
|
|
* we need some work put into the db structure so that it is capable of
|
|
* being updated.
|
|
*
|
|
* Even if we reached a point like in 0.0.53 where the old channels have to
|
|
* be completely deleted and can't be recovered. Then that's what the migration
|
|
* should do. Remove the information that can't be recovered and notify the
|
|
* user about it.
|
|
*
|
|
* A lot of this will look like overkill during the first versions it's used
|
|
* but with time it will be worth it, really.
|
|
*
|
|
***/
|
|
const TARGET_VERSION = 200;
|
|
|
|
const STEPS = [
|
|
// [v, v2, x] : if the current version is v, call x(db), and version becomes v2
|
|
[ 0, 100, (db) => basicDB(db) ],
|
|
[ 100, 200, (db) => commercialsRemover(db) ],
|
|
]
|
|
|
|
|
|
function basicDB(db) {
|
|
//this one should either try recovering the db from a very old version
|
|
//or buildl a completely empty db at version 0
|
|
let ffmpegSettings = db['ffmpeg-settings'].find()
|
|
let plexSettings = db['plex-settings'].find()
|
|
|
|
var ffmpegRepaired = repairFFmpeg0(ffmpegSettings);
|
|
if (ffmpegRepaired.hasBeenRepaired) {
|
|
var fixed = ffmpegRepaired.fixedConfig;
|
|
var i = fixed._id;
|
|
if ( i == null || typeof(i) == 'undefined') {
|
|
db['ffmpeg-settings'].save(fixed);
|
|
} else {
|
|
db['ffmpeg-settings'].update( { _id: i } , fixed );
|
|
}
|
|
}
|
|
|
|
if (plexSettings.length === 0) {
|
|
db['plex-settings'].save({
|
|
streamPath: 'plex',
|
|
debugLogging: true,
|
|
directStreamBitrate: '40000',
|
|
transcodeBitrate: '3000',
|
|
mediaBufferSize: 1000,
|
|
transcodeMediaBufferSize: 20000,
|
|
maxPlayableResolution: "1920x1080",
|
|
maxTranscodeResolution: "1920x1080",
|
|
videoCodecs: 'h264,hevc,mpeg2video',
|
|
audioCodecs: 'ac3',
|
|
maxAudioChannels: '2',
|
|
audioBoost: '100',
|
|
enableSubtitles: false,
|
|
subtitleSize: '100',
|
|
updatePlayStatus: false,
|
|
streamProtocol: 'http',
|
|
forceDirectPlay: false,
|
|
pathReplace: '',
|
|
pathReplaceWith: ''
|
|
})
|
|
}
|
|
let plexServers = db['plex-servers'].find();
|
|
//plex servers exist, but they could be old
|
|
let newPlexServers = {};
|
|
for (let i = 0; i < plexServers.length; i++) {
|
|
let plex = plexServers[i];
|
|
if ( (typeof(plex.connections) === 'undefined') || plex.connections.length==0) {
|
|
let newPlex = attemptMigratePlexFrom51(plex);
|
|
newPlexServers[plex.name] = newPlex;
|
|
db['plex-servers'].update( { _id: plex._id }, newPlex );
|
|
}
|
|
}
|
|
if (Object.keys(newPlexServers).length !== 0) {
|
|
migrateChannelsFrom51(db, newPlexServers);
|
|
}
|
|
|
|
|
|
let xmltvSettings = db['xmltv-settings'].find()
|
|
if (xmltvSettings.length === 0) {
|
|
db['xmltv-settings'].save({
|
|
cache: 12,
|
|
refresh: 4,
|
|
file: `${process.env.DATABASE}/xmltv.xml`
|
|
})
|
|
}
|
|
let hdhrSettings = db['hdhr-settings'].find()
|
|
if (hdhrSettings.length === 0) {
|
|
db['hdhr-settings'].save({
|
|
tunerCount: 1,
|
|
autoDiscovery: true
|
|
})
|
|
}
|
|
}
|
|
|
|
function migrateChannelsFrom51(db, newPlexServers) {
|
|
console.log("Attempting to migrate channels from old format. This may take a while...");
|
|
let channels = db['channels'].find();
|
|
function fix(program) {
|
|
if (typeof(program.plexFile) === 'undefined') {
|
|
let file = program.file;
|
|
program.plexFile = file.slice( program.server.uri.length );
|
|
let i = 0;
|
|
while (i < program.plexFile.length && program.plexFile.charAt(i) != '?') {
|
|
i++;
|
|
}
|
|
program.plexFile = program.plexFile.slice(0, i);
|
|
delete program.file;
|
|
|
|
}
|
|
};
|
|
for (let i = 0; i < channels.length; i++) {
|
|
let channel = channels[i];
|
|
let programs = channel.programs;
|
|
let newPrograms = [];
|
|
for (let j = 0; j < programs.length; j++) {
|
|
let program = programs[j];
|
|
if (
|
|
(typeof(program.server) === 'undefined') || (typeof(program.server.name) === 'undefined')
|
|
|| ( ( typeof(program.plexFile)==='undefined' ) && ( typeof(program.file)==='undefined' ) )
|
|
) {
|
|
let duration = program.duration;
|
|
if (typeof(duration) !== 'undefined') {
|
|
console.log(`A program in channel ${channel.number} doesn't have server/plex file information. Replacing it with Flex time`);
|
|
program = {
|
|
isOffline : true,
|
|
actualDuration : duration,
|
|
duration : duration,
|
|
};
|
|
newPrograms.push( program );
|
|
} else {
|
|
console.log(`A program in channel ${channel.number} is completely invalid and has been removed.`);
|
|
}
|
|
} else {
|
|
if ( typeof(newPlexServers[program.server.name]) !== 'undefined' ) {
|
|
program.server = newPlexServers[program.server.name];
|
|
} else {
|
|
console.log("turns out '" + program.server.name + "' is not in " + JSON.stringify(newPlexServers) );
|
|
}
|
|
let commercials = program.commercials;
|
|
fix(program);
|
|
if ( (typeof(commercials)==='undefined') || (commercials.length == 0)) {
|
|
commercials = [];
|
|
}
|
|
let newCommercials = [];
|
|
for (let k = 0; k < commercials.length; k++) {
|
|
let commercial = commercials[k];
|
|
if (
|
|
(typeof(commercial.server) === 'undefined')
|
|
|| (typeof(commercial.server.name) === 'undefined')
|
|
|| ( ( typeof(commercial.plexFile)==='undefined' ) && ( typeof(commercial.file)==='undefined' ) )
|
|
) {
|
|
console.log(`A commercial in channel ${channel.number} has invalid server/plex file information and has been removed.`);
|
|
} else {
|
|
if (typeof(newPlexServers[commercial.server.name]) !== 'undefined') {
|
|
commercial.server = newPlexServers[commercial.server.name];
|
|
}
|
|
fix(commercial);
|
|
|
|
newCommercials.push(commercial);
|
|
}
|
|
}
|
|
program.commercials = newCommercials;
|
|
newPrograms.push(program);
|
|
}
|
|
}
|
|
channel.programs = newPrograms;
|
|
db.channels.update( { number: channel.number }, channel );
|
|
}
|
|
|
|
}
|
|
|
|
function attemptMigratePlexFrom51(plex) {
|
|
console.log("Attempting to migrate existing Plex server: " + plex.name + "...");
|
|
let u = "unknown(migrated from 0.0.51)";
|
|
//most of the new variables aren't really necessary so it doesn't matter
|
|
//to replace them with placeholders
|
|
let uri = plex.protocol + "://" + plex.host + ":" + plex.port;
|
|
let newPlex = {
|
|
"name": plex.name,
|
|
"product": "Plex Media Server",
|
|
"productVersion": u,
|
|
"platform": u,
|
|
"platformVersion": u,
|
|
"device": u,
|
|
"clientIdentifier": u,
|
|
"createdAt": u,
|
|
"lastSeenAt": u,
|
|
"provides": "server",
|
|
"ownerId": null,
|
|
"sourceTitle": null,
|
|
"publicAddress": plex.host,
|
|
"accessToken": plex.token,
|
|
"owned": true,
|
|
"home": false,
|
|
"synced": false,
|
|
"relay": true,
|
|
"presence": true,
|
|
"httpsRequired": true,
|
|
"publicAddressMatches": true,
|
|
"dnsRebindingProtection": false,
|
|
"natLoopbackSupported": false,
|
|
"connections": [
|
|
{
|
|
"protocol": plex.protocol,
|
|
"address": plex.host,
|
|
"port": plex.port,
|
|
"uri": uri,
|
|
"local": true,
|
|
"relay": false,
|
|
"IPv6": false
|
|
}
|
|
],
|
|
"uri": uri,
|
|
"protocol": plex.protocol,
|
|
"address": plex.host,
|
|
"port": plex.host,
|
|
"arGuide": plex.arGuide,
|
|
"arChannels": plex.arChannels,
|
|
"_id": plex._id,
|
|
};
|
|
console.log("Sucessfully migrated plex server: " + plex.name);
|
|
return newPlex;
|
|
}
|
|
|
|
function commercialsRemover(db) {
|
|
let getKey = (program) => {
|
|
let key = program.key;
|
|
return (typeof(key) === 'undefined')? "?" : key;
|
|
}
|
|
|
|
let channels = db['channels'].find();
|
|
for (let i = 0; i < channels.length; i++) {
|
|
let channel = channels[i];
|
|
let fixedPrograms = [];
|
|
let fixedFiller = channel.fillerContent;
|
|
if ( typeof(fixedFiller) === 'undefined') {
|
|
fixedFiller = [];
|
|
}
|
|
let addedFlex = false;
|
|
let seenPrograms = {};
|
|
for (let i = 0; i < fixedFiller.length; i++) {
|
|
seenPrograms[getKey( fixedFiller[i] ) ] = true;
|
|
}
|
|
for (let j = 0; j < channel.programs.length; j++) {
|
|
let fixedProgram = channel.programs[j];
|
|
let commercials = fixedProgram.commercials;
|
|
if ( typeof(commercials) == 'undefined') {
|
|
commercials = [];
|
|
}
|
|
delete fixedProgram.commercials;
|
|
for (let k = 0; k < commercials.length; k++) {
|
|
if ( typeof(seenPrograms[ getKey( commercials[k] ) ]) === 'undefined') {
|
|
seenPrograms[ getKey( commercials[k] ) ] = true;
|
|
fixedFiller.push( commercials[k] );
|
|
}
|
|
}
|
|
let diff = fixedProgram.duration - fixedProgram.actualDuration;
|
|
fixedProgram.duration = fixedProgram.actualDuration;
|
|
fixedPrograms.push( fixedProgram );
|
|
if (diff > 0) {
|
|
addedFlex = true;
|
|
fixedPrograms.push( {
|
|
isOffline : true,
|
|
duration : diff,
|
|
actualDuration : diff,
|
|
} );
|
|
}
|
|
}
|
|
channel.programs = fixedPrograms;
|
|
channel.fillerContent = fixedFiller;
|
|
//TODO: maybe remove duplicates?
|
|
if (addedFlex) {
|
|
//fill up some flex settings just in case
|
|
if ( typeof(channel.fillerRepeatCooldown) === 'undefined' ) {
|
|
channel.fillerRepeatCooldown = 10*60*1000;
|
|
}
|
|
if ( typeof(channel.offlineMode) === 'undefined' ) {
|
|
console.log("Added provisional fallback to channel #" + channel.number + " " + channel.name + " . You might want to tweak this value in channel configuration.");
|
|
channel.offlineMode = "pic";
|
|
channel.fallback = [ ];
|
|
channel.offlinePicture = `http://localhost:${process.env.PORT}/images/generic-offline-screen.png`
|
|
channel.offlineSoundtrack = ''
|
|
}
|
|
if ( typeof(channel.disableFillerOverlay) === 'undefined' ) {
|
|
channel.disableFillerOverlay = true;
|
|
}
|
|
}
|
|
db.channels.update( { number: channel.number }, channel );
|
|
}
|
|
}
|
|
|
|
|
|
function initDB(db) {
|
|
let dbVersion = db['db-version'].find()[0];
|
|
if (typeof(dbVersion) === 'undefined') {
|
|
dbVersion = { 'version': 0 };
|
|
}
|
|
while (dbVersion.version != TARGET_VERSION) {
|
|
let ran = false;
|
|
for (let i = 0; i < STEPS.length; i++) {
|
|
if (STEPS[i][0] == dbVersion.version) {
|
|
ran = true;
|
|
console.log("Migrating from db version " + dbVersion.version + " to: " + STEPS[i][1] + "...");
|
|
try {
|
|
STEPS[i][2](db);
|
|
if (typeof(dbVersion._id) === 'undefined') {
|
|
db['db-version'].save( {'version': STEPS[i][1] } );
|
|
} else {
|
|
db['db-version'].update( {_id: dbVersion._id} , {'version': STEPS[i][1] } );
|
|
}
|
|
dbVersion = db['db-version'].find()[0];
|
|
console.log("Done migrating db to version : " + dbVersion.version);
|
|
|
|
} catch (e) {
|
|
console.log("Error during migration. Sorry, we can't continue. Wiping out your .pseudotv folder might be a workaround, but that means you lose all your settings.", e);
|
|
throw Error("Migration error, step=" + dbVersion.version);
|
|
}
|
|
} else {
|
|
console.log(STEPS[i][0], " != " , dbVersion.version );
|
|
}
|
|
}
|
|
if (!ran) {
|
|
throw Error("Unable to find migration step from version: " + dbVersion.version );
|
|
}
|
|
}
|
|
console.log(`DB Version correct: ${dbVersion.version}` );
|
|
}
|
|
|
|
|
|
function ffmpeg() {
|
|
return {
|
|
//How default ffmpeg settings should look
|
|
configVersion: 5,
|
|
ffmpegPath: "/usr/bin/ffmpeg",
|
|
threads: 4,
|
|
concatMuxDelay: "0",
|
|
logFfmpeg: false,
|
|
enableFFMPEGTranscoding: true,
|
|
audioVolumePercent: 100,
|
|
videoEncoder: "mpeg2video",
|
|
audioEncoder: "ac3",
|
|
targetResolution: "1920x1080",
|
|
videoBitrate: 10000,
|
|
videoBufSize: 2000,
|
|
audioBitrate: 192,
|
|
audioBufSize: 50,
|
|
audioSampleRate: 48,
|
|
audioChannels: 2,
|
|
errorScreen: "pic",
|
|
errorAudio: "silent",
|
|
normalizeVideoCodec: true,
|
|
normalizeAudioCodec: true,
|
|
normalizeResolution: true,
|
|
normalizeAudio: true,
|
|
}
|
|
}
|
|
|
|
//This initializes ffmpeg config for db version 0
|
|
//there used to be a concept of configVersion which worked like this database
|
|
//migration thing but only for settings. Nowadays that sort of migration should
|
|
//be done at a db-version level.
|
|
function repairFFmpeg0(existingConfigs) {
|
|
|
|
var hasBeenRepaired = false;
|
|
var currentConfig = {};
|
|
var _id = null;
|
|
if (existingConfigs.length === 0) {
|
|
currentConfig = {};
|
|
} else {
|
|
currentConfig = existingConfigs[0];
|
|
_id = currentConfig._id;
|
|
}
|
|
if (
|
|
(typeof(currentConfig.configVersion) === 'undefined')
|
|
|| (currentConfig.configVersion < 3)
|
|
) {
|
|
hasBeenRepaired = true;
|
|
currentConfig = ffmpeg();
|
|
currentConfig._id = _id;
|
|
}
|
|
if (currentConfig.configVersion == 3) {
|
|
//migrate from version 3 to 4
|
|
hasBeenRepaired = true;
|
|
//new settings:
|
|
currentConfig.audioBitrate = 192;
|
|
currentConfig.audioBufSize = 50;
|
|
currentConfig.audioChannels = 2;
|
|
currentConfig.audioSampleRate = 48;
|
|
//this one has been renamed:
|
|
currentConfig.normalizeAudio = currentConfig.alignAudio;
|
|
currentConfig.configVersion = 4;
|
|
}
|
|
if (currentConfig.configVersion == 4) {
|
|
//migrate from version 4 to 5
|
|
hasBeenRepaired = true;
|
|
//new settings:
|
|
currentConfig.enableFFMPEGTranscoding = true;
|
|
currentConfig.normalizeVideoCodec = true;
|
|
currentConfig.normalizeAudioCodec = true;
|
|
currentConfig.normalizeResolution = true;
|
|
currentConfig.normalizeAudio = true;
|
|
|
|
currentConfig.configVersion = 5;
|
|
}
|
|
return {
|
|
hasBeenRepaired: hasBeenRepaired,
|
|
fixedConfig : currentConfig,
|
|
};
|
|
}
|
|
|
|
|
|
module.exports = {
|
|
initDB: initDB,
|
|
defaultFFMPEG: ffmpeg,
|
|
} |