Compare commits

..

1 Commits

Author SHA1 Message Date
Mathias Rasmussen
6656b75c1b WIP: add embedded mkv subtitles 2016-08-30 00:28:37 +02:00
169 changed files with 2465 additions and 4319 deletions

View File

@@ -1,5 +1,3 @@
<!-- 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?**

2
.gitignore vendored
View File

@@ -1,4 +1,4 @@
node_modules/
build/
dist/
npm-debug.log*
npm-debug.log.*

2
.nodemonignore Normal file
View File

@@ -0,0 +1,2 @@
build/
dist/

View File

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

View File

@@ -28,19 +28,5 @@
- Rémi Jouannet (remijouannet@gmail.com)
- Andrea Tupini (tupini07@gmail.com)
- grunjol (grunjol@gmail.com)
- Jason Kurian (jasonk92@gmail.com)
- Vamsi Krishna Avula (vamsi_ism@outlook.com)
- Noam Okman (noamokman@gmail.com)
- PurgingPanda (t3ch0wn3r@gmail.com)
- Kai Curtis (morecode@kcurtis.com)
- Omri Litov (omrilitov@gmail.com)
- Alexey Romanov (romanalexey@gmail.com)
- Karan Thakkar (karanjthakkar@gmail.com)
- Nuno Campos (nuno.campos@me.com)
- Ebrahim Byagowi (ebrahim@gnu.org)
- Emil Bay (github@tixz.dk)
- Auyer (rafa_auyer@icloud.com)
- SimplyAhmazing (ahmad19526@gmail.com)
- Cezar Carneiro (cezargcarneiro@gmail.com)
#### Generated by bin/update-authors.sh.

View File

