Compare commits

..

1 Commits

Author SHA1 Message Date
Borewit
a445c18979 Show error details in case it is not a 'NotSupportedError' error. 2019-10-07 19:03:14 +02:00
110 changed files with 6175 additions and 22226 deletions

1
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1 @@
github: feross

11
.github/ISSUE_TEMPLATE.md vendored Normal file
View File

@@ -0,0 +1,11 @@
<!-- DO NOT POST LINKS OR REFERENCES TO COPYRIGHTED CONTENT IN YOUR ISSUE. -->
**What version of WebTorrent Desktop?** (See the 'About WebTorrent' menu)
**What operating system and version?**
**What did you do?**
**What did you expect to happen?**
**What actually happened?**

View File

@@ -1,20 +0,0 @@
---
name: "🐞 Bug report"
about: Report an issue with this software
title: ''
labels: ''
assignees: ''
---
<!-- DO NOT POST LINKS OR REFERENCES TO COPYRIGHTED CONTENT IN YOUR ISSUE. -->
**What version of WebTorrent Desktop are you using?**
**What operating system and version?**
**What happened?**
**What did you expect to happen?**
**Are you willing to submit a pull request to fix this bug?**

View File

@@ -1,20 +0,0 @@
---
name: "⭐️ Feature request"
about: Request a new feature to be added
title: ''
labels: ''
assignees: ''
---
<!-- DO NOT POST LINKS OR REFERENCES TO COPYRIGHTED CONTENT IN YOUR ISSUE. -->
**What version of WebTorrent Desktop are you using?**
**What operating system and version?**
**What problem do you want to solve?**
**What do you think is the correct solution to this problem?**
**Are you willing to submit a pull request to implement this change?**

15
.github/config.yml vendored Normal file
View File

@@ -0,0 +1,15 @@
# ProBot Request Info (https://probot.github.io/apps/request-info/)
requestInfoReplyComment: >
👋 We would appreciate it if you could provide us with more information about this
issue. <br><br> ![](https://feross.net/s/cats/14.gif)
requestInfoLabelToAdd: 'need more info'
# ProBot Welcome (https://probot.github.io/apps/welcome/)
newPRWelcomeComment: >
🙌 Thanks for opening this pull request! You're awesome.
<br><br> ![](https://feross.net/s/cats/6.gif)
firstPRMergeComment: >
🎉 Congrats on getting your first pull request landed! <br><br>
![](https://feross.net/s/cats/29.gif)

4
.github/lock.yml vendored Normal file
View File

@@ -0,0 +1,4 @@
# ProBot Lock (https://probot.github.io/apps/lock/)
daysUntilLock: 365
lockComment: false

9
.github/no-response.yml vendored Normal file
View File

@@ -0,0 +1,9 @@
# ProBot No Response (https://probot.github.io/apps/no-response/)
daysUntilClose: 14
responseRequiredLabel: 'need more info'
closeComment: >
This issue has been automatically closed because there was no response to a
request for more information from the issue opener. Please leave a comment or
open a new issue if you have additional information related to this issue.
<br><br> ![](https://feross.net/s/cats/7.gif)

17
.github/stale.yml vendored Normal file
View File

@@ -0,0 +1,17 @@
# ProBot Stale (https://probot.github.io/apps/stale/)
daysUntilStale: 90
daysUntilClose: 14
exemptLabels:
- accepted
- blocked
- bug
- enhancement
- greenkeeper
- 'help wanted'
- meta
staleLabel: stale
markComment: >
This issue has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs.
closeComment: false

View File

@@ -1,21 +0,0 @@
name: ci
on: [push,pull_request]
jobs:
test:
name: Node ${{ matrix.node }} / ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os:
- ubuntu-latest
node:
- '16'
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node }}
- run: npm install
- run: npm run build --if-present
- run: npm test

View File

@@ -1,69 +0,0 @@
name: package
on:
workflow_dispatch:
jobs:
package_linux:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: '16'
- uses: actions/cache@v2
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- run: npm install
- run: npm run package -- linux
- uses: actions/upload-artifact@v2
with:
name: linux
path: |
dist/*.deb
dist/*.rpm
dist/*.zip
package_macos:
runs-on: macos-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: '16'
- uses: actions/cache@v2
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- run: npm install
- run: npm run package -- darwin
- uses: actions/upload-artifact@v2
with:
name: macos
path: |
dist/*.dmg
dist/*.zip
package_windows:
runs-on: windows-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: '16'
- uses: actions/cache@v2
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- run: npm install
- run: npm run package -- win32
- uses: actions/upload-artifact@v2
with:
name: windows
path: |
dist/*.exe
dist/*.nupkg
dist/*.zip

View File

@@ -1,23 +0,0 @@
name: Mark stale issues and pull requests
on:
schedule:
- cron: '0 12 * * *'
jobs:
stale:
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
steps:
- uses: actions/stale@v4
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
stale-issue-message: 'Is this still relevant? If so, what is blocking it? Is there anything you can do to help move it forward?'
stale-pr-message: 'Is this still relevant? If so, what is blocking it? Is there anything you can do to help move it forward?'
exempt-issue-labels: accepted,blocked,bug,dependency,enhancement,'help wanted',question,security,meta
exempt-pr-labels: accepted,blocked,bug,dependency,enhancement,'help wanted',question,security,meta
stale-issue-label: 'stale'
stale-pr-label: 'stale'

1
.gitignore vendored
View File

@@ -2,7 +2,6 @@ node_modules/
build/ build/
dist/ dist/
npm-debug.log* npm-debug.log*
.vscode/
# JetBrains IntelliJ IDEA project files # JetBrains IntelliJ IDEA project files
.idea .idea

3
.travis.yml Normal file
View File

@@ -0,0 +1,3 @@
language: node_js
node_js:
- lts/*

View File

@@ -39,7 +39,6 @@
- Nuno Campos (nuno.campos@me.com) - Nuno Campos (nuno.campos@me.com)
- Ebrahim Byagowi (ebrahim@gnu.org) - Ebrahim Byagowi (ebrahim@gnu.org)
- Josip Janzic (josip@jjanzic.com) - Josip Janzic (josip@jjanzic.com)
- Egor Yurtaev (yurtaev.egor@gmail.com)
- Emil Bay (github@tixz.dk) - Emil Bay (github@tixz.dk)
- Borewit (borewit@users.noreply.github.com) - Borewit (borewit@users.noreply.github.com)
- greenkeeper[bot] (greenkeeper[bot]@users.noreply.github.com) - greenkeeper[bot] (greenkeeper[bot]@users.noreply.github.com)
@@ -56,7 +55,6 @@
- Dan Flettre (flettre@gmail.com) - Dan Flettre (flettre@gmail.com)
- Sibiraj (dev.sibiraj@outlook.com) - Sibiraj (dev.sibiraj@outlook.com)
- clujin (clujin@gmail.com) - clujin (clujin@gmail.com)
- Sharon Grossman (sharong1337@gmail.com)
- Linus Unnebäck (linus@folkdatorn.se) - Linus Unnebäck (linus@folkdatorn.se)
- Adrian Tombu (adrian@otso.fr) - Adrian Tombu (adrian@otso.fr)
- Lucas (5874806+RecoX@users.noreply.github.com) - Lucas (5874806+RecoX@users.noreply.github.com)
@@ -66,18 +64,10 @@
- Recox (5874806+RecoX@users.noreply.github.com) - Recox (5874806+RecoX@users.noreply.github.com)
- greenkeeper[bot] (23040076+greenkeeper[bot]@users.noreply.github.com) - greenkeeper[bot] (23040076+greenkeeper[bot]@users.noreply.github.com)
- hicom150 (hicom150@gmail.com) - hicom150 (hicom150@gmail.com)
- Дамјан Георгиевски (gdamjan@gmail.com)
- Jimmy Wärting (jimmy@warting.se) - Jimmy Wärting (jimmy@warting.se)
- Julen Garcia Leunda (hicom150@gmail.com) - Julen Garcia Leunda (hicom150@gmail.com)
- Feross (feross@feross.org) - Feross (feross@feross.org)
- Daniele Debernardi (drebrez@gmail.com) - Daniele Debernardi (drebrez@gmail.com)
- Chandan Chowdary Bhagam (chandandharana@gmail.com) - Chandan Chowdary Bhagam (chandandharana@gmail.com)
- Pieter Goetschalckx (3.14.e.ter@gmail.com)
- Carey Metcalfe (carey@cmetcalfe.ca)
- Ameet Kaustav (akaustav@users.noreply.github.com)
- gpatarin (gael.patarin@outlook.com)
- Gael Patarin (gael.patarin@outlook.com)
- Subin Siby (mail@subinsb.com)
- Hinara (hinara.turevel@gmail.com)
#### Generated by bin/update-authors.sh. #### Generated by bin/update-authors.sh.

View File

@@ -1,64 +1,6 @@
# WebTorrent Desktop Version History # WebTorrent Desktop Version History
## v0.24.0 - 2020-08-28 ## v0.21.0 - 2019-08
### Added
- Support the `.m2ts` video container format ([hicom150](https://github.com/hicom150))
### Changed
- Update to Electron 10.1.0 [\#1864](https://github.com/webtorrent/webtorrent-desktop/pull/1864) ([feross](https://github.com/feross))
- Update the Windows installer loading image [\#1841](https://github.com/webtorrent/webtorrent-desktop/pull/1841) ([alxhotel](https://github.com/alxhotel))
### Fixed
- Fix music metadata not showing up [\#1847](https://github.com/webtorrent/webtorrent-desktop/pull/1847) ([Borewit](https://github.com/Borewit))
- Fix the "Play in VLC" functionality [\#1850](https://github.com/webtorrent/webtorrent-desktop/pull/1850) ([Hinara](https://github.com/Hinara))
- Prevent shortcuts from activating when user input elements are focused [\#1840](https://github.com/webtorrent/webtorrent-desktop/pull/1840) ([subins2000](https://github.com/subins2000))
## v0.23.0 - 2020-07-15
🔒 This release contains a critical security fix. Please update as soon as possible. [\#1837](https://github.com/webtorrent/webtorrent-desktop/issues/1837#issuecomment-729320901)
### Added
- Add macOS Notarization [\#1834](https://github.com/webtorrent/webtorrent-desktop/pull/1834) ([feross](https://github.com/feross))
### Changed
- Update to Electron 10 beta [\#1834](https://github.com/webtorrent/webtorrent-desktop/pull/1834) ([feross](https://github.com/feross))
## v0.22.0 - 2020-07-15
❤️✨ A new version of WebTorrent Desktop is out! ❤️✨
### Added
- Linux `.rpm` packages and `arm64` builds are now available! [\#1694](https://github.com/webtorrent/webtorrent-desktop/pull/1694) ([hicom150](https://github.com/hicom150))
- Add support for multiple audio tracks [\#1712](https://github.com/webtorrent/webtorrent-desktop/pull/1712) ([hicom150](https://github.com/hicom150))
- Improve codec unsupported detection [\#1711](https://github.com/webtorrent/webtorrent-desktop/pull/1711) ([hicom150](https://github.com/hicom150))
- Report when files are being verified [\#1717](https://github.com/webtorrent/webtorrent-desktop/pull/1717) ([pR0Ps](https://github.com/pR0Ps))
- Support additional audio files: MPEG-Layer-2, Musepack, Matroska audio, WavePack [\#1772](https://github.com/webtorrent/webtorrent-desktop/pull/1772)
### Changed
- Update to Electron 9 [\#1729](https://github.com/webtorrent/webtorrent-desktop/pull/1729) [\#1832](https://github.com/webtorrent/webtorrent-desktop/issues/1832)
- Update to music-metadata 4.8.0 [\#1719](https://github.com/webtorrent/webtorrent-desktop/pull/1719) ([Borewit](https://github.com/Borewit))
- Update Windows build documentation [\#1715](https://github.com/webtorrent/webtorrent-desktop/pull/1715) ([RecoX](https://github.com/RecoX))
- Remove unneeded dependencies ([feross](https://github.com/feross))
### Fixed
- Fix a few type errors [\#1720](https://github.com/webtorrent/webtorrent-desktop/pull/1720) ([mathiasvr](https://github.com/mathiasvr))
- Fix electron SUID sandbox error [\#1707](https://github.com/webtorrent/webtorrent-desktop/pull/1707) ([hicom150](https://github.com/hicom150))
- Fix percentage rounding error [\#1716](https://github.com/webtorrent/webtorrent-desktop/pull/1716) ([pR0Ps](https://github.com/pR0Ps))
- Fix path-selector in preferences page [\#1702](https://github.com/webtorrent/webtorrent-desktop/pull/1702) ([314eter](https://github.com/314eter))
- Fix path-selector in preferences page [\#1704](https://github.com/webtorrent/webtorrent-desktop/pull/1702) ([mathiasvr](https://github.com/mathiasvr))
- Fix: Increase height of 'About' window [\#1737](https://github.com/webtorrent/webtorrent-desktop/pull/1737) ([akaustav](https://github.com/akaustav))
- Fix "Save Torrent File As..." [\#1743](https://github.com/webtorrent/webtorrent-desktop/pull/1743) ([gpatarin](https://github.com/gpatarin))
## v0.21.0 - 2019-09-14
### Added ### Added

View File

@@ -35,7 +35,7 @@ Download the latest version of WebTorrent Desktop from
- Use [Homebrew-Cask](https://github.com/caskroom/homebrew-cask) to install from the command line: - Use [Homebrew-Cask](https://github.com/caskroom/homebrew-cask) to install from the command line:
``` ```
$ brew install --cask webtorrent $ brew cask install webtorrent
``` ```
- Try the (unstable) development version by cloning the Git repository. See the - Try the (unstable) development version by cloning the Git repository. See the
@@ -174,7 +174,7 @@ React.js (Framework to work with Frontend UI):
https://reactjs.org/docs/getting-started.html https://reactjs.org/docs/getting-started.html
Material UI (React components that implement Google's Material Design.): Material UI (React components that implement Google's Material Design.):
https://material-ui.com/getting-started/installation https://material-ui.com/getting-started
### Privacy ### Privacy

View File

@@ -46,11 +46,6 @@
``` ```
npm run package -- darwin --sign npm run package -- darwin --sign
```
Move the `.zip` and `.dmg` file somewhere because the next step wipes the `dist/` folder away.
```
npm run package -- linux --sign npm run package -- linux --sign
``` ```
@@ -74,29 +69,17 @@
**This is the most important part.** **This is the most important part.**
- Manually download the binaries for each platform from Github. - Manually download the binaries for each platform from Github.
**Do not use your locally built binaries.** Modern OSs treat executables differently if they've **Do not use your locally built binaries.** Modern OSs treat executables differently if they've
been downloaded, even though the files are byte for byte identical. This ensures that the been downloaded, even though the files are byte for byte identical. This ensures that the
codesigning worked and is valid. codesigning worked and is valid.
- Smoke test WebTorrent Desktop on each platform. Before a release, check that the following basic use cases work correctly: - Smoke test WebTorrent Desktop on each platform.
1. Click "Play" to stream a built-in torrent (e.g. Sintel) See Smoke Tests below for details. Open DevTools
- Ensure that seeking to undownloaded region works and plays immediately. on Windows and Mac, and ensure that the auto updater is running. If the auto updater does not
- Ensure that sintel.mp4 gets downloaded to `~/Downloads`. run, users will successfully auto update to this new version, and then be stuck there forever.
2. Check that the auto-updater works
- Open the console and check for the line "No update available" to indicate that the auto-updater is working. (If the auto updater does not run, users will successfully auto update to this new version, and then be stuck there forever.)
3. Add a new .torrent file via drag-and-drop.
- Ensure that it gets added to the list and starts downloading.
4. Remove a torrent from the client
- Ensure that the file is removed from `~/Downloads`
5. Create and seed a new a torrent via drag-and-drop.
- Ensure that the torrent gets created and seeding begins.
### 4. Ship it ### 4. Ship it
@@ -105,5 +88,25 @@
Create a pull request in [webtorrent.io](https://github.com/webtorrent/webtorrent.io). Update Create a pull request in [webtorrent.io](https://github.com/webtorrent/webtorrent.io). Update
`config.js`, updating the desktop app version. `config.js`, updating the desktop app version.
Once this PR is merged and Feross redeploys the WebTorrent website, As soon as this PR is merged, Jenkins will automatically redeploy the WebTorrent website, and
hundreds of thousands of users around the world will start auto updating. **Merge with care.** hundreds of thousands of users around the world will start auto updating. **Merge with care.**
## Smoke Tests
Before a release, check that the following basic use cases work correctly:
1. Click "Play" to stream a built-in torrent (e.g. Sintel)
- Ensure that seeking to undownloaded region works and plays immediately.
- Ensure that sintel.mp4 gets downloaded to `~/Downloads`.
2. Check that the auto-updater works
- Open the console and check for the line "No update available" to indicate
3. Add a new .torrent file via drag-and-drop.
- Ensure that it gets added to the list and starts downloading
4. Remove a torrent from the client
- Ensure that the file is removed from `~/Downloads`
5. Create and seed a new a torrent via drag-and-drop.
- Ensure that the torrent gets created and seeding begins.

View File

@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.cs.debugger</key>
<true/>
</dict>
</plist>

39
bin/extra-lint.js Executable file
View File

@@ -0,0 +1,39 @@
#!/usr/bin/env node
const walkSync = require('walk-sync')
const fs = require('fs')
const path = require('path')
let hasErrors = false
// Find all Javascript source files
const files = walkSync('src', { globs: ['**/*.js'] })
console.log('Running extra-lint on ' + files.length + ' files...')
// Read each file, line by line
files.forEach(function (file) {
const filepath = path.join('src', file)
const lines = fs.readFileSync(filepath, 'utf8').split('\n')
lines.forEach(function (line, i) {
let error
// No lines over 100 characters
if (line.length > 100) {
error = 'Line >100 chars'
}
if (line.match(/^var /) || line.match(/ var /)) {
error = 'Use const or let'
}
if (error) {
const name = path.basename(file)
console.log('%s:%d - %s:\n%s', name, i + 1, error, line)
hasErrors = true
}
})
})
if (hasErrors) process.exit(1)
else console.log('Looks good!')

View File

@@ -8,6 +8,7 @@ const cp = require('child_process')
const electronPackager = require('electron-packager') const electronPackager = require('electron-packager')
const fs = require('fs') const fs = require('fs')
const minimist = require('minimist') const minimist = require('minimist')
const mkdirp = require('mkdirp')
const os = require('os') const os = require('os')
const path = require('path') const path = require('path')
const rimraf = require('rimraf') const rimraf = require('rimraf')
@@ -36,7 +37,7 @@ const argv = minimist(process.argv.slice(2), {
}) })
function build () { function build () {
console.log('Installing node_modules...') console.log('Reinstalling node_modules...')
rimraf.sync(NODE_MODULES_PATH) rimraf.sync(NODE_MODULES_PATH)
cp.execSync('npm ci', { stdio: 'inherit' }) cp.execSync('npm ci', { stdio: 'inherit' })
@@ -172,8 +173,8 @@ const linux = {
// Build for Linux. // Build for Linux.
platform: 'linux', platform: 'linux',
// Build x64, armv7l, and arm64 binaries. // Build x64 and arm64 binaries.
arch: ['x64', 'armv7l', 'arm64'] arch: ['x64', 'arm64']
// Note: Application icon for Linux is specified via the BrowserWindow `icon` option. // Note: Application icon for Linux is specified via the BrowserWindow `icon` option.
} }
@@ -266,7 +267,6 @@ function buildDarwin (cb) {
function signApp (cb) { function signApp (cb) {
const sign = require('electron-osx-sign') const sign = require('electron-osx-sign')
const { notarize } = require('electron-notarize')
/* /*
* Sign the app with Apple Developer ID certificates. We sign the app for 2 reasons: * Sign the app with Apple Developer ID certificates. We sign the app for 2 reasons:
@@ -282,37 +282,16 @@ function buildDarwin (cb) {
* - Membership in the Apple Developer Program * - Membership in the Apple Developer Program
*/ */
const signOpts = { const signOpts = {
verbose: true,
app: appPath, app: appPath,
platform: 'darwin', platform: 'darwin',
identity: 'Developer ID Application: WebTorrent, LLC (5MAMC8G3L8)', verbose: true
hardenedRuntime: true,
entitlements: path.join(config.ROOT_PATH, 'bin', 'darwin-entitlements.plist'),
'entitlements-inherit': path.join(config.ROOT_PATH, 'bin', 'darwin-entitlements.plist'),
'signature-flags': 'library'
}
const notarizeOpts = {
appBundleId: darwin.appBundleId,
appPath,
appleId: 'feross@feross.org',
appleIdPassword: '@keychain:AC_PASSWORD'
} }
console.log('Mac: Signing app...') console.log('Mac: Signing app...')
sign(signOpts, function (err) { sign(signOpts, function (err) {
if (err) return cb(err) if (err) return cb(err)
console.log('Mac: Signed app.') console.log('Mac: Signed app.')
cb(null)
console.log('Mac: Notarizing app...')
notarize(notarizeOpts).then(
function () {
console.log('Mac: Notarized app.')
cb(null)
},
function (err) {
cb(err)
})
}) })
} }
@@ -451,7 +430,7 @@ function buildWin32 (cb) {
// remoteToken: process.env.WEBTORRENT_GITHUB_API_TOKEN, // remoteToken: process.env.WEBTORRENT_GITHUB_API_TOKEN,
setupExe: config.APP_NAME + 'Setup-v' + config.APP_VERSION + '.exe', setupExe: config.APP_NAME + 'Setup-v' + config.APP_VERSION + '.exe',
setupIcon: config.APP_ICON + '.ico', setupIcon: config.APP_ICON + '.ico',
signWithParams, signWithParams: signWithParams,
title: config.APP_NAME, title: config.APP_NAME,
usePackageJson: false, usePackageJson: false,
version: pkg.version version: pkg.version
@@ -478,13 +457,13 @@ function buildWin32 (cb) {
console.log('Windows: Creating portable app...') console.log('Windows: Creating portable app...')
const portablePath = path.join(filesPath, 'Portable Settings') const portablePath = path.join(filesPath, 'Portable Settings')
fs.mkdirSync(portablePath, { recursive: true }) mkdirp.sync(portablePath)
const downloadsPath = path.join(portablePath, 'Downloads') const downloadsPath = path.join(portablePath, 'Downloads')
fs.mkdirSync(downloadsPath, { recursive: true }) mkdirp.sync(downloadsPath)
const tempPath = path.join(portablePath, 'Temp') const tempPath = path.join(portablePath, 'Temp')
fs.mkdirSync(tempPath, { recursive: true }) mkdirp.sync(tempPath)
const inPath = path.join(DIST_PATH, path.basename(filesPath)) const inPath = path.join(DIST_PATH, path.basename(filesPath))
const outPath = path.join(DIST_PATH, BUILD_NAME + '-win.zip') const outPath = path.join(DIST_PATH, BUILD_NAME + '-win.zip')
@@ -545,17 +524,7 @@ function buildLinux (cb) {
}, },
categories: ['Network', 'FileTransfer', 'P2P'], categories: ['Network', 'FileTransfer', 'P2P'],
mimeType: ['application/x-bittorrent', 'x-scheme-handler/magnet', 'x-scheme-handler/stream-magnet'], mimeType: ['application/x-bittorrent', 'x-scheme-handler/magnet', 'x-scheme-handler/stream-magnet'],
desktopTemplate: path.join(config.STATIC_PATH, 'linux/webtorrent-desktop.ejs'), desktopTemplate: path.join(config.STATIC_PATH, 'linux/webtorrent-desktop.ejs')
lintianOverrides: [
'unstripped-binary-or-object',
'embedded-library',
'missing-dependency-on-libc',
'changelog-file-missing-in-native-package',
'description-synopsis-is-duplicated',
'setuid-binary',
'binary-without-manpage',
'shlib-with-executable-bit'
]
} }
installer(options).then( installer(options).then(

View File

@@ -13,7 +13,6 @@ while (<>) {
next if /(dc\@DCs-MacBook.local)/; next if /(dc\@DCs-MacBook.local)/;
next if /(rolandoguedes\@gmail.com)/; next if /(rolandoguedes\@gmail.com)/;
next if /(grunjol\@users.noreply.github.com)/; next if /(grunjol\@users.noreply.github.com)/;
next if /(dependabot)/;
$seen{$_} = push @authors, "- ", $_; $seen{$_} = push @authors, "- ", $_;
} }
END { END {

26142
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,88 +1,76 @@
{ {
"name": "webtorrent-desktop", "name": "webtorrent-desktop",
"description": "WebTorrent, the streaming torrent client. For Mac, Windows, and Linux.", "description": "WebTorrent, the streaming torrent client. For Mac, Windows, and Linux.",
"version": "0.24.0", "version": "0.21.0",
"author": { "author": {
"name": "WebTorrent, LLC", "name": "WebTorrent, LLC",
"email": "feross@webtorrent.io", "email": "feross@webtorrent.io",
"url": "https://webtorrent.io" "url": "https://webtorrent.io"
}, },
"babel": {
"plugins": [
[
"@babel/plugin-transform-react-jsx",
{
"useBuiltIns": true
}
]
]
},
"bugs": { "bugs": {
"url": "https://github.com/webtorrent/webtorrent-desktop/issues" "url": "https://github.com/webtorrent/webtorrent-desktop/issues"
}, },
"dependencies": { "dependencies": {
"@electron/remote": "2.0.1",
"airplayer": "github:webtorrent/airplayer#fix-security", "airplayer": "github:webtorrent/airplayer#fix-security",
"application-config": "2.0.0", "application-config": "^1.0.1",
"arch": "2.2.0", "arch": "^2.1.1",
"auto-launch": "5.0.5", "auto-launch": "^5.0.5",
"bitfield": "4.0.0", "bitfield": "^3.0.0",
"capture-frame": "4.0.0", "capture-frame": "^3.0.0",
"chokidar": "3.5.2", "chokidar": "^3.0.2",
"chromecasts": "1.10.2", "chromecasts": "^1.9.1",
"create-torrent": "5.0.1", "create-torrent": "^4.4.1",
"debounce": "1.2.1", "debounce": "^1.2.0",
"dlnacasts": "0.1.0", "deep-equal": "^1.1.0",
"drag-drop": "7.2.0", "dlnacasts": "^0.1.0",
"es6-error": "4.1.1", "drag-drop": "^5.0.1",
"fn-getter": "1.0.0", "es6-error": "^4.1.1",
"iso-639-1": "2.1.9", "fn-getter": "^1.0.0",
"languagedetect": "2.0.0", "iso-639-1": "^2.1.0",
"location-history": "1.1.2", "languagedetect": "^1.2.0",
"material-ui": "0.20.2", "location-history": "^1.1.1",
"music-metadata": "7.11.4", "material-ui": "^0.20.2",
"network-address": "1.1.2", "mkdirp": "^0.5.1",
"parse-torrent": "9.1.4", "music-metadata": "^4.5.3",
"prettier-bytes": "1.0.4", "network-address": "^1.1.2",
"prop-types": "15.7.2", "parse-torrent": "^7.0.1",
"react": "17.0.2", "prettier-bytes": "^1.0.4",
"react-dom": "17.0.2", "prop-types": "^15.7.2",
"rimraf": "3.0.2", "react": "^16.9.0",
"run-parallel": "1.2.0", "react-dom": "^16.9.0",
"semver": "7.3.5", "rimraf": "^3.0.0",
"simple-concat": "1.0.1", "run-parallel": "^1.1.9",
"simple-get": "4.0.0", "semver": "^6.3.0",
"srt-to-vtt": "1.1.3", "simple-concat": "^1.0.0",
"vlc-command": "1.2.0", "simple-get": "^3.0.3",
"webtorrent": "1.5.6", "srt-to-vtt": "^1.1.3",
"winreg": "1.2.4" "vlc-command": "^1.2.0",
"webtorrent": ">=0.107.16",
"winreg": "^1.2.4"
}, },
"devDependencies": { "devDependencies": {
"@babel/cli": "7.15.7", "babel-eslint": "^10.0.3",
"@babel/core": "7.15.8", "buble": "^0.19.8",
"@babel/eslint-parser": "7.15.8", "cross-zip": "^2.1.6",
"@babel/plugin-transform-react-jsx": "7.14.9", "depcheck": "^0.8.3",
"cross-zip": "4.0.0", "electron": "~6.0.9",
"depcheck": "1.4.2", "electron-osx-sign": "^0.4.13",
"electron": "15.1.2", "electron-packager": "^14.0.6",
"electron-notarize": "1.1.1", "electron-winstaller": "^4.0.0",
"electron-osx-sign": "0.5.0", "gh-release": "^3.5.0",
"electron-packager": "15.4.0", "minimist": "^1.2.0",
"electron-winstaller": "5.0.0", "nodemon": "^1.19.2",
"gh-release": "6.0.1", "open": "^6.4.0",
"minimist": "1.2.5", "plist": "^3.0.1",
"nodemon": "2.0.13", "pngjs": "^3.4.0",
"open": "8.3.0", "run-series": "^1.1.8",
"plist": "3.0.4", "spectron": "^8.0.0",
"pngjs": "6.0.0", "standard": "*",
"run-series": "1.1.9", "tape": "^4.11.0",
"spectron": "15.0.0", "walk-sync": "^2.0.2"
"standard": "16.0.4",
"tape": "5.3.1"
}, },
"engines": { "engines": {
"node": "^16.0.0", "node": ">=4.0.0"
"npm": "^7.10.0 || ^8.0.0"
}, },
"homepage": "https://webtorrent.io", "homepage": "https://webtorrent.io",
"keywords": [ "keywords": [
@@ -99,8 +87,8 @@
"main": "index.js", "main": "index.js",
"optionalDependencies": { "optionalDependencies": {
"appdmg": "^0.6.0", "appdmg": "^0.6.0",
"electron-installer-debian": "^3.1.0", "electron-installer-debian": "^2.0.0",
"electron-installer-redhat": "^3.3.0" "electron-installer-redhat": "^2.0.0"
}, },
"private": true, "private": true,
"productName": "WebTorrent", "productName": "WebTorrent",
@@ -109,24 +97,19 @@
"url": "git://github.com/webtorrent/webtorrent-desktop.git" "url": "git://github.com/webtorrent/webtorrent-desktop.git"
}, },
"scripts": { "scripts": {
"build": "babel src --out-dir build", "build": "buble src --target chrome:71 --output build",
"clean": "node ./bin/clean.js", "clean": "node ./bin/clean.js",
"gh-release": "gh-release", "gh-release": "gh-release",
"install-system-deps": "brew install fakeroot dpkg rpm", "install-system-deps": "brew install fakeroot dpkg rpm",
"open-config": "node ./bin/open-config.js", "open-config": "node ./bin/open-config.js",
"package": "node ./bin/package.js", "package": "node ./bin/package.js",
"start": "npm run build && electron --no-sandbox .", "start": "npm run build && electron .",
"test": "standard && depcheck --ignores=standard,@babel/eslint-parser --ignore-dirs=build,dist", "test": "standard && depcheck --ignores=standard,babel-eslint --ignore-dirs=build,dist && node ./bin/extra-lint.js",
"test-integration": "npm run build && node ./test", "test-integration": "npm run build && node ./test",
"update-authors": "./bin/update-authors.sh", "update-authors": "./bin/update-authors.sh",
"watch": "nodemon --exec \"npm run start\" --ext js,css --ignore build/ --ignore dist/" "watch": "nodemon --exec \"npm run start\" --ext js,css --ignore build/ --ignore dist/"
}, },
"standard": { "standard": {
"parser": "@babel/eslint-parser" "parser": "babel-eslint"
},
"renovate": {
"extends": [
"github>webtorrent/renovate-config"
]
} }
} }

View File

@@ -1,6 +1,6 @@
const appConfig = require('application-config')('WebTorrent') const appConfig = require('application-config')('WebTorrent')
const path = require('path') const path = require('path')
const { app } = require('electron') const electron = require('electron')
const arch = require('arch') const arch = require('arch')
const APP_NAME = 'WebTorrent' const APP_NAME = 'WebTorrent'
@@ -23,12 +23,12 @@ module.exports = {
CRASH_REPORT_URL: 'https://webtorrent.io/desktop/crash-report', CRASH_REPORT_URL: 'https://webtorrent.io/desktop/crash-report',
TELEMETRY_URL: 'https://webtorrent.io/desktop/telemetry', TELEMETRY_URL: 'https://webtorrent.io/desktop/telemetry',
APP_COPYRIGHT: `Copyright © 2014-${new Date().getFullYear()} ${APP_TEAM}`, APP_COPYRIGHT: 'Copyright © 2014-2019 ' + APP_TEAM,
APP_FILE_ICON: path.join(__dirname, '..', 'static', 'WebTorrentFile'), APP_FILE_ICON: path.join(__dirname, '..', 'static', 'WebTorrentFile'),
APP_ICON: path.join(__dirname, '..', 'static', 'WebTorrent'), APP_ICON: path.join(__dirname, '..', 'static', 'WebTorrent'),
APP_NAME, APP_NAME: APP_NAME,
APP_TEAM, APP_TEAM: APP_TEAM,
APP_VERSION, APP_VERSION: APP_VERSION,
APP_WINDOW_TITLE: APP_NAME, APP_WINDOW_TITLE: APP_NAME,
CONFIG_PATH: getConfigPath(), CONFIG_PATH: getConfigPath(),
@@ -78,9 +78,9 @@ module.exports = {
HOME_PAGE_URL: 'https://webtorrent.io', HOME_PAGE_URL: 'https://webtorrent.io',
TWITTER_PAGE_URL: 'https://twitter.com/WebTorrentApp', TWITTER_PAGE_URL: 'https://twitter.com/WebTorrentApp',
IS_PORTABLE, IS_PORTABLE: IS_PORTABLE,
IS_PRODUCTION, IS_PRODUCTION: IS_PRODUCTION,
IS_TEST, IS_TEST: IS_TEST,
OS_SYSARCH: arch() === 'x64' ? 'x64' : 'ia32', OS_SYSARCH: arch() === 'x64' ? 'x64' : 'ia32',
@@ -100,8 +100,8 @@ module.exports = {
WINDOW_MIN_HEIGHT: UI_HEADER_HEIGHT + (UI_TORRENT_HEIGHT * 2), // header + 2 torrents WINDOW_MIN_HEIGHT: UI_HEADER_HEIGHT + (UI_TORRENT_HEIGHT * 2), // header + 2 torrents
WINDOW_MIN_WIDTH: 425, WINDOW_MIN_WIDTH: 425,
UI_HEADER_HEIGHT, UI_HEADER_HEIGHT: UI_HEADER_HEIGHT,
UI_TORRENT_HEIGHT UI_TORRENT_HEIGHT: UI_TORRENT_HEIGHT
} }
function getConfigPath () { function getConfigPath () {
@@ -126,10 +126,10 @@ function getPath (key) {
return '' return ''
} else if (process.type === 'renderer') { } else if (process.type === 'renderer') {
// Electron renderer process // Electron renderer process
return require('@electron/remote').app.getPath(key) return electron.remote.app.getPath(key)
} else { } else {
// Electron main process // Electron main process
return app.getPath(key) return electron.app.getPath(key)
} }
} }

View File

@@ -4,12 +4,11 @@ module.exports = {
function init () { function init () {
const config = require('./config') const config = require('./config')
const { crashReporter } = require('electron') const electron = require('electron')
crashReporter.start({ electron.crashReporter.start({
companyName: config.APP_NAME,
productName: config.APP_NAME, productName: config.APP_NAME,
submitURL: config.CRASH_REPORT_URL, submitURL: config.CRASH_REPORT_URL
globalExtra: { _companyName: config.APP_NAME },
compress: true
}) })
} }

View File

@@ -2,7 +2,7 @@ module.exports = {
init init
} }
const { dialog } = require('electron') const electron = require('electron')
const config = require('../config') const config = require('../config')
const log = require('./log') const log = require('./log')
@@ -44,11 +44,13 @@ function onResponse (err, res, data) {
} }
} }
dialog.showMessageBox({ electron.dialog.showMessageBox({
type: 'info', type: 'info',
buttons: ['OK'], buttons: ['OK'],
title: data.title, title: data.title,
message: data.message, message: data.message,
detail: data.detail detail: data.detail
}) }, noop)
} }
function noop () {}

