<commit_before>"use strict";

const fs = require('fs');
const path = require('path');
const util = require('util');

const Module = require('../Core/API/Module');

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

class MusicPlayer extends Module {
  constructor(container) {
    super(container);

    this.bot = this.container.get('bot');
    this.redisClient = this.container.get('redisBrain');
    this.settings = this.container.get('settings');

    this.currentlyPlaying = null;
    this.voiceConnection = null;
    this.recordCache = new Map();

    this.QueuedMedia = QueuedMedia;

    this.hears(/np/i, this.onNowPlaying.bind(this));
    this.hears(/li/i, this.onDisplayPlaylist.bind(this));
    this.hears(/queue/i, this.queueItems.bind(this));
    this.hears(/next/i, this.skipSong.bind(this));
    this.hears(/yt/i, this.onYoutube.bind(this));
    this.hears(/sel/i, this.onSelectSearchResult.bind(this));

    if (MusicPlayer.useMPD) {
      this.search = new LocalMusicSearch(this.container);
      this.hears(/search/i, this.onSearch.bind(this));
    }

    QueuedMedia.initialize(this.container);
  }

  shutdown() {
    if (MusicPlayer.useMPD) {
      this.search.shutdown();
    }
  }

  getRedisKey(guildId, scope) {
    return `cardinal.${guildId}.${scope}`;
  }

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

