nypublicradio/nypr-audio-services

View on GitHub
addon/services/dj.js

Summary

Maintainability
A
1 hr
Test Coverage
B
89%
import { A } from '@ember/array';
import { bind } from '@ember/runloop';
import Service, { inject as service } from '@ember/service';
import RSVP from 'rsvp';
import { get, set } from '@ember/object';
import { and, not, reads } from '@ember/object/computed';

/* DJ knows how to play anything. Pass it a stream/story PK, or pass it a
stream/story model and DJ will queue it up on the hifi with the appropriate
metadata inserted */

const STREAMS = ['wqxr', 'q2', 'wqxr-special', 'wnyc-fm939', 'wnyc-am820', 'njpr', 'jonathan-channel', 'special-events-stream', 'wqxr-special2', 'takeaway', 'wqxr-special2-same-as-holiday', 'indivisible'];

export default Service.extend({
  hifi                : service(),
  store               : service(),
  actionQueue         : service(),
  listenAnalytics     : service(),

  noErrors            : not('hasErrors'),
  showPlayer          : and('noErrors', 'playedOnce'),
  playedOnce          : false,

  /* So components can just depend on DJ, and not DJ + hifi (for better testing)*/
  currentSound        : reads('hifi.currentSound'),
  currentContentModel : reads('currentSound.metadata.contentModel'),
  currentContentId    : reads('currentSound.metadata.contentId'),
  currentContentType  : reads('currentSound.metadata.contentModelType'),
  isReady             : reads('hifi.isReady'),
  isPlaying           : reads('hifi.isPlaying'),

  init() {
    this._super(...arguments);
    let actionQueue = get(this, 'actionQueue');
    let hifi        = get(this, 'hifi');
    hifi.on('current-sound-changed', () => {
      if (get(this, 'isDestroying') || get(this, 'isDestroyed')) {
        return;
      }
      this.set('playedOnce', true);
    });

    actionQueue.addAction(hifi, 'audio-ended', {priority: 1, name: 'segmented-audio'}, bind(this, this.playSegmentedAudio));

    this.set('currentlyLoadingIds', []); // so loading buttons can get updated when the request starts
    this.initCurrentLoadingIdsWatcher();
  },

  initCurrentLoadingIdsWatcher() {
    let hifi = get(this, 'hifi');
    hifi.on('new-load-request', ({loadPromise, options}) => {
      let currentlyLoadingIds = A(get(this, 'currentlyLoadingIds'));
      let id = String(get(options, 'metadata.contentId'));

      if (id) {
        currentlyLoadingIds.push(id);
        set(this, 'currentlyLoadingIds', currentlyLoadingIds.uniq());
      }

      loadPromise.finally(() => {
        set(this, 'currentlyLoadingIds', currentlyLoadingIds.without(id));
      });
    });
  },

  playSegmentedAudio(sound) {
    let story    = get(sound, 'metadata.contentModel');

    if (story && get(story, 'segmentedAudio') && story.hasNextSegment()) {
      story.getNextSegment(); // trigger next segment
      this.play(story, {position: 0});
      return true;
    }
  },

  fetchRecord(itemIdOrItem) {
    let modelName = this.itemModelName(itemIdOrItem);
    if (typeof(itemIdOrItem) === 'string') {
      return get(this, 'store').findRecord(modelName, itemIdOrItem);
    }
    else {
      return RSVP.Promise.resolve(itemIdOrItem);
    }
  },

  itemModelName(itemIdOrItem) {
    if (typeof(itemIdOrItem) === 'string') {
      return (STREAMS.includes(itemIdOrItem) ? 'stream': 'story');
    }
    else { // could be a model, detect if model or stream
      return (get(itemIdOrItem, 'constructor.modelName') || get(itemIdOrItem, 'modelName'));
    }
  },

  itemId(itemIdOrItem) {
    if (typeof(itemIdOrItem) === 'string') {
      return itemIdOrItem;
    }
    else {
      return itemIdOrItem.id;
    }
  },

  isNewPlay(itemIdOrItem) {
    return get(this, 'hifi.currentSound.metadata.contentId') !== this.itemId(itemIdOrItem);
  },

  play(itemIdOrItem, options = {}) {
    let itemModelName   = this.itemModelName(itemIdOrItem);
    let itemId          = this.itemId(itemIdOrItem);
    let recordRequest   = this.fetchRecord(itemIdOrItem);
    let newPlay         = this.isNewPlay(itemIdOrItem);

    let { playContext, position, autoPlayChoice, metadata = {} } = options;

    let audioUrlPromise = recordRequest.then(s => {
      // TODO: Make this consistent between models
      if (itemModelName === 'story') {
        return newPlay ? s.resetSegments() : s.getCurrentSegment();
      }
      else {
        return get(s, 'urls');
      }
    });

    let listenAnalytics = get(this, 'listenAnalytics');

    metadata.contentId = itemId;
    metadata.contentModelType = itemModelName;
    metadata.playContext = playContext;
    metadata.autoPlayChoice = autoPlayChoice;

    let playRequest = get(this, 'hifi').play(audioUrlPromise, {metadata, position});
    // This should resolve around the same time, and then set the metadata
    recordRequest.then(story => {
      set(metadata, 'contentModel', story);
      if (story.forListenAction) {
        story.forListenAction().then(data => set(metadata, 'analytics', data));
      }
    });
    playRequest.then(({sound, failures}) => {
      this.set('hasErrors', false);
      listenAnalytics.trackAllCodecFailures(failures, sound);
    }).catch(e => {
      this.set('hasErrors', true);
      listenAnalytics.trackSoundFailure(e);
    });

    return playRequest;
  },

  pause() {
    get(this, 'hifi').pause();
  },

  addBrowserId(id) {
    get(this, 'hifi').on('pre-load', urlsToTry => {
      urlsToTry.forEach((val, i) => {
        // `val` can be a string value or an object with a `url` key
        let url = typeof val === 'string' ? val : val.url;
        let parts = url.split('?');
        if (parts.length > 2) {
          // malformed query
          let [base, ...query] = parts;
          parts = [base, query.join('&')];
          url = `${base}?${parts[1]}`;
        }
        if (parts[1] && !parts[1].match('nyprBrowserId')) {
          // there's a query string that doesn't have browser_id
          url += `&nyprBrowserId=${id}`;
        } else if (!parts[1]){ // no query
          url += `?nyprBrowserId=${id}`;
        }
        if (val.url) {
          val.url = url;
        } else {
          val = url;
        }
        urlsToTry[i] = val;
      });
    });
  }
});