<commit_before>
/** This hook updates platform configuration files based on preferences and config-file data defined in config.xml.
    Currently only the AndroidManifest.xml and IOS *-Info.plist file are supported.
    Preferences:
    1.  Preferences defined outside of the platform element will apply to all platforms
    2.  Preferences defined inside a platform element will apply only to the specified platform
    3.  Platform preferences take precedence over common preferences
    4.  The preferenceMappingData object contains all of the possible custom preferences to date including the
        target file they belong to, parent element, and destination element or attribute
    Config Files
    1.  config-file elements MUST be defined inside a platform element, otherwise they will be ignored.
    2.  config-file target attributes specify the target file to update. (AndroidManifest.xml or *-Info.plist)
    3.  config-file parent attributes specify the parent element (AndroidManifest.xml) or parent key (*-Info.plist)
        that the child data will replace or be appended to.
    4.  config-file elements are uniquely indexed by target AND parent for each platform.
    5.  If there are multiple config-file's defined with the same target AND parent, the last config-file will be used
    6.  Elements defined WITHIN a config-file will replace or be appended to the same elements relative to the parent element
    7.  If a unique config-file contains multiples of the same elements (other than uses-permssion elements which are
        selected by by the uses-permission name attribute), the last defined element will be retrieved.
    Examples:
    AndroidManifest.xml
    NOTE: For possible manifest values see http://developer.android.com/guide/topics/manifest/manifest-intro.html
    <platform name="android">
        //These preferences are actually available in Cordova by default although not currently documented
        <preference name="android-minSdkVersion" value="8" />
        <preference name="android-maxSdkVersion" value="19" />
        <preference name="android-targetSdkVersion" value="19" />
        //custom preferences examples
        <preference name="android-windowSoftInputMode" value="stateVisible" />
        <preference name="android-installLocation" value="auto" />
        <preference name="android-launchMode" value="singleTop" />
        <preference name="android-activity-hardwareAccelerated" value="false" />
        <preference name="android-manifest-hardwareAccelerated" value="false" />
        <preference name="android-configChanges" value="orientation" />
        <preference name="android-theme" value="@android:style/Theme.Black.NoTitleBar" />
        <config-file target="AndroidManifest.xml" parent="/*>
            <supports-screens
                android:xlargeScreens="false"
                android:largeScreens="false"
                android:smallScreens="false" />
            <uses-permission android:name="android.permission.READ_CONTACTS" android:maxSdkVersion="15" />
            <uses-permission android:name="android.permission.WRITE_CONTACTS" />
        </config-file>
    </platform>
    *-Info.plist
    <platform name="ios">
        <config-file platform="ios" target="*-Info.plist" parent="UISupportedInterfaceOrientations">
            <array>
                <string>UIInterfaceOrientationLandscapeOmg</string>
            </array>
        </config-file>
        <config-file platform="ios" target="*-Info.plist" parent="SomeOtherPlistKey">
            <string>someValue</string>
        </config-file>
    </platform>
    NOTE: Currently, items aren't removed from the platform config files if you remove them from config.xml.
          For example, if you add a custom permission, build the remove it, it will still be in the manifest.
          If you make a mistake, for example adding an element to the wrong parent, you may need to remove and add your platform,
          or revert to your previous manifest/plist file.
    TODO: We may need to capture all default manifest/plist elements/keys created by Cordova along with any plugin elements/keys to compare against custom elements to remove.
 */

// global vars

const fs = require("fs"),
      path = require("path"),
      et = require("elementtree"),
      plist = require('plist');

