diff --git a/app/Controllers/Http/DownloadController.js b/app/Controllers/Http/DownloadController.js index e6d0654..6fb46c6 100644 --- a/app/Controllers/Http/DownloadController.js +++ b/app/Controllers/Http/DownloadController.js @@ -3,15 +3,15 @@ const youtubedl = require('youtube-dl') const fs = require('fs') const ffmpeg = require('fluent-ffmpeg') const { version } = require('../../../package.json'); -const Antl = use('Antl') +const Ws = use('Ws'); +const Antl = use('Antl'); let viewCounter = 0; let files = []; let day; let month; let announcementArray; -let announcement - +let announcement; function formatBytes(bytes, decimals = 2) { // https://stackoverflow.com/a/18650828 if (bytes === 0) return '0 Bytes'; @@ -30,17 +30,16 @@ class DownloadController { async index ({ view, request, locale }) { viewCounter++; // Coudln't find a cleaner way to make it change with the browser locale - announcementArray = [Antl.forLocale(locale).formatMessage('announcement.1'), Antl.forLocale(locale).formatMessage('announcement.2'), Antl.forLocale(locale).formatMessage('announcement.3'), Antl.forLocale(locale).formatMessage('announcement.4'), Antl.forLocale(locale).formatMessage('announcement.5'), Antl.forLocale(locale).formatMessage('announcement.6')]; + announcementArray = [Antl.forLocale(locale).formatMessage('announcement.1'), Antl.forLocale(locale).formatMessage('announcement.2'), Antl.forLocale(locale).formatMessage('announcement.3'), Antl.forLocale(locale).formatMessage('announcement.4'), Antl.forLocale(locale).formatMessage('announcement.5'), Antl.forLocale(locale).formatMessage('announcement.6'), 'Playlist download is experimental']; // Get random announcement announcement = announcementArray[Math.floor(Math.random() * announcementArray.length)]; - // Get date for some event let today = new Date(); day = today.getDay(); month = today.getMonth(); // If legacy link return if (request.url() == '/legacy') return view.render('legacy', { version: version, viewCounter: viewCounter, day: day, month: month, announcement: announcement}); - + files = []; let file = []; for (let f of fs.readdirSync('./public/uploads')) { @@ -48,23 +47,23 @@ class DownloadController { } // get the 5 most recent files file = file.sort((a, b) => { - if ((a || b).endsWith('.mp4') || (a || b).endsWith('.webm') || (a || b).endsWith('.mp3') || (a || b).endsWith('.flac')) { + if ((a || b).endsWith('.mp4') || (a || b).endsWith('.webm') || (a || b).endsWith('.mp3') || (a || b).endsWith('.flac') || (a || b).endsWith('.zip')) { let time1 = fs.statSync(`./public/uploads/${b}`).ctime; - let time2 = fs.statSync(`./public/uploads/${a}`).ctime; + let time2 = fs.statSync(`./public/uploads/${a}`).ctime; if (time1 < time2) return -1; if (time1 > time2) return 1; } return 0; }).slice(0, 5) - - // Save space by deleting file that doesn't appear in the recent feed + + // Save space by deleting file that doesn't appear in the recent feed for (let f of fs.readdirSync('./public/uploads')) { - if (!file.includes(f) && (f != 'hidden' && f != '.keep')) { + if (!file.includes(f) && (f != 'hidden' && f != '.keep' && f != 'playlist')) { fs.unlinkSync(`./public/uploads/${f}`); } } - for (let f of file) { + for (let f of file) { if (f.endsWith('.mp4') || f.endsWith('.webm')) { // Send file name, file size in MB relative path for the file let fileInfo = formatBytes(fs.statSync(`./public/uploads/${f}`).size).split(' '); @@ -75,12 +74,12 @@ class DownloadController { files.push({ name: f.split('.').slice(0, -1).join('.'), size: fileInfo[0], unit: fileInfo[1], date: fs.statSync(`./public/uploads/${f}`).ctime, location: `uploads/${f}`, ext: f.split('.').pop(), img: `/asset/music.png` }); } } + return view.render('index', { version: version, viewCounter: viewCounter, file: files, day: day, month: month, announcement: announcement}); } async download({ view, request, response }) { - let page = 'index'; - if (response.request.url == '/legacy') page = 'legacy'; + const ws = Ws.getChannel('progress').topic('progress'); // To be honest i forgot what it does, but i think i need it response.implicitEnd = false @@ -95,14 +94,10 @@ class DownloadController { } if (!data.url) { - return view.render(page, { - version: version, - viewCounter: viewCounter, - file: files, - day: day, month: month, announcement: announcement , - error: true, - errormsg: 'bruh moment, you didin\'t input a link.' - }); + if (ws) { + ws.socket.emit('error', 'bruh moment, you didin\'t input a link.'); + } + return; } // Youtube-dl quality settings @@ -125,74 +120,175 @@ class DownloadController { if (err); }); } - + return youtubedl.exec(data.url, ['--format=mp4', '-o', altFolder], {}, function(err, output) { if (err) { console.error(err); - return response.send(view.render(page, { - version: version, - viewCounter: viewCounter, - file: files, - day: day, month: month, announcement: announcement , - error: true, - errormsg: err - })); + if (ws) { + ws.socket.emit('error', err.toString()); + } + return; } - - return response.attachment(altFolder); + console.log(altFolder.slice(17)) + if (ws) { + ws.socket.emit('end', altFolder.slice(17)); + } + return; }); } else { - // Download as mp4 if possible - let video = youtubedl(data.url, ['--format=mp4', '-f', option]); + if (data.url.match( /^.*(youtu.be\/|list=)([^#\&\?]*).*/)) { + playlistDownload(data) + } else { + // Download as mp4 if possible + let video = youtubedl(data.url, ['--format=mp4', '-f', option]); + + video.on('error', function(err) { + console.error(err); + if (ws) { + ws.socket.emit('error', err.toString()); + } + return; + }) + + let ext; + let size = 0 + video.on('info', function(info) { + size = info.size + // Set file name + ext = info.ext; + let title = info.title.slice(0,50); + DLFile = `${title.replace(/\s/g, '_')}.${ext}`; + DLFile = DLFile.replace(/[()]|[/]|[\\]|[?]|[!]/g, '_'); + + // If no title use the ID + if (title == '_') title = `_${info.id}`; + // If user want to hide from the feed + if (data.feed == 'on') + DLFile = `hidden/${title}.${ext}`; + + video.pipe(fs.createWriteStream(`./public/uploads/${DLFile}`)); + }); + + let pos = 0 + video.on('data', function data(chunk) { + pos += chunk.length + // `size` should not be 0 here. + if (size) { + let percent = (pos / size * 100).toFixed(2) + if (ws) { + ws.socket.emit('progress', percent); + } + } + }) - video.on('error', function(err) { + video.on('end', function() { + console.log('end'); + if (ws) { + ws.socket.emit('message', 'end'); + } + if (data.format == 'mp4' || data.format == 'webm') { + // If user requested mp4 directly attach the file + if (ws) { + ws.socket.emit('end', DLFile); + } + return; + } else { + // If user requested an audio format, convert it + ffmpeg(`./public/uploads/${DLFile}`) + .noVideo() + .audioChannels('2') + .audioFrequency('44100') + .audioBitrate('320k') + .format(data.format) + .save(`./public/uploads/${DLFile.replace(`.${ext}`, `.${data.format}`)}`) + .on('progress', (progress) => { + wb.broadcast(progress.percent) + }) + .on('end', () => { + fs.unlinkSync(`./public/uploads/${DLFile}`); + if (ws) { + ws.socket.emit('end', DLFile.replace(`.${ext}`, `.${data.format}`)); + } + }); + } + }); + } + } + + function playlistDownload(data) { + const video = youtubedl(data.url) + + video.on('error', function error(err) { console.error(err); - return response.send(view.render(page, { - version: version, - viewCounter: viewCounter, - file: files, - day: day, month: month, announcement: announcement , - error: true, - errormsg: err - })); - }) + if (ws) { + ws.socket.emit('error', err.toString()); + } + return; + }); let ext; + let size = 0 video.on('info', function(info) { + console.log(info); + size = info.size // Set file name ext = info.ext; let title = info.title.slice(0,50); DLFile = `${title.replace(/\s/g, '_')}.${ext}`; - DLFile = DLFile.replace(/[()]|[/]|[\\]/g, '_'); + DLFile = DLFile.replace(/[()]|[/]|[\\]|[?]|[!]/g, '_'); // If no title use the ID if (title == '_') title = `_${info.id}`; - // If user want to hide from the feed - if (data.feed == 'on') - DLFile = `hidden/${title}.${ext}`; + // If user want to hide from the feed + if (data.feed == 'on') + DLFile = `hidden/playlist/${title}.${ext}`; - video.pipe(fs.createWriteStream(`./public/uploads/${DLFile}`)); + video.pipe(fs.createWriteStream(`./public/uploads/playlist/${DLFile}`)); + }); + + + let pos = 0 + video.on('data', function data(chunk) { + pos += chunk.length + // `size` should not be 0 here. + if (size) { + let percent = (pos / size * 100).toFixed(2) + process.stdout.cursorTo(0) + process.stdout.clearLine(1) + if (ws) { + ws.socket.emit('progress', percent); + } + } }); video.on('end', function() { - if (data.format == 'mp4' || data.format == 'webm') { - // If user requested mp4 directly attach the file - return response.attachment(`./public/uploads/${DLFile}`) - } else { + if (data.format == 'mp3' || data.format == 'flac') { // If user requested an audio format, convert it - ffmpeg(`./public/uploads/${DLFile}`) + ffmpeg(`./public/uploads/playlist/${DLFile}`) .noVideo() .audioChannels('2') .audioFrequency('44100') .audioBitrate('320k') .format(data.format) - .save(`./public/uploads/${DLFile.replace(`.${ext}`, `.${data.format}`)}`) - .on('end', () => { - fs.unlinkSync(`./public/uploads/${DLFile}`); - return response.attachment(`./public/uploads/${DLFile.replace(`.${ext}`, `.${data.format}`)}`); + .save(`./public/uploads/playlist/${DLFile.replace(`.${ext}`, `.${data.format}`)}`) + .on('progress', (progress) => { + ws.socket.emit(progress.percent) }) + .on('end', () => { + fs.unlinkSync(`./public/uploads/playlist/${DLFile}`); + if (ws) { + ws.socket.emit('end', `./public/uploads/playlist/${DLFile.replace(`.${ext}`, `.${data.format}`)}`); + } + }); + } else { + if (ws) { + ws.socket.emit('end', `./public/uploads/playlist/${DLFile}`); + } } }); + + video.on('next', playlistDownload); + } } } diff --git a/app/Controllers/Ws/ProgressController.js b/app/Controllers/Ws/ProgressController.js new file mode 100644 index 0000000..51c2602 --- /dev/null +++ b/app/Controllers/Ws/ProgressController.js @@ -0,0 +1,10 @@ +'use strict' + +class ProgressController { + constructor ({ socket, request }) { + this.socket = socket + this.request = request + } +} + +module.exports = ProgressController diff --git a/config/socket.js b/config/socket.js new file mode 100644 index 0000000..007a677 --- /dev/null +++ b/config/socket.js @@ -0,0 +1,66 @@ +'use strict' + +/* +|-------------------------------------------------------------------------- +| Websocket Config +|-------------------------------------------------------------------------- +| +| Used by AdonisJs websocket server +| +*/ +module.exports = { + /* + |-------------------------------------------------------------------------- + | Path + |-------------------------------------------------------------------------- + | + | The base path on which the websocket server will accept connections. + | + */ + path: '/adonis-ws', + + /* + |-------------------------------------------------------------------------- + | Server Interval + |-------------------------------------------------------------------------- + | + | This interval is used to create a timer for identifying dead client + | connections. + | + */ + serverInterval: 30000, + + /* + |-------------------------------------------------------------------------- + | Server Attempts + |-------------------------------------------------------------------------- + | + | Server attempts are used with serverInterval to identify dead client + | connections. A total of `serverAttempts` attmepts after `serverInterval` + | will be made before terminating the client connection. + | + */ + serverAttempts: 3, + + /* + |-------------------------------------------------------------------------- + | Client Interval + |-------------------------------------------------------------------------- + | + | This interval is used by client to send ping frames to the server. + | + */ + clientInterval: 25000, + + /* + |-------------------------------------------------------------------------- + | Client Attempts + |-------------------------------------------------------------------------- + | + | Clients attempts are number of times the client will attempt to send the + | ping, without receiving a pong from the server. After attempts have + | been elapsed, the client will consider server as dead. + | + */ + clientAttempts: 3 +} diff --git a/package.json b/package.json index 2a0b645..58e1481 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "jeff-downloader", - "version": "0.13.0", + "version": "0.14.0", "adonis-version": "4.1.0", "description": "A video downloader based on youtube-dl", "main": "server.js", @@ -30,8 +30,11 @@ "@adonisjs/session": "^1.0.27", "@adonisjs/shield": "^1.0.8", "@adonisjs/validator": "^5.0.6", + "@adonisjs/websocket": "^1.0.12", + "@adonisjs/websocket-client": "^1.0.9", + "archiver": "^3.1.1", "fluent-ffmpeg": "^2.1.2", - "mysql": "^2.17.1", + "mysql": "^2.18.1", "node-fetch": "^2.6.0", "youtube-dl": "^1.13.1" }, diff --git a/public/JS/tis.min.js b/public/JS/tis.min.js new file mode 100644 index 0000000..4be2377 --- /dev/null +++ b/public/JS/tis.min.js @@ -0,0 +1 @@ +!function(){var e,t=document,i="addEventListener",o="charCodeAt",n="keyCode",r="keydown",f=0;t[i](r,function(c){f=c[n]=="&&((%'%'BA"[o](f)?f+1:0,!e&&f>9&&(e=3,function(){function f(e){if(k=j[e])k.currentTime=0;else{k=new Uint8Array(9e3),O=50;for(m in k)N=m>1e3?e>>4&63:e>>10,k[m]=127+(O*=1-(15&e)/1e4)*(N/2>m/10%N?-1:1);k=j[y]=c(k)}k.play()}function c(e){return k=e.length,new Audio(URL.createObjectURL(new Blob(["RIFF",new Uint32Array([k+36,1163280727,544501094,16,65537,22050,22050,524289,1635017060,k]),e],{type:"audio/wav"})))}function p(e,t,i,n){return A&&!(-4&e)&&!(-4&t)&&z[o](8*(n||A)-8+2*i+(t>>1))&1<<4*(1&t)+e}function a(){for(B=g;A&&s(h,g+1,w,1););for(L=0;F+4>L;L++)for(T=0;D>T;T++)y=L*D+T,L>=F&&(K[y]=p(T,L-F,0,V[0])?V[0]:0),R[y]=p(T-h,L-B,w)?A:p(T-h,L-g,w)?A+8:K[y]||0,(N=q["tis-"+y])&&(N=N.style,N.background="#"+(1==e&&2>b%4&&x[L]?"fff":Z[R[y]%8]),N.opacity=R[y]>7?.2:1);g=B,B=it+'0;text-align:right;font-size:150%">',q["tis-status"].innerHTML="Score"+B+W+ot+"Lines"+B+X+ot+"Level"+B+_+ot}function s(e,t,i,o){for(m=0;T=3&m,L=m>>2,16>m;m++)if(p(T,L,i)&&((T+=e)<0||T>=D||(L+=t)<0||L>=F||K[L*D+T]))return;return h=e,g=t,w=i,et=0,o||a(),1}function d(t){if(e){if(C=(t-S)/1e3||0,C>.1&&(C=.1),S=t,2==e){if(b-->4&&!(b%4)){for(T=0;D>T;)K[b*D/4+T++]=1+~~(7*I.random());a()}}else if(1==e){if(--b<0){for(L in x)for(y=L*D+D-1;y>=0;y--)K[y]=K[y-D];e=3}a()}else{for(Y in tt){if((37==Y||39==Y)&&tt[Y]>=0&&(tt[Y]-=tt[Y]?.05:.2,s(h+1-2*(37==Y),g,w)),32==Y&&!tt[Y]){for(;s(h,g+1,w););et=9}if((38==Y||18==Y||17==Y)&&(O=1-2*(17==Y),!tt[Y]))for(y=0;5>y;)if(B=(1==A?Q:P)[o]((w+4+(O-1)/2)%4*5+y++)-32,s(h+O*((7&B)-2),g+O*(2-(B>>3)),(w+4+O)%4)){f(8303);break}tt[Y]+=C}if(v+=I.max(tt[40]?.2:0,C*I.pow(1.23,_)),v>1&&(v=0,s(h,g+1,w)),et>1){A&&f(31445);for(y in K)K[y]=R[y];Y=0,x=[];e:for(L=0;F>L;L++){for(T=0;D>T;)if(!K[L*D+T++])continue e;x[L]=e=1,Y++,b=6}if(Y&&f([,8392,8260,8242,8225][Y]),W+=100*[0,1,3,5,8][Y]*_,X+=Y,_=1+~~(X/10),V.length<2)for(B=1;255!=B;){for(m=0;B&1<=0&&i.preventDefault()}function l(e){delete tt[e[n]]}var A,h,g,w,b,x,C,v,S,y,m,T,L,B,Y,N,O,k,q=window,E="createElement",M="removeEventListener",G="keyup",I=Math,U="LSOSLSOSKSNSKSNS",J=["`+,^,+Y),`.,C,^`\\Yq^.1e31H,01.,C,^`\\Yq","T$$T,+)$),Y))<$TTYTl.).1^..D,\\.,<$TTTTT`","GNKNGNKN"+U+"LSNSL@BCESOSESOSCOCOCOCOBNBN?J?J@CGL@@@@"],$=["xtvstqpsxtvs\\`}|x","tqspqqpptqspY`xx",U+U+U+U],j=[],K=[],R=[],D=10,F=22,H=D*F+20,Z="08080890dd936f9e809dd090e09c0c9f22".split(9),z=atob("8ABERAAPIiJxACYCcAQiA3QAIgZwASMCZgBmAGYAZgA2AGIEYAMxAnIAYgJwAjICYwBkAjAGMgE"),Q="203(C214A,241",nt=it+'-14pc -10pc;position:fixed;width:20pc;left:50%;top:50%;font:12px Arial;background:rgba(0,0,0,.8);box-shadow:0 0 2pc #000;border-radius:1pc">'+it+'1pc 2pc;color:#888">Tis: Tetris clone in just 4 kB of JavaScript

Left/right: move | Up/Ctrl/Alt: rotate | Esc: quit
Down/space: soft/hard drop | M: music'+ot+it+'0 1pc;float:right;color:#eee;font-size:1pc">
'+ot+"Next"+it+'8px 0;width:4pc">';for(Y=it+'0;width:1pc;height:1pc;float:left;box-shadow:-2px -2px 8px rgba(0,0,0,.4) inset,0 0 2px #000 inset" id="tis-',y=220;H>y;y++)4>y%D&&(nt+=Y+y+'">'+ot);for(nt+=ot+ot+it+'0 2pc 2pc;background:#000;width:10pc;height:20pc">',y=0;H>y;y++)K.push(0),y>19&&220>y&&(nt+=Y+y+'">'+ot);nt+=ot+ot,B=t[E]("div"),B.innerHTML=nt,t.body.appendChild(nt=B),B=new Uint8Array(881856);for(C in J)for(J[C]+=J[C]+$[C],y=0,m=0;m
+ @endif @@ -42,29 +56,29 @@
-

{{ antl.formatMessage('messages.title') }} v{{ version }}

+

{{ antl.formatMessage('messages.title') }} v{{ version }}

{{ csrfField() }} - -
-
+ +
+
- + - + - +
-
- +
+
@@ -86,15 +100,18 @@
- - + +
- + +
+
+
@@ -103,12 +120,12 @@ Video? - + - +
-
+
- -
-
+ +
+
+ @if(error)
@@ -133,7 +151,7 @@
@if(file != "") -

{{ antl.formatMessage('messages.recentFeed') }}

+

{{ antl.formatMessage('messages.recentFeed') }}

@each(file in file) @@ -152,7 +170,7 @@

{{ antl.formatMessage('messages.recentDownload') }}

-

+

@@ -185,29 +203,39 @@ @endif

{{ antl.formatMessage('messages.footer') }}

- @if(antl._locale == 'ar') + @if(antl._locale == 'ar')

{{ antl.formatMessage('messages.footer2p1') }} youtube-dl - {{ antl.formatMessage('messages.footer2p2') }} Haha yes & ExplosmRCG twitter bot - {{ antl.formatMessage('messages.footer2p3') }}: {{ viewCounter }} - {{ antl.formatMessage('messages.footer2p4') }} Supositware#1616 {{ antl.formatMessage('messages.footer2p5') }}

{{ antl.formatMessage('messages.footer3p1') }} Paypal {{ antl.formatMessage('messages.footer3p2') }} BAT {{ antl.formatMessage('messages.footer3p3') }} Brave Browser @else

{{ antl.formatMessage('messages.footer2p1') }} youtube-dl - {{ antl.formatMessage('messages.footer2p2') }} Haha yes & ExplosmRCG twitter bot - {{ antl.formatMessage('messages.footer2p3') }}: {{ viewCounter }} - {{ antl.formatMessage('messages.footer2p4') }} Supositware#1616 {{ antl.formatMessage('messages.footer2p5') }}

-

{{ antl.formatMessage('messages.footer3p1') }} Paypal {{ antl.formatMessage('messages.footer3p2') }} BAT {{ antl.formatMessage('messages.footer3p3') }} Brave Browser +

{{ antl.formatMessage('messages.footer3p1') }} Paypal {{ antl.formatMessage('messages.footer3p2') }} BAT {{ antl.formatMessage('messages.footer3p3') }} Brave Browser @endif

{{ antl.formatMessage('messages.footer4') }}

@if(month == '11') @endif + + + - \ No newline at end of file + diff --git a/server.js b/server.js index add041a..b522b1e 100644 --- a/server.js +++ b/server.js @@ -20,5 +20,6 @@ const { Ignitor } = require('@adonisjs/ignitor') new Ignitor(require('@adonisjs/fold')) .appRoot(__dirname) + .wsServer() .fireHttpServer() .catch(console.error) diff --git a/start/app.js b/start/app.js index e3f937c..2211b7a 100644 --- a/start/app.js +++ b/start/app.js @@ -21,6 +21,7 @@ const providers = [ '@adonisjs/auth/providers/AuthProvider', '@adonisjs/validator/providers/ValidatorProvider', '@adonisjs/antl/providers/AntlProvider', + '@adonisjs/websocket/providers/WsProvider', ] /* diff --git a/start/socket.js b/start/socket.js new file mode 100644 index 0000000..096dd8d --- /dev/null +++ b/start/socket.js @@ -0,0 +1,18 @@ +'use strict' + +/* +|-------------------------------------------------------------------------- +| Websocket +|-------------------------------------------------------------------------- +| +| This file is used to register websocket channels and start the Ws server. +| Learn more about same in the official documentation. +| https://adonisjs.com/docs/websocket +| +| For middleware, do check `wsKernel.js` file. +| +*/ + +const Ws = use('Ws') + +Ws.channel('progress', 'ProgressController'); diff --git a/start/wsKernel.js b/start/wsKernel.js new file mode 100644 index 0000000..5f9b77e --- /dev/null +++ b/start/wsKernel.js @@ -0,0 +1,39 @@ +'use strict' + +const Ws = use('Ws') + +/* +|-------------------------------------------------------------------------- +| Global middleware +|-------------------------------------------------------------------------- +| +| Global middleware are executed on each Websocket channel subscription. +| +*/ +const globalMiddleware = [ +] + + +/* +|-------------------------------------------------------------------------- +| Named middleware +|-------------------------------------------------------------------------- +| +| Named middleware are defined as key/value pairs. Later you can use the +| keys to run selected middleware on a given channel. +| +| // define +| { +| auth: 'Adonis/Middleware/Auth' +| } +| +| // use +| Ws.channel('chat', 'ChatController').middleware(['auth']) +*/ +const namedMiddleware = { +} + + +Ws + .registerGlobal(globalMiddleware) + .registerNamed(namedMiddleware)