Compare commits

..

12 Commits

Author SHA1 Message Date
Alberto Miranda
e2c3c182d0 Plugins divided into main and renderer processes. 2017-04-23 13:46:52 -03:00
Alberto Miranda
687038560c Passing state and params to plugins.
Dispatcher now sends params to plugins; notifying about plugin load
errors; allowing local plugin paths not to end with a slash.
2017-04-19 09:00:11 -03:00
Alberto Miranda
09d00c6383 Passing dispatcher to plugins.
This will enable plugins to dispatch events and interact with the
renderer process; removed unused extension keys.
2017-04-16 16:31:39 -03:00
Alberto Miranda
957c5b64e9 Merge branch 'master' into plugins 2017-04-16 13:29:51 -03:00
Alberto Miranda
568fede67e fixed periodic plugins update 2017-02-06 08:06:02 -03:00
Alberto Miranda
944eb8b8b0 styling fixes; tests passing; added missing ms dependency. 2017-02-06 00:54:16 -03:00
Alberto Miranda
c3cdda354e added node-notifier; showing plugin notifications. 2017-02-06 00:38:27 -03:00
Alberto Miranda
25ed12ba3c removed extra logging; cleanup 2017-02-06 00:00:26 -03:00
Alberto Miranda
830312842b hot reloading config; hot loading plugins when config changes. 2017-02-05 13:15:20 -03:00
Alberto Miranda
086d8bf00a fixed initialization on first run 2017-02-01 14:35:28 -03:00
Alberto Miranda
50c100130a removed unused packages; hashing installed plugins; saving plugins config; detecting newly installed plugins. 2017-02-01 00:34:28 -03:00
Alberto Miranda
edcea2661a working on plugins features: lots of todos, but current version loads remote and local plugins. 2017-01-30 00:08:22 -03:00
123 changed files with 2113 additions and 13044 deletions

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

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

View File

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

View File

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

4
.gitignore vendored
View File

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

1
.npmrc
View File

@@ -1 +0,0 @@
package-lock=true

View File

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

View File

@@ -37,45 +37,5 @@
- Alexey Romanov (romanalexey@gmail.com)
- Karan Thakkar (karanjthakkar@gmail.com)
- Nuno Campos (nuno.campos@me.com)
- Ebrahim Byagowi (ebrahim@gnu.org)
- Josip Janzic (josip@jjanzic.com)
- Egor Yurtaev (yurtaev.egor@gmail.com)
- Emil Bay (github@tixz.dk)
- Borewit (borewit@users.noreply.github.com)
- greenkeeper[bot] (greenkeeper[bot]@users.noreply.github.com)
- Auyer (rafa_auyer@icloud.com)
- Jon Koops (jonkoops@gmail.com)
- Michael George Attard (michaelgeorgeattard@gmail.com)
- SimplyAhmazing (ahmad19526@gmail.com)
- Cezar Carneiro (cezargcarneiro@gmail.com)
- Bilal Elmoussaoui (bil.elmoussaoui@gmail.com)
- Terry Hau (terryhau@gmail.com)
- Vítor Galvão (info@vitorgalvao.com)
- Borewit (Borewit@users.noreply.github.com)
- Diego Rodríguez (diegorbaquero@gmail.com)
- Dan Flettre (flettre@gmail.com)
- Sibiraj (dev.sibiraj@outlook.com)
- clujin (clujin@gmail.com)
- Sharon Grossman (sharong1337@gmail.com)
- Linus Unnebäck (linus@folkdatorn.se)
- Adrian Tombu (adrian@otso.fr)
- Lucas (5874806+RecoX@users.noreply.github.com)
- David Ernst (dsernst@users.noreply.github.com)
- David Ernst (git@dsernst.com)
- Jimmy Wärting (jimmy@warting.se)
- Recox (5874806+RecoX@users.noreply.github.com)
- greenkeeper[bot] (23040076+greenkeeper[bot]@users.noreply.github.com)
- hicom150 (hicom150@gmail.com)
- Дамјан Георгиевски (gdamjan@gmail.com)
- Jimmy Wärting (jimmy@warting.se)
- Julen Garcia Leunda (hicom150@gmail.com)
- Feross (feross@feross.org)
- Daniele Debernardi (drebrez@gmail.com)
- Chandan Chowdary Bhagam (chandandharana@gmail.com)
- Pieter Goetschalckx (3.14.e.ter@gmail.com)
- Carey Metcalfe (carey@cmetcalfe.ca)
- Ameet Kaustav (akaustav@users.noreply.github.com)
- gpatarin (gael.patarin@outlook.com)
- Gael Patarin (gael.patarin@outlook.com)
#### Generated by bin/update-authors.sh.

View File