module.exports = function(context) {

    const cordovaUtil = context.requireCordovaModule('cordova-lib/src/cordova/util');
    const rootdir = cordovaUtil.isCordova();

    const platformConfig = (function(){
        /*  Global object that defines the available custom preferences for each platform.
         Maps a config.xml preference to a specific target file, parent element, and destination attribute or element
         */
        const preferenceMappingData = {
            'android': {
                'android-manifest-hardwareAccelerated': {target: 'AndroidManifest.xml', parent: './', destination: 'android:hardwareAccelerated'},
                'android-installLocation': {target: 'AndroidManifest.xml', parent: './', destination: 'android:installLocation'},
                'android-activity-hardwareAccelerated': {target: 'AndroidManifest.xml', parent: 'application', destination: 'android:hardwareAccelerated'},
                'android-configChanges': {target: 'AndroidManifest.xml', parent: "__cordovaMainActivity__", destination: 'android:configChanges'},
                'android-launchMode': {target: 'AndroidManifest.xml', parent: "__cordovaMainActivity__", destination: 'android:launchMode'},
                'android-theme': {target: 'AndroidManifest.xml', parent: "__cordovaMainActivity__", destination: 'android:theme'},
                'android-windowSoftInputMode': {target: 'AndroidManifest.xml', parent: "__cordovaMainActivity__", destination: 'android:windowSoftInputMode'},
                'android-applicationName': {target: 'AndroidManifest.xml', parent: 'application', destination: 'android:name'}
            },
            'ios': {}
        };
        let configXmlData, preferencesData;

        return {
            // Parses a given file into an elementtree object
            parseElementtreeSync: function (filename) {
                var contents = fs.readFileSync(filename, 'utf-8');
                if(contents) {
                    //Windows is the BOM. Skip the Byte Order Mark.
                    contents = contents.substring(contents.indexOf('<'));
                }
                return new et.ElementTree(et.XML(contents));
            },

            // Converts an elementtree object to an xml string.  Since this is used for plist values, we don't care about attributes
            eltreeToXmlString: function (data) {
                const tag = data.tag;
                let el = '<' + tag + '>';

                if(data.text && data.text.trim()) {
                    el += data.text.trim();
                } else {
                    data.getchildren().forEach(function (child) {
                        el += platformConfig.eltreeToXmlString(child);
                    });
                }

                el += '</' + tag + '>';
                return el;
            },

            // Parses the config.xml into an elementtree object and stores in the config object
            getConfigXml: function () {
                if(!configXmlData) {
                    configXmlData = this.parseElementtreeSync(path.join(rootdir, 'config.xml'));
                }

                return configXmlData;
            },

            /* Retrieves all <preferences ..> from config.xml and returns a map of preferences with platform as the key.
               If a platform is supplied, common prefs + platform prefs will be returned, otherwise just common prefs are returned.
             */
            getPreferences: function (platform) {
                const configXml = this.getConfigXml();

                //init common config.xml prefs if we haven't already
                if(!preferencesData) {
                    preferencesData = {
                        common: configXml.findall('preference')
                    };
                }

                let prefs = preferencesData.common || [];
                if(platform) {
                    if(!preferencesData[platform]) {
                        preferencesData[platform] = configXml.findall('platform[@name=\'' + platform + '\']/preference');
                    }
                    prefs = prefs.concat(preferencesData[platform]);
                }

                return prefs;
            },

            /* Retrieves all configured xml for a specific platform/target/parent element nested inside a platforms config-file
               element within the config.xml.  The config-file elements are then indexed by target|parent so if there are
               any config-file elements per platform that have the same target and parent, the last config-file element is used.
             */
            getConfigFilesByTargetAndParent: function (platform) {
                const configFileData = this.getConfigXml().findall('platform[@name=\'' + platform + '\']/config-file');

                let result = {};

                configFileData.forEach(function(item) {

                    let parent = item.attrib.parent;
                    //if parent attribute is undefined /* or */, set parent to top level elementree selector
                    if(!parent || parent === '/*' || parent === '*/') {
                        parent = './';
                    }
                    const key = item.attrib.target + '|' + parent;

                    result[key] = item;
                });

                return result;
            },

            // Parses the config.xml's preferences and config-file elements for a given platform
            parseConfigXml: function (platform) {
                const configData = {};
                this.parsePreferences(configData, platform);
                this.parseConfigFiles(configData, platform);

                return configData;
            },

            // Retrieves the config.xml's pereferences for a given platform and parses them into JSON data
            parsePreferences: function (configData, platform) {
                const preferences = this.getPreferences(platform),
                    type = 'preference';

                preferences.forEach( function (preference) {
                    // check if there are specific configuration to map to config for this platform
                    if (!preferenceMappingData[platform]) {
                        return;
                    }
                    const prefMappingData = preferenceMappingData[platform][preference.attrib.name];
                    let target,
                        prefData;

                    if (prefMappingData) {
                        prefData = {
                            parent: prefMappingData.parent,
                            type: type,
                            destination: prefMappingData.destination,
                            data: preference
                        };

                        target = prefMappingData.target;
                        if(!configData[target]) {
                            configData[target] = [];
                        }
                        configData[target].push(prefData);
                    }
                });
            },

            // Retrieves the config.xml's config-file elements for a given platform and parses them into JSON data
            parseConfigFiles: function (configData, platform) {
                const configFiles = this.getConfigFilesByTargetAndParent(platform),
                    type = 'configFile';

                for (let key in configFiles) {
                    if (configFiles.hasOwnProperty(key)) {
                        const configFile = configFiles[key];

                        const keyParts = key.split('|');
                        const target = keyParts[0];
                        const parent = keyParts[1];
                        const items = configData[target] || [];

                        configFile.getchildren().forEach( function (element) {
                            items.push({
                                parent: parent,
                                type: type,
                                destination: element.tag,
                                data: element
                            });
                        });

                        configData[target] = items;
                    }
                }
            },

            // Parses config.xml data, and update each target file for a specified platform
            updatePlatformConfig: function (platform) {
                const configData = this.parseConfigXml(platform),
                    platformPath = path.join(rootdir, 'platforms', platform);

                for (let targetFileName in configData) {
                    if (configData.hasOwnProperty(targetFileName)) {
                        const configItems = configData[targetFileName];

                        let projectName, targetFile;

                        if (platform === 'ios' && targetFileName.indexOf("Info.plist") > -1) {
                            projectName = platformConfig.getConfigXml().findtext('name');
                            targetFile = path.join(platformPath, projectName, projectName + '-Info.plist');
                            platformConfig.updateIosPlist(targetFile, configItems);
                        } else if (platform === 'android' && targetFileName === 'AndroidManifest.xml') {
                            targetFile = path.join(platformPath, targetFileName);
                            platformConfig.updateAndroidManifest(targetFile, configItems);
                        }
                    }
                }
            },

            getMainAndroidActivityNode: function(rootManifest) {
                const cordovaApp = "application/activity/intent-filter/action[@android:name='android.intent.action.MAIN']/../..";
                const tempNode = rootManifest.find(cordovaApp);
                return tempNode;
            },

            // Updates the AndroidManifest.xml target file with data from config.xml
            updateAndroidManifest: function (targetFile, configItems) {
                const tempManifest = platformConfig.parseElementtreeSync(targetFile),
                    root = tempManifest.getroot();
                const mainActivity = platformConfig.getMainAndroidActivityNode(root);

                configItems.forEach( function (item) {

                    let parentEl;
                    if (item.parent === "__cordovaMainActivity__") {
                        parentEl = mainActivity;
                    } else {
                        // if parent is not found on the root, child/grandchild nodes are searched
                        parentEl = root.find(item.parent) || root.find('*/' + item.parent);
                    }

                    const data = item.data;
                    let childSelector = item.destination,
                        childEl;

                    if(!parentEl) {
                        return;
                    }

                    if(item.type === 'preference') {
                        parentEl.attrib[childSelector] = data.attrib['value'];
                    } else {
                        // since there can be multiple uses-permission elements, we need to select them by unique name
                        if(childSelector === 'uses-permission') {
                            childSelector += '[@android:name=\'' + data.attrib['android:name'] + '\']';
                        }

                        childEl = parentEl.find(childSelector);
                        // if child element doesnt exist, create new element
                        if(!childEl) {
                            childEl = new et.Element(item.destination);
                            parentEl.append(childEl);
                        }

                        if (typeof data === "object") {
                            // copy all config.xml data except for the generated _id property
                            for (let key in data) {
                                // skip loop if the property is from prototype
                                if (!data.hasOwnProperty(key)) continue;

                                if(key !== '_id') {
                                    childEl[key] = data[key];
                                }
                            }
                        }
                    }
                });

                fs.writeFileSync(targetFile, tempManifest.write({indent: 4}), 'utf-8');
                console.log("Wrote AndroidManifest.xml: "+targetFile);
            },

            /* Updates the *-Info.plist file with data from config.xml by parsing to an xml string, then using the plist
               module to convert the data to a map.  The config.xml data is then replaced or appended to the original plist file
             */
            updateIosPlist: function (targetFile, configItems) {
                const infoPlist = plist.parse(fs.readFileSync(targetFile, 'utf-8'));
                let tempInfoPlist;

                configItems.forEach( function (item) {
                    const key = item.parent;
                    const plistXml = '<plist><dict><key>' + key + '</key>' +
                          platformConfig.eltreeToXmlString(item.data) + '</dict></plist>';

                    const configPlistObj = plist.parse(plistXml);
                    infoPlist[key] = configPlistObj[key];
                });

                tempInfoPlist = plist.build(infoPlist);
                tempInfoPlist = tempInfoPlist.replace(/<string>[\s\r\n]*<\/string>/g,'<string></string>');
                fs.writeFileSync(targetFile, tempInfoPlist, 'utf-8');
                console.log("Wrote iOS Plist: "+ targetFile);
            }
        };
    })();

    // Main
    (function () {
        if (rootdir) {
            // go through each of the platform directories that have been prepared
            const platforms = [];

            fs.readdirSync('platforms').forEach( function (file) {
                if (fs.statSync(path.resolve('platforms', file)).isDirectory()) {
                    platforms.push(file);
                }
            });

            platforms.forEach( function (platform) {
                try {
                    platform = platform.trim().toLowerCase();
                    console.log("Processing settings for platform: "+ platform);
                    platformConfig.updatePlatformConfig(platform);
                } catch (e) {
                    console.error(e);
                }
            });
        }
    })();
}<commit_msg>Fixes #14<commit_after>
/** This hook updates platform configuration files based on preferences and config-file data defined in config.xml.
    Currently only the AndroidManifest.xml and IOS *-Info.plist file are supported.
    Preferences:
    1.  Preferences defined outside of the platform element will apply to all platforms
    2.  Preferences defined inside a platform element will apply only to the specified platform
    3.  Platform preferences take precedence over common preferences
    4.  The preferenceMappingData object contains all of the possible custom preferences to date including the
        target file they belong to, parent element, and destination element or attribute
    Config Files
    1.  config-file elements MUST be defined inside a platform element, otherwise they will be ignored.
    2.  config-file target attributes specify the target file to update. (AndroidManifest.xml or *-Info.plist)
    3.  config-file parent attributes specify the parent element (AndroidManifest.xml) or parent key (*-Info.plist)
        that the child data will replace or be appended to.
    4.  config-file elements are uniquely indexed by target AND parent for each platform.
    5.  If there are multiple config-file's defined with the same target AND parent, the last config-file will be used
    6.  Elements defined WITHIN a config-file will replace or be appended to the same elements relative to the parent element
    7.  If a unique config-file contains multiples of the same elements (other than uses-permssion elements which are
        selected by by the uses-permission name attribute), the last defined element will be retrieved.
    Examples:
    AndroidManifest.xml
    NOTE: For possible manifest values see http://developer.android.com/guide/topics/manifest/manifest-intro.html
    <platform name="android">
        //These preferences are actually available in Cordova by default although not currently documented
        <preference name="android-minSdkVersion" value="8" />
        <preference name="android-maxSdkVersion" value="19" />
        <preference name="android-targetSdkVersion" value="19" />
        //custom preferences examples
        <preference name="android-windowSoftInputMode" value="stateVisible" />
        <preference name="android-installLocation" value="auto" />
        <preference name="android-launchMode" value="singleTop" />
        <preference name="android-activity-hardwareAccelerated" value="false" />
        <preference name="android-manifest-hardwareAccelerated" value="false" />
        <preference name="android-configChanges" value="orientation" />
        <preference name="android-theme" value="@android:style/Theme.Black.NoTitleBar" />
        <config-file target="AndroidManifest.xml" parent="/*>
            <supports-screens
                android:xlargeScreens="false"
                android:largeScreens="false"
                android:smallScreens="false" />
            <uses-permission android:name="android.permission.READ_CONTACTS" android:maxSdkVersion="15" />
            <uses-permission android:name="android.permission.WRITE_CONTACTS" />
        </config-file>
    </platform>
    *-Info.plist
    <platform name="ios">
        <config-file platform="ios" target="*-Info.plist" parent="UISupportedInterfaceOrientations">
            <array>
                <string>UIInterfaceOrientationLandscapeOmg</string>
            </array>
        </config-file>
        <config-file platform="ios" target="*-Info.plist" parent="SomeOtherPlistKey">
            <string>someValue</string>
        </config-file>
    </platform>
    NOTE: Currently, items aren't removed from the platform config files if you remove them from config.xml.
          For example, if you add a custom permission, build the remove it, it will still be in the manifest.
          If you make a mistake, for example adding an element to the wrong parent, you may need to remove and add your platform,
          or revert to your previous manifest/plist file.
    TODO: We may need to capture all default manifest/plist elements/keys created by Cordova along with any plugin elements/keys to compare against custom elements to remove.
 */

// global vars

const fs = require("fs"),
      path = require("path"),
      et = require("elementtree"),
      plist = require('plist');