View File

@@ -6,7 +6,7 @@ module.exports = {
openFiles openFiles
} }
const { dialog } = require('electron') const electron = require('electron')
const log = require('./log') const log = require('./log')
const windows = require('./windows') const windows = require('./windows')
@@ -34,13 +34,13 @@ function openSeedDirectory () {
log('openSeedDirectory') log('openSeedDirectory')
const opts = process.platform === 'darwin' const opts = process.platform === 'darwin'
? { ? {
title: 'Select a file or folder for the torrent.', title: 'Select a file or folder for the torrent.',
properties: ['openFile', 'openDirectory'] properties: ['openFile', 'openDirectory']
} }
: { : {
title: 'Select a folder for the torrent.', title: 'Select a folder for the torrent.',
properties: ['openDirectory'] properties: ['openDirectory']
} }
showOpenSeed(opts) showOpenSeed(opts)
} }
@@ -53,18 +53,19 @@ function openFiles () {
log('openFiles') log('openFiles')
const opts = process.platform === 'darwin' const opts = process.platform === 'darwin'
? { ? {
title: 'Select a file or folder to add.', title: 'Select a file or folder to add.',
properties: ['openFile', 'openDirectory'] properties: ['openFile', 'openDirectory']
} }
: { : {
title: 'Select a file to add.', title: 'Select a file to add.',
properties: ['openFile'] properties: ['openFile']
} }
setTitle(opts.title) setTitle(opts.title)
const selectedPaths = dialog.showOpenDialogSync(windows.main.win, opts) electron.dialog.showOpenDialog(windows.main.win, opts, function (selectedPaths) {
resetTitle() resetTitle()
if (!Array.isArray(selectedPaths)) return if (!Array.isArray(selectedPaths)) return
windows.main.dispatch('onOpen', selectedPaths) windows.main.dispatch('onOpen', selectedPaths)
})
} }
/* /*
@@ -79,11 +80,12 @@ function openTorrentFile () {
properties: ['openFile', 'multiSelections'] properties: ['openFile', 'multiSelections']
} }
setTitle(opts.title) setTitle(opts.title)
const selectedPaths = dialog.showOpenDialogSync(windows.main.win, opts) electron.dialog.showOpenDialog(windows.main.win, opts, function (selectedPaths) {
resetTitle() resetTitle()
if (!Array.isArray(selectedPaths)) return if (!Array.isArray(selectedPaths)) return
selectedPaths.forEach(selectedPath => { selectedPaths.forEach(function (selectedPath) {
windows.main.dispatch('addTorrent', selectedPath) windows.main.dispatch('addTorrent', selectedPath)
})
}) })
} }
@@ -114,8 +116,9 @@ function resetTitle () {
*/ */
function showOpenSeed (opts) { function showOpenSeed (opts) {
setTitle(opts.title) setTitle(opts.title)
const selectedPaths = dialog.showOpenDialogSync(windows.main.win, opts) electron.dialog.showOpenDialog(windows.main.win, opts, function (selectedPaths) {
resetTitle() resetTitle()
if (!Array.isArray(selectedPaths)) return if (!Array.isArray(selectedPaths)) return
windows.main.dispatch('showCreateTorrent', selectedPaths) windows.main.dispatch('showCreateTorrent', selectedPaths)
})
} }

View File

@@ -34,7 +34,7 @@ function setBadge (count) {
if (process.platform === 'darwin' || if (process.platform === 'darwin' ||
(process.platform === 'linux' && app.isUnityRunning())) { (process.platform === 'linux' && app.isUnityRunning())) {
log(`setBadge: ${count}`) log(`setBadge: ${count}`)
app.badgeCount = Number(count) app.setBadgeCount(Number(count))
} }
} }

View File

