<commit_before>
/* This file contains objecsts for chat interactions between player and NPCs. */

const RG = require('./rg');
const Keys = require('./keymap');
const Menu = require('./menu');

const Chat = {};
const stats = RG.STATS;

/* Chat object added to actors which have any interesting things to chat about.
 */
class ChatBase {

    constructor() {
        this.options = [];
        this.parent = null;
    }

    /* Adds a chat object or an option into this object. */
    add({name, option}) {
        this.options.push({name, option});
        if (option && option.setParent) {
            option.setParent(this);
        }
    }

    setParent(parent) {
        this.parent = parent;
    }

    getParent() {
        return this.parent;
    }

    getSelectionObject() {
        const selObj = {
            showMenu: () => true,
            getMenu: () => {
                const menuObj = {};
                this.options.forEach((opt, i) => {
                    menuObj[Keys.menuIndices[i]] = opt.name;
                });
                return menuObj;
            },
            select: code => {
                const selection = Keys.codeToIndex(code);
                if (selection < this.options.length) {
                    console.log('ChatBase got sel', selection);
                    const value = this.options[selection].option;
                    if (value.getSelectionObject) {
                        return value.getSelectionObject();
                    }
                    console.log('ChatBase ret value', value);
                    return value;
                }
                return Menu.EXIT_MENU;
            }
        };
        if (this.pre) {
            selObj.pre = this.pre;
        }
        return selObj;
    }

}
Chat.ChatBase = ChatBase;

/* Object used in actors which can give quests. */
class ChatQuest extends ChatBase {

    constructor() {
        super();
        const acceptOpt = {
            name: 'Accept the quest',
            option: this.questCallback.bind(this)
        };
        const refuseOpt = {
            name: 'Refuse the quest',
            option: Menu.EXIT_MENU
        };
        this.add(acceptOpt);
        this.add(refuseOpt);
    }

    /* getSelectionObject() {
        return this.selectionObject;
    }*/

    setQuestGiver(giver) {
        this.questGiver = giver;
        const qLen = 'lengthy';
        this.pre = [
            `${giver.getName} wants to offer a ${qLen} quest.`,
            'What do you want to do?'
        ];
    }

    setTarget(target) {
        this.target = target;
    }

    questCallback() {
        if (RG.isNullOrUndef([this.target, this.questGiver])) {
            RG.err('ChatQuest', 'questCallback',
                'target and questGiver must be defined');
        }
        // Create quest comp
        const giveQuestComp = new RG.Component.GiveQuest();
        giveQuestComp.setTarget(this.target);
        giveQuestComp.setGiver(this.questGiver);
        this.target.add(giveQuestComp);
    }
}
Chat.Quest = ChatQuest;

/* Chat Object for trainers in the game. */
class ChatTrainer {

    constructor() {
        this.selectionObject = {
            showMenu: () => true,
            pre: 'Please select a stat to train:',
            getMenu: () => {
                // RG.gameMsg('');
                const indices = Keys.menuIndices.slice(0, 6);
                const menuObj = {};
                stats.forEach((stat, index) => {
                    menuObj[indices[index]] = stats[index];
                    menuObj[indices[index]] += ` (${this.costs[index]} gold)`;
                });
                return menuObj;

            },
            select: code => {
                const selection = Keys.codeToIndex(code);
                if (selection < stats.length) {
                    const statSel = stats[selection];
                    const cost = this.costs[selection];
                    return this.trainCallback(statSel, cost);
                }
                return null;
            }
        };
        this.trainCallback = this.trainCallback.bind(this);
    }

    /* Sets the target to train. */
    setTrainer(trainer) {
        this.trainer = trainer;
    }

    /* Sets the target to train. Computes also the training costs based on the
     * stats of the target. */
    setTarget(target) {
        this.target = target;
        this.costs = [];
        stats.forEach(stat => {
            const getFunc = 'get' + stat;
            this.costs.push(100 * target.get('Stats')[getFunc]());
        });
    }

    getSelectionObject() {
        return this.selectionObject;
    }

    trainCallback(statSel, cost) {
        const cb = () => {
            const gw = RG.valueToGoldWeight(cost);
            const taName = this.target.getName();

            if (!RG.hasEnoughGold(this.target, gw)) {
                const msg = `${taName} does not have enough gold.`;
                RG.gameMsg({cell: this.target.getCell(), msg});
                return;
            }
            else {
                RG.tradeGoldWeightFromTo(gw, this.target, this.trainer);
            }

            const targetStats = this.target.get('Stats');
            const trainerStats = this.trainer.get('Stats');
            const getFunc = 'get' + statSel;
            const setFunc = 'set' + statSel;
            const targetVal = targetStats[getFunc]();
            const trainerVal = trainerStats[getFunc]();

            const trName = this.trainer.getName();
            if (targetVal < trainerVal) {
                const newTargetVal = targetVal + 1;
                targetStats[setFunc](newTargetVal);
                const msg = `${trName} trains ${statSel} of ${taName}`;
                RG.gameMsg({cell: this.target.getCell(), msg});
            }
            else {
                const msg = `${trName} doesn't have skill to train that stat`;
                RG.gameMsg({cell: this.target.getCell(), msg});
            }
        };
        return cb;
    }

}
Chat.Trainer = ChatTrainer;

/* Object attached to wizards selling magical services. */
class ChatWizard {

    constructor() {
        this.selectionObject = {
            showMenu: () => true,

            getMenu: () => {
                RG.gameMsg('Please select a magical service to buy:');
                const spellPower = this.wizard.get('SpellPower');
                const ppLeft = spellPower.getPP();
                const menuObj = {};
                if (ppLeft >= 10) {
                    menuObj[0] = 'Restore 10pp - 10 gold coins';
                }
                if (ppLeft >= 50) {
                    menuObj[1] = 'Restore 50pp - 45 gold coins';
                }
                if (ppLeft >= 25) {
                    menuObj[2] = 'Add one charge to rune - 50 gold coins';
                }
            },
            select: code => {
                const selection = Keys.codeToIndex(code);
                console.log(selection);
                return this.wizardCallback();
            }
        };
        this.wizardCallback = this.wizardCallback.bind(this);
    }

    /* Sets the target to train. */
    setWizard(wizard) {
        this.wizard = wizard;
    }

    getSelectionObject() {
        return this.selectionObject;
    }

    wizardCallback(index, cost) {
        const cb = () => {
            if (!RG.hasEnoughGold(this.target, cost)) {
                return;
            }
            switch (index) {
                case 0: this.restorePP(10); break;
                case 1: this.restorePP(50); break;
                case 2: this.setRuneSelectionObject(); break;
                default: break;
            }

        };
        return cb;
    }

    restorePP(numPP) {
        const spellPower = this.wizard.get('SpellPower');
        spellPower.decrPP(numPP);
        const spTarget = this.target.get('SpellPower');
        spTarget.addPP(numPP);
    }

    setRuneSelectionObject() {
        // Create a list of possible runes to charge up
        const selObj = {};

        this.target.setSelectionObject(selObj);
    }

}
Chat.Wizard = ChatWizard;

module.exports = Chat;
<commit_msg>Added support for post/pre in menus.<commit_after>
/* This file contains objecsts for chat interactions between player and NPCs. */

const RG = require('./rg');
const Keys = require('./keymap');
const Menu = require('./menu');

const Chat = {};
const stats = RG.STATS;

/* Chat object added to actors which have any interesting things to chat about.
 */
class ChatBase {

    constructor() {
        this.options = [];
        this.parent = null;
    }

    /* Adds a chat object or an option into this object. */
    add({name, option}) {
        this.options.push({name, option});
        if (option && option.setParent) {
            option.setParent(this);
        }
    }

    clearOptions() {
        this.options = [];
    }

    setParent(parent) {
        this.parent = parent;
    }

    getParent() {
        return this.parent;
    }

    getSelectionObject() {
        const selObj = {
            showMenu: () => true,
            getMenu: () => {
                const menuObj = {};
                this.options.forEach((opt, i) => {
                    menuObj[Keys.menuIndices[i]] = opt.name;
                });
                if (this.pre) {
                    menuObj.pre = this.pre;
                }
                if (this.post) {
                    menuObj.post = this.post;
                }
                return menuObj;
            },
            select: code => {
                const selection = Keys.codeToIndex(code);
                if (selection < this.options.length) {
                    const value = this.options[selection].option;
                    if (value !== Menu.EXIT_MENU) {
                        if (value.getSelectionObject) {
                            return value.getSelectionObject();
                        }
                    }
                    return value;
                }
                return Menu.EXIT_MENU;
            }
        };
        return selObj;
    }

}
Chat.ChatBase = ChatBase;

/* Object used in actors which can give quests. */
class ChatQuest extends ChatBase {

    constructor() {
        super();
        const acceptOpt = {
            name: 'Accept the quest',
            option: this.questCallback.bind(this)
        };
        const refuseOpt = {
            name: 'Refuse the quest',
            option: Menu.EXIT_MENU
        };
        this.add(acceptOpt);
        this.add(refuseOpt);
    }

    setQuestGiver(giver) {
        this.questGiver = giver;
    }

    setTarget(target) {
        const giver = this.questGiver;
        this.target = target;
        const qLen = 'lengthy';
        const questData = this.questGiver.get('QuestGiver').getQuestData();
        if (!this.questGiver.get('QuestGiver').getHasGivenQuest()) {
            this.pre = [
                `${giver.getName()} wants to offer a ${qLen} quest:`,
                `${questData.toString()}`,
                'What do you want to do?'
            ];
        }
        else {
            this.pre = [
                `${giver.getName()} has already given this quest:`,
                `${questData.toString()}`,
                'What do you want to do?'
            ];
            this.clearOptions();
            const rewardOpt = {
                name: 'Claim the reward',
                option: this.rewardCallback.bind(this)
            };
            this.add(rewardOpt);
        }
    }

    questCallback() {
        if (RG.isNullOrUndef([this.target, this.questGiver])) {
            RG.err('ChatQuest', 'questCallback',
                'target and questGiver must be defined');
        }
        const giveQuestComp = new RG.Component.GiveQuest();
        giveQuestComp.setTarget(this.target);
        giveQuestComp.setGiver(this.questGiver);
        this.target.add(giveQuestComp);
    }

    rewardCallback() {
        const questCompl = new RG.Component.QuestCompleted();
        questCompl.setGiver(this.questGiver);
        this.target.add(questCompl);
    }

}
Chat.Quest = ChatQuest;

/* Chat Object for trainers in the game. */
class ChatTrainer {

    constructor() {
        this.selectionObject = {
            showMenu: () => true,
            pre: 'Please select a stat to train:',
            getMenu: () => {
                // RG.gameMsg('');
                const indices = Keys.menuIndices.slice(0, 6);
                const menuObj = {};
                stats.forEach((stat, index) => {
                    menuObj[indices[index]] = stats[index];
                    menuObj[indices[index]] += ` (${this.costs[index]} gold)`;
                });
                return menuObj;

            },
            select: code => {
                const selection = Keys.codeToIndex(code);
                if (selection < stats.length) {
                    const statSel = stats[selection];
                    const cost = this.costs[selection];
                    return this.trainCallback(statSel, cost);
                }
                return null;
            }
        };
        this.trainCallback = this.trainCallback.bind(this);
    }

    /* Sets the target to train. */
    setTrainer(trainer) {
        this.trainer = trainer;
    }

    /* Sets the target to train. Computes also the training costs based on the
     * stats of the target. */
    setTarget(target) {
        this.target = target;
        this.costs = [];
        stats.forEach(stat => {
            const getFunc = 'get' + stat;
            this.costs.push(100 * target.get('Stats')[getFunc]());
        });
    }

    getSelectionObject() {
        return this.selectionObject;
    }