@@ -1,158 +1,18 @@
# WebTorrent Desktop Version History
## v0.19.0 - 2018-01-26
### Added
- Added watch folder feature: Automatically add new torrent files added to a folder on disk (#1154)
- Added highest playback priority feature: pauses other active torrents when playback starts (#840)
- Add 'Start Speaking' and 'Stop Speaking' menu item (Mac) (#439)
- Add pinch-to-zoom gesture to enter/exit fullscreen (#1148)
### Changed
- [SECURITY] Mitigate
- Moved project from Feross's GitHub account to the WebTorrent GitHub organization
- Updated to electron@1.6.16
- Updated to material-ui@0.17
- Treat .FLAC as playable audio (#1127)
### Fixed
- Fix time and duration so it doesn't bounce in the UI (#1233)
- Fix 'About WebTorrent' menu location on Windows (#1120)
## v0.18.0 - 2017-02-03
### Added
- Add a new "Transfers" menu for pausing or resuming all torrents (#1027)
### Changed
- Update Electron to 1.4.15
- Windows 32-bit: App can use 4GB of memory instead of just 2GB
- Fix "Portable App" writing crash reports to "%APPDATA%\Temp" (Windows)
- Updated WebTorrent engine to 0.98.5
- Fix issue where http web seeds would sometimes stall
- Don't send 'completed' event to tracker again if torrent is already complete
- Add more peer ID entropy
- Set user-agent header for tracker http requests
### Fixed
- Fix paste shortcut in tracker list on Create Torrent page (#1112)
- Auto-focus the 'OK' button in modal dialogs (#1058)
- Fix formatting issue in the speed stats on the Player page (#1039)
## v0.17.2 - 2016-10-10
### Fixed
- Windows: Fix impossible-to-delete "Wired CD" default torrent
- Throttle browser-window 'move' and 'resize' events
- Fix crash ("Cannot read property 'files' of null" error)
- Fix crash ("TypeError: Cannot read property 'startPiece' of undefined")
## v0.17.1 - 2016-10-03
### Changed
- Faster startup (improved by ~25%)
- Update Electron to 1.4.2
- Remove support for pasting multiple newline-separated magnet links
- Reduce UX sound volume
### Fixed
- Fix external player (VLC, etc.) opening before HTTP server was ready
- Windows (Portable App): Fix "Portable App" mode
- Write application support files to the "Portable Settings" folder
- Stop writing Electron "single instance" lock file to "%APPDATA%\Roaming\WebTorrent"
- Some temp data is still written to "%APPDATA%\Temp" (will be fixed in future version)
- Don't show pointer cursor on torrent list checkbox
- Trim extra whitespace from magnet links pasted into "Open Torrent Address" dialog
- Fix weird outline on 'Create Torrent' button
## v0.17.0 - 2016-09-23
### Added
- Remember window size and position
### Changed
- Torrent list redesign
- Quieter, more subtle sounds
- Got rid of the play button spinner, now goes to the player immediately
- Faster startup
### Fixed
- Fix bug where playback rate could go negative
- Don't hide header when moused over player controls
- Fix Delete Data File on Windows
- Fix a sad, sad bug that resulted in 100+ MB config files
- Fix app DMG background image
## v0.16.0 - 2016-09-18
### Added
- **Windows 64-bit support!** (#931)
- Existing 32-bit users will update to 64-bit automatically in next release
- 64-bit reduces likelihood of out-of-memory errors by increasing the address space
### Fixed
- Mac: Fix background image on .DMG
## v0.15.0 - 2016-09-16
### Added
- Option to start automatically on login
- Add integration tests
- Add more detailed telemetry to diagnose "buffer allocation failed"
### Changed
- Disable playback controls while in external player (#909)
### Fixed
- Fix several uncaught errors (#889, #891, #892)
- Update to the latest webtorrent.js, fixing some more uncaught errors
- Clicking on the "torrent finished" notification works again (#912)
## v0.14.0 - 2016-09-03
### Added
- Autoplay through all files in a torrent (#871)
- Torrents now have a progress bar (#844)
### Changed
- Modals now use Material UI
- Torrent list style improvements
### Fixed
- Fix App.js crash in Linux (#882)
- Fix error on Windows caused by `setBadge` (#867)
- Don't crash when restarting after adding a magnet link (#869)
- Restore playback state when reopening player (#877)
## v0.13.1 - 2016-08-31
### Fixed
- Fixed the Create Torrent page
## v0.13.0 - 2016-08-31
### Added
- Support .m4a audio
- Better telemetry: log error versions, report more types of errors
### Changed
- New look - Material UI. Rewrote Create Torrent and Preferences pages.
### Fixed
- Fixed telemetry [object Object] and [object HTMLMediaElement] bugs
- Don't render player controls when playing externally, eg in VLC
- Don't play notification sounds during media playback
## v0.12.0 - 2016-08-23
### Added
- Custom external media player
- Linux: add system-wide launcher and icons for Debian, including Ubuntu
### Changed
- Telemetry improvements: redact stacktraces, log app version
### Fixed
- Fix playback and download of default torrents ("missing path" error) (#804)
- Fix Delete Torrent + Data for newly added magnet links
- Fix jumpToTime error (#804)
@@ -165,11 +25,13 @@
- Check for missing default download path and torrent folders on start up (#776)
### Changed
- Do not automatically set WebTorrent as the default handler for torrents (#771)
- Torrents can only be created from the home screen (#770)
- Update Electron to 1.3.3 (#772)
### Fixed
- Allow modifying the default tracker list on the Create Torrent page (#775)
- Prevent opening multiple stacked Preference windows or Create Torrent windows (#770)
- Windows: Player window auto-resize does not match video aspect ratio (#565)
@@ -178,11 +40,13 @@
## v0.10.0 - 2016-08-05
### Added
- Drag-and-drop magnet links (selected text) is now supported (#284)
- Windows: Add "User Tasks" shortcuts to app icon in Start Menu (#114)
- Linux: Show badge count for completed torrent downloads
### Changed
- Change WebTorrent Desktop peer ID prefix to 'WD' to distinguish from WebTorrent in the browser, 'WW' (#688)
- Switch UI to React to improve UI rendering speed (#729)
- The primary bottleneck was actually `hyperx`, not `virtual-dom`.
@@ -199,6 +63,7 @@
- Location history abstraction released independently as [`location-history`](https://www.npmjs.com/package/location-history)
### Fixed
- When streaming to VLC, set VLC window title to torrent file name (#746)
- Fix "Cannot read property 'numPiecesPresent' of undefined" exception (#695)
- Fix rare case where config file could not be completely written (#733)
@@ -206,29 +71,35 @@
## v0.9.0 - 2016-07-20
### Added
- Save selected subtitles
- Ask for confirmation before deleting torrents
- Support Debian Jessie
### Changed
- Only send telemetry in production
- Clean up the code. Split main.js, refactor lots of things
### Fixed
- Fix state.playing.jumpToTime behavior
- Remove torrent file and poster image when deleting a torrent
## v0.8.1 - 2016-06-24
### Added
- New URI handler: stream-magnet
### Fixed
- DLNA crashing bug
## v0.8.0 - 2016-06-23
### Added
- Cast menu: choose which Chromecast, Airplay, or DLNA device you want to use
- Telemetry: send basic data, plus stats on how often the play button works
- Make posters from jpeg files, not just jpg
@@ -236,14 +107,17 @@
- Windows thumbnail bar with a play/pause button
### Changed
- Nicer modal styles
### Fixed
- Windows tray icon now stays in the right state
## v0.7.2 - 2016-06-02
### Fixed
- Fix exception that affects users upgrading from v0.5.1 or older
- Ensure `state.saved.prefs` configuration exists
- Fix window title on "About WebTorrent" window
@@ -251,19 +125,23 @@
## v0.7.1 - 2016-06-02
### Changed
- Change "Step Forward" keyboard shortcut to `Alt+Left` (Windows)
- Change "Step Backward" keyboard shortcut to to `Alt+Right` (Windows)
### Fixed
- First time startup bug -- invalid torrent/poster paths
## v0.7.0 - 2016-06-02
### Added
- Improved AirPlay support -- using the new [`airplayer`](https://www.npmjs.com/package/airplayer) package
- Remember volume setting in player, for as long as the app is open
### Changed
- Add (+) button now also accepts non .torrent files and creates a torrent from
those files
- Show prompt text in title bar for open dialogs (OS X)
@@ -273,6 +151,7 @@
- Fix crash reporter not working (Windows)
### Fixed
- Re-enable WebRTC (web peers)! (OS X, Windows)
- Windows support was disabled in v0.6.1 to work around a bug in Electron
- OS X support was disabled in v0.4.0 to work around a 100% CPU bug
@@ -283,6 +162,7 @@
- Fix torrent loading message UI misalignment
### Known issues
- When upgrading to WebTorrent Desktop v0.7.0, some torrent metadata (file list,
selected files, whether torrent is streamable) will be cleared. Just start the
torrent to re-populate the metadata.
@@ -290,6 +170,7 @@
## v0.6.1 - 2016-05-26
### Fixed
- Disable WebRTC to work around Electron crash (Windows)
- Will be re-enabled in the next version of WebTorrent, which will be based on
the next version of Electron, where the bug is fixed.
@@ -301,6 +182,7 @@
## v0.6.0 - 2016-05-24
### Added
- Added Preferences page to set Download folder
- Save video position, resume playback from saved position
- Add additional video player keyboard shortcuts (#275)
@@ -310,6 +192,7 @@
- Add announcement feature
### Changed
- Nicer player UI
- Reduce startup jank, improve startup time (#568)
- Cleanup unsupported codec detection (#569, #570)
@@ -317,6 +200,7 @@
- Improve subtitle positioning (#551)
### Fixed
- Fix Uncaught TypeError: Cannot read property 'update' of undefined (#567)
- Fix bugs in LocationHistory
- When player is active, and magnet link is pasted, go back to list
@@ -327,19 +211,23 @@
## v0.5.1 - 2016-05-18
### Fixed
- Fix auto-updater (OS X, Windows).
## v0.5.0 - 2016-05-17
### Added
- Select/deselect individual files to torrent.
- Automatically include subtitle files (.srt, .vtt) from torrent in the subtitles menu.
- "Add Subtitle File..." menu item.
### Changed
- When manually adding subtitle track(s), always switch to the new track.
### Fixed
- Magnet links throw exception on app launch. (OS X)
- Multi-file torrents would not seed in-place, were copied to Downloads folder.
- Missing 'About WebTorrent' menu item. (Windows)
@@ -348,6 +236,7 @@
## v0.4.0 - 2016-05-13
### Added
- Better Windows support!
- Windows 32-bit build.
- Windows Portable App build.
@@ -370,6 +259,7 @@
- New default torrent on first launch: The WIRED CD.
### Changed
- Improve app startup time by 40%.
- UI tweaks: Reduce font size, reduce torrent list item height.
- Add Playback menu for playback-related functionality.
@@ -380,6 +270,7 @@
- Remove "Add Fake Airplay/Chromecast" menu items.
### Fixed
- Disable WebRTC to fix 100% CPU usage/crashes caused by Chromium issue. This is
temporary. (OS X)
- When fullscreen, make controls use the full window. (OS X)
@@ -404,20 +295,24 @@ to this release!
## v0.3.3 - 2016-04-07
### Fixed
- App icon was incorrect (OS X)
## v0.3.2 - 2016-04-07
### Added
- Register WebTorrent as default handler for magnet links (OS X)
### Changed
- Faster startup time (50ms)
- Update Electron to 0.37.5
- Remove the white flash when loading pages and resizing the window
- Fix crash when sending IPC messages
### Fixed
- Fix installation bugs with .deb file (Linux)
- Pause audio reliably when closing the window
- Enforce minimimum window size when resizing player (for audio-only .mov files, which are 0x0)
@@ -425,14 +320,17 @@ to this release!
## v0.3.1 - 2016-04-06
### Added
- Add crash reporter to torrent engine process
### Fixed
- Fix cast screen background: cover, don't tile
## v0.3.0 - 2016-04-06
### Added
- **Ubuntu/Debian support!** (.deb installer)
- **DLNA streaming support**
- Add "File > Quit" menu item (Linux)
@@ -440,12 +338,14 @@ to this release!
- Crash reporting
### Changed
- On startup, do not re-verify files when timestamps are unchanged
- Moved torrent engine to an independent process, for better UI performance
- Removed media queries (UI resizing based on window width)
- Improved Chromecast icon, when connected
### Fixed
- "Download Complete" notification shows consistently
- Create new torrents and seed them without copying to temporary folder
- Clicking the "Download Complete" notification will always activate app
@@ -464,6 +364,7 @@ Thanks to @dcposch, @grunjol, and @feross for contributing to this release.
## v0.2.0 - 2016-03-29
### Added
- Minimise to tray (Windows, Linux)
- Show spinner and download speed when player is stalled waiting for data
- Highlight window on drag-and-drop
@@ -472,10 +373,12 @@ Thanks to @dcposch, @grunjol, and @feross for contributing to this release.
Linux users need to download new versions manually.
### Changed
- Renamed WebTorrent.app to WebTorrent Desktop
- Add Cosmos Laundromat as a default torrent
### Fixed
- Only capture media keys when player is active
- Update WebTorrent to 0.88.1 for performance improvements
- When seeding, do not proactively connect to new peers
@@ -535,7 +438,7 @@ Windows, and Linux. For now, we're only releasing binaries for OS X.
WebTorrent Desktop is in ALPHA and under very active development  expect lots more polish in
the coming weeks! If you know JavaScript and want to help us out, there's
[lots to do](https://github.com/webtorrent/webtorrent-desktop/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+contribution%22)!
[lots to do](https://github.com/feross/webtorrent-desktop/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+contribution%22)!
### Features

View File

@@ -24,7 +24,7 @@ and to avoid style arguments. `npm test` runs `standard` automatically, so you d
to!
[standard-image]: https://cdn.rawgit.com/feross/standard/master/badge.svg
[standard-url]: https://standardjs.com
[standard-url]: https://github.com/feross/standard
## Project Governance
@@ -36,7 +36,7 @@ standard guarded open source project.
There are a few basic ground-rules for contributors:
1. **No `--force` pushes to master** or modifying history in any way. Rebasing and force pushing your own PR branch is fine.
1. **No `--force` pushes** or modifying the Git history in any way.
2. **Non-master branches** should be used for ongoing work.
3. **Significant modifications** like API changes should be subject to a **pull request**
to solicit feedback from other contributors.
@@ -74,99 +74,6 @@ By making a contribution to this project, I certify that:
including my sign-off) is maintained indefinitely and may be redistributed consistent
with this project or the open source license(s) involved.
## Release Procedure
### 1. Create a new version
- Update `AUTHORS`
```
npm run update-authors
```
Commit if necessary. The commit message should be "authors".
- Write the changelog
You can use `git log --oneline <last version tag>..HEAD` to get a list of changes.
Summarize them concisely in `CHANGELOG.md`. The commit message should be "changelog".
- Update the version
```
npm version [major|minor|patch]
```
This creates both a commit and a git tag.
- Make a PR
Once the PR is reviewed, merge it:
```
git push origin <branch-name>:master
```
This makes it so that the commit hash on master matches the commit hash of the version tag.
Finally, run:
```
git push --tags
```
### 2. Create the release binaries
- On a Mac:
```
npm run package -- darwin --sign
npm run package -- linux --sign
```
- On Windows, or in a Windows VM:
```
npm run package -- win32 --sign
```
- Then, upload the release binaries to Github:
```
npm run gh-release
```
Follow the URL to a newly created Github release page. Manually upload the binaries from
`webtorrent-desktop/dist/`. Open the previous release in another tab, and make sure that you
are uploading the same set of files, no more, no less.
### 3. Test it
**This is the most important part.**
- Manually download the binaries for each platform from Github.
**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
codesigning worked and is valid.
- Smoke test WebTorrent Desktop on each platform.
See Smoke Tests below for details. Open DevTools
on Windows and Mac, and ensure that the auto updater is running. If the auto updater does not
run, users will successfully auto update to this new version, and then be stuck there forever.
### 4. Ship it
- Update the website
Create a pull request in [webtorrent.io](https://github.com/webtorrent/webtorrent.io). Update
`config.js`, updating the desktop app version.
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.**
## Smoke Tests
Before a release, check that the following basic use cases work correctly:

View File

@@ -1,8 +1,6 @@
<h1 align="center">
<br>
<a href="https://webtorrent.io">
<img src="https://webtorrent.io/img/WebTorrent.png" alt="WebTorrent" width="200">
</a>
<a href="https://webtorrent.io"><img src="https://webtorrent.io/img/WebTorrent.png" alt="WebTorrent" width="200"></a>
<br>
WebTorrent Desktop
<br>
@@ -12,17 +10,16 @@
<h4 align="center">The streaming torrent app. For Mac, Windows, and Linux.</h4>
<p align="center">
<a href="https://gitter.im/webtorrent/webtorrent"><img src="https://img.shields.io/badge/gitter-join%20chat%20%E2%86%92-brightgreen.svg" alt="gitter"></a>
<a href="https://github.com/webtorrent/webtorrent-desktop/releases"><img src="https://img.shields.io/github/release/webtorrent/webtorrent-desktop.svg" alt="github release"></a>
<a href="https://travis-ci.org/webtorrent/webtorrent-desktop"><img src="https://img.shields.io/travis/webtorrent/webtorrent-desktop/master.svg" alt="travis"></a>
<a href="https://standardjs.com"><img src="https://img.shields.io/badge/code_style-standard-brightgreen.svg" alt="Standard - JavaScript Style Guide"></a>
<a href="https://gitter.im/feross/webtorrent"><img src="https://img.shields.io/badge/gitter-join%20chat%20%E2%86%92-brightgreen.svg" alt="Gitter"></a>
<a href="https://travis-ci.org/feross/webtorrent-desktop"><img src="https://img.shields.io/travis/feross/webtorrent-desktop/master.svg" alt="Travis"></a>
<a href="https://github.com/feross/webtorrent-desktop/releases"><img src="https://img.shields.io/github/release/feross/webtorrent-desktop.svg" alt="Release"></a>
</p>
## Install
Download the latest version of WebTorrent Desktop from
[the official website](https://webtorrent.io/desktop/) or the
[GitHub releases](https://github.com/webtorrent/webtorrent-desktop/releases) page.
[GitHub releases](https://github.com/feross/webtorrent-desktop/releases) page.
**WebTorrent Desktop** is under very active development. You can try out the
current (unstable) development version by cloning the Git repo. See the
@@ -40,7 +37,7 @@ instructions below in the ["How to Contribute"](#how-to-contribute) section.
### Get the code
```
$ git clone https://github.com/webtorrent/webtorrent-desktop.git
$ git clone https://github.com/feross/webtorrent-desktop.git
$ cd webtorrent-desktop
$ npm install
```
@@ -59,38 +56,6 @@ Restart the app automatically every time code changes. Useful during development
$ npm run watch
```
### Run linters
```
$ npm test
```
### Run integration tests
```
$ npm run test-integration
```
The integration tests use Spectron and Tape. They click through the app, taking screenshots and
comparing each one to a reference. Why screenshots?
* Ad-hoc checking makes the tests a lot more work to write
* Even diffing the whole HTML is not as thorough as screenshot diffing. For example, it wouldn't
catch an bug where hitting ESC from a video doesn't correctly restore window size.
* Chrome's own integration tests use screenshot diffing iirc
* Small UI changes will break a few tests, but the fix is as easy as deleting the offending
screenshots and running the tests, which will recreate them with the new look.
* The resulting Github PR will then show, pixel by pixel, the exact UI changes that were made! See
https://github.com/blog/817-behold-image-view-modes
For MacOS, you'll need a Retina screen for the integration tests to pass. Your screen should have
the same resolution as a 2016 12" Macbook.
For Windows, you'll need Windows 10 with a 1366x768 screen.
When running integration tests, keep the mouse on the edge of the screen and don't touch the mouse
or keyboard while the tests are running.
### Package the app
Builds app binaries for Mac, Linux, and Windows.
@@ -119,7 +84,7 @@ The following optional arguments are available:
- `all` - All platforms (default)
Note: Even with the `--package` option, the auto-update files (.nupkg for Windows,
-darwin.zip for Mac) will always be produced.
*-darwin.zip for Mac) will always be produced.
#### Windows build notes
@@ -154,6 +119,10 @@ Time out? Show a missing codec error?
The app never sends any personally identifying information, nor does it track which
torrents you add.
### Code Style
[![js-standard-style](https://cdn.rawgit.com/feross/standard/master/badge.svg)](https://github.com/feross/standard)
## License
MIT. Copyright (c) [WebTorrent, LLC](https://webtorrent.io).

View File

@@ -1,43 +0,0 @@
# Security Policies and Procedures
This document outlines security procedures and general policies for the WebTorrent
project.
* [Reporting a Bug](#reporting-a-bug)
* [Disclosure Policy](#disclosure-policy)
* [Comments on this Policy](#comments-on-this-policy)
## Reporting a Bug
The WebTorrent team and community take all security bugs in WebTorrent seriously.
Thank you for improving the security of WebTorrent. We appreciate your efforts and
responsible disclosure and will make every effort to acknowledge your
contributions.
Report security bugs by emailing the lead maintainer at feross@feross.org.
The lead maintainer will acknowledge your email within 48 hours, and will send a
more detailed response within 48 hours indicating the next steps in handling
your report. After the initial reply to your report, the security team will
endeavor to keep you informed of the progress towards a fix and full
announcement, and may ask for additional information or guidance.
Report security bugs in third-party modules to the person or team maintaining
the module. You can also report a vulnerability through the
[Node Security Project](https://nodesecurity.io/report).
## Disclosure Policy
When the security team receives a security bug report, they will assign it to a
primary handler. This person will coordinate the fix and release process,
involving the following steps:
* Confirm the problem and determine the affected versions.
* Audit code to find any potential similar problems.
* Prepare fixes for all releases still under maintenance. These fixes will be
released as fast as possible to npm.
## Comments on this Policy
If you have suggestions on how this process could be improved please submit a
pull request.

108
bin/check-deps.js Executable file
View File

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

View File

@@ -5,13 +5,13 @@
* Useful for developers.
*/
const fs = require('fs')
const os = require('os')
const path = require('path')
const rimraf = require('rimraf')
var fs = require('fs')
var os = require('os')
var path = require('path')
var rimraf = require('rimraf')
const config = require('../src/config')
const handlers = require('../src/main/handlers')
var config = require('../src/config')
var handlers = require('../src/main/handlers')
// First, remove generated files
rimraf.sync('build/')
@@ -21,11 +21,11 @@ rimraf.sync('dist/')
rimraf.sync(config.CONFIG_PATH)
// Remove any temporary files
let tmpPath
var tmpPath
try {
tmpPath = path.join(fs.statSync('/tmp') && '/tmp', 'webtorrent')
} catch (err) {
tmpPath = path.join(os.tmpdir(), 'webtorrent')
tmpPath = path.join(os.tmpDir(), 'webtorrent')
}
rimraf.sync(tmpPath)

10
bin/cmd.js Executable file
View File

@@ -0,0 +1,10 @@
#!/usr/bin/env node
var electron = require('electron')
var cp = require('child_process')
var path = require('path')
var child = cp.spawn(electron, [path.join(__dirname, '..')], {stdio: 'inherit'})
child.on('close', function (code) {
process.exitCode = code
})

View File

@@ -1,46 +0,0 @@
#!/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
// Consistent JSX tag closing
if (line.match(/' {2}\/> *$/) ||
line.match('[^ ]/> *$') ||
line.match(' > *$')) {
error = 'JSX tag spacing'
}
// 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) {
let 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!')

10
bin/list-deps.sh Executable file
View File

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

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env node
const config = require('../src/config')
const open = require('open')
var config = require('../src/config')
var open = require('open')
open(config.CONFIG_PATH)

View File

@@ -1,29 +1,28 @@
#!/usr/bin/env node
/**
* Builds app binaries for Mac, Windows, and Linux.
* Builds app binaries for Mac, Linux, and Windows.
*/
const cp = require('child_process')
const electronPackager = require('electron-packager')
const fs = require('fs')
const minimist = require('minimist')
const mkdirp = require('mkdirp')
const os = require('os')
const path = require('path')
const rimraf = require('rimraf')
const series = require('run-series')
const zip = require('cross-zip')
var cp = require('child_process')
var electronPackager = require('electron-packager')
var fs = require('fs')
var minimist = require('minimist')
var mkdirp = require('mkdirp')
var os = require('os')
var path = require('path')
var rimraf = require('rimraf')
var series = require('run-series')
var zip = require('cross-zip')
const config = require('../src/config')
const pkg = require('../package.json')
var config = require('../src/config')
var pkg = require('../package.json')
const BUILD_NAME = config.APP_NAME + '-v' + config.APP_VERSION
const BUILD_PATH = path.join(config.ROOT_PATH, 'build')
const DIST_PATH = path.join(config.ROOT_PATH, 'dist')
const NODE_MODULES_PATH = path.join(config.ROOT_PATH, 'node_modules')
var BUILD_NAME = config.APP_NAME + '-v' + config.APP_VERSION
var BUILD_PATH = path.join(config.ROOT_PATH, 'build')
var DIST_PATH = path.join(config.ROOT_PATH, 'dist')
const argv = minimist(process.argv.slice(2), {
var argv = minimist(process.argv.slice(2), {
boolean: [
'sign'
],
@@ -37,20 +36,14 @@ const argv = minimist(process.argv.slice(2), {
})
function build () {
console.log('Reinstalling node_modules...')
rimraf.sync(NODE_MODULES_PATH)
cp.execSync('npm install', { stdio: 'inherit' })
cp.execSync('npm dedupe', { stdio: 'inherit' })
console.log('Nuking dist/ and build/...')
rimraf.sync(DIST_PATH)
rimraf.sync(BUILD_PATH)
console.log('Build: Transpiling to ES5...')
console.log('Babel: Building JSX...')
cp.execSync('npm run build', { NODE_ENV: 'production', stdio: 'inherit' })
console.log('Build: Transpiled to ES5.')
console.log('Babel: Built JSX.')
const platform = argv._[0]
var platform = argv._[0]
if (platform === 'darwin') {
buildDarwin(printDone)
} else if (platform === 'win32') {
@@ -68,7 +61,7 @@ function build () {
}
}
const all = {
var all = {
// The human-readable copyright line for the app. Maps to the `LegalCopyright` metadata
// property on Windows, and `NSHumanReadableCopyright` on Mac.
'app-copyright': config.APP_COPYRIGHT,
@@ -80,11 +73,11 @@ const all = {
// Package the application's source code into an archive, using Electron's archive
// format. Mitigates issues around long path names on Windows and slightly speeds up
// require().
asar: {
// A glob expression, that unpacks the files with matching names to the
// "app.asar.unpacked" directory.
unpack: 'WebTorrent*'
},
asar: true,
// A glob expression, that unpacks the files with matching names to the
// "app.asar.unpacked" directory.
'asar-unpack': 'WebTorrent*',
// The build version of the application. Maps to the FileVersion metadata property on
// Windows, and CFBundleVersion on Mac. Note: Windows requires the build version to
@@ -96,7 +89,7 @@ const all = {
// Pattern which specifies which files to ignore when copying files to create the
// package(s).
ignore: /^\/src|^\/dist|\/(appveyor.yml|\.appveyor.yml|\.github|appdmg|AUTHORS|CONTRIBUTORS|bench|benchmark|benchmark\.js|bin|bower\.json|component\.json|coverage|doc|docs|docs\.mli|dragdrop\.min\.js|example|examples|example\.html|example\.js|externs|ipaddr\.min\.js|Makefile|min|minimist|perf|rusha|simplepeer\.min\.js|simplewebsocket\.min\.js|static\/screenshot\.png|test|tests|test\.js|tests\.js|webtorrent\.min\.js|\.[^/]*|.*\.md|.*\.markdown)$/,
ignore: /^\/src|^\/dist|\/(appveyor.yml|\.appveyor.yml|\.github|appdmg|AUTHORS|CONTRIBUTORS|bench|benchmark|benchmark\.js|bin|bower\.json|component\.json|coverage|doc|docs|docs\.mli|dragdrop\.min\.js|example|examples|example\.html|example\.js|externs|ipaddr\.min\.js|Makefile|min|minimist|perf|rusha|simplepeer\.min\.js|simplewebsocket\.min\.js|static\/screenshot\.png|test|tests|test\.js|tests\.js|webtorrent\.min\.js|\.[^\/]*|.*\.md|.*\.markdown)$/,
// The application name.
name: config.APP_NAME,
@@ -111,15 +104,15 @@ const all = {
// "devDependencies" before starting to package the app.
prune: true,
// The Electron version that the app is built with (without the leading 'v')
electronVersion: require('electron/package.json').version
// The Electron version with which the app is built (without the leading 'v')
version: require('electron/package.json').version
}
const darwin = {
var darwin = {
// Build for Mac
platform: 'darwin',
// Build x64 binaries only.
// Build 64 bit binaries only.
arch: 'x64',
// The bundle identifier to use in the application's plist (Mac only).
@@ -136,15 +129,15 @@ const darwin = {
icon: config.APP_ICON + '.icns'
}
const win32 = {
var win32 = {
// Build for Windows.
platform: 'win32',
// Build ia32 and x64 binaries.
arch: ['ia32', 'x64'],
// Build 32 bit binaries only.
arch: 'ia32',
// Object hash of application metadata to embed into the executable (Windows only)
win32metadata: {
'version-string': {
// Company that produced the file.
CompanyName: config.APP_NAME,
@@ -170,12 +163,12 @@ const win32 = {
icon: config.APP_ICON + '.ico'
}
const linux = {
var linux = {
// Build for Linux.
platform: 'linux',
// Build ia32 and x64 binaries.
arch: ['ia32', 'x64']
// Build 32 and 64 bit binaries.
arch: 'all'
// Note: Application icon for Linux is specified via the BrowserWindow `icon` option.
}
@@ -183,18 +176,18 @@ const linux = {
build()
function buildDarwin (cb) {
const plist = require('plist')
var plist = require('plist')
console.log('Mac: Packaging electron...')
electronPackager(Object.assign({}, all, darwin), function (err, buildPath) {
if (err) return cb(err)
console.log('Mac: Packaged electron. ' + buildPath)
const appPath = path.join(buildPath[0], config.APP_NAME + '.app')
const contentsPath = path.join(appPath, 'Contents')
const resourcesPath = path.join(contentsPath, 'Resources')
const infoPlistPath = path.join(contentsPath, 'Info.plist')
const infoPlist = plist.parse(fs.readFileSync(infoPlistPath, 'utf8'))
var appPath = path.join(buildPath[0], config.APP_NAME + '.app')
var contentsPath = path.join(appPath, 'Contents')
var resourcesPath = path.join(contentsPath, 'Resources')
var infoPlistPath = path.join(contentsPath, 'Info.plist')
var infoPlist = plist.parse(fs.readFileSync(infoPlistPath, 'utf8'))
infoPlist.CFBundleDocumentTypes = [
{
@@ -268,7 +261,7 @@ function buildDarwin (cb) {
}
function signApp (cb) {
const sign = require('electron-osx-sign')
var sign = require('electron-osx-sign')
/*
* Sign the app with Apple Developer ID certificates. We sign the app for 2 reasons:
@@ -283,7 +276,7 @@ function buildDarwin (cb) {
* - Xcode Command Line Tools (xcode-select --install)
* - Membership in the Apple Developer Program
*/
const signOpts = {
var signOpts = {
app: appPath,
platform: 'darwin',
verbose: true
@@ -309,8 +302,8 @@ function buildDarwin (cb) {
// Create .zip file (used by the auto-updater)
console.log('Mac: Creating zip...')
const inPath = path.join(buildPath[0], config.APP_NAME + '.app')
const outPath = path.join(DIST_PATH, BUILD_NAME + '-darwin.zip')
var inPath = path.join(buildPath[0], config.APP_NAME + '.app')
var outPath = path.join(DIST_PATH, BUILD_NAME + '-darwin.zip')
zip.zipSync(inPath, outPath)
console.log('Mac: Created zip.')
@@ -319,13 +312,13 @@ function buildDarwin (cb) {
function packageDmg (cb) {
console.log('Mac: Creating dmg...')
const appDmg = require('appdmg')
var appDmg = require('appdmg')
const targetPath = path.join(DIST_PATH, BUILD_NAME + '.dmg')
var targetPath = path.join(DIST_PATH, BUILD_NAME + '.dmg')
rimraf.sync(targetPath)
// Create a .dmg (Mac disk image) file, for easy user installation.
const dmgOpts = {
var dmgOpts = {
basepath: config.ROOT_PATH,
target: targetPath,
specification: {
@@ -346,7 +339,7 @@ function buildDarwin (cb) {
}
}
const dmg = appDmg(dmgOpts)
var dmg = appDmg(dmgOpts)
dmg.once('error', cb)
dmg.on('progress', function (info) {
if (info.type === 'step-begin') console.log(info.title + '...')
@@ -360,7 +353,7 @@ function buildDarwin (cb) {
}
function buildWin32 (cb) {
const installer = require('electron-winstaller')
var installer = require('electron-winstaller')
console.log('Windows: Packaging electron...')
/*
@@ -368,7 +361,7 @@ function buildWin32 (cb) {
* - Windows Authenticode private key and cert (authenticode.p12)
* - Windows Authenticode password file (authenticode.txt)
*/
let CERT_PATH
var CERT_PATH
try {
fs.accessSync('D:')
CERT_PATH = 'D:'
@@ -380,12 +373,12 @@ function buildWin32 (cb) {
if (err) return cb(err)
console.log('Windows: Packaged electron. ' + buildPath)
let signWithParams
var signWithParams
if (process.platform === 'win32') {
if (argv.sign) {
const certificateFile = path.join(CERT_PATH, 'authenticode.p12')
const certificatePassword = fs.readFileSync(path.join(CERT_PATH, 'authenticode.txt'), 'utf8')
const timestampServer = 'http://timestamp.comodoca.com'
var certificateFile = path.join(CERT_PATH, 'authenticode.p12')
var certificatePassword = fs.readFileSync(path.join(CERT_PATH, 'authenticode.txt'), 'utf8')
var timestampServer = 'http://timestamp.comodoca.com'
signWithParams = `/a /f "${certificateFile}" /p "${certificatePassword}" /tr "${timestampServer}" /td sha256`
} else {
printWarning()
@@ -394,26 +387,20 @@ function buildWin32 (cb) {
printWarning()
}
const tasks = []
buildPath.forEach(function (filesPath) {
const destArch = filesPath.split('-').pop()
if (argv.package === 'exe' || argv.package === 'all') {
tasks.push((cb) => packageInstaller(filesPath, destArch, cb))
}
if (argv.package === 'portable' || argv.package === 'all') {
tasks.push((cb) => packagePortable(filesPath, destArch, cb))
}
})
var tasks = []
if (argv.package === 'exe' || argv.package === 'all') {
tasks.push((cb) => packageInstaller(cb))
}
if (argv.package === 'portable' || argv.package === 'all') {
tasks.push((cb) => packagePortable(cb))
}
series(tasks, cb)
function packageInstaller (filesPath, destArch, cb) {
console.log(`Windows: Creating ${destArch} installer...`)
const archStr = destArch === 'ia32' ? '-ia32' : ''
function packageInstaller (cb) {
console.log('Windows: Creating installer...')
installer.createWindowsInstaller({
appDirectory: filesPath,
appDirectory: buildPath[0],
authors: config.APP_TEAM,
description: config.APP_NAME,
exe: config.APP_NAME + '.exe',
@@ -423,26 +410,8 @@ function buildWin32 (cb) {
noMsi: true,
outputDirectory: DIST_PATH,
productName: config.APP_NAME,
/**
* Only create delta updates for the Windows x64 build because 90% of our
* users have Windows x64 and the delta files take a *very* long time to
* generate. Also, the ia32 files on GitHub have non-standard Squirrel
* names (i.e. RELEASES-ia32 instead of RELEASES) and so Squirrel won't
* find them unless we proxy the requests.
*/
// TODO: Re-enable Windows 64-bit delta updates when we confirm that they
// work correctly in the presence of the "ia32" .nupkg files. I
// (feross) noticed them listed in the 64-bit RELEASES file and
// manually edited them out for the v0.17 release. Shipping only
// full updates for now will work fine, with no ill-effects.
// remoteReleases: destArch === 'x64'
// ? config.GITHUB_URL
// : undefined,
/**
* If you hit a "GitHub API rate limit exceeded" error, set this token!
*/
// remoteToken: process.env.WEBTORRENT_GITHUB_API_TOKEN,
setupExe: config.APP_NAME + 'Setup-v' + config.APP_VERSION + archStr + '.exe',
remoteReleases: config.GITHUB_URL,
setupExe: config.APP_NAME + 'Setup-v' + config.APP_VERSION + '.exe',
setupIcon: config.APP_ICON + '.ico',
signWithParams: signWithParams,
title: config.APP_NAME,
@@ -450,71 +419,23 @@ function buildWin32 (cb) {
version: pkg.version
})
.then(function () {
console.log(`Windows: Created ${destArch} installer.`)
/**
* Delete extraneous Squirrel files (i.e. *.nupkg delta files for older
* versions of the app)
*/
fs.readdirSync(DIST_PATH)
.filter((name) => name.endsWith('.nupkg') && !name.includes(pkg.version))
.forEach((filename) => {
fs.unlinkSync(path.join(DIST_PATH, filename))
})
if (destArch === 'ia32') {
console.log('Windows: Renaming ia32 installer files...')
// RELEASES -> RELEASES-ia32
const relPath = path.join(DIST_PATH, 'RELEASES-ia32')
fs.renameSync(
path.join(DIST_PATH, 'RELEASES'),
relPath
)
// WebTorrent-vX.X.X-full.nupkg -> WebTorrent-vX.X.X-ia32-full.nupkg
fs.renameSync(
path.join(DIST_PATH, `${config.APP_NAME}-${config.APP_VERSION}-full.nupkg`),
path.join(DIST_PATH, `${config.APP_NAME}-${config.APP_VERSION}-ia32-full.nupkg`)
)
// Change file name inside RELEASES-ia32 to match renamed file
const relContent = fs.readFileSync(relPath, 'utf8')
const relContent32 = relContent.replace('full.nupkg', 'ia32-full.nupkg')
fs.writeFileSync(relPath, relContent32)
if (relContent === relContent32) {
// Sanity check
throw new Error('Fixing RELEASES-ia32 failed. Replacement did not modify the file.')
}
console.log('Windows: Renamed ia32 installer files.')
}
console.log('Windows: Created installer.')
cb(null)
})
.catch(cb)
}
function packagePortable (filesPath, destArch, cb) {
console.log(`Windows: Creating ${destArch} portable app...`)
function packagePortable (cb) {
console.log('Windows: Creating portable app...')
const portablePath = path.join(filesPath, 'Portable Settings')
var portablePath = path.join(buildPath[0], 'Portable Settings')
mkdirp.sync(portablePath)
const downloadsPath = path.join(portablePath, 'Downloads')
mkdirp.sync(downloadsPath)
const tempPath = path.join(portablePath, 'Temp')
mkdirp.sync(tempPath)
const archStr = destArch === 'ia32' ? '-ia32' : ''
const inPath = path.join(DIST_PATH, path.basename(filesPath))
const outPath = path.join(DIST_PATH, BUILD_NAME + '-win' + archStr + '.zip')
var inPath = path.join(DIST_PATH, path.basename(buildPath[0]))
var outPath = path.join(DIST_PATH, BUILD_NAME + '-win.zip')
zip.zipSync(inPath, outPath)
console.log(`Windows: Created ${destArch} portable app.`)
console.log('Windows: Created portable app.')
cb(null)
}
})
@@ -526,9 +447,9 @@ function buildLinux (cb) {
if (err) return cb(err)
console.log('Linux: Packaged electron. ' + buildPath)
const tasks = []
var tasks = []
buildPath.forEach(function (filesPath) {
const destArch = filesPath.split('-').pop()
var destArch = filesPath.split('-').pop()
if (argv.package === 'deb' || argv.package === 'all') {
tasks.push((cb) => packageDeb(filesPath, destArch, cb))
@@ -544,8 +465,8 @@ function buildLinux (cb) {
// Create .deb file for Debian-based platforms
console.log(`Linux: Creating ${destArch} deb...`)
const deb = require('nobin-debian-installer')()
const destPath = path.join('/opt', pkg.name)
var deb = require('nobin-debian-installer')()
var destPath = path.join('/opt', pkg.name)
deb.pack({
package: pkg,
@@ -579,10 +500,8 @@ function buildLinux (cb) {
// Create .zip file for Linux
console.log(`Linux: Creating ${destArch} zip...`)
const archStr = destArch === 'ia32' ? '-ia32' : ''
const inPath = path.join(DIST_PATH, path.basename(filesPath))
const outPath = path.join(DIST_PATH, BUILD_NAME + '-linux' + archStr + '.zip')
var inPath = path.join(DIST_PATH, path.basename(filesPath))
var outPath = path.join(DIST_PATH, BUILD_NAME + '-linux-' + destArch + '.zip')
zip.zipSync(inPath, outPath)
console.log(`Linux: Created ${destArch} zip.`)

10
bin/release-_post.sh Executable file
View File

@@ -0,0 +1,10 @@
#!/bin/sh
set -e
npm run update-authors
git diff --exit-code
npm run package -- --sign
git push
git push --tags
npm publish
./node_modules/.bin/gh-release

8
bin/release-_pre.sh Executable file
View File

@@ -0,0 +1,8 @@
#!/bin/sh
set -e
git pull
rm -rf node_modules/
npm install
npm dedupe
npm test

7
bin/release-major.sh Executable file
View File

@@ -0,0 +1,7 @@
#!/bin/sh
set -e
BIN=`dirname $0`
$BIN/release-_pre.sh
npm version major
$BIN/release-_post.sh

7
bin/release-minor.sh Executable file
View File

@@ -0,0 +1,7 @@
#!/bin/sh
set -e
BIN=`dirname $0`
$BIN/release-_pre.sh
npm version minor
$BIN/release-_post.sh

7
bin/release-patch.sh Executable file
View File

@@ -0,0 +1,7 @@
#!/bin/sh
set -e
BIN=`dirname $0`
$BIN/release-_pre.sh
npm version patch
$BIN/release-_post.sh

View File

@@ -1,45 +1,41 @@
{
"name": "webtorrent-desktop",
"description": "WebTorrent, the streaming torrent client. For Mac, Windows, and Linux.",
"version": "0.19.0",
"version": "0.12.0",
"author": {
"name": "WebTorrent, LLC",
"email": "feross@webtorrent.io",
"url": "https://webtorrent.io"
},
"bin": {
"webtorrent-desktop": "./bin/cmd.js"
},
"bugs": {
"url": "https://github.com/webtorrent/webtorrent-desktop/issues"
"url": "https://github.com/feross/webtorrent-desktop/issues"
},
"dependencies": {
"airplayer": "^2.0.0",
"application-config": "^1.0.0",
"arch": "^2.0.0",
"auto-launch": "^4.0.1",
"bitfield": "^1.0.2",
"capture-frame": "^1.0.0",
"chokidar": "^1.6.1",
"chromecasts": "^1.8.0",
"cp-file": "^4.0.1",
"create-torrent": "^3.24.5",
"debounce": "^1.0.0",
"deep-equal": "^1.0.1",
"dlnacasts": "^0.1.0",
"drag-drop": "^2.12.1",
"es6-error": "^4.0.0",
"fn-getter": "^1.0.0",
"electron": "1.3.3",
"fs-extra": "^0.30.0",
"iso-639-1": "^1.2.1",
"languagedetect": "^1.1.1",
"location-history": "^1.0.0",
"material-ui": "^0.17.0",
"mkdirp": "^0.5.1",
"material-ui": "^0.15.4",
"matroska-subtitles": "^2.0.0",
"musicmetadata": "^2.0.2",
"network-address": "^1.1.0",
"parse-torrent": "^5.7.3",
"prettier-bytes": "^1.0.1",
"react": "^15.4.2",
"react-dom": "^15.4.2",
"react-tap-event-plugin": "^2.0.1",
"rimraf": "^2.5.2",
"react": "^15.2.1",
"react-dom": "^15.2.1",
"react-tap-event-plugin": "^1.0.0",
"run-parallel": "^1.1.6",
"semver": "^5.1.0",
"simple-concat": "^1.0.0",
@@ -51,25 +47,25 @@
"zero-fill": "^2.2.3"
},
"devDependencies": {
"buble": "^0.15.2",
"babel-cli": "^6.11.4",
"babel-plugin-syntax-jsx": "^6.13.0",
"babel-plugin-transform-es2015-destructuring": "^6.9.0",
"babel-plugin-transform-object-rest-spread": "^6.8.0",
"babel-plugin-transform-react-jsx": "^6.8.0",
"cross-zip": "^2.0.1",
"depcheck": "^0.6.4",
"electron": "1.6.16",
"electron-osx-sign": "0.4.3",
"electron-packager": "~8.5.1",
"electron-winstaller": "~2.5.2",
"electron-osx-sign": "^0.3.0",
"electron-packager": "^7.0.0",
"electron-winstaller": "^2.3.0",
"gh-release": "^2.0.3",
"minimist": "^1.2.0",
"nobin-debian-installer": "0.0.10",
"mkdirp": "^0.5.1",
"nobin-debian-installer": "^0.0.10",
"nodemon": "^1.10.2",
"open": "0.0.5",
"plist": "^2.0.1",
"pngjs": "^3.0.0",
"rimraf": "^2.5.2",
"run-series": "^1.1.4",
"spectron": "^3.3.0",
"standard": "*",
"tape": "^4.6.0",
"walk-sync": "^0.3.1"
"standard": "*"
},
"engines": {
"node": ">=4.0.0"
@@ -90,23 +86,20 @@
"optionalDependencies": {
"appdmg": "^0.4.3"
},
"private": true,
"productName": "WebTorrent",
"repository": {
"type": "git",
"url": "git://github.com/webtorrent/webtorrent-desktop.git"
"url": "git://github.com/feross/webtorrent-desktop.git"
},
"scripts": {
"build": "buble src --output build",
"build": "babel --quiet src --out-dir build",
"clean": "node ./bin/clean.js",
"gh-release": "gh-release",
"open-config": "node ./bin/open-config.js",
"package": "node ./bin/package.js",
"prepublish": "npm run build",
"start": "npm run build && electron .",
"test": "standard && depcheck --ignores=buble,nodemon,gh-release --ignore-dirs=build,dist && node ./bin/extra-lint.js",
"test-integration": "npm run build && node ./test",
"test": "standard && node ./bin/check-deps.js",
"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,pug,css"
}
}

8
src/.babelrc Normal file
View File

@@ -0,0 +1,8 @@
{
"plugins": [
"syntax-jsx",
"transform-es2015-destructuring",
"transform-object-rest-spread",
"transform-react-jsx"
]
}

View File

@@ -1,21 +1,12 @@
const appConfig = require('application-config')('WebTorrent')
const path = require('path')
const electron = require('electron')
const arch = require('arch')
var appConfig = require('application-config')('WebTorrent')
var fs = require('fs')
var path = require('path')
const APP_NAME = 'WebTorrent'
const APP_TEAM = 'WebTorrent, LLC'
const APP_VERSION = require('../package.json').version
var APP_NAME = 'WebTorrent'
var APP_TEAM = 'WebTorrent, LLC'
var APP_VERSION = require('../package.json').version
const IS_TEST = isTest()
const PORTABLE_PATH = IS_TEST
? path.join(process.platform === 'win32' ? 'C:\\Windows\\Temp' : '/tmp', 'WebTorrentTest')
: path.join(path.dirname(process.execPath), 'Portable Settings')
const IS_PRODUCTION = isProduction()
const IS_PORTABLE = isPortable()
const UI_HEADER_HEIGHT = 38
const UI_TORRENT_HEIGHT = 100
var PORTABLE_PATH = path.join(path.dirname(process.execPath), 'Portable Settings')
module.exports = {
ANNOUNCEMENT_URL: 'https://webtorrent.io/desktop/announcement',
@@ -23,7 +14,7 @@ module.exports = {
CRASH_REPORT_URL: 'https://webtorrent.io/desktop/crash-report',
TELEMETRY_URL: 'https://webtorrent.io/desktop/telemetry',
APP_COPYRIGHT: 'Copyright © 2014-2018 ' + APP_TEAM,
APP_COPYRIGHT: 'Copyright © 2014-2016 ' + APP_TEAM,
APP_FILE_ICON: path.join(__dirname, '..', 'static', 'WebTorrentFile'),
APP_ICON: path.join(__dirname, '..', 'static', 'WebTorrent'),
APP_NAME: APP_NAME,
@@ -35,32 +26,27 @@ module.exports = {
DEFAULT_TORRENTS: [
{
testID: 'bbb',
name: 'Big Buck Bunny',
posterFileName: 'bigBuckBunny.jpg',
torrentFileName: 'bigBuckBunny.torrent'
},
{
testID: 'cosmos',
name: 'Cosmos Laundromat (Preview)',
posterFileName: 'cosmosLaundromat.jpg',
torrentFileName: 'cosmosLaundromat.torrent'
},
{
testID: 'sintel',
name: 'Sintel',
posterFileName: 'sintel.jpg',
torrentFileName: 'sintel.torrent'
},
{
testID: 'tears',
name: 'Tears of Steel',
posterFileName: 'tearsOfSteel.jpg',
torrentFileName: 'tearsOfSteel.torrent'
},
{
testID: 'wired',
name: 'The WIRED CD - Rip. Sample. Mash. Share',
name: 'The WIRED CD - Rip. Sample. Mash. Share.',
posterFileName: 'wiredCd.jpg',
torrentFileName: 'wiredCd.torrent'
}
@@ -70,17 +56,14 @@ module.exports = {
DEFAULT_DOWNLOAD_PATH: getDefaultDownloadPath(),
GITHUB_URL: 'https://github.com/webtorrent/webtorrent-desktop',
GITHUB_URL_ISSUES: 'https://github.com/webtorrent/webtorrent-desktop/issues',
GITHUB_URL_RAW: 'https://raw.githubusercontent.com/webtorrent/webtorrent-desktop/master',
GITHUB_URL: 'https://github.com/feross/webtorrent-desktop',
GITHUB_URL_ISSUES: 'https://github.com/feross/webtorrent-desktop/issues',
GITHUB_URL_RAW: 'https://raw.githubusercontent.com/feross/webtorrent-desktop/master',
HOME_PAGE_URL: 'https://webtorrent.io',
IS_PORTABLE: IS_PORTABLE,
IS_PRODUCTION: IS_PRODUCTION,
IS_TEST: IS_TEST,
OS_SYSARCH: arch() === 'x64' ? 'x64' : 'ia32',
IS_PORTABLE: isPortable(),
IS_PRODUCTION: isProduction(),
POSTER_PATH: path.join(getConfigPath(), 'Posters'),
ROOT_PATH: path.join(__dirname, '..'),
@@ -91,19 +74,12 @@ module.exports = {
WINDOW_MAIN: 'file://' + path.join(__dirname, '..', 'static', 'main.html'),
WINDOW_WEBTORRENT: 'file://' + path.join(__dirname, '..', 'static', 'webtorrent.html'),
WINDOW_INITIAL_BOUNDS: {
width: 500,
height: UI_HEADER_HEIGHT + (UI_TORRENT_HEIGHT * 6) // header + 6 torrents
},
WINDOW_MIN_HEIGHT: UI_HEADER_HEIGHT + (UI_TORRENT_HEIGHT * 2), // header + 2 torrents
WINDOW_MIN_WIDTH: 425,
UI_HEADER_HEIGHT: UI_HEADER_HEIGHT,
UI_TORRENT_HEIGHT: UI_TORRENT_HEIGHT
WINDOW_MIN_HEIGHT: 38 + (120 * 2), // header height + 2 torrents
WINDOW_MIN_WIDTH: 425
}
function getConfigPath () {
if (IS_PORTABLE) {
if (isPortable()) {
return PORTABLE_PATH
} else {
return path.dirname(appConfig.filePath)
@@ -111,47 +87,24 @@ function getConfigPath () {
}
function getDefaultDownloadPath () {
if (IS_PORTABLE) {
return path.join(getConfigPath(), 'Downloads')
} else {
return getPath('downloads')
}
}
function getPath (key) {
if (!process.versions.electron) {
// Node.js process
if (!process || !process.type) {
return ''
} else if (process.type === 'renderer') {
// Electron renderer process
return electron.remote.app.getPath(key)
} else {
// Electron main process
return electron.app.getPath(key)
}
}
function isTest () {
return process.env.NODE_ENV === 'test'
if (isPortable()) {
return path.join(getConfigPath(), 'Downloads')
}
var electron = require('electron')
return process.type === 'renderer'
? electron.remote.app.getPath('downloads')
: electron.app.getPath('downloads')
}
function isPortable () {
if (IS_TEST) {
return true
}
if (process.platform !== 'win32' || !IS_PRODUCTION) {
// Fast path: Non-Windows platforms should not check for path on disk
return false
}
const fs = require('fs')
try {
// This line throws if the "Portable Settings" folder does not exist, and does
// nothing otherwise.
fs.accessSync(PORTABLE_PATH, fs.constants.R_OK | fs.constants.W_OK)
return true
return process.platform === 'win32' && isProduction() && !!fs.statSync(PORTABLE_PATH)
} catch (err) {
return false
}
@@ -159,7 +112,6 @@ function isPortable () {
function isProduction () {
if (!process.versions.electron) {
// Node.js process
return false
}
if (process.platform === 'darwin') {

View File

@@ -2,10 +2,10 @@ module.exports = {
init
}
function init () {
const config = require('./config')
const electron = require('electron')
var config = require('./config')
var electron = require('electron')
function init () {
electron.crashReporter.start({
companyName: config.APP_NAME,
productName: config.APP_NAME,

View File

@@ -2,12 +2,12 @@ module.exports = {
init
}
const electron = require('electron')
var electron = require('electron')
const config = require('../config')
const log = require('./log')
var config = require('../config')
var log = require('./log')
const ANNOUNCEMENT_URL = config.ANNOUNCEMENT_URL +
var ANNOUNCEMENT_URL = config.ANNOUNCEMENT_URL +
'?version=' + config.APP_VERSION +
'&platform=' + process.platform
@@ -26,7 +26,7 @@ const ANNOUNCEMENT_URL = config.ANNOUNCEMENT_URL +
* }
*/
function init () {
const get = require('simple-get')
var get = require('simple-get')
get.concat(ANNOUNCEMENT_URL, onResponse)
}

View File

@@ -6,10 +6,10 @@ module.exports = {
openFiles
}
const electron = require('electron')
var electron = require('electron')
const log = require('./log')
const windows = require('./windows')
var log = require('./log')
var windows = require('./windows')
/**
* Show open dialog to create a single-file torrent.
@@ -17,11 +17,16 @@ const windows = require('./windows')
function openSeedFile () {
if (!windows.main.win) return
log('openSeedFile')
const opts = {
var opts = {
title: 'Select a file for the torrent.',
properties: [ 'openFile' ]
}
showOpenSeed(opts)
setTitle(opts.title)
electron.dialog.showOpenDialog(windows.main.win, opts, function (selectedPaths) {
resetTitle()
if (!Array.isArray(selectedPaths)) return
windows.main.dispatch('showCreateTorrent', selectedPaths)
})
}
/*
@@ -32,7 +37,7 @@ function openSeedFile () {
function openSeedDirectory () {
if (!windows.main.win) return
log('openSeedDirectory')
const opts = process.platform === 'darwin'
var opts = process.platform === 'darwin'
? {
title: 'Select a file or folder for the torrent.',
properties: [ 'openFile', 'openDirectory' ]
@@ -41,7 +46,12 @@ function openSeedDirectory () {
title: 'Select a folder for the torrent.',
properties: [ 'openDirectory' ]
}
showOpenSeed(opts)
setTitle(opts.title)
electron.dialog.showOpenDialog(windows.main.win, opts, function (selectedPaths) {
resetTitle()
if (!Array.isArray(selectedPaths)) return
windows.main.dispatch('showCreateTorrent', selectedPaths)
})
}
/*
@@ -51,7 +61,7 @@ function openSeedDirectory () {
function openFiles () {
if (!windows.main.win) return
log('openFiles')
const opts = process.platform === 'darwin'
var opts = process.platform === 'darwin'
? {
title: 'Select a file or folder to add.',
properties: [ 'openFile', 'openDirectory' ]
@@ -74,7 +84,7 @@ function openFiles () {
function openTorrentFile () {
if (!windows.main.win) return
log('openTorrentFile')
const opts = {
var opts = {
title: 'Select a .torrent file.',
filters: [{ name: 'Torrent Files', extensions: ['torrent'] }],
properties: [ 'openFile', 'multiSelections' ]
@@ -109,16 +119,3 @@ function setTitle (title) {
function resetTitle () {
windows.main.dispatch('resetTitle')
}
/**
* Pops up an Open File dialog with the given options.
* After the user selects files / folders, shows the Create Torrent page.
*/
function showOpenSeed (opts) {
setTitle(opts.title)
electron.dialog.showOpenDialog(windows.main.win, opts, function (selectedPaths) {
resetTitle()
if (!Array.isArray(selectedPaths)) return
windows.main.dispatch('showCreateTorrent', selectedPaths)
})
}

View File

@@ -4,17 +4,19 @@ module.exports = {
setBadge
}
const {app, Menu} = require('electron')
var electron = require('electron')
const dialog = require('./dialog')
const log = require('./log')
var app = electron.app
var dialog = require('./dialog')
var log = require('./log')
/**
* Add a right-click menu to the dock icon. (Mac)
*/
function init () {
if (!app.dock) return
const menu = Menu.buildFromTemplate(getMenuTemplate())
var menu = electron.Menu.buildFromTemplate(getMenuTemplate())
app.dock.setMenu(menu)
}
@@ -31,11 +33,8 @@ function downloadFinished (path) {
* Display a counter badge for the app. (Mac, Linux)
*/
function setBadge (count) {
if (process.platform === 'darwin' ||
(process.platform === 'linux' && app.isUnityRunning())) {
log(`setBadge: ${count}`)
app.setBadgeCount(Number(count))
}
log(`setBadge: ${count}`)
app.setBadgeCount(Number(count))
}
function getMenuTemplate () {

View File

@@ -4,36 +4,29 @@ module.exports = {
checkInstall
}
const cp = require('child_process')
const path = require('path')
const vlcCommand = require('vlc-command')
var cp = require('child_process')
var vlcCommand = require('vlc-command')
const log = require('./log')
const windows = require('./windows')
var log = require('./log')
var windows = require('./windows')
// holds a ChildProcess while we're playing a video in an external player, null otherwise
let proc = null
var proc
function checkInstall (playerPath, cb) {
function checkInstall (path, cb) {
// check for VLC if external player has not been specified by the user
// otherwise assume the player is installed
if (playerPath == null) return vlcCommand(cb)
process.nextTick(() => cb(null))
if (path == null) return vlcCommand((err) => cb(!err))
process.nextTick(() => cb(true))
}
function spawn (playerPath, url, title) {
if (playerPath != null) return spawnExternal(playerPath, [url])
function spawn (path, url, title) {
if (path != null) return spawnExternal(path, [url])
// Try to find and use VLC if external player is not specified
vlcCommand(function (err, vlcPath) {
if (err) return windows.main.dispatch('externalPlayerNotFound')
const args = [
'--play-and-exit',
'--video-on-top',
'--quiet',
`--meta-title=${JSON.stringify(title)}`,
url
]
var args = ['--play-and-exit', '--video-on-top', '--quiet', `--meta-title=${JSON.stringify(title)}`, url]
spawnExternal(vlcPath, args)
})
}
@@ -45,18 +38,13 @@ function kill () {
proc = null
}
function spawnExternal (playerPath, args) {
log('Running external media player:', playerPath + ' ' + args.join(' '))
function spawnExternal (path, args) {
log('Running external media player:', path + ' ' + args.join(' '))
if (process.platform === 'darwin' && path.extname(playerPath) === '.app') {
// Mac: Use executable in packaged .app bundle
playerPath += '/Contents/MacOS/' + path.basename(playerPath, '.app')
}
proc = cp.spawn(playerPath, args, {stdio: 'ignore'})
proc = cp.spawn(path, args, {stdio: 'ignore'})
// If it works, close the modal after a second
const closeModalTimeout = setTimeout(() =>
var closeModalTimeout = setTimeout(() =>
windows.main.dispatch('exitModal'), 1000)
proc.on('close', function (code) {

View File

@@ -1,50 +0,0 @@
const chokidar = require('chokidar')
const log = require('./log')
class FolderWatcher {
constructor ({window, state}) {
this.window = window
this.state = state
this.torrentsFolderPath = null
this.watching = false
}
isEnabled () {
return this.state.saved.prefs.autoAddTorrents
}
start () {
// Stop watching previous folder before
// start watching a new one.
if (this.watching) this.stop()
const torrentsFolderPath = this.state.saved.prefs.torrentsFolderPath
this.torrentsFolderPath = torrentsFolderPath
if (!torrentsFolderPath) return
const glob = `${torrentsFolderPath}/**/*.torrent`
log('Folder Watcher: watching: ', glob)
const options = {
ignoreInitial: true,
awaitWriteFinish: true
}
this.watcher = chokidar.watch(glob, options)
this.watcher
.on('add', (path) => {
log('Folder Watcher: added torrent: ', path)
this.window.dispatch('addTorrent', path)
})
this.watching = true
}
stop () {
log('Folder Watcher: stop.')
if (!this.watching) return
this.watcher.close()
this.watching = false
}
}
module.exports = FolderWatcher

View File

@@ -3,8 +3,8 @@ module.exports = {
uninstall
}
const config = require('../config')
const path = require('path')
var config = require('../config')
var path = require('path')
function install () {
if (process.platform === 'darwin') {
@@ -31,8 +31,8 @@ function uninstall () {
}
function installDarwin () {
const electron = require('electron')
const app = electron.app
var electron = require('electron')
var app = electron.app
// On Mac, only protocols that are listed in `Info.plist` can be set as the
// default handler at runtime.
@@ -44,18 +44,18 @@ function installDarwin () {
function uninstallDarwin () {}
const EXEC_COMMAND = [ process.execPath, '--' ]
var EXEC_COMMAND = [ process.execPath ]
if (!config.IS_PRODUCTION) {
EXEC_COMMAND.push(config.ROOT_PATH)
}
function installWin32 () {
const Registry = require('winreg')
var Registry = require('winreg')
const log = require('./log')
var log = require('./log')
const iconPath = path.join(
var iconPath = path.join(
process.resourcesPath, 'app.asar.unpacked', 'static', 'WebTorrentFile.ico'
)
registerProtocolHandlerWin32(
@@ -100,7 +100,7 @@ function installWin32 () {
*/
function registerProtocolHandlerWin32 (protocol, name, icon, command) {
const protocolKey = new Registry({
var protocolKey = new Registry({
hive: Registry.HKCU, // HKEY_CURRENT_USER
key: '\\Software\\Classes\\' + protocol
})
@@ -120,7 +120,7 @@ function installWin32 () {
function setIcon (err) {
if (err) log.error(err.message)
const iconKey = new Registry({
var iconKey = new Registry({
hive: Registry.HKCU,
key: '\\Software\\Classes\\' + protocol + '\\DefaultIcon'
})
@@ -130,7 +130,7 @@ function installWin32 () {
function setCommand (err) {
if (err) log.error(err.message)
const commandKey = new Registry({
var commandKey = new Registry({
hive: Registry.HKCU,
key: '\\Software\\Classes\\' + protocol + '\\shell\\open\\command'
})
@@ -161,7 +161,7 @@ function installWin32 () {
setExt()
function setExt () {
const extKey = new Registry({
var extKey = new Registry({
hive: Registry.HKCU, // HKEY_CURRENT_USER
key: '\\Software\\Classes\\' + ext
})
@@ -171,7 +171,7 @@ function installWin32 () {
function setId (err) {
if (err) log.error(err.message)
const idKey = new Registry({
var idKey = new Registry({
hive: Registry.HKCU,
key: '\\Software\\Classes\\' + id
})
@@ -181,7 +181,7 @@ function installWin32 () {
function setIcon (err) {
if (err) log.error(err.message)
const iconKey = new Registry({
var iconKey = new Registry({
hive: Registry.HKCU,
key: '\\Software\\Classes\\' + id + '\\DefaultIcon'
})
@@ -191,7 +191,7 @@ function installWin32 () {
function setCommand (err) {
if (err) log.error(err.message)
const commandKey = new Registry({
var commandKey = new Registry({
hive: Registry.HKCU,
key: '\\Software\\Classes\\' + id + '\\shell\\open\\command'
})
@@ -205,7 +205,7 @@ function installWin32 () {
}
function uninstallWin32 () {
const Registry = require('winreg')
var Registry = require('winreg')
unregisterProtocolHandlerWin32('magnet', EXEC_COMMAND)
unregisterProtocolHandlerWin32('stream-magnet', EXEC_COMMAND)
@@ -215,7 +215,7 @@ function uninstallWin32 () {
getCommand()
function getCommand () {
const commandKey = new Registry({
var commandKey = new Registry({
hive: Registry.HKCU, // HKEY_CURRENT_USER
key: '\\Software\\Classes\\' + protocol + '\\shell\\open\\command'
})
@@ -227,7 +227,7 @@ function uninstallWin32 () {
}
function destroyProtocol () {
const protocolKey = new Registry({
var protocolKey = new Registry({
hive: Registry.HKCU,
key: '\\Software\\Classes\\' + protocol
})
@@ -239,7 +239,7 @@ function uninstallWin32 () {
eraseId()
function eraseId () {
const idKey = new Registry({
var idKey = new Registry({
hive: Registry.HKCU, // HKEY_CURRENT_USER
key: '\\Software\\Classes\\' + id
})
@@ -247,7 +247,7 @@ function uninstallWin32 () {
}
function getExt () {
const extKey = new Registry({
var extKey = new Registry({
hive: Registry.HKCU,
key: '\\Software\\Classes\\' + ext
})
@@ -259,7 +259,7 @@ function uninstallWin32 () {
}
function destroyExt () {
const extKey = new Registry({
var extKey = new Registry({
hive: Registry.HKCU, // HKEY_CURRENT_USER
key: '\\Software\\Classes\\' + ext
})
@@ -273,12 +273,12 @@ function commandToArgs (command) {
}
function installLinux () {
const fs = require('fs')
const os = require('os')
const path = require('path')
var fs = require('fs-extra')
var os = require('os')
var path = require('path')
const config = require('../config')
const log = require('./log')
var config = require('../config')
var log = require('./log')
// Do not install in user dir if running on system
if (/^\/opt/.test(process.execPath)) return
@@ -287,7 +287,7 @@ function installLinux () {
installIconFile()
function installDesktopFile () {
const templatePath = path.join(
var templatePath = path.join(
config.STATIC_PATH, 'linux', 'webtorrent-desktop.desktop'
)
fs.readFile(templatePath, 'utf8', writeDesktopFile)
@@ -296,7 +296,7 @@ function installLinux () {
function writeDesktopFile (err, desktopFile) {
if (err) return log.error(err.message)
const appPath = config.IS_PRODUCTION
var appPath = config.IS_PRODUCTION
? path.dirname(process.execPath)
: config.ROOT_PATH
@@ -305,7 +305,7 @@ function installLinux () {
desktopFile = desktopFile.replace(/\$EXEC_PATH/g, EXEC_COMMAND.join(' '))
desktopFile = desktopFile.replace(/\$TRY_EXEC_PATH/g, process.execPath)
const desktopFilePath = path.join(
var desktopFilePath = path.join(
os.homedir(),
'.local',
'share',
@@ -319,51 +319,47 @@ function installLinux () {
}
function installIconFile () {
const iconStaticPath = path.join(config.STATIC_PATH, 'WebTorrent.png')
var iconStaticPath = path.join(config.STATIC_PATH, 'WebTorrent.png')
fs.readFile(iconStaticPath, writeIconFile)
}
function writeIconFile (err, iconFile) {
if (err) return log.error(err.message)
const mkdirp = require('mkdirp')
const iconFilePath = path.join(
var iconFilePath = path.join(
os.homedir(),
'.local',
'share',
'icons',
'webtorrent-desktop.png'
)
mkdirp(path.dirname(iconFilePath), (err) => {
fs.mkdirp(path.dirname(iconFilePath))
fs.writeFile(iconFilePath, iconFile, function (err) {
if (err) return log.error(err.message)
fs.writeFile(iconFilePath, iconFile, (err) => {
if (err) log.error(err.message)
})
})
}
}
function uninstallLinux () {
const os = require('os')
const path = require('path')
const rimraf = require('rimraf')
var os = require('os')
var path = require('path')
var fs = require('fs-extra')
const desktopFilePath = path.join(
var desktopFilePath = path.join(
os.homedir(),
'.local',
'share',
'applications',
'webtorrent-desktop.desktop'
)
rimraf(desktopFilePath)
fs.removeSync(desktopFilePath)
const iconFilePath = path.join(
var iconFilePath = path.join(
os.homedir(),
'.local',
'share',
'icons',
'webtorrent-desktop.png'
)
rimraf(iconFilePath)
fs.removeSync(iconFilePath)
}

View File

@@ -1,25 +1,26 @@
console.time('init')
const electron = require('electron')
const app = electron.app
var electron = require('electron')
const parallel = require('run-parallel')
var app = electron.app
var ipcMain = electron.ipcMain
const config = require('../config')
const crashReporter = require('../crash-reporter')
const ipc = require('./ipc')
const log = require('./log')
const menu = require('./menu')
const State = require('../renderer/lib/state')
const windows = require('./windows')
var announcement = require('./announcement')
var config = require('../config')
var crashReporter = require('../crash-reporter')
var dialog = require('./dialog')
var dock = require('./dock')
var ipc = require('./ipc')
var log = require('./log')
var menu = require('./menu')
var squirrelWin32 = require('./squirrel-win32')
var tray = require('./tray')
var updater = require('./updater')
var userTasks = require('./user-tasks')
var windows = require('./windows')
let shouldQuit = false
let argv = sliceArgv(process.argv)
// Start the app without showing the main window when auto launching on login
// (On Windows and Linux, we get a flag. On MacOS, we get special API.)
const hidden = argv.includes('--hidden') ||
(process.platform === 'darwin' && app.getLoginItemSettings().wasOpenedAsHidden)
var shouldQuit = false
var argv = sliceArgv(process.argv)
if (config.IS_PRODUCTION) {
// When Electron is running in production mode (packaged app), then run React
@@ -28,16 +29,13 @@ if (config.IS_PRODUCTION) {
}
if (process.platform === 'win32') {
const squirrelWin32 = require('./squirrel-win32')
shouldQuit = squirrelWin32.handleEvent(argv[0])
argv = argv.filter((arg) => !arg.includes('--squirrel'))
}
if (!shouldQuit && !config.IS_PORTABLE) {
if (!shouldQuit) {
// Prevent multiple instances of app from running at same time. New instances
// signal this instance and quit. Note: This feature creates a lock file in
// %APPDATA%\Roaming\WebTorrent so we do not do it for the Portable App since
// we want to be "silent" as well as "portable".
// signal this instance and quit.
shouldQuit = app.makeSingleInstance(onAppOpen)
if (shouldQuit) {
app.quit()
@@ -50,47 +48,14 @@ if (!shouldQuit) {
function init () {
if (config.IS_PORTABLE) {
const path = require('path')
// Put all user data into the "Portable Settings" folder
app.setPath('userData', config.CONFIG_PATH)
// Put Electron crash files, etc. into the "Portable Settings\Temp" folder
app.setPath('temp', path.join(config.CONFIG_PATH, 'Temp'))
}
const ipcMain = electron.ipcMain
let isReady = false // app ready, windows can be created
var isReady = false // app ready, windows can be created
app.ipcReady = false // main window has finished loading and IPC is ready
app.isQuitting = false
parallel({
appReady: (cb) => app.on('ready', () => cb(null)),
state: (cb) => State.load(cb)
}, onReady)
function onReady (err, results) {
if (err) throw err
isReady = true
const state = results.state
windows.main.init(state, {hidden: hidden})
windows.webtorrent.init()
menu.init()
// To keep app startup fast, some code is delayed.
setTimeout(() => {
delayedInit(state)
}, config.DELAYED_INIT)
// Report uncaught exceptions
process.on('uncaughtException', (err) => {
console.error(err)
const error = {message: err.message, stack: err.stack}
windows.main.dispatch('uncaughtError', 'main', error)
})
}
// Open handlers must be added as early as possible
app.on('open-file', onOpen)
app.on('open-url', onOpen)
@@ -100,6 +65,24 @@ function init () {
crashReporter.init()
})
app.on('ready', function () {
isReady = true
windows.main.init()
windows.webtorrent.init()
menu.init()
// To keep app startup fast, some code is delayed.
setTimeout(delayedInit, config.DELAYED_INIT)
// Report uncaught exceptions
process.on('uncaughtException', (err) => {
console.error(err)
var error = {message: err.message, stack: err.stack}
windows.main.dispatch('uncaughtError', 'main', error)
})
})
app.once('ipcReady', function () {
log('Command line args:', argv)
processArgv(argv)
@@ -111,12 +94,12 @@ function init () {
app.isQuitting = true
e.preventDefault()
windows.main.dispatch('stateSaveImmediate') // try to save state on exit
ipcMain.once('stateSaved', () => app.quit())
windows.main.dispatch('saveState') // try to save state on exit
ipcMain.once('savedState', () => app.quit())
setTimeout(() => {
console.error('Saving state took too long. Quitting.')
app.quit()
}, 4000) // quit after 4 secs, at most
}, 2000) // quit after 2 secs, at most
})
app.on('activate', function () {
@@ -124,33 +107,12 @@ function init () {
})
}
function delayedInit (state) {
if (app.isQuitting) return
const announcement = require('./announcement')
const dock = require('./dock')
const updater = require('./updater')
const FolderWatcher = require('./folder-watcher')
const folderWatcher = new FolderWatcher({window: windows.main, state})
function delayedInit () {
announcement.init()
dock.init()
tray.init()
updater.init()
ipc.setModule('folderWatcher', folderWatcher)
if (folderWatcher.isEnabled()) {
folderWatcher.start()
}
if (process.platform === 'win32') {
const userTasks = require('./user-tasks')
userTasks.init()
}
if (process.platform !== 'darwin') {
const tray = require('./tray')
tray.init()
}
userTasks.init()
}
function onOpen (e, torrentId) {
@@ -181,38 +143,22 @@ function onAppOpen (newArgv) {
}
}
// Remove leading args.
// Production: 1 arg, eg: /Applications/WebTorrent.app/Contents/MacOS/WebTorrent
// Development: 2 args, eg: electron .
// Test: 4 args, eg: electron -r .../mocks.js .
function sliceArgv (argv) {
return argv.slice(config.IS_PRODUCTION ? 1
: config.IS_TEST ? 4
: 2)
return argv.slice(config.IS_PRODUCTION ? 1 : 2)
}
function processArgv (argv) {
let torrentIds = []
var torrentIds = []
argv.forEach(function (arg) {
if (arg === '-n' || arg === '-o' || arg === '-u') {
// Critical path: Only load the 'dialog' package if it is needed
const dialog = require('./dialog')
if (arg === '-n') {
dialog.openSeedDirectory()
} else if (arg === '-o') {
dialog.openTorrentFile()
} else if (arg === '-u') {
dialog.openTorrentAddress()
}
} else if (arg === '--hidden') {
// Ignore hidden argument, already being handled
if (arg === '-n') {
dialog.openSeedDirectory()
} else if (arg === '-o') {
dialog.openTorrentFile()
} else if (arg === '-u') {
dialog.openTorrentAddress()
} else if (arg.startsWith('-psn')) {
// Ignore Mac launchd "process serial number" argument
// Issue: https://github.com/webtorrent/webtorrent-desktop/issues/214
} else if (arg.startsWith('--')) {
// Ignore Spectron flags
} else if (arg === 'data:,') {
// Ignore weird Spectron argument
// Issue: https://github.com/feross/webtorrent-desktop/issues/214
} else if (arg !== '.') {
// Ignore '.' argument, which gets misinterpreted as a torrent id, when a
// development copy of WebTorrent is started while a production version is

View File

@@ -1,29 +1,28 @@
module.exports = {
init,
setModule
init
}
const electron = require('electron')
var electron = require('electron')
const app = electron.app
var app = electron.app
const log = require('./log')
const menu = require('./menu')
const windows = require('./windows')
var dialog = require('./dialog')
var dock = require('./dock')
var handlers = require('./handlers')
var log = require('./log')
var menu = require('./menu')
var powerSaveBlocker = require('./power-save-blocker')
var shell = require('./shell')
var shortcuts = require('./shortcuts')
var externalPlayer = require('./external-player')
var windows = require('./windows')
var thumbar = require('./thumbar')
// Messages from the main process, to be sent once the WebTorrent process starts
const messageQueueMainToWebTorrent = []
// Will hold modules injected from the app that will be used on fired
// IPC events.
const modules = {}
function setModule (name, module) {
modules[name] = module
}
var messageQueueMainToWebTorrent = []
function init () {
const ipc = electron.ipcMain
var ipc = electron.ipcMain
ipc.once('ipcReady', function (e) {
app.ipcReady = true
@@ -44,143 +43,65 @@ function init () {
* Dialog
*/
ipc.on('openTorrentFile', () => {
const dialog = require('./dialog')
dialog.openTorrentFile()
})
ipc.on('openFiles', () => {
const dialog = require('./dialog')
dialog.openFiles()
})
ipc.on('openTorrentFile', () => dialog.openTorrentFile())
ipc.on('openFiles', () => dialog.openFiles())
/**
* Dock
*/
ipc.on('setBadge', (e, ...args) => {
const dock = require('./dock')
dock.setBadge(...args)
})
ipc.on('downloadFinished', (e, ...args) => {
const dock = require('./dock')
dock.downloadFinished(...args)
})
ipc.on('setBadge', (e, ...args) => dock.setBadge(...args))
ipc.on('downloadFinished', (e, ...args) => dock.downloadFinished(...args))
/**
* Player Events
* Events
*/
ipc.on('onPlayerOpen', function () {
const powerSaveBlocker = require('./power-save-blocker')
const shortcuts = require('./shortcuts')
const thumbar = require('./thumbar')
menu.togglePlaybackControls(true)
menu.setPlayerOpen(true)
powerSaveBlocker.enable()
shortcuts.enable()
thumbar.enable()
})
ipc.on('onPlayerUpdate', function (e, ...args) {
const thumbar = require('./thumbar')
menu.onPlayerUpdate(...args)
thumbar.onPlayerUpdate(...args)
})
ipc.on('onPlayerClose', function () {
const powerSaveBlocker = require('./power-save-blocker')
const shortcuts = require('./shortcuts')
const thumbar = require('./thumbar')
menu.togglePlaybackControls(false)
menu.setPlayerOpen(false)
powerSaveBlocker.disable()
shortcuts.disable()
thumbar.disable()
})
ipc.on('onPlayerPlay', function () {
const powerSaveBlocker = require('./power-save-blocker')
const thumbar = require('./thumbar')
powerSaveBlocker.enable()
thumbar.onPlayerPlay()
})
ipc.on('onPlayerPause', function () {
const powerSaveBlocker = require('./power-save-blocker')
const thumbar = require('./thumbar')
powerSaveBlocker.disable()
thumbar.onPlayerPause()
})
/**
* Folder Watcher Events
*/
ipc.on('startFolderWatcher', function () {
if (!modules['folderWatcher']) {
log('IPC ERR: folderWatcher module is not defined.')
return
}
modules['folderWatcher'].start()
})
ipc.on('stopFolderWatcher', function () {
if (!modules['folderWatcher']) {
log('IPC ERR: folderWatcher module is not defined.')
return
}
modules['folderWatcher'].stop()
})
/**
* Shell
*/
ipc.on('openItem', (e, ...args) => {
const shell = require('./shell')
shell.openItem(...args)
})
ipc.on('showItemInFolder', (e, ...args) => {
const shell = require('./shell')
shell.showItemInFolder(...args)
})
ipc.on('moveItemToTrash', (e, ...args) => {
const shell = require('./shell')
shell.moveItemToTrash(...args)
})
ipc.on('openItem', (e, ...args) => shell.openItem(...args))
ipc.on('showItemInFolder', (e, ...args) => shell.showItemInFolder(...args))
ipc.on('moveItemToTrash', (e, ...args) => shell.moveItemToTrash(...args))
/**
* File handlers
*/
ipc.on('setDefaultFileHandler', (e, flag) => {
const handlers = require('./handlers')
if (flag) handlers.install()
else handlers.uninstall()
})
/**
* Auto start on login
*/
ipc.on('setStartup', (e, flag) => {
const startup = require('./startup')
if (flag) startup.install()
else startup.uninstall()
})
/**
* Windows: Main
*/
const main = windows.main
var main = windows.main
ipc.on('setAspectRatio', (e, ...args) => main.setAspectRatio(...args))
ipc.on('setBounds', (e, ...args) => main.setBounds(...args))
@@ -195,32 +116,16 @@ function init () {
*/
ipc.on('checkForExternalPlayer', function (e, path) {
const externalPlayer = require('./external-player')
externalPlayer.checkInstall(path, function (err) {
windows.main.send('checkForExternalPlayer', !err)
externalPlayer.checkInstall(path, function (isInstalled) {
windows.main.send('checkForExternalPlayer', isInstalled)
})
})
ipc.on('openExternalPlayer', (e, ...args) => {
const externalPlayer = require('./external-player')
const thumbar = require('./thumbar')
ipc.on('openExternalPlayer', (e, ...args) => externalPlayer.spawn(...args))
ipc.on('quitExternalPlayer', () => externalPlayer.kill())
menu.togglePlaybackControls(false)
thumbar.disable()
externalPlayer.spawn(...args)
})
ipc.on('quitExternalPlayer', () => {
const externalPlayer = require('./external-player')
externalPlayer.kill()
})
/**
* Message passing
*/
const oldEmit = ipc.emit
// Capture all events
var oldEmit = ipc.emit
ipc.emit = function (name, e, ...args) {
// Relay messages between the main window and the WebTorrent hidden window
if (name.startsWith('wt-') && !app.isQuitting) {

View File

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

View File

@@ -1,31 +1,30 @@
module.exports = {
init,
togglePlaybackControls,
setPlayerOpen,
setWindowFocus,
setAllowNav,
onPlayerUpdate,
onToggleAlwaysOnTop,
onToggleFullScreen
}
const electron = require('electron')
var electron = require('electron')
const app = electron.app
var app = electron.app
const config = require('../config')
const windows = require('./windows')
var config = require('../config')
var dialog = require('./dialog')
var shell = require('./shell')
var windows = require('./windows')
let menu = null
var menu
function init () {
menu = electron.Menu.buildFromTemplate(getMenuTemplate())
electron.Menu.setApplicationMenu(menu)
}
function togglePlaybackControls (flag) {
function setPlayerOpen (flag) {
getMenuItem('Play/Pause').enabled = flag
getMenuItem('Skip Next').enabled = flag
getMenuItem('Skip Previous').enabled = flag
getMenuItem('Increase Volume').enabled = flag
getMenuItem('Decrease Volume').enabled = flag
getMenuItem('Step Forward').enabled = flag
@@ -33,16 +32,6 @@ function togglePlaybackControls (flag) {
getMenuItem('Increase Speed').enabled = flag
getMenuItem('Decrease Speed').enabled = flag
getMenuItem('Add Subtitles File...').enabled = flag
if (flag === false) {
getMenuItem('Skip Next').enabled = false
getMenuItem('Skip Previous').enabled = false
}
}
function onPlayerUpdate (hasNext, hasPrevious) {
getMenuItem('Skip Next').enabled = hasNext
getMenuItem('Skip Previous').enabled = hasPrevious
}
function setWindowFocus (flag) {
@@ -70,8 +59,8 @@ function onToggleFullScreen (flag) {
}
function getMenuItem (label) {
for (let i = 0; i < menu.items.length; i++) {
const menuItem = menu.items[i].submenu.items.find(function (item) {
for (var i = 0; i < menu.items.length; i++) {
var menuItem = menu.items[i].submenu.items.find(function (item) {
return item.label === label
})
if (menuItem) return menuItem
@@ -79,7 +68,7 @@ function getMenuItem (label) {
}
function getMenuTemplate () {
const template = [
var template = [
{
label: 'File',
submenu: [
@@ -88,26 +77,17 @@ function getMenuTemplate () {
? 'Create New Torrent...'
: 'Create New Torrent from Folder...',
accelerator: 'CmdOrCtrl+N',
click: () => {
const dialog = require('./dialog')
dialog.openSeedDirectory()
}
click: () => dialog.openSeedDirectory()
},
{
label: 'Open Torrent File...',
accelerator: 'CmdOrCtrl+O',
click: () => {
const dialog = require('./dialog')
dialog.openTorrentFile()
}
click: () => dialog.openTorrentFile()
},
{
label: 'Open Torrent Address...',
accelerator: 'CmdOrCtrl+U',
click: () => {
const dialog = require('./dialog')
dialog.openTorrentAddress()
}
click: () => dialog.openTorrentAddress()
},
{
type: 'separator'
@@ -207,21 +187,6 @@ function getMenuTemplate () {
{
type: 'separator'
},
{
label: 'Skip Next',
accelerator: 'N',
click: () => windows.main.dispatch('nextTrack'),
enabled: false
},
{
label: 'Skip Previous',
accelerator: 'P',
click: () => windows.main.dispatch('previousTrack'),
enabled: false
},
{
type: 'separator'
},
{
label: 'Increase Volume',
accelerator: 'CmdOrCtrl+Up',
@@ -278,53 +243,31 @@ function getMenuTemplate () {
}
]
},
{
label: 'Transfers',
submenu: [
{
label: 'Pause All',
click: () => windows.main.dispatch('pauseAllTorrents')
},
{
label: 'Resume All',
click: () => windows.main.dispatch('resumeAllTorrents')
}
]
},
{
label: 'Help',
role: 'help',
submenu: [
{
label: 'Learn more about ' + config.APP_NAME,
click: () => {
const shell = require('./shell')
shell.openExternal(config.HOME_PAGE_URL)
}
click: () => shell.openExternal(config.HOME_PAGE_URL)
},
{
label: 'Contribute on GitHub',
click: () => {
const shell = require('./shell')
shell.openExternal(config.GITHUB_URL)
}
click: () => shell.openExternal(config.GITHUB_URL)
},
{
type: 'separator'
},
{
label: 'Report an Issue...',
click: () => {
const shell = require('./shell')
shell.openExternal(config.GITHUB_URL_ISSUES)
}
click: () => shell.openExternal(config.GITHUB_URL_ISSUES)
}
]
}
]
if (process.platform === 'darwin') {
// WebTorrent menu (Mac)
// Add WebTorrent app menu (Mac)
template.unshift({
label: config.APP_NAME,
submenu: [
@@ -367,26 +310,8 @@ function getMenuTemplate () {
]
})
// Edit menu (Mac)
template[2].submenu.push(
{
type: 'separator'
},
{
label: 'Speech',
submenu: [
{
role: 'startspeaking'
},
{
role: 'stopspeaking'
}
]
}
)
// Window menu (Mac)
template.splice(6, 0, {
// Add Window menu (Mac)
template.splice(5, 0, {
role: 'window',
submenu: [
{
@@ -408,10 +333,7 @@ function getMenuTemplate () {
// File menu (Windows, Linux)
template[0].submenu.unshift({
label: 'Create New Torrent from File...',
click: () => {
const dialog = require('./dialog')
dialog.openSeedFile()
}
click: () => dialog.openSeedFile()
})
// Edit menu (Windows, Linux)
@@ -426,7 +348,7 @@ function getMenuTemplate () {
})
// Help menu (Windows, Linux)
template[5].submenu.push(
template[4].submenu.push(
{
type: 'separator'
},

View File

@@ -3,10 +3,10 @@ module.exports = {
disable
}
const electron = require('electron')
const log = require('./log')
var electron = require('electron')
var log = require('./log')
let blockId = 0
var blockId = 0
/**
* Block the system from entering low-power (sleep) mode or turning off the

View File

@@ -5,8 +5,8 @@ module.exports = {
moveItemToTrash
}
const electron = require('electron')
const log = require('./log')
var electron = require('electron')
var log = require('./log')
/**
* Open the given external protocol URL in the desktops default manner.

View File

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

View File

@@ -2,18 +2,18 @@ module.exports = {
handleEvent
}
const cp = require('child_process')
const electron = require('electron')
const fs = require('fs')
const os = require('os')
const path = require('path')
var cp = require('child_process')
var electron = require('electron')
var fs = require('fs')
var os = require('os')
var path = require('path')
const app = electron.app
var app = electron.app
const handlers = require('./handlers')
var handlers = require('./handlers')
const EXE_NAME = path.basename(process.execPath)
const UPDATE_EXE = path.join(process.execPath, '..', '..', 'Update.exe')
var EXE_NAME = path.basename(process.execPath)
var UPDATE_EXE = path.join(process.execPath, '..', '..', 'Update.exe')
function handleEvent (cmd) {
if (cmd === '--squirrel-install') {
@@ -73,9 +73,9 @@ function handleEvent (cmd) {
* the output from standard out.
*/
function spawn (command, args, cb) {
let stdout = ''
let error = null
let child = null
var stdout = ''
var child
try {
child = cp.spawn(command, args)
} catch (err) {
@@ -90,10 +90,10 @@ function spawn (command, args, cb) {
stdout += data
})
var error = null
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
@@ -122,12 +122,12 @@ function createShortcuts (cb) {
* command.
*/
function updateShortcuts (cb) {
const homeDir = os.homedir()
var homeDir = os.homedir()
if (homeDir) {
const desktopShortcutPath = path.join(homeDir, 'Desktop', 'WebTorrent.lnk')
var 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
var desktopShortcutExists = !err
createShortcuts(function () {
if (desktopShortcutExists) {
cb()

View File

@@ -1,36 +0,0 @@
module.exports = {
install,
uninstall
}
const config = require('../config')
const AutoLaunch = require('auto-launch')
const { app } = require('electron')
// On Mac, work around a bug in auto-launch where it opens a Terminal window
// See https://github.com/Teamwork/node-auto-launch/issues/28#issuecomment-222194437
const appPath = process.platform === 'darwin'
? app.getPath('exe').replace(/\.app\/Content.*/, '.app')
: undefined // Use the default
const appLauncher = new AutoLaunch({
name: config.APP_NAME,
path: appPath,
isHidden: true
})
function install () {
return appLauncher
.isEnabled()
.then(enabled => {
if (!enabled) return appLauncher.enable()
})
}
function uninstall () {
return appLauncher
.isEnabled()
.then(enabled => {
if (enabled) return appLauncher.disable()
})
}

View File

@@ -2,8 +2,7 @@ module.exports = {
disable,
enable,
onPlayerPause,
onPlayerPlay,
onPlayerUpdate
onPlayerPlay
}
/**
@@ -12,80 +11,44 @@ module.exports = {
* or activating the window.
*/
const path = require('path')
const config = require('../config')
var path = require('path')
var config = require('../config')
const windows = require('./windows')
const PREV_ICON = path.join(config.STATIC_PATH, 'PreviousTrackThumbnailBarButton.png')
const PLAY_ICON = path.join(config.STATIC_PATH, 'PlayThumbnailBarButton.png')
const PAUSE_ICON = path.join(config.STATIC_PATH, 'PauseThumbnailBarButton.png')
const NEXT_ICON = path.join(config.STATIC_PATH, 'NextTrackThumbnailBarButton.png')
// Array indices for each button
const PREV = 0
const PLAY_PAUSE = 1
const NEXT = 2
let buttons = []
var windows = require('./windows')
/**
* Show the Windows thumbnail toolbar buttons.
*/
function enable () {
buttons = [
{
tooltip: 'Previous Track',
icon: PREV_ICON,
click: () => windows.main.dispatch('previousTrack')
},
{
tooltip: 'Pause',
icon: PAUSE_ICON,
click: () => windows.main.dispatch('playPause')
},
{
tooltip: 'Next Track',
icon: NEXT_ICON,
click: () => windows.main.dispatch('nextTrack')
}
]
update()
update(false)
}
/**
* Hide the Windows thumbnail toolbar buttons.
*/
function disable () {
buttons = []
update()
windows.main.win.setThumbarButtons([])
}
function onPlayerPause () {
if (!isEnabled()) return
buttons[PLAY_PAUSE].tooltip = 'Play'
buttons[PLAY_PAUSE].icon = PLAY_ICON
update()
update(true)
}
function onPlayerPlay () {
if (!isEnabled()) return
buttons[PLAY_PAUSE].tooltip = 'Pause'
buttons[PLAY_PAUSE].icon = PAUSE_ICON
update()
update(false)
}
function onPlayerUpdate (state) {
if (!isEnabled()) return
buttons[PREV].flags = [ state.hasPrevious ? 'enabled' : 'disabled' ]
buttons[NEXT].flags = [ state.hasNext ? 'enabled' : 'disabled' ]
update()
}
function update (isPaused) {
var icon = isPaused
? 'PlayThumbnailBarButton.png'
: 'PauseThumbnailBarButton.png'
function isEnabled () {
return buttons.length > 0
}
function update () {
var buttons = [
{
tooltip: isPaused ? 'Play' : 'Pause',
icon: path.join(config.STATIC_PATH, icon),
click: () => windows.main.dispatch('playPause')
}
]
windows.main.win.setThumbarButtons(buttons)
}

View File

@@ -4,14 +4,14 @@ module.exports = {
setWindowFocus
}
const electron = require('electron')
var electron = require('electron')
const app = electron.app
var app = electron.app
const config = require('../config')
const windows = require('./windows')
var config = require('../config')
var windows = require('./windows')
let tray
var tray
function init () {
if (process.platform === 'linux') {
@@ -36,8 +36,8 @@ function setWindowFocus (flag) {
}
function initLinux () {
checkLinuxTraySupport(function (err) {
if (!err) createTray()
checkLinuxTraySupport(function (supportsTray) {
if (supportsTray) createTray()
})
}
@@ -49,20 +49,16 @@ function initWin32 () {
* Check for libappindicator1 support before creating tray icon
*/
function checkLinuxTraySupport (cb) {
const cp = require('child_process')
var cp = require('child_process')
// Check that we're on Ubuntu (or another debian system) and that we have
// 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(false)
// 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'))
}
cb(stdout.endsWith('\tinstall\n'))
})
}
@@ -78,7 +74,7 @@ function createTray () {
}
function updateTrayMenu () {
const contextMenu = electron.Menu.buildFromTemplate(getMenuTemplate())
var contextMenu = electron.Menu.buildFromTemplate(getMenuTemplate())
tray.setContextMenu(contextMenu)
}

View File

@@ -2,17 +2,16 @@ module.exports = {
init
}
const electron = require('electron')
const get = require('simple-get')
var electron = require('electron')
var get = require('simple-get')
const config = require('../config')
const log = require('./log')
const windows = require('./windows')
var config = require('../config')
var log = require('./log')
var windows = require('./windows')
const AUTO_UPDATE_URL = config.AUTO_UPDATE_URL +
var AUTO_UPDATE_URL = config.AUTO_UPDATE_URL +
'?version=' + config.APP_VERSION +
'&platform=' + process.platform +
'&sysarch=' + config.OS_SYSARCH
'&platform=' + process.platform
function init () {
if (process.platform === 'linux') {

View File

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

View File

@@ -1,17 +1,17 @@
const about = module.exports = {
var about = module.exports = {
init,
win: null
}
const config = require('../../config')
const electron = require('electron')
var config = require('../../config')
var electron = require('electron')
function init () {
if (about.win) {
return about.win.show()
}
const win = about.win = new electron.BrowserWindow({
var win = about.win = new electron.BrowserWindow({
backgroundColor: '#ECECEC',
center: true,
fullscreen: false,
@@ -32,7 +32,7 @@ function init () {
// No menu on the About window
win.setMenu(null)
win.once('ready-to-show', function () {
win.webContents.once('did-finish-load', function () {
win.show()
})

View File

@@ -1,4 +1,4 @@
const main = module.exports = {
var main = module.exports = {
dispatch,
hide,
init,
@@ -14,59 +14,43 @@ const main = module.exports = {
win: null
}
const electron = require('electron')
const debounce = require('debounce')
var electron = require('electron')
const app = electron.app
var app = electron.app
const config = require('../../config')
const log = require('../log')
const menu = require('../menu')
var config = require('../../config')
var log = require('../log')
var menu = require('../menu')
var tray = require('../tray')
function init (state, options) {
var HEADER_HEIGHT = 38
var TORRENT_HEIGHT = 100
function init () {
if (main.win) {
return main.win.show()
}
const initialBounds = Object.assign(config.WINDOW_INITIAL_BOUNDS, state.saved.bounds)
const win = main.win = new electron.BrowserWindow({
var win = main.win = new electron.BrowserWindow({
backgroundColor: '#282828',
backgroundThrottling: false, // do not throttle animations/timers when page is background
darkTheme: true, // Forces dark theme (GTK+3)
height: initialBounds.height,
icon: getIconPath(), // Window icon (Windows, Linux)
minHeight: config.WINDOW_MIN_HEIGHT,
minWidth: config.WINDOW_MIN_WIDTH,
show: false,
minHeight: config.WINDOW_MIN_HEIGHT,
title: config.APP_WINDOW_TITLE,
titleBarStyle: 'hidden-inset', // Hide title bar (Mac)
useContentSize: true, // Specify web page size without OS chrome
width: initialBounds.width,
x: initialBounds.x,
y: initialBounds.y
width: 500,
height: HEADER_HEIGHT + (TORRENT_HEIGHT * 6) // header height + 5 torrents
})
win.loadURL(config.WINDOW_MAIN)
win.once('ready-to-show', () => {
if (!options.hidden) win.show()
})
if (win.setSheetOffset) {
win.setSheetOffset(config.UI_HEADER_HEIGHT)
}
if (win.setSheetOffset) win.setSheetOffset(HEADER_HEIGHT)
win.webContents.on('dom-ready', function () {
menu.onToggleFullScreen(main.win.isFullScreen())
})
win.webContents.on('will-navigate', (e, url) => {
// Prevent drag-and-drop from navigating the Electron window, which can happen
// before our drag-and-drop handlers have been initialized.
e.preventDefault()
})
win.on('blur', onWindowBlur)
win.on('focus', onWindowFocus)
@@ -85,23 +69,10 @@ function init (state, options) {
win.setMenuBarVisibility(true)
})
win.on('move', debounce(function (e) {
send('windowBoundsChanged', e.sender.getBounds())
}, 1000))
win.on('resize', debounce(function (e) {
send('windowBoundsChanged', e.sender.getBounds())
}, 1000))
win.on('close', function (e) {
if (process.platform !== 'darwin') {
const tray = require('../tray')
if (!tray.hasTray()) {
app.quit()
return
}
}
if (!app.isQuitting) {
if (process.platform !== 'darwin' && !tray.hasTray()) {
app.quit()
} else if (!app.isQuitting) {
e.preventDefault()
hide()
}
@@ -114,7 +85,7 @@ function dispatch (...args) {
function hide () {
if (!main.win) return
dispatch('backToList')
main.win.send('dispatch', 'backToList')
main.win.hide()
}
@@ -143,7 +114,7 @@ function setBounds (bounds, maximize) {
}
// Maximize or minimize, if the second argument is present
let willBeMaximized
var willBeMaximized
if (maximize === true) {
if (!main.win.isMaximized()) {
log('setBounds: maximizing')
@@ -165,9 +136,9 @@ function setBounds (bounds, maximize) {
log('setBounds: setting bounds to ' + JSON.stringify(bounds))
if (bounds.x === null && bounds.y === null) {
// X and Y not specified? By default, center on current screen
const scr = electron.screen.getDisplayMatching(main.win.getBounds())
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))
var scr = electron.screen.getDisplayMatching(main.win.getBounds())
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)
log('setBounds: centered to ' + JSON.stringify(bounds))
}
// Resize the window's content area (so window border doesn't need to be taken
@@ -240,20 +211,12 @@ function toggleFullScreen (flag) {
function onWindowBlur () {
menu.setWindowFocus(false)
if (process.platform !== 'darwin') {
const tray = require('../tray')
tray.setWindowFocus(false)
}
tray.setWindowFocus(false)
}
function onWindowFocus () {
menu.setWindowFocus(true)
if (process.platform !== 'darwin') {
const tray = require('../tray')
tray.setWindowFocus(true)
}
tray.setWindowFocus(true)
}
function getIconPath () {

View File

@@ -1,4 +1,4 @@
const webtorrent = module.exports = {
var webtorrent = module.exports = {
init,
send,
show,
@@ -6,14 +6,14 @@ const webtorrent = module.exports = {
win: null
}
const electron = require('electron')
var electron = require('electron')
const config = require('../../config')
var config = require('../../config')
var log = require('../log')
function init () {
const win = webtorrent.win = new electron.BrowserWindow({
var win = webtorrent.win = new electron.BrowserWindow({
backgroundColor: '#1E1E1E',
backgroundThrottling: false, // do not throttle animations/timers when page is background
center: true,
fullscreen: false,
fullscreenable: false,
@@ -52,6 +52,7 @@ function send (...args) {
function toggleDevTools () {
if (!webtorrent.win) return
log('toggleDevTools')
if (webtorrent.win.webContents.isDevToolsOpened()) {
webtorrent.win.webContents.closeDevTools()
webtorrent.win.hide()

View File

@@ -17,14 +17,15 @@ class Heading extends React.Component {
render () {
const HeadingTag = 'h' + this.props.level
const style = {
color: colors.grey100,
fontSize: 20,
marginBottom: 15,
marginTop: 30
}
return (
<HeadingTag style={style}>
<HeadingTag
style={{
color: colors.grey100,
fontSize: 20,
marginBottom: 15,
marginTop: 30
}}
>
{this.props.children}
</HeadingTag>
)

View File

@@ -7,9 +7,6 @@ const remote = electron.remote
const RaisedButton = require('material-ui/RaisedButton').default
const TextField = require('material-ui/TextField').default
// Lets you pick a file or directory.
// Uses the system Open File dialog.
// You can't edit the text field directly.
class PathSelector extends React.Component {
static get propTypes () {
return {
@@ -29,7 +26,7 @@ class PathSelector extends React.Component {
}
handleClick () {
const opts = Object.assign({
var opts = Object.assign({
defaultPath: this.props.value,
properties: [ 'openFile', 'openDirectory' ]
}, this.props.dialog)
@@ -46,38 +43,48 @@ class PathSelector extends React.Component {
render () {
const id = this.props.title.replace(' ', '-').toLowerCase()
const wrapperStyle = {
alignItems: 'center',
display: 'flex',
width: '100%'
}
const labelStyle = {
flex: '0 auto',
marginRight: 10,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap'
}
const textareaStyle = {
color: colors.grey50
}
const textFieldStyle = {
flex: '1'
}
const text = this.props.displayValue || this.props.value
const buttonStyle = {
marginLeft: 10
}
return (
<div className={this.props.className} style={wrapperStyle}>
<div className='label' style={labelStyle}>
<div
className={this.props.className}
style={{
alignItems: 'center',
display: 'flex',
width: '100%'
}}
>
<div
className='label'
style={{
flex: '0 auto',
marginRight: 10,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap'
}}
>
{this.props.title}:
</div>
<TextField className='control' disabled id={id} value={text}
inputStyle={textareaStyle} style={textFieldStyle} />
<RaisedButton className='control' label='Change' onClick={this.handleClick}
style={buttonStyle} />
<TextField
className='control'
disabled
id={id}
inputStyle={{
color: colors.grey50
}}
style={{
flex: '1',
fontSize: 14
}}
value={this.props.displayValue || this.props.value}
/>
<RaisedButton
className='control'
label='Change'
onClick={this.handleClick}
style={{
marginLeft: 10
}}
/>
</div>
)
}

View File

@@ -1,6 +1,6 @@
const React = require('react')
const RaisedButton = require('material-ui/RaisedButton').default
const FlatButton = require('material-ui/FlatButton').default
class ShowMore extends React.Component {
static get propTypes () {
@@ -39,12 +39,14 @@ class ShowMore extends React.Component {
? this.props.hideLabel
: this.props.showLabel
return (
<div className='show-more' style={this.props.style}>
<div
style={this.props.style}
>
{this.state.expanded ? this.props.children : null}
<RaisedButton
className='control'
<FlatButton
onClick={this.handleClick}
label={label} />
label={label}
/>
</div>
)
}

View File

@@ -4,12 +4,9 @@ const {dispatcher} = require('../lib/dispatcher')
class Header extends React.Component {
render () {
const loc = this.props.state.location
var loc = this.props.state.location
return (
<div className='header'
onMouseMove={dispatcher('mediaMouseMoved')}
onMouseEnter={dispatcher('mediaControlsMouseEnter')}
onMouseLeave={dispatcher('mediaControlsMouseLeave')}>
<div className='header'>
{this.getTitle()}
<div className='nav left float-left'>
<i
@@ -34,12 +31,12 @@ class Header extends React.Component {
getTitle () {
if (process.platform !== 'darwin') return null
const state = this.props.state
var state = this.props.state
return (<div className='title ellipsis'>{state.window.title}</div>)
}
getAddButton () {
const state = this.props.state
var state = this.props.state
if (state.location.url() !== 'home') return null
return (
<i

View File

@@ -1,25 +0,0 @@
const React = require('react')
const FlatButton = require('material-ui/FlatButton').default
const RaisedButton = require('material-ui/RaisedButton').default
module.exports = class ModalOKCancel extends React.Component {
render () {
const cancelStyle = { marginRight: 10, color: 'black' }
const {cancelText, onCancel, okText, onOK} = this.props
return (
<div className='float-right'>
<FlatButton
className='control cancel'
style={cancelStyle}
label={cancelText}
onClick={onCancel} />
<RaisedButton
className='control ok'
primary
label={okText}
onClick={onOK}
autoFocus />
</div>
)
}
}

View File

@@ -1,41 +1,32 @@
const React = require('react')
const TextField = require('material-ui/TextField').default
const ModalOKCancel = require('./modal-ok-cancel')
const {dispatch, dispatcher} = require('../lib/dispatcher')
module.exports = class OpenTorrentAddressModal extends React.Component {
render () {
// TODO: dcposch remove janky inline <script>
return (
<div className='open-torrent-address-modal'>
<p><label>Enter torrent address or magnet link</label></p>
<div>
<TextField
id='torrent-address-field'
className='control'
ref={(c) => { this.torrentURL = c }}
fullWidth
onKeyDown={handleKeyDown.bind(this)} />
</div>
<ModalOKCancel
cancelText='CANCEL'
onCancel={dispatcher('exitModal')}
okText='OK'
onOK={handleOK.bind(this)} />
<p>
<input id='add-torrent-url' type='text' onKeyPress={handleKeyPress} />
</p>
<p className='float-right'>
<button className='button button-flat' onClick={dispatcher('exitModal')}>Cancel</button>
<button className='button button-raised' onClick={handleOK}>OK</button>
</p>
<script>document.querySelector('#add-torrent-url').focus()</script>
</div>
)
}
componentDidMount () {
this.torrentURL.input.focus()
}
}
function handleKeyDown (e) {
if (e.which === 13) handleOK.call(this) /* hit Enter to submit */
function handleKeyPress (e) {
if (e.which === 13) handleOK() /* hit Enter to submit */
}
function handleOK () {
dispatch('exitModal')
dispatch('addTorrent', this.torrentURL.input.value)
// TODO: dcposch use React refs instead
dispatch('addTorrent', document.querySelector('#add-torrent-url').value)
}

View File

@@ -1,24 +1,22 @@
const React = require('react')
const ModalOKCancel = require('./modal-ok-cancel')
const {dispatch, dispatcher} = require('../lib/dispatcher')
module.exports = class RemoveTorrentModal extends React.Component {
render () {
const state = this.props.state
const message = state.modal.deleteData
var state = this.props.state
var message = state.modal.deleteData
? 'Are you sure you want to remove this torrent from the list and delete the data file?'
: 'Are you sure you want to remove this torrent from the list?'
const buttonText = state.modal.deleteData ? 'REMOVE DATA' : 'REMOVE'
var buttonText = state.modal.deleteData ? 'Remove Data' : 'Remove'
return (
<div>
<p><strong>{message}</strong></p>
<ModalOKCancel
cancelText='CANCEL'
onCancel={dispatcher('exitModal')}
okText={buttonText}
onOK={handleRemove} />
<p className='float-right'>
<button className='button button-flat' onClick={dispatcher('exitModal')}>Cancel</button>
<button className='button button-raised' onClick={handleRemove}>{buttonText}</button>
</p>
</div>
)

View File

@@ -1,35 +1,35 @@
const React = require('react')
const electron = require('electron')
const path = require('path')
const ModalOKCancel = require('./modal-ok-cancel')
const {dispatcher} = require('../lib/dispatcher')
module.exports = class UnsupportedMediaModal extends React.Component {
render () {
const state = this.props.state
const err = state.modal.error
const message = (err && err.getMessage)
var state = this.props.state
var err = state.modal.error
var message = (err && err.getMessage)
? err.getMessage()
: err
const onAction = state.modal.externalPlayerInstalled
? dispatcher('openExternalPlayer')
: () => this.onInstall()
const actionText = state.modal.externalPlayerInstalled
? 'PLAY IN ' + state.getExternalPlayerName().toUpperCase()
: 'INSTALL VLC'
const errorMessage = state.modal.externalPlayerNotFound
var playerPath = state.saved.prefs.externalPlayerPath
var playerName = playerPath
? path.basename(playerPath).split('.')[0]
: 'VLC'
var actionButton = state.modal.externalPlayerInstalled
? (<button className='button-raised' onClick={dispatcher('openExternalPlayer')}>Play in {playerName}</button>)
: (<button className='button-raised' onClick={() => this.onInstall}>Install VLC</button>)
var playerMessage = state.modal.externalPlayerNotFound
? 'Couldn\'t run external player. Please make sure it\'s installed.'
: ''
return (
<div>
<p><strong>Sorry, we can't play that file.</strong></p>
<p>{message}</p>
<ModalOKCancel
cancelText='CANCEL'
onCancel={dispatcher('backToList')}
okText={actionText}
onOK={onAction} />
<p className='error-text'>{errorMessage}</p>
<p className='float-right'>
<button className='button-flat' onClick={dispatcher('backToList')}>Cancel</button>
{actionButton}
</p>
<p className='error-text'>{playerMessage}</p>
</div>
)
}
@@ -38,7 +38,7 @@ module.exports = class UnsupportedMediaModal extends React.Component {
electron.shell.openExternal('http://www.videolan.org/vlc/')
// TODO: dcposch send a dispatch rather than modifying state directly
const state = this.props.state
var state = this.props.state
state.modal.externalPlayerInstalled = true // Assume they'll install it successfully
}
}

View File

@@ -1,30 +1,24 @@
const React = require('react')
const electron = require('electron')
const ModalOKCancel = require('./modal-ok-cancel')
const {dispatch} = require('../lib/dispatcher')
module.exports = class UpdateAvailableModal extends React.Component {
render () {
const state = this.props.state
var state = this.props.state
return (
<div className='update-available-modal'>
<p><strong>A new version of WebTorrent is available: v{state.modal.version}</strong></p>
<p>
We have an auto-updater for Windows and Mac.
We don't have one for Linux yet, so you'll have to download the new version manually.
<p>We have an auto-updater for Windows and Mac. We don't have one for Linux yet, so you'll have to download the new version manually.</p>
<p className='float-right'>
<button className='button button-flat' onClick={handleSkip}>Skip This Release</button>
<button className='button button-raised' onClick={handleShow}>Show Download Page</button>
</p>
<ModalOKCancel
cancelText='SKIP THIS RELEASE'
onCancel={handleSkip}
okText='SHOW DOWNLOAD PAGE'
onOK={handleShow} />
</div>
)
function handleShow () {
// TODO: use the GitHub urls from config.js
electron.shell.openExternal('https://github.com/webtorrent/webtorrent-desktop/releases')
electron.shell.openExternal('https://github.com/feross/webtorrent-desktop/releases')
dispatch('exitModal')
}

View File

@@ -1,13 +0,0 @@
const {ipcRenderer} = require('electron')
module.exports = class FolderWatcherController {
start () {
console.log('-- IPC: start folder watcher')
ipcRenderer.send('startFolderWatcher')
}
stop () {
console.log('-- IPC: stop folder watcher')
ipcRenderer.send('stopFolderWatcher')
}
}

View File

@@ -1,6 +1,6 @@
const {ipcRenderer} = require('electron')
const telemetry = require('../lib/telemetry')
const Playlist = require('../lib/playlist')
const electron = require('electron')
const ipcRenderer = electron.ipcRenderer
// Controls local play back: the <video>/<audio> tag and VLC
// Does not control remote casting (Chromecast etc)
@@ -10,7 +10,7 @@ module.exports = class MediaController {
}
mediaSuccess () {
telemetry.logPlayAttempt('success')
this.state.playing.result = 'success'
}
mediaStalled () {
@@ -18,9 +18,9 @@ module.exports = class MediaController {
}
mediaError (error) {
const state = this.state
var state = this.state
if (state.location.url() === 'player') {
telemetry.logPlayAttempt('error')
state.playing.result = 'error'
state.playing.location = 'error'
ipcRenderer.send('checkForExternalPlayer', state.saved.prefs.externalPlayerPath)
ipcRenderer.once('checkForExternalPlayer', function (e, isInstalled) {
@@ -42,37 +42,14 @@ module.exports = class MediaController {
this.state.playing.mouseStationarySince = new Date().getTime()
}
controlsMouseEnter () {
this.state.playing.mouseInControls = true
this.state.playing.mouseStationarySince = new Date().getTime()
}
controlsMouseLeave () {
this.state.playing.mouseInControls = false
this.state.playing.mouseStationarySince = new Date().getTime()
}
openExternalPlayer () {
const state = this.state
var state = this.state
ipcRenderer.send('openExternalPlayer', state.saved.prefs.externalPlayerPath, state.server.localURL, state.window.title)
state.playing.location = 'external'
const onServerRunning = function () {
state.playing.isReady = true
telemetry.logPlayAttempt('external')
const mediaURL = Playlist.getCurrentLocalURL(state)
ipcRenderer.send('openExternalPlayer',
state.saved.prefs.externalPlayerPath,
mediaURL,
state.window.title)
}
if (state.server != null) onServerRunning()
else ipcRenderer.once('wt-server-running', onServerRunning)
}
externalPlayerNotFound () {
const modal = this.state.modal
var modal = this.state.modal
if (modal && modal.id === 'unsupported-media-modal') {
modal.externalPlayerNotFound = true
}

View File

@@ -4,11 +4,10 @@ const path = require('path')
const Cast = require('../lib/cast')
const {dispatch} = require('../lib/dispatcher')
const telemetry = require('../lib/telemetry')
const {UnplayableFileError, UnplayableTorrentError} = require('../lib/errors')
const errors = require('../lib/errors')
const sound = require('../lib/sound')
const TorrentPlayer = require('../lib/torrent-player')
const TorrentSummary = require('../lib/torrent-summary')
const Playlist = require('../lib/playlist')
const State = require('../lib/state')
const ipcRenderer = electron.ipcRenderer
@@ -25,45 +24,24 @@ module.exports = class PlaybackController {
// Play a file in a torrent.
// * Start torrenting, if necessary
// * Stream, if not already fully downloaded
// * If no file index is provided, restore the most recently viewed file or autoplay the first
// * If no file index is provided, pick the default file to play
playFile (infoHash, index /* optional */) {
this.pauseActiveTorrents(infoHash)
const state = this.state
if (state.location.url() === 'player') {
this.updatePlayer(infoHash, index, false, (err) => {
if (err) dispatch('error', err)
else this.play()
})
} else {
let initialized = false
state.location.go({
url: 'player',
setup: (cb) => {
const torrentSummary = TorrentSummary.getByKey(state, infoHash)
if (index === undefined || initialized) index = torrentSummary.mostRecentFileIndex
if (index === undefined) index = torrentSummary.files.findIndex(TorrentPlayer.isPlayable)
if (index === undefined) return cb(new UnplayableTorrentError())
initialized = true
this.openPlayer(infoHash, index, (err) => {
if (!err) this.play()
cb(err)
})
},
destroy: () => this.closePlayer()
}, (err) => {
if (err) dispatch('error', err)
})
}
this.state.location.go({
url: 'player',
setup: (cb) => {
this.play()
this.openPlayer(infoHash, index, cb)
},
destroy: () => this.closePlayer()
}, (err) => {
if (err) dispatch('error', err)
})
}
// Open a file in OS default app.
openItem (infoHash, index) {
const torrentSummary = TorrentSummary.getByKey(this.state, infoHash)
const filePath = path.join(
var torrentSummary = TorrentSummary.getByKey(this.state, infoHash)
var filePath = path.join(
torrentSummary.path,
torrentSummary.files[index].path)
ipcRenderer.send('openItem', filePath)
@@ -71,12 +49,12 @@ module.exports = class PlaybackController {
// Toggle (play or pause) the currently playing media
playPause () {
const state = this.state
var state = this.state
if (state.location.url() !== 'player') return
// force rerendering if window is hidden,
// in order to bypass `raf` and play/pause media immediately
const mediaTag = document.querySelector('video,audio')
var mediaTag = document.querySelector('video,audio')
if (!state.window.isVisible && mediaTag) {
if (state.playing.isPaused) mediaTag.play()
else mediaTag.pause()
@@ -86,44 +64,9 @@ module.exports = class PlaybackController {
else this.pause()
}
pauseActiveTorrents (infoHash) {
// Playback Priority: pause all active torrents if needed.
if (!this.state.saved.prefs.highestPlaybackPriority) return
// Do not pause active torrents if playing a fully downloaded torrent.
const torrentSummary = TorrentSummary.getByKey(this.state, infoHash)
if (torrentSummary.status === 'seeding') return
dispatch('prioritizeTorrent', infoHash)
}
// Play next file in list (if any)
nextTrack () {
const state = this.state
if (Playlist.hasNext(state) && state.playing.location !== 'external') {
this.updatePlayer(
state.playing.infoHash, Playlist.getNextIndex(state), false, (err) => {
if (err) dispatch('error', err)
else this.play()
})
}
}
// Play previous track in list (if any)
previousTrack () {
const state = this.state
if (Playlist.hasPrevious(state) && state.playing.location !== 'external') {
this.updatePlayer(
state.playing.infoHash, Playlist.getPreviousIndex(state), false, (err) => {
if (err) dispatch('error', err)
else this.play()
})
}
}
// Play (unpause) the current media
play () {
const state = this.state
var state = this.state
if (!state.playing.isPaused) return
state.playing.isPaused = false
if (isCasting(state)) {
@@ -134,7 +77,7 @@ module.exports = class PlaybackController {
// Pause the currently playing media
pause () {
const state = this.state
var state = this.state
if (state.playing.isPaused) return
state.playing.isPaused = true
if (isCasting(state)) {
@@ -163,23 +106,25 @@ module.exports = class PlaybackController {
// to 0.25 (quarter-speed playback), then goes to -0.25, -0.5, -1, -2, etc
// until -16 (fast rewind)
changePlaybackRate (direction) {
const state = this.state
let rate = state.playing.playbackRate
if (direction > 0 && rate < 2) {
var state = this.state
var rate = state.playing.playbackRate
if (direction > 0 && rate >= 0.25 && rate < 2) {
rate += 0.25
} else if (direction < 0 && rate > 0.25 && rate <= 2) {
rate -= 0.25
} else if (direction > 0 && rate >= 1 && rate < 16) {
} else if (direction < 0 && rate === 0.25) { /* when we set playback rate at 0 in html 5, playback hangs ;( */
rate = -1
} else if (direction > 0 && rate === -1) {
rate = 0.25
} else if ((direction > 0 && rate >= 1 && rate < 16) || (direction < 0 && rate > -16 && rate <= -1)) {
rate *= 2
} else if (direction < 0 && rate > 1 && rate <= 16) {
} else if ((direction < 0 && rate > 1 && rate <= 16) || (direction > 0 && rate >= -16 && rate < -1)) {
rate /= 2
}
state.playing.playbackRate = rate
if (isCasting(state) && !Cast.setRate(rate)) {
state.playing.playbackRate = 1
}
// Wait a bit before we hide the controls and header again
state.playing.mouseStationarySince = new Date().getTime()
}
// Change the volume, in range [0, 1], by some amount
@@ -194,7 +139,7 @@ module.exports = class PlaybackController {
// check if its in [0.0 - 1.0] range
volume = Math.max(0, Math.min(1, volume))
const state = this.state
var state = this.state
if (isCasting(state)) {
Cast.setVolume(volume)
} else {
@@ -208,8 +153,12 @@ module.exports = class PlaybackController {
// * The video is paused
// * The video is playing remotely on Chromecast or Airplay
showOrHidePlayerControls () {
const state = this.state
const hideControls = state.shouldHidePlayerControls()
var state = this.state
var hideControls = state.location.url() === 'player' &&
state.playing.mouseStationarySince !== 0 &&
new Date().getTime() - state.playing.mouseStationarySince > 2000 &&
!state.playing.isPaused &&
state.playing.location === 'local'
if (hideControls !== state.playing.hideControls) {
state.playing.hideControls = hideControls
@@ -220,81 +169,60 @@ module.exports = class PlaybackController {
// Opens the video player to a specific torrent
openPlayer (infoHash, index, cb) {
const state = this.state
const torrentSummary = TorrentSummary.getByKey(state, infoHash)
var torrentSummary = TorrentSummary.getByKey(this.state, infoHash)
state.playing.infoHash = torrentSummary.infoHash
state.playing.isReady = false
// automatically choose which file in the torrent to play, if necessary
if (index === undefined) index = torrentSummary.defaultPlayFileIndex
if (index === undefined) index = TorrentPlayer.pickFileToPlay(torrentSummary.files)
if (index === undefined) return cb(new errors.UnplayableError())
// update UI to show pending playback
sound.play('PLAY')
if (torrentSummary.progress !== 1) sound.play('PLAY')
// TODO: remove torrentSummary.playStatus
torrentSummary.playStatus = 'requested'
this.update()
this.startServer(torrentSummary)
ipcRenderer.send('onPlayerOpen')
this.updatePlayer(infoHash, index, true, cb)
}
// Starts WebTorrent server for media streaming
startServer (torrentSummary) {
const state = this.state
var timeout = setTimeout(() => {
telemetry.logPlayAttempt('timeout')
// TODO: remove torrentSummary.playStatus
torrentSummary.playStatus = 'timeout' /* no seeders available? */
sound.play('ERROR')
cb(new Error('Playback timed out. Try again.'))
this.update()
}, 10000) /* give it a few seconds */
if (torrentSummary.status === 'paused') {
dispatch('startTorrentingSummary', torrentSummary.torrentKey)
ipcRenderer.once('wt-ready-' + torrentSummary.infoHash,
() => onTorrentReady())
() => this.openPlayerFromActiveTorrent(torrentSummary, index, timeout, cb))
} else {
onTorrentReady()
}
function onTorrentReady () {
ipcRenderer.send('wt-start-server', torrentSummary.infoHash)
ipcRenderer.once('wt-server-running', () => { state.playing.isReady = true })
this.openPlayerFromActiveTorrent(torrentSummary, index, timeout, cb)
}
}
// Called each time the current file changes
updatePlayer (infoHash, index, resume, cb) {
const state = this.state
const torrentSummary = TorrentSummary.getByKey(state, infoHash)
const fileSummary = torrentSummary.files[index]
if (!TorrentPlayer.isPlayable(fileSummary)) {
torrentSummary.mostRecentFileIndex = undefined
return cb(new UnplayableFileError())
}
torrentSummary.mostRecentFileIndex = index
openPlayerFromActiveTorrent (torrentSummary, index, timeout, cb) {
var fileSummary = torrentSummary.files[index]
// update state
state.playing.infoHash = infoHash
var state = this.state
state.playing.infoHash = torrentSummary.infoHash
state.playing.fileIndex = index
state.playing.type = TorrentPlayer.isVideo(fileSummary) ? 'video'
: TorrentPlayer.isAudio(fileSummary) ? 'audio'
: 'other'
// pick up where we left off
let jumpToTime = 0
if (resume && fileSummary.currentTime) {
const fraction = fileSummary.currentTime / fileSummary.duration
const secondsLeft = fileSummary.duration - fileSummary.currentTime
if (fileSummary.currentTime) {
var fraction = fileSummary.currentTime / fileSummary.duration
var secondsLeft = fileSummary.duration - fileSummary.currentTime
if (fraction < 0.9 && secondsLeft > 10) {
jumpToTime = fileSummary.currentTime
state.playing.jumpToTime = fileSummary.currentTime
}
}
state.playing.jumpToTime = jumpToTime
// if it's audio, parse out the metadata (artist, title, etc)
if (torrentSummary.status === 'paused') {
ipcRenderer.once('wt-ready-' + torrentSummary.infoHash, getAudioMetadata)
} else {
getAudioMetadata()
}
function getAudioMetadata () {
if (state.playing.type === 'audio' && !fileSummary.audioInfo) {
ipcRenderer.send('wt-get-audio-metadata', torrentSummary.infoHash, index)
}
if (state.playing.type === 'audio' && !fileSummary.audioInfo) {
ipcRenderer.send('wt-get-audio-metadata', torrentSummary.infoHash, index)
}
// if it's video, check for subtitles files that are done downloading
@@ -305,28 +233,41 @@ module.exports = class PlaybackController {
dispatch('addSubtitles', [fileSummary.selectedSubtitle], true)
}
state.window.title = fileSummary.name
ipcRenderer.send('wt-start-server', torrentSummary.infoHash, index)
ipcRenderer.once('wt-server-' + torrentSummary.infoHash, (e, info) => {
clearTimeout(timeout)
// play in VLC if set as default player (Preferences / Playback / Play in VLC)
if (this.state.saved.prefs.openExternalPlayer) {
dispatch('openExternalPlayer')
// if we timed out (user clicked play a long time ago), don't autoplay
var timedOut = torrentSummary.playStatus === 'timeout'
delete torrentSummary.playStatus
if (timedOut) {
ipcRenderer.send('wt-stop-server')
return this.update()
}
state.window.title = torrentSummary.files[state.playing.fileIndex].name
// play in VLC if set as default player (Preferences / Playback / Play in VLC)
if (this.state.saved.prefs.openExternalPlayer) {
dispatch('openExternalPlayer')
this.update()
cb()
return
}
// otherwise, play the video
this.update()
ipcRenderer.send('onPlayerOpen')
cb()
return
}
// otherwise, play the video
this.update()
ipcRenderer.send('onPlayerUpdate', Playlist.hasNext(state), Playlist.hasPrevious(state))
cb()
})
}
closePlayer () {
console.log('closePlayer')
// Quit any external players, like Chromecast/Airplay/etc or VLC
const state = this.state
var state = this.state
if (isCasting(state)) {
Cast.stop()
}
@@ -337,7 +278,12 @@ module.exports = class PlaybackController {
// Save volume (this session only, not in state.saved)
state.previousVolume = state.playing.volume
if (!state.playing.isReady) telemetry.logPlayAttempt('abandoned') // user gave up waiting
// Telemetry: track what happens after the user clicks play
var result = state.playing.result // 'success' or 'error'
if (result === 'success') telemetry.logPlayAttempt('success') // first frame displayed
else if (result === 'error') telemetry.logPlayAttempt('error') // codec missing, etc
else if (result === undefined) telemetry.logPlayAttempt('abandoned') // user exited before first frame
else console.error('Unknown state.playing.result', state.playing.result)
// Reset the window contents back to the home screen
state.playing = State.getDefaultPlayState()
@@ -354,11 +300,6 @@ module.exports = class PlaybackController {
ipcRenderer.send('onPlayerClose')
// Playback Priority: resume previously paused downloads.
if (this.state.saved.prefs.highestPlaybackPriority) {
dispatch('resumePausedTorrents')
}
this.update()
}
}

View File

@@ -1,3 +1,4 @@
const State = require('../lib/state')
const {dispatch} = require('../lib/dispatcher')
const ipcRenderer = require('electron').ipcRenderer
@@ -10,15 +11,13 @@ module.exports = class PrefsController {
// Goes to the Preferences screen
show () {
const state = this.state
var state = this.state
state.location.go({
url: 'preferences',
setup: function (cb) {
// initialize preferences
state.window.title = 'Preferences'
state.unsaved = Object.assign(state.unsaved || {}, {
prefs: Object.assign({}, state.saved.prefs)
})
state.unsaved = Object.assign(state.unsaved || {}, {prefs: state.saved.prefs || {}})
ipcRenderer.send('setAllowNav', false)
cb()
},
@@ -33,29 +32,25 @@ module.exports = class PrefsController {
// For example: updatePreferences('foo.bar', 'baz')
// Call save() to save to config.json
update (property, value) {
const path = property.split('.')
let obj = this.state.unsaved.prefs
let i
for (i = 0; i < path.length - 1; i++) {
if (typeof obj[path[i]] === 'undefined') {
obj[path[i]] = {}
var path = property.split('.')
var key = this.state.unsaved.prefs
for (var i = 0; i < path.length - 1; i++) {
if (typeof key[path[i]] === 'undefined') {
key[path[i]] = {}
}
obj = obj[path[i]]
key = key[path[i]]
}
obj[path[i]] = value
key[path[i]] = value
}
// All unsaved prefs take effect atomically, and are saved to config.json
save () {
const state = this.state
var state = this.state
if (state.unsaved.prefs.isFileHandler !== state.saved.prefs.isFileHandler) {
ipcRenderer.send('setDefaultFileHandler', state.unsaved.prefs.isFileHandler)
}
if (state.unsaved.prefs.startup !== state.saved.prefs.startup) {
ipcRenderer.send('setStartup', state.unsaved.prefs.startup)
}
state.saved.prefs = Object.assign(state.saved.prefs || {}, state.unsaved.prefs)
dispatch('stateSaveImmediate')
State.save(state)
dispatch('checkDownloadPath')
}
}

View File

@@ -1,9 +1,11 @@
const electron = require('electron')
const fs = require('fs')
const fs = require('fs-extra')
const path = require('path')
const parallel = require('run-parallel')
const zeroFill = require('zero-fill')
const remote = electron.remote
const ipcRenderer = electron.ipcRenderer
const {dispatch} = require('../lib/dispatcher')
@@ -28,35 +30,36 @@ module.exports = class SubtitlesController {
}
toggleSubtitlesMenu () {
const subtitles = this.state.playing.subtitles
var subtitles = this.state.playing.subtitles
subtitles.showMenu = !subtitles.showMenu
}
addSubtitles (files, autoSelect) {
var state = this.state
// Subtitles are only supported when playing video files
if (this.state.playing.type !== 'video') return
if (state.playing.type !== 'video') return
if (files.length === 0) return
const subtitles = this.state.playing.subtitles
var subtitles = state.playing.subtitles
// Read the files concurrently, then add all resulting subtitle tracks
const tasks = files.map((file) => (cb) => loadSubtitle(file, cb))
var tasks = files.map((file) => (cb) => loadSubtitle(file, cb))
parallel(tasks, function (err, tracks) {
if (err) return dispatch('error', err)
for (let i = 0; i < tracks.length; i++) {
for (var i = 0; i < tracks.length; i++) {
// No dupes allowed
const track = tracks[i]
let trackIndex = subtitles.tracks.findIndex((t) =>
track.filePath === t.filePath)
var track = tracks[i]
var trackIndex = state.playing.subtitles.tracks
.findIndex((t) => track.filePath === t.filePath)
// Add the track
if (trackIndex === -1) {
trackIndex = subtitles.tracks.push(track) - 1
trackIndex = state.playing.subtitles.tracks.push(track) - 1
}
// If we're auto-selecting a track, try to find one in the user's language
if (autoSelect && (i === 0 || isSystemLanguage(track.language))) {
subtitles.selectedIndex = trackIndex
state.playing.subtitles.selectedIndex = trackIndex
}
}
@@ -67,46 +70,62 @@ module.exports = class SubtitlesController {
checkForSubtitles () {
if (this.state.playing.type !== 'video') return
const torrentSummary = this.state.getPlayingTorrentSummary()
var torrentSummary = this.state.getPlayingTorrentSummary()
if (!torrentSummary || !torrentSummary.progress) return
torrentSummary.progress.files.forEach((fp, ix) => {
if (fp.numPieces !== fp.numPiecesPresent) return // ignore incomplete files
const file = torrentSummary.files[ix]
var file = torrentSummary.files[ix]
if (this.state.playing.fileIndex === ix) return this.checkForEmbeddedMKVSubtitles(file)
if (!this.isSubtitle(file.name)) return
const filePath = path.join(torrentSummary.path, file.path)
var filePath = path.join(torrentSummary.path, file.path)
this.addSubtitles([filePath], false)
})
}
isSubtitle (file) {
const name = typeof file === 'string' ? file : file.name
const ext = path.extname(name).toLowerCase()
var name = typeof file === 'string' ? file : file.name
var ext = path.extname(name).toLowerCase()
return ext === '.srt' || ext === '.vtt'
}
checkForEmbeddedMKVSubtitles (file) {
var playing = this.state.playing
// var playingFile = this.state.getPlayingFileSummary()
// var playingPath = path.join(torrentSummary.path, playingFile.path)
if (path.extname(file.name).toLowerCase() === '.mkv') {
ipcRenderer.send('wt-get-mkv-subtitles', playing.infoHash, playing.fileIndex)
ipcRenderer.once('wt-mkv-subtitles', function (e, tracks) {
tracks.forEach(function (trackEntry) {
var track = loadEmbeddedSubtitle(trackEntry)
console.log('loaded emb subs', track)
playing.subtitles.tracks.push(track)
})
if (tracks.length > 0) relabelSubtitles(playing.subtitles)
})
}
}
}
function loadSubtitle (file, cb) {
// Lazy load to keep startup fast
const concat = require('simple-concat')
const LanguageDetect = require('languagedetect')
const srtToVtt = require('srt-to-vtt')
var concat = require('simple-concat')
var srtToVtt = require('srt-to-vtt')
// Read the .SRT or .VTT file, parse it, add subtitle track
const filePath = file.path || file
var filePath = file.path || file
const vttStream = fs.createReadStream(filePath).pipe(srtToVtt())
var vttStream = fs.createReadStream(filePath).pipe(srtToVtt())
concat(vttStream, function (err, buf) {
if (err) return dispatch('error', 'Can\'t parse subtitles file.')
// Detect what language the subtitles are in
const vttContents = buf.toString().replace(/(.*-->.*)/g, '')
let langDetected = (new LanguageDetect()).detect(vttContents, 2)
langDetected = langDetected.length ? langDetected[0][0] : 'subtitle'
langDetected = langDetected.slice(0, 1).toUpperCase() + langDetected.slice(1)
var langDetected = detectVTTLanguage(buf)
const track = {
var track = {
buffer: 'data:text/vtt;base64,' + buf.toString('base64'),
language: langDetected,
label: langDetected,
@@ -120,19 +139,65 @@ function loadSubtitle (file, cb) {
// Checks whether a language name like 'English' or 'German' matches the system
// language, aka the current locale
function isSystemLanguage (language) {
const iso639 = require('iso-639-1')
const osLangISO = window.navigator.language.split('-')[0] // eg 'en'
const langIso = iso639.getCode(language) // eg 'de' if language is 'German'
var iso639 = require('iso-639-1')
var osLangISO = window.navigator.language.split('-')[0] // eg 'en'
var langIso = iso639.getCode(language) // eg 'de' if language is 'German'
return langIso === osLangISO
}
// Make sure we don't have two subtitle tracks with the same label
// Labels each track by language, eg 'German', 'English', 'English 2', ...
function relabelSubtitles (subtitles) {
const counts = {}
var counts = {}
subtitles.tracks.forEach(function (track) {
const lang = track.language
var lang = track.language
counts[lang] = (counts[lang] || 0) + 1
track.label = counts[lang] > 1 ? (lang + ' ' + counts[lang]) : lang
})
}
function detectVTTLanguage (buffer) {
var LanguageDetect = require('languagedetect')
// Detect what language the subtitles are in
var vttContents = buffer.toString().replace(/(.*-->.*)/g, '') // remove numbers?
var langDetected = (new LanguageDetect()).detect(vttContents, 2)
langDetected = langDetected.length ? langDetected[0][0] : 'subtitle'
langDetected = langDetected.slice(0, 1).toUpperCase() + langDetected.slice(1)
return langDetected
}
function loadEmbeddedSubtitle (trackEntry) {
// convert to .vtt format
var vtt = 'WEBVTT FILE\r\n\r\n'
trackEntry.subtitles.forEach(function (sub, i) {
vtt += `${i + 1}\r\n`
vtt += `${msToTime(sub.time)} --> ${msToTime(sub.time + sub.duration)}\r\n`
vtt += `${sub.text}\r\n\r\n`
})
function msToTime (s) {
var ms = s % 1000
s = (s - ms) / 1000
var secs = s % 60
s = (s - secs) / 60
var mins = s % 60
var hrs = (s - mins) / 60
var z = zeroFill
return z(2, hrs) + ':' + z(2, mins) + ':' + z(2, secs) + '.' + z(3, ms)
}
var buf = new Buffer(vtt)
var langDetected = detectVTTLanguage(buf)
var track = {
buffer: 'data:text/vtt;base64,' + buf.toString('base64'),
language: langDetected,
label: langDetected,
filePath: null
}
return track
}

View File

@@ -2,6 +2,7 @@ const path = require('path')
const ipcRenderer = require('electron').ipcRenderer
const TorrentSummary = require('../lib/torrent-summary')
const TorrentPlayer = require('../lib/torrent-player')
const sound = require('../lib/sound')
const {dispatch} = require('../lib/dispatcher')
@@ -11,12 +12,12 @@ module.exports = class TorrentController {
}
torrentInfoHash (torrentKey, infoHash) {
let torrentSummary = this.getTorrentSummary(torrentKey)
var torrentSummary = this.getTorrentSummary(torrentKey)
console.log('got infohash for %s torrent %s',
torrentSummary ? 'existing' : 'new', torrentKey)
if (!torrentSummary) {
const torrents = this.state.saved.torrents
var torrents = this.state.saved.torrents
// Check if an existing (non-active) torrent has the same info hash
if (torrents.find((t) => t.infoHash === infoHash)) {
@@ -48,7 +49,7 @@ module.exports = class TorrentController {
}
dispatch('error', message)
const torrentSummary = this.getTorrentSummary(torrentKey)
var torrentSummary = this.getTorrentSummary(torrentKey)
if (torrentSummary) {
console.log('Pausing torrent %s due to error: %s', torrentSummary.infoHash, message)
torrentSummary.status = 'paused'
@@ -58,20 +59,21 @@ module.exports = class TorrentController {
torrentMetadata (torrentKey, torrentInfo) {
// Summarize torrent
const torrentSummary = this.getTorrentSummary(torrentKey)
var torrentSummary = this.getTorrentSummary(torrentKey)
torrentSummary.status = 'downloading'
torrentSummary.name = torrentSummary.displayName || torrentInfo.name
torrentSummary.path = torrentInfo.path
torrentSummary.magnetURI = torrentInfo.magnetURI
// TODO: make torrentInfo immutable, save separately as torrentSummary.info
// For now, check whether torrentSummary.files has already been set:
const hasDetailedFileInfo = torrentSummary.files && torrentSummary.files[0].path
var hasDetailedFileInfo = torrentSummary.files && torrentSummary.files[0].path
if (!hasDetailedFileInfo) {
torrentSummary.files = torrentInfo.files
}
if (!torrentSummary.selections) {
torrentSummary.selections = torrentSummary.files.map((x) => true)
}
torrentSummary.defaultPlayFileIndex = TorrentPlayer.pickFileToPlay(torrentInfo.files)
dispatch('update')
// Save the .torrent file, if it hasn't been saved already
@@ -83,7 +85,7 @@ module.exports = class TorrentController {
torrentDone (torrentKey, torrentInfo) {
// Update the torrent summary
const torrentSummary = this.getTorrentSummary(torrentKey)
var torrentSummary = this.getTorrentSummary(torrentKey)
torrentSummary.status = 'seeding'
// Notify the user that a torrent finished, but only if we actually DL'd at least part of it.
@@ -100,18 +102,22 @@ module.exports = class TorrentController {
}
torrentProgress (progressInfo) {
// Overall progress across all active torrents, 0 to 1, or -1 to hide the progress bar
// Overall progress across all active torrents, 0 to 1
var progress = progressInfo.progress
var hasActiveTorrents = progressInfo.hasActiveTorrents
// Hide progress bar when client has no torrents, or progress is 100%
const progress = (!progressInfo.hasActiveTorrents || progressInfo.progress === 1)
? -1
: progressInfo.progress
// TODO: isn't this equivalent to: if (progress === 1) ?
if (!hasActiveTorrents || progress === 1) {
progress = -1
}
// Show progress bar under the WebTorrent taskbar icon, on OSX
this.state.dock.progress = progress
// Update progress for each individual torrent
progressInfo.torrents.forEach((p) => {
const torrentSummary = this.getTorrentSummary(p.torrentKey)
var torrentSummary = this.getTorrentSummary(p.torrentKey)
if (!torrentSummary) {
console.log('warning: got progress for missing torrent %s', p.torrentKey)
return
@@ -126,28 +132,27 @@ module.exports = class TorrentController {
}
torrentFileModtimes (torrentKey, fileModtimes) {
const torrentSummary = this.getTorrentSummary(torrentKey)
if (!torrentSummary) throw new Error('Not saving modtimes for deleted torrent ' + torrentKey)
var torrentSummary = this.getTorrentSummary(torrentKey)
torrentSummary.fileModtimes = fileModtimes
dispatch('stateSave')
dispatch('saveStateThrottled')
}
torrentFileSaved (torrentKey, torrentFileName) {
console.log('torrent file saved %s: %s', torrentKey, torrentFileName)
const torrentSummary = this.getTorrentSummary(torrentKey)
var torrentSummary = this.getTorrentSummary(torrentKey)
torrentSummary.torrentFileName = torrentFileName
dispatch('stateSave')
dispatch('saveStateThrottled')
}
torrentPosterSaved (torrentKey, posterFileName) {
const torrentSummary = this.getTorrentSummary(torrentKey)
var torrentSummary = this.getTorrentSummary(torrentKey)
torrentSummary.posterFileName = posterFileName
dispatch('stateSave')
dispatch('saveStateThrottled')
}
torrentAudioMetadata (infoHash, index, info) {
const torrentSummary = this.getTorrentSummary(infoHash)
const fileSummary = torrentSummary.files[index]
var torrentSummary = this.getTorrentSummary(infoHash)
var fileSummary = torrentSummary.files[index]
fileSummary.audioInfo = info
dispatch('update')
}
@@ -164,7 +169,7 @@ module.exports = class TorrentController {
}
function getTorrentPath (torrentSummary) {
let itemPath = TorrentSummary.getFileOrFolder(torrentSummary)
var itemPath = TorrentSummary.getFileOrFolder(torrentSummary)
if (torrentSummary.files.length > 1) {
itemPath = path.dirname(itemPath)
}
@@ -172,12 +177,12 @@ function getTorrentPath (torrentSummary) {
}
function showDoneNotification (torrent) {
const notif = new window.Notification('Download Complete', {
var notif = new window.Notification('Download Complete', {
body: torrent.name,
silent: true
})
notif.onclick = function () {
notif.onClick = function () {
ipcRenderer.send('show')
}

View File

@@ -3,7 +3,7 @@ const path = require('path')
const electron = require('electron')
const {dispatch} = require('../lib/dispatcher')
const {TorrentKeyNotFoundError} = require('../lib/errors')
const State = require('../lib/state')
const sound = require('../lib/sound')
const TorrentSummary = require('../lib/torrent-summary')
@@ -17,26 +17,21 @@ module.exports = class TorrentListController {
this.state = state
}
// Adds a torrent to the list, starts downloading/seeding.
// TorrentID can be a magnet URI, infohash, or torrent file: https://git.io/vik9M
// Adds a torrent to the list, starts downloading/seeding. TorrentID can be a
// magnet URI, infohash, or torrent file: https://github.com/feross/webtorrent#clientaddtorrentid-opts-function-ontorrent-torrent-
addTorrent (torrentId) {
if (torrentId.path) {
// Use path string instead of W3C File object
torrentId = torrentId.path
}
// Trim extra spaces off pasted magnet links
if (typeof torrentId === 'string') {
torrentId = torrentId.trim()
}
// Allow a instant.io link to be pasted
if (typeof torrentId === 'string' && instantIoRegex.test(torrentId)) {
torrentId = torrentId.slice(torrentId.indexOf('#') + 1)
}
const torrentKey = this.state.nextTorrentKey++
const path = this.state.saved.prefs.downloadPath
var torrentKey = this.state.nextTorrentKey++
var path = this.state.saved.prefs.downloadPath
ipcRenderer.send('wt-start-torrenting', torrentKey, torrentId, path)
@@ -71,16 +66,16 @@ module.exports = class TorrentListController {
// Creates a new torrent and start seeeding
createTorrent (options) {
const state = this.state
const torrentKey = state.nextTorrentKey++
var state = this.state
var torrentKey = state.nextTorrentKey++
ipcRenderer.send('wt-create-torrent', torrentKey, options)
state.location.cancel()
}
// Starts downloading and/or seeding a given torrentSummary.
startTorrentingSummary (torrentKey) {
const s = TorrentSummary.getByKey(this.state, torrentKey)
if (!s) throw new TorrentKeyNotFoundError(torrentKey)
var s = TorrentSummary.getByKey(this.state, torrentKey)
if (!s) throw new Error('Missing key: ' + torrentKey)
// New torrent: give it a path
if (!s.path) {
@@ -89,16 +84,10 @@ module.exports = class TorrentListController {
return start()
}
const fileOrFolder = TorrentSummary.getFileOrFolder(s)
// New torrent: metadata not yet received
if (!fileOrFolder) return start()
// Existing torrent: check that the path is still there
fs.stat(fileOrFolder, function (err) {
fs.stat(TorrentSummary.getFileOrFolder(s), function (err) {
if (err) {
s.error = 'path-missing'
dispatch('backToList')
return
}
start()
@@ -107,7 +96,7 @@ module.exports = class TorrentListController {
function start () {
ipcRenderer.send('wt-start-torrenting',
s.torrentKey,
TorrentSummary.getTorrentId(s),
TorrentSummary.getTorrentID(s),
s.path,
s.fileModtimes,
s.selections)
@@ -116,80 +105,24 @@ module.exports = class TorrentListController {
// TODO: use torrentKey, not infoHash
toggleTorrent (infoHash) {
const torrentSummary = TorrentSummary.getByKey(this.state, infoHash)
var torrentSummary = TorrentSummary.getByKey(this.state, infoHash)
if (torrentSummary.status === 'paused') {
torrentSummary.status = 'new'
this.startTorrentingSummary(torrentSummary.torrentKey)
sound.play('ENABLE')
return
} else {
torrentSummary.status = 'paused'
ipcRenderer.send('wt-stop-torrenting', torrentSummary.infoHash)
sound.play('DISABLE')
}
this.pauseTorrent(torrentSummary, true)
}
pauseAllTorrents () {
this.state.saved.torrents.forEach((torrentSummary) => {
if (torrentSummary.status === 'downloading' ||
torrentSummary.status === 'seeding') {
torrentSummary.status = 'paused'
ipcRenderer.send('wt-stop-torrenting', torrentSummary.infoHash)
}
})
sound.play('DISABLE')
}
resumeAllTorrents () {
this.state.saved.torrents.forEach((torrentSummary) => {
if (torrentSummary.status === 'paused') {
torrentSummary.status = 'downloading'
this.startTorrentingSummary(torrentSummary.torrentKey)
}
})
sound.play('ENABLE')
}
pauseTorrent (torrentSummary, playSound) {
torrentSummary.status = 'paused'
ipcRenderer.send('wt-stop-torrenting', torrentSummary.infoHash)
if (playSound) sound.play('DISABLE')
}
prioritizeTorrent (infoHash) {
this.state.saved.torrents
.filter((torrent) => { // We're interested in active torrents only.
return (['downloading', 'seeding'].indexOf(torrent.status) !== -1)
})
.map((torrent) => { // Pause all active torrents except the one that started playing.
if (infoHash === torrent.infoHash) return
// Pause torrent without playing sounds.
this.pauseTorrent(torrent, false)
this.state.saved.torrentsToResume.push(torrent.infoHash)
})
console.log('Playback Priority: paused torrents: ', this.state.saved.torrentsToResume)
}
resumePausedTorrents () {
console.log('Playback Priority: resuming paused torrents')
this.state.saved.torrentsToResume.map((infoHash) => {
this.toggleTorrent(infoHash)
})
// reset paused torrents
this.state.saved.torrentsToResume = []
}
toggleTorrentFile (infoHash, index) {
const torrentSummary = TorrentSummary.getByKey(this.state, infoHash)
var torrentSummary = TorrentSummary.getByKey(this.state, infoHash)
torrentSummary.selections[index] = !torrentSummary.selections[index]
// Let the WebTorrent process know to start or stop fetching that file
if (torrentSummary.status !== 'paused') {
ipcRenderer.send('wt-select-files', infoHash, torrentSummary.selections)
}
ipcRenderer.send('wt-select-files', infoHash, torrentSummary.selections)
}
confirmDeleteTorrent (infoHash, deleteData) {
@@ -204,25 +137,24 @@ module.exports = class TorrentListController {
deleteTorrent (infoHash, deleteData) {
ipcRenderer.send('wt-stop-torrenting', infoHash)
const index = this.state.saved.torrents.findIndex((x) => x.infoHash === infoHash)
var index = this.state.saved.torrents.findIndex((x) => x.infoHash === infoHash)
if (index > -1) {
const summary = this.state.saved.torrents[index]
var summary = this.state.saved.torrents[index]
// remove torrent and poster file
deleteFile(TorrentSummary.getTorrentPath(summary))
deleteFile(TorrentSummary.getPosterPath(summary))
deleteFile(TorrentSummary.getPosterPath(summary)) // TODO: will the css path hack affect windows?
// optionally delete the torrent data
if (deleteData) moveItemToTrash(summary)
// remove torrent from saved list
this.state.saved.torrents.splice(index, 1)
dispatch('stateSave')
State.saveThrottled(this.state)
}
// prevent user from going forward to a deleted torrent
this.state.location.clearForward('player')
this.state.location.clearForward('player') // prevent user from going forward to a deleted torrent
sound.play('DELETE')
}
@@ -235,8 +167,8 @@ module.exports = class TorrentListController {
}
openTorrentContextMenu (infoHash) {
const torrentSummary = TorrentSummary.getByKey(this.state, infoHash)
const menu = new electron.remote.Menu()
var torrentSummary = TorrentSummary.getByKey(this.state, infoHash)
var menu = new electron.remote.Menu()
menu.append(new electron.remote.MenuItem({
label: 'Remove From List',
@@ -274,70 +206,39 @@ module.exports = class TorrentListController {
menu.append(new electron.remote.MenuItem({
label: 'Save Torrent File As...',
click: () => dispatch('saveTorrentFileAs', torrentSummary.torrentKey),
enabled: torrentSummary.torrentFileName != null
click: () => saveTorrentFileAs(torrentSummary)
}))
menu.popup(electron.remote.getCurrentWindow())
}
// Takes a torrentSummary or torrentKey
// Shows a Save File dialog, then saves the .torrent file wherever the user requests
saveTorrentFileAs (torrentKey) {
const torrentSummary = TorrentSummary.getByKey(this.state, torrentKey)
if (!torrentSummary) throw new Error('Missing torrentKey: ' + torrentKey)
const downloadPath = this.state.saved.prefs.downloadPath
const newFileName = path.parse(torrentSummary.name).name + '.torrent'
const win = electron.remote.getCurrentWindow()
const opts = {
title: 'Save Torrent File',
defaultPath: path.join(downloadPath, newFileName),
filters: [
{ name: 'Torrent Files', extensions: ['torrent'] },
{ name: 'All Files', extensions: ['*'] }
]
}
electron.remote.dialog.showSaveDialog(win, opts, function (savePath) {
console.log('Saving torrent ' + torrentKey + ' to ' + savePath)
if (!savePath) return // They clicked Cancel
const torrentPath = TorrentSummary.getTorrentPath(torrentSummary)
fs.readFile(torrentPath, function (err, torrentFile) {
if (err) return dispatch('error', err)
fs.writeFile(savePath, torrentFile, function (err) {
if (err) return dispatch('error', err)
})
})
})
}
}
// Recursively finds {name, path, size} for all files in a folder
// Calls `cb` on success, calls `onError` on failure
function findFilesRecursive (paths, cb_) {
function findFilesRecursive (paths, cb) {
if (paths.length > 1) {
let numComplete = 0
let ret = []
var numComplete = 0
var ret = []
paths.forEach(function (path) {
findFilesRecursive([path], function (fileObjs) {
ret.push(...fileObjs)
ret = ret.concat(fileObjs)
if (++numComplete === paths.length) {
ret.sort((a, b) => a.path < b.path ? -1 : a.path > b.path)
cb_(ret)
cb(ret)
}
})
})
return
}
const fileOrFolder = paths[0]
var fileOrFolder = paths[0]
fs.stat(fileOrFolder, function (err, stat) {
if (err) return dispatch('error', err)
// Files: return name, path, and size
if (!stat.isDirectory()) {
const filePath = fileOrFolder
return cb_([{
var filePath = fileOrFolder
return cb([{
name: path.basename(filePath),
path: filePath,
size: stat.size
@@ -345,11 +246,11 @@ function findFilesRecursive (paths, cb_) {
}
// Folders: recurse, make a list of all the files
const folderPath = fileOrFolder
var folderPath = fileOrFolder
fs.readdir(folderPath, function (err, fileNames) {
if (err) return dispatch('error', err)
const paths = fileNames.map((fileName) => path.join(folderPath, fileName))
findFilesRecursive(paths, cb_)
var paths = fileNames.map((fileName) => path.join(folderPath, fileName))
findFilesRecursive(paths, cb)
})
})
}
@@ -363,10 +264,33 @@ function deleteFile (path) {
// Delete all files in a torrent
function moveItemToTrash (torrentSummary) {
const filePath = TorrentSummary.getFileOrFolder(torrentSummary)
var filePath = TorrentSummary.getFileOrFolder(torrentSummary)
if (filePath) ipcRenderer.send('moveItemToTrash', filePath)
}
function showItemInFolder (torrentSummary) {
ipcRenderer.send('showItemInFolder', TorrentSummary.getFileOrFolder(torrentSummary))
}
function saveTorrentFileAs (torrentSummary) {
var downloadPath = this.state.saved.prefs.downloadPath
var newFileName = path.parse(torrentSummary.name).name + '.torrent'
var opts = {
title: 'Save Torrent File',
defaultPath: path.join(downloadPath, newFileName),
filters: [
{ name: 'Torrent Files', extensions: ['torrent'] },
{ name: 'All Files', extensions: ['*'] }
]
}
electron.remote.dialog.showSaveDialog(electron.remote.getCurrentWindow(), opts, function (savePath) {
if (!savePath) return // They clicked Cancel
var torrentPath = TorrentSummary.getTorrentPath(torrentSummary)
fs.readFile(torrentPath, function (err, torrentFile) {
if (err) return dispatch('error', err)
fs.writeFile(savePath, torrentFile, function (err) {
if (err) return dispatch('error', err)
})
})
})
}

View File

@@ -1,4 +1,4 @@
const {dispatch} = require('../lib/dispatcher')
const State = require('../lib/state')
// Controls the UI checking for new versions of the app, prompting install
module.exports = class UpdateController {
@@ -8,7 +8,7 @@ module.exports = class UpdateController {
// Shows a modal saying that we have an update
updateAvailable (version) {
const skipped = this.state.saved.skippedVersions
var skipped = this.state.saved.skippedVersions
if (skipped && skipped.includes(version)) {
console.log('new version skipped by user: v' + version)
return
@@ -18,9 +18,9 @@ module.exports = class UpdateController {
// Don't show the modal again until the next version
skipVersion (version) {
let skipped = this.state.saved.skippedVersions
var skipped = this.state.saved.skippedVersions
if (!skipped) skipped = this.state.saved.skippedVersions = []
skipped.push(version)
dispatch('stateSave')
State.saveThrottled(this.state)
}
}

View File

@@ -0,0 +1,30 @@
module.exports = captureVideoFrame
function captureVideoFrame (video, format) {
if (typeof video === 'string') {
video = document.querySelector(video)
}
if (video == null || video.nodeName !== 'VIDEO') {
throw new Error('First argument must be a <video> element or selector')
}
if (format == null) {
format = 'png'
}
if (format !== 'png' && format !== 'jpg' && format !== 'webp') {
throw new Error('Second argument must be one of "png", "jpg", or "webp"')
}
var canvas = document.createElement('canvas')
canvas.width = video.videoWidth
canvas.height = video.videoHeight
canvas.getContext('2d').drawImage(video, 0, 0)
var dataUri = canvas.toDataURL('image/' + format)
var data = dataUri.split(',')[1]
return new Buffer(data, 'base64')
}

View File

@@ -13,35 +13,25 @@ module.exports = {
setRate
}
const config = require('../../config')
const {CastingError} = require('./errors')
// Lazy load these for a ~300ms improvement in startup time
let airplayer, chromecasts, dlnacasts
var airplayer, chromecasts, dlnacasts
var config = require('../../config')
// App state. Cast modifies state.playing and state.errors in response to events
let state
var state
// Callback to notify module users when state has changed
let update
var update
// setInterval() for updating cast status
let statusInterval = null
var statusInterval = null
// Start looking for cast devices on the local network
function init (appState, callback) {
state = appState
update = callback
// Don't actually cast during integration tests
// (Otherwise you'd need a physical Chromecast + AppleTV + DLNA TV to run them.)
if (config.IS_TEST) {
state.devices.chromecast = testPlayer('chromecast')
state.devices.airplay = testPlayer('airplay')
state.devices.dlna = testPlayer('dlna')
return
}
// Load modules, scan the network for devices
airplayer = require('airplayer')()
chromecasts = require('chromecasts')()
@@ -67,35 +57,9 @@ function init (appState, callback) {
})
}
// integration test player implementation
function testPlayer (type) {
return {
getDevices,
open,
play,
pause,
stop,
status,
seek,
volume
}
function getDevices () {
return [{name: type + '-1'}, {name: type + '-2'}]
}
function open () {}
function play () {}
function pause () {}
function stop () {}
function status () {}
function seek () {}
function volume () {}
}
// chromecast player implementation
function chromecastPlayer () {
const ret = {
var ret = {
device: null,
addDevice,
getDevices,
@@ -131,8 +95,8 @@ function chromecastPlayer () {
}
function open () {
const torrentSummary = state.saved.torrents.find((x) => x.infoHash === state.playing.infoHash)
ret.device.play(state.server.networkURL + '/' + state.playing.fileIndex, {
var torrentSummary = state.saved.torrents.find((x) => x.infoHash === state.playing.infoHash)
ret.device.play(state.server.networkURL, {
type: 'video/mp4',
title: config.APP_NAME + ' - ' + torrentSummary.name
}, function (err) {
@@ -162,7 +126,13 @@ function chromecastPlayer () {
}
function status () {
ret.device.status(handleStatus)
ret.device.status(function (err, status) {
if (err) return console.log('error getting %s status: %o', state.playing.location, err)
state.playing.isPaused = status.playerState === 'PAUSED'
state.playing.currentTime = status.currentTime
state.playing.volume = status.volume.muted ? 0 : status.volume.level
update()
})
}
function seek (time, callback) {
@@ -176,7 +146,7 @@ function chromecastPlayer () {
// airplay player implementation
function airplayPlayer () {
const ret = {
var ret = {
device: null,
addDevice,
getDevices,
@@ -213,7 +183,7 @@ function airplayPlayer () {
}
function open () {
ret.device.play(state.server.networkURL + '/' + state.playing.fileIndex, function (err, res) {
ret.device.play(state.server.networkURL, function (err, res) {
if (err) {
state.playing.location = 'local'
state.errors.push({
@@ -268,7 +238,7 @@ function airplayPlayer () {
// DLNA player implementation
function dlnaPlayer (player) {
const ret = {
var ret = {
device: null,
addDevice,
getDevices,
@@ -304,8 +274,8 @@ function dlnaPlayer (player) {
}
function open () {
const torrentSummary = state.saved.torrents.find((x) => x.infoHash === state.playing.infoHash)
ret.device.play(state.server.networkURL + '/' + state.playing.fileIndex, {
var torrentSummary = state.saved.torrents.find((x) => x.infoHash === state.playing.infoHash)
ret.device.play(state.server.networkURL, {
type: 'video/mp4',
title: config.APP_NAME + ' - ' + torrentSummary.name,
seek: state.playing.currentTime > 10 ? state.playing.currentTime : 0
@@ -336,7 +306,13 @@ function dlnaPlayer (player) {
}
function status () {
ret.device.status(handleStatus)
ret.device.status(function (err, status) {
if (err) return console.log('error getting %s status: %o', state.playing.location, err)
state.playing.isPaused = status.playerState === 'PAUSED'
state.playing.currentTime = status.currentTime
state.playing.volume = status.volume.level
update()
})
}
function seek (time, callback) {
@@ -352,22 +328,10 @@ function dlnaPlayer (player) {
}
}
function handleStatus (err, status) {
if (err || !status) {
return console.log('error getting %s status: %o',
state.playing.location,
err || 'missing response')
}
state.playing.isPaused = status.playerState === 'PAUSED'
state.playing.currentTime = status.currentTime
state.playing.volume = status.volume.muted ? 0 : status.volume.level
update()
}
// Start polling cast device state, whenever we're connected
function startStatusInterval () {
statusInterval = setInterval(function () {
const player = getPlayer()
var player = getPlayer()
if (player) player.status()
}, 1000)
}
@@ -386,27 +350,23 @@ function toggleMenu (location) {
// Never cast to two devices at the same time
if (state.playing.location !== 'local') {
throw new CastingError(
`You can't connect to ${location} when already connected to another device`
)
throw new Error('You can\'t connect to ' + location + ' when already connected to another device')
}
// Find all cast devices of the given type
const player = getPlayer(location)
const devices = player ? player.getDevices() : []
if (devices.length === 0) {
throw new CastingError(`No ${location} devices available`)
}
var player = getPlayer(location)
var devices = player ? player.getDevices() : []
if (devices.length === 0) throw new Error('No ' + location + ' devices available')
// Show a menu
state.devices.castMenu = {location, devices}
}
function selectDevice (index) {
const {location, devices} = state.devices.castMenu
var {location, devices} = state.devices.castMenu
// Start casting
const player = getPlayer(location)
var player = getPlayer(location)
player.device = devices[index]
player.open()
@@ -422,7 +382,7 @@ function selectDevice (index) {
// Stops casting, move video back to local screen
function stop () {
const player = getPlayer()
var player = getPlayer()
if (player) {
player.stop(function () {
player.device = null
@@ -457,18 +417,18 @@ function getPlayer (location) {
}
function play () {
const player = getPlayer()
var player = getPlayer()
if (player) player.play(castCallback)
}
function pause () {
const player = getPlayer()
var player = getPlayer()
if (player) player.pause(castCallback)
}
function setRate (rate) {
let player
let result = true
var player
var result = true
if (state.playing.location === 'chromecast') {
// TODO find how to control playback rate on chromecast
castCallback()
@@ -483,12 +443,12 @@ function setRate (rate) {
}
function seek (time) {
const player = getPlayer()
var player = getPlayer()
if (player) player.seek(time, castCallback)
}
function setVolume (volume) {
const player = getPlayer()
var player = getPlayer()
if (player) player.volume(volume, castCallback)
}

View File

@@ -4,8 +4,8 @@ module.exports = {
setDispatch
}
const dispatchers = {}
let _dispatch = function () {}
var dispatchers = {}
var _dispatch = function () {}
function setDispatch (dispatch) {
_dispatch = dispatch
@@ -20,8 +20,8 @@ function dispatch (...args) {
// function. This prevents React from updating the listener functions on
// each update().
function dispatcher (...args) {
const str = JSON.stringify(args)
let handler = dispatchers[str]
var str = JSON.stringify(args)
var handler = dispatchers[str]
if (!handler) {
handler = dispatchers[str] = function (e) {
// Do not propagate click to elements below the button

View File

@@ -1,41 +1,8 @@
const ExtendableError = require('es6-error')
/* Generic errors */
class CastingError extends ExtendableError {}
class PlaybackError extends ExtendableError {}
class SoundError extends ExtendableError {}
class TorrentError extends ExtendableError {}
/* Playback */
class UnplayableTorrentError extends PlaybackError {
constructor () { super('Can\'t play any files in torrent') }
}
class UnplayableFileError extends PlaybackError {
constructor () { super('Can\'t play that file') }
}
/* Sound */
class InvalidSoundNameError extends SoundError {
constructor (name) { super(`Invalid sound name: ${name}`) }
}
/* Torrent */
class TorrentKeyNotFoundError extends TorrentError {
constructor (torrentKey) { super(`Can't resolve torrent key ${torrentKey}`) }
}
module.exports = {
CastingError,
PlaybackError,
SoundError,
TorrentError,
UnplayableTorrentError,
UnplayableFileError,
InvalidSoundNameError,
TorrentKeyNotFoundError
UnplayableError
}
function UnplayableError () {
this.message = 'Can\'t play any files in torrent'
}
UnplayableError.prototype = Error

View File

@@ -4,11 +4,10 @@ module.exports = {
run
}
const fs = require('fs')
const path = require('path')
const semver = require('semver')
const config = require('../../config')
const TorrentSummary = require('./torrent-summary')
const fs = require('fs')
// Change `state.saved` (which will be saved back to config.json on exit) as
// needed, for example to deal with config.json format changes across versions
@@ -18,27 +17,34 @@ function run (state) {
state.saved.version = '0.0.0' // Pre-0.7.0 version, so run all migrations
}
const version = state.saved.version
const saved = state.saved
var version = state.saved.version
if (semver.lt(version, '0.7.0')) migrate_0_7_0(saved)
if (semver.lt(version, '0.7.2')) migrate_0_7_2(saved)
if (semver.lt(version, '0.11.0')) migrate_0_11_0(saved)
if (semver.lt(version, '0.12.0')) migrate_0_12_0(saved)
if (semver.lt(version, '0.14.0')) migrate_0_14_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.7.0')) {
migrate_0_7_0(state.saved)
}
if (semver.lt(version, '0.7.2')) {
migrate_0_7_2(state.saved)
}
if (semver.lt(version, '0.11.0')) {
migrate_0_11_0(state.saved)
}
if (semver.lt(version, '0.12.0')) {
migrate_0_12_0(state.saved)
}
// Config is now on the new version
state.saved.version = config.APP_VERSION
}
function migrate_0_7_0 (saved) {
const cpFile = require('cp-file')
const path = require('path')
var fs = require('fs-extra')
var path = require('path')
saved.torrents.forEach(function (ts) {
const infoHash = ts.infoHash
var infoHash = ts.infoHash
// Replace torrentPath with torrentFileName
// There are a number of cases to handle here:
@@ -46,7 +52,7 @@ function migrate_0_7_0 (saved) {
// * Then, relative paths for the default torrents, eg '../static/sintel.torrent'
// * Then, paths computed at runtime for default torrents, eg 'sintel.torrent'
// * Finally, now we're getting rid of torrentPath altogether
let src, dst
var src, dst
if (ts.torrentPath) {
if (path.isAbsolute(ts.torrentPath) || ts.torrentPath.startsWith('..')) {
src = ts.torrentPath
@@ -56,7 +62,7 @@ function migrate_0_7_0 (saved) {
dst = path.join(config.TORRENT_PATH, infoHash + '.torrent')
// Synchronous FS calls aren't ideal, but probably OK in a migration
// that only runs once
if (src !== dst) cpFile.sync(src, dst)
if (src !== dst) fs.copySync(src, dst)
delete ts.torrentPath
ts.torrentFileName = infoHash + '.torrent'
@@ -64,21 +70,21 @@ function migrate_0_7_0 (saved) {
// Replace posterURL with posterFileName
if (ts.posterURL) {
const extension = path.extname(ts.posterURL)
var extension = path.extname(ts.posterURL)
src = path.isAbsolute(ts.posterURL)
? ts.posterURL
: path.join(config.STATIC_PATH, ts.posterURL)
dst = path.join(config.POSTER_PATH, infoHash + extension)
// Synchronous FS calls aren't ideal, but probably OK in a migration
// that only runs once
if (src !== dst) cpFile.sync(src, dst)
if (src !== dst) fs.copySync(src, dst)
delete ts.posterURL
ts.posterFileName = infoHash + extension
}
// Fix exception caused by incorrect file ordering.
// https://github.com/webtorrent/webtorrent-desktop/pull/604#issuecomment-222805214
// https://github.com/feross/webtorrent-desktop/pull/604#issuecomment-222805214
delete ts.defaultPlayFileIndex
delete ts.files
delete ts.selections
@@ -102,8 +108,6 @@ function migrate_0_11_0 (saved) {
}
function migrate_0_12_0 (saved) {
const TorrentSummary = require('./torrent-summary')
if (saved.prefs.openExternalPlayer == null && saved.prefs.playInVlc != null) {
saved.prefs.openExternalPlayer = saved.prefs.playInVlc
}
@@ -111,8 +115,8 @@ function migrate_0_12_0 (saved) {
// Undo a terrible bug where clicking Play on a default torrent on a fresh
// install results in a "path missing" error
// See https://github.com/webtorrent/webtorrent-desktop/pull/806
const defaultTorrentFiles = [
// See https://github.com/feross/webtorrent-desktop/pull/806
var defaultTorrentFiles = [
'6a9759bffd5c0af65319979fb7832189f4f3c35d.torrent',
'88594aaacbde40ef3e2510c47374ec0aa396c08e.torrent',
'6a02592d2bbc069628cd5ed8a54f88ee06ac0ba5.torrent',
@@ -121,88 +125,13 @@ function migrate_0_12_0 (saved) {
]
saved.torrents.forEach(function (torrentSummary) {
if (!defaultTorrentFiles.includes(torrentSummary.torrentFileName)) return
const fileOrFolder = TorrentSummary.getFileOrFolder(torrentSummary)
var fileOrFolder = TorrentSummary.getFileOrFolder(torrentSummary)
if (!fileOrFolder) return
try {
fs.statSync(fileOrFolder)
} catch (err) {
} catch (e) {
// Default torrent with "missing path" error. Clear path.
delete torrentSummary.path
}
})
}
function migrate_0_14_0 (saved) {
saved.torrents.forEach(function (ts) {
delete ts.defaultPlayFileIndex
})
}
function migrate_0_17_0 (saved) {
// Fix a sad, sad bug that resulted in 100MB+ config.json files
saved.torrents.forEach(function (ts) {
if (!ts.files) return
ts.files.forEach(function (file) {
if (!file.audioInfo || !file.audioInfo.picture) return
// This contained a Buffer, which 30x'd in size when serialized to JSON
delete file.audioInfo.picture
})
})
}
function migrate_0_17_2 (saved) {
// Remove the trailing dot (.) from the Wired CD torrent name, since
// folders/files that end in a trailing dot (.) or space are not deletable from
// Windows Explorer. See: https://github.com/webtorrent/webtorrent-desktop/issues/905
const cpFile = require('cp-file')
const rimraf = require('rimraf')
const OLD_NAME = 'The WIRED CD - Rip. Sample. Mash. Share.'
const NEW_NAME = 'The WIRED CD - Rip. Sample. Mash. Share'
const OLD_HASH = '3ba219a8634bf7bae3d848192b2da75ae995589d'
const NEW_HASH = 'a88fda5954e89178c372716a6a78b8180ed4dad3'
const ts = saved.torrents.find((ts) => {
return ts.infoHash === OLD_HASH
})
if (!ts) return // Wired CD torrent does not exist
// New versions of WebTorrent ship with a fixed torrent file. Let's fix up the
// name in existing versions of WebTorrent.
ts.name = ts.displayName = NEW_NAME
ts.files.forEach((file) => {
file.path = file.path.replace(OLD_NAME, NEW_NAME)
})
// Changing the torrent name causes the info hash to change
ts.infoHash = NEW_HASH
ts.magnetURI = ts.magnetURI.replace(OLD_HASH, NEW_HASH)
try {
fs.renameSync(
path.join(config.POSTER_PATH, ts.posterFileName),
path.join(config.POSTER_PATH, NEW_HASH + '.jpg')
)
} catch (err) {}
ts.posterFileName = NEW_HASH + '.jpg'
rimraf.sync(path.join(config.TORRENT_PATH, ts.torrentFileName))
cpFile.sync(
path.join(config.STATIC_PATH, 'wiredCd.torrent'),
path.join(config.TORRENT_PATH, NEW_HASH + '.torrent')
)
ts.torrentFileName = NEW_HASH + '.torrent'
if (ts.path) {
// If torrent folder already exists on disk, try to rename it
try {
fs.renameSync(
path.join(ts.path, OLD_NAME),
path.join(ts.path, NEW_NAME)
)
} catch (err) {}
}
}

View File

@@ -1,87 +0,0 @@
module.exports = {
hasNext,
getNextIndex,
hasPrevious,
getPreviousIndex,
getCurrentLocalURL
}
const TorrentSummary = require('./torrent-summary')
const TorrentPlayer = require('./torrent-player')
const cache = {
infoHash: null,
previousIndex: null,
currentIndex: null,
nextIndex: null
}
function hasNext (state) {
updateCache(state)
return cache.nextIndex !== null
}
function getNextIndex (state) {
updateCache(state)
return cache.nextIndex
}
function hasPrevious (state) {
updateCache(state)
return cache.previousIndex !== null
}
function getPreviousIndex (state) {
updateCache(state)
return cache.previousIndex
}
function getCurrentLocalURL (state) {
return state.server
? state.server.localURL + '/' + state.playing.fileIndex
: ''
}
function updateCache (state) {
const infoHash = state.playing.infoHash
const fileIndex = state.playing.fileIndex
if (infoHash === cache.infoHash) {
switch (fileIndex) {
case cache.currentIndex:
return
case cache.nextIndex:
cache.previousIndex = cache.currentIndex
cache.currentIndex = fileIndex
cache.nextIndex = findNextIndex(state)
return
case cache.previousIndex:
cache.previousIndex = findPreviousIndex(state)
cache.nextIndex = cache.currentIndex
cache.currentIndex = fileIndex
return
}
} else {
cache.infoHash = infoHash
}
cache.previousIndex = findPreviousIndex(state)
cache.currentIndex = fileIndex
cache.nextIndex = findNextIndex(state)
}
function findPreviousIndex (state) {
const files = TorrentSummary.getByKey(state, state.playing.infoHash).files
for (let i = state.playing.fileIndex - 1; i >= 0; i--) {
if (TorrentPlayer.isPlayable(files[i])) return i
}
return null
}
function findNextIndex (state) {
const files = TorrentSummary.getByKey(state, state.playing.infoHash).files
for (let i = state.playing.fileIndex + 1; i < files.length; i++) {
if (TorrentPlayer.isPlayable(files[i])) return i
}
return null
}

View File

@@ -1,28 +1,28 @@
module.exports = {
preload,
play
}
const config = require('../../config')
const {InvalidSoundNameError} = require('./errors')
const path = require('path')
var config = require('../../config')
var path = require('path')
const VOLUME = 0.25
var VOLUME = 0.15
/* Cache of Audio elements, for instant playback */
const cache = {}
var cache = {}
const sounds = {
var sounds = {
ADD: {
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'add.wav'),
volume: VOLUME
},
DELETE: {
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'delete.wav'),
volume: VOLUME * 0.5
volume: VOLUME
},
DISABLE: {
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'disable.wav'),
volume: VOLUME * 0.5
volume: VOLUME
},
DONE: {
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'done.wav'),
@@ -30,7 +30,7 @@ const sounds = {
},
ENABLE: {
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'enable.wav'),
volume: VOLUME * 0.5
volume: VOLUME
},
ERROR: {
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'error.wav'),
@@ -42,16 +42,27 @@ const sounds = {
},
STARTUP: {
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'startup.wav'),
volume: VOLUME
volume: VOLUME * 2
}
}
function preload () {
for (var name in sounds) {
if (!cache[name]) {
var sound = sounds[name]
var audio = cache[name] = new window.Audio()
audio.volume = sound.volume
audio.src = sound.url
}
}
}
function play (name) {
let audio = cache[name]
var audio = cache[name]
if (!audio) {
const sound = sounds[name]
var sound = sounds[name]
if (!sound) {
throw new InvalidSoundNameError(name)
throw new Error('Invalid sound name')
}
audio = cache[name] = new window.Audio()
audio.volume = sound.volume

View File

@@ -1,30 +1,21 @@
const appConfig = require('application-config')('WebTorrent')
const path = require('path')
const {EventEmitter} = require('events')
var appConfig = require('application-config')('WebTorrent')
var path = require('path')
var {EventEmitter} = require('events')
const config = require('../../config')
var config = require('../../config')
var migrations = require('./migrations')
const SAVE_DEBOUNCE_INTERVAL = 1000
var State = module.exports = Object.assign(new EventEmitter(), {
getDefaultPlayState,
load,
save,
saveThrottled
})
appConfig.filePath = path.join(config.CONFIG_PATH, 'config.json')
const State = module.exports = Object.assign(new EventEmitter(), {
getDefaultPlayState,
load,
// state.save() calls are rate-limited. Use state.saveImmediate() to skip limit.
save: function () {
// Perf optimization: Lazy-require debounce (and it's dependencies)
const debounce = require('debounce')
// After first State.save() invokation, future calls go straight to the
// debounced function
State.save = debounce(saveImmediate, SAVE_DEBOUNCE_INTERVAL)
State.save(...arguments)
},
saveImmediate
})
function getDefaultState () {
const LocationHistory = require('location-history')
var LocationHistory = require('location-history')
return {
/*
@@ -77,9 +68,7 @@ function getDefaultState () {
* Getters, for convenience
*/
getPlayingTorrentSummary,
getPlayingFileSummary,
getExternalPlayerName,
shouldHidePlayerControls
getPlayingFileSummary
}
}
@@ -92,7 +81,6 @@ function getDefaultPlayState () {
type: null, /* 'audio' or 'video', could be 'other' if ever support eg streaming to VLC */
currentTime: 0, /* seconds */
duration: 1, /* seconds */
isReady: false,
isPaused: true,
isStalled: false,
lastTimeUpdate: 0, /* Unix time in ms */
@@ -109,42 +97,39 @@ function getDefaultPlayState () {
}
/* If the saved state file doesn't exist yet, here's what we use instead */
function setupStateSaved (cb) {
const cpFile = require('cp-file')
const fs = require('fs')
const parseTorrent = require('parse-torrent')
const parallel = require('run-parallel')
function setupSavedState (cb) {
var fs = require('fs-extra')
var parseTorrent = require('parse-torrent')
var parallel = require('run-parallel')
const saved = {
var saved = {
prefs: {
downloadPath: config.DEFAULT_DOWNLOAD_PATH,
isFileHandler: false,
openExternalPlayer: false,
externalPlayerPath: null,
startup: false,
autoAddTorrents: false,
torrentsFolderPath: ''
externalPlayerPath: null
},
torrents: config.DEFAULT_TORRENTS.map(createTorrentObject),
torrentsToResume: [],
version: config.APP_VERSION /* make sure we can upgrade gracefully later */
}
const tasks = []
var tasks = []
config.DEFAULT_TORRENTS.map(function (t, i) {
const infoHash = saved.torrents[i].infoHash
var infoHash = saved.torrents[i].infoHash
tasks.push(function (cb) {
cpFile(
fs.copy(
path.join(config.STATIC_PATH, t.posterFileName),
path.join(config.POSTER_PATH, infoHash + path.extname(t.posterFileName))
).then(cb).catch(cb)
path.join(config.POSTER_PATH, infoHash + path.extname(t.posterFileName)),
cb
)
})
tasks.push(function (cb) {
cpFile(
fs.copy(
path.join(config.STATIC_PATH, t.torrentFileName),
path.join(config.TORRENT_PATH, infoHash + '.torrent')
).then(cb).catch(cb)
path.join(config.TORRENT_PATH, infoHash + '.torrent'),
cb
)
})
})
@@ -154,9 +139,8 @@ function setupStateSaved (cb) {
})
function createTorrentObject (t) {
// TODO: Doing several fs.readFileSync calls during first startup is not ideal
const torrent = fs.readFileSync(path.join(config.STATIC_PATH, t.torrentFileName))
const parsedTorrent = parseTorrent(torrent)
var torrent = fs.readFileSync(path.join(config.STATIC_PATH, t.torrentFileName))
var parsedTorrent = parseTorrent(torrent)
return {
status: 'paused',
@@ -167,80 +151,62 @@ function setupStateSaved (cb) {
torrentFileName: parsedTorrent.infoHash + '.torrent',
magnetURI: parseTorrent.toMagnetURI(parsedTorrent),
files: parsedTorrent.files,
selections: parsedTorrent.files.map((x) => true),
testID: t.testID
selections: parsedTorrent.files.map((x) => true)
}
}
}
function getPlayingTorrentSummary () {
const infoHash = this.playing.infoHash
var infoHash = this.playing.infoHash
return this.saved.torrents.find((x) => x.infoHash === infoHash)
}
function getPlayingFileSummary () {
const torrentSummary = this.getPlayingTorrentSummary()
var torrentSummary = this.getPlayingTorrentSummary()
if (!torrentSummary) return null
return torrentSummary.files[this.playing.fileIndex]
}
function getExternalPlayerName () {
const playerPath = this.saved.prefs.externalPlayerPath
if (!playerPath) return 'VLC'
return path.basename(playerPath).split('.')[0]
}
function shouldHidePlayerControls () {
return this.location.url() === 'player' &&
this.playing.mouseStationarySince !== 0 &&
new Date().getTime() - this.playing.mouseStationarySince > 2000 &&
!this.playing.mouseInControls &&
!this.playing.isPaused &&
this.playing.location === 'local' &&
this.playing.playbackRate === 1
}
function load (cb) {
var state = getDefaultState()
appConfig.read(function (err, saved) {
if (err || !saved.version) {
console.log('Missing config file: Creating new one')
setupStateSaved(onSavedState)
setupSavedState(onSaved)
} else {
onSavedState(null, saved)
onSaved(null, saved)
}
})
function onSavedState (err, saved) {
function onSaved (err, saved) {
if (err) return cb(err)
const state = getDefaultState()
state.saved = saved
if (process.type === 'renderer') {
// Perf optimization: Save require() calls in the main process
const migrations = require('./migrations')
migrations.run(state)
}
migrations.run(state)
cb(null, state)
}
}
// Write state.saved to the JSON state file
function saveImmediate (state, cb) {
function save (state, cb) {
console.log('Saving state to ' + appConfig.filePath)
delete state.saveStateTimeout
// Clean up, so that we're not saving any pending state
const copy = Object.assign({}, state.saved)
var copy = Object.assign({}, state.saved)
// Remove torrents pending addition to the list, where we haven't finished
// reading the torrent file or file(s) to seed & don't have an infohash
copy.torrents = copy.torrents
.filter((x) => x.infoHash)
.map(function (x) {
const torrent = {}
for (let key in x) {
var torrent = {}
for (var key in x) {
if (key === 'progress' || key === 'torrentKey') {
continue // Don't save progress info or key for the webtorrent process
}
if (key === 'playStatus') {
continue // Don't save whether a torrent is playing / pending
}
if (key === 'error') {
continue // Don't save error states
}
@@ -251,6 +217,15 @@ function saveImmediate (state, cb) {
appConfig.write(copy, (err) => {
if (err) console.error(err)
else State.emit('stateSaved')
else State.emit('savedState')
})
}
// Write, but no more than once a second
function saveThrottled (state) {
if (state.saveStateTimeout) return
state.saveStateTimeout = setTimeout(function () {
if (!state.saveStateTimeout) return
save(state)
}, 1000)
}

View File

@@ -2,62 +2,44 @@
// Reports back so that we can improve WebTorrent Desktop
module.exports = {
init,
send,
logUncaughtError,
logPlayAttempt
}
const crypto = require('crypto')
const electron = require('electron')
const https = require('https')
const os = require('os')
const url = require('url')
const config = require('../../config')
let telemetry
var telemetry
function init (state) {
telemetry = state.saved.telemetry
// First app run
if (!telemetry) {
const crypto = require('crypto')
telemetry = state.saved.telemetry = {
userID: crypto.randomBytes(32).toString('hex') // 256-bit random ID
}
telemetry = state.saved.telemetry = createSummary()
reset()
}
}
function send (state) {
const now = new Date()
var now = new Date()
telemetry.version = config.APP_VERSION
telemetry.timestamp = now.toISOString()
telemetry.localTime = now.toTimeString()
telemetry.screens = getScreenInfo()
telemetry.system = getSystemInfo()
telemetry.torrentStats = getTorrentStats(state)
telemetry.approxNumTorrents = telemetry.torrentStats.approxCount
telemetry.approxNumTorrents = getApproxNumTorrents(state)
if (!config.IS_PRODUCTION) {
if (config.IS_PRODUCTION) {
postToServer()
// If the user keeps WebTorrent running for a long time, post every 12h
setInterval(postToServer, 12 * 3600 * 1000)
} else {
// Development: telemetry used only for local debugging
// Empty uncaught errors, etc at the start of every run
return reset()
}
const get = require('simple-get')
const opts = {
url: config.TELEMETRY_URL,
body: telemetry,
json: true
}
get.post(opts, function (err, res) {
if (err) return console.error('Error sending telemetry', err)
if (res.statusCode !== 200) {
return console.error(`Error sending telemetry, status code: ${res.statusCode}`)
}
console.log('Sent telemetry')
reset()
})
}
}
function reset () {
@@ -66,12 +48,47 @@ function reset () {
minVersion: config.APP_VERSION,
total: 0,
success: 0,
timeout: 0,
error: 0,
external: 0,
abandoned: 0
}
}
function postToServer () {
// Serialize the telemetry summary
var payload = new Buffer(JSON.stringify(telemetry), 'utf8')
// POST to our server
var options = url.parse(config.TELEMETRY_URL)
options.method = 'POST'
options.headers = {
'Content-Type': 'application/json',
'Content-Length': payload.length
}
var req = https.request(options, function (res) {
if (res.statusCode === 200) {
console.log('Successfully posted telemetry summary')
reset()
} else {
console.error('Couldn\'t post telemetry summary, got HTTP ' + res.statusCode)
}
})
req.on('error', function (e) {
console.error('Couldn\'t post telemetry summary', e)
})
req.write(payload)
req.end()
}
// Creates a new telemetry summary. Gives the user a unique ID,
// collects screen resolution, etc
function createSummary () {
// Make a 256-bit random unique ID
var userID = crypto.randomBytes(32).toString('hex')
return { userID }
}
// Track screen resolution
function getScreenInfo () {
return electron.screen.getAllDisplays().map((screen) => ({
@@ -83,69 +100,22 @@ function getScreenInfo () {
// Track basic system info like OS version and amount of RAM
function getSystemInfo () {
const os = require('os')
return {
osPlatform: process.platform,
osRelease: os.type() + ' ' + os.release(),
architecture: os.arch(),
systemArchitecture: config.OS_SYSARCH,
totalMemoryMB: roundPow2(os.totalmem() / (1 << 20)),
totalMemoryMB: os.totalmem() / (1 << 20),
numCores: os.cpus().length
}
}
// Get stats like the # of torrents currently active, # in list, total size
function getTorrentStats (state) {
const count = state.saved.torrents.length
let sizeMB = 0
let byStatus = {
new: { count: 0, sizeMB: 0 },
downloading: { count: 0, sizeMB: 0 },
seeding: { count: 0, sizeMB: 0 },
paused: { count: 0, sizeMB: 0 }
}
// First, count torrents & total file size
for (let i = 0; i < count; i++) {
const t = state.saved.torrents[i]
const stat = byStatus[t.status]
if (!t || !t.files || !stat) continue
stat.count++
for (let j = 0; j < t.files.length; j++) {
const f = t.files[j]
if (!f || !f.length) continue
const fileSizeMB = f.length / (1 << 20)
sizeMB += fileSizeMB
stat.sizeMB += fileSizeMB
}
}
// Then, round all the counts and sums to the nearest power of 2
const ret = roundTorrentStats({count, sizeMB})
ret.byStatus = {
new: roundTorrentStats(byStatus.new),
downloading: roundTorrentStats(byStatus.downloading),
seeding: roundTorrentStats(byStatus.seeding),
paused: roundTorrentStats(byStatus.paused)
}
return ret
}
function roundTorrentStats (stats) {
return {
approxCount: roundPow2(stats.count),
approxSizeMB: roundPow2(stats.sizeMB)
}
}
// Rounds to the nearest power of 2, for privacy and easy bucketing.
// Rounds 35 to 32, 70 to 64, 5 to 4, 1 to 1, 0 to 0.
// Supports nonnegative numbers only.
function roundPow2 (n) {
if (n <= 0) return 0
// Get the number of torrents, rounded to the nearest power of two
function getApproxNumTorrents (state) {
var exactNum = state.saved.torrents.length
if (exactNum === 0) return 0
// Otherwise, return 1, 2, 4, 8, etc by rounding in log space
const log2 = Math.log(n) / Math.log(2)
return Math.pow(2, Math.round(log2))
var log2 = Math.log(exactNum) / Math.log(2)
return 1 << Math.round(log2)
}
// An uncaught error happened in the main process or in one of the windows
@@ -154,10 +124,12 @@ function logUncaughtError (procName, e) {
// Hopefully uncaught errors immediately on startup are fixed in dev
if (!telemetry) return
let message
let stack = ''
if (e == null) {
message = 'Unexpected undefined error'
var message
var stack = ''
if (e.message) {
// err is either an Error or a plain object {message, stack}
message = e.message
stack = e.stack
} else if (e.error) {
// Uncaught Javascript errors (window.onerror), err is an ErrorEvent
if (!e.error.message) {
@@ -166,10 +138,6 @@ function logUncaughtError (procName, e) {
message = e.error.message
stack = e.error.stack
}
} else if (e.message) {
// err is either an Error or a plain object {message, stack}
message = e.message
stack = e.stack
} else {
// Resource errors (captured element.onerror), err is an Event
if (!e.target) {
@@ -181,14 +149,11 @@ function logUncaughtError (procName, e) {
}
}
if (typeof stack !== 'string') stack = 'Unexpected stack: ' + stack
if (typeof message !== 'string') message = 'Unexpected message: ' + message
// Remove the first part of each file path in the stack trace.
// - Privacy: remove personal info like C:\Users\<full name>
// - Aggregation: this lets us find which stacktraces occur often
stack = stack.replace(/\(.*app.asar/g, '(...')
stack = stack.replace(/at .*app.asar/g, 'at ...')
if (stack && typeof stack === 'string') stack = stack.replace(/\(.*app.asar/g, '(...')
else if (stack) stack = 'Unexpected stack: ' + stack
// We need to POST the telemetry object, make sure it stays < 100kb
if (telemetry.uncaughtErrors.length > 20) return
@@ -196,28 +161,28 @@ function logUncaughtError (procName, e) {
if (stack.length > 1000) stack = stack.substring(0, 1000)
// Log the app version *at the time of the error*
const version = config.APP_VERSION
var version = config.APP_VERSION
telemetry.uncaughtErrors.push({process: procName, message, stack, version})
}
// Turns a DOM element into a string, eg "DIV.my-class.visible"
function getElemString (elem) {
let ret = elem.tagName
var ret = elem.tagName
try {
ret += '.' + Array.from(elem.classList).join('.')
} catch (err) {}
} catch (e) {}
return ret
}
// The user pressed play. Did it work, display an error,
// open an external player or did user abandon the attempt?
// The user pressed play. It either worked, timed out, or showed the
// 'Play in VLC' codec error
function logPlayAttempt (result) {
if (!['success', 'error', 'external', 'abandoned'].includes(result)) {
if (!['success', 'timeout', 'error', 'abandoned'].includes(result)) {
return console.error('Unknown play attempt result', result)
}
const attempts = telemetry.playAttempts
var attempts = telemetry.playAttempts
attempts.total = (attempts.total || 0) + 1
attempts[result] = (attempts[result] || 0) + 1
}

View File

@@ -3,10 +3,11 @@ module.exports = {
isVideo,
isAudio,
isTorrent,
isPlayableTorrentSummary
isPlayableTorrentSummary,
pickFileToPlay
}
const path = require('path')
var path = require('path')
// Checks whether a fileSummary or file path is audio/video that we can play,
// based on the file extension
@@ -37,7 +38,6 @@ function isAudio (file) {
'.mp3',
'.ogg',
'.wav',
'.flac',
'.m4a'
].includes(getFileExtension(file))
}
@@ -47,16 +47,38 @@ function isAudio (file) {
// - a file object where obj.name is ends in .torrent
// - a string that's a magnet link (magnet://...)
function isTorrent (file) {
const isTorrentFile = getFileExtension(file) === '.torrent'
const isMagnet = typeof file === 'string' && /^(stream-)?magnet:/.test(file)
var isTorrentFile = getFileExtension(file) === '.torrent'
var isMagnet = typeof file === 'string' && /^(stream-)?magnet:/.test(file)
return isTorrentFile || isMagnet
}
function getFileExtension (file) {
const name = typeof file === 'string' ? file : file.name
var name = typeof file === 'string' ? file : file.name
return path.extname(name).toLowerCase()
}
function isPlayableTorrentSummary (torrentSummary) {
return torrentSummary.files && torrentSummary.files.some(isPlayable)
}
// Picks the default file to play from a list of torrent or torrentSummary files
// Returns an index or undefined, if no files are playable
function pickFileToPlay (files) {
// first, try to find the biggest video file
var videoFiles = files.filter(isVideo)
if (videoFiles.length > 0) {
var largestVideoFile = videoFiles.reduce(function (a, b) {
return a.length > b.length ? a : b
})
return files.indexOf(largestVideoFile)
}
// if there are no videos, play the first audio file
var audioFiles = files.filter(isAudio)
if (audioFiles.length > 0) {
return files.indexOf(audioFiles[0])
}
// no video or audio means nothing is playable
return undefined
}

View File

@@ -1,22 +1,22 @@
module.exports = torrentPoster
const captureFrame = require('capture-frame')
const path = require('path')
var captureVideoFrame = require('./capture-video-frame')
var path = require('path')
function torrentPoster (torrent, cb) {
// First, try to use a poster image if available
const posterFile = torrent.files.filter(function (file) {
var posterFile = torrent.files.filter(function (file) {
return /^poster\.(jpg|png|gif)$/.test(file.name)
})[0]
if (posterFile) return torrentPosterFromImage(posterFile, torrent, cb)
// Second, try to use the largest video file
// Filter out file formats that the <video> tag definitely can't play
const videoFile = getLargestFileByExtension(torrent, ['.mp4', '.m4v', '.webm', '.mov', '.mkv'])
var videoFile = getLargestFileByExtension(torrent, ['.mp4', '.m4v', '.webm', '.mov', '.mkv'])
if (videoFile) return torrentPosterFromVideo(videoFile, torrent, cb)
// Third, try to use the largest image file
const imgFile = getLargestFileByExtension(torrent, ['.gif', '.jpg', '.jpeg', '.png'])
var imgFile = getLargestFileByExtension(torrent, ['.gif', '.jpg', '.jpeg', '.png'])
if (imgFile) return torrentPosterFromImage(imgFile, torrent, cb)
// TODO: generate a waveform from the largest sound file
@@ -25,8 +25,8 @@ function torrentPoster (torrent, cb) {
}
function getLargestFileByExtension (torrent, extensions) {
const files = torrent.files.filter(function (file) {
const extname = path.extname(file.name).toLowerCase()
var files = torrent.files.filter(function (file) {
var extname = path.extname(file.name).toLowerCase()
return extensions.indexOf(extname) !== -1
})
if (files.length === 0) return undefined
@@ -36,15 +36,15 @@ function getLargestFileByExtension (torrent, extensions) {
}
function torrentPosterFromVideo (file, torrent, cb) {
const index = torrent.files.indexOf(file)
var index = torrent.files.indexOf(file)
const server = torrent.createServer(0)
var server = torrent.createServer(0)
server.listen(0, onListening)
function onListening () {
const port = server.address().port
const url = 'http://localhost:' + port + '/' + index
const video = document.createElement('video')
var port = server.address().port
var url = 'http://localhost:' + port + '/' + index
var video = document.createElement('video')
video.addEventListener('canplay', onCanPlay)
video.volume = 0
@@ -61,7 +61,7 @@ function torrentPosterFromVideo (file, torrent, cb) {
function onSeeked () {
video.removeEventListener('seeked', onSeeked)
const buf = captureFrame(video)
var buf = captureVideoFrame(video)
// unload video element
video.pause()
@@ -78,6 +78,6 @@ function torrentPosterFromVideo (file, torrent, cb) {
}
function torrentPosterFromImage (file, torrent, cb) {
const extname = path.extname(file.name)
var extname = path.extname(file.name)
file.getBuffer((err, buf) => cb(err, buf, extname))
}

View File

@@ -2,12 +2,12 @@ module.exports = {
getPosterPath,
getTorrentPath,
getByKey,
getTorrentId,
getTorrentID,
getFileOrFolder
}
const path = require('path')
const config = require('../../config')
var path = require('path')
var config = require('../../config')
// Expects a torrentSummary
// Returns an absolute path to the torrent file, or null if unavailable
@@ -20,7 +20,7 @@ function getTorrentPath (torrentSummary) {
// Returns an absolute path to the poster image, or null if unavailable
function getPosterPath (torrentSummary) {
if (!torrentSummary || !torrentSummary.posterFileName) return null
const posterPath = path.join(config.POSTER_PATH, torrentSummary.posterFileName)
var posterPath = path.join(config.POSTER_PATH, torrentSummary.posterFileName)
// Work around a Chrome bug (reproduced in vanilla Chrome, not just Electron):
// Backslashes in URLS in CSS cause bizarre string encoding issues
return posterPath.replace(/\\/g, '/')
@@ -28,8 +28,8 @@ function getPosterPath (torrentSummary) {
// Expects a torrentSummary
// Returns a torrentID: filename, magnet URI, or infohash
function getTorrentId (torrentSummary) {
const s = torrentSummary
function getTorrentID (torrentSummary) {
var s = torrentSummary
if (s.torrentFileName) { // Load torrent file from disk
return getTorrentPath(s)
} else { // Load torrent from DHT
@@ -51,8 +51,7 @@ function getByKey (state, torrentKey) {
// TODO: make this assumption explicit, enforce it in the `create-torrent`
// module. Store root folder explicitly to avoid hacky path processing below.
function getFileOrFolder (torrentSummary) {
const ts = torrentSummary
var ts = torrentSummary
if (!ts.path || !ts.files || ts.files.length === 0) return null
const dirname = ts.files[0].path.split(path.sep)[0]
return path.join(ts.path, dirname)
return path.join(ts.path, ts.files[0].path.split('/')[0])
}

View File

@@ -1,30 +1,8 @@
/**
* Perf optimization: Hook into require() to modify how certain modules load:
*
* - `inline-style-prefixer` (used by `material-ui`) takes ~40ms. It is not
* actually used because auto-prefixing is disabled with
* `darkBaseTheme.userAgent = false`. Return a fake object.
*/
let Module = require('module')
const _require = Module.prototype.require
Module.prototype.require = function (id) {
if (id === 'inline-style-prefixer') return {}
return _require.apply(this, arguments)
}
console.time('init')
const crashReporter = require('../crash-reporter')
crashReporter.init()
// Perf optimization: Start asynchronously read on config file before all the
// blocking require() calls below.
const State = require('./lib/state')
State.load(onState)
const createGetter = require('fn-getter')
const debounce = require('debounce')
const dragDrop = require('drag-drop')
const electron = require('electron')
const fs = require('fs')
@@ -34,88 +12,62 @@ const ReactDOM = require('react-dom')
const config = require('../config')
const telemetry = require('./lib/telemetry')
const sound = require('./lib/sound')
const State = require('./lib/state')
const TorrentPlayer = require('./lib/torrent-player')
// Perf optimization: Needed immediately, so do not lazy load it below
const TorrentListController = require('./controllers/torrent-list-controller')
// Required by Material UI -- adds `onTouchTap` event
require('react-tap-event-plugin')()
const App = require('./pages/app')
const App = require('./pages/App')
// Electron apps have two processes: a main process (node) runs first and starts
// a renderer process (essentially a Chrome window). We're in the renderer process,
// and this IPC channel receives from and sends messages to the main process
const ipcRenderer = electron.ipcRenderer
const MediaController = require('./controllers/media-controller')
const UpdateController = require('./controllers/update-controller')
const PrefsController = require('./controllers/prefs-controller')
const PlaybackController = require('./controllers/playback-controller')
const SubtitlesController = require('./controllers/subtitles-controller')
const TorrentListController = require('./controllers/torrent-list-controller')
const TorrentController = require('./controllers/torrent-controller')
// Yo-yo pattern: state object lives here and percolates down thru all the views.
// Events come back up from the views via dispatch(...)
require('./lib/dispatcher').setDispatch(dispatch)
// From dispatch(...), events are sent to one of the controllers
let controllers = null
var controllers = null
// This dependency is the slowest-loading, so we lazy load it
let Cast = null
var Cast = null
// Electron apps have two processes: a main process (node) runs first and starts
// a renderer process (essentially a Chrome window). We're in the renderer process,
// and this IPC channel receives from and sends messages to the main process
var ipcRenderer = electron.ipcRenderer
// All state lives in state.js. `state.saved` is read from and written to a file.
// All other state is ephemeral. First we load state.saved then initialize the app.
let state
var state
// Root React component
let app
var app
State.load(onState)
// Called once when the application loads. (Not once per window.)
// Connects to the torrent networks, sets up the UI and OS integrations like
// the dock icon and drag+drop.
function onState (err, _state) {
if (err) return onError(err)
// Make available for easier debugging
state = window.state = _state
window.dispatch = dispatch
telemetry.init(state)
// Log uncaught JS errors
window.addEventListener(
'error', (e) => telemetry.logUncaughtError('window', e), true /* capture */
)
state = window.state = _state // Make available for easier debugging
// Create controllers
controllers = {
media: createGetter(() => {
const MediaController = require('./controllers/media-controller')
return new MediaController(state)
}),
playback: createGetter(() => {
const PlaybackController = require('./controllers/playback-controller')
return new PlaybackController(state, config, update)
}),
prefs: createGetter(() => {
const PrefsController = require('./controllers/prefs-controller')
return new PrefsController(state, config)
}),
subtitles: createGetter(() => {
const SubtitlesController = require('./controllers/subtitles-controller')
return new SubtitlesController(state)
}),
torrent: createGetter(() => {
const TorrentController = require('./controllers/torrent-controller')
return new TorrentController(state)
}),
torrentList: createGetter(() => {
return new TorrentListController(state)
}),
update: createGetter(() => {
const UpdateController = require('./controllers/update-controller')
return new UpdateController(state)
}),
folderWatcher: createGetter(() => {
const FolderWatcherController = require('./controllers/folder-watcher-controller')
return new FolderWatcherController()
})
media: new MediaController(state),
update: new UpdateController(state),
prefs: new PrefsController(state, config),
playback: new PlaybackController(state, config, update),
subtitles: new SubtitlesController(state),
torrentList: new TorrentListController(state),
torrent: new TorrentController(state)
}
// Add first page to location history
@@ -130,18 +82,23 @@ function onState (err, _state) {
// Restart everything we were torrenting last time the app ran
resumeTorrents()
// Initialize ReactDOM
app = ReactDOM.render(<App state={state} />, document.querySelector('#body'))
// Calling update() updates the UI given the current state
// Do this at least once a second to give every file in every torrentSummary
// a progress bar and to keep the cursor in sync when playing a video
setInterval(update, 1000)
app = ReactDOM.render(<App state={state} />, document.querySelector('#body'))
// Lazy-load other stuff, like the AppleTV module, later to keep startup fast
window.setTimeout(delayedInit, config.DELAYED_INIT)
// Listen for messages from the main process
setupIpc()
// Drag and drop files/text to start torrenting or seeding
// Warn if the download dir is gone, eg b/c an external drive is unplugged
checkDownloadPath()
// OS integrations:
// ...drag and drop files/text to start torrenting or seeding
dragDrop('body', {
onDrop: onOpen,
onDropText: onOpen
@@ -150,49 +107,28 @@ function onState (err, _state) {
// ...same thing if you paste a torrent
document.addEventListener('paste', onPaste)
const debouncedFullscreenToggle = debounce(function () {
dispatch('toggleFullScreen')
}, 1000, true)
document.addEventListener('wheel', function (event) {
// ctrlKey detects pinch to zoom, http://crbug.com/289887
if (event.ctrlKey) {
event.preventDefault()
debouncedFullscreenToggle()
}
})
// ...focus and blur. Needed to show correct dock icon text ('badge') in OSX
window.addEventListener('focus', onFocus)
window.addEventListener('blur', onBlur)
if (electron.remote.getCurrentWindow().isVisible()) {
sound.play('STARTUP')
}
// ...window visibility state.
document.addEventListener('webkitvisibilitychange', onVisibilityChange)
// To keep app startup fast, some code is delayed.
window.setTimeout(delayedInit, config.DELAYED_INIT)
// Log uncaught JS errors
window.addEventListener('error',
(e) => telemetry.logUncaughtError('window', e),
true /* capture */)
// Done! Ideally we want to get here < 500ms after the user clicks the app
sound.play('STARTUP')
console.timeEnd('init')
}
// Runs a few seconds after the app loads, to avoid slowing down startup time
function delayedInit () {
telemetry.send(state)
// Send telemetry data every 12 hours, for users who keep the app running
// for extended periods of time
setInterval(() => telemetry.send(state), 12 * 3600 * 1000)
// Warn if the download dir is gone, eg b/c an external drive is unplugged
checkDownloadPath()
// ...window visibility state.
document.addEventListener('webkitvisibilitychange', onVisibilityChange)
onVisibilityChange()
lazyLoadCast()
sound.preload()
telemetry.init(state)
}
// Lazily loads Chromecast and Airplay support
@@ -210,7 +146,7 @@ function lazyLoadCast () {
// 3. dispatch - the event handler calls dispatch(), main.js sends it to a controller
// 4. controller - the controller handles the event, changing the state object
function update () {
controllers.playback().showOrHidePlayerControls()
controllers.playback.showOrHidePlayerControls()
app.setState(state)
updateElectron()
}
@@ -238,58 +174,42 @@ const dispatchHandlers = {
'openFiles': () => ipcRenderer.send('openFiles'), /* shows the open file dialog */
'openTorrentAddress': () => { state.modal = { id: 'open-torrent-address-modal' } },
'addTorrent': (torrentId) => controllers.torrentList().addTorrent(torrentId),
'showCreateTorrent': (paths) => controllers.torrentList().showCreateTorrent(paths),
'createTorrent': (options) => controllers.torrentList().createTorrent(options),
'toggleTorrent': (infoHash) => controllers.torrentList().toggleTorrent(infoHash),
'pauseAllTorrents': () => controllers.torrentList().pauseAllTorrents(),
'resumeAllTorrents': () => controllers.torrentList().resumeAllTorrents(),
'toggleTorrentFile': (infoHash, index) =>
controllers.torrentList().toggleTorrentFile(infoHash, index),
'confirmDeleteTorrent': (infoHash, deleteData) =>
controllers.torrentList().confirmDeleteTorrent(infoHash, deleteData),
'deleteTorrent': (infoHash, deleteData) =>
controllers.torrentList().deleteTorrent(infoHash, deleteData),
'toggleSelectTorrent': (infoHash) =>
controllers.torrentList().toggleSelectTorrent(infoHash),
'openTorrentContextMenu': (infoHash) =>
controllers.torrentList().openTorrentContextMenu(infoHash),
'startTorrentingSummary': (torrentKey) =>
controllers.torrentList().startTorrentingSummary(torrentKey),
'saveTorrentFileAs': (torrentKey) =>
controllers.torrentList().saveTorrentFileAs(torrentKey),
'prioritizeTorrent': (infoHash) => controllers.torrentList().prioritizeTorrent(infoHash),
'resumePausedTorrents': () => controllers.torrentList().resumePausedTorrents(),
'addTorrent': (torrentId) => controllers.torrentList.addTorrent(torrentId),
'showCreateTorrent': (paths) => controllers.torrentList.showCreateTorrent(paths),
'createTorrent': (options) => controllers.torrentList.createTorrent(options),
'toggleTorrent': (infoHash) => controllers.torrentList.toggleTorrent(infoHash),
'toggleTorrentFile': (infoHash, index) => controllers.torrentList.toggleTorrentFile(infoHash, index),
'confirmDeleteTorrent': (infoHash, deleteData) => controllers.torrentList.confirmDeleteTorrent(infoHash, deleteData),
'deleteTorrent': (infoHash, deleteData) => controllers.torrentList.deleteTorrent(infoHash, deleteData),
'toggleSelectTorrent': (infoHash) => controllers.torrentList.toggleSelectTorrent(infoHash),
'openTorrentContextMenu': (infoHash) => controllers.torrentList.openTorrentContextMenu(infoHash),
'startTorrentingSummary': (torrentKey) => controllers.torrentList.startTorrentingSummary(torrentKey),
// Playback
'playFile': (infoHash, index) => controllers.playback().playFile(infoHash, index),
'playPause': () => controllers.playback().playPause(),
'nextTrack': () => controllers.playback().nextTrack(),
'previousTrack': () => controllers.playback().previousTrack(),
'skip': (time) => controllers.playback().skip(time),
'skipTo': (time) => controllers.playback().skipTo(time),
'changePlaybackRate': (dir) => controllers.playback().changePlaybackRate(dir),
'changeVolume': (delta) => controllers.playback().changeVolume(delta),
'setVolume': (vol) => controllers.playback().setVolume(vol),
'openItem': (infoHash, index) => controllers.playback().openItem(infoHash, index),
'playFile': (infoHash, index) => controllers.playback.playFile(infoHash, index),
'playPause': () => controllers.playback.playPause(),
'skip': (time) => controllers.playback.skip(time),
'skipTo': (time) => controllers.playback.skipTo(time),
'changePlaybackRate': (dir) => controllers.playback.changePlaybackRate(dir),
'changeVolume': (delta) => controllers.playback.changeVolume(delta),
'setVolume': (vol) => controllers.playback.setVolume(vol),
'openItem': (infoHash, index) => controllers.playback.openItem(infoHash, index),
// Subtitles
'openSubtitles': () => controllers.subtitles().openSubtitles(),
'selectSubtitle': (index) => controllers.subtitles().selectSubtitle(index),
'toggleSubtitlesMenu': () => controllers.subtitles().toggleSubtitlesMenu(),
'checkForSubtitles': () => controllers.subtitles().checkForSubtitles(),
'addSubtitles': (files, autoSelect) => controllers.subtitles().addSubtitles(files, autoSelect),
'openSubtitles': () => controllers.subtitles.openSubtitles(),
'selectSubtitle': (index) => controllers.subtitles.selectSubtitle(index),
'toggleSubtitlesMenu': () => controllers.subtitles.toggleSubtitlesMenu(),
'checkForSubtitles': () => controllers.subtitles.checkForSubtitles(),
'addSubtitles': (files, autoSelect) => controllers.subtitles.addSubtitles(files, autoSelect),
// Local media: <video>, <audio>, external players
'mediaStalled': () => controllers.media().mediaStalled(),
'mediaError': (err) => controllers.media().mediaError(err),
'mediaSuccess': () => controllers.media().mediaSuccess(),
'mediaTimeUpdate': () => controllers.media().mediaTimeUpdate(),
'mediaMouseMoved': () => controllers.media().mediaMouseMoved(),
'mediaControlsMouseEnter': () => controllers.media().controlsMouseEnter(),
'mediaControlsMouseLeave': () => controllers.media().controlsMouseLeave(),
'openExternalPlayer': () => controllers.media().openExternalPlayer(),
'externalPlayerNotFound': () => controllers.media().externalPlayerNotFound(),
'mediaStalled': () => controllers.media.mediaStalled(),
'mediaError': (err) => controllers.media.mediaError(err),
'mediaSuccess': () => controllers.media.mediaSuccess(),
'mediaTimeUpdate': () => controllers.media.mediaTimeUpdate(),
'mediaMouseMoved': () => controllers.media.mediaMouseMoved(),
'openExternalPlayer': () => controllers.media.openExternalPlayer(),
'externalPlayerNotFound': () => controllers.media.externalPlayerNotFound(),
// Remote casting: Chromecast, Airplay, etc
'toggleCastMenu': (deviceType) => lazyLoadCast().toggleMenu(deviceType),
@@ -297,15 +217,13 @@ const dispatchHandlers = {
'stopCasting': () => lazyLoadCast().stop(),
// Preferences screen
'preferences': () => controllers.prefs().show(),
'updatePreferences': (key, value) => controllers.prefs().update(key, value),
'preferences': () => controllers.prefs.show(),
'updatePreferences': (key, value) => controllers.prefs.update(key, value),
'checkDownloadPath': checkDownloadPath,
'startFolderWatcher': () => controllers.folderWatcher().start(),
'stopFolderWatcher': () => controllers.folderWatcher().stop(),
// Update (check for new versions on Linux, where there's no auto updater)
'updateAvailable': (version) => controllers.update().updateAvailable(version),
'skipVersion': (version) => controllers.update().skipVersion(version),
'updateAvailable': (version) => controllers.update.updateAvailable(version),
'skipVersion': (version) => controllers.update.skipVersion(version),
// Navigation between screens (back, forward, ESC, etc)
'exitModal': () => { state.modal = null },
@@ -325,8 +243,8 @@ const dispatchHandlers = {
'onOpen': onOpen,
'error': onError,
'uncaughtError': (proc, err) => telemetry.logUncaughtError(proc, err),
'stateSave': () => State.save(state),
'stateSaveImmediate': () => State.saveImmediate(state),
'saveState': () => State.save(state),
'saveStateThrottled': () => State.saveThrottled(state),
'update': () => {} // No-op, just trigger an update
}
@@ -337,13 +255,13 @@ function dispatch (action, ...args) {
console.log('dispatch: %s %o', action, args)
}
const handler = dispatchHandlers[action]
var handler = dispatchHandlers[action]
if (handler) handler(...args)
else console.error('Missing dispatch handler: ' + action)
// Update the virtual DOM, unless it's just a mouse move event
if (action !== 'mediaMouseMoved' ||
controllers.playback().showOrHidePlayerControls()) {
controllers.playback.showOrHidePlayerControls()) {
update()
}
}
@@ -356,13 +274,11 @@ function setupIpc () {
ipcRenderer.on('dispatch', (e, ...args) => dispatch(...args))
ipcRenderer.on('fullscreenChanged', onFullscreenChanged)
ipcRenderer.on('windowBoundsChanged', onWindowBoundsChanged)
const tc = controllers.torrent()
var tc = controllers.torrent
ipcRenderer.on('wt-infohash', (e, ...args) => tc.torrentInfoHash(...args))
ipcRenderer.on('wt-metadata', (e, ...args) => tc.torrentMetadata(...args))
ipcRenderer.on('wt-done', (e, ...args) => tc.torrentDone(...args))
ipcRenderer.on('wt-done', () => controllers.torrentList().resumePausedTorrents())
ipcRenderer.on('wt-warning', (e, ...args) => tc.torrentWarning(...args))
ipcRenderer.on('wt-error', (e, ...args) => tc.torrentError(...args))
@@ -377,7 +293,7 @@ function setupIpc () {
ipcRenderer.send('ipcReady')
State.on('stateSaved', () => ipcRenderer.send('stateSaved'))
State.on('savedState', () => ipcRenderer.send('savedState'))
}
// Quits any modal popovers and returns to the torrent list screen
@@ -386,7 +302,7 @@ function backToList () {
state.modal = null
state.location.backToFirst(function () {
// If we were already on the torrent list, scroll to the top
const contentTag = document.querySelector('.content')
var contentTag = document.querySelector('.content')
if (contentTag) contentTag.scrollTop = 0
})
}
@@ -412,7 +328,7 @@ function resumeTorrents () {
return torrentSummary
})
.filter((s) => s.status !== 'paused')
.forEach((s) => controllers.torrentList().startTorrentingSummary(s.torrentKey))
.forEach((s) => controllers.torrentList.startTorrentingSummary(s.torrentKey))
}
// Set window dimensions to match video dimensions or fill the screen
@@ -433,18 +349,18 @@ function setDimensions (dimensions) {
state.window.wasMaximized = electron.remote.getCurrentWindow().isMaximized
// Limit window size to screen size
const screenWidth = window.screen.width
const screenHeight = window.screen.height
const aspectRatio = dimensions.width / dimensions.height
const scaleFactor = Math.min(
var screenWidth = window.screen.width
var screenHeight = window.screen.height
var aspectRatio = dimensions.width / dimensions.height
var scaleFactor = Math.min(
Math.min(screenWidth / dimensions.width, 1),
Math.min(screenHeight / dimensions.height, 1)
)
const width = Math.max(
var width = Math.max(
Math.floor(dimensions.width * scaleFactor),
config.WINDOW_MIN_WIDTH
)
const height = Math.max(
var height = Math.max(
Math.floor(dimensions.height * scaleFactor),
config.WINDOW_MIN_HEIGHT
)
@@ -459,22 +375,22 @@ function setDimensions (dimensions) {
function onOpen (files) {
if (!Array.isArray(files)) files = [ files ]
const url = state.location.url()
const allTorrents = files.every(TorrentPlayer.isTorrent)
const allSubtitles = files.every(controllers.subtitles().isSubtitle)
var url = state.location.url()
var allTorrents = files.every(TorrentPlayer.isTorrent)
var allSubtitles = files.every(controllers.subtitles.isSubtitle)
if (allTorrents) {
// Drop torrents onto the app: go to home screen, add torrents, no matter what
dispatch('backToList')
// All .torrent files? Add them.
files.forEach((file) => controllers.torrentList().addTorrent(file))
files.forEach((file) => controllers.torrentList.addTorrent(file))
} else if (url === 'player' && allSubtitles) {
// Drop subtitles onto a playing video: add subtitles
controllers.subtitles().addSubtitles(files, true)
controllers.subtitles.addSubtitles(files, true)
} else if (url === 'home') {
// Drop files onto home screen: show Create Torrent
state.modal = null
controllers.torrentList().showCreateTorrent(files)
controllers.torrentList.showCreateTorrent(files)
} else {
// Drop files onto any other screen: show error
return onError('Please go back to the torrent list before creating a new torrent.')
@@ -494,11 +410,15 @@ function onError (err) {
update()
}
const editableHtmlTags = new Set(['input', 'textarea'])
function onPaste (e) {
if (editableHtmlTags.has(e.target.tagName.toLowerCase())) return
controllers.torrentList().addTorrent(electron.clipboard.readText())
if (e.target.tagName.toLowerCase() === 'input') return
var torrentIds = electron.clipboard.readText().split('\n')
torrentIds.forEach(function (torrentId) {
torrentId = torrentId.trim()
if (torrentId.length === 0) return
controllers.torrentList.addTorrent(torrentId)
})
update()
}
@@ -528,13 +448,6 @@ function onFullscreenChanged (e, isFullScreen) {
update()
}
function onWindowBoundsChanged (e, newBounds) {
if (state.location.url() !== 'player') {
state.saved.bounds = newBounds
dispatch('stateSave')
}
}
function checkDownloadPath () {
fs.stat(state.saved.prefs.downloadPath, function (err, stat) {
if (err) {

111
src/renderer/pages/App.js Normal file
View File

@@ -0,0 +1,111 @@
const colors = require('material-ui/styles/colors')
const React = require('react')
const darkBaseTheme = require('material-ui/styles/baseThemes/darkBaseTheme').default
const getMuiTheme = require('material-ui/styles/getMuiTheme').default
const MuiThemeProvider = require('material-ui/styles/MuiThemeProvider').default
const Header = require('../components/header')
const Views = {
'home': require('./TorrentListPage'),
'player': require('./PlayerPage'),
'create-torrent': require('./CreateTorrentPage'),
'preferences': require('./PreferencesPage')
}
const Modals = {
'open-torrent-address-modal': require('../components/open-torrent-address-modal'),
'remove-torrent-modal': require('../components/remove-torrent-modal'),
'update-available-modal': require('../components/update-available-modal'),
'unsupported-media-modal': require('../components/unsupported-media-modal')
}
darkBaseTheme.fontFamily = process.platform === 'win32'
? '"Segoe UI", sans-serif'
: 'BlinkMacSystemFont, "Helvetica Neue", Helvetica, sans-serif'
darkBaseTheme.palette.primary1Color = colors.cyan500
darkBaseTheme.palette.primary2Color = colors.cyan500
darkBaseTheme.palette.primary3Color = colors.grey600
darkBaseTheme.palette.accent1Color = colors.redA200
darkBaseTheme.palette.accent2Color = colors.redA400
darkBaseTheme.palette.accent3Color = colors.redA100
class App extends React.Component {
render () {
var state = this.props.state
// Hide player controls while playing video, if the mouse stays still for a while
// Never hide the controls when:
// * The mouse is over the controls or we're scrubbing (see CSS)
// * The video is paused
// * The video is playing remotely on Chromecast or Airplay
var hideControls = state.location.url() === 'player' &&
state.playing.mouseStationarySince !== 0 &&
new Date().getTime() - state.playing.mouseStationarySince > 2000 &&
!state.playing.isPaused &&
state.playing.location === 'local' &&
state.playing.playbackRate === 1
var cls = [
'view-' + state.location.url(), /* e.g. view-home, view-player */
'is-' + process.platform /* e.g. is-darwin, is-win32, is-linux */
]
if (state.window.isFullScreen) cls.push('is-fullscreen')
if (state.window.isFocused) cls.push('is-focused')
if (hideControls) cls.push('hide-video-controls')
var vdom = (
<MuiThemeProvider muiTheme={getMuiTheme(darkBaseTheme)}>
<div className={'app ' + cls.join(' ')}>
<Header state={state} />
{this.getErrorPopover()}
<div key='content' className='content'>{this.getView()}</div>
{this.getModal()}
</div>
</MuiThemeProvider>
)
return vdom
}
getErrorPopover () {
var state = this.props.state
var now = new Date().getTime()
var recentErrors = state.errors.filter((x) => now - x.time < 5000)
var hasErrors = recentErrors.length > 0
var errorElems = recentErrors.map(function (error, i) {
return (<div key={i} className='error'>{error.message}</div>)
})
return (
<div key='errors'
className={'error-popover ' + (hasErrors ? 'visible' : 'hidden')}>
<div key='title' className='title'>Error</div>
{errorElems}
</div>
)
}
getModal () {
var state = this.props.state
if (!state.modal) return
var ModalContents = Modals[state.modal.id]
return (
<div key='modal' className='modal'>
<div key='modal-background' className='modal-background' />
<div key='modal-content' className='modal-content'>
<ModalContents state={state} />
</div>
</div>
)
}
getView () {
var state = this.props.state
var View = Views[state.location.url()]
return (<View state={state} />)
}
}
module.exports = App

View File

@@ -0,0 +1,171 @@
const createTorrent = require('create-torrent')
const path = require('path')
const prettyBytes = require('prettier-bytes')
const React = require('react')
const {dispatch, dispatcher} = require('../lib/dispatcher')
const FlatButton = require('material-ui/FlatButton').default
const RaisedButton = require('material-ui/RaisedButton').default
const TextField = require('material-ui/TextField').default
const CreateTorrentErrorPage = require('../components/create-torrent-error-page')
const Heading = require('../components/Heading')
const ShowMore = require('../components/ShowMore')
class CreateTorrentPage extends React.Component {
constructor (props) {
super(props)
var state = this.props.state
var info = state.location.current()
// Preprocess: exclude .DS_Store and other dotfiles
var files = info.files
.filter((f) => !f.name.startsWith('.'))
.map((f) => ({name: f.name, path: f.path, size: f.size}))
if (files.length === 0) return (<CreateTorrentErrorPage state={state} />)
// First, extract the base folder that the files are all in
var pathPrefix = info.folderPath
if (!pathPrefix) {
pathPrefix = files.map((x) => x.path).reduce(findCommonPrefix)
if (!pathPrefix.endsWith('/') && !pathPrefix.endsWith('\\')) {
pathPrefix = path.dirname(pathPrefix)
}
}
// Sanity check: show the number of files and total size
var numFiles = files.length
var totalBytes = files
.map((f) => f.size)
.reduce((a, b) => a + b, 0)
var torrentInfo = `${numFiles} files, ${prettyBytes(totalBytes)}`
// Then, use the name of the base folder (or sole file, for a single file torrent)
// as the default name. Show all files relative to the base folder.
var defaultName, basePath
if (files.length === 1) {
// Single file torrent: /a/b/foo.jpg -> torrent name 'foo.jpg', path '/a/b'
defaultName = files[0].name
basePath = pathPrefix
} else {
// Multi file torrent: /a/b/{foo, bar}.jpg -> torrent name 'b', path '/a'
defaultName = path.basename(pathPrefix)
basePath = path.dirname(pathPrefix)
}
var maxFileElems = 100
var fileElems = files.slice(0, maxFileElems).map(function (file, i) {
var relativePath = files.length === 0 ? file.name : path.relative(pathPrefix, file.path)
return (<div key={i}>{relativePath}</div>)
})
if (files.length > maxFileElems) {
fileElems.push(<div key='more'>+ {maxFileElems - files.length} more</div>)
}
var trackers = createTorrent.announceList.join('\n')
this.state = {
basePath,
defaultName,
fileElems,
torrentInfo,
trackers
}
}
handleSubmit () {
var announceList = document.querySelector('.torrent-trackers').value
.split('\n')
.map((s) => s.trim())
.filter((s) => s !== '')
var isPrivate = document.querySelector('.torrent-is-private').checked
var comment = document.querySelector('.torrent-comment').value.trim()
var options = {
// We can't let the user choose their own name if we want WebTorrent
// to use the files in place rather than creating a new folder.
// If we ever want to add support for that:
// name: document.querySelector('.torrent-name').value
name: this.state.defaultName,
path: this.state.basePath,
files: this.state.files,
announce: announceList,
private: isPrivate,
comment: comment
}
dispatch('createTorrent', options)
}
render () {
return (
<div className='create-torrent'>
<Heading level={1}>
Create torrent "{this.state.defaultName}"
</Heading>
<div className='torrent-info'>
{this.state.torrentInfo}
</div>
<div className='torrent-attribute'>
<label>Path:</label>
<div className='torrent-attribute'>{this.state.pathPrefix}</div>
</div>
<ShowMore
style={{
marginBottom: 10
}}
hideLabel='Hide advanced settings...'
showLabel='Show advanced settings...'
>
<div key='advanced' className='create-torrent-advanced'>
<div key='private' className='torrent-attribute'>
<label>Private:</label>
<input type='checkbox' className='torrent-is-private' value='torrent-is-private' />
</div>
<Heading level={2}>Trackers:</Heading>
<TextField
className='torrent-trackers'
hintText='Tracker'
multiLine
rows={2}
rowsMax={10}
defaultValue={this.state.trackers}
/>
<div key='comment' className='torrent-attribute'>
<label>Comment:</label>
<textarea className='torrent-attribute torrent-comment' />
</div>
<div key='files' className='torrent-attribute'>
<label>Files:</label>
<div>{this.state.fileElems}</div>
</div>
</div>
</ShowMore>
<div className='float-right'>
<FlatButton
label='Cancel'
style={{
marginRight: 10
}}
onClick={dispatcher('cancel')}
/>
<RaisedButton
label='Create Torrent'
primary
onClick={this.handleSubmit}
/>
</div>
</div>
)
}
}
// Finds the longest common prefix
function findCommonPrefix (a, b) {
for (var i = 0; i < a.length && i < b.length; i++) {
if (a.charCodeAt(i) !== b.charCodeAt(i)) break
}
if (i === a.length) return a
if (i === b.length) return b
return a.substring(0, i)
}
module.exports = CreateTorrentPage

View File

@@ -2,20 +2,19 @@ const React = require('react')
const Bitfield = require('bitfield')
const prettyBytes = require('prettier-bytes')
const zeroFill = require('zero-fill')
const path = require('path')
const TorrentSummary = require('../lib/torrent-summary')
const Playlist = require('../lib/playlist')
const {dispatch, dispatcher} = require('../lib/dispatcher')
const config = require('../../config')
// Shows a streaming video player. Standard features + Chromecast + Airplay
module.exports = class Player extends React.Component {
render () {
// Show the video as large as will fit in the window, play immediately
// If the video is on Chromecast or Airplay, show a title screen instead
const state = this.props.state
const showVideo = state.playing.location === 'local'
const showControls = state.playing.location !== 'external'
var state = this.props.state
var showVideo = state.playing.location === 'local'
var showControls = state.playing.location !== 'external'
return (
<div
className='player'
@@ -26,15 +25,6 @@ module.exports = class Player extends React.Component {
</div>
)
}
onComponentWillUnmount () {
// Unload the media element so that Chromium stops trying to fetch data
const tag = document.querySelector('audio,video')
if (!tag) return
tag.pause()
tag.src = ''
tag.load()
}
}
// Handles volume change by wheel
@@ -48,7 +38,7 @@ function renderMedia (state) {
// Unfortunately, play/pause can't be done just by modifying HTML.
// Instead, grab the DOM node and play/pause it if necessary
// Get the <video> or <audio> tag
const mediaElement = document.querySelector(state.playing.type)
var mediaElement = document.querySelector(state.playing.type)
if (mediaElement !== null) {
if (state.playing.isPaused && !mediaElement.paused) {
mediaElement.pause()
@@ -76,20 +66,20 @@ function renderMedia (state) {
}
// Switch to the newly added subtitle track, if available
const tracks = mediaElement.textTracks || []
for (let j = 0; j < tracks.length; j++) {
const isSelectedTrack = j === state.playing.subtitles.selectedIndex
var tracks = mediaElement.textTracks || []
for (var j = 0; j < tracks.length; j++) {
var isSelectedTrack = j === state.playing.subtitles.selectedIndex
tracks[j].mode = isSelectedTrack ? 'showing' : 'hidden'
}
// Save video position
const file = state.getPlayingFileSummary()
var file = state.getPlayingFileSummary()
file.currentTime = state.playing.currentTime = mediaElement.currentTime
file.duration = state.playing.duration = mediaElement.duration
// Save selected subtitle
if (state.playing.subtitles.selectedIndex !== -1) {
const index = state.playing.subtitles.selectedIndex
var index = state.playing.subtitles.selectedIndex
file.selectedSubtitle = state.playing.subtitles.tracks[index].filePath
} else if (file.selectedSubtitle != null) {
delete file.selectedSubtitle
@@ -99,11 +89,11 @@ function renderMedia (state) {
}
// Add subtitles to the <video> tag
const trackTags = []
var trackTags = []
if (state.playing.subtitles.selectedIndex >= 0) {
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
for (var i = 0; i < state.playing.subtitles.tracks.length; i++) {
var track = state.playing.subtitles.tracks[i]
var isSelected = state.playing.subtitles.selectedIndex === i
trackTags.push(
<track
key={i}
@@ -116,10 +106,10 @@ function renderMedia (state) {
}
// Create the <audio> or <video> tag
const MediaTagName = state.playing.type
const mediaTag = (
var MediaTagName = state.playing.type
var mediaTag = (
<MediaTagName
src={Playlist.getCurrentLocalURL(state)}
src={state.server.localURL}
onDoubleClick={dispatcher('toggleFullScreen')}
onLoadedMetadata={onLoadedMetadata}
onEnded={onEnded}
@@ -146,26 +136,21 @@ function renderMedia (state) {
// As soon as we know the video dimensions, resize the window
function onLoadedMetadata (e) {
if (state.playing.type !== 'video') return
const video = e.target
const dimensions = {
var video = e.target
var dimensions = {
width: video.videoWidth,
height: video.videoHeight
}
dispatch('setDimensions', dimensions)
}
// When the video completes, pause the video instead of looping
function onEnded (e) {
if (Playlist.hasNext(state)) {
dispatch('nextTrack')
} else {
// When the last video completes, pause the video instead of looping
state.playing.isPaused = true
if (state.window.isFullScreen) dispatch('toggleFullScreen')
}
state.playing.isPaused = true
}
function onCanPlay (e) {
const elem = e.target
var elem = e.target
if (state.playing.type === 'video' &&
elem.webkitVideoDecodedByteCount === 0) {
dispatch('mediaError', 'Video codec unsupported')
@@ -179,15 +164,15 @@ function renderMedia (state) {
}
function renderOverlay (state) {
const elems = []
const audioMetadataElem = renderAudioMetadata(state)
const spinnerElem = renderLoadingSpinner(state)
var elems = []
var audioMetadataElem = renderAudioMetadata(state)
var spinnerElem = renderLoadingSpinner(state)
if (audioMetadataElem) elems.push(audioMetadataElem)
if (spinnerElem) elems.push(spinnerElem)
// Video fills the window, centered with black bars if necessary
// Audio gets a static poster image and a summary of the file metadata.
let style
var style
if (state.playing.type === 'audio') {
style = { backgroundImage: cssBackgroundImagePoster(state) }
} else if (elems.length !== 0) {
@@ -205,27 +190,27 @@ function renderOverlay (state) {
}
function renderAudioMetadata (state) {
const fileSummary = state.getPlayingFileSummary()
var fileSummary = state.getPlayingFileSummary()
if (!fileSummary.audioInfo) return
const info = fileSummary.audioInfo
var info = fileSummary.audioInfo
// Get audio track info
let title = info.title
var title = info.title
if (!title) {
title = fileSummary.name
}
let artist = info.artist && info.artist[0]
let album = info.album
var artist = info.artist && info.artist[0]
var album = info.album
if (album && info.year && !album.includes(info.year)) {
album += ' (' + info.year + ')'
}
let track
var track
if (info.track && info.track.no && info.track.of) {
track = info.track.no + ' of ' + info.track.of
}
// Show a small info box in the middle of the screen with title/album/etc
const elems = []
var elems = []
if (artist) {
elems.push((
<div key='artist' className='audio-artist'>
@@ -249,7 +234,7 @@ function renderAudioMetadata (state) {
}
// Align the title with the other info, if available. Otherwise, center title
const emptyLabel = (<label />)
var emptyLabel = (<label />)
elems.unshift((
<div key='title' className='audio-title'>
{elems.length ? emptyLabel : undefined}{title}
@@ -261,14 +246,14 @@ function renderAudioMetadata (state) {
function renderLoadingSpinner (state) {
if (state.playing.isPaused) return
const isProbablyStalled = state.playing.isStalled ||
var isProbablyStalled = state.playing.isStalled ||
(new Date().getTime() - state.playing.lastTimeUpdate > 2000)
if (!isProbablyStalled) return
const prog = state.getPlayingTorrentSummary().progress || {}
let fileProgress = 0
var prog = state.getPlayingTorrentSummary().progress || {}
var fileProgress = 0
if (prog.files) {
const file = prog.files[state.playing.fileIndex]
var file = prog.files[state.playing.fileIndex]
fileProgress = Math.floor(100 * file.numPiecesPresent / file.numPieces)
}
@@ -276,16 +261,16 @@ function renderLoadingSpinner (state) {
<div key='loading' className='media-stalled'>
<div key='loading-spinner' className='loading-spinner'>&nbsp;</div>
<div key='loading-progress' className='loading-status ellipsis'>
<span className='progress'>{fileProgress}%</span> downloaded
<span> {prettyBytes(prog.downloadSpeed || 0)}/s</span>
<span> {prettyBytes(prog.uploadSpeed || 0)}/s</span>
<span className='progress'>{fileProgress}%</span> downloaded,
<span> {prettyBytes(prog.downloadSpeed || 0)}/s</span>
<span> {prettyBytes(prog.uploadSpeed || 0)}/s</span>
</div>
</div>
)
}
function renderCastScreen (state) {
let castIcon, castType, isCast
var castIcon, castType, isCast
if (state.playing.location.startsWith('chromecast')) {
castIcon = 'cast_connected'
castType = 'Chromecast'
@@ -299,24 +284,27 @@ function renderCastScreen (state) {
castType = 'DLNA'
isCast = true
} else if (state.playing.location === 'external') {
// TODO: get the player name in a more reliable way
var playerPath = state.saved.prefs.externalPlayerPath
var playerName = playerPath ? path.basename(playerPath).split('.')[0] : 'VLC'
castIcon = 'tv'
castType = state.getExternalPlayerName()
castType = playerName
isCast = false
} else if (state.playing.location === 'error') {
castIcon = 'error_outline'
castType = 'Unable to Play'
castType = 'Error'
isCast = false
}
const isStarting = state.playing.location.endsWith('-pending')
const castName = state.playing.castName
let castStatus
var isStarting = state.playing.location.endsWith('-pending')
var castName = state.playing.castName
var castStatus
if (isCast && isStarting) castStatus = 'Connecting to ' + castName + '...'
else if (isCast && !isStarting) castStatus = 'Connected to ' + castName
else castStatus = ''
// Show a nice title image, if possible
const style = {
var style = {
backgroundImage: cssBackgroundImagePoster(state)
}
@@ -334,12 +322,12 @@ function renderCastScreen (state) {
function renderCastOptions (state) {
if (!state.devices.castMenu) return
const {location, devices} = state.devices.castMenu
const player = state.devices[location]
var {location, devices} = state.devices.castMenu
var player = state.devices[location]
const items = devices.map(function (device, ix) {
const isSelected = player.device === device
const name = device.name
var items = devices.map(function (device, ix) {
var isSelected = player.device === device
var name = device.name
return (
<li key={ix} onClick={dispatcher('selectCastDevice', ix)}>
<i className='icon'>{isSelected ? 'radio_button_checked' : 'radio_button_unchecked'}</i>
@@ -356,11 +344,11 @@ function renderCastOptions (state) {
}
function renderSubtitleOptions (state) {
const subtitles = state.playing.subtitles
var subtitles = state.playing.subtitles
if (!subtitles.tracks.length || !subtitles.showMenu) return
const items = subtitles.tracks.map(function (track, ix) {
const isSelected = state.playing.subtitles.selectedIndex === ix
var items = subtitles.tracks.map(function (track, ix) {
var isSelected = state.playing.subtitles.selectedIndex === ix
return (
<li key={ix} onClick={dispatcher('selectSubtitle', ix)}>
<i className='icon'>{'radio_button_' + (isSelected ? 'checked' : 'unchecked')}</i>
@@ -369,8 +357,8 @@ function renderSubtitleOptions (state) {
)
})
const noneSelected = state.playing.subtitles.selectedIndex === -1
const noneClass = 'radio_button_' + (noneSelected ? 'checked' : 'unchecked')
var noneSelected = state.playing.subtitles.selectedIndex === -1
var noneClass = 'radio_button_' + (noneSelected ? 'checked' : 'unchecked')
return (
<ul key='subtitle-options' className='options-list'>
{items}
@@ -383,39 +371,32 @@ function renderSubtitleOptions (state) {
}
function renderPlayerControls (state) {
const positionPercent = 100 * state.playing.currentTime / state.playing.duration
const playbackCursorStyle = { left: 'calc(' + positionPercent + '% - 3px)' }
const captionsClass = state.playing.subtitles.tracks.length === 0
var positionPercent = 100 * state.playing.currentTime / state.playing.duration
var playbackCursorStyle = { left: 'calc(' + positionPercent + '% - 3px)' }
var captionsClass = state.playing.subtitles.tracks.length === 0
? 'disabled'
: state.playing.subtitles.selectedIndex >= 0
? 'active'
: ''
const prevClass = Playlist.hasPrevious(state) ? '' : 'disabled'
const nextClass = Playlist.hasNext(state) ? '' : 'disabled'
const elements = [
var elements = [
<div key='playback-bar' className='playback-bar'>
{renderLoadingBar(state)}
<div
key='cursor'
className='playback-cursor'
style={playbackCursorStyle} />
style={playbackCursorStyle}
/>
<div
key='scrub-bar'
className='scrub-bar'
draggable='true'
onDragStart={handleDragStart}
onClick={handleScrub}
onDrag={handleScrub} />
onDrag={handleScrub}
/>
</div>,
<i
key='skip-previous'
className={'icon skip-previous float-left ' + prevClass}
onClick={dispatcher('previousTrack')}>
skip_previous
</i>,
<i
key='play'
className='icon play-pause float-left'
@@ -423,13 +404,6 @@ function renderPlayerControls (state) {
{state.playing.isPaused ? 'play_arrow' : 'pause'}
</i>,
<i
key='skip-next'
className={'icon skip-next float-left ' + nextClass}
onClick={dispatcher('nextTrack')}>
skip_next
</i>,
<i
key='fullscreen'
className='icon fullscreen float-right'
@@ -439,7 +413,7 @@ function renderPlayerControls (state) {
]
if (state.playing.type === 'video') {
// Show closed captions icon
// show closed captions icon
elements.push((
<i
key='subtitles'
@@ -451,24 +425,24 @@ function renderPlayerControls (state) {
}
// If we've detected a Chromecast or AppleTV, the user can play video there
const castTypes = ['chromecast', 'airplay', 'dlna']
const isCastingAnywhere = castTypes.some(
var castTypes = ['chromecast', 'airplay', 'dlna']
var isCastingAnywhere = castTypes.some(
(castType) => state.playing.location.startsWith(castType))
// Add the cast buttons. Icons for each cast type, connected/disconnected:
const buttonIcons = {
var buttonIcons = {
'chromecast': {true: 'cast_connected', false: 'cast'},
'airplay': {true: 'airplay', false: 'airplay'},
'dlna': {true: 'tv', false: 'tv'}
}
castTypes.forEach(function (castType) {
// Do we show this button (eg. the Chromecast button) at all?
const isCasting = state.playing.location.startsWith(castType)
const player = state.devices[castType]
var isCasting = state.playing.location.startsWith(castType)
var player = state.devices[castType]
if ((!player || player.getDevices().length === 0) && !isCasting) return
// Show the button. Three options for eg the Chromecast button:
let buttonClass, buttonHandler
var buttonClass, buttonHandler
if (isCasting) {
// Option 1: we are currently connected to Chromecast. Button stops the cast.
buttonClass = 'active'
@@ -482,7 +456,7 @@ function renderPlayerControls (state) {
buttonClass = ''
buttonHandler = dispatcher('toggleCastMenu', castType)
}
const buttonIcon = buttonIcons[castType][isCasting]
var buttonIcon = buttonIcons[castType][isCasting]
elements.push((
<i
@@ -495,18 +469,20 @@ function renderPlayerControls (state) {
})
// Render volume slider
const volume = state.playing.volume
const volumeIcon = 'volume_' + (
var volume = state.playing.volume
var volumeIcon = 'volume_' + (
volume === 0 ? 'off'
: volume < 0.3 ? 'mute'
: volume < 0.6 ? 'down'
: 'up')
const volumeStyle = {
var volumeStyle = {
background: '-webkit-gradient(linear, left top, right top, ' +
'color-stop(' + (volume * 100) + '%, #eee), ' +
'color-stop(' + (volume * 100) + '%, #727272))'
}
// TODO: dcposch change the range input to use value / onChanged instead of
// "readonly" / onMouse[Down,Move,Up]
elements.push((
<div key='volume' className='volume float-left'>
<i
@@ -519,20 +495,21 @@ function renderPlayerControls (state) {
type='range' min='0' max='1' step='0.05'
value={volume}
onChange={handleVolumeScrub}
style={volumeStyle} />
style={volumeStyle}
/>
</div>
))
// Show video playback progress
const currentTimeStr = formatTime(state.playing.currentTime, state.playing.duration)
const durationStr = formatTime(state.playing.duration, state.playing.duration)
var currentTimeStr = formatTime(state.playing.currentTime)
var durationStr = formatTime(state.playing.duration)
elements.push((
<span key='time' className='time float-left'>
{currentTimeStr} / {durationStr}
</span>
))
// Render playback rate
// render playback rate
if (state.playing.playbackRate !== 1) {
elements.push((
<span key='rate' className='rate float-left'>
@@ -542,9 +519,7 @@ function renderPlayerControls (state) {
}
return (
<div key='controls' className='controls'
onMouseEnter={dispatcher('mediaControlsMouseEnter')}
onMouseLeave={dispatcher('mediaControlsMouseLeave')}>
<div key='controls' className='controls'>
{elements}
{renderCastOptions(state)}
{renderSubtitleOptions(state)}
@@ -554,7 +529,7 @@ function renderPlayerControls (state) {
function handleDragStart (e) {
// Prevent the cursor from changing, eg to a green + icon on Mac
if (e.dataTransfer) {
const dt = e.dataTransfer
var dt = e.dataTransfer
dt.effectAllowed = 'none'
}
}
@@ -563,9 +538,9 @@ function renderPlayerControls (state) {
function handleScrub (e) {
if (!e.clientX) return
dispatch('mediaMouseMoved')
const windowWidth = document.querySelector('body').clientWidth
const fraction = e.clientX / windowWidth
const position = fraction * state.playing.duration /* seconds */
var windowWidth = document.querySelector('body').clientWidth
var fraction = e.clientX / windowWidth
var position = fraction * state.playing.duration /* seconds */
dispatch('skipTo', position)
}
@@ -596,23 +571,18 @@ function renderPlayerControls (state) {
// Renders the loading bar. Shows which parts of the torrent are loaded, which
// can be 'spongey' / non-contiguous
function renderLoadingBar (state) {
if (config.IS_TEST) return // Don't integration test the loading bar. Screenshots won't match.
const torrentSummary = state.getPlayingTorrentSummary()
var torrentSummary = state.getPlayingTorrentSummary()
if (!torrentSummary.progress) {
return null
return []
}
// Find all contiguous parts of the torrent which are loaded
const prog = torrentSummary.progress
const fileProg = prog.files[state.playing.fileIndex]
if (!fileProg) return null
const parts = []
let lastPiecePresent = false
for (let i = fileProg.startPiece; i <= fileProg.endPiece; i++) {
const partPresent = Bitfield.prototype.get.call(prog.bitfield, i)
var prog = torrentSummary.progress
var fileProg = prog.files[state.playing.fileIndex]
var parts = []
var lastPiecePresent = false
for (var i = fileProg.startPiece; i <= fileProg.endPiece; i++) {
var partPresent = Bitfield.prototype.get.call(prog.bitfield, i)
if (partPresent && !lastPiecePresent) {
parts.push({start: i - fileProg.startPiece, count: 1})
} else if (partPresent) {
@@ -622,22 +592,21 @@ function renderLoadingBar (state) {
}
// Output some bars to show which parts of the file are loaded
const loadingBarElems = parts.map(function (part, i) {
const style = {
var loadingBarElems = parts.map(function (part, i) {
var style = {
left: (100 * part.start / fileProg.numPieces) + '%',
width: (100 * part.count / fileProg.numPieces) + '%'
}
return (<div key={i} className='loading-bar-part' style={style} />)
})
return (<div key='loading-bar' className='loading-bar'>{loadingBarElems}</div>)
}
// Returns the CSS background-image string for a poster image + dark vignette
function cssBackgroundImagePoster (state) {
const torrentSummary = state.getPlayingTorrentSummary()
const posterPath = TorrentSummary.getPosterPath(torrentSummary)
var torrentSummary = state.getPlayingTorrentSummary()
var posterPath = TorrentSummary.getPosterPath(torrentSummary)
if (!posterPath) return ''
return cssBackgroundImageDarkGradient() + `, url('${posterPath}')`
}
@@ -647,19 +616,17 @@ function cssBackgroundImageDarkGradient () {
'rgba(0,0,0,0.4) 0%, rgba(0,0,0,1) 100%)'
}
function formatTime (time, total) {
function formatTime (time) {
if (typeof time !== 'number' || Number.isNaN(time)) {
return '0:00'
}
let totalHours = Math.floor(total / 3600)
let totalMinutes = Math.floor(total % 3600 / 60)
let hours = Math.floor(time / 3600)
let minutes = Math.floor(time % 3600 / 60)
if (totalMinutes > 9) {
var hours = Math.floor(time / 3600)
var minutes = Math.floor(time % 3600 / 60)
if (hours > 0) {
minutes = zeroFill(2, minutes)
}
let seconds = zeroFill(2, Math.floor(time % 60))
var seconds = zeroFill(2, Math.floor(time % 60))
return (totalHours > 0 ? hours + ':' : '') + minutes + ':' + seconds
return (hours > 0 ? hours + ':' : '') + minutes + ':' + seconds
}

View File

@@ -0,0 +1,177 @@
const colors = require('material-ui/styles/colors')
const path = require('path')
const React = require('react')
const Checkbox = require('material-ui/Checkbox').default
const Heading = require('../components/Heading')
const PathSelector = require('../components/PathSelector')
const RaisedButton = require('material-ui/RaisedButton').default
const {dispatch} = require('../lib/dispatcher')
class PreferencesPage extends React.Component {
constructor (props) {
super(props)
this.handleDownloadPathChange =
this.handleDownloadPathChange.bind(this)
this.handleOpenExternalPlayerChange =
this.handleOpenExternalPlayerChange.bind(this)
this.handleExternalPlayerPathChange =
this.handleExternalPlayerPathChange.bind(this)
}
downloadPathSelector () {
return (
<Preference>
<PathSelector
dialog={{
title: 'Select download directory',
properties: [ 'openDirectory' ]
}}
onChange={this.handleDownloadPathChange}
title='Download location'
value={this.props.state.unsaved.prefs.downloadPath}
/>
</Preference>
)
}
handleDownloadPathChange (filePath) {
dispatch('updatePreferences', 'downloadPath', filePath)
}
openExternalPlayerCheckbox () {
return (
<Preference>
<Checkbox
className='control'
checked={!this.props.state.unsaved.prefs.openExternalPlayer}
label={'Play torrent media files using WebTorrent'}
onCheck={this.handleOpenExternalPlayerChange}
/>
</Preference>
)
}
handleOpenExternalPlayerChange (e, isChecked) {
dispatch('updatePreferences', 'openExternalPlayer', !isChecked)
}
externalPlayerPathSelector () {
const playerName = path.basename(
this.props.state.unsaved.prefs.externalPlayerPath || 'VLC'
)
const description = this.props.state.unsaved.prefs.openExternalPlayer
? `Torrent media files will always play in ${playerName}.`
: `Torrent media files will play in ${playerName} if WebTorrent cannot ` +
'play them.'
return (
<Preference>
<p>{description}</p>
<PathSelector
dialog={{
title: 'Select media player app',
properties: [ 'openFile' ]
}}
displayValue={playerName}
onChange={this.handleExternalPlayerPathChange}
title='External player'
value={this.props.state.unsaved.prefs.externalPlayerPath}
/>
</Preference>
)
}
handleExternalPlayerPathChange (filePath) {
if (path.extname(filePath) === '.app') {
// Mac: Use executable in packaged .app bundle
filePath += '/Contents/MacOS/' + path.basename(filePath, '.app')
}
dispatch('updatePreferences', 'externalPlayerPath', filePath)
}
setDefaultAppButton () {
return (
<Preference>
<p>WebTorrent is not currently the default torrent app.</p>
<RaisedButton
className='control'
onClick={this.handleSetDefaultApp}
label='Make WebTorrent the default'
/>
</Preference>
)
}
handleSetDefaultApp () {
window.alert('TODO')
// var isFileHandler = state.unsaved.prefs.isFileHandler
// dispatch('updatePreferences', 'isFileHandler', !isFileHandler)
}
render () {
return (
<div
style={{
color: colors.grey400,
marginLeft: 25,
marginRight: 25
}}
>
<PreferencesSection title='Downloads'>
{this.downloadPathSelector()}
</PreferencesSection>
<PreferencesSection title='Playback'>
{this.openExternalPlayerCheckbox()}
{this.externalPlayerPathSelector()}
</PreferencesSection>
<PreferencesSection title='Default torrent app'>
{this.setDefaultAppButton()}
</PreferencesSection>
</div>
)
}
}
class PreferencesSection extends React.Component {
static get propTypes () {
return {
title: React.PropTypes.string
}
}
render () {
return (
<div
style={{
marginBottom: 25,
marginTop: 25
}}
>
<Heading level={2}>{this.props.title}</Heading>
{this.props.children}
</div>
)
}
}
class Preference extends React.Component {
render () {
return (
<div
style={{
marginBottom: 10
}}
>
{this.props.children}
</div>
)
}
}
module.exports = PreferencesPage

View File

@@ -1,18 +1,15 @@
const React = require('react')
const prettyBytes = require('prettier-bytes')
const Checkbox = require('material-ui/Checkbox').default
const LinearProgress = require('material-ui/LinearProgress').default
const TorrentSummary = require('../lib/torrent-summary')
const TorrentPlayer = require('../lib/torrent-player')
const {dispatcher} = require('../lib/dispatcher')
module.exports = class TorrentList extends React.Component {
render () {
const state = this.props.state
var state = this.props.state
const contents = []
var contents = []
if (state.downloadPathStatus === 'missing') {
contents.push(
<div key='torrent-missing-path'>
@@ -24,7 +21,7 @@ module.exports = class TorrentList extends React.Component {
</div>
)
}
const torrentElems = state.saved.torrents.map(
var torrentElems = state.saved.torrents.map(
(torrentSummary) => this.renderTorrent(torrentSummary)
)
contents.push(...torrentElems)
@@ -42,27 +39,30 @@ module.exports = class TorrentList extends React.Component {
}
renderTorrent (torrentSummary) {
const state = this.props.state
const infoHash = torrentSummary.infoHash
const isSelected = infoHash && state.selectedInfoHash === infoHash
var state = this.props.state
var infoHash = torrentSummary.infoHash
var isSelected = infoHash && state.selectedInfoHash === infoHash
// Background image: show some nice visuals, like a frame from the movie, if possible
const style = {}
var style = {}
if (torrentSummary.posterFileName) {
const gradient = 'linear-gradient(to bottom, rgba(0, 0, 0, 0.4) 0%, rgba(0, 0, 0, 0.4) 100%)'
const posterPath = TorrentSummary.getPosterPath(torrentSummary)
style.backgroundImage = `${gradient}, url('${posterPath}')`
var gradient = isSelected
? 'linear-gradient(to bottom, rgba(0, 0, 0, 0.8) 0%, rgba(0, 0, 0, 0.4) 100%)'
: 'linear-gradient(to bottom, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0) 100%)'
var posterPath = TorrentSummary.getPosterPath(torrentSummary)
style.backgroundImage = gradient + `, url('${posterPath}')`
}
// Foreground: name of the torrent, basic info like size, play button,
// cast buttons if available, and delete
const classes = ['torrent']
var classes = ['torrent']
// playStatus turns the play button into a loading spinner or error icon
if (torrentSummary.playStatus) classes.push(torrentSummary.playStatus)
if (isSelected) classes.push('selected')
if (!infoHash) classes.push('disabled')
if (!torrentSummary.torrentKey) throw new Error('Missing torrentKey')
return (
<div
id={torrentSummary.testID && ('torrent-' + torrentSummary.testID)}
key={torrentSummary.torrentKey}
style={style}
className={classes.join(' ')}
@@ -71,96 +71,48 @@ module.exports = class TorrentList extends React.Component {
{this.renderTorrentMetadata(torrentSummary)}
{infoHash ? this.renderTorrentButtons(torrentSummary) : null}
{isSelected ? this.renderTorrentDetails(torrentSummary) : null}
<hr />
</div>
)
}
// Show name, download status, % complete
renderTorrentMetadata (torrentSummary) {
const name = torrentSummary.name || 'Loading torrent...'
const elements = [(
var name = torrentSummary.name || 'Loading torrent...'
var elements = [(
<div key='name' className='name ellipsis'>{name}</div>
)]
// If it's downloading/seeding then show progress info
const prog = torrentSummary.progress
let progElems
var prog = torrentSummary.progress
if (torrentSummary.error) {
progElems = [getErrorMessage(torrentSummary)]
} else if (torrentSummary.status !== 'paused' && prog) {
progElems = [
renderDownloadCheckbox(),
renderTorrentStatus(),
renderProgressBar(),
renderPercentProgress(),
renderTotalProgress(),
renderPeers(),
renderSpeeds(),
renderEta()
]
} else {
progElems = [
renderDownloadCheckbox(),
renderTorrentStatus()
]
}
elements.push(
<div key='progress-info' className='ellipsis'>
{progElems}
</div>
)
return (<div key='metadata' className='metadata'>{elements}</div>)
function renderDownloadCheckbox () {
const infoHash = torrentSummary.infoHash
const isActive = ['downloading', 'seeding'].includes(torrentSummary.status)
return (
<Checkbox
key='download-button'
className={'control download ' + torrentSummary.status}
style={{
display: 'inline-block',
width: 32
}}
iconStyle={{
width: 20,
height: 20
}}
checked={isActive}
onClick={stopPropagation}
onCheck={dispatcher('toggleTorrent', infoHash)} />
elements.push(
<div key='progress-info' className='ellipsis'>
{getErrorMessage(torrentSummary)}
</div>
)
}
function renderProgressBar () {
const progress = Math.floor(100 * prog.progress)
const styles = {
wrapper: {
display: 'inline-block',
marginRight: 8
},
progress: {
height: 8,
width: 30
}
}
return (
<div style={styles.wrapper}>
<LinearProgress style={styles.progress} mode='determinate' value={progress} />
} else if (torrentSummary.status !== 'paused' && prog) {
elements.push(
<div key='progress-info' className='ellipsis'>
{renderPercentProgress()}
{renderTotalProgress()}
{renderPeers()}
{renderDownloadSpeed()}
{renderUploadSpeed()}
{renderEta()}
</div>
)
}
return (<div key='metadata' className='metadata'>{elements}</div>)
function renderPercentProgress () {
const progress = Math.floor(100 * prog.progress)
var progress = Math.floor(100 * prog.progress)
return (<span key='percent-progress'>{progress}%</span>)
}
function renderTotalProgress () {
const downloaded = prettyBytes(prog.downloaded)
const total = prettyBytes(prog.length || 0)
var downloaded = prettyBytes(prog.downloaded)
var total = prettyBytes(prog.length || 0)
if (downloaded === total) {
return (<span key='total-progress'>{downloaded}</span>)
} else {
@@ -170,78 +122,112 @@ module.exports = class TorrentList extends React.Component {
function renderPeers () {
if (prog.numPeers === 0) return
const count = prog.numPeers === 1 ? 'peer' : 'peers'
var count = prog.numPeers === 1 ? 'peer' : 'peers'
return (<span key='peers'>{prog.numPeers} {count}</span>)
}
function renderSpeeds () {
let str = ''
if (prog.downloadSpeed > 0) str += ' ↓ ' + prettyBytes(prog.downloadSpeed) + '/s'
if (prog.uploadSpeed > 0) str += ' ↑ ' + prettyBytes(prog.uploadSpeed) + '/s'
if (str === '') return
return (<span key='download'>{str}</span>)
function renderDownloadSpeed () {
if (prog.downloadSpeed === 0) return
return (<span key='download'> {prettyBytes(prog.downloadSpeed)}/s</span>)
}
function renderUploadSpeed () {
if (prog.uploadSpeed === 0) return
return (<span key='upload'> {prettyBytes(prog.uploadSpeed)}/s</span>)
}
function renderEta () {
const downloaded = prog.downloaded
const total = prog.length || 0
const missing = total - downloaded
const downloadSpeed = prog.downloadSpeed
var downloaded = prog.downloaded
var total = prog.length || 0
var missing = total - downloaded
var downloadSpeed = prog.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)
var rawEta = missing / downloadSpeed
var hours = Math.floor(rawEta / 3600) % 24
var minutes = Math.floor(rawEta / 60) % 60
var 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 + 'm' : ''
const secondsStr = seconds + 's'
var hoursStr = hours ? hours + 'h' : ''
var minutesStr = (hours || minutes) ? minutes + 'm' : ''
var secondsStr = seconds + 's'
return (<span>{hoursStr} {minutesStr} {secondsStr} remaining</span>)
}
function renderTorrentStatus () {
let status
if (torrentSummary.status === 'paused') {
if (!torrentSummary.progress) status = ''
else if (torrentSummary.progress.progress === 1) status = 'Not seeding'
else status = 'Paused'
} else if (torrentSummary.status === 'downloading') {
status = 'Downloading'
} else if (torrentSummary.status === 'seeding') {
status = 'Seeding'
} else { // torrentSummary.status is 'new' or something unexpected
status = ''
}
return (<span>{status}</span>)
return (<span>ETA: {hoursStr} {minutesStr} {secondsStr}</span>)
}
}
// Download button toggles between torrenting (DL/seed) and paused
// Play button starts streaming the torrent immediately, unpausing if needed
renderTorrentButtons (torrentSummary) {
const infoHash = torrentSummary.infoHash
var infoHash = torrentSummary.infoHash
var playIcon, playTooltip, playClass
if (torrentSummary.playStatus === 'timeout') {
playIcon = 'warning'
playTooltip = 'Playback timed out. No seeds? No internet? Click to try again.'
} else {
playIcon = 'play_arrow'
playTooltip = 'Start streaming'
}
var downloadIcon, downloadTooltip
if (torrentSummary.status === 'seeding') {
downloadIcon = 'file_upload'
downloadTooltip = 'Seeding. Click to stop.'
} else if (torrentSummary.status === 'downloading') {
downloadIcon = 'file_download'
downloadTooltip = 'Torrenting. Click to stop.'
} else {
downloadIcon = 'file_download'
downloadTooltip = 'Click to start torrenting.'
}
// Only show the play/dowload buttons for torrents that contain playable media
let playButton
if (!torrentSummary.error && TorrentPlayer.isPlayableTorrentSummary(torrentSummary)) {
playButton = (
var playButton, downloadButton, positionElem
if (!torrentSummary.error) {
downloadButton = (
<i
key='play-button'
title='Start streaming'
className={'icon play'}
onClick={dispatcher('playFile', infoHash)}>
play_circle_outline
key='download-button'
className={'button-round icon download ' + torrentSummary.status}
title={downloadTooltip}
onClick={dispatcher('toggleTorrent', infoHash)}
>
{downloadIcon}
</i>
)
// Do we have a saved position? Show it using a radial progress bar on top
// of the play button, unless already showing a spinner there:
var willShowSpinner = torrentSummary.playStatus === 'requested'
var defaultFile = torrentSummary.files &&
torrentSummary.files[torrentSummary.defaultPlayFileIndex]
if (defaultFile && defaultFile.currentTime && !willShowSpinner) {
var fraction = defaultFile.currentTime / defaultFile.duration
positionElem = this.renderRadialProgressBar(fraction, 'radial-progress-large')
playClass = 'resume-position'
}
if (TorrentPlayer.isPlayableTorrentSummary(torrentSummary)) {
playButton = (
<i
key='play-button'
title={playTooltip}
className={'button-round icon play ' + playClass}
onClick={dispatcher('playFile', infoHash)}
>
{playIcon}
</i>
)
}
}
return (
<div className='torrent-controls'>
<div key='buttons' className='buttons'>
{positionElem}
{playButton}
{downloadButton}
<i
key='delete-button'
className='icon delete'
@@ -255,9 +241,9 @@ module.exports = class TorrentList extends React.Component {
// Show files, per-file download status and play buttons, and so on
renderTorrentDetails (torrentSummary) {
let filesElement
var filesElement
if (torrentSummary.error || !torrentSummary.files) {
let message = ''
var message = ''
if (torrentSummary.error === 'path-missing') {
// Special case error: this torrent's download dir or file is missing
message = 'Missing path: ' + TorrentSummary.getFileOrFolder(torrentSummary)
@@ -278,9 +264,14 @@ module.exports = class TorrentList extends React.Component {
)
} else {
// We do know the files. List them and show download stats for each one
const fileRows = torrentSummary.files
var fileRows = torrentSummary.files
.filter((file) => !file.path.includes('/.____padding_file/'))
.map((file, index) => ({ file, index }))
.sort(function (a, b) {
if (a.file.name < b.file.name) return -1
if (b.file.name < a.file.name) return 1
return 0
})
.map((object) => this.renderFileRow(torrentSummary, object.file, object.index))
filesElement = (
@@ -305,28 +296,28 @@ module.exports = class TorrentList extends React.Component {
renderFileRow (torrentSummary, file, index) {
// First, find out how much of the file we've downloaded
// Are we even torrenting it?
const isSelected = torrentSummary.selections && torrentSummary.selections[index]
let isDone = false // Are we finished torrenting it?
let progress = ''
var isSelected = torrentSummary.selections && torrentSummary.selections[index]
var isDone = false // Are we finished torrenting it?
var progress = ''
if (torrentSummary.progress && torrentSummary.progress.files &&
torrentSummary.progress.files[index]) {
const fileProg = torrentSummary.progress.files[index]
var fileProg = torrentSummary.progress.files[index]
isDone = fileProg.numPiecesPresent === fileProg.numPieces
progress = Math.round(100 * fileProg.numPiecesPresent / fileProg.numPieces) + '%'
}
// Second, for media files where we saved our position, show how far we got
let positionElem
var positionElem
if (file.currentTime) {
// Radial progress bar. 0% = start from 0:00, 270% = 3/4 of the way thru
positionElem = this.renderRadialProgressBar(file.currentTime / file.duration)
}
// Finally, render the file as a table row
const isPlayable = TorrentPlayer.isPlayable(file)
const infoHash = torrentSummary.infoHash
let icon
let handleClick
var isPlayable = TorrentPlayer.isPlayable(file)
var infoHash = torrentSummary.infoHash
var icon
var handleClick
if (isPlayable) {
icon = 'play_arrow' /* playable? add option to play */
handleClick = dispatcher('playFile', infoHash, index)
@@ -337,7 +328,7 @@ module.exports = class TorrentList extends React.Component {
: (e) => e.stopPropagation() // noop if file is not ready
}
// TODO: add a css 'disabled' class to indicate that a file cannot be opened/streamed
let rowClass = ''
var rowClass = ''
if (!isSelected) rowClass = 'disabled' // File deselected, not being torrented
if (!isDone && !isPlayable) rowClass = 'disabled' // Can't open yet, can't stream
return (
@@ -357,16 +348,16 @@ module.exports = class TorrentList extends React.Component {
</td>
<td className='col-select'
onClick={dispatcher('toggleTorrentFile', infoHash, index)}>
<i className='icon deselect-file'>{isSelected ? 'close' : 'add'}</i>
<i className='icon'>{isSelected ? 'close' : 'add'}</i>
</td>
</tr>
)
}
renderRadialProgressBar (fraction, cssClass) {
const rotation = 360 * fraction
const transformFill = {transform: 'rotate(' + (rotation / 2) + 'deg)'}
const transformFix = {transform: 'rotate(' + rotation + 'deg)'}
var rotation = 360 * fraction
var transformFill = {transform: 'rotate(' + (rotation / 2) + 'deg)'}
var transformFix = {transform: 'rotate(' + rotation + 'deg)'}
return (
<div key='radial-progress' className={'radial-progress ' + cssClass}>
@@ -385,12 +376,8 @@ module.exports = class TorrentList extends React.Component {
}
}
function stopPropagation (e) {
e.stopPropagation()
}
function getErrorMessage (torrentSummary) {
const err = torrentSummary.error
var err = torrentSummary.error
if (err === 'path-missing') {
return (
<span>

View File

@@ -1,130 +0,0 @@
const colors = require('material-ui/styles/colors')
const createGetter = require('fn-getter')
const React = require('react')
const darkBaseTheme = require('material-ui/styles/baseThemes/darkBaseTheme').default
const getMuiTheme = require('material-ui/styles/getMuiTheme').default
const MuiThemeProvider = require('material-ui/styles/MuiThemeProvider').default
const Header = require('../components/header')
// Perf optimization: Needed immediately, so do not lazy load it below
const TorrentListPage = require('./torrent-list-page')
const Views = {
'home': createGetter(() => TorrentListPage),
'player': createGetter(() => require('./player-page')),
'create-torrent': createGetter(() => require('./create-torrent-page')),
'preferences': createGetter(() => require('./preferences-page'))
}
const Modals = {
'open-torrent-address-modal': createGetter(
() => require('../components/open-torrent-address-modal')
),
'remove-torrent-modal': createGetter(() => require('../components/remove-torrent-modal')),
'update-available-modal': createGetter(() => require('../components/update-available-modal')),
'unsupported-media-modal': createGetter(() => require('../components/unsupported-media-modal'))
}
const fontFamily = process.platform === 'win32'
? '"Segoe UI", sans-serif'
: 'BlinkMacSystemFont, "Helvetica Neue", Helvetica, sans-serif'
darkBaseTheme.fontFamily = fontFamily
darkBaseTheme.userAgent = false
darkBaseTheme.palette.primary1Color = colors.grey50
darkBaseTheme.palette.primary2Color = colors.grey50
darkBaseTheme.palette.primary3Color = colors.grey600
darkBaseTheme.palette.accent1Color = colors.redA200
darkBaseTheme.palette.accent2Color = colors.redA400
darkBaseTheme.palette.accent3Color = colors.redA100
let darkMuiTheme
let lightMuiTheme
class App extends React.Component {
render () {
const state = this.props.state
// Hide player controls while playing video, if the mouse stays still for a while
// Never hide the controls when:
// * The mouse is over the controls or we're scrubbing (see CSS)
// * The video is paused
// * The video is playing remotely on Chromecast or Airplay
const hideControls = state.shouldHidePlayerControls()
const cls = [
'view-' + state.location.url(), /* e.g. view-home, view-player */
'is-' + process.platform /* e.g. is-darwin, is-win32, is-linux */
]
if (state.window.isFullScreen) cls.push('is-fullscreen')
if (state.window.isFocused) cls.push('is-focused')
if (hideControls) cls.push('hide-video-controls')
if (!darkMuiTheme) {
darkMuiTheme = getMuiTheme(darkBaseTheme)
}
return (
<MuiThemeProvider muiTheme={darkMuiTheme}>
<div className={'app ' + cls.join(' ')}>
<Header state={state} />
{this.getErrorPopover()}
<div key='content' className='content'>{this.getView()}</div>
{this.getModal()}
</div>
</MuiThemeProvider>
)
}
getErrorPopover () {
const state = this.props.state
const now = new Date().getTime()
const recentErrors = state.errors.filter((x) => now - x.time < 5000)
const hasErrors = recentErrors.length > 0
const errorElems = recentErrors.map(function (error, i) {
return (<div key={i} className='error'>{error.message}</div>)
})
return (
<div key='errors'
className={'error-popover ' + (hasErrors ? 'visible' : 'hidden')}>
<div key='title' className='title'>Error</div>
{errorElems}
</div>
)
}
getModal () {
const state = this.props.state
if (!state.modal) return
if (!lightMuiTheme) {
const lightBaseTheme = require('material-ui/styles/baseThemes/lightBaseTheme').default
lightBaseTheme.fontFamily = fontFamily
lightBaseTheme.userAgent = false
lightMuiTheme = getMuiTheme(lightBaseTheme)
}
const ModalContents = Modals[state.modal.id]()
return (
<MuiThemeProvider muiTheme={lightMuiTheme}>
<div key='modal' className='modal'>
<div key='modal-background' className='modal-background' />
<div key='modal-content' className='modal-content'>
<ModalContents state={state} />
</div>
</div>
</MuiThemeProvider>
)
}
getView () {
const state = this.props.state
const View = Views[state.location.url()]()
return (<View state={state} />)
}
}
module.exports = App

View File

@@ -1,216 +0,0 @@
const createTorrent = require('create-torrent')
const path = require('path')
const prettyBytes = require('prettier-bytes')
const React = require('react')
const {dispatch, dispatcher} = require('../lib/dispatcher')
const FlatButton = require('material-ui/FlatButton').default
const RaisedButton = require('material-ui/RaisedButton').default
const TextField = require('material-ui/TextField').default
const Checkbox = require('material-ui/Checkbox').default
const CreateTorrentErrorPage = require('../components/create-torrent-error-page')
const Heading = require('../components/heading')
const ShowMore = require('../components/show-more')
// Shows a basic UI to create a torrent, from an already-selected file or folder.
// Includes a "Show Advanced..." button and more advanced UI.
class CreateTorrentPage extends React.Component {
constructor (props) {
super(props)
const state = this.props.state
const info = state.location.current()
// First, extract the base folder that the files are all in
let pathPrefix = info.folderPath
if (!pathPrefix) {
pathPrefix = info.files.map((x) => x.path).reduce(findCommonPrefix)
if (!pathPrefix.endsWith('/') && !pathPrefix.endsWith('\\')) {
pathPrefix = path.dirname(pathPrefix)
}
}
// Then, exclude .DS_Store and other dotfiles
const files = info.files
.filter((f) => !containsDots(f.path, pathPrefix))
.map((f) => ({name: f.name, path: f.path, size: f.size}))
if (files.length === 0) return (<CreateTorrentErrorPage state={state} />)
// Then, use the name of the base folder (or sole file, for a single file torrent)
// as the default name. Show all files relative to the base folder.
let defaultName, basePath
if (files.length === 1) {
// Single file torrent: /a/b/foo.jpg -> torrent name 'foo.jpg', path '/a/b'
defaultName = files[0].name
basePath = pathPrefix
} else {
// Multi file torrent: /a/b/{foo, bar}.jpg -> torrent name 'b', path '/a'
defaultName = path.basename(pathPrefix)
basePath = path.dirname(pathPrefix)
}
// Default trackers
const trackers = createTorrent.announceList.join('\n')
this.state = {
comment: '',
isPrivate: false,
pathPrefix,
basePath,
defaultName,
files,
trackers
}
// Create React event handlers only once
this.setIsPrivate = (_, isPrivate) => this.setState({isPrivate})
this.setComment = (_, comment) => this.setState({comment})
this.setTrackers = (_, trackers) => this.setState({trackers})
this.handleSubmit = handleSubmit.bind(this)
}
render () {
const files = this.state.files
// Sanity check: show the number of files and total size
const numFiles = files.length
const totalBytes = files
.map((f) => f.size)
.reduce((a, b) => a + b, 0)
const torrentInfo = `${numFiles} files, ${prettyBytes(totalBytes)}`
return (
<div className='create-torrent'>
<Heading level={1}>Create torrent {this.state.defaultName}</Heading>
<div className='torrent-info'>{torrentInfo}</div>
<div className='torrent-attribute'>
<label>Path:</label>
<div>{this.state.pathPrefix}</div>
</div>
<ShowMore
style={{
marginBottom: 10
}}
hideLabel='Hide advanced settings...'
showLabel='Show advanced settings...'>
{this.renderAdvanced()}
</ShowMore>
<div className='float-right'>
<FlatButton
className='control cancel'
label='Cancel'
style={{
marginRight: 10
}}
onClick={dispatcher('cancel')} />
<RaisedButton
className='control create-torrent-button'
label='Create Torrent'
primary
onClick={this.handleSubmit} />
</div>
</div>
)
}
// Renders everything after clicking Show Advanced...:
// * Is Private? (private torrents, not announced to DHT)
// * Announce list (trackers)
// * Comment
renderAdvanced () {
// Create file list
const maxFileElems = 100
const files = this.state.files
const fileElems = files.slice(0, maxFileElems).map((file, i) => {
const relativePath = path.relative(this.state.pathPrefix, file.path)
return (<div key={i}>{relativePath}</div>)
})
if (files.length > maxFileElems) {
fileElems.push(<div key='more'>+ {files.length - maxFileElems} more</div>)
}
// Align the text fields
const textFieldStyle = { width: '' }
const textareaStyle = { margin: 0 }
return (
<div key='advanced' className='create-torrent-advanced'>
<div key='private' className='torrent-attribute'>
<label>Private:</label>
<Checkbox
className='torrent-is-private control'
style={{display: ''}}
checked={this.state.isPrivate}
onCheck={this.setIsPrivate} />
</div>
<div key='trackers' className='torrent-attribute'>
<label>Trackers:</label>
<TextField
className='torrent-trackers control'
style={textFieldStyle}
textareaStyle={textareaStyle}
multiLine
rows={2}
rowsMax={10}
value={this.state.trackers}
onChange={this.setTrackers} />
</div>
<div key='comment' className='torrent-attribute'>
<label>Comment:</label>
<TextField
className='torrent-comment control'
style={textFieldStyle}
textareaStyle={textareaStyle}
hintText='Optionally describe your torrent...'
multiLine
rows={2}
rowsMax={10}
value={this.state.comment}
onChange={this.setComment} />
</div>
<div key='files' className='torrent-attribute'>
<label>Files:</label>
<div>{fileElems}</div>
</div>
</div>
)
}
}
function handleSubmit () {
const announceList = this.state.trackers
.split('\n')
.map((s) => s.trim())
.filter((s) => s !== '')
const options = {
// We can't let the user choose their own name if we want WebTorrent
// to use the files in place rather than creating a new folder.
name: this.state.defaultName,
path: this.state.basePath,
files: this.state.files,
announce: announceList,
private: this.state.isPrivate,
comment: this.state.comment.trim()
}
dispatch('createTorrent', options)
}
// Finds the longest common prefix
function findCommonPrefix (a, b) {
let i
for (i = 0; i < a.length && i < b.length; i++) {
if (a.charCodeAt(i) !== b.charCodeAt(i)) break
}
if (i === a.length) return a
if (i === b.length) return b
return a.substring(0, i)
}
function containsDots (path, pathPrefix) {
const suffix = path.substring(pathPrefix.length).replace(/\\/g, '/')
return ('/' + suffix).includes('/.')
}
module.exports = CreateTorrentPage

View File

@@ -1,266 +0,0 @@
const path = require('path')
const React = require('react')
const colors = require('material-ui/styles/colors')
const Checkbox = require('material-ui/Checkbox').default
const RaisedButton = require('material-ui/RaisedButton').default
const Heading = require('../components/heading')
const PathSelector = require('../components/path-selector')
const {dispatch} = require('../lib/dispatcher')
const config = require('../../config')
class PreferencesPage extends React.Component {
constructor (props) {
super(props)
this.handleDownloadPathChange =
this.handleDownloadPathChange.bind(this)
this.handleOpenExternalPlayerChange =
this.handleOpenExternalPlayerChange.bind(this)
this.handleExternalPlayerPathChange =
this.handleExternalPlayerPathChange.bind(this)
this.handleStartupChange =
this.handleStartupChange.bind(this)
}
downloadPathSelector () {
return (
<Preference>
<PathSelector
dialog={{
title: 'Select download directory',
properties: [ 'openDirectory' ]
}}
onChange={this.handleDownloadPathChange}
title='Download location'
value={this.props.state.unsaved.prefs.downloadPath} />
</Preference>
)
}
handleDownloadPathChange (filePath) {
dispatch('updatePreferences', 'downloadPath', filePath)
}
openExternalPlayerCheckbox () {
return (
<Preference>
<Checkbox
className='control'
checked={!this.props.state.unsaved.prefs.openExternalPlayer}
label={'Play torrent media files using WebTorrent'}
onCheck={this.handleOpenExternalPlayerChange} />
</Preference>
)
}
handleOpenExternalPlayerChange (e, isChecked) {
dispatch('updatePreferences', 'openExternalPlayer', !isChecked)
}
highestPlaybackPriorityCheckbox () {
return (
<Preference>
<Checkbox
className='control'
checked={this.props.state.unsaved.prefs.highestPlaybackPriority}
label={'Highest Playback Priority'}
onCheck={this.handleHighestPlaybackPriorityChange}
/>
<p>Pauses all active torrents to allow playback to use all of the available bandwidth.</p>
</Preference>
)
}
handleHighestPlaybackPriorityChange (e, isChecked) {
dispatch('updatePreferences', 'highestPlaybackPriority', isChecked)
}
externalPlayerPathSelector () {
const playerPath = this.props.state.unsaved.prefs.externalPlayerPath
const playerName = this.props.state.getExternalPlayerName()
const description = this.props.state.unsaved.prefs.openExternalPlayer
? `Torrent media files will always play in ${playerName}.`
: `Torrent media files will play in ${playerName} if WebTorrent cannot play them.`
return (
<Preference>
<p>{description}</p>
<PathSelector
dialog={{
title: 'Select media player app',
properties: [ 'openFile' ]
}}
displayValue={playerName}
onChange={this.handleExternalPlayerPathChange}
title='External player'
value={playerPath ? path.dirname(playerPath) : null} />
</Preference>
)
}
handleExternalPlayerPathChange (filePath) {
dispatch('updatePreferences', 'externalPlayerPath', filePath)
}
autoAddTorrentsCheckbox () {
return (
<Preference>
<Checkbox
className='control'
checked={this.props.state.unsaved.prefs.autoAddTorrents}
label={'Watch for new .torrent files and add them immediately'}
onCheck={(e, value) => { this.handleAutoAddTorrentsChange(e, value) }}
/>
</Preference>
)
}
handleAutoAddTorrentsChange (e, isChecked) {
const torrentsFolderPath = this.props.state.unsaved.prefs.torrentsFolderPath
if (isChecked && !torrentsFolderPath) {
alert('Select a torrents folder first.') // eslint-disable-line
e.preventDefault()
return
}
dispatch('updatePreferences', 'autoAddTorrents', isChecked)
if (isChecked) {
dispatch('startFolderWatcher', null)
return
}
dispatch('stopFolderWatcher', null)
}
torrentsFolderPathSelector () {
const torrentsFolderPath = this.props.state.unsaved.prefs.torrentsFolderPath
return (
<Preference>
<PathSelector
dialog={{
title: 'Select folder to watch for new torrents',
properties: [ 'openDirectory' ]
}}
displayValue={torrentsFolderPath || ''}
onChange={this.handletorrentsFolderPathChange}
title='Folder to watch'
value={torrentsFolderPath ? path.dirname(torrentsFolderPath) : null} />
</Preference>
)
}
handletorrentsFolderPathChange (filePath) {
dispatch('updatePreferences', 'torrentsFolderPath', filePath)
}
setDefaultAppButton () {
const isFileHandler = this.props.state.unsaved.prefs.isFileHandler
if (isFileHandler) {
return (
<Preference>
<p>WebTorrent is your default torrent app. Hooray!</p>
</Preference>
)
}
return (
<Preference>
<p>WebTorrent is not currently the default torrent app.</p>
<RaisedButton
className='control'
onClick={this.handleSetDefaultApp}
label='Make WebTorrent the default' />
</Preference>
)
}
handleStartupChange (e, isChecked) {
dispatch('updatePreferences', 'startup', isChecked)
}
setStartupSection () {
if (config.IS_PORTABLE) {
return
}
return (
<PreferencesSection title='Startup'>
<Preference>
<Checkbox
className='control'
checked={this.props.state.unsaved.prefs.startup}
label={'Open WebTorrent on startup.'}
onCheck={this.handleStartupChange}
/>
</Preference>
</PreferencesSection>
)
}
handleSetDefaultApp () {
dispatch('updatePreferences', 'isFileHandler', true)
}
render () {
const style = {
color: colors.grey400,
marginLeft: 25,
marginRight: 25
}
return (
<div style={style}>
<PreferencesSection title='Folders'>
{this.downloadPathSelector()}
{this.autoAddTorrentsCheckbox()}
{this.torrentsFolderPathSelector()}
</PreferencesSection>
<PreferencesSection title='Playback'>
{this.openExternalPlayerCheckbox()}
{this.externalPlayerPathSelector()}
{this.highestPlaybackPriorityCheckbox()}
</PreferencesSection>
<PreferencesSection title='Default torrent app'>
{this.setDefaultAppButton()}
</PreferencesSection>
{this.setStartupSection()}
</div>
)
}
}
class PreferencesSection extends React.Component {
static get propTypes () {
return {
title: React.PropTypes.string
}
}
render () {
const style = {
marginBottom: 25,
marginTop: 25
}
return (
<div style={style}>
<Heading level={2}>{this.props.title}</Heading>
{this.props.children}
</div>
)
}
}
class Preference extends React.Component {
render () {
const style = { marginBottom: 10 }
return (<div style={style}>{this.props.children}</div>)
}
}
module.exports = PreferencesPage

View File

@@ -2,28 +2,26 @@
// process from the main window.
console.time('init')
const crypto = require('crypto')
const deepEqual = require('deep-equal')
const defaultAnnounceList = require('create-torrent').announceList
const electron = require('electron')
const fs = require('fs')
const mkdirp = require('mkdirp')
const musicmetadata = require('musicmetadata')
const networkAddress = require('network-address')
const path = require('path')
const WebTorrent = require('webtorrent')
const zeroFill = require('zero-fill')
var crypto = require('crypto')
var deepEqual = require('deep-equal')
var defaultAnnounceList = require('create-torrent').announceList
var electron = require('electron')
var fs = require('fs-extra')
var musicmetadata = require('musicmetadata')
var networkAddress = require('network-address')
var path = require('path')
var WebTorrent = require('webtorrent')
var zeroFill = require('zero-fill')
const crashReporter = require('../crash-reporter')
const config = require('../config')
const {TorrentKeyNotFoundError} = require('./lib/errors')
const torrentPoster = require('./lib/torrent-poster')
var crashReporter = require('../crash-reporter')
var config = require('../config')
var torrentPoster = require('./lib/torrent-poster')
// Report when the process crashes
crashReporter.init()
// Send & receive messages from the main window
const ipc = electron.ipcRenderer
var ipc = electron.ipcRenderer
// Force use of webtorrent trackers on all torrents
global.WEBTORRENT_ANNOUNCE = defaultAnnounceList
@@ -33,7 +31,7 @@ global.WEBTORRENT_ANNOUNCE = defaultAnnounceList
/**
* WebTorrent version.
*/
const VERSION = require('../../package.json').version
var VERSION = require('../../package.json').version
/**
* Version number in Azureus-style. Generated from major and minor semver version.
@@ -41,7 +39,7 @@ const VERSION = require('../../package.json').version
* '0.16.1' -> '0016'
* '1.2.5' -> '0102'
*/
const VERSION_STR = VERSION.match(/([0-9]+)/g)
var VERSION_STR = VERSION.match(/([0-9]+)/g)
.slice(0, 2)
.map((v) => zeroFill(2, v))
.join('')
@@ -53,27 +51,25 @@ const VERSION_STR = VERSION.match(/([0-9]+)/g)
* For example:
* '-WW0102-'...
*/
const VERSION_PREFIX = '-WD' + VERSION_STR + '-'
/**
* Generate an ephemeral peer ID each time.
*/
const PEER_ID = Buffer.from(VERSION_PREFIX + crypto.randomBytes(9).toString('base64'))
var VERSION_PREFIX = '-WD' + VERSION_STR + '-'
// Connect to the WebTorrent and BitTorrent networks. WebTorrent Desktop is a hybrid
// client, as explained here: https://webtorrent.io/faq
let client = window.client = new WebTorrent({ peerId: PEER_ID })
var client = window.client = new WebTorrent({
peerId: Buffer.from(VERSION_PREFIX + crypto.randomBytes(6).toString('hex'))
})
// WebTorrent-to-HTTP streaming sever
let server = null
var server = null
// Used for diffing, so we only send progress updates when necessary
let prevProgress = null
var prevProgress = null
init()
function init () {
listenToClientEvents()
client.on('warning', (err) => ipc.send('wt-warning', null, err.message))
client.on('error', (err) => ipc.send('wt-error', null, err.message))
ipc.on('wt-start-torrenting', (e, torrentKey, torrentID, path, fileModtimes, selections) =>
startTorrenting(torrentKey, torrentID, path, fileModtimes, selections))
@@ -87,8 +83,10 @@ function init () {
generateTorrentPoster(torrentKey))
ipc.on('wt-get-audio-metadata', (e, infoHash, index) =>
getAudioMetadata(infoHash, index))
ipc.on('wt-start-server', (e, infoHash) =>
startServer(infoHash))
ipc.on('wt-get-mkv-subtitles', (e, infoHash, index) =>
getMKVSubtitles(infoHash, index))
ipc.on('wt-start-server', (e, infoHash, index) =>
startServer(infoHash, index))
ipc.on('wt-stop-server', (e) =>
stopServer())
ipc.on('wt-select-files', (e, infoHash, selections) =>
@@ -104,17 +102,12 @@ function init () {
console.timeEnd('init')
}
function listenToClientEvents () {
client.on('warning', (err) => ipc.send('wt-warning', 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.
// Returns a WebTorrent object. See https://git.io/vik9M
// Starts a given TorrentID, which can be an infohash, magnet URI, etc. Returns WebTorrent object
// See https://github.com/feross/webtorrent/blob/master/docs/api.md#clientaddtorrentid-opts-function-ontorrent-torrent-
function startTorrenting (torrentKey, torrentID, path, fileModtimes, selections) {
console.log('starting torrent %s: %s', torrentKey, torrentID)
const torrent = client.add(torrentID, {
var torrent = client.add(torrentID, {
path: path,
fileModtimes: fileModtimes
})
@@ -125,19 +118,20 @@ function startTorrenting (torrentKey, torrentID, path, fileModtimes, selections)
// Only download the files the user wants, not necessarily all files
torrent.once('ready', () => selectFiles(torrent, selections))
return torrent
}
function stopTorrenting (infoHash) {
console.log('--- STOP TORRENTING: ', infoHash)
const torrent = client.get(infoHash)
var torrent = client.get(infoHash)
if (torrent) torrent.destroy()
}
// Create a new torrent, start seeding
function createTorrent (torrentKey, options) {
console.log('creating torrent', torrentKey, options)
const paths = options.files.map((f) => f.path)
const torrent = client.seed(paths, options)
var paths = options.files.map((f) => f.path)
var torrent = client.seed(paths, options)
torrent.key = torrentKey
addTorrentEvents(torrent)
ipc.send('wt-new-torrent')
@@ -155,22 +149,22 @@ function addTorrentEvents (torrent) {
torrent.on('done', torrentDone)
function torrentMetadata () {
const info = getTorrentInfo(torrent)
var info = getTorrentInfo(torrent)
ipc.send('wt-metadata', torrent.key, info)
updateTorrentProgress()
}
function torrentReady () {
const info = getTorrentInfo(torrent)
var info = getTorrentInfo(torrent)
ipc.send('wt-ready', torrent.key, info)
ipc.send('wt-ready-' + torrent.infoHash, torrent.key, info)
ipc.send('wt-ready-' + torrent.infoHash, torrent.key, info) // TODO: hack
updateTorrentProgress()
}
function torrentDone () {
const info = getTorrentInfo(torrent)
var info = getTorrentInfo(torrent)
ipc.send('wt-done', torrent.key, info)
updateTorrentProgress()
@@ -203,22 +197,19 @@ function getTorrentFileInfo (file) {
}
}
// Every time we resolve a magnet URI, save the torrent file so that we can use
// it on next startup. Starting with the full torrent metadata will be faster
// than re-fetching it from peers using ut_metadata.
// Every time we resolve a magnet URI, save the torrent file so that we never
// have to download it again. Never ask the DHT the same question twice.
function saveTorrentFile (torrentKey) {
const torrent = getTorrent(torrentKey)
const torrentPath = path.join(config.TORRENT_PATH, torrent.infoHash + '.torrent')
fs.access(torrentPath, fs.constants.R_OK, function (err) {
const fileName = torrent.infoHash + '.torrent'
if (!err) {
var torrent = getTorrent(torrentKey)
checkIfTorrentFileExists(torrent.infoHash, function (torrentPath, exists) {
var fileName = torrent.infoHash + '.torrent'
if (exists) {
// We've already saved the file
return ipc.send('wt-file-saved', torrentKey, fileName)
}
// Otherwise, save the .torrent file, under the app config folder
mkdirp(config.TORRENT_PATH, function (_) {
fs.mkdir(config.TORRENT_PATH, function (_) {
fs.writeFile(torrentPath, torrent.torrentFile, function (err) {
if (err) return console.log('error saving torrent file %s: %o', torrentPath, err)
console.log('saved torrent file %s', torrentPath)
@@ -228,17 +219,26 @@ function saveTorrentFile (torrentKey) {
})
}
// Checks whether we've already resolved a given infohash to a torrent file
// Calls back with (torrentPath, exists). Logs, does not call back on error
function checkIfTorrentFileExists (infoHash, cb) {
var torrentPath = path.join(config.TORRENT_PATH, infoHash + '.torrent')
fs.exists(torrentPath, function (exists) {
cb(torrentPath, exists)
})
}
// Save a JPG that represents a torrent.
// Auto chooses either a frame from a video file, an image, etc
function generateTorrentPoster (torrentKey) {
const torrent = getTorrent(torrentKey)
var torrent = getTorrent(torrentKey)
torrentPoster(torrent, function (err, buf, extension) {
if (err) return console.log('error generating poster: %o', err)
// save it for next time
mkdirp(config.POSTER_PATH, function (err) {
fs.mkdirp(config.POSTER_PATH, function (err) {
if (err) return console.log('error creating poster dir: %o', err)
const posterFileName = torrent.infoHash + extension
const posterFilePath = path.join(config.POSTER_PATH, posterFileName)
var posterFileName = torrent.infoHash + extension
var posterFilePath = path.join(config.POSTER_PATH, posterFileName)
fs.writeFile(posterFilePath, buf, function (err) {
if (err) return console.log('error saving poster: %o', err)
// show the poster
@@ -249,7 +249,7 @@ function generateTorrentPoster (torrentKey) {
}
function updateTorrentProgress () {
const progress = getTorrentProgress()
var progress = getTorrentProgress()
// TODO: diff torrent-by-torrent, not once for the whole update
if (prevProgress && deepEqual(progress, prevProgress, {strict: true})) {
return /* don't send heavy object if it hasn't changed */
@@ -260,19 +260,19 @@ function updateTorrentProgress () {
function getTorrentProgress () {
// First, track overall progress
const progress = client.progress
const hasActiveTorrents = client.torrents.some(function (torrent) {
var progress = client.progress
var hasActiveTorrents = client.torrents.some(function (torrent) {
return torrent.progress !== 1
})
// Track progress for every file in each torrent
// TODO: ideally this would be tracked by WebTorrent, which could do it
// more efficiently than looping over torrent.bitfield
const torrentProg = client.torrents.map(function (torrent) {
const fileProg = torrent.files && torrent.files.map(function (file, index) {
const numPieces = file._endPiece - file._startPiece + 1
let numPiecesPresent = 0
for (let piece = file._startPiece; piece <= file._endPiece; piece++) {
var torrentProg = client.torrents.map(function (torrent) {
var fileProg = torrent.files && torrent.files.map(function (file, index) {
var numPieces = file._endPiece - file._startPiece + 1
var numPiecesPresent = 0
for (var piece = file._startPiece; piece <= file._endPiece; piece++) {
if (torrent.bitfield.get(piece)) numPiecesPresent++
}
return {
@@ -303,28 +303,28 @@ function getTorrentProgress () {
}
}
function startServer (infoHash) {
const torrent = client.get(infoHash)
if (torrent.ready) startServerFromReadyTorrent(torrent)
else torrent.once('ready', () => startServerFromReadyTorrent(torrent))
function startServer (infoHash, index) {
var torrent = client.get(infoHash)
if (torrent.ready) startServerFromReadyTorrent(torrent, index)
else torrent.once('ready', () => startServerFromReadyTorrent(torrent, index))
}
function startServerFromReadyTorrent (torrent, cb) {
function startServerFromReadyTorrent (torrent, index, cb) {
if (server) return
// start the streaming torrent-to-http server
server = torrent.createServer()
server.listen(0, function () {
const port = server.address().port
const urlSuffix = ':' + port
const info = {
var port = server.address().port
var urlSuffix = ':' + port + '/' + index
var info = {
torrentKey: torrent.key,
localURL: 'http://localhost' + urlSuffix,
networkURL: 'http://' + networkAddress() + urlSuffix
}
ipc.send('wt-server-running', info)
ipc.send('wt-server-' + torrent.infoHash, info)
ipc.send('wt-server-' + torrent.infoHash, info) // TODO: hack
})
}
@@ -335,28 +335,49 @@ function stopServer () {
}
function getAudioMetadata (infoHash, index) {
const torrent = client.get(infoHash)
const file = torrent.files[index]
var torrent = client.get(infoHash)
var file = torrent.files[index]
musicmetadata(file.createReadStream(), function (err, info) {
if (err) return console.log('error getting audio metadata for ' + infoHash + ':' + index, err)
const { artist, album, albumartist, title, year, track, disk, genre } = info
const importantInfo = { artist, album, albumartist, title, year, track, disk, genre }
console.log('got audio metadata for %s: %o', file.name, importantInfo)
ipc.send('wt-audio-metadata', infoHash, index, importantInfo)
if (err) return
console.log('got audio metadata for %s: %o', file.name, info)
ipc.send('wt-audio-metadata', infoHash, index, info)
})
}
function getMKVSubtitles (infoHash, index) {
var torrent = client.get(infoHash)
var file = torrent.files[index]
var MatroskaSubtitles = require('matroska-subtitles')
var subtitleParser = new MatroskaSubtitles()
var textTracks = new Map()
subtitleParser.once('tracks', function (tracks) {
tracks.forEach(function (track) {
textTracks.set(track.number, { track: track, subtitles: [] })
})
})
subtitleParser.on('subtitle', function (subtitle, trackNumber) {
textTracks.get(trackNumber).subtitles.push(subtitle)
})
subtitleParser.on('finish', function () {
ipc.send('wt-mkv-subtitles', Array.from(textTracks.values()))
})
file.createReadStream().pipe(subtitleParser)
}
function selectFiles (torrentOrInfoHash, selections) {
// Get the torrent object
let torrent
var torrent
if (typeof torrentOrInfoHash === 'string') {
torrent = client.get(torrentOrInfoHash)
} else {
torrent = torrentOrInfoHash
}
if (!torrent) {
throw new Error('selectFiles: missing torrent ' + torrentOrInfoHash)
}
// Selections not specified?
// Load all files. We still need to replace the default whole-torrent
@@ -376,8 +397,8 @@ function selectFiles (torrentOrInfoHash, selections) {
torrent.deselect(0, torrent.pieces.length - 1, false)
// Add selections (individual files)
for (let i = 0; i < selections.length; i++) {
const file = torrent.files[i]
for (var i = 0; i < selections.length; i++) {
var file = torrent.files[i]
if (selections[i]) {
file.select()
} else {
@@ -390,25 +411,11 @@ function selectFiles (torrentOrInfoHash, selections) {
// Gets a WebTorrent handle by torrentKey
// Throws an Error if we're not currently torrenting anything w/ that key
function getTorrent (torrentKey) {
const ret = client.torrents.find((x) => x.key === torrentKey)
if (!ret) throw new TorrentKeyNotFoundError(torrentKey)
var ret = client.torrents.find((x) => x.key === torrentKey)
if (!ret) throw new Error('missing torrent key ' + torrentKey)
return ret
}
function onError (err) {
console.log(err)
}
// TODO: remove this once the following bugs are fixed:
// https://bugs.chromium.org/p/chromium/issues/detail?id=490143
// https://github.com/electron/electron/issues/7212
window.testOfflineMode = function () {
console.log('Test, going OFFLINE')
client = window.client = new WebTorrent({
peerId: PEER_ID,
tracker: false,
dht: false,
webSeeds: false
})
listenToClientEvents()
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 225 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 233 B

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -265,6 +265,10 @@ table {
font-weight: bold;
}
.open-torrent-address-modal input {
width: 100%;
}
.create-torrent {
padding: 10px 25px;
overflow: hidden;
@@ -272,7 +276,6 @@ table {
.create-torrent .torrent-attribute {
white-space: nowrap;
margin: 8px 0;
}
.create-torrent .torrent-attribute>* {
@@ -280,12 +283,13 @@ table {
}
.create-torrent .torrent-attribute label {
width: 100px;
width: 60px;
margin-right: 10px;
vertical-align: top;
}
.create-torrent .torrent-attribute>div {
width: calc(100% - 100px);
width: calc(100% - 90px);
}
.create-torrent .torrent-attribute div {
@@ -294,6 +298,18 @@ table {
text-overflow: ellipsis;
}
.create-torrent .torrent-attribute textarea {
width: calc(100% - 80px);
height: 80px;
color: #eee;
background-color: transparent;
line-height: 1.5;
font-size: 14px;
font-family: inherit;
border-radius: 2px;
padding: 4px 6px;
}
/*
* BUTTONS
*/
@@ -309,13 +325,13 @@ i:not(.disabled):hover { /* Show they're clickable without pointer: cursor */
-webkit-filter: brightness(1.3);
}
/*
* INPUTS
*/
input,
textarea,
.control {
font-size: 14px !important;
.button-round { /* Circular icon buttons, used on <i> tags */
width: 40px;
height: 40px;
border-radius: 20px;
font-size: 22px;
transition: all 0.1s ease-out;
text-align: center;
}
/*
@@ -338,18 +354,26 @@ textarea,
}
.torrent:not(:last-child) {
border-bottom: 1px solid #282828;
border-bottom: 1px solid rgb(20, 20, 20);
}
.torrent:hover {
-webkit-filter: brightness(1.1);
}
.torrent .metadata {
position: absolute;
top: 20px;
top: 25px;
left: 15px;
right: 15px;
width: calc(100% - 30px);
width: calc(100% - 40px);
text-shadow: rgba(0, 0, 0, 0.5) 0 0 4px;
}
.torrent:hover .metadata {
width: calc(100% - 150px);
}
.torrent .metadata span:not(:last-child)::after {
content: ' • ';
opacity: 0.7;
@@ -357,34 +381,79 @@ textarea,
padding-right: 4px;
}
.torrent:hover .metadata {
width: calc(100% - 120px);
}
.torrent .torrent-controls {
.torrent .buttons {
position: absolute;
top: 29px;
right: 10px;
align-items: center;
display: none;
}
.torrent:hover .torrent-controls {
display: block;
.torrent:hover .buttons {
display: flex;
}
.torrent .play {
position: absolute;
top: 23px;
right: 45px;
font-size: 55px;
.torrent .buttons > * {
margin-left: 6px; /* space buttons apart */
}
.torrent .delete {
position: absolute;
top: 38px;
right: 12px;
.torrent .buttons .download {
background-color: #2233BB;
width: 28px;
height: 28px;
border-radius: 14px;
font-size: 18px;
padding-top: 6px;
}
.torrent .download {
vertical-align: -0.4em;
margin-left: -2px;
.torrent .buttons .download.downloading {
color: #44dd44;
}
.torrent .buttons .download.seeding {
color: #44dd44;
}
.torrent .buttons .play {
padding-top: 10px;
background-color: #F44336;
}
.torrent.timeout .play {
padding-top: 8px;
}
.torrent.requested .play,
.loading-spinner {
border-top: 6px solid rgba(255, 255, 255, 0.2);
border-right: 6px solid rgba(255, 255, 255, 0.2);
border-bottom: 6px solid rgba(255, 255, 255, 0.2);
border-left: 6px solid #ffffff;
border-radius: 50%;
color: transparent;
animation: load8 1.1s infinite linear;
}
@keyframes load8 {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.torrent .buttons .play.resume-position {
position: relative;
-webkit-clip-path: circle(18px at center);
}
.torrent .buttons .delete {
opacity: 0.5;
}
.torrent .buttons .delete:hover {
opacity: 0.7;
}
.torrent .buttons .radial-progress {
@@ -392,27 +461,9 @@ textarea,
}
.torrent .name {
font-size: 20px;
font-size: 18px;
font-weight: bold;
line-height: 1.5em;
margin-bottom: 6px;
}
.torrent hr {
position: absolute;
bottom: 5px;
left: calc(50% - 20px);
width: 40px;
height: 1px;
background: #ccc;
display: none;
margin: 0;
padding: 0;
border: none;
}
.torrent:hover hr {
display: block;
}
/*
@@ -466,7 +517,6 @@ body.drag .app::after {
/*
* TORRENT LIST: EXPANDED TORRENT DETAILS
*/
.torrent.selected {
height: auto;
}
@@ -490,6 +540,10 @@ body.drag .app::after {
height: 28px;
}
.torrent-details tr:hover {
background-color: rgba(200, 200, 200, 0.3);
}
.torrent-details td {
overflow: hidden;
padding: 0;
@@ -586,15 +640,11 @@ body.drag .app::after {
opacity: 1;
}
.player .controls .icon.disabled {
opacity: 0.3;
}
.player .controls .icon.skip-previous,
.player .controls .icon.play-pause,
.player .controls .icon.skip-next {
.player .controls .play-pause {
font-size: 28px;
margin: 5px;
margin-top: 5px;
margin-right: 10px;
margin-left: 15px;
}
.player .controls .volume-slider {
@@ -615,7 +665,6 @@ body.drag .app::after {
font-size: 13px;
margin: 9px 8px 8px 8px;
opacity: 0.8;
font-variant-numeric: tabular-nums;
}
.player .controls .icon.closed-caption {
@@ -637,6 +686,14 @@ body.drag .app::after {
cursor: none;
}
/* TODO: find better way to handle this (that also
* keeps the header visible too).
*/
.app.hide-video-controls .player .controls:hover {
opacity: 1;
cursor: default;
}
/* invisible click target for scrubbing */
.player .controls .scrub-bar {
position: absolute;

View File

@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Main Window</title>
<title>WebTorrent Desktop</title>
<link rel="stylesheet" href="main.css">
</head>
<body>

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>WebTorrent Hidden Window</title>
<title>WebTorrent Desktop</title>
<style>
body {
background-color: #282828;

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