@@ -17,12 +17,12 @@ let proc = null
function checkInstall (playerPath, cb) { function checkInstall (playerPath, cb) {
// check for VLC if external player has not been specified by the user // check for VLC if external player has not been specified by the user
// otherwise assume the player is installed // otherwise assume the player is installed
if (!playerPath) return vlcCommand(cb) if (playerPath == null) return vlcCommand(cb)
process.nextTick(() => cb(null)) process.nextTick(() => cb(null))
} }
function spawn (playerPath, url, title) { function spawn (playerPath, url, title) {
if (playerPath) return spawnExternal(playerPath, [url]) if (playerPath != null) return spawnExternal(playerPath, [url])
// Try to find and use VLC if external player is not specified // Try to find and use VLC if external player is not specified
vlcCommand((err, vlcPath) => { vlcCommand((err, vlcPath) => {

View File

@@ -25,7 +25,8 @@ function uninstall () {
} }
function installDarwin () { function installDarwin () {
const { app } = require('electron') const electron = require('electron')
const app = electron.app
// On Mac, only protocols that are listed in `Info.plist` can be set as the // On Mac, only protocols that are listed in `Info.plist` can be set as the
// default handler at runtime. // default handler at runtime.

View File

@@ -1,15 +1,12 @@
console.time('init') console.time('init')
require('@electron/remote/main').initialize() const electron = require('electron')
const { app, ipcMain } = require('electron') const app = electron.app
// Start crash reporter early, so it takes effect for child processes
const crashReporter = require('../crash-reporter')
crashReporter.init()
const parallel = require('run-parallel') const parallel = require('run-parallel')
const config = require('../config') const config = require('../config')
const crashReporter = require('../crash-reporter')
const ipc = require('./ipc') const ipc = require('./ipc')
const log = require('./log') const log = require('./log')
const menu = require('./menu') const menu = require('./menu')
@@ -67,6 +64,8 @@ function init () {
app.setPath('temp', path.join(config.CONFIG_PATH, 'Temp')) app.setPath('temp', path.join(config.CONFIG_PATH, 'Temp'))
} }
const ipcMain = electron.ipcMain
let isReady = false // app ready, windows can be created let isReady = false // app ready, windows can be created
app.ipcReady = false // main window has finished loading and IPC is ready app.ipcReady = false // main window has finished loading and IPC is ready
app.isQuitting = false app.isQuitting = false
@@ -82,9 +81,9 @@ function init () {
isReady = true isReady = true
const state = results.state const state = results.state
menu.init() windows.main.init(state, { hidden: hidden })
windows.main.init(state, { hidden })
windows.webtorrent.init() windows.webtorrent.init()
menu.init()
// To keep app startup fast, some code is delayed. // To keep app startup fast, some code is delayed.
setTimeout(() => { setTimeout(() => {
@@ -110,13 +109,17 @@ function init () {
ipc.init() ipc.init()
app.once('ipcReady', () => { app.once('will-finish-launching', function () {
crashReporter.init()
})
app.once('ipcReady', function () {
log('Command line args:', argv) log('Command line args:', argv)
processArgv(argv) processArgv(argv)
console.timeEnd('init') console.timeEnd('init')
}) })
app.on('before-quit', e => { app.on('before-quit', function (e) {
if (app.isQuitting) return if (app.isQuitting) return
app.isQuitting = true app.isQuitting = true
@@ -129,7 +132,7 @@ function init () {
}, 4000) // quit after 4 secs, at most }, 4000) // quit after 4 secs, at most
}) })
app.on('activate', () => { app.on('activate', function () {
if (isReady) windows.main.show() if (isReady) windows.main.show()
}) })
} }
@@ -196,18 +199,14 @@ function onAppOpen (newArgv) {
// Development: 2 args, eg: electron . // Development: 2 args, eg: electron .
// Test: 4 args, eg: electron -r .../mocks.js . // Test: 4 args, eg: electron -r .../mocks.js .
function sliceArgv (argv) { function sliceArgv (argv) {
return argv.slice( return argv.slice(config.IS_PRODUCTION ? 1
config.IS_PRODUCTION : config.IS_TEST ? 4
? 1 : 2)
: config.IS_TEST
? 4
: 2
)
} }
function processArgv (argv) { function processArgv (argv) {
const torrentIds = [] const torrentIds = []
argv.forEach(arg => { argv.forEach(function (arg) {
if (arg === '-n' || arg === '-o' || arg === '-u') { if (arg === '-n' || arg === '-o' || arg === '-u') {
// Critical path: Only load the 'dialog' package if it is needed // Critical path: Only load the 'dialog' package if it is needed
const dialog = require('./dialog') const dialog = require('./dialog')

View File

@@ -3,7 +3,9 @@ module.exports = {
setModule setModule
} }
const { app, ipcMain } = require('electron') const electron = require('electron')
const app = electron.app
const log = require('./log') const log = require('./log')
const menu = require('./menu') const menu = require('./menu')
@@ -21,16 +23,18 @@ function setModule (name, module) {
} }
function init () { function init () {
ipcMain.once('ipcReady', e => { const ipc = electron.ipcMain
ipc.once('ipcReady', function (e) {
app.ipcReady = true app.ipcReady = true
app.emit('ipcReady') app.emit('ipcReady')
}) })
ipcMain.once('ipcReadyWebTorrent', e => { ipc.once('ipcReadyWebTorrent', function (e) {
app.ipcReadyWebTorrent = true app.ipcReadyWebTorrent = true
log('sending %d queued messages from the main win to the webtorrent window', log('sending %d queued messages from the main win to the webtorrent window',
messageQueueMainToWebTorrent.length) messageQueueMainToWebTorrent.length)
messageQueueMainToWebTorrent.forEach(message => { messageQueueMainToWebTorrent.forEach(function (message) {
windows.webtorrent.send(message.name, ...message.args) windows.webtorrent.send(message.name, ...message.args)
log('webtorrent: sent queued %s', message.name) log('webtorrent: sent queued %s', message.name)
}) })
@@ -40,11 +44,11 @@ function init () {
* Dialog * Dialog
*/ */
ipcMain.on('openTorrentFile', () => { ipc.on('openTorrentFile', () => {
const dialog = require('./dialog') const dialog = require('./dialog')
dialog.openTorrentFile() dialog.openTorrentFile()
}) })
ipcMain.on('openFiles', () => { ipc.on('openFiles', () => {
const dialog = require('./dialog') const dialog = require('./dialog')
dialog.openFiles() dialog.openFiles()
}) })
@@ -53,11 +57,11 @@ function init () {
* Dock * Dock
*/ */
ipcMain.on('setBadge', (e, ...args) => { ipc.on('setBadge', (e, ...args) => {
const dock = require('./dock') const dock = require('./dock')
dock.setBadge(...args) dock.setBadge(...args)
}) })
ipcMain.on('downloadFinished', (e, ...args) => { ipc.on('downloadFinished', (e, ...args) => {
const dock = require('./dock') const dock = require('./dock')
dock.downloadFinished(...args) dock.downloadFinished(...args)
}) })
@@ -66,7 +70,7 @@ function init () {
* Player Events * Player Events
*/ */
ipcMain.on('onPlayerOpen', () => { ipc.on('onPlayerOpen', function () {
const powerSaveBlocker = require('./power-save-blocker') const powerSaveBlocker = require('./power-save-blocker')
const shortcuts = require('./shortcuts') const shortcuts = require('./shortcuts')
const thumbar = require('./thumbar') const thumbar = require('./thumbar')
@@ -77,14 +81,14 @@ function init () {
thumbar.enable() thumbar.enable()
}) })
ipcMain.on('onPlayerUpdate', (e, ...args) => { ipc.on('onPlayerUpdate', function (e, ...args) {
const thumbar = require('./thumbar') const thumbar = require('./thumbar')
menu.onPlayerUpdate(...args) menu.onPlayerUpdate(...args)
thumbar.onPlayerUpdate(...args) thumbar.onPlayerUpdate(...args)
}) })
ipcMain.on('onPlayerClose', () => { ipc.on('onPlayerClose', function () {
const powerSaveBlocker = require('./power-save-blocker') const powerSaveBlocker = require('./power-save-blocker')
const shortcuts = require('./shortcuts') const shortcuts = require('./shortcuts')
const thumbar = require('./thumbar') const thumbar = require('./thumbar')
@@ -95,7 +99,7 @@ function init () {
thumbar.disable() thumbar.disable()
}) })
ipcMain.on('onPlayerPlay', () => { ipc.on('onPlayerPlay', function () {
const powerSaveBlocker = require('./power-save-blocker') const powerSaveBlocker = require('./power-save-blocker')
const thumbar = require('./thumbar') const thumbar = require('./thumbar')
@@ -103,7 +107,7 @@ function init () {
thumbar.onPlayerPlay() thumbar.onPlayerPlay()
}) })
ipcMain.on('onPlayerPause', () => { ipc.on('onPlayerPause', function () {
const powerSaveBlocker = require('./power-save-blocker') const powerSaveBlocker = require('./power-save-blocker')
const thumbar = require('./thumbar') const thumbar = require('./thumbar')
@@ -115,7 +119,7 @@ function init () {
* Folder Watcher Events * Folder Watcher Events
*/ */
ipcMain.on('startFolderWatcher', () => { ipc.on('startFolderWatcher', function () {
if (!modules.folderWatcher) { if (!modules.folderWatcher) {
log('IPC ERR: folderWatcher module is not defined.') log('IPC ERR: folderWatcher module is not defined.')
return return
@@ -124,7 +128,7 @@ function init () {
modules.folderWatcher.start() modules.folderWatcher.start()
}) })
ipcMain.on('stopFolderWatcher', () => { ipc.on('stopFolderWatcher', function () {
if (!modules.folderWatcher) { if (!modules.folderWatcher) {
log('IPC ERR: folderWatcher module is not defined.') log('IPC ERR: folderWatcher module is not defined.')
return return
@@ -137,15 +141,15 @@ function init () {
* Shell * Shell
*/ */
ipcMain.on('openPath', (e, ...args) => { ipc.on('openItem', (e, ...args) => {
const shell = require('./shell') const shell = require('./shell')
shell.openPath(...args) shell.openItem(...args)
}) })
ipcMain.on('showItemInFolder', (e, ...args) => { ipc.on('showItemInFolder', (e, ...args) => {
const shell = require('./shell') const shell = require('./shell')
shell.showItemInFolder(...args) shell.showItemInFolder(...args)
}) })
ipcMain.on('moveItemToTrash', (e, ...args) => { ipc.on('moveItemToTrash', (e, ...args) => {
const shell = require('./shell') const shell = require('./shell')
shell.moveItemToTrash(...args) shell.moveItemToTrash(...args)
}) })
@@ -154,7 +158,7 @@ function init () {
* File handlers * File handlers
*/ */
ipcMain.on('setDefaultFileHandler', (e, flag) => { ipc.on('setDefaultFileHandler', (e, flag) => {
const handlers = require('./handlers') const handlers = require('./handlers')
if (flag) handlers.install() if (flag) handlers.install()
@@ -165,7 +169,7 @@ function init () {
* Auto start on login * Auto start on login
*/ */
ipcMain.on('setStartup', (e, flag) => { ipc.on('setStartup', (e, flag) => {
const startup = require('./startup') const startup = require('./startup')
if (flag) startup.install() if (flag) startup.install()
@@ -178,38 +182,36 @@ function init () {
const main = windows.main const main = windows.main
ipcMain.on('setAspectRatio', (e, ...args) => main.setAspectRatio(...args)) ipc.on('setAspectRatio', (e, ...args) => main.setAspectRatio(...args))
ipcMain.on('setBounds', (e, ...args) => main.setBounds(...args)) ipc.on('setBounds', (e, ...args) => main.setBounds(...args))
ipcMain.on('setProgress', (e, ...args) => main.setProgress(...args)) ipc.on('setProgress', (e, ...args) => main.setProgress(...args))
ipcMain.on('setTitle', (e, ...args) => main.setTitle(...args)) ipc.on('setTitle', (e, ...args) => main.setTitle(...args))
ipcMain.on('show', () => main.show()) ipc.on('show', () => main.show())
ipcMain.on('toggleFullScreen', (e, ...args) => main.toggleFullScreen(...args)) ipc.on('toggleFullScreen', (e, ...args) => main.toggleFullScreen(...args))
ipcMain.on('setAllowNav', (e, ...args) => menu.setAllowNav(...args)) ipc.on('setAllowNav', (e, ...args) => menu.setAllowNav(...args))
/** /**
* External Media Player * External Media Player
*/ */
ipcMain.on('checkForExternalPlayer', (e, path) => { ipc.on('checkForExternalPlayer', function (e, path) {
const externalPlayer = require('./external-player') const externalPlayer = require('./external-player')
externalPlayer.checkInstall(path, err => { externalPlayer.checkInstall(path, function (err) {
windows.main.send('checkForExternalPlayer', !err) windows.main.send('checkForExternalPlayer', !err)
}) })
}) })
ipcMain.on('openExternalPlayer', (e, ...args) => { ipc.on('openExternalPlayer', (e, ...args) => {
const externalPlayer = require('./external-player') const externalPlayer = require('./external-player')
const shortcuts = require('./shortcuts')
const thumbar = require('./thumbar') const thumbar = require('./thumbar')
menu.togglePlaybackControls(false) menu.togglePlaybackControls(false)
shortcuts.disable()
thumbar.disable() thumbar.disable()
externalPlayer.spawn(...args) externalPlayer.spawn(...args)
}) })
ipcMain.on('quitExternalPlayer', () => { ipc.on('quitExternalPlayer', () => {
const externalPlayer = require('./external-player') const externalPlayer = require('./external-player')
externalPlayer.kill() externalPlayer.kill()
}) })
@@ -218,12 +220,11 @@ function init () {
* Message passing * Message passing
*/ */
const oldEmit = ipcMain.emit const oldEmit = ipc.emit
ipcMain.emit = (name, e, ...args) => { ipc.emit = function (name, e, ...args) {
// Relay messages between the main window and the WebTorrent hidden window // Relay messages between the main window and the WebTorrent hidden window
if (name.startsWith('wt-') && !app.isQuitting) { if (name.startsWith('wt-') && !app.isQuitting) {
console.dir(e.sender.getTitle()) if (e.sender.browserWindowOptions.title === 'webtorrent-hidden-window') {
if (e.sender.getTitle() === 'WebTorrent Hidden Window') {
// Send message to main window // Send message to main window
windows.main.send(name, ...args) windows.main.send(name, ...args)
log('webtorrent: got %s', name) log('webtorrent: got %s', name)
@@ -234,8 +235,8 @@ function init () {
} else { } else {
// Queue message for webtorrent window, it hasn't finished loading yet // Queue message for webtorrent window, it hasn't finished loading yet
messageQueueMainToWebTorrent.push({ messageQueueMainToWebTorrent.push({
name, name: name,
args args: args
}) })
log('webtorrent: queueing %s', name) log('webtorrent: queueing %s', name)
} }
@@ -243,6 +244,6 @@ function init () {
} }
// Emit all other events normally // Emit all other events normally
oldEmit.call(ipcMain, name, e, ...args) oldEmit.call(ipc, name, e, ...args)
} }
} }

View File

@@ -8,9 +8,11 @@ module.exports.error = error
* where they can be viewed in Developer Tools. * where they can be viewed in Developer Tools.
*/ */
const { app } = require('electron') const electron = require('electron')
const windows = require('./windows') const windows = require('./windows')
const app = electron.app
function log (...args) { function log (...args) {
if (app.ipcReady) { if (app.ipcReady) {
windows.main.send('log', ...args) windows.main.send('log', ...args)

View File

@@ -8,7 +8,9 @@ module.exports = {
onToggleFullScreen onToggleFullScreen
} }
const { app, Menu } = require('electron') const electron = require('electron')
const app = electron.app
const config = require('../config') const config = require('../config')
const windows = require('./windows') const windows = require('./windows')
@@ -16,8 +18,8 @@ const windows = require('./windows')
let menu = null let menu = null
function init () { function init () {
menu = Menu.buildFromTemplate(getMenuTemplate()) menu = electron.Menu.buildFromTemplate(getMenuTemplate())
Menu.setApplicationMenu(menu) electron.Menu.setApplicationMenu(menu)
} }
function togglePlaybackControls (flag) { function togglePlaybackControls (flag) {
@@ -68,11 +70,12 @@ function onToggleFullScreen (flag) {
} }
function getMenuItem (label) { function getMenuItem (label) {
for (const menuItem of menu.items) { for (let i = 0; i < menu.items.length; i++) {
const submenuItem = menuItem.submenu.items.find(item => item.label === label) const menuItem = menu.items[i].submenu.items.find(function (item) {
if (submenuItem) return submenuItem return item.label === label
})
if (menuItem) return menuItem
} }
return {}
} }
function getMenuTemplate () { function getMenuTemplate () {
@@ -285,14 +288,6 @@ function getMenuTemplate () {
{ {
label: 'Resume All', label: 'Resume All',
click: () => windows.main.dispatch('resumeAllTorrents') click: () => windows.main.dispatch('resumeAllTorrents')
},
{
label: 'Remove All From List',
click: () => windows.main.dispatch('confirmDeleteAllTorrents', false)
},
{
label: 'Remove All Data Files',
click: () => windows.main.dispatch('confirmDeleteAllTorrents', true)
} }
] ]
}, },
@@ -362,7 +357,8 @@ function getMenuTemplate () {
type: 'separator' type: 'separator'
}, },
{ {
role: 'services' role: 'services',
submenu: []
}, },
{ {
type: 'separator' type: 'separator'

View File

@@ -3,7 +3,7 @@ module.exports = {
disable disable
} }
const { powerSaveBlocker } = require('electron') const electron = require('electron')
const log = require('./log') const log = require('./log')
let blockId = 0 let blockId = 0
@@ -13,11 +13,11 @@ let blockId = 0
* display. * display.
*/ */
function enable () { function enable () {
if (powerSaveBlocker.isStarted(blockId)) { if (electron.powerSaveBlocker.isStarted(blockId)) {
// If a power saver block already exists, do nothing. // If a power saver block already exists, do nothing.
return return
} }
blockId = powerSaveBlocker.start('prevent-display-sleep') blockId = electron.powerSaveBlocker.start('prevent-display-sleep')
log(`powerSaveBlocker.enable: ${blockId}`) log(`powerSaveBlocker.enable: ${blockId}`)
} }
@@ -25,10 +25,10 @@ function enable () {
* Stop blocking the system from entering low-power mode. * Stop blocking the system from entering low-power mode.
*/ */
function disable () { function disable () {
if (!powerSaveBlocker.isStarted(blockId)) { if (!electron.powerSaveBlocker.isStarted(blockId)) {
// If a power saver block does not exist, do nothing. // If a power saver block does not exist, do nothing.
return return
} }
powerSaveBlocker.stop(blockId) electron.powerSaveBlocker.stop(blockId)
log(`powerSaveBlocker.disable: ${blockId}`) log(`powerSaveBlocker.disable: ${blockId}`)
} }

View File

@@ -1,11 +1,11 @@
module.exports = { module.exports = {
openExternal, openExternal,
openPath, openItem,
showItemInFolder, showItemInFolder,
moveItemToTrash moveItemToTrash
} }
const { shell } = require('electron') const electron = require('electron')
const log = require('./log') const log = require('./log')
/** /**
@@ -13,16 +13,15 @@ const log = require('./log')
*/ */
function openExternal (url) { function openExternal (url) {
log(`openExternal: ${url}`) log(`openExternal: ${url}`)
shell.openExternal(url) electron.shell.openExternal(url)
} }
/** /**
* Open the given file in the desktops default manner. * Open the given file in the desktops default manner.
*/ */
function openItem (path) {
function openPath (path) { log(`openItem: ${path}`)
log(`openPath: ${path}`) electron.shell.openItem(path)
shell.openPath(path)
} }
/** /**
@@ -30,7 +29,7 @@ function openPath (path) {
*/ */
function showItemInFolder (path) { function showItemInFolder (path) {
log(`showItemInFolder: ${path}`) log(`showItemInFolder: ${path}`)
shell.showItemInFolder(path) electron.shell.showItemInFolder(path)
} }
/** /**
@@ -38,5 +37,5 @@ function showItemInFolder (path) {
*/ */
function moveItemToTrash (path) { function moveItemToTrash (path) {
log(`moveItemToTrash: ${path}`) log(`moveItemToTrash: ${path}`)
shell.trashItem(path) electron.shell.moveItemToTrash(path)
} }

View File

@@ -3,20 +3,20 @@ module.exports = {
enable enable
} }
const { globalShortcut } = require('electron') const electron = require('electron')
const windows = require('./windows') const windows = require('./windows')
function enable () { function enable () {
// Register play/pause media key, available on some keyboards. // Register play/pause media key, available on some keyboards.
globalShortcut.register( electron.globalShortcut.register(
'MediaPlayPause', 'MediaPlayPause',
() => windows.main.dispatch('playPause') () => windows.main.dispatch('playPause')
) )
globalShortcut.register( electron.globalShortcut.register(
'MediaNextTrack', 'MediaNextTrack',
() => windows.main.dispatch('nextTrack') () => windows.main.dispatch('nextTrack')
) )
globalShortcut.register( electron.globalShortcut.register(
'MediaPreviousTrack', 'MediaPreviousTrack',
() => windows.main.dispatch('previousTrack') () => windows.main.dispatch('previousTrack')
) )
@@ -24,7 +24,7 @@ function enable () {
function disable () { function disable () {
// Return the media key to the OS, so other apps can use it. // Return the media key to the OS, so other apps can use it.
globalShortcut.unregister('MediaPlayPause') electron.globalShortcut.unregister('MediaPlayPause')
globalShortcut.unregister('MediaNextTrack') electron.globalShortcut.unregister('MediaNextTrack')
globalShortcut.unregister('MediaPreviousTrack') electron.globalShortcut.unregister('MediaPreviousTrack')
} }

View File

@@ -2,39 +2,150 @@ module.exports = {
handleEvent handleEvent
} }
const { app } = require('electron') const cp = require('child_process')
const electron = require('electron')
const fs = require('fs')
const os = require('os')
const path = require('path') const path = require('path')
const spawn = require('child_process').spawn
const app = electron.app
const handlers = require('./handlers') const handlers = require('./handlers')
const EXE_NAME = path.basename(process.execPath) const EXE_NAME = path.basename(process.execPath)
const UPDATE_EXE = path.join(process.execPath, '..', '..', 'Update.exe') const UPDATE_EXE = path.join(process.execPath, '..', '..', 'Update.exe')
const run = (args, done) => {
spawn(UPDATE_EXE, args, { detached: true })
.on('close', done)
}
function handleEvent (cmd) { function handleEvent (cmd) {
if (cmd === '--squirrel-install' || cmd === '--squirrel-updated') { if (cmd === '--squirrel-install') {
run([`--createShortcut=${EXE_NAME}`], app.quit) // App was installed. Install desktop/start menu shortcuts.
createShortcuts(function () {
// Ensure user sees install splash screen so they realize that Setup.exe actually
// installed an application and isn't the application itself.
setTimeout(function () {
app.quit()
}, 3000)
})
return true
}
if (cmd === '--squirrel-updated') {
// App was updated. (Called on new version of app)
updateShortcuts(function () {
app.quit()
})
return true return true
} }
if (cmd === '--squirrel-uninstall') { if (cmd === '--squirrel-uninstall') {
// App was just uninstalled. Undo anything we did in the --squirrel-install and
// --squirrel-updated handlers
// Uninstall .torrent file and magnet link handlers // Uninstall .torrent file and magnet link handlers
handlers.uninstall() handlers.uninstall()
run([`--removeShortcut=${EXE_NAME}`], app.quit) // Remove desktop/start menu shortcuts.
// HACK: add a callback to handlers.uninstall() so we can remove this setTimeout
setTimeout(function () {
removeShortcuts(function () {
app.quit()
})
}, 1000)
return true return true
} }
if (cmd === '--squirrel-obsolete') { if (cmd === '--squirrel-obsolete') {
// App will be updated. (Called on outgoing version of app)
app.quit() app.quit()
return true return true
} }
if (cmd === '--squirrel-firstrun') {
// App is running for the first time. Do not quit, allow startup to continue.
return false
}
return false return false
} }
/**
* Spawn a command and invoke the callback when it completes with an error and
* the output from standard out.
*/
function spawn (command, args, cb) {
let stdout = ''
let error = null
let child = null
try {
child = cp.spawn(command, args)
} catch (err) {
// Spawn can throw an error
process.nextTick(function () {
cb(error, stdout)
})
return
}
child.stdout.on('data', function (data) {
stdout += data
})
child.on('error', function (processError) {
error = processError
})
child.on('close', function (code, signal) {
if (code !== 0 && !error) error = new Error('Command failed: #{signal || code}')
if (error) error.stdout = stdout
cb(error, stdout)
})
}
/**
* Spawn the Squirrel `Update.exe` command with the given arguments and invoke
* the callback when the command completes.
*/
function spawnUpdate (args, cb) {
spawn(UPDATE_EXE, args, cb)
}
/**
* Create desktop and start menu shortcuts using the Squirrel `Update.exe`
* command.
*/
function createShortcuts (cb) {
spawnUpdate(['--createShortcut', EXE_NAME], cb)
}
/**
* Update desktop and start menu shortcuts using the Squirrel `Update.exe`
* command.
*/
function updateShortcuts (cb) {
const homeDir = os.homedir()
if (homeDir) {
const desktopShortcutPath = path.join(homeDir, 'Desktop', 'WebTorrent.lnk')
// If the desktop shortcut was deleted by the user, then keep it deleted.
fs.access(desktopShortcutPath, function (err) {
const desktopShortcutExists = !err
createShortcuts(function () {
if (desktopShortcutExists) {
cb()
} else {
// Remove the unwanted desktop shortcut that was recreated
fs.unlink(desktopShortcutPath, cb)
}
})
})
} else {
createShortcuts(cb)
}
}
/**
* Remove desktop and start menu shortcuts using the Squirrel `Update.exe`
* command.
*/
function removeShortcuts (cb) {
spawnUpdate(['--removeShortcut', EXE_NAME], cb)
}

View File

@@ -4,7 +4,9 @@ module.exports = {
setWindowFocus setWindowFocus
} }
const { app, Tray, Menu } = require('electron') const electron = require('electron')
const app = electron.app
const config = require('../config') const config = require('../config')
const windows = require('./windows') const windows = require('./windows')
@@ -34,7 +36,7 @@ function setWindowFocus (flag) {
} }
function initLinux () { function initLinux () {
checkLinuxTraySupport(err => { checkLinuxTraySupport(function (err) {
if (!err) createTray() if (!err) createTray()
}) })
} }
@@ -44,20 +46,28 @@ function initWin32 () {
} }
/** /**
* Check for libappindicator support before creating tray icon. * Check for libappindicator1 support before creating tray icon
*/ */
function checkLinuxTraySupport (cb) { function checkLinuxTraySupport (cb) {
const cp = require('child_process') const cp = require('child_process')
// Check that libappindicator libraries are installed in system. // Check that we're on Ubuntu (or another debian system) and that we have
cp.exec('ldconfig -p | grep libappindicator', (err, stdout) => { // libappindicator1. If WebTorrent was installed from the deb file, we should
// always have it. If it was installed from the zip file, we might not.
cp.exec('dpkg --get-selections libappindicator1', function (err, stdout) {
if (err) return cb(err) if (err) return cb(err)
cb(null) // Unfortunately there's no cleaner way, as far as I can tell, to check
// whether a debian package is installed:
if (stdout.endsWith('\tinstall\n')) {
cb(null)
} else {
cb(new Error('debian package not installed'))
}
}) })
} }
function createTray () { function createTray () {
tray = new Tray(getIconPath()) tray = new electron.Tray(getIconPath())
// On Windows, left click opens the app, right click opens the context menu. // On Windows, left click opens the app, right click opens the context menu.
// On Linux, any click (left or right) opens the context menu. // On Linux, any click (left or right) opens the context menu.
@@ -68,7 +78,7 @@ function createTray () {
} }
function updateTrayMenu () { function updateTrayMenu () {
const contextMenu = Menu.buildFromTemplate(getMenuTemplate()) const contextMenu = electron.Menu.buildFromTemplate(getMenuTemplate())
tray.setContextMenu(contextMenu) tray.setContextMenu(contextMenu)
} }

View File

@@ -2,7 +2,7 @@ module.exports = {
init init
} }
const { autoUpdater } = require('electron') const electron = require('electron')
const get = require('simple-get') const get = require('simple-get')
const config = require('../config') const config = require('../config')
@@ -47,31 +47,31 @@ function onResponse (err, res, data) {
} }
function initDarwinWin32 () { function initDarwinWin32 () {
autoUpdater.on( electron.autoUpdater.on(
'error', 'error',
(err) => log.error(`Update error: ${err.message}`) (err) => log.error(`Update error: ${err.message}`)
) )
autoUpdater.on( electron.autoUpdater.on(
'checking-for-update', 'checking-for-update',
() => log('Checking for update') () => log('Checking for update')
) )
autoUpdater.on( electron.autoUpdater.on(
'update-available', 'update-available',
() => log('Update available') () => log('Update available')
) )
autoUpdater.on( electron.autoUpdater.on(
'update-not-available', 'update-not-available',
() => log('No update available') () => log('No update available')
) )
autoUpdater.on( electron.autoUpdater.on(
'update-downloaded', 'update-downloaded',
(e, notes, name, date, url) => log(`Update downloaded: ${name}: ${url}`) (e, notes, name, date, url) => log(`Update downloaded: ${name}: ${url}`)
) )
autoUpdater.setFeedURL({ url: AUTO_UPDATE_URL }) electron.autoUpdater.setFeedURL(AUTO_UPDATE_URL)
autoUpdater.checkForUpdates() electron.autoUpdater.checkForUpdates()
} }

View File

@@ -2,7 +2,9 @@ module.exports = {
init init
} }
const { app } = require('electron') const electron = require('electron')
const app = electron.app
/** /**
* Add a user task menu to the app icon on right-click. (Windows) * Add a user task menu to the app icon on right-click. (Windows)

View File

@@ -4,18 +4,18 @@ const about = module.exports = {
} }
const config = require('../../config') const config = require('../../config')
const { BrowserWindow } = require('electron') const electron = require('electron')
function init () { function init () {
if (about.win) { if (about.win) {
return about.win.show() return about.win.show()
} }
const win = about.win = new BrowserWindow({ const win = about.win = new electron.BrowserWindow({
backgroundColor: '#ECECEC', backgroundColor: '#ECECEC',
center: true, center: true,
fullscreen: false, fullscreen: false,
height: 250, height: 170,
icon: getIconPath(), icon: getIconPath(),
maximizable: false, maximizable: false,
minimizable: false, minimizable: false,
@@ -25,27 +25,21 @@ function init () {
title: 'About ' + config.APP_WINDOW_TITLE, title: 'About ' + config.APP_WINDOW_TITLE,
useContentSize: true, useContentSize: true,
webPreferences: { webPreferences: {
nodeIntegration: true, nodeIntegration: true
contextIsolation: false,
enableBlinkFeatures: 'AudioVideoTracks',
enableRemoteModule: true,
backgroundThrottling: false
}, },
width: 300 width: 300
}) })
require('@electron/remote/main').enable(win.webContents)
win.loadURL(config.WINDOW_ABOUT) win.loadURL(config.WINDOW_ABOUT)
win.once('ready-to-show', () => { // No menu on the About window
win.setMenu(null)
win.once('ready-to-show', function () {
win.show() win.show()
// No menu on the About window
// Hack: BrowserWindow removeMenu method not working on electron@7
// https://github.com/electron/electron/issues/21088
win.setMenuBarVisibility(false)
}) })
win.once('closed', () => { win.once('closed', function () {
about.win = null about.win = null
}) })
} }

View File

@@ -14,9 +14,11 @@ const main = module.exports = {
win: null win: null
} }
const { app, BrowserWindow, screen } = require('electron') const electron = require('electron')
const debounce = require('debounce') const debounce = require('debounce')
const app = electron.app
const config = require('../../config') const config = require('../../config')
const log = require('../log') const log = require('../log')
const menu = require('../menu') const menu = require('../menu')
@@ -28,8 +30,9 @@ function init (state, options) {
const initialBounds = Object.assign(config.WINDOW_INITIAL_BOUNDS, state.saved.bounds) const initialBounds = Object.assign(config.WINDOW_INITIAL_BOUNDS, state.saved.bounds)
const win = main.win = new BrowserWindow({ const win = main.win = new electron.BrowserWindow({
backgroundColor: '#282828', backgroundColor: '#282828',
backgroundThrottling: false, // do not throttle animations/timers when page is background
darkTheme: true, // Forces dark theme (GTK+3) darkTheme: true, // Forces dark theme (GTK+3)
height: initialBounds.height, height: initialBounds.height,
icon: getIconPath(), // Window icon (Windows, Linux) icon: getIconPath(), // Window icon (Windows, Linux)
@@ -41,16 +44,11 @@ function init (state, options) {
useContentSize: true, // Specify web page size without OS chrome useContentSize: true, // Specify web page size without OS chrome
width: initialBounds.width, width: initialBounds.width,
webPreferences: { webPreferences: {
nodeIntegration: true, nodeIntegration: true
contextIsolation: false,
enableBlinkFeatures: 'AudioVideoTracks',
enableRemoteModule: true,
backgroundThrottling: false
}, },
x: initialBounds.x, x: initialBounds.x,
y: initialBounds.y y: initialBounds.y
}) })
require('@electron/remote/main').enable(win.webContents)
win.loadURL(config.WINDOW_MAIN) win.loadURL(config.WINDOW_MAIN)
@@ -66,7 +64,7 @@ function init (state, options) {
menu.onToggleFullScreen(main.win.isFullScreen()) menu.onToggleFullScreen(main.win.isFullScreen())
}) })
win.webContents.on('will-navigate', (e) => { win.webContents.on('will-navigate', (e, url) => {
// Prevent drag-and-drop from navigating the Electron window, which can happen // Prevent drag-and-drop from navigating the Electron window, which can happen
// before our drag-and-drop handlers have been initialized. // before our drag-and-drop handlers have been initialized.
e.preventDefault() e.preventDefault()
@@ -152,7 +150,7 @@ function setBounds (bounds, maximize) {
log('setBounds: maximizing') log('setBounds: maximizing')
main.win.maximize() main.win.maximize()
} else if (maximize === false && main.win.isMaximized()) { } else if (maximize === false && main.win.isMaximized()) {
log('setBounds: minimizing') log('setBounds: unmaximizing')
main.win.unmaximize() main.win.unmaximize()
} }
@@ -162,7 +160,7 @@ function setBounds (bounds, maximize) {
log(`setBounds: setting bounds to ${JSON.stringify(bounds)}`) log(`setBounds: setting bounds to ${JSON.stringify(bounds)}`)
if (bounds.x === null && bounds.y === null) { if (bounds.x === null && bounds.y === null) {
// X and Y not specified? By default, center on current screen // X and Y not specified? By default, center on current screen
const scr = screen.getDisplayMatching(main.win.getBounds()) const scr = electron.screen.getDisplayMatching(main.win.getBounds())
bounds.x = Math.round(scr.bounds.x + (scr.bounds.width / 2) - (bounds.width / 2)) bounds.x = Math.round(scr.bounds.x + (scr.bounds.width / 2) - (bounds.width / 2))
bounds.y = Math.round(scr.bounds.y + (scr.bounds.height / 2) - (bounds.height / 2)) bounds.y = Math.round(scr.bounds.y + (scr.bounds.height / 2) - (bounds.height / 2))
log(`setBounds: centered to ${JSON.stringify(bounds)}`) log(`setBounds: centered to ${JSON.stringify(bounds)}`)

View File

@@ -6,13 +6,14 @@ const webtorrent = module.exports = {
win: null win: null
} }
const { app, BrowserWindow } = require('electron') const electron = require('electron')
const config = require('../../config') const config = require('../../config')
function init () { function init () {
const win = webtorrent.win = new BrowserWindow({ const win = webtorrent.win = new electron.BrowserWindow({
backgroundColor: '#1E1E1E', backgroundColor: '#1E1E1E',
backgroundThrottling: false, // do not throttle animations/timers when page is background
center: true, center: true,
fullscreen: false, fullscreen: false,
fullscreenable: false, fullscreenable: false,
@@ -25,21 +26,16 @@ function init () {
title: 'webtorrent-hidden-window', title: 'webtorrent-hidden-window',
useContentSize: true, useContentSize: true,
webPreferences: { webPreferences: {
nodeIntegration: true, nodeIntegration: true
contextIsolation: false,
enableBlinkFeatures: 'AudioVideoTracks',
enableRemoteModule: true,
backgroundThrottling: false
}, },
width: 150 width: 150
}) })
require('@electron/remote/main').enable(win.webContents)
win.loadURL(config.WINDOW_WEBTORRENT) win.loadURL(config.WINDOW_WEBTORRENT)
// Prevent killing the WebTorrent process // Prevent killing the WebTorrent process
win.on('close', e => { win.on('close', function (e) {
if (app.isQuitting) { if (electron.app.isQuitting) {
return return
} }
e.preventDefault() e.preventDefault()

View File

@@ -1,31 +0,0 @@
const React = require('react')
const ModalOKCancel = require('./modal-ok-cancel')
const { dispatch, dispatcher } = require('../lib/dispatcher')
module.exports = class DeleteAllTorrentsModal extends React.Component {
render () {
const { state: { modal: { deleteData } } } = this.props
const message = deleteData
? 'Are you sure you want to remove all the torrents from the list and delete the data files?'
: 'Are you sure you want to remove all the torrents from the list?'
const buttonText = deleteData ? 'REMOVE DATA' : 'REMOVE'
return (
<div>
<p><strong>{message}</strong></p>
<ModalOKCancel
cancelText='CANCEL'
onCancel={dispatcher('exitModal')}
okText={buttonText}
onOK={handleRemove}
/>
</div>
)
function handleRemove () {
dispatch('deleteAllTorrents', deleteData)
dispatch('exitModal')
}
}
}

View File

@@ -1,10 +1,12 @@
const path = require('path') const path = require('path')
const colors = require('material-ui/styles/colors') const colors = require('material-ui/styles/colors')
const remote = require('@electron/remote') const electron = require('electron')
const React = require('react') const React = require('react')
const PropTypes = require('prop-types') const PropTypes = require('prop-types')
const remote = electron.remote
const RaisedButton = require('material-ui/RaisedButton').default const RaisedButton = require('material-ui/RaisedButton').default
const TextField = require('material-ui/TextField').default const TextField = require('material-ui/TextField').default
@@ -34,9 +36,14 @@ class PathSelector extends React.Component {
properties: ['openFile', 'openDirectory'] properties: ['openFile', 'openDirectory']
}, this.props.dialog) }, this.props.dialog)
const filenames = remote.dialog.showOpenDialogSync(remote.getCurrentWindow(), opts) remote.dialog.showOpenDialog(
if (!Array.isArray(filenames)) return remote.getCurrentWindow(),
this.props.onChange && this.props.onChange(filenames[0]) opts,
(filenames) => {
if (!Array.isArray(filenames)) return
this.props.onChange && this.props.onChange(filenames[0])
}
)
} }
render () { render () {

View File

@@ -1,5 +1,5 @@
const React = require('react') const React = require('react')
const { shell } = require('electron') const electron = require('electron')
const ModalOKCancel = require('./modal-ok-cancel') const ModalOKCancel = require('./modal-ok-cancel')
const { dispatcher } = require('../lib/dispatcher') const { dispatcher } = require('../lib/dispatcher')
@@ -36,7 +36,7 @@ module.exports = class UnsupportedMediaModal extends React.Component {
} }
onInstall () { onInstall () {
shell.openExternal('https://www.videolan.org/vlc/') electron.shell.openExternal('http://www.videolan.org/vlc/')
// TODO: dcposch send a dispatch rather than modifying state directly // TODO: dcposch send a dispatch rather than modifying state directly
const state = this.props.state const state = this.props.state

View File

@@ -1,5 +1,5 @@
const React = require('react') const React = require('react')
const { shell } = require('electron') const electron = require('electron')
const ModalOKCancel = require('./modal-ok-cancel') const ModalOKCancel = require('./modal-ok-cancel')
const { dispatch } = require('../lib/dispatcher') const { dispatch } = require('../lib/dispatcher')
@@ -25,7 +25,7 @@ module.exports = class UpdateAvailableModal extends React.Component {
function handleShow () { function handleShow () {
// TODO: use the GitHub urls from config.js // TODO: use the GitHub urls from config.js
shell.openExternal('https://github.com/webtorrent/webtorrent-desktop/releases') electron.shell.openExternal('https://github.com/webtorrent/webtorrent-desktop/releases')
dispatch('exitModal') dispatch('exitModal')
} }

View File

@@ -1,17 +0,0 @@
const { dispatch } = require('../lib/dispatcher')
module.exports = class AudioTracksController {
constructor (state) {
this.state = state
}
selectAudioTrack (ix) {
this.state.playing.audioTracks.selectedIndex = ix
dispatch('skip', 0.2) // HACK: hardcoded seek value for smooth audio change
}
toggleAudioTracksMenu () {
const audioTracks = this.state.playing.audioTracks
audioTracks.showMenu = !audioTracks.showMenu
}
}

View File

@@ -23,10 +23,10 @@ module.exports = class MediaController {
telemetry.logPlayAttempt('error') telemetry.logPlayAttempt('error')
state.playing.location = 'error' state.playing.location = 'error'
ipcRenderer.send('checkForExternalPlayer', state.saved.prefs.externalPlayerPath) ipcRenderer.send('checkForExternalPlayer', state.saved.prefs.externalPlayerPath)
ipcRenderer.once('checkForExternalPlayer', (e, isInstalled) => { ipcRenderer.once('checkForExternalPlayer', function (e, isInstalled) {
state.modal = { state.modal = {
id: 'unsupported-media-modal', id: 'unsupported-media-modal',
error, error: error,
externalPlayerInstalled: isInstalled externalPlayerInstalled: isInstalled
} }
}) })
@@ -56,7 +56,7 @@ module.exports = class MediaController {
const state = this.state const state = this.state
state.playing.location = 'external' state.playing.location = 'external'
const onServerRunning = () => { const onServerRunning = function () {
state.playing.isReady = true state.playing.isReady = true
telemetry.logPlayAttempt('external') telemetry.logPlayAttempt('external')

View File

@@ -1,4 +1,4 @@
const { ipcRenderer } = require('electron') const electron = require('electron')
const path = require('path') const path = require('path')
const Cast = require('../lib/cast') const Cast = require('../lib/cast')
@@ -11,6 +11,8 @@ const TorrentSummary = require('../lib/torrent-summary')
const Playlist = require('../lib/playlist') const Playlist = require('../lib/playlist')
const State = require('../lib/state') const State = require('../lib/state')
const ipcRenderer = electron.ipcRenderer
// Controls playback of torrents and files within torrents // Controls playback of torrents and files within torrents
// both local (<video>,<audio>,external player) and remote (cast) // both local (<video>,<audio>,external player) and remote (cast)
module.exports = class PlaybackController { module.exports = class PlaybackController {
@@ -59,12 +61,12 @@ module.exports = class PlaybackController {
} }
// Open a file in OS default app. // Open a file in OS default app.
openPath (infoHash, index) { openItem (infoHash, index) {
const torrentSummary = TorrentSummary.getByKey(this.state, infoHash) const torrentSummary = TorrentSummary.getByKey(this.state, infoHash)
const filePath = path.join( const filePath = path.join(
torrentSummary.path, torrentSummary.path,
torrentSummary.files[index].path) torrentSummary.files[index].path)
ipcRenderer.send('openPath', filePath) ipcRenderer.send('openItem', filePath)
} }
// Toggle (play or pause) the currently playing media // Toggle (play or pause) the currently playing media
@@ -156,20 +158,6 @@ module.exports = class PlaybackController {
else this.state.playing.jumpToTime = time else this.state.playing.jumpToTime = time
} }
// Show video preview
preview (x) {
if (!Number.isFinite(x)) {
console.error('Tried to preview a non-finite position ' + x)
return console.trace()
}
this.state.playing.previewXCoord = x
}
// Hide video preview
clearPreview () {
this.state.playing.previewXCoord = null
}
// Change playback speed. 1 = faster, -1 = slower // Change playback speed. 1 = faster, -1 = slower
// Playback speed ranges from 16 (fast forward) to 1 (normal playback) // Playback speed ranges from 16 (fast forward) to 1 (normal playback)
// to 0.25 (quarter-speed playback), then goes to -0.25, -0.5, -1, -2, etc // to 0.25 (quarter-speed playback), then goes to -0.25, -0.5, -1, -2, etc
@@ -282,10 +270,8 @@ module.exports = class PlaybackController {
state.playing.infoHash = infoHash state.playing.infoHash = infoHash
state.playing.fileIndex = index state.playing.fileIndex = index
state.playing.fileName = fileSummary.name state.playing.fileName = fileSummary.name
state.playing.type = TorrentPlayer.isVideo(fileSummary) state.playing.type = TorrentPlayer.isVideo(fileSummary) ? 'video'
? 'video' : TorrentPlayer.isAudio(fileSummary) ? 'audio'
: TorrentPlayer.isAudio(fileSummary)
? 'audio'
: 'other' : 'other'
// pick up where we left off // pick up where we left off

View File

@@ -1,5 +1,5 @@
const { dispatch } = require('../lib/dispatcher') const { dispatch } = require('../lib/dispatcher')
const { ipcRenderer } = require('electron') const ipcRenderer = require('electron').ipcRenderer
// Controls the Preferences screen // Controls the Preferences screen
module.exports = class PrefsController { module.exports = class PrefsController {
@@ -13,7 +13,7 @@ module.exports = class PrefsController {
const state = this.state const state = this.state
state.location.go({ state.location.go({
url: 'preferences', url: 'preferences',
setup (cb) { setup: function (cb) {
// initialize preferences // initialize preferences
state.window.title = 'Preferences' state.window.title = 'Preferences'
ipcRenderer.send('setAllowNav', false) ipcRenderer.send('setAllowNav', false)

View File

@@ -1,8 +1,10 @@
const remote = require('@electron/remote') const electron = require('electron')
const fs = require('fs') const fs = require('fs')
const path = require('path') const path = require('path')
const parallel = require('run-parallel') const parallel = require('run-parallel')
const remote = electron.remote
const { dispatch } = require('../lib/dispatcher') const { dispatch } = require('../lib/dispatcher')
module.exports = class SubtitlesController { module.exports = class SubtitlesController {
@@ -11,13 +13,14 @@ module.exports = class SubtitlesController {
} }
openSubtitles () { openSubtitles () {
const filenames = remote.dialog.showOpenDialogSync({ remote.dialog.showOpenDialog({
title: 'Select a subtitles file.', title: 'Select a subtitles file.',
filters: [{ name: 'Subtitles', extensions: ['vtt', 'srt'] }], filters: [{ name: 'Subtitles', extensions: ['vtt', 'srt'] }],
properties: ['openFile'] properties: ['openFile']
}, (filenames) => {
if (!Array.isArray(filenames)) return
this.addSubtitles(filenames, true)
}) })
if (!Array.isArray(filenames)) return
this.addSubtitles(filenames, true)
} }
selectSubtitle (ix) { selectSubtitle (ix) {
@@ -37,11 +40,12 @@ module.exports = class SubtitlesController {
// Read the files concurrently, then add all resulting subtitle tracks // Read the files concurrently, then add all resulting subtitle tracks
const tasks = files.map((file) => (cb) => loadSubtitle(file, cb)) const tasks = files.map((file) => (cb) => loadSubtitle(file, cb))
parallel(tasks, (err, tracks) => { parallel(tasks, function (err, tracks) {
if (err) return dispatch('error', err) if (err) return dispatch('error', err)
// No dupes allowed for (let i = 0; i < tracks.length; i++) {
tracks.forEach((track, i) => { // No dupes allowed
const track = tracks[i]
let trackIndex = subtitles.tracks.findIndex((t) => let trackIndex = subtitles.tracks.findIndex((t) =>
track.filePath === t.filePath) track.filePath === t.filePath)
@@ -54,7 +58,7 @@ module.exports = class SubtitlesController {
if (autoSelect && (i === 0 || isSystemLanguage(track.language))) { if (autoSelect && (i === 0 || isSystemLanguage(track.language))) {
subtitles.selectedIndex = trackIndex subtitles.selectedIndex = trackIndex
} }
}) }
// Finally, make sure no two tracks have the same label // Finally, make sure no two tracks have the same label
relabelSubtitles(subtitles) relabelSubtitles(subtitles)
@@ -93,7 +97,7 @@ function loadSubtitle (file, cb) {
const vttStream = fs.createReadStream(filePath).pipe(srtToVtt()) const vttStream = fs.createReadStream(filePath).pipe(srtToVtt())
concat(vttStream, (err, buf) => { concat(vttStream, function (err, buf) {
if (err) return dispatch('error', 'Can\'t parse subtitles file.') if (err) return dispatch('error', 'Can\'t parse subtitles file.')
// Detect what language the subtitles are in // Detect what language the subtitles are in
@@ -106,7 +110,7 @@ function loadSubtitle (file, cb) {
buffer: 'data:text/vtt;base64,' + buf.toString('base64'), buffer: 'data:text/vtt;base64,' + buf.toString('base64'),
language: langDetected, language: langDetected,
label: langDetected, label: langDetected,
filePath filePath: filePath
} }
cb(null, track) cb(null, track)
@@ -126,7 +130,7 @@ function isSystemLanguage (language) {
// Labels each track by language, eg 'German', 'English', 'English 2', ... // Labels each track by language, eg 'German', 'English', 'English 2', ...
function relabelSubtitles (subtitles) { function relabelSubtitles (subtitles) {
const counts = {} const counts = {}
subtitles.tracks.forEach(track => { subtitles.tracks.forEach(function (track) {
const lang = track.language const lang = track.language
counts[lang] = (counts[lang] || 0) + 1 counts[lang] = (counts[lang] || 0) + 1
track.label = counts[lang] > 1 ? (lang + ' ' + counts[lang]) : lang track.label = counts[lang] > 1 ? (lang + ' ' + counts[lang]) : lang

View File

@@ -1,5 +1,5 @@
const path = require('path') const path = require('path')
const { ipcRenderer } = require('electron') const ipcRenderer = require('electron').ipcRenderer
const TorrentSummary = require('../lib/torrent-summary') const TorrentSummary = require('../lib/torrent-summary')
const sound = require('../lib/sound') const sound = require('../lib/sound')
@@ -25,7 +25,7 @@ module.exports = class TorrentController {
} }
torrentSummary = { torrentSummary = {
torrentKey, torrentKey: torrentKey,
status: 'new' status: 'new'
} }
torrents.unshift(torrentSummary) torrents.unshift(torrentSummary)
@@ -177,7 +177,7 @@ function showDoneNotification (torrent) {
silent: true silent: true
}) })
notif.onclick = () => { notif.onclick = function () {
ipcRenderer.send('show') ipcRenderer.send('show')
} }

View File

@@ -1,13 +1,14 @@
const fs = require('fs') const fs = require('fs')
const path = require('path') const path = require('path')
const { ipcRenderer, clipboard } = require('electron') const electron = require('electron')
const remote = require('@electron/remote')
const { dispatch } = require('../lib/dispatcher') const { dispatch } = require('../lib/dispatcher')
const { TorrentKeyNotFoundError } = require('../lib/errors') const { TorrentKeyNotFoundError } = require('../lib/errors')
const sound = require('../lib/sound') const sound = require('../lib/sound')
const TorrentSummary = require('../lib/torrent-summary') const TorrentSummary = require('../lib/torrent-summary')
const ipcRenderer = electron.ipcRenderer
const instantIoRegex = /^(https:\/\/)?instant\.io\/#/ const instantIoRegex = /^(https:\/\/)?instant\.io\/#/
// Controls the torrent list: creating, adding, deleting, & manipulating torrents // Controls the torrent list: creating, adding, deleting, & manipulating torrents
@@ -54,7 +55,7 @@ module.exports = class TorrentListController {
if (files.length === 0 || typeof files[0] !== 'string') { if (files.length === 0 || typeof files[0] !== 'string') {
this.state.location.go({ this.state.location.go({
url: 'create-torrent', url: 'create-torrent',
files, files: files,
setup: (cb) => { setup: (cb) => {
this.state.window.title = 'Create New Torrent' this.state.window.title = 'Create New Torrent'
cb(null) cb(null)
@@ -94,7 +95,7 @@ module.exports = class TorrentListController {
if (!fileOrFolder) return start() if (!fileOrFolder) return start()
// Existing torrent: check that the path is still there // Existing torrent: check that the path is still there
fs.stat(fileOrFolder, err => { fs.stat(fileOrFolder, function (err) {
if (err) { if (err) {
s.error = 'path-missing' s.error = 'path-missing'
dispatch('backToList') dispatch('backToList')
@@ -156,8 +157,10 @@ module.exports = class TorrentListController {
prioritizeTorrent (infoHash) { prioritizeTorrent (infoHash) {
this.state.saved.torrents this.state.saved.torrents
.filter(torrent => ['downloading', 'seeding'].includes(torrent.status)) // Active torrents only. .filter((torrent) => { // We're interested in active torrents only.
.forEach((torrent) => { // Pause all active torrents except the one that started playing. return (['downloading', 'seeding'].indexOf(torrent.status) !== -1)
})
.map((torrent) => { // Pause all active torrents except the one that started playing.
if (infoHash === torrent.infoHash) return if (infoHash === torrent.infoHash) return
// Pause torrent without playing sounds. // Pause torrent without playing sounds.
@@ -172,7 +175,7 @@ module.exports = class TorrentListController {
resumePausedTorrents () { resumePausedTorrents () {
console.log('Playback Priority: resuming paused torrents') console.log('Playback Priority: resuming paused torrents')
if (!this.state.saved.torrentsToResume || !this.state.saved.torrentsToResume.length) return if (!this.state.saved.torrentsToResume || !this.state.saved.torrentsToResume.length) return
this.state.saved.torrentsToResume.forEach((infoHash) => { this.state.saved.torrentsToResume.map((infoHash) => {
this.toggleTorrent(infoHash) this.toggleTorrent(infoHash)
}) })
@@ -198,43 +201,26 @@ module.exports = class TorrentListController {
} }
} }
confirmDeleteAllTorrents (deleteData) {
this.state.modal = {
id: 'delete-all-torrents-modal',
deleteData
}
}
// TODO: use torrentKey, not infoHash // TODO: use torrentKey, not infoHash
deleteTorrent (infoHash, deleteData) { deleteTorrent (infoHash, deleteData) {
ipcRenderer.send('wt-stop-torrenting', infoHash)
const index = this.state.saved.torrents.findIndex((x) => x.infoHash === infoHash) const index = this.state.saved.torrents.findIndex((x) => x.infoHash === infoHash)
if (index > -1) { if (index > -1) {
const summary = this.state.saved.torrents[index] const summary = this.state.saved.torrents[index]
deleteTorrentFile(summary, deleteData)
// remove torrent and poster file
deleteFile(TorrentSummary.getTorrentPath(summary))
deleteFile(TorrentSummary.getPosterPath(summary))
// optionally delete the torrent data
if (deleteData) moveItemToTrash(summary)
// remove torrent from saved list // remove torrent from saved list
this.state.saved.torrents.splice(index, 1) this.state.saved.torrents.splice(index, 1)
dispatch('stateSave') dispatch('stateSave')
// prevent user from going forward to a deleted torrent
this.state.location.clearForward('player')
sound.play('DELETE')
} else {
throw new TorrentKeyNotFoundError(infoHash)
} }
}
deleteAllTorrents (deleteData) {
// Go back to list before the current playing torrent is deleted
if (this.state.location.url() === 'player') {
dispatch('backToList')
}
this.state.saved.torrents.forEach((summary) => deleteTorrentFile(summary, deleteData))
this.state.saved.torrents = []
dispatch('stateSave')
// prevent user from going forward to a deleted torrent // prevent user from going forward to a deleted torrent
this.state.location.clearForward('player') this.state.location.clearForward('player')
@@ -251,88 +237,77 @@ module.exports = class TorrentListController {
openTorrentContextMenu (infoHash) { openTorrentContextMenu (infoHash) {
const torrentSummary = TorrentSummary.getByKey(this.state, infoHash) const torrentSummary = TorrentSummary.getByKey(this.state, infoHash)
const menu = new remote.Menu() const menu = new electron.remote.Menu()
menu.append(new remote.MenuItem({ menu.append(new electron.remote.MenuItem({
label: 'Remove From List', label: 'Remove From List',
click: () => dispatch('confirmDeleteTorrent', torrentSummary.infoHash, false) click: () => dispatch('confirmDeleteTorrent', torrentSummary.infoHash, false)
})) }))
menu.append(new remote.MenuItem({ menu.append(new electron.remote.MenuItem({
label: 'Remove Data File', label: 'Remove Data File',
click: () => dispatch('confirmDeleteTorrent', torrentSummary.infoHash, true) click: () => dispatch('confirmDeleteTorrent', torrentSummary.infoHash, true)
})) }))
menu.append(new remote.MenuItem({ menu.append(new electron.remote.MenuItem({
type: 'separator' type: 'separator'
})) }))
if (torrentSummary.files) { if (torrentSummary.files) {
menu.append(new remote.MenuItem({ menu.append(new electron.remote.MenuItem({
label: process.platform === 'darwin' ? 'Show in Finder' : 'Show in Folder', label: process.platform === 'darwin' ? 'Show in Finder' : 'Show in Folder',
click: () => showItemInFolder(torrentSummary) click: () => showItemInFolder(torrentSummary)
})) }))
menu.append(new remote.MenuItem({ menu.append(new electron.remote.MenuItem({
type: 'separator' type: 'separator'
})) }))
} }
menu.append(new remote.MenuItem({ menu.append(new electron.remote.MenuItem({
label: 'Copy Magnet Link to Clipboard', label: 'Copy Magnet Link to Clipboard',
click: () => clipboard.writeText(torrentSummary.magnetURI) click: () => electron.clipboard.writeText(torrentSummary.magnetURI)
})) }))
menu.append(new remote.MenuItem({ menu.append(new electron.remote.MenuItem({
label: 'Copy Instant.io Link to Clipboard', label: 'Copy Instant.io Link to Clipboard',
click: () => clipboard.writeText(`https://instant.io/#${torrentSummary.infoHash}`) click: () => electron.clipboard.writeText(`https://instant.io/#${torrentSummary.infoHash}`)
})) }))
menu.append(new remote.MenuItem({ menu.append(new electron.remote.MenuItem({
label: 'Save Torrent File As...', label: 'Save Torrent File As...',
click: () => dispatch('saveTorrentFileAs', torrentSummary.torrentKey), click: () => dispatch('saveTorrentFileAs', torrentSummary.torrentKey),
enabled: torrentSummary.torrentFileName != null enabled: torrentSummary.torrentFileName != null
})) }))
menu.append(new remote.MenuItem({ menu.popup(electron.remote.getCurrentWindow())
type: 'separator'
}))
const sortedByName = this.state.saved.prefs.sortByName
menu.append(new remote.MenuItem({
label: `${sortedByName ? '✓ ' : ''}Sort by Name`,
click: () => dispatch('updatePreferences', 'sortByName', !sortedByName)
}))
menu.popup({ window: remote.getCurrentWindow() })
} }
// Takes a torrentSummary or torrentKey // Takes a torrentSummary or torrentKey
// Shows a Save File dialog, then saves the .torrent file wherever the user requests // Shows a Save File dialog, then saves the .torrent file wherever the user requests
saveTorrentFileAs (torrentKey) { saveTorrentFileAs (torrentKey) {
const torrentSummary = TorrentSummary.getByKey(this.state, torrentKey) const torrentSummary = TorrentSummary.getByKey(this.state, torrentKey)
if (!torrentSummary) throw new TorrentKeyNotFoundError(torrentKey) if (!torrentSummary) throw new Error('Missing torrentKey: ' + torrentKey)
const downloadPath = this.state.saved.prefs.downloadPath const downloadPath = this.state.saved.prefs.downloadPath
const newFileName = path.parse(torrentSummary.name).name + '.torrent' const newFileName = path.parse(torrentSummary.name).name + '.torrent'
const win = remote.getCurrentWindow() const win = electron.remote.getCurrentWindow()
const opts = { const opts = {
title: 'Save Torrent File', title: 'Save Torrent File',
defaultPath: path.join(downloadPath, newFileName), defaultPath: path.join(downloadPath, newFileName),
filters: [ filters: [
{ name: 'Torrent Files', extensions: ['torrent'] }, { name: 'Torrent Files', extensions: ['torrent'] },
{ name: 'All Files', extensions: ['*'] } { name: 'All Files', extensions: ['*'] }
], ]
buttonLabel: 'Save'
} }
const savePath = remote.dialog.showSaveDialogSync(win, opts) electron.remote.dialog.showSaveDialog(win, opts, function (savePath) {
console.log('Saving torrent ' + torrentKey + ' to ' + savePath)
if (!savePath) return // They clicked Cancel if (!savePath) return // They clicked Cancel
console.log('Saving torrent ' + torrentKey + ' to ' + savePath) const torrentPath = TorrentSummary.getTorrentPath(torrentSummary)
const torrentPath = TorrentSummary.getTorrentPath(torrentSummary) fs.readFile(torrentPath, function (err, torrentFile) {
fs.readFile(torrentPath, (err, torrentFile) => {
if (err) return dispatch('error', err)
fs.writeFile(savePath, torrentFile, err => {
if (err) return dispatch('error', err) if (err) return dispatch('error', err)
fs.writeFile(savePath, torrentFile, function (err) {
if (err) return dispatch('error', err)
})
}) })
}) })
} }
@@ -344,11 +319,11 @@ function findFilesRecursive (paths, cb_) {
if (paths.length > 1) { if (paths.length > 1) {
let numComplete = 0 let numComplete = 0
const ret = [] const ret = []
paths.forEach(path => { paths.forEach(function (path) {
findFilesRecursive([path], fileObjs => { findFilesRecursive([path], function (fileObjs) {
ret.push(...fileObjs) ret.push(...fileObjs)
if (++numComplete === paths.length) { if (++numComplete === paths.length) {
ret.sort((a, b) => a.path < b.path ? -1 : Number(a.path > b.path)) ret.sort((a, b) => a.path < b.path ? -1 : a.path > b.path)
cb_(ret) cb_(ret)
} }
}) })
@@ -357,7 +332,7 @@ function findFilesRecursive (paths, cb_) {
} }
const fileOrFolder = paths[0] const fileOrFolder = paths[0]
fs.stat(fileOrFolder, (err, stat) => { fs.stat(fileOrFolder, function (err, stat) {
if (err) return dispatch('error', err) if (err) return dispatch('error', err)
// Files: return name, path, and size // Files: return name, path, and size
@@ -372,7 +347,7 @@ function findFilesRecursive (paths, cb_) {
// Folders: recurse, make a list of all the files // Folders: recurse, make a list of all the files
const folderPath = fileOrFolder const folderPath = fileOrFolder
fs.readdir(folderPath, (err, fileNames) => { fs.readdir(folderPath, function (err, fileNames) {
if (err) return dispatch('error', err) if (err) return dispatch('error', err)
const paths = fileNames.map((fileName) => path.join(folderPath, fileName)) const paths = fileNames.map((fileName) => path.join(folderPath, fileName))
findFilesRecursive(paths, cb_) findFilesRecursive(paths, cb_)
@@ -382,7 +357,7 @@ function findFilesRecursive (paths, cb_) {
function deleteFile (path) { function deleteFile (path) {
if (!path) return if (!path) return
fs.unlink(path, err => { fs.unlink(path, function (err) {
if (err) dispatch('error', err) if (err) dispatch('error', err)
}) })
} }
@@ -396,14 +371,3 @@ function moveItemToTrash (torrentSummary) {
function showItemInFolder (torrentSummary) { function showItemInFolder (torrentSummary) {
ipcRenderer.send('showItemInFolder', TorrentSummary.getFileOrFolder(torrentSummary)) ipcRenderer.send('showItemInFolder', TorrentSummary.getFileOrFolder(torrentSummary))
} }
function deleteTorrentFile (torrentSummary, deleteData) {
ipcRenderer.send('wt-stop-torrenting', torrentSummary.infoHash)
// remove torrent and poster file
deleteFile(TorrentSummary.getTorrentPath(torrentSummary))
deleteFile(TorrentSummary.getPosterPath(torrentSummary))
// optionally delete the torrent data
if (deleteData) moveItemToTrash(torrentSummary)
}

View File

@@ -13,7 +13,7 @@ module.exports = class UpdateController {
console.log('new version skipped by user: v' + version) console.log('new version skipped by user: v' + version)
return return
} }
this.state.modal = { id: 'update-available-modal', version } this.state.modal = { id: 'update-available-modal', version: version }
} }
// Don't show the modal again until the next version // Don't show the modal again until the next version

View File

@@ -54,17 +54,17 @@ function init (appState, callback) {
state.devices.airplay = airplayPlayer() state.devices.airplay = airplayPlayer()
// Listen for devices: Chromecast, DLNA and Airplay // Listen for devices: Chromecast, DLNA and Airplay
chromecasts.on('update', device => { chromecasts.on('update', function (device) {
// TODO: how do we tell if there are *no longer* any Chromecasts available? // TODO: how do we tell if there are *no longer* any Chromecasts available?
// From looking at the code, chromecasts.players only grows, never shrinks // From looking at the code, chromecasts.players only grows, never shrinks
state.devices.chromecast.addDevice(device) state.devices.chromecast.addDevice(device)
}) })
dlnacasts.on('update', device => { dlnacasts.on('update', function (device) {
state.devices.dlna.addDevice(device) state.devices.dlna.addDevice(device)
}) })
airplayer.on('update', device => { airplayer.on('update', function (device) {
state.devices.airplay.addDevice(device) state.devices.airplay.addDevice(device)
}) })
} }
@@ -116,7 +116,7 @@ function chromecastPlayer () {
} }
function addDevice (device) { function addDevice (device) {
device.on('error', err => { device.on('error', function (err) {
if (device !== ret.device) return if (device !== ret.device) return
state.playing.location = 'local' state.playing.location = 'local'
state.errors.push({ state.errors.push({
@@ -125,7 +125,7 @@ function chromecastPlayer () {
}) })
update() update()
}) })
device.on('disconnect', () => { device.on('disconnect', function () {
if (device !== ret.device) return if (device !== ret.device) return
state.playing.location = 'local' state.playing.location = 'local'
update() update()
@@ -145,7 +145,7 @@ function chromecastPlayer () {
'Transfer-Encoding': 'chunked' 'Transfer-Encoding': 'chunked'
}) })
res.end(Buffer.from(selectedSubtitle.buffer.substr(21), 'base64')) res.end(Buffer.from(selectedSubtitle.buffer.substr(21), 'base64'))
}).listen(0, () => { }).listen(0, function () {
const port = ret.subServer.address().port const port = ret.subServer.address().port
const subtitlesUrl = 'http://' + state.server.networkAddress + ':' + port + '/' const subtitlesUrl = 'http://' + state.server.networkAddress + ':' + port + '/'
callback(subtitlesUrl) callback(subtitlesUrl)
@@ -155,13 +155,13 @@ function chromecastPlayer () {
function open () { function open () {
const torrentSummary = state.saved.torrents.find((x) => x.infoHash === state.playing.infoHash) const torrentSummary = state.saved.torrents.find((x) => x.infoHash === state.playing.infoHash)
serveSubtitles(subtitlesUrl => { serveSubtitles(function (subtitlesUrl) {
ret.device.play(state.server.networkURL + '/' + state.playing.fileIndex, { ret.device.play(state.server.networkURL + '/' + state.playing.fileIndex, {
type: 'video/mp4', type: 'video/mp4',
title: config.APP_NAME + ' - ' + torrentSummary.name, title: config.APP_NAME + ' - ' + torrentSummary.name,
subtitles: subtitlesUrl ? [subtitlesUrl] : [], subtitles: subtitlesUrl ? [subtitlesUrl] : [],
autoSubtitles: !!subtitlesUrl autoSubtitles: !!subtitlesUrl
}, err => { }, function (err) {
if (err) { if (err) {
state.playing.location = 'local' state.playing.location = 'local'
state.errors.push({ state.errors.push({
@@ -221,7 +221,7 @@ function airplayPlayer () {
return ret return ret
function addDevice (player) { function addDevice (player) {
player.on('event', event => { player.on('event', function (event) {
switch (event.state) { switch (event.state) {
case 'loading': case 'loading':
break break
@@ -243,7 +243,7 @@ function airplayPlayer () {
} }
function open () { function open () {
ret.device.play(state.server.networkURL + '/' + state.playing.fileIndex, (err, res) => { ret.device.play(state.server.networkURL + '/' + state.playing.fileIndex, function (err, res) {
if (err) { if (err) {
state.playing.location = 'local' state.playing.location = 'local'
state.errors.push({ state.errors.push({
@@ -270,7 +270,7 @@ function airplayPlayer () {
} }
function status () { function status () {
ret.device.playbackInfo((err, res, status) => { ret.device.playbackInfo(function (err, res, status) {
if (err) { if (err) {
state.playing.location = 'local' state.playing.location = 'local'
state.errors.push({ state.errors.push({
@@ -317,7 +317,7 @@ function dlnaPlayer (player) {
} }
function addDevice (device) { function addDevice (device) {
device.on('error', err => { device.on('error', function (err) {
if (device !== ret.device) return if (device !== ret.device) return
state.playing.location = 'local' state.playing.location = 'local'
state.errors.push({ state.errors.push({
@@ -326,7 +326,7 @@ function dlnaPlayer (player) {
}) })
update() update()
}) })
device.on('disconnect', () => { device.on('disconnect', function () {
if (device !== ret.device) return if (device !== ret.device) return
state.playing.location = 'local' state.playing.location = 'local'
update() update()
@@ -339,7 +339,7 @@ function dlnaPlayer (player) {
type: 'video/mp4', type: 'video/mp4',
title: config.APP_NAME + ' - ' + torrentSummary.name, title: config.APP_NAME + ' - ' + torrentSummary.name,
seek: state.playing.currentTime > 10 ? state.playing.currentTime : 0 seek: state.playing.currentTime > 10 ? state.playing.currentTime : 0
}, err => { }, function (err) {
if (err) { if (err) {
state.playing.location = 'local' state.playing.location = 'local'
state.errors.push({ state.errors.push({
@@ -374,7 +374,7 @@ function dlnaPlayer (player) {
} }
function volume (volume, callback) { function volume (volume, callback) {
ret.device.volume(volume, err => { ret.device.volume(volume, function (err) {
// quick volume update // quick volume update
state.playing.volume = volume state.playing.volume = volume
callback(err) callback(err)
@@ -396,7 +396,7 @@ function handleStatus (err, status) {
// Start polling cast device state, whenever we're connected // Start polling cast device state, whenever we're connected
function startStatusInterval () { function startStatusInterval () {
statusInterval = setInterval(() => { statusInterval = setInterval(function () {
const player = getPlayer() const player = getPlayer()
if (player) player.status() if (player) player.status()
}, 1000) }, 1000)
@@ -454,7 +454,7 @@ function selectDevice (index) {
function stop () { function stop () {
const player = getPlayer() const player = getPlayer()
if (player) { if (player) {
player.stop(() => { player.stop(function () {
player.device = null player.device = null
stoppedCasting() stoppedCasting()
}) })
@@ -522,6 +522,6 @@ function setVolume (volume) {
if (player) player.volume(volume, castCallback) if (player) player.volume(volume, castCallback)
} }
function castCallback (...args) { function castCallback () {
console.log('%s callback: %o', state.playing.location, args) console.log('%s callback: %o', state.playing.location, arguments)
} }

View File

@@ -5,7 +5,7 @@ module.exports = {
} }
const dispatchers = {} const dispatchers = {}
let _dispatch = () => {} let _dispatch = function () {}
function setDispatch (dispatch) { function setDispatch (dispatch) {
_dispatch = dispatch _dispatch = dispatch
@@ -23,7 +23,7 @@ function dispatcher (...args) {
const str = JSON.stringify(args) const str = JSON.stringify(args)
let handler = dispatchers[str] let handler = dispatchers[str]
if (!handler) { if (!handler) {
handler = dispatchers[str] = e => { handler = dispatchers[str] = function (e) {
// Do not propagate click to elements below the button // Do not propagate click to elements below the button
e.stopPropagation() e.stopPropagation()

View File

@@ -1,11 +1,11 @@
const mediaExtensions = { const mediaExtensions = {
audio: [ audio: [
'.aac', '.aif', '.aiff', '.asf', '.dff', '.dsf', '.flac', '.m2a', '.aac', '.aif', '.aiff', '.asf', '.dff', '.dsf', '.flac', '.m2a',
'.m2a', '.m4a', '.mpc', '.m4b', '.mka', '.mp2', '.mp3', '.mpc', '.oga', '.m4a', '.m4b', '.mp2', '.mp3', '.mpc', '.oga', '.ogg', '.opus',
'.ogg', '.opus', '.spx', '.wma', '.wav', '.wv', '.wvp'], '.spx', '.wma', '.wav', '.wv', '.wvp'],
video: [ video: [
'.avi', '.mp4', '.m4v', '.webm', '.mov', '.mkv', '.mpg', '.mpeg', '.avi', '.mp4', '.m4v', '.webm', '.mov', '.mkv', '.mpg', '.mpeg',
'.ogv', '.webm', '.wmv', '.m2ts'], '.ogv', '.webm', '.wmv'],
image: ['.gif', '.jpg', '.jpeg', '.png'] image: ['.gif', '.jpg', '.jpeg', '.png']
} }

View File

@@ -29,30 +29,17 @@ function run (state) {
if (semver.lt(version, '0.17.0')) migrate_0_17_0(saved) if (semver.lt(version, '0.17.0')) migrate_0_17_0(saved)
if (semver.lt(version, '0.17.2')) migrate_0_17_2(saved) if (semver.lt(version, '0.17.2')) migrate_0_17_2(saved)
if (semver.lt(version, '0.21.0')) migrate_0_21_0(saved) if (semver.lt(version, '0.21.0')) migrate_0_21_0(saved)
if (semver.lt(version, '0.22.0')) migrate_0_22_0(saved) if (semver.lt(version, '0.21.1')) migrate_0_21_1(saved)
if (semver.lt(version, config.APP_VERSION)) {
installHandlers(state.saved)
}
// Config is now on the new version // Config is now on the new version
state.saved.version = config.APP_VERSION state.saved.version = config.APP_VERSION
} }
// Whenever the app is updated, re-install default handlers if the user has
// enabled them.
function installHandlers (saved) {
if (saved.prefs.isFileHandler) {
const ipcRenderer = require('electron').ipcRenderer
ipcRenderer.send('setDefaultFileHandler', true)
}
}
function migrate_0_7_0 (saved) { function migrate_0_7_0 (saved) {
const { copyFileSync } = require('fs') const { copyFileSync } = require('fs')
const path = require('path') const path = require('path')
saved.torrents.forEach(ts => { saved.torrents.forEach(function (ts) {
const infoHash = ts.infoHash const infoHash = ts.infoHash
// Replace torrentPath with torrentFileName // Replace torrentPath with torrentFileName
@@ -134,7 +121,7 @@ function migrate_0_12_0 (saved) {
'02767050e0be2fd4db9a2ad6c12416ac806ed6ed.torrent', '02767050e0be2fd4db9a2ad6c12416ac806ed6ed.torrent',
'3ba219a8634bf7bae3d848192b2da75ae995589d.torrent' '3ba219a8634bf7bae3d848192b2da75ae995589d.torrent'
] ]
saved.torrents.forEach(torrentSummary => { saved.torrents.forEach(function (torrentSummary) {
if (!defaultTorrentFiles.includes(torrentSummary.torrentFileName)) return if (!defaultTorrentFiles.includes(torrentSummary.torrentFileName)) return
const fileOrFolder = TorrentSummary.getFileOrFolder(torrentSummary) const fileOrFolder = TorrentSummary.getFileOrFolder(torrentSummary)
if (!fileOrFolder) return if (!fileOrFolder) return
@@ -148,16 +135,16 @@ function migrate_0_12_0 (saved) {
} }
function migrate_0_14_0 (saved) { function migrate_0_14_0 (saved) {
saved.torrents.forEach(ts => { saved.torrents.forEach(function (ts) {
delete ts.defaultPlayFileIndex delete ts.defaultPlayFileIndex
}) })
} }
function migrate_0_17_0 (saved) { function migrate_0_17_0 (saved) {
// Fix a sad, sad bug that resulted in 100MB+ config.json files // Fix a sad, sad bug that resulted in 100MB+ config.json files
saved.torrents.forEach(ts => { saved.torrents.forEach(function (ts) {
if (!ts.files) return if (!ts.files) return
ts.files.forEach(file => { ts.files.forEach(function (file) {
if (!file.audioInfo || !file.audioInfo.picture) return if (!file.audioInfo || !file.audioInfo.picture) return
// This contained a Buffer, which 30x'd in size when serialized to JSON // This contained a Buffer, which 30x'd in size when serialized to JSON
delete file.audioInfo.picture delete file.audioInfo.picture
@@ -179,7 +166,9 @@ function migrate_0_17_2 (saved) {
const OLD_HASH = '3ba219a8634bf7bae3d848192b2da75ae995589d' const OLD_HASH = '3ba219a8634bf7bae3d848192b2da75ae995589d'
const NEW_HASH = 'a88fda5954e89178c372716a6a78b8180ed4dad3' const NEW_HASH = 'a88fda5954e89178c372716a6a78b8180ed4dad3'
const ts = saved.torrents.find(ts => ts.infoHash === OLD_HASH) const ts = saved.torrents.find((ts) => {
return ts.infoHash === OLD_HASH
})
if (!ts) return // Wired CD torrent does not exist if (!ts) return // Wired CD torrent does not exist
@@ -227,7 +216,7 @@ function migrate_0_21_0 (saved) {
} }
} }
function migrate_0_22_0 (saved) { function migrate_0_21_1 (saved) {
if (saved.prefs.externalPlayerPath == null) { if (saved.prefs.externalPlayerPath == null) {
saved.prefs.externalPlayerPath = '' saved.prefs.externalPlayerPath = ''
} }

View File

@@ -12,13 +12,13 @@ const State = module.exports = Object.assign(new EventEmitter(), {
getDefaultPlayState, getDefaultPlayState,
load, load,
// state.save() calls are rate-limited. Use state.saveImmediate() to skip limit. // state.save() calls are rate-limited. Use state.saveImmediate() to skip limit.
save (...args) { save: function () {
// Perf optimization: Lazy-require debounce (and it's dependencies) // Perf optimization: Lazy-require debounce (and it's dependencies)
const debounce = require('debounce') const debounce = require('debounce')
// After first State.save() invokation, future calls go straight to the // After first State.save() invokation, future calls go straight to the
// debounced function // debounced function
State.save = debounce(saveImmediate, SAVE_DEBOUNCE_INTERVAL) State.save = debounce(saveImmediate, SAVE_DEBOUNCE_INTERVAL)
State.save(...args) State.save(...arguments)
}, },
saveImmediate saveImmediate
}) })
@@ -105,11 +105,6 @@ function getDefaultPlayState () {
selectedIndex: -1, /* current subtitle track */ selectedIndex: -1, /* current subtitle track */
showMenu: false /* popover menu, above the video */ showMenu: false /* popover menu, above the video */
}, },
audioTracks: {
tracks: [],
selectedIndex: 0, /* current audio track */
showMenu: false /* popover menu, above the video */
},
aspectRatio: 0 /* aspect ratio of the video */ aspectRatio: 0 /* aspect ratio of the video */
} }
} }
@@ -202,20 +197,19 @@ function shouldHidePlayerControls () {
this.playing.playbackRate === 1 this.playing.playbackRate === 1
} }
async function load (cb) { function load (cb) {
let saved = await appConfig.read() appConfig.read(function (err, saved) {
if (err || !saved.version) {
if (!saved || !saved.version) { console.log('Missing config file: Creating new one')
console.log('Missing config file: Creating new one') try {
try { saved = setupStateSaved()
saved = setupStateSaved() } catch (err) {
} catch (err) { onSavedState(err)
onSavedState(err) return
return }
} }
} onSavedState(null, saved)
})
onSavedState(null, saved)
function onSavedState (err, saved) { function onSavedState (err, saved) {
if (err) return cb(err) if (err) return cb(err)
@@ -233,7 +227,7 @@ async function load (cb) {
} }
// Write state.saved to the JSON state file // Write state.saved to the JSON state file
async function saveImmediate (state, cb) { function saveImmediate (state, cb) {
console.log('Saving state to ' + appConfig.filePath) console.log('Saving state to ' + appConfig.filePath)
// Clean up, so that we're not saving any pending state // Clean up, so that we're not saving any pending state
@@ -242,7 +236,7 @@ async function saveImmediate (state, cb) {
// reading the torrent file or file(s) to seed & don't have an infohash // reading the torrent file or file(s) to seed & don't have an infohash
copy.torrents = copy.torrents copy.torrents = copy.torrents
.filter((x) => x.infoHash) .filter((x) => x.infoHash)
.map(x => { .map(function (x) {
const torrent = {} const torrent = {}
for (const key in x) { for (const key in x) {
if (key === 'progress' || key === 'torrentKey') { if (key === 'progress' || key === 'torrentKey') {
@@ -256,10 +250,8 @@ async function saveImmediate (state, cb) {
return torrent return torrent
}) })
try { appConfig.write(copy, (err) => {
await appConfig.write(copy) if (err) console.error(err)
State.emit('stateSaved') else State.emit('stateSaved')
} catch (err) { })
console.error(err)
}
} }

View File

@@ -7,7 +7,7 @@ module.exports = {
logPlayAttempt logPlayAttempt
} }
const remote = require('@electron/remote') const electron = require('electron')
const config = require('../../config') const config = require('../../config')
@@ -50,7 +50,7 @@ function send (state) {
json: true json: true
} }
get.post(opts, (err, res) => { get.post(opts, function (err, res) {
if (err) return console.error('Error sending telemetry', err) if (err) return console.error('Error sending telemetry', err)
if (res.statusCode !== 200) { if (res.statusCode !== 200) {
return console.error(`Error sending telemetry, status code: ${res.statusCode}`) return console.error(`Error sending telemetry, status code: ${res.statusCode}`)
@@ -74,7 +74,7 @@ function reset () {
// Track screen resolution // Track screen resolution
function getScreenInfo () { function getScreenInfo () {
return remote.screen.getAllDisplays().map((screen) => ({ return electron.remote.screen.getAllDisplays().map((screen) => ({
width: screen.size.width, width: screen.size.width,
height: screen.size.height, height: screen.size.height,
scaleFactor: screen.scaleFactor scaleFactor: screen.scaleFactor
@@ -106,14 +106,15 @@ function getTorrentStats (state) {
} }
// First, count torrents & total file size // First, count torrents & total file size
for (const torrent of state.saved.torrents) { for (let i = 0; i < count; i++) {
const stat = byStatus[torrent.status] const t = state.saved.torrents[i]
if (!torrent || !torrent.files || !stat) continue const stat = byStatus[t.status]
if (!t || !t.files || !stat) continue
stat.count++ stat.count++
for (let j = 0; j < t.files.length; j++) {
for (const file of torrent.files) { const f = t.files[j]
if (!file || !file.length) continue if (!f || !f.length) continue
const fileSizeMB = file.length / (1 << 20) const fileSizeMB = f.length / (1 << 20)
sizeMB += fileSizeMB sizeMB += fileSizeMB
stat.sizeMB += fileSizeMB stat.sizeMB += fileSizeMB
} }
@@ -144,7 +145,7 @@ function roundPow2 (n) {
if (n <= 0) return 0 if (n <= 0) return 0
// Otherwise, return 1, 2, 4, 8, etc by rounding in log space // Otherwise, return 1, 2, 4, 8, etc by rounding in log space
const log2 = Math.log(n) / Math.log(2) const log2 = Math.log(n) / Math.log(2)
return 2 ** Math.round(log2) return Math.pow(2, Math.round(log2))
} }
// An uncaught error happened in the main process or in one of the windows // An uncaught error happened in the main process or in one of the windows

View File

@@ -1,20 +0,0 @@
module.exports = {
calculateEta
}
function calculateEta (missing, downloadSpeed) {
if (downloadSpeed === 0 || missing === 0) return
const rawEta = missing / downloadSpeed
const hours = Math.floor(rawEta / 3600) % 24
const minutes = Math.floor(rawEta / 60) % 60
const seconds = Math.floor(rawEta % 60)
// Only display hours and minutes if they are greater than 0 but always
// display minutes if hours is being displayed
const hoursStr = hours ? hours + ' h' : ''
const minutesStr = (hours || minutes) ? minutes + ' min' : ''
const secondsStr = seconds + ' s'
return `${hoursStr} ${minutesStr} ${secondsStr} remaining`
}

View File

@@ -9,14 +9,20 @@ const msgNoSuitablePoster = 'Cannot generate a poster from any files in the torr
function torrentPoster (torrent, cb) { function torrentPoster (torrent, cb) {
// First, try to use a poster image if available // First, try to use a poster image if available
const posterFile = torrent.files.filter(file => /^poster\.(jpg|png|gif)$/.test(file.name))[0] const posterFile = torrent.files.filter(function (file) {
return /^poster\.(jpg|png|gif)$/.test(file.name)
})[0]
if (posterFile) return extractPoster(posterFile, cb) if (posterFile) return extractPoster(posterFile, cb)
// 'score' each media type based on total size present in torrent // 'score' each media type based on total size present in torrent
const bestScore = ['audio', 'video', 'image'].map(mediaType => ({ const bestScore = ['audio', 'video', 'image'].map(mediaType => {
type: mediaType, return {
size: calculateDataLengthByExtension(torrent, mediaExtensions[mediaType]) type: mediaType,
})).sort((a, b) => b.size - a.size)[0] // sort descending on size size: calculateDataLengthByExtension(torrent, mediaExtensions[mediaType])
}
}).sort((a, b) => { // sort descending on size
return b.size - a.size
})[0]
if (bestScore.size === 0) { if (bestScore.size === 0) {
// Admit defeat, no video, audio or image had a significant presence // Admit defeat, no video, audio or image had a significant presence
@@ -45,7 +51,9 @@ function calculateDataLengthByExtension (torrent, extensions) {
if (files.length === 0) return 0 if (files.length === 0) return 0
return files return files
.map(file => file.length) .map(file => file.length)
.reduce((a, b) => a + b) .reduce((a, b) => {
return a + b
})
} }
/** /**
@@ -57,14 +65,16 @@ function calculateDataLengthByExtension (torrent, extensions) {
function getLargestFileByExtension (torrent, extensions) { function getLargestFileByExtension (torrent, extensions) {
const files = filterOnExtension(torrent, extensions) const files = filterOnExtension(torrent, extensions)
if (files.length === 0) return undefined if (files.length === 0) return undefined
return files.reduce((a, b) => a.length > b.length ? a : b) return files.reduce((a, b) => {
return a.length > b.length ? a : b
})
} }
/** /**
* Filter file on a list extension, can be used to find al image files * Filter file on a list extension, can be used to find al image files
* @param torrent Torrent to filter files from * @param torrent Torrent to filter files from
* @param extensions File extensions to filter on * @param extensions File extensions to filter on
* @returns {Array} Array of torrent file objects matching one of the given extensions * @returns {number} Array of torrent file objects matching one of the given extensions
*/ */
function filterOnExtension (torrent, extensions) { function filterOnExtension (torrent, extensions) {
return torrent.files.filter(file => { return torrent.files.filter(file => {
@@ -105,10 +115,12 @@ function torrentPosterFromAudio (torrent, cb) {
if (imageFiles.length === 0) return cb(new Error(msgNoSuitablePoster)) if (imageFiles.length === 0) return cb(new Error(msgNoSuitablePoster))
const bestCover = imageFiles.map(file => ({ const bestCover = imageFiles.map(file => {
file, return {
score: scoreAudioCoverFile(file) file: file,
})).reduce((a, b) => { score: scoreAudioCoverFile(file)
}
}).reduce((a, b) => {
if (a.score > b.score) { if (a.score > b.score) {
return a return a
} }
@@ -154,8 +166,7 @@ function torrentPosterFromVideo (torrent, cb) {
function onSeeked () { function onSeeked () {
video.removeEventListener('seeked', onSeeked) video.removeEventListener('seeked', onSeeked)
const frame = captureFrame(video) const buf = captureFrame(video)
const buf = frame && frame.image
// unload video element // unload video element
video.pause() video.pause()
@@ -178,5 +189,5 @@ function torrentPosterFromImage (torrent, cb) {
function extractPoster (file, cb) { function extractPoster (file, cb) {
const extname = path.extname(file.name) const extname = path.extname(file.name)
file.getBuffer((err, buf) => cb(err, buf, extname)) file.getBuffer((err, buf) => { return cb(err, buf, extname) })
} }

View File

@@ -14,6 +14,9 @@ Module.prototype.require = function (id) {
console.time('init') console.time('init')
const crashReporter = require('../crash-reporter')
crashReporter.init()
// Perf optimization: Start asynchronously read on config file before all the // Perf optimization: Start asynchronously read on config file before all the
// blocking require() calls below. // blocking require() calls below.
@@ -96,15 +99,13 @@ function onState (err, _state) {
const SubtitlesController = require('./controllers/subtitles-controller') const SubtitlesController = require('./controllers/subtitles-controller')
return new SubtitlesController(state) return new SubtitlesController(state)
}), }),
audioTracks: createGetter(() => {
const AudioTracksController = require('./controllers/audio-tracks-controller')
return new AudioTracksController(state)
}),
torrent: createGetter(() => { torrent: createGetter(() => {
const TorrentController = require('./controllers/torrent-controller') const TorrentController = require('./controllers/torrent-controller')
return new TorrentController(state) return new TorrentController(state)
}), }),
torrentList: createGetter(() => new TorrentListController(state)), torrentList: createGetter(() => {
return new TorrentListController(state)
}),
update: createGetter(() => { update: createGetter(() => {
const UpdateController = require('./controllers/update-controller') const UpdateController = require('./controllers/update-controller')
return new UpdateController(state) return new UpdateController(state)
@@ -128,10 +129,7 @@ function onState (err, _state) {
resumeTorrents() resumeTorrents()
// Initialize ReactDOM // Initialize ReactDOM
ReactDOM.render( app = ReactDOM.render(<App state={state} />, document.querySelector('#body'))
<App state={state} ref={elem => { app = elem }} />,
document.querySelector('#body')
)
// Calling update() updates the UI given the current state // Calling update() updates the UI given the current state
// Do this at least once a second to give every file in every torrentSummary // Do this at least once a second to give every file in every torrentSummary
@@ -153,11 +151,11 @@ function onState (err, _state) {
// Add YouTube style hotkey shortcuts // Add YouTube style hotkey shortcuts
window.addEventListener('keydown', onKeydown) window.addEventListener('keydown', onKeydown)
const debouncedFullscreenToggle = debounce(() => { const debouncedFullscreenToggle = debounce(function () {
dispatch('toggleFullScreen') dispatch('toggleFullScreen')
}, 1000, true) }, 1000, true)
document.addEventListener('wheel', event => { document.addEventListener('wheel', function (event) {
// ctrlKey detects pinch to zoom, http://crbug.com/289887 // ctrlKey detects pinch to zoom, http://crbug.com/289887
if (event.ctrlKey) { if (event.ctrlKey) {
event.preventDefault() event.preventDefault()
@@ -253,11 +251,6 @@ const dispatchHandlers = {
controllers.torrentList().confirmDeleteTorrent(infoHash, deleteData), controllers.torrentList().confirmDeleteTorrent(infoHash, deleteData),
deleteTorrent: (infoHash, deleteData) => deleteTorrent: (infoHash, deleteData) =>
controllers.torrentList().deleteTorrent(infoHash, deleteData), controllers.torrentList().deleteTorrent(infoHash, deleteData),
openTorrentListContextMenu: () => onPaste(),
confirmDeleteAllTorrents: (deleteData) =>
controllers.torrentList().confirmDeleteAllTorrents(deleteData),
deleteAllTorrents: (deleteData) =>
controllers.torrentList().deleteAllTorrents(deleteData),
toggleSelectTorrent: (infoHash) => toggleSelectTorrent: (infoHash) =>
controllers.torrentList().toggleSelectTorrent(infoHash), controllers.torrentList().toggleSelectTorrent(infoHash),
openTorrentContextMenu: (infoHash) => openTorrentContextMenu: (infoHash) =>
@@ -276,12 +269,10 @@ const dispatchHandlers = {
previousTrack: () => controllers.playback().previousTrack(), previousTrack: () => controllers.playback().previousTrack(),
skip: (time) => controllers.playback().skip(time), skip: (time) => controllers.playback().skip(time),
skipTo: (time) => controllers.playback().skipTo(time), skipTo: (time) => controllers.playback().skipTo(time),
preview: (x) => controllers.playback().preview(x),
clearPreview: () => controllers.playback().clearPreview(),
changePlaybackRate: (dir) => controllers.playback().changePlaybackRate(dir), changePlaybackRate: (dir) => controllers.playback().changePlaybackRate(dir),
changeVolume: (delta) => controllers.playback().changeVolume(delta), changeVolume: (delta) => controllers.playback().changeVolume(delta),
setVolume: (vol) => controllers.playback().setVolume(vol), setVolume: (vol) => controllers.playback().setVolume(vol),
openPath: (infoHash, index) => controllers.playback().openPath(infoHash, index), openItem: (infoHash, index) => controllers.playback().openItem(infoHash, index),
// Subtitles // Subtitles
openSubtitles: () => controllers.subtitles().openSubtitles(), openSubtitles: () => controllers.subtitles().openSubtitles(),
@@ -290,10 +281,6 @@ const dispatchHandlers = {
checkForSubtitles: () => controllers.subtitles().checkForSubtitles(), checkForSubtitles: () => controllers.subtitles().checkForSubtitles(),
addSubtitles: (files, autoSelect) => controllers.subtitles().addSubtitles(files, autoSelect), addSubtitles: (files, autoSelect) => controllers.subtitles().addSubtitles(files, autoSelect),
// Audio Tracks
selectAudioTrack: (index) => controllers.audioTracks().selectAudioTrack(index),
toggleAudioTracksMenu: () => controllers.audioTracks().toggleAudioTracksMenu(),
// Local media: <video>, <audio>, external players // Local media: <video>, <audio>, external players
mediaStalled: () => controllers.media().mediaStalled(), mediaStalled: () => controllers.media().mediaStalled(),
mediaError: (err) => controllers.media().mediaError(err), mediaError: (err) => controllers.media().mediaError(err),
@@ -313,7 +300,7 @@ const dispatchHandlers = {
// Preferences screen // Preferences screen
preferences: () => controllers.prefs().show(), preferences: () => controllers.prefs().show(),
updatePreferences: (key, value) => controllers.prefs().update(key, value), updatePreferences: (key, value) => controllers.prefs().update(key, value),
checkDownloadPath, checkDownloadPath: checkDownloadPath,
startFolderWatcher: () => controllers.folderWatcher().start(), startFolderWatcher: () => controllers.folderWatcher().start(),
stopFolderWatcher: () => controllers.folderWatcher().stop(), stopFolderWatcher: () => controllers.folderWatcher().stop(),
@@ -323,20 +310,20 @@ const dispatchHandlers = {
// Navigation between screens (back, forward, ESC, etc) // Navigation between screens (back, forward, ESC, etc)
exitModal: () => { state.modal = null }, exitModal: () => { state.modal = null },
backToList, backToList: backToList,
escapeBack, escapeBack: escapeBack,
back: () => state.location.back(), back: () => state.location.back(),
forward: () => state.location.forward(), forward: () => state.location.forward(),
cancel: () => state.location.cancel(), cancel: () => state.location.cancel(),
// Controlling the window // Controlling the window
setDimensions, setDimensions: setDimensions,
toggleFullScreen: (setTo) => ipcRenderer.send('toggleFullScreen', setTo), toggleFullScreen: (setTo) => ipcRenderer.send('toggleFullScreen', setTo),
setTitle: (title) => { state.window.title = title }, setTitle: (title) => { state.window.title = title },
resetTitle: () => { state.window.title = config.APP_WINDOW_TITLE }, resetTitle: () => { state.window.title = config.APP_WINDOW_TITLE },
// Everything else // Everything else
onOpen, onOpen: onOpen,
error: onError, error: onError,
uncaughtError: (proc, err) => telemetry.logUncaughtError(proc, err), uncaughtError: (proc, err) => telemetry.logUncaughtError(proc, err),
stateSave: () => State.save(state), stateSave: () => State.save(state),
@@ -398,7 +385,7 @@ function setupIpc () {
function backToList () { function backToList () {
// Exit any modals and screens with a back button // Exit any modals and screens with a back button
state.modal = null state.modal = null
state.location.backToFirst(() => { state.location.backToFirst(function () {
// If we were already on the torrent list, scroll to the top // If we were already on the torrent list, scroll to the top
const contentTag = document.querySelector('.content') const contentTag = document.querySelector('.content')
if (contentTag) contentTag.scrollTop = 0 if (contentTag) contentTag.scrollTop = 0
@@ -518,16 +505,13 @@ function onError (err) {
const editableHtmlTags = new Set(['input', 'textarea']) const editableHtmlTags = new Set(['input', 'textarea'])
function onPaste (e) { function onPaste (e) {
if (e && editableHtmlTags.has(e.target.tagName.toLowerCase())) return if (editableHtmlTags.has(e.target.tagName.toLowerCase())) return
controllers.torrentList().addTorrent(electron.clipboard.readText()) controllers.torrentList().addTorrent(electron.clipboard.readText())
update() update()
} }
function onKeydown (e) { function onKeydown (e) {
// prevent event fire on user input elements
if (editableHtmlTags.has(e.target.tagName.toLowerCase())) return
const key = e.key const key = e.key
if (key === 'ArrowLeft') { if (key === 'ArrowLeft') {
@@ -567,7 +551,7 @@ function onBlur () {
} }
function onVisibilityChange () { function onVisibilityChange () {
state.window.isVisible = !document.hidden state.window.isVisible = !document.webkitHidden
} }
function onFullscreenChanged (e, isFullScreen) { function onFullscreenChanged (e, isFullScreen) {
@@ -588,7 +572,7 @@ function onWindowBoundsChanged (e, newBounds) {
} }
function checkDownloadPath () { function checkDownloadPath () {
fs.stat(state.saved.prefs.downloadPath, (err, stat) => { fs.stat(state.saved.prefs.downloadPath, function (err, stat) {
if (err) { if (err) {
state.downloadPathStatus = 'missing' state.downloadPathStatus = 'missing'
return console.error(err) return console.error(err)

View File

@@ -24,9 +24,7 @@ const Modals = {
), ),
'remove-torrent-modal': createGetter(() => require('../components/remove-torrent-modal')), 'remove-torrent-modal': createGetter(() => require('../components/remove-torrent-modal')),
'update-available-modal': createGetter(() => require('../components/update-available-modal')), 'update-available-modal': createGetter(() => require('../components/update-available-modal')),
'unsupported-media-modal': createGetter(() => require('../components/unsupported-media-modal')), 'unsupported-media-modal': createGetter(() => require('../components/unsupported-media-modal'))
'delete-all-torrents-modal':
createGetter(() => require('../components/delete-all-torrents-modal'))
} }
const fontFamily = process.platform === 'win32' const fontFamily = process.platform === 'win32'
@@ -86,7 +84,9 @@ class App extends React.Component {
const recentErrors = state.errors.filter((x) => now - x.time < 5000) const recentErrors = state.errors.filter((x) => now - x.time < 5000)
const hasErrors = recentErrors.length > 0 const hasErrors = recentErrors.length > 0
const errorElems = recentErrors.map((error, i) => <div key={i} className='error'>{error.message}</div>) const errorElems = recentErrors.map(function (error, i) {
return (<div key={i} className='error'>{error.message}</div>)
})
return ( return (
<div <div
key='errors' key='errors'

View File

@@ -1,12 +1,11 @@
const React = require('react') const React = require('react')
const BitField = require('bitfield').default const Bitfield = require('bitfield')
const prettyBytes = require('prettier-bytes') const prettyBytes = require('prettier-bytes')
const TorrentSummary = require('../lib/torrent-summary') const TorrentSummary = require('../lib/torrent-summary')
const Playlist = require('../lib/playlist') const Playlist = require('../lib/playlist')
const { dispatch, dispatcher } = require('../lib/dispatcher') const { dispatch, dispatcher } = require('../lib/dispatcher')
const config = require('../../config') const config = require('../../config')
const { calculateEta } = require('../lib/time')
// Shows a streaming video player. Standard features + Chromecast + Airplay // Shows a streaming video player. Standard features + Chromecast + Airplay
module.exports = class Player extends React.Component { module.exports = class Player extends React.Component {
@@ -55,6 +54,10 @@ function renderMedia (state) {
mediaElement.pause() mediaElement.pause()
} else if (!state.playing.isPaused && mediaElement.paused) { } else if (!state.playing.isPaused && mediaElement.paused) {
mediaElement.play() mediaElement.play()
.then(
() => dispatch('mediaSuccess'),
err => dispatch('mediaError', err.name === 'NotSupportedError' ? 'Codec unsupported' : `${err.name}: ${err.message}`)
)
} }
// When the user clicks or drags on the progress bar, jump to that position // When the user clicks or drags on the progress bar, jump to that position
if (state.playing.jumpToTime != null) { if (state.playing.jumpToTime != null) {
@@ -96,31 +99,25 @@ function renderMedia (state) {
delete file.selectedSubtitle delete file.selectedSubtitle
} }
// Switch to selected audio track
const audioTracks = mediaElement.audioTracks || []
for (let j = 0; j < audioTracks.length; j++) {
const isSelectedTrack = j === state.playing.audioTracks.selectedIndex
audioTracks[j].enabled = isSelectedTrack
}
state.playing.volume = mediaElement.volume state.playing.volume = mediaElement.volume
} }
// Add subtitles to the <video> tag // Add subtitles to the <video> tag
const trackTags = [] const trackTags = []
if (state.playing.subtitles.selectedIndex >= 0) { if (state.playing.subtitles.selectedIndex >= 0) {
state.playing.subtitles.tracks.forEach((track, i) => { for (let i = 0; i < state.playing.subtitles.tracks.length; i++) {
const track = state.playing.subtitles.tracks[i]
const isSelected = state.playing.subtitles.selectedIndex === i const isSelected = state.playing.subtitles.selectedIndex === i
trackTags.push( trackTags.push(
<track <track
key={i} key={i}
default={isSelected} default={isSelected ? 'default' : ''}
label={track.label} label={track.label}
kind='subtitles' type='subtitles'
src={track.buffer} src={track.buffer}
/> />
) )
}) }
} }
// Create the <audio> or <video> tag // Create the <audio> or <video> tag
@@ -129,11 +126,9 @@ function renderMedia (state) {
<MediaTagName <MediaTagName
src={Playlist.getCurrentLocalURL(state)} src={Playlist.getCurrentLocalURL(state)}
onDoubleClick={dispatcher('toggleFullScreen')} onDoubleClick={dispatcher('toggleFullScreen')}
onClick={dispatcher('playPause')}
onLoadedMetadata={onLoadedMetadata} onLoadedMetadata={onLoadedMetadata}
onEnded={onEnded} onEnded={onEnded}
onStalled={dispatcher('mediaStalled')} onStalled={dispatcher('mediaStalled')}
onError={dispatcher('mediaError')}
onTimeUpdate={dispatcher('mediaTimeUpdate')} onTimeUpdate={dispatcher('mediaTimeUpdate')}
onEncrypted={dispatcher('mediaEncrypted')} onEncrypted={dispatcher('mediaEncrypted')}
> >
@@ -153,53 +148,18 @@ function renderMedia (state) {
</div> </div>
) )
// As soon as we know the video dimensions, resize the window
function onLoadedMetadata (e) { function onLoadedMetadata (e) {
const mediaElement = e.target if (state.playing.type !== 'video') return
const video = e.target
// check if we can decode video and audio track const dimensions = {
if (state.playing.type === 'video') { width: video.videoWidth,
if (mediaElement.videoTracks.length === 0) { height: video.videoHeight
dispatch('mediaError', 'Video codec unsupported')
}
if (mediaElement.audioTracks.length === 0) {
dispatch('mediaError', 'Audio codec unsupported')
}
dispatch('mediaSuccess')
const dimensions = {
width: mediaElement.videoWidth,
height: mediaElement.videoHeight
}
// As soon as we know the video dimensions, resize the window
dispatch('setDimensions', dimensions)
// set audioTracks
const tracks = []
for (let i = 0; i < mediaElement.audioTracks.length; i++) {
tracks.push({
label: mediaElement.audioTracks[i].label || `Track ${i + 1}`,
language: mediaElement.audioTracks[i].language
})
}
state.playing.audioTracks.tracks = tracks
state.playing.audioTracks.selectedIndex = 0
}
// check if we can decode audio track
if (state.playing.type === 'audio') {
if (mediaElement.audioTracks.length === 0) {
dispatch('mediaError', 'Audio codec unsupported')
}
dispatch('mediaSuccess')
} }
dispatch('setDimensions', dimensions)
} }
function onEnded () { function onEnded (e) {
if (Playlist.hasNext(state)) { if (Playlist.hasNext(state)) {
dispatch('nextTrack') dispatch('nextTrack')
} else { } else {
@@ -341,13 +301,13 @@ function renderAudioMetadata (state) {
format.push(fileSummary.audioInfo.format.codec) format.push(fileSummary.audioInfo.format.codec)
} }
if (fileSummary.audioInfo.format.bitrate) { if (fileSummary.audioInfo.format.bitrate) {
format.push(Math.round(fileSummary.audioInfo.format.bitrate / 1000) + ' kbit/s') // 128 kbit/s format.push(Math.round(fileSummary.audioInfo.format.bitrate / 1000) + ' kbps') // 128 kbps
} }
if (fileSummary.audioInfo.format.sampleRate) { if (fileSummary.audioInfo.format.sampleRate) {
format.push(Math.round(fileSummary.audioInfo.format.sampleRate / 100) / 10 + ' kHz') format.push(Math.round(fileSummary.audioInfo.format.sampleRate / 100) / 10 + ' kHz')
} }
if (fileSummary.audioInfo.format.bitsPerSample) { if (fileSummary.audioInfo.format.bitsPerSample) {
format.push(fileSummary.audioInfo.format.bitsPerSample + '-bit') format.push(fileSummary.audioInfo.format.bitsPerSample + ' bit')
} }
if (format.length > 0) { if (format.length > 0) {
elems.push(( elems.push((
@@ -428,70 +388,22 @@ function renderCastScreen (state) {
const isStarting = state.playing.location.endsWith('-pending') const isStarting = state.playing.location.endsWith('-pending')
const castName = state.playing.castName const castName = state.playing.castName
const fileName = state.getPlayingFileSummary().name || ''
let castStatus let castStatus
if (isCast && isStarting) castStatus = 'Connecting to ' + castName + '...' if (isCast && isStarting) castStatus = 'Connecting to ' + castName + '...'
else if (isCast && !isStarting) castStatus = 'Connected to ' + castName else if (isCast && !isStarting) castStatus = 'Connected to ' + castName
else castStatus = '' else castStatus = ''
const prog = state.getPlayingTorrentSummary().progress || {}
// Show a nice title image, if possible // Show a nice title image, if possible
const style = { const style = {
backgroundImage: cssBackgroundImagePoster(state) backgroundImage: cssBackgroundImagePoster(state)
} }
function renderEta (total, downloaded) {
const missing = (total || 0) - (downloaded || 0)
const downloadSpeed = prog.downloadSpeed || 0
if (downloadSpeed === 0 || missing === 0) return
const etaStr = calculateEta(missing, downloadSpeed)
return (<span>{etaStr}</span>)
}
function renderDownloadProgress () {
if (!prog.files) return
const fileProg = prog.files[state.playing.fileIndex]
const fileProgress = fileProg.numPiecesPresent / fileProg.numPieces
const fileLength = state.getPlayingFileSummary().length
const fileDownloaded = fileProgress * fileLength
const progress = Math.round(100 * fileProgress)
const total = prettyBytes(fileLength)
const completed = prettyBytes(fileDownloaded)
const downloadSpeed = prettyBytes(prog.downloadSpeed || 0)
const uploadSpeed = prettyBytes(prog.uploadSpeed || 0)
let sizes
if (fileProgress < 1) {
sizes = <span> | {completed} / {total}</span>
} else {
sizes = <span> | {completed}</span>
}
return (
<div key='download-progress'>
<span className='progress'>{progress}% downloaded {sizes}</span>
<br />
<span>↓ {downloadSpeed}/s ↑ {uploadSpeed}/s | {prog.numPeers || 0} peer(s)</span>
<br />
{renderEta(fileLength, fileDownloaded)}
</div>
)
}
return ( return (
<div key='cast' className='letterbox' style={style}> <div key='cast' className='letterbox' style={style}>
<div className='cast-screen'> <div className='cast-screen'>
<i className='icon'>{castIcon}</i> <i className='icon'>{castIcon}</i>
<div key='type' className='cast-type'>{castType}</div> <div key='type' className='cast-type'>{castType}</div>
<div key='status' className='cast-status'>{castStatus}</div> <div key='status' className='cast-status'>{castStatus}</div>
<div key='name' className='name'>{fileName}</div>
{renderDownloadProgress()}
</div> </div>
</div> </div>
) )
@@ -503,7 +415,7 @@ function renderCastOptions (state) {
const { location, devices } = state.devices.castMenu const { location, devices } = state.devices.castMenu
const player = state.devices[location] const player = state.devices[location]
const items = devices.map((device, ix) => { const items = devices.map(function (device, ix) {
const isSelected = player.device === device const isSelected = player.device === device
const name = device.name const name = device.name
return ( return (
@@ -526,7 +438,7 @@ function renderSubtitleOptions (state) {
const subtitles = state.playing.subtitles const subtitles = state.playing.subtitles
if (!subtitles.tracks.length || !subtitles.showMenu) return if (!subtitles.tracks.length || !subtitles.showMenu) return
const items = subtitles.tracks.map((track, ix) => { const items = subtitles.tracks.map(function (track, ix) {
const isSelected = state.playing.subtitles.selectedIndex === ix const isSelected = state.playing.subtitles.selectedIndex === ix
return ( return (
<li key={ix} onClick={dispatcher('selectSubtitle', ix)}> <li key={ix} onClick={dispatcher('selectSubtitle', ix)}>
@@ -549,27 +461,6 @@ function renderSubtitleOptions (state) {
) )
} }
function renderAudioTrackOptions (state) {
const audioTracks = state.playing.audioTracks
if (!audioTracks.tracks.length || !audioTracks.showMenu) return
const items = audioTracks.tracks.map((track, ix) => {
const isSelected = state.playing.audioTracks.selectedIndex === ix
return (
<li key={ix} onClick={dispatcher('selectAudioTrack', ix)}>
<i className='icon'>{'radio_button_' + (isSelected ? 'checked' : 'unchecked')}</i>
{track.label}
</li>
)
})
return (
<ul key='audio-track-options' className='options-list'>
{items}
</ul>
)
}
function renderPlayerControls (state) { function renderPlayerControls (state) {
const positionPercent = 100 * state.playing.currentTime / state.playing.duration const positionPercent = 100 * state.playing.currentTime / state.playing.duration
const playbackCursorStyle = { left: 'calc(' + positionPercent + '% - 3px)' } const playbackCursorStyle = { left: 'calc(' + positionPercent + '% - 3px)' }
@@ -578,15 +469,10 @@ function renderPlayerControls (state) {
: state.playing.subtitles.selectedIndex >= 0 : state.playing.subtitles.selectedIndex >= 0
? 'active' ? 'active'
: '' : ''
const multiAudioClass = state.playing.audioTracks.tracks.length > 1
? 'active'
: 'disabled'
const prevClass = Playlist.hasPrevious(state) ? '' : 'disabled' const prevClass = Playlist.hasPrevious(state) ? '' : 'disabled'
const nextClass = Playlist.hasNext(state) ? '' : 'disabled' const nextClass = Playlist.hasNext(state) ? '' : 'disabled'
const elements = [ const elements = [
renderPreview(state),
<div key='playback-bar' className='playback-bar'> <div key='playback-bar' className='playback-bar'>
{renderLoadingBar(state)} {renderLoadingBar(state)}
<div <div
@@ -598,8 +484,6 @@ function renderPlayerControls (state) {
key='scrub-bar' key='scrub-bar'
className='scrub-bar' className='scrub-bar'
draggable='true' draggable='true'
onMouseMove={handleScrubPreview}
onMouseOut={clearPreview}
onDragStart={handleDragStart} onDragStart={handleDragStart}
onClick={handleScrub} onClick={handleScrub}
onDrag={handleScrub} onDrag={handleScrub}
@@ -649,14 +533,6 @@ function renderPlayerControls (state) {
> >
closed_caption closed_caption
</i> </i>
), (
<i
key='audio-tracks'
className={'icon multi-audio float-right ' + multiAudioClass}
onClick={handleAudioTracks}
>
library_music
</i>
)) ))
} }
@@ -671,7 +547,7 @@ function renderPlayerControls (state) {
airplay: { true: 'airplay', false: 'airplay' }, airplay: { true: 'airplay', false: 'airplay' },
dlna: { true: 'tv', false: 'tv' } dlna: { true: 'tv', false: 'tv' }
} }
castTypes.forEach(castType => { castTypes.forEach(function (castType) {
// Do we show this button (eg. the Chromecast button) at all? // Do we show this button (eg. the Chromecast button) at all?
const isCasting = state.playing.location.startsWith(castType) const isCasting = state.playing.location.startsWith(castType)
const player = state.devices[castType] const player = state.devices[castType]
@@ -708,14 +584,10 @@ function renderPlayerControls (state) {
// Render volume slider // Render volume slider
const volume = state.playing.volume const volume = state.playing.volume
const volumeIcon = 'volume_' + ( const volumeIcon = 'volume_' + (
volume === 0 volume === 0 ? 'off'
? 'off' : volume < 0.3 ? 'mute'
: volume < 0.3 : volume < 0.6 ? 'down'
? 'mute' : 'up')
: volume < 0.6
? 'down'
: 'up'
)
const volumeStyle = { const volumeStyle = {
background: '-webkit-gradient(linear, left top, right top, ' + background: '-webkit-gradient(linear, left top, right top, ' +
'color-stop(' + (volume * 100) + '%, #eee), ' + 'color-stop(' + (volume * 100) + '%, #eee), ' +
@@ -758,31 +630,26 @@ function renderPlayerControls (state) {
)) ))
} }
const emptyImage = new window.Image(0, 0) return (
emptyImage.src = 'data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs%3D' <div
key='controls' className='controls'
onMouseEnter={dispatcher('mediaControlsMouseEnter')}
onMouseLeave={dispatcher('mediaControlsMouseLeave')}
>
{elements}
{renderCastOptions(state)}
{renderSubtitleOptions(state)}
</div>
)
function handleDragStart (e) { function handleDragStart (e) {
// Prevent the cursor from changing, eg to a green + icon on Mac
if (e.dataTransfer) { if (e.dataTransfer) {
const dt = e.dataTransfer const dt = e.dataTransfer
// Prevent the cursor from changing, eg to a green + icon on Mac
dt.effectAllowed = 'none' dt.effectAllowed = 'none'
// Prevent ghost image
dt.setDragImage(emptyImage, 0, 0)
} }
} }
// Handles a scrub hover (preview another position in the video)
function handleScrubPreview (e) {
// Only show for videos
if (!e.clientX || state.playing.type !== 'video') return
dispatch('mediaMouseMoved')
dispatch('preview', e.clientX)
}
function clearPreview () {
if (state.playing.type !== 'video') return
dispatch('clearPreview')
}
// Handles a click or drag to scrub (jump to another position in the video) // Handles a click or drag to scrub (jump to another position in the video)
function handleScrub (e) { function handleScrub (e) {
if (!e.clientX) return if (!e.clientX) return
@@ -794,7 +661,7 @@ function renderPlayerControls (state) {
} }
// Handles volume muting and Unmuting // Handles volume muting and Unmuting
function handleVolumeMute () { function handleVolumeMute (e) {
if (state.playing.volume === 0.0) { if (state.playing.volume === 0.0) {
dispatch('setVolume', 1.0) dispatch('setVolume', 1.0)
} else { } else {
@@ -815,73 +682,6 @@ function renderPlayerControls (state) {
dispatch('toggleSubtitlesMenu') dispatch('toggleSubtitlesMenu')
} }
} }
function handleAudioTracks () {
dispatch('toggleAudioTracksMenu')
}
return (
<div
key='controls' className='controls'
onMouseEnter={dispatcher('mediaControlsMouseEnter')}
onMouseLeave={dispatcher('mediaControlsMouseLeave')}
>
{elements}
{renderCastOptions(state)}
{renderSubtitleOptions(state)}
{renderAudioTrackOptions(state)}
</div>
)
}
function renderPreview (state) {
const { previewXCoord = null } = state.playing
// Calculate time from x-coord as fraction of track width
const windowWidth = document.querySelector('body').clientWidth
const fraction = previewXCoord / windowWidth
const time = fraction * state.playing.duration /* seconds */
const height = 70
let width = 0
const previewEl = document.querySelector('video#preview')
if (previewEl !== null && previewXCoord !== null) {
previewEl.currentTime = time
// Auto adjust width to maintain video aspect ratio
width = Math.floor((previewEl.videoWidth / previewEl.videoHeight) * height)
}
// Center preview window on mouse cursor,
// while avoiding falling off the left or right edges
const xPos = Math.min(Math.max(previewXCoord - (width / 2), 5), windowWidth - width - 5)
return (
<div
key='preview' style={{
position: 'absolute',
bottom: 50,
left: xPos,
display: previewXCoord == null && 'none' // Hide preview when XCoord unset
}}
>
<div style={{ width, height, backgroundColor: 'black' }}>
<video
src={Playlist.getCurrentLocalURL(state)}
id='preview'
style={{ border: '1px solid lightgrey', borderRadius: 2 }}
/>
</div>
<p
style={{
textAlign: 'center', margin: 5, textShadow: '0 0 2px rgba(0,0,0,.5)', color: '#eee'
}}
>
{formatTime(time, state.playing.duration)}
</p>
</div>
)
} }
// Renders the loading bar. Shows which parts of the torrent are loaded, which // Renders the loading bar. Shows which parts of the torrent are loaded, which
@@ -903,7 +703,7 @@ function renderLoadingBar (state) {
const parts = [] const parts = []
let lastPiecePresent = false let lastPiecePresent = false
for (let i = fileProg.startPiece; i <= fileProg.endPiece; i++) { for (let i = fileProg.startPiece; i <= fileProg.endPiece; i++) {
const partPresent = BitField.prototype.get.call(prog.bitfield, i) const partPresent = Bitfield.prototype.get.call(prog.bitfield, i)
if (partPresent && !lastPiecePresent) { if (partPresent && !lastPiecePresent) {
parts.push({ start: i - fileProg.startPiece, count: 1 }) parts.push({ start: i - fileProg.startPiece, count: 1 })
} else if (partPresent) { } else if (partPresent) {
@@ -913,7 +713,7 @@ function renderLoadingBar (state) {
} }
// Output some bars to show which parts of the file are loaded // Output some bars to show which parts of the file are loaded
const loadingBarElems = parts.map((part, i) => { const loadingBarElems = parts.map(function (part, i) {
const style = { const style = {
left: (100 * part.start / fileProg.numPieces) + '%', left: (100 * part.start / fileProg.numPieces) + '%',
width: (100 * part.count / fileProg.numPieces) + '%' width: (100 * part.count / fileProg.numPieces) + '%'

View File

@@ -7,7 +7,6 @@ const LinearProgress = require('material-ui/LinearProgress').default
const TorrentSummary = require('../lib/torrent-summary') const TorrentSummary = require('../lib/torrent-summary')
const TorrentPlayer = require('../lib/torrent-player') const TorrentPlayer = require('../lib/torrent-player')
const { dispatcher } = require('../lib/dispatcher') const { dispatcher } = require('../lib/dispatcher')
const { calculateEta } = require('../lib/time')
module.exports = class TorrentList extends React.Component { module.exports = class TorrentList extends React.Component {
render () { render () {
@@ -36,11 +35,7 @@ module.exports = class TorrentList extends React.Component {
) )
return ( return (
<div <div key='torrent-list' className='torrent-list'>
key='torrent-list'
className='torrent-list'
onContextMenu={dispatcher('openTorrentListContextMenu')}
>
{contents} {contents}
</div> </div>
) )
@@ -196,9 +191,18 @@ module.exports = class TorrentList extends React.Component {
const downloadSpeed = prog.downloadSpeed const downloadSpeed = prog.downloadSpeed
if (downloadSpeed === 0 || missing === 0) return if (downloadSpeed === 0 || missing === 0) return
const etaStr = calculateEta(missing, downloadSpeed) const rawEta = missing / downloadSpeed
const hours = Math.floor(rawEta / 3600) % 24
const minutes = Math.floor(rawEta / 60) % 60
const seconds = Math.floor(rawEta % 60)
return (<span key='eta'>{etaStr}</span>) // Only display hours and minutes if they are greater than 0 but always
// display minutes if hours is being displayed
const hoursStr = hours ? hours + 'h' : ''
const minutesStr = (hours || minutes) ? minutes + 'm' : ''
const secondsStr = seconds + 's'
return (<span key='eta'>{hoursStr} {minutesStr} {secondsStr} remaining</span>)
} }
function renderTorrentStatus () { function renderTorrentStatus () {
@@ -280,17 +284,10 @@ module.exports = class TorrentList extends React.Component {
) )
} else { } else {
// We do know the files. List them and show download stats for each one // We do know the files. List them and show download stats for each one
const sortByName = this.props.state.saved.prefs.sortByName const fileRows = torrentSummary.files
const collator = new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' })
let fileRows = torrentSummary.files
.filter((file) => !file.path.includes('/.____padding_file/')) .filter((file) => !file.path.includes('/.____padding_file/'))
.map((file, index) => ({ file, index })) .map((file, index) => ({ file, index }))
.map((object) => this.renderFileRow(torrentSummary, object.file, object.index))
if (sortByName) {
fileRows = fileRows.sort((a, b) => collator.compare(a.file.name, b.file.name))
}
fileRows = fileRows.map((obj) => this.renderFileRow(torrentSummary, obj.file, obj.index))
filesElement = ( filesElement = (
<div key='files' className='files'> <div key='files' className='files'>
@@ -342,7 +339,7 @@ module.exports = class TorrentList extends React.Component {
} else { } else {
icon = 'description' /* file icon, opens in OS default app */ icon = 'description' /* file icon, opens in OS default app */
handleClick = isDone handleClick = isDone
? dispatcher('openPath', infoHash, index) ? dispatcher('openItem', infoHash, index)
: (e) => e.stopPropagation() // noop if file is not ready : (e) => e.stopPropagation() // noop if file is not ready
} }
// TODO: add a css 'disabled' class to indicate that a file cannot be opened/streamed // TODO: add a css 'disabled' class to indicate that a file cannot be opened/streamed

View File

@@ -3,21 +3,29 @@
console.time('init') console.time('init')
const crypto = require('crypto') const crypto = require('crypto')
const util = require('util') const deepEqual = require('deep-equal')
const defaultAnnounceList = require('create-torrent').announceList const defaultAnnounceList = require('create-torrent').announceList
const { ipcRenderer } = require('electron') const electron = require('electron')
const fs = require('fs') const fs = require('fs')
const mkdirp = require('mkdirp')
const mm = require('music-metadata') const mm = require('music-metadata')
const networkAddress = require('network-address') const networkAddress = require('network-address')
const path = require('path') const path = require('path')
const WebTorrent = require('webtorrent') const WebTorrent = require('webtorrent')
const crashReporter = require('../crash-reporter')
const config = require('../config') const config = require('../config')
const { TorrentKeyNotFoundError } = require('./lib/errors') const { TorrentKeyNotFoundError } = require('./lib/errors')
const torrentPoster = require('./lib/torrent-poster') const torrentPoster = require('./lib/torrent-poster')
// Report when the process crashes
crashReporter.init()
// Send & receive messages from the main window
const ipc = electron.ipcRenderer
// Force use of webtorrent trackers on all torrents // Force use of webtorrent trackers on all torrents
globalThis.WEBTORRENT_ANNOUNCE = defaultAnnounceList global.WEBTORRENT_ANNOUNCE = defaultAnnounceList
.map((arr) => arr[0]) .map((arr) => arr[0])
.filter((url) => url.indexOf('wss://') === 0 || url.indexOf('ws://') === 0) .filter((url) => url.indexOf('wss://') === 0 || url.indexOf('ws://') === 0)
@@ -65,29 +73,29 @@ init()
function init () { function init () {
listenToClientEvents() listenToClientEvents()
ipcRenderer.on('wt-start-torrenting', (e, torrentKey, torrentID, path, fileModtimes, selections) => ipc.on('wt-start-torrenting', (e, torrentKey, torrentID, path, fileModtimes, selections) =>
startTorrenting(torrentKey, torrentID, path, fileModtimes, selections)) startTorrenting(torrentKey, torrentID, path, fileModtimes, selections))
ipcRenderer.on('wt-stop-torrenting', (e, infoHash) => ipc.on('wt-stop-torrenting', (e, infoHash) =>
stopTorrenting(infoHash)) stopTorrenting(infoHash))
ipcRenderer.on('wt-create-torrent', (e, torrentKey, options) => ipc.on('wt-create-torrent', (e, torrentKey, options) =>
createTorrent(torrentKey, options)) createTorrent(torrentKey, options))
ipcRenderer.on('wt-save-torrent-file', (e, torrentKey) => ipc.on('wt-save-torrent-file', (e, torrentKey) =>
saveTorrentFile(torrentKey)) saveTorrentFile(torrentKey))
ipcRenderer.on('wt-generate-torrent-poster', (e, torrentKey) => ipc.on('wt-generate-torrent-poster', (e, torrentKey) =>
generateTorrentPoster(torrentKey)) generateTorrentPoster(torrentKey))
ipcRenderer.on('wt-get-audio-metadata', (e, infoHash, index) => ipc.on('wt-get-audio-metadata', (e, infoHash, index) =>
getAudioMetadata(infoHash, index)) getAudioMetadata(infoHash, index))
ipcRenderer.on('wt-start-server', (e, infoHash) => ipc.on('wt-start-server', (e, infoHash) =>
startServer(infoHash)) startServer(infoHash))
ipcRenderer.on('wt-stop-server', () => ipc.on('wt-stop-server', (e) =>
stopServer()) stopServer())
ipcRenderer.on('wt-select-files', (e, infoHash, selections) => ipc.on('wt-select-files', (e, infoHash, selections) =>
selectFiles(infoHash, selections)) selectFiles(infoHash, selections))
ipcRenderer.send('ipcReadyWebTorrent') ipc.send('ipcReadyWebTorrent')
window.addEventListener('error', (e) => window.addEventListener('error', (e) =>
ipcRenderer.send('wt-uncaught-error', { message: e.error.message, stack: e.error.stack }), ipc.send('wt-uncaught-error', { message: e.error.message, stack: e.error.stack }),
true) true)
setInterval(updateTorrentProgress, 1000) setInterval(updateTorrentProgress, 1000)
@@ -95,8 +103,8 @@ function init () {
} }
function listenToClientEvents () { function listenToClientEvents () {
client.on('warning', (err) => ipcRenderer.send('wt-warning', null, err.message)) client.on('warning', (err) => ipc.send('wt-warning', null, err.message))
client.on('error', (err) => ipcRenderer.send('wt-error', null, err.message)) client.on('error', (err) => ipc.send('wt-error', null, err.message))
} }
// Starts a given TorrentID, which can be an infohash, magnet URI, etc. // Starts a given TorrentID, which can be an infohash, magnet URI, etc.
@@ -105,8 +113,8 @@ function startTorrenting (torrentKey, torrentID, path, fileModtimes, selections)
console.log('starting torrent %s: %s', torrentKey, torrentID) console.log('starting torrent %s: %s', torrentKey, torrentID)
const torrent = client.add(torrentID, { const torrent = client.add(torrentID, {
path, path: path,
fileModtimes fileModtimes: fileModtimes
}) })
torrent.key = torrentKey torrent.key = torrentKey
@@ -130,44 +138,44 @@ function createTorrent (torrentKey, options) {
const torrent = client.seed(paths, options) const torrent = client.seed(paths, options)
torrent.key = torrentKey torrent.key = torrentKey
addTorrentEvents(torrent) addTorrentEvents(torrent)
ipcRenderer.send('wt-new-torrent') ipc.send('wt-new-torrent')
} }
function addTorrentEvents (torrent) { function addTorrentEvents (torrent) {
torrent.on('warning', (err) => torrent.on('warning', (err) =>
ipcRenderer.send('wt-warning', torrent.key, err.message)) ipc.send('wt-warning', torrent.key, err.message))
torrent.on('error', (err) => torrent.on('error', (err) =>
ipcRenderer.send('wt-error', torrent.key, err.message)) ipc.send('wt-error', torrent.key, err.message))
torrent.on('infoHash', () => torrent.on('infoHash', () =>
ipcRenderer.send('wt-parsed', torrent.key, torrent.infoHash, torrent.magnetURI)) ipc.send('wt-parsed', torrent.key, torrent.infoHash, torrent.magnetURI))
torrent.on('metadata', torrentMetadata) torrent.on('metadata', torrentMetadata)
torrent.on('ready', torrentReady) torrent.on('ready', torrentReady)
torrent.on('done', torrentDone) torrent.on('done', torrentDone)
function torrentMetadata () { function torrentMetadata () {
const info = getTorrentInfo(torrent) const info = getTorrentInfo(torrent)
ipcRenderer.send('wt-metadata', torrent.key, info) ipc.send('wt-metadata', torrent.key, info)
updateTorrentProgress() updateTorrentProgress()
} }
function torrentReady () { function torrentReady () {
const info = getTorrentInfo(torrent) const info = getTorrentInfo(torrent)
ipcRenderer.send('wt-ready', torrent.key, info) ipc.send('wt-ready', torrent.key, info)
ipcRenderer.send('wt-ready-' + torrent.infoHash, torrent.key, info) ipc.send('wt-ready-' + torrent.infoHash, torrent.key, info)
updateTorrentProgress() updateTorrentProgress()
} }
function torrentDone () { function torrentDone () {
const info = getTorrentInfo(torrent) const info = getTorrentInfo(torrent)
ipcRenderer.send('wt-done', torrent.key, info) ipc.send('wt-done', torrent.key, info)
updateTorrentProgress() updateTorrentProgress()
torrent.getFileModtimes((err, fileModtimes) => { torrent.getFileModtimes(function (err, fileModtimes) {
if (err) return onError(err) if (err) return onError(err)
ipcRenderer.send('wt-file-modtimes', torrent.key, fileModtimes) ipc.send('wt-file-modtimes', torrent.key, fileModtimes)
}) })
} }
} }
@@ -200,19 +208,19 @@ function saveTorrentFile (torrentKey) {
const torrent = getTorrent(torrentKey) const torrent = getTorrent(torrentKey)
const torrentPath = path.join(config.TORRENT_PATH, torrent.infoHash + '.torrent') const torrentPath = path.join(config.TORRENT_PATH, torrent.infoHash + '.torrent')
fs.access(torrentPath, fs.constants.R_OK, err => { fs.access(torrentPath, fs.constants.R_OK, function (err) {
const fileName = torrent.infoHash + '.torrent' const fileName = torrent.infoHash + '.torrent'
if (!err) { if (!err) {
// We've already saved the file // We've already saved the file
return ipcRenderer.send('wt-file-saved', torrentKey, fileName) return ipc.send('wt-file-saved', torrentKey, fileName)
} }
// Otherwise, save the .torrent file, under the app config folder // Otherwise, save the .torrent file, under the app config folder
fs.mkdir(config.TORRENT_PATH, { recursive: true }, _ => { mkdirp(config.TORRENT_PATH, function (_) {
fs.writeFile(torrentPath, torrent.torrentFile, err => { fs.writeFile(torrentPath, torrent.torrentFile, function (err) {
if (err) return console.log('error saving torrent file %s: %o', torrentPath, err) if (err) return console.log('error saving torrent file %s: %o', torrentPath, err)
console.log('saved torrent file %s', torrentPath) console.log('saved torrent file %s', torrentPath)
return ipcRenderer.send('wt-file-saved', torrentKey, fileName) return ipc.send('wt-file-saved', torrentKey, fileName)
}) })
}) })
}) })
@@ -222,17 +230,17 @@ function saveTorrentFile (torrentKey) {
// Auto chooses either a frame from a video file, an image, etc // Auto chooses either a frame from a video file, an image, etc
function generateTorrentPoster (torrentKey) { function generateTorrentPoster (torrentKey) {
const torrent = getTorrent(torrentKey) const torrent = getTorrent(torrentKey)
torrentPoster(torrent, (err, buf, extension) => { torrentPoster(torrent, function (err, buf, extension) {
if (err) return console.log('error generating poster: %o', err) if (err) return console.log('error generating poster: %o', err)
// save it for next time // save it for next time
fs.mkdir(config.POSTER_PATH, { recursive: true }, err => { mkdirp(config.POSTER_PATH, function (err) {
if (err) return console.log('error creating poster dir: %o', err) if (err) return console.log('error creating poster dir: %o', err)
const posterFileName = torrent.infoHash + extension const posterFileName = torrent.infoHash + extension
const posterFilePath = path.join(config.POSTER_PATH, posterFileName) const posterFilePath = path.join(config.POSTER_PATH, posterFileName)
fs.writeFile(posterFilePath, buf, err => { fs.writeFile(posterFilePath, buf, function (err) {
if (err) return console.log('error saving poster: %o', err) if (err) return console.log('error saving poster: %o', err)
// show the poster // show the poster
ipcRenderer.send('wt-poster', torrentKey, posterFileName) ipc.send('wt-poster', torrentKey, posterFileName)
}) })
}) })
}) })
@@ -241,23 +249,25 @@ function generateTorrentPoster (torrentKey) {
function updateTorrentProgress () { function updateTorrentProgress () {
const progress = getTorrentProgress() const progress = getTorrentProgress()
// TODO: diff torrent-by-torrent, not once for the whole update // TODO: diff torrent-by-torrent, not once for the whole update
if (prevProgress && util.isDeepStrictEqual(progress, prevProgress)) { if (prevProgress && deepEqual(progress, prevProgress, { strict: true })) {
return /* don't send heavy object if it hasn't changed */ return /* don't send heavy object if it hasn't changed */
} }
ipcRenderer.send('wt-progress', progress) ipc.send('wt-progress', progress)
prevProgress = progress prevProgress = progress
} }
function getTorrentProgress () { function getTorrentProgress () {
// First, track overall progress // First, track overall progress
const progress = client.progress const progress = client.progress
const hasActiveTorrents = client.torrents.some(torrent => torrent.progress !== 1) const hasActiveTorrents = client.torrents.some(function (torrent) {
return torrent.progress !== 1
})
// Track progress for every file in each torrent // Track progress for every file in each torrent
// TODO: ideally this would be tracked by WebTorrent, which could do it // TODO: ideally this would be tracked by WebTorrent, which could do it
// more efficiently than looping over torrent.bitfield // more efficiently than looping over torrent.bitfield
const torrentProg = client.torrents.map(torrent => { const torrentProg = client.torrents.map(function (torrent) {
const fileProg = torrent.files && torrent.files.map(file => { const fileProg = torrent.files && torrent.files.map(function (file, index) {
const numPieces = file._endPiece - file._startPiece + 1 const numPieces = file._endPiece - file._startPiece + 1
let numPiecesPresent = 0 let numPiecesPresent = 0
for (let piece = file._startPiece; piece <= file._endPiece; piece++) { for (let piece = file._startPiece; piece <= file._endPiece; piece++) {
@@ -297,12 +307,12 @@ function startServer (infoHash) {
else torrent.once('ready', () => startServerFromReadyTorrent(torrent)) else torrent.once('ready', () => startServerFromReadyTorrent(torrent))
} }
function startServerFromReadyTorrent (torrent) { function startServerFromReadyTorrent (torrent, cb) {
if (server) return if (server) return
// start the streaming torrent-to-http server // start the streaming torrent-to-http server
server = torrent.createServer() server = torrent.createServer()
server.listen(0, () => { server.listen(0, function () {
const port = server.address().port const port = server.address().port
const urlSuffix = ':' + port const urlSuffix = ':' + port
const info = { const info = {
@@ -312,8 +322,8 @@ function startServerFromReadyTorrent (torrent) {
networkAddress: networkAddress() networkAddress: networkAddress()
} }
ipcRenderer.send('wt-server-running', info) ipc.send('wt-server-running', info)
ipcRenderer.send('wt-server-' + torrent.infoHash, info) ipc.send('wt-server-' + torrent.infoHash, info)
}) })
} }
@@ -331,31 +341,25 @@ function getAudioMetadata (infoHash, index) {
// Set initial matadata to display the filename first. // Set initial matadata to display the filename first.
const metadata = { title: file.name } const metadata = { title: file.name }
ipcRenderer.send('wt-audio-metadata', infoHash, index, metadata) ipc.send('wt-audio-metadata', infoHash, index, metadata)
const options = { const options = {
native: false, native: false,
skipCovers: true, skipCovers: true,
fileSize: file.length, fileSize: file.length,
observer: () => { observer: event => {
ipcRenderer.send('wt-audio-metadata', infoHash, index, { ipc.send('wt-audio-metadata', infoHash, index, event.metadata)
common: metadata.common,
format: metadata.format
})
} }
} }
const onMetadata = file.done const onMetaData = file.done
// If completed; use direct file access // If completed; use direct file access
? mm.parseFile(path.join(torrent.path, file.path), options) ? mm.parseFile(path.join(torrent.path, file.path), options)
// otherwise stream // otherwise stream
: mm.parseStream(file.createReadStream(), file.name, options) : mm.parseStream(file.createReadStream(), file.name, options)
onMetadata onMetaData
.then( .then(
metadata => { () => console.log(`metadata for file='${file.name}' completed.`),
ipcRenderer.send('wt-audio-metadata', infoHash, index, metadata)
console.log(`metadata for file='${file.name}' completed.`)
},
err => { err => {
console.log( console.log(
`error getting audio metadata for ${infoHash}:${index}`, `error getting audio metadata for ${infoHash}:${index}`,
@@ -382,7 +386,7 @@ function selectFiles (torrentOrInfoHash, selections) {
// selection with individual selections for each file, so we can // selection with individual selections for each file, so we can
// select/deselect files later on // select/deselect files later on
if (!selections) { if (!selections) {
selections = new Array(torrent.files.length).fill(true) selections = torrent.files.map((x) => true)
} }
// Selections specified incorrectly? // Selections specified incorrectly?
@@ -395,15 +399,15 @@ function selectFiles (torrentOrInfoHash, selections) {
torrent.deselect(0, torrent.pieces.length - 1, false) torrent.deselect(0, torrent.pieces.length - 1, false)
// Add selections (individual files) // Add selections (individual files)
selections.forEach((selection, i) => { for (let i = 0; i < selections.length; i++) {
const file = torrent.files[i] const file = torrent.files[i]
if (selection) { if (selections[i]) {
file.select() file.select()
} else { } else {
console.log('deselecting file ' + i + ' of torrent ' + torrent.name) console.log('deselecting file ' + i + ' of torrent ' + torrent.name)
file.deselect() file.deselect()
} }
}) }
} }
// Gets a WebTorrent handle by torrentKey // Gets a WebTorrent handle by torrentKey
@@ -421,7 +425,7 @@ function onError (err) {
// TODO: remove this once the following bugs are fixed: // TODO: remove this once the following bugs are fixed:
// https://bugs.chromium.org/p/chromium/issues/detail?id=490143 // https://bugs.chromium.org/p/chromium/issues/detail?id=490143
// https://github.com/electron/electron/issues/7212 // https://github.com/electron/electron/issues/7212
window.testOfflineMode = () => { window.testOfflineMode = function () {
console.log('Test, going OFFLINE') console.log('Test, going OFFLINE')
client = window.client = new WebTorrent({ client = window.client = new WebTorrent({
peerId: PEER_ID, peerId: PEER_ID,

View File

@@ -32,8 +32,8 @@
<p> <p>
Version <script>document.write(require('../package.json').version)</script> Version <script>document.write(require('../package.json').version)</script>
(<script>document.write(require('webtorrent/package.json').version)</script>) (<script>document.write(require('webtorrent/package.json').version)</script>)
(<script>document.write(process.arch)</script>) (<script>document.write(process.arch === 'x64' ? '64-bit' : '32-bit')</script>)
</p> </p>
<p><script>document.write(require('../build/config').APP_COPYRIGHT)</script></p> <p><script>document.write(require('../config').APP_COPYRIGHT)</script></p>
</body> </body>
</html> </html>

Binary file not shown.

Binary file not shown.

View File

@@ -1,25 +1,26 @@
[Desktop Entry] [Desktop Entry]
Type=Application Type=Application
<% if (version) { %>Version=<%= version %><% } %>
Name=<%= productName %> Name=<%= productName %>
<% if (genericName) { %>GenericName=<%= genericName %><% } %> <% if (genericName) { %>GenericName=<%= genericName %><% } %>
<% if (description) { %>Comment=<%= description %><% } %> <% if (description) { %>Comment=<%= description %><% } %>
Icon=<%= name %> Icon=<%= name %>
<% if (name) { %>Exec=<%= name %> --no-sandbox %U<% } %><%/*HACK: --no-sandbox fixes an Electron 6 bug. See: #1703*/%> <% if (name) { %>Exec=<%= name %> %U<% } %>
Terminal=false Terminal=false
Actions=CreateNewTorrent;OpenTorrentFile;OpenTorrentAddress; Actions=CreateNewTorrent;OpenTorrentFile;OpenTorrentAddress;
<% if (mimeType && mimeType.length) { %>MimeType=<%= mimeType.join(';') %>;<% } %> <% if (mimeType && mimeType.length) { %>MimeType=<%= mimeType.join(';') %>;<% } %>
<% if (categories && categories.length) { %>Categories=<%= categories.join(';') %>;<% } %> <% if (categories && categories.length) { %>Categories=<%= categories.join(';') %>;<% } %>
StartupNotify=true StartupNotify=true
StartupWMClass=<%= productName %> <% if (name) { %>StartupWMClass=<%= name %> <% } %>
[Desktop Action CreateNewTorrent] [Desktop Action CreateNewTorrent]
Name=Create New Torrent... Name=Create New Torrent...
<% if (name) { %>Exec=<%= name %> --no-sandbox -n <% } %><%/*HACK: --no-sandbox fixes an Electron 6 bug. See: #1703*/%> <% if (name) { %>Exec=<%= name %> -n <% } %>
[Desktop Action OpenTorrentFile] [Desktop Action OpenTorrentFile]
Name=Open Torrent File... Name=Open Torrent File...
<% if (name) { %>Exec=<%= name %> --no-sandbox -o <% } %><%/*HACK: --no-sandbox fixes an Electron 6 bug. See: #1703*/%> <% if (name) { %>Exec=<%= name %> -o <% } %>
[Desktop Action OpenTorrentAddress] [Desktop Action OpenTorrentAddress]
Name=Open Torrent Address... Name=Open Torrent Address...
<% if (name) { %>Exec=<%= name %> --no-sandbox -u <% } %><%/*HACK: --no-sandbox fixes an Electron 6 bug. See: #1703*/%> <% if (name) { %>Exec=<%= name %> -u <% } %>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View File

@@ -322,10 +322,6 @@ textarea,
* TORRENT LIST * TORRENT LIST
*/ */
.torrent-list {
height: 100%;
}
.torrent { .torrent {
background: linear-gradient(to bottom right, #4B79A1, #283E51); background: linear-gradient(to bottom right, #4B79A1, #283E51);
background-repeat: no-repeat; background-repeat: no-repeat;
@@ -562,13 +558,6 @@ body.drag .app::after {
width: 100%; width: 100%;
} }
.player .name {
font-size: 20px;
font-weight: bold;
line-height: 1.5em;
margin-bottom: 6px;
}
/* /*
* PLAYER CONTROLS * PLAYER CONTROLS
*/ */
@@ -634,11 +623,6 @@ body.drag .app::after {
margin-top: 6px; margin-top: 6px;
} }
.player .controls .icon.multi-audio {
font-size: 26px;
margin-top: 6px;
}
.player .controls .icon.fullscreen { .player .controls .icon.fullscreen {
font-size: 26px; font-size: 26px;
margin-right: 15px; margin-right: 15px;

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,18 +1,15 @@
const electron = require('electron') const electron = require('electron')
const config = require('./config') const config = require('./config')
console.log('Mocking electron.dialog.showOpenDialogSync...') console.log('Mocking electron.dialog.showOpenDialog...')
electron.dialog.showOpenDialogSync = showOpenDialogSync electron.dialog.showOpenDialog = function (win, opts, cb) {
const ret = /select.*torrent file/i.test(opts.title)
function showOpenDialogSync (win, opts) {
return /select.*torrent file/i.test(opts.title)
? config.TORRENT_FILES ? config.TORRENT_FILES
: config.SEED_FILES : config.SEED_FILES
cb(ret)
} }
console.log('Mocking electron.dialog.showSaveDialogSync...') console.log('Mocking electron.remote.dialog.showSaveDialog...')
electron.dialog.showSaveDialogSync = showSaveDialogSync electron.dialog.showSaveDialog = function (win, opts, cb) {
cb(config.SAVED_TORRENT_FILE)
function showSaveDialogSync (win, opts) {
return config.SAVED_TORRENT_FILE
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 966 KiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 899 KiB

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 115 KiB

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 351 KiB

After

Width:  |  Height:  |  Size: 400 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 630 KiB

After

Width:  |  Height:  |  Size: 737 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 378 KiB

After

Width:  |  Height:  |  Size: 511 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 380 KiB

After

Width:  |  Height:  |  Size: 514 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 380 KiB

After

Width:  |  Height:  |  Size: 514 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 380 KiB

After

Width:  |  Height:  |  Size: 514 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 381 KiB

After

Width:  |  Height:  |  Size: 515 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 116 KiB

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 904 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 746 KiB

After

Width:  |  Height:  |  Size: 873 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1003 KiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1019 KiB

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1003 KiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Some files were not shown because too many files have changed in this diff Show More