<commit_before>/**
 * Copyright 2015 CANAL+ Group
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

var Promise_ = require("canal-js-utils/promise");

var _ = require("canal-js-utils/misc");
var log = require("canal-js-utils/log");
var { CompositeDisposable, BehaviorSubject, Observable, Subject } = require("canal-js-utils/rx");
var { combineLatest, defer, zip } = Observable;
var { on } = require("canal-js-utils/rx-ext");
var EventEmitter = require("canal-js-utils/eventemitter");
var debugPane = require("../utils/debug");
var assert = require("canal-js-utils/assert");

var {
  HTMLVideoElement_,
  exitFullscreen,
  requestFullscreen,
  isFullscreen,
  onFullscreenChange
} = require("./compat");

var { timingsSampler, toWallClockTime, fromWallClockTime, getLiveGap } = require("./timings");
var { InitializationSegmentCache } = require("./cache");
var { getSize, getLoaded } = require("./ranges");
var { parseTimeFragment } = require("./time-fragment");
var DeviceEvents = require("./device-events");
var manifestHelpers = require("./manifest");
// TODO(pierre): separate transports from main build
var Transports = require("../net");
var PipeLines = require("./pipelines");
var Adaptive = require("../adaptive");
var Stream = require("./stream");
var EME = require("./eme");

var PLAYER_STOPPED   = "STOPPED";
var PLAYER_LOADED    = "LOADED";
var PLAYER_LOADING   = "LOADING";
var PLAYER_PLAYING   = "PLAYING";
var PLAYER_PAUSED    = "PAUSED";
var PLAYER_ENDED     = "ENDED";
var PLAYER_BUFFERING = "BUFFERING";
var PLAYER_SEEKING   = "SEEKING";

var CHEAT = "&&((%'%'"; // Konami Code

function createDirectFileManifest() {
  return {
    isLive: false,
    duration: Infinity,
  };
}

function assertMan(player) {
  assert(player.man, "player: no manifest loaded");
}

function filterStreamByType(stream, type) {
  return stream.filter(o => o.type == type).pluck("value");
}

class Player extends EventEmitter {

  constructor(options) {
    var {
      videoElement,
      transport,
      transportOptions,
      initVideoBitrate,
      initAudioBitrate
    } = options;

    this.defaultTransport = transport;
    this.defaultTransportOptions = transportOptions || {};

    if (!videoElement)
      videoElement = document.createElement("video");

    assert((videoElement instanceof HTMLVideoElement_),
      "requires an actual HTMLVideoElement");

    this.version = __RX_PLAYER_VERSION_PLACEHOLDER__;
    this.video = videoElement;

    // fullscreen change
    this.fullscreen = onFullscreenChange(videoElement)
      .subscribe(() => this.trigger("fullscreenChange", this.isFullscreen()));

    // playing state change
    this.playing = new BehaviorSubject();

    // multicaster forwarding all streams events
    this.stream = new Subject();

    var { createPipelines, metrics } = PipeLines();

    var timings = timingsSampler(videoElement);
    var deviceEvents = DeviceEvents(videoElement);

    this.createPipelines = createPipelines;
    this.metrics = metrics;
    this.timings = timings;

    this.adaptive = Adaptive(metrics, timings, deviceEvents, {
      initVideoBitrate,
      initAudioBitrate
    });

    // volume muted memory
    this.muted = 0.1;

    // states
    this._setState(PLAYER_STOPPED);
    this.resetStates();

    this.log = log;

    on(document, "keydown")
      .map(e => String.fromCharCode(e.which).toLowerCase())
      .scan("", (w, l) => (w + l).slice(-CHEAT.length))
      .filter(w => w == CHEAT)
      .subscribe(() => this.showDebug());
  }

  resetStates() {
    this.man = null;
    this.reps = { video: null, audio: null, text: null };
    this.adas = { video: null, audio: null, text: null };
    this.evts = {};
    this.frag = { start: null, end: null };
  }

  _clear() {
    if (this.subscriptions) {
      this.subscriptions.dispose();
      this.subscriptions = null;
    }
  }

  stop() {
    if (this.state !== PLAYER_STOPPED) {
      this.resetStates();
      this._clear();
      this._setState(PLAYER_STOPPED);
    }
  }

  dispose() {
    this.stop();
    this.metrics.dispose();
    this.adaptive.dispose();
    this.fullscreen.dispose();
    this.stream.dispose();

    this.metrics = null;
    this.adaptive = null;
    this.fullscreen = null;
    this.stream = null;

    this.timings = null;
    this.createPipelines = null;
    this.video = null;
  }

  __recordState(type, value) {
    var prev = this.evts[type];
    if (prev !== value) {
      this.evts[type] = value;
      this.trigger(`${type}Change`, value);
    }
  }

  _parseOptions(opts) {
    var {
      transport,
      transportOptions,
      url,
      keySystems,
      timeFragment,
      subtitles,
      autoPlay,
      directFile
    } = _.defaults(_.cloneObject(opts), {
      transport: this.defaultTransport,
      transportOptions: {},
      keySystems: [],
      timeFragment: {},
      subtitles: [],
      autoPlay: false,
      directFile: false,
    });

    timeFragment = parseTimeFragment(timeFragment);

    if (_.isString(transport))
      transport = Transports[transport];

    if (_.isFunction(transport))
      transport = transport(_.defaults(transportOptions, this.defaultTransportOptions));

    assert(transport, "transport: transport " + opts.transport + " is not supported");

    if (directFile)
      directFile = createDirectFileManifest();

    return { url, keySystems, subtitles, timeFragment, autoPlay, transport, directFile };
  }

  loadVideo(options = {}) {
    options = this._parseOptions(options);
    log.info("loadvideo", options);

    var {
      url,
      keySystems,
      subtitles,
      timeFragment,
      autoPlay,
      transport,
      directFile
    } = options;

    this.stop();
    this.frag = timeFragment;
    this.playing.onNext(autoPlay);

    var pipelines = this.createPipelines(transport, {
      segment: {
        cache: InitializationSegmentCache()
      }
    });

    var { adaptive, timings, video } = this;
    var stream;
    try {
      stream = Stream({
        url,
        keySystems,
        subtitles,
        timings,
        timeFragment,
        adaptive,
        pipelines,
        videoElement: video,
        autoPlay,
        directFile,
      });
    }
    catch(err) {
      stream = defer(() => { throw err; });
    }

    stream = stream.publish();

    var segments = filterStreamByType(stream, "segment");
    var manifest = filterStreamByType(stream, "manifest").take(1);

    var stalled = filterStreamByType(stream, "stalled").startWith(null);
    var canPlay = filterStreamByType(stream, "loaded").filter(v => v === true);

    var loaded = directFile
      ? canPlay.take(1)
      : zip(canPlay,
          segments.filter(({ adaptation }) => adaptation.type == "audio"),
          segments.filter(({ adaptation }) => adaptation.type == "video"),
          _.noop).take(1);

    var stateChanges = loaded.map(PLAYER_LOADED)
      .concat(combineLatest(this.playing, stalled,
        (isPlaying, isStalled) => {
          if (isStalled)
            return (isStalled.name == "seeking")
              ? PLAYER_SEEKING
              : PLAYER_BUFFERING;

          if (isPlaying)
            return PLAYER_PLAYING;

          return PLAYER_PAUSED;
        })
      )
      .changes()
      .startWith(PLAYER_LOADING);

    this.subscriptions = new CompositeDisposable();
    var subs = [
      on(video, ["play", "pause"])
        .each(evt => this.playing.onNext(evt.type == "play")),

      segments.each((segment) => {
        var type = segment.adaptation.type;

        var rep = segment.representation;
        var ada = segment.adaptation;
        this.reps[type] = rep;
        this.adas[type] = ada;

        if (type == "text") {
          this.__recordState("subtitle", ada.lang);
        }
        if (type == "video") {
          this.__recordState("videoBitrate", rep.bitrate);
        }
        if (type == "audio") {
          this.__recordState("language", ada.lang);
          this.__recordState("audioBitrate", rep.bitrate);
        }

        this.trigger("progress", segment);
      }),

      manifest.each(m => {
        this.man = m;
        this.trigger("manifestChange", m);
      }),

      stateChanges
        .each(s => this._setState(s)),

      timings
        .each(t => {
          if (!this.man)
            return;

          if (this.man.isLive && t.ts > 0) {
            t.wallClockTime = toWallClockTime(t.ts, this.man);
            t.liveGap = getLiveGap(t.ts, this.man);
          }

          this.trigger("currentTimeChange", t);
        }),

      stream.subscribe(
        () => {},
        (e) => {
          this.resetStates();
          this.trigger("error", e);
          this._setState(PLAYER_STOPPED);
          this._clear();
        },
        () => {
          this.resetStates();
          this._setState(PLAYER_ENDED);
          this._clear();
        }
      ),

      stream.subscribe(
        n => this.stream.onNext(n),
        e => this.stream.onNext({ type: "error", value: e })
      ),

      stream.connect()
    ];

    _.each(subs, s => this.subscriptions.add(s));

    return loaded.toPromise();
  }

  _setState(s) {
    if (this.state !== s) {
      this.state = s;
      this.trigger("playerStateChange", s);
    }
  }

  getManifest() {
    return this.man;
  }

  getVideoElement() {
    return this.video;
  }

  getNativeTextTrack() {
    return this.video.textTracks[0];
  }

  getPlayerState() {
    return this.state;
  }

  isLive() {
    assertMan(this);
    return this.man.isLive;
  }

  getUrl() {
    assertMan(this);
    return this.man.baseURL;
  }

  getVideoDuration() {
    return this.video.duration;
  }

  getVideoLoadedTime() {
    return getSize(this.video.currentTime, this.video.buffered);
  }

  getVideoPlayedTime() {
    return getLoaded(this.video.currentTime, this.video.buffered);
  }

  getCurrentTime() {
    if (!this.man)
      return NaN;
    var ct = this.video.currentTime;
    if (this.man.isLive) {
      return toWallClockTime(ct, this.man);
    } else {
      return ct;
    }
  }

  getStartTime() {
    return this.frag.start;
  }

  getEndTime() {
    return this.frag.end;
  }

  getPlaybackRate() {
    return this.video.playbackRate;
  }

  getVolume() {
    return this.video.volume;
  }

  isFullscreen() {
    return isFullscreen();
  }

  getAvailableLanguages() {
    return this.man && manifestHelpers.getAvailableLanguages(this.man) || [];
  }

  getAvailableSubtitles() {
    return this.man && manifestHelpers.getAvailableSubtitles(this.man) || [];
  }

  getLanguage() {
    return this.adaptive.getLanguage();
  }

  getSubtitle() {
    return this.adaptive.getSubtitle();
  }

  getAvailableVideoBitrates() {
    var video = this.man && this.man.adaptations.video[0];
    return (video && video.bitrates) || [];
  }

  getAvailableAudioBitrates() {
    var audio = this.adas.audio;
    return (audio && audio.bitrates) || [];
  }

  getVideoBitrate() {
    return this.evts.videoBitrate;
  }

  getAudioBitrate() {
    return this.evts.audioBitrate;
  }

  getVideoMaxBitrate() {
    return this.adaptive.getVideoMaxBitrate();
  }

  getAudioMaxBitrate() {
    return this.adaptive.getAudioMaxBitrate();
  }

  getVideoBufferSize() {
    return this.adaptive.getVideoBufferSize();
  }

  getAudioBufferSize() {
    return this.adaptive.getAudioBufferSize();
  }

  getAverageBitrates() {
    return this.adaptive.getAverageBitrates();
  }

  getMetrics() {
    return this.metrics;
  }

  getTimings() {
    return this.timings;
  }

  play() {
    this.video.play();
  }

  pause() {
    this.video.pause();
  }

  setPlaybackRate(rate) {
    return new Promise_(res => res(this.video.playbackRate = rate));
  }

  goToStart() {
    return this.seekTo(this.getStartTime());
  }

  seekTo(time) {
    return new Promise_(res => {
      assert(this.man);
      var currentTs = this.video.currentTime;
      if (this.man.isLive) time = fromWallClockTime(time, this.man);
      if (time !== currentTs) {
        log.info("seek to", time);
        res(this.video.currentTime = time);
      } else {
        res(currentTs);
      }
    });
  }

  setFullscreen(toggle = true) {
    if (toggle === false)
      exitFullscreen();
    else
      requestFullscreen(this.video);
  }

  setVolume(volume) {
    if (volume !== this.video.volume) {
      this.video.volume = volume;
      this.trigger("volumeChange", volume);
    }
  }

  mute() {
    this.muted = this.getVolume() || 0.1;
    this.setVolume(0);
  }

  unMute() {
    var vol = this.getVolume();
    if (vol === 0) this.setVolume(this.muted);
  }

  setLanguage(lng) {
    // TODO(pierre): proper promise
    return new Promise_(res => {
      assert(_.contains(this.getAvailableLanguages(), lng), "player: unknown language");
      res(this.adaptive.setLanguage(lng));
    });
  }

  setSubtitle(sub) {
    // TODO(pierre): proper promise
    return new Promise_(res => {
      assert(!sub || _.contains(this.getAvailableSubtitles(), sub), "player: unknown subtitle");
      res(this.adaptive.setSubtitle(sub || ""));
    })
      .then(() => {
        if (!sub)
          this.__recordState("subtitle", null);
      });
  }

  setVideoBitrate(btr) {
    // TODO(pierre): proper promise
    return new Promise_(res => {
      assert(btr === 0 || _.contains(this.getAvailableVideoBitrates(), btr), "player: video bitrate unavailable");
      res(this.adaptive.setVideoBitrate(btr));
    });
  }

  setAudioBitrate(btr) {
    // TODO(pierre): proper promise
    return new Promise_(res => {
      assert(btr === 0 || _.contains(this.getAvailableAudioBitrates(), btr), "player: audio bitrate unavailable");
      res(this.adaptive.setAudioBitrate(btr));
    });
  }

  setVideoMaxBitrate(btr) {
    // TODO(pierre): proper promise
    return new Promise_(res => {
      res(this.adaptive.setVideoMaxBitrate(btr));
    });
  }

  setAudioMaxBitrate(btr) {
    // TODO(pierre): proper promise
    return new Promise_(res => {
      res(this.adaptive.setAudioMaxBitrate(btr));
    });
  }

  setVideoBufferSize(size) {
    // TODO(pierre): proper promise
    return new Promise_(res => res(this.adaptive.setVideoBufferSize(size)));
  }

  setAudioBufferSize(size) {
    // TODO(pierre): proper promise
    return new Promise_(res => res(this.adaptive.setAudioBufferSize(size)));
  }

  getStreamObservable() {
    return this.stream;
  }

  getDebug() {
    return debugPane.getDebug(this);
  }

  showDebug() {
    debugPane.showDebug(this, this.video);
  }

  hideDebug() {
    debugPane.hideDebug();
  }

  toggleDebug() {
    debugPane.toggleDebug(this,this.video);
  }
  getCurrentKeySystem() {
    return EME.getCurrentKeySystem();
  }
}

module.exports = Player;
<commit_msg>player: fix "manifestChange" event<commit_after>/**
 * Copyright 2015 CANAL+ Group
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

var Promise_ = require("canal-js-utils/promise");

var _ = require("canal-js-utils/misc");
var log = require("canal-js-utils/log");
var { CompositeDisposable, BehaviorSubject, Observable, Subject } = require("canal-js-utils/rx");
var { combineLatest, defer, zip } = Observable;
var { on } = require("canal-js-utils/rx-ext");
var EventEmitter = require("canal-js-utils/eventemitter");
var debugPane = require("../utils/debug");
var assert = require("canal-js-utils/assert");

var {
  HTMLVideoElement_,
  exitFullscreen,
  requestFullscreen,
  isFullscreen,
  onFullscreenChange
} = require("./compat");

var { timingsSampler, toWallClockTime, fromWallClockTime, getLiveGap } = require("./timings");
var { InitializationSegmentCache } = require("./cache");
var { getSize, getLoaded } = require("./ranges");
var { parseTimeFragment } = require("./time-fragment");
var DeviceEvents = require("./device-events");
var manifestHelpers = require("./manifest");
// TODO(pierre): separate transports from main build
var Transports = require("../net");
var PipeLines = require("./pipelines");
var Adaptive = require("../adaptive");
var Stream = require("./stream");
var EME = require("./eme");

var PLAYER_STOPPED   = "STOPPED";
var PLAYER_LOADED    = "LOADED";
var PLAYER_LOADING   = "LOADING";
var PLAYER_PLAYING   = "PLAYING";
var PLAYER_PAUSED    = "PAUSED";
var PLAYER_ENDED     = "ENDED";
var PLAYER_BUFFERING = "BUFFERING";
var PLAYER_SEEKING   = "SEEKING";

var CHEAT = "&&((%'%'"; // Konami Code

function createDirectFileManifest() {
  return {
    isLive: false,
    duration: Infinity,
  };
}

function assertMan(player) {
  assert(player.man, "player: no manifest loaded");
}

function filterStreamByType(stream, type) {
  return stream.filter(o => o.type == type).pluck("value");
}

class Player extends EventEmitter {

  constructor(options) {
    var {
      videoElement,
      transport,
      transportOptions,
      initVideoBitrate,
      initAudioBitrate
    } = options;

    this.defaultTransport = transport;
    this.defaultTransportOptions = transportOptions || {};

    if (!videoElement)
      videoElement = document.createElement("video");

    assert((videoElement instanceof HTMLVideoElement_),
      "requires an actual HTMLVideoElement");

    this.version = __RX_PLAYER_VERSION_PLACEHOLDER__;
    this.video = videoElement;

    // fullscreen change
    this.fullscreen = onFullscreenChange(videoElement)
      .subscribe(() => this.trigger("fullscreenChange", this.isFullscreen()));

    // playing state change
    this.playing = new BehaviorSubject();

    // multicaster forwarding all streams events
    this.stream = new Subject();

    var { createPipelines, metrics } = PipeLines();

    var timings = timingsSampler(videoElement);
    var deviceEvents = DeviceEvents(videoElement);

    this.createPipelines = createPipelines;
    this.metrics = metrics;
    this.timings = timings;

    this.adaptive = Adaptive(metrics, timings, deviceEvents, {
      initVideoBitrate,
      initAudioBitrate
    });

    // volume muted memory
    this.muted = 0.1;

    // states
    this._setState(PLAYER_STOPPED);
    this.resetStates();

    this.log = log;

    on(document, "keydown")
      .map(e => String.fromCharCode(e.which).toLowerCase())
      .scan("", (w, l) => (w + l).slice(-CHEAT.length))
      .filter(w => w == CHEAT)
      .subscribe(() => this.showDebug());
  }

  resetStates() {
    this.man = null;
    this.reps = { video: null, audio: null, text: null };
    this.adas = { video: null, audio: null, text: null };
    this.evts = {};
    this.frag = { start: null, end: null };
  }

  _clear() {
    if (this.subscriptions) {
      this.subscriptions.dispose();
      this.subscriptions = null;
    }
  }

  stop() {
    if (this.state !== PLAYER_STOPPED) {
      this.resetStates();
      this._clear();
      this._setState(PLAYER_STOPPED);
    }
  }

  dispose() {
    this.stop();
    this.metrics.dispose();
    this.adaptive.dispose();
    this.fullscreen.dispose();
    this.stream.dispose();

    this.metrics = null;
    this.adaptive = null;
    this.fullscreen = null;
    this.stream = null;

    this.timings = null;
    this.createPipelines = null;
    this.video = null;
  }

  __recordState(type, value) {
    var prev = this.evts[type];
    if (prev !== value) {
      this.evts[type] = value;
      this.trigger(`${type}Change`, value);
    }
  }

  _parseOptions(opts) {
    var {
      transport,
      transportOptions,
      url,
      keySystems,
      timeFragment,
      subtitles,
      autoPlay,
      directFile
    } = _.defaults(_.cloneObject(opts), {
      transport: this.defaultTransport,
      transportOptions: {},
      keySystems: [],
      timeFragment: {},
      subtitles: [],
      autoPlay: false,
      directFile: false,
    });

    timeFragment = parseTimeFragment(timeFragment);

    if (_.isString(transport))
      transport = Transports[transport];

    if (_.isFunction(transport))
      transport = transport(_.defaults(transportOptions, this.defaultTransportOptions));

    assert(transport, "transport: transport " + opts.transport + " is not supported");

    if (directFile)
      directFile = createDirectFileManifest();

    return { url, keySystems, subtitles, timeFragment, autoPlay, transport, directFile };
  }

  loadVideo(options = {}) {
    options = this._parseOptions(options);
    log.info("loadvideo", options);

    var {
      url,
      keySystems,
      subtitles,
      timeFragment,
      autoPlay,
      transport,
      directFile
    } = options;

    this.stop();
    this.frag = timeFragment;
    this.playing.onNext(autoPlay);

    var pipelines = this.createPipelines(transport, {
      segment: {
        cache: InitializationSegmentCache()
      }
    });

    var { adaptive, timings, video } = this;
    var stream;
    try {
      stream = Stream({
        url,
        keySystems,
        subtitles,
        timings,
        timeFragment,
        adaptive,
        pipelines,
        videoElement: video,
        autoPlay,
        directFile,
      });
    }
    catch(err) {
      stream = defer(() => { throw err; });
    }

    stream = stream.publish();

    var segments = filterStreamByType(stream, "segment");
    var manifests = filterStreamByType(stream, "manifest");

    var stalled = filterStreamByType(stream, "stalled").startWith(null);
    var canPlay = filterStreamByType(stream, "loaded").filter(v => v === true);

    var loaded;

    if (directFile) {
      loaded = canPlay;
    } else {
      loaded = zip(
        canPlay,
        filterStreamByType(segments.pluck("adaptation"), "audio"),
        filterStreamByType(segments.pluck("adaptation"), "video"),
        _.noop);
    }

    loaded = loaded.take(1);

    var stateChanges = loaded.map(PLAYER_LOADED)
      .concat(combineLatest(this.playing, stalled,
        (isPlaying, isStalled) => {
          if (isStalled)
            return (isStalled.name == "seeking")
              ? PLAYER_SEEKING
              : PLAYER_BUFFERING;

          if (isPlaying)
            return PLAYER_PLAYING;

          return PLAYER_PAUSED;
        })
      )
      .changes()
      .startWith(PLAYER_LOADING);

    this.subscriptions = new CompositeDisposable();
    var subs = [
      on(video, ["play", "pause"])
        .each(evt => this.playing.onNext(evt.type == "play")),

      segments.each((segment) => {
        var type = segment.adaptation.type;

        var rep = segment.representation;
        var ada = segment.adaptation;
        this.reps[type] = rep;
        this.adas[type] = ada;

        if (type == "text") {
          this.__recordState("subtitle", ada.lang);
        }
        if (type == "video") {
          this.__recordState("videoBitrate", rep.bitrate);
        }
        if (type == "audio") {
          this.__recordState("language", ada.lang);
          this.__recordState("audioBitrate", rep.bitrate);
        }

        this.trigger("progress", segment);
      }),

      manifests.each(m => {
        this.man = m;
        this.trigger("manifestChange", m);
      }),

      stateChanges
        .each(s => this._setState(s)),

      timings
        .each(t => {
          if (!this.man)
            return;

          if (this.man.isLive && t.ts > 0) {
            t.wallClockTime = toWallClockTime(t.ts, this.man);
            t.liveGap = getLiveGap(t.ts, this.man);
          }

          this.trigger("currentTimeChange", t);
        }),

      stream.subscribe(
        () => {},
        (e) => {
          this.resetStates();
          this.trigger("error", e);
          this._setState(PLAYER_STOPPED);
          this._clear();
        },
        () => {
          this.resetStates();
          this._setState(PLAYER_ENDED);
          this._clear();
        }
      ),

      stream.subscribe(
        n => this.stream.onNext(n),
        e => this.stream.onNext({ type: "error", value: e })
      ),

      stream.connect()
    ];

    _.each(subs, s => this.subscriptions.add(s));

    return loaded.toPromise();
  }

  _setState(s) {
    if (this.state !== s) {
      this.state = s;
      this.trigger("playerStateChange", s);
    }
  }

  getManifest() {
    return this.man;
  }

  getVideoElement() {
    return this.video;
  }

  getNativeTextTrack() {
    return this.video.textTracks[0];
  }

  getPlayerState() {
    return this.state;
  }

  isLive() {
    assertMan(this);
    return this.man.isLive;
  }

  getUrl() {
    assertMan(this);
    return this.man.baseURL;
  }

  getVideoDuration() {
    return this.video.duration;
  }

  getVideoLoadedTime() {
    return getSize(this.video.currentTime, this.video.buffered);
  }

  getVideoPlayedTime() {
    return getLoaded(this.video.currentTime, this.video.buffered);
  }

  getCurrentTime() {
    if (!this.man)
      return NaN;
    var ct = this.video.currentTime;
    if (this.man.isLive) {
      return toWallClockTime(ct, this.man);
    } else {
      return ct;
    }
  }

  getStartTime() {
    return this.frag.start;
  }

  getEndTime() {
    return this.frag.end;
  }

  getPlaybackRate() {
    return this.video.playbackRate;
  }

  getVolume() {
    return this.video.volume;
  }

  isFullscreen() {
    return isFullscreen();
  }

  getAvailableLanguages() {
    return this.man && manifestHelpers.getAvailableLanguages(this.man) || [];
  }

  getAvailableSubtitles() {
    return this.man && manifestHelpers.getAvailableSubtitles(this.man) || [];
  }

  getLanguage() {
    return this.adaptive.getLanguage();
  }

  getSubtitle() {
    return this.adaptive.getSubtitle();
  }

  getAvailableVideoBitrates() {
    var video = this.man && this.man.adaptations.video[0];
    return (video && video.bitrates) || [];
  }

  getAvailableAudioBitrates() {
    var audio = this.adas.audio;
    return (audio && audio.bitrates) || [];
  }

  getVideoBitrate() {
    return this.evts.videoBitrate;
  }

  getAudioBitrate() {
    return this.evts.audioBitrate;
  }

  getVideoMaxBitrate() {
    return this.adaptive.getVideoMaxBitrate();
  }

  getAudioMaxBitrate() {
    return this.adaptive.getAudioMaxBitrate();
  }

  getVideoBufferSize() {
    return this.adaptive.getVideoBufferSize();
  }

  getAudioBufferSize() {
    return this.adaptive.getAudioBufferSize();
  }

  getAverageBitrates() {
    return this.adaptive.getAverageBitrates();
  }

  getMetrics() {
    return this.metrics;
  }

  getTimings() {
    return this.timings;
  }

  play() {
    this.video.play();
  }

  pause() {
    this.video.pause();
  }

  setPlaybackRate(rate) {
    return new Promise_(res => res(this.video.playbackRate = rate));
  }

  goToStart() {
    return this.seekTo(this.getStartTime());
  }

  seekTo(time) {
    return new Promise_(res => {
      assert(this.man);
      var currentTs = this.video.currentTime;
      if (this.man.isLive) time = fromWallClockTime(time, this.man);
      if (time !== currentTs) {
        log.info("seek to", time);
        res(this.video.currentTime = time);
      } else {
        res(currentTs);
      }
    });
  }

  setFullscreen(toggle = true) {
    if (toggle === false)
      exitFullscreen();
    else
      requestFullscreen(this.video);
  }

  setVolume(volume) {
    if (volume !== this.video.volume) {
      this.video.volume = volume;
      this.trigger("volumeChange", volume);
    }
  }

  mute() {
    this.muted = this.getVolume() || 0.1;
    this.setVolume(0);
  }

  unMute() {
    var vol = this.getVolume();
    if (vol === 0) this.setVolume(this.muted);
  }

  setLanguage(lng) {
    // TODO(pierre): proper promise
    return new Promise_(res => {
      assert(_.contains(this.getAvailableLanguages(), lng), "player: unknown language");
      res(this.adaptive.setLanguage(lng));
    });
  }

  setSubtitle(sub) {
    // TODO(pierre): proper promise
    return new Promise_(res => {
      assert(!sub || _.contains(this.getAvailableSubtitles(), sub), "player: unknown subtitle");
      res(this.adaptive.setSubtitle(sub || ""));
    })
      .then(() => {
        if (!sub)
          this.__recordState("subtitle", null);
      });
  }

  setVideoBitrate(btr) {
    // TODO(pierre): proper promise
    return new Promise_(res => {
      assert(btr === 0 || _.contains(this.getAvailableVideoBitrates(), btr), "player: video bitrate unavailable");
      res(this.adaptive.setVideoBitrate(btr));
    });
  }

  setAudioBitrate(btr) {
    // TODO(pierre): proper promise
    return new Promise_(res => {
      assert(btr === 0 || _.contains(this.getAvailableAudioBitrates(), btr), "player: audio bitrate unavailable");
      res(this.adaptive.setAudioBitrate(btr));
    });
  }

  setVideoMaxBitrate(btr) {
    // TODO(pierre): proper promise
    return new Promise_(res => {
      res(this.adaptive.setVideoMaxBitrate(btr));
    });
  }

  setAudioMaxBitrate(btr) {
    // TODO(pierre): proper promise
    return new Promise_(res => {
      res(this.adaptive.setAudioMaxBitrate(btr));
    });
  }

  setVideoBufferSize(size) {
    // TODO(pierre): proper promise
    return new Promise_(res => res(this.adaptive.setVideoBufferSize(size)));
  }

  setAudioBufferSize(size) {
    // TODO(pierre): proper promise
    return new Promise_(res => res(this.adaptive.setAudioBufferSize(size)));
  }

  getStreamObservable() {
    return this.stream;
  }

  getDebug() {
    return debugPane.getDebug(this);
  }

  showDebug() {
    debugPane.showDebug(this, this.video);
  }

  hideDebug() {
    debugPane.hideDebug();
  }

  toggleDebug() {
    debugPane.toggleDebug(this,this.video);
  }
  getCurrentKeySystem() {
    return EME.getCurrentKeySystem();
  }
}

module.exports = Player;
<|endoftext|>


<commit_before>// This is a manifest file that'll be compiled into application.js, which will include all the files
// listed below.
//
// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
// or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path.
//
// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
// compiled file. JavaScript code in this file should be added after the last require_* statement.
//
// Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details
// about supported directives.
//
//= require_tree ./channels
//= require jquery
//= require jquery_ujs
//= require d3
//= require react
//= require react_ujs
//= require components
//= require_tree .

$(document).ready(function(e) {

  if ($("#wr").length > 0 ){
    console.log("eutfoirejfgfcdafiojrg");
    setTimeout(checkForFirstPlayer, 2000)
  }

  (function blink() {
  $('#waiting').fadeOut(500).fadeIn(500, blink);
  })();

  if ($(".prize-header").length > 0){
    console.log("home page!")
    // timer
  }
});

  // var update_times = function(){
  //   $.ajax({
  //     url: '/restaurants/1',
  //     type: 'GET',
  //     dataType: 'JSON'
  //   })
  //   .done(function(response) {
  //     for (var i = 0; i < response.length; i++){
  //       var x = i+1;
  //       $("td.elapsed").html(response[i]);
  //     }

  //   })
  //   .fail(function() {
  //     console.log("error");
  //   });
  // }

    // var timer = setInterval(update_times, 1000);

      // function formatTime(time, num){
      //   var $elapsedTag = $("<td>", {class: num})
      // }

  function checkForFirstPlayer(){
    var id = $("#key").text()
    console.log(id);
    var checker;
    $.ajax({
      url: '/check',
      type: 'GET',
      dataType: 'JSON',
      data: {}
    })
    .done(function(response) {
      console.log(response)
      if (response.length === 2){
        url = "http://localhost:3000/rounds/key/" + id
        console.log(response);
        location.href=url;
      } else if (response.length === 1){
        console.log("you are alone")
        console.log(response);
        setTimeout(checkForFirstPlayer, 1000)
      }
    })
    .fail(function() {
      console.log("error");
    });

  }
// var timer = setInterval(update_times, 100000);
  // timer;
<commit_msg>Fix redirect route<commit_after>// This is a manifest file that'll be compiled into application.js, which will include all the files
// listed below.
//
// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
// or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path.
//
// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
// compiled file. JavaScript code in this file should be added after the last require_* statement.
//
// Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details
// about supported directives.
//
//= require_tree ./channels
//= require jquery
//= require jquery_ujs
//= require d3
//= require react
//= require react_ujs
//= require components
//= require_tree .

$(document).ready(function(e) {

  if ($("#wr").length > 0 ){
    console.log("eutfoirejfgfcdafiojrg");
    setTimeout(checkForFirstPlayer, 2000)
  }

  (function blink() {
  $('#waiting').fadeOut(500).fadeIn(500, blink);
  })();

  if ($(".prize-header").length > 0){
    console.log("home page!")
    // timer
  }
});

  // var update_times = function(){
  //   $.ajax({
  //     url: '/restaurants/1',
  //     type: 'GET',
  //     dataType: 'JSON'
  //   })
  //   .done(function(response) {
  //     for (var i = 0; i < response.length; i++){
  //       var x = i+1;
  //       $("td.elapsed").html(response[i]);
  //     }

  //   })
  //   .fail(function() {
  //     console.log("error");
  //   });
  // }

    // var timer = setInterval(update_times, 1000);

      // function formatTime(time, num){
      //   var $elapsedTag = $("<td>", {class: num})
      // }

  function checkForFirstPlayer(){
    var id = $("#key").text()
    console.log(id);
    var checker;
    $.ajax({
      url: '/check',
      type: 'GET',
      dataType: 'JSON',
      data: {}
    })
    .done(function(response) {
      console.log(response)
      if (response.length === 2){
        url = "https://waitr-app.herokuapp.com/rounds/key/" + id
        console.log(response);
        location.href=url;
      } else if (response.length === 1){
        console.log("you are alone")
        console.log(response);
        setTimeout(checkForFirstPlayer, 1000)
      }
    })
    .fail(function() {
      console.log("error");
    });

  }
// var timer = setInterval(update_times, 100000);
  // timer;
<|endoftext|>


<commit_before>'use strict'
module.exports = getFic

const fs = require('fs')
const url = require('url')

const Chapter = use('fic').Chapter
const FicStream = use('fic-stream')
const html = use('html-template-tag')
const progress = use('progress')
const Site = use('site')
const forEach = use('for-each')
const map = use('map')

function rewriteLinks (fic, chapter, handleLink) {
  chapter.$content.find('a').each((ii, a) => {
    const $a = chapter.$content.find(a)
    const startAs = $a.attr('href')
    if (!startAs) {
      $a.remove()
      return
    }
    if (startAs[0] === '#') return
    let src
    if (startAs.substr(0,4) !== 'http' && /^www[.]|[.]com$/.test(startAs)) {
      src = 'http://' + startAs
    } else if (/^[\w.]+(com|org|net)[/]/.test(startAs)) {
      src = 'http://' + startAs
    } else {
      src = chapter.base ? url.resolve(chapter.base, startAs) : startAs
    }
    const newHref = handleLink(fic.normalizeLink(src, chapter.base), $a)
    $a.attr('href', newHref || src)
  })
}

function rewriteIframes (fic, chapter) {
  chapter.$content.find('iframe').each((ii, iframe) => {
    const $iframe = chapter.$content.find(iframe)
    const src = url.resolve(chapter.base, $iframe.attr('src'))
    $iframe.replaceWith(`<a href="${src}">Video Link</a>`)
  })
}

function rewriteImages (fic, chapter, handleImage) {
  chapter.$content.find('img').each((ii, img) => {
    const $img = chapter.$content.find(img)
    const startAs = ($img.attr('src') || '').replace(/(https?:[/])([^/])/, '$1/$2')
    if (!startAs) return
    const src = url.resolve(chapter.base, startAs)
    if (!url.parse(src).hostname) return
    const newsrc = handleImage(fic.normalizeLink(src, chapter.base), $img)
    $img.attr('src', newsrc || src)
  })
}

function findChapter (href, fic) {
  const matching = fic.chapters.filter(index => fic.normalizeLink(index.link) === fic.normalizeLink(href))
  return matching && matching[0]
}

function externalName (external) {
  return `_LINK_external#LINK#${external.num||external.order}#LINK#_LINK_`
}
function chapterLinkname (chapter) {
  return `_LINK_chapter#LINK#${chapter.num||chapter.order}#LINK#${chapter.name||''}_LINK_`
}

function inlineImages (images) {
  return (src, $img) => {
    if (/clear[.]png$/.test(src)) return // xenforo
    if (/Special:CentralAutoLogin/.test(src)) return // wikipedia
    src = src.replace(/^https?:[/][/]api[.]imgble[.]com[/](.*)[/]\d+[/]\d+$/, '$1')
    src = src.replace(/\b(?:41|36|67)[.]media[.]tumblr[.]com\b/, '40.media.tumblr.com')
    if (!images[src]) {
      let ext = src.match(/([.](?:jpe?g|gif|png))/i) || src.match(/([.]svg)/i)
      ext = ext && ext[1]
      if (ext === '.svg' && /wikia.nocookie.net/.test(src)) ext = '.png'
      if (ext === '.jpeg') ext = '.jpg'
      images[src] = {
        filename: `image-${Object.keys(images).length + 1}${ext || '.guess.jpg'}`
      }
    }
    return images[src].filename
  }
}

function linklocalChapters (fic, externals) {
  return (href, $a, orElse) => {
    if (!orElse) orElse = () => { }
    if ($a.text() === '↑') {
      $a.remove()
      return
    }
    if ($a.attr('external') === 'false') return
    const linkedChapter = findChapter(href, fic)
    if (linkedChapter) {
      return chapterLinkname(linkedChapter)
    } else if (externals[href]) {
      return externalName(externals[href])
    } else {
      return orElse(href) || href
    }
  }
}

function getFic (fetch, fic) {
  const stream = new FicStream(fic, {highWaterMark: 8})
  const externals = {}
  const images = {}
  const chapters = fic.chapters.filter(ch => ch.type !== 'Staff Post')
  const maxConcurrency = 40 // limit saves memory, not network, network is protected elsewhere

  process.emit('debug', `Outputting ${chapters.length} chapters of ${fic.title}`)
  let completed = 0
  let headIndex = 0
  let tailIndex = 0
  function showChapterStatus () {
    progress.show(`Fetching chapters [${completed}/${chapters.length}]`)
  }
  showChapterStatus()
  const finishedChapters = []

  const identifyBuffer = require('buffer-signature').identify
  async function addImage (src, type, filename) {
    if (!src) return
    process.emit('debug', `Outputting ${type} of ${fic.title}`)
    if (/:/.test(src)) {
      let referer = fic.link
      // if this is an image from fanfiction.net then it MUST have a fanfiction.net referrer
      if (/fictionpressllc/.test(src) && !/fanfiction[.]net/.test(referer)) {
        const altlinks = fic.altlinks || []
        const ffnetlinks = fic.altlinks.filter(l => /fanfiction[.]net/.test(l))
        if (!ffnetlinks.length) throw new Error('Found fanfiction.net image without fanfiction.net link in: ' + fic.title)
        referer = ffnetlinks[0]
      }
      fetch.tracker.addWork(1)
      progress.show(`Fetching ${type}…`)
      try {
        const [meta, imageData] = await fetch(src, {referer})
        const info = identifyBuffer(imageData)
        if (filename && !/[.]\w+$/.test(filename) && info.extensions.length) {
          filename += '.' + info.extensions[0]
        }

        const chapter = {
          outputType: type,
          content: imageData,
          mime: info.mimeType
        }
        if (filename) chapter.filename = filename
        await stream.queueChapter(chapter)
      } catch (err) {
        process.emit('error', `Error while fetching ${type} ${src}: ${err.message}`)
      }
    } else {
      await stream.queueChapter({
        outputType: type,
        content: fs.createReadStream(src)
      })
    }
  }

  process.emit('debug', `Considering art`)

  // we return `stream` and this feeds it, so it's intentionally allowed to
  // go off async and not be captured
  Promise.all([addImage(fic.cover, 'cover', 'cover'), addImage(fic.art, 'art', 'art')])
  .then(() => forEach(chapters, async chapterInfo => {
    try {
      const chapter = await chapterInfo.getContent(fetch)

      chapterInfo.order = chapter.order = chapterInfo.type === 'chapter' ? headIndex++ : (8000 + tailIndex ++)
      if (chapterInfo.type !== 'chapter' && !/:/.test(chapter.name)) {
        chapter.name = `${chapterInfo.type}: ${chapterInfo.name}`
      }
      if ((fic.chapterHeadings && chapterInfo.headings !== false) || chapterInfo.headings) {
        const headerName = html`${chapterInfo.name}`
        const byline = !chapterInfo.author ? ''
          : (' by ' + (!chapterInfo.authorUrl ? chapterInfo.author
            : html`<a href="${chapterInfo.authorUrl}">${chapterInfo.author}</a>`))
        const headerLine = `<header><h2>${headerName}${byline}</h2></header>`
        chapter.content = headerLine + chapter.content
      }
      rewriteImages(fic, chapter, inlineImages(images))
      rewriteIframes(fic, chapter)
      chapter.outputType = 'chapter'
      finishedChapters.push({info: chapterInfo, content: chapter})
    } catch (err) {
      process.emit('error', 'Error while fetching chapter', chapterInfo, err.stack)
    } finally {
      ++completed
      showChapterStatus()
    }
  })).then(async () => {
    completed = 0
    showChapterStatus()
    await forEach(finishedChapters, async (chapter, ii) => {
      rewriteLinks(fic, chapter.content, (href, $a) => {
        return linklocalChapters(fic, externals)(href, $a, (href) => {
          if (!chapter.info.externals || !fic.externals) return
          try {
            Site.fromUrl(href)
            if (/[/](?:user|members)[/]/.test(href)) return
          } catch (ex) {
            return
          }
          externals[href] = {
            order: 9000 + Object.keys(externals).length,
            num: Object.keys(externals).length + 1,
            requestedBy: chapter.info
          }
          return externalName(externals[href])
        })
      })

      await stream.queueChapter(chapter.content)
      ++completed
      showChapterStatus()
    })

    const externalCount = Object.keys(externals).length
    process.emit('debug', `Outputting ${externalCount} externals of ${fic.title}`)
    fetch.tracker.addWork(externalCount)
    completed = 0
    function showExternalStatus () {
      progress.show(`Fetching externals [${completed}/${externalCount}]`)
    }
    showExternalStatus()
    const pages = externalCount === 1 ? 'page' : 'pages'
    await forEach(Object.keys(externals), maxConcurrency, async (href, exterNum) => {
      const externalInfo = externals[href]
      try {
        const external = await Chapter.getContent(fetch, href)
        external.order = externalInfo.order
        external.num = externalInfo.num
        const name = external.name || external.ficTitle
        let header = ''
        const linkSource = externalInfo.requestedBy.link || externalInfo.requestedBy.fetchFrom
        header += `<div>Linked to from: <a href="${linkSource}">${externalInfo.requestedBy.name}</a></div>`
        const byline = !external.author ? ''
          : (!external.authorUrl ? external.author : html`<a href="${external.authorUrl}">${external.author}</a>`)
        if (name) {
          const headerName = html`${name}`
          header += `<header><h2><a external="false" href="${href}">${headerName}</a>${byline ? ' by ' + byline : ''}</h2></header>`
        } else if (byline) {
          const wrappableLink = href.replace(/(.....)/g, '$1<wbr>')
          header += `<header><h2><div style="font-size: 11px"><a external="false" href="${href}">${wrappableLink}</a></div>`
          header += `by ${byline}</h2></header>`
        }
        external.content = `${header}<hr>${external.content}`
        external.name = exterNum ? null : `External References (${externalCount} ${pages})`
        external.filename = externalName(external)
        external.outputType = 'external'
        rewriteImages(fic, external, inlineImages(images))
        rewriteLinks(fic, external, linklocalChapters(fic, externals))
        rewriteIframes(fic, external)
        await stream.queueChapter(external)
      } catch (err) {
        process.emit('error', `Warning, skipping external ${href}: ${err.stack}`)
        await stream.queueChapter({
          order: 9000 + exterNum,
          name: exterNum ? null : `External References (${externalCount} ${pages})`,
          filename: externalName(externals[href]),
          outputType: 'external',
          content: html`<p>External link to <a href="${href}">${href}</a></p><pre>${err.stack}</pre>`
        })
      } finally {
        ++completed
        showExternalStatus()
      }
    })

    const imageCount = Object.keys(images).length
    fetch.tracker.addWork(imageCount)
    completed = 0
    function showImageStatus () {
      progress.show(`Fetching images [${completed}/${imageCount}]`)
    }
    showImageStatus()
    await forEach(Object.keys(images), maxConcurrency, async (src, imageNum) => {
      try {
        await addImage(src, 'image', images[src].filename)
      } catch (err) {
        process.emit('error', `Error while fetching image ${src}: ${err.message}`)
      } finally {
        ++completed
        showImageStatus()
      }
    })

    process.emit('debug', `Outputting ${fic.title} complete`)
    return stream.queueChapter(null)
  }).catch(err => {
    process.emit('error', `Error in get fic ${err.stack}`)
  })

  return stream
}
<commit_msg>get-fic: Parsing iframes hsould be non-fatal on errors<commit_after>'use strict'
module.exports = getFic

const fs = require('fs')
const url = require('url')

const Chapter = use('fic').Chapter
const FicStream = use('fic-stream')
const html = use('html-template-tag')
const progress = use('progress')
const Site = use('site')
const forEach = use('for-each')
const map = use('map')

function rewriteLinks (fic, chapter, handleLink) {
  chapter.$content.find('a').each((ii, a) => {
    const $a = chapter.$content.find(a)
    const startAs = $a.attr('href')
    if (!startAs) {
      $a.remove()
      return
    }
    if (startAs[0] === '#') return
    let src
    if (startAs.substr(0,4) !== 'http' && /^www[.]|[.]com$/.test(startAs)) {
      src = 'http://' + startAs
    } else if (/^[\w.]+(com|org|net)[/]/.test(startAs)) {
      src = 'http://' + startAs
    } else {
      src = chapter.base ? url.resolve(chapter.base, startAs) : startAs
    }
    const newHref = handleLink(fic.normalizeLink(src, chapter.base), $a)
    $a.attr('href', newHref || src)
  })
}

function rewriteIframes (fic, chapter) {
  chapter.$content.find('iframe').each((ii, iframe) => {
    try {
      const $iframe = chapter.$content.find(iframe)
      const src = url.resolve(chapter.base, $iframe.attr('src'))
      $iframe.replaceWith(`<a href="${src}">Video Link</a>`)
    } catch (_) {}
  })
}

function rewriteImages (fic, chapter, handleImage) {
  chapter.$content.find('img').each((ii, img) => {
    const $img = chapter.$content.find(img)
    const startAs = ($img.attr('src') || '').replace(/(https?:[/])([^/])/, '$1/$2')
    if (!startAs) return
    const src = url.resolve(chapter.base, startAs)
    if (!url.parse(src).hostname) return
    const newsrc = handleImage(fic.normalizeLink(src, chapter.base), $img)
    $img.attr('src', newsrc || src)
  })
}

function findChapter (href, fic) {
  const matching = fic.chapters.filter(index => fic.normalizeLink(index.link) === fic.normalizeLink(href))
  return matching && matching[0]
}

function externalName (external) {
  return `_LINK_external#LINK#${external.num||external.order}#LINK#_LINK_`
}
function chapterLinkname (chapter) {
  return `_LINK_chapter#LINK#${chapter.num||chapter.order}#LINK#${chapter.name||''}_LINK_`
}

function inlineImages (images) {
  return (src, $img) => {
    if (/clear[.]png$/.test(src)) return // xenforo
    if (/Special:CentralAutoLogin/.test(src)) return // wikipedia
    src = src.replace(/^https?:[/][/]api[.]imgble[.]com[/](.*)[/]\d+[/]\d+$/, '$1')
    src = src.replace(/\b(?:41|36|67)[.]media[.]tumblr[.]com\b/, '40.media.tumblr.com')
    if (!images[src]) {
      let ext = src.match(/([.](?:jpe?g|gif|png))/i) || src.match(/([.]svg)/i)
      ext = ext && ext[1]
      if (ext === '.svg' && /wikia.nocookie.net/.test(src)) ext = '.png'
      if (ext === '.jpeg') ext = '.jpg'
      images[src] = {
        filename: `image-${Object.keys(images).length + 1}${ext || '.guess.jpg'}`
      }
    }
    return images[src].filename
  }
}

function linklocalChapters (fic, externals) {
  return (href, $a, orElse) => {
    if (!orElse) orElse = () => { }
    if ($a.text() === '↑') {
      $a.remove()
      return
    }
    if ($a.attr('external') === 'false') return
    const linkedChapter = findChapter(href, fic)
    if (linkedChapter) {
      return chapterLinkname(linkedChapter)
    } else if (externals[href]) {
      return externalName(externals[href])
    } else {
      return orElse(href) || href
    }
  }
}

function getFic (fetch, fic) {
  const stream = new FicStream(fic, {highWaterMark: 8})
  const externals = {}
  const images = {}
  const chapters = fic.chapters.filter(ch => ch.type !== 'Staff Post')
  const maxConcurrency = 40 // limit saves memory, not network, network is protected elsewhere

  process.emit('debug', `Outputting ${chapters.length} chapters of ${fic.title}`)
  let completed = 0
  let headIndex = 0
  let tailIndex = 0
  function showChapterStatus () {
    progress.show(`Fetching chapters [${completed}/${chapters.length}]`)
  }
  showChapterStatus()
  const finishedChapters = []

  const identifyBuffer = require('buffer-signature').identify
  async function addImage (src, type, filename) {
    if (!src) return
    process.emit('debug', `Outputting ${type} of ${fic.title}`)
    if (/:/.test(src)) {
      let referer = fic.link
      // if this is an image from fanfiction.net then it MUST have a fanfiction.net referrer
      if (/fictionpressllc/.test(src) && !/fanfiction[.]net/.test(referer)) {
        const altlinks = fic.altlinks || []
        const ffnetlinks = fic.altlinks.filter(l => /fanfiction[.]net/.test(l))
        if (!ffnetlinks.length) throw new Error('Found fanfiction.net image without fanfiction.net link in: ' + fic.title)
        referer = ffnetlinks[0]
      }
      fetch.tracker.addWork(1)
      progress.show(`Fetching ${type}…`)
      try {
        const [meta, imageData] = await fetch(src, {referer})
        const info = identifyBuffer(imageData)
        if (filename && !/[.]\w+$/.test(filename) && info.extensions.length) {
          filename += '.' + info.extensions[0]
        }

        const chapter = {
          outputType: type,
          content: imageData,
          mime: info.mimeType
        }
        if (filename) chapter.filename = filename
        await stream.queueChapter(chapter)
      } catch (err) {
        process.emit('error', `Error while fetching ${type} ${src}: ${err.message}`)
      }
    } else {
      await stream.queueChapter({
        outputType: type,
        content: fs.createReadStream(src)
      })
    }
  }

  process.emit('debug', `Considering art`)

  // we return `stream` and this feeds it, so it's intentionally allowed to
  // go off async and not be captured
  Promise.all([addImage(fic.cover, 'cover', 'cover'), addImage(fic.art, 'art', 'art')])
  .then(() => forEach(chapters, async chapterInfo => {
    try {
      const chapter = await chapterInfo.getContent(fetch)

      chapterInfo.order = chapter.order = chapterInfo.type === 'chapter' ? headIndex++ : (8000 + tailIndex ++)
      if (chapterInfo.type !== 'chapter' && !/:/.test(chapter.name)) {
        chapter.name = `${chapterInfo.type}: ${chapterInfo.name}`
      }
      if ((fic.chapterHeadings && chapterInfo.headings !== false) || chapterInfo.headings) {
        const headerName = html`${chapterInfo.name}`
        const byline = !chapterInfo.author ? ''
          : (' by ' + (!chapterInfo.authorUrl ? chapterInfo.author
            : html`<a href="${chapterInfo.authorUrl}">${chapterInfo.author}</a>`))
        const headerLine = `<header><h2>${headerName}${byline}</h2></header>`
        chapter.content = headerLine + chapter.content
      }
      rewriteImages(fic, chapter, inlineImages(images))
      rewriteIframes(fic, chapter)
      chapter.outputType = 'chapter'
      finishedChapters.push({info: chapterInfo, content: chapter})
    } catch (err) {
      process.emit('error', 'Error while fetching chapter', chapterInfo, err.stack)
    } finally {
      ++completed
      showChapterStatus()
    }
  })).then(async () => {
    completed = 0
    showChapterStatus()
    await forEach(finishedChapters, async (chapter, ii) => {
      rewriteLinks(fic, chapter.content, (href, $a) => {
        return linklocalChapters(fic, externals)(href, $a, (href) => {
          if (!chapter.info.externals || !fic.externals) return
          try {
            Site.fromUrl(href)
            if (/[/](?:user|members)[/]/.test(href)) return
          } catch (ex) {
            return
          }
          externals[href] = {
            order: 9000 + Object.keys(externals).length,
            num: Object.keys(externals).length + 1,
            requestedBy: chapter.info
          }
          return externalName(externals[href])
        })
      })

      await stream.queueChapter(chapter.content)
      ++completed
      showChapterStatus()
    })

    const externalCount = Object.keys(externals).length
    process.emit('debug', `Outputting ${externalCount} externals of ${fic.title}`)
    fetch.tracker.addWork(externalCount)
    completed = 0
    function showExternalStatus () {
      progress.show(`Fetching externals [${completed}/${externalCount}]`)
    }
    showExternalStatus()
    const pages = externalCount === 1 ? 'page' : 'pages'
    await forEach(Object.keys(externals), maxConcurrency, async (href, exterNum) => {
      const externalInfo = externals[href]
      try {
        const external = await Chapter.getContent(fetch, href)
        external.order = externalInfo.order
        external.num = externalInfo.num
        const name = external.name || external.ficTitle
        let header = ''
        const linkSource = externalInfo.requestedBy.link || externalInfo.requestedBy.fetchFrom
        header += `<div>Linked to from: <a href="${linkSource}">${externalInfo.requestedBy.name}</a></div>`
        const byline = !external.author ? ''
          : (!external.authorUrl ? external.author : html`<a href="${external.authorUrl}">${external.author}</a>`)
        if (name) {
          const headerName = html`${name}`
          header += `<header><h2><a external="false" href="${href}">${headerName}</a>${byline ? ' by ' + byline : ''}</h2></header>`
        } else if (byline) {
          const wrappableLink = href.replace(/(.....)/g, '$1<wbr>')
          header += `<header><h2><div style="font-size: 11px"><a external="false" href="${href}">${wrappableLink}</a></div>`
          header += `by ${byline}</h2></header>`
        }
        external.content = `${header}<hr>${external.content}`
        external.name = exterNum ? null : `External References (${externalCount} ${pages})`
        external.filename = externalName(external)
        external.outputType = 'external'
        rewriteImages(fic, external, inlineImages(images))
        rewriteLinks(fic, external, linklocalChapters(fic, externals))
        rewriteIframes(fic, external)
        await stream.queueChapter(external)
      } catch (err) {
        process.emit('error', `Warning, skipping external ${href}: ${err.stack}`)
        await stream.queueChapter({
          order: 9000 + exterNum,
          name: exterNum ? null : `External References (${externalCount} ${pages})`,
          filename: externalName(externals[href]),
          outputType: 'external',
          content: html`<p>External link to <a href="${href}">${href}</a></p><pre>${err.stack}</pre>`
        })
      } finally {
        ++completed
        showExternalStatus()
      }
    })

    const imageCount = Object.keys(images).length
    fetch.tracker.addWork(imageCount)
    completed = 0
    function showImageStatus () {
      progress.show(`Fetching images [${completed}/${imageCount}]`)
    }
    showImageStatus()
    await forEach(Object.keys(images), maxConcurrency, async (src, imageNum) => {
      try {
        await addImage(src, 'image', images[src].filename)
      } catch (err) {
        process.emit('error', `Error while fetching image ${src}: ${err.message}`)
      } finally {
        ++completed
        showImageStatus()
      }
    })

    process.emit('debug', `Outputting ${fic.title} complete`)
    return stream.queueChapter(null)
  }).catch(err => {
    process.emit('error', `Error in get fic ${err.stack}`)
  })

  return stream
}
<|endoftext|>


<commit_before>/* global Rollbar */

