Switch from virtualdom to React
This commit is contained in:
30
src/renderer/lib/capture-video-frame.js
Normal file
30
src/renderer/lib/capture-video-frame.js
Normal file
@@ -0,0 +1,30 @@
|
||||
module.exports = captureVideoFrame
|
||||
|
||||
function captureVideoFrame (video, format) {
|
||||
if (typeof video === 'string') {
|
||||
video = document.querySelector(video)
|
||||
}
|
||||
|
||||
if (video == null || video.nodeName !== 'VIDEO') {
|
||||
throw new Error('First argument must be a <video> element or selector')
|
||||
}
|
||||
|
||||
if (format == null) {
|
||||
format = 'png'
|
||||
}
|
||||
|
||||
if (format !== 'png' && format !== 'jpg' && format !== 'webp') {
|
||||
throw new Error('Second argument must be one of "png", "jpg", or "webp"')
|
||||
}
|
||||
|
||||
var canvas = document.createElement('canvas')
|
||||
canvas.width = video.videoWidth
|
||||
canvas.height = video.videoHeight
|
||||
|
||||
canvas.getContext('2d').drawImage(video, 0, 0)
|
||||
|
||||
var dataUri = canvas.toDataURL('image/' + format)
|
||||
var data = dataUri.split(',')[1]
|
||||
|
||||
return new Buffer(data, 'base64')
|
||||
}
|
||||
455
src/renderer/lib/cast.js
Normal file
455
src/renderer/lib/cast.js
Normal file
@@ -0,0 +1,455 @@
|
||||
// The Cast module talks to Airplay and Chromecast
|
||||
// * Modifies state when things change
|
||||
// * Starts and stops casting, provides remote video controls
|
||||
module.exports = {
|
||||
init,
|
||||
toggleMenu,
|
||||
selectDevice,
|
||||
stop,
|
||||
play,
|
||||
pause,
|
||||
seek,
|
||||
setVolume,
|
||||
setRate
|
||||
}
|
||||
|
||||
// Lazy load these for a ~300ms improvement in startup time
|
||||
var airplayer, chromecasts, dlnacasts
|
||||
|
||||
var config = require('../../config')
|
||||
|
||||
// App state. Cast modifies state.playing and state.errors in response to events
|
||||
var state
|
||||
|
||||
// Callback to notify module users when state has changed
|
||||
var update
|
||||
|
||||
// setInterval() for updating cast status
|
||||
var statusInterval = null
|
||||
|
||||
// Start looking for cast devices on the local network
|
||||
function init (appState, callback) {
|
||||
state = appState
|
||||
update = callback
|
||||
|
||||
// Load modules, scan the network for devices
|
||||
airplayer = require('airplayer')()
|
||||
chromecasts = require('chromecasts')()
|
||||
dlnacasts = require('dlnacasts')()
|
||||
|
||||
state.devices.chromecast = chromecastPlayer()
|
||||
state.devices.dlna = dlnaPlayer()
|
||||
state.devices.airplay = airplayPlayer()
|
||||
|
||||
// Listen for devices: Chromecast, DLNA and Airplay
|
||||
chromecasts.on('update', function (device) {
|
||||
// TODO: how do we tell if there are *no longer* any Chromecasts available?
|
||||
// From looking at the code, chromecasts.players only grows, never shrinks
|
||||
state.devices.chromecast.addDevice(device)
|
||||
})
|
||||
|
||||
dlnacasts.on('update', function (device) {
|
||||
state.devices.dlna.addDevice(device)
|
||||
})
|
||||
|
||||
airplayer.on('update', function (device) {
|
||||
state.devices.airplay.addDevice(device)
|
||||
})
|
||||
}
|
||||
|
||||
// chromecast player implementation
|
||||
function chromecastPlayer () {
|
||||
var ret = {
|
||||
device: null,
|
||||
addDevice,
|
||||
getDevices,
|
||||
open,
|
||||
play,
|
||||
pause,
|
||||
stop,
|
||||
status,
|
||||
seek,
|
||||
volume
|
||||
}
|
||||
return ret
|
||||
|
||||
function getDevices () {
|
||||
return chromecasts.players
|
||||
}
|
||||
|
||||
function addDevice (device) {
|
||||
device.on('error', function (err) {
|
||||
if (device !== ret.device) return
|
||||
state.playing.location = 'local'
|
||||
state.errors.push({
|
||||
time: new Date().getTime(),
|
||||
message: 'Could not connect to Chromecast. ' + err.message
|
||||
})
|
||||
update()
|
||||
})
|
||||
device.on('disconnect', function () {
|
||||
if (device !== ret.device) return
|
||||
state.playing.location = 'local'
|
||||
update()
|
||||
})
|
||||
}
|
||||
|
||||
function open () {
|
||||
var torrentSummary = state.saved.torrents.find((x) => x.infoHash === state.playing.infoHash)
|
||||
ret.device.play(state.server.networkURL, {
|
||||
type: 'video/mp4',
|
||||
title: config.APP_NAME + ' - ' + torrentSummary.name
|
||||
}, function (err) {
|
||||
if (err) {
|
||||
state.playing.location = 'local'
|
||||
state.errors.push({
|
||||
time: new Date().getTime(),
|
||||
message: 'Could not connect to Chromecast. ' + err.message
|
||||
})
|
||||
} else {
|
||||
state.playing.location = 'chromecast'
|
||||
}
|
||||
update()
|
||||
})
|
||||
}
|
||||
|
||||
function play (callback) {
|
||||
ret.device.play(null, null, callback)
|
||||
}
|
||||
|
||||
function pause (callback) {
|
||||
ret.device.pause(callback)
|
||||
}
|
||||
|
||||
function stop (callback) {
|
||||
ret.device.stop(callback)
|
||||
}
|
||||
|
||||
function status () {
|
||||
ret.device.status(function (err, status) {
|
||||
if (err) return console.log('error getting %s status: %o', state.playing.location, err)
|
||||
state.playing.isPaused = status.playerState === 'PAUSED'
|
||||
state.playing.currentTime = status.currentTime
|
||||
state.playing.volume = status.volume.muted ? 0 : status.volume.level
|
||||
update()
|
||||
})
|
||||
}
|
||||
|
||||
function seek (time, callback) {
|
||||
ret.device.seek(time, callback)
|
||||
}
|
||||
|
||||
function volume (volume, callback) {
|
||||
ret.device.volume(volume, callback)
|
||||
}
|
||||
}
|
||||
|
||||
// airplay player implementation
|
||||
function airplayPlayer () {
|
||||
var ret = {
|
||||
device: null,
|
||||
addDevice,
|
||||
getDevices,
|
||||
open,
|
||||
play,
|
||||
pause,
|
||||
stop,
|
||||
status,
|
||||
seek,
|
||||
volume
|
||||
}
|
||||
return ret
|
||||
|
||||
function addDevice (player) {
|
||||
player.on('event', function (event) {
|
||||
switch (event.state) {
|
||||
case 'loading':
|
||||
break
|
||||
case 'playing':
|
||||
state.playing.isPaused = false
|
||||
break
|
||||
case 'paused':
|
||||
state.playing.isPaused = true
|
||||
break
|
||||
case 'stopped':
|
||||
break
|
||||
}
|
||||
update()
|
||||
})
|
||||
}
|
||||
|
||||
function getDevices () {
|
||||
return airplayer.players
|
||||
}
|
||||
|
||||
function open () {
|
||||
ret.device.play(state.server.networkURL, function (err, res) {
|
||||
if (err) {
|
||||
state.playing.location = 'local'
|
||||
state.errors.push({
|
||||
time: new Date().getTime(),
|
||||
message: 'Could not connect to AirPlay. ' + err.message
|
||||
})
|
||||
} else {
|
||||
state.playing.location = 'airplay'
|
||||
}
|
||||
update()
|
||||
})
|
||||
}
|
||||
|
||||
function play (callback) {
|
||||
ret.device.resume(callback)
|
||||
}
|
||||
|
||||
function pause (callback) {
|
||||
ret.device.pause(callback)
|
||||
}
|
||||
|
||||
function stop (callback) {
|
||||
ret.device.stop(callback)
|
||||
}
|
||||
|
||||
function status () {
|
||||
ret.device.playbackInfo(function (err, res, status) {
|
||||
if (err) {
|
||||
state.playing.location = 'local'
|
||||
state.errors.push({
|
||||
time: new Date().getTime(),
|
||||
message: 'Could not connect to AirPlay. ' + err.message
|
||||
})
|
||||
} else {
|
||||
state.playing.isPaused = status.rate === 0
|
||||
state.playing.currentTime = status.position
|
||||
update()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function seek (time, callback) {
|
||||
ret.device.scrub(time, callback)
|
||||
}
|
||||
|
||||
function volume (volume, callback) {
|
||||
// AirPlay doesn't support volume
|
||||
// TODO: We should just disable the volume slider
|
||||
state.playing.volume = volume
|
||||
}
|
||||
}
|
||||
|
||||
// DLNA player implementation
|
||||
function dlnaPlayer (player) {
|
||||
var ret = {
|
||||
device: null,
|
||||
addDevice,
|
||||
getDevices,
|
||||
open,
|
||||
play,
|
||||
pause,
|
||||
stop,
|
||||
status,
|
||||
seek,
|
||||
volume
|
||||
}
|
||||
return ret
|
||||
|
||||
function getDevices () {
|
||||
return dlnacasts.players
|
||||
}
|
||||
|
||||
function addDevice (device) {
|
||||
device.on('error', function (err) {
|
||||
if (device !== ret.device) return
|
||||
state.playing.location = 'local'
|
||||
state.errors.push({
|
||||
time: new Date().getTime(),
|
||||
message: 'Could not connect to DLNA. ' + err.message
|
||||
})
|
||||
update()
|
||||
})
|
||||
device.on('disconnect', function () {
|
||||
if (device !== ret.device) return
|
||||
state.playing.location = 'local'
|
||||
update()
|
||||
})
|
||||
}
|
||||
|
||||
function open () {
|
||||
var torrentSummary = state.saved.torrents.find((x) => x.infoHash === state.playing.infoHash)
|
||||
ret.device.play(state.server.networkURL, {
|
||||
type: 'video/mp4',
|
||||
title: config.APP_NAME + ' - ' + torrentSummary.name,
|
||||
seek: state.playing.currentTime > 10 ? state.playing.currentTime : 0
|
||||
}, function (err) {
|
||||
if (err) {
|
||||
state.playing.location = 'local'
|
||||
state.errors.push({
|
||||
time: new Date().getTime(),
|
||||
message: 'Could not connect to DLNA. ' + err.message
|
||||
})
|
||||
} else {
|
||||
state.playing.location = 'dlna'
|
||||
}
|
||||
update()
|
||||
})
|
||||
}
|
||||
|
||||
function play (callback) {
|
||||
ret.device.play(null, null, callback)
|
||||
}
|
||||
|
||||
function pause (callback) {
|
||||
ret.device.pause(callback)
|
||||
}
|
||||
|
||||
function stop (callback) {
|
||||
ret.device.stop(callback)
|
||||
}
|
||||
|
||||
function status () {
|
||||
ret.device.status(function (err, status) {
|
||||
if (err) return console.log('error getting %s status: %o', state.playing.location, err)
|
||||
state.playing.isPaused = status.playerState === 'PAUSED'
|
||||
state.playing.currentTime = status.currentTime
|
||||
state.playing.volume = status.volume.level
|
||||
update()
|
||||
})
|
||||
}
|
||||
|
||||
function seek (time, callback) {
|
||||
ret.device.seek(time, callback)
|
||||
}
|
||||
|
||||
function volume (volume, callback) {
|
||||
ret.device.volume(volume, function (err) {
|
||||
// quick volume update
|
||||
state.playing.volume = volume
|
||||
callback(err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Start polling cast device state, whenever we're connected
|
||||
function startStatusInterval () {
|
||||
statusInterval = setInterval(function () {
|
||||
var player = getPlayer()
|
||||
if (player) player.status()
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
/*
|
||||
* Shows the device menu for a given cast type ('chromecast', 'airplay', etc)
|
||||
* The menu lists eg. all Chromecasts detected; the user can click one to cast.
|
||||
* If the menu was already showing for that type, hides the menu.
|
||||
*/
|
||||
function toggleMenu (location) {
|
||||
// If the menu is already showing, hide it
|
||||
if (state.devices.castMenu && state.devices.castMenu.location === location) {
|
||||
state.devices.castMenu = null
|
||||
return
|
||||
}
|
||||
|
||||
// Never cast to two devices at the same time
|
||||
if (state.playing.location !== 'local') {
|
||||
throw new Error('You can\'t connect to ' + location + ' when already connected to another device')
|
||||
}
|
||||
|
||||
// Find all cast devices of the given type
|
||||
var player = getPlayer(location)
|
||||
var devices = player ? player.getDevices() : []
|
||||
if (devices.length === 0) throw new Error('No ' + location + ' devices available')
|
||||
|
||||
// Show a menu
|
||||
state.devices.castMenu = {location, devices}
|
||||
}
|
||||
|
||||
function selectDevice (index) {
|
||||
var {location, devices} = state.devices.castMenu
|
||||
|
||||
// Start casting
|
||||
var player = getPlayer(location)
|
||||
player.device = devices[index]
|
||||
player.open()
|
||||
|
||||
// Poll the casting device's status every few seconds
|
||||
startStatusInterval()
|
||||
|
||||
// Show the Connecting... screen
|
||||
state.devices.castMenu = null
|
||||
state.playing.castName = devices[index].name
|
||||
state.playing.location = location + '-pending'
|
||||
update()
|
||||
}
|
||||
|
||||
// Stops casting, move video back to local screen
|
||||
function stop () {
|
||||
var player = getPlayer()
|
||||
if (player) {
|
||||
player.stop(function () {
|
||||
player.device = null
|
||||
stoppedCasting()
|
||||
})
|
||||
clearInterval(statusInterval)
|
||||
} else {
|
||||
stoppedCasting()
|
||||
}
|
||||
}
|
||||
|
||||
function stoppedCasting () {
|
||||
state.playing.location = 'local'
|
||||
state.playing.jumpToTime = state.playing.currentTime
|
||||
update()
|
||||
}
|
||||
|
||||
function getPlayer (location) {
|
||||
if (location) {
|
||||
return state.devices[location]
|
||||
} else if (state.playing.location === 'chromecast') {
|
||||
return state.devices.chromecast
|
||||
} else if (state.playing.location === 'airplay') {
|
||||
return state.devices.airplay
|
||||
} else if (state.playing.location === 'dlna') {
|
||||
return state.devices.dlna
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function play () {
|
||||
var player = getPlayer()
|
||||
if (player) player.play(castCallback)
|
||||
}
|
||||
|
||||
function pause () {
|
||||
var player = getPlayer()
|
||||
if (player) player.pause(castCallback)
|
||||
}
|
||||
|
||||
function setRate (rate) {
|
||||
var player
|
||||
var result = true
|
||||
if (state.playing.location === 'chromecast') {
|
||||
// TODO find how to control playback rate on chromecast
|
||||
castCallback()
|
||||
result = false
|
||||
} else if (state.playing.location === 'airplay') {
|
||||
player = state.devices.airplay
|
||||
player.rate(rate, castCallback)
|
||||
} else {
|
||||
result = false
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
function seek (time) {
|
||||
var player = getPlayer()
|
||||
if (player) player.seek(time, castCallback)
|
||||
}
|
||||
|
||||
function setVolume (volume) {
|
||||
var player = getPlayer()
|
||||
if (player) player.volume(volume, castCallback)
|
||||
}
|
||||
|
||||
function castCallback () {
|
||||
console.log('%s callback: %o', state.playing.location, arguments)
|
||||
}
|
||||
39
src/renderer/lib/dispatcher.js
Normal file
39
src/renderer/lib/dispatcher.js
Normal file
@@ -0,0 +1,39 @@
|
||||
module.exports = {
|
||||
dispatch,
|
||||
dispatcher,
|
||||
setDispatch
|
||||
}
|
||||
|
||||
var dispatchers = {}
|
||||
var _dispatch = function () {}
|
||||
|
||||
function setDispatch (dispatch) {
|
||||
_dispatch = dispatch
|
||||
}
|
||||
|
||||
function dispatch (...args) {
|
||||
_dispatch(...args)
|
||||
}
|
||||
|
||||
// Most DOM event handlers are trivial functions like `() => dispatch(<args>)`.
|
||||
// For these, `dispatcher(<args>)` is preferred because it memoizes the handler
|
||||
// function. This prevents React from updating the listener functions on
|
||||
// each update().
|
||||
function dispatcher (...args) {
|
||||
var str = JSON.stringify(args)
|
||||
var handler = dispatchers[str]
|
||||
if (!handler) {
|
||||
handler = dispatchers[str] = function (e) {
|
||||
// Do not propagate click to elements below the button
|
||||
e.stopPropagation()
|
||||
|
||||
if (e.currentTarget.classList.contains('disabled')) {
|
||||
// Ignore clicks on disabled elements
|
||||
return
|
||||
}
|
||||
|
||||
dispatch(...args)
|
||||
}
|
||||
}
|
||||
return handler
|
||||
}
|
||||
8
src/renderer/lib/errors.js
Normal file
8
src/renderer/lib/errors.js
Normal file
@@ -0,0 +1,8 @@
|
||||
module.exports = {
|
||||
UnplayableError
|
||||
}
|
||||
|
||||
function UnplayableError () {
|
||||
this.message = 'Can\'t play any files in torrent'
|
||||
}
|
||||
UnplayableError.prototype = Error
|
||||
125
src/renderer/lib/location-history.js
Normal file
125
src/renderer/lib/location-history.js
Normal file
@@ -0,0 +1,125 @@
|
||||
module.exports = LocationHistory
|
||||
|
||||
function LocationHistory () {
|
||||
this._history = []
|
||||
this._forward = []
|
||||
this._pending = false
|
||||
}
|
||||
|
||||
LocationHistory.prototype.url = function () {
|
||||
return this.current() && this.current().url
|
||||
}
|
||||
|
||||
LocationHistory.prototype.current = function () {
|
||||
return this._history[this._history.length - 1]
|
||||
}
|
||||
|
||||
LocationHistory.prototype.go = function (page, cb) {
|
||||
if (!cb) cb = noop
|
||||
if (this._pending) return cb(null)
|
||||
|
||||
console.log('go', page)
|
||||
|
||||
this.clearForward()
|
||||
this._go(page, cb)
|
||||
}
|
||||
|
||||
LocationHistory.prototype.back = function (cb) {
|
||||
var self = this
|
||||
if (!cb) cb = noop
|
||||
if (self._history.length <= 1 || self._pending) return cb(null)
|
||||
|
||||
var page = self._history.pop()
|
||||
self._unload(page, done)
|
||||
|
||||
function done (err) {
|
||||
if (err) return cb(err)
|
||||
self._forward.push(page)
|
||||
self._load(self.current(), cb)
|
||||
}
|
||||
}
|
||||
|
||||
LocationHistory.prototype.hasBack = function () {
|
||||
return this._history.length > 1
|
||||
}
|
||||
|
||||
LocationHistory.prototype.forward = function (cb) {
|
||||
if (!cb) cb = noop
|
||||
if (this._forward.length === 0 || this._pending) return cb(null)
|
||||
|
||||
var page = this._forward.pop()
|
||||
this._go(page, cb)
|
||||
}
|
||||
|
||||
LocationHistory.prototype.hasForward = function () {
|
||||
return this._forward.length > 0
|
||||
}
|
||||
|
||||
LocationHistory.prototype.clearForward = function (url) {
|
||||
if (url == null) {
|
||||
this._forward = []
|
||||
} else {
|
||||
console.log(this._forward)
|
||||
console.log(url)
|
||||
this._forward = this._forward.filter(function (page) {
|
||||
return page.url !== url
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
LocationHistory.prototype.backToFirst = function (cb) {
|
||||
var self = this
|
||||
if (!cb) cb = noop
|
||||
if (self._history.length <= 1) return cb(null)
|
||||
|
||||
self.back(function (err) {
|
||||
if (err) return cb(err)
|
||||
self.backToFirst(cb)
|
||||
})
|
||||
}
|
||||
|
||||
LocationHistory.prototype._go = function (page, cb) {
|
||||
var self = this
|
||||
if (!cb) cb = noop
|
||||
|
||||
self._unload(self.current(), done1)
|
||||
|
||||
function done1 (err) {
|
||||
if (err) return cb(err)
|
||||
self._load(page, done2)
|
||||
}
|
||||
|
||||
function done2 (err) {
|
||||
if (err) return cb(err)
|
||||
self._history.push(page)
|
||||
cb(null)
|
||||
}
|
||||
}
|
||||
|
||||
LocationHistory.prototype._load = function (page, cb) {
|
||||
var self = this
|
||||
self._pending = true
|
||||
|
||||
if (page && page.onbeforeload) page.onbeforeload(done)
|
||||
else done(null)
|
||||
|
||||
function done (err) {
|
||||
self._pending = false
|
||||
cb(err)
|
||||
}
|
||||
}
|
||||
|
||||
LocationHistory.prototype._unload = function (page, cb) {
|
||||
var self = this
|
||||
self._pending = true
|
||||
|
||||
if (page && page.onbeforeunload) page.onbeforeunload(done)
|
||||
else done(null)
|
||||
|
||||
function done (err) {
|
||||
self._pending = false
|
||||
cb(err)
|
||||
}
|
||||
}
|
||||
|
||||
function noop () {}
|
||||
95
src/renderer/lib/migrations.js
Normal file
95
src/renderer/lib/migrations.js
Normal file
@@ -0,0 +1,95 @@
|
||||
/* eslint-disable camelcase */
|
||||
|
||||
module.exports = {
|
||||
run
|
||||
}
|
||||
|
||||
var semver = require('semver')
|
||||
var config = require('../../config')
|
||||
|
||||
// Change `state.saved` (which will be saved back to config.json on exit) as
|
||||
// needed, for example to deal with config.json format changes across versions
|
||||
function run (state) {
|
||||
// Replace '{ version: 1 }' with app version (semver)
|
||||
if (!semver.valid(state.saved.version)) {
|
||||
state.saved.version = '0.0.0' // Pre-0.7.0 version, so run all migrations
|
||||
}
|
||||
|
||||
var version = state.saved.version
|
||||
|
||||
if (semver.lt(version, '0.7.0')) {
|
||||
migrate_0_7_0(state.saved)
|
||||
}
|
||||
|
||||
if (semver.lt(version, '0.7.2')) {
|
||||
migrate_0_7_2(state.saved)
|
||||
}
|
||||
|
||||
// Config is now on the new version
|
||||
state.saved.version = config.APP_VERSION
|
||||
}
|
||||
|
||||
function migrate_0_7_0 (saved) {
|
||||
console.log('migrate to 0.7.0')
|
||||
|
||||
var fs = require('fs-extra')
|
||||
var path = require('path')
|
||||
|
||||
saved.torrents.forEach(function (ts) {
|
||||
var infoHash = ts.infoHash
|
||||
|
||||
// Replace torrentPath with torrentFileName
|
||||
// There are a number of cases to handle here:
|
||||
// * Originally we used absolute paths
|
||||
// * Then, relative paths for the default torrents, eg '../static/sintel.torrent'
|
||||
// * Then, paths computed at runtime for default torrents, eg 'sintel.torrent'
|
||||
// * Finally, now we're getting rid of torrentPath altogether
|
||||
var src, dst
|
||||
if (ts.torrentPath) {
|
||||
console.log('replacing torrentPath %s', ts.torrentPath)
|
||||
if (path.isAbsolute(ts.torrentPath) || ts.torrentPath.startsWith('..')) {
|
||||
src = ts.torrentPath
|
||||
} else {
|
||||
src = path.join(config.STATIC_PATH, ts.torrentPath)
|
||||
}
|
||||
dst = path.join(config.TORRENT_PATH, infoHash + '.torrent')
|
||||
// Synchronous FS calls aren't ideal, but probably OK in a migration
|
||||
// that only runs once
|
||||
if (src !== dst) fs.copySync(src, dst)
|
||||
|
||||
delete ts.torrentPath
|
||||
ts.torrentFileName = infoHash + '.torrent'
|
||||
}
|
||||
|
||||
// Replace posterURL with posterFileName
|
||||
if (ts.posterURL) {
|
||||
console.log('replacing posterURL %s', ts.posterURL)
|
||||
var extension = path.extname(ts.posterURL)
|
||||
src = path.isAbsolute(ts.posterURL)
|
||||
? ts.posterURL
|
||||
: path.join(config.STATIC_PATH, ts.posterURL)
|
||||
dst = path.join(config.POSTER_PATH, infoHash + extension)
|
||||
// Synchronous FS calls aren't ideal, but probably OK in a migration
|
||||
// that only runs once
|
||||
if (src !== dst) fs.copySync(src, dst)
|
||||
|
||||
delete ts.posterURL
|
||||
ts.posterFileName = infoHash + extension
|
||||
}
|
||||
|
||||
// Fix exception caused by incorrect file ordering.
|
||||
// https://github.com/feross/webtorrent-desktop/pull/604#issuecomment-222805214
|
||||
delete ts.defaultPlayFileIndex
|
||||
delete ts.files
|
||||
delete ts.selections
|
||||
delete ts.fileModtimes
|
||||
})
|
||||
}
|
||||
|
||||
function migrate_0_7_2 (saved) {
|
||||
if (!saved.prefs) {
|
||||
saved.prefs = {
|
||||
downloadPath: config.DEFAULT_DOWNLOAD_PATH
|
||||
}
|
||||
}
|
||||
}
|
||||
73
src/renderer/lib/sound.js
Normal file
73
src/renderer/lib/sound.js
Normal file
@@ -0,0 +1,73 @@
|
||||
module.exports = {
|
||||
preload,
|
||||
play
|
||||
}
|
||||
|
||||
var config = require('../../config')
|
||||
var path = require('path')
|
||||
|
||||
var VOLUME = 0.15
|
||||
|
||||
/* Cache of Audio elements, for instant playback */
|
||||
var cache = {}
|
||||
|
||||
var sounds = {
|
||||
ADD: {
|
||||
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'add.wav'),
|
||||
volume: VOLUME
|
||||
},
|
||||
DELETE: {
|
||||
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'delete.wav'),
|
||||
volume: VOLUME
|
||||
},
|
||||
DISABLE: {
|
||||
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'disable.wav'),
|
||||
volume: VOLUME
|
||||
},
|
||||
DONE: {
|
||||
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'done.wav'),
|
||||
volume: VOLUME
|
||||
},
|
||||
ENABLE: {
|
||||
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'enable.wav'),
|
||||
volume: VOLUME
|
||||
},
|
||||
ERROR: {
|
||||
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'error.wav'),
|
||||
volume: VOLUME
|
||||
},
|
||||
PLAY: {
|
||||
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'play.wav'),
|
||||
volume: VOLUME
|
||||
},
|
||||
STARTUP: {
|
||||
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'startup.wav'),
|
||||
volume: VOLUME * 2
|
||||
}
|
||||
}
|
||||
|
||||
function preload () {
|
||||
for (var name in sounds) {
|
||||
if (!cache[name]) {
|
||||
var sound = sounds[name]
|
||||
var audio = cache[name] = new window.Audio()
|
||||
audio.volume = sound.volume
|
||||
audio.src = sound.url
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function play (name) {
|
||||
var audio = cache[name]
|
||||
if (!audio) {
|
||||
var sound = sounds[name]
|
||||
if (!sound) {
|
||||
throw new Error('Invalid sound name')
|
||||
}
|
||||
audio = cache[name] = new window.Audio()
|
||||
audio.volume = sound.volume
|
||||
audio.src = sound.url
|
||||
}
|
||||
audio.currentTime = 0
|
||||
audio.play()
|
||||
}
|
||||
220
src/renderer/lib/state.js
Normal file
220
src/renderer/lib/state.js
Normal file
@@ -0,0 +1,220 @@
|
||||
var appConfig = require('application-config')('WebTorrent')
|
||||
var path = require('path')
|
||||
var {EventEmitter} = require('events')
|
||||
|
||||
var config = require('../../config')
|
||||
var migrations = require('./migrations')
|
||||
|
||||
var State = module.exports = Object.assign(new EventEmitter(), {
|
||||
getDefaultPlayState,
|
||||
load,
|
||||
save,
|
||||
saveThrottled
|
||||
})
|
||||
|
||||
appConfig.filePath = path.join(config.CONFIG_PATH, 'config.json')
|
||||
|
||||
function getDefaultState () {
|
||||
var LocationHistory = require('./location-history')
|
||||
|
||||
return {
|
||||
/*
|
||||
* Temporary state disappears once the program exits.
|
||||
* It can contain complex objects like open connections, etc.
|
||||
*/
|
||||
client: null, /* the WebTorrent client */
|
||||
server: null, /* local WebTorrent-to-HTTP server */
|
||||
prev: {}, /* used for state diffing in updateElectron() */
|
||||
location: new LocationHistory(),
|
||||
window: {
|
||||
bounds: null, /* {x, y, width, height } */
|
||||
isFocused: true,
|
||||
isFullScreen: false,
|
||||
title: config.APP_WINDOW_TITLE
|
||||
},
|
||||
selectedInfoHash: null, /* the torrent we've selected to view details. see state.torrents */
|
||||
playing: getDefaultPlayState(), /* the media (audio or video) that we're currently playing */
|
||||
devices: {}, /* playback devices like Chromecast and AppleTV */
|
||||
dock: {
|
||||
badge: 0,
|
||||
progress: 0
|
||||
},
|
||||
modal: null, /* modal popover */
|
||||
errors: [], /* user-facing errors */
|
||||
nextTorrentKey: 1, /* identify torrents for IPC between the main and webtorrent windows */
|
||||
|
||||
/*
|
||||
* Saved state is read from and written to a file every time the app runs.
|
||||
* It should be simple and minimal and must be JSON.
|
||||
* It must never contain absolute paths since we have a portable app.
|
||||
*
|
||||
* Config path:
|
||||
*
|
||||
* OS X ~/Library/Application Support/WebTorrent/config.json
|
||||
* Linux (XDG) $XDG_CONFIG_HOME/WebTorrent/config.json
|
||||
* Linux (Legacy) ~/.config/WebTorrent/config.json
|
||||
* Windows (> Vista) %LOCALAPPDATA%/WebTorrent/config.json
|
||||
* Windows (XP, 2000) %USERPROFILE%/Local Settings/Application Data/WebTorrent/config.json
|
||||
*
|
||||
* Also accessible via `require('application-config')('WebTorrent').filePath`
|
||||
*/
|
||||
saved: {},
|
||||
|
||||
/*
|
||||
* Getters, for convenience
|
||||
*/
|
||||
getPlayingTorrentSummary,
|
||||
getPlayingFileSummary
|
||||
}
|
||||
}
|
||||
|
||||
/* Whenever we stop playing video or audio, here's what we reset state.playing to */
|
||||
function getDefaultPlayState () {
|
||||
return {
|
||||
infoHash: null, /* the info hash of the torrent we're playing */
|
||||
fileIndex: null, /* the zero-based index within the torrent */
|
||||
location: 'local', /* 'local', 'chromecast', 'airplay' */
|
||||
type: null, /* 'audio' or 'video', could be 'other' if ever support eg streaming to VLC */
|
||||
currentTime: 0, /* seconds */
|
||||
duration: 1, /* seconds */
|
||||
isPaused: true,
|
||||
isStalled: false,
|
||||
lastTimeUpdate: 0, /* Unix time in ms */
|
||||
mouseStationarySince: 0, /* Unix time in ms */
|
||||
playbackRate: 1,
|
||||
subtitles: {
|
||||
tracks: [], /* subtitle tracks, each {label, language, ...} */
|
||||
selectedIndex: -1, /* current subtitle track */
|
||||
showMenu: false /* popover menu, above the video */
|
||||
},
|
||||
aspectRatio: 0 /* aspect ratio of the video */
|
||||
}
|
||||
}
|
||||
|
||||
/* If the saved state file doesn't exist yet, here's what we use instead */
|
||||
function setupSavedState (cb) {
|
||||
var fs = require('fs-extra')
|
||||
var parseTorrent = require('parse-torrent')
|
||||
var parallel = require('run-parallel')
|
||||
|
||||
var saved = {
|
||||
prefs: {
|
||||
downloadPath: config.DEFAULT_DOWNLOAD_PATH
|
||||
},
|
||||
torrents: config.DEFAULT_TORRENTS.map(createTorrentObject),
|
||||
version: config.APP_VERSION /* make sure we can upgrade gracefully later */
|
||||
}
|
||||
|
||||
var tasks = []
|
||||
|
||||
config.DEFAULT_TORRENTS.map(function (t, i) {
|
||||
var infoHash = saved.torrents[i].infoHash
|
||||
tasks.push(function (cb) {
|
||||
fs.copy(
|
||||
path.join(config.STATIC_PATH, t.posterFileName),
|
||||
path.join(config.POSTER_PATH, infoHash + path.extname(t.posterFileName)),
|
||||
cb
|
||||
)
|
||||
})
|
||||
tasks.push(function (cb) {
|
||||
fs.copy(
|
||||
path.join(config.STATIC_PATH, t.torrentFileName),
|
||||
path.join(config.TORRENT_PATH, infoHash + '.torrent'),
|
||||
cb
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
parallel(tasks, function (err) {
|
||||
if (err) return cb(err)
|
||||
cb(null, saved)
|
||||
})
|
||||
|
||||
function createTorrentObject (t) {
|
||||
var torrent = fs.readFileSync(path.join(config.STATIC_PATH, t.torrentFileName))
|
||||
var parsedTorrent = parseTorrent(torrent)
|
||||
|
||||
return {
|
||||
status: 'paused',
|
||||
infoHash: parsedTorrent.infoHash,
|
||||
name: t.name,
|
||||
displayName: t.name,
|
||||
posterFileName: parsedTorrent.infoHash + path.extname(t.posterFileName),
|
||||
torrentFileName: parsedTorrent.infoHash + '.torrent',
|
||||
magnetURI: parseTorrent.toMagnetURI(parsedTorrent),
|
||||
files: parsedTorrent.files,
|
||||
selections: parsedTorrent.files.map((x) => true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getPlayingTorrentSummary () {
|
||||
var infoHash = this.playing.infoHash
|
||||
return this.saved.torrents.find((x) => x.infoHash === infoHash)
|
||||
}
|
||||
|
||||
function getPlayingFileSummary () {
|
||||
var torrentSummary = this.getPlayingTorrentSummary()
|
||||
if (!torrentSummary) return null
|
||||
return torrentSummary.files[this.playing.fileIndex]
|
||||
}
|
||||
|
||||
function load (cb) {
|
||||
var state = getDefaultState()
|
||||
|
||||
appConfig.read(function (err, saved) {
|
||||
if (err || !saved.version) {
|
||||
console.log('Missing config file: Creating new one')
|
||||
setupSavedState(onSaved)
|
||||
} else {
|
||||
onSaved(null, saved)
|
||||
}
|
||||
})
|
||||
|
||||
function onSaved (err, saved) {
|
||||
if (err) return cb(err)
|
||||
state.saved = saved
|
||||
migrations.run(state)
|
||||
cb(null, state)
|
||||
}
|
||||
}
|
||||
|
||||
// Write state.saved to the JSON state file
|
||||
function save (state, cb) {
|
||||
console.log('Saving state to ' + appConfig.filePath)
|
||||
delete state.saveStateTimeout
|
||||
|
||||
// Clean up, so that we're not saving any pending state
|
||||
var copy = Object.assign({}, state.saved)
|
||||
// Remove torrents pending addition to the list, where we haven't finished
|
||||
// reading the torrent file or file(s) to seed & don't have an infohash
|
||||
copy.torrents = copy.torrents
|
||||
.filter((x) => x.infoHash)
|
||||
.map(function (x) {
|
||||
var torrent = {}
|
||||
for (var key in x) {
|
||||
if (key === 'progress' || key === 'torrentKey') {
|
||||
continue // Don't save progress info or key for the webtorrent process
|
||||
}
|
||||
if (key === 'playStatus') {
|
||||
continue // Don't save whether a torrent is playing / pending
|
||||
}
|
||||
torrent[key] = x[key]
|
||||
}
|
||||
return torrent
|
||||
})
|
||||
|
||||
appConfig.write(copy, (err) => {
|
||||
if (err) console.error(err)
|
||||
else State.emit('savedState')
|
||||
})
|
||||
}
|
||||
|
||||
// Write, but no more than once a second
|
||||
function saveThrottled (state) {
|
||||
if (state.saveStateTimeout) return
|
||||
state.saveStateTimeout = setTimeout(function () {
|
||||
if (!state.saveStateTimeout) return
|
||||
save(state)
|
||||
}, 1000)
|
||||
}
|
||||
152
src/renderer/lib/telemetry.js
Normal file
152
src/renderer/lib/telemetry.js
Normal file
@@ -0,0 +1,152 @@
|
||||
// Collects anonymous usage stats and uncaught errors
|
||||
// Reports back so that we can improve WebTorrent Desktop
|
||||
module.exports = {
|
||||
init,
|
||||
logUncaughtError,
|
||||
logPlayAttempt
|
||||
}
|
||||
|
||||
const crypto = require('crypto')
|
||||
const electron = require('electron')
|
||||
const https = require('https')
|
||||
const os = require('os')
|
||||
const url = require('url')
|
||||
|
||||
const config = require('../../config')
|
||||
|
||||
var telemetry
|
||||
|
||||
function init (state) {
|
||||
telemetry = state.saved.telemetry
|
||||
if (!telemetry) {
|
||||
telemetry = state.saved.telemetry = createSummary()
|
||||
reset()
|
||||
}
|
||||
|
||||
var now = new Date()
|
||||
telemetry.timestamp = now.toISOString()
|
||||
telemetry.localTime = now.toTimeString()
|
||||
telemetry.screens = getScreenInfo()
|
||||
telemetry.system = getSystemInfo()
|
||||
telemetry.approxNumTorrents = getApproxNumTorrents(state)
|
||||
|
||||
if (config.IS_PRODUCTION) {
|
||||
postToServer()
|
||||
} else {
|
||||
// Development: telemetry used only for local debugging
|
||||
// Empty uncaught errors, etc at the start of every run
|
||||
reset()
|
||||
}
|
||||
}
|
||||
|
||||
function reset () {
|
||||
telemetry.uncaughtErrors = []
|
||||
telemetry.playAttempts = {
|
||||
total: 0,
|
||||
success: 0,
|
||||
timeout: 0,
|
||||
error: 0,
|
||||
abandoned: 0
|
||||
}
|
||||
}
|
||||
|
||||
function postToServer () {
|
||||
// Serialize the telemetry summary
|
||||
var payload = new Buffer(JSON.stringify(telemetry), 'utf8')
|
||||
|
||||
// POST to our server
|
||||
var options = url.parse(config.TELEMETRY_URL)
|
||||
options.method = 'POST'
|
||||
options.headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'Content-Length': payload.length
|
||||
}
|
||||
|
||||
var req = https.request(options, function (res) {
|
||||
if (res.statusCode === 200) {
|
||||
console.log('Successfully posted telemetry summary')
|
||||
reset()
|
||||
} else {
|
||||
console.error('Couldn\'t post telemetry summary, got HTTP ' + res.statusCode)
|
||||
}
|
||||
})
|
||||
req.on('error', function (e) {
|
||||
console.error('Couldn\'t post telemetry summary', e)
|
||||
})
|
||||
req.write(payload)
|
||||
req.end()
|
||||
}
|
||||
|
||||
// Creates a new telemetry summary. Gives the user a unique ID,
|
||||
// collects screen resolution, etc
|
||||
function createSummary () {
|
||||
// Make a 256-bit random unique ID
|
||||
var userID = crypto.randomBytes(32).toString('hex')
|
||||
return { userID }
|
||||
}
|
||||
|
||||
// Track screen resolution
|
||||
function getScreenInfo () {
|
||||
return electron.screen.getAllDisplays().map((screen) => ({
|
||||
width: screen.size.width,
|
||||
height: screen.size.height,
|
||||
scaleFactor: screen.scaleFactor
|
||||
}))
|
||||
}
|
||||
|
||||
// Track basic system info like OS version and amount of RAM
|
||||
function getSystemInfo () {
|
||||
return {
|
||||
osPlatform: process.platform,
|
||||
osRelease: os.type() + ' ' + os.release(),
|
||||
architecture: os.arch(),
|
||||
totalMemoryMB: os.totalmem() / (1 << 20),
|
||||
numCores: os.cpus().length
|
||||
}
|
||||
}
|
||||
|
||||
// Get the number of torrents, rounded to the nearest power of two
|
||||
function getApproxNumTorrents (state) {
|
||||
var exactNum = state.saved.torrents.length
|
||||
if (exactNum === 0) return 0
|
||||
// Otherwise, return 1, 2, 4, 8, etc by rounding in log space
|
||||
var log2 = Math.log(exactNum) / Math.log(2)
|
||||
return 1 << Math.round(log2)
|
||||
}
|
||||
|
||||
// An uncaught error happened in the main process or in one of the windows
|
||||
function logUncaughtError (procName, err) {
|
||||
console.error('uncaught error', procName, err)
|
||||
|
||||
// Not initialized yet? Ignore.
|
||||
// Hopefully uncaught errors immediately on startup are fixed in dev
|
||||
if (!telemetry) return
|
||||
|
||||
var message, stack
|
||||
if (err instanceof Error) {
|
||||
message = err.message
|
||||
stack = err.stack
|
||||
} else {
|
||||
message = String(err)
|
||||
stack = ''
|
||||
}
|
||||
|
||||
// We need to POST the telemetry object, make sure it stays < 100kb
|
||||
if (telemetry.uncaughtErrors.length > 20) return
|
||||
if (message.length > 1000) message = message.substring(0, 1000)
|
||||
if (stack.length > 1000) stack = stack.substring(0, 1000)
|
||||
|
||||
telemetry.uncaughtErrors.push({process: procName, message, stack})
|
||||
}
|
||||
|
||||
// The user pressed play. It either worked, timed out, or showed the
|
||||
// 'Play in VLC' codec error
|
||||
function logPlayAttempt (result) {
|
||||
if (!['success', 'timeout', 'error', 'abandoned'].includes(result)) {
|
||||
return console.error('Unknown play attempt result', result)
|
||||
}
|
||||
|
||||
var attempts = telemetry.playAttempts
|
||||
attempts.total = (attempts.total || 0) + 1
|
||||
attempts[result] = (attempts[result] || 0) + 1
|
||||
}
|
||||
83
src/renderer/lib/torrent-player.js
Normal file
83
src/renderer/lib/torrent-player.js
Normal file
@@ -0,0 +1,83 @@
|
||||
module.exports = {
|
||||
isPlayable,
|
||||
isVideo,
|
||||
isAudio,
|
||||
isTorrent,
|
||||
isPlayableTorrentSummary,
|
||||
pickFileToPlay
|
||||
}
|
||||
|
||||
var path = require('path')
|
||||
|
||||
// Checks whether a fileSummary or file path is audio/video that we can play,
|
||||
// based on the file extension
|
||||
function isPlayable (file) {
|
||||
return isVideo(file) || isAudio(file)
|
||||
}
|
||||
|
||||
// Checks whether a fileSummary or file path is playable video
|
||||
function isVideo (file) {
|
||||
return [
|
||||
'.avi',
|
||||
'.m4v',
|
||||
'.mkv',
|
||||
'.mov',
|
||||
'.mp4',
|
||||
'.mpg',
|
||||
'.ogv',
|
||||
'.webm',
|
||||
'.wmv'
|
||||
].includes(getFileExtension(file))
|
||||
}
|
||||
|
||||
// Checks whether a fileSummary or file path is playable audio
|
||||
function isAudio (file) {
|
||||
return [
|
||||
'.aac',
|
||||
'.ac3',
|
||||
'.mp3',
|
||||
'.ogg',
|
||||
'.wav'
|
||||
].includes(getFileExtension(file))
|
||||
}
|
||||
|
||||
// Checks if the argument is either:
|
||||
// - a string that's a valid filename ending in .torrent
|
||||
// - a file object where obj.name is ends in .torrent
|
||||
// - a string that's a magnet link (magnet://...)
|
||||
function isTorrent (file) {
|
||||
var isTorrentFile = getFileExtension(file) === '.torrent'
|
||||
var isMagnet = typeof file === 'string' && /^(stream-)?magnet:/.test(file)
|
||||
return isTorrentFile || isMagnet
|
||||
}
|
||||
|
||||
function getFileExtension (file) {
|
||||
var name = typeof file === 'string' ? file : file.name
|
||||
return path.extname(name).toLowerCase()
|
||||
}
|
||||
|
||||
function isPlayableTorrentSummary (torrentSummary) {
|
||||
return torrentSummary.files && torrentSummary.files.some(isPlayable)
|
||||
}
|
||||
|
||||
// Picks the default file to play from a list of torrent or torrentSummary files
|
||||
// Returns an index or undefined, if no files are playable
|
||||
function pickFileToPlay (files) {
|
||||
// first, try to find the biggest video file
|
||||
var videoFiles = files.filter(isVideo)
|
||||
if (videoFiles.length > 0) {
|
||||
var largestVideoFile = videoFiles.reduce(function (a, b) {
|
||||
return a.length > b.length ? a : b
|
||||
})
|
||||
return files.indexOf(largestVideoFile)
|
||||
}
|
||||
|
||||
// if there are no videos, play the first audio file
|
||||
var audioFiles = files.filter(isAudio)
|
||||
if (audioFiles.length > 0) {
|
||||
return files.indexOf(audioFiles[0])
|
||||
}
|
||||
|
||||
// no video or audio means nothing is playable
|
||||
return undefined
|
||||
}
|
||||
83
src/renderer/lib/torrent-poster.js
Normal file
83
src/renderer/lib/torrent-poster.js
Normal file
@@ -0,0 +1,83 @@
|
||||
module.exports = torrentPoster
|
||||
|
||||
var captureVideoFrame = require('./capture-video-frame')
|
||||
var path = require('path')
|
||||
|
||||
function torrentPoster (torrent, cb) {
|
||||
// First, try to use a poster image if available
|
||||
var posterFile = torrent.files.filter(function (file) {
|
||||
return /^poster\.(jpg|png|gif)$/.test(file.name)
|
||||
})[0]
|
||||
if (posterFile) return torrentPosterFromImage(posterFile, torrent, cb)
|
||||
|
||||
// Second, try to use the largest video file
|
||||
// Filter out file formats that the <video> tag definitely can't play
|
||||
var videoFile = getLargestFileByExtension(torrent, ['.mp4', '.m4v', '.webm', '.mov', '.mkv'])
|
||||
if (videoFile) return torrentPosterFromVideo(videoFile, torrent, cb)
|
||||
|
||||
// Third, try to use the largest image file
|
||||
var imgFile = getLargestFileByExtension(torrent, ['.gif', '.jpg', '.jpeg', '.png'])
|
||||
if (imgFile) return torrentPosterFromImage(imgFile, torrent, cb)
|
||||
|
||||
// TODO: generate a waveform from the largest sound file
|
||||
// Finally, admit defeat
|
||||
return cb(new Error('Cannot generate a poster from any files in the torrent'))
|
||||
}
|
||||
|
||||
function getLargestFileByExtension (torrent, extensions) {
|
||||
var files = torrent.files.filter(function (file) {
|
||||
var extname = path.extname(file.name).toLowerCase()
|
||||
return extensions.indexOf(extname) !== -1
|
||||
})
|
||||
if (files.length === 0) return undefined
|
||||
return files.reduce(function (a, b) {
|
||||
return a.length > b.length ? a : b
|
||||
})
|
||||
}
|
||||
|
||||
function torrentPosterFromVideo (file, torrent, cb) {
|
||||
var index = torrent.files.indexOf(file)
|
||||
|
||||
var server = torrent.createServer(0)
|
||||
server.listen(0, onListening)
|
||||
|
||||
function onListening () {
|
||||
var port = server.address().port
|
||||
var url = 'http://localhost:' + port + '/' + index
|
||||
var video = document.createElement('video')
|
||||
video.addEventListener('canplay', onCanPlay)
|
||||
|
||||
video.volume = 0
|
||||
video.src = url
|
||||
video.play()
|
||||
|
||||
function onCanPlay () {
|
||||
video.removeEventListener('canplay', onCanPlay)
|
||||
video.addEventListener('seeked', onSeeked)
|
||||
|
||||
video.currentTime = Math.min((video.duration || 600) * 0.03, 60)
|
||||
}
|
||||
|
||||
function onSeeked () {
|
||||
video.removeEventListener('seeked', onSeeked)
|
||||
|
||||
var buf = captureVideoFrame(video)
|
||||
|
||||
// unload video element
|
||||
video.pause()
|
||||
video.src = ''
|
||||
video.load()
|
||||
|
||||
server.destroy()
|
||||
|
||||
if (buf.length === 0) return cb(new Error('Generated poster contains no data'))
|
||||
|
||||
cb(null, buf, '.jpg')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function torrentPosterFromImage (file, torrent, cb) {
|
||||
var extname = path.extname(file.name)
|
||||
file.getBuffer((err, buf) => cb(err, buf, extname))
|
||||
}
|
||||
56
src/renderer/lib/torrent-summary.js
Normal file
56
src/renderer/lib/torrent-summary.js
Normal file
@@ -0,0 +1,56 @@
|
||||
module.exports = {
|
||||
getPosterPath,
|
||||
getTorrentPath,
|
||||
getByKey,
|
||||
getTorrentID,
|
||||
getFileOrFolder
|
||||
}
|
||||
|
||||
var path = require('path')
|
||||
var config = require('../../config')
|
||||
|
||||
// Expects a torrentSummary
|
||||
// Returns an absolute path to the torrent file, or null if unavailable
|
||||
function getTorrentPath (torrentSummary) {
|
||||
if (!torrentSummary || !torrentSummary.torrentFileName) return null
|
||||
return path.join(config.TORRENT_PATH, torrentSummary.torrentFileName)
|
||||
}
|
||||
|
||||
// Expects a torrentSummary
|
||||
// Returns an absolute path to the poster image, or null if unavailable
|
||||
function getPosterPath (torrentSummary) {
|
||||
if (!torrentSummary || !torrentSummary.posterFileName) return null
|
||||
var posterPath = path.join(config.POSTER_PATH, torrentSummary.posterFileName)
|
||||
// Work around a Chrome bug (reproduced in vanilla Chrome, not just Electron):
|
||||
// Backslashes in URLS in CSS cause bizarre string encoding issues
|
||||
return posterPath.replace(/\\/g, '/')
|
||||
}
|
||||
|
||||
// Expects a torrentSummary
|
||||
// Returns a torrentID: filename, magnet URI, or infohash
|
||||
function getTorrentID (torrentSummary) {
|
||||
var s = torrentSummary
|
||||
if (s.torrentFileName) { // Load torrent file from disk
|
||||
return getTorrentPath(s)
|
||||
} else { // Load torrent from DHT
|
||||
return s.magnetURI || s.infoHash
|
||||
}
|
||||
}
|
||||
|
||||
// Expects a torrentKey or infoHash
|
||||
// Returns the corresponding torrentSummary, or undefined
|
||||
function getByKey (state, torrentKey) {
|
||||
if (!torrentKey) return undefined
|
||||
return state.saved.torrents.find((x) =>
|
||||
x.torrentKey === torrentKey || x.infoHash === torrentKey)
|
||||
}
|
||||
|
||||
// Returns the path to either the file (in a single-file torrent) or the root
|
||||
// folder (in multi-file torrent)
|
||||
// WARNING: assumes that multi-file torrents consist of a SINGLE folder.
|
||||
// TODO: make this assumption explicit, enforce it in the `create-torrent`
|
||||
// module. Store root folder explicitly to avoid hacky path processing below.
|
||||
function getFileOrFolder (torrentSummary) {
|
||||
var ts = torrentSummary
|
||||
return path.join(ts.path, ts.files[0].path.split('/')[0])
|
||||
}
|
||||
Reference in New Issue
Block a user