itunes-bridge.js

/**
 * This JavaScript file contains the magic.
 *
 * iTunes-bridge
 * @author AngryKiller
 * @copyright 2018
 * @license GPL-3.0
 *
 */

var exports = module.exports = {};
var fs = require('fs');
var {execSync} = require('child_process');
var events = require('events');
var event = new events.EventEmitter();
var plist = require('plist');
var path = require('path');
var os = require('os');


if(process.platform === "darwin"){
    var libPath = path.resolve(os.homedir() + "/Music/iTunes/iTunes Library.xml");
}else if(process.platform === "win32"){
    var libPath = path.resolve(os.homedir() + "/My Music/iTunes/iTunes Library.xml");
}

var that = this;

/** Get informations about the current playing track
 * @returns {object}
 * @example {"name":"Business",
  "artist":"Eminem",
  "album":"The Eminem Show (Explicit Version)",
  "mediaKind":"song",
  "duration":251,
  "elapsedTime":2,
  "remainingTime":249,
  "genre":"Rap/Hip Hop",
  "releaseYear":2002,
  "id":2630,
  "playerState":"playing"}
 */
exports.getCurrentTrack = function () {
    if (exports.isRunning()) {
        try {
            return runScript('getCurrentTrack', 'fetch', undefined, true);
        } catch (e) {
            console.log(e);
        }
    } else {
        return {playerState: "stopped"};
    }
};
/**
 * Get the player state
 * @returns {string} - Possible values: playing, stopped or paused
 * @example "playing"
 */
exports.getPlayerState = function() {
    if (exports.isRunning()) {
        try {
            return runScript('getPlayerState', 'fetch', undefined, false);
        } catch (e) {
            console.log(e);
        }
    } else {
        return "stopped";
    }
};
/**
 * Gets the iTunes sound volume or sets it if there's a parameter (Windows only)
 * @param volume {int} - Windows only
 * @returns {int}
 */
exports.soundVolume = function(volume) {
    if (exports.isRunning()) {
        if(volume !== undefined && !isNaN(volume) && process.platform === "win32"){
            try{
                return runScript('setSoundVolume', 'control', volume);
            }catch(e){
                console.log(e);
            }
        }else{
            try {
                return runScript('getSoundVolume', 'fetch', undefined, false);
            } catch (e) {
                console.log(e);
            }
        }
    }else{
        console.log("iTunes is not running");
    }
};
/**
 * Tells iTunes to play
 */
exports.play = function (song) {
    runScript('play', 'control');
};
/**
 * Tells iTunes to pause
 */
exports.pause = function (){
    runScript('pause', 'control');
};
/**
 * Tells iTunes to stop
 */
exports.stop = function (){
    runScript('stop', 'control');
};
/**
 * Gets informations about a track from the library
 * @param {int} id - The id of the track
 * @returns {object}
 * @example  { 'Track ID': 1428,
     Size: 9019045,
     'Total Time': 217103,
     'Disc Number': 1,
     'Disc Count': 1,
     'Track Number': 14,
     'Track Count': 16,
     Year: 2011,
     BPM: 99,
     'Date Modified': 2018-03-18T22:37:46.000Z,
     'Date Added': 2018-03-24T14:03:15.000Z,
     'Bit Rate': 320,
     'Sample Rate': 44100,
     'Play Count': 3,
     'Play Date': 3604816264,
     'Play Date UTC': 2018-03-25T07:51:04.000Z,
     'Artwork Count': 1,
     'Persistent ID': '535F1580FAEB42E4',
     'Track Type': 'File',
     'File Folder Count': 5,
     'Library Folder Count': 1,
     Name: 'Ils sont cools',
     Artist: 'Orelsan, Gringe',
     'Album Artist': 'Orelsan',
     Album: 'Le chant des sirènes',
     Genre: 'Rap/Hip Hop',
     Kind: 'Fichier audio MPEG',
     'Sort Album': 'chant des sirènes',
     Location: 'file:///Users/steve/Music/iTunes/iTunes%20Media/Music/Orelsan/Le%20chant%20des%20sire%CC%80nes/14%20Ils%20sont%20cools.mp3' }
 */
exports.getTrack = function(id) {
    try {
        var obj = plist.parse(fs.readFileSync(libPath, 'utf8'));
        return obj.Tracks[id];
    }catch(err){
        return "not_found";
    }
};
/**
 * Gets the playlist count from the library
 * @returns {int}
 */
exports.getPlaylistCount = function () {
    try {
        var obj = plist.parse(fs.readFileSync(libPath, 'utf8'));
        return Object.keys(obj.Playlists).length;
    } catch (err) {
        return null;
    }
};
// TODO: Support for arguments in the track count (album, artist, playlist...)
/**
 * Gets the track count from the library
 * @returns {int}
 */
