dizquetv/src/database-migration.js

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,
}