// thanks to dpa99c/cordova-custom-config !
const manifestPath = {
    cordovaAndroid6: 'platforms/android/AndroidManifest.xml',
    cordovaAndroid7: 'platforms/android/app/src/main/AndroidManifest.xml'
};
function getAndroidManifestFilePath(rootdir) {
    var cordovaAndroid6Path = path.join(rootdir, manifestPath.cordovaAndroid6);
    var cordovaAndroid7Path = path.join(rootdir, manifestPath.cordovaAndroid7);

    if(fs.existsSync(cordovaAndroid7Path)){
        return cordovaAndroid7Path;
    }else if(fs.existsSync(cordovaAndroid6Path)){
        return cordovaAndroid6Path;
    }else{
        throw "Can't find AndroidManifest.xml in platforms/Android";
    }
}

module.exports = function(context) {

    const cordovaUtil = context.requireCordovaModule('cordova-lib/src/cordova/util');
    const rootdir = cordovaUtil.isCordova();

    const platformConfig = (function(){
        /*  Global object that defines the available custom preferences for each platform.
         Maps a config.xml preference to a specific target file, parent element, and destination attribute or element
         */
        const preferenceMappingData = {
            'android': {
                'android-manifest-hardwareAccelerated': {target: 'AndroidManifest.xml', parent: './', destination: 'android:hardwareAccelerated'},
                'android-installLocation': {target: 'AndroidManifest.xml', parent: './', destination: 'android:installLocation'},
                'android-activity-hardwareAccelerated': {target: 'AndroidManifest.xml', parent: 'application', destination: 'android:hardwareAccelerated'},
                'android-configChanges': {target: 'AndroidManifest.xml', parent: "__cordovaMainActivity__", destination: 'android:configChanges'},
                'android-launchMode': {target: 'AndroidManifest.xml', parent: "__cordovaMainActivity__", destination: 'android:launchMode'},
                'android-theme': {target: 'AndroidManifest.xml', parent: "__cordovaMainActivity__", destination: 'android:theme'},
                'android-windowSoftInputMode': {target: 'AndroidManifest.xml', parent: "__cordovaMainActivity__", destination: 'android:windowSoftInputMode'},
                'android-applicationName': {target: 'AndroidManifest.xml', parent: 'application', destination: 'android:name'}
            },
            'ios': {}
        };
        let configXmlData, preferencesData;

        return {
            // Parses a given file into an elementtree object
            parseElementtreeSync: function (filename) {
                var contents = fs.readFileSync(filename, 'utf-8');
                if(contents) {
                    //Windows is the BOM. Skip the Byte Order Mark.
                    contents = contents.substring(contents.indexOf('<'));
                }
                return new et.ElementTree(et.XML(contents));
            },

            // Converts an elementtree object to an xml string.  Since this is used for plist values, we don't care about attributes
            eltreeToXmlString: function (data) {
                const tag = data.tag;
                let el = '<' + tag + '>';

                if(data.text && data.text.trim()) {
                    el += data.text.trim();
                } else {
                    data.getchildren().forEach(function (child) {
                        el += platformConfig.eltreeToXmlString(child);
                    });
                }

                el += '</' + tag + '>';
                return el;
            },

            // Parses the config.xml into an elementtree object and stores in the config object
            getConfigXml: function () {
                if(!configXmlData) {
                    configXmlData = this.parseElementtreeSync(path.join(rootdir, 'config.xml'));
                }

                return configXmlData;
            },

            /* Retrieves all <preferences ..> from config.xml and returns a map of preferences with platform as the key.
               If a platform is supplied, common prefs + platform prefs will be returned, otherwise just common prefs are returned.
             */
            getPreferences: function (platform) {
                const configXml = this.getConfigXml();

                //init common config.xml prefs if we haven't already
                if(!preferencesData) {
                    preferencesData = {
                        common: configXml.findall('preference')
                    };
                }

                let prefs = preferencesData.common || [];
                if(platform) {
                    if(!preferencesData[platform]) {
                        preferencesData[platform] = configXml.findall('platform[@name=\'' + platform + '\']/preference');
                    }
                    prefs = prefs.concat(preferencesData[platform]);
                }

                return prefs;
            },

            /* Retrieves all configured xml for a specific platform/target/parent element nested inside a platforms config-file
               element within the config.xml.  The config-file elements are then indexed by target|parent so if there are
               any config-file elements per platform that have the same target and parent, the last config-file element is used.
             */
            getConfigFilesByTargetAndParent: function (platform) {
                const configFileData = this.getConfigXml().findall('platform[@name=\'' + platform + '\']/config-file');

                let result = {};

                configFileData.forEach(function(item) {

                    let parent = item.attrib.parent;
                    //if parent attribute is undefined /* or */, set parent to top level elementree selector
                    if(!parent || parent === '/*' || parent === '*/') {
                        parent = './';
                    }
                    const key = item.attrib.target + '|' + parent;

                    result[key] = item;
                });

                return result;
            },

            // Parses the config.xml's preferences and config-file elements for a given platform
            parseConfigXml: function (platform) {
                const configData = {};
                this.parsePreferences(configData, platform);
                this.parseConfigFiles(configData, platform);

                return configData;
            },

            // Retrieves the config.xml's pereferences for a given platform and parses them into JSON data
            parsePreferences: function (configData, platform) {
                const preferences = this.getPreferences(platform),
                    type = 'preference';

                preferences.forEach( function (preference) {
                    // check if there are specific configuration to map to config for this platform
                    if (!preferenceMappingData[platform]) {
                        return;
                    }
                    const prefMappingData = preferenceMappingData[platform][preference.attrib.name];
                    let target,
                        prefData;

                    if (prefMappingData) {
                        prefData = {
                            parent: prefMappingData.parent,
                            type: type,
                            destination: prefMappingData.destination,
                            data: preference
                        };

                        target = prefMappingData.target;
                        if(!configData[target]) {
                            configData[target] = [];
                        }
                        configData[target].push(prefData);
                    }
                });
            },

            // Retrieves the config.xml's config-file elements for a given platform and parses them into JSON data
            parseConfigFiles: function (configData, platform) {
                const configFiles = this.getConfigFilesByTargetAndParent(platform),
                    type = 'configFile';

                for (let key in configFiles) {
                    if (configFiles.hasOwnProperty(key)) {
                        const configFile = configFiles[key];

                        const keyParts = key.split('|');
                        const target = keyParts[0];
                        const parent = keyParts[1];
                        const items = configData[target] || [];

                        configFile.getchildren().forEach( function (element) {
                            items.push({
                                parent: parent,
                                type: type,
                                destination: element.tag,
                                data: element
                            });
                        });

                        configData[target] = items;
                    }
                }
            },

            // Parses config.xml data, and update each target file for a specified platform
            updatePlatformConfig: function (platform) {
                const configData = this.parseConfigXml(platform),
                    platformPath = path.join(rootdir, 'platforms', platform);

                for (let targetFileName in configData) {
                    if (configData.hasOwnProperty(targetFileName)) {
                        const configItems = configData[targetFileName];

                        let projectName, targetFile;

                        if (platform === 'ios' && targetFileName.indexOf("Info.plist") > -1) {
                            projectName = platformConfig.getConfigXml().findtext('name');
                            targetFile = path.join(platformPath, projectName, projectName + '-Info.plist');
                            platformConfig.updateIosPlist(targetFile, configItems);
                        } else if (platform === 'android' && targetFileName === 'AndroidManifest.xml') {
                            targetFile = getAndroidManifestFilePath(rootdir);
                            platformConfig.updateAndroidManifest(targetFile, configItems);
                        }
                    }
                }
            },

            getMainAndroidActivityNode: function(rootManifest) {
                const cordovaApp = "application/activity/intent-filter/action[@android:name='android.intent.action.MAIN']/../..";
                const tempNode = rootManifest.find(cordovaApp);
                return tempNode;
            },

            // Updates the AndroidManifest.xml target file with data from config.xml
            updateAndroidManifest: function (targetFile, configItems) {
                const tempManifest = platformConfig.parseElementtreeSync(targetFile),
                    root = tempManifest.getroot();
                const mainActivity = platformConfig.getMainAndroidActivityNode(root);

                configItems.forEach( function (item) {

                    let parentEl;
                    if (item.parent === "__cordovaMainActivity__") {
                        parentEl = mainActivity;
                    } else {
                        // if parent is not found on the root, child/grandchild nodes are searched
                        parentEl = root.find(item.parent) || root.find('*/' + item.parent);
                    }

                    const data = item.data;
                    let childSelector = item.destination,
                        childEl;

                    if(!parentEl) {
                        return;
                    }

                    if(item.type === 'preference') {
                        parentEl.attrib[childSelector] = data.attrib['value'];
                    } else {
                        // since there can be multiple uses-permission elements, we need to select them by unique name
                        if(childSelector === 'uses-permission') {
                            childSelector += '[@android:name=\'' + data.attrib['android:name'] + '\']';
                        }

                        childEl = parentEl.find(childSelector);
                        // if child element doesnt exist, create new element
                        if(!childEl) {
                            childEl = new et.Element(item.destination);
                            parentEl.append(childEl);
                        }

                        if (typeof data === "object") {
                            // copy all config.xml data except for the generated _id property
                            for (let key in data) {
                                // skip loop if the property is from prototype
                                if (!data.hasOwnProperty(key)) continue;

                                if(key !== '_id') {
                                    childEl[key] = data[key];
                                }
                            }
                        }
                    }
                });

                fs.writeFileSync(targetFile, tempManifest.write({indent: 4}), 'utf-8');
                console.log("Wrote AndroidManifest.xml: "+targetFile);
            },

            /* Updates the *-Info.plist file with data from config.xml by parsing to an xml string, then using the plist
               module to convert the data to a map.  The config.xml data is then replaced or appended to the original plist file
             */
            updateIosPlist: function (targetFile, configItems) {
                const infoPlist = plist.parse(fs.readFileSync(targetFile, 'utf-8'));
                let tempInfoPlist;

                configItems.forEach( function (item) {
                    const key = item.parent;
                    const plistXml = '<plist><dict><key>' + key + '</key>' +
                          platformConfig.eltreeToXmlString(item.data) + '</dict></plist>';

                    const configPlistObj = plist.parse(plistXml);
                    infoPlist[key] = configPlistObj[key];
                });

                tempInfoPlist = plist.build(infoPlist);
                tempInfoPlist = tempInfoPlist.replace(/<string>[\s\r\n]*<\/string>/g,'<string></string>');
                fs.writeFileSync(targetFile, tempInfoPlist, 'utf-8');
                console.log("Wrote iOS Plist: "+ targetFile);
            }
        };
    })();

    // Main
    (function () {
        if (rootdir) {
            // go through each of the platform directories that have been prepared
            const platforms = [];

            fs.readdirSync('platforms').forEach( function (file) {
                if (fs.statSync(path.resolve('platforms', file)).isDirectory()) {
                    platforms.push(file);
                }
            });

            platforms.forEach( function (platform) {
                try {
                    platform = platform.trim().toLowerCase();
                    console.log("Processing settings for platform: "+ platform);
                    platformConfig.updatePlatformConfig(platform);
                } catch (e) {
                    console.error(e);
                }
            });
        }
    })();
}<|endoftext|>


