From 2eff5e54c071067743b24a9f0281ededfdbd9f92 Mon Sep 17 00:00:00 2001 From: Omar Najjar Date: Tue, 30 Apr 2024 20:33:17 +1000 Subject: [PATCH] Initial commit --- .gitattributes | 2 + 404.html | 1 + CNAME | 1 + LICENSE | 21 +++ README.md | 79 ++++++++++ app.js | 373 ++++++++++++++++++++++++++++++++++++++++++++ favicon.ico | Bin 0 -> 1927 bytes index.html | 36 +++++ style.css | 87 +++++++++++ views/download.html | 88 +++++++++++ views/full.html | 64 ++++++++ views/view.html | 20 +++ 12 files changed, 772 insertions(+) create mode 100644 .gitattributes create mode 100644 404.html create mode 100644 CNAME create mode 100644 LICENSE create mode 100644 README.md create mode 100644 app.js create mode 100644 favicon.ico create mode 100644 index.html create mode 100644 style.css create mode 100644 views/download.html create mode 100644 views/full.html create mode 100644 views/view.html diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..dfe0770 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/404.html b/404.html new file mode 100644 index 0000000..64233a9 --- /dev/null +++ b/404.html @@ -0,0 +1 @@ +index.html \ No newline at end of file diff --git a/CNAME b/CNAME new file mode 100644 index 0000000..e246f7d --- /dev/null +++ b/CNAME @@ -0,0 +1 @@ +btorrent.xyz \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e1c1ba3 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2015 Diego Rodríguez Baquero + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..5be5e46 --- /dev/null +++ b/README.md @@ -0,0 +1,79 @@ +βTorrent +======== +**[βTorrent]** is a fully-featured **[WebTorrent]** browser client written in HTML, JS and CSS + +### Features +- [x] Informative GUI with easy sharing options +- [x] Downloading from an info hash or magnet URI +- [x] Downloading from a .torrent file (Coming Soon) +- [x] Seeding files (Single/multiple files) +- [ ] Seeding CORS-enabled remote files (Coming Soon) +- [x] Download/Upload speed per torrent +- [x] Download/Upload speed of client (All torrents) +- [x] Removing torrents from the client +- [x] Pause/Resume torrent +- [x] Selecting/Deselecting files (Coming Soon) +- [x] Client Debugging +- [ ] Use custom trackers/rtcConfig + +### Built with +- [WebTorrent] +- [AngularJS] +- [Skeleton] +- [Normalize.css] +- [Moment.js] +- [ui-grid] +- [pretty-bytes] +- [ng-file-upload] +- [ng-notify] + +Website powered by [jsDelivr] and [CloudFlare]. I use [nginx] in my server. + +### HTML5 serving +**You must serve index.html as the default** + +For nginx, use this conf: +``` + location / { + try_files $uri$args $uri$args/ /index.html; + } +``` + +### Enable Debugging +Enable βTorrent (Debug logging) and WebTorrent (Logs logging) debug logs by running this in the developer console: +```js +localStorage.debug = '*' +``` +Disable by running this: +```js +localStorage.removeItem('debug') +``` + +### Help βTorrent +- **[Create a new issue](https://github.com/DiegoRBaquero/bTorrent/issues/new)** to report bugs or suggest new features +- **[Send a PR](https://github.com/DiegoRBaquero/BTorrent/pull/new/master)** with your changes + +### Thanks +- [jasalo](https://github.com/jasalo) For the logo and favicon +- [whitef0x0](https://github.com/whitef0x0) For cleanup and ng-file-upload and other ideas + +### License +MIT. Copyright (c) [Diego Rodríguez Baquero](https://diegorbaquero.com) + +[βTorrent]: https://btorrent.xyz +[WebTorrent]: https://webtorrent.io +[AngularJS]: https://angularjs.org/ +[Skeleton]: http://getskeleton.com/ +[Normalize.css]: https://necolas.github.io/normalize.css/ +[Moment.js]: http://momentjs.com/ +[ui-grid]: http://ui-grid.info/ +[pretty-bytes]: https://github.com/sindresorhus/pretty-bytes +[ng-file-upload]: https://github.com/danialfarid/ng-file-upload +[ng-notify]: https://github.com/matowens/ng-notify +[Jade]: http://jade-lang.com/ +[CoffeeScript]: http://coffeescript.org/ +[Sass]: http://sass-lang.com/ +[Harp]: http://harpjs.com/ +[jsDelivr]: https://www.jsdelivr.com/ +[CloudFlare]: https://www.cloudflare.com/ +[nginx]: http://nginx.org/ diff --git a/app.js b/app.js new file mode 100644 index 0000000..c3b648a --- /dev/null +++ b/app.js @@ -0,0 +1,373 @@ +/* global WebTorrent, angular, moment, prompt */ + +const VERSION = '1.1' +const trackers = ['wss://tracker.btorrent.xyz', 'wss://tracker.openwebtorrent.com'] +const rtcConfig = { + 'iceServers': [ + { + 'urls': ['stun:stun.l.google.com:19305', 'stun:stun1.l.google.com:19305'] + } + ] +} + +const torrentOpts = { + announce: trackers +} + +const trackerOpts = { + announce: trackers, + rtcConfig: rtcConfig +} + +const debug = window.localStorage.getItem('debug') !== null + +const dbg = function (string, item, color) { + color = color !== null ? color : '#333333' + if (debug) { + if (item && item.name) { + return console.debug(`%cβTorrent:${item.infoHash !== null ? 'torrent ' : 'torrent ' + item._torrent.name + ':file '}${item.name}${item.infoHash !== null ? ' (' + item.infoHash + ')' : ''} %c${string}`, 'color: #33C3F0', `color: ${color}`) + } else { + return console.debug(`%cβTorrent:client %c${string}`, 'color: #33C3F0', `color: ${color}`) + } + } +} + +const er = function (err, item) { dbg(err, item, '#FF0000') } + +dbg(`Starting v${VERSION}. WebTorrent ${WebTorrent.VERSION}`) + +const client = new WebTorrent({ + tracker: trackerOpts +}) + +const app = angular.module('BTorrent', + ['ngRoute', 'ui.grid', 'ui.grid.resizeColumns', 'ui.grid.selection', 'ngFileUpload', 'ngNotify'], + ['$compileProvider', '$locationProvider', '$routeProvider', function ($compileProvider, $locationProvider, $routeProvider) { + $compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|magnet|blob|javascript):/) + $locationProvider.html5Mode({ + enabled: true, + requireBase: false + }).hashPrefix('#') + $routeProvider.when('/view', { + templateUrl: 'views/view.html', + controller: 'ViewCtrl' + }).when('/download', { + templateUrl: 'views/download.html', + controller: 'DownloadCtrl' + }).otherwise({ + templateUrl: 'views/full.html', + controller: 'FullCtrl' + }) + }] +) + +app.controller('BTorrentCtrl', ['$scope', '$rootScope', '$http', '$log', '$location', 'ngNotify', function ($scope, $rootScope, $http, $log, $location, ngNotify) { + let updateAll + $rootScope.version = VERSION + $rootScope.webtorrentVersion = WebTorrent.VERSION + ngNotify.config({ + duration: 5000, + html: true + }) + if (!WebTorrent.WEBRTC_SUPPORT) { + $rootScope.disabled = true + ngNotify.set('Please use a WebRTC compatible browser', { + type: 'error', + sticky: true, + button: false + }) + } + $rootScope.client = client + updateAll = function () { + if ($rootScope.client.processing) { + return + } + $rootScope.$apply() + } + setInterval(updateAll, 500) + $rootScope.seedFiles = function (files) { + let name + if ((files != null) && files.length > 0) { + if (files.length === 1) { + dbg(`Seeding file ${files[0].name}`) + } else { + dbg(`Seeding ${files.length} files`) + name = prompt('Please name your torrent', 'My Awesome Torrent') || 'My Awesome Torrent' + torrentOpts.name = name + } + $rootScope.client.processing = true + $rootScope.client.seed(files, torrentOpts, $rootScope.onSeed) + delete torrentOpts.name + } + } + $rootScope.openTorrentFile = function (file) { + if (file != null) { + dbg(`Adding torrent file ${file.name}`) + $rootScope.client.processing = true + $rootScope.client.add(file, torrentOpts, $rootScope.onTorrent) + } + } + $rootScope.client.on('error', function (err, torrent) { + $rootScope.client.processing = false + ngNotify.set(err, 'error') + er(err, torrent) + }) + $rootScope.addMagnet = function (magnet, onTorrent) { + if ((magnet != null) && magnet.length > 0) { + dbg(`Adding magnet/hash ${magnet}`) + $rootScope.client.processing = true + $rootScope.client.add(magnet, torrentOpts, onTorrent || $rootScope.onTorrent) + } + } + $rootScope.destroyedTorrent = function (err) { + if (err) { + throw err + } + dbg('Destroyed torrent', $rootScope.selectedTorrent) + $rootScope.selectedTorrent = null + $rootScope.client.processing = false + } + $rootScope.changePriority = function (file) { + if (file.priority === '-1') { + dbg('Deselected', file) + file.deselect() + } else { + dbg(`Selected with priority ${file.priority}`, file) + file.select(file.priority) + } + } + $rootScope.onTorrent = function (torrent, isSeed) { + dbg(torrent.magnetURI) + torrent.safeTorrentFileURL = torrent.torrentFileBlobURL + torrent.fileName = `${torrent.name}.torrent` + if (!isSeed) { + dbg('Received metadata', torrent) + ngNotify.set(`Received ${torrent.name} metadata`) + if (!($rootScope.selectedTorrent != null)) { + $rootScope.selectedTorrent = torrent + } + $rootScope.client.processing = false + } + torrent.files.forEach(function (file) { + file.getBlobURL(function (err, url) { + if (err) { + throw err + } + if (isSeed) { + dbg('Started seeding', torrent) + if (!($rootScope.selectedTorrent != null)) { + $rootScope.selectedTorrent = torrent + } + $rootScope.client.processing = false + } + file.url = url + if (!isSeed) { + dbg('Done ', file) + ngNotify.set(`${file.name} ready for download`, 'success') + } + }) + }) + torrent.on('done', function () { + if (!isSeed) { + dbg('Done', torrent) + } + ngNotify.set(`${torrent.name} has finished downloading`, 'success') + }) + torrent.on('wire', function (wire, addr) { dbg(`Wire ${addr}`, torrent) }) + torrent.on('error', er) + } + $rootScope.onSeed = function (torrent) { $rootScope.onTorrent(torrent, true) } + dbg('Ready') +} +]) + +app.controller('FullCtrl', ['$scope', '$rootScope', '$http', '$log', '$location', 'ngNotify', function ($scope, $rootScope, $http, $log, $location, ngNotify) { + ngNotify.config({ + duration: 5000, + html: true + }) + $scope.addMagnet = function () { + $rootScope.addMagnet($scope.torrentInput) + $scope.torrentInput = '' + } + $scope.columns = [ + { + field: 'name', + cellTooltip: true, + minWidth: '200' + }, { + field: 'length', + name: 'Size', + cellFilter: 'pbytes', + width: '80' + }, { + field: 'received', + displayName: 'Downloaded', + cellFilter: 'pbytes', + width: '135' + }, { + field: 'downloadSpeed', + displayName: '↓ Speed', + cellFilter: 'pbytes:1', + width: '100' + }, { + field: 'progress', + displayName: 'Progress', + cellFilter: 'progress', + width: '100' + }, { + field: 'timeRemaining', + displayName: 'ETA', + cellFilter: 'humanTime', + width: '140' + }, { + field: 'uploaded', + displayName: 'Uploaded', + cellFilter: 'pbytes', + width: '125' + }, { + field: 'uploadSpeed', + displayName: '↑ Speed', + cellFilter: 'pbytes:1', + width: '100' + }, { + field: 'numPeers', + displayName: 'Peers', + width: '80' + }, { + field: 'ratio', + cellFilter: 'number:2', + width: '80' + } + ] + $scope.gridOptions = { + columnDefs: $scope.columns, + data: $rootScope.client.torrents, + enableColumnResizing: true, + enableColumnMenus: false, + enableRowSelection: true, + enableRowHeaderSelection: false, + multiSelect: false + } + $scope.gridOptions.onRegisterApi = function (gridApi) { + $scope.gridApi = gridApi + gridApi.selection.on.rowSelectionChanged($scope, function (row) { + if (!row.isSelected && ($rootScope.selectedTorrent != null) && ($rootScope.selectedTorrent.infoHash = row.entity.infoHash)) { + $rootScope.selectedTorrent = null + } else { + $rootScope.selectedTorrent = row.entity + } + }) + } + if ($location.hash() !== '') { + $rootScope.client.processing = true + setTimeout(function () { + dbg(`Adding ${$location.hash()}`) + $rootScope.addMagnet($location.hash()) + }, 0) + } +} +]) + +app.controller('DownloadCtrl', ['$scope', '$rootScope', '$http', '$log', '$location', 'ngNotify', function ($scope, $rootScope, $http, $log, $location, ngNotify) { + ngNotify.config({ + duration: 5000, + html: true + }) + $scope.addMagnet = function () { + $rootScope.addMagnet($scope.torrentInput) + $scope.torrentInput = '' + } + if ($location.hash() !== '') { + $rootScope.client.processing = true + setTimeout(function () { + dbg(`Adding ${$location.hash()}`) + $rootScope.addMagnet($location.hash()) + }, 0) + } +} +]) + +app.controller('ViewCtrl', ['$scope', '$rootScope', '$http', '$log', '$location', 'ngNotify', function ($scope, $rootScope, $http, $log, $location, ngNotify) { + let onTorrent + ngNotify.config({ + duration: 2000, + html: true + }) + onTorrent = function (torrent) { + $rootScope.viewerStyle = { + 'margin-top': '-20px', + 'text-align': 'center' + } + dbg(torrent.magnetURI) + torrent.safeTorrentFileURL = torrent.torrentFileBlobURL + torrent.fileName = `${torrent.name}.torrent` + $rootScope.selectedTorrent = torrent + $rootScope.client.processing = false + dbg('Received metadata', torrent) + ngNotify.set(`Received ${torrent.name} metadata`) + torrent.files.forEach(function (file) { + file.appendTo('#viewer') + file.getBlobURL(function (err, url) { + if (err) { + throw err + } + file.url = url + dbg('Done ', file) + }) + }) + torrent.on('done', function () { dbg('Done', torrent) }) + torrent.on('wire', function (wire, addr) { dbg(`Wire ${addr}`, torrent) }) + torrent.on('error', er) + } + $scope.addMagnet = function () { + $rootScope.addMagnet($scope.torrentInput, onTorrent) + $scope.torrentInput = '' + } + if ($location.hash() !== '') { + $rootScope.client.processing = true + setTimeout(function () { + dbg(`Adding ${$location.hash()}`) + $rootScope.addMagnet($location.hash(), onTorrent) + }, 0) + } +} +]) + +app.filter('html', [ + '$sce', function ($sce) { + return function (input) { + $sce.trustAsHtml(input) + } + } +]) + +app.filter('pbytes', function () { + return function (num, speed) { + let exponent, unit, units + if (isNaN(num)) { + return '' + } + units = ['B', 'kB', 'MB', 'GB', 'TB'] + if (num < 1) { + return (speed ? '' : '0 B') + } + exponent = Math.min(Math.floor(Math.log(num) / 6.907755278982137), 8) + num = (num / Math.pow(1000, exponent)).toFixed(1) * 1 + unit = units[exponent] + return `${num} ${unit}${speed ? '/s' : ''}` + } +}) + +app.filter('humanTime', function () { + return function (millis) { + let remaining + if (millis < 1000) { + return '' + } + remaining = moment.duration(millis).humanize() + return remaining[0].toUpperCase() + remaining.substr(1) + } +}) + +app.filter('progress', function () { return function (num) { return `${(100 * num).toFixed(1)}%` } }) diff --git a/favicon.ico b/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..2e580174dbdb839a0c64262e1d6867ee175be653 GIT binary patch literal 1927 zcmV;22YC32P)b zotZQDoOkccoR51BeMVb92wVzW2CM*vfJMOhz+mV$0h|EFfid7c;2`j}Gisur_jgV| zglg+A0KNwd1J?laD~kFQcm>!4>~cmOYvvA5)0hFY^#yPfumSi6P&6ewFa^8>>;(Sh zjGAgfXl(;%>j!`z0h@tkweKj6QD7VJ7iZLD9YM7Wpsl|S*a2KpM@|ji1vWXO_Ei;7 z$pG4Vg(!Yo)rlMCPlzFW?!asVXzQ;4o&lC*4*djK*++rnz-Qt5dB9TOi@Lf)Br*G?ZCZ>lOG0dc1FFO*grP^w}7XS zgHsN_2JVX)A>|RbOtsesZNB)y=r!(r0y&8KOKwJM+;J?7Ugh{4=RVgcQc%_yB7lv`ifYFrqHM|bH z6Yi&zQ^2*(sMotSb~S*uegW_n;<%*nq%&%LSqE+XeB`uv7vc-c)qMLytg-G z$h#)2KV^($$ZJsmJA#hWE_SjOSeYsK)ku5k+R%eFk@oj1$OO@CZRdry0fJ>VX9}F4 zkd7SQ&>ZmcP^8&zY0u~q&{73H?LG3eZW0&XJR#hnZGMA~O< zXPFElI=2llTvK3^j5wqA_iEJEUqyZ}rW?ZmMOYD6Hz2SH#(_H{eKs{T*J^EjQ6P>< z-9@DdTCVhwULC?oZf&pjOcnym2FO47G|3pSANU*aVwp#%tzQKEx#9U&4iGW)uK>SkU|wG!iOREyOM%zA#qVaY4N17x;dlXj zP*X@hm=8SLa#_0(5-|U$InM_LvIw1xECwEk^eGLdu}UoSQ$%MmfHUe7U{?h}bHR9|y^1rpE9gA!D7mwOpt<01q&=2R z?nYbJP6O-@e`gb;&ZrZ;8bd05L8i=UdbSM^c7xJ9{~6$gNPCt1z$2Zp$C=*LUaF;@ zd+{OhG*>SsfZs;?#-7J(eN36lc`mf$tz6M5OXK@QX;FyU5EtKh~{$uU9}}3sTqH2%*+*1@Jq{ zUD%_*t8ksN_SBE_j3M0A=(S()52q%N;)pFQG?`a;Hypsk+=T!j=yx58Q{5U1=w zN@z@5|1{~9>K>_I?3!_o6-ur_j*Ptu60!|Y7j6AdfF}~WPXga|M(yp@p){rz25v~d zz5b%D{~>t?whd*vAX{F`1s_DBKSRLf$PqW! z%XVdyTygwi + + + + + βTorrent: Browser WebTorrent Client + + + + + + + + + + + +
+