    trainCallback(statSel, cost) {
        const cb = () => {
            const gw = RG.valueToGoldWeight(cost);
            const taName = this.target.getName();

            if (!RG.hasEnoughGold(this.target, gw)) {
                const msg = `${taName} does not have enough gold.`;
                RG.gameMsg({cell: this.target.getCell(), msg});
                return;
            }
            else {
                RG.tradeGoldWeightFromTo(gw, this.target, this.trainer);
            }

            const targetStats = this.target.get('Stats');
            const trainerStats = this.trainer.get('Stats');
            const getFunc = 'get' + statSel;
            const setFunc = 'set' + statSel;
            const targetVal = targetStats[getFunc]();
            const trainerVal = trainerStats[getFunc]();

            const trName = this.trainer.getName();
            if (targetVal < trainerVal) {
                const newTargetVal = targetVal + 1;
                targetStats[setFunc](newTargetVal);
                const msg = `${trName} trains ${statSel} of ${taName}`;
                RG.gameMsg({cell: this.target.getCell(), msg});
            }
            else {
                const msg = `${trName} doesn't have skill to train that stat`;
                RG.gameMsg({cell: this.target.getCell(), msg});
            }
        };
        return cb;
    }

}
Chat.Trainer = ChatTrainer;

/* Object attached to wizards selling magical services. */
class ChatWizard {

    constructor() {
        this.selectionObject = {
            showMenu: () => true,

            getMenu: () => {
                RG.gameMsg('Please select a magical service to buy:');
                const spellPower = this.wizard.get('SpellPower');
                const ppLeft = spellPower.getPP();
                const menuObj = {};
                if (ppLeft >= 10) {
                    menuObj[0] = 'Restore 10pp - 10 gold coins';
                }
                if (ppLeft >= 50) {
                    menuObj[1] = 'Restore 50pp - 45 gold coins';
                }
                if (ppLeft >= 25) {
                    menuObj[2] = 'Add one charge to rune - 50 gold coins';
                }
            },
            select: code => {
                const selection = Keys.codeToIndex(code);
                console.log(selection);
                return this.wizardCallback();
            }
        };
        this.wizardCallback = this.wizardCallback.bind(this);
    }

    /* Sets the target to train. */
    setWizard(wizard) {
        this.wizard = wizard;
    }

    getSelectionObject() {
        return this.selectionObject;
    }

    wizardCallback(index, cost) {
        const cb = () => {
            if (!RG.hasEnoughGold(this.target, cost)) {
                return;
            }
            switch (index) {
                case 0: this.restorePP(10); break;
                case 1: this.restorePP(50); break;
                case 2: this.setRuneSelectionObject(); break;
                default: break;
            }

        };
        return cb;
    }

    restorePP(numPP) {
        const spellPower = this.wizard.get('SpellPower');
        spellPower.decrPP(numPP);
        const spTarget = this.target.get('SpellPower');
        spTarget.addPP(numPP);
    }

    setRuneSelectionObject() {
        // Create a list of possible runes to charge up
        const selObj = {};

        this.target.setSelectionObject(selObj);
    }

}
Chat.Wizard = ChatWizard;

module.exports = Chat;
<|endoftext|>


<commit_before>"use strict";

const fs = require('fs');

const debug = require('debug')('cardinal:queue');
const redis = require('redis');
const ytdl = require('ytdl-core');

const Actions = require('./actions');
const Dispatcher = require('./dispatcher');

const Types = require('./queue/types');
const Utils = require('./queue/utils');
const QueuedMedia = require('./queue/queued-media');

const client = require('./bot').client;

const redisClient = redis.createClient();

class MusicPlayer {
  constructor() {
    this.queue = new Set();
    this.currentlyPlaying = null;
    this.voiceConnection = null;

    this.QueuedMedia = QueuedMedia;

    Dispatcher.on(Actions.QUEUE_DISPLAY_NOW_PLAYING, this.onNowPlaying.bind(this));
    Dispatcher.on(Actions.QUEUE_DISPLAY_PLAYLIST, this.onDisplayPlaylist.bind(this));
    Dispatcher.on(Actions.QUEUE_ITEM, this.queueItem.bind(this));
    Dispatcher.on(Actions.QUEUE_SKIP, this.skipSong.bind(this));
    Dispatcher.on(Actions.QUEUE_DONE_ITEM, this.queuedDonePlaying.bind(this));
  }

  onNowPlaying(m) {
    debug('QUEUE_DISPLAY_NOW_PLAYING');
    if (this.currentlyPlaying === null) {
      m.channel.sendMessage('No queued song');
      return;
    }
    m.channel.sendMessage(this.currentlyPlaying.printString());
  }

  onDisplayPlaylist(m) {
    debug('QUEUE_DISPLAY_PLAYLIST');
    const key = `cardinal.${m.guild.id}:music_queue`;
    let msg = '';

    redisClient.llen(key, (err, len) => {
      if (err) {
        debug('error reading list length from redis', err, err.stack);
        m.channel.sendMessage('Error reading queue list from Redis');
        return;
      }

      if (this.currentlyPlaying) {
        msg += 'Currently Playing:\n';
        msg += this.currentlyPlaying.printString();
        msg += '\n\n';
      }

      msg += 'Playlist:\n';
      if (!len) {
        msg += '- Nothing!\n';
        m.channel.sendMessage(msg);
      } else {
        redisClient.lrange(key, 0, len, (err, list) => {
          if (err) {
            debug('error redis lrange', err, err.stack);
            m.channel.sendMessage('Error getting queue playlist from Redis');
            return;
          }

          for (const item of list) {
            // TODO: cache these values earlier
            const a = new QueuedMedia(this, JSON.parse(item));
            msg += `${a.printString()}\n`;
          }

          m.channel.sendMessage(msg);
        });
      }
    });
  }

  fetchYoutubeInfo(url) {
    return new Promise((resolve, reject) => {
      ytdl.getInfo(url, { filter: 'audioonly' }, (err, info) => {
        if (err) {
          reject(err);
          return;
        }

        const formats = info.formats
          .filter(x => x.audioEncoding)
          .sort(Utils.sortFormats)
          .map(x => ({
            container: x.container,
            url: x.url,
            audioEncoding: x.audioEncoding,
            audioBitrate: x.audioBitrate,
          }));

        resolve([info, formats]);
      });
    });
  }

  queueItem(m, url) {
    debug('QUEUE_ITEM', url);
    if (!url) {
      debug('no valid url');
      m.channel.sendMessage(`${m.author.mention} Please give me a URL to play`);
      return;
    }

    this.fetchYoutubeInfo(url).then((arr) => {
      debug('fetchYoutubeInfo promise resolve');

      const fields = ['title', 'video_id', 'length_seconds'];
      const info = fields.reduce((obj, field) => {
        obj[field] = arr[0][field];
        return obj;
      }, {});
      const formats = arr[1];

      const record = {
        type: Types.YTDL,
        ownerId: m.author.id,
        guildId: m.guild.id,
        info,
        formats,
      };

      redisClient.rpush(`cardinal.${m.guild.id}:music_queue`, JSON.stringify(record), (err) => {
        if (err) {
          debug('error saving to redis', err);
          m.channel.sendMessage('An error occurred saving the queue request to Redis');
          return;
        }

        debug('saved to redis');

        this.handleQueued(m.guild, m.author, m.channel).then((ok) => {
          if (ok) m.channel.sendMessage(`Added ${queued.printString()}`);
        });
      });
    }).catch((err) => {
      if (err) {
        debug('error pulling youtube data', err.stack);
      }

      fs.access(url, fs.R_OK, (err2) => {
        debug('file access', err2);

        if (!err2) {
          const info = {
            title: 'ffmpeg',
            format: 'ffmpeg',
            encoding: 'ffmpeg',
            url,
          };

          const record = {
            type: Types.LOCAL,
            ownerId: m.author.id,
            guildId: m.guild.id,
            info,
          };

          redisClient.rpush(`cardinal.${m.guild.id}:music_queue`, JSON.stringify(record), (err) => {
            if (err) {
              debug('error saving to redis', err, err.stack);
              m.channel.sendMessage('An error occurred saving the queue request to Redis');
              return;
            }

            debug('saved to redis');

            this.handleQueued(m.guild, m.author, m.channel).then((ok) => {
              if (ok) m.channel.sendMessage(`Added ${queued.printString()}`);
            });
          });
        }
      });
    });
  }

  skipSong(m) {
    if (!this.currentlyPlaying) {
      m.channel.sendMessage('No currently playing song');
      return;
    } else if (!this.currentlyPlaying.stream) {
      m.channel.sendMessage('For some reason this song does not have a stream associated with it');
      return;
    }

    this.currentlyPlaying.stopPlaying();
    this.currentlyPlaying = null;
    this.handleQueued(m.guild);
  }

  playNext(guild, author, channel) {
    debug('playNext');

    const authorVoiceChannel = (guild && author) ? author.getVoiceChannel(guild) : null;

    return new Promise((resolve, reject) => {
      redisClient.lpop(`cardinal.${guild.id}:music_queue`, (err, reply) => {
        if (err) {
          debug('error getting next item from redis', err.stack);
          return;
        }

        let record = null;
        try {
          record = JSON.parse(reply);
        } catch (err) {
          debug('error parsing Redis response', err, reply);
          return;
        }

        const next = new QueuedMedia(this, record || {});
        this.currentlyPlaying = next;

        if (!authorVoiceChannel && channel) {
          channel.sendMessage(`${author.mention} Can you please join a voice channel I can play to?`);

          this.queuedDonePlaying(this.currentlyPlaying);

          return resolve(false);
        } else if (this.voiceConnection && !this.voiceConnection.disposed && this.voiceConnection.canStream) {
          this.currentlyPlaying.play(this.voiceConnection);
        } else {
          const promise = authorVoiceChannel.join(false, false).then((info) => {
            debug(`joined voice chat: ${info.voiceSocket.voiceServerURL}@${info.voiceSocket.mode}`);

            this.voiceConnection = info.voiceConnection;

            this.currentlyPlaying.play(this.voiceConnection);

            return true;
          }).catch((err) => {
            debug('failed to join voice chat', err, err.stack);

            if (err.message === 'Missing permission' && authorVoiceChannel) {
              channel.sendMessage(`${author.mention} I do not have permission to join the '${authorVoiceChannel.name}' voice channel`);
            }

            this.queuedDonePlaying(this.currentlyPlaying);

            return false;
          });

          resolve(promise);
        }

        return resolve(true);
      });
    });
  }

  handleQueued(guild, author, channel) {
    debug('handleQueued');

    if (!guild) {
      debug('no guild provided');
      return;
    }

    const key = `cardinal.${guild.id}:music_queue`;
    const deferred = Promise.defer();

    redisClient.llen(key, (err, len) => {
      debug(`redis ${key}`, len, err);

      if (this.currentlyPlaying === null && len > 0) {
        deferred.resolve(this.playNext(guild, author, channel));
        return;
      } else if (len === 0 && this.voiceConnection && !this.voiceConnection.disposed) {
        debug('handleQueued disconnect');

        this.voiceConnection.disconnect();
        this.voiceConnection = null;
      }

      deferred.resolve(true);
    });

    return deferred.promise;
  }

  queuedDonePlaying(queued) {
    if (queued === this.currentlyPlaying) {
      const guild = client.Guilds.get(queued.guildId);

      this.currentlyPlaying = null;
      this.handleQueued(guild);
    }
  }
}

module.exports = new MusicPlayer();
<commit_msg>queue: remove unneeded property<commit_after>"use strict";

const fs = require('fs');

const debug = require('debug')('cardinal:queue');
const redis = require('redis');
const ytdl = require('ytdl-core');

const Actions = require('./actions');
const Dispatcher = require('./dispatcher');