<commit_before>/* Generated by Opal 0.8.0 */
Opal.modules["chart-block-macro/extension"] = function (Opal) {
    Opal.dynamic_require_severity = "ignore";
    var __self = Opal.top, $scope = Opal, nil = Opal.nil, $breaker = Opal.breaker, $slice = Opal.slice, $klass = Opal.klass;

    if ($scope.get('RUBY_ENGINE')['$==']("opal")) {
    }
    ;
    __self.$include(Opal.get('Asciidoctor'));
    (function ($base, $super) {
        function $ChartBlockMacro() {
        };
        var __self = $ChartBlockMacro = $klass($base, $super, 'ChartBlockMacro', $ChartBlockMacro);

        var def = __self.$$proto, $scope = __self.$$scope;

        __self.$use_dsl();
        __self.$named("chart");

        return (def.$process = function (parent, target, attrs) {

                var $a, __self = this;

                var csvFile = (attrs['$[]']("data-uri")),
                    chartType = target,
                    title = (attrs['$[]']("title")),
                    alt = (attrs['$[]']("alt")),
                    caption = (attrs['$[]']("caption")),
                    width = (attrs['$[]']("width")),
                    height = (attrs['$[]']("height")),
                    scale = (attrs['$[]']("scale")),
                    align = (attrs['$[]']("align")),
                    type = (attrs['$[]']("type")),
                    role = (attrs['$[]']("role")),
                    link = (attrs['$[]']("link")),
                    float = (attrs['$[]']("float")),
                    filename = (attrs['$[]']("file")),
                    cache = (attrs['$[]']("cache")),
                    opt = (attrs['$[]']("opt")),
                    imagesdir = parent.$document().$attr('imagesdir', '');

                if (filename == "") {
                    return nil;
                }

                if (cache != "enabled") {
                    var parameters = [csvFile, imagesdir, parent.$image_uri(filename), chartType, opt].map(function (e) {
                        return e + "";
                    });

                    postMessage(JSON.stringify({
                        type: "afx",
                        func: "chartBuildFromCsv",
                        parameters: parameters
                    }));
                }

                var attributes = {
                    "target": filename,
                    "title": title,
                    "alt": alt,
                    "caption": caption,
                    "width": width,
                    "height": height,
                    "scale": scale,
                    "align": align,
                    "role": role,
                    "link": link,
                    "float": float,
                    "opt": opt
                };

                var keys = Object.keys(attributes);

                keys.forEach(function (key) {
                    if (attributes[key]["$nil?"]()) {
                        delete attributes[key];
                    }
                });

                return __self.$create_image_block(parent, Opal.hash(attributes))
                    ;
            }, nil) && 'process';
    })(__self, (($scope.get('Extensions')).$$scope.get('BlockMacroProcessor')));
    return (function ($base, $super) {
        function $ChartBlockProcessor() {
        };
        var __self = $ChartBlockProcessor = $klass($base, $super, 'ChartBlockProcessor', $ChartBlockProcessor);

        var def = __self.$$proto, $scope = __self.$$scope;

        __self.$use_dsl();
        __self.$named("chart");
        __self.$on_context(["open", "literal", "listing"]);
        __self.$parse_content_as("literal");

        return (def.$process = function (parent, reader, attrs) {

                var $a, __self = this;

                var chartType = (attrs['$[]'](2)),
                    title = (attrs['$[]']("title")),
                    alt = (attrs['$[]']("alt")),
                    caption = (attrs['$[]']("caption")),
                    width = (attrs['$[]']("width")),
                    height = (attrs['$[]']("height")),
                    scale = (attrs['$[]']("scale")),
                    align = (attrs['$[]']("align")),
                    type = (attrs['$[]']("type")),
                    role = (attrs['$[]']("role")),
                    link = (attrs['$[]']("link")),
                    float = (attrs['$[]']("float")),
                    filename = (attrs['$[]']("file")),
                    cache = (attrs['$[]']("cache")),
                    opt = (attrs['$[]']("opt")),
                    imagesdir = parent.$document().$attr('imagesdir', '');

                if (filename == "") {
                    return nil;
                }

                if (cache != "enabled") {

                    var parameters = [reader.$read(), imagesdir, parent.$image_uri(filename), chartType, opt].map(function (e) {
                        return e + "";
                    });

                    postMessage(JSON.stringify({
                        type: "afx",
                        func: "chartBuild",
                        parameters: parameters
                    }));
                    //afx.chartBuild();
                }

                var attributes = {
                    "target": filename,
                    "title": title,
                    "alt": alt,
                    "caption": caption,
                    "width": width,
                    "height": height,
                    "scale": scale,
                    "align": align,
                    "role": role,
                    "link": link,
                    "float": float,
                    "opt": opt
                };

                var keys = Object.keys(attributes);

                keys.forEach(function (key) {
                    if (attributes[key]["$nil?"]()) {
                        delete attributes[key];
                    }
                });

                return __self.$create_image_block(parent, Opal.hash(attributes))
                    ;
            }, nil) && 'process';
    })(__self, (($scope.get('Extensions')).$$scope.get('BlockProcessor')));
};
/* Generated by Opal 0.8.0 */
Opal.modules["chart-block-macro"] = function (Opal) {
    Opal.dynamic_require_severity = "ignore";
    var $a, $b, TMP_1, self = Opal.top, $scope = Opal, nil = Opal.nil, $breaker = Opal.breaker, $slice = Opal.slice;

    if ($scope.get('RUBY_ENGINE')['$==']("opal")) {
        self.$require("chart-block-macro/extension")
    }
    ;
    return ($a = ($b = $scope.get('Extensions')).$register, $a.$$p = (TMP_1 = function () {
        var self = TMP_1.$$s || this;

        self.$block_macro($scope.get('ChartBlockMacro'));
        return self.$block($scope.get('ChartBlockProcessor'));
    }, TMP_1.$$s = self, TMP_1), $a).call($b);
};

