Skip to content
Snippets Groups Projects
queue.js 7.51 KiB
Newer Older
import logger from '@/logging'
import cache from '@/cache'
import config from '@/config'
import Audio from '@/audio'
import backend from '@/audio/backend'
import radios from '@/radios'
import Vue from 'vue'
import url from '@/utils/url'
import auth from '@/auth'

class Queue {
  constructor (options = {}) {
    logger.default.info('Instanciating queue')
    this.previousQueue = cache.get('queue')
    this.tracks = []
    this.currentIndex = -1
    this.currentTrack = null
    this.ended = true
    this.state = {
      volume: cache.get('volume', 0.5)
    }
    this.audio = {
      state: {
        startLoad: false,
        failed: false,
        try: 3,
        tried: 0,
        playing: false,
        paused: false,
        playbackRate: 1.0,
        progress: 0,
        currentTime: 0,
        duration: 0,
        volume: this.state.volume,
        loaded: '0',
        durationTimerFormat: '00:00',
        currentTimeFormat: '00:00',
        lastTimeFormat: '00:00'
      }
    }
  }

  cache () {
    let cached = {
      tracks: this.tracks.map(track => {
        // we keep only valuable fields to make the cache lighter and avoid
        // cyclic value serialization errors
        let artist = {
          id: track.artist.id,
          mbid: track.artist.mbid,
          name: track.artist.name
        }
        return {
          id: track.id,
          title: track.title,
          mbid: track.mbid,
          album: {
            id: track.album.id,
            title: track.album.title,
            mbid: track.album.mbid,
            cover: track.album.cover,
            artist: artist
          },
          artist: artist,
          files: track.files
        }
      }),
      currentIndex: this.currentIndex
    }
    cache.set('queue', cached)
  }

  restore () {
    let cached = cache.get('queue')
    if (!cached) {
      return false
    }
    logger.default.info('Restoring previous queue...')
    this.tracks = cached.tracks
    this.play(cached.currentIndex)
    this.previousQueue = null
    return true
  }
  removePrevious () {
    this.previousQueue = undefined
    cache.remove('queue')
  }
  setVolume (newValue) {
    newValue = Math.min(newValue, 1)
    newValue = Math.max(newValue, 0)
    this.state.volume = newValue
    if (this.audio.setVolume) {
      this.audio.setVolume(newValue)
    } else {
      this.audio.state.volume = newValue
    }
    cache.set('volume', newValue)
  }
  incrementVolume (value) {
    this.setVolume(this.state.volume + value)
  }
  reorder (oldIndex, newIndex) {
    // called when the user uses drag / drop to reorder
    // tracks in queue
    if (oldIndex === this.currentIndex) {
      this.currentIndex = newIndex
      return
    }
    if (oldIndex < this.currentIndex && newIndex >= this.currentIndex) {
      // item before was moved after
      this.currentIndex -= 1
    }
    if (oldIndex > this.currentIndex && newIndex <= this.currentIndex) {
      // item after was moved before
      this.currentIndex += 1
    }
  }

  append (track, index, skipPlay) {
    this.previousQueue = null
    index = index || this.tracks.length
    if (index > this.tracks.length - 1) {
      // we simply push to the end
      this.tracks.push(track)
    } else {
      // we insert the track at given position
      this.tracks.splice(index, 0, track)
    }
    if (!skipPlay) {
      this.resumeQueue()
    logger.default.info('Appending many tracks to the queue', tracks.map(e => { return e.title }))
    if (this.tracks.length === 0) {
      index = 0
    } else {
      index = index || this.tracks.length
    }
    this.resumeQueue()
  }

  resumeQueue () {
    if (this.ended | this.errored) {
      this.next()
    }
  }

  populateFromRadio () {
    if (!radios.running) {
      return
    }
    var self = this
    radios.fetch().then((response) => {
      logger.default.info('Adding track to queue from radio')
      self.append(response.data.track)
    }, (response) => {
      logger.default.error('Error while adding track to queue from radio')
    })
  }

  clean () {
    this.stop()
    this.tracks = []
    this.currentIndex = -1
    this.currentTrack = null
    // so we replay automatically on next track append
    this.ended = true
    // are we removing current playin track
    let current = index === this.currentIndex
    if (current) {
      this.stop()
    }
    if (index < this.currentIndex) {
      this.currentIndex -= 1
    }
    this.tracks.splice(index, 1)
    if (current) {
      // we play next track, which now have the same index
      this.play(index)
    }
    if (this.currentIndex === this.tracks.length - 1) {
      this.populateFromRadio()
    }
    if (this.audio.pause) {
      this.audio.pause()
    }
    if (this.audio.destroyed) {
      this.audio.destroyed()
    }
    let self = this
    let currentIndex = index
    let currentTrack = this.tracks[index]

    if (this.audio.destroyed) {
      logger.default.debug('Destroying previous audio...', index - 1)
      this.audio.destroyed()
    }


    this.currentIndex = currentIndex
    this.currentTrack = currentTrack

    let file = this.currentTrack.files[0]
    if (!file) {
    let path = backend.absoluteUrl(file.path)
    if (auth.user.authenticated) {
      // we need to send the token directly in url
      // so authentication can be checked by the backend
      // because for audio files we cannot use the regular Authentication
      // header
      path = url.updateQueryString(path, 'jwt', auth.getAuthToken())
    }

    let audio = new Audio(path, {
      preload: true,
      autoplay: true,
      rate: 1,
      loop: false,
      volume: this.state.volume,
      onEnded: this.handleAudioEnded.bind(this),
      onError: function (src) {
        self.errored = true
        self.next()
      }
    this.audio = audio
    audio.updateHook('playState', function (e) {
      // in some situations, we may have a race condition, for example
      // if the user spams the next / previous buttons, with multiple audios
      // playing at the same time. To avoid that, we ensure the audio
      // still matches de queue current audio
      if (audio !== self.audio) {
        logger.default.debug('Destroying duplicate audio')
        audio.destroyed()
      }
    })
    if (this.currentIndex === this.tracks.length - 1) {
      this.populateFromRadio()
    }
    this.cache()
  }

  handleAudioEnded (e) {
    this.recordListen(this.currentTrack)
    if (this.currentIndex < this.tracks.length - 1) {
      logger.default.info('Audio track ended, playing next one')
      this.next()
    } else {
      logger.default.info('We reached the end of the queue')
      this.ended = true
    }
  }

  recordListen (track) {
    let url = config.API_URL + 'history/listenings/'
    let resource = Vue.resource(url)
    resource.save({}, {'track': track.id}).then((response) => {}, (response) => {
      logger.default.error('Could not record track in history')
    })
  }

  previous () {
    if (this.currentIndex > 0) {
      this.play(this.currentIndex - 1)
    }
  }

  next () {
    if (this.currentIndex < this.tracks.length - 1) {
      logger.default.debug('Playing next track')
      this.play(this.currentIndex + 1)
    }
  }

}

let queue = new Queue()

export default queue