βTorrent v{{$root.version}}

+ +
+
+
+
+
+ +
+ + + diff --git a/style.css b/style.css new file mode 100644 index 0000000..caec711 --- /dev/null +++ b/style.css @@ -0,0 +1,87 @@ +body { + height: 100%; + width: 100%; } + +header, footer, .center { + text-align: center; } + +th, td { + padding: 2px 15px; + max-width: 200px; + overflow: auto; + white-space: nowrap; } + +h2, h3, h4, h5, h6, ul, li { + margin-bottom: 0; } + +footer { + margin-top: 10px; } + +.container { + width: 95%; + max-width: 95%; } + +.grid { + margin-bottom: 20px; + width: 100%; + height: 200px; } + +.version { + color: #ccc; + font-size: 0.3em; } + +.views { + margin-top: -20px; + margin-bottom: 10px; } + +.download-button { + margin-left: 10px; } + +.no-margin { + margin: 0px; } + +.spinner { + position: absolute; + top: 30%; + left: 30%; + width: 40%; + height: 40%; + z-index: 1000; + background-color: grey; + border-radius: 25px; + opacity: .8; + text-align: center; } + +.spinner-icon { + position: relative; + top: 50%; + margin-top: -100px; + font-size: 200px; + transform: translateY(-50%); } + +.button.button-danger, +button.button-danger, +input[type="submit"].button-danger, +input[type="reset"].button-danger, +input[type="button"].button-danger { + color: #FFF; + background-color: #D9534F; + border-color: #D9534F; } + +.button.button-danger:hover, +button.button-danger:hover, +input[type="submit"].button-danger:hover, +input[type="reset"].button-danger:hover, +input[type="button"].button-danger:hover, +.button.button-danger:focus, +button.button-danger:focus, +input[type="submit"].button-danger:focus, +input[type="reset"].button-danger:focus, +input[type="button"].button-danger:focus { + color: #FFF; + background-color: #C43E3A; + border-color: #C43E3A; } + +a { + text-decoration: none +} diff --git a/views/download.html b/views/download.html new file mode 100644 index 0000000..fde09c1 --- /dev/null +++ b/views/download.html @@ -0,0 +1,88 @@ + +
+
+
+
+ + +
+ +
+
+ + +
+
+
+
+

Information

+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
Name{{$root.selectedTorrent.name}}
Size{{$root.selectedTorrent.length | pbytes}}
Completed{{$root.selectedTorrent.progress | progress}} ({{$root.selectedTorrent.downloaded | pbytes}})
Peers{{$root.selectedTorrent.numPeers}}
↓ Speed{{$root.selectedTorrent.downloadSpeed | pbytes:1}}
ETA{{$root.selectedTorrent.timeRemaining | humanTime}}
+
+
+

Files

+ + + + + + + + + + + + + + + + +
NameSizePriority
{{file.name}}{{file.name}}{{file.length | pbytes}} + +
+
↑ Click a file to download it
+
+
+

Share

+ +
+
+
\ No newline at end of file diff --git a/views/full.html b/views/full.html new file mode 100644 index 0000000..2d094ec --- /dev/null +++ b/views/full.html @@ -0,0 +1,64 @@ + +
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+
+
{{$root.selectedTorrent.name}} + + + +
+
Share
+ +
+
+
Files
+ + + + + + + + + + + + + + + + +
NameSizePriority
{{file.name}}{{file.name}}{{file.length | pbytes}} + +
+
+
+
Client Stats: + ↓ {{$root.client.downloadSpeed | pbytes}}/s · + ↑ {{$root.client.uploadSpeed | pbytes}}/s · + Ratio: {{$root.client.ratio | number:2}}
+
\ No newline at end of file diff --git a/views/view.html b/views/view.html new file mode 100644 index 0000000..040a902 --- /dev/null +++ b/views/view.html @@ -0,0 +1,20 @@ + +
+
+
+
+ + +
+ +
+
+ + +
+
+
Downloaded {{$root.selectedTorrent.downloaded | pbytes}}/{{$root.selectedTorrent.length | pbytes}} ({{$root.selectedTorrent.progress | progress}}) at {{$root.selectedTorrent.downloadSpeed | pbytes:1}} from {{$root.selectedTorrent.numPeers}} peers. ETA: {{$root.selectedTorrent.timeRemaining | humanTime}}
+
\ No newline at end of file