import EventCategory from '../EventAdmin/EventCategory';
import EventRun from '../PCSG/EventRun';
import ScheduleBlock from '../PCSG/ScheduleBlock';
import ScheduleGridLayout from './ScheduleGridLayout';
import { timespanFromConvention, getConventionDayTimespans } from '../TimespanUtils';

export default class Schedule {
  constructor(config, data) {
    this.config = config;
    this.data = data;

    this.timezoneName = data.convention.timezone_name;

    this.eventsById = new Map(data.events.map(event => [event.id, event]));
    this.runsById = new Map(data.events.map(event => (
      event.runs.map(run => [run.id, { ...run, event_id: event.id }])
    )).reduce((runEntries, entriesForEvent) => [...runEntries, ...entriesForEvent], []));

    this.conventionTimespan = timespanFromConvention(data.convention);

    this.conventionDayTimespans = getConventionDayTimespans(
      this.conventionTimespan,
      data.convention.timezone_name,
    );

    this.eventRuns = EventRun.buildEventRunsFromApi(data.events);
    this.runTimespansById = new Map(this.eventRuns
      .map(eventRun => [eventRun.runId, eventRun.timespan]));
  }

  getRun = runId => this.runsById.get(runId)

  getEvent = eventId => this.eventsById.get(eventId)