Opal.require('chart-block-macro');
Opal.require('chart-block-macro/extension');<commit_msg>Update context info on chart block extension<commit_after>/* Generated by Opal 0.8.0 */
Opal.modules["chart-block-macro/extension"] = function (Opal) {
    Opal.dynamic_require_severity = "ignore";
    var __self = Opal.top, $scope = Opal, nil = Opal.nil, $breaker = Opal.breaker, $slice = Opal.slice, $klass = Opal.klass;

    if ($scope.get('RUBY_ENGINE')['$==']("opal")) {
    }
    ;
    __self.$include(Opal.get('Asciidoctor'));
    (function ($base, $super) {
        function $ChartBlockMacro() {
        };
        var __self = $ChartBlockMacro = $klass($base, $super, 'ChartBlockMacro', $ChartBlockMacro);

        var def = __self.$$proto, $scope = __self.$$scope;

        __self.$use_dsl();
        __self.$named("chart");

        return (def.$process = function (parent, target, attrs) {

                var $a, __self = this;

                var csvFile = (attrs['$[]']("data-uri")),
                    chartType = target,
                    title = (attrs['$[]']("title")),
                    alt = (attrs['$[]']("alt")),
                    caption = (attrs['$[]']("caption")),
                    width = (attrs['$[]']("width")),
                    height = (attrs['$[]']("height")),
                    scale = (attrs['$[]']("scale")),
                    align = (attrs['$[]']("align")),
                    type = (attrs['$[]']("type")),
                    role = (attrs['$[]']("role")),
                    link = (attrs['$[]']("link")),
                    float = (attrs['$[]']("float")),
                    filename = (attrs['$[]']("file")),
                    cache = (attrs['$[]']("cache")),
                    opt = (attrs['$[]']("opt")),
                    imagesdir = parent.$document().$attr('imagesdir', '');

                if (filename == "") {
                    return nil;
                }

                if (cache != "enabled") {
                    var parameters = [csvFile, imagesdir, parent.$image_uri(filename), chartType, opt].map(function (e) {
                        return e + "";
                    });

                    postMessage(JSON.stringify({
                        type: "afx",
                        func: "chartBuildFromCsv",
                        parameters: parameters
                    }));
                }

                var attributes = {
                    "target": filename,
                    "title": title,
                    "alt": alt,
                    "caption": caption,
                    "width": width,
                    "height": height,
                    "scale": scale,
                    "align": align,
                    "role": role,
                    "link": link,
                    "float": float,
                    "opt": opt
                };

                var keys = Object.keys(attributes);

                keys.forEach(function (key) {
                    if (attributes[key]["$nil?"]()) {
                        delete attributes[key];
                    }
                });

                return __self.$create_image_block(parent, Opal.hash(attributes))
                    ;
            }, nil) && 'process';
    })(__self, (($scope.get('Extensions')).$$scope.get('BlockMacroProcessor')));
    return (function ($base, $super) {
        function $ChartBlockProcessor() {
        };
        var __self = $ChartBlockProcessor = $klass($base, $super, 'ChartBlockProcessor', $ChartBlockProcessor);

        var def = __self.$$proto, $scope = __self.$$scope;

        __self.$use_dsl();
        __self.$named("chart");
        __self.$on_context(["open", "literal", "listing","pass"]);
        __self.$parse_content_as("literal");

        return (def.$process = function (parent, reader, attrs) {

                var $a, __self = this;

                var chartType = (attrs['$[]'](2)),
                    title = (attrs['$[]']("title")),
                    alt = (attrs['$[]']("alt")),
                    caption = (attrs['$[]']("caption")),
                    width = (attrs['$[]']("width")),
                    height = (attrs['$[]']("height")),
                    scale = (attrs['$[]']("scale")),
                    align = (attrs['$[]']("align")),
                    type = (attrs['$[]']("type")),
                    role = (attrs['$[]']("role")),
                    link = (attrs['$[]']("link")),
                    float = (attrs['$[]']("float")),
                    filename = (attrs['$[]']("file")),
                    cache = (attrs['$[]']("cache")),
                    opt = (attrs['$[]']("opt")),
                    imagesdir = parent.$document().$attr('imagesdir', '');

                if (filename == "") {
                    return nil;
                }

                if (cache != "enabled") {

                    var parameters = [reader.$read(), imagesdir, parent.$image_uri(filename), chartType, opt].map(function (e) {
                        return e + "";
                    });

                    postMessage(JSON.stringify({
                        type: "afx",
                        func: "chartBuild",
                        parameters: parameters
                    }));
                    //afx.chartBuild();
                }

                var attributes = {
                    "target": filename,
                    "title": title,
                    "alt": alt,
                    "caption": caption,
                    "width": width,
                    "height": height,
                    "scale": scale,
                    "align": align,
                    "role": role,
                    "link": link,
                    "float": float,
                    "opt": opt
                };

                var keys = Object.keys(attributes);

                keys.forEach(function (key) {
                    if (attributes[key]["$nil?"]()) {
                        delete attributes[key];
                    }
                });

                return __self.$create_image_block(parent, Opal.hash(attributes))
                    ;
            }, nil) && 'process';
    })(__self, (($scope.get('Extensions')).$$scope.get('BlockProcessor')));
};
/* Generated by Opal 0.8.0 */
Opal.modules["chart-block-macro"] = function (Opal) {
    Opal.dynamic_require_severity = "ignore";
    var $a, $b, TMP_1, self = Opal.top, $scope = Opal, nil = Opal.nil, $breaker = Opal.breaker, $slice = Opal.slice;

    if ($scope.get('RUBY_ENGINE')['$==']("opal")) {
        self.$require("chart-block-macro/extension")
    }
    ;
    return ($a = ($b = $scope.get('Extensions')).$register, $a.$$p = (TMP_1 = function () {
        var self = TMP_1.$$s || this;

        self.$block_macro($scope.get('ChartBlockMacro'));
        return self.$block($scope.get('ChartBlockProcessor'));
    }, TMP_1.$$s = self, TMP_1), $a).call($b);
};

Opal.require('chart-block-macro');
Opal.require('chart-block-macro/extension');<|endoftext|>


<commit_before>var FSError = require( '../error' )
var ExFAT = require( '../exfat' )

/**
 * ExFAT Volume
 * @constructor
 * @param {Object} partition
 * @return {Volume}
 */
function Volume( partition ) {

  if( !(this instanceof Volume) )
    return new Volume( partition )

  this.partition = partition

  this.vbr = new Volume.BootRecord()
  this.allocTable = new Volume.AllocationTable( this )

  // Node compatible fs API
  // this.fs = new ExFAT.FileSystem( this )

}

/**
 * Volume Boot Record (VBR)
 * @type {Function}
 */
Volume.BootRecord = require( './boot-record' )

Volume.AllocationTable = require( './allocation-table' )

/**
 * Volume prototype
 * @type {Object}
 */
Volume.prototype = {

  constructor: Volume,

  readVBR: function( lba, callback ) {

    var self = this
    var done = callback.bind( this )

    this.partition.readBlocks( lba, lba + 1, null,
      function( error, buffer, bytesRead ) {
        if( error != null ) { return done( error ) }
        try { self.vbr.parse( buffer ) }
        catch( err ) { error = err }
        done( error )
      }
    )

  },

  readAllocTable: function( callback ) {

    var self = this
    var done = callback.bind( this )

    var from = this.vbr.fatSectorStart
    var to = from + this.vbr.fatSectorCount

    this.partition.readBlocks( from, to, function( error, buffer, bytesRead ) {
      if( error ) return done( error )
      if( bytesRead !== ( self.vbr.fatSectorCount * self.partition.device.blockSize ) )
        return done( new FSError( FSError.EIO ), 'Bytes read mismatch' )
      self.allocTable.parse( buffer )
      done()
    })

  },

  readCluster: function( cluster, callback ) {

    var self = this
    var done = callback.bind( this )

    // TODO: Cluster -> Sector number translation (necessary?)
    var from = this.vbr.fatSectorStart + cluster
    var to = from + 1

    this.partition.readBlocks( from, to, function( error, buffer, bytesRead ) {
      if( error ) return done( error )
      done( null, buffer )
    })

  },

  mount: function( callback ) {

    var self = this
    var done = callback.bind( this )

    this.readVBR( 0, function( error ) {
      if( error ) return done( error )
      this.readAllocTable( function( error ) {
        if( error ) return done( error )
        done()
      })
    })

  },

}

// Exports
module.exports = Volume
<commit_msg>Update lib/volume: Add sector/cluster size<commit_after>var FSError = require( '../error' )
var ExFAT = require( '../exfat' )

/**
 * ExFAT Volume
 * @constructor
 * @param {Object} partition
 * @return {Volume}
 */
function Volume( partition ) {

  if( !(this instanceof Volume) )
    return new Volume( partition )

  this.partition = partition

  this.vbr = new Volume.BootRecord()
  this.allocTable = new Volume.AllocationTable( this )

  // Node compatible fs API
  // this.fs = new ExFAT.FileSystem( this )

}

/**
 * Volume Boot Record (VBR)
 * @type {Function}
 */
Volume.BootRecord = require( './boot-record' )

Volume.AllocationTable = require( './allocation-table' )

/**
 * Volume prototype
 * @type {Object}
 */