const Types = require('./queue/types');
const Utils = require('./queue/utils');
const QueuedMedia = require('./queue/queued-media');

const client = require('./bot').client;

const redisClient = redis.createClient();

class MusicPlayer {
  constructor() {
    this.currentlyPlaying = null;
    this.voiceConnection = null;

    this.QueuedMedia = QueuedMedia;

    Dispatcher.on(Actions.QUEUE_DISPLAY_NOW_PLAYING, this.onNowPlaying.bind(this));
    Dispatcher.on(Actions.QUEUE_DISPLAY_PLAYLIST, this.onDisplayPlaylist.bind(this));
    Dispatcher.on(Actions.QUEUE_ITEM, this.queueItem.bind(this));
    Dispatcher.on(Actions.QUEUE_SKIP, this.skipSong.bind(this));
    Dispatcher.on(Actions.QUEUE_DONE_ITEM, this.queuedDonePlaying.bind(this));
  }

  onNowPlaying(m) {
    debug('QUEUE_DISPLAY_NOW_PLAYING');
    if (this.currentlyPlaying === null) {
      m.channel.sendMessage('No queued song');
      return;
    }
    m.channel.sendMessage(this.currentlyPlaying.printString());
  }

  onDisplayPlaylist(m) {
    debug('QUEUE_DISPLAY_PLAYLIST');
    const key = `cardinal.${m.guild.id}:music_queue`;
    let msg = '';

    redisClient.llen(key, (err, len) => {
      if (err) {
        debug('error reading list length from redis', err, err.stack);
        m.channel.sendMessage('Error reading queue list from Redis');
        return;
      }

      if (this.currentlyPlaying) {
        msg += 'Currently Playing:\n';
        msg += this.currentlyPlaying.printString();
        msg += '\n\n';
      }

      msg += 'Playlist:\n';
      if (!len) {
        msg += '- Nothing!\n';
        m.channel.sendMessage(msg);
      } else {
        redisClient.lrange(key, 0, len, (err, list) => {
          if (err) {
            debug('error redis lrange', err, err.stack);
            m.channel.sendMessage('Error getting queue playlist from Redis');
            return;
          }

          for (const item of list) {
            // TODO: cache these values earlier
            const a = new QueuedMedia(this, JSON.parse(item));
            msg += `${a.printString()}\n`;
          }

          m.channel.sendMessage(msg);
        });
      }
    });
  }

  fetchYoutubeInfo(url) {
    return new Promise((resolve, reject) => {
      ytdl.getInfo(url, { filter: 'audioonly' }, (err, info) => {
        if (err) {
          reject(err);
          return;
        }

        const formats = info.formats
          .filter(x => x.audioEncoding)
          .sort(Utils.sortFormats)
          .map(x => ({
            container: x.container,
            url: x.url,
            audioEncoding: x.audioEncoding,
            audioBitrate: x.audioBitrate,
          }));

        resolve([info, formats]);
      });
    });
  }

  queueItem(m, url) {
    debug('QUEUE_ITEM', url);
    if (!url) {
      debug('no valid url');
      m.channel.sendMessage(`${m.author.mention} Please give me a URL to play`);
      return;
    }

    this.fetchYoutubeInfo(url).then((arr) => {
      debug('fetchYoutubeInfo promise resolve');

      const fields = ['title', 'video_id', 'length_seconds'];
      const info = fields.reduce((obj, field) => {
        obj[field] = arr[0][field];
        return obj;
      }, {});
      const formats = arr[1];

      const record = {
        type: Types.YTDL,
        ownerId: m.author.id,
        guildId: m.guild.id,
        info,
        formats,
      };

      redisClient.rpush(`cardinal.${m.guild.id}:music_queue`, JSON.stringify(record), (err) => {
        if (err) {
          debug('error saving to redis', err);
          m.channel.sendMessage('An error occurred saving the queue request to Redis');
          return;
        }

        debug('saved to redis');

        this.handleQueued(m.guild, m.author, m.channel).then((ok) => {
          if (ok) m.channel.sendMessage(`Added ${queued.printString()}`);
        });
      });
    }).catch((err) => {
      if (err) {
        debug('error pulling youtube data', err.stack);
      }

      fs.access(url, fs.R_OK, (err2) => {
        debug('file access', err2);

        if (!err2) {
          const info = {
            title: 'ffmpeg',
            format: 'ffmpeg',
            encoding: 'ffmpeg',
            url,
          };

          const record = {
            type: Types.LOCAL,
            ownerId: m.author.id,
            guildId: m.guild.id,
            info,
          };

          redisClient.rpush(`cardinal.${m.guild.id}:music_queue`, JSON.stringify(record), (err) => {
            if (err) {
              debug('error saving to redis', err, err.stack);
              m.channel.sendMessage('An error occurred saving the queue request to Redis');
              return;
            }

            debug('saved to redis');

            this.handleQueued(m.guild, m.author, m.channel).then((ok) => {
              if (ok) m.channel.sendMessage(`Added ${queued.printString()}`);
            });
          });
        }
      });
    });
  }

  skipSong(m) {
    if (!this.currentlyPlaying) {
      m.channel.sendMessage('No currently playing song');
      return;
    } else if (!this.currentlyPlaying.stream) {
      m.channel.sendMessage('For some reason this song does not have a stream associated with it');
      return;
    }

    this.currentlyPlaying.stopPlaying();
    this.currentlyPlaying = null;
    this.handleQueued(m.guild);
  }

  playNext(guild, author, channel) {
    debug('playNext');

    const authorVoiceChannel = (guild && author) ? author.getVoiceChannel(guild) : null;

    return new Promise((resolve, reject) => {
      redisClient.lpop(`cardinal.${guild.id}:music_queue`, (err, reply) => {
        if (err) {
          debug('error getting next item from redis', err.stack);
          return;
        }

        let record = null;
        try {
          record = JSON.parse(reply);
        } catch (err) {
          debug('error parsing Redis response', err, reply);
          return;
        }

        const next = new QueuedMedia(this, record || {});
        this.currentlyPlaying = next;

        if (!authorVoiceChannel && channel) {
          channel.sendMessage(`${author.mention} Can you please join a voice channel I can play to?`);

          this.queuedDonePlaying(this.currentlyPlaying);

          return resolve(false);
        } else if (this.voiceConnection && !this.voiceConnection.disposed && this.voiceConnection.canStream) {
          this.currentlyPlaying.play(this.voiceConnection);
        } else {
          const promise = authorVoiceChannel.join(false, false).then((info) => {
            debug(`joined voice chat: ${info.voiceSocket.voiceServerURL}@${info.voiceSocket.mode}`);

            this.voiceConnection = info.voiceConnection;

            this.currentlyPlaying.play(this.voiceConnection);

            return true;
          }).catch((err) => {
            debug('failed to join voice chat', err, err.stack);

            if (err.message === 'Missing permission' && authorVoiceChannel) {
              channel.sendMessage(`${author.mention} I do not have permission to join the '${authorVoiceChannel.name}' voice channel`);
            }

            this.queuedDonePlaying(this.currentlyPlaying);

            return false;
          });

          resolve(promise);
        }

        return resolve(true);
      });
    });
  }

  handleQueued(guild, author, channel) {
    debug('handleQueued');

    if (!guild) {
      debug('no guild provided');
      return;
    }

    const key = `cardinal.${guild.id}:music_queue`;
    const deferred = Promise.defer();

    redisClient.llen(key, (err, len) => {
      debug(`redis ${key}`, len, err);

      if (this.currentlyPlaying === null && len > 0) {
        deferred.resolve(this.playNext(guild, author, channel));
        return;
      } else if (len === 0 && this.voiceConnection && !this.voiceConnection.disposed) {
        debug('handleQueued disconnect');

        this.voiceConnection.disconnect();
        this.voiceConnection = null;
      }

      deferred.resolve(true);
    });

    return deferred.promise;
  }

  queuedDonePlaying(queued) {
    if (queued === this.currentlyPlaying) {
      const guild = client.Guilds.get(queued.guildId);

      this.currentlyPlaying = null;
      this.handleQueued(guild);
    }
  }
}

module.exports = new MusicPlayer();
<|endoftext|>


<commit_before>(function() {

  // Global object reference.
  var root = this;

  // Global export.
  var Quilt = root.Quilt = {};

  // Current library version.
  Quilt.VERSION = '0.0.1';

  // Find dashes in attribute names.
  var undasher = /-([a-z]|[0-9])/ig;

  // Camel case data attributes.
  var camel = function(match, letter) {
    return (letter + '').toUpperCase();
  };

  // # Quilt.View
  // Provide a structure for declaring functionality through data attributes.
  var View = Quilt.View = Backbone.View.extend({

    constructor: function(options) {
      this.views = [];
      if (options && options.template) this.template = options.template;
      Backbone.View.apply(this, arguments);
    },

    // After executing the template function, search the view for relevant
    // attributes, match them with handlers and execute them.  If a handler
    // returns a view, store it for clean up.
    render: function() {
      var el, view, name, attr;

      // Destroy old views.
      while (view = this.views.pop()) if (view.destroy) view.destroy();

      // Render the template if it exists.
      if (this.template) {
        this.$el.html(this.template({
          view: this,
          model: this.model,
          collection: this.collection
        }));
      }

      var elements = this.$('*').get();

      // Execute the handler for each element/attr pair.
      while (el = elements.pop()) {

        var attrs = el.attributes;
        for (var i = 0; i < attrs.length; i++) {

          // Bail unless we have a data attribute.
          if ((name = attrs[i].name).indexOf('data-') !== 0) continue;

          // Camel case and strip "data-".
          name = name.replace(/^data-/, '').replace(undasher, camel);

          // Bail on non-quilt attributes.
          if (!(attr = Quilt.attributes[name])) continue;

          // Execute the handler.
          view = attr.call(this, el, $(el).data(name));

          // Render the view if appropriate.
          if (view instanceof View) this.views.push(view.render());
        }
      }

      return this;
    },

    // Destroy child views and ensure that references to this view are
    // eliminated to prevent memory leaks.
    destroy: function() {

      // Remove DOM listeners.
      this.undelegateEvents();

      // Destroy child views.
      _.invoke(this.views, 'destroy');

      // Clean up event handlers.
      if (this.model) this.model.off(null, null, this);
      if (this.collection) this.collection.off(null, null, this);

      return this;
    }

  });

  // Attribute handlers should be specified in camel case.  The arguments to
  // each handler will be a DOM element and the value of the data attribute.
  // The handler will be called with the parent view as context.
  //
  //    Quilt.attributes.exampleAttr = function(element, options) {
  //      // Called for elements with a "data-example-attr" attribute.
  //    };
  //
  Quilt.attributes = {};

})();
<commit_msg>Style.<commit_after>(function() {

  // Global object reference.
  var root = this;

  // Global export.
  var Quilt = root.Quilt = {};

  // Current library version.
  Quilt.VERSION = '0.0.1';

  // Find dashes in attribute names.
  var undasher = /-([a-z]|[0-9])/ig;

  // Camel case data attributes.
  var camel = function(match, letter) {
    return (letter + '').toUpperCase();
  };

  // # Quilt.View
  // Provide a structure for declaring functionality through data attributes.
  var View = Quilt.View = Backbone.View.extend({

    constructor: function(options) {
      this.views = [];
      if (options && options.template) this.template = options.template;
      Backbone.View.apply(this, arguments);
    },

    // After executing the template function, search the view for relevant
    // attributes, match them with handlers and execute them.  If a handler
    // returns a view, store it for clean up.
    render: function() {
      var elements, el, view, name, attrs, attr;

      // Destroy old views.
      while (view = this.views.pop()) if (view.destroy) view.destroy();

      // Render the template if it exists.
      if (this.template) {
        this.$el.html(this.template({
          view: this,
          model: this.model,
          collection: this.collection
        }));
      }

      elements = this.$('*').get();

      // Execute the handler for each element/attr pair.
      while (el = elements.pop()) {

        attrs = el.attributes;
        for (var i = 0; i < attrs.length; i++) {

          // Bail unless we have a data attribute.
          if ((name = attrs[i].name).indexOf('data-') !== 0) continue;

          // Camel case and strip "data-".
          name = name.replace(/^data-/, '').replace(undasher, camel);

          // Bail on non-quilt attributes.
          if (!(attr = Quilt.attributes[name])) continue;

          // Execute the handler.
          view = attr.call(this, el, $(el).data(name));

          // Render the view if appropriate.
          if (view instanceof View) this.views.push(view.render());
        }
      }

      return this;
    },

    // Destroy child views and ensure that references to this view are
    // eliminated to prevent memory leaks.
    destroy: function() {

      // Remove DOM listeners.
      this.undelegateEvents();

      // Destroy child views.
      _.invoke(this.views, 'destroy');

      // Clean up event handlers.
      if (this.model) this.model.off(null, null, this);
      if (this.collection) this.collection.off(null, null, this);

      return this;
    }

  });

  // Attribute handlers should be specified in camel case.  The arguments to
  // each handler will be a DOM element and the value of the data attribute.
  // The handler will be called with the parent view as context.
  //
  //    Quilt.attributes.exampleAttr = function(element, options) {
  //      // Called for elements with a "data-example-attr" attribute.
  //    };
  //
  Quilt.attributes = {};

})();
<|endoftext|>