  getEventRunsOverlapping = timespan => this.eventRuns.filter(eventRun => (
    timespan.overlapsTimespan(eventRun.timespan)
  ))

  getRunTimespan = runId => this.runTimespansById.get(runId)

  groupEventRunsByCategory = eventRuns => eventRuns.reduce(
    (eventRunsByCategory, eventRun) => {
      const { runId } = eventRun;
      const run = this.runsById.get(runId);
      const event = this.eventsById.get(run.event_id);
      const { category: categoryKey } = event;

      const category = EventCategory.get(categoryKey);
      let groupName = 'regular';

      if (!category && typeof Rollbar !== 'undefined') {
        Rollbar.warn(`Unknown category ${categoryKey} for event ${event.id} (${event.title})`);
      }

      if (category && category.isSingleRun()) {
        groupName = 'singleRun';
      }

      if (category && category.isRecurring()) {
        groupName = 'recurring';
      }

      if (!eventRunsByCategory.has(groupName)) {
        eventRunsByCategory.set(groupName, []);
      }

      return eventRunsByCategory.set(
        groupName,
        eventRunsByCategory.get(groupName).concat(eventRun),
      );
    },
    new Map(),
  )

  groupEventRunsByRoom = eventRuns => eventRuns.reduce(
    (eventRunsByRoom, eventRun) => {
      const { runId } = eventRun;
      const run = this.runsById.get(runId);

      run.rooms.forEach((room) => {
        if (!eventRunsByRoom.has(room.name)) {
          eventRunsByRoom.set(room.name, []);
        }

        eventRunsByRoom.set(room.name, [...eventRunsByRoom.get(room.name), eventRun]);
      });

      return eventRunsByRoom;
    },
    new Map(),
  )