Volume.prototype = {

  constructor: Volume,

  get sectorSize() {
    return 1 << this.vbr.sectorBits
  },

  get clusterSize() {
    return this.sectorSize << this.vbr.spcBits
  },

  readVBR: function( lba, callback ) {

    var self = this
    var done = callback.bind( this )

    this.partition.readBlocks( lba, lba + 1, null,
      function( error, buffer, bytesRead ) {
        if( error != null ) { return done( error ) }
        try { self.vbr.parse( buffer ) }
        catch( err ) { error = err }
        done( error )
      }
    )

  },

  readAllocTable: function( callback ) {

    var self = this
    var done = callback.bind( this )

    var from = this.vbr.fatSectorStart
    var to = from + this.vbr.fatSectorCount

    this.partition.readBlocks( from, to, function( error, buffer, bytesRead ) {
      if( error ) return done( error )
      if( bytesRead !== ( self.vbr.fatSectorCount * self.partition.device.blockSize ) )
        return done( new FSError( FSError.EIO ), 'Bytes read mismatch' )
      self.allocTable.parse( buffer )
      done()
    })

  },

  readCluster: function( cluster, callback ) {

    var self = this
    var done = callback.bind( this )

    // TODO: Cluster -> Sector number translation (necessary?)
    var from = this.vbr.fatSectorStart + cluster
    var to = from + ( this.clusterSize / this.sectorSize )

    this.partition.readBlocks( from, to, function( error, buffer, bytesRead ) {
      if( error ) return done( error )
      done( null, buffer )
    })

  },

  mount: function( callback ) {

    var self = this
    var done = callback.bind( this )

    this.readVBR( 0, function( error ) {
      if( error ) return done( error )
      this.readAllocTable( function( error ) {
        if( error ) return done( error )
        done()
      })
    })

  },

}

// Exports
module.exports = Volume
<|endoftext|>


<commit_before>;(function(player)
{

    class Err extends AbstractSite
    {
        constructor(player)
        {
            super(player);
            console.log("err.ee");
            this.contentApiUrl = "https://etv.err.ee/api/loader/GetTimeLineContent/%s";
            this.liveStreams = {
                "etv": "http://etvstream.err.ee/live/smil:etv/playlist.m3u8",
                "etv2": "http://etv2stream.err.ee/live/smil:etv2/playlist.m3u8",
                "etvpluss": "http://striimid.err.ee/live/smil:etvpluss/playlist.m3u8"
            };

            // alias
            this.liveStreams["etv+"] = this.liveStreams["etvpluss"];

            contextMenu.addSite("err.ee", this, ["*://*.err.ee/v/*/*-*-*-*-*/*"]);
            browserAction.addSite("err.ee", this, [
                ".*err.ee/v/.*/.*-.*-.*-.*-.*/.*",
                ".*otse.err.ee/.*"
            ]);
        }

        _getVideoIdFromUrl(url)
        {
            let parts = url.split("/");
            let lastChar = parts[parts.length - 1];
            if(!lastChar) {
                parts.pop();
            }
            let hasVOD = url.indexOf("/v/") >= 0;
            let starIndex = hasVOD ? parts.length - 2 : parts.length - 1;
            for (let i = starIndex; i >= 0; i--) {
                let text = parts[i];
                if(text) {
                    return text;
                }
            }
            return "";
        }

        getFileFromUrl(url)
        {
            return new Promise((resolve, reject) => {

                let videoId = this._getVideoIdFromUrl(url);
                console.log("videoId " + videoId);

                const isLive = url.indexOf("otse.err.ee") >= 0;
                if(isLive) {
                    console.log("live video");
                    let mediaUrl = this.liveStreams[videoId];
                    if(mediaUrl) {
                        resolve(mediaUrl);
                        return;
                    }
                }

                const apiUrl = sprintf(this.contentApiUrl, videoId);

                let urlRequest = new URLRequest(apiUrl);
                urlRequest.send().then(response => {

                    const responseData = JSON.parse(response);
                    let mediaSources = responseData.MediaSources;
                    const len = mediaSources.length;

                    let mediaUrl = null;
                    for (let i = 0; i < len; i++)
                    {
                        let media = mediaSources[i];
                        const videoUrl = media.Content;
                        if(videoUrl)
                        {
                            mediaUrl = "rtmp" + videoUrl.replace("@", "_definst_/");
                            break;
                        }
                    }

                    if(mediaUrl) {
                        resolve(mediaUrl);
                    } else {
                        // check if we can play live video
                        const scheduleTimeString = responseData.Updated;

                        let scheduledDate = new Date(scheduleTimeString);
                        let scheduledDateUTC = scheduledDate.getTime();
                        let currentTimeUTC = new Date().getTime();

                        const diff = currentTimeUTC - scheduledDateUTC;
                        console.log(diff + " milliseconds");
                        if(diff >= 0) {
                            // probably it is a live video
                            let portalName = responseData.Portal.PortalName.toLowerCase();
                            resolve(this.liveStreams[portalName]);
                        }
                        else {
                            reject({message: "Video is scheduled for " + scheduledDate.toString()});
                        }
                    }

                }).catch(() => {
                    resolve(null);
                });
            });
        }
    }

    new Err(player)
})(player);<commit_msg>simplified etv code<commit_after>;(function(player)
{

    class Err extends AbstractSite
    {
        constructor(player)
        {
            super(player);
            console.log("err.ee");
            this.contentApiUrl = "https://etv.err.ee/api/loader/GetTimeLineContent/%s";
            this.subtitleIdApiUrl = "http://etv.err.ee/services/api/subtitles/check?file=%s";

            this.subtitleContentUrl = "http://etv.err.ee/services/subtitles/file/%s/%s_%s";
            this.liveStreams = {
                "etv": "http://etvstream.err.ee/live/smil:etv/playlist.m3u8",
                "etv2": "http://etv2stream.err.ee/live/smil:etv2/playlist.m3u8",
                "etvpluss": "http://striimid.err.ee/live/smil:etvpluss/playlist.m3u8"
            };

            // alias
            this.liveStreams["etv+"] = this.liveStreams["etvpluss"];

            contextMenu.addSite("err.ee", this, ["*://*.err.ee/v/*/*-*-*-*-*/*"]);
            browserAction.addSite("err.ee", this, [
                ".*err.ee/v/.*/.*-.*-.*-.*-.*/.*",
                ".*otse.err.ee/.*"
            ]);
        }

        _getVideoIdFromUrl(url)
        {
            let parts = url.split("/");
            let lastChar = parts[parts.length - 1];
            if(!lastChar) {
                parts.pop();
            }
            let hasVOD = url.indexOf("/v/") >= 0;
            let starIndex = hasVOD ? parts.length - 2 : parts.length - 1;
            for (let i = starIndex; i >= 0; i--) {
                let text = parts[i];
                if(text) {
                    return text;
                }
            }
            return "";
        }

        _getContentUrl(videoId) {
            return new Promise((resolve, reject) => {

                const apiUrl = sprintf(this.contentApiUrl, videoId);

                let urlRequest = new URLRequest(apiUrl);
                urlRequest.send().then(response => {

                    const responseData = JSON.parse(response);
                    let mediaSources = responseData.MediaSources;

                    const len = mediaSources.length;
                    for (let i = 0; i < len; i++)
                    {
                        let media = mediaSources[i];
                        const videoUrl = media.Content;
                        if(videoUrl)
                        {
                            resolve(videoUrl);
                            return;
                        }
                    }

                    throw new Error("Content url not found");

                }).catch(err => {
                    console.error(err);
                    reject();
                });

            });
        }

        getSubtitleFiles(url) {

            return new Promise((resolve, reject) => {

                const subtitleIdApiUrl = sprintf(this.subtitleIdApiUrl, url);

                new URLRequest(subtitleIdApiUrl)
                    .send()
                    .then(content => {
                        console.log(content);

                        let subtitleUrls = [];
                        let contentJSON = JSON.parse(content);
                        let subData = contentJSON["subtitles"];

                        if(subData) {
                            let langs = ["ET", "VA", "RU"];
                            langs.forEach((lang) => {
                                if(subData[lang] && subData[lang]["id"]) {
                                    const subId = subData[lang]["id"].toString();
                                    let sUrl = sprintf(this.subtitleContentUrl, subId, subId, lang);
                                    subtitleUrls.push(sUrl);
                                }
                            });
                        }

                        if(subtitleUrls.length > 0) {
                            return subtitleUrls;
                        }

                        throw new Error("subtitles not found!");
                    })
                    .then(subUrls => {
                        resolve(subUrls);
                    })
                    .catch(err => {
                        console.error(err);
                        reject();
                    });

            });
        }

        getFileFromUrl(url) {

            return new Promise((resolve, reject) => {

                let videoId = this._getVideoIdFromUrl(url);
                console.log("videoId " + videoId);

                const isLive = url.indexOf("otse.err.ee") >= 0;
                if(isLive) {
                    console.log("live video, videoId " + videoId);
                    let mediaUrl = this.liveStreams[videoId];
                    if(mediaUrl) {
                        resolve(mediaUrl);
                        return;
                    }
                }

                this._getContentUrl(videoId)
                    .then(rawVideoUrl => {
                        let mediaUrl = "rtmp" + rawVideoUrl.replace("@", "_definst_/");
                        resolve(mediaUrl);
                    })
                    .catch(err => {
                        reject();
                    });
            });
        }
    }

    new Err(player)
})(player);<|endoftext|>