@@ -1,105 +1,6 @@
# WebTorrent Desktop Version History
## v0.23.0 - 2020-07-15
This release contains a critical security fix.
### Changed
- Update to Electron 10 beta [\#1834](https://github.com/webtorrent/webtorrent-desktop/pull/1834)
## v0.22.0 - 2020-07-15
### Added
- Linux `.rpm` packages and `arm64` builds are now available! [\#1694](https://github.com/webtorrent/webtorrent-desktop/pull/1694) ([hicom150](https://github.com/hicom150))
- Add support for multiple audio tracks [\#1712](https://github.com/webtorrent/webtorrent-desktop/pull/1712) ([hicom150](https://github.com/hicom150))
- Improve codec unsupported detection [\#1711](https://github.com/webtorrent/webtorrent-desktop/pull/1711) ([hicom150](https://github.com/hicom150))
- Report when files are being verified [\#1717](https://github.com/webtorrent/webtorrent-desktop/pull/1717) ([pR0Ps](https://github.com/pR0Ps))
- Support additional audio files: MPEG-Layer-2, Musepack, Matroska audio, WavePack [\#1772](https://github.com/webtorrent/webtorrent-desktop/pull/1772)
### Changed
- Update to Electron 9 [\#1729](https://github.com/webtorrent/webtorrent-desktop/pull/1729) [\#1832](https://github.com/webtorrent/webtorrent-desktop/issues/1832)
- Update to music-metadata 4.8.0 [\#1719](https://github.com/webtorrent/webtorrent-desktop/pull/1719) ([Borewit](https://github.com/Borewit))
- Update Windows build documentation [\#1715](https://github.com/webtorrent/webtorrent-desktop/pull/1715) ([RecoX](https://github.com/RecoX))
- Remove unneeded dependencies
### Fixed
- Fix a few type errors [\#1720](https://github.com/webtorrent/webtorrent-desktop/pull/1720) ([mathiasvr](https://github.com/mathiasvr))
- Fix electron SUID sandbox error [\#1707](https://github.com/webtorrent/webtorrent-desktop/pull/1707) ([hicom150](https://github.com/hicom150))
- Fix percentage rounding error [\#1716](https://github.com/webtorrent/webtorrent-desktop/pull/1716) ([pR0Ps](https://github.com/pR0Ps))
- Fix path-selector in preferences page [\#1702](https://github.com/webtorrent/webtorrent-desktop/pull/1702) ([314eter](https://github.com/314eter))
- Fix path-selector in preferences page [\#1704](https://github.com/webtorrent/webtorrent-desktop/pull/1702) ([mathiasvr](https://github.com/mathiasvr))
- Fix: Increase height of 'About' window [\#1737](https://github.com/webtorrent/webtorrent-desktop/pull/1737)
- Fix "Save Torrent File As..." [\#1743](https://github.com/webtorrent/webtorrent-desktop/pull/1743)
## v0.21.0 - 2019-09-14
### Added
- Add YouTube style hotkeys [\#1579](https://github.com/webtorrent/webtorrent-desktop/pull/1579) ([dsernst](https://github.com/dsernst))
- Toggle sound notifications on/off [\#1536](https://github.com/webtorrent/webtorrent-desktop/pull/1536) ([adriantombu](https://github.com/adriantombu))
- Ability to play MPEG-4 Audio Book \(.m4b\) [\#1450](https://github.com/webtorrent/webtorrent-desktop/pull/1450) ([Borewit](https://github.com/Borewit))
- Add support for subtitles on Chromecast [\#1165](https://github.com/webtorrent/webtorrent-desktop/pull/1165) ([janza](https://github.com/janza))
### Changed
- Update to Electron 4 [\#1590](https://github.com/webtorrent/webtorrent-desktop/pull/1590) ([Borewit](https://github.com/Borewit))
- Remove '\(BETA\)' from app window title [\#1562](https://github.com/webtorrent/webtorrent-desktop/pull/1562) ([dsernst](https://github.com/dsernst))
- Update React (v16) and Material-UI (v0.20) [\#1483](https://github.com/webtorrent/webtorrent-desktop/pull/1483) ([mathiasvr](https://github.com/mathiasvr))
- Show audio track and disk number [\#1454](https://github.com/webtorrent/webtorrent-desktop/pull/1454) ([Borewit](https://github.com/Borewit))
- Asynchronous music metadata updates while streaming [\#1449](https://github.com/webtorrent/webtorrent-desktop/pull/1449) ([Borewit](https://github.com/Borewit))
- If torrent is not private, leave private flag unset [\#1411](https://github.com/webtorrent/webtorrent-desktop/pull/1411) ([feross](https://github.com/feross))
- Improve audio poster selection: [\#1368](https://github.com/webtorrent/webtorrent-desktop/pull/1368) ([Borewit](https://github.com/Borewit))
- Save preferences immediately when changed [\#1042](https://github.com/webtorrent/webtorrent-desktop/pull/1042) ([Flet](https://github.com/Flet))
### Fixed
- Ensure that the minutes field in playback indicator is zero-padded. [\#1506](https://github.com/webtorrent/webtorrent-desktop/pull/1506) ([bnjmnt4n](https://github.com/bnjmnt4n))
- Bug Fix: Empty Array Reduce [\#1494](https://github.com/webtorrent/webtorrent-desktop/pull/1494) ([clujin](https://github.com/clujin))
- Fix startup problems [\#1419](https://github.com/webtorrent/webtorrent-desktop/pull/1419) ([Borewit](https://github.com/Borewit))
- Add back loading spinner for player page. [\#1311](https://github.com/webtorrent/webtorrent-desktop/pull/1311) ([bnjmnt4n](https://github.com/bnjmnt4n))
- Fix Linux desktop file [\#1309](https://github.com/webtorrent/webtorrent-desktop/pull/1309) ([bilelmoussaoui](https://github.com/bilelmoussaoui))
## v0.20.0 - 2018-04-26
### Added
- Added support for additional audio extensions: 'aiff', 'ape', 'mp2', 'oga', 'opus', 'wma' (#1240)
### Changed
- Displaying filename while music metadata is being downloaded (#1361)
- Improved the poster selection for audio/music based torrents (#1334)
- Launch VLC player without the `--video-on-top` flag (#1286)
### Fixed
- Fix silently failing to open magnets links on Linux (#1367)
## 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 Electron protocol handler issue (Windows)
- 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
## v0.18.0
### Added
- Add a new "Transfers" menu for pausing or resuming all torrents (#1027)
@@ -166,7 +67,7 @@ This release contains a critical security fix.
## v0.16.0 - 2016-09-18
### Added
- **Windows 64-bit support!** (#931)
- **Windows 64-bit support!** ([#931](https://github.com/webtorrent/webtorrent-desktop/pull/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

188
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,188 @@
# Contributing Guidelines
Contributions welcome!
**Before spending lots of time on something, ask for feedback on your idea first!**
Please search issues and pull requests before adding something new to avoid duplicating
efforts and conversations.
This project welcomes non-code contributions, too! The following types of contributions
are welcome:
- **Ideas**: participate in an issue thread or start your own to have your voice heard.
- **Writing**: contribute your expertise in an area by helping expand the included docs.
- **Copy editing**: fix typos, clarify language, and improve the quality of the docs.
- **Formatting**: help keep docs easy to read with consistent formatting.
## Code Style
[![standard][standard-image]][standard-url]
This repository uses [`standard`][standard-url] to maintain code style and consistency,
and to avoid style arguments. `npm test` runs `standard` automatically, so you don't have
to!
[standard-image]: https://cdn.rawgit.com/feross/standard/master/badge.svg
[standard-url]: https://standardjs.com
## Project Governance
Individuals making significant and valuable contributions are given commit-access to the
project to contribute as they see fit. This project is more like an open wiki than a
standard guarded open source project.
### Rules
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.
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.
4. **Pull requests** are *encouraged* for all contributions to solicit feedback, but left to
the discretion of the contributor.
### Releases
Declaring formal releases remains the prerogative of the project maintainer.
### Changes to this arrangement
This is an experiment and feedback is welcome! This document may also be subject to pull-
requests or changes by contributors where you believe you have something valuable to add
or change.
## Developer's Certificate of Origin 1.1
By making a contribution to this project, I certify that:
- (a) The contribution was created in whole or in part by me and I have the right to
submit it under the open source license indicated in the file; or
- (b) The contribution is based upon previous work that, to the best of my knowledge, is
covered under an appropriate open source license and I have the right under that license
to submit that work with modifications, whether created in whole or in part by me, under
the same open source license (unless I am permitted to submit under a different
license), as indicated in the file; or
- (c) The contribution was provided directly to me by some other person who certified
(a), (b) or (c) and I have not modified it.
- (d) I understand and agree that this project and the contribution are public and that a
record of the contribution (including all personal information I submit with it,
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:
1. Click "Play" to stream a built-in torrent (e.g. Sintel)
- Ensure that seeking to undownloaded region works and plays immediately.
- Ensure that sintel.mp4 gets downloaded to `~/Downloads`.
2. Check that the auto-updater works
- Open the console and check for the line "No update available" to indicate
3. Add a new .torrent file via drag-and-drop.
- Ensure that it gets added to the list and starts downloading
4. Remove a torrent from the client
- Ensure that the file is removed from `~/Downloads`
5. Create and seed a new a torrent via drag-and-drop.
- Ensure that the torrent gets created and seeding begins.

View File

@@ -12,34 +12,21 @@
<h4 align="center">The streaming torrent app. For Mac, Windows, and Linux.</h4>
<p align="center">
<a href="https://discord.gg/cnXkm4Z"><img src="https://img.shields.io/discord/612575111718895616" alt="discord"></a>
<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://github.com/webtorrent/webtorrent-desktop/releases"><img src="https://img.shields.io/github/release/webtorrent/webtorrent-desktop.svg" alt="github release version"></a>
<a href="https://github.com/webtorrent/webtorrent-desktop/releases"><img src="https://img.shields.io/github/downloads/webtorrent/webtorrent-desktop/total.svg" alt="github release downloads"></a>
<a href="https://standardjs.com"><img src="https://img.shields.io/badge/code_style-standard-brightgreen.svg" alt="Standard - JavaScript Style Guide"></a>
</p>
## Install
### Recommended Install
Download the latest version of WebTorrent Desktop from
[the official website](https://webtorrent.io/desktop/):
[the official website](https://webtorrent.io/desktop/) or the
[GitHub releases](https://github.com/webtorrent/webtorrent-desktop/releases) page.
### [✨ Download WebTorrent Desktop ✨](https://webtorrent.io/desktop/)
### Advanced Install
- Download specific installer files from the [GitHub releases](https://github.com/webtorrent/webtorrent-desktop/releases) page.
- Use [Homebrew-Cask](https://github.com/caskroom/homebrew-cask) to install from the command line:
```
$ brew cask install webtorrent
```
- Try the (unstable) development version by cloning the Git repository. See the
["How to Contribute"](#how-to-contribute) instructions.
**WebTorrent Desktop** is under very active development. You can try out the
current (unstable) development version by cloning the Git repo. See the
instructions below in the ["How to Contribute"](#how-to-contribute) section.
## Screenshots
@@ -97,7 +84,7 @@ comparing each one to a reference. Why screenshots?
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 2018 MacBook Pro 13".
the same resolution as a 2016 12" Macbook.
For Windows, you'll need Windows 10 with a 1366x768 screen.
@@ -125,7 +112,6 @@ The following optional arguments are available:
- `--sign` - Sign the application (Mac, Windows)
- `--package=[type]` - Package single output type.
- `deb` - Debian package
- `rpm` - RedHat package
- `zip` - Linux zip file
- `dmg` - Mac disk image
- `exe` - Windows installer
@@ -142,12 +128,11 @@ The Windows app can be packaged from **any** platform.
Note: Windows code signing only works from **Windows**, for now.
Note: To package the Windows app from non-Windows platforms,
[Wine](https://www.winehq.org/) and [Mono](https://www.mono-project.com/) need
to be installed. For example on Mac, first install
[XQuartz](http://www.xquartz.org/), then run:
[Wine](https://www.winehq.org/) needs to be installed. For example on Mac, first
install [XQuartz](http://www.xquartz.org/), then run:
```
brew install wine mono
brew install wine
```
(Requires the [Homebrew](http://brew.sh/) package manager.)
@@ -160,22 +145,6 @@ The Mac app can only be packaged from **macOS**.
The Linux app can be packaged from **any** platform.
If packaging from Mac, install system dependencies with Homebrew by running:
```
npm run install-system-deps
```
#### Recommended readings to start working in the app
Electron (Framework to make native apps for Windows, OSX and Linux in Javascript):
https://electronjs.org/docs/tutorial/quick-start
React.js (Framework to work with Frontend UI):
https://reactjs.org/docs/getting-started.html
Material UI (React components that implement Google's Material Design.):
https://material-ui.com/getting-started/installation
### Privacy
WebTorrent Desktop collects some basic usage stats to help us make the app better.

View File

@@ -1,117 +0,0 @@
## Release Process
### 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
```
Move the `.zip` and `.dmg` file somewhere because the next step wipes the `dist/` folder away.
```
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:
1. Click "Play" to stream a built-in torrent (e.g. Sintel)
- Ensure that seeking to undownloaded region works and plays immediately.
- Ensure that sintel.mp4 gets downloaded to `~/Downloads`.
2. Check that the auto-updater works
- Open the console and check for the line "No update available" to indicate that the auto-updater is working.
3. Add a new .torrent file via drag-and-drop.
- Ensure that it gets added to the list and starts downloading.
4. Remove a torrent from the client
- Ensure that the file is removed from `~/Downloads`
5. Create and seed a new a torrent via drag-and-drop.
- Ensure that the torrent gets created and seeding begins.

View File

@@ -7,7 +7,7 @@ const path = require('path')
let hasErrors = false
// Find all Javascript source files
const files = walkSync('src', { globs: ['**/*.js'] })
const files = walkSync('src', {globs: ['**/*.js']})
console.log('Running extra-lint on ' + files.length + ' files...')
// Read each file, line by line
@@ -18,6 +18,13 @@ files.forEach(function (file) {
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'
@@ -28,7 +35,7 @@ files.forEach(function (file) {
}
if (error) {
const name = path.basename(file)
let name = path.basename(file)
console.log('%s:%d - %s:\n%s', name, i + 1, error, line)
hasErrors = true
}

View File

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

View File

@@ -8,6 +8,7 @@ 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')
@@ -36,9 +37,10 @@ const argv = minimist(process.argv.slice(2), {
})
function build () {
console.log('Installing node_modules...')
console.log('Reinstalling node_modules...')
rimraf.sync(NODE_MODULES_PATH)
cp.execSync('npm ci', { stdio: 'inherit' })
cp.execSync('npm install', { stdio: 'inherit' })
cp.execSync('npm dedupe', { stdio: 'inherit' })
console.log('Nuking dist/ and build/...')
rimraf.sync(DIST_PATH)
@@ -69,11 +71,11 @@ function build () {
const all = {
// The human-readable copyright line for the app. Maps to the `LegalCopyright` metadata
// property on Windows, and `NSHumanReadableCopyright` on Mac.
appCopyright: config.APP_COPYRIGHT,
'app-copyright': config.APP_COPYRIGHT,
// The release version of the application. Maps to the `ProductVersion` metadata
// property on Windows, and `CFBundleShortVersionString` on Mac.
appVersion: pkg.version,
'app-version': pkg.version,
// 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
@@ -87,7 +89,7 @@ const all = {
// 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
// start with a number. We're using the version of the underlying WebTorrent library.
buildVersion: require('webtorrent/package.json').version,
'build-version': require('webtorrent/package.json').version,
// The application source directory.
dir: config.ROOT_PATH,
@@ -117,18 +119,18 @@ const darwin = {
// Build for Mac
platform: 'darwin',
// Build x64 binary only.
// Build x64 binaries only.
arch: 'x64',
// The bundle identifier to use in the application's plist (Mac only).
appBundleId: 'io.webtorrent.webtorrent',
'app-bundle-id': 'io.webtorrent.webtorrent',
// The application category type, as shown in the Finder via "View" -> "Arrange by
// Application Category" when viewing the Applications directory (Mac only).
appCategoryType: 'public.app-category.utilities',
'app-category-type': 'public.app-category.utilities',
// The bundle identifier to use in the application helper's plist (Mac only).
helperBundleId: 'io.webtorrent.webtorrent-helper',
'helper-bundle-id': 'io.webtorrent.webtorrent-helper',
// Application icon.
icon: config.APP_ICON + '.icns'
@@ -138,8 +140,8 @@ const win32 = {
// Build for Windows.
platform: 'win32',
// Build x64 binary only.
arch: 'x64',
// Build ia32 and x64 binaries.
arch: ['ia32', 'x64'],
// Object hash of application metadata to embed into the executable (Windows only)
win32metadata: {
@@ -172,8 +174,8 @@ const linux = {
// Build for Linux.
platform: 'linux',
// Build x64 and arm64 binaries.
arch: ['x64', 'arm64']
// Build ia32 and x64 binaries.
arch: ['ia32', 'x64']
// Note: Application icon for Linux is specified via the BrowserWindow `icon` option.
}
@@ -184,7 +186,8 @@ function buildDarwin (cb) {
const plist = require('plist')
console.log('Mac: Packaging electron...')
electronPackager(Object.assign({}, all, darwin)).then(function (buildPath) {
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')
@@ -195,16 +198,16 @@ function buildDarwin (cb) {
infoPlist.CFBundleDocumentTypes = [
{
CFBundleTypeExtensions: ['torrent'],
CFBundleTypeExtensions: [ 'torrent' ],
CFBundleTypeIconFile: path.basename(config.APP_FILE_ICON) + '.icns',
CFBundleTypeName: 'BitTorrent Document',
CFBundleTypeRole: 'Editor',
LSHandlerRank: 'Owner',
LSItemContentTypes: ['org.bittorrent.torrent']
LSItemContentTypes: [ 'org.bittorrent.torrent' ]
},
{
CFBundleTypeName: 'Any',
CFBundleTypeOSTypes: ['****'],
CFBundleTypeOSTypes: [ '****' ],
CFBundleTypeRole: 'Editor',
LSHandlerRank: 'Owner',
LSTypeIsPackage: false
@@ -216,13 +219,13 @@ function buildDarwin (cb) {
CFBundleTypeRole: 'Editor',
CFBundleURLIconFile: path.basename(config.APP_FILE_ICON) + '.icns',
CFBundleURLName: 'BitTorrent Magnet URL',
CFBundleURLSchemes: ['magnet']
CFBundleURLSchemes: [ 'magnet' ]
},
{
CFBundleTypeRole: 'Editor',
CFBundleURLIconFile: path.basename(config.APP_FILE_ICON) + '.icns',
CFBundleURLName: 'BitTorrent Stream-Magnet URL',
CFBundleURLSchemes: ['stream-magnet']
CFBundleURLSchemes: [ 'stream-magnet' ]
}
]
@@ -239,7 +242,7 @@ function buildDarwin (cb) {
UTTypeReferenceURL: 'http://www.bittorrent.org/beps/bep_0000.html',
UTTypeTagSpecification: {
'com.apple.ostype': 'TORR',
'public.filename-extension': ['torrent'],
'public.filename-extension': [ 'torrent' ],
'public.mime-type': 'application/x-bittorrent'
}
}
@@ -353,8 +356,6 @@ function buildDarwin (cb) {
cb(null)
})
}
}).catch(function (err) {
cb(err)
})
}
@@ -375,7 +376,8 @@ function buildWin32 (cb) {
CERT_PATH = path.join(os.homedir(), 'Desktop')
}
electronPackager(Object.assign({}, all, win32)).then(function (buildPath) {
electronPackager(Object.assign({}, all, win32), function (err, buildPath) {
if (err) return cb(err)
console.log('Windows: Packaged electron. ' + buildPath)
let signWithParams
@@ -394,17 +396,21 @@ function buildWin32 (cb) {
const tasks = []
buildPath.forEach(function (filesPath) {
const destArch = filesPath.split('-').pop()
if (argv.package === 'exe' || argv.package === 'all') {
tasks.push((cb) => packageInstaller(filesPath, cb))
tasks.push((cb) => packageInstaller(filesPath, destArch, cb))
}
if (argv.package === 'portable' || argv.package === 'all') {
tasks.push((cb) => packagePortable(filesPath, cb))
tasks.push((cb) => packagePortable(filesPath, destArch, cb))
}
})
series(tasks, cb)
function packageInstaller (filesPath, cb) {
console.log('Windows: Creating installer...')
function packageInstaller (filesPath, destArch, cb) {
console.log(`Windows: Creating ${destArch} installer...`)
const archStr = destArch === 'ia32' ? '-ia32' : ''
installer.createWindowsInstaller({
appDirectory: filesPath,
@@ -417,69 +423,107 @@ 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: config.GITHUB_URL,
// 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 + '.exe',
setupExe: config.APP_NAME + 'Setup-v' + config.APP_VERSION + archStr + '.exe',
setupIcon: config.APP_ICON + '.ico',
signWithParams,
signWithParams: signWithParams,
title: config.APP_NAME,
usePackageJson: false,
version: pkg.version
})
.then(function () {
console.log('Windows: Created installer.')
.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))
})
fs.readdirSync(DIST_PATH)
.filter((name) => name.endsWith('.nupkg') && !name.includes(pkg.version))
.forEach((filename) => {
fs.unlinkSync(path.join(DIST_PATH, filename))
})
cb(null)
})
.catch(cb)
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.')
}
cb(null)
})
.catch(cb)
}
function packagePortable (filesPath, cb) {
console.log('Windows: Creating portable app...')
function packagePortable (filesPath, destArch, cb) {
console.log(`Windows: Creating ${destArch} portable app...`)
const portablePath = path.join(filesPath, 'Portable Settings')
fs.mkdirSync(portablePath, { recursive: true })
mkdirp.sync(portablePath)
const downloadsPath = path.join(portablePath, 'Downloads')
fs.mkdirSync(downloadsPath, { recursive: true })
mkdirp.sync(downloadsPath)
const tempPath = path.join(portablePath, 'Temp')
fs.mkdirSync(tempPath, { recursive: true })
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.zip')
const outPath = path.join(DIST_PATH, BUILD_NAME + '-win' + archStr + '.zip')
zip.zipSync(inPath, outPath)
console.log('Windows: Created portable app.')
console.log(`Windows: Created ${destArch} portable app.`)
cb(null)
}
}).catch(function (err) {
cb(err)
})
}
function buildLinux (cb) {
console.log('Linux: Packaging electron...')
electronPackager(Object.assign({}, all, linux)).then(function (buildPath) {
electronPackager(Object.assign({}, all, linux), function (err, buildPath) {
if (err) return cb(err)
console.log('Linux: Packaged electron. ' + buildPath)
const tasks = []
@@ -489,92 +533,56 @@ function buildLinux (cb) {
if (argv.package === 'deb' || argv.package === 'all') {
tasks.push((cb) => packageDeb(filesPath, destArch, cb))
}
if (argv.package === 'rpm' || argv.package === 'all') {
tasks.push((cb) => packageRpm(filesPath, destArch, cb))
}
if (argv.package === 'zip' || argv.package === 'all') {
tasks.push((cb) => packageZip(filesPath, destArch, cb))
}
})
series(tasks, cb)
}).catch(function (err) {
cb(err)
})
function packageDeb (filesPath, destArch, cb) {
// Linux convention for Debian based 'x64' is 'amd64'
if (destArch === 'x64') {
destArch = 'amd64'
}
// Create .deb file for Debian-based platforms
console.log(`Linux: Creating ${destArch} deb...`)
const installer = require('electron-installer-debian')
const deb = require('nobin-debian-installer')()
const destPath = path.join('/opt', pkg.name)
const options = {
src: filesPath + '/',
dest: DIST_PATH,
arch: destArch,
bin: 'WebTorrent',
icon: {
'48x48': path.join(config.STATIC_PATH, 'linux/share/icons/hicolor/48x48/apps/webtorrent-desktop.png'),
'256x256': path.join(config.STATIC_PATH, 'linux/share/icons/hicolor/256x256/apps/webtorrent-desktop.png')
},
categories: ['Network', 'FileTransfer', 'P2P'],
mimeType: ['application/x-bittorrent', 'x-scheme-handler/magnet', 'x-scheme-handler/stream-magnet'],
desktopTemplate: path.join(config.STATIC_PATH, 'linux/webtorrent-desktop.ejs')
}
installer(options).then(
() => {
console.log(`Linux: Created ${destArch} deb.`)
cb(null)
},
(err) => cb(err)
)
}
function packageRpm (filesPath, destArch, cb) {
// Linux convention for RedHat based 'x64' is 'x86_64'
if (destArch === 'x64') {
destArch = 'x86_64'
}
// Create .rpm file for RedHat-based platforms
console.log(`Linux: Creating ${destArch} rpm...`)
const installer = require('electron-installer-redhat')
const options = {
src: filesPath + '/',
dest: DIST_PATH,
arch: destArch,
bin: 'WebTorrent',
icon: {
'48x48': path.join(config.STATIC_PATH, 'linux/share/icons/hicolor/48x48/apps/webtorrent-desktop.png'),
'256x256': path.join(config.STATIC_PATH, 'linux/share/icons/hicolor/256x256/apps/webtorrent-desktop.png')
},
categories: ['Network', 'FileTransfer', 'P2P'],
mimeType: ['application/x-bittorrent', 'x-scheme-handler/magnet', 'x-scheme-handler/stream-magnet'],
desktopTemplate: path.join(config.STATIC_PATH, 'linux/webtorrent-desktop.ejs')
}
installer(options).then(
() => {
console.log(`Linux: Created ${destArch} rpm.`)
cb(null)
},
(err) => cb(err)
)
deb.pack({
package: pkg,
info: {
arch: destArch === 'x64' ? 'amd64' : 'i386',
targetDir: DIST_PATH,
depends: 'gconf2, libgtk2.0-0, libnss3, libxss1',
scripts: {
postinst: path.join(config.STATIC_PATH, 'linux', 'postinst'),
prerm: path.join(config.STATIC_PATH, 'linux', 'prerm')
}
}
}, [{
src: ['./**'],
dest: destPath,
expand: true,
cwd: filesPath
}, {
src: ['./**'],
dest: path.join('/usr', 'share'),
expand: true,
cwd: path.join(config.STATIC_PATH, 'linux', 'share')
}], function (err) {
if (err) return cb(err)
console.log(`Linux: Created ${destArch} deb.`)
cb(null)
})
}
function packageZip (filesPath, destArch, 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-${destArch}.zip`)
const outPath = path.join(DIST_PATH, BUILD_NAME + '-linux' + archStr + '.zip')
zip.zipSync(inPath, outPath)
console.log(`Linux: Created ${destArch} zip.`)

View File

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

10922
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{
"name": "webtorrent-desktop",
"description": "WebTorrent, the streaming torrent client. For Mac, Windows, and Linux.",
"version": "0.23.0",
"version": "0.18.0",
"author": {
"name": "WebTorrent, LLC",
"email": "feross@webtorrent.io",
@@ -11,61 +11,68 @@
"url": "https://github.com/webtorrent/webtorrent-desktop/issues"
},
"dependencies": {
"airplayer": "github:webtorrent/airplayer#fix-security",
"application-config": "^2.0.0",
"arch": "^2.1.2",
"auto-launch": "^5.0.5",
"bitfield": "^3.0.0",
"capture-frame": "^3.0.2",
"chokidar": "^3.4.1",
"chromecasts": "^1.9.1",
"create-torrent": "^4.4.2",
"debounce": "^1.2.0",
"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",
"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": "^6.0.2",
"es6-error": "^4.1.1",
"drag-drop": "^2.12.1",
"es6-error": "^4.0.0",
"fn-getter": "^1.0.0",
"iso-639-1": "^2.1.3",
"languagedetect": "^2.0.0",
"location-history": "^1.1.2",
"material-ui": "^0.20.2",
"music-metadata": "6.3.6",
"network-address": "^1.1.2",
"parse-torrent": "^7.1.3",
"prettier-bytes": "^1.0.4",
"prop-types": "^15.7.2",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"rimraf": "^3.0.2",
"run-parallel": "^1.1.9",
"semver": "^7.3.2",
"gaze": "^1.1.2",
"iso-639-1": "^1.2.1",
"languagedetect": "^1.1.1",
"location-history": "^1.0.0",
"material-ui": "^0.17.0",
"mkdirp": "^0.5.1",
"ms": "^0.7.2",
"musicmetadata": "^2.0.2",
"network-address": "^1.1.0",
"node-notifier": "^5.0.2",
"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",
"run-parallel": "^1.1.6",
"semver": "^5.1.0",
"shell-env": "^0.3.0",
"simple-concat": "^1.0.0",
"simple-get": "^4.0.0",
"srt-to-vtt": "^1.1.3",
"vlc-command": "^1.2.0",
"webtorrent": ">=0.108.6",
"winreg": "^1.2.4"
"simple-get": "^2.0.0",
"srt-to-vtt": "^1.1.1",
"vlc-command": "^1.0.1",
"webtorrent": "0.x",
"winreg": "^1.2.0",
"zero-fill": "^2.2.3"
},
"devDependencies": {
"babel-eslint": "^10.1.0",
"buble": "^0.20.0",
"cross-zip": "^3.1.0",
"depcheck": "^1.0.0",
"electron": "~9.1.0",
"electron-osx-sign": "^0.4.17",
"electron-packager": "^15.0.0",
"electron-winstaller": "^4.0.1",
"gh-release": "^3.5.0",
"minimist": "^1.2.5",
"nodemon": "^2.0.4",
"open": "^7.0.4",
"plist": "^3.0.1",
"pngjs": "^5.0.0",
"run-series": "^1.1.8",
"spectron": "~11.0.0",
"buble": "^0.15.2",
"cross-zip": "^2.0.1",
"depcheck": "^0.6.4",
"electron": "1.6.0",
"electron-osx-sign": "0.4.3",
"electron-packager": "~8.5.1",
"electron-winstaller": "~2.5.2",
"gh-release": "^2.0.3",
"minimist": "^1.2.0",
"nobin-debian-installer": "0.0.10",
"nodemon": "^1.10.2",
"open": "0.0.5",
"plist": "^2.0.1",
"pngjs": "^3.0.0",
"run-series": "^1.1.4",
"spectron": "^3.3.0",
"standard": "*",
"tape": "^5.0.1",
"walk-sync": "^2.2.0"
"tape": "^4.6.0",
"walk-sync": "^0.3.1"
},
"engines": {
"node": ">=4.0.0"
@@ -84,9 +91,7 @@
"license": "MIT",
"main": "index.js",
"optionalDependencies": {
"appdmg": "^0.6.0",
"electron-installer-debian": "^3.1.0",
"electron-installer-redhat": "^3.1.0"
"appdmg": "^0.4.3"
},
"private": true,
"productName": "WebTorrent",
@@ -95,19 +100,16 @@
"url": "git://github.com/webtorrent/webtorrent-desktop.git"
},
"scripts": {
"build": "buble src --target chrome:71 --output build",
"build": "buble src --output build",
"clean": "node ./bin/clean.js",
"gh-release": "gh-release",
"install-system-deps": "brew install fakeroot dpkg rpm",
"open-config": "node ./bin/open-config.js",
"package": "node ./bin/package.js",
"start": "npm run build && electron --no-sandbox .",
"test": "standard && depcheck --ignores=standard,babel-eslint --ignore-dirs=build,dist && node ./bin/extra-lint.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",
"update-authors": "./bin/update-authors.sh",
"watch": "nodemon --exec \"npm run start\" --ext js,css --ignore build/ --ignore dist/"
},
"standard": {
"parser": "babel-eslint"
}
}

View File

@@ -1,7 +1,9 @@
const appConfig = require('application-config')('WebTorrent')
const fs = require('fs')
const path = require('path')
const electron = require('electron')
const arch = require('arch')
const gaze = require('gaze')
const APP_NAME = 'WebTorrent'
const APP_TEAM = 'WebTorrent, LLC'
@@ -17,19 +19,19 @@ const IS_PORTABLE = isPortable()
const UI_HEADER_HEIGHT = 38
const UI_TORRENT_HEIGHT = 100
module.exports = {
const exports = module.exports = {
ANNOUNCEMENT_URL: 'https://webtorrent.io/desktop/announcement',
AUTO_UPDATE_URL: 'https://webtorrent.io/desktop/update',
CRASH_REPORT_URL: 'https://webtorrent.io/desktop/crash-report',
TELEMETRY_URL: 'https://webtorrent.io/desktop/telemetry',
APP_COPYRIGHT: `Copyright © 2014-${new Date().getFullYear()} ${APP_TEAM}`,
APP_COPYRIGHT: 'Copyright © 2014-2017 ' + APP_TEAM,
APP_FILE_ICON: path.join(__dirname, '..', 'static', 'WebTorrentFile'),
APP_ICON: path.join(__dirname, '..', 'static', 'WebTorrent'),
APP_NAME,
APP_TEAM,
APP_VERSION,
APP_WINDOW_TITLE: APP_NAME,
APP_NAME: APP_NAME,
APP_TEAM: APP_TEAM,
APP_VERSION: APP_VERSION,
APP_WINDOW_TITLE: APP_NAME + ' (BETA)',
CONFIG_PATH: getConfigPath(),
@@ -73,14 +75,12 @@ module.exports = {
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_RELEASES: 'https://github.com/webtorrent/webtorrent-desktop/releases',
HOME_PAGE_URL: 'https://webtorrent.io',
TWITTER_PAGE_URL: 'https://twitter.com/WebTorrentApp',
IS_PORTABLE,
IS_PRODUCTION,
IS_TEST,
IS_PORTABLE: IS_PORTABLE,
IS_PRODUCTION: IS_PRODUCTION,
IS_TEST: IS_TEST,
OS_SYSARCH: arch() === 'x64' ? 'x64' : 'ia32',
@@ -100,8 +100,64 @@ module.exports = {
WINDOW_MIN_HEIGHT: UI_HEADER_HEIGHT + (UI_TORRENT_HEIGHT * 2), // header + 2 torrents
WINDOW_MIN_WIDTH: 425,
UI_HEADER_HEIGHT,
UI_TORRENT_HEIGHT
UI_HEADER_HEIGHT: UI_HEADER_HEIGHT,
UI_TORRENT_HEIGHT: UI_TORRENT_HEIGHT
}
const configFile = appConfig.filePath
let config = getConfig()
const watchers = []
function updateConfig () {
config = JSON.parse(fs.readFileSync(configFile))
}
function watch () {
gaze(configFile, function (err) {
if (err) {
throw err
}
this.on('changed', () => {
try {
updateConfig()
console.log('WebTorrent configuration reloaded!')
watchers.forEach(fn => fn())
} catch (err) {
// TODO: display notification
console.log(`An error occurred loading your configuration (${configFile}): ${err.message}`)
}
})
this.on('error', () => {
// Ignore file watching errors
})
})
}
// start watching for config changes
watch()
exports.subscribe = function (fn) {
watchers.push(fn)
return () => {
watchers.splice(watchers.indexOf(fn), 1)
}
}
function getPlugins () {
return config.plugins || {}
}
exports.getPlugins = getPlugins
exports.getConfigPath = getConfigPath
exports.getConfig = getConfig
function getConfig () {
const config = {}
try {
return require(configFile)
} catch (e) {
return config
}
}
function getConfigPath () {
@@ -147,8 +203,6 @@ function isPortable () {
return false
}
const fs = require('fs')
try {
// This line throws if the "Portable Settings" folder does not exist, and does
// nothing otherwise.

View File

@@ -7,8 +7,9 @@ const electron = require('electron')
const config = require('../config')
const log = require('./log')
const ANNOUNCEMENT_URL =
`${config.ANNOUNCEMENT_URL}?version=${config.APP_VERSION}&platform=${process.platform}`
const ANNOUNCEMENT_URL = config.ANNOUNCEMENT_URL +
'?version=' + config.APP_VERSION +
'&platform=' + process.platform
/**
* In certain situations, the WebTorrent team may need to show an announcement to
@@ -50,5 +51,7 @@ function onResponse (err, res, data) {
title: data.title,
message: data.message,
detail: data.detail
})
}, noop)
}
function noop () {}

View File

@@ -19,7 +19,7 @@ function openSeedFile () {
log('openSeedFile')
const opts = {
title: 'Select a file for the torrent.',
properties: ['openFile']
properties: [ 'openFile' ]
}
showOpenSeed(opts)
}
@@ -35,11 +35,11 @@ function openSeedDirectory () {
const opts = process.platform === 'darwin'
? {
title: 'Select a file or folder for the torrent.',
properties: ['openFile', 'openDirectory']
properties: [ 'openFile', 'openDirectory' ]
}
: {
title: 'Select a folder for the torrent.',
properties: ['openDirectory']
properties: [ 'openDirectory' ]
}
showOpenSeed(opts)
}
@@ -54,17 +54,18 @@ function openFiles () {
const opts = process.platform === 'darwin'
? {
title: 'Select a file or folder to add.',
properties: ['openFile', 'openDirectory']
properties: [ 'openFile', 'openDirectory' ]
}
: {
title: 'Select a file to add.',
properties: ['openFile']
properties: [ 'openFile' ]
}
setTitle(opts.title)
const selectedPaths = electron.dialog.showOpenDialogSync(windows.main.win, opts)
resetTitle()
if (!Array.isArray(selectedPaths)) return
windows.main.dispatch('onOpen', selectedPaths)
electron.dialog.showOpenDialog(windows.main.win, opts, function (selectedPaths) {
resetTitle()
if (!Array.isArray(selectedPaths)) return
windows.main.dispatch('onOpen', selectedPaths)
})
}
/*
@@ -76,14 +77,15 @@ function openTorrentFile () {
const opts = {
title: 'Select a .torrent file.',
filters: [{ name: 'Torrent Files', extensions: ['torrent'] }],
properties: ['openFile', 'multiSelections']
properties: [ 'openFile', 'multiSelections' ]
}
setTitle(opts.title)
const selectedPaths = electron.dialog.showOpenDialogSync(windows.main.win, opts)
resetTitle()
if (!Array.isArray(selectedPaths)) return
selectedPaths.forEach(function (selectedPath) {
windows.main.dispatch('addTorrent', selectedPath)
electron.dialog.showOpenDialog(windows.main.win, opts, function (selectedPaths) {
resetTitle()
if (!Array.isArray(selectedPaths)) return
selectedPaths.forEach(function (selectedPath) {
windows.main.dispatch('addTorrent', selectedPath)
})
})
}
@@ -114,8 +116,9 @@ function resetTitle () {
*/
function showOpenSeed (opts) {
setTitle(opts.title)
const selectedPaths = electron.dialog.showOpenDialogSync(windows.main.win, opts)
resetTitle()
if (!Array.isArray(selectedPaths)) return
windows.main.dispatch('showCreateTorrent', selectedPaths)
electron.dialog.showOpenDialog(windows.main.win, opts, function (selectedPaths) {
resetTitle()
if (!Array.isArray(selectedPaths)) return
windows.main.dispatch('showCreateTorrent', selectedPaths)
})
}

View File

@@ -4,7 +4,7 @@ module.exports = {
setBadge
}
const { app, Menu } = require('electron')
const {app, Menu} = require('electron')
const dialog = require('./dialog')
const log = require('./log')
@@ -34,7 +34,7 @@ function setBadge (count) {
if (process.platform === 'darwin' ||
(process.platform === 'linux' && app.isUnityRunning())) {
log(`setBadge: ${count}`)
app.badgeCount = Number(count)
app.setBadgeCount(Number(count))
}
}

View File

@@ -25,10 +25,11 @@ function spawn (playerPath, url, title) {
if (playerPath != null) return spawnExternal(playerPath, [url])
// Try to find and use VLC if external player is not specified
vlcCommand((err, vlcPath) => {
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
@@ -39,26 +40,26 @@ function spawn (playerPath, url, title) {
function kill () {
if (!proc) return
log(`Killing external player, pid ${proc.pid}`)
log('Killing external player, pid ' + proc.pid)
proc.kill('SIGKILL') // kill -9
proc = null
}
function spawnExternal (playerPath, args) {
log('Running external media player:', `${playerPath} ${args.join(' ')}`)
log('Running external media player:', playerPath + ' ' + args.join(' '))
if (process.platform === 'darwin' && path.extname(playerPath) === '.app') {
// Mac: Use executable in packaged .app bundle
playerPath += `/Contents/MacOS/${path.basename(playerPath, '.app')}`
playerPath += '/Contents/MacOS/' + path.basename(playerPath, '.app')
}
proc = cp.spawn(playerPath, args, { stdio: 'ignore' })
proc = cp.spawn(playerPath, args, {stdio: 'ignore'})
// If it works, close the modal after a second
const closeModalTimeout = setTimeout(() =>
windows.main.dispatch('exitModal'), 1000)
proc.on('close', code => {
proc.on('close', function (code) {
clearTimeout(closeModalTimeout)
if (!proc) return // Killed
log('External player exited with code ', code)
@@ -70,7 +71,7 @@ function spawnExternal (playerPath, args) {
proc = null
})
proc.on('error', err => {
log('External player error', err)
proc.on('error', function (e) {
log('External player error', e)
})
}

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

@@ -7,20 +7,26 @@ const config = require('../config')
const path = require('path')
function install () {
switch (process.platform) {
case 'darwin': installDarwin()
break
case 'win32': installWin32()
break
if (process.platform === 'darwin') {
installDarwin()
}
if (process.platform === 'win32') {
installWin32()
}
if (process.platform === 'linux') {
installLinux()
}
}
function uninstall () {
switch (process.platform) {
case 'darwin': uninstallDarwin()
break
case 'win32': uninstallWin32()
break
if (process.platform === 'darwin') {
uninstallDarwin()
}
if (process.platform === 'win32') {
uninstallWin32()
}
if (process.platform === 'linux') {
uninstallLinux()
}
}
@@ -38,7 +44,7 @@ function installDarwin () {
function uninstallDarwin () {}
const EXEC_COMMAND = [process.execPath, '--']
const EXEC_COMMAND = [ process.execPath ]
if (!config.IS_PRODUCTION) {
EXEC_COMMAND.push(config.ROOT_PATH)
@@ -102,37 +108,37 @@ function installWin32 () {
setProtocol()
function setProtocol (err) {
if (err) return log.error(err.message)
if (err) log.error(err.message)
protocolKey.set('', Registry.REG_SZ, name, setURLProtocol)
}
function setURLProtocol (err) {
if (err) return log.error(err.message)
if (err) log.error(err.message)
protocolKey.set('URL Protocol', Registry.REG_SZ, '', setIcon)
}
function setIcon (err) {
if (err) return log.error(err.message)
if (err) log.error(err.message)
const iconKey = new Registry({
hive: Registry.HKCU,
key: `\\Software\\Classes\\${protocol}\\DefaultIcon`
key: '\\Software\\Classes\\' + protocol + '\\DefaultIcon'
})
iconKey.set('', Registry.REG_SZ, icon, setCommand)
}
function setCommand (err) {
if (err) return log.error(err.message)
if (err) log.error(err.message)
const commandKey = new Registry({
hive: Registry.HKCU,
key: `\\Software\\Classes\\${protocol}\\shell\\open\\command`
key: '\\Software\\Classes\\' + protocol + '\\shell\\open\\command'
})
commandKey.set('', Registry.REG_SZ, `${commandToArgs(command)} "%1"`, done)
}
function done (err) {
if (err) return log.error(err.message)
if (err) log.error(err.message)
}
}
@@ -157,43 +163,43 @@ function installWin32 () {
function setExt () {
const extKey = new Registry({
hive: Registry.HKCU, // HKEY_CURRENT_USER
key: `\\Software\\Classes\\${ext}`
key: '\\Software\\Classes\\' + ext
})
extKey.set('', Registry.REG_SZ, id, setId)
}
function setId (err) {
if (err) return log.error(err.message)
if (err) log.error(err.message)
const idKey = new Registry({
hive: Registry.HKCU,
key: `\\Software\\Classes\\${id}`
key: '\\Software\\Classes\\' + id
})
idKey.set('', Registry.REG_SZ, name, setIcon)
}
function setIcon (err) {
if (err) return log.error(err.message)
if (err) log.error(err.message)
const iconKey = new Registry({
hive: Registry.HKCU,
key: `\\Software\\Classes\\${id}\\DefaultIcon`
key: '\\Software\\Classes\\' + id + '\\DefaultIcon'
})
iconKey.set('', Registry.REG_SZ, icon, setCommand)
}
function setCommand (err) {
if (err) return log.error(err.message)
if (err) log.error(err.message)
const commandKey = new Registry({
hive: Registry.HKCU,
key: `\\Software\\Classes\\${id}\\shell\\open\\command`
key: '\\Software\\Classes\\' + id + '\\shell\\open\\command'
})
commandKey.set('', Registry.REG_SZ, `${commandToArgs(command)} "%1"`, done)
}
function done (err) {
if (err) return log.error(err.message)
if (err) log.error(err.message)
}
}
}
@@ -211,9 +217,9 @@ function uninstallWin32 () {
function getCommand () {
const commandKey = new Registry({
hive: Registry.HKCU, // HKEY_CURRENT_USER
key: `\\Software\\Classes\\${protocol}\\shell\\open\\command`
key: '\\Software\\Classes\\' + protocol + '\\shell\\open\\command'
})
commandKey.get('', (err, item) => {
commandKey.get('', function (err, item) {
if (!err && item.value.indexOf(commandToArgs(command)) >= 0) {
destroyProtocol()
}
@@ -223,9 +229,9 @@ function uninstallWin32 () {
function destroyProtocol () {
const protocolKey = new Registry({
hive: Registry.HKCU,
key: `\\Software\\Classes\\${protocol}`
key: '\\Software\\Classes\\' + protocol
})
protocolKey.destroy(() => {})
protocolKey.destroy(function () {})
}
}
@@ -235,7 +241,7 @@ function uninstallWin32 () {
function eraseId () {
const idKey = new Registry({
hive: Registry.HKCU, // HKEY_CURRENT_USER
key: `\\Software\\Classes\\${id}`
key: '\\Software\\Classes\\' + id
})
idKey.destroy(getExt)
}
@@ -243,9 +249,9 @@ function uninstallWin32 () {
function getExt () {
const extKey = new Registry({
hive: Registry.HKCU,
key: `\\Software\\Classes\\${ext}`
key: '\\Software\\Classes\\' + ext
})
extKey.get('', (err, item) => {
extKey.get('', function (err, item) {
if (!err && item.value === id) {
destroyExt()
}
@@ -255,9 +261,9 @@ function uninstallWin32 () {
function destroyExt () {
const extKey = new Registry({
hive: Registry.HKCU, // HKEY_CURRENT_USER
key: `\\Software\\Classes\\${ext}`
key: '\\Software\\Classes\\' + ext
})
extKey.destroy(() => {})
extKey.destroy(function () {})
}
}
}
@@ -265,3 +271,99 @@ function uninstallWin32 () {
function commandToArgs (command) {
return command.map((arg) => `"${arg}"`).join(' ')
}
function installLinux () {
const fs = require('fs')
const os = require('os')
const path = require('path')
const config = require('../config')
const log = require('./log')
// Do not install in user dir if running on system
if (/^\/opt/.test(process.execPath)) return
installDesktopFile()
installIconFile()
function installDesktopFile () {
const templatePath = path.join(
config.STATIC_PATH, 'linux', 'webtorrent-desktop.desktop'
)
fs.readFile(templatePath, 'utf8', writeDesktopFile)
}
function writeDesktopFile (err, desktopFile) {
if (err) return log.error(err.message)
const appPath = config.IS_PRODUCTION
? path.dirname(process.execPath)
: config.ROOT_PATH
desktopFile = desktopFile.replace(/\$APP_NAME/g, config.APP_NAME)
desktopFile = desktopFile.replace(/\$APP_PATH/g, appPath)
desktopFile = desktopFile.replace(/\$EXEC_PATH/g, EXEC_COMMAND.join(' '))
desktopFile = desktopFile.replace(/\$TRY_EXEC_PATH/g, process.execPath)
const desktopFilePath = path.join(
os.homedir(),
'.local',
'share',
'applications',
'webtorrent-desktop.desktop'
)
fs.mkdirp(path.dirname(desktopFilePath))
fs.writeFile(desktopFilePath, desktopFile, function (err) {
if (err) return log.error(err.message)
})
}
function installIconFile () {
const 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(
os.homedir(),
'.local',
'share',
'icons',
'webtorrent-desktop.png'
)
mkdirp(path.dirname(iconFilePath), (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')
const desktopFilePath = path.join(
os.homedir(),
'.local',
'share',
'applications',
'webtorrent-desktop.desktop'
)
rimraf(desktopFilePath)
const iconFilePath = path.join(
os.homedir(),
'.local',
'share',
'icons',
'webtorrent-desktop.png'
)
rimraf(iconFilePath)
}

View File

@@ -12,15 +12,12 @@ const log = require('./log')
const menu = require('./menu')
const State = require('../renderer/lib/state')
const windows = require('./windows')
const WEBTORRENT_VERSION = require('webtorrent/package.json').version
const Plugins = require('./plugins')
const plugins = new Plugins()
let shouldQuit = false
let argv = sliceArgv(process.argv)
// allow electron/chromium to play startup sounds (without user interaction)
app.commandLine.appendSwitch('autoplay-policy', 'no-user-gesture-required')
// 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') ||
@@ -43,19 +40,17 @@ if (!shouldQuit && !config.IS_PORTABLE) {
// 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".
if (!app.requestSingleInstanceLock()) {
shouldQuit = true
shouldQuit = app.makeSingleInstance(onAppOpen)
if (shouldQuit) {
app.quit()
}
}
if (shouldQuit) {
app.quit()
} else {
if (!shouldQuit) {
init()
}
function init () {
app.on('second-instance', (event, commandLine, workingDirectory) => onAppOpen(commandLine))
if (config.IS_PORTABLE) {
const path = require('path')
// Put all user data into the "Portable Settings" folder
@@ -81,28 +76,46 @@ function init () {
isReady = true
const state = results.state
windows.main.init(state, { hidden })
windows.webtorrent.init()
menu.init()
// init new plugins then notify user
plugins.subscribe(() => {
// update menu and windows
// passing thru new plugin decorators
initApp(state)
})
plugins.init(state)
initApp(state)
// To keep app startup fast, some code is delayed.
setTimeout(() => {
delayedInit(state)
}, config.DELAYED_INIT)
setTimeout(delayedInit, config.DELAYED_INIT)
// Report uncaught exceptions
process.on('uncaughtException', (err) => {
console.error(err)
const error = { message: err.message, stack: err.stack }
const error = {message: err.message, stack: err.stack}
windows.main.dispatch('uncaughtError', 'main', error)
})
}
// Enable app logging into default directory, i.e. /Library/Logs/WebTorrent
// on Mac, %APPDATA% on Windows, $XDG_CONFIG_HOME or ~/.config on Linux.
app.setAppLogsPath()
function initApp (state) {
// decorate app
plugins.onApp(app)
app.userAgentFallback = `WebTorrent/${WEBTORRENT_VERSION} (https://webtorrent.io)`
// init decorate menu
menu.init((tpl) => plugins.decorateMenu(tpl))
// init and decorate window
windows.main.init(
state,
{hidden: hidden},
(options) => plugins.decorateWindow(options)
)
windows.webtorrent.init((options) => plugins.decorateWindow(options))
plugins.onWindow([
windows.main.win,
windows.webtorrent.win
])
}
app.on('open-file', onOpen)
app.on('open-url', onOpen)
@@ -137,24 +150,17 @@ function init () {
})
}
function delayedInit (state) {
function delayedInit () {
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 })
announcement.init()
dock.init()
updater.init()
ipc.setModule('folderWatcher', folderWatcher)
if (folderWatcher.isEnabled()) {
folderWatcher.start()
}
if (process.platform === 'win32') {
const userTasks = require('./user-tasks')
userTasks.init()
@@ -175,7 +181,7 @@ function onOpen (e, torrentId) {
// Electron issue: https://github.com/atom/electron/issues/4338
setTimeout(() => windows.main.show(), 100)
processArgv([torrentId])
processArgv([ torrentId ])
} else {
argv.push(torrentId)
}
@@ -201,11 +207,11 @@ function onAppOpen (newArgv) {
function sliceArgv (argv) {
return argv.slice(config.IS_PRODUCTION ? 1
: config.IS_TEST ? 4
: 2)
: 2)
}
function processArgv (argv) {
const torrentIds = []
let torrentIds = []
argv.forEach(function (arg) {
if (arg === '-n' || arg === '-o' || arg === '-u') {
// Critical path: Only load the 'dialog' package if it is needed

View File

@@ -1,6 +1,5 @@
module.exports = {
init,
setModule
init
}
const electron = require('electron')
@@ -14,14 +13,6 @@ const windows = require('./windows')
// 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
}
function init () {
const ipc = electron.ipcMain
@@ -67,7 +58,7 @@ function init () {
})
/**
* Player Events
* Events
*/
ipc.on('onPlayerOpen', function () {
@@ -115,35 +106,13 @@ function init () {
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('openPath', (e, ...args) => {
ipc.on('openItem', (e, ...args) => {
const shell = require('./shell')
shell.openPath(...args)
shell.openItem(...args)
})
ipc.on('showItemInFolder', (e, ...args) => {
const shell = require('./shell')
@@ -235,8 +204,8 @@ function init () {
} else {
// Queue message for webtorrent window, it hasn't finished loading yet
messageQueueMainToWebTorrent.push({
name,
args
name: name,
args: args
})
log('webtorrent: queueing %s', name)
}

View File

@@ -17,8 +17,11 @@ const windows = require('./windows')
let menu = null
function init () {
menu = electron.Menu.buildFromTemplate(getMenuTemplate())
function init (decorate) {
let template = getMenuTemplate()
if (decorate) template = decorate(template)
menu = electron.Menu.buildFromTemplate(template)
electron.Menu.setApplicationMenu(menu)
}
@@ -288,14 +291,6 @@ function getMenuTemplate () {
{
label: 'Resume All',
click: () => windows.main.dispatch('resumeAllTorrents')
},
{
label: 'Remove All From List',
click: () => windows.main.dispatch('confirmDeleteAllTorrents', false)
},
{
label: 'Remove All Data Files',
click: () => windows.main.dispatch('confirmDeleteAllTorrents', true)
}
]
},
@@ -310,13 +305,6 @@ function getMenuTemplate () {
shell.openExternal(config.HOME_PAGE_URL)
}
},
{
label: 'Release Notes',
click: () => {
const shell = require('./shell')
shell.openExternal(config.GITHUB_URL_RELEASES)
}
},
{
label: 'Contribute on GitHub',
click: () => {
@@ -333,13 +321,6 @@ function getMenuTemplate () {
const shell = require('./shell')
shell.openExternal(config.GITHUB_URL_ISSUES)
}
},
{
label: 'Follow us on Twitter',
click: () => {
const shell = require('./shell')
shell.openExternal(config.TWITTER_PAGE_URL)
}
}
]
}
@@ -365,7 +346,8 @@ function getMenuTemplate () {
type: 'separator'
},
{
role: 'services'
role: 'services',
submenu: []
},
{
type: 'separator'

424
src/main/plugins.js Normal file
View File

@@ -0,0 +1,424 @@
const {exec} = require('child_process')
const {resolve, basename} = require('path')
const {writeFileSync} = require('fs')
const State = require('../renderer/lib/state')
const notifier = require('node-notifier')
const {app} = require('electron')
const {sync: mkdirpSync} = require('mkdirp')
const ms = require('ms')
const shellEnv = require('shell-env')
const crypto = require('crypto')
const config = require('../config')
module.exports = class Plugins {
constructor () {
// modules path
this.path = resolve(config.getConfigPath(), 'plugins')
log('Path: ', this.path)
this.availableExtensions = new Set([
'onApp', 'onWindow', 'decorateMenu', 'decorateWindow', 'decorateConfig'
])
this.forceUpdate = false
this.updating = false
this.watchers = []
}
init (state) {
this.state = state
// initialize state
this.state.saved = Object.assign(this.state.saved || {})
// init plugin directories if not present
mkdirpSync(this.path)
// caches
this.plugins = config.getPlugins()
this.paths = this.getPaths(this.plugins)
this.id = this.getId(this.plugins)
this.modules = this.requirePlugins()
// we listen on configuration updates to trigger
// plugin installation
config.subscribe(() => {
const plugins = config.getPlugins()
if (plugins !== this.plugins) {
const id = this.getId(plugins)
if (this.id !== id) {
this.alert('Installing plugins...')
log('UPDATING...')
this.id = id
this.plugins = plugins
this.paths = this.getPaths(this.plugins)
this.updatePlugins()
}
}
})
// schedule the initial plugins update
// a bit after the user launches the app
// to prevent slowness
if (this.needsUpdate()) {
setTimeout(() => {
this.updatePlugins()
}, 5000)
log('Installation scheduled')
}
// update plugins every 5 hours
setInterval(() => {
this.updatePlugins()
}, ms('5h'))
}
on (action, params) {
log(`ON ${action}:`, params)
this.modules.forEach(plugin => {
const actionName = this.capitalizeFirstLetter(action)
const methodName = `on${actionName}`
if (typeof plugin[methodName] === 'function') {
plugin[methodName](params)
}
})
}
capitalizeFirstLetter (string) {
return string.charAt(0).toUpperCase() + string.slice(1)
}
didPluginsChange () {
return this.state.saved.installedPlugins !== this.id
}
hasPlugins () {
return !this.isEmptyObject(this.plugins)
}
isFirstInstall () {
return (!this.state.saved.installedPlugins && this.hasPlugins())
}
needsUpdate () {
return (this.didPluginsChange() || this.isFirstInstall())
}
getId (plugins) {
const hash = crypto.createHash('sha256')
hash.update(JSON.stringify(plugins))
return hash.digest('hex')
}
updatePlugins (forceUpdate = false) {
this.forceUpdate = forceUpdate
if (this.updating) {
// TODO
// return notify('Plugin update in progress')
}
this.updating = true
this.syncPackageJSON()
this.installPackages((err) => this.loadPlugins(err))
}
loadPlugins (err, localOnly = false) {
this.updating = false
// handle errors first
if (err) {
console.error(err.stack)
if (/not a recognized/.test(err.message) || /command not found/.test(err.message)) {
this.alert(
'Error updating plugins: We could not find the "npm" command. Make sure it\'s in $PATH'
)
return
}
this.alert(`Error updating plugins: Check '${this.path}/npm-debug.log' for more information.`)
return
}
// update state with latest plugins
this.state.saved.plugins = this.plugins
// cache modules
this.modules = this.requirePlugins()
// clear require cache
this.clearCache()
// we're done with local plugins
if (localOnly) return
// OK, no errors
// flag successful plugin update
this.state.saved.installedPlugins = this.id
// check if package based plugins were updated
const loaded = this.modules.length
const total = this.paths.plugins.length
const pluginVersions = JSON.stringify(this.getPluginVersions())
const changed = this.state.saved.installedPluginVersions !== pluginVersions && loaded === total
this.state.saved.installedPluginVersions = pluginVersions
// notify watchers
if (this.forceUpdate || changed) {
this.watchers.forEach(fn => fn(err, {forceUpdate: this.forceUpdate}))
this.alert('Installation completed')
log('Installation completed')
}
// save state
State.save(this.state)
}
getPluginVersions () {
const paths_ = this.paths.plugins
return paths_.map(path => {
let version = null
try {
// eslint-disable-next-line import/no-dynamic-require
version = require(resolve(path, 'package.json')).version
} catch (err) { }
return [
basename(path),
version
]
})
}
clearCache () {
// trigger unload hooks
this.modules.forEach(mod => {
if (mod.onUnload) {
mod.onUnload(app)
}
})
// clear require cache
for (const entry in require.cache) {
if (entry.indexOf(this.path) === 0) {
delete require.cache[entry]
}
}
}
isEmptyObject (obj) {
return (Object.keys(obj).length === 0)
}
syncPackageJSON () {
const dependencies = this.toDependencies(this.plugins)
const pkg = {
name: 'webtorrent-plugins',
description: 'Auto-generated from WebTorrent config.',
private: true,
version: '0.0.1',
repository: 'feross/webtorrent-desktop',
license: 'MIT',
homepage: 'https://webtorrent.io',
dependencies
}
const file = resolve(this.path, 'package.json')
try {
writeFileSync(file, JSON.stringify(pkg, null, 2))
return true
} catch (err) {
this.alert(`An error occurred writing to ${file}`)
}
}
alert (message) {
notifier.notify({
title: 'WebTorrent Plugins',
// icon: config.icon, // TODO: save icon in webtorrent local folder and set config.icon
message: message
})
}
isLocalPath (string) {
// matches unix and windows local paths
return string.match(/^(\/|[a-z]:\/)/i)
}
toDependencies (plugins) {
const obj = {}
const pluginNames = Object.keys(plugins)
pluginNames.forEach(name => {
let url = plugins[name]
if (this.isLocalPath(url)) return
obj[name] = url
})
return obj
}
installPackages (fn) {
const {shell = '', npmRegistry} = config
shellEnv(shell).then(env => {
if (npmRegistry) {
env.NPM_CONFIG_REGISTRY = npmRegistry
}
/* eslint-disable camelcase */
env.npm_config_runtime = 'electron'
env.npm_config_target = process.versions.electron
env.npm_config_disturl = 'https://atom.io/download/atom-shell'
/* eslint-enable camelcase */
// Shell-specific installation commands
const installCommands = {
fish: 'npm prune; and npm install --production',
posix: 'npm prune && npm install --production'
}
// determine the shell we're running in
const whichShell = shell.match(/fish/) ? 'fish' : 'posix'
// Use the install command that is appropriate for our shell
exec(installCommands[whichShell], {
cwd: this.path
}, err => {
if (err) return fn(err)
fn(null)
})
}).catch(fn)
}
subscribe (fn) {
this.watchers.push(fn)
return () => {
this.watchers.splice(this.watchers.indexOf(fn), 1)
}
}
getPaths (plugins) {
const pluginNames = Object.keys(plugins)
return {
plugins: pluginNames.map(name => {
let url = plugins[name]
// plugin is already on a local folder
// directly load it from its current location
if (this.isLocalPath(url)) return url
// plugin will be installed with npm install from a remote url
return resolve(this.path, 'node_modules', name.split('#')[0])
})
}
}
requirePlugins () {
const {plugins} = this.paths
let installNeeded = false
const load = (path) => {
if (!path.match(/\/$/)) {
path += '/'
}
const mainPath = `${path}main.js`
try {
const Plugin = require(mainPath) // eslint-disable import/no-dynamic-require
const plugin = new Plugin()
const exposed = plugin && Object.keys(plugin).some(key => this.availableExtensions.has(key))
if (!exposed) return
// populate the name for internal errors here
plugin._name = basename(mainPath)
return plugin
} catch (err) {
log('Require plugins ERROR:', err)
this.alert(`Error loading plugin: ${mainPath}`)
// plugin not installed
// node_modules removed? did a manual plugin uninstall?
// try installing and then loading if successfull
installNeeded = true
}
}
// Plugin installation happens on the MAIN process.
// If plugins haven't finished installing, wait for them.
if (installNeeded) {
log('Plugins install needed, wait...')
setTimeout(() => {
this.requirePlugins()
}, 3000)
}
return plugins.map(load).filter(v => Boolean(v))
}
onApp (app) {
this.modules.forEach(plugin => {
if (plugin.onApp) {
plugin.onApp(app)
}
})
}
onWindow (win) {
this.modules.forEach(plugin => {
if (plugin.onWindow) {
plugin.onWindow(win)
}
})
}
// decorates the base object by calling plugin[key]
// for all the available plugins
decorateObject (base, key) {
let decorated = base
this.modules.forEach(plugin => {
if (plugin[key]) {
const res = plugin[key](decorated)
if (res && typeof res === 'object') {
decorated = res
} else {
this.alert(`Plugin error: "${plugin._name}": invalid return type for \`${key}\``)
}
}
})
return decorated
}
decorateMenu (tpl) {
return this.decorateObject(tpl, 'decorateMenu')
}
decorateWindow (options) {
return this.decorateObject(options, 'decorateWindow')
}
getDecoratedEnv (baseEnv) {
return this.decorateObject(baseEnv, 'decorateEnv')
}
getDecoratedConfig () {
const baseConfig = config.getConfig()
return this.decorateObject(baseConfig, 'decorateConfig')
}
getDecoratedBrowserOptions (defaults) {
return this.decorateObject(defaults, 'decorateBrowserOptions')
}
}
/**
* Logs passed arguments to console using a prefix.
*
*/
function log () {
const prefix = '[ PLUGINS.Main ]-->'
const args = [prefix]
for (var i = 0; i < arguments.length; ++i) {
args.push(arguments[i])
}
console.log.apply(console, args)
}

View File

@@ -1,6 +1,6 @@
module.exports = {
openExternal,
openPath,
openItem,
showItemInFolder,
moveItemToTrash
}
@@ -19,9 +19,9 @@ function openExternal (url) {
/**
* Open the given file in the desktops default manner.
*/
function openPath (path) {
log(`openPath: ${path}`)
electron.shell.openPath(path)
function openItem (path) {
log(`openItem: ${path}`)
electron.shell.openItem(path)
}
/**

View File

@@ -3,11 +3,19 @@ module.exports = {
uninstall
}
const { APP_NAME } = require('../config')
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: APP_NAME,
name: config.APP_NAME,
path: appPath,
isHidden: true
})

View File

@@ -77,8 +77,8 @@ function onPlayerPlay () {
function onPlayerUpdate (state) {
if (!isEnabled()) return
buttons[PREV].flags = [state.hasPrevious ? 'enabled' : 'disabled']
buttons[NEXT].flags = [state.hasNext ? 'enabled' : 'disabled']
buttons[PREV].flags = [ state.hasPrevious ? 'enabled' : 'disabled' ]
buttons[NEXT].flags = [ state.hasNext ? 'enabled' : 'disabled' ]
update()
}

View File

@@ -72,6 +72,6 @@ function initDarwinWin32 () {
(e, notes, name, date, url) => log(`Update downloaded: ${name}: ${url}`)
)
electron.autoUpdater.setFeedURL({ url: AUTO_UPDATE_URL })
electron.autoUpdater.setFeedURL(AUTO_UPDATE_URL)
electron.autoUpdater.checkForUpdates()
}

View File

@@ -15,7 +15,7 @@ function init () {
backgroundColor: '#ECECEC',
center: true,
fullscreen: false,
height: 250,
height: 170,
icon: getIconPath(),
maximizable: false,
minimizable: false,
@@ -24,21 +24,16 @@ function init () {
skipTaskbar: true,
title: 'About ' + config.APP_WINDOW_TITLE,
useContentSize: true,
webPreferences: {
nodeIntegration: true,
enableBlinkFeatures: 'AudioVideoTracks'
},
width: 300
})
win.loadURL(config.WINDOW_ABOUT)
// No menu on the About window
win.setMenu(null)
win.once('ready-to-show', function () {
win.show()
// No menu on the About window
// Hack: BrowserWindow removeMenu method not working on electron@7
// https://github.com/electron/electron/issues/21088
win.setMenuBarVisibility(false)
})
win.once('closed', function () {

View File

@@ -23,15 +23,16 @@ const config = require('../../config')
const log = require('../log')
const menu = require('../menu')
function init (state, options) {
function init (state, options, decorate) {
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({
let windowOptions = {
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)
@@ -39,16 +40,15 @@ function init (state, options) {
minWidth: config.WINDOW_MIN_WIDTH,
show: false,
title: config.APP_WINDOW_TITLE,
titleBarStyle: 'hiddenInset', // Hide title bar (Mac)
titleBarStyle: 'hidden-inset', // Hide title bar (Mac)
useContentSize: true, // Specify web page size without OS chrome
width: initialBounds.width,
webPreferences: {
nodeIntegration: true,
enableBlinkFeatures: 'AudioVideoTracks'
},
x: initialBounds.x,
y: initialBounds.y
})
}
if (decorate) windowOptions = decorate(windowOptions)
const win = main.win = new electron.BrowserWindow(windowOptions)
win.loadURL(config.WINDOW_MAIN)
@@ -60,7 +60,7 @@ function init (state, options) {
win.setSheetOffset(config.UI_HEADER_HEIGHT)
}
win.webContents.on('dom-ready', () => {
win.webContents.on('dom-ready', function () {
menu.onToggleFullScreen(main.win.isFullScreen())
})
@@ -76,27 +76,27 @@ function init (state, options) {
win.on('hide', onWindowBlur)
win.on('show', onWindowFocus)
win.on('enter-full-screen', () => {
win.on('enter-full-screen', function () {
menu.onToggleFullScreen(true)
send('fullscreenChanged', true)
win.setMenuBarVisibility(false)
})
win.on('leave-full-screen', () => {
win.on('leave-full-screen', function () {
menu.onToggleFullScreen(false)
send('fullscreenChanged', false)
win.setMenuBarVisibility(true)
})
win.on('move', debounce(e => {
win.on('move', debounce(function (e) {
send('windowBoundsChanged', e.sender.getBounds())
}, 1000))
win.on('resize', debounce(e => {
win.on('resize', debounce(function (e) {
send('windowBoundsChanged', e.sender.getBounds())
}, 1000))
win.on('close', e => {
win.on('close', function (e) {
if (process.platform !== 'darwin') {
const tray = require('../tray')
if (!tray.hasTray()) {
@@ -141,29 +141,37 @@ function setAspectRatio (aspectRatio) {
function setBounds (bounds, maximize) {
// Do nothing in fullscreen
if (!main.win || main.win.isFullScreen()) {
log('setBounds: not setting bounds because already in full screen mode')
log('setBounds: not setting bounds because we\'re in full screen')
return
}
// Maximize or minimize, if the second argument is present
if (maximize === true && !main.win.isMaximized()) {
log('setBounds: maximizing')
main.win.maximize()
} else if (maximize === false && main.win.isMaximized()) {
log('setBounds: unmaximizing')
main.win.unmaximize()
let willBeMaximized
if (maximize === true) {
if (!main.win.isMaximized()) {
log('setBounds: maximizing')
main.win.maximize()
}
willBeMaximized = true
} else if (maximize === false) {
if (main.win.isMaximized()) {
log('setBounds: unmaximizing')
main.win.unmaximize()
}
willBeMaximized = false
} else {
willBeMaximized = main.win.isMaximized()
}
const willBeMaximized = typeof maximize === 'boolean' ? maximize : main.win.isMaximized()
// Assuming we're not maximized or maximizing, set the window size
if (!willBeMaximized) {
log(`setBounds: setting bounds to ${JSON.stringify(bounds)}`)
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))
log(`setBounds: centered to ${JSON.stringify(bounds)}`)
log('setBounds: centered to ' + JSON.stringify(bounds))
}
// Resize the window's content area (so window border doesn't need to be taken
// into account)
@@ -212,7 +220,7 @@ function toggleDevTools () {
if (main.win.webContents.isDevToolsOpened()) {
main.win.webContents.closeDevTools()
} else {
main.win.webContents.openDevTools({ mode: 'detach' })
main.win.webContents.openDevTools({ detach: true })
}
}

View File

@@ -10,9 +10,10 @@ const electron = require('electron')
const config = require('../../config')
function init () {
const win = webtorrent.win = new electron.BrowserWindow({
function init (decorate) {
let options = {
backgroundColor: '#1E1E1E',
backgroundThrottling: false, // do not throttle animations/timers when page is background
center: true,
fullscreen: false,
fullscreenable: false,
@@ -24,12 +25,10 @@ function init () {
skipTaskbar: true,
title: 'webtorrent-hidden-window',
useContentSize: true,
webPreferences: {
nodeIntegration: true,
enableBlinkFeatures: 'AudioVideoTracks'
},
width: 150
})
}
if (decorate) options = decorate(options)
const win = webtorrent.win = new electron.BrowserWindow(options)
win.loadURL(config.WINDOW_WEBTORRENT)
@@ -59,6 +58,6 @@ function toggleDevTools () {
webtorrent.win.webContents.closeDevTools()
webtorrent.win.hide()
} else {
webtorrent.win.webContents.openDevTools({ mode: 'detach' })
webtorrent.win.webContents.openDevTools({ detach: true })
}
}

View File

@@ -1,6 +1,6 @@
const React = require('react')
const { dispatcher } = require('../lib/dispatcher')
const {dispatcher} = require('../lib/dispatcher')
module.exports = class CreateTorrentErrorPage extends React.Component {
render () {

View File

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

View File

@@ -1,31 +1,27 @@
const React = require('react')
const { dispatcher } = require('../lib/dispatcher')
const {dispatcher} = require('../lib/dispatcher')
class Header extends React.Component {
render () {
const loc = this.props.state.location
return (
<div
className='header'
<div className='header'
onMouseMove={dispatcher('mediaMouseMoved')}
onMouseEnter={dispatcher('mediaControlsMouseEnter')}
onMouseLeave={dispatcher('mediaControlsMouseLeave')}
>
onMouseLeave={dispatcher('mediaControlsMouseLeave')}>
{this.getTitle()}
<div className='nav left float-left'>
<i
className={'icon back ' + (loc.hasBack() ? '' : 'disabled')}
title='Back'
onClick={dispatcher('back')}
>
onClick={dispatcher('back')}>
chevron_left
</i>
<i
className={'icon forward ' + (loc.hasForward() ? '' : 'disabled')}
title='Forward'
onClick={dispatcher('forward')}
>
onClick={dispatcher('forward')}>
chevron_right
</i>
</div>
@@ -49,8 +45,7 @@ class Header extends React.Component {
<i
className='icon add'
title='Add torrent'
onClick={dispatcher('openFiles')}
>
onClick={dispatcher('openFiles')}>
add
</i>
)

View File

@@ -1,12 +1,11 @@
const React = require('react')
const PropTypes = require('prop-types')
const colors = require('material-ui/styles/colors')
class Heading extends React.Component {
static get propTypes () {
return {
level: PropTypes.number
level: React.PropTypes.number
}
}

View File

@@ -5,22 +5,20 @@ 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
const {cancelText, onCancel, okText, onOK} = this.props
return (
<div className='float-right'>
<FlatButton
className='control cancel'
style={cancelStyle}
label={cancelText}
onClick={onCancel}
/>
onClick={onCancel} />
<RaisedButton
className='control ok'
primary
label={okText}
onClick={onOK}
autoFocus
/>
autoFocus />
</div>
)
}

View File

@@ -1,10 +1,8 @@
const React = require('react')
const TextField = require('material-ui/TextField').default
const { clipboard } = require('electron')
const ModalOKCancel = require('./modal-ok-cancel')
const { dispatch, dispatcher } = require('../lib/dispatcher')
const { isMagnetLink } = require('../lib/torrent-player')
const {dispatch, dispatcher} = require('../lib/dispatcher')
module.exports = class OpenTorrentAddressModal extends React.Component {
render () {
@@ -17,27 +15,19 @@ module.exports = class OpenTorrentAddressModal extends React.Component {
className='control'
ref={(c) => { this.torrentURL = c }}
fullWidth
onKeyDown={handleKeyDown.bind(this)}
/>
onKeyDown={handleKeyDown.bind(this)} />
</div>
<ModalOKCancel
cancelText='CANCEL'
onCancel={dispatcher('exitModal')}
okText='OK'
onOK={handleOK.bind(this)}
/>
onOK={handleOK.bind(this)} />
</div>
)
}
componentDidMount () {
this.torrentURL.input.focus()
const clipboardContent = clipboard.readText()
if (isMagnetLink(clipboardContent)) {
this.torrentURL.input.value = clipboardContent
this.torrentURL.input.select()
}
}
}

View File

@@ -1,9 +1,6 @@
const path = require('path')
const colors = require('material-ui/styles/colors')
const electron = require('electron')
const React = require('react')
const PropTypes = require('prop-types')
const remote = electron.remote
@@ -14,14 +11,15 @@ const TextField = require('material-ui/TextField').default
// Uses the system Open File dialog.
// You can't edit the text field directly.
class PathSelector extends React.Component {
static propTypes () {
static get propTypes () {
return {
className: PropTypes.string,
dialog: PropTypes.object,
id: PropTypes.string,
onChange: PropTypes.func,
title: PropTypes.string.isRequired,
value: PropTypes.string
className: React.PropTypes.string,
dialog: React.PropTypes.object,
displayValue: React.PropTypes.string,
id: React.PropTypes.string,
onChange: React.PropTypes.func,
title: React.PropTypes.string.isRequired,
value: React.PropTypes.string
}
}
@@ -32,13 +30,18 @@ class PathSelector extends React.Component {
handleClick () {
const opts = Object.assign({
defaultPath: path.dirname(this.props.value || ''),
properties: ['openFile', 'openDirectory']
defaultPath: this.props.value,
properties: [ 'openFile', 'openDirectory' ]
}, this.props.dialog)
const filenames = remote.dialog.showOpenDialogSync(remote.getCurrentWindow(), opts)
if (!Array.isArray(filenames)) return
this.props.onChange && this.props.onChange(filenames[0])
remote.dialog.showOpenDialog(
remote.getCurrentWindow(),
opts,
(filenames) => {
if (!Array.isArray(filenames)) return
this.props.onChange && this.props.onChange(filenames[0])
}
)
}
render () {
@@ -61,7 +64,7 @@ class PathSelector extends React.Component {
const textFieldStyle = {
flex: '1'
}
const text = this.props.value || ''
const text = this.props.displayValue || this.props.value
const buttonStyle = {
marginLeft: 10
}
@@ -71,14 +74,10 @@ class PathSelector extends React.Component {
<div className='label' style={labelStyle}>
{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} value={text}
inputStyle={textareaStyle} style={textFieldStyle} />
<RaisedButton className='control' label='Change' onClick={this.handleClick}
style={buttonStyle} />
</div>
)
}

View File

@@ -1,7 +1,7 @@
const React = require('react')
const ModalOKCancel = require('./modal-ok-cancel')
const { dispatch, dispatcher } = require('../lib/dispatcher')
const {dispatch, dispatcher} = require('../lib/dispatcher')
module.exports = class RemoveTorrentModal extends React.Component {
render () {
@@ -18,8 +18,7 @@ module.exports = class RemoveTorrentModal extends React.Component {
cancelText='CANCEL'
onCancel={dispatcher('exitModal')}
okText={buttonText}
onOK={handleRemove}
/>
onOK={handleRemove} />
</div>
)

View File

@@ -1,14 +1,13 @@
const React = require('react')
const PropTypes = require('prop-types')
const RaisedButton = require('material-ui/RaisedButton').default
class ShowMore extends React.Component {
static get propTypes () {
return {
defaultExpanded: PropTypes.bool,
hideLabel: PropTypes.string,
showLabel: PropTypes.string
defaultExpanded: React.PropTypes.bool,
hideLabel: React.PropTypes.string,
showLabel: React.PropTypes.string
}
}
@@ -45,8 +44,7 @@ class ShowMore extends React.Component {
<RaisedButton
className='control'
onClick={this.handleClick}
label={label}
/>
label={label} />
</div>
)
}

View File

@@ -2,7 +2,7 @@ const React = require('react')
const electron = require('electron')
const ModalOKCancel = require('./modal-ok-cancel')
const { dispatcher } = require('../lib/dispatcher')
const {dispatcher} = require('../lib/dispatcher')
module.exports = class UnsupportedMediaModal extends React.Component {
render () {
@@ -28,8 +28,7 @@ module.exports = class UnsupportedMediaModal extends React.Component {
cancelText='CANCEL'
onCancel={dispatcher('backToList')}
okText={actionText}
onOK={onAction}
/>
onOK={onAction} />
<p className='error-text'>{errorMessage}</p>
</div>
)

View File

@@ -2,7 +2,7 @@ const React = require('react')
const electron = require('electron')
const ModalOKCancel = require('./modal-ok-cancel')
const { dispatch } = require('../lib/dispatcher')
const {dispatch} = require('../lib/dispatcher')
module.exports = class UpdateAvailableModal extends React.Component {
render () {
@@ -18,8 +18,7 @@ module.exports = class UpdateAvailableModal extends React.Component {
cancelText='SKIP THIS RELEASE'
onCancel={handleSkip}
okText='SHOW DOWNLOAD PAGE'
onOK={handleShow}
/>
onOK={handleShow} />
</div>
)

View File

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

View File

@@ -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,4 +1,4 @@
const { ipcRenderer } = require('electron')
const {ipcRenderer} = require('electron')
const telemetry = require('../lib/telemetry')
const Playlist = require('../lib/playlist')
@@ -26,7 +26,7 @@ module.exports = class MediaController {
ipcRenderer.once('checkForExternalPlayer', function (e, isInstalled) {
state.modal = {
id: 'unsupported-media-modal',
error,
error: error,
externalPlayerInstalled: isInstalled
}
})

View File

@@ -2,9 +2,9 @@ const electron = require('electron')
const path = require('path')
const Cast = require('../lib/cast')
const { dispatch } = require('../lib/dispatcher')
const {dispatch} = require('../lib/dispatcher')
const telemetry = require('../lib/telemetry')
const { UnplayableFileError, UnplayableTorrentError } = require('../lib/errors')
const {UnplayableFileError, UnplayableTorrentError} = require('../lib/errors')
const sound = require('../lib/sound')
const TorrentPlayer = require('../lib/torrent-player')
const TorrentSummary = require('../lib/torrent-summary')
@@ -61,12 +61,12 @@ module.exports = class PlaybackController {
}
// Open a file in OS default app.
openPath (infoHash, index) {
openItem (infoHash, index) {
const torrentSummary = TorrentSummary.getByKey(this.state, infoHash)
const filePath = path.join(
torrentSummary.path,
torrentSummary.files[index].path)
ipcRenderer.send('openPath', filePath)
ipcRenderer.send('openItem', filePath)
}
// Toggle (play or pause) the currently playing media
@@ -102,10 +102,10 @@ module.exports = class PlaybackController {
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()
})
state.playing.infoHash, Playlist.getNextIndex(state), false, (err) => {
if (err) dispatch('error', err)
else this.play()
})
}
}
@@ -269,10 +269,9 @@ module.exports = class PlaybackController {
// update state
state.playing.infoHash = infoHash
state.playing.fileIndex = index
state.playing.fileName = fileSummary.name
state.playing.type = TorrentPlayer.isVideo(fileSummary) ? 'video'
: TorrentPlayer.isAudio(fileSummary) ? 'audio'
: 'other'
: 'other'
// pick up where we left off
let jumpToTime = 0
@@ -293,7 +292,7 @@ module.exports = class PlaybackController {
}
function getAudioMetadata () {
if (state.playing.type === 'audio') {
if (state.playing.type === 'audio' && !fileSummary.audioInfo) {
ipcRenderer.send('wt-get-audio-metadata', torrentSummary.infoHash, index)
}
}

View File

@@ -1,4 +1,4 @@
const { dispatch } = require('../lib/dispatcher')
const {dispatch} = require('../lib/dispatcher')
const ipcRenderer = require('electron').ipcRenderer
// Controls the Preferences screen
@@ -16,22 +16,45 @@ module.exports = class PrefsController {
setup: function (cb) {
// initialize preferences
state.window.title = 'Preferences'
state.unsaved = Object.assign(state.unsaved || {}, {
prefs: Object.assign({}, state.saved.prefs)
})
ipcRenderer.send('setAllowNav', false)
cb()
},
destroy: () => {
ipcRenderer.send('setAllowNav', true)
this.save()
}
})
}
// Updates a single property in the saved prefs
// For example: updatePreferences('isFileHandler', true)
// Updates a single property in the UNSAVED prefs
// For example: updatePreferences('foo.bar', 'baz')
// Call save() to save to config.json
update (property, value) {
if (property === 'isFileHandler') ipcRenderer.send('setDefaultFileHandler', value)
else if (property === 'startup') ipcRenderer.send('setStartup', 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]] = {}
}
obj = obj[path[i]]
}
obj[path[i]] = value
}
this.state.saved.prefs[property] = value
// All unsaved prefs take effect atomically, and are saved to config.json
save () {
const 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')
dispatch('checkDownloadPath')
}

View File

@@ -5,7 +5,7 @@ const parallel = require('run-parallel')
const remote = electron.remote
const { dispatch } = require('../lib/dispatcher')
const {dispatch} = require('../lib/dispatcher')
module.exports = class SubtitlesController {
constructor (state) {
@@ -13,13 +13,14 @@ module.exports = class SubtitlesController {
}
openSubtitles () {
const filenames = remote.dialog.showOpenDialogSync({
remote.dialog.showOpenDialog({
title: 'Select a subtitles file.',
filters: [{ name: 'Subtitles', extensions: ['vtt', 'srt'] }],
properties: ['openFile']
filters: [ { name: 'Subtitles', extensions: ['vtt', 'srt'] } ],
properties: [ 'openFile' ]
}, (filenames) => {
if (!Array.isArray(filenames)) return
this.addSubtitles(filenames, true)
})
if (!Array.isArray(filenames)) return
this.addSubtitles(filenames, true)
}
selectSubtitle (ix) {
@@ -109,7 +110,7 @@ function loadSubtitle (file, cb) {
buffer: 'data:text/vtt;base64,' + buf.toString('base64'),
language: langDetected,
label: langDetected,
filePath
filePath: filePath
}
cb(null, track)

View File

@@ -3,14 +3,14 @@ const ipcRenderer = require('electron').ipcRenderer
const TorrentSummary = require('../lib/torrent-summary')
const sound = require('../lib/sound')
const { dispatch } = require('../lib/dispatcher')
const {dispatch} = require('../lib/dispatcher')
module.exports = class TorrentController {
constructor (state) {
this.state = state
}
torrentParsed (torrentKey, infoHash, magnetURI) {
torrentInfoHash (torrentKey, infoHash) {
let torrentSummary = this.getTorrentSummary(torrentKey)
console.log('got infohash for %s torrent %s',
torrentSummary ? 'existing' : 'new', torrentKey)
@@ -25,7 +25,7 @@ module.exports = class TorrentController {
}
torrentSummary = {
torrentKey,
torrentKey: torrentKey,
status: 'new'
}
torrents.unshift(torrentSummary)
@@ -33,7 +33,6 @@ module.exports = class TorrentController {
}
torrentSummary.infoHash = infoHash
torrentSummary.magnetURI = magnetURI
dispatch('update')
}
@@ -63,6 +62,7 @@ module.exports = class TorrentController {
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

View File

@@ -2,8 +2,8 @@ const fs = require('fs')
const path = require('path')
const electron = require('electron')
const { dispatch } = require('../lib/dispatcher')
const { TorrentKeyNotFoundError } = require('../lib/errors')
const {dispatch} = require('../lib/dispatcher')
const {TorrentKeyNotFoundError} = require('../lib/errors')
const sound = require('../lib/sound')
const TorrentSummary = require('../lib/torrent-summary')
@@ -55,7 +55,7 @@ module.exports = class TorrentListController {
if (files.length === 0 || typeof files[0] !== 'string') {
this.state.location.go({
url: 'create-torrent',
files,
files: files,
setup: (cb) => {
this.state.window.title = 'Create New Torrent'
cb(null)
@@ -157,24 +157,23 @@ module.exports = class TorrentListController {
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
.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)
// Pause torrent without playing sounds.
this.pauseTorrent(torrent, false)
this.state.saved.torrentsToResume.push(torrent.infoHash)
})
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')
if (!this.state.saved.torrentsToResume || !this.state.saved.torrentsToResume.length) return
this.state.saved.torrentsToResume.map((infoHash) => {
this.toggleTorrent(infoHash)
})
@@ -201,38 +200,26 @@ module.exports = class TorrentListController {
}
}
confirmDeleteAllTorrents (deleteData) {
this.state.modal = {
id: 'delete-all-torrents-modal',
deleteData
}
}
// TODO: use torrentKey, not infoHash
deleteTorrent (infoHash, deleteData) {
ipcRenderer.send('wt-stop-torrenting', infoHash)
const index = this.state.saved.torrents.findIndex((x) => x.infoHash === infoHash)
if (index > -1) {
const summary = this.state.saved.torrents[index]
deleteTorrentFile(summary, deleteData)
// remove torrent and poster file
deleteFile(TorrentSummary.getTorrentPath(summary))
deleteFile(TorrentSummary.getPosterPath(summary))
// optionally delete the torrent data
if (deleteData) moveItemToTrash(summary)
// remove torrent from saved list
this.state.saved.torrents.splice(index, 1)
dispatch('stateSave')
// prevent user from going forward to a deleted torrent
this.state.location.clearForward('player')
sound.play('DELETE')
} else {
throw new TorrentKeyNotFoundError(infoHash)
}
}
deleteAllTorrents (deleteData) {
this.state.saved.torrents.forEach((summary) => deleteTorrentFile(summary, deleteData))
this.state.saved.torrents = []
dispatch('stateSave')
// prevent user from going forward to a deleted torrent
this.state.location.clearForward('player')
@@ -291,24 +278,14 @@ module.exports = class TorrentListController {
enabled: torrentSummary.torrentFileName != null
}))
menu.append(new electron.remote.MenuItem({
type: 'separator'
}))
const sortedByName = this.state.saved.prefs.sortByName
menu.append(new electron.remote.MenuItem({
label: `${sortedByName ? '✓ ' : ''}Sort by Name`,
click: () => dispatch('updatePreferences', 'sortByName', !sortedByName)
}))
menu.popup({ window: electron.remote.getCurrentWindow() })
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 TorrentKeyNotFoundError(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()
@@ -318,19 +295,18 @@ module.exports = class TorrentListController {
filters: [
{ name: 'Torrent Files', extensions: ['torrent'] },
{ name: 'All Files', extensions: ['*'] }
],
buttonLabel: 'Save'
]
}
const savePath = electron.remote.dialog.showSaveDialogSync(win, opts)
if (!savePath) return // They clicked Cancel
console.log('Saving torrent ' + torrentKey + ' to ' + savePath)
const torrentPath = TorrentSummary.getTorrentPath(torrentSummary)
fs.readFile(torrentPath, function (err, torrentFile) {
if (err) return dispatch('error', err)
fs.writeFile(savePath, torrentFile, function (err) {
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)
})
})
})
}
@@ -341,12 +317,12 @@ module.exports = class TorrentListController {
function findFilesRecursive (paths, cb_) {
if (paths.length > 1) {
let numComplete = 0
const ret = []
let ret = []
paths.forEach(function (path) {
findFilesRecursive([path], function (fileObjs) {
ret.push(...fileObjs)
if (++numComplete === paths.length) {
ret.sort((a, b) => a.path < b.path ? -1 : Number(a.path > b.path))
ret.sort((a, b) => a.path < b.path ? -1 : a.path > b.path)
cb_(ret)
}
})
@@ -394,14 +370,3 @@ function moveItemToTrash (torrentSummary) {
function showItemInFolder (torrentSummary) {
ipcRenderer.send('showItemInFolder', TorrentSummary.getFileOrFolder(torrentSummary))
}
function deleteTorrentFile (torrentSummary, deleteData) {
ipcRenderer.send('wt-stop-torrenting', torrentSummary.infoHash)
// remove torrent and poster file
deleteFile(TorrentSummary.getTorrentPath(torrentSummary))
deleteFile(TorrentSummary.getPosterPath(torrentSummary))
// optionally delete the torrent data
if (deleteData) moveItemToTrash(torrentSummary)
}

View File

@@ -1,4 +1,4 @@
const { dispatch } = require('../lib/dispatcher')
const {dispatch} = require('../lib/dispatcher')
// Controls the UI checking for new versions of the app, prompting install
module.exports = class UpdateController {
@@ -13,7 +13,7 @@ module.exports = class UpdateController {
console.log('new version skipped by user: v' + version)
return
}
this.state.modal = { id: 'update-available-modal', version }
this.state.modal = { id: 'update-available-modal', version: version }
}
// Don't show the modal again until the next version

View File

@@ -13,10 +13,8 @@ module.exports = {
setRate
}
const http = require('http')
const config = require('../../config')
const { CastingError } = require('./errors')
const {CastingError} = require('./errors')
// Lazy load these for a ~300ms improvement in startup time
let airplayer, chromecasts, dlnacasts
@@ -83,7 +81,7 @@ function testPlayer (type) {
}
function getDevices () {
return [{ name: type + '-1' }, { name: type + '-2' }]
return [{name: type + '-1'}, {name: type + '-2'}]
}
function open () {}
@@ -132,47 +130,22 @@ function chromecastPlayer () {
})
}
function serveSubtitles (callback) {
const subtitles = state.playing.subtitles
const selectedSubtitle = subtitles.tracks[subtitles.selectedIndex]
if (!selectedSubtitle) {
callback()
} else {
ret.subServer = http.createServer((req, res) => {
res.writeHead(200, {
'Content-Type': 'text/vtt;charset=utf-8',
'Access-Control-Allow-Origin': '*',
'Transfer-Encoding': 'chunked'
})
res.end(Buffer.from(selectedSubtitle.buffer.substr(21), 'base64'))
}).listen(0, function () {
const port = ret.subServer.address().port
const subtitlesUrl = 'http://' + state.server.networkAddress + ':' + port + '/'
callback(subtitlesUrl)
})
}
}
function open () {
const torrentSummary = state.saved.torrents.find((x) => x.infoHash === state.playing.infoHash)
serveSubtitles(function (subtitlesUrl) {
ret.device.play(state.server.networkURL + '/' + state.playing.fileIndex, {
type: 'video/mp4',
title: config.APP_NAME + ' - ' + torrentSummary.name,
subtitles: subtitlesUrl ? [subtitlesUrl] : [],
autoSubtitles: !!subtitlesUrl
}, function (err) {
if (err) {
state.playing.location = 'local'
state.errors.push({
time: new Date().getTime(),
message: 'Could not connect to Chromecast. ' + err.message
})
} else {
state.playing.location = 'chromecast'
}
update()
})
ret.device.play(state.server.networkURL + '/' + state.playing.fileIndex, {
type: 'video/mp4',
title: config.APP_NAME + ' - ' + torrentSummary.name
}, function (err) {
if (err) {
state.playing.location = 'local'
state.errors.push({
time: new Date().getTime(),
message: 'Could not connect to Chromecast. ' + err.message
})
} else {
state.playing.location = 'chromecast'
}
update()
})
}
@@ -186,9 +159,6 @@ function chromecastPlayer () {
function stop (callback) {
ret.device.stop(callback)
if (ret.subServer) {
ret.subServer.close()
}
}
function status () {
@@ -429,11 +399,11 @@ function toggleMenu (location) {
}
// Show a menu
state.devices.castMenu = { location, devices }
state.devices.castMenu = {location, devices}
}
function selectDevice (index) {
const { location, devices } = state.devices.castMenu
const {location, devices} = state.devices.castMenu
// Start casting
const player = getPlayer(location)

View File

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

View File

@@ -28,15 +28,13 @@ function run (state) {
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.21.0')) migrate_0_21_0(saved)
if (semver.lt(version, '0.22.0')) migrate_0_22_0(saved)
// Config is now on the new version
state.saved.version = config.APP_VERSION
}
function migrate_0_7_0 (saved) {
const { copyFileSync } = require('fs')
const cpFile = require('cp-file')
const path = require('path')
saved.torrents.forEach(function (ts) {
@@ -58,7 +56,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) copyFileSync(src, dst)
if (src !== dst) cpFile.sync(src, dst)
delete ts.torrentPath
ts.torrentFileName = infoHash + '.torrent'
@@ -73,7 +71,7 @@ function migrate_0_7_0 (saved) {
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) copyFileSync(src, dst)
if (src !== dst) cpFile.sync(src, dst)
delete ts.posterURL
ts.posterFileName = infoHash + extension
@@ -157,7 +155,7 @@ function migrate_0_17_2 (saved) {
// 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 { copyFileSync } = require('fs')
const cpFile = require('cp-file')
const rimraf = require('rimraf')
const OLD_NAME = 'The WIRED CD - Rip. Sample. Mash. Share.'
@@ -192,7 +190,7 @@ function migrate_0_17_2 (saved) {
ts.posterFileName = NEW_HASH + '.jpg'
rimraf.sync(path.join(config.TORRENT_PATH, ts.torrentFileName))
copyFileSync(
cpFile.sync(
path.join(config.STATIC_PATH, 'wiredCd.torrent'),
path.join(config.TORRENT_PATH, NEW_HASH + '.torrent')
)
@@ -208,16 +206,3 @@ function migrate_0_17_2 (saved) {
} catch (err) {}
}
}
function migrate_0_21_0 (saved) {
if (saved.prefs.soundNotifications == null) {
// The app used to always have sound notifications enabled
saved.prefs.soundNotifications = true
}
}
function migrate_0_22_0 (saved) {
if (saved.prefs.externalPlayerPath == null) {
saved.prefs.externalPlayerPath = ''
}
}

View File

@@ -38,8 +38,7 @@ function getPreviousIndex (state) {
function getCurrentLocalURL (state) {
return state.server
? state.server.localURL + '/' + state.playing.fileIndex + '/' +
encodeURIComponent(state.playing.fileName)
? state.server.localURL + '/' + state.playing.fileIndex
: ''
}

View File

@@ -1,17 +1,13 @@
module.exports = {
init,
play
}
const config = require('../../config')
const { InvalidSoundNameError } = require('./errors')
const {InvalidSoundNameError} = require('./errors')
const path = require('path')
const VOLUME = 0.25
// App state to access the soundNotifications preference
let state
/* Cache of Audio elements, for instant playback */
const cache = {}
@@ -50,19 +46,7 @@ const sounds = {
}
}
function init (appState) {
state = appState
}
function play (name) {
if (state == null) {
return
}
if (!state.saved.prefs.soundNotifications) {
return
}
let audio = cache[name]
if (!audio) {
const sound = sounds[name]

View File

@@ -1,6 +1,6 @@
const appConfig = require('application-config')('WebTorrent')
const path = require('path')
const { EventEmitter } = require('events')
const {EventEmitter} = require('events')
const config = require('../../config')
@@ -88,7 +88,6 @@ function getDefaultPlayState () {
return {
infoHash: null, /* the info hash of the torrent we're playing */
fileIndex: null, /* the zero-based index within the torrent */
fileName: null, /* name of the file that is playing */
location: 'local', /* 'local', 'chromecast', 'airplay' */
type: null, /* 'audio' or 'video', could be 'other' if ever support eg streaming to VLC */
currentTime: 0, /* seconds */
@@ -105,59 +104,56 @@ function getDefaultPlayState () {
selectedIndex: -1, /* current subtitle track */
showMenu: false /* popover menu, above the video */
},
audioTracks: {
tracks: [],
selectedIndex: 0, /* current audio track */
showMenu: false /* popover menu, above the video */
},
aspectRatio: 0 /* aspect ratio of the video */
}
}
/* If the saved state file doesn't exist yet, here's what we use instead */
function setupStateSaved () {
const { copyFileSync, mkdirSync, readFileSync } = require('fs')
function setupStateSaved (cb) {
const cpFile = require('cp-file')
const fs = require('fs')
const parseTorrent = require('parse-torrent')
const parallel = require('run-parallel')
const saved = {
prefs: {
downloadPath: config.DEFAULT_DOWNLOAD_PATH,
isFileHandler: false,
openExternalPlayer: false,
externalPlayerPath: '',
startup: false,
soundNotifications: true,
autoAddTorrents: false,
torrentsFolderPath: '',
highestPlaybackPriority: true
externalPlayerPath: null,
startup: false
},
torrents: config.DEFAULT_TORRENTS.map(createTorrentObject),
torrentsToResume: [],
version: config.APP_VERSION /* make sure we can upgrade gracefully later */
}
// TODO: Doing several sync calls during first startup is not ideal
mkdirSync(config.POSTER_PATH, { recursive: true })
mkdirSync(config.TORRENT_PATH, { recursive: true })
const tasks = []
config.DEFAULT_TORRENTS.forEach((t, i) => {
config.DEFAULT_TORRENTS.map(function (t, i) {
const infoHash = saved.torrents[i].infoHash
// TODO: Doing several sync calls during first startup is not ideal
copyFileSync(
path.join(config.STATIC_PATH, t.posterFileName),
path.join(config.POSTER_PATH, infoHash + path.extname(t.posterFileName))
)
copyFileSync(
path.join(config.STATIC_PATH, t.torrentFileName),
path.join(config.TORRENT_PATH, infoHash + '.torrent')
)
tasks.push(function (cb) {
cpFile(
path.join(config.STATIC_PATH, t.posterFileName),
path.join(config.POSTER_PATH, infoHash + path.extname(t.posterFileName))
).then(cb).catch(cb)
})
tasks.push(function (cb) {
cpFile(
path.join(config.STATIC_PATH, t.torrentFileName),
path.join(config.TORRENT_PATH, infoHash + '.torrent')
).then(cb).catch(cb)
})
})
return saved
parallel(tasks, function (err) {
if (err) return cb(err)
cb(null, saved)
})
function createTorrentObject (t) {
// TODO: Doing several sync calls during first startup is not ideal
const torrent = readFileSync(path.join(config.STATIC_PATH, t.torrentFileName))
// 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)
return {
@@ -202,20 +198,15 @@ function shouldHidePlayerControls () {
this.playing.playbackRate === 1
}
async function load (cb) {
let saved = await appConfig.read()
if (!saved || !saved.version) {
console.log('Missing config file: Creating new one')
try {
saved = setupStateSaved()
} catch (err) {
onSavedState(err)
return
function load (cb) {
appConfig.read(function (err, saved) {
if (err || !saved.version) {
console.log('Missing config file: Creating new one')
setupStateSaved(onSavedState)
} else {
onSavedState(null, saved)
}
}
onSavedState(null, saved)
})
function onSavedState (err, saved) {
if (err) return cb(err)
@@ -233,7 +224,7 @@ async function load (cb) {
}
// Write state.saved to the JSON state file
async function saveImmediate (state, cb) {
function saveImmediate (state, cb) {
console.log('Saving state to ' + appConfig.filePath)
// Clean up, so that we're not saving any pending state
@@ -244,7 +235,7 @@ async function saveImmediate (state, cb) {
.filter((x) => x.infoHash)
.map(function (x) {
const torrent = {}
for (const key in x) {
for (let key in x) {
if (key === 'progress' || key === 'torrentKey') {
continue // Don't save progress info or key for the webtorrent process
}
@@ -256,10 +247,8 @@ async function saveImmediate (state, cb) {
return torrent
})
try {
await appConfig.write(copy)
State.emit('stateSaved')
} catch (err) {
console.error(err)
}
appConfig.write(copy, (err) => {
if (err) console.error(err)
else State.emit('stateSaved')
})
}

View File

@@ -74,7 +74,7 @@ function reset () {
// Track screen resolution
function getScreenInfo () {
return electron.remote.screen.getAllDisplays().map((screen) => ({
return electron.screen.getAllDisplays().map((screen) => ({
width: screen.size.width,
height: screen.size.height,
scaleFactor: screen.scaleFactor
@@ -98,7 +98,7 @@ function getSystemInfo () {
function getTorrentStats (state) {
const count = state.saved.torrents.length
let sizeMB = 0
const byStatus = {
let byStatus = {
new: { count: 0, sizeMB: 0 },
downloading: { count: 0, sizeMB: 0 },
seeding: { count: 0, sizeMB: 0 },
@@ -121,7 +121,7 @@ function getTorrentStats (state) {
}
// Then, round all the counts and sums to the nearest power of 2
const ret = roundTorrentStats({ count, sizeMB })
const ret = roundTorrentStats({count, sizeMB})
ret.byStatus = {
new: roundTorrentStats(byStatus.new),
downloading: roundTorrentStats(byStatus.downloading),
@@ -198,7 +198,7 @@ function logUncaughtError (procName, e) {
// Log the app version *at the time of the error*
const version = config.APP_VERSION
telemetry.uncaughtErrors.push({ process: procName, message, stack, version })
telemetry.uncaughtErrors.push({process: procName, message, stack, version})
}
// Turns a DOM element into a string, eg "DIV.my-class.visible"

View File

@@ -3,14 +3,11 @@ module.exports = {
isVideo,
isAudio,
isTorrent,
isMagnetLink,
isPlayableTorrentSummary
}
const path = require('path')
const mediaExtensions = require('./media-extensions')
// Checks whether a fileSummary or file path is audio/video that we can play,
// based on the file extension
function isPlayable (file) {
@@ -19,12 +16,30 @@ function isPlayable (file) {
// Checks whether a fileSummary or file path is playable video
function isVideo (file) {
return mediaExtensions.video.includes(getFileExtension(file))
return [
'.avi',
'.m4v',
'.mkv',
'.mov',
'.mp4',
'.mpg',
'.ogv',
'.webm',
'.wmv'
].includes(getFileExtension(file))
}
// Checks whether a fileSummary or file path is playable audio
function isAudio (file) {
return mediaExtensions.audio.includes(getFileExtension(file))
return [
'.aac',
'.ac3',
'.mp3',
'.ogg',
'.wav',
'.flac',
'.m4a'
].includes(getFileExtension(file))
}
// Checks if the argument is either:
@@ -32,15 +47,9 @@ function isAudio (file) {
// - a file object where obj.name is ends in .torrent
// - a string that's a magnet link (magnet://...)
function isTorrent (file) {
return isTorrentFile(file) || isMagnetLink(file)
}
function isTorrentFile (file) {
return getFileExtension(file) === '.torrent'
}
function isMagnetLink (link) {
return typeof link === 'string' && /^(stream-)?magnet:/.test(link)
const isTorrentFile = getFileExtension(file) === '.torrent'
const isMagnet = typeof file === 'string' && /^(stream-)?magnet:/.test(file)
return isTorrentFile || isMagnet
}
function getFileExtension (file) {

View File

@@ -3,144 +3,39 @@ module.exports = torrentPoster
const captureFrame = require('capture-frame')
const path = require('path')
const mediaExtensions = require('./media-extensions')
const msgNoSuitablePoster = 'Cannot generate a poster from any files in the torrent'
function torrentPoster (torrent, cb) {
// First, try to use a poster image if available
const posterFile = torrent.files.filter(function (file) {
return /^poster\.(jpg|png|gif)$/.test(file.name)
})[0]
if (posterFile) return extractPoster(posterFile, cb)
if (posterFile) return torrentPosterFromImage(posterFile, torrent, cb)
// 'score' each media type based on total size present in torrent
const bestScore = ['audio', 'video', 'image'].map(mediaType => {
return {
type: mediaType,
size: calculateDataLengthByExtension(torrent, mediaExtensions[mediaType])
}
}).sort((a, b) => { // sort descending on size
return b.size - a.size
})[0]
// 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'])
if (videoFile) return torrentPosterFromVideo(videoFile, torrent, cb)
if (bestScore.size === 0) {
// Admit defeat, no video, audio or image had a significant presence
return cb(new Error(msgNoSuitablePoster))
}
// Third, try to use the largest image file
const imgFile = getLargestFileByExtension(torrent, ['.gif', '.jpg', '.jpeg', '.png'])
if (imgFile) return torrentPosterFromImage(imgFile, torrent, cb)
// Based on which media type is dominant we select the corresponding poster function
switch (bestScore.type) {
case 'audio':
return torrentPosterFromAudio(torrent, cb)
case 'image':
return torrentPosterFromImage(torrent, cb)
case 'video':
return torrentPosterFromVideo(torrent, cb)
}
// TODO: generate a waveform from the largest sound file
// Finally, admit defeat
return cb(new Error('Cannot generate a poster from any files in the torrent'))
}
/**
* Calculate the total data size of file matching one of the provided extensions
* @param torrent
* @param extensions List of extension to match
* @returns {number} total size, of matches found (>= 0)
*/
function calculateDataLengthByExtension (torrent, extensions) {
const files = filterOnExtension(torrent, extensions)
if (files.length === 0) return 0
return files
.map(file => file.length)
.reduce((a, b) => {
return a + b
})
}
/**
* Get the largest file of a given torrent, filtered by provided extension
* @param torrent Torrent to search in
* @param extensions Extension whitelist filter
* @returns Torrent file object
*/
function getLargestFileByExtension (torrent, extensions) {
const files = filterOnExtension(torrent, extensions)
const files = torrent.files.filter(function (file) {
const extname = path.extname(file.name).toLowerCase()
return extensions.indexOf(extname) !== -1
})
if (files.length === 0) return undefined
return files.reduce((a, b) => {
return files.reduce(function (a, b) {
return a.length > b.length ? a : b
})
}
/**
* Filter file on a list extension, can be used to find al image files
* @param torrent Torrent to filter files from
* @param extensions File extensions to filter on
* @returns {Array} Array of torrent file objects matching one of the given extensions
*/
function filterOnExtension (torrent, extensions) {
return torrent.files.filter(file => {
const extname = path.extname(file.name).toLowerCase()
return extensions.indexOf(extname) !== -1
})
}
/**
* Returns a score how likely the file is suitable as a poster
* @param imgFile File object of an image
* @returns {number} Score, higher score is a better match
*/
function scoreAudioCoverFile (imgFile) {
const fileName = path.basename(imgFile.name, path.extname(imgFile.name)).toLowerCase()
const relevanceScore = {
cover: 80,
folder: 80,
album: 80,
front: 80,
back: 20,
spectrogram: -80
}
for (const keyword in relevanceScore) {
if (fileName === keyword) {
return relevanceScore[keyword]
}
if (fileName.indexOf(keyword) !== -1) {
return relevanceScore[keyword]
}
}
return 0
}
function torrentPosterFromAudio (torrent, cb) {
const imageFiles = filterOnExtension(torrent, mediaExtensions.image)
if (imageFiles.length === 0) return cb(new Error(msgNoSuitablePoster))
const bestCover = imageFiles.map(file => {
return {
file,
score: scoreAudioCoverFile(file)
}
}).reduce((a, b) => {
if (a.score > b.score) {
return a
}
if (b.score > a.score) {
return b
}
// If score is equal, pick the largest file, aiming for highest resolution
if (a.file.length > b.file.length) {
return a
}
return b
})
const extname = path.extname(bestCover.file.name)
bestCover.file.getBuffer((err, buf) => cb(err, buf, extname))
}
function torrentPosterFromVideo (torrent, cb) {
const file = getLargestFileByExtension(torrent, mediaExtensions.video)
function torrentPosterFromVideo (file, torrent, cb) {
const index = torrent.files.indexOf(file)
const server = torrent.createServer(0)
@@ -175,19 +70,14 @@ function torrentPosterFromVideo (torrent, cb) {
server.destroy()
if (buf.length === 0) return cb(new Error(msgNoSuitablePoster))
if (buf.length === 0) return cb(new Error('Generated poster contains no data'))
cb(null, buf, '.jpg')
}
}
}
function torrentPosterFromImage (torrent, cb) {
const file = getLargestFileByExtension(torrent, mediaExtensions.image)
extractPoster(file, cb)
}
function extractPoster (file, cb) {
function torrentPosterFromImage (file, torrent, cb) {
const extname = path.extname(file.name)
file.getBuffer((err, buf) => { return cb(err, buf, extname) })
file.getBuffer((err, buf) => cb(err, buf, extname))
}

View File

@@ -5,7 +5,7 @@
* actually used because auto-prefixing is disabled with
* `darkBaseTheme.userAgent = false`. Return a fake object.
*/
const Module = require('module')
let Module = require('module')
const _require = Module.prototype.require
Module.prototype.require = function (id) {
if (id === 'inline-style-prefixer') return {}
@@ -36,9 +36,15 @@ const telemetry = require('./lib/telemetry')
const sound = require('./lib/sound')
const TorrentPlayer = require('./lib/torrent-player')
const Plugins = require('./plugins')
const plugins = new Plugins()
// 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')
// Electron apps have two processes: a main process (node) runs first and starts
@@ -74,7 +80,6 @@ function onState (err, _state) {
window.dispatch = dispatch
telemetry.init(state)
sound.init(state)
// Log uncaught JS errors
window.addEventListener(
@@ -99,10 +104,6 @@ function onState (err, _state) {
const SubtitlesController = require('./controllers/subtitles-controller')
return new SubtitlesController(state)
}),
audioTracks: createGetter(() => {
const AudioTracksController = require('./controllers/audio-tracks-controller')
return new AudioTracksController(state)
}),
torrent: createGetter(() => {
const TorrentController = require('./controllers/torrent-controller')
return new TorrentController(state)
@@ -113,13 +114,11 @@ function onState (err, _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()
})
}
plugins.init({dispatch, state})
// Add first page to location history
state.location.go({
url: 'home',
@@ -152,9 +151,6 @@ function onState (err, _state) {
// ...same thing if you paste a torrent
document.addEventListener('paste', onPaste)
// Add YouTube style hotkey shortcuts
window.addEventListener('keydown', onKeydown)
const debouncedFullscreenToggle = debounce(function () {
dispatch('toggleFullScreen')
}, 1000, true)
@@ -239,108 +235,98 @@ function updateElectron () {
const dispatchHandlers = {
// Torrent list: creating, deleting, selecting torrents
openTorrentFile: () => ipcRenderer.send('openTorrentFile'),
openFiles: () => ipcRenderer.send('openFiles'), /* shows the open file dialog */
openTorrentAddress: () => { state.modal = { id: 'open-torrent-address-modal' } },
'openTorrentFile': () => ipcRenderer.send('openTorrentFile'),
'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) =>
'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) =>
'confirmDeleteTorrent': (infoHash, deleteData) =>
controllers.torrentList().confirmDeleteTorrent(infoHash, deleteData),
deleteTorrent: (infoHash, deleteData) =>
'deleteTorrent': (infoHash, deleteData) =>
controllers.torrentList().deleteTorrent(infoHash, deleteData),
confirmDeleteAllTorrents: (deleteData) =>
controllers.torrentList().confirmDeleteAllTorrents(deleteData),
deleteAllTorrents: (deleteData) =>
controllers.torrentList().deleteAllTorrents(deleteData),
toggleSelectTorrent: (infoHash) =>
'toggleSelectTorrent': (infoHash) =>
controllers.torrentList().toggleSelectTorrent(infoHash),
openTorrentContextMenu: (infoHash) =>
'openTorrentContextMenu': (infoHash) =>
controllers.torrentList().openTorrentContextMenu(infoHash),
startTorrentingSummary: (torrentKey) =>
'startTorrentingSummary': (torrentKey) =>
controllers.torrentList().startTorrentingSummary(torrentKey),
saveTorrentFileAs: (torrentKey) =>
'saveTorrentFileAs': (torrentKey) =>
controllers.torrentList().saveTorrentFileAs(torrentKey),
prioritizeTorrent: (infoHash) => controllers.torrentList().prioritizeTorrent(infoHash),
resumePausedTorrents: () => controllers.torrentList().resumePausedTorrents(),
'prioritizeTorrent': (infoHash) => controllers.torrentList().prioritizeTorrent(infoHash),
'resumePausedTorrents': () => controllers.torrentList().resumePausedTorrents(),
// 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),
openPath: (infoHash, index) => controllers.playback().openPath(infoHash, index),
'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),
// 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),
// Audio Tracks
selectAudioTrack: (index) => controllers.audioTracks().selectAudioTrack(index),
toggleAudioTracksMenu: () => controllers.audioTracks().toggleAudioTracksMenu(),
'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(),
'mediaControlsMouseEnter': () => controllers.media().controlsMouseEnter(),
'mediaControlsMouseLeave': () => controllers.media().controlsMouseLeave(),
'openExternalPlayer': () => controllers.media().openExternalPlayer(),
'externalPlayerNotFound': () => controllers.media().externalPlayerNotFound(),
// Remote casting: Chromecast, Airplay, etc
toggleCastMenu: (deviceType) => lazyLoadCast().toggleMenu(deviceType),
selectCastDevice: (index) => lazyLoadCast().selectDevice(index),
stopCasting: () => lazyLoadCast().stop(),
'toggleCastMenu': (deviceType) => lazyLoadCast().toggleMenu(deviceType),
'selectCastDevice': (index) => lazyLoadCast().selectDevice(index),
'stopCasting': () => lazyLoadCast().stop(),
// Preferences screen
preferences: () => controllers.prefs().show(),
updatePreferences: (key, value) => controllers.prefs().update(key, value),
checkDownloadPath,
startFolderWatcher: () => controllers.folderWatcher().start(),
stopFolderWatcher: () => controllers.folderWatcher().stop(),
'preferences': () => controllers.prefs().show(),
'updatePreferences': (key, value) => controllers.prefs().update(key, value),
'checkDownloadPath': checkDownloadPath,
// 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 },
backToList,
escapeBack,
back: () => state.location.back(),
forward: () => state.location.forward(),
cancel: () => state.location.cancel(),
'exitModal': () => { state.modal = null },
'backToList': backToList,
'escapeBack': escapeBack,
'back': () => state.location.back(),
'forward': () => state.location.forward(),
'cancel': () => state.location.cancel(),
// Controlling the window
setDimensions,
toggleFullScreen: (setTo) => ipcRenderer.send('toggleFullScreen', setTo),
setTitle: (title) => { state.window.title = title },
resetTitle: () => { state.window.title = config.APP_WINDOW_TITLE },
'setDimensions': setDimensions,
'toggleFullScreen': (setTo) => ipcRenderer.send('toggleFullScreen', setTo),
'setTitle': (title) => { state.window.title = title },
'resetTitle': () => { state.window.title = config.APP_WINDOW_TITLE },
// Everything else
onOpen,
error: onError,
uncaughtError: (proc, err) => telemetry.logUncaughtError(proc, err),
stateSave: () => State.save(state),
stateSaveImmediate: () => State.saveImmediate(state),
update: () => {} // No-op, just trigger an update
'onOpen': onOpen,
'error': onError,
'uncaughtError': (proc, err) => telemetry.logUncaughtError(proc, err),
'stateSave': () => State.save(state),
'stateSaveImmediate': () => State.saveImmediate(state),
'update': () => {} // No-op, just trigger an update
}
// Events from the UI never modify state directly. Instead they call dispatch()
@@ -351,6 +337,7 @@ function dispatch (action, ...args) {
}
const handler = dispatchHandlers[action]
plugins.on(action, args)
if (handler) handler(...args)
else console.error('Missing dispatch handler: ' + action)
@@ -372,7 +359,7 @@ function setupIpc () {
ipcRenderer.on('windowBoundsChanged', onWindowBoundsChanged)
const tc = controllers.torrent()
ipcRenderer.on('wt-parsed', (e, ...args) => tc.torrentParsed(...args))
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())
@@ -463,21 +450,14 @@ function setDimensions (dimensions) {
)
ipcRenderer.send('setAspectRatio', aspectRatio)
ipcRenderer.send('setBounds', { contentBounds: true, x: null, y: null, width, height })
ipcRenderer.send('setBounds', {contentBounds: true, x: null, y: null, width, height})
state.playing.aspectRatio = aspectRatio
}
// Called when the user adds files (.torrent, files to seed, subtitles) to the app
// via any method (drag-drop, drag to app icon, command line)
function onOpen (files) {
if (!Array.isArray(files)) files = [files]
// File API seems to transform "magnet:?foo" in "magnet:///?foo"
// this is a sanitization
files = files.map(file => {
if (typeof file !== 'string') return file
return file.replace(/^magnet:\/+\?/i, 'magnet:?')
})
if (!Array.isArray(files)) files = [ files ]
const url = state.location.url()
const allTorrents = files.every(TorrentPlayer.isTorrent)
@@ -523,34 +503,6 @@ function onPaste (e) {
update()
}
function onKeydown (e) {
const key = e.key
if (key === 'ArrowLeft') {
dispatch('skip', -5)
} else if (key === 'ArrowRight') {
dispatch('skip', 5)
} else if (key === 'ArrowUp') {
dispatch('changeVolume', 0.1)
} else if (key === 'ArrowDown') {
dispatch('changeVolume', -0.1)
} else if (key === 'j') {
dispatch('skip', -10)
} else if (key === 'l') {
dispatch('skip', 10)
} else if (key === 'k') {
dispatch('playPause')
} else if (key === '>') {
dispatch('changePlaybackRate', 1)
} else if (key === '<') {
dispatch('changePlaybackRate', -1)
} else if (key === 'f') {
dispatch('toggleFullScreen')
}
update()
}
function onFocus (e) {
state.window.isFocused = true
state.dock.badge = 0
@@ -563,7 +515,7 @@ function onBlur () {
}
function onVisibilityChange () {
state.window.isVisible = !document.hidden
state.window.isVisible = !document.webkitHidden
}
function onFullscreenChanged (e, isFullScreen) {

View File

@@ -12,10 +12,10 @@ const Header = require('../components/header')
const TorrentListPage = require('./torrent-list-page')
const Views = {
home: createGetter(() => TorrentListPage),
player: createGetter(() => require('./player-page')),
'home': createGetter(() => TorrentListPage),
'player': createGetter(() => require('./player-page')),
'create-torrent': createGetter(() => require('./create-torrent-page')),
preferences: createGetter(() => require('./preferences-page'))
'preferences': createGetter(() => require('./preferences-page'))
}
const Modals = {
@@ -24,9 +24,7 @@ const Modals = {
),
'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')),
'delete-all-torrents-modal':
createGetter(() => require('../components/delete-all-torrents-modal'))
'unsupported-media-modal': createGetter(() => require('../components/unsupported-media-modal'))
}
const fontFamily = process.platform === 'win32'
@@ -90,10 +88,8 @@ class App extends React.Component {
return (<div key={i} className='error'>{error.message}</div>)
})
return (
<div
key='errors'
className={'error-popover ' + (hasErrors ? 'visible' : 'hidden')}
>
<div key='errors'
className={'error-popover ' + (hasErrors ? 'visible' : 'hidden')}>
<div key='title' className='title'>Error</div>
{errorElems}
</div>

View File

@@ -3,7 +3,7 @@ const path = require('path')
const prettyBytes = require('prettier-bytes')
const React = require('react')
const { dispatch, dispatcher } = require('../lib/dispatcher')
const {dispatch, dispatcher} = require('../lib/dispatcher')
const FlatButton = require('material-ui/FlatButton').default
const RaisedButton = require('material-ui/RaisedButton').default
@@ -35,7 +35,7 @@ class CreateTorrentPage extends React.Component {
// 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 }))
.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)
@@ -65,9 +65,9 @@ class CreateTorrentPage extends React.Component {
}
// Create React event handlers only once
this.handleSetIsPrivate = (_, isPrivate) => this.setState({ isPrivate })
this.handleSetComment = (_, comment) => this.setState({ comment })
this.handleSetTrackers = (_, trackers) => this.setState({ trackers })
this.setIsPrivate = (_, isPrivate) => this.setState({isPrivate})
this.setComment = (_, comment) => this.setState({comment})
this.setTrackers = (_, trackers) => this.setState({trackers})
this.handleSubmit = handleSubmit.bind(this)
}
@@ -94,8 +94,7 @@ class CreateTorrentPage extends React.Component {
marginBottom: 10
}}
hideLabel='Hide advanced settings...'
showLabel='Show advanced settings...'
>
showLabel='Show advanced settings...'>
{this.renderAdvanced()}
</ShowMore>
<div className='float-right'>
@@ -105,14 +104,12 @@ class CreateTorrentPage extends React.Component {
style={{
marginRight: 10
}}
onClick={dispatcher('cancel')}
/>
onClick={dispatcher('cancel')} />
<RaisedButton
className='control create-torrent-button'
label='Create Torrent'
primary
onClick={this.handleSubmit}
/>
onClick={this.handleSubmit} />
</div>
</div>
)
@@ -144,10 +141,9 @@ class CreateTorrentPage extends React.Component {
<label>Private:</label>
<Checkbox
className='torrent-is-private control'
style={{ display: '' }}
style={{display: ''}}
checked={this.state.isPrivate}
onCheck={this.handleSetIsPrivate}
/>
onCheck={this.setIsPrivate} />
</div>
<div key='trackers' className='torrent-attribute'>
<label>Trackers:</label>
@@ -159,8 +155,7 @@ class CreateTorrentPage extends React.Component {
rows={2}
rowsMax={10}
value={this.state.trackers}
onChange={this.handleSetTrackers}
/>
onChange={this.setTrackers} />
</div>
<div key='comment' className='torrent-attribute'>
<label>Comment:</label>
@@ -173,8 +168,7 @@ class CreateTorrentPage extends React.Component {
rows={2}
rowsMax={10}
value={this.state.comment}
onChange={this.handleSetComment}
/>
onChange={this.setComment} />
</div>
<div key='files' className='torrent-attribute'>
<label>Files:</label>
@@ -197,14 +191,9 @@ function handleSubmit () {
path: this.state.basePath,
files: this.state.files,
announce: announceList,
private: this.state.isPrivate,
comment: this.state.comment.trim()
}
// If torrent is not private, leave private flag unset. This ensures that
// the torrent info hash will match the result generated by other tools,
// including webtorrent-cli.
if (this.state.isPrivate) options.private = true
dispatch('createTorrent', options)
}

View File

@@ -1,10 +1,11 @@
const React = require('react')
const Bitfield = require('bitfield')
const prettyBytes = require('prettier-bytes')
const zeroFill = require('zero-fill')
const TorrentSummary = require('../lib/torrent-summary')
const Playlist = require('../lib/playlist')
const { dispatch, dispatcher } = require('../lib/dispatcher')
const {dispatch, dispatcher} = require('../lib/dispatcher')
const config = require('../../config')
// Shows a streaming video player. Standard features + Chromecast + Airplay
@@ -19,8 +20,7 @@ module.exports = class Player extends React.Component {
<div
className='player'
onWheel={handleVolumeWheel}
onMouseMove={dispatcher('mediaMouseMoved')}
>
onMouseMove={dispatcher('mediaMouseMoved')}>
{showVideo ? renderMedia(state) : renderCastScreen(state)}
{showControls ? renderPlayerControls(state) : null}
</div>
@@ -95,13 +95,6 @@ function renderMedia (state) {
delete file.selectedSubtitle
}
// Switch to selected audio track
const audioTracks = mediaElement.audioTracks || []
for (let j = 0; j < audioTracks.length; j++) {
const isSelectedTrack = j === state.playing.audioTracks.selectedIndex
audioTracks[j].enabled = isSelectedTrack
}
state.playing.volume = mediaElement.volume
}
@@ -114,11 +107,10 @@ function renderMedia (state) {
trackTags.push(
<track
key={i}
default={isSelected}
default={isSelected ? 'default' : ''}
label={track.label}
kind='subtitles'
src={track.buffer}
/>
type='subtitles'
src={track.buffer} />
)
}
}
@@ -135,7 +127,7 @@ function renderMedia (state) {
onError={dispatcher('mediaError')}
onTimeUpdate={dispatcher('mediaTimeUpdate')}
onEncrypted={dispatcher('mediaEncrypted')}
>
onCanPlay={onCanPlay}>
{trackTags}
</MediaTagName>
)
@@ -145,57 +137,21 @@ function renderMedia (state) {
<div
key='letterbox'
className='letterbox'
onMouseMove={dispatcher('mediaMouseMoved')}
>
onMouseMove={dispatcher('mediaMouseMoved')}>
{mediaTag}
{renderOverlay(state)}
</div>
)
// As soon as we know the video dimensions, resize the window
function onLoadedMetadata (e) {
const mediaElement = e.target
// check if we can decode video and audio track
if (state.playing.type === 'video') {
if (mediaElement.videoTracks.length === 0) {
dispatch('mediaError', 'Video codec unsupported')
}
if (mediaElement.audioTracks.length === 0) {
dispatch('mediaError', 'Audio codec unsupported')
}
dispatch('mediaSuccess')
const dimensions = {
width: mediaElement.videoWidth,
height: mediaElement.videoHeight
}
// As soon as we know the video dimensions, resize the window
dispatch('setDimensions', dimensions)
// set audioTracks
const tracks = []
for (let i = 0; i < mediaElement.audioTracks.length; i++) {
tracks.push({
label: mediaElement.audioTracks[i].label || `Track ${i + 1}`,
language: mediaElement.audioTracks[i].language
})
}
state.playing.audioTracks.tracks = tracks
state.playing.audioTracks.selectedIndex = 0
}
// check if we can decode audio track
if (state.playing.type === 'audio') {
if (mediaElement.audioTracks.length === 0) {
dispatch('mediaError', 'Audio codec unsupported')
}
dispatch('mediaSuccess')
if (state.playing.type !== 'video') return
const video = e.target
const dimensions = {
width: video.videoWidth,
height: video.videoHeight
}
dispatch('setDimensions', dimensions)
}
function onEnded (e) {
@@ -204,7 +160,19 @@ function renderMedia (state) {
} else {
// When the last video completes, pause the video instead of looping
state.playing.isPaused = true
if (state.window.isFullScreen) dispatch('toggleFullScreen')
}
}
function onCanPlay (e) {
const elem = e.target
if (state.playing.type === 'video' &&
elem.webkitVideoDecodedByteCount === 0) {
dispatch('mediaError', 'Video codec unsupported')
} else if (elem.webkitAudioDecodedByteCount === 0) {
dispatch('mediaError', 'Audio codec unsupported')
} else {
dispatch('mediaSuccess')
elem.play()
}
}
}
@@ -235,41 +203,28 @@ function renderOverlay (state) {
)
}
/**
* Render track or disk number string
* @param common metadata.common part
* @param key should be either 'track' or 'disk'
* @return track or disk number metadata as JSX block
*/
function renderTrack (common, key) {
// Audio metadata: track-number
if (common[key] && common[key].no) {
let str = `${common[key].no}`
if (common[key].of) {
str += ` of ${common[key].of}`
}
const style = { textTransform: 'capitalize' }
return (
<div className={`audio-${key}`}>
<label style={style}>{key}</label> {str}
</div>
)
}
}
function renderAudioMetadata (state) {
const fileSummary = state.getPlayingFileSummary()
if (!fileSummary.audioInfo) return
const common = fileSummary.audioInfo.common || {}
const info = fileSummary.audioInfo
// Get audio track info
const title = common.title ? common.title : fileSummary.name
let title = info.title
if (!title) {
title = fileSummary.name
}
let artist = info.artist && info.artist[0]
let album = info.album
if (album && info.year && !album.includes(info.year)) {
album += ' (' + info.year + ')'
}
let 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 = []
// Audio metadata: artist(s)
const artist = common.artist || common.albumartist
if (artist) {
elems.push((
<div key='artist' className='audio-artist'>
@@ -277,90 +232,17 @@ function renderAudioMetadata (state) {
</div>
))
}
// Audio metadata: disk & track-number
const count = ['track', 'disk']
count.forEach(key => {
const nrElem = renderTrack(common, key)
if (nrElem) {
elems.push(nrElem)
}
})
// Audio metadata: album
if (common.album) {
if (album) {
elems.push((
<div key='album' className='audio-album'>
<label>Album</label>{common.album}
<label>Album</label>{album}
</div>
))
}
// Audio metadata: year
if (common.year) {
if (track) {
elems.push((
<div key='year' className='audio-year'>
<label>Year</label>{common.year}
</div>
))
}
// Audio metadata: release information (label & catalog-number)
if (common.label || common.catalognumber) {
const releaseInfo = []
if (common.label && common.catalognumber &&
common.label.length === common.catalognumber.length) {
// Assume labels & catalog-numbers are pairs
for (let n = 0; n < common.label.length; ++n) {
releaseInfo.push(common.label[0] + ' / ' + common.catalognumber[n])
}
} else {
if (common.label) {
releaseInfo.push(...common.label)
}
if (common.catalognumber) {
releaseInfo.push(...common.catalognumber)
}
}
elems.push((
<div key='release' className='audio-release'>
<label>Release</label>{releaseInfo.join(', ')}
</div>
))
}
// Audio metadata: format
const format = []
fileSummary.audioInfo.format = fileSummary.audioInfo.format || ''
if (fileSummary.audioInfo.format.container) {
format.push(fileSummary.audioInfo.format.container)
}
if (fileSummary.audioInfo.format.codec &&
fileSummary.audioInfo.format.container !== fileSummary.audioInfo.format.codec) {
format.push(fileSummary.audioInfo.format.codec)
}
if (fileSummary.audioInfo.format.bitrate) {
format.push(Math.round(fileSummary.audioInfo.format.bitrate / 1000) + ' kbps') // 128 kbps
}
if (fileSummary.audioInfo.format.sampleRate) {
format.push(Math.round(fileSummary.audioInfo.format.sampleRate / 100) / 10 + ' kHz')
}
if (fileSummary.audioInfo.format.bitsPerSample) {
format.push(fileSummary.audioInfo.format.bitsPerSample + ' bit')
}
if (format.length > 0) {
elems.push((
<div key='format' className='audio-format'>
<label>Format</label>{format.join(', ')}
</div>
))
}
// Audio metadata: comments
if (common.comment) {
elems.push((
<div key='comments' className='audio-comments'>
<label>Comments</label>{common.comment.join(' / ')}
<div key='track' className='audio-track'>
<label>Track</label>{track}
</div>
))
}
@@ -391,9 +273,9 @@ function renderLoadingSpinner (state) {
return (
<div key='loading' className='media-stalled'>
<div key='loading-spinner' className='loading-spinner' />
<div key='loading-spinner' className='loading-spinner'>&nbsp;</div>
<div key='loading-progress' className='loading-status ellipsis'>
<span><span className='progress'>{fileProgress}%</span> downloaded</span>
<span className='progress'>{fileProgress}%</span> downloaded
<span> {prettyBytes(prog.downloadSpeed || 0)}/s</span>
<span> {prettyBytes(prog.uploadSpeed || 0)}/s</span>
</div>
@@ -421,7 +303,7 @@ function renderCastScreen (state) {
isCast = false
} else if (state.playing.location === 'error') {
castIcon = 'error_outline'
castType = 'Unable to Play'
castType = 'Error'
isCast = false
}
@@ -451,7 +333,7 @@ function renderCastScreen (state) {
function renderCastOptions (state) {
if (!state.devices.castMenu) return
const { location, devices } = state.devices.castMenu
const {location, devices} = state.devices.castMenu
const player = state.devices[location]
const items = devices.map(function (device, ix) {
@@ -460,7 +342,6 @@ function renderCastOptions (state) {
return (
<li key={ix} onClick={dispatcher('selectCastDevice', ix)}>
<i className='icon'>{isSelected ? 'radio_button_checked' : 'radio_button_unchecked'}</i>
{' '}
{name}
</li>
)
@@ -500,27 +381,6 @@ function renderSubtitleOptions (state) {
)
}
function renderAudioTrackOptions (state) {
const audioTracks = state.playing.audioTracks
if (!audioTracks.tracks.length || !audioTracks.showMenu) return
const items = audioTracks.tracks.map(function (track, ix) {
const isSelected = state.playing.audioTracks.selectedIndex === ix
return (
<li key={ix} onClick={dispatcher('selectAudioTrack', ix)}>
<i className='icon'>{'radio_button_' + (isSelected ? 'checked' : 'unchecked')}</i>
{track.label}
</li>
)
})
return (
<ul key='audio-track-options' className='options-list'>
{items}
</ul>
)
}
function renderPlayerControls (state) {
const positionPercent = 100 * state.playing.currentTime / state.playing.duration
const playbackCursorStyle = { left: 'calc(' + positionPercent + '% - 3px)' }
@@ -529,9 +389,6 @@ function renderPlayerControls (state) {
: state.playing.subtitles.selectedIndex >= 0
? 'active'
: ''
const multiAudioClass = state.playing.audioTracks.tracks.length > 1
? 'active'
: 'disabled'
const prevClass = Playlist.hasPrevious(state) ? '' : 'disabled'
const nextClass = Playlist.hasNext(state) ? '' : 'disabled'
@@ -541,47 +398,41 @@ function renderPlayerControls (state) {
<div
key='cursor'
className='playback-cursor'
style={playbackCursorStyle}
/>
style={playbackCursorStyle} />
<div
key='scrub-bar'
className='scrub-bar'
draggable
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')}
>
onClick={dispatcher('previousTrack')}>
skip_previous
</i>,
<i
key='play'
className='icon play-pause float-left'
onClick={dispatcher('playPause')}
>
onClick={dispatcher('playPause')}>
{state.playing.isPaused ? 'play_arrow' : 'pause'}
</i>,
<i
key='skip-next'
className={'icon skip-next float-left ' + nextClass}
onClick={dispatcher('nextTrack')}
>
onClick={dispatcher('nextTrack')}>
skip_next
</i>,
<i
key='fullscreen'
className='icon fullscreen float-right'
onClick={dispatcher('toggleFullScreen')}
>
onClick={dispatcher('toggleFullScreen')}>
{state.window.isFullScreen ? 'fullscreen_exit' : 'fullscreen'}
</i>
]
@@ -592,18 +443,9 @@ function renderPlayerControls (state) {
<i
key='subtitles'
className={'icon closed-caption float-right ' + captionsClass}
onClick={handleSubtitles}
>
onClick={handleSubtitles}>
closed_caption
</i>
), (
<i
key='audio-tracks'
className={'icon multi-audio float-right ' + multiAudioClass}
onClick={handleAudioTracks}
>
library_music
</i>
))
}
@@ -614,9 +456,9 @@ function renderPlayerControls (state) {
// Add the cast buttons. Icons for each cast type, connected/disconnected:
const buttonIcons = {
chromecast: { true: 'cast_connected', false: 'cast' },
airplay: { true: 'airplay', false: 'airplay' },
dlna: { true: 'tv', false: 'tv' }
'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?
@@ -645,8 +487,7 @@ function renderPlayerControls (state) {
<i
key={castType}
className={'icon device float-right ' + buttonClass}
onClick={buttonHandler}
>
onClick={buttonHandler}>
{buttonIcon}
</i>
))
@@ -656,9 +497,9 @@ function renderPlayerControls (state) {
const volume = state.playing.volume
const volumeIcon = 'volume_' + (
volume === 0 ? 'off'
: volume < 0.3 ? 'mute'
: volume < 0.6 ? 'down'
: 'up')
: volume < 0.3 ? 'mute'
: volume < 0.6 ? 'down'
: 'up')
const volumeStyle = {
background: '-webkit-gradient(linear, left top, right top, ' +
'color-stop(' + (volume * 100) + '%, #eee), ' +
@@ -669,8 +510,7 @@ function renderPlayerControls (state) {
<div key='volume' className='volume float-left'>
<i
className='icon volume-icon float-left'
onMouseDown={handleVolumeMute}
>
onMouseDown={handleVolumeMute}>
{volumeIcon}
</i>
<input
@@ -678,14 +518,13 @@ 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)
const currentTimeStr = formatTime(state.playing.currentTime)
const durationStr = formatTime(state.playing.duration)
elements.push((
<span key='time' className='time float-left'>
{currentTimeStr} / {durationStr}
@@ -702,15 +541,12 @@ function renderPlayerControls (state) {
}
return (
<div
key='controls' className='controls'
<div key='controls' className='controls'
onMouseEnter={dispatcher('mediaControlsMouseEnter')}
onMouseLeave={dispatcher('mediaControlsMouseLeave')}
>
onMouseLeave={dispatcher('mediaControlsMouseLeave')}>
{elements}
{renderCastOptions(state)}
{renderSubtitleOptions(state)}
{renderAudioTrackOptions(state)}
</div>
)
@@ -754,10 +590,6 @@ function renderPlayerControls (state) {
dispatch('toggleSubtitlesMenu')
}
}
function handleAudioTracks (e) {
dispatch('toggleAudioTracksMenu')
}
}
// Renders the loading bar. Shows which parts of the torrent are loaded, which
@@ -781,7 +613,7 @@ function renderLoadingBar (state) {
for (let i = fileProg.startPiece; i <= fileProg.endPiece; i++) {
const partPresent = Bitfield.prototype.get.call(prog.bitfield, i)
if (partPresent && !lastPiecePresent) {
parts.push({ start: i - fileProg.startPiece, count: 1 })
parts.push({start: i - fileProg.startPiece, count: 1})
} else if (partPresent) {
parts[parts.length - 1].count++
}
@@ -814,19 +646,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'
}
const totalHours = Math.floor(total / 3600)
const totalMinutes = Math.floor(total / 60)
const hours = Math.floor(time / 3600)
let hours = Math.floor(time / 3600)
let minutes = Math.floor(time % 3600 / 60)
if (totalMinutes > 9 && minutes < 10) {
minutes = '0' + minutes
if (hours > 0) {
minutes = zeroFill(2, minutes)
}
const seconds = `0${Math.floor(time % 60)}`.slice(-2)
let seconds = zeroFill(2, Math.floor(time % 60))
return (totalHours > 0 ? hours + ':' : '') + minutes + ':' + seconds
return (hours > 0 ? hours + ':' : '') + minutes + ':' + seconds
}

View File

@@ -1,5 +1,5 @@
const path = require('path')
const React = require('react')
const PropTypes = require('prop-types')
const colors = require('material-ui/styles/colors')
const Checkbox = require('material-ui/Checkbox').default
@@ -7,7 +7,7 @@ const RaisedButton = require('material-ui/RaisedButton').default
const Heading = require('../components/heading')
const PathSelector = require('../components/path-selector')
const { dispatch } = require('../lib/dispatcher')
const {dispatch} = require('../lib/dispatcher')
const config = require('../../config')
class PreferencesPage extends React.Component {
@@ -25,9 +25,6 @@ class PreferencesPage extends React.Component {
this.handleStartupChange =
this.handleStartupChange.bind(this)
this.handleSoundNotificationsChange =
this.handleSoundNotificationsChange.bind(this)
}
downloadPathSelector () {
@@ -36,12 +33,11 @@ class PreferencesPage extends React.Component {
<PathSelector
dialog={{
title: 'Select download directory',
properties: ['openDirectory']
properties: [ 'openDirectory' ]
}}
onChange={this.handleDownloadPathChange}
title='Download location'
value={this.props.state.saved.prefs.downloadPath}
/>
value={this.props.state.unsaved.prefs.downloadPath} />
</Preference>
)
}
@@ -55,10 +51,9 @@ class PreferencesPage extends React.Component {
<Preference>
<Checkbox
className='control'
checked={!this.props.state.saved.prefs.openExternalPlayer}
label='Play torrent media files using WebTorrent'
onCheck={this.handleOpenExternalPlayerChange}
/>
checked={!this.props.state.unsaved.prefs.openExternalPlayer}
label={'Play torrent media files using WebTorrent'}
onCheck={this.handleOpenExternalPlayerChange} />
</Preference>
)
}
@@ -72,8 +67,8 @@ class PreferencesPage extends React.Component {
<Preference>
<Checkbox
className='control'
checked={this.props.state.saved.prefs.highestPlaybackPriority}
label='Highest Playback Priority'
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>
@@ -86,10 +81,10 @@ class PreferencesPage extends React.Component {
}
externalPlayerPathSelector () {
const playerPath = this.props.state.saved.prefs.externalPlayerPath
const playerPath = this.props.state.unsaved.prefs.externalPlayerPath
const playerName = this.props.state.getExternalPlayerName()
const description = this.props.state.saved.prefs.openExternalPlayer
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.`
@@ -99,12 +94,12 @@ class PreferencesPage extends React.Component {
<PathSelector
dialog={{
title: 'Select media player app',
properties: ['openFile']
properties: [ 'openFile' ]
}}
displayValue={playerName}
onChange={this.handleExternalPlayerPathChange}
title='External player'
value={playerPath}
/>
value={playerPath ? path.dirname(playerPath) : null} />
</Preference>
)
}
@@ -113,61 +108,8 @@ class PreferencesPage extends React.Component {
dispatch('updatePreferences', 'externalPlayerPath', filePath)
}
autoAddTorrentsCheckbox () {
return (
<Preference>
<Checkbox
className='control'
checked={this.props.state.saved.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.saved.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')
return
}
dispatch('stopFolderWatcher')
}
torrentsFolderPathSelector () {
const torrentsFolderPath = this.props.state.saved.prefs.torrentsFolderPath
return (
<Preference>
<PathSelector
dialog={{
title: 'Select folder to watch for new torrents',
properties: ['openDirectory']
}}
onChange={this.handleTorrentsFolderPathChange}
title='Folder to watch'
value={torrentsFolderPath}
/>
</Preference>
)
}
handleTorrentsFolderPathChange (filePath) {
dispatch('updatePreferences', 'torrentsFolderPath', filePath)
}
setDefaultAppButton () {
const isFileHandler = this.props.state.saved.prefs.isFileHandler
const isFileHandler = this.props.state.unsaved.prefs.isFileHandler
if (isFileHandler) {
return (
<Preference>
@@ -181,8 +123,7 @@ class PreferencesPage extends React.Component {
<RaisedButton
className='control'
onClick={this.handleSetDefaultApp}
label='Make WebTorrent the default'
/>
label='Make WebTorrent the default' />
</Preference>
)
}
@@ -191,40 +132,25 @@ class PreferencesPage extends React.Component {
dispatch('updatePreferences', 'startup', isChecked)
}
setStartupCheckbox () {
setStartupSection () {
if (config.IS_PORTABLE) {
return
}
return (
<Preference>
<Checkbox
className='control'
checked={this.props.state.saved.prefs.startup}
label='Open WebTorrent on startup'
onCheck={this.handleStartupChange}
/>
</Preference>
<PreferencesSection title='Startup'>
<Preference>
<Checkbox
className='control'
checked={this.props.state.unsaved.prefs.startup}
label={'Open WebTorrent on startup.'}
onCheck={this.handleStartupChange}
/>
</Preference>
</PreferencesSection>
)
}
soundNotificationsCheckbox () {
return (
<Preference>
<Checkbox
className='control'
checked={this.props.state.saved.prefs.soundNotifications}
label='Enable sounds'
onCheck={this.handleSoundNotificationsChange}
/>
</Preference>
)
}
handleSoundNotificationsChange (e, isChecked) {
dispatch('updatePreferences', 'soundNotifications', isChecked)
}
handleSetDefaultApp () {
dispatch('updatePreferences', 'isFileHandler', true)
}
@@ -237,10 +163,8 @@ class PreferencesPage extends React.Component {
}
return (
<div style={style}>
<PreferencesSection title='Folders'>
<PreferencesSection title='Downloads'>
{this.downloadPathSelector()}
{this.autoAddTorrentsCheckbox()}
{this.torrentsFolderPathSelector()}
</PreferencesSection>
<PreferencesSection title='Playback'>
{this.openExternalPlayerCheckbox()}
@@ -250,10 +174,7 @@ class PreferencesPage extends React.Component {
<PreferencesSection title='Default torrent app'>
{this.setDefaultAppButton()}
</PreferencesSection>
<PreferencesSection title='General'>
{this.setStartupCheckbox()}
{this.soundNotificationsCheckbox()}
</PreferencesSection>
{this.setStartupSection()}
</div>
)
}
@@ -262,7 +183,7 @@ class PreferencesPage extends React.Component {
class PreferencesSection extends React.Component {
static get propTypes () {
return {
title: PropTypes.string
title: React.PropTypes.string
}
}

View File

@@ -6,7 +6,7 @@ const LinearProgress = require('material-ui/LinearProgress').default
const TorrentSummary = require('../lib/torrent-summary')
const TorrentPlayer = require('../lib/torrent-player')
const { dispatcher } = require('../lib/dispatcher')
const {dispatcher} = require('../lib/dispatcher')
module.exports = class TorrentList extends React.Component {
render () {
@@ -67,8 +67,7 @@ module.exports = class TorrentList extends React.Component {
style={style}
className={classes.join(' ')}
onContextMenu={infoHash && dispatcher('openTorrentContextMenu', infoHash)}
onClick={infoHash && dispatcher('toggleSelectTorrent', infoHash)}
>
onClick={infoHash && dispatcher('toggleSelectTorrent', infoHash)}>
{this.renderTorrentMetadata(torrentSummary)}
{infoHash ? this.renderTorrentButtons(torrentSummary) : null}
{isSelected ? this.renderTorrentDetails(torrentSummary) : null}
@@ -131,8 +130,7 @@ module.exports = class TorrentList extends React.Component {
}}
checked={isActive}
onClick={stopPropagation}
onCheck={dispatcher('toggleTorrent', infoHash)}
/>
onCheck={dispatcher('toggleTorrent', infoHash)} />
)
}
@@ -149,7 +147,7 @@ module.exports = class TorrentList extends React.Component {
}
}
return (
<div key='progress-bar' style={styles.wrapper}>
<div style={styles.wrapper}>
<LinearProgress style={styles.progress} mode='determinate' value={progress} />
</div>
)
@@ -202,7 +200,7 @@ module.exports = class TorrentList extends React.Component {
const minutesStr = (hours || minutes) ? minutes + 'm' : ''
const secondsStr = seconds + 's'
return (<span key='eta'>{hoursStr} {minutesStr} {secondsStr} remaining</span>)
return (<span>{hoursStr} {minutesStr} {secondsStr} remaining</span>)
}
function renderTorrentStatus () {
@@ -212,15 +210,13 @@ module.exports = class TorrentList extends React.Component {
else if (torrentSummary.progress.progress === 1) status = 'Not seeding'
else status = 'Paused'
} else if (torrentSummary.status === 'downloading') {
if (!torrentSummary.progress) status = ''
else if (!torrentSummary.progress.ready) status = 'Verifying'
else status = 'Downloading'
status = 'Downloading'
} else if (torrentSummary.status === 'seeding') {
status = 'Seeding'
} else { // torrentSummary.status is 'new' or something unexpected
status = ''
}
return (<span key='torrent-status'>{status}</span>)
return (<span>{status}</span>)
}
}
@@ -236,9 +232,8 @@ module.exports = class TorrentList extends React.Component {
<i
key='play-button'
title='Start streaming'
className='icon play'
onClick={dispatcher('playFile', infoHash)}
>
className={'icon play'}
onClick={dispatcher('playFile', infoHash)}>
play_circle_outline
</i>
)
@@ -251,8 +246,7 @@ module.exports = class TorrentList extends React.Component {
key='delete-button'
className='icon delete'
title='Remove torrent'
onClick={dispatcher('confirmDeleteTorrent', infoHash, false)}
>
onClick={dispatcher('confirmDeleteTorrent', infoHash, false)}>
close
</i>
</div>
@@ -284,17 +278,10 @@ module.exports = class TorrentList extends React.Component {
)
} else {
// We do know the files. List them and show download stats for each one
const sortByName = this.props.state.saved.prefs.sortByName
const collator = new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' })
let fileRows = torrentSummary.files
const fileRows = torrentSummary.files
.filter((file) => !file.path.includes('/.____padding_file/'))
.map((file, index) => ({ file, index }))
if (sortByName) {
fileRows = fileRows.sort((a, b) => collator.compare(a.file.name, b.file.name))
}
fileRows = fileRows.map((obj) => this.renderFileRow(torrentSummary, obj.file, obj.index))
.map((object) => this.renderFileRow(torrentSummary, object.file, object.index))
filesElement = (
<div key='files' className='files'>
@@ -325,7 +312,7 @@ module.exports = class TorrentList extends React.Component {
torrentSummary.progress.files[index]) {
const fileProg = torrentSummary.progress.files[index]
isDone = fileProg.numPiecesPresent === fileProg.numPieces
progress = Math.floor(100 * 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
@@ -346,7 +333,7 @@ module.exports = class TorrentList extends React.Component {
} else {
icon = 'description' /* file icon, opens in OS default app */
handleClick = isDone
? dispatcher('openPath', infoHash, index)
? dispatcher('openItem', infoHash, index)
: (e) => e.stopPropagation() // noop if file is not ready
}
// TODO: add a css 'disabled' class to indicate that a file cannot be opened/streamed
@@ -368,10 +355,8 @@ module.exports = class TorrentList extends React.Component {
<td className={'col-size ' + rowClass}>
{prettyBytes(file.length)}
</td>
<td
className='col-select'
onClick={dispatcher('toggleTorrentFile', infoHash, index)}
>
<td className='col-select'
onClick={dispatcher('toggleTorrentFile', infoHash, index)}>
<i className='icon deselect-file'>{isSelected ? 'close' : 'add'}</i>
</td>
</tr>
@@ -380,21 +365,21 @@ module.exports = class TorrentList extends React.Component {
renderRadialProgressBar (fraction, cssClass) {
const rotation = 360 * fraction
const transformFill = { transform: 'rotate(' + (rotation / 2) + 'deg)' }
const transformFix = { transform: 'rotate(' + rotation + 'deg)' }
const transformFill = {transform: 'rotate(' + (rotation / 2) + 'deg)'}
const transformFix = {transform: 'rotate(' + rotation + 'deg)'}
return (
<div key='radial-progress' className={'radial-progress ' + cssClass}>
<div className='circle'>
<div className='mask full' style={transformFill}>
<div className='fill' style={transformFill} />
<div key='circle' className='circle'>
<div key='mask-full' className='mask full' style={transformFill}>
<div key='fill' className='fill' style={transformFill} />
</div>
<div className='mask half'>
<div className='fill' style={transformFill} />
<div className='fill fix' style={transformFix} />
<div key='mask-half' className='mask half'>
<div key='fill' className='fill' style={transformFill} />
<div key='fill-fix' className='fill fix' style={transformFix} />
</div>
</div>
<div className='inset' />
<div key='inset' className='inset' />
</div>
)
}
@@ -408,7 +393,7 @@ function getErrorMessage (torrentSummary) {
const err = torrentSummary.error
if (err === 'path-missing') {
return (
<span key='path-missing'>
<span>
Path missing.<br />
Fix and restart the app, or delete the torrent.
</span>

287
src/renderer/plugins.js Normal file
View File

@@ -0,0 +1,287 @@
const {resolve, basename} = require('path')
const notifier = require('node-notifier')
const crypto = require('crypto')
const config = require('../config')
module.exports = class Plugins {
constructor () {
// modules path
this.path = resolve(config.getConfigPath(), 'plugins')
log('Path: ', this.path)
this.availableExtensions = [
'onCheckForSubtitles'
]
this.forceUpdate = false
this.updating = false
this.watchers = []
this.state = {}
}
init (params) {
this.state = params.state
// initialize state
this.state.saved = Object.assign(this.state.saved || {})
// caches
this.plugins = config.getPlugins()
this.paths = this.getPaths(this.plugins)
this.id = this.getId(this.plugins)
this.modules = this.requirePlugins()
// TODO: fire an event when plugins finish updating and listen to that.
// Listen to plugin changes on config.
// New plugins added, plugins removed or updated.
// The actual plugin update action will take place in the MAIN process.
config.subscribe(() => {
const plugins = config.getPlugins()
if (plugins !== this.plugins) {
const id = this.getId(plugins)
if (this.id !== id) {
log('UPDATING...')
this.id = id
this.plugins = plugins
this.paths = this.getPaths(this.plugins)
}
}
})
// Plugins will be updated on the MAIN process after 5s.
if (this.needsUpdate()) {
setTimeout(() => {
this.init(params)
}, 6000)
log('Plugins need update, init scheduled')
return
}
this.loadPlugins()
this.initPlugins(params)
}
initPlugins (params) {
this.modules.forEach(plugin => {
if (plugin.init) {
plugin.init(params)
}
})
}
on (action, params) {
log(`ON ${action}:`, params)
this.modules.forEach(plugin => {
const actionName = this.capitalizeFirstLetter(action)
const methodName = `on${actionName}`
if (typeof plugin[methodName] === 'function') {
plugin[methodName](params)
}
})
}
capitalizeFirstLetter (string) {
return string.charAt(0).toUpperCase() + string.slice(1)
}
didPluginsChange () {
return this.state.saved.installedPlugins !== this.id
}
hasPlugins () {
return !this.isEmptyObject(this.plugins)
}
isFirstInstall () {
return (!this.state.saved.installedPlugins && this.hasPlugins())
}
needsUpdate () {
return (this.didPluginsChange() || this.isFirstInstall())
}
getId (plugins) {
const hash = crypto.createHash('sha256')
hash.update(JSON.stringify(plugins))
return hash.digest('hex')
}
loadPlugins (err, localOnly = false) {
this.updating = false
// handle errors first
if (err) {
console.error(err.stack)
if (/not a recognized/.test(err.message) || /command not found/.test(err.message)) {
this.alert(
'Error updating plugins: We could not find the "npm" command. Make sure it\'s in $PATH'
)
return
}
this.alert(`Error updating plugins: Check '${this.path}/npm-debug.log' for more information.`)
return
}
// update state with latest plugins
this.state.saved.plugins = this.plugins
// cache modules
this.modules = this.requirePlugins()
// clear require cache
this.clearCache()
}
getPluginVersions () {
const paths_ = this.paths.plugins
return paths_.map(path => {
let version = null
try {
// eslint-disable-next-line import/no-dynamic-require
version = require(resolve(path, 'package.json')).version
} catch (err) { }
return [
basename(path),
version
]
})
}
clearCache () {
// clear require cache
for (const entry in require.cache) {
if (entry.indexOf(this.path) === 0) {
delete require.cache[entry]
}
}
}
isEmptyObject (obj) {
return (Object.keys(obj).length === 0)
}
alert (message) {
notifier.notify({
title: 'WebTorrent Plugins',
// icon: config.icon, // TODO: save icon in webtorrent local folder and set config.icon
message: message
})
}
subscribe (fn) {
this.watchers.push(fn)
return () => {
this.watchers.splice(this.watchers.indexOf(fn), 1)
}
}
isLocalPath (string) {
// matches unix and windows local paths
return string.match(/^(\/|[a-z]:\/)/i)
}
getPaths (plugins) {
const pluginNames = Object.keys(plugins)
return {
plugins: pluginNames.map(name => {
let url = plugins[name]
// plugin is already on a local folder
// directly load it from its current location
if (this.isLocalPath(url)) return url
// plugin will be installed with npm install from a remote url
return resolve(this.path, 'node_modules', name.split('#')[0])
})
}
}
exposesSupportedApi (plugin) {
if (!plugin) return false
return this.availableExtensions.some((methodName) => {
return (typeof plugin[methodName] === 'function')
})
}
requirePlugins () {
const {plugins} = this.paths
let installNeeded = false
const load = (path) => {
if (!path.match(/\/$/)) {
path += '/'
}
const rendererPath = `${path}renderer.js`
try {
const Plugin = require(rendererPath) // eslint-disable import/no-dynamic-require
const plugin = new Plugin()
const exposed = this.exposesSupportedApi(plugin)
if (!exposed) {
log('Plugin not exposing any available extensions.', rendererPath, Object.keys(plugin))
return
}
// populate the name for internal errors here
plugin._name = basename(rendererPath)
return plugin
} catch (err) {
log('Require plugins ERROR:', err)
this.alert(`Error loading plugin: ${rendererPath}`)
// plugin not installed
// node_modules removed? did a manual plugin uninstall?
// try installing and then loading if successfull
installNeeded = true
}
}
// Plugin installation happens on the MAIN process.
// If plugins haven't finished installing, wait for them.
if (installNeeded) {
log('Plugins install needed, wait...')
setTimeout(() => {
this.requirePlugins()
}, 3000)
}
return plugins.map(load).filter(v => Boolean(v))
}
// decorates the base object by calling plugin[key]
// for all the available plugins
decorateObject (base, key) {
let decorated = base
this.modules.forEach(plugin => {
if (plugin[key]) {
const res = plugin[key](decorated)
if (res && typeof res === 'object') {
decorated = res
} else {
this.alert(`Plugin error: "${plugin._name}": invalid return type for \`${key}\``)
}
}
})
return decorated
}
}
/**
* Logs passed arguments to console using a prefix.
*
*/
function log () {
const prefix = '[ PLUGINS.Renderer ]-->'
const args = [prefix]
for (var i = 0; i < arguments.length; ++i) {
args.push(arguments[i])
}
console.log.apply(console, args)
}

View File

@@ -3,18 +3,20 @@
console.time('init')
const crypto = require('crypto')
const util = require('util')
const deepEqual = require('deep-equal')
const defaultAnnounceList = require('create-torrent').announceList
const electron = require('electron')
const fs = require('fs')
const mm = require('music-metadata')
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')
const crashReporter = require('../crash-reporter')
const config = require('../config')
const { TorrentKeyNotFoundError } = require('./lib/errors')
const {TorrentKeyNotFoundError} = require('./lib/errors')
const torrentPoster = require('./lib/torrent-poster')
// Report when the process crashes
@@ -39,9 +41,10 @@ const VERSION = require('../../package.json').version
* '0.16.1' -> '0016'
* '1.2.5' -> '0102'
*/
const VERSION_STR = VERSION
.replace(/\d*./g, v => `0${v % 100}`.slice(-2))
.slice(0, 4)
const VERSION_STR = VERSION.match(/([0-9]+)/g)
.slice(0, 2)
.map((v) => zeroFill(2, v))
.join('')
/**
* Version prefix string (used in peer ID). WebTorrent uses the Azureus-style
@@ -94,8 +97,8 @@ function init () {
ipc.send('ipcReadyWebTorrent')
window.addEventListener('error', (e) =>
ipc.send('wt-uncaught-error', { message: e.error.message, stack: e.error.stack }),
true)
ipc.send('wt-uncaught-error', {message: e.error.message, stack: e.error.stack}),
true)
setInterval(updateTorrentProgress, 1000)
console.timeEnd('init')
@@ -112,8 +115,8 @@ function startTorrenting (torrentKey, torrentID, path, fileModtimes, selections)
console.log('starting torrent %s: %s', torrentKey, torrentID)
const torrent = client.add(torrentID, {
path,
fileModtimes
path: path,
fileModtimes: fileModtimes
})
torrent.key = torrentKey
@@ -146,7 +149,7 @@ function addTorrentEvents (torrent) {
torrent.on('error', (err) =>
ipc.send('wt-error', torrent.key, err.message))
torrent.on('infoHash', () =>
ipc.send('wt-parsed', torrent.key, torrent.infoHash, torrent.magnetURI))
ipc.send('wt-infohash', torrent.key, torrent.infoHash))
torrent.on('metadata', torrentMetadata)
torrent.on('ready', torrentReady)
torrent.on('done', torrentDone)
@@ -215,7 +218,7 @@ function saveTorrentFile (torrentKey) {
}
// Otherwise, save the .torrent file, under the app config folder
fs.mkdir(config.TORRENT_PATH, { recursive: true }, function (_) {
mkdirp(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)
@@ -232,7 +235,7 @@ function generateTorrentPoster (torrentKey) {
torrentPoster(torrent, function (err, buf, extension) {
if (err) return console.log('error generating poster: %o', err)
// save it for next time
fs.mkdir(config.POSTER_PATH, { recursive: true }, function (err) {
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)
@@ -248,7 +251,7 @@ function generateTorrentPoster (torrentKey) {
function updateTorrentProgress () {
const progress = getTorrentProgress()
// TODO: diff torrent-by-torrent, not once for the whole update
if (prevProgress && util.isDeepStrictEqual(progress, prevProgress)) {
if (prevProgress && deepEqual(progress, prevProgress, {strict: true})) {
return /* don't send heavy object if it hasn't changed */
}
ipc.send('wt-progress', progress)
@@ -317,8 +320,7 @@ function startServerFromReadyTorrent (torrent, cb) {
const info = {
torrentKey: torrent.key,
localURL: 'http://localhost' + urlSuffix,
networkURL: 'http://' + networkAddress() + urlSuffix,
networkAddress: networkAddress()
networkURL: 'http://' + networkAddress() + urlSuffix
}
ipc.send('wt-server-running', info)
@@ -332,43 +334,16 @@ function stopServer () {
server = null
}
console.log('Initializing...')
function getAudioMetadata (infoHash, index) {
const torrent = client.get(infoHash)
const file = torrent.files[index]
// Set initial matadata to display the filename first.
const metadata = { title: file.name }
ipc.send('wt-audio-metadata', infoHash, index, metadata)
const options = {
native: false,
skipCovers: true,
fileSize: file.length,
observer: event => {
ipc.send('wt-audio-metadata', infoHash, index, event.metadata)
}
}
const onMetadata = file.done
// If completed; use direct file access
? mm.parseFile(path.join(torrent.path, file.path), options)
// otherwise stream
: mm.parseStream(file.createReadStream(), file.name, options)
onMetadata
.then(
metadata => {
ipc.send('wt-audio-metadata', infoHash, index, metadata)
console.log(`metadata for file='${file.name}' completed.`)
},
err => {
console.log(
`error getting audio metadata for ${infoHash}:${index}`,
err
)
}
)
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)
})
}
function selectFiles (torrentOrInfoHash, selections) {

View File

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

4
static/linux/postinst Normal file
View File

@@ -0,0 +1,4 @@
#!/bin/sh
set -e
chmod +x /opt/webtorrent-desktop/WebTorrent
ln -s -f /opt/webtorrent-desktop/WebTorrent /usr/bin/webtorrent-desktop

3
static/linux/prerm Normal file
View File

@@ -0,0 +1,3 @@
#!/bin/sh
set -e
rm /usr/bin/webtorrent-desktop

View File

@@ -0,0 +1,33 @@
[Desktop Entry]
Name=$APP_NAME
Version=1.0
GenericName=BitTorrent Client
X-GNOME-FullName=$APP_NAME
Comment=Download and share files over BitTorrent
Encoding=UTF-8
Type=Application
Icon=webtorrent-desktop
Terminal=false
Path=$APP_PATH
Exec=$EXEC_PATH %U
TryExec=$TRY_EXEC_PATH
StartupNotify=false
Categories=Network;FileTransfer;P2P;
MimeType=application/x-bittorrent;x-scheme-handler/magnet;x-scheme-handler/stream-magnet;
Actions=CreateNewTorrent;OpenTorrentFile;OpenTorrentAddress;
[Desktop Action CreateNewTorrent]
Name=Create New Torrent...
Exec=$EXEC_PATH -n
Path=$APP_PATH
[Desktop Action OpenTorrentFile]
Name=Open Torrent File...
Exec=$EXEC_PATH -o
Path=$APP_PATH
[Desktop Action OpenTorrentAddress]
Name=Open Torrent Address...
Exec=$EXEC_PATH -u
Path=$APP_PATH

View File

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

View File

@@ -615,7 +615,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 {
@@ -623,11 +622,6 @@ body.drag .app::after {
margin-top: 6px;
}
.player .controls .icon.multi-audio {
font-size: 26px;
margin-top: 6px;
}
.player .controls .icon.fullscreen {
font-size: 26px;
margin-right: 15px;
@@ -806,23 +800,6 @@ video::-webkit-media-text-track-container {
width: 40px;
height: 40px;
margin: 40px auto;
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: spinning 1.1s infinite linear;
}
@keyframes spinning {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.media-stalled .loading-status {
@@ -842,17 +819,12 @@ video::-webkit-media-text-track-container {
.audio-metadata label {
display:inline-block;
width: 120px;
width: 100px;
text-align: right;
font-weight: normal;
margin-right: 25px;
}
.audio-metadata .audio-format,
.audio-metadata .audio-comments {
font-weight: normal;
}
/*
* ERRORS
*/

View File

@@ -10,7 +10,7 @@ test('app runs', function (t) {
setup.waitForLoad(app, t)
.then(() => setup.screenshotCreateOrCompare(app, t, 'app-basic'))
.then(() => setup.endTest(app, t),
(err) => setup.endTest(app, t, err || 'error'))
(err) => setup.endTest(app, t, err || 'error'))
})
require('./test-torrent-list')

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 966 KiB

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 899 KiB

After

Width:  |  Height:  |  Size: 1011 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 115 KiB

After

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 351 KiB

After

Width:  |  Height:  |  Size: 302 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 630 KiB

After

Width:  |  Height:  |  Size: 698 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 378 KiB

After

Width:  |  Height:  |  Size: 479 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 380 KiB

After

Width:  |  Height:  |  Size: 480 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 380 KiB

After

Width:  |  Height:  |  Size: 480 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 380 KiB

After

Width:  |  Height:  |  Size: 480 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 381 KiB

After

Width:  |  Height:  |  Size: 481 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 116 KiB

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 904 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 681 KiB

After

Width:  |  Height:  |  Size: 627 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 746 KiB

After

Width:  |  Height:  |  Size: 844 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1003 KiB

After

Width:  |  Height:  |  Size: 1.1 MiB

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