  buildLayoutForTimespanRange = (minTimespan, maxTimespan) => {
    const eventRuns = this.getEventRunsOverlapping(maxTimespan);
    const actualTimespan = eventRuns.reduce(
      (currentMaxTimespan, eventRun) => currentMaxTimespan.expandedToFit(eventRun.timespan),
      minTimespan,
    );

    if (this.config.groupEventsBy === 'room') {
      const eventRunsByRoom = this.groupEventRunsByRoom(eventRuns);
      const roomNames = [...eventRunsByRoom.keys()]
        .sort((a, b) => a.localeCompare(b, { sensitivity: 'base' }));

      const scheduleBlocksWithOptions = roomNames.map(roomName => (
        [
          new ScheduleBlock(actualTimespan, eventRunsByRoom.get(roomName)),
          { rowHeader: roomName },
        ]
      ));

      return new ScheduleGridLayout(eventRuns, actualTimespan, scheduleBlocksWithOptions);
    }

    const eventRunsByCategory = this.groupEventRunsByCategory(eventRuns);

    const recurringRuns = eventRunsByCategory.get('recurring') || [];
    const singleRuns = eventRunsByCategory.get('singleRun') || [];
    const regularRuns = eventRunsByCategory.get('regular') || [];

    const scheduleBlocksWithOptions = [
      [new ScheduleBlock(actualTimespan, singleRuns)],
      [new ScheduleBlock(actualTimespan, regularRuns), { flexGrow: 1 }],
      [new ScheduleBlock(actualTimespan, recurringRuns)],
    ].filter(scheduleBlock => scheduleBlock[0].eventRuns.length > 0);

    return new ScheduleGridLayout(eventRuns, actualTimespan, scheduleBlocksWithOptions);
  }