<commit_before>var FSError = require( '../error' )
var ExFAT = require( '../exfat' )
var hex = require( 'hex' )
var debug = require( 'debug' )( 'exfat:volume' )

/**
 * ExFAT Volume
 * @constructor
 * @param {Object} partition
 * @return {Volume}
 */
function Volume( partition ) {

  if( !(this instanceof Volume) )
    return new Volume( partition )

  this.partition = partition

  this.vbr = new Volume.BootRecord()
  this.allocTable = new Volume.AllocationTable( this )
  this.root = new ExFAT.Node()

  // Node compatible fs API
  // this.fs = new ExFAT.FileSystem( this )

}

/**
 * Volume Boot Record (VBR)
 * @type {Function}
 */
Volume.BootRecord = require( './boot-record' )

Volume.AllocationTable = require( './allocation-table' )

/**
 * Volume prototype
 * @type {Object}
 */
Volume.prototype = {

  constructor: Volume,

  get sectorSize() {
    return 1 << this.vbr.sectorBits
  },

  get clusterSize() {
    return this.sectorSize << this.vbr.spcBits
  },

  readVBR: function( lba, callback ) {

    var self = this
    var done = callback.bind( this )

    debug( 'read_vbr' )

    this.partition.readBlocks( lba, lba + 1, null,
      function( error, buffer, bytesRead ) {
        if( error != null ) { return done( error ) }
        try { self.vbr.parse( buffer ) }
        catch( err ) { error = err }
        done( error )
      }
    )

  },

  readAllocTable: function( callback ) {

    var self = this
    var done = callback.bind( this )

    var from = this.vbr.fatSectorStart
    var to = from + this.vbr.fatSectorCount

    debug( 'read_alloc_table', from, to )

    this.partition.readBlocks( from, to, function( error, buffer, bytesRead ) {
      if( error ) return done( error )
      var expected = ( self.vbr.fatSectorCount * self.partition.device.blockSize )
      if( bytesRead !== expected )
        return done( new FSError( FSError.EIO, `Bytes read mismatch (read ${bytesRead}, expected ${expected})` ) )
      self.allocTable.parse( buffer )
      done()
    })

  },

  readCluster: function( cluster, callback ) {

    var self = this
    var done = callback.bind( this )

    // TODO: Cluster -> Sector number translation (necessary?)
    var from = this.vbr.fatSectorStart + cluster
    var to = from + ( this.clusterSize / this.sectorSize )

    debug( 'read_cluster', from, to )

    this.partition.readBlocks( from, to, function( error, buffer, bytesRead ) {
      if( error ) return done( error )
      done( null, buffer )
    })

  },

  readRootDir: function( callback ) {

    var self = this
    var done = callback.bind( this )

    this.root.startCluster = this.vbr.rootDirCluster
    this.root.fptrCluster = this.root.startCluster
    this.root.name = '\0'
    this.root.size = 0

    var clusters = 0
    var maxClusters = this.vbr.clusterCount
    var rootCluster = this.vbr.rootDirCluster

    debug( 'read_root_dir', rootCluster )

    this.readCluster( this.vbr.clusterSectorStart, function( error, buffer ) {
      error && console.log( error )
      if( !error ) {
        hex( buffer.slice( 0, 512 ) )
      }
    })

    // if( this.root.size === 0 ) {
    //   return done( new FSError( FSError.EIO, 'Invalid root directory size' ) )
    // }

    // TODO
    done()

  },

  readdir: function( cluster, callback ) {

    this.readCluster( this.vbr.clusterSectorStart, function( error, buffer ) {

      if( error ) {
        throw error
      }

      var cluster = this.vbr.clusterSectorStart
      var offset = 0
      var contiguous = false // ?!
      var continuations = 0

      var chunk, entry
      var entries = []

      while( offset < this.clusterSize ) {

        chunk = buffer.slice( offset, offset += 32 )
        entry = ExFAT.Entry.parse( chunk )
        entry && entries.push( entry )

      }

      this.root.entries = entries

      callback()

    })

  },

  mount: function( callback ) {

    var self = this
    var done = callback.bind( this )

    this.readVBR( 0, function( error ) {
      if( error ) return done( error )
      this.readAllocTable( function( error ) {
        if( error ) return done( error )
        this.readRootDir( function( error ) {
          if( error ) return done( error )
          this.readdir( 12345678, function( error ) {
            if( error ) return done( error )
            done()
          })
        })
      })
    })

  },

}

// Exports
module.exports = Volume
<commit_msg>Update lib/volume: Fix assortment of errors<commit_after>var FSError = require( '../error' )
var ExFAT = require( '../exfat' )
var debug = require( 'debug' )( 'exfat:volume' )

/**
 * ExFAT Volume
 * @constructor
 * @param {Object} partition
 * @return {Volume}
 */
function Volume( partition ) {

  if( !(this instanceof Volume) )
    return new Volume( partition )

  this.partition = partition

  this.vbr = new Volume.BootRecord()
  this.allocTable = new Volume.AllocationTable( this )
  this.root = new ExFAT.Node()

  // Node compatible fs API
  // this.fs = new ExFAT.FileSystem( this )

}

/**
 * Volume Boot Record (VBR)
 * @type {Function}
 */
Volume.BootRecord = require( './boot-record' )

Volume.AllocationTable = require( './allocation-table' )

/**
 * Volume prototype
 * @type {Object}
 */
Volume.prototype = {

  constructor: Volume,

  get sectorSize() {
    return 1 << this.vbr.sectorBits
  },

  get clusterSize() {
    return this.sectorSize << this.vbr.spcBits
  },

  readVBR: function( lba, callback ) {

    var self = this
    var done = callback.bind( this )

    debug( 'read_vbr' )

    this.partition.readBlocks( lba, lba + 1, null,
      function( error, buffer, bytesRead ) {
        if( error != null ) { return done( error ) }
        try { self.vbr.parse( buffer ) }
        catch( err ) { error = err }
        done( error )
      }
    )

  },

  readAllocTable: function( callback ) {

    var self = this
    var done = callback.bind( this )

    var from = this.vbr.fatSectorStart
    var to = from + this.vbr.fatSectorCount

    debug( 'read_alloc_table', from, to )

    this.partition.readBlocks( from, to, function( error, buffer, bytesRead ) {
      if( error ) return done( error )
      var expected = ( self.vbr.fatSectorCount * self.partition.device.blockSize )
      if( bytesRead !== expected )
        return done( new FSError( FSError.EIO, `Bytes read mismatch (read ${bytesRead}, expected ${expected})` ) )
      self.allocTable.parse( buffer )
      done()
    })

  },

  readCluster: function( cluster, callback ) {

    var self = this
    var done = callback.bind( this )

    // TODO: Cluster -> Sector number translation (necessary?)
    var from = this.vbr.fatSectorStart + cluster
    var to = from + ( this.clusterSize / this.sectorSize )

    debug( 'read_cluster', from, to )

    this.partition.readBlocks( from, to, function( error, buffer, bytesRead ) {
      if( error ) return done( error )
      done( error, buffer )
    })

  },

  readRootDir: function( callback ) {

    var self = this
    var done = callback.bind( this )

    this.root.startCluster = this.vbr.rootDirCluster
    this.root.fptrCluster = this.root.startCluster
    this.root.name = '\0'
    this.root.size = 0

    var clusters = 0
    var maxClusters = this.vbr.clusterCount
    var rootCluster = this.vbr.rootDirCluster

    debug( 'read_root_dir', rootCluster )

    if( this.root.size === 0 ) {
      return done( new FSError( FSError.EIO, 'Invalid root directory size' ) )
    }

    this.readCluster( this.vbr.clusterSectorStart, function( error, buffer ) {
      if( error ) debug( 'read_root_dir:error', error )
      // TODO: Parse rootDir
      done( error, buffer )
    })

  },

  readdir: function( cluster, callback ) {

    var self = this
    var done = callback.bind( this )

    debug( 'readdir' )

    this.readCluster( cluster, function( error, buffer ) {

      if( error ) {
        return done( error )
      }

      var cluster = this.vbr.clusterSectorStart
      var offset = 0
      var contiguous = false // ?!
      var continuations = 0

      var chunk, entry
      var entries = []

      while( offset < this.clusterSize ) {

        chunk = buffer.slice( offset, offset += 32 )
        entry = ExFAT.Entry.parse( chunk )
        entry && entries.push( entry )

      }

      this.root.entries = entries

      done( error, entries )

    })

  },

  mount: function( callback ) {

    var self = this
    var done = callback.bind( this )

    // TODO: Callback hell avoidance maneuver
    this.readVBR( 0, function( error ) {
      if( error ) return done( error )
      this.readAllocTable( function( error ) {
        if( error ) return done( error )
        this.readRootDir( function( error ) {
          if( error ) return done( error )
          // TODO: ...
          this.readdir( 12345678, function( error ) {
            if( error ) return done( error )
            done()
          })
        })
      })
    })

  },

}

