Add playlists feature (#871)

* Open multi-file torrents as playlists

* Add `repeat` and `shuffle` options to the player

* Autoplay first file in torrent

* replaces `pickFileToPlay` feature
* when reopening player, restores the most recently viewed file

* Add playlist navigation buttons to Windows thumbar

* Remove `repeat` and `shuffle` options

This reverts commit 9284122461.

* Play files in order they appear in torrent

* Clean up playlists code
This commit is contained in:
Adam Gotlib
2016-09-02 04:18:48 +02:00
committed by DC
parent 14102ab3e6
commit 30732305ff
18 changed files with 363 additions and 117 deletions

View File

@@ -8,6 +8,7 @@ const errors = require('../lib/errors')
const sound = require('../lib/sound')
const TorrentPlayer = require('../lib/torrent-player')
const TorrentSummary = require('../lib/torrent-summary')
const Playlist = require('../lib/playlist')
const State = require('../lib/state')
const ipcRenderer = electron.ipcRenderer
@@ -24,18 +25,29 @@ module.exports = class PlaybackController {
// Play a file in a torrent.
// * Start torrenting, if necessary
// * Stream, if not already fully downloaded
// * If no file index is provided, pick the default file to play
// * If no file index is provided, restore the most recently viewed file or autoplay the first
playFile (infoHash, index /* optional */) {
this.state.location.go({
url: 'player',
setup: (cb) => {
this.play()
this.openPlayer(infoHash, index, cb)
},
destroy: () => this.closePlayer()
}, (err) => {
if (err) dispatch('error', err)
})
var state = this.state
if (state.location.url() === 'player') {
this.updatePlayer(infoHash, index, false, (err) => {
if (err) dispatch('error', err)
else this.play()
})
} else {
state.location.go({
url: 'player',
setup: (cb) => {
this.play()
this.openPlayer(infoHash, index, (err) => {
if (!err) this.play
cb(err)
})
},
destroy: () => this.closePlayer()
}, (err) => {
if (err) dispatch('error', err)
})
}
}
// Open a file in OS default app.
@@ -64,6 +76,30 @@ module.exports = class PlaybackController {
else this.pause()
}
// Play next file in list (if any)
nextTrack () {
var state = this.state
if (Playlist.hasNext(state)) {
this.updatePlayer(
state.playing.infoHash, Playlist.getNextIndex(state), false, (err) => {
if (err) dispatch('error', err)
else this.play()
})
}
}
// Play previous track in list (if any)
previousTrack () {
var state = this.state
if (Playlist.hasPrevious(state)) {
this.updatePlayer(
state.playing.infoHash, Playlist.getPreviousIndex(state), false, (err) => {
if (err) dispatch('error', err)
else this.play()
})
}
}
// Play (unpause) the current media
play () {
var state = this.state
@@ -169,13 +205,16 @@ module.exports = class PlaybackController {
// Opens the video player to a specific torrent
openPlayer (infoHash, index, cb) {
var torrentSummary = TorrentSummary.getByKey(this.state, infoHash)
var state = this.state
// automatically choose which file in the torrent to play, if necessary
if (index === undefined) index = torrentSummary.defaultPlayFileIndex
if (index === undefined) index = TorrentPlayer.pickFileToPlay(torrentSummary.files)
var torrentSummary = TorrentSummary.getByKey(state, infoHash)
if (index === undefined) index = torrentSummary.mostRecentFileIndex
if (index === undefined) index = torrentSummary.files.findIndex(TorrentPlayer.isPlayable)
if (index === undefined) return cb(new errors.UnplayableError())
state.playing.infoHash = torrentSummary.infoHash
// update UI to show pending playback
if (torrentSummary.progress !== 1) sound.play('PLAY')
// TODO: remove torrentSummary.playStatus
@@ -191,34 +230,68 @@ module.exports = class PlaybackController {
this.update()
}, 10000) /* give it a few seconds */
this.startServer(torrentSummary, () => {
clearTimeout(timeout)
// if we timed out (user clicked play a long time ago), don't autoplay
var timedOut = torrentSummary.playStatus === 'timeout'
delete torrentSummary.playStatus
if (timedOut) {
ipcRenderer.send('wt-stop-server')
return this.update()
}
ipcRenderer.send('onPlayerOpen')
this.updatePlayer(infoHash, index, true, cb)
})
}
// Starts WebTorrent server for media streaming
startServer (torrentSummary, cb) {
if (torrentSummary.status === 'paused') {
dispatch('startTorrentingSummary', torrentSummary.torrentKey)
ipcRenderer.once('wt-ready-' + torrentSummary.infoHash,
() => this.openPlayerFromActiveTorrent(torrentSummary, index, timeout, cb))
() => onTorrentReady())
} else {
this.openPlayerFromActiveTorrent(torrentSummary, index, timeout, cb)
onTorrentReady()
}
function onTorrentReady () {
ipcRenderer.send('wt-start-server', torrentSummary.infoHash)
ipcRenderer.once('wt-server-' + torrentSummary.infoHash, () => cb())
}
}
openPlayerFromActiveTorrent (torrentSummary, index, timeout, cb) {
// Called each time the current file changes
updatePlayer (infoHash, index, resume, cb) {
var state = this.state
var torrentSummary = TorrentSummary.getByKey(state, infoHash)
var fileSummary = torrentSummary.files[index]
if (!TorrentPlayer.isPlayable(fileSummary)) {
torrentSummary.mostRecentFileIndex = undefined
return cb(new Error('Can\'t play that file'))
}
torrentSummary.mostRecentFileIndex = index
// update state
var state = this.state
state.playing.infoHash = torrentSummary.infoHash
state.playing.fileIndex = index
state.playing.type = TorrentPlayer.isVideo(fileSummary) ? 'video'
: TorrentPlayer.isAudio(fileSummary) ? 'audio'
: 'other'
// pick up where we left off
if (fileSummary.currentTime) {
var jumpToTime = 0
if (resume && fileSummary.currentTime) {
var fraction = fileSummary.currentTime / fileSummary.duration
var secondsLeft = fileSummary.duration - fileSummary.currentTime
if (fraction < 0.9 && secondsLeft > 10) {
state.playing.jumpToTime = fileSummary.currentTime
jumpToTime = fileSummary.currentTime
}
}
state.playing.jumpToTime = jumpToTime
// if it's audio, parse out the metadata (artist, title, etc)
if (state.playing.type === 'audio' && !fileSummary.audioInfo) {
@@ -233,34 +306,21 @@ module.exports = class PlaybackController {
dispatch('addSubtitles', [fileSummary.selectedSubtitle], true)
}
ipcRenderer.send('wt-start-server', torrentSummary.infoHash, index)
ipcRenderer.once('wt-server-' + torrentSummary.infoHash, (e, info) => {
clearTimeout(timeout)
state.window.title = fileSummary.name
// if we timed out (user clicked play a long time ago), don't autoplay
var timedOut = torrentSummary.playStatus === 'timeout'
delete torrentSummary.playStatus
if (timedOut) {
ipcRenderer.send('wt-stop-server')
return this.update()
}
state.window.title = torrentSummary.files[state.playing.fileIndex].name
// play in VLC if set as default player (Preferences / Playback / Play in VLC)
if (this.state.saved.prefs.openExternalPlayer) {
dispatch('openExternalPlayer')
this.update()
cb()
return
}
// otherwise, play the video
// play in VLC if set as default player (Preferences / Playback / Play in VLC)
if (this.state.saved.prefs.openExternalPlayer) {
dispatch('openExternalPlayer')
this.update()
ipcRenderer.send('onPlayerOpen')
cb()
})
return
}
// otherwise, play the video
this.update()
ipcRenderer.send('onPlayerUpdate', Playlist.hasNext(state), Playlist.hasPrevious(state))
cb()
}
closePlayer () {