  onDisplayPlaylist(m) {
    this.logger.debug('QUEUE_DISPLAY_PLAYLIST');
    const key = this.getRedisKey(m.guild.id, 'music_queue');
    let msg = '';

    this.redisClient.llen(key, (err, len) => {
      if (err) {
        this.logger.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 {
        this.redisClient.lrange(key, 0, len, (err, list) => {
          if (err) {
            this.logger.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);
        });
      }
    });
  }

  queueItems(m, args) {
    const promises = [];

    for (const url of args) {
      promises.push(this.queueItem(m, url));
    }

    return Promise.all(promises);
  }

  queueItem(m, url) {
    this.logger.debug('QUEUE_ITEM', url);
    if (!url) {
      this.logger.debug('no valid url');
      return m.reply('Please give me a URL to play');
    }

    Utils.fetchYoutubeInfo(url).then((obj) => {
      this.logger.debug('fetchYoutubeInfo promise resolve');

      const fields = [
        { from: 'title' },
        { from: 'display_id' },
        { from: 'duration' },
        { from: 'acodec' },
        { from: 'url' },
        { from: 'ext', to: 'extension' },
      ];
      const info = fields.reduce((info, field) => {
        info[field.to || field.from] = obj[field.from];
        return info;
      }, {});
      const formats = [obj.formats.find(elem => elem.format_id === obj.format_id)];

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

      return this.queueSave(m.guild.id, record).then(this.afterRedisSave.bind(this, m));
    }).catch((err) => {
      if (err) {
        this.logger.debug('error pulling youtube data', err.stack);
      }

      return m.reply('Sorry, I was not able to queue that song.');
    });
  }

  queueSave(guildId, record) {
    const redisKey = this.getRedisKey(guildId, 'music_queue');

    return this.redisClient.rpushAsync(redisKey, JSON.stringify(record)).then(() => {
      this.logger.debug('saved to redis');

      return record;
    });
  }

  onSearch(m, args) {
    const text = args.join(' ');

    return this.search.byAnyField(text).then((results) =>
      results.slice(0, 5).map((entry) => {
        const filePath = path.join(this.settings.mpd.baseDirectory, entry.file);
        return { id: filePath, title: entry.file };
      })
    ).then((entries) => {
      const redisKey = this.getRedisKey(m.guild.id, `${m.channel.id}.search`);
      this.redisClient.set(redisKey, JSON.stringify(entries));

      const response = entries.map((item, i) => `**${i + 1}:** ${item.title}`).join('\n');
      return m.channel.sendMessage(response);<commit_msg>MusicPlayer: handle YTDL playlist responses<commit_after>"use strict";

const fs = require('fs');
const path = require('path');
const util = require('util');

const Module = require('../Core/API/Module');

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

class MusicPlayer extends Module {
  constructor(container) {
    super(container);

    this.bot = this.container.get('bot');
    this.redisClient = this.container.get('redisBrain');
    this.settings = this.container.get('settings');

    this.currentlyPlaying = null;
    this.voiceConnection = null;
    this.recordCache = new Map();

    this.QueuedMedia = QueuedMedia;

    this.hears(/np/i, this.onNowPlaying.bind(this));
    this.hears(/li/i, this.onDisplayPlaylist.bind(this));
    this.hears(/queue/i, this.queueItems.bind(this));
    this.hears(/next/i, this.skipSong.bind(this));
    this.hears(/yt/i, this.onYoutube.bind(this));
    this.hears(/sel/i, this.onSelectSearchResult.bind(this));

    if (MusicPlayer.useMPD) {
      this.search = new LocalMusicSearch(this.container);
      this.hears(/search/i, this.onSearch.bind(this));
    }

    QueuedMedia.initialize(this.container);
  }

  shutdown() {
    if (MusicPlayer.useMPD) {
      this.search.shutdown();
    }
  }

  getRedisKey(guildId, scope) {
    return `cardinal.${guildId}.${scope}`;
  }

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

  onDisplayPlaylist(m) {
    this.logger.debug('QUEUE_DISPLAY_PLAYLIST');
    const key = this.getRedisKey(m.guild.id, 'music_queue');
    let msg = '';

    this.redisClient.llen(key, (err, len) => {
      if (err) {
        this.logger.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 {
        this.redisClient.lrange(key, 0, len, (err, list) => {
          if (err) {
            this.logger.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);
        });
      }
    });
  }

  queueItems(m, args) {
    const promises = [];

    for (const url of args) {
      promises.push(this.queueItem(m, url));
    }

    return Promise.all(promises);
  }

  queueItem(m, url) {
    this.logger.debug('QUEUE_ITEM', url);
    if (!url) {
      this.logger.debug('no valid url');
      return m.reply('Please give me a URL to play');
    }

    const guildId = m.guild.id;

    Utils.fetchYoutubeInfo(url).then((obj) => {
      this.logger.debug('fetchYoutubeInfo promise resolve');

      if (obj['_type'] === 'playlist') {
        const promises = [];

        for (const entry of obj.entries) {
          promises.push(this.addItemToQueue(guildId, entry));
        }

        return promises;
      }

      return this.addItemToQueue(guildId, obj);
    }).catch((err) => {
      if (err) {
        this.logger.debug('error pulling youtube data', err.stack);
      }

      return m.reply('Sorry, I was not able to queue that song.');
    });
  }

  addItemToQueue(guildId, obj) {
    const fields = [
      { from: 'title' },
      { from: 'display_id' },
      { from: 'duration' },
      { from: 'acodec' },
      { from: 'url' },
      { from: 'ext', to: 'extension' },
    ];
    const info = fields.reduce((info, field) => {
      info[field.to || field.from] = obj[field.from];
      return info;
    }, {});
    const formats = [obj.formats.find(elem => elem.format_id === obj.format_id)];

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

    return this.queueSave(guildId, record).then(this.afterRedisSave.bind(this, m));
  }

  queueSave(guildId, record) {
    const redisKey = this.getRedisKey(guildId, 'music_queue');

    return this.redisClient.rpushAsync(redisKey, JSON.stringify(record)).then(() => {
      this.logger.debug('saved to redis');

      return record;
    });
  }

  onSearch(m, args) {
    const text = args.join(' ');

    return this.search.byAnyField(text).then((results) =>
      results.slice(0, 5).map((entry) => {
        const filePath = path.join(this.settings.mpd.baseDirectory, entry.file);
        return { id: filePath, title: entry.file };
      })
    ).then((entries) => {
      const redisKey = this.getRedisKey(m.guild.id, `${m.channel.id}.search`);
      this.redisClient.set(redisKey, JSON.stringify(entries));

      const response = entries.map((item, i) => `**${i + 1}:** ${item.title}`).join('\n');
      return m.channel.sendMessage(response);
{'fraction_non_alphanumeric': 0.16513413593705564, 'fraction_numerical': 0.0007583657218693715, 'mean_word_length': 2.590878148400272, 'pattern_counts': {'":': 0, '<': 3, '<?xml version=': 0, '>': 25, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2047003', 'n_tokens_mistral': 3520, 'n_tokens_neox': 3416, 'n_words': 647}