otu-tool
Version:
OBS Themes Utility is an handle tool for work with your scenes of Open Broadcaster Software
503 lines (419 loc) • 17.6 kB
JavaScript
#!/usr/bin/env node
"use strict";
const fs = require('fs');
const path = require('path');
const Archive = require('adm-zip');
const yargs = require('yargs/yargs');
const {
hideBin
} = require('yargs/helpers');
const openType = require('opentype.js');
const {
exec
} = require('child_process');
yargs(hideBin(process.argv))
.scriptName("otu-tool")
.usage('Usage: $0 <command> [options]')
.command('import [input] [output]', 'import theme obs', {
input: {
description: 'insert path of theme want import',
alias: 'i',
demandOption: true,
type: 'string',
requiresArg: true
},
output: {
description: 'insert path where save',
alias: 'o',
type: 'string',
requiresArg: true,
default: ''
}
},
(argv) => {
if (fs.existsSync(argv.input)) {
importTheme(argv.input, argv.output, argv)
} else {
console.log(argv.input + "file doesn't exist")
return false;
}
}
)
.command('export [input] [output]', 'export your themes with files', {
input: {
description: 'insert path of theme want export',
alias: 'i',
demandOption: true,
type: 'string',
requiresArg: true,
},
output: {
description: 'insert path where save',
alias: 'o',
type: 'string',
requiresArg: true
}
},
(argv) => {
if (fs.existsSync(argv.input)) {
exportTheme(argv.input, argv.output, argv);
} else {
console.log(argv.input + "file doesn't exist")
return false;
}
}
).command('extract [scene] [input] [output]', 'extract scene from theme', {
input: {
description: 'insert path of theme want extract a scene',
alias: 'i',
demandOption: true,
type: 'string',
requiresArg: true,
},
output: {
description: 'insert path where save',
alias: 'o',
type: 'string',
requiresArg: true
},
scene: {
description: 'name scene want extract',
alias: 's',
type: 'string',
requiresArg: true
}
},
(argv) => {
if (fs.existsSync(argv.input)) {
extractScene(argv.input, argv.output, argv.scene, argv);
} else {
console.log(argv.input + "file doesn't exist")
return false;
}
})
.command('add [scene] [input] [output]', 'add scene to theme', {
input: {
description: 'insert path of theme want to add scene',
alias: 'i',
demandOption: true,
type: 'string',
requiresArg: true,
},
output: {
description: 'insert path where save new theme',
alias: 'o',
type: 'string',
demandOption: true,
requiresArg: true
},
scene: {
description: 'insert path of scene want add',
alias: 's',
type: 'string',
requiresArg: true
}
},
(argv) => {
if (fs.existsSync(argv.input)) {
addScene(argv.input, argv.output, argv.scene, argv);
} else {
console.log(argv.input + "file doesn't exist")
return false;
}
})
.command('show [input] [Options]', 'show info about theme', {
scenes: {
description: 'show list of scene present',
type: 'boolean',
default: false
},
input: {
description: 'insert path of theme want show info',
alias: 'i',
demandOption: true,
type: 'string',
requiresArg: true,
}
},
(argv) => {
if (fs.existsSync(argv.input)) {
showInfo(argv.input, argv);
} else {
console.log(argv.input + "file doesn't exist")
return false;
}
})
.help()
.alias('help', 'h')
.alias('version', 'v')
.argv;
/**
* @description Show info about theme scene
* @param {string} fsource - source file
* @param {Object} options - Options
*/
function showInfo(fsource, options) {
fs.readFile(fsource, 'utf8', (err, data) => {
const theme = JSON.parse(data);
if (options.scenes) {
theme.scene_order.forEach((scene, index) => {
console.log(index + '\t- "' + scene.name + '"');
const itemsRequired = theme['sources'].filter(item => item.name === scene.name);
if (itemsRequired[0].settings.items !== undefined) {
itemsRequired[0].settings.items.forEach(res => {
console.log('\t\t -"' + res.name + '"')
});
}
})
}
})
}
/**
* @description addScene add scene to theme
* @param {string} fsource - source file
* @param {string} fdest - destination file
* @param {string} sceneName - name of scene archive want add
* @param {Object} options - Options
*/
function addScene(fsource, fdest, sceneArchive, options) {
fs.readFile(fsource, 'utf8', (err, data) => {
const zip = new Archive(sceneArchive);
const scene = JSON.parse(zip.getEntry('scene.json').getData().toString("utf8"));
const theme = JSON.parse(data);
const currentDir = path.dirname(fdest);
const fontDir = process.env.LOCALAPPDATA + '\\Microsoft\\Windows\\Fonts\\';
theme.scene_order.push({
"name": theme.name
});
if (scene.hasOwnProperty('groups')) {
if (theme.hasOwnProperty('groups')) {
theme.groups.push(...scene.groups);
} else {
theme.groups = scene.groups;
}
}
scene.sources.forEach(src => {
if (typeof(src.settings.files) !== 'undefined') {
src.settings.files.forEach((item, index) => {
if ((fs.statSync(item.value).isFile()) && (zip.getEntry('files/' + path.win32.basename((item.value))))) {
zip.extractEntryTo("files/" + path.win32.basename(item.value), currentDir, true, true);
} else zip.extractEntryTo("files/" + path.win32.basename(item.value) + "/", currentDir, true, true);
src.settings.files[index].value = path.resolve(currentDir, 'files', path.win32.basename(src.settings.files[index].value));
})
}
if ((typeof(src.settings.file) !== 'undefined') && (zip.getEntry('files/' + path.win32.basename(src.settings.file)))) {
src.settings.file = path.resolve(currentDir, 'files', path.win32.basename(src.settings.file));
zip.extractEntryTo('files/' + path.win32.basename(src.settings.file), currentDir, true, true);
}
if ((typeof(src.settings.custom_font) !== 'undefined') && (zip.getEntry('files/' + path.win32.basename(src.settings.custom_font)))) {
let fontPath = fontDir + path.win32.basename(src.settings.custom_font);
src.settings.custom_font = fontPath;
if (!fs.existsSync(fontPath)) {
zip.extractEntryTo('files/' + path.win32.basename(src.settings.custom_font), fontDir, false, false);
let font = openType.loadSync(fontPath);
let fontFamilyName = font["tables"]["name"]["fontFamily"]["en"] + ' (TrueType)';
let strReg = 'reg add "HKCU\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Fonts" /v ' + '"' + fontFamilyName + '"' + ' /t REG_SZ /d ' + '"' + fontPath + '"' + ' /f';
const result = exec(strReg, (error, stdout, stderr) => {
if (error) {
console.error(`error: ${error.message}`);
return;
}
if (stderr) {
console.error(`stderr: ${stderr}`);
return;
}
console.log(`stdout:\n${stdout}`);
})
} else console.log("File exist : " + fontPath);
}
if (typeof(src.settings.local_file) !== 'undefined') {
src.settings.local_file = path.resolve(currentDir, 'files', path.win32.basename(src.settings.local_file));
zip.extractEntryTo("files/" + path.win32.basename(src.settings.local_file), currentDir, true, true);
}
theme.sources.push(src);
});
fdest = fdest || theme.name;
fs.writeFile(fdest + '.json', JSON.stringify(theme), function(err) {
if (err) return console.log(err);
console.log('Completed!!! ==> ' + fdest + '.json');
return;
});
});
}
/**
* @description checkItemsRequired check all items required from scene
* @param {Object} theme - full source of original theme
* @param {Object} source - items required from scene
*/
function checkItemsRequired(theme, source) {
const itemsRequired = [];
source.forEach(name => {
itemsRequired.push(name);
let sceneCurr = theme["sources"].filter(scene => scene.name == name)[0];
if (sceneCurr !== undefined) {
if (sceneCurr.settings.items !== undefined) {
const items = sceneCurr.settings.items.map(item => item.name);
itemsRequired.push(checkItemsRequired(theme, items));
}
}
});
return itemsRequired.flat();
}
/**
* @description extractScene extract scene from theme
* @param {string} fsource - source file
* @param {string} fdest - destination file
* @param {string} sceneName - name of scene want extract
* @param {Object} options - Options
*/
function extractScene(fsource, fdest, sceneName, options) {
fs.readFile(fsource, 'utf8', (err, data) => {
const theme = JSON.parse(data);
const scene = {};
const itemsRequired = new Set();
const zip = new Archive();
scene.sources = theme['sources'].filter(item => item.name === sceneName);
if (scene.sources.length > 0) {
scene.name = sceneName;
if (scene.sources[0].settings.items !== undefined) {
const itemsList = Array.from(itemsRequired.add(checkItemsRequired(theme, scene.sources[0].settings.items.map(item => item.name)))).flat();
itemsList.forEach(itemName => {
let sceneCurr = theme['sources'].filter(itemCurr => itemCurr.name === itemName);
if (sceneCurr.length > 0) {
if (typeof(sceneCurr[0].settings.files) !== 'undefined') {
sceneCurr[0].settings.files.forEach(item => {
if (fs.existsSync(item.value)) {
if (fs.statSync(item.value).isFile()) {
zip.addLocalFile(item.value, 'files/', path.win32.basename(item.value));
} else {
zip.addFile('files/' + path.win32.basename(item.value) + '/', Buffer.alloc(0), "", fs.statSync(item.value));
zip.addLocalFolder(item.value, 'files/' + path.win32.basename(item.value) + '/');
}
}
})
};
if ((typeof(sceneCurr[0].settings.file) !== 'undefined') && fs.existsSync(sceneCurr[0].settings.file)) {
zip.addLocalFile(sceneCurr[0].settings.file, 'files/', path.win32.basename(sceneCurr[0].settings.file));
};
if ((typeof(sceneCurr[0].settings.custom_font) !== 'undefined') && fs.existsSync(sceneCurr[0].settings.custom_font)) {
zip.addLocalFile(sceneCurr[0].settings.custom_font, 'files/', path.win32.basename(sceneCurr[0].settings.custom_font));
};
if ((typeof(sceneCurr[0].settings.local_file) !== 'undefined') && fs.existsSync(sceneCurr[0].settings.local_file)) {
zip.addLocalFile(sceneCurr[0].settings.local_file, 'files/', path.win32.basename(sceneCurr[0].settings.local_file));
};
scene.sources.push(...sceneCurr);
} else {
if (theme.hasOwnProperty('groups')) {
if (scene.hasOwnProperty('groups')) {
scene.groups.push(theme['groups'].filter(item => (item.name === itemName))[0]);
} else {
scene.groups = (theme['groups'].filter(item => (item.name === itemName)));
}
}
}
});
}
fdest = fdest || scene.name;
zip.addFile("scene.json", Buffer.from(JSON.stringify(scene), "utf8"));
zip.writeZip(fdest + ".otu");
console.log('Completed!!! ' + fdest + '.otu');
} else console.log('"' + sceneName + '" is not present, please use showInfo command for see list of scene available, more info --help');
});
}
/**
* @description importTheme return compatible scene version from original and extract files
* @param {string} fsource - source file
* @param {string} fdest - destination file
* @param {Object} options - Options
*/
function importTheme(fsource, fdest, options) {
const zip = new Archive(fsource);
const scene = zip.getEntry('theme.json');
const theme = JSON.parse(scene.getData().toString("utf8"));
const currentDir = path.dirname(fdest);
const fontDir = process.env.LOCALAPPDATA + '\\Microsoft\\Windows\\Fonts\\';
theme["sources"].forEach(src => {
if (typeof(src.settings.files) !== 'undefined') {
src.settings.files.forEach((item, index) => {
if ((fs.statSync(item.value).isFile()) && (zip.getEntry('files/' + path.win32.basename((item.value))))) {
zip.extractEntryTo("files/" + path.win32.basename(item.value), currentDir, true, true);
} else zip.extractEntryTo("files/" + path.win32.basename(item.value) + "/", currentDir, true, true);
src.settings.files[index].value = path.resolve(currentDir, 'files', path.win32.basename(src.settings.files[index].value));
})
}
if ((typeof(src.settings.file) !== 'undefined') && (zip.getEntry('files/' + path.win32.basename(src.settings.file)))) {
src.settings.file = path.resolve(currentDir, 'files', path.win32.basename(src.settings.file));
zip.extractEntryTo('files/' + path.win32.basename(src.settings.file), currentDir, true, true);
}
if ((typeof(src.settings.custom_font) !== 'undefined') && (zip.getEntry('files/' + path.win32.basename(src.settings.custom_font)))) {
let fontPath = fontDir + path.win32.basename(src.settings.custom_font);
src.settings.custom_font = fontPath;
if (!fs.existsSync(fontPath)) {
zip.extractEntryTo('files/' + path.win32.basename(src.settings.custom_font), fontDir, false, false);
let font = openType.loadSync(fontPath);
let fontFamilyName = font["tables"]["name"]["fontFamily"]["en"] + ' (TrueType)';
let strReg = 'reg add "HKCU\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Fonts" /v ' + '"' + fontFamilyName + '"' + ' /t REG_SZ /d ' + '"' + fontPath + '"' + ' /f';
const result = exec(strReg, (error, stdout, stderr) => {
if (error) {
console.error(`error: ${error.message}`);
return;
}
if (stderr) {
console.error(`stderr: ${stderr}`);
return;
}
console.log(`stdout:\n${stdout}`);
})
} else console.log("File exist : " + fontPath);
}
if (typeof(src.settings.local_file) !== 'undefined') {
src.settings.local_file = path.resolve(currentDir, 'files', path.win32.basename(src.settings.local_file));
zip.extractEntryTo("files/" + path.win32.basename(src.settings.local_file), currentDir, true, true);
}
});
fdest = fdest || theme["name"];
fs.writeFile(fdest + '.json', JSON.stringify(theme), function(err) {
if (err) return console.log(err);
console.log('Completed!!! ' + fdest + '.json');
return;
});
}
/**
* @description exportTheme export in an archive obs scene with files
* @param {string} fsource - json source file of scene
* @param {string} fdest - destination save archive
* @param {Object} options - Options
*/
function exportTheme(fsource, fdest, options) {
const zip = new Archive();
fs.readFile(fsource, 'utf8', (err, data) => {
const theme = JSON.parse(data);
theme["sources"].forEach(src => {
if (typeof(src.settings.files) !== 'undefined') {
src.settings.files.forEach(item => {
if (fs.existsSync(item.value)) {
if (fs.statSync(item.value).isFile()) {
zip.addLocalFile(item.value, 'files/', path.win32.basename(item.value));
} else {
zip.addFile('files/' + path.win32.basename(item.value) + '/', Buffer.alloc(0), "", fs.statSync(item.value));
zip.addLocalFolder(item.value, 'files/' + path.win32.basename(item.value) + '/');
}
}
})
};
if ((typeof(src.settings.file) !== 'undefined') && fs.existsSync(src.settings.file)) {
zip.addLocalFile(src.settings.file, 'files/', path.win32.basename(src.settings.file));
};
if ((typeof(src.settings.custom_font) !== 'undefined') && fs.existsSync(src.settings.custom_font)) {
zip.addLocalFile(src.settings.custom_font, 'files/', path.win32.basename(src.settings.custom_font));
};
if ((typeof(src.settings.local_file) !== 'undefined') && fs.existsSync(src.settings.local_file)) {
zip.addLocalFile(src.settings.local_file, 'files/', path.win32.basename(src.settings.local_file));
};
});
fdest = fdest || theme["name"];
zip.addFile("theme.json", Buffer.from(JSON.stringify(theme), "utf8"));
zip.writeZip(fdest + ".otu");
console.log('Completed!!! ' + fdest + '.otu');
});
}