<commit_before>cbus.audio = {
  DEFAULT_JUMP_AMOUNT_BACKWARD: -10,
  DEFAULT_JUMP_AMOUNT_FORWARD: 30,

  PLAYBACK_RATE_MIN: 0.5,
  PLAYBACK_RATE_MAX: 4,

  element: null,
  videoCanvasAnimationFrameID: null,
  state: {},

  tryRestoreProgress: function() {
    let episodeID = cbus.audio.element.dataset.id;
    if (cbus.data.episodeProgresses.hasOwnProperty(episodeID)) {
      let time;
      if (cbus.data.episodeCompletedStatuses[episodeID] === true) {
        time = 0;
      } else {
        time = Math.max(cbus.data.episodeProgresses[episodeID] - 5, 0);
      }
      cbus.audio.element.currentTime = time;
    }
  },

  setElement: function(elem, disableAutomaticProgressRestore) {
    var prevPlaybackRate;
    var prevVolume;

    if (cbus.audio.element) {
      cbus.audio.pause();
      cbus.audio.element.oncanplay = null;
      cbus.audio.element.onseeking = null;
      cbus.audio.element.onseeked = null;
      cbus.audio.element.onloadedmetadata = null;
      cbus.audio.element.onended = null;

      prevPlaybackRate = cbus.audio.element.playbackRate;
      prevVolume = cbus.audio.element.volume;
    }

    cbus.audio.element = elem;

    if (prevPlaybackRate) {
      cbus.audio.element.playbackRate = prevPlaybackRate;
      cbus.audio.element.volume = prevVolume
    }

    if (disableAutomaticProgressRestore === true) {
      cbus.audio.element.currentTime = 0;
    } else {
      cbus.audio.tryRestoreProgress();
    }

    cbus.audio.element.onerror = function() {
      if (this.error === MediaError.MEDIA_ERR_ABORTED) {
        // cbus.ui.showSnackbar("The user canceled the audio.", "error");
      } else if (this.error === MediaError.MEDIA_ERR_NETWORK) {
        cbus.ui.showSnackbar(i18n.__("snackbar_error-media-network"), "error");
      } else if (this.error === MediaError.MEDIA_ERR_DECODE) {
        cbus.ui.showSnackbar(i18n.__("snackbar_error-media-decode"), "error");
      } else if (this.error === MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED) {
        cbus.ui.showSnackbar(i18n.__("snackbar_error-media-unsupported"), "error");
      } else {
        cbus.ui.showSnackbar(i18n.__("snackbar_error-media-unknown"), "error");
      }
    };

    cbus.audio.element.oncanplay = function() {
      cbus.broadcast.send("audioBufferEnd");
    };
    cbus.audio.element.onseeking = function() {
      cbus.broadcast.send("audioBufferStart");
    };
    cbus.audio.element.onseeked = function() {
      cbus.broadcast.send("audioBufferEnd");
      cbus.audio.updatePlayerTime();
      if (cbus.audio.mprisPlayer) {
        cbus.audio.mprisPlayer.interfaces.player.emitSignal("Seeked", cbus.audio.element.currentTime * 1e6);
      }
    };
    cbus.audio.element.onloadedmetadata = function() {
      if (disableAutomaticProgressRestore === true) {
        cbus.audio.element.currentTime = 0;
      } else {
        cbus.audio.tryRestoreProgress();
      }
      cbus.audio.updatePlayerTime();
    };
    cbus.audio.element.onended = function() {
      if (cbus.audio.queue.length && cbus.audio.queue[0].src !== cbus.audio.element.src) {
        cbus.audio.playQueueItem(0);
      } else {
        // up next
        if (existsRecursive(cbus.data.state, ["upNext", "source"])) {
          let upNextState = cbus.data.state.upNext;
          let currentEpisode = cbus.data.getEpisodeData({ audioElement: cbus.audio.element });
          let currentPodcast = cbus.data.getFeedData({ url: currentEpisode.feedURL });
          if (upNextState.source === "home") {
            var currentIndex = -1;
            let l = cbus.data.episodes.length;
            for (let i = 0; i < l; i++) {
              if (
                currentIndex === -1 &&
                cbus.data.episodes[i].url === currentEpisode.url &&
                cbus.data.episodes[i].feedURL === currentEpisode.feedURL
              ) {
                currentIndex = i;
                break;
              }
            }
            for (let i = currentIndex + 1; i < l; i++) {
              if (!cbus.data.getEpisodeProgress(cbus.data.episodes[i].url).completed) {
                cbus.audio.setElement(getElem(".audios").querySelector(`[data-id="${cbus.data.episodes[i].url}"]`));
                cbus.audio.play();
                break;
              }
            }
          } else if (upNextState.source === "podcast") {
            // TODO
          }
        }
      }
    };

    cbus.audio.state.episode = cbus.data.getEpisodeData({
      audioElement: elem
    });
    cbus.audio.state.episode.urlSha1 = sha1(cbus.audio.state.episode.url);
    cbus.audio.state.feed = cbus.data.getFeedData({
      url: cbus.audio.state.episode.feedURL
    });

    if (cbus.audio.state.episode.isVideo) {
      let draw = function() {
        cbus.ui.videoCanvasContext.drawImage(
          cbus.audio.element,
          0, 0,
          cbus.ui.videoCanvasElement.width, cbus.ui.videoCanvasElement.height
        );
        cbus.audio.videoCanvasAnimationFrameID = requestAnimationFrame(draw);
      }
      cbus.audio.videoCanvasAnimationFrameID = requestAnimationFrame(draw);
    } else if (cbus.audio.videoCanvasAnimationFrameID) {
      cancelAnimationFrame(cbus.audio.videoCanvasAnimationFrameID);
    }

    cbus.broadcast.send("audioChange", cbus.audio.state.episode);
    cbus.ui.updateThumbarButtons();

    localforage.setItem("cbus-last-audio-url", elem.dataset.id);

    cbus.audio.updatePlayerTime();
  },

  updatePlayerTime: function(options) {
    if (cbus.audio.element && !cbus.audio.element.paused) {
      cbus.broadcast.send("audioTick", {
        currentTime: cbus.audio.element.currentTime,
        duration: cbus.audio.element.duration
      });
      if (cbus.audio.mprisPlayer) {
        let trackID = cbus.audio.mprisPlayer.objectPath("track/" + cbus.audio.state.episode.urlSha1);
        if (
          !Number.isNaN(cbus.audio.element.duration) &&
          (
            !cbus.audio.mprisPlayer.metadata ||
            cbus.audio.mprisPlayer.metadata["mpris:trackid"] !== trackID
          )
        ) {
          if (cbus.audio.state.feed.image instanceof Blob && cbus.audio.state.updatingArtFor !== cbus.audio.state.feed.url) {
            cbus.audio.state.updatingArtFor = cbus.audio.state.feed.url;
            cbus.broadcast.send("updateFeedArtworks", {
              feedUrl: cbus.audio.state.feed.url,
              callback: function(updatedFeeds) {
                if (updatedFeeds[0].url === cbus.audio.state.feed.url) {
                  cbus.audio.state.feed.image = cbus.const.IMAGE_ON_DISK_PLACEHOLDER;
                  cbus.audio.mprisSetMetadata(trackID);
                  cbus.audio.state.updatingArtFor = null;
                }
              }
            });
          } else if (cbus.audio.state.updatingArtFor !== cbus.audio.state.feed.url) {
            cbus.audio.mprisSetMetadata(trackID);
          }
        }
      }
    }
  },
  sliderUpdateInterval: null,

  playQueueItem: function(index) {
    if (cbus.audio.queue[index]) {
      cbus.audio.setElement(cbus.audio.queue[index]);
      cbus.audio.removeQueueItem(index);
      cbus.audio.play();

      cbus.broadcast.send("queueChanged");

      return true;
    } else {
      return false;
    }
  },

  removeQueueItem: function(index) {
    if (cbus.audio.queue[index]) {
      cbus.audio.queue.splice(index, 1);
      $(".list--queue .episode").eq(index).remove();
      cbus.broadcast.send("queueChanged");
    }
  },

  play: function() {
    cbus.audio.element.play();
    $(".player_button--play").html("pause");
    cbus.broadcast.send("audio-play");
    cbus.broadcast.send("audioBufferStart");
    if (cbus.audio.mprisPlayer) {
      cbus.audio.mprisPlayer.playbackStatus = "Playing";
    }
    cbus.ui.updateThumbarButtons();
  },
  pause: function() {
    cbus.audio.element.pause();
    $(".player_button--play").html("play_arrow");
    cbus.broadcast.send("audio-pause");
    if (cbus.audio.mprisPlayer) {
      cbus.audio.mprisPlayer.playbackStatus = "Paused";
    }
    cbus.ui.updateThumbarButtons();
  },
  playpause: function() {
    if (cbus.audio.element.paused) {
      cbus.audio.play();
    } else {
      cbus.audio.pause();
    }
  },
  stop: function() {
    cbus.audio.element.pause();
    cbus.audio.element.currentTime = 0;
    cbus.broadcast.send("audio-stop");
    if (cbus.audio.mprisPlayer) {
      cbus.audio.mprisPlayer.playbackStatus = "Stopped";
    }
    cbus.ui.updateThumbarButtons();
  },
  jump: function(amount) {
    cbus.audio.element.currentTime += amount;
  },

  setPlaybackRate: function(rate) {
    cbus.audio.element.playbackRate = rate ? clamp(rate, cbus.audio.PLAYBACK_RATE_MIN, cbus.audio.PLAYBACK_RATE_MAX) : 1;
    if (cbus.audio.mprisPlayer) {
      cbus.audio.mprisPlayer.rate = cbus.audio.element.playbackRate;
    }
    cbus.broadcast.send("playbackRateChanged", cbus.audio.element.playbackRate);
  },
  setVolume: function(volume) {
    cbus.audio.element.volume = volume / 100;

    // TODO send broadcast after adding keyboard shortcut
  },

  queue: [],
  enqueue: function(audioElement, hiddenEnqueue) {
    cbus.audio.queue.push(audioElement);

    let episodeData = cbus.data.getEpisodeData({
      audioElement: audioElement
    });

    cbus.broadcast.send("episodeEnqueue", {
      episodeData: episodeData,
      hiddenEnqueue: (hiddenEnqueue === true ? true : false)
    });
    cbus.broadcast.send("queueChanged");
    cbus.ui.updateThumbarButtons();

    if (!hiddenEnqueue && cbus.settings.data.queueAutoDownload) {
      if (cbus.data.episodesOffline.indexOf(episodeData.id) === -1) {
        cbus.data.downloadEpisode(audioElement);
      }
    }
  },

  mprisPlayer: null,
  mprisSetMetadata: function(trackID) {
    cbus.audio.mprisPlayer.metadata = {
      "mpris:trackid": trackID,
      "mpris:length": cbus.audio.element.duration * 1000000,
      "mpris:artUrl": cbus.data.getPodcastImageURI(cbus.audio.state.feed),
      "xesam:title": cbus.audio.state.episode.title,
      "xesam:album": cbus.audio.state.feed.title,
      "xesam:artist": [ cbus.audio.state.feed.title ]
    };
    cbus.audio.mprisPlayer.position = cbus.audio.element.currentTime * 1000000;
    console.log("metadata set", cbus.audio.mprisPlayer.metadata)
  }
};

