Compare commits

..

10 Commits

Author SHA1 Message Date
DC
74349129f4 0.13.0 2016-08-31 16:09:41 -07:00
DC
cdc2c1d718 Fix Create Torrent file list 2016-08-31 15:53:00 -07:00
DC
f8cc155650 Material UI: finish Create Torrent 2016-08-31 03:35:27 -07:00
DC
b6bdeab50b depcheck: ignore generated code 2016-08-31 00:58:00 -07:00
DC
f528f6033f Preferences: fix Make Default 2016-08-31 00:54:31 -07:00
DC
ef1bc13c38 use depcheck (#841)
* use depcheck to replace our own check-dependencies script

* ignore packges used in npm scripts
2016-08-30 20:42:45 -07:00
DC
2f54feac74 Telemetry: handle undefined values (#851) 2016-08-30 20:22:49 -07:00
Mathias Rasmussen
75d30baaa5 Telemetry: handle undefined values 2016-08-29 03:08:54 +02:00
Noam Okman
990fb57839 ignore packges used in npm scripts 2016-08-27 21:40:04 +03:00
Noam Okman
1883341ddb use depcheck 2016-08-27 21:40:02 +03:00
12 changed files with 148 additions and 331 deletions

View File

@@ -1,4 +1,4 @@
language: node_js
node_js:
- 'node'
install: npm install standard
install: npm install standard depcheck

View File

@@ -1,108 +0,0 @@
#!/usr/bin/env node
var fs = require('fs')
var cp = require('child_process')
// We can't use `builtin-modules` here since our TravisCI
// setup expects this file to run with no dependencies
var BUILT_IN_NODE_MODULES = [
'assert',
'buffer',
'child_process',
'cluster',
'console',
'constants',
'crypto',
'dgram',
'dns',
'domain',
'events',
'fs',
'http',
'https',
'module',
'net',
'os',
'path',
'process',
'punycode',
'querystring',
'readline',
'repl',
'stream',
'string_decoder',
'timers',
'tls',
'tty',
'url',
'util',
'v8',
'vm',
'zlib'
]
var BUILT_IN_ELECTRON_MODULES = [ 'electron' ]
var BUILT_IN_DEPS = [].concat(BUILT_IN_NODE_MODULES, BUILT_IN_ELECTRON_MODULES)
var EXECUTABLE_DEPS = [
'babel-cli',
'babel-plugin-syntax-jsx',
'babel-plugin-transform-es2015-destructuring',
'babel-plugin-transform-object-rest-spread',
'babel-plugin-transform-react-jsx',
'gh-release',
'nodemon',
'standard'
]
main()
// Scans codebase for missing or unused dependencies. Exits with code 0 on success.
function main () {
if (process.platform === 'win32') {
console.error('Sorry, check-deps only works on Mac and Linux')
return
}
var usedDeps = findUsedDeps()
var packageDeps = findPackageDeps()
var missingDeps = usedDeps.filter(
(dep) => !includes(packageDeps, dep) && !includes(BUILT_IN_DEPS, dep)
)
var unusedDeps = packageDeps.filter(
(dep) => !includes(usedDeps, dep) && !includes(EXECUTABLE_DEPS, dep)
)
if (missingDeps.length > 0) {
console.error('Missing package dependencies: ' + missingDeps)
}
if (unusedDeps.length > 0) {
console.error('Unused package dependencies: ' + unusedDeps)
}
if (missingDeps.length + unusedDeps.length > 0) {
process.exitCode = 1
}
}
// Finds all dependencies specified in `package.json`
function findPackageDeps () {
var pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'))
var deps = Object.keys(pkg.dependencies)
var devDeps = Object.keys(pkg.devDependencies)
var optionalDeps = Object.keys(pkg.optionalDependencies)
return [].concat(deps, devDeps, optionalDeps)
}
// Finds all dependencies that used with `require()`
function findUsedDeps () {
var stdout = cp.execSync('./bin/list-deps.sh')
return stdout.toString().trim().split('\n')
}
function includes (arr, elem) {
return arr.indexOf(elem) >= 0
}

View File

@@ -1,10 +0,0 @@
#!/bin/sh
# This is a truly heinous hack, but it works pretty nicely.
# Find all modules we're requiring---even conditional requires.
grep "require('" src/ bin/ -R |
grep '.js:' |
sed "s/.*require('\([^'\/]*\).*/\1/" |
grep -v '^\.' |
sort |
uniq

View File

@@ -7,4 +7,4 @@ npm run package -- --sign
git push
git push --tags
npm publish
./node_modules/.bin/gh-release
npm run gh-release

View File

@@ -1,7 +1,7 @@
{
"name": "webtorrent-desktop",
"description": "WebTorrent, the streaming torrent client. For Mac, Windows, and Linux.",
"version": "0.12.0",
"version": "0.13.0",
"author": {
"name": "WebTorrent, LLC",
"email": "feross@webtorrent.io",
@@ -28,7 +28,6 @@
"languagedetect": "^1.1.1",
"location-history": "^1.0.0",
"material-ui": "^0.15.4",
"matroska-subtitles": "^2.0.0",
"musicmetadata": "^2.0.2",
"network-address": "^1.1.0",
"parse-torrent": "^5.7.3",
@@ -53,6 +52,7 @@
"babel-plugin-transform-object-rest-spread": "^6.8.0",
"babel-plugin-transform-react-jsx": "^6.8.0",
"cross-zip": "^2.0.1",
"depcheck": "^0.6.4",
"electron-osx-sign": "^0.3.0",
"electron-packager": "^7.0.0",
"electron-winstaller": "^2.3.0",
@@ -98,7 +98,8 @@
"package": "node ./bin/package.js",
"prepublish": "npm run build",
"start": "npm run build && electron .",
"test": "standard && node ./bin/check-deps.js",
"test": "standard && depcheck --ignores=babel-cli,nodemon,gh-release --ignore-dirs=build,dist",
"gh-release": "gh-release",
"update-authors": "./bin/update-authors.sh",
"watch": "nodemon --exec 'npm run start' --ext js,pug,css"
}

View File

@@ -39,14 +39,11 @@ class ShowMore extends React.Component {
? this.props.hideLabel
: this.props.showLabel
return (
<div
style={this.props.style}
>
<div style={this.props.style}>
{this.state.expanded ? this.props.children : null}
<FlatButton
onClick={this.handleClick}
label={label}
/>
label={label} />
</div>
)
}

View File

@@ -2,10 +2,8 @@ const electron = require('electron')
const fs = require('fs-extra')
const path = require('path')
const parallel = require('run-parallel')
const zeroFill = require('zero-fill')
const remote = electron.remote
const ipcRenderer = electron.ipcRenderer
const {dispatch} = require('../lib/dispatcher')
@@ -76,7 +74,6 @@ module.exports = class SubtitlesController {
torrentSummary.progress.files.forEach((fp, ix) => {
if (fp.numPieces !== fp.numPiecesPresent) return // ignore incomplete files
var file = torrentSummary.files[ix]
if (this.state.playing.fileIndex === ix) return this.checkForEmbeddedMKVSubtitles(file)
if (!this.isSubtitle(file.name)) return
var filePath = path.join(torrentSummary.path, file.path)
this.addSubtitles([filePath], false)
@@ -88,31 +85,12 @@ module.exports = class SubtitlesController {
var ext = path.extname(name).toLowerCase()
return ext === '.srt' || ext === '.vtt'
}
checkForEmbeddedMKVSubtitles (file) {
var playing = this.state.playing
// var playingFile = this.state.getPlayingFileSummary()
// var playingPath = path.join(torrentSummary.path, playingFile.path)
if (path.extname(file.name).toLowerCase() === '.mkv') {
ipcRenderer.send('wt-get-mkv-subtitles', playing.infoHash, playing.fileIndex)
ipcRenderer.once('wt-mkv-subtitles', function (e, tracks) {
tracks.forEach(function (trackEntry) {
var track = loadEmbeddedSubtitle(trackEntry)
console.log('loaded emb subs', track)
playing.subtitles.tracks.push(track)
})
if (tracks.length > 0) relabelSubtitles(playing.subtitles)
})
}
}
}
function loadSubtitle (file, cb) {
// Lazy load to keep startup fast
var concat = require('simple-concat')
var LanguageDetect = require('languagedetect')
var srtToVtt = require('srt-to-vtt')
// Read the .SRT or .VTT file, parse it, add subtitle track
@@ -123,7 +101,11 @@ function loadSubtitle (file, cb) {
concat(vttStream, function (err, buf) {
if (err) return dispatch('error', 'Can\'t parse subtitles file.')
var langDetected = detectVTTLanguage(buf)
// Detect what language the subtitles are in
var vttContents = buf.toString().replace(/(.*-->.*)/g, '')
var langDetected = (new LanguageDetect()).detect(vttContents, 2)
langDetected = langDetected.length ? langDetected[0][0] : 'subtitle'
langDetected = langDetected.slice(0, 1).toUpperCase() + langDetected.slice(1)
var track = {
buffer: 'data:text/vtt;base64,' + buf.toString('base64'),
@@ -155,49 +137,3 @@ function relabelSubtitles (subtitles) {
track.label = counts[lang] > 1 ? (lang + ' ' + counts[lang]) : lang
})
}
function detectVTTLanguage (buffer) {
var LanguageDetect = require('languagedetect')
// Detect what language the subtitles are in
var vttContents = buffer.toString().replace(/(.*-->.*)/g, '') // remove numbers?
var langDetected = (new LanguageDetect()).detect(vttContents, 2)
langDetected = langDetected.length ? langDetected[0][0] : 'subtitle'
langDetected = langDetected.slice(0, 1).toUpperCase() + langDetected.slice(1)
return langDetected
}
function loadEmbeddedSubtitle (trackEntry) {
// convert to .vtt format
var vtt = 'WEBVTT FILE\r\n\r\n'
trackEntry.subtitles.forEach(function (sub, i) {
vtt += `${i + 1}\r\n`
vtt += `${msToTime(sub.time)} --> ${msToTime(sub.time + sub.duration)}\r\n`
vtt += `${sub.text}\r\n\r\n`
})
function msToTime (s) {
var ms = s % 1000
s = (s - ms) / 1000
var secs = s % 60
s = (s - secs) / 60
var mins = s % 60
var hrs = (s - mins) / 60
var z = zeroFill
return z(2, hrs) + ':' + z(2, mins) + ':' + z(2, secs) + '.' + z(3, ms)
}
var buf = new Buffer(vtt)
var langDetected = detectVTTLanguage(buf)
var track = {
buffer: 'data:text/vtt;base64,' + buf.toString('base64'),
language: langDetected,
label: langDetected,
filePath: null
}
return track
}

View File

@@ -126,7 +126,9 @@ function logUncaughtError (procName, e) {
var message
var stack = ''
if (e.message) {
if (e == null) {
message = 'Unexpected undefined error'
} else if (e.message) {
// err is either an Error or a plain object {message, stack}
message = e.message
stack = e.stack
@@ -149,11 +151,13 @@ function logUncaughtError (procName, e) {
}
}
if (typeof stack !== 'string') stack = 'Unexpected stack: ' + stack
if (typeof message !== 'string') message = 'Unexpected message: ' + message
// Remove the first part of each file path in the stack trace.
// - Privacy: remove personal info like C:\Users\<full name>
// - Aggregation: this lets us find which stacktraces occur often
if (stack && typeof stack === 'string') stack = stack.replace(/\(.*app.asar/g, '(...')
else if (stack) stack = 'Unexpected stack: ' + stack
stack = stack.replace(/\(.*app.asar/g, '(...')
// We need to POST the telemetry object, make sure it stays < 100kb
if (telemetry.uncaughtErrors.length > 20) return

View File

@@ -8,6 +8,7 @@ const {dispatch, dispatcher} = require('../lib/dispatcher')
const FlatButton = require('material-ui/FlatButton').default
const RaisedButton = require('material-ui/RaisedButton').default
const TextField = require('material-ui/TextField').default
const Checkbox = require('material-ui/Checkbox').default
const CreateTorrentErrorPage = require('../components/create-torrent-error-page')
const Heading = require('../components/Heading')
@@ -20,27 +21,20 @@ class CreateTorrentPage extends React.Component {
var state = this.props.state
var info = state.location.current()
// Preprocess: exclude .DS_Store and other dotfiles
var files = info.files
.filter((f) => !f.name.startsWith('.'))
.map((f) => ({name: f.name, path: f.path, size: f.size}))
if (files.length === 0) return (<CreateTorrentErrorPage state={state} />)
// First, extract the base folder that the files are all in
var pathPrefix = info.folderPath
if (!pathPrefix) {
pathPrefix = files.map((x) => x.path).reduce(findCommonPrefix)
pathPrefix = info.files.map((x) => x.path).reduce(findCommonPrefix)
if (!pathPrefix.endsWith('/') && !pathPrefix.endsWith('\\')) {
pathPrefix = path.dirname(pathPrefix)
}
}
// Sanity check: show the number of files and total size
var numFiles = files.length
var totalBytes = files
.map((f) => f.size)
.reduce((a, b) => a + b, 0)
var torrentInfo = `${numFiles} files, ${prettyBytes(totalBytes)}`
// Then, exclude .DS_Store and other dotfiles
var files = info.files
.filter((f) => !containsDots(f.path, pathPrefix))
.map((f) => ({name: f.name, path: f.path, size: f.size}))
if (files.length === 0) return (<CreateTorrentErrorPage state={state} />)
// Then, use the name of the base folder (or sole file, for a single file torrent)
// as the default name. Show all files relative to the base folder.
@@ -54,90 +48,70 @@ class CreateTorrentPage extends React.Component {
defaultName = path.basename(pathPrefix)
basePath = path.dirname(pathPrefix)
}
var maxFileElems = 100
var fileElems = files.slice(0, maxFileElems).map(function (file, i) {
var relativePath = files.length === 0 ? file.name : path.relative(pathPrefix, file.path)
return (<div key={i}>{relativePath}</div>)
})
if (files.length > maxFileElems) {
fileElems.push(<div key='more'>+ {maxFileElems - files.length} more</div>)
}
// Default trackers
var trackers = createTorrent.announceList.join('\n')
this.state = {
comment: '',
isPrivate: false,
pathPrefix,
basePath,
defaultName,
fileElems,
torrentInfo,
files,
trackers
}
// Create React event handlers only once
this.setIsPrivate = (_, isPrivate) => this.setState({isPrivate})
this.setComment = (_, comment) => this.setState({comment})
this.setTrackers = (_, trackers) => this.setState({trackers})
this.handleSubmit = () => this.handleSubmit
}
handleSubmit () {
var announceList = document.querySelector('.torrent-trackers').value
var announceList = this.state.trackers
.split('\n')
.map((s) => s.trim())
.filter((s) => s !== '')
var isPrivate = document.querySelector('.torrent-is-private').checked
var comment = document.querySelector('.torrent-comment').value.trim()
var options = {
// We can't let the user choose their own name if we want WebTorrent
// to use the files in place rather than creating a new folder.
// If we ever want to add support for that:
// name: document.querySelector('.torrent-name').value
name: this.state.defaultName,
path: this.state.basePath,
files: this.state.files,
announce: announceList,
private: isPrivate,
comment: comment
private: this.state.isPrivate,
comment: this.state.comment.trim()
}
dispatch('createTorrent', options)
}
render () {
var files = this.state.files
// Sanity check: show the number of files and total size
var numFiles = files.length
var totalBytes = files
.map((f) => f.size)
.reduce((a, b) => a + b, 0)
var torrentInfo = `${numFiles} files, ${prettyBytes(totalBytes)}`
return (
<div className='create-torrent'>
<Heading level={1}>
Create torrent "{this.state.defaultName}"
</Heading>
<div className='torrent-info'>
{this.state.torrentInfo}
</div>
<Heading level={1}>Create torrent {this.state.defaultName}</Heading>
<div className='torrent-info'>{torrentInfo}</div>
<div className='torrent-attribute'>
<label>Path:</label>
<div className='torrent-attribute'>{this.state.pathPrefix}</div>
<div>{this.state.pathPrefix}</div>
</div>
<ShowMore
style={{
marginBottom: 10
}}
hideLabel='Hide advanced settings...'
showLabel='Show advanced settings...'
>
<div key='advanced' className='create-torrent-advanced'>
<div key='private' className='torrent-attribute'>
<label>Private:</label>
<input type='checkbox' className='torrent-is-private' value='torrent-is-private' />
</div>
<Heading level={2}>Trackers:</Heading>
<TextField
className='torrent-trackers'
hintText='Tracker'
multiLine
rows={2}
rowsMax={10}
defaultValue={this.state.trackers}
/>
<div key='comment' className='torrent-attribute'>
<label>Comment:</label>
<textarea className='torrent-attribute torrent-comment' />
</div>
<div key='files' className='torrent-attribute'>
<label>Files:</label>
<div>{this.state.fileElems}</div>
</div>
</div>
showLabel='Show advanced settings...' >
{this.renderAdvanced()}
</ShowMore>
<div className='float-right'>
<FlatButton
@@ -156,6 +130,65 @@ class CreateTorrentPage extends React.Component {
</div>
)
}
renderAdvanced () {
// Create file list
var maxFileElems = 100
var files = this.state.files
var fileElems = files.slice(0, maxFileElems).map((file, i) => {
var relativePath = path.relative(this.state.pathPrefix, file.path)
return (<div key={i}>{relativePath}</div>)
})
if (files.length > maxFileElems) {
fileElems.push(<div key='more'>+ {files.length - maxFileElems} more</div>)
}
// Align the text fields
var textFieldStyle = { width: '' }
var textareaStyle = { margin: 0 }
return (
<div key='advanced' className='create-torrent-advanced'>
<div key='private' className='torrent-attribute'>
<label>Private:</label>
<Checkbox
className='torrent-is-private'
style={{display: ''}}
value={this.state.isPrivate}
onChange={this.setIsPrivate} />
</div>
<div key='trackers' className='torrent-attribute'>
<label>Trackers:</label>
<TextField
className='torrent-trackers'
style={textFieldStyle}
textareaStyle={textareaStyle}
multiLine
rows={2}
rowsMax={10}
value={this.state.trackers}
onChange={this.setTrackers} />
</div>
<div key='comment' className='torrent-attribute'>
<label>Comment:</label>
<TextField
className='torrent-comment'
style={textFieldStyle}
textareaStyle={textareaStyle}
hintText='Optionally describe your torrent...'
multiLine
rows={2}
rowsMax={10}
value={this.state.comment}
onChange={this.setComment} />
</div>
<div key='files' className='torrent-attribute'>
<label>Files:</label>
<div>{fileElems}</div>
</div>
</div>
)
}
}
// Finds the longest common prefix
@@ -168,4 +201,10 @@ function findCommonPrefix (a, b) {
return a.substring(0, i)
}
function containsDots (path, pathPrefix) {
var suffix = path.substring(pathPrefix.length).replace(/\\/g, '/')
console.log('SUFFIX ' + suffix)
return ('/' + suffix).includes('/.')
}
module.exports = CreateTorrentPage

View File

@@ -96,6 +96,14 @@ class PreferencesPage extends React.Component {
}
setDefaultAppButton () {
var isFileHandler = this.props.state.unsaved.prefs.isFileHandler
if (isFileHandler) {
return (
<Preference>
<p>WebTorrent is your default torrent app. Hooray!</p>
</Preference>
)
}
return (
<Preference>
<p>WebTorrent is not currently the default torrent app.</p>
@@ -109,20 +117,17 @@ class PreferencesPage extends React.Component {
}
handleSetDefaultApp () {
window.alert('TODO')
// var isFileHandler = state.unsaved.prefs.isFileHandler
// dispatch('updatePreferences', 'isFileHandler', !isFileHandler)
dispatch('updatePreferences', 'isFileHandler', true)
}
render () {
var style = {
color: colors.grey400,
marginLeft: 25,
marginRight: 25
}
return (
<div
style={{
color: colors.grey400,
marginLeft: 25,
marginRight: 25
}}
>
<div style={style} >
<PreferencesSection title='Downloads'>
{this.downloadPathSelector()}
</PreferencesSection>
@@ -146,13 +151,12 @@ class PreferencesSection extends React.Component {
}
render () {
var style = {
marginBottom: 25,
marginTop: 25
}
return (
<div
style={{
marginBottom: 25,
marginTop: 25
}}
>
<div style={style}>
<Heading level={2}>{this.props.title}</Heading>
{this.props.children}
</div>
@@ -162,15 +166,8 @@ class PreferencesSection extends React.Component {
class Preference extends React.Component {
render () {
return (
<div
style={{
marginBottom: 10
}}
>
{this.props.children}
</div>
)
var style = { marginBottom: 10 }
return (<div style={style}>{this.props.children}</div>)
}
}

View File

@@ -83,8 +83,6 @@ function init () {
generateTorrentPoster(torrentKey))
ipc.on('wt-get-audio-metadata', (e, infoHash, index) =>
getAudioMetadata(infoHash, index))
ipc.on('wt-get-mkv-subtitles', (e, infoHash, index) =>
getMKVSubtitles(infoHash, index))
ipc.on('wt-start-server', (e, infoHash, index) =>
startServer(infoHash, index))
ipc.on('wt-stop-server', (e) =>
@@ -344,32 +342,6 @@ function getAudioMetadata (infoHash, index) {
})
}
function getMKVSubtitles (infoHash, index) {
var torrent = client.get(infoHash)
var file = torrent.files[index]
var MatroskaSubtitles = require('matroska-subtitles')
var subtitleParser = new MatroskaSubtitles()
var textTracks = new Map()
subtitleParser.once('tracks', function (tracks) {
tracks.forEach(function (track) {
textTracks.set(track.number, { track: track, subtitles: [] })
})
})
subtitleParser.on('subtitle', function (subtitle, trackNumber) {
textTracks.get(trackNumber).subtitles.push(subtitle)
})
subtitleParser.on('finish', function () {
ipc.send('wt-mkv-subtitles', Array.from(textTracks.values()))
})
file.createReadStream().pipe(subtitleParser)
}
function selectFiles (torrentOrInfoHash, selections) {
// Get the torrent object
var torrent

View File

@@ -272,10 +272,12 @@ table {
.create-torrent {
padding: 10px 25px;
overflow: hidden;
font: 16px/24px BlinkMacSystemFont, "Helvetica Neue", Helvetica, sans-serif;
}
.create-torrent .torrent-attribute {
white-space: nowrap;
margin: 8px 0;
}
.create-torrent .torrent-attribute>* {
@@ -283,13 +285,12 @@ table {
}
.create-torrent .torrent-attribute label {
width: 60px;
margin-right: 10px;
width: 100px;
vertical-align: top;
}
.create-torrent .torrent-attribute>div {
width: calc(100% - 90px);
width: calc(100% - 100px);
}
.create-torrent .torrent-attribute div {
@@ -298,18 +299,6 @@ table {
text-overflow: ellipsis;
}
.create-torrent .torrent-attribute textarea {
width: calc(100% - 80px);
height: 80px;
color: #eee;
background-color: transparent;
line-height: 1.5;
font-size: 14px;
font-family: inherit;
border-radius: 2px;
padding: 4px 6px;
}
/*
* BUTTONS
*/