610 lines
22 KiB
JavaScript
610 lines
22 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 path = require('path');
|
|
var fs = require('fs');
|
|
|
|
const TARGET_VERSION = 501;
|
|
|
|
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) ],
|
|
[ 200, 300, (db) => appNameChange(db) ],
|
|
[ 300, 400, (db) => createDeviceId(db) ],
|
|
[ 400, 500, (db,channels) => splitServersSingleChannels(db, channels) ],
|
|
[ 500, 501, (db) => fixCorruptedServer(db) ],
|
|
]
|
|
|
|
const { v4: uuidv4 } = require('uuid');
|
|
|
|
function createDeviceId(db) {
|
|
let deviceId = db['client-id'].find();
|
|
if (deviceId.length == 0) {
|
|
let clientId = uuidv4().replace(/-/g,"").slice(0,16) + "-org-dizquetv-" + process.platform
|
|
let dev = {
|
|
clientId: clientId,
|
|
}
|
|
db['client-id'].save( dev );
|
|
}
|
|
}
|
|
|
|
|
|
function appNameChange(db) {
|
|
let xmltv = db['xmltv-settings'].find()
|
|
if (xmltv.length > 0) {
|
|
xmltv = xmltv[0];
|
|
if (typeof(xmltv.file) !== 'undefined') {
|
|
xmltv.file = xmltv.file.replace(/\.pseudotv/, ".dizquetv");
|
|
db['xmltv-settings'].update( { "_id" : xmltv._id} , xmltv );
|
|
}
|
|
}
|
|
}
|
|
|
|
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,aac',
|
|
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: 2,
|
|
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, channelDB ) {
|
|
if (typeof(channelDB) === 'undefined') {
|
|
throw Error("???");
|
|
}
|
|
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, channelDB);
|
|
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 .dizquetv folder might be a workaround, but that means you lose all your settings.", e);
|
|
throw Error("Migration error, step=" + 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,
|
|
};
|
|
}
|
|
|
|
function splitServersSingleChannels(db, channelDB ) {
|
|
console.log("Migrating channels and plex servers so that plex servers are no longer embedded in program data");
|
|
let servers = db['plex-servers'].find();
|
|
let serverCache = {};
|
|
let serverNames = {};
|
|
let newServers = [];
|
|
|
|
let getServerKey = (uri, accessToken) => {
|
|
return uri + "|" + accessToken;
|
|
}
|
|
|
|
let getNewName = (name) => {
|
|
if ( (typeof(name) === 'undefined') || (typeof(serverNames[name])!=='undefined') ) {
|
|
//recurse because what if some genius actually named their server plex#3 ?
|
|
name = getNewName("plex#" + (Object.keys(serverNames).length + 1));
|
|
}
|
|
serverNames[name] = true;
|
|
return name;
|
|
}
|
|
|
|
let saveServer = (name, uri, accessToken, arGuide, arChannels) => {
|
|
if (typeof(arGuide) === 'undefined') {
|
|
arGuide = true;
|
|
}
|
|
if (typeof(arChannels) === 'undefined') {
|
|
arChannels = false;
|
|
}
|
|
if (uri.endsWith("/")) {
|
|
uri = uri.slice(0,-1);
|
|
}
|
|
let key = getServerKey(uri, accessToken);
|
|
if (typeof(serverCache[key]) === 'undefined') {
|
|
serverCache[key] = getNewName(name);
|
|
console.log(`for key=${key} found server with name=${serverCache[key]}, uri=${uri}, accessToken=${accessToken}` );
|
|
newServers.push({
|
|
name: serverCache[key],
|
|
uri: uri,
|
|
accessToken: accessToken,
|
|
index: newServers.length,
|
|
arChannels : arChannels,
|
|
arGuide: arGuide,
|
|
});
|
|
}
|
|
return serverCache[key];
|
|
}
|
|
for (let i = 0; i < servers.length; i++) {
|
|
let server = servers[i];
|
|
saveServer( server.name, server.uri, server.accessToken, server.arGuide, server.arChannels);
|
|
}
|
|
|
|
let cleanupProgram = (program) => {
|
|
delete program.actualDuration;
|
|
delete program.commercials;
|
|
delete program.durationStr;
|
|
delete program.start;
|
|
delete program.stop;
|
|
delete program.streams;
|
|
delete program.opts;
|
|
}
|
|
|
|
let fixProgram = (program) => {
|
|
//Also remove the "actualDuration" and "commercials" fields.
|
|
try {
|
|
cleanupProgram(program);
|
|
if (program.isOffline) {
|
|
return program;
|
|
}
|
|
let newProgram = JSON.parse( JSON.stringify(program) );
|
|
let s = newProgram.server;
|
|
delete newProgram.server;
|
|
let name = saveServer( undefined, s.uri, s.accessToken, undefined, undefined);
|
|
if (typeof(name) === "undefined") {
|
|
throw Error("Unable to find server name");
|
|
}
|
|
//console.log(newProgram.title + " : " + name);
|
|
newProgram.serverKey = name;
|
|
return newProgram;
|
|
} catch (err) {
|
|
console.error("Unable to migrate program. Replacing it with flex");
|
|
return {
|
|
isOffline: true,
|
|
duration : program.duration,
|
|
};
|
|
}
|
|
}
|
|
|
|
let fixChannel = (channel) => {
|
|
console.log("Migrating channel: " + channel.name + " " + channel.number);
|
|
for (let i = 0; i < channel.programs.length; i++) {
|
|
channel.programs[i] = fixProgram( channel.programs[i] );
|
|
}
|
|
//if (channel.programs.length > 10) {
|
|
//channel.programs = channel.programs.slice(0, 10);
|
|
//}
|
|
channel.duration = 0;
|
|
for (let i = 0; i < channel.programs.length; i++) {
|
|
channel.duration += channel.programs[i].duration;
|
|
}
|
|
if ( typeof(channel.fallback) === 'undefined') {
|
|
channel.fallback = [];
|
|
}
|
|
for (let i = 0; i < channel.fallback.length; i++) {
|
|
channel.fallback[i] = fixProgram( channel.fallback[i] );
|
|
}
|
|
if ( typeof(channel.fillerContent) === 'undefined') {
|
|
channel.fillerContent = [];
|
|
}
|
|
for (let i = 0; i < channel.fillerContent.length; i++) {
|
|
channel.fillerContent[i] = fixProgram( channel.fillerContent[i] );
|
|
}
|
|
return channel;
|
|
}
|
|
|
|
let channels = db['channels'].find();
|
|
for (let i = 0; i < channels.length; i++) {
|
|
channels[i] = fixChannel(channels[i]);
|
|
}
|
|
|
|
console.log("Done migrating channels for this step. Saving updates to storage...");
|
|
|
|
//wipe out servers
|
|
for (let i = 0; i < servers.length; i++) {
|
|
db['plex-servers'].remove( { _id: servers[i]._id } );
|
|
}
|
|
//wipe out old channels
|
|
db['channels'].remove();
|
|
// insert all over again
|
|
if (newServers.length > 0) {
|
|
db['plex-servers'].save( newServers );
|
|
}
|
|
for (let i = 0; i < channels.length; i++) {
|
|
channelDB.saveChannelSync( channels[i].number, channels[i] );
|
|
}
|
|
console.log("Done migrating channels for this step...");
|
|
|
|
}
|
|
|
|
function fixCorruptedServer(db) {
|
|
let arr = db['plex-servers'].find();
|
|
let servers = [];
|
|
let badFound = false;
|
|
for (let i = 0; i < arr.length; i++) {
|
|
let server = arr[i];
|
|
if ( (typeof(server.name) === 'undefined') && (server.length == 0) ) {
|
|
badFound = true;
|
|
console.log("Found a corrupted plex server. And that's why 63 is a bad version. BAD");
|
|
} else {
|
|
servers.push(server);
|
|
}
|
|
}
|
|
if (badFound) {
|
|
console.log("Fixing the plex-server.json...");
|
|
let f = path.join(process.env.DATABASE, `plex-servers.json` );
|
|
fs.writeFileSync( f, JSON.stringify(servers) );
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
initDB: initDB,
|
|
defaultFFMPEG: ffmpeg,
|
|
} |