├── Procfile ├── .gitignore ├── views ├── css │ ├── styles.css │ └── flipclock.css ├── img │ ├── galaxy.jpg │ ├── trash.ico │ └── matterhorn.jpg ├── error.pug ├── index.pug ├── js │ └── flipclock.min.js └── dashboard.pug ├── data └── driveCredentials.json ├── .eslintrc.js ├── README.md ├── package.json └── server.js /Procfile: -------------------------------------------------------------------------------- 1 | web: node server.js -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /.idea 3 | push.bat 4 | -------------------------------------------------------------------------------- /views/css/styles.css: -------------------------------------------------------------------------------- 1 | h1.ui.header { 2 | font-family: Source Sans Pro 3 | } -------------------------------------------------------------------------------- /views/img/galaxy.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karl-chan/google-drive-torrent/HEAD/views/img/galaxy.jpg -------------------------------------------------------------------------------- /views/img/trash.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karl-chan/google-drive-torrent/HEAD/views/img/trash.ico -------------------------------------------------------------------------------- /views/img/matterhorn.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karl-chan/google-drive-torrent/HEAD/views/img/matterhorn.jpg -------------------------------------------------------------------------------- /data/driveCredentials.json: -------------------------------------------------------------------------------- 1 | { 2 | "clientId": "replace with your client id", 3 | "clientSecret": "replace with your client secret" 4 | } -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | commonjs: true, 4 | es2021: true, 5 | node: true 6 | }, 7 | extends: [ 8 | 'standard' 9 | ], 10 | parserOptions: { 11 | ecmaVersion: 'latest' 12 | }, 13 | rules: { 14 | } 15 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # google-drive-torrent 2 | 3 | Download torrents to your Google Drive directly 4 | 5 | ## Installation 6 | 7 | 1. Register this project as a client on the [Google API Console](http://console.developers.google.com) 8 | 2. Enable the **People API** and **Google Drive API** from the Google API Console. 9 | 3. Add `http://localhost` as an "Authorised JavaScript origin" and `http://localhost/login-callback` as an "Authorised redirect URI" in the Google API Console. Replace `localhost` with your host origin if you are running the project on a remote server. 10 | 4. Replace the fields in `data/driveCredentials.json` with your registered `clientId` and `clientSecret`. 11 | 5. If you are running the project on a remote server, set the environmental variable `DRIVE_REDIRECT_URI=[insert your host origin here]/login-callback`. 12 | 6. Run `npm start`. Enjoy! 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "google-drive-torrent", 3 | "version": "1.0.0", 4 | "main": "server.js", 5 | "scripts": { 6 | "start": "node server.js", 7 | "dev": "nodemon --inspect server.js", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "license": "UNLICENSED", 11 | "dependencies": { 12 | "@types/uuid": "^8.3.4", 13 | "body-parser": "1.19.0", 14 | "express": "4.17.1", 15 | "express-fileupload": "1.1.6", 16 | "express-force-https": "1.0.0", 17 | "express-session": "1.17.0", 18 | "express-zip": "3.0.0", 19 | "google-drive-io": "2.0.3", 20 | "googleapis": "47.0.0", 21 | "helmet": "3.21.2", 22 | "http": "0.0.0", 23 | "locks": "0.2.2", 24 | "lodash": "4.17.15", 25 | "pug": "2.0.4", 26 | "rimraf": "3.0.2", 27 | "socket.io": "2.3.0", 28 | "socket.io-express-session": "0.1.3", 29 | "uuid": "8.3.2", 30 | "webtorrent": "1.8.20" 31 | }, 32 | "devDependencies": { 33 | "@types/eslint": "^8.4.2", 34 | "eslint": "^8.15.0", 35 | "eslint-config-standard": "^17.0.0", 36 | "eslint-plugin-import": "^2.26.0", 37 | "eslint-plugin-n": "^15.2.0", 38 | "eslint-plugin-promise": "^6.0.0", 39 | "nodemon": "2.0.2" 40 | } 41 | } -------------------------------------------------------------------------------- /views/error.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | link(rel="stylesheet", type="text/css", href="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.2.7/semantic.min.css") 5 | link(href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css", rel="stylesheet", integrity="sha384-wvfXpqpZZVQGK6TAh5PVlGOfQNHSoD2xbE+QkPxCAFlNEevoEH3Sl0sibVcOQVnN", crossorigin="anonymous") 6 | link(href="https://fonts.googleapis.com/css?family=Raleway:100", rel="stylesheet") 7 | link(href="css/flipclock.css", rel="stylesheet") 8 | style. 9 | .flip-clock-divider, .clock ul:nth-child(-n+8) { 10 | display: none; 11 | } 12 | 13 | .inline { 14 | float: left; 15 | } 16 | 17 | 18 | 19 | body(style="background-image:url('./img/matterhorn.jpg'); background-size: cover; overflow: hidden") 20 | 21 | .ui.center.aligned.huge.inverted.header(style="margin-top: 70px; font-family: 'Raleway'") Sorry!  Something went wrong 22 | 23 | .ui.middle.aligned.centered.grid(style="margin-top: 50px; font-family: 'Raleway'; color: white") 24 | .row 25 | .column(style="width: 150px; font-size: 20px") Redirecting in 26 | .column(style="margin-left: -20px") 27 | .clock 28 | .column(style="margin-left: 40px; font-size: 20px") seconds 29 | 30 | script(src="https://code.jquery.com/jquery-3.1.1.min.js", integrity="sha256-hVVnYaiADRTO2PzUGmuLJr8BLUSjGIZsDYGmIJLv2b8=", crossorigin="anonymous") 31 | script(src="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.2.7/semantic.min.js") 32 | script(src="js/flipclock.min.js") 33 | script. 34 | var clock = $('.clock').FlipClock(3, { 35 | countdown: true, 36 | callbacks: { 37 | stop: () => { 38 | window.location.replace('/'); 39 | } 40 | } 41 | }); 42 | -------------------------------------------------------------------------------- /views/index.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | link(rel="stylesheet", type="text/css", href="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.2.9/semantic.min.css") 5 | link(href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css", rel="stylesheet", integrity="sha384-wvfXpqpZZVQGK6TAh5PVlGOfQNHSoD2xbE+QkPxCAFlNEevoEH3Sl0sibVcOQVnN", crossorigin="anonymous") 6 | 7 | link(href="https://fonts.googleapis.com/css?family=Source+Sans+Pro", rel="stylesheet") 8 | link(href="css/styles.css", rel="stylesheet") 9 | 10 | body 11 | 12 | .ui.fixed.inverted.secondary.menu 13 | a.item 14 | i.fa.fa-bars 15 | a.item.red Home 16 | .right.menu 17 | a.item(href="/login") 18 | .ui.primary.button Log in 19 | 20 | .ui.inverted.vertical.segment(style="padding-top: 40px; min-height: 100%; background-image: url('./img/galaxy.jpg'); background-size: cover") 21 | if error 22 | .ui.small.basic.error.modal 23 | i.gold.close.icon 24 | .header(style="color: orange; font-size: 36px") 25 | i.warning.sign.icon 26 | | Error 27 | .image.content 28 | .ui.items 29 | .item 30 | .ui.small.image 31 | img(src="http://ctboom.com/wp-content/uploads/Road-Work-Ahead-sign-from-Web1.jpg") 32 | .middle.aligned.content(style="margin-left: 50px; font-size: 24px")= error 33 | .actions 34 | .ui.positive.right.labeled.icon.button OK 35 | i.checkmark.icon 36 | 37 | .ui.container 38 | .ui.grid(style="padding-top: 40px") 39 | .centered.row 40 | h1.ui.inverted.header(style="font-family: Source Sans Pro; font-size: 3rem; font-weight: 300") Download torrents the fast way 41 | 42 | .centered.row 43 | .ui.steps 44 | a.active.step(href="/login") 45 | i.google.icon(style="color: #4885ed") 46 | .content 47 | .title Login 48 | .description with your Google Account 49 | .step 50 | i.magnet.green.icon 51 | .content 52 | .title Upload 53 | .description your torrent file 54 | .step 55 | i.desktop.orange.icon 56 | .content 57 | .title View 58 | .description files in Google Drive 59 | .centered.row 60 | .twelve.wide.column.middle.aligned 61 | #speedometer.ui.orange.active.striped.indicating.progress(data-percent="0") 62 | .bar(style="width: 100%") 63 | .progress 64 | .ui.tiny.image(style="margin-left: 20px") 65 | img(src="https://cdn1.iconfinder.com/data/icons/miscellaneous-4/32/dashboard-high-512.png") 66 | 67 | .centered.row 68 | a(href="/login") 69 | button.ui.pink.right.labeled.icon.button 70 | i.right.arrow.icon 71 | | Get Started 72 | 73 | script(src="https://code.jquery.com/jquery-3.1.1.min.js", integrity="sha256-hVVnYaiADRTO2PzUGmuLJr8BLUSjGIZsDYGmIJLv2b8=", crossorigin="anonymous") 74 | script(src="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.2.9/semantic.min.js") 75 | script. 76 | const updateSpeedometer = () => { 77 | const speedometer = $('#speedometer'); 78 | speedometer.progress('increment'); 79 | if(speedometer.progress('is complete')) { 80 | speedometer.progress('reset') 81 | } 82 | } 83 | setInterval(updateSpeedometer, 250); 84 | 85 | $('.message .close').click(() => { 86 | $('.message').transition('fade'); 87 | }); 88 | 89 | $('.ui.small.error.modal').modal('show'); 90 | -------------------------------------------------------------------------------- /views/css/flipclock.css: -------------------------------------------------------------------------------- 1 | /* Get the bourbon mixin from http://bourbon.io */ 2 | /* Reset */ 3 | .flip-clock-wrapper * { 4 | -webkit-box-sizing: border-box; 5 | -moz-box-sizing: border-box; 6 | -ms-box-sizing: border-box; 7 | -o-box-sizing: border-box; 8 | box-sizing: border-box; 9 | -webkit-backface-visibility: hidden; 10 | -moz-backface-visibility: hidden; 11 | -ms-backface-visibility: hidden; 12 | -o-backface-visibility: hidden; 13 | backface-visibility: hidden; 14 | } 15 | 16 | .flip-clock-wrapper a { 17 | cursor: pointer; 18 | text-decoration: none; 19 | color: #ccc; } 20 | 21 | .flip-clock-wrapper a:hover { 22 | color: #fff; } 23 | 24 | .flip-clock-wrapper ul { 25 | list-style: none; } 26 | 27 | .flip-clock-wrapper.clearfix:before, 28 | .flip-clock-wrapper.clearfix:after { 29 | content: " "; 30 | display: table; } 31 | 32 | .flip-clock-wrapper.clearfix:after { 33 | clear: both; } 34 | 35 | .flip-clock-wrapper.clearfix { 36 | *zoom: 1; } 37 | 38 | /* Main */ 39 | .flip-clock-wrapper { 40 | font: normal 11px "Helvetica Neue", Helvetica, sans-serif; 41 | -webkit-user-select: none; } 42 | 43 | .flip-clock-meridium { 44 | background: none !important; 45 | box-shadow: 0 0 0 !important; 46 | font-size: 36px !important; } 47 | 48 | .flip-clock-meridium a { color: #313333; } 49 | 50 | .flip-clock-wrapper { 51 | text-align: center; 52 | position: relative; 53 | width: 100%; 54 | margin: 1em; 55 | } 56 | 57 | .flip-clock-wrapper:before, 58 | .flip-clock-wrapper:after { 59 | content: " "; /* 1 */ 60 | display: table; /* 2 */ 61 | } 62 | .flip-clock-wrapper:after { 63 | clear: both; 64 | } 65 | 66 | /* Skeleton */ 67 | .flip-clock-wrapper ul { 68 | position: relative; 69 | float: left; 70 | margin: 5px; 71 | width: 60px; 72 | height: 90px; 73 | font-size: 80px; 74 | font-weight: bold; 75 | line-height: 87px; 76 | border-radius: 6px; 77 | background: #000; 78 | } 79 | 80 | .flip-clock-wrapper ul li { 81 | z-index: 1; 82 | position: absolute; 83 | left: 0; 84 | top: 0; 85 | width: 100%; 86 | height: 100%; 87 | line-height: 87px; 88 | text-decoration: none !important; 89 | } 90 | 91 | .flip-clock-wrapper ul li:first-child { 92 | z-index: 2; } 93 | 94 | .flip-clock-wrapper ul li a { 95 | display: block; 96 | height: 100%; 97 | -webkit-perspective: 200px; 98 | -moz-perspective: 200px; 99 | perspective: 200px; 100 | margin: 0 !important; 101 | overflow: visible !important; 102 | cursor: default !important; } 103 | 104 | .flip-clock-wrapper ul li a div { 105 | z-index: 1; 106 | position: absolute; 107 | left: 0; 108 | width: 100%; 109 | height: 50%; 110 | font-size: 80px; 111 | overflow: hidden; 112 | outline: 1px solid transparent; } 113 | 114 | .flip-clock-wrapper ul li a div .shadow { 115 | position: absolute; 116 | width: 100%; 117 | height: 100%; 118 | z-index: 2; } 119 | 120 | .flip-clock-wrapper ul li a div.up { 121 | -webkit-transform-origin: 50% 100%; 122 | -moz-transform-origin: 50% 100%; 123 | -ms-transform-origin: 50% 100%; 124 | -o-transform-origin: 50% 100%; 125 | transform-origin: 50% 100%; 126 | top: 0; } 127 | 128 | .flip-clock-wrapper ul li a div.up:after { 129 | content: ""; 130 | position: absolute; 131 | top: 44px; 132 | left: 0; 133 | z-index: 5; 134 | width: 100%; 135 | height: 3px; 136 | background-color: #000; 137 | background-color: rgba(0, 0, 0, 0.4); } 138 | 139 | .flip-clock-wrapper ul li a div.down { 140 | -webkit-transform-origin: 50% 0; 141 | -moz-transform-origin: 50% 0; 142 | -ms-transform-origin: 50% 0; 143 | -o-transform-origin: 50% 0; 144 | transform-origin: 50% 0; 145 | bottom: 0; 146 | border-bottom-left-radius: 6px; 147 | border-bottom-right-radius: 6px; 148 | } 149 | 150 | .flip-clock-wrapper ul li a div div.inn { 151 | position: absolute; 152 | left: 0; 153 | z-index: 1; 154 | width: 100%; 155 | height: 200%; 156 | color: #ccc; 157 | text-shadow: 0 1px 2px #000; 158 | text-align: center; 159 | background-color: #333; 160 | border-radius: 6px; 161 | font-size: 70px; } 162 | 163 | .flip-clock-wrapper ul li a div.up div.inn { 164 | top: 0; } 165 | 166 | .flip-clock-wrapper ul li a div.down div.inn { 167 | bottom: 0; } 168 | 169 | /* PLAY */ 170 | .flip-clock-wrapper ul.play li.flip-clock-before { 171 | z-index: 3; } 172 | 173 | .flip-clock-wrapper .flip { box-shadow: 0 2px 5px rgba(0, 0, 0, 0.7); } 174 | 175 | .flip-clock-wrapper ul.play li.flip-clock-active { 176 | -webkit-animation: asd 0.5s 0.5s linear both; 177 | -moz-animation: asd 0.5s 0.5s linear both; 178 | animation: asd 0.5s 0.5s linear both; 179 | z-index: 5; } 180 | 181 | .flip-clock-divider { 182 | float: left; 183 | display: inline-block; 184 | position: relative; 185 | width: 20px; 186 | height: 100px; } 187 | 188 | .flip-clock-divider:first-child { 189 | width: 0; } 190 | 191 | .flip-clock-dot { 192 | display: block; 193 | background: #323434; 194 | width: 10px; 195 | height: 10px; 196 | position: absolute; 197 | border-radius: 50%; 198 | box-shadow: 0 0 5px rgba(0, 0, 0, 0.5); 199 | left: 5px; } 200 | 201 | .flip-clock-divider .flip-clock-label { 202 | position: absolute; 203 | top: -1.5em; 204 | right: -86px; 205 | color: black; 206 | text-shadow: none; } 207 | 208 | .flip-clock-divider.minutes .flip-clock-label { 209 | right: -88px; } 210 | 211 | .flip-clock-divider.seconds .flip-clock-label { 212 | right: -91px; } 213 | 214 | .flip-clock-dot.top { 215 | top: 30px; } 216 | 217 | .flip-clock-dot.bottom { 218 | bottom: 30px; } 219 | 220 | @-webkit-keyframes asd { 221 | 0% { 222 | z-index: 2; } 223 | 224 | 20% { 225 | z-index: 4; } 226 | 227 | 100% { 228 | z-index: 4; } } 229 | 230 | @-moz-keyframes asd { 231 | 0% { 232 | z-index: 2; } 233 | 234 | 20% { 235 | z-index: 4; } 236 | 237 | 100% { 238 | z-index: 4; } } 239 | 240 | @-o-keyframes asd { 241 | 0% { 242 | z-index: 2; } 243 | 244 | 20% { 245 | z-index: 4; } 246 | 247 | 100% { 248 | z-index: 4; } } 249 | 250 | @keyframes asd { 251 | 0% { 252 | z-index: 2; } 253 | 254 | 20% { 255 | z-index: 4; } 256 | 257 | 100% { 258 | z-index: 4; } } 259 | 260 | .flip-clock-wrapper ul.play li.flip-clock-active .down { 261 | z-index: 2; 262 | -webkit-animation: turn 0.5s 0.5s linear both; 263 | -moz-animation: turn 0.5s 0.5s linear both; 264 | animation: turn 0.5s 0.5s linear both; } 265 | 266 | @-webkit-keyframes turn { 267 | 0% { 268 | -webkit-transform: rotateX(90deg); } 269 | 270 | 100% { 271 | -webkit-transform: rotateX(0deg); } } 272 | 273 | @-moz-keyframes turn { 274 | 0% { 275 | -moz-transform: rotateX(90deg); } 276 | 277 | 100% { 278 | -moz-transform: rotateX(0deg); } } 279 | 280 | @-o-keyframes turn { 281 | 0% { 282 | -o-transform: rotateX(90deg); } 283 | 284 | 100% { 285 | -o-transform: rotateX(0deg); } } 286 | 287 | @keyframes turn { 288 | 0% { 289 | transform: rotateX(90deg); } 290 | 291 | 100% { 292 | transform: rotateX(0deg); } } 293 | 294 | .flip-clock-wrapper ul.play li.flip-clock-before .up { 295 | z-index: 2; 296 | -webkit-animation: turn2 0.5s linear both; 297 | -moz-animation: turn2 0.5s linear both; 298 | animation: turn2 0.5s linear both; } 299 | 300 | @-webkit-keyframes turn2 { 301 | 0% { 302 | -webkit-transform: rotateX(0deg); } 303 | 304 | 100% { 305 | -webkit-transform: rotateX(-90deg); } } 306 | 307 | @-moz-keyframes turn2 { 308 | 0% { 309 | -moz-transform: rotateX(0deg); } 310 | 311 | 100% { 312 | -moz-transform: rotateX(-90deg); } } 313 | 314 | @-o-keyframes turn2 { 315 | 0% { 316 | -o-transform: rotateX(0deg); } 317 | 318 | 100% { 319 | -o-transform: rotateX(-90deg); } } 320 | 321 | @keyframes turn2 { 322 | 0% { 323 | transform: rotateX(0deg); } 324 | 325 | 100% { 326 | transform: rotateX(-90deg); } } 327 | 328 | .flip-clock-wrapper ul li.flip-clock-active { 329 | z-index: 3; } 330 | 331 | /* SHADOW */ 332 | .flip-clock-wrapper ul.play li.flip-clock-before .up .shadow { 333 | background: -moz-linear-gradient(top, rgba(0, 0, 0, 0.1) 0%, black 100%); 334 | background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, rgba(0, 0, 0, 0.1)), color-stop(100%, black)); 335 | background: linear, top, rgba(0, 0, 0, 0.1) 0%, black 100%; 336 | background: -o-linear-gradient(top, rgba(0, 0, 0, 0.1) 0%, black 100%); 337 | background: -ms-linear-gradient(top, rgba(0, 0, 0, 0.1) 0%, black 100%); 338 | background: linear, to bottom, rgba(0, 0, 0, 0.1) 0%, black 100%; 339 | -webkit-animation: show 0.5s linear both; 340 | -moz-animation: show 0.5s linear both; 341 | animation: show 0.5s linear both; } 342 | 343 | .flip-clock-wrapper ul.play li.flip-clock-active .up .shadow { 344 | background: -moz-linear-gradient(top, rgba(0, 0, 0, 0.1) 0%, black 100%); 345 | background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, rgba(0, 0, 0, 0.1)), color-stop(100%, black)); 346 | background: linear, top, rgba(0, 0, 0, 0.1) 0%, black 100%; 347 | background: -o-linear-gradient(top, rgba(0, 0, 0, 0.1) 0%, black 100%); 348 | background: -ms-linear-gradient(top, rgba(0, 0, 0, 0.1) 0%, black 100%); 349 | background: linear, to bottom, rgba(0, 0, 0, 0.1) 0%, black 100%; 350 | -webkit-animation: hide 0.5s 0.3s linear both; 351 | -moz-animation: hide 0.5s 0.3s linear both; 352 | animation: hide 0.5s 0.3s linear both; } 353 | 354 | /*DOWN*/ 355 | .flip-clock-wrapper ul.play li.flip-clock-before .down .shadow { 356 | background: -moz-linear-gradient(top, black 0%, rgba(0, 0, 0, 0.1) 100%); 357 | background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, black), color-stop(100%, rgba(0, 0, 0, 0.1))); 358 | background: linear, top, black 0%, rgba(0, 0, 0, 0.1) 100%; 359 | background: -o-linear-gradient(top, black 0%, rgba(0, 0, 0, 0.1) 100%); 360 | background: -ms-linear-gradient(top, black 0%, rgba(0, 0, 0, 0.1) 100%); 361 | background: linear, to bottom, black 0%, rgba(0, 0, 0, 0.1) 100%; 362 | -webkit-animation: show 0.5s linear both; 363 | -moz-animation: show 0.5s linear both; 364 | animation: show 0.5s linear both; } 365 | 366 | .flip-clock-wrapper ul.play li.flip-clock-active .down .shadow { 367 | background: -moz-linear-gradient(top, black 0%, rgba(0, 0, 0, 0.1) 100%); 368 | background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, black), color-stop(100%, rgba(0, 0, 0, 0.1))); 369 | background: linear, top, black 0%, rgba(0, 0, 0, 0.1) 100%; 370 | background: -o-linear-gradient(top, black 0%, rgba(0, 0, 0, 0.1) 100%); 371 | background: -ms-linear-gradient(top, black 0%, rgba(0, 0, 0, 0.1) 100%); 372 | background: linear, to bottom, black 0%, rgba(0, 0, 0, 0.1) 100%; 373 | -webkit-animation: hide 0.5s 0.3s linear both; 374 | -moz-animation: hide 0.5s 0.3s linear both; 375 | animation: hide 0.5s 0.2s linear both; } 376 | 377 | @-webkit-keyframes show { 378 | 0% { 379 | opacity: 0; } 380 | 381 | 100% { 382 | opacity: 1; } } 383 | 384 | @-moz-keyframes show { 385 | 0% { 386 | opacity: 0; } 387 | 388 | 100% { 389 | opacity: 1; } } 390 | 391 | @-o-keyframes show { 392 | 0% { 393 | opacity: 0; } 394 | 395 | 100% { 396 | opacity: 1; } } 397 | 398 | @keyframes show { 399 | 0% { 400 | opacity: 0; } 401 | 402 | 100% { 403 | opacity: 1; } } 404 | 405 | @-webkit-keyframes hide { 406 | 0% { 407 | opacity: 1; } 408 | 409 | 100% { 410 | opacity: 0; } } 411 | 412 | @-moz-keyframes hide { 413 | 0% { 414 | opacity: 1; } 415 | 416 | 100% { 417 | opacity: 0; } } 418 | 419 | @-o-keyframes hide { 420 | 0% { 421 | opacity: 1; } 422 | 423 | 100% { 424 | opacity: 0; } } 425 | 426 | @keyframes hide { 427 | 0% { 428 | opacity: 1; } 429 | 430 | 100% { 431 | opacity: 0; } } 432 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const driveCredentials = require('./data/driveCredentials.json') 4 | 5 | const DRIVE_CLIENT_ID = driveCredentials.clientId 6 | const DRIVE_CLIENT_SECRET = driveCredentials.clientSecret 7 | const DRIVE_REDIRECT_URI = process.env.DRIVE_REDIRECT_URI || 'http://localhost:3000/login-callback' 8 | const DRIVE_RETURN_FIELDS = 'id,name,webViewLink' 9 | const DRIVE_TORRENT_DIR = 'My torrents' 10 | 11 | const isProduction = process.env.NODE_ENV === 'production' 12 | 13 | const torrentClients = {} // {userId: Webtorrent client} 14 | const sockets = {} // {userId: socket} 15 | 16 | const parseTorrent = require('parse-torrent') 17 | const WebTorrent = require('webtorrent') 18 | 19 | const { google } = require('googleapis') 20 | const express = require('express') 21 | require('express-zip') 22 | const helmet = require('helmet') 23 | const { v4: uuidv4 } = require('uuid') 24 | const forceHttps = require('express-force-https') 25 | const bodyParser = require('body-parser') 26 | const fileUpload = require('express-fileupload') 27 | const fs = require('fs') 28 | const path = require('path') 29 | const os = require('os') 30 | const locks = require('locks') 31 | const _ = require('lodash') 32 | const driveIO = require('google-drive-io') 33 | const app = express() 34 | 35 | const server = require('http').createServer(app) 36 | const io = require('socket.io')(server) 37 | const ios = require('socket.io-express-session') 38 | const session = require('express-session')({ 39 | secret: 'google-drive-torrent', 40 | resave: false, 41 | saveUninitialized: false, 42 | rolling: true, 43 | cookie: { httpOnly: true, maxAge: 3600000 } 44 | }) 45 | 46 | app.set('port', (process.env.PORT || 3000)) 47 | app.set('views', path.join(__dirname, 'views')) 48 | app.set('view engine', 'pug') 49 | 50 | app.use(express.static(path.join(__dirname, 'views'))) 51 | app.use(bodyParser.json()) 52 | app.use(bodyParser.urlencoded({ extended: true })) 53 | app.use(fileUpload()) 54 | app.use(helmet()) 55 | app.use(session) 56 | 57 | if (isProduction) { 58 | app.use(forceHttps) 59 | } 60 | 61 | io.use(ios(session)) 62 | io.on('connection', (socket) => { 63 | const session = socket.handshake.session 64 | if ('user' in session) { 65 | const user = session.user 66 | sockets[user.id] = socket 67 | 68 | // send updates every second 69 | const updateInterval = 1000 70 | sendUpdate(user, socket) 71 | const updateTask = setInterval(() => sendUpdate(user, socket), updateInterval) 72 | 73 | // stop updates when user disconnects 74 | socket.on('disconnect', () => clearInterval(updateTask)) 75 | } 76 | }) 77 | 78 | /* PART I: Routes for views */ 79 | app.get('/', (req, res) => { 80 | loggedIn(req) ? res.redirect('/dashboard') : res.redirect('/home') 81 | }) 82 | 83 | app.get('/home', (req, res) => { 84 | unlessLoggedIn(req, res, () => { 85 | const options = {} 86 | if ('error' in req.query) { 87 | options.error = req.query.error 88 | } 89 | res.render('index.pug', options) 90 | }) 91 | }) 92 | 93 | app.get('/dashboard', (req, res) => { 94 | ifLoggedIn(req, res, () => { 95 | const user = req.session.user 96 | const driveUrl = req.session.driveUrl 97 | res.render('dashboard.pug', { 98 | name: user.names[0].displayName, 99 | firstName: user.names[0].givenName, 100 | pic: user.photos[0].url, 101 | driveUrl 102 | }) 103 | }) 104 | }) 105 | 106 | /* PART II: Routes for authentication */ 107 | // Login to Google 108 | app.get('/login', (req, res) => { 109 | unlessLoggedIn(req, res, () => { 110 | const url = newOAuth2Client().generateAuthUrl({ 111 | access_type: 'offline', 112 | prompt: 'consent', 113 | scope: [ 114 | 'https://www.googleapis.com/auth/plus.me', 115 | 'https://www.googleapis.com/auth/drive', 116 | 'profile' 117 | ] 118 | }) 119 | res.redirect(url) 120 | }) 121 | }) 122 | 123 | // Accept authorisation code from Google 124 | app.get('/login-callback', (req, res) => { 125 | unlessLoggedIn(req, res, () => { 126 | // Redirect to home page and display error message if authentication failure 127 | if ('error' in req.query) { 128 | console.error(`Login error: ${req.query.error}`) 129 | return res.redirect('/error') 130 | } 131 | 132 | // Otherwise proceed, get tokens and save auth client and user details 133 | const code = req.query.code 134 | const oAuth2Client = newOAuth2Client() 135 | oAuth2Client.getToken(code, (err, tokens) => { 136 | if (err) { 137 | console.error(`OAuth2 failed: ${err}`) 138 | return res.redirect('/error') 139 | } 140 | 141 | // store access and refresh tokens in session 142 | req.session.tokens = tokens 143 | oAuth2Client.setCredentials(tokens) 144 | console.log(`Obtained tokens: ${JSON.stringify(tokens)}`) 145 | 146 | google.people('v1').people.get({ 147 | auth: oAuth2Client, 148 | resourceName: 'people/me', 149 | personFields: 'names,photos,metadata' 150 | }, (err, data) => { 151 | if (err) { 152 | console.error(`Failed to get user details: ${err}`) 153 | return res.redirect('/error') 154 | } 155 | const user = data.data 156 | user.id = user.metadata.sources[0].id 157 | req.session.user = user 158 | console.log(`Obtained user: ${JSON.stringify(user)}`) 159 | 160 | driveIO.createFolderIfNotExists(DRIVE_TORRENT_DIR, DRIVE_RETURN_FIELDS, oAuth2Client) 161 | .then(folder => { 162 | req.session.driveUrl = folder.webViewLink 163 | return res.redirect('/dashboard') 164 | }) 165 | .catch(err => { 166 | console.error(`Failed to create google drive folder: ${err}`) 167 | return res.redirect('/error') 168 | }) 169 | }) 170 | }) 171 | }) 172 | }) 173 | 174 | app.get('/logout', (req, res) => { 175 | ifLoggedIn(req, res, () => { 176 | delete req.session.oAuth2Client 177 | delete req.session.user 178 | res.redirect('/home') 179 | }) 180 | }) 181 | 182 | /* PART III: Routes for torrent interaction */ 183 | app.post('/add-torrent', (req, res) => { 184 | ifLoggedIn(req, res, () => { 185 | // extract torrent from post request 186 | let torrentId = null 187 | if ('files' in req && 'torrent' in req.files) { 188 | torrentId = req.files.torrent.data 189 | } else if ('body' in req && 'magnet' in req.body) { 190 | torrentId = req.body.magnet 191 | } else { 192 | return res.status(500).json({ message: 'Invalid request in parse-torrent' }) 193 | } 194 | 195 | const user = req.session.user 196 | const oAuth2Client = newOAuth2Client(req.session.tokens) 197 | const torrent = addTorrentForUser(torrentId, user, (err, torrent) => { 198 | if (err) { 199 | return res.status(500).json({ message: err.message }) 200 | } 201 | console.log(`Added torrent: ${torrent.name} with files ${torrent.files.map(f => f.name).join(', ')}`) 202 | const torrentFiles = { 203 | infoHash: torrent.infoHash, 204 | files: getFileInfos(torrent) 205 | } 206 | return res.json(torrentFiles) 207 | }) 208 | 209 | const socket = getSocketForUser(user) 210 | 211 | // Add callback handlers so that files get uploaded to google drive once ready 212 | torrent.once('ready', () => { 213 | console.log(`Torrent ${torrent.infoHash} is ready`) 214 | attachCompleteHandler(torrent, oAuth2Client, socket) 215 | }) 216 | 217 | torrent.on('warning', (err) => { 218 | console.warn('Torrent on warning: ' + err) 219 | socket.emit('torrent-warning', { 220 | message: err.message 221 | }) 222 | }) 223 | 224 | torrent.on('error', (err) => { 225 | torrent.error = err.message // Attach error onto torrent (hack!) 226 | const info = getTorrentInfo(torrent) 227 | socket.emit('torrent-error', info) 228 | socket.emit('torrent-update', info) 229 | console.error('Torrent on error: ' + err) 230 | }) 231 | }) 232 | }) 233 | 234 | app.post('/update-torrent', (req, res) => { 235 | ifLoggedIn(req, res, () => { 236 | const user = req.session.user 237 | const infoHash = req.body.infoHash 238 | const selectedFiles = req.body.selectedFiles // array of booleans 239 | 240 | const torrent = getTorrentForUser(infoHash, user) 241 | 242 | // Needed as a workaround since deselect() doesn't work on its own - https://github.com/webtorrent/webtorrent/issues/164#issuecomment-248395202 243 | torrent.deselect(0, torrent.pieces.length - 1, false) 244 | 245 | // deselect files based on user choice 246 | for (let i = 0; i < torrent.files.length; i++) { 247 | const file = torrent.files[i] 248 | const newSelection = selectedFiles[i] === 'true' // somehow selectedFiles are received as strings, need to parse 249 | 250 | if (newSelection) { 251 | // file selected 252 | file.select() 253 | file.selected = true // attach to file object (hack!) to retrieve later 254 | console.log(`Selected file: ${file.name} for user ${user.id}`) 255 | } else { 256 | // file deselected 257 | file.deselect() 258 | file.selected = false // attach to file object (hack!) to retrieve later 259 | console.log(`Deselected file: ${file.name} for user ${user.id}`) 260 | } 261 | } 262 | res.end() 263 | }) 264 | }) 265 | 266 | app.post('/delete-torrent', (req, res) => { 267 | ifLoggedIn(req, res, () => { 268 | const user = req.session.user 269 | const infoHash = req.body.infoHash 270 | 271 | const client = torrentClients[user.id] 272 | if (!client) { 273 | return res.status(500).json({ message: 'Client not found for user' }) 274 | } 275 | client.remove(infoHash, (err) => { 276 | if (err) { 277 | return res.status(500).json({ message: err.message }) 278 | } 279 | console.log(`Deleted torrent: ${infoHash}`) 280 | return res.end() 281 | }) 282 | }) 283 | }) 284 | 285 | app.get('/get-torrents', (req, res) => { 286 | ifLoggedIn(req, res, () => { 287 | const user = req.session.user 288 | const client = torrentClients[user.id] 289 | if (!client) { 290 | // return empty array if client is not yet initialised 291 | res.json([]); return 292 | } 293 | const torrentsInfo = getTorrentsInfo(client.torrents) 294 | res.json(torrentsInfo) 295 | }) 296 | }) 297 | 298 | app.get('/download/:infoHash', (req, res) => { 299 | ifLoggedIn(req, res, () => { 300 | const user = req.session.user 301 | const infoHash = req.params.infoHash 302 | const torrent = getTorrentForUser(infoHash, user) 303 | const files = getSelectedFiles(torrent) 304 | 305 | const targets = files.map((file) => { 306 | return { name: file.name, path: file.path } 307 | }) 308 | res.zip(targets, `${torrent.name}.zip`) 309 | }) 310 | }) 311 | 312 | app.get('/download/:infoHash/:fileId', (req, res) => { 313 | ifLoggedIn(req, res, () => { 314 | const user = req.session.user 315 | const infoHash = req.params.infoHash 316 | const fileId = req.params.fileId 317 | const torrent = getTorrentForUser(infoHash, user) 318 | 319 | const file = torrent.files.find((file) => file.fileId === fileId) 320 | res.download(file.path, file.name) 321 | }) 322 | }) 323 | 324 | app.get('*', (req, res) => { 325 | res.render('error.pug') 326 | }) 327 | 328 | server.listen(app.get('port'), () => { 329 | console.log('Node app is running on port', app.get('port')) 330 | }) 331 | 332 | /* Helper functions */ 333 | const loggedIn = (req) => { 334 | return 'tokens' in req.session && 'user' in req.session 335 | } 336 | 337 | const ifLoggedIn = (req, res, callback, otherwise) => { 338 | if (!otherwise) { 339 | otherwise = () => { 340 | res.redirect('/home') 341 | } 342 | } 343 | loggedIn(req) ? callback() : otherwise() 344 | } 345 | 346 | const unlessLoggedIn = (req, res, callback, otherwise) => { 347 | if (!otherwise) { 348 | otherwise = () => { 349 | res.redirect('/dashboard') 350 | } 351 | } 352 | loggedIn(req) ? otherwise() : callback() 353 | } 354 | 355 | const newOAuth2Client = (tokens) => { 356 | const oAuth2Client = new google.auth.OAuth2( 357 | DRIVE_CLIENT_ID, 358 | DRIVE_CLIENT_SECRET, 359 | DRIVE_REDIRECT_URI 360 | ) 361 | if (tokens) { 362 | oAuth2Client.setCredentials(tokens) 363 | } 364 | return oAuth2Client 365 | } 366 | 367 | /** 368 | * Adds a new torrent for the user (creating a new Webtorrent Client if neccessary), 369 | * returns the added torrent and invokes callback(err, torrent). 370 | * @param torrent anything Webtorrent identifies as a torrent (infoHash/magnetURI/.torrent etc) 371 | * @param user Google Plus API user object 372 | * @param callback(err, torrent) 373 | * @return reference to Webtorrent torrent object 374 | */ 375 | const addTorrentForUser = (torrent, user, callback) => { 376 | const client = (user.id in torrentClients) ? torrentClients[user.id] : new WebTorrent() 377 | torrentClients[user.id] = client 378 | 379 | try { 380 | const parsedTorrent = parseTorrent(torrent) 381 | const infoHash = parsedTorrent.infoHash 382 | console.log(`Parsed infohash: ${infoHash}`) 383 | 384 | // Catch error in case client.add() later fails 385 | const callbackWithError = (err) => { 386 | callback(err) 387 | } 388 | 389 | const saveToPath = path.join(os.tmpdir(), user.id, infoHash) 390 | console.log(`Torrent ${infoHash} will be saved to: ${saveToPath}`) 391 | const torrentHandle = client.add(torrent, { 392 | path: saveToPath, 393 | destroyStoreOnDestroy: true, // Delete the torrent's chunk store (e.g. files on disk) when the torrent is destroyed 394 | storeCacheSlots: 0 // Number of chunk store entries (torrent pieces) to cache in memory [default=20]; 0 to disable caching 395 | }, (torrent) => { 396 | torrentHandle.removeListener('error', callbackWithError) 397 | 398 | for (let i = 0; i < torrent.files.length; i++) { 399 | // Attach boolean to file (hack!), by default all files are selected 400 | torrent.files[i].selected = true 401 | // Attach unique id to file (hack!), to support direct downloads 402 | torrent.files[i].fileId = uuidv4() 403 | } 404 | callback(null, torrent) 405 | }) 406 | 407 | torrentHandle.once('error', callbackWithError) 408 | 409 | return torrentHandle 410 | } catch (err) { 411 | callback(err) 412 | } 413 | } 414 | 415 | const getTorrentForUser = (torrent, user) => { 416 | const client = torrentClients[user.id] 417 | return client.get(torrent) 418 | } 419 | 420 | const getSocketForUser = (user) => { 421 | return sockets[user.id] 422 | } 423 | 424 | const getFileInfos = (torrent) => { 425 | return torrent.files.map((f) => { 426 | return { 427 | name: f.name, 428 | fileId: f.fileId, 429 | length: f.length, 430 | downloaded: f.downloaded, 431 | progress: f.progress, 432 | selected: f.selected 433 | } 434 | }) 435 | } 436 | 437 | const getTorrentInfo = (torrent) => { 438 | return getTorrentsInfo([torrent]) 439 | } 440 | 441 | const getTorrentsInfo = (torrents) => { 442 | return torrents.map((torrent) => { 443 | const files = getSelectedFiles(torrent) 444 | const received = _.sumBy(files, (file) => file.downloaded) 445 | const size = _.sumBy(files, (file) => file.length) 446 | const progress = Math.min(received / size, 1) 447 | 448 | return { 449 | infoHash: torrent.infoHash, 450 | magnetURI: torrent.magnetURI, 451 | name: torrent.name, 452 | files: getFileInfos(torrent), 453 | received, 454 | size, 455 | progress, 456 | timeRemaining: torrent.timeRemaining, 457 | downloaded: torrent.downloaded, 458 | downloadSpeed: torrent.downloadSpeed, 459 | uploaded: torrent.uploaded, 460 | uploadSpeed: torrent.uploadSpeed, 461 | ratio: torrent.ratio, 462 | numPeers: torrent.numPeers, 463 | error: torrent.error, 464 | driveUrl: torrent.driveUrl 465 | } 466 | }) 467 | } 468 | 469 | const getSelectedFiles = (torrent) => { 470 | return torrent.files.filter((file) => file.selected) 471 | } 472 | 473 | const torrentIsDone = (torrent) => { 474 | return _.every(getSelectedFiles(torrent), file => file.done) 475 | } 476 | 477 | const attachCompleteHandler = (torrent, auth, socket) => { 478 | const mutex = locks.createMutex() // prevent race condition during google drive operations 479 | 480 | torrent.files.forEach((file) => { 481 | file.on('done', () => { 482 | // Update torrent as success if all files have completed 483 | console.log(`Done for file: ${file.path}`) 484 | const torrentFolderPath = path.join(DRIVE_TORRENT_DIR, torrent.name) 485 | if (torrentIsDone(torrent)) { 486 | mutex.lock(() => { 487 | driveIO.createFolderIfNotExists(torrentFolderPath, DRIVE_RETURN_FIELDS, auth) 488 | .then(torrentFolder => { 489 | torrent.driveUrl = torrentFolder.webViewLink 490 | socket.emit('torrent-success', getTorrentInfo(torrent)) 491 | console.log(`Created torrent folder on google drive: ${torrent.name}`) 492 | }) 493 | .catch(err => { 494 | console.error(err) 495 | torrent.error = err.message 496 | socket.emit('torrent-error', getTorrentInfo(torrent)) 497 | }) 498 | .finally(() => { 499 | mutex.unlock() 500 | }) 501 | }) 502 | } 503 | if (file.selected) { 504 | const uploadPath = path.join(DRIVE_TORRENT_DIR, path.relative(torrent.path, file.path)) 505 | console.log(`Directory: ${torrent.path} exists: ${fs.existsSync(torrent.path)}`) 506 | console.log(`File: ${file.path} exists: ${fs.existsSync(file.path)}`) 507 | mutex.lock(() => { 508 | driveIO.uploadFileIfNotExists(file.path, uploadPath, DRIVE_RETURN_FIELDS, auth) 509 | .then(uploaded => { 510 | socket.emit('torrent-update', getTorrentInfo(torrent)) 511 | console.log(`File uploaded to google drive: ${uploadPath}, with id: ${uploaded.id}`) 512 | }) 513 | .catch(err => { 514 | console.error(err) 515 | torrent.error = err.message 516 | socket.emit('torrent-error', getTorrentInfo(torrent)) 517 | }) 518 | .finally(() => { 519 | mutex.unlock() 520 | }) 521 | }) 522 | } 523 | }) 524 | }) 525 | } 526 | 527 | const sendUpdate = (user, socket) => { 528 | const client = torrentClients[user.id] 529 | if (!client) { 530 | return socket.emit('all-torrents', []) 531 | } 532 | socket.emit('all-torrents', getTorrentsInfo(client.torrents)) 533 | } 534 | -------------------------------------------------------------------------------- /views/js/flipclock.min.js: -------------------------------------------------------------------------------- 1 | /*! flipclock 2015-08-31 */ 2 | var Base=function(){};Base.extend=function(a,b){"use strict";var c=Base.prototype.extend;Base._prototyping=!0;var d=new this;c.call(d,a),d.base=function(){},delete Base._prototyping;var e=d.constructor,f=d.constructor=function(){if(!Base._prototyping)if(this._constructing||this.constructor==f)this._constructing=!0,e.apply(this,arguments),delete this._constructing;else if(null!==arguments[0])return(arguments[0].extend||c).call(arguments[0],d)};return f.ancestor=this,f.extend=this.extend,f.forEach=this.forEach,f.implement=this.implement,f.prototype=d,f.toString=this.toString,f.valueOf=function(a){return"object"==a?f:e.valueOf()},c.call(f,b),"function"==typeof f.init&&f.init(),f},Base.prototype={extend:function(a,b){if(arguments.length>1){var c=this[a];if(c&&"function"==typeof b&&(!c.valueOf||c.valueOf()!=b.valueOf())&&/\bbase\b/.test(b)){var d=b.valueOf();b=function(){var a=this.base||Base.prototype.base;this.base=c;var b=d.apply(this,arguments);return this.base=a,b},b.valueOf=function(a){return"object"==a?b:d},b.toString=Base.toString}this[a]=b}else if(a){var e=Base.prototype.extend;Base._prototyping||"function"==typeof this||(e=this.extend||e);for(var f={toSource:null},g=["constructor","toString","valueOf"],h=Base._prototyping?0:1;i=g[h++];)a[i]!=f[i]&&e.call(this,i,a[i]);for(var i in a)f[i]||e.call(this,i,a[i])}return this}},Base=Base.extend({constructor:function(){this.extend(arguments[0])}},{ancestor:Object,version:"1.1",forEach:function(a,b,c){for(var d in a)void 0===this.prototype[d]&&b.call(c,a[d],d,a)},implement:function(){for(var a=0;a',''].join("");d&&(e=""),b=this.factory.localize(b);var f=['',''+(b?b:"")+"",e,""],g=a(f.join(""));return this.dividers.push(g),g},createList:function(a,b){"object"==typeof a&&(b=a,a=0);var c=new FlipClock.List(this.factory,a,b);return this.lists.push(c),c},reset:function(){this.factory.time=new FlipClock.Time(this.factory,this.factory.original?Math.round(this.factory.original):0,{minimumDigits:this.factory.minimumDigits}),this.flip(this.factory.original,!1)},appendDigitToClock:function(a){a.$el.append(!1)},addDigit:function(a){var b=this.createList(a,{classes:{active:this.factory.classes.active,before:this.factory.classes.before,flip:this.factory.classes.flip}});this.appendDigitToClock(b)},start:function(){},stop:function(){},autoIncrement:function(){this.factory.countdown?this.decrement():this.increment()},increment:function(){this.factory.time.addSecond()},decrement:function(){0==this.factory.time.getTimeSeconds()?this.factory.stop():this.factory.time.subSecond()},flip:function(b,c){var d=this;a.each(b,function(a,b){var e=d.lists[a];e?(c||b==e.digit||e.play(),e.select(b)):d.addDigit(b)})}})}(jQuery),function(a){"use strict";FlipClock.Factory=FlipClock.Base.extend({animationRate:1e3,autoStart:!0,callbacks:{destroy:!1,create:!1,init:!1,interval:!1,start:!1,stop:!1,reset:!1},classes:{active:"flip-clock-active",before:"flip-clock-before",divider:"flip-clock-divider",dot:"flip-clock-dot",label:"flip-clock-label",flip:"flip",play:"play",wrapper:"flip-clock-wrapper"},clockFace:"HourlyCounter",countdown:!1,defaultClockFace:"HourlyCounter",defaultLanguage:"english",$el:!1,face:!0,lang:!1,language:"english",minimumDigits:0,original:!1,running:!1,time:!1,timer:!1,$wrapper:!1,constructor:function(b,c,d){d||(d={}),this.lists=[],this.running=!1,this.base(d),this.$el=a(b).addClass(this.classes.wrapper),this.$wrapper=this.$el,this.original=c instanceof Date?c:c?Math.round(c):0,this.time=new FlipClock.Time(this,this.original,{minimumDigits:this.minimumDigits,animationRate:this.animationRate}),this.timer=new FlipClock.Timer(this,d),this.loadLanguage(this.language),this.loadClockFace(this.clockFace,d),this.autoStart&&this.start()},loadClockFace:function(a,b){var c,d="Face",e=!1;return a=a.ucfirst()+d,this.face.stop&&(this.stop(),e=!0),this.$el.html(""),this.time.minimumDigits=this.minimumDigits,c=FlipClock[a]?new FlipClock[a](this,b):new FlipClock[this.defaultClockFace+d](this,b),c.build(),this.face=c,e&&this.start(),this.face},loadLanguage:function(a){var b;return b=FlipClock.Lang[a.ucfirst()]?FlipClock.Lang[a.ucfirst()]:FlipClock.Lang[a]?FlipClock.Lang[a]:FlipClock.Lang[this.defaultLanguage],this.lang=b},localize:function(a,b){var c=this.lang;if(!a)return null;var d=a.toLowerCase();return"object"==typeof b&&(c=b),c&&c[d]?c[d]:a},start:function(a){var b=this;b.running||b.countdown&&!(b.countdown&&b.time.time>0)?b.log("Trying to start timer when countdown already at 0"):(b.face.start(b.time),b.timer.start(function(){b.flip(),"function"==typeof a&&a()}))},stop:function(a){this.face.stop(),this.timer.stop(a);for(var b in this.lists)this.lists.hasOwnProperty(b)&&this.lists[b].stop()},reset:function(a){this.timer.reset(a),this.face.reset()},setTime:function(a){this.time.time=a,this.flip(!0)},getTime:function(a){return this.time},setCountdown:function(a){var b=this.running;this.countdown=a?!0:!1,b&&(this.stop(),this.start())},flip:function(a){this.face.flip(!1,a)}})}(jQuery),function(a){"use strict";FlipClock.List=FlipClock.Base.extend({digit:0,classes:{active:"flip-clock-active",before:"flip-clock-before",flip:"flip"},factory:!1,$el:!1,$obj:!1,items:[],lastDigit:0,constructor:function(a,b,c){this.factory=a,this.digit=b,this.lastDigit=b,this.$el=this.createList(),this.$obj=this.$el,b>0&&this.select(b),this.factory.$el.append(this.$el)},select:function(a){if("undefined"==typeof a?a=this.digit:this.digit=a,this.digit!=this.lastDigit){var b=this.$el.find("."+this.classes.before).removeClass(this.classes.before);this.$el.find("."+this.classes.active).removeClass(this.classes.active).addClass(this.classes.before),this.appendListItem(this.classes.active,this.digit),b.remove(),this.lastDigit=this.digit}},play:function(){this.$el.addClass(this.factory.classes.play)},stop:function(){var a=this;setTimeout(function(){a.$el.removeClass(a.factory.classes.play)},this.factory.timer.interval)},createListItem:function(a,b){return['
  • ','','
    ','
    ','
    '+(b?b:"")+"
    ","
    ",'
    ','
    ','
    '+(b?b:"")+"
    ","
    ","
    ","
  • "].join("")},appendListItem:function(a,b){var c=this.createListItem(a,b);this.$el.append(c)},createList:function(){var b=this.getPrevDigit()?this.getPrevDigit():this.digit,c=a(['"].join(""));return c},getNextDigit:function(){return 9==this.digit?0:this.digit+1},getPrevDigit:function(){return 0==this.digit?9:this.digit-1}})}(jQuery),function(a){"use strict";String.prototype.ucfirst=function(){return this.substr(0,1).toUpperCase()+this.substr(1)},a.fn.FlipClock=function(b,c){return new FlipClock(a(this),b,c)},a.fn.flipClock=function(b,c){return a.fn.FlipClock(b,c)}}(jQuery),function(a){"use strict";FlipClock.Time=FlipClock.Base.extend({time:0,factory:!1,minimumDigits:0,constructor:function(a,b,c){"object"!=typeof c&&(c={}),c.minimumDigits||(c.minimumDigits=a.minimumDigits),this.base(c),this.factory=a,b&&(this.time=b)},convertDigitsToArray:function(a){var b=[];a=a.toString();for(var c=0;cthis.minimumDigits&&(this.minimumDigits=c.length),this.minimumDigits>c.length)for(var d=c.length;d12?c-12:0===c?12:c,a.getMinutes()];return b===!0&&d.push(a.getSeconds()),this.digitize(d)},getSeconds:function(a){var b=this.getTimeSeconds();return a&&(60==b?b=0:b%=60),Math.ceil(b)},getWeeks:function(a){var b=this.getTimeSeconds()/60/60/24/7;return a&&(b%=52),Math.floor(b)},removeLeadingZeros:function(b,c){var d=0,e=[];return a.each(c,function(a,f){b>a?d+=parseInt(c[a],10):e.push(c[a])}),0===d?e:c},addSeconds:function(a){this.time instanceof Date?this.time.setSeconds(this.time.getSeconds()+a):this.time+=a},addSecond:function(){this.addSeconds(1)},subSeconds:function(a){this.time instanceof Date?this.time.setSeconds(this.time.getSeconds()-a):this.time-=a},subSecond:function(){this.subSeconds(1)},toString:function(){return this.getTimeSeconds().toString()}})}(jQuery),function(a){"use strict";FlipClock.Timer=FlipClock.Base.extend({callbacks:{destroy:!1,create:!1,init:!1,interval:!1,start:!1,stop:!1,reset:!1},count:0,factory:!1,interval:1e3,animationRate:1e3,constructor:function(a,b){this.base(b),this.factory=a,this.callback(this.callbacks.init),this.callback(this.callbacks.create)},getElapsed:function(){return this.count*this.interval},getElapsedTime:function(){return new Date(this.time+this.getElapsed())},reset:function(a){clearInterval(this.timer),this.count=0,this._setInterval(a),this.callback(this.callbacks.reset)},start:function(a){this.factory.running=!0,this._createTimer(a),this.callback(this.callbacks.start)},stop:function(a){this.factory.running=!1,this._clearInterval(a),this.callback(this.callbacks.stop),this.callback(a)},_clearInterval:function(){clearInterval(this.timer)},_createTimer:function(a){this._setInterval(a)},_destroyTimer:function(a){this._clearInterval(),this.timer=!1,this.callback(a),this.callback(this.callbacks.destroy)},_interval:function(a){this.callback(this.callbacks.interval),this.callback(a),this.count++},_setInterval:function(a){var b=this;b._interval(a),b.timer=setInterval(function(){b._interval(a)},this.interval)}})}(jQuery),function(a){FlipClock.TwentyFourHourClockFace=FlipClock.Face.extend({constructor:function(a,b){this.base(a,b)},build:function(b){var c=this,d=this.factory.$el.find("ul");this.factory.time.time||(this.factory.original=new Date,this.factory.time=new FlipClock.Time(this.factory,this.factory.original));var b=b?b:this.factory.time.getMilitaryTime(!1,this.showSeconds);b.length>d.length&&a.each(b,function(a,b){c.createList(b)}),this.createDivider(),this.createDivider(),a(this.dividers[0]).insertBefore(this.lists[this.lists.length-2].$el),a(this.dividers[1]).insertBefore(this.lists[this.lists.length-4].$el),this.base()},flip:function(a,b){this.autoIncrement(),a=a?a:this.factory.time.getMilitaryTime(!1,this.showSeconds),this.base(a,b)}})}(jQuery),function(a){FlipClock.CounterFace=FlipClock.Face.extend({shouldAutoIncrement:!1,constructor:function(a,b){"object"!=typeof b&&(b={}),a.autoStart=b.autoStart?!0:!1,b.autoStart&&(this.shouldAutoIncrement=!0),a.increment=function(){a.countdown=!1,a.setTime(a.getTime().getTimeSeconds()+1)},a.decrement=function(){a.countdown=!0;var b=a.getTime().getTimeSeconds();b>0&&a.setTime(b-1)},a.setValue=function(b){a.setTime(b)},a.setCounter=function(b){a.setTime(b)},this.base(a,b)},build:function(){var b=this,c=this.factory.$el.find("ul"),d=this.factory.getTime().digitize([this.factory.getTime().time]);d.length>c.length&&a.each(d,function(a,c){var d=b.createList(c);d.select(c)}),a.each(this.lists,function(a,b){b.play()}),this.base()},flip:function(a,b){this.shouldAutoIncrement&&this.autoIncrement(),a||(a=this.factory.getTime().digitize([this.factory.getTime().time])),this.base(a,b)},reset:function(){this.factory.time=new FlipClock.Time(this.factory,this.factory.original?Math.round(this.factory.original):0),this.flip()}})}(jQuery),function(a){FlipClock.DailyCounterFace=FlipClock.Face.extend({showSeconds:!0,constructor:function(a,b){this.base(a,b)},build:function(b){var c=this,d=this.factory.$el.find("ul"),e=0;b=b?b:this.factory.time.getDayCounter(this.showSeconds),b.length>d.length&&a.each(b,function(a,b){c.createList(b)}),this.showSeconds?a(this.createDivider("Seconds")).insertBefore(this.lists[this.lists.length-2].$el):e=2,a(this.createDivider("Minutes")).insertBefore(this.lists[this.lists.length-4+e].$el),a(this.createDivider("Hours")).insertBefore(this.lists[this.lists.length-6+e].$el),a(this.createDivider("Days",!0)).insertBefore(this.lists[0].$el),this.base()},flip:function(a,b){a||(a=this.factory.time.getDayCounter(this.showSeconds)),this.autoIncrement(),this.base(a,b)}})}(jQuery),function(a){FlipClock.HourlyCounterFace=FlipClock.Face.extend({constructor:function(a,b){this.base(a,b)},build:function(b,c){var d=this,e=this.factory.$el.find("ul");c=c?c:this.factory.time.getHourCounter(),c.length>e.length&&a.each(c,function(a,b){d.createList(b)}),a(this.createDivider("Seconds")).insertBefore(this.lists[this.lists.length-2].$el),a(this.createDivider("Minutes")).insertBefore(this.lists[this.lists.length-4].$el),b||a(this.createDivider("Hours",!0)).insertBefore(this.lists[0].$el),this.base()},flip:function(a,b){a||(a=this.factory.time.getHourCounter()),this.autoIncrement(),this.base(a,b)},appendDigitToClock:function(a){this.base(a),this.dividers[0].insertAfter(this.dividers[0].next())}})}(jQuery),function(a){FlipClock.MinuteCounterFace=FlipClock.HourlyCounterFace.extend({clearExcessDigits:!1,constructor:function(a,b){this.base(a,b)},build:function(){this.base(!0,this.factory.time.getMinuteCounter())},flip:function(a,b){a||(a=this.factory.time.getMinuteCounter()),this.base(a,b)}})}(jQuery),function(a){FlipClock.TwelveHourClockFace=FlipClock.TwentyFourHourClockFace.extend({meridium:!1,meridiumText:"AM",build:function(){var b=this.factory.time.getTime(!1,this.showSeconds);this.base(b),this.meridiumText=this.getMeridium(),this.meridium=a(['"].join("")),this.meridium.insertAfter(this.lists[this.lists.length-1].$el)},flip:function(a,b){this.meridiumText!=this.getMeridium()&&(this.meridiumText=this.getMeridium(),this.meridium.find("a").html(this.meridiumText)),this.base(this.factory.time.getTime(!1,this.showSeconds),b)},getMeridium:function(){return(new Date).getHours()>=12?"PM":"AM"},isPM:function(){return"PM"==this.getMeridium()?!0:!1},isAM:function(){return"AM"==this.getMeridium()?!0:!1}})}(jQuery),function(a){FlipClock.Lang.Arabic={years:"سنوات",months:"شهور",days:"أيام",hours:"ساعات",minutes:"دقائق",seconds:"ثواني"},FlipClock.Lang.ar=FlipClock.Lang.Arabic,FlipClock.Lang["ar-ar"]=FlipClock.Lang.Arabic,FlipClock.Lang.arabic=FlipClock.Lang.Arabic}(jQuery),function(a){FlipClock.Lang.Danish={years:"År",months:"Måneder",days:"Dage",hours:"Timer",minutes:"Minutter",seconds:"Sekunder"},FlipClock.Lang.da=FlipClock.Lang.Danish,FlipClock.Lang["da-dk"]=FlipClock.Lang.Danish,FlipClock.Lang.danish=FlipClock.Lang.Danish}(jQuery),function(a){FlipClock.Lang.German={years:"Jahre",months:"Monate",days:"Tage",hours:"Stunden",minutes:"Minuten",seconds:"Sekunden"},FlipClock.Lang.de=FlipClock.Lang.German,FlipClock.Lang["de-de"]=FlipClock.Lang.German,FlipClock.Lang.german=FlipClock.Lang.German}(jQuery),function(a){FlipClock.Lang.English={years:"Years",months:"Months",days:"Days",hours:"Hours",minutes:"Minutes",seconds:"Seconds"},FlipClock.Lang.en=FlipClock.Lang.English,FlipClock.Lang["en-us"]=FlipClock.Lang.English,FlipClock.Lang.english=FlipClock.Lang.English}(jQuery),function(a){FlipClock.Lang.Spanish={years:"Años",months:"Meses",days:"Días",hours:"Horas",minutes:"Minutos",seconds:"Segundos"},FlipClock.Lang.es=FlipClock.Lang.Spanish,FlipClock.Lang["es-es"]=FlipClock.Lang.Spanish,FlipClock.Lang.spanish=FlipClock.Lang.Spanish}(jQuery),function(a){FlipClock.Lang.Finnish={years:"Vuotta",months:"Kuukautta",days:"Päivää",hours:"Tuntia",minutes:"Minuuttia",seconds:"Sekuntia"},FlipClock.Lang.fi=FlipClock.Lang.Finnish,FlipClock.Lang["fi-fi"]=FlipClock.Lang.Finnish,FlipClock.Lang.finnish=FlipClock.Lang.Finnish}(jQuery),function(a){FlipClock.Lang.French={years:"Ans",months:"Mois",days:"Jours",hours:"Heures",minutes:"Minutes",seconds:"Secondes"},FlipClock.Lang.fr=FlipClock.Lang.French,FlipClock.Lang["fr-ca"]=FlipClock.Lang.French,FlipClock.Lang.french=FlipClock.Lang.French}(jQuery),function(a){FlipClock.Lang.Italian={years:"Anni",months:"Mesi",days:"Giorni",hours:"Ore",minutes:"Minuti",seconds:"Secondi"},FlipClock.Lang.it=FlipClock.Lang.Italian,FlipClock.Lang["it-it"]=FlipClock.Lang.Italian,FlipClock.Lang.italian=FlipClock.Lang.Italian}(jQuery),function(a){FlipClock.Lang.Latvian={years:"Gadi",months:"Mēneši",days:"Dienas",hours:"Stundas",minutes:"Minūtes",seconds:"Sekundes"},FlipClock.Lang.lv=FlipClock.Lang.Latvian,FlipClock.Lang["lv-lv"]=FlipClock.Lang.Latvian,FlipClock.Lang.latvian=FlipClock.Lang.Latvian}(jQuery),function(a){FlipClock.Lang.Dutch={years:"Jaren",months:"Maanden",days:"Dagen",hours:"Uren",minutes:"Minuten",seconds:"Seconden"},FlipClock.Lang.nl=FlipClock.Lang.Dutch,FlipClock.Lang["nl-be"]=FlipClock.Lang.Dutch,FlipClock.Lang.dutch=FlipClock.Lang.Dutch}(jQuery),function(a){FlipClock.Lang.Norwegian={years:"År",months:"Måneder",days:"Dager",hours:"Timer",minutes:"Minutter",seconds:"Sekunder"},FlipClock.Lang.no=FlipClock.Lang.Norwegian,FlipClock.Lang.nb=FlipClock.Lang.Norwegian,FlipClock.Lang["no-nb"]=FlipClock.Lang.Norwegian,FlipClock.Lang.norwegian=FlipClock.Lang.Norwegian}(jQuery),function(a){FlipClock.Lang.Portuguese={years:"Anos",months:"Meses",days:"Dias",hours:"Horas",minutes:"Minutos",seconds:"Segundos"},FlipClock.Lang.pt=FlipClock.Lang.Portuguese,FlipClock.Lang["pt-br"]=FlipClock.Lang.Portuguese,FlipClock.Lang.portuguese=FlipClock.Lang.Portuguese}(jQuery),function(a){FlipClock.Lang.Russian={years:"лет",months:"месяцев",days:"дней",hours:"часов",minutes:"минут",seconds:"секунд"},FlipClock.Lang.ru=FlipClock.Lang.Russian,FlipClock.Lang["ru-ru"]=FlipClock.Lang.Russian,FlipClock.Lang.russian=FlipClock.Lang.Russian}(jQuery),function(a){FlipClock.Lang.Swedish={years:"År",months:"Månader",days:"Dagar",hours:"Timmar",minutes:"Minuter",seconds:"Sekunder"},FlipClock.Lang.sv=FlipClock.Lang.Swedish,FlipClock.Lang["sv-se"]=FlipClock.Lang.Swedish,FlipClock.Lang.swedish=FlipClock.Lang.Swedish}(jQuery),function(a){FlipClock.Lang.Chinese={years:"年",months:"月",days:"日",hours:"时",minutes:"分",seconds:"秒"},FlipClock.Lang.zh=FlipClock.Lang.Chinese,FlipClock.Lang["zh-cn"]=FlipClock.Lang.Chinese,FlipClock.Lang.chinese=FlipClock.Lang.Chinese}(jQuery); -------------------------------------------------------------------------------- /views/dashboard.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | link(rel="stylesheet", type="text/css", href="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.2.9/semantic.min.css") 5 | link(href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css", rel="stylesheet", integrity="sha384-wvfXpqpZZVQGK6TAh5PVlGOfQNHSoD2xbE+QkPxCAFlNEevoEH3Sl0sibVcOQVnN", crossorigin="anonymous") 6 | 7 | link(href="https://fonts.googleapis.com/css?family=Source+Sans+Pro" rel="stylesheet") 8 | link(href="css/styles.css", rel="stylesheet") 9 | 10 | body 11 | 12 | // Navbar 13 | .ui.fixed.yellow.inverted.secondary.menu 14 | a.item 15 | i.fa.fa-bars 16 | a.item.red Dashboard 17 | .right.menu 18 | a.item#menu-profile-icon 19 | img.ui.circular.mini.image(src=pic) 20 | #profile-popup.ui.popup 21 | .ui.grid 22 | .row 23 | .item 24 | .ui.tiny.circular.image 25 | img(src=pic) 26 | .middle.aligned.content 27 | .header(style="color: grey; font-weight: bold; font-size: 16px")= name || 'Guest' 28 | a.ui.google.plus.labeled.icon.mini.button(href="https://myaccount.google.com" style="margin-top: 10px") 29 | i.google.plus.icon 30 | | My account 31 | 32 | .row(style="background-color: #ECEFF1") 33 | .column 34 | a.ui.primary.button(href="/logout") Sign Out 35 | 36 | 37 | 38 | // Main content 39 | .ui.inverted.yellow.vertical.segment(style="padding-top: 40px; min-height: 100%; background-image: url('./img/lego.svg'); background-repeat: repeat") 40 | 41 | // Torrent error and success messages (if any) 42 | .torrent.messages(style="padding-top: 15px") 43 | #torrent-errors 44 | #torrent-success 45 | 46 | .ui.container 47 | .ui.grid(style="padding-top: 40px") 48 | .row 49 | .left.floated.column 50 | h1.ui.inverted.header(style="font-family: Source Sans Pro; font-size: 3rem; font-weight: 300") 51 | span(style="padding: 5px 10px; background: rgba(0, 0, 0, 0.125)") Hi #{name || 'Guest'} 52 | 53 | .centered.row 54 | .ui.steps 55 | .completed.disabled.step 56 | i.google.icon(style="color: #4885ed") 57 | .content 58 | .title Login 59 | .description with your Google Account 60 | a#upload-step.active.step 61 | i.magnet.green.icon 62 | .content 63 | .title Upload 64 | .description your torrent file 65 | a.active.step(href=`${driveUrl}`, target="_blank") 66 | i.desktop.orange.icon 67 | .content 68 | .title View 69 | .description files in Google Drive 70 | 71 | // If no torrents 72 | #zero-torrents.centered.row(style="display: none") 73 | .add-torrent.ui.pink.button 74 | .ui.items 75 | .item 76 | i.ui.add.to.cart.large.icon 77 | .middle.aligned.content You have nothing here yet. Click to add your first torrent 78 | // If torrents 79 | #if-torrents.centered.row(style="display: none") 80 | .ui.vertical.left.aligned.segment(style="width: 80%") 81 | #active-torrents 82 | 83 | // Initial upload modal 84 | #add-torrent-modal.ui.basic.modal 85 | i.close.icon 86 | .header Upload your torrent file 87 | #torrent-upload-form.ui.form.image.content 88 | #torrent-upload.ui.medium.image.dropzone 89 | .ui.fade.reveal.clickable 90 | .visible.content 91 | img(src="https://www.booklogix.com/application/files/5214/1858/8110/FileUpload.png") 92 | .hidden.content 93 | img(src="http://samcloud.spacial.com/Images/Drop-Files-Here-extra.png") 94 | 95 | .description 96 | .ui.inverted.header 97 | i.ui.pointing.left.icon 98 | | Click to add your .torrent file 99 | .ui.action.input 100 | input(type="text", name="magnet", placeholder="Or paste your magnet link here", style="width: 300px") 101 | .ui.teal.right.labeled.icon.submit.button Go 102 | i.upload.icon 103 | .ui.yellow.error.message // for invalid magnet regex 104 | .ui.hidden.divider 105 | 106 | #torrent-upload-progress 107 | .dz-error-message(style="color: orange") 108 | 109 | #torrent-upload-spinner(style="display: none") 110 | .ui.processing.active.dimmer 111 | .ui.indeterminate.text.loader Processing torrent 112 | .actions 113 | .ui.negative.right.labeled.icon.button Close 114 | i.remove.icon 115 | .ui.positive.right.labeled.icon.button Done 116 | i.checkmark.icon 117 | 118 | 119 | // (De)select torrent files modal 120 | #update-torrent-files-modal.ui.modal 121 | .header Select files to download 122 | .ui.green.tiny.select-all.button(style="margin-left: 20px") Select all 123 | .ui.red.tiny.clear-all.button(style="margin-left: 10px") Clear all 124 | .content 125 | #torrent-files-select-form.ui.form 126 | .field 127 | .ui.relaxed.list 128 | .actions 129 | .ui.positive.right.labeled.icon.button Done 130 | i.checkmark.icon 131 | 132 | // Delete torrent modal 133 | #delete-torrent-modal.ui.modal 134 | .header Delete torrent? 135 | .image.content 136 | .ui.small.image 137 | img(src="./img/trash.ico") 138 | .description 139 | .ui.header You are trying to delete torrent 140 | ul 141 | li 142 | p Proceed? 143 | p(style="color: orange") Note: This action will not remove files in your Google Drive. 144 | .actions 145 | .ui.negative.right.labeled.icon.button Cancel 146 | i.remove.icon 147 | .ui.positive.right.labeled.icon.button Proceed 148 | i.checkmark.icon 149 | 150 | script(src="https://code.jquery.com/jquery-3.1.1.min.js", integrity="sha256-hVVnYaiADRTO2PzUGmuLJr8BLUSjGIZsDYGmIJLv2b8=", crossorigin="anonymous") 151 | script(src="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.2.9/semantic.min.js") 152 | script(src="https://cdnjs.cloudflare.com/ajax/libs/dropzone/4.3.0/min/dropzone.min.js") 153 | script(src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.0.4/socket.io.js") 154 | script(src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.17.1/moment.min.js") 155 | script(src="https://cdnjs.cloudflare.com/ajax/libs/moment-duration-format/1.3.0/moment-duration-format.min.js") 156 | script(src="https://cdnjs.cloudflare.com/ajax/libs/filesize/3.5.10/filesize.min.js") 157 | script. 158 | /* PART I: page interaction */ 159 | // profile icon menu bar 160 | $('#menu-profile-icon').popup({ 161 | popup: $('#profile-popup'), 162 | on: 'click' 163 | }); 164 | 165 | // upload modal 166 | $('#upload-step, .add-torrent.button').click(() => { 167 | $('#add-torrent-modal').modal('show'); 168 | }); 169 | 170 | // dismissible success / error messages 171 | $('body').on('click', '.message .close', (event) => { 172 | $(event.target).closest('.message').transition('fade'); 173 | }); 174 | 175 | // magnet input field in upload modal 176 | $('#torrent-upload-form').form({ 177 | fields: { 178 | magnet: { 179 | identifier: 'magnet', 180 | rules: [{ 181 | type: 'regExp', 182 | value: '/^magnet:\\?/', 183 | prompt: 'Please enter a valid magnet link. It should begin with magnet:?' 184 | }] 185 | } 186 | }, 187 | onSuccess: (event, fields) => { 188 | $('#torrent-upload-progress').html(` 189 |
    190 | You have added magnet: 191 |
  • 192 | ${fields.magnet} 193 |
  • 194 |
    195 | `); 196 | torrentUploadSpinner.show(); 197 | $.ajax({ 198 | type: 'POST', 199 | url: '/add-torrent', 200 | data: { magnet: fields.magnet.trim() }, 201 | success: (data) => addTorrentCallback(null, data), 202 | error: (xhr) => addTorrentCallback(xhr.responseJSON), 203 | timeout: 20000 // 20 seconds 204 | }); 205 | } 206 | }); 207 | 208 | // .torrent file upload in upload modal 209 | Dropzone.autoDiscover = false; 210 | const dropZone = new Dropzone('#torrent-upload', { 211 | url: '/add-torrent', 212 | paramName: "torrent", // The name that will be used to transfer the file 213 | maxFilesize: 1, // MB 214 | clickable: '.ui.fade.reveal.clickable', 215 | previewsContainer: '#torrent-upload-progress', 216 | previewTemplate: ` 217 |
    218 | You have chosen file: 219 |
  • 220 |      () 221 |
  • 222 |
    223 | `, 224 | dictDefaultMessage: '', 225 | maxFiles: 1, 226 | init: function() { 227 | this.on('addedfile', (file) => { 228 | if (this.files.length > 1) { 229 | this.removeFile(this.files[0]); 230 | } 231 | }); 232 | 233 | this.on('success', (file, data) => { 234 | addTorrentCallback(null, data); 235 | }); 236 | 237 | this.on('error', (file, err) => { 238 | addTorrentCallback(err) 239 | }); 240 | }, 241 | accept: function(file, done) { 242 | if (!file.name.endsWith('.torrent')) { 243 | return done('Only .torrent files are supported'); 244 | } 245 | if (file.size == 0) { 246 | return done('File must not be empty'); 247 | } 248 | done(); 249 | }, 250 | sending: function() { 251 | torrentUploadSpinner.show(); 252 | } 253 | }); 254 | 255 | script. 256 | /* PART II: torrent interaction utility functions */ 257 | // server callback from POST /add-torrent 258 | const addTorrentCallback = (err, torrent) => { 259 | torrentUploadSpinner.hide(); 260 | if (err) { 261 | return $('.dz-error-message').html(` 262 | Error:  ${err.message}.
    263 | Please try again. 264 | `); 265 | } 266 | showUpdateTorrentFilesModal(torrent); 267 | }; 268 | 269 | // show spinner while waiting for server response during /add-torrent 270 | const torrentUploadSpinner = (() => { 271 | let task = null; 272 | return { 273 | show: () => { 274 | task = setTimeout(() => { 275 | $('#torrent-upload-spinner').fadeIn(); 276 | }, 500); 277 | }, 278 | hide: () => { 279 | clearTimeout(task); 280 | $('#torrent-upload-spinner').hide(); 281 | } 282 | } 283 | })(); 284 | 285 | // get all torrents 286 | const getTorrents = () => { 287 | $.get('/get-torrents', (torrents) => { 288 | if (torrents.length) { 289 | $('#zero-torrents').hide(); 290 | $('#if-torrents').show(); 291 | } else { 292 | $('#zero-torrents').show(); 293 | $('#if-torrents').hide(); 294 | } 295 | updatePageWithTorrents(torrents); 296 | }); 297 | } 298 | 299 | // delete torrent 300 | const deleteTorrent = (torrent) => { 301 | $.post('/delete-torrent', {infoHash: torrent.infoHash}) 302 | .done(() => { 303 | showTorrentSuccess([torrent], 'Removed torrent'); 304 | }) 305 | .always(() => { 306 | $(`#torrent-${torrent.infoHash}`).remove(); 307 | }); 308 | } 309 | 310 | // display torrents on page 311 | const updatePageWithTorrents = (torrents) => { 312 | torrents.forEach((torrent) => { 313 | const name = torrent.name; 314 | const size = filesize(torrent.size); // B, KB, MB, etc... 315 | const downloaded = filesize(torrent.downloaded); 316 | const uploaded = filesize(torrent.uploaded); 317 | const received = filesize(torrent.received); 318 | const downloadSpeed = `${filesize(torrent.downloadSpeed)}/s`; // B/s, KB/s, MB/s, etc... 319 | const uploadSpeed = `${filesize(torrent.uploadSpeed)}/s`; 320 | const eta = torrent.timeRemaining >= 0? 321 | moment.duration(torrent.timeRemaining).format('d[d] h[h] m[m] s[s]'): // seconds 322 | '∞'; // infinity if eta is null 323 | 324 | if ($(`#torrent-${torrent.infoHash}`).length) { 325 | // torrent already exists in page, update bar and table only 326 | $(`#name-${torrent.infoHash}`).text(name); 327 | $(`#filesize-${torrent.infoHash}`).text(size); 328 | 329 | const table = $(`#table-${torrent.infoHash}`); 330 | table.find('.downloaded').first().text(downloaded); 331 | table.find('.uploaded').first().text(uploaded); 332 | table.find('.eta').first().text(eta); 333 | table.find('.received').first().text(received); 334 | table.find('.ratio').first().text(torrent.ratio); 335 | table.find('.peers').first().text(torrent.peers); 336 | } else { 337 | // display new torrent in page 338 | const html = ` 339 |
    340 |
    341 |
    342 | ${torrent.name} 343 |   344 | (${filesize(torrent.size)}) 345 |
    346 |
    347 |
    348 |
    349 |
    350 |
    351 |
    352 |
    353 |
    354 |
    355 |
    356 |
    357 |
    358 | 359 | Torrent info 360 |
    361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 |
    370 |
    371 |
    372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 |
    DownloadedUploadedETAReceivedRatioPeers
    ${downloaded}${uploaded}${eta}${received}${torrent.ratio}${torrent.numPeers}
    394 |
    395 |
    396 |
    397 |
    398 | `; 399 | $(html).hide().appendTo('#active-torrents').fadeIn(); 400 | 401 | // delete torrent icon 402 | $(`#torrent-${torrent.infoHash} .torrent.trash.icon`).click(() => { 403 | $('#delete-torrent-modal li').text(`${torrent.name} (${filesize(torrent.size)})`) 404 | $('#delete-torrent-modal') 405 | .modal({ 406 | closable: false, 407 | onApprove: () => deleteTorrent(torrent) 408 | }) 409 | .modal('show'); 410 | }); 411 | } 412 | // update torrent progress 413 | $(`#progress-${torrent.infoHash}`).progress({ 414 | label: 'percent', 415 | percent: torrent.progress * 100, 416 | text: { 417 | active: `Down ${downloadSpeed} Up ${uploadSpeed} ETA ${eta}`, 418 | success: 'Complete' 419 | } 420 | }); 421 | // update torrent files modal checkboxes 422 | $(`#torrent-${torrent.infoHash} .torrent.settings.icon`).off('click').click(() => { 423 | showUpdateTorrentFilesModal(torrent); 424 | }); 425 | // update modal / drive shortcut visibility 426 | if (torrent.driveUrl) { 427 | const downloadIcon = $(`#torrent-${torrent.infoHash} a:has(.torrent.cloud.download.icon)`); 428 | downloadIcon.attr('href', `download/${torrent.infoHash}`); 429 | downloadIcon.show(); 430 | 431 | const driveIcon = $(`#torrent-${torrent.infoHash} a:has(.torrent.folder.open.icon)`); 432 | driveIcon.attr('href', torrent.driveUrl); 433 | driveIcon.show(); 434 | } 435 | }); 436 | $('.ui.accordion').accordion(); 437 | } 438 | 439 | // display error messages 440 | const showTorrentErrors = (torrents) => { 441 | torrents.map((torrent) => { 442 | const html = ` 443 |
    444 | 445 |
    446 | 447 | Download failed 448 |
    449 |
      450 |
    • Torrent: ${torrent.name} (${filesize(torrent.size)})
    • 451 |
    • Error: ${torrent.error}
    • 452 |
    453 |
    454 | `; 455 | $(html).hide().appendTo('#torrent-errors').fadeIn(); 456 | }); 457 | } 458 | 459 | // display success messages 460 | const showTorrentSuccess = (torrents, overrideMessage) => { 461 | const successMessage = overrideMessage? overrideMessage: 'Download success'; 462 | torrents.map((torrent) => { 463 | const html = ` 464 |
    465 | 466 |
    467 | 468 | ${successMessage} 469 |
    470 |
      471 |
    • Torrent: ${torrent.name} (${filesize(torrent.size)})
    • 472 |
    473 |
    474 | `; 475 | $(html).hide().appendTo('#torrent-success').fadeIn(); 476 | }); 477 | } 478 | 479 | // display #update-torrent-files-modal 480 | const showUpdateTorrentFilesModal = (torrent) => { 481 | const fileListHtml = torrent.files.map((file) => { 482 | return ` 483 |
    484 |
    485 | 486 | 487 |
    488 | ${ 489 | file.progress == 1? 490 | ` 491 | 492 | `: 493 | '' 494 | } 495 |
    496 | `; 497 | }); 498 | $('#update-torrent-files-modal .list').html(fileListHtml); 499 | $('#update-torrent-files-modal') 500 | .modal({ 501 | closable: false, 502 | onApprove: () => { 503 | const selectedFiles = []; 504 | $(".ui.relaxed.list input[type='checkbox']").each((i, checkbox) => { 505 | selectedFiles.push(checkbox.checked); 506 | }); 507 | $.post('/update-torrent', { 508 | infoHash: torrent.infoHash, 509 | selectedFiles: selectedFiles 510 | }); 511 | } 512 | }) 513 | .modal('show'); 514 | 515 | // Set / clear all checkboxes when labels are clicked 516 | $('#update-torrent-files-modal .select-all').click(() => { 517 | $('#update-torrent-files-modal input[type="checkbox"]').prop('checked', true); 518 | }); 519 | $('#update-torrent-files-modal .clear-all').click(() => { 520 | $('#update-torrent-files-modal input[type="checkbox"]').prop('checked', false); 521 | }); 522 | } 523 | 524 | 525 | script. 526 | // PART III. Data transfer 527 | // Socket.io code goes here 528 | const socket = io(); 529 | 530 | socket.on('all-torrents', (torrents) => { 531 | if (torrents.length) { 532 | $('#zero-torrents').hide(); 533 | $('#if-torrents').show(); 534 | } else { 535 | $('#zero-torrents').show(); 536 | $('#if-torrents').hide(); 537 | } 538 | updatePageWithTorrents(torrents); 539 | }); 540 | 541 | socket.on('torrent-update', (torrents) => { 542 | updatePageWithTorrents(torrents); 543 | }); 544 | 545 | socket.on('torrent-error', (torrents) => { 546 | showTorrentErrors(torrents); 547 | }); 548 | 549 | socket.on('torrent-success', (torrents) => { 550 | showTorrentSuccess(torrents); 551 | }); 552 | --------------------------------------------------------------------------------