Skip to content

Commit

Permalink
Open multi-file torrents as playlists
Browse files Browse the repository at this point in the history
  • Loading branch information
Goldob committed Sep 1, 2016
1 parent db650ca commit b8f6f9f
Show file tree
Hide file tree
Showing 13 changed files with 309 additions and 74 deletions.
4 changes: 4 additions & 0 deletions src/main/ipc.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ function init () {
thumbar.enable()
})

ipc.on('onPlayerUpdate', function (e, ...args) {
menu.onPlayerUpdate(...args)
})

ipc.on('onPlayerClose', function () {
menu.setPlayerOpen(false)
powerSaveBlocker.disable()
Expand Down
28 changes: 28 additions & 0 deletions src/main/menu.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module.exports = {
setPlayerOpen,
setWindowFocus,
setAllowNav,
onPlayerUpdate,
onToggleAlwaysOnTop,
onToggleFullScreen
}
Expand All @@ -25,13 +26,25 @@ function init () {

function setPlayerOpen (flag) {
getMenuItem('Play/Pause').enabled = flag
getMenuItem('Skip Next').enabled = flag
getMenuItem('Skip Previous').enabled = flag
getMenuItem('Increase Volume').enabled = flag
getMenuItem('Decrease Volume').enabled = flag
getMenuItem('Step Forward').enabled = flag
getMenuItem('Step Backward').enabled = flag
getMenuItem('Increase Speed').enabled = flag
getMenuItem('Decrease Speed').enabled = flag
getMenuItem('Add Subtitles File...').enabled = flag

if (flag === false) {
getMenuItem('Skip Next').enabled = false
getMenuItem('Skip Previous').enabled = false
}
}

function onPlayerUpdate (hasNext, hasPrevious) {
getMenuItem('Skip Next').enabled = hasNext
getMenuItem('Skip Previous').enabled = hasPrevious
}

function setWindowFocus (flag) {
Expand Down Expand Up @@ -187,6 +200,21 @@ function getMenuTemplate () {
{
type: 'separator'
},
{
label: 'Skip Next',
accelerator: 'N',
click: () => windows.main.dispatch('nextTrack'),
enabled: false
},
{
label: 'Skip Previous',
accelerator: 'P',
click: () => windows.main.dispatch('previousTrack'),
enabled: false
},
{
type: 'separator'
},
{
label: 'Increase Volume',
accelerator: 'CmdOrCtrl+Up',
Expand Down
10 changes: 10 additions & 0 deletions src/main/shortcuts.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,19 @@ function enable () {
'MediaPlayPause',
() => windows.main.dispatch('playPause')
)
electron.globalShortcut.register(
'MediaNextTrack',
() => windows.main.dispatch('nextTrack')
)
electron.globalShortcut.register(
'MediaPreviousTrack',
() => windows.main.dispatch('previousTrack')
)
}

function disable () {
// Return the media key to the OS, so other apps can use it.
electron.globalShortcut.unregister('MediaPlayPause')
electron.globalShortcut.unregister('MediaNextTrack')
electron.globalShortcut.unregister('MediaPreviousTrack')
}
3 changes: 2 additions & 1 deletion src/renderer/controllers/media-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ module.exports = class MediaController {

openExternalPlayer () {
var state = this.state
ipcRenderer.send('openExternalPlayer', state.saved.prefs.externalPlayerPath, state.server.localURL, state.window.title)
var mediaURL = state.server.localURL + '/' + state.playlist.getCurrent().fileIndex
ipcRenderer.send('openExternalPlayer', state.saved.prefs.externalPlayerPath, mediaURL, state.window.title)
state.playing.location = 'external'
}

Expand Down
176 changes: 121 additions & 55 deletions src/renderer/controllers/playback-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -26,16 +27,37 @@ module.exports = class PlaybackController {
// * Stream, if not already fully downloaded
// * If no file index is provided, pick the default file to play
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.play()
state.playlist.jumpToFile(infoHash, index)
this.updatePlayer(false, callback)
} else {
var torrentSummary = TorrentSummary.getByKey(this.state, infoHash)
var playlist = new Playlist(torrentSummary)

// 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)
if (index === undefined) return this.onError(new errors.UnplayableError())

playlist.jumpToFile(infoHash, index)

state.location.go({
url: 'player',
setup: (cb) => {
this.play()
this.openPlayer(playlist, cb)
},
destroy: () => this.closePlayer()
}, (err) => {
if (err) dispatch('error', err)
})
}

function callback (err) {
if (err) this.onError(err)
}
}

// Open a file in OS default app.
Expand Down Expand Up @@ -64,6 +86,30 @@ module.exports = class PlaybackController {
else this.pause()
}

// Play next file in list (if any)
nextTrack () {
var state = this.state
if (state.playlist && state.playlist.hasNext()) {
state.playlist.next()
this.updatePlayer(false, (err) => {
if (err) this.onError(err)
else this.play()
})
}
}

// Play previous track in list (if any)
previousTrack () {
var state = this.state
if (state.playlist && state.playlist.hasPrevious()) {
state.playlist.previous()
this.updatePlayer(false, (err) => {
if (err) this.onError(err)
else this.play()
})
}
}

// Play (unpause) the current media
play () {
var state = this.state
Expand Down Expand Up @@ -167,14 +213,16 @@ module.exports = class PlaybackController {
return false
}

// Opens the video player to a specific torrent
openPlayer (infoHash, index, cb) {
var torrentSummary = TorrentSummary.getByKey(this.state, infoHash)
// Opens the video player to a specific playlist
openPlayer (playlist, cb) {
var state = this.state
state.playlist = playlist

var track = playlist.getCurrent()
if (track === undefined) return cb(new errors.UnplayableError())

// 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)
if (index === undefined) return cb(new errors.UnplayableError())
var torrentSummary = TorrentSummary.getByKey(state, state.playlist.getInfoHash())
state.playing.infoHash = torrentSummary.infoHash

// update UI to show pending playback
if (torrentSummary.progress !== 1) sound.play('PLAY')
Expand All @@ -191,38 +239,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(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) {
var fileSummary = torrentSummary.files[index]
// Called each time the playlist state changes
updatePlayer (resume, cb) {
var state = this.state
var track = state.playlist.getCurrent()

if (track === undefined) {
return cb(new Error('Can\'t play that file'))
}

var torrentSummary = TorrentSummary.getByKey(this.state, state.playlist.getInfoHash())
var fileSummary = torrentSummary.files[track.fileIndex]

// 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'
state.playing.fileIndex = track.fileIndex
state.playing.type = track.type

// 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) {
ipcRenderer.send('wt-get-audio-metadata', torrentSummary.infoHash, index)
ipcRenderer.send('wt-get-audio-metadata', torrentSummary.infoHash, track.fileIndex)
}

// if it's video, check for subtitles files that are done downloading
Expand All @@ -233,34 +311,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', state.playlist.hasNext(), state.playlist.hasPrevious())
cb()
}

closePlayer () {
Expand All @@ -287,6 +352,7 @@ module.exports = class PlaybackController {

// Reset the window contents back to the home screen
state.playing = State.getDefaultPlayState()
state.playlist = null
state.server = null

// Reset the window size and location back to where it was
Expand Down
6 changes: 3 additions & 3 deletions src/renderer/lib/cast.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ function chromecastPlayer () {

function open () {
var torrentSummary = state.saved.torrents.find((x) => x.infoHash === state.playing.infoHash)
ret.device.play(state.server.networkURL, {
ret.device.play(state.server.networkURL + '/' + state.playing.fileIndex, {
type: 'video/mp4',
title: config.APP_NAME + ' - ' + torrentSummary.name
}, function (err) {
Expand Down Expand Up @@ -183,7 +183,7 @@ function airplayPlayer () {
}

function open () {
ret.device.play(state.server.networkURL, function (err, res) {
ret.device.play(state.server.networkURL + '/' + state.playing.fileIndex, function (err, res) {
if (err) {
state.playing.location = 'local'
state.errors.push({
Expand Down Expand Up @@ -275,7 +275,7 @@ function dlnaPlayer (player) {

function open () {
var torrentSummary = state.saved.torrents.find((x) => x.infoHash === state.playing.infoHash)
ret.device.play(state.server.networkURL, {
ret.device.play(state.server.networkURL + '/' + state.playing.fileIndex, {
type: 'video/mp4',
title: config.APP_NAME + ' - ' + torrentSummary.name,
seek: state.playing.currentTime > 10 ? state.playing.currentTime : 0
Expand Down
13 changes: 10 additions & 3 deletions src/renderer/lib/errors.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
module.exports = {
UnplayableError
UnplayableTorrentError,
UnplayableFileError
}

function UnplayableError () {
function UnplayableTorrentError () {
this.message = 'Can\'t play any files in torrent'
}
UnplayableError.prototype = Error

function UnplayableFileError () {
this.message = 'Can\'t play that file'
}

UnplayableTorrentError.prototype = Error
UnplayableFileError.prototype = Error
Loading

0 comments on commit b8f6f9f

Please sign in to comment.