exports.getTrackCount = function () {
    try {
        var obj = plist.parse(fs.readFileSync(libPath, 'utf8'));
        return (Object.keys(obj.Tracks).length + 1);
    } catch (err) {
        return null;
    }
};




// Starting the event system (track change and player state change)
that.currentTrack = null;
setInterval(function () {
    var currentTrack = exports.getCurrentTrack();
    if (currentTrack && that.currentTrack) {
        // On track change
        /**
         * Emits a playing event
         *
         * @fires iTunes-bridge#playing
         */
        if (currentTrack.id !== that.currentTrack.id && currentTrack.playerState === "playing") {
            that.currentTrack = currentTrack;
            /**
             * Playing event
             *
             * @event iTunes-bridge#playing
             * @type {object}
             * @property {string} type - Indicates whenever the player has been resumed or this is a new track being played.
             * @property {object} currentTrack - Gives the current track
             */
            event.emit('playing', 'new_track', currentTrack);
        }
        /**
         * Emits a paused event
         *
         * @fires iTunes-bridge#paused
         */
        else if (currentTrack.id !== that.currentTrack.id && currentTrack.playerState === "paused") {
            that.currentTrack = currentTrack;
            /**
             * Paused event
             *
             * @event iTunes-bridge#paused
             * @type {object}
             * @property {string} type - Indicates whenever the player has been resumed or this is a new track being played.
             * @property {object} currentTrack - Gives the current track
             */
            event.emit('paused', 'new_track', currentTrack);
        }
        /**
         * Emits a stopped event
         *
         * @fires iTunes-bridge#stopped
         */
        else if (currentTrack.id !== that.currentTrack.id && currentTrack.playerState === "stopped") {
            that.currentTrack = {"playerState": "stopped"};
            /**
             * Stopped event.
             *
             * @event iTunes-bridge#stopped
             * @type {object}
             */
            event.emit('stopped');
        }
        // On player state change
        if (currentTrack.playerState !== that.currentTrack.playerState && currentTrack.id === that.currentTrack.id) {
            that.currentTrack.playerState = currentTrack.playerState;
            event.emit(currentTrack.playerState, 'player_state_change', currentTrack);
        }
    } else {
        that.currentTrack = currentTrack;
    }
}, 1500);

exports.emitter = event;

/**
 * Function to know if iTunes is running
 * @returns {boolean} - true or false
 */
exports.isRunning = function() {
    if(process.platform === "darwin") {
        try {
            execSync('pgrep -x "iTunes"');
            return true;
        }
        catch (err) {
            return false;
        }
    }else if(process.platform === "win32"){
        try {
            execSync('tasklist | find "iTunes.exe"');
            return true;
        }
        catch (err) {
            return false;
        }
    }
};


function runScript(req, type, args, isJson) {

    if (process.platform === "darwin"){
            var iTunesCtrlScpt  = path.join(__dirname, '/jxa/iTunesControl.js');
            var iTunesFetcherScpt  = path.join(__dirname, '/jxa/iTunesFetcher.js');
        switch(type){
            case "fetch": {
                if(isJson) {
                    return JSON.parse(execSync('osascript ' +iTunesFetcherScpt+' '  + req));
                }else{
                    return execSync('osascript ' +iTunesFetcherScpt+' ' + req);
                }
                break;
            }
            case "control": {
                try {
                    execSync('osascript '+iTunesCtrlScpt+' ' + req+' '+args);
                }catch(e){
                    console.error(e);
                }
                break;
            }
        }
    } else if (process.platform === "win32") {
            var iTunesCtrlScpt  = path.join(__dirname, '/wscript/iTunesControl.js');
            var iTunesFetcherScpt  = path.join(__dirname, '/wscript/iTunesFetcher.js');
        switch(type){
            case "fetch": {
                if(isJson) {
                    return JSON.parse(execSync('cscript //Nologo ' + iTunesFetcherScpt + ' ' + req, { encoding: 'utf8'}));
                }else{
                    return execSync('cscript //Nologo ' + iTunesFetcherScpt+' ' + req, { encoding: 'utf8'});
                }
                break;
            }
            case "control": {
                try {
                    execSync('cscript //Nologo '+ iTunesCtrlScpt+' ' + req+' '+args);
                }catch(e){
                    console.error(e);
                }
                break;
            }
        }
    }

}

// Sends an event on module load

// If you're wondering why there's a timeout, that's because if you use this in another module, you will require this file AND THEN load the eventemitter, so if these events are emitted immediately, you will never receive them :')
setTimeout(function(){
    switch(exports.getCurrentTrack().playerState){
        case "playing":{
            event.emit('playing', 'new_track', exports.getCurrentTrack());
            break;
        }
        case "paused":{
            event.emit('paused', 'new_track', exports.getCurrentTrack());
            break;
        }
        case "stopped":{
            event.emit('stopped');
            break;
        }
    }
}, 500);