  buildLayoutForConventionDayTimespan = (conventionDayTimespan) => {
    const minTimespan = conventionDayTimespan.clone();
    minTimespan.start.add(3, 'hours'); // start grid at 9am unless something is earlier
    minTimespan.finish.subtract(6, 'hours'); // end grid at midnight unless something is earlier

    return this.buildLayoutForTimespanRange(minTimespan, conventionDayTimespan);
  }

  shouldUseRowHeaders = () => this.config.groupEventsBy === 'room'
}
<commit_msg>Fix for schedule by room<commit_after>/* global Rollbar */

import EventCategory from '../EventAdmin/EventCategory';
import EventRun from '../PCSG/EventRun';
import ScheduleBlock from '../PCSG/ScheduleBlock';
import ScheduleGridLayout from './ScheduleGridLayout';
import { timespanFromConvention, getConventionDayTimespans } from '../TimespanUtils';

export default class Schedule {
  constructor(config, data) {
    this.config = config;
    this.data = data;

    this.timezoneName = data.convention.timezone_name;

    this.eventsById = new Map(data.events.map(event => [event.id, event]));
    this.runsById = new Map(data.events.map(event => (
      event.runs.map(run => [run.id, { ...run, event_id: event.id }])
    )).reduce((runEntries, entriesForEvent) => [...runEntries, ...entriesForEvent], []));

    this.conventionTimespan = timespanFromConvention(data.convention);

    this.conventionDayTimespans = getConventionDayTimespans(
      this.conventionTimespan,
      data.convention.timezone_name,
    );

    this.eventRuns = EventRun.buildEventRunsFromApi(data.events);
    this.runTimespansById = new Map(this.eventRuns
      .map(eventRun => [eventRun.runId, eventRun.timespan]));
  }