// Exports
module.exports = Volume
<|endoftext|>


<commit_before>export function sortBySelection(filtered, sortBy, sortIsDescending, selectsDefault) {
  const cloned = [...filtered];

  if (sortBy === selectsDefault) {
    if (!sortIsDescending) {
      cloned.reverse();
    }
  } else {
    if (sortIsDescending) {
      cloned.sort((a, b) => {
        if (a.sortable[sortBy] === b.sortable[sortBy]) { return 0; }
        return a.sortable[sortBy] > b.sortable[sortBy] ? -1 : 1;
      });
    } else {
      cloned.sort((a, b) => {
        if (a.sortable[sortBy] === b.sortable[sortBy]) { return 0; }
        return a.sortable[sortBy] < b.sortable[sortBy] ? -1 : 1;
      });
    }
  }

  return cloned;
}<commit_msg>Only clone if mutating<commit_after>export function sortBySelection(filtered, sortBy, sortIsDescending, selectsDefault) {
  if (sortBy === selectsDefault && sortIsDescending) {
    return filtered; // return as-is if default
  }

  const cloned = [...filtered];

  if (sortBy === selectsDefault) {
    cloned.reverse();
  } else if (sortIsDescending) {
    cloned.sort((a, b) => {
      if (a.sortable[sortBy] === b.sortable[sortBy]) { return 0; }
      return a.sortable[sortBy] > b.sortable[sortBy] ? -1 : 1;
    });
  } else {
    cloned.sort((a, b) => {
      if (a.sortable[sortBy] === b.sortable[sortBy]) { return 0; }
      return a.sortable[sortBy] < b.sortable[sortBy] ? -1 : 1;
    });
  }

  return cloned;
}<|endoftext|>


<commit_before>;(function()
{

    class Err extends AbstractSite
    {
        constructor()
        {
            super();
            console.log("err.ee");
            this.contentApiUrl = "https://etv.err.ee/api/loader/GetTimeLineContent/%s";
            this.liveStreams = {
                "etv": "http://etvstream.err.ee/live/smil:etv/playlist.m3u8",
                "etv2": "http://etv2stream.err.ee/live/smil:etv2/playlist.m3u8",
                "etvpluss": "http://striimid.err.ee/live/smil:etvpluss/playlist.m3u8"
            };

            // alias
            this.liveStreams["etv+"] = this.liveStreams["etvpluss"];

            contextMenu.addSite("err.ee", this, ["*://*.err.ee/v/*/*-*-*-*-*/*"]);
            browserAction.addSite("err.ee", this, [
                ".*err.ee/v/.*/.*-.*-.*-.*-.*/.*",
                ".*otse.err.ee/k/.*"
            ]);
        }

        _getVideoIdFromUrl(url)
        {
            let parts = url.split("/");
            let lastChar = parts[parts.length - 1];
            if(!lastChar) {
                parts.pop();
            }
            let hasVOD = url.indexOf("/v/") >= 0;
            let starIndex = hasVOD ? parts.length - 2 : parts.length - 1;
            for (let i = starIndex; i >= 0; i--) {
                let text = parts[i];
                if(text) {
                    return text;
                }
            }
            return "";
        }

        getFileFromUrl(url)
        {
            return new Promise((resolve, reject) => {

                let videoId = this._getVideoIdFromUrl(url);
                console.log("videoId " + videoId);

                const isLive = url.indexOf("otse.err.ee") >= 0;
                if(isLive) {
                    console.log("live video");
                    let mediaUrl = this.liveStreams[videoId];
                    if(mediaUrl) {
                        resolve(mediaUrl);
                        return;
                    }
                }

                const apiUrl = sprintf(this.contentApiUrl, videoId);

                let urlRequest = new URLRequest(apiUrl);
                urlRequest.send().then(response => {

                    const responseData = JSON.parse(response);
                    let mediaSources = responseData.MediaSources;
                    const len = mediaSources.length;

                    let mediaUrl = null;
                    for (let i = 0; i < len; i++)
                    {
                        let media = mediaSources[i];
                        const videoUrl = media.Content;
                        if(videoUrl)
                        {
                            mediaUrl = "rtmp" + videoUrl.replace("@", "_definst_/");
                            break;
                        }
                    }

                    if(mediaUrl) {
                        resolve(mediaUrl);
                    } else {
                        // check if we can play live video
                        const scheduleTimeString = responseData.Updated;

                        let scheduledDate = new Date(scheduleTimeString);
                        let scheduledDateUTC = scheduledDate.getTime();
                        let currentTimeUTC = new Date().getTime();

                        const diff = currentTimeUTC - scheduledDateUTC;
                        console.log(diff + " milliseconds");
                        if(diff >= 0) {
                            // probably it is a live video
                            let portalName = responseData.Portal.PortalName.toLowerCase();
                            resolve(this.liveStreams[portalName]);
                        }
                        else {
                            reject({message: "Video is scheduled for " + scheduledDate.toString()});
                        }
                    }

                }).catch(() => {
                    resolve(null);
                });
            });
        }
    }

    new Err()
})();<commit_msg>update<commit_after>;(function()
{

    class Err extends AbstractSite
    {
        constructor()
        {
            super();
            console.log("err.ee");
            this.contentApiUrl = "https://etv.err.ee/api/loader/GetTimeLineContent/%s";
            this.liveStreams = {
                "etv": "http://etvstream.err.ee/live/smil:etv/playlist.m3u8",
                "etv2": "http://etv2stream.err.ee/live/smil:etv2/playlist.m3u8",
                "etvpluss": "http://striimid.err.ee/live/smil:etvpluss/playlist.m3u8"
            };

            // alias
            this.liveStreams["etv+"] = this.liveStreams["etvpluss"];

            contextMenu.addSite("err.ee", this, ["*://*.err.ee/v/*/*-*-*-*-*/*"]);
            browserAction.addSite("err.ee", this, [
                ".*err.ee/v/.*/.*-.*-.*-.*-.*/.*",
                ".*otse.err.ee/.*"
            ]);
        }

        _getVideoIdFromUrl(url)
        {
            let parts = url.split("/");
            let lastChar = parts[parts.length - 1];
            if(!lastChar) {
                parts.pop();
            }
            let hasVOD = url.indexOf("/v/") >= 0;
            let starIndex = hasVOD ? parts.length - 2 : parts.length - 1;
            for (let i = starIndex; i >= 0; i--) {
                let text = parts[i];
                if(text) {
                    return text;
                }
            }
            return "";
        }

        getFileFromUrl(url)
        {
            return new Promise((resolve, reject) => {

                let videoId = this._getVideoIdFromUrl(url);
                console.log("videoId " + videoId);

                const isLive = url.indexOf("otse.err.ee") >= 0;
                if(isLive) {
                    console.log("live video");
                    let mediaUrl = this.liveStreams[videoId];
                    if(mediaUrl) {
                        resolve(mediaUrl);
                        return;
                    }
                }

                const apiUrl = sprintf(this.contentApiUrl, videoId);

                let urlRequest = new URLRequest(apiUrl);
                urlRequest.send().then(response => {

                    const responseData = JSON.parse(response);
                    let mediaSources = responseData.MediaSources;
                    const len = mediaSources.length;

                    let mediaUrl = null;
                    for (let i = 0; i < len; i++)
                    {
                        let media = mediaSources[i];
                        const videoUrl = media.Content;
                        if(videoUrl)
                        {
                            mediaUrl = "rtmp" + videoUrl.replace("@", "_definst_/");
                            break;
                        }
                    }

                    if(mediaUrl) {
                        resolve(mediaUrl);
                    } else {
                        // check if we can play live video
                        const scheduleTimeString = responseData.Updated;

                        let scheduledDate = new Date(scheduleTimeString);
                        let scheduledDateUTC = scheduledDate.getTime();
                        let currentTimeUTC = new Date().getTime();

                        const diff = currentTimeUTC - scheduledDateUTC;
                        console.log(diff + " milliseconds");
                        if(diff >= 0) {
                            // probably it is a live video
                            let portalName = responseData.Portal.PortalName.toLowerCase();
                            resolve(this.liveStreams[portalName]);
                        }
                        else {
                            reject({message: "Video is scheduled for " + scheduledDate.toString()});
                        }
                    }

                }).catch(() => {
                    resolve(null);
                });
            });
        }
    }

    new Err()
})();<|endoftext|>