cbus.audio.sliderUpdateInterval = setInterval(cbus.audio.updatePlayerTime, 500);

if (MPRISPlayer) {
  cbus.audio.mprisPlayer = MPRISPlayer({
    name: "cumulonimbus",
    identity: "CPod",
    supportedUriSchemes: [ "file", "http", "https" ],
    supportedMimeTypes: [ "audio/*" ],
    supportedInterfaces: [ "player" ]
  });

  cbus.audio.mprisPlayer.canGoNext = false;
  cbus.audio.mprisPlayer.canGoPrevious = false;
  cbus.audio.mprisPlayer.playbackStatus = "Paused";
  cbus.audio.mprisPlayer.minimumRate = cbus.audio.PLAYBACK_RATE_MIN;
  cbus.audio.mprisPlayer.maximumRate = cbus.audio.PLAYBACK_RATE_MAX;

  cbus.audio.mprisPlayer.on("play", () => {
    cbus.audio.play();
  });
  cbus.audio.mprisPlayer.on("pause", () => {
    cbus.audio.pause();
  });
  cbus.audio.mprisPlayer.on("stop", () => {
    cbus.audio.pause();
  });
  cbus.audio.mprisPlayer.on("playpause", () => {
    if (cbus.audio.element.paused) {
      cbus.audio.play();
    } else {
      cbus.audio.pause();
    }
  });
  cbus.audio.mprisPlayer.on("next", () => {
    cbus.audio.playQueueItem(0);
  });
  cbus.audio.mprisPlayer.on("seek", (data) => {
    if (cbus.audio.element) {
      cbus.audio.element.currentTime = data.position / 1000000;
    }
  });
  cbus.audio.mprisPlayer.on("position", (data) => {
    if (cbus.audio.element) {
      cbus.audio.element.currentTime = data.position / 1000000;
    }
  });
  cbus.audio.mprisPlayer.on("volume", (data) => {
    if (cbus.audio.element) {
      cbus.audio.element.volume = clamp(data.volume, 0, 1);
    }
  });
  cbus.audio.mprisPlayer.on("rate", (data) => {
    if (cbus.audio.element) {
      cbus.audio.setPlaybackRate(data.rate);
    }
  });

  cbus.broadcast.listen("queueChanged", () => {
    if (cbus.audio.queue.length) {
      cbus.audio.mprisPlayer.canGoNext = true;
    } else {
      cbus.audio.mprisPlayer.canGoNext = false;
    }
  });
}
<commit_msg>add mprisPlayer check<commit_after>cbus.audio = {
  DEFAULT_JUMP_AMOUNT_BACKWARD: -10,
  DEFAULT_JUMP_AMOUNT_FORWARD: 30,

  PLAYBACK_RATE_MIN: 0.5,
  PLAYBACK_RATE_MAX: 4,

  element: null,
  videoCanvasAnimationFrameID: null,
  state: {},

  tryRestoreProgress: function() {
    let episodeID = cbus.audio.element.dataset.id;
    if (cbus.data.episodeProgresses.hasOwnProperty(episodeID)) {
      let time;
      if (cbus.data.episodeCompletedStatuses[episodeID] === true) {
        time = 0;
      } else {
        time = Math.max(cbus.data.episodeProgresses[episodeID] - 5, 0);
      }
      cbus.audio.element.currentTime = time;
    }
  },

  setElement: function(elem, disableAutomaticProgressRestore) {
    var prevPlaybackRate;
    var prevVolume;

    if (cbus.audio.element) {
      cbus.audio.pause();
      cbus.audio.element.oncanplay = null;
      cbus.audio.element.onseeking = null;
      cbus.audio.element.onseeked = null;
      cbus.audio.element.onloadedmetadata = null;
      cbus.audio.element.onended = null;

      prevPlaybackRate = cbus.audio.element.playbackRate;
      prevVolume = cbus.audio.element.volume;
    }

    cbus.audio.element = elem;

    if (prevPlaybackRate) {
      cbus.audio.element.playbackRate = prevPlaybackRate;
      cbus.audio.element.volume = prevVolume
    }

    if (disableAutomaticProgressRestore === true) {
      cbus.audio.element.currentTime = 0;
    } else {
      cbus.audio.tryRestoreProgress();
    }

    cbus.audio.element.onerror = function() {
      if (this.error === MediaError.MEDIA_ERR_ABORTED) {
        // cbus.ui.showSnackbar("The user canceled the audio.", "error");
      } else if (this.error === MediaError.MEDIA_ERR_NETWORK) {
        cbus.ui.showSnackbar(i18n.__("snackbar_error-media-network"), "error");
      } else if (this.error === MediaError.MEDIA_ERR_DECODE) {
        cbus.ui.showSnackbar(i18n.__("snackbar_error-media-decode"), "error");
      } else if (this.error === MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED) {
        cbus.ui.showSnackbar(i18n.__("snackbar_error-media-unsupported"), "error");
      } else {
        cbus.ui.showSnackbar(i18n.__("snackbar_error-media-unknown"), "error");
      }
    };

    cbus.audio.element.oncanplay = function() {
      cbus.broadcast.send("audioBufferEnd");
    };
    cbus.audio.element.onseeking = function() {
      cbus.broadcast.send("audioBufferStart");
    };
    cbus.audio.element.onseeked = function() {
      cbus.broadcast.send("audioBufferEnd");
      cbus.audio.updatePlayerTime();
      if (cbus.audio.mprisPlayer) {
        cbus.audio.mprisPlayer.interfaces.player.emitSignal("Seeked", cbus.audio.element.currentTime * 1e6);
      }
    };
    cbus.audio.element.onloadedmetadata = function() {
      if (disableAutomaticProgressRestore === true) {
        cbus.audio.element.currentTime = 0;
      } else {
        cbus.audio.tryRestoreProgress();
      }
      cbus.audio.updatePlayerTime();
    };
    cbus.audio.element.onended = function() {
      if (cbus.audio.queue.length && cbus.audio.queue[0].src !== cbus.audio.element.src) {
        cbus.audio.playQueueItem(0);
      } else {
        // up next
        if (existsRecursive(cbus.data.state, ["upNext", "source"])) {
          let upNextState = cbus.data.state.upNext;
          let currentEpisode = cbus.data.getEpisodeData({ audioElement: cbus.audio.element });
          let currentPodcast = cbus.data.getFeedData({ url: currentEpisode.feedURL });
          if (upNextState.source === "home") {
            var currentIndex = -1;
            let l = cbus.data.episodes.length;
            for (let i = 0; i < l; i++) {
              if (
                currentIndex === -1 &&
                cbus.data.episodes[i].url === currentEpisode.url &&
                cbus.data.episodes[i].feedURL === currentEpisode.feedURL
              ) {
                currentIndex = i;
                break;
              }
            }
            for (let i = currentIndex + 1; i < l; i++) {
              if (!cbus.data.getEpisodeProgress(cbus.data.episodes[i].url).completed) {
                cbus.audio.setElement(getElem(".audios").querySelector(`[data-id="${cbus.data.episodes[i].url}"]`));
                cbus.audio.play();
                break;
              }
            }
          } else if (upNextState.source === "podcast") {
            // TODO
          }
        }
      }
    };

    cbus.audio.state.episode = cbus.data.getEpisodeData({
      audioElement: elem
    });
    cbus.audio.state.episode.urlSha1 = sha1(cbus.audio.state.episode.url);
    cbus.audio.state.feed = cbus.data.getFeedData({
      url: cbus.audio.state.episode.feedURL
    });

    if (cbus.audio.state.episode.isVideo) {
      let draw = function() {
        cbus.ui.videoCanvasContext.drawImage(
          cbus.audio.element,
          0, 0,
          cbus.ui.videoCanvasElement.width, cbus.ui.videoCanvasElement.height
        );
        cbus.audio.videoCanvasAnimationFrameID = requestAnimationFrame(draw);
      }
      cbus.audio.videoCanvasAnimationFrameID = requestAnimationFrame(draw);
    } else if (cbus.audio.videoCanvasAnimationFrameID) {
      cancelAnimationFrame(cbus.audio.videoCanvasAnimationFrameID);
    }

    cbus.broadcast.send("audioChange", cbus.audio.state.episode);
    cbus.ui.updateThumbarButtons();

    localforage.setItem("cbus-last-audio-url", elem.dataset.id);

    cbus.audio.updatePlayerTime();
  },

  updatePlayerTime: function(options) {
    if (cbus.audio.element && !cbus.audio.element.paused) {
      cbus.broadcast.send("audioTick", {
        currentTime: cbus.audio.element.currentTime,
        duration: cbus.audio.element.duration
      });
      if (cbus.audio.mprisPlayer) {
        let trackID = cbus.audio.mprisPlayer.objectPath("track/" + cbus.audio.state.episode.urlSha1);
        if (
          !Number.isNaN(cbus.audio.element.duration) &&
          (
            !cbus.audio.mprisPlayer.metadata ||
            cbus.audio.mprisPlayer.metadata["mpris:trackid"] !== trackID
          )
        ) {
          if (cbus.audio.state.feed.image instanceof Blob && cbus.audio.state.updatingArtFor !== cbus.audio.state.feed.url) {
            cbus.audio.state.updatingArtFor = cbus.audio.state.feed.url;
            cbus.broadcast.send("updateFeedArtworks", {
              feedUrl: cbus.audio.state.feed.url,
              callback: function(updatedFeeds) {
                if (updatedFeeds[0].url === cbus.audio.state.feed.url) {
                  cbus.audio.state.feed.image = cbus.const.IMAGE_ON_DISK_PLACEHOLDER;
                  cbus.audio.mprisSetMetadata(trackID);
                  cbus.audio.state.updatingArtFor = null;
                }
              }
            });
          } else if (cbus.audio.state.updatingArtFor !== cbus.audio.state.feed.url) {
            cbus.audio.mprisSetMetadata(trackID);
          }
        }
      }
    }
  },
  sliderUpdateInterval: null,

  playQueueItem: function(index) {
    if (cbus.audio.queue[index]) {
      cbus.audio.setElement(cbus.audio.queue[index]);
      cbus.audio.removeQueueItem(index);
      cbus.audio.play();

      cbus.broadcast.send("queueChanged");

      return true;
    } else {
      return false;
    }
  },

  removeQueueItem: function(index) {
    if (cbus.audio.queue[index]) {
      cbus.audio.queue.splice(index, 1);
      $(".list--queue .episode").eq(index).remove();
      cbus.broadcast.send("queueChanged");
    }
  },

  play: function() {
    cbus.audio.element.play();
    $(".player_button--play").html("pause");
    cbus.broadcast.send("audio-play");
    cbus.broadcast.send("audioBufferStart");
    if (cbus.audio.mprisPlayer) {
      cbus.audio.mprisPlayer.playbackStatus = "Playing";
    }
    cbus.ui.updateThumbarButtons();
  },
  pause: function() {
    cbus.audio.element.pause();
    $(".player_button--play").html("play_arrow");
    cbus.broadcast.send("audio-pause");
    if (cbus.audio.mprisPlayer) {
      cbus.audio.mprisPlayer.playbackStatus = "Paused";
    }
    cbus.ui.updateThumbarButtons();
  },
  playpause: function() {
    if (cbus.audio.element.paused) {
      cbus.audio.play();
    } else {
      cbus.audio.pause();
    }
  },
  stop: function() {
    cbus.audio.element.pause();
    cbus.audio.element.currentTime = 0;
    cbus.broadcast.send("audio-stop");
    if (cbus.audio.mprisPlayer) {
      cbus.audio.mprisPlayer.playbackStatus = "Stopped";
    }
    cbus.ui.updateThumbarButtons();
  },
  jump: function(amount) {
    cbus.audio.element.currentTime += amount;
  },

  setPlaybackRate: function(rate) {
    cbus.audio.element.playbackRate = rate ? clamp(rate, cbus.audio.PLAYBACK_RATE_MIN, cbus.audio.PLAYBACK_RATE_MAX) : 1;
    if (cbus.audio.mprisPlayer) {
      cbus.audio.mprisPlayer.rate = cbus.audio.element.playbackRate;
    }
    cbus.broadcast.send("playbackRateChanged", cbus.audio.element.playbackRate);
  },
  setVolume: function(volume) {
    cbus.audio.element.volume = volume / 100;
    if (cbus.audio.mprisPlayer) {
      cbus.audio.mprisPlayer.volume = cbus.audio.element.volume
    }
    // TODO send broadcast after adding keyboard shortcut
  },

  queue: [],
  enqueue: function(audioElement, hiddenEnqueue) {
    cbus.audio.queue.push(audioElement);

    let episodeData = cbus.data.getEpisodeData({
      audioElement: audioElement
    });

    cbus.broadcast.send("episodeEnqueue", {
      episodeData: episodeData,
      hiddenEnqueue: (hiddenEnqueue === true ? true : false)
    });
    cbus.broadcast.send("queueChanged");
    cbus.ui.updateThumbarButtons();

    if (!hiddenEnqueue && cbus.settings.data.queueAutoDownload) {
      if (cbus.data.episodesOffline.indexOf(episodeData.id) === -1) {
        cbus.data.downloadEpisode(audioElement);
      }
    }
  },

  mprisPlayer: null,
  mprisSetMetadata: function(trackID) {
    cbus.audio.mprisPlayer.metadata = {
      "mpris:trackid": trackID,
      "mpris:length": cbus.audio.element.duration * 1000000,
      "mpris:artUrl": cbus.data.getPodcastImageURI(cbus.audio.state.feed),
      "xesam:title": cbus.audio.state.episode.title,
      "xesam:album": cbus.audio.state.feed.title,
      "xesam:artist": [ cbus.audio.state.feed.title ]
    };
    cbus.audio.mprisPlayer.position = cbus.audio.element.currentTime * 1000000;
    console.log("metadata set", cbus.audio.mprisPlayer.metadata)
  }
};