  getRun = runId => this.runsById.get(runId)

  getEvent = eventId => this.eventsById.get(eventId)

  getEventRunsOverlapping = timespan => this.eventRuns.filter(eventRun => (
    timespan.overlapsTimespan(eventRun.timespan)
  ))

  getRunTimespan = runId => this.runTimespansById.get(runId)

  groupEventRunsByCategory = eventRuns => eventRuns.reduce(
    (eventRunsByCategory, eventRun) => {
      const { runId } = eventRun;
      const run = this.runsById.get(runId);
      const event = this.eventsById.get(run.event_id);
      const { category: categoryKey } = event;

      const category = EventCategory.get(categoryKey);
      let groupName = 'regular';

      if (!category && typeof Rollbar !== 'undefined') {
        Rollbar.warn(`Unknown category ${categoryKey} for event ${event.id} (${event.title})`);
      }

      if (category && category.isSingleRun()) {
        groupName = 'singleRun';
      }

      if (category && category.isRecurring()) {
        groupName = 'recurring';
      }

      if (!eventRunsByCategory.has(groupName)) {
        eventRunsByCategory.set(groupName, []);
      }

      return eventRunsByCategory.set(
        groupName,
        eventRunsByCategory.get(groupName).concat(eventRun),
      );
    },
    new Map(),
  )

  groupEventRunsByRoom = eventRuns => eventRuns.reduce(
    (eventRunsByRoom, eventRun) => {
      const { runId } = eventRun;
      const run = this.runsById.get(runId);

      run.room_names.forEach((roomName) => {
        if (!eventRunsByRoom.has(roomName)) {
          eventRunsByRoom.set(roomName, []);
        }

        eventRunsByRoom.set(roomName, [...eventRunsByRoom.get(roomName), eventRun]);
      });

      return eventRunsByRoom;
    },
    new Map(),
  )

  buildLayoutForTimespanRange = (minTimespan, maxTimespan) => {
    const eventRuns = this.getEventRunsOverlapping(maxTimespan);
    const actualTimespan = eventRuns.reduce(
      (currentMaxTimespan, eventRun) => currentMaxTimespan.expandedToFit(eventRun.timespan),
      minTimespan,
    );

    if (this.config.groupEventsBy === 'room') {
      const eventRunsByRoom = this.groupEventRunsByRoom(eventRuns);
      const roomNames = [...eventRunsByRoom.keys()]
        .sort((a, b) => a.localeCompare(b, { sensitivity: 'base' }));

      const scheduleBlocksWithOptions = roomNames.map(roomName => (
        [
          new ScheduleBlock(actualTimespan, eventRunsByRoom.get(roomName)),
          { rowHeader: roomName },
        ]
      ));

      return new ScheduleGridLayout(eventRuns, actualTimespan, scheduleBlocksWithOptions);
    }

    const eventRunsByCategory = this.groupEventRunsByCategory(eventRuns);

    const recurringRuns = eventRunsByCategory.get('recurring') || [];
    const singleRuns = eventRunsByCategory.get('singleRun') || [];
    const regularRuns = eventRunsByCategory.get('regular') || [];

    const scheduleBlocksWithOptions = [
      [new ScheduleBlock(actualTimespan, singleRuns)],
      [new ScheduleBlock(actualTimespan, regularRuns), { flexGrow: 1 }],
      [new ScheduleBlock(actualTimespan, recurringRuns)],
    ].filter(scheduleBlock => scheduleBlock[0].eventRuns.length > 0);

    return new ScheduleGridLayout(eventRuns, actualTimespan, scheduleBlocksWithOptions);
  }

  buildLayoutForConventionDayTimespan = (conventionDayTimespan) => {
    const minTimespan = conventionDayTimespan.clone();
    minTimespan.start.add(3, 'hours'); // start grid at 9am unless something is earlier
    minTimespan.finish.subtract(6, 'hours'); // end grid at midnight unless something is earlier

    return this.buildLayoutForTimespanRange(minTimespan, conventionDayTimespan);
  }

  shouldUseRowHeaders = () => this.config.groupEventsBy === 'room'
}
<|endoftext|>


<commit_before>// This is a manifest file that'll be compiled into application.js, which will include all the files
// listed below.
//
// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
// or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path.
//
// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
// compiled file.
//
// Read Sprockets README (https://github.com/sstephenson/sprockets#sprockets-directives) for details
// about supported directives.
//
//= require jquery
//= require jquery_ujs
//= require foundation
//= require redactor-rails
//= require redactor-rails/config
//= require_tree .

$(function(){ $(document).foundation(); });
<commit_msg>Change ordering of JS pipeline<commit_after>// This is a manifest file that'll be compiled into application.js, which will include all the files
// listed below.
//
// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
// or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path.
//
// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
// compiled file.
//
// Read Sprockets README (https://github.com/sstephenson/sprockets#sprockets-directives) for details
// about supported directives.
//
//= require jquery
//= require jquery_ujs
//= require_tree ../../../vendor/assets/javascripts/.
//= require redactor-rails
//= require redactor-rails/config
//= require foundation
//= require_tree .

$(function(){ $(document).foundation(); });
<|endoftext|>


<commit_before>// This is a manifest file that'll be compiled into application.js, which will include all the files
// listed below.
//
// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
// or vendor/assets/javascripts of plugins, if any, can be referenced here using a relative path.
//
// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
// compiled file.
//
// Read Sprockets README (https://github.com/sstephenson/sprockets#sprockets-directives) for details
// about supported directives.
//
//= require jquery
//= require jquery_ujs
//= require jquery.ui.all
//= require_tree .
// Loads all Bootstrap javascripts
//= require bootstrap

function buildYoutubeUrl(youtubeCode) {
  var url = 'https://www.youtubeeducation.com/embed/' +
             youtubeCode +
             '?modestbranding=1&rel=0&fs=1&showinfo=1';
  return url;
}

function showVideo(youtubeCode, name) {
  var src = buildYoutubeUrl(youtubeCode);
  var header = $('<h3/>').text(name);
  var video = $('<iframe/>').addClass('video-player').attr({
    src: src,
    scrolling: 'no'
  });

  var dialog = new Dialog({ header: header, body: video });
  $(dialog.div).addClass('video-modal');

  dialog.show();
  $('.modal-backdrop').addClass('video-backdrop');  // Hack to fix z-index.
}

function embed_thumbnail_image(data) {
  var thumbnails = data.entry.media$group.media$thumbnail;
  var video_code = data.entry.media$group.yt$videoid.$t;
  if (thumbnails && thumbnails.length > 0) {
      $("#thumbnail_" + video_code).attr('src', thumbnails[0].url);
  }
}

var addClickTouchEvent = function(element, handler) {
  element.on({
    'touchend': handler,
    'click': handler
  });
};

/**
 * Create a custom modal dialog box which takes a configurable options object.
 * Currently supported options include 'header' and 'body', which are DOM
 * elements.
 */
