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