cbus.audio.sliderUpdateInterval = setInterval(cbus.audio.updatePlayerTime, 500);

if (MPRISPlayer) {
  cbus.audio.mprisPlayer = MPRISPlayer({
    name: "cumulonimbus",
    identity: "CPod",
    supportedUriSchemes: [ "file", "http", "https" ],
    supportedMimeTypes: [ "audio/*" ],
    supportedInterfaces: [ "player" ]
  });

  cbus.audio.mprisPlayer.canGoNext = false;
  cbus.audio.mprisPlayer.canGoPrevious = false;
  cbus.audio.mprisPlayer.playbackStatus = "Paused";
  cbus.audio.mprisPlayer.minimumRate = cbus.audio.PLAYBACK_RATE_MIN;
  cbus.audio.mprisPlayer.maximumRate = cbus.audio.PLAYBACK_RATE_MAX;

  cbus.audio.mprisPlayer.on("play", () => {
    cbus.audio.play();
  });
  cbus.audio.mprisPlayer.on("pause", () => {
    cbus.audio.pause();
  });
  cbus.audio.mprisPlayer.on("stop", () => {
    cbus.audio.pause();
  });
  cbus.audio.mprisPlayer.on("playpause", () => {
    if (cbus.audio.element.paused) {
      cbus.audio.play();
    } else {
      cbus.audio.pause();
    }
  });
  cbus.audio.mprisPlayer.on("next", () => {
    cbus.audio.playQueueItem(0);
  });
  cbus.audio.mprisPlayer.on("seek", (data) => {
    if (cbus.audio.element) {
      cbus.audio.element.currentTime = data.position / 1000000;
    }
  });
  cbus.audio.mprisPlayer.on("position", (data) => {
    if (cbus.audio.element) {
      cbus.audio.element.currentTime = data.position / 1000000;
    }
  });
  cbus.audio.mprisPlayer.on("volume", (data) => {
    if (cbus.audio.element) {
      cbus.audio.element.volume = clamp(data.volume, 0, 1);
    }
  });
  cbus.audio.mprisPlayer.on("rate", (data) => {
    if (cbus.audio.element) {
      cbus.audio.setPlaybackRate(data.rate);
    }
  });

  cbus.broadcast.listen("queueChanged", () => {
    if (cbus.audio.queue.length) {
      cbus.audio.mprisPlayer.canGoNext = true;
    } else {
      cbus.audio.mprisPlayer.canGoNext = false;
    }
  });
}
<|endoftext|>


<commit_before>var Resource = require('./resource');
var util = require("util");
var _ = require("underscore");
var crypto = require('crypto');
var pruned = require('./json-pruned');
var fs = require('fs');
var request = require('request-promise');
var Promise = require("bluebird");

function Media(session, params) {
    Resource.apply(this, arguments);
}

util.inherits(Media, Resource);

module.exports = Media;
var Request = require('./request');
var Comment = require('./comment');
var Account = require('./account');
var Location = require('./location');
var Exceptions = require('./exceptions');

Media.prototype.parseParams = function (json) {
    var hash = {};
    var that = this;
    hash.code = json.code;
    hash.id = json.id;
    hash.likeCount = json.like_count;
    hash.hasLiked = json.has_liked;
    hash.hasMoreComments = json.has_more_comments;
    hash.photoOfYou = json.photo_of_you;
    hash.originalWidth = json.original_width;
    hash.commentsDisabled = json.comments_disabled;
    hash.commentCount = hash.commentsDisabled ? 0 : json.comment_count ;
    hash.originalHeight = json.original_height;
    hash.mediaType = json.media_type;
    hash.deviceTimestamp = json.device_timestamp;
    hash.webLink = "https://www.instagram.com/p/" + json.code + "/"
    hash.usertags = json.usertags;
    hash.carouselMedia = [];

    if(json.video_duration)
        hash.videoDuration = json.video_duration;
    if(json.filter_type)
        hash.filterType = json.filter_type;
    if(json.has_audio)
        hash.hasAudio = json.has_audio;
    if(json.view_count)
        hash.viewCount = json.view_count;
    if(_.isObject(json.location)) {
        var location = json.location;
        location.location = json.location;
        location.title = location.name;
        location.subtitle = null;
        this.location = new Location(that.session, location);
    }
    if (json.media_type === 2) {
        hash.video = {
            versions: json.video_versions,
            hasAudio: json.has_audio,
            duration: json.video_duration
        }
    }
    if (json.media_type === 8 && _.isArray(json.carousel_media)) {
        hash.carouselMedia = _.map(json.carousel_media, function (medium) {
           return new Media(that.session, medium);
        });
    }
    if (_.isObject(json.caption))
        hash.caption = json.caption.text;
    hash.takenAt = parseInt(json.taken_at) * 1000;
    if (_.isObject(json.image_versions2)) {
        hash.images = json.image_versions2.candidates;
    } else if (_.isObject(json.carousel_media)) {
        hash.images = json.carousel_media.map(function(media) {
            return media.image_versions2.candidates
        })
    }
    if (_.isArray(json.video_versions))
        hash.videos = json.video_versions;
    this.previewComments = _.map(json.preview_comments, function(comment) {
        return new Comment(that.session, comment);
    });
    // backward compability
    this.comments = this.previewComments;
    this.account = new Account(that.session, json.user);
    return hash;
};

Media.prototype.getParams = function () {
    return _.extend(this._params, {
        account: this.account.params,
        comments: _.pluck(this.comments, 'params'),
        location: this.location ? this.location.params : {},
        carouselMedia:  _.pluck(this._params.carouselMedia, 'params')
    });
};

Media.getById = function (session, id) {
    return new Request(session)
        .setMethod('GET')
        .setResource('mediaInfo', {mediaId: id})
        .send()
        .then(function(json) {
            return new Media(session, json.items[0])
        })
};

Media.getByUrl = function(session, url){
    var self = this;
    return request({
        url: 'https://api.instagram.com/oembed/',
        qs:{url:url},
        json:true
    })
        .then(function (response) {
            return self.getById(session, response.media_id)
        })
        .catch(function (reason) {
            if(reason.error === 'No URL Match')throw new Exceptions.NotFoundError('No URL Match');
            else throw reason;
        })
};

Media.likers = function(session, mediaId) {
    return new Request(session)
        .setMethod('GET')
        .setResource('mediaLikes', {mediaId: mediaId})
        .send()
        .then(function(data) {
            return _.map(data.users, function (user) {
                return new Account(session, user);
            });
        });
};

Media.delete = function(session, mediaId) {
    return new Request(session)
        .setMethod('POST')
        .setResource('mediaDeletePhoto', {mediaId: mediaId})
        .setData({
            media_id: mediaId
        })
        .generateUUID()
        .signPayload()
        .send()
        .then(function (json) {
            if(json.did_delete) return;
            throw new Exceptions.RequestError({
                messaage: 'Not posible to delete medium!'
            })
        })
};

Media.edit = function(session, mediaId, caption, userTags) {
    var requestPayload = {
        media_id: mediaId,
        caption_text: caption
    };

    if (userTags) {
        requestPayload.usertags = JSON.stringify(userTags);
    }

    return new Request(session)
        .setMethod('POST')
        .setResource('mediaEdit', {mediaId: mediaId})
        .setData(requestPayload)
        .generateUUID()
        .signPayload()
        .send()
        .then(function (json) {
            if(json.media.caption_is_edited) {
                return new Media(session, json.media);
            }
            throw new Exceptions.RequestError({
                messaage: 'Edit media not successful!'
            });
        });
};