function Dialog(options) {
  var body = options.body;
  var header = options.header;

  var close = $('<a/>').addClass('close')
                       .attr('data-dismiss', 'modal')
                       .text('\u2297');
  this.div = $('<div/>').addClass('modal');
  var modalBody = $('<div/>').addClass('modal-body');
  if (header) {
    var modalHeader = $('<div/>').addClass('modal-header')
                                 .append(close)
                                 .append(header);
    this.div.append(modalHeader);
  } else {
    modalBody.append(close);
  }
  modalBody.append(body);
  this.div.append(modalBody).appendTo('body');

  addClickTouchEvent(close, $.proxy(function() {
    this.hide();
  }, this));
}

/**
 * Options is configurable with a top and left properties, both are integers.
 */
Dialog.prototype.show = function(options) {
  options = options || {};

  $(this.div).modal('show');

  addClickTouchEvent($(this.div).next(), $.proxy(function() {
    this.hide();
  }, this));

  this.div.offset(options);
}
Dialog.prototype.hide = function() {
  // Hide let's bootstrap cleanup first, then we remove.
  $(this.div).modal('hide');
  $(this.div).remove();
}

$(document).ready(function() {
  $('.video-link').each(function() {
    addClickTouchEvent($(this), $.proxy(function() {
      showVideo($(this).attr('data-code'), $(this).attr('data-name'));
    }, this));
  });
});
<commit_msg>[Finishes 59703508]: use document.body - fixes Firefox<commit_after>// This is a manifest file that'll be compiled into application.js, which will include all the files
// listed below.
//
// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
// or vendor/assets/javascripts of plugins, if any, can be referenced here using a relative path.
//
// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
// compiled file.
//
// Read Sprockets README (https://github.com/sstephenson/sprockets#sprockets-directives) for details
// about supported directives.
//
//= require jquery
//= require jquery_ujs
//= require jquery.ui.all
//= require_tree .
// Loads all Bootstrap javascripts
//= require bootstrap

function buildYoutubeUrl(youtubeCode) {
  var url = 'https://www.youtubeeducation.com/embed/' +
             youtubeCode +
             '?modestbranding=1&rel=0&fs=1&showinfo=1';
  return url;
}

function showVideo(youtubeCode, name) {
  var src = buildYoutubeUrl(youtubeCode);
  var header = $('<h3/>').text(name);
  var video = $('<iframe/>').addClass('video-player').attr({
    src: src,
    scrolling: 'no'
  });

  var dialog = new Dialog({ header: header, body: video });
  $(dialog.div).addClass('video-modal');

  dialog.show();
  $('.modal-backdrop').addClass('video-backdrop');  // Hack to fix z-index.
}

function embed_thumbnail_image(data) {
  var thumbnails = data.entry.media$group.media$thumbnail;
  var video_code = data.entry.media$group.yt$videoid.$t;
  if (thumbnails && thumbnails.length > 0) {
      $("#thumbnail_" + video_code).attr('src', thumbnails[0].url);
  }
}

var addClickTouchEvent = function(element, handler) {
  element.on({
    'touchend': handler,
    'click': handler
  });
};

/**
 * Create a custom modal dialog box which takes a configurable options object.
 * Currently supported options include 'header' and 'body', which are DOM
 * elements.
 */
function Dialog(options) {
  var body = options.body;
  var header = options.header;

  var close = $('<a/>').addClass('close')
                       .attr('data-dismiss', 'modal')
                       .text('\u2297');
  this.div = $('<div/>').addClass('modal');
  var modalBody = $('<div/>').addClass('modal-body');
  if (header) {
    var modalHeader = $('<div/>').addClass('modal-header')
                                 .append(close)
                                 .append(header);
    this.div.append(modalHeader);
  } else {
    modalBody.append(close);
  }
  modalBody.append(body);
  this.div.append(modalBody).appendTo($(document.body));

  addClickTouchEvent(close, $.proxy(function() {
    this.hide();
  }, this));
}

/**
 * Options is configurable with a top and left properties, both are integers.
 */
Dialog.prototype.show = function(options) {
  options = options || {};

  $(this.div).modal('show');

  addClickTouchEvent($(this.div).next(), $.proxy(function() {
    this.hide();
  }, this));

  this.div.offset(options);
}
Dialog.prototype.hide = function() {
  // Hide let's bootstrap cleanup first, then we remove.
  $(this.div).modal('hide');
  $(this.div).remove();
}

$(document).ready(function() {
  $('.video-link').each(function() {
    addClickTouchEvent($(this), $.proxy(function() {
      showVideo($(this).attr('data-code'), $(this).attr('data-name'));
    }, this));
  });
});
<|endoftext|>


<commit_before>require('electron').ipcRenderer.on('loaded' , function(event, data) {
  document.getElementById('details').innerHTML = 'built with Electron v' + data.electronVersion;
  document.getElementById('versions').innerHTML = 'running on Node v' + data.nodeVersion + ' and Chromium v' + data.chromiumVersion;

  const sudo = require('sudo-prompt');
  const options = {
    name: 'HostsManager',
    //icns: '/Applications/Electron.app/Contents/Resources/Electron.icns', // (optional)
  };
  const uiAnimSavedDuration = 2000;
  const appHostsSettingIdx = 'hosts';
  const hostsFilePath = '/etc/hosts';
  const hostsDelimiterStart = '# ----- HostsManager config - Do not delete -----';
  const hostsDelimiterEnd   = '# -----      HostsManager config - End      -----';
  const regex = /(?:# ----- HostsManager config - Do not delete -----\n)([\s\S]*)(?:# -----      HostsManager config - End      -----\n?)/g;
  const sedDelete = "sed -i '/"+hostsDelimiterStart+"/,/"+hostsDelimiterEnd+"\\n/d' "+hostsFilePath;
  const appHostsJson = localStorage.getItem(appHostsSettingIdx);
  let appHosts = null;

  function loadSettings()
  {
    if (appHostsJson) {
      appHosts = JSON.parse(appHostsJson);
      showHostsList(appHosts.hosts);
    } else {
      appHosts = { hosts: [] };
      saveSettings();
    }
  }

  function saveSettings()
  {
    localStorage.setItem(appHostsSettingIdx, JSON.stringify(appHosts));
  }

  function computeHostsFileString()
  {
    let hostsFileString = hostsDelimiterStart+'\\n';
    appHosts.hosts.forEach((host, hostIndex) => {
      if (host.active) {
        hostsFileString += host.str+'\\n';
      }
    });
    hostsFileString += hostsDelimiterEnd+'\\n';

    return hostsFileString;
  }

  function createHostInputElement(hostId, hostString)
  {
    let li = document.createElement('li');
    let group = document.createElement('div');
    group.classList.add('input-group');
    let input = document.createElement('input');
    input.setAttribute('type', 'text');
    input.setAttribute('data-id', hostId);
    input.setAttribute('value', hostString);
    input.classList.add('form-control', 'input-host');
    ['change', 'keyup', 'cut', 'paste'].forEach(event => input.addEventListener(event, liveSaveSettingsEvent));
    let groupBtn = document.createElement('span');
    groupBtn.classList.add('input-group-btn');
    let btnRemove = document.createElement('button');
    btnRemove.setAttribute('type', 'button');
    btnRemove.setAttribute('data-id', hostId);
    btnRemove.classList.add('btn', 'btn-default');
    btnRemove.addEventListener('click', deleteRowEvent);
    let btnRemoveIcon = document.createElement('span');
    btnRemoveIcon.classList.add('glyphicon', 'glyphicon-remove');
    btnRemove.appendChild(btnRemoveIcon);
    groupBtn.appendChild(btnRemove);
    group.appendChild(input);
    group.appendChild(groupBtn);
    li.appendChild(group);

    return li;
  }

  function showHostsList(appHosts) {
    appHosts.forEach((host, hostIndex) => {
      const li = createHostInputElement(hostIndex, host.str);
      document.querySelector('#hosts ul').appendChild(li);
    });
  }

  function udpateHostsList(appHosts) {
    const list = document.querySelector('#hosts ul');
    while (list.firstChild) {
      list.removeChild(list.firstChild);
    }
    showHostsList(appHosts);
  }

  function liveSaveSettingsEvent(event) {
    const hostId = parseInt(event.target.getAttribute('data-id'));
    appHosts.hosts[hostId].str = event.target.value;
    saveSettings();
  }

  function deleteRowEvent(event) {
    let btn = event.target;
    if (event.target.classList.contains('glyphicon')) {
      btn = event.target.parentElement;
    }
    const hostId = parseInt(event.target.getAttribute('data-id'));
    appHosts.hosts.splice(hostId, 1);
    saveSettings();
    event.target.parentElement.parentElement.parentElement.remove();
    udpateHostsList(appHosts.hosts);
  }

  document.querySelector('#load-hosts').addEventListener('click', function(e){
    sudo.exec('cat '+hostsFilePath, options, function(error, stdout, stderr) {
      //$('#hosts > p').html(stdout);
      if ((m = regex.exec(stdout)) !== null) {
        if (m.length == 2) {
          const configLines = m[1].split('\n');
          configLines.forEach((line, lineIndex) => {
            console.log(line);
          });
          // showHostsList(configLines);
        }
      }
    });
  });

  document.querySelector('.btn-save-hosts').addEventListener('click', function(event){
    const timeStart = performance.now();
    let button = event.target;
    if (button.classList.contains('text-state') || button.classList.contains('glyphicon')) {
      button = button.parentElement;
    } else if (button.tagName == 'EM') {
      button = button.parentElement.parentElement;
    }
    console.log(button);
    button.classList.add('saving');
    const cmd = "bash -c \""+sedDelete+";"+"sed -i '$ a\\"+computeHostsFileString()+"' " + hostsFilePath+"\"";
    sudo.exec(cmd, options, function(error, stdout, stderr) {
      if (!error){
        // const fileContent = computeHostsFileString();
        // const fileUpdateCmd = 'echo "'+fileContent+'" | tee -a '+hostsFilePath;
        // const fileUpdateCmd = "sed -i '$ a\\"+fileContent+"' " + hostsFilePath;
        // sudo.exec(fileUpdateCmd, options, function(error, stdout, stderr) {
        //   if (!error){

        //   } else {
        //     console.error(error);
        //     console.log(stderr);
        //   }
        // });
        const timeEnd = performance.now();
        if (timeEnd - timeStart < 1000) {
          setTimeout(function(){
            button.classList.remove('saving');
            button.classList.add('saved');
          }, 1000 - (timeEnd - timeStart));
          setTimeout(function(){
            button.classList.remove('saved');
          }, uiAnimSavedDuration + (1000 - (timeEnd - timeStart)));
        } else {
          button.classList.remove('saving');
          button.classList.add('saved');
          setTimeout(function(){
            button.classList.remove('saved');
          }, uiAnimSavedDuration);
        }
      } else {
        button.classList.remove('saving');
        button.classList.add('error');
        console.error(error);
        console.log(stderr);
      }
    });
  });

  document.querySelector(".btn-add").addEventListener('click', function(event) {
    event.preventDefault();
    appHosts.hosts.push({str: '', active: true});
    saveSettings();
    const li = createHostInputElement((appHosts.hosts.length - 1), '');
    document.querySelector('#hosts ul').appendChild(li);
  });

  document.querySelector('.btn-remove').addEventListener('click', function(event) {
    event.preventDefault();
    appHosts.hosts.pop();
    saveSettings();
    document.querySelector('#hosts ul li:last-child').remove();
  });

  document.getElementById('title').innerHTML = data.appName + ' App';
  loadSettings();

  var app = new Vue({
    el: '#app',
    data: {
      hosts: [
        { str: '127.0.0.1 ', active: true },
        { str: ' ', active: false }
      ]
    },
    methods: {
      addHost: function(event) {
        event.preventDefault();
        this.hosts.push({ str: '', active: false });
      },
      removeHost: function(event) {
        event.preventDefault();
        this.hosts.pop();
      }
    }
  });
});<commit_msg>add hosts list saving in settings<commit_after>require('electron').ipcRenderer.on('loaded' , function(event, data) {
  document.getElementById('details').innerHTML = 'built with Electron v' + data.electronVersion;
  document.getElementById('versions').innerHTML = 'running on Node v' + data.nodeVersion + ' and Chromium v' + data.chromiumVersion;

  const sudo = require('sudo-prompt');
  const options = {
    name: 'HostsManager',
    //icns: '/Applications/Electron.app/Contents/Resources/Electron.icns', // (optional)
  };
  const uiAnimSavedDuration = 2000;
  const appHostsSettingIdx = 'hosts';
  const settingsHostsIdx = 'hostsSettings';
  const hostsFilePath = '/etc/hosts';
  const hostsDelimiterStart = '# ----- HostsManager config - Do not delete -----';
  const hostsDelimiterEnd   = '# -----      HostsManager config - End      -----';
  const regex = /(?:# ----- HostsManager config - Do not delete -----\n)([\s\S]*)(?:# -----      HostsManager config - End      -----\n?)/g;
  const sedDelete = "sed -i '/"+hostsDelimiterStart+"/,/"+hostsDelimiterEnd+"\\n/d' "+hostsFilePath;
  const appHostsJson = localStorage.getItem(appHostsSettingIdx);
  let appHosts = null;

  function loadSettings()
  {
    if (appHostsJson) {
      appHosts = JSON.parse(appHostsJson);
      showHostsList(appHosts.hosts);
    } else {
      appHosts = { hosts: [] };
      saveSettings();
    }
  }

  function saveSettings()
  {
    localStorage.setItem(appHostsSettingIdx, JSON.stringify(appHosts));
  }

  function computeHostsFileString()
  {
    let hostsFileString = hostsDelimiterStart+'\\n';
    appHosts.hosts.forEach((host, hostIndex) => {
      if (host.active) {
        hostsFileString += host.str+'\\n';
      }
    });
    hostsFileString += hostsDelimiterEnd+'\\n';

    return hostsFileString;
  }

  function createHostInputElement(hostId, hostString)
  {
    let li = document.createElement('li');
    let group = document.createElement('div');
    group.classList.add('input-group');
    let input = document.createElement('input');
    input.setAttribute('type', 'text');
    input.setAttribute('data-id', hostId);
    input.setAttribute('value', hostString);
    input.classList.add('form-control', 'input-host');
    ['change', 'keyup', 'cut', 'paste'].forEach(event => input.addEventListener(event, liveSaveSettingsEvent));
    let groupBtn = document.createElement('span');
    groupBtn.classList.add('input-group-btn');
    let btnRemove = document.createElement('button');
    btnRemove.setAttribute('type', 'button');
    btnRemove.setAttribute('data-id', hostId);
    btnRemove.classList.add('btn', 'btn-default');
    btnRemove.addEventListener('click', deleteRowEvent);
    let btnRemoveIcon = document.createElement('span');
    btnRemoveIcon.classList.add('glyphicon', 'glyphicon-remove');
    btnRemove.appendChild(btnRemoveIcon);
    groupBtn.appendChild(btnRemove);
    group.appendChild(input);
    group.appendChild(groupBtn);
    li.appendChild(group);

    return li;
  }

  function showHostsList(appHosts) {
    appHosts.forEach((host, hostIndex) => {
      const li = createHostInputElement(hostIndex, host.str);
      document.querySelector('#hosts ul').appendChild(li);
    });
  }

  function udpateHostsList(appHosts) {
    const list = document.querySelector('#hosts ul');
    while (list.firstChild) {
      list.removeChild(list.firstChild);
    }
    showHostsList(appHosts);
  }

  function liveSaveSettingsEvent(event) {
    const hostId = parseInt(event.target.getAttribute('data-id'));
    appHosts.hosts[hostId].str = event.target.value;
    saveSettings();
  }

  function deleteRowEvent(event) {
    let btn = event.target;
    if (event.target.classList.contains('glyphicon')) {
      btn = event.target.parentElement;
    }
    const hostId = parseInt(event.target.getAttribute('data-id'));
    appHosts.hosts.splice(hostId, 1);
    saveSettings();
    event.target.parentElement.parentElement.parentElement.remove();
    udpateHostsList(appHosts.hosts);
  }

  document.querySelector('#load-hosts').addEventListener('click', function(e){
    sudo.exec('cat '+hostsFilePath, options, function(error, stdout, stderr) {
      //$('#hosts > p').html(stdout);
      if ((m = regex.exec(stdout)) !== null) {
        if (m.length == 2) {
          const configLines = m[1].split('\n');
          configLines.forEach((line, lineIndex) => {
            console.log(line);
          });
          // showHostsList(configLines);
        }
      }
    });
  });

  document.querySelector('.btn-save-hosts').addEventListener('click', function(event){
    const timeStart = performance.now();
    let button = event.target;
    if (button.classList.contains('text-state') || button.classList.contains('glyphicon')) {
      button = button.parentElement;
    } else if (button.tagName == 'EM') {
      button = button.parentElement.parentElement;
    }
    console.log(button);
    button.classList.add('saving');
    const cmd = "bash -c \""+sedDelete+";"+"sed -i '$ a\\"+computeHostsFileString()+"' " + hostsFilePath+"\"";
    sudo.exec(cmd, options, function(error, stdout, stderr) {
      if (!error){
        // const fileContent = computeHostsFileString();
        // const fileUpdateCmd = 'echo "'+fileContent+'" | tee -a '+hostsFilePath;
        // const fileUpdateCmd = "sed -i '$ a\\"+fileContent+"' " + hostsFilePath;
        // sudo.exec(fileUpdateCmd, options, function(error, stdout, stderr) {
        //   if (!error){

        //   } else {
        //     console.error(error);
        //     console.log(stderr);
        //   }
        // });
        const timeEnd = performance.now();
        if (timeEnd - timeStart < 1000) {
          setTimeout(function(){
            button.classList.remove('saving');
            button.classList.add('saved');
          }, 1000 - (timeEnd - timeStart));
          setTimeout(function(){
            button.classList.remove('saved');
          }, uiAnimSavedDuration + (1000 - (timeEnd - timeStart)));
        } else {
          button.classList.remove('saving');
          button.classList.add('saved');
          setTimeout(function(){
            button.classList.remove('saved');
          }, uiAnimSavedDuration);
        }
      } else {
        button.classList.remove('saving');
        button.classList.add('error');
        console.error(error);
        console.log(stderr);
      }
    });
  });

  document.querySelector(".btn-add").addEventListener('click', function(event) {
    event.preventDefault();
    appHosts.hosts.push({str: '', active: true});
    saveSettings();
    const li = createHostInputElement((appHosts.hosts.length - 1), '');
    document.querySelector('#hosts ul').appendChild(li);
  });

  document.querySelector('.btn-remove').addEventListener('click', function(event) {
    event.preventDefault();
    appHosts.hosts.pop();
    saveSettings();
    document.querySelector('#hosts ul li:last-child').remove();
  });

  document.getElementById('title').innerHTML = data.appName + ' App';
  loadSettings();

  var app = new Vue({
    el: '#app',
    data: {
      hosts: [
        { str: '127.0.0.1 ', active: true },
        { str: ' ', active: false }
      ]
    },
    beforeMount: function() {
      const jsonHosts = localStorage.getItem(settingsHostsIdx);
      if (jsonHosts) {
        this.hosts = JSON.parse(jsonHosts);
      } else {

      }
    },
    methods: {
      addHost: function(event) {
        event.preventDefault();
        this.hosts.push({ str: '', active: false });
      },
      removeHost: function(event) {
        event.preventDefault();
        this.hosts.pop();
      },
      saveHostsSettings: function() {
        localStorage.setItem(settingsHostsIdx, JSON.stringify(this.hosts));
      }
    },
    watch: {
      hosts: function(newHosts) {
        this.saveHostsSettings();
      }
    }
  });
});<|endoftext|>


<commit_before>// This is a manifest file that'll be compiled into including all the files listed below.
// Add new JavaScript/Coffee code in separate files in this directory and they'll automatically
// be included in the compiled file accessible from http://example.com/assets/application.js
// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
// the compiled file.
//
//= require jquery_ujs
//= require_tree ./common
//= require_tree ./application<commit_msg>Update application.js.<commit_after>// This is a manifest file that'll be compiled into including all the files listed below.
// Add new JavaScript/Coffee code in separate files in this directory and they'll automatically
// be included in the compiled file accessible from http://example.com/assets/application.js
// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
// the compiled file.
//
//= require jquery_ujs
//= require_tree ./common<|endoftext|>


<commit_before>Ext.require([
    'Ext.Window',
    'GeoExt.panel.Map',
    'GeoExt.data.PrintProvider',
    'GeoExt.panel.PrintMap',
    'GeoExt.plugins.PrintExtent'
]);

var mapPanel, printProvider;

Ext.onReady(function() {
    // The printProvider that connects us to the print service
    printProvider = new GeoExt.data.PrintProvider({
        method: "GET", // "POST" recommended for production use
        capabilities: printCapabilities, // from the info.json script in the html
        customParams: {
            mapTitle: "Printing Demo",
            comment: "This is a map printed from GeoExt."
        }
    });

    var printExtent = new GeoExt.plugins.PrintExtent({
        printProvider: printProvider
    });

    // The map we want to print, with the PrintExtent added as item.
    mapPanel = new GeoExt.MapPanel({
        renderTo: "content",
        width: 450,
        height: 320,
        layers: [new OpenLayers.Layer.WMS("Tasmania", "http://demo.opengeo.org/geoserver/wms",
            {layers: "topp:tasmania_state_boundaries"}, {singleTile: true})],
        center: [146.56, -41.56],
        zoom: 6,
        plugins: [printExtent],
        bbar: [{
            text: "Create PDF",
            handler: function() {
                // the PrintExtent plugin is the mapPanel's 1st plugin
                mapPanel.plugins[0].print();
            }
        }]
    });
    printExtent.addPage();
});
<commit_msg>Adapted the PrintExtent example to ExtJS 4 syntax.<commit_after>Ext.require([
    'GeoExt.panel.Map',
    'GeoExt.data.PrintProvider',
    'GeoExt.plugins.PrintExtent'
]);

var mapPanel, printProvider;

Ext.onReady(function() {
    // The printProvider that connects us to the print service
    printProvider = Ext.create('GeoExt.data.PrintProvider', {
        method: "GET", // "POST" recommended for production use
        capabilities: printCapabilities, // from the info.json script in the html
        customParams: {
            mapTitle: "Printing Demo",
            comment: "This is a map printed from GeoExt."
        }
    });

    var printExtent = Ext.create('GeoExt.plugins.PrintExtent', {
        printProvider: printProvider
    });

    // The map we want to print, with the PrintExtent added as item.
    mapPanel = Ext.create('GeoExt.panel.Map', {
        renderTo: "content",
        width: 450,
        height: 320,
        layers: [new OpenLayers.Layer.WMS("Tasmania", "http://demo.opengeo.org/geoserver/wms",
            {layers: "topp:tasmania_state_boundaries"}, {singleTile: true})],
        center: [146.56, -41.56],
        zoom: 6,
        plugins: [printExtent],
        bbar: [{
            text: "Create PDF",
            handler: function() {
                // the PrintExtent plugin is the mapPanel's 1st plugin
                mapPanel.plugins[0].print();
            }
        }]
    });
    printExtent.addPage();
});
<|endoftext|>