Media.configurePhoto = function (session, uploadId, caption, width, height, userTags) {
    if(_.isEmpty(uploadId))
        throw new Error("Upload argument must be upload valid upload id");
    if(!caption) caption = "";
    if(!width) width = 800;
    if(!height) height = 800;
    if (!userTags) userTags = {};

    const CROP = 1;
    return session.getAccountId()
        .then(function(accountId){
            var payload = pruned({
                source_type: "4",
                caption: caption,
                upload_id: uploadId,
                usertags: JSON.stringify(userTags),
                _uid: accountId.toString(),
                device: session.device.payload,
                edits: {
                    crop_original_size:["$width","$height"],
                    crop_center: ["$zero","$negativeZero"],
                    crop_zoom: "$crop"
                },
                extra: {
                    source_width: width,
                    source_height: height
                }
            });
            payload = payload.replace(/\"\$width\"/gi, width.toFixed(1));
            payload = payload.replace(/\"\$height\"/gi, height.toFixed(1));
            payload = payload.replace(/\"\$zero\"/gi, (0).toFixed(1));
            payload = payload.replace(/\"\$negativeZero\"/gi, "-" + (0).toFixed(1));
            payload = payload.replace(/\"\$crop\"/gi, CROP.toFixed(1));

            return new Request(session)
                .setMethod('POST')
                .setResource('mediaConfigure')
                .setBodyType('form')
                .setData(JSON.parse(payload))
                .generateUUID()
                .signPayload()
                .send()
        })
        .then(function(json) {
            return new Media(session, json.media)
        })
};

Media.configurePhotoStory = function (session, uploadId, width, height) {
    if(_.isEmpty(uploadId))
        throw new Error("Upload argument must be upload valid upload id");
    if(!width) width = 800;
    if(!height) height = 800;
    const CROP = 1;
    return session.getAccountId()
        .then(function(accountId){
            var payload = pruned({
                source_type: "4",
                upload_id: uploadId,
                _uid: accountId.toString(),
                device: session.device.payload,
                edits: {
                    crop_original_size:["$width","$height"],
                    crop_center: ["$zero","$negativeZero"],
                    crop_zoom: "$crop"
                },
                extra: {
                    source_width: width,
                    source_height: height
                }
            });
            payload = payload.replace(/\"\$width\"/gi, width.toFixed(1));
            payload = payload.replace(/\"\$height\"/gi, height.toFixed(1));
            payload = payload.replace(/\"\$zero\"/gi, (0).toFixed(1));
            payload = payload.replace(/\"\$negativeZero\"/gi, "-" + (0).toFixed(1));
            payload = payload.replace(/\"\$crop\"/gi, CROP.toFixed(1));

            return new Request(session)
                .setMethod('POST')
                .setResource('mediaConfigureStory')
                .setBodyType('form')
                .setData(JSON.parse(payload))
                .generateUUID()
                .signPayload()
                .send()
        })
        .then(function(json) {
            return new Media(session, json.media)
        })
};

Media.configureVideo = function (session, uploadId, caption, durationms, delay) {
    if(_.isEmpty(uploadId))
        throw new Error("Upload argument must be upload valid upload id");
    if(typeof(durationms)==='undefined')
        throw new Error("Durationms argument must be upload valid video duration");
    var duration = durationms/1000;
    if(!caption) caption = "";
    if(!delay || typeof delay != "number") delay = 6500;
    return Promise.delay(delay)
        .then(function(){
            return session.getAccountId()
        })
        .then(function(accountId){
            var payload = pruned({
                "filter_type": "0",
                "source_type": "3",
                "video_result": "deprecated",
                "_uid":accountId.toString(),
                "caption": caption,
                "upload_id": uploadId.toString(),
                "device":{
                    "manufacturer":session.device.info.manufacturer,
                    "model":session.device.info.model,
                    "android_version":session.device._api,
                    "android_release":session.device._release
                },
                "length": duration,
                "clips": [
                    {
                        "length": duration,
                        "source_type": "3",
                        "camera_position": "back"
                    }
                ],
                "audio_muted": false,
                "poster_frame_index": 0
            });

            return new Request(session)
                .setMethod('POST')
                .setResource('videoConfigure')
                .setBodyType('form')
                .setData(JSON.parse(payload))
                .generateUUID()
                .signPayload()
                .send()
                .then(function(json) {
                    return new Media(session, json.media)
                })
                .catch(Exceptions.TranscodeTimeoutError,function(error){
                    //Well, we just want to repeat our request. Dunno why this is happening and we should not let our users deal with this crap themselves.
                    return Media.configureVideo(session,uploadId,caption,durationms,delay);
                })
    })
};

Media.configurePhotoAlbum = function (session, uploadId, caption, width, height, userTags) {
    if(_.isEmpty(uploadId))
        throw new Error("Upload argument must be upload valid upload id");
    if(!caption) caption = "";
    if(!width) width = 800;
    if(!height) height = 800;
    if (!userTags) userTags = {};

    const CROP = 1;

    var payload = {
        source_type: "4",
        caption: caption,
        upload_id: uploadId,
        media_folder: 'Instagram',
        device: session.device.payload,
        edits: {
            crop_original_size:[width.toFixed(1),height.toFixed(1)],
            crop_center: [(0).toFixed(1), "-" + (0).toFixed(1)],
            crop_zoom: CROP.toFixed(1)
        },
        extra: {
            source_width: width,
            source_height: height
        }
    };
    return Promise.resolve(payload);
};

Media.configureVideoAlbum = function (session, uploadId, caption, durationms, delay, width, height) {
    if(_.isEmpty(uploadId))
        throw new Error("Upload argument must be upload valid upload id");
    if(typeof(durationms)==='undefined')
        throw new Error("Durationms argument must be upload valid video duration");
    var duration = durationms/1000;
    if(!caption) caption = "";
    if(!delay || typeof delay != "number") delay = 6500;
    return Promise.delay(delay)
        .then(function(){
            var payload = {
                filter_type: "0",
                source_type: "3",
                video_result: "deprecated",
                caption: caption,
                upload_id: uploadId,
                device: session.device.payload,
                length: duration,
                clips: [
                    {
                        length: duration,
                        source_type: "3",
                        camera_position: "back"
                    }
                ],
                audio_muted: false,
                poster_frame_index: 0,
                extra: {
                    source_width: width,
                    source_height: height
                }
            };

            return Promise.resolve(payload);
        })
};

Media.configureAlbum = function (session, medias, caption, disableComments) {
    var albumUploadId = new Date().getTime();

    caption = caption || '';
    disableComments = disableComments || false;

    Promise.mapSeries(medias, function (media) {
        if(media.type === 'photo') {
            return Media.configurePhotoAlbum(session, media.uploadId, caption, media.size[0], media.size[1], media.usertags)
        } else if (media.type === 'video') {
            return Media.configureVideoAlbum(session, media.uploadId, caption, media.durationms, media.delay, media.size[0], media.size[1]);
        } else {
            throw new Error('Invalid media type: ' + media.type);
        }
    })
        .then(function (results) {
            var params = {
                caption: caption,
                client_sidecar_id: albumUploadId,
                children_metadata: results
            };

            if(disableComments) {
                params['disable_comments'] = '1';
            }

            return new Request(session)
                .setMethod('POST')
                .setResource('mediaConfigureSidecar')
                .setBodyType('form')
                .setData(params)
                .generateUUID()
                .signPayload()
                .send()
        })
};<commit_msg>Update configureVideo method to send contemporary parameters to instagram.<commit_after>var Resource = require('./resource');
var util = require("util");
var _ = require("underscore");
var crypto = require('crypto');
var pruned = require('./json-pruned');
var fs = require('fs');
var request = require('request-promise');
var Promise = require("bluebird");

function Media(session, params) {
    Resource.apply(this, arguments);
}

util.inherits(Media, Resource);

module.exports = Media;
var Request = require('./request');
var Comment = require('./comment');
var Account = require('./account');
var Location = require('./location');
var Exceptions = require('./exceptions');

Media.prototype.parseParams = function (json) {
    var hash = {};
    var that = this;
    hash.code = json.code;
    hash.id = json.id;
    hash.likeCount = json.like_count;
    hash.hasLiked = json.has_liked;
    hash.hasMoreComments = json.has_more_comments;
    hash.photoOfYou = json.photo_of_you;
    hash.originalWidth = json.original_width;
    hash.commentsDisabled = json.comments_disabled;
    hash.commentCount = hash.commentsDisabled ? 0 : json.comment_count ;
    hash.originalHeight = json.original_height;
    hash.mediaType = json.media_type;
    hash.deviceTimestamp = json.device_timestamp;
    hash.webLink = "https://www.instagram.com/p/" + json.code + "/"
    hash.usertags = json.usertags;
    hash.carouselMedia = [];

    if(json.video_duration)
        hash.videoDuration = json.video_duration;
    if(json.filter_type)
        hash.filterType = json.filter_type;
    if(json.has_audio)
        hash.hasAudio = json.has_audio;
    if(json.view_count)
        hash.viewCount = json.view_count;
    if(_.isObject(json.location)) {
        var location = json.location;
        location.location = json.location;
        location.title = location.name;
        location.subtitle = null;
        this.location = new Location(that.session, location);
    }
    if (json.media_type === 2) {
        hash.video = {
            versions: json.video_versions,
            hasAudio: json.has_audio,
            duration: json.video_duration
        }
    }
    if (json.media_type === 8 && _.isArray(json.carousel_media)) {
        hash.carouselMedia = _.map(json.carousel_media, function (medium) {
           return new Media(that.session, medium);
        });
    }
    if (_.isObject(json.caption))
        hash.caption = json.caption.text;
    hash.takenAt = parseInt(json.taken_at) * 1000;
    if (_.isObject(json.image_versions2)) {
        hash.images = json.image_versions2.candidates;
    } else if (_.isObject(json.carousel_media)) {
        hash.images = json.carousel_media.map(function(media) {
            return media.image_versions2.candidates
        })
    }
    if (_.isArray(json.video_versions))
        hash.videos = json.video_versions;
    this.previewComments = _.map(json.preview_comments, function(comment) {
        return new Comment(that.session, comment);
    });
    // backward compability
    this.comments = this.previewComments;
    this.account = new Account(that.session, json.user);
    return hash;
};

Media.prototype.getParams = function () {
    return _.extend(this._params, {
        account: this.account.params,
        comments: _.pluck(this.comments, 'params'),
        location: this.location ? this.location.params : {},
        carouselMedia:  _.pluck(this._params.carouselMedia, 'params')
    });
};

Media.getById = function (session, id) {
    return new Request(session)
        .setMethod('GET')
        .setResource('mediaInfo', {mediaId: id})
        .send()
        .then(function(json) {
            return new Media(session, json.items[0])
        })
};

Media.getByUrl = function(session, url){
    var self = this;
    return request({
        url: 'https://api.instagram.com/oembed/',
        qs:{url:url},
        json:true
    })
        .then(function (response) {
            return self.getById(session, response.media_id)
        })
        .catch(function (reason) {
            if(reason.error === 'No URL Match')throw new Exceptions.NotFoundError('No URL Match');
            else throw reason;
        })
};

Media.likers = function(session, mediaId) {
    return new Request(session)
        .setMethod('GET')
        .setResource('mediaLikes', {mediaId: mediaId})
        .send()
        .then(function(data) {
            return _.map(data.users, function (user) {
                return new Account(session, user);
            });
        });
};

Media.delete = function(session, mediaId) {
    return new Request(session)
        .setMethod('POST')
        .setResource('mediaDeletePhoto', {mediaId: mediaId})
        .setData({
            media_id: mediaId
        })
        .generateUUID()
        .signPayload()
        .send()
        .then(function (json) {
            if(json.did_delete) return;
            throw new Exceptions.RequestError({
                messaage: 'Not posible to delete medium!'
            })
        })
};

Media.edit = function(session, mediaId, caption, userTags) {
    var requestPayload = {
        media_id: mediaId,
        caption_text: caption
    };

    if (userTags) {
        requestPayload.usertags = JSON.stringify(userTags);
    }

    return new Request(session)
        .setMethod('POST')
        .setResource('mediaEdit', {mediaId: mediaId})
        .setData(requestPayload)
        .generateUUID()
        .signPayload()
        .send()
        .then(function (json) {
            if(json.media.caption_is_edited) {
                return new Media(session, json.media);
            }
            throw new Exceptions.RequestError({
                messaage: 'Edit media not successful!'
            });
        });
};

Media.configurePhoto = function (session, uploadId, caption, width, height, userTags) {
    if(_.isEmpty(uploadId))
        throw new Error("Upload argument must be upload valid upload id");
    if(!caption) caption = "";
    if(!width) width = 800;
    if(!height) height = 800;
    if (!userTags) userTags = {};

    const CROP = 1;
    return session.getAccountId()
        .then(function(accountId){
            var payload = pruned({
                source_type: "4",
                caption: caption,
                upload_id: uploadId,
                usertags: JSON.stringify(userTags),
                _uid: accountId.toString(),
                device: session.device.payload,
                edits: {
                    crop_original_size:["$width","$height"],
                    crop_center: ["$zero","$negativeZero"],
                    crop_zoom: "$crop"
                },
                extra: {
                    source_width: width,
                    source_height: height
                }
            });
            payload = payload.replace(/\"\$width\"/gi, width.toFixed(1));
            payload = payload.replace(/\"\$height\"/gi, height.toFixed(1));
            payload = payload.replace(/\"\$zero\"/gi, (0).toFixed(1));
            payload = payload.replace(/\"\$negativeZero\"/gi, "-" + (0).toFixed(1));
            payload = payload.replace(/\"\$crop\"/gi, CROP.toFixed(1));

            return new Request(session)
                .setMethod('POST')
                .setResource('mediaConfigure')
                .setBodyType('form')
                .setData(JSON.parse(payload))
                .generateUUID()
                .signPayload()
                .send()
        })
        .then(function(json) {
            return new Media(session, json.media)
        })
};

Media.configurePhotoStory = function (session, uploadId, width, height) {
    if(_.isEmpty(uploadId))
        throw new Error("Upload argument must be upload valid upload id");
    if(!width) width = 800;
    if(!height) height = 800;
    const CROP = 1;
    return session.getAccountId()
        .then(function(accountId){
            var payload = pruned({
                source_type: "4",
                upload_id: uploadId,
                _uid: accountId.toString(),
                device: session.device.payload,
                edits: {
                    crop_original_size:["$width","$height"],
                    crop_center: ["$zero","$negativeZero"],
                    crop_zoom: "$crop"
                },
                extra: {
                    source_width: width,
                    source_height: height
                }
            });
            payload = payload.replace(/\"\$width\"/gi, width.toFixed(1));
            payload = payload.replace(/\"\$height\"/gi, height.toFixed(1));
            payload = payload.replace(/\"\$zero\"/gi, (0).toFixed(1));
            payload = payload.replace(/\"\$negativeZero\"/gi, "-" + (0).toFixed(1));
            payload = payload.replace(/\"\$crop\"/gi, CROP.toFixed(1));

            return new Request(session)
                .setMethod('POST')
                .setResource('mediaConfigureStory')
                .setBodyType('form')
                .setData(JSON.parse(payload))
                .generateUUID()
                .signPayload()
                .send()
        })
        .then(function(json) {
            return new Media(session, json.media)
        })
};

Media.configureVideo = function (session, uploadId, caption, durationms, delay, {
  audio_muted = false,
  trim_type = 0,
  source_type = 'camera',
  mas_opt_in = 'NOT_PROMPTED',
  disable_comments = false,
  filter_type = 0,
  poster_frame_index = 0,
  geotag_enabled = false,
  camera_position = 'unknown'
} = {}) {
    if(_.isEmpty(uploadId))
        throw new Error("Upload argument must be upload valid upload id");
    if(typeof(durationms)==='undefined')
        throw new Error("Durationms argument must be upload valid video duration");
    var duration = durationms/1000;
    if(!caption) caption = "";
    if(!delay || typeof delay != "number") delay = 6500;
    return Promise.delay(delay)
        .then(function(){
            return session.getAccountId()
        })
        .then(function(accountId){
            var payload = pruned({
              "video_result": "deprecated",
              audio_muted,
              trim_type,
              "client_timestamp": String(new Date().getTime()).substr(0,10),
              "caption": caption,
              "edits": {
                "filter_strength": 1
              },
              "clips": [
                {
                  "length": duration,
                  "cinema": "unsupported",
                  "original_length": duration,
                  source_type,
                  start_time:0,
                  trim_type,
                  "camera_position": "back"
                }
              ],
              "_uid":accountId.toString(),
              source_type,
              mas_opt_in,
              "length": duration,
              disable_comments,
              filter_type,
              poster_frame_index,
              geotag_enabled,
              camera_position,
              "upload_id": uploadId.toString()
            });

            return new Request(session)
                .setMethod('POST')
                .setResource('videoConfigure')
                .setBodyType('form')
                .setData(JSON.parse(payload))
                .generateUUID()
                .signPayload()
                .send()
                .then(function(json) {
                    return new Media(session, json.media)
                })
                .catch(Exceptions.TranscodeTimeoutError,function(error){
                    //Well, we just want to repeat our request. Dunno why this is happening and we should not let our users deal with this crap themselves.
                    return Media.configureVideo(session,uploadId,caption,durationms,delay);
                })
    })
};

Media.configurePhotoAlbum = function (session, uploadId, caption, width, height, userTags) {
    if(_.isEmpty(uploadId))
        throw new Error("Upload argument must be upload valid upload id");
    if(!caption) caption = "";
    if(!width) width = 800;
    if(!height) height = 800;
    if (!userTags) userTags = {};

    const CROP = 1;

    var payload = {
        source_type: "4",
        caption: caption,
        upload_id: uploadId,
        media_folder: 'Instagram',
        device: session.device.payload,
        edits: {
            crop_original_size:[width.toFixed(1),height.toFixed(1)],
            crop_center: [(0).toFixed(1), "-" + (0).toFixed(1)],
            crop_zoom: CROP.toFixed(1)
        },
        extra: {
            source_width: width,
            source_height: height
        }
    };
    return Promise.resolve(payload);
};

Media.configureVideoAlbum = function (session, uploadId, caption, durationms, delay, width, height) {
    if(_.isEmpty(uploadId))
        throw new Error("Upload argument must be upload valid upload id");
    if(typeof(durationms)==='undefined')
        throw new Error("Durationms argument must be upload valid video duration");
    var duration = durationms/1000;
    if(!caption) caption = "";
    if(!delay || typeof delay != "number") delay = 6500;
    return Promise.delay(delay)
        .then(function(){
            var payload = {
                filter_type: "0",
                source_type: "3",
                video_result: "deprecated",
                caption: caption,
                upload_id: uploadId,
                device: session.device.payload,
                length: duration,
                clips: [
                    {
                        length: duration,
                        source_type: "3",
                        camera_position: "back"
                    }
                ],
                audio_muted: false,
                poster_frame_index: 0,
                extra: {
                    source_width: width,
                    source_height: height
                }
            };

            return Promise.resolve(payload);
        })
};

Media.configureAlbum = function (session, medias, caption, disableComments) {
    var albumUploadId = new Date().getTime();

    caption = caption || '';
    disableComments = disableComments || false;

    Promise.mapSeries(medias, function (media) {
        if(media.type === 'photo') {
            return Media.configurePhotoAlbum(session, media.uploadId, caption, media.size[0], media.size[1], media.usertags)
        } else if (media.type === 'video') {
            return Media.configureVideoAlbum(session, media.uploadId, caption, media.durationms, media.delay, media.size[0], media.size[1]);
        } else {
            throw new Error('Invalid media type: ' + media.type);
        }
    })
        .then(function (results) {
            var params = {
                caption: caption,
                client_sidecar_id: albumUploadId,
                children_metadata: results
            };

            if(disableComments) {
                params['disable_comments'] = '1';
            }

            return new Request(session)
                .setMethod('POST')
                .setResource('mediaConfigureSidecar')
                .setBodyType('form')
                .setData(params)
                .generateUUID()
                .signPayload()
                .send()
        })
};<|endoftext|>


<commit_before><commit_msg>build issues<commit_after><|endoftext|>


<commit_before>(function ($,window){
	var API_ROOT  = "%%api.root%%";
	var API_NAME  = "%%api.name%%";
	var API_FUNCS = %%api.funcs%%;
	var API_TIMEOUT = 30 * 1000;
	function copyArgumentsArray(args) {
		var copy = [];
		for (var i = 0; i < args.length; i++ ) {
			copy[i] = args[i];
		}
		return copy;
	}
	function sendAjaxCall( apiMethod, data, callback ) {

		function successAjaxCallback( xhrData, textStatus ) {
			var args = xhrData.args;
			callback && callback.apply(window,args);
		}
		var requestUrl = '/'+[API_ROOT,API_NAME,apiMethod].join('/');
		var dataObj = {};

		dataObj.args  = data;
		dataObj.hasCb = callback !== null;

		$.ajax({
			url: requestUrl,
			data: JSON.stringify(dataObj),
			timeout: API_TIMEOUT,
			type: 'POST',
			contentType: 'application/json'
		}).done( successAjaxCallback );
	}

	function getCallbackFromArgs( argumentList ) {
		var callbackSuccess = null;

		if ( argumentList && argumentList.length >= 1 ) {
			var lastArg = argumentList[argumentList.length - 1];
			if ( typeof lastArg === "function" ) {
				callbackSuccess = argumentList[argumentList.length - 1];
				argumentList.splice(argumentList.length - 1,1);
			}
		}

		return {
			callback: callbackSuccess,
			args: argumentList
		};
	}

	function clientAPIRequest(i,apiMethod) {
		window[API_NAME][apiMethod] = function () {
			var argumentList = copyArgumentsArray(arguments);
			var mutatedArgs = getCallbackFromArgs(argumentList);
			sendAjaxCall( apiMethod, mutatedArgs.args, mutatedArgs.callback );
		};
	}

	function initializeAPI( ) {
		window[API_NAME] = {};
		$.each(API_FUNCS,clientAPIRequest);
	}

	initializeAPI();
})($,window)<commit_msg>A promise will be returned on the client side so that error hadlers can be added, and bumped the version<commit_after>(function ($,window){
	var API_ROOT  = "%%api.root%%";
	var API_NAME  = "%%api.name%%";
	var API_FUNCS = %%api.funcs%%;
	var API_TIMEOUT = 30 * 1000;
	function copyArgumentsArray(args) {
		var copy = [];
		for (var i = 0; i < args.length; i++ ) {
			copy[i] = args[i];
		}
		return copy;
	}
	function sendAjaxCall( apiMethod, data, callback ) {

		function successAjaxCallback( xhrData, textStatus ) {
			var args = xhrData.args;
			callback && callback.apply(window,args);
		}
		var requestUrl = '/'+[API_ROOT,API_NAME,apiMethod].join('/');
		var dataObj = {};

		dataObj.args  = data;
		dataObj.hasCb = callback !== null;

		return $.ajax({
			url: requestUrl,
			data: JSON.stringify(dataObj),
			timeout: API_TIMEOUT,
			type: 'POST',
			contentType: 'application/json'
		}).done( successAjaxCallback );
	}

	function getCallbackFromArgs( argumentList ) {
		var callbackSuccess = null;

		if ( argumentList && argumentList.length >= 1 ) {
			var lastArg = argumentList[argumentList.length - 1];
			if ( typeof lastArg === "function" ) {
				callbackSuccess = argumentList[argumentList.length - 1];
				argumentList.splice(argumentList.length - 1,1);
			}
		}

		return {
			callback: callbackSuccess,
			args: argumentList
		};
	}

	function clientAPIRequest(i,apiMethod) {
		window[API_NAME][apiMethod] = function () {
			var argumentList = copyArgumentsArray(arguments);
			var mutatedArgs = getCallbackFromArgs(argumentList);
			return sendAjaxCall( apiMethod, mutatedArgs.args, mutatedArgs.callback );
		};
	}

	function initializeAPI( ) {
		window[API_NAME] = {};
		$.each(API_FUNCS,clientAPIRequest);
	}

	initializeAPI();
})($,window)<|endoftext|>